  /* App UI palette. Two schemes: dark (default) and light, switched via a
     data-theme attribute on <html>. The accent is blue (was green). These
     style the DIAGRAMMER chrome — the generated website's colors are a
     separate setting in Config. */
  :root[data-theme="dark"] {
    --bg: #0a0e14;
    --panel: #111820;
    --panel-2: #161f2a;
    --line: #1f2c3a;
    --ink: #d2e0f0;
    --dim: #6b8094;
    --accent: #5ab0ff;
    --accent-dim: #2f6dab;
    --warn: #ffb84a;
    --ok: #3ecf8e;
    --wire: #ff5a8c;
    --grid: #14202c;
    --grid-dot: #2a3a4a;
    --accent-text: #06121f;
    /* GitHub-style diff palette (dark). */
    --dm-added-bg:      #0b3b1c;
    --dm-added-gutter:  #0f5323;
    --dm-added-fg:      #aff5b4;
    --dm-removed-bg:    #4d1521;
    --dm-removed-gutter:#67191f;
    --dm-removed-fg:    #ffbcbb;
    --dm-gutter-bg:     #161b22;
    --dm-gutter-fg:     #6e7681;
    --dm-empty-bg:      #0f141b;
    --dm-hunk-header-bg:#0f2233;
    --dm-hunk-header-fg:#7fa9d1;
  }
  :root, :root[data-theme="light"] {
    --bg: #f9fafc;
    --panel: #e7ecf3;
    --panel-2: #d6dee8;
    --line: #bac6d6;
    --ink: #23303d;
    --dim: #5d7186;
    --accent: #1f7fd6;
    --accent-dim: #4a93d8;
    --warn: #b06d12;
    --ok: #1f9d63;
    --wire: #cc3b72;
    --grid: #d2dae4;
    --grid-dot: #aebccd;
    --accent-text: #ffffff;
    /* GitHub-style diff palette (light). */
    --dm-added-bg:      #e6ffec;
    --dm-added-gutter:  #ccffd8;
    --dm-added-fg:      #1a7f37;
    --dm-removed-bg:    #ffebe9;
    --dm-removed-gutter:#ffd7d5;
    --dm-removed-fg:    #cf222e;
    --dm-gutter-bg:     #f6f8fa;
    --dm-gutter-fg:     #57606a;
    --dm-empty-bg:      #f6f8fa;
    --dm-hunk-header-bg:#ddf4ff;
    --dm-hunk-header-fg:#0969da;
  }
  * { box-sizing: border-box; margin: 0; padding: 0; }
  html, body { height: 100%; overflow: hidden; }
  body {
    background: var(--bg);
    color: var(--ink);
    font-family: 'JetBrains Mono', monospace;
    font-size: 12px;
    display: grid;
    grid-template-columns: 240px 1fr 6px 360px;
    grid-template-rows: 44px 1fr 24px;
    grid-template-areas:
      "header header header header"
      "palette canvas splitter inspector"
      "footer footer footer footer";
  }
  header {
    grid-area: header;
    background: var(--panel);
    border-bottom: 1px solid var(--line);
    display: flex; align-items: center; padding: 0 16px;
    gap: 20px;
    /* Positioning context for the absolutely-centered update banner. */
    position: relative;
  }
  /* "New version available" banner — absolutely centered across the whole
     header so it reads as centered regardless of the h1 / actions widths,
     and doesn't reflow the flex layout when it appears. */
  .update-banner {
    position: absolute; left: 50%; top: 50%;
    transform: translate(-50%, -50%);
    display: flex; align-items: center; gap: 10px;
    padding: 4px 6px 4px 14px;
    background: var(--panel-2);
    border: 1px solid var(--warn);
    border-radius: 4px;
    z-index: 5;
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.25);
  }
  .update-banner[hidden] { display: none; }
  /* Firmware-target indicator — absolutely centered like the update banner, at a
     lower z-index so the (transient) banner sits over it when shown. */
  .target-indicator {
    position: absolute; left: 50%; top: 50%;
    transform: translate(-50%, -50%);
    display: flex; align-items: center; gap: 7px;
    padding: 3px 12px;
    background: var(--panel-2);
    border: 1px solid var(--line); border-radius: 999px;
    color: var(--dim);
    font-size: 11px; font-weight: 700;
    letter-spacing: 0.08em; text-transform: uppercase;
    white-space: nowrap; cursor: pointer; z-index: 4;
  }
  .target-indicator:hover { border-color: var(--accent); color: var(--ink); }
  .target-indicator-icon { color: var(--accent); font-size: 12px; line-height: 1; }
  .target-indicator-text::before {
    content: "Target: "; color: var(--dim); font-weight: 600;
  }
  /* Native ESP-IDF — a distinct (warn) accent so a non-default target is obvious. */
  .target-indicator.is-idf { border-color: var(--warn); color: var(--ink); }
  .target-indicator.is-idf .target-indicator-icon { color: var(--warn); }
  .update-banner-text {
    color: var(--ink); font-size: 11px;
    text-transform: uppercase; letter-spacing: 0.06em;
    white-space: nowrap;
  }
  header h1 {
    font-family: 'Space Grotesk', sans-serif;
    font-size: 14px; font-weight: 700; letter-spacing: 0.05em;
    color: var(--accent);
  }
  .beta-badge {
    display: inline-block; margin-left: 6px; padding: 1px 5px;
    font-size: 9px; font-weight: 700; letter-spacing: 0.08em;
    text-transform: uppercase; vertical-align: middle;
    color: var(--bg); background: var(--accent); border-radius: 4px;
  }
  header .meta { color: var(--dim); font-size: 11px; }
  header .actions { margin-left: auto; display: flex; gap: 6px; }
  button.btn {
    background: var(--panel-2); color: var(--ink);
    border: 1px solid var(--line); padding: 6px 12px;
    font-family: inherit; font-size: 11px;
    cursor: pointer; text-transform: uppercase; letter-spacing: 0.08em;
    transition: all 0.12s;
  }
  button.btn:hover { border-color: var(--accent-dim); color: var(--accent); }
  /* Buttons with a leading inline-SVG icon (File ▾, Examples ▾). Flex so
     the icon + label + caret stay vertically centered and evenly gapped.
     The icon inherits currentColor, so it tracks the button's hover and
     menu-open color states automatically. */
  button.btn.btn-icon-text {
    display: inline-flex; align-items: center; gap: 6px;
  }
  .btn-icon { display: block; flex-shrink: 0; }
  button.btn.primary { background: var(--accent-dim); color: var(--accent-text); border-color: var(--accent-dim); }
  button.btn.primary:hover { background: var(--accent); color: var(--accent-text); }
  button.btn.warn:hover { border-color: var(--wire); color: var(--wire); }
  /* Generate button, disabled after a successful generate until the diagram
     changes. Reads as "done / nothing to do" rather than broken. */
  button.btn.primary:disabled,
  button.btn.primary.is-generated {
    background: transparent; color: var(--dim);
    border-color: var(--line); cursor: default;
  }
  button.btn.primary:disabled:hover,
  button.btn.primary.is-generated:hover {
    background: transparent; color: var(--dim);
  }
  /* Generic disabled state for non-primary buttons — used by Download /
     Download ZIP when the Notes tab is active. Fades and inhibits hover. */
  button.btn:disabled,
  button.btn:disabled:hover {
    color: var(--dim);
    border-color: var(--line);
    background: var(--panel-2);
    cursor: not-allowed;
    opacity: 0.55;
  }

  /* Dropdown menu in header (e.g. File ▾) */
  .menu { position: relative; display: inline-block; }
  .menu-items {
    display: none;
    position: absolute; top: calc(100% + 4px); left: 0;
    min-width: 160px;
    background: var(--panel-2); border: 1px solid var(--line);
    box-shadow: 0 6px 24px rgba(0,0,0,0.5);
    z-index: 100;
    padding: 4px;
  }
  .menu.open .menu-items { display: block; }
  .menu.open > .btn {
    border-color: var(--accent-dim); color: var(--accent);
  }
  .menu-item {
    display: block; width: 100%; text-align: left;
    background: transparent; border: none;
    color: var(--ink); font-family: inherit; font-size: 11px;
    text-transform: uppercase; letter-spacing: 0.08em;
    padding: 8px 12px; cursor: pointer;
  }
  .menu-item:hover { background: var(--panel); color: var(--accent); }
  /* Hairline divider between every adjacent pair of dropdown items. The
     first item in a menu (or one right after a .menu-divider) gets none. */
  .menu-item + .menu-item { border-top: 1px solid var(--line); }
  /* Hairline rule between groups inside a dropdown menu. */
  .menu-divider {
    height: 0; margin: 4px 8px;
    border-top: 1px solid var(--line);
  }

  /* Header glyph buttons — Help, theme toggle, notifications bell, and the
     user-menu trigger. Square, icon-only, identical sizing so they align in
     the header actions row. */
  .btn-help, .btn-scheme, .btn-bell, .btn-user {
    display: inline-flex; align-items: center; justify-content: center;
    padding: 6px 10px;
    line-height: 1;
    min-width: 34px;
  }
  .icon-help, .icon-bell, .icon-user { display: block; }
  /* They read as icons, not buttons — strip the chrome (background +
     border) in every state. Higher specificity than the base `button.btn`,
     its `:hover`, and the `.menu.open > .btn` open state so all three are
     neutralized. `color` is left untouched so each glyph still recolors to
     accent on hover / when its menu is open (via currentColor). */
  button.btn.btn-help,   button.btn.btn-help:hover,
  button.btn.btn-scheme, button.btn.btn-scheme:hover,
  button.btn.btn-bell,   button.btn.btn-bell:hover,
  button.btn.btn-user,   button.btn.btn-user:hover,
  .menu.open > button.btn.btn-bell,
  .menu.open > button.btn.btn-user {
    background: transparent;
    border-color: transparent;
  }

  /* App color-scheme toggle — shows the theme you'd switch TO: a moon in
     light mode, a sun in dark mode (sizing + frameless treatment shared in
     the glyph-button rule above). The other icon is hidden per [data-theme]. */
  .btn-scheme .icon-sun  { display: none; }   /* default (light) → show moon */
  .btn-scheme .icon-moon { display: block; }
  :root[data-theme="dark"] .btn-scheme .icon-sun  { display: block; }
  :root[data-theme="dark"] .btn-scheme .icon-moon { display: none; }
  /* User menu drops down to the right side of the trigger (it's the
     last item in the header, so the default left-anchor would clip). */
  .menu-items-user {
    left: auto; right: 0;
    min-width: 200px;
  }
  /* Identity label at the top of the user dropdown. Two-line block:
     a small "Signed in as" eyebrow above the user's name/email. Not a
     clickable button — just informational. */
  .menu-user-label {
    display: block;
    padding: 8px 12px 10px;
    cursor: default;
  }
  .menu-user-eyebrow {
    display: block;
    color: var(--dim); font-size: 9px;
    text-transform: uppercase; letter-spacing: 0.1em;
    margin-bottom: 2px;
  }
  .menu-user-name {
    display: block;
    color: var(--ink); font-size: 12px;
    font-weight: 500;
    word-break: break-word;
  }

  /* Notification bell — sits to the left of the user button (sizing +
     frameless treatment shared in the glyph-button rule above).
     `position: relative` anchors the unread dot to its top-right corner. */
  .btn-bell {
    position: relative;
  }
  /* Unread indicator — small red dot over the top-right of the bell.
     Toggled via the `hidden` attribute from app.js. */
  .notify-dot {
    position: absolute; top: 5px; right: 7px;
    width: 7px; height: 7px;
    border-radius: 50%;
    background: #ff4d4f;
    border: 1px solid var(--panel);
    box-shadow: 0 0 0 1px rgba(0,0,0,0.25);
  }
  .notify-dot[hidden] { display: none; }
  /* Notifications dropdown — right-anchored like the user menu so it
     grows leftward instead of clipping at the viewport edge. Capped
     height with its list scrolling when there are many messages.
     Stays hidden (inherits .menu-items display:none) until .menu.open;
     do NOT set `display` here or it would override that and show always. */
  .menu-items-notify {
    left: auto; right: 0;
    width: 320px; min-width: 320px;
    padding: 0;
    max-height: 70vh;
    flex-direction: column;
  }
  /* Reveal as a flex column when open. Placed after `.menu.open
     .menu-items { display:block }` so this (equal specificity) wins. */
  .menu.open .menu-items-notify { display: flex; }
  .notify-header {
    display: flex; align-items: center; justify-content: space-between; gap: 8px;
    color: var(--dim); font-size: 9px;
    text-transform: uppercase; letter-spacing: 0.1em;
    padding: 10px 12px;
    border-bottom: 1px solid var(--line);
  }
  /* Dismiss-all button in the dropdown header. Subtle text button that
     brightens to the accent on hover; hidden by app.js when empty. */
  .notify-clear {
    background: none; border: none; padding: 0; cursor: pointer;
    color: var(--dim); font: inherit; font-size: 9px;
    text-transform: uppercase; letter-spacing: 0.1em;
  }
  .notify-clear:hover { color: var(--accent); }
  .notify-clear:focus-visible { outline: 1px solid var(--accent-dim); }
  .notify-clear[hidden] { display: none; }
  .notify-list { overflow-y: auto; }
  /* Empty state — shown only when there are no messages. */
  .notify-empty {
    color: var(--dim); font-size: 11px;
    text-align: center;
    padding: 18px 12px;
  }
  .notify-empty[hidden] { display: none; }
  /* One message widget: envelope icon (left) + a text column holding the
     subject heading, body, and a right-justified muted age. Positioning
     context for the absolutely-placed dismiss (×) button. */
  .notify-item {
    position: relative;
    display: grid;
    grid-template-columns: auto 1fr;
    gap: 10px;
    padding: 10px 12px;
    border-bottom: 1px solid var(--line);
    cursor: default;
  }
  .notify-item:last-child { border-bottom: none; }
  .notify-item:hover { background: var(--panel); }
  .notify-icon {
    color: var(--accent);
    padding-top: 1px;
  }
  .notify-icon svg { display: block; }
  .notify-text { min-width: 0; }   /* lets long words wrap instead of overflowing */
  .notify-subject {
    color: var(--ink); font-size: 12px; font-weight: 600;
    margin-bottom: 3px;
    padding-right: 16px;           /* clear the × in the top-right corner */
    word-break: break-word;
  }
  .notify-body {
    color: var(--ink); font-size: 11.5px; line-height: 1.45;
    word-break: break-word;
  }
  .notify-age {
    color: var(--dim); font-size: 10px;
    text-align: right;
    margin-top: 4px;
  }
  /* Per-user dismiss (×) — sits in the item's top-right corner. Muted by
     default, brightens on hover. Click is handled in app.js (stops
     propagation so the dropdown stays open). */
  .notify-delete {
    position: absolute; top: 6px; right: 8px;
    display: inline-flex; align-items: center; justify-content: center;
    width: 18px; height: 18px;
    padding: 0; border: none; border-radius: 3px;
    background: transparent; color: var(--dim);
    cursor: pointer; line-height: 0;
  }
  .notify-delete:hover { background: var(--line); color: var(--ink); }
  .notify-delete:focus-visible { outline: 1px solid var(--accent-dim); }
  .notify-delete svg { display: block; }

  aside.palette {
    grid-area: palette;
    background: var(--panel);
    border-right: 1px solid var(--line);
    overflow-y: auto;
    padding: 12px 0;
  }
  .palette-section { margin-bottom: 16px; }
  /* Collapsible <details> section. The <summary> is the clickable header; the
     native disclosure marker is replaced with a chevron that rotates on open
     so a long section (e.g. Page Modules) can be rolled up. */
  summary.palette-title {
    padding: 8px 16px; color: var(--dim);
    font-size: 10px; text-transform: uppercase;
    letter-spacing: 0.15em;
    border-bottom: 1px solid var(--line); margin-bottom: 6px;
    cursor: pointer; user-select: none;
    display: flex; align-items: center; gap: 7px;
    list-style: none;
  }
  summary.palette-title::-webkit-details-marker { display: none; }
  summary.palette-title::before {
    content: "\25B8"; /* ▸ */ font-size: 9px; color: var(--dim);
    transition: transform 0.12s ease;
  }
  details.palette-section[open] > summary.palette-title::before { transform: rotate(90deg); }
  summary.palette-title:hover { color: var(--ink); }
  .palette-item {
    margin: 4px 10px; padding: 8px 10px;
    background: var(--panel-2); border: 1px solid var(--line);
    cursor: grab; user-select: none;
    display: flex; align-items: center; gap: 8px;
    transition: all 0.1s;
  }
  .palette-item:hover { border-color: var(--accent); background: #1a2420; }
  .palette-item:active { cursor: grabbing; }
  .palette-item .icon { font-size: 14px; }
  .palette-item .name { font-weight: 500; }
  .palette-item .hint { display: block; color: var(--dim); font-size: 10px; margin-top: 2px; }
  /* Disabled palette items (e.g. WebSocket when the HTTP server is the
     built-in WebServer): visually faded, not draggable, and the cursor
     signals the click is a no-op. The reason lives on the element's
     title attribute so a hover surfaces it as a tooltip. */
  .palette-item.disabled {
    cursor: not-allowed;
    opacity: 0.4;
    background: var(--panel-2);
  }
  .palette-item.disabled:hover {
    border-color: var(--line); background: var(--panel-2);
  }

  main {
    grid-area: canvas;
    position: relative;
    min-width: 0;
    overflow: hidden;
    /* Dot grid — radial-gradient draws one dot per cell. 20px spacing with
       a 1.4px dot reads clearly without being noisy. Snap-to-grid is 10px,
       so nodes can sit between dots for finer placement. */
    background-color: var(--bg);
    background-image: radial-gradient(circle, var(--grid-dot) 1.4px, transparent 1.6px);
    background-size: 20px 20px;
    background-position: 0 0;
  }
  #canvas { width: 100%; height: 100%; overflow: auto; position: relative; }

  .canvas-toolbar {
    position: absolute; top: 10px; right: 15px;
    z-index: 5; display: flex; gap: 6px;
    flex-wrap: wrap; justify-content: flex-end;
  }
  /* Full-width row so the fullscreen icon wraps under the button row and
     right-aligns beneath Feedback. */
  .canvas-fullscreen-row {
    flex-basis: 100%; display: flex; justify-content: flex-end;
  }
  /* Square icon-only canvas button (fullscreen toggle). */
  .canvas-btn-icon {
    display: inline-flex; align-items: center; justify-content: center;
    padding: 5px; line-height: 0;
  }
  /* Left-anchored twin of .canvas-toolbar — holds the ESP32 Connect button
     opposite the right-side actions. */
  .canvas-toolbar-left { right: auto; left: 15px; }
  .canvas-btn {
    position: relative;
    background: var(--panel-2); color: var(--dim);
    border: 1px solid var(--line); padding: 5px 12px;
    font-family: inherit; font-size: 11px;
    text-transform: uppercase; letter-spacing: 0.08em;
    cursor: pointer; transition: all 0.12s;
    /* Slightly faded so they recede against the canvas; hover restores
       full opacity to confirm intent before a destructive action. */
    opacity: 0.8;
  }
  /* Round credit-count badge pinned to a button's upper-right corner
     (Describe / Generate Code / Compile). Shows the credits currently
     available for that action; hidden entirely on unlimited plans.
     Positioned relative to the button itself (position: relative above),
     so no extra wrapper markup is needed. */
  .credit-badge {
    position: absolute; top: -7px; right: -7px;
    min-width: 16px; height: 16px; padding: 0 3px;
    display: flex; align-items: center; justify-content: center;
    border-radius: 999px;
    background: var(--accent); color: var(--bg, #fff);
    border: 1px solid var(--panel);
    font-family: 'JetBrains Mono', monospace;
    font-size: 9px; font-weight: 700; letter-spacing: 0;
    text-transform: none;
    font-variant-numeric: tabular-nums;
    line-height: 1;
    pointer-events: none;
  }
  .credit-badge[data-state="low"] { background: #d9a441; }
  .credit-badge[data-state="out"] { background: #d9534f; }
  .canvas-btn:hover {
    color: var(--wire); border-color: var(--wire);
    background: var(--panel);
    opacity: 1;
  }
  /* Feedback isn't destructive — hover to the accent, not the warn color. */
  .canvas-btn-feedback:hover {
    color: var(--accent); border-color: var(--accent-dim);
  }
  /* Generate stands out from the other canvas buttons with blue (accent)
     text — it's the primary action. The dimmed .is-generated/:disabled
     state below still overrides this once code is current. */
  .canvas-btn-generate { color: color-mix(in srgb, var(--accent) 60%, black); }
  /* Generate / File / Config / Examples aren't destructive — accent hover
     like Feedback (not the wire-red Clear uses). */
  .canvas-btn-generate:hover,
  .canvas-btn-nav:hover {
    color: var(--accent); border-color: var(--accent-dim);
  }
  /* Open-state recolor for the File/Examples dropdown triggers, mirroring
     the old `.menu.open > .btn` treatment now that they're canvas buttons. */
  .menu.open > .canvas-btn {
    color: var(--accent); border-color: var(--accent-dim); opacity: 1;
  }
  /* After a successful generate the button is disabled + .is-generated until
     the diagram changes. Read it as "done / nothing to do": dimmed, no hover
     lift, default cursor. Mirrors the old primary-button behavior. */
  .canvas-btn-generate:disabled,
  .canvas-btn-generate.is-generated,
  .canvas-btn-generate:disabled:hover,
  .canvas-btn-generate.is-generated:hover {
    color: var(--dim); border-color: var(--line);
    background: var(--panel-2); opacity: 0.55;
    cursor: default;
  }
  /* Regenerate nudge: while the shown code is out of date (.needs-regen, set by
     setDiagramDirty() and cleared by markCodeGenerated()), an arrow marches on
     the Generate button to pull the eye back to it. The arrow takes no space
     when the code is current, so it can't leave a permanent gap. */
  .canvas-btn-generate .regen-arrow { display: none; }
  .canvas-btn-generate.needs-regen .regen-arrow {
    display: inline-block;
    margin-right: 0.4em;
    color: var(--accent);
    font-weight: 700;
    animation: regen-nudge 1s ease-in-out infinite;
  }
  @keyframes regen-nudge {
    0%, 100% { transform: translateX(-2px); opacity: 0.55; }
    50%      { transform: translateX(4px);  opacity: 1; }
  }
  /* Respect reduced-motion: keep the arrow as a static cue, just don't march. */
  @media (prefers-reduced-motion: reduce) {
    .canvas-btn-generate.needs-regen .regen-arrow { animation: none; opacity: 1; }
  }
  /* AI "Describe" button — the marquee entry point, accent-tinted so it
     reads as the primary way in (next to Generate). */
  .canvas-btn-ai {
    color: var(--accent); border-color: var(--accent-dim); opacity: 1;
  }
  .canvas-btn-ai:hover {
    color: var(--accent); border-color: var(--accent);
    background: color-mix(in srgb, var(--accent) 12%, var(--panel));
  }
  /* AI diagram modal — clickable example-prompt chips above the textarea. */
  .ai-examples {
    display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 10px;
  }
  .ai-example-chip {
    background: var(--panel-2); color: var(--dim);
    border: 1px solid var(--line); border-radius: 12px;
    padding: 4px 10px; font-family: inherit; font-size: 11px;
    cursor: pointer; transition: all 0.12s; text-align: left; line-height: 1.3;
  }
  .ai-example-chip:hover {
    color: var(--accent); border-color: var(--accent-dim); background: var(--panel);
  }
  .ai-example-chip:disabled { opacity: 0.4; cursor: default; }
  /* AI "thinking" indicator — a radar/sonar ping (on-theme for a WiFi device)
     plus a cycling build-phase caption, shown while the model request is in
     flight. Hidden via the `hidden` attribute when idle. */
  .ai-thinking {
    display: flex; align-items: center; gap: 14px;
    margin: 4px 0 12px; padding: 12px 14px;
    background: color-mix(in srgb, var(--accent) 7%, var(--panel));
    border: 1px solid var(--accent-dim); border-radius: 4px;
  }
  .ai-thinking[hidden] { display: none; }
  .ai-radar { position: relative; width: 40px; height: 40px; flex: 0 0 40px; }
  .ai-radar-ring, .ai-radar-core {
    position: absolute; top: 50%; left: 50%; border-radius: 50%;
  }
  .ai-radar-ring {
    width: 100%; height: 100%; border: 2px solid var(--accent);
    transform: translate(-50%, -50%) scale(0.2); opacity: 0;
    animation: ai-ping 1.8s cubic-bezier(0, 0.2, 0.8, 1) infinite;
  }
  .ai-radar-ring:nth-child(2) { animation-delay: 0.6s; }
  .ai-radar-ring:nth-child(3) { animation-delay: 1.2s; }
  .ai-radar-core {
    width: 8px; height: 8px; background: var(--accent);
    box-shadow: 0 0 8px var(--accent);
    animation: ai-core 1.2s ease-in-out infinite;
  }
  @keyframes ai-ping {
    0%   { transform: translate(-50%, -50%) scale(0.2); opacity: 0.85; }
    100% { transform: translate(-50%, -50%) scale(1);   opacity: 0; }
  }
  @keyframes ai-core {
    0%, 100% { transform: translate(-50%, -50%) scale(1);   opacity: 1;   }
    50%      { transform: translate(-50%, -50%) scale(1.5); opacity: 0.5; }
  }
  .ai-thinking-text {
    font-size: 12px; letter-spacing: 0.06em; text-transform: uppercase;
    color: var(--accent);
  }
  .ai-thinking-dots::after {
    content: ""; display: inline-block; width: 1.2em; text-align: left;
    animation: ai-dots 1.4s steps(4, end) infinite;
  }
  @keyframes ai-dots {
    0% { content: ""; } 25% { content: "."; } 50% { content: ".."; }
    75% { content: "..."; } 100% { content: ""; }
  }
  @media (prefers-reduced-motion: reduce) {
    .ai-radar-ring, .ai-radar-core, .ai-thinking-dots::after { animation: none; }
    .ai-radar-ring:first-child { opacity: 0.4; transform: translate(-50%, -50%) scale(0.8); }
  }

  /* Connect button — accent text like Generate (it's a primary, non-
     destructive action). When connected it carries .is-connected and reads
     "Disconnect": flip to the warn color so the toggle-off intent is clear. */
  .canvas-btn-connect { color: color-mix(in srgb, var(--accent) 60%, black); }
  .canvas-btn-connect:hover {
    color: var(--accent); border-color: var(--accent-dim);
  }
  .canvas-btn-connect.is-connected { color: var(--wire); }
  .canvas-btn-connect.is-connected:hover {
    color: var(--wire); border-color: var(--wire);
  }
  /* Busy state while the port picker / handshake is in flight. */
  .canvas-btn-connect.is-busy { opacity: 0.55; cursor: default; }

  /* Connected-chip label in the header, right after the logo. Monospace so
     the chip ID reads as an identifier; a small dot marks the live link. */
  .chip-id-label {
    /* Absolutely positioned so its left edge lands on the canvas's left
       edge (x = 240px, the fixed palette column width) regardless of the
       logo width — a clean vertical line down the left of the canvas.
       Vertically centered in the header like the update banner. */
    position: absolute; left: 240px; top: 50%; transform: translateY(-50%);
    display: inline-flex; align-items: center; gap: 6px;
    font-family: 'JetBrains Mono', ui-monospace, monospace;
    font-size: 11px; letter-spacing: 0.04em;
    color: var(--ink);
    padding: 2px 8px;
    border: 1px solid var(--line); border-radius: 3px;
    background: var(--panel-2);
  }
  .chip-id-label[hidden] { display: none; }
  .chip-id-label::before {
    content: ""; width: 6px; height: 6px; border-radius: 50%;
    background: var(--accent); flex: none;
  }

  /* ESP32 connect modal — chip-info table + troubleshooting, on the shared
     .feedback-modal shell. The two bodies are toggled via [hidden]; an
     explicit rule is needed because .feedback-body's display:flex would
     otherwise win over the UA [hidden] (display:none) style. */
  .connect-success[hidden], .connect-error[hidden] { display: none; }
  .connect-info { width: 100%; border-collapse: collapse; font-size: 12px; }
  .connect-info td {
    padding: 7px 0; border-bottom: 1px solid var(--line); vertical-align: top;
  }
  .connect-info tr:last-child td { border-bottom: none; }
  .connect-info td:first-child {
    color: var(--dim); width: 130px; white-space: nowrap;
    text-transform: uppercase; letter-spacing: 0.05em; font-size: 11px;
  }
  .connect-val {
    font-family: 'JetBrains Mono', ui-monospace, monospace; color: var(--ink);
  }
  .connect-chips { display: flex; flex-wrap: wrap; gap: 5px; }
  .connect-chips .connect-chip {
    font-family: 'JetBrains Mono', ui-monospace, monospace; font-size: 11px;
    color: var(--dim); background: var(--panel-2);
    border: 1px solid var(--line); border-radius: 2px; padding: 2px 7px;
  }
  .connect-error-msg { color: var(--wire); font-size: 13px; margin: 0; }
  .connect-troubleshoot {
    background: var(--panel-2); border: 1px solid var(--line);
    padding: 12px 16px;
  }
  .connect-troubleshoot-title {
    margin: 0 0 8px; font-size: 11px; color: var(--dim);
    text-transform: uppercase; letter-spacing: 0.06em;
  }
  .connect-troubleshoot ul {
    margin: 0; padding-left: 18px; color: var(--ink);
    font-size: 12px; line-height: 1.6;
  }
  .connect-troubleshoot li { margin-bottom: 4px; }
  .connect-troubleshoot li:last-child { margin-bottom: 0; }

  /* Compile button — accent like Generate (non-destructive primary-ish). */
  .canvas-btn-compile { color: color-mix(in srgb, var(--accent) 60%, black); }
  .canvas-btn-compile:hover { color: var(--accent); border-color: var(--accent-dim); }
  .canvas-btn-compile.is-busy { opacity: 0.55; cursor: default; }
  /* Disabled until there's generated code to compile — dimmed, no hover lift. */
  .canvas-btn-compile:disabled,
  .canvas-btn-compile:disabled:hover {
    color: var(--dim); border-color: var(--line);
    background: var(--panel-2); opacity: 0.55; cursor: default;
  }

  /* Server compile modal. Wider than the default feedback shell to fit the
     build log; the log itself scrolls. */
  /* Higher specificity than the base .feedback-modal (max-width: 520px), which
     is declared later in the file and would otherwise win at equal specificity. */
  .feedback-modal.build-modal { max-width: 760px; }

  /* Wizard step indicator (Compile · Connect · Flash) in the build modal. One
     overlay, swappable panels — so the connect/flash steps can never collide
     with the compile step the way two separate overlays did. */
  .wizard-steps {
    display: flex; align-items: center; gap: 8px;
    list-style: none; margin: 0;
    /* Match the header/body 24px horizontal inset so the steps line up with
       the modal title and panel content; 16px below the header and ~12px of
       breathing room beneath the steps (on top of the panel's 20px top pad). */
    padding: 16px 24px 12px;
    font-size: 12px; color: var(--dim);
  }
  .wizard-step {
    display: flex; align-items: center; gap: 6px;
    flex: 1; min-width: 0;
  }
  /* Last step has no trailing connector, so it shrinks to its label. */
  .wizard-step:last-child { flex: 0 0 auto; }
  /* Connector line — trails each non-last step's label and grows to fill the
     gap up to the next step's number, so it spans the full width. */
  .wizard-step:not(:last-child)::after {
    content: ""; height: 1px; flex: 1 1 auto; min-width: 16px;
    background: var(--line); margin-left: 8px;
  }
  .wizard-step-num {
    display: inline-flex; align-items: center; justify-content: center;
    width: 20px; height: 20px; flex: none; border-radius: 50%;
    border: 1px solid var(--line); background: var(--panel-2);
    font-size: 11px; font-weight: 600; color: var(--dim);
  }
  .wizard-step.is-active { color: var(--ink); }
  .wizard-step.is-active .wizard-step-num {
    border-color: var(--accent); color: var(--accent);
  }
  .wizard-step.is-done .wizard-step-num {
    border-color: var(--accent); background: var(--accent); color: var(--bg);
  }
  /* Panels share the overlay; only the active one is shown. */
  .wizard-panel[hidden] { display: none; }

  /* Compile target-board picker — a searchable combo over the board catalog.
     Extra bottom margin gives the selector a little breathing room from what
     follows (the Compile button row, or the status line once a build starts). */
  .build-board {
    position: relative; display: flex; flex-direction: column; gap: 5px;
    margin-bottom: 8px;
  }
  .build-board-label {
    font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em;
    color: var(--dim);
  }
  .build-board-search {
    width: 100%; box-sizing: border-box;
    background: var(--panel-2); color: var(--ink);
    border: 1px solid var(--line); border-radius: 2px;
    padding: 8px 10px; font-family: inherit; font-size: 12px;
  }
  .build-board-search:focus { outline: none; border-color: var(--accent-dim); }
  .build-board-search:disabled { opacity: 0.6; cursor: not-allowed; }
  /* Results dropdown — absolutely positioned under the input so it overlays the
     content below instead of pushing it down. */
  .build-board-list {
    position: absolute; top: 100%; left: 0; right: 0; z-index: 5;
    margin: 2px 0 0; padding: 4px; list-style: none;
    max-height: 240px; overflow-y: auto;
    background: var(--panel-2); border: 1px solid var(--line);
    box-shadow: 0 12px 32px rgba(0, 0, 0, 0.4);
  }
  .build-board-list[hidden] { display: none; }
  .build-board-opt {
    padding: 6px 8px; cursor: pointer; font-size: 12px; color: var(--ink);
    display: flex; flex-direction: column; gap: 1px;
  }
  .build-board-opt .opt-fqbn {
    font-family: 'JetBrains Mono', ui-monospace, monospace;
    font-size: 10px; color: var(--dim);
  }
  .build-board-opt:hover,
  .build-board-opt.is-active { background: var(--line); }
  .build-board-empty { padding: 6px 8px; font-size: 12px; color: var(--dim); }
  /* Selected FQBN caption under the input. */
  .build-board-fqbn {
    font-family: 'JetBrains Mono', ui-monospace, monospace;
    font-size: 10px; color: var(--accent);
  }

  /* Board options (Flash Mode / PSRAM) — Arduino-IDE "Tools"-menu style. A row of
     labelled selects shown only for chips that expose them; hidden otherwise. */
  .build-board-opts {
    display: flex; flex-wrap: wrap; gap: 8px 14px; margin-bottom: 8px;
  }
  .build-board-opts[hidden] { display: none; }
  .build-opt {
    display: flex; flex-direction: column; gap: 4px; flex: 1 1 140px; min-width: 0;
  }
  .build-opt-label {
    font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em;
    color: var(--dim);
  }
  .build-opt-select {
    width: 100%; box-sizing: border-box;
    background: var(--panel-2); color: var(--ink);
    border: 1px solid var(--line); border-radius: 2px;
    padding: 7px 8px; font-family: inherit; font-size: 12px;
  }
  .build-opt-select:focus { outline: none; border-color: var(--accent-dim); }
  .build-opt-select:disabled { opacity: 0.6; cursor: not-allowed; }

  /* Chip-aware pin conflict warnings under the board options. Amber for caution,
     red when a pin will actually crash the selected board. */
  .build-pin-warn {
    margin-bottom: 8px; padding: 9px 11px; border-radius: 3px;
    border: 1px solid var(--warn, #d29922);
    background: color-mix(in srgb, var(--warn, #d29922) 12%, transparent);
    font-size: 12px; color: var(--ink);
  }
  .build-pin-warn[hidden] { display: none; }
  .build-pin-warn.has-danger {
    border-color: var(--wire);
    background: color-mix(in srgb, var(--wire) 12%, transparent);
  }
  .build-pin-warn-head { font-weight: 600; margin-bottom: 5px; }
  .build-pin-warn-list { list-style: none; margin: 0; padding: 0; }
  .build-pin-item { padding: 2px 0; color: var(--dim); }
  .build-pin-item.danger { color: var(--ink); }
  .build-pin-item strong { color: var(--ink); }
  .build-pin-warn-foot { margin-top: 5px; font-size: 11px; color: var(--dim); }

  /* --- Pinout tab ---------------------------------------------------------
     Chip-family pin map (two columns of pins flanking a central die), with the
     diagram's hardware assignments overlaid and flash/PSRAM pins marked. */
  #tab-pinout { overflow: auto; }
  .pinout-toolbar {
    display: flex; align-items: center; gap: 8px;
    padding: 8px 10px; border-bottom: 1px solid var(--line);
  }
  .pinout-chip-label {
    font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--dim);
  }
  #pinout-chip {
    background: var(--panel-2); color: var(--ink); border: 1px solid var(--line);
    border-radius: 2px; padding: 5px 8px; font-family: inherit; font-size: 12px;
  }
  #pinout-refresh { padding: 4px 8px; }
  .pinout-detected { font-size: 11px; color: var(--dim); margin-left: auto; }
  .pinout-body { padding: 10px; }
  .pinout-summary { font-size: 12px; color: var(--ink); margin-bottom: 6px; }
  .pinout-conflict { color: var(--wire); font-weight: 600; }
  .pinout-warn {
    margin-bottom: 10px; padding: 9px 11px; border-radius: 3px;
    border: 1px solid var(--wire); background: color-mix(in srgb, var(--wire) 12%, transparent);
    font-size: 12px; color: var(--ink);
  }
  .pinout-warn-head { font-weight: 600; margin-bottom: 5px; }
  .pinout-warn-list { list-style: none; margin: 0; padding: 0; }
  .pinout-warn-item { padding: 2px 0; }
  .pinout-warn-item strong { color: var(--ink); }
  .pinout-legend {
    display: flex; flex-wrap: wrap; gap: 10px; font-size: 11px; color: var(--dim);
    margin-bottom: 12px;
  }
  .pinout-legend .lg { display: inline-flex; align-items: center; gap: 4px; }
  .pinout-legend .sw { width: 10px; height: 10px; border-radius: 2px; display: inline-block; background: var(--line); }
  .pinout-board { display: flex; align-items: stretch; gap: 6px; justify-content: center; }
  .pinout-side { display: flex; flex-direction: column; gap: 3px; flex: 1; min-width: 0; }
  .pinout-die {
    width: 34px; border-radius: 4px; background: var(--panel-2); border: 1px solid var(--line);
    display: flex; align-items: center; justify-content: center;
  }
  .pinout-die-name {
    writing-mode: vertical-rl; transform: rotate(180deg);
    font-size: 11px; letter-spacing: 0.1em; color: var(--dim);
    text-transform: uppercase; white-space: nowrap;
  }
  .pin-row { display: flex; align-items: center; gap: 5px; font-size: 11px; min-width: 0; }
  .pin-row.left { justify-content: flex-end; text-align: right; }
  .pin-row.right { justify-content: flex-start; text-align: left; }
  .pin-pad {
    flex: none; min-width: 22px; height: 18px; padding: 0 4px; border-radius: 3px;
    display: inline-flex; align-items: center; justify-content: center;
    font-variant-numeric: tabular-nums; font-size: 10px; font-weight: 600;
    background: var(--line); color: var(--ink); border: 1px solid transparent;
  }
  .pin-info { min-width: 0; display: flex; flex-direction: column; line-height: 1.15; overflow: hidden; }
  .pin-row.left .pin-info { align-items: flex-end; }
  .pin-label { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
  .pin-tags { display: flex; gap: 3px; flex-wrap: wrap; }
  .pin-row.left .pin-tags { justify-content: flex-end; }
  .pin-tag { font-size: 9px; padding: 0 3px; border-radius: 2px; background: var(--line); color: var(--dim); }
  .pin-row.role-free .pin-label { color: var(--dim); }
  .pin-row.role-assigned .pin-pad,
  .pinout-legend .sw.role-assigned { background: var(--accent); color: #001018; border-color: var(--accent); }
  .pin-row.role-assigned .pin-label { color: var(--ink); font-weight: 600; }
  .pin-row.role-reserved .pin-pad,
  .pinout-legend .sw.role-reserved { background: color-mix(in srgb, var(--wire) 30%, var(--panel-2)); color: var(--ink); }
  .pin-row.role-reserved .pin-label { color: var(--dim); font-style: italic; }
  .pin-row.role-conflict .pin-pad,
  .pinout-legend .sw.role-conflict { background: var(--wire); color: #fff; border-color: var(--wire); }
  .pin-row.role-conflict .pin-label { color: var(--wire); font-weight: 600; }
  .pinout-empty { color: var(--dim); font-size: 12px; padding: 20px; text-align: center; }

  /* --- Auto-detect "board detected" indicator --------------------------
     A green badge that pops in at the top-right of the board picker when
     Web Serial identifies the connected chip: an SVG checkmark draws itself
     in, the label slides in behind it, and the input + FQBN pulse green. */
  .build-board-detected {
    position: absolute; top: 0; right: 0;
    display: inline-flex; align-items: center; gap: 5px;
    font-size: 11px; font-weight: 600; letter-spacing: 0.02em;
    color: var(--ok);
  }
  .build-board-detected[hidden] { display: none; }
  .build-board-detected .detected-text {
    opacity: 0; transform: translateX(-4px);
    animation: detected-text-in 0.35s ease-out 0.24s forwards;
  }
  .detected-check { flex: none; overflow: visible; }
  .detected-check-ring {
    fill: none; stroke: var(--ok); stroke-width: 2;
    transform-box: fill-box; transform-origin: center;
    animation: detected-ring-pop 0.4s cubic-bezier(0.2, 0.9, 0.3, 1.4) both;
  }
  .detected-check-mark {
    fill: none; stroke: var(--ok); stroke-width: 2.6;
    stroke-linecap: round; stroke-linejoin: round;
    stroke-dasharray: 22; stroke-dashoffset: 22;
    animation: detected-mark-draw 0.3s ease-out 0.24s forwards;
  }
  @keyframes detected-ring-pop {
    0%   { transform: scale(0);    opacity: 0; }
    60%  { transform: scale(1.12); opacity: 1; }
    100% { transform: scale(1);    opacity: 1; }
  }
  @keyframes detected-mark-draw { to { stroke-dashoffset: 0; } }
  @keyframes detected-text-in { to { opacity: 1; transform: translateX(0); } }

  /* Green glow pulse on the input + FQBN flash while the badge is showing. */
  .build-board.is-detected .build-board-search {
    border-color: var(--ok);
    animation: detected-glow 1.1s ease-out;
  }
  .build-board.is-detected .build-board-fqbn {
    animation: detected-fqbn-flash 1.2s ease-out;
  }
  @keyframes detected-glow {
    0%   { box-shadow: 0 0 0 0 color-mix(in srgb, var(--ok) 55%, transparent); }
    100% { box-shadow: 0 0 0 9px color-mix(in srgb, var(--ok) 0%, transparent); }
  }
  @keyframes detected-fqbn-flash {
    0%, 35% { color: var(--ok); }
    100%    { color: var(--accent); }
  }

  /* Transient success state on the Auto Detect button itself. */
  button.btn.build-autodetect.is-ok {
    border-color: var(--ok); color: var(--ok);
  }
  button.btn.build-autodetect.is-ok:hover {
    border-color: var(--ok); color: var(--ok);
  }
  @media (prefers-reduced-motion: reduce) {
    .detected-check-ring, .detected-check-mark,
    .build-board-detected .detected-text,
    .build-board.is-detected .build-board-search,
    .build-board.is-detected .build-board-fqbn { animation: none; }
    .detected-check-mark { stroke-dashoffset: 0; }
    .build-board-detected .detected-text { opacity: 1; transform: none; }
  }

  .build-statusline {
    display: flex; align-items: center; gap: 10px;
    font-size: 12px; color: var(--ink);
  }
  /* Explicit override: the display:flex above otherwise beats the [hidden]
     attribute's UA display:none, so the status line wouldn't hide in the
     pre-compile "ready" state. */
  .build-statusline[hidden] { display: none; }
  .build-phase { font-weight: 500; }
  /* Live elapsed timer — tabular figures so the width doesn't jitter as it
     ticks. Hidden until a build starts. */
  .build-timer {
    font-family: 'JetBrains Mono', ui-monospace, monospace;
    font-size: 11px; color: var(--accent);
    font-variant-numeric: tabular-nums;
  }
  .build-timer[hidden] { display: none; }
  .build-fqbn {
    margin-left: auto; font-family: 'JetBrains Mono', ui-monospace, monospace;
    font-size: 11px; color: var(--dim);
  }
  /* Spinner — shown while a build is running, hidden via [hidden] otherwise. */
  .build-spinner {
    width: 13px; height: 13px; flex: none; border-radius: 50%;
    border: 2px solid var(--line); border-top-color: var(--accent);
    animation: build-spin 0.7s linear infinite;
  }
  .build-spinner[hidden] { display: none; }
  @keyframes build-spin { to { transform: rotate(360deg); } }
  /* Wraps the log so the Copy button can be superimposed over its corner. */
  .build-log-wrap { position: relative; }
  /* Copy-log button floated over the top-right of the log (keeps it out of the
     crowded action row). Sits above the scrolling log content and stays put as
     the log scrolls. Semi-transparent panel fill so it reads over the text. */
  .build-log-copy {
    /* right inset clears the log's vertical scrollbar (~14px) so the button
       doesn't sit flush against it. Filled with the accent colour (not a faint
       panel tint) so it stands out against the dark log instead of vanishing. */
    position: absolute; top: 8px; right: 20px; z-index: 2;
    padding: 3px 9px; font-size: 10px;
    background: var(--accent-dim); color: var(--accent-text);
    border-color: var(--accent-dim);
    opacity: 1; transition: background 0.12s ease, border-color 0.12s ease;
  }
  .build-log-wrap:hover .build-log-copy,
  .build-log-copy:hover,
  .build-log-copy:focus-visible { background: var(--accent); border-color: var(--accent); color: var(--accent-text); }
  .build-log-copy[hidden] { display: none; }
  .build-log {
    margin: 0; height: 300px; overflow: auto;
    background: var(--panel-2); border: 1px solid var(--line);
    padding: 10px 12px; font-family: 'JetBrains Mono', ui-monospace, monospace;
    font-size: 11px; line-height: 1.5; color: var(--ink);
    /* Don't wrap — long compiler lines scroll horizontally (overflow: auto
       above gives both scrollbars) so output stays aligned and readable. */
    white-space: pre;
  }
  /* stderr lines get a muted-warn tint so warnings/errors stand out. */
  .build-log .log-err { color: color-mix(in srgb, var(--warn) 80%, var(--ink)); }
  /* Verbose-log toggle in the build modal footer. */
  .build-verbose {
    display: flex; align-items: center; gap: 5px;
    color: var(--dim); font-size: 11px;
    text-transform: uppercase; letter-spacing: 0.06em;
    cursor: pointer; user-select: none; white-space: nowrap;
  }
  .build-verbose input { cursor: pointer; margin: 0; accent-color: var(--accent); }
  .build-verbose:hover { color: var(--ink); }
  .build-result { flex: 1; font-size: 11px; }
  .build-result.ok { color: var(--accent); }
  .build-result.err { color: var(--wire); }
  /* Flash-to-device — the headline action once a build succeeds. Accent fill
     to read as primary; dimmed + un-clickable when no board is connected.
     Selector mirrors `button.btn.primary` so it beats the base button.btn. */
  button.btn.build-flash {
    background: var(--accent); border-color: var(--accent);
    color: var(--accent-text);
  }
  button.btn.build-flash:hover:not(:disabled) { background: var(--accent-dim); border-color: var(--accent-dim); }
  button.btn.build-flash:disabled { opacity: 0.45; cursor: not-allowed; }

  /* Destructive confirm button (e.g. Clear Diagram) — red fill so the
     irreversible action reads as dangerous. Selector matches button.btn.* so
     it beats the base button.btn. */
  button.btn.danger {
    background: var(--wire); border-color: var(--wire); color: #fff;
  }
  button.btn.danger:hover {
    background: color-mix(in srgb, var(--wire) 80%, black);
    border-color: color-mix(in srgb, var(--wire) 80%, black);
  }
  /* Preserve blank-line breaks in multi-line confirm messages. */
  #confirm-message { white-space: pre-line; }
  /* The generic confirm can open ON TOP of another modal (e.g. Delete from the
     Load-diagram list), so it stacks above the base .help-overlay (z 1000). */
  #confirm-overlay { z-index: 1100; }

  /* Feedback modal — reuses .help-overlay for the backdrop. */
  .feedback-modal {
    background: var(--panel); border: 1px solid var(--line);
    width: 100%; max-width: 520px;
    display: flex; flex-direction: column;
    box-shadow: 0 20px 80px rgba(0, 0, 0, 0.45);
  }
  .feedback-modal .help-header h2::before { content: none; }
  .feedback-body {
    padding: 20px 24px; display: flex; flex-direction: column; gap: 14px;
  }
  .feedback-intro { color: var(--dim); font-size: 12px; line-height: 1.5; }
  .feedback-text {
    width: 100%; min-height: 140px; resize: vertical;
    background: var(--panel-2); color: var(--ink);
    border: 1px solid var(--line); border-radius: 2px;
    padding: 10px; font-family: inherit; font-size: 12px; line-height: 1.5;
  }
  .feedback-text:focus { outline: none; border-color: var(--accent-dim); }
  /* "Include Diagram" checkbox below the textarea — opt-in attach so the
     user explicitly decides whether their work-in-progress travels with
     a feedback report. */
  .feedback-include-diagram {
    display: flex; align-items: center; gap: 6px;
    color: var(--dim); font-size: 11px;
    text-transform: uppercase; letter-spacing: 0.06em;
    cursor: pointer; user-select: none;
  }
  .feedback-include-diagram input {
    cursor: pointer; margin: 0; accent-color: var(--accent);
  }
  .feedback-include-diagram:hover { color: var(--ink); }
  .feedback-actions { display: flex; align-items: center; gap: 8px; }
  .feedback-status { flex: 1; font-size: 11px; color: var(--dim); }

  /* Save-diagram modal — reuses .help-overlay backdrop + .feedback-modal shell. */
  .save-field { display: flex; flex-direction: column; gap: 6px; }
  .save-label {
    font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em;
    color: var(--dim);
  }
  .save-req { color: var(--accent); }
  .save-opt { opacity: 0.6; }
  .save-input, .save-textarea {
    width: 100%; background: var(--panel-2); color: var(--ink);
    border: 1px solid var(--line); border-radius: 2px;
    padding: 8px 10px; font-family: inherit; font-size: 12px; line-height: 1.5;
  }
  .save-textarea { resize: vertical; min-height: 64px; }
  .save-input:focus, .save-textarea:focus { outline: none; border-color: var(--accent-dim); }
  .save-input.invalid { border-color: #ff5a5a; }
  .save-hint { color: var(--dim); font-size: 11px; line-height: 1.4; margin: 0; }

  /* Load-diagram modal — saved-diagrams table. */
  .load-modal { max-width: 640px; }
  .load-list-wrap {
    max-height: 340px; overflow-y: auto;
    border: 1px solid var(--line); background: var(--panel-2);
  }
  .load-table { width: 100%; border-collapse: collapse; font-size: 12px; }
  .load-table thead th {
    position: sticky; top: 0; z-index: 1;
    background: var(--panel-2); color: var(--dim);
    text-align: left; font-weight: 600;
    text-transform: uppercase; letter-spacing: 0.06em; font-size: 11px;
    padding: 9px 12px; border-bottom: 1px solid var(--line);
  }
  .load-col-name { width: 34%; }
  .load-col-actions { width: 1%; }
  .load-table tbody td {
    padding: 9px 12px; border-bottom: 1px solid var(--line);
    color: var(--ink); vertical-align: top; line-height: 1.4;
  }
  .load-table tbody tr { cursor: pointer; }
  .load-table tbody tr:hover td { background: var(--panel); }
  .load-table tbody tr:last-child td { border-bottom: none; }
  .load-table .load-name { font-weight: 500; word-break: break-all; }
  .load-table .load-desc { color: var(--dim); }
  .load-table .load-actions { text-align: right; white-space: nowrap; }
  /* Per-row delete button — subtle by default, red on hover. */
  .load-delete {
    background: transparent; border: 1px solid transparent;
    color: var(--dim); cursor: pointer; border-radius: 2px;
    font-size: 13px; line-height: 1; padding: 3px 7px;
  }
  .load-delete:hover { color: #ff6b6b; border-color: #ff6b6b; }
  .load-delete:focus-visible { outline: 1px solid var(--accent-dim); }
  .load-empty {
    padding: 18px 12px !important; text-align: center;
    color: var(--dim); cursor: default;
  }
  .load-empty:hover { background: transparent !important; }

  /* ============================================================
   * Diff / Merge modal — large two-pane workspace. The base
   * .help-overlay flex layout centers the modal; we override
   * .help-modal sizing for a near-fullscreen workspace and turn
   * the body into a column flex so the diff panes can scroll
   * independently while the header / file loader / actions stay
   * fixed at top and bottom.
   * ============================================================ */
  .diffmerge-modal {
    width: 90vw; max-width: 1400px;
    height: 85vh; max-height: 900px;
    display: flex; flex-direction: column;
    padding: 0;  /* override .help-modal padding so panels reach the edges */
    transition: width 0.15s ease, height 0.15s ease, max-width 0.15s ease, max-height 0.15s ease;
  }
  /* Fullscreen state — fills the viewport edge to edge. Uses fixed
     positioning to escape any padding/centering the .help-overlay
     parent imposes so the modal really reaches the screen edges. */
  .diffmerge-modal.fullscreen {
    position: fixed;
    inset: 0;
    width: 100vw; max-width: 100vw;
    height: 100vh; max-height: 100vh;
    border-radius: 0;
    margin: 0;
  }
  .diffmerge-modal .help-header {
    padding: 10px 16px; flex-shrink: 0;
    gap: 10px;
    /* position:relative so the absolutely-positioned upload button
       below can center itself against this header's bounding box.
       Override the .help-header's default "? " glyph (added via the
       ::before on h2) — we don't render an h2 here at all. No-op
       defensively in case future markup reintroduces one. */
    position: relative;
  }
  /* Fullscreen toggle lives on the LEFT side. .help-close gives it
     the standard square-icon-button styling but also adds
     margin-left:auto which would push it right; null that out so it
     stays at the start of the header. Compound selector beats the
     bare .help-close rule's specificity so the cascade lands on 0.
     The close button (also .help-close, but last) keeps the
     auto-margin and pins right. */
  .help-close.diffmerge-fullscreen { margin-left: 0; }
  /* File-load button — centered horizontally and vertically in the
     modal header. position:absolute removes it from the flex flow,
     so the close button (margin-left:auto) still pins to the right
     with the fileinfo span landing in the empty space next to the
     fullscreen toggle. cursor:pointer because <label> wraps a
     hidden <input type="file">. */
  .diffmerge-modal .diffmerge-fileload-label {
    cursor: pointer;
    flex-shrink: 0;
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
  }
  .diffmerge-fileinfo {
    color: var(--dim); font-size: 11px;
    font-family: 'JetBrains Mono', monospace;
    /* Truncate ridiculously long filenames so they don't push the
       close button off the right edge of the modal. */
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    min-width: 0;
  }
  .diffmerge-panels {
    flex: 1; display: flex; overflow: hidden;
    border-bottom: 1px solid var(--line);
  }
  .diffmerge-panel {
    flex: 1; display: flex; flex-direction: column;
    overflow: hidden; min-width: 0;
  }
  .diffmerge-panel-left { border-right: 1px solid var(--line); }
  .diffmerge-panel-title {
    display: flex; align-items: center; gap: 8px;
    padding: 8px 14px;
    background: var(--panel);
    border-bottom: 1px solid var(--line);
    color: var(--accent);
    font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em;
    font-weight: 700;
  }
  /* "Set all hunks" shortcuts, pushed to the right of the Diff pane title.
     Reuse .dm-hunk-action styling; text-transform:none keeps their labels
     from being upper-cased by the title bar's rule. */
  .diffmerge-setall {
    margin-left: auto; display: flex; gap: 6px;
    text-transform: none; letter-spacing: normal;
  }
  .diffmerge-setall .dm-hunk-action:disabled {
    opacity: 0.4; cursor: default;
  }
  .diffmerge-setall .dm-hunk-action:disabled:hover {
    color: var(--dim); border-color: var(--line);
  }
  /* "? Help" button — accent-tinted so it reads as a guide, not a merge action.
     Scoped under .diffmerge-setall to outrank the later .dm-hunk-action color. */
  .diffmerge-setall .diffmerge-help-btn { color: var(--accent); border-color: var(--accent-dim); }
  .diffmerge-setall .diffmerge-help-btn:hover { color: var(--accent); border-color: var(--accent); }
  /* The merge guide is itself a .help-overlay (z 1000, same as the merge modal
     it sits on top of), so lift it above both that and the confirm overlay. */
  .diffmerge-help-overlay { z-index: 1200; }
  .diffmerge-panel-sub {
    color: var(--dim); font-weight: 400; letter-spacing: 0.06em;
  }
  .diffmerge-diff {
    flex: 1; overflow: auto; padding: 8px;
    font-family: 'JetBrains Mono', monospace;
    font-size: 11px; line-height: 1.5;
    color: var(--ink); background: var(--bg);
  }
  /* Merged-result pane: a read-only CodeMirror fills it (line numbers + C/C++
     highlighting). CM owns scrolling and padding, so the host just supplies the
     var(--panel) offset that sets the "output" half apart from the diff pane. */
  .diffmerge-merged {
    flex: 1; overflow: hidden; min-height: 0;
    background: var(--panel);
  }
  /* Fallback when CodeMirror failed to load — restore plain preformatted text. */
  .diffmerge-merged.cm-missing {
    overflow: auto; padding: 8px; white-space: pre;
    font-family: 'JetBrains Mono', monospace;
    font-size: 11px; line-height: 1.5; color: var(--ink);
  }
  /* Fill the pane and keep the var(--panel) offset under BOTH CM themes (the
     light default is near-white and the dark material-ocean is its own blue-
     black — either would erase the deliberate contrast), matching the diff
     pane's mono type. styles.css loads after the CM theme, so these win on
     equal specificity. */
  .diffmerge-panel-right .CodeMirror {
    height: 100%;
    background: var(--panel);
    font-family: 'JetBrains Mono', monospace;
    font-size: 11px; line-height: 1.5;
  }
  .diffmerge-panel-right .CodeMirror-gutters {
    background: var(--panel); border-right: 1px solid var(--line);
  }
  .diffmerge-empty {
    padding: 20px; color: var(--dim);
    font-family: 'JetBrains Mono', monospace; font-size: 12px;
    line-height: 1.6;
  }

  /* Action bar (bottom) — Cancel + Apply, status text on the left. */
  .diffmerge-actions {
    display: flex; align-items: center; gap: 8px;
    padding: 12px 20px;
    background: var(--panel);
    flex-shrink: 0;
  }
  .diffmerge-status { flex: 1; font-size: 11px; color: var(--dim); }

  /* ----- Diff hunks ----- */
  /* GitHub-style split-diff. Six columns per row:
     [line# L] [marker L] [code L] [line# R] [marker R] [code R] */
  .dm-row {
    display: grid;
    grid-template-columns: 44px 22px minmax(0, 1fr) 44px 22px minmax(0, 1fr);
    background: var(--bg);
    line-height: 18px;
    font-family: 'JetBrains Mono', monospace;
    font-size: 11.5px;
  }
  .dm-ln {
    padding: 1px 8px 1px 4px;
    text-align: right;
    color: var(--dm-gutter-fg);
    background: var(--dm-gutter-bg);
    border-right: 1px solid var(--line);
    user-select: none;
    white-space: nowrap;
    font-size: 10.5px;
  }
  .dm-mk {
    padding: 1px 4px;
    text-align: center;
    color: var(--dm-gutter-fg);
    background: var(--dm-gutter-bg);
    user-select: none;
  }
  .dm-ln-added,
  .dm-mk-added   { background: var(--dm-added-gutter);   color: var(--dm-added-fg); }
  .dm-ln-removed,
  .dm-mk-removed { background: var(--dm-removed-gutter); color: var(--dm-removed-fg); }
  .dm-ln-empty,
  .dm-mk-empty   { background: var(--dm-empty-bg); }
  .dm-cell {
    padding: 1px 8px;
    white-space: pre;
    background: var(--bg);
    overflow-x: auto;
    color: var(--ink);
  }
  .dm-cell-empty   { background: var(--dm-empty-bg); }
  .dm-cell-removed { background: var(--dm-removed-bg); color: var(--ink); }
  .dm-cell-added   { background: var(--dm-added-bg);   color: var(--ink); }
  .dm-cell-context { background: var(--bg); color: var(--ink); }

  /* Collapsed context band — full-width, spans all six columns. */
  .dm-context-collapsed {
    padding: 6px 8px;
    background: var(--panel);
    color: var(--dim);
    text-align: center;
    font-size: 10.5px;
    border-top: 1px solid var(--line);
    border-bottom: 1px solid var(--line);
  }

  /* Hunk container — keeps the action bar above the rows. */
  .dm-hunk {
    border: 1px solid var(--line);
    border-radius: 6px;
    margin: 10px 0;
    overflow: hidden;
    background: var(--bg);
  }
  .dm-hunk-actions {
    display: flex; gap: 6px; align-items: center;
    padding: 6px 10px;
    background: var(--dm-hunk-header-bg);
    color: var(--dm-hunk-header-fg);
    border-bottom: 1px solid var(--line);
    flex-wrap: wrap;
  }
  .dm-hunk-header {
    font-family: 'JetBrains Mono', monospace;
    font-size: 10.5px;
    color: var(--dm-hunk-header-fg);
    margin-right: auto;
    letter-spacing: 0.02em;
  }
  .dm-hunk-action {
    background: var(--bg);
    color: var(--dim);
    border: 1px solid var(--line);
    border-radius: 6px;
    padding: 3px 10px;
    font-family: 'JetBrains Mono', monospace;
    font-size: 10.5px;
    cursor: pointer;
    transition: all 0.1s;
  }
  .dm-hunk-action:hover { color: var(--ink); border-color: var(--ink); }
  .dm-hunk-action.selected {
    background: var(--accent);
    color: var(--accent-text);
    border-color: var(--accent);
  }
  .canvas-empty {
    position: absolute; top: 50%; left: 50%;
    transform: translate(-50%, -50%);
    text-align: center; pointer-events: none;
    color: var(--dim); z-index: 1;
    transition: opacity 0.25s, visibility 0s linear 0.25s;
  }
  .canvas-empty.hidden {
    opacity: 0;
    visibility: hidden;
  }
  /* Reverse the visibility-delay when becoming visible again so it appears
     immediately rather than waiting for the opacity transition to finish. */
  .canvas-empty:not(.hidden) {
    transition: opacity 0.25s, visibility 0s linear 0s;
  }
  .canvas-empty .glyph {
    font-size: 56px; color: var(--accent-dim);
    margin-bottom: 14px; opacity: 0.5;
    font-family: 'JetBrains Mono', monospace;
  }
  .canvas-empty .hint {
    font-size: 12px; letter-spacing: 0.08em;
    line-height: 1.7;
  }
  .canvas-empty .hint strong { color: var(--accent); }
  .canvas-empty .keys {
    margin-top: 14px; font-size: 10px;
    color: var(--dim); opacity: 0.75;
    text-transform: uppercase; letter-spacing: 0.15em;
  }
  .canvas-empty .empty-title {
    font-size: 15px; font-weight: 600; color: var(--ink);
    letter-spacing: 0.02em; margin-bottom: 16px;
  }
  /* The two entry buttons are the only interactive bits of the (otherwise
     click-through) empty overlay, so re-enable pointer-events just here. */
  .canvas-empty .empty-actions {
    display: flex; gap: 10px; justify-content: center;
    flex-wrap: wrap; margin-bottom: 16px;
  }
  .empty-btn {
    pointer-events: auto; cursor: pointer;
    padding: 8px 14px; font-size: 12px; font-weight: 500;
    border-radius: 7px; border: 1px solid var(--line);
    background: var(--panel); color: var(--ink);
    transition: border-color 0.12s, background 0.12s, color 0.12s;
  }
  .empty-btn:hover {
    border-color: var(--accent);
    background: color-mix(in srgb, var(--accent) 10%, var(--panel));
  }
  .empty-btn-ai { color: var(--accent); border-color: var(--accent-dim); }
  .empty-btn-ai:hover {
    color: var(--accent); border-color: var(--accent);
    background: color-mix(in srgb, var(--accent) 14%, var(--panel));
  }

  /* Attention arrow for the ✨ Describe button — points brand-new users at the
     zero-effort path while the canvas is empty. updateStats() adds/removes
     .hint-pulse; it self-dismisses once a module is placed or Describe is
     opened (see the localStorage flag in setupAiDiagram). The arrow is a
     pseudo-element below the button (the toolbar sits at the canvas top, so
     there's room beneath) and bounces up toward it. */
  @keyframes describe-arrow {
    0%, 100% { transform: translateX(-50%) translateY(0); }
    50%      { transform: translateX(-50%) translateY(-8px); }
  }
  .canvas-btn-ai.hint-pulse { border-color: var(--accent); }
  /* A bold, crisp up-arrow built from an SVG mask filled with the accent
     colour (so it scales cleanly and stays theme-aware, unlike a thin ↑
     glyph). Sits below the button and bounces up toward it. */
  .canvas-btn-ai.hint-pulse::after {
    content: "";
    position: absolute; top: calc(100% + 7px); left: 50%;
    width: 28px; height: 32px;
    transform: translateX(-50%);
    background: var(--accent);
    -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 2 L21 13 L15 13 L15 23 L9 23 L9 13 L3 13 Z' fill='%23000'/%3E%3C/svg%3E") no-repeat center / contain;
    mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 2 L21 13 L15 13 L15 23 L9 23 L9 13 L3 13 Z' fill='%23000'/%3E%3C/svg%3E") no-repeat center / contain;
    pointer-events: none;
    filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.3));
    animation: describe-arrow 1s ease-in-out infinite;
  }
  @media (prefers-reduced-motion: reduce) {
    .canvas-btn-ai.hint-pulse::after { animation: none; }
  }

  .splitter {
    grid-area: splitter;
    background: var(--line);
    cursor: col-resize;
    position: relative;
    transition: background 0.12s;
    user-select: none;
  }
  .splitter:hover, .splitter.dragging { background: var(--accent-dim); }
  .splitter::before {
    /* Visible grip dots in the middle */
    content: "";
    position: absolute;
    top: 50%; left: 50%;
    transform: translate(-50%, -50%);
    width: 2px; height: 32px;
    background:
      radial-gradient(circle, var(--dim) 1px, transparent 1.5px) 0 0 / 2px 6px;
    opacity: 0.7;
  }
  .splitter:hover::before, .splitter.dragging::before {
    background: radial-gradient(circle, var(--accent) 1px, transparent 1.5px) 0 0 / 2px 6px;
    opacity: 1;
  }
  /* Wider invisible hit zone for easier grabbing */
  .splitter::after {
    content: "";
    position: absolute;
    inset: 0 -4px;
  }
  /* While dragging: disable text selection and pointer events on iframes */
  body.dragging-splitter { cursor: col-resize; }
  body.dragging-splitter iframe { pointer-events: none; }

  aside.inspector {
    grid-area: inspector;
    background: var(--panel);
    overflow: hidden;
    display: flex; flex-direction: column;
    min-width: 0;
    position: relative;   /* anchor the code-gen success overlay */
  }

  /* --- Code-generation success burst -----------------------------------
     A centered green checkmark that pops in with two radiating rings the
     instant a sketch finishes generating, so a fast generate doesn't slip
     by unnoticed. Covers the inspector, ignores clicks, and fades itself
     out (JS un-hides it, then re-hides after the animation). */
  .codegen-done {
    position: absolute; inset: 0; z-index: 40;
    display: flex; align-items: center; justify-content: center;
    pointer-events: none;
    background: color-mix(in srgb, var(--panel) 55%, transparent);
    animation: codegen-fade 1.9s ease-out forwards;
  }
  .codegen-done[hidden] { display: none; }
  .codegen-done-badge {
    display: flex; flex-direction: column; align-items: center; gap: 10px;
    animation: codegen-pop 0.45s cubic-bezier(0.2, 0.9, 0.3, 1.5) both;
  }
  .codegen-done-text {
    font-size: 13px; font-weight: 700; letter-spacing: 0.06em;
    text-transform: uppercase; color: var(--ok);
    opacity: 0; animation: codegen-text-in 0.3s ease-out 0.28s forwards;
  }
  .codegen-check { flex: none; overflow: visible;
    filter: drop-shadow(0 0 10px color-mix(in srgb, var(--ok) 45%, transparent)); }
  .codegen-check-ring {
    fill: none; stroke: var(--ok); stroke-width: 2.5;
    stroke-dasharray: 151; stroke-dashoffset: 151;
    animation: codegen-ring-draw 0.5s ease-out forwards;
  }
  .codegen-check-mark {
    fill: none; stroke: var(--ok); stroke-width: 4;
    stroke-linecap: round; stroke-linejoin: round;
    stroke-dasharray: 40; stroke-dashoffset: 40;
    animation: codegen-mark-draw 0.28s ease-out 0.34s forwards;
  }
  /* Two concentric rings that expand outward and fade — the "burst". */
  .codegen-done-ring {
    position: absolute; width: 60px; height: 60px; border-radius: 50%;
    border: 2px solid var(--ok); opacity: 0;
    animation: codegen-burst 0.9s ease-out 0.1s forwards;
  }
  .codegen-done-ring-2 { animation-delay: 0.28s; }
  @keyframes codegen-pop {
    0%   { transform: scale(0.4); opacity: 0; }
    60%  { transform: scale(1.08); opacity: 1; }
    100% { transform: scale(1); opacity: 1; }
  }
  @keyframes codegen-ring-draw { to { stroke-dashoffset: 0; } }
  @keyframes codegen-mark-draw { to { stroke-dashoffset: 0; } }
  @keyframes codegen-text-in { to { opacity: 1; } }
  @keyframes codegen-burst {
    0%   { transform: scale(0.6); opacity: 0.7; }
    100% { transform: scale(3.2); opacity: 0; }
  }
  @keyframes codegen-fade {
    0%, 70% { opacity: 1; }
    100%    { opacity: 0; }
  }
  @media (prefers-reduced-motion: reduce) {
    .codegen-done, .codegen-done-badge, .codegen-done-text,
    .codegen-check-ring, .codegen-check-mark, .codegen-done-ring {
      animation-duration: 0.01s; animation-delay: 0s;
    }
    .codegen-check-ring, .codegen-check-mark { stroke-dashoffset: 0; }
    .codegen-done-text { opacity: 1; }
    .codegen-done-ring { display: none; }
  }
  .tab-bar { display: flex; border-bottom: 1px solid var(--line); }
  .tab {
    flex: 1; padding: 10px; background: transparent;
    border: none; color: var(--dim); font-family: inherit;
    font-size: 10px; text-transform: uppercase; letter-spacing: 0.1em;
    cursor: pointer; border-bottom: 2px solid transparent;
  }
  .tab.active { color: var(--accent); border-bottom-color: var(--accent); }
  .tab-content {
    display: none; padding: 14px;
    flex: 1; min-width: 0; min-height: 0;
    overflow: auto;
  }
  .tab-content.active { display: block; }

  /* AdSense slot pinned to the bottom of the inspector. Sits as a flex
     sibling of the tab-content panels so it doesn't scroll with them and
     the tab panel above absorbs any leftover height. Capped to keep the
     tab content workable when the inspector is narrow. */
  .inspector-ad {
    flex: 0 0 auto;
    min-height: 100px;
    max-height: 250px;
    border-top: 1px solid var(--line);
    padding: 8px;
    background: var(--panel);
    overflow: hidden;
  }
  .inspector-ad ins.adsbygoogle { width: 100%; height: 100%; min-height: 90px; }

  .field { margin-bottom: 12px; }
  .field label {
    display: block; color: var(--dim); font-size: 10px;
    text-transform: uppercase; letter-spacing: 0.1em; margin-bottom: 4px;
  }
  .field input, .field select, .field textarea {
    width: 100%; background: var(--bg); color: var(--ink);
    border: 1px solid var(--line); padding: 6px 8px;
    font-family: inherit; font-size: 12px;
  }
  .field input:focus, .field select:focus, .field textarea:focus {
    outline: none; border-color: var(--accent);
  }
  .field textarea { resize: vertical; min-height: 70px; }
  /* Native checkbox in the inspector — override the 100%-width rule from
     .field input above so the box stays at the browser's natural size. */
  .field input[type="checkbox"] {
    width: auto;
    margin: 2px 0;
    accent-color: var(--accent);
    cursor: pointer;
  }
  /* Informational notice surfaced below the standard prop inputs (e.g.
     the Firmware/Filesystem Upload partition prerequisites). */
  .firmware-upload-note {
    background: var(--bg);
    border: 1px solid var(--line);
    border-left: 3px solid var(--warn);
    padding: 8px 10px;
    color: var(--dim);
    font-size: 11px;
    line-height: 1.45;
  }
  .firmware-upload-note strong { color: var(--ink); font-weight: 600; }
  .firmware-upload-note em { color: var(--ink); font-style: normal; }
  /* Neutral informational explainer (e.g. the Tabs / Accordion description).
     Same shape as the firmware note but with an accent (not warn) stripe. */
  .inspector-note {
    background: var(--bg);
    border: 1px solid var(--line);
    border-left: 3px solid var(--accent);
    padding: 8px 10px;
    color: var(--dim);
    font-size: 11px;
    line-height: 1.45;
  }
  .inspector-note strong { color: var(--ink); font-weight: 600; }
  /* Inline per-field guidance (e.g. Captive Portal password). Neutral by
     default; the .warn modifier turns the accent stripe to the warn color. */
  .field-hint {
    margin-top: 6px;
    padding: 7px 9px;
    background: var(--bg);
    border: 1px solid var(--line);
    border-left: 3px solid var(--accent);
    color: var(--dim);
    font-size: 11px;
    line-height: 1.45;
  }
  .field-hint.warn { border-left-color: var(--warn); }
  .field-hint.error { border-left-color: #e85a5a; }
  .field-hint strong { color: var(--ink); font-weight: 600; }
  .field-hint[hidden] { display: none; }
  /* Required-field marker and invalid-input state (e.g. a blank required SSID). */
  .field label .req { color: #e85a5a; font-weight: 600; }
  .field input.invalid { border-color: #e85a5a; }
  .field input.invalid:focus { border-color: #ff7878; }
  .empty-state { color: var(--dim); text-align: center; padding: 30px 10px; font-size: 11px; }

  /* Read-only derived-value list (e.g. header nav links) */
  .derived-list {
    background: var(--bg); border: 1px solid var(--line);
    border-left: 3px solid var(--accent-dim);
    padding: 6px 8px; font-size: 11px;
  }
  .derived-item {
    display: flex; align-items: center; gap: 10px;
    padding: 6px 4px; border-bottom: 1px dashed var(--line);
    position: relative;
    transition: background 0.1s;
  }
  .derived-item:last-child { border-bottom: 0; }
  .derived-item[draggable="true"] { cursor: grab; }
  .derived-item[draggable="true"]:active { cursor: grabbing; }
  .derived-item:hover { background: rgba(127,255,90,0.04); }
  .derived-item.dragging { opacity: 0.35; }
  /* Drop indicators — a green bar appears above/below the hover target */
  .derived-item.drop-above::before,
  .derived-item.drop-below::after {
    content: ""; position: absolute; left: 0; right: 0;
    height: 2px; background: var(--accent);
    box-shadow: 0 0 6px var(--accent);
  }
  .derived-item.drop-above::before { top: -1px; }
  .derived-item.drop-below::after  { bottom: -1px; }
  .drag-handle {
    color: var(--dim); font-family: 'JetBrains Mono', monospace;
    font-size: 14px; letter-spacing: -2px; user-select: none;
    width: 14px; text-align: center; flex-shrink: 0;
  }
  /* When the handle itself is the draggable element (Button Group + Table
     editors, so their row inputs stay editable), show the grab cursor on
     the handle rather than the whole row. */
  .drag-handle[draggable="true"] { cursor: grab; }
  .drag-handle[draggable="true"]:active { cursor: grabbing; }
  .derived-item:hover .drag-handle { color: var(--accent); }
  .derived-label { color: var(--ink); flex: 1; }
  .derived-route { color: var(--accent); font-family: 'JetBrains Mono', monospace; font-size: 10.5px; }
  .derived-empty {
    color: var(--dim); font-style: italic; font-size: 11px;
    padding: 6px 4px; line-height: 1.5;
  }

  /* ButtonGroup label-row variants of the .derived-item / .derived-list
     widget — same drag-reorder shell but each row hosts an inline text
     input + a remove (×) button, and a dedicated "+ Add" row sits just
     below the list. */
  .bgrp-item .bgrp-label-input {
    flex: 1;
    background: var(--bg); color: var(--ink);
    border: 1px solid var(--line); border-radius: 2px;
    padding: 3px 6px; font-family: inherit; font-size: 11px;
  }
  .bgrp-item .bgrp-label-input:focus {
    outline: none; border-color: var(--accent);
  }
  .bgrp-item .bgrp-remove {
    background: transparent; border: 1px solid var(--line);
    color: var(--dim); cursor: pointer;
    width: 22px; height: 22px; line-height: 1; padding: 0;
    border-radius: 2px; font-size: 14px; flex-shrink: 0;
  }
  .bgrp-item .bgrp-remove:hover {
    color: var(--wire); border-color: var(--wire);
  }
  .bgrp-add-row {
    display: flex; gap: 6px; margin-top: 6px;
    padding: 4px 8px;
  }
  .bgrp-add-row .bgrp-add-label {
    flex: 1;
    background: var(--bg); color: var(--ink);
    border: 1px solid var(--line); border-radius: 2px;
    padding: 4px 6px; font-family: inherit; font-size: 11px;
  }
  .bgrp-add-row .bgrp-add-label:focus {
    outline: none; border-color: var(--accent);
  }
  .bgrp-add-row .btn {
    padding: 4px 10px; font-size: 11px;
  }

  /* Table-column row — borrows the .derived-item / .bgrp-add-row chrome
     but adds a per-row type <select> between the name input and the
     remove button. Type-select is also reused inside the "+ Add" row. */
  .tblcol-item .tblcol-name-input,
  .bgrp-add-row .tblcol-name-input {
    flex: 1;
    background: var(--bg); color: var(--ink);
    border: 1px solid var(--line); border-radius: 2px;
    padding: 3px 6px; font-family: inherit; font-size: 11px;
    /* At least ~10 characters wide so the field never collapses to an
       unclickable sliver in a narrow inspector. */
    min-width: 10ch;
  }
  .tblcol-item .tblcol-name-input:focus,
  .bgrp-add-row .tblcol-name-input:focus {
    outline: none; border-color: var(--accent);
  }
  .tblcol-item .tblcol-type-input,
  .tblcol-item .tblcol-align-input,
  .bgrp-add-row .tblcol-add-type,
  .bgrp-add-row .tblcol-add-align {
    background: var(--bg); color: var(--ink);
    border: 1px solid var(--line); border-radius: 2px;
    padding: 3px 4px; font-family: inherit; font-size: 11px;
    /* Override .field select { width:100% } and let the two selects
       share the line and shrink, so Type + Alignment stay on one row
       instead of wrapping. */
    flex: 1; width: auto; min-width: 0;
  }
  /* Column row layout: drag handle + a two-line body (Header Name on
     the first line; Type + Alignment together on the second) + remove
     button. Handle/remove center vertically against the body. */
  .tblcol-item { flex-wrap: nowrap; align-items: center; gap: 8px; }
  .tblcol-body {
    flex: 1; min-width: 0;
    display: flex; flex-direction: column; gap: 6px;
  }
  .tblcol-line { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
  .tblcol-lbl {
    color: var(--dim); font-size: 9.5px; font-weight: 600;
    text-transform: uppercase; letter-spacing: 0.05em;
    flex-shrink: 0;
  }
  /* Add-row keeps a single line but may wrap in a narrow inspector. */
  .tblcol-add-row { flex-wrap: wrap; align-items: center; }
  .tblcol-item .tblcol-remove {
    background: transparent; border: 1px solid var(--line);
    color: var(--dim); cursor: pointer;
    width: 22px; height: 22px; line-height: 1; padding: 0;
    border-radius: 2px; font-size: 14px; flex-shrink: 0;
  }
  .tblcol-item .tblcol-remove:hover {
    color: var(--wire); border-color: var(--wire);
  }

  /* Schedule entry rows — a read-only summary line per entry
     (days · time · duration + remove), plus a two-part add block: a row of
     seven day-toggle buttons above a time/duration/+Add line. Borrows the
     .derived-item / .bgrp chrome for consistency with the other list editors. */
  .sched-item { flex-wrap: nowrap; align-items: center; gap: 8px; }
  .sched-item .sched-days { flex: 1; min-width: 0; font-size: 11px; color: var(--accent); }
  .sched-item .sched-time { font-size: 11px; color: var(--ink); }
  .sched-item .sched-dur  { font-size: 11px; color: var(--dim); white-space: nowrap; }
  .sched-item .sched-remove {
    background: transparent; border: 1px solid var(--line);
    color: var(--dim); cursor: pointer;
    width: 22px; height: 22px; line-height: 1; padding: 0;
    border-radius: 2px; font-size: 14px; flex-shrink: 0;
  }
  .sched-item .sched-remove:hover { color: var(--wire); border-color: var(--wire); }
  .sched-add { padding: 6px 8px; }
  .sched-days-row { display: flex; gap: 3px; margin-bottom: 6px; }
  .sched-days-row .sched-day {
    flex: 1; background: var(--bg); color: var(--dim);
    border: 1px solid var(--line); border-radius: 2px;
    padding: 3px 0; font-family: inherit; font-size: 9.5px; cursor: pointer;
  }
  .sched-days-row .sched-day.on {
    background: var(--accent); color: var(--bg); border-color: var(--accent);
  }
  .sched-add-row { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
  .sched-add-row input[type="time"],
  .sched-add-row input[type="number"] {
    background: var(--bg); color: var(--ink);
    border: 1px solid var(--line); border-radius: 2px;
    padding: 3px 6px; font-family: inherit; font-size: 11px; min-width: 0;
  }
  .sched-add-row input[type="number"] { width: 60px; }
  .sched-add-row .btn { padding: 4px 10px; font-size: 11px; }

  pre.code-output {
    background: var(--bg); border: 1px solid var(--line);
    padding: 10px; font-size: 10.5px; line-height: 1.5;
    color: var(--ink);
    white-space: pre-wrap; word-break: break-word; overflow-wrap: anywhere;
    max-width: 100%; box-sizing: border-box;
    margin: 0;
  }

  /* File-tab strip above the code output. One chip per generated file
     (Callbacks.h, EmbeddedSite.h, demo.ino). Clicking swaps
     which file's contents appear in code-output. */
  .code-file-tabs {
    display: flex; gap: 4px; margin: 10px 0 6px 0;
    flex-wrap: wrap;
    align-items: center;
  }
  .code-file-tab {
    background: var(--bg); border: 1px solid var(--line);
    color: var(--dim); cursor: pointer;
    padding: 4px 10px; font-size: 10.5px;
    font-family: 'JetBrains Mono', monospace;
    border-radius: 2px;
    transition: color 0.12s, border-color 0.12s, background 0.12s;
  }
  .code-file-tab:hover { color: var(--ink); border-color: var(--ink); }
  .code-file-tab.active {
    color: var(--accent); border-color: var(--accent);
    background: color-mix(in srgb, var(--accent) 8%, transparent);
  }
  /* Diff/Merge button — sits at the far right of the file-tab strip
     (margin-left:auto pushes past the chips), only visible when the
     active file is Callbacks.h (toggled via hidden attr from JS). */
  .code-diff-merge-btn {
    margin-left: auto;
    padding: 4px 12px; font-size: 11px;
    flex-shrink: 0;
  }
  pre.code-output .kw { color: #ff5a8c; }
  pre.code-output .str { color: var(--accent); }
  pre.code-output .com { color: var(--dim); font-style: italic; }
  pre.code-output .num { color: var(--warn); }

  /* ----------------------------------------------------------
     Notes.md content inside pre.code-output. The wrapper turns OFF the
     <pre>'s default whitespace handling for headings/paragraphs/lists
     (which would otherwise show the renderer's inter-tag newlines) but
     re-enables it on <pre><code> blocks so embedded code samples keep
     their formatting.
     ---------------------------------------------------------- */
  .notes-md {
    white-space: normal;
    font-family: 'JetBrains Mono', monospace;
    font-size: 11px; line-height: 1.55;
    color: var(--ink);
  }
  .notes-md h1, .notes-md h2, .notes-md h3 {
    font-family: 'Space Grotesk', sans-serif;
    color: var(--accent);
    letter-spacing: -0.01em;
    margin-top: 18px; margin-bottom: 6px;
    line-height: 1.25;
  }
  .notes-md h1 { font-size: 17px; padding-bottom: 4px; border-bottom: 1px solid var(--line); }
  .notes-md h2 { font-size: 14px; }
  .notes-md h3 { font-size: 12px; color: var(--ink); text-transform: uppercase; letter-spacing: 0.06em; }
  .notes-md p  { margin: 0 0 8px 0; }
  .notes-md ul, .notes-md ol {
    margin: 4px 0 10px 0; padding-left: 18px;
  }
  .notes-md li { margin: 2px 0; }
  .notes-md a  { color: var(--accent); text-decoration: underline; }
  .notes-md code {
    background: var(--bg); color: var(--accent);
    padding: 1px 5px; border-radius: 2px;
    font-family: 'JetBrains Mono', monospace; font-size: 10.5px;
  }
  .notes-md pre {
    background: var(--bg); border: 1px solid var(--line);
    border-radius: 2px;
    padding: 8px 10px; margin: 6px 0 10px 0;
    white-space: pre; overflow-x: auto;
  }
  .notes-md pre code {
    background: transparent; color: var(--ink); padding: 0;
    font-size: 10.5px;
  }
  .notes-md strong { color: var(--ink); }

  /* Code tab toolbar with Copy / Download */
  .code-toolbar {
    display: flex; align-items: center; gap: 6px;
    padding-bottom: 10px; margin-bottom: 0;
    border-bottom: 1px dashed var(--line);
  }
  .code-toolbar .btn { padding: 5px 10px; font-size: 11px; }
  .code-toolbar .btn.flash-ok,
  .code-view-actions .btn.flash-ok {
    background: var(--accent-dim); border-color: var(--accent-dim); color: #fff;
  }
  /* Preview tab */
  #tab-preview { display: none; flex-direction: column; padding: 0; }
  #tab-preview.active { display: flex; }
  #tab-code { display: none; flex-direction: column; }
  #tab-code.active { display: flex; }

  /* Positioned wrapper around the source-code view so the Edit / Save / Cancel
     controls can float over the top-right corner of whichever view is active
     (#code-output or the inline CodeMirror). Fills the remaining tab height and
     hands that height down to the read-only <pre> / editor as flex children. */
  #code-view {
    position: relative; flex: 1; min-height: 0;
    display: flex; flex-direction: column;
  }
  #code-view pre.code-output { flex: 1; overflow: auto; min-height: 0; }

  /* Edit controls superimposed on the code window. Kept semi-transparent so
     they don't fully obscure the code beneath, then solidified on hover/focus
     (and always solid while editing, when Save is the primary action). */
  .code-view-actions {
    position: absolute; top: 8px; right: 14px; z-index: 6;
    display: flex; gap: 6px;
  }
  .code-view-actions .btn {
    padding: 4px 10px; font-size: 11px;
    background: color-mix(in srgb, var(--panel) 90%, transparent);
    box-shadow: 0 1px 5px rgba(0, 0, 0, 0.28);
    opacity: 0.8; transition: opacity 0.12s;
  }
  #code-view:hover .code-view-actions .btn,
  .code-view-actions .btn:focus-visible,
  .code-view-actions .btn.flash-ok,
  #tab-code.editing .code-view-actions .btn { opacity: 1; }

  /* ----------------------------------------------------------
     Inline source editor (#code-edit-host) — a CodeMirror 5 instance
     that takes the place of pre.code-output while editing the active
     file. Fills the code panel exactly like the read-only <pre>.
     ---------------------------------------------------------- */
  #tab-code .code-edit-host {
    flex: 1; min-height: 0; display: flex; overflow: hidden;
    border: 1px solid var(--accent-dim); box-sizing: border-box;
  }
  #tab-code .code-edit-host[hidden] { display: none; }
  .code-edit-host .CodeMirror {
    flex: 1; height: auto; width: 100%;
    font-family: 'JetBrains Mono', ui-monospace, monospace;
    font-size: 10.5px; line-height: 1.5;
  }
  /* While editing, lock the file-tab strip so the user commits via Save or
     discards via Cancel before switching files (a switch re-renders and would
     otherwise drop in-progress edits). */
  #tab-code.editing .code-file-tabs { pointer-events: none; opacity: 0.5; }
  .preview-toolbar {
    display: flex; gap: 6px; padding: 10px;
    border-bottom: 1px solid var(--line); background: var(--panel-2);
  }
  .preview-toolbar select {
    flex: 1; background: var(--bg); color: var(--ink);
    border: 1px solid var(--line); padding: 5px 8px;
    font-family: inherit; font-size: 11px;
  }
  .preview-toolbar select:focus { outline: none; border-color: var(--accent); }
  .preview-toolbar .btn { padding: 5px 10px; }
  .preview-frame-wrap {
    flex: 1; padding: 14px; overflow: auto;
    background: var(--bg);
    display: flex; align-items: flex-start; justify-content: center;
  }
  /* In flat mode the shell is transparent so the iframe lays out as a direct
     flex child (original behaviour). In device-mode it becomes the chassis. */
  .device-shell { display: contents; }
  #preview-iframe {
    width: 100%; min-height: 500px; height: 100%;
    border: 1px solid var(--line); background: #fff;
    transition: width 0.2s, height 0.2s;
  }

  /* The device picker sizes to its content; the page picker keeps the flex. */
  #preview-device-select { flex: 0 0 auto; }

  /* ===== Device frames (phone / tablet): shared dark chassis ===== */
  .preview-frame-wrap.device-mode {
    background: radial-gradient(circle at 50% 30%, #1a2420 0%, var(--bg) 70%);
  }
  .preview-frame-wrap.device-mode .device-shell {
    display: block;
    position: relative;
    flex: 0 0 auto;
    background: linear-gradient(160deg, #20242c 0%, #0c0e12 55%, #07080b 100%);
    box-shadow:
      inset 0 0 0 2px #2c303a,                 /* metallic inner rim */
      inset 0 1px 1px rgba(255,255,255,0.12),   /* top highlight */
      0 0 0 1px #000,                           /* crisp outer edge */
      0 18px 50px rgba(0,0,0,0.55);             /* drop shadow */
  }
  .preview-frame-wrap.device-mode #preview-iframe {
    display: block; border: 0; background: #fff;
  }
  /* Home indicator / gesture bar — shared; position + width tuned per device. */
  .preview-frame-wrap.device-mode .device-shell::after {
    content: "";
    position: absolute; left: 50%; transform: translateX(-50%);
    height: 5px; border-radius: 3px;
    background: rgba(255,255,255,0.55);
    z-index: 2; pointer-events: none;
  }

  /* ----- Phone: modern notch, tall 300×620 screen ----- */
  /* Taller top bezel so the notch sits in the "forehead" and the screen
     (iframe) starts below it — page content no longer hides under the notch. */
  .preview-frame-wrap.device-mode.phone .device-shell { padding: 40px 14px 22px; border-radius: 46px; }
  .preview-frame-wrap.device-mode.phone #preview-iframe { width: 300px; height: 620px; border-radius: 32px; }
  /* The notch — black rounded-bottom bar with a camera lens + speaker slit. */
  .preview-frame-wrap.device-mode.phone .device-shell::before {
    content: "";
    position: absolute;
    top: 8px; left: 50%; transform: translateX(-50%);
    width: 150px; height: 26px;
    border-radius: 0 0 16px 16px;
    background-color: #000;
    background-image:
      radial-gradient(circle at calc(50% + 36px) 50%, #13314a 0 3px, transparent 3.5px), /* camera lens */
      linear-gradient(#202329, #202329);                                                  /* speaker slit */
    background-repeat: no-repeat;
    background-position: center, calc(50% - 8px) center;
    background-size: auto, 38px 5px;
    z-index: 2; pointer-events: none;
  }
  .preview-frame-wrap.device-mode.phone .device-shell::after { bottom: 11px; width: 110px; }

  /* ----- Tablet: uniform bezel, centred front camera, 3:4 portrait ----- */
  .preview-frame-wrap.device-mode.tablet .device-shell { padding: 26px; border-radius: 30px; }
  .preview-frame-wrap.device-mode.tablet #preview-iframe { width: 480px; height: 640px; border-radius: 14px; }
  /* Front camera — a small lens centred in the top bezel (no notch). */
  .preview-frame-wrap.device-mode.tablet .device-shell::before {
    content: "";
    position: absolute;
    top: 12px; left: 50%; transform: translateX(-50%);
    width: 7px; height: 7px; border-radius: 50%;
    background: radial-gradient(circle at 40% 35%, #2a4a63 0 40%, #0a1a26 75%);
    box-shadow: 0 0 0 1px rgba(255,255,255,0.06);
    z-index: 2; pointer-events: none;
  }
  .preview-frame-wrap.device-mode.tablet .device-shell::after { bottom: 12px; width: 140px; }
  .preview-empty {
    display: none; color: var(--dim); text-align: center;
    padding: 30px 10px; font-size: 11px;
  }
  .preview-empty.show { display: block; }
  .preview-frame-wrap.hidden { display: none; }

  footer {
    /* Force-anchored to the viewport bottom — independent of the grid layout
       so it can never be pushed off-screen by oversized content. The body's
       grid still reserves space for it via the 24px footer row. */
    position: fixed;
    left: 0; right: 0; bottom: 0;
    height: 24px;
    background: var(--panel);
    border-top: 1px solid var(--line);
    display: flex; align-items: center; padding: 0 12px;
    color: var(--dim); font-size: 10px; gap: 16px;
    text-transform: uppercase; letter-spacing: 0.1em;
    z-index: 10;
  }
  footer .status { color: var(--accent); }
  footer .spacer { flex: 1; }
  #footer-version {
    font-family: 'JetBrains Mono', monospace;
    color: var(--accent); letter-spacing: 0.04em;
    text-transform: none;
  }
  #footer-copyright {
    color: var(--dim); letter-spacing: 0.04em;
    text-transform: none;
    margin-left: 8px;
    padding-left: 14px;
    border-left: 1px solid var(--line);
  }
  /* "Credits X/Y" indicator on the right side of the footer. Mirrors
     the credits ring around the Generate button — provides a precise
     numeric for users who'd rather read the count than eyeball the
     ring. Hidden when the user's plan has no cap. */
  #footer-credits, #footer-compile-credits, #footer-ai-gen-credits {
    font-family: 'JetBrains Mono', monospace;
    color: var(--accent); letter-spacing: 0.04em;
    text-transform: none;
    font-variant-numeric: tabular-nums;
    padding-right: 14px;
    border-right: 1px solid var(--line);
  }
  #footer-credits[data-state="low"], #footer-compile-credits[data-state="low"], #footer-ai-gen-credits[data-state="low"] { color: var(--accent); }
  #footer-credits[data-state="out"], #footer-compile-credits[data-state="out"], #footer-ai-gen-credits[data-state="out"] { color: #ff7676; }
  /* Pulse applied for one cycle whenever the credit count changes (e.g.
     after a generate spends a credit). transform-origin is right-center
     so the scale grows leftward into the empty spacer rather than
     shoving the db-activity indicator. The glow uses currentColor so it
     matches whatever state color is active (accent normally, red when
     depleted). Direct flex children are blockified, so the transform
     applies without needing display:inline-block. */
  @keyframes footer-credits-bump {
    0%   { transform: scale(1);    text-shadow: none; }
    25%  { transform: scale(1.35); text-shadow: 0 0 8px currentColor; }
    100% { transform: scale(1);    text-shadow: none; }
  }
  #footer-credits.credits-bump, #footer-compile-credits.credits-bump, #footer-ai-gen-credits.credits-bump {
    transform-origin: right center;
    animation: footer-credits-bump 0.9s ease-out;
  }
  @media (prefers-reduced-motion: reduce) {
    #footer-credits.credits-bump, #footer-compile-credits.credits-bump, #footer-ai-gen-credits.credits-bump { animation: none; }
  }

  /* DB-activity blink indicator — small dim circle next to the READY
     status. Pulses accent + expanding ring whenever apiCall() fires.
     The dot itself snaps on (50 ms) and fades back to dim (400 ms) so
     successive rapid pulses (e.g. auto-save bursts) stay visible. */
  .db-indicator {
    display: inline-block;
    width: 8px; height: 8px;
    border-radius: 50%;
    background: var(--line);
    margin-right: 8px;
    vertical-align: middle;
    transition: background 0.4s ease;
  }
  .db-indicator.active {
    background: var(--accent);
    transition: background 0.05s ease;
    animation: db-pulse 0.6s ease-out;
  }
  @keyframes db-pulse {
    0%   { box-shadow: 0 0 0 0   color-mix(in srgb, var(--accent) 65%, transparent); }
    100% { box-shadow: 0 0 0 8px color-mix(in srgb, var(--accent)  0%, transparent); }
  }

  /* Scrollbars */
  ::-webkit-scrollbar { width: 8px; height: 8px; }
  ::-webkit-scrollbar-track { background: var(--bg); }
  ::-webkit-scrollbar-thumb { background: var(--line); }
  ::-webkit-scrollbar-thumb:hover { background: var(--accent-dim); }

  /* Help modal */
  .help-overlay {
    position: fixed; inset: 0; z-index: 1000;
    background: rgba(0, 0, 0, 0.75);
    backdrop-filter: blur(4px);
    display: none; align-items: center; justify-content: center;
    padding: 40px;
  }
  .help-overlay.open { display: flex; }
  .help-modal {
    background: var(--panel);
    border: 1px solid var(--line);
    width: 100%; max-width: 900px; height: 100%; max-height: 90vh;
    display: grid; grid-template-rows: auto 1fr;
    box-shadow: 0 20px 80px rgba(127,255,90,0.08);
  }
  .help-header {
    display: flex; align-items: center; padding: 16px 24px;
    background: var(--panel-2); border-bottom: 1px solid var(--line);
  }
  .help-header h2 {
    font-family: 'Space Grotesk', sans-serif; font-size: 18px;
    color: var(--accent); letter-spacing: 0.05em;
  }
  .help-header h2::before { content: "? "; color: var(--accent-dim); }
  .help-close {
    margin-left: auto; background: transparent; border: 1px solid var(--line);
    color: var(--dim); width: 32px; height: 32px; cursor: pointer;
    font-family: inherit; font-size: 16px;
  }
  .help-close:hover { color: var(--wire); border-color: var(--wire); }
  .help-body {
    overflow-y: auto; padding: 28px 36px;
    color: var(--ink); line-height: 1.65; font-size: 13px;
  }
  .help-body h3 {
    font-family: 'Space Grotesk', sans-serif;
    font-size: 18px; color: var(--accent);
    margin: 28px 0 10px; padding-bottom: 6px;
    border-bottom: 1px solid var(--line);
    letter-spacing: 0.02em;
  }
  .help-body h3:first-child { margin-top: 0; }
  .help-body h4 {
    font-family: 'JetBrains Mono', monospace;
    font-size: 12px; color: #5ad7ff;
    text-transform: uppercase; letter-spacing: 0.12em;
    margin: 20px 0 8px;
  }
  .help-body p { margin-bottom: 12px; color: var(--ink); }
  .help-body ul, .help-body ol { margin: 0 0 14px 20px; }
  .help-body li { margin-bottom: 6px; }
  .help-body code {
    background: var(--bg); border: 1px solid var(--line);
    padding: 1px 6px; font-family: 'JetBrains Mono', monospace;
    font-size: 11.5px; color: var(--accent); border-radius: 2px;
  }
  .help-body strong { color: var(--accent); font-weight: 600; }
  .help-body .callout {
    border-left: 3px solid var(--warn);
    background: rgba(255, 184, 74, 0.06);
    padding: 10px 14px; margin: 14px 0;
    font-size: 12px; color: #d8c8a0;
  }
  .help-body .callout strong { color: var(--warn); }
  .help-body table {
    width: 100%; border-collapse: collapse; margin: 10px 0 18px;
    font-size: 12px;
  }
  .help-body th, .help-body td {
    border: 1px solid var(--line); padding: 8px 12px;
    text-align: left; vertical-align: top;
  }
  .help-body th {
    background: var(--panel-2); color: var(--dim);
    font-weight: 500; text-transform: uppercase;
    letter-spacing: 0.08em; font-size: 10.5px;
  }
  .help-body td code { font-size: 11px; }
  .help-body kbd {
    display: inline-block; padding: 1px 6px;
    background: var(--panel-2); border: 1px solid var(--line);
    border-bottom-width: 2px; border-radius: 2px;
    font-family: 'JetBrains Mono', monospace; font-size: 10.5px;
    color: var(--ink);
  }
  .help-toc {
    display: flex; flex-wrap: wrap; gap: 8px;
    margin-bottom: 24px; padding-bottom: 18px;
    border-bottom: 1px solid var(--line);
  }
  .help-toc a {
    color: var(--dim); text-decoration: none;
    padding: 4px 10px; border: 1px solid var(--line);
    font-size: 11px; font-family: 'JetBrains Mono', monospace;
    text-transform: uppercase; letter-spacing: 0.08em;
  }
  .help-toc a:hover { color: var(--accent); border-color: var(--accent-dim); }

  /* Theme picker modal */
  .theme-modal { max-width: 720px; }
  .theme-body { padding: 24px 32px; }
  .theme-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
    gap: 10px;
    margin-bottom: 24px;
  }
  .theme-swatch {
    display: flex; align-items: center; gap: 12px;
    background: var(--panel-2); border: 1px solid var(--line);
    padding: 10px 12px;
  }
  .theme-swatch label {
    flex: 1;
    display: flex; flex-direction: column; gap: 2px;
    color: var(--ink); font-size: 12px; cursor: pointer;
  }
  .theme-swatch .role-name {
    font-family: 'JetBrains Mono', monospace;
    font-size: 11px; color: var(--accent);
  }
  .theme-swatch .role-desc {
    color: var(--dim); font-size: 10.5px;
  }
  .theme-swatch input[type="color"] {
    width: 44px; height: 36px; padding: 0;
    border: 1px solid var(--line); background: var(--bg);
    cursor: pointer;
  }
  .theme-swatch input[type="text"] {
    width: 76px; background: var(--bg); border: 1px solid var(--line);
    color: var(--ink); padding: 6px 8px;
    font-family: 'JetBrains Mono', monospace; font-size: 11px;
  }
  .theme-swatch input[type="text"]:focus,
  .theme-swatch input[type="color"]:focus { outline: 1px solid var(--accent); }
  .theme-preset-row {
    display: flex; align-items: center; gap: 8px;
    padding-top: 16px; border-top: 1px solid var(--line);
    flex-wrap: wrap;
  }
  .theme-preset-label {
    color: var(--dim); font-size: 10px;
    text-transform: uppercase; letter-spacing: 0.12em;
    margin-right: 4px;
  }
  .theme-preset {
    background: var(--panel-2); color: var(--ink);
    border: 1px solid var(--line); padding: 5px 12px;
    font-family: inherit; font-size: 11px; cursor: pointer;
    letter-spacing: 0.04em;
  }
  .theme-preset:hover { border-color: var(--accent); color: var(--accent); }

  .build-row {
    display: flex; align-items: center; gap: 12px;
    background: var(--panel-2); border: 1px solid var(--line);
    padding: 10px 12px;
  }
  .build-row-label {
    flex: 1; display: flex; flex-direction: column; gap: 2px;
    color: var(--ink); font-size: 12px;
  }
  .build-row-label .role-name {
    font-family: 'JetBrains Mono', monospace;
    font-size: 11px; color: var(--accent);
  }
  .build-row-label .role-desc { color: var(--dim); font-size: 10.5px; }
  .build-select {
    background: var(--bg); color: var(--ink);
    border: 1px solid var(--line); padding: 6px 8px;
    font-family: 'JetBrains Mono', monospace; font-size: 11px;
    min-width: 280px;
  }
  .build-select:focus { outline: 1px solid var(--accent); }
  .build-checkbox { width: 18px; height: 18px; accent-color: var(--accent); cursor: pointer; }

  /* Live background swatch — its background-color/-image are injected at
     runtime (#bg-preview rule) from the real generated layer; this only sets
     the chrome + sample-text layout. Min-height keeps mesh blobs readable. */
  .bg-preview {
    position: relative; margin-top: 12px; min-height: 96px;
    border: 1px solid var(--line); border-radius: 4px; overflow: hidden;
    display: flex; flex-direction: column; justify-content: center;
    gap: 4px; padding: 14px 16px;
  }
  .bg-preview strong {
    font-family: 'JetBrains Mono', monospace; font-size: 10px;
    text-transform: uppercase; letter-spacing: 0.08em; opacity: 0.7;
  }
  .bg-preview span { font-size: 13px; }

  /* Style picker — clickable live thumbnails of each visual style, rendered
     in scaled iframes (see app.js renderStylePicker). Stacks under its label
     rather than sitting inline like the other build-rows. */
  .build-row-stack { flex-direction: column; align-items: stretch; }
  .style-picker { display: flex; gap: 10px; width: 100%; }
  .style-card {
    flex: 1 1 0; min-width: 0; display: flex; flex-direction: column; gap: 6px;
    padding: 6px; background: var(--bg); border: 1px solid var(--line);
    border-radius: 4px; cursor: pointer; font-family: inherit;
  }
  .style-card:hover { border-color: var(--accent); }
  .style-card.selected { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
  /* Fixed-size viewport that clips the scaled-down full-width page render. */
  .style-card-frame {
    width: 100%; height: 132px; overflow: hidden; position: relative;
    border: 1px solid var(--line); border-radius: 2px; background: #fff;
  }
  .style-card-frame iframe {
    position: absolute; top: 0; left: 0; width: 660px; height: 528px;
    border: 0; transform-origin: 0 0; pointer-events: none;
  }
  .style-card-label {
    font-family: 'JetBrains Mono', monospace; font-size: 11px;
    text-align: center; color: var(--ink); text-transform: capitalize;
  }
  .style-card.selected .style-card-label { color: var(--accent); }

  /* ====== Plan UI ============================================
     Plan name chip shown in the user menu beneath the email,
     and Account/Plan modal. Reuses the .feedback-modal shell so
     spacing and chrome line up with the other modals.            */
  .menu-user-plan {
    display: inline-block;
    margin-top: 4px;
    font-size: 10px; text-transform: uppercase; letter-spacing: 0.08em;
    color: var(--bg); background: var(--accent);
    padding: 1px 6px; border-radius: 2px;
  }
  .menu-user-plan[data-plan="pro"] { background: var(--accent); color: var(--bg); }
  .menu-user-plan[data-plan="free"] { background: var(--panel-2); color: var(--ink); border: 1px solid var(--line); }

  .account-modal { max-width: 600px; }
  .account-summary {
    display: flex; align-items: flex-start; justify-content: space-between;
    gap: 12px;
  }
  .account-eyebrow {
    font-size: 10px; text-transform: uppercase; letter-spacing: 0.08em;
    color: var(--dim); margin-bottom: 4px;
  }
  .account-plan-name {
    font-size: 18px; color: var(--ink); font-weight: 500;
  }
  .account-features {
    width: 100%; border-collapse: collapse; font-size: 12px;
  }
  .account-features th, .account-features td {
    padding: 8px 6px; border-bottom: 1px solid var(--line); text-align: left;
  }
  .account-features th {
    color: var(--dim); font-weight: 600; font-size: 10px;
    text-transform: uppercase; letter-spacing: 0.06em;
  }
  .account-features td { color: var(--ink); }
  .account-col-usage, .account-col-limit {
    text-align: right; white-space: nowrap;
    font-variant-numeric: tabular-nums;
  }
  .account-features .feature-warn { color: var(--accent); }
  .account-features .feature-over { color: #ff7676; }

  /* Danger zone — account deletion. Separated from the plan table by a
     rule so it reads as a distinct, destructive action. */
  .account-danger {
    display: flex; align-items: center; justify-content: space-between;
    gap: 16px; margin-top: 18px; padding-top: 16px;
    border-top: 1px solid var(--line);
  }
  .account-danger p { font-size: 12px; color: var(--dim); margin: 2px 0 0; }
  .account-danger .btn.danger { white-space: nowrap; flex-shrink: 0; }
  .account-delete-error {
    color: #ff7676; font-size: 12px; margin: 8px 0 0; text-align: right;
  }
  .account-legal {
    font-size: 11px; color: var(--dim); text-align: center; margin: 14px 0 0;
  }
  .account-legal a { color: var(--dim); text-decoration: underline; }
  .account-legal a:hover { color: var(--ink); }

  /* Upgrade modal — informational + a "coming soon" footer. */
  .upgrade-modal { max-width: 480px; }
  .upgrade-message { color: var(--ink); font-size: 13px; line-height: 1.5; }
  .upgrade-perks {
    margin: 0; padding-left: 20px;
    color: var(--ink); font-size: 12px; line-height: 1.7;
  }
  .upgrade-soon {
    color: var(--dim); font-size: 11px; font-style: italic;
    border-top: 1px solid var(--line); padding-top: 10px; margin: 0;
  }

  /* Buy Credits button — accented like Generate but distinct (warn hue). */
  .canvas-btn-credits {
    color: color-mix(in srgb, var(--warn) 70%, black);
    border-color: color-mix(in srgb, var(--warn) 40%, var(--line));
  }
  .canvas-btn-credits:hover {
    border-color: var(--warn);
    color: var(--warn);
  }

  /* Buy Credits modal. */
  .credits-modal { max-width: 460px; }
  .credits-intro, .credits-summary {
    color: var(--ink); font-size: 12px; line-height: 1.5; margin: 0 0 12px;
  }
  .credits-packs { display: flex; flex-direction: column; gap: 10px; }
  /* Group heading separating the code-gen and compile pack lists. */
  .credits-group-title {
    font-size: 10px; text-transform: uppercase; letter-spacing: 0.08em;
    color: var(--dim); margin-top: 6px;
  }
  .credits-group-title:first-child { margin-top: 0; }
  /* A selectable pack row: label/price on the left, a quantity stepper on
     the right. Highlights when one or more are in the cart. */
  .credits-pack {
    display: flex; justify-content: space-between; align-items: center;
    gap: 12px; padding: 10px 14px;
    border: 1px solid var(--line); background: var(--panel-2);
    transition: border-color 0.12s, background 0.12s;
  }
  .credits-pack.is-selected { border-color: var(--accent-dim); background: var(--panel); }
  .credits-pack-info { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
  .credits-pack-info strong { font-size: 14px; color: var(--ink); }
  .credits-pack-info span { font-size: 13px; color: var(--accent); font-variant-numeric: tabular-nums; }
  .credits-qty { display: flex; align-items: center; gap: 8px; flex: none; }
  .credits-qty-btn {
    width: 26px; height: 26px; padding: 0; line-height: 1;
    display: inline-flex; align-items: center; justify-content: center;
    font-size: 16px; cursor: pointer;
    background: var(--panel-2); color: var(--ink);
    border: 1px solid var(--line); border-radius: 4px;
    transition: all 0.12s;
  }
  .credits-qty-btn:hover:not(:disabled) { border-color: var(--accent-dim); color: var(--accent); }
  .credits-qty-btn:disabled { opacity: 0.4; cursor: default; }
  .credits-qty-val {
    min-width: 18px; text-align: center; font-size: 13px;
    color: var(--ink); font-variant-numeric: tabular-nums;
  }
  /* Cart footer — running total + Continue, separated from the pack list. */
  .credits-cart-foot {
    display: flex; align-items: center; justify-content: space-between;
    gap: 12px; margin-top: 14px; padding-top: 12px;
    border-top: 1px solid var(--line);
  }
  .credits-total { font-size: 14px; color: var(--ink); font-variant-numeric: tabular-nums; }
  /* Itemised summary on the payment step. */
  .credits-summary-row {
    display: flex; justify-content: space-between; gap: 12px;
    font-size: 13px; color: var(--ink); padding: 2px 0;
    font-variant-numeric: tabular-nums;
  }
  .credits-summary-total {
    margin-top: 6px; padding-top: 8px; border-top: 1px solid var(--line);
    font-weight: 600;
  }
  .credits-loading { color: var(--dim); font-size: 12px; }
  .credits-error {
    color: #ff7676; font-size: 12px; margin: 10px 0 0;
  }
  .credits-done { color: var(--ink); font-size: 13px; line-height: 1.5; }
  #credits-payment-element { margin: 4px 0 8px; min-height: 40px; }
  /* "Powered by Stripe" badge, bottom-left of the pack-picker step. */
  .credits-stripe-badge { margin-top: 16px; text-align: left; }
  .credits-stripe-badge img { display: block; opacity: 0.9; }

/* ============================================================
 * Guided product tour (coachmarks) — see public/tour.js
 * A spotlight overlay (dims the page except the highlighted
 * element) plus a positioned popover card. Inherits the app's
 * theme via the same CSS custom properties used elsewhere.
 * ============================================================ */
  .tour-overlay {
    position: fixed; inset: 0; z-index: 100000;
    display: none; background: transparent;
  }
  .tour-overlay.open { display: block; }
  /* Welcome / finish steps (no target) get a plain dimmed backdrop. */
  .tour-overlay.tour-centered { background: rgba(0, 0, 0, 0.55); }

  /* The spotlight: a transparent box over the target whose huge box-shadow
     dims everything else. Pointer-events off so it's purely visual; the
     overlay itself blocks interaction with the app during the tour. */
  .tour-spotlight {
    position: fixed; pointer-events: none;
    border-radius: 8px;
    box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.55),
                0 0 0 2px var(--accent),
                0 0 18px 2px color-mix(in srgb, var(--accent) 60%, transparent);
    transition: left 0.18s ease, top 0.18s ease,
                width 0.18s ease, height 0.18s ease, opacity 0.15s ease;
  }

  .tour-popover {
    position: fixed; width: 300px; max-width: calc(100vw - 24px);
    background: var(--panel); color: var(--ink);
    border: 1px solid var(--line); border-radius: 12px;
    padding: 16px 18px;
    box-shadow: 0 12px 40px rgba(0, 0, 0, 0.45);
    font-family: inherit;
    transition: left 0.18s ease, top 0.18s ease;
  }
  .tour-step-count {
    font-size: 11px; letter-spacing: 0.04em; text-transform: uppercase;
    color: var(--dim); margin-bottom: 6px;
  }
  .tour-title {
    margin: 0 0 8px; font-size: 16px; font-weight: 600;
    color: var(--ink);
  }
  .tour-body {
    margin: 0 0 16px; font-size: 13px; line-height: 1.55; color: var(--ink);
  }
  .tour-body strong { color: var(--accent); font-weight: 600; }
  .tour-actions {
    display: flex; align-items: center; justify-content: space-between; gap: 10px;
  }
  .tour-nav { display: flex; gap: 8px; }
  .tour-actions button {
    font: inherit; font-size: 13px; cursor: pointer;
    border-radius: 7px; padding: 7px 14px; border: 1px solid var(--line);
    background: var(--panel-2); color: var(--ink);
    transition: background 0.12s ease, border-color 0.12s ease;
  }
  .tour-actions button:hover { border-color: var(--accent-dim); }
  .tour-skip { color: var(--dim); background: transparent; border-color: transparent; padding-left: 0; }
  .tour-skip:hover { color: var(--ink); }
  .tour-next {
    background: var(--accent) !important; color: var(--accent-text) !important;
    border-color: var(--accent) !important; font-weight: 600;
  }
  .tour-next:hover { filter: brightness(1.08); }

  /* Caret pointing from the popover toward the highlighted element. */
  .tour-caret {
    position: absolute; width: 12px; height: 12px;
    background: var(--panel);
    border: 1px solid var(--line);
    transform: rotate(45deg);
  }
  .tour-caret-bottom { top: -7px; border-right: none; border-bottom: none; }
  .tour-caret-top    { bottom: -7px; border-left: none; border-top: none; }
  .tour-caret-right  { left: -7px; border-top: none; border-right: none; }
  .tour-caret-left   { right: -7px; border-bottom: none; border-left: none; }

  /* "Take a tour" button in the help modal header. */
  .help-tour-btn {
    font: inherit; font-size: 12px; cursor: pointer;
    margin-left: auto; margin-right: 8px;
    border-radius: 7px; padding: 6px 12px;
    border: 1px solid var(--accent-dim);
    background: var(--panel-2); color: var(--accent);
    transition: background 0.12s ease, border-color 0.12s ease;
  }
  .help-tour-btn:hover { border-color: var(--accent); }
