/* ==========================================================================
   AGENT BASE — DESIGN SYSTEM
   --------------------------------------------------------------------------
   Colour and geometry are pixel-sampled from the Magnific reference captures
   in `magnific ui/`. Depth is calibrated against measured reference shadows.
   Motion, spacing and the type scale are ours. Every token below says which
   it is: `measured` (with the capture) or `derived`.

   ESPECIFICACION.md carries the scan behind every measured number.

   ── THE ONE IDEA ─────────────────────────────────────────────────────────
   The reference reaches for a 1px hairline whenever surface and shadow are
   not enough on their own — its fields (both themes), its dark floating
   overlays, its dark secondary buttons. It is NOT a borderless system.

   The brief forbids hairlines: "todo es un juego de colores, containers y on
   hover". So: everywhere the reference would draw a line, we spend a surface
   step or a shadow instead. That single trade explains every deviation in
   this file, and each one is priced against a measurement.

   THE THREE LAWS
   1. NO BORDER CARRIES A COLOUR — our choice, not the reference's. See below.
   2. ELEVATION LIFTS TOWARD LIGHT — in BOTH themes. canvas < page < panel.
      dark  #101010 -> #161616 -> #1a1a1a      (+6, +4)   [measured]
      light #f5f5f5 -> #fafafa -> #ffffff      (+5, +5)   [measured]
   3. DEPTH IS EARNED, NOT DECORATED. Structural surfaces sit *in* the layout
      plane and never cast. Overlays leave the plane and do — which is what
      the reference does too, measured in both themes.

   ── LAW 1, HONESTLY ──────────────────────────────────────────────────────
   What is measured, and what is ours:

     BORDERLESS (measured, holds):  light structural containers. A card edge
       is a hard colour change — image copy 12, x=1200: y1172 #fafafa ->
       y1173 #ffffff. No border pixel, no ramp. Panels are separated by an
       8px gutter of canvas.

     HAIRLINED (measured, and we refuse to follow):
       - fields, BOTH themes — image copy 12 x=1200: #fafafa | #e7e7e7 | #ffffff
                               image copy 5  x=1874: #1a1a1a | #535353 | #2b2b2b
       - dark floating overlays — image copy 6 y=600, walking OUT: #1a1a1a
                                  (panel, x1498-1515) | #191919 (x1516-1521 —
                                  the dropdown's SHADOW, not the page) |
                                  #303030 (x1522, the hairline) | #191919 (its
                                  fill). The fill sits ONE level off its panel
                                  and the hairline carries the edge. image copy
                                  10 y=1420: rail and palette both carry 1px
                                  #252525.

                                  An earlier draft called this a ZERO step by
                                  reading the shadow as the page — the exact
                                  error that got law 3 retracted, committed a
                                  second time. A scan must terminate on a value
                                  proven to be the page by walking clear of the
                                  shadow, never on the first value that looks
                                  flat.
       - dark secondary buttons — image copy 6 y=644 "View all": #474747
                                  hairline around a fill identical to the
                                  backdrop. Transparent fill, line only.

   Note what this kills: the tempting rule "a border appears where the surface
   step fails" is FALSE. The reference's dark field is #2b2b2b on #1a1a1a — a
   17-level step, the exact step ours ships — and it is hairlined anyway.

   So borderless is not a property we found in the reference and copied. It is
   the brief, and we pay for it: a stronger fill on fields, a surface step
   where the reference used a line, a heavier shadow in dark where the
   reference leaned on #303030.

   TIERS
   Tier 1 `--p-*`  raw palette. Theme-agnostic. Never referenced by components.
   Tier 2 `--*`    semantic. Theme-aware. The only thing components may touch.
   ========================================================================== */

/* ── Typeface ─────────────────────────────────────────────────────────────
   Only Light (300) and Book (400) exist. Requesting 500/600/700 makes the
   browser SYNTHESISE a smeared fake bold. Since 1.13.0 the base text weight
   is Light (Blank, Hector 2026-10-01: `--weight-body` on `body`, inherited by
   buttons and fields); Book is emphasis — hierarchy comes from size, colour
   and tracking. Display sizes use 300, where Book reads heavy. `<strong>` in
   user content is the one sanctioned exception (meaning beats purity).
   Enforced by scripts/gate.mjs (`npm test`). */
@font-face {
  font-family: "PP Neue Montreal";
  src: url("fonts/PPNeueMontreal-Book.woff2") format("woff2"),
       url("fonts/PPNeueMontreal-Book.woff") format("woff");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "PP Neue Montreal";
  src: url("fonts/PPNeueMontreal-Light.woff2") format("woff2"),
       url("fonts/PPNeueMontreal-Light.woff") format("woff");
  font-weight: 300;
  font-style: normal;
  font-display: swap;
}

/* ══════════════════════════════════════════════════════════════════════════
   TIER 1 — PALETTE PRIMITIVES
   ══════════════════════════════════════════════════════════════════════════ */
:root {
  /* Neutral ramp — measured. The two ends are the two themes' ladders. */
  --p-ink-950: #101010;   /* measured — dark canvas      */
  --p-ink-900: #161616;   /* measured — dark page        */
  --p-ink-890: #191919;   /* measured — light text; the ink half of the
                             inverted primary button                    */
  --p-ink-880: #1a1a1a;   /* measured — dark panel       */
  --p-ink-800: #2b2b2b;   /* measured — dark fill        */
  --p-ink-600: #424242;
  --p-ink-550: #4a4a4a;  /* derived — a control on a hovered card, and the
                            floating chrome that has to clear it */   /* measured — dark skeleton    */

  /* Text greys, sampled from glyph cores, then nudged where the reference
     fails WCAG AA. Every deviation is tabled in ESPECIFICACION.md § Contrast.  */
  --p-muted-on-dark:  #ababab;  /* measured                        */
  --p-muted-on-light: #616161;  /* measured                        */

  --p-snow-100: #eaeaea;  /* measured — light fill-strong */
  --p-snow-90:  #ededed;  /* measured — light fill        */
  --p-snow-50:  #f5f5f5;  /* measured — light canvas; the paper half of the
                             inverted primary button                    */
  --p-snow-30:  #fafafa;  /* measured — light page        */
  --p-snow-0:   #ffffff;  /* measured — light panel       */

  /* Brand — the pink fills are gone (accent is the foreground colour now); only
     the tint that still labels the accent-badge in light theme remains. */
  --p-pink-750: #923e6a;   /* derived — AA-corrected from #a54578 (S-026-b,
                              gradient stops): the .ds-badge--accent text
                              colour in the light theme, on its own tint
                              over every stop of the Blank ground (worst:
                              the last stop, --p-day-low).                  */

  --p-violet-500: #5568ea;         /* measured — light icon/link  */
  --p-violet-400: #758af0;         /* measured — dark  icon/link  */
  --p-violet-tint-light: #e9ebf9;  /* measured — tool-tile bg     */
  --p-violet-tint-dark:  #20222c;  /* measured — tool-tile bg     */
  --p-violet-soft-light: #d5ddfb;  /* measured — "Beta" pill bg   */
  --p-violet-soft-dark:  #272930;  /* measured — "Beta" pill bg   */
  --p-violet-text-light: #252a80;  /* measured — "Beta" wordmark  */
  --p-violet-text-dark:  #95aaf4;  /* measured — "Beta" wordmark  */

  --p-sand-400: #ae9684;           /* measured — 3D affordance    */
  --p-sand-650: #907b6b;           /* derived  — AA-corrected from #9b8675
                                      (S-026-b, gradient stops). The measured
                                      sand is 2.45:1 on its own light tint,
                                      under the 3:1 an icon needs; #9b8675
                                      cleared it on paper and not on the Blank
                                      ground (2.71:1 on its last stop,
                                      #dfe0de), so it is the same hue darker,
                                      the first step that clears every stop. */
  --p-sand-750: #6f6155;           /* derived  — AA-corrected from #7e6d5f
                                      (S-026-b, gradient stops). Sand as TEXT:
                                      -650 is icon-corrected (3:1) and fails
                                      1.4.3 as a word. #7e6d5f was the
                                      lightest same-hue sand clearing 4.5:1 on
                                      the old canvas (#f5f5f5) and reads
                                      4.15:1 on the Blank canvas, so it is the
                                      same hue darker, the first step that
                                      clears every pairing of
                                      scripts/contrast.mjs on every stop of
                                      the Blank ground. */
  --p-sand-tint-light: #f2efed;    /* measured                    */
  --p-sand-tint-dark:  #262321;    /* measured                    */

  /* Red is NOT from the reference — an exhaustive scan of all 15 captures
     finds zero pixels of either value. These are Apple system reds, carried
     over from the app's existing palette. Marked derived so the header above
     stays true. */
  --p-red-500: #ff453a;   /* derived — the dot/fill red. On dark, 5.11:1     */
  --p-red-600: #de332a;   /* derived — Apple's #ff3b30 is 3.55:1 on paper    */
  --p-red-400: #ff554b;   /* derived — danger AS TEXT on dark, incl. on its
                             own soft tint, where #ff453a is 4.26. AA-
                             corrected from #ff5147 (S-026-b, gradient
                             stops): 4.42:1 on its tint over the Blank
                             night ground's first stop, moved toward the
                             night ink to the first value that passes.    */
  --p-red-750: #a82821;   /* derived — AA-corrected from #be2c24 (S-026-b,
                             gradient stops): danger AS TEXT on light,
                             worst case its own soft tint over the Blank
                             ground's last stop                            */

  /* Blank (S-026-b, 1.11.0) — the Design Studio prototype "Blank" is the
     source (Hector, 2026-10-01): its day and night grounds and inks, sampled
     from its `--ti-*` palette (docs/blank-inventory.md, Palette). Where its
     translucent soft ink, faint ink and track fail WCAG AA on the grounds
     this system pairs them with, the rung is the prototype colour flattened
     over the canvas and moved toward the ink (away from it for the track)
     until every pairing of scripts/contrast.mjs passes (Hector's ruling):
     derived, each saying what it was corrected from. */
  --p-day-top:   #f4f4f2;  /* measured — Blank day, the ground's first stop  */
  --p-day-mid:   #ebebe8;  /* measured — Blank day, the canvas               */
  --p-day-low:   #dfe0de;  /* measured — Blank day, the ground's last stop   */
  --p-day-ink:   #151515;  /* measured — Blank day ink                       */
  --p-night-top: #1b1c1e;  /* measured — Blank night, the ground's first stop */
  --p-night-mid: #141516;  /* measured — Blank night, the canvas             */
  --p-night-low: #0e0f10;  /* measured — Blank night, the ground's last stop */
  --p-night-ink: #ececea;  /* measured — Blank night ink                     */
  --p-day-muted:   #5d5d5d;  /* derived — AA-corrected from rgba(21,21,21,.55) (S-026-b, gradient stops) */
  --p-day-faint:   #636362;  /* derived — AA-corrected from rgba(21,21,21,.45) (S-026-b, gradient stops) */
  --p-day-track:   #dadad7;  /* derived — AA-corrected from rgba(21,21,21,.08) */
  --p-night-muted: #929291;  /* derived — AA-corrected from rgba(236,236,234,.5) */
  --p-night-faint: #919291;  /* derived — AA-corrected from rgba(236,236,234,.4) */
  --p-night-track: #2a2a2b;  /* derived — AA-corrected from rgba(236,236,234,.10) */
  --p-green-500: #34c759;  /* measured — Blank's live dot (Apple system green) */
  /* Blank (S-026-c, 1.12.0): the frosted ground of the filter card and its
     summary chip (Blank's --ti-bg-top at 82%, a colour-mix it wrote in place),
     the checked segment's ground (--ti-light), and the live dot on light
     grounds. */
  --p-day-frost:     rgba(244, 244, 242, 0.82);  /* measured — Blank day frost    */
  --p-night-frost:   rgba(27, 28, 30, 0.82);     /* measured — Blank night frost  */
  --p-day-segment:   rgba(255, 255, 255, 0.9);   /* measured — Blank day --ti-light */
  --p-night-segment: rgba(200, 205, 212, 0.1);   /* measured — Blank night --ti-light */
  --p-green-650: #248a3e;  /* derived — AA-corrected from #34c759 (S-026-c):
                              the live dot is 1.68:1 on the light ground's last
                              stop, under the 3:1 a shape needs; the same hue
                              and saturation darker, the first step that
                              clears 3:1 on every light stop, the panel and the
                              page, bare and hovered (worst: hovered on the
                              last stop, 3.01:1). */
  /* Blank (S-026-d, 1.13.0): the memory kinds' colours, a shape beside the
     kind's word (a dot), AA-corrected from Blank's (#5f93d8, #dc7d5e,
     #18a898 by day; #6392dc, #d6745a, #28a797 by night): each the same hue
     and saturation darker, the first step that clears 3:1 on every ground a
     kind's dot meets (the canvas's stops, the panel, the page, the hover,
     the hairline and the frost over each, and --text under a pressed pill).
     By day all three failed on the hairline over the last stop; by night
     only on --text. */
  --p-kind-skill-day:   #3677ce;  /* derived — from #5f93d8 (2.12:1)           */
  --p-kind-skill-night: #5488d9;  /* derived — from #6392dc (2.66:1 on --text) */
  --p-kind-fact-day:    #c7522b;  /* derived — from #dc7d5e (1.97:1)           */
  --p-kind-fact-night:  #d3694d;  /* derived — from #d6745a (2.73:1 on --text) */
  --p-kind-event-day:   #138579;  /* derived — from #18a898 (1.99:1)           */
  --p-kind-event-night: #249788;  /* derived — from #28a797 (2.51:1 on --text) */

  /* ── Geometry ─────────────────────────────────────────────────────────── */
  --radius-xs:   4px;   /* derived */
  /* Radii — measured by the AREA of the corner, not by fitting its curve.
     A quarter-arc of radius R removes R²(1 − π/4) from its corner box, so R
     falls out of an integral of coverage. That matters: the reference's card
     corners carry only 4–5 grey levels, and a point-fit on a signal that
     coarse lets the radius and the curve exponent trade off against each
     other. Four drafts of this file said 13, 15.4, 16.00 and 13.4 — each a
     different method, each with a story attached. An integral averages the
     quantisation out instead of steering on it.

     The estimator is CALIBRATED against known values rendered at the same
     grey-level counts, rather than trusted:
       border-radius:16px @ 5 levels  ->  reads 16.15  (error 0.15px)
       border-radius:8px  @ 13 levels ->  reads  8.04  (error 0.04px)  */
  --radius-chip: 12px;  /* derived — a tile or a portrait inside a card,
                           the rung between a card (8) and a container (16).
                           The website has carried it since its first sheet. */
  --radius-sm:   8px;   /* measured — 7.98 (tool tile, image copy 12), and the
                           reference's own buttons agree: 7.98 ("New agent",
                           image copy 6) and 8.00 ("Customize", image copy 12).
                           Tile and control share one rung.                  */
  --radius-lg:  16px;   /* measured — 15.94 (image copy 12, card corner
                           224,1173) and 15.95 (image copy 2, card corner
                           224,486). Two themes, two captures, spread 0.00px.
                           Card and container are one rung. The floating rail
                           is NOT claimed here: it carries a #252525 hairline
                           that contaminates the area integral, so it isn't a
                           clean target and no number is asserted for it.    */
  --radius-xl:  20px;   /* OURS — not measured, and no claim that the reference
                           has it. The reference's largest surface is a card in
                           a grid; ours is a full-height live viewport, and at
                           that size 16 reads as square. Only the live media
                           surface earns it, so --radius-lg stays the container
                           rung everywhere else.                             */
  --radius-pill: 999px;

  --gutter: 8px;        /* measured — 16 device px between every panel,
                           identical in both themes                         */
  --tile-size: 48px;    /* measured — every tool tile is 96 device px       */
  --rail-width: 58px;   /* the reference measures 72 (x=16..159 device at
                           y=800, image.png), narrowed deliberately: 34px
                           item + 12 either side, so the rail's inset matches
                           the --space-3 every panel already pads by         */
  --rail-item: 34px;    /* measured — the reference's "+" tile is 33 CSS;
                           34 keeps it on the 2px grid                      */
  --sidebar-width: 220px; /* derived — the panel's nav column (S-007 report
                             §3); a tags cap and a slug basis land on 220
                             by coincidence and are not this token        */

  /* ── Sizes (derived — S-007 report §3, values the panel repeats) ──────── */
  --icon-sm: 16px;        /* derived — icon box: nav tabs, menu items, the
                             logo mask; 15 and 17px glyphs fold into it     */
  --icon-md: 18px;        /* derived — chevron and filter icon              */
  --icon-lg: 20px;        /* derived — the plus in the round create button  */
  --icon-xl: 24px;        /* derived — nav tab icons on the compact layout  */
  --avatar-size: 22px;    /* derived — the account avatar                   */
  --avatar-size-touch: 26px; /* derived — the avatar in the compact tab bar,
                                sized against --icon-xl                     */
  --control-lg: 44px;     /* derived — a touch-size control (the dock's
                             search pill, filter and create buttons)        */
  --dot-size-sm: 6px;     /* derived — a small status dot (memory expiry)   */
  --dot-size: 8px;        /* derived — a separator dot                      */
  --dot-size-lg: 9px;     /* derived — chapter and drawer dots. .ds-dot keeps
                             its own 7px; unifying the dots is later work   */
  --field-min-sm: 120px;  /* derived — min width of a compact inline field  */
  --field-min: 160px;     /* derived — min width/height of a field          */
  --scroll-max-sm: 200px; /* derived — max height of a small scroll box     */
  --scroll-max-md: 240px; /* derived — an agent-created tool's folded code
                             (S-007 report, DS-6)                          */
  --scroll-max-lg: 420px; /* derived — a tall log pane (DS-6). Equal to
                             --measure-narrow by coincidence: a height, not
                             a column, so not that token                   */
  /* The switch (derived — S-007 report §3 and DS-5): the panel's two sizes.
     The knob keeps --switch-inset on every side of both tracks, so the travel
     is width - knob - 2 × inset. Its own inset token: --space-1 (4px) would
     break 3 + 16 + 3 = 22. */
  --switch-width: 38px;     /* derived — the switch track                   */
  --switch-height: 22px;    /* derived                                      */
  --switch-knob: 16px;      /* derived — the knob                           */
  --switch-inset: 3px;      /* derived — the knob's gap to the track, both
                               sizes                                        */
  --switch-width-sm: 30px;  /* derived — the small switch, sharing a line
                               with text                                    */
  --switch-height-sm: 18px; /* derived                                      */
  --switch-knob-sm: 12px;   /* derived                                      */
  /* Lists and content (derived — S-007 report §3 and DS-6). A hairline is a
     surface one rule wide, never a border (law 1): --rule-width is its
     thickness. The prose rhythm is the panel's, in em so it follows the size
     of the text it spaces: a custom property holds the text `0.7em`, and the
     em resolves against the element that spends it. */
  --rule-width: 1px;             /* derived — a row divider's thickness     */
  --prose-gap: 0.7em;            /* derived — after a paragraph, list,
                                    block of code or table                  */
  --prose-heading-top: 1.1em;    /* derived — above a prose heading         */
  --prose-heading-bottom: 0.4em; /* derived — below a prose heading         */
  --prose-indent: 1.35em;        /* derived — a list's indent               */
  --prose-item: 0.18em;          /* derived — between list items            */
  --prose-code-y: 0.1em;         /* derived — inline code, top and bottom   */
  --prose-code-x: 0.35em;        /* derived — inline code, left and right   */
  /* Chat and media (derived — S-007 report §3 and DS-7). A control key is in
     em, as the panel writes it, so it follows the size of its label. */
  --radius-bubble: 14px;      /* derived — a chat bubble's corner            */
  --bubble-max: 80%;          /* derived — a bubble's share of its column    */
  --control-key: 2.4em;       /* derived — a remote-control key's min width  */
  --control-key-field: 14em;  /* derived — the remote-control text field     */

  /* ── Measures (derived — S-007 report §3) ─────────────────────────────── */
  --measure-narrow: 420px; /* derived — a narrow sheet (pairing, camera)    */
  --measure-form: 460px;   /* derived — a form column                       */
  --measure-dock: 620px;   /* derived — a command bar and what it docks     */
  --measure-page: 900px;   /* derived — the reading measure of a view       */
  --measure-lead: 58ch;    /* derived — the lead text under a view's title
                              (S-007 report, DS-4): a measure of characters,
                              so it follows the text size it caps          */
  --measure-layer: 520px;  /* measured — Blank: a Blur Layer's card (DS-11)  */
  --find-width: 420px;     /* measured — Blank: the search bar behind Actions
                              (DS-12); --measure-narrow by coincidence     */
  --filter-card-width: 520px; /* measured — Blank: the filter card (DS-12);
                              --measure-layer by coincidence               */
  --filter-card-max: 360px;   /* measured — Blank: the open filter card's
                              tallest; it scrolls past it (DS-12)          */
  --filter-label-width: 78px; /* measured — Blank: a filter row's label
                              column (DS-12)                               */

  /* ── Header band (measured — Blank, DS-11) ────────────────────────────── */
  --header-height: 54px;   /* the band across the top of every view          */
  --header-mid: 32px;      /* its mid-line, where the title and the menu
                              button sit. Equal to --space-7 by coincidence:
                              a line, not a space                           */
  --header-fade-extra: 0px; /* an extra offset in the band's fade mask: a
                              view with a tool row under the band raises it */

  /* ── Footer band and Blank's glyphs (measured — Blank, DS-12) ─────────── */
  --footer-height: 54px;   /* the band across the bottom of every view       */
  --footer-lift: 16px;     /* its distance from the bottom edge             */
  --lines-width: 26px;     /* the lines glyph (menu, Actions): its long bar  */
  --lines-weight: 1.5px;   /* the thickness of each of its bars             */
  --avatar-size-lg: 56px;  /* the initials over the name in Settings        */

  /* ── Memory kinds, card, timeline and graph (measured — Blank, DS-13) ── */
  --swatch-size: 18px;     /* a colour swatch; --icon-md by coincidence      */
  --memcard-width: 520px;  /* the memory card; --measure-layer by coincidence */
  --timeline-row-width: 180px; /* a moment of the events timeline          */
  --preview-mark-size: 64px;   /* an artifact preview's initial             */
  --tooltip-max: 260px;        /* the graph's tooltip and birth label (Blank's
                                  260 and 280)                              */
  /* The memory graph (measured — Blank's three.js graph, DS-13): what the
     panel's graph reads at runtime (readNumber()). Radii are in the graph's
     own units (the whole graph has a radius of 1). */
  --graph-link-alpha: 0.05;    /* a relation's opacity                       */
  --graph-hub-alpha: 0.85;     /* a subject hub's opacity                    */
  --graph-node-radius: 0.02;   /* a memory                                   */
  --graph-hub-radius: 0.013;   /* a subject's hub                            */
  --graph-glow-radius: 0.05;   /* a memory lit by a recall                   */
  --graph-spark-radius: 0.014; /* a spark running along a relation           */
  /* DS-16 (1.14.0): the figure's two kinds of stem, Blank's own alphas
     (layer.js), drawn in --graph-link. */
  --graph-trunk-alpha: 0.32;   /* a subject's stem, from the centre to its hub */
  --graph-branch-alpha: 0.2;   /* a memory's stem, from its hub to it         */

  /* ── Breakpoint (derived — S-007 report §3) ───────────────────────────── */
  --bp-compact: 640px;    /* derived — the compact (phone) layout. A CONSTANT,
                             not a switch: a media query cannot read var(),
                             so consumers write @media (max-width: 640px)
                             literally and this token documents the number */

  /* ── Space — 4px base (derived) ───────────────────────────────────────── */
  --space-half: 2px;  /* derived — the hairline gap between stacked tabs and
                         rows (S-007 report §3)                             */
  --space-1:  4px;
  --space-2:  8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 20px;
  --space-6: 24px;
  --space-7: 32px;

  /* ── Type (derived) ───────────────────────────────────────────────────── */
  --font: "PP Neue Montreal", system-ui, -apple-system, sans-serif;
  --font-mono: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, monospace;

  --text-2xs: 11px;
  --text-xs:  12px;
  --text-sm:  13px;
  --text-base: 14px;
  --text-md:  16px;
  --text-lg:  20px;
  --text-xl:  26px;
  --text-2xl: 34px;
  --text-band: 18px;  /* measured — Blank: the band title and a Blur Layer's
                         title, uppercase and tracked (DS-11)             */

  --weight-display: 300;  /* display sizes only — Book reads heavy at 26px+ */
  --weight-normal: 400;   /* the ceiling for everything else: emphasis      */
  --weight-body: 300;     /* the base text weight (Blank, DS-13): Light on
                             body, inherited by buttons and fields          */

  --tracking-tight: -0.02em;
  --tracking-wide: 0.09em;
  --tracking-band: 0.14em;  /* measured — Blank: a band title's caps (DS-11) */

  --leading-tight: 1.3;
  --leading-base: 1.5;

  /* ── Motion (derived) ─────────────────────────────────────────────────── */
  --dur-fast: 120ms;
  --dur-base: 180ms;
  --dur-slow: 260ms;
  --ease-out: cubic-bezier(0.2, 0.7, 0.2, 1);
  --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
  /* Depth by receding (measured — Blank, DS-11). A plane behind an overlay
     steps back: smaller, blurred, dimmer. Deep is a plane under a Blur
     Layer's card, nearly gone; the rest keep a trace. A layer's card and a
     view settle in on --ease-settle; a pressed row shrinks to --press-scale. */
  --ease-settle: cubic-bezier(0.22, 1, 0.36, 1);
  --dur-recede: 650ms;        /* the plane's scale                          */
  --dur-layer: 450ms;         /* a fade or a blur: the plane, a layer, a view */
  --dur-settle: 480ms;        /* a Blur Layer's card arriving               */
  --dur-drop: 520ms;          /* a view's child dropping in                 */
  --stagger-step: 60ms;       /* between two children of a cascade (--i, --j) */
  --drop-distance: 14px;      /* how far a dropping child or a card travels */
  --enter-blur: 10px;         /* a view's blur as it enters or leaves       */
  --recede-scale: 0.94;
  --recede-blur: 5px;
  --recede-opacity: 0.45;
  --recede-opacity-deep: 0.05;
  --press-scale: 0.98;        /* a row held under a finger                  */
  --frost-blur: 14px;         /* Blank: what a frosted card blurs behind it
                                 (DS-12)                                    */
  --dim-opacity: 0.35;        /* Blank: a section switched off (DS-12): dim,
                                 grey and inert                             */

  /* ── Edge fade (derived) — the mask that dissolves scrolled content at its
        container edge, so a list ends by fading instead of being guillotined */
  --fade-size: 24px;
  --fade-blur: 6px;   /* derived — the reference's ramp spans ~14 device px
                         (7 CSS) at the tile scale; 6px reads similarly at the
                         larger scale a chat log needs. Not measured: the
                         reference's radius grows with distance and ours does
                         not, so there is no single number to sample. */
  --fade-mask-y: linear-gradient(
    to bottom,
    transparent 0,
    #000 var(--fade-size),
    #000 calc(100% - var(--fade-size)),
    transparent 100%
  );
  --fade-mask-x: linear-gradient(
    to right,
    transparent 0,
    #000 var(--fade-size),
    #000 calc(100% - var(--fade-size)),
    transparent 100%
  );
  --fade-mask-bottom: linear-gradient(
    to bottom,
    #000 0,
    #000 calc(100% - var(--fade-size)),
    transparent 100%
  );
}

/* ══════════════════════════════════════════════════════════════════════════
   TIER 2 — SEMANTIC · DARK (default)
   ══════════════════════════════════════════════════════════════════════════ */
:root {
  color-scheme: dark;

  --surface-canvas: var(--p-night-mid);   /* Blank night (1.11.0) */
  /* The room's ground, a three-stop gradient on .ds-canvas (DS-11): the
     canvas is its middle stop, these its first and last (Blank). */
  --surface-ground-top: var(--p-night-top);
  --surface-ground-low: var(--p-night-low);
  --surface-page:   var(--p-ink-900);
  --surface-panel:  var(--p-ink-880);
  --surface-fill:        var(--p-ink-800);
  --surface-fill-strong: var(--p-night-track);
  --surface-skeleton:    var(--p-ink-600);

  /* What a terminal paints behind its text. xterm takes colours as values, not
     CSS, so it cannot simply be transparent over whatever it sits on — it has to
     be TOLD the surface. Declared once here so the default is right for the
     common case (a terminal in a panel) and any pane on a different surface
     restates it for its own subtree; term-pane.js reads it off the mount
     element, so it inherits like any other token. Themed by inheritance too:
     both palettes below redefine --surface-panel, and this follows. */
  --term-bg: var(--surface-panel);

  --text:       var(--p-night-ink);
  --text-muted: var(--p-night-muted);
  --text-faint: var(--p-night-faint);

  /* State layers — alpha, so they compose over ANY surface. 7% in both
     themes, which is exactly what the measured fills resolve to. */
  --state-hover: rgba(236, 236, 234, 0.07);   /* Blank night (1.11.0) */
  --state-press: rgba(255, 255, 255, 0.12);   /* derived */
  /* The prototype's hairline (DS-11): law 1 lets a border carry it only as
     `var(--rule-width) solid|dashed var(--hairline[-strong])`. */
  --hairline:        rgba(236, 236, 234, 0.05);   /* Blank night */
  --hairline-strong: rgba(236, 236, 234, 0.2);    /* Blank night */
  /* Blank's frosted ground (DS-12): the filter card and its summary chip,
     over a backdrop blur of --frost-blur; and a checked segment's ground. */
  --surface-frost:   var(--p-night-frost);
  --surface-segment: var(--p-night-segment);

  /* Pink does three jobs and one value cannot do all three, so it doesn't
     pretend to:
       --accent        NON-TEXT fill — the "+" glyph, a dot, the tint source.
                       3.03:1 vs white: right for an icon (WCAG 1.4.11 asks
                       3:1), wrong for a word. It is 2.90:1 against the light
                       PAGE — a tenth short — so a pink fill belongs on a
                       panel, not loose on the page.
       --accent-solid  a fill that CARRIES a label. White on it: 5.64:1.
       --accent-text   pink used AS text. Flips per theme — the measured pink
                       reads on ink but not on paper.                        */
  /* Accent recoloured to the foreground itself (per request: white, not pink).
     Pointing at --text / --surface-canvas keeps it theme-adaptive — near-white
     on ink in dark, near-black on paper in light — so it stays legible and the
     contrast gate holds in both themes without a separate override. */
  --accent:              var(--text);
  --accent-solid:        var(--text);
  --accent-solid-hover:  var(--text-muted);
  --accent-text:         var(--text);
  --accent-contrast: var(--surface-canvas);
  --accent-soft:     rgba(236, 99, 171, 0.16);   /* derived */

  /* Violet splits the same way pink does, and for the same reason: the value
     we measured (#5568ea) is a tool-tile GLYPH — an icon colour. It is 4.42:1
     on the light page, fine for an icon (3:1) and short for a word (4.5:1).
     So --accent-2 is the icon/shape; words use --accent-2-text. */
  --accent-2:      var(--p-violet-400);
  --accent-2-tint: var(--p-violet-tint-dark);
  --accent-2-soft: var(--p-violet-soft-dark);
  --accent-2-text: var(--p-violet-text-dark);
  /* The focus ring is SOLID. It spent three review rounds as a 22% tint, which
     composited to 1.36-1.40:1 against every dark surface — a ring nobody can
     see is not a ring, and WCAG 2.4.11 asks 3:1. The gate now asserts it.
     ::selection keeps a tint, because selection paints UNDER text and must not
     bury it. */
  --accent-2-ring:      var(--p-violet-400);   /* 5.51:1 on panel, 6.03 on canvas */
  --accent-2-selection: rgba(117, 138, 240, 0.22);

  --sand:      var(--p-sand-400);
  --sand-text: var(--p-sand-400);   /* 6.21:1 on panel — carries a word as-is */
  --sand-tint: var(--p-sand-tint-dark);

  /* Tooltip — inverted in BOTH themes, which is the point rather than an
     oversight. The reference measured #2b2b2b for dark (image copy 3), and a
     chip that dark reads as one more rung of the same ladder: on a panel two
     shades away it needs a hairline to be seen, and law 1 has no hairline.
     Inverting instead makes the chip the one white object on a dark screen —
     it cannot be mistaken for a surface, so it needs no border to say it is
     floating. Light already did this in the other direction (a dark chip on
     paper, image copy 13); this makes the two themes agree on the RULE
     (invert) rather than on the value. */
  --surface-tooltip: var(--p-snow-0);
  --text-on-tooltip: var(--p-ink-950);

  /* The matte behind a screencast. Not a rung of the ladder: it is the
     letterbox around a photograph, so it stays ink in BOTH themes — a white
     matte would light-bomb a dark VM frame. */
  --surface-matte: var(--p-ink-950);

  /* ── Text on MEDIA ─────────────────────────────────────────────────────
     A frame is a photograph, not a surface. These do NOT flip with the theme —
     they flip with the picture. A label over a screencast has to survive a
     black terminal and a white document, and the app's theme knows about
     neither: in light theme, --text is ink, and ink on a terminal is invisible.
     So the component measures the frame and picks a side (see frameIsLight()
     in app.js) and these are the two sides.

     They are deliberately absent from the light blocks: media does not care
     what theme you are in.

     Honest limit: over an ARBITRARY image no pair guarantees WCAG contrast —
     a mid-grey frame defeats both. The measurement picks the better side and
     the blur veil buys the rest; it is best-effort by nature, which is why the
     contrast gate asserts these against nothing. */
  --media-ink:       var(--p-ink-890);
  --media-ink-dim:   var(--p-ink-800);
  --media-paper:     var(--p-snow-50);
  --media-paper-dim: var(--p-muted-on-dark);
  --state-on-light:  rgba(0, 0, 0, 0.07);
  --state-on-dark:   rgba(255, 255, 255, 0.07);

  --danger:      var(--p-red-500);
  --danger-text: var(--p-red-400);
  --danger-soft: rgba(255, 69, 58, 0.16);        /* derived */

  /* Blank (DS-11): a live connection's dot, a shape beside its words, and
     the colour wheel a custom-colour swatch wears. Since 1.12.0 (DS-12) the
     dot is themed: the light blocks darken it to --p-green-650 (AA). */
  --status-live: var(--p-green-500);
  --swatch-wheel: conic-gradient(#e34948, #eda100, #1baf7a, #3987e5, #9085e9, #e87ba4, #e34948);

  /* Blank (DS-13): the memory kinds' colours, Skill, Fact and Event, each a
     dot beside the kind's word; themed (the light blocks take the day
     rungs). A viewer's pick overrides one inline on the element that carries
     the theme (setKindColour(); SPEC, DS-13). */
  --kind-skill: var(--p-kind-skill-night);
  --kind-fact:  var(--p-kind-fact-night);
  --kind-event: var(--p-kind-event-night);
  /* The swatches a viewer picks a kind's colour from (DS-13): Blank's, then
     the two ends of the ladder. Unthemed and ungated: a pick is the viewer's
     own choice, and the kind is always written beside its colour (WCAG
     1.4.1); a swatch's hairline edge shows the two ends on either theme. */
  --swatch-blue:   #3987e5;
  --swatch-orange: #eb6834;
  --swatch-green:  #1baf7a;
  --swatch-amber:  #eda100;
  --swatch-pink:   #e87ba4;
  --swatch-leaf:   #008300;
  --swatch-violet: #9085e9;
  --swatch-red:    #e34948;
  --swatch-sky:    #6392dc;
  --swatch-clay:   #d6745a;
  --swatch-teal:   #28a797;
  --swatch-white:  #ffffff;
  --swatch-black:  #000000;
  /* The memory graph's colours (DS-13), read at runtime by the panel's
     three.js graph (readColour(), on the graph's own element): a memory
     with no kind, a relation, a subject's hub and a lit memory's halo are
     the ink, so they are restated in the scoped theme blocks (D12); the
     material each node's kind colour tints is the light end. */
  --graph-node: var(--text);
  --graph-link: var(--text);
  --graph-hub:  var(--text);
  --graph-glow: var(--text);
  --graph-tint: var(--swatch-white);

  /* Primary action is INVERTED — measured in DARK (image copy 6, y=644):
     #f5f5f5 paper carrying #191919 ink, not pure white on pure black. */
  --action-bg:       var(--p-snow-50);
  --action-bg-hover: var(--p-snow-100);   /* derived — an explicit surface,
                                             because brightness() on #f5f5f5
                                             moves two levels and on #ffffff
                                             moves nothing at all           */
  --action-text:     var(--p-ink-890);

  /* Depth — law 3. Structural surfaces get NONE of this.

     MEASURED, dark (image copy 6, x=1900, below the agent dropdown):
       y692 #303030 (its hairline) | y694-733 #181818..#191919 | y734 #1a1a1a
     — a ~20 CSS px shadow that bottoms out 2 levels under the page. A whisper.
     It can afford to be: the #303030 hairline is doing the separating.

     Ours is deliberately HEAVIER in dark, because law 1 took that hairline
     away and something has to carry the edge. This is an amplification of a
     measured shadow, not an invention — the earlier claim that "the reference
     casts no shadows at all" was a misreading of this very scan (#191919 was
     read as the page; the page is #1a1a1a, and #191919 was the shadow).

     Realistic = a tight contact umbra + a mid ambient + a wide diffuse, all
     cast downward (light lives above) with negative spread so they don't
     bleed sideways. Layer geometry is derived. */
  --shadow-raised:
    0 1px 2px -1px rgba(0, 0, 0, 0.5),
    0 4px 10px -3px rgba(0, 0, 0, 0.45);
  --shadow-overlay:
    0 2px 6px -2px rgba(0, 0, 0, 0.55),
    0 10px 24px -6px rgba(0, 0, 0, 0.5),
    0 28px 56px -14px rgba(0, 0, 0, 0.45);
  --shadow-modal:
    0 4px 10px -4px rgba(0, 0, 0, 0.6),
    0 20px 44px -10px rgba(0, 0, 0, 0.55),
    0 56px 112px -28px rgba(0, 0, 0, 0.5);

  /* Edge light — where a raised edge catches the light from above. Carries the
     elevation that a shadow cannot express on a near-black page. */
  --edge-light: inset 0 1px 0 rgba(255, 255, 255, 0.06);

  --scrim: rgba(0, 0, 0, 0.6);

  /* A CHIP THAT FLOATS ON A CARD. The store's filter row and its back control
     sit ON the hero, not in it, and they were painted --surface-panel: in dark
     that is DARKER than the card underneath, so a control that leaves the
     plane read as a hole punched through to the page. Overlay surfaces move
     AWAY from the ground they float on — up in dark, white in light. */
  /* A CONTROL ON A CARD is not the card's hover. They were the same token, so
     a button on a row vanished the moment the row lifted under the pointer —
     the same defect at the other end of the interaction from the one where
     the button matched the card at rest. */
  --surface-control: var(--p-ink-550);
  --surface-overlay: var(--p-ink-550);

  /* THE WATERMARK'S GROUND, SIGNED. A monogram behind copy was drawn in
     --surface-fill: one step DOWN in dark (a debossed letter) and one step UP
     in light (an embossed one, at 1.03:1 — invisible). The same element read
     two different ways per theme because "one step below its card" never said
     below in WHICH direction. This always steps toward the text, so the mark
     is a shadow of the same depth in both. */
  --mark-ground: color-mix(in srgb, var(--surface-fill-strong) 88%, var(--text));

  /* One focus ring, declared once. Five selectors had rewritten the same
     outline by hand, none of them with an offset — and a 3px ring at offset 0
     on an 8px corner reads as the border this system forbids. */
  --focus-ring: 1px solid var(--text-muted);
  --focus-offset: 4px;
  /* The same ring over a picture (DS-11): the media frame's controlling ring
     sits on the matte, ink in both themes, where the soft ink of light is
     under 3:1. It takes the media's own paper instead. */
  --focus-ring-media: 1px solid var(--media-paper-dim);


  /* ── LAYERS ────────────────────────────────────────────────────────────
     Seven rungs, and every one of them is a number this app already shipped
     as a literal in some component. Naming them is what makes "which is on
     top" answerable without grepping: a new overlay picks a rung rather than
     inventing 9998. The website carries the same idea as --z-* in tokens.css
     with its own numbers, and the two never meet in one document object. */
  --z-plane:   40;    /* panels, and the invisible scrim that closes a popover */
  --z-rail:    45;    /* the rail floats over the panels it sits beside       */
  --z-menu:    50;    /* menus, popovers, the voice view                      */
  --z-tour:    60;    /* the tour's scrim, its lit element, its bubble (60-62)*/
  --z-dialog:  80;    /* a modal and its scrim (80-81)                        */
  --z-loader: 100;    /* covers the panels, never the rail: you can always leave */
  --z-offline: 9999;  /* the one thing that outranks everything, including a
                         dialog: a machine with no connection cannot sign in,
                         and the overlay that says so must not sit under the
                         form it is explaining */

}

/* ══════════════════════════════════════════════════════════════════════════
   TIER 2 — SEMANTIC · LIGHT
   Written twice: once for the explicit toggle, once for the system
   preference. CSS has no mixin and this file refuses to earn a build step for
   one duplication. scripts/gate.mjs compares the two blocks BY VALUE and fails
   if they ever drift — that guard is the only reason the duplication is
   allowed to exist.
   ══════════════════════════════════════════════════════════════════════════ */
:root[data-theme="light"] {
  color-scheme: light;

  --surface-canvas: var(--p-day-mid);
  --surface-ground-top: var(--p-day-top);
  --surface-ground-low: var(--p-day-low);
  --surface-page:   var(--p-snow-30);
  --surface-panel:  var(--p-snow-0);
  --surface-fill:        var(--p-snow-90);
  --surface-fill-strong: var(--p-day-track);
  --surface-skeleton:    var(--p-snow-100);

  --text:       var(--p-day-ink);
  --text-muted: var(--p-day-muted);
  --text-faint: var(--p-day-faint);

  --state-hover: rgba(21, 21, 21, 0.05);
  --state-press: rgba(0, 0, 0, 0.12);
  --hairline:        rgba(21, 21, 21, 0.06);
  --hairline-strong: rgba(21, 21, 21, 0.18);
  --surface-frost:   var(--p-day-frost);
  --surface-segment: var(--p-day-segment);

  /* --accent / --accent-solid / --accent-contrast are deliberately absent:
     they measured IDENTICAL in both themes, so overriding them here would only
     invite the two copies to drift. A light block lists what actually changes. */
  --accent-text: var(--p-pink-750);   /* the measured pink is unreadable on paper */
  --accent-soft: rgba(236, 99, 171, 0.14);

  --accent-2:      var(--p-violet-500);
  --accent-2-tint: var(--p-violet-tint-light);
  --accent-2-soft: var(--p-violet-soft-light);
  --accent-2-text: var(--p-violet-text-light);
  --accent-2-ring:      var(--p-violet-500);   /* 4.61:1 on panel, 4.23 on canvas */
  --accent-2-selection: rgba(85, 104, 234, 0.20);

  --sand:      var(--p-sand-650);   /* the measured sand fails 3:1 on paper */
  --sand-text: var(--p-sand-750);   /* -650 is icon-corrected (3:1) and fails
                                       1.4.3 as text — see the palette note */
  --sand-tint: var(--p-sand-tint-light);

  --surface-tooltip: var(--p-muted-on-light);   /* measured — image copy 13 */
  --text-on-tooltip: var(--p-snow-0);

  --danger:      var(--p-red-600);
  --danger-text: var(--p-red-750);
  --danger-soft: rgba(222, 51, 42, 0.12);
  --status-live: var(--p-green-650);   /* AA: #34c759 is 1.68:1 on the last stop */
  --kind-skill: var(--p-kind-skill-day);  /* AA: the day rungs (DS-13) */
  --kind-fact:  var(--p-kind-fact-day);
  --kind-event: var(--p-kind-event-day);

  /* DERIVED, not measured. Scanning all three light captures for an ink-filled
     button returns nothing: the reference's light CTAs are pale pills
     (#f3f3f3, image copy 13 "Explore collections"). Mirroring the dark
     inversion onto paper is our extrapolation, and it is marked as such. */
  --action-bg:       var(--p-ink-890);      /* derived */
  --action-bg-hover: var(--p-ink-800);      /* derived */
  --action-text:     var(--p-snow-50);      /* derived */

  /* On paper the shadow does the whole job — and here the reference agrees
     with us loudly. MEASURED (image copy 13, x=180, under the Explore
     tooltip): a smooth ramp #d4d4d4 -> #f8f8f8 across ~16 CSS px. That is a
     real, strong, visible shadow: ~38 levels under the page at contact.
     Light's shadow needs no amplification; ours lands in the same register.
     The edge light retires to a no-op, because a white highlight on a white
     panel is a lie. */
  --shadow-raised:
    0 1px 2px -1px rgba(16, 16, 16, 0.08),
    0 4px 10px -3px rgba(16, 16, 16, 0.08);
  --shadow-overlay:
    0 2px 6px -2px rgba(16, 16, 16, 0.10),
    0 10px 24px -6px rgba(16, 16, 16, 0.12),
    0 28px 56px -14px rgba(16, 16, 16, 0.10);
  --shadow-modal:
    0 4px 10px -4px rgba(16, 16, 16, 0.12),
    0 20px 44px -10px rgba(16, 16, 16, 0.16),
    0 56px 112px -28px rgba(16, 16, 16, 0.14);

  --edge-light: inset 0 0 0 0 rgba(0, 0, 0, 0);

  --scrim: rgba(16, 16, 16, 0.32);
  --surface-control: var(--p-snow-0);
  --surface-overlay: var(--p-snow-0);
  --mark-ground: color-mix(in srgb, var(--surface-fill-strong) 88%, var(--text));
}

@media (prefers-color-scheme: light) {
  :root:not([data-theme="dark"]) {
    color-scheme: light;

    --surface-canvas: var(--p-day-mid);
    --surface-ground-top: var(--p-day-top);
    --surface-ground-low: var(--p-day-low);
    --surface-page:   var(--p-snow-30);
    --surface-panel:  var(--p-snow-0);
    --surface-fill:        var(--p-snow-90);
    --surface-fill-strong: var(--p-day-track);
    --surface-skeleton:    var(--p-snow-100);

    --text:       var(--p-day-ink);
    --text-muted: var(--p-day-muted);
    --text-faint: var(--p-day-faint);

    --state-hover: rgba(21, 21, 21, 0.05);
    --state-press: rgba(0, 0, 0, 0.12);
    --hairline:        rgba(21, 21, 21, 0.06);
    --hairline-strong: rgba(21, 21, 21, 0.18);
    --surface-frost:   var(--p-day-frost);
    --surface-segment: var(--p-day-segment);

    --accent-text: var(--p-pink-750);
    --accent-soft: rgba(236, 99, 171, 0.14);

    --accent-2:      var(--p-violet-500);
    --accent-2-tint: var(--p-violet-tint-light);
    --accent-2-soft: var(--p-violet-soft-light);
    --accent-2-text: var(--p-violet-text-light);
    --accent-2-ring:      var(--p-violet-500);
    --accent-2-selection: rgba(85, 104, 234, 0.20);

    --sand:      var(--p-sand-650);
    --sand-text: var(--p-sand-750);
    --sand-tint: var(--p-sand-tint-light);

    --surface-tooltip: var(--p-muted-on-light);
    --text-on-tooltip: var(--p-snow-0);

    --danger:      var(--p-red-600);
    --danger-text: var(--p-red-750);
    --danger-soft: rgba(222, 51, 42, 0.12);
    --status-live: var(--p-green-650);
    --kind-skill: var(--p-kind-skill-day);
    --kind-fact:  var(--p-kind-fact-day);
    --kind-event: var(--p-kind-event-day);

    --action-bg:       var(--p-ink-890);    /* derived — see the toggle block */
    --action-bg-hover: var(--p-ink-800);    /* derived */
    --action-text:     var(--p-snow-50);    /* derived */

    --shadow-raised:
      0 1px 2px -1px rgba(16, 16, 16, 0.08),
      0 4px 10px -3px rgba(16, 16, 16, 0.08);
    --shadow-overlay:
      0 2px 6px -2px rgba(16, 16, 16, 0.10),
      0 10px 24px -6px rgba(16, 16, 16, 0.12),
      0 28px 56px -14px rgba(16, 16, 16, 0.10);
    --shadow-modal:
      0 4px 10px -4px rgba(16, 16, 16, 0.12),
      0 20px 44px -10px rgba(16, 16, 16, 0.16),
      0 56px 112px -28px rgba(16, 16, 16, 0.14);

    --edge-light: inset 0 0 0 0 rgba(0, 0, 0, 0);

    --scrim: rgba(16, 16, 16, 0.32);
    --surface-control: var(--p-snow-0);
  --surface-overlay: var(--p-snow-0);
    --mark-ground: color-mix(in srgb, var(--surface-fill-strong) 88%, var(--text));
  }
}

/* ══════════════════════════════════════════════════════════════════════════
   TIER 2 — SEMANTIC · SCOPED (any element with data-theme)
   The blocks above theme the PAGE: they sit on :root, so a data-theme on any
   other element did nothing, and a "dark stage" in a light page rendered
   light. These two re-theme the SUBTREE of any element that carries
   data-theme, whatever the page is: the light one repeats the light block's
   values, the dark one restates every one of those keys at its :root (dark)
   value.

   Both also restate the :root tokens that are not themed but READ a themed
   one through var() (--accent: var(--text), --focus-ring, --term-bg…). A
   custom property computes where it is declared, so without them a subtree
   would inherit the page's already-resolved --accent. `color` resets the
   text the subtree inherits from the page's body.

   They start with :root, so every custom property still sits on :root, and
   `:root [data-theme]` never matches <html> itself: a page that themes only
   its root sees no change. scripts/gate.mjs holds both blocks to the light
   block and to :root BY VALUE, like law 3 holds the two light blocks.
   ══════════════════════════════════════════════════════════════════════════ */
:root [data-theme="light"] {
  color-scheme: light;
  color: var(--text);

  --surface-canvas: var(--p-day-mid);
  --surface-ground-top: var(--p-day-top);
  --surface-ground-low: var(--p-day-low);
  --surface-page: var(--p-snow-30);
  --surface-panel: var(--p-snow-0);
  --surface-fill: var(--p-snow-90);
  --surface-fill-strong: var(--p-day-track);
  --surface-skeleton: var(--p-snow-100);
  --text: var(--p-day-ink);
  --text-muted: var(--p-day-muted);
  --text-faint: var(--p-day-faint);
  --state-hover: rgba(21, 21, 21, 0.05);
  --state-press: rgba(0, 0, 0, 0.12);
  --hairline: rgba(21, 21, 21, 0.06);
  --hairline-strong: rgba(21, 21, 21, 0.18);
  --surface-frost: var(--p-day-frost);
  --surface-segment: var(--p-day-segment);
  --accent-text: var(--p-pink-750);
  --accent-soft: rgba(236, 99, 171, 0.14);
  --accent-2: var(--p-violet-500);
  --accent-2-tint: var(--p-violet-tint-light);
  --accent-2-soft: var(--p-violet-soft-light);
  --accent-2-text: var(--p-violet-text-light);
  --accent-2-ring: var(--p-violet-500);
  --accent-2-selection: rgba(85, 104, 234, 0.20);
  --sand: var(--p-sand-650);
  --sand-text: var(--p-sand-750);
  --sand-tint: var(--p-sand-tint-light);
  --surface-tooltip: var(--p-muted-on-light);
  --text-on-tooltip: var(--p-snow-0);
  --danger: var(--p-red-600);
  --danger-text: var(--p-red-750);
  --danger-soft: rgba(222, 51, 42, 0.12);
  --status-live: var(--p-green-650);
  --kind-skill: var(--p-kind-skill-day);
  --kind-fact: var(--p-kind-fact-day);
  --kind-event: var(--p-kind-event-day);
  --graph-node: var(--text);
  --graph-link: var(--text);
  --graph-hub: var(--text);
  --graph-glow: var(--text);
  --action-bg: var(--p-ink-890);
  --action-bg-hover: var(--p-ink-800);
  --action-text: var(--p-snow-50);
  --shadow-raised:
    0 1px 2px -1px rgba(16, 16, 16, 0.08),
    0 4px 10px -3px rgba(16, 16, 16, 0.08);
  --shadow-overlay:
    0 2px 6px -2px rgba(16, 16, 16, 0.10),
    0 10px 24px -6px rgba(16, 16, 16, 0.12),
    0 28px 56px -14px rgba(16, 16, 16, 0.10);
  --shadow-modal:
    0 4px 10px -4px rgba(16, 16, 16, 0.12),
    0 20px 44px -10px rgba(16, 16, 16, 0.16),
    0 56px 112px -28px rgba(16, 16, 16, 0.14);
  --edge-light: inset 0 0 0 0 rgba(0, 0, 0, 0);
  --scrim: rgba(16, 16, 16, 0.32);
  --surface-control: var(--p-snow-0);
  --surface-overlay: var(--p-snow-0);
  --mark-ground: color-mix(in srgb, var(--surface-fill-strong) 88%, var(--text));
  --term-bg: var(--surface-panel);
  --accent: var(--text);
  --accent-solid: var(--text);
  --accent-solid-hover: var(--text-muted);
  --accent-contrast: var(--surface-canvas);
  --focus-ring: 1px solid var(--text-muted);
}

:root [data-theme="dark"] {
  color-scheme: dark;
  color: var(--text);

  --surface-canvas: var(--p-night-mid);
  --surface-ground-top: var(--p-night-top);
  --surface-ground-low: var(--p-night-low);
  --surface-page: var(--p-ink-900);
  --surface-panel: var(--p-ink-880);
  --surface-fill: var(--p-ink-800);
  --surface-fill-strong: var(--p-night-track);
  --surface-skeleton: var(--p-ink-600);
  --text: var(--p-night-ink);
  --text-muted: var(--p-night-muted);
  --text-faint: var(--p-night-faint);
  --state-hover: rgba(236, 236, 234, 0.07);
  --state-press: rgba(255, 255, 255, 0.12);
  --hairline: rgba(236, 236, 234, 0.05);
  --hairline-strong: rgba(236, 236, 234, 0.2);
  --surface-frost: var(--p-night-frost);
  --surface-segment: var(--p-night-segment);
  --accent-text: var(--text);
  --accent-soft: rgba(236, 99, 171, 0.16);
  --accent-2: var(--p-violet-400);
  --accent-2-tint: var(--p-violet-tint-dark);
  --accent-2-soft: var(--p-violet-soft-dark);
  --accent-2-text: var(--p-violet-text-dark);
  --accent-2-ring: var(--p-violet-400);
  --accent-2-selection: rgba(117, 138, 240, 0.22);
  --sand: var(--p-sand-400);
  --sand-text: var(--p-sand-400);
  --sand-tint: var(--p-sand-tint-dark);
  --surface-tooltip: var(--p-snow-0);
  --text-on-tooltip: var(--p-ink-950);
  --danger: var(--p-red-500);
  --danger-text: var(--p-red-400);
  --danger-soft: rgba(255, 69, 58, 0.16);
  --status-live: var(--p-green-500);
  --kind-skill: var(--p-kind-skill-night);
  --kind-fact: var(--p-kind-fact-night);
  --kind-event: var(--p-kind-event-night);
  --graph-node: var(--text);
  --graph-link: var(--text);
  --graph-hub: var(--text);
  --graph-glow: var(--text);
  --action-bg: var(--p-snow-50);
  --action-bg-hover: var(--p-snow-100);
  --action-text: var(--p-ink-890);
  --shadow-raised:
    0 1px 2px -1px rgba(0, 0, 0, 0.5),
    0 4px 10px -3px rgba(0, 0, 0, 0.45);
  --shadow-overlay:
    0 2px 6px -2px rgba(0, 0, 0, 0.55),
    0 10px 24px -6px rgba(0, 0, 0, 0.5),
    0 28px 56px -14px rgba(0, 0, 0, 0.45);
  --shadow-modal:
    0 4px 10px -4px rgba(0, 0, 0, 0.6),
    0 20px 44px -10px rgba(0, 0, 0, 0.55),
    0 56px 112px -28px rgba(0, 0, 0, 0.5);
  --edge-light: inset 0 1px 0 rgba(255, 255, 255, 0.06);
  --scrim: rgba(0, 0, 0, 0.6);
  --surface-control: var(--p-ink-550);
  --surface-overlay: var(--p-ink-550);
  --mark-ground: color-mix(in srgb, var(--surface-fill-strong) 88%, var(--text));
  --term-bg: var(--surface-panel);
  --accent: var(--text);
  --accent-solid: var(--text);
  --accent-solid-hover: var(--text-muted);
  --accent-contrast: var(--surface-canvas);
  --focus-ring: 1px solid var(--text-muted);
}

/* ══════════════════════════════════════════════════════════════════════════
   BASE
   ══════════════════════════════════════════════════════════════════════════ */
* { box-sizing: border-box; }

/* `hidden` is a UA rule (`display: none`) with almost no specificity, so ANY
   class that sets display beats it — `.thing { display: flex }` silently makes
   `thing.hidden = true` do nothing, and the element just stays on screen. The
   attribute is the platform's way to say "not here"; the system makes it win. */
[hidden] { display: none !important; }

/* Visible to assistive tech, invisible on screen. `display: none` removes text
   from the a11y tree entirely — a stepper whose labels are display:none
   announces three empty list items and advertises progress it does not carry. */
.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

html, body { height: 100%; margin: 0; }

body {
  background: var(--surface-canvas);
  color: var(--text);
  font-family: var(--font);
  font-size: var(--text-base);
  font-weight: var(--weight-body);
  line-height: var(--leading-base);
  -webkit-font-smoothing: antialiased;
  transition: background-color var(--dur-slow) var(--ease-in-out),
              color var(--dur-slow) var(--ease-in-out);
}

button {
  font-family: inherit;
  font-size: inherit;
  font-weight: inherit;   /* the base weight, not the browser's 400 (DS-13) */
  cursor: pointer;
  border: 0;              /* law 1 — reset only; never re-added with a colour */
  padding: 0;             /* the UA's 1px 6px silently offsets any centring:
                             place-items centres inside the CONTENT box, so an
                             icon button ends up 2px right of its own middle */
  background: none;
  color: inherit;
}
input, select, textarea {
  font-family: inherit;
  /* The UA sizes form controls at its own 13px regardless of the page, so a
     field rendered visibly smaller than the sentence beside it — the single
     loudest "this is a web page" tell in the whole app. */
  font-size: inherit;
  font-weight: inherit;   /* the base weight, not the browser's 400 (DS-13) */
  border: 0;              /* law 1 */
  outline: none;
}

/* The native <select> paints its own stepper: a control we did not draw, in a
   metal this system does not use, at a size it does not set. Law 1's reasoning
   is about a coloured box we did not ask for, and the UA's chrome is exactly
   that — so it is reset here rather than fought with per-selector.
   :not(.pane-select) because that one already resets appearance and carries its
   own .pane-chev; without the exclusion it would wear two chevrons. */
select:not(.pane-select) {
  appearance: none;
  -webkit-appearance: none;
  cursor: pointer;
  /* Room for the indicator drawn below, so a long option never runs under it. */
  padding-right: calc(var(--space-3) + 16px);
  /* Two gradients rather than an image, for the same reason .pane-chev is a
     mask and not an <img>: chevron.svg is a BLACK stroke meant to be masked and
     tinted, so using it as a background-image would paint black on a dark
     surface. Gradients take a token, so this follows the light and dark
     palettes the way the rest of the system does.
     Each layer is a BAND, not a half — `transparent 40% / colour 40% / colour
     62% / transparent 62%` draws a stroke rather than filling a triangle. The
     first attempt used 50% halves and produced two solid wedges meeting at a
     gap, which is the ◤◥ this replaces. Rendered and compared before choosing:
     a thin ∨ matches chevron.svg's own shape, a solid ▼ does not. */
  background-image:
    linear-gradient(45deg, transparent 40%, var(--text-muted) 40%, var(--text-muted) 62%, transparent 62%),
    linear-gradient(135deg, transparent 40%, var(--text-muted) 40%, var(--text-muted) 62%, transparent 62%);
  background-position: right 14px center, right 8px center;
  background-size: 6px 6px;
  background-repeat: no-repeat;
  /* --text-muted, not currentColor: the chevron is an affordance and the value
     is information, so they are not the same weight — and currentColor would
     tie them together, since one `color` cannot say two things. */
  color: var(--text);
}
select:disabled { color: var(--text-faint); cursor: default; }
/* Law 1 has to hold against the USER AGENT too, not just against our CSS: a
   src-less <img> paints broken-image hairlines, which is a coloured border by
   any honest reading. */
img { border: 0; }

h1, h2, h3 {
  font-weight: var(--weight-normal);   /* 400 ceiling — no synthetic bold */
  letter-spacing: var(--tracking-tight);
  line-height: var(--leading-tight);
  margin: 0;
}

::selection { background: var(--accent-2-selection); }

/* Keyboard focus.
   An `outline` — not a box-shadow. This ring is the ONE sanctioned coloured
   outline in the system, and scripts/gate.mjs allows exactly this token and no
   other. A box-shadow ring would REPLACE the box-shadow of any elevated
   surface it landed on, flattening the edge light and the entire three-layer
   shadow the moment a menu took focus. An outline composes, and follows
   border-radius in every current browser. */
:focus-visible {
  /* THROUGH THE TOKENS, so the ring a control gets is the ring the system
     documents. This rule is what MOST controls receive; ten selectors had
     opted into `var(--focus-ring)` at `var(--focus-offset)` while everything
     else took the literal here at offset 0 — one ring described two ways, and
     the difference visible on every 8px corner. */
  outline: var(--focus-ring);
  outline-offset: var(--focus-offset);
}

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

/* ══════════════════════════════════════════════════════════════════════════
   PRIMITIVES
   ══════════════════════════════════════════════════════════════════════════ */

/* ── Surfaces ─────────────────────────────────────────────────────────────
   A panel is a colour and a radius. That is the entire definition. It does not
   cast and does not outline — it is told apart from its neighbours by the
   canvas showing through the gutter between them. */
.ds-canvas { background: linear-gradient(180deg, var(--surface-ground-top) 0%, var(--surface-canvas) 55%, var(--surface-ground-low) 100%); }
.ds-page   { background: var(--surface-page); }
/* THE LADDER, and until now the two rungs that name it were the same rung.
   .ds-panel and .ds-card shipped the same fill AND the same radius — one box
   drawn twice, which is the defect this system's second law exists to
   prevent, sitting in the system's own primitives while every page that used
   --radius-xl on a panel had the ladder right. A card sits INSIDE a panel: one
   surface step in, one radius step down. */
.ds-panel  { background: var(--surface-panel); border-radius: var(--radius-xl); }
.ds-card   { background: var(--surface-fill);  border-radius: var(--radius-lg); }

/* The gutter IS the separator (measured 8px). */
.ds-frame {
  display: flex;
  gap: var(--gutter);
  padding: var(--gutter);
  background: var(--surface-canvas);
}
/* DS-3: the app frame — a nav column at --sidebar-width beside the main
   panel, the gutter still the only separator. Compact (--bp-compact; a media
   query cannot read var(), so the number is written here) it becomes a
   column with the FIRST child, the nav, at the bottom — column-reverse, so no
   rule reaches into the children — and pads for the safe areas. The consumer
   gives main `flex: 1`. Visual order then differs from DOM and focus order:
   nav is first to Tab, last to the eye. */
.ds-frame--sidebar {
  display: grid;
  grid-template-columns: var(--sidebar-width) minmax(0, 1fr);
  height: 100%;
}
@media (max-width: 640px) {
  .ds-frame--sidebar {
    display: flex;
    flex-direction: column-reverse;
    padding:
      calc(var(--gutter) + env(safe-area-inset-top))
      calc(var(--space-4) + env(safe-area-inset-right))
      calc(var(--space-5) + env(safe-area-inset-bottom))
      calc(var(--space-4) + env(safe-area-inset-left));
  }
}

/* ── Elevation — only for surfaces that leave the layout plane ──────────── */
.ds-elev-raised  { box-shadow: var(--edge-light), var(--shadow-raised); }
.ds-elev-overlay { box-shadow: var(--edge-light), var(--shadow-overlay); }
.ds-elev-modal   { box-shadow: var(--edge-light), var(--shadow-modal); }

/* ── Edge fade ────────────────────────────────────────────────────────────
   The reference does NOT fade with opacity — it BLURS. Measured on the tool
   palette (image copy 11), the same columns across two tiles:

     unfaded ('Sound Effects', y=1440):  25 25 39 39 39 39 39 ...
     faded   ('Voiceover',    y=1360):  28 29 32 33 34 35 36 37 38 39 40 41 ...

   A 1px step becomes a 14px ramp; peak |dI/dx| falls 133 -> 24. An opacity mask
   SCALES an edge, it cannot SOFTEN one — only a blur turns a step into a ramp.
   The faded tile's own hard edges are gone, which opacity cannot do at any
   alpha.

   So there are two primitives, and they are not interchangeable:

     .ds-fade-blur   what the reference does. A masked backdrop-filter: the
                     radius is constant and its opacity ramps, where the
                     reference's radius grows with distance. An approximation,
                     and named as one — a true progressive blur needs a stack
                     of layers at increasing radii, which a single class cannot
                     build.
     .ds-fade-y      an opacity mask. Not what the reference does. It earns its
                     place where content must actually GO, not merely soften.

   The mask belongs to the container, so it tracks scroll for free.

   It masks everything the element PAINTS — its own background included. So
   this goes on a TRANSPARENT scroller, never on the filled box itself: put the
   fill and the radius on a wrapper and the mask on the scroller inside it.
   Applied to a filled box, it eats the fill at the edges and takes the corner
   radius with it. (The chat log is transparent, which is why the intended use
   is the safe one — but the primitive doesn't know that, so this note does.) */
.ds-fade-y {
  -webkit-mask-image: var(--fade-mask-y);
  mask-image: var(--fade-mask-y);
}
.ds-fade-bottom {
  -webkit-mask-image: var(--fade-mask-bottom);
  mask-image: var(--fade-mask-bottom);
}

/* The reference's treatment. The veil is a pseudo-element, so unlike
   .ds-fade-* it does NOT eat the container's fill.

   ⚠ It goes on a NON-SCROLLING wrapper AROUND the scroller — never on the
   scroller itself. An absolutely-positioned child of a scroll container is laid
   out against the padding box but PAINTS in the scrolled layer, so `bottom: 0`
   tracks the content instead of the edge: scroll down and the veil strands
   mid-list, blurring the middle while the real bottom edge stays sharp.

   Its sibling .ds-fade-* carries the same discipline for a different reason.
   Both rules reduce to: the thing that scrolls is not the thing that decorates. */
.ds-fade-blur { position: relative; }
.ds-fade-blur::after {
  content: "";
  position: absolute;
  inset: auto 0 0 0;
  height: var(--fade-size);
  pointer-events: none;
  -webkit-backdrop-filter: blur(var(--fade-blur));
  backdrop-filter: blur(var(--fade-blur));
  -webkit-mask-image: linear-gradient(to top, #000 0%, transparent 100%);
  mask-image: linear-gradient(to top, #000 0%, transparent 100%);
}

/* ── Interactive — the hover half of "colours, containers and on hover" ─── */
.ds-interactive {
  background: transparent;
  border-radius: var(--radius-sm);
  color: var(--text-muted);
  transition: background-color var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out);
}
.ds-interactive:hover { background: var(--state-hover); color: var(--text); }
.ds-interactive:active { background: var(--state-press); color: var(--text); }
.ds-interactive[aria-selected="true"],
.ds-interactive.is-selected { background: var(--surface-fill-strong); color: var(--text); }
/* DS-1: the page a nav tab or row points at is selected too. Its own rule, so
   the selector list above stays exactly as it shipped. */
.ds-interactive[aria-current="page"],
.ds-interactive[aria-current="true"] { background: var(--surface-fill-strong); color: var(--text); }
/* DS-1: a list row (subject, memory, artifact) — the chip radius, and words
   that rest at full ink instead of muted. */
.ds-interactive--row { border-radius: var(--radius-chip); color: var(--text); }

/* ── Buttons ──────────────────────────────────────────────────────────────
   Primary is inverted (paper on ink / ink on paper) exactly as the reference
   does it. Every hover is an explicit surface: `filter: brightness()` is
   mathematically dead at the ends of the ramp — 1.08x on #f5f5f5 moves two
   levels, and on #ffffff it moves nothing at all. */
.ds-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-4);
  border-radius: var(--radius-sm);
  font-size: var(--text-sm);
  color: var(--text);
  transition: background-color var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out);
}
.ds-btn--primary { background: var(--action-bg); color: var(--action-text); }
.ds-btn--primary:hover { background: var(--action-bg-hover); }

.ds-btn--accent { background: var(--accent-solid); color: var(--accent-contrast); }
.ds-btn--accent:hover { background: var(--accent-solid-hover); }

.ds-btn--fill { background: var(--surface-fill); color: var(--text); }
.ds-btn--fill:hover { background: var(--surface-fill-strong); }

/* DISABLED IS ONE VALUE, and it lives here rather than in nine component
   rules that had drifted to two (0.5 and 0.6 shipped side by side). Opacity
   and not a colour: a disabled control that changes hue is a second meaning
   for the same grey, and the person cannot tell it from a state. */
.ds-btn:disabled,
.ds-btn[aria-disabled="true"] { opacity: 0.6; cursor: default; }

.ds-btn--ghost { background: transparent; color: var(--text-muted); }
.ds-btn--ghost:hover { background: var(--state-hover); color: var(--text); }
/* DS-8 (1.9.0): the danger hover's words are --text, not --danger-text: on
   the tint over a card's fill --danger-text reads 3.70:1 in dark and 4.25:1
   in light. The tint marks the button; the words stay readable. */
.ds-btn--ghost.is-danger:hover { background: var(--danger-soft); color: var(--text); }

.ds-btn--icon  { padding: 0; width: 34px; height: 34px; }
.ds-btn--round { padding: 0; width: 38px; height: 38px; border-radius: var(--radius-pill); }

/* DS-1 sizes. `--sm` is the text-link button (worn with `--ghost`); `--back`
   is the ellipsed "← Parent" link that sits flush with the column's edge;
   `--round-lg` is the 44px touch circle (create, filters). */
.ds-btn--sm { padding: var(--space-1) var(--space-2); font-size: var(--text-xs); }
.ds-btn--back {
  display: inline-block;
  max-width: 100%;
  padding: var(--space-1) var(--space-2);
  margin-inline-start: calc(-1 * var(--space-2));
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ds-btn--round-lg { padding: 0; width: var(--control-lg); height: var(--control-lg); border-radius: var(--radius-pill); }
/* A toggle that is on inverts like primary. After every variant, so a pressed
   fill button beats `.ds-btn--fill:hover`. */
.ds-btn--round-lg[aria-pressed="true"] { background: var(--action-bg); color: var(--action-text); }
.ds-btn--round-lg[aria-pressed="true"]:hover { background: var(--action-bg-hover); }

/* DS-2: the Connect picker. `--glyph` is the icon inside a button's label, an
   svg drawn in the label's colour (stroke="currentColor"). `--answer` is a
   FILLED button that can answer "done": `.is-done` lays the button's own
   ground over its label, then wipes a tick in, in the label's colour — so the
   tick is the label's pairing on the label's ground, nothing new to contrast.
   On a ghost button `inherit` is transparent and the label would show
   through: wear it with --fill, --primary or --accent. Reduced motion is the
   global rule's. */
.ds-btn--glyph { width: var(--icon-sm); height: var(--icon-sm); flex: none; }
.ds-btn--answer { position: relative; }
.ds-btn--answer::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: inherit;
  opacity: 0;
  transition: opacity var(--dur-base) var(--ease-out);
}
.ds-btn--answer.is-done::before { opacity: 1; }
.ds-btn--answer::after {
  content: "";
  position: absolute;
  inset: 0;
  margin: auto;
  width: var(--icon-md);
  height: var(--icon-md);
  background: currentColor;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12.5l4.5 4.5L19 7'/%3E%3C/svg%3E") center / contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12.5l4.5 4.5L19 7'/%3E%3C/svg%3E") center / contain no-repeat;
  clip-path: inset(0 100% 0 0);
  transition: clip-path var(--dur-slow) var(--ease-out) var(--dur-fast);
}
.ds-btn--answer.is-done::after { clip-path: inset(0); }

/* ── Fields — a fill, never a hairline.
      The reference hairlines its inputs in BOTH themes, including the dark one
      whose step (#2b2b2b on #1a1a1a, 17 levels) is exactly ours. So "a border
      appears where the surface step fails" is NOT the reason — and this comment
      used to say it was, after the header had already retracted it. The reason
      is the brief. What our 18-level light fill buys is the right to obey the
      brief without the field vanishing.
      No hover fill: shifting it to fill-strong only dragged the placeholder
      under 4.5:1, and focus already says everything a hover would. */
.ds-field {
  width: 100%;
  padding: var(--space-3);
  border-radius: var(--radius-sm);
  background: var(--surface-fill);
  color: var(--text);
}
.ds-field::placeholder { color: var(--text-faint); }

/* DS-1: the search pill. A <label class="ds-field ds-field--search"> holds the
   icon and a bare <input class="ds-field--bare">; the pill owns the fill and
   the focus ring, the input owns nothing. The placeholder is muted, not faint:
   faint on fill-strong is 3.55:1 in dark. */
.ds-field--search {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  height: var(--control-lg);
  padding: 0 var(--space-3) 0 var(--space-4);
  border-radius: var(--radius-pill);
  background: var(--surface-fill-strong);
  color: var(--text);
  font-size: var(--text-sm);
}
.ds-field--search:focus-within { outline: var(--focus-ring); outline-offset: var(--focus-offset); }
.ds-field--bare {
  flex: 1;
  min-width: 0;
  padding: 0;
  background: transparent;
  color: inherit;
}
.ds-field--bare:focus-visible { outline: none; }
.ds-field--bare::placeholder { color: var(--text-muted); }

.ds-pill {
  border-radius: var(--radius-pill);
  padding: var(--space-2) var(--space-4);
}

/* ── Tiles — the tinted tool squares (violet = AI, sand = 3D). ──────────── */
.ds-tile {
  display: grid;
  place-items: center;
  width: var(--tile-size);
  height: var(--tile-size);
  border-radius: var(--radius-sm);
  transition: background-color var(--dur-fast) var(--ease-out);
}
.ds-tile--ai   { background: var(--accent-2-tint); color: var(--accent-2); }
.ds-tile--3d   { background: var(--sand-tint);     color: var(--sand); }
.ds-tile--calm { background: var(--surface-fill);  color: var(--text-muted); }

/* ── Menus / overlays — the only things allowed to cast. ────────────────── */
/* Priced in dark, where the reference's overlay is #191919 on a #1a1a1a panel:
   one level down, with a #303030 hairline carrying the edge. Law 1 denies us
   that hairline, so we step UP to the fill — with no line available the surface
   has to carry the edge, and up is the only direction with room.

   Light gets the same treatment for a different reason, and it is worth being
   explicit rather than letting one measurement stand in for two: on paper a
   menu already reads through its shadow (measured 38 levels at contact), so the
   #ededed fill is not load-bearing there — it is consistency with dark, chosen
   over a white-on-white menu that would depend on shadow alone. */
.ds-menu {
  background: var(--surface-fill);
  border-radius: var(--radius-lg);
  padding: var(--space-2);
}
/* DS-1: a menu row, worn with `.ds-interactive`. The consumer sizes its icon
   with var(--icon-sm): a descendant rule is not a shape the manifest reads. */
.ds-menu--item {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
  padding: var(--space-2);
  font-size: var(--text-sm);
  text-align: start;
}

/* A tooltip leaves the plane, so law 3 gives it depth. Measured: the
   reference's tooltip is #2b2b2b in dark (image copy 3), which is exactly
   --surface-tooltip.

   It is `position: fixed` and driven by rail.js, NOT a ::after on the trigger.
   That is not a preference — an absolutely-positioned tooltip is clipped by any
   scrolling ancestor, and `overflow-x: visible` cannot save it: CSS computes
   BOTH axes to `auto` the moment one of them is. The rail's agent list scrolls,
   so a CSS-only tooltip there is invisible by construction. Ask the viewport
   instead of the parent. */
.ds-tooltip {
  position: fixed;
  z-index: 60;
  white-space: nowrap;
  pointer-events: none;
  opacity: 0;
  background: var(--surface-tooltip);
  color: var(--text-on-tooltip);
  border-radius: var(--radius-sm);
  padding: var(--space-1) var(--space-2);
  font-size: var(--text-xs);
  transition: opacity var(--dur-fast) var(--ease-out);
}
.ds-tooltip.is-open { opacity: 1; }

.ds-scrim {
  position: fixed;
  inset: 0;
  background: var(--scrim);
  backdrop-filter: blur(24px) saturate(180%);
}

/* ── Badges / status — a badge is a tinted container, not an outlined one. ─ */
.ds-badge {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  padding: 3px var(--space-2);
  border-radius: var(--radius-pill);
  background: var(--surface-fill);
  color: var(--text-muted);
  font-size: var(--text-2xs);
}
.ds-badge--accent   { background: var(--accent-soft);   color: var(--accent-text); }
.ds-badge--accent-2 { background: var(--accent-2-soft); color: var(--accent-2-text); }
/* DS-1: the memory-kind labels. `--caps` combines with any tone. */
.ds-badge--caps { text-transform: uppercase; letter-spacing: var(--tracking-wide); }
.ds-badge--strong { background: var(--surface-fill); color: var(--text); }
.ds-badge--accent-2-plain { background: var(--surface-fill); color: var(--accent-2-text); }

.ds-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  flex: none;
  background: var(--text-faint);
}
.ds-dot--running { background: var(--accent-2); box-shadow: 0 0 0 3px var(--accent-2-selection); }
.ds-dot--error   { background: var(--danger); }
/* DS-1: the size pair (the base keeps 7px) and the drawer colours. */
.ds-dot--sm { width: var(--dot-size-sm); height: var(--dot-size-sm); }
.ds-dot--lg { width: var(--dot-size-lg); height: var(--dot-size-lg); }
.ds-dot--accent-2 { background: var(--accent-2); }
.ds-dot--ink { background: var(--text); }
.ds-dot--sand { background: var(--sand); }

/* ── Divider ──────────────────────────────────────────────────────────────
   The one place the system draws a line, and it is worth being straight about
   it: this IS a hairline. It is a box in the flow rather than a border, which
   buys one real thing — it participates in flex/grid gap, so a menu can space
   it like any other child. That is the whole justification. It is not that a
   border "cannot" carry an alpha; it can. */
.ds-divider {
  height: 1px;
  background: var(--state-hover);
  margin: var(--space-1) 0;
}

/* ── Skeleton ─────────────────────────────────────────────────────────────  */
.ds-skeleton {
  background: var(--surface-skeleton);
  border-radius: var(--radius-sm);
}

/* ── Motion primitives ────────────────────────────────────────────────────  */
@keyframes ds-rise { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
@keyframes ds-fade { from { opacity: 0; } to { opacity: 1; } }
@keyframes ds-spin { to { transform: rotate(360deg); } }

.ds-rise { animation: ds-rise var(--dur-slow) var(--ease-out) both; }
.ds-fade { animation: ds-fade var(--dur-fast) var(--ease-out); }

/* ══════════════════════════════════════════════════════════════════════════
   DS-4 — STRUCTURE AND NAVIGATION (1.5.0)
   The panel's frame pieces (S-007 report, DS-4), as components of their own.
   Each rule is one class: the parts are variants (`.ds-nav--tab`), never a
   descendant selector, and the compact layout is the --bp-compact query
   (written as a number: a media query cannot read var()).
   ══════════════════════════════════════════════════════════════════════════ */

/* ── Sidebar / tab bar ────────────────────────────────────────────────────
   The nav column: a panel that holds its tabs and scrolls nothing itself
   (min-height: 0 lets a list inside it scroll). Compact, it is the bottom tab
   bar: a row whose tabs share the width, icon over word. It stops clipping
   there, so a menu can open upward out of it. Wear it inside
   .ds-frame--sidebar, which puts it at the bottom. */
.ds-nav {
  display: flex;
  flex-direction: column;
  gap: var(--space-half);
  min-height: 0;
  overflow: hidden;
  padding: var(--space-4) var(--space-3);
  border-radius: var(--radius-xl);
  background: var(--surface-panel);
}
@media (max-width: 640px) {
  .ds-nav {
    flex-direction: row;
    align-items: center;
    gap: 0;
    padding: var(--space-1);
    border-radius: var(--radius-lg);
    overflow: visible;
    position: relative;
  }
}
/* A tab, worn with .ds-interactive: the ground, radius, resting grey, hover,
   press and the aria-current="true" state all come from there. Its icon is a
   .ds-glyph .ds-glyph--tab in currentColor, so it lights with the tab. */
.ds-nav--tab {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  text-align: start;
  padding: var(--space-2) var(--space-3);
  border: 0;
  font: inherit;
  font-size: var(--text-sm);
  cursor: pointer;
}
@media (max-width: 640px) {
  .ds-nav--tab {
    flex: 1;
    flex-direction: column;
    justify-content: center;
    gap: var(--space-2);
    padding: var(--space-2);
    font-size: var(--text-xs);
    line-height: 1;
    letter-spacing: normal;
  }
}

/* ── Brand lockup ─────────────────────────────────────────────────────────
   The logo and the product's name at the top of the nav, in the eyebrow's
   voice. Gone in the compact bar: a tab bar is for going places. */
.ds-brand {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3) var(--space-5);
  font-size: var(--text-xs);
  color: var(--text-faint);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
}
@media (max-width: 640px) {
  .ds-brand { display: none; }
}

/* ── Glyph ────────────────────────────────────────────────────────────────
   An icon box at --icon-sm that the text beside it cannot squash. An inline
   svg inside it draws in currentColor (tab icons, provider marks);
   `--logo` is the logo as a MASK over a --text fill, so one file follows
   both themes. The mask reads /logo.svg from the site root, like /fonts: a
   consumer serves it there. `--tab` grows to --icon-xl in the compact bar. */
.ds-glyph {
  display: inline-block;
  flex: none;
  width: var(--icon-sm);
  height: var(--icon-sm);
}
.ds-glyph--logo {
  background-color: var(--text);
  -webkit-mask: url("/logo.svg") center / contain no-repeat;
  mask: url("/logo.svg") center / contain no-repeat;
}
@media (max-width: 640px) {
  .ds-glyph--tab { width: var(--icon-xl); height: var(--icon-xl); }
}

/* ── View header ──────────────────────────────────────────────────────────
   A view's title (and its lead) beside the view's actions. `--main` holds the
   words and may shrink; `.ds-actions` keeps its size and sits at the end.
   `--stack` puts the actions under the words in the compact layout. */
.ds-header {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
}
@media (max-width: 640px) {
  .ds-header { gap: var(--space-2); }
}
.ds-header--main { flex: 1; min-width: 0; }
@media (max-width: 640px) {
  .ds-header--stack { flex-direction: column; align-items: stretch; }
}
/* The view's title, at the display weight (Book reads heavy at 26px). The
   base h1-h3 rule gives it the tight tracking and leading. */
.ds-title {
  margin: 0 0 var(--space-1);
  font-size: var(--text-xl);
  font-weight: var(--weight-display);
  overflow-wrap: anywhere;
}
@media (max-width: 640px) {
  .ds-title { font-size: var(--text-lg); }
}
.ds-actions {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex: none;
  margin-inline-start: auto;
}
@media (max-width: 640px) {
  .ds-actions { flex-wrap: wrap; justify-content: flex-end; }
}
/* DS-8 (1.9.0): `--row` is a row of actions under a block's content (a form's
   buttons, a sheet's footer), not beside a title: at the start, a step below
   what it follows, wrapping. The explicit 0 cancels the base's auto margin,
   which pushes the row to the end inside a grid parent. `--center` centres it.
   Both come after the compact rule above, which would otherwise override them. */
.ds-actions--row {
  margin-inline-start: 0;
  margin-top: var(--space-3);
  flex-wrap: wrap;
  justify-content: flex-start;
}
.ds-actions--center { justify-content: center; }

/* ── Lead text ────────────────────────────────────────────────────────────
   The sentence under a view's title, muted and capped at --measure-lead. */
.ds-lead {
  margin: 0 0 var(--space-6);
  max-width: var(--measure-lead);
  font-size: var(--text-sm);
  color: var(--text-muted);
}

/* ── Notice banner ────────────────────────────────────────────────────────
   A strip across the top of a view: its words, then its action. A fill-strong
   ground, not a line (law 1); `--danger` for what is wrong (an expired plan).
   Hide it with [hidden]. */
.ds-banner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  max-width: var(--measure-page);
  margin: 0 auto var(--space-3);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-sm);
  background: var(--surface-fill-strong);
  font-size: var(--text-sm);
  color: var(--text);
}
.ds-banner--danger { background: var(--danger-soft); color: var(--danger-text); }

/* ── Notice box (DS-8, 1.9.0) ─────────────────────────────────────────────
   A block of words inside a view or a card: what cannot happen and why (a
   refusal, a warning before an action). A fill-strong ground, not a line
   (law 1); it breaks a long token (a path, an id) anywhere. `--danger` is the
   danger tint, its words --text: --danger-text on the tint over a card reads
   3.70:1 in dark, so the danger is announced by the words and the tint, not
   by a red hue. */
.ds-notice {
  margin: 0 0 var(--space-3);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-sm);
  background: var(--surface-fill-strong);
  color: var(--text);
  font-size: var(--text-sm);
  text-align: start;
  overflow-wrap: anywhere;
}
.ds-notice--danger { background: var(--danger-soft); color: var(--text); }

/* ── Compact form row (DS-8, 1.9.0) ───────────────────────────────────────
   A label and its controls on one tight row that wraps when it runs out of
   room: a thread's name, a filter bar. `--label` is the muted label that keeps
   its size; `.ds-field--compact` is a field at the small step that takes the
   room left, never narrower than --field-min-sm. */
.ds-form-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
}
.ds-form-row--label { flex: none; color: var(--text-muted); font-size: var(--text-sm); }
.ds-field--compact {
  flex: 1;
  width: auto;
  min-width: var(--field-min-sm);
  padding: var(--space-1) var(--space-2);
  font-size: var(--text-sm);
}

/* ── Field pair and mono field (DS-8, 1.9.0) ──────────────────────────────
   `.ds-form-grid` lays fields side by side, each at least --field-min wide,
   and wraps to one column when two do not fit (a name and its slug).
   `.ds-field--mono` sets a field's value in the mono face (a slug, a link).
   Their builder twins are .sysset-fields--pair and .sysset-input--mono. */
.ds-form-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(var(--field-min), 1fr));
  gap: var(--space-3);
}
.ds-field--mono { font-family: var(--font-mono); }

/* ══════════════════════════════════════════════════════════════════════════
   DS-9 — PANEL COMPONENTS (1.10.0)
   Five jobs the system had only in the builder layer (nix-components.css),
   which a synced page never receives: the bare stage, the inline notice, the
   ruled version row, the plain fact row and the avatar. Each is one class
   and its `--` parts here, the canonical piece; the builder class stays as
   its twin (SPEC, DS-9 Twins), held equal to it. No token is added.
   ══════════════════════════════════════════════════════════════════════════ */

/* ── Stage (DS-9) ─────────────────────────────────────────────────────────
   The bare stage (onboarding, an empty view): no ground, the copy centred in
   the room at the form measure. `--body` holds it, `--title` in the display
   weight, `--summary` a step down in muted. Twin: .store-hero--bare. */
.ds-stage {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  gap: var(--space-3);
  min-height: 100%;
  padding: var(--space-6) var(--space-4);
}
.ds-stage--body {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2);
  width: 100%;
  max-width: var(--measure-form);
  text-align: center;
}
.ds-stage--title {
  margin: 0;
  /* 20ch: a title reads as one or two balanced lines, never a paragraph */
  max-width: 20ch;
  font-size: var(--text-2xl);
  line-height: var(--leading-tight);
  font-weight: var(--weight-display);
  text-wrap: balance;
}
/* 60ch: the summary's reading measure */
.ds-stage--summary { margin: 0; max-width: 60ch; font-size: var(--text-sm); color: var(--text-muted); }

/* ── Inline notice (DS-9) ─────────────────────────────────────────────────
   The line under a control: xs, muted, from the start, and one line's height
   kept when it is empty, so the layout does not jump as a message comes and
   goes. Tones: `--danger`, `--accent-2` (a confirmation: "copied") and
   `--faint`. Twin: .store-status--inline with its tones. */
.ds-status {
  margin: 0;
  min-height: calc(var(--text-xs) * var(--leading-base));
  font-size: var(--text-xs);
  color: var(--text-muted);
  text-align: start;
  overflow-wrap: anywhere;
}
.ds-status--danger { color: var(--danger-text); }
.ds-status--accent-2 { color: var(--accent-2-text); }
.ds-status--faint { color: var(--text-faint); }

/* ── Ruled version (DS-9) ─────────────────────────────────────────────────
   A version of something (a memory) on the page, not on a card: a
   --rule-width hairline above it (a surface, law 1), a faint xs `--head`
   (name, date) that ends in its `--action` (restore), and a `--body` of mono
   text that scrolls at --scroll-max-sm. Twin: .store-version--ruled. */
.ds-version {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  padding: var(--space-3) 0;
}
.ds-version::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  height: var(--rule-width);
  background: var(--state-hover);
  pointer-events: none;
}
.ds-version--head {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  font-size: var(--text-xs);
  color: var(--text-faint);
  overflow-wrap: anywhere;
}
.ds-version--action { margin-inline-start: auto; flex: none; }
.ds-version--body {
  margin: var(--space-1) 0 0;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  max-height: var(--scroll-max-sm);
  overflow: auto;
  color: var(--text-muted);
}

/* ── Plain fact row (DS-9) ────────────────────────────────────────────────
   A row with no box of its own and a trailing action (a similar memory):
   the `--label` is the title, one line that ellipses; the `--note` a small
   faint value beside it. Twin: .store-facts--plain .store-fact--action. */
.ds-fact {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-2) 0;
  font-size: var(--text-sm);
}
.ds-fact--label {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--text);
}
.ds-fact--note { font-size: var(--text-2xs); color: var(--text-faint); text-align: start; overflow-wrap: anywhere; max-width: 40%; }

/* ── Avatar (DS-9) ────────────────────────────────────────────────────────
   A person, not a listing: a circle in the accent with the initial in its
   contrast ink, larger where the pointer is a finger. The initial is the
   element's own text: line-height 1, a glyph centred in its box. Twin:
   .store-mark-avatar with its .store-mark-letter. */
.ds-avatar {
  width: var(--avatar-size);
  aspect-ratio: 1;
  flex: none;
  display: grid;
  place-items: center;
  border-radius: var(--radius-pill);
  background: var(--accent-solid);
  color: var(--accent-contrast);
  font-size: var(--text-2xs);
  line-height: 1;
}
@media (pointer: coarse) {
  .ds-avatar { width: var(--avatar-size-touch); font-size: var(--text-xs); }
}

/* ══════════════════════════════════════════════════════════════════════════
   DS-5 — CONTROLS (1.6.0)
   The panel's controls (S-007 report, DS-5), as components of their own: the
   switch in two sizes, a checkbox, a radio group with descriptions and a
   collapsible section. Native elements keep their semantics: the switch is a
   <button type="button" aria-pressed>, the inputs are the browser's own
   (themed per subtree by color-scheme), the section is <details>/<summary>.
   The keyboard ring is the base :focus-visible rule on all of them.
   ══════════════════════════════════════════════════════════════════════════ */

/* ── Switch ───────────────────────────────────────────────────────────────
   A fill track and an ink knob (DS-11, Blank; it rested muted until 1.11.0);
   pressed, the track takes the action ground and the knob its ink — the same
   pair as the primary button. `--sm` is the one that shares a line with
   text. */
.ds-switch {
  width: var(--switch-width);
  height: var(--switch-height);
  border-radius: var(--radius-pill);
  border: 0;
  background: var(--surface-fill-strong);
  position: relative;
  padding: 0;
  cursor: pointer;
  flex: none;
  transition: background-color var(--dur-base) var(--ease-out);
}
.ds-switch[aria-pressed="true"] { background: var(--action-bg); }
.ds-switch::after {
  content: "";
  position: absolute;
  top: var(--switch-inset);
  left: var(--switch-inset);
  width: var(--switch-knob);
  height: var(--switch-knob);
  border-radius: var(--radius-pill);
  background: var(--text);
  transition: transform var(--dur-base) var(--ease-out), background-color var(--dur-base) var(--ease-out);
}
.ds-switch[aria-pressed="true"]::after {
  transform: translateX(calc(var(--switch-width) - var(--switch-knob) - 2 * var(--switch-inset)));
  background: var(--action-text);
}
.ds-switch--sm { width: var(--switch-width-sm); height: var(--switch-height-sm); }
.ds-switch--sm::after { width: var(--switch-knob-sm); height: var(--switch-knob-sm); }
.ds-switch--sm[aria-pressed="true"]::after {
  transform: translateX(calc(var(--switch-width-sm) - var(--switch-knob-sm) - 2 * var(--switch-inset)));
}
/* DS-11 (1.11.0): a `role="switch"` button says its state with aria-checked
   (Blank's theme and settings switches); it reads the same as aria-pressed. */
.ds-switch[aria-checked="true"] { background: var(--action-bg); }
.ds-switch[aria-checked="true"]::after {
  transform: translateX(calc(var(--switch-width) - var(--switch-knob) - 2 * var(--switch-inset)));
  background: var(--action-text);
}
.ds-switch--sm[aria-checked="true"]::after {
  transform: translateX(calc(var(--switch-width-sm) - var(--switch-knob-sm) - 2 * var(--switch-inset)));
}

/* ── Checkbox ─────────────────────────────────────────────────────────────
   A <label> around a native checkbox: the words beside the box, not above. */
.ds-check {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--text-sm);
}

/* ── Radio group with descriptions ────────────────────────────────────────
   A <fieldset> and its <legend>; each option a <label> around a native radio,
   its name then a faint description. The name stays at the normal weight:
   the panel's bare <b> is a synthetic bold (law 4). */
.ds-choices {
  border: 0;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}
.ds-choices--legend { font-size: var(--text-sm); padding: 0; margin-bottom: var(--space-1); }
.ds-choices--option { display: block; font-size: var(--text-sm); line-height: 1.4; }
.ds-choices--name { color: var(--text); font-weight: var(--weight-normal); }
.ds-choices--desc { color: var(--text-faint); }

/* ── Collapsible section ──────────────────────────────────────────────────
   On a <summary>; the browser keeps its marker and the open/closed state.
   `--body` is what it reveals, set off below the summary (it only renders
   while the <details> is open). */
.ds-disclosure { cursor: pointer; font-size: var(--text-sm); color: var(--text); }
.ds-disclosure--body { margin-top: var(--space-3); }

/* ══════════════════════════════════════════════════════════════════════════
   DS-6 — LISTS AND CONTENT (1.7.0)
   The panel's lists and content (S-007 report, DS-6), as components of their
   own: the log row, the settings list, the code and log blocks, and prose.
   Row dividers are a --rule-width surface in --state-hover on a pseudo-
   element, never a border (law 1). Prose is the one piece that writes
   descendant selectors: a memory's markdown and the packaged release notes
   arrive as bare tags, and only `.ds-prose` and `.ds-prose--notes` style them.
   ══════════════════════════════════════════════════════════════════════════ */

/* ── Log row ──────────────────────────────────────────────────────────────
   A column of rows: the time in muted mono, what happened (a tool, or a
   `--note` in violet), then its detail in muted mono, one line and clipped
   (`--text` for a note's words: the body face, wrapping). `--marked` is the
   row a memory came from. Compact, the detail drops under the time and
   wraps, whole. `--error` colours the row, but every column restates its own
   colour, as in the panel: the class marks the row for a consumer. The time
   is muted, not the panel's faint: faint fails 4.5:1 on the hover over a
   light canvas and on the marked row in dark. */
.ds-log { display: flex; flex-direction: column; }
.ds-log--row {
  display: flex;
  gap: var(--space-3);
  align-items: baseline;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-sm);
  font-size: var(--text-xs);
}
.ds-log--row:hover { background: var(--state-hover); }
.ds-log--error { color: var(--danger-text); }
.ds-log--marked, .ds-log--marked:hover { background: var(--surface-fill-strong); }
.ds-log--time { flex: none; color: var(--text-muted); font-family: var(--font-mono); }
.ds-log--what { flex: none; color: var(--text); font-weight: var(--weight-normal); min-width: var(--field-min-sm); }
.ds-log--note { color: var(--accent-2-text); }
.ds-log--detail {
  flex: 1;
  min-width: 0;
  font-family: var(--font-mono);
  color: var(--text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ds-log--text { font-family: inherit; color: var(--text); white-space: normal; }
@media (max-width: 640px) {
  .ds-log--row { flex-wrap: wrap; gap: var(--space-1) var(--space-3); }
  .ds-log--what { min-width: 0; }
  .ds-log--detail { flex-basis: 100%; white-space: normal; overflow-wrap: anywhere; }
}

/* ── Settings list ────────────────────────────────────────────────────────
   Rows of a name and its description, a trailing control (a .ds-switch--sm,
   a ghost button) and a hairline above every row but the first. `--off` on
   the text dims a disabled row (a disabled control is exempt from contrast,
   WCAG 1.4.3). An agent-created row folds its code: `--fold` on a <details>,
   `--summary` on its <summary>, `--code` on the <pre>. `--notice` is a hint
   between rows. The text breaks a word longer than its room (an agent's
   snake_case tool name) instead of pushing the control out of the row. */
.ds-settings { margin: var(--space-3) 0 0; display: flex; flex-direction: column; }
.ds-settings--row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-2) 0;
  font-size: var(--text-sm);
  color: var(--text-muted);
  position: relative;
}
.ds-settings--row::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  height: var(--rule-width);
  background: var(--state-hover);
  pointer-events: none;
}
.ds-settings--row:first-child::before { display: none; }
.ds-settings--text { flex: 1; min-width: 0; line-height: 1.4; overflow-wrap: break-word; }
.ds-settings--name { color: var(--text); font-weight: var(--weight-normal); }
.ds-settings--off { opacity: 0.5; }
.ds-settings--fold { margin-top: var(--space-1); }
.ds-settings--summary { cursor: pointer; font-size: var(--text-xs); color: var(--text-muted); }
.ds-settings--code {
  max-height: var(--scroll-max-md);
  overflow: auto;
  margin: var(--space-1) 0 0;
  padding: var(--space-2);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  white-space: pre-wrap;
  word-break: break-all;
  background: var(--surface-fill-strong);
  border-radius: var(--radius-sm);
}
.ds-settings--notice { margin: var(--space-2) 0 0; }

/* ── Code and log blocks ──────────────────────────────────────────────────
   `.ds-code`: a value to copy (a command, a URL), a fill-strong block in
   mono; the consumer wires the copy. `.ds-code--log` (worn alone, on a
   <pre>): a log pane on the fill that takes the room it is given and
   scrolls, both ways. `--tall` (worn with it) caps it at --scroll-max-lg with
   each line on one line, so it scrolls sideways; `--short` at --scroll-max-sm; `--label` heads the
   pane that follows another. */
.ds-code {
  display: block;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  line-height: var(--leading-base);
  background: var(--surface-fill-strong);
  border: 0;
  border-radius: var(--radius-sm);
  padding: var(--space-2) var(--space-3);
  word-break: break-all;
  cursor: pointer;
  color: var(--text);
}
.ds-code--log {
  flex: 1;
  min-height: 0;
  overflow: auto;
  margin: 0;
  padding: var(--space-3);
  font-size: var(--text-xs);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  background: var(--surface-fill);
  border-radius: var(--radius-lg);
}
.ds-code--tall { max-height: var(--scroll-max-lg); margin-top: var(--space-3); white-space: pre; }
.ds-code--short { max-height: var(--scroll-max-sm); }
.ds-code--label { margin-top: var(--space-4); }

/* ── Prose ────────────────────────────────────────────────────────────────
   A memory's body: an agent's markdown, as DOM. The rhythm is the --prose-*
   tokens, in em. `<strong>` stays at Book: the panel's 600 is a synthetic
   bold (law 4). `--meta` is the line above the body (tags, dates);
   `--notes` (worn alone) is the release notes, one <article> per version
   with a hairline between them. */
.ds-prose { font-size: var(--text-base); line-height: 1.6; overflow-wrap: anywhere; }
.ds-prose > :first-child { margin-top: 0; }
.ds-prose > :last-child { margin-bottom: 0; }
.ds-prose p { margin: 0 0 var(--prose-gap); }
.ds-prose h4, .ds-prose h5, .ds-prose h6 {
  margin: var(--prose-heading-top) 0 var(--prose-heading-bottom);
  font-size: var(--text-base);
  font-weight: var(--weight-normal);
  letter-spacing: var(--tracking-tight);
}
.ds-prose ul, .ds-prose ol { margin: 0 0 var(--prose-gap); padding-left: var(--prose-indent); }
.ds-prose li { margin: var(--prose-item) 0; }
.ds-prose code {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  background: var(--surface-fill-strong);
  padding: var(--prose-code-y) var(--prose-code-x);
  border-radius: var(--radius-xs);
}
.ds-prose pre {
  margin: 0 0 var(--prose-gap);
  padding: var(--space-3);
  overflow-x: auto;
  background: var(--surface-panel);
  border-radius: var(--radius-sm);
}
.ds-prose pre code { background: none; padding: 0; line-height: 1.55; }
.ds-prose strong { font-weight: var(--weight-normal); }
.ds-prose table { border-collapse: collapse; margin: 0 0 var(--prose-gap); font-size: var(--text-sm); }
.ds-prose th, .ds-prose td { position: relative; padding: var(--space-1) var(--space-3); text-align: left; }
.ds-prose th::after, .ds-prose td::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: var(--rule-width);
  background: var(--state-hover);
  pointer-events: none;
}
.ds-prose--meta {
  display: flex;
  gap: var(--space-2);
  align-items: center;
  flex-wrap: wrap;
  font-size: var(--text-xs);
  color: var(--text-faint);
  margin-bottom: var(--space-2);
  overflow-wrap: anywhere;
}
.ds-prose--notes article { position: relative; padding: var(--space-3) 0; }
.ds-prose--notes article::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: var(--rule-width);
  background: var(--state-hover);
  pointer-events: none;
}
.ds-prose--notes article:last-child::after { display: none; }
.ds-prose--notes h2 { font-size: var(--text-lg); margin: 0 0 var(--space-2); }
.ds-prose--notes h3 { font-size: var(--text-md); margin: var(--space-3) 0 var(--space-1); }
.ds-prose--notes p, .ds-prose--notes li { font-size: var(--text-sm); line-height: var(--leading-base); }
.ds-prose--notes code { font-size: var(--text-xs); overflow-wrap: anywhere; }

/* ══════════════════════════════════════════════════════════════════════════
   DS-7 — CHAT AND MEDIA (1.8.0)
   The panel's chat and media pieces (S-007 report, DS-7), as components of
   their own: the chat transcript, the canvas toolbar and the media frame.
   Every part is a variant worn on its own element, never a descendant
   selector; the one state is `.is-controlling`, on the image it changes.
   ══════════════════════════════════════════════════════════════════════════ */

/* ── Chat transcript ──────────────────────────────────────────────────────
   A column of `--bubble`s: `--user` at the end on the violet tint, `--agent`
   at the start on the fill, each at most --bubble-max of the column, keeping
   its line breaks and breaking a long word anywhere. `--meta` is the line
   under a bubble's words (muted: faint is below 4.5:1 on the light tint).
   `--tool`, `--gap` and `--topic` are centred lines between bubbles: a tool
   call in faint mono, a gap in the record in sand, a change of topic in
   faint. `--padded` leaves room under the last bubble (a subject's copy). */
.ds-chat {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  overflow-y: auto;
}
.ds-chat--padded { padding-bottom: var(--space-4); }
.ds-chat--bubble {
  max-width: var(--bubble-max);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-bubble);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
.ds-chat--user { align-self: flex-end; background: var(--accent-2-tint); }
.ds-chat--agent { align-self: flex-start; background: var(--surface-fill); }
.ds-chat--meta { display: block; margin-top: var(--space-1); color: var(--text-muted); font-size: var(--text-xs); }
/* DS-8 (1.9.0): a tool line breaks a long unbroken token (a path, a URL)
   anywhere instead of pushing the transcript sideways. */
.ds-chat--tool {
  align-self: center;
  color: var(--text-faint);
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  padding: var(--space-half) var(--space-2);
  overflow-wrap: anywhere;
}
.ds-chat--gap { align-self: center; color: var(--sand-text); font-size: var(--text-xs); }
.ds-chat--topic { align-self: center; color: var(--text-faint); font-size: var(--text-xs); }

/* ── Canvas toolbar ───────────────────────────────────────────────────────
   One line above what the canvas shows: a `--back` button (worn with
   `.ds-btn--ghost .ds-btn--back`), the `--name` in mono, taking the room and
   clipped with an ellipsis, then the `--link`s (`.ds-btn--ghost
   .ds-btn--sm`), whole. `--row` is a live app's controls under it, wrapping:
   its state (`--live` in violet), links, `--switch` labels around a
   `.ds-switch--sm` and its words, inline `--code`, and the `--command` on a
   line of its own in faint. `--notice` is the public-link warning, violet,
   with its code and copy button. */
.ds-toolbar {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex: none;
  margin-bottom: var(--space-3);
  font-size: var(--text-sm);
  color: var(--text-muted);
}
.ds-toolbar--back { flex: none; }
.ds-toolbar--name {
  flex: 1;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ds-toolbar--link { white-space: nowrap; flex: none; }
.ds-toolbar--row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-4);
  flex: none;
  margin-bottom: var(--space-3);
  font-size: var(--text-sm);
  color: var(--text-muted);
}
.ds-toolbar--switch {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--text-xs);
  color: var(--text-muted);
  cursor: pointer;
}
.ds-toolbar--live { color: var(--accent-2-text); }
.ds-toolbar--code { font-size: var(--text-xs); overflow-wrap: anywhere; }
.ds-toolbar--command { flex-basis: 100%; color: var(--text-faint); }
.ds-toolbar--notice {
  flex: none;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  margin: 0 0 var(--space-2);
  font-size: var(--text-sm);
  color: var(--accent-2-text);
}

/* ── Media frame ──────────────────────────────────────────────────────────
   `.ds-media` is a letterbox on --surface-matte (ink in both themes) that
   takes the room it is given: the live `--image` fits inside it whole, a
   `--hint` and the control `--bar` under it, in --media-paper-dim (faint is
   below 3:1 on the matte in light). `.is-controlling` on the image draws the
   focus ring inside it while it takes the gestures. The bar holds `--keys`,
   each a `--key` (a filled `.ds-btn`), and an `--input` (with `.ds-field`).
   `--square` is the camera box, square and centred, its video `--cover`ing
   it; `--embed` an iframe that takes the room; `--sheet` a narrow column
   under the camera, `--status` a centred line in it. */
.ds-media {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  background: var(--surface-matte);
  border-radius: var(--radius-lg);
  overflow: hidden;
  padding: var(--space-2);
}
.ds-media--image { flex: 1; min-height: 0; max-width: 100%; object-fit: contain; display: block; }
.ds-media--image.is-controlling {
  touch-action: none;
  cursor: crosshair;
  outline: var(--focus-ring-media);
  outline-offset: calc(var(--focus-offset) * -1);
}
.ds-media--hint { flex: none; margin: 0; font-size: var(--text-xs); color: var(--media-paper-dim); text-align: center; }
.ds-media--bar {
  flex: none;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  font-size: var(--text-xs);
  color: var(--media-paper-dim);
}
.ds-media--keys { display: flex; flex-wrap: wrap; gap: var(--space-1); justify-content: center; }
.ds-media--key { min-width: var(--control-key); }
.ds-media--input { min-width: 0; width: var(--control-key-field); }
.ds-media--square {
  position: relative;
  max-width: var(--measure-narrow);
  margin: 0 auto var(--space-4);
  background: var(--surface-matte);
  border-radius: var(--radius-lg);
  overflow: hidden;
  aspect-ratio: 1;
}
.ds-media--cover { width: 100%; height: 100%; object-fit: cover; display: block; }
.ds-media--embed {
  flex: 1;
  min-height: 0;
  width: 100%;
  border: 0;
  border-radius: var(--radius-lg);
  background: transparent;
}
.ds-media--sheet { max-width: var(--measure-narrow); margin: 0 auto; }
.ds-media--status { text-align: center; }

/* ══════════════════════════════════════════════════════════════════════════
   DS-11 — BLANK: PALETTE, BLUR LAYER AND ACTIONS POPOVER (1.11.0)
   Phase b of S-026: the Design Studio prototype "Blank" is the source
   (Hector, 2026-10-01). Its palette is the system's tokens above (both
   themes; AA-corrected where it failed); these are its pieces: the header
   band, the view's motion, the Blur Layer and the plane it pushes back, the
   actions popover of words, and the held row. Each rule is one class or one
   class with a state, so the manifest reads each as a primitive, a variant
   or a state (SPEC, DS-11). The builder twins of the layer live in
   nix-components.css (`.confirm-layer*`), held equal to these.
   ══════════════════════════════════════════════════════════════════════════ */

/* ── Header band (DS-11) ──────────────────────────────────────────────────
   Blank's header is one band across the top of every view: `--header-height`
   tall, its title centred on `--header-mid`, `--space-6` in from either side.
   Worn with `.ds-header`, it lies over the top of its room (the nearest
   positioned ancestor) while the view scrolls under it. `.ds-title--band` is
   the view's title in the band: centred, uppercase, tracked, at the display
   weight, one line that ends in an ellipsis (a long token breaks anywhere
   and the band never grows over the view). `.ds-fade-band` goes on the view's
   scroller, the full height of the room (transparent, as every fade; the
   consumer pads its top past the band): its content fades out under the
   band, from the mid-line to `--space-7` below the band (plus
   `--header-fade-extra`, which a view with a tool row raises). */
.ds-header--band {
  position: absolute;
  inset: 0 0 auto;
  z-index: 1;
  height: var(--header-height);
  padding: calc(var(--header-mid) * 2 - var(--header-height)) var(--space-6) 0;
  align-items: center;
  justify-content: center;
}
.ds-title--band {
  margin: 0;
  min-width: 0;
  font-size: var(--text-band);
  font-weight: var(--weight-display);
  letter-spacing: var(--tracking-band);
  line-height: var(--leading-tight);
  text-transform: uppercase;
  text-align: center;
  overflow-wrap: anywhere;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 1;
  overflow: hidden;
}
.ds-fade-band {
  -webkit-mask-image: linear-gradient(to bottom, transparent 0, transparent calc(var(--header-mid) + var(--header-fade-extra)), #000 calc(var(--header-height) + var(--space-7) + var(--header-fade-extra)), #000 100%);
  mask-image: linear-gradient(to bottom, transparent 0, transparent calc(var(--header-mid) + var(--header-fade-extra)), #000 calc(var(--header-height) + var(--space-7) + var(--header-fade-extra)), #000 100%);
}

/* ── View motion (DS-11) ──────────────────────────────────────────────────
   A view fades in and out with a blur (`.ds-enter`, `.ds-leave`, which also
   stops taking taps), and its children drop in one after another: each
   `.ds-stagger--item` waits `--i` steps of `--stagger-step` (the consumer
   sets `--i` on the child, 0 for the first). Reduced motion drops all three
   (the global rule cuts durations; these also drop the delays). */
@keyframes ds-drop { from { opacity: 0; transform: translateY(calc(-1 * var(--drop-distance))); } to { opacity: 1; transform: none; } }
@keyframes ds-blurin { from { opacity: 0; filter: blur(var(--enter-blur)); } to { opacity: 1; filter: none; } }
.ds-enter { animation: ds-blurin var(--dur-layer) var(--ease-settle) both; }
.ds-leave {
  opacity: 0;
  filter: blur(var(--enter-blur));
  pointer-events: none;
  transition: opacity var(--dur-layer) var(--ease-settle), filter var(--dur-layer) var(--ease-settle);
}
.ds-stagger--item {
  animation: ds-drop var(--dur-drop) var(--ease-settle) both;
  animation-delay: calc(var(--i, 0) * var(--stagger-step));
}
@media (prefers-reduced-motion: reduce) {
  .ds-enter { animation: none; }
  .ds-leave { transition: none; }
  .ds-stagger--item { animation: none; }
}

/* ── Blur Layer (DS-11) ───────────────────────────────────────────────────
   A card over the view with no ground and no shadow (SPEC law 3, amended):
   its depth is the plane behind it receding. `.ds-layer` covers the window
   (fixed, the dialog rung) and shows its `.ds-layer--card` near the top,
   `--measure-layer` wide at most, a centred `.ds-layer--title` (the band
   title's type) over its `.ds-layer--body`. It is hidden until `.is-open`.
   `--top` stacks over a base layer (Billing over Account). The consumer
   puts `.ds-recede` on what the layer covers (the view `--deep`, the header
   and footer plain; under a top layer, the base layer `--deep`) and makes
   it `inert`; dialog({ layer: true }) closes on Escape and on a tap outside
   the card. The card's and the body's one column never grows past the card
   (DS-14): a long word that only breaks where it would overflow (a
   settings row's text) wraps inside it instead of widening the card past a
   phone's edge. Twins: .confirm-layer, .confirm-layer--top,
   .confirm-layer-card, .confirm-layer-title, .confirm-layer-body. */
.ds-layer {
  position: fixed;
  inset: 0;
  z-index: var(--z-dialog);
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: calc(var(--header-height) + var(--space-6)) var(--space-6) var(--space-7);
  overflow-y: auto;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--dur-layer) var(--ease-settle), transform var(--dur-recede) var(--ease-out), filter var(--dur-layer) var(--ease-settle);
}
.ds-layer.is-open { opacity: 1; pointer-events: auto; }
.ds-layer--top { z-index: calc(var(--z-dialog) + 1); }
.ds-layer--card {
  width: min(var(--measure-layer), 100%);
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-4);
  animation: ds-rise var(--dur-settle) var(--ease-settle) both;
}
.ds-layer--title {
  margin: 0;
  font-size: var(--text-band);
  font-weight: var(--weight-display);
  letter-spacing: var(--tracking-band);
  line-height: var(--leading-tight);
  text-transform: uppercase;
  text-align: center;
  color: var(--text);
  overflow-wrap: anywhere;
}
.ds-layer--body {
  margin: 0;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-4);
  font-size: var(--text-base);
  line-height: var(--leading-base);
  color: var(--text-muted);
  text-align: center;
  overflow-wrap: anywhere;
}

/* ── Receding plane (DS-11) ───────────────────────────────────────────────
   What an overlay covers steps back: smaller, blurred, dimmer, and it stops
   taking taps. `--deep` is the plane right under a Blur Layer's card, nearly
   gone (worn with `.ds-recede`). The words in it fade with it: that is depth,
   not text, so scripts/contrast.mjs exempts exactly these two selectors from
   its faded-text law, and the consumer makes the plane `inert`. */
.ds-recede {
  transform: scale(var(--recede-scale));
  filter: blur(var(--recede-blur));
  opacity: var(--recede-opacity);
  pointer-events: none;
  transition: transform var(--dur-recede) var(--ease-out), filter var(--dur-layer) var(--ease-settle), opacity var(--dur-layer) var(--ease-settle);
}
.ds-recede--deep { opacity: var(--recede-opacity-deep); }

/* ── Actions popover (DS-11) ──────────────────────────────────────────────
   Blank's popover is words with no ground: `.ds-menu--words` (worn with
   `.ds-menu`) drops the menu's fill, radius, padding and cast and stacks its
   `.ds-menu--word`s at the end, under the pressed row; the view recedes
   behind it. Each word rests hidden and rises in with `.is-open`, `--j`
   steps of `--stagger-step` after the first. A toggle word carries
   aria-pressed and data-state ("On", "Off"), shown after it in the soft ink;
   `.is-danger` is a destructive word. Disabled, a word is soft and takes no
   hover. */
.ds-menu--words {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: var(--space-half);
  padding: 0;
  border-radius: 0;
  background: transparent;
  box-shadow: none;
}
.ds-menu--word {
  max-width: 100%;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--text);
  font-size: var(--text-md);
  text-align: end;
  overflow-wrap: anywhere;
  opacity: 0;
  transform: translateY(var(--space-1));
  transition: opacity var(--dur-slow) var(--ease-out), transform var(--dur-slow) var(--ease-settle), background-color var(--dur-fast) var(--ease-out);
}
.ds-menu--word.is-open { opacity: 1; transform: none; transition-delay: calc(var(--j, 0) * var(--stagger-step)); }
.ds-menu--word:hover { background: var(--state-hover); }
.ds-menu--word:disabled { color: var(--text-muted); cursor: default; }
.ds-menu--word:disabled:hover { background: transparent; }
.ds-menu--word.is-danger { color: var(--danger-text); }
.ds-menu--word[aria-pressed]::after { content: " · " attr(data-state); color: var(--text-muted); }
@media (prefers-reduced-motion: reduce) {
  .ds-menu--word { transition: none; }
}

/* ── Held row (DS-11) ─────────────────────────────────────────────────────
   Press and hold a row to open its actions (holdable() in components.js):
   while held, `.is-pressing` shrinks it a little. A row a popover removes
   slides out with `.is-gone` before the consumer drops it. */
.ds-interactive.is-pressing {
  transform: scale(var(--press-scale));
  transition: transform var(--dur-recede) var(--ease-out), background-color var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out);
}
.ds-interactive--row.is-gone {
  opacity: 0;
  transform: translateX(var(--space-2));
  transition: opacity var(--dur-slow) var(--ease-out), transform var(--dur-slow) var(--ease-out);
}

/* ══════════════════════════════════════════════════════════════════════════
   DS-12 — BLANK: FIND, FILTER CARD, ROWS AND NAVIGATION (1.12.0)
   Phase c of S-026: the Design Studio prototype "Blank" is the source
   (Hector, 2026-10-01). These are its search behind Actions, its frosted
   filter card, the rows of its Settings, MCP and devices, its fields and
   choices, its sidebar of words with the profile and its menu, and its
   footer band. Each rule is one class, or one class with a state or a part,
   so the manifest reads each as a primitive, a variant, a state or a part
   (SPEC, DS-12). A parent's state reaches a child only through a class or an
   aria attribute the builder sets on that child: no descendant selector.
   ══════════════════════════════════════════════════════════════════════════ */

/* ── Lines glyph (DS-12) ──────────────────────────────────────────────────
   Blank's menu and Actions buttons draw three lines. `.ds-btn--lines` is the
   button (worn with `.ds-btn .ds-btn--icon`), in `--text`; `.is-away` fades
   it out (the sidebar's menu button while the words are up or peeking, where
   it also wears `.ds-recede` and sideNav() makes it inert). Its one
   `.ds-lines` span is the middle bar, `--lines-width` long and
   `--lines-weight` thick; `::before` and `::after` are the bars above and
   below. `--menu` hangs the short bars from the start; under `.is-even` (a
   mouse over the button, set by sideNav()) they even out, and under
   `.is-open` they cross into an X and the middle bar clears.
   `--actions` centres them; under `.is-open` the glyph turns a quarter. */
.ds-btn--lines {
  background: transparent;
  color: var(--text);
  transition: opacity var(--dur-layer) var(--ease-out), color var(--dur-fast) var(--ease-out), transform var(--dur-recede) var(--ease-out), filter var(--dur-layer) var(--ease-settle);
}
.ds-btn--lines.is-away { opacity: 0; pointer-events: none; }
.ds-lines {
  position: relative;
  display: block;
  width: var(--lines-width);
  height: var(--lines-weight);
  border-radius: var(--radius-xs);
  background: currentColor;
  transition: background-color var(--dur-base) var(--ease-out), transform var(--dur-layer) var(--ease-settle);
}
.ds-lines::before,
.ds-lines::after {
  content: "";
  position: absolute;
  left: 0;
  width: var(--icon-sm);
  height: var(--lines-weight);
  border-radius: var(--radius-xs);
  background: currentColor;
  transition: width var(--dur-slow) var(--ease-out), transform var(--dur-slow) var(--ease-out), top var(--dur-slow) var(--ease-out);
}
.ds-lines::before { top: calc(-1 * (var(--space-1) + var(--space-half))); }
.ds-lines::after { top: calc(var(--space-1) + var(--space-half)); }
.ds-lines--menu.is-even::before,
.ds-lines--menu.is-even::after { width: var(--lines-width); }
.ds-lines--menu.is-open { background: transparent; }
.ds-lines--menu.is-open::before { top: 0; width: var(--icon-xl); transform: rotate(45deg); }
.ds-lines--menu.is-open::after { top: 0; width: var(--icon-xl); transform: rotate(-45deg); }
.ds-lines--actions::before,
.ds-lines--actions::after { left: calc((var(--lines-width) - var(--icon-sm)) / 2); }
.ds-lines--actions.is-open { transform: rotate(90deg); }

/* ── Search behind Actions (DS-12) ────────────────────────────────────────
   `.ds-find` is the dock at the bottom of its room, `--footer-lift` up,
   stacking upward (`column-reverse`: the bar at the bottom, the filter card
   above it; focus goes bar, then card, in DOM order). It takes no taps
   itself, only its pieces do. `.ds-find--trigger` is the Actions button
   (worn with `.ds-btn .ds-btn--icon .ds-btn--lines`), centred on the footer
   band; `[aria-expanded="true"]` fades and shrinks it away while the bar is
   open, and `.has-dot` marks a search or a filter still on. `.ds-find--bar`
   rises in its place with `.is-open`, `--find-width` wide at most; it holds
   a `.ds-field--pill` and, at its end, `.ds-find--filter` (worn with
   `.ds-btn .ds-btn--icon`), which opens the filter card and wears the same
   dot. findBar() in components.js builds all of it. */
.ds-find {
  position: absolute;
  inset: auto 0 var(--footer-lift);
  display: flex;
  flex-direction: column-reverse;
  align-items: center;
  gap: var(--space-2);
  padding: 0 var(--space-6);
  pointer-events: none;
}
.ds-find--trigger {
  position: absolute;
  bottom: calc(var(--footer-height) / 2);
  left: 50%;
  translate: -50% 50%;
  color: var(--text-muted);
  pointer-events: auto;
  transition: opacity var(--dur-slow) var(--ease-out), scale var(--dur-slow) var(--ease-out), color var(--dur-fast) var(--ease-out);
}
.ds-find--trigger:hover { color: var(--text); }
.ds-find--trigger[aria-expanded="true"] { opacity: 0; scale: 0.8; pointer-events: none; }
.ds-find--trigger.has-dot::after {
  content: "";
  position: absolute;
  top: 0;
  right: 0;
  width: var(--dot-size-sm);
  height: var(--dot-size-sm);
  border-radius: var(--radius-pill);
  background: var(--text);
}
.ds-find--bar {
  position: relative;
  width: min(var(--find-width), 100%);
  opacity: 0;
  visibility: hidden;
  transform: translateY(var(--space-2));
  pointer-events: auto;
  transition: opacity var(--dur-slow) var(--ease-out), transform var(--dur-layer) var(--ease-settle), visibility var(--dur-slow);
}
.ds-find--bar.is-open {
  opacity: 1;
  visibility: visible;
  transform: none;
  transition: opacity var(--dur-slow) var(--ease-out), transform var(--dur-layer) var(--ease-settle), visibility 0s;
}
.ds-find--filter {
  position: absolute;
  top: 50%;
  right: var(--space-1);
  translate: 0 -50%;
  border-radius: var(--radius-pill);
  background: transparent;
  color: var(--text-muted);
}
.ds-find--filter:hover { background: var(--state-hover); color: var(--text); }
.ds-find--filter[aria-expanded="true"] { background: var(--state-hover); color: var(--text); }
.ds-find--filter.has-dot::after {
  content: "";
  position: absolute;
  top: calc(var(--space-1) + var(--space-half));
  right: calc(var(--space-1) + var(--space-half));
  width: var(--dot-size-sm);
  height: var(--dot-size-sm);
  border-radius: var(--radius-pill);
  background: var(--text);
}
/* The search pill: worn with `.ds-field` on the input. No border, the soft
   hover ground, a pill; its end leaves room for the filter button. The
   placeholder is muted, not faint: faint on the hover ground over the light
   canvas's last stop is 4.12:1. */
.ds-field--pill {
  padding: var(--space-2) var(--control-lg) var(--space-2) var(--space-4);
  border-radius: var(--radius-pill);
  background: var(--state-hover);
  color: var(--text);
  font-size: var(--text-base);
}
.ds-field--pill::placeholder { color: var(--text-muted); }

/* ── Filter card (DS-12) ──────────────────────────────────────────────────
   A frosted card (`--surface-frost` over a `--frost-blur` backdrop blur)
   that opens upward from the search with `.is-open`, `--filter-card-width`
   wide at most and scrolling past `--filter-card-max`. Each `--row` is a
   `--label` (small caps, muted, in a `--filter-label-width` column) beside
   its `--pills` cell (the row's control: pills, segments, selects), then a
   `--foot` with a `--hint` and Clear filters. Compact, a label sits over its
   control and the hint goes. filterCard() builds it. */
.ds-filter-card {
  width: min(var(--filter-card-width), 100%);
  display: grid;
  gap: var(--space-3);
  max-height: 0;
  padding: 0 var(--space-4);
  border-radius: var(--radius-lg);
  background: var(--surface-frost);
  -webkit-backdrop-filter: blur(var(--frost-blur));
  backdrop-filter: blur(var(--frost-blur));
  opacity: 0;
  visibility: hidden;
  overflow: hidden;
  pointer-events: auto;
  transition: max-height var(--dur-slow) var(--ease-out), padding var(--dur-slow) var(--ease-out), opacity var(--dur-slow) var(--ease-out), visibility var(--dur-slow);
}
.ds-filter-card.is-open {
  max-height: var(--filter-card-max);
  padding: var(--space-3) var(--space-4);
  opacity: 1;
  visibility: visible;
  overflow-y: auto;
  transition: max-height var(--dur-slow) var(--ease-out), padding var(--dur-slow) var(--ease-out), opacity var(--dur-slow) var(--ease-out), visibility 0s;
}
.ds-filter-card--row {
  display: grid;
  grid-template-columns: var(--filter-label-width) minmax(0, 1fr);
  align-items: center;
  gap: var(--space-2);
  margin: 0;
}
.ds-filter-card--label {
  font-size: var(--text-2xs);
  letter-spacing: var(--tracking-band);
  text-transform: uppercase;
  color: var(--text-muted);
  overflow-wrap: anywhere;
}
.ds-filter-card--pills {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
}
.ds-filter-card--foot {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--space-3);
  padding-top: var(--space-2);
}
.ds-filter-card--hint {
  min-width: 0;
  margin-inline-end: auto;
  font-size: var(--text-2xs);
  color: var(--text-muted);
  overflow-wrap: anywhere;
}
@media (max-width: 640px) {
  .ds-filter-card--row { grid-template-columns: 1fr; gap: var(--space-1); }
  .ds-filter-card--hint { display: none; }
}
/* A text button (Clear filters), worn with `.ds-btn`: no ground, no
   padding, its words on one line beside a long hint, underlined on hover;
   disabled it is faint, not faded (opacity would lower the contrast the
   check measures). */
.ds-btn--text {
  flex: none;
  padding: 0;
  border-radius: 0;
  background: transparent;
  color: var(--text);
  font-size: var(--text-xs);
  text-underline-offset: var(--space-1);
}
.ds-btn--text:hover { text-decoration: underline; }
.ds-btn--text:disabled { color: var(--text-faint); opacity: 1; text-decoration: none; }
/* The summary chip: what is on while the card is closed, frosted, its
   `--text` (a button that opens the card) beside its `--clear` ×. */
.ds-filter-summary {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  max-width: 100%;
  padding: var(--space-1) var(--space-2) var(--space-1) var(--space-3);
  border-radius: var(--radius-pill);
  background: var(--surface-frost);
  -webkit-backdrop-filter: blur(var(--frost-blur));
  backdrop-filter: blur(var(--frost-blur));
  color: var(--text);
  font-size: var(--text-xs);
  pointer-events: auto;
}
.ds-filter-summary--text { min-width: 0; text-align: start; overflow-wrap: anywhere; }
.ds-filter-summary--clear {
  flex: none;
  display: grid;
  place-items: center;
  width: var(--icon-lg);
  height: var(--icon-lg);
  border-radius: var(--radius-pill);
  color: var(--text-muted);
  font-size: var(--text-base);
  line-height: 1;
}
.ds-filter-summary--clear:hover { background: var(--state-hover); color: var(--text); }
/* Pills: a wrapping row of `.ds-chip` buttons (filterRow `"pills"`). A chip
   rests muted with no ground, takes the hover ground, and pressed it is ink
   with the first stop's paper on it. */
.ds-pills {
  display: flex;
  flex-wrap: wrap;
  gap: calc(var(--space-1) + var(--space-half));
  min-width: 0;
}
.ds-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  max-width: 100%;
  padding: var(--space-1) var(--space-3);
  border-radius: var(--radius-pill);
  background: transparent;
  color: var(--text-muted);
  font-size: var(--text-xs);
  text-align: start;
  overflow-wrap: anywhere;
  transition: background-color var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out);
}
.ds-chip:hover { background: var(--state-hover); }
.ds-chip[aria-pressed="true"] { background: var(--text); color: var(--surface-ground-top); }
/* Segments: one of a few (filterRow `"segments"`, the Mode row, Tone). An
   `--option` is a button with aria-pressed, or a label around a radio that
   wears `--input`; checked, it takes `--surface-segment` and the strong
   hairline (law 1's line, never the prototype's ink). Its words stay
   `--text`: muted is 4.39:1 on the dark segment. A radio's focus rings the
   option. `--other` is the field that unrolls under the segments when Other
   is picked (`.is-shown`). */
.ds-segment {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  min-width: 0;
}
.ds-segment--option {
  position: relative;
  display: inline-flex;
  align-items: center;
  max-width: 100%;
  padding: var(--space-1) var(--space-3);
  border: var(--rule-width) solid var(--hairline);
  border-radius: var(--radius-pill);
  background: transparent;
  color: var(--text);
  font-size: var(--text-xs);
  text-align: start;
  overflow-wrap: anywhere;
  cursor: pointer;
  transition: background-color var(--dur-base) var(--ease-out);
}
.ds-segment--option[aria-pressed="true"] { border: var(--rule-width) solid var(--hairline-strong); background: var(--surface-segment); }
.ds-segment--option:has(:checked) { border: var(--rule-width) solid var(--hairline-strong); background: var(--surface-segment); }
.ds-segment--option:has(:focus-visible) { outline: var(--focus-ring); outline-offset: var(--focus-offset); }
.ds-segment--input {
  position: absolute;
  inset: 0;
  margin: 0;
  opacity: 0;
  cursor: pointer;
}
.ds-segment--other { display: none; }
.ds-segment--other.is-shown { display: block; animation: ds-reveal var(--dur-layer) var(--ease-settle) both; }
/* The soft select (Subject, Chapter): worn with `.ds-field` on a <select>,
   on the hover ground, no border, the system's chevron kept. It outranks
   `select:not(.pane-select)` by one class (D7). Disabled (Chapter before a
   Subject) its words are muted, not faint: faint on the hover ground over
   the light canvas's last stop is 4.12:1. */
.ds-field--soft:not(.pane-select) {
  width: 100%;
  padding: var(--space-2) calc(var(--space-6) + var(--space-1)) var(--space-2) var(--space-3);
  border-radius: var(--radius-sm);
  background-color: var(--state-hover);
  color: var(--text);
  font-size: var(--text-sm);
}
.ds-field--soft:not(.pane-select):hover { background-color: var(--hairline); }
.ds-field--soft:not(.pane-select):disabled { background-color: var(--state-hover); color: var(--text-muted); cursor: default; }

/* ── Sidebar, profile and footer (DS-12) ──────────────────────────────────
   Blank's sidebar is words over the receding view, not a panel column.
   `.ds-nav--menu` places its lines button at the start of the header band;
   while the words are up it fades away and they take its place.
   `.ds-veil` covers the room under the words; `.is-open` makes it take the
   tap that closes them. `.ds-nav--words` (worn with `.ds-nav`) drops the
   nav's ground, padding, radius and clipping and stacks its `--word`s in
   the menu button's place (the glyph's left, in the band; under the band
   on a phone); each rests hidden and fades in with `.is-open` after `--i`
   steps of `--stagger-step`, uppercase and tracked in the band title's
   type, muted, `--text` when hovered, focused or current. `.ds-profile` is
   the initial and the name at the bottom, shown with `.is-open`; its
   `--menu` (a `.ds-menu--words` popover with `.ds-menu--start`) opens above
   it. sideNav() builds all of it. `.ds-footer` is the band at the bottom of
   every view, `--footer-height` tall and `--footer-lift` up: its `--bar`
   holds a view's actions (`--act`) or a `--placeholder` word. */
.ds-nav--menu {
  position: absolute;
  z-index: calc(var(--z-menu) + 1);
  top: calc(var(--header-mid) - var(--space-4));
  left: var(--space-6);
}
.ds-veil {
  position: absolute;
  inset: 0;
  z-index: var(--z-menu);
  pointer-events: none;
}
.ds-veil.is-open { pointer-events: auto; }
.ds-nav--words {
  position: absolute;
  z-index: calc(var(--z-menu) + 1);
  top: calc(var(--header-mid) + var(--space-1));
  left: calc(var(--space-6) + var(--space-1));
  max-width: calc(100% - 2 * var(--space-7));
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-3);
  padding: 0;
  border-radius: 0;
  background: transparent;
  overflow: visible;
  transform-origin: 0 50%;
  transition: transform var(--dur-recede) var(--ease-out), filter var(--dur-layer) var(--ease-settle), opacity var(--dur-layer) var(--ease-settle);
}
@media (max-width: 640px) {
  .ds-nav--words { flex-direction: column; align-items: flex-start; top: calc(var(--header-height) + var(--space-3)); max-width: calc(100% - 2 * var(--space-6)); }
}
.ds-nav--word {
  max-width: 100%;
  padding: 0;
  background: transparent;
  color: var(--text-muted);
  font-size: var(--text-band);
  font-weight: var(--weight-display);
  letter-spacing: var(--tracking-band);
  line-height: var(--leading-tight);
  text-transform: uppercase;
  text-align: start;
  overflow-wrap: anywhere;
  opacity: 0;
  transition: opacity var(--dur-layer) var(--ease-out), color var(--dur-base) var(--ease-out);
}
.ds-nav--word.is-open { opacity: 1; transition-delay: calc(var(--dur-slow) + var(--i, 0) * var(--stagger-step)), 0s; }
.ds-nav--word:hover { color: var(--text); }
.ds-nav--word:focus-visible { color: var(--text); }
.ds-nav--word[aria-current="page"] { color: var(--text); }
@media (prefers-reduced-motion: reduce) {
  .ds-nav--word { transition: none; }
}
.ds-profile {
  position: absolute;
  z-index: calc(var(--z-menu) + 1);
  left: calc(var(--space-6) + var(--space-1));
  bottom: var(--space-7);
  display: flex;
  align-items: center;
  gap: var(--space-3);
  max-width: calc(100% - 2 * var(--space-7));
  padding: 0;
  background: transparent;
  color: var(--text-muted);
  opacity: 0;
  pointer-events: none;
  transition: color var(--dur-base) var(--ease-out), opacity var(--dur-layer) var(--ease-out);
}
.ds-profile.is-open { opacity: 1; pointer-events: auto; transition-delay: 0s, var(--dur-layer); }
.ds-profile:hover { color: var(--text); }
.ds-profile--name {
  min-width: 0;
  font-size: var(--text-sm);
  letter-spacing: var(--tracking-band);
  text-transform: uppercase;
  text-align: start;
  overflow-wrap: anywhere;
}
.ds-profile--menu {
  position: absolute;
  z-index: calc(var(--z-menu) + 2);
  left: var(--space-4);
  bottom: calc(var(--space-7) + var(--control-lg));
  max-width: calc(100% - 2 * var(--space-5));
}
/* A person's initial in ink (the profile, the Settings head): `--ink` is
   the first stop's paper on --text; `--lg` the Settings head's size. */
.ds-avatar--ink { background: var(--text); color: var(--surface-ground-top); }
.ds-avatar--lg { width: var(--avatar-size-lg); font-size: var(--text-lg); }
/* The profile menu's words: start-aligned (over the name) or centred (over
   Actions); worn with `.ds-menu--words` and on each word. Like the sidebar's
   words they rest muted with no ground and sharpen to --text on hover. */
.ds-menu--start { align-items: flex-start; text-align: start; color: var(--text-muted); }
.ds-menu--start:hover { background: transparent; color: var(--text); }
.ds-menu--centre { align-items: center; text-align: center; color: var(--text-muted); }
.ds-menu--centre:hover { background: transparent; color: var(--text); }
/* A small switch inside a popover word (the profile menu's Theme). */
.ds-switch--inline { display: inline-block; vertical-align: middle; margin-inline-start: var(--space-3); }
.ds-footer {
  position: absolute;
  inset: auto 0 0;
  display: flex;
  justify-content: center;
  padding-bottom: var(--footer-lift);
  pointer-events: none;
}
.ds-footer--bar {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-6);
  width: min(var(--measure-dock), calc(100% - 2 * var(--space-6)));
  height: var(--footer-height);
  pointer-events: auto;
}
.ds-footer--placeholder {
  min-width: 0;
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-band);
  text-transform: uppercase;
  color: var(--text-muted);
  overflow-wrap: anywhere;
  text-align: center;
}
.ds-footer--act {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--space-7);
  height: var(--space-7);
  border-radius: var(--radius-pill);
  color: var(--text-muted);
  transition: color var(--dur-fast) var(--ease-out);
}
.ds-footer--act:hover { color: var(--text); }
/* The back chevron in the band (worn with `.ds-btn .ds-btn--icon`): muted,
   ink on hover, never faded. */
.ds-btn--chevron {
  background: transparent;
  color: var(--text-muted);
  transition: color var(--dur-base) var(--ease-out);
}
.ds-btn--chevron:hover { color: var(--text); }

/* ── Rows (DS-12) ─────────────────────────────────────────────────────────
   Blank's Settings, MCP, devices, language and artifact rows. A row that
   opens something wears `.ds-interactive .ds-interactive--row` and
   `--inset`: its words in --text-base, `--space-5` apart, a long token
   broken anywhere; on hover the ground comes in and the words step in by
   `--space-3`. `.ds-fact--value`
   is a read-only value (muted, one line, ellipsed) and `.ds-fact--meta` a
   row's small caps meta word. A switch row in Settings is a
   `.ds-settings--row` with `--bare`: Blank's row height and no hairline (in
   Blank a line means an editable field). A section switched off is `--dim`
   (dim, grey, no taps; the consumer makes it inert). `.ds-dot--live` is a
   live connection's dot, with a halo; `.ds-badge--via` an outlined small
   caps tag on the strong hairline (`.is-strong`: ink words). */
.ds-interactive--inset {
  width: 100%;
  gap: var(--space-5);
  padding: var(--space-3) 0;
  border-radius: var(--radius-sm);
  font-size: var(--text-base);
  text-align: start;
  overflow-wrap: anywhere;
  transition: background-color var(--dur-base) var(--ease-out), color var(--dur-fast) var(--ease-out), padding var(--dur-base) var(--ease-out);
}
.ds-interactive--inset:hover { padding-left: var(--space-3); padding-right: var(--space-3); }
/* Pressed, an inset row keeps the hover ground: the press ground
   (.ds-interactive:active, --state-press) puts its muted meta and value
   under 4.5:1 on the canvas (law 2; SPEC, DS-14). */
.ds-interactive--inset:active { background: var(--state-hover); }
.ds-fact--value {
  flex: 0 1 auto;
  min-width: 0;
  max-width: 60%;
  font-size: var(--text-base);
  color: var(--text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ds-fact--meta {
  flex: 0 1 auto;
  min-width: 0;
  max-width: 50%;
  font-size: var(--text-2xs);
  letter-spacing: var(--tracking-band);
  text-transform: uppercase;
  color: var(--text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ds-settings--bare { gap: var(--space-5); padding: var(--space-3) 0; }
.ds-settings--bare::before { display: none; }
.ds-settings--dim {
  opacity: var(--dim-opacity);
  filter: grayscale(1);
  pointer-events: none;
  transition: opacity var(--dur-slow) var(--ease-out), filter var(--dur-slow) var(--ease-out);
}
.ds-dot--live {
  background: var(--status-live);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--status-live) 18%, transparent);
}
.ds-badge--via {
  flex: none;
  padding: var(--space-1) var(--space-2);
  letter-spacing: var(--tracking-band);
  text-transform: uppercase;
  border: var(--rule-width) solid var(--hairline-strong);
  background: transparent;
  color: var(--text-muted);
}
.ds-badge--via.is-strong { color: var(--text); }
/* The address row (Web access, MCP, an artifact's public link): a value on
   the hover ground with a Copy word at its end, shown with `.is-shown`
   (it slides in, `ds-reveal`). The value breaks a long address anywhere,
   so it is read whole. addressRow() builds it. */
@keyframes ds-reveal { from { opacity: 0; transform: translateY(calc(-1 * var(--space-1))); } }
.ds-toolbar--address {
  display: none;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  margin: calc(-1 * var(--space-1)) 0 var(--space-2);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-sm);
  background: var(--state-hover);
}
.ds-toolbar--address.is-shown { display: flex; animation: ds-reveal var(--dur-layer) var(--ease-settle) both; }
.ds-toolbar--value { flex: 1; min-width: 0; font-size: var(--text-sm); color: var(--text); overflow-wrap: anywhere; }
.ds-toolbar--copy {
  flex: none;
  font-size: var(--text-2xs);
  letter-spacing: var(--tracking-band);
  text-transform: uppercase;
  color: var(--text-muted);
  transition: color var(--dur-base) var(--ease-out);
}
.ds-toolbar--copy:hover { color: var(--text); }
/* The language list: `--list` holds one `--pick` per language (a button
   with aria-pressed); the chosen one is --text and ends in a check (a
   mask in currentColor). Hovered, a pick steps in like a row. */
.ds-choices--list { display: flex; flex-direction: column; }
.ds-choices--pick {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  width: 100%;
  padding: var(--space-3) 0;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--text-muted);
  font-size: var(--text-base);
  text-align: start;
  overflow-wrap: anywhere;
  transition: background-color var(--dur-base) var(--ease-out), color var(--dur-base) var(--ease-out), padding var(--dur-base) var(--ease-out);
}
.ds-choices--pick:hover { padding-left: var(--space-3); padding-right: var(--space-3); background: var(--state-hover); color: var(--text); }
.ds-choices--pick[aria-pressed="true"] { color: var(--text); }
.ds-choices--pick[aria-pressed="true"]::after {
  content: "";
  flex: none;
  width: var(--icon-sm);
  height: var(--icon-sm);
  background: currentColor;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12.5l5 4.5 9-10'/%3E%3C/svg%3E") center / contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12.5l5 4.5 9-10'/%3E%3C/svg%3E") center / contain no-repeat;
}
/* Blank's radio group: each option a `--row` (a <label>: the radio, then
   its `--title`, its `--about` under it), the radios in ink. */
.ds-choices--row {
  display: grid;
  grid-template-columns: var(--icon-sm) minmax(0, 1fr);
  align-items: baseline;
  column-gap: var(--space-3);
  row-gap: var(--space-half);
  font-size: var(--text-base);
  accent-color: var(--text);
  cursor: pointer;
}
.ds-choices--title { color: var(--text); overflow-wrap: anywhere; }
.ds-choices--about { grid-column: 2; font-size: var(--text-sm); color: var(--text-muted); overflow-wrap: anywhere; }
/* Fields: `--line` is Blank's editable field, an underline on the strong
   hairline (a line means editable); its placeholder faint, read-only muted,
   and the focus ring (not an ink underline) when focused. `--caption` is
   the small caps label over a field, a radio group or segments. */
.ds-field--line {
  min-height: 0;
  padding: var(--space-1) 0 var(--space-2);
  border-bottom: var(--rule-width) solid var(--hairline-strong);
  border-radius: 0;
  background: transparent;
  color: var(--text);
  font-size: var(--text-md);
}
.ds-field--line::placeholder { color: var(--text-faint); }
.ds-field--line[readonly] { color: var(--text-muted); }
.ds-field--line:focus-visible { outline: var(--focus-ring); outline-offset: var(--focus-offset); }
.ds-field--caption {
  display: block;
  margin-bottom: var(--space-2);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--text-faint);
  overflow-wrap: anywhere;
}
/* A checkbox in ink (`.ds-check--ink`, the native box takes --text). */
.ds-check--ink { gap: var(--space-3); font-size: var(--text-base); color: var(--text); accent-color: var(--text); cursor: pointer; }
/* Notices: `--saved` is the word that fades in after a save (`.is-shown`),
   small caps, centred; `--empty` a list's empty state, centred. */
.ds-status--saved {
  justify-self: center;
  margin: var(--space-2) 0 var(--space-6);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-band);
  text-transform: uppercase;
  text-align: center;
  color: var(--text-muted);
  opacity: 0;
  transition: opacity var(--dur-layer) var(--ease-out);
}
.ds-status--saved.is-shown { opacity: 1; }
.ds-status--empty { margin: var(--space-6) 0 0; font-size: var(--text-base); text-align: center; color: var(--text-muted); }
/* Lead text: `--centre` is a section's one or two soft lines under its
   title (40ch: a short measure, centred); `--hint` the smaller line under a
   field. */
.ds-lead--centre { margin: calc(-1 * var(--space-2)) auto var(--space-2); max-width: 40ch; font-size: var(--text-base); text-align: center; overflow-wrap: anywhere; }
.ds-lead--hint { margin: 0; font-size: var(--text-xs); overflow-wrap: anywhere; }
/* Labels and ruled rows: `.ds-label` is the small caps label over a block
   of rows; `.ds-rows` a list whose `--row`s sit between strong hairlines
   (above each, and below the last). */
.ds-label {
  margin: 0 0 var(--space-3);
  font-size: var(--text-2xs);
  letter-spacing: var(--tracking-band);
  text-transform: uppercase;
  color: var(--text-muted);
  overflow-wrap: anywhere;
}
.ds-rows { display: flex; flex-direction: column; margin: 0; padding: 0; list-style: none; }
.ds-rows--row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-5);
  padding: var(--space-3) 0;
  border-top: var(--rule-width) solid var(--hairline-strong);
  font-size: var(--text-base);
  color: var(--text);
  overflow-wrap: anywhere;
}
.ds-rows--row:last-child { border-bottom: var(--rule-width) solid var(--hairline-strong); }

/* ══════════════════════════════════════════════════════════════════════════
   DS-13 — BLANK: MEMORY, EVENTS AND GRAPH (1.13.0)
   Phase d of S-026: the Design Studio prototype "Blank" is the source
   (Hector, 2026-10-01). These are its memory kind colours (user-pickable),
   its memory card with tags, fields and versions, its memory list, its chat
   log with transcript marks, its events timeline, the soft tooltip and the
   birth label of its graph, and its artifact preview. Each rule is one
   class, or one class with a state or a part (SPEC, DS-13): a parent's state
   reaches a child only through a class or an aria attribute the builder sets
   on that child.
   ══════════════════════════════════════════════════════════════════════════ */

/* ── Memory kinds (DS-13) ─────────────────────────────────────────────────
   Skill, Fact and Event each have a colour, `--kind-skill`, `--kind-fact`
   and `--kind-event`, AA-corrected from Blank's and pickable by the viewer:
   setKindColour() writes the pick as an inline override of the token on the
   element that carries the theme. The kind is always written beside its
   colour (WCAG 1.4.1). `.ds-badge--kind` is the outlined small caps kind
   pill (worn with `.ds-badge .ds-badge--caps`), its `::before` the dot in
   the kind's colour under `.is-skill`, `.is-fact` or `.is-event`.
   `.ds-dot--skill`, `--fact` and `--event` paint a dot. `.ds-chip--pair` is
   a Type pill of two buttons: `.ds-chip--dot` (the kind's dot; it picks the
   colour, `.is-editing` while the swatches edit its kind) and
   `.ds-chip--word` (it filters); `.is-on` draws the pressed ink. Each dot
   that wears a kind's colour is edged as a swatch is (DS-15), so a white or
   a black pick shows on either theme. `*` does not match a pseudo-element,
   so the badge's `::before` declares its own border-box: the edge does not
   grow it. */
.ds-badge--kind {
  flex: none;
  max-width: 100%;
  gap: calc(var(--space-1) + var(--space-half));
  padding: var(--space-half) var(--space-2) var(--space-half) calc(var(--space-1) + var(--space-half));
  border: var(--rule-width) solid var(--hairline-strong);
  background: transparent;
  color: var(--text-muted);
  overflow-wrap: anywhere;
}
.ds-badge--kind::before {
  content: "";
  flex: none;
  width: var(--dot-size-sm);
  height: var(--dot-size-sm);
  border-radius: 50%;
  background: var(--text-muted);
  box-sizing: border-box;
  border: var(--rule-width) solid var(--hairline-strong);
}
.ds-badge--kind.is-skill::before { background: var(--kind-skill); }
.ds-badge--kind.is-fact::before { background: var(--kind-fact); }
.ds-badge--kind.is-event::before { background: var(--kind-event); }
.ds-dot--skill { background: var(--kind-skill); border: var(--rule-width) solid var(--hairline-strong); }
.ds-dot--fact { background: var(--kind-fact); border: var(--rule-width) solid var(--hairline-strong); }
.ds-dot--event { background: var(--kind-event); border: var(--rule-width) solid var(--hairline-strong); }
.ds-chip--pair { gap: 0; padding: 0; }
.ds-chip--pair.is-on { background: var(--text); color: var(--surface-ground-top); }
.ds-chip--dot {
  flex: none;
  width: var(--dot-size-lg);
  height: var(--dot-size-lg);
  margin-left: var(--space-3);
  border-radius: 50%;
  transition: box-shadow var(--dur-base) var(--ease-out), transform var(--dur-base) var(--ease-out);
  border: var(--rule-width) solid var(--hairline-strong);
}
.ds-chip--dot:hover { box-shadow: 0 0 0 3px var(--hairline-strong); transform: scale(1.15); }
.ds-chip--dot.is-editing { box-shadow: 0 0 0 2px var(--surface-canvas), 0 0 0 3.5px var(--text); }
.ds-chip--word {
  min-width: 0;
  padding: var(--space-1) var(--space-3) var(--space-1) var(--space-2);
  border-radius: var(--radius-pill);
  color: inherit;
  text-align: start;
  overflow-wrap: anywhere;
}
/* The swatches: a pill on the hover ground (`.ds-swatches`, as wide as
   what it holds) holding a
   wrapping `--row` of `.ds-swatch` buttons, each painted by the builder
   with `var(--swatch-…)` and edged with the strong hairline so white and
   black show on either theme; the chosen one (`[aria-pressed="true"]`)
   wears a ring, a hovered one grows. `.ds-swatch--custom` is the colour
   wheel, a <label> over an invisible native colour input
   (`.ds-swatch--input`); it rings while its input has keyboard focus, and
   `.is-on` while the pick is a custom colour. */
.ds-swatches {
  display: flex;
  align-items: center;
  width: fit-content;
  max-width: 100%;
  gap: var(--space-3);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-pill);
  background: var(--state-hover);
}
.ds-swatches--row { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.ds-swatch {
  flex: none;
  width: var(--swatch-size);
  height: var(--swatch-size);
  padding: 0;
  border: var(--rule-width) solid var(--hairline-strong);
  border-radius: 50%;
  transition: transform var(--dur-fast) var(--ease-out), box-shadow var(--dur-fast) var(--ease-out);
}
.ds-swatch:hover { transform: scale(1.15); }
.ds-swatch[aria-pressed="true"], .ds-swatch--custom.is-on { box-shadow: 0 0 0 2px var(--surface-canvas), 0 0 0 3.5px var(--text); }
.ds-swatch--custom { position: relative; display: inline-block; overflow: hidden; background: var(--swatch-wheel); cursor: pointer; }
.ds-swatch--custom:has(:focus-visible) { outline: var(--focus-ring); outline-offset: var(--focus-offset); }
.ds-swatch--input {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  padding: 0;
  border: 0;
  opacity: 0;
  cursor: pointer;
}

/* ── Memory card (DS-13) ──────────────────────────────────────────────────
   One memory, centred at most `--memcard-width`: its kind
   (`.ds-memcard--kind` on the kind badge; `.ds-memcard--back` holds the way
   back at the start, in a layer whose body centres), its `--title` (the body weight),
   its `--tags` (outlined `.ds-badge--outline` pills, closed by a short rule),
   its `--text`, then `--fields`: label and value rows (`--field`, its
   `--term` small caps, never shrinking, at most 40%; its `--value`
   breaking anywhere; `--id` in mono),
   then the Versions fold. `--empty` is the fold's line when there are
   none. memoryCard() builds it. */
.ds-memcard {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: var(--space-4);
  width: min(var(--memcard-width), 100%);
  margin: 0 auto;
}
.ds-memcard--kind { align-self: center; }
.ds-memcard--back { align-self: flex-start; justify-self: start; }
.ds-memcard--title {
  margin: 0;
  font-size: var(--text-lg);
  font-weight: var(--weight-body);
  line-height: var(--leading-tight);
  text-align: center;
  color: var(--text);
  overflow-wrap: anywhere;
}
.ds-memcard--tags { display: flex; flex-wrap: wrap; justify-content: center; gap: var(--space-2); }
.ds-memcard--tags::after {
  content: "";
  display: block;
  flex-basis: 100%;
  width: calc(var(--space-7) + var(--space-2));
  height: var(--rule-width);
  margin: var(--space-3) auto var(--space-half);
  background: var(--hairline);
}
.ds-memcard--text {
  margin: 0;
  font-size: var(--text-band);
  line-height: var(--leading-base);
  text-align: start;
  color: var(--text);
  overflow-wrap: anywhere;
}
.ds-memcard--fields {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  width: 100%;
  margin: calc(var(--space-1) + var(--space-half)) 0 0;
  text-align: start;
}
.ds-memcard--field { display: flex; justify-content: space-between; gap: var(--space-4); }
.ds-memcard--term {
  flex: none;
  max-width: 40%;
  font-size: var(--text-2xs);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--text-muted);
  overflow-wrap: anywhere;
}
.ds-memcard--value {
  min-width: 0;
  margin: 0;
  font-size: var(--text-sm);
  text-align: end;
  color: var(--text);
  overflow-wrap: anywhere;
}
.ds-memcard--id { font-family: var(--font-mono); font-size: var(--text-2xs); color: var(--text-muted); }
.ds-memcard--empty { margin: 0; font-size: var(--text-sm); color: var(--text-muted); overflow-wrap: anywhere; }
/* Outlined pills: a tag, the model that answered, a chapter (with
   `.ds-badge--caps`). `.ds-badge--reveal` rests hidden and shows on hover,
   on keyboard focus and with `.is-shown` (a chapter pill beside the time:
   the consumer adds the class while its message is hovered, and always on a
   transcript mark). */
.ds-badge--outline {
  max-width: 100%;
  padding: var(--space-half) calc(var(--space-1) + var(--space-half));
  border: var(--rule-width) solid var(--hairline);
  background: transparent;
  color: var(--text-muted);
  overflow-wrap: anywhere;
}
.ds-badge--reveal { opacity: 0; transition: opacity var(--dur-fast) var(--ease-out); }
.ds-badge--reveal:hover, .ds-badge--reveal:focus, .ds-badge--reveal:focus-visible, .ds-badge--reveal.is-shown { opacity: 1; }
/* The Versions fold: a <details> (`.ds-fold`) whose summary
   (`.ds-fold--summary`, small caps, muted) holds its `--count` in --text and
   ends in a plus that turns into an × under `.is-open` (the builder sets it
   from the details' open state); `--body` holds the versions. */
.ds-fold { width: 100%; }
.ds-fold--summary {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  list-style: none;
  font-size: var(--text-2xs);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--text-muted);
  cursor: pointer;
  overflow-wrap: anywhere;
}
.ds-fold--summary::-webkit-details-marker { display: none; }
.ds-fold--summary::after {
  content: "+";
  margin-left: auto;
  font-size: var(--text-md);
  line-height: 1;
  color: var(--text-muted);
  transition: transform var(--dur-slow) var(--ease-out), color var(--dur-base) var(--ease-out);
}
.ds-fold--summary.is-open::after { transform: rotate(45deg); color: var(--text); }
.ds-fold--count { flex: none; color: var(--text); }
.ds-fold--body { margin-top: var(--space-3); }
/* A version: `.ds-version--row` is a button (its `--title`, then its
   `--when`) that insets on the hover ground; one version on its own is a
   `--badge` (small caps), the memory card's title and its `--full` body on
   the hover ground, then Restore, an ink button (`.ds-btn--ink`, worn with
   `.ds-btn`: --text ground, the first stop's paper words; hovered, the
   muted ink). versionDetail() builds it. */
.ds-version--row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  width: 100%;
  margin-bottom: var(--space-half);
  padding: var(--space-3) 0;
  border-radius: var(--radius-xs);
  background: transparent;
  color: var(--text);
  text-align: start;
  transition: background-color var(--dur-base) var(--ease-out), padding var(--dur-base) var(--ease-out);
}
.ds-version--row:last-child { margin-bottom: 0; }
.ds-version--row:hover { padding-left: var(--space-3); padding-right: var(--space-3); background: var(--state-hover); }
.ds-version--title { min-width: 0; font-size: var(--text-sm); color: var(--text); overflow-wrap: anywhere; }
.ds-version--when { flex: none; font-size: var(--text-2xs); color: var(--text-muted); }
.ds-version--badge {
  align-self: center;
  font-size: var(--text-2xs);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  text-align: center;
  color: var(--text-muted);
  overflow-wrap: anywhere;
}
.ds-version--full {
  width: 100%;
  margin: var(--space-1) 0 0;
  padding: var(--space-4);
  border-radius: var(--radius-sm);
  background: var(--state-hover);
  font-family: inherit;
  font-size: var(--text-base);
  line-height: var(--leading-base);
  text-align: start;
  color: var(--text);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
.ds-btn--ink {
  align-self: center;
  max-width: 100%;
  margin-top: var(--space-2);
  padding: calc(var(--space-2) + var(--space-half)) var(--space-5);
  border-radius: var(--radius-pill);
  background: var(--text);
  color: var(--surface-ground-top);
  font-size: var(--text-xs);
  overflow-wrap: anywhere;
}
.ds-btn--ink:hover { background: var(--text-muted); }
/* The memory list: a group's head is a `.ds-label` with `--row` (small
   caps, its `.ds-label--count` faint at its end); each memory is a
   `.ds-memlist--row` (a button worn with `.ds-interactive
   .ds-interactive--row`): its kind badge, its `--main` column (its one-line
   `--text`), its `--side` column (its `--when` and `--subject`), stepping
   in on hover. `.ds-label--centre` is a drilled-in view's label,
   centred. */
.ds-label--row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  margin: 0;
  padding: var(--space-4) var(--space-1) var(--space-2);
  font-size: var(--text-xs);
}
.ds-label--row:first-child { padding-top: var(--space-1); }
.ds-label--count { flex: none; font-size: var(--text-2xs); color: var(--text-faint); }
.ds-label--centre { margin: var(--space-1) 0 var(--space-3); text-align: center; }
.ds-memlist--row {
  display: flex;
  align-items: flex-start;
  gap: calc(var(--space-2) + var(--space-half));
  width: 100%;
  padding: var(--space-3) var(--space-1);
  border-radius: calc(var(--radius-sm) + var(--space-half));
  text-align: start;
  transition: background-color var(--dur-base) var(--ease-out), padding var(--dur-base) var(--ease-out);
}
.ds-memlist--row:hover { padding-left: var(--space-3); padding-right: var(--space-3); }
/* Pressed, a memory row keeps the hover ground: the press ground
   (.ds-interactive:active, --state-press) puts the light kinds' dots under
   3:1 and the muted words under 4.5:1 (law 2; SPEC, DS-13). */
.ds-memlist--row:active { background: var(--state-hover); }
.ds-memlist--main { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: var(--space-1); }
.ds-memlist--text {
  font-size: var(--text-base);
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ds-memlist--side {
  flex: 0 0 auto;
  max-width: 40%;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: var(--space-1);
  text-align: end;
  overflow-wrap: anywhere;
}
.ds-memlist--when { font-size: var(--text-xs); color: var(--text-muted); overflow-wrap: anywhere; }
.ds-memlist--subject { font-size: var(--text-xs); color: var(--text-muted); }
/* DS-16 (1.14.0): what a memory row adds under its title, in its `--main`
   column: a search's `--snippet` (the words that matched) and its `--tags`
   (one line, joined by middle dots); both wrap and break a long unbroken
   token anywhere, so neither runs under the `--side` column.
   `.ds-memlist--forgotten` holds a forgotten memory's row and the button
   that restores it side by side. It fades nothing (law: do not fade text
   with opacity): its title steps down from `--text` to `--text-muted`, the
   ink its snippet, tags, time and subject already wear, every one at 4.5:1
   on each canvas stop and on the hover ground (`--text-faint` reads 4.12:1
   on the light hover over the low stop, so the meta does not step down);
   its Recover stays a full control. */
.ds-memlist--snippet { font-size: var(--text-xs); color: var(--text-muted); overflow-wrap: anywhere; }
.ds-memlist--tags { font-size: var(--text-xs); color: var(--text-muted); overflow-wrap: anywhere; }
.ds-memlist--forgotten { display: flex; align-items: center; gap: var(--space-2); }
.ds-memlist--forgotten .ds-memlist--text { color: var(--text-muted); }

/* ── Chat log (DS-13) ─────────────────────────────────────────────────────
   A conversation as Blank draws it: `.ds-chat--log` (worn with `.ds-chat`)
   spaces its messages apart. `.ds-chat--yours` is your message, at the end;
   `.ds-chat--answer` the agent's, at the start; each holds its words
   (`.ds-chat--line`; yours also `.ds-chat--soft`, on the hover ground) and
   its `.ds-chat--when` line (the time, the model and the chapter pills;
   `.is-end` lines them up at the end, under yours). `.ds-chat--divider` is a
   subject between two hairlines. `.ds-chat--memory` is a transcript mark, a
   button in a dashed box (its kind badge, its chapter pill shown, its
   `.ds-chat--memory-text` in --text) that opens the memory. */
.ds-chat--log { gap: calc(var(--space-7) + var(--space-1) + var(--space-half)); width: 100%; overflow: visible; }
.ds-chat--yours, .ds-chat--answer { display: flex; flex-direction: column; min-width: 0; max-width: var(--bubble-max); }
.ds-chat--yours { align-self: flex-end; align-items: flex-end; }
.ds-chat--answer { align-self: flex-start; align-items: flex-start; }
.ds-chat--line {
  margin: 0;
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-sm);
  font-size: var(--text-base);
  line-height: var(--leading-base);
  text-align: start;
  color: var(--text);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
.ds-chat--soft { background: var(--state-hover); color: var(--text); }
.ds-chat--when {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-1);
  padding: 0 var(--space-4);
  font-size: var(--text-2xs);
  color: var(--text-muted);
}
.ds-chat--when.is-end { justify-content: flex-end; }
.ds-chat--divider {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin: var(--space-2) 0;
  font-size: var(--text-2xs);
  letter-spacing: var(--tracking-band);
  text-transform: uppercase;
  color: var(--text-muted);
  overflow-wrap: anywhere;
}
.ds-chat--divider::before, .ds-chat--divider::after { content: ""; flex: 1; height: var(--rule-width); background: var(--hairline); }
.ds-chat--memory {
  align-self: center;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: calc(var(--space-1) + var(--space-half));
  max-width: var(--bubble-max);
  margin: calc(var(--space-5) + var(--space-half)) 0;
  padding: calc(var(--space-2) + var(--space-half)) var(--space-3);
  border: var(--rule-width) dashed var(--hairline-strong);
  border-radius: var(--radius-sm);
  background: transparent;
  font-size: var(--text-xs);
  text-align: center;
  color: var(--text-muted);
  overflow-wrap: anywhere;
}
.ds-chat--memory-text { color: var(--text); }

/* ── Events timeline (DS-13) ──────────────────────────────────────────────
   Moments on one horizontal line that scrolls sideways (`.ds-timeline`, its
   line a `::before` hairline): each `.ds-timeline--row` is
   `--timeline-row-width` wide, its `--time`, its `--dot` on the line and its
   `--body` (its `--text`): yours under the line, the agent's over it
   (`.is-agent` reverses the column, so the DOM and focus order stay time,
   dot, body). A memory is a larger ringed dot (`.ds-timeline--dot.is-mark`)
   and a body that is a button (`.ds-timeline--body.is-mark`, the hover
   ground on hover) holding its kind badge and its words in --text at the
   normal weight (`.ds-timeline--text.is-mark`); `.is-you` is your words in
   --text, `.is-agent` the agent's muted. A subject is a pill on the line
   (`.ds-timeline--subject`, a button on the first stop's ground, at most a
   moment wide: a longer one wraps). */
.ds-timeline {
  position: relative;
  display: flex;
  align-items: center;
  width: 100%;
  padding: calc(var(--space-7) + var(--space-2)) var(--space-6);
  overflow-x: auto;
  overflow-y: hidden;
  scroll-snap-type: x proximity;
}
.ds-timeline::before { content: ""; position: absolute; left: 0; right: 0; top: 50%; height: var(--rule-width); background: var(--hairline); }
.ds-timeline--row {
  position: relative;
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  width: var(--timeline-row-width);
  scroll-snap-align: center;
}
.ds-timeline--row.is-agent { flex-direction: column-reverse; }
.ds-timeline--time { margin: calc(var(--space-1) + var(--space-half)) 0; font-size: var(--text-2xs); color: var(--text-muted); }
.ds-timeline--dot {
  position: relative;
  z-index: 1;
  flex: none;
  width: var(--dot-size-lg);
  height: var(--dot-size-lg);
  border-radius: 50%;
  background: var(--text-muted);
}
.ds-timeline--dot.is-mark {
  width: calc(var(--dot-size-lg) + var(--space-1));
  height: calc(var(--dot-size-lg) + var(--space-1));
  background: var(--text);
  box-shadow: 0 0 0 3px var(--surface-ground-top), 0 0 0 4px var(--hairline);
}
.ds-timeline--body {
  max-width: 100%;
  max-height: calc(var(--space-6) * 4);
  padding: var(--space-2) calc(var(--space-2) + var(--space-half));
  overflow: hidden;
  font-size: var(--text-sm);
  line-height: var(--leading-tight);
  text-align: center;
  overflow-wrap: anywhere;
}
.ds-timeline--body.is-mark {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: calc(var(--space-1) + var(--space-half));
  border-radius: calc(var(--radius-sm) + var(--space-half));
  background: transparent;
  color: var(--text);
  transition: background-color var(--dur-base) var(--ease-out);
}
.ds-timeline--body.is-mark:hover { background: var(--state-hover); }
.ds-timeline--text.is-you { color: var(--text); }
.ds-timeline--text.is-agent { color: var(--text-muted); }
.ds-timeline--text.is-mark { color: var(--text); font-weight: var(--weight-normal); }
.ds-timeline--subject {
  position: relative;
  z-index: 1;
  flex: 0 0 auto;
  align-self: center;
  max-width: var(--timeline-row-width);
  margin: 0 calc(var(--space-4) + var(--space-half));
  padding: var(--space-1) var(--space-3);
  border: var(--rule-width) solid var(--hairline);
  border-radius: var(--radius-pill);
  background: var(--surface-ground-top);
  font-size: var(--text-2xs);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--text-muted);
  overflow-wrap: anywhere;
}

/* ── Memory graph, tooltips and the artifact preview (DS-13) ──────────────
   The 3D graph is the panel's (three.js); the system ships its tokens
   (`--graph-*`, read at runtime with readColour() and readNumber() on the
   graph's own element, so a themed stage paints its own) and the room it
   draws in: `.ds-graph` fills its stage, its `--canvas` fills it, `--stage`
   centres what sits over it. `.ds-tooltip--soft` (worn with `.ds-tooltip`)
   is the hovered memory's words on the hover ground, beside its point;
   `.ds-tooltip--label` a new memory's name beside it, no ground. Both are
   shown with `.is-open`. `.ds-preview` is an artifact's preview: a glow of
   ink on the hover ground (`.is-screen`: a glow of the event colour), its
   initial (`.ds-preview--mark`) faint and large. */
.ds-graph { position: absolute; inset: 0; }
.ds-graph--canvas { position: absolute; inset: 0; z-index: 1; display: block; width: 100%; height: 100%; touch-action: none; }
.ds-graph--stage {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-6);
}
.ds-tooltip--soft {
  position: absolute;
  max-width: var(--tooltip-max);
  padding: calc(var(--space-1) + var(--space-half)) calc(var(--space-2) + var(--space-half));
  border-radius: var(--radius-sm);
  background: var(--state-hover);
  color: var(--text);
  font-size: var(--text-sm);
  overflow: hidden;
  text-overflow: ellipsis;
  transform: translate(var(--space-3), -50%);
}
.ds-tooltip--label {
  position: absolute;
  max-width: var(--tooltip-max);
  padding: 0;
  background: transparent;
  color: var(--text);
  font-size: var(--text-xs);
  overflow: hidden;
  text-overflow: ellipsis;
  transform: translate(calc(var(--space-3) + var(--space-half)), -50%);
}
.ds-preview {
  position: relative;
  display: grid;
  place-items: center;
  aspect-ratio: 16 / 10;
  border-radius: var(--radius-lg);
  overflow: hidden;
  background: radial-gradient(120% 90% at 20% 0%, color-mix(in srgb, var(--text) 10%, transparent), transparent 60%), var(--state-hover);
}
.ds-preview.is-screen { background: radial-gradient(90% 80% at 50% 40%, color-mix(in srgb, var(--kind-event) 22%, transparent), transparent 70%), var(--state-hover); }
.ds-preview--mark {
  font-size: var(--preview-mark-size);
  font-weight: var(--weight-display);
  color: var(--text-faint);
}
