/*
  facet.css — the whole Facet design system in one readable file.

  Facet is a plain HTML, CSS and JS design library. Any project consumes it
  with one link tag pointed at this file's public URL, plus one script tag
  for facet.js. No build step, no framework, never minified.

  How this file is organised, top to bottom:

    1. TOKENS       every design decision as a CSS variable, named by role,
                    one block per theme
    2. RESET        box sizing, margin reset, sensible element defaults
    3. BASE STYLES  defaults for every raw HTML tag: text, forms, media,
                    tables, focus rings, selection, scrollbars, print
    4. COMPONENTS   one clearly commented section per component

  Theming: one attribute on the <html> tag switches the theme, layout
  containers included. No attribute at all is the Default theme. Dark mode
  is a second attribute and composes with every theme:

    <html>                                 Default, light
    <html data-mode="dark">                Default, dark
    <html data-theme="velvet">             Velvet, light
    <html data-theme="aero" data-mode="dark">   Aero, dark

  The same attributes work on any element, not just <html>: put
  data-mode="dark" on a section and only that subtree flips — that is how
  the library site shows light and dark side by side. Tokens are inherited
  custom properties, so the nearest element carrying the attribute wins.

  Every color below is a semantic token (--background, --surface, --text...),
  so a theme swaps values in one block and the entire page restyles itself.

  Rules that hold everywhere in this file:
    - Tokens are named by role, not value: --surface, never --gray-100.
    - Components use tokens only. No raw hex or magic px below the token layer.
    - One component, one section. Nothing is scattered.
    - Every component section opens with its canonical description — what it
      is, what it is for, when to use it — the same text, word for word, as
      its entry on the library site and in llms.txt.
    - prefers-reduced-motion is honoured: all motion collapses to none.

  To-dos are marked "TODO" inline where they sit.
*/

/* (docs mirror — identical to llms.txt · Fonts)
   WHEN TO USE: never pick a font in a component — ask for a role. The
   core seven, one per job: --font-heading (calligraphic serif),
   --font-body (rounded sans, the interface voice), --font-mono
   (technical monospace — also the eyebrow voice at --weight-eyebrow,
   always caps), --font-quote (editorial serif, italic, for quotes and
   callouts), --font-reading (editorial serif, upright, for long-form
   article text), --font-numeric (display serif for big standalone
   numbers), --font-hand (a handwritten pen for margin notes and rough
   work, never body text). The extended set for special jobs:
   --font-hero (loud geometric display for one-word heroes and posters),
   --font-signature (formal cursive for sign-off lines and certificates),
   --font-typewriter (the typed-document voice for memos and drafts),
   --font-receipt (dot-matrix for receipts, tickets and counters),
   --font-barcode (renders text as a real scannable Code 128 barcode),
   --font-pixel (bitmap for retro screens and badges), --font-children
   (round and friendly for kids' products), and the script pairings
   --font-japanese, --font-chinese, --font-korean, --font-devanagari for
   translated content (Cyrillic ships inside the core faces, so Russian
   needs no extra family). The Default theme loads them all with one
   @import at the top of facet.css, served in unicode-range subsets — a
   page that never uses a face downloads none of its glyphs. The one
   exception is the hero face: it is not on Google Fonts, so facet.css
   self-hosts it from the library's own /fonts folder via @font-face —
   still nothing for a consumer to add. Every stack
   ends in a system fallback; a theme re-voices the library by swapping
   faces; nothing downstream changes. */
/* ----- Web fonts -----
   The faces, each loaded for one job (see --font-* in the token layer):
   Cormorant for calligraphic headings, Nunito Sans for friendly body text,
   JetBrains Mono for technical captions, code and the eyebrow voice,
   Newsreader for quotes, callouts and long-form reading, Playfair Display
   for big standalone numbers, Caveat for hand annotations — plus the
   extended set: Gadey (hero display — the one self-hosted face, served
   from this library's own /fonts folder because it is not on Google
   Fonts), Great Vibes (signature),
   Special Elite (typewriter), Doto (receipt), Libre Barcode 128 Text
   (real scannable barcodes), Pixelify Sans (pixel), Chewy (children), and
   the calligraphic script pairings Yuji Syuku (Japanese brush), Ma Shan
   Zheng (Chinese regular-script brush), Nanum Brush Script (Korean brush)
   and Tiro Devanagari Hindi (traditional, elegant). Google serves every
   family in unicode-range subsets, so a page that never uses a face
   downloads none of its glyphs — only these rules. Cyrillic ships inside
   the core faces already, so Russian needs no extra family. Loaded from Google
   Fonts through the stylesheet, so no extra tag in anyone's HTML and it works
   with JavaScript off. Each font stack still ends in a system fallback, so
   text renders instantly and survives the font failing to load. display=swap
   shows fallback text first, then swaps — never a blank flash of text.
   @import must stay at the very top, before any style rule. */
@import url('https://fonts.googleapis.com/css2?family=Caveat:wght@400;600&family=Chewy&family=Cormorant:wght@500;600;700&family=Doto:wght@700&family=Great+Vibes&family=JetBrains+Mono:wght@400;500;800&family=Libre+Barcode+128+Text&family=Ma+Shan+Zheng&family=Nanum+Brush+Script&family=Newsreader:ital,wght@0,400;0,600;1,400;1,500&family=Nunito+Sans:ital,wght@0,400;0,500;0,600;0,700;1,400&family=Pixelify+Sans:wght@400;700&family=Playfair+Display:wght@500;600;700&family=Special+Elite&family=Tiro+Devanagari+Hindi&family=Yuji+Syuku&display=swap');

/* Gadey — the hero display face, self-hosted (not on Google Fonts).
   The URL is relative to THIS stylesheet, so consumers pulling
   facet.css by URL load the font from the library's own domain;
   vercel.json sends the CORS header cross-origin fonts require.
   The unicode-range EXCLUDES the digits 0-9 on purpose: this copy is
   the demo cut, whose digit glyphs are watermarks — excluded, digits
   fall through to the stack's fallback and render honestly. When the
   full license lands, swap the file and delete the unicode-range. */
@font-face {
  font-family: "Gadey";
  src: url("../fonts/Gadey.woff2") format("woff2");
  font-display: swap;
  unicode-range: U+0000-002F, U+003A-10FFFF;
}


/* ==========================================================================
   1. TOKENS
   Layer 1 of the library. Everything downstream reads these variables.
   ========================================================================== */

/*
   Theme · Default (light). Ships first, no attribute needed.
   A soft off-white page with white surfaces that lift above it; softened
   ink (not pure black); a cool-tuned neutral ramp of semantic roles
   (surface / surface-hover / surface-active, three border weights, three
   text weights) so components pick the exact neutral they need; layered,
   whisper-light shadows; calm radii. Zero decoration, the neutral base.
   The accent ranks are the ink itself: accent-1 primary actions are the
   softened ink deepening to full black, accent-2 is a light-gray fill, and
   accent-3 is the interactive ink for links and small affordances.

   The non-color tokens (type, spacing, shape, elevation, motion, focus)
   live here once and are shared by every theme; themes override only what
   they change. [data-mode="light"] shares this block so a light subtree
   can sit inside a dark page.
*/
:root,
[data-mode="light"] {

  /* (docs mirror — identical to llms.txt · Color)
     WHEN TO USE: components and pages never pick raw colors — they pick
     roles and ranks. The neutral roles: --background (the page),
     --surface (cards and inputs, one step above), --surface-hover /
     --surface-active (interactive fills), --border-subtle / --border /
     --border-strong (three line weights), --text / --text-muted /
     --text-subtle (three text weights). The accent ranks, each with
     -hover, -pressed and an on-color: --accent-1 is the ONE primary action
     per screen, --accent-2 fills every secondary action, --accent-3 is the
     quiet interactive color (links, labels, focus). Status colors say what
     happened: --success, --warning, --error, --info — each with a derived
     -tint and -edge pair for washes and borders. The vivid palette
     (--color-1 red … --color-5 purple) is decoration only — card tints,
     charts, playful accents — never semantic status. The raw vivids are
     fills-only (yellow on white fails contrast); when the palette must
     WRITE — a tinted label, a stat, a link on a washed card — use the
     -text companion (--color-1-text … --color-5-text): the same hue
     carried to AA contrast on the page, darkened per color in light mode
     and lifted toward white in dark. Dark mode and every
     theme re-map all of it; your markup does not change. */
  /* ----- Colors: the neutral ramp -----
     Semantic roles, never numbers. One committed temperature (a faint cool
     cast). The page is a soft off-white; surfaces (cards, inputs) are white
     and LIFT above it (the charter's "one step above"); the two interactive
     fills, three border weights and three text weights give every component
     the exact neutral it needs instead of overloading one gray. */
  --background: #FBFCFD;      /* the page itself: cool off-white (Radix slate-1) */
  --surface: #FFFFFF;         /* one step above the page: cards, inputs, menus */
  --surface-hover: #F8F9FA;   /* interactive fill, rest→hover (slate-2) */
  --surface-active: #F1F3F5;  /* interactive fill, pressed (slate-3) */
  --border-subtle: #ECEEF0;   /* the faintest line: dividers, row separators (slate-4) */
  --border: #DFE3E6;          /* the outline of an element: cards, inputs, chips (slate-6) */
  --border-strong: #C1C8CD;   /* emphasis: hovered inputs, selected outlines (slate-8) */
  --text: #11181C;            /* primary ink, cool near-black (slate-12) */
  --text-muted: #687076;      /* secondary text, hints, captions — AA on surface (slate-11) */
  --text-subtle: #889199;     /* tertiary: placeholders, disabled, the non-essential */

  /* ----- Colors: the accent ranks ----- */
  /* Components never pick colors; they pick ranks. Accent-1 is the one
     primary action on a screen. Accent-2 is every secondary action's
     fill. Accent-3 is the quiet interactive color: links, labels, focus,
     small affordances. Each rank carries hover, pressed and an on-color
     for text sitting on it. In Default, accent-1 and accent-3 are the
     ink itself; accent-2 is the surface family. */
  --accent-1: #11181C;        /* the ink is the accent — a Vercel-black primary */
  --accent-1-hover: #0A0F12;
  --accent-1-pressed: #05080A;
  --on-accent-1: #FFFFFF;

  --accent-2: #F1F3F5;        /* a real light-gray fill, distinct from the white surface (slate-3) */
  --accent-2-hover: #ECEEF0;
  --accent-2-pressed: #DFE3E6;
  --on-accent-2: #11181C;

  --accent-3: #11181C;        /* the quiet interactive rank — the ink */
  --accent-3-hover: #0A0F12;
  --accent-3-pressed: #05080A;
  --on-accent-3: #FFFFFF;

  /* The OS accent, as its own opt-in color — dormant, used by nothing until
     you reach for it (var(--os-accent) on a link, a highlight, whatever).
     Defaults to the iOS system blue everywhere; where the browser exposes
     the real OS accent (see the @supports block below) it becomes that. So
     it is blue for everyone by default, and the user's own accent only for
     the few who set a custom one on a supporting browser. */
  --os-accent: #0A84FF;
  --os-accent-hover: color-mix(in srgb, #0A84FF 86%, black);
  --os-accent-pressed: color-mix(in srgb, #0A84FF 74%, black);
  --on-os-accent: #FFFFFF;

  /* ----- Colors: status ----- */
  --success: #187C3D;         /* things that went well */
  --warning: #8A5A00;         /* needs attention, not danger */
  --error: #B3261E;           /* something failed */
  --info: #295FA6;            /* neutral notices */

  /* Each status also ships its derived pair — a soft fill and a border —
     mixed once here from the base status color, so badges, fields, cards
     and toasts all tint the same way and every theme (which only re-sets
     the four bases above) gets the pairs for free. Components read the
     pair; they never hand-roll their own color-mix recipe. */
  --success-tint: color-mix(in srgb, var(--success) 14%, var(--background));
  --success-edge: color-mix(in srgb, var(--success) 38%, var(--background));
  --warning-tint: color-mix(in srgb, var(--warning) 14%, var(--background));
  --warning-edge: color-mix(in srgb, var(--warning) 38%, var(--background));
  --error-tint: color-mix(in srgb, var(--error) 14%, var(--background));
  --error-edge: color-mix(in srgb, var(--error) 38%, var(--background));
  --info-tint: color-mix(in srgb, var(--info) 14%, var(--background));
  --info-edge: color-mix(in srgb, var(--info) 38%, var(--background));

  /* The loading skin's placeholder ink: a shade just darker than the
     page in light mode, just lighter in dark — derived, so every theme
     and both modes get it with no extra tuning. */
  --skeleton: color-mix(in srgb, var(--text) 9%, var(--background));

  /* ----- Colors: the vivid palette -----
     Five bright decorative colors named by rank, like the accent ranks.
     Decoration only — card tints (mixed into the surface), charts,
     playful accents; never semantic status (the status tokens above carry
     meaning, these carry personality). A theme may re-tune them;
     components mix them into surfaces, they never paint text with them. */
  --color-1: #FF3B30;         /* red */
  --color-2: #FFCC00;         /* yellow */
  --color-3: #34C759;         /* green */
  --color-4: #007AFF;         /* blue */
  --color-5: #AF52DE;         /* purple */

  /* Each vivid color's -text companion: the same hue carried down to AA
     contrast (≥4.5) on the page and on surfaces, for the moments the
     palette must WRITE — a tinted label, a stat, a link on a washed card.
     The raw colors above stay fills-only (yellow on white fails hard).
     Derived per color (yellow needs far more darkening than red), so a
     theme that re-tunes the palette keeps the pairing; dark mode
     re-derives toward white below. */
  --color-1-text: color-mix(in srgb, var(--color-1) 78%, black);
  --color-2-text: color-mix(in srgb, var(--color-2) 54%, black);
  --color-3-text: color-mix(in srgb, var(--color-3) 58%, black);
  --color-4-text: color-mix(in srgb, var(--color-4) 84%, black);
  --color-5-text: color-mix(in srgb, var(--color-5) 82%, black);

  /* (docs mirror — identical to llms.txt · Typography)
     WHEN TO USE: you rarely set type directly — write semantic HTML
     (h1–h4, p, small, blockquote) and the base layer sets everything. Reach
     for the tokens only when building a component or a special piece (a
     stat, a hero) that needs a size, weight or rhythm by hand; reach for
     the .text-* classes to put a role's look on any element.

     Sizes by role: --text-display, --text-h1 … --text-h4, --text-body,
     --text-quote, --text-footnote, --text-caption. Weights:
     --weight-body, --weight-medium (UI labels, nav, buttons),
     --weight-strong, --weight-heading, --weight-eyebrow (the mono
     micro-label voice). Line height per role:
     --leading-display/-heading/-snug/-body/-ui — tighter as type grows.
     Tracking per role: --tracking-display/-heading/-snug/-body/-label/
     -caps/-wide — large type tightens, caps micro-labels open up, -wide is
     for spaced overlines. Reading measure: --measure, --measure-narrow.
     Prose rhythm (em-based margins that give raw HTML its flow):
     --flow-heading-above/-below, --flow-paragraph, --flow-tight; gap
     layouts (.stack, .row) zero these so rhythm and gap never stack.
     Global type size: data-text-size="small|medium|large" on html — set by
     the theme, independent of density. Text-role classes (.text-display …
     .text-caption) put the scale on any element. The eyebrow (.eyebrow) is
     the small thick spaced-caps label above a heading or a menu group — the
     mono face (--font-mono) at --text-caption, --weight-eyebrow (800) and
     --tracking-wide, accent-3 ink, ALWAYS caps (the class uppercases
     whatever case the author wrote). The same voice marks the library's own
     group names and menu labels.

         <p class="eyebrow">One tap away</p>
         <h2>Install this app</h2> */
  /* ----- Typography: font stacks, one per job -----
     Seven roles, each a face chosen for what it does, every stack
     ending in a system fallback (never `inherit` — that voids the whole
     declaration) so text renders instantly and survives a font failing to
     load. A theme re-voices the library by swapping these. */
  --font-heading: "Cormorant", Georgia, "Times New Roman", serif;         /* calligraphic display serif for titles */
  --font-body: "Nunito Sans", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; /* friendly, round, readable */
  --font-mono: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;   /* technical captions and code */
  --font-quote: "Newsreader", Georgia, "Times New Roman", serif;          /* quotes and callouts, set apart */
  --font-reading: "Newsreader", Georgia, "Times New Roman", serif;        /* long-form article text: the editorial reading voice, upright */
  --font-hand: "Caveat", "Segoe Print", "Bradley Hand", cursive;           /* hand annotations: margin notes, rough work, a human pen on the interface */
  /* The extended faces: display, paper, play and scripts. Same rule as the
     core seven — components ask for the role, never the font. */
  --font-hero: "Gadey", "Arial Black", sans-serif;                         /* the loud one: one-word heroes, posters, launch screens */
  --font-signature: "Great Vibes", "Snell Roundhand", "Apple Chancery", cursive; /* formal cursive: sign-off lines, letters, certificates */
  --font-typewriter: "Special Elite", "American Typewriter", "Courier New", monospace; /* the typed-document voice: memos, drafts, screenplays */
  --font-receipt: "Doto", "Courier New", monospace;                        /* dot-matrix printer: receipts, tickets, counters */
  --font-barcode: "Libre Barcode 128 Text", monospace;                     /* renders text as a real scannable Code 128 barcode */
  --font-pixel: "Pixelify Sans", "Courier New", monospace;                 /* pixel bitmap, fine grid: retro screens, badges, easter eggs */
  --font-children: "Chewy", "Comic Sans MS", sans-serif;                   /* properly playful: kids' products, stickers, celebration moments */
  --font-japanese: "Yuji Syuku", "Hiragino Mincho ProN", "Yu Mincho", serif;               /* Japanese: traditional brush calligraphy, formal */
  --font-chinese: "Ma Shan Zheng", "Kaiti SC", "KaiTi", serif;                             /* Chinese: regular-script brush, traditional and formal */
  --font-korean: "Nanum Brush Script", "Apple SD Gothic Neo", sans-serif;                  /* Korean: brush calligraphy, traditional */
  --font-devanagari: "Tiro Devanagari Hindi", "Kohinoor Devanagari", serif; /* Devanagari: traditional book hand, elegant */
  --font-numeric: "Playfair Display", Georgia, "Times New Roman", serif;  /* big standalone numbers */

  /* ----- Typography: type scale, named by role, scaled by size -----
     --text-scale (data-text-size small/medium/large) shifts type globally;
     medium is the default. A theme may override any of these. */
  --text-scale: 1;
  --text-display:  calc(clamp(2.5rem, 6vw, 3.5rem) * var(--text-scale));   /* hero numbers and statements */
  --text-h1:       calc(clamp(1.875rem, 4vw, 2.25rem) * var(--text-scale));
  --text-h2:       calc(1.5rem   * var(--text-scale));
  --text-h3:       calc(1.25rem  * var(--text-scale));
  --text-h4:       calc(1.125rem * var(--text-scale));
  --text-body:     calc(1rem     * var(--text-scale));
  --text-quote:    calc(1.125rem * var(--text-scale));   /* pull-quotes and lead-ins */
  --text-footnote: calc(0.875rem * var(--text-scale));   /* the fine print (was --text-footnote) */
  --text-caption:  calc(0.75rem  * var(--text-scale));   /* labels, badges, meta */

  /* ----- Typography: weights -----
     Four steps, named by role. Medium is the UI weight: nav, buttons,
     labels, table headers — the text that must read as deliberate without
     shouting. A theme swaps these to re-voice every heading and control. */
  --weight-body: 400;         /* running text */
  --weight-medium: 500;       /* UI labels, nav, buttons, table headers */
  --weight-strong: 600;       /* <strong>, emphasis inside body */
  --weight-heading: 600;      /* headings; suits Cormorant's display cut, falls back to semibold */
  --weight-eyebrow: 800;      /* the eyebrow voice: micro-labels in the mono face, thick and technical */

  /* ----- Typography: line height, named by role -----
     Tighter as type gets larger — big display text needs less leading, body
     text needs more to stay readable. One value per role so a theme sets the
     whole vertical feel at once. */
  --leading-display: 1.05;    /* hero statements and big numbers */
  --leading-heading: 1.15;    /* h1, h2 */
  --leading-snug: 1.3;        /* h3–h6, lead paragraphs, dense headings */
  --leading-body: 1.6;        /* running prose */
  --leading-ui: 1.4;          /* buttons, table cells, tight controls */

  /* ----- Typography: tracking (letter-spacing), named by role -----
     The elegant signature: large type tightens (negative), small caps-y
     labels open up (positive). Scales with size because the values are in em.
     A theme retunes the whole voice by shifting these. */
  --tracking-display: -0.03em;  /* hero and h1 */
  --tracking-heading: -0.02em;  /* h2 */
  --tracking-snug: -0.01em;     /* h3–h6 */
  --tracking-body: 0em;         /* running text */
  --tracking-label: 0.02em;     /* buttons, nav, field labels */
  --tracking-caps: 0.06em;      /* uppercase micro-labels: table headers, menu labels, meta */
  --tracking-wide: 0.14em;      /* spaced-out overlines: eyebrows, kickers, hero labels */

  /* ----- Typography: reading measure -----
     Max line length for comfortable reading, in ch (character widths) so it
     tracks the font. Prose caps at --measure; lead paragraphs and card copy
     use the narrower one. */
  --measure: 66ch;            /* body prose */
  --measure-narrow: 54ch;     /* lead-ins, card and callout copy */

  /* ----- Typography: prose rhythm (vertical margins) -----
     The space above and below headings and between blocks, in em so it
     scales with each element's own size (a big heading gets a big break).
     A heading sits close to the content it introduces (small below) and
     apart from what came before (larger above). Layout primitives that use
     gap (.stack, .row) zero these out, so rhythm governs raw prose while gap
     governs app layout — never both at once. */
  --flow-heading-above: 1.5em;  /* group break before a heading */
  --flow-heading-below: 0.5em;  /* heading hugs the content it introduces */
  --flow-paragraph: 1em;        /* between paragraphs, lists, quotes, tables */
  --flow-tight: 0.5em;          /* between closely related lines */

  /* (docs mirror — identical to llms.txt · Spacing)
     WHEN TO USE: name the intent, never a number. --space-tight (bound
     pairs, 4px), --space-control (inside buttons and inputs, 8px),
     --space-inline (gaps in a row, 12px), --space-stack (default block
     gap, 16px), --space-card (card padding, 24px), --space-page (page
     margins, 32px), --space-section (between page sections, 48px),
     --space-hero (the biggest breaks, 64px). One global control —
     data-density="small|medium|large" — tightens or opens the whole system
     at once, and the theme sets it; never pick a theme and its spacing
     separately. Container widths: --width-narrow 40rem (prose, forms),
     --width-default 64rem (most pages), --width-wide 80rem (dashboards).
     Safe areas: --safe-top / --safe-bottom carry the notch and home-bar
     insets; every full-viewport surface pads with them, and .bleed-top
     opts a subtree out deliberately. */
  /* ----- Spacing: named by intent, scaled by density -----
     Pick spacing by role, never by a bare number. Each token says where it
     belongs. --density-scale (data-density small/medium/large) shifts the
     whole system at once; medium is the default. A theme may override any
     of these to set its own rhythm. */
  --density-scale: 1;
  --space-tight:   calc(0.25rem * var(--density-scale));  /*  4px · bound pairs: a label and its field */
  --space-control: calc(0.5rem  * var(--density-scale));  /*  8px · padding inside buttons, inputs, chips */
  --space-inline:  calc(0.75rem * var(--density-scale));  /* 12px · gaps between items in a row */
  --space-stack:   calc(1rem    * var(--density-scale));  /* 16px · default gap between stacked blocks */
  --space-card:    calc(1.5rem  * var(--density-scale));  /* 24px · padding inside cards and panels */
  --space-page:    calc(2rem    * var(--density-scale));  /* 32px · container side padding, page margins */
  --space-section: calc(3rem    * var(--density-scale));  /* 48px · between whole page sections */
  --space-hero:    calc(4rem    * var(--density-scale));  /* 64px · the biggest breaks: hero bands */
  --space-pin:     20px;   /* the inset a viewport-pinned panel keeps from the screen edges — exact pixels, never density-scaled, so pinned panels fit exactly and never move with the scroll */

  /* ----- Spacing: the safe areas -----
     The device's own intrusions — Dynamic Island / notch above, home bar
     below — as role tokens. Every full-viewport surface the library owns
     pads with these, so content clears the hardware by DEFAULT (with a
     black-translucent status bar the page runs under the island, so
     respecting the inset must be the default and bleeding the choice).
     Opt out per subtree with .bleed-top below — for full-bleed maps and
     immersive pages that WANT to run up under the island. */
  --safe-top: env(safe-area-inset-top, 0px);
  --safe-bottom: env(safe-area-inset-bottom, 0px);

  /* ----- Spacing: container widths ----- */
  --width-narrow: 40rem;      /* prose, forms, calculators */
  --width-default: 64rem;     /* most pages — the safe centered standard */
  --width-wide: 80rem;        /* dashboards, galleries */
  --width-xwide: 96rem;       /* the super-wide tier, at home on ultrawide screens */

  /* (docs mirror — identical to llms.txt · Shape and elevation)
     WHEN TO USE: radii by what they round — --radius-control (buttons,
     inputs, chips), --radius-panel (fields, mid panels), --radius-card
     (cards, modals, hero), --radius-pill (pills, sliders, avatars).
     Borders by role: --border-hairline (faint separators),
     --border-element (an element's outline), --border-focus (the focus
     ring), --border-highlight (active bars, selected emphasis). Elevation:
     --shadow-flat, --shadow-raised, --shadow-floating, --shadow-overlay —
     whisper-light layered shadows; themes may trade shadow for carving
     (Elegant) or bevels (Velvet), and your markup does not change. */
  /* ----- Shape: radii named for what they round ----- */
  --radius-control: 8px;      /* buttons, inputs, chips — the small controls */
  --radius-panel: 12px;       /* fields, result blocks, mid-size panels */
  --radius-card: 16px;        /* cards, modals, hero — the large surfaces */
  --radius-pill: 999px;       /* pills, sliders, avatars */
  /* Borders named by role. Hairline and element are both 1px — the name
     says the job: a faint separator vs the outline of an element. Focus and
     highlight are the 2px emphasis pair: the focus ring vs drawing attention
     to an element (active bars, selected rows). */
  --border-hairline: 1px;     /* a faint separator: dividers, rules, row lines */
  --border-element: 1px;      /* the outline of an element: cards, inputs, chips */
  --border-focus: 2px;        /* the focus ring width — focus only */
  --border-highlight: 2px;    /* draw attention to an element: active bars, selected */

  /* ----- Elevation: layered, whisper-light shadows -----
     Two stops each — a tight contact shadow plus a soft ambient one, tinted
     with the cool ink rather than pure black — so a surface reads as lifted,
     not outlined. The higher the element, the softer and larger the spread. */
  --shadow-flat: none;
  /* Whisper-light, layered, tinted with a cool navy (16 24 40) rather than
     pure black, so a surface reads as lifted, not outlined — the premium cast
     shared by Radix, Stripe and Untitled UI. A tight contact shadow plus a
     soft ambient one; the higher the element, the softer and larger. */
  --shadow-raised:
    0 1px 2px rgb(16 24 40 / 0.04),
    0 1px 3px rgb(16 24 40 / 0.06);
  --shadow-floating:
    0 2px 4px rgb(16 24 40 / 0.05),
    0 4px 8px rgb(16 24 40 / 0.06);
  --shadow-overlay:
    0 8px 16px rgb(16 24 40 / 0.06),
    0 16px 32px rgb(16 24 40 / 0.08);

  /* (docs mirror — identical to llms.txt · Motion)
     WHEN TO USE: never hardcode a duration or an easing — every transition
     in the library and in your product reads the motion tokens, so one
     attribute can calm or stop everything. Lively is the default: momentum,
     weight, a settle in every arrival — --duration-fast 150ms,
     --duration-normal 300ms, --duration-slow 600ms, --ease-out,
     --ease-in-out, --ease-spring (a true spring curve where supported).
     data-motion="calm" makes everything short and near-linear;
     data-motion="off" stops animation entirely; both work on any subtree
     and through facet.set({motion}). prefers-reduced-motion collapses all
     library motion to nothing, above any page choice. Focus:
     --focus-ring, --focus-offset — one visible ring, keyboard only. */
  /* ----- Motion: timing and easing ----- */
  /* Animated, the default: momentum, weight, a settle in every arrival.
     Color changes use --ease-out; anything that moves or scales uses
     --ease-spring, which overshoots slightly and settles. Swap the whole
     motion behaviour with data-motion="calm" (subdued, near-linear) or stop it
     with data-motion="off" — one attribute, page-wide or per subtree.
     prefers-reduced-motion sits above all of it. */
  --duration-fast: 150ms;     /* hovers, color changes */
  --duration-normal: 300ms;   /* presses, reveals, toggles */
  --duration-slow: 600ms;     /* overlays, page-level moves — deliberately unhurried */
  --ease-out: cubic-bezier(0.2, 0, 0, 1);       /* things arriving */
  --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);  /* things moving */
  --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);  /* bouncy back-out;
                                upgraded to a true spring curve below */

  /* ----- Focus ----- */
  --focus-ring: var(--border-focus) solid var(--accent-3);
  --focus-offset: 2px;
}

/* The safe-area opt-out: put .bleed-top on any surface (a view, a snap
   section, a fixed overlay) whose content should run up UNDER the
   Dynamic Island / notch — full-bleed maps, immersive pages. Custom
   properties inherit, so the whole subtree bleeds with it. Respecting
   the inset stays the default everywhere; bleeding is the deliberate
   choice. */
.bleed-top { --safe-top: 0px; }

/*
   Theme · Default (dark). <html data-mode="dark">.
   Near-black ground, paper ink. The ranks flip with the ink: accent-1
   primary actions become paper on black. Only color tokens change; type,
   spacing, shape and motion carry over.
*/
[data-mode="dark"] {
  --background: #0F1011;      /* the page: near-black, never pure #000 (Linear) */
  --surface: #141516;        /* one step above: cards lift out of the ground */
  --surface-hover: #18191A;  /* interactive fill, rest→hover */
  --surface-active: #202425; /* interactive fill, pressed */
  --border-subtle: #202123;  /* the faintest line */
  --border: #2C2E31;         /* element outlines */
  --border-strong: #4C5155;  /* emphasis */
  --text: #ECEDEE;           /* paper ink, softened off pure white */
  --text-muted: #889096;     /* secondary */
  --text-subtle: #63676B;    /* tertiary */

  --accent-1: #ECEDEE;
  --accent-1-hover: #FFFFFF;
  --accent-1-pressed: #D4D6D8;
  --on-accent-1: #11181C;

  --accent-2: #202425;       /* a raised gray fill, distinct from the surface */
  --accent-2-hover: #262B2C;
  --accent-2-pressed: #2E3335;
  --on-accent-2: #ECEDEE;

  --accent-3: #ECEDEE;
  --accent-3-hover: #FFFFFF;
  --accent-3-pressed: #D4D6D8;
  --on-accent-3: #11181C;

  /* The vivid -text companions flip: on a near-black page the hue is
     carried UP toward white instead (the raw brights mostly pass here;
     one uniform lift puts all five comfortably past AA). Set on
     [data-mode="dark"] so every theme's dark twin inherits them. */
  --color-1-text: color-mix(in srgb, var(--color-1) 88%, white);
  --color-2-text: color-mix(in srgb, var(--color-2) 88%, white);
  --color-3-text: color-mix(in srgb, var(--color-3) 88%, white);
  --color-4-text: color-mix(in srgb, var(--color-4) 88%, white);
  --color-5-text: color-mix(in srgb, var(--color-5) 88%, white);

  --success: #56B87A;
  --warning: #D8A23E;
  --error: #E5766A;
  --info: #7AA7DC;

  /* Dark: shadows barely register, so hierarchy leans on borders plus a 1px
     inset top highlight — the "light catches the top edge" trick from Linear
     and Vercel — with just a soft cast underneath. */
  --shadow-raised:
    inset 0 1px 0 rgb(255 255 255 / 0.04),
    0 1px 2px rgb(0 0 0 / 0.40);
  --shadow-floating:
    inset 0 1px 0 rgb(255 255 255 / 0.05),
    0 4px 12px rgb(0 0 0 / 0.50);
  --shadow-overlay:
    inset 0 1px 0 rgb(255 255 255 / 0.05),
    0 16px 40px rgb(0 0 0 / 0.60);
}

/*
   Theme · Velvet (light). <html data-theme="velvet">.
   A CRED-inspired neumorphic material, extracted from the shipped
   inflation app (hand-tuned in the app's velvet-tuner and user-approved
   — trust these values over taste). Matte velvet
   surfaces, ONE light source from straight above: elements are either
   RAISED out of the fabric (face gradient + key light + cast shadow +
   bevel) or CARVED into it (wells and pits with interior shadows). Gold
   is the only accent ink.

   The behavioral laws that ARE this theme (full text ships with the
   library docs): one raised face per screen — the primary CTA carries the
   accent; press feedback — the dent is a fading overlay, the face
   gradient never changes on press; raised text rides up and dips while
   pressed; icons engrave like text; state changes ease, never jump;
   elements carrying velvet lift/press transforms must NOT register with
   parallax modules (inline transforms collide); never put `inherit`
   inside a font list.

   Standard tokens are mapped so every Facet component works here:
   accent-1 is the gold, accent-2 the raised gray face family, accent-3
   the gold ink. The --v-* material tokens are kept verbatim from the
   reference so recipes diff cleanly against the app.
*/
[data-theme="velvet"] {
  --background: #e4e4e9;
  --surface: #d6d6db;         /* the well: carved things sit on this */
  --text: #56565C;            /* deepened: AA even on the carved wells (4.13 before) */
  --text-muted: #5c5c61;      /* one step softer, still AA on the wells */
  --border: #cfcfd4;

  --accent-1: #8a6c26;        /* the gold */
  --accent-1-hover: #7a5f20;
  --accent-1-pressed: #6b531c;
  --on-accent-1: #FFFFFF;

  --accent-2: #e6e6ea;        /* the raised face family */
  --accent-2-hover: #dfdfe3;
  --accent-2-pressed: #d6d6db;
  --on-accent-2: #5c5c61;     /* the button ink: AA on the whole face gradient */

  --accent-3: #6B531C;        /* gold ink deepened: AA as link text on the wells */
  --accent-3-hover: #5F4A19;
  --accent-3-pressed: #544117;
  --on-accent-3: #FFFFFF;

  /* Status inks deepened too: the wells (--surface) are darker than
     the fabric, and status words must stay AA inside them. */
  --success: #0F5A30;
  --warning: #664300;
  --error: #9A2019;
  --info: #1B457A;

  /* Two type styles: a serif display for statements, the
     rounded stack for controls and micro labels. */
  --font-heading: "Baskerville", "Palatino", "New York", ui-serif, Georgia, serif;
  --weight-heading: 400;

  /* ---- VELVET value block · light (verbatim from the reference) ---- */
  --v-bg: #e4e4e9;
  --v-well-bg: #d6d6db;
  --v-face:
    radial-gradient(120% 90% at 50% 0%, rgba(255,255,255,0.45), transparent 68%),
    linear-gradient(180deg, #e6e6ea, #d6d6db);
  --v-face-accent:
    radial-gradient(120% 90% at 50% 0%, rgba(255,255,255,0.6), transparent 68%),
    linear-gradient(180deg, #ecd8b2, #d1bb91);
  --v-ink: #5c5c61;         /* deepened from the reference's #8c8c90: control labels must be AA on the face; the emboss shadow keeps the soft look */
  --v-ink-strong: #56565C;
  --v-ink-accent: #5C4A18;
  --v-gold: #8a6c26;
  --v-weight: 750;
  --v-text-shadow: 0 1px 1px rgba(255,255,255,0.5), 0 -1px 1px rgba(0,0,0,0.1);
  --v-icon-filter: drop-shadow(0 1px 1px rgba(255,255,255,0.65)) drop-shadow(0 -1px 1px rgba(0,0,0,0.13));
  --v-key: 0 -7px 16px rgba(255,255,255,0.9);
  --v-cast: 0 12px 28px rgba(0,0,0,0.15);
  --v-bevel:
    inset 0 1.5px 2.5px rgba(255,255,255,0.95),
    inset 0 -1.5px 2.5px rgba(255,255,255,0.35),
    inset 0 -3px 5px rgba(0,0,0,0.12);
  --v-raised: var(--v-key), var(--v-cast), var(--v-bevel);
  --v-raised-pressed: 0 -5.95px 13.6px rgba(255,255,255,0.9), 0 10.2px 23.8px rgba(0,0,0,0.15), var(--v-bevel);
  --v-pill: var(--v-cast), var(--v-bevel);   /* inside a well: no key light */
  --v-raised-2nd: 0 -3.15px 7.2px rgba(255,255,255,0.9), 0 5.4px 12.6px rgba(0,0,0,0.15), var(--v-bevel);
  --v-well-shadow:
    var(--v-key), var(--v-cast),
    inset 0 3px 6px rgba(255,255,255,0.5),
    inset 0 2.8px 7px rgba(0,0,0,0.22),
    inset 0 -2.8px 7px rgba(255,255,255,0.9);
  --v-pit:
    inset 0 2.8px 7px rgba(0,0,0,0.22),
    inset 0 -2.8px 7px rgba(255,255,255,0.9);
  --v-dent-btn: radial-gradient(72% 62% at 50% 58%, rgba(0,0,0,0.056), transparent 78%);
  --v-dent-tap: radial-gradient(72% 62% at 50% 58%, rgba(0,0,0,0.14), transparent 78%);
  --v-div-line: linear-gradient(180deg, transparent, rgba(0,0,0,0.18) 25%, rgba(0,0,0,0.18) 75%, transparent);
  --v-div-edge: 0 1px 2px rgba(255,255,255,0.6), 1px 0 2px rgba(255,255,255,0.6);
  --v-div-blur: 0.3px;
  --v-well-pad: 7px;  --v-pill-drop: 2px;  --v-face-up: 2.5px;
  --v-lift: 3px;  --v-dip: 1.5px;  --v-sink: 1px;
  --v-font: ui-rounded, "SF Pro Rounded", "Arial Rounded MT Bold", "Varela Round", "Nunito", sans-serif;
  /* press lands fast, release springs back — tuned, final, unaffected by the motion setting */
  --v-press-in: 0.09s ease-out;
  --v-press-out: 0.32s cubic-bezier(0.34, 1.45, 0.6, 1);
}

/*
   Theme · Velvet (dark). <html data-theme="velvet" data-mode="dark">.
   Same material at night: deeper wells, wider casts, gold turns pale.
*/
[data-theme="velvet"][data-mode="dark"] {
  --background: #1a1a1e;
  --surface: #17171a;
  --text: #ffffff;
  --text-muted: #9c9ca1;
  --border: #2e2e33;

  --accent-1: #dcbe8e;
  --accent-1-hover: #e6cc9f;
  --accent-1-pressed: #d0b07c;
  --on-accent-1: #160e00;

  --accent-2: #353539;
  --accent-2-hover: #3c3c41;
  --accent-2-pressed: #454549;
  --on-accent-2: #ffffff;

  --accent-3: #dcbe8e;
  --accent-3-hover: #e6cc9f;
  --accent-3-pressed: #d0b07c;
  --on-accent-3: #160e00;

  /* Status inks lifted back up: the light block deepened them for its
     wells, and this block must answer with dark-ground values. */
  --success: #56B87A;
  --warning: #D8A23E;
  --error: #E5766A;
  --info: #7AA7DC;

  /* ---- VELVET value block · dark (verbatim from the reference) ---- */
  --v-bg: #1a1a1e;
  --v-well-bg: #17171a;
  --v-face:
    radial-gradient(120% 110% at 50% 0%, rgba(255,255,255,0.08), transparent 68%),
    linear-gradient(180deg, #353539, #17171a);
  --v-face-accent:
    radial-gradient(120% 110% at 50% 0%, rgba(255,255,255,0.26), transparent 68%),
    linear-gradient(180deg, #423b2e, #160e00);
  --v-ink: #ffffff;
  --v-ink-strong: #ffffff;
  --v-ink-accent: #ffffff;
  --v-gold: #dcbe8e;
  --v-weight: 650;
  --v-text-shadow: 0 1px 1.5px rgba(0,0,0,0.5), 0 -1px 1.5px rgba(255,255,255,0.25);
  --v-icon-filter: drop-shadow(0 1px 1.5px rgba(0,0,0,0.65)) drop-shadow(0 -1px 1.5px rgba(255,255,255,0.33));
  --v-key: 0 -8px 32px rgba(255,255,255,0.11);
  --v-cast: 0 9px 53px rgba(0,0,0,0.73);
  --v-bevel:
    inset 0 1.5px 4.5px rgba(255,255,255,0.08),
    inset 0 -1.5px 4.5px rgba(255,255,255,0.08),
    inset 0 -3px 16px rgba(0,0,0,0.71);
  --v-raised: var(--v-key), var(--v-cast), var(--v-bevel);
  --v-raised-pressed: var(--v-key), var(--v-cast), var(--v-bevel); /* pressElev 1 */
  --v-pill: var(--v-cast), var(--v-bevel);
  --v-raised-2nd: 0 -2.8px 11.2px rgba(255,255,255,0.11), 0 3.15px 18.55px rgba(0,0,0,0.73), var(--v-bevel);
  --v-well-shadow:
    var(--v-key), var(--v-cast),
    inset 0 1.5px 3px rgba(255,255,255,0.1),
    inset 0 7.2px 18px rgba(0,0,0,0.91),
    inset 0 -9.6px 24px rgba(255,255,255,0.11);
  --v-pit:
    inset 0 5px 13px rgba(0,0,0,0.75),
    inset 0 -5px 14px rgba(255,255,255,0.09);
  --v-dent-btn: radial-gradient(65% 56% at 50% 40%, rgba(0,0,0,0.4), transparent 78%);
  --v-dent-tap: radial-gradient(65% 56% at 50% 40%, rgba(0,0,0,1), transparent 78%);
  --v-div-line: linear-gradient(180deg, rgba(0,0,0,1), rgba(0,0,0,1));
  --v-div-edge: 0 1px 2px rgba(255,255,255,0.17), 1px 0 2px rgba(255,255,255,0.17);
  --v-div-blur: 0.6px;
  --v-well-pad: 7px;  --v-pill-drop: 3px;  --v-face-up: 0px;
  --v-lift: 2.5px;  --v-dip: 1px;  --v-sink: 0px;
}

/*
   Velvet recipes: how existing Facet components use the material.
   Scoped under the theme; recipes for the handoff's new components (tab
   bar, sheet, gauge...) arrive with those components. Press language
   everywhere: the dent is a fading ::after overlay, the face gradient
   never changes; the tuned press physics are final in every motion
   setting — only data-motion="off" and prefers-reduced-motion stop
   them.
*/

/* Inputs are carved into the material. */
[data-theme="velvet"] input:not(.slider),
[data-theme="velvet"] textarea,
[data-theme="velvet"] select {
  background-color: var(--v-well-bg);   /* -color: the select keeps its chevron */
  border-color: transparent;
  box-shadow: var(--v-pit);
}
[data-theme="velvet"] input:not(.slider):hover,
[data-theme="velvet"] textarea:hover,
[data-theme="velvet"] select:hover { border-color: transparent; }

/* Buttons: rounded typeface, raised faces, press feedback. */
[data-theme="velvet"] .btn {
  position: relative;
  border: none;
  border-radius: var(--radius-pill);
  font-family: var(--v-font);
  font-weight: var(--v-weight);
  color: var(--v-ink);
  text-shadow: var(--v-text-shadow);
  background: var(--v-face);
  box-shadow: var(--v-raised-2nd);
  transition: transform var(--v-press-out), box-shadow var(--v-press-out);
}
[data-theme="velvet"] .btn::after {
  content: ""; position: absolute; inset: 0; border-radius: var(--radius-pill);
  background: var(--v-dent-btn); opacity: 0; pointer-events: none;
  transition: opacity var(--v-press-out);
}
[data-theme="velvet"] .btn:hover { background: var(--v-face); border: none; }
[data-theme="velvet"] .btn:active {
  transform: translateY(1px);
  transition-duration: 0.09s, 0.09s;
}
[data-theme="velvet"] .btn:active::after { opacity: 1; transition: opacity var(--v-press-in); }

/* The one raised face per screen: the primary CTA carries the gold. */
[data-theme="velvet"] .btn-primary {
  background: var(--v-face-accent);
  color: var(--v-ink-accent);
  box-shadow: var(--v-raised);
  transform: translateY(calc(-1 * var(--v-face-up)));
}
[data-theme="velvet"] .btn-primary:hover { background: var(--v-face-accent); }
[data-theme="velvet"] .btn-primary:active {
  background: var(--v-face-accent);
  box-shadow: var(--v-raised-pressed);
  transform: translateY(calc(var(--v-sink) - var(--v-face-up)));
}

/* Tertiary stays flat: label never lifts, 1px fixed press sink. */
[data-theme="velvet"] .btn-ghost {
  background: transparent;
  box-shadow: none;
}
[data-theme="velvet"] .btn-ghost:hover { background: transparent; border: none; }
[data-theme="velvet"] .btn-ghost:active { transform: translateY(1px); }

/* Sliders: carved groove, raised knob carrying the gold jewel; grabbing
   deepens the gold — the face itself never changes. */
[data-theme="velvet"] .slider::-webkit-slider-runnable-track {
  height: 10px; background: var(--v-well-bg); box-shadow: var(--v-pit);
}
[data-theme="velvet"] .slider::-moz-range-track {
  height: 10px; background: var(--v-well-bg); box-shadow: var(--v-pit);
}
[data-theme="velvet"] .slider::-moz-range-progress { background: transparent; }
[data-theme="velvet"] .slider::-webkit-slider-thumb {
  width: 32px; height: 32px; margin-top: -11px;
  background:
    radial-gradient(circle closest-side at 50% 42%,
      color-mix(in srgb, var(--v-gold) 78%, #fff 30%) 0,
      var(--v-gold) 55%,
      color-mix(in srgb, var(--v-gold) 72%, #000 28%) 88%,
      transparent 93%) no-repeat 50% 50% / 18px 18px,
    var(--v-face);
  border: none;
  box-shadow: var(--v-pill);
}
[data-theme="velvet"] .slider::-moz-range-thumb {
  width: 32px; height: 32px;
  background:
    radial-gradient(circle closest-side at 50% 42%,
      color-mix(in srgb, var(--v-gold) 78%, #fff 30%) 0,
      var(--v-gold) 55%,
      color-mix(in srgb, var(--v-gold) 72%, #000 28%) 88%,
      transparent 93%) no-repeat 50% 50% / 18px 18px,
    var(--v-face);
  border: none;
  box-shadow: var(--v-pill);
}
[data-theme="velvet"] .slider:active::-webkit-slider-thumb {
  background:
    radial-gradient(circle closest-side at 50% 42%,
      color-mix(in srgb, var(--v-gold) 88%, #000 8%) 0,
      color-mix(in srgb, var(--v-gold) 74%, #000 26%) 55%,
      color-mix(in srgb, var(--v-gold) 55%, #000 45%) 88%,
      transparent 93%) no-repeat 50% 50% / 18px 18px,
    var(--v-face);
}
[data-theme="velvet"] .slider:active::-moz-range-thumb {
  background:
    radial-gradient(circle closest-side at 50% 42%,
      color-mix(in srgb, var(--v-gold) 88%, #000 8%) 0,
      color-mix(in srgb, var(--v-gold) 74%, #000 26%) 55%,
      color-mix(in srgb, var(--v-gold) 55%, #000 45%) 88%,
      transparent 93%) no-repeat 50% 50% / 18px 18px,
    var(--v-face);
}

/* Result blocks are wells; the key numbers show in gold. */
[data-theme="velvet"] .result {
  background: var(--v-well-bg);
  border: none;
  border-radius: var(--radius-card);
  box-shadow: var(--v-pit);
}

/* Cards are raised faces lifted out of the fabric. */
[data-theme="velvet"] .card {
  background: var(--v-face);
  border: none;
  box-shadow: var(--v-raised-2nd);
}

/* The chart card is a well carved into the page; the gold line
   arrives through accent-1 with no chart-specific rule at all. */
[data-theme="velvet"] .chart-card {
  background: var(--v-well-bg);
  border: none;
  box-shadow: var(--v-pit);
}
[data-theme="velvet"] .result-number { color: var(--v-gold); }
[data-theme="velvet"] .number-words { color: var(--v-gold); }
[data-theme="velvet"] .field output { color: var(--v-gold); }

/* Micro labels: the rounded typeface, wide-tracked uppercase. */
[data-theme="velvet"] .field-label {
  font-family: var(--v-font);
  font-size: var(--text-caption);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.16em;
  color: var(--text-muted);
}

/*
   Theme · Aero (light). <html data-theme="aero">.
   Frutiger Aero era: sky aqua, glass gloss, translucent plastic, pill
   buttons. Everything sits on a pale sky; controls are glass — a hard
   gloss line across the top half, a translucent white border, a soft
   blue glow under the one primary action. Accent-1 is glossy sky blue,
   accent-2 the translucent plastic fill, accent-3 the aero grass green
   that keeps AA as link text. The --a-* material tokens carry the
   glass itself; recipes below apply it to the components.
*/
[data-theme="aero"] {
  --background: #EAF6FD;      /* pale sky */
  --surface: #F7FCFF;         /* white glass panel */
  --text: #0C3049;            /* deep sea ink */
  --text-muted: #4E7189;      /* haze over water */
  --border: #BCDCEF;          /* sky hairline */

  --accent-1: #0B6FB8;        /* glossy sky blue, deep enough for white labels */
  --accent-1-hover: #0A64A6;
  --accent-1-pressed: #095996;
  --on-accent-1: #FFFFFF;

  --accent-2: #DCEFFA;        /* translucent plastic fill */
  --accent-2-hover: #CDE7F7;
  --accent-2-pressed: #BBDDF2;
  --on-accent-2: #0C3049;

  --accent-3: #0A7D5F;        /* aero grass green: AA as text on sky */
  --accent-3-hover: #086C52;
  --accent-3-pressed: #075C46;
  --on-accent-3: #FFFFFF;

  --success: #0D7042;
  --warning: #7F5D08;
  --error: #C03A2E;
  --info: #1273B8;

  /* Blue-tinted depth: aero shadows are blue-toned, never black. */
  --shadow-raised: 0 1px 2px rgb(12 48 73 / 0.10);
  --shadow-floating: 0 4px 14px rgb(12 48 73 / 0.14);
  --shadow-overlay: 0 12px 36px rgb(12 48 73 / 0.22);

  /* ----- The glass material ----- */
  /* The gloss is the era's signature: a bright top half ending in a
     hard line at 50%, a faint rise at the bottom edge. Layered as a
     background-image over any rank's fill. */
  --a-gloss: linear-gradient(180deg,
    rgb(255 255 255 / 0.80), rgb(255 255 255 / 0.28) 48%,
    rgb(255 255 255 / 0.00) 52%, rgb(255 255 255 / 0.16));
  --a-glass-border: rgb(255 255 255 / 0.65);
  --a-glass-bg: rgb(255 255 255 / 0.55);
  --a-glow: 0 2px 12px rgb(11 111 184 / 0.35);

  /* A sky gradient behind everything, so the frosted glass has real colour to
     refract (flat glass reads as gray). Fixed so it stays put while scrolling. */
  background-image:
    radial-gradient(140% 100% at 50% -10%, rgb(255 255 255 / 0.55), transparent 55%),
    linear-gradient(168deg, #D6EEFB 0%, #EAF6FD 45%, #E1F5EE 100%);
  background-attachment: fixed;
}

/*
   Theme · Aero (dark). Composes: <html data-theme="aero" data-mode="dark">.
   Ocean glass: deep water ground, the same gloss dimmed for the dark ground,
   sky blue lifted to stay the action color, grass green turned to
   sea glass.
*/
[data-theme="aero"][data-mode="dark"] {
  --background: #062A40;      /* deep water */
  --surface: #0B3A57;         /* submerged glass panel */
  --text: #E9F6FF;
  --text-muted: #8FB4CA;
  --border: #1D537A;

  --accent-1: #35B3F5;
  --accent-1-hover: #4FBEF8;
  --accent-1-pressed: #1FA0E4;
  --on-accent-1: #06283D;

  --accent-2: #0F476B;
  --accent-2-hover: #135380;
  --accent-2-pressed: #175E90;
  --on-accent-2: #E9F6FF;

  --accent-3: #43D6A9;        /* sea glass: AA as text on deep water */
  --accent-3-hover: #5DDFB6;
  --accent-3-pressed: #2FC497;
  --on-accent-3: #052E22;

  --success: #3FC98A;
  --warning: #E0B54D;
  --error: #F08A75;
  --info: #6FC2F2;

  --shadow-raised: 0 1px 2px rgb(0 0 0 / 0.4);
  --shadow-floating: 0 4px 14px rgb(0 0 0 / 0.45);
  --shadow-overlay: 0 12px 36px rgb(0 0 0 / 0.55);

  --a-gloss: linear-gradient(180deg,
    rgb(255 255 255 / 0.30), rgb(255 255 255 / 0.10) 48%,
    rgb(255 255 255 / 0.00) 52%, rgb(255 255 255 / 0.06));
  --a-glass-border: rgb(255 255 255 / 0.28);
  --a-glass-bg: rgb(150 200 235 / 0.10);
  --a-glow: 0 2px 14px rgb(53 179 245 / 0.45);

  /* Deep-ocean gradient behind the submerged glass. */
  background-image:
    radial-gradient(140% 100% at 50% -10%, rgb(120 200 255 / 0.14), transparent 55%),
    linear-gradient(168deg, #04283D 0%, #0A3D5C 55%, #06304A 100%);
  background-attachment: fixed;
}

/*
   Aero recipes: how components use the glass. Pills everywhere, the
   gloss layered over each rank's own fill, a translucent white edge,
   and the primary action alone carries the blue glow. Tokens only —
   the ranks still come from the blocks above.
*/
[data-theme="aero"] .btn {
  border-radius: var(--radius-pill);
  border-color: var(--a-glass-border);
  background-image: var(--a-gloss);
  box-shadow: var(--shadow-raised), inset 0 1px 0 rgb(255 255 255 / 0.55);
}
[data-theme="aero"] .btn-primary {
  box-shadow: var(--a-glow), inset 0 1px 0 rgb(255 255 255 / 0.45);
  text-shadow: 0 1px 1px rgb(0 0 0 / 0.18);
}
[data-theme="aero"] .btn-ghost { background-image: none; }

/* Text inputs become glass capsules; the plastic look needs the frame. */
[data-theme="aero"] input:not(.slider),
[data-theme="aero"] select {
  border-radius: var(--radius-pill);
  border-color: var(--a-glass-border);
  background-color: var(--a-glass-bg);
  box-shadow: inset 0 1px 2px rgb(12 48 73 / 0.08);
}
[data-theme="aero"] textarea {
  border-color: var(--a-glass-border);
  background-color: var(--a-glass-bg);
}

/* Panels get the faint gloss too — interface controls and cards read as plastic. */
[data-theme="aero"] .result,
[data-theme="aero"] .tab-bar,
[data-theme="aero"] .tab-set,
[data-theme="aero"] .tab-menu,
[data-theme="aero"] .tab-bar:has(.tab-set) > .tab-settings,
[data-theme="aero"] .sheet,
[data-theme="aero"] .float-btn,
[data-theme="aero"] .card {
  /* the invisible-row bar stays invisible: it is excluded from this list
     and re-stripped below the fallback */
  background-color: var(--a-glass-bg);        /* translucent, so the blur shows through */
  background-image: var(--a-gloss);
  border-color: var(--a-glass-border);
  /* The 2025 signature: a real frosted-glass blur, plus a saturation boost so
     the colour behind the glass blooms — this is what reads as Aero, not gray. */
  -webkit-backdrop-filter: blur(16px) saturate(180%);
  backdrop-filter: blur(16px) saturate(180%);
}
[data-theme="aero"] input:not(.slider),
[data-theme="aero"] select,
[data-theme="aero"] textarea {
  -webkit-backdrop-filter: blur(10px) saturate(160%);
  backdrop-filter: blur(10px) saturate(160%);
}
/* Where backdrop-filter is unsupported, fall back to an opaque surface so
   glass panels never turn illegible. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  [data-theme="aero"] .result,
  [data-theme="aero"] .tab-bar,
  [data-theme="aero"] .tab-set,
  [data-theme="aero"] .tab-menu,
  [data-theme="aero"] .tab-bar:has(.tab-set) > .tab-settings,
  [data-theme="aero"] .sheet,
  [data-theme="aero"] .float-btn,
  [data-theme="aero"] .card { background-color: var(--surface); }
}

/* Aero gave the tab pieces its glass; the invisible-row bar itself must
   stay invisible in every anatomy. */
[data-theme="aero"] .tab-bar:has(.tab-set) {
  background: none;
  background-image: none;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  border: none;
}

/*
   Theme · Elegant (light). <html data-theme="elegant">.
   The old Gems signature carried forward: cream surfaces, gold
   hairlines, serif display type, carved elevation. Warm ivory ground,
   umber ink, antique gold as the only accent — accent-1 and
   accent-3 are golds, accent-2 the quiet cream fill. Headings switch
   to the serif stack through the --font-heading token alone; the
   --e-carve token presses panels gently INTO the cream instead of
   floating them above it.
*/
[data-theme="elegant"] {
  --background: #F7F2E9;      /* cream */
  --surface: #FDFAF3;         /* ivory panel */
  --text: #2B2417;            /* umber ink */
  --text-muted: #6F654E;      /* deepened for AA on cream */
  --border: #D6C08D;          /* the gold hairline */

  --accent-1: #8C6D1F;        /* antique gold: the one primary action */
  --accent-1-hover: #7A5F1B;
  --accent-1-pressed: #695217;
  --on-accent-1: #FFF8E7;

  --accent-2: #F1E9D6;        /* cream fill for every quiet action */
  --accent-2-hover: #EAE0C8;
  --accent-2-pressed: #DFD2B2;
  --on-accent-2: #2B2417;

  --accent-3: #7A5F1B;        /* deep gold: AA as text on cream */
  --accent-3-hover: #695217;
  --accent-3-pressed: #584512;
  --on-accent-3: #FFF8E7;

  --success: #516B31;         /* olive, AA on cream */
  --warning: #885D14;
  --error: #A44A35;           /* terracotta */
  --info: #5B6E8C;            /* slate */

  /* Warm depth, and shallow: this theme never casts a heavy shadow. */
  --shadow-raised: 0 1px 2px rgb(43 36 23 / 0.08);
  --shadow-floating: 0 4px 12px rgb(43 36 23 / 0.10);
  --shadow-overlay: 0 12px 32px rgb(43 36 23 / 0.18);

  /* High-contrast didone display serif — the editorial-luxury signature.
     Stack ends in a generic family (font-list law). */
  --font-heading: "Playfair Display", "Iowan Old Style", Palatino,
                  Georgia, "Times New Roman", serif;

  /* Luxury reads as crisp, not friendly — tighten the corners. */
  --radius-control: 3px;
  --radius-panel: 4px;
  --radius-card: 5px;

  /* ----- Carved (letterpress) elevation ----- */
  /* Panels are pressed into the paper, never floated: a dark inset top-left
     (ink settling in the well) and a bright inset bottom-right (paper catching
     light), plus a 1px outer light line to sell the debossed edge. */
  --e-carve:
    inset 2px 2px 5px rgb(90 70 30 / 0.20),
    inset -2px -2px 5px rgb(255 252 244 / 0.85),
    0 1px 0 rgb(255 255 255 / 0.55);
}

/*
   Theme · Elegant (dark). Composes: <html data-theme="elegant"
   data-mode="dark">. The obsidian counterpart: near-black warm ground, lit
   gold for both accents, the hairlines dimmed to old brass.
*/
[data-theme="elegant"][data-mode="dark"] {
  --background: #14110B;      /* obsidian */
  --surface: #1E1910;
  --text: #F0E7D4;
  --text-muted: #A79A76;
  --border: #4A3E20;          /* old brass hairline */

  --accent-1: #D8B75B;        /* lit gold */
  --accent-1-hover: #E2C46E;
  --accent-1-pressed: #C9A748;
  --on-accent-1: #201806;

  --accent-2: #1E1910;
  --accent-2-hover: #272113;
  --accent-2-pressed: #322A17;
  --on-accent-2: #F0E7D4;

  --accent-3: #DCC077;        /* pale gold ink: AA on obsidian */
  --accent-3-hover: #E6CB88;
  --accent-3-pressed: #D0B364;
  --on-accent-3: #1A1204;

  --success: #8FAE5F;
  --warning: #CFA145;
  --error: #C97B62;
  --info: #8FA3C0;

  --shadow-raised: 0 1px 2px rgb(0 0 0 / 0.4);
  --shadow-floating: 0 4px 12px rgb(0 0 0 / 0.45);
  --shadow-overlay: 0 12px 32px rgb(0 0 0 / 0.55);

  --e-carve:
    inset 2px 2px 6px rgb(0 0 0 / 0.60),
    inset -1px -1px 3px rgb(120 100 60 / 0.14);
}

/*
   Elegant recipes: carved panels and hairline gold edges. The serif
   headings arrive through --font-heading with no rule at all.
*/
[data-theme="elegant"] .result,
[data-theme="elegant"] .sheet,
[data-theme="elegant"] .menu-item,
[data-theme="elegant"] .card {
  box-shadow: var(--e-carve);
}
[data-theme="elegant"] input:not(.slider),
[data-theme="elegant"] textarea,
[data-theme="elegant"] select {
  box-shadow: var(--e-carve);
}
/* The one gold action gets a thin metallic gradient rather than a flat fill —
   brushed brass, not a paint chip. */
[data-theme="elegant"] .btn-primary {
  background-image: linear-gradient(145deg, #E6C458, #B8860B 45%, #8A6208);
  border-color: #8A6208;
}
[data-theme="elegant"][data-mode="dark"] .btn-primary,
[data-theme="elegant"] [data-mode="dark"] .btn-primary {
  background-image: linear-gradient(145deg, #E8CE7E, #D8B75B 45%, #B99A3E);
  border-color: #B99A3E;
}
/* Quiet actions carry the hairline instead of a filled edge. */
[data-theme="elegant"] .btn {
  border-color: var(--border);
}

/*
   The OS accent, as its own color (R2, reworked). --os-accent is a dormant,
   opt-in token: no component uses it, so nothing changes unless you write
   var(--os-accent) yourself. It defaults to the iOS system blue everywhere;
   where the browser exposes the real OS accent, it becomes that instead.

   Why it is opt-in and not the default accent-3: on iOS AccentColor is
   always the system blue, and on macOS almost nobody changes their accent,
   so wiring accent-3 to it made nearly every Apple user blue and every
   Chromium user ink — not the point of "wear the user's own accent". As a
   separate color, the effect is accurate: blue by default for all, and a
   user's chosen accent only for the few on a supporting browser who set one.

   Support (probed, July 2026): AccentColor / AccentColorText resolve on
   Safari 16.4+ and Firefox 103+; Chromium does not support the keyword
   (CSS.supports returns false), so it stays on the iOS-blue fallback —
   which is exactly the intended default there. color-mix() over the keyword
   is safe in every engine that passes the gate.
*/
@supports (color: AccentColor) {
  :root {
    --os-accent: AccentColor;
    --os-accent-hover: color-mix(in srgb, AccentColor 86%, black);
    --os-accent-pressed: color-mix(in srgb, AccentColor 74%, black);
    --on-os-accent: AccentColorText;
  }
}

/*
   Reader adaptation (R3). The page bends to the reader, not the other
   way around — and it bends at the token layer, so every theme and
   every component adapts with zero extra code.
*/

/* Stronger contrast on request: muted text gives up its gray, borders
   climb toward the ink, the focus ring thickens. Formulas over the
   active theme's own tokens, so this holds in all five themes, light
   and dark. Placed after every theme block on purpose — same-or-higher
   specificity, later wins. */
@media (prefers-contrast: more) {
  :root,
  [data-mode],
  [data-theme],
  [data-theme][data-mode] {
    --text-muted: var(--text);
    --border: color-mix(in srgb, var(--text) 55%, var(--background));
    --focus-ring: calc(var(--border-focus) + 1px) solid var(--accent-3);
  }
}

/* Forced colors (Windows High Contrast): the OS repaints everything in
   its own palette. The audit fixes the pieces that vanish — elements
   whose shape was carried by a background alone get a real border, so
   they keep their outline in the system palette. */
@media (forced-colors: active) {
  .tab-indicator,
  .scroll-gauge,
  .scroll-gauge-thumb,
  .sheet,
  .nudge-card,
  .float-btn { border: 1px solid ButtonText; }
  .slider { border: 1px solid ButtonText; border-radius: var(--radius-pill); }
  [data-tip]::after { border: 1px solid CanvasText; }
}

/* Density: one attribute scales the whole spacing system at once. Small
   tightens, large opens up; medium is the default (no attribute). Works on
   html or any subtree — <html data-density="small">, a script-tag config
   key, or facet.set({density:"small"}). */
[data-density="small"] { --density-scale: 0.75; }
[data-density="large"] { --density-scale: 1.3; }

/* Type size: the same three-step control for type alone, independent of
   density. <html data-text-size="large">, or facet.set({textSize:"large"}). */
[data-text-size="small"] { --text-scale: 0.92; }
[data-text-size="large"] { --text-scale: 1.12; }

/* Screen-reader-only text: visually gone, fully read. The standard
   clip pattern; undo by removing the class, never by overriding. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* Text roles: the type scale as classes, one per token, so any element
   can take a scale step without an inline style. Display and the
   heading steps carry the heading font; the rest stay in the body
   font. Same names as the tokens: .text-display, .text-h1 … .text-h4,
   .text-body, .text-quote, .text-footnote, .text-caption. */
.text-display { font-family: var(--font-heading); font-size: var(--text-display); font-weight: var(--weight-heading); line-height: var(--leading-display); letter-spacing: var(--tracking-display); }
.text-h1      { font-family: var(--font-heading); font-size: var(--text-h1); font-weight: var(--weight-heading); line-height: var(--leading-heading); letter-spacing: var(--tracking-display); }
.text-h2      { font-family: var(--font-heading); font-size: var(--text-h2); font-weight: var(--weight-heading); line-height: var(--leading-heading); letter-spacing: var(--tracking-heading); }
.text-h3      { font-family: var(--font-heading); font-size: var(--text-h3); font-weight: var(--weight-strong); line-height: var(--leading-snug); letter-spacing: var(--tracking-heading); }
.text-h4      { font-family: var(--font-heading); font-size: var(--text-h4); font-weight: var(--weight-strong); line-height: var(--leading-snug); letter-spacing: var(--tracking-snug); }
.text-body    { font-size: var(--text-body);    line-height: var(--leading-body); }
.text-quote   { font-size: var(--text-quote);   line-height: var(--leading-body); font-style: italic; }
.text-footnote   { font-size: var(--text-footnote);   line-height: var(--leading-ui); }
.text-caption { font-size: var(--text-caption); line-height: var(--leading-ui); color: var(--text-muted); }


/* ----- TEXT CLAMP: hold a long block to N lines with a fade -----
   What it is: a line clamp with a fade-out instead of an ellipsis —
   add .text-clamp to any text block and it shows the first
   --clamp-lines lines (default 3) and fades the last of them out at
   the bottom. Set the count per instance with --clamp-lines. The clamp
   is purely visual: the full text stays in the DOM, so screen readers,
   reader view, search crawlers and print get all of it (paper
   un-clamps automatically). What it is for: keeping cards and rows the
   same height when their copy runs long. When to use it: on long
   supporting text, never on a heading or the one line a reader must
   finish. */
.text-clamp {
  --clamp-lines: 3;
  display: block;
  overflow: hidden;
  max-height: calc(var(--clamp-lines) * var(--leading-body) * 1em);
  -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 1.15em), transparent);
          mask-image: linear-gradient(to bottom, #000 calc(100% - 1.15em), transparent);
}
@media print {
  .text-clamp { max-height: none; -webkit-mask-image: none; mask-image: none; }
}


/* ----- BACKGROUNDS: quiet materials in two variants, any theme -----
   What it is: very faint page and section backgrounds, in two
   variants that are built differently and tuned separately.
   VARIANT GRID — a repeating symbol on a grid. The looks: .bg-grid
   (technical drawing: plus marks at the intersections), .bg-dots
   (dot grid), .bg-circles (circle grid: outlined rings on the dots'
   half-cell rhythm, noticeably bigger than a dot; --bg-ring sets the
   ring diameter, default 15px), .bg-ruled (writing lines), .bg-graph
   (graph paper, deliberately the lightest), or the custom grid — data-bg-glyph="✦"
   with any character or emoji, a facet icon name, or several tokens
   separated by spaces for an alternating grid; facet.js draws the
   tile in the pattern ink and redraws it live whenever the attribute,
   the theme or the mode changes. Every grid tunes with three custom
   properties: --bg-tint (the ink color, default the muted text
   token), --bg-strength (its opacity, default 28%), --bg-cell (the
   spacing, default 2.5rem); the custom grid adds data-bg-glyph-size
   (spacing override, px), data-bg-glyph-scale (glyph size as a
   fraction of the cell, default 0.42).
   THE SCATTER — data-bg-scatter="✦ ❋" on .bg-grid, .bg-dots or
   .bg-circles sows the given characters (any character or emoji, or
   a facet icon name) across the lattice: roughly one point in ten
   loses its plus, dot or ring and wears a character instead.
   data-bg-scatter-rate tunes the frequency (one character per N
   lattice points, default 10); data-bg-scatter-scale multiplies the
   character size (0.1–2, default 1). The arrangement is seeded fresh
   on every page load — each visit scatters differently — but holds
   within the load, so a theme or mode change re-inks the same
   arrangement instead of reshuffling. facet.js draws the scatter
   tile in the same pattern ink and redraws it with the glyph engine.
   VARIANT FLUID — data-bg-fluid: a soft, slowly-breathing field of
   out-of-focus color pools and drifting bubbles, drawn from the
   accent and vivid tokens so every theme recolors it for free;
   facet.js mounts three GPU-composited layers whose desynced clocks
   keep it from ever repeating. Still under data-motion="off" and
   reduced motion; plain background with JS off.
   FIXED TO THE SCREEN — .bg-fixed on any grid look makes it the
   page's one still backdrop (body's first child, content floats over
   it, its own --bg-strength default raised to 22%);
   data-bg-fluid-fixed beside data-bg-fluid pins the pools to the
   viewport while the surface scrolls over them like a window,
   clipping with a clip-path rounded to the card radius
   (--bg-fluid-radius overrides).
   What it is for: giving big quiet surfaces a
   material — a hero, a section divider zone, a whole page. When to
   use it: one class or attribute on a section or body; content sits
   straight on top. All of it fades to nothing under
   prefers-contrast: more and never prints. */

.bg-grid,
.bg-dots,
.bg-circles,
.bg-ruled,
.bg-graph,
[data-bg-glyph] {
  --bg-tint: var(--text-muted);      /* the ink color — any token or color */
  --bg-strength: 12%;                /* the ink opacity — whisper-quiet by default */
  --bg-cell: 2.5rem;                 /* the grid spacing */
  --bg-ink: color-mix(in srgb, var(--bg-tint) var(--bg-strength), transparent);
  --bg-ink-faint: color-mix(in srgb, var(--bg-tint) calc(var(--bg-strength) / 2), transparent);
  --bg-ink-minor: color-mix(in srgb, var(--bg-tint) calc(var(--bg-strength) / 4), transparent);
}

/* Technical drawing: a plus mark at every grid intersection and
   nothing else. Built from four gradient layers — two full gridline
   layers underneath, two background-colored cover layers on top with
   a transparent window at each intersection, so only plus-length
   stubs of the lines show through. Gradients only, no images. */
.bg-grid {
  --bg-plus: 0.75rem;
  background-color: var(--background);
  background-image:
    /* the character scatter rides on top when facet.js sets it */
    var(--bg-scatter, none),
    /* cover with vertical windows (bounds the horizontal arms) */
    linear-gradient(90deg,
      var(--background) 0 calc(50% - var(--bg-plus) / 2),
      transparent 0 calc(50% + var(--bg-plus) / 2),
      var(--background) 0),
    /* cover with horizontal windows (bounds the vertical arms) */
    linear-gradient(180deg,
      var(--background) 0 calc(50% - var(--bg-plus) / 2),
      transparent 0 calc(50% + var(--bg-plus) / 2),
      var(--background) 0),
    /* the gridlines themselves, crossing at every tile center */
    linear-gradient(180deg,
      transparent 0 calc(50% - 0.5px), var(--bg-ink) 0 calc(50% + 0.5px), transparent 0),
    linear-gradient(90deg,
      transparent 0 calc(50% - 0.5px), var(--bg-ink) 0 calc(50% + 0.5px), transparent 0);
  background-size:
    var(--bg-scatter-tile, auto),
    var(--bg-cell) var(--bg-cell),
    var(--bg-cell) var(--bg-cell),
    var(--bg-cell) var(--bg-cell),
    var(--bg-cell) var(--bg-cell);
}

/* Dot grid: one soft dot per half cell. */
.bg-dots {
  background-color: var(--background);
  background-image:
    var(--bg-scatter, none),
    radial-gradient(circle, var(--bg-ink) 1px, transparent 1.5px);
  background-size:
    var(--bg-scatter-tile, auto),
    calc(var(--bg-cell) / 2) calc(var(--bg-cell) / 2);
}

/* Circle grid: an outlined ring per half cell — the dots' rhythm, but
   each lattice point is a stroked ring (no fill), noticeably bigger
   than a dot. A ring is a radial gradient that stays transparent to
   just inside the stroke, inks a hairline band, then fades back out.
   --bg-ring is the ring diameter, tunable per surface. */
.bg-circles {
  --bg-ring: 15px;
  background-color: var(--background);
  background-image:
    var(--bg-scatter, none),
    radial-gradient(circle,
      transparent calc(var(--bg-ring) / 2 - 2px),
      var(--bg-ink) calc(var(--bg-ring) / 2 - 1.5px) calc(var(--bg-ring) / 2 - 0.5px),
      transparent calc(var(--bg-ring) / 2));
  background-size:
    var(--bg-scatter-tile, auto),
    calc(var(--bg-cell) / 2) calc(var(--bg-cell) / 2);
}

/* Ruled: writing lines, generous leading. */
.bg-ruled {
  background-color: var(--background);
  background-image: linear-gradient(var(--bg-ink) 1px, transparent 1px);
  background-size: 100% 1.75rem;
}

/* Graph paper: faint minor squares, firmer major squares. */
.bg-graph {
  background-color: var(--background);
  background-image:
    linear-gradient(var(--bg-ink-faint) 1px, transparent 1px),
    linear-gradient(90deg, var(--bg-ink-faint) 1px, transparent 1px),
    linear-gradient(var(--bg-ink-minor) 1px, transparent 1px),
    linear-gradient(90deg, var(--bg-ink-minor) 1px, transparent 1px);
  background-size:
    var(--bg-cell) var(--bg-cell),
    var(--bg-cell) var(--bg-cell),
    calc(var(--bg-cell) / 5) calc(var(--bg-cell) / 5),
    calc(var(--bg-cell) / 5) calc(var(--bg-cell) / 5);
}

/* Glyph grid: your symbol as the pattern. facet.js writes the two
   custom properties from data-bg-glyph / data-bg-glyph-size. */
[data-bg-glyph] {
  background-color: var(--background);
  background-image: var(--bg-glyph);
  background-size: var(--bg-glyph-tile, 3.5rem 3.5rem);
}

/* Variant fluid: out-of-focus color pools breathing behind the page.
   facet.js mounts three .bg-fluid-layer spans; each carries its pools
   as radial gradients in theme colors (so themes recolor them free),
   heavily blurred, drifting on its own clock — desynced durations
   mean the field never visibly repeats. The first layer also paints
   the page ground, so the stack works over any ancestor. */
[data-bg-fluid] {
  position: relative;
  overflow: hidden;
  isolation: isolate;                /* the layers stay inside this surface */
}
.bg-fluid-layer {
  position: absolute;
  inset: -25%;
  z-index: -1;
  pointer-events: none;
  filter: blur(42px);
}
.bg-fluid-layer:nth-of-type(1) {
  inset: 0;
  filter: none;
  background: var(--background);     /* the ground the pools float over */
}
.bg-fluid-layer:nth-of-type(2) {
  background-image:
    radial-gradient(42% 38% at 24% 32%, color-mix(in srgb, var(--accent-3) 20%, transparent), transparent 70%),
    radial-gradient(34% 30% at 78% 18%, color-mix(in srgb, var(--color-4) 14%, transparent), transparent 70%),
    radial-gradient(40% 36% at 66% 82%, color-mix(in srgb, var(--color-5) 12%, transparent), transparent 70%);
  animation: facet-fluid-drift 38s ease-in-out infinite alternate;
}
.bg-fluid-layer:nth-of-type(3) {
  background-image:
    radial-gradient(7% 7% at 30% 78%, color-mix(in srgb, var(--accent-3) 26%, transparent), transparent 70%),
    radial-gradient(5% 5% at 58% 40%, color-mix(in srgb, var(--color-4) 22%, transparent), transparent 70%),
    radial-gradient(4% 4% at 82% 64%, color-mix(in srgb, var(--color-3) 20%, transparent), transparent 70%),
    radial-gradient(6% 6% at 12% 22%, color-mix(in srgb, var(--color-5) 18%, transparent), transparent 70%);
  animation: facet-fluid-rise 51s ease-in-out infinite alternate;
}
@keyframes facet-fluid-drift {
  to { transform: translate3d(5%, -4%, 0) scale(1.18) rotate(6deg); }
}
@keyframes facet-fluid-rise {
  to { transform: translate3d(-4%, -9%, 0) scale(1.12) rotate(-5deg); }
}

/* FIXED, WHOLE-PAGE: .bg-fixed turns any grid look into the page's one
   still backdrop — <div class="bg-dots bg-fixed"></div> as the body's
   first child, content floats over it with no wrapper, and the pattern
   holds still while the page scrolls. Fixed full-page use is exactly
   where the whisper default disappears on dim phone panels, so this
   variant speaks up a little; tune with --bg-strength as usual. */
.bg-fixed {
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  --bg-strength: 22%;
}

/* VARIANT FLUID, FIXED TO THE SCREEN: add data-bg-fluid-fixed beside
   data-bg-fluid and the pools pin to the VIEWPORT while the surface
   scrolls over them like a window — a hero card gliding over a still
   wash. overflow can't clip fixed children, so the surface clips its
   window with a clip-path instead (rounded to the card radius;
   --bg-fluid-radius overrides). The base rule's isolation keeps the
   fixed layers painting inside this surface, which is what lets the
   clip bite. */
[data-bg-fluid][data-bg-fluid-fixed] {
  clip-path: inset(0 round var(--bg-fluid-radius, var(--radius-card)));
}
[data-bg-fluid-fixed] > .bg-fluid-layer { position: fixed; }


/* Contrast request: the pattern is removed. */
@media (prefers-contrast: more) {
  .bg-grid, .bg-dots, .bg-circles, .bg-ruled, .bg-graph, [data-bg-glyph] { background-image: none; }
  .bg-fluid-layer { display: none; }
}


/*
   Motion · the spring, upgraded. Browsers that understand
   linear() get a real spring for --ease-spring: overshoot, then settle
   with momentum. Everyone else keeps the bouncy back-out fallback above.
*/
@supports (transition-timing-function: linear(0, 1)) {
  :root {
    --ease-spring: linear(
      0, 0.06 2.5%, 0.235 6.3%, 0.575 12.6%, 0.819 17.6%, 1.003 22.6%,
      1.071 26.2%, 1.104 30%, 1.106 33.9%, 1.084 38.7%, 1.032 47.3%,
      1.003 54.5%, 0.995 62.5%, 1
    );
  }
}

/*
   Motion · Calm. <html data-motion="calm"> (or any subtree).
   Short, near-linear, slight — for products that should stay subdued. Same
   tokens, quieter values; no component changes a thing.
*/
[data-motion="calm"] {
  --duration-fast: 100ms;
  --duration-normal: 160ms;
  --duration-slow: 240ms;
  --ease-out: cubic-bezier(0.33, 0, 0.67, 1);
  --ease-in-out: cubic-bezier(0.33, 0, 0.67, 1);
  --ease-spring: cubic-bezier(0.33, 0, 0.67, 1);
}

/*
   Motion · Off. <html data-motion="off">. The explicit stop:
   everything snaps, nothing animates. prefers-reduced-motion (further
   down) does the same thing for visitors who asked the OS for it.
*/
[data-motion="off"],
[data-motion="off"] *,
[data-motion="off"] *::before,
[data-motion="off"] *::after {
  transition-duration: 0.01ms !important;
  animation-duration: 0.01ms !important;
  scroll-behavior: auto !important;
}

/*
   Seamless page transitions. <html data-transition="page">. Moving
   between pages of a Facet product no longer looks like a full page
   reload: the old page eases away, the new one springs in, and
   the URL changes normally — real multi-page navigation, no router.

   Built on cross-document view transitions. The at-rule below arms them
   for every Facet page; the animations only play on pages that opt in
   with the attribute (everyone else keeps an instant swap), only within
   the same origin, and they ride the motion tokens — Animated
   pages arrive with a settle, Calm pages slide quietly, data-motion="off"
   and prefers-reduced-motion snap. Browsers without support simply
   navigate: never broken, just instant.

   Per-link opt-out: <a data-no-transition> snaps the old page straight
   out (facet.js drops the attribute before leaving); the arrival is the
   destination page's own choice. facet.go(url) navigates with the same
   transition programmatically.
*/
@view-transition {
  navigation: auto;
}

/* Pages that did not opt in keep the instant swap they always had. */
:root:not([data-transition="page"])::view-transition-old(root),
:root:not([data-transition="page"])::view-transition-new(root) {
  animation-duration: 0s;
}

/* The opted-in move: old page eases up and away, new page springs in. */
:root[data-transition="page"]::view-transition-old(root) {
  animation: facet-page-out var(--duration-normal) var(--ease-in-out) both;
}

:root[data-transition="page"]::view-transition-new(root) {
  animation: facet-page-in var(--duration-slow) var(--ease-spring) both;
}

@keyframes facet-page-out {
  to { opacity: 0; translate: 0 calc(-1 * var(--space-stack)); }
}

@keyframes facet-page-in {
  from { opacity: 0; translate: 0 var(--space-card); }
}

/* Motion off — by attribute or by OS preference — means pages snap. */
:root[data-motion="off"]::view-transition-old(root),
:root[data-motion="off"]::view-transition-new(root) {
  animation-duration: 0s;
}

@media (prefers-reduced-motion: reduce) {
  ::view-transition-old(root),
  ::view-transition-new(root) {
    animation-duration: 0s !important;
  }
}

/* The browser's own widgets (scrollbars, form controls) follow the mode. */
html[data-mode="dark"] { color-scheme: dark; }
html { color-scheme: light; }

/* A themed subtree (the attributes on any element below <html>) paints its
   own ground, so a dark panel on a light page reads as one. */
body [data-mode],
body [data-theme] {
  background: var(--background);
  color: var(--text);
}


/* ==========================================================================
   2. RESET
   Small on purpose: box sizing, margin zeroing, media defaults.
   ========================================================================== */

*,
*::before,
*::after {
  box-sizing: border-box;
}

* {
  margin: 0;
}

/* Color eases everywhere by default: hover tints, status flips, theme
   and mode switches all cross-fade instead of snapping. Paint-only
   properties on purpose — transform, size and position stay instant
   because the motion engines and layout own those. A component that
   declares its own transition overrides this list wholesale, and
   reduced motion collapses it to nothing. */
*,
*::before,
*::after {
  transition:
    color var(--duration-fast) var(--ease-out),
    background-color var(--duration-fast) var(--ease-out),
    border-color var(--duration-fast) var(--ease-out),
    box-shadow var(--duration-fast) var(--ease-out),
    fill var(--duration-fast) var(--ease-out),
    stroke var(--duration-fast) var(--ease-out),
    opacity var(--duration-fast) var(--ease-out);
}
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after { transition: none; }
}

/* Form elements inherit the page's typography instead of browser defaults. */
button,
input,
select,
textarea {
  font: inherit;
  color: inherit;
}

/* Media never overflows its container. */
img,
video,
svg,
canvas {
  display: block;
  max-width: 100%;
  height: auto;
}

/* The hidden attribute always wins, even over components that set their
   own display (flex, grid). Without this, .result and .stack ignore it. */
[hidden] {
  display: none !important;
}


/* ==========================================================================
   3. BASE STYLES
   What every raw HTML tag looks like before any class is added.
   Write plain semantic HTML and it already looks like Facet.
   ========================================================================== */

/* (docs mirror — identical to llms.txt · Base styles)
   WHEN TO USE: write raw semantic HTML and it already looks designed —
   this is the layer that makes a bare page presentable before any class.
   Headings, paragraphs, lists, blockquotes, tables, code, forms and
   buttons are all styled; one visible keyboard focus ring everywhere;
   selection colors; thin scrollbars; a skip-to-content link pattern; the
   hidden attribute always wins; tabular figures on numeric UI; images
   are block-level and never overflow. Controls refuse accidental text
   selection (buttons, labels, chips, tabs, navigation) while all content
   stays selectable — never turn selection off page-wide. Content,
   layout, links and forms all work with JavaScript off — facet.js only
   adds enhancements. */

/* ----- Page ----- */

html {
  background: var(--background);
  color: var(--text);
  font-family: var(--font-body);
  font-weight: var(--weight-body);
  line-height: var(--leading-body);
  -webkit-text-size-adjust: 100%;
  /* Let height/width transitions animate to and from keyword sizes (auto,
     max-content). Harmless where unsupported; needed so disclosures expand
     smoothly to their natural height instead of popping open. */
  interpolate-size: allow-keywords;
}

body {
  min-height: 100dvh;
  font-size: var(--text-body);
}

/* ----- Headings ----- */

/* Shared heading traits; size, line-height and tracking are set per level
   below so each reads at its own weight. Margins give raw prose its rhythm:
   a heading separates from what's above it and hugs the content below. */
h1, h2, h3, h4, h5, h6 {
  font-family: var(--font-heading);
  font-weight: var(--weight-heading);
  text-wrap: balance;
  margin: var(--flow-heading-above) 0 var(--flow-heading-below);
}

h1 { font-size: var(--text-h1); line-height: var(--leading-heading); letter-spacing: var(--tracking-display); }
h2 { font-size: var(--text-h2); line-height: var(--leading-heading); letter-spacing: var(--tracking-heading); }
h3 { font-size: var(--text-h3); line-height: var(--leading-snug);    letter-spacing: var(--tracking-heading); }
h4, h5, h6 { font-size: var(--text-h4); line-height: var(--leading-snug); letter-spacing: var(--tracking-snug); }

/* ----- Text ----- */

p {
  max-width: var(--measure);   /* comfortable reading measure by default */
  margin: 0 0 var(--flow-paragraph);
  text-wrap: pretty;
}

small { font-size: var(--text-footnote); }

strong, b { font-weight: var(--weight-strong); }

a {
  color: var(--accent-3);       /* the quiet interactive rank, every theme */
  text-decoration: underline;
  text-underline-offset: 0.15em;
  text-decoration-color: var(--border);
  transition: text-decoration-color var(--duration-fast) var(--ease-out);
}

a:hover { text-decoration-color: currentColor; }

ul, ol {
  margin: 0 0 var(--flow-paragraph);
  padding-left: var(--space-card);
}

li + li { margin-top: var(--flow-tight); }

blockquote {
  margin: 0 0 var(--flow-paragraph);
  padding-left: var(--space-stack);
  border-left: var(--border-highlight) solid var(--accent-3);
  font-family: var(--font-quote);   /* the editorial quote face, set apart from headings */
  font-size: var(--text-quote);
  font-style: italic;
  color: var(--text-muted);
}

figure, table, pre { margin: 0 0 var(--flow-paragraph); }

/* Prose rhythm, contained. The margins above give raw semantic HTML its
   vertical flow; these rules keep that flow from fighting anything:
   a container never opens with a top margin or closes with a trailing one,
   and gap-based layout primitives (.stack, .row) own their spacing outright
   so margins and gap never stack. The child reset carries real class
   specificity ON PURPOSE: it must beat the h1–h6/p element margins above
   (a zero-specificity :where() version shipped first and silently lost,
   so every stack holding prose double-spaced). Component rules that set
   a child margin by class still win — they tie on specificity and come
   later in the file. */
:where(h1, h2, h3, h4, h5, h6, p, ul, ol, blockquote, pre, table, figure):first-child { margin-top: 0; }
:where(h1, h2, h3, h4, h5, h6, p, ul, ol, blockquote, pre, table, figure):last-child { margin-bottom: 0; }
.stack > *, .stack-tight > *, .stack-loose > *,
.row > *, .row-tight > *, .row-loose > * { margin-block: 0; }

hr {
  border: none;
  border-top: var(--border-hairline) solid var(--border);
}

/* ----- Code ----- */

code, kbd, samp, pre {
  font-family: var(--font-mono);
  font-size: 0.9em;
}

/* Inline code gets a quiet chip; code inside pre stays bare. */
code {
  background: var(--surface);
  border: var(--border-element) solid var(--border);
  border-radius: var(--radius-control);
  padding: 0.1em 0.35em;
}

pre {
  background: var(--surface);
  border: var(--border-element) solid var(--border);
  border-radius: var(--radius-panel);
  padding: var(--space-stack);
  overflow-x: auto;             /* wrapper law: this container exists to scroll */
}

pre code {
  background: none;
  border: none;
  border-radius: 0;
  padding: 0;
}

/* ----- Forms ----- */

label {
  display: block;
  font-size: var(--text-footnote);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-label);
}

input,
textarea,
select {
  width: 100%;
  min-height: 44px;             /* touch target */
  padding: var(--space-control) var(--space-inline);
  background: var(--surface);
  border: var(--border-element) solid var(--border);
  border-radius: var(--radius-control);
  transition: border-color var(--duration-fast) var(--ease-out);
}

input:hover,
textarea:hover,
select:hover {
  border-color: var(--text-muted);
}

input:disabled,
textarea:disabled,
select:disabled,
button:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

::placeholder {
  color: var(--text-muted);
  opacity: 1;
}

textarea {
  min-height: calc(var(--space-hero) * 2);
  resize: vertical;
}

fieldset {
  border: var(--border-element) solid var(--border);
  border-radius: var(--radius-panel);
  padding: var(--space-stack);
}

legend {
  font-size: var(--text-footnote);
  font-weight: var(--weight-strong);
  padding-inline: var(--space-control);
}

/* Bare buttons pick up the secondary button look; see COMPONENTS for .btn. */
button {
  cursor: pointer;
}

/* ----- Tables ----- */

table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--text-footnote);
}

th, td {
  text-align: left;
  padding: var(--space-control) var(--space-inline);
  border-bottom: var(--border-hairline) solid var(--border);
}

th {
  font-weight: var(--weight-medium);
  color: var(--text-muted);
  font-size: var(--text-caption);
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
}

tbody tr:hover {
  background: var(--surface);
}

/* ----- Figures ----- */

figure img,
figure video {
  border-radius: var(--radius-panel);
}

figcaption {
  margin-top: var(--space-control);
  font-size: var(--text-caption);
  color: var(--text-muted);
}

/* ----- Focus rings ----- */
/* One visible ring everywhere, keyboard only. Never remove, only restyle. */

:focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-offset);
  border-radius: var(--radius-control);
}

/* ----- Text selection ----- */

::selection {
  background: var(--accent-3);
  color: var(--on-accent-3);
}

/* ----- Scrollbars ----- */

* {
  scrollbar-width: thin;
  scrollbar-color: var(--border) transparent;
}

/* ----- Skip-to-content link ----- */
/* First element inside <body> on every page:
   <a class="skip-link" href="#main">Skip to content</a> */

.skip-link {
  position: absolute;
  top: var(--space-control);
  left: var(--space-control);
  z-index: 100;
  padding: var(--space-control) var(--space-stack);
  background: var(--surface);
  border: var(--border-element) solid var(--border);
  border-radius: var(--radius-control);
  box-shadow: var(--shadow-floating);
  transform: translateY(-200%);   /* parked offscreen until focused */
  transition: transform var(--duration-normal) var(--ease-spring);
}

.skip-link:focus {
  transform: none;
}

/* (docs mirror — identical to llms.txt · Print & export)
   WHEN TO USE: nothing, usually — every Facet page already prints
   ink-on-paper clean with the interface controls hidden. Reach for the
   roles only to override one element (a letterhead that prints ONLY, a
   hint that never prints), for .page-break, or to give visitors a
   Save-as-PDF button.

   Printing is a first-class feature. Three roles name what belongs on
   paper: data-print="off" (alias .print-hide) never prints — page
   furniture; data-print="on" (.print-show) always prints, overriding a
   component that hides by default; data-print="only" (.print-only)
   prints ONLY — letterheads, "printed from" lines — invisible on screen.
   Interface controls (navs, the tab bar, float buttons, sheets, toasts,
   tooltips, gauges) are OFF by default; content (main, articles, cards,
   tables, figures) is ON — the content IS the paper. The paper
   stylesheet neutralises every theme to ink on paper, strips shadows and
   blur, unrolls the app shells (snap pages and view stacks print every
   screen in order), keeps headings with their text and avoids tearing
   cards, rows and figures across sheets. .page-break starts an element
   on a fresh sheet. .print-urls on <body> writes each external link's
   URL after it (off by default — noisy). facet.js opens every <details>
   on beforeprint and restores them after, and forces lazy images to
   load. Any button carrying data-print-action="page" prints the page —
   the Save-as-PDF affordance; give it data-event="export-pdf" for
   analytics. Fixed interface controls are user-select: none and are
   authored at the end of <body>, so a top-to-bottom selection sweeps the
   content in reading order and a full-page copy contains no control
   labels. @page margins default to 12mm; a template may override
   (the deck keeps its exact 1920x1080 sheet, the documents their A4).

       <button class="btn" data-print-action="page"
               data-event="export-pdf">Save as PDF</button>
       <header class="letterhead" data-print="only">Acme Ltd — invoice</header>
       <p class="badge" data-print="off">Screen-only hint</p> */
/* ----- PRINT & EXPORT: declarative print roles + the paper stylesheet -----
   What it is: printing as a first-class feature. Three roles name what
   belongs on paper: data-print="off" (alias .print-hide) never prints —
   page furniture; data-print="on" (alias .print-show) always prints,
   overriding a component that hides by default; data-print="only"
   (alias .print-only) prints ONLY — letterheads, "printed from" lines —
   invisible on screen. Interface controls (navs, the tab bar, float
   buttons, sheets, toasts, tooltips, gauges, scrims) are OFF by
   default; content (main, articles, cards, tables, figures) is ON —
   the content IS the paper. The paper stylesheet neutralises every
   theme to ink on paper, strips shadows and blur, unrolls the app
   shells (snap pages and view stacks print all their screens in
   order), keeps headings with their text and avoids tearing cards,
   rows and figures across sheets; .page-break starts an element on a
   fresh sheet; .print-urls on <body> writes each external link's URL
   after it; and facet.js opens every <details> on beforeprint and
   wires any button carrying data-print-action="page" to print the
   page — the Save-as-PDF affordance (give it data-event="export-pdf").
   What it is for: pages that print cleanly and export to PDF without a
   print-specific rewrite. When to use it: the defaults cover whole
   pages already; reach for a role only to override one element.
   (The ink-on-paper values here are a sanctioned raw-hex exception:
   paper has no theme and no dark mode.) */

/* The print-only role hides on screen (important: it must beat any
   component's own display, whatever the specificity)… */
[data-print="only"], .print-only { display: none !important; }

/* The reading-order and copy law, on screen: fixed interface controls
   are not content — they never land in a selection sweep or a copy.
   (They are also authored at the end of <body>, so DOM order stays
   reading order; layout never reorders content.) */
.tab-bar, .float-btn, .float-btn-right,
.scroll-gauge, .scroll-gauge-page,
.sheet, .sheet-scrim, .toast-rack {
  user-select: none;
  -webkit-user-select: none;
}

/* Controls are not content, part two: a tap, a drag or a long-press on
   a button, chip, tab, label or navigation row must never start a text
   selection — that is what makes a page feel like an app under a
   finger. Deliberately NOT page-wide: content (headings, paragraphs,
   lists, tables, code, results) stays fully selectable — a reader must
   always be able to copy what a page says; only the controls refuse. */
button, label, .btn,
[role="button"], [role="tab"], [role="tablist"],
.chip, .nav-link, .menu-item, .menu-label,
.breadcrumb, .pagination, .stepper, .choice-btn,
.nav-menu, .side-index {
  user-select: none;
  -webkit-user-select: none;
}
/* …but anything editable keeps its own selection (user-select inherits,
   and an input inside a label must never lose it). */
input, textarea, [contenteditable] {
  user-select: auto;
  -webkit-user-select: auto;
}

@media print {
  /* …and shows on paper, at its element's natural display */
  [data-print="only"], .print-only { display: revert !important; }

  /* ink on paper: no theme, no dark mode, no elevation */
  :root, [data-mode="dark"] {
    --background: #FFFFFF;
    --surface: #FFFFFF;
    --surface-hover: #FFFFFF;
    --surface-active: #F2F2F2;
    --text: #111111;
    --text-muted: #444444;
    --text-subtle: #666666;
    --border: #BBBBBB;
    --border-subtle: #CCCCCC;
    --border-strong: #999999;
    --shadow-flat: none;
    --shadow-raised: none;
    --shadow-floating: none;
    --shadow-overlay: none;
  }
  * {
    box-shadow: none !important;
    text-shadow: none !important;
    backdrop-filter: none !important;
  }
  .bg-grid, .bg-dots, .bg-circles, .bg-ruled, .bg-graph, [data-bg-glyph] { background-image: none !important; }
  .bg-fluid-layer { display: none !important; }

  /* page furniture never prints; an explicit on-role overrides */
  nav, .skip-link, .top-menu, .bg-fixed,
  .tab-bar, .float-btn, .float-btn-right,
  .sheet, .sheet-scrim, .nav-menu,
  .scroll-gauge, .scroll-gauge-page,
  .toast-rack, .nudge-scrim, .overlay-guide,
  [data-print="off"], .print-hide, [data-no-print] { display: none !important; }
  [data-print="on"], .print-show { display: revert !important; }

  /* tooltips are screen furniture */
  [data-tip]::before, [data-tip]::after { display: none !important; }

  /* the app shells unroll: every screen prints, in reading order */
  .is-app-shell, .is-app-shell body { height: auto; overflow: visible; }
  .snap, .snap.is-paged { height: auto; overflow: visible; scroll-snap-type: none; }
  .snap-section, .snap.is-paged .snap-section { height: auto; min-height: 0; overflow: visible; }
  .view-stack.is-stacked { height: auto; overflow: visible; }
  .view-stack.is-stacked .view {
    position: static;
    visibility: visible;
    height: auto;
    overflow: visible;
  }

  /* breaks: nothing orphans, nothing tears */
  .card, .list-item, tr, figure, .result, pre, .field { break-inside: avoid; }
  h1, h2, h3, h4, h5, h6 { break-after: avoid; }
  .page-break { break-before: page; }

  /* links print as ink; .print-urls on <body> spells them out */
  a { color: inherit; }
  .print-urls a[href^="http"]::after {
    content: " (" attr(href) ")";
    font-size: 0.85em;
    color: #444444;
  }
}
/* Sane paper margins; a template may override with its own @page
   (the deck keeps its exact 1920×1080 sheet, the documents their A4). */
@page { margin: 12mm; }

/* ----- Reduced motion ----- */
/* Every transition and animation in the library collapses to nothing. */

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    transition-duration: 0.01ms !important;
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    scroll-behavior: auto !important;
  }
}


/* ==========================================================================
   4. COMPONENTS
   One section per component. Each section opens with the component's
   canonical description — the same text, word for word, as its entry on
   the library site and in llms.txt — then how to use it and its options.
   ========================================================================== */

/* --------------------------------------------------------------------------
   CONTAINER

   (docs mirror — identical to llms.txt · Container)
   WHEN TO USE: on the top-level semantic tag of every ordinary page — it is the page's horizontal rhythm. Pick the width by the content: narrow for prose and forms, default for most pages, wide for dashboards. Never nest one inside another.

   What it is: a page-width wrapper with themed padding. It centers content,
   caps its width, and keeps consistent side padding from the spacing tokens.
   What it is for: giving every page the same horizontal rhythm, driven by
   tokens, so a theme flip restyles the layout too, not just the components
   inside it. When to use it: put it directly on the top-level semantic tag of
   any ordinary page — main, header, footer, section. Use .container-narrow
   for prose, forms and calculators, .container for most pages,
   .container-wide for dashboards, .container-xwide for ultrawide screens,
   .container-full for edge-to-edge strips that keep side padding. Never
   nest a container inside a container. The page can also choose its width
   ONCE: data-width="narrow|wide|xwide|full" on <html> re-maps what the
   standard .container means (also via facet.set({width: "wide"}) or the
   script tag), so the whole page widens and everything resizes; the
   suffixed containers stay absolute.

   Usage:  <main class="container"> ... </main>
   Options: .container-narrow  40rem   .container       64rem
            .container-wide    80rem   .container-xwide 96rem
            .container-full    edge to edge
            data-width on html: narrow | wide | xwide | full
   -------------------------------------------------------------------------- */

.container,
.container-narrow,
.container-wide,
.container-full {
  width: 100%;
  margin-inline: auto;
  padding-inline: var(--space-stack);
}

.container { max-width: var(--width-page, var(--width-default)); }
.container-narrow { max-width: var(--width-narrow); }
.container-wide { max-width: var(--width-wide); }
.container-xwide { max-width: var(--width-xwide); }

/* Page width, chosen once: data-width on <html> (or any subtree) re-maps
   what the standard .container means, so one attribute widens or narrows
   the whole page and everything inside resizes. The suffixed containers
   stay absolute — an explicit .container-wide means wide whatever the
   page says. --width-page can also be set directly to any measure. */
[data-width="narrow"] { --width-page: var(--width-narrow); }
[data-width="wide"]   { --width-page: var(--width-wide); }
[data-width="xwide"]  { --width-page: var(--width-xwide); }
[data-width="full"]   { --width-page: none; }

@media (min-width: 640px) {
  .container,
  .container-narrow,
  .container-wide,
  .container-full {
    padding-inline: var(--space-page);
  }
}

/* --------------------------------------------------------------------------
   STACK

   (docs mirror — identical to llms.txt · Stack)
   WHEN TO USE: for app-shaped layout — controls, cards, form rows — where children should sit a fixed gap apart. Never for prose meant to read as writing; that sits in normal flow (see the rhythm note below).

   What it is: vertical flow with themed gaps. A stack says one thing: these
   sit above and below each other, this far apart. What it is for: spacing
   content without per-element margins. The gaps come from the spacing tokens,
   so a theme change restyles the rhythm of the whole page. When to use it:
   .stack is the default for content inside a section. Use .stack-tight for
   lines that belong together, like a label and its field. Use .stack-loose
   between whole sections of a page.

   Usage:  <section class="stack"> ... </section>
   Options: .stack-tight  --space-control    .stack  --space-stack
            .stack-loose  --space-section
   -------------------------------------------------------------------------- */

.stack,
.stack-tight,
.stack-loose {
  display: flex;
  flex-direction: column;
  gap: var(--space-stack);
}

.stack-tight { gap: var(--space-control); }
.stack-loose { gap: var(--space-section); }

/* --------------------------------------------------------------------------
   SNAP SECTION

   (docs mirror — identical to llms.txt · Snap section)
   WHEN TO USE: a single-purpose page that tells one story one screen at a time — a calculator's intro, inputs, results. Not for documents (they just scroll), and not for apps with real screen-to-screen navigation (that is the view stack).

   What it is: full-viewport scroll-snap areas. The .snap wrapper scrolls one
   full screen at a time and each .snap-section centers its content at a
   readable width. What it is for: the calculator page skeleton — intro
   screen, then inputs, then results — and any page that tells one story
   one screen at a time. When to use it: single-purpose pages with a clear
   screen-by-screen flow. Not for ordinary documents; normal pages just
   scroll. The .snap wrapper exists to enable scrolling, which is the one
   reason the wrapper law allows a container inside a container.

   Usage:  <main class="snap">
             <section class="snap-section"> intro </section>
             <section class="snap-section"> inputs </section>
             <section class="snap-section"> results </section>
           </main>
   -------------------------------------------------------------------------- */

.snap {
  height: 100dvh;
  overflow-y: auto;
  scroll-snap-type: y mandatory;
  scroll-behavior: smooth;
}

.snap-section {
  min-height: 100dvh;
  scroll-snap-align: start;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: var(--space-card);
  padding: var(--space-page) var(--space-stack);
}

/* Keep content inside a snap section at a readable width, like a container. */
.snap-section > * {
  width: 100%;
  max-width: var(--width-narrow);
  margin-inline: auto;
}

/* THE PAGER MODEL (.is-paged, added by facet.js on full-viewport snaps).
   The law, learned the hard way on iOS: CSS mandatory snap re-snaps any
   section taller than the viewport and wedges the next gesture;
   proximity doesn't snap; free-scrolling outers fight their sections.
   So with JS present the outer NEVER free-scrolls — facet.js springs
   its scrollTop between section tops — and every section is exactly one
   viewport tall, scrolling natively inside itself. Without JS the plain
   CSS snapping above still works. */
.snap.is-paged {
  scroll-snap-type: none;
  overflow-y: hidden;
  overflow-x: hidden;
  scroll-behavior: auto;        /* the pager sets scrollTop per frame */
  overscroll-behavior-y: contain;
  scrollbar-width: none;
}
.snap.is-paged::-webkit-scrollbar { display: none; }
.snap.is-paged,
.snap.is-paged .snap-section { touch-action: pan-y; }
.snap.is-paged .snap-section {
  height: 100dvh;
  min-height: 100dvh;
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: none;
  justify-content: flex-start;  /* centering via auto margins below: a
                                   centered flex column would clip the
                                   top of overflowing content */
  /* More room top and bottom than a flat section, plus clearance so
     centered content never falls behind the fixed .nav-menu (bottom-left)
     on a short viewport. Safe-area insets keep it clear of notches/home bars. */
  padding-top: calc(var(--space-section) + var(--safe-top));
  padding-bottom: calc(var(--space-section) + var(--nav-menu-clearance, 3.75rem) + var(--safe-bottom));
}
.snap.is-paged .snap-section::-webkit-scrollbar { display: none; }
.snap.is-paged .snap-section > :first-child { margin-top: auto; }
.snap.is-paged .snap-section > :last-child { margin-bottom: auto; }
/* A hairline at each boundary. Each paged section is exactly 100dvh and
   border-box, so the border sits at the viewport's bottom edge when that
   section is snapped — the page bottom and the divider coincide. */
.snap.is-paged .snap-section:not(:last-child) {
  border-bottom: var(--border-hairline) solid var(--border);
}

/* --------------------------------------------------------------------------
   VIEW STACK

   (docs mirror — identical to llms.txt · View stack)
   WHEN TO USE: an app with real screens and a working Back button — home, question, result. One per page, as the page's <main>. For a one-story scroll, use the snap shell instead.

   What it is: the app shell for apps with real screens — a <main
   class="view-stack"> of .view sections where exactly one shows at a
   time, each scrolling on its own, content centered by auto margins when
   short. Navigation is plain hash links: any <a href="#result"> switches
   views, facet.views.go(id) and .back() do it from code, the URL always
   names the screen, and the browser's own Back walks the trail (back()
   falls home to the first view when there is nothing left to pop). Every
   view pads itself clear of the Dynamic Island (safe-area top) and of
   the home bar plus the nav menu (safe-area bottom + clearance); opt a
   view out with .bleed-top. What it is for: screen-to-screen apps — home
   → question → result → leaderboard — where the paged .snap shell (one
   long page you page through) is the wrong shape. When to use it: one
   view-stack per page, as the page's <main>, one .view per screen with a
   real id. facet.js upgrades it (.is-stacked, like .snap → .is-paged):
   html gets .is-app-shell, the page locks to the viewport, one view
   shows. With JS off the screens simply stack in reading order and the
   links jump between them — everything stays crawlable. Views keep their
   scroll positions while hidden, so Back returns exactly where the
   visitor left. Each arrival fires a bubbling "facet:view" CustomEvent
   ({view, from}) on the stack and moves focus to the view.

   Usage:  <main class="view-stack">
             <section class="view" id="home"> … <a href="#play">Play</a> </section>
             <section class="view" id="play"> … </section>
             <section class="view" id="result"> … </section>
           </main>
   -------------------------------------------------------------------------- */

/* Without JS: ordinary sections in flow, readable top to bottom. */
.view {
  padding: var(--space-page) var(--space-stack);
}
.view > * {
  width: 100%;
  max-width: var(--width-narrow);
  margin-inline: auto;
}

/* With JS (.is-stacked): the shell locks to the viewport and each view
   becomes its own scroll container. html gets .is-app-shell so the page
   itself never scrolls or rubber-bands behind the views. */
.is-app-shell,
.is-app-shell body {
  height: 100%;
  overflow: hidden;
  overscroll-behavior: none;
}
.view-stack.is-stacked {
  position: relative;
  height: 100dvh;
  overflow: hidden;
}
.view-stack.is-stacked .view {
  position: absolute;
  inset: 0;
  visibility: hidden;               /* not display:none — keeps each view's scroll position */
  display: flex;
  flex-direction: column;
  overflow-y: auto;
  overscroll-behavior: contain;
  touch-action: pan-y;              /* the gesture law: scrollers own their touches */
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  justify-content: flex-start;      /* centering via auto margins below: a centered
                                       flex column would clip overflowing content */
  /* Clear of the island above; clear of the home bar and the fixed
     .nav-menu below — the same guarantee as the paged snap shell. */
  padding-top: calc(var(--space-page) + var(--safe-top));
  padding-bottom: calc(var(--space-page) + var(--nav-menu-clearance, 3.75rem) + var(--safe-bottom));
}
.view-stack.is-stacked .view::-webkit-scrollbar { display: none; }
.view-stack.is-stacked .view > :first-child { margin-top: auto; }
.view-stack.is-stacked .view > :last-child { margin-bottom: auto; }
.view-stack.is-stacked .view.is-active {
  visibility: visible;
  animation: facet-view-in var(--duration-normal) var(--ease-spring);
}
@keyframes facet-view-in { from { opacity: 0; translate: 0 10px; } }

/* --------------------------------------------------------------------------
   BUTTON

   (docs mirror — identical to llms.txt · Button)
   WHEN TO USE: any click that does something. At most one primary per screen; navigation dressed as a button is an <a class="btn">.

   What it is: the action element, in three variants and three sizes, working
   identically on <button> and on <a>. What it is for: .btn-primary carries
   the screen's one main action in accent-1. .btn-secondary is every other
   action, on the accent-2 fill. .btn-ghost is for quiet actions inside busy
   areas, invisible until hovered. When to use it: any click that does something. One primary
   per screen at most. Navigation styled as a button is an <a class="btn">.
   Put the button's key action name in a data-event attribute as the
   analytics hook.

   Usage:  <button class="btn btn-primary" data-event="calc-run"
                   data-tip="Runs the calculation">Calculate</button>
   Variants: .btn-primary  .btn-secondary (default for bare .btn)  .btn-ghost
   Sizes:    .btn-small  .btn (default)  .btn-large
   States:   hover, focus ring, pressed, :disabled — all built in.
             aria-disabled="true" disables an <a class="btn"> the same way;
             aria-busy="true" shows a working spinner and blocks re-clicks
             (see the STATE GRAMMAR section).
   -------------------------------------------------------------------------- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-control);
  min-height: 44px;                       /* touch target */
  padding: var(--space-control) var(--space-card);
  border: var(--border-element) solid var(--border);
  border-radius: var(--radius-control);
  background: var(--accent-2);
  color: var(--on-accent-2);
  font-weight: var(--weight-medium);
  line-height: var(--leading-ui);
  letter-spacing: var(--tracking-label);
  text-decoration: none;                  /* .btn also works on <a> */
  transition:
    background var(--duration-fast) var(--ease-out),
    border-color var(--duration-fast) var(--ease-out),
    transform var(--duration-normal) var(--ease-spring);
}

/* Press with weight; release springs back with the motion setting. */
.btn:active { transform: translateY(1px) scale(0.97); }

/* Primary: filled with accent-1. One per screen. */
.btn-primary {
  background: var(--accent-1);
  border-color: var(--accent-1);
  color: var(--on-accent-1);
  box-shadow: var(--shadow-raised);
}

.btn-primary:hover { background: var(--accent-1-hover); border-color: var(--accent-1-hover); }
.btn-primary:active { background: var(--accent-1-pressed); border-color: var(--accent-1-pressed); }

/* Secondary: the accent-2 fill with a hairline. The default look. */
.btn-secondary:hover,
.btn:not(.btn-primary):not(.btn-ghost):hover {
  background: var(--accent-2-hover);
  border-color: var(--text-muted);
}

.btn-secondary:active,
.btn:not(.btn-primary):not(.btn-ghost):active {
  background: var(--accent-2-pressed);
}

/* Ghost: no fill, no border, until hovered. */
.btn-ghost {
  background: transparent;
  border-color: transparent;
}

.btn-ghost:hover {
  background: var(--accent-2);
  border-color: var(--border);
}

.btn-ghost:active {
  background: var(--accent-2-hover);
}

/* Sizes. Small keeps the 44px touch target via min-height on mobile. */
.btn-small {
  padding: var(--space-tight) var(--space-inline);
  font-size: var(--text-footnote);
  min-height: 36px;
}

.btn-large {
  padding: var(--space-inline) var(--space-page);
  font-size: var(--text-h4);
}

@media (pointer: coarse) {
  .btn-small { min-height: 44px; }        /* fingers get the full target */
}

/* Pill: the button becomes a fully-rounded capsule. The base .btn is
   already an inline-flex row of gap-separated children, so an inline
   <svg> icon plus a text label sit side by side with no extra rule —
   this modifier only rounds the ends. The nav menu is built entirely
   from .btn.btn-pill (its Menu trigger, its Settings button as
   .btn-pill.btn-icon, and every nav link), so any change to the base
   button — padding, height, weight, press physics — flows straight
   into the menu. Combines with every variant and size. */
.btn-pill { border-radius: var(--radius-pill); }

/* --------------------------------------------------------------------------
   ICON BUTTON

   (docs mirror — identical to llms.txt · Icon button)
   WHEN TO USE: compact, secondary actions where an icon plus its tooltip is enough — close, clear, share. Always with an aria-label.

   What it is: a square button holding a single icon. What it is for: compact
   actions whose meaning the icon plus its tooltip carries — the clear cross
   in a number field, a close button, a share arrow. When to use it: when
   space is tight and the action is secondary. The icon alone says nothing, so
   every icon button carries an aria-label for screen readers and a data-tip
   tooltip for everyone else. Combines with any button variant and size.
   Icons are inline SVG with stroke="currentColor" so they take the button's
   text color in every theme.

   Usage:  <button class="btn btn-icon" aria-label="Close"
                   data-tip="Close this panel"> <svg>...</svg> </button>
   -------------------------------------------------------------------------- */

.btn-icon {
  width: 44px;
  min-width: 44px;
  padding: 0;
}

.btn-icon.btn-small { width: 36px; min-width: 36px; }
.btn-icon.btn-large { width: 56px; min-width: 56px; }

/* --------------------------------------------------------------------------
   FIELD + NUMBER INPUT

   (docs mirror — identical to llms.txt · Number input)
   WHEN TO USE: every numeric entry in a calculator or money product — it groups digits for the visitor's locale as they type and reads the number back in words.

   What it is: a labelled field wrapping the calculator-grade number input,
   with a clear button inside the field and a words helper under it. What it
   is for: money and other large numbers, entered the way the visitor says
   them. The input ships prefilled with an example value, clears fully on
   the first tap — and never again once the visitor has typed — groups
   digits for the locale as they are typed (25,00,000 where lakh and crore
   are the norm, 2,500,000 elsewhere), and reads the number back in words
   underneath, in the active language: 25 lakh, 2.5 million, 25 लाख. The
   locale follows facet.location and the browser, overridable with
   data-numbers="indian|western"; the language with data-language. All
   behaviours come from facet.js the moment the input carries data-number.
   When to use it: every numeric entry in a calculator. The field shows the
   grouped text; read the plain number in app code by stripping separators —
   parseFloat(input.value.replace(/[^0-9.]/g, "")). For the error state,
   set data-status="error" on the field and swap the hint for a
   .field-msg line (.field-invalid + .field-error stay as aliases).

   Usage:
     <label class="field">
       <span class="field-label">Monthly investment</span>
       <span class="number-input">
         <input type="text" inputmode="numeric" value="25000" data-number>
         <button type="button" class="btn btn-ghost btn-icon btn-small number-clear"
                 aria-label="Clear" data-tip="Clear this field">&times;</button>
       </span>
       <span class="number-words" aria-live="polite"></span>
       <span class="field-hint">What you invest every month</span>
     </label>

   The .number-input wrapper exists to overlay the clear button on the
   input — a real layout job, allowed by the wrapper law.
   -------------------------------------------------------------------------- */

/* (docs mirror — identical to llms.txt · Fields: input, textarea, select, search)
   WHEN TO USE: any time a control needs a name — which is always. One pattern wraps every kind of control.

   What it is: the labelled field family — one pattern (label.field >
   .field-label + control + .field-hint) wrapped around every kind of
   control: text input, textarea, select (with a token-colored chevron,
   no image files), and a capsule search field. What it is for: every
   form. The label is real, so clicking it focuses the control and
   readers announce it; for validation set
   data-status="error|warning|success" on the field and swap the hint
   for a .field-msg line (.field-invalid + .field-error stay as aliases
   — see Component states). When to use it: any time a control needs a
   name — which is always. */
.field {
  display: flex;
  flex-direction: column;
  gap: var(--space-tight);
}

.field-label {
  font-size: var(--text-footnote);
  font-weight: var(--weight-strong);
}

.field-hint {
  font-size: var(--text-caption);
  color: var(--text-muted);
  font-weight: var(--weight-body);
}

/* The error line expands and fades in the moment it is added, so a
   validation message reveals itself instead of snapping the field taller.
   The animation plays on insertion; the max-block-size ceiling only shapes
   the ramp and is released once it finishes, so a long message never clips. */
.field-error {
  font-size: var(--text-caption);
  color: var(--error);
  font-weight: var(--weight-body);
  animation: facet-reveal var(--duration-normal) var(--ease-out);
}
@keyframes facet-reveal {
  from { opacity: 0; max-block-size: 0; }
  to   { opacity: 1; max-block-size: 4rem; }
}

.field-invalid input,
.field-invalid textarea,
.field-invalid select {
  border-color: var(--error);
}

/* Select shows a token-colored chevron (two small gradients — no
   image files, ever) once the native arrow is dropped. */
select {
  appearance: none;
  padding-right: var(--space-section);
  background-image:
    linear-gradient(45deg, transparent 50%, var(--text-muted) 50%),
    linear-gradient(135deg, var(--text-muted) 50%, transparent 50%);
  background-position:
    calc(100% - 1.15rem) 50%,
    calc(100% - 0.8rem) 50%;
  background-size: 0.35rem 0.35rem;
  background-repeat: no-repeat;
}

/* Search reads as search: a capsule, native clear button kept. */
input[type="search"] { border-radius: var(--radius-pill); }

/* ----- FILE UPLOAD & DATE INPUT: native controls, themed -----
   (docs mirror — identical to llms.txt · File upload & date input)
   WHEN TO USE: always these before any custom uploader or calendar — native means drag-drop, keyboard, the picker and the form value all work for free, in every theme.

   What it is: the native file and date controls, themed and kept native
   — the file input's button is styled as a secondary action while
   drag-drop, keyboard and the form value stay the browser's; the date
   input keeps its native picker with the accent coloring its highlight
   and color-scheme flipping it in dark mode. When to use it: always
   these before any custom uploader or calendar, wrapped in the same
   label.field pattern as every other control.*/

/* File upload: the native input stays (drag-drop, keyboard, form
   value all free); only its button is styled like a secondary action
   and the filename text steps back. */
input[type="file"] {
  border: none;
  background: none;
  padding: var(--space-tight) 0;
  font-size: var(--text-footnote);
  color: var(--text-muted);
  cursor: pointer;
}
input[type="file"]::file-selector-button {
  margin-right: var(--space-inline);
  padding: var(--space-control) var(--space-stack);
  border: var(--border-element) solid var(--border);
  border-radius: var(--radius-panel);
  background: var(--accent-2);
  color: var(--on-accent-2);
  font: inherit;
  font-weight: var(--weight-strong);
  cursor: pointer;
  transition: background var(--duration-fast) var(--ease-out);
}
input[type="file"]:hover::file-selector-button { background: var(--accent-2-hover); }

/* Date and time: native pickers, themed — accent-color colors the
   picker highlight, color-scheme already flips it in dark mode. */
input[type="date"],
input[type="time"],
input[type="datetime-local"],
input[type="month"],
input[type="week"] {
  accent-color: var(--accent-1);
}

/* The input + clear button overlay. */
.number-input {
  position: relative;
  display: block;
}

.number-input input {
  padding-right: var(--space-hero);          /* room for the clear button */
  font-size: var(--text-h4);              /* numbers need to be legible */
  font-variant-numeric: tabular-nums;     /* digits align while typing */
}

.number-input .number-clear {
  position: absolute;
  top: 50%;
  right: var(--space-tight);
  transform: translateY(-50%);
  color: var(--text-muted);
}

.number-input .number-clear:active { transform: translateY(-50%); }

/* Hide the clear button while the field is empty: nothing to clear. */
.number-input:has(input:placeholder-shown) .number-clear {
  display: none;
}

/* The words helper under the input: "25 lakh", "1.2 crore". */
.number-words {
  min-height: 1.2em;                      /* reserves the line: no layout jump */
  font-size: var(--text-caption);
  color: var(--accent-3);
  font-weight: var(--weight-strong);
}

/* --------------------------------------------------------------------------
   SLIDER

   (docs mirror — identical to llms.txt · Slider)
   WHEN TO USE: a bounded pick where the feel of more-and-less matters more than the exact digit — years, percentages. Pair it with the number input when precision matters.

   What it is: a real, themed range input — hairline track,
   accent-3 thumb — paired with a live value readout. What it is for: picking a number
   inside a known range where the feel of more-and-less matters more than
   exact digits: years, percentages, counts. When to use it: bounded inputs
   with sensible ends. When the exact number matters, pair it with or swap it
   for the number input. Put an <output> in the field label and facet.js
   keeps it showing the live value. Full keyboard support comes free with the
   native element.

   Usage:
     <label class="field">
       <span class="field-label">Years <output>10</output></span>
       <input type="range" class="slider" min="1" max="40" value="10"
              data-tip="Drag, or use the arrow keys">
     </label>
   -------------------------------------------------------------------------- */

.slider {
  appearance: none;
  -webkit-appearance: none;
  min-height: 44px;                       /* touch target around the track */
  padding: 0;
  background: transparent;
  border: none;
  cursor: pointer;
}

/* Track: a quiet hairline. */
.slider::-webkit-slider-runnable-track {
  height: 4px;
  background: var(--border);
  border-radius: var(--radius-pill);
}

.slider::-moz-range-track {
  height: 4px;
  background: var(--border);
  border-radius: var(--radius-pill);
}

/* Filled part of the track, Firefox only (WebKit has no equivalent). */
.slider::-moz-range-progress {
  height: 4px;
  background: var(--accent-3);
  border-radius: var(--radius-pill);
}

/* Thumb: accent circle, grows slightly on hover. */
.slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 24px;
  height: 24px;
  margin-top: -10px;                      /* centers on the 4px track */
  background: var(--accent-3);
  border: var(--border-highlight) solid var(--surface);
  border-radius: var(--radius-pill);
  box-shadow: var(--shadow-raised);
  transition: transform var(--duration-normal) var(--ease-spring);
}

.slider::-moz-range-thumb {
  width: 24px;
  height: 24px;
  background: var(--accent-3);
  border: var(--border-highlight) solid var(--surface);
  border-radius: var(--radius-pill);
  box-shadow: var(--shadow-raised);
  transition: transform var(--duration-normal) var(--ease-spring);
}

.slider:hover::-webkit-slider-thumb { transform: scale(1.15); }
.slider:hover::-moz-range-thumb { transform: scale(1.15); }
.slider:active::-webkit-slider-thumb { background: var(--accent-3-pressed); }
.slider:active::-moz-range-thumb { background: var(--accent-3-pressed); }

/* The live value readout inside the field label. */
.field output {
  float: right;                           /* sits at the label's right edge */
  font-variant-numeric: tabular-nums;
  color: var(--text-muted);
}

/* --------------------------------------------------------------------------
   DETAILED SLIDER

   (docs mirror — identical to llms.txt · Detailed slider)
   WHEN TO USE: a range too wide for a plain slider to land exactly — a price, a duration in seconds, a token in pixels. Fast coarse drag plus precise fine entry, always in sync.

   What it is: a slider for a wide range, paired with a precise entry. The
   track drags fast in coarse jumps (data-coarse) so you cross a big span in
   one swipe; the −/+ steppers and the number box reach the exact value
   between the jumps (data-fine). All three stay in sync. What it is for:
   values with many gradations where you want both speed and precision — a
   price, a duration in seconds, a token in pixels. When to use it: ranges
   too wide for a plain slider to land precisely. For a small bounded range,
   the plain .slider is enough. facet.js wires it and fires a bubbling
   facet:slide {value} on any change.

   Usage:
     <div class="slider-detail" data-min="0" data-max="1000"
          data-coarse="20" data-fine="1">
       <input type="range" class="slider" min="0" max="1000" step="1"
              value="200" aria-label="Price">
       <div class="slider-detail-entry">
         <button type="button" class="btn btn-icon slider-detail-step"
                 data-step="-1" aria-label="Decrease">&minus;</button>
         <input type="number" class="slider-detail-number" min="0" max="1000"
                value="200" aria-label="Exact price">
         <button type="button" class="btn btn-icon slider-detail-step"
                 data-step="1" aria-label="Increase">+</button>
       </div>
     </div>
   facet.js wires it and fires a bubbling facet:slide {value} on any change.
   -------------------------------------------------------------------------- */
.slider-detail {
  display: flex;
  align-items: center;
  gap: var(--space-inline);
  flex-wrap: wrap;
}
.slider-detail .slider { flex: 1 1 8rem; min-width: 6rem; }
.slider-detail-entry {
  display: inline-flex;
  align-items: center;
  gap: var(--space-tight);
}
.slider-detail-number {
  width: 4.5rem;
  text-align: center;
  font-variant-numeric: tabular-nums;
  /* the −/+ buttons replace the native spinner */
  -moz-appearance: textfield;
}
.slider-detail-number::-webkit-inner-spin-button,
.slider-detail-number::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
.slider-detail-step { font-size: var(--text-h4); line-height: 1; }

/* --------------------------------------------------------------------------
   RESULT BLOCK

   (docs mirror — identical to llms.txt · Result block)
   WHEN TO USE: whenever a calculation produces one headline answer — a verdict a person could repeat at dinner, then the number, then the supporting figures.

   What it is: a verdict-first result card: one plain-language sentence a
   person can repeat in conversation, one large number, then the supporting
   figures.
   What it is for: the centrepiece of a results screen — readable at a
   glance, and announced by screen readers on every recalculation when the
   block carries aria-live="polite". When to use it: whenever a calculation
   produces one headline answer. Leave out the figures list when there is
   nothing to support the verdict. For the empty state before any input, show
   a muted verdict line alone.

   Usage:
     <section class="result" aria-live="polite">
       <p class="result-verdict">Your money halves in value every 10 years</p>
       <p class="result-number">&#8377;46,319</p>
       <dl class="result-figures">
         <div><dt>Total invested</dt><dd>&#8377;12,00,000</dd></div>
         <div><dt>Real return</dt><dd>4.1% a year</dd></div>
       </dl>
     </section>

   The <dl> is the semantic fit for label–value pairs; each <div> row is
   grouping the dl spec itself provides, not decoration.
   -------------------------------------------------------------------------- */

.result {
  background: var(--surface);
  border: var(--border-element) solid var(--border);
  border-radius: var(--radius-panel);
  box-shadow: var(--shadow-raised);
  padding: var(--space-page);
  display: flex;
  flex-direction: column;
  gap: var(--space-stack);
  text-align: center;
}

.result-verdict {
  font-size: var(--text-h3);
  font-weight: var(--weight-strong);
  margin-inline: auto;
  text-wrap: balance;
}

.result-number {
  font-family: var(--font-numeric);       /* the big-number face, standing on its own */
  font-size: var(--text-display);
  font-weight: var(--weight-heading);
  line-height: var(--leading-display);
  color: var(--text);
  font-variant-numeric: tabular-nums lining-nums;
  letter-spacing: var(--tracking-display);
}

.result-figures {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-control) var(--space-page);
  border-top: var(--border-hairline) solid var(--border);
  padding-top: var(--space-stack);
}

.result-figures dt {
  font-size: var(--text-caption);
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
}

.result-figures dd {
  margin: 0;
  font-size: var(--text-h4);
  font-weight: var(--weight-strong);
  font-variant-numeric: tabular-nums;
}

/* --------------------------------------------------------------------------
   DESCRIPTION TOOLTIP

   (docs mirror — identical to llms.txt · Description tooltip)
   WHEN TO USE: on every interactive element — one data-tip attribute explains it in place. This is the library's everything-explained-in-place promise; write one for every control you ship.

   What it is: a one-attribute explanation bubble: put data-tip on any element
   and the text appears above it on hover and on keyboard focus. On touch
   screens a tap opens it instead — placed above, below or beside the
   element, wherever there is room, always inside the viewport, with a
   tail pointing at it — and it stays until the page scrolls, a tap
   lands anywhere else, or the same element is tapped again. A control that opens something else (a sheet, a menu, a dialog) acts without pinning a bubble over what it opened. A tablet's
   trackpad cursor shows it on hover too. The tap still performs the element's own
   action; the bubble explains, it never blocks. What it is for: explaining
   every interactive element in place — one of the library's core
   principles. The description doubles as documentation anyone can read by
   pointing at the thing. When to use it: every interactive element ships
   with one. Add data-tip-below on elements near the top of the viewport so
   the hover bubble drops under them instead of clipping off the screen.

   Usage:  <button class="btn" data-tip="Runs the calculation">Calculate</button>

   Limits: the hover bubble can be clipped by overflow:hidden ancestors;
   the touch bubble (initTouchTooltips in facet.js) floats free of any
   clipping and places itself by available space.
   -------------------------------------------------------------------------- */

[data-tip] {
  position: relative;
}

[data-tip]::after {
  content: attr(data-tip);
  position: absolute;
  bottom: calc(100% + var(--space-control));
  left: 50%;
  translate: -50% 2px;
  z-index: 10;
  width: max-content;
  max-width: 16rem;
  padding: var(--space-tight) var(--space-inline);
  background: var(--text);
  color: var(--background);
  font-size: var(--text-caption);
  font-weight: var(--weight-body);
  text-align: center;
  border-radius: var(--radius-control);
  box-shadow: var(--shadow-floating);
  /* Hidden bubbles leave layout entirely — an opacity-0 box hanging
     off a right-edge control would widen the whole page on phones. */
  display: none;
  opacity: 0;
  pointer-events: none;
  transition:
    opacity var(--duration-fast) var(--ease-out),
    translate var(--duration-normal) var(--ease-spring);
}

[data-tip]:hover::after,
[data-tip]:focus-visible::after {
  display: block;
  opacity: 1;
  translate: -50% 0;
}
/* The frame the bubble transitions FROM on the display flip. */
@starting-style {
  [data-tip]:hover::after,
  [data-tip]:focus-visible::after {
    opacity: 0;
    translate: -50% 2px;
  }
}

/* On touch screens the JS bubble owns the job — the hover/focus pseudo
   bubble would double up under the tap focus, so it stands down. */
@media (pointer: coarse) {
  [data-tip]:hover::after,
  [data-tip]:focus-visible::after,
  [data-tip]:hover::before,
  [data-tip]:focus-visible::before { display: none; }
}

/* The floating touch bubble (one per page, created by facet.js):
   the same skin as the hover bubble, positioned by script. */
.tip-bubble {
  position: fixed;
  z-index: 80;
  width: max-content;
  max-width: 16rem;
  padding: var(--space-tight) var(--space-inline);
  background: var(--text);
  color: var(--background);
  font-size: var(--text-caption);
  font-weight: var(--weight-body);
  text-align: center;
  border-radius: var(--radius-control);
  box-shadow: var(--shadow-floating);
  pointer-events: none;
  opacity: 0;
  translate: 0 2px;
  transition:
    opacity var(--duration-fast) var(--ease-out),
    translate var(--duration-normal) var(--ease-spring);
}
.tip-bubble.is-open { opacity: 1; translate: 0 0; }
/* The tail: a small triangle pointing back at the element, on the side
   facet.js placed the bubble (data-place above/below/left/right). */
.tip-bubble::after {
  content: "";
  position: absolute;
  width: 0;
  height: 0;
  border: 6px solid transparent;
}
.tip-bubble[data-place="above"]::after { top: 100%; left: 50%; translate: -50% 0; border-top-color: var(--text); }
.tip-bubble[data-place="below"]::after { bottom: 100%; left: 50%; translate: -50% 0; border-bottom-color: var(--text); }
.tip-bubble[data-place="left"]::after  { left: 100%; top: 50%; translate: 0 -50%; border-left-color: var(--text); }
.tip-bubble[data-place="right"]::after { right: 100%; top: 50%; translate: 0 -50%; border-right-color: var(--text); }

/* Elements near the top of the viewport add data-tip-below, so the tip
   drops under them instead of clipping off the screen. */
[data-tip][data-tip-below]::after {
  bottom: auto;
  top: calc(100% + var(--space-control));
}

/* The tail: a small triangle under the bubble, flipping with
   data-tip-below. */
[data-tip]::before {
  content: "";
  position: absolute;
  bottom: calc(100% + var(--space-control) - 5px);
  left: 50%;
  translate: -50% 2px;
  z-index: 10;
  border: 5px solid transparent;
  border-top-color: var(--text);
  display: none;
  opacity: 0;
  pointer-events: none;
  transition:
    opacity var(--duration-fast) var(--ease-out),
    translate var(--duration-fast) var(--ease-out);
}
[data-tip][data-tip-below]::before {
  bottom: auto;
  top: calc(100% + var(--space-control) - 5px);
  border-top-color: transparent;
  border-bottom-color: var(--text);
}
[data-tip]:hover::before,
[data-tip]:focus-visible::before { display: block; opacity: 1; translate: -50% 0; }
@starting-style {
  [data-tip]:hover::before,
  [data-tip]:focus-visible::before { opacity: 0; translate: -50% 2px; }
}

/* Touch: iOS applies a STICKY :hover on tap that never clears, so on
   touch devices the hover bubble is forced off — the tap bubble is the
   floating .tip-bubble that initTouchTooltips owns. */
@media (hover: none) {
  [data-tip]:hover::after,
  [data-tip]:hover::before { display: none; opacity: 0; }
}

/* ----- CHOICE CONTROLS: checkbox, radio, switch -----
   (docs mirror — identical to llms.txt · Switch, checkbox, radio)
   WHEN TO USE: checkbox for independent yes/nos, radio for one-of-few with every option visible, switch for a setting that takes effect the moment it flips.

   What it is: the yes/no and pick-one controls — native checkbox and
   radio themed through accent-color, and a switch that is a
   checkbox upgraded with one class. What it is for: settings, consents,
   filters — checkbox for independent yes/nos, radio for one-of-few with
   all options visible, switch for a setting that takes effect
   immediately. When to use it: wrap control and text in a
   label.check-row so the whole line is the hit target. The switch keeps
   checkbox semantics — form value, keyboard, screen readers — the class
   only changes the appearance.*/
input[type="checkbox"],
input[type="radio"] {
  accent-color: var(--accent-1);
  width: 1.15rem;
  height: 1.15rem;
  flex: none;
}

/* The whole line is the target: 44px tall, gap from the scale. */
.check-row {
  display: flex;
  align-items: center;
  gap: var(--space-inline);
  min-height: 2.75rem;
  cursor: pointer;
}
.check-row:has(input:disabled) { cursor: not-allowed; color: var(--text-muted); }

/* The switch: a checkbox styled as a track and thumb. */
input[type="checkbox"].switch {
  appearance: none;
  position: relative;
  width: 2.75rem;
  height: 1.625rem;
  min-height: 0;        /* the base input touch-target must not square the track — the .check-row gives the 44px target */
  padding: 0;
  border: none;
  border-radius: var(--radius-pill);
  background: var(--border);
  transition: background var(--duration-fast) var(--ease-out);
  touch-action: manipulation;
  cursor: pointer;
}
input[type="checkbox"].switch::before {
  content: "";
  position: absolute;
  top: 2px;
  left: 2px;
  width: calc(1.625rem - 4px);
  height: calc(1.625rem - 4px);
  border-radius: var(--radius-pill);
  background: var(--surface);        /* white knob pops against the gray track */
  box-shadow: var(--shadow-raised);
  transition: translate var(--duration-normal) var(--ease-spring);
}
input[type="checkbox"].switch:checked { background: var(--accent-1); }
input[type="checkbox"].switch:checked::before { translate: 1.125rem 0; }
input[type="checkbox"].switch:disabled { opacity: 0.5; cursor: not-allowed; }

/* ----- MODAL, DRAWER, POPOVER: the native top layer, styled -----
   (docs mirror — identical to llms.txt · Modal, drawer, popover)
   WHEN TO USE: a modal for a decision that blocks the page, a drawer for a side task that keeps context, a popover for a small anchored bite. All are the native elements, so focus trapping, Esc and the backdrop come free.

   What it is: overlays built on the platform's own machinery — modal and
   drawer are <dialog> (focus trap, Esc, inert page for free), popover is
   the popover attribute (top layer, light dismiss, no JS at all). What
   it is for: modal for a decision that blocks the page; drawer for a
   side panel of details or filters; popover for small floating bits.
   When to use it: open a dialog from any button whose aria-controls
   names it — facet.js calls showModal; close with a form method="dialog"
   button inside, Esc, or a backdrop click. Popovers need only
   popovertarget on the button. The app-kit .sheet stays the settings
   panel for app pages; the drawer is the document-world sibling.*/
/* A closed dialog must stay hidden even when a layout class on it (e.g.
   .stack, which sets display:flex) would otherwise beat the UA's
   dialog:not([open]) { display:none }. Without this, a <dialog class="modal
   stack"> renders in the flow while closed and its backdrop stacks up. */
dialog:not([open]) { display: none; }

dialog.modal,
dialog.drawer {
  border: none;
  padding: var(--space-page);
  background: var(--surface);
  color: var(--text);
  box-shadow: var(--shadow-overlay);
}
dialog.modal {
  width: min(92vw, 34rem);
  border-radius: var(--radius-card);
  margin: auto;                   /* centre it — the * { margin: 0 } reset
                                     wipes the UA's centring margin, so a
                                     modal without this pins to the corner */
}
dialog.drawer {
  margin: 0 0 0 auto;             /* pinned to the right edge */
  width: min(92vw, 24rem);
  height: 100dvh;
  max-height: 100dvh;
  border-radius: var(--radius-card) 0 0 var(--radius-card);
  /* full-height panel: its padding absorbs the island and the home bar */
  padding-top: calc(var(--space-page) + var(--safe-top));
  padding-bottom: calc(var(--space-page) + var(--safe-bottom));
}
dialog.modal::backdrop,
dialog.drawer::backdrop {
  background: rgb(0 0 0 / 0.5);
  backdrop-filter: blur(3px);
}
/* Arrive with the motion setting: the panel rises (modal) or slides in
   from the edge (drawer) while its backdrop fades up with it, so nothing
   hard-cuts onto the screen. Leave instantly — a closing <dialog> hits
   display:none before an exit could play. */
dialog.modal[open]  { animation: facet-modal-in  var(--duration-normal) var(--ease-spring); }
dialog.drawer[open] { animation: facet-drawer-in var(--duration-slow)   var(--ease-spring); }
dialog.modal[open]::backdrop,
dialog.drawer[open]::backdrop { animation: facet-fade-in var(--duration-normal) var(--ease-out); }
@keyframes facet-modal-in  { from { opacity: 0; translate: 0 20px; scale: 0.97; } }
@keyframes facet-drawer-in { from { translate: 100% 0; } }
@keyframes facet-fade-in   { from { opacity: 0; } }
@keyframes facet-pop-in    { from { opacity: 0; translate: 0 6px; scale: 0.97; } }

[popover] {
  margin: auto;                   /* centered fallback where no anchor */
  border: var(--border-element) solid var(--border);
  border-radius: var(--radius-panel);
  padding: var(--space-stack);
  background: var(--surface);
  color: var(--text);
  box-shadow: var(--shadow-floating);
}
[popover]:popover-open { animation: facet-pop-in var(--duration-fast) var(--ease-out); }
[popover]:popover-open::backdrop { animation: facet-fade-in var(--duration-fast) var(--ease-out); }
/* Layout utilities (display: flex/grid) must never beat the UA's
   closed-popover hiding — re-assert it at author origin. */
[popover]:not(:popover-open) { display: none; }

/* ----- BLOCK HELPERS: the few styles the block layer adds -----
   Blocks are assemblies of existing components — cards in grids,
   fields in forms, links in navs — documented as copyable patterns
   on the Library page. Most need no CSS at all; these are the handful of
   real jobs the assemblies add: a horizontal card scroller, the
   collapsing top menu, hero and CTA styles, stat cards, and the two
   editorial pieces (pull quote, media figure). */

/* Horizontal scrolling card row: side-scrolls with momentum, cards
   keep a readable width, gentle snap per card (proximity is safe
   here — cards never outgrow the viewport; the pager law is about
   full-page sections). */
.card-row {
  display: flex;
  gap: var(--space-stack);
  overflow-x: auto;
  padding-bottom: var(--space-control);   /* room for the scrollbar */
  scroll-snap-type: x proximity;
  touch-action: pan-x pan-y;
  -webkit-overflow-scrolling: touch;
}
.card-row > * {
  flex: 0 0 min(16rem, 78vw);
  scroll-snap-align: start;
}

/* Details label swap: inside any <details>, .when-closed shows only
   while shut and .when-open only while open — so a summary can say
   "Menu" shut and "Close" open with no JS. Generic; the top menu's
   fold below is the first user. */
details .when-open,
details[open] .when-closed { display: none; }
details .when-closed,
details[open] .when-open { display: inline-flex; align-items: center; gap: inherit; }

/* ----- SIDE INDEX: the sticky index panel beside long content -----
   (docs mirror — identical to llms.txt · Side index)
   WHEN TO USE: docs, references and settings hubs — one long or paged content column with a persistent map beside it.

   What it is: the index panel the library's own docs use — a sticky
   aside on a surface fill with a hairline and rounded corners, holding
   grouped link lists. Each .side-index-group opens with an uppercase
   name (make it an <a> to link the group's own page); groups divide with
   hairlines; .side-index-sub labels caption runs of links; the current
   link marks itself with aria-current="page" or .is-current and reads as
   a filled pill. The panel pins to the viewport — exactly 100dvh minus
   the --space-pin inset top and bottom — and scrolls its own content,
   never the page. When the marked link changes, scroll the panel so that
   link sits at the panel's own middle (links near the ends settle as
   close as the panel allows) — the panel scrolls, never the page. What
   it is for: docs, references, settings
   hubs — any page with one long or paged content column and a persistent
   map beside it. When to use it: put the aside and the content in a
   two-column grid (the grid wrapper is the page's layout job); on narrow
   screens stack it or hide the groups and keep a search box.

   Usage:
     <aside class="side-index" aria-label="Page index">
       <nav aria-label="Sections">
         <div class="side-index-group">
           <a class="side-index-name" href="#layer2">Components</a>
           <p class="side-index-sub">Forms &amp; controls</p>
           <ul>
             <li><a href="#button" aria-current="page">Button</a></li>
           </ul>
         </div>
       </nav>
     </aside>
*/
.side-index {
  position: sticky;
  top: var(--space-pin);
  height: calc(100dvh - 2 * var(--space-pin));   /* pinned = exact viewport fit, minus the pin inset */
  overflow-y: auto;
  overscroll-behavior: contain;
  display: flex;
  flex-direction: column;
  gap: var(--space-control);
  padding: var(--space-card) var(--space-inline);
  background: var(--surface);
  border: var(--border-hairline) solid var(--border);
  border-radius: var(--radius-card);
}
.side-index ul { list-style: none; padding: 0; margin: 0 0 var(--space-control); }
.side-index li + li { margin-top: 0; }   /* an app list, not prose: no flow rhythm between links */
.side-index a {
  display: block;
  padding: 0.4rem var(--space-inline);
  text-decoration: none;
  color: var(--text-muted);
  font-size: var(--text-footnote);
  border-radius: var(--radius-control);
  transition: background var(--duration-fast) var(--ease-out),
              color var(--duration-fast) var(--ease-out);
}
.side-index a:hover { color: var(--text); background: var(--surface-hover); }
.side-index a.is-current,
.side-index a[aria-current="page"] {
  color: var(--text);
  font-weight: var(--weight-strong);
  background: var(--surface-active);
}
.side-index-group { padding-block: var(--space-inline); }
.side-index-group + .side-index-group { border-top: var(--border-hairline) solid var(--border); }
.side-index a.side-index-name,
.side-index-name {
  display: block;
  font-family: var(--font-mono);   /* the eyebrow voice */
  font-size: var(--text-caption);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-weight: var(--weight-eyebrow);
  color: var(--text);              /* the category reads bright, its links muted */
  padding: var(--space-tight) var(--space-inline);
}
.side-index a.side-index-name:hover { color: var(--text); background: none; }
.side-index-sub {
  margin: var(--space-inline) 0 var(--space-tight);
  padding-inline: var(--space-inline);
  font-size: var(--text-caption);
  font-weight: var(--weight-strong);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-muted);
}
.side-index-sub:first-of-type { margin-top: var(--space-tight); }
.side-index-sub + ul a { padding-left: calc(var(--space-inline) + var(--space-stack)); }
.side-index-group > ul { padding-block: var(--space-tight) 0; margin-bottom: 0; }

/* (docs mirror — identical to llms.txt · Full top menu)
   WHEN TO USE: the first row of a website or document — wordmark, links, one action; on phones it stays one line and the links open under a darkening blur. Pages, not installed apps (apps get the tab bar).

   What it is: the site header — wordmark, nav links, actions. Add .top-menu-sticky on a wrapper around the header to pin it to the viewport top as a translucent, blurred, full-bleed strip (the blur lives on the wrapper, so the strip goes opaque while the fold is open and the header never darkens). Fill the wordmark slot with a .brand-lockup to put a logo chip beside the name. Nav links may carry a leading icon — an <svg data-icon> inside the .nav-link sits beside the label on its own. On phones the header stays ONE line — brand left, an icon-only details fold right (summary.fold-trigger with a data-icon="menu"/"close" pair on .when-closed/.when-open). Tapping it grows the panel down from the header's bottom edge in one clean motion (about 0.4s) while the page behind fades and blurs; tapping the trigger again, tapping anywhere outside, or Escape shrinks it back (about 0.3s) and the scrim leaves with it — the scrim never outlives the fold. Interrupting mid-motion continues from the current height. The engine is facet.js progressive enhancement on a plain <details>: with JS off or reduced motion the fold opens and closes instantly, no scrim. Row actions (a direct-child button like Sign up) hide on phones — author a copy of them inside the fold's links, the same way the links themselves are authored twice. What it is for: every site's first row. When to use it: pages, not installed apps (apps get the tab bar). Resize this page to see the fold. */
/* Top menu: logo, links, actions; on phones the header stays ONE line —
   brand left, an icon-only details fold right. facet.js owns the fold's
   motion (initTopMenuFold): it intercepts the summary click and animates
   the panel's height in the same task — the native toggle paints the
   panel at full height before the async toggle event fires, so anything
   hooked there starts a frame late and flashes. The scrim is a real
   element (.fold-scrim) the engine injects, so it rides the exact
   open/close timeline and layers under the sticky strip. */
.top-menu {
  position: relative;               /* anchors the phone fold panel */
  display: flex;
  align-items: center;
  gap: var(--space-stack);
  flex-wrap: wrap;
  padding-block: var(--space-inline);
  border-bottom: var(--border-hairline) solid var(--border);
}
.top-menu .wordmark-slot {
  font-weight: var(--weight-heading);
  font-size: var(--text-h4);
  margin-right: auto;
}
.top-menu-links { display: flex; gap: var(--space-tight); flex-wrap: wrap; }
.top-menu-fold { display: none; }
@media (max-width: 40rem) {
  .top-menu-links,
  .top-menu > .btn { display: none; }   /* row links and row actions both live in the fold on phones */
  .top-menu-fold { display: block; }    /* a compact flex item on the row's right — never a second row */
  .top-menu-fold summary { list-style: none; cursor: pointer; }
  .top-menu-fold summary::-webkit-details-marker { display: none; }
  /* While open the header lifts into its own layer (opaque, above the
     scrim) and hands its bottom border to the panel — the border reads
     as moving down with the reveal. */
  .top-menu:has(.top-menu-fold[open]) {
    z-index: 40;                      /* above the injected .fold-scrim (39) */
    border-bottom-color: transparent;
  }
  /* the header row's own surface, bled to the viewport edges so no dark
     bands frame it (painted after ::before, so it sits above the scrim) */
  .top-menu:has(.top-menu-fold[open])::after {
    content: "";
    position: absolute;
    top: 0;
    bottom: 0;
    left: 50%;
    width: 100vw;
    translate: -50%;
    z-index: -1;
    background: var(--background);
  }
  /* The open panel: the header's own surface extending down — same
     background, full width, hairline at its foot where the header's
     border now sits. facet.js grows its height from 0; the height is
     constrained mid-animation, so the column must never wrap sideways
     and the overflow must clip. */
  .top-menu-fold .top-menu-links {
    display: flex;
    flex-direction: column;
    flex-wrap: nowrap;
    gap: var(--space-tight);
    position: absolute;
    left: 50%;
    width: 100vw;                       /* bleed to the viewport edges, like the header strip */
    translate: -50%;
    top: 100%;
    margin: 0;
    padding: var(--space-control) var(--space-stack) var(--space-stack);
    background: var(--background);
    border-bottom: var(--border-hairline) solid var(--border);
    overflow: hidden;
  }
}

/* The fold scrim: injected by facet.js while the phone fold is open —
   a real element (not a pseudo) so it can ride the exact open/close
   timeline and layer UNDER the sticky strip. */
.fold-scrim {
  position: fixed;
  inset: 0;
  z-index: 39;                          /* under the open header (40) */
  background: rgb(0 0 0 / 0.45);
  -webkit-backdrop-filter: blur(5px);
  backdrop-filter: blur(5px);
}

/* Sticky variant: wrap the header in .top-menu-sticky to pin it to the
   viewport top as a translucent, blurred, full-bleed strip. The blur
   lives on the WRAPPER's pseudo — backdrop-filter on the nav itself
   would turn any fixed descendant into a nav-sized box (a filter
   creates a containing block). While the fold is open the strip goes
   opaque, so the scrim cannot darken the header through it. */
.top-menu-sticky {
  position: sticky;
  top: 0;
  z-index: 40;
}
.top-menu-sticky::before {
  content: "";
  position: absolute;
  inset: 0 auto 0 50%;
  width: 100vw;
  translate: -50%;
  z-index: -1;
  background: color-mix(in srgb, var(--background) 82%, transparent);
  -webkit-backdrop-filter: blur(12px);
  backdrop-filter: blur(12px);
  border-bottom: var(--border-hairline) solid var(--border);
  transition: background var(--duration-normal) var(--ease-out);
}
.top-menu-sticky:has(.top-menu-fold[open])::before { background: var(--background); }
.top-menu-sticky .top-menu { border-bottom-color: transparent; }  /* the strip carries the rule */

/* Brand lockup: the canonical wordmark-slot filling — a logo chip
   beside the name. The lockup owns the row's left push, so the
   anchor's own auto margin is handed over. */
.brand-lockup {
  display: flex;
  align-items: center;
  gap: var(--space-control);
  margin-right: auto;
  min-width: 0;
}
.brand-lockup .wordmark-slot {
  margin-right: 0;                      /* the lockup owns the auto margin */
  white-space: nowrap;
  text-decoration: none;
  color: var(--text);
}

/* Icon-only fold trigger: a bare glyph, no button box. The focus ring
   is keyboard-only — a touch tap never leaves one painted. */
.fold-trigger {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;                          /* touch target */
  height: 44px;
  padding: 0;
  border: 0;
  background: none;
  color: var(--text);
  cursor: pointer;
  list-style: none;
  -webkit-tap-highlight-color: transparent;
}
.fold-trigger::-webkit-details-marker { display: none; }
.fold-trigger:focus { outline: none; }
.fold-trigger:focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-offset);
  border-radius: var(--radius-pill);
}

/* Nav links with icons: a leading svg sits beside the label. */
.top-menu .nav-link {
  display: inline-flex;
  align-items: center;
  gap: var(--space-control);
}
.top-menu .nav-link svg { flex: none; }

/* ----- HEADING BAR: the accent stroke under a heading -----
   (docs mirror — identical to llms.txt · Heading bar)
   WHEN TO USE: the recommended treatment under a page or section heading — one short accent stroke, once or twice per page; never under every heading.

   What it is: a short thick accent stroke under a heading — add
   .heading-bar to any h1–h3 and a small pill-shaped bar in accent-3
   draws beneath the text. .heading-bar-center centers the heading and
   its bar. What it is for: section titles on list pages, launchers and
   landing sections — the designed look between plain text and a font
   change. When to use it: on the page's main headings, once or twice
   per page; never under every heading. */
.heading-bar::after {
  content: "";
  display: block;
  width: 2.6rem;
  height: 0.3rem;
  margin-top: calc(var(--space-tight) + 4px);
  background: var(--accent-3);
  border-radius: var(--radius-pill);
}
.heading-bar-center { text-align: center; }
.heading-bar-center::after { margin-inline: auto; }

/* Hero: the opening statement — display type, one line under it,
   one primary action. */
.hero {
  display: grid;
  gap: var(--space-stack);
  justify-items: start;
  padding-block: var(--space-section);
}
.hero h1, .hero .text-display { margin: 0; }

/* CTA band: the final call to action — an ink-filled strip that flips the
   tokens so everything inside just works. */
.cta-band {
  display: grid;
  gap: var(--space-inline);
  justify-items: center;
  text-align: center;
  padding: var(--space-section) var(--space-card);
  border-radius: var(--radius-card);
  background: var(--accent-1);
  color: var(--on-accent-1);
}

/* Stat cards: one number that matters, its label under it. */
.stat {
  display: grid;
  gap: var(--space-tight);
  padding: var(--space-stack) var(--space-card);
  background: var(--surface);
  border: var(--border-element) solid var(--border);
  border-radius: var(--radius-card);
}
.stat strong {
  font-family: var(--font-numeric);       /* the big-number face */
  font-size: var(--text-display);
  font-weight: var(--weight-heading);
  line-height: var(--leading-display);
  letter-spacing: var(--tracking-display);
  font-variant-numeric: tabular-nums lining-nums;
}
.stat span { font-size: var(--text-caption); color: var(--text-muted); }

/* Pull quote: the sentence worth emphasising, set large. */
.pullquote {
  border-left: var(--border-highlight) solid var(--accent-3);
  padding-left: var(--space-card);
  font-family: var(--font-quote);
  font-size: var(--text-h3);
  font-weight: var(--weight-body);
  font-style: italic;
  line-height: var(--leading-snug);
  color: var(--text);
}
.pullquote footer {
  margin-top: var(--space-control);
  font-size: var(--text-footnote);
  font-weight: var(--weight-body);
  color: var(--text-muted);
}

/* Media figure: an image that explains, with its caption attached. */
.media-figure { margin: 0; }
.media-figure img,
.media-figure video {
  width: 100%;
  border-radius: var(--radius-panel);
  border: var(--border-element) solid var(--border);
}
.media-figure figcaption {
  margin-top: var(--space-control);
  font-size: var(--text-caption);
  color: var(--text-muted);
}

/* ----- APP IDENTITY: the front matter of an app's about page -----
   (docs mirror — identical to llms.txt · App identity)
   WHEN TO USE: once, at the top of an app's about, release-notes or install page — which app this is, at a glance.

   What it is: the identity header of an app's about page — a centered
   rounded-square icon, the app's name, a muted one-line phrase, and an
   optional badge row; .app-identity-stats centers a row of .stat blocks
   under it. What it is for: the App-Store-style front matter of an
   about, release-notes or install page — which app this is, at a
   glance, before any detail. When to use it: once, at the top of the
   page, with the name as the page's one h1. The icon wears a hairline
   border so light icons keep an edge on light surfaces. Pairs with the
   stat row — opens, version, size — numbers first, prose after.

   Usage:
     <header class="app-identity">
       <img src="/apps/icons/inflation.png" alt="" width="96" height="96">
       <h1>Inflation calculator</h1>
       <p class="text-footnote">Time-travel your money</p>
       <p><span class="badge badge-warning">In development</span></p>
     </header>
     <div class="app-identity-stats">
       <div class="stat"><strong>4,120</strong><span>OPENS</span></div>
       <div class="stat"><strong>v0.9</strong><span>VERSION</span></div>
     </div>
*/
.app-identity {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
}
.app-identity img {
  width: 6rem;
  height: 6rem;
  border-radius: 22.5%;              /* the app-icon corner ratio, as in .app-tile */
  border: var(--border-hairline) solid var(--border-subtle);
  margin-bottom: var(--space-stack);
}
.app-identity h1,
.app-identity .text-h1 {             /* .text-h1 pairs like .hero .text-display: same look off the docs page */
  margin: 0 0 var(--space-tight);
  color: var(--text);
}
.app-identity p { margin: 0; color: var(--text-muted); }
.app-identity p:has(.badge) { margin-top: var(--space-control); }
.app-identity-stats {
  display: flex;
  justify-content: center;
  gap: var(--space-page);
  flex-wrap: wrap;
}

/* ----- MOTION PACK: parallax depths and idle motion -----
   (docs mirror — identical to llms.txt · Parallax & idle motion)
   WHEN TO USE: heroes, illustrations, decorative cards — movement at the edges, never on the reading path; one or two moving things per screen.

   What it is: ambient motion, on by default. data-parallax="depth" registers an
element with facet.motion — it drifts up to depth pixels behind the
desktop pointer, the scroll's momentum (a velocity impulse), the
device tilt where no permission gate blocks it (iOS gates it;
facet.motion.setMode("tilt") asks), or the engine's idle life where
nothing else drives. Three modes: off | idle | responsive — idle is
the DEFAULT, so nothing ever follows the device uninvited. Responsive
resolves itself to the device — cursor on fine pointers (a desktop
mouse, an iPad trackpad), tilt on touch — and the UI shows the
resolved word (Cursor or Tilt), never "responsive"; switching to it
from a tap asks the iOS orientation permission right there, and a
denial drops honestly back to idle. The choice persists (localStorage
motion-mode; parallax-enabled mirrors it as "true" only while
responsive, and an external "true" upgrades the mode on the next
load); every change fires a facet:motion event on document. The engine
boots on demand — registering a mover at runtime or switching the mode
wakes it even on a page that loaded with none — and registering an
element twice is safe; the second registration is ignored. Cursor and
tilt mirror the real world directly — just enough smoothing to hide
jitter, no easing lag, and a two-degree dead zone so hand tremor reads
as stillness — and the moment the pointer or device stops, the vector
eases back to centre (in and out, about half a second) and the idle
life plays until real input returns. Idle rests at that same centre
and, every few seconds, takes one fast smooth loop — down out of the
centre, once around, and back up into it (about 1.1s) — so idle starts
and ends exactly where everything else rests. The same engine's light channel drives the shine
effect (next entry) from the same vector. The idle-* classes
(.idle-float, .idle-sway, .idle-pulse) give resting elements a slow
pulse; they animate the separate translate/rotate/scale properties so
they compose with the engine's transform instead of colliding — the
parallax exclusion, solved structurally. Never put data-parallax on
elements with their own transform physics (velvet lift/press, the
pager). Calm stills idle life; data-motion="off" and
prefers-reduced-motion still everything.*/
.idle-float { animation: facet-idle-float 5s ease-in-out infinite alternate; }
.idle-sway  { animation: facet-idle-sway 7s ease-in-out infinite alternate; }
.idle-pulse { animation: facet-idle-pulse 4s ease-in-out infinite alternate; }
@keyframes facet-idle-float { from { translate: 0 2px; } to { translate: 0 -4px; } }
@keyframes facet-idle-sway  { from { rotate: -1.2deg; } to { rotate: 1.2deg; } }
@keyframes facet-idle-pulse { from { scale: 1; } to { scale: 1.02; } }
[data-motion="calm"] .idle-float,
[data-motion="calm"] .idle-sway,
[data-motion="calm"] .idle-pulse { animation: none; }

/* ----- SHINE: the moving light on things with depth -----
   (docs mirror — identical to llms.txt · Shine)
   WHEN TO USE: app icons, hero cards, any raised or 3D-feeling piece that should catch light as the device moves.

   What it is: the shine effect — a specular light on any element with
   visual depth, the way iOS icons catch light. data-shine lays a soft
   light spot plus a rim over the element: a bright edge toward the
   light, a soft shade away from it. facet.motion's light channel writes
   the light's direction (--shine-x / --shine-y, each -1..1) from the
   device tilt, the cursor, or the idle drift — the same normalised
   vector that drives parallax, so the element that translates and the
   light that sweeps across it always agree on which way the device is
   leaning. At rest the light sits centred above.
   What it is for: app icons, hero cards, any raised or 3D-feeling piece
   that should catch light as the device moves. When to use it: on the
   element that carries the depth; the overlay is a .shine child span
   facet.js injects for you (the pseudo-elements stay free for the
   tooltip; add the span yourself for a static resting shine with JS
   off), it inherits the element's border-radius, and images need a
   wrapper because an <img> cannot hold a child — the .icon-badge-holder
   doubles as one (give the wrapper the icon's radius). Unlike parallax, shine writes no
   transform — only custom properties — so the parallax exclusion does
   not apply; it composes with velvet lift/press, the pager, and
   data-parallax itself. --shine (0..1, default 0.5) tunes the strength.*/
[data-shine] {
  position: relative;
  /* the resting light: centred, from above — where it returns to */
  --shine-x: 0;
  --shine-y: -0.55;
  --shine: 0.5;
}
[data-shine] > .shine {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  user-select: none;
  /* the specular spot sits where the light is... */
  background: radial-gradient(
    120% 85% at calc(50% + var(--shine-x) * 55%) calc(50% + var(--shine-y) * 55%),
    rgb(255 255 255 / calc(var(--shine) * 0.5)),
    rgb(255 255 255 / 0) 60%);
  /* ...and the rim brightens toward it, shades away from it */
  box-shadow:
    inset calc(var(--shine-x) * -2px) calc(var(--shine-y) * -2px) 2px rgb(255 255 255 / calc(var(--shine) * 0.8)),
    inset calc(var(--shine-x) * 2px) calc(var(--shine-y) * 2px) 3px rgb(0 0 0 / calc(var(--shine) * 0.45));
}

/* ----- ICONS: thin line glyphs on one attribute -----
   (docs mirror — identical to llms.txt · Icons)
   WHEN TO USE: every small symbol a product needs — write <svg data-icon="name"></svg> and facet.js fills it. Never paste icon paths by hand; extend the table instead.

   What it is: 24x24 line glyphs at a 1.5px stroke with round
   caps, carried as markup inside facet.js — no image files, no sprite
   fetch. Write <svg data-icon="search"></svg> and facet.js fills it;
   icons ride currentColor and scale with the surrounding text.
   In running text add .glyph — it makes the svg sit in the sentence
   like a character (inline-block, sized to the line, baseline-aligned)
   instead of breaking the line, because the base media rule makes
   every svg a block.
   Decorative by default (aria-hidden); an aria-label on the svg makes
   it meaningful. Extend: facet.icons.rocket = '<path d="..."/>' then
   facet.icon(). The set: search close check plus minus arrow-right
   arrow-left arrow-up arrow-down chevron-right chevron-left chevron-up
   chevron-down menu more settings home user users heart star bookmark
   bell calendar clock download upload share external link copy edit
   trash filter eye lock mail globe image file folder sun moon info
   warning help play pause.*/
svg[data-icon] {
  width: 1.25em;
  height: 1.25em;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
  vertical-align: -0.25em;
  flex: none;
}

/* ----- INLINE GLYPH: an icon that sits in a sentence -----
   What it is: the inline fix for a glyph in a sentence — .glyph makes an
   svg sit in running text like a character (inline-block, sized to the
   line, baseline-aligned) instead of breaking the line, because the base
   media rule makes every svg a block. What it is for: instructional
   steps and any sentence that names a control by its icon. When to use
   it: add .glyph to any svg inside text; pairs with data-icon. */
.glyph {
  display: inline-block;
  width: 1.05em;
  height: 1.05em;
  vertical-align: -0.15em;
}

/* ----- FLAGSHIP LINK: the signature way to another page -----
   (docs mirror — identical to llms.txt · Flagship link)
   WHEN TO USE: the one onward step at the end of a section — at most one or two per screen; ordinary references stay plain links.

   What it is: a large link — heading-sized text, a strong accent
   underline, and an arrow that shifts forward on hover. What it is
   for: the one onward step at the end of a section. When to use it: at
   most one or two per screen; ordinary references stay plain underlined
   links. Always an <a> to a real URL, never a button styled as a link.*/
.flagship-link {
  display: inline-flex;
  align-items: baseline;
  gap: var(--space-control);
  font-family: var(--font-heading);
  font-size: var(--text-h3);
  font-weight: var(--weight-heading);
  color: var(--text);
  text-decoration: none;
  border-bottom: var(--border-highlight) solid var(--accent-3);
  padding-bottom: 2px;
  transition: color var(--duration-fast) var(--ease-out);
}
.flagship-link::after {
  content: "\27F6";               /* the long arrow */
  color: var(--accent-3);
  transition: translate var(--duration-normal) var(--ease-spring);
}
.flagship-link:hover { color: var(--accent-3); }
.flagship-link:hover::after { translate: 0.35em 0; }

/* ----- SKELETON, SPINNER, EMPTY STATE: the waiting states -----
   (docs mirror — identical to llms.txt · Skeleton, spinner, empty state)
   WHEN TO USE: a skeleton when the coming content's shape is known; a spinner when it is not; an empty state when there is nothing yet — that is content, not an error, and it offers the one action that fills it.

   What it is: three ways to show absence. Skeleton: a
   shimmering placeholder — put .skeleton on blocks sized like the
   content it stands in for; mark the group aria-hidden. Spinner: a
   small accent ring for indeterminate waits — give the row role=status
   and words; under reduced motion the ring stands still and the words
   do the talking. Empty state: .empty — a dashed quiet zone that names
   what would be here and offers the one action that fills it. Prefer
   skeletons over spinners whenever the shape is known; an empty state
   is content, not an error.*/
.skeleton {
  display: block;
  min-height: 1em;
  border-radius: var(--radius-control);
  background: linear-gradient(90deg,
    var(--accent-2-pressed) 25%, var(--accent-2) 42%, var(--accent-2-pressed) 60%);
  background-size: 200% 100%;
  animation: facet-shimmer 1.4s linear infinite;
  color: transparent;
  user-select: none;
}
@keyframes facet-shimmer {
  from { background-position: 120% 0; }
  to { background-position: -80% 0; }
}

.spinner {
  display: inline-block;
  width: 1.5rem;
  height: 1.5rem;
  border: 3px solid var(--accent-2-pressed);
  border-top-color: var(--accent-1);
  border-radius: var(--radius-pill);
  animation: facet-spin 0.8s linear infinite;
  flex: none;
}
@keyframes facet-spin { to { rotate: 360deg; } }

.empty {
  display: grid;
  place-items: center;
  gap: var(--space-control);
  padding: var(--space-section) var(--space-card);
  border: var(--border-element) dashed var(--border);
  border-radius: var(--radius-card);
  text-align: center;
  color: var(--text-muted);
}
.empty strong { color: var(--text); }

/* ----- STATE GRAMMAR: every state, one of three carriers -----
   What it is: the library's state system. Every component state is
   carried one of three ways, never by an invented class: a native
   pseudo-class where the platform has one (:disabled, :checked,
   :indeterminate, :user-invalid); an ARIA attribute where a real
   semantic exists (aria-pressed, aria-selected, aria-current,
   aria-expanded, aria-busy, aria-invalid, aria-disabled) — the CSS
   styles the attribute itself, so looks and accessibility can never
   drift apart; and data-status="success|warning|error|info" for
   statuses with no ARIA home (fields, cards, rows). What it is for:
   selected cards and rows, disabled anything, field validation in
   three colors, working buttons. When to use it: set the attribute
   and the look follows, in every theme, light and dark. The older
   hooks (.field-invalid, the toast's data-kind) stay as permanent
   aliases. */

/* Disabled by attribute: links and ARIA widgets cannot take :disabled,
   so aria-disabled="true" gets the disabled treatment everywhere. */
.btn[aria-disabled="true"],
.chip:disabled,
.chip[aria-disabled="true"],
.list-item[aria-disabled="true"],
.tabs [role="tab"][aria-disabled="true"],
.dropdown-menu [aria-disabled="true"] {
  opacity: 0.5;
  pointer-events: none;
}

/* A disabled segment: the option shows, muted, and takes no pick. */
.segmented input:disabled + span {
  opacity: 0.5;
  cursor: not-allowed;
}

/* A picked card: the same accent ring the option card earns when its
   radio checks — aria-selected (pickers), aria-pressed (toggling
   cards) and aria-current (the active one of a set) all light it. */
.card[aria-selected="true"],
.card[aria-pressed="true"],
.card[aria-current="true"] {
  border-color: var(--accent-1);
  box-shadow: 0 0 0 1px var(--accent-1), var(--shadow-raised);
}

/* A picked row: a quiet fill plus an accent bar at the reading edge. */
.list-item[aria-selected="true"],
.list-item[aria-current] {
  background: var(--surface-active);
  box-shadow: inset 3px 0 0 var(--accent-1);
}
tbody tr[aria-selected="true"] {
  background: color-mix(in srgb, var(--accent-1) 7%, var(--surface));
}

/* A statusful card: the status color as an edge at the reading side.
   The words carry the meaning (pair it with a badge); the edge only
   points at it. */
.card[data-status="success"] { border-inline-start: 3px solid var(--success); }
.card[data-status="warning"] { border-inline-start: 3px solid var(--warning); }
.card[data-status="error"]   { border-inline-start: 3px solid var(--error); }
.card[data-status="info"]    { border-inline-start: 3px solid var(--info); }

/* Field validation in three colors. data-status on the .field colors
   the control's border and the .field-msg line under it. aria-invalid
   on the control and the native :user-invalid (required, pattern,
   type) light the error state with no class at all — the old
   .field-invalid + .field-error pair stays as a permanent alias. */
:is(input, textarea, select)[aria-invalid="true"],
:is(input, textarea, select):user-invalid,
.field[data-status="error"] :is(input, textarea, select) {
  border-color: var(--error);
}
.field[data-status="warning"] :is(input, textarea, select) { border-color: var(--warning); }
.field[data-status="success"] :is(input, textarea, select) { border-color: var(--success); }

/* The one message line, colored by the field's status; same reveal
   animation as .field-error. */
.field-msg {
  font-size: var(--text-caption);
  font-weight: var(--weight-body);
  color: var(--text-muted);
  animation: facet-reveal var(--duration-normal) var(--ease-out);
}
.field[data-status="error"] .field-msg   { color: var(--error); }
.field[data-status="warning"] .field-msg { color: var(--warning); }
.field[data-status="success"] .field-msg { color: var(--success); }

/* Readonly reads as fact, not entry: the fill drops to the page, the
   border keeps the shape, the value stays selectable. */
input[readonly],
textarea[readonly] {
  background: var(--background);
  color: var(--text-muted);
  cursor: default;
}

/* A <progress> with no value attribute is indeterminate: an accent
   sweep slides the track until the real number arrives. */
progress:not([value]) {
  background: var(--accent-2-pressed);
  background-image: linear-gradient(90deg, transparent, var(--accent-1) 50%, transparent);
  background-size: 40% 100%;
  background-repeat: no-repeat;
  animation: facet-slide 1.4s ease-in-out infinite;
}
progress:not([value])::-webkit-progress-bar { background: transparent; }
progress:not([value])::-moz-progress-bar { background: transparent; }
@keyframes facet-slide {
  from { background-position: -66% 0; }
  to   { background-position: 166% 0; }
}

/* A working button: aria-busy="true" swaps the label for a spinner in
   the button's own ink, holds the width, and goes inert — no double
   submits. Add disabled too when the button should also leave the tab
   order. */
.btn[aria-busy="true"] {
  position: relative;
  color: transparent;
  text-shadow: none;         /* velvet's embossed label must vanish too */
  pointer-events: none;
}
/* The spinner reclaims ::after from the tooltip: a busy button is
   inert (no hover, no tap), so its data-tip bubble can never show —
   every tooltip property is reset here so the ring wins on tipped
   buttons too. */
.btn[aria-busy="true"]::after {
  content: "";
  position: absolute;
  inset: 0;
  translate: none;
  margin: auto;
  width: 1.1em;
  height: 1.1em;
  max-width: none;
  padding: 0;
  display: block;
  opacity: 1;
  background: transparent;
  box-shadow: none;
  border: 2px solid color-mix(in srgb, var(--on-accent-2) 30%, transparent);
  border-top-color: var(--on-accent-2);
  border-radius: var(--radius-pill);
  animation: facet-spin 0.8s linear infinite;
}
.btn-primary[aria-busy="true"]::after {
  border-color: color-mix(in srgb, var(--on-accent-1) 30%, transparent);
  border-top-color: var(--on-accent-1);
}
.btn-ghost[aria-busy="true"]::after {
  border-color: color-mix(in srgb, var(--text) 25%, transparent);
  border-top-color: var(--text);
}

/* ----- LOADING SKIN: aria-busy turns a region into its own skeleton -----
   (docs mirror — identical to llms.txt · Loading skin)
   WHEN TO USE: any region that waits — set aria-busy="true" on it and its own content becomes the pulsing placeholder; no separate skeleton markup to build.

   What it is: the page-refresh loading state. Put aria-busy="true" on
   any component or region whose content is being fetched and everything
   inside turns into quiet pulsing placeholders that trace the shapes
   already there: text becomes soft bars hugging its own lines, images
   and icons flatten to gray, controls go quiet and inert. Remove the
   attribute and the content is back — nothing was replaced, only
   skinned. What it is for: refreshing live data over a page that has
   already rendered — lists, cards, dashboards. When to use it: keep the
   stale content (or placeholder text) in place and skin it, instead of
   swapping markup for hand-built .skeleton blocks; .skeleton stays for
   first paint when no content exists yet. facet.busy(el, true|false)
   does the same and upgrades the bars to per-line pills that hug real
   text; an element carrying data-skeleton-lines="n" with no content yet
   gets n lines of generated placeholder words shaped like average text.
   Put data-static on anything inside that is not live data (a section
   heading, a label) to leave it readable. Under prefers-reduced-motion
   the pulse stands still. For table cells, use facet.busy (it wraps the
   text itself); the pure-CSS skin covers headings, paragraphs, list
   items, inline text, media and controls.*/

@keyframes facet-pulse {
  50% { opacity: 0.45; }
}
@keyframes facet-pulse-media {
  0%, 100% { opacity: 0.16; }
  50% { opacity: 0.08; }
}

/* The busy scope: work in progress — hands off until it lands. */
[aria-busy="true"] { cursor: progress; }
[aria-busy="true"] :is(a, button, input, select, textarea, label):not([data-static], [data-static] *) {
  pointer-events: none;
}

/* The per-line pill: invisible ink, a pulsing bar hugging the words.
   box-decoration-break gives every wrapped line its own rounded ends.
   facet.busy() wraps text nodes in this; authors can hand-place it. */
.skeleton-text {
  color: transparent;
  text-shadow: none;
  background: var(--skeleton);
  border-radius: 0.4em;
  box-decoration-break: clone;
  -webkit-box-decoration-break: clone;
  animation: facet-pulse 1.3s ease-in-out infinite;
  user-select: none;
}
.skeleton-text * { visibility: hidden; }

/* Block text traces its own shape: the bar hugs the words, not the
   column. Skipped once facet.busy() has wired the region (data-busy)
   — the per-line pills take over. */
[aria-busy="true"]:not([data-busy]) :is(h1, h2, h3, h4, h5, h6, p, li, dt, dd, blockquote, figcaption):not([data-static], [data-static] *) {
  color: transparent;
  text-shadow: none;
  background: var(--skeleton);
  border-radius: var(--radius-control);
  max-width: fit-content;
  animation: facet-pulse 1.3s ease-in-out infinite;
  user-select: none;
}

/* Inline text and the small signals hug their own words already. */
[aria-busy="true"]:not([data-busy]) :is(span, a, strong, em, small, time, code, .badge):not([data-static], [data-static] *) {
  color: transparent;
  text-shadow: none;
  background: var(--skeleton);
  border-color: transparent;
  border-radius: 0.4em;
  box-decoration-break: clone;
  -webkit-box-decoration-break: clone;
  animation: facet-pulse 1.3s ease-in-out infinite;
  user-select: none;
}

/* Media flattens to its own gray shape: exact size, no content. */
[aria-busy="true"] :is(img, svg, video, canvas, iframe):not([data-static], [data-static] *) {
  filter: grayscale(1) contrast(0);
  opacity: 0.12;
  animation: facet-pulse-media 1.3s ease-in-out infinite;
}

/* Controls go quiet: same shape, placeholder fill, nothing to press. */
[aria-busy="true"] :is(.btn, .chip, input, select, textarea):not([data-static], [data-static] *) {
  color: transparent;
  text-shadow: none;
  background: var(--skeleton);
  border-color: transparent;
  box-shadow: none;
  animation: facet-pulse 1.3s ease-in-out infinite;
}
[aria-busy="true"] :is(input, textarea)::placeholder { color: transparent; }

/* ----- BADGE, CHIP, AVATAR, PROGRESS: the small signals -----
   (docs mirror — identical to llms.txt · Badge, chip, avatar, progress)
   WHEN TO USE: badges state, they never act; chips toggle; an avatar names a person; progress shows completeness with words nearby.

   What it is: four tiny signals. Badge: a status word in a tinted pill —
   .badge plus .badge-success / -warning / -error / -info. Chip: a small
   toggleable pill button; aria-pressed carries its state and facet.js
   toggles a bare chip on click (chips wired to data attributes keep
   their own handlers); an svg glyph inside a chip sits beside the label
   on its own. Avatar: a round face — an image or initials on
   the quiet fill; .avatar-large for bigger. Progress: the native
   <progress> themed, accent-1 fill on a quiet track. Badges state, they
   never act; avatars carry the person's name in alt or aria-label;
   determinate progress needs value and max with words nearby.*/
.badge {
  display: inline-flex;
  align-items: center;
  padding: 1px var(--space-control);
  border-radius: var(--radius-pill);
  font-size: var(--text-caption);
  font-weight: var(--weight-strong);
  background: var(--accent-2);
  color: var(--on-accent-2);
  border: var(--border-element) solid var(--border);
}
.badge-success { background: var(--success-tint); color: var(--success); border-color: var(--success-edge); }
.badge-warning { background: var(--warning-tint); color: var(--warning); border-color: var(--warning-edge); }
.badge-error   { background: var(--error-tint);   color: var(--error);   border-color: var(--error-edge); }
.badge-info    { background: var(--info-tint);    color: var(--info);    border-color: var(--info-edge); }

.chips { display: flex; flex-wrap: wrap; gap: var(--space-tight); max-width: none; }
.chip {
  display: inline-flex;                /* an svg glyph sits beside the label
                                          (the base svg rule is display:block,
                                          which would stack it above the text
                                          in a default inline-block button) */
  align-items: center;
  justify-content: center;
  gap: var(--space-tight);
  min-height: 36px;
  padding: var(--space-tight) var(--space-inline);
  background: var(--surface);
  border: var(--border-element) solid var(--border);
  border-radius: var(--radius-pill);
  font: inherit;
  font-size: var(--text-footnote);
  color: var(--text);
  cursor: pointer;
  touch-action: manipulation;
  transition:
    background var(--duration-fast) var(--ease-out),
    border-color var(--duration-fast) var(--ease-out),
    transform var(--duration-normal) var(--ease-spring);
}
.chip:hover { border-color: var(--text-muted); }
.chip:active { transform: scale(0.92); }
.chip[aria-pressed="true"] {
  background: var(--accent-3);
  border-color: var(--accent-3);
  color: var(--on-accent-3);
}

.avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.5rem;
  height: 2.5rem;
  border-radius: var(--radius-pill);
  background: var(--accent-2-pressed);
  color: var(--on-accent-2);
  font-size: var(--text-footnote);
  font-weight: var(--weight-strong);
  overflow: hidden;
  flex: none;
}
.avatar img { width: 100%; height: 100%; object-fit: cover; }
.avatar-large { width: 3.5rem; height: 3.5rem; font-size: var(--text-body); }

progress {
  appearance: none;
  width: 100%;
  height: 0.5rem;
  border: none;
  border-radius: var(--radius-pill);
  background: var(--accent-2-pressed);
  overflow: hidden;
}
progress::-webkit-progress-bar { background: var(--accent-2-pressed); border-radius: var(--radius-pill); }
progress::-webkit-progress-value { background: var(--accent-1); border-radius: var(--radius-pill); transition: width var(--duration-normal) var(--ease-out); }
progress::-moz-progress-bar { background: var(--accent-1); border-radius: var(--radius-pill); }

/* ----- ICON BADGE: the status dot on an app icon -----
   (docs mirror — identical to llms.txt · Icon badge)
   WHEN TO USE: on app icons 3.5rem and larger — launcher tiles, featured cards — to say new, updated or in the works at a glance; smaller list-row icons take an inline version chip instead.

   What it is: a round status dot pinned to an app icon's top-right
   corner — a 1.25rem plasticine bead, its top edge lit and its underside
   softly shaded, wearing a hairline ring of the page background so it
   pops off any icon, carrying one solid white glyph and no text (the word lives in
   title/aria-label). One attribute sets the tone and glyph:
   data-status="new" (info blue, sparkle), "updated" (success green,
   refresh arrow), "wip" (warning amber, wrench). What it is for: marking
   apps in a launcher grid or a featured card as new, freshly updated, or
   in the works. When to use it: only on icons 3.5rem and larger; smaller
   list-row icons take an inline version chip instead. Which status an
   app gets is the app's own decision — the library ships only the
   visual (a common app-side rule: wip while version < 1, else new if
   published within 30 days, else updated if a release is within 7 days,
   in that priority). Wrap the icon in an .icon-badge-holder (the
   positioning context; it also takes data-shine, sized and rounded to
   the icon) and put the badge beside the img inside it.

   Usage:
     <span class="icon-badge-holder">
       <img src="/apps/icons/inflation.png" alt="" width="68" height="68">
       <span class="icon-badge" data-status="new" role="img"
             aria-label="New" title="New"><svg data-icon="sparkle"></svg></span>
     </span>
*/
.icon-badge-holder { position: relative; display: inline-block; }
.icon-badge {
  position: absolute;
  top: -0.35rem;
  right: -0.35rem;
  width: 1.25rem;
  height: 1.25rem;
  display: grid;
  place-items: center;
  border-radius: 50%;
  border: var(--border-hairline) solid var(--background);
  color: rgb(255 255 255);           /* solid white glyph in every theme and
                                          mode — dark paper ink smudges on the
                                          saturated disc; theme-neutral by
                                          design, like the sheen below */
  user-select: none;
  -webkit-user-select: none;
  /* the plasticine: a top-lit sheen over the tone, a soft self-shade
     along the underside, and a faint cast onto the icon beneath —
     white/black dims, theme-neutral like the shadow tokens */
  background-image: linear-gradient(to bottom, rgb(255 255 255 / 0.32), rgb(255 255 255 / 0) 58%);
  box-shadow:
    inset 0 1px 1px rgb(255 255 255 / 0.5),
    inset 0 -2px 3px rgb(0 0 0 / 0.22),
    0 1px 2.5px rgb(0 0 0 / 0.25);
}
.icon-badge svg[data-icon] {
  width: 0.78rem;
  height: 0.78rem;
  stroke-width: 2.5;                 /* the 1.5px set stroke vanishes this small; heavier so the white reads solid */
}
.icon-badge[data-status="new"]     { background-color: var(--info); }
.icon-badge[data-status="updated"] { background-color: var(--success); }
.icon-badge[data-status="wip"]     { background-color: var(--warning); }

/* ----- CHART: the themed SVG line chart -----
   (docs mirror — identical to llms.txt · Chart)
   WHEN TO USE: one series over time where the shape tells the story — prices, balances, rates. Keep the numbers in a table nearby for readers.

   What it is: a line chart drawn by facet.chart(el, {points, projectFrom,
   events, format}) into a .chart div — data line in accent-1 (2.5px,
   dashed from projectFrom on), dots ringed in the page background, grid
   on the border token, muted axis text, event markers as faint dashed
   verticals labelled in a band above the plot, and a drag crosshair that
   reads the nearest value, dropped on pointer lift. points is
   [{x, y, label?}]. Colors are all tokens, so theme and mode switches
   restyle a live chart with no redraw; velvet turns the line gold via
   accent-1 automatically. The SVG draws at ~1 unit per pixel so type
   reads true; the surface is touch-action: none (gesture law). Put the
   chart in a .chart-card — a well in velvet, a quiet panel elsewhere.
   update(next) redraws with new data.*/
.chart-card {
  background: var(--surface);
  border: var(--border-element) solid var(--border);
  border-radius: var(--radius-card);
  padding: var(--space-stack);
}
.chart svg { width: 100%; height: auto; touch-action: none; display: block; }
.chart .grid { stroke: var(--border); stroke-width: 1; }
.chart .line {
  stroke: var(--accent-1);
  stroke-width: 2.5;
  fill: none;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.chart .line.proj { stroke-dasharray: 7 7; opacity: 0.85; }
.chart .dot { fill: var(--accent-1); stroke: var(--background); stroke-width: 2; }
.chart .lbl { fill: var(--text); font-weight: var(--weight-strong); }
.chart .lbl-m { fill: var(--text-muted); }
.chart .xhair { stroke: var(--text-muted); stroke-dasharray: 3 3; }
.chart .evt { stroke: var(--border); stroke-dasharray: 2 5; }
.chart .lbl-e { fill: var(--text-muted); }

/* ----- TABLE: header, zebra rows, row hover -----
   (docs mirror — identical to llms.txt · Table)
   WHEN TO USE: real tabular data, where rows and columns mean something — comparisons, statements, breakdowns. Never for layout.

   What it is: the data table — muted header row, zebra striping, a
   hover that names the row you are reading, right-aligned tabular
   numbers via .num. What it is for: real records: transactions, users,
   line items. When to use it: data with columns; one-field lists go in
   list markup. Wrap wide tables in .table-scroll so they scroll
   horizontally on phones (the wrapper law allows it: it enables
   scrolling).*/
.table-scroll { overflow-x: auto; }
table.table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--text-footnote);
}
.table th {
  text-align: left;
  padding: var(--space-control) var(--space-inline);
  color: var(--text-muted);
  font-weight: var(--weight-strong);
  border-bottom: var(--border-highlight) solid var(--border);
  white-space: nowrap;
}
.table td {
  padding: var(--space-control) var(--space-inline);
  border-bottom: var(--border-hairline) solid var(--border);
}
.table tbody tr:nth-child(even) { background: var(--surface); }
.table tbody tr:hover { background: var(--accent-2-hover); }
.table .num {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

/* ----- NAV LINK: the three states of site navigation -----
   (docs mirror — identical to llms.txt · Nav link)
   WHEN TO USE: the one style for links inside navs, sidebars and rails; aria-current="page" marks where you are.

   What it is: the navigation link with its three states designed —
   quiet by default, named on hover, plainly marked when it is the page
   you are on (aria-current="page"). What it is for: headers, sidebars,
   footers. When to use it: every navigational link in navigation and controls; in
   content, plain underlined links stay the rule.*/
.nav-link {
  display: inline-flex;
  align-items: center;
  min-height: 2.75rem;
  padding: var(--space-tight) var(--space-inline);
  border-radius: var(--radius-control);
  color: var(--text-muted);
  text-decoration: none;
  transition:
    color var(--duration-fast) var(--ease-out),
    background var(--duration-fast) var(--ease-out);
}
.nav-link:hover {
  color: var(--text);
  background: var(--accent-2-hover);
}
.nav-link[aria-current="page"] {
  color: var(--text);
  font-weight: var(--weight-strong);
  background: var(--accent-2-pressed);
}

/* ----- TABS: in-page views under an underline -----
   (docs mirror — identical to llms.txt · Tabs, breadcrumb, pagination)
   WHEN TO USE: tabs switch views in place; the breadcrumb says where you are in a hierarchy; pagination walks a long set page by page.

   What it is: the wayfinding trio. Tabs are real ARIA tabs (role=
   tablist/tab/tabpanel; click or arrow keys; accent underline; facet.js
   wires panels and roving tabindex) for two-to-five in-page views.
   Breadcrumb is the path to here — nav.breadcrumb > ol of links with
   quiet separators, current page unlinked with aria-current="page".
   Pagination is numbered page links in 44px squares, current page
   filled in accent-1, edges disabled with aria-disabled on a span.
   When to use which: tabs switch views in place; the tab bar navigates
   an app; the segmented control is a form value; breadcrumb and
   pagination are links, never buttons.*/
.tabs {
  display: flex;
  gap: var(--space-tight);
  border-bottom: var(--border-hairline) solid var(--border);
}
.tabs [role="tab"] {
  border: none;
  background: none;
  padding: var(--space-control) var(--space-stack);
  min-height: 2.75rem;
  font: inherit;
  color: var(--text-muted);
  cursor: pointer;
  border-bottom: 2px solid transparent;
  margin-bottom: calc(-1 * var(--border-element));
  transition: color var(--duration-fast) var(--ease-out);
}
.tabs [role="tab"]:hover { color: var(--text); }
.tabs [role="tab"][aria-selected="true"] {
  color: var(--text);
  font-weight: var(--weight-strong);
  border-bottom-color: var(--accent-3);
}

/* ----- BREADCRUMB: where you are, as links -----
   What it is: the path to here — an ordered list of links with quiet
   separators, the current page unlinked and marked aria-current.
   What it is for: deep hierarchies: catalogues, docs, file trees.
   When to use it: three levels or more; two levels just need a back
   link. */
.breadcrumb ol {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  margin: 0;
  padding: 0;
}
.breadcrumb li {
  display: flex;
  align-items: center;
  font-size: var(--text-footnote);
}
.breadcrumb li + li::before {
  content: "/";
  color: var(--text-muted);
  margin-inline: var(--space-control);
}
.breadcrumb a {
  color: var(--text-muted);
  text-decoration: none;
}
.breadcrumb a:hover { text-decoration: underline; color: var(--text); }
.breadcrumb [aria-current="page"] {
  color: var(--text);
  font-weight: var(--weight-strong);
}

/* ----- PAGINATION: numbered pages as real links -----
   What it is: page links in 44px squares — previous, numbers, next —
   with the current page filled in accent-1 and marked aria-current.
   What it is for: long lists split across URLs, so every page is
   linkable and crawlable. When to use it: server-side or URL-driven
   paging. For infinite content, prefer a "load more" button — explicit
   and keyboard-friendly. Disable an edge arrow with
   aria-disabled="true" on a span, never a dead link. */
.pagination {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-tight);
}
.pagination a,
.pagination span {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 2.75rem;
  min-height: 2.75rem;
  padding-inline: var(--space-control);
  border-radius: var(--radius-panel);
  color: var(--text);
  text-decoration: none;
  transition: background var(--duration-fast) var(--ease-out);
}
.pagination a:hover { background: var(--accent-2-hover); }
.pagination [aria-current="page"] {
  background: var(--accent-1);
  color: var(--on-accent-1);
  font-weight: var(--weight-strong);
}
.pagination [aria-disabled="true"] { color: var(--text-muted); }

/* ----- ACCORDION: native details, one open at a time if named -----
   (docs mirror — identical to llms.txt · Accordion, toast, dropdown menu)
   WHEN TO USE: the accordion folds long optional reading; the toast confirms an outcome that needs no reply; the dropdown holds a few quiet actions behind one button.

   What it is: the accordion is native <details class="accordion">
   styled (hairline rows, rotating chevron; give siblings the same name
   attribute and the browser keeps exactly one open); the toast is
   facet.toast(message, kind) — a transient pill that says one thing and
   leaves after four seconds, announced politely (kinds success /
   warning / error / info color the dot); the dropdown is
   details.dropdown whose summary is styled as a button and whose
   .dropdown-menu holds real buttons and links, closed by outside
   clicks, Esc, or picking an item. When to use it: never a toast for a
   question (dialog) and never a dropdown for navigation (links).*/
details.accordion {
  border-bottom: var(--border-hairline) solid var(--border);
}
.accordion summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-inline);
  min-height: 2.75rem;
  padding-block: var(--space-control);
  font-weight: var(--weight-strong);
  cursor: pointer;
  list-style: none;
}
.accordion summary::-webkit-details-marker { display: none; }
.accordion summary::after {
  content: "";
  width: 0.5rem;
  height: 0.5rem;
  border-right: 2px solid var(--text-muted);
  border-bottom: 2px solid var(--text-muted);
  rotate: 45deg;
  flex: none;
  transition: rotate var(--duration-normal) var(--ease-spring);
}
.accordion[open] summary::after { rotate: 225deg; }
.accordion > :not(summary) { padding-bottom: var(--space-stack); }
/* The body expands to its natural height instead of popping open. Uses the
   ::details-content pseudo (the wrapper the browser puts around everything
   after the summary); where it is unsupported the row just opens instantly. */
@supports selector(::details-content) {
  details.accordion::details-content {
    block-size: 0;
    overflow: clip;
    transition: block-size var(--duration-normal) var(--ease-out),
                content-visibility var(--duration-normal) allow-discrete;
  }
  details.accordion[open]::details-content { block-size: auto; }
}

/* ----- TOAST: a transient notice that names itself -----
   What it is: a small pill that appears near the bottom, says one
   thing, and leaves on its own — created by facet.toast(message,
   kind). What it is for: confirmations and quiet failures: saved,
   copied, offline. When to use it: outcomes that need no reply.
   Never for questions — those are dialogs. The rack is aria-live
   polite, so readers hear the message without losing their place.
   Kinds color the leading dot: success, warning, error, info. */
.toast-rack {
  position: fixed;
  bottom: calc(var(--space-card) + var(--safe-bottom));
  left: 50%;
  translate: -50% 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-control);
  z-index: 60;
  pointer-events: none;
}
.toast {
  display: flex;
  align-items: center;
  gap: var(--space-control);
  max-width: min(92vw, 28rem);
  padding: var(--space-control) var(--space-card);
  border-radius: var(--radius-pill);
  background: var(--text);
  color: var(--background);
  font-size: var(--text-footnote);
  box-shadow: var(--shadow-overlay);
  pointer-events: auto;
  animation: facet-toast-in var(--duration-normal) var(--ease-spring);
  transition: opacity var(--duration-normal) var(--ease-out), translate var(--duration-normal) var(--ease-out);
}
@keyframes facet-toast-in { from { opacity: 0; translate: 0 12px; } }
.toast-out { opacity: 0; translate: 0 8px; }
.toast[data-kind]::before {
  content: "";
  width: 0.5rem;
  height: 0.5rem;
  border-radius: var(--radius-pill);
  flex: none;
}
.toast[data-kind="success"]::before { background: var(--success); }
.toast[data-kind="warning"]::before { background: var(--warning); }
.toast[data-kind="error"]::before { background: var(--error); }
.toast[data-kind="info"]::before { background: var(--info); }

/* ----- DROPDOWN MENU: a details that opens under its button -----
   What it is: a small action menu on <details class="dropdown"> — the
   summary is styled as a button, the .dropdown-menu panel drops in
   under it, outside clicks and Esc close it (facet.js), and every
   item is a real button or link. What it is for: two-to-seven
   secondary actions behind one trigger: export, duplicate, delete.
   When to use it: actions, never navigation between pages (links
   belong in nav) and never a form value (that is the select). */
details.dropdown {
  position: relative;
  display: inline-block;
}
.dropdown summary { list-style: none; }
.dropdown summary::-webkit-details-marker { display: none; }
.dropdown-menu {
  position: absolute;
  top: calc(100% + var(--space-tight));
  left: 0;
  min-width: 12rem;
  display: flex;
  flex-direction: column;
  padding: var(--space-tight);
  background: var(--surface);
  border: var(--border-element) solid var(--border);
  border-radius: var(--radius-panel);
  box-shadow: var(--shadow-floating);
  z-index: 30;
  transform-origin: top left;
  animation: facet-pop-in var(--duration-fast) var(--ease-out);
}
.dropdown-menu button,
.dropdown-menu a {
  display: block;
  width: 100%;
  padding: var(--space-control) var(--space-inline);
  border: none;
  border-radius: var(--radius-control);
  background: none;
  color: var(--text);
  font: inherit;
  text-align: left;
  text-decoration: none;
  cursor: pointer;
  min-height: 2.75rem;
  display: flex;
  align-items: center;
}
.dropdown-menu button:hover,
.dropdown-menu a:hover { background: var(--accent-2-hover); }
.dropdown-menu .dropdown-danger { color: var(--error); }

/* ----- CARD: default, outlined, clickable -----
   (docs mirror — identical to llms.txt · Card)
   WHEN TO USE: one thing per card — a product, a stat, a setting group. A clickable card is one <a> and carries at most that one action; specialised jobs (feature, option, inline) are the same primitive with one modifier.

   What it is: the grouped-surface primitive — a padded, rounded panel
   one step above the page. What it is for: one thing per card: a product,
   a stat, a setting group, a summary. When to use it: .card for most
   grouping; .card-outlined when many cards sit close and shadows would
   pile up; a clickable card is an <a class="card card-clickable"> — the
   whole surface is the link, it lifts on hover and presses on click.
   Never put more than one primary action inside a card that is itself
   clickable. Variants, each one modifier on .card: .card-feature displays a
   feature (a .card-feature-icon badge, a .card-feature-title, a
   .card-feature-text); .card-option is a selectable choice for quizzes and
   pickers — a real <label> wrapping a radio (single-pick) or a checkbox
   (add data-multi) plus a .card-option-mark, so :has(:checked) lights the
   whole card with no JavaScript and it stays keyboard operable;
   .card-inline is a compact horizontal row (.card-inline-media,
   .card-inline-body with -title/-meta, .card-inline-trailing). A card may
   also be a stack: class="card stack" (or stack-tight / stack-loose) lays
   the card's children out with the stack's gap — the pairing is supported
   directly, no extra rule needed. Variants use
   the neutral ramp's own roles (--surface-hover, --border-strong). Velvet
   raises cards out of the fabric; Elegant carves them in; Aero glosses them.*/
.card,
.card-outlined {
  display: block;
  background: var(--surface);
  border: var(--border-element) solid var(--border);
  border-radius: var(--radius-card);
  padding: var(--space-card);
  box-shadow: var(--shadow-raised);
  color: inherit;
}
.card-outlined {
  background: var(--background);
  box-shadow: none;
}
/* A card that is also a stack. .card's display:block sits later in the
   file than .stack's flex, so the compound form needs its own rule or
   the stack's gap silently dies. (.row needs none — it sits after .card
   and wins by file order.) */
.card.stack, .card.stack-tight, .card.stack-loose,
.card-outlined.stack, .card-outlined.stack-tight, .card-outlined.stack-loose {
  display: flex;
}
.card-clickable {
  text-decoration: none;
  cursor: pointer;
  transition:
    translate var(--duration-normal) var(--ease-spring),
    box-shadow var(--duration-normal) var(--ease-out);
}
.card-clickable:hover {
  translate: 0 -2px;
  box-shadow: var(--shadow-floating);
}
.card-clickable:active {
  translate: 0 0;
  box-shadow: var(--shadow-raised);
}

/* ----- CARD VARIANTS: one component, many jobs -----
   Each builds on .card (the surface primitive) and specialises it with one
   modifier — same lift, radius and padding underneath. .card-feature
   displays a feature: an icon badge over a heading and a line. .card-option
   is a selectable choice for quizzes and pickers: a real <label> around a
   radio or checkbox that lights the whole card when chosen — works with
   JavaScript off, keyboard operable. .card-inline is a compact horizontal
   item: leading media, a text block, an optional trailing action, for rich
   list rows. Combine any with .card-clickable to make the whole card a
   link. They use the neutral ramp's own roles (surface-hover fills,
   border-strong emphasis) so states stay consistent across themes.

   Usage:
     <article class="card card-feature">
       <span class="card-feature-icon"><svg data-icon="bolt"></svg></span>
       <h3 class="card-feature-title">Fast by default</h3>
       <p class="card-feature-text">No runtime between you and the platform.</p>
     </article>

     <label class="card card-option">
       <input type="radio" name="q1" class="card-option-input">
       <span class="card-option-mark" aria-hidden="true"></span>
       <span class="card-option-body"><span class="card-option-title">Paris</span></span>
     </label>

     <article class="card card-inline">
       <span class="card-inline-media"><svg data-icon="user"></svg></span>
       <span class="card-inline-body"><span class="card-inline-title">Ada Lovelace</span>
         <span class="card-inline-meta">Signed up today</span></span>
       <span class="card-inline-trailing">→</span>
     </article>
   -------------------------------------------------------------------------- */

/* Feature: an icon badge, a heading, a description, stacked. */
.card-feature { display: flex; flex-direction: column; gap: var(--space-inline); }
.card-feature-icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 2.75rem; height: 2.75rem;
  border-radius: var(--radius-panel);
  background: var(--surface-hover);
  color: var(--accent-3);
}
.card-feature-icon svg { width: 1.4rem; height: 1.4rem; }
.card-feature-title { margin: 0; font-size: var(--text-h4); }
.card-feature-text { margin: 0; color: var(--text-muted); font-size: var(--text-footnote); line-height: var(--leading-ui); }

/* Option: a selectable card. The real control is a radio (single-pick) or a
   checkbox (data-multi); :has(:checked) lights the whole card, so it works
   without JavaScript and stays keyboard operable. */
.card-option {
  display: flex; align-items: flex-start; gap: var(--space-inline);
  cursor: pointer;
  transition:
    border-color var(--duration-fast) var(--ease-out),
    background var(--duration-fast) var(--ease-out),
    box-shadow var(--duration-fast) var(--ease-out);
}
.card-option:hover { border-color: var(--border-strong); background: var(--surface-hover); }
/* The native control hides fully. The selector rides on .card-option so it
   out-specifies the base input[type="radio"] sizing — with the bare class
   alone the raw radio ghosts through behind .card-option-mark. */
.card-option .card-option-input {
  position: absolute; width: 1px; height: 1px; opacity: 0;
  margin: 0; padding: 0; border: 0; min-height: 0;
  appearance: none;
}
.card-option:has(.card-option-input:checked) {
  border-color: var(--accent-1);
  box-shadow: 0 0 0 1px var(--accent-1), var(--shadow-raised);
}
.card-option:has(.card-option-input:focus-visible) {
  outline: var(--focus-ring); outline-offset: var(--focus-offset);
}
/* the check indicator: a ring for a radio, a rounded square for a checkbox */
.card-option-mark {
  flex: none; width: 1.25rem; height: 1.25rem; margin-top: 1px;
  border: var(--border-element) solid var(--border-strong);
  border-radius: var(--radius-pill);
  display: grid; place-items: center;
  transition:
    background var(--duration-fast) var(--ease-out),
    border-color var(--duration-fast) var(--ease-out);
}
.card-option[data-multi] .card-option-mark { border-radius: var(--radius-control); }
.card-option-mark::after {
  content: ""; width: 0.5rem; height: 0.5rem; border-radius: var(--radius-pill);
  background: var(--on-accent-1); scale: 0;
  transition: scale var(--duration-fast) var(--ease-spring);
}
.card-option:has(.card-option-input:checked) .card-option-mark {
  background: var(--accent-1); border-color: var(--accent-1);
}
.card-option:has(.card-option-input:checked) .card-option-mark::after { scale: 1; }
.card-option-body { min-width: 0; }
.card-option-title { display: block; font-weight: var(--weight-strong); }
.card-option-note { display: block; color: var(--text-muted); font-size: var(--text-footnote); }

/* Inline: a compact horizontal item — media, text, trailing. */
.card-inline { display: flex; align-items: center; gap: var(--space-inline); }
.card-inline-media {
  flex: none; display: inline-flex; align-items: center; justify-content: center;
  width: 2.5rem; height: 2.5rem;
  border-radius: var(--radius-panel);
  background: var(--surface-hover);
  color: var(--accent-3);
}
.card-inline-media svg { width: 1.25rem; height: 1.25rem; }
.card-inline-body { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.card-inline-title { font-weight: var(--weight-strong); }
.card-inline-meta { color: var(--text-muted); font-size: var(--text-footnote); }
.card-inline-trailing { flex: none; margin-left: auto; color: var(--text-muted); }

/* ----- LIST: rows of items -----
   (docs mirror — identical to llms.txt · List)
   WHEN TO USE: any repeated row of items — settings, menus, feeds, search results. It is ONE component: every look is a composition of its slots, never a sibling component.

   What it is: a list of rows — each with an optional leading icon or avatar
   (.list-icon), a title (.list-title), an optional description line under it
   (.list-desc), and an optional trailing value or action (.list-trail). What
   it is for: settings, menus, feeds, search results, contact and file lists —
   any repeated row of items. When to use it: .list for rows on the page
   separated by hairlines; wrap the same markup in .list-boxed for a bordered
   card list. A row is one line (icon + title + trailing) or two (add
   .list-desc, which top-aligns the icon on its own). Make a row actionable by
   writing it as <a class="list-item" href> or <button class="list-item"> — it
   fills on hover, presses on click, and is a real keyboard-operable control
   (put a chevron in .list-trail to signal it). Every look is a composition
   of the same slots, never a new component: leave out .list-icon for a
   plain text row; put an avatar image in .list-icon for a person; put a
   .badge in .list-trail for a status; on a static <li> row, put a real
   button in .list-trail (a .btn-icon.btn-small with an aria-label) for a
   dismiss or per-row action — never inside an <a> or <button> row, where a
   nested control is invalid HTML. Rows are always direct children
   of .list, so a <ul> takes <li class="list-item"> and a clickable list is a
   <nav> of <a class="list-item">. Reuses the neutral ramp and type tokens, so
   it needs no tokens of its own and restyles with every theme.*/
.list {
  display: flex;
  flex-direction: column;
  list-style: none;
  margin: 0;
  padding: 0;
}
.list-boxed {
  border: var(--border-element) solid var(--border);
  border-radius: var(--radius-card);
  background: var(--surface);
  overflow: hidden;               /* clip row hovers to the rounded corners */
}
.list-item {
  display: flex;
  align-items: center;
  gap: var(--space-inline);
  min-height: 44px;               /* touch target */
  margin: 0;                      /* override the base li+li rhythm */
  padding: var(--space-inline) var(--space-card);
}
/* a hairline between adjacent rows, inside a box or bare on the page */
.list-item + .list-item {
  border-top: var(--border-hairline) solid var(--border-subtle);
}
/* leading slot: a glyph badge or a small avatar image */
.list-icon {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.25rem;
  height: 2.25rem;
  border-radius: var(--radius-panel);
  background: var(--surface-hover);
  color: var(--accent-3);
  overflow: hidden;
}
.list-icon svg { width: 1.25rem; height: 1.25rem; }
.list-icon img { width: 100%; height: 100%; object-fit: cover; }
/* text block: title, and an optional description under it */
.list-content {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-tight);
}
.list-title { font-weight: var(--weight-medium); }
.list-desc {
  color: var(--text-muted);
  font-size: var(--text-footnote);
  line-height: var(--leading-snug);
}
/* trailing slot: a value, a badge, an action or a chevron */
.list-trail {
  flex: none;
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  gap: var(--space-control);
  color: var(--text-muted);
  font-size: var(--text-footnote);
}
.list-trail svg { width: 1.15rem; height: 1.15rem; }
/* two-line rows top-align the icon so it sits with the title, not the middle */
.list-item:has(.list-desc) { align-items: flex-start; }
/* an actionable row: a real <a> or <button>, filling the whole width */
a.list-item,
button.list-item {
  width: 100%;
  border: none;
  background: none;
  color: inherit;
  font: inherit;
  text-align: left;
  text-decoration: none;
  cursor: pointer;
  transition: background var(--duration-fast) var(--ease-out);
}
a.list-item:hover,
button.list-item:hover { background: var(--surface-hover); }
a.list-item:active,
button.list-item:active { background: var(--surface-active); }

/* ----- STEPPER: minus, value, plus -----
   (docs mirror — identical to llms.txt · Stepper & segmented control)
   WHEN TO USE: the stepper for small counts where typing is overkill; the segmented control for flipping between two to five views of the same thing. Not for settings that act (the switch) or navigation (the tab bar).

   What it is: a quantity control — a real number input between two step
   buttons in one pill — and a segmented control, a pill of options where
   exactly one is pressed in, radios underneath. What it is for: the
   stepper for counts and small adjustments where typing is overkill; the
   segmented control for switching between 2–5 views or ranges of the
   same thing. When to use it: stepper for ranges of a few dozen at most
   (beyond that, the number input); segmented for choices that change
   what is shown — not settings that act (the switch) and not navigation
   (the tab bar). The stepper buttons call the input's own
   stepUp/stepDown so min/max/step rule; the radios keep arrows, form
   value and announcements native.*/
.stepper {
  display: inline-flex;
  align-items: center;
  padding: 2px;
  border: var(--border-element) solid var(--border);
  border-radius: var(--radius-pill);
  background: var(--surface);
}
.stepper button {
  width: 2.5rem;
  height: 2.5rem;
  border: none;
  border-radius: var(--radius-pill);
  background: transparent;
  color: var(--text);
  font-size: var(--text-h4);
  cursor: pointer;
  touch-action: manipulation;
  transition: background var(--duration-fast) var(--ease-out);
}
.stepper button:hover { background: var(--accent-2-hover); }
.stepper button:active { background: var(--accent-2-pressed); }
.stepper input {
  width: 3.5rem;
  border: none;
  background: transparent;
  text-align: center;
  font: inherit;
  font-weight: var(--weight-strong);
  color: var(--text);
  -moz-appearance: textfield;
  appearance: textfield;
}
.stepper input::-webkit-outer-spin-button,
.stepper input::-webkit-inner-spin-button { appearance: none; }

/* ----- SEGMENTED CONTROL: one pick, all options visible -----
   What it is: a pill of options where exactly one is pressed in —
   radios underneath, so the form value, keyboard arrows and screen
   reader behavior are all native. What it is for: switching between
   2–5 views or ranges of the same thing: 1Y/5Y/Max, list/grid. When
   to use it: choices that change what is shown, not settings that
   act (that is the switch) and not navigation (that is the tab bar).
   Markup: label > hidden radio + span per option. */
.segmented {
  display: inline-flex;
  margin: 0;                     /* it is often a fieldset */
  padding: 2px;
  gap: 2px;
  border: var(--border-element) solid var(--border);
  border-radius: var(--radius-pill);
  background: var(--surface);
}
.segmented label { position: relative; display: inline-flex; }
.segmented input {
  position: absolute;
  opacity: 0;
  width: 1px;
  height: 1px;
}
.segmented span {
  display: inline-flex;
  align-items: center;
  padding: var(--space-control) var(--space-stack);
  border-radius: var(--radius-pill);
  font-size: var(--text-footnote);
  color: var(--text-muted);
  cursor: pointer;
  transition:
    background var(--duration-fast) var(--ease-out),
    color var(--duration-fast) var(--ease-out);
}
.segmented input:checked + span {
  background: var(--background);
  color: var(--text);
  font-weight: var(--weight-strong);
  box-shadow: var(--shadow-raised);
}
.segmented input:focus-visible + span {
  outline: var(--focus-ring);
  outline-offset: var(--focus-offset);
}

/* ----- ROW: horizontal flow with wrap and themed gaps -----
   (docs mirror — identical to llms.txt · Row)
   WHEN TO USE: whenever small pieces sit side by side — button rows, chip sets, meta lines. The row owns the gap, so the children stay margin-free.

   What it is: things side by side that wrap when they run out of room,
   with the gap owned by the row. What it is for: button rows, chip sets,
   meta lines, any run of small pieces. When to use it: whenever things
   sit beside each other — the row owns the gap, so children stay
   margin-free, exactly like the stack owns vertical rhythm. Variants:
   .row-tight for close runs, .row-loose for more spacing.*/
.row,
.row-tight,
.row-loose {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-stack);
}
.row-tight { gap: var(--space-control); }
.row-loose { gap: var(--space-page); }

/* ----- GRID: responsive columns with themed gaps -----
   (docs mirror — identical to llms.txt · Grid)
   WHEN TO USE: any set of same-shaped pieces — cards, features, tiles. It decides the column count itself; you never write a breakpoint.

   What it is: an auto-fitting column grid — as many columns as fit, each
   at least a card wide, collapsing to one on phones with no breakpoints
   written. What it is for: card grids, feature grids, galleries,
   dashboards. When to use it: any set of same-shaped pieces; the grid
   decides the count, the content decides the size. Variants: .grid-wide
   for bigger cards, .grid-tight and .grid-loose for gap.*/
.grid,
.grid-wide,
.grid-tight,
.grid-loose {
  display: grid;
  gap: var(--space-stack);
  grid-template-columns: repeat(auto-fit, minmax(min(14rem, 100%), 1fr));
}
.grid-wide { grid-template-columns: repeat(auto-fit, minmax(min(20rem, 100%), 1fr)); }
.grid-tight { gap: var(--space-control); }
.grid-loose { gap: var(--space-page); }

/* ----- APP GRID: a launcher of app tiles -----
   (docs mirror — identical to llms.txt · App grid)
   WHEN TO USE: the front door of a suite of mini apps, a portal, a directory of tools — every tile one real link.

   What it is: a home-screen launcher — an auto-fill grid of app tiles,
   each tile one real link: a rounded-square icon, the app's name, a
   one-line caption, and an optional metric line. What it is for: the
   front door of a suite of mini apps, a portal, a directory of tools —
   many small destinations presented as equals. When to use it: when the
   destinations are apps rather than content — content belongs in the
   card grid or the list. The grid packs as many tile columns as fit,
   still two across on a phone, with no breakpoints written. A tile is
   an <a class="app-tile">: the whole tile is the hit target, it fills
   on hover and carries the keyboard focus ring. Put an open count or
   any live number in .app-count — tabular digits so tiles line up.
   Reuses the neutral ramp and type tokens, so it restyles with every
   theme.

   Usage:
     <nav class="app-grid" aria-label="Apps">
       <a class="app-tile" href="/apps/inflation">
         <img src="/apps/icons/inflation.png" alt="" width="68" height="68">
         <strong>Inflation calculator</strong>
         <span>Time-travel your money</span>
         <span class="app-count">1,240 opens</span>
       </a>
     </nav>
*/
.app-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(8.75rem, 100%), 1fr));
  gap: var(--space-stack) var(--space-control);
}
.app-tile {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  padding: var(--space-control) var(--space-tight);
  border-radius: var(--radius-card);
  text-decoration: none;
  color: inherit;
}
.app-tile:hover { background: var(--surface-hover); }
/* The icon: a full-bleed square image, rounded to the home-screen shape.
   22.5% is the icon's own geometry (the app-icon corner ratio), not a
   surface radius — the radius tokens stay for surfaces. */
.app-tile img {
  width: 4.25rem;
  height: 4.25rem;
  border-radius: 22.5%;
  display: block;
  margin-bottom: var(--space-inline);
}
.app-tile strong {
  font-size: var(--text-footnote);
  font-weight: var(--weight-medium);
  line-height: var(--leading-ui);
  margin-bottom: var(--space-tight);
}
.app-tile span {
  font-size: var(--text-caption);
  color: var(--text-muted);
  line-height: var(--leading-ui);
}
.app-tile .app-count {
  margin-top: var(--space-tight);
  font-variant-numeric: tabular-nums;
}

/* --------------------------------------------------------------------------
   PROMO RAIL

   (docs mirror — identical to llms.txt · Promo rail)
   WHEN TO USE: featured content at the top of a launcher or home screen — two to six promos; a single promo is just a card.

   What it is: the promo rail — a full-bleed carousel of wide feature
   cards, each washed with a different vivid-palette tint, snapping card
   by card with the next card peeking in from the edge. What it is for:
   featured content at the top of a launcher or home screen:
   announcements, highlights, seasonal picks. When to use it: two to six
   promos; a single promo is just a card. Each card is one real <a> — a
   .promo-kicker (small icon and muted name), a .promo-line in the
   heading face, and a .promo-cta with an arrow. The rail's negative side
   margins mirror the container's responsive padding exactly
   (space-stack, then space-page from 640px) — change one and you change
   both, or the page grows a sideways scrollbar. Tints cycle through the
   vivid palette by position, mixed into the surface so they stay soft in
   light and dark; the scrollbar hides and the whole card is the tap
   target.

   Usage:
     <div class="promo-scroll" aria-label="Featured">
       <a class="promo-card" href="/apps/inflation">
         <span class="promo-kicker"><img src="/icons/icon-192.png" alt=""
               width="28" height="28"> <span>Inflation calculator</span></span>
         <span class="promo-line">See what today's money was worth then</span>
         <span class="promo-cta">Open
           <svg data-icon="arrow-right" aria-hidden="true"></svg></span>
       </a>
     </div>
   -------------------------------------------------------------------------- */

/* full-bleed rail: the negative margin must MATCH the container's
   responsive padding (space-stack, then space-page from 640px) or the
   page grows a horizontal scrollbar */
.promo-scroll {
  display: flex;
  gap: var(--space-inline);
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  margin-inline: calc(-1 * var(--space-stack));
  padding-inline: var(--space-stack);
  scroll-padding-inline: var(--space-stack);
  padding-bottom: var(--space-tight);
  scrollbar-width: none;
  touch-action: pan-x pan-y;
  -webkit-overflow-scrolling: touch;
}
@media (min-width: 640px) {
  .promo-scroll {
    margin-inline: calc(-1 * var(--space-page));
    padding-inline: var(--space-page);
    scroll-padding-inline: var(--space-page);
  }
}
.promo-scroll::-webkit-scrollbar { display: none; }
.promo-card {
  flex: 0 0 min(24rem, calc(100% - 2.75rem));   /* the next card peeks */
  scroll-snap-align: start;
  display: flex;
  flex-direction: column;
  gap: var(--space-stack);
  padding: var(--space-card);
  border: var(--border-element) solid var(--border);
  border-radius: var(--radius-card);
  background: var(--surface);
  text-decoration: none;
  color: inherit;
}
/* the tints: each card washed with the next vivid-palette color, mixed
   into the surface so the wash stays soft in light and dark; red runs
   slightly lighter — it overpowers at equal strength */
.promo-card:nth-child(5n+1) { background: color-mix(in srgb, var(--color-4) 13%, var(--surface)); border-color: color-mix(in srgb, var(--color-4) 36%, var(--border)); }
.promo-card:nth-child(5n+2) { background: color-mix(in srgb, var(--color-3) 13%, var(--surface)); border-color: color-mix(in srgb, var(--color-3) 36%, var(--border)); }
.promo-card:nth-child(5n+3) { background: color-mix(in srgb, var(--color-2) 13%, var(--surface)); border-color: color-mix(in srgb, var(--color-2) 36%, var(--border)); }
.promo-card:nth-child(5n+4) { background: color-mix(in srgb, var(--color-1) 11%, var(--surface)); border-color: color-mix(in srgb, var(--color-1) 32%, var(--border)); }
.promo-card:nth-child(5n+5) { background: color-mix(in srgb, var(--color-5) 13%, var(--surface)); border-color: color-mix(in srgb, var(--color-5) 36%, var(--border)); }
.promo-kicker {
  display: flex;
  align-items: center;
  gap: var(--space-control);
  font-size: var(--text-caption);
  color: var(--text-muted);
}
.promo-kicker img { width: 1.75rem; height: 1.75rem; border-radius: 22.5%; display: block; }
.promo-line {
  font-family: var(--font-heading);
  font-size: var(--text-h3);
  font-weight: var(--weight-heading);
  line-height: var(--leading-heading);
  flex: 1;                    /* pushes the CTA to the card's foot */
}
.promo-cta {
  display: flex;
  align-items: center;
  gap: var(--space-tight);
  font-size: var(--text-footnote);
  font-weight: var(--weight-strong);
}
.promo-cta svg { width: 1em; height: 1em; }


/* --------------------------------------------------------------------------
   APP COMPONENTS — ingested verbatim from the shipped inflation app.
   Base recipes, tokens only; their velvet skins live in the velvet theme
   block. Every piece has its wall entry on the Library page; the copy button
   under each demo gives the exact markup contract.
     TAB BAR   .tab-bar > .tab-seg / .tab-divider / .tab-settings
     SHEET     .sheet-scrim + .sheet[data-open] > .sheet-scroll/.sheet-foot
     MENU LIST .menu-item / .menu-label / .menu-icon-row / .menu-icon-btn
     GAUGE     .scroll-gauge / .scroll-gauge-thumb / .scroll-gauge-page
   -------------------------------------------------------------------------- */


/* (docs mirror — identical to llms.txt · Tab bar)
   WHEN TO USE: the main navigation of an app-shaped page — a floating pill of three to five sections with the menu on its left and settings on its right; phones get it at the bottom, wide screens can move it to the top.

   What it is: the app's floating command bar, in the style of Apple's Photos pill. Three detached pieces sit in one row: a round .tab-menu button on the left that opens the navigation sheet from the LEFT edge, the long glass .tab-set pill of text segments with one raised highlight sliding behind the active one, and a round .tab-settings button on the right that opens the settings sheet from the RIGHT edge. Each piece is optional — write only the pill for a simple app, drop the menu or the settings button when there is nothing behind it; the markup IS the variant, nothing to configure. A segment is a <button> paging the snap pager (data-section) or a real <a> to another page (aria-current="page") — the site bar's tabs are links; on a narrow screen the pill scrolls sideways when the tabs outgrow it, starting with the current tab centred in view so nobody feels lost — and never re-centring once the reader scrolls the pill themselves. The highlight is one injected .tab-indicator moving on per-edge analytic springs: the leading edge leads with momentum, the trailing edge lags so the pill stretches, and both settle with a small fixed overshoot whatever the jump length. It wires itself to the page's snap pager ([data-section] segments page there, and the pager's moves slide the pill back behind a one-second nav lock so pass-through sections stay quiet); the corner buttons open whatever sheet they aria-controls, opening one condenses the other away first, and the whole bar rises above the sheet's scrim so the button that opened a panel stays visible as its close toggle. A tapped link segment glides the highlight onto itself before the page changes; coming back through the browser's back-forward cache puts the highlight back on the returning page's own tab. Add .tab-bar-top and wide screens pin the same bar to the TOP edge as the site bar: the pill caps at a centered reading-column width, the way content pages hold the middle of a wide screen. The convention inside it: the .tab-brand logo lockup on the left IS the home tab — the highlight sits around it when the home page is current, so Home never repeats in the tab row — the page tabs sit to the right side, and one optional .tab-action CTA can sit rightmost; the round menu and settings buttons hang just off the pill's edges, and their panels drop DOWN from those corners as phone-width glass dropdowns (see the sheet's .sheet-rise). Phones keep the bottom pill: the brand rides along as the pill's FIRST option — still the home tab, still highlightable — while the CTA slot hides itself; the same markup serves both shapes. The older anatomy (segments directly in the bar with a .tab-divider) still renders as the one-pill bar. Clearing the bar: the page never hand-measures it — put .page-under-bar on <main> (or body) with the .tab-bar-top site bar and it pads the top on wide screens and the bottom on phones, where the same bar folds back to the bottom pill; a page with the plain bottom .tab-bar pads itself with padding-bottom: var(--bar-clearance-bottom). The tokens behind it: --bar-height (48px, what the pill and corner buttons stand), --bar-clearance-top and --bar-clearance-bottom (height + the bar's edge inset + one card of air). What it is for: the primary navigation of one-page apps. When to use it: any app-shaped product; pages and documents take the top menu instead. On a real page it floats fixed; here it sits in the flow. */
/* ----- TAB BAR: the floating command bar (Photos-style pill) -----
   Embossed frosted glass: translucent over a backdrop blur, pressed INTO
   the page via inset shadows (dark from the top, faint from the bottom).
   The skin is shared by the legacy one-pill bar, the .tab-set pill and
   the detached round corner buttons; when the bar holds a .tab-set the
   bar itself becomes an invisible row and each piece wears the glass.
   The active highlight is one .tab-indicator pill (injected by
   facetTabIndicator) that moves between segments like iOS. */
.tab-bar,
.tab-set,
.tab-menu,
.tab-bar:has(.tab-set) > .tab-settings {
  /* Blur plus a mostly-surface ground: enough of the theme's own
     surface that labels keep contrast whatever scrolls beneath the
     glass — the translucency is the accent, not the material. */
  background: color-mix(in srgb, var(--surface) 62%, transparent);
  -webkit-backdrop-filter: blur(5px) saturate(1.4);
  backdrop-filter: blur(5px) saturate(1.4);
  border: var(--border-element) solid color-mix(in srgb, var(--border) 65%, transparent);
  border-radius: var(--radius-pill);
  box-shadow:
    inset 0 2px 5px color-mix(in srgb, var(--text) 22%, transparent),
    inset 0 -1px 3px color-mix(in srgb, var(--text) 9%, transparent),
    0 1px 0 color-mix(in srgb, var(--surface) 55%, transparent);
}
.tab-bar {
  position: fixed;
  bottom: calc(var(--space-inline) + var(--safe-bottom));
  left: 50%;
  transform: translateX(-50%);
  z-index: 30;
  display: flex;
  align-items: center;
  padding: var(--space-tight);
  white-space: nowrap;
  max-width: calc(100vw - 2 * var(--space-inline));   /* never wider than the screen */
}
/* The bar's footprint, as tokens — so a page clears the fixed bar
   without hand-measuring it. --bar-height is what the pill and the
   round corner buttons stand (48px); each clearance is that height
   plus the bar's inset at its edge plus one card of breathing room.
   Use them in your own padding, or take .page-under-bar below. */
:root {
  --bar-height: 48px;
  --bar-clearance-top: calc(var(--space-pin) + var(--safe-top) + var(--bar-height) + var(--space-card));
  --bar-clearance-bottom: calc(var(--space-inline) + var(--safe-bottom) + var(--bar-height) + var(--space-card));
}
/* Put .page-under-bar on the page's <main> (or body) when the page
   carries the .tab-bar-top site bar: it pads the top on wide screens
   (where the bar pins to the top edge) and the bottom on phones (where
   the same bar folds back to the bottom pill) — no more guessing bar
   heights per page. A page with the plain bottom .tab-bar pads itself
   with the token instead: padding-bottom: var(--bar-clearance-bottom). */
.page-under-bar { padding-bottom: var(--bar-clearance-bottom); }
@media (min-width: 40rem) {
  .page-under-bar {
    padding-top: var(--bar-clearance-top);
    padding-bottom: 0;
  }
}
/* When the tabs outgrow a narrow screen, the pill scrolls sideways
   inside itself instead of pushing the corner buttons off the edge. */
@media (max-width: 40rem) {
  .tab-set { overflow-x: auto; scrollbar-width: none; }
  .tab-set::-webkit-scrollbar { display: none; }
}
/* New anatomy: the bar is an invisible row; the pieces carry the glass. */
.tab-bar:has(.tab-set) {
  padding: 0;
  gap: var(--space-inline);          /* clear air between the pill and the corners */
  background: none;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  border: none;
  box-shadow: none;
}
/* While one of its panels is open, the bar rises above the sheet's scrim
   (40) and the sheet (41), so the opener stays visible as the close
   toggle — the same law as the float button. */
.tab-bar:has([aria-expanded="true"]) { z-index: 42; }
.tab-set {
  position: relative;                 /* anchors the indicator */
  display: flex;
  align-items: center;
  padding: var(--space-tight);
}
/* The round corner buttons: menu on the left edge, settings on the
   right — 48px, the same height the pill stands. */
.tab-menu,
.tab-bar:has(.tab-set) > .tab-settings {
  width: 48px;
  min-width: 48px;
  min-height: 48px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  color: var(--text-muted);
  cursor: pointer;
  touch-action: manipulation;
  transition: color var(--duration-fast) var(--ease-out);
}
.tab-menu[aria-expanded="true"] { color: var(--accent-pressed); }
/* The pill's slots. .tab-brand (the logo lockup, leftmost inside the
   pill) IS the home tab in every shape: on the desktop bar it anchors
   the left end, and on the phone pill it rides along as the FIRST
   option, scrolling with the tabs; the highlight sits around it when
   the home page is current. .tab-action (the one CTA) is desktop-only
   and hides itself on phones. */
.tab-brand {
  display: inline-flex;
  align-items: center;
  gap: var(--space-control);
  padding-inline: var(--space-stack);
  min-height: 40px;
  border-radius: var(--radius-pill);
  position: relative;
  z-index: 1;                         /* above the gliding indicator */
  font-weight: var(--weight-heading);
  text-decoration: none;
  color: var(--text);
  white-space: nowrap;
}
.tab-brand svg { flex: none; }
.tab-brand,
.tab-brand svg,
.tab-menu svg,
.tab-settings svg {
  /* the same survival halo for the brand and the corner glyphs */
  filter: drop-shadow(0 0 3px color-mix(in srgb, var(--surface) 90%, transparent));
}
.tab-action { display: none; }
/* Top placement: on wide screens the same bar pins to the top edge as
   the site bar — the round menu just off the pill's left, the round
   settings just off its right, and the pill itself capped at the
   standard content width (the same measure .container uses): logo on
   its left, the page tabs in the middle, the one CTA on its right.
   Phones below keep the bottom pill untouched. */
@media (min-width: 40rem) {
  .tab-bar-top {
    top: calc(var(--space-pin) + var(--safe-top));
    bottom: auto;
    left: var(--space-pin);
    right: var(--space-pin);
    transform: none;
    justify-content: center;          /* the pill caps; the corners hug it */
  }
  .tab-bar-top .tab-set {
    flex: 1 1 auto;
    max-width: 48rem;                 /* the reading-column convention: content width, never edge to edge */
    justify-content: center;          /* tabs centre when the slots are absent */
  }
  .tab-bar-top .tab-brand {
    margin-right: auto;               /* desktop: pushes the page tabs to the
                                         right side; the phone pill keeps the
                                         brand inline with the tabs instead */
  }
  .tab-bar-top .tab-action {
    display: inline-flex;
    margin-left: auto;                /* pushes itself to the pill's right */
    position: relative;
    z-index: 1;
  }
}
.tab-indicator {
  position: absolute;
  top: var(--space-tight);
  left: 0;
  height: calc(100% - var(--space-tight) * 2);
  width: 0;
  border-radius: var(--radius-pill);
  /* raised pill moving inside the inset bar; facetTabIndicator
     animates transform/width itself (squash-and-stretch), so no
     CSS transition here */
  background: color-mix(in srgb, var(--surface) 92%, transparent);
  box-shadow:
    0 2px 5px color-mix(in srgb, var(--text) 25%, transparent),
    inset 0 1px 0 color-mix(in srgb, var(--background) 85%, transparent);
  pointer-events: none;
}
.tab-seg {
  position: relative;
  z-index: 1;
  display: inline-flex;               /* a real <a> segment lays out like the button */
  align-items: center;
  justify-content: center;
  text-decoration: none;
  cursor: pointer;
  min-height: 40px;
  padding: 0 var(--space-stack);
  /* the survival halo: a surface-colored glow hugging the letters, so
     when something dark (or bright, in dark mode) slides under the
     glass the label keeps its own ground and stays readable */
  text-shadow:
    0 0 2px var(--surface),
    0 0 6px color-mix(in srgb, var(--surface) 85%, transparent);
  border: none;
  border-radius: var(--radius-pill);
  background: transparent;
  color: var(--text-muted);
  font-size: var(--text-footnote);
  font-weight: var(--weight-strong);
  /* letterpress: inactive labels sit pressed into the inset bar */
  text-shadow: 0 1px 0 color-mix(in srgb, var(--background) 65%, transparent);
  transition: color var(--duration-fast) var(--ease-out);
}
.tab-seg[aria-current="true"],
.tab-seg[aria-current="page"] {
  color: var(--accent-pressed);
  /* the active label sits on the raised pill: crisper, lighter press */
  text-shadow: 0 1px 0 color-mix(in srgb, var(--background) 40%, transparent);
}
.tab-divider {
  width: var(--border-element);
  height: var(--space-card);
  background: var(--border);
  margin-inline: var(--space-tight);
}
.tab-settings {
  position: relative;
  z-index: 1;
  width: 40px;
  min-width: 40px;
  min-height: 40px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: none;
  border-radius: var(--radius-pill);
  background: transparent;
  color: var(--text-muted);
  transition: color var(--duration-fast) var(--ease-out);
}
/* The open state shows in the ink alone — the glyph never spins. */
.tab-settings[aria-expanded="true"] {
  color: var(--accent-pressed);
}

/* (docs mirror — identical to llms.txt · Sheet & menu)
   WHEN TO USE: the app's edge panels, each behind one button — settings on the right (the default), navigation on the left (.sheet-left).

   What it is: the app's edge panel — a slab floating clear of the screen over a dark scrim, sliding in as one rigid block on a spring that overshoots slightly and eases back. The right edge is the settings side (the default); .sheet-left is the same panel sliding from the LEFT edge — the navigation side — and opening either closes the other first — never two panels moving at once. Add .sheet-rise and on phones the panel rises out of the tab bar instead: the bar's own frosted glass, condensing into and rising out of its own corner button as a narrow card — the menu slimmer on the left, settings wider on the right — standing only as tall as its options need — capped at half the screen so nothing sits out of thumb reach, taller content scrolling inside — mostly opaque so the options read, options left-aligned under eyebrow group names, a slim position thumb riding the panel's OUTER edge (left on the left panel, so that corner reads as the page's corner), and a foot of small labeled pill actions that never scrolls away. On wide screens the same panel becomes a corner DROPDOWN instead: it hangs from the top bar with its outer edge lined up with the bar's own corner button — a slim panel for the menu on the left, a wider one for settings on the right — options left-aligned, in the same glass, and the page does not dim (a click anywhere still closes it). Inside, menu groups of thin rows (.menu-item under a .menu-label) and a foot row of square icon toggles (.menu-icon-btn) whose ON state stands raised while OFF sinks into a darker well. What it is for: navigation, actions and app-level controls behind one trigger — any button that aria-controls the sheet opens it; scrim tap and Esc close; the page behind locks still while a panel is open; the trigger's aria-expanded is managed, a floating trigger stays visible above the open sheet as its close toggle, and a sheet left open never survives the browser's back-forward cache — it closes itself on return. When to use it: every app-shaped product's settings. Press a row: it sinks INTO the panel with a true bevel, never a dark smear. State changes ease — toggles must never jump. */
/* ----- SHEET: right-edge slide-in panel over a darkening scrim -----
   Vertically centred; caps at 60% of the viewport height and 80% of
   its width on phones. Content taller than the cap scrolls INSIDE
   .sheet-body. Toggle via data-open on both elements. */
/* The left-edge twin: .sheet-left slides the same panel in from the
   LEFT — the navigation side; settings stay on the right. Its closed
   resting spot is off the LEFT edge. */
.sheet-left {
  left: var(--space-inline);
  right: auto;
  transform: translate(calc(-100% - var(--space-stack)), -50%);
}

/* The riser: add .sheet-rise and the panel stops being an edge sheet.
   On phones it RISES out of the tab bar — as wide as the bar, from just
   above it. On wide screens it becomes a corner DROPDOWN under the top
   bar — a phone's worth of width, dropping from the bar's left edge for
   the menu (.sheet-left) and from its right edge for settings. Both
   shapes share everything else: the bar's glass made mostly opaque so
   the options read, only as tall as the options need capped at half the
   screen (taller content scrolls inside), options left-aligned, group
   names in the small eyebrow voice. facet.js reveals it with a clip
   wipe over its final layout — a CORNER wipe, condensing into and
   rising out of its own trigger button — so nothing reflows mid-motion,
   which is what keeps it smooth.
   Two classes on purpose: the base .sheet block sits later in the file
   and must lose here. */
.sheet.sheet-rise,
.sheet.sheet-rise.sheet-left {
  height: auto;                      /* only as tall as the options need... */
  max-height: 50dvh;                 /* ...never past half the screen */
  transform: none;
  overflow: hidden;
  /* the tab bar's glass, but mostly opaque — options must READ over
     whatever the dimmed page shows through */
  background: color-mix(in srgb, var(--surface) 85%, transparent);
  -webkit-backdrop-filter: blur(5px) saturate(1.4);
  backdrop-filter: blur(5px) saturate(1.4);
  border: var(--border-element) solid color-mix(in srgb, var(--border) 65%, transparent);
  border-radius: var(--radius-card);
  box-shadow:
    inset 0 2px 5px color-mix(in srgb, var(--text) 22%, transparent),
    inset 0 -1px 3px color-mix(in srgb, var(--text) 9%, transparent),
    0 1px 0 color-mix(in srgb, var(--surface) 55%, transparent);
}
/* the panel spans its own width — the edge sheet's right-hugging
   content band (width + margin-left auto) does not apply here */
.sheet.sheet-rise .sheet-scroll,
.sheet.sheet-rise .sheet-foot {
  width: 100%;
  margin-left: 0;
}
.sheet.sheet-rise .sheet-scroll {
  overflow-y: auto;                  /* taller content scrolls inside */
  /* rows pack tight — no gaps: their own 40px min-heights carry the
     rhythm, and each group name brings its own air in its padding */
  gap: 0;
  padding-top: var(--space-inline);  /* the same air above as beside */
}
.sheet.sheet-rise .sheet-scroll .sheet-group { gap: 0; }
/* group names read as eyebrows in both shapes */
.sheet-rise .menu-label {
  font-family: var(--font-mono);      /* the eyebrow voice */
  font-size: var(--text-caption);
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  font-weight: var(--weight-eyebrow);
  color: var(--text-muted);
  /* one clear rule under each group name, with air above it */
  padding-bottom: var(--space-control);
  border-bottom: var(--border-hairline) solid color-mix(in srgb, var(--border) 70%, transparent);
}
/* icons carry the same ink as their labels */
.sheet-rise .menu-item svg { color: currentColor; }
/* phones: the panel rises from just above the bottom bar off its own
   corner as a narrow card — the menu slimmer on the left, settings
   wider on the right. Options stay left-aligned in both shapes. */
@media (max-width: 40rem) {
  .sheet.sheet-rise,
  .sheet.sheet-rise.sheet-left {
    top: auto;
    /* just above the bar: its bottom inset + its 48px + one gap */
    bottom: calc(48px + 2 * var(--space-inline) + var(--safe-bottom));
  }
  .sheet.sheet-rise {
    left: auto;
    right: var(--space-inline);
    width: min(19rem, calc(100vw - 2 * var(--space-inline)));
  }
  .sheet.sheet-rise.sheet-left {
    left: var(--space-inline);
    right: auto;
    width: min(17rem, calc(100vw - 2 * var(--space-inline)));
  }
}
/* The position gauge: facet.js injects the library's own scroll gauge
   into a risen panel (iOS never shows a styled native scrollbar under
   the glass, so the thumb is ours). It rides the panel's OUTER edge —
   the LEFT edge on the left panel, so that corner reads as the page's
   own corner and the options shift toward the reader; the right edge
   on the right panel. */
.sheet.sheet-rise .scroll-gauge {
  top: var(--space-card);
  height: 30%;
}
.sheet.sheet-rise.sheet-left .scroll-gauge {
  left: var(--space-control);
  right: auto;
}
.sheet.sheet-rise.sheet-left .sheet-scroll {
  /* the options step clear of the left-edge gauge */
  padding-left: calc(var(--space-control) + 8px + var(--space-tight));
}
/* The foot: a row of labeled pill actions — real button faces that say
   what they do, sized for a thumb, one clear rule above, pinned outside
   the scroll so it never moves. */
.sheet-rise .sheet-foot {
  border-top: var(--border-hairline) solid color-mix(in srgb, var(--border) 70%, transparent);
  padding: var(--space-control) var(--space-inline);
}
.sheet-rise .menu-icon-btn {
  flex-direction: row;               /* icon beside its word, not above */
  gap: var(--space-tight);
  min-height: 40px;
  padding: var(--space-tight) var(--space-control);
  border-radius: var(--radius-pill);
  font-size: var(--text-caption);
  font-weight: var(--weight-strong);
  white-space: nowrap;
}
.sheet-rise .menu-icon-btn svg {
  flex: none;
  width: 16px;
  height: 16px;
  stroke-width: 2;
}
/* wide screens: a corner dropdown hanging from the top bar, its outer
   edge lined up with the bar's own corner button (the bar's cluster is
   centred, so the corner sits half the pill cap plus one button and
   one gap out from the middle; narrower viewports clamp to the pin
   inset). Options stay left-aligned; the menu panel is slim, the
   settings panel wider. The page does NOT dim under these — they are
   small; the scrim stays only as the click-anywhere-to-close surface. */
@media (min-width: 40rem) {
  .sheet.sheet-rise,
  .sheet.sheet-rise.sheet-left {
    top: calc(var(--space-pin) + var(--safe-top) + 48px + var(--space-inline));
    bottom: auto;
  }
  .sheet.sheet-rise {                /* settings: the right corner */
    left: auto;
    right: max(var(--space-pin), calc(50vw - 24rem - 48px - var(--space-inline)));
    width: 24rem;
  }
  .sheet.sheet-rise.sheet-left {     /* menu: the left corner, slim */
    left: max(var(--space-pin), calc(50vw - 24rem - 48px - var(--space-inline)));
    right: auto;
    width: 16rem;
  }
  .sheet-scrim:has(+ .sheet.sheet-rise) {
    background: transparent;
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }
}
/* While any sheet is open the page behind locks still — no wheel, no
   trackpad, no touch scroll reaches it (same law as the nav menu). */
body.sheet-open { overflow: hidden; touch-action: none; }
.sheet-scrim {
  position: fixed;
  inset: 0;
  z-index: 40;
  background: rgb(0 0 0 / 0.6);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--duration-slow) var(--ease-in-out);
}
.sheet-scrim[data-open="true"] {
  opacity: 1;
  pointer-events: auto;
}
.sheet {
  position: fixed;
  top: 50%;
  right: var(--space-inline);
  z-index: 41;
  width: min(80vw, 20rem);
  height: 80dvh;
  /* never taller than the viewport minus the island and the home bar */
  max-height: calc(100dvh - var(--safe-top) - var(--safe-bottom) - 2 * var(--space-inline));
  display: flex;
  flex-direction: column;
  /* Opaque surface so the panel reads as a clean, high-contrast card (white
     in Default light), not gray-on-gray. Aero re-declares its own frosted
     glass; the other themes inherit this solid fill. */
  background: var(--surface);
  border: var(--border-element) solid var(--border);
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-overlay);
  /* facetSheet drives entry/exit with the tab pill's per-edge springs
     (frame stretches, right edge never crosses its resting line) —
     no CSS transform transition here, JS owns transform + width. */
  transform: translate(calc(100% + var(--space-stack)), -50%);
  visibility: hidden;
}
/* Nav + actions scroll in the top region; the icon row lives in a foot
   pinned below it. The scroll region keeps the panel's RESTING width and
   anchors to the right edge, so while the frame's left edge overshoots
   and settles, the text inside lands linearly and then stays still —
   the tab bar's still-labels/moving-pill split, applied to the panel.
   The foot spans the frame itself, so its buttons stretch and snap back
   with the overshoot. */
.sheet-scroll {
  flex: 1;
  min-height: 0;
  width: min(80vw, 20rem);
  margin-left: auto;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: var(--space-card) var(--space-inline) var(--space-control);
  display: flex;
  flex-direction: column;
  justify-content: flex-start;        /* groups stack close, no spread */
  gap: var(--space-stack);
}
/* the foot keeps the RESTING width too: its buttons stay a fixed size
   while the frame overshoots around them */
.sheet-foot {
  width: min(80vw, 20rem);
  margin-left: auto;
  padding: var(--space-control) var(--space-inline) var(--space-inline);
}
.sheet-group {
  display: flex;
  flex-direction: column;
}
.sheet-scroll .sheet-group { gap: var(--space-tight); }
/* SCROLL GAUGE: a pill thumb inset in an embossed groove, top right —
   thumb height shows how much of the content is visible, position
   shows where you are. Spans only the top quarter; its left edge
   lines up with where the panel's corner radius ends. Momentum can
   push the thumb past the groove, where it clips away. Injected by
   facetScrollGauge; the native scrollbar hides in its favour. */
.sheet-scroll { scrollbar-width: none; }
.sheet-scroll::-webkit-scrollbar { display: none; }
.scroll-gauge {
  position: absolute;
  top: var(--space-stack);          /* clears the corner curve */
  right: var(--space-control);
  height: 25%;
  width: 8px;                   /* left edge lands at the radius line */
  padding: 2px;                 /* spacing around the pill */
  border-radius: var(--radius-pill);
  background: color-mix(in srgb, var(--text) 8%, transparent);
  box-shadow:
    inset 0 1px 2px color-mix(in srgb, var(--text) 20%, transparent),
    inset 0 -1px 1px color-mix(in srgb, var(--text) 8%, transparent),
    0 1px 0 color-mix(in srgb, var(--surface) 60%, transparent);
  overflow: hidden;             /* out-of-bounds momentum disappears */
  pointer-events: none;
}
.scroll-gauge-thumb {
  width: 100%;
  border-radius: var(--radius-pill);
  /* a light raised pill sitting in the inset groove */
  background: color-mix(in srgb, var(--surface) 96%, transparent);
  box-shadow:
    0 1px 2px color-mix(in srgb, var(--text) 30%, transparent),
    inset 0 1px 0 color-mix(in srgb, var(--background) 85%, transparent);
  transition: height var(--duration-fast) var(--ease-out);
}
/* Page-level variant: a fixed lane floating just off the right edge,
   half the viewport tall, vertically centred. The thumb is the
   viewport's true share of the page, so snaps and smooth scrolling
   read 1:1 on it. */
.scroll-gauge-page {
  position: fixed;
  top: 50%;
  right: var(--space-control);
  transform: translateY(-50%);
  height: 50dvh;
  width: 10px;
  z-index: 25;
  pointer-events: auto;
  cursor: grab;
  touch-action: none;         /* the gauge drags the page, never scrolls */
  transition: width 0.16s ease, right 0.16s ease;
}
/* grab: the lane widens and the pill highlights in the theme colour */
.scroll-gauge-page.grabbing {
  width: 18px;
  right: calc(var(--space-control) - 4px);
  cursor: grabbing;
}
.scroll-gauge-page .scroll-gauge-thumb { transition: background 0.16s ease; }
.scroll-gauge-page.grabbing .scroll-gauge-thumb {
  background: var(--v-gold);
  box-shadow: 0 1px 3px rgb(0 0 0 / 0.4);
}

/* ----- THEMED MAP: a Google Map styled with the active theme -----
   (docs mirror — identical to llms.txt · Themed map)
   WHEN TO USE: a Google map that must look like the page — facet.mapStyle() returns the active theme as a Maps styles array, so the map re-skins when the theme flips.

   A Google Map styled with the active theme — ground, water, parks and roads
   derived from the base family, labels from the text family, boundaries
   from the quiet accent, recomputed live when the theme or mode switches.
   Bring your own key (the library never ships one): put data-map on a
   div, the key in data-map-key (or data-maps-key on the facet.js script
   tag), optional data-lat / data-lng / data-zoom. Without a key, or
   offline, the div stays a quiet themed placeholder — never a broken
   embed. facet.mapStyle() returns the styles array for anything else.
   Styles derive from tokens by formula, never hand-tuned per theme, so
   every future theme gets its map for free.*/
[data-map] {
  aspect-ratio: 16 / 10;
  border: var(--border-element) solid var(--border);
  border-radius: var(--radius-panel);
  overflow: hidden;              /* the live canvas honours the radius */
  background:
    linear-gradient(var(--border) 1px, transparent 1px),
    linear-gradient(90deg, var(--border) 1px, transparent 1px),
    var(--surface);
  background-size: 2rem 2rem, 2rem 2rem, auto;
  display: grid;
  place-items: center;
  padding: var(--space-stack);
  color: var(--text-muted);
  font-size: var(--text-footnote);
  text-align: center;
}
[data-map="live"] {
  display: block;                /* hand layout back to the Maps canvas */
  padding: 0;
  background: var(--surface);
}

/* Apple-style continuous corners wherever a rectangle has a radius.
   Progressive: browsers without corner-shape keep plain rounding.
   Pills (tab bar, gauge) stay true capsules on purpose. */
@supports (corner-shape: squircle) {
  .sheet,
  .menu-item,
  .menu-icon-btn,
  .stat,
  .chart-card,
  .seg,
  .btn,
  input,
  .result {
    corner-shape: squircle;
  }
}

/* ----- MENU LIST: rows for a sheet or dropdown ----- */
.menu-item {
  display: flex;
  align-items: center;
  gap: var(--space-inline);
  width: 100%;
  min-height: 40px;
  padding: var(--space-control) var(--space-inline);
  border: none;
  border-radius: var(--radius-panel);
  background: transparent;
  color: var(--text);
  font-size: var(--text-footnote);
  font-weight: var(--weight-strong);
  text-align: left;
  text-decoration: none;
  transition: background var(--duration-fast) var(--ease-out);
}
.menu-item:hover { background: color-mix(in srgb, var(--background) 55%, transparent); }
.menu-item svg { flex: none; color: var(--text-muted); }
/* Trailing state readout: "Auto", "On", "Tilt"... */
.menu-value {
  margin-left: auto;
  color: var(--accent-pressed);
  font-weight: var(--weight-strong);
}
.menu-value[data-off="true"] { color: var(--text-muted); }
.menu-label {
  padding: var(--space-control) var(--space-inline) 0;
  margin-bottom: var(--space-control);
  font-family: var(--font-mono);   /* the eyebrow voice */
  font-size: var(--text-caption);
  font-weight: var(--weight-eyebrow);
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
}
.menu-divider {
  border: none;
  border-top: var(--border-hairline) solid var(--border);
  margin: var(--space-tight) var(--space-control);
}
/* Inset divider: a short centred hairline, not edge to edge. */
.menu-divider-inset {
  width: 55%;
  align-self: flex-start;
  margin-left: var(--space-inline);
  margin-right: auto;
}
/* A horizontal row of icon controls: equal widths, proper button faces,
   each with a tiny state caption. */
.menu-icon-row {
  flex-direction: row;
  align-items: stretch;
  gap: var(--space-control);
}
.menu-icon-btn {
  flex: 1 1 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-tight);
  min-height: 56px;
  padding: var(--space-control) var(--space-tight);
  border: var(--border-element) solid color-mix(in srgb, var(--border) 70%, transparent);
  border-radius: var(--radius-panel);
  /* ON reads raised: light face, soft drop shadow, lit top lip */
  background: color-mix(in srgb, var(--surface) 82%, transparent);
  box-shadow:
    0 2px 4px color-mix(in srgb, var(--text) 16%, transparent),
    inset 0 1px 0 color-mix(in srgb, var(--background) 85%, transparent);
  color: var(--text);
  transition:
    background var(--duration-fast) var(--ease-out),
    border-color var(--duration-fast) var(--ease-out),
    box-shadow var(--duration-fast) var(--ease-out),
    transform var(--duration-fast) var(--ease-out);
}
.menu-icon-btn:hover {
  background: color-mix(in srgb, var(--surface) 95%, transparent);
  border-color: var(--text-muted);
}
.menu-icon-btn:active { transform: translateY(1px); }
/* OFF reads pressed into the glass: darker well, inset shadows. Keyed on
   a .menu-value[data-off], or on the button itself when it has no value. */
.menu-icon-btn:has(.menu-value[data-off="true"]),
.menu-icon-btn[data-off="true"] {
  background: color-mix(in srgb, var(--text) 7%, transparent);
  border-color: color-mix(in srgb, var(--border) 50%, transparent);
  box-shadow:
    inset 0 2px 4px color-mix(in srgb, var(--text) 20%, transparent),
    inset 0 -1px 2px color-mix(in srgb, var(--text) 8%, transparent),
    0 1px 0 color-mix(in srgb, var(--surface) 60%, transparent);
  color: var(--text-muted);
}
.menu-icon-btn .menu-value {
  margin-left: 0;
  font-size: var(--text-caption);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 100%;
}

/* ================== end FACET-CANDIDATE COMPONENTS ================== */


/* --------------------------------------------------------------------------
   FLOAT BUTTON

   (docs mirror — identical to llms.txt · Float button)
   WHEN TO USE: the one floating control a screen needs — usually the menu or settings trigger the sheet wires itself to.

   What it is: a lone floating round button pinned to the bottom-left of the page — the tab bar's trailing icon without the bar. What it is for: pages that need the settings sheet (or any panel) but not section navigation: give it aria-controls pointing at a .sheet and facet.js wires the open/close, the aria-expanded state and the press feedback exactly like the tab bar's settings button. When to use it: whenever a page has one floating action; .float-btn-right pins it bottom-right instead. Always an aria-label and a data-tip: the icon alone says nothing. On a real page it floats fixed; here it sits in the flow — this one opens the sheet from the entry above.

   Usage:
     <button class="float-btn" aria-controls="settings-sheet"
             aria-expanded="false" aria-label="Open settings"
             data-tip="Settings" data-event="open-settings">
       <svg>...gear...</svg>
     </button>
     <div class="sheet-scrim" data-open="false"></div>
     <aside class="sheet" id="settings-sheet" data-open="false"
            aria-hidden="true" aria-label="Settings"> ... </aside>
   -------------------------------------------------------------------------- */

.float-btn {
  position: fixed;
  left: calc(var(--space-stack) + env(safe-area-inset-left, 0px));
  bottom: calc(var(--space-stack) + var(--safe-bottom));
  z-index: 30;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 52px;
  height: 52px;
  padding: 0;
  border: var(--border-element) solid color-mix(in srgb, var(--border) 65%, transparent);
  border-radius: var(--radius-pill);
  background: color-mix(in srgb, var(--surface) 32%, transparent);
  -webkit-backdrop-filter: blur(5px) saturate(1.4);
  backdrop-filter: blur(5px) saturate(1.4);
  box-shadow:
    inset 0 2px 5px color-mix(in srgb, var(--text) 22%, transparent),
    inset 0 -1px 3px color-mix(in srgb, var(--text) 9%, transparent),
    0 1px 0 color-mix(in srgb, var(--surface) 55%, transparent);
  color: var(--text);
  cursor: pointer;
  touch-action: manipulation;
  transition:
    background var(--duration-fast) var(--ease-out),
    transform var(--duration-normal) var(--ease-spring);
}
.float-btn:active { transform: translateY(1px) scale(0.96); }
.float-btn[aria-expanded="true"] { background: color-mix(in srgb, var(--surface) 92%, transparent); }
/* A float button that opens the sheet stays visible ABOVE the sheet's
   scrim (40) and the sheet itself (41), so the same button remains the
   close toggle instead of sinking behind its own overlay. */
.float-btn[aria-controls],
.float-btn-right[aria-controls] { z-index: 42; }

.float-btn-right {
  left: auto;
  right: calc(var(--space-stack) + env(safe-area-inset-right, 0px));
}

/* Velvet: the floating trigger is a raised face; open, it takes the
   pill styling like the tab bar's settings button. Press feedback, face never changes. */
[data-theme="velvet"] .float-btn {
  position: fixed;                 /* the .btn recipe must not unpin it */
  border: none;
  background: var(--v-face);
  box-shadow: var(--v-raised-2nd);
  color: var(--v-ink);
  transition: transform var(--v-press-out), box-shadow var(--v-press-out);
}
[data-theme="velvet"] .float-btn::after {
  content: ""; position: absolute; inset: 0; border-radius: var(--radius-pill);
  background: var(--v-dent-tap); opacity: 0; pointer-events: none;
  transition: opacity var(--v-press-out);
}
[data-theme="velvet"] .float-btn:active { transform: translateY(1px); transition-duration: 0.09s, 0.09s; }
[data-theme="velvet"] .float-btn:active::after { opacity: 1; transition: opacity var(--v-press-in); }
[data-theme="velvet"] .float-btn[aria-expanded="true"] {
  background: var(--v-face);
  box-shadow: var(--v-pill);
  color: var(--v-ink-strong);
}
[data-theme="velvet"] .float-btn svg { filter: var(--v-icon-filter); }

/* --------------------------------------------------------------------------
   NAV MENU

   (docs mirror — identical to llms.txt · Nav menu)
   WHEN TO USE: whole-site navigation on app-like pages and PWAs — the thumb-reachable Menu and Settings pills in a bottom corner.

   What it is: a floating navigation cluster pinned bottom-left — a "Menu" pill that opens a stack of page links, and a same-size round Settings button beside it that opens a stack of setting toggles. Both stacks rise above their own trigger; opening one closes the other, and neither trigger moves when a stack opens. Every link and the Menu pill are the base .btn.btn-pill (the Settings button is .btn-pill.btn-icon, canonical icon data-icon="sliders"); each setting is a .btn.btn-pill.nav-set whose on/off state shows in the pill itself: the pill stays solid in both states and only the trailing .menu-value chip changes (filled accent = on, outlined muted = off). What it is for: whole-site navigation on any Facet page, plus the app's own settings — theme, appearance, motion, sound, haptics, language — behind two thumb-reachable floating controls. When to use it: the primary nav for a content site or app. The Menu is a real <details> wrapping real <a> links, so it works with JavaScript off — the summary opens the links inline — and the links sit in the DOM for crawlers and AI. Every item is one line, left-aligned, sized to its own content and capped near 300px; wrap the label in .nav-label and a long one truncates with an ellipsis while the icon and any trailing .menu-value stay whole. facet.js upgrades both stacks to a floating overlay: a dimming scrim, body scroll-lock, a staggered reveal, Escape to close, and focus moved into the open stack and back. The setting pills self-wire through data-control (theme cycles Default/Velvet/Aero/Elegant; appearance is the separate Light/Dark/Auto and the two compose) and data-language-cycle. Clicking a nav link or action closes the panel; a .nav-set toggle leaves it open so several settings can be changed in a row. Reduced motion drops the stagger. Add .nav-menu-right to anchor the cluster bottom-right with right-aligned stacks (left stays the default); an optional .nav-menu-back round pill — a real link with a chevron-left — sits on the cluster's outer edge, in the canonical order [back] [Menu] [settings]. On a real page it floats fixed bottom-left or bottom-right; here it sits in the flow.

   Usage:
     <nav class="nav-menu" aria-label="Site" data-print="off">
       <a class="btn btn-pill btn-icon nav-menu-back" href="/"
          aria-label="Back"><svg data-icon="chevron-left"></svg></a>
       <details class="nav-menu-panel">
         <summary class="btn btn-pill nav-menu-trigger"
                  data-event="nav-menu"><svg>...menu...</svg> Menu</summary>
         <div class="nav-menu-items">
           <a class="btn btn-pill" href="/"><svg>...</svg> Home</a>
         </div>
       </details>
       <details class="nav-menu-panel">
         <summary class="btn btn-pill btn-icon nav-menu-trigger"
                  aria-label="Settings"><svg>...sliders...</svg></summary>
         <div class="nav-menu-items">
           <button type="button" class="btn btn-pill nav-set" data-control="sounds"
                   aria-label="Sound"><svg>...</svg> Sound
             <span class="menu-value">On</span></button>
         </div>
       </details>
     </nav>
   -------------------------------------------------------------------------- */

/* How much vertical room the fixed nav-menu occupies: its pill height (the
   44px touch target) plus its own bottom offset. Defined on :root so paged
   snap-sections can reserve exactly this much clearance and the two never
   drift apart. */
:root { --nav-menu-clearance: calc(2.75rem + var(--space-stack)); }

.nav-menu {
  position: fixed;
  left: calc(var(--space-stack) + env(safe-area-inset-left, 0px));
  bottom: calc(var(--space-stack) + var(--safe-bottom));
  z-index: 30;
  display: flex;
  align-items: flex-end;
  gap: var(--space-tight);
  user-select: none;                 /* a control, not content: never lands in a copy */
  -webkit-user-select: none;
}
/* each details holds a trigger and its pop-up stack; it must not stretch.
   It is NOT the positioning context — the stack anchors to .nav-menu (the
   whole cluster), so every stack, Menu or Settings, opens from the same
   left edge in the same column, never offset to its own trigger. */
.nav-menu-panel { display: flex; }

/* the Menu pill IS a .btn.btn-pill — strip the native disclosure marker */
.nav-menu-trigger { cursor: pointer; list-style: none; }
.nav-menu-trigger::-webkit-details-marker { display: none; }
.nav-menu-trigger::marker { content: ""; }
/* icons ride the button's text size, so they scale with the base button */
.nav-menu svg { width: 1.15em; height: 1.15em; flex: none; }

/* the stack floats ABOVE its own trigger, left-aligned to it, out of flow
   so the sibling trigger never moves when it opens */
.nav-menu-items {
  position: absolute;
  left: 0;
  bottom: calc(100% + var(--space-tight));
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-tight);
}

/* Every item is one line, left-aligned, sized to its own content, and
   capped so a long label can never run off-screen (never wider than ~300px
   nor than the viewport minus the menu's own side margins). Only the label
   may shrink and ellipsize (wrap it in .nav-label); the leading icon and any
   trailing value stay whole. The base .btn centres and wraps — this overrides
   that inside the nav only. Comfortable right padding comes from the pill. */
.nav-menu-items > * {
  justify-content: flex-start;
  text-align: left;
  white-space: nowrap;
  max-width: min(18.75rem, calc(100vw - 2 * var(--space-stack)));
  overflow: hidden;
}
.nav-menu-items .nav-label {
  min-width: 0;               /* shrink below the text so the ellipsis fires */
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Right-side variant: the whole cluster anchors bottom-right (safe-area
   aware) and every pop-up stack opens right-aligned to the cluster, so
   nothing can hang off-screen. Left stays the default. */
.nav-menu.nav-menu-right {
  left: auto;
  right: calc(var(--space-stack) + env(safe-area-inset-right, 0px));
}
.nav-menu.nav-menu-right .nav-menu-items {
  left: auto;
  right: 0;
  align-items: flex-end;
}

/* Back slot: an optional round pill — a real <a> back link with a
   chevron-left — on the cluster's outer edge. Canonical order:
   [back] [Menu] [settings]. */
.nav-menu-back { flex: none; }

/* setting pill: a toggle whose state shows IN the pill. The pill itself
   stays the solid base button in both states — a whole pill going
   translucent reads as broken next to its solid neighbours. Only the
   .menu-value chip changes: filled accent = on, outlined muted = off. */
.nav-set .menu-value {
  margin-left: auto;          /* push the value to the trailing edge */
  flex: none;                 /* the value never shrinks or truncates */
  font-size: var(--text-caption);
  font-weight: var(--weight-strong);
  line-height: 1.4;
  padding: 0.1rem 0.6em;
  border-radius: var(--radius-pill);
  border: 1px solid transparent;
  background: var(--accent-3);
  color: var(--on-accent-3);
}
.nav-set[aria-pressed="false"] .menu-value {
  background: transparent;
  border-color: var(--border);
  color: var(--text-muted);
}
/* Enhanced only: each pop-up link starts just below its resting spot and
   the JS adds .is-in one after another for the bottom-up cascade. With
   JS off there is no data-enhanced, so the links show at rest the moment
   the <details> opens — no flash, fully usable. */
.nav-menu[data-enhanced="true"] .nav-menu-items > * {
  opacity: 0;
  transform: translateY(6px);
  transition:
    opacity var(--duration-fast) var(--ease-out),
    transform var(--duration-normal) var(--ease-spring);
}
.nav-menu[data-enhanced="true"] .nav-menu-items > .is-in {
  opacity: 1;
  transform: none;
}

/* the dimming scrim behind an open menu: facet.js inserts one
   .nav-menu-scrim at the end of <body> and toggles data-open */
.nav-menu-scrim {
  position: fixed;
  inset: 0;
  z-index: 29;                       /* under the cluster (30), over content */
  background: rgb(0 0 0 / 0.45);
  -webkit-backdrop-filter: blur(2px);
  backdrop-filter: blur(2px);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--duration-slow) var(--ease-in-out);
}
.nav-menu-scrim[data-open="true"] { opacity: 1; pointer-events: auto; }
/* lock the page behind the overlay */
body.nav-menu-open { overflow: hidden; touch-action: none; }

@media (prefers-reduced-motion: reduce) {
  .nav-menu[data-enhanced="true"] .nav-menu-items > * {
    transition: none; opacity: 1; transform: none;
  }
  .nav-menu-scrim { transition: none; }
}

/* --------------------------------------------------------------------------
   CHOICE GRID

   (docs mirror — identical to llms.txt · Choice grid)
   WHEN TO USE: one exclusive pick from up to about ten compact, symbolic
   options — currencies, units, categories — too many for a segmented
   control, too visual for a select. What it is: a grid of small square
   choice buttons (.choice-grid of .choice-btn — a .choice-sym symbol
   above a .choice-cap caption), five per row on phones. aria-pressed
   carries the state and facet.js keeps the pick exclusive; on change the
   grid fires a bubbling CustomEvent "facet:choice" {value, button}
   (value = data-value ?? the button text), and
   facet.choiceSelect(grid, value) sets a pick from code (URL restore,
   reset). With JS off the buttons still render and submit nothing —
   pair with a hidden input when the form must post.

   Usage:
     <div class="choice-grid" role="group" aria-labelledby="cur-label">
       <button type="button" class="choice-btn" aria-pressed="true"
               data-tip="Indian rupee" data-event="cur-inr">
         <span class="choice-sym" aria-hidden="true">&#8377;</span>
         <span class="choice-cap">INR</span>
       </button>
       ...
     </div>
   -------------------------------------------------------------------------- */

.choice-grid {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: var(--space-control);
}
.choice-btn {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  min-height: 56px;
  padding: var(--space-control) 0;
  border: var(--border-element) solid var(--border);
  border-radius: var(--radius-panel);
  background: var(--surface);
  color: var(--text-muted);
  transition:
    background var(--duration-fast) var(--ease-out),
    border-color var(--duration-fast) var(--ease-out),
    color var(--duration-fast) var(--ease-out),
    transform var(--duration-normal) var(--ease-spring);
}
.choice-btn:active { transform: translateY(1px) scale(0.97); }
.choice-btn .choice-sym { font-size: var(--text-h4); font-weight: 700; }
.choice-btn .choice-cap { font-size: var(--text-caption); letter-spacing: var(--tracking-caps); }
.choice-btn[aria-pressed="true"] {
  background: var(--accent-2-hover);
  border-color: var(--text-muted);
  color: var(--text);
}
.choice-btn[aria-pressed="true"] .choice-sym { color: var(--accent-3); }

/* Velvet: wells for the field, a raised face for the pick, gold symbol. */
[data-theme="velvet"] .choice-btn {
  border: none;
  border-radius: var(--radius-panel);
  background: var(--v-well-bg);
  box-shadow: var(--v-pit);
  color: var(--text-muted);
  font-family: var(--v-font);
  transition:
    background 0.25s ease,
    box-shadow 0.25s ease,
    color 0.25s ease,
    transform var(--v-press-out);
}
[data-theme="velvet"] .choice-btn:active { transform: translateY(1px); }
[data-theme="velvet"] .choice-btn[aria-pressed="true"] {
  background: var(--v-face);
  box-shadow: var(--v-raised-2nd);
  color: var(--v-ink-strong);
}
[data-theme="velvet"] .choice-btn[aria-pressed="true"] .choice-sym { color: var(--v-gold); }

/* --------------------------------------------------------------------------
   SLIDER TICK SCALE

   What it is: a tick scale aligned to a slider's track — minor ticks,
   sparse labels, and an optional highlighted "now" tick marking a
   meaningful value (the current year where a projection begins). What
   it is for: giving a long range slider landmarks so positions read as
   values. When to use it: wrap slider and scale in a .slider-wrap; the
   scale insets by half the knob so tick positions stay true; facet.js
   builds the ticks from the slider's min/max (data-minor, data-label,
   data-now configure the steps) and rebuilds on facet.sliderScale(wrap)
   when the range changes.

   Usage:
     <div class="slider-wrap">
       <input type="range" class="slider" min="1960" max="2056" value="2000">
       <div class="slider-scale" data-minor="10" data-label="40"
            data-now="2026" aria-hidden="true"></div>
     </div>
   -------------------------------------------------------------------------- */

.slider-wrap {
  display: flex;
  flex-direction: column;
  min-width: 0;
}
.slider-wrap .slider { width: 100%; }
.slider-scale {
  position: relative;
  height: 18px;
  margin: -3px 16px 0;                /* aligned to the track; half-knob inset */
}
.slider-scale .tick {
  position: absolute;
  top: 0;
  width: 1px;
  height: 5px;
  background: var(--border);
}
.slider-scale .tick-label {
  position: absolute;
  top: 7px;
  transform: translateX(-50%);
  font-size: var(--text-caption);
  color: var(--text-muted);
  opacity: 0.8;
}
.slider-scale .tick-now {
  width: 2px;
  height: 8px;
  background: var(--accent-3);        /* velvet: accent-3 IS the gold */
}
.slider-scale .tick-now-label { color: var(--accent-3); opacity: 1; }

/* --------------------------------------------------------------------------
   INSTALL NUDGE CARD

   What it is: a card over a dim blurred scrim, sitting just above the
   bottom controls: eyebrow, title, one-line description, a primary "Add now", a
   dimmed "Add it later", fine-print "Don't ask again". What it is for:
   the add-to-home-screen prompt, driven by facet.installNudge —
   shown once per session after a delay, never when standalone,
   installed or opted out. When to use it: installable products; wire
   "Add now" to installNudge.addNow() and show the instruction overlay
   when it returns "guide" (iOS has no native prompt).

   Usage (markup contract, from the shipped app):
     <div class="nudge-scrim" id="a2hs-nudge" hidden>
       <div class="nudge-card" role="dialog" aria-modal="true"
            aria-labelledby="nudge-title">
         <span class="eyebrow">One tap away</span>
         <h2 id="nudge-title">Add this to your Home Screen</h2>
         <p>Quick access — full screen, works offline.</p>
         <button type="button" class="btn btn-primary nudge-add" data-event="nudge-add">Add now</button>
         <button type="button" class="btn nudge-later" data-event="nudge-later">Add it later</button>
         <button type="button" class="nudge-never" data-event="nudge-never">Don't ask again</button>
       </div>
     </div>
   -------------------------------------------------------------------------- */

.eyebrow {
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  font-weight: var(--weight-eyebrow);
  text-transform: uppercase;   /* the eyebrow is ALWAYS caps, whatever case the author wrote */
  letter-spacing: var(--tracking-wide);
  color: var(--accent-3);
  display: block;
}
.nudge-scrim {
  position: fixed;
  inset: 0;
  z-index: 55;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  padding: var(--space-card);
  padding-bottom: calc(var(--space-inline) + 48px + var(--safe-bottom) + var(--space-page));
  background: rgb(0 0 0 / 0.5);
  -webkit-backdrop-filter: blur(3px);
  backdrop-filter: blur(3px);
}
.nudge-card {
  width: min(100%, 22rem);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-inline);
  padding: var(--space-page) var(--space-card) var(--space-stack);
  border-radius: calc(var(--radius-card) + 12px);
  background: var(--background);
  box-shadow: var(--shadow-overlay);
  text-align: center;
}
.nudge-card h2 { font-size: var(--text-h3); }
.nudge-card p { color: var(--text-muted); font-size: var(--text-footnote); max-width: 30ch; }
.nudge-add { width: 100%; min-height: 52px; margin-top: var(--space-control); }
.nudge-later {
  width: 100%;
  border: none;
  background: transparent;
  box-shadow: none;
  color: var(--text-muted);
  opacity: 0.7;
}
.nudge-never {
  border: none;
  background: transparent;
  padding: var(--space-control);
  font-size: var(--text-caption);
  color: var(--text-muted);
  opacity: 0.6;
  text-decoration: underline;
  text-underline-offset: 2px;
}
[data-theme="velvet"] .nudge-card {
  background: var(--v-bg);
  box-shadow:
    0 30px 70px rgb(0 0 0 / 0.5),
    0 10px 28px rgb(0 0 0 / 0.32),
    var(--v-bevel);
}

/* --------------------------------------------------------------------------
   INSTRUCTION OVERLAY

   What it is: the whole screen goes near-black and the direction sits
   at the top — eyebrow, large title with the key phrase
   highlighted, muted body lines, a faint tap-anywhere-to-close. What it
   is for: full-screen how-tos; shipped as the iOS Add-to-Home-Screen
   walk-through. When to use it: any instruction that deserves the whole
   screen. The palette is deliberately fixed — the screen goes dark in
   every theme — with the highlight in the pale gold that reads on black.

   Usage:
     <div class="overlay-guide" hidden>
       <span class="eyebrow">Install this app</span>
       <p class="overlay-title">Select <strong>"Add to Home Screen"</strong></p>
       <p class="overlay-sub">Tap Safari's Share button, then choose it.</p>
       <p class="overlay-sub" style="opacity:.55">Tap anywhere to close</p>
     </div>
   -------------------------------------------------------------------------- */

.overlay-guide {
  position: fixed;
  inset: 0;
  z-index: 60;
  background: rgb(0 0 0 / 0.94);      /* deliberately fixed: dark in every theme */
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: var(--space-stack);
  padding: calc(var(--safe-top) + var(--space-hero)) var(--space-card) var(--space-hero);
}
.overlay-guide .eyebrow { color: #dcbe8e; }   /* pale gold on black, fixed */
.overlay-title {
  font-size: clamp(2rem, 9vw, 2.8rem);
  font-weight: 800;
  letter-spacing: -0.02em;
  line-height: 1.15;
  color: #ffffff;
  max-width: 20ch;
}
.overlay-title strong { color: #dcbe8e; }
.overlay-sub { color: rgb(255 255 255 / 0.6); font-size: var(--text-h4); }

/* Velvet capsule number input: the amount reads as one rounded capsule. */
[data-theme="velvet"] .number-input input {
  border-radius: var(--radius-pill);
  padding-inline: var(--space-card);
  min-height: 54px;
}
