/* ===========================================================================
   CrysViz base theme  ("default")
   ---------------------------------------------------------------------------
   This is the base layer every other theme builds on. The theme loader
   (ui/ThemeManager.js, driven by themes/themes.json) always loads this file
   first, then layers the selected theme's override file on top.

   It defines the full design-token vocabulary as CSS custom properties:
   surfaces/text, layout geometry, and the 3D scene colors. To re-skin the app,
   a theme override only needs to redefine the tokens (and/or rules) it changes.

   The default look = the app's historical appearance: dark UI panels over a
   light 3D scene. Override files (dark.css, fluorite/light.css, …) change the scene
   and/or surfaces; minimal.css changes the layout.

   NOTE: the accent tokens (--bg-color / --highlight-color / --accent-color /
   --hover-color / --border-color) are intentionally NOT defined in :root —
   they live in the .theme-standard / .theme-symmetry blocks below, one set
   per active compute backend. ui/BackendPanel/BackendTheme.js toggles which
   class is on <body>, so the accent follows the active backend. A theme
   override MAY redefine these classes to recolor the accent, but by default
   leaves them to backend mode.
   =========================================================================== */

:root {
  /* ---- Surfaces & text -------------------------------------------------- */
  --surface-bg: #002121;             /* page background behind the canvas/panel */
  --panel-bg: rgba(26, 26, 26, 1);   /* #ui side panel */
  --panel-fg: #ffffff;               /* primary text on panels */
  --muted-fg: #f9f9f9;               /* secondary text */
  --panel-border: rgba(255, 255, 255, 0.1);
  --panel-blur: 20px;                /* #ui backdrop blur */
  --group-bg: rgba(42, 42, 42, 0.8); /* .control-group boxes (the grey panels) */
  --menu-bg: rgba(26, 26, 26, 0.95); /* dropdown menus / near-opaque floaters */
  --popup-bg: rgba(26, 26, 26, 0.9); /* floating popups / pickers */
  --popup-border: rgba(255, 255, 255, 0.3);
  --input-bg: rgba(26, 26, 26, 0.9);
  --danger: rgba(240, 132, 18, 0.9);
  --warn: #FFBF00;
  --ok: #2e9c6e;
  --info: #00bcd4;

  /* ---- State colours, light-on-dark variants ----------------------------
     --warn/--ok/--info above are chosen to read on light fills. Used as TEXT
     on a dark panel they are too dark: the earlier consolidation folded the
     link/heading colours (#81c784, #7ee2a8, #4fc3f7, #6fb6ff, #ffcf4f) into
     them and measurably dropped contrast on the About panel, the comparison
     headings and the space-group links. These are the same three states at a
     luminance that survives a dark background — reach for them whenever the
     state colour is the foreground rather than the fill. */
  --warn-bright: #ffcf4f;
  --ok-bright: #7ee2a8;
  --info-bright: #4fc3f7;
  /* The anneal badge tints its own fill and border from the same hue as its
     text, so --ok-bright needs a triple too. Chosen over the other light
     green (#81c784) precisely because it collapses those three call sites
     onto one value instead of stranding them as literals. */
  --ok-bright-rgb: 126 226 168;

  /* ---- State colours, near-white tints (Wave 2.6) -------------------------
     --warn/-ok/-info-bright above are the saturated light-on-dark tier. A
     third, paler tier exists too: heading/link text on the About panel, the
     comparison table and a couple of badges use near-white colours with a
     faint state hue rather than the fully-saturated bright — visually much
     closer to --fg-1 (white) than to -bright, but with just enough tint to
     read as "this text belongs to that state". Two clusters found, one per
     hue actually in use (audit: docs/styles/TOKENS.md "tinted family"):
       blue  — #e7f5ff×3, #f5fbff×2, #cfe6ff×2, #cfeffb×2, #d7eef6×1 (10 sites)
       green — #f5fffa×1, #e8f5e9×1 (2 sites, thin but real — the About panel
               body text and its heading use the two different literals)
     Kept apart from -bright by the same rule that split -bright from the
     base state colour: a foreground tint and a saturated foreground are not
     interchangeable just because they're the same hue. */
  --info-tint: #e7f5ff;
  --ok-tint: #f5fffa;

  /* ---- State colours, bare channel triples (Wave 2.5) --------------------
     --danger/--warn/--ok/--info above stay exactly as they were — other
     stylesheets already reference them at their one fixed alpha. But real
     call sites need the same hue at several alphas (badge fill 0.14, badge
     border 0.18, solid icon 1 — see styles.css:838-840, backendPanel.css
     :120,131,144,151). Rather than mint a token per alpha, expose the raw
     channels so a call site composes its own: rgb(var(--danger-rgb) / .18).
     One source of truth for the hue; alpha stays local to the rule. */
  --danger-rgb: 240 132 18;   /* same hue as --danger */
  --warn-rgb: 255 191 0;      /* same hue as --warn (#FFBF00) */
  --ok-rgb: 46 156 110;       /* same hue as --ok (#2e9c6e) */
  --info-rgb: 0 188 212;      /* same hue as --info (#00bcd4) */

  /* ---- Additional alpha-bearing families (css_guard follow-up) -----------
     Six hue clusters the css_guard audit found with no home, each named only
     where the call sites cleared the bar (see docs/styles/TOKENS.md for the
     full count-by-count reasoning; several near-identical literals were
     deliberately left as one-off literals rather than folded in here). */
  --blue-accent-rgb: 91 168 255;      /* border/ring tier: si-wyckoff-badge border, .wyckoff-locked ring, .pi-forced-notice border, .sym-link underline (4 sites, 1 a near-dupe fold — see TOKENS.md) */
  --blue-accent-fill-rgb: 35 139 230; /* fill tier of the same family: si-wyckoff-badge's lighter gradient stop, .pi-forced-notice background (2 sites) — kept apart from the border tier per "cluster by role" */
  --shortcuts-danger-rgb: 220 53 53;  /* shortcuts-help destructive-action red (clear/reset buttons, key-conflict flag) — 10 sites in styles.css, a different hue from the orange --danger */
  --vacancy-amber-rgb: 255 210 120;   /* disordered-site species/vacancy text tint, 2 sites (structureInfoPanel.css) — thin but real, same call as --ok-tint's 2-site precedent above */
  --active-green-rgb: 125 206 160;    /* "active/applied" state accent (atom-editor button, coord-axis slider, new-row separator), 5 sites — checked against --ok-bright-rgb (126 226 168) and kept separate: the existing call sites already flagged it as "not an exact match", and the roles here are border/ring/accent-color, not the anneal badge's fill+text pairing */
  --hydrogen-bond-rgb: 51 214 214;    /* hydrogen-bond geometry and range indicator */

  --radius: 8px;
  --radius-sm: 6px;
  --radius-md: 10px;
  /* ---- Radius rungs (Wave 2.5) --------------------------------------------
     3/4/5/9/12/24px are all in real use (audit: 3px×15, 4px×21, 5px×3,
     9px×2, 12px×17, 24px×3) but sit at no clean semantic "step" relative to
     --radius-sm/--radius/--radius-md — unlike those three, this isn't a
     small/base/medium story, it's a dense, non-semantic packing of specific
     pixel values used by unrelated components. Named by literal pixel value
     instead of another tier of xs/sm/lg adjectives, which would invent
     distinctions the data doesn't support. */
  --radius-3: 3px;
  --radius-4: 4px;
  --radius-5: 5px;
  --radius-9: 9px;
  --radius-12: 12px;
  --radius-24: 24px;

  /* ---- Foregrounds & lines (greys the 545 colour literals in styles/*.css
     actually cluster into — see styles/TOKENS.md for the audit) ---------- */
  --fg-1: #ffffff;                   /* primary text; same value as --panel-fg */
  /* ---- Foreground mid-rungs (Wave 2.6) ------------------------------------
     Wave 1 sampled --fg-1/-2/-3 from styles.css's own idiom and happened to
     land on the two ends of the ramp (1.0, then a 0.6/0.4 pair) — every
     smaller panel's text alpha in between was still a literal. Re-run of the
     css_guard audit found two dense clusters sitting inside the 1.0–0.6 gap,
     plus one inside the 0.6–0.4 gap; each is tight enough (≤0.05 spread from
     its rung) to be one real value, not three:
       --fg-85 (0.82–0.96, 25 sites: .96×1 .92×2 .9×5 .88×4 .86×1 .85×5 .82×7)
       --fg-75 (0.7–0.8,   32 sites: .8×13 .78×2 .75×4 .72×2 .7×11)
       --fg-50 (0.5 only,  13 sites — .62×2 is 0.02 from --fg-2 and folds
                there instead of getting its own rung)
     Named by literal alpha×100, not another tier of ordinal suffixes,
     because there isn't a clean "how many tiers" story the way --fg-1/-2/-3
     told one — three unrelated panels each picked their own near-white and
     these are just where the picks actually landed. The number itself keeps
     brighter-vs-dimmer obvious next to --fg-1(100)/--fg-2(60)/--fg-3(40)
     without renaming anything that already ships. */
  --fg-85: rgba(255, 255, 255, 0.85);
  --fg-75: rgba(255, 255, 255, 0.75);
  --fg-2: rgba(255, 255, 255, 0.6);  /* secondary/de-emphasised text */
  --fg-50: rgba(255, 255, 255, 0.5); /* between --fg-2 and --fg-3 — see above */
  --fg-3: rgba(255, 255, 255, 0.4);  /* dim/disabled text */
  --line-1: rgba(255, 255, 255, 0.1); /* hairline border/divider; same value as --panel-border */
  --line-2: rgba(255, 255, 255, 0.2); /* stronger border/divider */
  --overlay: rgba(0, 0, 0, 0.5);     /* modal/backdrop scrim */
  --overlay-light: rgba(255, 255, 255, 0.95); /* light-on-dark inverse of --overlay
     (styles_file_browswer.css .error-panel background) — kept as its own
     token, not folded into --overlay, so a light theme can move the two
     independently the same way --overlay itself must stay apart from the
     shadow tokens below. */
  /* ---- Scrim mid-rungs (Wave 2.6) -----------------------------------------
     --overlay (0.5) was Wave 1's single representative of a family that
     actually spans 0.15-0.9 (TOKENS.md flagged this and deferred it). The
     re-audit found real mass at both ends beyond what "one scrim" covers:
       --overlay-20 (0.2×3)             — light backdrop / subtle dim
       --overlay-80 (0.8×4, 0.85×1, 0.9×2 = 7) — near-opaque modal backdrop
     0.55×6 is 0.05 from --overlay and folds there rather than getting a
     rung of its own; 0.6/0.65/0.7 (1 site each) are too sparse to cluster —
     still a genuine spread with no dominant tier between 0.2 and 0.8, same
     conclusion TOKENS.md already reached for this family. */
  --overlay-20: rgba(0, 0, 0, 0.2);
  --overlay-80: rgba(0, 0, 0, 0.8);

  /* ---- Opaque grey ramps (Wave 2.5) ---------------------------------------
     styles.css's white-alpha idiom (--fg-*/--line-*/--overlay above) covers
     most of the app, but a handful of smaller panels (TrajectoryPanel,
     ForcePanel, the file browser, the native <select> chrome) paint flat,
     OPAQUE greys instead — they can't be expressed as an alpha over
     --panel-bg because they're meant to read as solid widget chrome, not a
     translucent layer. Until now these panels had no token to reach for,
     so they were unthemeable. Clustered from actual call sites, not by
     lightness alone — see TOKENS.md for the full literal-to-rung mapping
     and the near-duplicates each rung absorbs. */
  --chrome-1: rgba(25, 25, 25, 0.8);  /* darkest widget bg (near-dupes: #161618, rgba(20,20,20,.85/.9)) */
  --chrome-1-9: rgba(25, 25, 25, 0.9); /* css_guard follow-up: near-opaque near-black backgrounds at a
     higher alpha than --chrome-1's 0.8 — .backend-error-panel, .backend-potential-toggle,
     .atomistic-grid input (rgba 25/25/25, 17/17/17, 22/22/22 at .9/.9/.88 — folded together the
     same way --chrome-2-5 sits between two rungs). Two other near-dupes at this hue
     (structureInfoPanel.css's dead .control-panel rule, styles_file_browswer.css's
     rgba(13,13,13,.95)) were already checked against --chrome-1 and explicitly found not to
     match — kept as their own literals, not folded in here. */
  --chrome-2: #2c2c2e;                /* widget bg (near-dupe: #303030) */
  --chrome-2-5: #333;                 /* Wave 2.6: 15 sites (13 backgrounds, 2 borders) — the single
     biggest opaque-grey miss in the whole audit, and it sits exactly 7
     units from --chrome-2 (44) and 7 from --chrome-3 (58): no nearer
     neighbour to snap to, same shape as the --sp-3-5 gap. #111×2 and
     #222×2 sit further down this same run but are too thin (2 sites each,
     no shared selector) to earn their own rung — fold toward --chrome-1
     at adoption. */
  --chrome-3: #3a3a3c;                /* widget bg/border (near-dupe: #3a3a3a) */
  --chrome-4: #4a4a4c;                /* hover border / lighter widget bg (near-dupes: #454547, #444) */
  --chrome-5: #555;                   /* mid-grey border/bg (near-dupe: #5a5a5a) */
  --chrome-6: #777;                   /* lightest chrome border */

  /* Bluish-tinted variant of the chrome family, used only by the Planes
     panel's sticky/selected table column (controlPanel.css). Genuinely a
     different hue (navy, not neutral grey) — folding it into --chrome-*
     would misrepresent the intended tint, so it gets its own small scale
     rather than being rounded into the nearest neutral rung. */
  --chrome-tint-1: rgba(14, 18, 24, 0.96);
  --chrome-tint-2: rgba(15, 18, 30, 0.9);
  --chrome-tint-3: rgba(22, 33, 56, 0.98);

  /* Off-white text/border family — the lighter counterpart to --chrome-*,
     same "smaller panels have no token" problem. --fg-2/--fg-3 above are
     alpha-over-panel-bg; these are flat opaque greys used mostly by
     TrajectoryPanel's frame-counter readout and a few borders. */
  --ink-1: #888;      /* dim/disabled text (near-dupes: #7a7a7a, #7f7f7f) */
  --ink-2: #999;      /* dim border/text */
  --ink-3: #ccc;      /* dominant mid-tone text/border — the mode of this family (near-dupe: #b8b8b8) */
  --ink-4: #ddd;      /* near-dupe: #dcdcdc */
  --ink-5: #e8e8e8;
  --ink-6: #f5f5f5;   /* near-white text */

  /* ---- Wash / tint scale (Wave 2.5) ---------------------------------------
     rgba(255,255,255,X) hover tints, card backgrounds and subtle input
     fills — the single most-repeated un-tokenised value in the Wave 2
     audit. Four alphas actually dominate (counts from docs/styles/*.css):
     0.08×20, 0.15×16, 0.05×14, 0.12×6. The rest of the 0.02-0.16 range is
     one-off long tail, not a real cluster — not given rungs of their own. */
  /* Wave 2.6 re-audit: the "one-off long tail" above turned out to have two
     real clusters in it once every panel's un-tokenised fill was counted,
     not just the ones already near an existing rung:
       --wash-0   (0.02×6, .025×4, .03×7, .04×6, .045×1 = 24) — fainter than
                  --wash-1 itself; .03 is the mode and centres the spread.
       --wash-1-5 (0.06×13, 0.07×4 = 17) — sits between --wash-1 (.05) and
                  --wash-2 (.08), same naming pattern as --sp-3-5.
     0.09/0.10 (1 site each) fold toward --wash-2; 0.14 folds toward
     --wash-4; 0.16×2 is close enough to --wash-4 (.15) to fold up rather
     than mint a fifth named rung for two sites. */
  --wash-0: rgba(255, 255, 255, 0.03);
  --wash-1: rgba(255, 255, 255, 0.05); /* subtle fill (backgrounds, faint borders) */
  --wash-1-5: rgba(255, 255, 255, 0.06);
  --wash-2: rgba(255, 255, 255, 0.08); /* the dominant card/tile background + hover tint */
  --wash-3: rgba(255, 255, 255, 0.12); /* stronger hover/active fill */
  --wash-4: rgba(255, 255, 255, 0.15); /* border tier between --line-1 (.1) and --line-2 (.2); mostly used on `border`, not fills — kept in the wash scale rather than the line family because it repeats the exact 0.02-0.16 white-alpha idiom this scale exists for, not the line pair's border-specific naming */

  /* ---- Shadow tokens (Wave 2.5) --------------------------------------------
     Deliberately separate from --overlay: a drop shadow and a full-screen
     scrim are different things and must be free to move independently once
     a light theme exists. --shadow-rgb is the bare channel triple (mirrors
     the --danger-rgb pattern above) for call sites that need an alpha this
     scale doesn't name. --shadow-sm/-md/-lg/-glow are whole box-shadow
     values, not just colours: blur/spread vary per call site (2px/6px vs
     4px/12px vs 0/4px/2px vs 4px/16px) so a bare colour token alone
     wouldn't remove the duplication — these four shapes are the ones that
     already repeat verbatim across multiple files. */
  --shadow-rgb: 0 0 0;
  --shadow-sm: 0 2px 6px rgba(0, 0, 0, 0.2);   /* file-browser panel shadow */
  --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.3);  /* elevated popover/menu (info-panel, toggle_styles ×2 exact) */
  --shadow-lg: 0 4px 16px rgba(0, 0, 0, 0.4);  /* biggest single-use panel shadow (crop toolbar) */
  --shadow-glow: 0 0 4px 2px rgba(0, 0, 0, 0.3); /* no-offset ring/glow — the most-repeated shadow shape in the app (6 near-dup sites at .2/.3 alpha) */

  /* ---- Controls ----------------------------------------------------------
     --control-h is the dominant button/input height found in the audit;
     --control-h-touch is the WCAG/iOS minimum tap target, used under
     pointer:coarse media queries. */
  --control-h: 34px;
  --control-h-touch: 44px;

  /* ---- Type --------------------------------------------------------------
     Every size is calc(<px> * var(--cv-font-scale, 1)) so the user font-scale
     control (ui/FontScaleModule.js, which sets --cv-font-scale on
     documentElement) keeps scaling every rung, not just the ones a component
     happened to opt into before. */
  --font-ui: 'CrysViz Sans', 'CrysViz Sans Math', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  --font-mono: 'Fira Code', 'SFMono-Regular', ui-monospace, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
  --font-display: 'CrysViz Sans', 'CrysViz Sans Math', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;

  --fs-xs: calc(10px * var(--cv-font-scale, 1));
  --fs-sm: calc(11px * var(--cv-font-scale, 1));
  --fs-base: calc(12px * var(--cv-font-scale, 1));  /* the single most common font-size in the app */
  --fs-md: calc(13px * var(--cv-font-scale, 1));
  --fs-lg: calc(14px * var(--cv-font-scale, 1));
  --fs-xl: calc(16px * var(--cv-font-scale, 1));
  --fs-2xl: calc(20px * var(--cv-font-scale, 1));
  --fs-3xl: calc(24px * var(--cv-font-scale, 1));
  --fs-hero: calc(64px * var(--cv-font-scale, 1));  /* the one big-number/splash outlier */

  --fw-normal: 400;
  --fw-medium: 500;
  --fw-bold: 600;    /* this codebase's actual "emphasis" weight — 600 far outnumbers literal 700/bold */
  --lh-tight: 1;
  --lh-normal: 1.4;

  /* ---- Space -------------------------------------------------------------
     4/6/8/12/16/24px — all six are independently common margin/padding
     values in the audit; see TOKENS.md for the off-grid 10px/5px note. */
  --sp-1: 4px;
  --sp-2: 6px;
  --sp-3: 8px;
  --sp-3-5: 10px; /* Wave 2.5: 10px is the single most-repeated off-grid spacing
     value (~60+ sites) and sits exactly equidistant between --sp-3 (8) and
     --sp-4 (12) — there's no "nearer neighbour" to snap to, so every one of
     those 60+ sites would need its own directional judgment call. 15px, by
     contrast, is 1px from --sp-5 (16px) and gets no rung — see TOKENS.md for
     the full reasoning on both. */
  --sp-4: 12px;
  --sp-5: 16px;
  --sp-6: 24px;

  /* ---- Depth ---------------------------------------------------------------
     Grouped by role, not a literal copy of today's numbers — the app's
     existing z-index values disagree with each other (see TOKENS.md "known
     inversions"). 1000 apart for headroom; nothing on screen changes until
     Wave 2 substitutes these in for the raw numbers. */
  --z-canvas: 0;
  --z-panel: 1;
  --z-dock: 1200;
  --z-popup: 2000;
  --z-menu: 3000;
  --z-modal: 4000;
  --z-tooltip: 5000;

  /* ---- Depth: literal stacking ladder (Wave 2.5) --------------------------
     The flat ladder above is a TARGET grouping — Wave 1 said outright its
     numbers aren't a copy of what's on screen today, and adopting it changes
     two real stacking relationships (see "known inversions" in TOKENS.md).
     This second ladder is the opposite: every value below is the exact
     current literal for its selector(s), so substituting one of these tokens
     in is a pure refactor with zero visual change. One exception, fixed on
     purpose rather than preserved: --z-context-menu (see its own comment
     below) — every other relationship here is untouched. Only z-index
     values that participate in the app-wide stack are here (100 and up);
     the small in-component values (-2 to 20, sticky-cell/slider/colorbar
     layers) stay literal, since tokenising them would imply a global
     relationship none of them have. Full value → token → selector mapping
     is in TOKENS.md so adoption is mechanical. --z-dock (1200, above)
     already equals its one real call site (.cv-panel.cv-floating) exactly,
     so it's reused here rather than redefined — everything below is new. */
  --z-inline-alert: 100;
  --z-mobile-scrim: 900;
  --z-anchor: 999;
  --z-overlay-widget: 1000;
  --z-gizmo-controls: 1001;
  --z-comp-panel: 1100;
  /* --z-dock: 1200 (above) */
  --z-dock-pane: 1300;
  --z-dock-handle: 1301;
  --z-dock-hint: 1302;
  --z-dock-scrim: 1350;
  --z-dock-expanded: 1400;
  --z-mobile-panel: 1500;
  --z-chrome: 2000;
  /* info-panel-overlay/info-panel (ui/InfoPanel.js) are a second popup-vs-dock
     inversion (TOKENS.md): the ⓘ button that opens them lives ON a floating
     .cv-panel/side-dock tab (--z-dock, 1200), so the overlay+card need to
     out-rank every dock-role element or they render behind the very panel
     that opened them. Same full-screen-scrim-plus-centered-card shape as
     #aboutOverlay/#aboutModal, so --z-chrome is the matching role; the card
     gets its own rung one above so it wins the paint order even if the two
     elements are ever reordered in the DOM (they're siblings, not nested,
     unlike #aboutModal inside #aboutOverlay). */
  --z-info-panel: calc(var(--z-chrome) + 1);
  --z-dialog: 3000;
  --z-confirm: 3100;
  /* Was 3200 — outranked the app's own blocking modals (3000/3100), the
     known inversion TOKENS.md flagged. .cv-panel-menu (panelWindow.css) is
     this token's only consumer, and a modal's full-viewport backdrop
     already covers every panel's ≡ button while open (see PanelWindow.js —
     no menuSections entry opens a modal either), so no flow can open this
     menu while a modal is up; moving it below the modal band is safe. 2500
     sits in the one gap nothing else claims: above --z-chrome (2000, the
     highest thing it still needs to outrank) and below --z-dialog (3000). */
  --z-context-menu: 2500;

  /* ---- Layout geometry (single source of truth) ------------------------- */
  --ui-width: 380px;                 /* panel content width */
  --ui-border: 1px;                  /* panel border width */
  --ui-padding: 20px;                /* panel padding */
  --ui-total-width: calc(var(--ui-width) + 2 * var(--ui-border) + 2 * var(--ui-padding));
  --gizmo-size: 90px;
  --gizmo-bottom: 20px;
  --gizmo-left: calc(var(--ui-total-width) + 10px);   /* beside the panel */
  --camera-tools-left: calc(var(--ui-total-width) + 10px);
  --popup-left: calc(var(--ui-total-width) + 10px);

  /* ---- 3D scene (read by ThemeManager → three.js) ----------------------- */
  --scene-bg: #ffffff;               /* WebGL canvas clear color */
  --lattice-color: #090a09;          /* unit-cell lines */
  /* Background-color picker dot (styles.css .background-dot) border — dark on
     this theme's light-ish scene, for the same contrast reason --lattice-color
     is dark here. Used to be a raw `@media (prefers-color-scheme: dark)` rule
     in responsive.css that forced the dark-theme value regardless of which
     theme was actually selected (picking "Light" on a dark-mode OS left the
     dot stuck dark) — that query is gone; dark/theme.css now overrides this
     token instead, so ThemeManager's own theme resolution is the only thing
     that decides it. twilight/theme.css deliberately does NOT restate this:
     its scene is light-ish too, so the same dark border applies via this
     base value, same as --lattice-color. */
  --dot-border: rgba(26, 26, 26, 0.8);

  /* Same job as --dot-border, for the element-picker tiles: their borders are
     chemistry data (PeriodicTablePickerCore.js's category colours, or the
     structure's own atom colours), all picked against THIS popup surface, so
     every palette that moves the surface loses one end of the range — the
     alkaline-earth yellow on the light panels, the superheavies' grey on
     Fluorite's plum. A palette sets this to a hairline and every tile keeps a
     findable edge whatever colour it carries; here the colours are already
     tuned against this exact background, so no ring. */
  --tile-edge: transparent;

  /* ---- Icons (in-panel "docked" measure tools) -------------------------- */
  /* These default to the app's shared icons. A theme can re-skin them by
     dropping its own files in themes/<id>/icons/ and overriding these vars
     (url(...) is resolved relative to that theme's theme.css). */
  --icon-measure-distance: url('../../data/icons/measure-icon2.svg');
  --icon-measure-angle:    url('../../data/icons/angle-icon2.svg');
  --icon-measure-clear:    url('../../data/icons/delete-icon2.svg');
  --icon-measure-restore:  url('../../data/icons/restore-icon.svg');
  /* Delete uses a ❌ glyph (like the floating panel), not an icon image. */

  /* The theme-picker glyphs are dark-on-transparent SVGs, inverted to sit on a
     dark panel. This was a hardcoded filter:invert(1) in controlPanel.css,
     which no light theme could reach — a light palette sets this to `none`. */
  --icon-filter: invert(1);

  /* The opposite polarity: the compact toolbar glyphs are white-on-transparent
     SVGs, already legible on a dark chip. A light palette inverts these. */
  --icon-filter-bright: none;

  /* Native form-control rendering (the <select> popup especially). A light
     palette must flip this or the UA paints dark widgets on a light panel. */
  --color-scheme: dark;
  --select-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath fill='%23c8c8c8' d='M0 0h10L5 6z'/%3E%3C/svg%3E");

  /* ==========================================================================
     Hue families that used to sit as literals in docs/styles/
     --------------------------------------------------------------------------
     These were the css_guard baseline: single-call-site colours the original
     consolidation judged too rare to name. Light-panelled themes changed that
     calculus — a literal in a component stylesheet is a hole a theme cannot
     reach, and every one of these would have shown through as a dark-panel
     assumption. Values are the originals, so the default palette is unchanged.
     ========================================================================== */

  /* ---- Danger ------------------------------------------------------------
     --danger (amber-orange) is the *fill*; these are the red foreground tier.
     Kept split per the fg-vs-fill rule: folding a foreground into its fill
     sibling has measurably dropped contrast here before. */
  --danger-bright: #ff6b6b;         /* error/destructive text on a dark panel */
  --danger-bright-rgb: 255 107 107; /* same hue, for rgb(... / alpha) glows */
  --danger-strong: #e63946;         /* saturated destructive icon hover */
  --danger-fill: rgba(200, 35, 35, 0.92); /* solid destructive button */


  /* ---- Caution ------------------------------------------------------------ */
  --warn-soft: rgba(243, 174, 100, 0.9);  /* soft-refusal borders and notes */
  --warn-banner-bg: rgba(40, 30, 0, 0.85);
  --best-row-rgb: 255 193 7;        /* "best match" row highlight (amber) */

  /* ---- Positive / switches ------------------------------------------------
     Every pill toggle's checked track is --highlight-color now (the toggle
     unification retired --switch-on/--switch-on-alt); only the OFF track and
     the knob keep switch-specific tokens. */
  /* The toggle knob contrasts with its TRACK, not with the panel, so it does
     not follow --fg-1 the way body text does. It was --fg-1, which is white
     here and correct — and a black dot on a light-panelled theme. */
  --switch-knob: #ffffff;
  /* The OFF track. Was --ink-3, its only user — a text-ramp token doing a
     control fill's job, which put a heavy dark pill on a light panel. */
  --switch-off: #cccccc;
  --seg-active-bg: #0d8a36;         /* active segmented-control button */
  --btn-glow-rgb: 54 143 110;       /* button hover glow */
  --on-accent: #0c1f17;             /* text sitting ON an accent fill */
  --thumb-border: #d7f5e8;          /* slider thumb ring */

  /* ---- Informational blues ------------------------------------------------ */
  --confirm-bg: #0066cc;
  --confirm-border: #004499;
  --link-hover: #81d4fa;
  --row-selected-bg: rgba(100, 150, 255, 0.12);
  --locked-bg: rgba(25, 55, 110, 0.28);       /* the Wyckoff "locked" surface */
  --locked-badge-bg: rgba(32, 77, 160, 0.35);
  --locked-grad-1: #1c5fb8;
  --locked-grad-2: #2493ff;

  /* ---- The About modal ----------------------------------------------------
     A surface of its own (green-tinted, unlike every other panel), so it needs
     its own handful rather than borrowing the panel tokens. */
  --about-bg: #0b1f1c;
  --about-border: rgba(129, 199, 132, 0.4);
  --about-code-bg: rgba(12, 45, 36, 0.7);
  --about-close-hover: #a5d6a7;

  /* ---- Neutrals ----------------------------------------------------------- */
  --overlay-60: rgba(0, 0, 0, 0.6);   /* between --overlay (.5) and --overlay-80 */
  --overlay-70: rgba(0, 0, 0, 0.7);
  --label-bg: rgba(0, 0, 0, 0.65);    /* in-scene measurement label backing */
  --label-ink: #000000;               /* ink on that label — it sits on a light sprite */
  --line-3: rgba(255, 255, 255, 0.5); /* brighter hairline than --line-2 (.2) */
  --line-dark: rgba(0, 0, 0, 0.15);   /* hairline ON a light accent fill */
  --line-grey: #808080;               /* opaque mid-grey border */
  --track-bg: rgba(128, 128, 128, 0.15);        /* slider/progress groove */
  --track-bg-strong: rgba(150, 150, 150, 0.5);

  /* ---- Extra opaque surfaces ---------------------------------------------- */
  --textarea-bg: rgba(16, 16, 16, 0.8);
  --float-panel-bg: rgba(26, 26, 26, 0.8);  /* floating scene widgets */
  /* Widget-mode composition legend: a very light gray, the same in every
     palette (the embed deliberately wants this look). Used only by
     body.widget-mode in docs/styles/widgetMode.css. */
  --comp-legend-widget-bg: #f2f2f2;
  --comp-legend-widget-fg: #222222; /* fixed dark ink for text on that light bg */
  --popup-bg-deep: rgba(13, 13, 13, 0.95);

  /* ---- Lattice axis legend ------------------------------------------------
     a/b/c encode DATA — they identify the axes, and the #axesLegend dots have
     to match the 3D gizmo arrows exactly. Those arrows are hardcoded three.js
     colours in ui/WindowAndSceneControls.js (makeArrow/makeLabel), so these
     three values are pinned to them and a palette must NOT override them:
     retinting one side without the other is what made the legend and the
     gizmo disagree. To make them themeable, mirror these into three.js the
     way ThemeManager already mirrors --scene-bg and --lattice-color, and
     recolour the arrow materials and label sprites on theme change. */
  --axis-a: #ff3333;
  --axis-b: #33cc33;
  --axis-c: #3366ff;

  /* ---- Active tool -------------------------------------------------------- */
  --tool-active: #ff6b35;
  --tool-active-rgb: 255 107 53;    /* same hue, for the active-tool glow */
}

/* ---- Accent palette (changes with the active backend mode) ----------------
   These classes are toggled on <body> by ui/BackendPanel/BackendTheme.js. A
   theme can recolor the whole app's accents by redefining these in its own
   theme.css (which loads after this base, so it wins the cascade). */
.theme-standard {
  --bg-color: rgba(6, 70, 50, 0.8);
  --highlight-color: rgba(6, 140, 50, 1.0);
  --accent-color: rgba(6, 140, 50, 0.8);
  --hover-color: rgba(6, 140, 50, 0.3);
  --border-color: rgba(6, 90, 50, 0.5);
  /* The accent used as TEXT. Same value as --highlight-color here, because on
     a dark panel one colour serves both roles; light themes must split them. */
  --accent-fg: rgba(6, 140, 50, 1.0);
}

.theme-symmetry {
  --bg-color: rgba(15, 3, 56, 0.8);
  --highlight-color: rgba(18, 141, 204, 1.0);
  --accent-color: rgba(18, 141, 204, 0.8);
  --hover-color: rgba(18, 141, 204, 0.2);
  --border-color: rgba(17, 71, 129, 0.5);
  --accent-fg: rgba(18, 141, 204, 1.0);
}
