/* ═══════════════════════════════════════════════════════
   @file theme.css
   @author Jouni Miikki
   SPDX-License-Identifier: MIT
   @description AIMEAT Unified Theme — single source of truth for all portal
     CSS variables (tokens), base styles, animations, and shared components.
     Defines the light theme in :root, a dark override in [data-theme="dark"],
     and a drift-alias layer so undefined-but-referenced vars resolve.
   @structure :root tokens, drift aliases, [data-theme="dark"] override,
     base/reset, animations, topnav, buttons, cards, badges, inputs, modal,
     toast, alert.
   @usage Loaded first in spa.html <head>; the theme is toggled by /js/theme.js
     which flips the data-theme attribute on <html>.
   @version-history
     v1.47.0 — 2026-09-27 — Four shape values for what the app catalogue draws in its own way, each at today's look so nothing changes in AIMEAT and a theme can set them: --shape-corner-soft (8px), --shape-corner-card (10px), --shape-ring (3px, a spinner's ring) and --shape-chosen-ground (the ground of a chosen row, the sun) (Jouni: appcat is themable).
     v1.46.0 — 2026-09-26 — --ink-ground and --on-ink-ground: a dark block that stays dark in dark mode (the contents rail, the page thumbnail), so its sun labels read (Jouni: "yellow text on so light colored background. unreadable").
     v1.41.0 — 2026-09-24 — The shape tokens (--shape-corner…, --shape-frame…, --shape-shadow…,
       --shape-case…): the poster's corners, frames, shadows and letter case as values a theme sets,
       with today's values, read by the component sheets (a move).
     v1.40.0 — 2026-09-14 —The toast wears the poster face: square, 3px ink frame on the card ground, a solid
       6px offset shadow (sun, coral for a failure, ink under the warning's sun ground) and a ✓ or ✗ in front.
       The green, red and blue pill tokens (--toast-*) leave; nothing else read them. .toast-hold keeps a
       message up for six seconds.
     v1.39.0 — 2026-09-13 — .toast undoes the browser's popover box (inset, margin, border), because the
       pill now enters the top layer as a manual popover to show above an open dialog.
     v1.38.0 — 2026-09-13 — The classic .modal-overlay / .modal / .modal-header / .modal-close / .modal-footer
       rules leave: components/Modal.js renders a native <dialog class="dlg"> styled by /css/dialog.css, and
       nothing else on the site used them.
     v1.37.0 — 2026-08-29 — html { overflow: clip }: a scrollIntoView() inside .page-content could scroll the
       WINDOW by the header's height (the static agent-footer makes the document taller than the
       viewport, and overflow: hidden does not stop programmatic scrolling), which hid the top bar and
       left a blank band at the bottom. clip makes the window unscrollable outright.
     v1.36.0 — 2026-08-29 — --font-wordmark (Archivo Black) for AIME♥AT, which is a mark and not a headline;
       --font-poster-tracking (.01em) and --font-poster-leading (1) for the headline face, read by every
       poster headline instead of the -.035em / .9 that had been measured for Archivo Black and made
       Fjalla One's letters touch.
     v1.35.0 — 2026-08-29 — Three master type tokens: --font-headline (Fjalla One), --font-body (Archivo) and
       --font-mono; --font, --font-showroom, --font-showroom-body, --font-poster and --font-poster-section
       are aliases of them. The inside pages' headlines move from Archivo Black to the same face the
       front page wears, and the classic shell's body from DM Sans to Archivo, so the site's type is
       changed on three lines rather than hunted across three registers.
     v1.34.0 — 2026-08-29 — --font-showroom is Fjalla One (design canvas "AIMEAT Index Pages", face H): the
       showroom headlines, stickers and slabs on the front page and the index pages change in this one
       token. Bungee read as noise to Jouni; Fjalla is condensed with a plain M and N.
     v1.33.0 — 2026-08-29 — The lighter cut the reviewed canvas showed on a phone: --font-poster-section
       (Archivo regular, uppercase) for the headline under a page title, running text at 400 and
       emphasis at 600 through the two weight tokens, the bar's links at 700.
     v1.32.0 — 2026-08-29 — Poster weight tokens: --font-poster-weight (the headline cut, 400 on
       Archivo Black), --font-poster-text-weight (500) and --font-poster-strong-weight (700) for the
       machine room's running text and its emphasised words, so the face and its weights change
       in one place. --on-sun is the ink for words on a sun selection. The top bar wears the poster
       face: a 3px ink rule under it, bold links with the open one underlined in coral, the morsel
       count on a sun chip. The sign-in pill itself lives in the auth lib (sdk-libs/auth/pill.js)
       and now draws its colours from the page's own tokens instead of a gold gradient.
     v1.31.0 — 2026-08-28 — --font-poster (Archivo Black): the display face of the poster skin the
       home, the settings overview and the chat wear (design canvas "AIMEAT Home, Settings and
       Chat", direction C). Additive only; loaded in spa.html with the other faces.
     v1.30.1 — 2026-08-28 — The two font tokens renamed --font-showroom / --font-showroom-body:
       --font-display was already the variable lib/aimeat-theme.css reads for every heading, so
       defining it here put Bungee on the whole shell.
     v1.30.0 — 2026-08-28 — Three tokens for the showroom front page: --font-display (Bungee),
       --font-marketing (Archivo) and --sun, the sticker yellow. Additive only.
     v1.29.0 — 2026-08-27 — .start-page replaces .ui-switch: the start-page setting
       (components/StartPageSetting.js) in place of the home-or-profile switch.
     v1.28.0 — 2026-08-17 — The palette bridge: html[data-palette] maps aimeat-theme.css's
       --color-* tokens onto the shell's own, so the golden pill's palette picker finally changes
       what the apex looks like. Inert with no palette chosen (the default removes the attribute).
     v1.27.0 — 2026-08-09 — Header slims, footer arrives. `.topnav-me*` (the "Me" dropdown) is gone:
       Apps/Profile/Admin are plain links again, because the marketing links now leave the bar when a
       person signs in. New `.site-footer` carries what the header stopped carrying, inside
       .page-content so it costs no viewport height.
     v1.26.0 — 2026-08-08 — Copy control, one style: `.copied` joins `.btn-primary.success` (the green <CopyButton>
         stamps for ~2s), `.btn-outline.copied`/`.btn-ghost.copied` give the quiet buttons the same
         confirmation, and `.btn-copy-inline` is the ONE copy modifier — the field/row case where a
         copy button must not stretch or wrap. Replaces 18 per-view copy-button classes.
     v1.24.0 — 2026-07-21 — App-shell scroll model: html/body no longer scroll (height:100%;
       overflow:hidden); #app is a full-viewport (100dvh) flex column with the topnav as a fixed-height
       row and .page-content as the sole overflow-y:auto scroll region. Removes the ~nav-height phantom
       overscroll and stops view content sliding under the (formerly sticky, now in-frame) header. Coupled
       offset fixes: .pf-sidebar/.adm-sidebar stick to top:0 of the region and cap at 100dvh−nav-height.
     v1.25.0 — 2026-07-28 — Header nav wraps instead of overflowing and its labels never break
       mid-phrase (.topnav-menu flex-wrap + > a white-space:nowrap). Adding Learn and EXCHANGE
       made two-word links split into "Try / it" on nodes that also run Companies/Members.
     v1.24.0 — 2026-07-28 — Live counters: add .stat-card-value.is-loading (quiet placeholder while
       the fetch is in flight — a hard 0 on a live counter reads as a dead node) and .stat-card-sub
       (second line under a counter, e.g. "2 online now"). Additive only.
     v1.23.1 — 2026-07-20 — Mobile: pin .notif-dropdown to fixed viewport insets under 560px (the
       320px, right:0-anchored panel ran off the LEFT edge — the bell sits mid-right of the bar — and
       got clipped by body{overflow-x:hidden}); long notif titles/bodies wrap (overflow-wrap:anywhere).
     v1.23.0 — 2026-07-18 — Notification bell inline actions: split .notif-item into a clickable
       .notif-item-main + an actions row; add .notif-actions/.notif-action-btn/.notif-reply-(field|send) and .notif-result.
     v1.22.0 — 2026-07-17 — UI-unification foundation: extend the spacing scale (--sp-5=20/--sp-6=24/
       --sp-8=32/--sp-12=48) and add a font-size scale (--text-xs..--text-xl) so views stop hardcoding
       padding:24px / font-size:0.85rem inline; add .muted/.text-dim/.text-faint/.meta text utilities
       (there was no muted/meta utility class, so ~8 views invented .xx-muted and ~30 invented .xx-*-meta).
       Additive only — no existing rule changed, so appearance is preserved until views adopt them.
     v1.21.0 — 2026-07-06 — Toast variants + mobile width: add --toast-info-bg/--toast-warn-bg tokens +
       .toast-info/.toast-warn classes (call sites pass 'info'/'warning' kinds; they previously fell into the
       truthy-isError red path). .toast width fit-content → max-content: anchored at left:50%, fit-content
       shrink-to-fit capped the pill at 50vw (a skinny tower on phones); max-width min(92vw, 40rem) now rules.
     v1.20.0 — 2026-07-03 — Toast legibility: .toast max-width 420px → min(92vw, 40rem) (was hard-capped
       narrow on wide screens so longer messages wrapped into a tall skinny "tower"), plus line-height 1.45,
       centered text, overflow-wrap:anywhere (guards long GAIIs/URLs) and a --shadow-xl lift. One class fixes
       every toast pathway. Appearance of short toasts unchanged (still a compact pill via width:fit-content).
     v1.19.0 — 2026-06-10 — Controls get their own surface: new --control-bg/--control-border tokens
       (light: white + gray-300; dark: raised #262934 + #4A4E5A). .btn-outline/.btn-ghost drop
       background:transparent (they melted into the page as "framed text") and .input-field drops
       background:var(--bg) (fields were LITERALLY the page color) — all three now use the control
       tokens, buttons additionally get --shadow-sm so they read as pressable.
     v1.18.0 — 2026-06-02 — Component unification (#10 third toast pathway): tokenize the shared .toast color literals. Added a theme-invariant --toast-success-bg (green-500 #22C55E @ .95, the legacy toast green — deliberately NOT --success emerald, which would shift the hue), --toast-danger-bg (= --danger #EF4444 @ .95) and --toast-fg (white), same rationale as the badge palette. The base .toast and the .toast.error/.toast-error/.toast-success modifiers now reference these tokens instead of raw rgba()/#fff. Appearance-preserving: identical resolved values; solid saturated pills intentionally do not flip in dark mode. profile.js keeps its own showToast pathway (distinct .pf.toast-container wrapper + .toast.error markup) — left as a convergence candidate, NOT migrated, to avoid changing DOM/behavior.
     v1.17.0 — 2026-06-02 — Component unification (#26 recipient badge): add a theme-invariant --badge-*-bg palette (wildcard→danger, ghii→purple, organism→success, gaii→blue; domain #F97316 / node #6B7280 kept as own invariant swatches as they have no palette match) + --badge-fg white, so the profile .badge-* recipient classifier can drop its hardcoded hex (#8b5cf6/#f97316/#6b7280/#3b82f6) and dead light-only fallbacks (var(--success,#22c55e)/var(--danger,#ef4444)). Appearance-preserving: values resolve identically and these solid swatches intentionally do not flip in dark mode.
     v1.16.0 — 2026-06-02 — Component unification (#25 pill base): add a canonical token-only .pill shape primitive (inline-flex chip, --radius-full, compact padding, small bold text, pointer + opacity hover) — shape only, no colors; tints come from caller classes. Mirrors the shape the profile .vis-pill already used. The scoped .pf .vis-pill keeps its own shape declaration (the (0,2,0)-beats-bare-.pill specificity trap) but its radius was retokenized to the same --radius-full so the two stay in lockstep.
     v1.15.0 — 2026-06-02 — Component unification (#23 collapsible): add canonical .collapsible / .collapsible-header / .collapsible-chevron (chevron rotates 180° via .open) / .collapsible-body block, backing /components/Collapsible.js. All colors are theme tokens so it flips in dark mode. Establishes the canonical expand/collapse section for the hand-rolled toggles (.expand-btn + .pf-chevron, .scope-advanced-toggle, .pkv-entry-arrow); those bespoke toggles are left in place where their expanded markup is wired into view state.
     v1.14.0 — 2026-06-02 — Component unification (#24 search bar): add canonical .search-bar (flex row, gap, flex:1) + .search-bar-input (flex:1) shell, backing /components/SearchBar.js. The input reuses .input-field for all visuals, so the shell only owns layout. Consolidates the hand-rolled search inputs (.pkv-search / .cap-search-input / .pf-agd-search-input) and the profile .search-bar + .input-field combo; the profile-scoped .pf .search-bar (max-width) coexists.
     v1.13.0 — 2026-06-02 — Component unification (#21 key/value rows): add canonical .kv-row / .kv-label / .kv-value (flex space-between, muted label, emphasized value, --border-subtle divider, last-child no border; .kv-value.mono for monospace ids/amounts), backing /components/KeyValueRow.js. Extracted as the common look of the admin .adm-erow/.adm-hrow and profile .pf-agd-info-row families. Existing per-view classes left in place where their look differs.
     v1.12.0 — 2026-06-02 — Component unification (#13 tables): add canonical .data-table block (full-width collapsed table, uppercase muted header divided by --border, cells by --border-subtle, --bg-surface hover, .mono cell) + generic .scrollable wrapper, backing /components/DataTable.js. Extracted as the common look of .adm table + .pf .consent-table so both converge on tokens that flip in dark mode.
     v1.11.0 — 2026-06-02 — Component unification (#16 tags): add --accent-border token (translucent accent hairline, light rgba(232,86,74,0.2) / dark rgba(255,111,98,0.30)) so tag/badge clones (mk-tag, hb-tag) can replace their hardcoded rgba(232,86,74,0.2) borders and flip in dark mode.
     v1.10.0 — 2026-06-02 — Component unification (#17 pagination): add canonical .pagination (prev / "Page X / Y" / next, token-based) + .pagination-info + .load-more (centered load-more wrapper), backing /components/Pagination.js. Consolidates the .adm-mem-pagination shape and the centered load-more wrapper. The numbered .hb-pagination variant is intentionally left in place (different UI).
     v1.9.0 — 2026-06-02 — Component unification (#18 category cards): add canonical .category-card / -icon / -name / -count (token-based, anchor- and div-safe) so the marketplace .mk-category-* and hobbies .hb-category-* clones converge. Tokenized the formerly hardcoded hover colors (#E8564A → var(--accent); 0 2px 8px rgba(0,0,0,0.06) → var(--shadow-md)) — dark-mode fix. Per-view *-categories-grid wrappers kept (column density differs).
     v1.8.0 — 2026-06-02 — Component unification (#11 status dots): add canonical .status-dot base + semantic token modifiers (online/active/healthy/live → success; warn/degraded/syncing/pending → warn; error/offline/dead/down → danger; idle/inactive/detached/muted → text-muted), backing /components/StatusDot.js, so per-view dots (.peer-dot/.pn-status-dot/.ext-status-dot/.dv-dot/.live-dot) converge. Tokenized away hardcoded hex in the former .ext-status-dot (#4ade80/#9ca3af) so dots flip in dark mode.
     v1.7.0 — 2026-06-02 — Component unification (#15 headers): add canonical .section-title/.section-desc (in-page accent header, lifted generic from .pf) + .page-title/.page-subtitle (top-of-view gradient header), all token-based, so per-view title/subtitle clones (mk-/hb-/cl-welcome-/adm-page-title) can converge.
     v1.6.0 — 2026-06-02 — Component unification (#22): add .card-glass modifier (Card variant="glass") reproducing the former profile .pf-glass-card appearance, fully tokenized (--surface / --border), so profile's GlassCard folds into the canonical Card.
     v1.5.0 — 2026-06-02 — Component unification (#6 stat cards): add canonical .stat-card-value tone (.accent/.success/.danger) + .mono modifiers and a .stat-grid wrapper, so per-view stat clones (mk-stat/hb-stat/dv-stat/wl-stat/...) converge onto .stat-card without losing accent-tint / earned-spent / monospace semantics.
     v1.4.0 — 2026-06-02 — Component unification (#5 inputs): add canonical textarea.input-field (resize/min-height) + select.input-field (cursor) so .input-field works on all field types and views can drop per-view form-input/textarea/select clones (mk-form/hb-form/...).
     v1.3.0 — 2026-06-02 — Component unification (#4): make canonical .btn-* anchor-safe (text-decoration:none) so views can drop their per-view <a>-button clones (mk-btn/hb-btn/...) and use the canonical classes.
     v1.2.0 — 2026-06-02 — Tokenize shell component literals (semantic tints, scrollbar, overlay scrim, morsel pill, lang-btn active, input focus) so the whole shell flips for dark mode.
     v1.1.0 — 2026-06-02 — Add dark theme override + drift-alias layer + .theme-toggle-btn.
     v1.0.0 — prior — Light theme tokens, base styles, shared components.
   ═══════════════════════════════════════════════════════ */


/* ── Root Variables ── */
:root {
  /* Background */
  --bg: #FAFAF8;
  --bg-surface: #F3F4F6;
  /* One card ground: --bg-card is the older name of --card-bg, kept as an alias so it follows it in
     every theme and palette (2026-09-24). */
  --bg-card: var(--card-bg);
  --bg-elevated: #FFFFFF;

  /* Cards & Surfaces */
  --card-bg: #FFFFFF;
  --card-bg-hover: #FFFFFF;
  --card-bg-solid: #FFFFFF;
  --card-bg-alt: #F3F4F6;
  --card-border: #E5E7EB;

  /* Accent (coral heart) */
  --accent: #E8564A;
  --accent-bright: #FF6B6B;
  --accent-deep: #E8564A;
  --accent-glow: rgba(232, 86, 74, 0.3);
  --accent-subtle: rgba(232, 86, 74, 0.06);
  --accent-border: rgba(232, 86, 74, 0.2);   /* translucent accent hairline for tag/badge edges */

  /* Love palette (profile gradients — now coral-based) */
  --love1: #E8564A;
  --love2: #FF6B6B;
  --love3: #FF8A80;
  --love4: #FF6B6B;
  --love5: #E8564A;

  /* Text */
  --text: #1A1A2E;
  --text-bright: #1A1A2E;
  --text-dim: #6B7280;
  --text-muted: #9CA3AF;

  /* Semantic colors */
  --success: #10B981;
  --warn: #F59E0B;
  --danger: #EF4444;
  --purple: #8B5CF6;

  /* Accent palette */
  --blue: #3B82F6;
  --mint: #34D399;
  --teal: #06B6D4;
  --pink: #F472B6;
  --orange: #FB923C;
  --yellow: #FBBF24;

  /* Recipient badge palette (profile permission-recipient classifier:
     wildcard/ghii/organism/domain/node/gaii). Theme-INVARIANT on purpose —
     these are saturated solid swatches carrying white text in both themes, so
     they are defined only here and NOT re-overridden in [data-theme="dark"].
     Four map onto existing palette tokens (identical values); domain/node have
     no palette match and are kept as their own invariant swatches rather than
     borrowed from --orange/--text-dim (which differ / flip in dark). */
  --badge-wildcard-bg: var(--danger);   /* #EF4444 */
  --badge-ghii-bg:     var(--purple);   /* #8B5CF6 */
  --badge-organism-bg: var(--success);  /* #10B981 */
  --badge-domain-bg:   #F97316;         /* no palette match (≠ --orange #FB923C) */
  --badge-node-bg:     #6B7280;         /* invariant gray (≠ --text-dim, which flips dark) */
  --badge-gaii-bg:     var(--blue);     /* #3B82F6 */
  --badge-fg:          #FFFFFF;          /* white text on every saturated swatch */

  /* Borders */
  --border: #E5E7EB;
  --border-subtle: #F3F4F6;
  --border-focus: #E8564A;

  /* Radii */
  --radius: 16px;
  --radius-sm: 10px;
  --radius-xs: 6px;
  --radius-xl: 20px;
  --radius-full: 9999px;

  /* Shapes (07 "New components follow the theme", 2026-09-24): the corners, frames, shadows and
     letter case every component sheet reads, so a theme changes them once and a component added
     later follows it. These are the built-in theme's values, today's poster shapes; a theme's own
     come in its sheet (Themes & Styles, aimeat_theme_save `shapes`). The server reads this block
     for the built-in values (src/services/themes/shapes.ts), so a value changes here and nowhere
     else. Colour values name a style's token, so they follow light, dark and every style. */
  --shape-corner: 0;
  --shape-corner-control: 0;
  --shape-corner-pill: 0;
  --shape-corner-dialog: 0;
  --shape-corner-soft: 8px;
  --shape-corner-card: 10px;
  --shape-frame: 2px;
  --shape-frame-heavy: 3px;
  --shape-frame-colour: var(--text);
  --shape-rule-colour: var(--text);
  --shape-field-colour: var(--text);
  --shape-ring: 3px;
  --shape-chosen-ground: var(--sun);
  --shape-shadow: none;
  --shape-shadow-raised: 8px 8px 0 var(--sun);
  --shape-shadow-dialog: 12px 12px 0 var(--sun);
  --shape-shadow-action: 4px 4px 0 var(--sun);
  --shape-shadow-action-pressed: 2px 2px 0 var(--sun);
  --shape-shadow-chosen: 4px 4px 0 var(--text);
  --shape-case-heading: uppercase;
  --shape-case-action: uppercase;
  --shape-tracking-action: .04em;
  --shape-case-label: uppercase;
  --shape-tracking-label: .1em;

  /* Typography.
     THE DESIGN LANGUAGE IS WRITTEN DOWN ONCE: skill `aimeat-design-language` (in this repo under
     .claude/skills and on the node's skill library) names the faces, colours and shapes and maps
     each token below to the surfaces that read it. Change a value here; read the skill to know how
     far it reaches.

     THREE FACES, THREE LINES. Every other font token on this sheet is an alias of one of these, so
     the site's type changes here and nowhere else (2026-08-29; before that the front page, the
     inside pages and the classic shell each named their own family and a change meant three
     edits and a hunt):
       --font-headline  every headline, big numeral, sticker, slab and the wordmark, outside and in
       --font-body      everything read as a sentence, the labels, the buttons, the row names
       --font-mono      addresses, commands, ids, code
     A face has to be loaded to be used: spa.html's font link carries these three; a served page
     that reads this sheet (privacy, terms) links the same families.
     --font-body is deliberately the same name lib/aimeat-theme.css (the palette bridge) gives a
     palette's body face, and the bridge's house palette reads --font-headline for its display
     face: with no palette chosen these three lines rule every heading and sentence, and a chosen
     palette (paper, circuit, …) brings its own faces on purpose.
     NOT named --font-display: lib/aimeat-theme.css (the palette bridge, loaded on every page)
     already styles h1-h6 with var(--font-display, …), and a token by that name put Bungee on
     every heading in the shell for one afternoon. */
  --font-headline: 'Fjalla One', 'Archivo Black', 'Archivo', 'DM Sans', system-ui, sans-serif;
  --font-body: 'Archivo', 'DM Sans', system-ui, -apple-system, 'Segoe UI', sans-serif;
  --font-mono: 'JetBrains Mono', 'SF Mono', monospace;
  /* The classic shell's family (admin, the older profile tabs, form controls): the body face. */
  --font: var(--font-body);
  /* The showroom voice of the front page and its companions: headlines, stickers, slabs. */
  --font-showroom: var(--font-headline);
  --font-showroom-body: var(--font-body);
  /* The poster voice of the inside pages (home, settings and its tabs, chat): the headline face
     for every headline and big numeral, --font-showroom-body for everything else. The weights are
     tokens so the whole skin changes in one place: the headline face has one weight (Fjalla One
     and Archivo Black ship one cut each; put 'Archivo' first in --font-headline and raise
     --font-poster-weight for a lighter headline), the running text of the machine room sits at
     --font-poster-text-weight and its emphasised words (row names, labels, actions) at
     --font-poster-strong-weight. */
  --font-poster: var(--font-headline);
  --font-poster-weight: 400;
  --font-poster-text-weight: 400;
  --font-poster-strong-weight: 600;
  /* How the headline face is set. Archivo Black wanted to be squeezed (-.035em, line-height .9);
     Fjalla One is condensed already and its letters touch at that tracking and its lines collide
     at that leading (Jouni, 2026-08-29: "as if the letters ran into each other"). Every poster
     headline reads these two, so a change of face brings its own spacing here, not in ten sheets. */
  --font-poster-tracking: 0.01em;
  --font-poster-leading: 1;
  /* The wordmark is a mark, not a headline: AIME♥AT stays in Archivo Black whatever the headline
     face is, on the top bar, the served pages and the sign-in dialog (the auth library carries the
     same stack as its fallback). */
  --font-wordmark: 'Archivo Black', 'Archivo', 'DM Sans', system-ui, sans-serif;
  /* The section face: the headline under the page title ("My skills", "Node library") is the
     same uppercase and tracking as the page title but in the body face's regular cut, which is
     what gives the reviewed canvas its air. One face, one weight, changed here. */
  --font-poster-section: var(--font-body);
  --font-poster-section-weight: 400;
  /* Ink on the sun: the words on a sun-yellow selection stay ink in every theme, because --sun
     does not flip in dark mode and --text does. */
  --on-sun: #1A1A2E;

  /* The sticker yellow of the showroom front page. Used for the kicker, the price sticker and the
     live counters; not a semantic colour (that is --warn) and it does not flip in dark mode. */
  --sun: #FFB52E;

  /* The ink ground: a dark block with light words and sun labels (the contents rail, the page
     thumbnail). In light mode it is the text colour; in dark mode it stays dark (below), so its sun
     labels and light words stay readable. */
  --ink-ground: var(--text);
  --on-ink-ground: var(--bg);

  /* Layout */
  --nav-height: 56px;

  /* Shadows */
  --shadow-sm: 0 1px 2px rgba(0,0,0,0.04);
  --shadow-md: 0 4px 12px rgba(0,0,0,0.06);
  --shadow-lg: 0 8px 24px rgba(0,0,0,0.08);
  --shadow-xl: 0 16px 48px rgba(0,0,0,0.12);
  --shadow-heart: 0 4px 14px rgba(232,86,74,0.3);
  /* The classic primary button's fill, and the green a copy or a success turns (formerly literals
     in the button rules; the same in both themes, as they were). */
  --primary-gradient: linear-gradient(135deg, #E8564A 0%, #FF6B6B 100%);
  --success-gradient: linear-gradient(135deg, #10B981, #059669);
  --success-glow: 0 4px 12px rgba(16, 185, 129, 0.25);
  /* A card menu's state (components/card-menu.css): a light tint, a strong border and dark ink,
     measured for contrast there. The same in both themes, as they were. */
  --state-open-bg: #FEF3C7; --state-open-border: #B45309; --state-open-ink: #78350F;
  --state-working-bg: #D1FAE5; --state-working-border: #047857; --state-working-ink: #065F46;

  /* Input */
  --bg-input: var(--bg);
  --bg-input-focus: #FFFFFF;

  /* Controls (outline/ghost buttons, input fields) — a surface DISTINCT from both the page bg and
     white cards: white fill + a clearly visible border, so controls read as controls instead of
     "framed text" that melts into the background. */
  --control-bg: #FFFFFF;
  --control-border: #D1D5DB;

  /* Code */
  --code-bg: #F3F4F6;

  /* Semantic tints (chip/banner backgrounds + readable text/border on them).
     The accent semantic colors (--success/--warn/--danger/--info) keep their
     meaning across themes; only these tint surfaces flip. */
  --success-bg: #D1FAE5; --success-fg: #047857; --success-border: #A7F3D0;
  --warn-bg: #FEF3C7;    --warn-fg: #B45309;    --warn-border: #FDE68A;
  --danger-bg: #FEE2E2;  --danger-fg: #DC2626;  --danger-border: #FECACA;
  --info-bg: #EFF6FF;    --info-fg: #1D4ED8;    --info-border: #BFDBFE;
  --purple-bg: #F5F3FF;  --purple-fg: #6D28D9;  --purple-border: #DDD6FE;

  /* Shell surfaces */
  --scrollbar-thumb: #D1D5DB;
  --overlay-scrim: rgba(26, 26, 46, 0.4);
  --morsel-bg: linear-gradient(135deg, #FFF1F0, #FFE0DE);

  /* Decorative view gradients (theme-aware) */
  --pf-hero-gradient: linear-gradient(135deg, #FAFAF8 0%, #FFF1F0 40%, #EFF6FF 100%);
  --pf-cta-gradient: linear-gradient(135deg, #FFF1F0, #EFF6FF);
  --pf-callout-gradient: linear-gradient(135deg, #EEF0FF, #F8F0FF);

  /* ── Drift-alias layer ──
     Variables referenced across view CSS but never defined (rendered with no
     fallback = broken). Defined here as aliases of the canonical tokens above,
     so they (a) work now and (b) inherit the dark override automatically — no
     entry needed in [data-theme="dark"]. The --sp-* scale is theme-invariant. */
  --card: var(--bg-card);
  --surface: var(--bg-surface);
  --bg-dim: var(--bg-surface);
  --bg-hover: var(--card-bg-hover);
  --muted: var(--text-dim);
  --error: var(--danger);
  --warning: var(--warn);
  --primary: var(--accent);
  --accent-faint: var(--accent-subtle);
  --accent-soft: var(--accent-subtle);   /* highlight/hover surfaces (was #FFF0EE) */
  --accent-dark: var(--accent-deep);      /* darker coral for button gradients/hovers */
  --border-soft: var(--border);           /* was #f0f0ec */
  --bg2: var(--bg-surface);               /* secondary panel bg (was #f5f5f5) */
  --surface-hover: var(--bg-surface);     /* hover surface (had a stale dark #252540 fallback) */
  --on-inverted: var(--bg); /* readable text on a var(--text) background — inverts opposite to --text */
  --mono: var(--font-mono);
  --sp-1: 4px;
  --sp-2: 8px;
  --sp-3: 12px;
  --sp-4: 16px;
  --sp-5: 20px;
  --sp-6: 24px;
  --sp-8: 32px;
  --sp-12: 48px;

  /* ── Font-size scale (theme-invariant) ──
     A shared type scale so views stop hardcoding font-size:0.7rem / 0.85rem /
     1.1rem inline. Adopt in new code; the raw rem values below are the common
     ones already scattered across the view CSS, so migration is a clean swap. */
  --text-xs: 0.72rem;
  --text-sm: 0.82rem;
  --text-base: 0.92rem;
  --text-lg: 1.1rem;
  --text-xl: 1.35rem;
  /* The sizes the library's sheets use most, at the values they already had (2026-09-24), so a
     theme changes a size here and every part that reads it follows. A size not on this list is
     still a literal in its sheet. */
  --text-2xs: 0.68rem;
  --text-caption: 0.75rem;
  --text-small: 0.8rem;
  --text-note: 0.85rem;
  --text-body-sm: 0.9rem;
  --text-body: 0.95rem;
  --text-md: 1rem;
  --text-md-plus: 1.05rem;
  /* The weights, by name. */
  --weight-regular: 400;
  --weight-medium: 500;
  --weight-semibold: 600;
  --weight-bold: 700;
  --weight-heavy: 800;
}

/* ── Dark Theme Override ──
   Redefines only the color/depth variables that must flip for dark mode.
   Brand identity (coral accent) preserved; accents brightened for dark
   contrast. Text tiers verified WCAG-AA on both --bg and --bg-card.
   Aliases above point at these tokens, so they invert automatically. */
[data-theme="dark"] {
  /* The ink ground stays dark in dark mode: deeper than the page, with the page's light words. */
  --ink-ground: color-mix(in srgb, var(--bg) 55%, #000);
  --on-ink-ground: var(--text);

  /* Background */
  --bg: #14151A;
  --bg-surface: #23262F;
  --bg-elevated: #23262F;

  /* Cards & Surfaces */
  --card-bg: #1C1E26;
  --card-bg-hover: #23262F;
  --card-bg-solid: #1C1E26;
  --card-bg-alt: #23262F;
  --card-border: #33363F;

  /* Accent (coral heart — brightened for dark contrast, brand stays coral) */
  --accent: #FF6F62;
  --accent-bright: #FF7A6E;
  --accent-deep: #E8564A;
  --accent-glow: rgba(255, 111, 98, 0.35);
  --accent-subtle: rgba(255, 111, 98, 0.10);
  --accent-border: rgba(255, 111, 98, 0.30);   /* translucent accent hairline for tag/badge edges */

  /* Text */
  --text: #EDEEF2;
  --text-bright: #FFFFFF;
  --text-dim: #A4A9B6;
  --text-muted: #868C9B;

  /* Borders */
  --border: #33363F;
  --border-subtle: #26282F;
  --border-focus: #FF6F62;

  /* Shadows (deeper/more opaque so elevation reads on dark surfaces) */
  --shadow-sm: 0 1px 2px rgba(0,0,0,0.40);
  --shadow-md: 0 4px 12px rgba(0,0,0,0.50);
  --shadow-lg: 0 8px 24px rgba(0,0,0,0.55);
  --shadow-xl: 0 16px 48px rgba(0,0,0,0.65);
  --shadow-heart: 0 4px 14px rgba(255,111,98,0.40);

  /* Input */
  --bg-input: #1C1E26;
  --bg-input-focus: #23262F;

  /* Controls — slightly lighter than the card surface so they read as raised, with a visible border */
  --control-bg: #262934;
  --control-border: #4A4E5A;

  /* Code */
  --code-bg: #0E0F13;

  /* Semantic tints (translucent so they read as native dark surfaces) */
  --success-bg: rgba(16, 185, 129, 0.16); --success-fg: #6EE7B7; --success-border: rgba(16, 185, 129, 0.32);
  --warn-bg: rgba(245, 158, 11, 0.16);    --warn-fg: #FCD34D;    --warn-border: rgba(245, 158, 11, 0.32);
  --danger-bg: rgba(239, 68, 68, 0.16);   --danger-fg: #FCA5A5;  --danger-border: rgba(239, 68, 68, 0.34);
  --info-bg: rgba(59, 130, 246, 0.16);    --info-fg: #93C5FD;    --info-border: rgba(59, 130, 246, 0.32);
  --purple-bg: rgba(139, 92, 246, 0.18);  --purple-fg: #C4B5FD;  --purple-border: rgba(139, 92, 246, 0.34);

  /* Shell surfaces */
  --scrollbar-thumb: #3A3D47;
  --overlay-scrim: rgba(0, 0, 0, 0.6);
  --morsel-bg: rgba(255, 111, 98, 0.16);

  /* Decorative view gradients — subtle dark tints matching the light intent */
  --pf-hero-gradient: linear-gradient(135deg, #16151A 0%, #201820 45%, #15191F 100%);
  --pf-cta-gradient: linear-gradient(135deg, #231A1E, #1A1F28);
  --pf-callout-gradient: linear-gradient(135deg, #1C1A29, #221A29);
}

/* ── PALETTE BRIDGE — the golden pill's palette picker, finally wired to this shell ──
   The pill sets `data-palette` on <html> and /lib/aimeat-theme.css defines each palette's
   --color-* tokens (light and dark per palette, contrast-audited there). Until this block
   existed, NOTHING on the apex read those tokens: picking Paper or Circuit wrote an attribute
   no rule reacted to, and the person watched nothing happen. spa.html now loads
   /lib/aimeat-theme.css BEFORE this file, and this block maps the shell's own vocabulary onto
   the palette's — so every view built on these tokens follows the choice without knowing
   palettes exist.

   Selector is `html[data-palette]`: the default 'aimeat' palette REMOVES the attribute
   (auth/palette.js), so with no palette chosen this block is inert and the shell renders
   byte-identically to before. Dark mode needs nothing here — aimeat-theme.css flips the
   --color-* values under html[data-palette][data-theme='dark'], and these are references.

   Two deliberate boundaries. --border is re-declared because aimeat-theme.css uses that name
   for a WIDTH (1px) while this shell uses it for a color — this file loads later, so the color
   wins. And the semantic family (success/warn/danger and their tint triplets) is NOT bridged:
   danger stays the same red in every palette, because a warning that changes hue with the
   wallpaper is a warning that trained people to look twice. */
html[data-palette] {
  /* Background tiers: base-100 is the page, base-200 the card (aimeat-theme's convention). */
  --bg: var(--color-base-100);
  --bg-surface: color-mix(in srgb, var(--color-base-300) 26%, var(--color-base-100));
  --bg-elevated: var(--color-base-200);
  --card-bg: var(--color-base-200);
  --card-bg-hover: color-mix(in srgb, var(--color-base-300) 12%, var(--color-base-200));
  --card-bg-solid: var(--color-base-200);
  --card-bg-alt: color-mix(in srgb, var(--color-base-300) 26%, var(--color-base-100));
  --card-border: color-mix(in srgb, var(--color-base-content) 14%, transparent);

  /* Accent: --color-primary is what each palette's swatch advertises as its accent. */
  --accent: var(--color-primary);
  --accent-bright: color-mix(in srgb, var(--color-primary) 82%, var(--color-base-200));
  --accent-deep: color-mix(in srgb, var(--color-primary) 86%, var(--color-base-content));
  --accent-glow: color-mix(in srgb, var(--color-primary) 30%, transparent);
  --accent-subtle: color-mix(in srgb, var(--color-primary) 7%, transparent);
  --accent-border: color-mix(in srgb, var(--color-primary) 24%, transparent);

  /* The love gradient follows the palette's primary/secondary pair. */
  --love1: var(--color-primary);
  --love2: color-mix(in srgb, var(--color-primary) 65%, var(--color-secondary));
  --love3: var(--color-secondary);
  --love4: color-mix(in srgb, var(--color-primary) 65%, var(--color-secondary));
  --love5: var(--color-primary);

  /* Text tiers, mixed toward the page so dimming works on any base. */
  --text: var(--color-base-content);
  --text-bright: var(--color-base-content);
  --text-dim: color-mix(in srgb, var(--color-base-content) 64%, var(--color-base-100));
  --text-muted: color-mix(in srgb, var(--color-base-content) 46%, var(--color-base-100));

  /* Borders as translucent content, so they sit on any surface. */
  --border: color-mix(in srgb, var(--color-base-content) 15%, transparent);
  --border-subtle: color-mix(in srgb, var(--color-base-content) 8%, transparent);
  --border-focus: var(--color-primary);

  /* Inputs and controls. */
  --bg-input: var(--color-base-100);
  --bg-input-focus: var(--color-base-200);
  --control-bg: var(--color-base-200);
  --control-border: color-mix(in srgb, var(--color-base-content) 24%, transparent);
  --code-bg: color-mix(in srgb, var(--color-base-300) 30%, var(--color-base-100));
  --scrollbar-thumb: color-mix(in srgb, var(--color-base-content) 25%, transparent);
  --morsel-bg: linear-gradient(135deg,
    color-mix(in srgb, var(--color-primary) 10%, var(--color-base-200)),
    color-mix(in srgb, var(--color-primary) 20%, var(--color-base-200)));

  /* Decorative gradients, from the same pair the love palette uses. */
  --pf-hero-gradient: linear-gradient(135deg,
    color-mix(in srgb, var(--color-primary) 8%, var(--color-base-100)) 0%,
    color-mix(in srgb, var(--color-secondary) 10%, var(--color-base-100)) 100%);
  --pf-cta-gradient: linear-gradient(135deg,
    color-mix(in srgb, var(--color-primary) 12%, var(--color-base-200)),
    color-mix(in srgb, var(--color-secondary) 10%, var(--color-base-200)));
  --pf-callout-gradient: linear-gradient(135deg,
    color-mix(in srgb, var(--color-secondary) 10%, var(--color-base-200)),
    color-mix(in srgb, var(--color-primary) 10%, var(--color-base-200)));
}

/* --border needs one more rung. Each palette's DARK block in aimeat-theme.css re-declares it as a
   width at html[data-palette][data-theme='dark'] specificity (0,2,1), which outranks the bridge
   above (0,1,1) — measured: every dark palette resolved --border to "1px" and every hairline in
   the shell vanished. This matches that specificity, and being in the later file, wins the tie. */
html[data-palette][data-theme] {
  --border: color-mix(in srgb, var(--color-base-content) 15%, transparent);
}

