* { margin: 0; padding: 0; box-sizing: border-box; }

/* Form controls do NOT inherit font-family from their ancestors per the
   UA stylesheet — without this, every <button>/<input>/<select>/<textarea>
   that doesn't explicitly set a family (the sidebar nav items, auth tabs,
   viewer buttons, etc.) falls back to the OS font (Segoe UI on Windows)
   instead of --font-sans, so those controls never picked up the platform
   typeface and looked "unchanged" against the rest of the UI. Inheriting
   the family here makes the whole app render in one font; components that
   want monospace still override with var(--font-mono), which wins. */
button, input, select, textarea, optgroup { font-family: inherit; }

/* ── Theme tokens ─────────────────────────────────────────────────
   All colors, radii, shadows, fonts, and motion timings are defined
   here. Components reference these tokens, never raw hex values, so
   the entire visual identity can be retuned in one place. */
:root {
    /* Brand / accent — deep scientific blue. Not teal, not indigo, not neon. */
    --accent:        #1f5f8b;
    --accent-strong: #16496d;
    --accent-soft:   #e8f2f8;   /* hover tint, focus ring background */
    --accent-muted:  #d2e5ef;   /* selected/active background — clearly visible */
    --accent-fg:     #ffffff;
    --accent-ring:   rgba(31, 95, 139, 0.18);

    /* ── Role-coloured tokens (the "hierarchy" layer) ─────────────────
       The accent family above is the BRAND palette. The tokens below
       give each functional role its own surface so adjacent UI states
       remain visually distinguishable instead of all reading as
       "blue tint". The intent:
         · neutral hover  → very low-contrast wash; says "interactive"
                            without competing with selection / active.
         · selection      → a stronger blue than --accent-soft so a
                            selected row reads unambiguously selected
                            even when it sits inside a column-group
                            band that also tints accent-blue.
         · group band     → slate-leaning blue, lower saturation than
                            --accent-soft. A group header reads as a
                            "structural region" rather than a
                            "selection".
         · sheet-tab active → no separate token — uses --accent-muted
                              + a strong --accent border-bottom strip
                              applied via a CSS rule, so the active
                              tab is unmistakable across density and
                              theme variations.
       Each token below has a comment documenting the role it owns. */

    /* Neutral hover — for surfaces that need a subtle "you can click
       here" cue without claiming "selected". Used by tab-menu /
       header-menu / context-menu items, the sidebar workspace exit,
       and other one-shot button-likes. Distinct from --accent-soft
       (which is now reserved for ACTIVE / SELECTED states only). */
    --hover-bg: #eef1f5;
    --hover-fg: var(--text-strong);

    /* Row / cell selection — slightly stronger than --accent-soft so
       a selected row's tint is unambiguously "chosen", not the same
       wash as a hover. The frozen / amber row tint sits in a separate
       hue family altogether (see --frozen-bg below) so the three
       row states (hover, selected, frozen) are visually distinct. */
    --row-selected-bg:     #d2e0f0;
    --row-selected-border: #a3bdda;

    /* Column-group band — slate-leaning blue used by the group
       header band over grouped columns. Same hue family as --accent
       but lower saturation, so a group band reads as a structural
       label rather than as the same "selection / active" tint that
       --accent-soft now signals. The accent-strong text colour stays
       legible against this background. */
    --group-bg:     #d8e3ee;
    --group-bg-hover: #c8d6e6;
    --group-border: #92aac4;
    --group-fg:     #15324d;

    /* Surfaces & background — cooler page bg so white cards read as panels */
    --bg:            #eff2f7;
    --surface:       #ffffff;
    --surface-soft:  #f7f9fc;
    --surface-sunk:  #eef2f6;

    /* Text */
    --text-strong:   #0f172a;
    --text:          #334155;
    --text-muted:    #64748b;
    --text-faint:    #94a3b8;

    /* Borders — slightly stronger so cards have a defined edge */
    --border:        #dde2e9;
    --border-strong: #c8d0db;
    --border-faint:  #e8ecf2;

    /* Status — badge variants (background / foreground / border).
       The semantic-state tokens below build on these so a button
       and a badge of the same status share the same hue family. */
    --success-bg: #ebf8f0; --success-fg: #1a6b3c; --success-border: #9ae6b4;
    --warning-bg: #fef9e7; --warning-fg: #7d5a00; --warning-border: #f6d860;
    --danger-bg:  #fff5f5; --danger-fg:  #9b2c2c; --danger-border:  #fc8181;
    --info-bg:    #e8f2f8; --info-fg:    #1f5f8b; --info-border:    #c4dbe9;

    /* Semantic interactive states — same naming pattern as the accent
       family ({name}, {name}-soft, {name}-strong, {name}-ring) so any
       button/chip can swap between "informational", "positive",
       "warning", or "destructive" by changing one token prefix. */
    --success:        #1a6b3c;     /* same as --success-fg */
    --success-soft:   #ebf8f0;     /* same as --success-bg */
    --success-strong: #0f5132;
    --success-ring:   rgba(26, 107, 60, 0.20);

    --warning:        #b97c00;     /* mid-amber for filled active states */
    --warning-soft:   #fef9e7;     /* same as --warning-bg */
    --warning-strong: #7d5a00;     /* same as --warning-fg */
    --warning-ring:   rgba(185, 124, 0, 0.22);

    --danger:         #dc2626;
    --danger-soft:    #fff5f5;     /* same as --danger-bg */
    --danger-strong:  #b91c1c;
    --danger-ring:    rgba(220, 38, 38, 0.20);

    /* Frozen-row tint — warm sand, kept clearly distinct from
       --accent (cool blue) and --warning (amber). Applied only to
       pinned rows. */
    --frozen-bg:        #fdf6e3;
    --frozen-bg-hover:  #f9eecf;
    --frozen-border:    #e9d8a6;
    --frozen-pin:       #c9a000;

    /* Category chip — purple, distinct from accent/warning families
       so the "category" filter type doesn't read as informational
       or as a warning. */
    --category:        #6b46c1;
    --category-soft:   #f3e8ff;
    --category-border: #ddd6fe;

    /* Radii — a tight, enterprise-leaning scale. Small controls 6px, buttons/
       cards 8px, modals/large surfaces capped at 12px (was 10/14; reduced so
       the UI reads as a serious commercial tool, not a consumer app). */
    --radius-sm: 6px;
    --radius-md: 8px;
    --radius-lg: 12px;

    /* Shadows — present but restrained; cards have depth without looking
       generic. Modal shadow tightened (was 0 12px 40px /.18) so dialogs read
       crisp rather than floating in a soft glow. */
    --shadow-1: 0 1px 2px rgba(15, 23, 42, 0.04), 0 1px 1px rgba(15, 23, 42, 0.03);
    --shadow-2: 0 1px 3px rgba(15, 23, 42, 0.06), 0 4px 12px rgba(15, 23, 42, 0.05);
    --shadow-3: 0 4px 18px rgba(15, 23, 42, 0.08);
    --shadow-modal: 0 10px 30px rgba(15, 23, 42, 0.14), 0 2px 6px rgba(15, 23, 42, 0.08);

    /* Typography */
    --font-sans: 'Source Sans 3', 'Source Sans Pro', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
    --font-mono: 'JetBrains Mono', 'IBM Plex Mono', ui-monospace, SFMono-Regular,
                 Menlo, Monaco, Consolas, monospace;

    /* Motion */
    --motion-fast: 120ms;
    --motion-base: 180ms;
    --ease: cubic-bezier(0.32, 0.72, 0, 1);

    /* Top command band — the unified height of the sidebar brand block
       and the Studio toolbar so they form one continuous horizontal
       band across the screen. The four boxes that share this height
       (Platform sidebar header, Studio sidebar brand, Studio toolbar,
       and the responsive collapsed-sidebar header) all reference this
       token; if the band height ever changes, it changes here. */
    --topband-h: 60px;
}

/* Rem base — 18px. Source Sans 3 has a slightly smaller x-height than the
   previous typeface, so at the old 17px the UI read a touch small. Bumping
   the rem base one px restores comfortable reading size across the whole app
   at once (body, buttons, form controls, sidebar, cards, table/grid cells,
   dropdowns, modals, alerts are all rem-based, so they scale together).
   Fixed-px chrome — sidebar/column widths, icons, borders, and the px-based
   grid row heights/paddings — does NOT scale, so the layout stays compact and
   data-dense; only the type grows. Keep this as the single global lever rather
   than re-tuning individual components. */
html { font-size: 18px; }

/* ── Studio theme variants ────────────────────────────────────────
   The :root tokens above define the DEFAULT ("Studio Blue") theme.
   The blocks below override only the BRAND / ROLE tokens that carry
   each theme's identity — accent family, hover, group palette,
   row-selected, focus rings, info hue, plus a SUBTLE per-theme tint
   on the column-header surface (--surface-soft) and the page bg
   (--bg) so the column-header band and page canvas pick up a quiet
   theme tone without losing the white-data-area / premium feel.

   What is INTENTIONALLY NOT themed (so it reads consistently across
   every theme):
     · Status colors (--success / --warning / --danger)
     · Frozen-row amber (--frozen-bg / --frozen-border / --frozen-pin)
     · Category purple (--category)
     · Body & data cell surfaces (--surface)
     · Text colors (--text / --text-strong / --text-muted / --text-faint)
   Icon colors are NOT touched — they continue to read from text /
   text-muted / text-strong (neutral) so a theme switch never
   recolors decorative or directional icons across the chrome.

   Themes are applied by setting `data-theme="<id>"` on .studio-shell
   (the studio root). The same data-theme attribute is also set on
   each preview-card's `.studio-theme-preview` block so each card's
   swatches paint in THAT theme's palette regardless of which theme
   the workspace is currently running. Both selectors below are
   listed for every theme — this is what makes the preview cards
   independent of the active theme, AND keeps the studio shell
   themed. The cascade pushes the overridden values into every
   descendant `var(--accent)` etc. without any per-rule rewrites. */

/* Slate Mono — neutral, reduced-saturation grayscale. */
.studio-shell[data-theme="slate"],
.studio-theme-preview[data-theme="slate"] {
    --accent:        #475569;
    --accent-strong: #334155;
    --accent-soft:   #eaeef3;
    --accent-muted:  #dadfe7;
    --accent-fg:     #ffffff;
    --accent-ring:   rgba(71, 85, 105, 0.20);

    --hover-bg:      #eef0f4;

    --row-selected-bg:     #d6dae3;
    --row-selected-border: #a4adba;

    --group-bg:       #e6e9ee;
    --group-bg-hover: #d3d8df;
    --group-border:   #a4adba;
    --group-fg:       #1f2937;

    --info-bg:     #eaeef3;
    --info-fg:     #475569;
    --info-border: #cdd5e0;

    /* Surface tints — kept very subtle (1-2 % shift) so themes
       feel present without making the data area look stained. */
    --surface-soft: #f3f5f8;
    --surface-sunk: #ebeef2;
    --bg:           #edeff3;
}

/* Forest — botanical green. Restrained but unambiguously green: the accent
   sits at a recognisable forest-green hue, and the soft surfaces carry a
   visible green tint that distinguishes the theme from Ocean/Slate at a
   glance — not desaturated into gray. */
.studio-shell[data-theme="forest"],
.studio-theme-preview[data-theme="forest"] {
    --accent:        #2f7558;
    --accent-strong: #1c4f3a;
    --accent-soft:   #e2efe7;
    --accent-muted:  #c1ddca;
    --accent-fg:     #ffffff;
    --accent-ring:   rgba(47, 117, 88, 0.20);

    --hover-bg:      #e8f0eb;

    --row-selected-bg:     #c2dccc;
    --row-selected-border: #82b59c;

    --group-bg:       #d0e1d6;
    --group-bg-hover: #bbd2c5;
    --group-border:   #7ea893;
    --group-fg:       #163a26;

    --info-bg:     #e2efe7;
    --info-fg:     #2f7558;
    --info-border: #b3d2bf;

    --surface-soft: #f0f5f1;
    --surface-sunk: #e7eee9;
    --bg:           #ebf1ec;
}

/* Ember — copper / warm clay. A clear copper-brown accent (between bronze
   and terracotta) with surfaces that read as warm parchment. Saturation is
   contained — the accent is unmistakable as warm/copper but never neon
   orange, and the surface tints carry just enough warmth to feel distinct
   from Slate at a glance. */
.studio-shell[data-theme="ember"],
.studio-theme-preview[data-theme="ember"] {
    --accent:        #a35a35;
    --accent-strong: #7a4023;
    --accent-soft:   #f6e8db;
    --accent-muted:  #e9cfb3;
    --accent-fg:     #ffffff;
    --accent-ring:   rgba(163, 90, 53, 0.20);

    --hover-bg:      #f3ebe2;

    --row-selected-bg:     #ead2b6;
    --row-selected-border: #c2a075;

    --group-bg:       #e7d3ba;
    --group-bg-hover: #d8c0a1;
    --group-border:   #b59373;
    --group-fg:       #4a2a14;

    --info-bg:     #f6e8db;
    --info-fg:     #a35a35;
    --info-border: #dec3a3;

    --surface-soft: #f7f0e8;
    --surface-sunk: #efe5d6;
    --bg:           #f2ebde;
}

/* Graphite — near-monochrome dark accent on a light surface. */
.studio-shell[data-theme="graphite"],
.studio-theme-preview[data-theme="graphite"] {
    --accent:        #1f2937;
    --accent-strong: #0f172a;
    --accent-soft:   #e9ecf0;
    --accent-muted:  #d2d7df;
    --accent-fg:     #ffffff;
    --accent-ring:   rgba(31, 41, 55, 0.20);

    --hover-bg:      #ecedf1;

    --row-selected-bg:     #d2d7df;
    --row-selected-border: #aab1bc;

    --group-bg:       #dee2e8;
    --group-bg-hover: #ccd2da;
    --group-border:   #a3a9b4;
    --group-fg:       #1f2937;

    --info-bg:     #e9ecf0;
    --info-fg:     #1f2937;
    --info-border: #cfd4dc;

    --surface-soft: #f1f3f6;
    --surface-sunk: #e8ebf0;
    --bg:           #eaedf1;
}

/* Ocean — analytical blue-teal. An instrument-blue accent (clearly cooler
   and more cyan than Studio Blue's slate) with surfaces that carry a
   visible blue-teal tint. Distinguishable from Forest (green) and from
   Slate (neutral) at a glance, without sliding into neon teal. */
.studio-shell[data-theme="ocean"],
.studio-theme-preview[data-theme="ocean"] {
    --accent:        #226a82;
    --accent-strong: #144b5e;
    --accent-soft:   #dbeef3;
    --accent-muted:  #b6dbe6;
    --accent-fg:     #ffffff;
    --accent-ring:   rgba(34, 106, 130, 0.20);

    --hover-bg:      #e3edf1;

    --row-selected-bg:     #b8d8e2;
    --row-selected-border: #79b0c0;

    --group-bg:       #c5dde6;
    --group-bg-hover: #afccd6;
    --group-border:   #7aa3b3;
    --group-fg:       #0e3a48;

    --info-bg:     #dbeef3;
    --info-fg:     #226a82;
    --info-border: #abd0db;

    --surface-soft: #ebf2f4;
    --surface-sunk: #dee9ed;
    --bg:           #e4eef2;
}

body {
    font-family: var(--font-sans);
    background: var(--bg);
    min-height: 100vh;
    font-size: 1rem;
    color: var(--text);
    /* Tabular numerals so figures align in tables / data grids. */
    font-feature-settings: "tnum";
}

/* Mono utility — apply to SMILES, sequences, file paths, IDs, hashes. */
.mono {
    font-family: var(--font-mono);
    font-feature-settings: "tnum";
    font-variant-numeric: tabular-nums;
}
/* ── App shell: fixed left sidebar + main content area ─────────── */
.app-shell {
    --sidebar-w: 248px;
    --sidebar-w-collapsed: 72px;
    --sidebar-bg: var(--surface);
    --sidebar-border: var(--border);
    --sidebar-fg: var(--text);
    --sidebar-active-bg: var(--accent-muted);
    --sidebar-active-fg: var(--accent-strong);
    min-height: 100vh;
    display: flex;
    background: var(--bg);
}
.app-shell.sidebar-collapsed { --sidebar-w: var(--sidebar-w-collapsed); }

/* ── Sidebar ──────────────────────────────────────────────────── */
.sidebar {
    flex: 0 0 var(--sidebar-w);
    width: var(--sidebar-w);
    position: sticky;
    top: 0;
    align-self: flex-start;
    height: 100vh;
    background: var(--sidebar-bg);
    border-right: 1px solid var(--sidebar-border);
    display: flex;
    flex-direction: column;
    /* Allow the edge-toggle to sit half-outside the right edge. */
    overflow: visible;
    /* NO width transition — collapse/expand snaps instantly (matches Alma
       Studio). `.sidebar` and the main content are flex siblings, so animating
       the flex width re-ran layout every frame and visibly shifted/resized the
       main content; snapping applies the new --sidebar-w in a SINGLE reflow.
       (The MOBILE drawer keeps its own `transition: transform` in the media
       query below — that slide is unaffected.) */
    z-index: 30;
    /* Sidebar chrome isn't selectable text — clicking/dragging nav items must
       not highlight labels. Consistent with .studio-sidebar. */
    -webkit-user-select: none;
    user-select: none;
}

.sidebar-header {
    display: flex;
    align-items: center;
    justify-content: flex-start;
    gap: 0.75rem;
    /* Locked to --topband-h so the brand block lines up exactly with
       the Studio toolbar to its right. Vertical padding is 0 — the
       flex `align-items: center` does the vertical centering, so the
       outer box height is deterministic and not affected by the
       intrinsic height of the icon or text rows. */
    height: var(--topband-h);
    min-height: var(--topband-h);
    padding: 0 1rem;
    border-bottom: 1px solid var(--sidebar-border);
}
.sidebar-logo-icon {
    /* No framed box: the AlmaAI mark sits directly in the header (no border,
       background container, or shadow) and just aligns with the brand text. */
    width: 36px; height: 36px;
    border-radius: var(--radius-md);
    display: flex; align-items: center; justify-content: center;
    flex-shrink: 0;
    overflow: hidden;
}
.sidebar-logo-icon img { width: 100%; height: 100%; object-fit: contain; display: block; }
.sidebar-logo-text { min-width: 0; overflow: hidden; }
.sidebar-logo-text h1 {
    font-size: 1.1rem; font-weight: 700; color: var(--text-strong);
    margin: 0; line-height: 1.1; white-space: nowrap;
    letter-spacing: -0.012em;
}
.sidebar-logo-text p {
    font-size: 0.68rem; color: var(--text-muted);
    text-transform: uppercase;
    /* Tighter than the previous 0.1em — the all-caps "Platform" reads
       more compact and lines up tonally with the Studio sub-label. */
    letter-spacing: 0.05em;
    margin: 0; white-space: nowrap; font-weight: 600;
}

/* Floating edge toggle — small circular control on the sidebar's right edge,
   anchored at the vertical middle so it doesn't crowd the brand area. Sits
   half-on / half-off the border. Translate handles the half-height offset. */
.sidebar-edge-toggle {
    position: absolute;
    top: 50%;
    right: -13px;       /* button is 26px; sits centered on the border line */
    transform: translateY(-50%);
    width: 26px; height: 26px;
    border-radius: 50%;
    border: 1px solid var(--border);
    background: var(--surface);
    color: var(--text-muted);
    display: flex; align-items: center; justify-content: center;
    cursor: pointer;
    z-index: 35;
    box-shadow: var(--shadow-1);
    transition: background var(--motion-fast) var(--ease),
                color var(--motion-fast) var(--ease),
                border-color var(--motion-fast) var(--ease),
                box-shadow var(--motion-fast) var(--ease);
}
.sidebar-edge-toggle:hover {
    background: var(--accent-soft);
    color: var(--accent);
    border-color: var(--accent-ring);
    box-shadow: var(--shadow-2);
}
.sidebar-edge-toggle svg { width: 14px; height: 14px; stroke: currentColor; fill: none; }

.sidebar-nav {
    flex: 1;
    display: flex; flex-direction: column;
    gap: 0.25rem;
    padding: 1.25rem 0.625rem 0.875rem;
    overflow-y: auto;
}
.sidebar-footer {
    padding: 0.625rem;
    border-top: 1px solid var(--sidebar-border);
    display: flex; flex-direction: column; gap: 0.25rem;
}
.sidebar-user {
    display: flex; align-items: center; gap: 0.625rem;
    padding: 0.6rem 0.75rem;
    color: var(--text);
    font-size: 0.9rem;
    font-weight: 500;
    border-radius: var(--radius-md);
    overflow: hidden;
}
.sidebar-user .sidebar-item-icon { color: var(--accent); }

.sidebar-item {
    display: flex; align-items: center; gap: 0.75rem;
    padding: 0.55rem 0.75rem;
    background: transparent; border: none;
    border-radius: var(--radius-md);
    color: var(--sidebar-fg);
    font-size: 0.875rem; font-weight: 500;
    text-align: left;
    cursor: pointer;
    transition: background var(--motion-fast) var(--ease),
                color var(--motion-fast) var(--ease);
    white-space: nowrap;
    overflow: hidden;
    width: 100%;
}
.sidebar-item:hover { background: var(--surface-sunk); color: var(--text-strong); }
.sidebar-item.active {
    background: var(--sidebar-active-bg);
    color: var(--sidebar-active-fg);
    font-weight: 600;
    /* Inset left accent — premium "selected" cue, clipped by border-radius. */
    box-shadow: inset 2px 0 0 var(--accent);
}
.sidebar-item.active .sidebar-item-icon { color: var(--sidebar-active-fg); }
/* In collapsed mode the strong bg already conveys active; the left edge would
   look off-center against centered icons, so suppress it. */
.app-shell.sidebar-collapsed .sidebar-item.active { box-shadow: none; }

/* Workspace entry — Alma Studio sits at the top of the nav with a slight
   visual elevation so it reads as a primary workspace, not just a tool. */
.sidebar-item-workspace {
    font-weight: 600;
    color: var(--text-strong);
}
.sidebar-item-workspace .sidebar-item-icon { color: var(--accent); }
.sidebar-item-workspace:hover { background: var(--accent-soft); color: var(--accent-strong); }
.sidebar-item-workspace:hover .sidebar-item-icon { color: var(--accent-strong); }

.sidebar-divider {
    height: 1px;
    background: var(--border-faint);
    /* Symmetric 0.5rem top/bottom — matches .studio-sidebar-divider so the
       gap between a sidebar item and the divider is identical in both
       sidebars (SuperGen previously had 0.625rem on top, reading as farther). */
    margin: 0.5rem 0.625rem;
}
/* In collapsed mode, only adjust horizontal margins so the divider sits
   nicely on the narrower rail. Vertical margins MUST match the expanded
   state — using the `margin` shorthand here previously rewrote top/bottom
   too, shifting every nav item below the divider by 2px on every toggle. */
.app-shell.sidebar-collapsed .sidebar-divider {
    margin-left: 0.75rem;
    margin-right: 0.75rem;
}
.sidebar-item-icon {
    flex-shrink: 0;
    width: 20px; height: 20px;
    display: flex; align-items: center; justify-content: center;
}
.sidebar-item-icon svg { width: 18px; height: 18px; stroke: currentColor; fill: none; }
.sidebar-item-label {
    flex: 1; min-width: 0;
    overflow: hidden; text-overflow: ellipsis;
    transition: opacity 0.18s ease;
}

.sidebar-logout { color: var(--text-muted); }
.sidebar-logout:hover { background: var(--danger-bg); color: var(--danger-strong); }

/* The sidebar width SNAPS instantly on collapse/expand (no width transition —
   see .sidebar above; matches Alma Studio). Layout-affecting properties on the
   inner elements (padding / gap / justify-content) stay constant in both
   states so the icon position never snaps. The labels collapse to zero width
   and fade (opacity) — NOT `justify-content: center; padding: 0; gap: 0`, which
   would kick the icons hard to the right. Icons stay left-anchored throughout. */
.app-shell.sidebar-collapsed .sidebar-logo-text,
.app-shell.sidebar-collapsed .sidebar-item-label {
    /* `flex: 0 0 0` + width:0 collapses the label out of the row so the
       container doesn't reserve space for it; opacity transitions
       handle the fade. */
    opacity: 0;
    flex: 0 0 0;
    width: 0;
    min-width: 0;
    margin: 0;
    padding: 0;
    overflow: hidden;
}

/* ── Main area ────────────────────────────────────────────────── */
.main-area {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
}
.main-content {
    flex: 1;
    max-width: 1800px;
    width: 100%;
    margin: 0 auto;
    padding: 2.75rem 2rem 2rem;
}

/* Mobile top bar (only visible on small screens) */
.mobile-topbar {
    display: none;
    align-items: center;
    gap: 0.75rem;
    padding: 0.5rem 0.875rem;
    background: var(--surface);
    border-bottom: 1px solid var(--border);
    position: sticky; top: 0;
    z-index: 20;
}
.mobile-menu-btn {
    width: 36px; height: 36px;
    border-radius: var(--radius-md);
    border: 1px solid var(--border);
    background: var(--surface);
    color: var(--text);
    display: flex; align-items: center; justify-content: center;
    cursor: pointer;
}
.mobile-menu-btn svg { width: 20px; height: 20px; stroke: currentColor; fill: none; }
.mobile-topbar-title { font-weight: 700; color: var(--text-strong); font-size: 0.95rem; }

/* Mobile drawer backdrop — hidden by default */
.sidebar-backdrop {
    display: none;
    position: fixed; inset: 0;
    background: rgba(15, 23, 42, 0.45);
    z-index: 25;
}

/* Internal page tabs (used by ResultsPage) — keep classic horizontal nav style */
.nav-menu { display: flex; gap: 0.375rem; flex-wrap: wrap; }
.nav-item { display: flex; align-items: center; gap: 0.4rem; padding: 0.5rem 1rem; background: transparent; border: none; border-radius: var(--radius-md); color: var(--text); font-size: 0.875rem; font-weight: 500; cursor: pointer; transition: background var(--motion-fast) var(--ease), color var(--motion-fast) var(--ease); }
.nav-item svg { width: 16px; height: 16px; stroke: currentColor; fill: none; }
.nav-item:hover { background: var(--accent-soft); color: var(--accent); }
.nav-item.active { background: var(--accent-muted); color: var(--accent-strong); font-weight: 600; }
.nav-item.active svg { stroke: var(--accent-strong); }

/* Nav / sheet-tab font-weight robustness — fixes the "unselected items look
   slightly LARGER than the selected one" regression.
   These items use font-weight 500 (Medium) when inactive and 600 (Semibold)
   when active. Since the web font loads with `display=optional` (see
   index.html), a hard refresh can fall back to a system font (e.g. Segoe UI)
   that has NO native Medium (500) face. The browser then SYNTHESISES 500 by
   faux-weighting the 400 face, which smears glyphs WIDER — so inactive items
   render larger than the active 600 items (which map to a real Semibold). The
   font SIZE is identical in every state; only this faux-weight inflates the
   inactive ones. Disabling weight synthesis makes 500 fall back to a real face
   (no inflation), so active and inactive stay the same visual size. No effect
   when Source Sans 3 IS loaded (it ships every weight). These elements never
   use italic, so `none` is safe (it won't suppress any real faux-italic). */
.sidebar-item, .nav-item, .studio-tab { font-synthesis: none; }

/* LoginPage logo helpers (kept for compatibility) */
.logo-icon { display: flex; align-items: center; justify-content: center; flex-shrink: 0; border-radius: 10px; }
.molecule-icon { width: 24px; height: 24px; color: white; display: block; }
.molecule-icon circle { fill: white; }
.molecule-icon line { stroke: white; stroke-width: 2; }
.page-header { margin-bottom: 2rem; text-align: center; }
.page-header h2 {
    font-size: 1.875rem; font-weight: 700; color: var(--text-strong);
    margin-bottom: 0.25rem; letter-spacing: -0.011em;
}
.page-header p { color: var(--text-muted); font-size: 1.0625rem; }
.card {
    background: var(--surface);
    border-radius: var(--radius-lg);
    padding: 1.75rem;
    margin-bottom: 1.25rem;
    border: 1px solid var(--border);
    box-shadow: var(--shadow-2);
    transition: border-color var(--motion-base) var(--ease),
                box-shadow var(--motion-base) var(--ease);
}
.card:hover { border-color: var(--border-strong); box-shadow: var(--shadow-3); }
.card h3 { font-size: 1rem; font-weight: 600; margin-bottom: 1.25rem; color: var(--text-strong); display: flex; align-items: center; gap: 0.5rem; }
.card h3 svg { width: 17px; height: 17px; stroke: var(--accent); fill: none; }
.viewer-panel { width: 100%; height: 600px; background: var(--surface-soft); border: 1px solid var(--border); border-radius: var(--radius-md); position: relative; overflow: hidden; }
.viewer-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: var(--text-faint); }
.viewer-empty svg { width: 56px; height: 56px; margin-bottom: 1rem; opacity: 0.4; }
#viewer-3d { width: 100%; height: 100%; }
.viewer-controls { position: absolute; top: 1rem; right: 1rem; display: flex; gap: 0.5rem; z-index: 10; }
.viewer-btn { padding: 0.5rem; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-sm); cursor: pointer; transition: border-color var(--motion-fast) var(--ease); display: flex; align-items: center; justify-content: center; }
.viewer-btn:hover { border-color: var(--accent); }
.viewer-btn svg { width: 17px; height: 17px; stroke: var(--text); fill: none; }
.atom-info { position: absolute; bottom: 1rem; left: 1rem; background: rgba(255, 255, 255, 0.96); padding: 0.625rem 0.875rem; border-radius: var(--radius-md); font-size: 0.875rem; color: var(--text); box-shadow: var(--shadow-2); }
.form-group { margin-bottom: 1.375rem; }
.form-label { display: block; margin-bottom: 0.5rem; color: var(--text); font-size: 0.875rem; font-weight: 500; }
.form-input, .form-textarea, .form-select { width: 100%; padding: 0.6875rem 1rem; background: var(--surface); border: 1.5px solid var(--border-strong); border-radius: var(--radius-sm); color: var(--text-strong); font-size: 0.9375rem; font-family: inherit; transition: border-color var(--motion-fast) var(--ease), box-shadow var(--motion-fast) var(--ease); }
.form-input:focus, .form-textarea:focus, .form-select:focus { outline: none; border-color: var(--accent); }
/* Native <select> styling. We strip the OS chrome and draw a chevron
   from an inline SVG so closed selects match the rest of the form
   (same border, same focus ring, same radius) instead of falling
   back to the browser's themed control. The right padding is bumped
   to 2.5rem so the chevron has breathing room — the previous
   default padding crowded the arrow against the right border.

   Note: the OPEN menu still uses native <option> rendering, which
   browsers paint with their own styles. Where consistency in the
   open state matters (e.g. in modals) the page should reach for
   the StudioDropdown component instead. */
.form-select {
    appearance: none;
    -webkit-appearance: none;
    -moz-appearance: none;
    padding-right: 2.5rem;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12' fill='none' stroke='%2364748b' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 4.5l3 3 3-3'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 0.875rem center;
    background-size: 12px 12px;
    cursor: pointer;
}
.form-select:hover { border-color: var(--accent-ring); }
.form-select::-ms-expand { display: none; }
.form-select:disabled {
    cursor: not-allowed;
    background-color: var(--surface-soft);
    opacity: 0.7;
}
.form-textarea { resize: vertical; min-height: 100px; line-height: 1.6; }
.checkbox-wrapper { display: flex; align-items: center; cursor: pointer; user-select: none; margin-bottom: 1rem; }
.checkbox-wrapper input[type="checkbox"] { display: none; }
.checkbox-custom { width: 18px; height: 18px; border: 1.5px solid var(--border-strong); border-radius: 4px; margin-right: 0.625rem; display: flex; align-items: center; justify-content: center; transition: all var(--motion-fast) var(--ease); background: var(--surface); flex-shrink: 0; }
.checkbox-wrapper input[type="checkbox"]:checked + .checkbox-custom { background: var(--accent); border-color: var(--accent); }
/* The check mark is a 5×9 box with only its right and bottom borders
   drawn, rotated 45deg → an L that reads as a tick. The flex parent
   centers the BOX (5×9) but the visible "ink" of the tick is
   asymmetric inside that box (lower-right of the box only), so a
   plain rotation lands the tick visually below the box's vertical
   centre. A −1px translateY pulls the tick up onto the optical
   centre — a one-pixel adjustment that fixes the "checkmark sits
   slightly low" report without changing the box geometry. */
.checkbox-custom::after { content: ''; width: 5px; height: 9px; border: solid white; border-width: 0 1.5px 1.5px 0; transform: translateY(-1px) rotate(45deg) scale(0); transition: transform var(--motion-fast) var(--ease); }
.checkbox-wrapper input[type="checkbox"]:checked + .checkbox-custom::after { transform: translateY(-1px) rotate(45deg) scale(1); }
.checkbox-label { color: var(--text); font-size: 0.9375rem; font-weight: 400; }
.file-upload-area { border: 1.5px dashed var(--border-strong); border-radius: var(--radius-md); padding: 1.75rem; text-align: center; transition: border-color var(--motion-fast) var(--ease), background var(--motion-fast) var(--ease); cursor: pointer; background: var(--surface-soft); }
.file-upload-area:hover { border-color: var(--accent); background: var(--accent-soft); }
.file-upload-area.drag-over { border-color: var(--accent); background: var(--accent-soft); box-shadow: 0 0 0 3px var(--accent-ring); }
.file-upload-icon { width: 40px; height: 40px; margin: 0 auto 0.875rem; background: var(--accent-soft); border-radius: var(--radius-md); display: flex; align-items: center; justify-content: center; }
.file-upload-icon svg { width: 20px; height: 20px; stroke: var(--accent); fill: none; }
.file-upload-text { color: var(--text); font-size: 0.9375rem; margin-bottom: 0.375rem; }
.file-upload-hint { color: var(--text-faint); font-size: 0.8125rem; }
.file-selected { display: flex; align-items: center; gap: 0.75rem; padding: 0.75rem 1rem; background: var(--accent-soft); border: 1px solid var(--info-border); border-radius: var(--radius-md); margin-top: 0.75rem; }
.file-selected svg { width: 18px; height: 18px; stroke: var(--accent); fill: none; }
.file-selected-name { flex: 1; color: var(--text-strong); font-weight: 500; font-size: 0.875rem; }
/* ── Buttons ──────────────────────────────────────────────────────
   One restrained button system. Variants (primary / secondary /
   danger / ghost) set only colour; size modifiers (small / icon) set
   only metrics; shared states (hover / active / focus-visible /
   disabled / loading) live on .btn so every button behaves the same.
   Hover DARKENS the fill rather than adding a coloured glow — the
   "premium scientific" read comes from solid, quiet surfaces, not
   from shadow bloom. A 1.5px transparent border on the base keeps
   filled and bordered variants the exact same height. */
.btn {
    display: inline-flex; align-items: center; justify-content: center;
    gap: 0.5rem;
    padding: 0.625rem 1.5rem;
    border: 1.5px solid transparent;
    border-radius: var(--radius-md);
    font-family: inherit;
    font-size: 0.9375rem; font-weight: 600;
    line-height: 1.2; white-space: nowrap;
    cursor: pointer;
    -webkit-user-select: none; user-select: none;
    transition: background var(--motion-fast) var(--ease),
                border-color var(--motion-fast) var(--ease),
                color var(--motion-fast) var(--ease),
                transform var(--motion-fast) var(--ease);
}
.btn svg { width: 16px; height: 16px; stroke: currentColor; fill: none; }

/* Pressed — a single-pixel nudge reads as tactile without bouncing. */
.btn:active:not(:disabled) { transform: translateY(1px); }

/* Keyboard focus only (never on a mouse click) — one consistent ring. */
.btn:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--accent-ring); }

/* Disabled — a single rule for every variant, including the
   inline-tinted buttons on the tool pages. Replaces the ad-hoc
   `opacity: 0.6` declarations scattered across the JSX. */
.btn:disabled, .btn[disabled] {
    opacity: 0.5; cursor: not-allowed;
    box-shadow: none; transform: none;
}

.btn-primary { background: var(--accent); color: var(--accent-fg); }
.btn-primary:hover:not(:disabled) { background: var(--accent-strong); }

.btn-secondary { background: var(--surface); border-color: var(--border-strong); color: var(--text); }
.btn-secondary:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); background: var(--accent-soft); }

.btn-danger { background: var(--danger); color: var(--accent-fg); }
.btn-danger:hover:not(:disabled) { background: var(--danger-strong); }
.btn-danger:focus-visible { box-shadow: 0 0 0 3px var(--danger-ring); }

/* Ghost — transparent until hovered; for toolbar / inline one-shot
   actions that shouldn't carry a filled button's visual weight. */
.btn-ghost { background: transparent; color: var(--text-muted); }
.btn-ghost:hover:not(:disabled) { background: var(--hover-bg); color: var(--text-strong); }

/* Size modifiers — metrics only, no colour. */
.btn-small { padding: 0.375rem 0.875rem; font-size: 0.8125rem; gap: 0.375rem; }
.btn-icon  { padding: 0.5rem; }                 /* square; pair with a single SVG child */
.btn-small.btn-icon { padding: 0.375rem; }

/* Loading — hide the label, spin a ring centred in place. The label
   still occupies layout (transparent), so the button never resizes
   when it enters the loading state. Pair with aria-busy="true". */
.btn.is-loading { color: transparent !important; pointer-events: none; position: relative; }
.btn.is-loading::after {
    content: ""; position: absolute;
    top: 50%; left: 50%; margin: -7.5px 0 0 -7.5px;
    width: 15px; height: 15px;
    border: 2px solid var(--accent-fg); border-top-color: transparent;
    border-radius: 50%;
    animation: btn-spin 0.6s linear infinite;
}
.btn-secondary.is-loading::after,
.btn-ghost.is-loading::after { border-color: var(--accent); border-top-color: transparent; }
@keyframes btn-spin { to { transform: rotate(360deg); } }
.grid-2 { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1.25rem; }
.grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; }
.status-badge { display: inline-flex; align-items: center; gap: 0.375rem; padding: 0.3rem 0.75rem; border-radius: var(--radius-sm); font-size: 0.8125rem; font-weight: 500; font-variant-numeric: tabular-nums; }
.status-running { background: var(--warning-bg); color: var(--warning-fg); border: 1px solid var(--warning-border); }
.status-completed { background: var(--success-bg); color: var(--success-fg); border: 1px solid var(--success-border); }
.status-failed { background: var(--danger-bg); color: var(--danger-fg); border: 1px solid var(--danger-border); }
.status-pending { background: var(--info-bg); color: var(--info-fg); border: 1px solid var(--info-border); }
.job-item { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-md); padding: 0.75rem 1rem; margin-bottom: 0.4rem; transition: border-color var(--motion-fast) var(--ease), box-shadow var(--motion-fast) var(--ease);
    /* Job cards are click/Shift-click selection targets (My Results +
       Fine-tune Jobs). Suppress text selection here ONLY so Shift-range
       selection never drags a page-wide text highlight. Scoped to the
       card class — the rest of the app keeps normal text selection. */
    -webkit-user-select: none; user-select: none; }
.job-item:hover { border-color: var(--accent-ring); box-shadow: var(--shadow-2); }
.job-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.4rem; }
.job-title { font-weight: 600; color: var(--text-strong); font-size: 0.9375rem; max-width: 340px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.job-actions { display: flex; gap: 0.4rem; }
.job-meta { display: flex; gap: 1rem; font-size: 0.8125rem; color: var(--text-muted); margin-bottom: 0; font-variant-numeric: tabular-nums; }
.progress-bar { width: 100%; height: 5px; background: var(--border-faint); border-radius: 999px; overflow: hidden; margin-top: 0.5rem; }
.progress-fill { height: 100%; background: var(--accent); transition: width 0.3s ease; border-radius: 999px; }
.seq-box { background: var(--surface-soft); border: 1px solid var(--border); border-radius: var(--radius-md); padding: 1.25rem; margin-bottom: 1rem; }
.seq-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; }
.seq-title { font-weight: 600; color: var(--accent); font-size: 0.9375rem; }
.constraint-box { background: var(--surface-soft); border: 1px solid var(--border); border-radius: var(--radius-md); padding: 1.25rem; margin-bottom: 1rem; }
.constraint-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; }
.modification-item { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 0.875rem; margin-bottom: 0.75rem; }
.alert { padding: 1rem 1.25rem; border-radius: var(--radius-md); margin-bottom: 1.25rem; font-weight: 500; display: flex; align-items: center; gap: 0.625rem; font-size: 0.9375rem; }
.alert svg { width: 18px; height: 18px; stroke: currentColor; fill: none; flex-shrink: 0; }
.alert-success { background: var(--success-bg); color: var(--success-fg); border: 1px solid var(--success-border); }
.alert-error { background: var(--danger-bg); color: var(--danger-fg); border: 1px solid var(--danger-border); }
.alert-warning { background: var(--warning-bg); color: var(--warning-strong); border: 1px solid var(--warning-border); align-items: flex-start; }
.modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(15, 23, 42, 0.45); display: flex; align-items: center; justify-content: center; z-index: 1000; }
.modal-content { background: var(--surface); border-radius: var(--radius-lg); padding: 1.75rem; max-width: 600px; width: 90%; max-height: 80vh; overflow-y: auto; box-shadow: var(--shadow-modal); border: 1px solid var(--border); }
.modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.25rem; }
.modal-header h3 {
    font-size: 1.125rem; font-weight: 700; color: var(--text-strong);
    letter-spacing: -0.01em;
}
.modal-close { background: none; border: none; cursor: pointer; padding: 0.375rem; }
.details-section { margin-bottom: 1.25rem; }
.details-section h4 { color: var(--accent); font-size: 0.875rem; font-weight: 600; margin-bottom: 0.625rem; }
.details-item { display: flex; justify-content: space-between; padding: 0.5rem 0; border-bottom: 1px solid var(--border-faint); gap: 1rem; }
.details-label { color: var(--text-muted); font-size: 0.875rem; }
.details-value { color: var(--text-strong); font-weight: 500; font-size: 0.875rem; font-variant-numeric: tabular-nums; }

/* ── Fine-tune Jobs: compact toolbar + consistent details type ──── */
/* Compact, equal-height toolbar controls (search / status / sort).
   ~34px to match the refresh square; reuses the existing controls
   rather than one-off heights per element. */
.ft-toolbar .form-input,
.ft-toolbar .molgen-dropdown {
    padding-top: 0.4rem;
    padding-bottom: 0.4rem;
    font-size: 0.84rem;
    min-height: 34px;
    box-sizing: border-box;
}
.ft-toolbar .molgen-dropdown { padding-right: 1.9rem; }
/* Details panel: label and value share one size so values no longer
   look oversized; value only slightly stronger in weight. */
.ft-details .details-item {
    border-bottom: none;
    padding: 0.2rem 0;
    gap: 1rem;
}
.ft-details .details-label { font-size: 0.8125rem; font-weight: 500; }
.ft-details .details-item > span:last-child {
    font-size: 0.8125rem;
    font-weight: 600;
    color: var(--text-strong);
    text-align: right;
}

.range-input {
    width: 100%;
    padding: 0;
    background: transparent;
    border: none;
    box-shadow: none;
    height: 28px;
    -webkit-appearance: none;
    appearance: none;
}

.range-input:focus {
    outline: none;
}

.range-input::-webkit-slider-runnable-track {
    height: 6px;
    border-radius: 999px;
    background: var(--border);
}

.range-input::-webkit-slider-thumb {
    -webkit-appearance: none;
    appearance: none;
    width: 16px;
    height: 16px;
    border-radius: 999px;
    background: var(--accent);
    margin-top: -5px;
}

.range-input::-moz-range-track {
    height: 6px;
    border-radius: 999px;
    background: var(--border);
}

.range-input::-moz-range-thumb {
    width: 16px;
    height: 16px;
    border: none;
    border-radius: 999px;
    background: var(--accent);
}

/* RDKit SVG cards — fill container; aspect-ratio on parent prevents letterboxing */
.molcard-svg svg { width: 100% !important; height: 100% !important; display: block; }
.molcard-svg { display: block; }
/* Enlarged hover popup — compact preview */
.molcard-svg-hover { display: block; }
.molcard-svg-hover svg { width: 440px !important; height: 340px !important; display: block; }


@keyframes slideUpFade {
  from {
    opacity: 0;
    transform: translateY(12px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@keyframes fadeIn {
  from { opacity: 0; }
  to { opacity: 1; }
}

/* ── Responsive: sidebar becomes a drawer on narrow viewports ─── */
@media (max-width: 900px) {
  /* Sidebar slides in from the left as an overlay */
  .app-shell { display: block; }
  .sidebar {
    position: fixed;
    top: 0; left: 0; bottom: 0;
    height: 100vh;
    width: 248px;
    flex: none;
    transform: translateX(-100%);
    transition: transform 0.22s ease;
    box-shadow: 0 8px 32px rgba(15, 23, 42, 0.18);
    z-index: 40;
  }
  .app-shell.mobile-drawer-open .sidebar { transform: translateX(0); }
  .app-shell.mobile-drawer-open .sidebar-backdrop { display: block; }

  /* On mobile the edge toggle is irrelevant — drawer is either open or hidden. */
  .sidebar-edge-toggle { display: none; }

  /* On mobile, ignore the desktop "collapsed" preference — drawer is either
     fully open or fully hidden; no in-between "icons only" state.
     Items/header/logo no longer change layout on desktop collapse, so
     only the label visibility needs to be unwound here. */
  .app-shell.sidebar-collapsed .sidebar { width: 248px; }
  .app-shell.sidebar-collapsed .sidebar-logo-text,
  .app-shell.sidebar-collapsed .sidebar-item-label { opacity: 1; flex: 1; width: auto; min-width: 0; overflow: visible; padding: revert; margin: revert; }

  .main-area { width: 100%; }
  .mobile-topbar { display: flex; }
  .main-content { padding: 1rem; }
}

@media (max-width: 768px) { .grid-2, .grid-3 { grid-template-columns: 1fr; } }

/* ── Login Page ──────────────────────────────────────────────── */
.login-page { min-height: 100vh; display: flex; align-items: center; justify-content: center; background: var(--bg); padding: 1.5rem; }
.login-card { background: var(--surface); border-radius: var(--radius-lg); padding: 2.25rem 2rem; width: 100%; max-width: 400px; box-shadow: var(--shadow-3); border: 1px solid var(--border); animation: slideUpFade 0.3s ease; }
.login-logo { text-align: center; margin-bottom: 1.75rem; }
.login-logo .logo-icon { width: 48px; height: 48px; background: var(--accent); border-radius: var(--radius-md); display: flex; align-items: center; justify-content: center; margin: 0 auto 0.875rem; }
.login-logo .molecule-icon { width: 28px; height: 28px; color: white; }
.login-logo h1 { font-size: 1.25rem; font-weight: 700; color: var(--text-strong); margin-bottom: 0.25rem; }
.login-logo p { font-size: 0.6875rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.06em; }

/* Compact form density — scoped to the Login / Sign Up / reset-password card
   only, so account/admin forms keep the default .form-input / .form-group
   metrics. Trims input height (~6px) and the inter-field gap (~4px); border,
   font-size, and focus-ring styling are inherited unchanged. */
.login-card .form-group { margin-bottom: 1.125rem; }
.login-card .form-input { padding: 0.5rem 1rem; }

/* Auth tabs */
.auth-tabs { display: flex; background: var(--surface-sunk); border-radius: var(--radius-md); padding: 3px; margin-bottom: 1.5rem; gap: 3px; }
.auth-tab { flex: 1; padding: 0.5rem; border: none; border-radius: var(--radius-sm); font-size: 0.875rem; font-weight: 500; cursor: pointer; transition: all var(--motion-fast) var(--ease); background: transparent; color: var(--text-muted); }
.auth-tab.active { background: var(--surface); color: var(--text-strong); font-weight: 600; box-shadow: var(--shadow-1); }
.auth-tab:not(.active):hover { background: var(--border-faint); color: var(--text); }

/* ════════════════════════════════════════════════════════════════
   Alma Studio — workspace shell (project + LiveReport workbench)
   ════════════════════════════════════════════════════════════════ */

.studio-shell {
    display: flex;
    height: 100vh;
    overflow: hidden;
    background: var(--bg);
    color: var(--text);
}

/* ── Sidebar — mirrors the platform .sidebar exactly so switching modes
   feels like changing rooms in the same building, not a new house. */
.studio-sidebar {
    flex: 0 0 248px;
    width: 248px;
    height: 100vh;
    background: var(--surface);
    border-right: 1px solid var(--border);
    display: flex;
    flex-direction: column;
    /* No outer padding — header, nav, and footer each own their padding. */
    padding: 0;
    /* Positioned + overflow:visible so the floating edge-toggle can sit
       half-outside the right edge (mirrors the platform .sidebar). The nav
       scrolls itself (overflow-y:auto), so the sidebar needs no own clip. */
    position: relative;
    overflow: visible;
    z-index: 30;
    /* NO width transition — the sidebar snaps instantly between expanded and
       collapsed. `.studio-sidebar` and `.studio-main` are flex siblings, so
       ANIMATING the width re-ran flex layout every frame for 0.22s: the sheet,
       its horizontal scroller, and the plot/analysis panel all visibly resized
       throughout the transition (the actual UX complaint — not the JS work).
       Snapping applies the new layout in a SINGLE reflow, so the workspace
       stays stable and the toggle feels instant. The freed horizontal space is
       therefore applied once, cleanly. (Labels still fade via opacity for a
       subtle touch — see below.) */
    /* Sidebar chrome is not selectable text — clicking/dragging items must not
       highlight labels. Scoped to the sidebar only; the grid, table cells, and
       editable fields live in .studio-main and keep normal text selection. */
    -webkit-user-select: none;
    user-select: none;
}
/* Layout-affecting properties on the inner elements (padding / gap /
   justify-content) stay CONSTANT in both states, so the icons never snap. The
   labels + brand text collapse to zero width with an opacity fade — NOT
   display:none + center-justify, which would kick the icons right. The icon
   stays left-anchored throughout. Mirrors the platform sidebar. */
.studio-shell.studio-sidebar-collapsed .studio-sidebar {
    flex-basis: 72px;
    width: 72px;
}
.studio-sidebar-brand-text,
.studio-action > span:not(.studio-action-icon) { transition: opacity 0.18s ease; }
.studio-shell.studio-sidebar-collapsed .studio-sidebar-brand-text,
.studio-shell.studio-sidebar-collapsed .studio-action > span:not(.studio-action-icon) {
    opacity: 0;
    flex: 0 0 0;
    width: 0;
    min-width: 0;
    margin: 0;
    padding: 0;
    overflow: hidden;
    white-space: nowrap;
}

/* Header — identical geometry to platform .sidebar-header. Locked to
   --topband-h so the brand block, the Platform sidebar header, and
   the Studio toolbar all share the exact same outer height. Vertical
   padding is 0; flex centering handles the vertical alignment. */
.studio-sidebar-brand {
    display: flex;
    align-items: center;
    justify-content: flex-start;
    gap: 0.75rem;
    height: var(--topband-h);
    min-height: var(--topband-h);
    padding: 0 1rem;
    border-bottom: 1px solid var(--border);
}
.studio-sidebar-brand-icon {
    /* No framed box (matches the main app sidebar logo). */
    width: 36px; height: 36px;
    border-radius: var(--radius-md);
    display: flex; align-items: center; justify-content: center;
    overflow: hidden; flex-shrink: 0;
}
.studio-sidebar-brand-icon img { width: 100%; height: 100%; object-fit: contain; display: block; }
.studio-sidebar-brand-text { min-width: 0; }
.studio-sidebar-brand-name {
    font-size: 1.1rem; font-weight: 700;
    color: var(--text-strong);
    letter-spacing: -0.012em;
    line-height: 1.1; white-space: nowrap;
}
.studio-sidebar-brand-sub {
    font-size: 0.68rem; color: var(--text-muted);
    text-transform: uppercase; letter-spacing: 0.05em;
    font-weight: 600;
    margin: 0;
    white-space: nowrap;
}

/* Nav — flex:1 scrollable area, mirrors .sidebar-nav padding */
.studio-sidebar-nav {
    flex: 1; min-height: 0;
    display: flex; flex-direction: column;
    gap: 0.25rem;
    padding: 1.25rem 0.625rem 0.875rem;
    overflow-y: auto;
}

.studio-sidebar-section {
    display: flex; flex-direction: column;
    gap: 0.125rem;
    margin-bottom: 0.5rem;
}
.studio-sidebar-spacer { flex: 1; }

/* Footer — anchored at the bottom, matches platform .sidebar-footer */
.studio-sidebar-footer {
    padding: 0.625rem;
    border-top: 1px solid var(--border);
    display: flex; flex-direction: column;
    gap: 0.25rem;
}

/* Action buttons — share geometry with platform .sidebar-item so the
   spacing rhythm is identical. */
.studio-action {
    display: flex; align-items: center; gap: 0.75rem;
    width: 100%;
    padding: 0.55rem 0.75rem;
    background: transparent; border: none;
    border-radius: var(--radius-md);
    color: var(--text);
    font-size: 0.875rem; font-weight: 500;
    text-align: left; cursor: pointer; white-space: nowrap;
    overflow: hidden;
    transition: background var(--motion-fast) var(--ease),
                color var(--motion-fast) var(--ease);
}
.studio-action:hover { background: var(--surface-sunk); color: var(--text-strong); }
.studio-action-icon {
    flex-shrink: 0;
    width: 20px; height: 20px;
    display: flex; align-items: center; justify-content: center;
    color: var(--text-muted);
}
.studio-action-icon svg { width: 18px; height: 18px; stroke: currentColor; fill: none; }
/* The molecule and column glyphs read better at 20px — slightly larger
   without making the row taller (icon-icon container stays 20×20). */
.studio-action-icon[data-icon="compound-plus"] svg,
.studio-action-icon[data-icon="column-plus"] svg,
.studio-action-icon[data-icon="model-score"] svg,
.studio-action-icon[data-icon="layout-grid"] svg { width: 20px; height: 20px; }
.studio-action:hover .studio-action-icon { color: var(--accent); }

/* Back to Platform — styled like the platform sidebar's workspace entry, so
   entering and exiting Studio feel symmetric. */
.studio-action-exit {
    color: var(--text-strong);
    font-weight: 600;
}
.studio-action-exit .studio-action-icon { color: var(--accent); }
.studio-action-exit:hover { background: var(--accent-soft); color: var(--accent-strong); }
.studio-action-exit:hover .studio-action-icon { color: var(--accent-strong); }
/* Divider used between the workspace exit and the rest of the actions. */
.studio-sidebar-divider {
    height: 1px;
    background: var(--border-faint);
    margin: 0.5rem 0.625rem;
}

.studio-action-logout { color: var(--text-muted); }
.studio-action-logout:hover { background: var(--danger-bg); color: var(--danger-strong); }
.studio-action-logout:hover .studio-action-icon { color: var(--danger-strong); }

/* ── Main area ─────────────────────────────────────────────────
   Vertical layout:
     · .studio-toolbar    — Project / Design Sheet row, full width.
                            Stays put when drawers open.
     · .studio-workspace  — horizontal flex of drawer + content.
                            Only this region resizes when a drawer
                            opens or closes.
*/
.studio-main {
    flex: 1; min-width: 0;
    display: flex; flex-direction: column;
    background: var(--bg);
    min-height: 0;             /* allow inner scroll regions to size */
    overflow: hidden;          /* contain the workspace flex children */
}

/* Workspace row — drawer on the left, sheet content on the right. */
.studio-workspace {
    flex: 1;
    display: flex;
    flex-direction: row;
    min-height: 0;
    min-width: 0;
    overflow: hidden;
    /* Anchor for the drawer-host overlay (Columns / Filter). The
       drawer is `position: absolute` so it doesn't push the table
       and trigger a costly table-layout reflow on open/close. */
    position: relative;
}

/* Toolbar — sits between the sidebar and the data canvas. White surface
   with a strong bottom border + subtle shadow, so it visually layers
   above the page background and reads as a distinct command bar. */
.studio-toolbar {
    display: flex; align-items: center;
    gap: 0.625rem;
    /* Vertical padding is 0 — flex `align-items: center` keeps the
       selector buttons and tabs vertically centered while the outer
       box stays exactly --topband-h tall. Previously the 0.75rem
       vertical padding plus the ~38 px selector buttons made the
       toolbar's real computed height ~63 px, which left the toolbar
       visibly taller than the 60 px sidebar brand block. Lock to the
       shared token so they line up to the pixel. */
    padding: 0 1rem;
    background: var(--surface);
    border-bottom: 1px solid var(--border-strong);
    box-shadow: 0 1px 0 rgba(15, 23, 42, 0.03);
    flex-shrink: 0;
    height: var(--topband-h);
    min-height: var(--topband-h);
    z-index: 5;
}

/* Vertical separator between the Project (top-level context) and the
   Design Sheet group (operational area). */
.studio-toolbar-divider {
    flex-shrink: 0;
    width: 1px;
    height: 24px;
    background: var(--border-strong);
    margin: 0 0.25rem;
}

/* Group containing the Design Sheet button + tab bar — visually one unit. */
.studio-sheet-group {
    flex: 1; min-width: 0;
    display: flex; align-items: center; gap: 0.5rem;
}

/* Project selector reads as a top-level context pill. */
.studio-selector-project {
    background: var(--accent-soft);
    border-color: var(--info-border);
}
.studio-selector-project .studio-selector-label { color: var(--accent-strong); }
.studio-selector-project .studio-selector-icon { color: var(--accent-strong); }
.studio-selector-project:hover:not(:disabled) {
    background: var(--accent-muted);
    border-color: var(--accent);
}

.studio-selector {
    display: inline-flex; align-items: center; gap: 0.5rem;
    padding: 0.45rem 0.75rem;
    background: var(--surface);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-md);
    color: var(--text);
    font: inherit; font-size: 0.875rem; font-weight: 500;
    cursor: pointer;
    box-shadow: var(--shadow-1);
    transition: border-color var(--motion-fast) var(--ease),
                background var(--motion-fast) var(--ease),
                box-shadow var(--motion-fast) var(--ease);
    max-width: 280px; flex-shrink: 0;
}
.studio-selector:hover:not(:disabled) {
    border-color: var(--accent);
    background: var(--accent-soft);
    box-shadow: var(--shadow-2);
}
.studio-selector:disabled { opacity: 0.55; cursor: not-allowed; box-shadow: none; }
.studio-selector-icon { display: flex; color: var(--accent); }
.studio-selector-icon svg { width: 15px; height: 15px; stroke: currentColor; fill: none; }
.studio-selector-label {
    font-size: 0.78rem; font-weight: 600;
    color: var(--text-muted);
    border-right: 1px solid var(--border-faint);
    padding-right: 0.5rem;
}
/* Compact selector — used by the Design Sheet button, which no longer shows
   a value. Active sheet is reflected in the tab bar instead. */
.studio-selector-simple .studio-selector-label {
    border-right: none;
    padding-right: 0;
    color: var(--text-strong);
}
.studio-selector-value {
    color: var(--text-strong); font-weight: 600;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    max-width: 180px;
}
.studio-selector-chevron { display: flex; color: var(--text-faint); }
.studio-selector-chevron svg { width: 14px; height: 14px; stroke: currentColor; fill: none; }

/* Tab bar — unified container that holds the scroll arrows and tabs together
   so the whole thing reads as a single tabbed control, not loose buttons.
   Note: the tab bar no longer takes the full leftover width — it shares
   space with the sheet-level search box on the right (see
   .studio-sheet-search below). The flex-basis here keeps the strip
   growing where it can; the sheet search reserves a fixed minimum of
   its own. */
.studio-tabs {
    flex: 1 1 auto; min-width: 0;
    display: flex; align-items: stretch; gap: 6px;
    margin-left: 0.5rem;
    padding: 4px 6px;
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    min-height: 38px;
}

/* ── Sheet-wide search input (right of the tab strip) ────────────
   Shrinks with the toolbar but stays large enough that the input
   is usable. The match-count chip + nav buttons appear inline once
   results are in. */
.studio-sheet-search {
    flex: 0 0 auto;
    margin-left: 0.5rem;
    display: flex; align-items: center; gap: 4px;
    padding: 0 6px;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    min-height: 38px;
    width: 280px;
    max-width: 360px;
    transition: border-color var(--motion-fast) var(--ease),
                box-shadow var(--motion-fast) var(--ease);
}
.studio-sheet-search:focus-within {
    border-color: var(--accent);
    box-shadow: 0 0 0 3px var(--accent-ring);
}
.studio-sheet-search-icon {
    flex-shrink: 0;
    color: var(--text-muted);
    display: flex; align-items: center;
}
.studio-sheet-search-input {
    flex: 1; min-width: 0;
    border: none;
    background: transparent;
    color: var(--text);
    font-size: 0.85rem;
    padding: 6px 4px;
    outline: none;
}
.studio-sheet-search-input::placeholder { color: var(--text-faint); }
.studio-sheet-search-chip {
    flex-shrink: 0;
    font-size: 0.72rem;
    color: var(--text-muted);
    background: var(--surface-soft);
    border: 1px solid var(--border-faint);
    border-radius: var(--radius-sm);
    padding: 2px 6px;
    line-height: 1;
    white-space: nowrap;
}
.studio-sheet-search-nav,
.studio-sheet-search-clear {
    flex-shrink: 0;
    width: 22px; height: 22px;
    border: 1px solid var(--border);
    background: var(--surface);
    color: var(--text-muted);
    border-radius: var(--radius-sm);
    cursor: pointer;
    display: inline-flex; align-items: center; justify-content: center;
    transition: color var(--motion-fast) var(--ease),
                border-color var(--motion-fast) var(--ease),
                background var(--motion-fast) var(--ease);
}
.studio-sheet-search-nav:hover,
.studio-sheet-search-clear:hover {
    color: var(--accent);
    border-color: var(--accent-ring);
    background: var(--accent-soft);
}

/* Per-cell + per-row search-hit highlights.
   Background-only — no inset border / shadow — so the row geometry
   (cell width / line-height / scroll position) doesn't shift when a
   cell becomes a match. The active-row gets a slightly more saturated
   wash on its matched cells so the user can locate the focused match
   without any layout impact. */
.studio-cell-search-hit {
    background-color: rgba(255, 213, 79, 0.32) !important;
}
.studio-row-search-active td.studio-cell-search-hit {
    background-color: rgba(255, 184, 0, 0.45) !important;
}

/* ── Column-group band overlay ───────────────────────────────────
   Renders as ONE absolute-positioned <div> per group, layered into
   the existing header area (NOT a separate <tr>). The user
   perceives the column header as one taller unified block:
     · grouped columns: a band stripe at the top + the existing
       column controls (label / grip / menu / resize) below;
     · ungrouped columns: visually identical to the no-grouping
       case — same cell height, same label position, same controls.

   Layering strategy that delivers BOTH of those:
     · The overlay strip is `position: sticky; top: 0; height:
       --group-band-h` — it takes ACTUAL vertical space ABOVE the
       <table>. So the table is shifted DOWN by exactly the band
       height; the column header row keeps its original height and
       the columns underneath are unchanged.
     · Grouped <th>s carry a `studio-th-grouped` className. Their
       sticky-top offset gets bumped to `var(--group-band-h)` so
       when the user scrolls vertically, those ths stick BELOW the
       overlay (which itself sticks at top:0). Ungrouped <th>s
       keep their original `top: 0` sticky offset so during scroll
       they stick at the very top, behind/beside the overlay — the
       overlay extends above EVERY column horizontally so there's
       no visible gap there.
     · The overlay's width is JS-mutated to match the table's full
       width (see the ResizeObserver effect in StudioGrid) so the
       band-area background runs the entire content width, not
       just the visible viewport.

   Geometry of each band (one per group):
     · `left`  = sum of sticky-left widths (rownumber + checkbox +
                 structure) + colCumWidths[firstMemberIdx]
     · `width` = sum of member column widths
   Both are content-coord values; the overlay is inside the same
   overflow-auto wrap as the table, so they scroll horizontally
   with the table content. The ResizeObserver effect updates these
   live during a column-resize drag (since columnWidths state
   only commits on mouseup). */
/* Single source of truth for the column-group band height. Defined
   on the wrap so it cascades into BOTH the overlay (for the strip's
   own height) and the thead (for the column ths' sticky-top offset
   below the overlay). Stays defined even when no groups exist —
   harmless because the overlay isn't rendered then. */
.studio-table-wrap {
    --group-band-h: 26px;
}
/* Grow EVERY column header th to make room for the band stripe
   INSIDE the existing column header area — NOT as a separate row
   above. The band stripe lives inside the top --group-band-h
   pixels of each grouped <th>; ungrouped ths stay at the same
   total height so the header reads as a single unified block
   (HTML tables share row height across cells anyway).

   Total th height = the user's --header-h (or default 44px) +
   --group-band-h. Inner controls (.studio-th-content / grip /
   menu / resize) keep their default `top: 0; bottom: 0`
   positioning AND `.studio-th-content`'s flex `align-items:
   center` — so the LABEL is centered in the full taller cell.
   No per-cell `top` offset, no padding-top, no visible "empty
   space above the label" cue. The bands absolute-position OVER
   the top portion of grouped ths only; ungrouped ths show no
   band, just a slightly larger cell with a centered label.

   Sticky-top stays at the inherited `top: 0` from the base
   `.studio-table thead th` rule — both the overlay (z:5,
   sticky-top:0) and the th row (sticky-top:0) anchor to the
   wrap's top edge, with the band overlay painting over the
   band-area portion of grouped ths via z-index. */
/* Specificity matters here. The base rule below
   (`.studio-table thead th { height: var(--header-h, auto) }`,
   declared further down the file at the thead-styles section) has
   the same (0,1,2) specificity as a plain
   `.studio-thead-with-group-bands > tr > th` selector and would
   win the source-order tiebreak — so the th would silently revert
   to `var(--header-h, auto)` and the row never grew when groups
   appeared. Anchor this rule at `.studio-table thead`-level to
   bump the specificity to (0,2,3) and beat the base rule
   regardless of declaration order. */
.studio-table thead.studio-thead-with-group-bands > tr > th {
    height: calc(var(--header-h, 44px) + var(--group-band-h, 26px));
    min-height: calc(var(--header-h, 44px) + var(--group-band-h, 26px));
}
/* For columns that ARE in a group, push the inner controls
   (label container, drag grip, menu chevron, resize handle)
   DOWN into the lower child-header region so they're centered
   below the band — not behind it. The band's accent-soft
   background paints the top --group-band-h pixels of the cell;
   without these offsets, .studio-th-content (which uses
   `inset: 0`) would still fill the entire cell and the label
   would sit behind the band, plus the grip/menu/resize would
   start at top:0 (inside the band) rather than at the top of
   the child-header region.

   Ungrouped ths receive NO offset — their inner controls keep
   their default `top: 0; bottom: 0` positions, so the label
   stays centered in the FULL taller cell (per user spec:
   "ungrouped column headers should remain centered in the full
   increased header height"). */
.studio-thead-with-group-bands > tr > th.studio-th-grouped .studio-th-content {
    top: var(--group-band-h, 26px);
}
.studio-thead-with-group-bands > tr > th.studio-th-grouped .studio-th-grip {
    top: var(--group-band-h, 26px);
}
.studio-thead-with-group-bands > tr > th.studio-th-grouped .studio-header-menu-btn {
    top: var(--group-band-h, 26px);
}
.studio-thead-with-group-bands > tr > th.studio-th-grouped .studio-col-resize {
    /* Push the handle below the group band AND shrink its height by the
       same amount. Without the height reduction the handle keeps
       `height: 100%` (full th) while starting at `top: band-h`, so it
       (and its hover ::after) overshoots the th bottom by band-h and
       the resize highlight bleeds down into the body cells. */
    top: var(--group-band-h, 26px);
    height: calc(100% - var(--group-band-h, 26px));
}

.studio-table-group-overlay {
    /* 0-height sticky anchor — does NOT contribute vertical space,
       so the table is NOT pushed down by a band-area strip.
       Bands render outside this 0-height box (overflow visible by
       default for absolute children) directly over the top portion
       of the column header row, so the column header reads as ONE
       unified taller block — band stripe at the top of grouped
       columns, controls below. The anchor sticks at top:0 of the
       wrap so bands stay pinned during vertical scroll. Bands
       scroll horizontally with the table content because they're
       inside the same overflow:auto wrap. */
    position: sticky;
    top: 0;
    height: 0;
    z-index: 5;       /* above thead's th z:4 so bands paint over the band-area portion */
    pointer-events: none;     /* clicks pass through except on bands themselves */
}
.studio-table-group-band {
    position: absolute;
    top: 0;
    height: var(--group-band-h, 26px);
    /* Use the dedicated group palette (slate-leaning blue) so a
       group header reads as a STRUCTURAL region distinct from a
       row/cell SELECTION (which now uses --row-selected-bg). When
       a row inside a grouped column is selected, the band stripe
       and the selection wash sit side-by-side and stay clearly
       separable. */
    background: var(--group-bg);
    /* Border-top is intentionally OMITTED. The .studio-table-wrap
       already paints a 1-px border at the very top of the wrap;
       since this band sticks at top:0 of the wrap, a band
       border-top would stack against the wrap's top border and
       read as a 2-px-thick divider above the group header. Three
       borders (left / right / bottom) carry the "real header cell"
       framing without that doubling. */
    border-top: 0;
    border-left: 1px solid var(--group-border);
    border-right: 1px solid var(--group-border);
    border-bottom: 1px solid var(--group-border);   /* visual divider between the group header and its child headers below — reads as a real header cell rather than a label decal */
    box-sizing: border-box;
    /* The overlay container has pointer-events:none so non-band
       areas above the column headers stay click-through. Bands
       themselves DO need to receive pointer events (drag + chevron
       click), so re-enable here — children inherit `auto` from
       this rule, so the grip / name / chevron all become clickable
       without their own per-element override. */
    pointer-events: auto;
    /* Three-zone flex layout matching the column header:
         · grip (left)        — same width (18px) as .studio-th-grip
         · name (centre)      — flex:1, text-aligned centre so the
                                group name sits at the visual midpoint
                                of the band
         · menu chevron (right) — same width (22px) as
                                  .studio-header-menu-btn
       So the band's LEFT control aligns flush with the first
       child column's left grip, and the band's RIGHT control
       aligns flush with the last child column's right chevron. */
    display: flex;
    align-items: stretch;
    padding: 0;
    color: var(--group-fg);
    font-size: 0.78rem;
    font-weight: 600;
    line-height: 1;
    cursor: grab;
    user-select: none;
    -webkit-user-select: none;
    overflow: hidden;
    /* Real-header interaction states: smooth bg/shadow transitions
       so hover, focus, drag are all visually distinct events. */
    transition: background var(--motion-fast) var(--ease),
                box-shadow var(--motion-fast) var(--ease),
                border-color var(--motion-fast) var(--ease);
}
.studio-table-group-band:hover {
    /* Stays in the slate-blue group family — just a slightly
       darker shade than the resting state — so hovering a group
       header doesn't suddenly recolor it as "selected". */
    background: var(--group-bg-hover);
    border-color: var(--group-border);
    box-shadow: 0 1px 2px rgba(15, 23, 42, 0.06);
}
.studio-table-group-band:active {
    cursor: grabbing;
    /* Pressed/dragging: shift to the brand accent so the user has
       an unmistakable "grabbed" cue. The hover→active transition
       reads as "in-flight action" rather than just hover-darker. */
    background: var(--accent);
    color: var(--accent-fg);
    border-color: var(--accent);
}
.studio-table-group-band:focus-visible {
    outline: 2px solid var(--accent-ring);
    outline-offset: -2px;
}
/* Group-band swap drop target — highlight the WHOLE target group band
   (accent ring + soft fill) so it reads as "drop here to swap with this
   group", matching the per-column .studio-th-drop highlight. The target
   group's column headers below light up via .studio-th-drop in tandem,
   so the entire group reads as the swap destination. Two-class
   specificity overrides the resting/hover band background. */
.studio-table-group-band.studio-band-drop {
    background: var(--accent-soft);
    border-color: var(--accent);
    box-shadow: inset 0 0 0 2px var(--accent);
    color: var(--accent-strong);
}
/* When a group is being actively dragged the band itself loses
   the cursor (pointer-capture moves with the cursor), so add an
   explicit "is being dragged" visual via the body class the
   pointer-event drag handler sets. */
body.studio-grip-dragging .studio-table-group-band {
    box-shadow: 0 2px 6px rgba(15, 23, 42, 0.10);
}

.studio-table-group-band-grip {
    /* Same 18px width as .studio-th-grip so this aligns with the
       first child column's grip below. */
    flex-shrink: 0;
    width: 18px;
    display: flex; align-items: center; justify-content: center;
    color: var(--group-fg);
    opacity: 0.55;
    cursor: grab;
}
.studio-table-group-band-grip svg {
    width: 8px; height: 14px;
    fill: currentColor; stroke: none;
}
.studio-table-group-band:hover .studio-table-group-band-grip { opacity: 1; }
.studio-table-group-band-name {
    flex: 1 1 0;
    min-width: 0;
    text-align: center;
    align-self: center;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    padding: 0 6px;
    line-height: 1;
}
.studio-table-group-band-menu {
    /* Same 22px width as .studio-header-menu-btn so this aligns
       with the last child column's menu chevron below. */
    flex-shrink: 0;
    width: 22px;
    display: flex; align-items: center; justify-content: center;
    border: none;
    background: transparent;
    color: var(--group-fg);
    cursor: pointer;
    padding: 0;
    transition: background 0.12s ease;
}
.studio-table-group-band-menu:hover { background: var(--group-bg-hover); }
.studio-table-group-band-menu svg {
    width: 12px; height: 12px;
    stroke: currentColor; fill: none;
}

/* Group action dropdown — uses the same .studio-header-menu base
   visuals as the column header dropdown, but with a tighter
   min-width since the menu only carries two items (Rename group,
   Ungroup). The header-menu's default 200px left a noticeable
   right-side whitespace gutter past the labels; 160 px hugs the
   text + icons closer without feeling cramped. The class is
   ALWAYS combined with .studio-header-menu in the JSX, so this
   rule wins on source-order against header-menu's own min-width
   (same specificity, declared later). */
.studio-group-menu {
    min-width: 160px;
}

/* ── Column-bar group card ────────────────────────────────────────
   Group cards sit interleaved with regular column rows in the
   ColumnsDrawer. They share most of the column-row structure
   (drag handle, eye toggle, name, ungroup button) so the visual
   hierarchy stays consistent — the differences are a chevron at
   the start (collapse/expand), an editable name input, and the
   group-only ungroup icon. */
.studio-column-group {
    display: flex; align-items: center; gap: 0.5rem;
    /* Padding-left MUST match `.studio-column-row` (0.5rem) so the
       leading slot (collapse / checkbox) starts at the same x on
       both card variants. The group card previously used 0.4rem
       which combined with the wider collapse button was pushing
       every downstream element ~9.6 px to the right of the column
       card's same-purpose elements. */
    /* Match the denser .studio-column-row vertical padding so group
       cards and column cards stay the same height. */
    padding: 0.4rem 0.625rem 0.4rem 0.5rem;
    margin-bottom: 4px;
    border-radius: var(--radius-md);
    border: 1px solid var(--accent);
    background: var(--accent-soft);
    cursor: grab;
}
.studio-column-group.studio-column-group-dragging { opacity: 0.4; }
.studio-column-group-collapse {
    /* `position: relative` is the anchor for the ::before hit-area
       extender below. Without it the pseudo would attach to the
       nearest positioned ancestor (the card) and grow far beyond
       the chevron. */
    position: relative;
    flex-shrink: 0;
    /* 14 × 14 visual size — matches `.studio-col-check` exactly so
       the slot occupied by the collapse chevron on a group card is
       the same width as the slot occupied by the row checkbox on
       a column card. With this width parity AND matching
       `gap: 0.5rem` on the parent, the next flex item (the drag-
       handle dots) lands at the same x on every card row, and so
       do every subsequent item: eye / hide button, info column,
       action slot. */
    width: 14px; height: 14px;
    border: none;
    background: transparent;
    color: var(--accent-strong);
    border-radius: var(--radius-sm);
    cursor: pointer;
    display: inline-flex; align-items: center; justify-content: center;
    padding: 0;
}
.studio-column-group-collapse:hover { background: var(--accent-muted); }
/* Shrink the chevron SVG so it fits inside the 14 × 14 button with
   a 2-px breathing margin on each side. The previous 12 × 12 SVG
   would have crowded the new smaller button. */
.studio-column-group-collapse svg {
    width: 10px; height: 10px;
}
/* Hit-area extender. The button's VISIBLE size and LAYOUT slot
   stay 14 × 14, but a transparent absolute-positioned ::before
   pseudo widens the actual click region to 26 × 26 (14 + 6 px
   padding on each side). The pseudo is a child of the button,
   so any click that lands on it bubbles to the button's onClick
   exactly as a click on the chevron itself would. The 6-px
   extension is well clear of the next flex item (the drag-handle
   dots, which start 8 px after the button's right edge thanks to
   the parent's `gap: 0.5rem`), so the extended hit area never
   intercepts clicks meant for the handle, and the visible
   chevron + hover background remain at the original 14 × 14
   footprint. */
.studio-column-group-collapse::before {
    content: '';
    position: absolute;
    inset: -6px;
}
.studio-column-group-name {
    flex: 1; min-width: 0;
    border: 1px solid transparent;
    background: transparent;
    color: var(--accent-strong);
    font-size: 0.85rem; font-weight: 600;
    padding: 2px 4px;
    border-radius: var(--radius-sm);
    outline: none;
}
.studio-column-group-name:hover {
    border-color: var(--accent-ring);
    background: var(--surface);
}
.studio-column-group-name:focus {
    border-color: var(--accent);
    background: var(--surface);
}
/* Group-card name↔meta rhythm. The rename INPUT carries padding + a
   (hover/focus) border that a plain column-name span doesn't, which pushed
   the "group · N cols" meta line noticeably lower than on regular column
   cards. Trim the input's vertical padding by 1px and pull the meta up 1px
   so the two lines sit as snug as a normal card's name/meta — without the
   negative margin ever reaching far enough to clip the input's focus ring. */
.studio-column-group .studio-column-group-name {
    padding-top: 1px;
    padding-bottom: 1px;
}
.studio-column-group .studio-column-meta {
    margin-top: -1px;
    /* Left-align the "group · N cols" meta with the rename INPUT's TEXT, not
       its box. The input insets its text by its border (1px) + left padding
       (4px) = 5px; the meta has no such inset, so without this it sat 5px to
       the LEFT of the name. Matching the inset lines the two up. */
    padding-left: 5px;
}
/* Indented column row when the column belongs to a group. The
   indent + accent left bar visually link members back to the
   group card above them. */
.studio-column-row.studio-column-row-in-group {
    margin-left: 16px;
    border-left: 2px solid var(--accent);
}

/* (Drag-to-select cell-range styling removed — feature reverted.) */
.studio-tab-scroll {
    flex-shrink: 0;
    width: 28px;
    border: none;
    border-radius: var(--radius-sm);
    background: transparent;
    color: var(--text-muted);
    display: flex; align-items: center; justify-content: center;
    cursor: pointer;
    transition: background var(--motion-fast) var(--ease),
                color var(--motion-fast) var(--ease);
}
.studio-tab-scroll:hover:not(:disabled) { background: var(--surface); color: var(--accent); }
.studio-tab-scroll:disabled { opacity: 0.35; cursor: not-allowed; }
.studio-tab-scroll svg { width: 14px; height: 14px; stroke: currentColor; fill: none; }

.studio-tabs-strip {
    flex: 1; min-width: 0;
    display: flex;
    gap: 8px;                 /* compact but visibly separated */
    align-items: stretch;
    overflow-x: auto;
    scroll-behavior: smooth;
    scrollbar-width: none;
}
.studio-tabs-strip::-webkit-scrollbar { display: none; }
.studio-tabs-empty {
    color: var(--text-faint); font-size: 0.85rem;
    padding: 0 0.625rem; white-space: nowrap;
    display: flex; align-items: center;
}

.studio-tab {
    display: inline-flex; align-items: center; gap: 0.25rem;
    padding: 0 0.25rem 0 0.75rem;     /* tighter right side, no extra blank after the chevron */
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    color: var(--text);
    font-size: 0.85rem; font-weight: 500;
    cursor: pointer; white-space: nowrap;
    flex-shrink: 0; max-width: 220px;
    transition: background var(--motion-fast) var(--ease),
                color var(--motion-fast) var(--ease),
                border-color var(--motion-fast) var(--ease),
                box-shadow var(--motion-fast) var(--ease);
}
.studio-tab:hover {
    background: var(--surface);
    color: var(--text-strong);
    border-color: var(--border-strong);
}
.studio-tab.active {
    background: var(--accent-soft);
    color: var(--accent-strong);
    font-weight: 600;
    border-color: var(--accent);
    box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
}
/* Swap drop target — highlight the WHOLE tab (full inset ring + soft
   fill) so it reads as "drop here to swap with this tab", not as an
   insertion line on one edge. */
.studio-tab-drop {
    box-shadow: inset 0 0 0 2px var(--accent);
    background: var(--accent-soft) !important;
}
/* (.studio-tab-drag inner wrapper rules removed — the tab is back
   to a draggable-on-the-outer-div structure that worked before
   the multi-round dropdown debugging. The chevron and close
   buttons are still children of the draggable outer div but
   carry their own onClick + draggable={false}, which has been
   the working pattern from the start.) */
.studio-tab-name {
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    max-width: 170px;
}
.studio-tab-lock {
    flex-shrink: 0;
    width: 13px; height: 13px;
    margin-right: 4px;
    color: var(--accent);
    opacity: 0.85;
}
/* Lock badge on a sheet card in the Design Sheets selector. */
.studio-modal-lock {
    flex-shrink: 0;
    width: 13px; height: 13px;
    margin-left: 0.4rem;
    color: var(--accent);
    opacity: 0.8;
}
.studio-tab-menu-btn {
    flex-shrink: 0;
    width: 20px; height: 20px;
    border: none; border-radius: 4px;
    background: transparent;
    color: var(--text-faint);
    display: flex; align-items: center; justify-content: center;
    cursor: pointer;
    transition: background var(--motion-fast) var(--ease),
                color var(--motion-fast) var(--ease);
}
.studio-tab-menu-btn:hover { background: rgba(15, 23, 42, 0.06); color: var(--text); }
.studio-tab.active .studio-tab-menu-btn { color: var(--text-muted); }
.studio-tab-menu-btn svg { width: 13px; height: 13px; stroke: currentColor; fill: none; }

/* Inactive tab close button — same geometry as the menu button so
   tab widths don't jump when the active tab swaps. Hover reads as
   destructive (danger soft / strong) so the user understands the
   click closes the tab. The button stops click propagation so the
   parent .studio-tab onClick (which would otherwise activate the tab)
   never fires. */
.studio-tab-close-btn {
    flex-shrink: 0;
    width: 20px; height: 20px;
    border: none; border-radius: 4px;
    background: transparent;
    color: var(--text-faint);
    display: flex; align-items: center; justify-content: center;
    cursor: pointer;
    transition: background var(--motion-fast) var(--ease),
                color var(--motion-fast) var(--ease);
}
.studio-tab-close-btn:hover {
    background: var(--danger-soft);
    color: var(--danger);
}
.studio-tab-close-btn:focus-visible {
    outline: none;
    box-shadow: 0 0 0 2px var(--accent-ring);
}

/* Tab dropdown — anchored below the tab via .studio-tab { position: relative } */
.studio-tab { position: relative; }
.studio-tab-menu {
    position: absolute;
    top: calc(100% + 6px);
    right: 0;
    min-width: 200px;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-3);
    padding: 4px;
    /* Portaled to document.body. The Alma Studio host (.app-studio-host
       in app.jsx) wraps the whole page in `position: fixed; z-index: 100`
       so it can stay mounted while hidden — that wrapper creates a
       stacking context. The tab menu sits OUTSIDE that wrapper (it
       portals to body), so its z-index competes with the wrapper's 100
       in the body's stacking context. Anything ≤100 renders BEHIND the
       Studio chrome and looks invisible. Match the other portaled
       menus (.studio-context-menu, .studio-header-menu) at 200. */
    z-index: 200;
    animation: slideUpFade 0.14s var(--ease);
}
.studio-tab-menu-item {
    display: flex; align-items: center; gap: 0.5rem;
    width: 100%;
    padding: 0.4rem 0.625rem;
    background: transparent; border: none;
    border-radius: var(--radius-sm);
    color: var(--text);
    font-size: 0.85rem; font-weight: 500;
    text-align: left; cursor: pointer; white-space: nowrap;
    transition: background var(--motion-fast) var(--ease),
                color var(--motion-fast) var(--ease);
}
.studio-tab-menu-item:hover { background: var(--hover-bg); color: var(--hover-fg); }
.studio-tab-menu-item.danger { color: var(--danger-strong); }
.studio-tab-menu-item.danger:hover { background: var(--danger-bg); color: var(--danger-strong); }
/* Context-menu icons read as small visual marks, not feature buttons.
   Muted by default; pick up the row's color on hover so they reinforce
   the active state without competing with the label. */
.studio-tab-menu-item svg {
    width: 14px; height: 14px;
    stroke: currentColor; fill: none;
    flex-shrink: 0;
    color: var(--text-muted);
    opacity: 0.85;
    transition: color var(--motion-fast) var(--ease), opacity var(--motion-fast) var(--ease);
}
.studio-tab-menu-item:hover svg { color: inherit; opacity: 1; }
.studio-tab-menu-item.danger svg { color: var(--danger-strong); opacity: 0.85; }
.studio-tab-menu-item.danger:hover svg { opacity: 1; }
.studio-tab-menu-item-disabled svg { color: var(--text-faint); opacity: 0.55; }
.studio-tab-menu-divider { height: 1px; background: var(--border-faint); margin: 4px 6px; }

/* Per-folder action menu (Design Sheets tree) — reuses the .studio-tab-menu
   look, but is portaled OVER the modal overlay (z-index 1000), so it needs a
   higher z-index than the shared menu's 200. The transparent backdrop just
   captures the outside click that dismisses it. */
.studio-folder-menu-backdrop { position: fixed; inset: 0; z-index: 1100; background: transparent; }
/* Tighter than the 200px shared tab-menu: the folder actions are short labels,
   so hug them (items stay nowrap + comfortably padded, just less dead space on
   the right, e.g. after "New subfolder"). */
.studio-folder-menu { z-index: 1101; min-width: 8.5rem; }

/* Content */
.studio-content {
    /* min-width: 0 lets the column shrink when a drawer opens; without
       it the table's intrinsic width pins the workspace and the drawer
       can't push the content. */
    flex: 1 1 0%;
    min-width: 0;
    min-height: 0;
    display: flex; flex-direction: column;
    padding: 1rem 1.125rem 1.125rem;
    overflow: hidden;
}

/* Empty states */
.studio-empty {
    flex: 1;
    display: flex; flex-direction: column;
    align-items: center; justify-content: center;
    text-align: center; padding: 2.5rem 1.5rem;
    color: var(--text-muted);
}
.studio-empty-icon {
    width: 64px; height: 64px;
    border-radius: var(--radius-md);
    background: var(--accent-soft);
    border: 1px solid var(--info-border);
    display: flex; align-items: center; justify-content: center;
    margin-bottom: 1rem;
}
.studio-empty-icon svg { width: 28px; height: 28px; stroke: var(--accent); fill: none; }
.studio-empty-title {
    font-size: 1.05rem; font-weight: 700; color: var(--text-strong);
    margin-bottom: 0.25rem; letter-spacing: -0.01em;
}
.studio-empty-sub { font-size: 0.9rem; color: var(--text-muted); max-width: 380px; }
.studio-empty-actions {
    display: flex;
    gap: 0.5rem;
    margin-top: 1rem;
    flex-wrap: wrap;
    justify-content: center;
}

/* ── Compound table ─────────────────────────────────────────── */
.studio-table-wrap {
    flex: 1; min-height: 0;
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-md);
    /* Subtle off-white canvas with a faint dot grid: when the table is
       narrower than the wrap, the empty right-side area shows a quiet
       grid pattern instead of a stark white void. The table itself sits
       on opaque white, so the pattern only shows in the empty area. */
    background-color: var(--surface-soft);
    background-image: radial-gradient(circle, var(--border) 1px, transparent 1px);
    background-size: 18px 18px;
    overflow: auto;
    box-shadow: var(--shadow-2);
}
.studio-table { background: var(--surface); }
.studio-table {
    /* `min-width: max-content` PREVENTS the table from compressing
       when the surrounding layout shrinks (drawer opens, browser
       narrows, etc.) — without it Chrome / Firefox squeeze table
       cells to fit the container. The wrap then shows a horizontal
       scrollbar.

       `width: 100%` makes the table fill the wrap when it would
       otherwise be narrower than the wrap. Combined with the
       trailing auto-width spacer column in <colgroup>, this lets
       the spacer absorb the leftover space so the table always
       reaches the right edge of the viewport. When the sum of
       fixed <col> widths exceeds the wrap, max-content takes over
       and the spacer collapses to 0. */
    width: 100%;
    min-width: max-content;
    table-layout: fixed;
    border-collapse: separate;
    border-spacing: 0;
    font-size: 0.875rem;
    color: var(--text);
}
/* Trailing spacer column. The <col> in colgroup has no width set
   so the table layout assigns it whatever slack is left after every
   sized column has taken its share. Its <th> and <td> have no
   borders / no padding so the cell reads as plain background — the
   row's hover / selected / frozen colors still apply (we set them
   explicitly here so the highlight band is continuous). */
.studio-table .studio-th-spacer {
    background: var(--surface-soft);
    border-right: 0;
    border-bottom: 1px solid var(--border-strong);
    padding: 0;
    /* Keeps the sticky header band the same height as the data
       headers when the spacer is non-zero. */
    min-width: 0;
}
.studio-table .studio-td-spacer {
    background: var(--surface);
    border-right: 0;
    border-bottom: 1px solid var(--border);
    padding: 0;
    min-width: 0;
}
/* Row-state backgrounds need to extend through the spacer so the
   highlight band stays continuous to the right edge. */
.studio-table tbody tr:hover td.studio-td-spacer { background: var(--surface-soft); }
.studio-table tbody tr.studio-row-selected td.studio-td-spacer { background: var(--surface-soft); }

/* Column-virtualization spacer cells. They occupy the horizontal
   real estate of the un-rendered windowed columns. Their widths are
   driven by the matching <col> in colgroup; the cells themselves
   are layout-only and visually inherit the row's background so the
   hover / selected / frozen highlight bands stay continuous across
   the spacer region (otherwise the un-rendered range would read as
   a gap in the highlight). No padding / no border on either axis.
   pointer-events: none so the spacer can't intercept clicks meant
   for the surrounding cells. */
.studio-table .studio-th-virt-spacer {
    background: var(--surface-soft);
    border-right: 0;
    border-bottom: 1px solid var(--border-strong);
    padding: 0;
    pointer-events: none;
}
.studio-table .studio-td-virt-spacer {
    background: var(--surface);
    border-right: 0;
    border-bottom: 1px solid var(--border);
    padding: 0;
    pointer-events: none;
}
.studio-table tbody tr:hover td.studio-td-virt-spacer { background: var(--surface-soft); }
.studio-table tbody tr.studio-row-selected td.studio-td-virt-spacer { background: var(--surface-soft); }
.studio-table thead th {
    position: sticky; top: 0;
    z-index: 4;
    background: var(--surface-soft);
    color: var(--text-muted);
    font-size: 0.82rem; font-weight: 600;
    text-align: center;
    padding: 0.7rem 0.875rem;
    border-bottom: 1px solid var(--border-strong);
    white-space: nowrap;
    /* When the user resizes the header row, the thead inline
       --header-h cascades into every th. The cell uses it as a
       hard height so the user-customised value wins over the
       natural padding-driven height. */
    height: var(--header-h, auto);
    /* Disable text-selection on header — without this, dragging from the
       padding area would start a text selection instead of an HTML5 drag,
       making the surrounding header area feel non-draggable. */
    user-select: none;
    -webkit-user-select: none;
}
/* Center body cells too. Specific cells (row-no, checkbox, structure)
   keep their own centering via their wrappers; long-text cells truncate
   with ellipsis from center. */
.studio-table tbody td { text-align: center; }
/* Notes / custom cells display: centered text, ellipsis on overflow.
   Edit-mode inputs use natural left text alignment for typing comfort.
   The full `.studio-cell-editable` block (with line-clamp / wrap) is
   declared below near the cell-cursor rules; here we just align it. */
.studio-cell-notes-wrap, .studio-cell-custom-wrap { text-align: center; }
.studio-cell-edit { text-align: left; }
.studio-table tbody td {
    padding: 0.65rem 0.875rem;
    border-bottom: 1px solid var(--border);     /* horizontal grid */
    border-right: 1px solid var(--border);      /* vertical grid */
    vertical-align: middle;
    /* Content must never push a column wider than its colgroup width.
       Combined with table-layout: fixed, this guarantees the column
       width is the only width source — every column shrinks to MIN_COL
       cleanly. */
    overflow: hidden;
    box-sizing: border-box;
}
/* ── Strict row-height enforcement during column virtualization ──
   Scoped to .studio-table-col-virt so non-virt sheets keep the
   pre-existing auto-stretch behaviour for multi-line content.

   Why the layered approach below: per CSS 2.1, on table cells
   `height` is treated as a MINIMUM and `max-height` is unreliably
   honoured (the spec explicitly says cell heights "can be increased
   to accommodate cell content"). Setting them on the td alone — what
   the previous fix tried — works in some browsers and not others,
   so the row could still stretch when a freshly-mounted virtualized
   column landed taller content than what was already rendered.

   The reliable fix is to constrain the cell's INNER WRAPPER (the
   block-level child of the td) to a height that, plus the td's
   padding, is exactly var(--row-h). The td is then content-driven
   to exactly --row-h regardless of what cells happen to be in the
   window. Each density publishes its td vertical padding via
   `--td-pad-y` (declared on the density classes above), so the
   cap formula `calc(var(--row-h) - var(--td-pad-y) * 2)` is
   accurate across compact / comfortable / spacious / custom.

   We still keep the td-level height + max-height + box-sizing as
   defence-in-depth — browsers that DO honour td.max-height pick
   them up and we get a second guarantee. */
.studio-table.studio-table-col-virt tbody td {
    height: var(--row-h, 56px);
    max-height: var(--row-h, 56px);
}
/* The load-bearing rule, v3. Targets each known data-cell wrapper
   class explicitly rather than using `td > *`, which previously
   matched non-content children too — the row-resize handle in the
   row-number cell, the absolute-positioned pencil button in Notes
   cells, the inner svg in StructureCell — and clamped them to the
   row-height calc. Targeting specific classes keeps the cap on
   actual cell content while leaving siblings (interactive handles,
   icons) at their declared sizes.

   Strict `height` (not max-height) so every wrapper renders at
   the EXACT same height regardless of its content. Uniform heights
   are what eliminates the small visible jitter that survived v2 —
   under max-height-only, short content cells (e.g. a single number)
   were ~19 px tall while wrapped Notes were ~35 px, and as those
   cells mounted/unmounted in different combinations during
   horizontal scroll, the row's max-cell-height varied within a
   sub-pixel band. Strict height removes that variance entirely.

   --td-pad-y is in PIXELS (declared on each density above) so the
   calc evaluates to integer pixels — no sub-pixel rounding from
   rem-to-px conversion when virt cols mount/unmount.

   This rule does NOT set `overflow`. The base per-class rules
   (.studio-cell-clickable etc.) keep `overflow-y: hidden` as the
   default, and the runtime overflow-measurement scheduler (see
   _flushOverflowMeasurements) adds `.is-overflowing-y` to cells
   whose content exceeds the cap, flipping that cell's overflow-y
   to `auto`. With the strict height bounding the cap, the JS
   toggle correctly sees an overflow when content is taller than
   (--row-h - 2 * --td-pad-y) and the user gets a real in-cell
   scrollbar — restoring the pre-v1 behaviour for long Notes /
   longtext / wrapped content that v1's `overflow: hidden !important`
   had blocked. */
/* `td > .studio-cell-clickable` (direct child) is deliberate. The
   bare descendant selector used to match the inner copyable span
   nested inside `.studio-cell-smiles-inner` too, which forced that
   inner span to the same strict row-h height. That was the source
   of the SMILES double-scrollbar in col-virt / wide-CSV imports —
   both the outer wrapper and the inner span had a strict height
   plus the runtime overflow detector toggling `.is-overflowing-y`
   on each, so two stacked overflow:auto scrollbars rendered.
   Direct-child match leaves the SMILES inner span content-sized,
   so only the wrapper can scroll. */
.studio-table.studio-table-col-virt tbody td > .studio-cell-clickable,
.studio-table.studio-table-col-virt tbody td > .studio-cell-smiles-inner,
.studio-table.studio-table-col-virt tbody td > .studio-cell-notes,
.studio-table.studio-table-col-virt tbody td > .studio-cell-editable {
    height: calc(var(--row-h, 56px) - var(--td-pad-y, 10px) * 2) !important;
    max-height: calc(var(--row-h, 56px) - var(--td-pad-y, 10px) * 2) !important;
    box-sizing: border-box;
}
/* Vertically centre cell text within the strict-height box.
   `td > .studio-cell-clickable` deliberately matches ONLY the
   wrapper that's a direct child of the td — the inner
   `.studio-cell-clickable` nested inside `.studio-cell-smiles-inner`
   is reached as a grandchild and is left alone, preserving the
   SMILES inline-flow scroll/wrap behaviour. */
.studio-table.studio-table-col-virt tbody td > .studio-cell-clickable,
.studio-table.studio-table-col-virt tbody td > .studio-cell-notes,
.studio-table.studio-table-col-virt tbody td > .studio-cell-editable {
    display: flex;
    flex-direction: column;
    justify-content: center;
}
/* Same vertical-centring for non-virt sheets — the td's
   `vertical-align: middle` already centres single-line content,
   but multi-line wrapped values used to ride the top of the
   cell. Flex column with justify-content: center fixes that and
   leaves one-line content visually identical to before.
   Specificity: kept at single-class so the existing
   `.studio-cell-smiles-inner .studio-cell-clickable { display: inline }`
   rule (line 2225 area) still wins inside SMILES cells. */
.studio-cell-clickable,
.studio-cell-notes,
.studio-cell-editable {
    display: flex;
    flex-direction: column;
    justify-content: center;
}
/* studio-cell-bulk-host (used by CustomCell + NotesCell editing)
   doesn't have a JS-toggled is-overflowing class like the simpler
   wrappers do, so it gets its own `overflow: auto` + scrollbar
   gutter for in-cell scrolling. Strict height for jitter
   stability matches the rule above. */
.studio-table.studio-table-col-virt .studio-cell-bulk-host {
    height: calc(var(--row-h, 56px) - var(--td-pad-y, 10px) * 2) !important;
    max-height: calc(var(--row-h, 56px) - var(--td-pad-y, 10px) * 2) !important;
    overflow: auto;
    box-sizing: border-box;
    /* scrollbar-gutter reserves space for the scrollbar even when
       it's not shown, so the cell's interior width can't jitter
       when content grows/shrinks across the scrollable threshold. */
    scrollbar-gutter: stable;
    scrollbar-width: thin;
    scrollbar-color: var(--border-strong) transparent;
}
/* …but a dropdown / boolean bulk-host holds a single SHORT control that can
   never overflow the row height, so the scroll treatment above is not just
   unnecessary, it's harmful: `scrollbar-gutter: stable` permanently reserves
   a right-edge gutter, which narrows the content box and pushes the full-width
   dropdown trigger (and its centered value + focus/open ring) visibly LEFT.
   That left-shift only surfaced once enough columns (a full property group)
   flipped the table into column-virtualization. Only the EDITING variant (a
   textarea that genuinely scrolls) keeps the gutter. Higher specificity than
   the rule above (extra :not()) so overflow/gutter win without !important. */
.studio-table.studio-table-col-virt .studio-cell-bulk-host:not(.studio-cell-bulk-host-editing) {
    overflow: visible;
    scrollbar-gutter: auto;
}
/* EDITING host alignment in col-virt sheets.
   The editing host is clamped to the strict row-height box (rule above) and
   was left top-aligned (justify-content: normal) + scrollbar-gutter: stable so
   a TALL longtext textarea could scroll without its top being clipped. But for
   the COMMON single-line editor (text / number / date), that combination made
   the input sit ABOVE and LEFT of centre — i.e. shifted upper-left versus the
   centered display value it replaces (the display wrapper uses
   justify-content: center). Two scoped corrections, both general (no hardcoded
   offset):
     · Drop the reserved right gutter — a single-line input never scrolls, so
       reserving it only narrowed + left-shifted the input. (A genuinely
       overflowing textarea still shows a scrollbar; it just isn't pre-reserved.)
     · Centre the editor with AUTO block margins instead of justify-content.
       Auto margins centre the child when there's free space (single-line input
       → vertically centred, matching the display) AND collapse to 0 when the
       child is taller than the box (longtext textarea → top-anchored and
       scrollable, never top-clipped — which justify-content: center WOULD
       clip). So both cases stay correct. */
.studio-table.studio-table-col-virt .studio-cell-bulk-host-editing {
    scrollbar-gutter: auto;
}
.studio-table.studio-table-col-virt .studio-cell-bulk-host-editing > .studio-cell-edit {
    margin-block: auto;
}
/* Virt-spacer cells (the col-virtualization placeholders that
   absorb the unrendered range's horizontal width) defensively
   bound to the row height too. They have no content, so this is
   belt-and-suspenders against any browser quirk where a freshly
   mounted spacer cell briefly forces a layout reflow that
   changes intrinsic row height. */
.studio-table.studio-table-col-virt tbody td.studio-td-virt-spacer {
    height: var(--row-h, 56px);
    max-height: var(--row-h, 56px);
    box-sizing: border-box;
    padding: 0;
}
/* The structure cell uses `padding: 2px` (overrides the density
   td padding) and the inner StructureCell sets its own height to
   `calc(var(--row-h) - 6px)` already — that already accounts for
   2px*2 padding + 2px borders, so it's well within the row cap.
   Reset --td-pad-y on the structure cell so the generic td > *
   rule above doesn't double-subtract. */
.studio-table.studio-table-col-virt tbody td.studio-cell-structure {
    --td-pad-y: 2px;
}
.studio-table tbody td > * { max-width: 100%; }
/* Structure td: minimal padding so the structure-cell fills the column
   visible area; explicit height ensures content can't push the row taller. */
.studio-table tbody td.studio-cell-structure { padding: 2px; }
.studio-table thead th {
    border-right: 1px solid var(--border-strong);
    border-bottom: 1px solid var(--border-strong);
}
/* Header-height resize handle — sits at the bottom edge of the
   top-left corner th, mirroring the per-row resize affordance
   that lives on every data row's row-number td. Reuses the
   .studio-row-resize sizing and cursor; the .studio-header-resize-handle
   modifier exists so we can target the header-only state for
   active-drag styling without affecting the per-row handles.
   The thead th is `position: sticky` (declared earlier), which
   already establishes a positioned ancestor for the absolute
   child — no `position: relative` override needed. The first
   data row sits flush with the header; no separate resize row,
   no blank gap. */
.studio-row-resize.studio-header-resize-handle:hover {
    background: linear-gradient(to bottom, transparent 0, transparent 2px, var(--accent) 2px, var(--accent) 4px, transparent 4px);
}
body.studio-row-resizing .studio-row-resize.studio-header-resize-handle {
    background: linear-gradient(to bottom, transparent 0, transparent 2px, var(--accent) 2px, var(--accent) 4px, transparent 4px);
}
/* Last cell keeps its right border so the grid is visually closed on the
   right side — without this rule the table looked open-ended. */
.studio-table tbody tr:hover td { background: var(--surface-soft); }
/* Last-row bottom border now visible (was previously suppressed). */
.studio-table tbody tr:last-child td { border-bottom: 1px solid var(--border); }

/* ── Spreadsheet-style drag-select prevention ──────────────────────
   The sheet grid behaves like Excel: holding the left mouse button
   and dragging across cells should NOT paint the browser's blue
   text-selection highlight over cell values. App-level handlers
   (active cell, row checkboxes, column resize, header click, etc.)
   own the click+drag semantics; the browser's native text selection
   would just compete with them and leave stray highlights when the
   user drags across a few cells.

   Scope: only the body cells. Headers already have user-select:none
   from their own rules (avoids text-select fights with the column-
   drag/grip handles). Editable controls inside body cells re-enable
   text selection so cell editing, BulkSaveBar, and any inline
   draft can still be selected, copied, and edited normally.

   Filter / search / modal / drawer inputs are OUTSIDE .studio-table,
   so they're untouched by these rules. */
.studio-table tbody td {
    user-select: none;
    -webkit-user-select: none;
}
.studio-table tbody td input,
.studio-table tbody td textarea,
.studio-table tbody td select,
.studio-table tbody td .studio-cell-edit,
.studio-table tbody td [contenteditable="true"],
.studio-table tbody td [contenteditable=""] {
    user-select: text;
    -webkit-user-select: text;
}

.studio-cell-strong { color: var(--text-strong); font-weight: 500; }

/* ── Wrap-then-conditional-scroll pattern for body text cells ─────
   Cells default to `overflow: hidden` so no scrollbar paints when
   the content fits. The current content-driven detector — a per-row
   useLayoutEffect that batch-measures scrollHeight / scrollWidth via
   a shared rAF — toggles `.is-overflowing-y` / `.is-overflowing-x`
   on cells whose content exceeds the cell box. Those classes are
   defined further down (search "Content-driven overflow scrolling")
   and flip overflow on the appropriate axis only.

   The single-axis legacy `.is-overflowing` (no -y / -x suffix) hooks
   below are NOT toggled by current code; they remain as a benign
   opt-in surface for any future JS that wants both axes at once.
   Removing them carries no immediate benefit and risks breaking any
   downstream integration that reads them.

   max-height = var(--row-h) caps the inner box at the row height;
   the parent td's `overflow: hidden` clips anything that escapes the
   td's content area so the row never grows. */

.studio-cell-smiles {
    /* Width is owned by <colgroup>; the td clips overflow so very
       narrow columns can't push the row taller. */
    color: var(--text);
    overflow: hidden;
}
.studio-cell-smiles-inner {
    /* SMILES vertical centring: the wrapper fills the td's content
       area (`row-h - 2*td-pad-y`) in BOTH virt and non-virt modes,
       and uses `display: flex` (row direction) + `align-items: center`
       to vertically centre the single block child. align-items on
       a row-direction flex container centres on the cross axis
       (vertical) without depending on inline-blockification edge
       cases that broke the previous flex-column attempt for
       imported SMILES on some browsers.

       Wrapping: the inner `.studio-cell-clickable` (forced to
       `display: block; width: 100%` below) keeps full width on the
       main axis, so the inherited `pre-wrap; word-break: break-all;
       overflow-wrap: anywhere` rules drive multi-line wrapping
       exactly as before.

       Overflow: long wrapped SMILES is clipped equally from top and
       bottom by `overflow-y: hidden` (flex centring positions the
       content around the centre line). When `.is-overflowing` is
       toggled by the JS overflow detector, the rule below switches
       to `overflow-y: auto` so the full content is scrollable. */
    display: flex;
    align-items: center;
    width: 100%;
    min-width: 0;
    height: calc(var(--row-h, 56px) - var(--td-pad-y, 10px) * 2);
    max-height: calc(var(--row-h, 56px) - var(--td-pad-y, 10px) * 2);
    overflow-y: hidden;
    overflow-x: hidden;
    white-space: pre-wrap;
    overflow-wrap: anywhere;
    word-break: break-all;
    line-height: 1.4;
    padding: 4px 6px;
    box-sizing: border-box;
}
.studio-cell-smiles-inner.is-overflowing {
    overflow-y: auto;
    scrollbar-width: thin;
    scrollbar-color: var(--border-strong) transparent;
}
/* The inner copyable span used to be `display: inline` so SMILES text
   could flow inline within the wrapper. With the wrapper now a flex
   column container, an explicit `display: block` makes the span a
   well-defined block flex item that takes the full cross-axis width
   and is positioned by `justify-content: center` on the wrapper. Text
   wrapping still happens inside the block via the inherited
   `white-space: pre-wrap; word-break: break-all; overflow-wrap: anywhere`
   rules — visually identical to the previous inline behaviour, but
   centring is reliable across browsers (no dependence on flex's
   inline-blockification edge cases). */
.studio-cell-smiles-inner .studio-cell-clickable {
    display: block;
    width: 100%;
    /* The .studio-cell-clickable rule further down sets its own
       padding (4px 6px), max-height, and overflow rules. The wrapper
       already enforces those, so neutralise the inner padding here
       to avoid doubling up vertical padding which would push the
       text off-centre. */
    padding: 0;
    max-height: none;
    overflow: visible;
    line-height: inherit;
}
.studio-num { text-align: right; font-variant-numeric: tabular-nums; }
.studio-cell-notes {
    /* Hidden by default (same rationale as .studio-cell-clickable
       above). `pre-wrap` preserves explicit newlines saved with the
       value. The td's title attribute and the cell's edit affordance
       expose the full text without a persistent scrollbar. Vertical
       padding consistent with .studio-cell-clickable. */
    margin: 0 auto;
    color: var(--text-muted); font-size: 0.85rem;
    max-width: 100%;
    text-align: center;
    display: block;
    max-height: var(--row-h, 56px);
    overflow-y: hidden;
    overflow-x: hidden;
    white-space: pre-wrap;
    overflow-wrap: anywhere;
    word-break: break-word;
    line-height: 1.4;
    padding: 2px 4px;
    box-sizing: border-box;
}
.studio-cell-notes.is-overflowing {
    overflow-y: auto;
    scrollbar-width: thin;
    scrollbar-color: var(--border-strong) transparent;
}

/* ── Content-driven overflow scrolling ─────────────────────────
   Scrollbars now appear based on whether each cell's content
   actually overflows its current box, not on column-width
   thresholds. The per-row useLayoutEffect in StudioRow queues
   visible cells into a shared rAF scheduler that batch-reads
   scrollHeight / scrollWidth and toggles is-overflowing-y /
   is-overflowing-x on the cells that need scroll. Non-
   overflowing cells stay on the default `overflow: hidden`, so
   the Chrome subpixel-rounding scrollbar regression cannot
   reappear (no cell has overflow:auto unless it actually
   needs it). Measurement happens once per cell mount + on
   layout-affecting changes (row data, column widths, row
   height, visible-column set) — never on scroll. */
.studio-cell-clickable.is-overflowing-y,
.studio-cell-editable.is-overflowing-y,
.studio-cell-smiles-inner.is-overflowing-y,
.studio-cell-notes.is-overflowing-y {
    overflow-y: auto;
    scrollbar-width: thin;
    scrollbar-color: var(--border-strong) transparent;
}
.studio-cell-clickable.is-overflowing-x,
.studio-cell-editable.is-overflowing-x,
.studio-cell-smiles-inner.is-overflowing-x,
.studio-cell-notes.is-overflowing-x {
    overflow-x: auto;
    scrollbar-width: thin;
    scrollbar-color: var(--border-strong) transparent;
}
/* Top-align a CENTERED cell once it becomes vertically scrollable so
   scrollTop:0 reveals the FIRST line. A flex container that centers its
   content positions OVERFLOWING content around its midline, pushing the
   top above the scroll origin where it can never be reached (the
   reported "first line cut off above the cell" bug). Only the VERTICAL-
   alignment property is switched, per flex direction:
     · .studio-cell-smiles-inner is a ROW flex → vertical = align-items;
     · .studio-cell-clickable / -editable are COLUMN flex (flex-direction:
       column, justify-content: center) → vertical = justify-content.
   Horizontal alignment and the centered look for non-overflowing cells
   are untouched. Both the rAF detector's `is-overflowing-y` and the cell
   components' legacy `is-overflowing` are covered. (`.studio-cell-notes`
   is display:block — top-aligned already — so it needs no rule.) */
.studio-cell-smiles-inner.is-overflowing-y,
.studio-cell-smiles-inner.is-overflowing {
    align-items: flex-start;
}
.studio-cell-clickable.is-overflowing-y,
.studio-cell-clickable.is-overflowing,
.studio-cell-editable.is-overflowing-y,
.studio-cell-editable.is-overflowing {
    justify-content: flex-start;
}
/* Column-virtualization mode re-centers these via a MORE specific
   selector (.studio-table-col-virt … > .studio-cell-clickable, 0-3-2),
   which would beat the rule above (0-2-0). Re-assert top-alignment at
   higher specificity (0-4-2) so the scroll-to-top fix also holds in
   virt mode. (smiles-inner isn't in that centering rule, so it needs
   no virt-specific override.) */
.studio-table.studio-table-col-virt tbody td > .studio-cell-clickable.is-overflowing-y,
.studio-table.studio-table-col-virt tbody td > .studio-cell-clickable.is-overflowing,
.studio-table.studio-table-col-virt tbody td > .studio-cell-editable.is-overflowing-y,
.studio-table.studio-table-col-virt tbody td > .studio-cell-editable.is-overflowing {
    justify-content: flex-start;
}

/* The is-narrow class on a td is now ONLY a wrap-mode hint for
   SMILES: at narrow widths SMILES switches to nowrap so a long
   value stays on one line and the overflow detector picks up the
   horizontal overflow. Normal-width SMILES still wrap (the
   un-narrow rule uses `word-break: break-all` + `pre-wrap`).
   Other cell types ignore is-narrow now — they wrap normally
   and the overflow detector decides whether vertical scroll is
   needed. */
.studio-table tbody td.is-narrow .studio-cell-smiles-inner {
    white-space: nowrap;
    word-break: normal;
}
/* Subtle webkit scrollbars — match the column-header label style.
   Webkit pseudo-elements only paint when a scrollbar is actually
   rendered, so these are inert when cells have overflow-y: hidden
   (the default state set by the per-cell rules above). */
.studio-cell-smiles-inner::-webkit-scrollbar,
.studio-cell-notes::-webkit-scrollbar,
.studio-cell-clickable::-webkit-scrollbar,
.studio-cell-editable::-webkit-scrollbar { width: 4px; }
.studio-cell-smiles-inner::-webkit-scrollbar-thumb,
.studio-cell-notes::-webkit-scrollbar-thumb,
.studio-cell-clickable::-webkit-scrollbar-thumb,
.studio-cell-editable::-webkit-scrollbar-thumb {
    background: var(--border-strong);
    border-radius: 2px;
}

/* Structure cell — explicit pixel height per density. This is the
   critical decoupling: the SVG inside has width: 100% (column width)
   but its visible height is bounded by .structure-cell's height, which
   is set in pixels per density. So widening the column never grows
   the row, because the cell height never grows from SVG content. */
.structure-cell {
    width: 100%;
    height: 36px;            /* default fallback (compact) */
    margin: 0;
    display: flex; align-items: center; justify-content: center;
    overflow: hidden;
    position: relative;
}
/* Per-density structure-cell height = matching tr inner area. The tr
   gets the same explicit height (above), so SVG <100%, 100%> of this
   cell fills the row's visible area without driving it taller. */
.studio-table-density-compact     .structure-cell { height: 30px; }
.studio-table-density-comfortable .structure-cell { height: 50px; }
.studio-table-density-spacious    .structure-cell { height: 88px; }
.studio-table-density-custom      .structure-cell { height: calc(var(--row-h, 56px) - 6px); }
/* SVG fits the cell exactly with preserveAspectRatio centering. The
   non-scaling-stroke rule (declared above) keeps bond widths constant. */
.structure-cell svg {
    width: 100%; height: 100%;
    max-width: 100%; max-height: 100%;
    display: block;
}
/* Structure cell scales naturally with column width × row height now —
   no per-density fixed pixel sizes are needed. The SVG inside fills the
   container and preserves aspect via its viewBox. */
.structure-cell svg { width: 100%; height: 100%; display: block; }
/* Clean empty state for a scaffold-structure cell with no scaffold
   (e.g. an acyclic compound has no Murcko scaffold) — a muted dash
   rather than a misleading molecule placeholder. */
.structure-cell-empty { display: flex; align-items: center; justify-content: center; }
.structure-cell-empty::after { content: '—'; color: var(--text-faint); font-size: 0.85rem; }
/* Keep bond stroke widths constant when the SVG is CSS-scaled to fit a
   larger row height. Without this, scaling up enlarges every stroke width
   too, making bonds look thick. */
.structure-cell svg path,
.structure-cell svg line,
.structure-cell svg polyline,
.structure-cell svg polygon,
.structure-cell svg circle,
.studio-structure-preview-svg svg path,
.studio-structure-preview-svg svg line,
.studio-structure-preview-svg svg polyline,
.studio-structure-preview-svg svg polygon,
.studio-structure-preview-svg svg circle {
    vector-effect: non-scaling-stroke;
}
/* Bond-line weight in SHEET structure cells only. The backend authors
   every render at bondLineWidth=1.5, and non-scaling-stroke (above) pins
   strokes to that authored px even when the SVG scales down to cell size —
   which reads slightly heavy at grid row heights. Thin just the stroked
   bond lines: RDKit emits them as `fill:none` paths, while atom-label
   glyphs, wedge bonds, and highlight bands are filled paths/ellipses that
   must keep their geometry. !important is required to beat the SVG's
   inline style attribute. Cliffs / Scaffold rail thumbs and the hover
   preview are NOT .structure-cell, so they keep the authored 1.5px. */
.structure-cell svg path[style*="fill:none"] {
    stroke-width: 1.2px !important;
}
.structure-cell-loading {
    background: var(--surface-sunk);
    /* Opacity-only pulse — compositor-friendly. The previous animated
       background-position gradient forced a full repaint every frame for
       each loading cell, which made scrolling a large sheet janky when many
       structure placeholders were visible at once. Opacity animates on the
       GPU and doesn't repaint, keeping scroll smooth with 5,000+ rows. */
    will-change: opacity;
    animation: structurePulse 1.4s ease-in-out infinite;
}
@keyframes structurePulse {
    0%, 100% { opacity: 0.5; }
    50% { opacity: 0.85; }
}
@media (prefers-reduced-motion: reduce) {
    .structure-cell-loading { animation: none; opacity: 0.7; }
}
/* Auto-managed virtual "Matched Structure" column — visually marked
   as derived/computed (subtle tinted backdrop, italic muted label)
   so it reads differently from real data columns. Header is
   reorderable (grip) and resizable (right-edge handle); no sort,
   no header menu, no edit/delete. */
.studio-table thead th.studio-th-virtual {
    background: var(--surface-soft);
}
.studio-table thead th.studio-th-virtual:hover {
    /* Override the .studio-table-sortable hover so the virtual column
       doesn't masquerade as a clickable sort target. The label keeps
       its muted styling on hover. */
    background: var(--surface-soft);
    color: var(--text-muted);
}
.studio-table thead th.studio-th-virtual .studio-table-th-label-text {
    font-style: italic;
    color: var(--text-muted);
}
/* The default .studio-th-content reserves 24 px on each side
   (grip on the left, dropdown arrow on the right) and centres the
   label with character-by-character wrapping. The virtual column
   has NO dropdown arrow, so reserving 24 px on the right is wasted
   space that pushed "Matched Structure 1" into a third wrap line
   the row height couldn't show. Slim the right padding down to
   what the resize handle actually needs, but otherwise inherit
   the rest of the centred-and-wrap behaviour from the default
   rule — the label stays centred and reflows the same way regular
   column headers do when the user resizes the column down. */
.studio-table thead th.studio-th-virtual .studio-th-content {
    padding: 4px 10px 4px 22px;
}
/* Pure white plate behind the matched molecule — keeps the light
   green highlight legible across themes (the previous tinted
   surface bled into the green and made it look murkier than it
   actually is). The thin border + radius keep the cell from looking
   like a hole punched in the row. */
.structure-cell-highlight {
    background: #ffffff;
    border-radius: 3px;
}
.studio-structure-preview-svg.is-highlight,
.studio-structure-preview.is-highlight .studio-structure-preview-svg {
    background: #ffffff;
}
/* Inverse-mode matched cell — empty on purpose (rows currently
   visible by definition do NOT contain the substructure, so there's
   nothing to highlight). A faint diagonal hatch reads as "intentionally
   empty" and lines up with the same surface tint the column header uses. */
.studio-cell-structure-inverted .structure-cell-inverted {
    width: 100%;
    height: calc(var(--row-h, 56px) - 6px);
    background:
        repeating-linear-gradient(
            135deg,
            transparent 0,
            transparent 6px,
            var(--surface-soft) 6px,
            var(--surface-soft) 7px
        );
    opacity: 0.55;
    border-radius: 3px;
}

/* Row number column — narrow, muted, spreadsheet-style.
   Sticky horizontally so row numbers + the row-resize handle stay
   visible during horizontal scroll. left:0 pins to the left edge of
   the table-wrap; the opaque background + z-index keep data cells
   from showing through underneath. */
.studio-table-rowno-cell {
    position: sticky;          /* sticky also anchors the row-resize handle */
    left: 0;
    width: 44px;
    text-align: center;
    vertical-align: middle;
    color: var(--text-faint);
    font-size: 0.78rem;
    font-variant-numeric: tabular-nums;
    background: var(--surface-soft);
    border-right: 1px solid var(--border-faint);
    padding: 0 !important;
    user-select: none;
    z-index: 2;                /* above body cells; below thead (z:4) */
}

/* Row resize handle — sits on the bottom edge of every body row's
   row-number cell. Drag changes the row height for the entire sheet. */
.studio-row-resize {
    position: absolute;
    left: 0; right: 0;
    bottom: -3px;
    height: 6px;
    /* Use ns-resize, not row-resize. row-resize on Chrome/Windows is
       drawn from a Chrome-shipped bitmap (same family as col-resize)
       and re-rasterises softly at 125 % / 150 % display scaling. The
       ns-resize alias is rendered by the OS at the active scale and
       stays sharp. The previous fix matched col-resize → ew-resize for
       this same reason; the assumption that row-resize was already
       OS-rendered was wrong, so this aligns the row handle with the
       same OS-native treatment. */
    cursor: ns-resize;
    z-index: 2;
    user-select: none;
}
.studio-row-resize:hover {
    background: linear-gradient(to bottom, transparent 0, transparent 2px, var(--accent) 2px, var(--accent) 4px, transparent 4px);
}
/* (Row-resize cursor override moved to a body-only rule a few sections
   below, alongside the column-resize and grip-drag overrides.) */
.studio-table thead .studio-table-rowno-cell {
    background: var(--surface-soft);
    /* Top-left corner cell — sticky in BOTH axes. Higher z-index than
       either lone-axis sticky so it sits cleanly on top of both the
       sticky thead row and the sticky row-number column when both are
       active. */
    z-index: 6;
    /* Frozen-seam mask: a single opaque 1px sliver (z:6, above the
       scrolled headers at z:4) bridging the header→header seam to the
       right so the header bg stays continuous. No DOWN sliver: selected/
       sorted underlines are now clipped inside their own th (see
       .studio-th-sorted::after), so nothing leaks across the header→body
       boundary and there's no overflow into the first data row. */
    box-shadow: 1px 0 0 0 var(--surface-soft);
}
.studio-table thead .studio-table-checkbox-cell {
    background: var(--surface-soft);
    z-index: 6;
    /* Frozen-seam mask (see row-number cell above) — right sliver only. */
    box-shadow: 1px 0 0 0 var(--surface-soft);
}
.studio-rowno-number { display: inline-block; }
.studio-rowno-pin {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    color: var(--frozen-pin);   /* amber accent matching frozen-row tint */
}
.studio-rowno-pin svg {
    width: 13px; height: 13px;
    stroke: currentColor; fill: none;
}

/* Row selection + sort header + editable cells. Row checkbox is wrapped
   in a flex wrapper so head and body align identically.
   Sticky horizontally to the right of the row-number column (left:44px
   matches the row-number column's width). */
.studio-table-checkbox-cell {
    position: sticky;
    left: 44px;
    width: 44px;
    text-align: center;
    vertical-align: middle;
    padding: 0 !important;
    background: var(--surface);
    border-right: 1px solid var(--border-faint);
    z-index: 2;
}
/* In the body, default cell background should match other rows so the
   sticky checkbox column doesn't read as different chrome. The row's
   own background-color (set by hover / selected / frozen states) wins
   via the rules below for those states. */
.studio-table tbody td.studio-table-checkbox-cell { background: var(--surface); }
.studio-table tbody tr:hover td.studio-table-checkbox-cell { background: var(--surface-soft); }
.studio-table tbody tr.studio-row-selected td.studio-table-checkbox-cell { background: var(--row-selected-bg); }
.studio-table tbody tr.studio-row-frozen td.studio-table-checkbox-cell { background: var(--frozen-bg); }
.studio-checkbox-wrap {
    display: flex; align-items: center; justify-content: center;
    width: 100%; height: 100%;
    min-height: 28px;
}
.studio-table-checkbox-cell input[type="checkbox"] {
    width: 16px; height: 16px;
    accent-color: var(--accent);
    cursor: pointer;
    margin: 0;
    display: inline-block;
}
.studio-table-sortable,
.studio-table thead th.studio-table-structure-cell {
    cursor: pointer;
    user-select: none;
    transition: background var(--motion-fast) var(--ease), color var(--motion-fast) var(--ease);
}
.studio-table-sortable:hover,
.studio-table thead th.studio-table-structure-cell:hover {
    background: var(--accent-soft);
    color: var(--accent-strong);
}
.studio-table-th-label {
    display: inline-flex; align-items: center; gap: 0.375rem;
}
.studio-table-th-sort {
    display: inline-flex; color: var(--accent);
}

/* Row selection uses a TRANSLUCENT inset overlay (like column selection)
   so conditionally-colored numeric cells keep their gradient visible
   underneath. The soft background is a non-!important bonus for plain
   cells (an inline cell color wins over it; the overlay layers on top). */
.studio-row-selected td { background: var(--row-selected-bg); box-shadow: inset 0 0 0 9999px var(--accent-ring); }
.studio-row-selected:hover td { box-shadow: inset 0 0 0 9999px var(--accent-ring), inset 0 0 0 9999px var(--accent-ring); }

/* Virtualization spacer — invisible row that only contributes height
   so the table's total scroll size matches the un-windowed render.
   No background, no borders, no hover. */
.studio-row-spacer,
.studio-row-spacer:hover { background: transparent !important; }
.studio-row-spacer td {
    background: transparent !important;
    border: 0 !important;
    padding: 0 !important;
}

/* ── Active cell — spreadsheet-style "current cell" ──────────────
   Click a cell body to make it the active cell. Subtle accent
   outline that doesn't dominate the table; Ctrl/Cmd+C copies the
   active cell's value. */
.studio-cell-active {
    position: relative;
    z-index: 1;
}
.studio-cell-active::after {
    content: '';
    position: absolute;
    inset: 0;
    border: 2px solid var(--accent);
    pointer-events: none;
    z-index: 3;
}

/* Copied state — dashed outline that PERSISTS until the user clicks
   another cell or outside the table. Uses --success (green) so the
   "value just copied" feedback reads distinctly from the active-cell
   blue border, even if the cell is both active and copied. No
   background flash, no value animation — the cell's text stays
   visually stable so reading the value during/after copy is
   undisturbed. */
.studio-cell-copied::after {
    content: '';
    position: absolute;
    inset: 0;
    border: 2px dashed var(--success);
    pointer-events: none;
    z-index: 3;
}

/* Suppress the active-cell outline while the cell hosts an inline
   editor — the input's own focus ring is the active state in that
   moment and a double ring would look messy. Modern :has() support
   covers Chrome / Safari / Firefox 121+; older browsers fall back
   to the harmless behaviour of both rings being visible. */
.studio-cell-active:has(.studio-cell-edit)::after,
.studio-cell-active:has(.studio-cell-bulk-host-editing)::after {
    display: none;
}

/* Frozen (pinned) row — subtle warm tint AND a sticky vertical
   position so the row stays visible below the column header during
   scrolling. The `--frozen-top` custom property is set on the <tr>
   inline by StudioRow (theadHeight + sum of previous frozen row
   heights). z-index sits below the sticky thead (z:4 in the existing
   thead rule) so column headers cover frozen rows on overlap, and
   above normal body cells.

   Selector specificity matters here: the previous `.studio-row-frozen td`
   (0,1,1) lost to `.studio-table tbody td.studio-cell-custom-wrap`
   (0,2,2), which sets `position: relative` to anchor the pencil
   edit-icon. That meant Notes and every custom column STOPPED sticking
   when the row was frozen — they scrolled with the body while
   Structure/ID/SMILES (whose tds have no position rule of their own)
   correctly stuck. The qualified `.studio-table tbody tr.studio-row-frozen td`
   selector (0,2,3) beats the custom-wrap rule, and `position: sticky
   !important` further hardens against any future per-td position rule
   (e.g. `.studio-cell-active { position: relative }`). Sticky still
   establishes a containing block for the absolute pencil-icon child,
   so the affordance keeps working in frozen rows. */
.studio-table tbody tr.studio-row-frozen td {
    background: var(--frozen-bg) !important;
    border-bottom: 1px solid var(--frozen-border) !important;
    position: sticky !important;
    top: var(--frozen-top, 0px);
    z-index: 3;
}
.studio-table tbody tr.studio-row-frozen:hover td { background: var(--frozen-bg-hover) !important; }
/* Sticky-left cells inside a frozen row need a higher z-index than
   the frozen row's own data cells. Without this bump, every frozen
   td has z-index: 3 (from the rule above) and paint order falls
   back to DOM order — data cells come AFTER the rowno/checkbox
   cells in the row, so during horizontal scroll the data cells
   slid OVER the sticky-left columns instead of behind them.

   z-index hierarchy now:
     thead corner cell (sticky in both axes): 6
     thead other ths (sticky-top):             4
     frozen row sticky-left (rowno/checkbox):  5  ← this rule
     frozen row data cells:                    3
     normal row sticky-left:                   2
     normal row data cells:                    auto

   Background stays opaque from the frozen-row td rule above so
   data cells slide cleanly behind without bleeding through. */
.studio-table tbody tr.studio-row-frozen td.studio-table-rowno-cell,
.studio-table tbody tr.studio-row-frozen td.studio-table-checkbox-cell {
    z-index: 5;
}

/* ── Frozen DATA columns ─────────────────────────────────────────
   `.studio-cell-col-frozen` decorates body tds and thead ths that
   are pinned to the left band right after Structure (and Structure
   itself). Inline styles handle `position: sticky` + the per-column
   `left:` offset (computed at the page level from cumulative
   widths); this class only paints the opaque background, row-state
   overrides, and the z-index stack so the sticky cells render
   cleanly during horizontal scroll.

   z-index hierarchy (paint order, highest on top):
     · thead corner / rowno-checkbox (sticky in both axes):  6
     · thead frozen DATA column ths (sticky in both axes):   6
       (matches the corner cells — they all live on the same row)
     · thead non-frozen ths (sticky-top only):               4
     · body frozen-row sticky-left chrome (rowno/checkbox):  5
     · body frozen-row FROZEN data columns (sticky x+y):     5
     · body frozen-row non-frozen data cells:                3
     · body normal-row FROZEN data columns:                  2
     · body normal-row non-frozen data cells:                auto
   The structure cell follows the same hierarchy by carrying the
   same class. */
.studio-table tbody td.studio-cell-col-frozen {
    background: var(--surface);
    z-index: 2;
}
.studio-table tbody tr:hover td.studio-cell-col-frozen {
    background: var(--surface-soft);
}
.studio-table tbody tr.studio-row-selected td.studio-cell-col-frozen {
    background: var(--row-selected-bg);
}
.studio-table tbody tr.studio-row-frozen td.studio-cell-col-frozen {
    background: var(--frozen-bg);
    z-index: 5;
}
.studio-table tbody tr.studio-row-frozen:hover td.studio-cell-col-frozen {
    background: var(--frozen-bg-hover);
}
.studio-table thead th.studio-cell-col-frozen {
    background: var(--surface-soft);
    /* z-index 6 sits above non-frozen headers (z:4), so a scrolled
       column's selected/sorted underline is masked WHERE a frozen header
       covers it (the frozen th's opaque border-box, including its own
       border-bottom, sits on top). Underlines are clipped inside their
       own th (see .studio-th-sorted::after) so nothing crosses the
       header↔body boundary. */
    z-index: 6;
}
/* 1px RIGHT bridge shadow — fills the seam between adjacent frozen
   headers so the surface stays continuous across the frozen band. Gated
   with :has(+ .studio-cell-col-frozen) so the LAST frozen header (the
   one whose right edge meets the non-frozen / scrolling band, e.g. ID
   after Structure) does NOT paint this shadow. Painting it on the last
   frozen would let the 1px sliver overhang into the non-frozen area, and
   on a selected ID column (accent-muted bg) that overhang read as a
   visibly thicker right border. */
.studio-table thead th.studio-cell-col-frozen:has(+ .studio-cell-col-frozen) {
    box-shadow: 1px 0 0 0 var(--surface-soft);
}

/* Density presets — defined intentionally so each mode has a clear
   purpose:
     · Compact: maximises rows on screen. Short row, tight padding,
       single-line cells, smaller structure thumbnails.
     · Comfortable: balanced default. Two-line wraps, mid-size
       structure thumbnails — what most sheets should look like.
     · Spacious: big structures + long-text inspection. Three-line
       wraps, taller rows, larger structure thumbnails.
   `--cell-clamp` controls max visible body-cell lines before ellipsis;
   `--row-h` flows from this (the structure-cell rule below picks up
   per-density heights as well). */
/* Each density publishes --row-h on the *table itself* (not on
   tbody tr), so the inline `style="--row-h: …"` set during a custom
   row-height drag cascades through cleanly. Declaring --row-h on
   tbody tr would shadow the inline value at and below tbody tr,
   which broke row-resize after the previous overflow refactor. */
/* --td-pad-y in WHOLE PIXELS (not rem) so the row-height calc
   below evaluates to integer pixel values — no sub-pixel rounding
   when columns mount/unmount during horizontal virt scroll. The
   td padding rules just below use the SAME variable so the actual
   rendered padding matches the value the calc subtracts. The
   horizontal padding stays in rem because it doesn't enter the
   row-height math. */
.studio-table-density-compact     { --row-h: 36px; --td-pad-y: 6px; }
.studio-table-density-comfortable { --row-h: 56px; --td-pad-y: 10px; }
.studio-table-density-spacious    { --row-h: 96px; --td-pad-y: 16px; }

.studio-table-density-compact     tbody tr { height: 36px; --cell-clamp: 1; }
.studio-table-density-comfortable tbody tr { height: 56px; --cell-clamp: 2; }
.studio-table-density-spacious    tbody tr { height: 96px; --cell-clamp: 3; }
.studio-table-density-compact tbody td      { padding: var(--td-pad-y) 0.75rem; }
.studio-table-density-comfortable tbody td  { padding: var(--td-pad-y) 0.875rem; }
.studio-table-density-spacious tbody td     { padding: var(--td-pad-y) 1rem; }
/* Padding rules for thead are not needed — the new header layout
   uses a fixed-height tr + .studio-th-content that handles padding
   internally (so the grip / arrow / resize handle line up perfectly
   regardless of density). */

/* Custom row height — applied via inline --row-h custom property. The
   structure cell fills the row naturally, so no special override here.
   The clamp scales loosely with row height so taller rows show more
   wrapped lines before ellipsizing. */
.studio-table-density-custom tbody tr {
    /* --row-h comes from the inline style on .studio-table during a
       row-resize drag (or from sheetLayout.row_height after commit).
       Custom density wins because this rule comes after the base
       density-class rules in source order and has equal specificity. */
    height: var(--row-h, 52px);
    --cell-clamp: max(1, calc((var(--row-h, 52px) - 12px) / 18));
}
.studio-table-density-custom { --td-pad-y: 4px; }
.studio-table-density-custom tbody td { padding-top: var(--td-pad-y); padding-bottom: var(--td-pad-y); }
/* Row-number and checkbox cells should ignore density vertical changes
   (they're already centered through their wrappers). */
.studio-table-density-compact .studio-table-checkbox-cell,
.studio-table-density-comfortable .studio-table-checkbox-cell,
.studio-table-density-spacious .studio-table-checkbox-cell { padding: 0 !important; }
.studio-table-density-compact .studio-table-rowno-cell,
.studio-table-density-comfortable .studio-table-rowno-cell,
.studio-table-density-spacious .studio-table-rowno-cell { padding: 0 !important; }

/* Cell cursors — copyable cells default, editable cells text cursor.
   Read-only display state for table values. The hover-tint on the
   inner span has been removed so the table doesn't get a "second"
   highlight following the cursor — the row-level hover is enough. */
.studio-cell-clickable {
    cursor: default;
    border-radius: 4px;
    /* Vertical breathing room (4 px each side) so narrow columns
       don't visually collide content with the row's top/bottom
       borders. Previous 1 px padding made narrow SMILES cells
       look cramped after we removed the JS overflow detection.
       Horizontal padding stays modest so wrapped lines don't lose
       too many characters per row. */
    padding: 4px 6px;
    /* Default = clipped, no scrollbar. Reverted from `overflow-y: auto`
       which painted scrollbars on cells whose content visually fit
       but tripped subpixel rounding (Chrome on Windows reports
       scrollHeight a fraction larger than clientHeight even when
       the content is fully visible). The previous fix used a
       per-cell ResizeObserver with a 1 px tolerance, which we
       removed for perf. With `hidden` the browser clips silently;
       the cell's `title` attribute (set on the wrapping td for
       SMILES / Notes) reveals the full text on hover, so users
       can still read clipped content. */
    display: block;
    max-height: var(--row-h, 56px);
    overflow-y: hidden;
    overflow-x: hidden;
    white-space: pre-wrap;
    overflow-wrap: anywhere;
    word-break: break-word;
    line-height: 1.35;
    max-width: 100%;
    /* `box-sizing: border-box` so the padding doesn't push the
       cell past the column width when the column is very narrow.
       Without it, padding adds to the content box and the cell
       can horizontally overflow into the neighbour column. */
    box-sizing: border-box;
}
/* Legacy `.is-overflowing` (no -y/-x suffix). The per-cell
   `useOverflowDetect` hook that used to toggle it is now a NO-OP SHIM
   (returns `false`), so this class is never actually added — the live
   mechanism is the row-level rAF detector that toggles `.is-overflowing-y`
   (see the rule above). Kept only as an inert hook for any markup/tests
   still referencing it; safe to remove in a future cleanup along with
   the shim. */
.studio-cell-clickable.is-overflowing {
    overflow-y: auto;
    scrollbar-width: thin;
    scrollbar-color: var(--border-strong) transparent;
}
.studio-cell-editable, .studio-cell-edit { cursor: text; }

/* ── Column header cursors — only one cursor per region, no flicker ──
   The th's "drag-reorder area" was previously `grab`, which on some
   browsers/OSes renders blurry near the cell edge AND flickered between
   pointer (label) → grab (gap) → col-resize (handle). We now use only:
     - pointer on the sort label and menu chevron
     - col-resize on the resize handle (browser-native, sharp)
     - default elsewhere on the th
   Drag-reorder still works because HTML5 drag is event-based, not
   cursor-based. While actively dragging, the OS shows its own cursor. */
/* The whole th is the click target for column selection / sort, so
   the th gets `cursor: pointer`. Inner zones (grip / menu / resize)
   override with their own cursors. */
.studio-table thead th.studio-table-sortable { cursor: pointer; }
.studio-table thead th .studio-header-menu-btn { cursor: pointer; }
/* (col-resize handle's own cursor lives further down at .studio-col-resize
   and is set to ew-resize there. The duplicate cursor: col-resize rule
   that used to live here was removed — it shipped the blurry cursor.) */
/* Whole-document cursor overrides during a drag. Pointer capture
   already pins the cursor at the OS level for whichever element
   captured the pointer, so these are just safety nets. ew-resize,
   ns-resize, and move are OS-rendered on Windows and used in place
   of col-resize, row-resize, and grabbing — those latter three ship
   as Chrome bitmaps and scale softly on non-integer DPI (125% /
   150% Windows scaling). The three resize states have separate body
   classes so the cursors never share a rule and never bleed across
   each other. */
body.studio-resizing { cursor: ew-resize !important; }       /* column width */
body.studio-row-resizing { cursor: ns-resize !important; }   /* row height   */
body.studio-grip-dragging { cursor: move !important; }       /* column reorder */

.studio-cell-notes-wrap { padding: 0.4rem 0.5rem; }
.studio-cell-custom-wrap { padding: 0.4rem 0.5rem; }
.studio-cell-editable {
    display: block;
    /* Same hidden-default rationale as .studio-cell-clickable above.
       Edit mode shows a textarea where the full content is fully
       visible / scrollable, so clipped read mode is acceptable. */
    max-height: var(--row-h, 56px);
    min-height: 28px;
    padding: 4px 8px;
    border-radius: 6px;
    cursor: text;
    text-align: center;
    white-space: pre-wrap;
    overflow-y: hidden;
    overflow-x: hidden;
    overflow-wrap: anywhere;
    word-break: break-word;
    line-height: 1.35;
    color: var(--text);
    font-size: 0.875rem;
}
.studio-cell-editable.is-overflowing {
    overflow-y: auto;
    scrollbar-width: thin;
    scrollbar-color: var(--border-strong) transparent;
}
.studio-cell-custom { font-size: 0.875rem; }

/* The TDs that wrap an editable cell become the offset parent for the
   pencil affordance. The pencil is rendered as a SIBLING of the cell
   content (not nested inside it), so it sits in the td's corner and
   never overlaps text, placeholder, or wrapped/ellipsized content. */
.studio-table tbody td.studio-cell-custom-wrap,
.studio-table tbody td.studio-cell-notes-wrap {
    position: relative;
}

/* Reserve a small bottom-right corner for the pencil so cell content
   never has to share that space — even when the row is hovered and the
   pencil fades in, the content layout doesn't shift and there's no
   text/icon overlap. The reserve only applies to editable cell types. */
.studio-cell-custom-wrap > .studio-cell-editable,
.studio-cell-notes-wrap > .studio-cell-notes {
    padding-right: 22px;
    padding-bottom: 4px;
}

/* Pencil affordance — anchored to the td's bottom-right corner. Hidden
   by default; fades in subtly on row hover and lifts to the accent
   color on its own hover. The button is non-focusable in the natural
   tab order (tabIndex=-1 in JSX) since the cell itself already opens
   the editor on Enter / click. */
.studio-cell-edit-icon {
    position: absolute;
    right: 4px; bottom: 4px;
    width: 22px; height: 22px;
    padding: 0;
    border: 1px solid transparent;
    border-radius: 5px;
    background: transparent;
    color: var(--text-muted);
    display: inline-flex; align-items: center; justify-content: center;
    cursor: pointer;
    opacity: 0;
    pointer-events: none;
    transition: opacity var(--motion-fast) var(--ease),
                color var(--motion-fast) var(--ease),
                background var(--motion-fast) var(--ease),
                border-color var(--motion-fast) var(--ease),
                box-shadow var(--motion-fast) var(--ease);
    z-index: 2;
}
.studio-cell-edit-icon svg { display: block; }

/* Cell hover: the pencil only appears for the cell the cursor is
   actually inside. Previously this was tr:hover which lit up every
   editable cell in the row, even ones the user wasn't pointing at —
   too noisy. Switching the trigger to td:hover scopes the affordance
   to the right cell. The td selectors target only EDITABLE wrappers
   (.studio-cell-custom-wrap holds custom columns; .studio-cell-notes-wrap
   holds Notes); read-only cells (ID / SMILES / etc.) don't carry
   either class so they correctly never expose a pencil. */
.studio-table tbody td.studio-cell-custom-wrap:hover .studio-cell-edit-icon,
.studio-table tbody td.studio-cell-notes-wrap:hover .studio-cell-edit-icon {
    opacity: 1;
    pointer-events: auto;
    background: var(--surface);
    border-color: var(--border);
    color: var(--text-muted);
    box-shadow: 0 1px 2px rgba(15, 30, 45, 0.06);
}
/* Selected rows already have an accent-soft background; the surface
   chip would look washed out against it, so swap to a slightly
   darker surface so the pencil stays distinct. Same cell-level
   scoping. */
.studio-table tbody tr.studio-row-selected td.studio-cell-custom-wrap:hover .studio-cell-edit-icon,
.studio-table tbody tr.studio-row-selected td.studio-cell-notes-wrap:hover .studio-cell-edit-icon {
    background: var(--surface);
    border-color: var(--accent-ring);
    color: var(--accent-strong);
}
/* Frozen rows are amber-tinted; same idea — keep the chip readable. */
.studio-table tbody tr.studio-row-frozen td.studio-cell-custom-wrap:hover .studio-cell-edit-icon,
.studio-table tbody tr.studio-row-frozen td.studio-cell-notes-wrap:hover .studio-cell-edit-icon {
    background: var(--frozen-bg);
    border-color: var(--frozen-border);
}

/* Direct pencil hover: bump to accent so the affordance is unmistakable. */
.studio-cell-edit-icon:hover {
    color: var(--accent-strong);
    background: var(--accent-soft);
    border-color: var(--accent-ring);
    box-shadow: 0 1px 3px rgba(31, 95, 139, 0.15);
}
.studio-cell-edit-icon:focus-visible {
    opacity: 1;
    pointer-events: auto;
    outline: 2px solid var(--accent-ring);
    outline-offset: 1px;
}

/* Wrapper for cells that show both the active control (input / select /
   segmented buttons) and the bulk-scope toggle. Stacks them vertically
   with a tight gap so the toggle reads as an attached option, not a
   separate row. `align-items: center` keeps natural-width controls
   (boolean segmented buttons) horizontally centered while still letting
   width:100% controls (input, select) span the cell. */
.studio-cell-bulk-host {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
    width: 100%;
    min-width: 0;
}
.studio-cell-bulk-host > .studio-cell-edit,
.studio-cell-bulk-host > .studio-cell-dropdown {
    align-self: stretch;
}
.studio-cell-bulk-host-editing { gap: 2px; }
/* Vertically centre the single short control (dropdown / boolean) within
   the cell. In col-virt mode .studio-cell-bulk-host is forced to the strict
   row height (see the .studio-table-col-virt .studio-cell-bulk-host rule),
   and with only `align-items: center` (cross-axis) its child rode the TOP of
   that taller box — which read as the Stage/Status dropdowns "shifting up as
   a group" once enough columns (e.g. a property group) triggered column
   virtualization. In non-virt sheets the host is content-height so this is a
   no-op (the td's `vertical-align: middle` already centres it). The editing
   variant is deliberately excluded: it holds a tall editor + bulk-save bar
   and stays top-aligned + scrollable so centered-overflow never clips its top. */
.studio-cell-bulk-host:not(.studio-cell-bulk-host-editing) {
    justify-content: center;
}

/* ── Bulk save action bar ────────────────────────────────────────
   Portaled overlay anchored beneath the editor of an editable cell
   when the current row is part of a multi-row selection. Sized to its
   own content (not the cell), so it never forces a column to grow.
   Two clearly-labelled buttons make the scope explicit (single row vs
   all selected); a small × cancels a staged change. Default behaviour
   stays single-cell — bulk requires an explicit click. */
.studio-bulk-bar-anchor {
    /* 0×0 marker that sits in the cell's flex-column flow so the
       portaled bar can compute its anchor position. Contributes
       nothing to layout. */
    display: block;
    width: 0;
    height: 0;
    pointer-events: none;
}
.studio-bulk-bar {
    /* Floats above table chrome; sized to content so column widths
       are unaffected. JS sets `left/top` and we recenter via the
       transform below. Icon-only buttons keep the bar compact. */
    transform: translateX(-50%);
    z-index: 11000;
    padding: 4px;
    background: var(--surface);
    border: 1px solid var(--accent-ring);
    border-radius: 8px;
    box-shadow: 0 8px 24px rgba(15, 30, 45, 0.16),
                0 2px 6px rgba(15, 30, 45, 0.08);
    display: inline-flex;
    user-select: none;
    /* Soft entry so the bar doesn't pop unnervingly. */
    animation: studioBulkBarIn 110ms var(--ease, ease-out);
}
@keyframes studioBulkBarIn {
    from { opacity: 0; transform: translate(-50%, -2px); }
    to   { opacity: 1; transform: translate(-50%, 0); }
}
.studio-bulk-bar-actions {
    display: inline-flex;
    align-items: center;
    gap: 3px;
}
.studio-bulk-bar-btn {
    /* Square icon-only target, generous enough for trackpad use. */
    width: 32px;
    height: 30px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    border: 1px solid var(--border);
    border-radius: 6px;
    background: var(--surface);
    color: var(--text);
    line-height: 1;
    cursor: pointer;
    transition: background var(--motion-fast) var(--ease),
                color var(--motion-fast) var(--ease),
                border-color var(--motion-fast) var(--ease),
                box-shadow var(--motion-fast) var(--ease);
}
.studio-bulk-bar-btn:hover {
    background: var(--surface-sunk);
    border-color: var(--border-strong);
    color: var(--text-strong);
}
.studio-bulk-bar-btn:active {
    transform: translateY(0.5px);
}
.studio-bulk-bar-btn:focus-visible {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 3px var(--accent-ring);
}
.studio-bulk-bar-btn[disabled] {
    opacity: 0.45;
    cursor: not-allowed;
    background: var(--surface);
}
/* Primary = single-row save (the safe default path; matches Enter).
   Neutral chip — distinct from the accent-tinted bulk button. */
.studio-bulk-bar-btn-primary {
    background: var(--surface);
    color: var(--text-strong);
}
.studio-bulk-bar-btn-primary:hover {
    background: var(--surface-sunk);
    color: var(--accent-strong);
    border-color: var(--accent-ring);
}
/* Bulk action — accent fill so it visually reads as the "wider scope"
   button. Bulk apply requires an explicit click; this styling never
   triggers automatically. */
.studio-bulk-bar-btn-bulk {
    background: var(--accent-soft);
    border-color: var(--accent-ring);
    color: var(--accent-strong);
}
.studio-bulk-bar-btn-bulk:hover {
    background: var(--accent);
    border-color: var(--accent);
    color: var(--accent-fg);
}
/* Cancel — discard a staged change. Small, low-visual-weight so it
   reads as a tertiary action next to the two save buttons. */
.studio-bulk-bar-btn-cancel {
    color: var(--text-muted);
    background: transparent;
    border-color: transparent;
}
.studio-bulk-bar-btn-cancel:hover {
    background: var(--danger-soft);
    color: var(--danger);
    border-color: transparent;
}
.studio-bulk-bar-btn svg { display: block; }

/* Pending preview state — a subtle ring/dashed border on the input
   control so the user can see they're staging a change that hasn't
   been committed yet. */
.studio-cell-pending,
.studio-bool-segmented-pending {
    box-shadow: 0 0 0 2px var(--accent-ring);
    border-color: var(--accent-ring) !important;
}

/* Dropdown cell — native select styled to fit the table aesthetic.
   Uses the SAME chevron asset as the rest of the unified select
   styling so cell, modal, and filter dropdowns all look like one
   family of controls. */
.studio-cell-dropdown {
    width: 100%;
    max-width: 100%;
    min-width: 0;
    padding: 4px 24px 4px 8px;
    border: 1px solid transparent;
    border-radius: 6px;
    background-color: transparent;
    color: var(--text);
    font-size: 0.875rem;
    font-family: inherit;
    cursor: pointer;
    appearance: none;
    -webkit-appearance: none;
    -moz-appearance: none;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath d='M3 4.5l3 3 3-3' stroke='%2364748b' stroke-width='1.6' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 6px center;
    transition: background-color var(--motion-fast) var(--ease),
                border-color var(--motion-fast) var(--ease),
                box-shadow var(--motion-fast) var(--ease);
    text-overflow: ellipsis;
    overflow: hidden;
    white-space: nowrap;
}
.studio-cell-dropdown:hover {
    background-color: var(--surface-sunk);
    border-color: var(--border);
}
.studio-cell-dropdown:focus {
    outline: none;
    border-color: var(--accent);
    background-color: var(--surface);
    box-shadow: 0 0 0 3px var(--accent-ring);
}
.studio-cell-dropdown:disabled {
    color: var(--text-faint);
    cursor: not-allowed;
}

/* Inline help text inside the Add/Edit column modal. */
.studio-form-hint {
    margin-top: 4px;
    font-size: 0.72rem;
    color: var(--text-muted);
    line-height: 1.35;
}

.studio-cell-bool { display: flex; align-items: center; padding: 4px 8px; }
.studio-cell-bool input[type="checkbox"] {
    width: 15px; height: 15px;
    accent-color: var(--accent);
    cursor: pointer;
}

/* True/False custom column — segmented control.
   Each cell shows two buttons: ✓ and ✗. The user clicks directly to set
   the value. Clicking the active option clears it back to null.
   Icons sized for confident clicking and quick visual scanning across
   row densities; buttons stay snug so the cell doesn't feel crowded. */
.studio-bool-segmented {
    display: inline-flex;
    gap: 3px;
    padding: 3px;
    background: var(--surface-sunk);
    border: 1px solid var(--border-faint);
    border-radius: 7px;
    user-select: none;
}
.studio-bool-seg-btn {
    width: 32px; height: 28px;
    background: transparent;
    border: none; border-radius: 5px;
    color: var(--text-faint);
    font-size: 1.25rem; font-weight: 700; line-height: 1;
    cursor: pointer;
    display: flex; align-items: center; justify-content: center;
    transition: background var(--motion-fast) var(--ease),
                color var(--motion-fast) var(--ease),
                box-shadow var(--motion-fast) var(--ease);
}
.studio-bool-seg-btn:hover { color: var(--text); background: var(--surface); }
.studio-bool-seg-btn.studio-bool-seg-true.active {
    background: var(--success-bg); color: var(--success-fg);
    box-shadow: inset 0 0 0 1px var(--success-border);
}
.studio-bool-seg-btn.studio-bool-seg-false.active {
    background: var(--danger-bg); color: var(--danger-strong);
    box-shadow: inset 0 0 0 1px var(--danger-border);
}
/* Compact density: shrink slightly to fit in 38px rows without clipping. */
.studio-table-density-compact .studio-bool-seg-btn {
    width: 28px; height: 24px;
    font-size: 1.1rem;
}
/* Spacious density: keep generous so the buttons feel proportionate
   to the larger cell. */
.studio-table-density-spacious .studio-bool-seg-btn {
    width: 36px; height: 32px;
    font-size: 1.4rem;
}
.studio-cell-placeholder { color: var(--text-faint); font-style: italic; }

/* Inline editor — redesigned to look like a real editable field rather
   than a hairline input crammed into the cell. Slightly taller, clearer
   border, two-tone focus ring, and a small inset shadow to read as
   "input affordance". The text-align: left override (vs the cell's
   centered display text) reinforces "you're now typing here". */
.studio-cell-edit {
    width: 100%;
    min-height: 32px;
    padding: 6px 10px;
    border: 1.5px solid var(--accent);
    border-radius: var(--radius-sm);
    background: var(--surface);
    color: var(--text-strong);
    font: inherit;
    font-size: 0.9rem;
    line-height: 1.35;
    /* Subtle inner depth only — NO surrounding accent-ring halo. The 1.5px
       accent border (→ accent-strong on focus) IS the active-field indicator,
       so the editor reads clearly as the active cell without an extra ring
       around it. (A :focus-visible ring can't be used to scope this: a text
       input always matches :focus-visible — it accepts keyboard input — so the
       ring would show even for a mouse click.) */
    box-shadow: inset 0 1px 0 rgba(15, 23, 42, 0.04);
    outline: none;
    resize: vertical;
    text-align: left;
    transition: border-color var(--motion-fast) var(--ease),
                box-shadow var(--motion-fast) var(--ease);
}
.studio-cell-edit:focus {
    border-color: var(--accent-strong);
    box-shadow: inset 0 1px 0 rgba(15, 23, 42, 0.04);
}
/* Density-aware sizing so the redesigned editor fits each row height
   gracefully — a 32px-min input dominates a 36px-tall compact row,
   so we shave a few px there. Spacious gets a slightly taller input
   so it doesn't feel lost in the cell. */
.studio-table-density-compact .studio-cell-edit  { min-height: 28px; padding: 4px 8px; font-size: 0.85rem; }
.studio-table-density-spacious .studio-cell-edit { min-height: 36px; padding: 8px 11px; font-size: 0.92rem; }

.studio-form-label {
    font-size: 0.78rem;
    font-weight: 600;
    color: var(--text-muted);
}
/* Section label inside the From CSV mapping pane. Replaces inline
   `marginTop: 0.625rem` on the JSX side so the spacing rhythm is
   defined in one place and stays consistent if more sections are
   added. The first such label collapses its top margin so there
   isn't an awkward gap right under the dropzone. */
.studio-form-label-section {
    margin-top: 0.625rem;
    /* Slight letter-tracking + uppercase reads as "section heading"
       and visually anchors each block of mapping controls without
       changing the existing label colour token. */
    text-transform: uppercase;
    letter-spacing: 0.05em;
    font-size: 0.72rem;
}
.studio-form-label-section:first-of-type {
    margin-top: 0.25rem;
}

/* Sheet panel — wraps the table + bottom status bar so they share the
   same elevated surface and bottom edge inside the workspace canvas. */
.studio-sheet-panel {
    flex: 1; min-height: 0;
    display: flex; flex-direction: column;
    background: var(--surface);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-2);
    overflow: hidden;
}
.studio-sheet-panel .studio-table-wrap {
    flex: 1; min-height: 0;
    border: none;
    border-radius: 0;
    box-shadow: none;
    background: var(--surface);
}

/* Bottom status bar — selection count + bulk actions, anchored inside
   the sheet panel so it reads as part of the data canvas. */
.studio-status-bar {
    /* Locked to a fixed height. Selecting rows used to introduce
       an additional row of action buttons that pushed the bar
       taller (~64 px → 80 px), causing a visible vertical jump in
       the surrounding layout. The bar now reserves a single 40 px
       lane regardless of contents — buttons are constrained to
       fit, and overflow content is hidden / truncated rather than
       wrapping. `flex-shrink: 0` keeps the bar from collapsing
       when the wrap above contains a wide horizontal scroll.

       Layout: 3-column grid with the center column auto-sized so
       the pager stays visually centered REGARDLESS of how long
       the left status text or right action group is. The two
       flanking columns each take an equal `minmax(0, 1fr)` track
       so they can shrink to 0 if needed without pushing the
       center off-axis. The previous flex layout let the long left
       text push the pager toward the right edge; grid is the
       canonical fix for "centered third column with flexing
       siblings". */
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
    align-items: center;
    gap: 0.5rem;
    padding: 0 0.875rem;
    border-top: 1px solid var(--border);
    background: var(--surface-soft);
    height: 40px;
    min-height: 40px;
    max-height: 40px;
    flex-shrink: 0;
    overflow: hidden;
}
.studio-status-left {
    /* Grid column 1. `min-width: 0` lets the column track shrink
       below the natural width of its text content so the pager
       doesn't get pushed by a long status string. */
    min-width: 0;
    display: flex; align-items: center;
    gap: 0.5rem;
    overflow: hidden;
    height: 100%;
    justify-self: start;
}
.studio-status-right {
    /* Grid column 3 — mirrors the left column. */
    min-width: 0;
    display: flex;
    align-items: center;
    gap: 0.375rem;
    height: 100%;
    justify-self: end;
}
/* Center region — column 2 of the grid, auto-sized to the pager's
   intrinsic width so the pager stays centered relative to the bar
   regardless of the flanking columns. */
.studio-status-center {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 100%;
    justify-self: center;
}

/* Pager — fits inside the 40-px status bar lane. Buttons are tiny
   icon-only squares; the page input is a narrow numeric field;
   the page-size select is a compact native select with custom
   chevron disabled (uses the OS default to keep visual weight low). */
.studio-pager {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    height: 26px;
    font-size: 0.78rem;
    color: var(--text-muted);
    user-select: none;
}
.studio-pager-btn {
    width: 26px;
    height: 26px;
    padding: 0;
    border: 1px solid var(--border);
    background: var(--surface);
    border-radius: var(--radius-sm, 4px);
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    color: var(--text-muted);
    transition: background var(--motion-fast) var(--ease),
                border-color var(--motion-fast) var(--ease),
                color var(--motion-fast) var(--ease);
}
.studio-pager-btn:hover:not(:disabled) {
    background: var(--surface-soft);
    border-color: var(--border-strong);
    color: var(--text-strong);
}
.studio-pager-btn:disabled {
    cursor: not-allowed;
    opacity: 0.4;
}
.studio-pager-btn svg { display: block; }

.studio-pager-page {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 0 4px;
}
.studio-pager-label {
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
}
.studio-pager-input {
    width: 36px;
    height: 22px;
    padding: 0 4px;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm, 4px);
    background: var(--surface);
    color: var(--text-strong);
    font: inherit;
    font-size: 0.78rem;
    font-variant-numeric: tabular-nums;
    text-align: center;
    /* Hide spin buttons (Chromium) — we accept only digits anyway
       and the native arrows are visually noisy at this size. */
    appearance: textfield;
    -moz-appearance: textfield;
}
.studio-pager-input::-webkit-outer-spin-button,
.studio-pager-input::-webkit-inner-spin-button {
    -webkit-appearance: none;
    margin: 0;
}
.studio-pager-input:focus {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 2px var(--accent-ring);
}
.studio-pager-input:disabled {
    background: var(--surface-soft);
    color: var(--text-faint);
    cursor: not-allowed;
}

.studio-pager-sep {
    color: var(--border-strong);
    margin: 0 2px;
}

.studio-pager-select {
    height: 22px;
    padding: 0 6px 0 4px;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm, 4px);
    background: var(--surface);
    color: var(--text-strong);
    font: inherit;
    font-size: 0.78rem;
    cursor: pointer;
}
.studio-pager-select:focus {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 2px var(--accent-ring);
}
.studio-pager-select:disabled {
    background: var(--surface-soft);
    color: var(--text-faint);
    cursor: not-allowed;
}

/* Footer responsiveness is driven by the status bar's OWN width, not the
   viewport — the bar shrinks when the analysis rail / sidebars open, so a
   viewport breakpoint would miss those cases. `.studio-status-ct` wraps the
   bar and is a size-query container; the rules below react to its width.
   (The viewport @media fallback for old browsers without container queries
   is kept at the end of this block.) */
.studio-status-ct {
    container-type: inline-size;
    container-name: statusbar;
    flex: 0 0 auto;
}
/* Hide the static pager labels ("Page", "of", "Rows") once the bar is
   moderately narrow. */
@container statusbar (max-width: 1040px) {
    .studio-pager-label { display: none; }
    .studio-pager-page { gap: 2px; }
}
/* Genuinely narrow: the three lanes (status text · pager · actions) can't
   share one 40px row without clipping/overlap, so wrap into clean rows —
   status text on its own full-width row, then pager + actions (each able to
   wrap further). Desktop keeps the fixed, centered 3-column grid. */
@container statusbar (max-width: 820px) {
    .studio-status-bar {
        display: flex;
        flex-wrap: wrap;
        align-items: center;
        height: auto;
        min-height: 40px;
        max-height: none;
        overflow: visible;
        row-gap: 6px;
        padding-top: 6px;
        padding-bottom: 6px;
    }
    .studio-status-left { flex: 1 1 100%; justify-self: auto; overflow: visible; }
    .studio-status-hint { white-space: normal; overflow: visible; }
    .studio-status-center { flex: 0 0 auto; justify-self: auto; justify-content: flex-start; }
    .studio-status-right { flex: 1 1 auto; justify-self: auto; flex-wrap: wrap; justify-content: flex-end; }
}
/* Fallback for browsers without container-query support — keyed to the
   viewport. Less precise (ignores rail/sidebar width) but prevents a hard
   overlap on very narrow windows. */
@media (max-width: 1100px) {
    .studio-pager-label { display: none; }
    .studio-pager-page { gap: 2px; }
}

/* Export dropdown — trigger button stays in the status bar; the
   menu is portaled to document.body and positioned with `position:
   fixed` against the trigger's bounding rect. Portaling escapes the
   status bar's `overflow: hidden` clip (which is required to keep
   the bar at a fixed 40-px height). The inline `bottom`/`right`
   come from JS measurement. */
.studio-export-trigger {
    display: inline-flex;
    align-items: center;
    gap: 4px;
}
.studio-export-trigger svg {
    /* The trigger has both a download icon (Icon component) and a
       chevron. The chevron sits at the right; both get the same
       muted color via currentColor. */
    flex-shrink: 0;
}
.studio-export-menu {
    /* `position: fixed` is set inline; the rule below provides the
       fallback for any non-portaled rendering path and keeps the
       visual styling consistent. */
    min-width: 220px;
    background: var(--surface);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-3, 0 8px 24px rgba(15, 23, 42, 0.12));
    padding: 4px;
    z-index: 1100;  /* above modals (z:1000) so it's never clipped */
    display: flex;
    flex-direction: column;
    gap: 1px;
}
/* Format segmented toggle at the top of the export menu (CSV | SDF). */
.studio-export-format {
    display: flex;
    gap: 2px;
    padding: 2px;
    margin: 2px 2px 4px;
    background: var(--surface-sunk);
    border-radius: var(--radius-sm, 6px);
}
.studio-export-format-opt {
    flex: 1 1 0;
    padding: 4px 8px;
    border: 0;
    background: transparent;
    border-radius: var(--radius-sm, 4px);
    font: inherit;
    font-size: 0.78rem;
    font-weight: 600;
    color: var(--text-muted);
    cursor: pointer;
    transition: background var(--motion-fast) var(--ease), color var(--motion-fast) var(--ease);
}
.studio-export-format-opt:hover { color: var(--text); }
.studio-export-format-opt.is-active {
    background: var(--surface);
    color: var(--accent-strong);
    box-shadow: var(--shadow-1, 0 1px 2px rgba(15, 23, 42, 0.08));
}
.studio-export-menu-item {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 5px 9px;
    border: 0;
    background: transparent;
    border-radius: var(--radius-sm, 4px);
    font: inherit;
    font-size: 0.83rem;
    color: var(--text);
    text-align: left;
    cursor: pointer;
    transition: background var(--motion-fast) var(--ease);
}
.studio-export-menu-item:hover:not(:disabled) {
    background: var(--surface-soft);
}
.studio-export-menu-item:disabled {
    color: var(--text-faint);
    cursor: not-allowed;
}
.studio-export-menu-item-strong { font-weight: 600; }
.studio-export-menu-label { flex: 1; min-width: 0; }
.studio-export-menu-count {
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
    font-size: 0.78rem;
}
/* Compact action buttons inside the status bar — same visual
   height as the status text so the bar reads as a single
   horizontal row. The base .btn-small padding made these too
   tall; we shrink them just for this context. */
.studio-status-bar .btn-small {
    padding: 0.2rem 0.625rem;
    font-size: 0.78rem;
    line-height: 1.4;
    /* SVG icon glyphs in btn-small were 14 px; trim slightly so
       the icon + label hug the 40 px bar height. */
    height: 26px;
}
.studio-status-bar .btn-small svg { width: 12px; height: 12px; }
.studio-status-count {
    font-size: 0.875rem;
    font-weight: 600;
    color: var(--accent-strong);
    white-space: nowrap;
}
.studio-status-sep {
    color: var(--border-strong);
    font-weight: 400;
    user-select: none;
}
.studio-status-hint {
    font-size: 0.825rem;
    color: var(--text-muted);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
/* Inline "(N hidden)" affordance — looks like a text link inside
   the otherwise quiet status hint. Underlined so users can see it
   is interactive without a separate button. Uses the same muted
   color as the surrounding text in idle state and lifts to the
   accent color on hover/focus. */
.studio-status-restore-link {
    background: transparent;
    border: 0;
    padding: 0;
    margin: 0;
    font: inherit;
    color: inherit;
    text-decoration: underline;
    text-underline-offset: 2px;
    cursor: pointer;
    /* Reset the default button alignment so the link reads as
       inline text and doesn't bump the surrounding line height. */
    line-height: inherit;
    vertical-align: baseline;
}
.studio-status-restore-link:hover,
.studio-status-restore-link:focus-visible {
    color: var(--accent-strong);
    text-decoration: underline;
}
.studio-status-restore-link:focus-visible {
    outline: 2px solid var(--accent-ring);
    outline-offset: 2px;
    border-radius: 2px;
}

/* Disabled studio sidebar action (e.g. when no sheet is open) */
.studio-action-disabled {
    opacity: 0.45;
    cursor: not-allowed;
}
.studio-action-disabled:hover { background: transparent; color: var(--text); }
.studio-action-disabled:hover .studio-action-icon { color: var(--text-muted); }

/* Active studio sidebar action — Columns/Filter when their drawer is open. */
.studio-action.studio-action-active {
    background: var(--accent-soft);
    color: var(--accent-strong);
    font-weight: 600;
    box-shadow: inset 2px 0 0 var(--accent);
}
.studio-action.studio-action-active .studio-action-icon { color: var(--accent); }

/* Right-click context menu — same visual language as tab dropdown. */
.studio-context-menu {
    min-width: 240px;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-3);
    padding: 4px;
    z-index: 200;
    animation: slideUpFade 0.14s var(--ease);
}
.studio-header-menu {
    min-width: 200px;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-3);
    padding: 4px;
    z-index: 200;
    animation: slideUpFade 0.14s var(--ease);
}
.studio-tab-menu-item-disabled {
    opacity: 0.4;
    cursor: not-allowed;
}
/* Numeric column cell-colour control (inside the header menu) */
.studio-colfmt { display: flex; flex-direction: column; gap: 6px; padding: 6px 8px; }
.studio-colfmt-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.studio-colfmt-title { font-size: 0.72rem; font-weight: 600; color: var(--text-muted); }
.studio-colfmt-actions { display: flex; align-items: center; gap: 4px; }
.studio-colfmt-act {
    display: inline-flex; align-items: center; justify-content: center;
    width: 22px; height: 22px; padding: 0;
    color: var(--text-muted); background: transparent;
    border: 1px solid var(--border-strong); border-radius: var(--radius-sm); cursor: pointer;
    transition: border-color var(--motion-fast) var(--ease), color var(--motion-fast) var(--ease), background var(--motion-fast) var(--ease);
}
.studio-colfmt-act:hover { border-color: var(--accent); color: var(--accent-strong); }
.studio-colfmt-act.is-danger:hover { border-color: var(--danger-strong, #be123c); color: var(--danger-strong, #be123c); background: var(--danger-bg, transparent); }
/* Low swatch · gradient bar · High swatch — reuses the plot's .viz-grad-ctrl
   so the column-header color control matches the settings Color scale. */
.studio-tab-menu-item-disabled:hover { background: transparent; color: var(--text); }

/* ── Column header redesign ───────────────────────────────────────
   The th is a positioned container. Three slots:
     · Drag grip on the LEFT (absolute, hover/focus reveal)
     · Centered label area (full width — grip and arrow do NOT push
       the label off-center because they're absolutely positioned)
     · Chevron menu button on the RIGHT (absolute, same reveal logic)
     · Resize handle on the right edge (absolute, overflows by 5px)
   The th itself is no longer draggable — only the grip is — so users
   can no longer accidentally start a reorder by clicking anywhere on
   the header.
   The label wraps freely on narrow columns (one char per line in the
   extreme), and if the wrapped text exceeds the th's content height
   it scrolls vertically inside the label container. */
.studio-table thead th.studio-table-sortable,
.studio-table thead th.studio-table-structure-cell {
    overflow: visible;            /* let resize handle + grip/arrow escape */
    /* Keep position: sticky from the base .studio-table thead th rule
       — sticky elements still establish a positioning context for
       absolute children (grip, arrow, resize handle), so we get sticky
       headers AND absolutely-positioned controls in one element. The
       earlier `position: relative` override silently disabled stickiness
       across the entire header row. */
    padding: 0;                   /* padding lives on .studio-th-content */
}

/* Per-density thead height — ths share the natural single-line height
   plus a comfortable amount of vertical padding. Long names that need
   to wrap use the same height + the inner scroll container. */
.studio-table-density-compact     thead tr { height: 36px; }
.studio-table-density-comfortable thead tr { height: 44px; }
.studio-table-density-spacious    thead tr { height: 56px; }
.studio-table-density-custom      thead tr { height: 44px; }

/* Centered label slot — fills the th interior. The horizontal padding
   reserves room for the absolutely-positioned grip and arrow so that
   the label's geometric centre always equals the th's centre, no
   matter whether the grip/arrow are currently visible. */
.studio-th-content {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 4px 24px;
    overflow: hidden;
    pointer-events: none;
}
/* Structure header has no grip/arrow, so it doesn't need the side
   reservation — gives the static label more room before wrapping. */
.studio-table thead th.studio-table-structure-cell .studio-th-content {
    padding: 4px 10px;
}
/* The label IS clickable (for sorting). Re-enable pointer events on
   the inner span. */
.studio-th-content > * { pointer-events: auto; }

.studio-table thead th .studio-table-th-label {
    /* Plain text — the click target lives on the parent th, so the
       label is no longer button-shaped and is no longer focusable.
       `height: 100%` gives the inner text a DEFINITE height to resolve
       its `max-height: 100%` against (the th has a fixed per-density
       height), which is what lets the text scroll only when its wrapped
       lines actually exceed the header. The sort badge sits beside the
       text on one row (flex-wrap: nowrap); the text wraps internally. */
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.25rem;
    flex-wrap: nowrap;
    max-width: 100%;
    height: 100%;
    min-width: 0;
    color: inherit;
}
/* Belt-and-braces: even if some other code path adds tabindex to the
   label, suppress the field-like focus rectangle. The hover/selected
   states live on the parent th, so a focus ring on the label would
   read as a redundant inner highlight. */
.studio-table thead th .studio-table-th-label:focus,
.studio-table thead th .studio-table-th-label:focus-visible {
    outline: none;
    box-shadow: none;
}
.studio-table thead th .studio-table-th-label-text {
    /* WRAP the column name (down to character-by-character on very
       narrow columns) so the width stays independent of the name
       length. The text only SCROLLS when its wrapped lines genuinely
       exceed the header height: the shared overflow detector measures
       scrollHeight − clientHeight > 1px and toggles `.is-overflowing-y`,
       so a name that fits on one or several lines shows NO scrollbar and
       there's no stray hover scrollbar (the old 1–2px false-overflow
       bug). Default overflow-y is hidden; the `.is-overflowing-y` rule
       below flips it to a subtle thin scrollbar. */
    white-space: normal;
    word-break: break-word;
    overflow-wrap: anywhere;
    min-width: 0;
    max-height: 100%;
    overflow-y: hidden;
    line-height: 1.2;
    text-align: center;
    font-weight: 600;
    scrollbar-width: none;
    scrollbar-color: var(--border-strong) transparent;
}
/* Real overflow only (set by scheduleOverflowMeasure with a >1px
   threshold) → reveal a subtle thin scrollbar so the full name is
   reachable. No hover toggle, so the header never jitters. */
.studio-table thead th .studio-table-th-label-text.is-overflowing-y {
    overflow-y: auto;
    scrollbar-width: thin;
}
.studio-table thead th .studio-table-th-label-text::-webkit-scrollbar { width: 0; }
.studio-table thead th .studio-table-th-label-text.is-overflowing-y::-webkit-scrollbar { width: 4px; }
.studio-table thead th .studio-table-th-label-text.is-overflowing-y::-webkit-scrollbar-thumb {
    background: var(--border-strong); border-radius: 2px;
}
/* Structure col uses the centered label slot too but doesn't need
   sort/menu/grip behaviour. */
.studio-th-static-label {
    color: var(--text-muted);
    font-weight: 600;
    text-align: center;
    line-height: 1.2;
    white-space: normal;
    word-break: break-word;
    overflow-wrap: anywhere;
}
/* Label hover removed — the th-level hover at .studio-table-sortable:hover
   provides the same cue across the whole header, so this inner tint
   would read as a button-like inner highlight. */

/* Sort badge — kept inline with the wrapped text. */
.studio-table-th-sort {
    display: inline-flex;
    align-items: center;
    color: var(--accent);
    flex-shrink: 0;
}

/* Sorted indicator — a slim accent rule along the th's bottom edge.
   Drawn via ::after with absolute positioning so the line spans the
   FULL width of the th, including under the grip and dropdown strips
   (a `box-shadow: inset` was previously occluded by their backgrounds
   on hover). z-index above grip/menu/resize strips guarantees the
   line is always continuous and visible. */
.studio-table thead th.studio-th-sorted::after {
    content: '';
    position: absolute;
    left: 0; right: 0;
    /* `bottom: 0` keeps the 2 px accent rule INSIDE the th (above its
       border-bottom) so it never overflows into the first data row nor
       leaks through the frozen-column seam when a selected/sorted column
       is scrolled under a frozen header. The th's border-bottom is
       recoloured to --accent for sorted/selected columns (rule below),
       so the underline + border read as one continuous accent edge down
       to the header↔body boundary — no grey gap, no overflow. */
    bottom: 0;
    height: 2px;
    background: var(--accent);
    z-index: 6;
    pointer-events: none;
}

/* ── Selected column (Excel-style) ────────────────────────────────
   Single-clicking a header label sets the column as selected. The
   header cell tints to --accent-muted, every body cell in the same
   column gets a paler --accent-soft wash, and a 2 px accent rule sits
   on the th's bottom edge. The wash uses the same hue family as the
   selected-row treatment so the two read as siblings; the difference
   is direction (vertical vs horizontal). */
.studio-table thead th.studio-th-selected {
    background: var(--accent-muted);
    color: var(--accent-strong);
}
.studio-table thead th.studio-th-selected::after {
    content: '';
    position: absolute;
    left: 0; right: 0;
    /* See .studio-th-sorted::after — clipped inside the th (bottom: 0)
       with an accent border-bottom below it, so the highlight reaches the
       header↔body boundary cleanly without overflowing or leaking through
       the frozen seam. */
    bottom: 0;
    height: 2px;
    background: var(--accent);
    z-index: 6;
    pointer-events: none;
}
/* Recolour the header↔body separator to accent for a sorted/selected
   column so the clipped underline above it forms one continuous accent
   edge with no grey gap. The border is part of the th's box, so it never
   overflows into the data row and is masked by the frozen header (its
   own opaque border-box) when scrolled under a frozen column. */
.studio-table thead th.studio-th-sorted,
.studio-table thead th.studio-th-selected {
    border-bottom-color: var(--accent);
}
/* Side zones inside a selected header — both strips drop their own
   backgrounds so the th's selected fill reads continuously. The
   grip's right separator (drawn via box-shadow) also goes transparent
   so the highlight isn't visually broken in two. The selectors are
   chained through the th element to raise specificity above the
   `thead th:hover .studio-th-grip` reveal-on-hover rule, so the
   continuous look survives a hover-on-a-selected-header. Hover
   treatments come back via :hover-on-strip below for direct mouse-in
   interactions.

   The grip's dotted SVG uses currentColor; in the selected state we
   inherit the th's accent-strong text colour so the dots match the
   menu chevron's saturation. Without this the dots stayed
   --text-faint (grey) and looked washed against the accent-muted
   selected background while the menu chevron read crisply. */
/* In selected state both side strips become darker than the th's
   centre so the grip + menu read as a "darker frame" around the
   highlighted column header. The strips share the same dark accent
   bg + light text, so they look like one continuous selected zone
   regardless of which side the user looks at. */
.studio-table thead th.studio-th-selected .studio-th-grip {
    background: var(--accent);
    box-shadow: inset -1px 0 0 transparent;
    color: var(--accent-fg);
}
.studio-table thead th.studio-th-selected .studio-th-grip svg { opacity: 1; }
/* Bump the dot radius slightly in selected state so the row of dots
   reads at the same visual weight as the menu chevron on the right. */
.studio-table thead th.studio-th-selected .studio-th-grip svg circle {
    r: 1.4;
}
/* Right menu strip stays neutral at rest in the selected state —
   it inherits the th's --accent-muted background, matching the
   centre header area. Only the LEFT grip strip gets the dark
   "you can drag me" treatment per the previous request. The chevron
   in selected-rest is in --accent-strong on the light bg for clear
   contrast. */
.studio-table thead th.studio-th-selected .studio-header-menu-btn {
    background: transparent;
    border-left-color: transparent;
    color: var(--accent-strong);
}
.studio-table thead th.studio-th-selected .studio-header-menu-btn svg { opacity: 1; }
/* In the selected state, the LEFT grip strip locks into its rest
   appearance — re-asserting the rest values here (specificity 0,3,3)
   beats the unselected `.studio-th-grip:hover` rule (0,2,0) so the
   grip never flickers when the cursor enters a selected header.
   The RIGHT menu strip keeps its hover feedback so users still see
   "click here to open the menu" — selected rest is neutral, hover
   flips to a dark accent fill, matching the visual grammar of the
   unselected state. */
.studio-table thead th.studio-th-selected .studio-th-grip:hover {
    background: var(--accent);
    color: var(--accent-fg);
    box-shadow: inset -1px 0 0 transparent;
}
.studio-table thead th.studio-th-selected .studio-header-menu-btn:hover {
    background: var(--accent);
    color: var(--accent-fg);
    border-left-color: var(--accent);
}

/* Body cells in a selected column. --accent-soft is paler than the
   header's --accent-muted, giving the column a tapered "stronger at
   the top, calmer down the body" appearance. The td.* form raises
   specificity so this wins over `tr:hover td` (which has no
   !important but is more specific than a single .class selector). */
/* Column selection uses a TRANSLUCENT inset overlay (box-shadow) rather
   than an opaque background, so a conditionally-colored cell keeps its
   gradient visible underneath while still reading as selected. The soft
   background is a non-!important bonus for plain cells (an inline cell
   color wins over it; the overlay layers on top of either). */
td.studio-col-selected {
    background: var(--accent-soft);
    box-shadow: inset 0 0 0 9999px var(--accent-ring);
}
/* Selected row × selected column intersection — a second translucent
   layer reads as the strongest emphasis without hiding cell colors. */
.studio-row-selected td.studio-col-selected,
.studio-row-selected:hover td.studio-col-selected {
    box-shadow: inset 0 0 0 9999px var(--accent-ring), inset 0 0 0 9999px var(--accent-ring);
}
/* Frozen rows are amber-tinted; in a selected column the warm + cool
   tones would clash. Keep frozen amber dominant (since pinning is the
   rarer / more structural state) and signal column selection via a
   slim accent stripe along the cell's left edge. */
/* Specificity bumped to .studio-table tbody tr.studio-row-frozen
   so this still wins after the frozen-row rule above was qualified
   to (0,2,3) to override custom-wrap's position:relative. */
.studio-table tbody tr.studio-row-frozen td.studio-col-selected {
    background: var(--frozen-bg-hover) !important;
    box-shadow: inset 3px 0 0 var(--accent);
}
/* Active cell ring stays on top of the column tint — no change needed
   since it's drawn via ::after at z-index 3. The dashed copied-cell
   ring also stays visible for the same reason. */

/* ── Drag grip ────────────────────────────────────────────────────
   The grip is a full-height strip on the LEFT of every header. The
   dotted glyph stays visually subtle, but the entire strip is the
   grab target — so users don't have to aim at the tiny dots.
   Distinct neutral hover (slate tint) marks "drag region", versus
   the dropdown arrow's accent-soft hover for "menu region". */
.studio-th-grip {
    position: absolute;
    top: 0; bottom: 0;
    /* Anchored to the th's left edge with no offset. The previous
       column's resize handle (z-index 5) correctly overlays the
       grip's z-index-4 leftmost 5 px at the boundary, so we don't
       need an extra padding offset. Width is 18 px — generous hit
       area, evenly divided so the 8-px-wide dot SVG centres
       exactly via flex with no subpixel drift. The right-edge
       separator is drawn via `box-shadow` rather than `border-right`
       so it doesn't consume 1 px of the box and re-introduce the
       half-pixel SVG offset bug we previously hit. The shadow itself
       is hidden at rest (transparent) — it only fades in when the
       user is actually interacting with the column header. */
    left: 0;
    width: 18px;
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--text-faint);
    background: transparent;
    box-shadow: inset -1px 0 0 transparent;
    /* cursor: move set by the dedicated rule below — see the comment
       there for the OS-native rendering rationale. */
    opacity: 1;
    transition: background var(--motion-fast) var(--ease),
                box-shadow var(--motion-fast) var(--ease),
                color var(--motion-fast) var(--ease);
    z-index: 4;
    user-select: none;
}
/* Grip separator reveals only when the column header is being
   interacted with — and only when the th is NOT in the selected
   state. The :not(.studio-th-selected) clause is what fixes the
   selected-grip flicker: the th:hover rule used to tie on
   specificity with the selected-state rule and win on source order,
   so a cursor moving across a selected header would briefly flash
   the unselected hover separator. Scoping this reveal to the
   unselected case lets the selected rules own the grip in selected
   state without extra specificity gymnastics.
   No layout shift either way — box-shadow doesn't take space. */
.studio-table thead th:not(.studio-th-selected):hover .studio-th-grip,
.studio-table thead th:not(.studio-th-selected):focus-within .studio-th-grip {
    box-shadow: inset -1px 0 0 var(--border-faint);
}
/* Cursor choice rationale (see also the body.* rules below):
   Chrome on Windows ships its OWN bitmaps for `grab` / `grabbing`
   and re-rasterises them at non-integer display scaling (125%, 150%)
   which makes them look soft. The OS-native `move` cursor (4-arrow
   cross) is always sharp because the system supplies the bitmap at
   the current scale. The grip's job is "drag this column to reorder",
   so `move` reads correctly even though `grab` is more semantic. */
.studio-th-grip { cursor: move; }
.studio-th-grip:active { cursor: move; }
.studio-th-grip svg {
    fill: currentColor;
    display: block;
    opacity: 0;
    transition: opacity var(--motion-fast) var(--ease);
}
/* Same :not(.studio-th-selected) scoping as the box-shadow rule
   above. The selected-state svg opacity (1) is set by a sibling rule
   later in the file; without this scoping, th:hover would tie on
   specificity and win on source order, flashing the dots back to
   0.7 every time the cursor enters a selected header. */
.studio-table thead th:not(.studio-th-selected):hover .studio-th-grip svg,
.studio-table thead th:not(.studio-th-selected):focus-within .studio-th-grip svg {
    opacity: 0.7;
}
/* Direct hover on the grip strip — uses the SAME treatment as
   .studio-header-menu-btn:hover so the two side interaction zones
   feel visually consistent in the unselected state. The right
   separator (drawn via box-shadow) bumps to --accent-ring to match
   the menu's border-left tint when hovered. The grip's drag
   semantics are conveyed by the cursor change (move) and by the
   dots becoming fully opaque, not by a different colour. */
.studio-th-grip:hover {
    background: var(--accent-muted);
    color: var(--accent-strong);
    box-shadow: inset -1px 0 0 var(--accent-ring);
}
.studio-th-grip:hover svg { opacity: 1; }
.studio-th-grip:focus-visible {
    outline: 2px solid var(--accent-ring);
    outline-offset: -2px;
}

/* ── Dropdown arrow — far right, full-height strip ───────────────
   Mirrors the grip in shape but uses an accent-soft hover so users
   can tell the two interactive regions apart at a glance: grip =
   drag (slate), arrow = menu (blue). */
.studio-header-menu-btn {
    position: absolute;
    top: 0; bottom: 0;
    right: 0;
    width: 22px;
    border: none;
    border-left: 1px solid transparent;
    border-radius: 0;
    background: transparent;
    color: var(--text-muted);
    display: inline-flex; align-items: center; justify-content: center;
    cursor: pointer;
    overflow: hidden;
    z-index: 4;
    transition: background var(--motion-fast) var(--ease),
                color var(--motion-fast) var(--ease),
                border-color var(--motion-fast) var(--ease);
}
.studio-header-menu-btn svg {
    opacity: 0;
    width: 14px; height: 14px;
    stroke: currentColor; fill: none;
    stroke-width: 2.5;
    transition: opacity var(--motion-fast) var(--ease);
}
.studio-table thead th:hover .studio-header-menu-btn svg,
.studio-table thead th:focus-within .studio-header-menu-btn svg {
    opacity: 0.85;
}
/* Menu hover uses the *saturated* accent tint (--accent-muted is darker
   than --accent-soft), specifically so the right strip reads visibly
   different from the label hover (which uses the paler --accent-soft).
   Together with the grip's neutral hover, the user sees three clearly
   distinct interactive zones. */
.studio-header-menu-btn:hover {
    background: var(--accent-muted);
    color: var(--accent-strong);
    border-left-color: var(--accent-ring);
}
.studio-header-menu-btn:hover svg { opacity: 1; }
.studio-header-menu-btn:focus-visible {
    outline: 2px solid var(--accent-ring);
    outline-offset: -2px;
}

/* Column resize handle — clean invisible hit zone. No gradient/pseudo
   elements that could be perceived as a custom cursor. The browser-native
   `col-resize` keyword is the ONLY visual cue. The handle is intentionally
   wider than visually expected so the cursor never flickers near the edge.
   z-index above the grip + arrow so the resize cursor reliably wins at the
   th's right edge — without this, the next column's grip (z-index 4) was
   visually overlapping the resize zone and the cursor flickered to grab. */
.studio-col-resize {
    position: absolute;
    top: 0; right: -5px;
    width: 10px; height: 100%;
    /* Use ew-resize instead of col-resize. They're aliases per spec
       but Chrome on Windows ships separate bitmaps and the ew-resize
       variant is rendered by the OS (sharp) on most installations,
       while col-resize uses Chrome's own bitmap which scales softly
       at 125% / 150% Windows scaling. row-resize already uses the
       OS cursor — that's why row-height resize stayed crisp while
       col-resize looked blurry. */
    cursor: ew-resize;
    background: transparent;
    z-index: 5;
    user-select: none;
    touch-action: none;
}
/* Subtle hover indicator: a single thin solid bar on the column boundary,
   drawn as a child via box-shadow so it cannot interfere with the cursor. */
.studio-col-resize:hover {
    box-shadow: inset 0 0 0 0 transparent;
}
.studio-col-resize:hover::after {
    content: "";
    position: absolute;
    top: 0; bottom: 0;
    left: 50%;
    width: 2px;
    margin-left: -1px;
    background: var(--accent);
    opacity: 0.4;
    pointer-events: none;
}

/* (Header drag-grip styling now lives near the column-header redesign
   above. The grip is the only drag source; the th is no longer the
   drag-source itself.) */

/* Header drag-and-drop highlight — swap-based, so the WHOLE target
   column header is highlighted (full inset ring + soft fill) to read as
   "drop here to swap with this column", rather than a one-edge
   insertion line. Used by both the single-column swap drag and the
   whole-group band drag. */
.studio-th-drop {
    box-shadow: inset 0 0 0 2px var(--accent);
    background: var(--accent-soft) !important;
}

/* Header dropdown shares the same icon sizing as other context menus
   so menus across the page feel consistent. */
.studio-header-menu .studio-tab-menu-item svg { width: 14px; height: 14px; }

/* ── Columns / Filter drawer (push layout) ────────────────────────
   The drawer is a flex sibling of .studio-content inside the
   .studio-workspace row. When open, it flexes to 320 px and pushes
   the sheet aside; when closed it collapses to 0. The drawer sits
   BELOW the toolbar — opening it never shifts the Project / Design
   Sheet row. No portal, no overlay, no backdrop. */
/* Scrim that fades in over the workspace when an overlay drawer is
   open. Visually anchors the drawer as the active surface and
   surfaces click-outside-to-close. Sits BELOW the drawer (z=8 vs
   the drawer's z=10), so the drawer's own border + shadow stay
   crisp. We use an rgba background-color only — no `backdrop-filter`
   or `filter: blur()`, both of which we measured paint at 30+ ms per
   scroll tick on a 500-row sheet on 4K displays. The fade-in
   matches the drawer's slide-in duration so they feel unified. */
.studio-drawer-scrim {
    position: absolute;
    inset: 0;
    background: rgba(15, 23, 42, 0.18);
    opacity: 0;
    pointer-events: none;
    z-index: 8;
    transition: opacity 0.22s var(--ease);
}
.studio-drawer-scrim.is-open {
    opacity: 1;
    pointer-events: auto;
    cursor: pointer;
}

.studio-drawer-host {
    /* Floating overlay strategy: previous design pushed the drawer
       into the flex flow, which forced the table beside it to
       re-lay out (column-width redistribution, sticky-header
       reflow, scrollbar lane recompute) on every open/close.

       Animation strategy: the drawer is kept at a fixed 320 px width
       and slid in via `transform: translateX(...)` instead of the
       previous width-animated approach. width transitions invalidate
       layout every frame (even with overflow:hidden the browser
       schedules reflow work for the host's box), which on a 7 000-row
       sheet produced visible frame drops during the slide. Transform
       is composite-only — the compositor moves the existing layer
       without touching layout/paint, so the slide stays smooth even
       when the underlying table has thousands of rows.
       `will-change: transform` hints the browser to promote this
       element to its own compositor layer up-front, avoiding a
       layer-creation hiccup on the first frame.

       The drawer is hidden via translateX(-100%) when closed so it
       sits entirely off-screen to the left; the parent
       .studio-workspace has overflow: hidden so the off-screen
       portion never paints. pointer-events: none keeps clicks
       passing through to the scrim / table behind it. */
    position: absolute;
    top: 0;
    bottom: 0;
    /* Anchored to the LEFT edge of the workspace area so the drawer
       sits next to the sidebar (which is to its left in the page
       flex row). The parent .studio-workspace is `position: relative`
       so absolute children anchor inside it. */
    left: 0;
    width: 320px;
    background: var(--surface);
    overflow: hidden;
    transform: translateX(-100%);
    /* Only `transform` is animated. border-color and box-shadow are
       intentionally NOT in the transition list — animating
       box-shadow on a 320×viewport-height element is a paint-heavy
       per-frame operation (the browser interpolates blur/spread
       and repaints the entire shadow region every tick), which
       reintroduces the same per-frame main-thread cost we just
       avoided by switching width → transform. The shadow snaps in
       at the end of the transition, which reads cleanly because
       the drawer is sliding INTO view rather than fading. */
    transition: transform 0.22s var(--ease);
    will-change: transform;
    border-right: 1px solid transparent;
    z-index: 10;
    pointer-events: none;
}
.studio-drawer-host.open {
    transform: translateX(0);
    border-right: 1px solid var(--border-strong);
    pointer-events: auto;
    box-shadow: var(--shadow-3, 0 8px 24px rgba(15, 23, 42, 0.06));
}
@media (max-width: 900px) {
    /* On narrow viewports, the drawer takes the full width. */
    .studio-drawer-host { width: 100%; }
}
.studio-drawer {
    width: 320px;
    height: 100%;
    background: var(--surface);
    display: flex; flex-direction: column;
}
@media (max-width: 900px) {
    /* width: 100% is now applied to the host directly above (regardless
       of open state) — drawers slide in via transform on narrow
       viewports too. The fixed-width drawer-inside still sets 100% so
       its content fills the host. */
    .studio-drawer { width: 100%; }
}
.studio-drawer-header {
    flex: 0 0 auto;
    display: flex; align-items: center; justify-content: space-between;
    padding: 1rem 1.125rem;
    border-bottom: 1px solid var(--border);
}
.studio-drawer-header h3 {
    font-size: 1.05rem; font-weight: 700;
    color: var(--text-strong);
    letter-spacing: -0.01em;
    margin: 0;
}
.studio-drawer-section {
    padding: 0.875rem 1.125rem;
    border-bottom: 1px solid var(--border-faint);
}
.studio-density-toggle {
    display: flex; gap: 4px;
    padding: 3px;
    background: var(--surface-sunk);
    border-radius: var(--radius-md);
}
.studio-density-btn {
    flex: 1;
    padding: 0.4rem 0.5rem;
    background: transparent; border: none;
    border-radius: var(--radius-sm);
    color: var(--text-muted);
    font: inherit; font-size: 0.82rem; font-weight: 500;
    cursor: pointer;
    transition: background var(--motion-fast) var(--ease), color var(--motion-fast) var(--ease);
}
.studio-density-btn:hover { color: var(--text-strong); }
.studio-density-btn.active {
    background: var(--surface);
    color: var(--text-strong); font-weight: 600;
    box-shadow: var(--shadow-1);
}
/* Top action row inside ColumnsDrawer — sits above the search bar
   so the primary create / add actions are the first thing the user
   sees. The two buttons split equal width to keep the visual
   hierarchy balanced. */
.studio-drawer-top-actions {
    display: flex;
    gap: 6px;
    /* 1rem horizontal so the Add/Create buttons line up with the search
       box and column rows below (and the Filter/Tools sidebars). */
    padding: 0.75rem 1rem 0.625rem;
    border-bottom: 1px solid var(--border-faint);
    background: var(--surface);
}
.studio-drawer-top-btn {
    flex: 1;
    min-width: 0;
    /* Force flex centering on both axes regardless of any default
       inline-block/text-align inheritance. The previous style was
       inline-flex and so the button's own text-align was still
       affecting horizontal alignment when the icon's intrinsic
       baseline tilted the row off-center. line-height: 1 strips
       extra vertical leading inside btn-small so the icon and the
       label visually share a midline. */
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.375rem;
    line-height: 1;
    text-align: center;
    white-space: nowrap;
}
.studio-drawer-top-btn svg {
    flex-shrink: 0;
    width: 14px; height: 14px;
    /* Drop SVG's baseline-aligned default — inside a flex parent the
       align-items rule already centers it, but lifting the baseline
       here makes the icon visually balance with the label glyph
       midline rather than its descender. */
    display: block;
}
.studio-drawer-top-btn > span {
    display: inline-block;
    line-height: 1;
}
.studio-drawer-search {
    /* Horizontal 1rem matches the Filter drawer body + the shared header,
       so the Columns drawer reads consistently with the other sidebars.
       (List + hidden-notice + actions below use the same 1rem inset.) */
    padding: 0.875rem 1rem 0.625rem;
}
.studio-drawer-search .studio-search-input {
    background: var(--surface-soft);
    /* Same border bump as the column / filter / model cards so the
       drawer's interactive surfaces all read as one consistent
       family of edges. */
    border-color: var(--border-strong);
}
.studio-drawer-list {
    flex: 1; min-height: 0;
    overflow-y: auto;
    /* SYMMETRIC 1rem left/right by default, so with no scrollbar the cards are
       balanced in the panel. `.has-vscroll` (set by useVScrollPadding only when
       the list actually overflows) tightens the RIGHT inset to 0.5rem so the
       gap to the scrollbar doesn't read as oversized. Left always stays 1rem to
       keep cards aligned with the search box/header above. */
    padding: 0.25rem 1rem 0.5rem 1rem;
}
.studio-drawer-list.has-vscroll { padding-right: 0.5rem; }

/* Hidden-columns notice — only visible when at least one column is
   hidden. Lets users restore everything in one click instead of
   toggling each eye icon. */
.studio-drawer-hidden-notice {
    display: flex; align-items: center; justify-content: space-between;
    margin: 0 1rem 0.375rem;
    padding: 0.4rem 0.625rem;
    background: var(--accent-soft);
    border: 1px solid var(--info-border);
    border-radius: var(--radius-sm);
    font-size: 0.78rem;
    color: var(--accent-strong);
    font-weight: 500;
}
.studio-drawer-hidden-restore {
    background: transparent;
    border: none;
    color: var(--accent-strong);
    font: inherit;
    font-weight: 600;
    text-decoration: underline;
    cursor: pointer;
    padding: 0;
}
.studio-drawer-hidden-restore:hover { color: var(--accent); }
.studio-column-row {
    display: flex; align-items: center; gap: 0.5rem;
    /* Slightly reduced vertical padding + gap for a denser, more
       refined Columns bar without cramping the click/drag target. */
    padding: 0.4rem 0.625rem 0.4rem 0.5rem;
    margin-bottom: 4px;
    border-radius: var(--radius-md);
    /* Slightly stronger border than the generic page surface so column
       cards stand out crisply inside the drawer body — prior `--border`
       value blended into the drawer surface and made the card edges
       hard to read. */
    border: 1px solid var(--border-strong);
    background: var(--surface-soft);
    transition: background var(--motion-fast) var(--ease),
                border-color var(--motion-fast) var(--ease),
                box-shadow var(--motion-fast) var(--ease);
    cursor: grab;
}
.studio-column-row:hover { background: var(--surface); border-color: var(--border-strong); box-shadow: var(--shadow-1); }
.studio-column-row.studio-column-row-selected {
    border-color: var(--accent);
    background: var(--accent-soft);
}
.studio-column-row.studio-column-row-dragging { opacity: 0.4; }
.studio-column-row.studio-column-row-drop { border-color: var(--accent); background: var(--accent-muted); }
.studio-column-handle {
    flex-shrink: 0;
    width: 12px;
    color: var(--text-faint);
    display: flex; align-items: center; justify-content: center;
    cursor: grab;
    user-select: none;
}
.studio-column-handle::before {
    content: "⋮⋮";
    font-weight: 800;
    letter-spacing: -2px;
    line-height: 1;
}
.studio-column-row .studio-col-check {
    flex-shrink: 0;
    width: 14px; height: 14px;
    accent-color: var(--accent);
    cursor: pointer;
    margin: 0;
}
.studio-drawer-actions {
    padding: 0.625rem 1rem;
    border-top: 1px solid var(--border-faint);
    /* Layout: [action group — flush left] … [clear — pinned right].
       The action group sits at the left edge of the strip and the
       Clear button pins to the right via margin-left:auto on the
       .studio-drawer-actions-clear element. The strip stays the
       same width whether 1 or N cards are selected. */
    display: flex; align-items: center; gap: 6px;
    background: var(--surface-soft);
}
.studio-drawer-actions-group {
    display: flex; align-items: center;
    gap: 4px;
    flex-shrink: 0;
}
/* Clear-selection icon is pinned to the far right of the strip
   regardless of whether other action buttons render. */
.studio-drawer-actions-clear {
    margin-left: auto;
    flex-shrink: 0;
}
.studio-drawer-actions .btn-small {
    padding: 0.3rem 0.55rem;
    font-size: 0.78rem;
}
.studio-drawer-icon-btn {
    width: 26px; height: 26px;
    display: inline-flex; align-items: center; justify-content: center;
    border: 1px solid var(--border);
    background: var(--surface);
    color: var(--text-muted);
    border-radius: var(--radius-sm);
    cursor: pointer;
    transition: color var(--motion-fast) var(--ease), border-color var(--motion-fast) var(--ease), background var(--motion-fast) var(--ease);
}
.studio-drawer-icon-btn:hover:not(:disabled) {
    color: var(--accent);
    border-color: var(--accent-ring);
    background: var(--accent-soft);
}
.studio-drawer-icon-btn:disabled {
    opacity: 0.4;
    cursor: not-allowed;
}
.studio-column-vis {
    flex-shrink: 0;
    width: 28px; height: 28px;
    border: 1px solid var(--border);
    background: var(--surface);
    color: var(--text-muted);
    border-radius: var(--radius-sm);
    display: flex; align-items: center; justify-content: center;
    cursor: pointer;
    transition: color var(--motion-fast) var(--ease), border-color var(--motion-fast) var(--ease);
}
.studio-column-vis:hover { color: var(--accent); border-color: var(--accent-ring); }
.studio-column-vis svg { display: block; stroke-linecap: round; stroke-linejoin: round; }
/* Spacer in place of the eye/hide button for virtual columns
   (matched-structure previews). Keeps column-info aligned with
   non-virtual rows, no border or hover affordance — it's not
   interactive. */
.studio-column-vis-virtual {
    border: none;
    background: transparent;
    cursor: default;
    pointer-events: none;
}
.studio-column-info { flex: 1; min-width: 0; }
.studio-column-name {
    font-size: 0.875rem; font-weight: 600;
    line-height: 1.2;
    color: var(--text-strong);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    display: flex; align-items: center; gap: 0.375rem;
}
.studio-column-name-text {
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    flex: 1; min-width: 0;
}
/* Lock badge — tiny rounded chip before the column name on read-only
   cards. Read-only cards keep the same shell as editable ones (no
   bg / border change), so the lock badge IS the signal — it gets a
   muted accent treatment to read clearly from across the drawer. */
.studio-column-lock {
    flex-shrink: 0;
    width: 18px; height: 18px;
    display: inline-flex; align-items: center; justify-content: center;
    color: var(--text-muted);
    background: var(--surface-sunk);
    border: 1px solid var(--border);
    border-radius: 4px;
}
.studio-column-row .studio-column-lock {
    color: var(--accent-strong);
    background: var(--accent-soft);
    border-color: var(--info-border);
}
/* Read-only column row — uses the same card shell as editable rows.
   The single visual signal is the lock badge before the column name,
   so the row reads as part of the same card system rather than a
   separate "muted" track. */
.studio-column-meta {
    margin-top: 0;
    font-size: 0.72rem; color: var(--text-muted);
    display: flex; gap: 0.375rem;
    align-items: center;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}
.studio-column-type {
    color: var(--text-faint);
    text-transform: capitalize;
}
.studio-column-meta-sep {
    color: var(--border-strong);
    user-select: none;
}
.studio-column-meta-state {
    color: var(--accent-strong);
    font-weight: 500;
}
.studio-column-meta-state.is-locked {
    color: var(--text-muted);
    font-weight: 400;
}
.studio-column-actions {
    /* Reserve a FIXED 52-px slot (= 2 × 24-px action buttons +
       4-px gap) for every row's action cluster, with the buttons
       LEFT-aligned inside. This guarantees pixel-identical button
       columns across all card variants:
         · group card  → 1 ungroup button at slot-left
         · in-group column card with both buttons (remove-from-group
           + trash) → both fill the slot, left-most at slot-left
         · in-group column card with only remove-from-group
           → 1 button at slot-left
         · custom column card with only trash → 1 button at slot-left
         · plain column card → empty 52 px slot
       Left-aligning means the GROUP card's lone ungroup button
       lands at the SAME x as the column card's left-most action
       button (instead of at the column card's right-most button
       position, which is what right-alignment did). The cards
       above and below the group card now show their leftmost
       action button stacked vertically with the group card's
       button — visually a clean icon column on every row.

       `margin-left: auto` continues to anchor the slot itself to
       the card's right padding edge; flex-shrink: 0 + fixed width
       prevent the slot from collapsing when the card is narrow. */
    flex-shrink: 0;
    /* `min-width` (not a fixed `width`) reserves the same 52-px slot for the
       common 1–2 button cards so their trash buttons stay column-aligned, but
       lets the slot grow LEFTWARD when a third button is present (an editable
       custom column that also sits in a group: edit + remove-from-group +
       trash). Because the cluster is right-aligned, growth never moves the
       trash — it always stays pinned to the card's right edge. */
    min-width: 52px;
    display: flex;
    gap: 4px;
    /* Right-aligned so the DELETE / trash button (always the right-most action)
       lands at the same x on every card variant — a deletable column card's
       trash now lines up with the group card's trash instead of sitting a slot
       to its left. */
    justify-content: flex-end;
    align-items: center;
    margin-left: auto;
}
.studio-column-arrow {
    width: 24px; height: 24px;
    border: 1px solid var(--border);
    background: var(--surface);
    color: var(--text-muted);
    border-radius: var(--radius-sm);
    display: flex; align-items: center; justify-content: center;
    cursor: pointer;
}
.studio-column-arrow:hover:not(:disabled) { color: var(--accent); border-color: var(--accent-ring); }
.studio-column-arrow:disabled { opacity: 0.35; cursor: not-allowed; }
.studio-column-remove {
    width: 24px; height: 24px;
    border: 1px solid var(--border);
    background: var(--surface);
    color: var(--text-muted);
    border-radius: var(--radius-sm);
    display: flex; align-items: center; justify-content: center;
    cursor: pointer;
}
.studio-column-remove:hover { color: var(--danger-strong); border-color: var(--danger-border); background: var(--danger-bg); }
.studio-column-remove svg { width: 12px; height: 12px; stroke: currentColor; fill: none; }
/* Edit button — same 24-px footprint as the trash, but an ACCENT (not
   danger) hover so the two read as distinct intents. Leads the action
   cluster, sitting to the left of any remove button. */
.studio-column-edit {
    width: 24px; height: 24px;
    border: 1px solid var(--border);
    background: var(--surface);
    color: var(--text-muted);
    border-radius: var(--radius-sm);
    display: flex; align-items: center; justify-content: center;
    cursor: pointer;
}
.studio-column-edit:hover { color: var(--accent); border-color: var(--accent-ring); background: var(--accent-soft); }
.studio-column-edit svg { width: 12px; height: 12px; stroke: currentColor; fill: none; }
.studio-drawer-footer {
    padding: 0.875rem 1.125rem;
    border-top: 1px solid var(--border);
    display: flex; justify-content: flex-end;
    background: var(--surface-soft);
}

/* ── Filter drawer (v2 — multi-card, searchable picker) ──────── */
.studio-filter-drawer { background: var(--surface); }
.studio-filter-drawer-body {
    flex: 1; min-height: 0;
    overflow-y: auto;
    /* Symmetric 1rem L/R by default (balanced when the cards fit); `.has-vscroll`
       tightens the right inset to 0.5rem only when the list overflows. The
       footer below is a non-scrolling sibling and keeps the full 1rem inset. */
    padding: 0.875rem 1rem 0.5rem 1rem;
    display: flex; flex-direction: column; gap: 0.75rem;
}
.studio-filter-drawer-body.has-vscroll { padding-right: 0.5rem; }
.studio-filter-drawer-footer {
    border-top: 1px solid var(--border);
    background: var(--surface-soft);
    padding: 0.625rem 1rem;
    display: flex; flex-direction: column; gap: 0.5rem;
}
/* Action row sits below the status line. Two compact buttons share
   the row evenly so the footer stays compact yet readable. */
.studio-filter-footer-actions {
    display: flex; gap: 0.375rem;
    justify-content: flex-end;
}
.studio-filter-footer-actions .btn-small {
    padding: 0.35rem 0.625rem;
    font-size: 0.78rem;
}
.studio-filter-status {
    flex: 1; min-width: 0;
    font-size: 0.8rem; color: var(--text-muted);
    overflow: hidden; text-overflow: ellipsis;
    white-space: nowrap;
}
.studio-filter-status strong {
    color: var(--accent-strong);
    font-weight: 700;
}

/* Master switch — uses a custom track + thumb so it reads as a real
   on/off switch rather than a small browser checkbox. */
.studio-filter-master {
    display: flex; align-items: center; gap: 0.625rem;
    padding: 0.625rem 0.75rem;
    background: var(--surface-sunk);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    cursor: pointer;
    user-select: none;
    transition: background var(--motion-fast) var(--ease), border-color var(--motion-fast) var(--ease);
}
.studio-filter-master:hover { background: var(--surface); border-color: var(--border-strong); }
.studio-filter-master.is-on {
    background: var(--accent-soft);
    border-color: var(--info-border);
}
.studio-filter-master-switch {
    position: relative;
    flex-shrink: 0;
    display: inline-flex;
}
.studio-filter-master-switch input {
    position: absolute; inset: 0;
    opacity: 0; cursor: pointer;
    margin: 0;
}
.studio-filter-master-track {
    width: 34px; height: 20px;
    border-radius: 999px;
    background: var(--border-strong);
    position: relative;
    transition: background var(--motion-fast) var(--ease);
    flex-shrink: 0;
}
.studio-filter-master-thumb {
    position: absolute;
    top: 2px; left: 2px;
    width: 16px; height: 16px;
    border-radius: 50%;
    background: var(--surface);
    box-shadow: 0 1px 2px rgba(15, 23, 42, 0.18);
    transition: transform var(--motion-fast) var(--ease);
}
.studio-filter-master.is-on .studio-filter-master-track { background: var(--accent); }
.studio-filter-master.is-on .studio-filter-master-thumb { transform: translateX(14px); }
.studio-filter-master-text {
    flex: 1; min-width: 0;
    display: flex; flex-direction: column;
    line-height: 1.2;
}
.studio-filter-master-text strong {
    font-size: 0.875rem; font-weight: 600;
    color: var(--text-strong);
}
.studio-filter-master.is-on .studio-filter-master-text strong { color: var(--accent-strong); }
.studio-filter-master-text em {
    font-style: normal;
    font-size: 0.72rem; color: var(--text-muted);
    margin-top: 1px;
}

/* Searchable column picker — text input + dropdown of columns. */
.studio-filter-picker {
    position: relative;
}
.studio-filter-picker-icon {
    position: absolute;
    top: 50%; left: 10px;
    transform: translateY(-50%);
    color: var(--text-faint);
    pointer-events: none;
    line-height: 0;
}
.studio-filter-picker-input {
    width: 100%;
    padding: 0.55rem 0.75rem 0.55rem 2rem;
    /* border-strong (vs default --border) so the input edge stays
       visible against the surface-soft fill in the drawer body. */
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-md);
    background: var(--surface-soft);
    font: inherit; font-size: 0.875rem;
    color: var(--text-strong);
    transition: border-color var(--motion-fast) var(--ease),
                box-shadow var(--motion-fast) var(--ease),
                background var(--motion-fast) var(--ease);
}
.studio-filter-picker-input:focus {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 3px var(--accent-ring);
    background: var(--surface);
}
.studio-filter-picker-list {
    position: absolute;
    top: calc(100% + 4px); left: 0; right: 0;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-3);
    z-index: 60;
    max-height: 240px;
    overflow-y: auto;
    padding: 4px;
    animation: slideUpFade 0.12s var(--ease);
}
.studio-filter-picker-empty {
    padding: 0.625rem 0.75rem;
    font-size: 0.85rem;
    color: var(--text-muted);
    text-align: center;
}
.studio-filter-picker-item {
    width: 100%;
    display: flex; align-items: center; gap: 0.5rem;
    padding: 0.4rem 0.625rem;
    background: transparent; border: none;
    border-radius: var(--radius-sm);
    color: var(--text);
    font: inherit; font-size: 0.875rem;
    text-align: left; cursor: pointer;
}
.studio-filter-picker-item:hover {
    background: var(--accent-soft);
    color: var(--accent-strong);
}
.studio-filter-picker-kind {
    flex-shrink: 0;
    width: 22px; height: 22px;
    display: inline-flex; align-items: center; justify-content: center;
    background: var(--surface-sunk);
    border: 1px solid var(--border-faint);
    border-radius: 5px;
    font-size: 0.78rem; font-weight: 700;
    color: var(--text-muted);
    font-family: var(--font-mono);
}
.studio-filter-picker-kind-number   { color: var(--accent-strong); background: var(--accent-soft); border-color: var(--info-border); }
.studio-filter-picker-kind-boolean  { color: var(--success-fg);    background: var(--success-bg);  border-color: var(--success-border); }
.studio-filter-picker-kind-date     { color: var(--warning-fg);    background: var(--warning-bg);  border-color: var(--warning-border); }
.studio-filter-picker-kind-category { color: var(--category);       background: var(--category-soft); border-color: var(--category-border); }
.studio-filter-picker-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.studio-filter-picker-type {
    flex-shrink: 0;
    font-size: 0.7rem;
    color: var(--text-faint);
    text-transform: uppercase;
    letter-spacing: 0.05em;
}

/* Filter cards list */
.studio-filter-cards {
    display: flex; flex-direction: column; gap: 0.5rem;
}

/* Empty state. */
.studio-filter-empty {
    padding: 1.25rem 1rem;
    text-align: center;
    color: var(--text-muted);
    background: var(--surface-soft);
    border: 1px dashed var(--border);
    border-radius: var(--radius-md);
}
.studio-filter-empty svg { width: 24px; height: 24px; stroke: var(--text-faint); fill: none; }
.studio-filter-empty-title {
    font-size: 0.9rem; font-weight: 600;
    color: var(--text-strong);
    margin-top: 0.25rem;
}
.studio-filter-empty-sub { font-size: 0.78rem; margin-top: 2px; }

/* Filter card */
.studio-filter-card {
    background: var(--surface);
    /* Match the bumped column-row border tone so cards across the
       Columns and Filter drawers read as one visual family. */
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-1);
    overflow: hidden;
    transition: border-color var(--motion-fast) var(--ease),
                opacity var(--motion-fast) var(--ease);
}
.studio-filter-card.is-disabled {
    opacity: 0.55;
    background: var(--surface-soft);
    border-style: dashed;
}
/* Inverted state — the invert button itself is the only visual cue.
   No card-level border or background change — that was too loud and
   made the card feel like an error state. */
.studio-filter-card-stale {
    border-style: dashed;
    border-color: var(--danger-border);
    background: var(--danger-bg);
    color: var(--danger-strong);
}

.studio-filter-card-header {
    display: flex; align-items: center; gap: 0.4rem;
    padding: 0.45rem 0.55rem 0.45rem 0.5rem;
    background: var(--surface-soft);
    border-bottom: 1px solid var(--border-faint);
}
.studio-filter-card.is-disabled .studio-filter-card-header { background: transparent; }

/* Stacked variant — used by the Structure filter card. The
   "SUBSTRUCTURE" type chip sits BELOW the title rather than to its
   right, so a long title isn't squeezed into a single ellipsis-cut
   line. The two header buttons (invert, remove) stay vertically
   centered against the whole title block, so the visual weight on
   the right side matches the standard filter cards. */
.studio-filter-card-header-stacked .studio-filter-card-title-block {
    flex: 1; min-width: 0;
    display: flex; flex-direction: column; align-items: flex-start;
    gap: 0.18rem;
    line-height: 1.15;
}
.studio-filter-card-header-stacked .studio-filter-card-title {
    /* Title gets the full width of the title-block; ellipsis still
       applies if the user sized the cards down. */
    width: 100%;
}
.studio-filter-card-header-stacked .studio-filter-card-kind {
    /* The chip is its own row now — let it shrink to fit content. */
    align-self: flex-start;
}

.studio-filter-card-toggle {
    flex-shrink: 0;
    display: inline-flex; align-items: center; justify-content: center;
    width: 18px; height: 18px;
}
.studio-filter-card-toggle input {
    width: 14px; height: 14px;
    accent-color: var(--accent);
    cursor: pointer;
    margin: 0;
}
.studio-filter-card-title {
    flex: 1; min-width: 0;
    font-size: 0.875rem; font-weight: 600;
    color: var(--text-strong);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.studio-filter-card-kind {
    flex-shrink: 0;
    font-size: 0.65rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    padding: 1px 6px;
    border-radius: 3px;
    background: var(--surface-sunk);
    color: var(--text-muted);
}
.studio-filter-card-kind-number   { color: var(--accent-strong); background: var(--accent-soft); }
.studio-filter-card-kind-boolean  { color: var(--success-fg);    background: var(--success-bg);  }
.studio-filter-card-kind-date     { color: var(--warning-fg);    background: var(--warning-bg);  }
.studio-filter-card-kind-category { color: var(--category);       background: var(--category-soft); }
.studio-filter-card-kind-structure { color: var(--accent-strong); background: var(--accent-soft); }
.studio-filter-card-btn {
    flex-shrink: 0;
    width: 24px; height: 24px;
    display: inline-flex; align-items: center; justify-content: center;
    border: 1px solid transparent;
    background: transparent;
    color: var(--text-muted);
    border-radius: 5px;
    cursor: pointer;
    transition: all var(--motion-fast) var(--ease);
}
.studio-filter-card-btn:hover {
    color: var(--text-strong);
    background: var(--surface);
    border-color: var(--border);
}
/* Active invert — the "⊘" icon switches to a saturated amber fill
   so "this filter is inverted" reads at a glance. Amber (warning) is
   the chosen semantic for invert across Studio so it's never confused
   with informational (accent blue) or destructive (red) states. */
.studio-filter-card-btn-invert.is-active {
    color: var(--accent-fg);
    background: var(--warning);
    border-color: var(--warning);
}
.studio-filter-card-btn-invert.is-active:hover {
    color: var(--accent-fg);
    background: var(--warning-strong);
    border-color: var(--warning-strong);
}
.studio-filter-card-btn-invert.is-active:focus-visible {
    outline: none;
    box-shadow: 0 0 0 3px var(--warning-ring);
}
.studio-filter-card-btn-remove:hover {
    color: var(--danger-strong);
    background: var(--danger-bg);
    border-color: var(--danger-border);
}

.studio-filter-card-body {
    padding: 0.625rem 0.625rem 0.75rem;
    display: flex; flex-direction: column; gap: 0.5rem;
}
.studio-filter-card-row {
    display: flex; align-items: center; gap: 0.5rem;
}
.studio-filter-card-label {
    font-size: 0.72rem;
    color: var(--text-muted);
    text-transform: uppercase;
    letter-spacing: 0.04em;
    font-weight: 600;
    flex-shrink: 0;
}
/* (The old "Inverted — keeps rows that don't match" hint banner has
   been removed. The new ⊘ icon's active state is visually unambiguous
   on its own.) */

/* Custom-styled filter input — replaces native select-default look. */
.studio-filter-input {
    width: 100%;
    padding: 0.45rem 0.625rem;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--surface);
    color: var(--text-strong);
    font: inherit; font-size: 0.85rem;
    transition: border-color var(--motion-fast) var(--ease),
                box-shadow var(--motion-fast) var(--ease);
}
.studio-filter-input:focus {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 3px var(--accent-ring);
}
/* ── Custom-styled native selects ────────────────────────────────
   Every <select> in Alma Studio that's marked .studio-select (or
   already styled via .studio-filter-input / .studio-prompt-input)
   gets the same visual treatment: no native chrome, a faint chevron
   drawn via background-image, consistent border + focus ring, and a
   matching disabled state. This keeps modal selects, filter selects,
   custom-cell dropdowns, and the column-picker selectors in sync. */
select.studio-select,
select.studio-filter-input,
select.studio-prompt-input {
    appearance: none;
    -webkit-appearance: none;
    -moz-appearance: none;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath d='M3 4.5l3 3 3-3' stroke='%2364748b' stroke-width='1.6' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 10px center;
    background-color: var(--surface);
    padding-right: 28px;
    cursor: pointer;
}
select.studio-select:hover,
select.studio-filter-input:hover,
select.studio-prompt-input:hover {
    border-color: var(--border-strong);
}
select.studio-select:focus,
select.studio-filter-input:focus,
select.studio-prompt-input:focus {
    border-color: var(--accent);
    box-shadow: 0 0 0 3px var(--accent-ring);
    outline: none;
}
select.studio-select:disabled,
select.studio-filter-input:disabled,
select.studio-prompt-input:disabled {
    background-color: var(--surface-sunk);
    color: var(--text-faint);
    cursor: not-allowed;
}
/* Standalone .studio-select utility — for selects outside filter and
   modal contexts. Picks up the same shape as .studio-filter-input. */
select.studio-select {
    width: 100%;
    padding: 0.45rem 28px 0.45rem 0.625rem;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    color: var(--text-strong);
    font: inherit; font-size: 0.85rem;
    transition: border-color var(--motion-fast) var(--ease),
                box-shadow var(--motion-fast) var(--ease);
}

/* ── StudioDropdown — fully custom dropdown widget ───────────────
   Replaces native <select> wherever the OS popup looked out of place
   (Chrome on Windows ignores most CSS for the open state of a native
   select). The trigger is a styled button; the menu is portaled to
   <body> via fixed positioning so it floats above tables, modals,
   and sticky chrome without being clipped. */
.studio-dropdown {
    /* Base = filter-input shape so it slots into the same rows. */
    appearance: none;
    -webkit-appearance: none;
    width: 100%;
    min-height: 32px;
    padding: 0.45rem 28px 0.45rem 0.625rem;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--surface);
    color: var(--text-strong);
    font: inherit; font-size: 0.85rem;
    text-align: left;
    cursor: pointer;
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem;
    transition: border-color var(--motion-fast) var(--ease),
                box-shadow var(--motion-fast) var(--ease),
                background var(--motion-fast) var(--ease);
}
.studio-dropdown:hover:not(.is-disabled) {
    border-color: var(--border-strong);
}
.studio-dropdown:focus-visible,
.studio-dropdown.is-open {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 3px var(--accent-ring);
}
.studio-dropdown.is-disabled {
    background: var(--surface-sunk);
    color: var(--text-faint);
    cursor: not-allowed;
}
.studio-dropdown-value {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.studio-dropdown-placeholder {
    color: var(--text-faint);
}
.studio-dropdown-chevron {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    color: var(--text-muted);
    transition: transform var(--motion-fast) var(--ease);
}
.studio-dropdown.is-open .studio-dropdown-chevron {
    transform: rotate(180deg);
}

/* Modal variant — matches .studio-prompt-input's heavier shape so
   the trigger sits comfortably inside Add Compounds / Add Column. */
.studio-dropdown.studio-dropdown-modal {
    /* Block-level (not inline-flex) so it stacks like the text inputs in
       the same modal — no baseline quirk — and matches their compact 36px
       height. The chevron is a flex child; a small right padding (0.7rem,
       not the old 32px) lets it sit close to the right edge with the
       value text + gap keeping comfortable separation. */
    display: flex;
    padding: 0.45rem 0.7rem;
    border: 1.5px solid var(--border-strong);
    border-radius: var(--radius-md);
    font-size: 0.95rem;
    min-height: 36px;
    /* Never let a long selected label grow the trigger past its field (which
       would reflow / widen the modal). It stays full-width of the field; the
       value span inside ellipsizes. The portaled menu is sized to the trigger
       width too, so long options ellipsize there rather than break the
       viewport. */
    width: 100%;
    min-width: 0;
    max-width: 100%;
    box-sizing: border-box;
}
.studio-dropdown.studio-dropdown-modal:focus-visible,
.studio-dropdown.studio-dropdown-modal.is-open {
    border-color: var(--accent);
    box-shadow: 0 0 0 3px var(--accent-ring);
}

/* Cell variant — slim, borderless inside the table cell unless open
   or hovered. Inherits row-height naturally. */
.studio-dropdown.studio-cell-dropdown-trigger {
    border-color: transparent;
    background: transparent;
    /* The VALUE is centered independently in the full cell width and the
       chevron is anchored absolutely on the right (see the two rules below).
       Centering the value+chevron as a *group* (the previous attempt) still
       read as off-center, because the arrow's width pushed the value's centre
       left of the cell centre. Anchoring the arrow takes it out of the
       centring flow, so the selected text sits dead-centre like the
       surrounding text/number cells while the arrow stays a compact,
       fixed affordance on the right. */
    position: relative;
    justify-content: center;
    gap: 0;
    padding: 0.25rem 8px;
    min-height: 0;
    border-radius: 4px;
}
/* Value fills the trigger and centers its text; symmetric inline padding
   keeps the centre true AND leaves room on the right so a longer value
   ellipsises before it reaches the anchored chevron. */
.studio-dropdown.studio-cell-dropdown-trigger .studio-dropdown-value {
    flex: 1 1 auto;
    text-align: center;
    padding: 0 12px;
}
/* Chevron anchored to the right edge, vertically centered via top/bottom
   (NOT transform, so the `.is-open` rotation transform stays free). */
.studio-dropdown.studio-cell-dropdown-trigger .studio-dropdown-chevron {
    position: absolute;
    right: 6px;
    top: 0;
    bottom: 0;
    display: inline-flex;
    align-items: center;
    pointer-events: none;
}
.studio-dropdown.studio-cell-dropdown-trigger:hover:not(.is-disabled) {
    background: var(--surface-sunk);
    border-color: var(--border);
}
.studio-dropdown.studio-cell-dropdown-trigger.is-open {
    background: var(--surface);
    border-color: var(--accent);
}
.studio-dropdown.studio-cell-dropdown-trigger .studio-dropdown-chevron svg {
    width: 10px; height: 10px;
}

/* Filter variant — same as default but with a touch less padding so
   the operator picker doesn't tower over the value input next to it. */
.studio-dropdown.studio-dropdown-filter {
    padding: 0.4rem 26px 0.4rem 0.625rem;
}

/* Portaled menu — fixed-position list rendered to <body>. */
.studio-dropdown-menu {
    position: fixed;
    z-index: 12000;        /* above sticky thead, modals' content layer */
    background: var(--surface);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-md);
    box-shadow: 0 12px 32px rgba(15, 30, 45, 0.18),
                0 2px 6px rgba(15, 30, 45, 0.08);
    padding: 4px;
    overflow-y: auto;
    min-width: 120px;
    /* Subtle entry — keeps the menu feeling responsive but not jarring. */
    animation: studioDropdownIn 110ms var(--ease, ease-out);
}
@keyframes studioDropdownIn {
    from { opacity: 0; transform: translateY(-2px); }
    to   { opacity: 1; transform: translateY(0); }
}
.studio-dropdown-option {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    width: 100%;
    padding: 0.45rem 0.625rem;
    border: 0;
    border-radius: 4px;
    background: transparent;
    color: var(--text-strong);
    font: inherit; font-size: 0.875rem;
    text-align: left;
    cursor: pointer;
    line-height: 1.3;
}
.studio-dropdown-option > span:first-child {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.studio-dropdown-option.is-highlight {
    background: var(--accent-soft);
    color: var(--accent-strong);
}
.studio-dropdown-option.is-selected {
    color: var(--accent-strong);
    font-weight: 600;
}
.studio-dropdown-option.is-selected.is-highlight {
    background: var(--accent-soft);
}
.studio-dropdown-option.is-disabled {
    color: var(--text-faint);
    cursor: not-allowed;
    background: transparent;
}
.studio-dropdown-option-empty {
    color: var(--text-faint);
    font-style: italic;
}
/* Compact density for Plot Settings dropdowns (menuClassName="viz-menu"):
   trimmer option rows so the menus read tight + professional. The menu
   already scrolls — overflow-y:auto + a JS-computed max-height bounded to
   the viewport — so long column lists get a scroller instead of overflowing. */
.studio-dropdown-menu.viz-menu { padding: 3px; }
.studio-dropdown-menu.viz-menu .studio-dropdown-option {
    padding: 0.26rem 0.55rem;
    font-size: 0.82rem;
    line-height: 1.25;
    border-radius: 3px;
}
.studio-dropdown-check {
    flex: 0 0 auto;
    color: var(--accent-strong);
}
.studio-dropdown-legacy {
    display: flex;
    align-items: flex-start;
    gap: 6px;
    margin-top: 4px;
    padding: 0.45rem 0.625rem;
    border-top: 1px dashed var(--border);
    font-size: 0.78rem;
    color: var(--text-muted);
    line-height: 1.35;
}
.studio-dropdown-legacy-mark {
    flex: 0 0 auto;
    width: 16px; height: 16px;
    display: inline-flex;
    align-items: center; justify-content: center;
    border-radius: 50%;
    background: var(--warning-soft);
    color: var(--warning-strong);
    font-weight: 700;
    font-size: 0.72rem;
    line-height: 1;
}
.studio-dropdown-empty-state {
    padding: 0.5rem 0.625rem;
    color: var(--text-faint);
    font-style: italic;
    font-size: 0.85rem;
    text-align: center;
}

/* Number range slider — two overlapping <input type=range> with a
   custom track and accent fill between handles. The thumbs are
   centred on the 4px-tall visible track via `margin-top` on Webkit
   (Firefox auto-centres) so both handles sit ON the track instead
   of floating above or below it. */
.studio-filter-range {
    display: flex; flex-direction: column;
    gap: 0.5rem;
}
.studio-filter-range-track-wrap {
    position: relative;
    height: 22px;
    margin: 4px 6px 0;
}
.studio-filter-range-track {
    position: absolute;
    top: 50%;
    left: 0; right: 0;
    height: 4px;
    transform: translateY(-50%);
    background: var(--surface-sunk);
    border: 1px solid var(--border);
    border-radius: 999px;
    pointer-events: none;
}
.studio-filter-range-fill {
    position: absolute;
    top: 0; bottom: 0;
    background: var(--accent);
    border-radius: 999px;
}
.studio-filter-range-input {
    position: absolute;
    top: 0; bottom: 0;
    left: 0; right: 0;
    width: 100%;
    height: 100%;
    margin: 0;
    background: transparent;
    pointer-events: none;
    appearance: none;
    -webkit-appearance: none;
}
.studio-filter-range-input::-webkit-slider-runnable-track {
    background: transparent;
    height: 4px;
}
.studio-filter-range-input::-webkit-slider-thumb {
    -webkit-appearance: none;
    pointer-events: auto;
    width: 16px; height: 16px;
    border-radius: 50%;
    background: var(--surface);
    border: 2px solid var(--accent);
    box-shadow: 0 1px 2px rgba(15, 23, 42, 0.15);
    cursor: pointer;
    /* Centre the thumb on the 4px-tall runnable track:
       (track-height − thumb-height) / 2 = (4 − 16) / 2 = −6 */
    margin-top: -6px;
}
.studio-filter-range-input::-webkit-slider-thumb:hover {
    background: var(--accent-soft);
    border-color: var(--accent-strong);
}
.studio-filter-range-input::-moz-range-thumb {
    pointer-events: auto;
    width: 16px; height: 16px;
    border-radius: 50%;
    background: var(--surface);
    border: 2px solid var(--accent);
    box-shadow: 0 1px 2px rgba(15, 23, 42, 0.15);
    cursor: pointer;
}
.studio-filter-range-input::-moz-range-thumb:hover {
    background: var(--accent-soft);
    border-color: var(--accent-strong);
}
.studio-filter-range-input::-moz-range-track { background: transparent; height: 4px; }
/* The "lo" handle sits on top so it's grabbable when handles overlap. */
.studio-filter-range-input-lo { z-index: 2; }
.studio-filter-range-input-hi { z-index: 1; }
.studio-filter-range-numbers {
    display: flex; gap: 0.5rem;
}
.studio-filter-range-field {
    flex: 1; min-width: 0;
    display: flex; flex-direction: column; gap: 2px;
    position: relative;
}
.studio-filter-range-field label {
    font-size: 0.7rem;
    color: var(--text-muted);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
}
.studio-filter-range-mode {
    position: absolute;
    top: 4px; right: 8px;
    font-size: 0.65rem;
    color: var(--text-faint);
    text-transform: uppercase;
    letter-spacing: 0.05em;
    background: var(--surface-soft);
    padding: 1px 5px;
    border-radius: 3px;
    pointer-events: none;
}
.studio-filter-range-mode.is-manual {
    color: var(--accent-strong);
    background: var(--accent-soft);
}

/* Boolean filter — three-button segmented control. The track has a
   single border so the buttons read as one unit; the active button
   lifts to the surface colour with a thin coloured rule on the
   bottom edge to indicate true/false/empty. Subtle and elegant —
   no saturated backgrounds, no shadows. */
.studio-filter-bool-row {
    display: flex;
    padding: 3px;
    background: var(--surface-sunk);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    gap: 0;
}
.studio-filter-bool-btn {
    flex: 1;
    display: inline-flex; align-items: center; justify-content: center;
    gap: 0.35rem;
    padding: 0.4rem 0.5rem;
    background: transparent;
    border: none;
    border-radius: 4px;
    color: var(--text-muted);
    font: inherit; font-size: 0.82rem; font-weight: 600;
    cursor: pointer;
    transition: background var(--motion-fast) var(--ease),
                color var(--motion-fast) var(--ease);
    border-bottom: 2px solid transparent;
}
.studio-filter-bool-btn:hover {
    color: var(--text-strong);
    background: rgba(15, 23, 42, 0.04);
}
.studio-filter-bool-glyph {
    font-size: 0.9rem;
    color: var(--text-faint);
    line-height: 1;
}
/* Active state — surface bg, accent-coloured bottom rule per variant. */
.studio-filter-bool-btn.is-on {
    background: var(--surface);
    color: var(--text-strong);
    box-shadow: var(--shadow-1);
}
.studio-filter-bool-btn-true.is-on {
    border-bottom-color: var(--success-fg);
}
.studio-filter-bool-btn-true.is-on .studio-filter-bool-glyph {
    color: var(--success-fg);
}
.studio-filter-bool-btn-false.is-on {
    border-bottom-color: var(--danger-strong);
}
.studio-filter-bool-btn-false.is-on .studio-filter-bool-glyph {
    color: var(--danger-strong);
}
.studio-filter-bool-btn-empty.is-on {
    border-bottom-color: var(--text-muted);
}
.studio-filter-bool-btn-empty.is-on .studio-filter-bool-glyph {
    color: var(--text-strong);
}

.studio-filter-date-row {
    display: flex; align-items: center; gap: 0.375rem;
}
.studio-filter-date-sep { color: var(--text-faint); font-size: 0.85rem; }

/* ── Token / chip text input for filter values ───────────────────
   A pill-input that lets users add multiple matching tokens. Click
   anywhere on the wrapper to focus the inner input; Enter or comma
   commits a chip; backspace on empty draft removes the last chip. */
.studio-filter-tokens {
    display: flex; flex-wrap: wrap;
    align-items: center;
    gap: 4px;
    min-height: 36px;
    padding: 4px 6px;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--surface);
    cursor: text;
    transition: border-color var(--motion-fast) var(--ease),
                box-shadow var(--motion-fast) var(--ease);
}
.studio-filter-tokens:focus-within {
    border-color: var(--accent);
    box-shadow: 0 0 0 3px var(--accent-ring);
}
.studio-filter-token {
    display: inline-flex; align-items: center;
    gap: 4px;
    padding: 2px 4px 2px 8px;
    background: var(--accent-soft);
    color: var(--accent-strong);
    border: 1px solid var(--info-border);
    border-radius: 999px;
    font-size: 0.78rem; font-weight: 500;
    line-height: 1.2;
    max-width: 100%;
}
.studio-filter-token-text {
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    max-width: 14ch;
}
.studio-filter-token-remove {
    display: inline-flex; align-items: center; justify-content: center;
    width: 16px; height: 16px;
    padding: 0;
    border: none;
    border-radius: 50%;
    background: transparent;
    color: var(--accent-strong);
    cursor: pointer;
    font-size: 0.95rem;
    line-height: 1;
    flex-shrink: 0;
}
.studio-filter-token-remove:hover {
    background: var(--accent);
    color: var(--accent-fg);
}
.studio-filter-tokens-input {
    flex: 1; min-width: 60px;
    padding: 4px 4px;
    border: none;
    background: transparent;
    color: var(--text-strong);
    font: inherit; font-size: 0.85rem;
    outline: none;
}

/* Categorical chip list */
.studio-filter-chips {
    display: flex; flex-wrap: wrap; gap: 4px;
}
.studio-filter-chip {
    padding: 0.3rem 0.625rem;
    border: 1px solid var(--border);
    border-radius: 999px;
    background: var(--surface);
    color: var(--text);
    font: inherit; font-size: 0.78rem; font-weight: 500;
    cursor: pointer;
    transition: all var(--motion-fast) var(--ease);
}
.studio-filter-chip:hover { border-color: var(--border-strong); }
.studio-filter-chip.is-on {
    background: var(--accent);
    border-color: var(--accent);
    color: var(--accent-fg);
}
.studio-filter-chip {
    display: inline-flex; align-items: center; gap: 6px;
}
.studio-filter-chip-count {
    font-size: 0.7rem; font-weight: 600;
    padding: 1px 6px;
    border-radius: 999px;
    background: var(--surface-soft);
    color: var(--text-muted);
    line-height: 1.4;
}
.studio-filter-chip.is-on .studio-filter-chip-count {
    background: rgba(255, 255, 255, 0.22);
    color: var(--accent-fg);
}
.studio-filter-chip.is-empty { opacity: 0.55; }
.studio-filter-bool-count {
    margin-left: 6px;
    font-size: 0.7rem; font-weight: 600;
    padding: 1px 6px;
    border-radius: 999px;
    background: var(--surface-soft);
    color: var(--text-muted);
}
.studio-filter-bool-btn.is-on .studio-filter-bool-count {
    background: rgba(255, 255, 255, 0.22);
    color: inherit;
}

/* CSV drop zone — replaces the bare file input. */
.studio-csv-dropzone {
    position: relative;
    display: flex; flex-direction: column;
    align-items: center; justify-content: center;
    text-align: center;
    padding: 1.25rem 1rem;
    border: 1.5px dashed var(--border-strong);
    border-radius: var(--radius-md);
    background: var(--surface-soft);
    color: var(--text);
    cursor: pointer;
    transition: border-color var(--motion-fast) var(--ease),
                background var(--motion-fast) var(--ease);
}
.studio-csv-dropzone:hover,
.studio-csv-dropzone-over { border-color: var(--accent); background: var(--accent-soft); }
.studio-csv-dropzone-loaded { border-style: solid; border-color: var(--accent); background: var(--accent-soft); }
.studio-csv-dropzone-icon {
    width: 36px; height: 36px;
    border-radius: 50%;
    background: var(--surface);
    border: 1px solid var(--border);
    display: flex; align-items: center; justify-content: center;
    color: var(--accent);
    margin-bottom: 0.5rem;
}
.studio-csv-dropzone-icon svg { width: 18px; height: 18px; stroke: currentColor; fill: none; }
.studio-csv-dropzone-text { font-size: 0.9rem; color: var(--text); }
.studio-csv-dropzone-text u { text-decoration: underline; color: var(--accent-strong); }
.studio-csv-dropzone-hint { font-size: 0.78rem; color: var(--text-muted); margin-top: 0.25rem; }

/* Remove-file affordance — small × in the loaded dropzone's top-right
   corner so the user can clear the upload without closing the modal.
   `.studio-csv-dropzone` already has `position: relative` from its
   main rule above, so the absolute child anchors against it. */
.studio-csv-dropzone-remove {
    position: absolute;
    top: 8px; right: 8px;
    width: 22px; height: 22px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: 1px solid var(--border);
    border-radius: 4px;
    background: var(--surface);
    color: var(--text-muted);
    cursor: pointer;
    line-height: 1;
    transition: background var(--motion-fast) var(--ease),
                color var(--motion-fast) var(--ease),
                border-color var(--motion-fast) var(--ease);
}
.studio-csv-dropzone-remove:hover {
    background: var(--danger-soft);
    color: var(--danger);
    border-color: var(--danger-border);
}
.studio-csv-dropzone-remove:focus-visible {
    outline: none;
    box-shadow: 0 0 0 3px var(--accent-ring);
}
.studio-csv-dropzone-remove svg { display: block; }
/* Two-column mapping grid — Columns to import (left) +
   Preview (right). Each column owns its scroll, so a long list
   on either side never pushes the other out of view. The grid
   absorbs whatever vertical space the modal body has after the
   identifier sections lay out. */
.studio-import-mapping-grid {
    display: grid;
    grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
    gap: 0.875rem;
    flex: 1 1 auto;
    min-height: 0;
}
.studio-import-mapping-col {
    display: flex;
    flex-direction: column;
    min-height: 0;
    min-width: 0;
}
.studio-import-mapping-col > .studio-form-label {
    margin-bottom: 0.25rem;
}
.studio-import-mapping-col > .studio-form-label-section {
    /* Inside the grid, the section label is the first child of its
       column — drop the global section top-margin so the two columns
       align on the same baseline. */
    margin-top: 0;
}
.studio-csv-preview {
    padding: 0.65rem 0.875rem;
    background: var(--surface-sunk);
    border: 1px solid var(--border-faint);
    border-radius: var(--radius-md);
    font-size: 0.8rem;
    color: var(--text);
    margin-top: 0;
    /* Right column inside the mapping grid — fills its column's
       remaining height and scrolls internally when the preview
       list is taller than the available space. */
    flex: 1 1 auto;
    min-height: 0;
    overflow: auto;
    line-height: 1.5;
}
.studio-csv-preview > strong {
    display: block;
    font-size: 0.72rem;
    font-weight: 600;
    color: var(--text-muted);
    text-transform: uppercase;
    letter-spacing: 0.04em;
    margin-bottom: 4px;
}
.studio-csv-preview ul {
    margin: 0;
    padding: 0;
    list-style: none;
}
.studio-csv-preview li {
    line-height: 1.45;
    padding: 2px 0;
    border-bottom: 1px solid var(--border-faint);
    word-break: break-all;
}
.studio-csv-preview li:last-child {
    border-bottom: none;
}

/* Floating structure preview — shown while hovering a Structure cell. */
.studio-structure-preview {
    width: 380px;
    background: var(--surface);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-3);
    z-index: 250;
    pointer-events: none;
    animation: slideUpFade 0.12s var(--ease);
    overflow: hidden;
}
.studio-structure-preview-svg {
    padding: 12px;
    background: var(--surface);
    display: flex; align-items: center; justify-content: center;
    height: 280px;
}
.studio-structure-preview-svg > div { width: 100%; height: 100%; }
.studio-structure-preview-svg svg { width: 100% !important; height: 100% !important; display: block; }
.studio-structure-preview-loading {
    width: 100%; height: 100%;
    background: linear-gradient(90deg, var(--surface-sunk) 0%, var(--surface-soft) 50%, var(--surface-sunk) 100%);
    background-size: 200% 100%;
    animation: structurePulse 1.4s ease-in-out infinite;
    border-radius: 6px;
}
.studio-structure-preview-smiles {
    padding: 6px 10px;
    border-top: 1px solid var(--border-faint);
    font-size: 0.78rem;
    color: var(--text-muted);
    background: var(--surface-soft);
    word-break: break-all;
    max-height: 64px;
    overflow: hidden;
    text-overflow: ellipsis;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
}

/* Add Compounds modal — paste / CSV / draw tabs. */
.studio-import-modal {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    /* Sized for Ketcher's full toolbar + a usable canvas. Paste
       SMILES and From CSV tolerate the extra room without layout
       changes (their content is left-aligned and capped); the body
       uses flex:1 so each tab fills whatever space is available. */
    width: min(1040px, 96vw);
    /* Fixed height (capped to viewport) instead of max-height so the
       iframe inside Draw always has the same predictable canvas. The
       body's flex chain depends on a known parent height — with
       max-height the modal could collapse to its content height when
       a tab without intrinsic height (Draw) is active, which is what
       was making the Ketcher iframe render in a tiny strip. */
    height: min(820px, 92vh);
    box-shadow: var(--shadow-modal);
    overflow: hidden;
    display: flex; flex-direction: column;
}
/* MPO builder reuses the import-modal shell but is content-sized (no Ketcher
   canvas), so override the fixed height and width to a comfortable form. */
.studio-import-modal.studio-mpo-modal {
    width: min(600px, 96vw);
    height: auto;
    max-height: 90vh;
}
.studio-mpo-modal .studio-import-body { overflow-y: auto; padding-top: 0.85rem; }
/* Name field — label STACKED above a compact input with the refined (less
   rounded) radius shared by the platform's inputs. */
.studio-mpo-name-field { display: flex; flex-direction: column; gap: 0.25rem; }
.studio-mpo-name-field .studio-form-label { margin: 0; }
.studio-mpo-name-input {
    height: 32px;
    border-radius: var(--radius-sm);
    max-width: 280px;
}
.studio-mpo-hint { margin: 0.45rem 0 0; font-size: 0.78rem; color: var(--text-muted); line-height: 1.3; }
.studio-mpo-props { display: flex; flex-direction: column; gap: 0.55rem; margin-top: 0.7rem; }
.studio-mpo-prop {
    /* A slightly stronger border so each property reads as a distinct card. */
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-md);
    padding: 0.6rem 0.65rem;
    background: var(--surface-soft);
    display: flex; flex-direction: column; gap: 0.5rem;
}
.studio-mpo-prop-main { display: flex; align-items: center; gap: 0.5rem; }
/* Column dropdown (wider) + mode dropdown — both pinned to one height so they
   line up with the delete button. */
.studio-mpo-prop-main .studio-mpo-dd { min-width: 0; height: 34px; }
.studio-mpo-prop-main .studio-mpo-dd:first-child { flex: 1.4 1 0; }
.studio-mpo-prop-main .studio-mpo-dd:nth-child(2) { flex: 1 1 0; }
.studio-mpo-prop-remove {
    flex: 0 0 auto; width: 34px; height: 34px;
    display: inline-flex; align-items: center; justify-content: center;
    border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
    background: var(--surface); color: var(--text-muted); cursor: pointer;
    transition: color var(--motion-fast) var(--ease), border-color var(--motion-fast) var(--ease);
}
.studio-mpo-prop-remove:hover:not(:disabled) { color: var(--danger); border-color: var(--danger); }
.studio-mpo-prop-remove:disabled { opacity: 0.4; cursor: not-allowed; }
.studio-mpo-prop-bounds { display: flex; gap: 0.45rem; flex-wrap: wrap; }
.studio-mpo-prop-bounds label {
    display: flex; flex-direction: column; gap: 0.2rem;
    font-size: 0.68rem; color: var(--text-muted);
    text-transform: uppercase; letter-spacing: 0.03em;
    flex: 1 1 0; min-width: 64px;
}
.studio-mpo-prop-bounds input { width: 100%; height: 30px; border-radius: var(--radius-sm); }
/* Add property — right-aligned to match the modal's primary-action edge. */
.studio-mpo-add-row { display: flex; justify-content: flex-end; margin-top: 0.65rem; }
.studio-mpo-error {
    margin-top: 0.6rem; padding: 0.4rem 0.6rem;
    background: var(--danger-bg, #fef2f2); border: 1px solid var(--danger-ring, #f87171);
    border-radius: var(--radius-sm); color: var(--danger-strong, #991b1b); font-size: 0.82rem;
}

.studio-import-tabs {
    display: flex;
    gap: 0;
    padding: 0 1rem;
    border-bottom: 1px solid var(--border);
    background: var(--surface);
}
.studio-import-tab {
    padding: 0.6rem 0.875rem;
    background: transparent;
    border: none; border-bottom: 2px solid transparent;
    color: var(--text-muted);
    font: inherit; font-size: 0.875rem; font-weight: 500;
    cursor: pointer;
    transition: color var(--motion-fast) var(--ease), border-color var(--motion-fast) var(--ease);
}
.studio-import-tab:hover { color: var(--text-strong); }
.studio-import-tab.active {
    color: var(--accent-strong);
    border-bottom-color: var(--accent);
    font-weight: 600;
}
.studio-import-body {
    padding: 1rem 1.25rem;
    flex: 1; min-height: 0;
    display: flex; flex-direction: column;
    gap: 0.5rem;
}
.studio-import-help {
    font-size: 0.85rem;
    color: var(--text-muted);
    margin: 0;
}
.studio-import-textarea {
    width: 100%;
    flex: 1;
    min-height: 200px;
    padding: 0.75rem;
    border: 1.5px solid var(--border-strong);
    border-radius: var(--radius-md);
    background: var(--surface-soft);
    color: var(--text-strong);
    font-size: 0.85rem;
    line-height: 1.5;
    resize: vertical;
    transition: border-color var(--motion-fast) var(--ease),
                box-shadow var(--motion-fast) var(--ease);
}
.studio-import-textarea:focus {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 3px var(--accent-ring);
    background: var(--surface);
}
.studio-import-counter {
    font-size: 0.78rem; color: var(--text-faint);
    text-align: right;
}

/* ── CSV import — Identifier type radio group ─────────────────── */
.studio-import-radio-group {
    display: flex;
    gap: 8px;
}
.studio-import-radio {
    flex: 1; min-width: 0;
    display: flex; flex-direction: column;
    gap: 2px;
    padding: 0.55rem 0.75rem;
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--surface);
    cursor: pointer;
    transition: border-color var(--motion-fast) var(--ease),
                background var(--motion-fast) var(--ease);
}
.studio-import-radio:hover { border-color: var(--border-strong); }
/* Selected scope card: clean state — a subtle accent border + soft fill only,
   no halo/box-shadow ring around the border. */
.studio-import-radio.is-selected {
    border-color: var(--accent);
    background: var(--accent-soft);
}
.studio-import-radio input[type="radio"] {
    position: absolute;
    opacity: 0;
    pointer-events: none;
}
.studio-import-radio-title {
    font-size: 0.875rem; font-weight: 600;
    color: var(--text-strong);
}
.studio-import-radio.is-selected .studio-import-radio-title { color: var(--accent-strong); }
.studio-import-radio-sub {
    font-size: 0.74rem; color: var(--text-muted);
    line-height: 1.35;
}

/* ── CSV import — column-to-import multi-select list ──────────── */
.studio-form-label-hint {
    margin-left: 6px;
    font-weight: 500;
    color: var(--text-faint);
    font-variant-numeric: tabular-nums;
}
.studio-import-col-list {
    display: flex; flex-direction: column;
    gap: 4px;
    /* Left column inside the mapping grid — fills its column's
       remaining height and scrolls internally. Min-height keeps
       the container usable for sheets with a single importable
       column. */
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    padding: 4px;
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--surface);
    scrollbar-width: thin;
    scrollbar-color: var(--border-strong) transparent;
}
.studio-import-col-item {
    display: flex; align-items: center;
    gap: 8px;
    padding: 0.4rem 0.5rem;
    border-radius: var(--radius-sm);
    cursor: pointer;
    transition: background var(--motion-fast) var(--ease);
    /* Shift-click on a label inside this list used to make the
       browser select the text contents of the modal back to the
       previous click point — same behaviour any contiguous text
       block has. The user reports this as "the modal looks broken"
       even though the checkbox toggle still fires. Disabling text
       selection on the row content is the standard fix; we still
       allow text selection inside .studio-import-col-sample below
       (the user might genuinely want to copy a sample value) by
       resetting it there. */
    user-select: none;
    -webkit-user-select: none;
}
.studio-import-col-item:hover { background: var(--surface-soft); }
.studio-import-col-item.is-checked { background: var(--accent-soft); }
/* Already-in-sheet rows in the Add Column picker. Visually muted so
   they read as "present, not addable" without needing to be removed
   from the list. Still hoverable so the title="Already in this
   sheet…" tooltip can surface its reason text. */
.studio-import-col-item.is-disabled {
    opacity: 0.55;
    cursor: not-allowed;
    background: var(--surface-soft);
}
.studio-import-col-item.is-disabled:hover {
    background: var(--surface-soft);
}
.studio-import-col-item.is-disabled input[type="checkbox"] {
    cursor: not-allowed;
}
.studio-import-col-item input[type="checkbox"] {
    flex-shrink: 0;
    width: 14px; height: 14px;
    accent-color: var(--accent);
}
/* Inferred-type badge alongside the column name. `A` = text,
   `#` = number. Clicking toggles. The badge is small and uses
   the same monospaced glyph style as the filter-picker kind
   indicator so the visual language stays consistent. */
.studio-import-col-type {
    flex-shrink: 0;
    width: 22px; height: 22px;
    display: inline-flex; align-items: center; justify-content: center;
    border: 1px solid var(--border);
    border-radius: 5px;
    background: var(--surface);
    color: var(--text-muted);
    font-size: 0.78rem; font-weight: 600;
    font-family: var(--font-mono);
    cursor: pointer;
    transition: background var(--motion-fast) var(--ease),
                border-color var(--motion-fast) var(--ease),
                color var(--motion-fast) var(--ease);
    user-select: none;
}
.studio-import-col-type:hover {
    border-color: var(--border-strong);
    color: var(--text);
}
.studio-import-col-type.is-number {
    background: var(--accent-soft);
    border-color: var(--accent);
    color: var(--accent-strong, var(--accent));
}
/* Invalid-Number state on the type badge — danger tint signals
   the column has non-numeric values and would fail validation
   if the user submitted now. Clicking still toggles back to
   Text. */
.studio-import-col-type.is-invalid {
    background: var(--danger-soft);
    border-color: var(--danger-border);
    color: var(--danger-strong);
}

/* Container row inside .studio-import-col-item. Pre-validation
   the item was a single flex row; with the inline error message
   we need a column layout (row + error). The new
   .studio-import-col-row carries the original flex-row layout
   so the rest of the column-list CSS stays untouched. */
.studio-import-col-item {
    /* Override the flex row from earlier rules — switch to
       column so the inline error wraps below the original row. */
    flex-direction: column;
    align-items: stretch;
}
.studio-import-col-row {
    display: flex; align-items: center; gap: 8px;
    width: 100%;
}
/* When the column has a validation error, tint the whole row
   subtly so it's distinguishable in a long list. */
.studio-import-col-item.has-error {
    background: var(--danger-soft);
    border: 1px solid var(--danger-border);
}
.studio-import-col-item.has-error.is-checked {
    /* Checked + error: still danger-tinted; the is-checked accent
       background would otherwise hide the error state. */
    background: var(--danger-soft);
    border-color: var(--danger-border);
}
.studio-import-col-error {
    display: flex; align-items: flex-start; gap: 6px;
    margin-top: 6px;
    padding: 6px 8px;
    background: var(--surface);
    border: 1px solid var(--danger-border);
    border-radius: var(--radius-sm);
    color: var(--danger-strong);
    font-size: 0.78rem;
    line-height: 1.35;
}
.studio-import-col-error-icon {
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    margin-top: 2px;
    color: var(--danger);
}
.studio-import-col-error-text {
    flex: 1; min-width: 0;
    word-break: break-word;
}
.studio-import-col-error-fix {
    flex-shrink: 0;
    background: transparent;
    border: 1px solid var(--danger-border);
    color: var(--danger-strong);
    font: inherit; font-size: 0.72rem; font-weight: 600;
    padding: 2px 8px;
    border-radius: var(--radius-sm);
    cursor: pointer;
    white-space: nowrap;
    align-self: center;
    transition: background var(--motion-fast) var(--ease);
}
.studio-import-col-error-fix:hover {
    background: var(--danger-soft);
}

/* Footer-strip variant of the import progress for validation
   errors — distinguishes "you can't import yet because of bad
   Number columns" from the existing "Working…" progress. */
.studio-import-progress-error {
    color: var(--danger-strong);
    font-weight: 600;
}

.studio-import-col-name {
    flex: 1; min-width: 0;
    font-size: 0.875rem; font-weight: 500;
    color: var(--text-strong);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.studio-import-col-sample {
    flex-shrink: 0;
    max-width: 180px;
    font-size: 0.72rem;
    color: var(--text-faint);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.studio-import-col-empty {
    padding: 0.5rem;
    text-align: center;
    font-size: 0.78rem;
    color: var(--text-faint);
    font-style: italic;
}

/* ── Add Column · existing-global picker rows ──────────────────────
   Compact single-row list of the project's user-created global columns
   in the Add Column modal. DEDICATED classes (not the shared
   .studio-import-col-* CSV-mapping styles, which switch the item to a
   stacked column layout for inline type-errors) so each global stays on
   ONE line: checkbox + truncated name + type pill + danger Remove. */
.studio-gcol-list {
    display: flex; flex-direction: column;
    gap: 2px;
    max-height: 320px;
    overflow-y: auto;
    padding: 4px;
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--surface);
    scrollbar-width: thin;
    scrollbar-color: var(--border-strong) transparent;
}
.studio-gcol-item {
    display: flex; align-items: center;
    gap: 6px;
    padding: 0 4px 0 6px;
    border-radius: var(--radius-sm);
    transition: background var(--motion-fast) var(--ease);
}
.studio-gcol-item:hover { background: var(--surface-soft); }
.studio-gcol-item.is-checked { background: var(--accent-soft); }
.studio-gcol-item.is-disabled { opacity: 0.6; }
/* Clickable pick region — the Remove button is a SIBLING of this label,
   never a child, so clicking Remove can't toggle the checkbox. The row's
   real height lives here (compact vertical padding). */
.studio-gcol-pick {
    flex: 1 1 auto; min-width: 0;
    display: flex; align-items: center; gap: 8px;
    padding: 6px 2px;
    cursor: pointer;
    user-select: none; -webkit-user-select: none;
}
.studio-gcol-item.is-disabled .studio-gcol-pick { cursor: default; }
.studio-gcol-pick input[type="checkbox"] {
    flex-shrink: 0;
    width: 14px; height: 14px;
    accent-color: var(--accent);
}
.studio-gcol-name {
    flex: 1 1 auto; min-width: 0;
    font-size: 0.875rem; font-weight: 500;
    color: var(--text-strong);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.studio-gcol-type {
    flex-shrink: 0;
    padding: 1px 8px;
    border: 1px solid var(--border);
    border-radius: 999px;
    background: var(--surface-soft);
    color: var(--text-muted);
    font-size: 0.68rem; font-weight: 600;
    letter-spacing: 0.02em;
    white-space: nowrap;
}
.studio-gcol-flag {
    flex-shrink: 0;
    font-size: 0.7rem;
    color: var(--text-faint);
    font-style: italic;
    white-space: nowrap;
}
/* Danger Remove — compact, visually secondary (ghost until hover) so it
   never competes with the row content; fixed square keeps the row layout
   stable whether or not it's present. */
.studio-gcol-del {
    flex-shrink: 0;
    display: inline-flex; align-items: center; justify-content: center;
    width: 26px; height: 26px;
    padding: 0;
    border: 1px solid transparent;
    border-radius: var(--radius-sm);
    background: transparent;
    color: var(--text-faint);
    cursor: pointer;
    transition: background var(--motion-fast) var(--ease),
                color var(--motion-fast) var(--ease),
                border-color var(--motion-fast) var(--ease);
}
.studio-gcol-del:hover {
    background: var(--danger-soft);
    border-color: var(--danger-border);
    color: var(--danger-strong);
}
.studio-gcol-del svg { width: 15px; height: 15px; display: block; }
.studio-gcol-empty {
    padding: 0.6rem 0.5rem;
    text-align: center;
    font-size: 0.78rem;
    color: var(--text-faint);
    font-style: italic;
}
.studio-import-coming {
    flex: 1;
    min-height: 200px;
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    text-align: center;
    color: var(--text-muted);
    padding: 2rem 1rem;
}
.studio-import-coming-title {
    margin-top: 0.875rem;
    font-size: 1rem; font-weight: 600;
    color: var(--text-strong);
    letter-spacing: -0.01em;
}
.studio-import-coming-sub {
    margin-top: 0.375rem;
    font-size: 0.85rem;
    max-width: 380px;
    color: var(--text-muted);
}

/* ── Draw tab (Add Compounds modal) ─────────────────────────────
   Hosts the Ketcher iframe. The pane is mounted once the user first
   activates Draw and stays mounted across tab switches via the
   .is-hidden modifier (toggling display:none preserves Ketcher's
   internal state and avoids paying its ~25 MB boot every swap). */
.studio-draw-pane {
    display: flex; flex-direction: column;
    flex: 1; min-height: 0;
    gap: 0.5rem;
}
.studio-draw-pane.is-hidden { display: none; }
.studio-draw-host {
    /* Anchor for the absolutely-positioned iframe below — the iframe
       fills inset:0 instead of relying on percentage-height
       inheritance, which was failing because the flex chain was
       letting the host collapse to ~0px when Ketcher booted. */
    position: relative;
    flex: 1;
    /* Fallback minimum height in case the modal's fixed height
       calculation gets squeezed (e.g. very short viewports). Keeps
       Ketcher usable down to ~620px viewport heights. */
    min-height: 480px;
    border: 1.5px solid var(--border-strong);
    border-radius: var(--radius-md);
    background: var(--surface-soft);
    overflow: hidden;
}
.studio-draw-host.is-error {
    border-color: var(--danger);
    background: rgba(220, 38, 38, 0.06);
}
.studio-draw-iframe {
    /* Absolute fill — guarantees the iframe matches the host even if
       the parent flex chain has 0 computed height during Ketcher's
       initial mount. Without this, Ketcher measured the iframe at a
       few pixels tall and rendered its UI compressed at the top. */
    position: absolute;
    inset: 0;
    width: 100%; height: 100%;
    display: block;
    border: 0;
    background: #fff;
}
.studio-draw-overlay {
    position: absolute; inset: 0;
    display: flex; align-items: center; justify-content: center;
    text-align: center;
    padding: 1rem;
    font-size: 0.85rem;
    color: var(--text-muted);
    background: var(--surface-soft);
    z-index: 1;
}
.studio-draw-overlay.is-error {
    color: var(--danger);
    font-weight: 500;
}
.studio-draw-footer {
    /* Fixed-height band so the canvas above (flex:1) keeps the SAME size
       whether the SMILES readout is empty or populated. */
    flex: 0 0 auto;
    height: 40px;
    display: flex; align-items: center; gap: 0.625rem;
}
/* Pin the Clear button to the readout's exact height so the two line up
   precisely (btn-small's natural height drifted a px or two from the readout
   box). Scoped to the draw footer — other btn-smalls are untouched. */
.studio-draw-footer .btn-small { height: 30px; }
.studio-draw-smiles {
    flex: 1; min-width: 0;
    /* Readout box height == the Clear button (both 30 px). Fixed height +
       line-height (not vertical padding) keeps it from growing when the SMILES
       populates, so the canvas above never shifts. */
    height: 30px; line-height: 28px;
    padding: 0 0.6rem;
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    background: var(--surface-soft);
    font-size: 0.78rem;
    color: var(--text-strong);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* The "SMILES" badge sits INSIDE the readout, shorter than it (≈18 px in a
   30 px box) so there's clear padding above/below, with a strong accent fill
   for visibility. */
.studio-draw-smiles-label {
    display: inline-block;
    margin-right: 0.5rem;
    height: 18px; line-height: 18px;
    padding: 0 0.45rem;
    border-radius: var(--radius-sm);
    background: var(--accent);
    color: #fff;
    font-size: 0.66rem; font-weight: 700;
    letter-spacing: 0.04em;
    vertical-align: middle;
}
.studio-draw-smiles-empty {
    color: var(--text-faint);
    font-style: italic;
}
.studio-draw-error {
    padding: 0.5rem 0.625rem;
    border-radius: var(--radius-sm);
    background: rgba(220, 38, 38, 0.10);
    color: var(--danger);
    font-size: 0.8rem;
    font-weight: 500;
}

/* Per-item delete button on modal rows (revealed on hover). */
.studio-modal-row {
    display: flex; align-items: stretch;
    border-radius: var(--radius-md);
    transition: background var(--motion-fast) var(--ease);
    /* Selector rows are list/grid targets, not prose — Shift-clicking
       to range-select must not drag a giant text selection across the
       modal. Scoped to selector rows only (projects AND sheets share
       this class); the rest of the app keeps normal text selection. */
    user-select: none;
    -webkit-user-select: none;
}
/* Whole-row hover: the highlight lives on the ROW so the trailing
   action buttons (rename/delete, transparent backgrounds) read as
   part of the same surface instead of the old item-only highlight
   that stopped before them. Active stays stronger/accent (below) so
   it's still distinct from a plain hover. */
.studio-modal-row:hover { background: var(--surface-soft); }
.studio-modal-row:hover .studio-modal-row-delete,
.studio-modal-row:hover .studio-modal-row-rename { opacity: 1; }
.studio-modal-row-delete,
.studio-modal-row-rename {
    flex-shrink: 0;
    width: 36px;
    border: none;
    background: transparent;
    color: var(--text-faint);
    cursor: pointer;
    display: flex; align-items: center; justify-content: center;
    opacity: 0;
    border-radius: var(--radius-md);
    transition: background var(--motion-fast) var(--ease),
                color var(--motion-fast) var(--ease),
                opacity var(--motion-fast) var(--ease);
}
.studio-modal-row-delete:hover { background: var(--danger-bg); color: var(--danger-strong); }
.studio-modal-row-rename:hover { background: var(--accent-soft); color: var(--accent-strong); }
.studio-modal-row-delete svg,
.studio-modal-row-rename svg { width: 14px; height: 14px; stroke: currentColor; fill: none; }
.studio-modal-row .studio-modal-item { flex: 1; }

/* ── Sheet-selector folder grouping ─────────────────────────────
   Opt-in: only the Design Sheets selector passes `folders`. The
   Projects selector is unaffected (flat list, none of this renders). */
.studio-modal-folder { margin-top: 0.15rem; }
.studio-modal-folder-header {
    display: flex; align-items: center; gap: 0.5rem;
    padding: 0.45rem 0.55rem;
    border-radius: var(--radius-md);
    cursor: pointer;
    color: var(--text-strong);
    font-weight: 600;
    font-size: 0.9rem;
    user-select: none;
}
.studio-modal-folder-header:hover { background: var(--surface-hover, rgba(120,130,150,0.08)); }
.studio-modal-folder-header:hover .studio-modal-row-delete,
.studio-modal-folder-header:hover .studio-modal-row-rename { opacity: 1; }
.studio-modal-folder-header:focus-visible {
    outline: none;
    box-shadow: 0 0 0 2px var(--accent-ring);
}
.studio-modal-folder-icon { color: var(--accent); flex-shrink: 0; }
.studio-modal-folder-name {
    flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.studio-modal-folder-count {
    flex-shrink: 0;
    font-size: 0.75rem; font-weight: 600;
    color: var(--text-muted);
    background: var(--surface-2, rgba(120,130,150,0.12));
    border-radius: 999px;
    padding: 0.05rem 0.5rem;
    margin-right: 0.15rem;
}
/* Sheets inside a folder are indented so the hierarchy reads at a
   glance without a heavy tree control. */
.studio-modal-group-sheets {
    margin-left: 0.85rem;
    padding-left: 0.5rem;
    border-left: 1.5px solid var(--border, rgba(120,130,150,0.25));
}
.studio-modal-folder-empty {
    padding: 0.4rem 0.6rem;
    color: var(--text-faint);
    font-size: 0.82rem;
    font-style: italic;
}
/* Active (open) sheet highlight in the grouped view. */
.studio-modal-row.is-active,
.studio-modal-row.is-active:hover {
    /* Active stays accent-tinted even while hovered so it never
       collapses into the subtler plain-hover surface. */
    background: var(--accent-soft);
}
.studio-modal-row.is-active .studio-modal-item-name { color: var(--accent-strong); }
/* (Per-row "move to folder" select removed — folder reassignment now
   lives in the styled Sheet Edit modal.) */

/* ── Modals ─────────────────────────────────────────────────── */
.studio-modal {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    width: min(560px, 92vw);
    max-height: 80vh;
    display: flex; flex-direction: column;
    box-shadow: var(--shadow-modal);
    overflow: hidden;
}

/* ── Two-panel selector modal (Sheet + Project pickers) ─────────────
   FIXED footprint: switching folders/categories, opening empty or full
   groups, and long names/descriptions must NOT resize the modal. The
   body is a flex row whose two panels scroll independently (min-height:0
   is what lets them scroll instead of growing the modal). */
.studio-modal.studio-modal-twopanel {
    width: min(1080px, 95vw);
    height: min(720px, 90vh);   /* fixed height (not max) → stable size */
}
.studio-modal-twopanel-body {
    flex: 1;
    min-height: 0;
    display: flex;
    border-top: 1px solid var(--border);
}
.studio-modal-panel { min-height: 0; overflow-y: auto; overscroll-behavior: contain; }
.studio-modal-panel-left {
    flex: 0 0 224px;            /* a touch wider so folder/category names breathe */
    border-right: 1px solid var(--border);
    background: var(--surface-soft);
    padding: 0.35rem 0.2rem;    /* tighter left edge */
}
/* Folder-tree panel (Design Sheets / copy picker) keeps a small left inset so
   the leading expand/collapse chevron isn't glued to the panel edge and the
   rounded row hover/active fills read cleanly. It's far tighter than before
   regardless, since the checkbox column is hidden by default (edit mode off).
   The category panel (Projects) keeps the default padding above. */
.studio-modal-panel-left.is-folder-panel { padding-left: 0.2rem; }
.studio-modal-panel-right { flex: 1 1 auto; min-width: 0; padding: 0.4rem 0.5rem; }
.studio-modal-panel-empty { padding: 0.75rem 0.6rem; color: var(--text-faint); font-size: 0.8rem; }

/* Left group rows (folders / categories) — kept tight on the left so the
   tree reads compactly while still showing hierarchy. */
.studio-modal-grouprow {
    display: flex; align-items: center; gap: 0.3rem;
    width: 100%;
    padding: 0.35rem 0.3rem;
    border: none; background: transparent;
    border-radius: var(--radius-sm);
    color: var(--text);
    font-size: 0.84rem; font-weight: 500;
    cursor: pointer; text-align: left;
    transition: background var(--motion-fast) var(--ease), color var(--motion-fast) var(--ease);
}
.studio-modal-grouprow:hover { background: var(--hover-bg); }
.studio-modal-grouprow.is-active { background: var(--accent-muted); color: var(--accent-strong); }
.studio-modal-grouprow .studio-modal-folder-icon { flex-shrink: 0; color: var(--text-muted); }
.studio-modal-grouprow.is-active .studio-modal-folder-icon { color: var(--accent-strong); }
.studio-modal-grouprow-name {
    flex: 1; min-width: 0;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.studio-modal-grouprow-count {
    flex-shrink: 0;
    font-size: 0.72rem; font-variant-numeric: tabular-nums;
    color: var(--text-faint); background: var(--surface-sunk);
    border-radius: 999px; padding: 0.05rem 0.4rem;
}
.studio-modal-grouprow.is-active .studio-modal-grouprow-count { color: var(--accent-strong); background: var(--surface); }
/* Selected (checked) group — soft accent fill + ring so a multi-select
   reads clearly, distinct from the active/viewing state above. */
.studio-modal-grouprow.is-picked { background: var(--accent-soft); box-shadow: inset 0 0 0 1px var(--accent-ring); }
.studio-modal-grouprow.is-picked.is-active { background: var(--accent-muted); }
/* Per-row checkbox (editable groups) + equal-width spacer (protected
   groups) so every row's glyph + name start at the same x. */
.studio-modal-grouprow-pick {
    flex-shrink: 0; width: 14px; height: 14px; margin: 0;
    cursor: pointer; accent-color: var(--accent);
}
.studio-modal-grouprow-pick-spacer { flex-shrink: 0; width: 14px; height: 14px; }
/* Expand/collapse twirl for a top folder with subfolders (+ a same-width
   spacer so rows without a twirl stay aligned). Subfolders indent. */
.studio-modal-grouprow-twirl,
.studio-modal-grouprow-twirl-spacer {
    flex-shrink: 0; width: 13px; height: 14px;
    display: inline-flex; align-items: center; justify-content: center;
}
.studio-modal-grouprow-twirl {
    border: none; background: transparent; padding: 0;
    border-radius: var(--radius-sm); color: var(--text-muted); cursor: pointer;
}
.studio-modal-grouprow-twirl:hover { background: var(--hover-bg); color: var(--text-strong); }
/* Left padding adapts to the row's leading column so names sit tight on the
   left without crowding whatever control leads the row:
   • Category rows (Projects) have no twirl. With a checkbox (org-admin) the
     checkbox gets breathing room; without one (member) the name sits tight.
   • Folder rows (Design Sheets / copy picker) lead with the twirl column, so
     the whole stack hugs the left while keeping the expand affordance. */
.studio-modal-grouprow.is-category { padding-left: 0.5rem; }
.studio-modal-grouprow.is-category.has-pick { padding-left: 0.6rem; }
/* position:relative anchors the absolutely-positioned row "…" action menu.
   Top-level rows keep a small indent so the expand chevron has breathing room
   from the panel edge without looking overly inset. */
.studio-modal-grouprow.is-folder { padding-left: 0.3rem; gap: 0.15rem; position: relative; }
/* Subfolder indent — clearly nested beneath its parent (≈1.1rem deeper) so the
   hierarchy reads at a glance and the row never hugs the left edge. */
.studio-modal-grouprow.is-folder.is-subfolder { padding-left: 1.4rem; }
/* A touch more breathing room between the folder icon and its name. Applied as
   an icon margin (folder mode only) so it does NOT widen the chevron→icon gap,
   move the chevron, or change the row's left indent. Projects untouched. */
.studio-modal-grouprow.is-folder .studio-modal-folder-icon { margin-right: 0.15rem; }
/* Per-folder "…" action button (file-browser style). Sits at the right edge,
   over the count slot; hidden (and non-interactive so row-click selection
   still works) until the row is hovered/focused or its menu is open. */
.studio-modal-grouprow-menu {
    position: absolute; right: 0.25rem; top: 50%; transform: translateY(-50%);
    flex-shrink: 0;
    display: inline-flex; align-items: center; justify-content: center;
    width: 22px; height: 22px; padding: 0;
    border: none; background: transparent; border-radius: var(--radius-sm);
    color: var(--text-muted); cursor: pointer;
    opacity: 0; pointer-events: none;
    transition: opacity var(--motion-fast) var(--ease),
                background var(--motion-fast) var(--ease),
                color var(--motion-fast) var(--ease);
}
.studio-modal-grouprow.is-folder:hover .studio-modal-grouprow-menu,
.studio-modal-grouprow.is-folder:focus-within .studio-modal-grouprow-menu,
.studio-modal-grouprow-menu.is-open { opacity: 1; pointer-events: auto; }
.studio-modal-grouprow-menu:hover { background: var(--hover-bg); color: var(--text-strong); }
/* The count badge gives way to the action button on hover/focus/open so they
   don't overlap (it keeps its space, so the name's ellipsis doesn't shift). */
.studio-modal-grouprow-count { transition: opacity var(--motion-fast) var(--ease); }
.studio-modal-grouprow.is-folder:hover .studio-modal-grouprow-count,
.studio-modal-grouprow.is-folder:focus-within .studio-modal-grouprow-count,
.studio-modal-grouprow.is-folder.is-menu-open .studio-modal-grouprow-count { opacity: 0; }

/* Right item cards (sheet / project metadata) */
.studio-modal-card {
    display: flex; align-items: flex-start; gap: 0.5rem;
    padding: 0.45rem 0.5rem;
    border: 1px solid var(--border-faint);   /* subtle definition (was invisible) */
    border-radius: var(--radius-md);
    margin-bottom: 5px;
    transition: background var(--motion-fast) var(--ease), border-color var(--motion-fast) var(--ease);
}
.studio-modal-card:hover { background: var(--surface-soft); border-color: var(--border); }
.studio-modal-card:hover .studio-modal-row-rename,
.studio-modal-card:hover .studio-modal-row-delete { opacity: 1; }
.studio-modal-card.is-picked { background: var(--accent-soft); border-color: var(--accent-ring); }
.studio-modal-card.is-active { border-color: var(--accent); }
/* Projects modal, MEMBER (no-checkbox) view only: without the leading checkbox
   the card content hugs the left edge, so give the name/description a little
   more breathing room. Scoped to the category panel that hides checkboxes
   (`is-roomy` = groupByCategory && !canDeleteItems), so the org-admin checkbox
   layout, Design Sheets sheet cards, and the copy picker are all untouched.
   Still far tighter than the admin view (no 15px checkbox + gap). */
.studio-modal-panel-right.is-roomy .studio-modal-card { padding-left: 0.85rem; }
/* The headline line height every card column aligns to (name 0.875rem +
   breathing room). The checkbox, text headline and actions all match it. */
.studio-modal-card { --card-line: 1.5rem; }
/* Checkbox nudged down to sit centered on the headline line. */
.studio-modal-card-pick {
    flex-shrink: 0; margin-top: 0.3rem;
    width: 15px; height: 15px; cursor: pointer; accent-color: var(--accent);
}
/* Text COLUMN (headline + description). Owning its own column means the
   description truncates to the name/date width and never runs under or
   past the action icons. */
.studio-modal-card-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
/* Headline = name + lock + date on ONE line; the name takes the room and
   truncates first, the date is small/muted and sits at the right edge of
   the text column. min-height = the shared line so the actions align. */
.studio-modal-card-body {
    min-width: 0; min-height: var(--card-line);
    border: none; background: transparent; padding: 0; margin: 0;
    text-align: left; cursor: pointer;
    display: flex; align-items: center; gap: 0.6rem;
}
.studio-modal-card-name-text {
    flex: 1 1 auto; min-width: 0;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    font-size: 0.875rem; font-weight: 600; color: var(--text-strong);
}
.studio-modal-card-body .studio-modal-lock { flex-shrink: 0; margin-left: 0; }
.studio-modal-card-meta {
    flex-shrink: 0; white-space: nowrap;
    font-size: 0.72rem; color: var(--text-muted); font-variant-numeric: tabular-nums;
}
/* Date as the FAR-RIGHT card sibling (after the hover actions). The text
   column's flex:1 pushes the actions + date to the right edge; the date stays
   put because the actions reserve width at opacity:0. Aligned to the headline
   line so it sits level with the name + action glyphs. Narrow windows: the name
   truncates first (text column min-width:0), date + actions never wrap. */
.studio-modal-card-date {
    align-self: flex-start;
    min-height: var(--card-line);
    display: inline-flex; align-items: center;
    padding-left: 0.4rem;
}
/* Concise single-line preview — the full text lives in the Details view.
   Constrained to the text column (its parent) so it can't reach the
   actions; clickable (opens the item) to match the headline. */
.studio-modal-card-desc {
    font-size: 0.74rem; color: var(--text-faint); line-height: 1.3;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
    cursor: pointer;
}
/* Action icons sit at the top of the card and center on the headline
   line (matching min-height + top alignment from the card's flex-start),
   so they never stretch down beside the description. Compact, no hover
   fill (color-only) — overrides the wider shared list-row buttons. */
.studio-modal-card-actions {
    flex-shrink: 0; display: flex; align-items: center; gap: 0;
    min-height: var(--card-line);
}
.studio-modal-card-actions .studio-modal-row-rename,
.studio-modal-card-actions .studio-modal-row-delete { width: 26px; }
.studio-modal-card-actions .studio-modal-row-rename:hover,
.studio-modal-card-actions .studio-modal-row-delete:hover { background: transparent; }
.studio-modal-card-actions .studio-modal-row-rename:hover { color: var(--accent-strong); }
.studio-modal-card-actions .studio-modal-row-delete:hover { color: var(--danger-strong); }

/* Edit Design Sheet — ONE "Lock & permissions" section: a lock glyph +
   status + a single "Manage" button that opens the SAME shared Lock &
   Permissions modal (lock/unlock AND editor grants). No second system. */
.studio-edit-lock-section {
    margin-top: 0.95rem;
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--surface-soft);
    padding: 0.6rem 0.7rem;
}
.studio-edit-lock-section.is-disabled { opacity: 0.92; }
.studio-edit-lock-main { display: flex; align-items: center; gap: 0.6rem; }
.studio-edit-lock-glyph { flex-shrink: 0; width: 17px; height: 17px; color: var(--accent); }
.studio-edit-lock-text { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.studio-edit-lock-label { font-size: 0.875rem; font-weight: 600; color: var(--text-strong); }
.studio-edit-lock-hint { font-size: 0.78rem; color: var(--text-faint); line-height: 1.35; margin-top: 1px; }
.studio-edit-lock-manage-btn { flex-shrink: 0; }
.studio-edit-lock-note {
    font-size: 0.76rem; color: var(--text-faint); line-height: 1.35;
    margin-top: 0.5rem; padding-top: 0.5rem; border-top: 1px solid var(--border-faint);
}

/* New-folder modal — Top-level vs Subfolder radio choice. */
.studio-folder-mode { display: flex; gap: 1rem; margin: 0.1rem 0 0.6rem; flex-wrap: wrap; }
.studio-folder-mode-opt {
    display: inline-flex; align-items: center; gap: 0.4rem;
    font-size: 0.875rem; color: var(--text); cursor: pointer;
}
.studio-folder-mode-opt input[type="radio"] { accent-color: var(--accent); cursor: pointer; }
.studio-folder-mode-opt.is-disabled { color: var(--text-faint); cursor: not-allowed; }
.studio-folder-mode-opt.is-disabled input[type="radio"] { cursor: not-allowed; }

/* Comment-thread row actions (Comments modal): compact icon buttons,
   right-aligned, subtle color-only hover (no background fill). */
.studio-comment-actions { display: flex; justify-content: flex-end; gap: 2px; margin-top: 0.35rem; }
.studio-comment-action {
    display: inline-flex; align-items: center; justify-content: center;
    width: 26px; height: 26px; padding: 0;
    border: none; background: transparent; border-radius: var(--radius-sm);
    color: var(--text-faint); cursor: pointer;
    transition: color var(--motion-fast) var(--ease);
}
.studio-comment-action:hover { background: transparent; color: var(--accent-strong); }
.studio-comment-action-delete:hover { color: var(--danger-strong); }
.studio-comment-action svg { width: 14px; height: 14px; stroke: currentColor; fill: none; }

/* Create actions moved into the search row in two-panel mode. The
   margin-left separates the icon buttons from the search input (they
   were touching); align-self:stretch makes the buttons exactly as tall
   as the input so the row reads as one aligned band. */
.studio-modal-search-actions {
    display: flex; align-items: stretch; gap: 6px;
    flex-shrink: 0; margin-left: 10px;
    /* Stretch the wrapper to the search input's height so the icon
       buttons inside (align-self:stretch) end up exactly input-tall. */
    align-self: stretch;
}
/* Compact square icon button (New folder / New sheet / New project).
   Sized to the search input's height via align-self:stretch + a square
   aspect so the glyph sits in a 1:1 hit area, matching the existing
   .studio-modal-search-edit toggle. */
.studio-modal-action-btn {
    align-self: stretch;
    aspect-ratio: 1 / 1;
    min-width: 38px;
    flex-shrink: 0;
    display: inline-flex; align-items: center; justify-content: center;
    padding: 0;
    border: 1.5px solid var(--border-strong);   /* stronger, still subtle */
    border-radius: var(--radius-md);
    background: var(--surface);
    color: var(--text-muted);
    cursor: pointer;
    transition: background var(--motion-fast) var(--ease),
                border-color var(--motion-fast) var(--ease),
                color var(--motion-fast) var(--ease);
}
.studio-modal-action-btn:hover {
    background: var(--surface-sunk);
    border-color: var(--accent);
    color: var(--text-strong);
}
.studio-modal-action-btn:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--accent-ring); }
/* Size the glyph explicitly — this isn't a .btn, so the global
   `.btn svg` sizing doesn't apply (covers the shared <Icon> plus). */
.studio-modal-action-btn svg { display: block; width: 17px; height: 17px; }
/* Primary (create) variant — accent-filled to read as the main action. */
.studio-modal-action-btn.is-primary {
    background: var(--accent); border-color: var(--accent); color: var(--accent-fg);
}
.studio-modal-action-btn.is-primary:hover { background: var(--accent-strong); border-color: var(--accent-strong); color: var(--accent-fg); }

/* Two-panel search bar — slightly shorter than the single-panel default
   and a touch stronger border, so it reads as a crisp, compact control
   matching the icon buttons beside it. The RIGHT padding matches the right
   list panel's inset (0.5rem) so the create-action buttons line up with the
   sheet rows below instead of sitting ~14px to their left. */
.studio-modal-twopanel .studio-modal-search { padding: 0.625rem 0.5rem 0.625rem 1.25rem; }
.studio-modal-twopanel .studio-search-input {
    padding-top: 0.45rem; padding-bottom: 0.45rem;
    border-color: var(--border-strong);
}
/* Center the magnifier inside the (now shorter) input. The shared rule
   nudges it up by -30%; in these modals it should be dead-centre. */
.studio-modal-twopanel .studio-search-icon { transform: translateY(-50%); }

/* Compact selection footer — shorter bar; "N selected" left
   (margin-right:auto pushes the actions to the right edge); the action
   buttons stay vertically centred via the footer's align-items:center. */
.studio-modal-footer-compact { padding: 0.4rem 0.75rem; gap: 0.4rem; }
.studio-modal-footer-count { font-size: 0.8rem; color: var(--text-muted); margin-right: auto; }
.studio-modal-footer-delete { flex-shrink: 0; }
/* Persistent left-aligned footer note (e.g. the copy picker's
   "Open target after copy" toggle). margin-right:auto keeps any
   selection actions pinned to the right. */
.studio-modal-footer-note { margin-left: auto; }
.studio-modal-footer-check {
    display: inline-flex; align-items: center; gap: 0.4rem;
    font-size: 0.8rem; color: var(--text); cursor: pointer; user-select: none;
}
.studio-modal-footer-check input[type="checkbox"] {
    width: 14px; height: 14px; margin: 0; accent-color: var(--accent); cursor: pointer;
}
/* Header secondary control (Reset demo data) — compact, sits beside the title. */
.studio-modal-header-btn { white-space: nowrap; }

/* Narrow viewports — stack the panels so the picker stays usable */
@media (max-width: 560px) {
    .studio-modal.studio-modal-twopanel { height: min(720px, 92vh); }
    .studio-modal-twopanel-body { flex-direction: column; }
    .studio-modal-panel-left {
        flex: 0 0 auto; max-height: 35%;
        border-right: none; border-bottom: 1px solid var(--border-faint);
    }
}
.studio-modal-header {
    display: flex; align-items: center; justify-content: space-between;
    padding: 1rem 1.25rem;
    border-bottom: 1px solid var(--border-faint);
}
.studio-modal-header h3 {
    font-size: 1.05rem; font-weight: 700;
    color: var(--text-strong); letter-spacing: -0.01em; margin: 0;
}
.studio-modal-header-actions {
    display: flex; align-items: center; gap: 6px;
}
/* Edit-mode icon toggle — lives at the right edge of the search bar
   so it sits in the user's scan path while browsing the list. The
   icon swaps between a pencil (enter edit) and a check (exit edit).
   Height matches the search input via `align-self: stretch` so the
   two controls form one visually-aligned row regardless of the
   search input's intrinsic height. Width stays square to the input
   height so the icon sits in a 1:1 hit area. */
.studio-modal-search-edit {
    flex-shrink: 0;
    align-self: stretch;
    margin-left: 6px;
    /* aspect-ratio: 1 keeps the button square at whatever height the
       search input ends up at. Older browsers fall back to a min-width
       of 36 px (close to the input's actual rendered height). */
    aspect-ratio: 1 / 1;
    min-width: 36px;
    display: inline-flex; align-items: center; justify-content: center;
    padding: 0;
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--surface);
    color: var(--text-muted);
    cursor: pointer;
    transition: background var(--motion-fast) var(--ease),
                border-color var(--motion-fast) var(--ease),
                color var(--motion-fast) var(--ease);
}
.studio-modal-search-edit:hover {
    background: var(--surface-sunk);
    border-color: var(--border-strong);
    color: var(--text-strong);
}
.studio-modal-search-edit.is-active {
    background: var(--accent-soft);
    border-color: var(--accent-ring);
    color: var(--accent-strong);
}
.studio-modal-search-edit:focus-visible {
    outline: none;
    box-shadow: 0 0 0 3px var(--accent-ring);
}
.studio-modal-search-edit svg { display: block; }
/* Edit bar — sits above the list to communicate "you're in edit mode"
   and surface the select-all toggle + count. */
.studio-modal-edit-bar {
    display: flex; align-items: center;
    padding: 0.5rem 1.25rem;
    background: var(--surface-soft);
    border-bottom: 1px solid var(--border-faint);
    font-size: 0.82rem;
    color: var(--text-muted);
}
.studio-modal-edit-bar-all {
    display: inline-flex; align-items: center;
    gap: 8px;
    cursor: pointer;
    user-select: none;
}
.studio-modal-edit-bar-all input[type="checkbox"] {
    width: 14px; height: 14px;
    accent-color: var(--accent);
    cursor: pointer;
}
/* Pickable row in edit mode — full-row label with leading checkbox,
   no chevron, no per-row trash. Click anywhere toggles selection. */
.studio-modal-row-pickable {
    display: flex; align-items: center;
    gap: 0.625rem;
    padding: 0 0.625rem;
    border-radius: var(--radius-md);
    cursor: pointer;
    transition: background var(--motion-fast) var(--ease);
    /* No text drag while Shift range-selecting. */
    user-select: none;
    -webkit-user-select: none;
}
.studio-modal-row-pickable:hover { background: var(--surface-soft); }
/* Selected-for-batch rows read as ONE coherent list selection, not a
   column of disconnected rounded pills. A soft accent fill + subtle
   left accent bar anchor the run; the outer-corner rounding is driven
   by adjacency classes computed in JS from the VISIBLE order + the
   picked set (is-picked-single / -start / -middle / -end) — NOT
   :first-child/:last-child, so each contiguous run rounds correctly
   wherever it sits and separate runs each round independently.
   Distinct from the active/open-sheet style (.studio-modal-row.is-active
   — used only outside edit mode, so the two never collide). */
.studio-modal-row-pickable.is-picked {
    background: var(--accent-soft);
    box-shadow: inset 3px 0 0 var(--accent);
    border-radius: 0;
}
.studio-modal-row-pickable.is-picked-single { border-radius: var(--radius-md); }
.studio-modal-row-pickable.is-picked-start  { border-radius: var(--radius-md) var(--radius-md) 0 0; }
.studio-modal-row-pickable.is-picked-middle { border-radius: 0; }
.studio-modal-row-pickable.is-picked-end    { border-radius: 0 0 var(--radius-md) var(--radius-md); }
.studio-modal-row-pick {
    flex-shrink: 0;
    width: 16px; height: 16px;
    accent-color: var(--accent);
    cursor: pointer;
    margin: 0;
}
.studio-modal-item-display {
    flex: 1; min-width: 0;
    background: transparent !important;
    border: none !important;
    box-shadow: none !important;
    padding: 0.625rem 0;
    cursor: pointer;
}
.studio-modal-search,
.studio-modal-list,
.studio-modal-empty,
.studio-modal-item-name,
.studio-modal-item-meta,
.studio-modal-footer { letter-spacing: 0; }
.studio-modal-search {
    position: relative;
    padding: 0.875rem 1.25rem 0.5rem;
    /* Flex so the optional edit icon button can sit at the right
       edge of the search input. The input takes the remaining space;
       the search-icon stays absolutely positioned over the input. */
    display: flex;
    align-items: center;
    gap: 0;
}
.studio-modal-search .studio-search-input { flex: 1; min-width: 0; }
.studio-search-icon {
    position: absolute;
    left: 1.625rem; top: 50%; transform: translateY(-30%);
    color: var(--text-faint);
    pointer-events: none;
}
/* Add Column modal search bar with a leading magnifier icon. The
   wrap is `position: relative` so the absolutely-positioned icon
   sits flush inside the input's left padding zone. The input gets
   a wider left padding via the `-with-icon` modifier so its
   placeholder and value never collide with the icon. */
.studio-search-input-wrap {
    position: relative;
    display: flex;
    align-items: center;
}
.studio-search-input-icon {
    position: absolute;
    left: 0.75rem;
    top: 50%;
    transform: translateY(-50%);
    color: var(--text-faint);
    display: flex; align-items: center;
    pointer-events: none;
}
.studio-search-input-with-icon {
    padding-left: 2.125rem !important;
}
.studio-search-input {
    width: 100%;
    padding: 0.55rem 0.75rem 0.55rem 2.125rem;
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--surface-soft);
    font: inherit; font-size: 0.9rem;
    color: var(--text-strong);
    transition: border-color var(--motion-fast) var(--ease),
                box-shadow var(--motion-fast) var(--ease),
                background var(--motion-fast) var(--ease);
}
.studio-search-input:focus {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 3px var(--accent-ring);
    background: var(--surface);
}
.studio-modal-list {
    flex: 1;
    /* `min-height: 0` is the canonical flex fix that lets an
       overflow:auto child actually scroll — without it the flex
       item's default `min-height: auto` lets the list expand to fit
       its content, defeating the modal's max-height: 80vh cap and
       making the page scroll instead of the list.
       `overscroll-behavior: contain` keeps wheel/touch scroll
       trapped inside the list so reaching the bottom of a long
       project list doesn't accidentally scroll the page below. */
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    padding: 0.25rem 0.625rem;
}
/* Compact sheet rows (Design Sheets selector only — scoped via the
   list class so the Projects modal keeps its roomier rows). Smaller
   vertical padding makes the list shorter while the whole row is
   still a comfortably large click target. */
.studio-modal-list-compact .studio-modal-item { padding-top: 0.45rem; padding-bottom: 0.45rem; }
.studio-modal-list-compact .studio-modal-folder-header { padding-top: 0.35rem; padding-bottom: 0.35rem; }
/* Inter-row separation MUST NOT use margin/padding here: a dynamic
   margin (1px when unpicked, 0 when picked) shrank the list height by
   1px per selected row as you multi-selected. Use a box-shadow
   hairline instead — box-shadow has no box-model footprint, so the
   list height is identical in every selection state. Picked rows opt
   out of the hairline so a contiguous selected run reads as ONE
   seamless block with no white line between adjacent cards. */
.studio-modal-list-compact .studio-modal-row { margin-bottom: 0; }
.studio-modal-list-compact .studio-modal-row-pickable:not(.is-picked) {
    box-shadow: 0 1px 0 var(--border-faint);
}

.studio-modal-item {
    width: 100%;
    display: flex; align-items: center; gap: 0.625rem;
    padding: 0.75rem 0.875rem;
    background: transparent; border: 1px solid transparent;
    border-radius: var(--radius-md);
    color: var(--text); text-align: left;
    cursor: pointer;
    transition: background var(--motion-fast) var(--ease),
                border-color var(--motion-fast) var(--ease);
}
/* Hover highlight is now owned by .studio-modal-row (full row incl.
   the trailing action buttons). The item button stays transparent so
   it doesn't paint a competing inner block that ends before the
   buttons. Active/selected styling is still applied at the row. */
.studio-modal-item:hover { background: transparent; border-color: transparent; }
.studio-modal-item-main { flex: 1; min-width: 0; }
.studio-modal-item-name {
    font-size: 0.95rem; font-weight: 600; color: var(--text-strong);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.studio-modal-item-meta {
    margin-top: 2px;
    font-size: 0.78rem; color: var(--text-muted);
    display: flex; gap: 0.875rem; flex-wrap: wrap;
}
.studio-modal-empty {
    padding: 1.5rem 1rem; text-align: center;
    color: var(--text-muted); font-size: 0.9rem;
}
.studio-modal-footer {
    padding: 0.625rem 1rem;
    border-top: 1px solid var(--border-faint);
    display: flex; justify-content: flex-end; align-items: center;
    /* Footer buttons need breathing room so Cancel and the primary
       action don't feel crammed together. The previous layout had no
       gap between them. */
    gap: 0.625rem;
    background: var(--surface-soft);
}

/* ── Column scope (Global / Local) — integrated header treatment ──
   Global columns get the default header treatment. Local columns
   layer a subtle diagonal-stripe pattern over the same base, so the
   two scopes are distinguishable at a glance even when they share
   the same column label. The stripe is drawn as a CSS
   linear-gradient repeating background image that paints on top of
   the th's solid background colour; the colour-mix keeps the stripe
   readable across hover, sort, selected, and frozen states without
   dedicated overrides for each.

   The th's `title` attribute carries the verbal explanation
   ("global column (values shared across…)" / "local column (values
   exist only in this sheet)") so users can confirm the scope on
   hover.

   Why a diagonal stripe and not, say, a tinted background?
     - A tint competes with the accent-soft / accent-muted hover and
       selected states. A pattern overlay survives those state
       changes because it's a separate paint layer.
     - The stripe is intentionally low-contrast so the column header
       still feels calm. */
/* Local-column treatment is two coordinated cues:
     1. A NARROW diagonal stripe behind the entire header. The stripe
        is intentionally thin (1.5 px line / 16 px gap = 17.5 px
        period) so glyphs land in clean white space most of the time
        and the label reads as continuous text. The colour is
        --border-faint — perceptible at a glance but doesn't compete
        with the label.
     2. A clear 2 px bottom accent rule on the th, drawn via inset
        box-shadow, that gives an at-a-glance "this column is local"
        signal even from across the table. The bottom rule is the
        primary signal; the stripe is the secondary texture cue.
   Global columns get the default header treatment (no stripe, no
   bottom rule). */
/* Local-column header treatment — single stripe layer, no extra
   bottom rule. The previous `inset 0 -2px 0 var(--border-strong)`
   shadow combined with the diagonal stripe was reading as "extra
   thickness" near the bottom of the cell, especially in narrow
   columns. Dropping the shadow leaves the stripe as the sole cue,
   which renders consistently across columns regardless of width.

   Pixel values are all integers (2 px line / 14 px period) so the
   browser doesn't sub-pixel-anti-alias the line edges differently
   from cell to cell. background-position is reset to a fixed
   origin so the diagonal pattern continues smoothly across th
   boundaries instead of restarting at each cell's top-left.

   The stripe colour bumped one step from --border to a custom
   blend that's slightly more visible at small widths but still
   reads as background texture, not foreground content. */
.studio-table thead th.studio-th-scope-local {
    background-image: repeating-linear-gradient(
        135deg,
        transparent 0,
        transparent 12px,
        var(--border) 12px,
        var(--border) 14px
    );
}
.studio-table thead th.studio-th-scope-local.studio-th-selected {
    background-image: repeating-linear-gradient(
        135deg,
        transparent 0,
        transparent 12px,
        var(--accent-ring) 12px,
        var(--accent-ring) 14px
    );
}
/* Hover on a local th (NOT selected) — keep the same stripe pitch
   so there's no perceived thickness change between rest and hover. */
.studio-table thead th.studio-th-scope-local:hover:not(.studio-th-selected) {
    background-image: repeating-linear-gradient(
        135deg,
        transparent 0,
        transparent 12px,
        var(--border) 12px,
        var(--border) 14px
    );
}
/* Selected + hover lock — re-asserts the selected stripe so hovering
   a selected local header doesn't fall through to the unselected
   hover rule above. Specificity 0,3,3 beats 0,2,2. */
.studio-table thead th.studio-th-scope-local.studio-th-selected:hover {
    background-image: repeating-linear-gradient(
        135deg,
        transparent 0,
        transparent 12px,
        var(--accent-ring) 12px,
        var(--accent-ring) 14px
    );
}
/* Function columns (.studio-th-scope-fn) get NO local diagonal stripe and NO
   bottom underline — they're distinguished solely by a small, subtle `fx`
   mark before the label (no background, slightly raised like a superscript).
   The column NAME stays centered via an invisible mirror mark on the right
   (see .studio-th-fn-mark-spacer). */
/* A compact, inline mathematical `fx` mark before the name — italic serif,
   the `f` slightly taller than the `x`/name (its natural ascender does the
   work). Baseline-aligned and vertically centered with the name (NOT raised
   into the corner), no background, no underline. The right-side spacer (same
   markup, hidden) keeps the column NAME centered. */
.studio-th-fn-mark {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: baseline;
    font-family: Georgia, 'Times New Roman', 'Times', serif;
    font-style: italic;
    font-size: 0.8rem;
    line-height: 1;
    letter-spacing: -0.01em;
    color: var(--text-muted);
}
.studio-th-fn-f { font-size: 1.2em; }   /* the f rises a touch above the x / name */
.studio-th-fn-mark-spacer { visibility: hidden; }
/* Sorted local column — the sort indicator and the local bottom
   accent both want the bottom edge. Stack them: the local accent
   sits at -2 px, the sort indicator at the very bottom (-2 px
   --accent) wins via the .studio-th-sorted::after rule that uses
   absolute positioning. No CSS conflict — the ::after is on top
   of the box-shadow. */
/* The grip and menu strips inside a local th paint solid fills via
   their own backgrounds; they should NOT carry the stripe pattern
   (it would clash with the strip's own visual identity). The
   strips use opaque background colours, so the th's background
   image is naturally hidden under them — no extra rule needed. */

/* ── Column-source mode tabs (AddColumnModal) ────────────────────
   Two-tab strip at the top of the modal body when the project has
   existing global columns to pick from. */
.studio-modal-tabs {
    display: flex;
    gap: 4px;
    margin-bottom: 0.875rem;
    padding: 4px;
    background: var(--surface-sunk);
    border-radius: var(--radius-md);
}
.studio-modal-tab {
    flex: 1;
    padding: 0.45rem 0.75rem;
    border: none;
    border-radius: var(--radius-sm);
    background: transparent;
    color: var(--text-muted);
    font: inherit; font-size: 0.85rem; font-weight: 600;
    cursor: pointer;
    transition: background var(--motion-fast) var(--ease),
                color var(--motion-fast) var(--ease);
}
.studio-modal-tab:hover { color: var(--text-strong); }
.studio-modal-tab.active {
    background: var(--surface);
    color: var(--accent-strong);
    box-shadow: var(--shadow-1);
}
.studio-modal-tab:focus-visible {
    outline: none;
    box-shadow: 0 0 0 3px var(--accent-ring);
}

/* ── Compact prompt modal (rename, confirm) ─────────────────── */
.studio-prompt {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    width: min(440px, 92vw);
    box-shadow: var(--shadow-modal);
    overflow: hidden;
    /* Positioning context for the optional top-right close (X) button. */
    position: relative;
}
/* Compact, focused variant for lightweight confirmations (e.g. the
   destructive "Delete Global Column" dialog). Noticeably narrower than the
   default 440px so it reads as SECONDARY to a larger parent modal (Add
   Column) instead of matching its width, with slightly tighter padding —
   the warning copy stays comfortably readable. Opt-in via the PromptModal
   `className` prop, so other confirmations are unaffected. */
.studio-prompt.studio-prompt-compact {
    width: min(380px, 92vw);
}
.studio-prompt-compact .studio-prompt-body {
    padding: 1rem 1.125rem;
}
.studio-prompt-compact .studio-prompt-title {
    font-size: 1rem;
    margin-bottom: 0.625rem;
}
.studio-prompt-compact .studio-prompt-message {
    margin-bottom: 0.875rem;
}
/* Top-right close button for studio-prompt modals that opt in (the form
   carries `studio-prompt-has-close`). Mirrors the .modal-close affordance
   used by the studio-modal-header dialogs so the X reads consistently. */
.studio-prompt-close {
    position: absolute;
    top: 10px; right: 10px;
    width: 28px; height: 28px;
    display: flex; align-items: center; justify-content: center;
    background: none; border: none; border-radius: var(--radius-sm);
    color: var(--text-muted); cursor: pointer;
    transition: background var(--motion-fast) var(--ease), color var(--motion-fast) var(--ease);
    z-index: 2;
}
.studio-prompt-close:hover { background: var(--hover-bg); color: var(--text-strong); }
/* Reserve room at the title's right edge so a long column name can't run
   under the X. Scoped to the opt-in modal so other prompts are untouched. */
.studio-prompt-has-close .studio-prompt-title { padding-right: 1.75rem; }
.studio-prompt-body {
    padding: 1.125rem 1.25rem;
}
.studio-prompt-title {
    font-size: 1.05rem; font-weight: 700;
    color: var(--text-strong);
    letter-spacing: -0.01em;
    /* Breathing room between the title and the first field so the form
       doesn't read as crammed against the top of the modal. */
    margin-bottom: 0.875rem;
}
.studio-prompt-message {
    font-size: 0.9rem; color: var(--text-muted);
    line-height: 1.5;
    margin-bottom: 1rem;
}
.studio-prompt-input {
    width: 100%;
    box-sizing: border-box;
    /* Compact single-line height; matches the modal dropdown so the
       Name input and Category/Location dropdowns line up in the same
       form. (Textareas override min-height inline for more room.) */
    min-height: 36px;
    padding: 0.45rem 0.75rem;
    background: var(--surface);
    border: 1.5px solid var(--border-strong);
    border-radius: var(--radius-md);
    color: var(--text-strong);
    font: inherit; font-size: 0.95rem;
    transition: border-color var(--motion-fast) var(--ease),
                box-shadow var(--motion-fast) var(--ease);
}
.studio-prompt-input:focus {
    outline: none;
    border-color: var(--accent);
}
.studio-prompt-input.is-invalid,
.studio-prompt-input.is-invalid:focus {
    border-color: var(--danger);
    box-shadow: 0 0 0 3px var(--danger-ring);
}
.studio-prompt-error {
    /* Inline validation message — rendered just under the input so
       the user sees exactly what's wrong without dismissing the modal. */
    margin-top: 0.5rem;
    font-size: 0.78rem;
    color: var(--danger-strong);
    line-height: 1.35;
}
.studio-prompt-footer {
    display: flex; justify-content: flex-end;
    gap: 0.5rem;
    padding: 0.625rem 1rem;
    border-top: 1px solid var(--border-faint);
    background: var(--surface-soft);
}
/* Live import-progress label that sits in the modal footer. Stays
   left of the Cancel button via `margin-right: auto` so the action
   buttons keep their right-edge anchoring. The dots animation gives
   a subtle "still working" cue when the label text doesn't change
   for a few seconds (e.g. while the server is canonicalising
   thousands of SMILES). */
.studio-import-progress {
    margin-right: auto;
    font-size: 0.82rem;
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
    /* Truncate gracefully when the modal is narrow. */
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    max-width: 60%;
}

/* CSV import summary — laid out as a compact two-column grid so
   every count lines up under one another and the user can scan the
   list. Accents (good = green, warn = amber) call out the "what
   skipped and why" rows that used to be a single comma-joined
   sentence. */
.studio-import-summary-grid {
    margin-top: 0.625rem;
    display: flex; flex-direction: column;
    gap: 0.35rem;
    padding: 0.5rem 0.625rem;
    background: var(--surface-soft);
    border: 1px solid var(--border-faint);
    border-radius: var(--radius-md);
    font-size: 0.85rem;
}
.studio-import-summary-row {
    display: flex; align-items: center; justify-content: space-between;
    gap: 0.75rem;
}
.studio-import-summary-label {
    color: var(--text-muted);
    flex: 1; min-width: 0;
}
.studio-import-summary-value {
    color: var(--text-strong);
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
.studio-import-summary-row.is-good .studio-import-summary-value { color: var(--success-strong, #15803d); }
.studio-import-summary-row.is-warn .studio-import-summary-value { color: var(--warning-strong, #b45309); }
.studio-prompt-hint {
    margin-top: 0.5rem;
    padding: 0.5rem 0.625rem;
    border-radius: var(--radius-md);
    /* Softened from the previous saturated warning wash: a neutral
       surface-soft background carries the supplemental note without
       upstaging the more important counts above. The amber accent
       lives only on the row-level `is-warn` value text in the grid,
       so the dominant colour cue is still where it belongs. */
    background: var(--surface-soft);
    color: var(--text-muted);
    font-size: 0.8rem;
    line-height: 1.4;
    border: 1px solid var(--border-faint);
}
/* Compact action buttons inside studio modals (Add Compounds, Add Column,
   confirm prompts) — keep the visual rhythm tighter than the page-level btn. */
/* Compact, consistent modal-footer buttons (Edit Project / Edit Sheet /
   Details / History / Create / selector footers). Fixed height keeps
   Cancel + Save (and View history / Close) visually balanced and stops
   them feeling bulky against the small modal body. Scoped to modal
   footers only — main-workflow buttons elsewhere are untouched. */
.studio-prompt-footer .btn:not(.btn-icon),
.studio-modal-footer .btn:not(.btn-icon),
.studio-import-modal .studio-modal-footer .btn:not(.btn-icon) {
    height: 32px;
    padding: 0 0.85rem;
    font-size: 0.84rem;
}

/* ── Lock & Permissions modal — one consistent compact control size
   for the Lock/Unlock button, the editor dropdown, the Grant/Revoke
   buttons and the footer Close. The base modal styles make the
   dropdown (40px) noticeably taller than .btn-small (~30px); this
   block pins them all to the same height / radius / font so the rows
   read as a tidy aligned group. Scoped to .studio-lock-modal only —
   every other modal keeps its existing sizing. */
.studio-lock-modal .studio-prompt-footer { padding: 0.5rem 1rem; }
.studio-lock-modal .studio-prompt-footer .btn,
.studio-lock-modal .btn.btn-small {
    padding: 0 0.85rem;
    height: 30px;
    font-size: 0.8125rem;
    border-radius: var(--radius-sm);
}
.studio-lock-modal .studio-dropdown {
    min-height: 30px;
    height: 30px;
    padding: 0 28px 0 0.625rem;
    font-size: 0.8125rem;
    border-width: 1.5px;
    border-radius: var(--radius-sm);
}
.studio-lock-modal .studio-dropdown-chevron svg { width: 13px; height: 13px; }
/* Status text ↔ Lock/Unlock button row: text left, button right,
   both centred on the same line; the button never stretches. */
.studio-lock-modal .studio-lock-statusrow {
    display: flex; align-items: center; justify-content: space-between;
    gap: 0.75rem;
}
.studio-lock-modal .studio-lock-statusrow .studio-prompt-message {
    margin: 0; flex: 1;
}
.studio-lock-modal .studio-lock-statusrow .btn { flex-shrink: 0; white-space: nowrap; }

/* ── Toast (placeholder action feedback) ─────────────────────── */
/* Vertical-only intro (preserves the X centering) so the toast doesn't
   appear to drift sideways on each show. */
@keyframes studioToastIn {
    from { opacity: 0; transform: translate(-50%, 8px); }
    to   { opacity: 1; transform: translate(-50%, 0); }
}
.studio-toast {
    position: fixed;
    bottom: 1.25rem; left: 50%;
    transform: translate(-50%, 0);
    /* Neutral themed base (NOT the old black/white system toast). Every
       variant below sets its own petrol/soft palette; this is just the
       safe fallback so an un-typed toast still matches the platform. */
    background: var(--surface);
    color: var(--text-strong);
    border: 1px solid var(--border);
    padding: 0.55rem 0.95rem;
    border-radius: var(--radius-md);
    font-size: 0.85rem; font-weight: 500;
    box-shadow: var(--shadow-3);
    z-index: 1100;
    animation: studioToastIn 0.18s var(--ease);
    will-change: transform, opacity;
    max-width: min(640px, 90vw);
    text-align: center;
    line-height: 1.4;
}
/* Info — the default kind. Petrol/teal wash so routine confirmations
   read as on-brand, never as a generic OS toast. */
.studio-toast-info {
    background: var(--info-bg);
    color: var(--info-fg);
    border: 1px solid var(--info-border);
}
/* Toast variants — error stays visually distinct from routine info /
   success messages so import failures don't blend in with the calm
   default tone. Previous design carried a 3 px colored left rule
   that read as too-loud against the otherwise calm Studio palette
   (especially for the import-finished-OK toast, which is the most
   common variant); the variant color now lives on the border + tint
   only. */
.studio-toast-error {
    background: var(--danger-soft);
    color: var(--danger-strong);
    border: 1px solid var(--danger-border);
    /* Slightly stronger shadow than info — errors need to feel
       weighter without being shouty. */
    box-shadow: var(--shadow-3);
}
.studio-toast-success {
    background: var(--success-soft);
    color: var(--success-strong);
    border: 1px solid var(--success-border);
}
.studio-toast-warning {
    /* Soft amber wash with dark text — paler than the previous
       saturated amber, sits comfortably alongside success and error
       in the same toast palette and matches the calmer Studio tone.
       The colored left rule was removed in favour of an inline ⚠
       icon at the start of the message, which carries the warning
       semantics without the heavier visual cue. */
    background: var(--warning-soft);
    color: var(--warning-strong);
    border: 1px solid var(--warning-border);
}
/* Inline icon prepended to the warning toast content. Sized to sit
   on the cap line of the text so it reads as part of the message. */
.studio-toast-icon {
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    margin-right: 8px;
    /* Inherit the variant's text colour so the glyph and message
       always stay in sync across success / error / warning / info. */
    color: currentColor;
}
.studio-toast-icon svg { display: block; width: 16px; height: 16px; }

/* ── Presence indicators (collaboration P5) ──────────────────────
   Compact stacked initials in the sheet status bar. Friendly labels
   only (the full name list lives in the title tooltip). Petrol theme
   to match the rest of Studio; the current user's dot is filled with
   the accent so "you" reads at a glance. */
.studio-presence {
    display: inline-flex;
    align-items: center;
    margin-right: 0.5rem;
}
.studio-presence-dot {
    width: 22px; height: 22px;
    border-radius: 50%;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: 0.7rem; font-weight: 600;
    background: var(--accent-soft);
    color: var(--accent-strong);
    border: 1.5px solid var(--surface);
    box-shadow: 0 0 0 1px var(--accent-ring);
    margin-left: -6px;
}
.studio-presence-dot:first-child { margin-left: 0; }
.studio-presence-dot.is-self {
    background: var(--accent);
    color: var(--accent-fg);
}
.studio-presence-more {
    margin-left: 4px;
    font-size: 0.72rem;
    color: var(--text-muted);
}
/* Toast root needs to be a flex row so the icon + text align centrally. */
.studio-toast { display: inline-flex; align-items: center; }

/* Refresh-Sheet icon — sits inside the status-bar Refresh button.
   The is-spinning class drives a 1s rotation while a refresh is
   in flight; the user gets a subtle "I'm working" cue without a
   full-screen overlay. The button itself flips its label between
   "Refresh Sheet" and "Refreshing…" so the loading state is
   readable for assistive tech as well. */
.studio-refresh-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    /* No margin — this button is icon-only, so any right margin would
       push the glyph left and leave a larger blank on the right than
       the left. (A label-adjacent variant would add its own gap.) */
    transform-origin: 50% 50%;
}
.studio-refresh-icon.is-spinning {
    animation: studio-refresh-spin 0.9s linear infinite;
}
@keyframes studio-refresh-spin {
    from { transform: rotate(0deg); }
    to   { transform: rotate(360deg); }
}

/* Streaming export-all progress card — sits where the toast would
   sit, but is structured (label + bar + meta + cancel) and stays
   on screen for the full duration of the download. */
.studio-export-progress {
    position: fixed;
    bottom: 1.25rem; left: 50%;
    transform: translate(-50%, 0);
    background: var(--surface);
    color: var(--text);
    border: 1px solid var(--border-strong);
    padding: 0.7rem 1rem 0.65rem;
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-3);
    z-index: 1099;
    width: min(420px, 90vw);
    animation: studioToastIn 0.18s var(--ease);
}
.studio-export-progress-row {
    display: flex; align-items: center; justify-content: space-between;
    gap: 8px;
    margin-bottom: 6px;
}
.studio-export-progress-label {
    font-size: 0.85rem; font-weight: 600; color: var(--text-strong);
}
.studio-export-progress-cancel {
    background: transparent;
    border: 1px solid var(--border);
    color: var(--text-muted);
    font: inherit; font-size: 0.75rem; font-weight: 600;
    padding: 2px 8px;
    border-radius: var(--radius-sm);
    cursor: pointer;
    transition: all var(--motion-fast) var(--ease);
}
.studio-export-progress-cancel:hover {
    color: var(--danger-strong);
    border-color: var(--danger-border);
    background: var(--danger-soft);
}
.studio-export-progress-bar {
    position: relative;
    height: 6px;
    background: var(--surface-soft);
    border-radius: 999px;
    overflow: hidden;
}
.studio-export-progress-bar-fill {
    position: absolute;
    top: 0; left: 0; bottom: 0;
    background: var(--accent);
    border-radius: 999px;
    transition: width 80ms linear;
}
.studio-export-progress-bar-fill[data-indeterminate="true"] {
    /* Slide back and forth while bytes arrive without a known total. */
    animation: studioExportIndeterminate 1.4s ease-in-out infinite;
}
@keyframes studioExportIndeterminate {
    0%   { left: -40%; }
    100% { left: 100%; }
}
.studio-export-progress-meta {
    margin-top: 5px;
    font-size: 0.72rem;
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
}

/* Briefly highlight rows that were imported as duplicates already in sheet. */
@keyframes studioRowFlash {
    0%   { background: var(--warning-bg); }
    70%  { background: var(--warning-bg); }
    100% { background: transparent; }
}
.studio-row-highlight td {
    animation: studioRowFlash 2.4s ease-out;
}

/* ── Studio responsive ──────────────────────────────────────── */
@media (max-width: 900px) {
    .studio-shell { flex-direction: column; height: auto; min-height: 100vh; }
    .studio-sidebar {
        flex: none; width: 100%;
        height: auto; border-right: none;
        border-bottom: 1px solid var(--border);
        overflow: visible;
    }
    .studio-sidebar-brand {
        /* Mobile keeps the same height token so the band stays aligned
           with the wrapped toolbar below it. */
        padding: 0 1rem;
        height: var(--topband-h);
        min-height: var(--topband-h);
    }
    .studio-sidebar-nav {
        flex: 0 0 auto;
        flex-direction: row; flex-wrap: wrap;
        padding: 0.625rem 0.75rem;
        overflow: visible;
        gap: 0.375rem;
    }
    .studio-sidebar-section {
        /* Take the full nav-row width so the action buttons WRAP within
           it. With `flex: 0 0 auto` (flex-basis:auto) the section grew to
           fit every button on one line, overflowing the band off-screen
           at ~390px (Tools / Layout / Theme became unreachable). */
        flex: 1 1 100%;
        margin-bottom: 0;
        flex-direction: row;
        flex-wrap: wrap;
        gap: 0.375rem;
    }
    .studio-sidebar-spacer { display: none; }
    .studio-action { width: auto; padding: 0.45rem 0.625rem; font-size: 0.8rem; }
    .studio-sidebar-footer {
        flex: 0 0 auto;
        flex-direction: row; flex-wrap: wrap;
        padding: 0.625rem 0.75rem;
        gap: 0.375rem;
    }
    .studio-toolbar {
        /* When the toolbar wraps onto multiple rows on narrow screens
           the fixed --topband-h would clip; let it grow vertically and
           give it just enough vertical padding for breathing room. */
        flex-wrap: wrap; gap: 0.5rem;
        height: auto;
        min-height: var(--topband-h);
        padding: 0.5rem 1rem;
    }
    .studio-toolbar-divider { display: none; }
    .studio-sheet-group { flex: 1 1 100%; flex-wrap: wrap; }
    .studio-selector { max-width: 100%; }
    .studio-selection-bar { flex-wrap: wrap; }
    .studio-table-wrap { font-size: 0.8rem; }
}

/* ── Theme picker modal ──────────────────────────────────────────
   Sidebar Action → Theme opens this modal. Theme is its OWN sidebar
   entry (sibling to Layout), not nested under Layout. The body
   carries the theme grid + a one-line hint; nothing else. */
.studio-theme-modal {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    width: min(720px, 96vw);
    max-height: 85vh;
    display: flex; flex-direction: column;
    box-shadow: var(--shadow-modal);
    overflow: hidden;
}
.studio-theme-modal-body {
    flex: 1; min-height: 0;
    overflow-y: auto;
    padding: 1rem 1.5rem 1.5rem;
}
.studio-theme-modal-hint {
    margin: 0 0 1rem;
    font-size: 0.85rem;
    color: var(--text-muted);
}

/* Theme picker grid — cards lay out in a responsive 2- or 3-column
   grid depending on the modal's available width. Each card carries
   its own preview swatch row + name + description, and the
   currently-selected card shows an accent ring + check glyph. */
.studio-theme-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
    gap: 12px;
}
.studio-theme-card {
    position: relative;
    display: flex; flex-direction: column;
    gap: 10px;
    padding: 12px;
    background: var(--surface);
    /* Bumped from `--border` to `--border-strong` to match the
       drawer-card refinement — keeps the theme picker visually
       balanced with the rest of the workspace's interactive cards
       so the edges read at a glance without going heavy. */
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-md);
    cursor: pointer;
    text-align: left;
    transition: border-color var(--motion-fast) var(--ease),
                box-shadow var(--motion-fast) var(--ease),
                background var(--motion-fast) var(--ease);
}
.studio-theme-card:hover {
    /* Hover lifts to the accent ring (vs the previous border-strong
       step) so the affordance is clearly distinct from the resting
       state now that the resting border is already at border-strong. */
    border-color: var(--accent-ring);
    background: var(--surface-soft);
    box-shadow: var(--shadow-1);
}
.studio-theme-card.is-selected {
    /* Accent ring + faint accent surface so the chosen card reads as
       a "primary" without obscuring the preview swatches it carries. */
    border-color: var(--accent);
    box-shadow: 0 0 0 2px var(--accent-ring);
    background: var(--accent-soft);
}
.studio-theme-card-check {
    position: absolute;
    top: 8px; right: 8px;
    width: 22px; height: 22px;
    display: flex; align-items: center; justify-content: center;
    background: var(--accent);
    color: var(--accent-fg);
    border-radius: 50%;
    box-shadow: 0 1px 2px rgba(15, 23, 42, 0.18);
}

/* Preview block — six swatches stacked in a horizontal row,
   styled to faithfully resemble the actual surfaces each theme
   colours: sidebar / sheet tab / column header / group band /
   selected row / accent button. The preview block itself carries
   a `data-theme="<id>"` attribute so the swatches inside can read
   the same CSS-variable overrides the studio-shell will use once
   the card is selected — i.e. each card always paints a faithful
   "what will I get?" snapshot, even when the card sits inside a
   different active theme. */
.studio-theme-preview {
    background: var(--surface-soft);
    border: 1px solid var(--border-faint);
    border-radius: var(--radius-sm);
    padding: 10px;
}
.studio-theme-preview-row {
    display: grid;
    grid-template-columns: repeat(6, 1fr);
    gap: 4px;
}
.studio-theme-swatch {
    height: 28px;
    border-radius: 4px;
    border: 1px solid var(--border-faint);
}
/* Each swatch reads the relevant token from the (possibly
   theme-scoped) variable space, so the preview reflects exactly
   what the theme produces in the live UI. */
/* Each swatch paints from a token that ACTUALLY differs across
   themes — so the six swatches together read as a faithful "what
   will I get?" snapshot. The sidebar swatch carries an accent
   left-stripe (matching the active sidebar item's left bar) so
   even on themes whose surface is left near-white the user can
   still tell the cards apart. The header swatch uses the per-theme
   --surface-soft tint we now override per theme. */
.studio-theme-swatch-sidebar {
    background: var(--surface);
    border-color: var(--border);
    box-shadow: inset 3px 0 0 var(--accent);
}
.studio-theme-swatch-tab      { background: var(--accent-soft);
                                border-color: var(--accent); }
.studio-theme-swatch-header   { background: var(--surface-soft);
                                border-color: var(--border); }
.studio-theme-swatch-group    { background: var(--group-bg);
                                border-color: var(--group-border); }
.studio-theme-swatch-selected { background: var(--row-selected-bg);
                                border-color: var(--row-selected-border); }
.studio-theme-swatch-accent   { background: var(--accent);
                                border-color: var(--accent-strong); }

.studio-theme-card-meta {
    display: flex; flex-direction: column; gap: 2px;
}
.studio-theme-card-name {
    font-size: 0.92rem;
    font-weight: 600;
    color: var(--text-strong);
}
.studio-theme-card-desc {
    font-size: 0.78rem;
    color: var(--text-muted);
    line-height: 1.35;
}

/* ── Layout picker modal ────────────────────────────────────────
   Shares the Theme modal's visual language so the workspace's
   chrome is consistent — three cards in a grid, each with a small
   preview block, label, description, and a check badge on the
   selected option. The previews are pure CSS shapes (no JS) so
   the picker stays fast and renders correctly across themes. */
.studio-layout-modal {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    width: min(760px, 96vw);
    max-height: 85vh;
    display: flex; flex-direction: column;
    box-shadow: var(--shadow-modal);
    overflow: hidden;
}
.studio-layout-modal-body {
    flex: 1; min-height: 0;
    overflow-y: auto;
    padding: 1rem 1.5rem 1.5rem;
}
.studio-layout-modal-hint {
    margin: 0 0 1rem;
    font-size: 0.85rem;
    color: var(--text-muted);
}
.studio-layout-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
    gap: 12px;
}

/* ── Custom layout builder modal (large 3-pane dashboard editor) ─────── */
.studio-custom-modal {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    width: min(1180px, 96vw);
    height: min(760px, 92vh);
    display: flex; flex-direction: column;
    box-shadow: var(--shadow-modal);
    overflow: hidden;
}
/* Body = left palette | center canvas | right inspector. Each pane scrolls
   independently; on narrow screens it collapses to a single scrolling column. */
.studio-custom-body {
    flex: 1; min-height: 0;
    display: grid;
    grid-template-columns: 240px minmax(0, 1fr) 250px;
    gap: 0;
}
.studio-custom-left, .studio-custom-right {
    min-height: 0; overflow-y: auto; padding: 1rem 1.25rem;
}
.studio-custom-left { border-right: 1px solid var(--border-faint); }
.studio-custom-right { border-left: 1px solid var(--border-faint); background: var(--surface-soft); }
.studio-custom-center { min-width: 0; min-height: 0; display: flex; flex-direction: column; padding: 1rem 1.25rem; }
@media (max-width: 900px) {
    .studio-custom-modal { height: 92vh; }
    .studio-custom-body { grid-template-columns: 1fr; overflow-y: auto; }
    .studio-custom-left, .studio-custom-right { border: none; border-bottom: 1px solid var(--border-faint); overflow: visible; }
    .studio-custom-center { overflow: visible; }
}

.studio-custom-section-label {
    font-size: 0.72rem; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase;
    color: var(--text-faint); margin: 0 0 8px;
}
.studio-custom-left .studio-custom-section-label:not(:first-child) { margin-top: 18px; }
.studio-custom-help { margin: 16px 0 0; font-size: 0.76rem; line-height: 1.45; color: var(--text-muted); }

/* Starting templates (stacked in the left pane). */
.studio-custom-presets { display: flex; flex-direction: column; gap: 6px; }
.studio-custom-preset {
    padding: 8px 12px; font-size: 0.82rem; font-weight: 500; text-align: left;
    background: var(--surface); color: var(--text); cursor: pointer;
    border: 1px solid var(--border-strong); border-radius: var(--radius-md);
    transition: border-color var(--motion-fast) var(--ease), background var(--motion-fast) var(--ease);
}
.studio-custom-preset:hover { border-color: var(--accent-ring); background: var(--surface-soft); }
.studio-custom-preset.is-selected { border-color: var(--accent); background: var(--accent-soft); color: var(--accent-strong); }

/* Module palette chips. */
.studio-custom-modules { display: flex; flex-wrap: wrap; gap: 6px; }
.studio-custom-chip {
    display: inline-flex; align-items: center; gap: 6px;
    padding: 6px 11px; font-size: 0.8rem; font-weight: 500;
    background: var(--surface); color: var(--text); cursor: pointer;
    border: 1px solid var(--border-strong); border-radius: 999px;
    transition: border-color var(--motion-fast) var(--ease), background var(--motion-fast) var(--ease);
}
.studio-custom-chip.is-on { border-color: var(--accent); background: var(--accent-soft); color: var(--accent-strong); }
.studio-custom-chip.is-locked { cursor: default; }
.studio-custom-chip.is-soon { cursor: not-allowed; opacity: 0.55; }
.studio-custom-chip-tag {
    font-size: 0.62rem; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase;
    color: var(--text-faint); background: var(--surface-sunk);
    border: 1px solid var(--border-faint); border-radius: 999px; padding: 0 6px;
}

/* Center canvas — the snap grid the modules are arranged on. */
.studio-dash-canvas {
    flex: 1; min-height: 240px;
    display: grid; gap: 6px; padding: 8px;
    background: var(--surface-sunk);
    border: 1px solid var(--border); border-radius: var(--radius-md);
    /* faint cell grid so snapping is visible */
    background-image:
        linear-gradient(to right, var(--border-faint) 1px, transparent 1px),
        linear-gradient(to bottom, var(--border-faint) 1px, transparent 1px);
    background-size: calc(100% / 12) calc(100% / 6);
}
.studio-dash-mod {
    position: relative;
    display: flex; align-items: flex-start; justify-content: flex-start;
    padding: 8px 10px;
    background: var(--surface); color: var(--text);
    border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
    box-shadow: var(--shadow-1);
    cursor: grab; user-select: none; touch-action: none;
    overflow: hidden;
}
.studio-dash-mod:active { cursor: grabbing; }
.studio-dash-mod.is-grid { background: var(--accent-soft); border-color: var(--accent-ring); }
.studio-dash-mod.is-selected { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-ring); }
.studio-dash-mod-label { font-size: 0.8rem; font-weight: 600; color: var(--text-strong); display: flex; align-items: center; gap: 6px; }
.studio-dash-mod-req {
    font-size: 0.6rem; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase;
    color: var(--text-faint); background: var(--surface-sunk);
    border: 1px solid var(--border-faint); border-radius: 999px; padding: 0 5px;
}
.studio-dash-mod-resize {
    position: absolute; right: 2px; bottom: 2px; width: 16px; height: 16px;
    cursor: nwse-resize; touch-action: none;
    background:
        linear-gradient(135deg, transparent 50%, var(--border-strong) 50%, var(--border-strong) 62%, transparent 62%, transparent 74%, var(--border-strong) 74%, var(--border-strong) 86%, transparent 86%);
    border-radius: 0 0 var(--radius-sm) 0;
}
.studio-custom-errors {
    margin-top: 10px; padding: 7px 12px; font-size: 0.8rem;
    color: var(--danger-strong); background: var(--danger-bg);
    border: 1px solid var(--danger-bg); border-radius: var(--radius-md);
}

/* Right inspector. */
.studio-custom-insp-empty { font-size: 0.82rem; color: var(--text-muted); }
.studio-custom-insp-name { font-size: 0.9rem; font-weight: 600; color: var(--text-strong); }
.studio-custom-insp-desc { font-size: 0.76rem; line-height: 1.4; color: var(--text-muted); margin: 4px 0 14px; }
.studio-custom-insp-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.studio-custom-insp-field { display: flex; flex-direction: column; gap: 4px; }
.studio-custom-insp-flabel { font-size: 0.72rem; color: var(--text-muted); }
.studio-custom-stepper {
    display: inline-flex; align-items: center; border: 1px solid var(--border-strong);
    border-radius: var(--radius-md); overflow: hidden;
}
.studio-custom-stepper button {
    width: 28px; height: 28px; border: none; background: var(--surface); color: var(--text);
    cursor: pointer; font-size: 1rem; line-height: 1;
}
.studio-custom-stepper button:hover { background: var(--surface-soft); }
.studio-custom-stepper-val { flex: 1; text-align: center; font-size: 0.84rem; font-variant-numeric: tabular-nums; }
.studio-custom-insp-remove { margin-top: 16px; width: 100%; }

.studio-custom-footer {
    display: flex; align-items: center; justify-content: space-between;
    padding: 0.85rem 1.5rem; border-top: 1px solid var(--border-faint); background: var(--surface-soft);
}
.studio-custom-footer-right { display: flex; gap: 8px; }

/* Dashboard render — the saved custom layout in the workspace. The panel is a
   cols×rows CSS grid; .studio-analysis-main / .studio-analysis-side get inline
   grid-area from their module rects (set in AlmaStudioPage). */
.studio-sheet-panel-custom.is-dashboard { display: grid; gap: 12px; align-items: stretch; }
.studio-layout-card {
    position: relative;
    display: flex; flex-direction: column;
    gap: 10px;
    padding: 12px;
    background: var(--surface);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-md);
    cursor: pointer;
    text-align: left;
    transition: border-color var(--motion-fast) var(--ease),
                box-shadow var(--motion-fast) var(--ease),
                background var(--motion-fast) var(--ease);
}
.studio-layout-card:hover {
    border-color: var(--accent-ring);
    background: var(--surface-soft);
    box-shadow: var(--shadow-1);
}
.studio-layout-card.is-selected {
    border-color: var(--accent);
    box-shadow: 0 0 0 2px var(--accent-ring);
    background: var(--accent-soft);
}
/* Coming-soon mode: shown for roadmap context but NOT selectable. Muted +
   not-allowed cursor; the hover lift is suppressed so it doesn't look clickable. */
.studio-layout-card.is-coming-soon {
    cursor: not-allowed;
    opacity: 0.6;
}
.studio-layout-card.is-coming-soon:hover {
    border-color: var(--border-strong);
    background: var(--surface);
    box-shadow: none;
}
.studio-layout-card-soon {
    margin-left: 8px;
    padding: 1px 7px;
    font-size: 0.66rem; font-weight: 700; letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--text-muted);
    background: var(--surface-sunk);
    border: 1px solid var(--border-strong);
    border-radius: 999px;
    vertical-align: middle;
}
.studio-layout-card-check {
    position: absolute;
    top: 8px; right: 8px;
    width: 22px; height: 22px;
    display: flex; align-items: center; justify-content: center;
    background: var(--accent);
    color: var(--accent-fg);
    border-radius: 50%;
    box-shadow: 0 1px 2px rgba(15, 23, 42, 0.18);
}
.studio-layout-card-meta {
    display: flex; flex-direction: column; gap: 2px;
}
.studio-layout-card-name {
    font-size: 0.92rem;
    font-weight: 600;
    color: var(--text-strong);
}
.studio-layout-card-desc {
    font-size: 0.78rem;
    color: var(--text-muted);
    line-height: 1.35;
}

/* Layout preview tile — fixed aspect-ratio block whose internal
   shapes mimic each mode's structure. Pure CSS, theme-aware. */
.studio-layout-preview {
    width: 100%;
    aspect-ratio: 5 / 3;
    background: var(--surface-soft);
    border: 1px solid var(--border-faint);
    border-radius: var(--radius-sm);
    padding: 8px;
    display: flex;
    overflow: hidden;
}
.studio-layout-preview-grid {
    flex: 1;
    display: grid;
    grid-template-rows: 12px repeat(4, 1fr);
    gap: 4px;
}
.studio-layout-preview-header {
    background: var(--accent-soft);
    border: 1px solid var(--accent);
    border-radius: 3px;
    height: 100%;
}
.studio-layout-preview-row {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 3px;
    height: 100%;
}
.studio-layout-preview-split {
    flex: 1;
    display: grid;
    grid-template-columns: 2fr 1fr;
    gap: 6px;
}
.studio-layout-preview-split-grid {
    display: grid;
    grid-template-rows: 12px repeat(3, 1fr);
    gap: 4px;
}
.studio-layout-preview-split-side {
    display: grid;
    grid-template-rows: 1fr 1fr;
    gap: 6px;
}
.studio-layout-preview-side-3d {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 3px;
    display: flex; align-items: center; justify-content: center;
}
.studio-layout-preview-mol {
    width: 60%;
    aspect-ratio: 1 / 1;
    border: 1.5px solid var(--accent);
    border-radius: 50%;
    background: radial-gradient(circle at 35% 35%, var(--accent-soft) 0%, transparent 70%);
}
.studio-layout-preview-side-metrics {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 3px;
    padding: 4px;
    display: flex; flex-direction: column; gap: 3px;
    justify-content: center;
}
.studio-layout-preview-bar {
    height: 5px;
    border-radius: 2px;
    background: var(--accent);
    opacity: 0.6;
}
.studio-layout-preview-bar-short {
    width: 60%;
}
.studio-layout-preview-custom {
    flex: 1;
    display: grid;
    grid-template-columns: 1fr 1fr;
    grid-template-rows: 1fr 1fr;
    gap: 4px;
}
.studio-layout-preview-tile {
    background: var(--surface);
    border: 1px dashed var(--border-strong);
    border-radius: 3px;
}
.studio-layout-preview-tile-tall {
    grid-row: span 2;
    background: var(--accent-soft);
    border-style: solid;
    border-color: var(--accent);
}

/* ── Analysis layout — sheet panel split into grid + right rail ──
   `.studio-sheet-panel-analysis` is a CSS-grid container with two
   columns (grid 2fr, side 1fr). The grid + status bar live inside
   `.studio-analysis-main`; the side rail stacks two cards.
   `.studio-sheet-panel-default` and `.studio-sheet-panel-custom`
   leave `.studio-analysis-main` as a transparent passthrough so the
   default and custom branches keep the existing visual behaviour. */
.studio-sheet-panel {
    /* `.studio-analysis-main` is always present now — make sure the
       panel still hosts its child flex column when in default mode.
       The default panel was already display:flex / flex-direction:column
       in the upstream rule; that rule keeps applying. The wrapper
       below just passes through. */
}
.studio-analysis-main {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    min-width: 0;
    min-height: 0;
}
.studio-sheet-panel-analysis {
    /* Analysis rail width — narrower than before so the sheet/grid gets more
       room. `clamp(min, ideal, max)` keeps it responsive: it never drops below
       280px (controls stay usable) and never balloons past 420px on wide
       screens (the sheet, minmax(0,1fr), reclaims all extra width). Tune here. */
    --studio-analysis-w: clamp(460px, 40%, 680px);
    display: grid;
    grid-template-columns: minmax(0, 1fr) var(--studio-analysis-w);
    gap: 12px;
    align-items: stretch;
}
.studio-analysis-side {
    display: grid;
    grid-template-rows: minmax(0, 1fr) minmax(0, 1fr);
    gap: 12px;
    min-width: 0;
    min-height: 0;
}
.studio-analysis-panel {
    display: flex;
    flex-direction: column;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-1);
    min-height: 0;
    overflow: hidden;
}
.studio-analysis-panel-head {
    padding: 10px 14px;
    border-bottom: 1px solid var(--border-faint);
    background: var(--surface-soft);
    display: flex;
    flex-direction: column;
    gap: 2px;
}
.studio-analysis-panel-title {
    font-size: 0.92rem;
    font-weight: 600;
    color: var(--text-strong);
}
.studio-analysis-panel-sub {
    font-size: 0.76rem;
    color: var(--text-muted);
}
.studio-analysis-panel-body {
    flex: 1 1 auto;
    min-height: 0;
    overflow: auto;
    padding: 14px;
    display: flex;
    align-items: center;
    justify-content: center;
}
.studio-analysis-placeholder {
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    gap: 8px;
    color: var(--text-muted);
    max-width: 280px;
}
.studio-analysis-placeholder-icon {
    width: 40px;
    height: 40px;
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--accent);
    background: var(--accent-soft);
    border-radius: 50%;
}
.studio-analysis-placeholder-title {
    font-size: 0.9rem;
    font-weight: 600;
    color: var(--text-strong);
}
.studio-analysis-placeholder-sub {
    font-size: 0.8rem;
    line-height: 1.4;
    color: var(--text-muted);
}

/* ════════════════════════════════════════════════════════════════
   Phase 2 — Visualization workspace (scatter plots in the analysis rail).
   The workspace replaces the two stacked placeholder panels, so it spans
   the full height of the analysis grid rail.
   ════════════════════════════════════════════════════════════════ */
.studio-analysis-side > .viz-workspace { grid-row: 1 / -1; }
.viz-workspace {
    /* Visualization-only border, one step stronger than --border-strong, so
       the plot frame and analysis cards read as clearly-framed panels without
       heavy dark lines. Scoped here and inherited by all viz descendants.
       Safe as a literal: the Studio themes override accent/surface tokens but
       NOT the --border-* palette, so every theme shares this light gray. */
    --viz-border: #b6c0ce;
    display: flex;
    flex-direction: column;
    min-height: 0;
    min-width: 0;
    gap: 8px;
    background: var(--surface);
    border: 1px solid var(--viz-border, var(--border-strong));
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-1);
    /* Scrollable rail: Plot Settings opens IN-FLOW above the chart and the
       chart has a protected fixed height (see .viz-split-top), so on a short
       rail the whole rail scrolls rather than the chart being squeezed. On a
       normal rail nothing overflows (the summary absorbs the change). */
    overflow-y: auto;
    overflow-x: hidden;
}

/* Plot tabs — sticky header.
   The workspace (.viz-workspace) is the scroll container: opening Plot
   Settings / Chemical Plot Settings can make the rail overflow. Pinning
   this strip to the top keeps the tab switcher + settings/add/delete
   controls reachable while the settings panel and analysis content scroll
   underneath it. top:0 sits just inside the workspace's 1px border; the
   opaque surface-soft background + z-index keep scrolling content from
   showing through or painting over the controls. */
.viz-tabs {
    position: sticky;
    top: 0;
    z-index: 3;
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 8px 10px;
    /* Divider between the plot-tab strip and the plot area. Uses the
       workspace framing gray (one step stronger than --border) so the tab
       row reads as an intentional header band, not a faint hairline —
       without the heaviness of a dark rule. */
    border-bottom: 1px solid var(--viz-border, var(--border-strong));
    background: var(--surface-soft);
}
.viz-tabs-list {
    display: flex;
    align-items: center;
    gap: 4px;
    flex: 1 1 auto;
    min-width: 0;
    align-self: stretch;
    overflow-x: auto;
    overflow-y: hidden;
    /* Hide the native horizontal scrollbar so it can NEVER consume vertical space
       inside the band and push the tabs out of vertical alignment with the
       right-side action buttons. Overflow is instead navigated with the chevron
       buttons below (+ wheel / trackpad / keyboard); the active tab auto-reveals.
       Scoped to this element only; no global scrollbar CSS. Matches the sheet-tab
       strip (.studio-tabs-strip), which hides its scrollbar the same way. */
    scrollbar-width: none;
    scroll-behavior: smooth;
}
.viz-tabs-list::-webkit-scrollbar { height: 0; width: 0; display: none; }
/* Left / right scroll chevrons — same affordance + styling as the sheet-tab
   strip's .studio-tab-scroll. Rendered only when the strip overflows; each is
   disabled at its scroll boundary. Centred in the band, so they stay aligned
   with the tabs + the action buttons. */
.viz-tab-scroll {
    flex: 0 0 auto;
    width: 24px;
    height: 26px;
    border: none;
    border-radius: var(--radius-sm);
    background: transparent;
    color: var(--text-muted);
    display: flex; align-items: center; justify-content: center;
    cursor: pointer;
    transition: background var(--motion-fast) var(--ease), color var(--motion-fast) var(--ease);
}
.viz-tab-scroll:hover:not(:disabled) { background: var(--surface); color: var(--accent); }
.viz-tab-scroll:disabled { opacity: 0.3; cursor: not-allowed; }
.viz-tab {
    flex: 0 0 auto;
    padding: 4px 10px;
    font-size: 0.8rem;
    font-weight: 500;
    color: var(--text-muted);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    cursor: pointer;
    white-space: nowrap;
    transition: background var(--motion-fast) var(--ease), color var(--motion-fast) var(--ease), border-color var(--motion-fast) var(--ease);
}
.viz-tab:hover { color: var(--text); border-color: var(--border-strong); }
.viz-tab.is-active { color: #fff; background: var(--accent); border-color: var(--accent); }
.viz-tab-rename {
    flex: 0 0 auto;
    width: 124px;
    padding: 3px 8px;
    font-size: 0.8rem;
    /* Subtle resting border (was a heavy solid-accent line); the global input
       focus rule supplies the accent border on focus, with no halo. */
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    background: var(--surface);
    color: var(--text);
}
.viz-tabs-actions { display: flex; align-items: center; gap: 4px; flex: 0 0 auto; }
/* The hide control closes the whole rail — set it apart from the
   per-plot actions with extra space so it isn't mistaken for delete. */
.viz-tabs-actions .viz-hide-btn { margin-left: 8px; }
/* Add-plot type menu (portaled below the + button) — mirrors the Tools-drawer
   "Add visualization" menu styling. */
/* z-index matches the Tools-drawer add menu (.studio-tool-viz-addmenu): the
   analysis rail's Plot Settings panel sits in a high stacking context, so a
   low z would paint the portaled menu UNDER the settings panel (unclickable).
   Position (fixed top/right) is still set inline from the trigger button. */
.viz-add-menu { display: flex; flex-direction: column; min-width: 168px; padding: 3px; z-index: 12000;
    background: var(--surface); border: 1px solid var(--border-strong); border-radius: var(--radius-md); box-shadow: var(--shadow-2); }
.viz-add-menu-item { display: flex; align-items: center; gap: 8px; padding: 7px 8px; font-size: 0.82rem; font-weight: 500;
    text-align: left; background: transparent; border: none; border-radius: var(--radius-sm); color: var(--text); cursor: pointer; white-space: nowrap; }
.viz-add-menu-item:hover { background: var(--accent-soft); color: var(--accent-strong); }
.viz-add-menu-glyph { flex: 0 0 auto; color: var(--text-muted); }
.viz-add-menu-item:hover .viz-add-menu-glyph { color: var(--accent-strong); }

/* Missing-column warning */
.viz-warn {
    margin: 0 10px;
    padding: 6px 10px;
    font-size: 0.74rem;
    color: var(--text-strong);
    background: var(--surface-sunk);
    border: 1px solid var(--border-strong);
    border-left: 3px solid var(--accent);
    border-radius: var(--radius-sm);
}

/* Settings — collapsed by default; opened by the gear in the toolbar. */
.viz-settings {
    /* Shared control height for every single-line control in the panel
       (inputs, dropdowns, swatches, reset). Combined with box-sizing:
       border-box this guarantees identical RENDERED height across controls
       regardless of differing border widths (1px vs 1.5px). */
    --viz-ctrl-h: 30px;
    /* IN-FLOW panel above the chart (between the toolbar and the chart). Opening
       it pushes the chart DOWN naturally — but the chart has a protected fixed
       height (.viz-split-top), so the chart is NOT compressed; the lower summary
       absorbs the change (shrinks/scrolls), and the whole rail scrolls if space
       runs out. Capped + internal scroll so a fully-expanded panel can't grow
       without bound. Not an overlay — it never covers the chart. */
    flex: 0 0 auto;
    max-height: 320px;
    overflow-y: auto;
    overflow-x: hidden;
    margin: 0 10px;
    padding: 6px 8px 8px;
    background: var(--surface-sunk);
    border: 1px solid var(--viz-border, var(--border));
    border-radius: var(--radius-sm);
    display: flex;
    flex-direction: column;
    gap: 2px;
    /* Labels, toggle rows and compact controls are not meant to be
       selected; clicking a checkbox used to drag-select adjacent label
       text. Disable selection panel-wide, then restore it for real text
       fields so values stay selectable/editable. Scoped to this panel —
       grid cells and other inputs are unaffected. */
    user-select: none;
    -webkit-user-select: none;
}
.viz-settings input,
.viz-settings textarea {
    user-select: text;
    -webkit-user-select: text;
}
/* Collapsible subsections (Axes / Encoding / Guides / Analysis) */
.viz-set-sec + .viz-set-sec { border-top: 1px solid var(--border-faint); }
.viz-set-sec-head {
    display: flex;
    align-items: center;
    gap: 6px;
    width: 100%;
    padding: 5px 2px;
    background: transparent;
    border: none;
    cursor: pointer;
    font-size: 0.7rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--accent-strong);
    text-align: left;
}
.viz-set-chevron { flex: 0 0 auto; color: var(--text-muted); transition: transform var(--motion-fast) var(--ease); }
.viz-set-sec.is-open .viz-set-chevron { transform: rotate(0deg); }
.viz-set-sec:not(.is-open) .viz-set-chevron { transform: rotate(-90deg); }
.viz-set-sec-body { display: flex; flex-direction: column; gap: 7px; padding: 2px 2px 8px; }
.viz-settings-grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 7px 8px;
}
.viz-field { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
/* Secondary label — matches Alma Studio's form-label scale (0.72rem / 600 /
   muted, e.g. .studio-colfmt-title). Single line + ellipsis so a label can
   never wrap or push its control out of the panel. */
.viz-field-label {
    font-size: 0.72rem;
    font-weight: 600;
    line-height: 1.3;
    color: var(--text-muted);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.viz-toggle { display: inline-flex; align-items: center; gap: 5px; font-size: 0.78rem; color: var(--text); cursor: pointer; }
.viz-toggle input { cursor: pointer; margin: 0; }
/* Chemical Space settings: space the grid blocks like the rows WITHIN a grid
   (.viz-settings-grid row-gap is 7px) so every setting row shares one vertical
   rhythm — the panel's default 2px child gap was too tight between blocks. */
.studio-cs-settings { gap: 7px; }
/* "Cluster regions" toggle switch — a compact slider that reads as part of the
   settings UI (vs a bare checkbox). Sits in a control-height cell so it
   bottom-aligns with the "View" dropdown beside it. */
.viz-switch-cell { display: flex; align-items: center; min-height: var(--viz-ctrl-h, 30px); }
.viz-switch { position: relative; display: inline-block; width: 32px; height: 18px; flex: 0 0 auto; cursor: pointer; }
.viz-switch input { position: absolute; width: 0; height: 0; opacity: 0; margin: 0; }
/* Restrained, rectangular toggle (small radius) — reads as a data-tool
   control, not a fully-round "pill". */
.viz-switch-slider { position: absolute; inset: 0; border-radius: 4px; background: var(--border-strong); transition: background var(--motion-fast) var(--ease); }
.viz-switch-slider::before {
    content: ''; position: absolute; left: 2px; top: 2px; width: 14px; height: 14px;
    border-radius: 3px; background: #fff; box-shadow: 0 1px 2px rgba(15, 30, 45, 0.25);
    transition: transform var(--motion-fast) var(--ease);
}
.viz-switch input:checked + .viz-switch-slider { background: var(--accent); }
.viz-switch input:checked + .viz-switch-slider::before { transform: translateX(14px); }
.viz-switch input:checked:disabled + .viz-switch-slider { background: var(--border-strong); }
.viz-switch input:focus-visible + .viz-switch-slider { box-shadow: 0 0 0 3px var(--accent-ring); }
.viz-toggle.is-disabled { opacity: 0.45; cursor: not-allowed; }
.viz-toggle.is-disabled input { cursor: not-allowed; }
/* Settings header — title + reset icon (reset no longer takes its own row) */
.viz-settings-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 0 2px 2px; }
.viz-settings-head-title { font-size: 0.66rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-faint); }
.viz-settings-reset-btn {
    display: inline-flex; align-items: center; justify-content: center;
    width: 22px; height: 22px; flex: 0 0 auto;
    color: var(--text-muted); background: transparent; border: none; border-radius: var(--radius-sm); cursor: pointer;
}
.viz-settings-reset-btn:hover { background: var(--surface); color: var(--accent-strong); }
/* Axes — full-width Scale row beneath the X/Y selectors (avoids the
   awkward 3-column alignment); then the min/max + reset row. */
.viz-scale-row { display: flex; align-items: center; gap: 12px; }
.viz-scale-row > .viz-field-label { margin-right: 2px; }
.viz-axes-ranges { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)) auto; gap: 6px 8px; align-items: end; }
/* Reset icon — square button, exactly the shared control height and 1.5px
   border (matching the inputs) so border-box yields the same rendered
   height; bottom-aligned with the inputs via the grid's align-items:end. */
.viz-reset-icon {
    display: inline-flex; align-items: center; justify-content: center;
    box-sizing: border-box;
    width: var(--viz-ctrl-h); height: var(--viz-ctrl-h); flex: 0 0 auto;
    color: var(--text-muted); background: var(--surface);
    border: 1.5px solid var(--border-strong); border-radius: var(--radius-sm);
    cursor: pointer;
    transition: border-color var(--motion-fast) var(--ease), color var(--motion-fast) var(--ease);
}
.viz-reset-icon:hover { border-color: var(--accent); color: var(--accent-strong); }
/* Encoding — every row is the same 2-col grid as .viz-settings-grid (same
   track width + column gap), so Color by, Size by and Dot color all share
   one column-1 width and align with the Axes "X column". align-items:start
   keeps each pair's labels on one top baseline. */
.viz-enc-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 7px 8px; align-items: start; }
/* Dot styling cluster — Dot size · Dot color · Opacity share column 2 of
   the Size by row on ONE line, and wrap cleanly (whole controls, no
   overlap) when column 2 gets too narrow. */
.viz-enc-dots { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 7px 8px; min-width: 0; }
.viz-enc-dots > .viz-field:nth-child(1) { flex: 1 1 48px; min-width: 0; }   /* Dot size */
.viz-enc-dots > .viz-field:nth-child(2) { flex: 0 0 auto; }                 /* Dot color — hugs label */
.viz-enc-dots > .viz-field:nth-child(3) { flex: 1 1 52px; min-width: 0; }   /* Opacity */
/* Dot-color swatch is a TRUE SQUARE (control-height square), centered under
   its label so there's no lopsided dead space beside it. Only the square
   (the button) is clickable — the field is a <div>, so the label and the
   centering margins never open the picker. */
.viz-enc-dots > .viz-field > .viz-color-swatch { width: var(--viz-ctrl-h); height: var(--viz-ctrl-h); align-self: center; }
/* Decision-guides direction + value, two aligned columns */
.viz-crit-row { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 6px 8px; }
/* ── Two-zone setting row (Decision guides + Analysis) ──────────────
   The row is the SAME 2-col grid as the criteria rows; the interactive
   content lives in a single zone that spans grid-column 1 ONLY, so the
   zone's right edge is exactly the Y-direction input's right edge above.
   Inside the zone: the toggle sits at the left, and its dependent control
   is pushed to the zone's right edge (margin-left:auto) — giving the
   requested RIGHT-edge alignment for Offset / LipE / Show equation /
   Band ± while keeping each control on its toggle's row. min-height pins
   the row to the standard control height, so toggling a guide/fit option
   on or off never changes the row height. Wraps cleanly when too narrow. */
.viz-pair-row { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 6px 8px; align-items: center; min-height: var(--viz-ctrl-h); }
.viz-pair-zone { grid-column: 1; display: flex; align-items: center; flex-wrap: wrap; gap: 4px 8px; min-width: 0; }
/* A zone with no right-aligned dependent control (e.g. the two "passing"
   toggles) spans the full row so both toggles fit on one line, wrapping
   cleanly only when genuinely too narrow. */
.viz-pair-zone-wrap { grid-column: 1 / -1; column-gap: 14px; }
.viz-pair-ctl { margin-left: auto; display: inline-flex; align-items: center; gap: 6px; min-width: 0; }
.viz-pair-ctl.is-disabled { opacity: 0.45; }
.viz-pair-ctl-lbl { font-size: 0.72rem; color: var(--text-muted); white-space: nowrap; }
/* All dependent inputs share ONE width + right edge (the Y-direction input's
   right edge, via the ctl's margin-left:auto). Same width ⇒ Offset, LipE and
   Band ± also share the same LEFT edge. flex-shrink lets them narrow at tight
   widths without overflowing. */
.viz-pair-ctl .viz-input { width: 80px; flex: 0 1 80px; min-width: 0; padding: 0.25rem 0.4rem; text-align: left; }
/* Auxiliary control that intentionally sits to the RIGHT of the alignment
   line (grid-column 2), e.g. LipE Auto-map — so it does NOT count toward the
   col-1 input's right-edge alignment. */
.viz-pair-aux { grid-column: 2; justify-self: start; align-self: center; }
.viz-pair-aux:disabled { opacity: 0.55; cursor: not-allowed; text-decoration: none; }
.viz-metrics-label { margin-top: 2px; }
/* Stats — compact 3-per-row grid pinned to grid-column 1 of a pair row, so
   the whole block stops at the Y-direction input's right edge (it never
   spills into column 2). Tight gaps; minmax(0,1fr) lets cells shrink, and
   at very narrow widths the toggles wrap cleanly within the half-width. */
.viz-pair-row > .viz-metrics-grid { grid-column: 1; }
.viz-metrics-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 4px; }
/* Stats chips — compact custom segmented toggles (NOT default checkboxes).
   The native checkbox is visually hidden but stays focusable for keyboard /
   a11y; the chip shows its on/off state via the Studio accent theme. Each
   chip stretches to fill its grid cell, so three equal chips read as a
   segmented row; labels ellipsize rather than overflow when very tight. */
.viz-stat-chip {
    position: relative;
    display: flex; align-items: center; justify-content: center;
    min-width: 0; padding: 3px 6px;
    font-size: 0.74rem; line-height: 1.2; color: var(--text-muted);
    background: var(--surface);
    border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
    cursor: pointer; user-select: none;
    transition: border-color var(--motion-fast) var(--ease),
                background var(--motion-fast) var(--ease),
                color var(--motion-fast) var(--ease);
}
.viz-stat-chip > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.viz-stat-chip > input {
    position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
    border: 0; opacity: 0; pointer-events: none; clip: rect(0 0 0 0); overflow: hidden;
}
.viz-stat-chip:hover { border-color: var(--accent); color: var(--text); }
.viz-stat-chip.is-on { background: var(--accent-soft); border-color: var(--accent); color: var(--accent-strong); font-weight: 600; }
.viz-stat-chip:focus-within { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-ring); }
/* Numeric gradient control: Low swatch · gradient preview · High swatch.
   Swatches are true squares; min-height matches the standard control so the
   bar is vertically centered against the Color by dropdown beside it. */
.viz-grad-ctrl { display: flex; align-items: center; gap: 6px; min-height: var(--viz-ctrl-h, 30px); }
.viz-grad-ctrl .viz-color-swatch { width: 22px; height: 22px; }
.viz-grad-ctrl-bar { flex: 1 1 auto; min-width: 24px; height: 16px; border-radius: var(--radius-sm); border: 1px solid var(--border-faint); }
/* ── Custom color picker (ColorSwatchPicker) — styled swatch + popover.
   Only the swatch is a click target. ── */
.viz-color-swatch {
    box-sizing: border-box;
    width: 30px; height: 22px; flex: 0 0 auto; padding: 0;
    border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
    cursor: pointer;
    transition: border-color var(--motion-fast) var(--ease);
}
.viz-color-swatch:hover { border-color: var(--accent); }
.viz-color-pop {
    z-index: 400;
    display: flex; flex-direction: column; gap: 7px;
    padding: 8px;
    background: var(--surface);
    border: 1px solid var(--border-strong); border-radius: var(--radius-md);
    box-shadow: var(--shadow-2, 0 6px 20px rgba(0, 0, 0, 0.16));
}
.viz-color-pop-grid { display: grid; grid-template-columns: repeat(6, 18px); gap: 5px; }
.viz-color-cell {
    width: 18px; height: 18px; padding: 0;
    border: 1px solid var(--border-faint); border-radius: 4px; cursor: pointer;
}
.viz-color-cell:hover { border-color: var(--text-faint); }
.viz-color-cell.is-on { box-shadow: 0 0 0 1.5px var(--surface), 0 0 0 2.5px var(--accent); }
.viz-color-pop-hex { display: flex; align-items: center; gap: 5px; }
.viz-color-pop-preview { width: 18px; height: 18px; flex: 0 0 auto; border: 1px solid var(--border-faint); border-radius: 4px; }
.viz-color-pop-hash { color: var(--text-faint); font-size: 0.8rem; }
.viz-color-pop-hex input {
    flex: 1 1 auto; min-width: 0; width: 72px;
    min-height: 26px; padding: 0.2rem 0.45rem;
    font-size: 0.8rem; font-variant-numeric: tabular-nums;
    border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
    background: var(--surface); color: var(--text-strong);
}
/* Inline link-style action (e.g. LipE auto-map) */
.viz-inline-btn { flex: 0 0 auto; font-size: 0.72rem; font-weight: 600; color: var(--accent-strong); background: none; border: none; padding: 0; cursor: pointer; white-space: nowrap; }
.viz-inline-btn:hover { text-decoration: underline; }
/* Compact control variants — denser than the modal defaults, with a
   consistent (smaller) radius across dropdowns / inputs so the settings
   panel reads as one aligned set rather than mixed pill / boxy controls. */
.viz-settings .studio-dropdown.viz-dropdown {
    height: var(--viz-ctrl-h);
    min-height: var(--viz-ctrl-h);
    padding: 0.25rem 0.5rem;
    font-size: 0.78rem;
    border-radius: var(--radius-sm);
}
.viz-settings .studio-prompt-input.viz-input {
    width: 100%;
    min-width: 0;
    height: var(--viz-ctrl-h);
    min-height: 0;
    padding: 0.25rem 0.5rem;
    font-size: 0.78rem;
    line-height: 1.2;
    border-radius: var(--radius-sm);
}
.viz-settings .studio-prompt-input.viz-input.is-invalid { border-color: var(--danger, #be123c); }
/* Drop number-input spin buttons inside the settings panel only — keeps the
   compact inputs (Dot size, Opacity, Band ±, axis min/max) narrow and tidy.
   Scoped to .viz-settings so no other Alma Studio input is affected. */
.viz-settings .viz-input[type="number"] { -moz-appearance: textfield; appearance: textfield; }
.viz-settings .viz-input[type="number"]::-webkit-outer-spin-button,
.viz-settings .viz-input[type="number"]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
/* Opt back IN to native up/down steppers for the Chemical Space numeric
   inputs (Cluster k, Morgan radius). These replace the old "2–50" / "1–6"
   range helper text: the arrows + min/max attributes communicate and enforce
   the range. Higher specificity (extra .viz-num-stepper class) overrides the
   spinner-hiding rule above; scoped so the scatter settings stay spinner-free. */
.viz-settings .viz-input.viz-num-stepper[type="number"] {
    -moz-appearance: number-input; appearance: auto;
    /* Right padding tuned so the native spinner lines up with the chevron of
       the sibling `.viz-dropdown` selects (whose chevron sits at their 0.5rem
       right padding). The base 0.5rem looked too far LEFT (the spinner has its
       own inset), and a flush ~0.125rem looked too far RIGHT — 0.3rem sits the
       arrows at the dropdown-arrow position without hugging the edge. Left
       padding unchanged; scoped to these two Chemical Space inputs only. */
    padding-right: 0.3rem;
}
.viz-settings .viz-input.viz-num-stepper[type="number"]::-webkit-outer-spin-button,
.viz-settings .viz-input.viz-num-stepper[type="number"]::-webkit-inner-spin-button { -webkit-appearance: auto; opacity: 1; margin: 0; }

/* Vertical split — chart (Part A) over summary (Part B) */
.viz-split {
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
    flex-direction: column;
    gap: 8px;
    padding: 0 10px 10px;
}
.viz-split-top {
    /* PROTECTED chart height — fixed (clamped), NOT a flex fraction. So opening
       Plot Settings above the split (or shrinking the summary) never compresses
       the chart: it keeps this exact height and simply shifts down. Sized to
       fill most of the rail's vertical space (less blank area at the bottom);
       tune the clamp to taste. */
    flex: 0 0 auto;
    height: clamp(420px, 56vh, 640px);
    display: flex;
    flex-direction: column;
    gap: 6px;
    /* Positioning context for the chemical-space "Recomputing…" overlay. */
    position: relative;
}
/* Non-blocking recompute badge: the chart stays mounted and interactive
   underneath (pointer-events:none) while a new projection is fetched, so
   changing settings never unmounts/re-measures the chart (no zoom jump). */
.viz-cs-recomputing {
    position: absolute; top: 8px; right: 8px; z-index: 4; pointer-events: none;
    display: inline-flex; align-items: center; gap: 6px;
    padding: 3px 9px; border-radius: var(--radius-sm);
    background: var(--surface); border: 1px solid var(--border-strong);
    box-shadow: var(--shadow-1);
    font-size: 0.72rem; color: var(--text-muted);
}
.viz-cs-recomputing svg { width: 13px; height: 13px; color: var(--accent); opacity: 0.8; }
.viz-split-bottom {
    /* The summary area (Relationship / Selected Compounds / distributions). It
       takes the remaining space and scrolls — so it ABSORBS the layout change
       when Plot Settings opens above the chart (the chart keeps its fixed
       height). */
    flex: 1 1 auto;
    min-height: 0;
    overflow: auto;
    /* Section break between the chart and the analysis summary. Uses the
       workspace framing gray (matching the chart frame + the sticky tab
       header divider) so the panel's internal hierarchy reads cleanly,
       rather than a faint hairline. */
    border-top: 1px solid var(--viz-border, var(--border-strong));
    padding-top: 8px;
}

/* Chart (Part A) */
.viz-chart {
    position: relative;
    flex: 1 1 auto;
    min-height: 0;
    background: var(--surface);
    /* Plot frame — strongest viz border so the chart reads as a clearly
       framed section, not a faint outline. */
    border: 1px solid var(--viz-border, var(--border-strong));
    border-radius: var(--radius-sm);
    overflow: hidden;
    /* Drag-to-brush must never start a native text selection of the tick /
       axis-label SVG text. Disable selection across the plot surface (the
       transient tooltip / pinned-card text live here too and aren't meant to
       be selected). */
    -webkit-user-select: none;
    user-select: none;
}
/* The SVG and the point canvas are both positioned (no explicit z-index), so
   they paint in DOM order: canvas (first) under the SVG, and the tooltip /
   pinned-card / notes overlays (rendered after the SVG) stay on top of both. */
.viz-svg { display: block; width: 100%; height: 100%; position: relative; }
/* Base point layer (Option B hybrid) — a <canvas> beneath the SVG so axes /
   gridlines / guides / regression / hover ring / brush rect (SVG) paint over
   it. pointer-events:none routes all interaction to the SVG capture rect;
   hit-testing for hover/click is done in JS. */
.viz-points-canvas { position: absolute; top: 0; left: 0; pointer-events: none; }
/* Axis lines + tick/label text. (Grid lines are drawn on the canvas layer.) */
.viz-axis { stroke: var(--border-strong); stroke-width: 1.5; }
.viz-axis-tick { fill: var(--text-muted); font-size: 12.5px; font-variant-numeric: tabular-nums; }
.viz-axis-label { fill: var(--text-strong); font-size: 14px; font-weight: 700; }
/* Base scatter points (and selection halos) now render on the canvas layer
   (.viz-points-canvas); the per-point .viz-point / .viz-point-ring.is-selected
   styles they used were removed. The single hover ring stays in SVG: */
.viz-point-ring { fill: none; pointer-events: none; }
.viz-point-ring.is-hover { stroke: var(--text-strong); stroke-width: 1.6; opacity: 0.85; }
/* (The regression line is drawn on the canvas layer, above the grid and below
   the points.) */
.viz-diagonal { stroke: var(--text-faint); stroke-width: 1; stroke-dasharray: 4 3; }
/* Regression error band — a subtle filled ribbon under the line */
.viz-band { fill: var(--accent); fill-opacity: 0.1; stroke: none; }
/* Vertical / horizontal reference lines */
.viz-refline line { stroke: var(--text-muted); stroke-width: 1; stroke-dasharray: 5 3; }
/* LipE reference lines */
.viz-lipe-line line { stroke: var(--accent); stroke-width: 1; stroke-dasharray: 2 3; opacity: 0.7; }
/* Decision criteria — cutoff lines. (The desirable-region shade is drawn on
   the canvas layer.) */
.viz-critline line { stroke: #16a34a; stroke-width: 1.25; stroke-dasharray: 6 3; opacity: 0.9; }
.viz-ref-label { fill: var(--text-muted); font-size: 10.5px; font-weight: 600; font-variant-numeric: tabular-nums; }
.viz-brush { fill: var(--accent-soft); fill-opacity: 0.25; stroke: var(--accent); stroke-width: 1; stroke-dasharray: 3 2; }
/* All passive overlays are non-interactive so brush-select / Alt-pan work
   anywhere on the plot background — even over the error band, the criteria
   sweet-spot shade, guide / regression / LipE lines, grid, and labels.
   Only the data points and the brush-capture <rect> stay interactive. */
.viz-axis, .viz-axis-tick, .viz-axis-label,
.viz-diagonal, .viz-band, .viz-brush,
.viz-refline, .viz-lipe-line, .viz-critline,
.viz-ref-label { pointer-events: none; }
.viz-chart-note {
    position: absolute;
    left: 8px;
    bottom: 6px;
    font-size: 0.68rem;
    color: var(--text-faint);
    background: var(--surface);
    padding: 1px 5px;
    border-radius: var(--radius-sm);
}
/* Stats annotation box — an aligned label / = / value mini-table (the
   shared label column lines up the "="; values are tabular). Simple subtle
   bordered box, no accent bar / highlight strip. */
.viz-chart-eq {
    position: absolute;
    left: 8px;
    top: 8px;
    display: grid;
    grid-template-columns: max-content max-content 1fr;
    align-items: baseline;
    gap: 2px 6px;
    max-width: calc(100% - 16px);
    padding: 6px 9px;
    font-size: 0.82rem;
    line-height: 1.45;
    color: var(--text);
    background: var(--surface);
    border: 1.5px solid var(--border-strong);
    border-radius: var(--radius-sm);
    box-shadow: var(--shadow-1);
    font-variant-numeric: tabular-nums;
    pointer-events: none;
}
.viz-eq-k { color: var(--text); }
.viz-eq-sep { color: var(--text-faint); }
.viz-eq-v { color: var(--text); white-space: nowrap; }
.viz-eq-note {
    grid-column: 1 / -1;
    margin-top: 1px;
    font-size: 0.66rem;
    color: var(--text-faint);
    text-transform: uppercase;
    letter-spacing: 0.03em;
}
/* Distribution chart (histogram / density / box / violin) — a clean toolbar
   (selected columns + plot type + a settings gear) above a client-side SVG
   chart. The gear-toggled settings panel REUSES the shared .viz-settings
   primitives (fields, toggles, inputs, dropdowns) so it matches the Scatter
   panel's heights, padding, radius and typography exactly. */
/* flex:0 0 auto — the distribution view keeps its natural content height and is
   NEVER shrunk by the scrolling workspace. Without this, adding a column grows
   the summary table, the workspace runs short, and flex compresses the whole
   view (chart included). Now the chart stays put and the rail scrolls instead. */
.viz-dist { --dist-h: 30px; display: flex; flex-direction: column; gap: 0.55rem; padding: 0 0.8rem 0.7rem; flex: 0 0 auto; min-height: 0; }
/* Shared segmented control (orientation / multiples). Sized to the
   same --viz-ctrl-h the settings inputs use, so it lines up with them. */
.viz-seg { display: inline-flex; align-items: stretch; gap: 2px; padding: 2px; height: var(--viz-ctrl-h, 30px);
    background: var(--surface-sunk); border: 1px solid var(--border-strong); border-radius: var(--radius-sm); box-sizing: border-box; flex: 0 0 auto; }
.viz-seg-btn { display: inline-flex; align-items: center; justify-content: center; padding: 0 11px; border: 0; background: transparent;
    border-radius: calc(var(--radius-sm) - 1px); font: inherit; font-size: 0.76rem; font-weight: 600;
    color: var(--text-muted); cursor: pointer; white-space: nowrap; }
.viz-seg-btn:hover { color: var(--text); }
.viz-seg-btn.is-active { background: var(--surface); color: var(--accent-strong); box-shadow: var(--shadow-1, 0 1px 2px rgba(15,23,42,0.08)); }
/* Series colour dot — shared by the overlay legend + the summary table. (The
   above-plot badge bar was removed; columns are managed in the Settings card.) */
.viz-dist-chip-dot { width: 9px; height: 9px; border-radius: 50%; flex: 0 0 auto; }

/* Settings panel — reuses the shared .viz-settings + collapsible .viz-set-sec
   primitives (same as the Scatter panel); this just drops the scatter panel's
   side margin since .viz-dist already has padding. */
.viz-dist-settings { margin: 0; }
/* Inside a settings field, the segmented control fills the cell width and its
   buttons share the space equally (so Vertical|Horizontal etc. align). */
.viz-dist-settings .viz-seg { width: 100%; }
.viz-dist-settings .viz-seg .viz-seg-btn { flex: 1 1 0; padding: 0 8px; }
/* Axis min / max / ticks — three equal columns aligned with the panel grid. */
.viz-dist-axgrid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 6px 8px; align-items: end; }
/* Mode-specific settings: up to THREE inputs on ONE row when there's width;
   wraps cleanly on a narrow panel (never stacks unnecessarily). */
.viz-dist-modegrid { display: grid; grid-template-columns: repeat(auto-fit, minmax(80px, 1fr)); gap: 6px 8px; align-items: end; }
/* Columns section: add-column picker + a compact CARD listing the selected
   columns (per-column colour swatch · name · remove). Mode stays global. */
.viz-dist-addrow { margin-bottom: 6px; }
.viz-dist-addcol.studio-dropdown-modal { width: 100%; min-height: var(--dist-h); box-shadow: none; }
.viz-dist-collist { border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface); overflow: hidden; }
.viz-dist-srow { display: flex; align-items: center; gap: 8px; padding: 5px 8px; min-width: 0; }
.viz-dist-srow + .viz-dist-srow { border-top: 1px solid var(--border-faint); }
/* Cap the name so a long column name truncates EARLY and the Columns card stays
   compact; the full name is on the title tooltip. The × is pinned to the right
   edge (margin-left:auto) regardless of how short the truncated name is. */
.viz-dist-srow-name { flex: 0 1 auto; min-width: 0; max-width: 200px; font-size: 0.78rem; font-weight: 600; color: var(--text);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.viz-dist-srow-x { flex: 0 0 auto; margin-left: auto; border: 0; background: transparent; cursor: pointer; color: var(--text-muted);
    font-size: 1.05rem; line-height: 1; padding: 0 4px; border-radius: 3px; }
.viz-dist-srow-x:hover { color: var(--danger); background: var(--surface-sunk); }
.viz-dist-srow-hint { font-size: 0.78rem; color: var(--text-muted); padding: 2px 0; }
/* Axis sub-block (Value axis / Frequency axis), global for all columns. */
.viz-dist-axsub { display: flex; flex-direction: column; gap: 4px; }
.viz-dist-axsub + .viz-dist-axsub { margin-top: 6px; }
/* Settings dropdown menus must not exceed the panel width (long column names). */
.viz-menu { max-width: min(360px, 90vw); }
.viz-menu .studio-dropdown-option { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Setup / empty state — an obvious prompt to choose a column to plot. */
.viz-dist-setup .viz-dist-setup-title { font-size: 0.92rem; font-weight: 600; color: var(--text); }
.viz-dist-setup .viz-dist-setup-sub { font-size: 0.8rem; color: var(--text-muted); max-width: 320px; }
.viz-dist-warn { font-size: 0.74rem; color: var(--warn-strong, #92400e); background: var(--warn-soft, #fffbeb);
    border: 1px solid var(--warn-border, #fde68a); border-radius: var(--radius-sm); padding: 4px 8px; }
/* min-height gives the plot a stable floor: the chart keeps a consistent
   height regardless of how many columns are charted or how tall the summary
   grows. (aspect-ratio drives the natural height; this just prevents collapse.) */
.viz-dist-svg { width: 100%; height: auto; min-height: 220px; max-height: 360px; display: block; }
/* Plot card — the SHARED container for BOTH the overlay chart and each facet
   small-multiple, so they read consistently. Clean, not-heavy border.
   flex:0 0 auto so the card is never compressed by the column flex layout. */
.viz-dist-card { border: 1px solid var(--border-strong); border-radius: var(--radius-md); padding: 6px; background: var(--surface); min-width: 0; flex: 0 0 auto; }
/* Facet small-multiples — ONE card per row (full width). No title header — the
   value axis labels each card with its column name. flex:0 0 auto so a tall
   stack scrolls (in the workspace) rather than squeezing each panel. */
.viz-dist-facets { display: flex; flex-direction: column; gap: 0.6rem; flex: 0 0 auto; }
/* Facet panels use the SAME canvas height as the overlay panel (DistPanel
   height=320 for both) so a column's plot does NOT shrink when you switch
   Overlay → Facet; a tall facet stack scrolls in the workspace instead. */
.viz-dist-facet .viz-dist-svg { min-height: 220px; max-height: 360px; }
.viz-dist-facet-empty { font-size: 0.76rem; color: var(--text-muted); padding: 24px 8px; text-align: center; }
/* Summary-statistics table — compact, scientific, tabular figures, in its own
   card so it reads as a distinct, structured block below the plot. */
.viz-dist-stats-card { border: 1.5px solid var(--border-strong); border-radius: var(--radius-md); background: var(--surface); overflow: hidden; }
.viz-dist-stats-head { font-size: 0.66rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em;
    color: var(--text-faint); padding: 7px 10px 5px; border-bottom: 1px solid var(--border); }
.viz-dist-stats-wrap { overflow-x: auto; padding: 0 6px 4px; }
/* width:auto (not 100%) so columns size to their CONTENT and the table never
   stretches the first (Column) cell to fill spare width — that stretch was the
   too-large Column→n gap. The table sits left-aligned in its card; the numeric
   columns stay tight next to the names. */
/* width:100% so the table fills its card (no large empty area on the right);
   the first column gets an EXPLICIT width (below) so auto table layout keeps it
   fixed and distributes the SPARE width to the auto numeric columns instead of
   stretching the Column cell — tight Column→n AND no right-side blank. When the
   card is narrow the numeric columns keep their content width and the table just
   scrolls (.viz-dist-stats-wrap), so numbers never overlap. */
.viz-dist-stats-table { width: 100%; border-collapse: collapse; font-size: 0.76rem; font-variant-numeric: tabular-nums; }
.viz-dist-stats-table th, .viz-dist-stats-table td { padding: 4px 8px; text-align: right; white-space: nowrap; }
/* Row separators live on the ROW, not each cell, so the divider is ONE
   continuous line across the full width — a long, truncated name in the first
   cell can never break or interrupt it (the previous per-cell borders could). */
.viz-dist-stats-table thead th { border-bottom: 1.5px solid var(--border-strong); }
.viz-dist-stats-table tbody tr { border-bottom: 1px solid var(--border-strong); }
.viz-dist-stats-table tbody tr:last-child { border-bottom: 0; }
.viz-dist-stats-table th { color: var(--text-muted); font-weight: 600; }
.viz-dist-stats-table th:first-child, .viz-dist-stats-table td:first-child { text-align: left; }
/* First cell: an EXPLICIT width (auto layout honours it, unlike max-width which
   browsers ignore when stretching) so the Column cell stays fixed and long names
   truncate cleanly (ellipsis + tooltip) without overflowing the cell / divider.
   The auto numeric columns soak up the rest of the card width. */
.viz-dist-stats-table th:first-child, .viz-dist-stats-table td:first-child { width: 150px; max-width: 150px; }
.viz-dist-stat-name { display: flex; align-items: center; gap: 6px; min-width: 0; max-width: 100%; overflow: hidden; }
.viz-dist-stat-name span:last-child { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.viz-dist-na { color: var(--danger); }

.viz-chart-empty {
    flex: 1 1 auto;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 8px;
    text-align: center;
    color: var(--text-muted);
    font-size: 0.82rem;
    line-height: 1.45;
    padding: 20px;
}
.viz-chart-empty svg { width: 34px; height: 34px; color: var(--accent); opacity: 0.7; }

/* Hover tooltip — compact key values only (structure lives in the pinned
   card on click). */
.viz-tooltip {
    position: absolute;
    z-index: 30;
    display: flex;
    flex-direction: column;
    gap: 2px;
    width: 148px;
    padding: 7px 9px;
    background: var(--surface);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    box-shadow: var(--shadow-2, 0 6px 20px rgba(0, 0, 0, 0.16));
    pointer-events: none;
}
.viz-tooltip-id { font-size: 0.78rem; font-weight: 700; color: var(--text-strong); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.viz-tooltip-row { display: flex; justify-content: space-between; gap: 8px; font-size: 0.76rem; color: var(--text-muted); }
.viz-tooltip-row span { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.viz-tooltip-row b { color: var(--text); font-variant-numeric: tabular-nums; flex: 0 0 auto; }

/* Pinned structure card — shown on point click, top-right of the chart. */
.viz-pinned {
    position: absolute;
    top: 8px;
    right: 8px;
    z-index: 31;
    display: flex;
    flex-direction: column;
    gap: 4px;
    padding: 7px;
    background: var(--surface);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-2, 0 6px 20px rgba(0, 0, 0, 0.16));
}
.viz-pinned-head { display: flex; align-items: center; gap: 6px; }
.viz-pinned-id {
    flex: 1 1 auto;
    min-width: 0;
    max-width: 150px;
    font-size: 0.76rem;
    font-weight: 700;
    color: var(--text-strong);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.viz-pinned-close {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 20px;
    height: 20px;
    padding: 0;
    color: var(--text-muted);
    background: transparent;
    border: none;
    border-radius: var(--radius-sm);
    cursor: pointer;
    transition: background var(--motion-fast) var(--ease), color var(--motion-fast) var(--ease);
}
.viz-pinned-close:hover { background: var(--surface-sunk); color: var(--text-strong); }
.viz-thumb {
    width: 64px;
    height: 64px;
    flex: 0 0 64px;
    background: #fff;
    border: 1px solid var(--border-faint);
    border-radius: var(--radius-sm);
    overflow: hidden;
    display: flex;
    align-items: center;
    justify-content: center;
}
.viz-thumb svg { width: 100%; height: 100%; }
.viz-thumb-empty { color: var(--text-faint); }
.viz-thumb-empty svg { width: 30%; height: 30%; }

/* Chart wrapper = plot + footer (so legend / hint / reset never overlap
   axis labels, ticks, or plotted content). */
.viz-chart-wrap { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; gap: 6px; }
.viz-chart-wrap > .viz-chart { flex: 1 1 auto; min-height: 0; }
/* Stable three-zone footer — empty left · centered zoom hint · right reset.
   Fixed min-height + equal 1fr side zones keep the hint centered whether or
   not Reset view is shown. Color-scale info is NOT shown here (it lives in
   the settings panel's Color scale control). */
.viz-chart-foot {
    flex: 0 0 auto;
    display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 8px;
    min-height: 26px;
    font-size: 0.72rem; color: var(--text-faint);
}
.viz-chart-foot-l { justify-self: start; min-width: 0; }
.viz-chart-foot-c { justify-self: center; min-width: 0; }
.viz-chart-foot-r { justify-self: end; }
.viz-chart-foot-hint { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* Save plot — compact icon-only footer button, left of Reset view. */
.viz-foot-btn {
    display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto;
    width: 24px; height: 22px; padding: 0;
    color: var(--text-muted);
    background: var(--surface);
    border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
    cursor: pointer;
    transition: border-color var(--motion-fast) var(--ease), color var(--motion-fast) var(--ease);
}
.viz-foot-btn:hover:not(:disabled) { border-color: var(--accent); color: var(--accent-strong); }
.viz-foot-btn:disabled { opacity: 0.45; cursor: not-allowed; }
.viz-foot-btn svg { display: block; }
/* Reset view — a compact footer button (only shown when zoomed/panned). */
.viz-reset-view {
    display: inline-flex; align-items: center; gap: 5px; flex: 0 0 auto;
    padding: 2px 8px;
    font-size: 0.72rem; color: var(--text);
    background: var(--surface);
    border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
    cursor: pointer;
    transition: border-color var(--motion-fast) var(--ease), color var(--motion-fast) var(--ease);
}
.viz-reset-view:hover { border-color: var(--accent); color: var(--accent-strong); }
.viz-reset-view svg { flex: 0 0 auto; }

/* Summary (Part B) — fills the lower rail so distributions can grow. */
.viz-summary { display: flex; flex-direction: column; gap: 7px; min-height: 100%; }
.viz-summary-empty { color: var(--text-muted); font-size: 0.82rem; padding: 12px; text-align: center; }
/* Header chips — Plotted + Selected (always), and Meet-criteria (only when
   decision-guide criteria are set). */
.viz-sum-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.viz-sum-chip {
    display: inline-flex;
    align-items: baseline;
    gap: 5px;
    padding: 2px 9px;
    background: var(--surface-sunk);
    border: 1px solid var(--viz-border, var(--border));
    /* Restrained corner instead of a full pill — the pills read as
       consumer/"AI dashboard"; a small radius matches the rest of the
       analysis cards and Alma Studio's quieter, more professional look. */
    border-radius: var(--radius-sm);
}
.viz-sum-chip b { font-size: 0.88rem; font-weight: 700; color: var(--text-strong); font-variant-numeric: tabular-nums; }
.viz-sum-chip span { font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.03em; color: var(--text-muted); }
.viz-sum-chip.is-accent { background: var(--accent-soft); border-color: var(--accent-muted); }
.viz-sum-chip.is-accent b { color: var(--accent-strong); }
.viz-sum-chip.is-pass { background: rgba(22, 163, 74, 0.12); border-color: rgba(22, 163, 74, 0.4); }
.viz-sum-chip.is-pass b { color: #15803d; }
/* Selected-compound review — a self-contained table card (border / radius /
   surface) matching the distribution cards: an integrated header bar over a
   scrollable body with a sticky column-header row. */
.viz-seltable {
    display: flex;
    flex-direction: column;
    background: var(--surface);
    border: 1px solid var(--viz-border, var(--border));
    border-radius: var(--radius-sm);
    overflow: hidden;
}
.viz-seltable-head {
    display: flex; align-items: center; justify-content: space-between; gap: 8px;
    flex: 0 0 auto;
    padding: 5px 8px;
    border-bottom: 1px solid var(--border-faint);
}
.viz-seltable-title { font-size: 0.72rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.03em; color: var(--text-strong); }
/* Fixed height + flex centering + explicit line-height so the "Copy IDs" and
   "Copied ✓" states share the EXACT same footprint. Without this the check
   glyph (often emoji-rendered with taller metrics) grew the line box, so the
   button — and the Selected-compounds header row — jumped on click. Scoped to
   this button only. */
.viz-copy-ids {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    box-sizing: border-box;
    height: 22px;
    padding: 0 8px;
    line-height: 1;
    font-size: 0.72rem;
    white-space: nowrap;
    overflow: hidden;
    color: var(--text-muted);
    background: var(--surface);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    cursor: pointer;
    transition: border-color var(--motion-fast) var(--ease), color var(--motion-fast) var(--ease);
}
.viz-copy-ids:hover { border-color: var(--accent); color: var(--accent-strong); }
/* Body — scrolls internally; the card supplies the boundary so the grid has no
   border/radius of its own. */
.viz-seltable-grid { display: flex; flex-direction: column; max-height: 160px; overflow-y: auto; }
.viz-seltable-row {
    display: grid;
    grid-template-columns: minmax(0, 1.2fr) 1fr 1fr;
    gap: 6px; align-items: center; width: 100%;
    padding: 3px 8px; font-size: 0.74rem; color: var(--text-muted);
    background: transparent; border: none; border-bottom: 1px solid var(--border-faint);
    cursor: pointer; text-align: left; font-variant-numeric: tabular-nums;
}
.viz-seltable-row.has-crit { grid-template-columns: minmax(0, 1.2fr) 1fr 1fr 16px; }
.viz-seltable-row:not(.is-head):hover { background: var(--accent-soft); color: var(--text); }
/* Last data row sits flush on the card's bottom edge — drop its divider. */
.viz-seltable-grid .viz-seltable-row:last-child { border-bottom: none; }
.viz-seltable-row.is-head {
    position: sticky; top: 0; cursor: default;
    font-size: 0.62rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.02em;
    color: var(--text-faint); background: var(--surface-sunk);
}
.viz-sel-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text); }
.viz-sel-pass { color: #15803d; font-weight: 700; }
.viz-sel-fail { color: var(--text-faint); }
.viz-seltable-more { flex: 0 0 auto; font-size: 0.68rem; color: var(--text-faint); text-align: center; padding: 3px 8px; border-top: 1px solid var(--border-faint); background: var(--surface-sunk); }
/* Relationship / correlation — a plain card matching the distribution cards
   (no left accent strip). */
.viz-rel-card {
    display: flex;
    flex-direction: column;
    gap: 5px;
    padding: 6px 8px;
    background: var(--surface);
    border: 1px solid var(--viz-border, var(--border));
    border-radius: var(--radius-sm);
}
.viz-rel-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
.viz-rel-title { font-size: 0.72rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.03em; color: var(--text-strong); }
.viz-rel-space { font-size: 0.62rem; text-transform: uppercase; letter-spacing: 0.03em; color: var(--text-faint); }
/* Metric cluster — reuses the distribution cards' .viz-metric cell so r / R²
   / slope / RMSE align as one cohesive group rather than loose chips. */
.viz-rel-metrics { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 4px; }
/* X / Y distribution cards — grow to fill the lower rail area evenly */
/* Stable analysis module: `flex: 0 0 auto` (no grow/shrink) so the row sizes to
   its content — which is anchored by the fixed-height histogram body (.viz-hist)
   — and the X/Y bar plots stay tall and identical in every state (no selection,
   one, or many selected). The Selected-compounds table sits below and scrolls on
   its own, so it can never take height from the bars; when the rail is short the
   analysis panel (.viz-split-bottom, overflow:auto) scrolls instead of
   compressing them. */
.viz-stat-row { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; align-items: stretch; flex: 0 0 auto; }
.viz-stat-card {
    display: flex;
    flex-direction: column;
    gap: 6px;
    min-width: 0;
    background: var(--surface);
    border: 1px solid var(--viz-border, var(--border));
    border-radius: var(--radius-sm);
    padding: 8px;
}
/* Compact card header — column name on a subtle divider so each distribution
   reads as its own titled panel. */
.viz-stat-head {
    display: flex;
    align-items: baseline;
    gap: 8px;
    padding-bottom: 5px;
    border-bottom: 1px solid var(--border-faint);
}
.viz-stat-title {
    flex: 1 1 auto;
    min-width: 0;
    font-size: 0.78rem;
    font-weight: 700;
    color: var(--text-strong);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
/* Metric mini-cards (n / mean / median / std / min / max) */
.viz-metrics { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 4px; }
.viz-metric {
    display: flex;
    flex-direction: column;
    gap: 1px;
    padding: 3px 5px;
    background: var(--surface-sunk);
    border-radius: var(--radius-sm);
}
.viz-metric-label { font-size: 0.62rem; text-transform: uppercase; letter-spacing: 0.02em; color: var(--text-faint); }
.viz-metric-value { font-size: 0.78rem; font-weight: 600; color: var(--text); font-variant-numeric: tabular-nums; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* Distribution histogram — a FIXED-height bar-plot body so the bars stay tall
   and identical in every state (no selection, one, or many selected). Because
   the height is fixed here (not flex-filled), the parent .viz-stat-row doesn't
   need to grow. Each column is a full-height track with its bar anchored to
   the baseline (the container's bottom border = x-axis); a min…max row beneath
   gives a lightweight value axis. When a selection is active a faint "all"
   backdrop sits behind the solid "selected" bars so the subset reads clearly. */
/* `margin-top` (on top of the card's 6px gap) separates the bar plot from the
   stats/metric grid above it — a bit more breathing room without stretching. */
.viz-hist-wrap { display: flex; flex-direction: column; gap: 3px; flex: 0 0 auto; margin-top: 6px; }
.viz-hist {
    display: flex;
    align-items: flex-end;
    gap: 2px;
    flex: 0 0 auto;
    height: 150px;
    padding: 4px 4px 0;
    background: var(--surface-sunk);
    border: 1px solid var(--viz-border, var(--border));
    border-bottom-width: 1.5px;
    border-bottom-color: var(--border-strong);
    border-radius: var(--radius-sm) var(--radius-sm) 0 0;
}
.viz-hist-col { position: relative; flex: 1 1 auto; min-width: 0; height: 100%; }
/* Bin(s) containing selected points — a subtle full-height tint so even a
   single clicked compound reads as a highlighted bin. */
.viz-hist-col.is-hit { background: var(--accent-soft); border-radius: 2px; }
.viz-hist-bar {
    position: absolute; left: 0; right: 0; bottom: 0;
    background: var(--accent); opacity: 0.55;
    border-radius: 2px 2px 0 0; min-height: 1px;
}
/* Overlay mode — faint full-population backdrop + solid selected subset. */
.viz-hist-bar.is-all { opacity: 0.16; }
.viz-hist-bar.is-sel { opacity: 0.85; }
.viz-hist-axis { display: flex; justify-content: space-between; gap: 8px; font-size: 0.6rem; color: var(--text-faint); font-variant-numeric: tabular-nums; padding: 0 1px; }
.viz-hist-empty { align-items: center; justify-content: center; color: var(--text-faint); font-size: 0.72rem; }

/* Tools → Visualizations card */
.studio-tool-viz-body { display: flex; flex-direction: column; }
.studio-tool-viz-list { display: flex; flex-direction: column; gap: 4px; }
/* Each row hosts an "open" button (glyph + name/subtitle) plus compact
   rename / delete actions, or an inline rename input / delete-confirm. */
.studio-tool-viz-row {
    display: flex;
    align-items: center;
    gap: 4px;
    width: 100%;
    /* Slightly taller rows (was 4px) so Function / Visualization cards feel
       balanced and are easier to click, without bulking up the Tools bar. */
    padding: 7px 5px 7px 8px;
    color: var(--text);
    background: var(--surface);
    border: 1px solid var(--border-faint);
    /* All rows carry a subtle left accent so they align; the active row
       gets a stronger accent + tint (no "ACTIVE" badge). */
    border-left: 2px solid var(--border-strong);
    border-radius: var(--radius-sm);
    transition: background var(--motion-fast) var(--ease), border-color var(--motion-fast) var(--ease);
}
.studio-tool-viz-row:hover { border-color: var(--border-strong); border-left-color: var(--text-faint); background: var(--surface-soft); }
.studio-tool-viz-row.is-active {
    border-color: var(--accent-muted);
    border-left-color: var(--accent);
    background: var(--accent-soft);
}
.studio-tool-viz-open {
    display: flex;
    align-items: center;
    gap: 7px;
    flex: 1 1 auto;
    min-width: 0;
    padding: 2px 0;
    color: inherit;
    background: transparent;
    border: none;
    cursor: pointer;
    text-align: left;
}
/* inline-flex + block svg so the leading glyph (e.g. the Function card's
   "fx"/T mark) is vertically centered with the name on its right. */
.studio-tool-viz-glyph { color: var(--accent); flex: 0 0 auto; display: inline-flex; align-items: center; }
.studio-tool-viz-glyph svg { display: block; }
.studio-tool-viz-name { flex: 1 1 auto; font-size: 0.8rem; line-height: 1.2; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.studio-tool-viz-row.is-active .studio-tool-viz-name { color: var(--accent-strong); font-weight: 600; }
.studio-tool-viz-warn { flex: 0 0 auto; color: var(--danger-strong, #be123c); }
.studio-tool-viz-actions { display: flex; align-items: center; gap: 2px; flex: 0 0 auto; }
/* Smaller, lighter per-plot action buttons (rename / delete). */
.studio-tool-icon-btn.studio-tool-viz-act {
    width: 22px; height: 22px;
    border-color: transparent; background: transparent; color: var(--text-faint);
}
.studio-tool-icon-btn.studio-tool-viz-act:hover:not(:disabled) { background: var(--surface-sunk); color: var(--text-strong); border-color: var(--border); }
.studio-tool-icon-btn.studio-tool-viz-act.is-danger:hover:not(:disabled) { color: var(--danger-strong); border-color: var(--danger-strong); background: var(--danger-bg); }
.studio-tool-viz-rename {
    flex: 1 1 auto;
    min-width: 0;
    padding: 3px 6px;
    font-size: 0.8rem;
    color: var(--text);
    background: var(--surface);
    /* Subtle resting border (was a heavy solid-accent line); the global input
       focus rule supplies the accent border on focus, with no halo. */
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
}
.studio-tool-viz-confirm { display: flex; align-items: center; gap: 6px; flex: 1 1 auto; min-width: 0; }
.studio-tool-viz-confirm-text { flex: 1 1 auto; min-width: 0; font-size: 0.76rem; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Footer — separates the list from the create action; button right-aligned. */
.studio-tool-viz-foot { margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--border-faint); display: flex; justify-content: flex-end; }
/* Shared "+ <label>" add buttons (Add visualization, Add function, …). Icon +
   label are centered as a group; block svg + line-height:1 stop the label from
   riding above the plus icon (inline-svg baseline gap). Balanced L/R padding:
   the leading icon makes the left edge feel tight, so trim left padding by the
   icon+gap width so the optical padding matches the right. The plus glyph uses
   strokeWidth 2 to match the ~600-weight label. */
.studio-tool-viz-add { display: inline-flex; align-items: center; justify-content: center; gap: 6px; line-height: 1; padding-left: 10px; padding-right: 12px; }
.studio-tool-viz-add svg, .studio-plus-glyph { display: block; flex: 0 0 auto; }

/* ── Function tool: card "empty" tag + expression editor modal ──────
   The Function card reuses the .studio-tool-viz-* row/list/confirm styles
   above; only the "no expression yet" tag is new. */
.studio-tool-fn-empty {
    flex: 0 0 auto;
    margin-left: 6px;
    font-size: 0.62rem;
    font-style: italic;
    color: var(--text-faint);
    text-transform: uppercase;
    letter-spacing: 0.04em;
}
/* Editor modal — sits on .studio-import-modal (same shell as Add/Draw) but
   must OVERRIDE that shell's fixed `height: min(820px,92vh)` (sized for the
   Ketcher canvas). Without this the short Function content left a large empty
   gap below the footer — the "too much blank space" symptom. Auto height +
   a viewport cap makes the modal hug its content. */
.studio-fn-modal {
    max-width: 520px;
    width: 92vw;
    height: auto;
    max-height: 90vh;
}
.studio-fn-body {
    display: flex;
    flex-direction: column;
    gap: 12px;
    padding: 16px 18px;
    /* min-height:0 lets this flex child scroll inside the capped modal; it
       grows with content and only scrolls once the modal hits max-height. */
    min-height: 0;
    overflow-y: auto;
}
.studio-fn-field { display: flex; flex-direction: column; gap: 5px; }
.studio-fn-flabel {
    font-size: 0.72rem; font-weight: 600; color: var(--text-muted);
    text-transform: uppercase; letter-spacing: 0.03em;
}
.studio-fn-name-input {
    width: 100%;
    box-sizing: border-box;
    padding: 0.5rem 0.7rem;
    border: 1.5px solid var(--border-strong);
    border-radius: var(--radius-sm);
    background: var(--surface);
    color: var(--text-strong);
    font-size: 0.9rem;
    font-family: inherit;
}
.studio-fn-name-input:focus {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 3px var(--accent-ring);
}
/* ── Expression editor: resizable textarea + highlight overlay ─────
   A real <textarea> (normal caret / selection / IME, resizable) sits ON TOP
   with transparent text; a highlight <div> BEHIND renders the same text with
   `[column]` refs tinted. The two layers MUST share an identical box model or
   the caret drifts — every metric below is duplicated across both, and the
   ref tint uses background + radius + an INSET box-shadow (never padding/
   border/margin) so highlighted text keeps the exact same width as plain text. */
.studio-fn-expr-wrap { position: relative; width: 100%; }
.studio-fn-expr-hl,
.studio-fn-expr {
    width: 100%;
    box-sizing: border-box;
    margin: 0;
    padding: 0.5rem 0.7rem;
    border: 1.5px solid transparent;
    border-radius: var(--radius-sm);
    font-family: var(--font-mono, ui-monospace, monospace);
    font-size: 0.85rem;
    line-height: 1.5;
    letter-spacing: normal;
    white-space: pre-wrap;
    overflow-wrap: break-word;
    word-break: break-word;
    tab-size: 2;
}
.studio-fn-expr-hl {
    position: absolute;
    inset: 0;
    overflow: hidden;
    pointer-events: none;
    background: var(--surface);
    color: var(--text-strong);
    z-index: 0;
}
.studio-fn-expr {
    position: relative;
    z-index: 1;
    min-height: 4.6em;            /* ≈ 3 rows */
    resize: vertical;            /* normal textarea resize handle */
    background: transparent;
    color: transparent;
    -webkit-text-fill-color: transparent;
    caret-color: var(--text-strong);
    border-color: var(--border-strong);
}
.studio-fn-expr::placeholder { color: var(--text-faint); -webkit-text-fill-color: var(--text-faint); }
.studio-fn-expr:focus {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 3px var(--accent-ring);
}
/* Column-reference highlight — tint only (no padding/border/margin) so the
   highlighted run keeps the exact width of the underlying textarea text. */
.studio-fn-ref {
    background: var(--accent-soft);
    color: var(--accent-strong);
    border-radius: 3px;
    box-shadow: inset 0 0 0 1px var(--accent-ring);
}
.studio-fn-ref.is-bad {
    background: var(--danger-soft);
    color: var(--danger);
    box-shadow: inset 0 0 0 1px var(--danger);
}
/* The [ ] brackets of a column ref: kept in the overlay (so their character
   width still matches the textarea's raw `[Label]` text → caret alignment)
   but painted transparent so the user sees just the highlighted name. The
   bracket cells double as the pill's visual padding. */
.studio-fn-brk { color: transparent; }
/* ── Operator / function palette — grouped rows of compact chips ── */
.studio-fn-palette { display: flex; flex-direction: column; gap: 7px; }
.studio-fn-pgroup { display: flex; align-items: center; gap: 8px; }
.studio-fn-pgroup-label {
    flex: 0 0 62px;
    font-size: 0.66rem; font-weight: 600; color: var(--text-faint);
    text-transform: uppercase; letter-spacing: 0.04em;
}
.studio-fn-pgroup-toks { flex: 1 1 auto; display: flex; flex-wrap: wrap; gap: 6px; }
.studio-fn-tok {
    min-width: 30px;
    padding: 4px 9px;
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    background: var(--surface-sunk);
    color: var(--text-strong);
    font-size: 0.82rem;
    font-family: var(--font-mono, monospace);
    cursor: pointer;
    transition: background var(--motion-fast) var(--ease), border-color var(--motion-fast) var(--ease);
}
.studio-fn-tok.is-fn { font-family: inherit; font-size: 0.78rem; }
.studio-fn-tok:hover { background: var(--accent-soft); border-color: var(--accent-ring); color: var(--accent-strong); }
.studio-fn-cols-label {
    font-size: 0.72rem; font-weight: 600; color: var(--text-muted);
    text-transform: uppercase; letter-spacing: 0.03em;
    margin-top: 2px;
}
/* Insert-column list — a light, bordered container (no gray fill) that blends
   with the modal surface; the row separators carry the structure. NO padding:
   each row spans the full container width so its hover background reaches both
   edges and (via overflow clipping) the first/last row sit flush with the
   rounded top/bottom. All inset spacing lives on the rows. Scrolls when long. */
.studio-fn-cols-panel {
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: transparent;
    max-height: 168px;
    overflow-y: auto;
    padding: 0;
}
/* Vertical list — one column per row; each row is a full-width clickable
   insert target. The panel above (.studio-fn-cols-panel) caps height + scrolls. */
.studio-fn-cols { display: flex; flex-direction: column; }
/* One row per column; a thin bottom rule separates rows (none on the last).
   Hover is a restrained background tint only — no border box. */
.studio-fn-col-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    width: 100%;
    padding: 6px 8px;
    border: none;
    border-bottom: 1px solid var(--border);
    border-radius: 0;
    background: transparent;
    color: var(--text);
    font-size: 0.82rem;
    text-align: left;
    cursor: pointer;
    transition: background var(--motion-fast) var(--ease), color var(--motion-fast) var(--ease);
}
.studio-fn-col-row:last-child { border-bottom: none; }
.studio-fn-col-row-name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.studio-fn-col-row-add { flex: 0 0 auto; font-size: 0.95rem; line-height: 1; color: var(--text-faint); opacity: 0; transition: opacity var(--motion-fast) var(--ease); }
.studio-fn-col-row:hover { background: var(--accent-soft); color: var(--accent-strong); }
.studio-fn-col-row:hover .studio-fn-col-row-add { opacity: 1; color: var(--accent-strong); }
.studio-fn-cols-empty { font-size: 0.78rem; color: var(--text-muted); font-style: italic; padding: 2px; }
/* Validation / status line — colour-coded by state. Always shown: a subtle
   hint when empty, the valid/warn state when good, the parser error otherwise. */
.studio-fn-validate {
    font-size: 0.78rem;
    line-height: 1.35;
    padding: 6px 9px;
    border-radius: var(--radius-sm);
    border: 1px solid transparent;
}
.studio-fn-validate.is-hint  { color: var(--text-muted); background: var(--surface-sunk); }
.studio-fn-validate.is-ok    { color: var(--success, #15803d); background: var(--success-soft, rgba(22,163,74,0.10)); border-color: var(--success-soft, rgba(22,163,74,0.25)); }
.studio-fn-validate.is-warn  { color: var(--warning, #b45309); background: var(--warning-soft, rgba(217,119,6,0.10)); border-color: var(--warning-soft, rgba(217,119,6,0.25)); }
.studio-fn-validate.is-error { color: var(--danger); background: var(--danger-soft); border-color: var(--danger-soft); }
/* Preview row — sample evaluation against the first row. */
.studio-fn-preview {
    display: flex; align-items: center; justify-content: space-between; gap: 10px;
    padding: 7px 10px;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--surface-sunk);
}
.studio-fn-preview-label { font-size: 0.74rem; color: var(--text-muted); }
.studio-fn-preview-val { font-size: 0.9rem; font-weight: 600; color: var(--text-strong); }
.studio-fn-footer {
    display: flex; justify-content: flex-end; gap: 8px;
    padding: 10px 18px;
    border-top: 1px solid var(--border);
}

/* Custom layout — split panel. Grid-only custom keeps the base flex column
   (the grid fills the work area). When the user docks the Plots panel, a 2-track
   grid sized by --studio-custom-size (clamped 25–60% in JS) places it on the
   right or the bottom — reusing the same .studio-analysis-side/VisualizationWorkspace
   as the Analysis layout, so plots behave identically. */
.studio-sheet-panel-custom.is-split-right {
    display: grid;
    grid-template-columns: minmax(0, 1fr) var(--studio-custom-size, 38%);
    gap: 12px;
    align-items: stretch;
}
.studio-sheet-panel-custom.is-split-bottom {
    display: grid;
    grid-template-rows: minmax(0, 1fr) var(--studio-custom-size, 38%);
    gap: 12px;
}
/* Bottom dock: the side becomes a horizontal strip — its two placeholder cards
   sit side by side, and a present plots workspace spans the whole strip. */
.studio-sheet-panel-custom.is-split-bottom .studio-analysis-side {
    grid-template-rows: minmax(0, 1fr);
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
}
.studio-sheet-panel-custom.is-split-bottom .studio-analysis-side > .viz-workspace {
    grid-row: auto;
    grid-column: 1 / -1;
}

.studio-layout-placeholder {
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    gap: 10px;
    max-width: 420px;
    color: var(--text-muted);
}
.studio-layout-placeholder-icon {
    width: 48px;
    height: 48px;
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--accent);
    background: var(--accent-soft);
    border-radius: 50%;
}
.studio-layout-placeholder-title {
    font-size: 1.05rem;
    font-weight: 600;
    color: var(--text-strong);
}
.studio-layout-placeholder-sub {
    font-size: 0.86rem;
    line-height: 1.5;
    color: var(--text-muted);
}


/* ── Structure filter card ──────────────────────────────────────
   Special filter card used by the synthetic Structure entry in
   the picker. Larger than regular cards because it carries a
   square preview panel for the drawn substructure query.

   The card body shows ONE of:
     · "Click to draw substructure" placeholder (no query yet);
     · the rendered SVG of the drawn query (saved SMILES);
     · a small loading shimmer while /render-smiles round-trips.
   The whole panel is a button — clicking opens the simplified
   Draw modal (AddCompoundsModal in `mode='substructure'`). */
.studio-filter-card-structure .studio-filter-card-body {
    display: flex; flex-direction: column;
    gap: 0.5rem;
    padding: 0.75rem;
}
.studio-filter-structure-panel {
    display: flex; align-items: center; justify-content: center;
    width: 100%;
    aspect-ratio: 1 / 1;
    min-height: 180px;
    background: var(--surface-soft);
    border: 1px dashed var(--border-strong);
    border-radius: var(--radius-md);
    color: var(--text-muted);
    cursor: pointer;
    overflow: hidden;
    transition: border-color var(--motion-fast) var(--ease),
                background var(--motion-fast) var(--ease);
}
.studio-filter-structure-panel:hover {
    border-color: var(--accent);
    background: var(--surface);
    color: var(--accent-strong);
}
.studio-filter-structure-placeholder {
    display: flex; flex-direction: column;
    align-items: center; justify-content: center;
    gap: 0.5rem;
    color: var(--text-muted);
    font-size: 0.85rem;
    text-align: center;
    padding: 1rem;
}
.studio-filter-structure-placeholder svg { color: var(--text-faint); }
.studio-filter-structure-loading {
    width: 60%; height: 60%;
    background: linear-gradient(90deg, var(--surface-sunk) 0%, var(--surface) 50%, var(--surface-sunk) 100%);
    background-size: 200% 100%;
    animation: structurePulse 1.4s ease-in-out infinite;
    border-radius: var(--radius-sm);
}
.studio-filter-structure-svg {
    display: block;
    width: 100%; height: 100%;
}
.studio-filter-structure-svg svg {
    width: 100% !important;
    height: 100% !important;
    display: block;
}
.studio-filter-structure-meta {
    display: flex; align-items: center; gap: 0.375rem;
    font-size: 0.78rem;
    color: var(--text-muted);
    overflow: hidden;
}
.studio-filter-structure-meta-label {
    flex-shrink: 0;
    font-weight: 600;
    color: var(--text-faint);
    text-transform: uppercase;
    letter-spacing: 0.05em;
    font-size: 0.7rem;
}
.studio-filter-structure-meta-value {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    color: var(--text);
}

/* ── Alma Agent ────────────────────────────────────────────────────────────
   Three-panel chat workspace: history (left) │ conversation (center) │
   files (right). Pinned with `position: fixed` so it escapes
   .main-content's `max-width: 1800px; margin: 0 auto; padding: 2.75rem
   2rem` and fills every pixel beside the platform sidebar. `--sidebar-w`
   inherits from .app-shell, so the left edge follows the sidebar
   collapse / expand width and animates with it via the matching 0.22s
   transition. */

.alma-agent-page {
    position: fixed;
    top: 0;
    left: var(--sidebar-w, 248px);
    right: 0;
    bottom: 0;
    display: grid;
    grid-template-columns: 280px minmax(0, 1fr) 320px;
    background: var(--bg);
    overflow: hidden;
    box-sizing: border-box;
    transition: left 0.22s ease;
    z-index: 1;
}

/* ── Left panel: chat history ───────────────────────────────────────────── */
.alma-agent-history {
    display: flex;
    flex-direction: column;
    background: var(--surface);
    border-right: 1px solid var(--border);
    min-width: 0;
}
.alma-agent-history-header {
    /* Locked to --topband-h so the bottom border lines up exactly with
       the platform sidebar's header divider on the left and with the
       conversation header divider on the right — gives the page one
       continuous header strip. Vertical padding is 0; alignment comes
       from align-items:center. */
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0 0.75rem;
    height: var(--topband-h);
    min-height: var(--topband-h);
    border-bottom: 1px solid var(--border-faint);
}
.alma-agent-new-chat {
    /* Compact primary action that fills the row beside the collapse
       toggle. The plus icon is absolutely positioned on the left so
       the "New chat" label stays visually centred regardless of the
       icon's width — flex-centring would push the text off-centre by
       the icon + gap (~22px). */
    flex: 1;
    position: relative;
    min-width: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    height: 32px;
    padding: 0 1rem 0 2rem;
    background: var(--accent);
    color: #fff;
    border: none;
    border-radius: var(--radius-md);
    font-size: 0.8125rem;
    font-weight: 600;
    cursor: pointer;
    transition: background var(--motion-fast) var(--ease),
                box-shadow var(--motion-fast) var(--ease);
    box-shadow: var(--shadow-1);
}
.alma-agent-new-chat:hover {
    background: var(--accent-strong);
    box-shadow: var(--shadow-2);
}
.alma-agent-new-chat svg {
    position: absolute;
    left: 0.7rem;
    top: 50%;
    transform: translateY(-50%);
    width: 14px;
    height: 14px;
    stroke: currentColor;
    fill: none;
}
.alma-agent-new-chat-label {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Collapse / expand toggle for the chat history panel — sits to the right
   of the New chat button. Subtle by default; lights up on hover. The
   matching expand-only button is shown when the panel is collapsed. */
.alma-agent-history-collapse-btn,
.alma-agent-history-expand-btn {
    flex-shrink: 0;
    width: 32px;
    height: 32px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: transparent;
    border: 1px solid var(--border);
    color: var(--text-muted);
    border-radius: var(--radius-md);
    cursor: pointer;
    transition: background var(--motion-fast) var(--ease),
                color var(--motion-fast) var(--ease),
                border-color var(--motion-fast) var(--ease);
}
.alma-agent-history-collapse-btn:hover,
.alma-agent-history-expand-btn:hover {
    background: var(--surface-sunk);
    color: var(--text-strong);
    border-color: var(--border-strong);
}
.alma-agent-history-collapse-btn svg,
.alma-agent-history-expand-btn svg {
    width: 14px;
    height: 14px;
    stroke: currentColor;
    fill: none;
}

/* Search bar — sits between the header row and the session list. Style
   tracks the API-key field at the bottom of the panel for visual
   consistency. */
.alma-agent-history-search {
    padding: 0.625rem 0.75rem 0.5rem;
}
.alma-agent-history-search-row {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    background: var(--surface-soft);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    padding: 0 0.4rem 0 0.55rem;
    transition: border-color var(--motion-fast) var(--ease),
                box-shadow var(--motion-fast) var(--ease);
}
.alma-agent-history-search-row:focus-within {
    border-color: var(--accent);
    box-shadow: 0 0 0 3px var(--accent-ring);
}
.alma-agent-history-search-row > svg {
    width: 14px;
    height: 14px;
    stroke: var(--text-muted);
    fill: none;
    flex-shrink: 0;
}
.alma-agent-history-search-input {
    flex: 1;
    min-width: 0;
    padding: 0.4rem 0;
    background: transparent;
    border: none;
    outline: none;
    color: var(--text-strong);
    font-size: 0.8125rem;
}
.alma-agent-history-search-input::placeholder { color: var(--text-faint); }
/* Suppress the native Webkit/Chromium clear button so it doesn't double
   up with our custom .alma-agent-history-search-clear. We keep
   type="search" for accessibility / Escape-clears semantics. */
.alma-agent-history-search-input::-webkit-search-cancel-button,
.alma-agent-history-search-input::-webkit-search-decoration,
.alma-agent-history-search-input::-webkit-search-results-button,
.alma-agent-history-search-input::-webkit-search-results-decoration {
    -webkit-appearance: none;
    appearance: none;
    display: none;
}
.alma-agent-history-search-clear {
    flex-shrink: 0;
    width: 22px;
    height: 22px;
    border: none;
    background: transparent;
    color: var(--text-faint);
    border-radius: var(--radius-sm);
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: background var(--motion-fast) var(--ease),
                color var(--motion-fast) var(--ease);
}
.alma-agent-history-search-clear:hover {
    background: var(--surface-sunk);
    color: var(--text-strong);
}
.alma-agent-history-search-clear svg { width: 12px; height: 12px; stroke: currentColor; fill: none; }

/* Empty state shown when the search query matches no sessions. */
.alma-agent-history-empty {
    padding: 1.5rem 1rem;
    text-align: center;
    color: var(--text-muted);
    font-size: 0.8125rem;
}

/* Collapsed-panel layout — narrow vertical strip with just the toggle
   and a "new chat" affordance. Driven by a class on .alma-agent-page so
   the grid columns can resize from the same selector. */
.alma-agent-page.history-collapsed {
    grid-template-columns: 56px minmax(0, 1fr) 320px;
}
@media (max-width: 1100px) {
    .alma-agent-page.history-collapsed {
        grid-template-columns: 56px minmax(0, 1fr) 280px;
    }
}
@media (max-width: 900px) {
    .alma-agent-page.history-collapsed {
        grid-template-columns: 56px minmax(0, 1fr);
    }
}
.alma-agent-history-collapsed-bar {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.5rem;
    padding: 0.875rem 0;
    height: 100%;
    box-sizing: border-box;
}
.alma-agent-history-collapsed-bar .alma-agent-history-expand-btn {
    /* Pin the expand button at the top so it sits on the same row as
       the platform header dividers on either side. The 14px top margin
       (12px panel padding + 2px tweak) lines the chevron up with the
       New chat button on the expanded version. */
    margin-top: 0;
}
.alma-agent-history-collapsed-bar .alma-agent-history-collapsed-new {
    width: 32px;
    height: 32px;
    border: none;
    border-radius: var(--radius-md);
    background: var(--accent);
    color: #fff;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    box-shadow: var(--shadow-1);
    transition: background var(--motion-fast) var(--ease),
                box-shadow var(--motion-fast) var(--ease);
}
.alma-agent-history-collapsed-bar .alma-agent-history-collapsed-new:hover {
    background: var(--accent-strong);
    box-shadow: var(--shadow-2);
}
.alma-agent-history-collapsed-bar .alma-agent-history-collapsed-new svg {
    width: 14px;
    height: 14px;
    stroke: currentColor;
    fill: none;
}
.alma-agent-history-list {
    flex: 1;
    overflow-y: auto;
    padding: 0.5rem 0.5rem 1rem;
    display: flex;
    flex-direction: column;
    gap: 2px;
}
.alma-agent-history-item {
    /* No background/color transition on the item itself: when the user
       clicks New chat, the previously-active session loses .is-active
       and would otherwise spend ~120ms fading from accent-muted back to
       transparent while the list reorders to make room for the
       prepended row. The combined paint + reflow read as a flash. With
       instant state changes (matching Claude / ChatGPT sidebar style)
       the new chat slots in cleanly. The delete button keeps its own
       fade-in transition. */
    display: flex;
    align-items: center;
    gap: 0.625rem;
    padding: 0.55rem 0.625rem;
    border-radius: var(--radius-md);
    color: var(--text);
    font-size: 0.875rem;
    cursor: pointer;
    min-width: 0;
}
.alma-agent-history-item:hover {
    background: var(--surface-sunk);
    color: var(--text-strong);
}
.alma-agent-history-item.is-active {
    background: var(--accent-muted);
    color: var(--accent-strong);
    font-weight: 600;
}
.alma-agent-history-item svg {
    width: 16px;
    height: 16px;
    stroke: currentColor;
    fill: none;
    flex-shrink: 0;
    opacity: 0.85;
}
.alma-agent-history-item.is-active svg { opacity: 1; }
.alma-agent-history-title {
    flex: 1;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.alma-agent-history-delete {
    /* Hover-only visibility. Showing it on .is-active too caused a flash:
       when the user clicks New chat, the previously-active row drops
       .is-active and its delete icon spent ~120ms transitioning from
       opacity 1 → 0, which the user sees as a flicker. Hover-only
       matches Claude/ChatGPT and removes the surface that could flash. */
    flex-shrink: 0;
    width: 24px;
    height: 24px;
    border: none;
    background: transparent;
    color: var(--text-faint);
    border-radius: var(--radius-sm);
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    opacity: 0;
    transition: background var(--motion-fast) var(--ease),
                color var(--motion-fast) var(--ease);
}
.alma-agent-history-item:hover .alma-agent-history-delete,
.alma-agent-history-item:focus-within .alma-agent-history-delete {
    opacity: 1;
}
.alma-agent-history-delete:hover {
    background: var(--danger-bg);
    color: var(--danger-strong);
}
.alma-agent-history-delete svg {
    width: 14px;
    height: 14px;
    stroke: currentColor;
    fill: none;
}

/* History footer — API key input lives here so it's always visible without
   competing for space with the main composer. */
.alma-agent-history-footer {
    border-top: 1px solid var(--border-faint);
    padding: 0.875rem 1rem 1rem;
    background: var(--surface);
}
.alma-agent-key-label {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    font-size: 0.75rem;
    font-weight: 600;
    color: var(--text-muted);
    text-transform: uppercase;
    letter-spacing: 0.05em;
    margin-bottom: 0.45rem;
}
.alma-agent-key-label svg {
    width: 13px;
    height: 13px;
    stroke: currentColor;
    fill: none;
}
.alma-agent-key-row {
    display: flex;
    align-items: center;
    gap: 4px;
    background: var(--surface-soft);
    border: 1.5px solid var(--border-strong);
    border-radius: var(--radius-md);
    transition: border-color var(--motion-fast) var(--ease),
                box-shadow var(--motion-fast) var(--ease);
}
.alma-agent-key-row:focus-within {
    border-color: var(--accent);
    box-shadow: 0 0 0 3px var(--accent-ring);
}
.alma-agent-key-input {
    flex: 1;
    min-width: 0;
    padding: 0.5rem 0.625rem;
    background: transparent;
    border: none;
    outline: none;
    color: var(--text-strong);
    font-family: var(--font-mono);
    font-size: 0.8125rem;
    letter-spacing: 0.02em;
}
.alma-agent-key-input::placeholder { color: var(--text-faint); }
.alma-agent-key-toggle {
    flex-shrink: 0;
    width: 30px;
    height: 30px;
    margin-right: 3px;
    border: none;
    background: transparent;
    color: var(--text-muted);
    border-radius: var(--radius-sm);
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: background var(--motion-fast) var(--ease),
                color var(--motion-fast) var(--ease);
}
.alma-agent-key-toggle:hover {
    background: var(--surface-sunk);
    color: var(--text-strong);
}
.alma-agent-key-toggle svg {
    width: 14px;
    height: 14px;
    stroke: currentColor;
    fill: none;
}
.alma-agent-key-hint {
    margin: 0.5rem 0 0;
    font-size: 0.7rem;
    color: var(--text-faint);
    line-height: 1.45;
}

/* ── Center panel: conversation ─────────────────────────────────────────── */
.alma-agent-main {
    display: flex;
    flex-direction: column;
    background: var(--bg);
    min-width: 0;
    min-height: 0;
}
.alma-agent-main-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    padding: 0 1.5rem;
    height: var(--topband-h);
    min-height: var(--topband-h);
    border-bottom: 1px solid var(--border-faint);
    background: var(--surface);
}
.alma-agent-main-title {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    font-size: 0.95rem;
    font-weight: 600;
    color: var(--text-strong);
    min-width: 0;
}
.alma-agent-main-title svg {
    width: 18px;
    height: 18px;
    stroke: var(--accent);
    fill: none;
    flex-shrink: 0;
}
.alma-agent-main-title span {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Rename trigger — looks like static title text, with a subtle hover hint
   that it's clickable. The negative left margin lets the hover background
   wrap a touch beyond the visible glyphs without shifting layout. */
.alma-agent-rename-trigger {
    margin: 0;
    padding: 0.2rem 0.4rem;
    margin-left: -0.4rem;
    background: transparent;
    border: 1px solid transparent;
    color: inherit;
    font: inherit;
    font-weight: 600;
    line-height: 1.3;
    text-align: left;
    cursor: text;
    border-radius: var(--radius-sm);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    min-width: 0;
    max-width: 100%;
    transition: background var(--motion-fast) var(--ease),
                border-color var(--motion-fast) var(--ease);
}
.alma-agent-rename-trigger:hover:not(:disabled) {
    background: var(--surface-sunk);
    border-color: var(--border-faint);
}
.alma-agent-rename-trigger:disabled {
    cursor: default;
    opacity: 0.7;
}

/* Inline rename input — same metrics as the trigger so the title doesn't
   jump in/out of edit mode. The accent ring marks the field as live. */
.alma-agent-rename-input {
    margin: 0;
    padding: 0.2rem 0.4rem;
    margin-left: -0.4rem;
    background: var(--surface);
    border: 1px solid var(--accent);
    color: var(--text-strong);
    font: inherit;
    font-weight: 600;
    line-height: 1.3;
    border-radius: var(--radius-sm);
    box-shadow: 0 0 0 3px var(--accent-ring);
    outline: none;
    min-width: 0;
    max-width: 100%;
    width: clamp(140px, 24rem, 100%);
}

.alma-agent-main-status {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    font-size: 0.75rem;
    color: var(--text-muted);
    flex-shrink: 0;
}
.alma-agent-status-dot {
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: #d97706;
    box-shadow: 0 0 0 3px rgba(217, 119, 6, 0.18);
}

/* Messages scroll area — flex grow so the composer pins to the bottom. */
.alma-agent-messages {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    padding: 1.5rem 0;
}
.alma-agent-messages-inner {
    max-width: 760px;
    margin: 0 auto;
    padding: 0 1.5rem;
    display: flex;
    flex-direction: column;
    gap: 1.25rem;
}

/* Empty state — vertically centred, low-key. */
.alma-agent-empty {
    height: 100%;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    text-align: center;
    padding: 2rem;
    color: var(--text-muted);
}
.alma-agent-empty-icon {
    width: 56px;
    height: 56px;
    border-radius: 50%;
    background: var(--accent-soft);
    color: var(--accent);
    display: flex;
    align-items: center;
    justify-content: center;
    margin-bottom: 1rem;
    box-shadow: var(--shadow-1);
}
.alma-agent-empty-icon svg {
    width: 26px;
    height: 26px;
    stroke: currentColor;
    fill: none;
}
.alma-agent-empty h3 {
    margin: 0 0 0.4rem;
    font-size: 1.125rem;
    font-weight: 700;
    color: var(--text-strong);
    letter-spacing: -0.01em;
}
.alma-agent-empty p {
    margin: 0;
    max-width: 380px;
    font-size: 0.9rem;
    line-height: 1.55;
}
.alma-agent-empty-hint {
    margin-top: 0.6rem !important;
    font-size: 0.8rem !important;
    color: var(--text-faint) !important;
}

/* Message bubbles — author avatar on the left, content beside it. We
   intentionally keep both user and assistant on the same axis so the
   reading flow stays vertical (no zig-zag). The colour difference does
   the speaker disambiguation. */
.alma-agent-msg {
    display: flex;
    gap: 0.875rem;
    align-items: flex-start;
}
.alma-agent-msg-avatar {
    flex-shrink: 0;
    width: 32px;
    height: 32px;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--surface);
    color: var(--text-muted);
    border: 1px solid var(--border-faint);
    box-shadow: var(--shadow-1);
}
.alma-agent-msg.is-assistant .alma-agent-msg-avatar {
    background: var(--accent-soft);
    color: var(--accent);
    border-color: var(--accent-ring);
}
.alma-agent-msg-avatar svg {
    width: 16px;
    height: 16px;
    stroke: currentColor;
    fill: none;
}
.alma-agent-msg-body {
    flex: 1;
    min-width: 0;
}
.alma-agent-msg-author {
    font-size: 0.78rem;
    font-weight: 600;
    color: var(--text-strong);
    margin-bottom: 0.25rem;
    letter-spacing: -0.005em;
}
.alma-agent-msg.is-assistant .alma-agent-msg-author { color: var(--accent-strong); }
.alma-agent-msg-content {
    font-size: 0.9375rem;
    line-height: 1.6;
    color: var(--text);
    white-space: pre-wrap;
    word-wrap: break-word;
}

/* Composer — pinned to the bottom of the conversation column. */
.alma-agent-composer {
    border-top: 1px solid var(--border-faint);
    padding: 0.875rem 1.5rem 1.25rem;
    background: var(--surface);
}
.alma-agent-composer-input {
    width: 100%;
    box-sizing: border-box;
    max-width: 760px;
    margin: 0 auto;
    display: block;
    resize: none;
    /* Default height fits ~2 lines comfortably so a single short prompt
       has room to breathe; max-height grows the box up to ~12 lines for
       longer prompts before the scrollbar takes over. The auto-resize
       effect in AlmaAgentPage sets inline height = scrollHeight on every
       draft change; CSS clamps both ends. */
    min-height: 64px;
    max-height: 320px;
    /* Extra right padding so the scrollbar thumb (when it appears) sits
       in its own gutter and never collides with the text caret or the
       border-radius corner. */
    padding: 0.75rem 0.875rem 0.75rem 1rem;
    background: var(--surface-soft);
    border: 1.5px solid var(--border-strong);
    border-radius: var(--radius-md);
    color: var(--text-strong);
    font-family: inherit;
    font-size: 0.9375rem;
    line-height: 1.5;
    outline: none;
    transition: border-color var(--motion-fast) var(--ease),
                box-shadow var(--motion-fast) var(--ease);
    /* Firefox: thin scrollbar that uses the textarea background, so the
       white default track no longer cuts into the rounded border. */
    scrollbar-width: thin;
    scrollbar-color: var(--border-strong) transparent;
}
.alma-agent-composer-input::placeholder { color: var(--text-faint); }
.alma-agent-composer-input:focus {
    border-color: var(--accent);
    box-shadow: 0 0 0 3px var(--accent-ring);
}
/* WebKit/Chromium: hide the white track and shrink the thumb. The 3px
   transparent border on the thumb fakes inner padding so it visually
   sits inside the textarea gutter rather than flush against the border. */
.alma-agent-composer-input::-webkit-scrollbar {
    width: 10px;
    height: 10px;
    background: transparent;
}
.alma-agent-composer-input::-webkit-scrollbar-track {
    background: transparent;
    /* Match the textarea radius so the track corner doesn't peek past
       the rounded border. */
    border-radius: var(--radius-md);
    margin: 4px 0;
}
.alma-agent-composer-input::-webkit-scrollbar-thumb {
    background-color: var(--border-strong);
    border-radius: 8px;
    border: 3px solid transparent;
    background-clip: padding-box;
    min-height: 32px;
}
.alma-agent-composer-input::-webkit-scrollbar-thumb:hover {
    background-color: var(--text-faint);
}
.alma-agent-composer-input::-webkit-scrollbar-corner {
    background: transparent;
}
.alma-agent-composer-actions {
    max-width: 760px;
    margin: 0.5rem auto 0;
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 0.5rem;
}
.alma-agent-composer-attach,
.alma-agent-composer-send {
    width: 36px;
    height: 36px;
    border-radius: var(--radius-md);
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    transition: background var(--motion-fast) var(--ease),
                color var(--motion-fast) var(--ease),
                border-color var(--motion-fast) var(--ease),
                box-shadow var(--motion-fast) var(--ease);
}
.alma-agent-composer-attach {
    background: transparent;
    color: var(--text-muted);
    border: 1px solid var(--border);
}
.alma-agent-composer-attach:hover {
    background: var(--surface-sunk);
    color: var(--text-strong);
    border-color: var(--border-strong);
}
.alma-agent-composer-send {
    background: var(--accent);
    color: #fff;
    border: none;
    box-shadow: var(--shadow-1);
}
.alma-agent-composer-send:hover:not(:disabled) {
    background: var(--accent-strong);
    box-shadow: var(--shadow-2);
}
.alma-agent-composer-send:disabled {
    background: var(--border-strong);
    cursor: not-allowed;
    box-shadow: none;
    opacity: 0.7;
}
.alma-agent-composer-attach svg,
.alma-agent-composer-send svg {
    width: 16px;
    height: 16px;
    stroke: currentColor;
    fill: none;
}

/* ── Right panel: uploaded files ────────────────────────────────────────── */
.alma-agent-files {
    display: flex;
    flex-direction: column;
    background: var(--surface);
    border-left: 1px solid var(--border);
    min-width: 0;
    transition: background var(--motion-fast) var(--ease),
                box-shadow var(--motion-fast) var(--ease);
}
.alma-agent-files.is-drag-over {
    background: var(--accent-soft);
    box-shadow: inset 0 0 0 2px var(--accent);
}
.alma-agent-files-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem;
    padding: 0 1rem;
    height: var(--topband-h);
    min-height: var(--topband-h);
    border-bottom: 1px solid var(--border-faint);
}
.alma-agent-files-title {
    display: flex;
    align-items: center;
    gap: 0.45rem;
    font-size: 0.9rem;
    font-weight: 600;
    color: var(--text-strong);
    min-width: 0;
}
.alma-agent-files-title svg {
    width: 16px;
    height: 16px;
    stroke: var(--accent);
    fill: none;
}
.alma-agent-files-count {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 22px;
    height: 20px;
    padding: 0 6px;
    background: var(--surface-sunk);
    color: var(--text-muted);
    border-radius: 10px;
    font-size: 0.72rem;
    font-weight: 600;
    margin-left: 0.2rem;
}
.alma-agent-files-add {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    padding: 0.4rem 0.7rem;
    background: var(--surface);
    border: 1px solid var(--border);
    color: var(--text);
    border-radius: var(--radius-md);
    font-size: 0.8125rem;
    font-weight: 500;
    cursor: pointer;
    transition: background var(--motion-fast) var(--ease),
                color var(--motion-fast) var(--ease),
                border-color var(--motion-fast) var(--ease);
}
.alma-agent-files-add:hover {
    background: var(--accent-soft);
    color: var(--accent);
    border-color: var(--accent-ring);
}
.alma-agent-files-add svg {
    width: 14px;
    height: 14px;
    stroke: currentColor;
    fill: none;
}
.alma-agent-files-list {
    flex: 1;
    overflow-y: auto;
    padding: 0.75rem 0.75rem 1rem;
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}
.alma-agent-files-empty {
    margin: auto;
    text-align: center;
    color: var(--text-muted);
    padding: 1.25rem 1rem;
}
.alma-agent-files-empty svg {
    width: 22px;
    height: 22px;
    stroke: currentColor;
    fill: none;
    opacity: 0.5;
    margin-bottom: 0.5rem;
}
.alma-agent-files-empty p {
    margin: 0;
    font-size: 0.85rem;
    line-height: 1.5;
}
.alma-agent-files-empty-hint {
    margin-top: 0.4rem !important;
    font-size: 0.75rem !important;
    color: var(--text-faint) !important;
}
.alma-agent-file {
    display: flex;
    align-items: center;
    gap: 0.625rem;
    padding: 0.55rem 0.625rem;
    background: var(--surface-soft);
    border: 1px solid var(--border-faint);
    border-radius: var(--radius-md);
    transition: border-color var(--motion-fast) var(--ease),
                box-shadow var(--motion-fast) var(--ease);
}
.alma-agent-file:hover {
    border-color: var(--border-strong);
    box-shadow: var(--shadow-1);
}
.alma-agent-file-icon {
    flex-shrink: 0;
    width: 32px;
    height: 32px;
    border-radius: var(--radius-sm);
    background: var(--surface);
    color: var(--accent);
    display: flex;
    align-items: center;
    justify-content: center;
    border: 1px solid var(--border-faint);
}
.alma-agent-file-icon svg {
    width: 16px;
    height: 16px;
    stroke: currentColor;
    fill: none;
}
.alma-agent-file-meta {
    flex: 1;
    min-width: 0;
}
.alma-agent-file-name {
    font-size: 0.8125rem;
    font-weight: 500;
    color: var(--text-strong);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.alma-agent-file-sub {
    margin-top: 0.15rem;
    display: flex;
    align-items: center;
    gap: 0.35rem;
    font-size: 0.72rem;
    color: var(--text-muted);
}
.alma-agent-file-kind {
    text-transform: uppercase;
    letter-spacing: 0.05em;
    font-weight: 600;
    color: var(--text-faint);
}
.alma-agent-file-remove {
    flex-shrink: 0;
    width: 26px;
    height: 26px;
    border: none;
    background: transparent;
    color: var(--text-faint);
    border-radius: var(--radius-sm);
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    opacity: 0;
    transition: opacity var(--motion-fast) var(--ease),
                background var(--motion-fast) var(--ease),
                color var(--motion-fast) var(--ease);
}
.alma-agent-file:hover .alma-agent-file-remove { opacity: 1; }
.alma-agent-file-remove:hover {
    background: var(--danger-bg);
    color: var(--danger-strong);
}
.alma-agent-file-remove svg {
    width: 13px;
    height: 13px;
    stroke: currentColor;
    fill: none;
}

/* ── Responsive: collapse the side panels on narrow screens ─────────────── */
@media (max-width: 1100px) {
    .alma-agent-page {
        grid-template-columns: 240px minmax(0, 1fr) 280px;
    }
}
@media (max-width: 900px) {
    /* Drop the right files panel — keep history + main. The composer's
       paperclip button still adds files. The platform sidebar slides in
       as a drawer on mobile, so the page anchors to the left edge and
       sits below the mobile topbar. */
    .alma-agent-page {
        top: 53px;
        left: 0;
        grid-template-columns: 220px minmax(0, 1fr);
    }
    .alma-agent-files { display: none; }
}
@media (max-width: 640px) {
    /* Drop the history panel too — single-column chat. The new-chat /
       history is still reachable via the main header on tap. */
    .alma-agent-page {
        grid-template-columns: minmax(0, 1fr);
    }
    .alma-agent-history { display: none; }
}

/* ── Demo controls ─────────────────────────────────────────────────────────
   Small subtle controls at the top of the history footer. They sit above
   the API key block so the user can reset between recording takes
   without scrolling, but the muted typography keeps them out of the way
   during normal use. */
.alma-agent-demo-controls {
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
    margin-bottom: 0.875rem;
    padding-bottom: 0.875rem;
    border-bottom: 1px dashed var(--border-faint);
}
.alma-agent-demo-controls-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.4rem;
    /* Negative right margin pulls the hide button into the trailing edge
       so the "Demo" label still anchors flush with the buttons below. */
    margin-right: -0.25rem;
}
.alma-agent-demo-label {
    font-size: 0.7rem;
    font-weight: 600;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--text-faint);
}
.alma-agent-demo-hide {
    width: 22px;
    height: 22px;
    border: none;
    background: transparent;
    color: var(--text-faint);
    border-radius: var(--radius-sm);
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: background var(--motion-fast) var(--ease),
                color var(--motion-fast) var(--ease);
}
.alma-agent-demo-hide:hover {
    background: var(--surface-sunk);
    color: var(--text-strong);
}
.alma-agent-demo-hide svg {
    width: 13px;
    height: 13px;
    stroke: currentColor;
    fill: none;
}

/* "Show demo controls" replacement — appears in place of the controls
   block when the user has hidden them. Muted text-button styling so it
   reads as a low-priority utility rather than a primary action. */
.alma-agent-demo-show {
    display: flex;
    align-items: center;
    gap: 0.35rem;
    padding: 0.3rem 0.45rem;
    margin-bottom: 0.875rem;
    margin-left: -0.45rem;
    background: transparent;
    border: none;
    color: var(--text-faint);
    font-size: 0.72rem;
    font-weight: 500;
    cursor: pointer;
    border-radius: var(--radius-sm);
    transition: background var(--motion-fast) var(--ease),
                color var(--motion-fast) var(--ease);
}
.alma-agent-demo-show:hover {
    background: var(--surface-sunk);
    color: var(--text-strong);
}
.alma-agent-demo-show svg {
    width: 12px;
    height: 12px;
    stroke: currentColor;
    fill: none;
}
.alma-agent-demo-btn {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    padding: 0.35rem 0.55rem;
    background: var(--surface-soft);
    border: 1px solid var(--border-faint);
    border-radius: var(--radius-sm);
    color: var(--text);
    font-size: 0.78rem;
    font-weight: 500;
    cursor: pointer;
    transition: background var(--motion-fast) var(--ease),
                color var(--motion-fast) var(--ease),
                border-color var(--motion-fast) var(--ease);
}
.alma-agent-demo-btn:hover {
    background: var(--surface);
    border-color: var(--border-strong);
    color: var(--text-strong);
}
.alma-agent-demo-btn svg {
    width: 13px;
    height: 13px;
    stroke: currentColor;
    fill: none;
    flex-shrink: 0;
}
.alma-agent-demo-btn-danger:hover {
    background: var(--danger-bg);
    color: var(--danger-strong);
    border-color: var(--danger-bg);
}

/* ── Embedded demo cards inside assistant messages ─────────────────────────
   These cards sit below the assistant text in MessageBubble. Both are
   constrained to ~480px wide so they don't dominate the conversation
   column on wide displays. */

.alma-agent-structure-card,
.alma-agent-workflow-card {
    margin-top: 0.625rem;
    max-width: 480px;
    border-radius: var(--radius-md);
    overflow: hidden;
    box-shadow: var(--shadow-1);
}

/* Structure card — dark header + viewer to read like a 3D viewer panel. */
.alma-agent-structure-card {
    background: #0b1220;
    border: 1px solid #1f2937;
    color: #e2e8f0;
}
.alma-agent-structure-header {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.5rem 0.75rem;
    background: linear-gradient(180deg, rgba(255,255,255,0.04), rgba(255,255,255,0.0));
    border-bottom: 1px solid rgba(148, 163, 184, 0.18);
    font-size: 0.8125rem;
    font-weight: 600;
}
.alma-agent-structure-header svg {
    width: 14px;
    height: 14px;
    stroke: #93c5fd;
    fill: none;
    flex-shrink: 0;
}
.alma-agent-structure-name {
    flex: 1;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    color: #e2e8f0;
    font-family: var(--font-mono);
    font-size: 0.78rem;
    font-weight: 500;
    letter-spacing: 0.01em;
}
.alma-agent-structure-badge {
    padding: 1px 6px;
    background: rgba(59, 130, 246, 0.18);
    color: #93c5fd;
    border-radius: 4px;
    font-size: 0.65rem;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
}
.alma-agent-structure-viewer {
    position: relative;
    width: 100%;
    height: 280px;
    background: #0b1220;
    /* 3Dmol mounts a canvas inside this div. Hide the mouse cursor
       hint until interaction starts. */
    cursor: grab;
}
.alma-agent-structure-viewer:active { cursor: grabbing; }
.alma-agent-structure-fallback {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
    color: #64748b;
    font-size: 0.85rem;
}
.alma-agent-structure-fallback svg {
    width: 32px;
    height: 32px;
    stroke: currentColor;
    fill: none;
    opacity: 0.6;
}
.alma-agent-structure-footer {
    padding: 0.45rem 0.75rem;
    background: rgba(255, 255, 255, 0.03);
    border-top: 1px solid rgba(148, 163, 184, 0.14);
    color: #94a3b8;
    font-size: 0.72rem;
    letter-spacing: 0.01em;
}

/* Workflow card — light surface; reads as a status panel. */
.alma-agent-workflow-card {
    background: var(--surface);
    border: 1px solid var(--border);
    color: var(--text);
}
.alma-agent-workflow-header {
    display: flex;
    align-items: center;
    gap: 0.45rem;
    padding: 0.55rem 0.75rem;
    background: var(--surface-soft);
    border-bottom: 1px solid var(--border-faint);
    font-size: 0.8125rem;
    font-weight: 600;
    color: var(--text-strong);
}
.alma-agent-workflow-header svg {
    width: 14px;
    height: 14px;
    stroke: var(--accent);
    fill: none;
}
.alma-agent-workflow-steps {
    padding: 0.65rem 0.75rem;
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
}
.alma-agent-workflow-step {
    display: flex;
    align-items: center;
    gap: 0.55rem;
    padding: 0.25rem 0;
    font-size: 0.84rem;
    color: var(--text-muted);
    transition: color var(--motion-fast) var(--ease);
}
.alma-agent-workflow-step.is-running { color: var(--text-strong); font-weight: 500; }
.alma-agent-workflow-step.is-done    { color: var(--text); }
.alma-agent-workflow-step-icon {
    width: 18px;
    height: 18px;
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: center;
}
.alma-agent-workflow-step-icon svg {
    width: 14px;
    height: 14px;
    stroke: currentColor;
    fill: none;
}
.alma-agent-workflow-step.is-done .alma-agent-workflow-step-icon { color: #16a34a; }
.alma-agent-workflow-step.is-running .alma-agent-workflow-step-icon { color: var(--accent); }
.alma-agent-workflow-step.is-running .alma-agent-workflow-step-icon svg {
    animation: alma-agent-spin 0.9s linear infinite;
}
.alma-agent-workflow-step-dot {
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: var(--border-strong);
}
.alma-agent-workflow-step-label {
    flex: 1;
    min-width: 0;
}
.alma-agent-workflow-success {
    display: flex;
    align-items: flex-start;
    gap: 0.5rem;
    padding: 0.6rem 0.75rem;
    border-top: 1px solid var(--border-faint);
    background: #f0fdf4;
    color: #15803d;
    font-size: 0.84rem;
    line-height: 1.5;
}
.alma-agent-workflow-success svg {
    width: 14px;
    height: 14px;
    stroke: currentColor;
    fill: none;
    flex-shrink: 0;
    margin-top: 0.15rem;
}

@keyframes alma-agent-spin {
    from { transform: rotate(0deg); }
    to   { transform: rotate(360deg); }
}

/* Spinner inside the structure-card "Loading structure…" placeholder.
   Scoped to is-loading so the molecule icon used by the error /
   unsupported fallbacks doesn't spin. */
.alma-agent-structure-fallback.is-loading > svg:first-child {
    animation: alma-agent-spin 0.9s linear infinite;
    opacity: 0.85;
}

/* ── Typing indicator ──────────────────────────────────────────────────────
   Three bouncing dots in an assistant-row layout. Sits in the message
   stream while a scripted demo reply is queued. */
.alma-agent-typing-dots {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    padding: 8px 0 4px;
    height: 18px;
}
.alma-agent-typing-dots span {
    display: block;
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--text-faint);
    animation: alma-agent-typing 1.3s infinite ease-in-out;
}
.alma-agent-typing-dots span:nth-child(2) { animation-delay: 0.18s; }
.alma-agent-typing-dots span:nth-child(3) { animation-delay: 0.36s; }
@keyframes alma-agent-typing {
    0%, 70%, 100% { opacity: 0.3; transform: translateY(0); }
    35%           { opacity: 1;   transform: translateY(-3px); }
}

/* ── Streaming text cursor ────────────────────────────────────────────────
   A blinking caret-like block at the end of streaming assistant text so
   the reveal reads as a live cursor instead of a wandering edge. */
.alma-agent-msg-cursor {
    display: inline-block;
    width: 2px;
    height: 1em;
    margin-left: 2px;
    vertical-align: text-bottom;
    background: var(--accent);
    animation: alma-agent-cursor-blink 1s steps(2, start) infinite;
}
@keyframes alma-agent-cursor-blink {
    to { visibility: hidden; }
}

/* ── Add Model: cards + cell + header badge ───────────
   The model workflow now renders inline inside the Tools drawer's
   "Add Model" card; these card/cell/badge styles are reused there.
   Visual goals:
     · Model cards show a clear "configured / pending" state so
       users can see at a glance which models are ready to run.
     · Picker buttons have the same affordance as buttons in
       other studio drawers (subtle border, accent on hover).
     · Model column headers are visually distinct from data columns
       — soft accent tint + a tiny "M" badge — so it's obvious which
       columns are computed by an attached model.
     · The Run button in each cell is small and centered; it inherits
       the .btn affordance hierarchy without a separate variant. */
/* Active-model cards — compact two-row layout (name/type, then
   status + icon actions) so each added model reads consistently with
   the other Tools cards and the actions never wrap. */
.studio-models-list {
    display: flex; flex-direction: column;
    gap: 0.5rem;
    padding: 0 1.125rem 1rem;
}
.studio-model-card {
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-md);
    background: var(--surface);
    padding: 0.55rem 0.65rem;
    display: flex; flex-direction: column;
    gap: 0.4rem;
}
.studio-model-card-ready {
    border-color: var(--accent-ring);
    background: linear-gradient(180deg, var(--accent-soft) 0%, var(--surface) 60%);
}
.studio-model-card-head {
    display: flex; align-items: baseline;
    justify-content: space-between;
    gap: 0.5rem;
}
.studio-model-card-name {
    font-size: 0.85rem; font-weight: 600; color: var(--text-strong);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.studio-model-card-type {
    flex-shrink: 0;
    font-size: 0.66rem;
    color: var(--text-muted);
    text-transform: uppercase; letter-spacing: 0.04em;
}
.studio-model-card-foot {
    display: flex; align-items: center; justify-content: space-between; gap: 0.5rem;
}
.studio-model-card-status {
    font-size: 0.74rem;
    color: var(--text-muted);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.studio-model-card-status.is-ready { color: var(--accent-strong); }
.studio-model-card-status.is-pending { color: var(--warning-strong, #b45309); }
.studio-model-card-actions {
    display: flex; gap: 0.3rem; flex-shrink: 0; flex-wrap: nowrap;
}
/* Picker cards. Sized as a one-column grid (instead of a flex
   column) so each card lands on the same row track and inherits the
   tallest member's height — adding more types later doesn't require
   per-card min-height tweaks. align-items: stretch (the grid
   default) makes every card fill its row, giving the bar a clean
   visual rhythm even when descriptions vary in length.

   Inside each card the content uses a flex column with the title
   pinned to the top and the description allowed to grow into the
   remaining space; padding is symmetric so the visible "card body"
   reads as a fixed shape regardless of content. */
.studio-models-picker {
    display: grid;
    grid-template-columns: 1fr;
    gap: 0.5rem;
    padding: 0 1.125rem 1.125rem;
    align-items: stretch;
}
.studio-model-picker-card {
    display: flex;
    flex-direction: column;
    gap: 2px;
    text-align: left;
    background: var(--surface);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-md);
    /* Compact: sized to content (title + short description), no fixed
       min-height that padded the list out and made it feel tall. */
    padding: 0.4rem 0.6rem;
    cursor: pointer;
    font: inherit;
    color: inherit;
    transition: border-color var(--motion-fast) var(--ease),
                background var(--motion-fast) var(--ease),
                box-shadow var(--motion-fast) var(--ease);
}
.studio-model-picker-card:hover {
    border-color: var(--accent-ring);
    background: var(--accent-soft);
    box-shadow: 0 1px 2px rgba(15, 23, 42, 0.06);
}
.studio-model-picker-card:focus-visible {
    outline: none;
    border-color: var(--accent-ring);
    box-shadow: 0 0 0 3px var(--accent-ring);
}
.studio-model-picker-name {
    font-weight: 600;
    color: var(--text-strong);
    font-size: 0.85rem;
    line-height: 1.25;
}
.studio-model-picker-desc {
    font-size: 0.74rem;
    color: var(--text-muted);
    line-height: 1.3;
    /* Clamp to one line so the picker stays short; the full text is in
       the tooltip via the title attribute on the parent button. */
    display: -webkit-box;
    -webkit-line-clamp: 1;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

/* ── Tools drawer (Properties / Scaffolds / Add Model cards) ── */
.studio-drawer-tools { padding: 0; }
/* Cards column — top padding gives the first card breathing room below
   the drawer header. Fills the remaining drawer height and scrolls, so when
   every tool section is expanded the lower options stay reachable instead of
   being clipped off the bottom. */
.studio-tool-cards {
    padding: 0.75rem 0 0.3rem;
    flex: 1 1 auto; min-height: 0;
    overflow-y: auto; overscroll-behavior: contain;
}
.studio-tool-card {
    /* Symmetric 1.125rem left/right by default so cards are balanced when the
       list fits. When it overflows, `.studio-tool-cards.has-vscroll` (set by
       useVScrollPadding) tightens the right margin to 0.5rem so the gap to the
       scrollbar isn't oversized. Left always stays aligned with the header. */
    margin: 0 1.125rem 0.7rem 1.125rem;
    /* Slightly stronger border so each card reads as a distinct surface
       without looking heavy. */
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-md);
    background: var(--surface);
    overflow: hidden;
}
.studio-tool-cards.has-vscroll .studio-tool-card { margin-right: 0.5rem; }
/* Card header: a clickable collapse toggle (chevron-left) that fills the
   row, with optional compact action icons pinned to the right. The hover
   tint sits on the WHOLE head row (toggle + action area) so it reads as
   one cohesive interactive row. The card's overflow:hidden + radius clips
   the tint to the card corners — so a COLLAPSED header is fully rounded,
   while an EXPANDED header is top-rounded only and the body connects
   squarely below it (no awkward bottom rounding). */
.studio-tool-card-head {
    display: flex; align-items: center; gap: 0.25rem;
    min-height: 40px;
    padding-right: 0.5rem;
    transition: background var(--motion-fast) var(--ease);
}
.studio-tool-card-head:hover { background: var(--hover-bg); }
.studio-tool-card-toggle {
    display: flex; align-items: center; gap: 0.4rem; flex: 1; min-width: 0;
    padding: 0.6rem 0.5rem 0.6rem 0.7rem;
    background: transparent; border: none; cursor: pointer;
    font: inherit; text-align: left; color: var(--text-strong);
}
.studio-tool-card-chevron {
    flex-shrink: 0; color: var(--text-faint);
    transition: transform var(--motion-fast) var(--ease);
}
.studio-tool-card-chevron.is-open { transform: rotate(90deg); }
.studio-tool-card-body { padding: 0.5rem 0.7rem 0.6rem; }
.studio-tool-card-title {
    min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    font-weight: 600; color: var(--text-strong); font-size: 0.9rem;
}
/* Inline model workflow lives in the card body — neutralise the drawer
   padding the picker/list carried from the old standalone drawer. */
.studio-tool-card-body .studio-models-picker,
.studio-tool-card-body .studio-models-list { padding: 0; }

.studio-tool-cat-count {
    flex-shrink: 0; font-size: 0.66rem; font-weight: 700;
    color: var(--accent-fg); background: var(--accent);
    border-radius: 999px; padding: 0.02rem 0.35rem;
}
.studio-tool-cat-actions { display: flex; align-items: center; gap: 0.25rem; flex-shrink: 0; }
/* Compact icon action buttons (Add all / Add selected). */
.studio-tool-icon-btn {
    display: inline-flex; align-items: center; justify-content: center;
    width: 26px; height: 26px; padding: 0; flex-shrink: 0;
    border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
    background: var(--surface); color: var(--text-muted); cursor: pointer;
    transition: background var(--motion-fast) var(--ease),
                border-color var(--motion-fast) var(--ease),
                color var(--motion-fast) var(--ease);
}
.studio-tool-icon-btn:hover:not(:disabled) { border-color: var(--accent); color: var(--text-strong); background: var(--surface-sunk); }
.studio-tool-icon-btn.is-primary { background: var(--accent); border-color: var(--accent); color: var(--accent-fg); }
.studio-tool-icon-btn.is-primary:hover { background: var(--accent-strong); border-color: var(--accent-strong); color: var(--accent-fg); }
/* Subtle "add" affordance (Function / Visualization section headers) — a
   ghost-accent OUTLINE (no filled background), so it reads as the section's
   add action without the abruptness of the filled .is-primary style. */
.studio-tool-icon-btn.studio-tool-add-btn {
    background: transparent;
    border-color: var(--accent-ring);
    color: var(--accent-strong);
}
.studio-tool-icon-btn.studio-tool-add-btn:hover:not(:disabled) {
    background: var(--accent-soft);
    border-color: var(--accent);
    color: var(--accent-strong);
}
.studio-tool-icon-btn.is-danger:hover:not(:disabled) { border-color: var(--danger-strong); color: var(--danger-strong); background: var(--danger-bg); }
.studio-tool-icon-btn:disabled { opacity: 0.4; cursor: default; }
.studio-tool-desc-list { display: flex; flex-direction: column; padding: 0.1rem 0.55rem 0.55rem; }
.studio-tool-desc-row {
    display: flex; align-items: center; gap: 0.5rem;
    padding: 0.3rem 0.35rem; border-radius: var(--radius-sm);
    font-size: 0.85rem; color: var(--text); cursor: pointer;
    /* Prevent text-highlighting while shift-clicking to range-select. */
    user-select: none; -webkit-user-select: none;
}
.studio-tool-desc-row:hover { background: var(--hover-bg); }
.studio-tool-desc-row input[type="checkbox"] { accent-color: var(--accent); cursor: pointer; flex-shrink: 0; }
.studio-tool-desc-row.is-added { color: var(--text-muted); cursor: default; }
.studio-tool-desc-row.is-added:hover { background: transparent; }
.studio-tool-desc-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.studio-tool-desc-added {
    flex-shrink: 0; font-size: 0.68rem; font-weight: 600;
    color: var(--accent-strong); background: var(--accent-soft);
    border-radius: 999px; padding: 0.05rem 0.4rem;
}
.studio-tool-empty { font-size: 0.78rem; color: var(--text-muted); padding: 0.35rem 0 0.55rem; }
/* Short helper text inside a tool card (e.g. the Chemical Space card). */
.studio-tool-hint { font-size: 0.74rem; color: var(--text-muted); line-height: 1.4; padding: 0 0 8px; }
/* Chemical Space workspace: a thin "nearby = similar" hint above the chart,
   the settings-panel helper line, and per-field "not installed" notes. */
/* Left padding (10px) matches .viz-settings margin / .viz-split padding so the
   note shares the plot/settings left edge instead of sitting flush to the rail. */
.viz-cs-hint { flex: 0 0 auto; font-size: 0.72rem; color: var(--text-muted); padding: 4px 10px 6px; }
.viz-cs-hint.is-warn { color: var(--warning, #b45309); display: flex; align-items: flex-start; gap: 8px; }
.viz-cs-hint.is-warn > span { flex: 1 1 auto; }
.viz-cs-hint-close {
    flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center;
    width: 18px; height: 18px; margin-top: -1px; padding: 0;
    color: inherit; background: transparent; border: none; border-radius: 3px;
    opacity: 0.7; cursor: pointer;
}
.viz-cs-hint-close:hover { opacity: 1; background: rgba(180, 83, 9, 0.12); }
/* Inline "Retry" inside the recompute-error hint — a compact text affordance. */
.viz-cs-retry-inline {
    flex: 0 0 auto; width: auto; padding: 1px 8px; margin-top: -1px;
    font-size: 0.7rem; font-weight: 700; opacity: 1;
    border: 1px solid currentColor; border-radius: var(--radius-sm, 6px); background: transparent; cursor: pointer;
}
.viz-cs-retry-inline:hover { background: rgba(180, 83, 9, 0.12); }
.viz-cs-fieldhint { display: block; margin-top: 3px; font-size: 0.66rem; line-height: 1.3; color: var(--warning, #b45309); }

/* ── Chemical Space analysis panel (cluster tabs + mini-sheet table) ── */
.viz-cs-summary { display: flex; flex-direction: column; gap: 8px; height: 100%; min-height: 0; }
/* Cluster tab bar — single-row horizontal scroller, like the sheet tabs. Never
   wraps; a thin styled scrollbar is the overflow affordance when many clusters. */
.viz-cs-tabbar {
    flex: 0 0 auto; display: flex; gap: 2px;
    overflow-x: auto; overflow-y: hidden;
    border-bottom: 1px solid var(--border);
    scroll-behavior: smooth;
    scrollbar-width: thin;
    scrollbar-color: var(--border-strong) transparent;
}
.viz-cs-tabbar::-webkit-scrollbar { height: 6px; }
.viz-cs-tabbar::-webkit-scrollbar-track { background: transparent; }
.viz-cs-tabbar::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 3px; }
.viz-cs-tabbar::-webkit-scrollbar-thumb:hover { background: var(--text-faint); }
.viz-cs-tab {
    flex: 0 0 auto; display: inline-flex; align-items: center; gap: 5px;
    padding: 5px 10px;
    border: 1px solid transparent; border-bottom: none;
    border-radius: var(--radius-sm) var(--radius-sm) 0 0;
    background: transparent; color: var(--text-muted);
    font-size: 0.76rem; white-space: nowrap; cursor: pointer;
    transition: background var(--motion-fast) var(--ease), color var(--motion-fast) var(--ease);
}
.viz-cs-tab:hover { background: var(--surface-soft); color: var(--text); }
/* Active tab: stronger border + an accent top bar (inset shadow, no layout
   shift) so the selected cluster reads clearly against the bar. */
.viz-cs-tab.is-active { background: var(--surface); color: var(--text-strong); font-weight: 600; border-color: var(--border-strong); box-shadow: inset 0 2px 0 var(--accent); margin-bottom: -1px; }
.viz-cs-tab-dot { width: 9px; height: 9px; border-radius: 2px; flex: 0 0 auto; }
.viz-cs-tab-n { color: var(--text-faint); font-variant-numeric: tabular-nums; font-size: 0.7rem; }
.viz-cs-tab.is-active .viz-cs-tab-n { color: var(--text-muted); }
/* Mini-sheet table for the active cluster. */
.viz-cs-table { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; border: 1px solid var(--border); border-radius: var(--radius-sm); overflow: hidden; }
.viz-cs-table-toolbar { flex: 0 0 auto; display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 5px 8px; border-bottom: 1px solid var(--border); background: var(--surface-soft); }
.viz-cs-table-title { font-size: 0.74rem; font-weight: 600; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.viz-cs-colsel { position: relative; flex: 0 0 auto; }
.viz-cs-colsel-btn { display: inline-flex; align-items: center; gap: 4px; padding: 2px 8px; font-size: 0.72rem; color: var(--text-muted); background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-sm); cursor: pointer; }
.viz-cs-colsel-btn:hover { color: var(--text-strong); border-color: var(--border-strong); }
.viz-cs-colsel-menu { position: absolute; right: 0; top: calc(100% + 4px); z-index: 30; min-width: 150px; max-height: 240px; overflow-y: auto; padding: 4px; background: var(--surface); border: 1px solid var(--border-strong); border-radius: var(--radius-sm); box-shadow: 0 6px 20px rgba(15, 30, 45, 0.16); }
.viz-cs-colsel-item { display: flex; align-items: center; gap: 6px; padding: 4px 6px; font-size: 0.78rem; color: var(--text); border-radius: 3px; cursor: pointer; }
.viz-cs-colsel-item:hover { background: var(--surface-soft); }
.viz-cs-grid { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
.viz-cs-grid-head { flex: 0 0 auto; display: grid; align-items: center; gap: 8px; padding: 4px 8px; background: var(--surface-sunk); border-bottom: 1px solid var(--border); font-size: 0.62rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.03em; color: var(--text-muted); }
.viz-cs-grid-head > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.viz-cs-grid-body { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
.viz-cs-grow { display: grid; align-items: center; gap: 8px; width: 100%; padding: 3px 8px; text-align: left; background: transparent; border: none; border-bottom: 1px solid var(--border-faint); cursor: pointer; color: var(--text); font-size: 0.78rem; transition: background var(--motion-fast) var(--ease); }
.viz-cs-grow:hover { background: var(--surface-soft); }
.viz-cs-grow.is-sel { background: var(--accent-soft); }
/* The single active (clicked) compound — stronger emphasis than .is-sel
   (which marks any brushed/listed pick). Border keeps it readable without
   shifting the row layout. */
.viz-cs-grow.is-active { background: var(--accent-soft); box-shadow: inset 2px 0 0 var(--accent); }
.viz-cs-gc { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-variant-numeric: tabular-nums; }
/* Cluster cell — just the number, tinted in the cluster palette colour so it
   reads as a coloured tag while staying flush-left with the "Cluster" header
   (the previous dot+number badge pushed the digits ~14px right). */
.viz-cs-cl-num { font-weight: 600; font-variant-numeric: tabular-nums; }
.viz-cs-grid-more { padding: 8px; font-size: 0.74rem; color: var(--text-muted); font-style: italic; }
.viz-cs-empty { flex: 1 1 auto; display: flex; align-items: center; justify-content: center; padding: 20px; font-size: 0.78rem; color: var(--text-muted); font-style: italic; text-align: center; }

/* ── Scaffold Navigator result panel (analysis right-rail) ──
   Reuses the .studio-analysis-panel card shell; spans the full rail height
   (over the stacked plot/metric panels) while active. */
.studio-analysis-side > .viz-scaffold-panel { grid-row: 1 / -1; }
.viz-scaffold-head { flex-direction: row; align-items: flex-start; gap: 8px; }
.viz-scaffold-head-text { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.viz-scaffold-summary { display: flex; flex-wrap: wrap; gap: 6px 14px; font-size: 0.76rem; color: var(--text-muted); }
.viz-scaffold-summary b { color: var(--text-strong); font-variant-numeric: tabular-nums; }
.viz-scaffold-trunc { color: var(--accent-strong); }
.viz-scaffold-tiles {
    display: grid; grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); gap: 8px;
    /* 1px breathing room so an edge tile's active border isn't clipped by the
       scroll container's overflow. */
    padding: 1px;
}
.viz-scaffold-tile {
    display: flex; flex-direction: column; align-items: center; gap: 4px; padding: 6px;
    background: var(--surface); border: 1px solid var(--border-strong); border-radius: var(--radius-md);
    cursor: pointer; transition: border-color 0.12s, background 0.12s; min-width: 0;
}
.viz-scaffold-tile:hover { border-color: var(--accent-ring); }
/* Active state is a uniform 1px accent border + soft tint (no extra box-shadow
   ring, which read as inconsistent thickness and clipped on edge tiles). */
.viz-scaffold-tile.is-active { border-color: var(--accent-strong); background: var(--accent-soft); }
.viz-scaffold-acyclic {
    display: flex; align-items: center; justify-content: center; width: 80px; height: 80px;
    font-size: 0.72rem; color: var(--text-muted); font-style: italic;
    background: var(--surface-sunk); border-radius: var(--radius-sm);
}
.viz-scaffold-tile-meta { display: flex; align-items: center; gap: 6px; width: 100%; justify-content: center; }
.viz-scaffold-tile-name { font-size: 0.74rem; color: var(--text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Tiles take ~2/3 of the rail and scroll internally; the detail card below
   takes the remaining ~1/3 and scrolls its compound list on its own — so the
   panel never grows the rail no matter how many groups / compounds there are. */
.viz-scaffold-tiles-scroll { flex: 2 1 0; }
.viz-scaffold-detail {
    flex: 1 1 0; min-height: 96px;
    display: flex; flex-direction: column; gap: 8px; padding: 10px;
    background: var(--surface-soft); border: 1px solid var(--border-strong); border-radius: var(--radius-md);
}
.viz-scaffold-detail-head { display: flex; align-items: center; gap: 8px; flex: 0 0 auto; }
.viz-scaffold-detail-title { font-size: 0.82rem; font-weight: 600; color: var(--text-strong); flex: 1 1 auto; }
.viz-scaffold-detail-smiles {
    display: block; flex: 0 0 auto;
    font-family: var(--font-mono, monospace); font-size: 0.72rem; color: var(--text-strong);
    /* A clean white pill on the soft-gray detail card — subtle, no left accent. */
    background: var(--surface); border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
    padding: 4px 8px; overflow-x: auto; white-space: nowrap;
}
/* The compound list is the shared .viz-cs-grid table (ID + SMILES); it fills
   the detail card's remaining height and scrolls its own body. A white surface
   + stronger border makes the table distinct from the soft-gray detail card. */
.viz-scaffold-cmpd-grid {
    flex: 1 1 auto; min-height: 0; overflow: hidden;
    background: var(--surface); border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
}

/* ── Shared med-chem rail result panels (Scaffold / Alerts / Cliffs) ──
   Common chrome + the two-region (scroll + detail) body layout that keeps the
   panel bounded inside the analysis rail. */
.studio-analysis-side > .viz-mc-panel { grid-row: 1 / -1; }
.viz-mc-head { flex-direction: row; align-items: flex-start; gap: 8px; }
.viz-mc-head-text { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.viz-mc-close {
    flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center;
    width: 24px; height: 24px; padding: 0; border: 1px solid transparent;
    background: transparent; color: var(--text-muted); border-radius: var(--radius-sm); cursor: pointer;
}
.viz-mc-close:hover { background: var(--surface-sunk); color: var(--text-strong); border-color: var(--border-faint); }
/* Header Edit/Settings button — same compact affordance as the close button. */
.viz-mc-edit {
    flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center;
    width: 24px; height: 24px; padding: 0; border: 1px solid transparent;
    background: transparent; color: var(--text-muted); border-radius: var(--radius-sm); cursor: pointer;
}
.viz-mc-edit:hover { background: var(--accent-soft); color: var(--accent-strong); border-color: var(--accent-ring); }
.viz-mc-edit.is-danger:hover { background: var(--danger-bg); color: var(--danger-strong); border-color: var(--danger-bg); }
/* Bounded body: fixed header rows + flexible scroll region(s), nothing grows
   the rail. (Overrides the centered base .studio-analysis-panel-body.) */
.viz-mc-body {
    align-items: stretch; justify-content: flex-start; flex-direction: column;
    gap: 10px; overflow: hidden; min-height: 0;
}
.viz-mc-scroll { min-height: 0; overflow-y: auto; flex: 1 1 0; }
.viz-mc-note { font-size: 0.74rem; color: var(--accent-strong); font-weight: 500; }
.viz-mc-note.is-warn { color: var(--warning, #b45309); }
/* Compact, non-dominant "Select in sheet" / "Select pair" action. */
.viz-mc-selbtn {
    flex: 0 0 auto; padding: 2px 9px; font-size: 0.72rem; font-weight: 500; white-space: nowrap;
    color: var(--accent-strong); background: var(--accent-soft);
    border: 1px solid var(--accent-ring); border-radius: var(--radius-sm); cursor: pointer;
}
.viz-mc-selbtn:hover { border-color: var(--accent-strong); }
.viz-mc-selbtn:disabled { opacity: 0.6; cursor: default; }
/* "Result may be out of date" banner. */
.viz-mc-stale {
    display: flex; align-items: center; gap: 8px; flex: 0 0 auto;
    padding: 6px 10px; font-size: 0.74rem; color: var(--warning, #b45309);
    background: var(--warning-bg, #fef3c7); border: 1px solid var(--warning-ring, #fcd34d);
    border-radius: var(--radius-sm);
}
.viz-mc-stale-icon { flex: 0 0 auto; }
.viz-mc-stale span { flex: 1 1 auto; min-width: 0; }
.viz-mc-stale-btn {
    flex: 0 0 auto; padding: 2px 9px; font-size: 0.72rem; font-weight: 600;
    color: var(--warning, #b45309); background: var(--surface);
    border: 1px solid var(--warning-ring, #fcd34d); border-radius: var(--radius-sm); cursor: pointer;
}
.viz-mc-stale-btn:hover:not(:disabled) { background: var(--warning-bg, #fef3c7); }
.viz-mc-stale-btn:disabled { opacity: 0.6; cursor: default; }

/* ── Activity Cliffs result panel ──
   A compact A | Δ | B comparison list. The sort control sits in the panel
   header; each card leads with the two structures + their activities and a
   slim metrics row (similarity · SALI · subtle Select). */
/* Two-row Cliffs header: title + action row on top; a second row holding the
   summary/meta on the LEFT and the Sort control on the RIGHT, sharing one line.
   Wraps gracefully on narrow rails (meta drops, Sort follows). */
.viz-cliffs-head { flex-direction: column; align-items: stretch; gap: 6px; }
.viz-mc-head-row { display: flex; flex-direction: row; align-items: flex-start; gap: 8px; }
.viz-cliffs-toolbar { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 6px 10px; }
/* Meta: counts never truncate; the activity-column name ellipsizes EARLY (a
   strict max-width) so the line stays compact next to Sort. Full name in title. */
.viz-cliffs-meta { display: flex; align-items: baseline; min-width: 0; flex: 1 1 auto; }
.viz-cliffs-meta-counts { flex: 0 0 auto; white-space: nowrap; }
.viz-cliffs-actname { flex: 0 1 auto; min-width: 0; max-width: 11ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Sort uses the shared StudioDropdown (custom, platform-consistent) — just a
   compact wrapper + a tiny label in the toolbar row. */
.viz-cliffs-sortwrap { flex: 0 0 auto; display: inline-flex; align-items: center; gap: 5px; }
.viz-cliffs-sortlbl { font-size: 0.68rem; color: var(--text-muted); }
.viz-cliffs-sortdd.studio-dropdown {
    width: auto; min-width: 72px; min-height: 0; padding: 2px 7px;
    font-size: 0.72rem; gap: 4px; color: var(--text-muted);
}
.viz-cliffs-sortdd.studio-dropdown .studio-dropdown-chevron svg { width: 10px; height: 10px; }
/* No focus/open halo — just a subtle accent border, matching the global
   no-halo input cue (the base .studio-dropdown adds a 3px ring otherwise). */
.viz-cliffs-sortdd.studio-dropdown:focus-visible,
.viz-cliffs-sortdd.studio-dropdown.is-open {
    box-shadow: none;
    border-color: var(--accent);
}
.viz-cliffs-list { display: flex; flex-direction: column; gap: 6px; }
.viz-cliffs-pair {
    display: flex; flex-direction: column; gap: 5px; padding: 7px 8px;
    background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-md);
    transition: border-color 0.12s var(--ease);
}
.viz-cliffs-pair:hover { border-color: var(--border-strong); }
/* Two equal structure columns, side by side (no empty centre). 1fr tracks let
   the images scale responsively in a narrow rail. */
.viz-cliffs-mols { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.viz-cliffs-mol { min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.viz-cliffs-thumb {
    width: 100%; aspect-ratio: 1 / 1;
    /* A little internal breathing room so the (common-scale) structures don't
       touch the frame. Block layout (NOT flex) is deliberate: VizStructureThumb
       gives the inner .viz-thumb an inline `flex: 0 0 <renderPx>` (300px for
       cliffs); as a flex child that fixed, non-shrinking basis would size it at
       300px and overflow / overlap the neighbouring thumbnail. In normal flow
       the inline `flex` is inert and width/height:100% govern. overflow:hidden
       is a final guard so nothing can ever spill past the card. */
    padding: 5px; box-sizing: border-box; overflow: hidden;
    /* No border here — the pair card (.viz-cliffs-pair) already draws the
       boundary; an inner frame around each thumb read as a double border. */
    background: var(--surface); border-radius: var(--radius-sm);
}
.viz-cliffs-thumb .viz-thumb { display: block; width: 100% !important; height: 100% !important; }
.viz-cliffs-thumb .viz-thumb svg { display: block; width: 100%; height: 100%; }
/* While a thumbnail loads, show a calm skeleton box instead of the benzene
   placeholder glyph — avoids the glyph→structure flash ("flicker") on open. */
.viz-cliffs-thumb .viz-thumb-empty { background: var(--surface-sunk); }
.viz-cliffs-thumb .viz-thumb-empty svg { display: none; }
/* id + activity on one line: clickable id (selects the row) on the left, the
   activity as a subtle value badge on the right with clear separation. */
.viz-cliffs-mol-foot { display: flex; align-items: center; gap: 8px; min-width: 0; }
.viz-cliffs-mol-id {
    flex: 1 1 auto; min-width: 0; text-align: left; font: inherit; font-size: 0.78rem; color: var(--text-muted);
    background: transparent; border: none; padding: 0; cursor: pointer;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.viz-cliffs-mol-id:hover { color: var(--accent-strong); text-decoration: underline; }
.viz-cliffs-mol-act {
    flex: 0 0 auto; max-width: 55%; font-size: 0.76rem; font-weight: 700; font-variant-numeric: tabular-nums; color: var(--text-strong);
    background: var(--surface-sunk); border: 1px solid var(--border-faint);
    border-radius: var(--radius-sm); padding: 1px 7px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Compact metrics strip: similarity · Δ · SALI + a subtle Select. */
.viz-cliffs-metrics { display: flex; align-items: center; gap: 5px; flex-wrap: wrap; }
.viz-cliffs-chip {
    font-size: 0.68rem; color: var(--text-muted); font-variant-numeric: tabular-nums;
    background: var(--surface-sunk); border: 1px solid var(--border-faint); border-radius: var(--radius-sm); padding: 1px 6px;
}
.viz-cliffs-chip b { color: var(--text-strong); font-weight: 700; }
.viz-cliffs-chip.is-delta {
    color: var(--warning, #b45309); background: var(--warning-bg, #fef3c7); border-color: var(--warning-ring, #fcd34d); font-weight: 700;
}
/* SALI for a near-identical pair (similarity ≥ 99%) — denominator-dominated and
   unstable; flag it so the huge magnitude reads as a caveat, not a real ranking. */
.viz-cliffs-chip.is-warn {
    color: var(--danger, #b91c1c); background: var(--danger-soft, #fef2f2); border-color: var(--danger-ring, #fecaca); font-weight: 700; cursor: help;
}
/* Subtle ghost "Select" — not a prominent accent button. */
.viz-cliffs-select {
    margin-left: auto; display: inline-flex; align-items: center; gap: 3px; cursor: pointer;
    font: inherit; font-size: 0.7rem; color: var(--text-muted);
    background: transparent; border: 1px solid transparent; border-radius: var(--radius-sm); padding: 1px 6px;
}
.viz-cliffs-select:hover { color: var(--accent-strong); background: var(--accent-soft); }

/* ── Med-chem tools (drawer card buttons + MedchemModal) ──
   Cards mirror the Add-Model picker cards (.studio-model-picker-card): same
   surface, border-strong, radius-md, padding, accent hover + focus ring, and
   title/description hierarchy — so the two tool lists feel consistent. */
/* gap matches the Add-Model "Available models" picker (.studio-models-picker)
   so the two tool lists share the same vertical rhythm. */
.studio-tool-medchem-list { display: flex; flex-direction: column; gap: 0.5rem; }
.studio-tool-medchem-btn {
    display: flex; flex-direction: column; gap: 2px; width: 100%; text-align: left;
    padding: 0.4rem 0.6rem;
    background: var(--surface); border: 1px solid var(--border-strong);
    border-radius: var(--radius-md); cursor: pointer; font: inherit; color: inherit;
    transition: border-color var(--motion-fast) var(--ease),
                background var(--motion-fast) var(--ease),
                box-shadow var(--motion-fast) var(--ease);
}
.studio-tool-medchem-btn:hover {
    background: var(--accent-soft); border-color: var(--accent-ring);
    box-shadow: 0 1px 2px rgba(15, 23, 42, 0.06);
}
.studio-tool-medchem-btn:focus-visible {
    outline: none; border-color: var(--accent-ring); box-shadow: 0 0 0 3px var(--accent-ring);
}
.studio-tool-medchem-name { font-size: 0.85rem; font-weight: 600; color: var(--text-strong); line-height: 1.25; }
.studio-tool-medchem-desc { font-size: 0.74rem; color: var(--text-muted); line-height: 1.3; }

/* ── Saved Analyses — one consistent result card per analysis kind
   (Scaffold Navigator / Structure Alerts / Activity Cliffs). ── */
.studio-tool-results { display: flex; flex-direction: column; gap: 6px; margin-top: 0.7rem; }
.studio-tool-results-label { font-size: 0.66rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-faint); }
.studio-tool-results-labelrow { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; }
.studio-tool-refresh-all {
    display: inline-flex; align-items: center; gap: 0.3rem;
    padding: 2px 7px; border: 1px solid var(--border); border-radius: var(--radius-sm);
    background: var(--surface); color: var(--text-muted); cursor: pointer;
    font: inherit; font-size: 0.7rem; font-weight: 600;
}
.studio-tool-refresh-all:hover:not(:disabled) { color: var(--accent-strong); border-color: var(--accent); }
.studio-tool-refresh-all:disabled { opacity: 0.5; cursor: not-allowed; }
.studio-tool-rescard {
    padding: 8px 10px; display: flex; flex-direction: column; gap: 5px;
    background: var(--surface); border: 1px solid var(--border-strong); border-radius: var(--radius-md);
}
.studio-tool-rescard.is-stale { border-color: var(--warning-ring, #fcd34d); }
.studio-tool-rescard-head { display: flex; align-items: center; gap: 8px; }
.studio-tool-rescard-title { flex: 1 1 auto; min-width: 0; font-size: 0.82rem; font-weight: 600; color: var(--text-strong); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.studio-tool-rescard-status {
    flex: 0 0 auto; font-size: 0.6rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.02em;
    color: var(--text-muted); background: var(--surface-sunk);
    border: 1px solid var(--border-faint); border-radius: 999px; padding: 1px 7px;
}
.studio-tool-rescard-status.is-stale { color: var(--warning, #b45309); background: var(--warning-bg, #fef3c7); border-color: var(--warning-ring, #fcd34d); }
/* Summary as small chips (catalogs · SMARTS count · flagged) — wraps in the
   narrow drawer so nothing is clipped. */
.studio-tool-rescard-chips { display: flex; flex-wrap: wrap; gap: 4px; }
.studio-tool-rescard-chip {
    font-size: 0.68rem; color: var(--text-muted); font-variant-numeric: tabular-nums;
    background: var(--surface-sunk); border: 1px solid var(--border-faint);
    border-radius: var(--radius-sm); padding: 1px 6px;
    max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Compact, subtle action row — ghost/link-style, right-aligned. */
/* nowrap + min-height so swapping the trailing remove control for confirm/
   cancel icons never wraps or changes the card height (no reflow). */
.studio-tool-rescard-actions { display: flex; align-items: center; gap: 2px; justify-content: flex-end; flex-wrap: nowrap; min-height: 26px; }
.studio-tool-resact-confirm { display: inline-flex; align-items: center; gap: 2px; }
.studio-tool-resact {
    display: inline-flex; align-items: center; gap: 3px; cursor: pointer;
    font: inherit; font-size: 0.7rem; color: var(--text-muted);
    background: transparent; border: 1px solid transparent; border-radius: var(--radius-sm); padding: 2px 6px;
}
.studio-tool-resact:hover:not(:disabled) { color: var(--accent-strong); background: var(--accent-soft); }
.studio-tool-resact.is-danger:hover:not(:disabled) { color: var(--danger-strong); background: var(--danger-bg); }
/* Rerun is the call-to-action on an outdated card — give it a subtle accent
   resting state so it reads as the recommended next step. */
.studio-tool-resact.is-rerun { color: var(--accent-strong); border-color: var(--accent-ring); background: var(--accent-soft); }
.studio-tool-resact.is-rerun:hover:not(:disabled) { border-color: var(--accent-strong); }
.studio-tool-resact:disabled { opacity: 0.55; cursor: default; }

/* Unified Visualization tool — the "Add visualization" type-picker menu.
   (The per-row text type-badge was removed; the left glyph distinguishes
   chemical-space vs scatter rows.) */
.studio-tool-viz-addwrap { position: relative; }
/* Portaled to <body> with fixed coords (set inline from the button's rect), so
   it clears the tool rail's scroll/overflow clipping. z-index matches the other
   portaled Studio menus (.studio-dropdown-menu) so it layers above panels. */
.studio-tool-viz-addmenu {
    position: fixed; z-index: 12000;
    /* Width is set INLINE to the trigger button's measured width (see
       updateVizAddPos) so the menu matches the button exactly. The global
       `box-sizing:border-box` makes that inline width == the button's
       getBoundingClientRect().width. */
    display: flex; flex-direction: column; padding: 3px;
    background: var(--surface); border: 1px solid var(--border-strong);
    border-radius: var(--radius-md); box-shadow: var(--shadow-2);
    overflow-y: auto;
}
.studio-tool-viz-addmenu-item {
    display: flex; align-items: center; gap: 8px;
    padding: 7px 8px; font-size: 0.82rem; font-weight: 500; text-align: left;
    background: transparent; border: none; border-radius: var(--radius-sm);
    color: var(--text); cursor: pointer; white-space: nowrap;
}
.studio-tool-viz-addmenu-item:hover { background: var(--accent-soft); color: var(--accent-strong); }
.studio-tool-viz-addmenu-item svg { flex: 0 0 auto; }

/* R-group enumeration drawer --------------------------------------- */
/* Fixed-size .studio-prompt column used by the R-group ENUMERATION drawer
   (RGroupEnumDrawer). The decomposition scaffold EDITOR no longer uses this
   class — it reuses the Add Compounds modal surfaces (.studio-import-modal /
   -header / -tabs / -body + .studio-draw-*) so the two drawing flows are
   identical in size, padding, iframe footprint, and density. */
.studio-prompt.rgroup-drawer {
    width: min(840px, 96vw);
    height: min(720px, 92vh);
    display: flex; flex-direction: column;
    padding: 0;
}

/* ── R-group Enumeration wizard ─────────────────────────────────────── */
/* A full workflow dialog (Ketcher + multi-step + candidate sections), so it is
   deliberately large but responsive — clamped to the viewport so it never
   overflows on small screens. Scoped to .rgenum-modal only; the shared
   .studio-import-modal sizing for Add Compounds / decomposition is untouched. */
.studio-import-modal.rgenum-modal {
    width: min(1120px, 96vw);
    height: min(880px, 94vh);
    max-width: 96vw;
    max-height: 94vh;
    display: flex;
    flex-direction: column;
}
/* Fixed-height body with internal scroll so steps never resize the modal. */
.rgenum-body { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
/* Give the scaffold Ketcher canvas room (the draw-pane keeps its base
   flex-COLUMN so the SMILES footer stacks under the canvas — don't override
   display/direction here). */
.rgenum-step1 { display: flex; flex-direction: column; min-height: 0; height: 100%; }
.rgenum-step1 .studio-draw-pane { flex: 1 1 auto; min-height: 380px; }
.rgenum-step1 .studio-draw-host { min-height: 360px; }
/* Step indicator — stable width across steps (no bold reflow: active is shown
   via colour + a filled number badge, never a weight change). Steps are
   buttons, clickable when reachable. */
.rgenum-steps {
    display: flex; gap: 4px; align-items: center;
    padding: 10px 16px; border-bottom: 1px solid var(--border);
    background: var(--surface-soft);
}
.rgenum-step {
    display: inline-flex; align-items: center; gap: 6px;
    font-size: 0.8rem; font-weight: 500; color: var(--text-muted);
    background: none; border: none; padding: 4px 6px; border-radius: var(--radius-sm);
    cursor: pointer; white-space: nowrap;
}
.rgenum-step:not(:last-child)::after { content: '›'; margin-left: 6px; color: var(--text-faint); cursor: default; }
.rgenum-step:disabled { cursor: default; }
.rgenum-step:not(:disabled):hover { color: var(--text); }
.rgenum-step.is-active { color: var(--text-strong); }   /* colour only — width stays constant */
.rgenum-step-num {
    display: inline-flex; align-items: center; justify-content: center;
    width: 20px; height: 20px; border-radius: 50%;
    background: var(--surface-sunk); color: var(--text-muted); font-size: 0.72rem; font-weight: 700;
}
.rgenum-step.is-active .rgenum-step-num { background: var(--accent); color: #fff; }
.rgenum-step.is-done .rgenum-step-num { background: var(--accent-muted); color: var(--accent-strong); }

/* Step 1 — the SMILES readout reuses the canonical .studio-draw-footer /
   .studio-draw-smiles pattern from the Add-Compounds draw panel, so no
   bespoke readout styles are needed here. */

/* Step 2 — summary + substituent sections. */
/* Substituents step is a flex column so the single shared fragment-draw card
   can be `order`-floated directly beneath the R# card that opened it (the total
   stays first at the default order:0; sections take odd orders, the draw card an
   even order between them). */
/* Uniform `gap` (not per-child margins) so the order-floated draw card gets an
   equal comfortable gap ABOVE and BELOW — margins were one-sided
   (section margin-bottom + draw margin-top), leaving the draw card stuck to the
   NEXT R card. gap is symmetric and collapses cleanly when the draw card is
   display:none. */
.rgenum-step2 { display: flex; flex-direction: column; gap: 12px; }
.rgenum-total { font-size: 0.92rem; padding: 4px 0 0; color: var(--text); font-weight: 600; }
.rgenum-total.is-warn { color: var(--warning, #b45309); }
/* Informational "will cap at N" note (not an error — enumeration truncates). */
.rgenum-total-capped { font-weight: 500; color: var(--text-muted); }
.rgenum-warn { color: var(--warning, #b45309); font-weight: 600; }

.rgenum-section { border: 1px solid var(--border-strong); border-radius: var(--radius-md); padding: 12px 14px; background: var(--surface); }
.rgenum-section-head { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
.rgenum-section-title { font-weight: 600; font-size: 0.92rem; color: var(--text-strong); }
.rgenum-section-count {
    display: inline-flex; align-items: center; justify-content: center;
    min-width: 22px; height: 20px; padding: 0 7px; border-radius: 10px;
    background: var(--accent-soft); color: var(--accent-strong); font-size: 0.74rem; font-weight: 700;
}
/* Add actions (Draw / Import / Clear all) live on the header row, pushed
   to the right so the body below is a clean two-pane. */
.rgenum-section-head-actions { margin-left: auto; display: flex; align-items: center; gap: 6px; flex: 0 0 auto; }
/* Clear all reads as a destructive/clear action: red outline, compact.
   Scoped to the enum modal so global .btn-ghost is untouched. */
.rgenum-clear-all {
    border: 1px solid var(--danger, #c0392b);
    color: var(--danger, #c0392b);
    background: transparent;
}
.rgenum-clear-all:hover:not(:disabled) {
    background: var(--danger, #c0392b);
    color: #fff;
    border-color: var(--danger, #c0392b);
}

/* Compact, consistent action buttons inside the enum modal. Scoped to
   .rgenum-btn so global .btn / other Studio modals are untouched. */
.rgenum-btn { padding: 0.3rem 0.7rem; font-size: 0.8rem; }

/* Two-pane body: paste fragments (left) · added fragments (right). The body
   has a FIXED height so importing/adding fragments fills (and scrolls) the
   Added pane internally instead of stretching the whole R-card — the modal
   layout stays stable after import. */
.rgenum-section-body { display: flex; gap: 14px; align-items: stretch; height: 300px; }
.rgenum-input-half {
    flex: 1 1 0; min-width: 0; min-height: 0; display: flex; flex-direction: column; gap: 8px;
    padding: 10px; border: 1px solid var(--border); border-radius: var(--radius-sm);
    background: var(--surface-soft);
}
.rgenum-input-half-title {
    font-size: 0.64rem; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase;
    color: var(--text-faint); flex: 0 0 auto;
}
/* Textarea fills the (fixed-height) paste pane; no manual resize so the card
   height stays stable. */
.rgenum-paste-area { flex: 1 1 auto; min-height: 0; resize: none; }
/* Inline "skipped invalid fragment" notice under the paste box. */
.rgenum-paste-error { flex: 0 0 auto; font-size: 0.74rem; line-height: 1.35; color: var(--danger, #c0392b); }
/* "Add pasted" hugs the bottom-right of the paste pane. */
.rgenum-add-pasted { align-self: flex-end; flex: 0 0 auto; }
.rgenum-empty { font-size: 0.82rem; color: var(--text-faint); font-style: italic; padding: 6px 2px; }

/* Added-fragments pane: fills the fixed-height pane and scrolls internally. */
.rgenum-cands-half { min-height: 0; }
.rgenum-cands-list { flex: 1 1 auto; min-height: 0; overflow-y: auto;
    display: flex; flex-direction: column; gap: 6px; }
.rgenum-cand-row {
    display: flex; align-items: center; gap: 10px;
    border: 1px solid var(--border); border-radius: var(--radius-sm);
    padding: 5px 8px; background: var(--surface);
}
.rgenum-cand-thumb { width: 46px; height: 46px; flex: 0 0 auto; background: #fff; border: 1px solid var(--border); border-radius: 4px; overflow: hidden; }
.rgenum-cand-thumb svg, .rgenum-cand-thumb img { width: 100%; height: 100%; object-fit: contain; }
.rgenum-cand-smiles { flex: 1 1 auto; min-width: 0; font-size: 0.8rem; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rgenum-cand-remove { border: none; background: none; cursor: pointer; color: var(--text-muted); padding: 3px; line-height: 0; flex: 0 0 auto; border-radius: 4px; }
.rgenum-cand-remove:hover { color: var(--danger, #c0392b); background: var(--surface-sunk); }

.rgenum-fragdraw {
    border: 1px solid var(--border-strong); border-radius: var(--radius-md);
    padding: 10px; background: var(--surface-soft);   /* gap on .rgenum-step2 spaces it from the cards above & below */
    /* Flex column with NO uniform gap so the head sits tight against the canvas
       (compact, connected header) while the canvas keeps comfortable room
       before the footer (margins on the children below). */
    display: flex; flex-direction: column; gap: 0;
}
/* Compact header → connected to the draw panel directly beneath it. */
.rgenum-fragdraw-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 6px; }
.rgenum-fragdraw-title { font-weight: 500; color: var(--text); font-size: 0.9rem; }
/* Tall, responsive draw canvas on par with the Add-Compounds / Decomposition
   editors (their host min-height is 480px). The base .studio-draw-host is
   `flex: 1` — inside this auto-height flex-COLUMN that resolves to flex-basis 0
   and collapses the canvas to a thin line, so pin it to a non-flexing, explicit
   height instead. Height comfortably exceeds Ketcher's own min UI so the canvas
   never shows an internal scrollbar; on short viewports the modal BODY scrolls. */
.rgenum-fragdraw .studio-draw-host {
    flex: 0 0 auto;
    height: clamp(460px, 56vh, 640px);
    min-height: 460px;
    position: relative;
    margin-bottom: 12px;   /* comfortable room before the Clear · Add footer */
}
/* Bottom action row of the fragment editor: Clear · Add to R# (right-aligned). */
.rgenum-fragdraw-actions { display: flex; align-items: center; gap: 8px; flex: 0 0 auto; }

/* Step 3 — review cards. */
.rgenum-step3 { display: flex; flex-direction: column; gap: 14px; }
.rgenum-rev-card { border: 1px solid var(--border-strong); border-radius: var(--radius-md); padding: 12px 14px; background: var(--surface); }
.rgenum-rev-card-title { font-size: 0.72rem; font-weight: 700; letter-spacing: 0.05em; text-transform: uppercase; color: var(--text-faint); margin-bottom: 10px; }

/* Enumeration filters — a clean, aligned product-style filter panel: a
   dedup SWITCH + an aligned min/max property grid. The card header carries the
   title, an "optional" hint, and a Clear-filters control that is ALWAYS present
   (disabled when nothing is active) so the card height never jumps. */
.rgenum-filters-head { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; margin-bottom: 0.6rem; }
.rgenum-filters-head .rgenum-rev-card-title { margin: 0; }
.rgenum-filters-opt { font-weight: 500; text-transform: none; letter-spacing: 0; color: var(--text-faint); font-size: 0.7rem; margin-left: 0.35rem; }
.rgenum-filters-clear {
    border: 0; background: transparent; color: var(--accent-strong); cursor: pointer;
    font: inherit; font-size: 0.76rem; font-weight: 600; padding: 0;
}
.rgenum-filters-clear:hover:not(:disabled) { text-decoration: underline; }
.rgenum-filters-clear:disabled { color: var(--text-faint); cursor: default; opacity: 0.55; }
.rgenum-filter-skip {
    display: inline-flex; align-items: center; gap: 0.5rem; font-size: 0.84rem;
    color: var(--text); cursor: pointer; margin-bottom: 0.85rem;
}
/* Aligned min/max grid: label | min | – | max, with a column header row.
   `display:contents` lets the header + each gate share the same grid tracks so
   every input lines up into clean columns. */
.rgenum-gates { display: grid; grid-template-columns: 3.2rem 1fr 0.7rem 1fr; gap: 0.45rem 0.6rem; align-items: center; }
.rgenum-gates-head { display: contents; }
.rgenum-gates-head > span:nth-child(1) { grid-column: 1; font-size: 0.64rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-faint); }
.rgenum-gates-head-mm { font-size: 0.62rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.03em; color: var(--text-faint); text-align: center; }
.rgenum-gates-head > .rgenum-gates-head-mm:nth-child(2) { grid-column: 2; }
.rgenum-gates-head > .rgenum-gates-head-mm:nth-child(3) { grid-column: 4; }
.rgenum-gate { display: contents; }
.rgenum-gate-label { font-size: 0.8rem; font-weight: 600; color: var(--text-muted); }
.rgenum-gate-input { width: 100%; min-width: 0; height: 30px; border-radius: var(--radius-sm); text-align: center; }
.rgenum-gate-dash { color: var(--text-faint); text-align: center; }

/* Summary/destination bar at the top — a cohesive row of stat cells
   (Products · Combinations · Destination), divided by hairlines, with the
   numbers as the dominant element and labels secondary. Wraps cleanly on
   narrow screens (cells stack, dividers move to the top edge). */
.rgenum-rev-summary { padding-top: 14px; padding-bottom: 14px; }
/* A single row of stat cells (Products · Combinations · Max products ·
   Destination) divided by visible hairlines. All values share one font
   weight/size so the row aligns cleanly. Each cell aligns its label+value to
   the top so the (taller) Destination cell doesn't drag the others' baselines. */
.rgenum-rev-summary-grid { display: flex; flex-wrap: wrap; align-items: stretch; gap: 12px 0; }
.rgenum-rev-summary-cell {
    display: flex; flex-direction: column; gap: 4px; min-width: 0;
    padding: 0 36px; border-left: 1px solid var(--border-strong);
}
.rgenum-rev-summary-cell:first-child { padding-left: 0; border-left: none; }
/* Destination flows right after Max products (not pushed to the far right) and
   flexes to fill the remaining width — a stable footprint that doesn't shift
   when a sheet is chosen (the pill ellipsizes within it). A modest basis lets
   the row stay single-line across mid widths before the cells wrap. */
.rgenum-rev-summary-dest { flex: 1 1 200px; align-items: flex-start; }
.rgenum-rev-summary-k { font-size: 0.62rem; font-weight: 700; letter-spacing: 0.05em; text-transform: uppercase; color: var(--text-faint); }
/* A shared min-height + vertical centering so the value row of every cell —
   plain text, the Max-products <input>, and the destination button/pill — lines
   up on the same band (the input no longer floats above the button). */
.rgenum-rev-summary-v { min-width: 0; min-height: 30px; display: flex; align-items: center; gap: 6px; font-size: 0.95rem; font-weight: 500; color: var(--text-strong); }
/* "capped" chip next to the Products number when the full set exceeds the max. */
.rgenum-cap-tag {
    font-size: 0.64rem; font-weight: 700; letter-spacing: 0.02em; text-transform: uppercase;
    color: var(--warning, #b45309); background: var(--warning-soft, #fef3c7);
    border-radius: var(--radius-sm); padding: 1px 6px;
}
/* Same 28px box-sizing:border-box height as the destination button so the two
   controls line up on the same baseline/center; horizontal-only padding keeps
   the single-line value vertically centred by the browser. The descendant
   selector raises specificity above `input.form-input-sm` (0,1,1), whose
   `height:auto` + vertical padding would otherwise win and make it taller. */
.rgenum-rev-summary-v input.rgenum-max-input {
    box-sizing: border-box; width: 96px; height: 28px;
    padding: 0 0.5rem; font-size: 0.82rem; line-height: 1.3;
}
@media (max-width: 640px) {
    .rgenum-rev-summary-cell {
        flex: 1 1 100%; padding: 8px 0 0;
        border-left: none; border-top: 1px solid var(--border);
    }
    .rgenum-rev-summary-cell:first-child { padding-top: 0; border-top: none; }
}

/* Bottom row: scaffold preview (left, compact fixed column) + sample products
   (right, gets the rest of the width). A touch more gap so the two cards read
   as a balanced pair even when the preview shows a horizontal scrollbar. */
.rgenum-rev-bottom { display: grid; grid-template-columns: 186px 1fr; gap: 16px; align-items: stretch; }
@media (max-width: 720px) { .rgenum-rev-bottom { grid-template-columns: 1fr; } }
.rgenum-rev-scaffold-card { display: flex; flex-direction: column; }
/* The preview card MUST be able to shrink below its content width so its inner
   `.rgenum-rev-preview` (overflow-x:auto) owns the horizontal scroll — without
   min-width:0 a grid item refuses to shrink and the whole MODAL scrolls. */
.rgenum-rev-preview-card { min-width: 0; }
.rgenum-rev-thumb { background: #fff; border: 1px solid var(--border-strong); border-radius: var(--radius-sm); overflow: hidden; }
/* The scaffold image uses the SAME 140×140 viewport as each preview product
   thumbnail so molecule images render at an identical visual height. It is
   TOP-aligned (margin: 0 auto) — sitting directly under the "Scaffold" title
   just like the preview thumbnails sit under their title — so the two cards'
   images line up horizontally. (Vertical centering pushed it down and made it
   look misaligned with the preview row.) */
.rgenum-rev-thumb-scaffold { width: 140px; height: 140px; max-width: 100%; margin: 0 auto; display: flex; align-items: center; justify-content: center; }
.rgenum-rev-thumb-scaffold svg { width: 100%; height: 100%; object-fit: contain; }

.rgenum-rev-rg { display: flex; flex-direction: column; gap: 8px; padding: 10px 0; border-top: 1px solid var(--border-faint); }
.rgenum-rev-rg:first-of-type { border-top: none; padding-top: 2px; }
.rgenum-rev-rg-head { display: flex; align-items: baseline; gap: 10px; }
.rgenum-rev-rg-label { font-weight: 700; color: var(--accent-strong); min-width: 28px; }
.rgenum-rev-rg-count { font-size: 0.82rem; color: var(--text-muted); }
/* Substituent thumbnails: larger + a single horizontally-scrolling row so many
   substituents stay visible without the card growing vertically without bound. */
.rgenum-rev-rg-thumbs { display: flex; align-items: center; gap: 7px; flex-wrap: nowrap; overflow-x: auto; padding-bottom: 4px; scrollbar-width: thin; }
.rgenum-rev-rg-thumb { width: 92px; height: 92px; flex: 0 0 auto; background: #fff; border: 1px solid var(--border-strong); border-radius: var(--radius-sm); overflow: hidden; display: inline-block; }
.rgenum-rev-rg-thumb svg { width: 100%; height: 100%; object-fit: contain; }
.rgenum-rev-rg-more { font-size: 0.78rem; color: var(--text-muted); align-self: center; flex: 0 0 auto; padding: 0 4px; white-space: nowrap; }
/* (.rgenum-rev-total / .rgenum-rev-total b are styled in the Summary block.) */

/* Enumeration preview — sample product thumbnails. */
/* Sample products — a single horizontally-scrolling row (like the substituent
   thumbnails) so the card height stays bounded no matter how many products.
   Thumbnails share the SAME 140×140 viewport as the scaffold image. */
.rgenum-rev-preview { display: flex; flex-wrap: nowrap; gap: 8px; overflow-x: auto; padding-bottom: 8px; scrollbar-width: thin; }
.rgenum-rev-preview-thumb { width: 140px; height: 140px; flex: 0 0 auto; background: #fff; border: 1px solid var(--border-strong); border-radius: var(--radius-sm); overflow: hidden; display: inline-block; }
.rgenum-rev-preview-thumb svg { width: 100%; height: 100%; object-fit: contain; }
/* Destination — a compact action button + the current selection pill, on a
   SINGLE horizontal row (the pill ellipsizes rather than wrapping/stacking, so
   the Summary card height stays stable before/after choosing). */
.rgenum-rev-dest { display: flex; align-items: center; gap: 8px; flex-wrap: nowrap; min-width: 0; }
/* Compact action sharing an EXACT pixel height with the Max-products input
   (.rgenum-max-input) so the two controls match visually. Both use
   box-sizing:border-box + an explicit 28px height; .btn is inline-flex so its
   label centers regardless of line-height. */
.rgenum-dest-btn { flex: 0 0 auto; box-sizing: border-box; height: 28px; padding: 0 0.7rem; font-size: 0.78rem; line-height: 1; }
.rgenum-rev-dest-empty { flex: 0 0 auto; font-size: 0.8rem; color: var(--text-faint); font-style: italic; }
/* Clean, rectangular chip (small radius, subtle surface) — matches the rest of
   Alma Studio rather than a pill-shaped "AI" badge. */
.rgenum-dest-pill {
    display: inline-flex; align-items: baseline; gap: 4px; flex: 0 1 auto; min-width: 0;
    padding: 3px 8px; border-radius: var(--radius-sm);
    background: var(--surface-soft); border: 1px solid var(--border);
    font-size: 0.8rem; line-height: 1.3;
}
.rgenum-dest-pill-path { color: var(--text-faint); flex: 0 0 auto; }
.rgenum-dest-pill-name { color: var(--accent-strong); font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.rgenum-error { color: var(--danger, #c0392b); font-size: 0.82rem; padding: 8px 0 0; }
.rgenum-footer { display: flex; align-items: center; }
.rgenum-footer-left { flex: 0 0 auto; }
.rgenum-footer-right { margin-left: auto; display: flex; gap: 8px; }

/* Custom discard confirmation — stacked above the wizard. */
.rgenum-confirm-overlay { z-index: 60; }
.studio-prompt.rgenum-confirm { width: min(420px, 92vw); }
.rgenum-confirm .studio-prompt-text { font-size: 0.86rem; color: var(--text-muted); margin-top: 6px; line-height: 1.5; }
.rgenum-confirm .studio-prompt-footer { display: flex; justify-content: flex-end; gap: 8px; padding: 12px 16px 16px; }
/* Decomposition scaffold editor (RGroupCardModal) — reuses
   .studio-import-modal for the frame, so only the pieces unique to this
   editor carry their own rules: the Scaffold name on the tab row and the
   advanced SMILES/SMARTS pane. */
/* Tab strip carries the Scaffold name on its right side. .studio-import-tabs
   is a flex row whose items stretch full-height (keeps the active-tab
   underline on the bottom border); the name group is pushed right with
   margin-left:auto and self-centers vertically. */
.rgroup-name-inline {
    margin-left: auto;            /* push to the right side of the tab row */
    align-self: center;
    display: flex; align-items: center; gap: 6px;
    /* The tab strip is inset 1rem; the body below is inset 1.25rem. This extra
       0.25rem lines the input's right edge up with the draw panel's right edge. */
    padding-right: 0.25rem;
}
.rgroup-name-inline label {
    font-size: 0.8rem; font-weight: 500; color: var(--text-muted);
    white-space: nowrap;
}
/* Compact input — kept short so its visual height balances the label text. */
.rgroup-name-input {
    width: 190px; height: 21px; padding: 0 0.45rem;
    font-size: 0.82rem; line-height: 1.1;
}
/* Invalid name (bad chars OR duplicate) → red border only, no message. */
.rgroup-name-input.has-error {
    border-color: var(--danger, #dc2626);
}
/* SMILES/SMARTS pane — fills the import body like the Draw pane and toggles
   via .is-hidden, so the Draw pane's Ketcher iframe never unmounts on a tab
   swap. */
.rgroup-smiles-pane {
    flex: 1; min-height: 0;
    display: flex; flex-direction: column;
}
.rgroup-smiles-pane.is-hidden { display: none; }
.rgroup-smiles-field {
    flex: 1; min-height: 0;
    display: flex; flex-direction: column; gap: 0.375rem;
}
.rgroup-smiles-field > span { font-size: 0.8rem; color: var(--text-muted); }
.rgroup-smiles-field textarea { flex: 1; min-height: 0; resize: none; }

/* R-group analysis section ----------------------------------------- */
/* Decomposition card list — each card mirrors the Structure filter card: a
   header row (name + edit/delete) above a clickable SQUARE 2D preview panel
   that opens the scaffold editor. The preview REUSES the Structure filter's
   .studio-filter-structure-panel (square aspect, soft bg, dashed border,
   hover, placeholder) + StructurePreviewSvg, so the two are visually
   identical and failures show a clean fallback. A subtle status line under
   the preview reports the live match count. */
/* Card list scrolls within its own bounded region so three+ tall cards don't
   push the rest of the Tools bar off-screen (the list is capped; scroll for
   the rest). */
.studio-tool-rgroup-cards {
    display: flex; flex-direction: column; gap: 6px;
    max-height: 420px; overflow-y: auto; overscroll-behavior: contain;
    padding-right: 2px;
}
.studio-tool-rgroup-card {
    /* flex:0 0 auto — never shrink in the scrollable column. Without it the
       cards could be compressed below their content and the preview panel
       (an aspect-ratio box) bled past the card box into the next card. */
    flex: 0 0 auto;
    display: flex; flex-direction: column;
    background: var(--surface-soft);
    border: 1px solid var(--border-faint);
    border-radius: var(--radius-sm);
    overflow: hidden;
    transition: border-color var(--motion-fast) var(--ease);
}
.studio-tool-rgroup-card:hover { border-color: var(--border); }
/* In this constrained, scrollable list a fixed preview height is reliable;
   aspect-ratio inside a flex column mis-measured and let cards overlap. */
.studio-tool-rgroup-card .studio-filter-structure-panel {
    aspect-ratio: auto;
    height: 160px;
    min-height: 0;
    /* WHITE preview backdrop (not the generic soft-gray), matching the sheet's
       structure cells — RDKit SVGs are drawn for a white ground, so this reads
       cleaner and stays correct in every theme. A solid light border (vs the
       generic dashed one) makes it feel like a structure cell rather than a
       drop target, while still signalling it's clickable on hover. */
    background: #ffffff;
    border-style: solid;
    border-color: var(--border);
}
.studio-tool-rgroup-card .studio-filter-structure-panel:hover {
    /* Keep the ground white on hover so the molecule never sits on a shifting
       backdrop; only the border picks up the accent. */
    background: #ffffff;
    border-color: var(--accent);
}
.studio-tool-rgroup-card-head {
    display: flex; align-items: center; gap: 4px;
    padding: 5px 5px 5px 8px;
}
.studio-tool-rgroup-name {
    flex: 1 1 auto; min-width: 0;
    font-size: 0.82rem; font-weight: 600;
    color: var(--text-strong);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Body wraps the reused square preview panel + the match-count status. */
.studio-tool-rgroup-card-body {
    display: flex; flex-direction: column; gap: 6px;
    padding: 0 8px 8px;
}
.studio-tool-rgroup-status {
    font-size: 0.72rem; color: var(--text-muted); text-align: center;
}
.studio-tool-rgroup-status.is-warn { color: var(--warning, #b45309); }

.studio-medchem { position: relative; }
/* Top-right X close — matches the other Studio modals' close affordance. */
.studio-medchem-x { position: absolute; top: 10px; right: 10px; z-index: 2; }
.studio-medchem .studio-prompt-body { display: flex; flex-direction: column; gap: 10px; }
.studio-medchem .studio-prompt-title { padding-right: 28px; }   /* clear the X */
/* The validation message is the last body row when a run is rejected (no
   result yet); the shared 1rem bottom margin leaves a big empty gap above the
   footer. The body's flex gap already spaces it, so drop the extra margin. */
.studio-medchem .studio-prompt-message { margin-bottom: 0; }
/* Large-sheet guidance note inside the med-chem modal (smart warning). */
.medchem-bignote {
    font-size: 0.78rem; line-height: 1.4; color: var(--warning, #92400e);
    background: var(--warning-bg, #fffbeb); border: 1px solid var(--warning-ring, #fde68a);
    border-radius: var(--radius-sm); padding: 7px 10px;
}
.medchem-controls { display: flex; flex-direction: column; gap: 8px; }
.medchem-row { display: flex; gap: 10px; flex-wrap: wrap; align-items: flex-end; }
.medchem-field { display: flex; flex-direction: column; gap: 3px; flex: 1 1 auto; min-width: 0; }
.medchem-field-sm { flex: 0 0 130px; }
/* Cliffs row: keep the activity column + the two number fields on ONE row. The
   growing field uses flex-basis:0 so its width is share-based (never driven by
   the selected label's length), and the row doesn't wrap. */
.medchem-row-nowrap { flex-wrap: nowrap; }
.medchem-field-grow { flex: 1 1 0; min-width: 0; }
/* Balanced two-column form (Scaffold Navigator): fields fill the modal width
   instead of hugging the left edge. `-full` items span both columns. */
.medchem-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px 14px; align-items: end; }
.medchem-grid-full { grid-column: 1 / -1; }
.medchem-grid .medchem-seg { display: flex; }
@media (max-width: 480px) { .medchem-grid { grid-template-columns: 1fr; } }
/* Switch row: description on the LEFT, toggle pushed to the RIGHT. */
.medchem-switchrow { display: flex; align-items: center; justify-content: space-between; gap: 10px; cursor: pointer; }
.medchem-switchrow-text { flex: 1 1 auto; min-width: 0; font-size: 0.8rem; color: var(--text); }
.medchem-field > span { font-size: 0.72rem; color: var(--text-muted); }
.medchem-hint { font-size: 0.7rem; color: var(--text-faint); line-height: 1.45; }
.medchem-hint code { font-family: var(--font-mono, monospace); font-size: 0.95em; background: var(--surface-sunk); padding: 0 3px; border-radius: 3px; }
/* Segmented toggle (e.g. Murcko vs Generic scaffold mode). */
.medchem-seg { display: inline-flex; padding: 2px; gap: 2px; background: var(--surface-sunk); border: 1px solid var(--border-faint); border-radius: var(--radius-md); }
.medchem-seg-btn {
    flex: 1 1 0; padding: 4px 12px; border: none; border-radius: var(--radius-sm);
    background: transparent; color: var(--text-muted); font-size: 0.78rem; font-weight: 500;
    cursor: pointer; white-space: nowrap; transition: background 0.12s, color 0.12s;
}
.medchem-seg-btn:hover { color: var(--text); }
.medchem-seg-btn.is-selected { background: var(--surface); color: var(--text-strong); box-shadow: var(--shadow-xs, 0 1px 2px rgba(0,0,0,0.08)); }
.medchem-cats { display: flex; gap: 14px; flex-wrap: wrap; align-items: center; }
.medchem-cats-label { font-size: 0.72rem; color: var(--text-muted); }
.medchem-results { display: flex; flex-direction: column; gap: 10px; }
.medchem-meta { font-size: 0.74rem; color: var(--text-muted); }

/* (The in-modal Scaffold/Alerts/Cliffs result styles were removed with their
   components — those tools now render in the analysis rail / as a sheet
   column group. The classes below are still used by the R-group decompose +
   Enumerate in-modal results.) */
.medchem-empty { padding: 16px; font-size: 0.8rem; color: var(--text-muted); font-style: italic; text-align: center; }
.medchem-table { display: grid; gap: 1px; max-height: 46vh; overflow: auto; background: var(--border-faint); border: 1px solid var(--border); border-radius: var(--radius-sm); }
.medchem-th { position: sticky; top: 0; z-index: 1; padding: 4px 6px; background: var(--surface-sunk); font-size: 0.62rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.03em; color: var(--text-muted); }
.medchem-td { display: flex; align-items: center; justify-content: center; padding: 3px; background: var(--surface); border: none; cursor: pointer; }
.medchem-td:hover { background: var(--surface-soft); }
.medchem-grid-thumbs { display: grid; grid-template-columns: repeat(auto-fill, minmax(96px, 1fr)); gap: 8px; max-height: 48vh; overflow-y: auto; }
.medchem-thumb { display: flex; flex-direction: column; align-items: center; gap: 3px; padding: 6px; border: 1px solid var(--border-faint); border-radius: var(--radius-sm); }
.medchem-thumb-lbl { font-size: 0.6rem; color: var(--text-muted); max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.medchem-search { flex: 1 1 auto; min-width: 0; }
/* "Add to new sheet" row under enumerate results. */
.medchem-addrow { display: flex; gap: 8px; align-items: center; padding-top: 4px; border-top: 1px solid var(--border-faint); margin-top: 4px; }
.studio-tool-section-label {
    font-size: 0.68rem; font-weight: 700; letter-spacing: 0.03em;
    text-transform: uppercase; color: var(--text-faint);
    margin: 0.55rem 0 0.4rem;
}

/* Static (read-only) form value — used for a calculated column's fixed
   type in the Edit modal, and the disabled "no groups yet" hint. */
.studio-form-static {
    display: flex; align-items: center; gap: 0.5rem;
    padding: 0.5rem 0.625rem;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--surface-soft);
    color: var(--text); font-size: 0.875rem;
}
.studio-form-static-note {
    margin-left: auto; font-size: 0.68rem; color: var(--text-faint);
    text-transform: uppercase; letter-spacing: 0.03em;
}
.studio-form-hint-disabled { color: var(--text-faint); font-style: italic; font-size: 0.78rem; }

/* ── Calculated (RDKit descriptor/scaffold) cell — read-only ── */
.studio-cell-calc {
    width: 100%;
    font-size: 0.875rem; color: var(--text);
    font-variant-numeric: tabular-nums;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Scaffold (and other long-text) calculated values behave like the main
   SMILES column: the text WRAPS naturally and, when it exceeds the cell's
   content area, scrolls VERTICALLY (never horizontally, never ellipsis).
   max-height is capped to the row's content area so wrapping can't grow
   the row — the overflow scrolls inside the cell instead. */
.studio-cell-calc-text {
    max-height: calc(var(--row-h, 56px) - var(--td-pad-y, 10px) * 2);
    overflow-y: auto; overflow-x: hidden;
    white-space: normal; overflow-wrap: anywhere; word-break: break-all;
    text-overflow: clip; line-height: 1.4;
    font-variant-numeric: normal;
    scrollbar-width: thin; scrollbar-color: var(--border-strong) transparent;
}
.studio-cell-calc-text::-webkit-scrollbar { width: 6px; }
.studio-cell-calc-text::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 3px; }
.studio-cell-calc-empty { color: var(--text-faint); }
/* Boolean calculated descriptors (RO5 Pass, Veber Pass, …) — a centered
   ✓/✗ glyph, green/red tinted, so a column of pass/fail flags scans at a
   glance. Inherits the no-row-growth behaviour from .studio-cell-calc. */
.studio-cell-calc-bool {
    text-align: center;
    font-variant-numeric: normal;
    font-weight: 600;
}
.studio-cell-calc-bool.is-true { color: var(--success, #16a34a); }
.studio-cell-calc-bool.is-false { color: var(--danger, #dc2626); }
/* Pending computed cells now reuse the muted .studio-cell-calc-empty dash —
   no per-cell animation (the old animated dots/keyframes were removed because
   animating thousands of cells made large-sheet scrolling janky). */

/* Model cell + header */
.studio-cell-model {
    text-align: center;
    vertical-align: middle;
}
/* Run button sized for comfortable click targets (~40 px wide,
   28 px tall) — large enough to register clearly even in the
   compact density mode but still leaves padding inside the cell.
   A subtle play-glyph would land here later when actual run
   status is wired up; for now the text label stays as is. */
.studio-model-run-btn {
    display: inline-flex; align-items: center; justify-content: center;
    gap: 4px;
    min-width: 60px;
    height: 28px;
    padding: 0 0.85rem;
    border: 1px solid var(--border-strong);
    background: var(--surface);
    color: var(--text-strong);
    border-radius: var(--radius-sm);
    font-size: 0.82rem; font-weight: 600;
    line-height: 1;
    cursor: pointer;
    transition: background var(--motion-fast) var(--ease),
                border-color var(--motion-fast) var(--ease),
                color var(--motion-fast) var(--ease),
                box-shadow var(--motion-fast) var(--ease);
}
.studio-model-run-btn:hover {
    background: var(--accent);
    border-color: var(--accent);
    color: var(--accent-fg);
    box-shadow: 0 1px 2px var(--accent-ring);
}
.studio-model-run-btn:focus-visible {
    outline: none;
    box-shadow: 0 0 0 3px var(--accent-ring);
}
/* Model column header — quiet, premium accent rather than a loud
   badge. The header reuses the standard column header chrome but
   layers a faint accent-tinted gradient + a 2px accent rule along
   the bottom edge so the column reads as "computed by a model"
   without shouting. The label text picks up the accent-strong
   colour for the same reason. No icon: the user explicitly asked
   that we differentiate via styling only. */
.studio-th-scope-model {
    background: linear-gradient(180deg, var(--accent-soft) 0%, var(--surface) 70%);
    box-shadow: inset 0 -2px 0 0 var(--accent-ring);
}
.studio-th-scope-model .studio-table-th-label-text {
    color: var(--accent-strong);
    font-weight: 600;
    letter-spacing: 0.01em;
}

/* ── Co-folding config modal ───────────────────────────────────
   The modal has more content than the default studio modal can
   gracefully hold (sequences, MSA + template uploads, three
   constraint groups, model settings) so it gets a wider shell and
   a vertically scrolling body. Width scales with viewport so on
   smaller laptops the modal still leaves margin around the page,
   but on a typical 1440 px+ display it lands at 960 px wide which
   matches the SuperGen Co-folding page's main column. */
.studio-model-config-modal {
    width: min(960px, 94vw);
    max-height: 90vh;
}
.studio-model-config-modal .studio-modal-body {
    padding: 1.25rem 1.5rem;
    overflow: auto;
    display: flex; flex-direction: column;
    gap: 1rem;
    background: var(--surface-soft, var(--surface));
}
/* Each top-level section is a card so users can scan the modal as
   discrete groups (General, Sequences, MSA, Template, Constraints,
   Affinity, Model settings) — same hierarchy as CofoldingPage's
   `.card` blocks. */
.studio-cofold-card {
    background: var(--surface);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-md);
    padding: 1.1rem 1.25rem;
}
.studio-cofold-card-title {
    margin: 0 0 0.875rem;
    font-size: 0.95rem; font-weight: 700;
    color: var(--text-strong);
    letter-spacing: -0.005em;
}
.studio-cofold-help {
    font-size: 0.8rem;
    color: var(--text-muted);
    margin-bottom: 0.625rem;
    line-height: 1.45;
}
.studio-cofold-warn {
    color: var(--warning-strong, var(--danger-strong));
}
.studio-cofold-empty-hint {
    font-size: 0.82rem;
    color: var(--text-muted);
    padding: 0.5rem 0.65rem;
    background: var(--surface-soft);
    border: 1px dashed var(--border);
    border-radius: var(--radius-sm);
    margin-bottom: 0.75rem;
}
/* Sub-headings within Constraints (Covalent bonds, Pocket restraints,
   Contacts) and within constraint rows (Atom 1 / Atom 2). Distinct
   weights so the user can tell which level they're looking at. */
.studio-cofold-subhead {
    margin: 1.25rem 0 0.5rem;
    font-size: 0.85rem; font-weight: 700;
    color: var(--text-strong);
}
.studio-cofold-subsubhead {
    font-size: 0.78rem; font-weight: 600;
    color: var(--text-muted);
    text-transform: uppercase; letter-spacing: 0.04em;
    margin-bottom: 0.4rem;
}

/* Per-chain sequence card. The textarea reuses .form-textarea so
   its font is the regular UI font, not monospace — the user
   explicitly preferred consistency with the rest of the form over
   the column-alignment benefit of monospace. */
.studio-cofold-seq {
    background: var(--surface-soft);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-md);
    padding: 0.75rem 0.85rem;
    display: flex; flex-direction: column;
    gap: 0.6rem;
    margin-bottom: 0.6rem;
}
.studio-cofold-seq-head {
    display: flex; align-items: center;
    gap: 1rem;
    flex-wrap: wrap;
}
.studio-cofold-seq-chain {
    font-weight: 700;
    color: var(--text-strong);
    font-size: 0.95rem;
    min-width: 4.5rem;
}
.studio-cofold-seq-remove { margin-left: auto; }
.studio-cofold-seq-input {
    /* Inherit the form textarea look — same font, same border,
       same focus ring. The previous monospace override felt
       inconsistent with the rest of the modal. Sequences don't
       benefit much from column alignment in a single-row chain
       textbox; the validation message below catches typos
       precisely without needing monospace. */
    min-height: 80px;
    line-height: 1.55;
    letter-spacing: 0.02em;
}
.studio-cofold-seq-error {
    font-size: 0.82rem;
    color: var(--danger-strong);
    background: var(--danger-bg);
    padding: 0.4rem 0.6rem;
    border-radius: var(--radius-sm);
    border: 1px solid var(--danger-border, var(--danger-strong));
}

.studio-cofold-mods {
    border-top: 1px dashed var(--border-strong);
    padding-top: 0.75rem;
    margin-top: 0.25rem;
}
.studio-cofold-mods-title {
    font-size: 0.85rem; font-weight: 600;
    color: var(--text-strong);
    margin-bottom: 0.5rem;
}
.studio-cofold-mod-remove { margin-top: 0.65rem; }

.studio-cofold-files {
    margin-top: 0.75rem;
    display: flex; flex-direction: column;
    gap: 0.5rem;
}
.studio-cofold-file-row {
    display: flex; align-items: center;
    gap: 0.5rem;
}
.studio-cofold-file-row .file-selected {
    margin: 0;
}
.studio-cofold-chain-select {
    min-width: 9rem;
    width: auto;
}

.studio-cofold-mapping-row {
    display: flex; align-items: end;
    gap: 0.5rem;
    margin-bottom: 0.5rem;
}
.studio-cofold-mapping-remove {
    margin-bottom: 1px;       /* visual baseline with the inputs above */
}

.studio-form-error {
    margin-top: 0.5rem;
    padding: 0.6rem 0.75rem;
    background: var(--danger-bg);
    color: var(--danger-strong);
    border: 1px solid var(--danger-border, var(--danger-strong));
    border-radius: var(--radius-sm);
    font-size: 0.85rem;
    line-height: 1.45;
}

/* Disabled checkbox inside the modal — the .checkbox-wrapper class
   itself already handles the cursor / opacity, but we drop the
   click target so a disabled affinity checkbox can't toggle. */
.checkbox-wrapper.is-disabled {
    opacity: 0.55;
    pointer-events: none;
}

/* ── MolGen.Dropdown ───────────────────────────────────────────
   Cross-page styled dropdown. Matches the form-input visual when
   closed (same height, border, focus ring) and opens a portaled
   menu styled with the site's surface tokens. Used wherever the
   default <select>'s open menu reads as out-of-place — modals,
   dense forms, places where the option labels are long enough
   that the OS native menu wraps unattractively.

   The trigger reuses .form-input/.form-select metrics so a
   .molgen-dropdown sitting beside .form-input fields lines up
   pixel-for-pixel without extra wrapping. */
.molgen-dropdown {
    width: 100%;
    display: inline-flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem;
    padding: 0.6875rem 1rem;
    padding-right: 2.5rem;
    background: var(--surface);
    border: 1.5px solid var(--border-strong);
    border-radius: var(--radius-md);
    color: var(--text-strong);
    font: inherit;
    font-size: 0.9375rem;
    text-align: left;
    cursor: pointer;
    transition: border-color var(--motion-fast) var(--ease),
                box-shadow var(--motion-fast) var(--ease),
                background var(--motion-fast) var(--ease);
    position: relative;
}
.molgen-dropdown:hover { border-color: var(--accent-ring); }
.molgen-dropdown:focus-visible,
.molgen-dropdown.is-open {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 3px var(--accent-ring);
}
/* Compact variant — matches `.form-input-sm` geometry so dropdowns
   sit on the same baseline as compact admin form inputs (used by
   the Individual Accounts create form, Activity Log filter row,
   etc). Keep the same border/radius/font-family so the trigger
   still reads as a styled control, just shorter. */
.molgen-dropdown.molgen-dropdown-sm {
    padding: 0.3rem 0.55rem;
    padding-right: 2rem;
    font-size: 0.84rem;
    line-height: 1.3;
    border-width: 1px;
}
.molgen-dropdown.molgen-dropdown-sm .molgen-dropdown-chevron {
    right: 0.55rem;
}
.molgen-dropdown.molgen-dropdown-sm .molgen-dropdown-chevron svg {
    width: 12px; height: 12px;
}
.molgen-dropdown.is-disabled {
    cursor: not-allowed;
    background: var(--surface-soft);
    color: var(--text-muted);
    opacity: 0.7;
}
.molgen-dropdown-value {
    flex: 1;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.molgen-dropdown-placeholder {
    color: var(--text-muted);
}
.molgen-dropdown-chevron {
    position: absolute;
    right: 0.875rem;
    top: 50%;
    transform: translateY(-50%);
    color: var(--text-muted);
    display: inline-flex;
    pointer-events: none;
    transition: transform var(--motion-fast) var(--ease);
}
.molgen-dropdown.is-open .molgen-dropdown-chevron {
    transform: translateY(-50%) rotate(180deg);
}

.molgen-dropdown-menu {
    background: var(--surface);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-3, 0 8px 24px rgba(15, 23, 42, 0.12));
    padding: 4px;
    /* z-index has to clear modal overlays (z=1000) so dropdowns
       opened inside a modal float on top. */
    z-index: 1100;
    overflow-y: auto;
    overscroll-behavior: contain;
    animation: slideUpFade 0.14s var(--ease);
}
.molgen-dropdown-menu:focus { outline: none; }
.molgen-dropdown-option {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem;
    width: 100%;
    padding: 0.5rem 0.75rem;
    background: transparent;
    border: none;
    border-radius: var(--radius-sm);
    color: var(--text);
    font: inherit;
    font-size: 0.9rem;
    text-align: left;
    cursor: pointer;
    transition: background var(--motion-fast) var(--ease),
                color var(--motion-fast) var(--ease);
}
.molgen-dropdown-option:hover,
.molgen-dropdown-option.is-highlight {
    background: var(--accent-soft);
    color: var(--accent-strong);
}
.molgen-dropdown-option.is-selected {
    color: var(--accent-strong);
    font-weight: 600;
}
.molgen-dropdown-option.is-disabled {
    color: var(--text-faint);
    cursor: not-allowed;
    background: transparent;
}
.molgen-dropdown-option-placeholder {
    color: var(--text-muted);
    font-style: italic;
}
.molgen-dropdown-option-label {
    flex: 1;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.molgen-dropdown-option-check {
    flex-shrink: 0;
    color: var(--accent);
}
.molgen-dropdown-empty {
    padding: 0.75rem;
    text-align: center;
    color: var(--text-muted);
    font-size: 0.85rem;
}


/* ── Admin page ──────────────────────────────────────────────────
   Two-mode admin surface (Global Admin / Org Admin). Tabs at the
   top of Global; flat user list for Org. Reuses the platform's
   card + button styles — only the table chrome, role/active badges,
   and inline-form layout are bespoke. */

.admin-tabs {
    display: flex;
    gap: 0.5rem;
    margin-bottom: 1rem;
    border-bottom: 1px solid var(--border);
}
.admin-tab {
    padding: 0.6rem 1rem;
    background: transparent;
    border: none;
    border-bottom: 2px solid transparent;
    color: var(--text-muted);
    font-weight: 500;
    cursor: pointer;
    transition: color 120ms, border-color 120ms;
}
.admin-tab:hover { color: var(--text); }
.admin-tab.active {
    color: var(--accent-strong);
    border-bottom-color: var(--accent);
}

.admin-card-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    margin-bottom: 1rem;
}
.admin-card-head h3 { margin: 0; }

.admin-inline-form {
    display: grid;
    grid-template-columns: 1fr 1fr auto;
    gap: 0.5rem;
    margin-bottom: 1rem;
    padding: 0.75rem;
    background: var(--surface-soft);
    border: 1px solid var(--border-faint);
    border-radius: var(--radius-md);
}
@media (max-width: 720px) {
    .admin-inline-form {
        grid-template-columns: 1fr;
    }
}

.admin-table {
    width: 100%;
    border-collapse: collapse;
}
.admin-table thead th {
    text-align: left;
    padding: 0.5rem 0.75rem;
    font-size: 0.8rem;
    font-weight: 600;
    color: var(--text-muted);
    text-transform: uppercase;
    letter-spacing: 0.04em;
    border-bottom: 1px solid var(--border);
}
.admin-table tbody td {
    padding: 0.75rem;
    border-bottom: 1px solid var(--border-faint);
    vertical-align: middle;
    font-size: 0.9rem;
}
.admin-table tbody tr:last-child td { border-bottom: 0; }
.admin-table tbody tr:hover td { background: var(--surface-soft); }
.admin-table .muted { color: var(--text-muted); font-weight: 400; }
.admin-table .small { font-size: 0.8rem; }
.admin-table .mono { font-family: var(--font-mono); }
.admin-actions-cell {
    display: flex;
    gap: 0.4rem;
    align-items: center;
    flex-wrap: wrap;
}

/* ── Activity Log panel ──────────────────────────────────────────
   Tighter geometry than the default `.card` because the panel
   carries a lot of rows; we want the eye to scan the table, not
   the chrome. All controls drop to `.btn-small` so the action row
   doesn't visually dominate the filter grid. */
.admin-activity-log {}
.admin-activity-log-header {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 0.75rem;
    padding: 0.75rem 1rem;
}
.admin-activity-log-title h3 { margin: 0; font-size: 1rem; }
.admin-activity-log-title p { margin: 0.15rem 0 0; font-size: 0.8rem; }
.admin-activity-log-body { padding: 0.75rem 1rem 1rem; }

.admin-activity-log-filters {
    display: grid;
    /* 6 compact filters per row at desktop; collapse to 3 / 2 as the
       viewport narrows so the form stays readable without dwarfing
       the table below it. */
    grid-template-columns: repeat(6, minmax(0, 1fr));
    gap: 0.5rem 0.6rem;
    margin-bottom: 0.6rem;
}
@media (max-width: 1100px) {
    .admin-activity-log-filters { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@media (max-width: 640px) {
    .admin-activity-log-filters { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
.admin-activity-log-filter .form-label {
    font-size: 0.72rem;
    margin-bottom: 0.2rem;
    color: var(--text-muted);
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

/* Compact form input variant — slightly shorter padding so the
   filter row stays low-profile. Keeps the default `.form-input`
   look elsewhere untouched. */
.form-input-sm,
input.form-input-sm,
select.form-input-sm {
    padding: 0.3rem 0.55rem;
    font-size: 0.84rem;
    line-height: 1.3;
    height: auto;
}

/* A compact <select> that also opts into .form-select (the
   site-standard custom chevron) needs its right padding restored —
   the .form-input-sm shorthand above otherwise clobbers
   .form-select's padding-right and the text runs under the arrow.
   Pull the chevron in to match the tighter compact geometry. */
select.form-select.form-input-sm {
    padding-right: 1.9rem;
    background-position: right 0.6rem center;
}

/* Native datetime-local controls in compact filter rows: match the
   custom select's box model + height and tame the browser picker
   icon so the Status / Since / Until trio reads as one consistent
   control group. Popup calendar rendering stays native (intentional
   — no heavy date-picker dependency). */
.admin-activity-log-filter input[type="datetime-local"].form-input-sm {
    box-sizing: border-box;
    color-scheme: light;
    cursor: text;
}
.admin-activity-log-filter input[type="datetime-local"].form-input-sm::-webkit-calendar-picker-indicator {
    cursor: pointer;
    opacity: 0.55;
    transition: opacity var(--motion-fast) var(--ease);
}
.admin-activity-log-filter input[type="datetime-local"].form-input-sm:hover::-webkit-calendar-picker-indicator {
    opacity: 0.85;
}
/* Status dropdown (MolGen.Dropdown) compacted to the same box model
   as the .form-input-sm date inputs beside it, so Status / Since /
   Until line up on one clean row. The component already supplies the
   site-themed closed control AND open menu (border, radius, focus
   ring, chevron, hover/active) — this only matches the compact
   filter-row height. */
.admin-activity-log-filter .molgen-dropdown.admin-activity-log-status-dd {
    padding: 0.3rem 1.7rem 0.3rem 0.55rem;
    font-size: 0.84rem;
    line-height: 1.3;
    min-height: 0;
}
/* Keep all three filter controls the same height regardless of
   intrinsic content metrics. */
.admin-activity-log-filter .molgen-dropdown.admin-activity-log-status-dd,
.admin-activity-log-filter input[type="datetime-local"].form-input-sm {
    height: 30px;
    box-sizing: border-box;
}

/* ── Custom date-time field (Activity Log Since/Until) ───────────
   Native input kept for typing/format/keyboard; its OS picker
   indicator is suppressed and a site-styled popover is opened by the
   adjacent calendar button. Input + button render as one control. */
.admin-dtf { display: inline-flex; align-items: stretch; width: 100%; border-radius: var(--radius-md); }
.admin-dtf-input {
    flex: 1; min-width: 0;
    border-top-right-radius: 0; border-bottom-right-radius: 0;
    border-right: none;
}
/* Focus reads as ONE control: the input's own ring is suppressed and a
   single accent ring + border is drawn around the whole input+button
   group via :focus-within (previously only the text input highlighted,
   leaving the calendar button visually detached). */
/* Suppress the input's OWN focus ring only — the wrapper draws the single
   ring below. (Don't touch border-color here: it must stay accent so the
   input + button read as one highlighted unit.) */
.admin-dtf .admin-dtf-input:focus { box-shadow: none; }
.admin-dtf:focus-within { box-shadow: 0 0 0 3px var(--accent-ring); }
.admin-dtf:focus-within .admin-dtf-input,
.admin-dtf:focus-within .admin-dtf-btn { border-color: var(--accent); }
.admin-dtf:focus-within .admin-dtf-btn { color: var(--accent); }
/* Kill the native calendar/clock popup trigger — our button replaces it. */
.admin-dtf-input::-webkit-calendar-picker-indicator { display: none; }
.admin-dtf-btn {
    display: flex; align-items: center; justify-content: center;
    /* Stretch to the input's height (align-items:stretch on .admin-dtf)
       rather than pinning a fixed 30px — the .form-input-sm field computes
       to ~30.07px, so a hard 30px left the button's bottom border ~1px
       above the input's, mis-aligning the joined control's underline. */
    width: 30px; align-self: stretch; flex-shrink: 0;
    box-sizing: border-box;
    border: 1.5px solid var(--border-strong);
    border-left: none;
    border-radius: 0 var(--radius-md) var(--radius-md) 0;
    background: var(--surface);
    color: var(--text-muted);
    cursor: pointer;
    transition: background var(--motion-fast) var(--ease),
                color var(--motion-fast) var(--ease);
}
.admin-dtf-btn:hover { background: var(--surface-soft); color: var(--accent); }
.admin-dtf-btn:focus-visible {
    outline: none;
    border-color: var(--accent);
    /* No separate ring — the wrapper's :focus-within ring already wraps
       the whole control, so a second box-shadow here would double up. */
}
.admin-dtf-pop {
    z-index: 2147483646;
    width: 260px;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-modal);
    padding: 0.6rem;
    font-size: 0.84rem;
}
.admin-dtf-pop-head {
    display: flex; align-items: center; justify-content: space-between;
    margin-bottom: 0.45rem;
}
.admin-dtf-month { font-weight: 600; color: var(--text-strong); }
.admin-dtf-nav {
    width: 24px; height: 24px;
    border: none; background: transparent;
    color: var(--text-muted); cursor: pointer;
    border-radius: var(--radius-sm, 6px);
    font-size: 1rem; line-height: 1;
}
.admin-dtf-nav:hover { background: var(--surface-soft); color: var(--accent); }
.admin-dtf-grid {
    display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px;
}
.admin-dtf-dow { margin-bottom: 2px; }
.admin-dtf-dow-cell {
    text-align: center; font-size: 0.7rem; font-weight: 600;
    color: var(--text-faint); padding: 2px 0;
}
.admin-dtf-day {
    aspect-ratio: 1 / 1;
    border: none; background: transparent;
    color: var(--text-strong); cursor: pointer;
    border-radius: var(--radius-sm, 6px);
    font-size: 0.8rem;
    transition: background var(--motion-fast) var(--ease),
                color var(--motion-fast) var(--ease);
}
.admin-dtf-day.is-empty { cursor: default; }
.admin-dtf-day:not(.is-empty):hover { background: var(--surface-soft); }
.admin-dtf-day.is-today { box-shadow: inset 0 0 0 1.5px var(--accent-ring); }
.admin-dtf-day.is-selected {
    background: var(--accent); color: #fff; font-weight: 600;
}
.admin-dtf-pop-foot {
    margin-top: 0.55rem; padding-top: 0.55rem;
    border-top: 1px solid var(--border);
    display: flex; flex-direction: column; gap: 0.5rem;
}
.admin-dtf-time-label {
    display: flex; align-items: center; gap: 0.5rem;
    font-size: 0.78rem; color: var(--text-muted);
}
.admin-dtf-time { flex: 1; height: 28px; }
.admin-dtf-actions {
    display: flex; gap: 0.4rem; justify-content: flex-end;
}
/* Compact the Clear / Now / Apply trio in the calendar popover — the
   default .btn-small reads oversized in a 260px popup. Scoped so only
   these popover buttons shrink. */
.admin-dtf-actions .btn-small {
    padding: 0.25rem 0.6rem;
    font-size: 0.78rem;
}

.admin-activity-log-actions {
    display: flex;
    gap: 0.4rem;
    align-items: center;
    /* Apply / Clear belong on the right so they don't compete with
       the filter inputs above them. Order in markup is preserved
       (Apply first, then Clear) — the primary action lands at the
       far-right edge of the toolbar, matching admin convention. */
    justify-content: flex-end;
    flex-wrap: wrap;
    margin-bottom: 0.6rem;
}
/* Clear / Apply / Delete selected — compact, matching the 30px
   filter controls above them; default .btn-small reads oversized
   next to the .form-input-sm filter row. */
.admin-activity-log-actions .btn-small {
    padding: 0.28rem 0.7rem;
    font-size: 0.8rem;
}
.admin-activity-log-count {
    margin-left: auto;
    color: var(--text-muted);
    font-size: 0.82rem;
}

/* Table — tighter row height + smaller font than the default
   admin-table, since each row already carries an actor badge and a
   monospace action label. */
/* (Activity Log + Invite Keys tables now use the shared
   `.admin-table-scroll` wrapper — same horizontal-scroll behavior as the
   All Users / Organizations tables.) */
.admin-activity-log-table thead th {
    padding: 0.4rem 0.65rem;
    font-size: 0.72rem;
}
.admin-activity-log-table tbody td {
    padding: 0.5rem 0.65rem;
    font-size: 0.84rem;
    vertical-align: top;
}
.admin-activity-log-when {
    white-space: nowrap;
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
}
.admin-activity-log-actor {
    font-weight: 500;
    margin-bottom: 2px;
    /* One line at any viewport width — long usernames ellipsize
       instead of wrapping and stretching the row. */
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
/* Badges in the log table truncate inside their fixed-layout column
   rather than wrapping (the base .role-badge/.active-badge rules pin
   white-space: nowrap; this caps them at the cell width so a narrow
   Actor/Status column can't be stretched by an over-wide badge). */
.admin-activity-log-table .role-badge,
.admin-activity-log-table .active-badge {
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    vertical-align: bottom;
}
.admin-activity-log-action {
    font-family: var(--font-mono);
    font-size: 0.78rem;
    padding: 0.1rem 0.3rem;
    background: var(--surface-sunk);
    border: 1px solid var(--border-faint);
    border-radius: var(--radius-sm, 4px);
    white-space: nowrap;
}
.admin-activity-log-target { font-weight: 500; }
/* `.admin-activity-log-target-meta`, `.admin-activity-log-extra`,
   `.admin-activity-log-extra-toggle`, and the previous pre-block
   variant have been removed — the details panel now uses the
   shared `.admin-iik-key-row` chrome for monospace blocks and
   `.admin-activity-log-extra-pre` is redefined further down
   inside the layout block where it actually renders. The bottom
   pagination is the platform `<Pager>` component, so the
   `.admin-activity-log-pagination` rules are gone too. */

/* ── Individual Accounts invite keys card ────────────────────────
   Sibling to the per-org Invite Keys card. Same tight geometry as
   the Activity Log so the global-admin invite-keys tab reads as
   one consistent surface. */
.admin-individual-keys-card {
    /* Match the plain .card metrics (1.75rem padding / 1.25rem margin) so
       this block aligns with the Organization Invite Keys card — they read
       as the same component family. (Was 0.75rem/1rem — visibly tighter.) */
    margin-bottom: 1.25rem;
    padding: 1.75rem;
}
.admin-keys-create-form {
    padding: 0.6rem 0.75rem 0.75rem;
    background: var(--surface-sunk);
    border: 1px solid var(--border-faint);
    border-radius: var(--radius-md);
    margin-bottom: 0.6rem;
}
.admin-keys-create-row {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: 0.6rem;
    margin-bottom: 0.4rem;
}
/* Tighten label↔control spacing in both invite-key create forms
   (Individual + Organization) — the default .form-label 0.5rem bottom
   margin reads as too loose in these compact, gridded forms. Also drop
   the default .form-group bottom margin inside the gridded rows so grid
   gap is the single source of spacing. */
.admin-keys-create-form .form-label,
.admin-keys-form .form-label { margin-bottom: 0.25rem; }
.admin-keys-create-row .form-group { margin-bottom: 0; }
.admin-keys-create-form > .form-group { margin-bottom: 0.55rem; }
@media (max-width: 900px) {
    .admin-keys-create-row { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 540px) {
    .admin-keys-create-row { grid-template-columns: 1fr; }
}
.admin-individual-keys-table thead th {
    padding: 0.4rem 0.65rem;
    font-size: 0.72rem;
}
.admin-individual-keys-table tbody td {
    padding: 0.5rem 0.65rem;
    font-size: 0.84rem;
    vertical-align: middle;
}

/* Column widths — proportional, not pixel-exact. The Name column
   gets the most room (it carries the Show details toggle), Key
   gets enough for the prefix + eye, Plan / Uses / Status are
   compact. Last used can wrap to its container's natural width. */
.admin-individual-keys-table .iik-col-name    { width: 22%; }
.admin-individual-keys-table .iik-col-key     { width: 22%; }
.admin-individual-keys-table .iik-col-plan    { width: 9%; }
.admin-individual-keys-table .iik-col-uses    { width: 7%; }
.admin-individual-keys-table .iik-col-expires { width: 10%; }
.admin-individual-keys-table .iik-col-status  { width: 10%; }
.admin-individual-keys-table .iik-col-last    { width: 14%; }
.admin-individual-keys-table .iik-col-actions { width: 56px; }
.admin-individual-keys-table .admin-iik-uses {
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
.admin-iik-name {
    font-weight: 600;
    color: var(--text-strong);
}

/* Plan badge — small badge so the column doesn't read as plain
   prose. Same chrome as the activity-log chip family. Square-ish
   radius (admin badges standardize on --radius-sm, not pills). */
.admin-plan-pill {
    display: inline-block;
    padding: 0.1rem 0.55rem;
    background: var(--surface-sunk);
    border: 1px solid var(--border-faint);
    border-radius: var(--radius-sm);
    font-size: 0.78rem;
    font-weight: 500;
    color: var(--text);
}

/* Create-form action row — right-aligned, Cancel before primary,
   matches the convention used by ConfirmModal's
   `.admin-confirm-actions`. */
.admin-keys-create-actions {
    display: flex;
    justify-content: flex-end;
    gap: 0.5rem;
    margin-top: 0.5rem;
    padding-top: 0.5rem;
    border-top: 1px solid var(--border-faint);
}
.admin-keys-create-actions .btn { min-width: 88px; }

/* Compact icon-only admin button — used by the Activity Log
   refresh button and the Individual Accounts eye-reveal. Matches
   the visual weight of the per-row action menu trigger so the
   filter toolbar doesn't grow taller. */
.admin-icon-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    padding: 0;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm, 6px);
    background: var(--surface);
    color: var(--text-muted);
    cursor: pointer;
    transition: background var(--motion-fast) var(--ease),
                color var(--motion-fast) var(--ease),
                border-color var(--motion-fast) var(--ease);
}
.admin-icon-btn:hover:not(:disabled) {
    background: var(--surface-soft);
    border-color: var(--accent-ring, var(--border));
    color: var(--text-strong);
}
.admin-icon-btn:disabled {
    opacity: 0.45;
    cursor: not-allowed;
}
.admin-icon-btn svg { display: block; }

/* Activity Log row chip — splits an action label like
   `invite_key.reveal` into a namespace part and a verb part so
   the column reads as structured data, not prose. */
.admin-activity-log-chip {
    display: inline-flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 0.15rem 0.3rem;
    padding: 0.15rem 0.5rem;
    background: var(--surface-sunk);
    border: 1px solid var(--border-faint);
    border-radius: var(--radius-sm);
    font-family: var(--font-mono);
    font-size: 0.74rem;
    line-height: 1.35;
    /* Wrap long verbs (`individual.create`, `redeem.transfer`) onto
       a second line inside the chip rather than truncating the
       Action column. */
    white-space: normal;
    word-break: break-word;
    max-width: 100%;
    color: var(--text);
}
.admin-activity-log-chip-ns {
    color: var(--accent, var(--text-strong));
    font-weight: 600;
}
.admin-activity-log-chip-sep { color: var(--text-muted); }
.admin-activity-log-chip-verb { color: var(--text); }

/* Activity Log table — compact and visually structured. Column
   widths rebalanced so:
     · `When` gets the room a `MMM D, HH:MM` stamp needs,
     · `Action` gets enough width for the chip to render on one
       line for most labels (and wrap cleanly when it can't),
     · `Target` shows label only (type/id moved to details). */
.admin-activity-log-table { table-layout: fixed; }
/* Responsive: the 7-column log needs more room than the generic 640px
   `.admin-table-scroll > .admin-table` minimum — at 640px the Action chip
   and Target label crush together. Give it a readable floor so a narrow
   viewport scrolls the wrapper horizontally (content stays legible) instead
   of squeezing the columns. Desktop is unchanged (table still fills 100%). */
.admin-table-scroll > table.admin-activity-log-table { min-width: 860px; }
.admin-activity-log-table .al-col-when    { width: 13%; }
.admin-activity-log-table .al-col-actor   { width: 16%; }
.admin-activity-log-table .al-col-action  { width: 26%; }
.admin-activity-log-table .al-col-target  { width: 21%; }
.admin-activity-log-table .al-col-status  { width: 11%; }
.admin-activity-log-table .al-col-details { width: 13%; }

.admin-activity-log-table tbody td {
    vertical-align: middle;
    /* Allow cells to participate in the fixed layout without
       overflowing. Individual cells override word-break / wrap
       where needed. */
    word-break: break-word;
}
.admin-activity-log-table tbody tr.has-expansion td {
    border-bottom: 0;
}
.admin-activity-log-toggle {
    font-size: 0.78rem;
}
.admin-activity-log-details-cell {
    white-space: nowrap;
}
.admin-activity-log-when {
    white-space: nowrap;
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
    font-size: 0.8rem;
}
.admin-activity-log-target-cell .admin-activity-log-target {
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    word-break: break-word;
    font-weight: 500;
}

/* Expansion row — clean two-column-ish grid for the metadata
   fields, with the JSON metadata block spanning full width so it
   doesn't fight with the column layout. */
.admin-activity-log-expansion-row > td {
    background: var(--surface-sunk);
    padding: 0 0 0.65rem;
}
.admin-activity-log-expansion {
    padding: 0.6rem 0.95rem;
}
.admin-activity-log-expansion-fields {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 0.5rem 1.5rem;
}
@media (max-width: 720px) {
    .admin-activity-log-expansion-fields { grid-template-columns: 1fr; }
}
.admin-activity-log-extra-field {
    grid-column: 1 / -1;
}
.admin-activity-log-ua-wrap {
    /* Long UAs need to wrap inside the field, not push the cell. */
    display: block;
    word-break: break-all;
    white-space: normal;
    line-height: 1.35;
}
/* Legacy alias — older selector kept around as the metadata
   block fallback, but the field-grid above is now the canonical
   layout. Left in place so any cached styles don't fight it. */
.admin-activity-log-expansion-meta {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem 1.5rem;
    margin-bottom: 0.45rem;
}
.admin-activity-log-ua {
    display: inline-block;
    max-width: 520px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    vertical-align: bottom;
}

/* JSON pre block — compact, scrollable, full width of its grid
   cell. */
.admin-activity-log-extra-pre {
    max-width: 100%;
    max-height: 240px;
    overflow: auto;
    margin: 0.25rem 0 0;
    padding: 0.55rem 0.7rem;
    background: var(--surface);
    border: 1px solid var(--border-faint);
    border-radius: var(--radius-md);
    font-size: 0.74rem;
    line-height: 1.4;
    white-space: pre;
}

/* Individual Accounts invite key — compact key field used inside
   Show details. One-line monospace with horizontal-scroll
   overflow + an icon-only Copy button to its right. */
.admin-iik-key-row {
    display: flex;
    align-items: center;
    gap: 0.45rem;
    margin-top: 0.25rem;
    max-width: 100%;
}
.admin-iik-key-code {
    flex: 1 1 auto;
    min-width: 0;
    display: block;
    padding: 0.3rem 0.55rem;
    background: var(--surface);
    border: 1px solid var(--border-faint);
    border-radius: var(--radius-sm, 6px);
    font-family: var(--font-mono);
    font-size: 0.82rem;
    line-height: 1.35;
    color: var(--text-strong);
    white-space: nowrap;
    overflow-x: auto;
    overflow-y: hidden;
    /* iOS / Edge friendlier scrollbar gutter so the code doesn't
       jump when scrollbar appears on hover. */
    scrollbar-width: thin;
}
.admin-iik-key-code-masked {
    color: var(--text);
    overflow: hidden;
    text-overflow: ellipsis;
}
.admin-iik-copy-btn {
    flex: 0 0 auto;
}
.admin-iik-reveal-inline {
    flex: 0 0 auto;
    margin-left: auto;
}
.admin-iik-reveal-icon { flex: 0 0 auto; }
.admin-iik-key-note { flex: 1 1 auto; }

/* Inline-reveal spinner — 360° rotation while `revealBusyId === k.id`.
   Tiny SVG inside the icon button; doesn't affect the row height. */
.admin-iik-spin {
    animation: admin-iik-spin 0.8s linear infinite;
    transform-origin: center;
}
@keyframes admin-iik-spin {
    from { transform: rotate(0deg); }
    to   { transform: rotate(360deg); }
}

/* ── Account Settings page — compact controls ────────────────────
   Scope ONLY to `.account-page` so the admin / studio / job /
   login surfaces keep their default sizing. Reduces button and
   input height to match the platform's premium-but-compact feel.
   Validation, focus rings, and primary/secondary/destructive
   hierarchy are preserved. */
.account-page .btn {
    padding: 0.45rem 1.05rem;
    font-size: 0.875rem;
    border-radius: var(--radius-md);
}
/* Leave the SVG sizing at the platform default (16px) so the eye
   icon on the API-key Show button and any future in-button icons
   stay visually consistent with other Account Settings icons.
   Earlier this override shrank them to 14px, which read as "much
   too small" against the section h3 icons (~20px). */
.account-page .btn-small {
    padding: 0.3rem 0.8rem;
    font-size: 0.8rem;
}
.account-page .btn-small svg { width: 14px; height: 14px; }

/* API-key Show/Hide + Copy — compact ICON-ONLY buttons sitting beside
   the key input. They stretch to the input's height (the row is
   align-items:stretch) and stay square-ish via trimmed equal padding.
   The 18px glyph reads at full weight next to the surrounding h3 icons.
   Scoped to `.account-key-icon-btn` so other pages keep the default. */
.account-page .account-key-icon-btn {
    flex-shrink: 0;
    padding: 0.45rem 0.6rem;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}
.account-page .account-key-icon-btn svg {
    width: 18px;
    height: 18px;
    flex-shrink: 0;
    stroke-width: 2;
}

/* ── Role-change confirm modal ───────────────────────────────────
   Shared layout with the org-move summary block — same labelled
   key/value pairs and the same `→` separator between current and
   new role. Keeps both confirmation flows visually consistent. */
.admin-role-change-summary {
    display: grid;
    grid-template-columns: 1fr;
    gap: 0.4rem;
    margin: 0.6rem 0 0.4rem;
    padding: 0.65rem 0.8rem;
    background: var(--surface-sunk);
    border: 1px solid var(--border-faint);
    border-radius: var(--radius-md);
}
.admin-role-change-summary > div {
    display: flex;
    align-items: baseline;
    gap: 0.5rem;
}
.admin-role-change-summary > div > span.muted {
    flex: 0 0 110px;
    font-size: 0.78rem;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--text-muted);
}
.admin-role-change-summary > div > strong { font-weight: 600; }
.admin-role-change-row {
    display: grid !important;
    grid-template-columns: 1fr auto 1fr;
    gap: 0.6rem !important;
    align-items: center !important;
    padding-top: 0.5rem;
    margin-top: 0.4rem;
    border-top: 1px dashed var(--border-faint);
}
.admin-role-change-row > div {
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
}
.admin-role-change-row > div > span.muted {
    font-size: 0.72rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
}
.admin-role-change-row .admin-role-change-arrow {
    color: var(--text-muted);
    font-size: 1.3rem;
    line-height: 1;
    text-align: center;
}

/* ── User details modal ──────────────────────────────────────────
   Same `.admin-edit-modal` chrome as the org-edit / delete-user
   modals, just wider so the metadata grid + password reset section
   sit comfortably without horizontal scroll. */
.admin-user-details-modal {
    width: min(720px, 96vw);
    max-height: 90vh;
}
.admin-user-details-meta { margin-bottom: 1rem; }
.admin-user-details-section {
    margin-top: 1rem;
    padding-top: 0.85rem;
    border-top: 1px solid var(--border-faint);
}
.admin-user-details-section h4 {
    margin: 0 0 0.35rem;
    font-size: 0.95rem;
    color: var(--text-strong);
}
.admin-user-details-activity {
    /* Inherits .admin-table — compact rows so the section doesn't
       bloat the modal. */
    margin-top: 0.5rem;
}
.admin-user-details-activity thead th {
    padding: 0.35rem 0.65rem;
    font-size: 0.72rem;
}
.admin-user-details-activity tbody td {
    padding: 0.4rem 0.65rem;
    font-size: 0.8rem;
    vertical-align: middle;
}

/* Password reset row — input + eye toggle on the same baseline.
   `align-items: stretch` so the icon button matches the (compact)
   input's height exactly, regardless of which size variant the
   input uses. */
.admin-user-pw-row {
    display: flex;
    align-items: stretch;
    gap: 0.4rem;
}
.admin-user-pw-row .form-input {
    flex: 1 1 auto;
    min-width: 0;
}
.admin-user-pw-toggle {
    flex: 0 0 auto;
    width: auto;
    min-width: 36px;
    padding: 0 0.45rem;
    height: auto;
}
.admin-user-pw-toggle svg { width: 16px; height: 16px; }

/* Reset Password section — keep the section visually lighter than
   the surrounding card body. The h4 + bullet description provide
   enough hierarchy on their own; the previous tinted background
   read as "this section is special" which it isn't. */
.admin-user-details-reset {
    background: transparent;
    border-top: 1px solid var(--border-faint);
    margin-top: 1rem;
    padding-top: 0.85rem;
}
.admin-user-details-reset-desc {
    margin: 0 0 0.6rem;
    line-height: 1.45;
}
.admin-user-details-reset-actions {
    border-top: none !important;
    padding: 0.25rem 0 0 !important;
    background: transparent !important;
}

/* Activity Log row checkbox — narrow column, centered. Same
   visual chrome as the organizations-table checkbox column so the
   admin surface stays consistent. */
.admin-activity-log-table .al-col-select { width: 36px; }
.admin-activity-log-table .admin-th-checkbox,
.admin-activity-log-table .admin-td-checkbox {
    width: 36px;
    padding: 0;
    text-align: center;
    vertical-align: middle;
}
.admin-activity-log-table .admin-th-checkbox input[type="checkbox"],
.admin-activity-log-table .admin-td-checkbox input[type="checkbox"] {
    width: 14px;
    height: 14px;
    margin: 0;
    cursor: pointer;
    accent-color: var(--accent);
    vertical-align: middle;
}
.admin-activity-log-table tbody tr.is-selected td {
    background: var(--accent-soft);
}
/* Form fields — input + native select used by the redeem-key form
   and password fields. Vertical padding shrinks from 11px to 7.5px;
   font drops from 15px to 14px. Border / focus styles inherited. */
.account-page .form-input,
.account-page .form-textarea,
.account-page .form-select {
    padding: 0.45rem 0.75rem;
    font-size: 0.875rem;
    line-height: 1.4;
}
.account-page .form-label {
    font-size: 0.82rem;
}
/* Card titles inside the page — slightly tighter so the h3 + icon
   row doesn't feel oversized next to the new compact controls. */
.account-page .card h3 {
    font-size: 1.02rem;
}

.role-badge {
    display: inline-block;
    padding: 0.15rem 0.5rem;
    border-radius: var(--radius-sm);
    font-size: 0.72rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    border: 1px solid transparent;
    /* Badges never wrap — a two-line "GLOBAL ADMIN" badge used to
       stretch its table row on narrow viewports. */
    white-space: nowrap;
}
.role-badge-member {
    background: var(--surface-soft);
    color: var(--text-muted);
    border-color: var(--border);
}
.role-badge-org {
    background: var(--accent-soft);
    color: var(--accent-strong);
    border-color: var(--accent);
}
.role-badge-global {
    background: #fef3c7;
    color: #92400e;
    border-color: #f59e0b;
}

.active-badge {
    display: inline-block;
    padding: 0.15rem 0.55rem;
    border-radius: var(--radius-sm);
    font-size: 0.74rem;
    font-weight: 600;
    border: 1px solid transparent;
    white-space: nowrap;
}
.active-badge.is-active {
    background: #ecfdf5;
    color: #065f46;
    border-color: #10b981;
}
.active-badge.is-inactive {
    background: #fef2f2;
    color: #991b1b;
    border-color: #f87171;
}

.form-input-compact {
    padding: 0.3rem 0.55rem !important;
    font-size: 0.85rem !important;
    width: auto !important;
    min-width: 9rem;
}

/* Org block on Account page */
.account-org-block {
    display: flex;
    align-items: center;
    gap: 0.75rem;
}
.account-org-block .org-name {
    font-weight: 600;
    color: var(--text-strong);
}
.account-org-block .org-role-row {
    display: flex;
    gap: 0.5rem;
    align-items: center;
    margin-top: 0.25rem;
    color: var(--text-muted);
    font-size: 0.85rem;
}

/* Results page org label — far-right hint in the tab row. */
.results-org-label {
    margin-left: auto;
    color: var(--text-muted);
    font-size: 0.85rem;
}
.results-org-label .org-name {
    color: var(--text-strong);
    font-weight: 600;
}



/* ── Admin page polish — search/paging/cards/modals ──────────────
   Adds the commercial-readiness chrome on top of the base Admin
   styles defined further up: search bar in card headers, paginator,
   collapsible org cards in the All Users view, custom confirm /
   edit modals, and a row-level "More actions" menu. */

.admin-card-head-actions {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    flex-wrap: wrap;
}
/* Search box in the card-head actions. Desktop min-width lives here (a
   class) instead of an inline style, so the narrow-width override below
   can release it WITHOUT `!important`. */
.admin-search-input { min-width: 220px; }
.admin-inline-form-compact {
    padding: 0.55rem;
    margin-bottom: 0.75rem;
}
.admin-empty {
    padding: 1.5rem 1rem;
    border: 1px dashed var(--border-strong);
    border-radius: var(--radius-md);
    color: var(--text-muted);
    background: var(--surface-soft);
    text-align: center;
    font-size: 0.9rem;
}
.admin-actions-col { width: 1%; white-space: nowrap; }
/* The canonical `.admin-pager` rule lives further down (search
   for "Admin pager: count left, controls truly centered"). The
   earlier scaffolding rules were consolidated into that block —
   if you find a stale `.admin-pager { … }` near here in a future
   bisect, it has already been removed. */

/* Visual mark for protected organizations. The platform-owner row
   needs to be recognisable at a glance — identification flows from
   the stable `organizations.is_protected` column (see backend
   migration 0017); the pill below is purely visual.

   The pill keeps a fixed visual identity across every render site
   (Organizations table, All Users card head, Invite Keys card
   head) by pinning its OWN font-size, line-height, padding and
   min-width — so the surrounding context's font-size (e.g.
   `.admin-org-card-title strong { font-size: 0.94rem }` vs the
   table cell's 0.9rem) never changes the pill's dimensions. The
   `999px` radius gives a fully rounded "soft pill" shape that
   reads as a label rather than a button.

   The fixed line-height (1.2) + 0.18rem vertical padding plus
   the 1px border total ~25px — well inside the table's 52px row
   height, so this rule never expands rows or card heads. */
.admin-org-name-protected,
.admin-org-card-title strong.admin-org-name-protected {
    /* `inline-block` lets the pill auto-size to its text and stay
       in the natural left-to-right flow of the surrounding row /
       card head — the org name sits exactly where it would have
       sat without the pill, just with a label background. */
    display: inline-block;
    vertical-align: middle;
    color: var(--accent-strong);
    font-weight: 700;
    /* Pinning font-size + line-height keeps the pill shape
       identical across sites with different ambient font sizes
       (table cells at 0.9rem vs card-head strongs at 0.94rem). */
    font-size: 0.85rem;
    line-height: 1.2;
    background: var(--accent-soft);
    border: 1px solid var(--accent);
    /* Small radius, same as every other admin badge — the old 999px
       capsule read as oversized next to plain table text. */
    border-radius: var(--radius-sm);
    /* Compact horizontal padding — enough to read as a label but
       no wider than the text it surrounds. */
    padding: 0.15rem 0.5rem;
    /* Width hugs the content. `max-width: none` and `width: auto`
       defeat the parent `.admin-org-name` rule's `max-width: 100%`
       which (combined with flex grow from the row) was stretching
       the pill across the Organizations Name cell. The pill always
       sizes to its text. */
    width: auto;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    /* Reset any italic / oblique inherited from card heads. */
    font-style: normal;
}
/* Organizations table — the protected strong ALSO carries the
   `.admin-org-name` class, which is a flex item with
   `flex: 1 1 auto` in `.admin-org-name-row`. Without these
   overrides the pill would stretch to fill the leftover space in
   the Name cell (the empty whitespace the user reported). Pin
   the flex behaviour to `0 0 auto` so the pill stays content-
   sized and the Show details button picks up the leftover. */
.admin-org-name.admin-org-name-protected {
    flex: 0 0 auto;
    min-width: 0;
    max-width: 100%;
}

/* Read-only plan label rendered in the All Users table for users
   that belong to an organization. The org's plan is authoritative
   for those users, so we show the value as a static pill (no
   dropdown chrome) and hint via tooltip that editing happens on
   the organization. */
.admin-plan-badge {
    display: inline-block;
    padding: 0.18rem 0.55rem;
    border-radius: 6px;
    font-size: 0.82rem;
    font-weight: 500;
    color: var(--text);
    background: var(--surface-soft);
    border: 1px solid var(--border-faint);
}

/* ── Org cards (All Users tab) ────────────────────────────────── */
.admin-org-card {
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    margin-bottom: 0.75rem;
    background: var(--surface);
    overflow: hidden;
}
.admin-org-card.is-open {
    box-shadow: var(--shadow-1);
}
.admin-org-card-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    width: 100%;
    padding: 0.75rem 1rem;
    background: var(--surface-soft);
    border: 0;
    cursor: pointer;
    text-align: left;
    transition: background var(--motion-fast) var(--ease);
}
.admin-org-card-head:hover { background: var(--accent-soft); }
.admin-org-card-title {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    color: var(--text-strong);
}
.admin-org-card-chev {
    color: var(--text-muted);
    font-size: 0.8rem;
    width: 0.9rem;
    display: inline-block;
    text-align: center;
}
.admin-org-card-stats {
    display: flex;
    align-items: center;
    gap: 0.35rem;
    color: var(--text);
    font-size: 0.85rem;
}
.admin-org-card-empty {
    padding: 0.9rem 1rem;
    color: var(--text-muted);
    background: var(--surface);
    border-top: 1px solid var(--border-faint);
    font-size: 0.85rem;
}
.admin-table-inner {
    border-top: 1px solid var(--border-faint);
}
.admin-table-inner thead th {
    background: var(--surface);
}

/* ── Move-user confirmation summary ──────────────────────────── */
.admin-move-summary {
    display: grid;
    grid-template-columns: 1fr auto 1fr;
    align-items: center;
    gap: 0.75rem;
    margin: 0.75rem 0;
    padding: 0.75rem;
    background: var(--surface-soft);
    border: 1px solid var(--border-faint);
    border-radius: var(--radius-md);
}
.admin-move-summary > div:not(.admin-move-arrow) {
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
    font-size: 0.9rem;
}
.admin-move-summary .muted {
    font-size: 0.75rem;
    text-transform: uppercase;
    letter-spacing: 0.05em;
}
.admin-move-arrow {
    color: var(--accent);
    font-size: 1.4rem;
    font-weight: 600;
    text-align: center;
}

/* ── Custom confirm + edit modals ────────────────────────────── */
.admin-confirm-modal,
.admin-edit-modal {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    width: min(520px, 96vw);
    max-height: 90vh;
    display: flex;
    flex-direction: column;
    box-shadow: var(--shadow-modal);
    overflow: hidden;
}
.admin-edit-modal { width: min(620px, 96vw); }
.admin-confirm-body {
    padding: 1rem 1.25rem;
    color: var(--text);
    font-size: 0.92rem;
    line-height: 1.5;
    overflow-y: auto;
}
.admin-confirm-body p { margin: 0 0 0.6rem; }
.admin-confirm-list {
    margin: 0.4rem 0;
    padding-left: 1.1rem;
    color: var(--text);
}
.admin-confirm-list li { margin-bottom: 0.2rem; }
.admin-confirm-actions {
    display: flex;
    justify-content: flex-end;
    gap: 0.5rem;
    padding: 0.85rem 1.25rem 1rem;
    border-top: 1px solid var(--border-faint);
    background: var(--surface-soft);
}
.admin-edit-body {
    padding: 1rem 1.25rem;
    overflow-y: auto;
    overflow-x: hidden;
    /* Long org names in the typed-phrase block were the only known
       cause of a horizontal scrollbar inside the modal — the body
       now hides any horizontal overflow as a defence in depth,
       since every legitimate child (form-input, mono phrase, label)
       already constrains its width. */
    max-width: 100%;
}
.admin-edit-body .form-group { margin-bottom: 1rem; }

/* "Add user to …" modal — compact form rhythm matching the refined
   invite-key forms: tighter label↔control + group spacing, and a
   slimmer action footer (the band sat inside the already-padded body,
   double-padding the bottom). Scoped to .admin-adduser-body so other
   admin-edit modals (delete / edit) are untouched. */
.admin-adduser-body .form-group { margin-bottom: 0.7rem; }
.admin-adduser-body .form-label { margin-bottom: 0.25rem; }
.admin-adduser-body .admin-confirm-actions {
    padding: 0.65rem 0 0;
    margin-top: 0.5rem;
    background: transparent;
}

/* Password input with attached visibility toggle. Used by the
   Add User modal — the toggle sits inside the input's visual
   box, right-aligned, with the input's right padding bumped so
   the typed password doesn't slide under the icon. */
.admin-password-input {
    position: relative;
    display: block;
}
.admin-password-input > .form-input {
    /* Reserve space on the right for the toggle button. */
    padding-right: 2.4rem;
    width: 100%;
}
.admin-password-toggle {
    position: absolute;
    top: 50%;
    right: 0.35rem;
    transform: translateY(-50%);
    width: 28px;
    height: 28px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: transparent;
    border: 1px solid transparent;
    border-radius: var(--radius-sm);
    color: var(--text-muted);
    cursor: pointer;
    padding: 0;
    transition: background var(--motion-fast) var(--ease),
                border-color var(--motion-fast) var(--ease),
                color var(--motion-fast) var(--ease);
}
.admin-password-toggle:hover:not(:disabled) {
    background: var(--accent-soft);
    border-color: var(--accent);
    color: var(--accent-strong);
}
.admin-password-toggle:focus-visible {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 2px var(--accent-ring);
}
.admin-password-toggle:disabled {
    cursor: not-allowed;
    color: var(--text-faint);
}

/* Wrap-anywhere helper for places inside the admin modals where
   a user-supplied value (org name, username) might be a single
   long unbreakable token. Without `overflow-wrap: anywhere` the
   word would push the modal beyond its container and produce a
   horizontal scrollbar; with it the token wraps mid-string. */
.admin-wrap-anywhere {
    overflow-wrap: anywhere;
    word-break: break-word;
    /* `inline-block` so the wrapping applies cleanly even when
       the value sits inside flowing inline text. */
    display: inline;
    max-width: 100%;
}
/* Defensive — every paragraph and list item inside the admin
   modal body wraps long names. Catches future code paths that
   render an org/user name without the explicit
   `.admin-wrap-anywhere` class. */
.admin-edit-body p,
.admin-edit-body li,
.admin-edit-body strong,
.admin-edit-body .form-label {
    overflow-wrap: anywhere;
    word-break: break-word;
}

/* Typed-phrase block used by the Delete Organization (and future
   Delete User) modal. Renders the required confirmation string in
   a wrapped monospace box so a long org / username never pushes
   the modal beyond its width. `word-break: break-all` lets a
   single very long token wrap; `white-space: pre-wrap` preserves
   the spaces inside the phrase. */
.admin-confirm-phrase {
    display: block;
    margin: 0.35rem 0 0.5rem;
    padding: 0.45rem 0.6rem;
    background: var(--surface-soft);
    border: 1px solid var(--border-faint);
    border-radius: var(--radius-sm);
    font-family: var(--font-mono);
    font-size: 0.85rem;
    color: var(--text-strong);
    white-space: pre-wrap;
    word-break: break-all;
    overflow-wrap: anywhere;
    line-height: 1.4;
    user-select: text;
}

/* ── Row-action "More" menu ──────────────────────────────────── */
.admin-more-btn {
    width: 2.1rem;
    font-weight: 700;
    letter-spacing: 0.1em;
    padding: 0.3rem 0.5rem;
}
.admin-row-menu {
    background: var(--surface);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-2);
    padding: 0.3rem;
    min-width: 180px;
    z-index: 1200;
}
.admin-row-menu-item {
    display: block;
    width: 100%;
    text-align: left;
    padding: 0.45rem 0.7rem;
    background: transparent;
    border: 0;
    border-radius: var(--radius-sm);
    color: var(--text);
    font-size: 0.88rem;
    cursor: pointer;
    transition: background var(--motion-fast) var(--ease);
}
.admin-row-menu-item:hover:not(:disabled) {
    background: var(--accent-soft);
    color: var(--accent-strong);
}
.admin-row-menu-item:disabled {
    color: var(--text-faint);
    cursor: not-allowed;
}
.admin-row-menu-item.is-danger { color: var(--danger-strong); }
.admin-row-menu-item.is-danger:hover:not(:disabled) {
    background: var(--danger-bg);
    color: var(--danger-strong);
}
.admin-row-menu-sep {
    height: 1px;
    background: var(--border-faint);
    margin: 0.25rem 0.1rem;
}

/* ── Invite-key creation form (4-col grid that wraps on narrow). */
.admin-keys-form {
    display: grid;
    grid-template-columns: 2fr 1fr 1fr 1fr;
    gap: 0.75rem 1rem;
    align-items: start;
    padding: 0.85rem 1rem;
    background: var(--surface-soft);
    border: 1px solid var(--border-faint);
    border-radius: var(--radius-md);
    margin-bottom: 1rem;
}
.admin-keys-form .form-group { margin-bottom: 0; }
.admin-keys-form-actions {
    grid-column: 1 / -1;
    display: flex;
    justify-content: flex-end;
}
@media (max-width: 800px) {
    .admin-keys-form { grid-template-columns: 1fr 1fr; }
}
@media (max-width: 520px) {
    .admin-keys-form { grid-template-columns: 1fr; }
}



/* ── Admin loading card + spinner ──────────────────────────────
   Replaces the previous "Loading organization…" paragraph that
   floated raw on the page. Same skeleton as `.card` so spacing
   matches; spinner uses a single border-mask animation so we don't
   need a separate SVG asset. */
.admin-loading-card {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.9rem;
    padding: 2rem 1.5rem;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-1);
    color: var(--text);
    margin-bottom: 1rem;
}
.admin-loading-text {
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
    align-items: flex-start;
}
.admin-loading-title {
    font-size: 0.95rem;
    font-weight: 600;
    color: var(--text-strong);
}
.admin-loading-detail {
    font-size: 0.82rem;
    color: var(--text-muted);
}
.admin-spinner {
    width: 22px; height: 22px;
    border-radius: 50%;
    border: 2.4px solid var(--accent-soft);
    border-top-color: var(--accent);
    animation: admin-spin 0.85s linear infinite;
    flex-shrink: 0;
}
@keyframes admin-spin {
    to { transform: rotate(360deg); }
}

/* ── Admin button-layout polish pass ───────────────────────────
   Tight, uniform sizing across every admin row + form action.
   Reuses .btn / .btn-small but tightens the cell-action layout so
   primary, secondary, and destructive controls sit on one
   baseline regardless of label length. */
.admin-actions-cell {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 0.45rem;
    flex-wrap: nowrap;
}
.admin-actions-cell .btn {
    /* Cell buttons live on tight rows — strip the default
       margin-block that .btn applies in headers / forms. */
    margin: 0;
}
.admin-actions-cell .molgen-dropdown {
    /* Compact dropdown trigger so it lines up with adjacent buttons.
       (No actions cell currently renders a MolGenDropdown — they use
       the RowMenu button — but keep the right padding reserved for the
       absolutely-positioned chevron so reusing this class can never
       reintroduce the value-under-chevron overlap bug.) */
    height: 30px;
    padding: 0 1.9rem 0 0.6rem;
    font-size: 0.85rem;
}
.admin-actions-col {
    width: 1%;
    white-space: nowrap;
    text-align: right;
}

/* Card-head action area: title + actions on one row, action group
   on the right. Becomes column-stacked on narrow viewports. */
.admin-card-head {
    flex-wrap: wrap;
    gap: 0.6rem;
}
.admin-card-head-actions {
    margin-left: auto;
    flex-wrap: nowrap;
}
@media (max-width: 720px) {
    .admin-card-head-actions { width: 100%; margin-left: 0; }
}
@media (max-width: 560px) {
    /* Phone widths: the head actions (search box + a create/toggle
       button) must wrap and the search input must go fluid. The desktop
       min-width now lives on `.admin-search-input` (a single class), so
       this more-specific `.admin-card-head-actions .form-input` selector
       releases it without `!important`. */
    .admin-card-head-actions { flex-wrap: wrap; }
    .admin-card-head-actions .form-input,
    .admin-card-head-actions .form-input-compact {
        min-width: 0;
        flex: 1 1 100%;
    }
}

/* Inline create form: tightened so it doesn't dominate the section. */
.admin-inline-form,
.admin-inline-form-compact {
    background: var(--surface-soft);
    border: 1px solid var(--border-faint);
    border-radius: var(--radius-md);
    padding: 0.6rem;
    margin: 0 0 0.85rem;
}
.admin-inline-form { padding: 0.75rem; }
.admin-inline-form .btn,
.admin-inline-form-compact .btn { white-space: nowrap; }

/* Modal footer button group — same gap and alignment as
   .admin-confirm-actions (used by all admin modals). */
.admin-confirm-actions .btn,
.admin-edit-modal .admin-confirm-actions .btn {
    min-width: 96px;
}

/* Long-name protection: stop overflowing org names / emails from
   breaking the row layout. Truncate with ellipsis at the cell
   level — the full value is still in the DOM for tooling. */
.admin-table td,
.admin-org-card-head .admin-org-card-title strong {
    max-width: 360px;
    overflow: hidden;
    text-overflow: ellipsis;
}
.admin-org-card-head .admin-org-card-title strong {
    white-space: nowrap;
}
.admin-table .muted.small {
    max-width: 320px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Reveal control for org-invite raw key (rendered next to prefix). */
.admin-key-reveal-row {
    display: flex;
    align-items: center;
    gap: 0.4rem;
}
.admin-key-reveal-row .mono { font-family: var(--font-mono); }
.admin-key-reveal-row .btn { padding: 0.2rem 0.55rem; font-size: 0.78rem; }

.admin-inline-loading {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.5rem 0.2rem;
    color: var(--text-muted);
    font-size: 0.88rem;
}
.admin-spinner-sm {
    width: 14px; height: 14px;
    border-width: 2px;
}


/* ── Admin commercial-readiness polish pass ─────────────────────
   Layered on top of the earlier admin styles. Covers:
   - org table column sizing + truncation
   - description expand/collapse + link-styled buttons
   - bulk-action bar
   - create-org form (textarea + footer buttons)
   - delete modal (mode pickers + typed-phrase confirm)
   - pager (Prev / Next / jump-to)
   - user table row alignment (compact dropdowns, vertical center)
   - invite key cell + reveal modal */

.admin-table { table-layout: fixed; width: 100%; }
.admin-table th,
.admin-table td {
    vertical-align: middle;
    overflow: hidden;
}
/* Horizontal-scroll wrapper for the wide, multi-column data tables.
   A `table-layout: fixed` table cannot shrink below the sum of its
   explicit column widths, so on tablet / phone widths the table would
   otherwise push the whole admin page into horizontal scroll. Wrapping
   it lets the table scroll WITHIN its card instead. The dropdown and
   row-action menus portal to <body>, so this clip never hides them. */
.admin-table-scroll {
    overflow-x: auto;
    overflow-y: visible;
    max-width: 100%;
    -webkit-overflow-scrolling: touch;
}
.admin-table-scroll > .admin-table { min-width: 640px; }
.admin-truncate {
    display: inline-block;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    vertical-align: middle;
}

/* Org rows */
.admin-table-orgs td:nth-child(1) {
    /* Name + description column needs more breathing room and
       must allow the description block to wrap inline. */
    white-space: normal;
}
.admin-org-name-row {
    display: flex;
    align-items: center;
    gap: 0.45rem;
    min-width: 0;
}
.admin-org-name {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    min-width: 0;
    flex: 1 1 auto;
    max-width: 340px;
}
.admin-org-desc {
    margin-top: 0.18rem;
    display: flex;
    align-items: baseline;
    gap: 0.4rem;
    flex-wrap: wrap;
}
.admin-org-desc .muted.small {
    max-width: 100%;
    white-space: normal;
    overflow: visible;
}

/* Bulk-action bar */
.admin-bulk-bar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem;
    padding: 0.55rem 0.75rem;
    margin-bottom: 0.75rem;
    background: var(--accent-soft);
    border: 1px solid var(--accent);
    border-radius: var(--radius-md);
    color: var(--accent-strong);
    font-size: 0.88rem;
}
.admin-bulk-bar-info { display: inline-flex; gap: 0.5rem; align-items: center; }
.admin-bulk-bar-actions { display: inline-flex; gap: 0.45rem; }
.admin-bulk-clear {
    background: transparent;
    border: 0;
    color: var(--accent-strong);
    text-decoration: underline;
    cursor: pointer;
    font-size: 0.85rem;
    padding: 0 0.25rem;
}
.admin-bulk-clear:disabled { color: var(--text-faint); cursor: not-allowed; }

/* Inline link-styled button used inside the org description for
   show-more / show-less. Keeps the visual weight of a real link
   without using a real <a> (which would lose the form-button
   semantics on enter / focus management). */
.admin-link-btn {
    background: transparent;
    border: 0;
    padding: 0;
    color: var(--accent-strong);
    cursor: pointer;
    font-size: 0.78rem;
    text-decoration: underline;
}
.admin-link-btn:hover { color: var(--accent); }

/* Create-org form — clean two-row form with right-aligned actions. */
.admin-create-org-form {
    padding: 0.85rem;
    margin-bottom: 0.9rem;
    background: var(--surface-soft);
    border: 1px solid var(--border-faint);
    border-radius: var(--radius-md);
}
.admin-create-org-form .form-group { margin-bottom: 0.75rem; }
.admin-create-org-actions {
    display: flex;
    justify-content: flex-end;
    gap: 0.5rem;
    margin-top: 0.25rem;
}
.admin-textarea {
    resize: vertical;
    min-height: 80px;
    max-height: 220px;
    font-family: inherit;
    line-height: 1.45;
}

/* `.admin-pager` rule + `.admin-pager-jump*` previously lived here;
   the canonical pager layout now lives further down (search
   "Admin pager: count left, controls truly centered"). The
   `.admin-pager-jump*` classes had no remaining call site after
   the Pager component switched to inline numeric input, so they
   were removed too. */

/* Delete-mode radio cards inside the org-delete modal. */
.admin-delete-mode {
    display: flex;
    align-items: flex-start;
    gap: 0.65rem;
    padding: 0.75rem 0.9rem;
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--surface);
    margin-bottom: 0.55rem;
    cursor: pointer;
    transition: border-color var(--motion-fast) var(--ease),
                background var(--motion-fast) var(--ease);
}
.admin-delete-mode:hover { border-color: var(--accent-ring); background: var(--surface-soft); }
.admin-delete-mode.is-selected {
    border-color: var(--accent);
    background: var(--accent-soft);
}
.admin-delete-mode.is-destructive.is-selected {
    border-color: var(--danger);
    background: var(--danger-bg);
}
.admin-delete-mode input[type="radio"] {
    margin-top: 0.18rem;
    accent-color: var(--accent);
}
.admin-delete-mode-title {
    font-size: 0.92rem;
    font-weight: 600;
    color: var(--text-strong);
    margin-bottom: 0.15rem;
}
.admin-delete-mode-detail { line-height: 1.45; }

/* Users table — compact dropdowns + vertical alignment. */
.admin-table-users td { padding-top: 0.4rem; padding-bottom: 0.4rem; }
.admin-user-name {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    max-width: 100%;
}
.admin-user-email { margin-top: 0.1rem; }
.admin-user-plan { white-space: nowrap; }
.admin-self-chip {
    background: var(--accent-soft);
    color: var(--accent-strong);
    border: 1px solid var(--accent);
    border-radius: var(--radius-sm);
    padding: 0.05rem 0.45rem;
    font-size: 0.7rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.05em;
}
.admin-user-role-cell,
.admin-user-org-cell {
    /* Each inner cell is a single Dropdown — give it a tight width
       so the row stays compact and the columns line up. The
       dropdown's own padding makes the trigger ~32 px tall. */
    width: 100%;
    max-width: 180px;
}
.admin-user-role-cell { max-width: 140px; }
.admin-user-role-cell .molgen-dropdown,
.admin-user-org-cell .molgen-dropdown {
    height: 32px;
    /* Reserve room on the RIGHT for the absolutely-positioned chevron
       (it sits at right:0.875rem and is 12px wide). The base
       `.molgen-dropdown` reserves 2.5rem; this compact override must
       keep its own reservation or the value text renders underneath
       the chevron. */
    padding: 0 1.9rem 0 0.55rem;
    font-size: 0.85rem;
}
.admin-actions-cell .admin-more-btn { margin-left: auto; }

/* Invite keys — prefix cell + reveal popover. */
.admin-key-prefix-cell {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    min-width: 0;
}
.admin-key-prefix-cell .admin-truncate {
    flex: 1 1 auto;
    min-width: 0;
}
/* `.admin-reveal-key` removed — the reveal modal now uses the shared
   `.admin-iik-key-row` + `.admin-iik-key-code` chrome (see
   `.admin-reveal-key-row` further down). */


/* ── Admin polish pass — round 2 ────────────────────────────────
   - Badge sits immediately next to org name (no fluid flex).
   - Slug, plan, status cells vertically centered.
   - Row vertical padding is consistent regardless of badge / desc.
   - Description expansion lives on a dedicated colspan row, so the
     main row height never shifts.
   - Pager: Studio-style compact pill buttons + page-number input.
   - Modal action buttons normalised to btn-small min-width 88px.
   - Header checkbox vertical-align middle. */

.admin-table-orgs td { vertical-align: middle; padding-top: 0.55rem; padding-bottom: 0.55rem; }
.admin-th-checkbox,
.admin-td-checkbox {
    text-align: center;
    vertical-align: middle;
    padding-left: 0.65rem;
    padding-right: 0.35rem;
}
.admin-th-checkbox input[type="checkbox"],
.admin-td-checkbox input[type="checkbox"] {
    vertical-align: middle;
    margin: 0;
    accent-color: var(--accent);
}

/* Org Name + badge: pinned together. The previous flex:1 1 auto
   on .admin-org-name made the badge float to the far right of the
   cell; now the name shrinks to its content (with ellipsis when
   overlong) and the badge follows immediately. */
.admin-org-name-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.45rem;
    max-width: 100%;
}
.admin-org-name {
    flex: 0 1 auto;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    max-width: 180px;
}
.admin-org-toggle {
    margin-left: 0.25rem;
    font-size: 0.74rem;
}

.admin-slug-cell {
    display: inline-block;
    max-width: 100%;
    vertical-align: middle;
    line-height: 1.4;
    /* Clip overflow — NO ellipsis glyph. A previous version used
       `text-overflow: ellipsis` which painted a `…` (three-dot)
       character at the end of every truncated slug, and the user
       perceived that glyph as stray dots in the row. The full
       slug is always available in the Show-details expansion
       row, so silently clipping in the table cell is fine. */
    white-space: nowrap;
    overflow: hidden;
    text-overflow: clip;
}
.admin-slug-td {
    /* Slug cell stays compact so the Name column gets all of the
       leftover horizontal space. */
    min-width: 0;
}

/* Org description expansion — two-field grid (Slug + Description)
   so admins can see + copy the full opaque slug from the inline
   detail view, then read the org's description below it. */
.admin-org-expansion-fields {
    display: grid;
    grid-template-columns: 1fr;
    gap: 0.65rem;
}
.admin-org-expansion-field {
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
}
.admin-org-expansion-field .mono {
    font-family: var(--font-mono);
    user-select: text;
    word-break: break-all;
}

/* Description expansion row: lives in a separate <tr>, spans the
   full table width, gets a soft surface so it visually attaches
   to the row above. The MAIN row never grows. */
.admin-org-expansion-row td {
    background: var(--surface-soft);
    border-top: 0 !important;
    padding: 0 !important;
}
.admin-org-expansion {
    padding: 0.55rem 0.85rem 0.7rem 0.85rem;
    border-top: 1px dashed var(--border-faint);
}
.admin-org-expansion-label {
    text-transform: uppercase;
    letter-spacing: 0.05em;
    margin-bottom: 0.2rem;
}
.admin-org-expansion-text {
    line-height: 1.5;
    color: var(--text);
}
/* Removing the main row's border-bottom when an expansion row
   opens used to shift the Status cell by 1px (the badge sits
   centred, so losing the border changed its baseline). Instead,
   keep the border but make it transparent — same box model, no
   visual seam between the rows. The expansion row supplies its
   own dashed border-top via `.admin-org-expansion`. */
.admin-table-orgs tr.has-expansion td {
    border-bottom-color: transparent;
}

/* Fixed row height so toggling the description expansion never
   shifts any cell. Computed from the cell padding (0.55rem × 2)
   plus the natural line height of badge / link content; the body
   never grows or shrinks. */
.admin-table-orgs tbody tr { height: 52px; }
.admin-table-orgs tbody tr.admin-org-expansion-row { height: auto; }

/* RowMenu (⋯) button — square, vertically centered, no
   letter-spacing artefact. */
.admin-more-btn {
    width: 32px !important;
    height: 32px !important;
    padding: 0 !important;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    letter-spacing: 0 !important;
    line-height: 1 !important;
    /* Icon-button — no text weight; SVG owns the visual. */
    color: var(--text);
}
.admin-more-btn svg { display: block; }
.admin-actions-cell { text-align: right; }

/* Modal button minimums — destructive items get the same min-width
   as primary so the row is balanced. */
.admin-confirm-actions .btn { min-width: 88px; }
.admin-confirm-actions .btn-small { min-width: 88px; }

/* Studio-style pager controls. The `.admin-pager` layout rules
   themselves live in the canonical block further down; only the
   per-control geometry (btn / page / label / input) lives here. */
.admin-pager-btn {
    width: 28px;
    height: 28px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: var(--surface);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    color: var(--text);
    cursor: pointer;
    padding: 0;
    transition: background var(--motion-fast) var(--ease),
                border-color var(--motion-fast) var(--ease),
                color var(--motion-fast) var(--ease);
}
.admin-pager-btn:hover:not(:disabled) {
    background: var(--accent-soft);
    border-color: var(--accent);
    color: var(--accent-strong);
}
.admin-pager-btn:disabled {
    color: var(--text-faint);
    cursor: not-allowed;
    background: var(--surface);
}
.admin-pager-page {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    padding: 0 0.25rem;
}
.admin-pager-label {
    font-size: 0.82rem;
    color: var(--text-muted);
}
.admin-pager-input {
    width: 38px;
    height: 26px;
    padding: 0 0.25rem;
    text-align: center;
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    background: var(--surface);
    color: var(--text);
    font-size: 0.85rem;
    font-variant-numeric: tabular-nums;
}
.admin-pager-input:focus {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 2px var(--accent-ring);
}

/* Header checkbox and column heading row alignment — make sure the
   header tr matches the data-row vertical rhythm. */
.admin-table thead th { vertical-align: middle; }


/* All Users tab — bigger Role / Org dropdowns so the selected
   label never wraps. The Plan editor mirrors Role's width so the
   row reads consistently across columns. */
.admin-user-role-cell {
    max-width: 140px;
    width: 100%;
}
.admin-user-org-cell {
    max-width: 180px;
    width: 100%;
}
.admin-user-plan-cell {
    max-width: 128px;
    width: 100%;
}
.admin-user-role-cell .molgen-dropdown,
.admin-user-org-cell .molgen-dropdown,
.admin-user-plan-cell .molgen-dropdown {
    width: 100%;
    height: 32px;
    /* Right padding reserves space for the absolutely-positioned
       chevron so the (ellipsised) value text never renders under it.
       Without this the base `.molgen-dropdown`'s padding-right:2.5rem
       is clobbered and the role/org/plan label overlaps the chevron
       at every viewport width. */
    padding: 0 1.9rem 0 0.6rem;
    font-size: 0.85rem;
    /* Prevent the trigger from breaking onto two lines if its label
       is unusually long; the menu items themselves still display
       in full when opened. */
    white-space: nowrap;
    overflow: hidden;
}
.admin-user-role-cell .molgen-dropdown-value,
.admin-user-org-cell .molgen-dropdown-value,
.admin-user-plan-cell .molgen-dropdown-value {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    min-width: 0;
}

/* Invite Keys card layout — body padding + create-button toolbar */
.admin-org-card-body {
    padding: 0.65rem 0.9rem 0.85rem;
    background: var(--surface);
    border-top: 1px solid var(--border-faint);
}
.admin-keys-card-toolbar {
    display: flex;
    justify-content: flex-end;
    margin-bottom: 0.6rem;
}
/* Reveal button in the prefix cell — keep it compact and aligned */
.admin-key-prefix-cell .btn-small {
    height: 26px;
    padding: 0 0.55rem;
    font-size: 0.78rem;
}


/* The previous "stack the info under the controls" rule lived
   here. It was superseded by the absolute-positioned layout
   below (count anchored left, controls truly centered) and has
   been removed. */

/* ── Org description expansion: align with Name column ─────────
   The expansion row spans the full table width (colSpan=7), so by
   default its content starts at the row's left edge — flush with
   the checkbox column, not the Name text. Pad it forward by the
   checkbox column width (40px) + the td horizontal padding plus a
   small reading indent so the description visually attaches under
   the Name text. The dashed top border anchors it to the row above. */
.admin-org-expansion {
    padding: 0.55rem 0.85rem 0.7rem 3.6rem;
}
@media (max-width: 720px) {
    /* On narrow viewports the table itself collapses; keep the
       expansion readable with a smaller indent. */
    .admin-org-expansion { padding-left: 1rem; }
}


/* ── All Users — explicit two-line user cell ───────────────────
   The user cell holds `username` on line 1 and `email` on line 2.
   Both children inherit `display: inline-block` from `.admin-truncate`
   so they previously squeezed onto one line. The rules below force
   each into its own block, while keeping `.admin-truncate`'s
   ellipsis + tooltip behaviour intact via an inner span. */
.admin-table-users td:first-child {
    /* Reserve enough vertical room so a two-line cell never makes
       the row taller than a single-line cell — the badge row and
       dropdowns are ~32 px each, leaving plenty of headroom. */
    padding-top: 0.45rem;
    padding-bottom: 0.45rem;
}
.admin-user-name {
    display: flex !important;
    align-items: center;
    gap: 0.4rem;
    min-width: 0;
    max-width: 100%;
    line-height: 1.25;
}
.admin-user-name > .admin-truncate {
    /* Inside the name flex row the truncated <strong> needs to be
       allowed to shrink so the email tooltip / chip stays visible. */
    min-width: 0;
    flex: 0 1 auto;
}
.admin-user-email {
    display: block !important;
    margin-top: 0.1rem;
    /* Re-assert truncation in case `.muted.small` overrode it. */
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    line-height: 1.2;
}


/* ── Invite Keys: new-key banner spacing + role badge ──────────
   The post-create banner used to read as one tight stack:
       test · grants org_admin
       oik_…
   Now it has explicit vertical rhythm: title line, meta row with
   a RoleBadge for the granted role, and a key block with
   comfortable padding. */
.admin-keys-new-banner {
    align-items: flex-start;
    gap: 0.65rem;
}
.admin-keys-new-banner-head {
    margin-bottom: 0.7rem;
    line-height: 1.4;
}
.admin-keys-new-banner-meta {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 0.5rem;
    margin-bottom: 0.85rem;
    font-size: 0.82rem;
    color: var(--text-muted);
}
.admin-keys-new-banner-meta > span:first-child {
    text-transform: uppercase;
    letter-spacing: 0.05em;
    font-weight: 600;
    color: var(--text-muted);
}
.admin-keys-new-banner-chip {
    display: inline-flex;
    align-items: center;
    padding: 0.15rem 0.55rem;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    color: var(--text);
    font-size: 0.78rem;
}
.admin-keys-new-banner-key {
    display: flex;
    gap: 0.45rem;
    /* Center children vertically so the compact `<code>` field and
       the icon-only copy button sit on the same baseline; the old
       `align-items: stretch` made the inline `<input>` and the
       button equal height, which is exactly what we no longer want. */
    align-items: center;
    min-width: 0;
}
.admin-keys-new-banner-key .admin-iik-key-code {
    /* Slight bump in vertical padding here so the key row reads
       comfortably in the alert banner without making the banner
       tall. Still well under the original `.form-input` size. */
    padding: 0.35rem 0.6rem;
}

/* Reveal modal — compact key row reuses .admin-iik-key-row /
   .admin-iik-key-code so this stays in sync with the rest of the
   key-display surface. */
.admin-reveal-key-row {
    margin: 0.5rem 0;
}

/* ── Invite key reveal modal — same spacing rhythm as the banner */
.admin-reveal-meta {
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
    margin-bottom: 0.85rem;
}
.admin-reveal-name {
    font-size: 1rem;
    color: var(--text-strong);
}
.admin-reveal-meta-row {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
}
.admin-reveal-note {
    margin-top: 0.65rem !important;
    margin-bottom: 0 !important;
    line-height: 1.5;
}

/* ── Invite key prefix cell: compact inline group ───────────────
   Prefix shrinks to its natural width (`flex: 0 1 auto`) so the
   eye-icon button sits immediately to the right of the visible
   key prefix instead of being pushed to the far edge of the Key
   column. Any leftover space in the column stays on the right. */
.admin-key-prefix-cell {
    display: flex;
    align-items: center;
    gap: 0.35rem;
    max-width: 100%;
    min-width: 0;
}
.admin-key-prefix-cell > .admin-truncate {
    flex: 0 1 auto;
    min-width: 0;
    max-width: 100%;
}
.admin-key-prefix-cell .btn-small {
    height: 26px;
    padding: 0 0.6rem;
    font-size: 0.78rem;
    flex-shrink: 0;
}
/* `.admin-reveal-btn` was the old text-style reveal button. The
   reveal flow now uses `.admin-icon-btn` (compact eye icon),
   which handles its own `:disabled` state. The legacy rule has
   been removed. */


/* ── Admin pager: count left, controls truly centered ────────────
   Earlier passes tried flex `space-between`, then `flex-direction:
   column` (which broke single-row), then a 3-zone grid. All three
   left at least one panel rendering the count on a row of its own
   under certain viewport widths because cascading earlier rules
   reintroduced `flex-wrap: wrap` or `order` properties.

   Final layout — `position: relative` parent. The count is
   absolutely positioned against the left edge of the pager strip
   and vertically centred; the controls are flex-centred inside
   the same strip. Because the count is taken out of normal flow,
   it can never push or wrap the controls onto a different row,
   and the controls' horizontal centre is always the pager strip's
   centre regardless of count text length. */
.admin-pager {
    position: relative;
    display: flex;
    flex-direction: row;
    flex-wrap: nowrap;
    align-items: center;
    justify-content: center;
    gap: 0;
    min-height: 44px;
    padding: 0.55rem 0;
    margin-top: 1rem;
    border-top: 1px solid var(--border-faint);
}
.admin-pager-info {
    position: absolute;
    left: 0;
    top: 50%;
    transform: translateY(-50%);
    margin: 0;
    order: initial;
    font-size: 0.82rem;
    color: var(--text-muted);
}
.admin-pager-controls {
    position: relative;
    margin: 0;
    order: initial;
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
}

/* ── Orgs table — column rebalance ─────────────────────────────
   Name col is fixed at a moderate width so the Slug column can sit
   immediately to its right (instead of being pushed far across the
   table when Name absorbed all leftover space). Slug now has the
   flexible `width: auto` slot and grows to fill the row; the full
   slug usually displays inline, and on very narrow tables it
   clips silently (no ellipsis glyph). */
.admin-table-orgs colgroup col:nth-child(2) { width: 240px; }   /* Name */
.admin-table-orgs colgroup col:nth-child(3) { width: auto; }    /* Slug — flex */
.admin-table-orgs colgroup col:nth-child(4) { width: 120px; }   /* Plan */
.admin-table-orgs colgroup col:nth-child(5) { width: 96px;  }   /* Members */
.admin-table-orgs colgroup col:nth-child(6) { width: 110px; }   /* Status */
.admin-table-orgs colgroup col:nth-child(7) { width: 52px;  }   /* Actions */
/* Org name in the table row stays on a SINGLE LINE and ellipses
   when longer than the available space. The Show details button
   sits on the same row and must NEVER wrap below — it's the only
   affordance for opening the expanded fields where the full name
   is shown. The strong is a flex item that shrinks (`flex: 1 1
   auto` + `min-width: 0`) so its ellipsis kicks in; the button is
   `flex-shrink: 0` so it always holds its width. The row's
   `display: flex; flex-wrap: nowrap` keeps both children on the
   same line. */
.admin-org-name-row {
    display: flex;
    flex-direction: row;
    flex-wrap: nowrap;
    align-items: center;
    gap: 0.45rem;
    min-width: 0;
    width: 100%;
}
.admin-org-name {
    flex: 1 1 auto;
    min-width: 0;
    max-width: 100%;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    line-height: 1.3;
    vertical-align: middle;
}
.admin-org-toggle {
    flex: 0 0 auto;
    white-space: nowrap;
    /* Pin Show details to the right edge of the Name cell. For
       normal org names the cell's `.admin-org-name` (flex: 1 1
       auto) already eats all the leftover space and pushes the
       toggle right; for protected orgs the pill is fixed-width
       (flex: 0 0 auto) so without `margin-left: auto` the toggle
       would sit immediately next to the pill on the left. CSS
       flex spec: auto margins absorb any leftover space AFTER
       flex grow runs, so this is a no-op in the normal case and
       a right-align in the protected case. */
    margin-left: auto;
}
/* Defensive — every cell in the orgs table that is NOT the Name
   cell clips without an ellipsis glyph (to avoid the historical
   "three small dots" artefact next to the checkbox). The Name
   cell deliberately keeps `text-overflow: ellipsis` via the
   `.admin-org-name` rule above, so we allow it through here. */
.admin-table-orgs td:not(:nth-child(2)),
.admin-table-orgs td:not(:nth-child(2)) * {
    text-overflow: clip;
}
/* Badges keep their rounded surface visible even though the parent
   td has `overflow:hidden`. */
.admin-table-orgs td .active-badge,
.admin-table-orgs td .role-badge {
    overflow: visible;
}

/* Slug ↔ Plan gutter. Adding padding on the right of Slug and
   left of Plan reads as a visible gap without changing column
   widths. */
.admin-table-orgs th:nth-child(3),
.admin-table-orgs td:nth-child(3) { padding-right: 1.1rem; }
.admin-table-orgs th:nth-child(4),
.admin-table-orgs td:nth-child(4) { padding-left: 1.1rem; }

/* ── Checkbox sizing + alignment ──────────────────────────────
   Both `.admin-table thead th` (padding 0.5rem 0.75rem) and
   `.admin-table tbody td` (padding 0.75rem) win out over a plain
   `.admin-th-checkbox` / `.admin-td-checkbox` selector because of
   element-level specificity (`.admin-table thead th` = 0,1,2).
   To force the checkbox column to render symmetrically in BOTH
   the header AND the body, we use a scope-locked selector that
   matches with specificity 0,2,1 — beating the cascading defaults
   without needing `!important`. The 40px-wide column (set in the
   colgroup) plus zero horizontal padding plus `text-align:center`
   guarantees the checkbox lands at x = 20px from the cell's left
   edge in BOTH rows, so the header tick lines up perfectly with
   every body tick below it. */
.admin-table-orgs th.admin-th-checkbox,
.admin-table-orgs td.admin-td-checkbox {
    width: 40px;
    padding: 0;
    text-align: center;
    vertical-align: middle;
    line-height: 1;
}
.admin-table-orgs th.admin-th-checkbox input[type="checkbox"],
.admin-table-orgs td.admin-td-checkbox input[type="checkbox"] {
    display: inline-block;
    width: 16px;
    height: 16px;
    margin: 0;
    padding: 0;
    vertical-align: middle;
    accent-color: var(--accent);
    cursor: pointer;
}
.admin-th-checkbox input[type="checkbox"],
.admin-td-checkbox input[type="checkbox"] {
    width: 16px;
    height: 16px;
    cursor: pointer;
    accent-color: var(--accent);
    margin: 0;
    /* Lift the checkbox a hair so its centre lines up with adjacent
       text baselines (browsers vertically centre checkboxes inside
       their own layout box, which sits slightly below middle when
       the surrounding row has tall content). */
    vertical-align: middle;
}

/* ── Users / Invite Keys card name — prominent but compact ──── */
.admin-org-card-title strong {
    font-size: 0.94rem;
    color: var(--text-strong);
    line-height: 1.25;
}
.admin-org-card-stats { font-size: 0.85rem; }

/* Disable text + chevron selection while clicking the card head
   toggle. Double-clicking the title to expand was selecting the
   org name and the ▾ glyph; user-select:none keeps the gesture
   purely button-like. The interactive children still capture
   keyboard focus + click events. */
.admin-org-card-head,
.admin-org-card-toggle,
.admin-org-card-title,
.admin-org-card-chev,
.admin-org-card-stats {
    user-select: none;
    -webkit-user-select: none;
    -ms-user-select: none;
}

/* ── Card head with new actions slot ──────────────────────────
   The old card head was a single <button>. Now it's a flex div
   with a clickable toggle area on the left and an optional
   actions group on the right. */
.admin-org-card-head {
    display: flex;
    align-items: center;
    justify-content: flex-start;
    gap: 0.75rem;
    padding: 0.55rem 0.85rem;
    background: var(--surface-soft);
    border-radius: var(--radius-md) var(--radius-md) 0 0;
    /* The ENTIRE head is the click target so clicks in the
       padding never miss the toggle. The Title cluster takes
       its natural width; the Stats cluster gets the remaining
       space and pushes the actions slot to the far right. */
    min-height: 48px;
    cursor: pointer;
    outline: none;
}
.admin-org-card-head:focus-visible {
    box-shadow: inset 0 0 0 2px var(--accent-ring);
}
.admin-org-card-title {
    flex: 0 0 auto;
    min-width: 0;
}
.admin-org-card-stats {
    flex: 1 1 auto;
    /* Push the actions slot all the way to the right. */
    margin-right: auto;
    min-width: 0;
}
.admin-org-card-actions {
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    /* Reserve the action row's height even before the button mounts so the
       head never grows when a card is expanded (the per-card primary action
       only renders while open). */
    min-height: 30px;
}
.admin-org-card-actions .btn { white-space: nowrap; }
/* Pin the per-card action button to the head's content height so its
   appearance on expand can't nudge the 48px head taller. */
.admin-org-card-actions .btn-small {
    height: 30px;
    padding-top: 0;
    padding-bottom: 0;
    line-height: 1;
}
@media (max-width: 560px) {
    /* Phone widths: the card head is a non-wrapping flex row, so a long
       org-name title would push the "Add user" / "New invite key" action
       button off-screen (it sits in an `overflow:hidden` head, so it
       can't even be scrolled to). Let the head wrap — the title takes
       the first line (ellipsised) and the stats + action button sit on a
       second line with the button still reachable on the right. */
    .admin-org-card-head { flex-wrap: wrap; }
    .admin-org-card-title { flex: 1 1 100%; min-width: 0; }
    .admin-org-card-stats { flex: 1 1 auto; margin-right: 0; }
    .admin-org-card-actions { margin-left: auto; }
}

/* ── Invite Keys table — narrower Name ─────────────────────── */
.admin-table-keys colgroup col:nth-child(1) { width: 160px; }  /* Name */
.admin-table-keys colgroup col:nth-child(2) { width: auto;  }  /* Key — flex */

/* Reveal button — small pill that sits right next to the prefix,
   no margin drift, no opacity flicker. Wrapped in btn-secondary
   styles so the hover/focus states match the rest of the page. */
.admin-reveal-btn {
    height: 26px;
    padding: 0 0.6rem;
    font-size: 0.78rem;
    font-weight: 500;
    flex-shrink: 0;
    letter-spacing: 0 !important;
}



/* ── Invite key reveal icon button ──────────────────────────────
   Compact eye / eye-off button replacing the previous text
   "Reveal" / "Unavailable" labels. Saves horizontal space in the
   Key column and removes the chance of a disabled text button
   being misread as trailing dots near the row's other cells. */
.admin-reveal-icon-btn {
    width: 28px;
    height: 26px;
    padding: 0;
    background: var(--surface);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    color: var(--text);
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    transition: background var(--motion-fast) var(--ease),
                border-color var(--motion-fast) var(--ease),
                color var(--motion-fast) var(--ease);
}
.admin-reveal-icon-btn:hover:not(:disabled) {
    background: var(--accent-soft);
    border-color: var(--accent);
    color: var(--accent-strong);
}
.admin-reveal-icon-btn:focus-visible {
    outline: none;
    box-shadow: 0 0 0 2px var(--accent-ring);
}
.admin-reveal-icon-btn:disabled {
    opacity: 0.5;
    cursor: not-allowed;
    color: var(--text-muted);
}
.admin-reveal-icon-btn .admin-spinner-sm {
    border-width: 2px;
}

/* ── Revoked status dots — strip the truncation indicator ──────
   The Uses column for unlimited keys reads "0 · Unlimited", and
   when the cell is narrow the `.muted.small · Unlimited` span
   used to overflow and pick up an inherited ellipsis from the
   surrounding td. After a row is revoked the row's data
   reconciles and the Uses cell is wide enough not to overflow,
   BUT older CSS rules from earlier polish passes leave
   text-overflow:ellipsis on every td via `.admin-truncate` if it
   ever inherits. Defensively clip every keys-row Status td so
   the Revoked badge never picks up a `…` glyph. */
.admin-table-keys td {
    overflow: hidden;
    text-overflow: clip;
}
.admin-table-keys td .active-badge,
.admin-table-keys td .role-badge {
    /* Badges have their own background, padding, and rounded
       border. Make sure no overflow rule clips them. */
    overflow: visible;
}

/* ════════════════════════════════════════════════════════════════
   Focus cleanup (platform-wide — SuperGen + Alma Studio)
   ────────────────────────────────────────────────────────────────
   Text inputs / textareas / selects show a minimal accent BORDER on
   focus, not a glow ring/halo. Component rules already set the accent
   border-colour; this single authoritative block strips the decorative
   box-shadow ring everywhere (so we don't chase per-component overrides)
   and guarantees the border cue stays for keyboard accessibility.

   Scoped to text-like controls only: checkboxes/radios/range keep their
   own treatment, and switches' small keyboard ring, selected-row insets,
   drop indicators, and card-selection box-shadows are NOT inputs, so
   they're untouched. Ketcher iframes / molecule viewers aren't form
   controls either, so they're unaffected. !important is intentional —
   it's the one place focus rings are governed, not a one-off patch.
   ════════════════════════════════════════════════════════════════ */
input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):focus,
input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):focus-visible,
textarea:focus, textarea:focus-visible,
select:focus, select:focus-visible {
    box-shadow: none !important;
    border-color: var(--accent);
}
