      @font-face {
        font-family: "Fira Code";
        font-style: normal; font-weight: 400; font-display: swap;
        src: url("vendor/fonts/fira-code-latin-400-normal.woff2") format("woff2");
      }
      @font-face {
        font-family: "Fira Code";
        font-style: normal; font-weight: 500; font-display: swap;
        src: url("vendor/fonts/fira-code-latin-500-normal.woff2") format("woff2");
      }
      @font-face {
        font-family: "Fira Code";
        font-style: normal; font-weight: 700; font-display: swap;
        src: url("vendor/fonts/fira-code-latin-700-normal.woff2") format("woff2");
      }
      @font-face {
        font-family: "Inter";
        font-style: normal; font-weight: 400; font-display: swap;
        src: url("vendor/fonts/inter-latin-400-normal.woff2") format("woff2");
      }
      @font-face {
        font-family: "Inter";
        font-style: normal; font-weight: 700; font-display: swap;
        src: url("vendor/fonts/inter-latin-700-normal.woff2") format("woff2");
      }
      :root {
        /* Warm neutral grey palette (stone family). Same lightness ramp as
           zinc but with a touch of red+yellow under the surface — the page
           reads as "sand" rather than "ash" without sliding back to brown.
           Variable names retain the historical `brown`/`cream` prefixes so
           existing selectors don't churn; see OUTSTANDING_QUESTIONS for the
           rename follow-up.

           Dark mode (below) inverts the lightness ramp on every named
           token. The new `--surface` / `--surface-2` / `--ink-mute` /
           `--card-hover-bg` / `--danger-*` tokens factor out colours
           that used to be hard-coded (#fff, #71717a, #ebe0cd, #c0392b)
           so they can flip in dark mode without touching every rule. */
        --brown: #57534e;          /* stone-600 — primary accent */
        --brown-dark: #44403c;     /* stone-700 — hover / strong text */
        --brown-darker: #292524;   /* stone-800 — headings */
        --brown-light: #a8a29e;    /* stone-400 — muted ink */
        --brown-border: #d6d3d1;   /* stone-300 — borders */
        --cream: #fafaf9;          /* stone-50  — page background */
        --cream-2: #f5f5f4;        /* stone-100 — cards / panels */
        --text: #1c1917;           /* stone-900 — body text */
        --surface: #ffffff;        /* opaque card / input / modal surface */
        --surface-2: #f5f5f4;      /* same as cream-2 — secondary surface */
        --ink-mute: #71717a;       /* muted small-text colour */
        --card-hover-bg: #ebe0cd;  /* grid-card hover (warm cream) */
        --danger: #c0392b;         /* danger ink */
        --danger-strong: #a02a1f;  /* danger hover ink */
        --danger-ink: #7f1d1d;     /* danger text on --danger-bg */
        --danger-bg: #fdecea;      /* danger background */
        --danger-hover-bg: #f8d7d3;
        /* Warning (red-tinged orange, used by the persistence-at-risk
           banner) — distinct from the gentler "notice" (amber/yellow) */
        --warn-bg: #fde9e2;
        --warn-border: #d99477;
        --warn-ink: #6a2210;
        --notice-bg: #fef3c7;
        --notice-border: #d97706;
        --notice-ink: #92400e;
        --ok-bg: rgba(74,107,42,.18);
        --ok-ink: #4a6b2a;
        --code-tint: rgba(0,0,0,.06);
        --legend-tint: rgba(255,255,255,.92);
        --legend-shadow: rgba(58,40,23,.1);
        /* Reverse-video chip — used by row-count pills, focal-table
           name chip in schema-editor titles, and the saved-choice
           badge in the schema editor's column rows. Dark plate +
           light ink in light theme; flipped in dark. */
        --chip-bg: #1f1d1c;
        --chip-ink: #fafaf9;
        --mono: "Fira Code", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
        --sans: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
        color-scheme: light;
      }
      /* Dark mode. Applies when (a) the document carries `data-theme=
         "dark"` (the manual toggle pill writes this attribute and
         persists the choice to localStorage), OR (b) no manual choice
         exists and the OS reports a dark colour-scheme preference.
         Both rules write the same overrides — the auto rule is the
         fallback the toggle can override either way.

         Token values mirror the stone ramp inverted: page background
         drops to stone-950, surfaces to stone-800, ink rises to
         stone-100. The accent stays in the stone-300/400 range so it
         reads as the same family across both modes. */
      html[data-theme="dark"] {
        --brown: #d6d3d1;          /* primary accent — light enough on dark bg */
        --brown-dark: #e7e5e4;     /* hover / strong text */
        --brown-darker: #fafaf9;   /* headings — near-white */
        --brown-light: #78716c;    /* muted ink (darker than light mode) */
        --brown-border: #44403c;   /* borders — stone-700 on dark surface */
        --cream: #0c0a09;          /* page background — stone-950 */
        --cream-2: #1c1917;        /* cards / panels — stone-900 */
        --text: #f5f5f4;           /* body text */
        --surface: #292524;        /* card / input / modal surface */
        --surface-2: #1c1917;
        --ink-mute: #a8a29e;
        --card-hover-bg: #3f3a36;
        --danger: #f87171;
        --danger-strong: #fca5a5;
        --danger-ink: #fecaca;
        --danger-bg: #3a1414;
        --danger-hover-bg: #4a1c1c;
        /* Dark equivalents — pull saturation down (less neon), drop bg
           ~20× to read as a tinted dark panel rather than a glow. */
        --warn-bg: #3a1f10;
        --warn-border: #6e3a26;
        --warn-ink: #f6a48a;
        --notice-bg: #332810;
        --notice-border: #6e5320;
        --notice-ink: #f5c97a;
        --ok-bg: rgba(120,180,90,.18);
        --ok-ink: #a3d977;
        --code-tint: rgba(255,255,255,.08);
        --legend-tint: rgba(28,25,23,.92);
        --legend-shadow: rgba(0,0,0,.4);
        --chip-bg: #fafaf9;
        --chip-ink: #1f1d1c;
        color-scheme: dark;
      }
      @media (prefers-color-scheme: dark) {
        html:not([data-theme="light"]) {
          --brown: #d6d3d1;
          --brown-dark: #e7e5e4;
          --brown-darker: #fafaf9;
          --brown-light: #78716c;
          --brown-border: #44403c;
          --cream: #0c0a09;
          --cream-2: #1c1917;
          --text: #f5f5f4;
          --surface: #292524;
          --surface-2: #1c1917;
          --ink-mute: #a8a29e;
          --card-hover-bg: #3f3a36;
          --danger: #f87171;
          --danger-strong: #fca5a5;
          --danger-ink: #fecaca;
          --danger-bg: #3a1414;
          --danger-hover-bg: #4a1c1c;
          --warn-bg: #3a1f10;
          --warn-border: #6e3a26;
          --warn-ink: #f6a48a;
          --notice-bg: #332810;
          --notice-border: #6e5320;
          --notice-ink: #f5c97a;
          --ok-bg: rgba(120,180,90,.18);
          --ok-ink: #a3d977;
          --code-tint: rgba(255,255,255,.08);
          --legend-tint: rgba(28,25,23,.92);
          --legend-shadow: rgba(0,0,0,.4);
          --chip-bg: #fafaf9;
          --chip-ink: #1f1d1c;
          color-scheme: dark;
        }
      }
      /* Theme-toggle pill — small fixed-position button that cycles
         auto → light → dark. Sits to the LEFT of the data-source
         pill on every page so cog + data-source + theme make a
         three-button cluster in the corner. The glyph (☀ / ☾ / ◐)
         is set by frontend/lib/theme.js when the data-theme attr
         flips. */
      .theme-toggle {
        /* Sits between the cog (right: 1.25rem, width 3.6rem) and the
           mode pill (right: 10.45rem). Pinned to the right of BOTH
           data-source pills so the offline-pill animation-in doesn't
           cover it. No dynamic JS positioning needed at this spot. */
        position: fixed; top: 1.25rem; right: 5.85rem; z-index: 11;
        height: 3.6rem; width: 3.6rem; padding: 0; line-height: 1;
        display: inline-flex; align-items: center; justify-content: center;
        font-family: var(--sans); font-size: 1.8rem; font-weight: 700;
        color: var(--brown-darker); background: var(--cream);
        border: 1px solid var(--brown-border); border-radius: 50%;
        cursor: pointer; transition: background .2s ease;
        text-transform: none; letter-spacing: 0; margin: 0;
      }
      /* The icon is an inline SVG (see frontend/lib/theme.js). Block
         layout + width/height attributes on the SVG keep it precisely
         centred via the parent's flex centring — no per-glyph offsets
         needed (the old unicode glyphs drifted off-centre per font). */
      .theme-toggle > svg { display: block; width: 1.8rem; height: 1.8rem; }
      .theme-toggle:hover { background: var(--cream-2); }
      .theme-toggle:focus-visible { outline: 2px solid var(--brown); outline-offset: 2px; }
      /* Public viewer: no cog / mode pill, so pin the toggle into the grid
         title bar — right-attached with the bar's own 1rem horizontal
         padding, and sized + top-aligned to the bar's controls (.75rem top
         padding, 3rem control height) so it reads as part of the bar. */
      body.public-viewer .theme-toggle {
        top: .75rem; right: 1rem; height: 3rem; width: 3rem; font-size: 1.5rem;
      }
      body.public-viewer .theme-toggle > svg { width: 1.5rem; height: 1.5rem; }

      /* Grid-cell state colours: light mode uses warm peach tints
         (#ebdfca etc) that read as "highlighted but related to the
         page colour family". In dark mode the warm tints would look
         alien on a stone-900 surface; we override the same selectors
         to give them slightly-warm-on-dark tones instead. Only the
         backgrounds need overrides — the ink colour tokens already
         flipped via the palette block above. */
      html[data-theme="dark"] .grid-table tbody td.frozen[data-selected="1"],
      html[data-theme="dark"] .grid-table tbody tr td[data-selected="1"] { background: #3a3530; }
      html[data-theme="dark"] body.grid-kb-nav .grid-table tbody td.frozen[data-focused="1"],
      html[data-theme="dark"] body.grid-kb-nav .grid-table tbody tr td[data-focused="1"],
      html[data-theme="dark"] body:not(.grid-kb-nav) .grid-table tbody td.frozen:hover { background: #4a3f33; }
      html[data-theme="dark"] .grid-table tbody td.frozen[data-readonly="1"] { background: #2a2723; }
      html[data-theme="dark"] .grid-table tbody td.frozen[data-readonly="1"][data-selected="1"] { background: #3a342a; }
      html[data-theme="dark"] body.grid-kb-nav .grid-table tbody td.frozen[data-readonly="1"][data-focused="1"],
      html[data-theme="dark"] body:not(.grid-kb-nav) .grid-table tbody td.frozen[data-readonly="1"]:hover { background: #44403c; }
      /* Dark-mode row stripe — same gating as light mode (kb-nav shows
         keyboard-focused row, mouse mode shows mouse-hovered row). One
         row at a time. */
      html[data-theme="dark"] body.grid-kb-nav .grid-table tbody tr[data-focused-row="1"] td,
      html[data-theme="dark"] body.grid-kb-nav .grid-table tbody tr[data-focused-row="1"] td[data-readonly="1"],
      html[data-theme="dark"] body:not(.grid-kb-nav) .grid-table tbody tr:hover td,
      html[data-theme="dark"] body:not(.grid-kb-nav) .grid-table tbody tr:hover td[data-readonly="1"] { background: #3a3026; }
      /* Per-cell current-cell — kb-nav shows the keyboard's focused
         cell, mouse mode shows the cell directly under the pointer.
         Mouse-hover "steals" the current cell from keyboard when the
         pointer moves to a different cell. The `tr td` and the
         `td[data-readonly]` variants are listed together so a hovered
         readonly cell wins over the general `td[data-readonly]` rule
         in light mode (specificity (0,4,4) here beats light's (0,4,3)
         readonly-hover). */
      html[data-theme="dark"] body.grid-kb-nav .grid-table tbody tr td[data-focused="1"],
      html[data-theme="dark"] body.grid-kb-nav .grid-table tbody tr td[data-readonly="1"][data-focused="1"],
      html[data-theme="dark"] body:not(.grid-kb-nav) .grid-table tbody td:hover,
      html[data-theme="dark"] body:not(.grid-kb-nav) .grid-table tbody td[data-readonly="1"]:hover { outline: 2px solid color-mix(in srgb, var(--brown) 55%, transparent); outline-offset: -2px; background: #4a3f33; }
      /* Selection tint. */
      html[data-theme="dark"] .grid-table tbody tr td[data-selected="1"] { background: #3a3530; }
      html[data-theme="dark"] .grid-table tbody tr td[data-readonly="1"] { background: #25211e; }
      /* Column separator: light mode uses a peach hairline (#efe6d3)
         that just whispers between cells. The default token would
         flip that to --brown-border (#44403c) on dark which reads
         too loud; this override is closer to the cell background so
         the separator is felt more than seen. */
      html[data-theme="dark"] .grid-table tbody td { --grid-col-sep: #3d3833; }
      html[data-theme="dark"] .grid-table thead th { border-right-color: #4a443e; }
      @media (prefers-color-scheme: dark) {
        html:not([data-theme="light"]) .grid-table tbody td { --grid-col-sep: #3d3833; }
        html:not([data-theme="light"]) .grid-table thead th { border-right-color: #4a443e; }
        html:not([data-theme="light"]) .grid-table tbody td.frozen[data-selected="1"],
        html:not([data-theme="light"]) .grid-table tbody tr td[data-selected="1"] { background: #3a3530; }
        html:not([data-theme="light"]) body.grid-kb-nav .grid-table tbody td.frozen[data-focused="1"],
        html:not([data-theme="light"]) body.grid-kb-nav .grid-table tbody tr td[data-focused="1"],
        html:not([data-theme="light"]) body:not(.grid-kb-nav) .grid-table tbody td.frozen:hover { background: #4a3f33; }
        html:not([data-theme="light"]) .grid-table tbody td.frozen[data-readonly="1"] { background: #2a2723; }
        html:not([data-theme="light"]) .grid-table tbody td.frozen[data-readonly="1"][data-selected="1"] { background: #3a342a; }
        html:not([data-theme="light"]) body.grid-kb-nav .grid-table tbody td.frozen[data-readonly="1"][data-focused="1"],
        html:not([data-theme="light"]) body:not(.grid-kb-nav) .grid-table tbody td.frozen[data-readonly="1"]:hover { background: #44403c; }
        html:not([data-theme="light"]) body.grid-kb-nav .grid-table tbody tr[data-focused-row="1"] td,
        html:not([data-theme="light"]) body.grid-kb-nav .grid-table tbody tr[data-focused-row="1"] td[data-readonly="1"],
        html:not([data-theme="light"]) body:not(.grid-kb-nav) .grid-table tbody tr:hover td,
        html:not([data-theme="light"]) body:not(.grid-kb-nav) .grid-table tbody tr:hover td[data-readonly="1"] { background: #3a3026; }
        html:not([data-theme="light"]) body.grid-kb-nav .grid-table tbody tr td[data-focused="1"],
        html:not([data-theme="light"]) body.grid-kb-nav .grid-table tbody tr td[data-readonly="1"][data-focused="1"],
        html:not([data-theme="light"]) body:not(.grid-kb-nav) .grid-table tbody td:hover,
        html:not([data-theme="light"]) body:not(.grid-kb-nav) .grid-table tbody td[data-readonly="1"]:hover { outline: 2px solid color-mix(in srgb, var(--brown) 55%, transparent); outline-offset: -2px; background: #4a3f33; }
        html:not([data-theme="light"]) .grid-table tbody tr td[data-selected="1"] { background: #3a3530; }
        html:not([data-theme="light"]) .grid-table tbody tr td[data-readonly="1"] { background: #25211e; }
      }
      html, body, button, input, select, textarea, pre, code {
        font-family: var(--mono);
        font-variant-ligatures: contextual;
      }
      /* `scale-text` is applied statically on `<html>` in the markup
         above (and re-asserted in frontend/app.js for safety) so the
         first paint already runs at 1.2× — without it the status
         pills + brand text briefly rendered at 10px before the JS
         router added the class. Milligram's root is 62.5% (10px);
         this lifts it to 75% (12px), and every rem-based rule in the
         cascade picks up the bump from this one declaration. Applies
         on every route (grid / grid2 / home / settings). */
      html.scale-text { font-size: 75%; }
      body { padding: 1.5rem; background: var(--cream); color: var(--text); }
      /* Drop Milligram's `.container { max-width: 112rem }` cap on every
         route — the home page in particular now needs the room for a side
         rail of default-grid links + a responsive grid of saved-grid cards.
         The grid page already overrides this further via `body.on-grid`. */
      #root.container, .container { max-width: none; padding: 0; }
      /* Inter for headings so each page doesn't read like a settings panel.
         Fira Code stays on body + table cells where vertical alignment of
         identifiers matters. */
      h1, h2, h3, h4, h5, h6 { color: var(--brown-darker); font-family: var(--sans); letter-spacing: -0.01em; }
      h2 { font-size: 2.6rem; margin-bottom: 1rem; }

      /* Three-state diamond used in the grid-config dialog. The icon is
         driven by adding/removing icon-diamond-{off,rw,ro} classes; the
         SVG markup is generated in JS so colour/lock vary per state. */
      .icon-diamond { display: inline-block; vertical-align: middle; cursor: pointer; flex-shrink: 0; }
      .icon-diamond svg { width: 1.4rem; height: 1.4rem; display: block; }
      .icon-diamond-off { color: var(--brown-border); }
      .icon-diamond-off:hover { color: var(--brown); }
      .icon-diamond-rw { color: var(--brown); }
      .icon-diamond-ro { color: var(--brown); }
      /* The read-only padlock glyph is drawn ON the (currentColor = --brown)
         diamond, so it must contrast with --brown in BOTH themes. --cream
         flips light↔dark with the theme (≈white in light, near-black in
         dark), so the lock stays visible — fixing the dark-mode case where a
         hardcoded near-white lock vanished on the light dark-mode diamond
         and read-only looked identical to editable. */
      .icon-diamond-lock-fill { fill: var(--cream); }
      .icon-diamond-lock-arc { stroke: var(--cream); fill: none; }

      /* "pills shown:" row under the subgrid mode radios. Compact so it
         doesn't crowd the picker; label + dropdown share a single line. */
      .related-pill-limit { display: flex; align-items: center; gap: .35rem; margin: .1rem 0 .25rem 0; }
      .related-pill-limit > .muted { font-size: 1rem; line-height: 1; }
      select.related-pill-limit-select {
        font-size: 1rem; line-height: 1; height: auto;
        padding: .1rem .35rem; margin: 0; width: auto;
        background: transparent; border: 1px solid var(--brown-border);
        border-radius: 3px;
      }

      /* Two-state half-pill toggle used in the Selected panel sub-tiles:
         "on" = filled (column appears in pill summaries), "off" = outline. */
      .icon-pill { display: inline-block; vertical-align: middle; cursor: pointer; flex-shrink: 0; }
      .icon-pill svg { width: 1.3rem; height: 1.3rem; display: block; }
      /* Muted grey, matching the related-table "(via …)" label so the pill
         toggle reads as secondary chrome rather than primary UI. */
      .icon-pill-off { color: var(--brown-light, #a1a1aa); opacity: .55; }
      .icon-pill-off:hover { opacity: 1; }
      .icon-pill-on { color: var(--brown-light, #a1a1aa); }

      /* Grid-config dialog — column picker (left) + selected-sequence (right). */
      /* ~20% narrower than the previous 92vw — the two side-by-side
         panels comfortably hold their content at 74vw without the
         dialog dominating the page. */
      .modal-grid-config { min-width: 720px; max-width: 74vw; width: 74vw; }
      /* grid2 builder — shares modal-grid-config sizing but lays out
         vertically: name + base picker, then GRID COLUMNS strip, then
         two side-by-side panes (available + GROUPS). */
      /* Grid2's body has an extra SELECTED strip on top, which pushed
         the inner panes past the viewport — the fixed `60vh` height on
         `.gcfg-available` (inherited from the grid1 styles) bottomed
         out below the bottom of the modal and the user couldn't reach
         it. Make the body fill the remaining modal height (min-height:0
         + overflow:hidden so flex layouts shrink properly) and let the
         inner panes scroll within their grid cells. */
      /* `.modal-grid2.modal` (3-class specificity) beats the shared
         `.modal-body.gcfg-body { padding: 1rem 1.25rem }` rule that
         was sneaking white padding around the panes. Body is now a
         flex column: full-width strip on top, two-pane row below. */
      .modal-grid2.modal .g2-body {
        padding: 0; display: flex; flex-direction: column; gap: 0;
        flex: 1 1 auto; min-height: 0; overflow: hidden;
      }
      .modal-grid2 .g2-below {
        flex: 1 1 auto; min-height: 0;
        display: flex; flex-direction: row;
      }
      /* 3/10 + 7/10 split below the title — mirrors the schema
         editor's `.se-pane-left` + `.se-pane-right` two-pane shape
         and uses the same vertical scroll behaviour. The left pane
         carries BASE / CONDITIONS / AGGREGATIONS; the right pane
         carries DISPLAY (horizontal column strip) + AVAILABLE
         (column groups). */
      .modal-grid2 .g2-pane {
        min-width: 0; min-height: 0; padding: 1rem 1.25rem;
        display: flex; flex-direction: column; gap: 1.25rem;
        overflow: auto;
      }
      /* Below-strip panes each scroll independently. Left pane is
         cream-backed (BASE / CONDITIONS / AGGREGATIONS); right pane
         is white (AVAILABLE — the column pool). The single vertical
         border-right on the left pane is the only divider between
         them. */
      .modal-grid2 .g2-pane-left  { flex: 1 1 0; background: var(--cream); border-right: 1px solid var(--brown-border); }
      .modal-grid2 .g2-pane-right { flex: 1 1 0; background: var(--cream); }

      /* Mobile: stack into one column with the order
           left-pane → SELECTED strip → right-pane.
         `.g2-below` flattens via `display: contents` so its two child
         panes participate in `.g2-body`'s flex layout directly,
         letting CSS `order` slot the strip between them without
         touching the markup. The left pane's vertical divider becomes
         a horizontal one; the right pane gains a matching top border
         so the strip sits inside a clear band. */
      @media (max-width: 720px) {
        .modal-grid-config { min-width: 0; max-width: 100vw; width: 100vw; }
        .modal-grid2.modal { min-width: 0; max-width: 100vw; max-height: 100vh; }
        /* Header has too many controls (title + grid name + SAVE + CANCEL)
           to fit a phone width on one line — let it wrap. */
        .modal-grid2 .modal-header { flex-wrap: wrap; row-gap: .5rem; }
        .modal-grid2.modal .g2-body { overflow-y: auto; }
        .modal-grid2 .g2-below { display: contents; }
        .modal-grid2 .g2-strip-wrap {
          order: 2;
          border-top: 1px solid var(--brown-border);
        }
        .modal-grid2 .g2-pane-left {
          order: 1; flex: 0 0 auto;
          border-right: 0;
        }
        .modal-grid2 .g2-pane-right {
          order: 3; flex: 1 1 auto;
        }
        /* Panes scroll with the body on mobile rather than each
           independently — the body owns the single vertical scrollbar. */
        .modal-grid2 .g2-pane { overflow: visible; }
      }
      /* Full-width strip wrapper across the top of the body. Its
         border-bottom is the modal's main horizontal split. */
      .modal-grid2 .g2-strip-wrap {
        flex: 0 0 auto;
        padding: 0 1.25rem;
        background: var(--surface);
        border-bottom: 1px solid var(--brown-border);
      }
      /* Section headings inside each pane reuse the schema editor's
         `.se-section` chrome — same h3 rule (`.se-section h3` →
         1.1rem brown-darker). Adding the `.se-section-title` class
         is a no-op visually but reads cleaner in the JSX-style
         element list. */
      .modal-grid2 .se-section { margin: 0; }
      .modal-grid2 .se-section + .se-section { margin-top: 0; }
      /* The two h3 rules `.se-section h3` and `.modal-grid2 .g2-pane h3`
         compete; pin the size here so grid2's h3 doesn't override
         the smaller schema-editor size. */
      .modal-grid2 .g2-pane h3.se-section-title {
        margin: 0 0 .5rem; font-size: 1.1rem; color: var(--brown-darker);
      }
      /* BASE section's table picker styling — same height + font as
         the other pickers and the cond-row controls. */
      .modal-grid2 .g2-pane-left .g2-base-section { display: flex; align-items: center; gap: .5rem; }
      .modal-grid2 .g2-pane-left .g2-base-prefix {
        font-size: 1.4rem; color: var(--brown-darker);
        font-weight: 400;
      }
      .modal-grid2 .g2-pane-left .gcfg-base {
        margin: 0; height: 2.6rem; padding: 0 1.6rem 0 .5rem;
        font: inherit; font-size: 1.6rem; line-height: 1;
        background-image: none; width: auto; max-width: 18rem;
      }
      .modal-grid2 .g2-pane-left .g2-base-conds,
      .modal-grid2 .g2-pane-left .g2-base-conds .cond-row,
      .modal-grid2 .g2-pane-left .g2-base-conds .cond-row select,
      .modal-grid2 .g2-pane-left .g2-base-conds .cond-row input,
      .modal-grid2 .g2-pane-left .g2-base-conds .cond-summary-op { font-size: 1.4rem; }
      /* grid2 reuses the existing `.gcfg-header*` rules for its title
         row (ᚙ + grid name input + BASE TABLE picker + save / cancel),
         so the two builders read as siblings. Only g2-specific things
         remain below. */
      .modal-grid2 .g2-select { height: 2.4rem; padding: 0 .5rem; margin: 0; }
      /* grid2's header puts "A GRID ROW PER" + base picker FIRST (left),
         followed by the name input. Override the grid1 centering rule
         that the shared `.gcfg-header-base` carries — for grid2 we want
         the label hugged to the left edge with a tight gap to the rest
         of the header. */
      .modal-grid2 .gcfg-header-base {
        flex: 0 0 auto; justify-content: flex-start; gap: .75rem;
        margin-left: -.25rem; padding-left: 0;
      }
      .modal-grid2 .gcfg-header { gap: .5rem; }
      /* Base-table picker height matches the grid-name input next to
         it — without this override, Milligram's bare `<select>` rule
         applies a 3.8rem height that towers over the 2.6rem input.
         AGENTS C7 / C8: element-qualify to beat the bare selector,
         null out the chevron background, and pin line-height so the
         text doesn't drop to the bottom of the shorter control. */
      .modal-grid2 .gcfg-header select.gcfg-base {
        height: 2.6rem; line-height: 1; padding: 0 .5rem;
        background-image: none;
        font: inherit;
      }
      .modal-grid2 .g2-section-h { margin: 0; font-size: 1.2rem; color: var(--brown-darker); letter-spacing: .05rem; text-transform: uppercase; }
      /* Inner box that holds the tile row + the scrollbar. Margins
         on this element provide the visible vertical gap inside the
         strip's box, on both sides of the tile row. `margin` is
         outside the scroll content + scrollbar region in every
         browser, so Firefox's in-box horizontal scrollbar never
         eats the visible whitespace. */
      .modal-grid2 .g2-selected-strip {
        border: 0; border-radius: 0; background: transparent;
        min-height: 0;
      }
      .modal-grid2 .g2-selected-scroll {
        display: flex; gap: .5rem;
        overflow-x: auto; overflow-y: hidden; min-width: 0;
        /* `.5rem` padding-top supplies the visible gap above the
           cards. `2rem` padding-bottom is sized to BEAT the height
           of a horizontal scrollbar (~15px in Firefox / classic
           Chrome) plus leave a noticeable gap above it — per
           CSS-overflow-3 the scrollbar is painted inside the
           padding region between content edge and border edge, so
           the bottom of the padding area is where the scrollbar
           lives. Anything taller than the scrollbar height shows
           as real whitespace between cards and scrollbar. */
        padding: 2rem 0 1.75rem;
        margin: 0;
        align-items: flex-start;
      }
      .modal-grid2 .g2-tile {
        display: inline-flex; align-items: stretch; gap: 0; flex: 0 0 auto;
        background: var(--surface); border: 2px solid #6b6b6b;
        border-radius: 4px; white-space: nowrap; overflow: hidden;
        transition: opacity .12s ease, box-shadow .12s ease, transform .12s ease;
      }
      /* Two-line tile body: table name on top (muted), column name
         below. Aggregate-column picker cards (`.g2-tile-pickable`)
         keep their single-line layout since they show one column id. */
      .modal-grid2 .g2-tile-body {
        display: flex; flex-direction: column; align-items: flex-start;
        /* Top padding lifts the "table name" line so it aligns with the
           same row inside a subgrid inner card (the inner card sits
           further down because of the container's own padding). */
        padding: .4rem .5rem .15rem; line-height: 1.1; gap: 0;
      }
      .modal-grid2 .g2-tile-name-table {
        font-size: .85em; color: var(--brown-light); padding: 0;
      }
      /* Column-name line uses the same default weight / colour as the
         text on a regular base-column tile; only the table-name line
         above it is muted + smaller. */
      .modal-grid2 .g2-tile-name-col { padding: 0; }
      /* Editable col-name input — invisible chrome (no border / bg)
         in the resting state so it reads as plain text; gains a soft
         outline + cream fill on focus / hover to invite editing.
         Width fits the placeholder's longest plausible column name. */
      .modal-grid2 .g2-tile-name-col-input {
        font: inherit; color: inherit; padding: 0; margin: 0; height: auto;
        background: transparent; border: 1px solid transparent; border-radius: 3px;
        min-width: 4rem; max-width: 11rem; width: auto;
        line-height: 1.15;
      }
      .modal-grid2 .g2-tile-name-col-input:hover {
        border-color: var(--brown-border);
      }
      .modal-grid2 .g2-tile-name-col-input:focus {
        outline: 0; border-color: var(--brown); background: var(--surface);
      }
      .modal-grid2 .g2-tile-name-col-input::placeholder {
        color: var(--brown-light);
      }
      /* When the user has overridden the column name, the top line
         shows `(<original col>)` as a reference. Slight de-emphasis
         so the overridden display still leads with the user's text on
         the bottom line. */
      .modal-grid2 .g2-tile-name-table-overridden { font-style: italic; }
      /* Selected-card bottom line: italic grey when the column is in
         the readonly diamond state. The left-pane diamond carries the
         lock glyph for the picker; on the card the column name itself
         takes on the visual cue so the user can read out their grid at
         a glance. */
      .modal-grid2 .g2-tile-name-ro { font-style: italic; color: var(--brown-light); }
      /* Checkbox-mode tile: ☐ glyph + readonly-until-dblclick input. */
      .modal-grid2 .g2-tile-checkbox-line {
        display: flex; align-items: center; justify-content: center; gap: .35rem;
      }
      .modal-grid2 .g2-tile-checkbox-mark {
        color: var(--brown); font-size: 1.1em; line-height: 1;
        user-select: none;
      }
      .modal-grid2 .g2-tile-name-col-input-locked {
        cursor: text;
      }
      .modal-grid2 .g2-tile-name-col-input-locked:hover {
        border-color: transparent;
      }
      .modal-grid2 .g2-tile.dragging { opacity: .5; }
      .modal-grid2 .g2-tile.drag-over {
        box-shadow: -3px 0 0 0 var(--brown);
        transform: translateX(2px);
      }
      /* Left-side grab handle on regular cards — matches the subgrid
         container's vertical handle in colour so the strip reads as
         "everything black-bordered is draggable". */
      .modal-grid2 .g2-tile-handle {
        display: inline-flex; align-items: center; justify-content: center;
        padding: 0 .4rem; background: #6b6b6b; color: #fff;
        font-size: 1rem; line-height: 1;
        cursor: grab; user-select: none;
      }
      .modal-grid2 .g2-tile-handle:active { cursor: grabbing; }
      .modal-grid2 .g2-tile-name { padding: .35rem .6rem; align-self: center; }
      .modal-grid2 .g2-tile-x {
        background: transparent; border: 0; padding: 0 .55rem; cursor: pointer;
        color: var(--brown-light); font-size: 1.2rem; line-height: 1; height: auto;
        border-left: 1px solid var(--brown-border);
      }
      .modal-grid2 .g2-tile-x:hover { color: var(--danger); }
      .modal-grid2 .g2-tile-empty { padding: .35rem .6rem; }
      /* Subgrid container: black-bordered region that holds one inner
         card per projected column. Left edge is a drag handle whose
         label is the subgrid's name rendered vertically (rotated 90°
         anti-clockwise). The inner cards mirror regular .g2-tile
         visuals but flow horizontally inside the bordered region. */
      .modal-grid2 .g2-tile-subgrid-container {
        display: inline-flex; align-items: stretch; flex: 0 0 auto;
        background: var(--surface);
        border: 2px solid #6b6b6b; border-radius: 4px;
        white-space: nowrap; overflow: hidden;
        transition: opacity .12s ease, box-shadow .12s ease, transform .12s ease;
      }
      .modal-grid2 .g2-tile-subgrid-container.dragging { opacity: .5; }
      .modal-grid2 .g2-tile-subgrid-container.drag-over {
        box-shadow: -3px 0 0 0 var(--brown);
        transform: translateX(2px);
      }
      .modal-grid2 .g2-subgrid-handle {
        display: inline-flex; align-items: center; justify-content: center;
        padding: .4rem .25rem; background: #6b6b6b; color: #fff;
        cursor: grab; user-select: none; min-width: 1.6rem;
      }
      .modal-grid2 .g2-subgrid-handle:active { cursor: grabbing; }
      .modal-grid2 .g2-subgrid-name {
        writing-mode: vertical-rl;
        transform: rotate(180deg); /* combined with vertical-rl = bottom-to-top, reads left side */
        font-weight: 700; letter-spacing: .04rem; font-size: 1rem;
        line-height: 1; padding: .2rem 0;
        cursor: text;
      }
      .modal-grid2 .g2-subgrid-name-input {
        writing-mode: vertical-rl; transform: rotate(180deg);
        font: inherit; font-weight: 700; color: #000;
        background: var(--surface); border: 1px solid var(--brown); border-radius: 3px;
        padding: .15rem .25rem; min-height: 6rem; max-height: 14rem;
        width: 1.6rem; line-height: 1; margin: 0;
      }
      .modal-grid2 .g2-subgrid-inner {
        display: inline-flex; align-items: stretch; gap: .25rem;
        padding: .2rem; flex: 0 0 auto;
      }
      .modal-grid2 .g2-subgrid-empty {
        padding: .35rem .6rem; font-size: .9em; align-self: center;
      }
      .modal-grid2 .g2-subgrid-innercard {
        display: inline-flex; align-items: stretch; flex: 0 0 auto;
        background: var(--cream-2); border: 1px solid var(--brown-border);
        border-radius: 3px; white-space: nowrap; overflow: hidden;
      }
      .modal-grid2 .g2-subgrid-innercard.dragging { opacity: .5; }
      .modal-grid2 .g2-subgrid-innercard.drag-over {
        box-shadow: -3px 0 0 0 var(--brown);
      }
      .modal-grid2 .g2-subgrid-innercard .g2-tile-body {
        padding: .1rem .4rem;
      }
      .modal-grid2 .g2-subgrid-removeall {
        align-self: stretch;
      }
      /* AVAILABLE COLUMNS is the wider pane (3/5); AGGREGATIONS (2/5)
         holds the group-by picker + function rows. The grid stretches
         to fill the remaining body height; min-height:0 lets each
         column shrink so the inner panes' overflow-y can do its job.
         `grid-auto-rows: minmax(0, 1fr)` forces the (sole) implicit
         row to fill the container rather than sizing to content —
         without it the grid cell takes its content's full height and
         the inner pane's overflow-y never engages, leaving the
         bottom unreachable. */
      .modal-grid2 .g2-two-panes {
        display: grid; grid-template-columns: 3fr 2fr; gap: 1rem;
        flex: 1 1 auto; min-height: 0;
        grid-auto-rows: minmax(0, 1fr);
      }
      /* Override grid1's fixed `height: 60vh` on .gcfg-available — in
         grid2 the inner box fills its grid cell (which is sized by the
         remaining body height after the SELECTED strip). */
      .modal-grid2 .gcfg-col { min-height: 0; height: 100%; }
      .modal-grid2 .gcfg-available { height: auto; min-height: 0; flex: 1 1 auto; }
      .modal-grid2 .g2-col-list { display: flex; flex-direction: column; }
      .modal-grid2 .g2-col-row {
        display: flex; align-items: center; gap: .5rem;
        padding: .35rem .5rem; border-radius: 4px; cursor: pointer;
      }
      .modal-grid2 .g2-col-row:hover { background: var(--cream-2); }
      .modal-grid2 .g2-col-row.taken { color: var(--brown-light); cursor: default; }
      .modal-grid2 .g2-col-row.taken:hover { background: transparent; }
      .modal-grid2 .g2-col-type { font-size: .9em; font-family: var(--mono); }
      .modal-grid2 .g2-col-marker { margin-left: auto; color: var(--brown); }
      .modal-grid2 .g2-group-pick {
        display: flex; align-items: center; gap: .5rem;
        margin-bottom: .75rem; flex-wrap: nowrap;
      }
      .modal-grid2 .g2-label { font-weight: 700; white-space: nowrap; flex: 0 0 auto; }
      /* Group-by dropdown sizes to the longest column name rather than
         stretching to the pane width. `width: max-content` matches the
         widest option text + native select chrome; flex: 0 0 auto stops
         the parent flex layout from stretching it. */
      .modal-grid2 .g2-group-pick .g2-select {
        flex: 0 0 auto; width: max-content; min-width: 0; max-width: 100%;
      }
      .modal-grid2 .g2-agg-row {
        border: 1px dashed var(--brown-border); border-radius: 4px;
        padding: .5rem; margin-bottom: .5rem; background: var(--cream-2);
      }
      .modal-grid2 .g2-agg-head { display: flex; align-items: center; gap: .5rem; margin-bottom: .4rem; }
      .modal-grid2 .g2-agg-fn { flex: 1; max-width: 12rem; }
      .modal-grid2 .g2-agg-x {
        background: transparent; border: 0; cursor: pointer; color: var(--brown-light);
        font-size: 1.2rem; padding: 0 .35rem; height: auto;
      }
      .modal-grid2 .g2-agg-cols { display: flex; flex-wrap: wrap; gap: .35rem; }
      /* Pickable card: same chrome as the selected-strip tile, minus
         the drag handle and × button. Greyed out (muted ink + low-
         saturation fill) when not picked; clicks toggle membership in
         the current function row. */
      .modal-grid2 .g2-tile-pickable {
        cursor: pointer;
        opacity: .55;
        background: var(--cream-2);
        color: var(--brown-light);
        transition: opacity .12s ease, background .12s ease, color .12s ease, border-color .12s ease;
      }
      .modal-grid2 .g2-tile-pickable:hover { opacity: .8; }
      .modal-grid2 .g2-tile-pickable.picked {
        opacity: 1;
        background: var(--brown);
        color: #fff;
        border-color: var(--brown);
      }
      .modal-grid2 .g2-tile-pickable .g2-tile-name { padding: .35rem .6rem; }
      .modal-grid2 .g2-agg-empty { padding: .25rem 0; }
      .modal-grid2 .g2-add-agg-row { margin-top: .5rem; }
      /* Override the global `.modal-body { overflow: auto }` so the outer
         body never scrolls — only the two .gcfg-available / .gcfg-sequence
         boxes scroll internally. Element-qualified would be cleaner but
         .modal-body is itself class-only; same-specificity here is fine
         because this rule is loaded later in the cascade. */
      .modal-body.gcfg-body { padding: 1rem 1.25rem; overflow: visible; }
      .gcfg-cols-wrap { display: grid; grid-template-columns: 1fr 1fr; gap: 1.25rem; margin-top: .75rem; }
      .gcfg-col { display: flex; flex-direction: column; gap: .35rem; }
      .gcfg-col-title { font-size: 1.2rem; font-weight: 700; color: var(--brown-darker); text-transform: uppercase; letter-spacing: .05rem; }
      .gcfg-available, .gcfg-sequence {
        border: 1px solid var(--brown-border); border-radius: 4px;
        /* Generous left+right padding so content (pinning chips, tile
           labels, etc.) doesn't push against the inner borders or the
           scrollbar. Vertical padding stays tight. */
        padding: .25rem .75rem;
        background: var(--surface); height: 60vh; overflow-y: auto;
      }
      /* Grid2's AVAILABLE area lives inside its own scrollable section
         (`.g2-pane-right-scroll`) — strip the inherited box / fixed
         height / inner scroll so it lays out flat against the white
         pane background. */
      .modal-grid2 .gcfg-available {
        border: 0; border-radius: 0;
        padding: 0; height: auto; overflow: visible;
        background: transparent;
      }
      /* Breathing room between the left column's scrollbar and the
         column gap, so the scrollbar doesn't sit flush against the
         right-pane border. */
      .gcfg-available-col { padding-right: .35rem; }
      /* Darker dashed separator between related-table sections so the
         boundary reads at a glance even when adjacent sections both have
         solid pinning bars sitting close to the line. Vertical padding
         is generous (.9rem) so the dashed line breathes — the previous
         .35rem made it press against the surrounding pinning chips. */
      .gcfg-available .col-group { padding: .9rem .25rem; border-bottom: 1px dashed #78716c; }
      .gcfg-available .col-group:last-child { border-bottom: 0; }
      .gcfg-available .col-group-title { font-size: 1.15rem; font-weight: 700; color: var(--brown-darker); margin-bottom: .25rem; }
      .gcfg-available .col-group.n1 .col-group-title { color: #2f6f8f; }
      .gcfg-available .col-group.nm .col-group-title { color: #5d6e4a; }
      .gcfg-available .col-group.base .col-group-title { font-size: 1.4rem; }
      /* grid2: related-table headers use the same neutral palette as
         base. Grey at rest, black when expanded (on). Clicking the
         row toggles the "on" state which also reveals the body. */
      .modal-grid2 .gcfg-available .col-group.related .col-group-title { color: var(--brown-light); font-size: 1.4rem; }
      .modal-grid2 .gcfg-available .col-group.related .col-group-title.is-on { color: var(--brown-darker); }
      .modal-grid2 .gcfg-available .col-group .col-group-title { display: flex; align-items: center; gap: .4rem; line-height: 1; }
      .modal-grid2 .gcfg-available .col-group .col-group-title .icon-table {
        display: inline-flex; align-items: center; justify-content: center;
        width: 1.4rem; height: 1.4rem; flex: 0 0 1.4rem;
      }
      .modal-grid2 .gcfg-available .col-group .col-group-title .icon-table svg {
        width: 100%; height: 100%; display: block;
      }
      .modal-grid2 .gcfg-available .col-group .col-group-title .col-group-title-name {
        font-weight: inherit; line-height: 1;
      }
      .gcfg-available .col-group-title.clickable { cursor: pointer; user-select: none; display: flex; align-items: center; gap: .4rem; }
      .gcfg-available .col-group-title.clickable:hover { background: var(--cream-2); }
      .modal-grid2 .gcfg-via { font-style: italic; color: var(--brown-light); font-weight: 400; font-size: 1.1rem; display: inline-flex; align-items: center; gap: .15rem; }
      .modal-grid2 .gcfg-via .icon-chain {
        display: inline-flex; align-items: center; justify-content: center;
        width: 1rem; height: 1rem; flex: 0 0 1rem;
      }
      .modal-grid2 .gcfg-via .icon-chain svg { width: 100%; height: 100%; display: block; }

      /* grid2 only: base / related col-row lists wrap as chips across
         the pane rather than stacking vertically. Each chip pairs the
         diamond with its column name in a tight pill, so the binding
         stays visually obvious even when several chips share a row.
         The related-col-row layout (per-row default + render selects)
         is unchanged — those rows still stack vertically because they
         carry richer per-column UI. */
      .modal-grid2 .gcfg-available .col-rows-flow {
        display: flex; flex-wrap: wrap; gap: .5rem;
        padding: .15rem 0;
      }
      /* Both base chips and related-table chips share `.g2-chip` styling
         so the two panes look consistent — same border, radius, padding,
         hover, focus. Each kind still keeps its kind-specific
         class for layout (chips wrap as `inline-flex` for base,
         `inline-grid` for related, since related chips carry extra
         render + default inputs alongside the diamond + name). */
      .modal-grid2 .gcfg-available .g2-chip {
        border: 1px solid var(--brown-border);
        border-radius: 6px;
        padding: .1rem .55rem .1rem .4rem;
        background: var(--surface);
      }
      .modal-grid2 .gcfg-available .g2-chip:hover { background: var(--cream-2); }
      .modal-grid2 .gcfg-available .g2-chip:focus { outline: 2px solid var(--brown); outline-offset: 1px; }
      .modal-grid2 .gcfg-available .col-rows-flow .col-row { gap: .3rem; }
      /* Related-table junction + linked-table column lists wrap as
         chips too. Each chip keeps the existing grid layout (diamond,
         name, render select, default input) but as an inline-grid so
         it can sit alongside other chips on the same row when there's
         space. Vertical stacking still kicks in once a chip's content
         is wider than the available row. */
      .modal-grid2 .gcfg-available .related-columns {
        display: flex; flex-wrap: wrap; gap: .5rem; padding: .15rem 0;
      }
      .modal-grid2 .gcfg-available .related-columns .related-columns-label {
        flex: 1 1 100%; padding: 0 .15rem;
      }
      .modal-grid2 .gcfg-available .related-columns .related-col-row {
        display: inline-grid;
        grid-template-columns: 1.2rem auto minmax(0, max-content) minmax(0, max-content);
        gap: .3rem; align-items: center;
      }
      .modal-grid2 .gcfg-available .related-columns .related-col-row .related-col-default,
      .modal-grid2 .gcfg-available .related-columns .related-col-row .related-col-render {
        height: 1.8rem; padding: 0 .35rem; margin: 0; font-size: .95rem;
      }

      /* Expanded related-table body */
      .related-mode { display: flex; gap: .75rem; padding: .35rem .25rem; }
      /* Grid2 mode radios borrow the right-rail TABLES heading style
         (mono, uppercase, light weight, generous letter spacing) so the
         join modes read as section labels rather than form fields. */
      .modal-grid2 .related-mode-label {
        display: inline-flex; align-items: center; gap: .35rem;
        cursor: pointer; font-size: 1.2rem;
        font-family: "Fira Code", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
        font-weight: 300; letter-spacing: .6px; text-transform: uppercase;
        color: var(--text);
      }
      .related-mode-label { display: inline-flex; align-items: center; gap: .25rem; cursor: pointer; font-size: 1.25rem; }
      .related-mode-label input { margin: 0; }
      /* Each pinning (inline-column / checkbox / subgrid block) gets a
         solid left bar running from the lookup row through the +condition
         line so the user can tell sibling pinnings apart at a glance. */
      .related-pinning {
        border-left: 2px solid #78716c;
        padding-left: .5rem;
        /* Generous vertical gap between sibling pinnings so each one
           reads as a self-contained block. */
        margin: .9rem 0;
      }
      .related-pinning:first-of-type { margin-top: .35rem; }
      .related-pinning:last-of-type { margin-bottom: .35rem; }
      /* When the pinning owns the outer left bar, the inner column /
         conditions lists don't need their own — collapse them so we
         show one clear vertical guide per pinning, not three nested. */
      .related-pinning .related-columns,
      .related-pinning .related-conditions { padding-left: 0; border-left: 0; }
      .related-columns { padding-left: .5rem; border-left: 2px solid var(--brown-border); margin: .25rem 0; }
      /* Second column list for N:M subgrid linked-table columns. Sits
         under the junction column list; the small italic label above
         names the linked table so the user can tell where each diamond
         draws its data from. */
      .related-columns-linked { margin-top: .35rem; }
      .related-columns-label { font-size: .9rem; font-style: italic; padding: 0 .25rem .15rem; color: var(--brown-dark); }
      .related-col-row { display: grid; grid-template-columns: 1.4rem 1fr 6rem 11rem; gap: .4rem; align-items: center; padding: .1rem .25rem; }
      /* When the render-pref slot is empty (non-inline mode or non-included
         column) the grid still reserves a column so the defaults input
         stays aligned across rows. */
      select.related-col-render { margin: 0; padding: 0 .35rem; height: auto; font-size: .95rem; line-height: 1.5; }
      .related-col-row.inline-included { background: rgba(139, 58, 98, 0.06); border-radius: 3px; }
      .related-col-row .col-row-name { font-size: 1.1rem; }
      .related-col-row .icon-diamond.clickable { cursor: pointer; }
      input.related-col-default { margin: 0; padding: .1rem .35rem; height: auto; font-size: 1rem; line-height: 1.5; background: var(--surface); }
      input.related-col-default.locked { background: var(--cream-2); color: var(--brown-light, #a1a1aa); }
      /* Choice-default editor — same compact height as the text input so
         the two render identically across a column. Milligram's default
         <select> styling pushes ~3.8rem tall; this clamps it. */
      select.related-col-default {
        margin: 0; padding: 0 .35rem; height: auto; line-height: 1.5;
        font-size: 1rem; background: var(--surface);
      }
      /* The pinning's identity row: 'for <table>: [col] = [value]'. This
         is NOT a where-condition — it pins the cell to one row in the
         linked table. Styled as a chip (rounded, accent border, pale
         fill) so it reads visually distinct from the 'only rows where:'
         filter block below. The two selects share the leftover
         horizontal space via flex: 1 1 0; labels stay shrink-to-fit. */
      .related-linked-lookup {
        display: flex; align-items: center; flex-wrap: nowrap;
        gap: .35rem; padding: .25rem .5rem; font-size: 1.1rem;
        min-width: 0;
        background: var(--cream-2);
        border: 1px solid var(--brown-light);
        border-radius: 6px;
        margin: 0 0 .25rem 0;
      }
      .related-linked-lookup > span { flex: 0 0 auto; white-space: nowrap; }
      .related-linked-lookup-pin {
        /* 📌 emoji greyed to fit the stone palette — pin reads as a
           subtle marker, not a colored alert. */
        filter: grayscale(1) opacity(.65);
        font-size: 1rem; line-height: 1;
      }
      .related-linked-lookup-label { font-weight: 600; color: var(--brown-darker); }
      /* Lookup picker is required for N:M inline / checkbox entries —
         flag it in red until the value is picked, so the user sees the
         missing input BEFORE clicking Save. */
      .related-linked-lookup-invalid {
        border-color: var(--danger);
        background: var(--danger-bg);
      }
      .related-linked-lookup-warn {
        color: var(--danger); font-weight: 600; font-size: .95em;
      }
      .related-linked-lookup select,
      .related-linked-lookup input.related-linked-lookup-value {
        margin: 0; padding: 0 .25rem; height: auto; line-height: 1.5;
        font-size: 1rem; min-width: 0;
        flex: 0 1 12rem; max-width: 12rem; width: 12rem;
        background: var(--surface);
      }
      /* Per-pinning kind radio (value vs checkbox) + read-only/editable
         select. Sits right under the linked-lookup row of each pinning so
         the user can pick a cell shape per pinning. Reuses the lookup
         row's font-size so both rows read as a single header block. */
      .related-pinning-kind {
        display: flex; align-items: center; gap: .75rem;
        padding: .15rem .25rem; font-size: 1.1rem;
      }
      .related-pinning-kind-label {
        display: inline-flex; align-items: center; gap: .25rem; cursor: pointer;
        font-size: 1.1rem; font-weight: 400; margin: 0;
      }
      .related-pinning-kind-label input { margin: 0; }
      .related-pinning-readonly {
        /* Sit immediately after the "checkbox" radio rather than being
           pushed to the right edge of the row. Width fits content + a
           little extra so the native dropdown arrow has room. */
        margin: 0; padding: 0 1.5rem 0 .35rem; height: auto;
        line-height: 1.5; font-size: 1rem; min-width: 0;
        width: max-content;
      }
      /* Checkbox-mode column rows: diamond column is empty (the per-row
         include/exclude is not used here — every column's default is
         consulted on INSERT). Style identical to inline rows otherwise. */
      .related-col-row.checkbox-defaults > :first-child { visibility: hidden; }
      /* Right-panel checkbox icon for the (single) sub-tile shown when
         a related entry is in checkbox mode. Larger so it reads as a
         real "this is a checkbox" affordance, in the same ink as the
         surrounding column names. Chained selector wins over the
         later `.seq-sub-name` size/colour rule. */
      .seq-sub-name.seq-sub-checkbox-icon { font-size: 1.7rem; line-height: 1; color: var(--text); }
      .related-conditions { padding-left: .5rem; border-left: 2px solid var(--brown-border); margin: .25rem 0; }
      .related-conditions-label { font-size: .95rem; }
      .cond-row { display: flex; gap: .25rem; align-items: center; margin-bottom: .15rem; }
      select.cond-col, select.cond-op, select.cond-val {
        padding: 0 .35rem; height: auto; font-size: .95rem; line-height: 1.5; margin: 0;
        flex: 0 0 auto;
      }
      /* CONDITIONS list inside the left pane. No surrounding box —
         the section's `<h3>` heading is the only chrome; each cond
         stacks vertically inside via `flex-direction: column`. */
      .g2-base-conds {
        background: var(--surface);
        border: 1px solid rgba(120, 90, 60, .18);
        border-radius: 6px;
        padding: .35rem .5rem;
        box-shadow: 0 1px 2px rgba(58,40,23,.04);
      }
      .g2-base-conds .cond-flow {
        flex-direction: column; align-items: stretch; gap: .15rem;
      }
      .g2-base-conds .cond-flow .cond-sep { display: none; }
      .g2-base-conds .cond-flow .cond-row { width: 100%; }
      .g2-base-conds .cond-row.cond-row-compact { display: flex; }
      /* Vertical label: rotated 90° counter-clockwise so reading
         direction is bottom-to-top. `vertical-rl` + 180deg gives the
         same glyph order with the correct upright stance. The
         dedicated column is narrow because it only holds the rotated
         word. */
      .g2-base-conds-label {
        writing-mode: vertical-rl;
        transform: rotate(180deg);
        /* Match `.gcfg-header-label` (A GRID ROW PER) — same color,
           size, weight, letter-spacing, uppercase. */
        color: var(--ink-mute); font-size: 1.2rem;
        font-weight: 700; letter-spacing: .05rem; text-transform: uppercase;
        align-self: center;
        line-height: 1; padding: 0;
        user-select: none;
      }
      .g2-base-conds-rows { flex: 1 1 auto; min-width: 0; }
      .g2-base-conds .related-conditions { border-left: 0; padding-left: 0; margin: 0; }
      .g2-base-conds .cond-row { margin-bottom: .25rem; }
      .g2-base-conds select.cond-col,
      .g2-base-conds select.cond-op,
      .g2-base-conds select.cond-rhs-mode,
      .g2-base-conds select.cond-val {
        height: 2.2rem; padding: 0 1.6rem 0 .5rem; font: inherit;
      }
      .g2-base-conds select.cond-col { flex: 0 0 15rem; width: 15rem; max-width: 15rem; }
      .g2-base-conds select.cond-val,
      .g2-base-conds input.cond-val { flex: 0 0 15rem; width: 15rem; max-width: 15rem; }
      .g2-base-conds select.cond-op  { flex: 0 0 auto; width: auto; min-width: 3rem; }
      /* RHS mode toggle (value ↔ column) sizes like the op picker; the
         column picker carries .cond-val so it inherits its width above. */
      .g2-base-conds select.cond-rhs-mode { flex: 0 0 auto; width: auto; min-width: 5rem; }
      .g2-base-conds .cond-summary-rhscol { font-style: italic; color: var(--brown-dark); }
      .g2-base-conds .cond-row.cond-row-compact {
        height: 2.2rem; padding: 0 .5rem; font: inherit;
      }
      /* Focused compact cond rows get the same warm-brown tint the
         `.cond-add-row` placeholder uses on focus — keeps the kbnav
         "currently here" cue consistent across the WHERE block.
         Wins against the section-level rule (which lands on
         `--cream-2`, same as the panel background and therefore
         invisible). */
      .g2-base-conds .cond-row.cond-row-compact:hover,
      .g2-base-conds .cond-row.cond-row-compact:focus {
        background: rgba(120, 90, 60, .08);
        border-color: transparent;
        outline: none;
        color: inherit;
      }
      .g2-base-conds button.cond-rm, .g2-base-conds button.cond-add {
        height: 2.2rem; padding: 0 .65rem; font: inherit; line-height: 1;
      }
      .g2-base-conds .cond-row.cond-row-compact button.cond-rm { margin-left: auto; }
      /* Grouped layout (left-pane base conds): each source table gets
         a bold heading above its conds; rows beneath sit indented and
         drop the column-name bold the flat layout used. Right-pane
         related-pin lists still render flat (no grouped class). */
      .modal-grid2 .g2-base-conds .cond-grouped { gap: .35rem; }
      .modal-grid2 .g2-base-conds .cond-table-group { display: flex; flex-direction: column; gap: .1rem; }
      .modal-grid2 .g2-base-conds .cond-table-heading {
        font-weight: 700; color: var(--brown-darker); font-size: 1.4rem;
        line-height: 1.2; padding: .15rem 0;
      }
      .modal-grid2 .g2-base-conds .cond-table-rows { display: flex; flex-direction: column; gap: .15rem; padding-left: 1rem; }
      .modal-grid2 .g2-base-conds .cond-grouped .cond-summary-col { font-weight: normal; }
      /* Soft-delete pending state: struck through + muted until SAVE
         GRID actually prunes the row. Applies in both panes — rule is
         scoped on `.cond-pending-delete` so left- and right-pane lists
         share the styling. */
      .modal-grid2 .cond-row.cond-pending-delete .cond-summary { text-decoration: line-through; color: var(--brown-light, #a1a1aa); }
      /* Right-pane pin conditions — same compact-row UX as the left
         pane, but at a slightly smaller text size to fit the denser
         pinning block. No grouping in this scope. */
      .modal-grid2 .related-pinning .related-conditions {
        background: var(--surface);
        border: 1px solid rgba(120, 90, 60, .18);
        border-radius: 6px;
        padding: .35rem .5rem;
        box-shadow: 0 1px 2px rgba(58,40,23,.04);
        margin-top: .25rem;
      }
      .modal-grid2 .related-pinning .related-conditions,
      .modal-grid2 .related-pinning .related-conditions .cond-row,
      .modal-grid2 .related-pinning .related-conditions .cond-row select,
      .modal-grid2 .related-pinning .related-conditions .cond-row input,
      .modal-grid2 .related-pinning .related-conditions .cond-summary-op { font-size: 1.2rem; }
      .modal-grid2 .related-pinning .related-conditions .cond-row.cond-row-compact {
        display: flex; align-items: center; height: 2rem; padding: 0 .5rem; font: inherit; font-size: 1.2rem;
        border-radius: 3px;
      }
      .modal-grid2 .related-pinning .related-conditions .cond-row.cond-row-compact:hover,
      .modal-grid2 .related-pinning .related-conditions .cond-row.cond-row-compact:focus {
        background: rgba(120, 90, 60, .08);
        outline: none;
      }
      .modal-grid2 .related-pinning .related-conditions .cond-row.cond-row-compact button.cond-rm { margin-left: auto; }
      .modal-grid2 .related-pinning .related-conditions .cond-flow {
        display: flex; flex-direction: column; gap: .15rem;
        align-items: stretch;
      }
      .modal-grid2 .related-pinning .related-conditions .cond-row { width: 100%; flex: 0 0 auto; }
      .modal-grid2 .related-pinning .related-conditions .cond-row.cond-add-row { justify-content: flex-start; }
      .modal-grid2 .related-pinning .related-conditions .cond-sep { display: none; }
      .modal-grid2 .related-pinning .related-conditions select.cond-col { flex: 0 0 12rem; width: 12rem; max-width: 12rem; }
      .modal-grid2 .related-pinning .related-conditions select.cond-val,
      .modal-grid2 .related-pinning .related-conditions input.cond-val { flex: 0 0 12rem; width: 12rem; max-width: 12rem; }
      .modal-grid2 .related-pinning .related-conditions select.cond-col,
      .modal-grid2 .related-pinning .related-conditions select.cond-op,
      .modal-grid2 .related-pinning .related-conditions select.cond-val {
        height: 2rem; padding: 0 1.6rem 0 .5rem; font: inherit;
      }
      .modal-grid2 .related-pinning .related-conditions .cond-row:not(.cond-row-compact):not(.cond-add-row) {
        display: flex; flex-wrap: wrap; align-items: center; gap: .35rem;
      }
      .modal-grid2 .related-pinning .related-conditions button.cond-rm,
      .modal-grid2 .related-pinning .related-conditions button.cond-add {
        height: 2rem; padding: 0 .5rem; font: inherit; line-height: 1;
      }
      /* `+ condition` row: mirrors `.se-col-add` in the schema editor.
         Click / Enter / Space adds a blank cond and immediately opens
         it in edit mode (ActAddBaseCond / ActAddRelatedCond push the
         new key into editingCondKeys). */
      .cond-row.cond-add-row {
        display: flex; align-items: center; gap: .35rem;
        min-height: 2.2rem; padding: .1rem .5rem;
        border-radius: 3px;
        color: var(--brown-light, #a1a1aa); cursor: text;
        user-select: none; font: inherit;
      }
      .cond-row.cond-add-row:hover,
      .cond-row.cond-add-row:focus {
        outline: none;
        background: rgba(120, 90, 60, .08); color: var(--brown);
      }
      .cond-add-handle { display: inline-flex; align-items: center; justify-content: center; width: 1.4rem; font-weight: 700; }
      .cond-add-placeholder { font-style: italic; }
      /* Match column dropdown's width so the row reads as three
         same-height, same-width controls + an `×` button. */
      select.cond-col, select.cond-val { flex: 0 0 9rem; width: 9rem; max-width: 9rem; }
      select.cond-op { flex: 0 0 3.5rem; width: 3.5rem; }
      input.cond-val { padding: .1rem .35rem; height: auto; font-size: .95rem; line-height: 1.5; margin: 0; width: 8rem; }
      button.cond-rm, button.cond-add { padding: 0 .5rem; height: auto; line-height: 1.5; font-size: .9rem; }
      /* `.cond-flow` lays cond cells (compact spans) inline with
         flex-wrap, so they read as "x = 1, y = 2, ..." across the
         line and wrap on narrow modals. Edit-mode rows (full
         selects) opt out of the inline layout via `flex-basis: 100%`
         so they take their own line. */
      .cond-flow {
        display: flex; flex-wrap: wrap; align-items: center;
        gap: .15rem .35rem; min-width: 0;
      }
      .cond-flow .cond-row { flex: 0 0 auto; }
      .cond-flow .cond-row:not(.cond-row-compact):not(.cond-add-row) {
        flex: 1 1 100%; min-width: 0;
      }
      .cond-sep { user-select: none; }

      /* Compact (read-only) condition cell. Tabindex + dblclick +
         keydown live on the cell. Dbl-click or Enter / Space expands
         to the edit controls. Renders inline so a list of conds
         flows like "x = 1, y = 2, ...". */
      .cond-row.cond-row-compact {
        display: inline-flex; align-items: center; gap: .25rem;
        padding: .1rem .4rem; border-radius: 3px;
        cursor: pointer;
        background: transparent; border: 1px dashed transparent;
        font-size: .95rem; line-height: 1.4;
      }
      .cond-row.cond-row-compact:hover,
      .cond-row.cond-row-compact:focus {
        background: var(--cream-2); border-color: var(--brown-border); outline: none;
      }
      .cond-row-compact .cond-summary {
        display: inline-flex; align-items: center; gap: .25rem;
        min-width: 0;
      }
      .cond-row-compact .cond-summary-col { color: var(--brown-darker); font-weight: 600; }
      .cond-row-compact .cond-summary-op  { font-size: .9rem; }
      .cond-row-compact .cond-summary-val { color: var(--text); }
      .cond-row-compact .cond-summary-val.muted { font-style: italic; }
      /* Bump the × glyph inside the builder's delete buttons (seq tile
         remove, pinning remove, condition remove) so the click target
         is unambiguous. cond-add (a + label) keeps its smaller size. */
      button.cond-rm,
      button.pinning-remove,
      button.seq-remove { font-size: 1.4rem; font-weight: 600; line-height: 1; }

      /* Right-panel tiles for reorder. Simple columns are a single row;
         related-group tiles stack a header row + a list of sub-tiles
         underneath, all moving together when dragged. */
      .seq-tile {
        display: flex; flex-direction: column; gap: .25rem;
        padding: .35rem .5rem; border-radius: 3px;
        background: var(--cream-2); margin-bottom: .6rem;
        cursor: grab;
      }
      .seq-tile-head { display: flex; align-items: center; gap: .5rem; }
      .seq-tile.dragging { opacity: .5; }
      .seq-tile.drag-over { box-shadow: 0 -2px 0 0 var(--brown); }
      /* Symmetric left-edge accent so base / related tiles line up. A
         dark grey for base tiles — same colour as the in-tile base-table
         label below, so the eye associates "table name + left edge" as
         one grouping cue. Related tiles override with the rose accent. */
      .seq-tile { border-left: 3px solid var(--brown-dark); }
      .seq-tile-related { border-left: 3px solid #8b3a62; }
      /* Pinning context line: "<linked-table> with <col> = <value>" */
      /* Pinning context (e.g. "certification with name = Fedramp High")
         reads as supporting metadata — slightly lighter than the column
         names below so the column names lead the eye. The rename input
         (replaces the old `(subgrid)` tag) sits to the right and inherits
         the same compact `.seq-name-input` styling. */
      .seq-tile-pin { flex: 1; font-size: 1.05rem; color: var(--brown); display: flex; align-items: center; gap: .35rem; min-width: 0; }
      .seq-tile-pin-text { font-style: italic; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
      /* Sub-tiles list under a related group. Indent matches the head
         row's drag-handle + gap (≈ 1.9 rem) so each sub-tile's name
         x-aligns with base-column tile names elsewhere in the panel.
         The parent tile's `.seq-tile-related` left accent conveys the
         grouping; no vertical rule needed. */
      .seq-subtiles {
        /* Three-column grid: column name, rename input, pill toggle slot.
           `display: contents` on each sub-tile pulls its children into the
           parent grid so name + input edges align across rows regardless
           of column-name length. */
        display: grid;
        grid-template-columns: auto 1fr auto;
        align-items: center;
        gap: .2rem .5rem;
        margin-left: 1.5rem; padding-left: 0;
      }
      .seq-subtile { display: contents; }
      .seq-sub-name {
        font-size: 1.1rem;
        color: var(--brown-darker); font-weight: 700;
      }
      /* Empty placeholder when a sub-tile doesn't carry a pill toggle —
         keeps the trailing icon column aligned. */
      .seq-sub-iconslot { display: inline-block; min-width: 1.3rem; }
      .seq-sub-empty { font-size: 1rem; padding: .15rem .35rem; }
      /* Inline rename input — sits inside a tile or sub-tile, picks up
         remaining horizontal space. Small + transparent so it doesn't
         look like a heavyweight form field. */
      input.seq-name-input {
        flex: 1; min-width: 0; margin: 0;
        padding: .1rem .35rem; height: auto; font-size: 1.1rem; line-height: 1.5;
        background: transparent; border: 1px solid transparent; border-radius: 3px;
      }
      input.seq-name-input:focus,
      input.seq-name-input:hover { background: var(--surface); border-color: var(--brown-border); }
      .col-row { display: flex; align-items: center; gap: .5rem; padding: .15rem .4rem; cursor: pointer; border-radius: 3px; }
      .col-row:hover { background: var(--cream-2); }
      .col-row-name { font-size: 1.2rem; color: var(--text); }
      .seq-handle { color: var(--brown-border); font-size: 1.4rem; line-height: 1; user-select: none; }
      .seq-remove { padding: 0 .6rem; height: 2rem; font-size: 1.1rem; }
      /* The grid-config header is a three-column band: name input on
         the left, BASE TABLE picker centered, status/save/cancel
         cluster on the right. The base picker is wrapped in a flex:1
         child with justify-content: center so it sits visually centered
         regardless of the side groups' widths. */
      .gcfg-header { gap: .75rem; }
      /* Glyph + name input form one visual title group with a tight gap,
         independent of the wider .gcfg-header gap that separates the
         three header sections. */
      /* Layout-only: typography (size/weight/letter-spacing/color)
         comes from the shared `.modal-title` class the same way the
         schema editor's title gets it. Keeping the rules layout-only
         here is what guarantees the two modals' headers render at
         identical type size and row height. */
      .gcfg-header-title {
        display: flex; align-items: center; gap: .15rem;
        flex: 0 0 auto; min-width: 0;
      }
      .gcfg-header-title > .gcfg-header-glyph {
        flex: 0 0 auto; line-height: 1; user-select: none;
        /* `1em` scales with the parent `.modal-title` font-size so the
           glyph never inflates the header beyond the schema editor's
           row height. */
        font-size: 1em;
        margin: 0 .15rem 0 .75rem;
      }
      .gcfg-header-title > .gcfg-name {
        margin: 0; flex: 0 0 auto; width: 26rem;
        /* Grid name renders in the muted grey that the "Edit grid"
           prefix used to wear so the title reads as "fixed prefix +
           editable target". Everything else (size, weight, letter
           spacing) inherits from `.modal-title`. */
        color: var(--ink-mute); background: transparent;
        font: inherit; letter-spacing: inherit;
        border: 1px solid transparent;
        padding: 0 .35rem; height: auto; line-height: inherit;
        text-overflow: ellipsis;
      }
      .gcfg-header-title > .gcfg-name:hover,
      .gcfg-header-title > .gcfg-name:focus {
        background: var(--surface); border-color: var(--brown-border);
      }
      .gcfg-header-base {
        display: flex; align-items: center; justify-content: center;
        gap: .5rem; flex: 1 1 auto; min-width: 0;
      }
      .gcfg-header-label {
        margin: 0; flex: 0 0 auto; color: var(--ink-mute); font-size: 1.2rem;
        text-transform: uppercase; letter-spacing: .05rem; font-weight: 700;
      }
      /* "Edit grid" prefix sits inside `.gcfg-header-title` and
         inherits the title's brown-darker / 1.4rem chrome (matches
         the schema editor's modal-title). The greyed-out style now
         lives on `.gcfg-name` (the editable grid name), pairing the
         two as "fixed prefix + user-editable target". */
      .gcfg-header-edit-label { flex: 0 0 auto; }
      .gcfg-header .gcfg-base { margin: 0; max-width: 20rem; min-width: 0; }
      /* SAVE + CANCEL pair lives in the shared `.modal-actions`
         container — the schema editor uses the same one — so the
         right-side clustering + gap behaviour is defined once. */
      .gcfg-header .gcfg-status { flex: 0 0 auto; min-width: 0; }
      .gcfg-header .gcfg-submit, .gcfg-header > button { flex: 0 0 auto; margin: 0; }
      .gcfg-status { font-size: 1.1rem; flex-shrink: 0; }

      /* Home page primary action button replacing "Saved grids" title */
      /* + grid now lives in the home header next to the cog (see
         home-head below) rather than above the grid list. The earlier
         flex-start placement is no longer needed. */
      .home-head .home-new-grid { margin: 0; padding: 0 1.25rem; font-size: 1.1rem; height: 3.2rem; }

      /* Shared chrome for the home + settings header (`<header
         class="page-header">`). Display, border, gap, and margin are
         identical across both pages so the brand sits on the same
         baseline. Per-page rules below add the bits each page needs
         (home: right-padding for the cog + data-source pill, space-
         between for the + grid button; settings: padding-top to match
         the home body offset, flex-wrap for tab buttons). */
      .page-header {
        display: flex; align-items: center; gap: 1rem;
        border-bottom: 1px solid var(--brown-border); margin: 0 0 1.5rem;
      }
      .home-head {
        justify-content: space-between;
        padding-bottom: .75rem;
        /* Reserve room on the right for the fixed cog (3.6rem + 1.25rem
           offset = ~6rem) AND the two-line data-source pill. The pill's
           left edge reaches ~28rem from the viewport's right edge; the
           home-head sits inside body's 1.5rem padding, so we need ~28rem
           of reserved space inside it for the + grid button to clear. */
        padding-right: 30rem;
      }
      /* When the OFFLINE ready indicator is visible, push the + grid
         button (and the brand row) further left so it isn't covered.
         The body class is toggled by header-pill.js once the SW has
         precached the shell. The offline pill plus its 1rem gap before
         the data-source pill add ~11rem to the right-side cluster. */
      body.has-offline-indicator .home-head { padding-right: 42rem; }
      /* The brand renders as `<h1>` on the home page and `<a>` on settings.
         Without an explicit `font-family` + `letter-spacing` here, h1
         inherits the heading rule (Inter / -0.01em) while `a` inherits
         body's monospace stack — same class, different typography. Pin
         both so the rendered glyphs are identical regardless of which
         element wraps them. */
      .home-brand {
        margin: 0; font-size: 3.6rem; color: var(--brown-darker); font-weight: 700;
        font-family: var(--sans); letter-spacing: -0.01em;
        position: relative; display: inline-block; line-height: 1; isolation: isolate;
      }
      /* ᚙ watermark sits behind the "csvdb" wordmark on the home page. The
         glyph is anchored to the brand element's left edge and sized ~2× the
         text height, with a soft brown-light tint at low opacity so it
         reads as a backdrop rather than a foreground letter. `isolation` on
         the parent gives us a fresh stacking context so the negative
         z-index doesn't escape into the page. */
      .home-brand-glyph {
        position: absolute; left: -.6em; top: 50%;
        transform: translateY(-50%);
        font-size: 1.9em; line-height: 1;
        color: var(--brown-light); opacity: .35;
        pointer-events: none; z-index: -1;
      }
      .home-brand-text { position: relative; z-index: 1; }
      /* Home page splits into the primary saved-grid list (left, expands to
         fill the page width) and a narrow right rail of default-grid jump
         links keyed by user table. Collapses to a single column on narrow
         viewports so the right rail doesn't squeeze the cards. */
      .home-layout { display: grid; grid-template-columns: 1fr 240px; gap: 1.5rem; align-items: start; }
      @media (max-width: 720px) { .home-layout { grid-template-columns: 1fr; } }
      .home-primary { min-width: 0; }
      /* Saved-grid cards laid out as a responsive auto-fill grid — each card
         claims ~280 px and we pack as many as fit per row. */
      .home-grids { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 1.25rem; }
      .home-defaults { display: flex; flex-direction: column; gap: .35rem; padding: 1rem 1.25rem;
                       background: var(--cream-2); border: 1px solid var(--brown-border); border-radius: 4px; }
      .home-defaults .section-title { margin-bottom: .25rem; }
      .home-defaults-header { display: flex; align-items: center; justify-content: space-between; gap: .5rem; }
      .home-defaults-header .section-title { margin-bottom: 0; }
      /* Match the grid-card hamburger's dimensions exactly so every
         toggle in the app — grid-card ☰, table-row ☰, and the [+]
         button here — renders at the same size. The pinned width
         keeps the narrower `+` glyph from rendering a button smaller
         than the wider ☰ siblings, while still matching the
         grid-card hamburger's 2.6rem footprint. */
      .home-defaults-actions { display: flex; align-items: center; gap: .4rem; }
      button.home-defaults-add, button.home-defaults-import {
        margin: 0; padding: 0 .65rem; height: 2.2rem; line-height: 1; font-size: 1.2rem;
        width: 2.6rem;
      }
      /* Import button is icon-only — drop the text padding and centre the glyph. */
      button.home-defaults-import { padding: 0; display: inline-flex; align-items: center; justify-content: center; }
      button.home-defaults-import .icon-import-table { display: block; }
      button.home-defaults-add:focus-visible,
      button.home-defaults-import:focus-visible { outline: 2px solid var(--brown-darker); outline-offset: 2px; }
      .home-defaults-list { display: flex; flex-direction: column; gap: .25rem; }
      /* Junction list sits directly below the tables list without its
         own heading; add a small top gap so the two groups remain
         visually distinguishable. */
      .home-defaults-list-sub { margin-top: .75rem; }
      /* Row wraps the link + per-row hamburger. The hamburger is hidden
         until the row is hovered (or the menu is open) so the rail stays
         visually quiet — same affordance as the grid card menu, but
         pinned to the row's right edge. */
      .home-default-row { position: relative; display: flex; align-items: center; }
      .home-default-row .home-default-link { flex: 1; min-width: 0; padding-right: 2.5rem; }
      /* Span the row's full height and centre the toggle with flex —
         transform-based centring drifted because Milligram's button line
         metrics don't match the link's text line-height exactly. */
      .home-default-actions {
        /* `right: 0` so the hamburger's right edge lines up exactly
           with the [+] button at the top of the TABLES list (both
           buttons now share the same dimensions — see
           .home-defaults-add above). */
        position: absolute; right: 0; top: 0; bottom: 0;
        display: flex; align-items: center;
      }
      /* Fade only the toggle button — the drop-down stays fully opaque the
         moment it renders so menu items never bleed through to the link
         or rail rows beneath. */
      /* Mirror the grid-card hamburger toggle chrome so both surfaces
         read as the same control. Same height / padding / font-size /
         cream lift-off + hover-to-white flip. Scoped to
         `.home-default-actions` and element-qualified twice to
         outweigh Milligram's bare `.button.button-outline` darker
         border (convention C8). */
      .home-default-actions button.card-menu-toggle.card-menu-toggle {
        /* The shared `.card-menu-toggle` rule (settings page) sets
           `min-width: 3rem`. Without resetting it here, our `width:
           2.6rem` is shadowed (effective width = max(width, min-width)
           = 3rem) and this toggle silently renders bigger than the
           grid-card hamburger and the [+] button. */
        height: 2.2rem; width: 2.6rem; min-width: 2.6rem;
        padding: 0 .65rem; margin: 0;
        font-size: 1.2rem; line-height: 1;
        background-color: var(--cream); border-color: var(--brown-border);
        color: var(--brown);
        opacity: 0; transition: opacity .12s ease;
      }
      /* Override the global `.button.button-outline:hover { color: #fff }`
         (light text on the hover-coloured bg) — when our row toggle
         hovers we keep a light surface bg and the dark ink for the
         glyph, otherwise the ☰ glyph vanishes on light theme. */
      .home-default-actions button.card-menu-toggle.card-menu-toggle:hover,
      .home-default-actions.home-default-actions-open button.card-menu-toggle.card-menu-toggle {
        background-color: var(--surface); border-color: var(--brown-light);
        color: var(--brown-darker);
      }
      /* Hamburger visibility follows the same mouse-vs-keyboard rule
         as the row background: mouse-hover reveals it in mouse mode,
         focus-within reveals it in keyboard mode. The previous
         unconditional `:focus-within` rule left the hamburger opaque
         on the previously-keyboarded row even after the mouse stole
         the highlight, giving the user a stale "this row is current"
         affordance the row background no longer agreed with. */
      body:not(.kb-nav) .home-default-row:hover button.card-menu-toggle.card-menu-toggle,
      body.kb-nav .home-default-row:focus-within button.card-menu-toggle.card-menu-toggle,
      .home-default-actions-open button.card-menu-toggle.card-menu-toggle { opacity: 1; }
      /* Open row floats above its siblings so the drop-down paints over
         later rows cleanly, and the rail's hover styles stop firing on
         everything else while a menu is open. The active row keeps its
         hover-style background so the user can see which row the menu
         is acting on. */
      .home-default-row:has(.home-default-actions-open) { z-index: 5; }
      /* While ANY row has its menu open, suppress mouse-hover background
         on the others so the open row stays the only visually-active row.
         The active row keeps its highlight via the rule directly below. */
      .home-defaults-list:has(.home-default-actions-open) .home-default-row:hover {
        background: transparent;
      }
      .home-defaults-list:has(.home-default-actions-open) .home-default-row:hover .home-default-link {
        color: var(--brown-dark);
      }
      .home-default-row:has(.home-default-actions-open) {
        background: var(--surface); border-radius: 3px;
      }
      .home-default-row:has(.home-default-actions-open) .home-default-link {
        color: var(--brown-darker);
      }
      .home-default-actions .card-menu { background: var(--surface); }
      .home-default-link {
        display: flex; align-items: center; gap: .5rem;
        padding: .45rem .6rem;
        font-family: var(--mono); font-size: 1.15rem;
        color: var(--brown-dark); text-decoration: none;
        background: transparent; transition: color .12s ease;
      }
      /* Kill the browser-default focus ring on these links — without
         this, a link focused via keyboard nav keeps showing the dotted
         ring even after the user takes over with the mouse, producing
         a second visible highlight alongside the mouse-hovered row.
         The `body.kb-nav` rules below paint a real background
         highlight when the keyboard is active; that's the only focus
         affordance we need. */
      .home-default-link:focus { outline: none; }
      /* Row-level highlight (not link-level) so the surface tint spans
         the entire row — link cell + hamburger zone alike — instead of
         stopping at the link's flex column edge. The `:hover` paints
         only in mouse mode; `:focus-within` paints only in kb-nav mode
         so we never see two highlights at once. */
      .home-default-row { border-radius: 3px; transition: background .12s ease; }
      body:not(.kb-nav) .home-default-row:hover { background: var(--surface); }
      body:not(.kb-nav) .home-default-row:hover .home-default-link { color: var(--brown-darker); }
      body.kb-nav .home-default-row:focus-within { background: var(--surface); }
      body.kb-nav .home-default-row:focus-within .home-default-link { color: var(--brown-darker); }
      .home-default-link .icon-table { color: var(--brown-light); flex-shrink: 0; }
      body:not(.kb-nav) .home-default-row:hover .home-default-link .icon-table,
      body.kb-nav .home-default-row:focus-within .home-default-link .icon-table { color: var(--brown-darker); }
      .home-default-link .icon-junction {
        color: var(--brown-light); flex-shrink: 0;
        display: inline-flex; align-items: center; justify-content: center;
        width: 16px; height: 16px; font-size: 14px; line-height: 1;
      }
      body:not(.kb-nav) .home-default-row:hover .home-default-link .icon-junction,
      body.kb-nav .home-default-row:focus-within .home-default-link .icon-junction { color: var(--brown-darker); }
      .home-default-name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
      .section-title { margin: 0 0 .5rem; font-size: 1.4rem; color: var(--brown-darker); text-transform: uppercase; letter-spacing: .05rem; }
      .grid-card { display: block; padding: 1rem 1.25rem; background: var(--cream-2); border: 1px solid var(--brown-border); border-radius: 4px; text-decoration: none; color: var(--text); position: relative; transition: background .15s ease; }
      /* Same focus-ring suppression as .home-default-link — see note
         there. Mouse and keyboard each paint their own background; we
         don't want the browser default outline on top in mouse mode. */
      .grid-card:focus { outline: none; }
      /* Mouse mode (no body.kb-nav): hover paints the highlight; a
         keyboard-focused but not-hovered card stays plain so only
         one card is highlighted at a time. Keyboard mode (body.kb-nav
         — set on Arrow/Space, cleared on mousemove): focus paints
         the highlight; hover is suppressed unless it lands on the
         same card. */
      .grid-card:hover { background: var(--card-hover-bg); color: var(--text); outline: none; }
      body.kb-nav .grid-card:focus,
      body.kb-nav .grid-card:focus-visible { background: var(--card-hover-bg); color: var(--text); outline: none; }
      body.kb-nav .grid-card:hover:not(:focus) { background: transparent; color: var(--brown-darker); }
      .grid-name { font-weight: 700; font-size: 1.5rem; color: var(--brown-darker); }
      .grid-meta { font-size: 1.1rem; color: var(--ink-mute); margin-top: .15rem; }
      /* Actions block stays in layout — only its opacity toggles. This
         mirrors the home-rail pattern (.home-default-actions) and is
         deliberately not the previous `display: none` because that
         removes the button from layout: when the cursor enters the
         button's would-be position FROM the card edge, the button
         hadn't materialized yet, and Chrome would not register the
         hover-on-the-button-itself reliably — so the hamburger looked
         like it "disappeared" when you tried to mouse onto it. */
      .grid-actions { position: absolute; top: .8rem; right: .8rem; display: flex; gap: .35rem;
        opacity: 0; transition: opacity .12s ease;
      }
      /* Grid-card hamburger visibility mirrors the table-list rule:
         mouse-hover reveals it in mouse mode, focus-within reveals it
         in keyboard mode. Without the body.kb-nav gating, the
         previously-keyboarded card kept its hamburger opaque after
         the mouse moved on — a stale affordance that visually fought
         the new row's mouse highlight. */
      body:not(.kb-nav) .grid-card:hover .grid-actions,
      body.kb-nav .grid-card:focus .grid-actions,
      body.kb-nav .grid-card:focus-within .grid-actions,
      .grid-actions:hover,
      .grid-actions:focus-within,
      .grid-actions:has(.grid-menu) { opacity: 1; }
      .grid-actions .button { height: 2.2rem; padding: 0 .65rem; font-size: 1rem; }
      /* Opaque cream background so the hamburger 'lifts' off the
         cream-2 card and reads as an actionable button rather than an
         overlay glyph. Hovering tilts it to white. The button.<class>
         compound selector outweighs milligram's .button.button-outline
         transparent-background rule that follows later in the cascade. */
      button.grid-menu-toggle.grid-menu-toggle {
        /* Identical footprint to the table-row ☰ and the [+] table
           button (see `.home-defaults-add` / `.home-default-actions
           button.card-menu-toggle` above). Pinned width — the ☰
           glyph's natural width creeps past `min-width: 2.6rem` by
           ~0.7px under .7rem padding, which is enough to make this
           toggle visibly wider than its siblings; .65rem padding +
           a pinned width locks it. */
        height: 2.2rem; width: 2.6rem; min-width: 2.6rem;
        padding: 0 .65rem; font-size: 1.2rem; line-height: 1;
        background-color: var(--cream); border-color: var(--brown-border);
        color: var(--brown);
      }
      /* Override the global `.button.button-outline:hover { color: #fff }`
         (light text designed for the brown hover bg) — our hover keeps
         a light surface and a dark glyph, otherwise ☰ becomes white-on-
         white and disappears on light theme. Same fix as the table-row
         toggle above. */
      button.grid-menu-toggle.grid-menu-toggle:hover,
      .grid-actions:has(.grid-menu) button.grid-menu-toggle.grid-menu-toggle {
        background-color: var(--surface); border-color: var(--brown-light);
        color: var(--brown-darker);
      }
      /* Pop-out menu mirrors the tables-tab card hamburger so the UX is
         consistent across the app. Anchored to the card's right edge. */
      .grid-menu {
        position: absolute; right: 0; top: calc(100% + .25rem);
        min-width: 160px; z-index: 30;
        background: var(--surface); border: 1px solid var(--brown-border); border-radius: 4px;
        box-shadow: 0 6px 18px rgba(0,0,0,.12);
        padding: .25rem 0; display: flex; flex-direction: column;
        animation: modal-in 100ms cubic-bezier(.2,.7,.2,1);
      }
      .grid-menu-item {
        background: transparent; border: 0; color: var(--text);
        text-align: left; padding: .5rem .9rem; font-size: 1.1rem; cursor: pointer;
        font-family: var(--mono); margin: 0; height: auto; line-height: 1.4;
      }
      .grid-menu-item:hover { background: var(--cream-2); color: var(--brown-darker); }
      .grid-menu-danger { color: var(--danger); }
      .grid-menu-danger:hover { background: var(--danger-hover-bg); color: var(--danger-strong); }
      .col-group { padding: .5rem .25rem; border-bottom: 1px dashed var(--brown-border); }
      .col-group:last-child { border-bottom: 0; }
      .col-group-title { font-size: 1.15rem; font-weight: 700; color: var(--brown-darker); margin-bottom: .25rem; }

      .cog {
        position: fixed; top: 1.25rem; right: 1.25rem;
        width: 3.6rem; height: 3.6rem; line-height: 1;
        display: inline-flex; align-items: center; justify-content: center;
        font-size: 2.4rem; color: var(--brown); text-decoration: none;
        border: 1px solid var(--brown-border); border-radius: 50%;
        background: var(--cream); transition: transform .2s ease, background .2s ease;
      }
      .cog:hover { background: var(--brown); color: var(--cream); transform: rotate(45deg); }

      /* "A newer build is live" nudge. The service worker skipWaiting()s and
         claims the page, so the next navigation serves the new code — but a
         long-lived tab keeps running the old /app.js until it reloads. This
         toast (mounted by frontend/app.js on the register-sw update signal)
         offers that reload instead of letting stale code run silently. */
      .sw-update-toast {
        position: fixed; bottom: 1.25rem; left: 50%; transform: translateX(-50%);
        z-index: 2147483200;
        display: inline-flex; align-items: center; gap: 1rem;
        padding: .6rem .6rem .6rem 1.2rem;
        font-family: var(--sans); font-weight: 700;
        color: var(--brown-darker); background: var(--cream);
        border: 1px solid var(--brown-border); border-radius: 1.8rem;
        box-shadow: 0 4px 16px rgba(58,40,23,.25);
      }
      .sw-update-toast button { margin: 0; }

      /* Always-visible data-source pill. Positioned to the LEFT of the cog
         on every page (cog sits at right: 1.25rem with width 3.6rem, plus
         a 1rem gap → right offset of 5.85rem). Click opens the picker
         (frontend/local/header-pill.js). */
      .csvdb-mode-pill {
        /* Sits left of the theme-toggle (which sits left of the cog).
           5.85rem (cog width + gap) + 3.6rem (theme width) + 1rem gap
           = 10.45rem. Offline pill is positioned dynamically to the
           LEFT of this pill by frontend/local/header-pill.js. */
        position: fixed; top: 1.25rem; right: 10.45rem; z-index: 10;
        height: 3.6rem; padding: 0 1rem; line-height: 1;
        display: inline-flex; align-items: center; gap: .6rem;
        font-family: var(--sans); font-weight: 700;
        color: var(--brown-darker); background: var(--cream);
        border: 1px solid var(--brown-border); border-radius: 1.8rem;
        cursor: pointer; transition: background .2s ease;
        text-transform: none; letter-spacing: 0;
      }
      .csvdb-mode-pill:hover { background: var(--cream-2); }
      .csvdb-mode-pill .csvdb-mode-dot {
        width: .9rem; height: .9rem; border-radius: 50%;
        background: #71717a; box-shadow: 0 0 0 1px rgba(0,0,0,.08) inset;
        flex-shrink: 0;
      }
      .csvdb-mode-pill .csvdb-mode-lines {
        display: inline-flex; flex-direction: column; align-items: flex-start;
        line-height: 1.1; gap: .15rem; min-width: 0;
      }
      .csvdb-mode-pill .csvdb-mode-line1 {
        font-size: 1.05rem; text-transform: uppercase; letter-spacing: .04rem;
        color: var(--brown);
      }
      .csvdb-mode-pill .csvdb-mode-line2 {
        font-size: .95rem; color: var(--brown-darker); font-weight: 500;
        max-width: 14rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
      }
      .csvdb-mode-pill[data-mode="cloud"] .csvdb-mode-dot { background: #4a6b2a; }
      .csvdb-mode-pill[data-mode="local"] .csvdb-mode-dot { background: #b7791f; }
      .csvdb-mode-pill[data-mode="local"] {
        background: #fef6e7; border-color: #e0c590;
      }
      /* Local mode without persistent storage: hotter colour so the user
         sees at a glance that their data is at risk of being cleared
         when the browser closes. Driven by data-persistence which is
         "temporary" / "unsupported" / "unknown" / "persistent". */
      .csvdb-mode-pill[data-mode="local"][data-persistence="temporary"],
      .csvdb-mode-pill[data-mode="local"][data-persistence="unsupported"] {
        background: #fde9e2; border-color: #d99477;
      }
      .csvdb-mode-pill[data-mode="local"][data-persistence="temporary"] .csvdb-mode-dot,
      .csvdb-mode-pill[data-mode="local"][data-persistence="unsupported"] .csvdb-mode-dot {
        background: #c54028;
      }
      .csvdb-mode-pill[data-mode="local"][data-persistence="temporary"] .csvdb-mode-line1,
      .csvdb-mode-pill[data-mode="local"][data-persistence="unsupported"] .csvdb-mode-line1 {
        color: #a83019;
      }

      /* Secondary "offline ready" indicator. Mounted by the header pill
         module next to the mode pill when local mode is active AND the
         service worker has finished precaching the shell — i.e. the
         tab can survive a network drop. Positioned to the left of the
         mode pill (which sits at right: 5.85rem). */
      .csvdb-offline-pill {
        position: fixed; top: 1.25rem; z-index: 10;
        height: 3.6rem; padding: 0 .9rem; line-height: 1;
        display: inline-flex; align-items: center; gap: .5rem;
        font-family: var(--sans); font-weight: 700;
        color: #2f6f3a; background: #e8f3e6;
        border: 1px solid #b7d6a8; border-radius: 1.8rem;
        pointer-events: none; user-select: none;
      }
      .csvdb-offline-pill .csvdb-offline-dot {
        width: .8rem; height: .8rem; border-radius: 50%;
        background: #2f8f3a; box-shadow: 0 0 0 1px rgba(0,0,0,.08) inset;
        flex-shrink: 0;
      }
      .csvdb-offline-pill .csvdb-offline-lines {
        display: inline-flex; flex-direction: column; align-items: flex-start;
        line-height: 1.1; gap: .15rem;
      }
      .csvdb-offline-pill .csvdb-offline-line1 {
        font-size: 1.05rem; text-transform: uppercase; letter-spacing: .04rem;
        color: #2f6f3a;
      }
      .csvdb-offline-pill .csvdb-offline-line2 {
        font-size: .95rem; color: #4a8a55; text-transform: none; letter-spacing: 0;
        font-weight: 500;
      }
      .csvdb-offline-pill[hidden] { display: none; }
      /* Disconnected: the cached shell still runs, but there's no network.
         Shift the accent dot + subtitle to a muted amber so "offline" reads
         as a state change, not an error. */
      .csvdb-offline-pill.is-disconnected .csvdb-offline-dot { background: #c98a2f; }
      .csvdb-offline-pill.is-disconnected .csvdb-offline-line2 { color: #a9742a; }

      /* Dark-mode pill overrides. Light mode uses warm pastels (peach,
         soft red, mint) that read as "tinted highlight on cream". On a
         stone-900 page those same pastels glow too bright; we
         desaturate + darken them so they sit calmly on the dark
         surface and the second-line text stays legible. */
      html[data-theme="dark"] .csvdb-mode-pill {
        color: var(--brown-darker); background: var(--cream-2);
        border-color: var(--brown-border);
      }
      html[data-theme="dark"] .csvdb-mode-pill .csvdb-mode-line1 { color: var(--brown-dark); }
      html[data-theme="dark"] .csvdb-mode-pill .csvdb-mode-line2 { color: var(--brown-darker); }
      html[data-theme="dark"] .csvdb-mode-pill[data-mode="local"] {
        background: #3a3326; border-color: #5d4f30;
      }
      html[data-theme="dark"] .csvdb-mode-pill[data-mode="local"][data-persistence="temporary"],
      html[data-theme="dark"] .csvdb-mode-pill[data-mode="local"][data-persistence="unsupported"] {
        background: #3a1f14; border-color: #6e3a26;
      }
      html[data-theme="dark"] .csvdb-mode-pill[data-mode="local"][data-persistence="temporary"] .csvdb-mode-line1,
      html[data-theme="dark"] .csvdb-mode-pill[data-mode="local"][data-persistence="unsupported"] .csvdb-mode-line1 {
        color: #f6a48a;
      }
      html[data-theme="dark"] .csvdb-offline-pill {
        color: #b7d6a8; background: #1c2a1b; border-color: #2f4a2a;
      }
      html[data-theme="dark"] .csvdb-offline-pill .csvdb-offline-line1 { color: #b7d6a8; }
      html[data-theme="dark"] .csvdb-offline-pill .csvdb-offline-line2 { color: #8aa78a; }
      html[data-theme="dark"] .csvdb-offline-pill .csvdb-offline-dot { background: #5b9e57; }
      html[data-theme="dark"] .csvdb-offline-pill.is-disconnected .csvdb-offline-dot { background: #d6a23f; }
      html[data-theme="dark"] .csvdb-offline-pill.is-disconnected .csvdb-offline-line2 { color: #d6a23f; }
      @media (prefers-color-scheme: dark) {
        html:not([data-theme="light"]) .csvdb-mode-pill {
          color: var(--brown-darker); background: var(--cream-2);
          border-color: var(--brown-border);
        }
        html:not([data-theme="light"]) .csvdb-mode-pill .csvdb-mode-line1 { color: var(--brown-dark); }
        html:not([data-theme="light"]) .csvdb-mode-pill .csvdb-mode-line2 { color: var(--brown-darker); }
        html:not([data-theme="light"]) .csvdb-mode-pill[data-mode="local"] {
          background: #3a3326; border-color: #5d4f30;
        }
        html:not([data-theme="light"]) .csvdb-mode-pill[data-mode="local"][data-persistence="temporary"],
        html:not([data-theme="light"]) .csvdb-mode-pill[data-mode="local"][data-persistence="unsupported"] {
          background: #3a1f14; border-color: #6e3a26;
        }
        html:not([data-theme="light"]) .csvdb-mode-pill[data-mode="local"][data-persistence="temporary"] .csvdb-mode-line1,
        html:not([data-theme="light"]) .csvdb-mode-pill[data-mode="local"][data-persistence="unsupported"] .csvdb-mode-line1 {
          color: #f6a48a;
        }
        html:not([data-theme="light"]) .csvdb-offline-pill {
          color: #b7d6a8; background: #1c2a1b; border-color: #2f4a2a;
        }
        html:not([data-theme="light"]) .csvdb-offline-pill .csvdb-offline-line1 { color: #b7d6a8; }
        html:not([data-theme="light"]) .csvdb-offline-pill .csvdb-offline-line2 { color: #8aa78a; }
        html:not([data-theme="light"]) .csvdb-offline-pill .csvdb-offline-dot { background: #5b9e57; }
      }
      /* Mobile layout for the top-right utility cluster.
         All four controls (offline pill, mode pill, theme toggle, cog)
         sit in a single row at the top of the document. They use
         `position: absolute` (NOT fixed) so they scroll out of the way
         with the page. The home page then makes its header (logo + grid
         buttons) `position: sticky` so it pins to the top of the viewport
         once the cluster has scrolled past — giving the user a frozen
         action row while the pills disappear above. */
      @media (max-width: 720px) {
        /* Pills */
        .csvdb-mode-pill,
        .csvdb-offline-pill {
          position: absolute;
          top: .5rem;
          height: 2rem; padding: 0 .5rem; gap: .35rem;
          border-radius: 1rem;
        }
        .csvdb-mode-pill { right: 5.5rem; }
        .csvdb-mode-pill .csvdb-mode-dot,
        .csvdb-offline-pill .csvdb-offline-dot {
          width: .65rem; height: .65rem;
        }
        .csvdb-mode-pill .csvdb-mode-line2,
        .csvdb-offline-pill .csvdb-offline-line2 { display: none; }
        .csvdb-mode-pill .csvdb-mode-line1,
        .csvdb-offline-pill .csvdb-offline-line1 {
          font-size: .85rem; letter-spacing: .02rem;
        }
        /* Theme + cog — shrink to match the 2rem pill height and join
           the same row. Drop the rotate-on-hover transform from the cog
           since the smaller circle reads as a button without it. */
        .theme-toggle, .cog {
          position: absolute; top: .5rem;
          width: 2rem; height: 2rem;
          font-size: 1.2rem;
        }
        .theme-toggle { right: 3rem; }
        .theme-toggle > svg { width: 1.1rem; height: 1.1rem; }
        .cog { right: .5rem; }
        /* NOTE: the cog icon shrink lives in the late mobile block at the end
           of this stylesheet, NOT here — the base `.cog .icon-cog { 1.8rem }`
           rule appears LATER in source order than this media query, so an
           override here (equal specificity) would lose to it. */

        /* Reclaim the right-side padding on the home header — the
           pill cluster is no longer overlapping the in-flow buttons. */
        .home-head,
        body.has-offline-indicator .home-head { padding-right: 1.25rem; }

        /* Push the first content row down enough to clear the absolute
           pill cluster sitting above it. Applies to every page so the
           pill row has space; grid pages restore their flush layout
           below (they don't use body scroll). */
        body { padding-top: 2.75rem; }
        .home-head {
          position: sticky; top: 0;
          background: var(--cream); z-index: 5;
        }

        /* Grid pages: keep the body fixed to the viewport so the grid's
           internal scroll (`.grid-scroll`) is the ONLY scroller — that
           way the sticky `<thead>` inside `.grid-scroll` reads as a
           frozen column-header row from the user's perspective, no
           matter how far they scroll the data. Without this, body scroll
           would carry the grid (thead included) up and the headers
           would disappear. */
        /* overflow:hidden makes `.grid-scroll` the ONLY scroller so the
           sticky <thead> can't be carried off-screen by a body scroll, and
           `dvh` sizes against the *visible* viewport (mobile `100vh` is the
           URL-bar-collapsed height, which overflows and reintroduces body
           scroll). Without both, column headers + filter row scroll away —
           worst on long-titled views (e.g. a _JUNCTION_ default grid whose
           title wraps the title bar to two lines). */
        body.on-grid { padding: 2.75rem 0 0 0; overflow: hidden; }
        body.on-grid .grid-wrap { height: calc(100dvh - 2.75rem - 4rem); }

        /* Compact the grid title bar's `+ row` and `filter` buttons into
           icon-only chips so the search input has room next to them on
           a phone. `.grid-btn-label` carries the original text — hide
           it and inject a glyph via `::before`. */
        .grid-add-row .grid-btn-label,
        .grid-filter-toggle .grid-btn-label { display: none; }
        .grid-add-row,
        .grid-filter-toggle {
          padding: 0; min-width: 2.4rem; width: 2.4rem; height: 2.4rem;
          display: inline-flex; align-items: center; justify-content: center;
          line-height: 1;
        }
        .grid-add-row::before {
          content: "+"; font-size: 1.8rem; font-weight: 700; line-height: 1;
        }
        .grid-filter-toggle::before {
          /* Funnel glyph; renders as a downward filter triangle in most
             system fonts. Padding tweak keeps it optically centred. */
          content: "▽"; font-size: 1.4rem; line-height: 1;
        }
      }

      /* OPFS lock / fatal-error banner. Rendered into #root by app.js
         renderError() when the local SQLite worker can't acquire its
         exclusive sync-access handles (another tab on the same origin
         is holding them). */
      .local-locked-banner {
        max-width: 56rem; margin: 6rem auto;
        padding: 2rem 2.5rem; border-radius: 6px;
        background: var(--notice-bg, #fef6e7);
        border: 1px solid var(--notice-border, #e0c590);
        color: var(--brown-darker);
        font-family: var(--sans);
      }
      .local-locked-banner h2 { margin: 0 0 .75rem; font-size: 2.4rem; font-weight: 700; }
      .local-locked-banner p { font-size: 1.4rem; line-height: 1.4; margin: 0 0 1.5rem; }
      .local-locked-banner code { background: var(--code-tint, rgba(0,0,0,.06)); padding: .05rem .35rem; border-radius: 3px; font-size: .95em; }
      .local-locked-actions { display: flex; gap: .75rem; }
      .local-locked-actions .button { margin-bottom: 0; }

      /* Offline variant of the locked banner: shown by app.js renderError()
         when cloud storage is unreachable. Keeps the brand visible so the
         page reads as resilient, not broken. */
      .offline-banner .home-brand { display: inline-flex; margin: 0 0 1.25rem; text-decoration: none; }

      /* DB picker modal */
      .modal.modal-picker { min-width: 460px; max-width: 680px; }
      .picker-body { padding: 1.25rem 1.5rem; display: flex; flex-direction: column; gap: 1rem; }
      .picker-modes { display: flex; gap: .5rem; }
      .picker-modes .button { margin-bottom: 0; }
      .picker-modes .button.active { background: var(--brown); color: var(--cream); border-color: var(--brown); }
      .picker-blurb { margin: 0; }
      /* Shared scroll area: persistence warning + DB list scroll together. */
      .picker-scroll { display: flex; flex-direction: column; gap: 1rem; max-height: 50vh; overflow: auto; }
      .picker-list { display: flex; flex-direction: column; gap: .35rem; }
      .picker-row {
        display: flex; align-items: center; gap: .5rem;
        padding: .35rem .6rem; border: 1px solid var(--brown-border); border-radius: 4px;
        background: var(--cream-2);
      }
      .picker-row.active { border-color: var(--notice-border); background: var(--notice-bg); }
      .picker-row-name { flex: 1; min-width: 0; font-family: var(--mono); font-size: 1.2rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
      .picker-row-actions { display: flex; gap: .35rem; flex-shrink: 0; }
      .picker-row-actions .button { margin-bottom: 0; height: 2.6rem; padding: 0 .8rem; font-size: 1.05rem; }
      .picker-actions { display: flex; gap: .5rem; padding-top: .25rem; border-top: 1px dotted var(--brown-border); }
      .picker-actions .button { margin-bottom: 0; }

      /* Demo-database pills — one-click loaders for the bundled example
         .sqlite files (public/<name>.sqlite). Rendered as rounded chips so
         they read as quick presets, distinct from the action buttons below. */
      .picker-demos { display: flex; align-items: center; flex-wrap: wrap; gap: .5rem; }
      .picker-demos-label { font-size: 1.1rem; }
      .picker-demos-pills { display: flex; flex-wrap: wrap; gap: .4rem; }
      .picker-demo-pill.button {
        margin-bottom: 0; height: 2.6rem; padding: 0 1rem;
        border-radius: 1.3rem; font-size: 1.05rem;
      }

      /* Persistence warning shown inside the picker when local mode is
         selected but the browser hasn't granted persistent storage.
         Colours are pulled from CSS variables so the dark-mode block
         further down can flip them without re-stating the rule. */
      .picker-persistence-warning {
        background: var(--warn-bg, #fde9e2);
        border: 1px solid var(--warn-border, #d99477);
        border-radius: 6px;
        padding: 1rem 1.25rem;
        color: var(--warn-ink, #6a2210);
        font-family: var(--sans);
        display: flex; flex-direction: column; gap: .5rem;
      }
      .picker-persistence-warning strong { font-size: 1.25rem; font-weight: 700; }
      .picker-persistence-warning p { margin: 0; font-size: 1.15rem; line-height: 1.4; }
      .picker-persistence-warning .button { margin-bottom: 0; align-self: flex-start; }

      /* Small back-to-home brand on settings page */
      .brand {
        font-weight: 700; color: var(--brown-darker); text-decoration: none;
        font-size: 1.6rem; margin-right: 1rem;
        font-family: var(--sans); letter-spacing: -0.01em;
        position: relative; display: inline-block; line-height: 1; isolation: isolate;
        padding-left: .65em;
        /* Anchor the brand to the top of the header on every page so the
           ᚙ glyph lands at the same y-coordinate regardless of header
           height (the settings header is taller because the tab buttons
           stretch it). `flex-start` + a tiny matching offset keeps the
           visual position identical to the grid page. */
        align-self: flex-start;
      }
      .brand:hover { color: var(--brown); }
      /* Smaller-scale equivalent of the home wordmark watermark — same ᚙ,
         same tint, sits behind the "csvdb" text in every page header. */
      .brand-glyph {
        position: absolute; left: -.55em; top: 50%;
        transform: translateY(-50%);
        font-size: 1.9em; line-height: 1;
        color: var(--brown-light); opacity: .35;
        pointer-events: none; z-index: -1;
      }
      /* Pull the wordmark left so it sits visually over the ᚙ watermark
         on the grid + settings page headers. The home page uses its own
         larger-scale `.home-brand-text` rule and isn't affected. */
      .brand-text { position: relative; z-index: 1; left: -1rem; }
      a { color: var(--brown); }
      a:hover, a:focus { color: var(--brown-dark); }

      /* Buttons: filled = brown, outline = brown border/text */
      .button, button[type="submit"] {
        height: 2.8rem; line-height: 1; padding: 0 1.2rem; font-size: 1.1rem; letter-spacing: .05rem;
        display: inline-flex; align-items: center; justify-content: center;
        font-weight: 700; -webkit-text-stroke: .3px currentColor; text-rendering: geometricPrecision;
        background-color: var(--brown); border-color: var(--brown); color: var(--cream);
      }
      .button:hover, .button:focus, button[type="submit"]:hover, button[type="submit"]:focus {
        background-color: var(--brown-dark); border-color: var(--brown-dark); color: #fff;
      }
      .button.button-outline {
        background-color: transparent; border-color: var(--brown); color: var(--brown);
      }
      .button.button-outline:hover, .button.button-outline:focus {
        background-color: var(--brown); border-color: var(--brown); color: var(--cream);
      }
      .button.danger { background-color: transparent; color: var(--danger); border-color: var(--danger); }
      .button.accent { background-color: transparent; color: #5d6e4a; border-color: #7a8b65; }
      .button.accent:hover, .button.accent:focus { background-color: #7a8b65; border-color: #7a8b65; color: #fff; }

      /* Relationships: horizontal layout */
      .relationship-row, .relationship-form {
        display: flex; flex-wrap: wrap; align-items: center; gap: .5rem;
      }
      .relationship-form select { width: auto; min-width: 12rem; margin: 0; }
      .relationship-form select.rel-select-op { min-width: 6rem; width: 7rem; }
      .relationship-form select.rel-select-field { min-width: 10rem; }
      .rel-table { color: var(--brown-darker); font-weight: 700; }
      .rel-field { color: var(--text); }
      .rel-dot { color: var(--brown); font-weight: 700; font-size: 1.6rem; line-height: 1; padding: 0 .1rem; }
      .rel-op { color: var(--brown); padding: 0 .5rem; font-size: 1.4rem; }
      .rel-spacer { flex: 1 1 auto; }
      .relationship-form-stack { display: flex; flex-direction: column; gap: .65rem; }
      .relationship-form-name-line .rel-name-input { flex: 1 1 12rem; max-width: 28rem; min-width: 10rem; }
      .relationship-row-name { align-items: center; }
      .rel-name-label { font-weight: 700; color: var(--brown-darker); flex-shrink: 0; }
      .rel-name-input { margin-bottom: 0; }
      .relationship-card { padding-bottom: .75rem; }
      /* Relationship validators (business rules) — chips + inline add picker. */
      .rel-val-row { align-items: center; flex-wrap: wrap; gap: .4rem; margin-top: .35rem; padding-top: .5rem; border-top: 1px dotted var(--brown-border); }
      .rel-val-section-label { font-size: .95rem; text-transform: uppercase; letter-spacing: .04em; color: var(--ink-mute); margin-right: .35rem; }
      .rel-val-chip { display: inline-flex; align-items: center; gap: .3rem; font-size: 1.05rem; padding: .15rem .55rem; border: 1px solid var(--brown-border); border-radius: 999px; background: var(--cream-2); color: var(--brown-darker); }
      .rel-val-chip-x { border: 0; background: none; cursor: pointer; color: var(--ink-mute); font-size: 1.2rem; line-height: 1; padding: 0 .1rem; }
      .rel-val-chip-x:hover { color: var(--danger); }
      .rel-val-add { padding: .15rem .6rem; }
      .rel-val-picker { display: inline-flex; align-items: center; gap: .4rem; flex-wrap: wrap; flex: 1 1 auto; }
      .rel-val-picker select, .rel-val-picker input { margin-bottom: 0; height: auto; padding: .25rem .4rem; }
      .rel-val-num { width: 6rem; }
      .rel-val-lbl { color: var(--ink-mute); }
      .link-row-wrap { border-bottom: 1px dotted var(--brown-border); padding: .35rem 0; }
      .link-row-wrap:last-of-type { border-bottom: 0; }
      .link-row-wrap > .link-row.sentence { border-bottom: none; padding-top: .25rem; padding-bottom: .25rem; }
      .link-row-wrap-active { background: var(--cream-2); margin: 0 -.35rem; padding-left: .35rem; padding-right: .35rem; border-radius: 4px; }
      .link-composer-hint { margin: .5rem 0 .35rem; line-height: 1.4; }
      .link-composer-name-line { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; margin-bottom: .5rem; }
      .link-composer-name { flex: 1 1 14rem; max-width: 28rem; margin-bottom: 0; }
      .link-relationship-composer .link-verb-sel:disabled, .link-relationship-composer .link-other-sel:disabled {
        opacity: .75; cursor: not-allowed; color: var(--text);
      }
      .link-edit-label { font-weight: 700; color: var(--brown-darker); flex-shrink: 0; }
      .button.danger:hover, .button.danger:focus { background-color: var(--danger); color: #fff; border-color: var(--danger); }

      /* Header tabs */
      header { display: flex; gap: .5rem; margin-bottom: 1.5rem; border-bottom: 1px solid var(--brown-border); padding-bottom: .5rem; position: relative; }
      header .button { margin-bottom: 0; position: relative; }
      header .button:not(.active) {
        background-color: transparent; color: var(--brown); border-color: var(--brown-border);
      }
      header .button:not(.active):hover {
        background-color: var(--cream-2); color: var(--brown-dark); border-color: var(--brown-light);
      }
      header .button.active { background-color: var(--brown); border-color: var(--brown); color: var(--cream); }
      /* Active tab indicator: a 2 px slab in `--brown-darker` that sits flush
         with (and overrides) the header's grey bottom border, so the eye lands
         on the current tab without needing to read the URL. */
      header .button.active::after {
        content: ""; position: absolute; left: 0; right: 0; bottom: calc(-.5rem - 1px);
        height: 2px; background: var(--brown-darker);
      }

      /* Cards / inputs */
      /* Reset Milligram bottom-margins so flex/grid centering isn't thrown off by phantom space */
      button, .button, input, select, textarea { margin-bottom: 0; }

      .table-card { border: 1px solid var(--brown-border); border-radius: 4px; padding: 1.5rem; margin-bottom: 1.5rem; background: var(--cream-2); }
      .table-card > * + * { margin-top: 1rem; }
      input[type="text"], input[type="email"], input[type="password"], input[type="number"], select, textarea {
        border-color: var(--brown-border); background: var(--surface); color: var(--text);
      }
      input:focus, select:focus, textarea:focus { border-color: var(--brown); }

      /* Field row in the schema editor: [drag-handle] [name-input] [type-select] [validators] [×] */
      .field-row { display: grid; grid-template-columns: 1.4rem 1fr 160px 7rem 7rem 1fr 3.2rem; gap: .5rem; align-items: center; margin: 0; }
      .field-required-toggle { display: inline-flex; align-items: center; gap: .25rem; font-size: .85rem; color: var(--brown, #6d5640); margin: 0; user-select: none; }
      .field-required-toggle input { margin: 0; }

      /* Junction-column editor inside the link overlay (phase 3b). Reuses
         .field-row styling so it visually matches the tables-page editor. */
      .junction-cols-panel { padding: .5rem; margin: .25rem 0 .5rem; background: var(--cream-2); border-radius: 4px; }
      .junction-cols-hint { font-size: .95rem; margin: 0 0 .5rem; }
      .junction-cols-rows { display: flex; flex-direction: column; gap: .35rem; }
      /* Match the tables-card '+ field' button: content-width, not full row. */
      .junction-cols-rows > button.button { align-self: flex-start; }
      .junction-cols-actions { gap: .5rem; margin-top: .5rem; align-items: center; }

      /* Choice type — chip editor renders as a sub-row beneath the main
         field-row when type=choice. Reuses .validator-chip styling. */
      .field-row-wrap { display: flex; flex-direction: column; gap: .25rem; }
      .field-choice-row { display: flex; flex-wrap: wrap; align-items: center; gap: .35rem;
                          padding-left: 1.9rem; }
      .field-choice-label { font-size: .95rem; }
      input.field-choice-input { margin: 0; padding: .1rem .4rem; height: auto;
                                 font-size: .95rem; line-height: 1.5; width: 8rem; }

      /* Grid <select> editor (choice columns + FK dropdown) — match the
         grid cell's typography (Fira Code, 1.2rem) so the editor reads
         as the same surface the user was hovering over. font-family is
         set explicitly to var(--mono) because Firefox does not always
         inherit font-family onto <option> elements (the native popup
         resolves <option> styling independently of the select's
         `inherit`); naming the family directly forces Firefox to honour
         it. line-height stays 1 to keep the chevron-free control short
         (see AGENTS.md / C7). */
      select.grid-edit-choice { box-sizing: border-box; padding: 0 .35rem;
                                font-family: var(--mono); font-size: 1.2rem;
                                line-height: 1; background: var(--surface); }
      select.grid-edit-choice option { font-family: var(--mono); font-size: 1.2rem; }
      .field-row.dragging { opacity: .5; }
      .field-row.drag-over { box-shadow: 0 -2px 0 0 var(--brown); }
      .field-handle { color: var(--brown-border); font-size: 1.4rem; line-height: 1; user-select: none; cursor: grab; text-align: center; }
      .field-handle:hover { color: var(--brown); }
      .field-row > .button.danger { justify-self: end; padding: 0; width: 3.2rem; height: 2.6rem; }
      .field-row + .field-row, .field-row + button, button + .field-row { margin-top: .5rem; }
      /* Bigger gap between field editing actions (+ field) and the SAVE row */
      .table-body > .actions { margin-top: 1.75rem !important; }

      .section-header { display: flex; align-items: center; gap: 1rem; margin-bottom: 0.5rem; }
      .section-header h2 { margin: 0; }
      .toggle-active { background: var(--brown) !important; color: var(--cream) !important; }
      .new-item-form { 
        background: var(--brown-light); 
        padding: 1rem; border-radius: 4px; border: 1px solid var(--brown-border); margin-bottom: 1.5rem; 
      }
      
      /* Tables list (Settings → Tables) flows as a responsive grid. Each
         `.table-row` is one logical unit (card + adjacent link-cell). At
         typical desktop widths that's two units per row; wider viewports
         pack more, narrow ones collapse to one. Cards that are EXPANDED
         (i.e. contain a .table-body) span the full row so the schema
         editor isn't squeezed into a half-width column. The grid lives
         on `.tables-grid` (a child of `.tables-tab`) so the section
         header, intro paragraph, and new-form slot stay in normal block
         flow — matching the validators / relationships tabs and keeping
         the intro text tight under the section header. */
      .tables-grid > .table-row + .table-row { margin-top: 0; }
      .tables-grid {
        display: grid;
        grid-template-columns: repeat(auto-fill, minmax(440px, 1fr));
        gap: 1.25rem;
      }
      /* Inside a row, the card sits left of its link-cell as before. */
      .table-row {
        display: flex; gap: 1rem; align-items: stretch;
      }
      .table-row > .table-card { flex: 1 1 auto; margin-bottom: 0; min-width: 0; }
      /* Expanded cards (have a .table-body inside) take the full row so the
         schema editor has room to breathe. :has() is supported in 96%+ of
         browsers as of 2025; falls back to the multi-column layout below
         that, which is still readable. */
      .tables-grid > .table-row:has(.table-body) { grid-column: 1 / -1; }
      .link-cell {
        flex: 0 0 11rem;
        position: relative;
        display: flex; flex-direction: column; align-items: center;
        padding: 0.5rem;
        cursor: pointer;
        border: 1px solid var(--brown-border); border-radius: 4px;
        background: var(--cream-2);
      }
      .link-cell:hover { background: var(--cream); }
      .link-icon-bg {
        position: absolute; top: 1.5rem;
        display: flex; align-items: center; justify-content: center;
        color: var(--brown-border); pointer-events: none;
        z-index: 0;
      }
      .icon-chain { width: 4rem; height: 4rem; }
      .linked-tables { 
        position: relative; z-index: 1;
        margin: 3.5rem 0 0; padding: 0 0.5rem; text-align: center; font-size: 1.1rem; 
        color: var(--brown-dark); width: 100%; 
        background: transparent; 
        white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
        pointer-events: none;
      }
      /* Names truncate cleanly with an ellipsis instead of wrapping; full name available via title tooltip. */
      .linked-tables li {
        padding: .15rem 0;
        max-width: 100%;
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
      }
      .linked-tables.empty { color: var(--ink-mute); font-style: italic; }
      .table-card .field-row strong {
        font-weight: 400; color: var(--ink-mute);
        font-size: 1.1rem; text-transform: uppercase; letter-spacing: .08rem;
      }
      .table-card .field-row .button.danger {
        padding: 0 .5rem; min-width: auto;
      }
      /* GLOBAL select chevron override.
         Milligram ships a purple data-uri SVG chevron on every <select>.
         We replace it once, here, with our own brown chevron so the visual
         is consistent across every page (Tables editor, NEW GRID base
         picker, link editor, relationships builder, validator-add pill,
         everywhere). Per-container overrides have been a recurring source
         of regressions — see docs/conventions.md C6. Anything that needs a
         CHEVRON-LESS pill (e.g. .validator-add) opts out below. */
      select {
        appearance: none; -webkit-appearance: none; -moz-appearance: none;
        background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8' fill='none' stroke='%238b5e34' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'><path d='M2 2 L6 6 L10 2'/></svg>");
        background-repeat: no-repeat;
        background-position: right .8rem center;
        background-size: 1rem;
        padding-right: 2.4rem;
      }
      /* Pills like the validator-add ("+ validator") opt out of the chevron;
         they look more like buttons than dropdowns. */
      .field-validators select.validator-add { background-image: none; padding-right: .9rem; }
      .actions { display: flex; gap: .5rem; align-items: center; flex-wrap: wrap; }
      pre { max-height: 320px; overflow: auto; background: var(--cream); border: 1px solid var(--brown-border); }
      textarea { font-family: monospace; min-height: 100px; }

      .err { color: var(--danger); white-space: pre-wrap; font-family: monospace; font-size: 1.2rem; }
      .ok { color: #4a6b2a; font-size: 1.2rem; }
      .warn { color: #b7791f; white-space: pre-wrap; font-family: monospace; font-size: 1.2rem; }
      .message-box { display: flex; gap: .5rem; align-items: center; flex: 1; }
      .message-box .dismiss { font-size: .9rem; padding: 0 .8rem; height: 2.4rem; line-height: 2.4rem; }
      .muted { color: var(--ink-mute); font-size: 1.3rem; }
      .table-name { margin: 0; font-size: 2rem; line-height: 1; color: var(--brown-darker); letter-spacing: -.02rem; font-weight: 700; -webkit-text-stroke: .25px currentColor; }
      .field-validators { display: flex; flex-wrap: wrap; gap: .35rem; align-items: center; font-size: 1.2rem; }
      .field-validators select.validator-add {
        appearance: none; -webkit-appearance: none; -moz-appearance: none;
        background-image: none;
        margin-bottom: 0; padding: 0 .9rem; height: 2.4rem; line-height: 1; width: auto; max-width: 100%;
        font-size: 1.1rem; border-color: var(--brown-border); color: var(--brown);
        background-color: transparent; border-radius: 1.2rem;
        cursor: pointer;
      }
      .field-validators select.validator-add:hover { background-color: rgba(87,83,78,.06); }
      .validator-chip {
        display: inline-flex; align-items: center; gap: .4rem;
        background: var(--brown); color: var(--cream);
        padding: 0 .35rem 0 .8rem; height: 2.4rem; border-radius: 1.2rem;
        font-size: 1.1rem; letter-spacing: .02rem;
      }
      .validator-chip .chip-x {
        height: 1.8rem; line-height: 1; padding: 0 .5rem; font-size: 1.2rem;
        background: transparent; border: 0; color: #fff; cursor: pointer; border-radius: 999px;
      }
      .validator-chip .chip-x:hover { background: rgba(255,255,255,.18); }
      .modal-back {
        position: fixed; inset: 0; background: rgba(58,40,23,.55);
        display: flex; align-items: center; justify-content: center; z-index: 99;
        animation: modal-back-in 120ms ease-out;
      }
      .modal {
        background: var(--cream); border: 1px solid var(--brown-border); border-radius: 6px;
        padding: 0; min-width: 480px; max-width: 80vw; max-height: 80vh;
        overflow: hidden; display: flex; flex-direction: column;
        box-shadow: 0 12px 40px rgba(58,40,23,.35);
        animation: modal-in 120ms cubic-bezier(.2,.7,.2,1);
      }
      /* Reduce harshness on modal entry: backdrop fades + dialog scales up
         from 96% over 120 ms. Both animations stop after one play so the
         finished state still composes with whatever inline styles the modal
         body manipulates. */
      @keyframes modal-back-in { from { opacity: 0; } to { opacity: 1; } }
      @keyframes modal-in {
        from { opacity: 0; transform: scale(.96); }
        to   { opacity: 1; transform: scale(1); }
      }
      @media (prefers-reduced-motion: reduce) {
        .modal-back, .modal { animation: none; }
      }
      .modal-header {
        display: flex; justify-content: space-between; align-items: center; gap: 1rem;
        padding: .9rem 1.25rem;
        background: var(--cream-2);
        border-bottom: 1px solid var(--brown-border);
      }
      .modal-title { font-size: 1.5rem; color: var(--brown-darker); font-weight: 700; }
      /* Reverse-video pill used in modal titles to highlight a noun
         (e.g. the table name in "Import complete for <table>"). The
         dark fill on light background makes the badge stand out against
         the muted title text without shouting. */
      .title-badge,
      a.title-badge,
      a.title-badge:visited,
      a.title-badge:hover {
        display: inline-block;
        background: var(--brown-darker);
        color: var(--cream);
        font-family: var(--mono);
        font-size: .85em;
        font-weight: 600;
        padding: .15em .55em;
        border-radius: .35em;
        line-height: 1.2;
        vertical-align: baseline;
        text-decoration: none;
      }
      .modal-actions { display: flex; gap: .5rem; align-items: center; }
      .modal-close, .modal-copy { height: 2.4rem; padding: 0 1rem; font-size: 1.05rem; }
      .modal-copy.copied { background-color: var(--brown); color: var(--cream); border-color: var(--brown); }

      /* Import-adoption dialog (frontend/local/import-adopt.js) */
      .adopt-modal { min-width: 480px; max-width: 680px; }
      .adopt-list { display: flex; flex-direction: column; gap: .75rem; margin-top: .75rem; }
      .adopt-card { border: 1px solid var(--brown-border); border-radius: .5rem; padding: .6rem .75rem; }
      .adopt-card-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: .5rem; }
      .adopt-pknote { font-size: .85rem; }
      .adopt-cols { display: flex; flex-wrap: wrap; gap: .4rem .9rem; margin-top: .5rem; }
      .adopt-col { display: inline-flex; align-items: center; gap: .4rem; margin: 0; font-weight: 400; }
      .adopt-col-name { font-family: var(--mono); font-size: .9rem; }
      select.adopt-col-type { height: 2.2rem; padding: 0 1.6rem 0 .5rem; font-size: .85rem; line-height: 1; background-image: none; margin: 0; }
      .adopt-empty { font-size: .85rem; margin-top: .4rem; }
      .adopt-status { margin-top: .6rem; min-height: 1.2rem; font-size: .9rem; }
      .adopt-status.adopt-error { color: var(--danger, #b3261e); }
      .adopt-views-note { font-size: .85rem; border-left: 2px solid var(--brown-border); padding-left: .6rem; margin: 0 0 .25rem; }
      /* Adoption completion log */
      .adopt-log { gap: .25rem; }
      .adopt-log-row { display: flex; align-items: baseline; gap: .5rem; font-size: .9rem; }
      .adopt-log-row.adopt-error { color: var(--danger, #b3261e); }
      .adopt-log-mark { width: 1rem; flex-shrink: 0; font-weight: 700; }
      .adopt-log-name { font-family: var(--mono); }
      .adopt-log-msg { font-size: .82rem; }
      /* Convert-to-link-table dialog rows */
      .pj-row { display: flex; align-items: center; gap: .5rem; margin-top: .6rem; }
      .pj-label { min-width: 3.5rem; font-weight: 600; }
      .pj-row select.modal-select { margin: 0; height: 2.4rem; line-height: 1; background-image: none; }
      .pj-arrow { opacity: .6; }

      /* Link editor — list + sentence builder */
      .modal-link { min-width: 620px; max-width: 800px; }
      .link-body { padding: 1.25rem 1.5rem; font-family: inherit; font-size: 1.3rem; line-height: 1.5; white-space: normal; max-height: 70vh; overflow-y: auto; }
      .link-row.sentence {
        display: flex; align-items: center; flex-wrap: wrap; gap: .6rem;
        padding: .5rem 0; border-bottom: 1px dotted var(--brown-border);
      }
      .link-row.sentence:last-child { border-bottom: 0; }
      .link-row.sentence.add-link { margin-top: 0; padding-top: .75rem; border-top: 1px solid var(--brown-border); border-bottom: 0; }
      .link-open-new { margin-top: .75rem; }
      .link-new-form { margin-top: .5rem; padding-top: .75rem; border-top: 1px solid var(--brown-border); }
      .link-new-form-actions {
        display: flex; flex-wrap: wrap; align-items: center; gap: .75rem;
        margin-top: 1rem; padding-top: .25rem;
      }
      .link-save-relationship { min-width: 12rem; }
      .modal-body .link-dual-clauses {
        display: block !important;
      }
      .modal-body .link-clause { display: flex !important; align-items: center !important; gap: .6rem !important; margin-bottom: 0.5rem !important; }
      .link-row-header {
        display: flex; align-items: center; gap: .6rem; cursor: pointer;
        padding: .4rem .35rem; border-radius: 4px;
      }
      .link-row-header:hover { background: rgba(87,83,78,.08); }
      .link-row-header .chevron { color: var(--brown); width: 1rem; font-size: 1.4rem; line-height: 1; }
      .link-row-name { font-weight: 700; color: var(--brown-darker); flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
      .link-row-content { margin-top: .5rem; padding: 0 .35rem .75rem 1.6rem; border-bottom: 1px dotted var(--brown-border); }
      .link-row-wrap:last-of-type .link-row-content { border-bottom: 0; }
      .modal-body .link-clause select {
        display: inline-block !important;
        width: auto !important;
        min-width: 10rem !important;
        padding: 0.15rem 2rem 0.15rem 0.5rem !important;
        background-image: none !important;
        appearance: auto !important;
      }
      .link-row-actions { display: flex; align-items: center; gap: .5rem; margin-left: auto; flex-shrink: 0; }
      .link-table {
        font-weight: 700; color: var(--brown-darker);
        background: var(--cream-2); border: 1px solid var(--brown-border); border-radius: 3px;
        padding: .15rem .5rem;
        white-space: nowrap; flex-shrink: 0;
      }
      .link-verb { color: var(--brown); font-style: italic; }
      .link-row.sentence select { margin: 0; min-width: 9rem; }

      /* Confirm dialog */
      .modal-confirm { min-width: 360px; max-width: 480px; }
      .modal-confirm .confirm-body {
        font-family: inherit; font-size: 1.4rem; line-height: 1.5;
        white-space: normal; padding: 1.25rem 1.5rem;
        color: var(--text);
      }
      /* Multi-line prompts inside confirm/choose dialogs preserve
         intentional `\n` from the caller so options like the
         export-schema chooser can lay out as
           Export schema for:
             _JUNCTION_x_y
           as:
         instead of squashing onto one line. */
      .modal-confirm .confirm-body > p { white-space: pre-wrap; margin: 0; }
      .modal-footer {
        display: flex; justify-content: flex-end; gap: .5rem;
        padding: .9rem 1.25rem;
        background: var(--cream-2);
        border-top: 1px solid var(--brown-border);
      }
      .button.danger-filled {
        background-color: var(--danger); border-color: var(--danger); color: #fff;
      }
      .button.danger-filled:hover, .button.danger-filled:focus {
        background-color: #a3311f; border-color: #a3311f; color: #fff;
      }
      .modal-body {
        margin: 0; padding: 1.25rem 1.5rem;
        overflow: auto; background: var(--surface);
        font-family: var(--mono); font-size: 1.25rem; line-height: 1.55;
        white-space: pre-wrap; word-break: normal; overflow-wrap: anywhere;
        font-variant-ligatures: none; font-feature-settings: "liga" 0, "calt" 0;
        flex: 1;
      }
      .modal-body code { font: inherit; background: transparent; padding: 0; color: var(--text); }

      /* Permissions tab */
      .perm-row { display: flex; align-items: center; gap: .75rem; padding: .35rem 0; border-bottom: 1px dotted var(--brown-border); }
      .perm-row:last-child { border-bottom: 0; }
      .perm-email { font-weight: 700; color: var(--brown-darker); flex: 0 0 auto; }
      .perm-note { flex: 1; }
      .perm-add { margin-top: .75rem; }
      .perm-group { padding: 1rem 0; border-bottom: 1px dashed var(--brown-border); }
      .perm-group:last-child { border-bottom: 0; }
      .perm-group-head { display: flex; align-items: center; gap: .75rem; margin-bottom: .5rem; }
      .perm-group-head strong { color: var(--brown-darker); font-size: 1.5rem; }
      .perm-list, .perm-members { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; padding: .5rem 0; }
      .perm-sub-title { width: 8rem; font-size: 1.1rem; text-transform: uppercase; letter-spacing: .05rem; }
      .perm-check { display: inline-flex; align-items: center; gap: .25rem; padding: .15rem .5rem .15rem 0; font-size: 1.15rem; cursor: pointer; }
      .perm-check input { margin: 0; }
      .perm-members input { margin: 0; max-width: 20rem; }

      /* JSON key tokens — distinct from string values */
      .tok-json-key { color: #6b4423; font-weight: 500; }

      /* Grid page */
      .grid-main { padding: 0; }
      /* Grid page is edge-to-edge: drop body padding + Milligram's .container width cap. */
      body.on-grid { padding: 0; }
      body.on-grid #root.container,
      body.on-grid .container { max-width: none; padding: 0; }
      body.on-grid header { padding: .75rem .5rem .75rem .5rem; margin: 0; border-bottom: 1px solid var(--brown-border); }
      body.on-grid .grid-wrap { height: calc(100vh - 4rem); }

      /* Settings page borrows the grid page's edge-to-edge header so the
         `csvdb ᚙ` brand pins to the top-left corner. Page body still gets
         comfortable inner padding via `main`; only the header is flush. */
      body.on-settings { padding: 0; }
      body.on-settings #root.container,
      body.on-settings .container { max-width: none; padding: 0; }
      /* Settings adds the bits the home page doesn't need: a top
         padding (home's brand sits below the body's 15px top padding;
         settings is flush to top=0 so the header itself eats it), and
         flex-wrap so a long row of tab buttons wraps cleanly. The
         common chrome (flex / border / gap / margin) lives on
         `.page-header` above. */
      body.on-settings .page-header { padding: 1.5rem 1.5rem .75rem 1.5rem; flex-wrap: wrap; }
      body.on-settings main { padding: 0 1.5rem 1.5rem; }
      /* Settings' brand is an anchor (back to home); home's is an <h1>.
         Same `.home-brand` class otherwise — kill the default underline
         + visited colour so the wordmark looks identical. */
      a.home-brand, a.home-brand:visited { color: var(--brown-darker); text-decoration: none; }
      a.home-brand:hover { color: var(--brown); }
      .grid-wrap { position: relative; display: flex; flex-direction: column; height: calc(100vh - 6rem); }
      /* Override clusterize.css's default `max-height: 200px` on the
         scroll container — we want the grid body to take whatever
         vertical space the flex parent allots, all the way to the
         bottom of the page. */
      .grid-scroll.clusterize-scroll { max-height: none; flex: 1 1 0%; min-height: 0; }
      .grid-title-bar {
        display: flex; align-items: center; gap: 1rem; flex-wrap: wrap;
        padding: .75rem 1rem; border-bottom: 1px solid var(--brown-border); background: var(--cream-2);
      }
      .grid-title { font-size: 1.6rem; font-weight: 700; color: var(--brown-darker); display: inline-flex; align-items: center; gap: .4rem; }
      /* ☰ next to the grid title. Element-qualified so the rule wins
         against Milligram's bare `button` defaults (uppercase +
         letter-spaced + tall purple chrome) — see AGENTS.md C8. */
      button.grid-title-actions {
        display: inline-flex; align-items: center; color: var(--brown-light);
        background: transparent; border: 0; padding: 0 .25rem; margin: 0 0 0 .35rem;
        cursor: pointer; line-height: 1; border-radius: 3px;
        font-size: inherit; font-weight: inherit; letter-spacing: 0;
        text-transform: none; height: auto; min-height: 0;
      }
      button.grid-title-actions:hover, button.grid-title-actions:focus {
        color: var(--brown-darker); background: var(--cream-2);
      }
      .grid-title-icon { display: inline-flex; align-items: center; justify-content: center; width: 18px; height: 18px; }
      .grid-title-icon-menu { font-size: 1.2rem; line-height: 1; }
      /* URL pre-filter chip — surfaces when a copy-pasted URL or FK-link
         click carried `?f.<col>=<expr>` overlay filters. Lives in the
         grid title bar; × clears the overlay (saved filters untouched). */
      .grid-prefilter-chip {
        display: inline-flex; align-items: center; gap: .35rem;
        padding: .15rem .5rem; border-radius: 1rem;
        background: var(--cream-2); border: 1px solid var(--brown-border);
        font-size: .95rem; color: var(--brown-darker);
      }
      .grid-prefilter-chip .prefilter-label { color: var(--brown-light); }
      .grid-prefilter-chip .prefilter-pair strong { font-weight: 700; }
      .grid-prefilter-chip .prefilter-clear {
        background: transparent; border: 0; padding: 0 .15rem; margin: 0;
        color: var(--brown-light); cursor: pointer; height: auto;
        line-height: 1; font-size: 1.1rem;
      }
      .grid-prefilter-chip .prefilter-clear:hover { color: var(--brown-darker); }
      /* Junction-grid badge — chain-link icon stands in for the
         `_JUNCTION_` filename prefix. Same muted tint as the table icon
         so it reads as glyph-not-text. */
      /* Junction-grid header glyph: chain-link character at the title
         font-size (so it scales with the title). Class also carries
         `.icon-junction` for source/cross-surface consistency, but the
         home-rail-specific 16px sizing is scoped to .home-default-link
         and doesn't apply here. */
      .grid-title-junction { display: inline-flex; align-items: center; color: var(--brown-light); margin-right: .25rem; }
      /* Table-glyph badge to the left of default-grid titles. Matches
         the home rail's `.icon-table` size + tint so the affordance
         reads as the same icon on both surfaces. */
      .grid-title-default { display: inline-flex; align-items: center; color: var(--brown-light); margin-right: .35rem; }
      .grid-base { font-size: 1.1rem; }
      /* Milligram's default `input { height: 3.8rem }` makes the grid
         search bar taller than the adjacent buttons (which sit at
         ~3rem). Shrink it to match the title-bar button height so the
         row reads as one consistent line. */
      input.grid-search { flex: 1; max-width: 36rem; margin: 0; height: 3rem; padding: 0 .8rem; line-height: 1; }
      .grid-filter-toggle.active { background-color: var(--brown); border-color: var(--brown); color: var(--cream); }
      .grid-count { font-size: 1.1rem; }
      /* Status pill — sits at the right end of the title bar. Idle: invisible
         to keep the bar clean. Saving / saved / error states use distinct
         colours so a glance tells the user what just happened. Error state
         is sticky until the next successful save clears it. */
      .grid-status {
        font-size: 1.1rem; padding: .15rem .55rem; border-radius: 1rem;
        font-family: var(--mono); white-space: nowrap; max-width: 28rem;
        overflow: hidden; text-overflow: ellipsis;
        transition: opacity .15s ease;
      }
      .grid-status[data-kind="idle"]   { opacity: 0; pointer-events: none; }
      .grid-status[data-kind="saving"] { background: var(--cream-2); color: var(--ink-mute); opacity: 1; }
      .grid-status[data-kind="ok"]     { background: var(--ok-bg, rgba(74,107,42,.18)); color: var(--ok-ink, #4a6b2a); opacity: 1; }
      /* Error state: show the FULL message, but the pill only hugs its text
         (no flex-grow) — it can extend up to the visible edge, then wrap. The
         title bar's flex-wrap drops it onto its own line under the FILTER /
         + ROW row on small screens instead of clipping. */
      .grid-status[data-kind="err"] {
        background: var(--danger-bg); color: var(--danger); opacity: 1; cursor: help;
        white-space: normal; max-width: 100%; overflow: visible; text-overflow: clip;
      }
      .grid-scroll { flex: 1; overflow: auto; outline: none; background: var(--surface); }
      .grid-scroll:focus { box-shadow: inset 0 0 0 2px var(--brown-light); }
      /* Element-qualify (AGENTS C8) so Milligram's responsive
         `table { display: block; overflow-x: auto }` below 640px
         doesn't hijack `.grid-table` as its own scroll container —
         which would anchor the sticky `<thead>` to the (non-scrolling)
         table rather than `.grid-scroll`, and the user would see the
         column header row scroll off with the data. */
      /* `table-layout: fixed` is load-bearing for scroll performance: the grid
         already assigns explicit per-column widths to the <colgroup>, so fixed
         layout takes them as authoritative and lays out in O(visible rows). With
         the default `auto`, every forced reflow — Clusterize reads scrollTop on
         each scroll tick — re-measures EVERY cell across all rendered rows to
         recompute column widths (O(rows×cols)), which thrashed badly on wide /
         tall imported tables (measured ~7s of layout under fast scroll). */
      table.grid-table { table-layout: fixed; border-collapse: separate; border-spacing: 0; width: max-content; min-width: 100%; font-size: 1.2rem; display: table; overflow: visible; }
      /* Pin the header row to the top of `.grid-scroll` as the user
         scrolls. `position: sticky` on `<th>` alone leaves a gap on
         some browsers when the row is the table's containing block;
         also pinning `<thead>` and `<tr>` plugs that. z-index here
         must beat the frozen-column header rule (z: 5) only on the
         intersection, so we keep the th z at 2 (frozen columns
         override with their own higher z below). */
      .grid-table thead { position: sticky; top: 0; z-index: 3; }
      .grid-table thead tr { position: sticky; top: 0; }
      .grid-table thead th { position: sticky; top: 0; z-index: 2; background: var(--cream-2); border-bottom: 1px solid var(--brown-border); border-right: 1px solid var(--brown-border); padding: .35rem .5rem; text-align: left; font-weight: 700; color: var(--brown-darker); font-size: 1.15rem; line-height: 1.4; user-select: none; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
      /* Align the key icon's baseline to the label's so FK columns and
         plain columns drop their text at the same vertical position. */
      .grid-table thead th .th-label,
      .grid-table thead th .th-fk-key { vertical-align: middle; }
      /* Required-column marker — green superscript `*` after the title, matching
         the schema editor's required star (#3b6e6a). */
      .th-required-star { color: #3b6e6a; font-weight: 700; margin-left: .1rem; }
      /* Frozen columns — sticky horizontally + opaque so scrolled content does
         not bleed through. Header frozen z-index sits above body frozen, which
         sits above any non-frozen cell. */
      .grid-table thead th.frozen { position: sticky; z-index: 5; background: var(--cream-2); }
      .grid-table tbody td.frozen { position: sticky; z-index: 3; background: var(--surface); }
      /* Visual freeze line on the rightmost frozen column. Set via class on the
         server-rendered cell when ci === frozenN-1 — but we don't track that
         attr; instead use box-shadow on every frozen cell that targets a
         later sibling. The last frozen column is the one without a `.frozen`
         next sibling, so a CSS `:has` selector handles it. */
      .grid-table thead th.frozen:not(:has(+ .frozen)),
      .grid-table tbody td.frozen:not(:has(+ .frozen)) {
        box-shadow: 1px 0 0 var(--brown-border), 4px 0 6px -3px rgba(58,40,23,.18);
      }
      /* Selection / focus colours on frozen cells must stay opaque; rgba over
         white parent reads OK but we explicitly bias to remain readable. */
      .grid-table tbody td.frozen[data-selected="1"] { background: #ebdfca; }
      /* Focused frozen cell — only paint in kb-nav mode so mouse mode
         shows nothing extra on a previously-clicked cell that isn't
         currently under the pointer (matches the row-stripe gating
         above). In mouse mode the cell under the pointer gets the
         td:hover styling via the rule lower down. */
      body.grid-kb-nav .grid-table tbody td.frozen[data-focused="1"] { background: #d8c0a0; }
      body:not(.grid-kb-nav) .grid-table tbody td.frozen:hover { background: #d8c0a0; }
      /* Read-only on frozen cells: solid blend of white + readonly tint, so
         scrolled content cannot bleed through the way the rgba overlay would. */
      .grid-table tbody td.frozen[data-readonly="1"] { background: #f7f0e2; }
      .grid-table tbody td.frozen[data-readonly="1"][data-selected="1"] { background: #e6d8be; }
      body.grid-kb-nav .grid-table tbody td.frozen[data-readonly="1"][data-focused="1"] { background: #d4d4d8; }
      body:not(.grid-kb-nav) .grid-table tbody td.frozen[data-readonly="1"]:hover { background: #d4d4d8; }
      /* overflow:visible (vs the base thead th's `overflow:hidden`, which
         ellipsises header labels) so the FK/choice pill typeahead dropdown
         — absolutely positioned at `top:100%` of its cell — can escape the
         filter cell instead of being clipped to the ~2rem row and rendering
         invisible/unclickable behind the data rows. */
      .grid-table thead tr.filter-row th { top: 2.6rem; padding: .15rem .25rem; cursor: default; overflow: visible; }
      /* Funnel icon stands in for the literal placeholder word "filter" — a
         left adornment in every filter input (plain + pill). The existing
         `.filter-active` rules below use the `background` shorthand, which
         clears this image, so the funnel naturally disappears once the column
         is actually filtered (and the yellow wash takes over). */
      .grid-table thead .filter-input {
        width: 100%; height: 2rem; padding: 0 .35rem 0 1.4rem; font-size: 1rem; margin: 0; border-color: var(--brown-border);
        background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24'%3E%3Cpath d='M3 5h18l-7 8v5l-4 2v-7z' fill='%23a08b6a'/%3E%3C/svg%3E");
        background-repeat: no-repeat; background-position: .4rem center; background-size: 12px 12px;
      }
      /* Keep any placeholder word very faint — the funnel icon is the real
         affordance now; the text is just a fallback hint. */
      .grid-table thead .filter-input::placeholder { color: var(--ink-mute, #b8a888); opacity: .35; }
      /* Faint yellow wash on a filter cell that currently holds a value, so an
         active filter is obvious at a glance (a view can look "empty" when it's
         really just filtered). Works for both the plain text box and the
         FK/choices pill widget. */
      .grid-table thead .filter-input.filter-active,
      .grid-table thead .filter-pills.filter-active { background: rgba(245, 205, 66, .22); }
      .grid-table thead .filter-pills.filter-active { border-radius: 3px; }
      .grid-table thead .filter-pills.filter-active .filter-pill-input { background: transparent; }
      /* FK / choices multi-select pill filter (replaces the text box for those
         columns). Pills + the search input share ONE wrapping row so the input
         (and its funnel icon) sits to the right of the last pill and only wraps
         to a new line when it runs out of horizontal room. position:relative
         anchors the absolutely-positioned menu. `display:contents` dissolves the
         pills box so its chips become direct flex items of `.filter-pills`,
         flowing inline with the input. */
      .grid-table thead .filter-pills { position: relative; display: flex; flex-wrap: wrap; align-items: center; gap: .2rem; }
      .filter-pills-box { display: contents; }
      /* The pill search input flows inline (not full-width like the plain
         filter box) and grows to fill the leftover space on the row. */
      .grid-table thead .filter-pill-input { width: auto; flex: 1 1 3rem; min-width: 2.5rem; }
      .filter-pills .filter-pill {
        display: inline-flex; align-items: center; gap: .2rem;
        max-width: 100%; padding: 0 .15rem 0 .4rem; height: 1.6rem;
        background: var(--brown-darker); color: var(--cream);
        border-radius: .8rem; font-size: .95rem; line-height: 1;
        white-space: nowrap; overflow: hidden;
      }
      .filter-pills .filter-pill > :first-child { overflow: hidden; text-overflow: ellipsis; }
      .filter-pill-x {
        flex-shrink: 0; height: 1.3rem; width: 1.3rem; padding: 0; margin: 0;
        display: inline-flex; align-items: center; justify-content: center;
        background: transparent; border: 0; color: var(--cream); cursor: pointer;
        font-size: 1.1rem; line-height: 1; border-radius: 999px;
      }
      .filter-pill-x:hover { background: rgba(255,255,255,.2); }
      .filter-pill-menu {
        position: absolute; top: 100%; left: 0; z-index: 30;
        /* Size to the widest option, not the column: a wide FK column (e.g. one
           given the "extend to fill" leftover width) would otherwise stretch the
           menu across the screen, because a `min-width: 100%` larger than the cap
           wins over `max-width`. Bound it between a sensible floor and cap so the
           dropdown stays a tidy width regardless of the underlying cell width. */
        width: max-content; min-width: 12rem; max-width: min(22rem, 90vw);
        max-height: 14rem; overflow-y: auto;
        background: var(--surface); border: 1px solid var(--brown-border);
        border-radius: 4px; box-shadow: 0 6px 20px rgba(58,40,23,.25); margin-top: 2px;
      }
      .filter-pill-opt { padding: .3rem .5rem; font-size: 1rem; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
      /* Single highlight only: the `.active` class (driven by BOTH keyboard
         nav and mousemove) is the one source of truth. Deliberately NOT using
         `:hover` — otherwise a stationary mouse keeps its hover background while
         the keyboard highlights a different row, showing two selected items. */
      .filter-pill-opt.active { background: rgba(80,140,235,.18); }
      .filter-pill-empty { padding: .3rem .5rem; font-size: .95rem; color: var(--brown-light, #a1a1aa); font-style: italic; }
      .grid-table tbody td { border-bottom: 1px dotted var(--brown-border); border-right: 1px solid var(--grid-col-sep, #efe6d3); padding: .25rem .5rem; vertical-align: top; white-space: pre-wrap; word-break: break-word; }
      /* Save confirmation: a very faint diagonal pattern that pulses once over a
         cell that just persisted. Overlay via ::after so it doesn't disturb the
         cell's own background / selection tint. Frozen cells are already
         position:sticky (a containing block); non-frozen need relative. */
      .grid-table tbody td.cell-saved-pulse:not(.frozen) { position: relative; }
      .grid-table tbody td.cell-saved-pulse::after {
        content: ""; position: absolute; inset: 0; pointer-events: none; z-index: 1;
        background-image: repeating-linear-gradient(45deg, #6aa84f 0 1.5px, transparent 1.5px 7px);
        opacity: 0; animation: cell-saved-pulse .75s ease-out;
      }
      @keyframes cell-saved-pulse { 0%, 100% { opacity: 0; } 35% { opacity: .45; } }
      /* Clamp wrapped cells at exactly N lines (no half-line of empty space
         below). 4em was a fractional multiple of line-height (~2.5 lines)
         which left visible whitespace under 2-line content. */
      .grid-table tbody td .cell-inner {
        display: -webkit-box;
        -webkit-line-clamp: 2;
        -webkit-box-orient: vertical;
        overflow: hidden;
      }
      /* id / nid hold a single fixed-length token (an 8-char nid). Never let it
         wrap to a second line: `word-break: break-word` on the td would
         otherwise break the token mid-string whenever the column happens to be
         narrow (e.g. when autoFit measures before the cells are rendered and
         falls back to the short "nid" header width). Forcing nowrap + a single
         clamped line makes wrapping impossible regardless of width; the `ch`
         min-width gives a deterministic, font-relative floor wide enough for
         the whole nid (so it survives grid font-size changes without a JS
         re-measure). This removes the recurring regression at its source. */
      .grid-table tbody td.grid-cell-nid { white-space: nowrap; word-break: normal; min-width: 10ch; }
      .grid-table tbody td.grid-cell-nid .cell-inner {
        -webkit-line-clamp: 1;
        white-space: nowrap;
        min-width: 9ch;
      }
      /* Mouse-hover and keyboard-focused row both get the cream stripe so
         keyboard navigation has the same visual cue as the mouse. The
         `data-focused-row="1"` marker is set on the <tr> containing the
         focused cell — see buildClusterRows() in grid.js. While the user is
         in keyboard-nav mode (body.grid-kb-nav), the `:hover` rule is
         suppressed so the mouse's last-touched row doesn't linger alongside
         the keyboard-focused row. */
      /* Row stripe: mouse-hover OR keyboard-focused row — only one at a
         time. The body.grid-kb-nav class is added on every keydown
         inside the grid and cleared on every mousemove / mousedown
         (frontend/grid/index.js), so the two sources of "current row"
         alternate cleanly:
           - kb-nav mode → keep the keyboard's focused-row stripe; hide
             the mouse-hover stripe (so a last-touched row doesn't
             linger next to the keyboard's row).
           - mouse mode → keep the mouse-hover stripe; hide the
             keyboard's focused-row stripe (mouse "steals" the cursor
             back from the keyboard).
         The :hover and the data-focused-row selectors carry the same
         peach #faf3e6; gating them per mode means the user sees
         exactly one row highlighted at any moment. */
      body.grid-kb-nav .grid-table tbody tr[data-focused-row="1"] td { background: #faf3e6; }
      body:not(.grid-kb-nav) .grid-table tbody tr:hover td { background: #faf3e6; }
      /* Each of these state rules adds `tr` to the selector chain so they have
         the same specificity as `tr:hover td` (which has an extra `:hover`
         pseudo on `tr`). Without the `tr` here, hover would beat selected
         while the user is mid-drag — visible as "alternating" highlights on
         the hovered row across a multi-row selection. */
      .grid-table tbody tr td[data-selected="1"] { background: rgba(87,83,78,.18); }
      /* Per-cell "current cell" tile: keyboard sets data-focused when
         arrow-keying, mouse hovers do not. We gate the focused-cell
         outline behind kb-nav mode and surface a mirrored td:hover
         outline in mouse mode so mouse hover "steals" the current
         cell from any prior keyboard focus when the pointer moves.
         This current-cell outline (hover / keyboard cursor) is a SOFTENED
         brown (color-mix → ~55%) so it reads as less bright than the cell
         being EDITED, whose border is the editor input's full-`--brown`
         box-shadow. Same softening is applied to the dark / auto theme
         hover rules above. */
      body.grid-kb-nav .grid-table tbody tr td[data-focused="1"] { outline: 2px solid color-mix(in srgb, var(--brown) 55%, transparent); outline-offset: -2px; background: rgba(87,83,78,.28); }
      body:not(.grid-kb-nav) .grid-table tbody td:hover { outline: 2px solid color-mix(in srgb, var(--brown) 55%, transparent); outline-offset: -2px; background: rgba(87,83,78,.28); }
      /* NB: the current-cell BORDER is exclusively the kb-nav data-focused rule
         above (keyboard mode) or td:hover (mouse mode) — never an unconditional
         data-focused outline. In mouse mode the DOM's data-focused is allowed to
         go stale relative to the pointer (mouseover updates JS state without a
         re-render), so an always-on data-focused outline would paint a second,
         stray "current cell". A single click instead shows the data-selected
         tint below, persisting like a 1-cell shift-selection. */
      .grid-table tbody tr td[data-readonly="1"] { color: var(--ink-mute); background: rgba(87,83,78,.04); }
      /* Hover (or keyboard-focused row) on a read-only cell — same cream as a
         hovered non-readonly cell so the row's stripe reads as a unified line
         across all columns (id included). Higher specificity than plain readonly. */
      body:not(.grid-kb-nav) .grid-table tbody tr:hover td[data-readonly="1"] { background: #faf3e6; }
      body.grid-kb-nav .grid-table tbody tr[data-focused-row="1"] td[data-readonly="1"] { background: #faf3e6; }
      /* Selection on a read-only cell needs an explicit override — without
         these combos, the read-only background (defined earlier with equal
         or lower specificity) wins and the selection highlight disappears.
         Listed AFTER hover-readonly so selection/focus beat hover when both
         apply (e.g., user hovers a row that contains a selected id cell). */
      .grid-table tbody tr td[data-readonly="1"][data-selected="1"] { background: rgba(87,83,78,.18); }
      /* Readonly + focused: same gating as the non-readonly focused rule
         above — only paint in kb-nav mode, and mirror via td:hover in
         mouse mode, so the highlight follows whichever path the user
         most recently used. */
      body.grid-kb-nav .grid-table tbody tr td[data-readonly="1"][data-focused="1"] { background: rgba(87,83,78,.28); }
      body:not(.grid-kb-nav) .grid-table tbody td[data-readonly="1"]:hover { background: rgba(87,83,78,.28); }
      .ws { color: var(--brown-light); opacity: .7; }
      .cell-null { color: #b0a085; font-style: italic; opacity: .7; }
      /* "(auto)" placeholder in a new row's server-assigned id / nid cells. */
      .cell-auto { color: #b0a085; font-style: italic; opacity: .7; }
      /* Boolean cells: readonly glyph (✓ / ✗ / –) reads the same colour
         family as the rest of the row; editable inline checkbox dims to
         .35 opacity when the underlying value is null so the unset
         state is distinguishable from explicit `false`. */
      .cell-bool { display: inline-block; line-height: 1; font-weight: 700; }
      /* Readonly glyphs render bigger than the cell text so the ✓ / ✗
         remain legible at a glance — matches the visual weight of the
         editable checkbox. */
      .cell-bool-readonly { font-size: 1.4rem; }
      .cell-bool-true { color: var(--ok-ink, #2f7a3a); }
      .cell-bool-false { color: var(--danger); }
      .cell-bool-null { color: #b0a085; }
      .cell-bool-cb { cursor: pointer; margin: 0; vertical-align: middle; }
      /* Null booleans share the same italic "null" placeholder as
         other column types. The editable variant adds a cursor +
         keyboard affordance via tabindex/role on the span (rendered
         inline). */
      .cell-bool-null-text { cursor: pointer; }
      .grid-footer { padding: .35rem .75rem; border-top: 1px solid var(--brown-border); background: var(--cream-2); font-size: 1.1rem; color: var(--ink-mute); }
      .grid-error.err:empty { display: none; }
      .grid-error.err:not(:empty) { padding: .6rem .75rem; }
      /* Fatal / load error text scrolls in its own region so a long message
         can't push the grid off-screen. (Insert/save errors don't come here —
         they use the title-bar status pill + the suggestion row below it.) */
      .grid-error-text { max-height: 8rem; overflow-y: auto; white-space: pre-wrap; }
      /* Suggestion row directly under the title bar. Green squared "SUGGESTION"
         tag + advice text; hidden when empty. */
      .grid-suggest-host:empty { display: none; }
      /* Block container so the tag + text flow inline on one line (text wraps
         naturally right after the SUGGESTION chip — no forced line break). */
      .grid-suggest {
        display: block; padding: .4rem 1rem; border-bottom: 1px solid var(--brown-border);
        background: rgba(74, 107, 42, .1); font-size: 1.15rem; line-height: 1.5;
      }
      .grid-suggest-tag {
        display: inline-block; vertical-align: baseline; margin-right: .15rem;
        background: var(--ok-ink, #4a6b2a); color: #fff;
        padding: .1rem .5rem; border: 1px solid #3c5722; border-radius: 0;
        font-weight: 700; font-size: 1rem; letter-spacing: .03em;
      }
      .grid-suggest-text { color: var(--brown-darker); }
      .grid-edit-layer { z-index: 5; }
      /* Editor box: border drawn via box-shadow so it does not reduce the
         content area, keeping the editor's text at the same vertical
         position as the cell's text (top-aligned inside the same .25rem
         padding box). The `input.` / `textarea.` qualifiers raise the
         specificity above Milligram's element-typed input/textarea rules
         which would otherwise win. */
      textarea.grid-edit {
        font-family: var(--mono);
        font-size: 1.2rem;
        padding: .25rem .5rem;
        border: 0;
        box-shadow: 0 0 0 2px var(--brown);
        background: var(--surface);
        color: var(--text);
        margin: 0;
        line-height: 19.2px;
        vertical-align: top;
        min-height: 0;         /* defeat Milligram's textarea min-height: 6.5rem */
        height: auto;
        resize: none;          /* default: single-line edit, no resize handle */
        overflow: hidden;      /* hide scrollbar for single-line edits */
      }
      textarea.grid-edit.grid-edit-multi { resize: vertical; overflow: auto; }
      /* Date / datetime ghost-text scaffold (frontend/lib/date-scaffold.js).
         The editor goes transparent-bg + monospace; a ghost div sits behind
         it showing the remaining YYYY-MM-DD HH:MM:SS template. The time part
         is rendered EVEN lighter than the date part to signal it's optional
         (a date alone is a valid datetime). Shared by the grid cell editor
         AND the public form so both read identically. */
      .datelike-input { font-family: var(--mono); background: transparent !important; position: relative; z-index: 1; white-space: nowrap; overflow-x: hidden; }
      .datelike-ghost {
        position: absolute; margin: 0; box-sizing: border-box;
        pointer-events: none; white-space: pre; overflow: hidden;
        border-style: solid; border-color: transparent;
        background: var(--surface); color: transparent; z-index: 0;
      }
      /* Muted date ghost; even fainter time ghost — opacity over --text so it
         reads as "grey / lighter grey" in both the light and dark themes. */
      .datelike-ghost-date { color: var(--text); opacity: .40; }
      .datelike-ghost-time { color: var(--text); opacity: .22; }
      /* FK typeahead editor (frontend/lib/fk-typeahead.js): a text input + a
         floating results list that queries the server as the user types.
         Used as the grid FK cell editor and (later) published/owner forms. */
      .fk-ta { position: relative; box-sizing: border-box; }
      input.fk-ta-input {
        box-sizing: border-box; width: 100%;
        /* Mirror the non-editing cell text exactly — same font, line-height and
           padding as .cell-inner / the td (1.2rem / 1.6, .25rem .5rem) so the
           value stays in the identical spot when the editor opens. Square
           corners + an INSET ring so the box edges sit on the cell's own top /
           bottom gridlines rather than a rounded pill floating over them. */
        font-family: var(--mono); font-size: 1.2rem; line-height: 1.6;
        padding: .1rem .5rem .4rem; margin: 0; border: 0; border-radius: 0;
        height: calc(1.6em + .5rem);
        box-shadow: 0 0 0 2px var(--brown);
        background: var(--surface); color: var(--text);
      }
      .fk-ta-list {
        position: absolute; top: 100%; left: 0; z-index: 30;
        /* Grow to fit the widest option (people / city names are often wider
           than a narrow cell) instead of ellipsis-clipping; capped so a very
           long value can't run off-screen. The min tracks the input width but
           is itself capped (`min(100%, 14rem)`) so a wide stretchy last column
           doesn't force a near-full-screen-wide list — content + repositionList
           keep it sized + on-screen. */
        min-width: min(100%, 14rem); width: max-content; max-width: min(36rem, 90vw);
        max-height: 240px; overflow-y: auto;
        background: var(--surface);
        box-shadow: 0 0 0 2px var(--brown), 0 4px 14px rgba(0, 0, 0, .18);
        font-family: var(--mono); font-size: 1.1rem;
      }
      .fk-ta-row { padding: .2rem .5rem; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
      .fk-ta-active { background: var(--brown-light); }
      .fk-ta-none, .fk-ta-new { color: var(--ink-mute); }
      .fk-ta-new { border-top: 1px solid var(--brown-light); }
      .fk-ta-empty { padding: .3rem .5rem; color: var(--ink-mute); font-style: italic; }
      /* The FK editor popup is given a comfortable min width in JS (it floats
         above the grid, so overflowing a narrow cell is fine — see grid/index.js). */
      .grid-edit-fk { box-sizing: border-box; }
      /* Stepped (two-level) FK picker: a back-crumb above the active level. */
      .fk-ta-stepped { position: relative; box-sizing: border-box; }
      .fk-ta-back {
        display: block; width: 100%; box-sizing: border-box; text-align: left;
        font-family: var(--mono); font-size: 1.2rem; line-height: 1.6;
        text-transform: none; letter-spacing: normal; font-weight: 400;
        padding: .25rem .5rem; margin: 2px 0 0; border: 0; border-radius: 0;
        height: calc(1.6em + .5rem);
        box-shadow: 0 0 0 2px var(--brown);
        background: var(--brown-light); color: var(--text); cursor: pointer;
      }
      .col-resize-handle { position: absolute; top: 0; right: 0; width: .5rem; height: 100%; cursor: col-resize; user-select: none; }
      .col-resize-handle:hover { background: var(--brown-light); opacity: .5; }
      .ctx-menu { position: fixed; z-index: 220; background: var(--surface); border: 1px solid var(--brown-border); border-radius: 4px; box-shadow: 0 4px 16px rgba(58,40,23,.25); min-width: 12rem; padding: .25rem 0; max-height: calc(100vh - 12px); overflow-y: auto; }
      .ctx-item { padding: .35rem .9rem; cursor: pointer; font-size: 1.2rem; color: var(--text); }
      .ctx-item:hover { background: var(--cream-2); }
      .ctx-item.ctx-null em { font-style: italic; color: #b0a085; }
      .ctx-item.ctx-danger { color: var(--red, #c0392b); }
      .ctx-item.ctx-danger:hover { background: var(--red, #c0392b); color: #fff; }
      /* Keyboard-roving highlight (Space-opened menu); mirrors :hover. */
      .ctx-item.ctx-active { background: var(--cream-2); }
      .ctx-item.ctx-danger.ctx-active { background: var(--red, #c0392b); color: #fff; }
      .grid-header-spacer { flex: 1; }

      /* Typed value input (Fill with value + Generate constants). The select
         needs C7's defensive rules: kill Milligram's purple chevron
         (background-image:none) + line-height:1 so text doesn't drop. */
      /* Scope under .modal so this beats Milligram's input[type=…]/select
         height (specificity 0,1,1) — a bare .value-input (0,1,0) loses to it. */
      .modal .value-input { height: 2.4rem; margin: 0; padding: 0 .6rem; font-size: 1.2rem; }
      select.vi-select { background-image: none; line-height: 1; }
      /* Generate values… modal */
      .modal-generate { min-width: 460px; max-width: 92vw; width: 640px; }
      .gen-body { padding: 1.25rem 1.5rem; display: flex; flex-direction: column; gap: 1rem; max-height: 70vh; overflow: auto; }
      .gen-section { display: flex; flex-direction: column; gap: .6rem; }
      /* Calendar / Iterator tab switcher for date + datetime generators. */
      .gen-tabs { display: flex; gap: .25rem; border-bottom: 1px solid var(--brown-border); }
      button.gen-tab { margin: 0; padding: .35rem .9rem; height: auto; min-height: 0; background: transparent; border: 0; border-bottom: 2px solid transparent; color: var(--brown-light); font-size: 1.2rem; font-weight: 600; letter-spacing: 0; text-transform: none; cursor: pointer; line-height: 1.4; border-radius: 0; }
      button.gen-tab:hover { color: var(--brown-darker); }
      button.gen-tab.active { color: var(--brown-darker); border-bottom-color: #3b6e6a; }
      .gen-tab-panel { display: flex; flex-direction: column; gap: .6rem; }
      .gen-row { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
      .gen-lab { font-weight: 600; color: var(--brown-darker); margin: 0; white-space: nowrap; }
      .gen-lab.gen-req { font-family: var(--mono); font-weight: 500; flex: 0 0 8rem; }
      /* Constant inputs sit beside their 8rem label and fill the rest (flex
         basis 0 so Milligram's input width:100% doesn't force a wrap). */
      .gen-consts .gen-row { flex-wrap: nowrap; }
      .gen-consts .value-input { flex: 1 1 0; width: auto; min-width: 0; }
      /* Size via flex-basis, not width — Milligram's input width:100% rule (a
         high-specificity :not() chain) beats a plain .class width, but doesn't
         touch flex. */
      .gen-num, .gen-iter, .gen-time { flex: 0 0 6rem; width: auto; }
      /* Datetime step-unit dropdown (days/months/years/hours/minutes/seconds) —
         a touch wider than the numeric step box so the longer words fit. */
      .gen-unit { flex: 0 0 9rem; width: auto; }
      /* Date field fills the rest of the modal row (as it did before the echo
         was added); the readable echo overlays the right edge inside it. */
      .gen-date-row { flex-wrap: nowrap; }
      /* Uniform leading-label width so every row's first input starts at the
         same x (matches the .gen-req constant rows' 8rem). */
      .gen-section .gen-row > .gen-lab:first-child { flex: 0 0 8rem; }
      .gen-date-wrap { position: relative; display: flex; flex: 1 1 auto; min-width: 0; }
      .gen-date { width: 100%; }
      /* Faint, read-only human-readable echo overlaid INSIDE the input, right. */
      .gen-date-readable { position: absolute; right: 1rem; top: 50%; transform: translateY(-50%); color: var(--ink-mute); font-style: italic; font-size: 1.05rem; white-space: nowrap; pointer-events: none; }
      .gen-consts { border-top: 1px solid var(--brown-border); padding-top: .75rem; display: flex; flex-direction: column; gap: .5rem; }
      .gen-consts-note, .gen-cal-hint { font-size: 1rem; margin: 0; }
      .gen-summary { border-top: 1px solid var(--brown-border); padding-top: .75rem; }
      /* Inline error under the N-values summary on a failed generate. The
         SUGGESTION bar (.grid-suggest) renders above the scrollable message. */
      .gen-error-host:empty { display: none; }
      .gen-error-host .grid-suggest { border: 0; border-radius: 3px; margin-top: .5rem; }
      .gen-error { max-height: 8rem; overflow-y: auto; white-space: pre-wrap; margin-top: .5rem; }
      .gen-count { font-weight: 700; color: var(--brown-darker); }
      .gen-preview { font-family: var(--mono); font-size: 1rem; margin-top: .25rem; word-break: break-word; }
      /* Paint-the-calendar */
      .gen-cal-months { display: flex; flex-wrap: wrap; gap: 1rem; }
      .gen-cal-month { border: 1px solid var(--brown-border); border-radius: 4px; padding: .5rem; }
      .gen-cal-title { font-weight: 700; color: var(--brown-darker); text-align: center; margin-bottom: .35rem; font-size: 1.05rem; }
      .gen-cal-grid { display: grid; grid-template-columns: repeat(7, 1.9rem); gap: 2px; }
      button.gen-cal-wd { padding: 0; height: 1.6rem; min-height: 0; margin: 0; background: transparent; border: 0; color: var(--brown-light); font-size: .9rem; font-weight: 700; letter-spacing: 0; text-transform: none; cursor: pointer; line-height: 1; border-radius: 3px; }
      button.gen-cal-wd:hover { background: var(--cream-2); color: var(--brown-darker); }
      .gen-cal-day { height: 1.9rem; display: inline-flex; align-items: center; justify-content: center; font-size: 1rem; border-radius: 3px; cursor: pointer; user-select: none; color: var(--text); }
      .gen-cal-day:hover { background: var(--cream-2); }
      .gen-cal-day.empty, .gen-cal-day.out { visibility: hidden; pointer-events: none; }
      .gen-cal-day.sel { background: var(--brown); color: var(--cream); }
      .gen-cal-day.sel:hover { background: var(--brown-darker); }

      /* Table card collapse (mirror of validator-card collapse) */
      .table-header { display: flex; justify-content: space-between; align-items: center; gap: .5rem; }
      .table-header-name {
        flex: 1; display: flex; align-items: center; gap: .6rem; cursor: pointer;
        padding: .2rem .2rem; border-radius: 4px; min-width: 0;
      }
      .table-header-name:hover { background: rgba(87,83,78,.08); }
      .table-header-name.renaming { cursor: default; padding: 0; }
      .table-header-name.renaming:hover { background: transparent; }
      .rename-input { font-family: var(--mono); font-size: 1.6rem; max-width: 24rem; margin: 0; }
      .table-header-name .chevron { color: var(--brown); width: 1rem; font-size: 1.4rem; line-height: 1; }
      .table-body { margin-top: 1rem; }
      .table-body.collapsed { display: none; }

      /* SVG icons crisper than unicode glyphs */
      .icon { display: inline-block; vertical-align: middle; }
      .icon-x { width: 1.4rem; height: 1.4rem; stroke: currentColor; stroke-width: 2; fill: none; stroke-linecap: round; }
      .cog .icon-cog { width: 1.8rem; height: 1.8rem; fill: currentColor; }

      /* Validator card collapse */
      .validator-header { display: flex; justify-content: space-between; align-items: center; gap: .5rem; }
      .validator-header-name {
        flex: 1; display: flex; align-items: center; gap: .6rem; cursor: pointer;
        padding: .3rem .2rem; border-radius: 4px; min-width: 0;
      }
      .validator-header-name:hover { background: rgba(87,83,78,.08); }
      .validator-header-name .chevron { color: var(--brown); width: 1rem; font-size: 1.4rem; line-height: 1; }
      .validator-name { font-size: 1.6rem; color: var(--brown-darker); }
      .validator-preview {
        font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
        font-size: 1.2rem; color: #6b5a4a;
        white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; flex: 1;
        background: transparent; padding: 0;
      }
      .validator-body { margin-top: .75rem; }
      .validator-body.collapsed { display: none; }
      .validator-status { margin-left: .75rem; }

      /* Code editor with syntax-highlighted overlay.
         Ligatures DISABLED so the textarea text and the per-token <span> text
         in the overlay always render at identical glyph widths. */
      .code-editor {
        position: relative; min-height: 6rem;
        font-family: var(--mono);
        font-size: 1.3rem; line-height: 1.5;
        font-variant-ligatures: none;
        -webkit-font-feature-settings: "liga" 0, "calt" 0;
        font-feature-settings: "liga" 0, "calt" 0;
      }
      .code-editor .code-overlay,
      .code-editor .code-input {
        margin: 0; padding: .8rem;
        font: inherit;
        font-variant-ligatures: none;
        font-feature-settings: "liga" 0, "calt" 0;
        white-space: pre-wrap; word-break: normal; overflow-wrap: anywhere;
        tab-size: 2; -moz-tab-size: 2;
        border: 1px solid var(--brown-border); border-radius: 4px;
        box-sizing: border-box;
        letter-spacing: 0;
      }
      .code-editor .code-overlay {
        position: absolute; inset: 0;
        background: var(--surface); color: var(--text);
        pointer-events: none; overflow: hidden;
      }
      .code-editor .code-overlay code {
        font: inherit; background: transparent; padding: 0; color: inherit;
        font-variant-ligatures: inherit; font-feature-settings: inherit;
        white-space: inherit; word-break: inherit; overflow-wrap: inherit;
        display: block;
      }
      .code-editor .code-overlay code span { font: inherit; }
      .code-editor .code-input {
        position: relative; z-index: 1; width: 100%; min-height: 6rem;
        background: transparent; color: transparent; caret-color: var(--text);
        border-color: transparent;
        resize: vertical; outline: none;
      }
      .code-editor .code-input:focus { border-color: var(--brown); }
      .code-editor .code-input::selection { background: rgba(87,83,78,.25); color: transparent; }

      /* Syntax tokens */
      .tok-keyword { color: #8b3a62; font-weight: 600; }
      .tok-type    { color: #2f6f8f; }
      .tok-string  { color: #5a7a3d; }
      .tok-number  { color: #b8742b; }
      .tok-comment { color: var(--ink-mute); font-style: italic; }
      .tok-ident   { color: var(--text); }
      .tok-punct, .tok-op { color: #6b5a4a; }
      .tok-other   { color: var(--text); }

      /* grid2 aggregate rows. Tagged `data-aggregate="1"` (per-group)
         or `"grand"` (the bottom roll-up), plus `data-agg-fn` carrying
         the function identifier (sum / avg / count / count_distinct /
         min / max). Each function gets a very subtle background tint
         (~6% saturation) so a multi-function grid still reads as one
         aggregate stripe but the eye can tell sum-rows from avg-rows
         at a glance. Tabular-nums keeps the right-aligned digits tidy. */
      /* Specificity bump: `html` element + `td[data-readonly]` attr
         bring these rules to (0,3,4) — ties the dark-mode override
         `html[data-theme="dark"] .grid-table tbody tr td[data-readonly="1"]`
         that would otherwise paint #25211e on top of the per-function
         tint. Source order makes the later rule (this one) win. The
         first attempt only matched `td[data-readonly]` (B=3, C=3) which
         still lost to dark readonly (B=3, C=4) — adding `html`
         restores parity on C. Aggregate cells are always rendered
         with data-readonly="1" (see buildClusterRows). */
      html .grid-table tbody tr[data-aggregate] td[data-readonly] {
        background: var(--agg-tint, #f4ecdc);
        font-variant-numeric: tabular-nums;
      }
      html .grid-table tbody tr[data-aggregate="grand"] td[data-readonly] {
        background: var(--agg-tint-grand, #ede1c5);
      }
      /* Per-function tints — light mode. Each is a barely-saturated
         pastel so the row stripe doesn't compete with the detail rows.
         The grand-total variant is the same hue, one step darker. */
      .grid-table tbody tr[data-agg-fn="sum"] td            { --agg-tint: #e8eff7; --agg-tint-grand: #d6e1ee; }
      .grid-table tbody tr[data-agg-fn="avg"] td            { --agg-tint: #e6f0e8; --agg-tint-grand: #d4e2d8; }
      .grid-table tbody tr[data-agg-fn="count"] td          { --agg-tint: #efeaf3; --agg-tint-grand: #ddd5e4; }
      .grid-table tbody tr[data-agg-fn="count_distinct"] td { --agg-tint: #e4f0f1; --agg-tint-grand: #cde0e3; }
      .grid-table tbody tr[data-agg-fn="min"] td            { --agg-tint: #f3ece1; --agg-tint-grand: #e6d9c4; }
      .grid-table tbody tr[data-agg-fn="max"] td            { --agg-tint: #f5e7e7; --agg-tint-grand: #ead0d0; }
      /* Dark mode: lower-lightness equivalents. The same hue family
         each function carries in light mode, but dropped to a tint
         that sits one or two values brighter than the cell bg
         (#292524) so the function ID is still distinguishable. */
      html[data-theme="dark"] .grid-table tbody tr[data-agg-fn="sum"] td            { --agg-tint: #232a33; --agg-tint-grand: #2c3845; }
      html[data-theme="dark"] .grid-table tbody tr[data-agg-fn="avg"] td            { --agg-tint: #232a26; --agg-tint-grand: #2c3830; }
      html[data-theme="dark"] .grid-table tbody tr[data-agg-fn="count"] td          { --agg-tint: #28252e; --agg-tint-grand: #34303e; }
      html[data-theme="dark"] .grid-table tbody tr[data-agg-fn="count_distinct"] td { --agg-tint: #222a2c; --agg-tint-grand: #2b383a; }
      html[data-theme="dark"] .grid-table tbody tr[data-agg-fn="min"] td            { --agg-tint: #2d2823; --agg-tint-grand: #3a322a; }
      html[data-theme="dark"] .grid-table tbody tr[data-agg-fn="max"] td            { --agg-tint: #2e2424; --agg-tint-grand: #3c2e2e; }
      @media (prefers-color-scheme: dark) {
        html:not([data-theme="light"]) .grid-table tbody tr[data-agg-fn="sum"] td            { --agg-tint: #232a33; --agg-tint-grand: #2c3845; }
        html:not([data-theme="light"]) .grid-table tbody tr[data-agg-fn="avg"] td            { --agg-tint: #232a26; --agg-tint-grand: #2c3830; }
        html:not([data-theme="light"]) .grid-table tbody tr[data-agg-fn="count"] td          { --agg-tint: #28252e; --agg-tint-grand: #34303e; }
        html:not([data-theme="light"]) .grid-table tbody tr[data-agg-fn="count_distinct"] td { --agg-tint: #222a2c; --agg-tint-grand: #2b383a; }
        html:not([data-theme="light"]) .grid-table tbody tr[data-agg-fn="min"] td            { --agg-tint: #2d2823; --agg-tint-grand: #3a322a; }
        html:not([data-theme="light"]) .grid-table tbody tr[data-agg-fn="max"] td            { --agg-tint: #2e2424; --agg-tint-grand: #3c2e2e; }
      }
      .grid-cell-agg-label {
        font-family: var(--mono);
        font-weight: 700;
        color: var(--brown-darker);
      }
      /* Layout inside the synthetic `__agg__N` cell on aggregate rows:
         label flush-right (so it sits next to the numbers it labels),
         caret (when present) flush-left in the same cell. Flexbox +
         `margin-right: auto` on the caret does both:
           - label-only inner → `justify-content: flex-end` pushes it right
           - caret + label    → caret takes the left edge via auto-margin
         Scoped to inners that actually contain the label so other
         aggregate-row cells (group-value, aggregated numbers) keep
         their normal alignment. Detail-row `__agg__N` cells with just
         a ▼ caret don't match this rule and inherit the default
         left-align — exactly what we want. */
      .grid-table tbody td .cell-inner.cell-inner-agg {
        display: flex; align-items: center; justify-content: flex-end;
      }
      .grid-table tbody td .cell-inner.cell-inner-agg .agg-group-toggle {
        margin-right: auto;
      }
      /* Collapse / expand caret rendered in the synthetic __agg__N
         columns. ▼ sits in the first detail row of an expanded group;
         ▶ sits on the first aggregate row of a collapsed group, before
         the function label. Both tokens flip via --brown in dark mode. */
      /* Whole-cell hit target for group-collapse toggles: clicking
         anywhere in a synthetic `__agg__N` cell that carries the
         caret folds or expands the group. The cursor cues that. */
      .grid-table tbody td.cell-clickable { cursor: pointer; }
      /* Inline caret button: ▼ / ▶ glyphs render tall by default
         (Milligram's bare `button` rule also injects a default 3.8rem
         min-height and line-height: 1.5 which would balloon the row).
         Pin everything explicitly — 0 vertical padding, line-height
         matching font-size, height auto — so the button collapses
         to the glyph's own metric and never pushes its `<tr>` taller
         than a regular text cell. */
      button.agg-group-toggle {
        appearance: none; background: transparent; border: 0;
        padding: 0; margin: 0 .25rem 0 0; cursor: pointer;
        font-family: var(--mono); font-size: 1rem; line-height: 1;
        height: 1.2rem; min-height: 0;
        vertical-align: middle;
        color: var(--brown);
        display: inline-flex; align-items: center; justify-content: center;
      }
      button.agg-group-toggle:hover,
      button.agg-group-toggle:focus-visible {
        color: var(--brown-darker);
        outline: none;
      }
      .grid-cell-agg-group {
        font-weight: 700;
        color: var(--brown-light);
      }
      .cell-agg-null {
        color: var(--brown-light);
      }

      /* Related-column cell (lazy-loaded count + pills) */
      .grid-cell-related .cell-inner { cursor: pointer; }
      /* Related-cell hover paints the inner pill-container, not the td.
         That means my mode-gated `td:hover` rule doesn't reach it — the
         inner element needs its own gating + dark override so a hovered
         related cell doesn't flash light peach on a dark surface, and
         doesn't linger when the keyboard takes over. */
      body:not(.grid-kb-nav) .grid-cell-related:hover .cell-inner { background-color: #f4ecdc; }
      html[data-theme="dark"] body:not(.grid-kb-nav) .grid-cell-related:hover .cell-inner { background-color: #4a3f33; }
      @media (prefers-color-scheme: dark) {
        html:not([data-theme="light"]) body:not(.grid-kb-nav) .grid-cell-related:hover .cell-inner { background-color: #4a3f33; }
      }
      .cell-related-loading { color: var(--brown-light, #a1a1aa); opacity: 0.6; }
      /* Reverse-video pill containing the subgrid's row count. Reads as
         "there are N rows behind this cell" without competing with the
         pill labels next to it. */
      .cell-related-count   {
        display: inline-block; padding: 0 .45rem; margin-right: .35rem;
        background: var(--text); color: var(--cream); border-radius: 10px;
        font-weight: 600; font-size: 0.85em; line-height: 1.4;
      }
      .cell-related-pill    { display: inline-block; padding: 0 .35rem; margin-right: .25rem;
                              border: 1px solid var(--brown-light, #a1a1aa); border-radius: 10px;
                              font-size: 0.85em; }
      .cell-related-more    { color: var(--brown-light, #a1a1aa); }

      /* Related-overlay modal (item 4 onwards) — hosts a real mountGrid
         instance with chrome="minimal", so the modal body just gives it
         space and lets the grid render natively. */
      .modal-related { width: 90vw; max-width: 1200px; }
      .related-overlay-body { padding: 0; }
      .related-overlay-grid-host { display: flex; flex-direction: column;
                                   height: 65vh; overflow: hidden; }
      .related-overlay-grid-host .grid-wrap { flex: 1 1 auto; min-height: 0; }
      .grid-title-bar-minimal { gap: .5rem; }

      /* Per-card hamburger menu — collapses RENAME / JSON SCHEMA / SQL /
         DIAGRAM / GRID / DELETE behind a single ☰ button. Anchored to the
         card header's right edge via position:relative on .actions. */
      .actions { position: relative; }
      .card-menu-toggle { font-size: 1.4rem; line-height: 1; padding: 0 .8rem; min-width: 3rem; height: 3rem; }
      .card-menu {
        position: absolute; right: 0; top: calc(100% + .25rem);
        min-width: 160px; z-index: 30;
        background: var(--surface); border: 1px solid var(--brown-border); border-radius: 4px;
        box-shadow: 0 6px 18px rgba(0,0,0,.12);
        padding: .25rem 0; display: flex; flex-direction: column;
        animation: modal-in 100ms cubic-bezier(.2,.7,.2,1);
      }
      .card-menu-item {
        background: transparent; border: 0; color: var(--text);
        text-align: left; padding: .5rem .9rem; font-size: 1.1rem; cursor: pointer;
        font-family: var(--mono); margin: 0; height: auto; line-height: 1.4;
      }
      .card-menu-item:hover { background: var(--cream-2); color: var(--brown-darker); }
      .card-menu-danger { color: var(--danger); }
      .card-menu-danger:hover { background: var(--danger-hover-bg); color: var(--danger-strong); }

      /* Empty-grid affordance — visible only when the underlying table has
         no rows AND the user hasn't filtered them out. Points users at the
         `+ row` button so first-time use isn't a hunt. */
      .grid-empty-hint {
        padding: .85rem 1rem; margin: .25rem 0;
        background: var(--cream-2); border: 1px dashed var(--brown-border);
        border-radius: 4px; font-size: 1.15rem;
      }
      .grid-empty-hint kbd {
        font-family: var(--mono); font-size: 0.95em;
        background: var(--surface); border: 1px solid var(--brown-border); border-bottom-width: 2px;
        border-radius: 3px; padding: 0 .35rem;
      }

      /* Header-side FK affordance — small key icon prefixing the column
         label. Clicking it jumps to the linked table's default grid in
         a named tab (same routing as the per-cell .cell-fk-link). The
         sort-cycle click handler skips it so users can pick the icon
         without changing the sort order.
         The button is inline-flex with vertical-align: middle so the
         18-px icon centres on the label's text baseline instead of
         pushing the row taller than its siblings; padding-block: 0
         keeps the th content from growing vertically. */
      .th-fk-key {
        background: transparent; border: 0;
        padding: 0; padding-right: .2rem;
        margin: 0 .25rem 0 0;
        vertical-align: middle;
        color: var(--brown-light); cursor: pointer;
        line-height: 0;
        display: inline-flex; align-items: center; justify-content: center;
      }
      .th-fk-key:hover { color: var(--brown-darker); }
      .th-fk-key svg { display: block; vertical-align: middle; }

      /* Foreign-key peek (eyeball) — sits between the value and the
         link-arrow. Hover only; click is a no-op (the arrow handles
         navigation). Same muted → accent hover treatment as the arrow. */
      .cell-fk-peek {
        background: transparent; border: 0; padding: 0 .15rem;
        margin-left: .35rem; vertical-align: baseline;
        color: var(--brown-light); cursor: help; height: auto;
        display: inline-flex; align-items: center;
      }
      .cell-fk-peek:hover { color: var(--brown-darker); }
      .cell-fk-peek svg { display: block; }
      .fk-peek-tooltip {
        position: fixed; z-index: 200;
        background: var(--surface); color: var(--text);
        border: 1px solid var(--brown-border); border-radius: 4px;
        padding: .5rem .75rem; min-width: 220px; max-width: 360px;
        box-shadow: 0 6px 18px rgba(0,0,0,.12);
        font-size: 1rem; line-height: 1.5; pointer-events: none;
        /* Never exceed the viewport: width is capped on phones (360px would
           overflow), and a long field list is height-capped so positioning
           (placeFloating) can keep the whole tooltip on-screen. */
        max-width: min(360px, calc(100vw - 12px));
        max-height: calc(100vh - 12px); overflow-y: auto;
      }
      .fk-peek-head { margin-bottom: .35rem; padding-bottom: .25rem;
                      border-bottom: 1px solid var(--brown-border);
                      display: flex; align-items: baseline; gap: .75rem; }
      /* Table name takes the left; the meta cluster (id / nid) is
         pushed to the right edge of the head. */
      .fk-peek-head > strong { flex: 0 1 auto; min-width: 0;
                               overflow: hidden; text-overflow: ellipsis; }
      .fk-peek-meta-cluster { margin-left: auto; display: flex;
                              gap: .5rem; align-items: baseline;
                              flex: 0 0 auto; }
      /* id / nid identity labels — muted so the table name stays the
         dominant glyph. */
      .fk-peek-meta { color: var(--brown-light); font-size: .9em;
                      white-space: nowrap; }
      /* Two-column grid so the longest key (e.g. "display_name") sets
         the key-column width and every row's value aligns to the same
         left edge. */
      .fk-peek-body { display: grid; grid-template-columns: max-content 1fr;
                      column-gap: .75rem; row-gap: .15rem; }
      .fk-peek-k { color: var(--brown-light); white-space: nowrap; }
      .fk-peek-v { color: var(--text); word-break: break-word; }

      /* Foreign-key cell affordance — a small arrow icon next to the
         value, pointing bottom-left → top-right ("go to linked row").
         Sits to the right of the value with .35rem gap; muted by
         default, picks up the accent on hover. */
      .cell-fk-link {
        background: transparent; border: 0; padding: 0 .15rem;
        margin-left: .35rem; vertical-align: baseline;
        color: var(--brown-light); cursor: pointer; height: auto;
        display: inline-flex; align-items: center;
      }
      .cell-fk-link:hover { color: var(--brown-darker); }
      .cell-fk-link svg { display: block; }

      /* File-cell action row. The cell shows up to five icon buttons:
         download (primary, larger) + copy + camera + mic + upload,
         followed by the filename when populated. Layout is inline-flex so a
         narrow cell tucks the label out via text-overflow while the
         icons stay legible. Each icon button is transparent so the
         row reads as a control cluster, not a row of chips. */
      .cell-file-actions {
        /* Block-level flex (not inline-flex) so the icon row sits in
           its own block box rather than the cell's inline line-box —
           that line-box's leading was padding the file-cell row ~4px
           taller than a plain text row. As a block flex it matches the
           natural row height exactly. */
        display: flex; align-items: center; gap: .25rem;
        max-width: 100%;
      }
      button.cell-file-icon {
        background: transparent; border: 0; padding: 0 .2rem;
        margin: 0; line-height: 1; height: auto;
        color: var(--brown-dark); cursor: pointer;
        font-size: 1.3em; text-transform: none; letter-spacing: 0;
        flex-shrink: 0;
        /* Emoji icons render full-colour by default; desaturate them
           so the cell reads as a row of subtle controls instead of a
           rainbow. `saturate(0)` covers older Safari that ignores
           `grayscale(1)` on emoji glyphs. */
        filter: grayscale(1) saturate(0);
      }
      button.cell-file-icon:hover { color: var(--brown-darker); filter: grayscale(1) saturate(0) brightness(0.85); }
      /* Download/upload render as inline SVG "tau" arrows (rounded-top
         rectangular shaft + wide triangular head). Solid fills via
         currentColor so they inherit the cell text colour and the
         greyscale filter on the parent applies uniformly.
         The SVG is a replaced element with an explicit box, so an
         oversized one stretches the grid row past its natural text
         height. Sizing the arrow at ~the emoji glyph height (and
         resetting the button's inherited 1.3em so the em base is the
         cell font, not the larger icon font) keeps the file-cell row
         the same height as a plain text row. `vertical-align: middle`
         + the actions row's `line-height: 1` stop the SVG's box from
         adding leading. */
      button.cell-file-download,
      button.cell-file-upload {
        padding: 0 .25rem;
        font-size: 1em;
        display: inline-flex; align-items: center;
      }
      .cell-file-arrow-svg {
        width: 1.2em; height: 1.2em;
        display: inline-block; vertical-align: middle;
      }
      /* Download reuses the up-arrow SVG, rotated 180° → down arrow.
         Same geometry as the upload icon, just flipped. */
      .cell-file-arrow-down { transform: rotate(180deg); }
      .cell-file-actions[data-has-file="1"] .cell-file-camera,
      .cell-file-actions[data-has-file="1"] .cell-file-mic,
      .cell-file-actions[data-has-file="1"] .cell-file-upload,
      .cell-file-actions .cell-file-copy { opacity: .6; }
      .cell-file-actions[data-has-file="1"] .cell-file-camera:hover,
      .cell-file-actions[data-has-file="1"] .cell-file-mic:hover,
      .cell-file-actions[data-has-file="1"] .cell-file-upload:hover,
      .cell-file-actions .cell-file-copy:hover { opacity: 1; }
      .cell-file-name {
        overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
        color: var(--brown-dark); font-family: var(--mono);
        margin-left: .25rem;
      }
      .cell-file-empty { font-style: italic; opacity: .7; }
      /* While a file cell's upload is in flight its icons are replaced
         by a "progress: N%" readout + a cancel button (see
         showCellUploading). */
      .cell-file-uploading { display: inline-flex; align-items: center; gap: .5rem; }
      .cell-file-uploading-label { color: var(--brown-dark); font-family: var(--mono); white-space: nowrap; }
      /* Element-qualified so it beats Milligram's `.button` defaults
         (uppercasing, full height) — a compact lowercase pill (C8). */
      button.cell-file-cancel {
        height: auto; line-height: 1.2; padding: .1rem .5rem; margin: 0;
        font-size: .85em; text-transform: none; letter-spacing: 0;
      }

      /* Camera capture modal — live preview + retake/confirm review. */
      .modal-camera-capture { max-width: 720px; }
      .camera-body { display: flex; justify-content: center; padding: 0; background: #000; position: relative; }
      .camera-preview, .camera-review {
        display: block; width: 100%; max-height: 60vh;
        object-fit: contain; background: #000;
      }
      /* No mirror: the live preview must match the captured still and
         the saved file. A `scaleX(-1)` preview made the capture appear
         to "flip" the moment the un-mirrored still replaced the
         mirrored preview — a confusing glitch. drawImage / grabFrame
         always read the intrinsic (un-mirrored) frame, so keeping the
         preview un-mirrored too means what you see is what you get. */
      .camera-actions { gap: .5rem; }
      /* Front/rear flip — an overlay on the live preview, styled like
         the nomnoml legend checkbox labels (translucent surface,
         uppercase, .02em tracking, normal weight, brown-darker). Only
         shown on devices with 2+ cameras; hidden during still review.
         Element-qualified so it beats Milligram's bare `.button`. */
      button.camera-flip {
        position: absolute; bottom: .75rem; right: .9rem; z-index: 2;
        margin: 0; height: auto; line-height: 1.2; padding: .45rem .8rem;
        background: var(--legend-tint, rgba(255, 255, 255, .92));
        border: 1px solid var(--brown-border); border-radius: 4px;
        box-shadow: 0 1px 4px var(--legend-shadow, rgba(58, 40, 23, .1));
        font-family: inherit; font-size: .95rem; font-weight: 400;
        text-transform: uppercase; letter-spacing: .02em;
        color: var(--brown-darker); cursor: pointer;
      }
      button.camera-flip:hover { background: var(--surface, #fff); }
      /* Milligram styles `.button { display: inline-block }`, which
         overrides the HTML `hidden` attribute (equal specificity, but
         the class rule loads later). The retake / use-photo buttons
         and the review <img> rely on `hidden` to stay out of view
         until a frame is captured. Scope an attribute rule under the
         modal so it out-specifies `.button` (0,2,0 vs 0,1,0) without
         needing !important (convention C6). */
      .modal-camera-capture [hidden] { display: none; }

      /* Audio capture modal — record → review (<audio controls>) →
         re-record/confirm. Mirrors the camera modal's review shape but
         needs no preview surface, so the body is a centred column with
         a status readout above the player. */
      .modal-audio-capture { max-width: 460px; }
      .audio-body {
        display: flex; flex-direction: column; align-items: center;
        gap: 1rem; padding: 1.5rem 1rem;
      }
      .audio-status {
        font-family: var(--mono); color: var(--brown-dark);
        font-size: 1.1rem; letter-spacing: .02em;
      }
      .audio-review { width: 100%; }
      .audio-actions { gap: .5rem; }
      /* Same `hidden`-vs-Milligram-`.button` specificity fix as the
         camera modal (convention C6) — the stop/re-record/confirm
         buttons and the <audio> review rely on `hidden`. */
      .modal-audio-capture [hidden] { display: none; }

      /* Inline upload error for the capture modals. Shown — with the
         confirm button relabelled "Retry upload" — when committing the
         captured photo / clip to the server fails, so a flaky network
         no longer discards the capture by closing the modal. The
         per-modal `[hidden]` rules above hide it until there's an error. */
      .capture-error {
        margin: 0 1rem .75rem; padding: .5rem .75rem;
        border: 1px solid var(--danger); border-radius: 4px;
        background: var(--danger-bg); color: var(--danger-ink);
        font-size: .9rem; text-align: center;
      }

      /* Clickable filename label for previewable file cells. Element-
         qualified so it beats Milligram's bare `button` defaults
         (uppercase, full 3.8rem height, letter-spacing) — convention
         C8. Reads as the plain `.cell-file-name` label, but underlines
         on hover/focus to signal it opens the in-app preview. */
      button.cell-file-name-preview {
        display: inline; height: auto; line-height: inherit;
        margin: 0 0 0 .25rem; padding: 0; border: 0; background: none;
        font-family: var(--mono); font-size: inherit; font-weight: 400;
        text-transform: none; letter-spacing: 0; color: var(--brown-dark);
        cursor: pointer; max-width: 100%;
        overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
        vertical-align: bottom;
      }
      button.cell-file-name-preview:hover,
      button.cell-file-name-preview:focus-visible { text-decoration: underline; }

      /* In-app file preview modal — image overlay / audio + video
         player streamed from R2. Reuses `.modal-back / .modal` chrome
         (C3); `.modal-back` already flex-centers the dialog. The modal
         shrink-wraps the media (min-width:0, no fixed max) and the
         media is capped at 80% of each viewport dimension, so a large
         image scales DOWN proportionally and stays centred — rather
         than overflowing the modal and clipping to the top-left. The
         modal's own box is allowed to grow to 90vh so the 80vh media +
         header doesn't get clipped by the base `.modal max-height:80vh`. */
      .modal-file-preview { min-width: 0; max-width: 88vw; max-height: 90vh; }
      .file-preview-body {
        display: flex; justify-content: center; align-items: center;
        padding: 0; background: #000;
      }
      .file-preview-image, .file-preview-video {
        display: block; width: auto; height: auto;
        max-width: 80vw; max-height: 80vh;
        object-fit: contain; background: #000;
      }
      /* Audio has no visual frame — drop the black media surface so the
         controls sit on the normal modal background. */
      /* Horizontal breathing room comes from body padding, not the
         player's margin — `width:100%` + side margins overflowed the
         shrink-wrapped modal and showed a scrollbar. */
      .file-preview-back-audio .file-preview-body { background: transparent; padding: 0 1rem; }
      .file-preview-audio { width: 100%; margin: 1.5rem 0; min-width: 16rem; }
      /* Header action cluster (copy + close) — keeps the buttons grouped
         at the right of the modal header (which is justify-content:
         space-between, so the title stays left). */
      .file-preview-actions { display: flex; gap: .5rem; align-items: center; }

      /* Publish modal — "Publication of <grid>". Column list with
         tri-state diamonds, projection picker, writes switch, URL list. */
      /* Pin both ends so the publish modal stays a stable 760px even
         when the title bar is short (no toggle, no badge) — without
         this the box collapses toward .modal's 480px floor. */
      .modal-publish { width: 760px; max-width: 90vw; }
      /* Stacked toggle in the title bar: switch on top, "READ-ONLY" label
         underneath. Keeps the modal header compact while replacing the
         body-level Writes-enabled checkbox. */
      .publish-ro-toggle {
        display: inline-flex; flex-direction: column; align-items: center;
        gap: .15rem; cursor: pointer; user-select: none;
        margin-left: .75rem; vertical-align: middle;
      }
      .publish-ro-toggle input { margin: 0; }
      .publish-ro-label { font-size: .75rem; color: var(--brown-darker); letter-spacing: .03em; }
      /* ACCESS-section rows ("publish a column-restricted grid per…" and
         "Allow visitors to add rows"): between the body scale (1.6rem
         column-name text) and the small in-line glyph size. */
      .publish-projected-row,
      .publish-inserts-label,
      .publish-deletes-label,
      .publish-row-access,
      .publish-stylesheet-row,
      .publish-success-row,
      .publish-form-title-row,
      .publish-form-intro-row { font-size: 1.3rem; }
      /* URLs sub-modal: wider than the publish modal so long URLs stay
         on one line; reuses `.modal-back`/`.modal` skeleton so z-index
         stacks naturally above the publish modal. */
      .modal-publish-urls { max-width: 900px; }
      .publish-urls-body { font-size: 1.3rem; }
      /* Single readonly textarea holding the URL list — tab-separated
         so it pastes cleanly into a spreadsheet. Picks up monospace +
         theme-aware surface so it stays legible in both light and dark. */
      .publish-urls-textarea {
        width: 100%; box-sizing: border-box;
        font-family: var(--mono); font-size: 1.1rem;
        padding: .55rem .7rem; margin: 0;
        background: var(--surface); color: var(--text);
        border: 1px solid var(--brown-border); border-radius: 4px;
        resize: vertical; min-height: 12rem;
        white-space: pre; overflow: auto;
      }
      .publish-urls-textarea:focus { outline: 2px solid var(--brown); outline-offset: 1px; }
      .publish-urls-hint {
        font-size: .9rem; color: var(--ink-mute); margin-top: .35rem;
        font-style: italic;
      }
      .publish-urls-hint.err { color: var(--danger); font-style: normal; }
      /* "expires in N h" badge inside the submissions list. Muted so
         it reads as a secondary annotation next to the URL + revoke. */
      .publish-submission-expiry {
        font-size: .9rem; color: var(--ink-mute);
        font-family: var(--mono); white-space: nowrap;
      }
      /* Projection-column chip in the sub-modal title — uses the same
         reverse-video chip palette as the rest of the app. Both --chip-bg
         and --chip-ink flip with the theme so it stays legible. */
      .modal-publish-urls .modal-title code {
        background: var(--chip-bg); color: var(--chip-ink);
        padding: .05rem .35rem; border-radius: 3px;
        font-family: var(--mono); font-size: .9em;
      }
      /* URLs sub-modal slice blocks: one block per projection value,
         each listing GRID / FORM / CSV / TSV in a tagged row. */
      .publish-slice-block { margin-bottom: .9rem; }
      .publish-slice-block:last-child { margin-bottom: 0; }
      .publish-slice-head { font-weight: 700; margin-bottom: .25rem; color: var(--brown-darker); }
      .publish-url-tag {
        display: inline-block; min-width: 3.5rem; padding: .1rem .35rem;
        font-size: .85rem; font-weight: 700; letter-spacing: .04em;
        background: var(--brown); color: var(--cream); border-radius: 3px;
        text-align: center;
      }
      /* Public form viewer (`/~/<token>/form`). Single vertical form
         in a centered card. Title bar across the top with SAVE +
         CANCEL on the right; label-over-input rows in the body. */
      .public-form-page { padding: 0; }
      .pf-shell { max-width: 56rem; margin: 0 auto; padding: 1.5rem; }
      /* Owner "Edit row in form" modal: the reused form renders into a
         scrolling modal body. Neutralise the full-page shell centring so it
         fills the dialog. */
      .modal-owner-form { width: min(56rem, 92vw); }
      .modal-owner-form .of-body { overflow-y: auto; }
      /* An FK typeahead dropdown is absolutely positioned at the field's
         bottom edge (`.fk-ta-list`, up to 240px tall). Without room below the
         last field, the scrollable body clips it (e.g. a short edit form whose
         only/last field is the FK — the dropdown gets cut off at the modal's
         bottom). Reserve that room ONLY when the form actually contains a
         typeahead, so plain forms don't grow a band of empty space. */
      .modal-owner-form .of-body:has(.fk-ta) { padding-bottom: 16rem; }
      .modal-owner-form .pf-shell { max-width: none; margin: 0; padding: 1.25rem 1.5rem; }
      /* Currency prefix adornment on a decimal field's label. */
      .pf-field-prefix { color: var(--ink-mute); font-weight: 400; font-size: .9em; }
      .pf-head {
        display: flex; align-items: center; justify-content: space-between;
        gap: 1rem; margin: 0 0 2rem; padding: 0 0 .75rem;
        border-bottom: 1px solid var(--brown-border);
      }
      .pf-headline { font-size: 2rem; margin: 0; color: var(--brown-darker); font-weight: 700; }
      .pf-nid {
        display: inline-block; padding: .1rem .5rem; margin-left: .25rem;
        font-family: var(--mono); font-size: 1.4rem; font-weight: 400;
        background: var(--chip-bg); color: var(--chip-ink); border-radius: 4px;
      }
      .pf-head-actions { display: flex; gap: .5rem; }
      /* "← Back to menu" link on a public grid/form opened from a published
         menu (frontend/lib/menu-back-link.js). Rounded pill matching the
         public-viewer chrome (cf. the theme toggle). In the grid it is the
         first item in the title bar; in the form it is the first item in
         .pf-head. */
      .pub-menu-back {
        display: inline-flex; align-items: center; gap: .35rem; flex: 0 0 auto;
        font-family: var(--sans); font-size: 1.2rem; font-weight: 700;
        color: var(--brown-darker); text-decoration: none;
        height: 3rem; padding: 0 .9rem; border-radius: 1.5rem;
        border: 1px solid var(--brown-border); background: var(--cream);
        white-space: nowrap; max-width: 16rem;
        transition: background .2s ease;
      }
      .pub-menu-back:hover, .pub-menu-back:focus { background: var(--cream-2); color: var(--brown-darker); }
      .pub-menu-back .pub-menu-back-arrow { font-size: 1.4rem; line-height: 1; }
      .pub-menu-back .pub-menu-back-label { overflow: hidden; text-overflow: ellipsis; }
      .grid-title-bar > .pub-menu-back { margin-right: .25rem; }
      /* pf-head is justify-content: space-between (title left, actions right).
         With the back link added as a third child the title would float to the
         centre — pin the headline's right margin so the back-link + title stay
         grouped at the left and the actions stay at the right. */
      .pf-head:has(.pub-menu-back) .pf-headline { margin-right: auto; }
      /* ── Public menu viewer (/~/<token>/menu) ── */
      .pm-shell { max-width: 40rem; margin: 0 auto; padding: 1.5rem; }
      .pm-head { margin: 0 0 1.5rem; padding: 0 0 .75rem; border-bottom: 1px solid var(--brown-border); }
      .pm-headline { font-size: 2rem; margin: 0; color: var(--brown-darker); font-weight: 700; }
      .pm-list { display: flex; flex-direction: column; gap: .5rem; }
      .pm-item {
        display: block; padding: .75rem 1rem; font-size: 1.4rem;
        border: 1px solid var(--brown-border); border-radius: 4px;
        background: var(--surface); color: var(--text); text-decoration: none;
      }
      a.pm-item:hover { border-color: var(--brown); background: var(--cream-2); }
      .pm-item-disabled { color: var(--ink-mute); background: var(--cream-2); cursor: not-allowed; }
      .pm-item-submenu { font-weight: 700; }
      .pm-chevron { float: right; color: var(--ink-mute); font-weight: 700; }
      .pm-unavailable {
        float: right; font-size: 1rem; font-style: italic; color: var(--ink-mute);
      }
      .pm-empty { color: var(--ink-mute); }
      /* ── Settings → Menus editor ── */
      .menu-url-row { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
      .menu-url { flex: 1 1 auto; word-break: break-all; padding: .3rem .5rem; background: var(--cream-2); border-radius: 4px; }
      .menu-item-row {
        display: flex; align-items: center; gap: .5rem; padding: .4rem 0;
        border-bottom: 1px solid var(--brown-border);
      }
      .menu-item-missing { opacity: .7; }
      .menu-item-controls { display: flex; flex-direction: column; gap: .15rem; }
      .menu-item-controls .button { padding: 0 .5rem; line-height: 1.6; height: auto; }
      .menu-item-label { flex: 1 1 auto; min-width: 8rem; }
      .menu-item-target { flex: 0 1 auto; white-space: nowrap; }
      .menu-add-row { display: flex; gap: .5rem; margin-top: .75rem; flex-wrap: wrap; }
      .pf-form { display: grid; grid-template-columns: 14rem 1fr; gap: .75rem 2rem; align-items: center; }
      .pf-field { display: contents; }
      .pf-field-label {
        font-size: 1.2rem; font-weight: 700; color: var(--brown-darker);
        font-family: var(--sans); text-align: right;
      }
      .pf-required { color: var(--danger); margin-left: .2rem; }
      .pf-field-input {
        font: inherit; font-size: 1.4rem; padding: .4rem .6rem;
        border: 1px solid var(--brown-border); border-radius: 4px;
        background: var(--surface); color: var(--text); width: 100%;
      }
      .pf-field-input:focus { outline: 2px solid var(--brown); outline-offset: 1px; }
      /* Read-only / locked fields. A plain muted background read too close to
         an editable input — a visitor only learned a field was locked by
         trying to type. A dashed border + lock glyph on the label make the
         state legible at a glance. The `[readonly]` selector also catches the
         n1 sibling-display inputs (which use readOnly rather than disabled). */
      .pf-field-input[disabled],
      .pf-field-input[readonly] {
        background: var(--cream-2); color: var(--ink-mute); cursor: not-allowed;
        border-style: dashed;
      }
      .pf-lock { margin-left: .35rem; font-size: .8em; opacity: .7; }
      .pf-field-readonly .pf-field-label { color: var(--ink-mute); }
      /* Read-only related summary (n:m / subgrid): the linked rows the parent
         is related to, shown as non-editable pills so a member can see e.g.
         the classes they're enrolled in on their own edit link. */
      .pf-related-summary {
        display: flex; flex-wrap: wrap; gap: .4rem; align-items: center;
        padding: .3rem 0;
      }
      .pf-related-pill {
        font-size: 1.1rem; padding: .15rem .6rem;
        border: 1px solid var(--brown-border); border-radius: 999px;
        background: var(--cream-2); color: var(--brown-darker);
        font-family: var(--sans);
      }
      .pf-related-empty { color: var(--ink-mute); font-style: italic; font-size: 1.1rem; }
      .pf-related-more { color: var(--ink-mute); font-size: 1rem; }
      /* C7: Milligram paints a purple chevron on <select> via a
         background-image; combined with the native caret that's a double
         arrow. Suppress it and draw our own caret; line-height:1 keeps the
         value vertically centred in the short control. */
      select.pf-field-input {
        background-image: none; line-height: 1; appearance: none; -webkit-appearance: none;
        padding-right: 2rem;
        background-repeat: no-repeat; background-position: right .6rem center; background-size: .7rem;
        background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 6' fill='none' stroke='%238a7a66' stroke-width='1.5'><path d='M1 1l4 4 4-4'/></svg>");
      }
      select.pf-field-input[disabled] { background-image: none; padding-right: .6rem; }
      /* Boolean checkbox: native checkboxes render ~13px regardless of
         font-size and ignore a custom border colour, so we draw our own
         box (appearance: none) sized up to read as a peer control with the
         SAME border colour as the text inputs, filled on :checked. The
         wrapping cell carries the text-input row height (3.8rem, Milligram's
         input height) so the boolean row lines up with its neighbours. */
      .pf-checkbox-cell { display: flex; align-items: center; min-height: 3.8rem; }
      input[type="checkbox"].pf-field-input {
        appearance: none; -webkit-appearance: none;
        width: 1.8rem; height: 1.8rem; padding: 0; margin: 0; cursor: pointer;
        border: 1px solid var(--brown-border); border-radius: 4px;
        background: var(--surface);
        display: inline-grid; place-content: center;
      }
      input[type="checkbox"].pf-field-input::before {
        content: ""; width: 1rem; height: 1rem; border-radius: 2px;
        background: var(--brown); transform: scale(0); transition: transform .12s ease;
      }
      input[type="checkbox"].pf-field-input:checked::before { transform: scale(1); }
      /* A null boolean hides the checkbox and shows the literal "null" marker
         (clickable to set a value), matching how every other column type shows
         null. The checkbox reappears once it has a true/false value. */
      .pf-checkbox-cell input[type="checkbox"].pf-field-input:indeterminate { display: none; }
      .pf-bool-null { display: none; cursor: pointer; }
      .pf-checkbox-cell input[type="checkbox"].pf-field-input:indeterminate ~ .pf-bool-null { display: inline; }
      input[type="checkbox"].pf-field-input:focus { outline: 2px solid var(--brown); outline-offset: 1px; }
      input[type="checkbox"].pf-field-input[disabled] { background: var(--cream-2); cursor: not-allowed; }
      /* File field — action buttons FIRST, then the filename /
         "(no file)" readout, all on one row. Mirrors the grid's
         file-cell row order so the visual scan starts on actions. */
      .pf-field-file .pf-file-pane {
        display: flex; flex-direction: row; align-items: center;
        gap: .4rem; flex-wrap: wrap;
      }
      .pf-file-info {
        font-size: 1rem; color: var(--brown-darker);
        font-family: var(--mono); text-transform: lowercase;
      }
      .pf-file-empty { color: var(--brown-light); font-style: italic; }
      .pf-file-actions {
        display: flex; flex-wrap: wrap; gap: .35rem;
      }
      /* File-field controls on a public form render at 2× the grid's compact
         size — the camera / mic glyphs are emoji sized by font-size, so
         doubling font-size + padding doubles the icons' height and width (and
         scales the Download / Copy URL / Upload buttons to match). */
      .pf-file-actions .button {
        height: auto; padding: .4rem 1.1rem; margin: 0;
        font-size: 2.1rem; line-height: 1.2;
      }
      .pf-file-busy { opacity: .55; pointer-events: none; }
      /* Filename-as-preview link. Mirrors `.cell-file-name-preview` in
         the grid — looks like a plain link to invite the click.
         height/line-height are explicit (C8): a bare <button> inherits
         Milligram's 3.8rem height, which padded ~30px of dead space above
         the filename and below the action buttons. */
      button.pf-file-name-preview {
        background: none; border: 0; padding: 0; margin: 0;
        height: auto; line-height: 1.2;
        font: inherit; color: var(--brown-darker);
        text-decoration: underline; cursor: pointer;
        font-family: var(--mono); font-size: 1rem; text-transform: lowercase;
      }
      button.pf-file-name-preview:hover { color: var(--brown-dark); }
      /* Terminal "row deleted" message after a successful delete. */
      .pf-empty { color: var(--ink-mute); font-style: italic; padding: 2rem; text-align: center; }
      /* DELETE button is danger-styled to remind the visitor it's
         destructive — even when it's an "outline" button. */
      .pf-delete { color: var(--danger); border-color: var(--danger); }
      .pf-delete:hover { background: var(--danger); color: var(--cream); }
      /* Drop-zone highlight on the whole field while a file drag hovers
         it. Subtle dashed accent so visitors know "yes, you can drop". */
      .pf-field-file.pf-file-drop-active {
        outline: 2px dashed var(--brown);
        outline-offset: 2px;
        background: var(--cream-2);
        border-radius: 4px;
      }
      @media (max-width: 720px) {
        .pf-form { grid-template-columns: 1fr; }
        .pf-field-label { text-align: left; }
      }
      /* Publish modal tab strip. Sits just below the warning. Mirrors
         the schema-editor pane-switch buttons in spirit (uppercase, small
         caps, brown-darker active). */
      .publish-tabs {
        display: flex; align-items: center; gap: .25rem;
        border-bottom: 1px solid var(--brown);
        margin: 0 0 .25rem;
      }
      /* Spacer between the tab buttons and the right-floated URLs button. */
      .publish-tabs-spacer { flex: 1 1 auto; }
      /* URLs / SUBMISSIONS buttons hang off the right end of the tab row;
         slight negative margin-bottom so they sit inline with the tabs
         above the border. */
      .publish-tabs .publish-urls-btn,
      .publish-tabs .publish-submissions-btn {
        margin: 0 0 -1px; padding: .25rem .75rem;
        font-size: 1rem; letter-spacing: .03em; font-weight: 700;
        height: auto; line-height: 1.4;
      }
      .publish-tab {
        background: none; border: 0; padding: .35rem .75rem;
        font-family: inherit; font-size: 1rem; font-weight: 700;
        letter-spacing: .04em; color: var(--brown);
        cursor: pointer; border-bottom: 2px solid transparent;
        margin-bottom: -1px;
      }
      .publish-tab:hover { color: var(--brown-darker); }
      .publish-tab-active {
        color: var(--brown-darker); border-bottom-color: var(--brown-darker);
      }
      /* Drop the global `.modal-body { font-size: 1.25rem }` reduction so the
         publish modal renders at the app's body scale — same as the schema
         editor — letting it reuse `.se-col-name-display` / `.se-section h3`
         sizing directly instead of re-stating font sizes. */
      .publish-body { font-size: 1.6rem; }
      .publish-actions { display: flex; gap: .5rem; align-items: center; }
      /* Section headings match the schema editor's `.se-section h3`
         (1.1rem, brown-darker, body weight) so "COLUMNS" / "PROJECTED"
         read like its "TABLES" / "RELATIONSHIPS". */
      .publish-section-title { font-size: 1.1rem; font-weight: 700; color: var(--brown-darker); margin: 1.1rem 0 .4rem; }
      .publish-section-title:first-child { margin-top: 0; }
      .publish-col-list { display: flex; flex-direction: column; }
      .publish-col-row { display: flex; align-items: center; gap: .5rem; padding: .25rem .35rem; border-radius: 3px; cursor: pointer; user-select: none; }
      .publish-col-row:hover { background: var(--cream-2); }
      /* Element-qualified so it beats Milligram's bare `button` (C8): a
         bare icon button, not a pill. */
      button.publish-diamond {
        background: none; border: 0; margin: 0; padding: 0; height: auto;
        line-height: 1; cursor: pointer; display: inline-flex; width: 1.5rem; flex-shrink: 0;
      }
      button.publish-diamond .icon-diamond svg { width: 1.4rem; height: 1.4rem; display: block; }
      /* Diamond colours come from the shared `.icon-diamond-{off,ro,rw}`
         rules used everywhere else (off = faint hollow, read-only / editable
         = brown filled, read-only carrying the padlock glyph). No
         publish-only colour — the system uses one diamond palette. */
      /* Column names reuse `.se-col-name-display` (the schema editor's
         column-name class) for both layout AND size — no publish-specific
         font rule. */
      .publish-col-count { color: var(--brown-dark); font-size: .82em; flex-shrink: 0; }
      .publish-col-note { margin-left: auto; color: #9a8f80; font-size: .78em; font-style: italic; flex-shrink: 0; }
      /* Related-column group header + indented sub-column rows. */
      .publish-col-group { font-size: .82em; font-weight: 700; color: var(--brown-dark); padding: .45rem .35rem .1rem; text-transform: uppercase; letter-spacing: .03em; }
      .publish-col-row.publish-col-sub { padding-left: 1.6rem; }
      /* Published-grids index list */
      .publications-row { display: flex; align-items: center; gap: .6rem; padding: .4rem 0; border-bottom: 1px solid var(--brown-border); }
      .publications-row:last-child { border-bottom: 0; }
      .publications-name { font-weight: 600; }
      .publications-meta { color: var(--brown-dark); font-size: .82em; }
      .publications-row .publications-manage { margin-left: auto; }
      /* Toggle rows (projected / writes / inserts). Element-qualified
         `label` per C8 so they don't inherit Milligram's bold-block-large
         label defaults; sized to match the column-name rows and aligned on
         a common left edge with their checkbox. */
      label.publish-toggle {
        display: flex; align-items: center; gap: .45rem; flex-wrap: wrap;
        font-weight: 400; margin: 0; padding: .15rem .35rem;
      }
      label.publish-toggle > input[type="checkbox"] { width: auto; height: auto; margin: 0; flex-shrink: 0; }
      /* Compact select so the projected row's visible height matches
         the adjacent inserts / deletes rows (which are pure text +
         checkbox). At height: 2.2rem the select used to make this row
         read as "bigger" than its siblings even though the prose font
         size was identical. */
      select.publish-proj-select { display: inline-block; width: auto; max-width: 16rem; height: 1.8rem; padding: 0 1.6rem 0 .5rem; margin: 0; background-image: none; line-height: 1; font-size: .92em; }
      .publish-proj-total { font-family: var(--mono); color: var(--brown-dark); font-size: .85em; }
      .publish-proj-total.over { color: #b4453a; font-family: inherit; }
      .publish-writes-row { margin-top: .1rem; display: flex; flex-direction: column; gap: .15rem; }
      /* Patient-facing form chrome inputs (FORM tab): friendly title + intro.
         Stacked label-over-control rows matching the surrounding sections. */
      .publish-form-chrome { display: flex; flex-direction: column; gap: .6rem; margin-top: .4rem; }
      .publish-form-chrome > label { display: flex; flex-direction: column; gap: .2rem; margin: 0; }
      .publish-form-chrome > label > span { font-weight: 400; }
      /* Inputs inherit the CUSTOMIZATION area's 1.3rem (matching the ACCESS
         controls + the .auto-select selects above), via `font: inherit`. */
      input.publish-form-title-input, textarea.publish-form-intro-input {
        width: 100%; margin: 0; font: inherit; padding: .4rem .5rem; line-height: 1.4;
      }
      input.publish-form-title-input { height: 2.8rem; }
      textarea.publish-form-intro-input { min-height: 5.6rem; resize: vertical; }
      .publish-toggle-disabled { opacity: .45; cursor: not-allowed; }
      /* "row-edit token expires after …" — duration picked from a small
         dropdown of presets (1m / 5m / 15m / 1h / 8h / 24h). Stays on
         the same line as the prose; the select shrinks to fit its
         widest option (Milligram's bare `<select>` rule otherwise
         stretches it to fill the row). */
      .publish-row-access { gap: .35rem; flex-wrap: nowrap; }
      select.publish-row-access-select {
        display: inline-block; width: auto; max-width: 10rem;
        height: 2.2rem; padding: 0 1.6rem 0 .5rem; margin: 0;
        background-image: none; line-height: 1;
        font: inherit; font-size: 1.3rem;
      }
      /* REPUBLISH button when disabled (no pending changes) renders
         muted so it reads as inert. Active state inherits .accent. */
      .publish-republish[disabled] {
        opacity: .35; cursor: not-allowed; filter: grayscale(60%);
      }
      .publish-warn {
        margin: 0; background: #fff6e0; border: 1px solid #e6c766;
        color: #6b5410; padding: .5rem .7rem; border-radius: 4px; font-size: .92rem; line-height: 1.4; font-weight: 700;
      }
      /* Dark-scheme warning — amber on a dark warm panel. Covers explicit
         dark (data-theme) AND auto dark (prefers-color-scheme, no override). */
      html[data-theme="dark"] .publish-warn { background: #2e2510; border-color: #6b5410; color: #f0d98a; }
      @media (prefers-color-scheme: dark) {
        html:not([data-theme="light"]) .publish-warn { background: #2e2510; border-color: #6b5410; color: #f0d98a; }
      }
      .publish-urls { margin-top: 1rem; border-top: 1px solid var(--brown-border); padding-top: .75rem; }
      .publish-urls-head { font-size: .85rem; color: var(--brown-dark); margin-bottom: .4rem; }
      .publish-url-row { display: flex; align-items: center; gap: .5rem; margin: .3rem 0; }
      .publish-url-val { font-family: var(--mono); font-size: .82rem; min-width: 5rem; max-width: 9rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
      input.publish-url-input { flex: 1; font-family: var(--mono); font-size: .8rem; height: 2.2rem; margin: 0; padding: 0 .5rem; }
      button.publish-url-copy { height: auto; line-height: 1.2; padding: .25rem .6rem; margin: 0; text-transform: none; letter-spacing: 0; font-size: .85em; }
      /* Prominent API help/rows endpoint block on the publish modal's API tab. */
      .publish-api-help { margin: .6rem 0 1rem; padding: .6rem .75rem; background: var(--cream-2, #f3ead7); border: 1px solid var(--brown-border, #d8c9a8); border-radius: 4px; }
      .publish-api-help-label { font-size: .82rem; color: var(--brown-dark, #6b5a3a); margin-bottom: .35rem; }

      /* Drop-target highlight on a `file`-type cell while a file drag
         hovers it. Subtle outline + warm tint so the user sees which
         cell will accept the drop. Cleared by JS on dragleave / drop. */
      .grid-table tbody td.cell-drop-active {
        outline: 2px dashed var(--brown);
        outline-offset: -2px;
        background: var(--card-hover-bg, #ebe0cd);
      }

      /* Conditions indicator on related column header */
      .th-cond-marker { color: var(--brown-light, #a1a1aa); cursor: help;
                        margin-left: .15rem; }

      /* ── Reusable primitives ─────────────────────────────────────── */

      /* empty-slot: a focusable placeholder that becomes a form on
         focus / click. Used by the schema editor for + column / +
         relationship rows; can be reused elsewhere.
         No font-size override — children inherit the surrounding
         pane's default so the +new placeholder + the revealed form
         match the rest of the section's text. */
      .empty-slot {
        display: flex; align-items: center; min-height: 2.4rem;
        padding: .25rem .5rem; border: 1px dashed transparent; border-radius: 4px;
        color: var(--brown-light, #a1a1aa); cursor: text;
      }
      .empty-slot:hover, .empty-slot:focus { border-color: var(--brown-border); outline: none; color: var(--brown); background: rgba(120, 90, 60, .04); }
      .empty-slot.active { border-color: var(--brown-border); background: rgba(120, 90, 60, .04); padding: .5rem; align-items: stretch; }
      .empty-slot-placeholder { font-style: italic; }

      /* collapsible: a focusable header with chevron + optional pill,
         and a body that renders only when open. Used by the schema
         editor's TABLES and RELATIONSHIPS sections. */
      .collapsible { margin-bottom: .35rem; }
      .collapsible-header {
        display: flex; align-items: center; gap: .5rem;
        padding: .35rem .5rem; border-radius: 4px; cursor: pointer; outline: none;
        /* No text selection on double-click — the dblclick opens rename
           and a selected name flickering through the toggle is noisy. */
        user-select: none;
      }
      .collapsible-header:hover, .collapsible-header:focus { background: rgba(120, 90, 60, .08); }
      .collapsible-chevron {
        width: 1.4rem; font-size: 1.6rem; line-height: 1; font-weight: 700;
        color: var(--brown-darker); text-align: center;
      }
      .collapsible-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; color: var(--brown-darker); }
      .collapsible-pill {
        /* Reverse-video chip — `--chip-bg` is the inverse of the
           surface in each theme so the pill always reads as the
           opposite-luminance plate of its surroundings. Same tokens
           drive `.se-title-tablename` and `.cell-related-count`. */
        background: var(--chip-bg, #1f1d1c); color: var(--chip-ink, #fff);
        border-radius: 999px; padding: .25rem .9rem;
        font-size: 1.05rem; font-weight: 700; line-height: 1.1;
        min-width: 2.5rem; text-align: center;
      }
      .collapsible-body { padding: .25rem 0 .5rem 1.5rem; }

      /* ── Schema editor modal ─────────────────────────────────────── */
      .modal-schema-editor { min-width: 960px; max-width: 95vw; max-height: 92vh; width: 1200px; }
      /* Import modal — narrower and shorter than the schema editor.
         Two pages share the same chrome; the inner host swaps views. */
      .modal-import { min-width: 640px; max-width: 90vw; width: 880px; max-height: 90vh; }
      /* Data export modal — small two-button affordance. CSS mostly
         inherited from .modal / .modal-header / .modal-body; the
         button row is a tight flex strip and the bullet list uses the
         shared `muted` token for the asterisks-vs-bullets distinction. */
      .modal-export { min-width: 420px; max-width: 90vw; width: 560px; }
      .imp-export-body { padding: 1.25rem 1.5rem; display: flex; flex-direction: column; gap: 1rem; }
      .imp-export-buttons { display: flex; gap: .75rem; flex-wrap: wrap; }
      .imp-export-buttons a.imp-export-link { margin: 0; min-width: 12rem; text-align: center; }
      .imp-export-note { margin: 0; font-size: 1rem; }
      .imp-export-points { margin: 0; padding-left: 1.25rem; font-size: 1.05rem; line-height: 1.6; }
      .imp-export-points li code { background: var(--code-tint, rgba(0,0,0,.06)); padding: .05rem .35rem; border-radius: 3px; font-size: .95em; }
      /* Modal host: full height so .modal-header / .modal-body / .modal-footer
         can flex-grow correctly. No internal padding here — that lives on
         .modal-body so the header / footer span edge-to-edge. */
      .imp-host { display: flex; flex-direction: column; min-height: 0; height: 100%; overflow: hidden; }
      /* Wrap pins the upload button over the centre of the (empty)
         textarea. :focus-within hides it the moment the textarea takes
         focus so it doesn't sit on top of the user's paste — clicking
         the button itself opens the OS file picker (mousedown blocks
         focus from leaking through to the textarea). */
      .imp-paste-wrap { position: relative; }
      .imp-paste { width: 100%; min-height: 240px; font-family: var(--mono); font-size: 1rem; padding: .5rem; box-sizing: border-box; }
      .imp-paste-wrap > .imp-file-pick-wrap {
        position: absolute; top: 50%; left: 50%;
        transform: translate(-50%, -50%);
        display: flex; align-items: center; gap: .35rem;
      }
      .imp-paste-wrap > .imp-file-pick-wrap > .imp-file-pick {
        margin: 0; box-shadow: 0 1px 4px rgba(58,40,23,.15);
      }
      .imp-or-prefix { font-size: 1rem; }
      .imp-paste-wrap:focus-within > .imp-file-pick-wrap { display: none; }
      .imp-warn { padding: .5rem .75rem; background: var(--notice-bg, #fef3c7); border: 1px solid var(--notice-border, #d97706); border-radius: 4px; color: var(--notice-ink, #92400e); font-size: .95rem; }
      .imp-error { padding: .5rem .75rem; background: var(--danger-bg); border: 1px solid var(--danger); border-radius: 4px; color: var(--danger-ink, #7f1d1d); font-size: .95rem; }
      .imp-table-row { display: flex; align-items: center; gap: .5rem; }
      /* Milligram puts margin-bottom on every label — kill it so the
         label baselines with the input inside this inline row. */
      .imp-table-label { margin: 0; font-weight: 700; color: var(--brown-darker); }
      .imp-table-name { flex: 1; max-width: 28rem; }
      .imp-sanit-hint { font-family: var(--mono); font-size: .9rem; }
      .imp-header-toggle-row { display: flex; align-items: center; gap: .5rem; font-size: 1.1rem; color: var(--brown-light); }
      /* Both the FIRST ROW label and its `is data, not header` hint
         share the same muted, regular-weight typography so the row
         reads as one phrase. Override Milligram's `label { font-size:
         1.6rem; font-weight: 400 }` so the label doesn't render bigger
         than the sibling hint. */
      .imp-header-toggle { display: inline-flex; align-items: center; gap: .35rem; cursor: pointer; font: inherit; margin: 0; }
      .imp-header-toggle-hint { /* font-size inherited from the row */ }
      /* List is the SINGLE grid that owns the column tracks; each row
         consumes the full span via subgrid so column widths line up
         across rows (without subgrid each row was its own grid and
         `auto` sized per-row, staggering the inputs). The first column
         is `auto` — sized to the widest upload heading across all
         rows. */
      .imp-col-list {
        display: grid;
        grid-template-columns: auto 1fr 1.6fr 1fr 1.6fr;
        column-gap: .5rem; row-gap: .25rem;
      }
      /* Rows are FIXED HEIGHT so the layout stays stable as the user
         flips column types. The cell with the most content is the
         choice chips: capped at two rows of chips by the row's
         overflow: hidden — a column with five chips wraps to the
         second row, anything beyond is clipped. Sample data + chips
         share .85rem so two lines of either fit in the same vertical
         budget (~2 × 1.5rem chip + .25rem gap + .45rem * 2 padding). */
      .imp-col-row {
        display: grid;
        grid-template-columns: subgrid;
        grid-column: 1 / -1;
        align-items: center; padding: .45rem .5rem;
        border-bottom: 1px dotted var(--brown-border);
        height: 4rem; overflow: hidden;
      }
      .imp-col-row:hover { background: var(--cream-2); }
      .imp-col-cell { min-width: 0; }
      .imp-col-upload {
        font-family: var(--mono); color: var(--brown-light);
        margin-right: 2.5rem;     /* + .5rem grid gap = 3rem total */
      }
      .imp-col-name { margin-right: 2.5rem; }     /* + .5rem grid gap = 3rem to sample column */
      .imp-col-name-input { width: 100%; }
      /* Sample text + choice chips can grow to two rows; align them
         to the top of the cell so the second row flows below rather
         than pushing the centred content off-balance. */
      .imp-col-sample {
        font-size: .85rem; line-height: 1.25;
        display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
        overflow: hidden;
        align-self: start; padding-top: .15rem;
      }
      .imp-col-choices { align-self: start; padding-top: .15rem; }
      .imp-col-type-select { width: 100%; }
      /* Compact chip sizing scoped to the importer's column rows so
         the schema editor's chip family is untouched. Long chip text
         is capped at ~40 chars with ellipsis so any one chip can't
         hog the row. */
      .imp-col-row .chip {
        height: 1.5rem; padding: 0 .45rem; font-size: .85rem; line-height: 1;
        max-width: 40ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
      }
      .imp-col-row .chip-x { font-size: .85rem; }
      .imp-choices { gap: .25rem .35rem; align-content: flex-start; max-height: 100%; }
      /* ── Append mode (mapping editor) ──────────────────────────── */
      .imp-map-list { display: flex; flex-direction: column; gap: .35rem; }
      .imp-map-row {
        display: grid; grid-template-columns: 1fr auto 1fr;
        gap: .75rem; align-items: center;
        padding: .45rem .5rem; border-bottom: 1px dotted var(--brown-border);
      }
      .imp-map-source-name { font-family: var(--mono); }
      .imp-map-source-sample { font-size: .85rem; line-height: 1.25;
        display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
      }
      .imp-map-arrow { font-size: 1.2rem; text-align: center; }
      .imp-map-target-select { width: 100%; }
      .imp-map-unmapped { padding: .5rem .75rem; background: var(--cream-2); border-radius: 4px; font-size: .95rem; }
      .imp-dryrun-panel { margin-top: .5rem; }
      .imp-dryrun-empty, .imp-dryrun { padding: .5rem .75rem; background: var(--cream-2); border-radius: 4px; font-size: .95rem; }
      .imp-dryrun-line { color: var(--brown-darker); }
      .imp-dryrun-errors { margin: .35rem 0 0 1.25rem; font-family: var(--mono); font-size: .85rem; }
      .imp-decimals { display: flex; align-items: center; gap: .35rem; }
      .imp-decimals-prefix { font-family: var(--mono); }
      .imp-decimals-label { margin: 0; font-size: .9rem; }
      .imp-decimals-select { width: auto; min-width: 4rem; }
      .imp-report { padding: .5rem .75rem; background: var(--cream-2); border-radius: 4px; font-size: .95rem; }
      .imp-report-line { color: var(--brown-darker); }
      .home-head-actions { display: flex; gap: .5rem; align-items: center; }
      /* Alt-held shortcut hints: small letter badges over the home
         action buttons advertise their Alt-key shortcuts. Letter
         badge fades in only while Alt is held — see `onDocKey` /
         `onDocKeyUp` in frontend/home/index.js for the body-class
         toggle. `+ view` → Alt+G, import (Tables rail) → Alt+I. */
      .home-new-grid2, .home-defaults-import { position: relative; }
      .home-new-grid2::after,
      .home-defaults-import::after {
        position: absolute; top: -.45rem; right: -.45rem;
        min-width: 1.4rem; height: 1.4rem; padding: 0 .25rem;
        background: var(--accent, #3b6e6a); color: #fff;
        border-radius: 50%;
        font-family: inherit; font-size: .9rem; font-weight: 700;
        line-height: 1.4rem; text-align: center; text-transform: none; letter-spacing: 0;
        box-shadow: 0 1px 2px rgba(58,40,23,.25);
        opacity: 0; transform: scale(.8);
        transition: opacity .08s ease, transform .08s ease;
        pointer-events: none;
      }
      .home-new-grid2::after { content: "g"; }
      .home-defaults-import::after { content: "i"; }
      /* `csvdb` brand: Alt+C navigates home. Same badge chrome as the
         action buttons but anchored to the title's right edge; the
         title itself shifts to dark green while Alt is held so the
         shortcut hint reads as part of the brand. */
      .home-brand { position: relative; }
      .home-brand::after {
        content: "c";
        position: absolute; top: -.45rem; right: -1.05rem;
        min-width: 1.4rem; height: 1.4rem; padding: 0 .25rem;
        background: var(--accent, #3b6e6a); color: #fff;
        border-radius: 50%;
        font-family: inherit; font-size: .9rem; font-weight: 700;
        line-height: 1.4rem; text-align: center; text-transform: none; letter-spacing: 0;
        box-shadow: 0 1px 2px rgba(58,40,23,.25);
        opacity: 0; transform: scale(.8);
        transition: opacity .08s ease, transform .08s ease;
        pointer-events: none;
      }
      body.alt-held .home-new-grid2::after,
      body.alt-held .home-defaults-import::after,
      body.alt-held .home-brand::after { opacity: 1; transform: scale(1); }
      body.alt-held .home-brand .home-brand-text,
      body.alt-held .home-brand .home-brand-glyph { color: #2f4f30; }
      /* Grid page uses the `.brand` variant of the same component;
         repeat the alt-held hint chrome so the shortcut is visible
         outside the home page too. */
      .brand { position: relative; }
      .brand::after {
        content: "c";
        position: absolute; top: -.45rem; right: -1.05rem;
        min-width: 1.4rem; height: 1.4rem; padding: 0 .25rem;
        background: var(--accent, #3b6e6a); color: #fff;
        border-radius: 50%;
        font-family: inherit; font-size: .9rem; font-weight: 700;
        line-height: 1.4rem; text-align: center; text-transform: none; letter-spacing: 0;
        box-shadow: 0 1px 2px rgba(58,40,23,.25);
        opacity: 0; transform: scale(.8);
        transition: opacity .08s ease, transform .08s ease;
        pointer-events: none;
      }
      body.alt-held .brand::after { opacity: 1; transform: scale(1); }
      body.alt-held .brand .brand-text,
      body.alt-held .brand .brand-glyph { color: #2f4f30; }
      /* Generic modal chrome — reusable across modals (schema editor,
         import, future). Each modal wraps its content in this header /
         body / optional footer; primary actions live in `.modal-actions`
         pinned to the right of the header. Padding + cream-2 background
         + brown-border bottom rule are the shared visual signature. */
      .modal-shell { display: flex; flex-direction: column; min-height: 0; height: 100%; }
      .modal-header {
        display: flex; justify-content: space-between; align-items: center; gap: 1rem;
        padding: .9rem 1.25rem;
        background: var(--cream-2); border-bottom: 1px solid var(--brown-border);
      }
      .modal-title {
        margin: 0; font-size: 1.4rem; color: var(--brown-darker);
        letter-spacing: .04em; font-weight: 700;
      }
      .modal-actions { display: flex; gap: .5rem; align-items: center; }
      .modal-body {
        padding: 1rem 1.25rem; overflow: auto; min-width: 0; flex: 1;
        display: flex; flex-direction: column; gap: .9rem;
      }
      .modal-footer {
        display: flex; justify-content: flex-start; gap: .5rem; align-items: center;
        padding: .9rem 1.25rem;
        background: var(--cream-2); border-top: 1px solid var(--brown-border);
      }
      /* Legacy alias for the schema editor's wrapper; the wrapper
         keeps a stable class name so any external selectors still resolve. */
      .schema-editor { display: flex; flex-direction: column; min-height: 0; height: 100%; }
      /* ── Reusable form controls (modal-input / modal-select) ──────
         Sized to match the schema editor's row inputs (2.2rem high) so
         every modal renders form controls at the same baseline. Use
         these on any new modal control — `.se-col-name-input` and
         `.se-col-type-select` remain as the schema editor's specialised
         contexts but inherit the same sizing. */
      input.modal-input {
        height: 2.2rem; padding: 0 .5rem; margin: 0; font: inherit;
      }
      select.modal-select {
        height: 2.2rem; padding: 0 1.6rem 0 .5rem; margin: 0; min-width: 7rem;
        /* Convention C7: kill Milligram's chevron + pin line-height so
           option text doesn't drop to the bottom of short selects. */
        background-image: none; line-height: 1;
      }
      .se-save-error { color: var(--danger); font-size: 1rem; }
      .se-save-btn { min-width: 12rem; }
      /* Reverse-video chip for the focal table name in the modal title.
         Matches the schema editor's dark-chip family (saved choice
         chips, row-count pills) so it reads as "this is an entity". */
      .se-title-tablename {
        background: var(--chip-bg, #1f1d1c); color: var(--chip-ink, #fff);
        padding: .15rem .55rem; border-radius: 4px;
        margin: 0 .15rem; font-weight: 700; letter-spacing: .01em;
      }
      .se-body { display: flex; flex: 1; min-height: 0; overflow: hidden; position: relative; }
      /* Diagram pane is 2/5 of the modal, right pane is 3/5 — the
         relationships / column lists benefit from the wider column. */
      .se-pane { min-width: 0; padding: 1rem; overflow: auto; }
      /* The left pane scrolls the diagram inside .se-diagram-scroll so
         the toggle row can stay pinned via absolute positioning. */
      .se-pane-left { flex: 2 1 0; border-right: 1px solid var(--brown-border); background: var(--surface); position: relative; padding: 0; overflow: hidden; }
      .se-pane-right { flex: 3 1 0; }
      .se-diagram-scroll { position: absolute; inset: 0; overflow: auto; padding: 1rem; }
      /* Grab-to-pan affordance on the diagram scroll containers (drag to
         scroll); flips to grabbing mid-drag. Table boxes keep their own
         pointer cursor (they're click-to-focus) until a pan is in progress. */
      .se-diagram-scroll, .se-diagram-max-scroll { cursor: grab; }
      .se-diagram-scroll.se-panning, .se-diagram-max-scroll.se-panning,
      .se-diagram-scroll.se-panning [data-name], .se-diagram-max-scroll.se-panning [data-name] { cursor: grabbing; }
      .se-diagram-toggles {
        position: absolute; bottom: .75rem; right: .9rem;
        display: flex; gap: .9rem; padding: .45rem .8rem;
        /* Translucent surface so the diagram is faintly visible behind
           it. `--surface` flips in dark mode (#fff → #292524), and the
           translucent `--legend-tint` does too via the dark-mode block
           further up. The shadow turns to a subtle dark glow on dark
           backgrounds. */
        background: var(--legend-tint, rgba(255, 255, 255, .92));
        border: 1px solid var(--brown-border);
        border-radius: 4px; box-shadow: 0 1px 4px var(--legend-shadow, rgba(58,40,23,.1));
        /* Family / size / all-caps match the TABLES / RELATIONSHIPS
           section headings; weight stays normal so the toggle labels
           read as controls rather than headings of their own. */
        font-family: inherit; font-size: 1.1rem; font-weight: 400;
        text-transform: uppercase; letter-spacing: .02em;
        color: var(--brown-darker);
      }
      .se-toggle { display: inline-flex; align-items: center; gap: .35rem; margin: 0; cursor: pointer; font: inherit; color: inherit; text-transform: inherit; letter-spacing: inherit; }
      .se-toggle input { margin: 0; }
      /* "maximize" / "minimize" read as text links, sharing the toggle row's
         uppercase chrome. */
      .se-diagram-maximize {
        background: none; border: 0; padding: 0; margin: 0; cursor: pointer;
        font: inherit; color: inherit; text-transform: inherit; letter-spacing: inherit;
      }
      .se-diagram-maximize:hover { color: var(--brown); text-decoration: underline; }
      /* Maximized diagram: covers the whole body (both panes) below the title,
         scrolling the diagram at natural size. */
      .se-diagram-max {
        position: absolute; inset: 0; z-index: 6;
        display: flex; flex-direction: column; background: var(--surface);
      }
      .se-diagram-max-bar {
        display: flex; align-items: center; gap: .9rem;
        padding: .5rem .9rem; border-bottom: 1px solid var(--brown-border);
        background: var(--legend-tint, rgba(255, 255, 255, .92));
        font-family: inherit; font-size: 1.1rem; font-weight: 400;
        text-transform: uppercase; letter-spacing: .02em; color: var(--brown-darker);
      }
      .se-diagram-max-bar .se-diagram-min { margin-left: auto; }
      .se-diagram-max-scroll { flex: 1; min-height: 0; overflow: auto; padding: 1rem; }
      /* Natural size inside the scroller (don't shrink-to-fit like the pinned
         pane does) so large schemas can be panned. */
      .se-diagram-max-scroll .se-diagram svg { max-width: none; }
      .se-pane-right { background: var(--cream); }
      .se-diagram { display: block; min-height: 100%; }
      .se-diagram svg { max-width: 100%; height: auto; }
      /* "fit" off → render the diagram at natural size so .se-diagram-scroll
         pans it. Same specificity as the rule above, so it must come after. */
      .se-diagram-nofit svg { max-width: none; }
      /* Table boxes are clickable — jump to the table's right-pane card. */
      .se-diagram svg [data-name] { cursor: pointer; }

      /* Mobile: stack the two panes vertically with the editable right
         pane on top (the user's primary touch target) and the read-only
         diagram below. `column-reverse` flips the visual order without
         touching markup. The left pane needs an explicit min-height
         because its children are absolute-positioned, and the divider
         flips from border-right to border-top. */
      @media (max-width: 720px) {
        .modal-schema-editor { min-width: 0; width: 100%; max-width: 100vw; max-height: 100vh; }
        .modal-schema-editor .modal-header { flex-wrap: wrap; row-gap: .5rem; }
        .se-body { flex-direction: column-reverse; }
        .se-pane-left {
          flex: 0 0 auto; min-height: 18rem;
          border-right: 0; border-top: 1px solid var(--brown-border);
        }
        .se-pane-right { flex: 1 1 auto; }
      }
      .se-section { margin-bottom: 1.5rem; }
      .se-section h3 { margin: 0 0 .5rem; font-size: 1.1rem; color: var(--brown-darker); }

      .se-table-card {
        background: var(--surface);
        border: 1px solid rgba(120, 90, 60, .18);
        border-radius: 6px;
        margin-bottom: .5rem;
        padding: .15rem .4rem;
        box-shadow: 0 1px 2px rgba(58,40,23,.04);
      }
      .se-table-card .collapsible-header:hover,
      .se-table-card .collapsible-header:focus { background: rgba(120, 90, 60, .06); }
      /* Pending-drop card (junction whose owning N:M was changed away
         or soft-deleted). Visual mirror of the rel / col soft-delete
         pattern — strike-through name + faded chrome. The card stays
         expandable so the user can still inspect what's about to go. */
      .se-table-card.se-table-pending-drop .collapsible-title,
      .se-table-card.se-table-pending-drop .se-table-name { text-decoration: line-through; color: var(--brown-light, #a1a1aa); }
      .se-table-card.se-table-pending-drop { opacity: .85; }
      .se-table-name { font-weight: 600; color: var(--brown-darker); }
      .se-table-edit { display: inline-flex; align-items: center; gap: .25rem; }
      .se-table-edit input { margin: 0; height: 2.2rem; padding: 0 .5rem; font: inherit; }
      /* Non-editable chain glyph prefix shown when renaming a junction
         table. The user only edits the suffix; state.renameTable
         re-prepends `_JUNCTION_` on commit. */
      .se-junction-prefix { color: var(--brown); user-select: none; font-size: 1.1rem; }
      .se-table-body { display: flex; flex-direction: column; gap: .2rem; }

      .se-col-row {
        display: flex; align-items: center; gap: .5rem;
        padding: .25rem .35rem; border-radius: 3px; outline: none;
      }
      .se-col-display { cursor: pointer; }
      .se-col-display:hover, .se-col-display:focus { background: rgba(120, 90, 60, .08); }
      .se-col-add { cursor: text; color: var(--brown-light, #a1a1aa); user-select: none; }
      .se-col-add:hover, .se-col-add:focus { background: rgba(120, 90, 60, .06); color: var(--brown); }
      /* Mouse-vs-keyboard mode (convention C5). While the keyboard is
         in active use the modal box carries .se-kb-nav; suppress hover
         backgrounds on rows the cursor *isn't* also focused on, so they
         don't compete visually with the keyboard-focused row. When the
         mouse and keyboard happen to land on the same row, :focus wins
         and the highlight stays. Any mousemove drops the class. */
      .se-kb-nav .se-col-display:hover:not(:focus),
      .se-kb-nav .se-col-add:hover:not(:focus),
      .se-kb-nav .collapsible-header:hover:not(:focus) { background: transparent; }
      .se-kb-nav .se-col-add:hover:not(:focus) { color: var(--brown-light, #a1a1aa); }
      .se-col-add .empty-slot-placeholder { font-style: italic; }
      .se-col-handle {
        color: var(--brown-light, #a1a1aa); cursor: grab; user-select: none;
        width: 1.1rem; text-align: center; font-size: 1.05rem; line-height: 1;
      }
      .se-col-handle:active { cursor: grabbing; }
      /* Seam between plain columns and the FK / relationship rows. Same grey
         as the column drag handles to the left of each name. */
      .se-col-sep { height: 0; margin: .3rem 0; border-top: 1px solid var(--brown-light, #a1a1aa); }
      .se-col-row.dragging { opacity: .55; }
      .se-col-row.drag-over { box-shadow: inset 0 2px 0 0 var(--brown); }
      .se-col-name-display { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
      /* Right-align the type text + push the cell to the row's right
         edge (just before the delete button) so type endings line up
         across rows regardless of name length. font: inherit keeps
         it at the right-pane default (the column-name display next
         to it inherits the same). */
      .se-col-type-display {
        color: var(--brown); font: inherit;
        margin-left: auto; min-width: 5rem;
        text-align: right; font-variant-numeric: tabular-nums;
      }
      /* Choice chip list. Renders only inside a column row in edit
         mode when col.type === "choice". Each chip is focusable and
         the same four-state widget as columns / relationships:
         display, edit (in-place input), soft-deleted, and the "+ choice"
         empty-slot variant. */
      /* Column-edit row stacks the main row + chip / validator rows
         vertically. The main row keeps its intrinsic width (align-
         items: flex-start) so the name input doesn't stretch to the
         pane edge just because chips / validators appear below. */
      .se-col-edit.se-col-edit-stack { flex-direction: column; align-items: flex-start; gap: .35rem; }
      .se-col-edit-main { display: flex; align-items: center; gap: .5rem; }
      /* Stretch the main row to the pane width so the name input grows
         and the "*" pill + type select sit at the right edge — mirrors
         the display row's name-left / type-right layout. The options /
         validator rows render below at their natural width. */
      .se-col-edit .se-col-edit-main { align-self: stretch; }
      /* Flex spacer used by the virtual N:M edit row to push the target
         table name flush to the right edge (same place the display
         row's `×` button sits). */
      .se-col-edit-spacer { flex: 1 1 auto; }
      .se-validator-row {
        display: flex; flex-wrap: wrap; gap: .35rem; padding-left: 1.6rem;
        align-items: center;
      }
      /* Leading "cardinality:" / similar label text — italic + slightly
         muted, matching the "+ validator" placeholder treatment so the
         second row's leading text has consistent typography. */
      .se-validator-label {
        font-style: italic; color: var(--brown-light);
      }
      /* Relationship validators: an indented sub-row beneath the relationship
         line so the pill / "+ validator" reads as subordinate to the rel. */
      .se-relval-subrow {
        display: flex; flex-wrap: wrap; gap: .35rem; align-items: center;
        padding-left: 2.4rem; margin: .1rem 0 .3rem;
      }
      /* Two-line picker: [validator] per [subject]  /  [params]. The faintest
         green of the three states — it's being entered, not even staged yet. */
      .se-relval-picker { display: inline-flex; flex-direction: column; gap: .3rem; align-items: flex-start; background: rgba(59, 110, 106, 0.2); border-radius: 6px; padding: .3rem .4rem; }
      .se-relval-line { display: inline-flex; flex-wrap: wrap; gap: .35rem; align-items: center; }
      .se-relval-per { font-style: italic; color: var(--brown-light); margin: 0 .45rem; }
      /* Savedness reads off the green: a SAVED chip is full green (the base
         .se-validator-chip #3b6e6a); a STAGED (not-yet-saved) chip is a fainter
         green + dashed outline; the OPEN picker (being entered, not even staged
         yet) is fainter still — three steps from committed → tentative. */
      .se-validator-chip.se-relval-chip-pending { background: rgba(59, 110, 106, 0.45); border: 1px dashed var(--brown-light, #a1a1aa); }
      .se-relval-subject { color: var(--brown-darker); }
      /* Keep the picker's selects/inputs inline + content-sized (Milligram
         defaults them to display:block; width:100%, which would wrap each
         control onto its own line inside the flex line). */
      .se-relval-picker select, .se-relval-picker input { height: 1.8rem; margin: 0; width: auto; flex: 0 0 auto; }
      .se-relval-picker input[type="number"] { width: 5rem; }
      .se-relval-picker .se-validator-pick { border: 1px solid var(--brown-border); border-radius: 4px; background: var(--cream-2); }
      .se-validator-chip {
        display: inline-flex; align-items: center; gap: .25rem;
        background: #3b6e6a; color: #fff; border-radius: 999px;
        height: 1.8rem; box-sizing: border-box;
        padding: 0 .6rem; font: inherit; line-height: 1; outline: none;
        user-select: none;
      }
      .se-validator-chip:focus { box-shadow: 0 0 0 2px var(--brown-border); }
      /* Relationship-validator chips can carry a long limit_related_rows_where
         summary. Let them grow past the fixed 1.8rem chip height to wrap (the
         summary breaks before "where" → two lines) and cap at the subrow width
         so a long rule never bleeds out. */
      .se-validator-chip.se-relval-chip {
        height: auto; min-height: 1.8rem; max-width: 100%;
        padding-top: .2rem; padding-bottom: .2rem;
      }
      .se-relval-chip .se-choice-text {
        white-space: pre-line; line-height: 1.3;
        min-width: 0; overflow-wrap: anywhere;
      }
      /* Cardinality pills (FK column "one" / "many" picker) reuse the
         validator-chip shape. Inactive pill is muted; active stays at
         the regular accent colour. Disabled state (existing FK column)
         dims further + sets cursor:not-allowed. */
      .se-cardinality-pill,
      button.se-flag-pill {
        cursor: pointer; border: 0;
        background: var(--cream-2); color: var(--brown-light);
        /* Override Milligram's bare-`button` uppercase + letter-
           spacing so the pill text reads as plain lowercase "one" /
           "many" / "unique" matching the adjacent label. */
        text-transform: none; letter-spacing: 0;
      }
      .se-cardinality-pill.active,
      button.se-flag-pill.active { background: #3b6e6a; color: #fff; }
      .se-cardinality-pill.disabled,
      .se-cardinality-pill[disabled] {
        cursor: not-allowed; opacity: .55;
      }
      /* The "*" required toggle in the edit row mimics the display-mode
         superscript star: a raised green pill — but kept a comfortable click
         target (generous padding + min-width), not a tiny glyph. */
      .se-col-edit-main button.se-flag-pill {
        height: auto; min-width: 1.7rem; padding: .25rem .55rem;
        font-weight: 700; font-size: 1.2rem; line-height: 1;
        justify-content: center; align-self: flex-start; margin-top: .15rem;
      }
      /* Bold "*" after a required column's name in display mode (e.g.
         surname*). Uses the accent so it stands out from the name. */
      .se-col-required-star { font-weight: 700; color: #3b6e6a; margin-left: .1rem; }
      /* Plain-language gloss after the stamp pill in the options row. */
      .se-flag-note { color: var(--brown-light); }
      /* Comma joining the unique pill to the "(stamp) moment of creation"
         clause — pulled left to cancel the row's flex gap so it hugs the
         preceding pill and reads as a sentence ("unique, (stamp) …"). */
      .se-flag-comma { margin-left: -.3rem; color: var(--brown-light); }
      /* Symmetric checkbox label for self-N:M. Element-qualified to
         outweigh Milligram's bare `label { display: block; font-weight: 700; margin-bottom: .5rem; }`
         — without that override the label would break onto its own line
         AND render bold, breaking vertical alignment with the cardinality
         pills next to it. Matches the cardinality label's italic +
         muted treatment. */
      label.se-cardinality-symmetric {
        display: inline-flex; align-items: center; gap: .25rem;
        margin: 0 0 0 .75rem;
        font-weight: 400; font-style: italic;
        color: var(--brown-light);
        cursor: pointer;
      }
      label.se-cardinality-symmetric input[type="checkbox"] { margin: 0; }
      .se-validator-chip.se-choice-add { background: transparent; color: var(--brown-light, #a1a1aa); border: 1px dashed var(--brown-border); cursor: text; padding: 0 calc(.6rem - 1px); }
      .se-validator-chip.se-choice-add:hover, .se-validator-chip.se-choice-add:focus { color: var(--brown); border-color: var(--brown); }
      .se-validator-chip.se-choice-edit { background: var(--cream-2); color: var(--brown-darker); border: 1px solid var(--brown-border); padding: 0 calc(.6rem - 1px); }
      .se-validator-pick {
        margin: 0; height: 100%; padding: 0 1.6rem 0 .25rem;
        font: inherit; line-height: 1;
        border: 0; background-color: transparent; color: inherit;
        /* Milligram paints a purple chevron via background-image on
           every <select> — suppress it here so we only see the
           browser's native caret. */
        background-image: none;
      }
      .se-choice-row { display: flex; flex-wrap: wrap; gap: .35rem; padding-left: 1.6rem; }
      /* Generic chip row — no padding-left because non-schema-editor
         contexts (e.g. the import modal) don't need the rail-style
         indent the schema editor uses for nested chip lists. */
      .chip-row { display: flex; flex-wrap: wrap; gap: .35rem; align-items: center; }
      .se-choice-chip, .chip {
        display: inline-flex; align-items: center; gap: .25rem;
        /* Saved chips read as near-black so they visually separate
           from new in-session chips. */
        background: #1f1d1c; color: #fff; border-radius: 999px;
        /* Fixed height + box-sizing so display, edit, and `+ choice`
           chip variants line up at the same height. Text size matches
           the column-name input so chips and inputs share legibility. */
        height: 1.8rem; box-sizing: border-box;
        padding: 0 .6rem; font: inherit; line-height: 1; outline: none;
        user-select: none;
      }
      /* New chips (no origValue — staged this session, not yet saved)
         use a dark grey so the user can tell at a glance which chips
         the next SAVE SCHEMA will create. The text stays full white
         (overriding the .se-row-dirty fade) for readability. */
      .se-choice-chip.se-choice-new { background: #7a7470; }
      .se-choice-chip.se-choice-new.se-row-dirty .se-choice-text { color: #fff; }
      .se-choice-chip:focus { box-shadow: 0 0 0 2px var(--brown-border); }
      .se-choice-x {
        margin: 0 0 0 .15rem; padding: 0 .25rem; background: transparent;
        border: 0; color: inherit; font-size: 1rem; line-height: 1; cursor: pointer;
        border-radius: 999px;
      }
      .se-choice-x:hover { background: rgba(255,255,255,.18); }
      .se-choice-deleted { background: rgba(120, 90, 60, .35); }
      .se-choice-deleted .se-choice-text { text-decoration: line-through; }
      /* The edit variant has a border, so trim the padding by 1px on each
         side so its outer height still matches the display / add chips. */
      .se-choice-edit { background: var(--cream-2); color: var(--brown-darker); border: 1px solid var(--brown-border); padding: 0 calc(.5rem - 1px); }
      /* Tag-qualified so we beat Milligram's `input[type="text"], input:not([type])`
         defaults (which would otherwise push padding/height to its 3.8rem baseline). */
      input.se-choice-chip-input, input.chip-input { margin: 0; height: 100%; padding: 0 .15rem; font: inherit; border: 0; background: transparent; color: inherit; min-width: 6rem; line-height: 1; }
      /* Milligram paints a purple focus border + outline on every text
         input by default. Inside a chip the chip itself carries the
         visual border, so the input's focus chrome doubles up — kill it. */
      input.se-choice-chip-input:focus, input.chip-input:focus {
        outline: 0; border: 0; box-shadow: none;
      }
      .se-choice-add, .chip-add { background: transparent; color: var(--brown-light, #a1a1aa); border: 1px dashed var(--brown-border); cursor: text; padding: 0 calc(.5rem - 1px); }
      .se-choice-add:hover, .se-choice-add:focus,
      .chip-add:hover, .chip-add:focus { color: var(--brown); border-color: var(--brown); }
      /* Small × button for removing a chip — matches the schema
         editor's .se-choice-x sizing. */
      .chip-x {
        margin: 0 0 0 .15rem; padding: 0 .25rem; background: transparent;
        border: 0; color: inherit; font-size: 1rem; line-height: 1;
        cursor: pointer; border-radius: 999px;
      }
      .chip-x:hover { background: rgba(255, 255, 255, .18); }
      /* Dirty tint applies to chip text the same way as to other rows. */
      .se-choice-chip.se-row-dirty .se-choice-text { color: rgba(255, 255, 255, .65); }

      .choose-list { display: flex; flex-direction: column; gap: .35rem; margin-top: .75rem; }
      /* Override Milligram's `label { font-size: 1.6rem; font-weight: 700 }`
         so the radio-row labels match the body text of the dialog. */
      .choose-row { display: flex; align-items: center; gap: .5rem; cursor: pointer; font: inherit; margin: 0; }
      /* Inline edit row: the name input takes the bulk of the width,
         the type select sizes to its longest option (auto width), and
         neither save/cancel buttons crowd the row (Enter / Esc do the
         commit). flex-basis ensures the input has a sensible minimum
         even when the row is narrow. */
      .se-col-edit input.se-col-name-input {
        flex: 1 1 60%; min-width: 8rem;
        margin: 0; height: 2.2rem; padding: 0 .5rem; font: inherit;
      }
      .se-col-edit select.se-col-type-select {
        flex: 0 0 auto; width: auto; min-width: 0;
        margin: 0; height: 2.2rem; padding: 0 1.6rem 0 .5rem;
      }
      /* Decimals picker on `decimal` columns. Convention C7 applies —
         the schema-editor select gets `background-image: none` so the
         browser caret isn't paired with Milligram's purple chevron,
         and `line-height: 1` plus an explicit height so the bare-
         element `select { height: 3.8rem; line-height: 1.6 }` doesn't
         balloon the row. Width pinned narrow because the dropdown
         only offers the single-digit values 1–5. */
      .se-col-edit select.se-col-decimals-select {
        margin: 0; height: 2.2rem;
        padding: 0 1.4rem 0 .5rem;
        font: inherit; line-height: 1;
        background-image: none;
        width: auto; min-width: 0;
      }
      /* Optional currency-prefix input on a decimal column. */
      .se-col-edit input.se-col-prefix-input {
        margin: 0; height: 2.2rem; width: 2.2rem;
        padding: 0 .25rem; font: inherit; line-height: 1;
        text-align: center;
      }
      /* Row buttons (× delete, ↺ undo) — sized so the glyph is
         comfortably readable rather than crammed into the chrome. */
      .se-col-delete, .se-rel-delete {
        padding: 0 .65rem; height: 2rem;
        font-size: 1.35rem; line-height: 1; font-weight: 700;
      }
      /* Dirty-row tint: a row that has any pending change relative to
         the snapshot (rename, retype, add, verb-flip) reads in a
         lighter colour so the user can see at a glance which rows
         the next SAVE SCHEMA will touch. Soft-delete (strike-through)
         takes precedence — see the more-specific .se-col-deleted /
         .se-rel-deleted rules below. */
      .se-row-dirty,
      .se-row-dirty .se-col-name-display,
      .se-row-dirty .se-col-type-display,
      .se-row-dirty .se-rel-verb-text,
      .se-row-dirty .se-rel-target { color: var(--brown-light, #a1a1aa); }
      /* Soft-deleted column row: visible-but-dormant. */
      .se-col-deleted .se-col-name-display,
      .se-col-deleted .se-col-type-display { text-decoration: line-through; color: var(--brown-light, #a1a1aa); }
      .se-col-deleted .se-col-handle { color: var(--brown-light, #a1a1aa); cursor: default; }

      .se-new-col-form, .se-new-rel-form, .se-new-table-form {
        display: flex; align-items: center; gap: .75rem; width: 100%;
      }
      .se-new-col-form input.se-col-name-input { flex: 1; margin: 0; height: 2.2rem; padding: 0 .5rem; }
      .se-new-col-form select.se-col-type-select { margin: 0; height: 2.2rem; min-width: 7rem; }
      .se-new-table-form input { flex: 1; margin: 0; height: 2.2rem; padding: 0 .5rem; }
      .se-new-rel-form select { margin: 0; height: 2.2rem; }
      .se-rel-table { font-weight: 600; color: var(--brown-darker); }
      .se-rel-row {
        display: flex; align-items: center; gap: .5rem;
        padding: .25rem .35rem; border-radius: 3px;
        flex-wrap: wrap;
        /* Tight cross-axis gap between the two clauses on edit/add
           rows so the "which …" line sits visually near "verb target
           — name", not stranded a full row below. */
        row-gap: .15rem;
      }
      /* Breathing room between an edit/add row and the following
         "+ relationship" placeholder so the user can see the second
         clause and the new-rel call-to-action as distinct things. */
      .se-rel-row.se-rel-edit { margin-bottom: .65rem; }
      /* Forced break — sits inside the flex row, takes a full row, and
         pushes the next item onto a new line. */
      .se-rel-row-break { flex-basis: 100%; height: 0; }
      /* Comma sits flush against the name input on line 1 (no extra
         left gap from the row's `gap` rule). */
      .se-rel-comma { margin-left: -.4rem; }
      .se-rel-row-break + .se-rel-conj { margin-left: 2rem; }
      .se-rel-row:hover { background: rgba(120, 90, 60, .08); }
      /* Verb dropdown matches the column-type look — neutral border,
         padded on the right for the caret, wide enough that "has many"
         fits comfortably (no clipping in the new-rel form). */
      .se-rel-verb {
        margin: 0; height: 2rem; padding: 0 2rem 0 .65rem;
        min-width: 8.5rem; width: auto;
        /* `font: inherit` brings the body's line-height along, which
           is taller than the select's height — text drops to the
           bottom. line-height: 1 caps it. See AGENTS.md C7. */
        font: inherit; line-height: 1;
      }
      /* Extra left margin so the target table name doesn't sit
         crammed against the verb — reads as "<verb>   <table>". */
      .se-rel-target { font-weight: 600; color: var(--brown-darker); flex: 0 0 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-left: .5rem; }
      /* Size to the longest option rather than stretching to fill
         the form row. width: auto overrides Milligram's `width: 100%`
         default on selects; background-image: none kills the
         Milligram chevron (see AGENTS.md C7). */
      .se-rel-target-select {
        margin: 0; height: 2rem;
        width: auto; min-width: 0;
        padding: 0 1.8rem 0 .55rem;
        background-image: none;
        font: inherit; line-height: 1;
      }
      .se-rel-conj { color: var(--brown); font-style: italic; }
      .se-rel-dash { color: var(--brown-light); user-select: none; margin: 0 .15rem; }
      .se-rel-name-text {
        font-family: "Fira Code", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
        font-weight: 400; color: var(--brown-darker);
      }
      /* Element-qualified to outweigh Milligram's bare `input[type=...]`
         rules (see AGENTS.md C6) — without it Milligram's 3.8rem
         default height wins and the input grows taller than the
         adjacent selects. */
      input.se-rel-name-input {
        margin: 0; padding: 0 .55rem;
        height: 2rem; min-height: 2rem; max-height: 2rem;
        line-height: 1; font: inherit;
        box-sizing: border-box;
        background: var(--surface); border: 1px solid var(--brown-border);
        border-radius: 3px;
        width: 12rem; min-width: 4rem; max-width: 18rem;
        color: var(--text);
      }
      /* Half-size placeholder — the input is optional and visually less
         important than the verb / target selects sitting beside it.
         Once the user focuses or types, their text renders at the full
         inherited size. */
      input.se-rel-name-input::placeholder {
        font-size: 50%;
      }
      .se-rel-name-input:hover { border-color: var(--brown); }
      .se-rel-name-input:focus { outline: 0; border-color: var(--brown-darker); }
      .se-rel-name-input.se-rel-name-warn {
        border-color: var(--danger); background: var(--danger-bg);
      }
      .se-rel-name-warn-icon {
        color: var(--danger); font-weight: 600; cursor: help;
        margin: 0 .15rem;
      }
      .se-rel-pending { color: var(--brown); font-size: .85rem; font-style: italic; }
      .se-rel-checking { color: var(--brown-light, #a1a1aa); font-size: .9rem; }
      /* Inline warning chip rendered when a verb change's convert
         dry-run came back refused. Title carries the full server
         message; the chip itself stays compact so the row layout
         doesn't jump. */
      .se-rel-warning {
        background: var(--danger-bg); color: var(--danger-ink, #991b1b);
        border: 1px solid var(--danger); border-radius: 4px;
        padding: .05rem .45rem; font-size: .85rem; font-weight: 600;
        cursor: help; white-space: nowrap;
      }
      /* `-soft` variant: non-blocking warning (data may be lost on
         save). SAVE SCHEMA still proceeds; the chip is amber instead
         of red so the user can tell at a glance which chips refuse
         vs. just warn. */
      .se-rel-warning.se-rel-warning-soft {
        background: var(--notice-bg); color: var(--notice-ink);
        border-color: var(--notice-border);
      }
      /* FK conversion pre-flight "all values resolve" confirmation chip —
         green counterpart to the amber/red warning chips above. */
      .se-fk-ok {
        color: var(--ok-ink, #166534); font-size: .85rem; font-weight: 600;
        white-space: nowrap; cursor: help;
      }
      /* (.se-rel-delete sized together with .se-col-delete above.) */
      /* Soft-deleted relationship row: keeps its place in the list
         with a strike-through so the user can see (and undo) the
         pending delete before SAVE SCHEMA. */
      .se-rel-deleted .se-rel-verb-text,
      .se-rel-deleted .se-rel-target { text-decoration: line-through; color: var(--brown-light, #a1a1aa); }
      .se-rel-deleted .se-rel-pending { color: var(--brown-light, #a1a1aa); }

      /* ── Settings → Automations (Data Actions engine) ─────────────
         Mirrors the validators/permissions settings tabs. The form
         fields stack a small caption over each control. `.auto-field`
         is a <label>, so it must be element-qualified to beat
         Milligram's `label { display:block; font-weight:700 }` (C8). */
      label.auto-field {
        display: flex; flex-direction: column; gap: .25rem;
        margin: 0 0 .85rem; font-weight: 400;
      }
      .auto-field-label { font-size: 1.2rem; font-weight: 600; color: var(--brown-darker); }
      /* C7 + C9: suppress Milligram's purple chevron (background-image)
         so it doesn't pair with the native caret, give an explicit
         line-height:1 and a height matched to the form so the inherited
         body line-height doesn't bottom-align the text or balloon the
         control past its siblings. */
      select.auto-select {
        margin: 0; height: 2.8rem; padding: 0 1.6rem 0 .5rem;
        font: inherit; line-height: 1;
        background-image: none;
        width: auto; min-width: 12rem;
      }
      /* Monospace JSON editors for the condition + config fields. */
      textarea.auto-code {
        margin: 0; width: 100%; padding: .45rem .55rem;
        font-family: var(--mono); font-size: 1.2rem; line-height: 1.45;
      }
      .automation-header { display: flex; justify-content: space-between; align-items: flex-start; gap: 1rem; flex-wrap: wrap; }
      .automation-header .actions { display: flex; gap: .35rem; flex-wrap: wrap; }
      .automation-cond { display: inline-block; margin: .5rem 0; font-size: 1.15rem; color: var(--brown-light); }
      .automation-test {
        margin: .5rem 0 0; padding: .55rem .65rem; overflow: auto;
        background: var(--cream); border: 1px solid var(--brown-border);
        border-radius: 4px; font-family: var(--mono); font-size: 1.1rem; line-height: 1.4;
      }
      table.automation-runs { width: 100%; font-size: 1.2rem; border-collapse: collapse; }
      table.automation-runs th { text-align: left; color: var(--brown-light); font-weight: 600; }
      table.automation-runs th, table.automation-runs td { padding: .25rem .5rem; border-bottom: 1px solid var(--brown-border); vertical-align: top; }
      .auto-status { font-size: 1.2rem; padding: 0 .4rem; }
      .auto-status.ok { color: var(--ok-ink); }
      .auto-status.err { color: var(--danger); }

      /* ── Settings → Pages editor ──────────────────────────────────── */
      input.page-slug-input { margin: 0; height: 2.8rem; padding: 0 .5rem; font: inherit; }
      .page-preview-wrap { display: flex; flex-direction: column; gap: .25rem; margin: .85rem 0; }
      /* Renders the page as a visitor sees it (the publication's custom CSS is
         not applied in this neutral preview — it shows structure/content). */
      .page-preview {
        border: 1px solid var(--brown-border); border-radius: 4px;
        padding: 1rem 1.25rem; background: var(--cream); overflow: auto;
      }
      .page-preview h1 { font-size: 2rem; margin: 0 0 .5rem; }
      .page-preview h2 { font-size: 1.6rem; margin: 1rem 0 .4rem; }
      .page-preview h3, .page-preview h4 { font-size: 1.35rem; margin: .85rem 0 .35rem; }
      .page-preview p { margin: 0 0 .6rem; }
      .page-preview ul { margin: 0 0 .6rem 1.25rem; }

      /* ── Publish modal: customization section ─────────────────────── */
      .publish-css-block { display: flex; flex-direction: column; gap: .35rem; margin: .35rem 0 .75rem; }
      .publish-css-label { font-size: 1.2rem; color: var(--brown-light); }
      textarea.publish-css-input {
        margin: 0; width: 100%; padding: .45rem .55rem;
        font-family: var(--mono); font-size: 1.2rem; line-height: 1.45;
      }
      /* C7/C9: the success-page select reuses .auto-select's chevron/line
         suppression; pin a height matching the row. */
      select.publish-success-select { margin: 0; min-width: 14rem; }
      .publish-success-row { display: flex; align-items: center; gap: .5rem; }

      /* ── Settings → Styles: CSS editor with a line-number gutter ──── */
      .style-code-editor {
        display: flex; align-items: stretch; width: 100%;
        font-family: var(--mono); font-size: 1.3rem; line-height: 1.5;
        font-variant-ligatures: none; -webkit-font-feature-settings: "liga" 0, "calt" 0; font-feature-settings: "liga" 0, "calt" 0;
        border: 1px solid var(--brown-border); border-radius: 4px;
        overflow: hidden; background: var(--surface);
      }
      .style-code-editor .code-gutter {
        flex: 0 0 auto; overflow: hidden;
        background: var(--cream-2); color: var(--ink-mute);
        border-right: 1px solid var(--brown-border);
        text-align: right; user-select: none;
      }
      .style-code-editor .code-gutter-inner { padding: .8rem .5rem; margin: 0; white-space: pre; font: inherit; min-width: 2ch; }
      .style-code-editor .code-area { position: relative; flex: 1 1 auto; min-width: 0; }
      .style-code-editor .code-overlay, .style-code-editor .code-input {
        margin: 0; padding: .8rem; font: inherit; box-sizing: border-box;
        white-space: pre; word-break: normal; overflow-wrap: normal;
        tab-size: 2; -moz-tab-size: 2; letter-spacing: 0; border: 0;
      }
      .style-code-editor .code-overlay { position: absolute; inset: 0; background: transparent; color: var(--text); pointer-events: none; overflow: hidden; }
      .style-code-editor .code-overlay code { font: inherit; display: block; white-space: pre; background: transparent; color: inherit; }
      .style-code-editor .code-input {
        position: relative; z-index: 1; width: 100%; display: block;
        background: transparent; color: transparent; caret-color: var(--text);
        resize: none; outline: none; overflow: auto;
      }
      .style-code-editor .code-input::selection { background: rgba(87,83,78,.25); color: transparent; }
      .style-code-editor.readonly .code-input { cursor: default; }

      /* ── Public surface: in-system content page (e.g. form success) ── */
      .pf-page { max-width: 42rem; margin: 2.5rem auto; padding: 0 1.25rem; line-height: 1.5; }
      .pf-page h1 { margin: 0 0 .75rem; }
      .pf-page p { margin: 0 0 .75rem; }
      .pf-page ul { margin: 0 0 .75rem 1.25rem; }

      /* === DEMO SYSTEM CSS (delete this block + frontend/demo/ + the
         demo block in app.js to fully remove the demo feature) === */
      /* Anchored to the right of the page logo by frontend/demo/index.js,
         which sets top/left inline; the transform vertically centres the pill
         on the logo. Sized to sit as a sibling of the brand rather than dwarf
         it. */
      /* Shared "look" of the demo button — the page launcher (.demo-launch,
         fixed near the logo) and the inline copy in the support modal
         (.demo-launch-inline) render identically; only the positioning
         differs (see the .demo-launch rule below). */
      .demo-launch, .demo-launch-inline {
        align-items: center; justify-content: center;
        height: auto; min-width: 0; margin: 0; padding: .55rem 1.35rem;
        border: 0; border-radius: 999px;
        background: linear-gradient(135deg, #3b7bf6 0%, #1b4fd0 100%);
        color: #fff; font-family: Inter, sans-serif; font-size: 1.4rem;
        font-weight: 800; line-height: 1; letter-spacing: .1em; text-indent: .1em;
        text-transform: uppercase; cursor: pointer;
        text-shadow: 0 1px 2px rgba(0,10,40,.45);
        animation: demo-pulse 2.4s ease-in-out infinite;
      }
      .demo-launch {
        /* Above page chrome but BELOW modals (`.modal-back` is z-index 99) so
           an open dialog covers the launcher instead of it floating on top.
           The running-demo overlays (shield / cursor) use their own much
           higher z-indices, so this doesn't affect a demo in flight. */
        position: fixed; top: 1.75rem; left: 10rem; z-index: 50;
        transform: translateY(-50%);
        display: flex;
      }
      /* Inline variant sits in a sentence in the support modal — align it to
         the surrounding text baseline rather than floating it fixed, and size
         the DEMO text to match the body copy (override the launcher's 1.4rem)
         with proportionally tighter padding so it reads as an inline pill. */
      .demo-launch-inline {
        display: inline-flex; vertical-align: middle;
        position: relative; top: -.1em;
        font-size: .85em; padding: .15rem .55rem;
      }
      /* `display: flex` above is an author rule, so it overrides the UA
         `[hidden] { display: none }` — meaning `launchEl.hidden = true` alone
         wouldn't hide the button (it'd flash top-left before the logo renders).
         This more-specific rule lets the hidden attribute actually win. */
      .demo-launch[hidden] { display: none; }
      .demo-launch:hover, .demo-launch-inline:hover { filter: brightness(1.07); }
      /* The launcher sits to the right of the logo and would overlap the first
         settings tab — push the tabs over by the launcher's measured footprint
         (--demo-launch-clear, set by frontend/demo/index.js) when it's shown. */
      body.on-settings.has-demo-launch .page-header > .button:first-of-type {
        margin-left: var(--demo-launch-clear, 0px);
      }
      .demo-launch:disabled { opacity: .5; cursor: default; animation: none; }
      @keyframes demo-pulse {
        0%, 100% { box-shadow: 0 3px 10px rgba(0,20,60,.35), 0 0 14px 3px rgba(90,150,245,.6), 0 0 0 0 rgba(90,150,245,.55); }
        50%      { box-shadow: 0 3px 10px rgba(0,20,60,.35), 0 0 28px 10px rgba(90,150,245,.85), 0 0 0 12px rgba(90,150,245,0); }
      }
      .demo-menu {
        position: fixed; top: 4.5rem; left: 1.25rem; z-index: 2147483000;
        min-width: 16rem; padding: .5rem; border-radius: 10px;
        background: var(--surface); color: var(--text);
        box-shadow: 0 8px 30px rgba(0,0,0,.25);
      }
      .demo-menu-item {
        display: block; width: 100%; text-align: left; padding: .55rem .6rem;
        border: 0; background: transparent; color: inherit; cursor: pointer;
        border-radius: 6px; font: 500 .95rem/1.3 Inter, sans-serif;
      }
      .demo-menu-item:not(:disabled):hover { background: rgba(80,140,235,.22); color: var(--text); }
      .demo-menu-item:disabled { opacity: .4; cursor: default; }
      /* The support / welcome modal is wider than a stock confirm dialog so
         its two-column feature list has room without wrapping each item. */
      .support-modal .modal-confirm { min-width: 560px; max-width: 640px; }
      /* Two-column feature list in the support / welcome modal. */
      .support-features { columns: 2; column-gap: 2rem; }
      /* Slightly muted so the list reads as secondary detail under the prompt. */
      .support-features li { break-inside: avoid; color: var(--ink-mute, #8a7a63); }
      /* Divider between the demos and the SUPPORT entry below them. */
      .demo-menu-sep { height: 1px; background: var(--brown-border); margin: .4rem .3rem; }
      .demo-menu-support { text-align: right; font-weight: 700; letter-spacing: .04em; }
      .demo-shield {
        position: fixed; inset: 0; z-index: 2147483100; cursor: none;
        background: transparent;
      }
      /* Floating clone of the tile being dragged (synthetic DnD has no native
         drag image). Rides under the cursor; box-sizing + margin reset so the
         clone keeps the source's size without its layout margins. */
      .demo-drag-ghost {
        position: fixed; top: 0; left: 0; margin: 0; box-sizing: border-box;
        z-index: 2147483250; pointer-events: none; opacity: .9;
        box-shadow: 0 10px 26px rgba(0,0,0,.4);
        transition: transform 700ms cubic-bezier(.33,0,.2,1);
        will-change: transform;
      }
      .demo-running { cursor: none; }
      .demo-cursor {
        position: fixed; top: 0; left: 0; z-index: 2147483300;
        pointer-events: none; line-height: 0;
        filter: drop-shadow(1px 3px 3px rgba(0,0,0,.45));
        transition: transform 700ms cubic-bezier(.33,0,.2,1);
        will-change: transform;
      }
      .demo-cursor svg { display: block; transition: transform 120ms ease; transform-origin: 4px 4px; }
      .demo-cursor-press svg { transform: scale(.82); }
      /* Modifier keycap shown beside the pointer while a key is "held" during a
         gesture (e.g. SHIFT on a shift-click range select). Sits to the lower-
         right of the arrow tip (which is at the cursor element's 0,0). */
      .demo-key-badge {
        position: absolute; left: 32px; top: 36px;
        padding: .3rem .65rem; border-radius: 7px;
        background: #1a1a1a; color: #fff; border: 2px solid #fff;
        font: 700 1.15rem/1 var(--mono, ui-monospace, monospace); letter-spacing: .07em;
        white-space: nowrap; box-shadow: 0 3px 10px rgba(0,0,0,.5);
      }
      .demo-key-badge[hidden] { display: none; }
      .demo-toast {
        position: fixed; left: 50%; bottom: 1.75rem; transform: translateX(-50%);
        z-index: 2147483300; max-width: 92vw; padding: .85rem 1.5rem; border-radius: 10px;
        background: var(--surface); color: var(--text); font: 600 1.5rem/1.3 Inter, sans-serif;
        box-shadow: 0 6px 24px rgba(0,0,0,.25);
      }
      /* Faint blue end-card, matching the demo menu's accent (a translucent
         blue laid over the opaque surface). Theme-aware `--text`/`--surface`
         keep it readable in both light and dark themes — unlike the old
         white-on-`--brown`, which became white-on-light-grey in dark mode. */
      .demo-toast-handoff {
        background: linear-gradient(rgba(80,140,235,.22), rgba(80,140,235,.22)), var(--surface);
        color: var(--text);
      }
      .demo-toast-error { background: var(--danger, #b00); color: #fff; }
      /* === END DEMO SYSTEM CSS === */

      /* ============================================================
         Small-screen / mobile adjustments. Placed LAST so they win over
         the per-modal desktop widths defined earlier (equal specificity,
         later source order).
         ============================================================ */
      @media (max-width: 720px) {
        /* The welcome / support pitch drops from two columns to one so each
           feature reads on its own line without a cramped column split. */
        .support-features { columns: 1; }

        /* Modals must stay fully reachable on a phone. Two desktop assumptions
           break on a narrow screen:
             • Per-modal `min-width` (480–640px) is wider than many phones,
               which pushed the header's title buttons (close / save) off the
               right edge.
             • `.modal-back` vertically CENTERS the dialog, so a modal taller
               than the viewport has its header — and those buttons — pushed up
               off the top of the screen, out of reach.
           Pin modals to the top, clamp them to the viewport, and stick the
           header (and footer) so their action buttons stay reachable while only
           the body scrolls. */
        .modal-back { align-items: flex-start; }
        /* Use the two-class `.modal-back .modal` (not a bare `.modal`) so this
           clamp BEATS every per-modal width rule, including the two-class ones
           like `.support-modal .modal-confirm` and `.modal.modal-picker` that
           would otherwise keep their wide desktop `min-width` and overflow the
           phone. Being last in source, it also wins the equal-specificity ties. */
        .modal-back .modal {
          min-width: 0; max-width: 100vw; width: 100%;
          max-height: 100dvh; border-radius: 0;
        }
        .modal-header { position: sticky; top: 0; z-index: 4; flex-wrap: wrap; row-gap: .4rem; }
        .modal-footer { position: sticky; bottom: 0; z-index: 4; flex-wrap: wrap; row-gap: .4rem; }

        /* Lock the page behind an open modal so a touch-scroll acts on the
           modal's OWN scroll area, not the document underneath. Without this
           the scroll "chains" through to the page beneath (the reported
           new-view-modal bug). `.modal-back` is appended to <body> by every
           modal, so `:has()` flips the lock on whenever any modal is up. */
        body:has(.modal-back) { overflow: hidden; }
        /* Belt-and-braces: keep scroll momentum from escaping a modal's scroll
           container out to the page once it hits the top/bottom. Applied to the
           known scroll regions across the modal family. */
        .modal-body, .g2-body, .se-pane, .se-pane-left, .se-pane-right,
        .link-body, .confirm-body, .related-overlay-body {
          overscroll-behavior: contain;
        }

        /* Import modal: full-width on a phone. Its dense column-mapping grids
           can't usefully squeeze into ~360px, so let them keep a workable
           minimum width and scroll horizontally inside the body rather than
           crushing every input into illegibility. */
        .modal-import { min-width: 0; width: 100vw; max-width: 100vw; }
        .imp-col-list, .imp-map-row { min-width: 540px; }

        /* Shrink the cog icon to match the theme toggle's sun/moon SVG so both
           sit in their 2rem bubbles at the same scale. This must live HERE (the
           end of the stylesheet) rather than in the earlier pill media query,
           because the base `.cog .icon-cog { 1.8rem }` rule comes later in
           source order than that block and would otherwise win. */
        .cog .icon-cog { width: 1.1rem; height: 1.1rem; }

        /* On a phone the DEMO launcher matches the local-code / storage pills:
           same 2rem height, pill radius, and quiet styling (no attention-pulse).
           It locks to the top row of the screen beside the logo. frontend/demo/
           index.js positions it by the logo (inline `left`), but it also sets an
           inline `top` centred on the logo with a translateY(-50%) — override
           both with `top: .5rem` / `transform: none` so it sits in the same top
           row as the right-hand pill cluster (which is `top: .5rem`). The
           attention-pulse from the base rule is kept so it still draws the eye.
           `position: absolute` (vs the desktop `fixed`) matches the pill
           cluster: the pill scrolls UP off the page with the content instead of
           staying frozen to the viewport top. A fixed `left: 8rem` pins it the
           same distance from the screen's left edge on every page (overriding
           the logo-relative inline `left` set by frontend/demo/index.js). */
        .demo-launch {
          position: absolute;
          left: 8rem !important;
          top: .5rem !important;
          transform: none !important;
          height: 2rem; padding: 0 .6rem;
          font-size: .85rem; font-weight: 700; letter-spacing: .04em;
          border-radius: 1rem;
        }
      }
    