/* ============================================================
 * TIDE v0.1 — 29Court design system · COMPONENTS (CSS)   DRAFT
 * Needs tokens.css first. Plain CSS, @layer, no build step.
 *
 * Original 29Court code, except the parts marked [MIT-PORT],
 * whose behaviour/geometry is ported from Bencho free blocks
 * (slide-confirm, toast-stack, time-scrubber, one-time-code,
 * checklist, dynamic-island, slosh):
 *   MIT License — Copyright (c) 2026 Lorenzo Cabra
 *   Permission is hereby granted, free of charge, to any person
 *   obtaining a copy of this software ... (full text in
 *   THIRD-PARTY.md, which ships with this file).
 * Pro-licensed patterns (swipe deck, inline confirm, feedback bar,
 * pay button, access request, activity stack, AI chat input,
 * copy field, thinking, skeleton morph, shimmer) are clean-room
 * rebuilds from behaviour specs — no Bencho Pro code.
 * ============================================================ */

@layer c29.reset {
  *, *::before, *::after { box-sizing: border-box; }
  html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }
  body { margin: 0; }
  /* Axis lock (Rev 1, 2026-10-17): the page never scrolls sideways.
     Gesture components may paint past the viewport mid-flight; that
     travel must never become page scroll. clip (not hidden), so
     position:sticky keeps working; hidden is the legacy fallback. */
  html, body { overflow-x: hidden; overflow-x: clip; }
  button, input, textarea, select { font: inherit; color: inherit; }
  img, svg { display: block; max-width: 100%; }
  dialog { padding: 0; border: 0; color: inherit; }
  [hidden] { display: none !important; }
}

/* Derived tokens are re-declared on every component root so a knob
   set on the component (or any ancestor) recomputes them there. */
@layer c29.tokens {
  :root, .tc, [data-tune] {
    --d1: calc(160ms / var(--speed)); --d2: calc(220ms / var(--speed));
    --d3: calc(320ms / var(--speed)); --d4: calc(420ms / var(--speed)); --d5: calc(480ms / var(--speed));
    --ease-give: cubic-bezier(.24, calc(1 + .5 * var(--bounce)), .38, 1);
    --s1:calc(4px * var(--density)); --s2:calc(8px * var(--density)); --s3:calc(12px * var(--density));
    --s4:calc(16px * var(--density)); --s5:calc(20px * var(--density)); --s6:calc(24px * var(--density));
    --s8:calc(32px * var(--density)); --s12:calc(48px * var(--density));
    --accent-soft: color-mix(in srgb, var(--accent) 14%, var(--surface));
  }
}

@layer c29.base {
  html { background: var(--bg); color: var(--ink); font-family: var(--font-ui);
    font-size: var(--fs-body); line-height: var(--lh-body); -webkit-font-smoothing: antialiased; }
  body { background: var(--bg); position: relative; min-height: 100dvh; }
  h1, h2, h3 { font-family: var(--font-display); letter-spacing: var(--track-head); line-height: 1.15; margin: 0; text-wrap: balance; font-weight: 750; }
  p { margin: 0; text-wrap: pretty; }
  a { color: var(--accent-text); }
  :focus-visible { outline: none; box-shadow: var(--focus); border-radius: 8px; }
  /* FOCUS LAW (2026-10-19, Principal field defect): the ring belongs to
     INTERACTIVE elements only. Programmatic focus targets — dialogs,
     sheet/dialog containers, headings, landmarks and other tabindex="-1"
     waypoints that exist so focus has somewhere accessibility-correct to
     LAND — never paint a ring: focus there is an announcement, not a
     selection. (sheet() focuses its <h2> on open; the unscoped rule above
     drew the sea-green box around the title on iOS. The 2026-10-17 grab-only
     suppression below is this law's first, too-narrow instance.)
     Real controls are excluded from the tabindex clause by element/role:
     a roving-tabindex control (c29-tabs) moves tabindex to 0 before focus
     lands, so no keyboard user loses a cue. */
  :is(dialog, [role="dialog"], [role="alertdialog"]):focus-visible,
  :is(h1, h2, h3, h4, h5, h6):focus-visible,
  [tabindex="-1"]:not(button, a, input, select, textarea, summary,
    [role="button"], [role="tab"], [role="menuitem"], [role="menuitemcheckbox"],
    [role="menuitemradio"], [role="option"], [role="link"], [role="checkbox"],
    [role="radio"], [role="switch"], [role="slider"], [role="spinbutton"],
    [role="combobox"], [role="listbox"], [role="textbox"],
    [contenteditable]):focus-visible { outline: none; box-shadow: none; }
  /* THEME SWAP LAW (2026-10-19): while <html> wears .c29-swapping (set by
     theme.set() in tide.js for the swap frames), nothing transitions — a
     theme change is an instant token change, never 60 staggered colour
     animations under a view-transition snapshot (the reported toggle
     jitter). Component-owned deliberate transitions resume untouched the
     moment the class lifts. */
  html.c29-swapping *, html.c29-swapping *::before, html.c29-swapping *::after { transition: none !important; }
  /* THEME TRANSITION (2026-10-20, Principal's order — supersedes the
     still swap for PAINT ONLY; the 2026-10-19 GEOMETRY LAW still binds:
     nothing may move, resize, or reflow across a theme change, and the
     theme-stability suite must still pass). While <html> wears
     .c29-theme-anim (theme.set() in tide.js, full-motion path only —
     reduced motion keeps the instant .c29-swapping path), every element
     crossfades its themed PAINT together on ONE duration and ONE ease,
     so no surface snaps early or lags late (the jank the Principal
     screenshotted: each component's own d1–d5 firing unsynced). Only
     paint properties are listed — never `all`, never a layout or
     transform property — so geometry cannot ride the fade. Foreground
     colours run a hair shorter than backgrounds so text never passes
     through an unreadable mid-state. Card surfaces trail by 20ms, the
     one joyful stagger (≤30ms per the order). The theme toggle's own
     icons are excluded from the paint list: they keep their morph
     (rotate + bloom) on --d4/--ease-give. The class is only ever added
     by an explicit set()/toggle — never at init — so first paint never
     transitions. */
  html.c29-theme-anim *, html.c29-theme-anim *::before, html.c29-theme-anim *::after {
    transition-property: background-color, border-color, outline-color, box-shadow, color, fill, stroke, caret-color, text-decoration-color !important;
    transition-duration: var(--d-theme), var(--d-theme), var(--d-theme), var(--d-theme), calc(var(--d-theme) - 70ms), calc(var(--d-theme) - 70ms), calc(var(--d-theme) - 70ms), calc(var(--d-theme) - 70ms), calc(var(--d-theme) - 70ms) !important;
    transition-timing-function: var(--ease) !important;
    transition-delay: 0ms !important;
  }
  html.c29-theme-anim .t-card, html.c29-theme-anim c29-deck .deck-card { transition-delay: 20ms !important; }
  html.c29-theme-anim .t-theme .sun, html.c29-theme-anim .t-theme .moon {
    transition-property: transform, opacity !important;
    transition-duration: var(--d4) !important;
    transition-timing-function: var(--ease-give) !important;
    transition-delay: 0ms !important;
  }
  ::selection { background: color-mix(in srgb, var(--accent) 26%, transparent); }
  .tnum { font-variant-numeric: tabular-nums; }
  @view-transition { navigation: auto; }
}

@layer c29.components {

/* ---------- page frame ---------- */
.t-page { max-width: 560px; margin: 0 auto; padding: calc(var(--s4) + var(--safe-t)) var(--s4) calc(var(--s12) + var(--safe-b)); }
.t-sky { position: fixed; inset: 0; background: var(--glow); pointer-events: none; z-index: 0; }
.t-page > * { position: relative; z-index: 1; }
.t-cap { font-size: var(--fs-cap); letter-spacing: var(--track-cap); text-transform: uppercase; font-weight: 650; color: var(--ink-mute); }
.t-muted { color: var(--ink-mute); }
.t-small { font-size: var(--fs-sm); }
.t-stack { display: grid; gap: var(--s3); grid-template-columns: minmax(0, 1fr); }
.t-row { display: flex; align-items: center; gap: var(--s2); flex-wrap: wrap; }
.t-spacer { flex: 1; }
.t-sim { display: inline-flex; align-items: center; gap: 4px; font: 650 11px/1 var(--font-ui); letter-spacing: .08em;
  text-transform: uppercase; color: var(--warn-text); background: var(--warn-bg); padding: 4px 7px; border-radius: 99px; white-space: nowrap; }

/* ---------- DESKTOP POSTURE (2026-10-20, the Principal's order) ----------
   "It looks like I'm on a phone. It's too skinny. If it's on desktop,
   it should be desktop." The default .t-page frame above is a PHONE
   frame (560px) — correct for focused surfaces, wrong for working
   ones. The posture contract:
   - WORKING surfaces (the Desk, dashboards, boards, the showcase,
     catalogs — anywhere the Principal works across many things at
     once) declare `.t-page.is-desk`. Below 720px it is byte-identical
     to .t-page; at 720px the canvas opens to 1120px; at 1080px to
     1480px with desktop gutters. The cap never lifts: past 1480px the
     margins grow, the canvas does not — width is spent on columns,
     never on line length.
   - FOCUSED surfaces stay narrow BY DESIGN (the default .t-page):
     gates, single-task tools, checkout-class flows, phone-app
     frames, one-thing-at-a-time forms. Narrow there is the feature.
   - PROSE caps with .t-measure (68ch) wherever text is the content,
     at any canvas width. Grids carry width; paragraphs never do.
   - COMPOSITION comes from canon, never per-page invention:
     .t-cols is the working grid (1 col → 2 @720 → 3 @1080, .is-4
     adds a 4th @1440); .t-split is master–detail (stacked → rail +
     detail @1080, rail sticky; .is-flip mirrors it).
   Canon breakpoints are 720px and 1080px throughout the system —
   new rules use those two numbers, no others.
   A working surface that renders phone-width on a desktop viewport
   is a canon violation. Enforced by posture-pass.mjs in
   tide-showcase (phone boxes box-identical to the pre-change
   baseline; desktop regions side by side; no h-scroll at
   390/768/1024/1440/1920). Record: brains projects/tide/
   DESKTOP-POSTURE-2026-10-20.md. */
@media (min-width: 720px) {
  .t-page.is-desk { max-width: 1120px; padding-inline: var(--s5); }
}
@media (min-width: 1080px) {
  .t-page.is-desk { max-width: 1480px; padding-inline: var(--s6); }
}
.t-measure { max-width: 68ch; }
.t-cols { display: grid; gap: var(--cols-gap, var(--s4)); grid-template-columns: minmax(0, 1fr); }
@media (min-width: 720px) { .t-cols { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 1080px) { .t-cols { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (min-width: 1440px) { .t-cols.is-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
.t-split { display: grid; gap: var(--s5); grid-template-columns: minmax(0, 1fr); align-items: start; }
@media (min-width: 1080px) {
  .t-split { grid-template-columns: minmax(280px, 380px) minmax(0, 1fr); }
  .t-split.is-flip { grid-template-columns: minmax(0, 1fr) minmax(280px, 380px); }
  .t-split > .t-split-rail { position: sticky; top: var(--s4); }
}

/* ---------- card / pane ---------- */
.t-card { background: var(--surface); border-radius: var(--corner); box-shadow: var(--hair); padding: var(--s5); }
.t-pane { background: var(--pane); backdrop-filter: blur(var(--pane-blur)) saturate(1.2); -webkit-backdrop-filter: blur(var(--pane-blur)) saturate(1.2);
  border-radius: var(--corner); box-shadow: 0 0 0 1px var(--pane-edge), var(--lift); background-image: var(--sheen); }

/* ---------- buttons ---------- */
.t-btn { --b-bg: var(--surface); --b-ink: var(--ink); --b-edge: var(--edge);
  display: inline-flex; align-items: center; justify-content: center; gap: 8px; min-height: var(--tap); min-width: var(--tap);
  padding: 0 calc(var(--s4) + 2px); border: 0; border-radius: var(--r-pill); background: var(--b-bg); color: var(--b-ink);
  box-shadow: 0 0 0 1px var(--b-edge); font-weight: 600; font-size: 15px; cursor: pointer; text-decoration: none;
  transition: transform var(--d1) var(--ease), background var(--d2) var(--ease), box-shadow var(--d2) var(--ease);
  -webkit-tap-highlight-color: transparent; touch-action: manipulation; white-space: nowrap; }
.t-btn:hover { background: color-mix(in srgb, var(--b-bg) 92%, var(--ink)); }
.t-btn:active { transform: scale(.97); }
.t-btn[disabled] { opacity: .45; pointer-events: none; }
.t-btn.is-accent { --b-bg: var(--accent); --b-ink: var(--on-accent); --b-edge: transparent; }
.t-btn.is-money { --b-bg: var(--money); --b-ink: var(--on-money); --b-edge: transparent; }
.t-btn.is-ghost { --b-bg: transparent; --b-edge: transparent; }
.t-btn.is-quiet { --b-bg: var(--wash); --b-edge: transparent; }
.t-btn.is-sm { min-height: 36px; padding: 0 14px; font-size: 14px; }
.t-btn.is-icon { padding: 0; width: var(--tap); }
.t-btn svg { width: 18px; height: 18px; flex: none; }
.t-chip { display: inline-flex; align-items: center; gap: 6px; min-height: 36px; padding: 0 14px; border-radius: 99px; border: 0;
  background: var(--wash); color: var(--ink-soft); font-size: 14px; font-weight: 550; cursor: pointer; transition: background var(--d2) var(--ease), color var(--d2) var(--ease), transform var(--d1) var(--ease); }
.t-chip[aria-pressed="true"], .t-chip.is-on { background: var(--accent); color: var(--on-accent); }
.t-chip:active { transform: scale(.96); }
.t-icon { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }

/* ---------- inputs ---------- */
.t-field { display: grid; gap: 6px; }
.t-field label { font-size: 13px; font-weight: 600; color: var(--ink-soft); }
.t-input { width: 100%; min-height: 48px; padding: 0 16px; border-radius: var(--r-md); border: 0; background: var(--raised);
  box-shadow: 0 0 0 1px var(--edge); font-size: 17px; color: var(--ink); transition: box-shadow var(--d2) var(--ease); }
.t-input:focus { outline: none; box-shadow: 0 0 0 1.5px var(--accent), 0 0 0 5px color-mix(in srgb, var(--accent) 16%, transparent); }
.t-input.is-bad { box-shadow: 0 0 0 1.5px var(--bad); animation: t-shake var(--d4) var(--ease); }
/* Rev 5 (2026-10-18): plain .t-input placeholders had no themed colour and
   fell back to UA grey at 3.10:1 on the dark raised surface. --ink-mute is
   theme-aware: 5.64:1 on light raised, 4.68:1 on dark raised. opacity:1
   stops engines that dim ::placeholder by default from undoing it. */
.t-input::placeholder { color: var(--ink-mute); opacity: 1; }
/* Rev 7 (2026-10-18): the dark placeholder was the system's weak spot —
   4.68:1 on the dark raised surface cleared the bar but sat under the
   4.7 aim every other Rev 5 fix met. Pinned one step lighter in dark
   only (#9AABB3, ≈6.0:1 on dark raised, measured in the Rev 7 render
   pass), the .pg-pct pin pattern; opacity:1 rides along so engines
   that dim ::placeholder cannot undo it on the composer either.
   Light is untouched: 5.64:1 already clears the aim. */
[data-theme="dark"] .t-input::placeholder,
[data-theme="dark"] c29-composer textarea::placeholder { color: #9AABB3; opacity: 1; }
@keyframes t-shake { 20%{translate:-6px} 45%{translate:5px} 70%{translate:-3px} 100%{translate:0} }
.t-unfold { display: grid; grid-template-rows: 0fr; transition: grid-template-rows var(--d4) var(--ease); }
.t-unfold > * { overflow: hidden; }
.t-unfold.is-open { grid-template-rows: 1fr; }

/* ---------- segmented / switch ---------- */
.t-seg { display: inline-grid; grid-auto-flow: row; grid-template-columns: repeat(auto-fit, minmax(88px, 1fr)); padding: 3px; border-radius: 99px; background: var(--wash); position: relative; isolation: isolate; max-width: 100%; } /* P6 (night loop 2026-10-25): was grid-auto-flow:column with no max-width — a seg with many/long options sized to content (Tone Tool family seg blew the panel to scrollW 495 vs clientW 352; Logos theme seg escaped to x377; all-places sweep, both engines). Capping alone crushed the 7-option Logos seg to 32px-wide buttons, so the grid now wraps: options flow into rows of >=56px columns instead of overflowing or crushing. The thumb is row-aware as of the same round (tide.js thumb()). */ /* P10 (night loop 2026-10-25): floor 56->88px — P6's ellipsis trade ellipsized real labels at phone width (segtext census, both engines): the filing seg in every fileable room rendered Work/Media/Tools at 60px columns vs 63-66px of text ("W…/Me…/To…", photographed), and in-card name segs crushed to 56px columns vs 58-140px of text (Tones families, Logos themes, Toast durations). At an 88px floor the grid wraps before it ellipsizes: filing wraps 3+2 at ~100px, name segs wrap 2-per-row at ~112px. The two longest Tones names (137-140px) still ellipsize at 112px — recorded in ROUND-10-PLEASANT, a label-length remainder, not a layout escape. */
.t-seg button { min-height: 40px; /* P3 (night loop 2026-10-24): was 38px — rendered at 38px in every room carrying a c29-seg (Desk/Learn/Notebook "Filed under" + theme segs, Round 3 rendered probe, both engines), under the house 40px control floor P1/P2 enforced elsewhere; fixed in the component so every consumer inherits it */ padding: 0 14px; border: 0; background: none; border-radius: 99px; font-size: 14px; font-weight: 600; color: var(--ink-mute); cursor: pointer; transition: color var(--d2) var(--ease); white-space: nowrap; min-width: 0; overflow: hidden; text-overflow: ellipsis; } /* P6: with the container capped, buttons shrink to their 1fr column and long labels ellipsis instead of spilling */
.t-seg.seg-wide { grid-template-columns: repeat(auto-fit, minmax(144px, 1fr)); } /* P11 (night loop 2026-10-25): the name-variant P10 recorded as option (a) — a seg whose options are NAMES (the Tone Tool's families: 95-140px of text) opts in with the seg-wide class and its columns floor at 144px, so the grid wraps 2-per-row at phone width instead of ellipsizing the names at the 88px floor. 144 is measured, not rounded: P10's suggested ~132px floor still ellipsized the two long names in Chromium (text 137/140px vs 132px columns; WebKit passed only because its scrollWidth under-reports under ellipsis). At 144 the Tones seg renders 294px wide, 2x2, every label whole, thumb exact on the selected button, both engines (tones-p11 experiment rig, pre-land). Short-label segs are untouched — the variant is opt-in per seg, never global. */
.t-seg button[aria-pressed="true"] { color: var(--ink); }
.t-seg .t-seg-thumb { position: absolute; z-index: -1; top: 3px; bottom: 3px; border-radius: 99px; background: var(--raised); box-shadow: var(--lift);
  transition: left var(--d3) var(--ease-give), width var(--d3) var(--ease-give); }
.t-switch { appearance: none; width: 50px; height: 30px; border-radius: 99px; background: var(--wash-2); position: relative; cursor: pointer; flex: none; margin: 7px 0;
  transition: background var(--d2) var(--ease); }
.t-switch::after { content: ""; position: absolute; top: 3px; left: 3px; width: 24px; height: 24px; border-radius: 50%; background: #fff;
  box-shadow: 0 2px 6px rgba(0,0,0,.18); transition: translate var(--d3) var(--ease-give); }
.t-switch:checked { background: var(--accent); }
.t-switch:checked::after { translate: 20px 0; }

/* ---------- theme toggle ---------- */
.t-theme { position: relative; overflow: hidden; }
.t-theme .sun, .t-theme .moon { position: absolute; transition: transform var(--d4) var(--ease-give), opacity var(--d2) var(--ease); }
[data-theme="dark"] .t-theme .sun { transform: translateY(26px) rotate(70deg) scale(.45); opacity: 0; }
:root:not([data-theme="dark"]) .t-theme .moon { transform: translateY(-26px) rotate(-50deg) scale(.45); opacity: 0; }

/* ======================================================
   SWIPE DECK  <c29-deck>   (clean-room, spec: swipe-deck)
   ====================================================== */
c29-deck { display: block; position: relative; --deck-h: 360px; --fan: 1; outline: none; --deck-on-sun: #3b2a12; }
/* on-sun word colour (deck-pass, measured): light #3b2a12 on sun-300 is
   8.85:1; on the dark theme's deeper sun (#C08442) it drops to 4.34:1,
   so the night beach deepens the word to the same brown family (5.8:1). */
[data-theme="dark"] c29-deck { --deck-on-sun: #1d1406; }
c29-deck .deck-stage { position: relative; height: var(--deck-h); touch-action: none; user-select: none; -webkit-user-select: none; }
c29-deck .deck-card { position: absolute; inset: 0; border-radius: var(--corner); background: var(--sheen), var(--raised); box-shadow: var(--lift);
  padding: var(--s5); display: flex; flex-direction: column; gap: var(--s3); overflow: hidden; will-change: transform;
  transition: transform var(--d5) var(--ease-give), opacity var(--d3) var(--ease), box-shadow var(--d3) var(--ease); cursor: grab; }
c29-deck .deck-card.is-held { transition: none; cursor: grabbing; box-shadow: var(--lift-hi); }
/* while a drag runs, the whole stack is JS-posed every move (the
   anticipation rise) — CSS transitions would lag the hand. */
c29-deck .deck-stage.is-dragging .deck-card { transition: none; }
c29-deck .deck-card.is-gone { pointer-events: none; }
c29-deck .deck-card:not(.is-top) { pointer-events: none; }
c29-deck .deck-card:not(.is-top) > :not(.deck-tint) { opacity: .0; transition: opacity var(--d3) var(--ease); }
c29-deck .deck-card.is-top > * { opacity: 1; }
c29-deck .deck-tint { position: absolute; inset: 0; pointer-events: none; opacity: 0; border-radius: inherit; }
/* The ONE indicator: a full-stage layer clipped (per move, in JS) to
   exactly the strip the dragged card has vacated on the trailing edge —
   it never overlays card content. Its colour is NOT in this stylesheet:
   the gesture machine paints background-color + the word's colour
   inline on every move from the drag scalar p (soft suggestion tint →
   fully saturated at p = 1, melting across hues on a sector change).
   Any CSS colour or transition here would reintroduce the snap the
   Slide-Cards campaign exists to kill. */
c29-deck .deck-reveal { position: absolute; inset: 0; z-index: 60; pointer-events: none; opacity: 0; clip-path: inset(0 100% 0 0); }
c29-deck .deck-reveal.is-on { opacity: 1; }
/* the tick, made physical: a colour disc + its soft paint bulge riding
   the flood's leading edge at the pointer (Gate 1). Position, colour
   and scale are painted inline per move from the displayed colour;
   only size/shape live here. They exist only while a gesture is live
   (opacity 0 at rest) and never take pointer events. */
c29-deck .deck-disc { position: absolute; left: 0; top: 0; z-index: 62; width: 44px; height: 44px; border-radius: 50%;
  display: grid; place-items: center; pointer-events: none; opacity: 0; will-change: transform, opacity;
  box-shadow: 0 4px 14px -4px rgb(0 0 0 / .35); }
c29-deck .deck-disc .t-icon { width: 22px; height: 22px; }
c29-deck .deck-bulge { position: absolute; left: 0; top: 0; z-index: 59; width: 120px; height: 200px; border-radius: 50%;
  pointer-events: none; opacity: 0; will-change: transform, opacity; }
c29-deck .deck-reveal-in { position: absolute; display: flex; align-items: center; gap: 6px; color: var(--ink); }
c29-deck .deck-reveal[data-dir="right"] .deck-reveal-in { left: 16px; top: 50%; translate: 0 -50%; flex-direction: column; text-align: center; }
c29-deck .deck-reveal[data-dir="left"] .deck-reveal-in { right: 16px; top: 50%; translate: 0 -50%; flex-direction: column; text-align: center; }
c29-deck .deck-reveal[data-dir="down"] .deck-reveal-in { top: 14px; left: 50%; translate: -50% 0; }
c29-deck .deck-reveal[data-dir="up"] .deck-reveal-in { bottom: 14px; left: 50%; translate: -50% 0; }
c29-deck .deck-reveal-ic { display: grid; place-items: center; will-change: transform; }
c29-deck .deck-reveal-ic .t-icon { width: 26px; height: 26px; }
c29-deck .deck-reveal-word { font: 800 15px/1 var(--font-display); letter-spacing: .12em; text-transform: uppercase; white-space: nowrap; }
c29-deck .deck-reveal-sub { font-size: 11px; font-weight: 650; letter-spacing: .04em; opacity: .75; white-space: nowrap; }
c29-deck .deck-empty { position: absolute; inset: 0; display: grid; place-items: center; text-align: center; padding: var(--s6);
  border-radius: var(--corner); box-shadow: inset 0 0 0 1.5px var(--edge); border: 0; color: var(--ink-mute); opacity: 0; transition: opacity var(--d4) var(--ease) var(--d2); }
c29-deck[data-empty] .deck-empty { opacity: 1; }
c29-deck .deck-bar { display: grid; grid-template-columns: repeat(var(--deck-actions, 4), 1fr); gap: var(--s2); margin-top: calc(var(--s4) + 28px); }
c29-deck .deck-bar button { min-height: 56px; border: 0; border-radius: var(--r-md); background: var(--surface); box-shadow: var(--hair); cursor: pointer;
  display: grid; place-items: center; gap: 2px; padding: 6px 2px; font-size: 12px; font-weight: 600; color: var(--ink-soft);
  transition: transform var(--d1) var(--ease), background var(--d2) var(--ease); }
c29-deck .deck-bar button:active { transform: scale(.95); }
c29-deck .deck-bar button[data-primary="true"] { background: var(--act-c, var(--accent)); color: var(--act-on, var(--on-accent)); box-shadow: none; }
c29-deck .deck-bar .t-icon { width: 20px; height: 20px; }
/* the direction chip: a little badge riding the button's icon, tinted
   with the action's own colour — the button row teaches the gesture.
   Decorative (aria-hidden in the markup); the direction is named in
   the button's accessible label. */
c29-deck .deck-btn-ic { position: relative; display: grid; place-items: center; }
c29-deck .deck-arrow { position: absolute; top: -8px; right: -13px; min-width: 17px; height: 17px; padding: 0 3px; border-radius: 99px;
  background: color-mix(in srgb, var(--act-c, var(--ink)) 18%, var(--surface)); color: color-mix(in srgb, var(--act-c, var(--ink)) 72%, var(--ink));
  font-size: 11px; font-weight: 800; line-height: 17px; text-align: center; box-shadow: var(--hair); pointer-events: none; }
c29-deck .deck-bar button[data-primary="true"] .deck-arrow { background: color-mix(in srgb, var(--act-on, var(--on-accent)) 22%, transparent); color: var(--act-on, var(--on-accent)); box-shadow: none; }
c29-deck .deck-hint { margin-top: var(--s2); text-align: center; font-size: 12px; color: var(--ink-mute); }
c29-deck .deck-count { position: absolute; top: -12px; right: 12px; z-index: 50; }

/* item anatomy (used in deck + inbox) */
.t-item-kind { display: flex; align-items: center; gap: 8px; font-size: 12px; font-weight: 650; letter-spacing: .05em; text-transform: uppercase; color: var(--ink-mute); }
.t-item-kind i { width: 8px; height: 8px; border-radius: 50%; background: var(--accent); }
.t-item-kind i.money { background: var(--money); } .t-item-kind i.warn { background: var(--sun-500); }
.t-item-title { font: 750 var(--fs-xl)/1.2 var(--font-display); letter-spacing: var(--track-head); }
.t-item-body { color: var(--ink-soft); font-size: 15px; }
.t-item-foot { margin-top: auto; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; font-size: 13px; color: var(--ink-mute); }
.t-amount { font: 750 var(--fs-2xl)/1 var(--font-display); font-variant-numeric: tabular-nums; letter-spacing: -.02em; }
.t-tag { white-space: nowrap; display: inline-flex; align-items: center; min-height: 24px; padding: 0 9px; border-radius: 99px; background: var(--wash); font-size: 12px; font-weight: 600; color: var(--ink-soft); }
.t-tag.is-accent { background: var(--accent-soft); color: var(--accent-text); }
.t-tag.is-money { background: color-mix(in srgb, var(--money) 14%, var(--surface)); color: var(--money-text); }
/* status tones (2026-10-19, Desk composition rebuild) — the designed
   ok / warn / bad token pairs; text rides on its paired soft ground */
.t-tag.is-ok { background: var(--ok-bg); color: var(--ok-text); }
.t-tag.is-warn { background: var(--warn-bg); color: var(--warn-text); }
.t-tag.is-bad { background: var(--bad-bg); color: var(--bad-text); }

/* ======================================================
   SLIDE TO CONFIRM  <c29-slide>   [MIT-PORT slide-confirm]
   The handle follows the finger 1:1; only the end of the
   track commits; on commit the handle unfurls leftward and
   fills the track (right edge pinned), arrow becomes check.
   ====================================================== */
c29-slide { --h: 60px; --pad: 4px; display: block; }
c29-slide .sl-track { position: relative; height: var(--h); border-radius: min(var(--corner), calc(var(--h) / 2)); background: var(--wash);
  box-shadow: inset 0 0 0 1px var(--edge-soft); overflow: hidden; touch-action: none; user-select: none; -webkit-user-select: none;
  transition: scale var(--d3) var(--ease); }
c29-slide .sl-wash { position: absolute; inset: 0; background: linear-gradient(90deg, color-mix(in srgb, var(--sl-c, var(--accent)) 22%, transparent), transparent); opacity: 0; }
c29-slide .sl-label { position: absolute; inset: 0; display: grid; place-items: center; padding-left: calc(var(--h) + 4px); padding-right: 16px;
  font-weight: 650; font-size: 15px; color: var(--ink-soft); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; pointer-events: none; }
c29-slide .sl-label span { background: linear-gradient(90deg, var(--ink-soft) 0 40%, var(--ink-mute) 50%, var(--ink-soft) 60%) 0 0/250% 100%;
  -webkit-background-clip: text; background-clip: text; color: transparent; animation: t-sweep var(--shimmer-period) linear infinite; }
c29-slide .sl-grip { position: absolute; top: var(--pad); left: var(--pad); height: calc(var(--h) - 2 * var(--pad)); width: calc(var(--h) - 2 * var(--pad));
  border-radius: max(0px, calc(min(var(--corner), calc(var(--h) / 2)) - var(--pad))); background: var(--sl-c, var(--accent)); color: var(--on-accent);
  display: flex; align-items: center; justify-content: flex-end; padding-right: calc((var(--h) - 2 * var(--pad) - 22px) / 2);
  box-shadow: 0 4px 12px -4px color-mix(in srgb, var(--sl-c, var(--accent)) 60%, transparent); cursor: grab; border: 0; }
c29-slide .sl-grip:focus-visible { box-shadow: var(--focus); }
c29-slide .sl-grip svg { width: 22px; height: 22px; transition: opacity var(--d2) var(--ease), transform var(--d3) var(--ease-give); }
c29-slide .sl-grip .ok { position: absolute; right: calc((var(--h) - 2 * var(--pad) - 22px) / 2); opacity: 0; transform: scale(.4); }
c29-slide .sl-grip .ok path { stroke-dasharray: 24; stroke-dashoffset: 24; transition: stroke-dashoffset var(--d4) var(--ease) var(--d2); }
c29-slide .sl-done { position: absolute; inset: 0; z-index: 2; display: grid; place-items: center; color: var(--on-accent); font-weight: 700; font-size: 15px; opacity: 0; pointer-events: none;
  transition: opacity var(--d3) var(--ease) var(--d3); }
c29-slide[data-state="done"] .sl-grip .go { opacity: 0; transform: translateX(8px); }
c29-slide[data-state="done"] .sl-grip .ok { opacity: 1; transform: none; }
c29-slide[data-state="done"] .sl-grip .ok path { stroke-dashoffset: 0; }
c29-slide[data-state="done"] .sl-done { opacity: 1; }
c29-slide[data-state="done"] .sl-label { opacity: 0; }
c29-slide[tone="money"] { --sl-c: var(--money); }
c29-slide[tone="money"] .sl-done, c29-slide[tone="money"] .sl-grip { color: var(--on-money); }
@keyframes t-sweep { from { background-position: 100% 0; } to { background-position: -150% 0; } }

/* ======================================================
   TOASTS  <c29-toasts>   [MIT-PORT toast-stack: every place is its depth]
   ====================================================== */
c29-toasts { position: fixed; z-index: 90; left: 50%; bottom: calc(16px + var(--safe-b)); width: min(420px, calc(100vw - 24px)); translate: -50% 0;
  height: 0; pointer-events: none; }
c29-toasts .toast { position: absolute; left: 0; right: 0; bottom: 0; pointer-events: auto; display: flex; align-items: center; gap: 10px;
  min-height: 56px; padding: 8px 8px 8px 16px; border-radius: min(var(--corner), 22px); overflow: hidden;
  background: var(--pane-strong); backdrop-filter: blur(16px) saturate(1.3); -webkit-backdrop-filter: blur(16px) saturate(1.3);
  box-shadow: 0 0 0 1px var(--pane-edge), var(--lift-hi); transform-origin: 50% 100%;
  transition: transform var(--d5) var(--ease-give), opacity var(--d3) var(--ease); }
c29-toasts .toast .tx { flex: 1; min-width: 0; font-size: 15px; font-weight: 550; line-height: 1.3; }
c29-toasts .toast .tx small { display: block; font-weight: 450; color: var(--ink-mute); font-size: 13px; }
c29-toasts .toast .dot { width: 26px; height: 26px; border-radius: 50%; display: grid; place-items: center; flex: none; background: var(--accent); color: var(--on-accent); }
c29-toasts .toast .dot svg { width: 15px; height: 15px; }
c29-toasts .toast[data-tone="neutral"] .dot { background: var(--ink); color: var(--bg); }
c29-toasts .toast[data-tone="sun"] .dot { background: var(--sun-300); color: #3b2a12; }
c29-toasts .toast[data-tone="bad"] .dot { background: var(--bad); color: var(--on-bad); }
c29-toasts .toast .drain { position: absolute; left: 0; bottom: 0; height: 2.5px; width: 100%; background: var(--accent); transform-origin: 0 50%; opacity: .7; }
c29-toasts .toast .t-btn { min-height: 40px; }
c29-toasts:hover .toast, c29-toasts:focus-within .toast { transition-duration: var(--d4); }

/* ======================================================
   BOTTOM SHEET  <c29-sheet> wraps native <dialog>
   ====================================================== */
dialog.t-sheet { position: fixed; inset: auto 0 0 0; margin: 0 auto; width: min(560px, 100%); max-width: 100%; max-height: min(88dvh, 760px);
  border-radius: calc(var(--corner) + 6px) calc(var(--corner) + 6px) 0 0; background: var(--raised); box-shadow: var(--lift-hi);
  padding: 0 var(--s5) calc(var(--s6) + var(--safe-b)); overflow: auto; overscroll-behavior: contain;
  transform: translateY(100%); transition: transform var(--d5) var(--ease-sheet), display var(--d5) allow-discrete, overlay var(--d5) allow-discrete; }
dialog.t-sheet[open] { transform: translateY(0); }
@starting-style { dialog.t-sheet[open] { transform: translateY(100%); } }
dialog.t-sheet::backdrop { background: color-mix(in srgb, #0b1622 34%, transparent); backdrop-filter: blur(2px); opacity: 0;
  transition: opacity var(--d4) var(--ease), display var(--d4) allow-discrete, overlay var(--d4) allow-discrete; }
dialog.t-sheet[open]::backdrop { opacity: 1; }
@starting-style { dialog.t-sheet[open]::backdrop { opacity: 0; } }
dialog.t-sheet.is-held { transition: none; }
.t-sheet .grab { display: block; margin: 0 auto; width: 100%; height: 28px; border: 0; background: var(--raised); cursor: grab; touch-action: none; position: sticky; top: 0; z-index: 2; }
/* The grab strip is a pointer affordance, not a focus destination.
   showModal's initial focus lands on it (first focusable element in the
   sheet) and the global :focus-visible ring painted a sea-green outline
   around the full-width strip — the "green border" defect (Rev 1,
   2026-10-17). Sheet focus lives on the dialog itself (tide.js sheet()).
   The grab also paints the sheet's own background so content scrolling
   beneath the sticky strip no longer ghosts through it. */
.t-sheet .grab:focus-visible { box-shadow: none; border-radius: 0; }
.t-sheet .grab::before { content: ""; display: block; margin: 10px auto 0; width: 40px; height: 5px; border-radius: 99px; background: var(--grab-pill); }
.t-sheet h2 { font-size: var(--fs-xl); margin-bottom: var(--s2); }
.t-sheet .sheet-body { display: grid; gap: var(--s4); grid-template-columns: minmax(0, 1fr); }
/* SCROLL LOCK (the sheet rule, Victor's order 2026-10-20): while any
   modal sheet/panel is open the background page must not scroll —
   by touch, wheel, trackpad, or keyboard — and must not rubber-band
   or chain. tide.js (scrollLock) is the mechanism: it fixes the body
   in place at the captured scroll position and restores it exactly
   on close, flagging the state as html[data-scroll-lock]. This rule
   is the CSS half of the contract. The sheet's own content keeps
   scrolling (overflow:auto + overscroll-behavior:contain above).
   WHEN TO USE: always — every modal inherits it via the canon lock;
   there is no opt-out. Record: SHEET-SCROLL-LOCK-2026-10-20.md. */
html[data-scroll-lock] { overscroll-behavior: none; }
html[data-scroll-lock] body { overflow: hidden; }

/* ======================================================
   SNOOZE  <c29-snooze>   presets + [MIT-PORT time-scrubber] ruler
   ====================================================== */
c29-snooze { display: grid; gap: var(--s4); }
c29-snooze .sz-presets { display: grid; grid-template-columns: 1fr 1fr; gap: var(--s2); }
c29-snooze .sz-presets button { min-height: 64px; border: 0; border-radius: var(--r-md); background: var(--surface); box-shadow: var(--hair); text-align: left;
  padding: 10px 14px; cursor: pointer; display: grid; gap: 2px; transition: background var(--d2) var(--ease), box-shadow var(--d2) var(--ease), transform var(--d1) var(--ease); }
c29-snooze .sz-presets button b { font-size: 15px; font-weight: 650; }
c29-snooze .sz-presets button span { font-size: 13px; color: var(--ink-mute); }
c29-snooze .sz-presets button[aria-pressed="true"] { background: var(--accent-soft); box-shadow: inset 0 0 0 1.5px var(--accent); }
/* Rev 5 (2026-10-18): the selected preset sits on the --accent-soft tint,
   where the sublabel's --ink-mute fell to 3.99:1 in dark. On the tint the
   sublabel steps up one ink level: --ink-soft measures 6.71:1 on the dark
   tint and stays clear on the light tint too. Unselected presets keep
   --ink-mute on --surface, which passes in both themes. */
c29-snooze .sz-presets button[aria-pressed="true"] span { color: var(--ink-soft); }
c29-snooze .sz-presets button:active { transform: scale(.97); }
c29-snooze .sz-read { text-align: center; }
c29-snooze .sz-time { font: 750 var(--fs-3xl)/1.1 var(--font-display); font-variant-numeric: tabular-nums; letter-spacing: -.02em; }
c29-snooze .sz-day { font-size: 14px; color: var(--ink-mute); }
c29-snooze .sz-ruler { position: relative; height: 64px; border-radius: var(--r-md); background: var(--sunken); overflow: hidden; touch-action: none; cursor: ew-resize;
  -webkit-mask: linear-gradient(90deg, transparent, #000 18%, #000 82%, transparent); mask: linear-gradient(90deg, transparent, #000 18%, #000 82%, transparent); }
c29-snooze .sz-ruler:focus-visible { box-shadow: var(--focus); }
c29-snooze .sz-ticks { position: absolute; inset: 0; }
c29-snooze .sz-ticks i { position: absolute; bottom: 14px; width: 2px; border-radius: 2px; background: var(--ink); translate: -1px 0; }
c29-snooze .sz-ticks em { position: absolute; bottom: 0; font: 600 10px/1 var(--font-ui); font-style: normal; color: var(--ink-mute); translate: -50% 0; white-space: nowrap; }
c29-snooze .sz-mark { position: absolute; left: 50%; top: 6px; bottom: 10px; width: 3px; translate: -1.5px 0; border-radius: 3px; background: var(--accent); }

/* ======================================================
   COMPOSER  <c29-composer>   (clean-room, spec: ai-chat-input)
   pill → box on one springy curve; radius never changes
   ====================================================== */
/* MULTILINE LAW (2026-10-20, Principal's field defect on the Desk):
   the composer is a dictation surface, not a one-line pill. The box
   AUTO-GROWS with its content (JS sizes the textarea; the box follows),
   the placeholder WRAPS — it is never clipped mid-word and never runs
   under the send/mic disc: the textarea's right padding (60px) is the
   disc's reserved slot on every line, in every state. Radius 20px —
   a growing box is not a pill. Comment mode starts at two lines: a
   comment is an instruction, and the field must look like one. */
c29-composer { display: block; --c-h: 54px; }
c29-composer .cp-box { position: relative; min-height: var(--c-h); border-radius: 20px; background: var(--raised);
  box-shadow: 0 0 0 1px var(--edge), var(--lift); transition: box-shadow var(--d3) var(--ease); overflow: hidden;
  display: flex; flex-direction: column; }
c29-composer.is-open textarea { padding-bottom: 58px; } /* P7 (night loop 2026-10-25): 50px -> 58px with the tools-row floor below — the row grew 8px and the textarea's reserved slot grows with it, so text never slides under the chips. */
c29-composer.is-open .cp-box { box-shadow: 0 0 0 1.5px color-mix(in srgb, var(--accent) 60%, transparent), var(--lift-hi); }
c29-composer textarea { flex: none; width: 100%; box-sizing: border-box; resize: none; border: 0; background: none; outline: none; padding: 15px 60px 13px 18px; font-size: 16px; line-height: 1.45; color: var(--ink); min-height: 0; max-height: 184px; overflow-y: auto; transition: height var(--d2) var(--ease); }
c29-composer[mode="comment"] textarea { min-height: 74px; }
c29-composer textarea::placeholder { color: var(--ink-mute); }
/* ======================================================
   SERIES  <c29-series>   (2026-10-20) — one question per step
   ====================================================== */
c29-series { display: block; }
c29-series .qs-progress { display: grid; gap: var(--s2); margin-bottom: var(--s5); }
c29-series .qs-count { font-size: var(--fs-sm); font-weight: 650; color: var(--ink-soft); overflow-wrap: anywhere; } /* D7 (night loop 2026-10-25): the SERIES secondary family (count/why/default/row-q/note) sat on --ink-mute and was the shell's thinnest passing text — ELEMENT-SAMPLED in the Desk host at why 4.83, default 4.93, count 4.91, note 5.41, row-q 4.61 (dark; row-q sits on the --raised ground, the family's worst pairing). The component's secondary text steps up one ink level to --ink-soft — the c29-snooze Rev 5 precedent (a sublabel steps up where its ground is tinted) applied family-wide, since a series host's ground is the host's card, never the plain surface --ink-mute was tuned for. After: 7.20–9.17 element-sampled, both themes (shots-r7). */
c29-series .qs-bar { display: block; height: 5px; border-radius: 99px; background: var(--wash-2); overflow: hidden; }
c29-series .qs-bar i { display: block; height: 100%; background: var(--accent); border-radius: 99px; transition: width var(--d3) var(--ease); }
c29-series .qs-step { display: grid; gap: var(--s3); min-width: 0; }
c29-series .qs-prompt { font-size: 22px; line-height: 1.3; font-weight: 700; color: var(--ink); overflow-wrap: anywhere; } /* D6 (night loop 2026-10-25): the SERIES text family carried NO wrap guard — RENDERED in the D6 rig (a Desk question_series card with 120-char tokens in prompt/why/option/default_note): the step grid blew the Desk panel's scrollW to 1597 (Chromium) / 1621 (WebKit) vs clientW 352, escapers to x1616/1640, both themes — the first series fixture in six rounds found it. overflow-wrap:anywhere on every text-bearing qs-* rule + min-width:0 on the component's grids/buttons is the component fix (house pattern, c29-tile precedent); the host .pl-card was already min-width:0 — the component's own grid items were not. */
c29-series .qs-why { font-size: var(--fs-sm); color: var(--ink-soft); overflow-wrap: anywhere; } /* D7: --ink-mute -> --ink-soft, see .qs-count note */
c29-series .qs-opts { display: grid; gap: var(--s2); min-width: 0; }
c29-series .qs-opt { min-height: 52px; padding: var(--s3) var(--s4); border-radius: var(--r-md); border: 0; background: var(--raised); box-shadow: 0 0 0 1px var(--edge), var(--lift);
  font: inherit; font-size: 16px; font-weight: 600; color: var(--ink); text-align: left; cursor: pointer; transition: box-shadow var(--d2) var(--ease), background var(--d2) var(--ease), transform var(--d1) var(--ease); min-width: 0; overflow-wrap: anywhere; }
c29-series .qs-opt:active { transform: scale(.985); }
c29-series .qs-opt.is-on { background: color-mix(in srgb, var(--accent) 16%, var(--raised)); box-shadow: 0 0 0 2px var(--accent), var(--lift); }
c29-series .qs-default { font-size: var(--fs-sm); color: var(--ink-soft); overflow-wrap: anywhere; } /* D7: --ink-mute -> --ink-soft, see .qs-count note */
c29-series .qs-nav { display: flex; align-items: center; gap: var(--s2); margin-top: var(--s2); }
c29-series .qs-review { display: grid; gap: var(--s2); min-width: 0; }
c29-series .qs-row { display: grid; gap: 2px; width: 100%; padding: var(--s3) var(--s4); border: 0; border-radius: var(--r-md); background: var(--raised);
  box-shadow: 0 0 0 1px var(--edge), var(--lift); font: inherit; text-align: left; cursor: pointer; min-width: 0; overflow-wrap: anywhere; }
c29-series .qs-row-q { font-size: var(--fs-sm); color: var(--ink-soft); overflow-wrap: anywhere; } /* D7: --ink-mute -> --ink-soft, see .qs-count note (this rule was the family's thinnest: 4.61 dark on --raised) */
c29-series .qs-row-a { font-size: 16px; font-weight: 650; color: var(--ink); overflow-wrap: anywhere; }
c29-series .qs-note { font-size: var(--fs-sm); color: var(--ink-soft); margin-top: var(--s3); overflow-wrap: anywhere; } /* D7: --ink-mute -> --ink-soft, see .qs-count note */

c29-composer .cp-tools { position: absolute; left: 0; right: 0; bottom: 0; display: flex; align-items: center; gap: 6px; padding: 0 12px 10px; opacity: 0; transform: translateY(6px);
  transition: opacity var(--d2) var(--ease), transform var(--d3) var(--ease); pointer-events: none; overflow-x: auto; scrollbar-width: none; margin-right: 54px; }
c29-composer.is-open .cp-tools { opacity: 1; transform: none; pointer-events: auto; transition-delay: var(--d1); }
c29-composer .cp-tools .t-chip { min-height: 40px; padding: 0 12px; font-size: 13px; flex: none; } /* P7 (night loop 2026-10-25): the tools row overrode the chip to 32px — RENDERED in the P7 states rig (a series short-text step's Dictate chip at 87x32, both engines/themes): a sub-40 target inside the shell's 40px floor, in a state no resting-state sweep opens (the tools render only when the composer is open). The component owns the floor: 40px here, and the voice strip's close control below takes the same floor. */
c29-composer .cp-send { position: absolute; right: 6px; bottom: 6px; width: 40px; height: 40px; border-radius: 50%; border: 0; display: grid; place-items: center; cursor: pointer;
  background: transparent; color: var(--ink-mute); transition: background var(--d3) var(--ease), color var(--d3) var(--ease), transform var(--d1) var(--ease); }
c29-composer .cp-send svg { position: absolute; width: 20px; height: 20px; transition: opacity var(--d2) var(--ease), transform var(--d3) var(--ease-give); }
c29-composer .cp-send .send { opacity: 0; transform: translateY(8px) scale(.6); }
c29-composer.has-text .cp-send { background: var(--ink); color: var(--bg); }
c29-composer.has-text .cp-send .mic { opacity: 0; transform: scale(.6); }
c29-composer.has-text .cp-send .send { opacity: 1; transform: none; }
c29-composer.is-sent .cp-send { transform: translateY(-4px) scale(.92); }
c29-composer .cp-ctx { display: flex; align-items: center; gap: 6px; margin-bottom: 8px; font-size: 13px; color: var(--ink-mute); min-height: 20px; }
c29-composer .cp-ctx:empty { display: none; }

/* ---------- voice (D3 variant: absorbs c29-voicebar) ----------
   The voice strip lives inside the box, above the tools: state
   word, ONE level meter painted from the single host-set value,
   and the interim words as plain visible text. Nothing types
   itself; text appears only when the host's recognizer reports
   it, and final text lands in the textarea (see tide.js). */
c29-composer .cp-voice { display: flex; align-items: center; gap: 8px; margin: 0 64px 0 12px; padding: 8px 12px; border-radius: var(--r-md);
  background: var(--accent-soft); color: var(--accent-text); font-size: 13.5px; position: relative; z-index: 1; }
c29-composer .cp-voice[hidden] { display: none; }
c29-composer.is-open .cp-voice { margin-bottom: 52px; } /* P7: 44px -> 52px, tracking the tools row's 8px floor growth above. */
c29-composer .cp-voice-dot { flex: none; width: 9px; height: 9px; border-radius: 50%; background: var(--accent); }
c29-composer[data-voice="listening"] .cp-voice-dot { animation: cp-pulse 1.2s var(--ease) infinite; }
@keyframes cp-pulse { 50% { opacity: .35; } }
html[data-motion="reduced"] c29-composer[data-voice="listening"] .cp-voice-dot { animation: none; }
c29-composer .cp-voice-lab { font-weight: 650; flex: none; }
c29-composer .cp-voice-meter { flex: none; width: 56px; height: 6px; border-radius: 99px; background: var(--wash-2); overflow: hidden; }
c29-composer .cp-voice-meter i { display: block; height: 100%; width: 0; background: var(--accent); border-radius: 99px; transition: width var(--d1) linear; }
c29-composer .cp-voice-live { flex: 1; min-width: 0; color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
c29-composer .cp-voice-x { flex: none; width: 40px; height: 40px; border: 0; border-radius: 50%; background: none; color: inherit; display: grid; place-items: center; cursor: pointer; } /* P7: 32px -> 40px floor, see .cp-tools .t-chip note. */
c29-composer .cp-voice-x .t-icon { width: 15px; height: 15px; }
c29-composer[data-voice="error"] .cp-voice { background: var(--bad-bg); color: var(--bad-text); }
c29-composer[data-voice="error"] .cp-voice-dot { background: var(--bad); animation: none; }
c29-composer[data-voice="error"] .cp-voice-lab { font-weight: 600; }
c29-composer[data-voice="transcribing"] .cp-voice-dot { background: var(--ink-mute); animation: none; }
c29-composer .cp-send .stopic { opacity: 0; transform: scale(.6); }
c29-composer[data-voice="listening"] .cp-send, c29-composer[data-voice="transcribing"] .cp-send { background: var(--bad); color: var(--on-bad); }
c29-composer[data-voice="listening"] .cp-send .mic, c29-composer[data-voice="transcribing"] .cp-send .mic,
c29-composer[data-voice="listening"] .cp-send .send, c29-composer[data-voice="transcribing"] .cp-send .send { opacity: 0; }
c29-composer[data-voice="listening"] .cp-send .stopic, c29-composer[data-voice="transcribing"] .cp-send .stopic { opacity: 1; transform: none; }
c29-composer .cp-tools .cp-dictate .t-icon { width: 15px; height: 15px; }

/* ======================================================
   FEEDBACK BAR  <c29-feedback>   (clean-room, spec: feedback-bar)
   one bar changing shape; width eases to measured content
   ====================================================== */
c29-feedback { display: inline-block; max-width: 100%; }
c29-feedback .fb { display: inline-flex; align-items: center; overflow: hidden; border-radius: 26px; background: var(--surface); box-shadow: var(--hair);
  min-height: 52px; transition: width var(--d4) var(--ease), height var(--d4) var(--ease); max-width: 100%; }
c29-feedback .fb-in { display: flex; align-items: center; gap: 6px; padding: 4px 6px 4px 16px; flex-wrap: wrap; animation: t-in var(--d3) var(--ease) both; }
c29-feedback .fb-in > span { font-size: 15px; font-weight: 550; margin-right: 4px; }
c29-feedback .fb-thumb { width: 44px; height: 44px; border: 0; border-radius: 50%; background: transparent; cursor: pointer; display: grid; place-items: center; color: var(--ink-soft);
  transition: background var(--d2) var(--ease), transform var(--d3) var(--ease-give); }
c29-feedback .fb-thumb:hover { background: var(--wash); }
c29-feedback .fb-thumb.is-on { background: var(--accent); color: var(--on-accent); animation: t-pop var(--d4) var(--ease-give); }
c29-feedback .fb-thumb.is-shake { background: var(--ink); color: var(--bg); animation: t-shake var(--d4) var(--ease); }
c29-feedback .fb-reasons { display: flex; flex-wrap: wrap; gap: 6px; padding: 2px 0 6px; width: 100%; }
@keyframes t-in { from { opacity: 0; filter: blur(4px); transform: translateY(4px); } }
@keyframes t-pop { 40% { transform: scale(1.18); } }

/* ======================================================
   DECISION INBOX  <c29-inbox>   (clean-room, specs: activity-stack + access-request)
   stacked by depth; press opens into the list; rows approve/dismiss
   ====================================================== */
c29-inbox { display: block; --peek: 10px; }
c29-inbox .ib-pile { position: relative; display: block; width: 100%; border: 0; padding: 0; background: none; text-align: left; cursor: pointer; color: inherit;
  height: calc(92px + 2 * var(--peek)); transition: height var(--d4) var(--ease); }
c29-inbox .ib-card { position: absolute; left: 0; right: 0; bottom: 0; height: 92px; border-radius: var(--corner); background: var(--raised); box-shadow: var(--lift);
  padding: 14px 16px; display: flex; gap: 12px; align-items: center; transform-origin: 50% 0;
  transition: transform var(--d5) var(--ease-give), opacity var(--d4) var(--ease), background-color var(--d4) var(--ease), box-shadow var(--d4) var(--ease), height var(--d4) var(--ease); }
c29-inbox .ib-card .ib-av { width: 44px; height: 44px; border-radius: 14px; display: grid; place-items: center; flex: none; background: var(--accent-soft); color: var(--accent); }
c29-inbox .ib-card .ib-av.money { background: color-mix(in srgb, var(--money) 14%, var(--surface)); color: var(--money); }
c29-inbox .ib-card .ib-t { min-width: 0; flex: 1; }
c29-inbox .ib-card .ib-t b { display: block; font-size: 15px; font-weight: 650; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
c29-inbox .ib-card .ib-t span { display: block; font-size: 13px; color: var(--ink-mute); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
c29-inbox .ib-badge { position: absolute; z-index: 9; top: -6px; right: 10px; min-width: 26px; height: 26px; padding: 0 8px; border-radius: 99px; background: var(--money); color: var(--on-money);
  font: 700 13px/26px var(--font-ui); text-align: center; font-variant-numeric: tabular-nums; box-shadow: 0 0 0 3px var(--bg); transition: transform var(--d3) var(--ease-give); }
c29-inbox .ib-badge.bump { transform: scale(1.25); }
c29-inbox.is-open .ib-pile { height: 64px; }
c29-inbox.is-empty .ib-pile { display: none; }
/* The pile is one persistent stack (2026-10-20, the violent-flip
   fix): depth cards fold INTO the header on open and re-emerge on
   close; the front and fold faces crossfade inside the same shell;
   one chevron rotates and glides instead of swapping; the badge is
   never re-created, so it cannot teleport. All durations are the
   motion tokens — --speed (Tune) and the reduced-motion gate in
   tokens.css collapse the whole move for free. */
c29-inbox .ib-back { z-index: 1; }
c29-inbox .ib-back[data-d="1"] { transform: translateY(calc(-1 * var(--peek))) scale(.95); opacity: .82; }
c29-inbox .ib-back[data-d="2"] { transform: translateY(calc(-2 * var(--peek))) scale(.9); opacity: .64; }
c29-inbox.is-open .ib-back { transform: translateY(0) scale(.97); opacity: 0; }
c29-inbox .ib-face { z-index: 2; }
c29-inbox .ib-front { visibility: visible; opacity: 1; transition: transform var(--d4) var(--ease), opacity var(--d3) var(--ease), height var(--d4) var(--ease), visibility 0s 0s; }
c29-inbox.is-open .ib-front { visibility: hidden; opacity: 0; height: 64px; transform: translateY(8px) scale(.99); transition: transform var(--d4) var(--ease), opacity var(--d3) var(--ease), height var(--d4) var(--ease), visibility 0s var(--d3); }
c29-inbox .ib-fold { height: 64px; background: var(--accent-soft); box-shadow: var(--hair); visibility: hidden; opacity: 0; transform: translateY(-6px); transition: transform var(--d4) var(--ease), opacity var(--d3) var(--ease), visibility 0s var(--d3); }
c29-inbox.is-open .ib-fold { visibility: visible; opacity: 1; transform: none; transition: transform var(--d4) var(--ease), opacity var(--d3) var(--ease), visibility 0s 0s; }
c29-inbox .ib-chev { position: absolute; z-index: 3; right: 16px; bottom: 34px; width: 24px; height: 24px; display: grid; place-items: center; pointer-events: none;
  transition: transform var(--d4) var(--ease), bottom var(--d4) var(--ease); }
c29-inbox.is-open .ib-chev { transform: rotate(180deg); bottom: 20px; }
/* The list unfolds by grid-rows (the .t-unfold technique) so the
   page below glides with the container's real height instead of
   teleporting; visibility keeps closed rows out of tab order and
   releases only when the fold has finished. */
c29-inbox .ib-unfold { display: grid; grid-template-rows: 0fr; visibility: hidden; transition: grid-template-rows var(--d4) var(--ease), visibility 0s var(--d4); }
c29-inbox .ib-unfold > .ib-clip { overflow: hidden; min-height: 0; }
c29-inbox.is-open .ib-unfold, c29-inbox.is-empty .ib-unfold { grid-template-rows: 1fr; visibility: visible; transition: grid-template-rows var(--d4) var(--ease), visibility 0s 0s; }
c29-inbox .ib-list { display: grid; gap: 8px; margin-top: var(--s3); }
c29-inbox .ib-list.is-settling { transition: height var(--d3) var(--ease); }
c29-inbox .ib-row { display: grid; grid-template-columns: 40px 1fr; gap: 6px 12px; align-items: center; padding: 12px 12px 12px 14px; border-radius: var(--r-md); background: var(--surface); box-shadow: var(--hair);
  transition: opacity var(--d3) var(--ease), transform var(--d4) var(--ease), grid-template-rows var(--d4) var(--ease); }
/* Rows ride the unfold: tucked up under the header while closed,
   settling in with a small stagger on open and folding back in
   reverse order on close. The stagger delays live only while
   .is-anim is on the host (the toggle window), so rows rebuilt by
   a decide/undo land settled, never replaying the entrance. */
c29-inbox:not(.is-open):not(.is-empty) .ib-row { opacity: 0; transform: translateY(-10px); }
c29-inbox.is-anim .ib-row { transition-delay: calc(var(--i, 0) * 55ms); }
c29-inbox.is-anim:not(.is-open) .ib-row { transition-delay: calc((var(--n, 1) - 1 - var(--i, 0)) * 40ms); }
c29-inbox .ib-row .ib-av { width: 40px; height: 40px; border-radius: 12px; display: grid; place-items: center; background: var(--accent-soft); color: var(--accent); }
c29-inbox .ib-row .ib-av.money { background: color-mix(in srgb, var(--money) 14%, var(--surface)); color: var(--money); }
c29-inbox .ib-row .ib-t b { display: block; font-size: 15px; font-weight: 650; }
c29-inbox .ib-row .ib-t span { display: block; font-size: 13px; color: var(--ink-mute); }
c29-inbox .ib-row .ib-acts { grid-column: 1 / -1; display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
c29-inbox .ib-row.is-out { opacity: 0; transform: translateX(40px) scale(.98); }
c29-inbox .ib-row.is-out-l { opacity: 0; transform: translateX(-40px) scale(.98); }
c29-inbox .ib-empty { text-align: center; padding: var(--s6); color: var(--ink-mute); border-radius: var(--corner); box-shadow: inset 0 0 0 1.5px var(--edge); }

/* ======================================================
   ROW LIST  <c29-list>  +  STAR  <c29-star>
   (2026-10-19, Desk composition rebuild — rows that open;
   interiors are composed by the host from t-* primitives)
   ====================================================== */
c29-list { display: grid; gap: var(--s2); list-style: none; }
/* Row chrome + bullet suppression are PURE CSS on the <li> itself —
   never a side effect of the JS-added .ls-row class. Post-mortem
   2026-10-19 (Desk iOS render failure): a fresh page paired with a
   stale cached tide.js (no c29-list definition) left rows un-adopted,
   and every protection the component had rode on adoption — native
   bullets rendered and the row interior fell back to inline flow.
   Canon law from that day: a component's layout must hold with its
   JS dead. .ls-row stays as the adoption marker for compat. */
c29-list > li { list-style: none; margin: 0; padding: 0; }
c29-list > li::marker { content: none; }
c29-list > li, c29-list .ls-row { display: flex; align-items: stretch; background: var(--surface); border-radius: var(--corner); box-shadow: var(--hair); overflow: hidden; }
c29-list [data-open] { flex: 1 1 auto; min-width: 0; display: grid; gap: 3px; align-content: center; text-align: left; padding: var(--s3) var(--s4); font: inherit; color: inherit; background: transparent; border: 0; cursor: pointer; }
c29-list [data-open]:active { background: var(--wash); }
c29-list .ls-row > c29-star { align-self: center; margin-right: var(--s2); }
c29-list .ls-empty { text-align: center; padding: var(--s6); color: var(--ink-mute); border-radius: var(--corner); box-shadow: inset 0 0 0 1.5px var(--edge); }

c29-star { display: inline-flex; flex: none; }
c29-star .st-btn { width: var(--tap); height: var(--tap); display: grid; place-items: center; border: 0; border-radius: var(--r-md); background: transparent; color: var(--ink-mute); cursor: pointer; transition: color var(--d2) var(--ease), background var(--d2) var(--ease), transform var(--d1) var(--ease); }
c29-star .st-btn:hover { background: var(--wash); }
c29-star .st-btn:active { transform: scale(.92); }
c29-star .st-btn svg { width: 21px; height: 21px; fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linejoin: round; }
c29-star[pressed] .st-btn { color: var(--29d-amber-600); }
c29-star[pressed] .st-btn svg { fill: currentColor; }

/* ======================================================
   BUY  <c29-buy>   (clean-room, spec: pay-button) — one shape the whole way
   ====================================================== */
c29-buy { display: block; }
c29-buy .by { position: relative; display: flex; align-items: center; justify-content: center; margin: 0 auto; width: 100%; height: 56px; border: 0; border-radius: 28px;
  background: var(--money); color: var(--on-money); font-weight: 700; font-size: 16px; cursor: pointer; overflow: hidden;
  transition: width var(--d4) var(--ease-sheet), background var(--d3) var(--ease), transform var(--d1) var(--ease); }
c29-buy .by:active { transform: scale(.98); }
c29-buy .by-l { display: flex; align-items: center; gap: 8px; white-space: nowrap; transition: opacity var(--d2) var(--ease), filter var(--d2) var(--ease); }
c29-buy .by-l svg { width: 16px; height: 16px; }
c29-buy .by-price { font-variant-numeric: tabular-nums; padding-left: 8px; margin-left: 2px; border-left: 1px solid color-mix(in srgb, currentColor 35%, transparent); }
c29-buy .by-track { position: absolute; left: 22px; right: 22px; top: 50%; height: 3px; translate: 0 -50%; border-radius: 3px; background: color-mix(in srgb, currentColor 25%, transparent); opacity: 0; overflow: hidden; }
c29-buy .by-track::after { content: ""; position: absolute; inset: 0; width: 40%; border-radius: 3px; background: currentColor; translate: -100% 0; }
c29-buy .by-ok { position: absolute; display: flex; align-items: center; gap: 8px; opacity: 0; white-space: nowrap; }
c29-buy .by-ok path { stroke-dasharray: 24; stroke-dashoffset: 24; transition: stroke-dashoffset var(--d4) var(--ease) var(--d3); }
c29-buy[data-state="processing"] .by { width: 128px; }
c29-buy[data-state="processing"] .by-l { opacity: 0; filter: blur(3px); }
c29-buy[data-state="processing"] .by-track { opacity: 1; transition: opacity var(--d2) var(--ease) var(--d3); }
c29-buy[data-state="processing"] .by-track::after { animation: t-run calc(1100ms / var(--speed)) var(--ease) infinite; }
c29-buy[data-state="success"] .by { background: var(--ok); color: var(--on-ok); }
c29-buy[data-state="success"] .by-l { opacity: 0; }
c29-buy[data-state="success"] .by-ok { opacity: 1; transition: opacity var(--d3) var(--ease) var(--d3); }
c29-buy[data-state="success"] .by-ok path { stroke-dashoffset: 0; }
c29-buy .by-meta { display: flex; align-items: center; justify-content: center; gap: 8px; margin-top: 10px; font-size: 13px; color: var(--ink-mute); flex-wrap: wrap; text-align: center; }
c29-buy .by-receipt { margin-top: 10px; }
@keyframes t-run { to { translate: 260% 0; } }

/* ======================================================
   UPGRADE / RESET / LAPSED  <c29-upgrade>
   ====================================================== */
c29-upgrade { display: grid; position: relative; border-radius: var(--corner); overflow: hidden; box-shadow: var(--hair); background: var(--surface); }
c29-upgrade .up-preview, c29-upgrade .up-gate { grid-area: 1 / 1; }
c29-upgrade .up-preview { padding: var(--s5); filter: blur(5px) saturate(.8); opacity: .55; pointer-events: none; user-select: none; }
c29-upgrade[data-unlocked] .up-preview { filter: none; opacity: 1; pointer-events: auto; transition: filter var(--d5) var(--ease), opacity var(--d5) var(--ease); }
c29-upgrade .up-gate { display: grid; place-items: center; padding: var(--s4); transition: opacity var(--d3) var(--ease); }
c29-upgrade[data-unlocked] .up-gate { opacity: 0; pointer-events: none; }
c29-upgrade .up-card { max-width: 320px; width: 100%; text-align: center; display: grid; gap: 10px; justify-items: center; padding: var(--s5); }
c29-upgrade .up-card .t-pane { padding: 0; }
c29-upgrade .up-ic { width: 44px; height: 44px; border-radius: 14px; display: grid; place-items: center; background: var(--sun-100); color: #8a5a17; }
[data-theme="dark"] c29-upgrade .up-ic { background: #3a2a14; color: var(--sun-300); }
c29-upgrade h3 { font-size: var(--fs-lg); }
c29-upgrade p { font-size: 14px; color: var(--ink-soft); }

/* ======================================================
   ACCOUNT  <c29-account>
   ====================================================== */
c29-account { display: inline-block; position: relative; flex: none; }
c29-account > .t-btn { padding: 0 14px; }
c29-account .ac-av { width: 44px; height: 44px; border-radius: 50%; border: 0; cursor: pointer; padding: 3px; background: conic-gradient(from 200deg, var(--sea-300), var(--sun-300), var(--sea-300)); }
c29-account .ac-av span { display: grid; place-items: center; width: 100%; height: 100%; border-radius: 50%; background: var(--raised); font-weight: 750; font-size: 15px; color: var(--ink); }
c29-account [popover] { position: fixed; inset: auto; margin: 0; border: 0; padding: 8px; min-width: 240px; border-radius: 18px; background: var(--pane-strong);
  backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px); box-shadow: 0 0 0 1px var(--pane-edge), var(--lift-hi); color: var(--ink);
  opacity: 0; transform: translateY(-6px) scale(.97); transform-origin: 100% 0;
  transition: opacity var(--d2) var(--ease), transform var(--d3) var(--ease-give), display var(--d3) allow-discrete, overlay var(--d3) allow-discrete; }
c29-account [popover]:popover-open { opacity: 1; transform: none; }
@starting-style { c29-account [popover]:popover-open { opacity: 0; transform: translateY(-6px) scale(.97); } }
c29-account .ac-who { padding: 10px 12px 12px; border-bottom: 1px solid var(--edge-soft); margin-bottom: 6px; }
c29-account .ac-who b { display: block; font-size: 15px; } c29-account .ac-who span { font-size: 13px; color: var(--ink-mute); }
c29-account .ac-item { display: flex; align-items: center; gap: 10px; width: 100%; min-height: 44px; padding: 0 12px; border: 0; border-radius: 12px; background: none; cursor: pointer; font-size: 15px; text-align: left; color: var(--ink); }
c29-account .ac-item:hover { background: var(--wash); }
.c29-if-member, .c29-if-anon { display: contents; }
html[data-session="anon"] .c29-if-member, html:not([data-session="anon"]) .c29-if-anon { display: none; }

/* ======================================================
   ONE-TIME CODE  <c29-otp>   [MIT-PORT one-time-code: one real input under six drawn cells]
   ====================================================== */
c29-otp { display: block; position: relative; max-width: 360px; }
c29-otp input { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; font-size: 17px; letter-spacing: 2em; caret-color: transparent; border: 0; z-index: 2; }
c29-otp .otp-cells { display: grid; grid-template-columns: repeat(6, 1fr); gap: 8px; }
c29-otp .otp-cells i { height: 58px; border-radius: 14px; background: var(--raised); box-shadow: 0 0 0 1px var(--edge); display: grid; place-items: center; font: 700 24px/1 var(--font-display); font-style: normal;
  font-variant-numeric: tabular-nums; transition: box-shadow var(--d2) var(--ease), transform var(--d3) var(--ease-give), border-radius var(--d4) var(--ease), background var(--d3) var(--ease); }
c29-otp .otp-cells i.is-cur { box-shadow: 0 0 0 1.5px var(--accent), 0 0 0 5px color-mix(in srgb, var(--accent) 15%, transparent); }
c29-otp .otp-cells i.is-cur::after { content: ""; width: 2px; height: 24px; background: var(--accent); animation: t-blink 1s steps(1) infinite; }
c29-otp .otp-cells i.pop { transform: scale(1.08); }
c29-otp[data-state="ok"] .otp-cells { gap: 0; transition: gap var(--d4) var(--ease); }
c29-otp[data-state="ok"] .otp-cells i { border-radius: 0; background: var(--accent-soft); box-shadow: none; color: var(--accent-text); }
c29-otp[data-state="ok"] .otp-cells i:first-child { border-radius: 14px 0 0 14px; } c29-otp[data-state="ok"] .otp-cells i:last-child { border-radius: 0 14px 14px 0; }
c29-otp[data-state="bad"] .otp-cells { animation: t-shake var(--d4) var(--ease); }
c29-otp[data-state="bad"] .otp-cells i { box-shadow: 0 0 0 1.5px var(--bad); }
@keyframes t-blink { 50% { opacity: 0; } }

/* ======================================================
   COPY FIELD  <c29-copy>   (clean-room, spec: copy-field)
   ====================================================== */
c29-copy { min-width: 0; display: flex; align-items: center; gap: 4px; min-height: 52px; padding: 4px 4px 4px 16px; border-radius: var(--r-md); background: var(--sunken); box-shadow: inset 0 0 0 1px var(--edge-soft); max-width: 100%; }
c29-copy code { flex: 1; min-width: 0; font: 500 14px/1.3 var(--font-mono); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; position: relative;
  background: linear-gradient(90deg, var(--ink) 0 40%, var(--accent) 50%, var(--ink) 60%) 100% 0/250% 100%; -webkit-background-clip: text; background-clip: text; color: transparent; }
c29-copy.is-copied code { animation: t-sweep calc(700ms / var(--speed)) var(--ease) 1 both; }
c29-copy button { position: relative; width: 44px; height: 44px; border: 0; border-radius: 12px; background: var(--raised); box-shadow: var(--hair); cursor: pointer; display: grid; place-items: center; flex: none; color: var(--ink-soft); }
c29-copy button svg { position: absolute; width: 18px; height: 18px; transition: opacity var(--d2) var(--ease), transform var(--d2) var(--ease); }
c29-copy button .pg-b { transform: translate(2px, -2px); }
c29-copy button .ok { opacity: 0; color: var(--accent); }
c29-copy button .ok path { stroke-dasharray: 24; stroke-dashoffset: 24; transition: stroke-dashoffset var(--d3) var(--ease) var(--d2); }
c29-copy.is-copied .pg-b { transform: none; opacity: 0; } c29-copy.is-copied .pg-f { opacity: 0; transition-delay: var(--d1); }
c29-copy.is-copied .ok { opacity: 1; transition-delay: var(--d1); } c29-copy.is-copied .ok path { stroke-dashoffset: 0; }

/* ======================================================
   CONNECT  <c29-connect>
   ====================================================== */
c29-connect { display: grid; grid-template-columns: 44px 1fr auto; align-items: center; gap: 12px; padding: 12px 12px 12px 14px; border-radius: var(--r-md); background: var(--surface); box-shadow: var(--hair); position: relative; overflow: hidden; }
c29-connect .cn-logo { width: 44px; height: 44px; border-radius: 12px; display: grid; place-items: center; background: var(--sunken); font-weight: 800; font-size: 16px; color: var(--ink-soft); }
c29-connect .cn-t b { display: block; font-size: 15px; font-weight: 650; }
c29-connect .cn-t span { display: block; font-size: 13px; color: var(--ink-mute); min-height: 18px; }
c29-connect .cn-bar { position: absolute; left: 0; bottom: 0; height: 2.5px; width: 100%; background: var(--accent); transform: scaleX(0); transform-origin: 0 50%; transition: transform var(--d5) var(--ease); }
c29-connect[data-state="syncing"] .cn-bar { transform: scaleX(.7); transition-duration: calc(1400ms / var(--speed)); }
c29-connect[data-state="on"] .cn-bar { transform: scaleX(1); opacity: 0; transition: transform var(--d3) var(--ease), opacity var(--d4) var(--ease) var(--d3); }
c29-connect[data-state="on"] .cn-logo { background: var(--accent-soft); color: var(--accent); }

/* ---------- recover / reset prompt ---------- */
c29-recover { display: block; }

/* ======================================================
   SKELETON / SHIMMER (clean-room, specs: skeleton-morph, shimmer-text)
   ====================================================== */
.t-skel { position: relative; overflow: hidden; border-radius: 10px; background: var(--sunken); min-height: 14px; }
.t-skel::after { content: ""; position: absolute; inset: 0; translate: -100% 0;
  background: linear-gradient(90deg, transparent, color-mix(in srgb, var(--sun-100) 55%, transparent), transparent); animation: t-wave calc(var(--shimmer-period) / var(--speed)) var(--ease) infinite; }
[data-theme="dark"] .t-skel::after { background: linear-gradient(90deg, transparent, rgba(220,240,255,.06), transparent); }
@keyframes t-wave { 60%, 100% { translate: 100% 0; } }
.t-shimmer { background: linear-gradient(90deg, var(--ink-mute) 0 35%, var(--ink) 50%, var(--ink-mute) 65%) 0 0/250% 100%; -webkit-background-clip: text; background-clip: text; color: transparent;
  animation: t-sweep calc(var(--shimmer-period) / var(--speed)) linear infinite; }
.t-morph > * { animation: t-rise var(--d5) var(--ease) both; }
.t-morph > *:nth-child(2) { animation-delay: calc(80ms / var(--speed)); } .t-morph > *:nth-child(3) { animation-delay: calc(160ms / var(--speed)); }
.t-morph > *:nth-child(4) { animation-delay: calc(240ms / var(--speed)); }
@keyframes t-rise { from { opacity: 0; transform: translateY(8px); filter: blur(5px); } }

/* ======================================================
   TRACE / TIMELINE  <c29-trace>   (clean-room, spec: thinking) + [MIT-PORT step-player dots]
   ====================================================== */
c29-trace { display: block; }
c29-trace .tr-head { display: flex; align-items: center; gap: 10px; width: 100%; min-height: 44px; border: 0; background: none; padding: 0; cursor: pointer; text-align: left; font-size: 15px; font-weight: 650; color: var(--ink); }
c29-trace .tr-head svg { transition: transform var(--d3) var(--ease); }
c29-trace[data-folded] .tr-head svg.chev { transform: rotate(-90deg); }
c29-trace .tr-body { display: grid; grid-template-rows: 1fr; transition: grid-template-rows var(--d5) var(--ease); }
c29-trace[data-folded] .tr-body { grid-template-rows: 0fr; }
c29-trace .tr-body > ol { overflow: hidden; margin: 0; padding: 0; list-style: none; }
c29-trace li { position: relative; padding: 0 0 18px 34px; animation: t-rise var(--d4) var(--ease) both; }
c29-trace li::before { content: ""; position: absolute; left: 10px; top: 22px; bottom: -2px; width: 2px; border-radius: 2px; background: var(--edge); }
c29-trace li:last-child::before { display: none; }
c29-trace li .dot { position: absolute; left: 3px; top: 3px; width: 16px; height: 16px; border-radius: 50%; background: var(--raised); box-shadow: inset 0 0 0 2px var(--accent); }
c29-trace li[data-s="done"] .dot { background: var(--accent); box-shadow: none; }
c29-trace li[data-s="done"] .dot::after { content: ""; position: absolute; left: 5px; top: 3px; width: 4px; height: 7px; border: solid var(--on-accent); border-width: 0 2px 2px 0; rotate: 45deg; }
c29-trace li[data-s="run"] .dot { animation: t-breathe 1.6s var(--ease) infinite; }
c29-trace li[data-s="wait"] .dot { box-shadow: inset 0 0 0 2px var(--sun-500); background: var(--sun-100); }
c29-trace li b { display: block; font-size: 15px; font-weight: 600; }
c29-trace li span { display: block; font-size: 13px; color: var(--ink-mute); }
c29-trace li .tr-acts { display: flex; gap: 6px; margin-top: 8px; flex-wrap: wrap; }
c29-trace li .tr-note { margin-top: 8px; padding: 10px 12px; border-radius: 12px; background: var(--accent-soft); font-size: 14px; color: var(--ink-soft); animation: t-rise var(--d4) var(--ease) both; }
@keyframes t-breathe { 50% { box-shadow: inset 0 0 0 2px var(--accent), 0 0 0 6px color-mix(in srgb, var(--accent) 18%, transparent); } }

/* ---------- checklist  [MIT-PORT checklist: box fills, tick draws, words strike] ---------- */
.t-check { display: flex; align-items: center; gap: 12px; min-height: 44px; cursor: pointer; font-size: 15px; }
.t-check input { appearance: none; width: 24px; height: 24px; border-radius: 8px; box-shadow: inset 0 0 0 1.5px var(--edge); margin: 0; flex: none; display: grid; place-items: center; cursor: pointer;
  transition: background var(--d2) var(--ease), box-shadow var(--d2) var(--ease); }
.t-check input::after { content: ""; width: 6px; height: 11px; border: solid var(--on-accent); border-width: 0 2.2px 2.2px 0; rotate: 45deg; translate: 0 -1px; scale: 0; transition: scale var(--d3) var(--ease-give); }
.t-check input:checked { background: var(--accent); box-shadow: none; }
.t-check input:checked::after { scale: 1; }
.t-check span { background: linear-gradient(currentColor, currentColor) 0 55%/0% 1.5px no-repeat; transition: background-size var(--d4) var(--ease), color var(--d3) var(--ease); }
.t-check input:checked + span { background-size: 100% 1.5px; color: var(--ink-mute); }

/* ---------- tune panel ---------- */
.t-tune { display: grid; gap: var(--s4); }
.t-tune .tn-row { display: grid; grid-template-columns: 76px 1fr 46px; align-items: center; gap: 10px; font-size: 14px; }
.t-tune .tn-row output { font-variant-numeric: tabular-nums; color: var(--ink-mute); text-align: right; font-size: 13px; }
.t-tune input[type=range] { width: 100%; accent-color: var(--accent); height: 44px; margin: 0; }
.t-tune .tn-sw { display: flex; gap: 8px; flex-wrap: wrap; }
.t-tune .tn-sw button { width: 40px; height: 40px; border-radius: 50%; border: 0; cursor: pointer; box-shadow: inset 0 0 0 2px rgba(255,255,255,.4); }
.t-tune .tn-sw button[aria-pressed="true"] { box-shadow: 0 0 0 2px var(--bg), 0 0 0 4px var(--ink); }


/* ---------- TASTE WAVE 1 (2026-10-19) ---------- */
/* ============================================================
 * LANE A / A1 — c29-showcode + c29-countdown CSS FRAGMENT
 * Integration: paste inside the @layer c29.components block of
 * tide.css (the fragment ships wrapped in that layer so it also
 * loads standalone after tide.css in the lane harness).
 * Tokens only — no colour literals anywhere in this file.
 * ============================================================ */

/* ---------- SHOWCODE  <c29-showcode> ---------- */
c29-showcode { display: grid; gap: var(--s2); justify-items: center; text-align: center; min-width: 0; }
c29-showcode .sc-label { display: block; }
c29-showcode .sc-code {
  font: 800 clamp(30px, 11vw, 44px)/1.15 var(--font-display);
  letter-spacing: .14em; color: var(--ink);
  font-variant-numeric: tabular-nums;
  overflow-wrap: anywhere; user-select: text; -webkit-user-select: text;
  max-width: 100%; padding: 0 var(--s2);
}
c29-showcode .sc-code:focus-visible { box-shadow: var(--focus); border-radius: 8px; outline: none; }
c29-showcode .sc-url, c29-showcode .sc-urltext {
  font: 500 var(--fs-sm)/1.4 var(--font-mono); color: var(--accent-text);
  overflow-wrap: anywhere; max-width: 100%;
}
c29-showcode .sc-urltext { color: var(--ink-mute); }
c29-showcode .sc-expiry { display: flex; justify-content: center; }
c29-showcode .sc-actions { display: grid; gap: var(--s2); justify-items: center; margin-top: var(--s1); }
c29-showcode .sc-copy { min-width: 168px; }
c29-showcode .sc-copy[disabled] { opacity: .45; pointer-events: none; }
/* The status line is ALWAYS in the layout (min-height) so an
   answer never shifts the page — and failure is a sentence in
   --bad-text, not a colour alone. */
c29-showcode .sc-status { min-height: 22px; font-size: var(--fs-sm); font-weight: 600; color: var(--accent-text); max-width: 34ch; }
c29-showcode[data-state="copied"] .sc-status { color: var(--ok-text); }
c29-showcode[data-state="copy-failed"] .sc-status { color: var(--bad-text); }
c29-showcode[data-state="empty"] .sc-code { color: var(--ink-faint); }
c29-showcode[data-state="empty"] .sc-status { color: var(--ink-mute); font-weight: 550; }

/* ---------- COUNTDOWN  <c29-countdown> ---------- */
c29-countdown { display: inline-flex; align-items: baseline; gap: var(--s2); font-variant-numeric: tabular-nums; }
c29-countdown .cd-label { font-size: var(--fs-sm); color: var(--ink-mute); font-weight: 550; }
c29-countdown .cd-num { font-size: var(--fs-sm); font-weight: 700; color: var(--ink); min-width: 5ch; }
c29-countdown[data-state="expired"] .cd-num { color: var(--bad-text); }
c29-countdown[data-state="expired"] .cd-label { color: var(--bad-text); font-weight: 650; }
c29-countdown[data-state="unset"] .cd-num { color: var(--ink-faint); }
c29-countdown[data-state="paused"] .cd-num { color: var(--ink-mute); }
/* ============================================================
 * LANE A / A3 — c29-log CSS FRAGMENT
 * Integration: paste inside the @layer c29.components block of
 * tide.css. TOKENS ONLY: this file contains no hex / rgb / hsl
 * colour literal of any kind — grep it. The sunken pair
 * (--sunken background, --ink text) resolves per theme from
 * tokens.css, so a dark-on-light rendering cannot be written.
 * ============================================================ */

c29-log { display: block; min-width: 0; }
c29-log .lg-lines {
  list-style: none; margin: 0; padding: var(--s3) var(--s4);
  background: var(--sunken); color: var(--ink);
  box-shadow: inset 0 0 0 1px var(--edge-soft);
  border-radius: calc(var(--corner) - 6px);
  font: 500 13px/1.65 var(--font-mono);
  max-height: 264px; overflow-y: auto; overflow-x: hidden;
  overflow-wrap: anywhere; word-break: break-word;
}
c29-log .lg-line { padding: 1px 0 1px var(--s3); border-left: 2px solid transparent; min-width: 0; }
/* Tones are a border + a token text colour — never colour alone:
   the tone word is the line's own text, written by the host. */
c29-log .lg-line[data-tone="ok"] { border-left-color: var(--ok); }
c29-log .lg-line[data-tone="warn"] { border-left-color: var(--warn); }
c29-log .lg-line[data-tone="bad"] { border-left-color: var(--bad); color: var(--bad-text); }
c29-log .lg-empty {
  padding: var(--s4); text-align: center;
  background: var(--sunken); color: var(--ink-mute);
  box-shadow: inset 0 0 0 1px var(--edge-soft);
  border-radius: calc(var(--corner) - 6px);
  font-size: var(--fs-sm);
}
/* ============================================================
 * LANE A / A4 — c29-steps finish-parity CSS FRAGMENT
 * The variant reuses the canon .wv-steps-* rules UNCHANGED.
 * The only addition: the latched finished state reads as
 * settled, not merely done — the status line takes the ok
 * token and the advance button (disabled by the latch) keeps
 * its label legible. Tokens only.
 * Integration: append inside @layer c29.components in tide.css,
 * after the canon c29-steps block.
 * ============================================================ */

c29-steps[data-state="finished"] .wv-steps-status { color: var(--ok-text); }
c29-steps[data-state="finished"] .wv-steps-ctl .t-btn[data-act="advance"][disabled] { opacity: .75; }
/* ============================================================
 * FIELD  <c29-field>   (Taste Fleet Wave 1, Lane B — B1)
 * Label + control + hint + error composed in place, as ONE
 * pattern. The host supplies the control (input / textarea /
 * select) and the words; the component owns the wiring:
 * label-for, aria-describedby (hint + error), aria-invalid,
 * and an error that is kind and specific — a soft bad-bg line
 * with an icon and the host's exact words, never a bare red
 * shout and never a shake. Tokens only; works Tide-only and
 * loaded the bridge way (ds-tide-bridge.css order), both themes.
 * Integration: append inside @layer c29.components in tide.css.
 * ============================================================ */
c29-field { display: grid; gap: 6px; grid-template-columns: minmax(0, 1fr); text-align: left; }
c29-field .fld-label { font-size: 13px; font-weight: 650; color: var(--ink-soft); display: flex; align-items: baseline; gap: 6px; flex-wrap: wrap; }
c29-field .fld-req { font-size: 11px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase; color: var(--accent-text); }
c29-field .fld-opt { font-size: 11px; font-weight: 600; letter-spacing: .05em; text-transform: uppercase; color: var(--ink-mute); }
/* The control: the .t-input picture, owned here so a plain
   host <input> composes without extra classes. */
c29-field :is(input:not([type="checkbox"]):not([type="radio"]), textarea, select) {
  width: 100%; min-height: 48px; padding: 0 16px; border-radius: var(--r-md); border: 0;
  background: var(--raised); box-shadow: 0 0 0 1px var(--edge);
  font-size: 17px; color: var(--ink); transition: box-shadow var(--d2) var(--ease); }
c29-field textarea { min-height: 96px; padding: 12px 16px; line-height: 1.45; resize: vertical; }
c29-field select { appearance: none; }
c29-field :is(input, textarea, select):focus { outline: none; box-shadow: 0 0 0 1.5px var(--accent), 0 0 0 5px color-mix(in srgb, var(--accent) 16%, transparent); }
c29-field :is(input, textarea, select)::placeholder { color: var(--ink-mute); opacity: 1; }
[data-theme="dark"] c29-field :is(input, textarea, select)::placeholder { color: #9AABB3; opacity: 1; }
c29-field .fld-hint { font-size: var(--fs-sm); line-height: 1.45; color: var(--ink-mute); overflow-wrap: anywhere; }
c29-field .fld-hint:empty { display: none; }
/* Error: specific words on the soft bad ground, bad-text ink
   (the AA token pair), an icon so colour is never the only
   signal. No shake, no all-caps, no exclamation styling. */
c29-field .fld-error { display: flex; gap: 8px; align-items: flex-start; font-size: var(--fs-sm); line-height: 1.45;
  color: var(--bad-text); background: var(--bad-bg); border-radius: var(--r-md); padding: var(--s2) var(--s3); overflow-wrap: anywhere; }
c29-field .fld-error[hidden] { display: none; }
c29-field .fld-error .t-icon { width: 17px; height: 17px; flex: none; margin-top: 1px; }
c29-field[data-state="error"] :is(input, textarea, select) { box-shadow: 0 0 0 1.5px var(--bad); }
c29-field[data-state="error"] :is(input, textarea, select):focus { box-shadow: 0 0 0 1.5px var(--bad), 0 0 0 5px color-mix(in srgb, var(--bad) 16%, transparent); }
c29-field[data-state="ok"] .fld-ok { display: flex; }
c29-field .fld-ok { display: none; gap: 8px; align-items: flex-start; font-size: var(--fs-sm); line-height: 1.45; color: var(--ok-text); }
c29-field .fld-ok .t-icon { width: 17px; height: 17px; flex: none; margin-top: 1px; }
c29-field[data-disabled] { opacity: .55; }
c29-field[data-disabled] :is(input, textarea, select) { pointer-events: none; }
/* Honeypot: present in the form, absent from every human
   sense — off-screen and clipped, NOT display:none (the
   house intake's bot trap; see the showcase composition). */
c29-field[honeypot] { position: absolute !important; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; opacity: 0; pointer-events: none; }
/* ============================================================
 * OFFER  <c29-offer>   (Taste Fleet Wave 1, Lane B — B2)
 * One calm card: what it is, what it costs, what happens
 * next, one CTA. The component renders price STRUCTURES the
 * host passes in the price slot (.of-price-row / .of-amount /
 * .of-per / .of-arrow / .of-note) — pilot→ongoing and
 * install+monthly are both just two rows — and it never
 * hardcodes, parses, or restyles a number off-token: figures
 * ride the display face, tabular, in --ink; the money colour
 * appears once, on the CTA the host supplies.
 * Integration: append inside @layer c29.components in tide.css.
 * ============================================================ */
c29-offer { display: grid; gap: var(--s3); align-content: start; grid-template-columns: minmax(0, 1fr);
  background: var(--surface); border-radius: var(--corner); box-shadow: var(--hair); padding: var(--s5); text-align: left; }
c29-offer .of-head { display: grid; gap: var(--s1); }
c29-offer .of-kicker { font-size: var(--fs-cap); letter-spacing: var(--track-cap); text-transform: uppercase; font-weight: 650; color: var(--ink-mute); }
c29-offer .of-name { font-family: var(--font-display); font-weight: 750; font-size: var(--fs-xl); letter-spacing: var(--track-head); line-height: 1.15; text-wrap: balance; overflow-wrap: anywhere; }
c29-offer .of-tagrow { display: flex; gap: var(--s2); flex-wrap: wrap; align-items: center; }
c29-offer .of-what { color: var(--ink-soft); font-size: 15px; line-height: 1.5; overflow-wrap: anywhere; }
c29-offer .of-what:empty { display: none; }
c29-offer .of-price { display: grid; gap: var(--s2); padding: var(--s3) 0; border-top: 1px solid var(--edge-soft); border-bottom: 1px solid var(--edge-soft); }
c29-offer .of-price:empty { display: none; }
c29-offer .of-price-row { display: flex; align-items: baseline; gap: var(--s2); flex-wrap: wrap; }
c29-offer .of-amount { font: 750 var(--fs-2xl)/1.05 var(--font-display); font-variant-numeric: tabular-nums; letter-spacing: -.02em; color: var(--ink); white-space: nowrap; }
c29-offer .of-amount.is-small { font-size: var(--fs-xl); }
c29-offer .of-per { font-size: var(--fs-sm); color: var(--ink-mute); overflow-wrap: anywhere; }
c29-offer .of-arrow { color: var(--ink-mute); font-weight: 650; }
c29-offer .of-note { font-size: var(--fs-sm); color: var(--ink-mute); line-height: 1.45; overflow-wrap: anywhere; flex-basis: 100%; }
c29-offer .of-next { font-size: var(--fs-sm); line-height: 1.5; color: var(--ink-soft); overflow-wrap: anywhere; }
c29-offer .of-next:empty { display: none; }
c29-offer .of-next b { font-weight: 650; color: var(--ink); }
c29-offer .of-cta { margin-top: auto; padding-top: var(--s2); display: grid; }
c29-offer .of-cta .t-btn { width: 100%; }
c29-offer[data-cta-extra] .of-cta-extra-note { font-size: var(--fs-cap); color: var(--warn-text); }
/* ============================================================
 * FAQ  <c29-faq>   (Taste Fleet Wave 1, Lane B — B3)
 * Questions open like a drawer, not a performance: the panel
 * travels its MEASURED height on the house curve (--ease,
 * --d3 / --speed via JS) and is instant under the motion
 * gate. The closed state is the considered state — hairline
 * rows, a quiet chevron, generous tap rows — in both themes.
 * Real <button> semantics, aria-expanded honest, focus
 * visible via the global :focus-visible ring.
 * Integration: append inside @layer c29.components in tide.css.
 * ============================================================ */
c29-faq { display: block; background: var(--surface); border-radius: var(--corner); box-shadow: var(--hair); padding: 0 var(--s5); text-align: left; }
c29-faq .faq-item + .faq-item { border-top: 1px solid var(--edge-soft); }
c29-faq .faq-qh { margin: 0; }
c29-faq .faq-btn { display: flex; align-items: center; gap: var(--s3); width: 100%; min-height: 56px; padding: var(--s3) 0;
  border: 0; background: none; cursor: pointer; text-align: left;
  font: 650 15.5px/1.35 var(--font-ui); color: var(--ink); -webkit-tap-highlight-color: transparent; touch-action: manipulation; }
c29-faq .faq-btn:hover { color: var(--accent-text); }
c29-faq .faq-q { flex: 1; min-width: 0; overflow-wrap: anywhere; }
c29-faq .faq-chev { flex: none; width: 30px; height: 30px; border-radius: 50%; background: var(--wash); display: grid; place-items: center; color: var(--ink-soft);
  transition: transform var(--d3) var(--ease), background var(--d2) var(--ease); }
c29-faq .faq-chev .t-icon { width: 16px; height: 16px; }
c29-faq .faq-btn[aria-expanded="true"] .faq-chev { transform: rotate(180deg); background: var(--accent-soft); color: var(--accent-text); }
c29-faq .faq-panel { overflow: hidden; }
c29-faq .faq-panel[hidden] { display: none; }
c29-faq .faq-a { padding: 0 0 var(--s5); color: var(--ink-soft); font-size: 15px; line-height: 1.55; display: grid; gap: var(--s2); overflow-wrap: anywhere; }
c29-faq .faq-a a { color: var(--accent-text); }
/* ============================================================
 * BANNER  <c29-banner>   (Taste Fleet Wave 1, Lane B — B4)
 * The persistent sibling of c29-toasts: an inline, toned
 * state line that stays until the underlying state resolves.
 * It has NO timer, NO entrance travel, NO fixed positioning —
 * it never borrows a toast's urgency or a modal's
 * interruption. Tone is carried by the designed token pairs
 * (bg + text + dot) AND a glyph AND a word (the tone label),
 * never colour alone. One action max, supplied by the host.
 * Dismiss exists only where the host declares `dismissible` —
 * a state that must be faced gets no ×.
 * Integration: append inside @layer c29.components in tide.css.
 * ============================================================ */
c29-banner { display: flex; gap: var(--s3); align-items: flex-start; text-align: left;
  background: var(--surface); border-radius: var(--corner); box-shadow: var(--hair);
  padding: var(--s4); grid-template-columns: none; }
c29-banner[hidden] { display: none; }
c29-banner .bn-dot { flex: none; width: 28px; height: 28px; border-radius: 50%; display: grid; place-items: center; background: var(--wash); color: var(--ink-soft); }
c29-banner .bn-dot .t-icon { width: 16px; height: 16px; }
c29-banner[tone="accent"] .bn-dot { background: var(--accent); color: var(--on-accent); }
c29-banner[tone="ok"] { background: var(--ok-bg); } c29-banner[tone="ok"] .bn-dot { background: var(--ok); color: var(--on-ok); }
c29-banner[tone="warn"] { background: var(--warn-bg); } c29-banner[tone="warn"] .bn-dot { background: var(--warn); color: var(--surface); }
c29-banner[tone="bad"] { background: var(--bad-bg); } c29-banner[tone="bad"] .bn-dot { background: var(--bad); color: var(--on-bad); }
c29-banner[tone="ok"] .bn-title, c29-banner[tone="ok"] .bn-text { color: var(--ok-text); }
c29-banner[tone="warn"] .bn-title, c29-banner[tone="warn"] .bn-text { color: var(--warn-text); }
c29-banner[tone="bad"] .bn-title, c29-banner[tone="bad"] .bn-text { color: var(--bad-text); }
c29-banner .bn-main { flex: 1; min-width: 0; display: grid; gap: 2px; }
c29-banner .bn-title { font-weight: 650; font-size: 15px; line-height: 1.4; overflow-wrap: anywhere; }
c29-banner .bn-text { font-size: var(--fs-sm); line-height: 1.5; color: var(--ink-soft); overflow-wrap: anywhere; }
c29-banner .bn-text:empty { display: none; }
c29-banner .bn-actions { flex: none; display: flex; align-items: center; gap: var(--s1); }
c29-banner .bn-x { width: 36px; height: 36px; min-width: 36px; padding: 0; }
c29-banner .bn-x .t-icon { width: 16px; height: 16px; }
@media (max-width: 400px) {
  /* Phone width: the words get the full row — dot + text on the
     first line, the action wraps under the text instead of
     squeezing it into a column. */
  c29-banner { flex-wrap: wrap; }
  c29-banner .bn-main { flex: 1 1 calc(100% - 44px); }
  c29-banner .bn-actions { margin-left: 44px; }
}
/* ============================================================
 * TIDE — LAUNCHER TILE  <c29-tile>  +  .tl-grid
 * Wave 1 Lane C (C1), Taste Fleet 2026-10-19.
 *
 * Provenance: census item 3 / CG-2 — the Hub launcher tile
 * (.hub-tile, a fork of .ds-card-clickable with grey monogram
 * squares, no lift, no art slot) and the hand-rolled apps-shelf
 * grid are the same shape invented twice. This is the one
 * canonical shape for both.
 *
 * Construction law (canon): the tile's layout and chrome are
 * PURE CSS on the host-authored anchor — the tile works, links
 * and reads finished with tide.js / tile.js dead. JS only adds
 * the `open` event and the attribute-form adoption. Tokens
 * only; no literal colours except the on-* pairs canon itself
 * pins. Motion: a whisper of lift on fine-pointer hover only;
 * touch gets a wash, never travel; the global motion gate
 * (tokens.css duration zeroing) collapses what little moves.
 * INTEGRATION: append inside @layer c29.components in tide.css.
 * ============================================================ */

/* ---------- the shelf: group-grid layout ---------- */
.tl-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s2); }
@media (max-width: 340px) { .tl-grid { grid-template-columns: minmax(0, 1fr); } }

/* ---------- the stack variant: .tl-grid.is-stack (opt-in) ----------
 * Copycat R1 redo, 2026-10-28 (lanes/R1-redo-critic.md, Ruling 1).
 * The text-first shelf, following canon's opt-in variant precedent
 * (.t-seg.seg-wide): a host opts in with one class on the shelf,
 * never global, and the default shelf above is untouched.
 * Why it exists, by arithmetic: at phone width a 2-across tile in a
 * 342px content column is 167px wide, so its prose column caps at
 * 167 - 32 padding = 135px even with the art stacked — under the
 * Squish Law floor (160px / 45% of container) by construction. A
 * text-first tile (name + sentence + mechanism line) is therefore a
 * one-column shape at 390px. In this variant .tl-hit is a column:
 * the 52px .tl-art block sits ABOVE .tl-main, so no side furniture
 * ever shares the prose row — the Squish defence is structural.
 * Prose guarantee (registry contract): prose column = tile width
 * - 2 * --s4 padding, by construction (310px in a 342px column).
 * The stack tile does NOT inherit the default min-height stack
 * (calc(71px + 2 * --s4) is the row tile's name+sub+gate stack);
 * its height is its content height. c29-skeleton shape=tile parity
 * is pinned to the default tile's metrics and does NOT extend to
 * this variant until the skeleton learns it (named Tide debt). */
.tl-grid.is-stack { grid-template-columns: minmax(0, 1fr); }
.tl-grid.is-stack c29-tile .tl-hit { flex-direction: column; align-items: stretch; min-height: 0; }

/* ---------- the stack variant, non-door form (Copycat R3,
 * 2026-10-28, lanes/R3-critic.md Ruling 5, Fight 2) ----------
 * A stack tile that goes nowhere authors .tl-hit as a <div>; in
 * the stack shelf a non-anchor hit carries NO door affordances:
 * no hover lift and no :active wash — affordances bind to
 * a.tl-hit only. The construction reuses the disabled tile's own
 * undressing idiom below (var(--surface) on :active, transform:
 * none + var(--hair) on hover). Anchor hits in a stack shelf
 * keep lift and wash: a stack shelf of real doors stays
 * possible. tide.js matches this: C29Tile wires the `open`
 * click listener only when the hit is an anchor. */
.tl-grid.is-stack c29-tile div.tl-hit:active { background: var(--surface); }
@media (hover: hover) and (pointer: fine) {
  .tl-grid.is-stack c29-tile div.tl-hit:hover { transform: none; box-shadow: var(--hair); }
}

/* ---------- the tile ---------- */
c29-tile { display: block; min-width: 0; height: 100%; }
c29-tile .tl-hit { display: flex; align-items: center; gap: var(--s3); height: 100%;
  /* Pinned to the full name+sub+gate stack (71px: 20 name + 18 sub
     + 24 gate + 4 of grid gaps + 4 gate margin + rounding) + padding,
     so a gated and an ungated tile are the same height in the shelf —
     and c29-skeleton shape=tile stands in at this same metric. */
  min-height: calc(71px + 2 * var(--s4));
  padding: var(--s4); border-radius: var(--corner);
  background: var(--surface); box-shadow: var(--hair);
  color: inherit; text-decoration: none; -webkit-tap-highlight-color: transparent;
  transition: transform var(--d2) var(--ease), box-shadow var(--d2) var(--ease), background var(--d2) var(--ease); }
c29-tile .tl-hit:active { background: var(--wash); }
/* The whisper: fine pointers only, 2px, no scale theatre. */
@media (hover: hover) and (pointer: fine) {
  c29-tile .tl-hit:hover { transform: translateY(-2px); box-shadow: var(--lift); }
}
c29-tile .tl-hit:focus-visible { outline: none; box-shadow: var(--focus); }

/* Art slot: a real image, an inline SVG, or a monogram letter.
   One ground, one radius (concentric: corner − padding step). */
c29-tile .tl-art { flex: none; width: 52px; height: 52px; border-radius: calc(var(--corner) - 6px);
  display: grid; place-items: center; overflow: hidden;
  background: var(--accent-soft); color: var(--accent-text);
  font: 800 21px/1 var(--font-display); letter-spacing: var(--track-head); }
c29-tile .tl-art img { width: 100%; height: 100%; object-fit: cover; }
c29-tile .tl-art svg { width: 26px; height: 26px; }
c29-tile .tl-art.is-money { background: color-mix(in srgb, var(--money) 12%, var(--surface)); color: var(--money-text); }
c29-tile .tl-art.is-sun { background: var(--sun-100); color: var(--warn-text); }
[data-theme="dark"] c29-tile .tl-art.is-sun { background: var(--warn-bg); }

c29-tile .tl-main { flex: 1 1 auto; min-width: 0; display: grid; gap: 2px; align-content: center; }
c29-tile .tl-name { font: 700 15.5px/1.3 var(--font-display); letter-spacing: var(--track-head);
  overflow-wrap: anywhere; }
c29-tile .tl-sub { font-size: 13px; line-height: 1.4; color: var(--ink-mute); overflow-wrap: anywhere; }

/* Gate tag: sits under the words, never crowding the art. It is
   a fact about the door (Gated / Open / Pro), set in the house
   tag voice — word, not colour alone. */
c29-tile .tl-gate { justify-self: start; margin-top: var(--s1); }

/* Disabled: the door exists but is not openable today. JS
   removes it from the tab order; CSS carries the look alone. */
c29-tile[disabled] .tl-hit { opacity: .55; cursor: default; }
c29-tile[disabled] .tl-hit:active { background: var(--surface); }
@media (hover: hover) and (pointer: fine) {
  c29-tile[disabled] .tl-hit:hover { transform: none; box-shadow: var(--hair); }
}
/* ============================================================
 * TIDE — LEAVING TILE  <c29-extlink>  (the external-link square)
 * Fleet 4, 2026-10-23 — Victor's iframe law: no new square may
 * require an iframe; a square that cannot be native is an
 * external link, marked so clearly no one mistakes it for a
 * room. Provenance: the framed-room pattern this replaces let
 * a foreign page wear the shell's chrome and inherit its
 * trust. This tile is the honest exit, in the shelf's own
 * metrics (the same min-height stack as c29-tile, so a mixed
 * shelf stays even) with every meaning-signal flipped:
 * dashed rim for the solid hairline, the sunken well for the
 * accent cushion, an arrow that leaves for a tile that lifts.
 *
 * Construction law (canon, as c29-tile): layout and chrome are
 * PURE CSS on the host-authored anchor — the tile reads
 * finished with tide.js dead. JS only enforces the exit chip,
 * target/rel, the `leave` event, and attribute-form adoption.
 * Tokens only. Motion rides the system's one global gate
 * (tide.css, Rev 4 item 5): under reduced motion the travel
 * collapses — transform leaves the transitioned properties —
 * and the states land as instant paint, as everywhere in Tide.
 * INTEGRATION: append inside @layer c29.components in tide.css,
 * after the LAUNCHER TILE block.
 * ============================================================ */

c29-extlink { display: block; min-width: 0; height: 100%; }
c29-extlink .xl-hit { display: flex; align-items: center; gap: var(--s3); height: 100%;
  /* Same stack metric as .tl-hit (71px name+sub+tag + padding):
     a leaving tile and a room tile are the same height in the
     shelf, so the difference is read from the markings, never
     from the size. The rim is a real dashed border, 1.5px, in
     place of the room's solid --hair shadow. */
  min-height: calc(71px + 2 * var(--s4));
  padding: var(--s4); border-radius: var(--corner);
  border: 1.5px dashed var(--edge);
  background: var(--surface);
  color: inherit; text-decoration: none; -webkit-tap-highlight-color: transparent;
  transition: border-color var(--d2) var(--ease), background var(--d2) var(--ease), box-shadow var(--d2) var(--ease); }
c29-extlink .xl-hit:active { background: var(--wash); }
c29-extlink .xl-hit:active .xl-arrow { transform: translate(2px, -2px); }
/* No lift: lifting promises a room. The rim firms up and the
   arrow steps out — on fine pointers only, as the tile law. */
@media (hover: hover) and (pointer: fine) {
  c29-extlink .xl-hit:hover { border-color: color-mix(in srgb, var(--ink-3) 65%, transparent); }
  c29-extlink .xl-hit:hover .xl-arrow { transform: translate(2px, -2px); }
}
c29-extlink .xl-hit:focus-visible { outline: none; box-shadow: var(--focus); }

/* Art slot: same 52px geometry as .tl-art, but the ground is
   the sunken well and the monogram is quiet ink — this art is
   a label for somewhere else, not one of ours. */
c29-extlink .xl-art { flex: none; width: 52px; height: 52px; border-radius: calc(var(--corner) - 6px);
  display: grid; place-items: center; overflow: hidden;
  background: var(--sunken); color: var(--ink-soft);
  font: 800 21px/1 var(--font-display); letter-spacing: var(--track-head); }
c29-extlink .xl-art img { width: 100%; height: 100%; object-fit: cover; }
c29-extlink .xl-art svg { width: 26px; height: 26px; }

c29-extlink .xl-main { flex: 1 1 auto; min-width: 0; display: grid; gap: 2px; align-content: center; }
c29-extlink .xl-name { font: 700 15.5px/1.3 var(--font-display); letter-spacing: var(--track-head);
  overflow-wrap: anywhere; }
c29-extlink .xl-sub { font-size: 13px; line-height: 1.4; color: var(--ink-mute); overflow-wrap: anywhere; }

/* The exit chip: sits under the words, where c29-tile's gate
   tag sits — same shelf position, different fact. The word is
   the marking (never colour alone); the arrow is its glyph.
   Ink-soft on the sunken well is a text pair, held to the
   same 4.5:1 bar as any words a user must read (proof:
   extlink-pass.mjs samples it from the render, both themes). */
c29-extlink .xl-tag { justify-self: start; margin-top: var(--s1);
  display: inline-flex; align-items: center; gap: 5px; white-space: nowrap;
  min-height: 24px; padding: 0 9px; border-radius: 99px;
  background: var(--sunken); color: var(--ink-soft);
  font-size: 12px; font-weight: 600; }
c29-extlink .xl-arrow { width: 13px; height: 13px; flex: none;
  fill: none; stroke: currentColor; stroke-width: 2.4; stroke-linecap: round; stroke-linejoin: round;
  transition: transform var(--d2) var(--ease); }

/* Inline form (P1-B, 2026-10-24): the SAME chip, stamped by the
   shell's one shared helper (plMarkExt in Pleasant's app.js,
   built on this component's arrow SVG) into prose/rendered
   anchors that are not shelf tiles — a renderer link, a board
   door, a buy button. One chip, one meaning, everywhere: the
   declarations above, repeated for the helper's .pl-ext-inline
   stamp so no surface ever invents its own exit badge. */
.xl-tag.pl-ext-inline { display: inline-flex; align-items: center; gap: 5px; white-space: nowrap;
  min-height: 24px; padding: 0 9px; border-radius: 99px; margin-left: 6px; vertical-align: middle;
  background: var(--sunken); color: var(--ink-soft);
  font-size: 12px; font-weight: 600; }
.xl-tag.pl-ext-inline .xl-arrow { width: 13px; height: 13px; flex: none;
  fill: none; stroke: currentColor; stroke-width: 2.4; stroke-linecap: round; stroke-linejoin: round; }

/* Disabled: the exit exists but is not openable today — the
   same law as the tile (dimmed, out of the tab order, the
   arrow stays home). */
c29-extlink[disabled] .xl-hit { opacity: .55; cursor: default; }
c29-extlink[disabled] .xl-hit:active { background: var(--surface); }
c29-extlink[disabled] .xl-hit:active .xl-arrow { transform: none; }
@media (hover: hover) and (pointer: fine) {
  c29-extlink[disabled] .xl-hit:hover { border-color: var(--edge); }
  c29-extlink[disabled] .xl-hit:hover .xl-arrow { transform: none; }
}
/* ============================================================
 * TIDE — EMPTY STATE  <c29-empty>
 * Wave 1 Lane C (C2, states package with c29-skeleton).
 *
 * Provenance: census item 6 — emptiness in the Hub was forked
 * as .f2-absence at 5+ call sites plus .hub-empty because a
 * page-level absence had no Tide carrier (list/deck `empty`
 * attributes are list states, not a view with nothing in it).
 * Board bar: title + ONE honest sentence + AT MOST ONE action.
 *
 * Design ruling: an empty view is open sky, not a grey apology
 * box — no dashed ring, no inset border (the .ib-empty /
 * .ls-empty look this replaces). A soft sea-glass medallion,
 * a display line, one sentence, one action, generous air.
 * Chrome is pure CSS on host-authored slots (works JS-dead);
 * JS only adopts the attribute form. Tokens only.
 * INTEGRATION: append inside @layer c29.components in tide.css.
 * ============================================================ */

c29-empty { display: grid; justify-items: center; text-align: center;
  gap: var(--s2); padding: var(--s8) var(--s5); }
c29-empty .em-mark { width: 56px; height: 56px; border-radius: 50%;
  display: grid; place-items: center; margin-bottom: var(--s1);
  background: var(--accent-soft); color: var(--accent-text); }
c29-empty .em-mark svg { width: 26px; height: 26px; }
c29-empty .em-mark .em-mono { font: 800 22px/1 var(--font-display); letter-spacing: var(--track-head); }
c29-empty [slot="title"], c29-empty .em-title { font: 750 var(--fs-xl)/1.2 var(--font-display);
  letter-spacing: var(--track-head); text-wrap: balance; }
c29-empty [slot="body"], c29-empty .em-body { font-size: 15px; line-height: 1.55;
  color: var(--ink-soft); max-width: 36ch; text-wrap: pretty; }
c29-empty [slot="action"], c29-empty .em-action { margin-top: var(--s2); }
/* ============================================================
 * TIDE — SKELETON  <c29-skeleton>
 * Wave 1 Lane C (C2, states package with c29-empty).
 *
 * Provenance: census item 7 — the .t-skel shimmer existed as
 * an unregistered gallery demo while the Hub shipped ZERO
 * skeletons (loading was bare text: "Checking this machine.").
 * This registers the primitive with shapes that stand in for
 * real canon shapes, at the SAME metrics, so the swap to loaded
 * content causes NO layout jump:
 *   tile      103px = c29-tile min-height calc(71px + 2*--s4)
 *   row       65px  = the standard two-line c29-list row
 *             (20px title line + 3px gap + 18px sub + 24px pad)
 *   settings  60px  = c29-settings row min-height, in its card
 *   checklist 64px  = c29-checklist row min-height, in its card
 *   card      --sk-h (default 132px), text = body-rhythm lines
 * The blocks are canon .t-skel (sunken ground + the slow wave);
 * shimmer period is the token --shimmer-period (3s, slow and
 * low-contrast). Under the motion gate the canon collapse
 * zeroes the wave; the extra rule below pins it fully static
 * so a reduced-motion skeleton is a still shape, not a flicker.
 * JS-dead posture: an unrendered <c29-skeleton> still holds its
 * shape's height in sunken ground — the space is reserved even
 * if the detailed blocks never stamp.
 * INTEGRATION: append inside @layer c29.components in tide.css.
 * ============================================================ */

c29-skeleton { display: block; }
c29-skeleton .sk-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s2); }
@media (max-width: 340px) { c29-skeleton .sk-grid { grid-template-columns: minmax(0, 1fr); } }
c29-skeleton .sk-stack { display: grid; gap: var(--s2); }
c29-skeleton .sk-card { background: var(--surface); border-radius: var(--corner); box-shadow: var(--hair); overflow: hidden; }
/* Lines are grids with the bar centered — never margins, which
   collapse out of the fixed-height line and break the metric. */
c29-skeleton .sk-line { height: 60px; display: grid; align-items: center; padding: 0 var(--s4); }
c29-skeleton .sk-line + .sk-line { box-shadow: inset 0 1px 0 var(--edge-soft); }
c29-skeleton .sk-check-line { height: 64px; display: grid; align-items: center; padding: 0 var(--s4); }
c29-skeleton .sk-check-line + .sk-check-line { box-shadow: inset 0 1px 0 var(--edge-soft); }
c29-skeleton .sk-tile { height: calc(71px + 2 * var(--s4)); border-radius: var(--corner); }
c29-skeleton .sk-row { height: 65px; border-radius: var(--corner); }
c29-skeleton .sk-text { display: grid; gap: 10px; padding: 5px 0; }
c29-skeleton .sk-text .t-skel { height: 14px; }

/* Reserved-space fallback before/without JS (see header). */
c29-skeleton[shape="tile"]:empty { min-height: calc(71px + 2 * var(--s4)); border-radius: var(--corner); background: var(--sunken); }
c29-skeleton[shape="row"]:empty { min-height: 65px; border-radius: var(--corner); background: var(--sunken); }
c29-skeleton[shape="card"]:empty { min-height: var(--sk-h, 132px); border-radius: var(--corner); background: var(--sunken); }

/* Reduced motion: the skeleton is a still shape. */
html[data-motion="reduced"] c29-skeleton .t-skel::after { animation: none; }
@media (prefers-reduced-motion: reduce) {
  html:not([data-motion="full"]) c29-skeleton .t-skel::after { animation: none; }
}
/* ============================================================
 * TIDE — SETTINGS ROWS  <c29-settings>
 * Wave 1 Lane C (C3).
 *
 * Provenance: census item 17 — the Hub has NO settings view at
 * all (audit S13/M1): sign-out hides inside Ceremony step 2,
 * theme is a top-bar text button, update state lives in a shim
 * pill. DS v2 carries a full .ds-settings-* family used
 * nowhere; this is Tide's living version.
 *
 * The five row shapes — nothing in the family invents a sixth:
 *   nav row      <a class="st-row">    label + value + chevron
 *   toggle row   <div class="st-row">  label + a real .t-switch
 *   select row   <div class="st-row">  label + a real <select>
 *   action row   <button class="st-row"> label (+ value), no chevron
 *   info row     <div class="st-row">  label + value, not interactive
 * plus two earned modifiers: .is-destructive (sign-out, delete)
 * and a .st-detail second line under a label.
 * Rows sit in .st-group cards under .st-group-title caps; the
 * identity block above them is c29-account's job, not this
 * family's. Chrome is pure CSS (works JS-dead); JS only wires
 * the change/setting events for toggle/select/action rows.
 * Row height is pinned at 60px — c29-skeleton shape=settings
 * stands in at exactly this metric.
 * INTEGRATION: append inside @layer c29.components in tide.css.
 * ============================================================ */

c29-settings { display: grid; gap: var(--s5); }
c29-settings .st-group-title { font-size: var(--fs-cap); letter-spacing: var(--track-cap);
  text-transform: uppercase; font-weight: 650; color: var(--ink-mute);
  margin: 0 0 var(--s2) var(--s1); }
c29-settings .st-group { background: var(--surface); border-radius: var(--corner);
  box-shadow: var(--hair); overflow: hidden; }

c29-settings .st-row { display: flex; align-items: center; gap: var(--s3);
  width: 100%; min-height: 60px; padding: var(--s2) var(--s4);
  font: inherit; color: inherit; text-align: left; text-decoration: none;
  background: none; border: 0; }
c29-settings .st-group .st-row + .st-row { box-shadow: inset 0 1px 0 var(--edge-soft); }
a.st-row, button.st-row { cursor: pointer; -webkit-tap-highlight-color: transparent;
  transition: background var(--d2) var(--ease); }
a.st-row:active, button.st-row:active { background: var(--wash); }
a.st-row:focus-visible, button.st-row:focus-visible { outline: none; box-shadow: var(--focus); }

c29-settings .st-ic { flex: none; width: 34px; height: 34px; border-radius: 10px;
  display: grid; place-items: center; background: var(--accent-soft); color: var(--accent-text); }
c29-settings .st-ic svg { width: 18px; height: 18px; }
c29-settings .st-main { flex: 1 1 auto; min-width: 0; display: grid; gap: 1px; }
c29-settings .st-label { font-size: 15.5px; font-weight: 600; line-height: 1.35; overflow-wrap: anywhere; }
c29-settings .st-detail { font-size: 13px; line-height: 1.4; color: var(--ink-mute); overflow-wrap: anywhere; }
c29-settings .st-value { flex: none; max-width: 46%; font-size: 14px; color: var(--ink-mute);
  text-align: right; overflow-wrap: anywhere; }
c29-settings .st-chev { flex: none; color: var(--ink-mute); display: grid; place-items: center; }
c29-settings .st-chev svg { width: 18px; height: 18px; }

/* Controls keep their own canon shapes — the switch is .t-switch,
   the select is a native select in quiet dress (44px target). */
c29-settings .st-row .t-switch { flex: none; }
c29-settings .st-select { flex: none; max-width: 52%; min-height: 44px;
  border: 0; background: transparent; color: var(--ink-mute);
  font-size: 14px; text-align: right; cursor: pointer; }
c29-settings .st-select:focus-visible { outline: none; box-shadow: var(--focus); border-radius: 8px; }

/* Destructive is a tone, earned by the act — never a red box. */
c29-settings .st-row.is-destructive .st-label { color: var(--bad-text); }
c29-settings .st-row.is-destructive .st-ic { background: var(--bad-bg); color: var(--bad-text); }

/* Disabled row: present, legible, not pressable. */
c29-settings .st-row[disabled], c29-settings .st-row[aria-disabled="true"] { opacity: .55; cursor: default; }
c29-settings .st-row[aria-disabled="true"]:active { background: none; }
/* ============================================================
 * TIDE — STATUS CHECKLIST  <c29-checklist>
 * Wave 1 Lane C (C4).
 *
 * Provenance: census item 10 — the Hub Ceremony's check rows
 * are text-only ("Lives in the desktop app" ×3 in muted text,
 * no difference between checked-fine, not-applicable, and
 * missing) at the exact moment the product claims "we checked
 * before asking". Permit Runner's desk and Learn onboarding
 * hand-composed the same shape. This is its canonical carrier.
 *
 * The law of the row: a state is carried THREE ways at once —
 * SHAPE (filled check disc / slashed hollow disc / warning
 * triangle / hollow ring), WORD (a visible status word in the
 * status's own chip), and TONE. Colour alone never carries it:
 * strip the colour and the rows still read differently.
 *   done     ✓ disc, filled ok        word "Done"
 *   na       ⊘ hollow disc + slash    word "Not needed"
 *   missing  ⚠ triangle, bad tone     word "Missing"
 *   pending  ○ hollow ring            word "Not checked"
 * Row layout is pure CSS on host <li data-status> children —
 * with JS dead the words come from the host's own .cl-word and
 * the tone/shape ring still paints (li::before per status);
 * JS stamps the SVG icon + word only where the host left them
 * out, and computes the optional summary line. Row height
 * pinned at 64px — c29-skeleton shape=checklist matches it.
 * INTEGRATION: append inside @layer c29.components in tide.css.
 * NOTE the existing .t-check (a checkbox row) is a different
 * truth — an input, not a verified state — and stays untouched.
 * ============================================================ */

c29-checklist { display: block; }
c29-checklist .cl-summary { display: flex; align-items: center; gap: var(--s2);
  margin-bottom: var(--s3); font-size: 14px; font-weight: 600; color: var(--ink-soft); }
c29-checklist .cl-card { background: var(--surface); border-radius: var(--corner);
  box-shadow: var(--hair); overflow: hidden; }
c29-checklist > ul, c29-checklist .cl-card > ul { list-style: none; margin: 0; padding: 0; }
c29-checklist li { list-style: none; }
c29-checklist li::marker { content: none; }
c29-checklist li { position: relative; display: flex; align-items: center; gap: var(--s3);
  min-height: 64px; padding: var(--s2) var(--s4); }
c29-checklist li + li { box-shadow: inset 0 1px 0 var(--edge-soft); }

c29-checklist .cl-ic { flex: none; width: 30px; height: 30px; display: grid; place-items: center; }
c29-checklist .cl-ic svg { width: 26px; height: 26px; fill: none; stroke: currentColor;
  stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round; }
c29-checklist li[data-status="done"] .cl-ic { color: var(--ok-text); }
c29-checklist li[data-status="done"] .cl-ic .cl-disc { fill: var(--ok); stroke: none; }
c29-checklist li[data-status="done"] .cl-ic .cl-tick { stroke: var(--on-ok); }
c29-checklist li[data-status="na"] .cl-ic { color: var(--ink-mute); }
c29-checklist li[data-status="missing"] .cl-ic { color: var(--bad-text); }
c29-checklist li[data-status="missing"] .cl-ic .cl-tri { fill: var(--bad-bg); }
c29-checklist li[data-status="pending"] .cl-ic { color: var(--ink-mute); }

/* JS-dead shape fallback: a status ring paints per row even
   before the icon stamps (tone + ring weight differ per state). */
c29-checklist li:not(:has(.cl-ic))::before { content: ""; flex: none; width: 26px; height: 26px;
  border-radius: 50%; box-shadow: inset 0 0 0 2px var(--ink-mute); }
c29-checklist li[data-status="done"]:not(:has(.cl-ic))::before { background: var(--ok); box-shadow: none; }
c29-checklist li[data-status="missing"]:not(:has(.cl-ic))::before { border-radius: 6px;
  background: var(--bad-bg); box-shadow: inset 0 0 0 2px var(--bad); }

c29-checklist .cl-main { flex: 1 1 auto; min-width: 0; display: grid; gap: 1px; }
c29-checklist .cl-label { font-size: 15.5px; font-weight: 600; line-height: 1.35; overflow-wrap: anywhere; }
c29-checklist .cl-detail { font-size: 13px; line-height: 1.4; color: var(--ink-mute); overflow-wrap: anywhere; }
c29-checklist li[data-status="na"] .cl-label { color: var(--ink-soft); font-weight: 550; }

/* The word chip: the status SAID, in its paired tone tokens. */
c29-checklist .cl-word { flex: none; display: inline-flex; align-items: center; min-height: 24px;
  padding: 0 9px; border-radius: 99px; background: var(--wash); color: var(--ink-soft);
  font-size: 12px; font-weight: 650; white-space: nowrap; }
c29-checklist li[data-status="done"] .cl-word { background: var(--ok-bg); color: var(--ok-text); }
c29-checklist li[data-status="missing"] .cl-word { background: var(--bad-bg); color: var(--bad-text); }
c29-checklist li[data-status="na"] .cl-word { background: var(--wash); color: var(--ink-mute); }

c29-checklist .cl-act { flex: none; }
/* ======================================================
   PROPOSAL  <c29-proposal>   (Lane D · D1, Taste Wave 1)
   A machine-proposed action a calm human can approve in one
   read: what will happen (payload, plainly), why (rationale),
   who proposed it, approve / deny as real actions, and a
   decided state that keeps its result + provenance forever.
   Tokens only; layout is pure CSS on the host element so the
   card holds its shape even before JS adopts it.
   ====================================================== */
c29-proposal { display: block; background: var(--surface); border-radius: var(--corner); box-shadow: var(--hair); padding: var(--s5); }
c29-proposal .pp-kind { display: flex; align-items: center; gap: 8px; font-size: 12px; font-weight: 650; letter-spacing: .05em; text-transform: uppercase; color: var(--ink-mute); }
c29-proposal .pp-kind i { width: 8px; height: 8px; border-radius: 50%; background: var(--accent); flex: none; }
c29-proposal .pp-title { margin-top: var(--s2); font: 750 var(--fs-xl)/1.22 var(--font-display); letter-spacing: var(--track-head); overflow-wrap: anywhere; }
c29-proposal .pp-why { margin-top: var(--s2); color: var(--ink-soft); font-size: 15px; line-height: 1.55; overflow-wrap: anywhere; }
c29-proposal .pp-why:empty { display: none; }
/* The payload: what will actually happen, rendered plainly.
   Sunken and quiet — background truth, never an alarm, and
   never raw-JSON-only: hosts compose readable lines here. */
c29-proposal .pp-payload { margin-top: var(--s4); background: var(--sunken); border-radius: var(--r-md); box-shadow: inset 0 0 0 1px var(--edge-soft);
  padding: var(--s3) var(--s4); font-size: 14.5px; line-height: 1.55; color: var(--ink); overflow-wrap: anywhere; }
c29-proposal .pp-payload:empty { display: none; }
c29-proposal .pp-payload-cap { display: block; font-size: 11px; font-weight: 700; letter-spacing: .09em; text-transform: uppercase; color: var(--ink-mute); margin-bottom: 4px; }
c29-proposal .pp-empty { margin-top: var(--s4); font-size: 14px; color: var(--ink-mute); border-radius: var(--r-md);
  box-shadow: inset 0 0 0 1.5px var(--edge); padding: var(--s3) var(--s4); }
c29-proposal .pp-meta { margin-top: var(--s3); display: flex; align-items: center; gap: 8px; flex-wrap: wrap; font-size: 13px; color: var(--ink-mute); }
c29-proposal .pp-acts { margin-top: var(--s4); display: grid; grid-template-columns: 1fr 1fr; gap: var(--s2); }
c29-proposal .pp-acts .t-btn { width: 100%; }
/* Decided state: the card keeps its payload + rationale and
   gains a provenance band. Colour is never the only signal —
   the band states the decision in words. */
c29-proposal .pp-decided { margin-top: var(--s4); display: none; align-items: flex-start; gap: var(--s2);
  border-radius: var(--r-md); padding: var(--s3) var(--s4); font-size: 14px; line-height: 1.5; }
c29-proposal .pp-decided .t-icon { width: 18px; height: 18px; flex: none; margin-top: 2px; }
c29-proposal .pp-decided b { font-weight: 700; }
c29-proposal .pp-decided .pp-result { display: block; color: var(--ink-soft); overflow-wrap: anywhere; }
c29-proposal[data-state="approved"] .pp-decided { display: flex; background: var(--ok-bg); color: var(--ok-text); }
c29-proposal[data-state="denied"] .pp-decided { display: flex; background: var(--bad-bg); color: var(--bad-text); }
c29-proposal[data-state="approved"] .pp-decided .pp-result,
c29-proposal[data-state="denied"] .pp-decided .pp-result { color: inherit; opacity: .85; }
c29-proposal[data-state="approved"] .pp-acts, c29-proposal[data-state="denied"] .pp-acts { display: none; }
c29-proposal[disabled] .pp-acts .t-btn { opacity: .45; pointer-events: none; }
/* ======================================================
   CHECK + CITE  <c29-check> · <c29-cite>   (Lane D · D2)
   The Learn pair. Check: a question whose options are real
   buttons; the verdict + explanation land IN PLACE the instant
   an option is tapped. Cite: a small chip that opens to the
   exact quoted words it cites. Tokens only, both themes.
   ====================================================== */
c29-check { display: block; background: var(--surface); border-radius: var(--corner); box-shadow: var(--hair); padding: var(--s5); }
c29-check .ck-q { font: 700 var(--fs-lg)/1.3 var(--font-display); letter-spacing: var(--track-head); overflow-wrap: anywhere; }
c29-check .ck-opts { display: grid; gap: var(--s2); margin-top: var(--s4); }
c29-check .ck-opt { display: flex; align-items: center; gap: var(--s3); width: 100%; min-height: 52px; text-align: left;
  padding: var(--s3) var(--s4); border: 0; border-radius: var(--r-md); background: var(--raised);
  box-shadow: 0 0 0 1px var(--edge); font-size: 15.5px; font-weight: 550; color: var(--ink); cursor: pointer;
  transition: background var(--d2) var(--ease), box-shadow var(--d2) var(--ease), transform var(--d1) var(--ease); overflow-wrap: anywhere; }
c29-check .ck-opt:hover { background: var(--wash); }
c29-check .ck-opt:active { transform: scale(.99); }
c29-check .ck-opt .ck-mark { flex: none; width: 26px; height: 26px; border-radius: 50%; display: grid; place-items: center;
  background: var(--wash); color: var(--ink-soft); font: 700 13px/1 var(--font-ui); }
  /* Integrator fix (2026-10-19, post-landing render pass): the letter was --ink-mute, which samples 4.02:1 in dark on the wash disc — under the 4.5 bar. --ink-soft samples ≈6.6:1 in dark and stronger in light. */
c29-check .ck-opt .ck-mark .t-icon { width: 15px; height: 15px; }
c29-check .ck-opt[disabled] { cursor: default; }
c29-check[data-state="correct"] .ck-opt.is-picked { background: var(--ok-bg); box-shadow: 0 0 0 1.5px var(--ok); color: var(--ok-text); }
c29-check[data-state="correct"] .ck-opt.is-picked .ck-mark { background: var(--ok); color: var(--on-ok); }
c29-check .ck-opt.is-wrong { background: var(--bad-bg); box-shadow: 0 0 0 1.5px var(--bad); color: var(--bad-text); }
c29-check .ck-opt.is-wrong .ck-mark { background: var(--bad); color: var(--on-bad); }
c29-check .ck-opt.is-dim { opacity: .55; }
c29-check .ck-fb { margin-top: var(--s4); display: none; gap: var(--s2); border-radius: var(--r-md); padding: var(--s3) var(--s4); font-size: 14.5px; line-height: 1.55; }
c29-check .ck-fb .t-icon { width: 18px; height: 18px; flex: none; margin-top: 2px; }
c29-check .ck-fb b { font-weight: 700; }
c29-check .ck-fb .ck-why { display: block; color: inherit; opacity: .88; overflow-wrap: anywhere; }
c29-check[data-state="correct"] .ck-fb { display: flex; background: var(--ok-bg); color: var(--ok-text); }
c29-check[data-state="incorrect"] .ck-fb { display: flex; background: var(--bad-bg); color: var(--bad-text); }
c29-check .ck-loop { margin-top: var(--s3); font-size: 13px; color: var(--ink-mute); }
c29-check .ck-loop:empty { display: none; }

/* ---------- cite ---------- */
c29-cite { display: inline-block; max-width: 100%; vertical-align: baseline; }
c29-cite .ct-chip { display: inline-flex; align-items: center; gap: 6px; min-height: 32px; max-width: 100%; padding: 2px 12px;
  border: 0; border-radius: 99px; background: var(--accent-soft); color: var(--accent-text);
  font-size: 13px; font-weight: 650; cursor: pointer; transition: background var(--d2) var(--ease), transform var(--d1) var(--ease); }
c29-cite .ct-chip:active { transform: scale(.97); }
c29-cite .ct-chip .t-icon { width: 14px; height: 14px; flex: none; }
c29-cite .ct-chip span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
c29-cite .ct-chip[disabled] { background: var(--wash); color: var(--ink-mute); cursor: default; }
c29-cite .ct-quote { display: none; margin-top: var(--s2); max-width: min(480px, 88vw); background: var(--sunken);
  border-radius: var(--r-md); box-shadow: inset 0 0 0 1px var(--edge-soft); padding: var(--s3) var(--s4); }
c29-cite.is-open .ct-quote { display: block; }
c29-cite .ct-quote blockquote { margin: 0; font-size: 14.5px; line-height: 1.6; color: var(--ink);
  overflow-wrap: anywhere; max-height: 220px; overflow: auto; }
c29-cite .ct-quote blockquote::before { content: "“"; } c29-cite .ct-quote blockquote::after { content: "”"; }
c29-cite .ct-src { margin-top: var(--s2); font-size: 12.5px; color: var(--ink-mute); }

} /* end components */

@layer c29.utilities {
  .vh { position: absolute !important; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
}

/* ---------- reduced motion: ONE GLOBAL GATE (Rev 4 item 5) ----------
   Keep states, lose the travel; springs collapse to fades. The
   effective mode is resolved ONCE by tide.js — a stored user
   override (sh29core_motion) wins over the OS in either direction;
   no stored override follows the OS — and published as
   <html data-motion="reduced"|"full">. The media-query block below
   therefore stands down the moment the user has explicitly chosen
   "full", and the [data-motion="reduced"] block applies the same
   collapse when the user chose reduced against a full-motion OS.
   The duration scale itself is zeroed at the token layer
   (tokens.css), and every JS-driven animation consults
   Tide.reduced() — the gate is a system feature, not a per-surface
   negotiation. */
@media (prefers-reduced-motion: reduce) {
  html:not([data-motion="full"]) *, html:not([data-motion="full"]) *::before, html:not([data-motion="full"]) *::after { animation-duration: 1ms !important; animation-iteration-count: 1 !important; scroll-behavior: auto !important;
    transition-property: opacity, background-color, color, box-shadow !important; transition-duration: 160ms !important; }
  html:not([data-motion="full"]) .t-shimmer, html:not([data-motion="full"]) c29-slide .sl-label span { animation: none !important; color: var(--ink-soft) !important; background: none !important; }
  @view-transition { navigation: none; }
}
html[data-motion="reduced"] *, html[data-motion="reduced"] *::before, html[data-motion="reduced"] *::after { animation-duration: 1ms !important; animation-iteration-count: 1 !important; scroll-behavior: auto !important;
  transition-property: opacity, background-color, color, box-shadow !important; transition-duration: 160ms !important; }
html[data-motion="reduced"] .t-shimmer, html[data-motion="reduced"] c29-slide .sl-label span { animation: none !important; color: var(--ink-soft) !important; background: none !important; }

/* ============================================================
 * REV 2 — mined from the AccountabilityPals First-Name pages
 * (PALS-INVENTORY-2026-10-17.md). Patterns generalized, costumes
 * left behind; every value a token.
 * ============================================================ */

/* ---------- c29-sweep + c29-sweep-entry: the dated update board ----------
   Rev 4 overhaul (2026-10-18; the Principal's steering: "the sweep is
   shit and needs a full overhaul"). A timeline board: date medallions
   ride a rail, facts read as a spec sheet, and today is the one coral
   day. The date state machine is unchanged — past fades + Done, today
   is coral + Today, an all-past board declares itself stale — only
   the presentation was rebuilt, a full tier up.
   Every value below is a real token. The Rev 2 block named tokens
   that never existed (--hairline, --shadow-pop, --r-lg, a rise/fadein
   keyframe pair), so its borders, shadows and radii silently never
   rendered; that, not taste, is why the trio read flat. */
c29-sweep { display: block; }
c29-sweep .sw-head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--s3); }
c29-sweep .sw-head h3 { font: 700 19px/1.2 var(--font-display); letter-spacing: -.01em; }
c29-sweep .sw-range { flex: none; margin-top: 1px; }
c29-sweep .sw-checked { display: flex; align-items: center; gap: 6px; margin-top: var(--s1); font-size: 12.5px; color: var(--ink-mute); }
c29-sweep .sw-checked .t-icon { width: 14px; height: 14px; flex: none; }
c29-sweep .sw-entries { position: relative; margin-top: var(--s4); display: grid; gap: var(--s4); }
c29-sweep .sw-entries::before { content: ""; position: absolute; top: 12px; bottom: 12px; left: 26px; width: 2px; border-radius: 2px;
  background: color-mix(in srgb, var(--ink-1) 9%, transparent); }
c29-sweep-entry { position: relative; display: grid; grid-template-columns: 54px minmax(0, 1fr); gap: var(--s3); }
c29-sweep-entry .se-medal { position: relative; z-index: 1; width: 54px; height: 54px; border-radius: 17px;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  background: var(--sunken); color: var(--ink-soft); box-shadow: inset 0 0 0 1px var(--edge-soft); }
c29-sweep-entry .se-medal b { font: 800 19px/1 var(--font-display); letter-spacing: -.01em; }
c29-sweep-entry .se-medal span { margin-top: 3px; font-size: 9.5px; font-weight: 700; letter-spacing: .1em; text-transform: uppercase; }
c29-sweep-entry .se-medal .t-icon { width: 22px; height: 22px; }
c29-sweep-entry .se-main { min-width: 0; padding-top: 3px; }
c29-sweep-entry .se-top { display: flex; align-items: baseline; gap: var(--s2); }
c29-sweep-entry .se-title { font-weight: 700; font-size: 15.5px; line-height: 1.3; }
c29-sweep-entry .se-tag { margin-left: auto; flex: none; }
c29-sweep-entry .se-when { margin-top: 1px; font-size: 13px; font-weight: 650; color: var(--ink-soft); }
c29-sweep-entry .se-body { margin-top: 3px; font-size: 14px; line-height: 1.55; color: var(--ink-2); }
c29-sweep-entry .se-kvs { margin-top: var(--s2); }
c29-sweep-entry .se-kv { display: flex; gap: var(--s3); padding: 6px 0; border-top: 1px solid var(--edge-soft); font-size: 13.5px; line-height: 1.45; }
c29-sweep-entry .se-kv:last-child { border-bottom: 1px solid var(--edge-soft); }
c29-sweep-entry .se-kv b { flex: none; min-width: 48px; padding-top: 2px; font-size: 10.5px; font-weight: 700;
  letter-spacing: .08em; text-transform: uppercase; color: var(--ink-mute); }
c29-sweep-entry .se-src { margin-top: var(--s2); font-size: 13px; font-weight: 650; }
c29-sweep-entry .se-src a { text-decoration: none; }
c29-sweep-entry[data-state="past"] { opacity: .6; }
c29-sweep-entry[data-state="past"] .se-medal { background: transparent; }
c29-sweep-entry[data-state="today"] .se-medal { background: linear-gradient(155deg, var(--29d-coral-500), var(--money));
  color: var(--on-money); box-shadow: var(--lift); }
c29-sweep-entry[data-state="today"] .se-main { margin: calc(-1 * var(--s2)) calc(-1 * var(--s3)); padding: calc(var(--s2) + 3px) var(--s3) var(--s2);
  border-radius: var(--r-md); background: color-mix(in srgb, var(--money) 6%, transparent);
  box-shadow: inset 3px 0 0 var(--money); }
c29-sweep .sw-cadence { margin-top: var(--s4); padding-top: var(--s3); border-top: 1px solid var(--edge-soft);
  font-size: 13px; color: var(--ink-soft); }
c29-sweep[data-state="stale"] .sw-cadence { display: inline-block; padding: 7px 13px; border-top: 0; border-radius: var(--r-pill);
  background: var(--warn-bg); color: var(--warn-text); font-weight: 650; }
c29-sweep .sw-note { margin-top: var(--s3); padding: var(--s3) var(--s4); border-radius: var(--r-md);
  background: var(--sunken); font-size: 13.5px; line-height: 1.55; color: var(--ink-2); }
c29-sweep .sw-note:empty { display: none; }

/* ---------- c29-gift: the present / drawer ----------
   One component, two modes (inline box | announce bar -> sheet
   drawer), one seen-store, presence owned by the host page through
   html[data-gift-open]; the bar stands down while the mini player
   docks. Rev 4 (2026-10-18: "the gift doesn't open or confetti or
   anything") gives the open its moment: ONE master progress value
   (--gp, 0..1) driven by a rAF clock in tide.js flies the lid, swells
   the box and fades the ribbon; at p=.42 a tasteful confetti burst
   fires (canvas, token colors, ~1.2s, sound off, one buzz) and the
   reveal rises in behind it. Reduced motion — or a gift restored as
   already opened — skips the sequence entirely: instant reveal, no
   confetti, at most the static settle of the end state. */
/* Box internals are scoped to .gift-box, never to the element: the
   drawer path MOVES the box into Tide.sheet's <dialog>, out from
   under <c29-gift> — element-scoped rules silently stopped applying
   there (the Rev 2 drawer box rendered naked). The box mirrors the
   component's state + costume on itself for the same reason. */
c29-gift { display: block; }
.gift-box { position: relative; text-align: center; padding: var(--s2) 0; }
.gift-box .gift-kicker { font-size: 11px; font-weight: 700; letter-spacing: .09em; text-transform: uppercase; color: var(--money-text); }
.gift-box .gift-title { margin-top: var(--s2); font: 700 19px/1.25 var(--font-display); letter-spacing: -.01em; }
.gift-box .gift-stage { position: relative; width: 120px; height: 106px; margin: var(--s4) auto var(--s1); }
.gift-box .gift-shadow { position: absolute; left: 50%; bottom: 0; width: 78px; height: 12px; transform: translateX(-50%);
  border-radius: 50%; background: color-mix(in srgb, var(--ink-1) 16%, transparent); filter: blur(5px); }
.gift-box .gift-present { --gp: 0; position: absolute; left: 50%; top: 8px; width: 88px; height: 88px; margin-left: -44px;
  display: grid; place-items: center; border: 0; border-radius: 20px; cursor: pointer; color: var(--money-text);
  background: linear-gradient(160deg, color-mix(in srgb, var(--accent) 26%, var(--surface)), color-mix(in srgb, var(--accent) 12%, var(--surface)));
  box-shadow: inset 0 0 0 1px var(--edge-soft);
  transform: scale(calc(1 + var(--gp) * .07)); }
.gift-box .gift-present::before { content: ""; position: absolute; left: -5px; right: -5px; top: -7px; height: 25px; border-radius: 9px;
  background: linear-gradient(160deg, var(--29d-coral-500), var(--money)); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--ink-1) 8%, transparent);
  transform-origin: 10% 90%;
  transform: rotate(calc(var(--gp) * -26deg)) translate(calc(var(--gp) * -16px), calc(var(--gp) * -24px));
  opacity: calc(1 - var(--gp) * .92);
  transition: transform var(--d2) var(--ease), opacity var(--d2) var(--ease); }
.gift-box .gift-present::after { content: ""; position: absolute; top: 0; bottom: 0; left: 50%; width: 15px; margin-left: -7.5px;
  border-radius: 2px; background: linear-gradient(180deg, var(--29d-coral-500), var(--money));
  opacity: calc(1 - var(--gp) * .5); transition: opacity var(--d2) var(--ease); }
.gift-box[data-state="opening"] .gift-present::before, .gift-box[data-state="opening"] .gift-present::after { transition: none; }
.gift-box .gift-present .t-icon { position: relative; z-index: 1; width: 34px; height: 34px; margin-top: 10px; }
.gift-box .gift-present:active { transform: scale(calc(.94 + var(--gp) * .07)); }
/* the waiting life is a breathing shadow, never a moving tap target:
   the present sits still (agents and thumbs alike get a stable hit
   area — Bot Manners rule 76) while its ground shadow breathes. */
.gift-box[data-state="waiting"] .gift-shadow, .gift-box[data-state="panel"] .gift-shadow { animation: gift-bob 3.4s ease-in-out infinite; }
@keyframes gift-bob { 0%, 100% { transform: translateX(-50%) scaleX(1); opacity: 1; } 50% { transform: translateX(-50%) scaleX(.84); opacity: .55; } }
.gift-box .gift-confetti { position: absolute; z-index: 6; pointer-events: none; }
.gift-box .gift-hint { min-height: 20px; margin-top: var(--s2); font-size: 13.5px; font-weight: 600; color: var(--ink-soft); }
.gift-box .gift-reveal { margin-top: var(--s3); text-align: left; }
.gift-box .gift-reveal[hidden] { display: none; }
.gift-box .gift-reveal .t-stack > * { animation: t-rise var(--d3) var(--ease) both; }
.gift-box .gift-actions { display: flex; gap: var(--s2); justify-content: center; margin-top: var(--s4); }
c29-gift.gift-bar { position: fixed; left: 50%; transform: translateX(-50%); bottom: calc(var(--s3) + var(--safe-b)); z-index: 75;
  width: min(calc(100% - 2 * var(--s4)), 560px); min-height: 60px;
  display: flex; align-items: center; gap: var(--s3); text-align: left;
  padding: var(--s2) var(--s4) var(--s2) var(--s3); border-radius: var(--corner);
  background: var(--surface); border: 1px solid var(--edge-soft); box-shadow: var(--lift);
  animation: t-rise var(--d2) var(--ease); }
html[data-player="mini"] c29-gift.gift-bar { display: none; } /* stand-down: one floating bar at a time */
c29-gift[hidden], c29-gift.gift-bar[hidden] { display: none; } /* author display rules must never beat [hidden] */
c29-gift.gift-bar .t-icon { width: 26px; height: 26px; color: var(--accent-text); flex: none; }
c29-gift .gb-open { flex: 1; min-width: 0; min-height: 44px; display: flex; align-items: center; gap: var(--s3); text-align: left; border: 0; background: none; cursor: pointer; }
c29-gift .gb-x { flex: none; width: 40px; height: 44px; display: grid; place-items: center; border: 0; background: none; border-radius: 99px; color: var(--ink-soft); cursor: pointer; }
c29-gift .gb-x .t-icon { width: 19px; height: 19px; color: var(--ink-soft); }
c29-gift .gift-bar-tx { flex: 1; min-width: 0; }
c29-gift .gift-bar-tx b { display: block; font-size: 14.5px; font-weight: 700; }
c29-gift .gift-bar-tx span { display: block; font-size: 12.5px; color: var(--ink-mute); }
c29-gift[theme="gameboy"] { --gift-a: #9BBC0F; --gift-b: #0F380F; }
.gift-box[data-costume="gameboy"] { --gift-a: #9BBC0F; --gift-b: #0F380F; }
.gift-box[data-costume="gameboy"] .gift-present { background: var(--gift-a); border: 3px solid var(--gift-b); border-radius: 6px; color: var(--gift-b); box-shadow: none; }
.gift-box[data-costume="gameboy"] .gift-present::before { background: var(--gift-b); box-shadow: none; border-radius: 4px; }
.gift-box[data-costume="gameboy"] .gift-present::after { background: var(--gift-b); }
c29-gift[theme="gameboy"].gift-bar { background: var(--gift-a); border: 3px solid var(--gift-b); border-radius: 6px; box-shadow: none; }

/* ---------- c29-player: the listen player ----------
   Rev 4 visual overhaul (2026-10-18: "looks like ass and not as
   nice as Therese's") to the wc player standard of the pals' pages:
   a real stage card with artwork, a 22px title, bordered skip
   circles, a 58px coral play, and a seek you can SEE — a filled
   track painted from the component's own progress (--p, 0..1) over
   the range input, so the keyboard contract is the native one.
   Behavior is untouched Rev 2 law: Stop is split from Dismiss, the
   html[data-player] attribute + --player-slot-h own presence, the
   component formats chapters, persist="local" never autoplays,
   auto-advance stays OFF. (The Rev 2 CSS below this line also named
   dead tokens and a classless .t-range — the seek rendered as a
   naked browser input. Gone.) */
c29-player { display: block; }
c29-player .pl-stage { position: relative; overflow: hidden; padding: var(--s5);
  background: var(--raised); border-radius: calc(var(--corner) - 4px); box-shadow: var(--hair); }
c29-player .pl-stage::before { content: ""; position: absolute; inset: 0; background: var(--sheen); pointer-events: none; }
c29-player .pl-stage > * { position: relative; }
c29-player .pl-top { display: flex; align-items: center; gap: var(--s3); }
c29-player .pl-art, c29-player .pm-art { flex: none; position: relative; overflow: hidden;
  background: radial-gradient(120% 120% at 30% 20%, var(--sea-200), var(--sea-600) 78%); }
c29-player .pl-art::after, c29-player .pm-art::after { content: ""; position: absolute; inset: 0; background: var(--sheen); }
c29-player .pl-art[data-art="1"], c29-player .pm-art[data-art="1"] { background: radial-gradient(120% 120% at 30% 20%, var(--sun-300), var(--29d-coral-500) 80%); }
c29-player .pl-art[data-art="2"], c29-player .pm-art[data-art="2"] { background: radial-gradient(120% 120% at 70% 15%, var(--sea-300), var(--sea-700) 80%); }
c29-player .pl-art[data-art="3"], c29-player .pm-art[data-art="3"] { background: radial-gradient(120% 120% at 25% 25%, var(--sun-100), var(--sun-500) 82%); }
c29-player .pl-art { width: 52px; height: 52px; border-radius: 14px; box-shadow: var(--hair); }
c29-player .pl-id { flex: 1; min-width: 0; }
c29-player .pl-id .t-kicker { font-size: 11px; font-weight: 700; letter-spacing: .09em; text-transform: uppercase; color: var(--accent-text); }
c29-player .pl-show { margin-top: 1px; font-size: 13px; color: var(--ink-soft); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
c29-player .pl-title { margin-top: var(--s4); font: 750 22px/1.22 var(--font-display); letter-spacing: -.01em; }
c29-player .pl-note { margin-top: 3px; font-size: 14.5px; line-height: 1.55; color: var(--ink-soft); }
c29-player .pl-note:empty { display: none; }
c29-player .pl-seek { margin-top: var(--s4); }
c29-player input[type="range"].pl-range { --p: 0; -webkit-appearance: none; appearance: none; display: block;
  width: 100%; height: 28px; margin: 0; padding: 0; background: transparent; cursor: pointer; }
c29-player .pl-range::-webkit-slider-runnable-track { height: 8px; border-radius: 99px;
  background: linear-gradient(90deg, var(--money) calc(var(--p) * 100%), var(--sunken) calc(var(--p) * 100%)); }
c29-player .pl-range::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 20px; height: 20px; margin-top: -6px;
  border-radius: 50%; background: var(--raised); border: 1px solid var(--edge); box-shadow: var(--lift); }
c29-player .pl-range::-moz-range-track { height: 8px; border-radius: 99px; background: var(--sunken); }
c29-player .pl-range::-moz-range-progress { height: 8px; border-radius: 99px; background: var(--money); }
c29-player .pl-range::-moz-range-thumb { width: 18px; height: 18px; border-radius: 50%;
  background: var(--raised); border: 1px solid var(--edge); box-shadow: var(--lift); }
c29-player .pl-times { display: flex; justify-content: space-between; margin-top: 1px; }
c29-player .pl-t { font-size: 12px; font-weight: 600; color: var(--ink-mute); font-variant-numeric: tabular-nums; }
c29-player .pl-t[data-now] { color: var(--ink-soft); }
/* TRANSPORT CLUSTER LAW (2026-10-19, Principal-spotted on Listen: skips
   spread edge-to-edge "piss me off"): the transport is ONE tight cluster
   centered on the play button — a single even gap (--s3) between every
   pair of controls, no control carries its own side margins, nothing
   stretches or space-betweens. The primary skip pair flanks play; the
   optional outer pair (skip2) wears the same 44px circle with a quieter
   glyph. Every target >= 44px. A transport spread across the bar is a
   defect, whoever renders it. */
c29-player .pl-ctl { display: flex; align-items: center; justify-content: center; gap: var(--s3); margin-top: var(--s3); }
c29-player .pl-ctl-dual { gap: 10px; } /* five controls: the same one-gap rhythm, drawn 2px tighter so the cluster still fits the stage's content box */
c29-player .pl-skip { flex: none; width: 44px; height: 44px; display: grid; place-items: center; border-radius: 50%;
  border: 1px solid var(--edge); background: transparent; color: var(--money-text);
  transition: transform var(--d1) var(--ease-give), background var(--d1) var(--ease); }
c29-player .pl-skip .t-icon { width: 23px; height: 23px; }
c29-player .pl-skip:active { background: var(--wash); transform: scale(.94); }
c29-player .pl-skip2 { color: var(--ink-soft); }
c29-player .pl-skip2 .t-icon { width: 21px; height: 21px; }
c29-player .pl-play { flex: none; width: 58px; height: 58px; display: grid; place-items: center; border: 0; border-radius: 50%; cursor: pointer;
  color: var(--on-money); background: linear-gradient(155deg, var(--29d-coral-500), var(--money)); box-shadow: var(--lift);
  transition: transform var(--d1) var(--ease-give); }
c29-player .pl-play .t-icon { width: 25px; height: 25px; }
c29-player .pl-play:active { transform: scale(.93); }
c29-player .pl-err { margin-top: var(--s3); font-size: 13px; font-weight: 600; color: var(--bad-text); text-align: center; }
c29-player .pl-err[hidden] { display: none; }
c29-player .pl-listlab { margin-top: var(--s5); font-size: 12px; font-weight: 700; letter-spacing: .09em;
  text-transform: uppercase; color: var(--ink-mute); }
c29-player [data-list], c29-player [data-chaplist] { border-bottom: 1px solid var(--edge-soft); }
c29-player .pl-ep { display: flex; align-items: center; gap: var(--s3); width: 100%; min-height: 52px;
  padding: var(--s3) 2px; border: 0; border-top: 1px solid var(--edge-soft); background: none; text-align: left; cursor: pointer; }
c29-player .pl-ep .dot { flex: none; width: 9px; height: 9px; border-radius: 50%; border: 2px solid var(--accent); opacity: .35; }
c29-player .pl-ep.is-on .dot { background: var(--accent); opacity: 1; }
c29-player .pl-ep .tx { flex: 1; min-width: 0; display: grid; }
c29-player .pl-ep .tx b { font-size: 15px; font-weight: 650; line-height: 1.3; }
c29-player .pl-ep.is-on .tx b { color: var(--accent-text); }
c29-player .pl-ep .tx small { font-size: 13px; color: var(--ink-mute); }
c29-player .pl-ep .pl-dur { flex: none; padding-left: var(--s2); font-size: 12.5px; color: var(--ink-mute); font-variant-numeric: tabular-nums; }
c29-player .pl-chaplab { margin-top: var(--s4); font-size: 13px; color: var(--ink-soft); }
c29-player .pl-chap { display: flex; align-items: center; gap: var(--s3); width: 100%; min-height: 44px;
  padding: var(--s2) 2px; border: 0; border-top: 1px solid var(--edge-soft); background: none; text-align: left; font-size: 14.5px; cursor: pointer; }
c29-player .pl-chap .ct { flex: none; padding: 3px 9px; border-radius: 99px; background: var(--accent-soft);
  color: var(--accent-text); font-size: 12.5px; font-weight: 700; font-variant-numeric: tabular-nums; }
c29-player .pl-chap.is-on .ct { background: var(--accent); color: var(--on-accent); }
c29-player .pl-chap.is-on .cl { font-weight: 700; }
c29-player .pl-mini { --p: 0; position: fixed; left: 50%; transform: translateX(-50%); bottom: calc(var(--s2) + var(--safe-b)); z-index: 80;
  width: min(calc(100% - 2 * var(--s3)), 560px); height: calc(var(--player-slot-h) - 8px);
  display: flex; align-items: center; gap: var(--s2); padding: 0 var(--s3) 0 var(--s2); overflow: hidden;
  border-radius: 99px; background: var(--raised); border: 1px solid var(--edge-soft); box-shadow: var(--lift);
  animation: t-rise var(--d2) var(--ease); }
c29-player .pl-mini::after { content: ""; position: absolute; left: 16px; right: 16px; bottom: 4px; height: 3px; border-radius: 99px;
  background: linear-gradient(90deg, var(--money) calc(var(--p) * 100%), var(--wash-2) calc(var(--p) * 100%)); }
c29-player .pm-art { width: 34px; height: 34px; border-radius: 10px; }
c29-player .pl-mini[hidden] { display: none; }
c29-player .pm-play { flex: none; width: 44px; height: 44px; display: grid; place-items: center; border: 0; border-radius: 50%; cursor: pointer;
  color: var(--on-money); background: linear-gradient(155deg, var(--29d-coral-500), var(--money)); }
c29-player .pm-play .t-icon { width: 19px; height: 19px; }
c29-player .pm-words { flex: 1; min-width: 0; display: grid; text-align: left; padding: 2px 0; border: 0; background: none; cursor: pointer; }
c29-player .pm-words b { font-size: 14px; font-weight: 650; line-height: 1.3; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
c29-player .pm-words small { font-size: 12px; color: var(--ink-mute); font-variant-numeric: tabular-nums; }
c29-player .pm-btn { flex: none; width: 40px; height: 44px; display: grid; place-items: center; border: 0; background: none; border-radius: 99px; color: var(--ink-soft); cursor: pointer; }
c29-player .pm-btn .t-icon { width: 19px; height: 19px; }
c29-player .pl-resume { position: fixed; left: 50%; transform: translateX(-50%); bottom: calc(var(--s3) + var(--safe-b)); z-index: 79;
  display: flex; align-items: center; gap: var(--s2); padding: var(--s2) var(--s4); border-radius: 99px;
  background: var(--surface); border: 1px solid var(--edge-soft); box-shadow: var(--lift);
  font-weight: 650; font-size: 13.5px; animation: t-rise var(--d2) var(--ease); }
c29-player .pl-resume[hidden] { display: none; }

/* ---------- Now-Playing sheet (D4 variant) ----------
   The same player, larger: the sheet face reuses the stage's
   seek + transport classes wholesale, so the cluster law and
   the seek paint hold identically — only the artwork and the
   title grow. The dialog itself is canon's dialog.t-sheet. */
c29-player dialog.pl-sheet { text-align: left; }
c29-player .pl-sheet-in { display: grid; gap: var(--s3); justify-items: stretch; }
c29-player .pl-sheet-art { width: 168px; height: 168px; border-radius: 22px; box-shadow: var(--lift); justify-self: center; margin-top: var(--s2);
  position: relative; overflow: hidden; background: radial-gradient(120% 120% at 30% 20%, var(--sea-200), var(--sea-600) 78%); }
c29-player .pl-sheet-art::after { content: ""; position: absolute; inset: 0; background: var(--sheen); }
c29-player .pl-sheet-art[data-art="1"] { background: radial-gradient(120% 120% at 30% 20%, var(--sun-300), var(--29d-coral-500) 80%); }
c29-player .pl-sheet-art[data-art="2"] { background: radial-gradient(120% 120% at 70% 15%, var(--sea-300), var(--sea-700) 80%); }
c29-player .pl-sheet-art[data-art="3"] { background: radial-gradient(120% 120% at 25% 25%, var(--sun-100), var(--sun-500) 82%); }
c29-player dialog.pl-sheet .t-kicker { text-align: center; font-size: 11px; font-weight: 700; letter-spacing: .09em; text-transform: uppercase; color: var(--accent-text); }
c29-player .pl-sheet-title { font: 750 26px/1.2 var(--font-display); letter-spacing: -.01em; text-align: center; overflow-wrap: anywhere; }
c29-player dialog.pl-sheet .pl-note { text-align: center; margin-top: 0; }
c29-player dialog.pl-sheet .pl-seek { margin-top: var(--s2); }
c29-player dialog.pl-sheet .pl-ctl { margin-top: var(--s2); }
c29-player dialog.pl-sheet [data-schaplist] { border-bottom: 1px solid var(--edge-soft); }
c29-player dialog.pl-sheet [data-sheet-close] { justify-self: center; margin-top: var(--s2); }

/* ============================================================
 * REV 5 — the Bencho steals (WO-TIDE-601/602/603), ported from
 * the tide-lab prototypes P2/P3/P1 in real Tide tokens. Every
 * behavior collapses under the global motion gate: tide.js
 * snaps the springs, glues the fill, and never arms the rise.
 * ============================================================ */

/* ---------- c29-progress: the activity island [MIT-PORT dynamic-island] ----------
   One black pill that IS the activity. Width, height and corner
   are written by tide.js from ONE spring scalar; the faces are
   absolutely placed over the morphing box and crossfade against
   that scalar. The island is a dark surface in both themes (its
   own --pg-bg), so its text colors are pinned locally, exactly
   as the prototype pinned them. */
c29-progress { display: block; }
c29-progress .pg-stage { display: flex; justify-content: center; min-height: 216px; padding-top: var(--s1); }
c29-progress .pg-island { --pg-bg: var(--ink-1); position: relative; flex: none; overflow: hidden; cursor: pointer;
  background: var(--pg-bg); color: #FBF7EF; box-shadow: var(--lift-hi); -webkit-tap-highlight-color: transparent; }
[data-theme="dark"] c29-progress .pg-island { --pg-bg: #050D14; }
c29-progress .pg-face { position: absolute; inset: 0; padding: 0 var(--s4); }
c29-progress .pg-compact { display: flex; align-items: center; gap: var(--s2); }
c29-progress .pg-dot { width: 9px; height: 9px; border-radius: 50%; background: var(--sea-300); flex: none; }
c29-progress[data-state="paused"] .pg-dot { background: var(--sun-300); }
/* THE FIT RULE (2026-10-19): the compact island's width is measured
   from its content in tide.js (_fitCompact) so the value always sits
   inside the padding. These two lines are only the last-resort guard
   for a label longer than the whole stage: the LABEL ellipsizes, the
   value (flex:none) never does. A fixed-width pill with a dynamic
   value is a defect. */
c29-progress .pg-compact .pg-label { font-size: var(--fs-sm); font-weight: 600; white-space: nowrap;
  min-width: 0; overflow: hidden; text-overflow: ellipsis; }
c29-progress .pg-compact .pg-pct { margin-left: auto; font-variant-numeric: tabular-nums; color: #8DCABD;
  font-size: var(--fs-sm); font-weight: 700; white-space: nowrap; flex: none; }
c29-progress .pg-full { padding: var(--s4); display: flex; flex-direction: column; gap: var(--s2); }
c29-progress .pg-full h3 { margin: 0; font-family: var(--font-display); font-size: var(--fs-lg); letter-spacing: var(--track-head); }
c29-progress .pg-detail { margin: 0; font-size: var(--fs-sm); color: #C7D2D6; font-variant-numeric: tabular-nums; }
c29-progress .pg-note { margin: 0; font-size: var(--fs-sm); color: #C7D2D6; }
/* flex-crush guard (WO-TIDE-601 regression, caught by the lab rig):
   the full face is a morphing flex column — without flex:none the
   bar shrinks to 0px tall mid-morph. The bar NEVER shrinks. */
c29-progress .pg-bar { flex: none; height: 6px; min-height: 6px; border-radius: 3px; background: rgba(255,255,255,.16); overflow: hidden; margin-top: var(--s1); }
c29-progress .pg-bar i { display: block; height: 100%; width: 0%; background: var(--sea-300); border-radius: 3px; }
c29-progress .pg-actions { margin-top: auto; display: flex; gap: var(--s2); flex: none; }
c29-progress .pg-btn { appearance: none; border: 0; cursor: pointer; min-height: 38px; padding: 0 var(--s4);
  border-radius: var(--r-pill); background: rgba(255,255,255,.14); color: #fff; font: 650 var(--fs-sm) var(--font-ui); }
c29-progress .pg-btn:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--pg-bg), 0 0 0 4px var(--sea-300); }

/* ---------- .t-range: the range family [MIT-PORT slosh] ----------
   A pill track, a drawn handle, and a fill with weight: tide.js
   rides the handle 1:1 under the finger and chases it with the
   fill on a softer second spring (the skew is the liquid leaning
   into the move, driven by the fill spring's own velocity).
   overflow:hidden keeps a surging fill inside the pill. With a
   nested native input the input is the control (invisible, full
   size, focusable) and the drawn pieces are its picture. */
.t-range { position: relative; display: block; height: 56px; border-radius: var(--r-pill); background: var(--sunken);
  box-shadow: inset 0 0 0 1px var(--edge-soft); overflow: hidden; touch-action: none; cursor: grab; }
.t-range:active { cursor: grabbing; }
.t-range:focus-visible { outline: none; box-shadow: var(--focus); }
.t-range:focus-within { box-shadow: var(--focus); }
.t-range .tr-fill { position: absolute; left: 0; top: 0; bottom: 0; width: 0; border-radius: var(--r-pill);
  transform-origin: left center; background: linear-gradient(180deg, var(--sea-300), var(--sea-500)); pointer-events: none; }
.t-range .tr-handle { position: absolute; top: 50%; width: 48px; height: 48px; border-radius: 50%;
  background: var(--raised); box-shadow: var(--lift); transform: translate(-50%, -50%);
  display: flex; align-items: center; justify-content: center; gap: 3px; pointer-events: none; }
.t-range .tr-handle i { width: 3px; height: 16px; border-radius: 2px; background: var(--ink-4); }
.t-range input[type="range"] { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; opacity: 0; cursor: grab; }
.t-range input[type="range"]:active { cursor: grabbing; }

/* ---------- type in motion: [data-rise] ----------
   Words are split by tide.js (never pre-hidden in markup); the
   rise itself is one WAAPI pass armed only under full motion. */
[data-rise] .t-rise-w { display: inline-block; }

/* ============================================================
 * REV 6 — the peel (WO-TIDE-604), CEREMONY CLASS. The ceremony
 * doctrine (the Principal's ruling on Desk card
 * tide-ceremony-doctrine, consumed by buzzsaw R116): ceremony
 * surfaces (gift, front door, pricing) may carry one flourish;
 * work surfaces (approvals, decks, sheets) stay calm. These
 * styles serve c29-peel and c29-gift[reveal=peel] ONLY — both
 * stamp data-surface="ceremony" on the peel root. Geometry and
 * shading are the tide-lab P4 prototype's, in canonical tokens;
 * tide.js drives the fold, and under the motion gate the corner
 * is a plain toggle (JS snaps; nothing here animates).
 * ============================================================ */

/* ---------- the peel: shared geometry [CLEAN-ROOM page-peel] ---------- */
.peel-card { position: relative; border-radius: var(--corner); overflow: hidden; box-shadow: var(--lift);
  touch-action: none; background: var(--sea-100); }
.peel-under { position: absolute; inset: 0; padding: var(--s4);
  background: linear-gradient(160deg, var(--sea-100), var(--sea-200)); }
.peel-face { position: absolute; inset: 0; padding: var(--s5); background: var(--raised); }
.peel-flap { position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(225deg, var(--sand-300), var(--sand-50) 55%, var(--sand-200));
  filter: drop-shadow(-5px 5px 7px rgba(74, 56, 28, .28)); visibility: hidden; }
.peel-hint { position: absolute; top: 0; right: 0; width: 74px; height: 74px; pointer-events: none;
  transition: opacity var(--d2) var(--ease); }
.peel-hint i { position: absolute; top: 0; right: 0; width: 0; height: 0; border-style: solid;
  border-width: 0 34px 34px 0; border-color: transparent var(--sand-200) transparent transparent; }
.peel-hint span { position: absolute; top: 30px; right: 8px; font-size: var(--fs-cap); font-weight: 700;
  letter-spacing: var(--track-cap); text-transform: uppercase; color: var(--ink-soft); } /* ink-soft, not ink-mute: the label must clear 4.5 on the card's sea ground in dark too (render-pass, Rev 6) */
.peel-btn { position: absolute; top: 0; right: 0; width: 84px; height: 84px; z-index: 3;
  appearance: none; border: 0; background: transparent; cursor: pointer; }
.peel-btn:focus-visible { outline: none; box-shadow: var(--focus); border-radius: 0 var(--corner) 0 var(--corner); }
/* face + under typography (hosts may override; the under's key
   lines belong in the corner triangle the fold uncovers, laid
   out as a right-aligned staircase clear of the fold at rest) */
.peel-face h3 { margin: 0 0 var(--s2); font-family: var(--font-display); font-size: var(--fs-xl); letter-spacing: var(--track-head); }
.peel-face p { margin: 0; color: var(--ink-soft); max-width: 26ch; }
.peel-face .peel-price { font-family: var(--font-display); font-weight: 800; font-size: var(--fs-2xl); color: var(--money-text); margin-top: var(--s3); }
.peel-face .peel-price span { font-size: var(--fs-body); color: var(--ink-mute); }
.peel-under h3 { margin: 0 0 var(--s2); font-family: var(--font-display); font-size: var(--fs-lg); letter-spacing: var(--track-head); color: var(--ink-1); }
.peel-under p { margin: 0; font-size: var(--fs-sm); color: var(--ink-2); line-height: 1.65; }
.peel-under .peel-note { margin-left: auto; width: 72%; text-align: right; }
.peel-under .peel-note p { line-height: 1.45; } /* tighter staircase pitch: each descending line drops one pitch closer to the 45deg fold — 1.45 keeps equal-width lines clear of it at rest (Rev 6 render-pass: the 1.65 pitch grazed by 2.7px) */
.peel-under .peel-note .nl { display: block; }

/* ---------- c29-peel: the ceremony reveal ---------- */
c29-peel { display: block; }
c29-peel .peel-card { height: var(--peel-h, 244px); }

/* ---------- c29-gift[reveal=peel]: the ceremony opening ----------
   The payload layer is IN FLOW (it sizes the card) under an
   absolute wrapping face that carries the present; the fold
   uncovers the real payload, and the open settle sweeps the
   face fully off. Scoped to .gift-box like every gift internal
   (the drawer path moves the box out from under the element). */
.gift-box .gift-peel { margin-top: var(--s4); text-align: left; background: var(--sunken); }
.gift-box .gift-peel .peel-under { position: relative; inset: auto; padding: var(--s4); background: none; }
.gift-box .gift-peel .gift-reveal { margin-top: 0; }
.gift-box .gift-peel-face { display: flex; align-items: center; justify-content: center; padding: var(--s4); }
.gift-box .gift-peel-face .gift-stage { margin: 0; }

/* ============================================================
 * WAVE TWO (2026-10-19) — twelve new gallery demos.
 * Appended as one block; every selector is scoped to its
 * c29-* element or a unique .wv- class, tokens only.
 * ============================================================ */

/* ---- wave: sort ---- */
/* ============================================================
   SORT RAIL  <c29-sortrail>   (Wave A)
   Every value is a token. Shift and settle ride var(--d2) /
   var(--d3) (so --speed bites) on var(--ease-give) (so --bounce
   bites); row radius derives from var(--corner); row padding
   scales with var(--density).
   ============================================================ */
c29-sortrail { display: block; }
c29-sortrail .wv-sort-list { display: grid; gap: var(--s2); position: relative; }
c29-sortrail .wv-sort-row { display: flex; align-items: center; gap: var(--s3); position: relative;
  padding: calc(8px * var(--density)) calc(10px * var(--density));
  background: var(--surface); border-radius: calc(var(--corner) - 6px); box-shadow: var(--hair);
  transition: transform var(--d2) var(--ease-give), box-shadow var(--d2) var(--ease), background var(--d2) var(--ease); }
c29-sortrail .wv-sort-row.is-drag { transition: none; background: var(--raised); box-shadow: var(--lift-hi); z-index: 3; }
c29-sortrail .wv-sort-list.is-plain .wv-sort-row { transition: none; }
c29-sortrail .wv-sort-handle { width: 44px; height: 44px; flex: none; display: grid; place-items: center;
  border: 0; border-radius: 12px; background: var(--wash); color: var(--ink-mute); cursor: grab; touch-action: none;
  transition: background var(--d2) var(--ease), color var(--d2) var(--ease), transform var(--d1) var(--ease); }
c29-sortrail .wv-sort-handle:active { cursor: grabbing; transform: scale(.96); }
c29-sortrail .wv-sort-handle .t-icon { width: 20px; height: 20px; }
c29-sortrail .wv-sort-main { flex: 1; min-width: 0; display: grid; gap: 1px; }
c29-sortrail .wv-sort-name { font-size: 15px; font-weight: 650; line-height: 1.3; }
c29-sortrail .wv-sort-meta { font-size: 13px; line-height: 1.4; color: var(--ink-mute); }
c29-sortrail .wv-sort-pos { flex: none; min-width: 22px; text-align: center; font-size: 13px; font-weight: 650;
  color: var(--ink-mute); font-variant-numeric: tabular-nums; }
c29-sortrail .wv-sort-read { margin-top: var(--s3); font-size: 14px; line-height: 1.5; color: var(--ink-soft); }
c29-sortrail .wv-sort-hint { margin-top: var(--s1); font-size: 13px; line-height: 1.5; color: var(--ink-mute); }
@media (hover: hover) {
  c29-sortrail .wv-sort-handle:hover { background: var(--wash-2); color: var(--ink-soft); }
  c29-sortrail .wv-sort-row:hover { background: color-mix(in srgb, var(--surface) 94%, var(--ink)); }
}

/* ---- wave: pull ---- */
/* ============================================================
   PULL TO REFRESH  <c29-pull>   (Wave A)
   The card clips to var(--corner); the header strip is the only
   drag surface. Fill height and snap ride var(--d3) on
   var(--ease-give) (so --speed and --bounce bite); the wave spin
   runs calc(900ms / var(--speed)); row padding scales with
   var(--density).
   ============================================================ */
c29-pull { display: block; background: var(--surface); border-radius: var(--corner); box-shadow: var(--hair); overflow: hidden; }
c29-pull .wv-pull-head { position: relative; height: 56px; display: flex; align-items: center; justify-content: center; gap: var(--s2);
  padding: 0 var(--s4); background: var(--sunken); overflow: hidden; cursor: grab; touch-action: none; user-select: none; -webkit-user-select: none; }
c29-pull .wv-pull-head:active { cursor: grabbing; }
c29-pull .wv-pull-fill { position: absolute; left: 0; right: 0; bottom: 0; height: 0;
  background: color-mix(in srgb, var(--sea-300) 68%, var(--sea-100));
  transition: height var(--d3) var(--ease-give); }
c29-pull .wv-pull-head.is-drag .wv-pull-fill { transition: none; }
c29-pull .wv-pull-wave { position: relative; z-index: 1; display: none; width: 22px; height: 22px; color: var(--accent-text); }
c29-pull .wv-pull-wave .t-icon { width: 22px; height: 22px; }
c29-pull[data-state="refreshing"] .wv-pull-wave { display: grid; place-items: center; animation: wv-pull-spin calc(900ms / var(--speed)) linear infinite; }
c29-pull .wv-pull-label { position: relative; z-index: 1; font-size: 14px; font-weight: 650; color: var(--ink-soft); }
c29-pull .wv-pull-rows { padding: var(--s1) var(--s5); }
c29-pull .wv-pull-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: var(--s3);
  padding: calc(10px * var(--density)) 0; border-top: 1px solid var(--edge-soft); }
c29-pull .wv-pull-row:first-child { border-top: 0; }
c29-pull .wv-pull-row-main { display: grid; gap: 1px; min-width: 0; }
c29-pull .wv-pull-row-label { font-size: 15px; font-weight: 600; line-height: 1.35; }
c29-pull .wv-pull-row-sub { font-size: 13px; line-height: 1.4; color: var(--ink-mute); }
c29-pull .wv-pull-row-value { font-family: var(--font-display); font-weight: 750; font-size: 17px; letter-spacing: -.01em; font-variant-numeric: tabular-nums; }
c29-pull .wv-pull-rows.is-new .wv-pull-row { animation: wv-pull-in var(--d3) var(--ease) both; }
c29-pull .wv-pull-rows.is-new .wv-pull-row:nth-child(2) { animation-delay: calc(50ms / var(--speed)); }
c29-pull .wv-pull-rows.is-new .wv-pull-row:nth-child(3) { animation-delay: calc(100ms / var(--speed)); }
c29-pull .wv-pull-rows.is-new .wv-pull-row:nth-child(4) { animation-delay: calc(150ms / var(--speed)); }
c29-pull .wv-pull-foot { display: flex; align-items: center; gap: var(--s2); flex-wrap: wrap;
  padding: var(--s3) var(--s5) var(--s4); border-top: 1px solid var(--edge-soft); }
c29-pull .wv-pull-updated { font-size: 13px; color: var(--ink-mute); font-variant-numeric: tabular-nums; }
@keyframes wv-pull-spin { to { transform: rotate(360deg); } }
@keyframes wv-pull-in { from { opacity: 0; transform: translateY(4px); } }
@media (hover: hover) {
  c29-pull .wv-pull-head:hover { background: color-mix(in srgb, var(--sunken) 92%, var(--ink)); }
}

/* ---- wave: tabs ---- */
/* ============================================================
   TABS  <c29-tabs>   (Wave A)
   The pill rides left + width on var(--d3) var(--ease-give), so
   --speed sets its pace and --bounce its overshoot. Panels
   crossfade and slide on var(--d3). The bar radius is the pill
   radius; panel padding scales with var(--density).
   ============================================================ */
c29-tabs { display: block; }
c29-tabs .wv-tabs-bar { position: relative; isolation: isolate; display: grid; grid-auto-flow: column; grid-auto-columns: 1fr;
  gap: 2px; padding: 3px; border-radius: var(--r-pill); background: var(--wash); }
c29-tabs .wv-tabs-pill { position: absolute; z-index: -1; top: 3px; bottom: 3px; border-radius: var(--r-pill);
  background: var(--raised); box-shadow: var(--lift);
  transition: left var(--d3) var(--ease-give), width var(--d3) var(--ease-give); }
c29-tabs .wv-tabs-tab { min-height: 44px; padding: 0 var(--s3); border: 0; background: none; border-radius: var(--r-pill);
  font-size: 14px; font-weight: 600; color: var(--ink-mute); cursor: pointer; white-space: nowrap;
  transition: color var(--d2) var(--ease), transform var(--d1) var(--ease); }
c29-tabs .wv-tabs-tab:active { transform: scale(.97); }
c29-tabs .wv-tabs-tab[aria-selected="true"] { color: var(--ink); }
c29-tabs .wv-tabs-panel { padding: calc(var(--s4) * var(--density)) var(--s1) 0; display: grid; gap: var(--s2); }
c29-tabs .wv-tabs-panel p { font-size: 14px; line-height: 1.55; color: var(--ink-soft); }
c29-tabs .wv-tabs-panel.is-in { animation: wv-tabs-in var(--d3) var(--ease) both; }
c29-tabs .wv-tabs-stat { display: flex; align-items: baseline; gap: var(--s2); flex-wrap: wrap; }
c29-tabs .wv-tabs-num { font-family: var(--font-display); font-weight: 750; font-size: 20px; letter-spacing: -.01em;
  color: var(--ink); font-variant-numeric: tabular-nums; }
@keyframes wv-tabs-in { from { opacity: 0; transform: translateX(6px); } }
@media (hover: hover) {
  c29-tabs .wv-tabs-tab:hover { color: var(--ink-soft); }
  c29-tabs .wv-tabs-tab[aria-selected="true"]:hover { color: var(--ink); }
}

/* ---- wave: chart ---- */
/* ---------- c29-livechart: a working week, two data sets (Wave B) ---------- */
c29-livechart { display: block; }
c29-livechart .wv-chart-ctl { display: grid; gap: var(--s2); }
c29-livechart .wv-chart-row { display: flex; align-items: center; gap: var(--s3); flex-wrap: wrap; }
c29-livechart .wv-chart-lab { flex: none; min-width: 44px; font-size: 13px; font-weight: 650; color: var(--ink-mute); }
c29-livechart .wv-chart-unit { margin-top: var(--s3); font-size: 13px; font-weight: 600; color: var(--ink-soft); }
c29-livechart .wv-chart-plot { position: relative; margin-top: var(--s2); background: var(--sunken); border-radius: calc(var(--corner) - 6px); overflow: hidden; }
c29-livechart .wv-chart-plot svg { display: block; width: 100%; height: auto; }
c29-livechart .wv-chart-grid { stroke: var(--edge-soft); stroke-width: 1; }
c29-livechart .wv-chart-base { stroke: var(--edge); stroke-width: 1.5; }
c29-livechart .wv-chart-area { fill: color-mix(in srgb, var(--accent) 14%, transparent); stroke: none; transition: d var(--d3) var(--ease-give), opacity var(--d3) var(--ease); }
c29-livechart .wv-chart-line { fill: none; stroke: var(--accent); stroke-width: 3; stroke-linecap: round; stroke-linejoin: round; transition: d var(--d3) var(--ease-give), opacity var(--d3) var(--ease); }
c29-livechart .wv-chart-dot { fill: var(--surface); stroke: var(--accent); stroke-width: 3; cursor: pointer; transition: cy var(--d3) var(--ease-give), cx var(--d3) var(--ease-give), fill var(--d2) var(--ease); }
c29-livechart .wv-chart-dot.is-on { fill: var(--accent); }
c29-livechart .wv-chart-hit { fill: transparent; stroke: none; cursor: pointer; }
c29-livechart .wv-chart-pt { outline: none; }
c29-livechart .wv-chart-pt:focus-visible .wv-chart-dot { fill: var(--accent); stroke: var(--focus-ring); }
c29-livechart .wv-chart-bar { fill: var(--accent); cursor: pointer; transition: y var(--d3) var(--ease-give), height var(--d3) var(--ease-give), opacity var(--d3) var(--ease), fill var(--d2) var(--ease); }
c29-livechart .wv-chart-bar.is-on { fill: var(--sea-500); }
c29-livechart .wv-chart-bar:focus-visible { stroke: var(--focus-ring); stroke-width: 2; outline: none; }
c29-livechart .wv-chart-g-line, c29-livechart .wv-chart-g-points, c29-livechart .wv-chart-g-bars { transition: opacity var(--d3) var(--ease); }
c29-livechart[data-mode="bars"] .wv-chart-g-line, c29-livechart[data-mode="bars"] .wv-chart-g-points { opacity: 0; pointer-events: none; }
c29-livechart[data-mode="line"] .wv-chart-g-bars { opacity: 0; pointer-events: none; }
c29-livechart.is-init .wv-chart-line, c29-livechart.is-init .wv-chart-area, c29-livechart.is-init .wv-chart-dot, c29-livechart.is-init .wv-chart-bar { transition: none; }
c29-livechart .wv-chart-tag { position: absolute; z-index: 2; transform: translate(-50%, -140%); padding: 5px 10px; border-radius: 99px; white-space: nowrap; pointer-events: none;
  background: var(--ink); color: var(--bg); font-size: 13px; font-weight: 700; font-variant-numeric: tabular-nums;
  transition: left var(--d3) var(--ease-give), top var(--d3) var(--ease-give), opacity var(--d2) var(--ease); }
c29-livechart .wv-chart-days { display: grid; grid-template-columns: repeat(7, 1fr); margin-top: var(--s2); text-align: center; font-size: 13px; font-weight: 600; color: var(--ink-mute); }
c29-livechart .wv-chart-read { margin-top: var(--s2); font-size: 13px; color: var(--ink-mute); font-variant-numeric: tabular-nums; }
c29-livechart .wv-chart-row[hidden] { display: none; }
c29-livechart .wv-chart-ref { margin-top: var(--s3); border-top: 2px dashed var(--edge); padding-top: var(--s2); font-size: 13px; font-weight: 600; color: var(--ink-soft); }
c29-livechart .wv-chart-ref[hidden] { display: none; }
@media (hover: hover) {
  c29-livechart .wv-chart-pt:hover .wv-chart-dot { fill: var(--accent); }
  c29-livechart .wv-chart-bar:hover { fill: var(--sea-500); }
}

/* ---- wave: odo ---- */
/* ---------- c29-odometer: rolling-digit totals (Wave B) ---------- */
c29-odometer { display: block; }
c29-odometer .wv-odo-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--s3); }
c29-odometer .wv-odo-stat { display: grid; justify-items: center; gap: 2px; padding: var(--s4) var(--s2); text-align: center; background: var(--sunken); border-radius: calc(var(--corner) - 6px); }
c29-odometer .wv-odo-num { display: flex; align-items: flex-start; height: 1em; overflow: hidden; font: 800 32px/1 var(--font-display); letter-spacing: -0.01em; color: var(--ink); font-variant-numeric: tabular-nums; }
c29-odometer .wv-odo-col { display: block; width: 0.62em; height: 1em; overflow: hidden; text-align: center; }
c29-odometer .wv-odo-comma { display: block; width: 0.3em; text-align: center; }
c29-odometer .wv-odo-strip { display: flex; flex-direction: column; will-change: transform; transition: transform var(--d5) var(--ease-give); transition-delay: calc(var(--wv-i, 0) * 70ms / var(--speed)); }
c29-odometer .wv-odo-strip > span { display: block; height: 1em; line-height: 1; }
c29-odometer.is-instant .wv-odo-strip { transition: none; }
c29-odometer .wv-odo-label { margin-top: var(--s2); font-size: 15px; font-weight: 700; color: var(--ink); }
c29-odometer .wv-odo-sub { font-size: 13px; line-height: 1.45; color: var(--ink-mute); }
c29-odometer .wv-odo-foot { display: flex; align-items: center; gap: var(--s3); flex-wrap: wrap; margin-top: var(--s4); }
c29-odometer .wv-odo-note { font-size: 13px; color: var(--ink-mute); }
@media (max-width: 400px) {
  c29-odometer .wv-odo-num { font-size: 27px; }
  c29-odometer .wv-odo-grid { gap: var(--s2); }
}

/* ---- wave: job ---- */
/* ============================================================
 * LANE A / A2 — c29-jobrun real-data variant CSS FRAGMENT
 * Integration: REPLACE the canon c29-jobrun CSS block (tide.css,
 * the .wv-job-* rules) with these .jr-* rules, inside the
 * @layer c29.components block. Tokens only.
 * ============================================================ */

c29-jobrun { display: block; }
c29-jobrun .jr-head { display: flex; align-items: center; gap: var(--s3); flex-wrap: wrap; }
c29-jobrun .jr-title { font: 750 var(--fs-xl)/1.2 var(--font-display); letter-spacing: var(--track-head); color: var(--ink); min-width: 0; overflow-wrap: anywhere; }
c29-jobrun .jr-note { margin-top: var(--s2); font-size: var(--fs-sm); color: var(--ink-mute); line-height: 1.5; overflow-wrap: anywhere; }
/* The beam exists ONLY when the record carries a measured
   percent (the JS hides the element otherwise). */
c29-jobrun .jr-beam { display: block; height: 5px; border-radius: 99px; background: var(--wash-2); overflow: hidden; margin-top: var(--s4); }
c29-jobrun .jr-beam i { display: block; height: 100%; width: 0; background: var(--accent); border-radius: inherit; transition: width var(--d3) var(--ease); }
c29-jobrun .jr-trace { list-style: none; margin: var(--s4) 0 0; padding: 0; }
c29-jobrun .jr-trace:empty { margin: 0; }
c29-jobrun .jr-trace li { position: relative; padding: 0 0 var(--s4) 34px; }
c29-jobrun .jr-trace li::before { content: ""; position: absolute; left: 10px; top: 22px; bottom: -2px; width: 2px; border-radius: 2px; background: var(--edge); }
c29-jobrun .jr-trace li:last-child { padding-bottom: 0; }
c29-jobrun .jr-trace li:last-child::before { display: none; }
c29-jobrun .jr-dot { position: absolute; left: 3px; top: 3px; width: 16px; height: 16px; border-radius: 50%; background: var(--raised); box-shadow: inset 0 0 0 2px var(--accent); }
c29-jobrun .jr-trace li[data-s="done"] .jr-dot { background: var(--accent); box-shadow: none; }
c29-jobrun .jr-trace li[data-s="done"] .jr-dot::after { content: ""; position: absolute; left: 5px; top: 3px; width: 4px; height: 7px; border: solid var(--on-accent); border-width: 0 2px 2px 0; rotate: 45deg; }
c29-jobrun .jr-trace li[data-s="bad"] .jr-dot { background: var(--bad); box-shadow: none; }
c29-jobrun .jr-trace li[data-s="wait"] .jr-dot { box-shadow: inset 0 0 0 2px var(--sun-500); background: var(--sun-100); }
/* Running breathes ONLY via the canon t-breathe keyframe and
   collapses under the motion gate like every Tide animation.
   Stalled never animates: honest stillness. */
c29-jobrun .jr-trace li[data-s="run"] .jr-dot { animation: t-breathe 1.6s var(--ease) infinite; }
c29-jobrun .jr-trace b { display: block; font-size: 15px; font-weight: 600; overflow-wrap: anywhere; }
c29-jobrun .jr-trace span { display: block; font-size: 13px; color: var(--ink-mute); overflow-wrap: anywhere; }
c29-jobrun .jr-meta { display: flex; align-items: center; justify-content: space-between; gap: var(--s3); margin-top: var(--s4); font-size: var(--fs-sm); color: var(--ink-mute); }
c29-jobrun .jr-meta:empty { margin: 0; }
c29-jobrun[data-state="empty"] .jr-title { color: var(--ink-mute); font-size: var(--fs-lg); }

/* ---- wave: palette ---- */
/* ---------- c29-palette: the command palette (Wave C) ----------
   The dialog rounds on --corner, the flash pulse spends
   --accent, and every duration divides by --speed, so Tune
   reaches the palette and its jump flash alike. The search
   field computes at 17px: iOS never zooms it. */
c29-palette { display: block; }
c29-palette .wv-palette-kbd { display: inline-flex; align-items: center; min-height: 24px; padding: 0 9px;
  border-radius: 99px; background: var(--wash); color: var(--ink-soft);
  font-size: 13px; font-weight: 650; white-space: nowrap; }
c29-palette .wv-palette-hint { margin-top: var(--s2); font-size: 14px; color: var(--ink-mute); line-height: 1.5; }
c29-palette .wv-palette-dialog { width: min(540px, calc(100vw - 32px)); max-height: min(72vh, 580px);
  margin: 10vh auto auto; padding: var(--s3); border: 0; border-radius: var(--corner);
  background: var(--raised); color: var(--ink); box-shadow: var(--lift-hi); overflow: hidden; }
c29-palette .wv-palette-dialog[open] { animation: wv-palette-in var(--d2) var(--ease-give); }
@keyframes wv-palette-in { from { opacity: 0; transform: translateY(8px) scale(.98); } }
c29-palette .wv-palette-dialog::backdrop { background: color-mix(in srgb, var(--ink-1) 42%, transparent);
  backdrop-filter: blur(2px); -webkit-backdrop-filter: blur(2px); }
c29-palette .wv-palette-input { width: 100%; min-height: 52px; padding: 0 var(--s4); border: 0;
  border-radius: calc(var(--corner) - 8px); background: var(--sunken); color: var(--ink);
  font-size: 17px; box-shadow: inset 0 0 0 1px var(--edge-soft);
  transition: box-shadow var(--d2) var(--ease); }
c29-palette .wv-palette-input::placeholder { color: var(--ink-mute); opacity: 1; }
c29-palette .wv-palette-input:focus { outline: none; box-shadow: var(--focus); }
c29-palette .wv-palette-list { display: grid; gap: 2px; margin-top: var(--s2);
  max-height: 324px; overflow-y: auto; overscroll-behavior: contain; }
c29-palette .wv-palette-row { display: flex; align-items: center; gap: var(--s3); width: 100%;
  min-height: 48px; padding: var(--s2) var(--s3); border: 0; border-radius: var(--r-md);
  background: none; color: var(--ink); text-align: left; cursor: pointer; font-size: 15px;
  transition: background var(--d1) var(--ease); }
c29-palette .wv-palette-row.is-active { background: var(--accent-soft); box-shadow: inset 0 0 0 1.5px var(--accent); }
@media (hover: hover) {
  c29-palette .wv-palette-row:hover { background: var(--wash); }
  c29-palette .wv-palette-row.is-active:hover { background: var(--accent-soft); }
}
c29-palette .wv-palette-name { flex: 1; min-width: 0; font-weight: 550;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
c29-palette .wv-palette-kind { flex: none; padding: 3px 9px; border-radius: 99px; background: var(--wash);
  font-size: 13px; font-weight: 650; color: var(--ink-mute); white-space: nowrap; }
c29-palette .wv-palette-row.is-active .wv-palette-kind { color: var(--accent-text); background: var(--raised); }
c29-palette .wv-palette-empty { padding: var(--s5) var(--s3); text-align: center;
  font-size: 14px; color: var(--ink-mute); }
c29-palette .wv-palette-foot { display: flex; flex-wrap: wrap; gap: var(--s2) var(--s4);
  margin-top: var(--s2); padding: var(--s3) var(--s1) 0; border-top: 1px solid var(--edge-soft);
  font-size: 13px; color: var(--ink-mute); }
.wv-palette-flash > header { border-radius: var(--r-md);
  animation: wv-palette-pulse calc(950ms / var(--speed)) var(--ease) 2; }
@keyframes wv-palette-pulse {
  0% { box-shadow: 0 0 0 0 transparent; }
  30% { box-shadow: 0 0 0 3px var(--accent), 0 0 0 11px color-mix(in srgb, var(--accent) 22%, transparent); }
  100% { box-shadow: 0 0 0 0 transparent; } }

/* ---- wave: steps ---- */
/* ---------- c29-steps: the stepper (Wave C) ----------
   The connector fill and the check draw ride --speed and
   --ease-give, the active node breathes on --speed, and the
   accent knob recolors the whole run. Wide lays the four
   stages across; narrow stacks them and the line turns with
   them, driven by the same --wv-f position value. */
c29-steps { display: block; --wv-f: 0%; }
c29-steps .wv-steps-wrap { position: relative; }
c29-steps .wv-steps-line { position: absolute; top: 22px; left: 12.5%; right: 12.5%; height: 4px;
  border-radius: 99px; background: var(--wash-2); overflow: hidden; }
c29-steps .wv-steps-fill { display: block; height: 100%; width: var(--wv-f);
  background: var(--accent); border-radius: inherit;
  transition: width calc(520ms / var(--speed)) var(--ease-give), height calc(520ms / var(--speed)) var(--ease-give); }
c29-steps .wv-steps-rail { position: relative; z-index: 1; display: flex; list-style: none; margin: 0; padding: 0; }
c29-steps .wv-steps-rail li { flex: 1; min-width: 0; display: grid; justify-items: center; gap: var(--s2); text-align: center; }
c29-steps .wv-steps-node { position: relative; width: 44px; height: 44px; border-radius: 50%;
  display: grid; place-items: center; background: var(--surface);
  box-shadow: inset 0 0 0 2px var(--edge);
  transition: background var(--d2) var(--ease), box-shadow var(--d2) var(--ease); }
c29-steps .wv-steps-num { font-size: 15px; font-weight: 700; font-variant-numeric: tabular-nums; color: var(--ink-soft);
  transition: opacity var(--d1) var(--ease); }
c29-steps .wv-steps-check { position: absolute; width: 22px; height: 22px; fill: none;
  stroke: var(--on-accent); stroke-width: 2.4; stroke-linecap: round; stroke-linejoin: round; }
c29-steps .wv-steps-check path { stroke-dasharray: 24; stroke-dashoffset: 24;
  transition: stroke-dashoffset var(--d3) var(--ease-give); }
c29-steps .wv-steps-rail li.is-done .wv-steps-node { background: var(--accent); box-shadow: none; }
c29-steps .wv-steps-rail li.is-done .wv-steps-num { opacity: 0; }
c29-steps .wv-steps-rail li.is-done .wv-steps-check path { stroke-dashoffset: 0; }
c29-steps .wv-steps-rail li.is-active .wv-steps-node { background: var(--accent-soft);
  box-shadow: inset 0 0 0 2px var(--accent);
  animation: wv-steps-pulse calc(1800ms / var(--speed)) var(--ease) infinite; }
c29-steps .wv-steps-rail li.is-active .wv-steps-num { color: var(--accent-text); }
@keyframes wv-steps-pulse { 0% { box-shadow: inset 0 0 0 2px var(--accent), 0 0 0 0 color-mix(in srgb, var(--accent) 32%, transparent); }
  70% { box-shadow: inset 0 0 0 2px var(--accent), 0 0 0 10px transparent; }
  100% { box-shadow: inset 0 0 0 2px var(--accent), 0 0 0 0 transparent; } }
c29-steps .wv-steps-lab { display: grid; gap: 1px; }
c29-steps .wv-steps-lab b { font-size: 14px; font-weight: 650; line-height: 1.3; }
c29-steps .wv-steps-state { font-size: 13px; color: var(--ink-mute); }
c29-steps .wv-steps-rail li.is-active .wv-steps-state { color: var(--accent-text); font-weight: 650; }
c29-steps .wv-steps-status { margin-top: var(--s5); font-size: 15px; font-weight: 600; line-height: 1.45; min-height: 22px; }
c29-steps .wv-steps-ctl { margin-top: var(--s3); }
c29-steps .wv-steps-ctl .t-btn[disabled] { opacity: .45; pointer-events: none; }
@media (max-width: 520px) {
  c29-steps .wv-steps-rail { flex-direction: column; align-items: stretch; gap: var(--s4); }
  c29-steps .wv-steps-rail li { grid-template-columns: 44px minmax(0, 1fr); justify-items: start; text-align: left; align-items: center; }
  c29-steps .wv-steps-line { top: 22px; bottom: 22px; left: 20px; right: auto; width: 4px; height: auto; }
  c29-steps .wv-steps-fill { width: 100%; height: var(--wv-f); }
}

/* ---- wave: tuner ---- */
/* ---------- c29-themetuner: the theme tuner (Wave D, Delight) ---------- */
c29-themetuner { display: block; --wv-paper: var(--sand-100); --wv-pane: var(--sea-100); }
c29-themetuner .wv-tuner-root { display: grid; gap: var(--s4); }
c29-themetuner .wv-tuner-row { display: grid; gap: var(--s2); }
c29-themetuner .wv-tuner-label { font-size: 13px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--ink-mute); }
c29-themetuner .wv-tuner-swatches { display: flex; flex-wrap: wrap; gap: var(--s2); }
c29-themetuner .wv-tuner-swatch { display: inline-flex; align-items: center; gap: 8px; min-height: 44px; padding: 6px 14px 6px 8px; border: 0; border-radius: var(--r-pill); background: var(--wash); color: var(--ink); font-size: 14px; font-weight: 600; cursor: pointer; transition: background var(--d2) var(--ease), box-shadow var(--d2) var(--ease), transform var(--d1) var(--ease); }
c29-themetuner .wv-tuner-swatch i { width: 26px; height: 26px; border-radius: 50%; background: var(--wv-sw); box-shadow: inset 0 0 0 1px var(--edge-soft); flex: none; }
c29-themetuner .wv-tuner-swatch[aria-pressed="true"] { background: var(--surface); box-shadow: 0 0 0 2px var(--ink); }
c29-themetuner .wv-tuner-swatch:active { transform: scale(.97); }
@media (hover: hover) {
  c29-themetuner .wv-tuner-swatch:hover { background: var(--wash-2); }
  c29-themetuner .wv-tuner-swatch[aria-pressed="true"]:hover { background: var(--surface); }
}
c29-themetuner .wv-tuner-actions { display: flex; align-items: center; gap: var(--s3); flex-wrap: wrap; }
c29-themetuner .wv-tuner-read { font-size: 14px; color: var(--ink-soft); }
c29-themetuner .wv-tuner-preview { border-radius: var(--corner); background: var(--wv-paper); box-shadow: var(--hair); padding: var(--s4); transition: background-color var(--d3) var(--ease), border-radius var(--d3) var(--ease-give); }
c29-themetuner .wv-tuner-pane { display: flex; align-items: center; gap: var(--s2); padding: var(--s3); border-radius: calc(var(--corner) - 6px); background: var(--wv-pane); transition: background-color var(--d3) var(--ease), border-radius var(--d3) var(--ease-give); }
c29-themetuner .wv-tuner-price { margin-left: auto; font: 750 20px/1 var(--font-display); font-variant-numeric: tabular-nums; }
c29-themetuner .wv-tuner-title { margin-top: var(--s3); font: 750 19px/1.25 var(--font-display); letter-spacing: var(--track-head); }
c29-themetuner .wv-tuner-line { margin-top: 2px; font-size: 14px; line-height: 1.5; color: var(--ink-soft); }
c29-themetuner .wv-tuner-buy { margin-top: var(--s3); transition: background var(--d2) var(--ease), color var(--d2) var(--ease), transform var(--d1) var(--ease); }

/* ---- wave: ticker ---- */
/* ---------- c29-ticker: the notice ticker (Wave D, Delight) ---------- */
c29-ticker { display: block; }
c29-ticker .wv-ticker-viewport { overflow: hidden; border-radius: var(--corner); background: var(--sunken); box-shadow: inset 0 0 0 1px var(--edge-soft); transition: border-radius var(--d3) var(--ease-give); }
c29-ticker .wv-ticker-track { display: flex; width: max-content; animation: wv-ticker-scroll calc(30s / var(--speed)) linear infinite; }
c29-ticker .wv-ticker-group { display: flex; align-items: center; gap: var(--s2); padding: var(--s2); flex: none; }
c29-ticker .wv-ticker-list { display: flex; flex-wrap: wrap; gap: var(--s2); padding: var(--s2); }
c29-ticker .wv-ticker-item { flex: none; display: inline-flex; align-items: center; min-height: 44px; padding: 0 14px; border: 0; border-radius: var(--r-pill); background: var(--raised); box-shadow: var(--hair); color: var(--ink); font-size: 14px; font-weight: 550; white-space: nowrap; font-variant-numeric: tabular-nums; }
c29-ticker button.wv-ticker-item { cursor: pointer; transition: background var(--d2) var(--ease), transform var(--d1) var(--ease); }
c29-ticker button.wv-ticker-item:active { transform: scale(.97); }
c29-ticker.is-held .wv-ticker-track, c29-ticker:focus-within .wv-ticker-track { animation-play-state: paused; }
c29-ticker.is-static .wv-ticker-track { animation: none; }
@media (hover: hover) {
  c29-ticker:hover .wv-ticker-track { animation-play-state: paused; }
  c29-ticker button.wv-ticker-item:hover { background: var(--wash); }
}
@keyframes wv-ticker-scroll { to { transform: translateX(-50%); } }
c29-ticker .wv-ticker-pin { display: flex; align-items: center; gap: var(--s2); flex-wrap: wrap; margin-top: var(--s3); min-height: 44px; font-size: 14px; }
c29-ticker .wv-ticker-pin-note { color: var(--ink-mute); font-size: 14px; }
c29-ticker .wv-ticker-pin-label { font-size: 13px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--accent-text); }
c29-ticker .wv-ticker-pin-text { font-weight: 600; font-variant-numeric: tabular-nums; }

/* ---- wave: gauge ---- */
/* ---------- c29-gauge: the level gauge (Wave D, Delight) ---------- */
c29-gauge { display: block; }
c29-gauge .wv-gauge-frame { border-radius: var(--corner); background: var(--sunken); box-shadow: inset 0 0 0 1px var(--edge-soft); padding: var(--s4); transition: border-radius var(--d3) var(--ease-give); }
c29-gauge .wv-gauge-svg { width: 100%; height: auto; display: block; }
c29-gauge .wv-gauge-line { stroke: var(--ink); stroke-width: 1; fill: none; stroke-linecap: round; stroke-linejoin: round; }
c29-gauge .wv-gauge-solid { fill: var(--ink); }
c29-gauge .wv-gauge-hit { fill: transparent; }
c29-gauge .wv-gauge-water { transition: transform var(--d4) var(--ease-give); }
c29-gauge .wv-gauge-waterfill { fill: color-mix(in srgb, var(--sea-300) 42%, transparent); }
c29-gauge .wv-gauge-waterline { stroke: var(--sea-700); stroke-width: 1; stroke-linecap: round; }
c29-gauge .wv-gauge-wave { stroke: var(--sea-700); stroke-width: 1; fill: none; stroke-linecap: round; opacity: .65; }
c29-gauge .wv-gauge-beam { fill: var(--sun-300); opacity: 0; transition: opacity var(--d3) var(--ease); }
c29-gauge[data-lit="true"] .wv-gauge-beam { opacity: .48; }
c29-gauge .wv-gauge-lampglass { fill: var(--sun-300); opacity: 0; transition: opacity var(--d2) var(--ease); }
c29-gauge[data-lit="true"] .wv-gauge-lampglass { opacity: 1; }
c29-gauge .wv-gauge-lampbtn { cursor: pointer; }
c29-gauge .wv-gauge-lampbtn:focus-visible { outline: none; }
c29-gauge .wv-gauge-lampbtn:focus-visible .wv-gauge-lampframe { stroke: var(--accent); stroke-width: 2; }
@media (hover: hover) {
  c29-gauge .wv-gauge-lampbtn:hover .wv-gauge-lampframe { stroke: var(--accent-text); }
}
c29-gauge .wv-gauge-read { margin-top: var(--s3); font-size: 14px; font-weight: 600; font-variant-numeric: tabular-nums; }
c29-gauge .wv-gauge-ctl { display: flex; gap: var(--s2); flex-wrap: wrap; margin-top: var(--s3); }

/* ---------- release cards + wide page (download surfaces) ----------
   Added 2026-10-19 by the download-page run (TIDE FIRST: pattern lands in
   canon with its adversary round BEFORE the page consumes it; round in
   brains projects/download-page/DOWNLOAD-PAGE-2026-10-19.md).
   Anatomy: .t-page.is-wide for marketing-width surfaces; .t-rel-grid of
   .t-rel cards (compose with .t-card). Each card: .t-rel-head (name +
   honest status .t-tag), .t-rel-shot (a REAL screenshot, or .is-empty —
   the honest empty state that says the shot lands with the first build;
   never a mockup), .t-rel-req (requirement line), .t-rel-act (one
   action). Status tones are the canon tag tones: is-ok available,
   is-warn in the workshop, plain tag listed. */
@layer c29.components {
  .t-page.is-wide { max-width: 1120px; }
  .t-rel-grid { display: grid; gap: var(--s4); grid-template-columns: minmax(0, 1fr); }
  @media (min-width: 720px) { .t-rel-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
  @media (min-width: 1080px) { .t-rel-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
  .t-rel { display: grid; gap: var(--s3); align-content: start; }
  .t-rel-head { display: flex; align-items: center; gap: var(--s2); flex-wrap: wrap; }
  .t-rel-name { font-family: var(--font-display); font-weight: 750; font-size: var(--fs-lg); letter-spacing: var(--track-head); }
  .t-rel-shot { border-radius: calc(var(--corner) - var(--s2)); background: var(--wash); overflow: hidden; box-shadow: inset 0 0 0 1px var(--edge); }
  .t-rel-shot img { display: block; width: 100%; height: auto; }
  .t-rel-shot.is-empty { display: grid; place-items: center; padding: var(--s6) var(--s4); text-align: center; }
  .t-rel-req { font-size: var(--fs-sm); color: var(--ink-mute); }
  .t-rel-act { display: flex; align-items: center; gap: var(--s2); flex-wrap: wrap; margin-top: auto; padding-top: var(--s2); }
}

/* ---------- device stage — staged product scenes (download surfaces) ----------
   Added 2026-10-19 by the download-page v2 run (TIDE FIRST: pattern lands
   in canon with its adversary round BEFORE the page consumes it; round in
   brains projects/download-page/DOWNLOAD-PAGE-2026-10-19.md).
   Anatomy: .t-stage is the composed scene a device stands in (a calm
   panel; captions sit inside it, under the device). .t-dev is device
   HARDWARE — a bezel the real capture sits inside: .is-mac (laptop,
   with a base foot), .is-phone (handset), .is-browser (a browser bar
   over a web capture; the bar names the real domain the capture came
   from). Frames are presentation only: the pixels inside are the
   untouched capture — never redrawn, never a mockup. .t-dev-crop crops
   a capture's own surround (window-shadow margins, letterbox) in CSS:
   the author measures the real content box of the capture and sets
   --cx/--cy/--cw/--ch (fractions of the ORIGINAL image) plus the
   visible aspect-ratio inline; the frame then shows exactly that
   region of the real pixels. .t-dev-screen.is-empty is the honest
   empty state inside a frame: the slot, labeled, shot pending. */
@layer c29.components {
  .t-stage { position: relative; border-radius: var(--corner); overflow: hidden;
    background: linear-gradient(180deg, color-mix(in srgb, var(--sea-100) 60%, var(--surface)) 0%, var(--surface) 78%);
    box-shadow: inset 0 0 0 1px var(--edge); padding: var(--s8) var(--s4) var(--s5); }
  @media (min-width: 720px) { .t-stage { padding: var(--s12) var(--s8) var(--s6); } }
  .t-stage .t-dev { margin-inline: auto; }
  .t-dev { --dev-bezel: #11161B; position: relative; background: var(--dev-bezel);
    border-radius: 22px; padding: 10px; box-shadow: 0 0 0 1px var(--edge), var(--lift-hi); }
  .t-dev-screen { border-radius: 13px; overflow: hidden; background: var(--wash); }
  .t-dev-screen img { display: block; width: 100%; height: auto; }
  .t-dev-screen.is-empty { display: grid; place-items: center; padding: var(--s6) var(--s4);
    text-align: center; background: var(--sunken); }
  .t-dev.is-mac { margin-bottom: 14px; }
  .t-dev.is-mac::after { content: ""; position: absolute; left: -3%; right: -3%; bottom: -13px; height: 13px;
    background: linear-gradient(180deg, #2B333B, #12161B); border-radius: 3px 3px 12px 12px;
    box-shadow: 0 10px 18px -10px rgba(20, 16, 8, .45); }
  .t-dev.is-phone { border-radius: 42px; padding: 11px; max-width: 330px; }
  .t-dev.is-phone .t-dev-screen { border-radius: 32px; }
  .t-dev.is-phone .t-dev-screen.is-empty { aspect-ratio: 9 / 18.6; padding: var(--s4); }
  .t-dev.is-browser { background: var(--raised); padding: 0; border-radius: 16px; overflow: hidden; }
  .t-dev-bar { display: flex; align-items: center; gap: var(--s3); padding: var(--s2) var(--s3); background: var(--sunken); }
  .t-dev-dots { display: flex; gap: 6px; flex: none; }
  .t-dev-dots i { width: 10px; height: 10px; border-radius: 50%; background: var(--ink-4); }
  .t-dev-url { flex: 1; min-width: 0; text-align: center; font-family: var(--font-mono); font-size: var(--fs-cap);
    color: var(--ink-mute); background: var(--raised); border-radius: var(--r-pill);
    padding: var(--s1) var(--s3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .t-dev.is-browser .t-dev-screen { border-radius: 0; }
  .t-dev-crop { position: relative; overflow: hidden; }
  .t-dev-crop img { position: absolute; max-width: none;
    width: calc(100% / var(--cw)); height: auto;
    left: calc(var(--cx) / var(--cw) * -100%); top: calc(var(--cy) / var(--ch) * -100%); }
}

/* ============================================================
   PRESENCE <c29-presence> · BOARD <c29-board> · LIST REORDER
   (Workbench lane, 2026-10-19 — see tide.js for the contracts)
   ============================================================ */
@layer c29.components {
  c29-presence { display: flex; align-items: center; gap: var(--s2); flex-wrap: wrap; min-height: 32px; }
  c29-presence .pr-stack { display: inline-flex; align-items: center; }
  c29-presence .pr-av { width: 28px; height: 28px; border-radius: 50%; display: inline-grid; place-items: center;
    font: 700 11px/1 var(--font-ui); color: #fff; box-shadow: 0 0 0 2px var(--bg); margin-left: -6px; flex: none; }
  c29-presence .pr-av:first-child { margin-left: 0; }
  c29-presence .pr-more { background: var(--wash-2); color: var(--ink-soft); font-size: 10px; }
  c29-presence .pr-label { font-size: var(--fs-sm); color: var(--ink-soft); font-weight: 600; }
  c29-presence .pr-note { font-size: var(--fs-sm); color: var(--accent-text); font-weight: 600; }

  c29-board { display: block; position: relative; overflow: hidden; background:
      radial-gradient(circle, var(--edge-soft) 1px, transparent 1.2px) 0 0 / 24px 24px, var(--sunken);
    border-radius: var(--corner); box-shadow: var(--hair); touch-action: none; min-height: 320px; }
  c29-board .bd-world { position: absolute; left: 0; top: 0; width: 6000px; height: 6000px; transform-origin: 0 0; }
  c29-board .bd-links { position: absolute; left: 0; top: 0; overflow: visible; pointer-events: none; }
  c29-board .bd-line { stroke: var(--ink-mute); stroke-width: 2; opacity: .55; }
  c29-board .bd-item { position: absolute; min-height: 72px; padding: var(--s3); border-radius: 10px;
    background: var(--sand-50); color: var(--ink-1); box-shadow: 0 1px 2px rgba(30,30,20,.18), 0 8px 20px -12px rgba(30,30,20,.35);
    font-size: var(--fs-sm); line-height: 1.4; cursor: grab; overflow-wrap: anywhere; user-select: none; -webkit-user-select: none; }
  c29-board .bd-item.is-drag { cursor: grabbing; box-shadow: var(--lift-hi); }
  c29-board .bd-item:focus-visible { outline: none; box-shadow: var(--focus); }
  c29-board .bd-item[data-color="sand"] { background: var(--sand-50); }
  c29-board .bd-item[data-color="sun"] { background: var(--sun-100); }
  c29-board .bd-item[data-color="sea"] { background: var(--sea-100); }
  c29-board .bd-item[data-color="coral"] { background: var(--29d-coral-100); }
  c29-board .bd-item[data-color="sky"] { background: #DCEBF7; }
  c29-board .bd-item[data-color="lilac"] { background: #E7E1F5; }
  [data-theme="dark"] c29-board .bd-item { background: var(--raised); color: var(--ink); }
  [data-theme="dark"] c29-board .bd-item[data-color="sun"] { background: #4A3A1E; }
  [data-theme="dark"] c29-board .bd-item[data-color="sea"] { background: #173F39; }
  [data-theme="dark"] c29-board .bd-item[data-color="coral"] { background: #4E2A1E; }
  [data-theme="dark"] c29-board .bd-item[data-color="sky"] { background: #1E3448; }
  [data-theme="dark"] c29-board .bd-item[data-color="lilac"] { background: #322A4E; }
  c29-board .bd-check { margin-right: 6px; }
  c29-board.is-connect .bd-item { cursor: crosshair; }
  c29-board .bd-cursor { position: absolute; pointer-events: none; z-index: 5; transform: translate(-2px, -2px); }
  c29-board .bd-cursor-dot { display: block; width: 12px; height: 12px; border-radius: 50% 50% 50% 0; transform: rotate(-45deg); box-shadow: 0 0 0 1.5px #fff; }
  c29-board .bd-cursor-name { position: absolute; left: 12px; top: 10px; color: #fff; font: 650 11px/1 var(--font-ui); padding: 3px 7px; border-radius: 99px; white-space: nowrap; }
  c29-board .bd-tools { position: absolute; right: var(--s3); bottom: calc(var(--s3) + var(--safe-b, 0px)); display: flex; gap: var(--s2); z-index: 6; }
  c29-board .bd-tool { min-width: 44px; height: 44px; padding: 0 var(--s3); border: 0; border-radius: var(--r-pill);
    background: var(--surface); color: var(--ink); box-shadow: var(--hair), var(--lift); font: 700 17px/1 var(--font-ui); cursor: pointer; }
  c29-board .bd-fit { font-size: var(--fs-sm); }

  c29-list .ls-handle { flex: none; width: 40px; min-height: 44px; align-self: stretch; border: 0; background: transparent;
    color: var(--ink-mute); font-size: 17px; cursor: grab; touch-action: none; border-radius: 0; }
  c29-list .ls-handle:active { cursor: grabbing; }
  c29-list > li.is-dragging { box-shadow: var(--lift-hi); background: var(--raised); }
}

/* ============================================================
 * FLEET BOARD FAMILY — integrated 2026-10-20 (Fleet 1).
 * c29-band · c29-agent · c29-routines · c29-factory · c29-runtime
 * Contracts: FLEET-BOARD-CAMPAIGN-2026-10-20.md §2 (as amended
 * by Sacca Round 0, §6). Fragments follow verbatim.
 * ============================================================ */
/* ============================================================
 * TIDE — FLEET BOARD  <c29-band> + <c29-agent>
 * Fleet-Board campaign Round 1 · Lane A (2026-10-20), with the
 * Sacca Round 0 amendments to c29-band folded in (see the band
 * contracts below). The specialist band + the agent card: the
 * poster's bands of named bots, rebuilt as a tool — every count
 * derived, every state carried by dot + word + tone, stale
 * LOOKS stale, and a band with an error or a stale child says
 * so in its header instead of reading calm.
 *
 * INTEGRATION: append to tide.css verbatim (this fragment
 * carries its own @layer c29.components block, the way the
 * presence/board block already rides at the end of tide.css).
 * Tokens only: the only literals are c29-board's sky/lilac tone
 * grounds, reused verbatim (light + dark) for tone parity with
 * .bd-item[data-color]; every other ground is a token whose
 * dark value the token layer already flips.
 * ============================================================ */
@layer c29.components {

/* ---------- c29-band: the specialist band ----------
   Contracts:
   - the count is DERIVED: JS counts element children and
     mirrors data-count; there is no host `count` attribute,
     by design — a count that can drift from the cards under
     it is a lie.
   - data-attention is derived the same way (error + stale
     children) and mirrored; when it is > 0 the header renders
     it as a worded warn tag — the exception truth rides the
     header, never colour alone (c29-checklist law).
   - data-state precedence (Sacca Round 0): attention
     (≥1 error or derived-stale child) > live (≥1 child not
     stale/parked/error/off) > quiet (children, none live)
     > empty.
   - children render severity-first by construction — error,
     stale, working, idle, parked — re-sorted by the component
     for display; host DOM order is not the host's burden.
   - tint is a header wash in the tone ground + a hairline,
     never a filled panel competing with the card grounds.
     Coral is ABSENT from the tone list: money law (RS-F41). */
c29-band { display: block; min-width: 0; background: var(--surface);
  border-radius: var(--corner); box-shadow: var(--hair); overflow: hidden; }
c29-band .bn-head { display: flex; align-items: center; gap: var(--s2); flex-wrap: wrap;
  padding: var(--s3) var(--s4); background: var(--sand-50);
  box-shadow: inset 0 -1px 0 var(--edge-soft); }
c29-band[tone="sun"] .bn-head { background: var(--sun-100); }
c29-band[tone="sea"] .bn-head { background: var(--sea-100); }
c29-band[tone="sky"] .bn-head { background: #DCEBF7; }
c29-band[tone="lilac"] .bn-head { background: #E7E1F5; }
[data-theme="dark"] c29-band .bn-head { background: var(--raised); }
[data-theme="dark"] c29-band[tone="sun"] .bn-head { background: var(--warn-bg); }
[data-theme="dark"] c29-band[tone="sea"] .bn-head { background: var(--sea-100); }
[data-theme="dark"] c29-band[tone="sky"] .bn-head { background: #1E3448; }
[data-theme="dark"] c29-band[tone="lilac"] .bn-head { background: #322A4E; }
c29-band .bn-label { font: 700 var(--fs-lg)/1.25 var(--font-display);
  letter-spacing: var(--track-head); color: var(--ink);
  min-width: 0; overflow-wrap: anywhere; }
c29-band .bn-count { flex: none; }
c29-band .bn-attn { flex: none; }
c29-band .bn-attn[hidden] { display: none; }
c29-band .bn-meta { margin-left: auto; text-align: right;
  font-size: var(--fs-sm); color: var(--ink-mute);
  min-width: 0; overflow-wrap: anywhere; }
c29-band .bn-meta[hidden] { display: none; }
c29-band .bn-grid { display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(230px, 100%), 1fr));
  gap: var(--s3); padding: var(--s4); }
c29-band .bn-grid[hidden] { display: none; }
c29-band .bn-grid > * { min-width: 0; }
c29-band .bn-empty { padding: var(--s4); font-size: var(--fs-sm); color: var(--ink-mute); }
c29-band .bn-empty[hidden] { display: none; }

/* ---------- c29-agent: the agent card ----------
   Contracts:
   - host-fed truth only: the component renders name, role,
     state and seen from attributes and invents nothing; no
     `seen` → no seen line, ever (never a fake "just now").
   - derived stale stamps ONLY over working/idle when `seen`
     is older than `stale-after` (default 3600s); a host-set
     stale/parked/error always wins.
   - state is carried by dot + WORD + tone together, never
     colour alone (c29-checklist law); error is the only
     bad-tone state; parked is calm — a neutral tag, no warn.
   - stale LOOKS stale: the avatar desaturates, the card drops
     its shadow for the flat surface ground, the name mutes.
   - names and roles wrap, never truncate
     (overflow-wrap:anywhere, c29-tile precedent).
   - the avatar is the house medallion (c29-presence
     language): initials on a token tone — identity colour,
     small-area only, never a state signal. Initials ride
     var(--ink-1), which flips correctly in both themes. */
c29-agent { display: flex; gap: var(--s3); align-items: flex-start;
  position: relative; min-width: 0; height: 100%;
  background: var(--raised); border-radius: var(--r-md);
  box-shadow: var(--hair); padding: var(--s3); }
c29-agent .ag-av { flex: none; width: 36px; height: 36px; border-radius: 50%;
  display: grid; place-items: center; overflow: hidden;
  background: var(--sand-50); color: var(--ink-1);
  font: 700 13px/1 var(--font-display); letter-spacing: .02em;
  box-shadow: inset 0 0 0 1px var(--edge-soft); }
c29-agent[tone="sun"] .ag-av { background: var(--sun-100); }
c29-agent[tone="sea"] .ag-av { background: var(--sea-100); }
c29-agent[tone="coral"] .ag-av { background: var(--29d-coral-100); }
c29-agent[tone="sky"] .ag-av { background: #DCEBF7; }
c29-agent[tone="lilac"] .ag-av { background: #E7E1F5; }
[data-theme="dark"] c29-agent[tone="sun"] .ag-av { background: var(--warn-bg); }
[data-theme="dark"] c29-agent[tone="sky"] .ag-av { background: #1E3448; }
[data-theme="dark"] c29-agent[tone="lilac"] .ag-av { background: #322A4E; }
c29-agent .ag-main { flex: 1 1 auto; min-width: 0; display: grid; gap: 2px; align-content: start; }
c29-agent .ag-name { font-size: var(--fs-body); font-weight: 600; line-height: 1.3;
  color: var(--ink); overflow-wrap: anywhere; }
c29-agent .ag-role { font-size: var(--fs-sm); line-height: 1.4;
  color: var(--ink-soft); overflow-wrap: anywhere; }
c29-agent .ag-role[hidden] { display: none; }
c29-agent .ag-state { display: flex; align-items: center; gap: var(--s2); margin-top: var(--s1); }
c29-agent .ag-dot { flex: none; width: 8px; height: 8px; border-radius: 50%;
  background: var(--ink-mute); }
c29-agent .ag-word { font-size: var(--fs-cap); font-weight: 650;
  letter-spacing: var(--track-cap); text-transform: uppercase; color: var(--ink-soft); }
c29-agent[data-state="working"] .ag-dot { background: var(--accent); }
c29-agent[data-state="working"] .ag-word { color: var(--accent-text); }
c29-agent[data-state="stale"] .ag-dot { background: var(--warn); }
c29-agent[data-state="stale"] .ag-word { color: var(--warn-text); }
c29-agent[data-state="parked"] .ag-dot { background: var(--ink-mute); }
c29-agent[data-state="parked"] .ag-word { color: var(--ink-mute);
  background: var(--wash); padding: 2px 8px; border-radius: 99px; }
c29-agent[data-state="error"] .ag-dot { background: var(--bad); }
c29-agent[data-state="error"] .ag-word { color: var(--bad-text); }
c29-agent .ag-seen { font-size: var(--fs-cap); color: var(--ink-mute); overflow-wrap: anywhere; }
c29-agent .ag-seen[hidden] { display: none; }
/* Stale, worn on the card's face (contract above). */
c29-agent[data-state="stale"] { background: var(--surface); box-shadow: none; }
c29-agent[data-state="stale"] .ag-av { filter: saturate(.25); }
c29-agent[data-state="stale"] .ag-name { color: var(--ink-mute); }
/* The stretched link (c29-tile pattern): the whole card is
   the tap target; the open event, not the anchor, decides. */
c29-agent .ag-hit { position: absolute; inset: 0; z-index: 1; border-radius: var(--r-md); }
c29-agent .ag-hit:focus-visible { outline: none; box-shadow: var(--focus); }
}

/* ============================================================
 * TIDE — FLEET BOARD PATTERNS  <c29-routines> · <c29-factory>
 * Fleet 1 · Lane B fragment (Fleet-Board campaign, 2026-10-20).
 * INTEGRATION: append inside @layer c29.components in tide.css,
 * after the c29-list block. Pair with routines-factory.js (a
 * tide.js section) + the two entries in registry.json.
 *
 * Both components are host-fed truth only (NO-FABRICATION):
 * the host writes the rows, the component renders them. Neither
 * component invents a routine, a task, an owner, or a status,
 * and neither carries a control — rows are display, not buttons.
 * Tokens only; both themes; layout holds with JS dead (the
 * heading is the one JS-rendered part, as on c29-sweep).
 * ============================================================ */

@layer c29.components {

/* ======================================================
   ROUTINES  <c29-routines>   (Fleet-Board P4: the routines rail)
   The honesty line IS the pattern: "Empty runs stay silent" —
   with the Round 0 amendment (Sacca): silence is an honesty
   rule for EMPTY SUCCESS only, never a hiding place for
   failure. A row may carry the host-fed outcome of its last
   run (li[data-result] = ok | failed | skipped | never; absent
   = not rendered, never defaulted to ok) and the moment it
   last completed (li[data-last], ISO/ms):
     ok      → silence: no tag, no badge, no noise
     skipped → silence: no tag
     failed  → ALWAYS the word "Failed" (.t-tag.is-bad) + the
               "last ran …" line — failure is never silent
     never   → the words "Never ran" (.t-tag.is-warn)
   The "last ran …" line is quiet, in the c29-jobrun elapsed
   style, computed from data-last and nothing else; no
   data-last = no line, never an invented age. Paused keeps
   its word ("Paused", a plain .t-tag) plus the mute — never
   grey alone (c29-checklist law). When any child is failed
   the rail's data-state reads `attention`, so a composition
   can surface it without scraping rows.
   Host shape:
     <c29-routines heading="Routines" note="Empty runs stay silent">
       <li data-when="Weekdays 7:30" data-result="ok"
           data-last="2026-10-20T07:30:00-07:00">Morning brief</li>
       <li data-when="Daily 21:00" data-state="off">Inbox sweep</li>
     </c29-routines>
   Host data-state = rows | empty | attention (derived when
   any child is failed); empty renders the `empty` line
   (default "No routines scheduled.").
   ====================================================== */
c29-routines { display: block; background: var(--surface); border-radius: var(--corner); box-shadow: var(--hair); padding: var(--s5); }
c29-routines > li { list-style: none; margin: 0; padding: 0; }
c29-routines > li::marker { content: none; }
c29-routines .ru-head { margin-bottom: var(--s2); }
c29-routines .ru-title { font: 700 var(--fs-lg)/1.2 var(--font-display); letter-spacing: var(--track-head); overflow-wrap: anywhere; }
c29-routines .ru-note { margin-top: var(--s1); font-size: var(--fs-sm); line-height: 1.5; color: var(--ink-mute); overflow-wrap: anywhere; }
c29-routines .ru-note:empty { display: none; }
/* The adopted row: chip + name block (+ outcome words when
   the host fed an outcome that must not stay silent). */
c29-routines .ru-row { display: flex; align-items: center; gap: var(--s3); min-height: 52px; padding: var(--s3) 0; }
c29-routines .ru-row + .ru-row { box-shadow: inset 0 1px 0 var(--edge-soft); }
c29-routines .ru-main { flex: 1 1 auto; min-width: 0; display: grid; gap: 1px; }
/* The "last ran …" line: quiet by design — the words and the
   tag carry the failure, this line only says when. */
c29-routines .ru-last { font-size: 13px; line-height: 1.4; color: var(--ink-mute); overflow-wrap: anywhere; }
c29-routines .ru-tags { flex: none; display: flex; align-items: center; gap: var(--s2); }
c29-routines .ru-chip { flex: none; display: inline-flex; align-items: center; min-height: 26px; max-width: 48%;
  padding: 2px 10px; border-radius: var(--r-pill); background: var(--wash-2); color: var(--ink-soft);
  font-family: var(--font-mono); font-size: 12px; font-weight: 600; line-height: 1.3; overflow-wrap: anywhere; }
c29-routines .ru-name { flex: 1 1 auto; min-width: 0; font-size: 15px; font-weight: 600; line-height: 1.35; color: var(--ink); overflow-wrap: anywhere; }
c29-routines .ru-row[data-state="off"] .ru-name { color: var(--ink-mute); font-weight: 550; }
c29-routines .ru-row[data-state="off"] .ru-chip { color: var(--ink-mute); }
c29-routines .ru-paused { flex: none; }
/* JS-dead fallback (c29-checklist precedent): before adoption
   the schedule still paints as a chip from the attribute and a
   paused row still says Paused — the words are the host's own
   attribute values, never component inventions. */
c29-routines > li:not(.ru-row) { display: flex; align-items: center; gap: var(--s3); min-height: 52px; padding: var(--s3) 0;
  font-size: 15px; font-weight: 600; line-height: 1.35; color: var(--ink); overflow-wrap: anywhere; }
c29-routines > li:not(.ru-row) + li:not(.ru-row) { box-shadow: inset 0 1px 0 var(--edge-soft); }
c29-routines > li:not(.ru-row)::before { content: attr(data-when); flex: none; display: inline-flex; align-items: center; min-height: 26px; max-width: 48%;
  padding: 2px 10px; border-radius: var(--r-pill); background: var(--wash-2); color: var(--ink-soft);
  font-family: var(--font-mono); font-size: 12px; font-weight: 600; line-height: 1.3; overflow-wrap: anywhere; }
c29-routines > li[data-state="off"]:not(.ru-row) { color: var(--ink-mute); font-weight: 550; }
c29-routines > li[data-state="off"]:not(.ru-row)::before { color: var(--ink-mute); }
c29-routines > li[data-state="off"]:not(.ru-row)::after { content: "Paused"; flex: none; display: inline-flex; align-items: center; min-height: 24px;
  padding: 0 9px; border-radius: 99px; background: var(--wash); color: var(--ink-soft);
  font-family: var(--font-ui); font-size: 12px; font-weight: 600; white-space: nowrap; }
/* JS-dead outcome words: failure and never-ran are words even
   before adoption (the one ::after slot means paused+failed
   together shows the failure word — failure wins the slot;
   JS adoption renders both). The "last ran" line itself is
   JS-rendered: CSS cannot format an elapsed honestly. */
c29-routines > li[data-result="failed"]:not(.ru-row)::after { content: "Failed"; flex: none; display: inline-flex; align-items: center; min-height: 24px;
  padding: 0 9px; border-radius: 99px; background: var(--bad-bg); color: var(--bad-text);
  font-family: var(--font-ui); font-size: 12px; font-weight: 600; white-space: nowrap; }
c29-routines > li[data-result="never"]:not(.ru-row)::after { content: "Never ran"; flex: none; display: inline-flex; align-items: center; min-height: 24px;
  padding: 0 9px; border-radius: 99px; background: var(--warn-bg); color: var(--warn-text);
  font-family: var(--font-ui); font-size: 12px; font-weight: 600; white-space: nowrap; }
/* The empty line: one quiet sentence, no box, no apology panel. */
c29-routines .ru-empty { padding: var(--s3) 0 var(--s1); font-size: var(--fs-sm); line-height: 1.5; color: var(--ink-mute); overflow-wrap: anywhere; }
c29-routines[data-state="empty"] > li { display: none; }

/* ======================================================
   FACTORY  <c29-factory>   (Fleet-Board P8: the task table)
   Task ID / Owner / Status, host truth in a calm table. The
   steal inside the steal is Parked as a first-class status:
   parked is a DECISION, not a failure — it renders as a plain
   tag in the neutral words, and no warn tone ever comes near
   it. Status is carried by word + tone together, never colour
   alone (c29-checklist law):
     queued  → "Queued"  plain .t-tag
     running → "Running" .t-tag.is-accent
     parked  → "Parked"  plain .t-tag (calm)
     done    → "Done"    .t-tag.is-ok
     blocked → "Blocked" .t-tag.is-bad
     failed  → "Failed"  .t-tag.is-bad (same register as
               blocked, a different word: blocked waits,
               failed ran and died — never conflated)
   An unknown data-status renders as queued (c29-jobrun
   precedent) — the component never invents a sixth status.
   Host shape:
     <c29-factory heading="Bot Factory" max="3">
       <li data-task="SHIP-88" data-owner="June Park"
           data-status="done">Optional second line</li>
       …
     </c29-factory>
   The li text (optional) is a second line under the task id.
   The owner is a 24px medallion (initials derived the
   c29-presence way, on the accent-soft ground in accent-text)
   plus the owner's name — both are host truth from data-owner.
   Round 0 amendments (Sacca), all three:
   (1) data-status gains `failed` — bad tone, same register as
   blocked, never conflated with it: blocked = waiting on
   something, failed = it ran and died.
   (2) li[data-since] (ISO/ms, start of the current status)
   renders an elapsed line in the c29-jobrun style on running
   and blocked rows only ("running 2h 14m"); absent = no line,
   never an invented age.
   (3) Overflow honesty + severity-first order: rendered rows
   sort blocked, failed, running, queued, parked, done —
   display only (flex order; host DOM order never moves, and
   the sort holds with JS dead because it reads data-status).
   Past `max`, the hidden rows are the sorted tail, and the
   overflow line breaks out its exceptions — "+N more · B
   blocked · F failed", counts derived from the actual hidden
   children — in the bad register when B+F > 0 (blocked and
   failed both wear bad tone; the words carry it too). With
   no exceptions hidden it stays the plain "+N more tasks".
   Host data-state = rows | empty.
   ====================================================== */
c29-factory { display: flex; flex-direction: column; background: var(--surface); border-radius: var(--corner); box-shadow: var(--hair); padding: var(--s5); }
c29-factory > li { list-style: none; margin: 0; padding: 0; }
c29-factory > li::marker { content: none; }
c29-factory .fc-head { order: 1; margin-bottom: var(--s1); }
c29-factory .fc-title { font: 700 var(--fs-lg)/1.2 var(--font-display); letter-spacing: var(--track-head); overflow-wrap: anywhere; }
c29-factory .fc-note { margin-top: var(--s1); font-size: var(--fs-sm); line-height: 1.5; color: var(--ink-mute); overflow-wrap: anywhere; }
c29-factory .fc-note:empty { display: none; }
/* The column header: caps labels on the rows' own grid, so a
   label can never drift from the column it names. */
c29-factory .fc-cols { order: 2; display: grid; grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr) auto; gap: var(--s3);
  margin-top: var(--s3); padding-bottom: var(--s2);
  font-size: var(--fs-cap); letter-spacing: var(--track-cap); text-transform: uppercase; font-weight: 650; color: var(--ink-mute); }
c29-factory .fc-cols span:last-child { text-align: right; }
c29-factory[data-state="empty"] .fc-cols { display: none; }
/* Severity-first render order (Round 0): flex order reads the
   host's data-status, so exceptions float and parked/done sink
   with the DOM untouched — and before JS adoption too. Queued
   (and any unknown status, which paints as queued) is the base
   rank. Every row carries its own top hairline, so separators
   stay honest in whatever order the rows land. */
c29-factory > li { order: 13; }
c29-factory > li[data-status="blocked"] { order: 10; }
c29-factory > li[data-status="failed"] { order: 11; }
c29-factory > li[data-status="running"] { order: 12; }
c29-factory > li[data-status="parked"] { order: 14; }
c29-factory > li[data-status="done"] { order: 15; }
/* The adopted row rides the header's grid exactly. */
c29-factory .fc-row { display: grid; grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr) auto; gap: var(--s3); align-items: center; min-width: 0; padding: var(--s3) 0; box-shadow: inset 0 1px 0 var(--edge-soft); }
c29-factory .fc-row.is-over { display: none; }
c29-factory .fc-main { min-width: 0; display: grid; gap: 1px; }
c29-factory .fc-task { font-family: var(--font-mono); font-size: var(--fs-sm); font-weight: 600; line-height: 1.35; color: var(--ink); overflow-wrap: anywhere; }
c29-factory .fc-sub { font-size: 13px; line-height: 1.4; color: var(--ink-mute); overflow-wrap: anywhere; }
/* The elapsed line (data-since, running/blocked only): the
   status word repeated small + the computed age — quiet, the
   tag above it carries the state. */
c29-factory .fc-elapsed { font-size: 13px; line-height: 1.4; color: var(--ink-mute); overflow-wrap: anywhere; }
c29-factory .fc-owner { display: flex; align-items: center; gap: var(--s2); min-width: 0; }
c29-factory .fc-med { flex: none; width: 24px; height: 24px; border-radius: 50%; display: grid; place-items: center;
  background: var(--accent-soft); color: var(--accent-text); font: 700 10px/1 var(--font-ui); letter-spacing: .02em; }
/* The owner name holds ONE line with an honest ellipsis
   (390px fix, 2026-10-20): the owner slot is ~53px of name at
   phone width, where overflow-wrap:anywhere stacked the name
   mid-word ("Alejand/ro", "Delgad/o") and even break-word
   shatters any name whose longest word beats the slot. A
   table cell follows canon's dense-row idiom instead
   (c29-inbox .ib-t, c29-player .pm-words): one line, the
   ellipsis says it is cut, the medallion's initials still
   name the owner, and the JS-dead floor below keeps the
   full name in text. The medallion is flex:none above and
   this cell never wraps, so the pair can never stack. */
c29-factory .fc-owner-name { min-width: 0; font-size: var(--fs-sm); font-weight: 600; line-height: 1.3; color: var(--ink-soft); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
c29-factory .fc-status { flex: none; justify-self: end; }
/* The overflow line: counts derived from the real hidden
   children. When the tail hides an exception it says so in
   words and takes the bad register (is-alert) — a buried
   blocked row is the board's most important fact and never
   hides inside an unlabelled "+N". */
c29-factory .fc-more { order: 90; margin-top: var(--s1); padding-top: var(--s3); box-shadow: inset 0 1px 0 var(--edge-soft);
  font-size: var(--fs-sm); font-weight: 600; color: var(--ink-mute); }
c29-factory .fc-more.is-alert { margin-top: var(--s3); padding: var(--s3) var(--s4); border-radius: var(--r-md);
  background: var(--bad-bg); color: var(--bad-text); box-shadow: none; }
/* JS-dead fallback: before adoption the row still reads — the
   task id in mono from its attribute, then the li's own text,
   then owner · status in the host's verbatim attribute words.
   JS adoption replaces this stack with the grid above. */
c29-factory > li:not(.fc-row) { display: block; padding: var(--s3) 0; font-size: var(--fs-sm); line-height: 1.45; color: var(--ink-mute); overflow-wrap: anywhere; box-shadow: inset 0 1px 0 var(--edge-soft); }
c29-factory > li:not(.fc-row)::before { content: attr(data-task); display: block;
  font-family: var(--font-mono); font-size: var(--fs-sm); font-weight: 600; line-height: 1.35; color: var(--ink); overflow-wrap: anywhere; }
c29-factory > li:not(.fc-row)::after { content: attr(data-owner) " · " attr(data-status); display: block; margin-top: 1px; }
/* The empty line, same quiet sentence shape as the rail's. */
c29-factory .fc-empty { order: 91; padding: var(--s3) 0 var(--s1); font-size: var(--fs-sm); line-height: 1.5; color: var(--ink-mute); overflow-wrap: anywhere; }
c29-factory[data-state="empty"] > li { display: none; }

}

/* ============================================================
 * TIDE — <c29-runtime> the runtime bar (Fleet 1 · Lane C,
 * as amended by the Sacca Round 0 verdict, 2026-10-20).
 * Campaign contracts: FLEET-BOARD-CAMPAIGN-2026-10-20.md §2
 * + staging/sacca-round0.md §c29-runtime.
 *
 * One machine, named, with what runs on it — and an honest
 * state. The bar has THREE faces, distinct at a glance:
 *   online   lit: chips on the accent tint, no state word
 *   unknown  un-lit but un-muted: chips fall back to the
 *            neutral wash, the word "Unknown" shows — this
 *            is also the DEFAULT face, because a bar nobody
 *            has fed must not declare the machine online
 *            (default-online was fabrication by default)
 *   offline  muted: the whole bar at .55, chips greyed, the
 *            word "Offline" shows — a dimmer alone is never
 *            the state, the word is
 * Light DOM; tokens only; both themes; no autoplay. No
 * number appears here that the host did not supply: no
 * invented uptime, load, or counts — the one computed thing
 * is the age of the host's own `seen` heartbeat.
 * INTEGRATION: append inside @layer c29.components in tide.css.
 * ============================================================ */

@layer c29.components {

c29-runtime { display: grid; gap: var(--s3); grid-template-columns: minmax(0, 1fr);
  background: var(--raised); border-radius: var(--corner); box-shadow: var(--hair);
  padding: var(--s5); text-align: left; min-width: 0;
  transition: opacity var(--d3) var(--ease); }
c29-runtime[data-state="offline"] { opacity: .55; }
c29-runtime[data-state="offline"] .rt-chip { filter: grayscale(.5); }
/* Unknown is un-lit, not muted: the chips lose the accent
   tint for the neutral wash, so the bar reads present but
   nobody is claiming the machine is alive. */
c29-runtime[data-state="unknown"] .rt-chip { background: var(--wash); }
c29-runtime .rt-top { display: flex; align-items: center; gap: var(--s2);
  flex-wrap: wrap; min-width: 0; }
c29-runtime .rt-name { font: 800 var(--fs-xl)/1.15 var(--font-display);
  letter-spacing: var(--track-head); overflow-wrap: anywhere; min-width: 0; }
c29-runtime .rt-status { margin-left: auto; }
/* The last-seen line: the host's heartbeat, aged by the
   component (c29-agent / c29-jobrun elapsed style). It is
   absent from layout entirely when the host fed no `seen` —
   no line, never a fake "just now". It stays visible when
   the bar derives to unknown: the age IS the evidence. */
c29-runtime .rt-seen { font-size: var(--fs-sm); line-height: 1.45; color: var(--ink-mute); }
c29-runtime .rt-seen[hidden] { display: none; }
/* Chip discipline (390px fix, 2026-10-20): the gap token is
   the only space two chips ever need — chips wrap to the next
   line as whole units, a chip never exceeds the bar, and its
   label/sub wrap at word boundaries (break-word: a break
   lands mid-word only when one word alone is wider than the
   chip). overflow-wrap:anywhere is gone from the chip: it
   collapsed the chip's min-content to a single character, so
   a squeezed chip shattered its text instead of wrapping. */
c29-runtime .rt-chips { display: flex; flex-wrap: wrap; gap: var(--s2); min-width: 0; }
c29-runtime .rt-chips:empty { display: none; }
c29-runtime .rt-chip { display: inline-grid; gap: 1px; padding: var(--s2) var(--s4);
  border-radius: var(--r-pill); background: var(--accent-soft); min-width: 0; max-width: 100%;
  transition: background var(--d2) var(--ease), filter var(--d2) var(--ease); }
c29-runtime .rt-chip-label { font-size: var(--fs-sm); font-weight: 600;
  color: var(--ink); line-height: 1.3; overflow-wrap: break-word; }
c29-runtime .rt-chip-sub { font-size: var(--fs-cap); line-height: 1.35;
  color: var(--ink-soft); overflow-wrap: break-word; }

}

/* ============================================================
 * FLEET BOARD — A-1 / A-2 STYLES + JS-DEAD FALLBACKS (F-2)
 * Coordinator fixes, 2026-10-20, landing Sacca Round 1's
 * binding amendments (campaign §6): A-1 (c29-agent `unknown`
 * state), A-2 (detail / evidence lines), R-1 (the JS-dead
 * runtime fallback must not affirm "Online"), F-2 (the JS-dead
 * floor, revised after the overseer's screenshot read).
 *
 * The fallback rules are gated on :not(.tc) — the class every
 * component latches when its JS builds it — so each vanishes
 * the moment the real render lands. What CSS cannot know, it
 * does not fake: no counts, no derivations, no elapsed lines.
 * In particular there is NO [state="online"] word rule for
 * c29-runtime: with JS dead there is no stale derivation, so
 * there is no affirmation either (R-1). Offline (a host claim
 * of absence) and Unknown (the honest default) still paint.
 * ============================================================ */
@layer c29.components {
  /* A-2 lines + the A-1 unknown word (built cards). */
  c29-agent .ag-detail { font-size: var(--fs-body); line-height: 1.4;
    color: var(--ink-soft); overflow-wrap: anywhere; }
  c29-agent .ag-detail[hidden] { display: none; }
  c29-agent .ag-evidence { font-size: var(--fs-sm); line-height: 1.4;
    color: var(--ink-mute); overflow-wrap: anywhere; }
  c29-agent .ag-evidence[hidden] { display: none; }
  c29-agent[data-state="unknown"] .ag-word { color: var(--ink-mute); }

  /* JS-dead fallbacks (F-2). */
  c29-agent:not(.tc) { display: grid; gap: 2px; align-content: start; height: auto; }
  c29-agent:not(.tc)::before { content: attr(name); font-size: var(--fs-body); font-weight: 600;
    line-height: 1.3; overflow-wrap: anywhere; }
  c29-agent:not(.tc)::after { content: attr(role); font-size: var(--fs-sm); line-height: 1.4;
    color: var(--ink-soft); overflow-wrap: anywhere; }
  c29-agent:not(.tc)[state]::after { content: attr(role) " · " attr(state); }
  c29-agent:not(.tc)[seen]:not([state])::after { content: attr(role) " · idle"; }
  c29-agent:not(.tc):not([seen]):not([state])::after { content: attr(role) " · unknown"; }
  c29-band:not(.tc) { display: grid; gap: var(--s3); padding-bottom: var(--s4); }
  c29-band:not(.tc)::before { content: attr(label); display: block;
    padding: var(--s3) var(--s4) 0; font: 700 var(--fs-lg)/1.25 var(--font-display);
    letter-spacing: var(--track-head); color: var(--ink); }
  c29-band:not(.tc) > c29-agent { margin: 0 var(--s4); }
  c29-runtime:not(.tc)::before { content: attr(name); font: 800 var(--fs-xl)/1.15 var(--font-display);
    overflow-wrap: anywhere; }
  c29-runtime:not(.tc)::after { content: "Unknown"; font-size: var(--fs-sm); font-weight: 650; color: var(--ink-soft); }
  c29-runtime:not(.tc)[state="online"]::after { content: none; }
  c29-runtime:not(.tc)[state="offline"]::after { content: "Offline"; }
  c29-runtime:not(.tc)[state="offline"] { opacity: .55; filter: grayscale(.5); }
  c29-banner:not(.tc)::before { content: attr(title); display: block;
    font-weight: 650; font-size: 15px; line-height: 1.4; overflow-wrap: anywhere;
    margin-bottom: var(--s1); }
}

/* ============================================================
 * TIDE — <c29-rest> THE LUX REMINDER (Posture fleet Lane 3;
 * registered into canon 2026-10-20, Tide backlog item 1).
 * The break + eye-rest ritual surface. Contracts (registry):
 * never a modal; cadence read from the shared RestPrefs object
 * on every scheduling decision, never hard-coded; at most one
 * pending arrival; snooze backs off, never escalates; presence
 * defers by default (waiting pill / quiet dot / allow); exactly
 * one face per state; appearance is pure CSS (Platform Rule 1);
 * motion gate collapses arrival, orb, ring and dot-breathe;
 * text colour is always a measured token pair (tightest: dark
 * mute 4.68:1 on the raised night surface); every target pins
 * --tap. Tokens only: the only literals are the breath-orb
 * gradient stops and switch knob, carried verbatim from the
 * adversary-passed source (decorative paint, never text).
 * INTEGRATION: appended as its own @layer c29.components block
 * at the end of tide.css, the Fleet Board family pattern.
 * ============================================================ */
@layer c29.components {

/* The element owns its width: full width of its container, capped
   at --rest-w. (First build used 100vw for the cap — adversary A7
   caught it overflowing any container narrower than the window,
   e.g. a small desktop window or a split view. 100% of the
   container is the correct measure at every size.) */
c29-rest { display: block; --rest-w: 400px; width: 100%; max-width: var(--rest-w); }
c29-rest[hidden] { display: none !important; }

/* ---------- the arrival card ---------- */
c29-rest .rest-card {
  width: 100%;
  background: var(--sheen), var(--raised);
  border-radius: var(--corner);
  box-shadow: var(--lift-hi);
  padding: calc(var(--s6) + 4px);
  display: grid; gap: var(--s4);
  /* soft arrival: a gentle rise + fade on the house ease.
     Under the motion gate (tokens collapse --d*) this is instant. */
  animation: rest-rise var(--d5) var(--ease) both;
}
@keyframes rest-rise {
  from { opacity: 0; transform: translateY(14px) scale(.985); }
  to   { opacity: 1; transform: translateY(0) scale(1); }
}
c29-rest .rest-kicker {
  font-size: var(--fs-cap); letter-spacing: var(--track-cap);
  text-transform: uppercase; font-weight: 650; color: var(--accent-text);
  display: flex; align-items: center; gap: 8px;
}
c29-rest .rest-kicker i { width: 8px; height: 8px; border-radius: 50%; background: var(--accent); flex: none; }
c29-rest .rest-title {
  font-family: var(--font-display); font-weight: 750;
  font-size: var(--fs-2xl); letter-spacing: var(--track-head); line-height: 1.15;
  text-wrap: balance; margin: 0;
}
c29-rest .rest-sub { color: var(--ink-soft); font-size: 15px; text-wrap: pretty; margin: 0; }
c29-rest .rest-source { color: var(--ink-mute); font-size: 13px; margin: 0; }

/* ---------- the ritual stage ---------- */
c29-rest .rest-stage { display: grid; justify-items: center; gap: var(--s3); padding: var(--s2) 0; }
c29-rest .rest-ring { position: relative; width: 132px; height: 132px; }
c29-rest .rest-ring svg { width: 132px; height: 132px; transform: rotate(-90deg); }
c29-rest .rest-ring .ring-track { fill: none; stroke: var(--wash-2); stroke-width: 5; }
c29-rest .rest-ring .ring-arc {
  fill: none; stroke: var(--accent); stroke-width: 5; stroke-linecap: round;
  stroke-dasharray: 327; stroke-dashoffset: 327;
  transition: stroke-dashoffset 1s linear;
}
c29-rest .rest-orb {
  position: absolute; inset: 26px; border-radius: 50%;
  background: radial-gradient(circle at 38% 32%,
    color-mix(in srgb, var(--sea-200) 90%, #fff), var(--sea-300) 72%);
  box-shadow: inset 0 0 0 1px var(--pane-edge), 0 10px 24px -12px rgba(31, 97, 88, .45);
  transform: scale(.82);
  transition: transform 4s var(--ease);
}
[data-theme="dark"] c29-rest .rest-orb {
  background: radial-gradient(circle at 38% 32%,
    color-mix(in srgb, var(--sea-200) 55%, #123a37), var(--sea-600) 78%);
}
c29-rest .rest-orb.is-in  { transform: scale(1); }
c29-rest .rest-orb.is-out { transform: scale(.86); transition-duration: 5s; }
c29-rest .rest-cue {
  font-family: var(--font-display); font-weight: 700; font-size: var(--fs-lg);
  text-align: center; min-height: 1.4em; margin: 0;
}
c29-rest .rest-count { color: var(--ink-mute); font-size: var(--fs-sm); margin: 0; font-variant-numeric: tabular-nums; }
c29-rest .rest-blinks { display: flex; gap: 7px; }
c29-rest .rest-blinks i {
  width: 9px; height: 9px; border-radius: 50%;
  background: var(--wash-2); transition: background var(--d3) var(--ease);
}
c29-rest .rest-blinks i.is-done { background: var(--accent); }

/* ---------- actions: every target ≥ 44px (--tap) ---------- */
c29-rest .rest-actions { display: flex; gap: var(--s2); flex-wrap: wrap; }
c29-rest .rest-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  min-height: var(--tap); min-width: var(--tap);
  padding: 0 calc(var(--s4) + 2px); border: 0; border-radius: var(--r-pill);
  background: var(--wash); color: var(--ink); box-shadow: none;
  font-weight: 600; font-size: 15px; cursor: pointer; white-space: nowrap;
  transition: transform var(--d1) var(--ease), background var(--d2) var(--ease);
  -webkit-tap-highlight-color: transparent; touch-action: manipulation;
}
c29-rest .rest-btn:hover { background: var(--wash-2); }
c29-rest .rest-btn:active { transform: scale(.97); }
c29-rest .rest-btn.is-accent { background: var(--accent); color: var(--on-accent); flex: 1; }
c29-rest .rest-btn:focus-visible { outline: none; box-shadow: var(--focus); }
c29-rest .rest-btn[disabled] { opacity: .45; pointer-events: none; }

/* ---------- the waiting pill (quiet intelligence) ---------- */
c29-rest .rest-wait {
  display: inline-flex; align-items: center; gap: 9px;
  min-height: var(--tap); padding: 0 var(--s4);
  border-radius: var(--r-pill); background: var(--pane);
  backdrop-filter: blur(var(--pane-blur)) saturate(1.2);
  -webkit-backdrop-filter: blur(var(--pane-blur)) saturate(1.2);
  box-shadow: 0 0 0 1px var(--pane-edge), var(--lift);
  color: var(--ink-soft); font-size: 14px; font-weight: 550;
  animation: rest-rise var(--d4) var(--ease) both;
}
c29-rest .rest-wait .wait-dot {
  width: 8px; height: 8px; border-radius: 50%; background: var(--sun-500); flex: none;
  animation: rest-breathe 4.5s ease-in-out infinite;
}
@keyframes rest-breathe { 0%, 100% { opacity: .45; } 50% { opacity: 1; } }
c29-rest .rest-wait button {
  min-height: var(--tap); min-width: var(--tap); border: 0; background: none;
  color: var(--accent-text); font-weight: 650; font-size: 14px; cursor: pointer;
  padding: 0 6px; border-radius: var(--r-pill);
}
c29-rest .rest-wait button:focus-visible { outline: none; box-shadow: var(--focus); }

/* ---------- the quiet dot (meetingSilence: "dot") ---------- */
c29-rest .rest-dot {
  width: var(--tap); height: var(--tap); border-radius: 50%;
  display: grid; place-items: center; border: 0; cursor: pointer;
  background: transparent; padding: 0;
}
c29-rest .rest-dot i { width: 10px; height: 10px; border-radius: 50%; background: var(--accent); opacity: .75; }
c29-rest .rest-dot:focus-visible { outline: none; box-shadow: var(--focus); }

/* ---------- cadence + settings row (lux, small, in-card) ---------- */
c29-rest .rest-settings {
  border-top: 1px solid var(--edge-soft); padding-top: var(--s4);
  display: grid; gap: var(--s3);
}
c29-rest .rest-set-row { display: flex; align-items: center; gap: var(--s3); flex-wrap: wrap; }
c29-rest .rest-set-label { font-size: 13px; font-weight: 600; color: var(--ink-soft); min-width: 92px; }
c29-rest .rest-seg {
  display: inline-grid; grid-auto-flow: column; grid-auto-columns: 1fr;
  padding: 3px; border-radius: 99px; background: var(--wash);
}
c29-rest .rest-seg button {
  min-height: 38px; min-width: var(--tap); padding: 0 12px; border: 0; background: none;
  /* Unpressed labels ride --ink-soft, not --ink-mute: on the seg's
     --wash ground over the dark raised card, --ink-mute renders at
     4.02:1 (canon render pass, 2026-10-20) — under the 4.5 floor.
     --ink-soft holds 7.87:1 on the card pair and stays clear of
     the wash. The lux source used --ink-mute here; this is the
     one deliberate canon deviation (B4 in the lane report). */
  border-radius: 99px; font-size: 13.5px; font-weight: 600; color: var(--ink-soft);
  cursor: pointer; white-space: nowrap;
}
c29-rest .rest-seg button[aria-pressed="true"] {
  color: var(--ink); background: var(--raised); box-shadow: var(--lift);
}
c29-rest .rest-seg button:focus-visible { outline: none; box-shadow: var(--focus); }
c29-rest .rest-switch {
  appearance: none; width: 50px; height: 30px; border-radius: 99px;
  background: var(--wash-2); position: relative; cursor: pointer; flex: none; margin: 0;
  transition: background var(--d2) var(--ease);
}
c29-rest .rest-switch::after {
  content: ""; position: absolute; top: 3px; left: 3px; width: 24px; height: 24px;
  border-radius: 50%; background: #fff; box-shadow: 0 2px 6px rgba(0,0,0,.18);
  transition: translate var(--d3) var(--ease-give);
}
c29-rest .rest-switch:checked { background: var(--accent); }
c29-rest .rest-switch:checked::after { translate: 20px 0; }
c29-rest .rest-switch:focus-visible { outline: none; box-shadow: var(--focus); }

/* ---------- motion gate: keyframe travel collapses ---------- */
html[data-motion="reduced"] c29-rest .rest-card,
html[data-motion="reduced"] c29-rest .rest-wait { animation: none; }
html[data-motion="reduced"] c29-rest .rest-wait .wait-dot { animation: none; opacity: .8; }
html[data-motion="reduced"] c29-rest .rest-orb { transition: none; }
html[data-motion="reduced"] c29-rest .rest-ring .ring-arc { transition: none; }

} /* @layer c29.components */

/* ============================================================
 * TIDE — CAPABILITY GATE (c29-capgate) + DEMO SHELL (c29-demoshell)
 * (Mirror fleet Lane 2, 2026-10-20). The JS lives in tide.js:
 * Tide.caps / Tide.capGate + the capture-phase [data-cap] gate,
 * and Tide.demoShell. This block is paint only: the gate works with
 * these bytes deleted (the toast still fires); the mark is how a
 * person can SEE a control is native before tapping it. Tokens
 * only; both themes ride the bar token pairs (--pane-strong
 * ground, --ink on it, --accent-text for the way home).
 * ============================================================ */
@layer c29.components {

/* ---------- c29-capgate: the native affordance ----------
   A tiny "native" pill riding the control's top-right corner.
   Absolutely positioned, so it never moves the control's layout;
   pointer-events none, so it never eats the tap it announces.
   Inputs are excluded (a switch's ::after is its knob); a gated
   input still gates — it just wears no pill. */
[data-cap] { position: relative; }
[data-cap]:not(input)::after {
  content: "native";
  position: absolute; top: -7px; right: -7px; z-index: 2;
  padding: 1px 6px 2px; border-radius: var(--r-pill);
  background: var(--ink); color: var(--bg);
  box-shadow: 0 0 0 2px var(--bg);
  font-size: 9.5px; font-weight: 700; line-height: 1.5;
  letter-spacing: .07em; text-transform: uppercase; white-space: nowrap;
  pointer-events: none;
}

/* ---------- c29-demoshell: universal demo chrome ----------
   The host's empty [data-demo-shell] div becomes this sticky bar
   when tide.js enhances it: back link, surface name, theme toggle.
   Sticky at top:0 with the safe-area inset folded into its own
   padding, exactly like the gallery's .g-bar; content scrolls
   under it, nothing is reserved or overlapped — the bar is in
   normal flow, so the page's first content starts below it. */
.c29-demoshell, [data-demo-shell] {
  position: sticky; top: 0; z-index: 40;
  display: flex; align-items: center; gap: var(--s2);
  min-height: calc(var(--tap) + 16px);
  padding: calc(8px + var(--safe-t)) var(--s4) 8px;
  background: var(--pane-strong);
  backdrop-filter: blur(14px) saturate(1.2);
  -webkit-backdrop-filter: blur(14px) saturate(1.2);
  box-shadow: 0 1px 0 var(--edge-soft);
}
.c29-demoshell .ds-back {
  display: inline-flex; align-items: center; flex: none;
  min-height: var(--tap); padding: 0 var(--s2) 0 0;
  color: var(--accent-text); font-weight: 650; font-size: 15px;
  text-decoration: none; white-space: nowrap; border-radius: var(--r-pill);
}
.c29-demoshell .ds-back:focus-visible { outline: none; box-shadow: var(--focus); }
.c29-demoshell .ds-title {
  font-family: var(--font-display); font-weight: 750; font-size: 17px;
  letter-spacing: var(--track-head); color: var(--ink);
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.c29-demoshell .ds-spacer { flex: 1; }
.c29-demoshell c29-theme { flex: none; }

} /* @layer c29.components (capgate + demoshell) */

/* ================================================================
   FLOW FIGURE · .t-flow (canonized 2026-10-20, Dillo Flows build;
   doctrine: inventions are welcome — the price is canonization.
   The Principal's clarification, 2026-10-20 17:29 PT.)
   WHEN TO USE: whenever a flow must be SHOWN, not run — product
   landings, template galleries, documentation, run summaries —
   anywhere a reader must see the shape of a workflow: its steps,
   a fan-out, a path split, and each step's state (a canon .t-chip
   rides the step's title line: .is-on = live/proven, the default
   chip = iced/inert). Steps are canon .t-card elements inside
   .t-flow; .t-flow-conn is the canal between steps; .t-flow-fan
   lays a fan-out two-across on wide screens, one-across on
   phones; .t-flow-path indents a branch's children under a rule
   (accent for the matched path, .is-alt in sun for the other).
   If a surface needs to draw a flow and this figure cannot say
   it, extend the figure HERE, in canon, with its gallery proof —
   never with a local one-off on the surface.
   ================================================================ */
@layer c29.components {
  .t-flow { display: grid; gap: 0; }
  .t-flow .t-card { padding: 12px 14px; }
  .t-flow .t-card b { font-size: 15.5px; }
  .t-flow-sub { display: block; color: var(--ink-soft); font-size: 14px; line-height: 1.45; margin-top: 2px; }
  .t-flow-conn { width: 2px; height: 18px; background: var(--edge); margin: 0 auto; }
  .t-flow-fan { display: grid; gap: 10px; }
  @media (min-width: 560px) { .t-flow-fan { grid-template-columns: 1fr 1fr; } }
  .t-flow-path { border-left: 3px solid var(--accent); padding: 4px 0 4px 12px; display: grid; gap: 8px; margin-top: 8px; }
  .t-flow-path.is-alt { border-left-color: var(--sun-500); }
  .t-flow-out { font-size: 14.5px; line-height: 1.55; color: var(--ink-soft); background: var(--pane); border: 1px solid var(--edge-soft); border-radius: var(--r-md); padding: 9px 12px; margin: 0; }
  .t-flow-out b { color: var(--ink); }
} /* @layer c29.components (flow figure) */

/* ================================================================
   APP STAGE · .c29-appstage + VIEW-TRANSITION CHOREOGRAPHY
   (canonized 2026-10-20, Tide App Mirror build.)
   WHEN TO USE: whenever a live application must be PRESENTED on a
   Tide page inside a phone frame — a mirror, a demo, a review
   surface — the app runs in .c29-appframe on a .c29-appstage, and
   its screen changes are choreographed by the view-transition
   rules below. The stage is presentation only: the app inside owns
   its own layout, theme and motion.
   THE FRAME: .c29-appframe is a device-proportioned box (390×844
   design points, capped by the viewport) with the house corner and
   lift; its child mount point fills it and clips to the corner.
   The veil (.c29-appframe-veil) covers the frame until the app
   signals readiness by setting data-ready on the frame — a still,
   branded wait state, never a spinner farm.
   THE TRANSITIONS: same-document view transitions only. The app
   marks its screen container with the transition name "app-screen"
   (inline style — the name is a contract, not a class); this canon
   block owns what that name DOES: the old screen glides out, the
   new one glides in, on token timing (--d3, --ease), while the
   page root cross-fades quickly so surrounding chrome settles
   instead of jumping. No view-transition support (older engines)
   and the app swaps instantly — the still state is the real state,
   so nothing here is load-bearing. Reduced motion stills ALL of
   it, canon-wide, below.
   ================================================================ */
@layer c29.components {
  .c29-appstage { display: grid; justify-items: center; padding: var(--s5, 20px) var(--s4, 16px) var(--s6, 28px); }
  .c29-appframe { position: relative; width: min(390px, 100%); height: min(844px, calc(100dvh - 190px)); min-height: 480px;
    border-radius: var(--corner); background: var(--raised); box-shadow: var(--lift-hi); overflow: hidden; }
  .c29-appframe > .c29-appmount { position: absolute; inset: 0; display: flex; flex-direction: column; }
  .c29-appframe > .c29-appmount > * { flex: 1 1 auto; min-height: 0; }
  .c29-appframe-veil { position: absolute; inset: 0; z-index: 5; display: grid; place-items: center; text-align: center;
    background: var(--raised); color: var(--ink-soft); transition: opacity var(--d3) var(--ease); }
  .c29-appframe[data-ready] .c29-appframe-veil { opacity: 0; pointer-events: none; }
  .c29-appframe-veil .veil-word { font: 700 22px/1.1 var(--font-display); color: var(--ink); display: block; margin-bottom: 6px; }
} /* @layer c29.components (app stage) */

/* View-transition choreography for the app-screen group (global —
   pseudo-elements cannot live inside @layer component scoping on
   every engine; these rules are keyed to the transition NAME, so
   they can only ever affect an element that opted in). */
::view-transition-old(app-screen) { animation: c29-vt-screen-out var(--d3) var(--ease) both; }
::view-transition-new(app-screen) { animation: c29-vt-screen-in var(--d3) var(--ease) both; }
::view-transition-old(root) { animation: c29-vt-root-out var(--d2, 240ms) var(--ease) both; }
::view-transition-new(root) { animation: c29-vt-root-in var(--d2, 240ms) var(--ease) both; }
@keyframes c29-vt-screen-out { to { opacity: 0; transform: translateX(-16px) scale(.995); } }
@keyframes c29-vt-screen-in { from { opacity: 0; transform: translateX(16px) scale(.995); } }
@keyframes c29-vt-root-out { to { opacity: 0; } }
@keyframes c29-vt-root-in { from { opacity: 0; } }
@media (prefers-reduced-motion: reduce) {
  ::view-transition-old(*), ::view-transition-new(*) { animation: none !important; }
}

/* ================================================================
   APP STAGE · FULLSCREEN / APP MODE (canonized 2026-10-20, the
   Mirror-becomes-front-door build). WHEN TO USE: any .c29-appstage
   surface the Principal LIVES in, not just visits. tide.js renders
   the one control (.c29-appstage-tools > .c29-appstage-fsbtn) into
   the stage; the same button enters and exits — the exit is always
   one obvious tap, and in true fullscreen the platform Escape also
   leaves (fullscreenchange re-syncs the button).
   TWO MECHANISMS, ONE LOOK: with the Element Fullscreen API
   (desktop, Android) the stage itself goes fullscreen — the
   :fullscreen rules below. iPhone Safari withholds that API from
   page elements, so there tide.js adds .is-appmode (+ .c29-appmode
   on the root): the stage pins to the viewport — 100dvh, safe-area
   aware — every piece of Tide chrome leaves, and what remains is
   the tools strip (the exit) over the app itself: masthead,
   screens, tab bar — the native app's own furniture. The frame
   drops its device costume under both mechanisms: full-bleed,
   square corners, no lift. It is not a picture of a phone; it is
   the screen. HONEST LINE: iPhone's true fullscreen is Add to
   Home Screen (standalone); the consuming page's help text says so.
   ================================================================ */
@layer c29.components {
  .c29-appstage-tools { width: min(390px, 100%); display: flex; justify-content: flex-end; margin-bottom: 10px; }
  .c29-appstage-fsbtn { min-height: 44px; padding: 0 18px; border: 0; border-radius: 999px; cursor: pointer;
    background: var(--raised); color: var(--ink); box-shadow: var(--hair);
    font: 600 14px/1 var(--font-body, inherit); display: inline-flex; align-items: center; gap: 8px; }
  .c29-appstage-fsbtn:active { transform: scale(.98); }
}
/* The fullscreen / app-mode STATE rules are global (unlayered) on
   purpose — same reasoning as the view-transition pseudos above:
   a page-local stylesheet (unlayered by default) must never be
   able to override the geometry of a state the Principal entered;
   a layered display:none here lost to a page's notes styling in
   the first live proof, and the notes ate the app. One column in
   either mechanism: a slim tools strip (safe-area aware, carries
   the exit) over the app, full-bleed, page chrome gone. */
.c29-appstage:fullscreen, .c29-appstage.is-appmode { padding: 0; background: var(--bg); display: flex; flex-direction: column; }
.c29-appstage:fullscreen .c29-appstage-tools, .c29-appstage.is-appmode .c29-appstage-tools {
  width: 100%; margin: 0; flex: 0 0 auto;
  padding: calc(6px + env(safe-area-inset-top, 0px)) calc(10px + env(safe-area-inset-right, 0px)) 6px calc(10px + env(safe-area-inset-left, 0px)); }
.c29-appstage:fullscreen .c29-appframe, .c29-appstage.is-appmode .c29-appframe {
  flex: 1 1 auto; width: 100%; height: auto; min-height: 0; border-radius: 0; box-shadow: none; }
.c29-appstage:fullscreen .m-notes, .c29-appstage.is-appmode .m-notes { display: none; }
.c29-appstage.is-appmode { position: fixed; inset: 0; z-index: 60; height: 100dvh; }
html.c29-appmode { overflow: hidden; }

/* ================================================================
   CORRECTION PAIR  <c29-correction>  (canonized 2026-10-20, Fleet 3,
   the Second Nature front page). CSS-only: the host swaps the three
   text slots (complaint / before / after / rule) and the component
   renders the pair. WHEN TO USE: whenever a page must SHOW a
   correction landing — a complaint in the boss's own words, what the
   untrained bot produced, what the trained bot produces, and the
   rule the complaint became. Landings for the LoRA offering, Fitting
   Reports, Cotillion proof beats. Never for live client data on a
   public page: public instances carry invented examples and say so.
   R5 fix (Copycat proof round, 2026-10-28): the component's two accent
   TEXT spots (the ::before label and .cx-after .cx-tag) ride
   --accent-text, never --accent — sea-600 text on the --wash side
   measured 4.25:1 in light (both engines); sea-700 passes the 4.5:1
   bar, and dark is unchanged because dark --accent-text aliases
   --accent. The F-03 text-pair rule, applied to this component.
   ================================================================ */
@layer c29.components {
  c29-correction { display: block; background: var(--surface); border-radius: var(--corner); box-shadow: var(--hair); padding: var(--s5); }
  c29-correction .cx-complaint { font: 700 19px/1.3 var(--font-display); color: var(--ink); margin: 0 0 var(--s4); }
  c29-correction .cx-complaint::before { content: "The correction"; display: block; font: 600 12px/1 var(--font-body, inherit);
    letter-spacing: .08em; text-transform: uppercase; color: var(--accent-text); margin-bottom: var(--s2); }
  c29-correction .cx-pair { display: grid; gap: var(--s3); }
  @media (min-width: 720px) { c29-correction .cx-pair { grid-template-columns: 1fr 1fr; } }
  c29-correction .cx-side { border-radius: calc(var(--corner) - 6px); padding: var(--s4); font-size: 15px; line-height: 1.5; }
  c29-correction .cx-before { background: var(--sunken); color: var(--ink-soft); }
  c29-correction .cx-after { background: var(--wash); color: var(--ink); box-shadow: inset 0 0 0 1px var(--edge); }
  c29-correction .cx-tag { display: block; font-size: 12px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; margin-bottom: var(--s2); }
  c29-correction .cx-before .cx-tag { color: var(--ink-mute); }
  c29-correction .cx-after .cx-tag { color: var(--accent-text); }
  c29-correction .cx-rule { margin: var(--s4) 0 0; padding-top: var(--s3); border-top: 1px solid var(--edge);
    font-size: 14px; color: var(--ink-soft); }
  c29-correction .cx-rule b { color: var(--ink); }
} /* @layer c29.components (correction pair) */

/* ================================================================
   CHAPTER MARKER  <c29-chaptermark>  (Copycat R3, 2026-10-28,
   lanes/R3-critic.md Ruling 1). CSS-only: no JS, no
   customElements.define, no tide.js block — host markup styled
   in place, in the c29-timeline / c29-correction kind. NOT
   c29-chapter: that tag is c29-player's folded timed-data child
   and is never reused for this. The marker is a numbered line on
   a 1px --edge hairline and nothing else: never a pill (no
   background on any part, no border except the top hairline,
   border-radius 0 by construction), never a kicker (no
   text-transform anywhere; the title prints in the host's
   sentence case). Tokens only — no literal in the rules below.
   Squish defence by construction: the number is the row's only
   furniture (flex: none) and the title is defended
   (flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere).
   ================================================================ */
@layer c29.components {
  c29-chaptermark { display: flex; align-items: baseline; gap: var(--s3);
    border-top: 1px solid var(--edge); padding-top: var(--s4);
    margin-bottom: var(--s6); min-width: 0; }
  c29-chaptermark .cm-num { font-family: var(--font-mono); font-size: var(--fs-sm);
    color: var(--ink-mute); flex: none; font-variant-numeric: tabular-nums; }
  c29-chaptermark .cm-title { font-family: var(--font-display); font-size: var(--fs-2xl);
    line-height: 1.15; font-weight: 750; margin: 0; flex: 1 1 auto;
    min-width: 0; overflow-wrap: anywhere; }
} /* @layer c29.components (chapter marker) */

/* ================================================================
   OPTIONS BALL  <c29-options>  (canonized 2026-10-20, toggle-
   presence contract — see the c29-options block in tide.js).
   Placement law: header first (a bare <c29-theme> in the header);
   the ball is the fallback where the header cannot hold a toggle.
   Geometry: 54px ball, bottom-right, --s3 from the corner plus
   the safe-area insets. --c29-options-lift (default 0) raises
   ball AND panel together above a bottom bar; canon sets it
   while the player mini owns the reserved slot, and a host sets
   it for any other bottom bar it ships. Z-order 70: above page
   content, below gift bar (75), player mini (80), toasts (90)
   and sheets/dialogs (top layer) — the ball never covers a
   playback bar, a nav row, or a sheet handle. Panel text is
   --ink on --raised in both themes (the contrast pair the token
   audit already holds to >= 4.5:1; the adversary pass measures
   it on the render, not the token sheet).
   ================================================================ */
@layer c29.components {
  html[data-player="mini"] { --c29-options-lift: calc(var(--player-slot-h) + var(--s2)); }
  c29-options .opt-ball { position: fixed; z-index: 70;
    right: calc(var(--s3) + env(safe-area-inset-right, 0px));
    bottom: calc(var(--s3) + var(--safe-b) + var(--c29-options-lift, 0px));
    width: 54px; height: 54px; border-radius: 50%; border: 0; cursor: pointer;
    display: grid; place-items: center; background: var(--raised); color: var(--ink);
    box-shadow: var(--lift);
    transition: transform var(--d1) var(--ease), bottom var(--d3) var(--ease), background var(--d2) var(--ease), box-shadow var(--d2) var(--ease); }
  c29-options .opt-ball .t-icon { width: 24px; height: 24px; }
  c29-options .opt-ball:active { transform: scale(.94); }
  c29-options .opt-ball[aria-expanded="true"] { background: var(--accent-soft); }
  c29-options .opt-panel { position: fixed; z-index: 70;
    right: calc(var(--s3) + env(safe-area-inset-right, 0px));
    bottom: calc(var(--s3) + var(--safe-b) + var(--c29-options-lift, 0px) + 54px + var(--s2));
    width: min(288px, calc(100vw - 2 * var(--s3)));
    background: var(--raised); color: var(--ink);
    border-radius: calc(var(--corner) - 4px); box-shadow: var(--lift-hi);
    padding: var(--s2); transform-origin: calc(100% - 27px) 100%;
    animation: c29-options-in var(--d2) var(--ease-give) both;
    transition: bottom var(--d3) var(--ease); }
  c29-options .opt-panel[hidden] { display: none; }
  @keyframes c29-options-in { from { opacity: 0; transform: translateY(6px) scale(.96); } to { opacity: 1; transform: none; } }
  c29-options .opt-row { display: flex; align-items: center; gap: var(--s2);
    min-height: 52px; padding: 6px 10px; border-radius: var(--r-md); }
  c29-options .opt-lab { flex: 1; min-width: 0; font-size: 15px; font-weight: 650; color: var(--ink); }
  c29-options .opt-val { font-size: 13.5px; font-weight: 600; color: var(--ink-mute); }
  c29-options .opt-theme-row c29-theme { flex: none; }
  c29-options .opt-rows { border-top: 1px solid var(--edge-soft); margin-top: 2px; padding-top: 2px; }
  c29-options .opt-slot-row > * { flex: 1; min-width: 0; }
} /* @layer c29.components (options ball) */

/* ================================================================
   FOCUS RING RESTATEMENT (canon defect, found 2026-10-20 by the
   Flows landing rescue fleet; folded into canon same night).
   The defect: the global ring — `:focus-visible { box-shadow:
   var(--focus) }` — lives in @layer c29.base, while components set
   their own box-shadow in the LATER @layer c29.components (.t-btn's
   `0 0 0 1px var(--b-edge)` edge is the headline case). In the
   cascade, a later layer beats an earlier one regardless of the
   pseudo-class, so EVERY focused .t-btn on EVERY Tide surface
   painted its resting edge and NO ring: keyboard focus was
   invisible on the system's most common control. Components that
   restated their own ring in-layer survived; the generic controls
   did not.
   The fix restates the base ring INSIDE c29.components, at the end
   of the layer, as ONE rule whose selector carries the whole
   2026-10-19 exclusion list — the ring never MATCHES a
   programmatic target, and there is no suppression rule at all.
   Two focus-pass catches shaped it:
   (1) A restated box-shadow:none suppression (the base pair,
   verbatim) strips a focused .t-sheet dialog of its own shadow —
   the suppression out-specifies the component (F3, both attempts
   that carried one). In the base layer the suppression was inert
   against component shadows because the whole components layer
   outranked it; inside this layer it bites, so it cannot come.
   (2) The tabindex="-1" clause, carried bare inside :not(),
   inflates the ring to (0,3,0) — its carve-out list is
   specificity-heavy — which TIED .t-sheet .grab's deliberate
   in-layer suppression (0,3,0) and, sitting later, painted the
   grab (F6). Wrapped in :where(), the clause costs zero
   specificity: the ring lands at (0,2,0), above the generic
   controls' resting shadows (.t-btn (0,1,0) is the defect case)
   and below every component's own focus rule (.grab's none,
   c29-progress's two-tone ring, c29-slide's grip, the deck's
   items), which all stay sovereign. Exclusion leaves every
   element's own shadow exactly as its component wrote it —
   programmatic targets get nothing, controls get the ring.
   The rescue fleet's page-layer restatement on the Flows landing
   (.t-btn:focus-visible in c29.page) is superseded by this block
   the next time that surface re-pins canon; until then both paint
   the identical var(--focus) ring, so nothing double-renders.
   ================================================================ */
@layer c29.components {
  /* The exclusion list mirrors the base suppression (tide.css,
     @layer c29.base): dialogs, headings, and tabindex="-1"
     waypoints that are not real controls — the carve-out list is
     the base law's own, so a roving-tabindex control still rings
     and no keyboard user loses a cue. The tabindex clause is
     :where()-wrapped: zero specificity, per catch (2) above.
     border-radius is deliberately NOT restated: the base rule's
     8px still applies wherever a component sets no radius of its
     own, and a component's own radius (the pill, the peel corner)
     is never fought on focus. */
  :focus-visible:not(dialog, [role="dialog"], [role="alertdialog"],
    h1, h2, h3, h4, h5, h6,
    :where([tabindex="-1"]:not(button, a, input, select, textarea, summary,
      [role="button"], [role="tab"], [role="menuitem"], [role="menuitemcheckbox"],
      [role="menuitemradio"], [role="option"], [role="link"], [role="checkbox"],
      [role="radio"], [role="switch"], [role="slider"], [role="spinbutton"],
      [role="combobox"], [role="listbox"], [role="textbox"],
      [contenteditable])))
    { outline: none; box-shadow: var(--focus); }
}

/* ================================================================
   THE DOCKET SHEET (canon pattern, 2026-10-21 — born on the Desk
   redesign, Victor: "I hate my desk UI"). A decision surface reads
   in three seconds: WHAT (the lede) → STAFF SAYS (the strip) →
   BUTTONS (the ruling block, surface-local). Everything else —
   the full detail, the history — lives in .t-record, a disclosure
   closed by default. One instruction line per surface, ever.
   Adversary notes: strips carry :not([hidden]) display guards so
   the hidden attribute always wins; the record summary is a real
   <summary> (keyboard + AT native); chevron rotates on [open],
   instant under reduced motion (canon motion law covers it).
   ================================================================ */
.t-lede { font-size: var(--fs-lg); line-height: 1.45; font-weight: 620; color: var(--ink); overflow-wrap: anywhere; }
.t-rec-strip:not([hidden]) { display: flex; align-items: flex-start; gap: var(--s3);
  background: var(--accent-soft); border-radius: var(--r-md); box-shadow: var(--hair); padding: var(--s3); }
.t-rec-strip .t-rec-body { display: grid; gap: var(--s1); min-width: 0; }
.t-rec-strip .t-rec-text { font-size: var(--fs-sm); color: var(--ink); line-height: 1.5; overflow-wrap: anywhere; }
.t-state-strip { border-radius: var(--r-md); box-shadow: var(--hair); background: var(--raised); padding: var(--s2) var(--s3); }
.t-state-strip[data-state="blocked"] { box-shadow: var(--hair), inset 3px 0 0 var(--bad); }
.t-state-strip[data-state="enacted"], .t-state-strip[data-state="done"] { box-shadow: var(--hair), inset 3px 0 0 var(--ok); }
.t-state-strip[data-state="received"] { box-shadow: var(--hair), inset 3px 0 0 var(--warn); }
.t-record { border-top: 1px solid var(--hairline); }
.t-record > summary { display: flex; align-items: center; gap: var(--s2); cursor: pointer;
  padding: var(--s3) 0; list-style: none; min-height: 44px; }
.t-record > summary::-webkit-details-marker { display: none; }
.t-record > summary .t-record-note { font-size: var(--fs-cap); color: var(--ink-2); }
.t-record > summary .t-record-chev { margin-left: auto; color: var(--ink-2); transition: transform 160ms ease; }
.t-record[open ] > summary .t-record-chev { transform: rotate(90deg); }
.t-artifact-tile:not([hidden]) { display: flex; align-items: center; gap: var(--s3);
  border-radius: var(--r-md); box-shadow: var(--hair); background: var(--raised); padding: var(--s3); }
/* Docket sheet adversary fix (2026-10-21): the canon eyebrow color
   (--ink-soft) computes 3.99:1 on the strip's accent-soft ground in
   dark. Inside docket surfaces the eyebrow steps up to --ink-2 —
   the pair, not the eyebrow's global color, is what was wrong. */
.t-rec-strip .t-cap, .t-artifact-tile .t-cap, .t-record > summary .t-cap { color: var(--ink-2); }

/* ================================================================
 * PROJECT PLEASANT — channel-home components (2026-10-21).
 * The innovation project stashed in Tide: c29-sky (the living
 * backdrop), c29-channel (the live tile), c29-orb (the round
 * shelf button), c29-clock (the date/time cluster), the pointer
 * hand, the pager, the shelf, the settings sheet, the lock.
 * Geometry law: theme changes crossfade PAINT only — the sky is
 * two stacked layers trading opacity (reinforced under
 * html.c29-theme-anim below), tiles trade background-color and
 * inset sheen; nothing moves, resizes, or reflows. Motion law:
 * every duration rides the tokens (--d*, --speed), so the global
 * reduced-motion gate collapses the whole surface at once.
 * ================================================================ */
@layer c29.components {
/* ---------- the stage ---------- */
.pl-stage { position: relative; min-height: 100dvh; display: flex; flex-direction: column;
  overflow: hidden; color: var(--pl-ink); font-family: var(--font-display); }
.pl-stage, .pl-stage * { -webkit-tap-highlight-color: transparent; }

/* ---------- c29-sky: day layer + dusk layer, one opacity trade ---------- */
c29-sky { position: absolute; inset: 0; z-index: 0; overflow: hidden; pointer-events: none; }
c29-sky .pl-sky-day, c29-sky .pl-sky-dusk { position: absolute; inset: 0;
  transition: opacity var(--d-theme) var(--ease); }
c29-sky .pl-sky-day { background: linear-gradient(180deg, var(--pl-sky-top) 0%, var(--pl-sky-mid) 52%, var(--pl-sky-low) 100%); }
c29-sky .pl-sky-dusk { background: linear-gradient(180deg, #120D1D 0%, #241631 52%, #3A2138 100%); opacity: 0; }
[data-theme="dark"] c29-sky .pl-sky-day { opacity: 0; }
[data-theme="dark"] c29-sky .pl-sky-dusk { opacity: 1; }
/* while canon runs its paint crossfade, the sky layers ride the same clock */
html.c29-theme-anim c29-sky .pl-sky-day, html.c29-theme-anim c29-sky .pl-sky-dusk {
  transition-property: opacity !important; transition-duration: var(--d-theme) !important;
  transition-timing-function: var(--ease) !important; }
/* sun + drifting clouds (day) */
.pl-sun { position: absolute; top: -70px; right: -50px; width: 300px; height: 300px; border-radius: 50%;
  background: radial-gradient(circle, var(--pl-glow) 0%, rgba(255,217,160,0) 62%); }
.pl-cloud { position: absolute; opacity: .85; filter: blur(11px);
  background:
    radial-gradient(50% 62% at 30% 58%, rgba(255,255,255,.95), rgba(255,255,255,0) 70%),
    radial-gradient(42% 55% at 55% 36%, rgba(255,255,255,.9), rgba(255,255,255,0) 70%),
    radial-gradient(46% 58% at 77% 56%, rgba(255,255,255,.85), rgba(255,255,255,0) 70%);
  animation: pl-drift calc(110s / var(--speed)) ease-in-out infinite alternate; }
.pl-cloud.c1 { width: 340px; height: 112px; top: 8%; left: 3%; }
.pl-cloud.c2 { width: 240px; height: 82px; top: 19%; left: 60%; animation-duration: calc(140s / var(--speed)); opacity: .6; }
.pl-cloud.c3 { width: 175px; height: 58px; top: 3.5%; left: 40%; animation-duration: calc(90s / var(--speed)); opacity: .5; }
@keyframes pl-drift { from { transform: translateX(-14px); } to { transform: translateX(26px); } }
/* stars + moon + ember horizon (dusk) */
.pl-stars { position: absolute; inset: 0;
  background-image:
    radial-gradient(1.6px 1.6px at 12% 18%, rgba(255,244,220,.95) 50%, transparent 51%),
    radial-gradient(1.2px 1.2px at 28% 8%, rgba(255,244,220,.8) 50%, transparent 51%),
    radial-gradient(1.8px 1.8px at 44% 22%, rgba(255,244,220,.9) 50%, transparent 51%),
    radial-gradient(1.1px 1.1px at 58% 12%, rgba(255,244,220,.7) 50%, transparent 51%),
    radial-gradient(1.5px 1.5px at 71% 26%, rgba(255,244,220,.85) 50%, transparent 51%),
    radial-gradient(1.2px 1.2px at 83% 9%, rgba(255,244,220,.75) 50%, transparent 51%),
    radial-gradient(1.4px 1.4px at 92% 30%, rgba(255,244,220,.8) 50%, transparent 51%),
    radial-gradient(1px 1px at 6% 38%, rgba(255,244,220,.6) 50%, transparent 51%),
    radial-gradient(1px 1px at 36% 34%, rgba(255,244,220,.55) 50%, transparent 51%),
    radial-gradient(1px 1px at 64% 40%, rgba(255,244,220,.5) 50%, transparent 51%);
  animation: pl-twinkle calc(5s / var(--speed)) ease-in-out infinite alternate; }
@keyframes pl-twinkle { from { opacity: .75; } to { opacity: 1; } }
.pl-moon { position: absolute; top: 3%; right: 3%; width: 60px; height: 60px; border-radius: 50%;
  background: radial-gradient(circle at 38% 34%, #FFF6DC, #F0DCA8 58%, #DEC48C);
  box-shadow: 0 0 44px 10px rgba(245,231,200,.28); }
.pl-ember { position: absolute; left: 0; right: 0; bottom: 0; height: 34%;
  background: radial-gradient(90% 100% at 50% 108%, var(--pl-glow) 0%, rgba(232,140,160,0) 60%); }

/* ---------- layout: main + shelf ---------- */
.pl-main { position: relative; z-index: 1; flex: 1; display: flex; flex-direction: column;
  align-items: center; justify-content: center; padding: calc(10px + var(--safe-t)) 0 6px; min-height: 0; }
.pl-pages-clip { width: 100%; overflow: hidden; }
.pl-pages { display: flex; transition: transform var(--d4) var(--ease-sheet); will-change: transform; }
.pl-page { flex: 0 0 100%; min-width: 0; display: grid; align-content: center;
  grid-template-columns: repeat(4, minmax(0, 1fr)); gap: clamp(10px, 1.9vmin, 24px);
  padding: 12px max(12px, calc((100% - 1180px) / 2)); box-sizing: border-box; }
@media (min-width: 1600px) { .pl-page { padding-inline: max(12px, calc((100% - 1520px) / 2)); } }
@media (max-width: 700px) and (orientation: portrait) {
  .pl-page { grid-template-columns: repeat(2, minmax(0, 1fr)); padding-inline: 3vw; gap: 12px; }
  .pl-shelf { padding-block: 8px; }
}
.pl-dots { display: flex; gap: 2px; justify-content: center; padding: 6px 0 0; }
/* the painted dot stays small; the TARGET is a full 44px (oversight
   R0-D12) — a page dot you can land a thumb on, at every width */
.pl-dots button { position: relative; width: 44px; height: 44px; border-radius: 99px; border: 0; padding: 0;
  background: transparent; cursor: pointer; }
/* the painted dot is small; the TARGET is not (adversary R1 SEV-3:
   inactive dots were 1.7:1 whispers on an 11px target — now ink at
   68% over the sky, ≥3:1 in both themes, on a 28px hit area) */
.pl-dots button::after { content: ""; position: absolute; left: 50%; top: 50%; width: 11px; height: 11px;
  border-radius: 99px; transform: translate(-50%, -50%);
  background: color-mix(in srgb, var(--pl-ink) 68%, transparent);
  transition: background-color var(--d2) var(--ease), width var(--d2) var(--ease); }
.pl-dots button[aria-current="true"]::after { width: 26px; background: var(--pl-ink); }
.pl-dots button:focus-visible { box-shadow: var(--focus); }

/* ---------- c29-channel: the live tile ---------- */
c29-channel { display: block; min-width: 0; position: relative; }
/* the square's + (R6 canon pattern): top-right corner, every square,
   one size, one feel; the label says what it does on THAT square. */
/* P4 (night loop 2026-10-24): the corner + was 30px — the one home
   control the R1–R3 sweeps never measured (they swept rooms and the
   fav star, not the tile corner grammar). It is a real button on
   every square (72 tiles in the rig); the shell's 40px floor applies
   to it like every other control. 30px → 40px. */
.pl-tile-plus { position: absolute; z-index: 5; top: 8px; right: 8px; width: 40px; height: 40px;
  border-radius: 50%; padding: 0; display: grid; place-items: center; cursor: pointer;
  background: color-mix(in srgb, var(--pl-tile-face) 80%, white);
  color: var(--pl-tile-ink); border: 1px solid color-mix(in srgb, var(--pl-tile-ink) 25%, transparent);
  font: 700 19px/1 var(--font-display); box-shadow: 0 2px 8px rgba(0,0,0,.18);
  transition: transform calc(120ms / var(--speed, 1)) ease-out, background 120ms ease-out; }
.pl-tile-plus:hover { transform: scale(1.1); }
.pl-tile-plus:active { transform: scale(.94); }
.pl-tile-plus:focus-visible { outline: 3px solid var(--pl-focus, #2b7de9); outline-offset: 2px; }
.pl-tile { position: relative; display: flex; flex-direction: column; text-decoration: none;
  aspect-ratio: 1.72; border-radius: clamp(16px, 2.4vmin, 26px);
  background: var(--pl-tile-face); color: var(--pl-tile-ink);
  border: 3px solid var(--pl-tile-rim); box-shadow: var(--pl-tile-shadow);
  overflow: hidden; isolation: isolate;
  transition: transform var(--d2) var(--ease-give), box-shadow var(--d2) var(--ease); }
.pl-tile::before { content: ""; position: absolute; inset: 0; z-index: 2; pointer-events: none;
  background: linear-gradient(180deg, rgba(255,255,255,.5), rgba(255,255,255,0) 44%); border-radius: inherit; }
[data-theme="dark"] .pl-tile::before { background: linear-gradient(180deg, rgba(255,226,180,.14), rgba(255,255,255,0) 44%); }
.pl-tile-preview { position: relative; flex: 1; min-height: 0; overflow: hidden; display: block; }
.pl-tile-preview > svg { position: absolute; inset: 0; width: 100%; height: 100%; }
.pl-tile-label { position: relative; z-index: 3; display: flex; align-items: baseline; gap: 8px;
  padding: 8px 15px 10px; background: color-mix(in srgb, var(--pl-tile-face) 88%, transparent); }
.pl-tile-name { font-weight: 800; font-size: clamp(14px, 1.8vmin, 19px); letter-spacing: -.01em; white-space: nowrap; }
/* the descriptive sentence is NOT grid furniture (overseer cut, R3): it
   travels with the launch splash, at the moment of intent. The class is
   kept for surfaces that explicitly opt in — Pleasant's grid does not. */
.pl-tile-sub { font-weight: 600; font-size: clamp(10.5px, 1.3vmin, 13.5px); color: var(--pl-tile-ink-soft);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pl-tile-badge { position: absolute; z-index: 4; top: 9px; right: 9px; min-width: 24px; height: 24px;
  padding: 0 7px; border-radius: 99px; display: grid; place-items: center;
  background: var(--pl-badge); color: var(--pl-on-badge);
  font: 800 13px/1 var(--font-display); box-shadow: 0 2px 8px rgba(0,0,0,.25); }
.pl-tile.is-empty { background: color-mix(in srgb, var(--pl-tile-face) 26%, transparent);
  border: 3px dashed color-mix(in srgb, var(--pl-tile-rim) 65%, transparent); box-shadow: none; }
.pl-tile.is-empty::before { display: none; }
@media (hover: hover) and (pointer: fine) {
  .pl-tile:hover { transform: scale(1.02); box-shadow: var(--pl-tile-shadow), inset 0 0 0 2px color-mix(in srgb, var(--pl-tile-ink) 55%, transparent); z-index: 3;
    transition-duration: calc(150ms / var(--speed)); }
}
/* dusk re-grade (overseer cut, R3): at night the tile art itself dims —
   one uniform grade over every scene, so no pastel rectangle shouts in
   the dark room. Warm-shifted, saturation and brightness down. */
[data-theme="dark"] .pl-tile-preview > svg { filter: sepia(.14) saturate(.68) brightness(.8) hue-rotate(-6deg); }
.pl-tile:focus-visible { outline: none; box-shadow: var(--pl-tile-shadow), 0 0 0 3px var(--pl-bar-edge), 0 0 0 6.5px var(--pl-focus); z-index: 3; }
.pl-tile:active { transform: scale(.965); }
.pl-tile.is-pressed { transform: scale(.965); }

/* ---------- the shelf (bottom bar) ---------- */
/* FLUSH-DOCK ruling (2026-10-25, the Principal): docked, the bar's outer
   margin is the safe-area inset alone — no floating gap at the docked
   edge in either home (top placement mirrors this in the shell's
   app.css §61 rules). Its home is a per-instance setting whose
   default is TOP (shell app.js §61); the dress here is position-
   neutral apart from this docked bottom margin. */
.pl-shelf { position: relative; z-index: 2; display: flex; align-items: center; gap: clamp(8px, 1.6vmin, 16px);
  width: min(880px, calc(100% - clamp(20px, 4.8vmin, 56px))); margin: 0 auto var(--safe-b);
  padding: 10px clamp(12px, 2vmin, 22px); border-radius: 999px;
  background: var(--pl-bar); border: 1.5px solid var(--pl-bar-edge);
  backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
  box-shadow: 0 14px 34px -14px rgba(20,50,74,.35); }
.pl-shelf .pl-spacer { flex: 1; }

/* ---------- c29-orb: the round shelf button ---------- */
c29-orb { display: inline-block; flex: none; }
.pl-orb { position: relative; width: clamp(50px, 7.2vmin, 64px); height: clamp(50px, 7.2vmin, 64px);
  border-radius: 50%; border: 2px solid var(--pl-bar-edge); background: var(--pl-orb-face);
  color: var(--pl-orb-ink); display: grid; place-items: center; cursor: pointer; padding: 0;
  box-shadow: 0 6px 16px -6px rgba(20,50,74,.4), inset 0 2px 3px rgba(255,255,255,.65);
  transition: transform var(--d1) var(--ease-give), box-shadow var(--d2) var(--ease); }
.pl-orb svg { width: 55%; height: 55%; }
.pl-orb:hover { transform: scale(1.07); }
.pl-orb:active, .pl-orb.is-pressed { transform: scale(.92); }
.pl-orb:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--pl-bar-edge), 0 0 0 6.5px var(--pl-focus); }
.pl-orb-badge { position: absolute; top: -3px; right: -3px; min-width: 21px; height: 21px; padding: 0 6px;
  border-radius: 99px; background: var(--pl-badge); color: var(--pl-on-badge);
  font: 800 12px/21px var(--font-display); box-shadow: 0 2px 6px rgba(0,0,0,.3); }
.pl-orb .ic-moon { display: none; }
[data-theme="dark"] .pl-orb .ic-sun { display: none; }
[data-theme="dark"] .pl-orb .ic-moon { display: block; }

/* ---------- c29-clock: the cluster ---------- */
c29-clock { display: block; text-align: right; line-height: 1; color: var(--pl-ink); flex: none; }
.pl-clock-date { font-size: clamp(11px, 1.3vmin, 12.5px); font-weight: 700; letter-spacing: .04em;
  color: var(--pl-ink-soft); text-transform: capitalize; }
.pl-clock-time { font-size: clamp(24px, 3.4vmin, 30px); font-weight: 700; letter-spacing: -.01em;
  font-variant-numeric: tabular-nums; text-shadow: 0 1px 0 rgba(255,255,255,.35); }
[data-theme="dark"] .pl-clock-time { text-shadow: 0 2px 14px rgba(0,0,0,.45); }
.pl-clock-time small { font-size: .4em; font-weight: 700; margin-left: 4px; color: var(--pl-ink-soft); }
@media (max-width: 700px) { .pl-clock-date { display: none; } }

/* ---------- launch: zoom ghost + splash ---------- */
.pl-zoomghost { position: fixed; z-index: 60; margin: 0; pointer-events: none;
  transition: transform calc(430ms / var(--speed)) var(--ease-sheet), border-radius calc(430ms / var(--speed)) var(--ease-sheet), opacity calc(200ms / var(--speed)) var(--ease); }
.pl-splash { position: fixed; inset: 0; z-index: 61; display: grid; place-items: center;
  /* adversary R1 SEV-2: the name rides a deep sky scrim, so the pair
     (ink on the theme's own horizon colour) passes in both themes over
     ANY tile the ghost is zooming from — ≈8:1 day, ≈7:1 dusk by the
     rig's worst-tile model. NO backdrop-filter: a full-screen blur over
     a 4x-scaling ghost crashed the headless renderer outright and is a
     jank hazard on lean-back hardware; the scrim carries it alone. */
  background: color-mix(in srgb, var(--pl-sky-low) 80%, transparent);
  opacity: 0; pointer-events: none;
  transition: opacity calc(300ms / var(--speed)) var(--ease); }
.pl-splash.is-on { opacity: 1; }
.pl-splash-name { font: 800 clamp(30px, 6vmin, 54px)/1 var(--font-display); color: var(--pl-ink); text-align: center;
  text-shadow: 0 2px 22px rgba(255,255,255,.5); transform: translateY(8px); transition: transform calc(300ms / var(--speed)) var(--ease-give); }
.pl-splash-sub { display: block; margin-top: 9px; font: 600 clamp(15px, 2.2vmin, 20px)/1.3 var(--font-display); color: var(--pl-ink-soft); }
.pl-splash.is-on .pl-splash-name { transform: none; }

/* ---------- settings sheet: calm static-feeling panels ---------- */
/* THE PLEASANT SCROLL CONTRACT (D5, 2026-10-23): every vertical
   scroller in the shell wears the same three properties —
   overflow-y:auto, touch-action:pan-y, overscroll-behavior:contain —
   and NEVER -webkit-overflow-scrolling. That property is deprecated
   since iOS 13 (momentum is native) and asks WebKit for a separate
   scrolling layer; on a surface whose content is still settling
   (Pleasant renders progressively from its feeds) that layer
   intermittently never engages, the pan falls through to the
   overflow:hidden stage, and the menu simply sits there — Victor's
   "sometimes the menu can't scroll". The contract is enforced by a
   grep gate in the D5/D6 proof rig: the property may not return to
   any vertical scroller in tide.css/app.css.
   THE EXIT LAW (D8, 2026-10-23 — Victor, on his own phone: "Settings
   has gotten stuck a bunch of times and you're just fucking stuck in
   settings"): a sheet's exit may NEVER live only inside the scroll
   region whose failure is the known failure mode. The sheet head —
   which carries the back orb — is therefore sticky (positioning in
   the shell layer, app.css, from the rooms' §46 header work — this
   component rule carries its backdrop so the stuck head reads as a
   header bar, never bare text over passing panels; the rooms' §46
   scrolled-state treatment that once overrode it was REMOVED by the
   Principal's ruling of 2026-10-25 — no scroll-triggered header
   morphs, so this backdrop is the head's one form at every scroll
   offset), every sheet opens at the top (app.js resets scrollTop in
   each opener), and the sheet never pans sideways. The shelf stays
   inert while a sheet is up by design (one sheet at a time; the
   state machine is built on it), so the sticky head is the
   guaranteed exit — proven by the D8 rig's frozen-scroller legs,
   which freeze the sheet's overflow mid-scroll and still close it
   with one tap on the stuck head's back orb. */
.pl-sheet { position: absolute; inset: 0 0 calc(88px + var(--safe-b)) 0; z-index: 20; display: flex; flex-direction: column;
  align-items: center; padding: calc(18px + var(--safe-t)) 16px calc(14px + var(--safe-b)); overflow-y: auto; overflow-x: hidden;
  touch-action: pan-y; overscroll-behavior: contain; }
.pl-sheet[hidden] { display: none; }
/* SETTINGS IS A SURFACE, NOT A WINDOW (Principal ruling, 2026-10-25,
   EDITS-3 item 4): the sheet family's layer ladder — room sheets ride
   the base 20 above, search 22, the Settings scrim 24, and Settings
   itself 26, on an opaque ground made of the sky's own gradient (the
   tokens flip it to the dusk stops after dark). #plSheet stands first
   in the DOM, so at the family's shared 20 an open room painted over
   it; and a sheet with no ground of its own lets the room ghost
   through its padding and edges. Settings opens OVER whatever is up:
   it covers its rect completely, the stage wears a scrim beneath it
   while it is open, and the room under it is inerted (app.js
   openSheet) — one surface, one scroll region. Mirrored in
   pleasant/app.css, which shipped bundles read until the next Tide
   build picks this up. */
.pl-stage.pl-sheet-open::before { content: ""; position: absolute; inset: 0; z-index: 24;
  background: rgba(13, 27, 40, .38); pointer-events: none;
  animation: pl-scrim-in calc(430ms / var(--speed)) var(--ease) both; }
@keyframes pl-scrim-in { from { opacity: 0; } to { opacity: 1; } }
html[data-theme="dark"] .pl-stage.pl-sheet-open::before { background: rgba(2, 4, 10, .55); }
html[data-motion="reduced"] .pl-stage.pl-sheet-open::before { animation: none; }
#plSheet { z-index: 26;
  background: linear-gradient(180deg, var(--pl-sky-top) 0%, var(--pl-sky-mid) 52%, var(--pl-sky-low) 100%); }
/* D1 (night loop 2026-10-24): SAFE CENTERING for the sheet's frame.
   align-items:center in a scrolling flex column is unsafe centering:
   a child wider than the content box overflows past the scroll origin
   on the inline-start side and is clipped there, unreachable — the
   exact signature on the Principal's phone (room frame hard against
   the left edge, aurora gap only on the right). Auto cross margins
   center identically when the child fits, and compute to 0 when it
   does not, pinning the child to the padding edge instead of pushing
   its start off-screen. P1's %-widths stop the overflow; this stops
   the clip if any future child outgrows the box again. */
.pl-sheet > .pl-sheet-head, .pl-sheet > .pl-panel { margin-inline: auto; }
/* D2 (night loop 2026-10-24): D1's safe centering covered only the
   head/panel pair. Three component-level extensions, same law:
   (1) EVERY direct child of a sheet is capped at the sheet's content
   box — a future oversized child shrinks/clips at the padding edge
   instead of pushing the room off the scroll origin.
   (2) The place panel is a vertical scroller (app.css: .pl-note
   .pl-panel overflow-y:auto); with overflow-x left at its default,
   the computed value becomes auto too, so one wide child makes the
   whole room horizontally scrollable and iOS keeps the offset —
   the room reads shifted/clipped even with the frame fixed. The
   panel's inline axis is now locked; wide content clips at the
   panel's own edge, never shifts the room.
   (3) Card text inside a panel can never force the panel wider:
   cards cap at the panel's content box and long unbroken strings
   (URLs, codes, file paths in titles/summaries/details) wrap
   instead of overflowing — .pl-card-detail's pre-wrap alone only
   breaks at spaces. */
.pl-sheet > * { max-width: 100%; }
.pl-panel.pl-note-panel { overflow-x: hidden; }
.pl-panel .pl-card { max-width: 100%; min-width: 0; }
.pl-panel .pl-card-title, .pl-panel .pl-card-sum, .pl-panel .pl-card-detail,
.pl-panel .pl-card-rec, .pl-panel .pl-desk-title, .pl-panel .pl-desk-bigtext { overflow-wrap: anywhere; }
/* D3 (night loop 2026-10-24): D2's wrap list missed three Desk text
   nodes — RENDERED-PROVEN in the D3 rig: a 120-char unbroken token in
   a card's last-words span (.pl-desk-lastwords, computed
   overflow-wrap:normal) reached x=961 at 390 wide and set the panel's
   scrollWidth to 942 against a 352 clientWidth; the panel lock then
   clipped it mid-word — the Principal's own comment text, cut off.
   The same guard now covers the last-words block (overflow-wrap
   inherits to its spans), the staff state line (free-text staff
   notes ride it) and the card meta line. */
.pl-panel .pl-card-meta, .pl-panel .pl-state, .pl-panel .pl-desk-lastwords { overflow-wrap: anywhere; }
/* EDITS-3 item 2 reconciliation (2026-10-25): the sheet head had two
   canon dresses. The SHIPPED bundle (tide.min.css) wears the newer
   form — solid tile-face ground, 3px rim, 24px top radius, padded,
   margin-bottom 0: the head in the Principal's screenshot and the
   EDITS-3/4 after-frames, measured fully opaque. This canon still
   carried the older dress (sky tint + backdrop blur, 0 0 18px
   radius, margin-bottom 16px), so any surface rebuilt from repo-src
   would have regressed the head on the next Tide build. The shipped
   dress wins — it is the proven, Principal-approved truth — and
   canon now carries it verbatim; the tint dress is retired. The
   item-2 visual audit beyond this reconciliation remains its own
   pass (EDITS-3 record). */
.pl-sheet-head { width: min(600px, 100%); display: flex; align-items: center; gap: 14px; margin-bottom: 0; color: var(--pl-tile-ink);
  background: var(--pl-tile-face); border: 3px solid var(--pl-tile-rim);
  border-radius: 24px 24px 0 0; padding: 12px 18px 10px; }
.pl-sheet-head h2 { font-size: clamp(24px, 4vmin, 34px); }
/* P1 (night loop 2026-10-24): the panel and the sheet head were sized
   in vw (94vw) inside the sheet's 16px side padding — at phone widths
   94vw is WIDER than the sheet's content box (100vw − 32px), so every
   room's frame bled past both edges and the sheet's overflow-x:hidden
   clipped it: rooms never sat "in" the UI. Widths are now relative to
   the sheet's content box (100%), capped as before. */
.pl-panel { width: min(600px, 100%); background: var(--pl-tile-face); color: var(--pl-tile-ink);
  border: 3px solid var(--pl-tile-rim); border-radius: 24px; box-shadow: var(--pl-tile-shadow);
  padding: 8px 22px; margin-bottom: 14px; }
/* seg labels inside the sheet, dusk (rig R4: canon's --ink-mute on the
   seg track over the plum panel measured 4.27:1 — the sheet's own ink
   carries them instead; day already passes and stays untouched) */
[data-theme="dark"] .pl-panel .t-seg button { color: var(--pl-tile-ink-soft); }
.pl-panel-foot { padding: 13px 0 9px; border-top: 1.5px solid color-mix(in srgb, var(--pl-tile-ink) 10%, transparent);
  font-size: 12px; font-weight: 600; color: var(--pl-tile-ink-soft); text-align: center; }
/* D3 (night loop 2026-10-24): the duplicate .pl-sheet-head width rule
   that sat here (min(600px,100%), silently overriding the 680px in the
   main rule above — D2-6's dead-code trap) is collapsed: the main rule
   now carries 600px itself. Cascade-identical at every width. */
.pl-row { display: flex; align-items: center; gap: 14px; padding: 15px 0; min-height: 56px; }
.pl-row + .pl-row { border-top: 1.5px solid color-mix(in srgb, var(--pl-tile-ink) 10%, transparent); }
.pl-row-text { flex: 1; min-width: 0; }
.pl-row-name { font-weight: 800; font-size: 16.5px; }
.pl-row-sub { font-size: 13.5px; color: var(--pl-tile-ink-soft); font-weight: 600; margin-top: 1px; }
.pl-note { width: min(680px, 100%); color: var(--pl-ink-soft); font-weight: 600; font-size: 13.5px; text-align: center; }
/* D1 (night loop 2026-10-24): .pl-note is a NOTE-sheet voice (centered
   prose, capped measure) that four sheet ELEMENTS also carry as a
   class — including #plPlace, the generic room sheet. Two leaks:
   (1) width on the sheet itself: an absolutely positioned inset:0
   sheet with a capped width is over-constrained and anchors to the
   inline-start edge on wide stages, taking every room off-center
   with it — sheet geometry comes from inset alone, never from the
   note cap. (2) the centered prose voice inherits into every room:
   the Desk's titles, summaries and recommendations rendered
   text-align:center on the Principal's phone (see his 2026-10-24
   screenshot) where the room spec is left-aligned app text. Prose
   rooms (Note, Assistants, Wave) keep the note voice; the place
   sheet is an app surface and starts its text at the left. */
.pl-sheet.pl-note { width: auto; }
.pl-sheet.pl-place { text-align: left; }

/* ---------- the lock (preview gate) ---------- */
.pl-lock { position: absolute; inset: 0; z-index: 50; display: grid; place-items: center; padding: 20px;
  /* P12 (night loop 2026-10-25): a definite column. Without it the single
     implicit track sizes to the card's max-content, so the card's
     width:min(400px,100%) resolves against an area wider than the
     container and the gate card renders 396-400px wide at x=20 on a
     390px phone — right rim 26-31px off-screen, in both engines
     (hunt2/lockdiag rigs, Round 12). The track is now the container's
     content box; the card's own cap centres it. */
  grid-template-columns: minmax(0, 1fr); }
.pl-lock[hidden] { display: none; }
/* BUILDER PF19 (R19, PF19-4): lock exit — unlock() stamps
   .pl-lock-out and lands `hidden` at the fade's end (200ms), so
   the gate dissolves over the home it reveals instead of
   vanishing in one step; the lock never intercepts taps while
   it leaves. Reduced motion: the JS stands the fade down and
   hides immediately. */
.pl-lock.pl-lock-out { animation: pl-lock-out 200ms var(--ease) both; pointer-events: none; }
@keyframes pl-lock-out { from { opacity: 1; } to { opacity: 0; } }
.pl-lock-card { width: min(400px, 100%); text-align: center; background: var(--pl-tile-face); color: var(--pl-tile-ink);
  border: 3px solid var(--pl-tile-rim); border-radius: 28px; box-shadow: var(--pl-tile-shadow); padding: 30px 28px 26px; }
.pl-lock-mark { width: 62px; height: 62px; margin: 0 auto 12px; border-radius: 50%;
  background: radial-gradient(circle at 35% 30%, var(--sun-300), var(--sea-400) 62%, var(--sea-600)); }
.pl-lock-card h2 { font-size: 26px; margin-bottom: 4px; }
.pl-lock-card p { color: var(--pl-tile-ink-soft); font-weight: 600; font-size: 14.5px; margin-bottom: 18px; }
.pl-lock-row { display: flex; gap: 9px; }
.pl-lock-input { flex: 1; min-width: 0; font: 600 16px var(--font-display); color: var(--pl-tile-ink);
  background: color-mix(in srgb, var(--pl-tile-ink) 5%, transparent); border: 2px solid color-mix(in srgb, var(--pl-tile-ink) 16%, transparent);
  border-radius: 999px; padding: 12px 18px; }
.pl-lock-input:focus { outline: none; box-shadow: var(--focus); }
.pl-lock-input::placeholder { color: var(--pl-tile-ink-soft); opacity: 1; }
.pl-lock-go { flex: none; border: 0; border-radius: 999px; padding: 0 22px; cursor: pointer;
  background: var(--money); color: var(--on-money); font: 800 15.5px var(--font-display); }
.pl-lock-err { min-height: 20px; margin-top: 10px; font-size: 13.5px; font-weight: 700; color: var(--bad-text); }
.pl-lock.is-shake .pl-lock-card { animation: pl-shake calc(320ms / var(--speed)) var(--ease); }
@keyframes pl-shake { 0%,100% { transform: none; } 22% { transform: translateX(-8px); } 55% { transform: translateX(7px); } 78% { transform: translateX(-4px); } }

/* ---------- the pointer (R7: a clean minimal arrow — the glove retired on the Principal's verdict) ---------- */
.pl-pointer { position: fixed; z-index: 80; left: 0; top: 0; width: 30px; height: 30px;
  pointer-events: none; opacity: 0; filter: drop-shadow(0 2px 4px rgba(15,40,60,.35));
  transition: opacity var(--d2) var(--ease); }
.pl-pointer.is-on { opacity: 1; }
@media (hover: hover) and (pointer: fine) {
  /* ONE POINTER (§29): while the glove is on, the native cursor is
     suppressed on EVERYTHING inside the stage — every element and
     pseudo-element, whatever its own rule asks for (pointer, grab,
     ew-resize, text, not-allowed). !important is the mechanism, not
     a shortcut: direct cursor declarations live in dozens of rules
     across canon and the shell sheet, each one beating the inherited
     none was the double-cursor bug. The class leaves with the
     Pointer setting and native cursors return untouched. */
  .pl-stage.pl-pointer-on, .pl-stage.pl-pointer-on *,
  .pl-stage.pl-pointer-on *::before, .pl-stage.pl-pointer-on *::after { cursor: none !important; }
}
/* wiggle-to-find (§29): ghost echoes the grown hand sheds along the
   wiggle path — WAAPI drives transform/opacity, this is only dress */
.pl-pointer-ghost { position: fixed; z-index: 79; left: 0; top: 0; width: 30px; height: 30px;
  pointer-events: none; opacity: 0; will-change: transform, opacity;
  filter: drop-shadow(0 2px 4px rgba(15,40,60,.25)); }
.pl-pointer-ghost svg, .pl-pointer svg { display: block; width: 100%; height: 100%; }

/* landscape phone (adversary R1 SEV-3: never rendered before) — the
   grid goes six-across so two calm rows fit the short sky; the date
   line stands down, the shelf keeps its orbs and the time. */
@media (max-height: 500px) and (orientation: landscape) {
  .pl-page { grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 10px; padding-block: 8px; }
  .pl-main { padding-top: 2px; }
  .pl-clock-date { display: none; }
  .pl-dots { padding-top: 2px; }
}

/* phone portrait, rebuilt not shrunk (overseer cut, R3) — declared
   AFTER the tile rules it overrides: the grid starts under a capped
   sky band, tiles grow taller, and the name rides the bottom of the
   art on a scrim of the tile's own face colour. */
@media (max-width: 700px) and (orientation: portrait) {
  .pl-main { justify-content: flex-start; padding-top: 33px; }
  /* the dots' honest 24px targets (R4) cost height the column does not
     have to spare — the row tightens so all 8 tiles + dots stay above
     the shelf with zero page overflow (rig-measured, 864 -> 844) */
  .pl-dots { padding-top: 0; }
  .pl-dots button { width: 24px; height: 24px; }
  .pl-tile { aspect-ratio: 1 / 0.9; }
  .pl-tile-label { position: absolute; left: 0; right: 0; bottom: 0;
    padding: 20px 13px 9px; background: linear-gradient(180deg, transparent, color-mix(in srgb, var(--pl-tile-face) 99%, transparent) 38%); }
}

/* ---------- reduced motion: the surface stills ---------- */
html[data-motion="reduced"] .pl-cloud, html[data-motion="reduced"] .pl-stars,
html[data-motion="reduced"] .pl-tile-preview svg * { animation: none !important; }
html[data-motion="reduced"] .pl-pages { transition: none; }
}

/* ================================================================
 * PROJECT PLEASANT R5 — the feel round (2026-10-21): flick paging,
 * search, the centred clock, and one quiet toast. Same laws as the
 * block above: paint-only theming, durations ride the tokens, the
 * reduced-motion gate stills everything at once.
 * ================================================================ */
@layer c29.components {
/* the grid group (pages + dots) centres as ONE body between the sky
   and the shelf — auto margins centre it when there is room and let
   it top-anchor when there is not, so no breakpoint needs its own
   exception (Directive 3 edge, closed R5) */
.pl-main { justify-content: flex-start; }
/* the flick engine drives this surface: the browser must not take
   the gesture over as a pan (a stolen gesture arrives as
   pointercancel and the drag dies mid-flight) */
.pl-pages-clip { margin-top: auto; touch-action: pan-y; } /* pan-y (P0 2026-10-21): was none — it claimed every touch and killed scrolling outright */
.pl-tile { -webkit-user-drag: none; }
.pl-dots { margin-bottom: auto; }
@media (max-width: 700px) and (orientation: portrait) {
  .pl-main { padding-top: calc(10px + var(--safe-t)); }
  /* R0-D12 holds in portrait too: the 24px dot target is retired */
  .pl-dots button { width: 44px; height: 44px; }
}

/* the clock sits at the shelf's true centre (Directive 3 edge,
   closed R5): the shelf is two equal clusters — home + search on
   the left, theme + settings on the right — and the time floats
   between them, centred on the shelf itself, not on what's left */
.pl-clock-center { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  text-align: center; pointer-events: none; }
.pl-clock-center c29-clock { text-align: center; }
.pl-clock-center .pl-clock-time { text-shadow: 0 1px 0 rgba(255,255,255,.35); }
@media (max-width: 700px) { .pl-clock-center .pl-clock-date { display: none; } }

/* §4 FRAME — RESERVED SLOTS (r12 fix, 2026-10-22, the Principal's
   iPhone field report on r10): the centred clock was ABSOLUTELY
   positioned over a flex row that reserved no space for it. On a
   390px shelf the five 50px orbs + gaps leave a ~56px free band,
   but the clock cluster (time + the weather line under it) needs
   ~100px — so the time painted over the search orb and the weather
   line stacked into the same pixels. Absolute centring is only
   safe where the free band provably exceeds the cluster; at phone
   width the cluster therefore takes a REAL in-flow slot: it flexes
   between the left and right orb clusters, the spacer and the
   wordmark stand down (the wordmark already stands down on compact
   / touch stages — this makes the phone rule width-true, not
   posture-true), orbs take the 44px floor, and the weather line is
   contained in its slot (one line, ellipsis) instead of spilling
   over its neighbours. No co-resident shelf element may ever share
   pixels with another: if it has no slot, it does not paint. */
@media (max-width: 700px) {
  .pl-shelf { gap: 6px; padding: 8px 10px; }
  .pl-shelf .pl-orb { width: 44px; height: 44px; }
  .pl-shelf .pl-spacer, .pl-shelf .pl-shelf-brand { display: none; }
  .pl-clock-center { position: static; transform: none; flex: 1 1 auto; min-width: 0; }
  .pl-clock-center .pl-clock-time { font-size: 20px; }
  .pl-clock-center .pl-clock-sub { white-space: nowrap; overflow: hidden;
    text-overflow: ellipsis; max-width: 100%; }
}

/* ---------- room actions: the decision buttons (r12 fix) ----------
   The rooms' action row (.pl-tour-row) and its buttons (.pl-tour-btn)
   shipped with NO author style at all: the class was used across the
   briefing room, the holder panel and the kits room but defined
   nowhere, so the buttons painted in full UA default dress — which
   on the Principal's iPhone rendered as near-black pills with dark,
   unreadable labels (Chromium's UA dress differs; the mechanism is
   the missing rule, not any one engine's default). Canon owns the
   pair now, in the tile ink/face tokens that invert together, so the
   label clears 4.5:1 in both themes by construction:
   day — ink #0A2540 on face #FFFFFF (15.9:1); the .primary inverts
   the same pair. Dusk — ink #E6EDF3 on face #131E29 (13.4:1).
   The quiet/disabled voice is a RECESSED fill (face mixed 12%
   toward ink) carrying the full-ink label — ≈9.6:1 day, ≈12:1
   dusk from rendered pixels. (First cuts failed honestly: riding
   --pl-tile-ink-soft rendered at ≈4.0:1 in day, and an 80/20
   ink-on-face text mix, while truly ≈6:1, sampled at 4.1–4.5:1
   under a naive two-cluster pixel read — too near the line for a
   state whose whole job is to be read at a glance. A fill change
   keeps the label at full ink under every sampler.) Never opacity,
   which would wash the label under the floor. .is-alert carries
   the alert channel's amber as an OUTER glow only — a coloured
   border inside the pill put a third colour mass beside the label
   in the pill's own pixels (amber + text sampled together at
   4.1:1 on one engine); the glow lives outside the pill, the label
   pair inside stays the default pair. .is-alert is the glow's
   room-side state; .is-quiet is that same button acknowledged —
   glow gone, fill recessed. */
.pl-place-note { font-size: 14.5px; line-height: 1.55; color: var(--pl-tile-ink); margin: 0 0 10px; }
.pl-tour-row { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
.pl-tour-btn { font: inherit; font-weight: 700; font-size: 14.5px; line-height: 1.2;
  min-height: 44px; padding: 8px 18px; border-radius: 999px;
  border: 1px solid var(--edge-soft); background: var(--pl-tile-face);
  color: var(--pl-tile-ink); cursor: pointer;
  transition: transform var(--d1) var(--ease), box-shadow var(--d2) var(--ease), background var(--d2) var(--ease); }
.pl-tour-btn:hover { box-shadow: 0 4px 14px rgba(20,40,60,.16); }
.pl-tour-btn:active { transform: scale(.97); }
.pl-tour-btn:focus-visible { outline: 3px solid var(--pl-focus, #2B7DE9); outline-offset: 2px; }
.pl-tour-btn.primary { background: var(--pl-tile-ink); color: var(--pl-tile-face); border-color: transparent; }
.pl-tour-btn.is-alert { box-shadow: 0 0 0 3px rgba(246,183,60,.35), 0 0 18px 2px rgba(217,119,6,.35); }
.pl-tour-btn.is-quiet { background: color-mix(in srgb, var(--pl-tile-face) 88%, var(--pl-tile-ink)); box-shadow: none; }
.pl-tour-btn:disabled, .pl-tour-btn[disabled] { background: color-mix(in srgb, var(--pl-tile-face) 88%, var(--pl-tile-ink));
  cursor: default; transform: none; box-shadow: none; }

/* ---------- search: find it by name, land on it by key ---------- */
.pl-search { position: absolute; inset: 0 0 calc(88px + var(--safe-b)) 0; z-index: 22; display: flex; flex-direction: column;
  align-items: center; padding: calc(16px + var(--safe-t)) 16px 16px; overflow: auto; }
.pl-search[hidden] { display: none; }
.pl-search-card { width: min(620px, 100%); background: var(--pl-tile-face); color: var(--pl-tile-ink);
  border: 3px solid var(--pl-tile-rim); border-radius: 26px; box-shadow: var(--pl-tile-shadow);
  padding: 18px; animation: pl-rise var(--d3) var(--ease-sheet); }
.pl-search-head { display: flex; align-items: center; gap: 12px; margin-bottom: 14px; }
.pl-search-head h2 { font-size: clamp(21px, 3.4vmin, 27px); }
.pl-search-head .pl-orb { width: 44px; height: 44px; }
.pl-search-row { display: flex; gap: 10px; align-items: center; }
.pl-search-input { flex: 1; min-width: 0; font: 700 20px/1.2 var(--font-display); color: var(--pl-tile-ink);
  background: color-mix(in srgb, var(--pl-tile-ink) 5%, transparent);
  border: 2px solid color-mix(in srgb, var(--pl-tile-ink) 16%, transparent);
  border-radius: 999px; padding: 14px 22px; }
.pl-search-input:focus { outline: none; box-shadow: var(--focus); }
.pl-search-input::placeholder { color: var(--pl-tile-ink-soft); opacity: 1; }
.pl-results { margin-top: 14px; display: grid; gap: 6px; max-height: 48dvh; overflow: auto; }
.pl-result { display: flex; align-items: baseline; gap: 10px; width: 100%; text-align: left;
  padding: 13px 16px; border-radius: 16px; border: 0; cursor: pointer; font: inherit;
  background: color-mix(in srgb, var(--pl-tile-ink) 5%, transparent); color: var(--pl-tile-ink); }
.pl-result-name { font-weight: 800; font-size: 17px; flex: none; }
.pl-result-sub { flex: 1; min-width: 0; font-weight: 600; font-size: 13.5px; color: var(--pl-tile-ink-soft);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pl-result-where { flex: none; font-size: 12px; font-weight: 700; color: var(--pl-tile-ink-soft); }
.pl-result.is-active { background: var(--pl-orb-face);
  box-shadow: inset 0 0 0 2.5px var(--pl-focus); }
.pl-result:focus-visible { outline: none; box-shadow: inset 0 0 0 2.5px var(--pl-focus); }
.pl-search-empty { padding: 18px 10px 8px; text-align: center; font-weight: 600;
  font-size: 14.5px; color: var(--pl-tile-ink-soft); }
.pl-search-hint { margin-top: 12px; text-align: center; font-size: 12.5px; font-weight: 600;
  color: var(--pl-tile-ink-soft); }
@keyframes pl-rise { from { opacity: 0; transform: translateY(14px) scale(.985); } }
html[data-motion="reduced"] .pl-search-card { animation: none; }

/* ---------- toast ---------- */
/* DOCK LAW (§68, the Principal's order 2026-10-23): the toast's dock
   FOLLOWS the host's shelf — shelf along the bottom, the toast rides
   the top; shelf moved to the top or the side, the toast docks at
   the bottom, clear of the bar and of the player. The element is
   body-level (VEIL LAW, tide.js), so stage-scoped vars cannot reach
   it: the host publishes the dock at ROOT scope and the toast reads
   it here —
     --pl-toast-top / --pl-toast-bottom   the anchor edges; the host
       sets exactly one, the other is "auto". Both unset = the
       original top dock, byte-for-byte the pre-law geometry.
     --pl-toast-x      a horizontal centre shift — at the side rail,
       half the rail's extent, so a bottom toast centres in the free
       stage instead of under the rail; the width budget shrinks by
       the same shift on both sides.
     --pl-toast-rise   the entrance offset's sign (+8px at the bottom
       dock, so the toast rises into view instead of dropping).
   The host owns the state (Pleasant: the §61 shelf placement, one
   setting, no second switch); the component owns this contract. */
.pl-toast { position: fixed; z-index: 90; left: calc(50% + var(--pl-toast-x, 0px));
  top: var(--pl-toast-top, calc(14px + var(--safe-t)));
  bottom: var(--pl-toast-bottom, auto);
  transform: translate(-50%, var(--pl-toast-rise, -8px));
  max-width: min(520px, calc(92vw - 2 * var(--pl-toast-x, 0px))); text-align: center;
  padding: 10px 20px; border-radius: 999px; pointer-events: none;
  background: var(--pl-bar); border: 1.5px solid var(--pl-bar-edge);
  backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
  color: var(--pl-ink); font: 700 14px/1.35 var(--font-display);
  box-shadow: 0 14px 34px -14px rgba(20,50,74,.4);
  opacity: 0; transition: opacity var(--d3) var(--ease), transform var(--d3) var(--ease-sheet); }
.pl-toast.is-on { opacity: 1; transform: translate(-50%, 0); }
}

/* ---------- R5: the preview door (select -> banner -> Start) ---------- */
@layer c29.components {
/* when a controller (or the spatial keyboard) is driving, the
   focus ring is the cursor — it shows on every landing, not only
   where :focus-visible heuristics guess a keyboard */
.pl-stage.pl-pad-on .pl-tile:focus,
.pl-stage.pl-pad-on .pl-orb:focus,
.pl-stage.pl-pad-on .pl-dots button:focus,
.pl-stage.pl-pad-on .pl-result:focus,
.pl-stage.pl-pad-on button:focus { outline: 3px solid var(--focus); outline-offset: 3px; }

.pl-preview { position: fixed; inset: 0; z-index: 61; display: grid; place-items: center; padding: 18px;
  background: color-mix(in srgb, var(--pl-sky-low) 72%, transparent);
  opacity: 0; pointer-events: none; transition: opacity calc(280ms / var(--speed)) var(--ease); }
.pl-preview.is-on { opacity: 1; pointer-events: auto; }
.pl-preview-card { width: min(640px, 100%); max-height: 92dvh; overflow: auto;
  background: var(--pl-tile-face); color: var(--pl-tile-ink);
  border: 3px solid var(--pl-tile-rim); border-radius: 26px; box-shadow: var(--pl-tile-shadow);
  padding: 16px; animation: pl-rise var(--d3) var(--ease-sheet); }
.pl-preview-banner { position: relative; aspect-ratio: 2.55; border-radius: 18px; overflow: hidden;
  background: var(--pl-tile-face); }
.pl-preview-banner .pl-tile-preview { position: absolute; inset: 0; }
.pl-preview-banner .pl-tile-preview > svg { position: absolute; inset: 0; width: 100%; height: 100%; }
.pl-preview-name { font: 800 clamp(24px, 4.4vmin, 34px)/1.1 var(--font-display); margin-top: 14px; }
.pl-preview-sub { display: block; margin-top: 5px; font: 600 clamp(14px, 2vmin, 16.5px)/1.35 var(--font-display);
  color: var(--pl-tile-ink-soft); }
.pl-preview-config { margin-top: 6px; }
.pl-preview-plus { margin-top: 12px; font: 600 clamp(13px, 1.8vmin, 15px)/1.4 var(--font-display);
  color: var(--pl-tile-ink-soft); }
.pl-preview-config:empty { display: none; }
.pl-preview-config .pl-row { padding: 12px 0; }
.pl-preview-actions { display: flex; gap: 12px; margin-top: 16px; }
.pl-btn-quiet { flex: none; min-width: 118px; min-height: 56px; padding: 0 22px; border-radius: 999px; cursor: pointer;
  border: 2px solid color-mix(in srgb, var(--pl-tile-ink) 22%, transparent); background: transparent;
  color: var(--pl-tile-ink); font: 800 16px var(--font-display); }
.pl-btn-go { flex: 1; min-height: 56px; padding: 0 22px; border-radius: 999px; cursor: pointer; border: 0;
  background: var(--money); color: var(--on-money); font: 800 17px var(--font-display);
  box-shadow: 0 10px 22px -10px rgba(20,50,74,.5); }
.pl-btn-quiet:active, .pl-btn-go:active { transform: scale(.97); }
.pl-btn-quiet:focus-visible, .pl-btn-go:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--pl-tile-face), 0 0 0 6.5px var(--pl-focus); }
.pl-fadeout { position: fixed; inset: 0; z-index: 70; background: #0B0F14; opacity: 0; pointer-events: none;
  transition: opacity calc(260ms / var(--speed)) var(--ease); }
.pl-fadeout.is-on { opacity: 1; }
html[data-motion="reduced"] .pl-preview-card { animation: none; }
}

/* ============================================================
 * TIDE — THE STORE FAMILY  <c29-appcard> · <c29-kitcallout> · <c29-grantcard>
 * §59 Lane 7 (Design/UI + Icons), 2026-10-22. Specimens:
 * verticals/app-store.html; the icon system lives in
 * project-pleasant/app-store/icons/ICON-SYSTEM.md.
 *
 * Provenance: §58 made apps.29court.com a store; the shelf it
 * replaces hand-rolled its cards per surface (the picker page),
 * and the grant card (SQUARE-INSTALL §3) had no Tide carrier at
 * all. These three are the store's only card shapes.
 *
 * Construction law (canon): layout + chrome are PURE CSS on
 * host-authored light DOM — every card reads finished with
 * tide.js dead. JS adds events (and the attribute-form adoption
 * for appcard) only. Tokens only; the one filled action per card
 * is the --money/--on-money pair, spelled with doubled
 * specificity so no host bare-button rule can strip its text
 * half (the 2026-10-14 pair-contrast lesson). Motion inside the
 * token durations; the global motion gate collapses it.
 * ============================================================ */
@layer c29.components {

/* ---------- the icon pane (.st-pane) — the glyph's seat ----------
   One radius + one size family with c29-tile .tl-art; the ground
   is the catalog category as DATA (ICON-SYSTEM.md): washes ride
   tokens, never per-entry hand-picked literals. */
.st-pane { flex: none; width: 52px; height: 52px; border-radius: calc(var(--corner) - 6px);
  display: grid; place-items: center; overflow: hidden;
  background: var(--accent-soft); color: var(--accent-text); }
.st-pane svg { width: 28px; height: 28px; }
.st-pane svg .ica { stroke: var(--29d-coral-500); }
.st-pane svg .icaf { fill: var(--29d-coral-500); }
.st-pane.is-sun   { background: var(--sun-100); color: var(--warn-text); }
[data-theme="dark"] .st-pane.is-sun { background: var(--warn-bg); }
.st-pane.is-sky   { background: color-mix(in srgb, #7FC7E8 20%, var(--surface)); color: var(--accent-text); }
.st-pane.is-stone { background: var(--wash); color: var(--ink-soft); }
.st-pane.is-coral { background: color-mix(in srgb, var(--money) 12%, var(--surface)); color: var(--money-text); }
.st-pane.is-hero  { width: 76px; height: 76px; border-radius: calc(var(--corner) - 4px); }
.st-pane.is-hero svg { width: 40px; height: 40px; }
.st-pane.is-workshop { background: var(--wash); color: var(--ink-mute);
  box-shadow: inset 0 0 0 1.5px var(--edge); border-radius: calc(var(--corner) - 6px); }
.st-pane.is-workshop svg { opacity: .55; }

/* ---------- <c29-appcard> — one catalog entry ---------- */
c29-appcard { display: block; min-width: 0; }
c29-appcard .stc-card { display: flex; gap: var(--s3); height: 100%;
  padding: var(--s4); border-radius: var(--corner);
  background: var(--surface); box-shadow: var(--hair); color: inherit;
  transition: background var(--d1) var(--ease); }
c29-appcard .stc-card:active { background: var(--wash); }
c29-appcard .stc-main { flex: 1 1 auto; min-width: 0; display: grid; gap: 2px; align-content: start; }
c29-appcard .stc-name { font: 700 16.5px/1.3 var(--font-display); letter-spacing: var(--track-head);
  overflow-wrap: anywhere; }
c29-appcard .stc-line { font-size: 13.5px; line-height: 1.45; color: var(--ink-mute); overflow-wrap: anywhere; }
c29-appcard .stc-row { display: flex; align-items: center; gap: var(--s2); flex-wrap: wrap; margin-top: var(--s2); }
c29-appcard .stc-kind { color: var(--ink-mute); }
/* State chip: word + glyph, never colour alone. Only the chip
   matching data-state shows — pure CSS, so state reads JS-dead. */
c29-appcard .stc-state { display: none; align-items: center; gap: 5px; }
c29-appcard .stc-state svg { width: 13px; height: 13px; }
c29-appcard[data-state="on-grid"] .stc-state.is-on-grid { display: inline-flex; color: var(--accent-text); }
c29-appcard[data-state="workshop"] .stc-state.is-workshop { display: inline-flex; color: var(--ink-mute); }
c29-appcard .stc-actions { margin-left: auto; display: inline-flex; gap: var(--s2); align-items: center; }
c29-appcard .stc-try, c29-appcard .stc-open { min-height: 40px; padding: 0 var(--s3);
  display: inline-flex; align-items: center; border-radius: calc(var(--corner) - 8px);
  font: 600 13.5px/1 var(--font-ui, inherit); color: var(--accent-text);
  background: transparent; border: 0; cursor: pointer; text-decoration: none; }
c29-appcard .stc-try:active, c29-appcard .stc-open:active { background: var(--wash); }
c29-appcard .stc-add { min-height: 40px; padding: 0 var(--s4);
  display: inline-flex; align-items: center; gap: 6px; border-radius: calc(var(--corner) - 8px);
  font: 700 13.5px/1 var(--font-ui, inherit); border: 0; cursor: pointer;
  transition: transform var(--d1) var(--ease), background var(--d1) var(--ease); }
c29-appcard .stc-add.stc-add { background: var(--money); color: var(--on-money); }
c29-appcard .stc-add:active { transform: scale(.97); }
c29-appcard .stc-add svg { width: 15px; height: 15px; }
c29-appcard .stc-try:focus-visible, c29-appcard .stc-open:focus-visible,
c29-appcard button.stc-add:focus-visible { outline: none; box-shadow: var(--focus); }
/* state decides which actions exist — CSS only */
c29-appcard[data-state="on-grid"] .stc-add, c29-appcard[data-state="on-grid"] .stc-try { display: none; }
c29-appcard[data-state="available"] .stc-open, c29-appcard[data-state="workshop"] .stc-open { display: none; }
c29-appcard[data-state="workshop"] .stc-add { background: var(--wash); color: var(--ink-soft); }

/* ---------- <c29-kitcallout> — the persona kit at login ---------- */
c29-kitcallout { display: block; min-width: 0; }
c29-kitcallout .stk-card { padding: var(--s4); border-radius: var(--corner);
  background: var(--raised); box-shadow: var(--hair); display: grid; gap: var(--s3); }
c29-kitcallout .stk-head { display: flex; align-items: flex-start; gap: var(--s3); }
c29-kitcallout .stk-badge { flex: none; width: 40px; height: 40px; border-radius: calc(var(--corner) - 8px);
  display: grid; place-items: center; background: color-mix(in srgb, var(--money) 12%, var(--surface)); color: var(--money-text); }
c29-kitcallout .stk-badge svg { width: 22px; height: 22px; }
c29-kitcallout .stk-kicker { font: 700 11px/1.2 var(--font-ui, inherit); letter-spacing: .12em;
  text-transform: uppercase; color: var(--ink-mute); }
c29-kitcallout .stk-title { font: 700 17px/1.3 var(--font-display); letter-spacing: var(--track-head); margin-top: 2px; }
c29-kitcallout .stk-line { font-size: 13.5px; line-height: 1.45; color: var(--ink-mute); }
c29-kitcallout .stk-dismiss { margin-left: auto; flex: none; width: 40px; height: 40px;
  display: grid; place-items: center; border: 0; border-radius: 50%;
  background: transparent; color: var(--ink-mute); cursor: pointer; }
c29-kitcallout .stk-dismiss:active { background: var(--wash); }
c29-kitcallout .stk-dismiss svg { width: 17px; height: 17px; }
c29-kitcallout .stk-dismiss:focus-visible { outline: none; box-shadow: var(--focus); }
c29-kitcallout .stk-items { display: grid; gap: var(--s2); }
c29-kitcallout .stk-row { display: flex; align-items: center; gap: var(--s3);
  padding: var(--s2) var(--s3); border-radius: calc(var(--corner) - 6px); background: var(--surface); box-shadow: var(--hair); }
c29-kitcallout .stk-row .st-pane { width: 40px; height: 40px; border-radius: calc(var(--corner) - 9px); }
c29-kitcallout .stk-row .st-pane svg { width: 22px; height: 22px; }
c29-kitcallout .stk-row-main { flex: 1 1 auto; min-width: 0; }
c29-kitcallout .stk-row-name { font: 700 14.5px/1.3 var(--font-display); overflow-wrap: anywhere; }
c29-kitcallout .stk-row-line { font-size: 12.5px; line-height: 1.4; color: var(--ink-mute); overflow-wrap: anywhere; }
c29-kitcallout .stk-row-add { flex: none; min-height: 40px; padding: 0 var(--s3);
  display: inline-flex; align-items: center; border-radius: calc(var(--corner) - 8px);
  font: 600 13px/1 var(--font-ui, inherit); border: 0; cursor: pointer; background: var(--wash); color: var(--ink); }
c29-kitcallout .stk-row-add.stk-row-add { background: var(--wash); color: var(--ink); }
c29-kitcallout .stk-row[data-added="true"] .stk-row-add { background: transparent; color: var(--accent-text); cursor: default; }
c29-kitcallout .stk-row-add:focus-visible { outline: none; box-shadow: var(--focus); }
c29-kitcallout .stk-foot { display: flex; align-items: center; gap: var(--s3); flex-wrap: wrap; }
c29-kitcallout .stk-addall { min-height: 44px; padding: 0 var(--s5);
  display: inline-flex; align-items: center; gap: 6px; border-radius: calc(var(--corner) - 8px);
  font: 700 14px/1 var(--font-ui, inherit); border: 0; cursor: pointer;
  transition: transform var(--d1) var(--ease); }
c29-kitcallout .stk-addall.stk-addall { background: var(--money); color: var(--on-money); }
c29-kitcallout .stk-addall:active { transform: scale(.97); }
c29-kitcallout .stk-addall:focus-visible { outline: none; box-shadow: var(--focus); }
c29-kitcallout .stk-note { font-size: 12.5px; color: var(--ink-mute); }
c29-kitcallout[data-dismissed="true"] { display: none; }

/* ---------- <c29-grantcard> — the install act (SQUARE-INSTALL §3) ----------
   The card IS the consent record: want / why / fallback render
   verbatim from the manifest, fallback always visible. Exactly
   one consent action, enforced like c29-offer (extras hidden in
   place and counted on data-consent-extra). */
c29-grantcard { display: block; min-width: 0; }
c29-grantcard .stg-card { padding: var(--s5); border-radius: var(--corner);
  background: var(--raised); box-shadow: var(--hair); display: grid; gap: var(--s4); }
c29-grantcard .stg-head { display: flex; gap: var(--s3); align-items: center; }
c29-grantcard .stg-name { font: 700 18px/1.3 var(--font-display); letter-spacing: var(--track-head); }
c29-grantcard .stg-sub { font-size: 13px; color: var(--ink-mute); margin-top: 1px; }
c29-grantcard .stg-rows { display: grid; gap: var(--s3); }
c29-grantcard .stg-row { display: grid; gap: 2px; padding: var(--s3) var(--s4);
  border-radius: calc(var(--corner) - 6px); background: var(--surface); box-shadow: var(--hair); }
c29-grantcard .stg-cap { display: flex; align-items: center; gap: 7px;
  font: 700 11px/1.2 var(--font-ui, inherit); letter-spacing: .12em; text-transform: uppercase; color: var(--ink-mute); }
c29-grantcard .stg-cap svg { width: 14px; height: 14px; }
c29-grantcard .stg-want { font: 600 14.5px/1.4 var(--font-ui, inherit); }
c29-grantcard .stg-why { font-size: 13.5px; line-height: 1.45; color: var(--ink-soft); }
c29-grantcard .stg-fallback { font-size: 13px; line-height: 1.45; color: var(--ink-mute); }
c29-grantcard .stg-fallback b { color: var(--ink-soft); font-weight: 600; }
c29-grantcard .stg-actions { display: flex; align-items: center; gap: var(--s3); flex-wrap: wrap; }
c29-grantcard .stg-consent { min-height: 44px; padding: 0 var(--s5);
  display: inline-flex; align-items: center; gap: 6px; border-radius: calc(var(--corner) - 8px);
  font: 700 14px/1 var(--font-ui, inherit); border: 0; cursor: pointer;
  transition: transform var(--d1) var(--ease); }
c29-grantcard .stg-consent.stg-consent { background: var(--money); color: var(--on-money); }
c29-grantcard .stg-consent:active { transform: scale(.97); }
c29-grantcard .stg-cancel { min-height: 44px; padding: 0 var(--s3);
  display: inline-flex; align-items: center; border-radius: calc(var(--corner) - 8px);
  font: 600 13.5px/1 var(--font-ui, inherit); border: 0; cursor: pointer;
  background: transparent; color: var(--ink-soft); }
c29-grantcard .stg-cancel:active { background: var(--wash); }
c29-grantcard .stg-consent:focus-visible, c29-grantcard .stg-cancel:focus-visible { outline: none; box-shadow: var(--focus); }
c29-grantcard .stg-extra-note { font-size: 12.5px; color: var(--warn-text); }

/* ============================================================
 * TIDE — THE DIAGRAM FAMILY  <c29-dnode> · <c29-dgroup> ·
 * <c29-dedgelens> · <c29-dstateline> · <c29-dstagebanner> ·
 * <c29-ddive>
 * Steals build — Fleet 2, 2026-10-22, from the Railway
 * teardown (29court-brains/projects/teardowns/railway-2026-10-21/,
 * TEARDOWN §4 grammar + STEALS #3/#4/#6). What is taken is the
 * GRAMMAR only: a node is icon + name + state in words;
 * containment carries structure and edges are drawn on demand,
 * never as a permanent layer; the hypothetical wears ONE
 * reserved treatment and commits through ONE button; depth is
 * navigated by the dive, never displayed all at once. None of
 * Railway's brand, name, trade dress, illustration style, copy
 * or purple appears here — the staged accent is the house
 * lagoon token below (hue ~205°), reserved to this one meaning
 * across the whole system and used for nothing else.
 *
 * Construction law (canon): layout + chrome are PURE CSS on
 * host-authored light DOM — nodes, groups, state lines and the
 * staged banner all read finished with tide.js dead. JS adds
 * events, counts, the edge lens and the dive. State is host
 * truth carried in data-state / data-kind; the component never
 * invents a state. Tokens only; motion rides the --d* scale
 * and the global motion gate collapses it.
 * Specimens: verticals/diagram.html (Tide expression +
 * Pleasant expression of the same data). Build + adversary
 * record: project-pleasant/diagram/.
 * ============================================================ */

/* ---------- the family's reserved tokens ----------
   --dgm-staged is THE "proposed, not real" accent. It is not
   the money colour, not the sea accent, not ok/warn/bad, and
   it is never violet. The treatment is always accent + dashed
   edge + hatch + the word "Staged", so it survives colour
   blindness, greyscale, and the Pleasant veil translation. */
c29-dnode, c29-dgroup, c29-dedgelens, c29-dstateline, c29-dstagebanner, c29-ddive, .dgm-canvas {
  --dgm-staged: #1F6FB2;
  --dgm-staged-text: #175C96;
  --dgm-staged-wash: color-mix(in srgb, var(--dgm-staged) 10%, var(--surface));
  --dgm-hatch: repeating-linear-gradient(45deg, color-mix(in srgb, var(--dgm-staged) 22%, transparent) 0 5px, transparent 5px 11px);
}
[data-theme="dark"] c29-dnode, [data-theme="dark"] c29-dgroup, [data-theme="dark"] c29-dedgelens,
[data-theme="dark"] c29-dstateline, [data-theme="dark"] c29-dstagebanner, [data-theme="dark"] c29-ddive,
[data-theme="dark"] .dgm-canvas {
  --dgm-staged: #8FC1E9;
  --dgm-staged-text: #A9D0F0;
  --dgm-staged-wash: color-mix(in srgb, var(--dgm-staged) 14%, var(--surface));
}

/* ---------- the canvas (.dgm-canvas) ----------
   A relative field the host composes: groups and loose nodes
   in a grid — containment first. The edge lens overlays it;
   everything else stacks above the lens. */
.dgm-canvas { position: relative; display: grid; gap: var(--s5); min-width: 0; }
.dgm-canvas > :not(c29-dedgelens) { position: relative; z-index: 1; }
.dgm-nodes { display: grid; gap: var(--s3); grid-template-columns: repeat(auto-fill, minmax(218px, 1fr)); min-width: 0; }

/* ---------- <c29-dstateline> — the state, in words ----------
   Word + glyph, never colour alone; pure CSS on data-state,
   so it reads with JS dead and inside any other component. */
c29-dstateline { display: inline-flex; align-items: center; gap: 7px; min-width: 0;
  font-size: 13px; line-height: 1.35; color: var(--ink-soft); }
c29-dstateline .dgsl-glyph { flex: none; width: 9px; height: 9px; border-radius: 50%;
  background: var(--ink-4); }
c29-dstateline .dgsl-words { min-width: 0; overflow-wrap: anywhere; }
c29-dstateline[data-state="live"] .dgsl-glyph { background: var(--sea-500); }
c29-dstateline[data-state="building"] .dgsl-glyph { background: var(--sea-500);
  animation: dgm-pulse calc(1.6s / var(--speed)) ease-in-out infinite; }
c29-dstateline[data-state="staged"] { color: var(--dgm-staged-text); font-weight: 600; }
c29-dstateline[data-state="staged"] .dgsl-glyph { border-radius: 2px; background: transparent;
  box-shadow: inset 0 0 0 2px var(--dgm-staged); }
c29-dstateline[data-state="blocked"] { color: var(--warn-text); font-weight: 600; }
c29-dstateline[data-state="blocked"] .dgsl-glyph { background: var(--warn); border-radius: 2px; }
c29-dstateline[data-state="failed"] { color: var(--bad-text); font-weight: 650; }
c29-dstateline[data-state="failed"] .dgsl-glyph { background: var(--bad); border-radius: 2px;
  clip-path: polygon(20% 0, 0 20%, 0 80%, 20% 100%, 80% 100%, 100% 80%, 100% 20%, 80% 0); }
c29-dstateline[data-state="neglected"] { color: var(--ink-mute); }
c29-dstateline[data-state="neglected"] .dgsl-glyph { background: transparent;
  box-shadow: inset 0 0 0 2px var(--ink-4); }
c29-dstateline[data-state="urgent"] { color: var(--ink); font-weight: 650; }
c29-dstateline[data-state="urgent"] .dgsl-glyph { background: var(--money);
  animation: dgm-pulse calc(2.4s / var(--speed)) ease-in-out infinite; }
c29-dstateline[data-state="requested"] { color: var(--ink-mute); }
c29-dstateline[data-state="requested"] .dgsl-glyph { background: transparent; border-radius: 2px;
  box-shadow: inset 0 0 0 2px var(--ink-4); }
c29-dstateline[data-state="stale"] { color: var(--ink-mute); }
@keyframes dgm-pulse { 50% { opacity: .35; } }

/* ---------- <c29-dnode> — one thing in the system ----------
   Anatomy (TEARDOWN §4.1): icon · plain name · state line in
   words · one secondary truth. The kind word rides the head;
   references (data-feeds / data-watches / data-routes-to /
   data-deploys / data-bills) are DATA the lens reads — they
   are never drawn by default. */
c29-dnode { display: block; min-width: 0; border-radius: var(--corner); }
c29-dnode:focus { outline: none; }
c29-dnode:focus-visible .dgn-card { box-shadow: var(--focus); }
c29-dnode .dgn-card { position: relative; height: 100%; display: grid; gap: 3px; align-content: start;
  padding: var(--s3) var(--s4); border-radius: var(--corner); overflow: hidden;
  background: var(--surface); box-shadow: var(--hair); color: inherit;
  transition: background var(--d1) var(--ease); }
c29-dnode .dgn-head { display: flex; align-items: center; gap: var(--s2); min-width: 0; }
c29-dnode .dgn-icon { flex: none; width: 34px; height: 34px; border-radius: calc(var(--corner) - 9px);
  display: grid; place-items: center; background: var(--accent-soft); color: var(--accent-text); }
c29-dnode .dgn-icon svg { width: 19px; height: 19px; }
c29-dnode .dgn-name { font: 700 15.5px/1.3 var(--font-display); letter-spacing: var(--track-head);
  overflow-wrap: anywhere; min-width: 0; }
c29-dnode .dgn-kind { margin-left: auto; flex: none; font: 700 10.5px/1.2 var(--font-ui, inherit);
  letter-spacing: .1em; text-transform: uppercase; color: var(--ink-mute); }
c29-dnode .dgn-truth { font-size: 12.5px; line-height: 1.4; color: var(--ink-mute); overflow-wrap: anywhere; }
/* state treatments — the words live in the state line; the
   card treatment is the redundant second channel. */
c29-dnode[data-state="failed"] .dgn-card { box-shadow: var(--hair), inset 3px 0 0 var(--bad); }
c29-dnode[data-state="blocked"] .dgn-card { box-shadow: var(--hair), inset 3px 0 0 var(--warn); }
c29-dnode[data-state="urgent"] .dgn-card { animation: dgm-urgent calc(2.6s / var(--speed)) ease-in-out infinite; }
c29-dnode[data-state="neglected"] .dgn-icon { background: var(--wash); color: var(--ink-mute); }
c29-dnode[data-state="requested"] .dgn-card { box-shadow: none; border: 1.5px dashed var(--edge); background: transparent; }
c29-dnode[data-state="staged"] .dgn-card { background: var(--dgm-staged-wash);
  border: 1.5px dashed var(--dgm-staged); box-shadow: none; }
c29-dnode[data-state="staged"] .dgn-card::before { content: ""; position: absolute; top: 0; left: 0; right: 0;
  height: 6px; background-image: var(--dgm-hatch); }
c29-dnode[data-state="staged"] .dgn-icon { background: color-mix(in srgb, var(--dgm-staged) 16%, var(--surface));
  color: var(--dgm-staged-text); }
@keyframes dgm-urgent { 50% { box-shadow: var(--hair), 0 0 0 5px color-mix(in srgb, var(--money) 16%, transparent); } }

/* ---------- <c29-dgroup> — containment carries structure ----
   A labeled bounding region. Collapsed, it IS a summary
   node: head + aggregate truth in words + links to the
   broken members (STEALS #5's aggregation, carried here). */
c29-dgroup { display: block; min-width: 0; }
c29-dgroup .dgg-box { display: grid; gap: var(--s3); padding: var(--s4);
  border: 1.5px solid var(--edge); border-radius: var(--corner);
  background: color-mix(in srgb, var(--surface) 55%, var(--bg)); }
c29-dgroup .dgg-head { display: flex; align-items: flex-start; gap: var(--s3); flex-wrap: wrap; min-width: 0; }
c29-dgroup .dgg-name { flex: none; padding: 5px 10px; border-radius: var(--r-pill);
  background: var(--raised); box-shadow: var(--hair);
  font: 700 12px/1.3 var(--font-ui, inherit); letter-spacing: .08em; text-transform: uppercase; color: var(--ink-soft); }
c29-dgroup .dgg-agg { flex: 1 1 220px; min-width: 0; padding-top: 3px; }
c29-dgroup .dgg-counts { display: inline-flex; gap: var(--s2); flex-wrap: wrap; }
c29-dgroup .dgg-count { display: none; align-items: center; padding: 3px 9px; border-radius: var(--r-pill);
  background: var(--wash); font: 600 12px/1.3 var(--font-ui, inherit); color: var(--ink-soft); }
c29-dgroup .dgg-count[data-n]:not([data-n="0"]) { display: inline-flex; }
c29-dgroup .dgg-count.is-failed { background: var(--bad-bg); color: var(--bad-text); }
c29-dgroup .dgg-count.is-blocked { background: var(--warn-bg); color: var(--warn-text); }
c29-dgroup .dgg-count.is-staged { background: var(--dgm-staged-wash); color: var(--dgm-staged-text);
  box-shadow: inset 0 0 0 1.5px var(--dgm-staged); }
c29-dgroup .dgg-actions { margin-left: auto; display: inline-flex; gap: var(--s2); flex: none; }
c29-dgroup .dgg-btn { min-height: 40px; padding: 0 var(--s3); display: inline-flex; align-items: center; gap: 6px;
  border-radius: calc(var(--corner) - 8px); border: 0; cursor: pointer;
  font: 600 13px/1 var(--font-ui, inherit); color: var(--accent-text); background: transparent; }
c29-dgroup .dgg-btn:active { background: var(--wash); }
c29-dgroup .dgg-btn:focus-visible { outline: none; box-shadow: var(--focus); }
c29-dgroup .dgg-broken { display: none; gap: var(--s2); flex-wrap: wrap; align-items: center;
  font-size: 13px; color: var(--ink-soft); }
c29-dgroup[data-has-broken="true"] .dgg-broken { display: flex; }
c29-dgroup .dgg-broken a { color: var(--bad-text); font-weight: 600; }
c29-dgroup .dgg-body { min-width: 0; }
c29-dgroup[data-collapsed="true"] .dgg-body { display: none; }
c29-dgroup[data-collapsed="true"] .dgg-box { background: var(--surface); box-shadow: var(--hair); border-color: transparent; }

/* ---------- <c29-dstagebanner> — proposed, not real ---------
   STEALS #3 whole: the count banner + per-item diff/discard
   + ONE Apply. The banner, its items, and any staged node
   share the one reserved treatment above; nothing else in
   the system may wear it. Apply is the commit, so Apply is
   the money pair (doubled specificity, house rule). */
c29-dstagebanner { display: block; min-width: 0; }
c29-dstagebanner[data-empty="true"] { display: none; }
c29-dstagebanner .dgs-bar { position: relative; overflow: hidden; display: flex; align-items: center;
  gap: var(--s3); flex-wrap: wrap; padding: var(--s3) var(--s4);
  border: 1.5px dashed var(--dgm-staged); border-radius: calc(var(--corner) - 4px);
  background: var(--dgm-staged-wash); }
c29-dstagebanner .dgs-bar::before { content: ""; position: absolute; top: 0; left: 0; right: 0;
  height: 6px; background-image: var(--dgm-hatch); }
c29-dstagebanner .dgs-count { font: 700 14.5px/1.35 var(--font-ui, inherit); color: var(--dgm-staged-text); }
c29-dstagebanner .dgs-note { font-size: 13px; color: var(--ink-soft); }
c29-dstagebanner .dgs-actions { margin-left: auto; display: inline-flex; gap: var(--s2); align-items: center; }
c29-dstagebanner .dgs-details-btn { min-height: 40px; padding: 0 var(--s3); display: inline-flex; align-items: center;
  border-radius: calc(var(--corner) - 8px); border: 0; cursor: pointer;
  font: 600 13.5px/1 var(--font-ui, inherit); color: var(--dgm-staged-text); background: transparent; }
c29-dstagebanner .dgs-details-btn:active { background: var(--wash-2); }
c29-dstagebanner .dgs-apply { min-height: 44px; padding: 0 var(--s5); display: inline-flex; align-items: center;
  gap: 6px; border-radius: calc(var(--corner) - 8px); border: 0; cursor: pointer;
  font: 700 14px/1 var(--font-ui, inherit);
  transition: transform var(--d1) var(--ease), background var(--d1) var(--ease); }
c29-dstagebanner .dgs-apply.dgs-apply { background: var(--money); color: var(--on-money); }
c29-dstagebanner .dgs-apply:active { transform: scale(.97); }
c29-dstagebanner .dgs-details-btn:focus-visible, c29-dstagebanner button.dgs-apply:focus-visible,
c29-dstagebanner .dgs-discard:focus-visible { outline: none; box-shadow: var(--focus); }
c29-dstagebanner .dgs-list { display: none; margin-top: var(--s3); }
c29-dstagebanner[data-open="true"] .dgs-list { display: grid; gap: var(--s2); }
c29-dstagebanner .dgs-item { display: flex; gap: var(--s3); align-items: flex-start;
  padding: var(--s3) var(--s4); border-radius: calc(var(--corner) - 6px);
  background: var(--surface); box-shadow: var(--hair), inset 3px 0 0 var(--dgm-staged); }
c29-dstagebanner .dgs-item[data-discarded="true"] { display: none; }
c29-dstagebanner .dgs-kind { flex: none; padding: 3px 9px; border-radius: var(--r-pill);
  font: 700 11px/1.3 var(--font-ui, inherit); letter-spacing: .06em; text-transform: uppercase;
  background: var(--wash); color: var(--ink-soft); }
c29-dstagebanner .dgs-kind.is-new { background: var(--ok-bg); color: var(--ok-text); }
c29-dstagebanner .dgs-kind.is-edited { background: var(--dgm-staged-wash); color: var(--dgm-staged-text); }
c29-dstagebanner .dgs-kind.is-removed { background: var(--bad-bg); color: var(--bad-text); }
c29-dstagebanner .dgs-main { flex: 1 1 auto; min-width: 0; display: grid; gap: 1px; }
c29-dstagebanner .dgs-what { font: 600 14px/1.4 var(--font-ui, inherit); overflow-wrap: anywhere; }
c29-dstagebanner .dgs-diff { font-size: 13px; line-height: 1.45; color: var(--ink-mute); overflow-wrap: anywhere; }
c29-dstagebanner .dgs-diff .dgs-old { text-decoration: line-through; }
c29-dstagebanner .dgs-discard { flex: none; min-height: 40px; padding: 0 var(--s3);
  display: inline-flex; align-items: center; gap: 5px; border-radius: calc(var(--corner) - 8px);
  border: 0; cursor: pointer; font: 600 13px/1 var(--font-ui, inherit);
  color: var(--ink-soft); background: transparent; }
c29-dstagebanner .dgs-discard:active { background: var(--wash); }
c29-dstagebanner .dgs-extra-note { font-size: 12.5px; color: var(--warn-text); margin-top: var(--s2); }

/* ---------- <c29-dedgelens> — edges on demand --------------
   STEALS #4: the default diagram has NO drawn wires. The
   lens draws a node's references only while that node holds
   focus or the pointer, or while the lens is switched on.
   While it draws, the overlay rides above the canvas (it is
   an inspection layer, pointer-transparent, and it exists
   only for the seconds the lens is active — at rest it draws
   nothing, so nothing is ever covered by default). Lines run
   card-edge to card-edge and carry their kind as a word at
   the midpoint. */
c29-dedgelens { position: absolute; inset: 0; z-index: 2; display: block; pointer-events: none; }
c29-dedgelens svg { display: block; width: 100%; height: 100%; overflow: visible; }
c29-dedgelens .dgl-edge { fill: none; stroke-width: 2; }
c29-dedgelens .dgl-edge.is-feeds { stroke: var(--accent); }
c29-dedgelens .dgl-edge.is-watches { stroke: var(--ink-4); stroke-dasharray: 3 5; }
c29-dedgelens .dgl-edge.is-routes-to { stroke: var(--29d-coral-500); }
c29-dedgelens .dgl-edge.is-deploys { stroke: var(--sea-700); }
c29-dedgelens .dgl-edge.is-bills { stroke: var(--warn); }
c29-dedgelens .dgl-label { font: 600 11px var(--font-ui, sans-serif); fill: var(--ink-soft);
  paint-order: stroke; stroke: var(--bg); stroke-width: 3px; stroke-linejoin: round; }

/* ---------- <c29-ddive> — depth navigated, not displayed ---
   STEALS #6: one level of detail at a time. With JS dead
   every view simply stacks and reads; with JS live the dive
   scopes to one view, the crumb names where you are, and
   focus + Esc bring you back to the exact door you entered. */
c29-ddive { display: block; min-width: 0; }
c29-ddive [data-dview] { min-width: 0; }
c29-ddive[data-live] [data-dview] { display: none; }
c29-ddive[data-live] [data-dview][data-active] { display: block;
  animation: dgm-dive-in var(--d3) var(--ease-sheet); }
@keyframes dgm-dive-in { from { opacity: 0; transform: translateY(12px) scale(.996); } }
c29-ddive .dgd-crumb { display: flex; align-items: center; gap: var(--s2); flex-wrap: wrap;
  font-size: 13.5px; color: var(--ink-mute); margin-bottom: var(--s3); }
c29-ddive .dgd-crumb [data-dive-crumb] { color: var(--ink); font-weight: 650; }
c29-ddive [data-dive-back] { min-height: 40px; padding: 0 var(--s3); display: inline-flex; align-items: center;
  gap: 6px; border-radius: calc(var(--corner) - 8px); border: 0; cursor: pointer;
  font: 600 13.5px/1 var(--font-ui, inherit); color: var(--accent-text); background: transparent; }
c29-ddive [data-dive-back]:active { background: var(--wash); }
c29-ddive [data-dive-back]:focus-visible { outline: none; box-shadow: var(--focus); }

/* ---------- the Pleasant expression (same data) -------------
   The grammar's second material: nodes become squares, type
   steps up to couch distance, groups become places. Same
   light DOM, same states, same words — a host opts in with
   data-expression="pleasant" on any ancestor. */
[data-expression="pleasant"] .dgm-nodes { grid-template-columns: repeat(auto-fill, minmax(158px, 1fr)); gap: var(--s4); }
[data-expression="pleasant"] c29-dnode .dgn-card { aspect-ratio: 1; padding: var(--s4);
  border-radius: calc(var(--corner) + 10px); align-content: start; gap: var(--s2); }
[data-expression="pleasant"] c29-dnode .dgn-icon { width: 44px; height: 44px; border-radius: calc(var(--corner) - 4px); }
[data-expression="pleasant"] c29-dnode .dgn-icon svg { width: 25px; height: 25px; }
[data-expression="pleasant"] c29-dnode .dgn-name { font-size: 19px; }
[data-expression="pleasant"] c29-dnode .dgn-kind { display: none; }
[data-expression="pleasant"] c29-dstateline { font-size: 14.5px; }
[data-expression="pleasant"] c29-dgroup .dgg-box { border-radius: calc(var(--corner) + 10px); border-width: 2px; }
[data-expression="pleasant"] c29-dstagebanner .dgs-bar { border-radius: var(--corner); }
/* ---------- end Diagram family ---------- */
}

/* ======================================================
   ERROR PAGE  <c29-error>   [the house error voice, 2026-10-24]
   Every error a human can hit on a 29Court surface renders
   through this one component. Presentation-only: the surface
   server-renders the instance with the canonical copy for its
   variant (registry entry c29-error carries the copy deck);
   actions are ordinary links/buttons the surface wires.
   Variants: lost (404) · gate (wrong word) · expired (410) ·
   signed-out (401) · not-allowed (403) · busy (429) ·
   broken (5xx, SERIOUS) · handoff (auth-chain refusal,
   SERIOUS) · payments (payment-path fault, SERIOUS) ·
   offline (client network). Serious variants carry the
   .er-assure line — and the line is only honest because the
   error-report mechanism (brains projects/error-pages) files
   exactly one ledger entry per serious error signature.
   Geometry is mobile-first: at 390px the card is 358px wide
   (390 - 2*16 page pad), actions stack full-width at the
   44px tap floor, nothing paints past the viewport.
   ====================================================== */
@layer c29.components {
c29-error { display: block; }
c29-error[page] { min-height: 100dvh; display: grid; place-items: center;
  padding: calc(var(--s6) + var(--safe-t)) var(--s4) calc(var(--s6) + var(--safe-b));
  background: var(--bg); }
c29-error .er-card { width: 100%; max-width: 420px; margin-inline: auto;
  background: var(--surface); border-radius: var(--corner); box-shadow: var(--lift);
  padding: var(--s6) var(--s5) var(--s5); display: grid; gap: var(--s3);
  animation: t-er-in var(--d3) var(--ease); }
@keyframes t-er-in { from { opacity: 0; transform: translateY(8px); } }
c29-error .er-mark { width: 44px; height: 44px; border-radius: 50%; flex: none;
  display: grid; place-items: center; background: var(--accent-soft); color: var(--accent-text); }
c29-error .er-mark svg { width: 22px; height: 22px; }
c29-error[variant="broken"] .er-mark, c29-error[variant="handoff"] .er-mark,
c29-error[variant="payments"] .er-mark { background: var(--bad-bg); color: var(--bad-text); }
c29-error[variant="busy"] .er-mark { background: var(--warn-bg); color: var(--warn-text); }
c29-error[variant="expired"] .er-mark, c29-error[variant="lost"] .er-mark {
  background: color-mix(in srgb, var(--sun-300) 45%, var(--surface)); color: var(--ink-2); }
c29-error .er-kicker { margin: 0; font-size: 11px; font-weight: 700; letter-spacing: .09em;
  text-transform: uppercase; color: var(--ink-mute); }
c29-error .er-title { margin: 0; font: 750 26px/1.2 var(--font-display); letter-spacing: -.01em;
  color: var(--ink); overflow-wrap: anywhere; }
c29-error .er-body { margin: 0; font-size: 15.5px; line-height: 1.55; color: var(--ink-soft); }
c29-error .er-body strong { color: var(--ink); }
c29-error .er-assure { margin: var(--s1) 0 0; display: flex; gap: var(--s2); align-items: flex-start;
  background: var(--ok-bg); color: var(--ok-text); border-radius: var(--r-md);
  padding: var(--s3) var(--s4); font-size: 14.5px; font-weight: 600; line-height: 1.45; }
c29-error .er-assure svg { width: 18px; height: 18px; flex: none; margin-top: 1px; }
c29-error .er-actions { display: grid; gap: var(--s2); margin-top: var(--s3); }
c29-error .er-actions .t-btn { min-height: var(--tap); display: inline-flex; align-items: center;
  justify-content: center; text-align: center; }
c29-error .er-actions a:focus-visible, c29-error .er-actions button:focus-visible {
  outline: none; box-shadow: var(--focus); }
c29-error .er-ref { margin: var(--s2) 0 0; font-family: var(--font-mono); font-size: 12px;
  color: var(--ink-mute); }
@media (min-width: 480px) {
  c29-error .er-actions { display: flex; flex-wrap: wrap; }
  c29-error .er-actions .t-btn { flex: 1 1 auto; }
}
}

/* ds-shell viewport-height fix (2026-10-25, Manners Lane 1, Tide-first):
   canonical fallback ordering for full-screen shells: 100vh first for
   older engines, then 100dvh so iOS Safari tracks the dynamic viewport.
   Mirrored in the vendored components.css copies (enroll/assets). */
.ds-shell { min-height: 100vh; min-height: 100dvh; }
.ds-sidebar { height: 100vh; height: 100dvh; }
.ds-drawer { height: 100vh; height: 100dvh; }

/* ============================================================
   RICH ROOM COMPONENTS (Fleet 10, 2026-10-25) — the machinery
   the Golden Hour room was missing. Rooms were prose because
   prose was all the shell gave them; these five make the rich
   version the easy version:
     <c29-sunarc>   the day as an arc + band strip, data-driven
     <c29-dayline>  the day as a labelled timeline
     <c29-verdict>  one big plain statement + one supporting fact
     <c29-rel>      a live relative-time chip ("in 3h 12m")
     <c29-statrow>  2–4 real numbers with plain labels
   Behaviour (positioning + the rel tick) lives in tide.js,
   section "rich room components". Without JS every component
   still renders: sunarc shows its resting band gradient and a
   flex row of moments, dayline a wrapped row of moments, rel
   the authored text. Tokens only (--pl-* / --29d-* / system);
   text always rides the tile face in --pl-ink / --pl-tile-ink-
   soft (≥4.5:1 measured in rich-rig proof, both themes) — the
   day bands are fills and never carry text. Nothing animates,
   nothing autoplays; the rel chip updates text on a slow tick.
   ============================================================ */
@layer c29.components {

/* ---------- <c29-sunarc> — the day, drawn ---------- */
c29-sunarc { display: block; }
c29-sunarc .sa-wrap { position: relative; }
c29-sunarc.has-arc .sa-wrap { padding-top: 60px; }
c29-sunarc .sa-arcbox { position: absolute; top: 0; height: 56px; min-width: 40px; }
c29-sunarc .sa-arcbox svg { display: block; width: 100%; height: 100%; overflow: visible; }
c29-sunarc .sa-arcbox path { fill: none; stroke-linecap: round; vector-effect: non-scaling-stroke;
  stroke: color-mix(in srgb, var(--sun-500) 62%, transparent); stroke-width: 2; }
c29-sunarc .sa-sun { position: absolute; width: 15px; height: 15px; border-radius: 50%;
  transform: translate(-50%, -50%);
  background: radial-gradient(circle at 38% 34%, var(--sun-100), var(--sun-500) 78%);
  box-shadow: 0 0 14px 3px var(--pl-glow); }
c29-sunarc .sa-sun.is-down { background: var(--pl-band-night); box-shadow: none; opacity: .5; }
c29-sunarc .sa-strip { position: relative; height: 16px; border-radius: var(--r-pill);
  box-shadow: inset 0 0 0 1px var(--edge-soft);
  background: linear-gradient(90deg, var(--pl-band-night) 0 6%, var(--pl-band-dawn) 10%,
    var(--pl-band-day) 22% 68%, var(--pl-band-gold) 82%, var(--pl-band-dusk) 93%,
    var(--pl-band-night) 100%); }
c29-sunarc .sa-nowline { position: absolute; top: -4px; bottom: -4px; width: 2px; margin-left: -1px;
  border-radius: 2px; background: var(--pl-ink); }
c29-sunarc .sa-nowline::after { content: ""; position: absolute; left: 50%; top: -4px;
  width: 8px; height: 8px; border-radius: 50%; background: var(--pl-ink); transform: translateX(-50%); }
c29-sunarc .sa-moments { display: flex; justify-content: space-between; gap: var(--s2);
  margin-top: var(--s2); }
c29-sunarc.is-laid .sa-moments { display: block; position: relative; height: 36px; }
c29-sunarc .sa-m { display: grid; gap: 0; max-width: 92px; text-align: left; }
c29-sunarc.is-laid .sa-m { position: absolute; top: 0; transform: translateX(-50%); text-align: center; }
c29-sunarc.is-laid .sa-m.is-first { transform: none; text-align: left; }
c29-sunarc.is-laid .sa-m.is-last { transform: translateX(-100%); text-align: right; }
c29-sunarc .sa-m b { font-size: 13px; font-weight: 700; line-height: 1.25; color: var(--pl-ink);
  font-variant-numeric: tabular-nums; white-space: nowrap; }
c29-sunarc .sa-m small { font-size: 11.5px; line-height: 1.3; color: var(--pl-tile-ink-soft);
  overflow-wrap: anywhere; }
c29-sunarc .sa-m.is-now b, c29-sunarc .sa-m.is-now small { color: var(--pl-badge); }

/* ---------- <c29-dayline> — the day, listed ---------- */
c29-dayline { display: block; }
c29-dayline .dl-track { position: relative; display: flex; flex-wrap: wrap;
  gap: var(--s2) var(--s4); padding-top: 14px; }
c29-dayline .dl-line { position: absolute; left: 0; right: 0; top: 5px; height: 2px;
  border-radius: 2px; background: var(--edge); }
c29-dayline .dl-m { position: relative; display: grid; gap: 1px; max-width: 96px; padding-left: 15px; }
c29-dayline .dl-m::before { content: ""; position: absolute; left: 0; top: 4px; width: 9px; height: 9px;
  border-radius: 50%; background: var(--pl-badge); box-shadow: 0 0 0 3px var(--pl-tile-face); }
c29-dayline.is-laid .dl-track { display: block; min-height: 56px; }
c29-dayline.is-laid .dl-m { position: absolute; top: 0; padding: 14px 0 0; max-width: 96px;
  transform: translateX(-50%); text-align: center; }
c29-dayline.is-laid .dl-m::before { left: 50%; top: 1px; transform: translateX(-50%); }
c29-dayline.is-laid .dl-m.is-first { transform: none; text-align: left; }
c29-dayline.is-laid .dl-m.is-first::before { left: 0; transform: none; }
c29-dayline.is-laid .dl-m.is-last { transform: translateX(-100%); text-align: right; }
c29-dayline.is-laid .dl-m.is-last::before { left: auto; right: 0; transform: none; }
c29-dayline.is-laid .dl-m.is-unplaced { display: none; }
c29-dayline .dl-m b { font-size: 13px; font-weight: 700; line-height: 1.25; color: var(--pl-ink);
  font-variant-numeric: tabular-nums; white-space: nowrap; }
c29-dayline .dl-m small { font-size: 11.5px; line-height: 1.3; color: var(--pl-tile-ink-soft);
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
c29-dayline .dl-now { position: absolute; top: 0; bottom: 0; width: 2px; margin-left: -1px;
  border-radius: 2px; background: var(--pl-ink); }
c29-dayline .dl-now::after { content: ""; position: absolute; left: 50%; top: 1px;
  width: 8px; height: 8px; border-radius: 50%; background: var(--pl-ink); transform: translateX(-50%); }

/* ---------- <c29-verdict> — the room's opening line ---------- */
c29-verdict { display: block; }
c29-verdict .vd-kick { margin: 0 0 var(--s2); font-size: 11px; font-weight: 700;
  letter-spacing: .09em; text-transform: uppercase; color: var(--accent-text); }
c29-verdict .vd-line { margin: 0; font: 750 29px/1.16 var(--font-display); letter-spacing: -.015em;
  color: var(--pl-ink); overflow-wrap: anywhere; text-wrap: balance; }
c29-verdict .vd-fact { margin: var(--s2) 0 0; font-size: 15.5px; line-height: 1.5;
  color: var(--pl-tile-ink-soft); overflow-wrap: anywhere; }
c29-verdict .vd-fact strong { color: var(--pl-ink); font-weight: 700; }
@media (min-width: 480px) { c29-verdict .vd-line { font-size: 32px; } }

/* ---------- <c29-rel> — the live relative-time chip ---------- */
c29-rel { display: inline-flex; align-items: center; gap: var(--s2); min-height: 40px;
  max-width: 100%; padding: var(--s2) var(--s4); border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--pl-ink) 6%, transparent);
  color: var(--pl-ink); font-size: 14.5px; line-height: 1.3; }
c29-rel::before { content: ""; flex: none; width: 8px; height: 8px; border-radius: 50%;
  background: var(--pl-badge); }
c29-rel .rl-label { font-weight: 600; overflow-wrap: anywhere; }
c29-rel .rl-when { font-weight: 750; font-variant-numeric: tabular-nums; white-space: nowrap; }
c29-rel .rl-when:empty { display: none; }

/* ---------- <c29-statrow> — real numbers, plain labels ---------- */
/* columns are counted, not fitted: auto-fit squeezed six stats into
   six ~44px columns and long numbers spilled across their neighbours
   (adversary A8) — an explicit count per child total cannot crush a
   cell, and anything past four wraps to a second row of three */
c29-statrow { display: grid; grid-template-columns: 1fr; gap: var(--s3); }
c29-statrow:has(> .sr-stat:nth-child(2):last-child) { grid-template-columns: repeat(2, 1fr); }
c29-statrow:has(> .sr-stat:nth-child(3):last-child) { grid-template-columns: repeat(3, 1fr); }
c29-statrow:has(> .sr-stat:nth-child(4):last-child) { grid-template-columns: repeat(4, 1fr); }
c29-statrow:has(> .sr-stat:nth-child(5)) { grid-template-columns: repeat(3, 1fr); }
c29-statrow .sr-stat { min-width: 0; display: grid; gap: 1px; align-content: start;
  grid-template-columns: minmax(0, 1fr); }
c29-statrow:has(> .sr-stat:nth-child(5)) > .sr-stat:nth-child(4) {
  border-inline-start: none; padding-inline-start: 0; }
c29-statrow .sr-stat + .sr-stat { border-inline-start: 1px solid var(--edge-soft);
  padding-inline-start: var(--s3); }
c29-statrow .sr-num { font: 750 26px/1.1 var(--font-display); letter-spacing: -.01em;
  color: var(--pl-ink); font-variant-numeric: tabular-nums; overflow-wrap: anywhere;
  min-width: 0; }
c29-statrow .sr-num small { font-size: 15px; font-weight: 700; }
c29-statrow .sr-num:empty::before { content: "—"; color: var(--pl-tile-ink-soft); }
c29-statrow .sr-lab { font-size: 12px; font-weight: 600; letter-spacing: .05em;
  text-transform: uppercase; line-height: 1.35; color: var(--pl-tile-ink-soft);
  overflow-wrap: break-word; }
@media (max-width: 400px) { c29-statrow .sr-num { font-size: 22px; } }
}
/* ---------- end rich room components ---------- */

/* ============================================================
   EPISODE VIEW (Fleet 3, Listen→Spotify look, 2026-10-25) —
   <c29-epview>: the Pleasant episode page. Designed from
   Fleet 1's teardown of the real Spotify page
   (FLEET-1-TEARDOWN.md), NOT from the assumed big-art hero:
   (1) artwork stays SMALL — 64x64, top-left, unframed; the
   TITLE does the hero work (24px/700, multi-line, episode
   number inside the title string); (2) identity comes from
   an art-derived GRADIENT WASH at the top (--ep-wash tinted
   into the tile face, fading down the hero), never a
   backdrop image; (3) ONE loud action — a single oversized
   play disc in the player's own coral (--money), alone at
   the right of the action row; every other control a quiet
   ghost; (4) one quiet meta line, "date • duration in
   words"; (5) the description in FULL — unclamped, no
   heading, no card; (6) the episode page carries NO
   same-show episode list — one "See all episodes" bridge
   row; the list itself lives on the show/index surface,
   where .ep-row (below) is the row pattern; (7) loudness
   follows commitment — the detail play disc is 72px and
   saturated, the list-row play is 48px and quiet.
   PRINCIPAL'S RULING (2026-10-25 ~07:08 PT, Reading A):
   build the per-episode page in this shape — artwork hero,
   full title, date + duration meta, big play, full
   description + show notes, highlights, Up Next — plus one
   borrowed piece for the index (a hero treatment for the
   newest episode; the index keeps its laws and shape). The
   row tap SPLITS: cover/title opens the episode page
   (c29:open), a play control on the row preserves one-tap
   play (c29:play-request). Chapters are NOT faked — no
   timed transcripts exist, so no demo carries a chapters
   section and .ep-chap stays DORMANT until real timed data
   exists; show notes carry the page.
   HARD LAWS: the view owns NO audio and NO playback bar —
   play taps hand off to the page's ONE c29-player
   (behaviour in tide.js, section "episode view"); nothing
   autoplays (a tap is the only path to select(i,true));
   Pleasant tokens only; targets >= 44px; long words wrap
   anywhere (the qs lesson). Hero text rides FULL
   --pl-tile-ink: the wash is a tint mixed into the tile
   face, and full ink keeps >=4.5:1 at every gradient stop
   (rig-measured, both themes, incl. pure-white and
   pure-black washes in epview-adv.html).
   ============================================================ */
@layer c29.components {

/* ---------- <c29-epview> — hero: small art, title-led ---------- */
c29-epview { display: block; color: var(--pl-tile-ink); }
/* BUILDER PF18 (R18, PF18-3): the episode VIEW arrives in the house
   motion language — a t-rise-family entrance on --d3/--ease — instead
   of the hard cut CRITIC PF18 measured (transition 0s, animation
   none, strip frames byte-identical). Scoped to the view class the
   shell stamps (.pl-ep-view): the shelf host (c29-epview.pl-ep-shelf)
   is a different surface and does not rise. The flip grammar is
   untouched (rowlink opens/never plays, rowplay plays). No exit
   animation: the view is removed by closeEpView's body rewrite, and
   an exit would need a JS removal delay PF18 records as owed.
   Reduced motion stands the entrance down (the canon gates at the
   head of this sheet already crush it; this line makes it explicit
   at the component). */
c29-epview.pl-ep-view { animation: t-rise var(--d3) var(--ease) both; }
html[data-motion="reduced"] c29-epview.pl-ep-view { animation: none; }
/* BUILDER PF19 (R19, PF19-1): the episode view's EXIT — mirror of
   the PF18-3 entrance grammar: closeEpView (shell) stamps
   .pl-ep-closing and lands its body rewrite at this animation's
   end (0.32s), so the view settles out (fade + the same 8px the
   entrance rose from) instead of cutting to ABSENT. Reduced
   motion stands it down, and the JS keeps its immediate rewrite
   on that path. */
c29-epview.pl-ep-view.pl-ep-closing { animation: t-settle-out var(--d3) var(--ease) both; }
@keyframes t-settle-out { from { opacity: 1; transform: none; } to { opacity: 0; transform: translateY(8px); } }
html[data-motion="reduced"] c29-epview.pl-ep-view.pl-ep-closing { animation: none; }
c29-epview .ep-hero { padding: var(--s5) var(--s5) var(--s4);
  background: var(--pl-tile-face); }
c29-epview .ep-hero.has-wash { background:
  linear-gradient(180deg,
    color-mix(in srgb, var(--ep-wash) 35%, var(--pl-tile-face)) 0%,
    color-mix(in srgb, var(--ep-wash) 18%, var(--pl-tile-face)) 48%,
    color-mix(in srgb, var(--ep-wash) 7%, var(--pl-tile-face)) 78%,
    var(--pl-tile-face) 100%); }
c29-epview .ep-art { position: relative; overflow: hidden; flex: none;
  width: 64px; height: 64px; border-radius: 8px; box-shadow: var(--hair);
  background: radial-gradient(120% 120% at 30% 20%, var(--sea-200), var(--sea-600) 78%); }
c29-epview .ep-art[data-art="1"] { background: radial-gradient(120% 120% at 30% 20%, var(--sun-300), var(--29d-coral-500) 80%); }
c29-epview .ep-art[data-art="2"] { background: radial-gradient(120% 120% at 70% 15%, var(--sea-300), var(--sea-700) 80%); }
c29-epview .ep-art[data-art="3"] { background: radial-gradient(120% 120% at 25% 25%, var(--sun-100), var(--sun-500) 82%); }
c29-epview .ep-art img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: block; }
c29-epview .ep-title { margin-top: var(--s4);
  font: 700 24px/1.2 var(--font-display); letter-spacing: -.01em;
  color: var(--pl-tile-ink); overflow-wrap: anywhere; }
/* the show identity chip — 20px art + the show's name, one tap */
c29-epview .ep-show { display: inline-flex; align-items: center; gap: var(--s2);
  min-height: 44px; margin-top: var(--s2); padding: 0 var(--s2) 0 0;
  border: 0; background: none; cursor: pointer; font: inherit;
  color: var(--pl-tile-ink); text-align: left; }
c29-epview .ep-showthumb { position: relative; overflow: hidden; flex: none;
  width: 20px; height: 20px; border-radius: 5px;
  background: radial-gradient(120% 120% at 30% 20%, var(--sea-200), var(--sea-600) 78%); }
c29-epview .ep-showthumb[data-art="1"] { background: radial-gradient(120% 120% at 30% 20%, var(--sun-300), var(--29d-coral-500) 80%); }
c29-epview .ep-showthumb[data-art="2"] { background: radial-gradient(120% 120% at 70% 15%, var(--sea-300), var(--sea-700) 80%); }
c29-epview .ep-showthumb[data-art="3"] { background: radial-gradient(120% 120% at 25% 25%, var(--sun-100), var(--sun-500) 82%); }
c29-epview .ep-showthumb img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: block; }
c29-epview .ep-showname { font-size: 16px; font-weight: 700; overflow-wrap: anywhere; }
c29-epview .ep-meta { margin-top: var(--s1); font-size: 14px; font-weight: 550;
  color: var(--pl-tile-ink); font-variant-numeric: tabular-nums;
  overflow-wrap: anywhere; }

/* ---------- the action row: quiet ghosts left, ONE loud disc right ---------- */
c29-epview .ep-actions { display: flex; align-items: center;
  gap: var(--s2); margin-top: var(--s5); }
c29-epview .ep-act { flex: none; width: 44px; height: 44px; display: grid; place-items: center;
  border: 0; border-radius: 50%; background: transparent; cursor: pointer;
  color: var(--pl-tile-ink); box-shadow: inset 0 0 0 1px var(--edge-soft); }
c29-epview .ep-act .t-icon { width: 21px; height: 21px; }
c29-epview .ep-act:active { transform: scale(.93); }
c29-epview .ep-act[aria-pressed="true"] { background: var(--accent-soft); color: var(--accent-text);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 35%, transparent); }
c29-epview .ep-play { flex: none; margin-left: auto; width: 72px; height: 72px;
  display: grid; place-items: center; border: 0; border-radius: 50%; cursor: pointer;
  background: linear-gradient(155deg, var(--29d-coral-500), var(--money));
  color: var(--on-money); box-shadow: var(--lift); }
c29-epview .ep-play .t-icon { width: 30px; height: 30px; margin-left: 3px; }
c29-epview .ep-play:active { transform: scale(.94); }

/* ---------- the body: no cards, no boxes — space and hairlines ---------- */
c29-epview .ep-main { padding: var(--s4) var(--s5) var(--s6);
  background: var(--pl-tile-face); }
c29-epview .ep-sec { margin-top: var(--s6); }
c29-epview .ep-sec-h { font-size: 12px; font-weight: 700; letter-spacing: .09em;
  text-transform: uppercase; color: var(--pl-tile-ink-soft); margin-bottom: var(--s3); }
c29-epview .ep-empty { font-size: 14.5px; line-height: 1.55; color: var(--pl-tile-ink-soft); }

/* description + show notes — FULL, unclamped, unheaded, trusted
   verbatim (teardown point 5). There is no fold: the words are
   the content. Grade-5 Pleasant voice styles. */
c29-epview .ep-desc-in { font-size: 15.5px; line-height: 1.58; color: var(--pl-tile-ink); }
c29-epview .ep-desc-in p { overflow-wrap: anywhere; }
c29-epview .ep-desc-in p + p { margin-top: var(--s3); }
c29-epview .ep-desc-in a { color: var(--accent-text); font-weight: 600;
  text-decoration: underline; text-underline-offset: 2px; overflow-wrap: anywhere; }

/* the bridge — the episode page's only same-show navigation */
c29-epview .ep-bridge { display: flex; align-items: center; justify-content: space-between;
  gap: var(--s3); width: 100%; min-height: 52px; margin-top: var(--s6);
  padding: var(--s3) 0 0; border: 0; border-top: 1px solid var(--edge-soft);
  background: none; cursor: pointer; text-align: left;
  font: 700 16px/1.3 var(--font-display); color: var(--pl-tile-ink); }
c29-epview .ep-bridge .ep-chev { flex: none; font-size: 24px; line-height: 1;
  color: var(--accent-text); }

/* highlights — the saved moments: a position chip + the line
   itself. Real data in Listen (the highlights store; positions
   are host-supplied estimates from the script — Listen has no
   timed transcript, and the chip never pretends otherwise).
   A tap seeks the ONE player to the moment. */
c29-epview .ep-hl { display: flex; align-items: flex-start; gap: var(--s3); width: 100%;
  min-height: 52px; padding: 9px 0; border: 0; border-bottom: 1px solid var(--edge-soft);
  background: none; cursor: pointer; text-align: left; font: inherit; color: var(--pl-tile-ink); }
c29-epview .ep-hl:last-child { border-bottom: 0; }
c29-epview .ep-hl .ht { flex: none; margin-top: 1px; padding: 4px 10px; border-radius: 99px;
  background: var(--accent-soft); color: var(--accent-text);
  font-size: 13px; font-weight: 700; font-variant-numeric: tabular-nums; }
c29-epview .ep-hl .hq { flex: 1; min-width: 0; font-size: 15px; line-height: 1.45;
  overflow-wrap: anywhere; }
c29-epview .ep-hl .hm { display: block; margin-top: 2px; font-size: 12.5px;
  color: var(--pl-tile-ink-soft); }

/* chapters — DORMANT (the ruling: chapters are not faked).
   The pattern waits here, in the player's chapter language,
   for real timed data; no demo renders it and no surface
   should until the data exists. */
c29-epview .ep-chap { display: flex; align-items: center; gap: var(--s3); width: 100%;
  min-height: 52px; padding: 7px 0; border: 0; border-bottom: 1px solid var(--edge-soft);
  background: none; cursor: pointer; text-align: left; font: inherit; color: var(--pl-tile-ink); }
c29-epview .ep-chap:last-child { border-bottom: 0; }
c29-epview .ep-chap .ct { flex: none; padding: 4px 10px; border-radius: 99px;
  background: var(--accent-soft); color: var(--accent-text);
  font-size: 13px; font-weight: 700; font-variant-numeric: tabular-nums; }
c29-epview .ep-chap .cl { flex: 1; min-width: 0; font-size: 15px; font-weight: 600;
  line-height: 1.35; overflow-wrap: anywhere; }
c29-epview .ep-chap .cd { flex: none; font-size: 12.5px; color: var(--pl-tile-ink-soft);
  font-variant-numeric: tabular-nums; }
c29-epview .ep-chap.is-on .ct { background: var(--accent); color: var(--on-accent); }
c29-epview .ep-chap.is-on .cl { font-weight: 750; }

/* ---------- the list row (.ep-row) — for the show page and the
   index, where lists live (teardown section 2). Anatomy: art +
   2-line title, then a full-width 2-line description snippet,
   then date + duration, then the row's own quiet play.
   SPLIT-TAP LAW (the ruling): the row is a container, never
   one big button — .ep-rowlink (cover + title) is the
   navigation target and fires c29:open; .ep-rowplay (48px,
   quiet) is the only play control on the row. ---------- */
c29-epview .ep-row { position: relative; padding: 14px 0;
  border-bottom: 1px solid var(--edge-soft); color: var(--pl-tile-ink); }
c29-epview .ep-row:last-child { border-bottom: 0; }
c29-epview .ep-rowlink { display: flex; align-items: flex-start; gap: var(--s3);
  width: 100%; min-height: 64px; padding: 0; border: 0; background: none;
  cursor: pointer; text-align: left; font: inherit; color: inherit;
  text-decoration: none; }
/* BUILDER PF18 (R18, PF18-10): :active parity with .ep-rowplay — the
   rowlink is the row's navigation half and now answers the press
   itself (CRITIC PF18 §4: rowplay had one, rowlink didn't). The
   scale rides .ep-thumb (the link's own art), NOT the link: Tide's
   transform on the link would lift the whole row's text block. */
c29-epview .ep-rowlink:active .ep-thumb { transform: scale(.93); }
c29-epview .ep-thumb { transition: transform var(--d1) var(--ease); }
c29-epview .ep-thumb { position: relative; overflow: hidden; flex: none;
  width: 56px; height: 56px; border-radius: 10px; box-shadow: var(--hair);
  background: radial-gradient(120% 120% at 30% 20%, var(--sea-200), var(--sea-600) 78%); }
c29-epview .ep-thumb[data-art="1"] { background: radial-gradient(120% 120% at 30% 20%, var(--sun-300), var(--29d-coral-500) 80%); }
c29-epview .ep-thumb[data-art="2"] { background: radial-gradient(120% 120% at 70% 15%, var(--sea-300), var(--sea-700) 80%); }
c29-epview .ep-thumb[data-art="3"] { background: radial-gradient(120% 120% at 25% 25%, var(--sun-100), var(--sun-500) 82%); }
c29-epview .ep-thumb img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: block; }
c29-epview .ep-row .udot { flex: none; width: 9px; height: 9px; margin-top: 7px;
  border-radius: 50%; background: var(--accent); }
c29-epview .ep-row .tx { flex: 1; min-width: 0; display: grid; gap: 2px; }
c29-epview .ep-row .tt { font-size: 16px; font-weight: 650; line-height: 1.3;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
  overflow-wrap: anywhere; }
c29-epview .ep-snip { margin-top: 8px; font-size: 14px; line-height: 1.45;
  color: var(--pl-tile-ink-soft);
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
  overflow-wrap: anywhere; }
c29-epview .ep-rowfoot { display: flex; align-items: center; gap: var(--s3);
  margin-top: 10px; }
c29-epview .ep-row .tm { font-size: 13px; color: var(--pl-tile-ink-soft);
  font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
c29-epview .ep-row .tm .played { font-weight: 700; color: var(--accent-text); }
c29-epview .ep-prog { flex: 1; max-width: 140px; height: 3px; border-radius: 99px;
  background: color-mix(in srgb, var(--pl-tile-ink) 12%, transparent); overflow: hidden; }
c29-epview .ep-prog i { display: block; height: 100%; border-radius: 99px;
  background: var(--accent); width: calc(var(--p, 0) * 100%); }
c29-epview .ep-rowplay { flex: none; margin-left: auto; width: 48px; height: 48px;
  display: grid; place-items: center; border: 0; border-radius: 50%; cursor: pointer;
  font: inherit; background: var(--pl-tile-face); color: var(--pl-tile-ink);
  box-shadow: inset 0 0 0 1px var(--edge), var(--hair); }
c29-epview .ep-rowplay .t-icon { width: 19px; height: 19px; margin-left: 2px; }
c29-epview .ep-rowplay:active { transform: scale(.93); }
c29-epview .ep-row[data-state="played"] .tt { color: var(--pl-tile-ink-soft); }
c29-epview .ep-row.is-on .tt { color: var(--accent-text); }

}
/* ---------- end episode view ---------- */

/* ======================================================
   EVENT CARD  <c29-event-card> + <c29-nav-pref>
   (Upcoming pattern, 2026-10-26 — actions, not text rows:
   tap-to-copy, directions in the stored maps app, paste-in
   notes + flyers, a quiet handled flag. Squish law: prose
   keeps its floor, actions wrap BELOW, never crush.)
   ====================================================== */
@layer c29.components {
c29-event-card { display: block; min-width: 0; background: var(--surface); border-radius: calc(var(--corner, 20px) - 2px);
  box-shadow: inset 0 0 0 1px var(--edge), var(--lift); padding: var(--s4); }
c29-event-card > [data-ev] { display: none; }
c29-event-card .ev-head { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px; }
c29-event-card .ev-day { font: 700 var(--fs-sm)/1.25 var(--font-ui); letter-spacing: .05em; text-transform: uppercase; color: var(--accent-text); min-width: 0; overflow-wrap: anywhere; }
c29-event-card .ev-chip { font: 650 12px/1 var(--font-ui); padding: 5px 9px; border-radius: 999px; background: var(--wash); color: var(--ink-soft); }
c29-event-card .ev-chip.is-proposed { background: var(--warn-bg); color: var(--warn-text); }
c29-event-card .ev-chip.is-handled-chip { background: var(--ok-bg); color: var(--ok-text); margin-left: auto; }
c29-event-card .ev-when { margin-top: 2px; font-size: var(--fs-body); font-weight: 600; color: var(--ink); font-variant-numeric: tabular-nums; }
c29-event-card .ev-title { margin: 6px 0 0; font: 800 var(--fs-xl)/1.18 var(--font-display); color: var(--ink); min-width: 0; overflow-wrap: anywhere; }
c29-event-card .ev-blurb { margin: 4px 0 0; font-size: var(--fs-body); line-height: var(--lh-body); color: var(--ink-soft); min-width: 0; overflow-wrap: anywhere; }
c29-event-card .ev-where { display: flex; gap: 8px; align-items: flex-start; margin-top: 10px; color: var(--ink); }
c29-event-card .ev-where .t-icon { width: 19px; height: 19px; flex: none; margin-top: 1px; color: var(--accent); }
c29-event-card .ev-where-t { min-width: 0; display: grid; gap: 1px; }
c29-event-card .ev-venue { font-weight: 650; overflow-wrap: anywhere; }
c29-event-card .ev-addr { color: var(--ink-mute); font-size: var(--fs-sm); overflow-wrap: anywhere; }
c29-event-card .ev-sessions { list-style: none; margin: 12px 0 0; padding: 0; display: grid; gap: 6px; }
c29-event-card .ev-sessions li { display: flex; flex-wrap: wrap; gap: 4px 12px; align-items: baseline; background: var(--sunken); border-radius: var(--r-md); padding: 8px 12px; }
c29-event-card .ev-sday { font-weight: 700; flex: none; }
c29-event-card .ev-stime { color: var(--ink-soft); font-variant-numeric: tabular-nums; }
c29-event-card .ev-facts { margin-top: 12px; display: grid; }
c29-event-card .ev-fact { display: flex; align-items: center; gap: 10px; min-height: 44px; padding: 5px 0; box-shadow: inset 0 -1px 0 var(--edge-soft); min-width: 0; }
c29-event-card .ev-fact:last-child { box-shadow: none; }
c29-event-card .ev-flabel { flex: 0 0 auto; max-width: 45%; font-size: var(--fs-sm); font-weight: 650; color: var(--ink-mute); overflow-wrap: normal; word-break: normal; hyphens: none; }
c29-event-card .ev-fval { flex: 1 1 auto; min-width: 0; font-weight: 600; color: var(--ink); overflow-wrap: anywhere; }
/* the copy chip — c29-copy's 44px truth, card-sized */
c29-event-card .ev-copybtn { position: relative; width: 44px; height: 44px; flex: none; margin-left: auto; border: 0; border-radius: 12px;
  background: var(--raised); box-shadow: var(--hair); cursor: pointer; display: grid; place-items: center; color: var(--ink-soft); }
c29-event-card .ev-copybtn svg { position: absolute; width: 18px; height: 18px; transition: opacity var(--d2) var(--ease), transform var(--d2) var(--ease); }
c29-event-card .ev-copybtn .pg-b { transform: translate(2px, -2px); }
c29-event-card .ev-copybtn .ok { opacity: 0; color: var(--accent); }
c29-event-card .ev-copybtn.is-copied .pg-b, c29-event-card .ev-copybtn.is-copied .pg-f { opacity: 0; }
c29-event-card .ev-copybtn.is-copied .ok { opacity: 1; }
c29-event-card .ev-notes { display: grid; gap: 8px; }
c29-event-card .ev-notes:not(:empty) { margin-top: 12px; }
c29-event-card .ev-note { position: relative; background: var(--wash); border-radius: var(--r-md); padding: 10px 44px 10px 12px; }
c29-event-card .ev-note p { margin: 0; color: var(--ink); overflow-wrap: anywhere; min-width: 0; }
c29-event-card .ev-flyerwrap { position: relative; margin: 0; }
c29-event-card .ev-flyer { display: block; width: 100%; height: auto; border-radius: var(--r-md); box-shadow: inset 0 0 0 1px var(--edge); }
c29-event-card .ev-notex { position: absolute; top: 6px; right: 6px; width: 44px; height: 44px; border: 0; border-radius: 50%;
  background: var(--surface); color: var(--ink-soft); box-shadow: var(--hair); cursor: pointer; font-size: 14px; display: grid; place-items: center; }
c29-event-card .ev-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 14px; }
c29-event-card .ev-act { min-height: 44px; padding: 0 15px; border-radius: 999px; display: inline-flex; align-items: center; gap: 7px;
  font: 650 14.5px/1 var(--font-ui); text-decoration: none; flex: 0 1 auto; min-width: 0; }
c29-event-card .ev-act .t-icon { width: 18px; height: 18px; flex: none; }
c29-event-card .ev-act span { min-width: 0; overflow-wrap: anywhere; }
c29-event-card .ev-handled[aria-pressed="true"] { background: var(--ok-bg); color: var(--ok-text); --b-bg: var(--ok-bg); --b-ink: var(--ok-text); }
c29-event-card[data-handled="true"] .ev-title, c29-event-card[data-handled="true"] .ev-blurb { opacity: .62; }
c29-event-card .ev-chooser { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; flex-basis: 100%; background: var(--sunken);
  border-radius: var(--r-md); padding: 10px 12px; }
c29-event-card .ev-chooser[hidden] { display: none; }
c29-event-card .ev-chooser-q { font-weight: 650; color: var(--ink-soft); }
c29-event-card .ev-pick { min-height: 44px; padding: 0 14px; border-radius: 999px; }
c29-event-card .ev-editor { margin-top: 12px; background: var(--sunken); border-radius: var(--r-md); padding: 12px; display: grid; gap: 10px; }
c29-event-card .ev-editor[hidden] { display: none; }
c29-event-card .ev-edlabel { font-weight: 650; font-size: var(--fs-sm); color: var(--ink-soft); }
c29-event-card .ev-text { width: 100%; box-sizing: border-box; border: 0; border-radius: var(--r-md); background: var(--surface);
  box-shadow: inset 0 0 0 1px var(--edge); padding: 10px 12px; font: inherit; color: var(--ink); resize: vertical; }
c29-event-card .ev-text:focus-visible { box-shadow: var(--focus, 0 0 0 2px var(--focus-ring)); outline: none; }
c29-event-card .ev-drop { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; border: 1.5px dashed var(--sand-400);
  border-radius: var(--r-md); padding: 10px 12px; color: var(--ink-mute); font-size: var(--fs-sm); }
c29-event-card .ev-drop.is-over { border-color: var(--accent); color: var(--ink); background: var(--accent-soft); }
c29-event-card .ev-filebtn { min-height: 44px; padding: 0 14px; border-radius: 999px; display: inline-flex; align-items: center; cursor: pointer; }
c29-event-card .ev-pending { display: flex; align-items: center; gap: 10px; }
c29-event-card .ev-pending[hidden] { display: none; }
c29-event-card .ev-pending img { width: 84px; height: auto; border-radius: 8px; box-shadow: inset 0 0 0 1px var(--edge); }
c29-event-card .ev-edrow { display: flex; flex-wrap: wrap; gap: 8px; }
c29-event-card .ev-save, c29-event-card .ev-cancel { min-height: 44px; padding: 0 16px; border-radius: 999px; }
c29-event-card .ev-seam { margin: 0; font-size: var(--fs-cap, 12.5px); color: var(--ink-mute); }
/* the preference control — one per surface, remembered everywhere */
c29-nav-pref { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; min-width: 0; }
c29-nav-pref .np-label { font: 650 var(--fs-sm)/1.2 var(--font-ui); color: var(--ink-mute); }
c29-nav-pref .np-opts { display: inline-flex; flex-wrap: wrap; gap: 4px; background: var(--sunken); border-radius: 999px; padding: 4px; }
c29-nav-pref .np-opt { min-height: 44px; padding: 0 13px; border: 0; border-radius: 999px; background: transparent; color: var(--ink-soft);
  font: 650 13.5px/1 var(--font-ui); cursor: pointer; }
c29-nav-pref .np-opt[aria-pressed="true"] { background: var(--surface); color: var(--ink); box-shadow: var(--hair); }
c29-nav-pref.is-unset .np-opt { color: var(--ink); }
}
/* ---------- end event card ---------- */

/* ================================================================
 * APP FRAME — c29-appshell (APPS 800 Round 4, 2026-10-26)
 * CSS for the live app frame (JS: tide.js, APP FRAME block).
 * The title block sits UNDER the furniture row at full width —
 * layout="bleed" (Round 35): host locks to 100dvh with no page
 * scroll and main is uncapped - the editor root inside owns layout.
 * the Squish Law by construction (SQ-1: prose keeps its floor;
 * furniture wraps on its own row, never beside the prose).
 * ================================================================ */
@layer c29.components {
c29-appshell { display: flex; flex-direction: column; min-height: 100dvh; min-width: 0; background: var(--bg); color: var(--ink); }
c29-appshell .af-head { flex: none; padding: calc(var(--s3) + var(--safe-t, 0px)) var(--s4) var(--s4); display: grid; gap: var(--s3); }
c29-appshell .af-top { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 10px; min-width: 0; }
c29-appshell .af-home { display: inline-flex; align-items: center; gap: 5px; min-height: 44px; padding: 0 14px 0 10px; border-radius: 999px;
  background: var(--raised); box-shadow: var(--hair); color: var(--ink); text-decoration: none; font: 650 14px/1 var(--font-ui); flex: 0 1 auto; min-width: 0; }
c29-appshell .af-home span { min-width: 0; overflow-wrap: anywhere; }
c29-appshell .af-home .t-icon { width: 18px; height: 18px; flex: none; }
c29-appshell .af-home:active { transform: scale(.98); }
c29-appshell .af-actions { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 8px; flex: 0 1 auto; min-width: 0; margin-left: auto; }
c29-appshell .af-actions:empty { display: none; }
c29-appshell .af-theme { flex: none; margin-left: auto; display: inline-flex; }
c29-appshell .af-actions:not(:empty) + .af-theme { margin-left: 0; }
c29-appshell .af-titleblock { min-width: 0; }
c29-appshell .af-kicker { margin: 0; font-size: var(--fs-cap); letter-spacing: var(--track-cap, .08em); text-transform: uppercase; font-weight: 650; color: var(--ink-mute); overflow-wrap: anywhere; }
c29-appshell .af-title { margin: 4px 0 0; font: 800 var(--fs-2xl)/1.15 var(--font-display); color: var(--ink); min-width: 0; overflow-wrap: anywhere; text-wrap: balance; }
c29-appshell .af-sub { margin: 6px 0 0; font-size: var(--fs-body); line-height: var(--lh-body); color: var(--ink-soft); min-width: 0; overflow-wrap: anywhere; max-width: 68ch; }
c29-appshell .af-main { flex: 1 1 auto; min-height: 0; min-width: 0; width: 100%; max-width: 560px; margin: 0 auto; padding: 0 var(--s4) var(--s6); display: block; }
c29-appshell .af-main.is-wide { max-width: 1120px; }
c29-appshell.is-bleed { height: 100dvh; overflow: hidden; }
c29-appshell .af-main.is-bleed { max-width: none; margin: 0; padding: 0; flex: 1 1 auto; min-height: 0; overflow: hidden; }
c29-appshell .af-foot { flex: none; display: flex; flex-wrap: wrap; align-items: center; gap: 8px 10px; min-width: 0;
  padding: var(--s3) var(--s4) calc(var(--s3) + var(--safe-b, 0px)); background: var(--surface); box-shadow: inset 0 1px 0 var(--edge-soft); }
c29-appshell .af-foot > * { flex: 0 1 auto; min-width: 0; }
}
/* ---------- end app frame (c29-appshell) ---------- */

/* ================================================================
 * ASSET ADD — c29-assetadd (APPS 800 Round 5, 2026-10-26)
 * CSS for the asset-add pattern (JS: tide.js, ASSET ADD block).
 * The row defends the filename: the name column flexes with a
 * 45% basis and the remove furniture is shrinkable and wraps
 * below the name when both cannot fit (Squish Law SQ-1..SQ-3).
 * Tokens only, both themes; every target at least 44px.
 * ================================================================ */
@layer c29.components {
c29-assetadd { display: block; position: relative; min-width: 0; color: var(--ink); }
c29-assetadd .aa-zone { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px; min-height: 44px; box-sizing: border-box;
  padding: var(--s3); border: 1.5px dashed var(--edge); border-radius: var(--r-md); background: var(--sunken); cursor: pointer; min-width: 0; }
c29-assetadd .aa-zone:focus-visible { box-shadow: var(--focus, 0 0 0 2px var(--focus-ring)); outline: none; }
c29-assetadd .aa-zone.aa-over { border-color: var(--accent); background: var(--accent-soft); color: var(--ink); }
c29-assetadd .aa-label { font: 650 var(--fs-body)/1.3 var(--font-ui); color: var(--ink); flex: 0 1 auto; min-width: 0; overflow-wrap: anywhere; }
c29-assetadd .aa-hint { font-size: var(--fs-sm); line-height: 1.45; color: var(--ink-soft); flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; }
c29-assetadd .aa-empty { font-size: var(--fs-sm); line-height: 1.45; color: var(--ink-mute); flex: 1 1 100%; min-width: 0; overflow-wrap: anywhere; }
c29-assetadd .aa-empty[hidden] { display: none; }
c29-assetadd .aa-input { display: none; }
c29-assetadd .aa-list { list-style: none; margin: var(--s3) 0 0; padding: 0; display: grid; gap: 8px; }
c29-assetadd .aa-row { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 10px; min-width: 0;
  background: var(--surface); border-radius: var(--r-md); box-shadow: var(--hair); padding: 8px 10px; }
c29-assetadd .aa-thumb { width: 40px; height: 40px; object-fit: cover; border-radius: var(--r-sm); background: var(--sunken); flex: none; }
c29-assetadd .aa-main { flex: 1 1 45%; min-width: 0; display: grid; gap: 2px; }
c29-assetadd .aa-name { min-width: 0; overflow-wrap: anywhere; font: 600 var(--fs-sm)/1.35 var(--font-ui); color: var(--ink); }
c29-assetadd .aa-meta { min-width: 0; overflow-wrap: anywhere; font-size: var(--fs-cap); color: var(--ink-mute); }
c29-assetadd .aa-remove { flex: 0 1 auto; min-width: 44px; min-height: 44px; padding: 0 12px; border: 0; border-radius: 999px;
  background: var(--raised); box-shadow: var(--hair); color: var(--ink); font: 650 13.5px/1 var(--font-ui); cursor: pointer; }
c29-assetadd .aa-remove:focus-visible { box-shadow: var(--focus, 0 0 0 2px var(--focus-ring)); outline: none; }
c29-assetadd .aa-remove:active { transform: scale(.98); }
c29-assetadd .aa-remove:disabled { opacity: .5; cursor: default; }
c29-assetadd .aa-live { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
c29-assetadd[disabled] .aa-zone { opacity: .55; cursor: default; }
}
/* ---------- end asset add (c29-assetadd) ---------- */

/* ================================================================
   DAY BOOK: c29-day-entry + c29-ruling + c29-day-feedback
   (Day Book commission, 2026-10-26, JS: tide.js, DAY BOOK block.)
   The prose-row grammar lives here as classes: .c29-prose-row
   holds one prose column plus side furniture; .c29-prose-main
   is defended (flex floor 45%, never under 160px) and
   .c29-prose-side is shrinkable furniture (flex 0 1 auto,
   min-width 0) that wraps BELOW the prose when the row cannot
   fit both, Squish Law SQ-1..SQ-3 by construction. Text
   children carry min-width 0; unbreakable tokens wrap inside
   the defended column with overflow-wrap:anywhere;
   word-break:break-all is banned. Tokens only, both themes;
   every target at least 44px.
   ================================================================ */
@layer c29.components {
.c29-prose-row { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 8px 12px; min-width: 0; }
.c29-prose-main { flex: 1 1 45%; min-width: 160px; max-width: 100%; }
.c29-prose-side { flex: 0 1 auto; min-width: 0; max-width: 100%; display: flex; flex-wrap: wrap; align-items: center; gap: 6px 8px; }

c29-day-entry { display: block; min-width: 0; background: var(--surface); border-radius: calc(var(--corner, 20px) - 2px);
  box-shadow: inset 0 0 0 1px var(--edge), var(--lift); padding: var(--s4); color: var(--ink); }
c29-day-entry .db-title { margin: 0; font: 750 var(--fs-lg)/1.25 var(--font-display); color: var(--ink); min-width: 0; overflow-wrap: anywhere; }
c29-day-entry .db-titlelink { color: inherit; text-decoration: none; overflow-wrap: anywhere; }
c29-day-entry .db-titlelink:hover { text-decoration: underline; text-underline-offset: 3px; }
c29-day-entry .db-titlelink:focus-visible { box-shadow: var(--focus, 0 0 0 2px var(--focus-ring)); outline: none; border-radius: 6px; }
c29-day-entry .db-when { margin: 3px 0 0; font-size: var(--fs-sm); font-weight: 600; color: var(--ink-mute); min-width: 0; overflow-wrap: anywhere; font-variant-numeric: tabular-nums; }
c29-day-entry .db-summary { margin-top: 6px; font-size: var(--fs-body); line-height: var(--lh-body); color: var(--ink-soft); min-width: 0; overflow-wrap: anywhere; }
c29-day-entry .db-summary > :first-child { margin-top: 0; }
c29-day-entry .db-summary > :last-child { margin-bottom: 0; }
c29-day-entry .db-proof { font: 650 12px/1 var(--font-ui); padding: 5px 9px; border-radius: 999px; background: var(--wash); color: var(--ink-soft);
  flex: 0 1 auto; min-width: 0; overflow-wrap: anywhere; }
c29-day-entry .db-proof.is-probed { background: var(--ok-bg); color: var(--ok-text); }
c29-day-entry .db-proof.is-staged { background: var(--warn-bg); color: var(--warn-text); }
c29-day-entry .db-proof.is-unproven { background: var(--wash); color: var(--ink-soft); }
c29-day-entry .db-proofnote { font-size: var(--fs-cap); color: var(--ink-mute); flex: 0 1 auto; min-width: 0; overflow-wrap: anywhere; }
c29-day-entry .db-feedback { margin-top: 12px; padding-top: 12px; box-shadow: inset 0 1px 0 var(--edge-soft); }
c29-day-entry .db-feedback[hidden] { display: none; }

c29-ruling { display: block; min-width: 0; background: var(--surface); border-radius: calc(var(--corner, 20px) - 2px);
  box-shadow: inset 0 0 0 1px var(--edge), var(--lift); padding: var(--s4); color: var(--ink); }
c29-ruling .db-rtop h3, c29-ruling .db-rtop [data-title] { margin: 0; font: 750 var(--fs-lg)/1.25 var(--font-display); color: var(--ink); min-width: 0; overflow-wrap: anywhere; }
c29-ruling .db-rsummary { margin-top: 6px; font-size: var(--fs-body); line-height: var(--lh-body); color: var(--ink-soft); min-width: 0; overflow-wrap: anywhere; }
c29-ruling .db-rsummary > :first-child { margin-top: 0; }
c29-ruling .db-rsummary > :last-child { margin-bottom: 0; }
c29-ruling .db-rrec { margin-top: 10px; background: var(--sunken); border-radius: var(--r-md); padding: 10px 12px; display: grid; gap: 4px; min-width: 0; }
c29-ruling .db-rrec[hidden] { display: none; }
c29-ruling .db-rreclabel { font: 650 var(--fs-sm)/1.3 var(--font-ui); color: var(--ink); min-width: 0; overflow-wrap: anywhere; }
c29-ruling .db-rrecbody { font-size: var(--fs-body); line-height: var(--lh-body); color: var(--ink-soft); min-width: 0; overflow-wrap: anywhere; }
c29-ruling .db-rrecbody > :first-child { margin-top: 0; }
c29-ruling .db-rrecbody > :last-child { margin-bottom: 0; }
c29-ruling .db-ractions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 14px; }
c29-ruling .db-btn, c29-day-feedback .db-btn { min-height: 44px; padding: 0 16px; border-radius: 999px;
  font: 650 14.5px/1 var(--font-ui); flex: 0 1 auto; min-width: 0; }
c29-ruling .db-btn span, c29-day-feedback .db-btn span { min-width: 0; overflow-wrap: anywhere; }
c29-ruling .db-btn:disabled, c29-day-feedback .db-btn:disabled { opacity: .5; cursor: default; }
c29-ruling .db-ceditor { margin-top: 12px; background: var(--sunken); border-radius: var(--r-md); padding: 12px; display: grid; gap: 10px; min-width: 0; }
c29-ruling .db-ceditor[hidden] { display: none; }
c29-ruling .db-clabel { display: grid; gap: 8px; font-weight: 650; font-size: var(--fs-sm); color: var(--ink-soft); min-width: 0; }
c29-ruling .db-ctext, c29-day-feedback .db-fbtext { width: 100%; box-sizing: border-box; border: 0; border-radius: var(--r-md); background: var(--surface);
  box-shadow: inset 0 0 0 1px var(--edge); padding: 10px 12px; font: inherit; color: var(--ink); resize: vertical; min-width: 0; overflow-wrap: anywhere; }
c29-ruling .db-ctext:focus-visible, c29-day-feedback .db-fbtext:focus-visible { box-shadow: var(--focus, 0 0 0 2px var(--focus-ring)); outline: none; }
c29-ruling .db-cedrow { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; min-width: 0; }
c29-ruling .db-chint { font-size: var(--fs-sm); line-height: 1.45; color: var(--ink-mute); flex: 1 1 45%; min-width: 160px; overflow-wrap: anywhere; }
c29-ruling .db-status, c29-day-feedback .db-fbstatus { margin: 10px 0 0; font-size: var(--fs-sm); line-height: 1.45; font-weight: 600;
  color: var(--ink-soft); min-width: 0; overflow-wrap: anywhere; }
c29-ruling .db-status:empty, c29-day-feedback .db-fbstatus:empty { margin: 0; }
c29-ruling .db-status.is-ok, c29-day-feedback .db-fbstatus.is-ok { color: var(--ok-text); }
c29-ruling .db-status.is-bad, c29-day-feedback .db-fbstatus.is-bad { color: var(--bad-text); }

c29-day-feedback { display: block; min-width: 0; color: var(--ink); }
c29-day-feedback .db-fblabel { display: block; font: 650 var(--fs-sm)/1.3 var(--font-ui); color: var(--ink); min-width: 0; overflow-wrap: anywhere; }
c29-day-feedback .db-verdicts { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
c29-day-feedback .db-vchip { min-height: 44px; padding: 0 14px; border: 0; border-radius: 999px; background: var(--wash); color: var(--ink-soft);
  font: 650 13.5px/1 var(--font-ui); cursor: pointer; flex: 0 1 auto; min-width: 0; }
c29-day-feedback .db-vchip:focus-visible { box-shadow: var(--focus, 0 0 0 2px var(--focus-ring)); outline: none; }
c29-day-feedback .db-vchip[aria-pressed="true"] { background: var(--accent-soft); color: var(--accent-text); box-shadow: inset 0 0 0 1.5px var(--accent); }
c29-day-feedback .db-fbtext { margin-top: 10px; }
c29-day-feedback .db-fbrow { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; margin-top: 10px; min-width: 0; }
c29-day-feedback .db-fbcount { font-size: var(--fs-cap); color: var(--ink-mute); font-variant-numeric: tabular-nums; flex: 0 1 auto; min-width: 0; }
c29-day-feedback .db-fbcount.is-over { color: var(--bad-text); font-weight: 650; }
c29-day-feedback .db-fbsend { margin-left: auto; }
}
/* ---------- end day book ---------- */

/* ================================================================
 * PRINT SHEET — c29-printsheet (APPS 800 Round 6, 2026-10-26)
 * CSS for the print sheet (JS: tide.js, PRINT SHEET block).
 * Prose is defended by construction: the what line and the
 * estimate line flex with a 45% floor and the furniture
 * (amounts, buttons) is shrinkable and wraps below the prose
 * when both cannot fit (Squish Law SQ-1..SQ-3). Tokens only,
 * both themes; every target at least 44px.
 * ================================================================ */
@layer c29.components {
c29-printsheet { display: block; position: relative; flex: 1 1 100%; width: 100%; min-width: 0; color: var(--ink); }
c29-printsheet .ps-root { display: grid; gap: var(--s4); min-width: 0; background: var(--surface); border-radius: var(--r-md); box-shadow: var(--hair); padding: var(--s4); }
c29-printsheet .ps-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px 10px; min-width: 0; }
c29-printsheet .ps-title { font: 700 var(--fs-lg)/1.3 var(--font-display); color: var(--ink); flex: 0 1 auto; min-width: 0; overflow-wrap: anywhere; }
c29-printsheet .ps-hint { font-size: var(--fs-sm); line-height: 1.45; color: var(--ink-soft); flex: 1 1 45%; min-width: 0; overflow-wrap: anywhere; }
c29-printsheet .ps-sec { display: grid; gap: 8px; min-width: 0; }
c29-printsheet .ps-seclabel { font: 650 var(--fs-cap)/1.3 var(--font-ui); letter-spacing: var(--track-cap, .08em); text-transform: uppercase; color: var(--ink-mute); min-width: 0; overflow-wrap: anywhere; }
c29-printsheet .ps-whatrow { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 10px; min-width: 0; }
c29-printsheet .ps-whatmain { flex: 1 1 45%; min-width: 0; overflow-wrap: anywhere; font-size: var(--fs-body); line-height: var(--lh-body); color: var(--ink); }
c29-printsheet .ps-whatline { margin: 0; font-size: var(--fs-sm); line-height: 1.45; color: var(--ink-soft); min-width: 0; overflow-wrap: anywhere; }
c29-printsheet .ps-items { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
c29-printsheet .ps-items:empty { display: none; }
c29-printsheet .ps-itemrow { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 10px; min-width: 0;
  background: var(--sunken); border-radius: var(--r-md); padding: 8px 10px; }
c29-printsheet .ps-itemmain { flex: 1 1 45%; min-width: 0; display: grid; gap: 2px; }
c29-printsheet .ps-itemname { min-width: 0; overflow-wrap: anywhere; font: 600 var(--fs-sm)/1.35 var(--font-ui); color: var(--ink); }
c29-printsheet .ps-itemmeta { flex: 0 1 auto; min-width: 0; overflow-wrap: anywhere; font-size: var(--fs-cap); color: var(--ink-mute); }
c29-printsheet .ps-kinds { display: grid; gap: 8px; min-width: 0; }
c29-printsheet .ps-kind { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 10px; min-height: 44px; box-sizing: border-box;
  background: var(--sunken); border-radius: var(--r-md); padding: 8px 12px; cursor: pointer; min-width: 0; }
c29-printsheet .ps-kindlabel { flex: 1 1 45%; min-width: 0; overflow-wrap: anywhere; font: 650 var(--fs-sm)/1.35 var(--font-ui); color: var(--ink); }
c29-printsheet .ps-kindinput { width: 20px; height: 20px; flex: none; accent-color: var(--accent); }
c29-printsheet .ps-kindinput:focus-visible { box-shadow: var(--focus, 0 0 0 2px var(--focus-ring)); outline: none; }
c29-printsheet .ps-field { display: grid; gap: 4px; min-width: 0; }
c29-printsheet .ps-flabel { font-size: var(--fs-sm); font-weight: 650; color: var(--ink-soft); min-width: 0; overflow-wrap: anywhere; }
c29-printsheet .ps-input, c29-printsheet .ps-qty, c29-printsheet .ps-select { width: 100%; box-sizing: border-box; min-height: 44px; border: 0; border-radius: var(--r-md);
  background: var(--sunken); box-shadow: inset 0 0 0 1px var(--edge); padding: 10px 12px; font: inherit; color: var(--ink); min-width: 0; }
c29-printsheet .ps-input:focus-visible, c29-printsheet .ps-qty:focus-visible, c29-printsheet .ps-select:focus-visible { box-shadow: var(--focus, 0 0 0 2px var(--focus-ring)); outline: none; }
c29-printsheet .ps-savednote { margin: 0; font-size: var(--fs-sm); line-height: 1.45; color: var(--ink-mute); min-width: 0; overflow-wrap: anywhere; }
c29-printsheet .ps-savednote:empty { display: none; }
c29-printsheet .ps-btnrow { display: flex; flex-wrap: wrap; gap: 8px 10px; min-width: 0; }
c29-printsheet .ps-btn { flex: 0 1 auto; min-width: 44px; min-height: 44px; padding: 0 14px; border: 0; border-radius: 999px;
  background: var(--raised); box-shadow: var(--hair); color: var(--ink); font: 650 13.5px/1 var(--font-ui); cursor: pointer; }
c29-printsheet .ps-btn:focus-visible { box-shadow: var(--focus, 0 0 0 2px var(--focus-ring)); outline: none; }
c29-printsheet .ps-btn:active { transform: scale(.98); }
c29-printsheet .ps-btn:disabled { opacity: .5; cursor: default; }
c29-printsheet .ps-build { background: var(--accent); color: var(--on-accent); }
c29-printsheet .ps-estmain { margin: 0; flex: 1 1 45%; min-width: 0; overflow-wrap: anywhere; font-size: var(--fs-body); line-height: var(--lh-body); font-weight: 650; color: var(--ink); }
c29-printsheet .ps-estlines { display: grid; gap: 4px; min-width: 0; }
c29-printsheet .ps-estrow { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 10px; min-width: 0; }
c29-printsheet .ps-estlabel { flex: 1 1 45%; min-width: 0; overflow-wrap: anywhere; font-size: var(--fs-sm); line-height: 1.45; color: var(--ink-soft); }
c29-printsheet .ps-estval { flex: 0 1 auto; min-width: 0; font-size: var(--fs-sm); font-weight: 650; color: var(--ink); font-variant-numeric: tabular-nums; }
c29-printsheet .ps-estnote { margin: 0; font-size: var(--fs-sm); line-height: 1.45; color: var(--ink-mute); min-width: 0; overflow-wrap: anywhere; }
c29-printsheet .ps-reasons { margin: 0; padding-left: 18px; display: grid; gap: 2px; font-size: var(--fs-sm); line-height: 1.45; color: var(--ink-soft); min-width: 0; }
c29-printsheet .ps-reasons:empty { display: none; }
c29-printsheet .ps-reasons li { min-width: 0; overflow-wrap: anywhere; }
c29-printsheet .ps-ready { background: var(--sunken); border-radius: var(--r-md); padding: 12px 14px; display: grid; gap: 6px; min-width: 0; }
c29-printsheet .ps-ready[hidden] { display: none; }
c29-printsheet .ps-readytext { margin: 0; flex: 1 1 45%; min-width: 0; overflow-wrap: anywhere; font-size: var(--fs-body); line-height: var(--lh-body); font-weight: 650; color: var(--ink); }
c29-printsheet .ps-notplaced { margin: 0; font-size: var(--fs-sm); line-height: 1.45; color: var(--ink-soft); min-width: 0; overflow-wrap: anywhere; }
c29-printsheet .ps-live { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
c29-printsheet[disabled] .ps-root { opacity: .75; }
}
/* ---------- end print sheet (c29-printsheet) ---------- */

/* ================================================================
 * EDGE SHEET — c29-sheet (TikTok Pattern Library Round 7,
 * 2026-10-27; JS: tide.js, EDGE SHEET block. Source spec:
 * Round 1 teardown Revision 2, corpus frames 02 + 08.)
 * Geometry of record: at the partial detent the panel's top
 * edge sits at 33% of the viewport height (panel height 67dvh);
 * the full detent leaves a 48px reveal of the content above.
 * Top corners only, radius 12px (the teardown's ≈12pt) — the
 * sheet does NOT ride --corner: the corpus radius is its own
 * measured value, and the bottom corners are square because
 * the panel is anchored to the screen's bottom edge. No
 * grabber is painted, ever. The close control is a bare ×
 * glyph — transparent ground, no circle, no chip.
 * The head row obeys the Squish Law by construction: the
 * header slot's column flexes at a 45% floor with a 160px
 * minimum (.sh-headmain), and .sh-head carries flex-wrap —
 * Squish-Law compliance comes from that .sh-head flex-wrap
 * plus .sh-headmain's 45%/160px floor, so the close control
 * wraps BELOW the header when the row cannot fit both. The
 * close control itself is flex: 0 1 auto with min-width: 44px
 * (the deliberate 44px touch target — it is NOT min-width: 0).
 * Body prose carries
 * min-width 0 and overflow-wrap:anywhere; word-break:break-all
 * is banned. Tokens only, both themes; the close target is
 * 44px. Entry is a translate-up over calc(350ms / --speed)
 * on --ease (the house ease-out); detent changes ride
 * --ease-sheet on --d3; under the motion gate both collapse.
 * ================================================================ */
@layer c29.components {
c29-sheet { display: block; min-width: 0; color: var(--ink); }
/* JS-dead: no chrome is built and the element never leaves the
   flow — the host's header, body and composer text stay
   readable in place, stacked in document order. */
c29-sheet:not([data-sheet-built]) { background: var(--raised); border-radius: 12px 12px 0 0;
  box-shadow: inset 0 0 0 1px var(--edge-soft); padding: var(--s4); }
c29-sheet[data-sheet-built] { position: fixed; inset: 0; z-index: 90; pointer-events: none; }
c29-sheet[data-sheet-built][open] { pointer-events: auto; }
c29-sheet .sh-scrim { position: absolute; inset: 0; background: transparent; opacity: 1;
  transition: opacity var(--d3) var(--ease); }
/* dim is opt-in: the comments case in the corpus is undimmed.
   Declared, the scrim darkens to the corpus's sampled modal
   range (≈46% black). */
c29-sheet[dim] .sh-scrim { background: color-mix(in srgb, var(--scrim-ink) 46%, transparent); }
c29-sheet[data-sheet-built]:not([open]) .sh-scrim { opacity: 0; }
c29-sheet .sh-panel { position: absolute; left: 0; right: 0; bottom: 0; margin: 0 auto;
  width: min(560px, 100%); max-width: 100%; height: 67dvh; max-height: calc(100dvh - var(--safe-t));
  display: flex; flex-direction: column; min-width: 0;
  background: var(--raised); color: var(--ink);
  border-radius: 12px 12px 0 0; box-shadow: var(--lift-hi);
  transform: translateY(0); visibility: visible;
  transition: transform calc(350ms / var(--speed)) var(--ease), height var(--d3) var(--ease-sheet), visibility 0s; }
c29-sheet[detent="full"] .sh-panel { height: calc(100dvh - 48px); }
c29-sheet[data-sheet-built]:not([open]) .sh-panel { transform: translateY(103%); visibility: hidden;
  transition: transform calc(350ms / var(--speed)) var(--ease), height var(--d3) var(--ease-sheet), visibility 0s calc(350ms / var(--speed)); }
c29-sheet .sh-head { flex: none; display: flex; flex-wrap: wrap; align-items: flex-start; gap: 4px 8px;
  min-width: 0; padding: var(--s4) var(--s4) var(--s3); box-shadow: inset 0 -1px 0 var(--edge-soft); }
c29-sheet .sh-headmain { flex: 1 1 45%; min-width: min(160px, 100%); max-width: 100%; overflow-wrap: anywhere; }
c29-sheet .sh-headmain > :first-child { margin-top: 0; }
c29-sheet .sh-headmain > :last-child { margin-bottom: 0; }
c29-sheet .sh-close { flex: 0 1 auto; min-width: 44px; width: 44px; height: 44px; margin-left: auto;
  border: 0; border-radius: 12px; background: transparent; color: var(--ink);
  font: 400 16px/1 var(--font-ui); cursor: pointer; display: grid; place-items: center; padding: 0 0 4px; }
c29-sheet .sh-close:hover { background: var(--wash); }
c29-sheet .sh-close:focus-visible { box-shadow: var(--focus, 0 0 0 2px var(--focus-ring)); outline: none; }
c29-sheet .sh-close:active { transform: scale(.96); }
c29-sheet .sh-body { flex: 1 1 auto; min-height: 0; min-width: 0; overflow-y: auto; overflow-x: hidden;
  overscroll-behavior: contain; padding: var(--s3) var(--s4) var(--s4); }
c29-sheet .sh-body > :first-child { margin-top: 0; }
c29-sheet .sh-body > :last-child { margin-bottom: 0; }
c29-sheet .sh-foot { flex: none; min-width: 0; background: var(--raised);
  padding: var(--s3) var(--s4) calc(var(--s3) + var(--safe-b)); box-shadow: inset 0 1px 0 var(--edge-soft); }
c29-sheet .sh-foot[hidden] { display: none; }
html[data-motion="reduced"] c29-sheet .sh-panel,
html[data-motion="reduced"] c29-sheet .sh-scrim { transition: none; }
}
/* ---------- end edge sheet (c29-sheet) ---------- */

/* ================================================================
 * SHEET STACK — c29-sheet-stack (TikTok Pattern Library Round 8,
 * 2026-10-27; JS: tide.js, SHEET STACK block. Source spec:
 * Round 1 teardown Revision 2, Part C — corpus frame 08.)
 * The coordinator's own paint only: wrapper, veil and the two
 * layer surfaces. The base sheet keeps c29-sheet's CSS — the
 * one stack rule that touches it reads the --stk-base-bright
 * custom property the coordinator sets on the sheet element
 * while the stack is non-empty (progressive dimming of the
 * base, one step per layer above it); no sheet rule is copied
 * or forked here.
 * Layer kinds, from the corpus: CARD — a floating card, insets
 * 8px at the sides, radius 20px (the --corner token's value,
 * the teardown's ≈20pt), anchored to the top of the viewport,
 * no close × and no grabber (dismissal is the stack's pop);
 * PANEL — an inset grey panel on --sunken (house design for
 * its exact inset: the frame proves a distinct grey panel
 * between card and sheet, not its measurements). A covered
 * layer is dimmed one step per layer above it: the surface's
 * brightness filter reads --stk-bright (the step's resulting
 * brightness, set per layer by the coordinator alongside
 * --stk-dim — a direct var, never a calc inside brightness(),
 * which the house WebKit rig cannot compute) and the
 * component-built veil paints black at the same step. Entry is a calm translate-up + fade over
 * calc(350ms / --speed) on --ease (the teardown's INF rebuild
 * spec, ≈350ms); under the motion gate the entry animation
 * and every transition here are removed outright.
 * Prose in layer content keeps the Squish Law by construction
 * in the rig's host markup; the surfaces here carry
 * min-width 0 and overflow-wrap:anywhere, and
 * word-break:break-all appears nowhere in this section.
 * ================================================================ */
@layer c29.components {
c29-sheet-stack { display: block; min-width: 0; color: var(--ink); }
/* JS-dead: no wrappers are built and the element stays in
   flow — each declared layer's content stays readable in
   place, in document order, as a plain block. */
c29-sheet-stack:not([data-stack-built]) > * { background: var(--surface); border-radius: var(--r-md);
  box-shadow: inset 0 0 0 1px var(--edge-soft); padding: var(--s4); margin: 0 0 var(--s3); }
c29-sheet-stack[data-stack-built] { position: fixed; inset: 0; z-index: 110; pointer-events: none; }
c29-sheet-stack .stk-layer { position: absolute; inset: 0; pointer-events: none; overflow: hidden; }
c29-sheet-stack .stk-layer[data-state="hidden"] { visibility: hidden; }
c29-sheet-stack .stk-layer[data-state="top"] { pointer-events: auto; visibility: visible; }
c29-sheet-stack .stk-layer[data-state="covered"] { visibility: visible; }
c29-sheet-stack .stk-surface { position: absolute; min-width: 0; max-width: 100%;
  box-sizing: border-box;
  overflow-wrap: anywhere; color: var(--ink);
  transition: filter calc(350ms / var(--speed)) var(--ease); }
c29-sheet-stack .stk-layer[data-state="covered"] .stk-surface { filter: brightness(var(--stk-bright, 1)); }
c29-sheet-stack .stk-veil { position: absolute; inset: 0; border-radius: inherit; background: #000;
  opacity: 0; pointer-events: none; }
c29-sheet-stack .stk-layer[data-state="covered"] .stk-veil { opacity: var(--stk-dim, 0); }
c29-sheet-stack .stk-layer[data-kind="card"] .stk-veil { border-radius: var(--corner); }
c29-sheet-stack .stk-layer[data-kind="panel"] .stk-veil { border-radius: var(--r-md); }
c29-sheet-stack .stk-layer[data-kind="card"] .stk-surface { top: calc(var(--safe-t) + 8px);
  left: 8px; right: 8px; margin: 0 auto; width: auto; max-width: 560px;
  background: var(--raised); border-radius: var(--corner); box-shadow: var(--lift-hi);
  padding: var(--s4);
  /* Scroll region (Round 31 adversary): the card caps at the
     viewport (its top offset plus an 8px bottom margin) and
     scrolls vertically, so a tall card's tail is reachable —
     before this rule the surface had no scroll region and a
     tall card's lower controls were clipped by the wrapper,
     unreachable by touch, focus or scroll in WebKit. The share
     row's box still overflows the card's right edge inside
     this scroll region (its measured overflow is unchanged);
     it is now reachable by the region's horizontal scroll
     instead of painting past the edge. Overscroll is
     contained: a layer scroll never chains to the sheet. */
  max-height: calc(100dvh - var(--safe-t) - 16px); overflow-y: auto; overscroll-behavior: contain; }
c29-sheet-stack .stk-layer[data-kind="panel"] .stk-surface { top: 24dvh; bottom: var(--s4);
  left: var(--s4); right: var(--s4); margin: 0 auto; width: auto; max-width: 560px;
  background: var(--sunken); border-radius: var(--r-md); box-shadow: var(--lift);
  padding: var(--s4);
  /* Scroll region (Round 31 adversary, as the card above): the
     panel's box is height-bounded by its top/bottom anchors, so
     its content scrolls inside it; before this rule a tall
     panel's tail was unreachable. Overscroll contained. */
  overflow-y: auto; overscroll-behavior: contain; }
c29-sheet-stack .stk-enter .stk-surface { animation: stk-rise calc(350ms / var(--speed)) var(--ease); }
@keyframes stk-rise { from { transform: translateY(18px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
c29-sheet[data-stack-covered] .sh-panel { filter: brightness(var(--stk-base-bright, 1)); }
html[data-motion="reduced"] c29-sheet-stack .stk-surface { transition: none; }
html[data-motion="reduced"] c29-sheet-stack .stk-enter .stk-surface { animation: none; }
@media (prefers-reduced-motion: reduce) {
  c29-sheet-stack .stk-enter .stk-surface { animation: none; }
  c29-sheet-stack .stk-surface { transition: none; }
}
}
/* ---------- end sheet stack (c29-sheet-stack) ---------- */

/* ================================================================
 * CHIP — c29-chip (TikTok Pattern Library Round 9, 2026-10-27;
 * JS: tide.js, CHIP block. Source spec: Round 2 teardown,
 * corpus frame 01, the interest picker.)
 * Geometry of record, every number confirmed by independent
 * pixel measurement in the teardown: the button is 48px tall
 * (the chip's tap target — above the 44px house floor),
 * radius full (999px), background --surface (the corpus's
 * #FFFFFF in house tokens — the house foam/sand surface; the
 * corpus frame is a light frame only, so the dark rendering
 * through the same tokens via [data-theme] is house design),
 * border 1px on --edge (the light hairline token, the
 * #E4E4E4-class border in light), padding 10px vertical ×
 * 18px horizontal, glyph 26px with a 10px gap to a 16px/600
 * label (the teardown's ≈26–28pt glyph and ≈16–17pt
 * semibold label at CSS px). The host is inline-flex and
 * content-width: a chip sizes to its face, never stretches.
 * SELECTED — HOUSE DESIGN, not subject evidence: the corpus
 * shows no selected chip. Canon's chosen treatment (of the
 * teardown's two candidates) is dark fill inversion: --ink
 * fill, --bg label, --ink border. Disabled — likewise house
 * design: the unselected ground at 0.55 opacity with mute
 * ink; the button's own disabled attribute makes it inert.
 * Motion: the fill/border/colour change rides 150ms (inside
 * the teardown's INF 120–180ms band) divided by --speed on
 * --ease; the selection pulse is the chip-pop keyframes,
 * scale 1 → 0.97 → 1 over the same 150ms on --ease-give
 * (which reads --bounce). Under the motion gate the pulse is
 * removed outright by the section's own rule, and the colour
 * change rides the global gate's collapse — its !important
 * transition allowlist (opacity, background-color, color,
 * box-shadow at 160ms) governs over the section's durations,
 * border-color is outside the allowlist so the border lands
 * instantly, and no transform ever transitions: paint only,
 * no travel, either way.
 * The chip's own row obeys the Squish Law: the label is the
 * prose — it carries min-width 0 and overflow-wrap:anywhere
 * and flexes at 1 1 auto, while the glyph is flex:none and
 * never shrinks; a long label wraps inside the chip, the
 * glyph keeps its size, and word-break:break-all appears
 * nowhere in this section.
 * ================================================================ */
@layer c29.components {
c29-chip { display: inline-flex; max-width: 100%; min-width: 0; color: var(--ink); }
/* JS-dead: no button is built — the host renders as the
   unselected pill itself and the declaration stays readable:
   the glyph attribute paints before, the label attribute
   after, and slot/plain-text children render in place. */
c29-chip:not([data-chip-built]) { align-items: center; gap: 10px; min-height: 48px;
  padding: 10px 18px; border-radius: 999px; background: var(--surface);
  box-shadow: inset 0 0 0 1px var(--edge);
  font: 600 16px/1.35 var(--font-ui); }
c29-chip[glyph]:not([data-chip-built])::before { content: attr(glyph); flex: none;
  font-size: 26px; line-height: 1; }
c29-chip[label]:not([data-chip-built])::after { content: attr(label); min-width: 0;
  overflow-wrap: anywhere; }
c29-chip .chip-btn { display: inline-flex; align-items: center; gap: 10px;
  min-height: 48px; max-width: 100%; min-width: 0;
  padding: 10px 18px; border: 1px solid var(--edge); border-radius: 999px;
  background: var(--surface); color: var(--ink);
  font: 600 16px/1.35 var(--font-ui); cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: background-color calc(150ms / var(--speed)) var(--ease),
    border-color calc(150ms / var(--speed)) var(--ease),
    color calc(150ms / var(--speed)) var(--ease),
    box-shadow calc(150ms / var(--speed)) var(--ease); }
c29-chip .chip-btn:hover { background: var(--wash); }
c29-chip .chip-btn:focus-visible { box-shadow: var(--focus, 0 0 0 2px var(--focus-ring)); outline: none; }
c29-chip[selected] .chip-btn { background: var(--ink); border-color: var(--ink); color: var(--bg); }
c29-chip[selected] .chip-btn:hover { background: var(--ink); }
c29-chip[disabled] .chip-btn { opacity: .55; color: var(--ink-mute); cursor: default; }
c29-chip[disabled] .chip-btn:hover { background: var(--surface); }
c29-chip[selected][disabled] .chip-btn { background: var(--ink); color: var(--bg); }
c29-chip .chip-glyph { flex: none; font-size: 26px; line-height: 1;
  display: inline-flex; align-items: center; }
c29-chip .chip-glyph[hidden] { display: none; }
c29-chip .chip-glyph svg, c29-chip .chip-glyph img { width: 26px; height: 26px; display: block; }
c29-chip .chip-label { flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; text-align: left; }
c29-chip .chip-btn.chip-pop { animation: chip-pop calc(150ms / var(--speed)) var(--ease-give); }
@keyframes chip-pop { 0% { transform: scale(1); } 40% { transform: scale(.97); } 100% { transform: scale(1); } }
html[data-motion="reduced"] c29-chip .chip-btn { transition: none; }
html[data-motion="reduced"] c29-chip .chip-btn.chip-pop { animation: none; }
@media (prefers-reduced-motion: reduce) {
  c29-chip .chip-btn.chip-pop { animation: none; }
}
}
/* ---------- end chip (c29-chip) ---------- */

/* ================================================================
 * CHIPWALL — c29-chipwall (TikTok Pattern Library Round 10,
 * 2026-10-27; JS: tide.js, CHIPWALL block. Source spec: Round 2
 * teardown, corpus frame 01, the interest picker.)
 * The wall is a flowing two-column RAGGED layout of
 * content-width c29-chip children: flex wrap, row gap 10px and
 * column gap 12px on the 48px chip gives the teardown's
 * measured row pitch of ≈58px. Rows pair chips as they fit —
 * a wide chip beside a narrow one — the last row may hold a
 * lone chip, and no row is a rigid grid: chips are flex
 * 0 0 auto, so a chip never stretches to fill a row and never
 * shrinks to fit one; a chip that does not fit the rest of a
 * row wraps WHOLE to the next row (the flex line breaks on the
 * chip's content size), and a chip wider than the wall itself
 * is capped by max-width:100% and wraps its label inside
 * itself (c29-chip's own Squish-Law construction), never
 * crushed by the wall.
 * The wall adds NO chrome of its own — no border, no card, no
 * background, no padding, no margin: in the corpus the wall
 * sits directly on the surface. The frame's ≈32pt side margins
 * are the SCREEN inset (the headline and footer share it); the
 * wall takes its host's inset, so that inset is host/rig CSS,
 * never this section's.
 * The layout is pure CSS on the host element: with JS dead the
 * wall still flows and every chip's label stays readable
 * (c29-chip's JS-dead pill) — JS adds only the selection model.
 * No tokens are read here: gaps and pitch are the teardown's
 * measured points, stated as literals like the chip's own
 * geometry.
 * ================================================================ */
@layer c29.components {
c29-chipwall { display: flex; flex-wrap: wrap; gap: 10px 12px;
  align-items: flex-start; min-width: 0; color: var(--ink); }
c29-chipwall > c29-chip { flex: 0 0 auto; max-width: 100%; min-width: 0; }
}
/* ---------- end chipwall (c29-chipwall) ---------- */

/* ================================================================
 * FLOWFOOT — c29-flowfoot (TikTok Pattern Library Round 11,
 * 2026-10-27; JS: tide.js, FLOWFOOT block. Source spec: Round 2
 * teardown, corpus frame 01, the interest picker.)
 * The flow footer: a hairline across the top and two pill
 * buttons in a row. Geometry of record from the teardown
 * (independently re-measured in its confirmation round):
 * hairline 1px, footer padding ≈12pt vertical, buttons EQUAL
 * width ≈159pt each (flex 1 1 0 on the pair — measured equal,
 * never a ratio), height ≈52px, radius full (999px), gap
 * ≈12px, label 17px/600 (the frame's label scale; the corpus
 * does not separately measure button type, so the size is the
 * teardown's ≈17pt UI scale, not a pixel claim).
 * INSET SPLIT — the frame's ≈32pt side margins are the SCREEN
 * inset, shared by the headline, the wall and the footer: the
 * footer carries the 12px vertical padding (plus the
 * safe-area bottom inset) and NO horizontal padding — the
 * host's inset positions the pair, exactly as c29-chipwall
 * takes its host's inset. PINNING IS HOST LAYOUT: this section
 * sets no position, no sticky, no fixed — where the footer
 * sits and whether a wall scrolls under it is the host's flex
 * or bar arrangement, never a component claim (the teardown
 * labels pinning INF; a still proves neither).
 * TREATMENTS, ALL TOKENS, BOTH THEMES:
 *   - footer ground --bg (the corpus's white page ground in
 *     house tokens; the dark ground flips through [data-theme]
 *     the same way) and hairline on --edge-soft (the house
 *     hairline token — the corpus's rgba(0,0,0,0.08)-class
 *     line translated to tokens, labelled as a translation).
 *   - Skip (secondary): --sunken ground, --ink label — the
 *     corpus's light ground (#F1F1F2-class) with a black label,
 *     translated to the house sunken pair.
 *   - Next ENABLED (primary): --ink ground, --bg label — the
 *     same ink/bg inversion pair as c29-chip's selected state.
 *     INF, labelled: the frame proves only the grey pole; the
 *     enabled appearance is the teardown's labelled inference.
 *   - Next DISABLED: --ink-faint ground, --bg label — the
 *     corpus-proven disabled appearance (#A9A9A9-class grey,
 *     white label — --ink-faint is #A3AAAA in light) translated
 *     to tokens: the enabled pair at the faint step, labelled
 *     as a translation. The button's native disabled state
 *     carries the inertness; this rule carries only the paint.
 * Motion: Next's enable/disable is a colour cross-fade of
 * ≈200ms (the teardown's INF rebuild spec) divided by --speed
 * on --ease. Under the motion gate the colour change rides the
 * global gate's collapse — its !important transition allowlist
 * (opacity, background-color, color, box-shadow at 160ms)
 * governs over the section's 200ms, exactly as it governs
 * c29-chip's colour change; the section's own
 * html[data-motion="reduced"] rule states the same intent for
 * any property outside that allowlist. There is no travel in
 * this component to collapse: paint only, either way.
 * The footer row obeys the Squish Law: both buttons are
 * min-width:0 flex children at 1 1 0, their labels centre and
 * wrap inside the button (overflow-wrap:anywhere), and
 * word-break:break-all appears nowhere in this section.
 * JS-dead: the chrome below is plain CSS on the host and on
 * the buttons' data attributes, so a HOST-DECLARED pair
 * renders complete without JS — hairline, equal pills, labels
 * readable, Next in exactly the state the host declared (the
 * rig declares it disabled, so nothing reads falsely enabled
 * without JS). What JS-dead cannot do is track validity: no
 * state changes without JS, and the attribute-built form
 * (labels only, no declared buttons) builds nothing at all.
 * ================================================================ */
@layer c29.components {
c29-flowfoot { display: flex; gap: 12px; align-items: stretch; min-width: 0;
  padding: 12px 0 calc(12px + var(--safe-b));
  border-top: 1px solid var(--edge-soft);
  background: var(--bg); color: var(--ink); }
c29-flowfoot > button { flex: 1 1 0; min-width: 0; min-height: 52px;
  /* 52px is a FLOOR, not a fixed height (Round 32 adversary):
     the corpus measure is the single-line pill; a label that
     wraps to more lines (a long label, or any label at 200%
     zoom) grows the pill instead of painting outside it — the
     fixed height let a three-line label's ink escape the pill
     by ~15px top and bottom at 390px and ~45px at 195px. The
     pair still measures exactly 52px on one line, and the
     footer's align-items:stretch keeps the pair equal. */
  padding: 0 16px; border: 0; border-radius: 999px;
  display: inline-flex; align-items: center; justify-content: center;
  text-align: center; overflow-wrap: anywhere;
  font: 600 17px/1.2 var(--font-ui); cursor: pointer;
  -webkit-tap-highlight-color: transparent; touch-action: manipulation; }
c29-flowfoot > button:focus-visible { box-shadow: var(--focus, 0 0 0 2px var(--focus-ring)); outline: none; }
c29-flowfoot > [data-flow-skip] { background: var(--sunken); color: var(--ink);
  transition: background-color calc(200ms / var(--speed)) var(--ease),
    color calc(200ms / var(--speed)) var(--ease); }
c29-flowfoot > [data-flow-skip]:hover { background: color-mix(in srgb, var(--sunken) 92%, var(--ink)); }
c29-flowfoot > [data-flow-next] { background: var(--ink); color: var(--bg);
  transition: background-color calc(200ms / var(--speed)) var(--ease),
    color calc(200ms / var(--speed)) var(--ease); }
c29-flowfoot > [data-flow-next]:hover:not(:disabled) { background: color-mix(in srgb, var(--ink) 92%, var(--bg)); }
c29-flowfoot > [data-flow-next]:disabled,
c29-flowfoot > [data-flow-next][disabled] { background: var(--ink-faint); color: var(--bg); cursor: default; }
html[data-motion="reduced"] c29-flowfoot > button { transition: none; }
}
/* ---------- end flowfoot (c29-flowfoot) ---------- */

/* ================================================================
 * SETROW — c29-setrow (TikTok Pattern Library Round 12,
 * 2026-10-27; JS: tide.js, SETROW block. Source spec: Round 3
 * teardown, corpus frame 06, the settings page.)
 * Geometry of record from the teardown (pixel-verified on
 * re-verification): inside the host card, the text block
 * flexes on the left and the control sits on the right in a
 * fixed ≈52pt column, the text-to-control gap ≈16pt. Title
 * ≈17pt bold in the ink token (the corpus's black, translated
 * to tokens); description ≈15pt regular in --ink-mute (the
 * corpus's grey, translated), directly beneath the title on
 * a 2px gap and wrapping — the consequence of the setting
 * written inside the row. In CSS px, as canon renders points:
 * title 700 17px/22px, description 400 15px/20px.
 * THE SIGNATURE, as a construction: the control wrapper is
 * exactly the title's line-height tall (22px), aligns to the
 * row's top, and centres its control — so the control's
 * vertical centre IS the title line's centre at any
 * description length, the alignment the teardown's
 * re-verification measured to within 1px. The wrapper never
 * grows for a taller control: the control overflows it
 * symmetrically (overflow visible) and its centre stays put.
 * The row carries NO border, NO separator, NO background and
 * NO padding of its own — in the corpus the row sits directly
 * in the card, which is the host's surface (the rig supplies
 * the card as rig context; this section never styles one).
 * The row obeys the Squish Law: the text block is flex
 * 1 1 auto with min-width 0 and its parts carry
 * overflow-wrap:anywhere, so prose wraps inside its defended
 * column and an unbreakable token wraps instead of escaping;
 * the control column is flex 0 0 52px and never shrinks, and
 * the control inside it is flex:none — prose is never crushed
 * by the control and the control is never crushed by prose.
 * DISABLED is HOUSE DESIGN (not in corpus, labelled): the
 * host's disabled attribute dims the text block to .55; the
 * control's own disabled state is JS-propagated only where
 * the control exposes a disabled property (see tide.js).
 * No pressed-row state is defined — not in corpus.
 * Tokens only: --ink for the title, --ink-mute for the
 * description; no colour literal anywhere in this section,
 * and the dark rendering flips through [data-theme] on those
 * same tokens (the corpus frame is light only; dark is house
 * rendering, not subject evidence).
 * JS-dead: the unbuilt rules below lay the declared parts out
 * in a grid on the same columns (text column + 52px control
 * column, 16px gap, title over description, the parts wearing
 * the same type) and the disabled dim applies the same way —
 * so the title and description stay readable in place, in
 * order, without JS. What JS-dead does not claim is the
 * signature centring: that construction is the built
 * wrapper's (see tide.js), and the grid fallback aligns the
 * control to the top of its row instead.
 * ================================================================ */
@layer c29.components {
c29-setrow { display: flex; align-items: flex-start; gap: 16px;
  min-width: 0; color: var(--ink); }
c29-setrow .set-text { flex: 1 1 auto; min-width: 0; display: grid; gap: 2px; }
c29-setrow .set-title,
c29-setrow:not([data-setrow-built]) > [data-set-title],
c29-setrow:not([data-setrow-built]) > [slot="title"] { display: block; min-width: 0;
  overflow-wrap: anywhere; margin: 0;
  font: 700 17px/22px var(--font-ui); color: var(--ink); }
c29-setrow .set-desc,
c29-setrow:not([data-setrow-built]) > [data-set-desc],
c29-setrow:not([data-setrow-built]) > [slot="description"] { display: block; min-width: 0;
  overflow-wrap: anywhere; margin: 0;
  font: 400 15px/20px var(--font-ui); color: var(--ink-mute); }
c29-setrow .set-control { flex: 0 0 52px; width: 52px; min-width: 52px; height: 22px;
  display: flex; align-items: center; justify-content: flex-end; overflow: visible; }
c29-setrow .set-control > * { flex: none; }
c29-setrow[disabled] .set-text { opacity: .55; }
/* JS-dead fallback layout (unbuilt): a grid on the same
   columns — title over description in column 1, the control
   part in the 52px column 2, top-aligned. */
c29-setrow:not([data-setrow-built]) { display: grid;
  grid-template-columns: minmax(0, 1fr) 52px; column-gap: 16px; align-items: start; }
c29-setrow:not([data-setrow-built]) > [data-set-title],
c29-setrow:not([data-setrow-built]) > [slot="title"] { grid-column: 1; grid-row: 1; }
c29-setrow:not([data-setrow-built]) > [data-set-desc],
c29-setrow:not([data-setrow-built]) > [slot="description"] { grid-column: 1; grid-row: 2; }
c29-setrow:not([data-setrow-built]) > :not([data-set-title]):not([data-set-desc]):not([slot="title"]):not([slot="description"]) { grid-column: 2; grid-row: 1; justify-self: end; }
c29-setrow[disabled]:not([data-setrow-built]) > [data-set-title],
c29-setrow[disabled]:not([data-setrow-built]) > [slot="title"],
c29-setrow[disabled]:not([data-setrow-built]) > [data-set-desc],
c29-setrow[disabled]:not([data-setrow-built]) > [slot="description"] { opacity: .55; }
}
/* ---------- end setrow (c29-setrow) ---------- */

/* ================================================================
 * TOGGLE — c29-toggle (TikTok Pattern Library Round 13,
 * 2026-10-27; JS: tide.js, TOGGLE block. Source spec: Round 3
 * teardown, corpus frame 06, the settings page.)
 * Geometry of record from the teardown (pixel-measured on
 * its re-verification): the track is 48px × 28px — the
 * frame's switch is slightly smaller than the 51×31 platform
 * standard, and canon builds to the MEASURED size — radius
 * full (999px). The knob is white with a soft shadow (the
 * teardown's sample and words), ≈24px across — EST from the
 * frame ratio (the teardown measures the track, not the
 * knob) — on a 2px inset, so its travel is exactly
 * 48 − 24 − 2×2 = 20px, left when OFF, right when ON.
 * The TOUCH TARGET is bigger than the picture: the host is a
 * 52×44 padded hit area (the track centred in it on 2px side
 * and 8px top/bottom padding), so the target clears the 44px
 * house floor in height and sits exactly in the corpus's
 * ≈52px control column — in a c29-setrow slot the 44px host
 * overflows the 22px title-line wrapper symmetrically and
 * the track's centre stays on the title line (Round 12's
 * construction, proven with this element on the rig).
 * PAINT, TOKENS, LABELLED TRANSLATIONS:
 *   - ON track: --accent, the house sea-glass accent token.
 *     The frame's ON track is the SUBJECT's accent, a sky
 *     cyan sampled #20D4EB; canon translates accents to
 *     house tokens and never copies a subject's brand
 *     colour — this is a translation, stated as one (the
 *     c29-flowfoot disabled-grey precedent).
 *   - OFF track: --wash-2, the house muted wash — HOUSE
 *     DESIGN, labelled: only ON is in the corpus; OFF as a
 *     muted track with the knob left is platform convention
 *     (the teardown's INF), and this token choice is canon's.
 *   - Knob: #fff with a soft shadow — the corpus's measured
 *     knob colour, theme-invariant by design (a switch knob
 *     reads white at night too, platform behaviour); #fff is
 *     the only paint colour literal in this section (the knob
 *     shadow uses rgba(0,0,0,…) literals per the .t-switch
 *     precedent), the .t-switch primitive's own precedent
 *     for this same knob.
 *   - Disabled: HOUSE DESIGN (not in corpus) — the input is
 *     natively disabled (the platform carries the inertness)
 *     and the whole switch dims to .55, c29-chip's disabled
 *     opacity.
 * Motion is the teardown's INF rebuild spec: the knob slides
 * and the track cross-fades at ≈180ms divided by --speed on
 * --ease. Under the motion gate the travel collapses by the
 * gate's true mechanism: the global gate's !important
 * transition allowlist (opacity, background-color, color,
 * box-shadow at 160ms) governs — background-color is on the
 * allowlist so the track still cross-fades at the gate's
 * 160ms, translate is NOT on it so the knob lands instantly
 * with no travel — and this section's own reduced rule states
 * the same intent for the properties it owns.
 * JS-dead, stated honestly: the painted rules ride the
 * .tog-input class, which only JS stamps — a host-DECLARED
 * input without JS is a plain native checkbox in the host's
 * hit area: it renders with the browser's own checkbox
 * face, it clicks, it toggles, it is a working checkbox —
 * it simply does not look like a switch and its state lives
 * on the input alone (nothing mirrors it to the host
 * without JS). The attribute form (no declared input) builds
 * nothing without JS at all: no input appears.
 * ================================================================ */
@layer c29.components {
c29-toggle { display: inline-flex; align-items: center; justify-content: center;
  width: 52px; height: 44px; min-width: 52px; min-height: 44px;
  padding: 8px 2px; color: var(--ink); }
c29-toggle .tog-input { appearance: none; -webkit-appearance: none;
  width: 48px; height: 28px; margin: 0; border: 0; border-radius: 999px;
  background: var(--wash-2); position: relative; cursor: pointer; flex: none;
  -webkit-tap-highlight-color: transparent; touch-action: manipulation;
  transition: background-color calc(180ms / var(--speed)) var(--ease); }
c29-toggle .tog-input::after { content: ""; position: absolute; top: 2px; left: 2px;
  width: 24px; height: 24px; border-radius: 50%; background: #fff;
  box-shadow: 0 1px 3px rgba(0,0,0,.22), 0 2px 6px rgba(0,0,0,.14);
  transition: translate calc(180ms / var(--speed)) var(--ease); }
c29-toggle .tog-input:checked { background: var(--accent); }
c29-toggle .tog-input:checked::after { translate: 20px 0; }
c29-toggle .tog-input:focus-visible { box-shadow: var(--focus, 0 0 0 2px var(--focus-ring)); outline: none; }
c29-toggle .tog-input:disabled { cursor: default; }
c29-toggle[disabled] .tog-input { opacity: .55; }
html[data-motion="reduced"] c29-toggle .tog-input,
html[data-motion="reduced"] c29-toggle .tog-input::after { transition: none; }
@media (prefers-reduced-motion: reduce) {
  c29-toggle .tog-input, c29-toggle .tog-input::after { transition: none; }
}
}
/* ---------- end toggle (c29-toggle) ---------- */

/* ================================================================
 * TOAST — c29-toast (TikTok Pattern Library Round 14,
 * 2026-10-27; JS: tide.js, TOAST block. Source spec: Round 3
 * teardown, corpus frame 06, the settings page.)
 * The pill that STRADDLES its card's top edge as a SHALLOW
 * CAP — the position contract lives in tide.js (the pill is
 * fixed at the anchor's centre-x, its top at the anchor's
 * top minus 8% of the pill's own measured height); this
 * section owns the pill's PAINT, TYPE and MOTION, and the
 * unanchored fallback dock (left 50% / top 14px + safe area,
 * the .pl-toast default dock) that governs only when JS
 * resolves no anchor and clears the inline placement.
 * Geometry of record from the teardown (pixel-measured /
 * EST as marked there): padding ≈14pt vertical × 20pt
 * horizontal; white text ≈16pt semibold; radius ≈18–20pt
 * (EST from crop). In CSS px, as canon renders points:
 * padding 14px 20px; font 600 16px/21px (the teardown's
 * 16/21). RADIUS IS 19px, DELIBERATELY NOT FULL: the corpus
 * pill stands ≈49px tall on its 14+21+14 stack, so a full
 * radius would compute to ≈24.5px — outside the teardown's
 * measured 18–20pt range; 19px is that range's midpoint,
 * built to the measurement, not the platform habit.
 * PAINT — A TOKEN TRANSLATION, labelled, AS AMENDED BY
 * THE R34 CONTRAST RULING (2026-10-27): the frame's pill
 * ground is a medium grey, pixel-sampled ≈ rgb(133,133,133)
 * (~50% grey — the re-verification corrected an earlier
 * "dark charcoal" reading, and canon does not resurrect
 * it). Canon translates it to a token expression, never a
 * copied literal. The original translation mixed the ink
 * halfway into the page ground in both themes, and its
 * white text measured 3.38:1 (light) and 4.01:1 (dark)
 * rendered — below the 4.5:1 AA floor for 16px semibold
 * text, which is not WCAG large-scale text. R34 REFUSED
 * that pairing as canon's default: the pill keeps the
 * corpus's white text and its mid-grey class in both
 * themes, mixed PER THEME — color-mix(in srgb,
 * var(--ink) 68%, var(--bg)) in the light theme and
 * var(--ink) 40% under [data-theme="dark"] (the ink is
 * pale at night, so the lever points the other way) —
 * landing both themes on nearly the same absolute grey
 * (≈ rgb(99–107)) at a measured ≥4.8:1, which is MORE
 * faithful to the corpus's single theme-invariant grey
 * than the flipping mix was. The corpus pairing itself
 * (white on the ≈133 grey, ≈3.4–4.0:1) remains what the
 * corpus shows; canon does not ship it as a default.
 * The mix is of two tokens because no existing single
 * token holds that class in both themes (--ink-faint
 * flips to a deep slate at night; --grab-pill is a sheet
 * handle's named paint, not a surface ground). The
 * TEXT is the corpus's measured white, #fff — a literal,
 * labelled, theme-invariant by design (white result copy
 * reads on the mid-grey ground in both themes, as the
 * frame's does; the knob's #fff on the .t-switch / c29-toggle
 * precedent). Apart from that #fff there is no colour
 * literal in this section.
 * The pill never intercepts touches: pointer-events none
 * in every state. Width is the copy's own (max-content)
 * capped at the viewport minus 24px; long copy wraps
 * inside the pill (overflow-wrap:anywhere on the text,
 * centred) and the pill's height — and with it the 8% cap,
 * which JS measures per-show — grows with the wrap.
 * MOTION — INF, HOUSE DESIGN, labelled: entry fades and
 * rises 8px over calc(200ms / var(--speed)) on --ease; exit
 * fades (and settles back the 8px) over calc(250ms /
 * var(--speed)); the base rule carries the exit durations
 * and the .is-on rule the entry durations, because a CSS
 * transition runs on the destination state's durations.
 * Visibility rides the same clocks (delayed to the exit's
 * end on the way out, instant on the way in) so a dismissed
 * pill is neither visible nor — pointer-events being none —
 * ever in the touch path. Under the motion gate the travel
 * collapses by the gate's true mechanism: the global gate's
 * !important allowlist (opacity, background-color, color,
 * box-shadow at 160ms) governs over this section's
 * durations — opacity is on the allowlist so the fade still
 * runs at the gate's 160ms, transform is NOT on it so there
 * is no rise, and visibility, also outside the allowlist,
 * flips without its delay — and this section's own reduced
 * rules state the same intent for the properties it owns.
 * JS-dead, stated honestly: the base state is opacity 0 /
 * visibility hidden and only JS adds .is-on, so without
 * JavaScript no toast is ever shown — the element sits
 * invisible and touch-transparent, and nothing fake is
 * presented as a confirmation.
 * ================================================================ */
@layer c29.components {
c29-toast { position: fixed; z-index: 90; left: 50%;
  top: calc(14px + var(--safe-t, 0px));
  transform: translate(-50%, 8px);
  box-sizing: border-box; width: max-content; max-width: calc(100vw - 24px);
  padding: 14px 20px; border-radius: 19px;
  background: color-mix(in srgb, var(--ink) 68%, var(--bg));
  color: #fff; font: 600 16px/21px var(--font-ui); text-align: center;
  pointer-events: none; opacity: 0; visibility: hidden;
  transition: opacity calc(250ms / var(--speed)) var(--ease),
    transform calc(250ms / var(--speed)) var(--ease),
    visibility 0s linear calc(250ms / var(--speed)); }
c29-toast.is-on { opacity: 1; visibility: visible; transform: translate(-50%, 0);
  transition: opacity calc(200ms / var(--speed)) var(--ease),
    transform calc(200ms / var(--speed)) var(--ease),
    visibility 0s; }
[data-theme="dark"] c29-toast { background: color-mix(in srgb, var(--ink) 40%, var(--bg)); }
c29-toast .toast-text { display: block; min-width: 0; overflow-wrap: anywhere; }
html[data-motion="reduced"] c29-toast { transition: none; }
@media (prefers-reduced-motion: reduce) {
  c29-toast { transition: none; }
}
}
/* ---------- end toast (c29-toast) ---------- */

/* ================================================================
 * FORMROW — c29-formrow (TikTok Pattern Library Round 15,
 * 2026-10-27; JS: tide.js, FORMROW block. Source spec: Round 4
 * teardown, corpus frame 07, the edit-profile form.)
 * Geometry of record from the teardown (pixel-verified twice,
 * including on re-verification): the label sits LEFT in a
 * fixed column, ≈16pt semibold dark grey; the VALUE begins at
 * a FIXED VALUE COLUMN (value start x ≈445px of 1179 ≈37–38%
 * of screen) and is LEFT-aligned from there — short values end
 * far short of the trailing glyph; set values are BOLD black,
 * placeholder (unset) values SEMIBOLD grey ≈ rgba(0,0,0,0.35);
 * the trailing chevron "›" is ≈18pt in a ≈ rgba(0,0,0,0.3)
 * tone at the card's right edge, aligned to the value's FIRST
 * line (pixel-measured on the 3-line Bio row); there are NO
 * separator lines anywhere — whitespace and rhythm divide the
 * rows; the single-line row pitch is ≈48–50pt; the row's own
 * padding is ≈16pt vertical × 16pt horizontal in the token
 * table — canon renders the pitch exactly: 14px + the 21px
 * value line + 14px = a 49px single-line pitch, on 16px
 * horizontal padding.
 * THE FIXED COLUMN, AS A CONSTRUCTION: the row's button is a
 * grid — [label minmax(0,32%)] [value minmax(0,1fr)] [glyph
 * auto] on a 12px gap — so the value begins at the row's left
 * padding (16px) plus 32% of the row's content box at ANY host
 * width, which measures ≈37–38% of the corpus card's width
 * (canon states ±3 points; the probe verifies every fixture
 * row). The LABEL column is defended the Squish way: it is
 * minmax(0,32%) with the label single-line, ellipsis-
 * truncating INSIDE its column (overflow hidden + ellipsis +
 * nowrap, min-width 0) — a long label can never enter the
 * value column, and the value column never moves. The value
 * carries min-width 0 and overflow-wrap:anywhere, so a
 * wrapping value wraps inside its own column.
 * THE GLYPH ON THE FIRST LINE, AS A CONSTRUCTION (the
 * c29-setrow signature trick, reused — not reinvented): the
 * glyph wrapper is exactly the value's line-height tall
 * (21px), top-aligned in the grid, and centres the glyph
 * inside itself — so the glyph's centre IS the first line's
 * centre whether the value runs one line or three.
 * VALUE DISPLAY: set = 700 in --ink (the corpus's bold black,
 * translated); placeholder = 600 in --ink-mute (the corpus's
 * semibold ≈ rgba(0,0,0,0.35) grey, translated to the muted
 * token), declared by the host's `placeholder` attribute —
 * pure CSS on the attribute, so the reading holds JS-dead.
 * The value WRAPS by default (the corpus's 3-line Bio); a
 * host `truncate` attribute — and every copy-variant row by
 * construction — renders it single-line with a trailing
 * ellipsis (the corpus's URL row: the general truncation
 * mechanism, stated as such).
 * HIGHLIGHT VARIANT — the corpus's featured row, as a
 * MECHANISM: a pale wash across the FULL row width (a
 * left-to-right gradient of a token mix — the accent at 8%
 * fading to transparent; the corpus's cyan-to-white wash,
 * translated) and the value as ONE CONTINUOUS italic-bold
 * gradient string: background-clip:text over a gradient
 * whose endpoints ride --fr-grad-from / --fr-grad-to
 * (defaults: --accent into a color-mix of the accent toward
 * --ink-soft — the teal-to-periwinkle direction, translated
 * to tokens). The endpoints are a SUBJECT treatment a host
 * re-skins through those two properties; the corpus
 * subject's brand colours are never copied as house tokens.
 * PRESSED is HOUSE DESIGN (not in corpus; the Round 4 note):
 * while the button is :active the row wears --wash.
 * DISABLED is HOUSE DESIGN (not in corpus): the host's
 * disabled attribute dims the whole row to .55 by pure CSS
 * (holds JS-dead); the built button's native disabled
 * property carries the inertness (JS, tide.js).
 * NO SEPARATORS, NO VISIBLE BORDER (phrasing corrected at
 * R35 to the c29-bannerrow form — the earlier "no border
 * is declared" claim was literally false beside the
 * button's reset): no visible border is drawn — the built
 * button's native border is removed with border: 0, so
 * host, button and wrappers all compute 0px borders;
 * rows are divided by whitespace alone, as the corpus
 * proves. The row carries no card and no background
 * of its own in the default variant: the grouped card
 * around the rows is Round 16's c29-formgroup (the rig
 * supplies a plain card as labelled rig context).
 * FOCUS: the button takes the house focus ring
 * (--focus) on :focus-visible. Tokens only apart from the
 * two labelled gradient custom-property defaults, which are
 * themselves token expressions; no colour literal exists in
 * this section; dark flips through [data-theme] on the same
 * tokens (the corpus frame is light only; dark is house
 * rendering, not subject evidence).
 * JS-dead, stated honestly: the unbuilt host lays its
 * declared parts out in the SAME grid (same columns, same
 * padding, same type, the placeholder reading and the
 * disabled dim applying the same way), so label and value
 * stay readable in place, the value at its fixed column,
 * without JS. What JS-dead does not claim is the door: no
 * button, no glyph and no activation exist without JS —
 * the glyph and the semantics are the built button's.
 * ================================================================ */
@layer c29.components {
c29-formrow { display: block; min-width: 0; color: var(--ink);
  --fr-grad-from: var(--accent);
  --fr-grad-to: color-mix(in srgb, var(--accent) 55%, var(--ink-soft)); }
c29-formrow .fr-hit { display: grid; width: 100%; box-sizing: border-box;
  grid-template-columns: minmax(0, 32%) minmax(0, 1fr) auto;
  column-gap: 12px; align-items: start;
  padding: 14px 16px; margin: 0; border: 0; background: transparent;
  font: inherit; color: inherit; text-align: left; cursor: pointer;
  -webkit-tap-highlight-color: transparent; touch-action: manipulation; }
c29-formrow .fr-hit:focus-visible { box-shadow: var(--focus, 0 0 0 2px var(--focus-ring)); outline: none; }
c29-formrow .fr-hit:active { background: var(--wash); }
c29-formrow .fr-labelcell { min-width: 0; overflow: hidden; }
c29-formrow .fr-label,
c29-formrow:not([data-formrow-built]) > [data-form-label],
c29-formrow:not([data-formrow-built]) > [slot="label"] { display: block; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin: 0;
  font: 600 16px/21px var(--font-ui); color: var(--ink-soft); }
c29-formrow .fr-value,
c29-formrow:not([data-formrow-built]) > [data-form-value],
c29-formrow:not([data-formrow-built]) > [slot="value"] { display: block; min-width: 0;
  overflow-wrap: anywhere; margin: 0;
  font: 700 16px/21px var(--font-ui); color: var(--ink); }
c29-formrow[placeholder] .fr-value,
c29-formrow[placeholder]:not([data-formrow-built]) > [data-form-value],
c29-formrow[placeholder]:not([data-formrow-built]) > [slot="value"] { font-weight: 600; color: var(--ink-mute); }
c29-formrow[truncate] .fr-value,
c29-formrow[variant="copy"] .fr-value { overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; overflow-wrap: normal; }
c29-formrow .fr-glyphwrap { height: 21px; min-width: 18px;
  display: flex; align-items: center; justify-content: flex-end;
  color: var(--ink-faint); }
c29-formrow .fr-glyph-chev { font: 400 18px/21px var(--font-ui); }
c29-formrow .fr-glyph-copy { width: 18px; height: 18px; display: block; }
c29-formrow[variant="highlight"] .fr-hit { background: linear-gradient(90deg,
  color-mix(in srgb, var(--accent) 8%, transparent), transparent); }
c29-formrow[variant="highlight"] .fr-hit:active { background: linear-gradient(90deg,
  color-mix(in srgb, var(--accent) 8%, transparent), transparent), var(--wash); }
c29-formrow[variant="highlight"] .fr-value { font-style: italic;
  background: linear-gradient(90deg, var(--fr-grad-from), var(--fr-grad-to));
  -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent; color: transparent; }
c29-formrow[disabled] { opacity: .55; }
c29-formrow .fr-hit:disabled { cursor: default; }
/* JS-dead fallback layout (unbuilt): the same grid on the
   host — label in column 1, value in column 2 at its fixed
   column, same padding and type; no glyph zone exists. */
c29-formrow:not([data-formrow-built]) { display: grid;
  grid-template-columns: minmax(0, 32%) minmax(0, 1fr);
  column-gap: 12px; align-items: start; padding: 14px 16px; box-sizing: border-box; }
c29-formrow:not([data-formrow-built]) > [data-form-label],
c29-formrow:not([data-formrow-built]) > [slot="label"] { grid-column: 1; }
c29-formrow:not([data-formrow-built]) > [data-form-value],
c29-formrow:not([data-formrow-built]) > [slot="value"] { grid-column: 2; }
c29-formrow[truncate]:not([data-formrow-built]) > [data-form-value],
c29-formrow[truncate]:not([data-formrow-built]) > [slot="value"],
c29-formrow[variant="copy"]:not([data-formrow-built]) > [data-form-value],
c29-formrow[variant="copy"]:not([data-formrow-built]) > [slot="value"] { overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; overflow-wrap: normal; }
}
/* ---------- end formrow (c29-formrow) ---------- */

/* ================================================================
 * FORMGROUP — c29-formgroup (TikTok Pattern Library Round 16,
 * 2026-10-27; JS: tide.js, FORMGROUP block. Source spec: Round 4
 * teardown, corpus frame 07, the edit-profile form.)
 * Geometry of record from the teardown (pixel-verified twice,
 * including on re-verification): the form's rows sit in grouped
 * CARDS — white cards, radius ≈12pt, inset 12pt from the screen
 * edges (pixel-measured; the banner above the cards insets 16pt
 * and is a different element, Round 17's c29-bannerrow, never
 * this component's). Inside a card the rows are divided by
 * WHITESPACE alone — there are NO separator lines anywhere in
 * either corpus card (pixel-verified) — and the single-line row
 * pitch inside is ≈48–50pt, which is c29-formrow's own pitch,
 * consumed here, never re-declared. A section LABEL stands over
 * two of the corpus's three groups ("Basic info", "Others" in
 * the subject's words): ≈15pt semibold in a grey the re-
 * verification measured at rgba(0,0,0,0.35–0.39); the label's
 * left edge is inset-aligned with its card; the gap from the
 * label to its card is ≈8pt and the gap from a card to the
 * NEXT group's label is ≈28pt. The corpus's first group (the
 * identity card) carries NO label — the label is optional and
 * its absence renders no placeholder.
 * THE GROUP'S OWNERSHIP, stated once: the group owns the card
 * chrome (the raised ground, the 12px radius, the house hair
 * ring, the corner clipping), the 12pt screen inset, the
 * label and its two gaps (8pt under the label, 28pt between
 * stacked groups), and the card's internal stacking (rows
 * directly stacked, no card padding, no inter-row gap — each
 * row carries its own padding, per c29-formrow). The group
 * owns NOTHING the rows own: no selector in this section
 * targets a row's interior (its label, value, glyph, button
 * or state), and the only row-host rules anywhere in this
 * section are the JS-dead surface rules below, which paint
 * the row HOST's own ground and end radii and nothing inside
 * the row.
 * THE INSET, AS A CONSTRUCTION: the host carries the 12px
 * inset as its own horizontal padding (border-box), so the
 * label and the card share one left edge and one right edge
 * at any host width, and a host places groups in a full-bleed
 * column with no inset of its own. The page ground behind the
 * inset is the HOST's surface, never painted here (the
 * corpus's page is a light grey, sampled #F8F8F8 EST in the
 * teardown's token table — a corpus sample, quoted in this
 * comment only; the rig supplies a token ground).
 * THE CARD: background --raised (the corpus's white card,
 * translated to the raised token — the same ground the
 * formrow rig's labelled rig-context cards used), radius
 * 12px, the house hair ring (--hair, the same ring those rig
 * cards carried), overflow hidden so a row's wash (the
 * formrow highlight variant) clips to the card's corners, NO
 * border and NO padding.
 * STACKING: stacked groups are separated by the sibling rule
 * below — a 28px top margin on every group after the first.
 * The teardown's measured 28pt is card-to-next-LABEL; the
 * margin sits between the groups themselves, so a following
 * group WITH a label measures exactly the corpus's 28pt from
 * the previous card to its label, and a following LABEL-LESS
 * group sits the same 28px below the previous card (the
 * corpus's only label-less group is its first, so the
 * label-less stacking gap is the construction's uniform
 * application of the one measured gap, stated as such).
 * THE LABEL: 600 at 15px/20px in --ink-mute (the corpus's
 * semibold ≈ rgba(0,0,0,0.35–0.39) grey, translated to the
 * muted token — the same translation c29-formrow's
 * placeholder reading uses), wrapping inside the inset
 * column (min-width 0, overflow-wrap:anywhere — the Squish
 * construction: a long label wraps, it never truncates and
 * never pushes the card), on an 8px gap to its card.
 * NO SEPARATORS: no border is declared anywhere in this
 * section — host, label, card and the JS-dead row hosts all
 * compute 0px borders; whitespace and rhythm divide, exactly
 * as the corpus proves.
 * TOKENS ONLY: no colour literal exists in this section's
 * rules (the literals above are corpus samples quoted in
 * this comment, the toast/formrow precedent); dark flips
 * through [data-theme] on the same tokens (the corpus frame
 * is light only; dark is house rendering, not subject
 * evidence).
 * JS-DEAD, STATED HONESTLY: the unbuilt host still carries
 * the inset, the label still reads in place in the same type
 * (the label selectors below cover the unbuilt label part
 * directly), and each unbuilt row host paints the raised
 * ground itself, the first row host rounding its top corners
 * and the last its bottom corners at the same 12px, so the
 * stack reads on the card ground in place, its rows at their
 * fixed columns (c29-formrow's own JS-dead grid). What
 * JS-dead does NOT claim is the one-piece card: no card
 * wrapper, no hair ring and no corner clipping exist without
 * JS — the surface is the rows' own grounds end to end.
 * ================================================================ */
@layer c29.components {
c29-formgroup { display: block; box-sizing: border-box; min-width: 0;
  padding: 0 12px; color: var(--ink); }
c29-formgroup + c29-formgroup { margin-top: 28px; }
c29-formgroup .fg-label,
c29-formgroup:not([data-formgroup-built]) > [data-group-label],
c29-formgroup:not([data-formgroup-built]) > [slot="label"] { display: block;
  min-width: 0; overflow-wrap: anywhere; margin: 0 0 8px;
  font: 600 15px/20px var(--font-ui); color: var(--ink-mute); }
c29-formgroup .fg-card { background: var(--raised); border-radius: 12px;
  box-shadow: var(--hair); overflow: hidden; }
/* JS-dead surface (unbuilt): each row host paints the raised
   ground itself; the first row host rounds the card's top
   corners, the last rounds its bottom corners. Row hosts
   only — nothing inside a row is touched. */
c29-formgroup:not([data-formgroup-built]) > c29-formrow { background: var(--raised); }
c29-formgroup:not([data-formgroup-built]) > c29-formrow:first-of-type { border-radius: 12px 12px 0 0; overflow: hidden; }
c29-formgroup:not([data-formgroup-built]) > c29-formrow:last-of-type { border-radius: 0 0 12px 12px; overflow: hidden; }
c29-formgroup:not([data-formgroup-built]) > c29-formrow:first-of-type:last-of-type { border-radius: 12px; }
}
/* ---------- end formgroup (c29-formgroup) ---------- */

/* ================================================================
 * BANNERROW — c29-bannerrow (TikTok Pattern Library Round 17,
 * 2026-10-27; JS: tide.js, BANNERROW block. Source spec: Round 4
 * teardown, corpus frame 07, the edit-profile form. This round
 * closes the forms family: R15 formrow · R16 formgroup · R17
 * bannerrow.)
 * Geometry of record from the teardown (pixel-measured on
 * re-verification): ABOVE the grouped form cards sits a
 * distinct banner row — height ≈45pt (measured; an earlier
 * draft's 56pt stands corrected), inset 16pt from the screen
 * edges (pixel-measured; the CARDS below inset 12pt — two
 * different insets for two different elements, never
 * conflated), radius ≈12pt, a gradient fill (cyan → teal,
 * EST endpoints), artwork LEFT, a white bold ≈17pt label,
 * and a chevron RIGHT — the form rows' own chevron, the
 * grammar of a door, on a promo that lives inside the form's
 * own grammar (same radius family, same chevron).
 * THE INSET + FACE, AS A CONSTRUCTION: the host element IS
 * the banner face — it carries the measured 16px inset as
 * its own horizontal margins, the 12px radius and the
 * gradient, with overflow hidden clipping the face (and the
 * artwork inside it) to the radius; the built button fills
 * the face transparently. A host places the banner in a
 * full-bleed column and supplies no inset of its own; placed
 * directly over c29-formgroup groups (12px inset) the
 * banner's extra 4px step-in reads exactly as the corpus's
 * two-inset rhythm reads. The page ground behind the inset
 * is the host's surface, never painted here.
 * THE HEIGHT, AS A CONSTRUCTION: the face's height is the
 * teardown's ≈45pt carried exactly — the button's min-height
 * is 45px and the unbuilt host's min-height is the same 45px,
 * with 6px of vertical padding inside that height (border-
 * box), so the label's 21px line centres in the 45px face.
 * Artwork is capped at 40px × 32px with overflow hidden,
 * so no admitted artwork can grow the face past 45px.
 * THE GRADIENT — the MECHANISM is the pattern, the colours
 * are the subject's (the c29-formrow highlight precedent):
 * linear-gradient(90deg, var(--br-grad-from),
 * var(--br-grad-to)) with the two custom properties
 * defaulting to token expressions — from a color-mix of
 * --accent 55% toward --sea-300, to --accent itself: the
 * corpus's cyan → teal direction, translated. A host
 * re-skins the endpoints through those two properties as
 * its own subject treatment; the corpus subject's brand
 * colours are never copied as house tokens. The label and
 * the chevron ride --br-on, defaulting to var(--on-accent)
 * — white over the light theme's teal, dark ink over the
 * dark theme's brightened accent, the designed on-fill
 * partner of the fill's own token in each theme.
 * TYPE: the label is 700 at 17px/21px (the teardown's white
 * bold ≈17pt), single-line and ellipsis-truncating INSIDE
 * its flex column (min-width 0 — the Squish construction:
 * the label defends its column against the artwork and the
 * chevron; the label truncates BY CONSTRUCTION — the 45px
 * face is a single-line row — and carries the data-truncate
 * declaration on the label element, stamped at build in
 * tide.js (the Squish gate's named exemption; the full
 * label stays reachable as the button's accessible name
 * and behind the banner's own door). The chevron "›" is 18px,
 * the form rows' own glyph size, in the same --br-on.
 * THE ARTWORK: the host's artwork part sits left, flex-none,
 * capped at 40px wide and 32px tall (the cap is HOUSE
 * DESIGN — the teardown names artwork's presence and place,
 * not its pixel size), its content host-supplied and
 * decorative beside the label. ARTWORK ABSENCE is HOUSE
 * DESIGN, labelled (the corpus proves no banner without
 * artwork): with no artwork part the label begins at the
 * face's left padding.
 * PRESSED is HOUSE DESIGN (not in corpus; the formrow
 * precedent): while the button is :active the face wears
 * --wash layered over its gradient. DISABLED is HOUSE
 * DESIGN (not in corpus): the host's disabled attribute
 * dims the whole banner to .55 by pure CSS (holds JS-dead);
 * the built button's native disabled property carries the
 * inertness (JS, tide.js).
 * NO SEPARATORS, NO VISIBLE BORDER: no visible border is
 * drawn — the button's native border is removed with
 * border: 0, so the host and the button compute 0px
 * borders, and the face's only edge is its radius and
 * its gradient.
 * FOCUS: the button takes the house focus ring (--focus)
 * on :focus-visible. Tokens only: no colour literal exists
 * in this section's rules; dark flips through [data-theme]
 * on the same tokens (the corpus frame is light only; dark
 * is house rendering, not subject evidence).
 * JS-DEAD, STATED HONESTLY: the unbuilt host IS the face —
 * it carries the same inset margins, radius, gradient,
 * min-height and face padding, and lays its declared parts
 * out in the same flex row (artwork left, label in its
 * column), so the promo stays readable in place, wearing
 * its gradient, without JS. What JS-dead does not claim is
 * the door: no button, no chevron and no activation exist
 * without JS — the glyph and the semantics are the built
 * button's.
 * ================================================================ */
@layer c29.components {
c29-bannerrow { display: block; box-sizing: border-box; min-width: 0;
  margin: 0 16px; border-radius: 12px; overflow: hidden;
  color: var(--br-on, var(--on-accent));
  --br-grad-from: color-mix(in srgb, var(--accent) 55%, var(--sea-300));
  --br-grad-to: var(--accent);
  --br-on: var(--on-accent);
  background: linear-gradient(90deg, var(--br-grad-from), var(--br-grad-to)); }
c29-bannerrow .br-hit { display: flex; width: 100%; box-sizing: border-box;
  min-height: 45px; align-items: center; gap: 10px;
  padding: 6px 16px; margin: 0; border: 0; background: transparent;
  font: inherit; color: inherit; text-align: left; cursor: pointer;
  -webkit-tap-highlight-color: transparent; touch-action: manipulation; }
c29-bannerrow .br-hit:focus-visible { box-shadow: var(--focus, 0 0 0 2px var(--focus-ring)); outline: none; }
c29-bannerrow .br-hit:active { background: var(--wash); }
c29-bannerrow .br-art { flex: none; display: flex; align-items: center;
  max-width: 40px; max-height: 32px; overflow: hidden; }
c29-bannerrow .br-label,
c29-bannerrow:not([data-bannerrow-built]) > [data-banner-label],
c29-bannerrow:not([data-bannerrow-built]) > [slot="label"] { display: block;
  flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; margin: 0;
  font: 700 17px/21px var(--font-ui); color: var(--br-on, var(--on-accent)); }
c29-bannerrow .br-glyphwrap { flex: none; height: 21px; min-width: 18px;
  margin-left: auto; display: flex; align-items: center; justify-content: flex-end;
  color: var(--br-on, var(--on-accent)); }
c29-bannerrow .br-glyph-chev { font: 400 18px/21px var(--font-ui); }
c29-bannerrow[disabled] { opacity: .55; }
c29-bannerrow .br-hit:disabled { cursor: default; }
/* JS-dead fallback layout (unbuilt): the host is the face —
   the same flex row on the host itself, artwork left and
   label in its column, same padding and type; no glyph zone
   exists. */
c29-bannerrow:not([data-bannerrow-built]) { display: flex;
  min-height: 45px; align-items: center; gap: 10px;
  padding: 6px 16px; }
c29-bannerrow:not([data-bannerrow-built]) > [data-banner-art],
c29-bannerrow:not([data-bannerrow-built]) > [slot="art"] { flex: none;
  display: flex; align-items: center; max-width: 40px; max-height: 32px;
  overflow: hidden; }
}
/* ---------- end bannerrow (c29-bannerrow) ---------- */

/* ================================================================
 * PROFHEAD — c29-profhead (TikTok Pattern Library Round 18,
 * 2026-10-27; CSS-ONLY — no tide.js block. Source spec: Round 5
 * teardown, corpus frame 03, the profile header.)
 * THE COMPOSITION BOUNDARY, stated once: profhead owns IDENTITY
 * only — the avatar, the display name, the handle and the bio —
 * plus the two empty slot REGIONS where the family's later
 * components sit: the stats region (c29-profstats, Round 19)
 * and the actions region (c29-pillbtn, Round 20).
 * Profhead builds neither: it declares no stat, no pill, no tab
 * and no nav, and whatever a host places inside a region is
 * that component's own construction, untouched here.
 * THE LAYOUT, AS A CONSTRUCTION: the host is a grid —
 * [text minmax(0,1fr)] [avatar auto] — with the name, handle
 * and bio stacked in the text column (the corpus's flow: name
 * and stats read left, the face sits right), the avatar
 * spanning the text rows at the top, and the two slot regions
 * on full-width rows beneath. Every text part carries
 * min-width: 0 and overflow-wrap: anywhere (the Squish
 * construction): a long name or an unbreakable bio token wraps
 * inside the defended text column and can never push the
 * avatar off the header.
 * GEOMETRY + TYPE, from the teardown (EST, translated to
 * tokens): the avatar is a 92px circle; the display name is
 * 800 at 24px/30px in --ink (the corpus's ≈24pt extrabold
 * black); the handle is 15px/20px in --ink-mute (the corpus's
 * ≈15pt grey); the bio is 15px/22px in --ink-soft (HOUSE
 * DESIGN — the corpus frame's header carries no bio text; the
 * bio's place in the identity block is the corpus's own
 * "+ Add bio" absence advertising it, and its type is canon's
 * translation at the handle's size). The header's own padding
 * is 16px at the sides and top; the page ground behind it is
 * the host's surface, never painted here.
 * THE AVATAR: the avatar part is the circle — position
 * relative, overflow hidden, a token ground (--accent-soft).
 * An <img> the host places inside it fills the circle (cover);
 * text the host places inside it instead renders as initials
 * (700 at 30px in --accent-text — the c29-presence medallion
 * precedent: initials on a token tone). Canon owns no
 * standalone avatar element (the registry was read: the
 * medallions live inside c29-presence, c29-agent and
 * c29-account, none of them a composable profile avatar), so
 * this circle is profhead's own and forks none of them.
 * THE BADGES — two treatments, never conflated:
 * (1) THE ACTION BADGE (the corpus's avatar badge): a 28px
 * circle overlapping the avatar's bottom-right, on --accent
 * with its glyph in --on-accent (the designed pair in both
 * themes), ringed by the page ground. The corpus's badge is
 * a cyan circle carrying a white "+" (EST colour, translated
 * to the accent pair); its glyph is the host's part text,
 * adopted as written. Its construction here: the badge part
 * is a child of the avatar part, positioned by this CSS.
 * (2) THE COUNT BADGE (data-tone="alert" on the badge part):
 * the corpus's red count treatment — a circle ≈16px, white
 * bold ≈11px numerals — proven in the frame on the utility
 * row's icons (add-friend, hamburger), translated here to
 * the designed pair --bad / --on-bad and re-sized as a pill
 * (min-width 16px, horizontal padding) so a two-digit count
 * fits; it sits at the avatar's TOP-right. The red treatment
 * is used for numeric counts ONLY — never for the action
 * badge, never decoratively. Placing a count badge in the
 * avatar's badge slot is HOUSE DESIGN, labelled: the corpus
 * shows the treatment on utility icons profhead does not own.
 * THE NAME CARET: the corpus's display name carries a small
 * dropdown caret (an account-switcher affordance; the
 * switching itself is INF — a still proves the caret, not the
 * switch). When the host declares `switchable`, the name part
 * gains a caret glyph after it (a CSS ::after, aria-hidden by
 * construction as generated content beside the name's own
 * text). Profhead fires nothing and routes nothing: whether
 * the caret is a door is the host's business.
 * CSS-ONLY, stated honestly: there is no tide.js block, no
 * built stamp, no event and no method — the declared parts
 * and this CSS are the whole component (the c29-statrow
 * precedent: a canon component that is CSS only). JS-dead is
 * therefore not a degraded mode: with scripting disabled the
 * header renders identically, part for part, and this
 * component claims no door of its own in either state.
 * ================================================================ */
@layer c29.components {
c29-profhead { display: grid; box-sizing: border-box; min-width: 0;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas: "name avatar" "handle avatar" "bio avatar" "stats stats" "actions actions";
  column-gap: 16px; padding: 16px 16px 0; color: var(--ink);
  --pf-badge-bg: var(--accent); --pf-badge-on: var(--on-accent); }
c29-profhead > [data-prof-name],
c29-profhead > [slot="name"] { grid-area: name; display: block; min-width: 0;
  overflow-wrap: anywhere; margin: 0;
  font: 800 24px/30px var(--font-display); color: var(--ink); }
c29-profhead[switchable] > [data-prof-name]::after,
c29-profhead[switchable] > [slot="name"]::after { content: " \25BE";
  font: 600 16px/30px var(--font-ui); color: var(--ink-mute); }
c29-profhead > [data-prof-handle],
c29-profhead > [slot="handle"] { grid-area: handle; display: block; min-width: 0;
  overflow-wrap: anywhere; margin: 2px 0 0;
  font: 400 15px/20px var(--font-ui); color: var(--ink-mute); }
c29-profhead > [data-prof-bio],
c29-profhead > [slot="bio"] { grid-area: bio; display: block; min-width: 0;
  overflow-wrap: anywhere; margin: 10px 0 0;
  font: 400 15px/22px var(--font-ui); color: var(--ink-soft); }
c29-profhead > [data-prof-avatar],
c29-profhead > [slot="avatar"] { grid-area: avatar; position: relative;
  width: 92px; height: 92px; border-radius: 50%; overflow: hidden;
  align-self: start; display: grid; place-items: center;
  background: var(--accent-soft); color: var(--accent-text);
  font: 700 30px/1 var(--font-display); }
c29-profhead > [data-prof-avatar] > img,
c29-profhead > [slot="avatar"] > img { position: absolute; inset: 0;
  width: 100%; height: 100%; object-fit: cover; display: block; }
c29-profhead [data-prof-badge] { position: absolute; right: 0; bottom: 0;
  z-index: 1; min-width: 28px; height: 28px; box-sizing: border-box;
  padding: 0 4px; border-radius: 999px; display: grid; place-items: center;
  background: var(--pf-badge-bg, var(--accent)); color: var(--pf-badge-on, var(--on-accent));
  box-shadow: 0 0 0 2px var(--bg);
  font: 700 16px/1 var(--font-ui); }
c29-profhead [data-prof-badge][data-tone="alert"] { top: 0; bottom: auto;
  min-width: 16px; height: 16px; padding: 0 4px;
  background: var(--bad); color: var(--on-bad);
  font: 700 11px/16px var(--font-ui); }
c29-profhead > [data-prof-stats],
c29-profhead > [slot="stats"] { grid-area: stats; display: block; min-width: 0; margin: 14px 0 0; }
c29-profhead > [data-prof-actions],
c29-profhead > [slot="actions"] { grid-area: actions; display: block; min-width: 0; margin: 12px 0 0; }
c29-profhead > [data-prof-stats]:empty,
c29-profhead > [slot="stats"]:empty,
c29-profhead > [data-prof-actions]:empty,
c29-profhead > [slot="actions"]:empty { display: none; }
}
/* ---------- end profhead (c29-profhead) ---------- */

/* ================================================================
 * TILEGRID — c29-tilegrid, THE SQUARES (TikTok Pattern Library
 * Round 18, 2026-10-27; CSS-ONLY — no tide.js block. Source
 * spec: Round 5 teardown, corpus frame 03, plus THE SQUARES
 * MAPPING addendum carrying the Principal's steering of
 * record, 2026-10-27 01:01 PT: "Like those video tiles on
 * mobile are our Squares." This component IS the house's
 * Squares build — the Pleasant squares/tiles — never a generic
 * content grid.)
 * THE SQUARES MAPPING, clause by clause, as built:
 * (1) GRID RHYTHM: 3 across at phone width, gutter ≈2px,
 * edge-to-edge — the grid is NOT a card and takes no side
 * inset: the host is a 3-column grid (repeat(3, minmax(0,
 * 1fr))) on a 2px gap with no padding and no margin, so at a
 * 390px host each square measures (390 - 2*2)/3 ≈ 128.7px.
 * (2) ASPECT IS A CANON TOKEN DECISION, never a corpus claim:
 * the corpus ratio was NOT measured (the frame's thumbnails
 * read portrait; no ratio is asserted from it). The default
 * square is 1:1 — the house square, the shape the name
 * carries — and the `portrait` variant sets 3:4 (HOUSE
 * DESIGN, labelled: canon's portrait step, not a measured
 * subject ratio).
 * (3) THE STAT OVERLAY: ONE glyph + ONE numeral (the host's
 * part text, adopted as written), white ≈13px semibold,
 * bottom-left of the square — and CONTRAST IS THE
 * COMPONENT'S CONTRACT, never the host's problem: a tile
 * carrying a stat part grows a scrim (a ::before gradient
 * over the tile's bottom 55%: transparent at its top, 54%
 * of --scrim-ink at 52% of the gradient, ending at 96% of
 * --scrim-ink at the tile's foot — the Round 18 fix's
 * steepened lower curve, so the scrim is already strong
 * across the stat's full box, not only at its centre line)
 * under the stat, and the stat itself rides --on-image with
 * a --scrim-ink text shadow. --on-image and --scrim-ink are
 * the over-imagery token pair (tokens.css, R18): never
 * theme-flipped, because imagery never flips. The guarantee:
 * over ANY host imagery, the stat's effective ground across
 * its full box is the scrim composite, probe-measured
 * against near-white and near-black fixture tiles in both
 * engines and both themes under BAND sampling (every pixel
 * beside the stat across its full 16px box height; the
 * lightest such pixel is the ground; the contract's floor
 * is 4.5:1 — see registry contract 4).
 * (4) RADIUS: the frame reads square-cut and proves no radius
 * (radius NOT MEASURED in the teardown) — the default is
 * square-cut (0) and a host may round its squares through
 * --tg-radius as its own treatment; canon asserts no radius
 * from the frame.
 * (5) THE PINNED / FEATURED BADGE: NOT IN CORPUS — no pinned
 * badge is visible in corpus frame 03. The requirement rides
 * from the Principal's steering itself (the steering line
 * names the pinned badge as part of the tile family), and
 * its provenance is recorded exactly that way here, in the
 * registry and in the round report — never as subject
 * evidence. Construction (HOUSE DESIGN on a steering-carried
 * requirement): a badge part inside a tile sits top-left on
 * the opaque raised ground (--raised / --ink, a designed
 * token pair, so its contrast is the pair's own in both
 * themes), 700 at 11px/14px, pill radius, above the imagery
 * and clear of the stat's bottom-left corner.
 * (6) THE TERMINAL STATE: "No more results" is a NAMED state
 * of the grid — while the host declares `ended`, the grid
 * renders the line itself as a full-width centred footer in
 * --ink-mute beneath the last row (a ::after, so the state
 * renders with or without JS). The wording is the corpus's
 * own UI string, kept verbatim for shape fidelity under the
 * campaign's carve-out: it is a generic platform UI string,
 * not the subject's personal content. The STATE is the
 * pattern; a host that paginates forever never declares it.
 * TILES ARE DOORS: a tile is a host element carrying
 * data-tile — an <a>, a <button>, or another activatable
 * element the host chooses; its activation, href and routing
 * are the host's own native contract, untouched here (a
 * link tile navigates, a button tile fires the host's
 * handler, in both cases with or without tide.js — this
 * component is CSS-only). Tile CONTENT is host-supplied
 * imagery: an <img> inside a tile fills it (cover, clipped
 * to the tile's radius). Canon owns no other tile grid
 * (the registry was read: c29-tile is a launcher row —
 * art + name + sub in a list — c29-channel is the Pleasant
 * launch tile, and .tl-grid / .t-grid are page layout
 * utilities; none is an edge-to-edge media grid), so this
 * grid is built new and forks none of them.
 * AN EMPTY GRID renders nothing: no tiles, no placeholder,
 * no invented empty copy — emptiness is the host's state to
 * name (with `ended` declared, the terminal line still
 * renders beneath the empty grid).
 * CSS-ONLY, stated honestly: no tide.js block, no built
 * stamp, no event and no method (the c29-statrow precedent).
 * JS-dead is not a degraded mode: the grid, the overlays,
 * the badge and the terminal state render identically with
 * scripting disabled, and the tiles' doors are the host's
 * native elements, alive in both states.
 * EXTENSION (2026-10-28 — the Dillo landing parts, on the
 * Principal's ruling "Extend the Tide components, deploy
 * Tide, then swap"; registry contracts 14–17): three
 * ADDITIVE constructions — nothing above changes for any
 * existing consumer. (E1) THE COLUMN RHYTHM, host-declared:
 * cols="2-4" runs two across below the 720px step and four
 * across at and above it, on the same 2px gutter; absent,
 * the rhythm stays 3 across at every width. (E2) THE TEXT
 * TILE: a tile carrying a data-tile-foot part carries words
 * instead of imagery — its face is the plain --surface
 * ground inside a hairline ring, a data-tile-slug part
 * rides top-left in --ink-soft mono (a theme pair on plain
 * ground; no scrim is grown at the top), and the foot
 * carries the tile's name (800 16px) and blurb (14px at
 * 85%) in the --on-image pair over the foot's OWN scrim
 * ground — a length-keyed gradient on the foot itself,
 * keyed to absolute lengths, never to the foot's height:
 * a short feather at the foot's head (transparent at 0px,
 * 54% of --scrim-ink at 10px, clause (3)'s curve shape in
 * miniature) reaching the full 96% of --scrim-ink by
 * 20px, then FLAT 96% of --scrim-ink for the entire
 * remainder of the foot, however tall it grows; the
 * foot's 26px top padding keeps every text box below the
 * feather — so the text's ground is the full-strength
 * scrim composite wherever in the foot the text sits; the
 * 4.5:1 floor is
 * the component's, band-sampled in the extension adversary
 * round. (E3) THE CELL + DOORS: a grid child may be a
 * data-tile-cell — a flex column holding exactly one tile
 * and one data-tile-doors region beneath its face; the
 * doors are the host's own <a data-tile-door> rows
 * (full-width, 44px floor, 600 ink, a hairline under each),
 * so the tap targets live below the face and no tap depends
 * on hitting the tile itself. A cell lays out in the grid's
 * tracks exactly as a bare tile does, and the tile inside
 * it keeps every clause above.
 * ================================================================ */
@layer c29.components {
c29-tilegrid { display: grid; box-sizing: border-box; min-width: 0;
  grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 2px;
  margin: 0; padding: 0; color: var(--ink);
  --tg-radius: 0px; }
c29-tilegrid [data-tile] { position: relative; display: block;
  box-sizing: border-box; min-width: 0; margin: 0; padding: 0;
  aspect-ratio: 1; overflow: hidden; border-radius: var(--tg-radius, 0px);
  background: var(--sunken); color: var(--on-image);
  font: inherit; text-align: left; text-decoration: none;
  -webkit-tap-highlight-color: transparent; }
c29-tilegrid [data-tile]:is(a, button) { cursor: pointer; }
c29-tilegrid button[data-tile] { border: 0; }
c29-tilegrid [data-tile]:is(a, button):focus-visible { box-shadow: inset 0 0 0 2px var(--focus-ring); outline: none; }
c29-tilegrid[variant="portrait"] [data-tile] { aspect-ratio: 3 / 4; }
c29-tilegrid [data-tile] > img { position: absolute; inset: 0;
  width: 100%; height: 100%; object-fit: cover; display: block; }
c29-tilegrid [data-tile]:has(> [data-tile-stat])::before { content: "";
  position: absolute; left: 0; right: 0; bottom: 0; height: 55%; z-index: 1;
  background: linear-gradient(180deg, transparent 0%, color-mix(in srgb, var(--scrim-ink) 54%, transparent) 52%, color-mix(in srgb, var(--scrim-ink) 96%, transparent) 100%);
  pointer-events: none; }
c29-tilegrid [data-tile-stat] { position: absolute; left: 8px; bottom: 8px;
  z-index: 2; display: inline-flex; align-items: center; gap: 4px;
  max-width: calc(100% - 16px); min-width: 0; overflow: hidden;
  white-space: nowrap; margin: 0;
  font: 600 13px/16px var(--font-ui); color: var(--on-image);
  text-shadow: 0 1px 3px color-mix(in srgb, var(--scrim-ink) 90%, transparent),
    0 0 8px color-mix(in srgb, var(--scrim-ink) 65%, transparent); }
c29-tilegrid [data-tile-badge] { position: absolute; top: 8px; left: 8px;
  z-index: 2; display: inline-flex; align-items: center;
  max-width: calc(100% - 16px); min-width: 0; overflow: hidden;
  white-space: nowrap; margin: 0; padding: 3px 8px; border-radius: 999px;
  background: var(--raised); color: var(--ink); box-shadow: var(--hair);
  font: 700 11px/14px var(--font-ui); }
c29-tilegrid[ended]::after { content: "No more results";
  grid-column: 1 / -1; display: block; text-align: center;
  padding: 18px 16px 26px;
  font: 400 14px/20px var(--font-ui); color: var(--ink-mute); }
c29-tilegrid[cols="2-4"] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
@media (min-width: 720px) {
c29-tilegrid[cols="2-4"] { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}
c29-tilegrid [data-tile-cell] { display: flex; flex-direction: column;
  box-sizing: border-box; min-width: 0; margin: 0; padding: 0; }
c29-tilegrid [data-tile]:has(> [data-tile-foot]) { background: var(--surface);
  box-shadow: inset 0 0 0 1px var(--edge); color: var(--ink); }
c29-tilegrid [data-tile-slug] { position: absolute; top: 10px; left: 10px;
  z-index: 2; display: block; max-width: calc(100% - 20px); min-width: 0;
  overflow-wrap: anywhere; margin: 0;
  font: 500 13px/18px var(--font-mono); color: var(--ink-soft); }
c29-tilegrid [data-tile-foot] { position: absolute; left: 0; right: 0; bottom: 0;
  z-index: 2; display: block; box-sizing: border-box; min-width: 0;
  margin: 0; padding: 26px 10px 10px;
  background: linear-gradient(180deg, transparent 0px, color-mix(in srgb, var(--scrim-ink) 54%, transparent) 10px, color-mix(in srgb, var(--scrim-ink) 96%, transparent) 20px, color-mix(in srgb, var(--scrim-ink) 96%, transparent) 100%); }
c29-tilegrid [data-tile-name] { display: block; min-width: 0; margin: 0;
  overflow-wrap: anywhere;
  font: 800 16px/20px var(--font-ui); color: var(--on-image); }
c29-tilegrid [data-tile-blurb] { display: block; min-width: 0; margin: 4px 0 0;
  overflow-wrap: anywhere;
  font: 400 14px/20px var(--font-ui);
  color: color-mix(in srgb, var(--on-image) 85%, transparent); }
c29-tilegrid [data-tile-doors] { display: flex; flex-direction: column;
  box-sizing: border-box; min-width: 0; margin: 0; padding: 0; }
c29-tilegrid [data-tile-door] { display: flex; align-items: center;
  box-sizing: border-box; min-width: 0; min-height: 44px;
  margin: 0; padding: 10px 12px;
  border-bottom: 1px solid var(--edge);
  font: 600 16px/22px var(--font-ui); color: var(--ink);
  text-decoration: none; overflow-wrap: anywhere;
  -webkit-tap-highlight-color: transparent;
  transition: transform var(--d1) var(--ease); }
c29-tilegrid [data-tile-door]:active { transform: scale(.99); }
@media (hover: hover) {
c29-tilegrid [data-tile-door]:hover { background: var(--surface); }
}
c29-tilegrid [data-tile-door]:focus-visible { box-shadow: inset 0 0 0 2px var(--focus-ring); outline: none; }
html[data-motion="reduced"] c29-tilegrid [data-tile-door] { transition: none; }
}
/* ---------- end tilegrid (c29-tilegrid) ---------- */

/* ================================================================
 * PROFSTATS — c29-profstats (TikTok Pattern Library Round 19,
 * 2026-10-27; CSS-ONLY — no tide.js block. Source spec: Round 5
 * teardown, corpus frame 03, the profile header's stat row.)
 * THE TAG COLLISION, stated once (also registry contract 1):
 * the slated tag c29-statrow is ALREADY HELD by a Fleet 10
 * component (2026-10-25) — a hairline-divided totals strip of
 * 2–4 (to 6) room statistics, class children (.sr-stat /
 * .sr-num / .sr-lab), whose first contract renders a MISSING
 * number as an em dash, never as 0. That is a different
 * pattern from this one, and its entry and CSS are untouched
 * by this round. The profile stat row canonizes here as
 * c29-profstats: exactly three equal tap-door columns,
 * numeral over label, no hairlines, and a zero the host
 * writes renders as the numeral "0" — the corpus frame
 * proves it ("0 Following": two of its three stats are zero
 * and the row does not flinch, Round 5 STATES). Silently
 * reusing the tag would have broken Fleet 10's pattern and
 * misrendered the corpus zeros.
 * THE PATTERN, from the teardown: three equal columns —
 * Following / Followers / Likes (generic platform words) —
 * numeral ≈18pt bold over label ≈14pt grey, generous
 * vertical padding ≈14pt, centred per column, divided by
 * whitespace alone: the teardown proves no hairline between
 * the profile row's columns (its only hairlines ride the
 * tab-row / bottom-nav boundary, other rounds' components).
 * That each column is a tap door is INF in the teardown (a
 * still cannot prove tappability); the destination is the
 * host's business, likewise INF.
 * SEMANTICS — columns are the host's OWN native elements
 * (the c29-tilegrid precedent, argued in registry contract
 * 2): an <a> column navigates by its href, a <button>
 * column fires the host's handler, a plain element column
 * is a display column the host has chosen not to make a
 * door. The component builds no button core of its own
 * (the c29-formrow / c29-bannerrow construction was
 * rejected here: those components own a host-routed action
 * with no native element in the host markup, while this
 * pattern's columns arrive AS native doors whose element
 * the host chooses — a built core would need JS, would
 * kill the door JS-dead, and would have to invent an event
 * payload for a destination the corpus leaves INF).
 * COUNTS ARE HOST DATA: the component formats nothing it
 * was not given — numeral and label text render exactly as
 * written, no abbreviation is produced or promised (the
 * corpus frame shows only plain short numerals; no
 * abbreviated form is proven anywhere in it), no placeholder
 * is generated, and an empty numeral part generates NO
 * em dash (that is c29-statrow's contract, not this one's).
 * NO DISABLED OR LOADING STATE is declared: the pattern
 * supports none honestly — a host that must disable a door
 * uses its chosen element's native means, outside this
 * component, and the row's data is the host's to load.
 * CSS-ONLY, stated honestly: no tide.js block, no built
 * stamp, no event, no method, no attribute of its own.
 * JS-dead is not a degraded mode: the row renders
 * identically with scripting disabled and its native doors
 * stay alive in both states.
 * ================================================================ */
@layer c29.components {
c29-profstats { display: grid; box-sizing: border-box; min-width: 0;
  grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0;
  margin: 0; padding: 0; color: var(--ink); }
c29-profstats [data-profstat] { display: flex; flex-direction: column;
  align-items: center; justify-content: flex-start;
  box-sizing: border-box; min-width: 0; margin: 0; padding: 14px 6px;
  color: inherit; font: inherit; text-align: center;
  text-decoration: none; -webkit-tap-highlight-color: transparent; }
c29-profstats [data-profstat]:is(a, button) { cursor: pointer; }
c29-profstats button[data-profstat] { border: 0; background: transparent; }
c29-profstats [data-profstat]:is(a, button):focus-visible { box-shadow: inset 0 0 0 2px var(--focus-ring); outline: none; }
c29-profstats [data-profstat-num] { display: block; min-width: 0;
  overflow-wrap: anywhere; text-align: center;
  font: 700 18px/22px var(--font-ui); color: var(--ink);
  font-variant-numeric: tabular-nums; }
c29-profstats [data-profstat-lab] { display: block; min-width: 0;
  overflow-wrap: anywhere; text-align: center;
  font: 400 14px/18px var(--font-ui); color: var(--ink-mute); }
}
/* ---------- end profstats (c29-profstats) ---------- */

/* ================================================================
 * PILLBTN — c29-pillbtn (TikTok Pattern Library Round 20,
 * 2026-10-27; CSS-ONLY — no tide.js block. Source spec: Round 5
 * teardown, corpus frame 03, the profile header's action pills.)
 * THE PATTERN, from the teardown (adversary-confirmed, its pill
 * height pixel-measured on confirmation): under the stat row
 * sit the action pills — small pill-shaped action buttons,
 * height ≈28pt, radius full, labels ≈15pt semibold, in TWO
 * treatments that are two weights of action: FILLED pills
 * (light-grey fill, black label) carrying a "+" prefix
 * glyph ("+ Add bio", "+ Add college" — the owner's own
 * completion actions), and below them an OUTLINED pill (white
 * ground, 1pt light border, the same ≈28pt geometry)
 * carrying a small lock-bag glyph ("Your orders"). The
 * builder's own pixel read of the frame agrees and adds the
 * arrangement: the filled band measures exactly 28.0pt tall;
 * the two filled pills are CONTENT-WIDTH (the longer label's
 * pill is the wider, ≈109pt against ≈85pt), ≈10pt apart,
 * left-aligned at the page inset; the outlined pill (≈111pt
 * wide) sits on its OWN row ≈14pt below — a deliberate
 * grouping, not a wrap, since it would have fit the first
 * row's remaining width. Canon therefore models ONE ROW per
 * c29-pillbtn element: the corpus block is composed from two
 * rows (a filled row, then an outlined row), and the 14px
 * rhythm between composed rows is the component's own
 * adjacent-row rule below.
 * NO DUPLICATION (the Reuse Law — buttons are the most-owned
 * primitive in canon, so the registry was swept first):
 * .t-btn (the button primitive: min-height --tap = 44px, its
 * is-sm step 36px — no row, no 28px geometry), .t-chip (a
 * 36px pressed-state chip class), c29-chip (a 48px selectable
 * TOGGLE pill with a 26px glyph and aria-pressed), c29-flowfoot
 * (a 52px EQUAL-width pair, JS-built, gated by validity),
 * c29-buy (a checkout state machine), c29-seg (a segmented
 * option row) — each owns a different pattern. None owns a
 * 28px content-width action-pill row with the corpus's
 * filled / outlined pair, and .t-btn cannot be composed into
 * one: its 44px floor (36px at small) would falsify the
 * pattern's defining, measured 28px geometry. Pillbtn builds
 * its own row and forks none of their markup or CSS.
 * SEMANTICS — pills are the host's OWN native elements (the
 * c29-profstats / c29-tilegrid precedent, argued against the
 * c29-formrow / c29-bannerrow built-button precedent): where
 * a pill lands is INF in the teardown, and the row admits
 * heterogeneous doors — the completion pills are ACTIONS (an
 * editor opens), the orders pill is a DESTINATION — so the
 * host chooses the element: an <a> pill navigates by its
 * href, a <button type="button"> pill fires the host's
 * handler, a plain-element pill is a display pill. Formrow and
 * bannerrow build a <button> core because their host markup
 * carries no native control and their activation is uniformly
 * host-routed; here a built core would need a JS build for a
 * pure layout pattern, would kill the doors with scripting
 * disabled, and would have to invent an event for destinations
 * the corpus leaves INF. The component adds no listener, no
 * event, no routing and no key handler; the keyboard contract
 * is the platform's native one (a link activates on Enter; a
 * button on Enter and Space), and a pill's accessible name is
 * its own text in DOM order — glyph, then label. With
 * profhead and profstats also CSS-only, the whole profile
 * header trio renders and navigates JS-dead.
 * THE TAP FLOOR, ADJUDICATED AT R35 (2026-10-27): the 28px
 * VISUAL height stands — it is the corpus's defining,
 * twice-measured geometry (the pattern IS the small pill)
 * and it passes WCAG 2.5.8 AA's 24px target minimum — and
 * the component now carries the house 44px floor where it
 * costs the pattern nothing: an invisible ::after hit
 * extension (inset -8px vertical, no horizontal extension)
 * makes every pill's EFFECTIVE target 44px tall without
 * moving a painted pixel (hit-tested at R35: visual 28,
 * effective span 44, both engines). One disclosure rides
 * with the mechanism: pill rows sit 14px apart, so two
 * stacked pills' extensions overlap by 2px and a tap in
 * that 2px band resolves to the topmost pill. The earlier
 * framing (the question simply "inherited by Round 35")
 * is discharged by this ruling; the registry says the same.
 * TREATMENTS, ALL TOKENS, BOTH THEMES (the corpus frame is
 * light only; dark is house rendering through [data-theme]):
 *   - filled (default): --sunken ground, --ink label — the
 *     corpus's light-grey / black pair translated to tokens (the
 *     c29-flowfoot Skip precedent: the same corpus grey pill,
 *     the same translation).
 *   - outline (data-tone="outline" on the pill): transparent
 *     ground — the pill takes its host's ground, as the
 *     corpus's pill takes the white page — with a 1px solid
 *     --edge border (the c29-chip border precedent: --edge is
 *     the light-hairline token, the corpus's border class).
 *   - primary (data-tone="primary" on the pill): HOUSE
 *     DESIGN, labelled — the --ink ground / --bg label
 *     inversion pair (the c29-chip selected / c29-flowfoot
 *     Next-enabled pair), built for the VISITOR profile's
 *     Follow action: the visitor state is NOT IN CORPUS, and
 *     Round 5's STATES section licenses canon to design the
 *     Follow / Message variants as labelled house design (a
 *     Message pill rides the filled or outline treatment, as
 *     the host chooses).
 * GLYPH: a pill may carry ONE glyph part before its label —
 * a text glyph ("+"), an inline SVG, or an <img> — in a 16px
 * flex-none box (HOUSE DESIGN size: the teardown names the
 * glyphs' presence, never their pixel size). The component
 * generates no glyph and no "+": the plus is host content,
 * riding in the glyph part or as the label's own first
 * characters, as the host writes it.
 * HOVER + PRESSED are HOUSE DESIGN (a still proves neither):
 * hover deepens the tone's ground on door pills; while a door
 * pill is :active it scales to .97 (the .t-btn pressed
 * precedent); both ride the motion tokens and the section's
 * own reduced-motion rule removes the transition.
 * DISABLED is HOUSE DESIGN, for button pills only: a native
 * disabled button pill dims to .55 and is inert and
 * unfocusable by the platform's machinery. Link and plain
 * pills have no component disabled state — a host makes them
 * inert by its chosen element's native means, outside this
 * component. There is NO toggle, selected or pressed STATE:
 * these pills are actions, not toggles — no aria-pressed is
 * set by the component, and a Follow pill's followed state is
 * the host swapping its label or treatment, never a component
 * state. There is no loading state.
 * CSS-ONLY, stated honestly: no tide.js block, no built
 * stamp, no event, no method, no attribute of its own on the
 * host. JS-dead is not a degraded mode: the rows render
 * identically with scripting disabled and their native doors
 * stay alive in both states.
 * ================================================================ */
@layer c29.components {
c29-pillbtn { display: flex; flex-wrap: wrap; gap: 14px 10px;
  align-items: flex-start; box-sizing: border-box; min-width: 0;
  margin: 0; padding: 0; color: var(--ink); }
c29-pillbtn + c29-pillbtn { margin-top: 14px; }
c29-pillbtn [data-pill] { display: inline-flex; align-items: center;
  justify-content: center; gap: 6px;
  box-sizing: border-box; min-width: 0; max-width: 100%;
  min-height: 28px; margin: 0; padding: 0 16px; border: 0;
  border-radius: 999px; background: var(--sunken); color: var(--ink);
  font: 600 15px/20px var(--font-ui); text-align: center;
  text-decoration: none; overflow-wrap: anywhere;
  -webkit-tap-highlight-color: transparent; touch-action: manipulation;
  transition: transform var(--d1) var(--ease),
    background-color var(--d2) var(--ease), color var(--d2) var(--ease),
    box-shadow var(--d2) var(--ease); }
c29-pillbtn [data-pill] { position: relative; }
c29-pillbtn [data-pill]::after { content: ""; position: absolute;
  inset: -8px 0; border-radius: inherit; }
c29-pillbtn [data-pill]:is(a, button) { cursor: pointer; }
c29-pillbtn [data-pill][data-tone="outline"] { background: transparent;
  border: 1px solid var(--edge); }
c29-pillbtn [data-pill][data-tone="primary"] { background: var(--ink);
  color: var(--bg); }
c29-pillbtn [data-pill]:is(a, button):hover { background: color-mix(in srgb, var(--sunken) 88%, var(--ink)); }
c29-pillbtn [data-pill][data-tone="outline"]:is(a, button):hover { background: var(--wash); }
c29-pillbtn [data-pill][data-tone="primary"]:is(a, button):hover { background: color-mix(in srgb, var(--ink) 88%, var(--bg)); }
c29-pillbtn [data-pill]:is(a, button):focus-visible { box-shadow: var(--focus); outline: none; }
c29-pillbtn [data-pill]:is(a, button):active { transform: scale(.97); }
c29-pillbtn [data-pill]:disabled { opacity: .55; cursor: default; }
c29-pillbtn [data-pill-glyph] { flex: none; display: inline-flex;
  align-items: center; justify-content: center;
  width: 16px; height: 16px; font-size: 15px; line-height: 1; }
c29-pillbtn [data-pill-glyph] svg,
c29-pillbtn [data-pill-glyph] img { width: 16px; height: 16px; display: block; }
html[data-motion="reduced"] c29-pillbtn [data-pill] { transition: none; }
}
/* ---------- end pillbtn (c29-pillbtn) ---------- */

/* ================================================================
 * TABROW — c29-tabrow (TikTok Pattern Library Round 21,
 * 2026-10-27; JS — tide.js owns the tablist semantics.
 * Source spec: Round 5 teardown, corpus frame 03, the profile's
 * content tab row.)
 * THE PATTERN, from the teardown (adversary-confirmed on its
 * confirmation round): directly under the action pills, SIX
 * icon slots spread full width (grid-with-caret, magic, lock,
 * repost, bookmark, heart) on a row ≈44pt tall; glyph ink
 * ≈16–23pt wide × ≈18–20pt tall (pixel-measured — v1's "24pt
 * icons" described the slot class, not the glyphs); the first
 * slot carries its own dropdown caret. The active slot's icon
 * is BLACK and the rest are GREY — THERE IS NO INDICATOR BAR
 * beneath the active slot (v1 invented one; the frame shows
 * plain white beneath it) and NO HAIRLINE above the row (the
 * white is uninterrupted; a hairline exists only above the
 * bottom nav). Selected is therefore told by INK ALONE in
 * canon: the selected tab paints in --ink, every unselected
 * tab in --ink-mute, and this section declares no border, no
 * background and no pseudo-element on the host or on any tab
 * in any state — there is no bar element to slide, and the
 * row's motion is a colour change on the glyph itself (the
 * teardown's INF tab-switch reading: no bar to slide, content
 * cross-fades — the cross-fade is the host's panel work).
 * SEMANTICS — the component OWNS the tablist (argued in the
 * registry against the c29-profstats native-door precedent):
 * tabs SWITCH content in place and imply a panels
 * relationship, which a door row does not — so tide.js sets
 * role=tablist on the host and role=tab, aria-selected and a
 * roving tabindex on the host's native <button> tabs, wires
 * click and ArrowLeft / ArrowRight / Home / End with
 * automatic activation, and fires tab-change for the host to
 * swap its panel. The component does NOT own panels: it
 * creates, shows, hides and labels no panel, and sets no
 * role=tabpanel anywhere; a tab may name its panel with
 * data-panel="<id>", which the component copies to the tab's
 * aria-controls — the only panel wiring it performs.
 * NOT c29-seg: seg is a segmented OPTION row — a sliding
 * thumb over labelled options carrying aria-pressed, whose
 * value the host reads like a form field; tabrow has no
 * thumb, no track, no pressed state and no labels — it is a
 * row of icon tabs carrying aria-selected, switching the
 * content beneath it. NOT the bottom nav (Round 22's
 * component): that is the app-level five-slot bar; tabrow is
 * a within-surface content switch. NOT c29-chip /
 * c29-chipwall: chips are selectable toggle pills in a
 * ragged flow (aria-pressed, many may be on); a tabrow holds
 * exactly ONE selected tab at all times.
 * GEOMETRY: the host is a flex row 44px tall with no padding,
 * no margin and no background of its own; each tab flexes
 * 1 1 0 (equal slots spread full width, as the frame's six
 * slots do; the count is the host's declaration, never fixed
 * at six), so at a 390px host six tabs measure 65px per slot.
 * The row height meets the house 44px tap floor exactly
 * (--tap) — no tap-floor tension rides this component.
 * GLYPHS: a tab's glyph is the host's own inline SVG (or img)
 * inside its data-tab-glyph part, painted by currentColor so
 * the ink split above is the tab's colour itself; the
 * component sizes an SVG glyph to 20px tall with its width
 * auto from its own viewBox (max 24px), so host glyphs land
 * inside the teardown's measured ≈16–23 × ≈18–20pt ink band
 * when drawn to it — the rig's fixture glyphs are drawn to
 * 16–23 wide × 20 tall and probe-measured. The caret part
 * (data-tab-caret, the first tab's in the corpus) rides after
 * the glyph in the same ink, its SVG 6px tall.
 * CHROME, STATED COMPLETELY: a tab is a native button whose
 * native border is removed with border: 0 and whose
 * background is set to transparent (the row paints no ground
 * of its own); focus paints an inset 0 0 0 2px ring in
 * --focus-ring on :focus-visible (the c29-profstats
 * precedent — an inset ring on the tab itself, never a
 * divider or a bar). HOVER is HOUSE DESIGN (a still proves
 * none): an unselected tab deepens toward --ink-soft; the
 * selected tab's ink does not change. DISABLED is HOUSE
 * DESIGN (the corpus proves no disabled tab): a native
 * disabled button tab dims to .55, is skipped by arrow
 * traversal and cannot be selected. Colour changes ride
 * --d2 on --ease, and the section's own reduced-motion rule
 * removes the transition.
 * EXTENSION — THE LABELLED VARIANT (2026-10-28, on the
 * Principal's ruling "Extend the Tide components, deploy
 * Tide, then swap"; registry contract 13): while the host
 * declares the labels attribute, a tab may carry a
 * data-tab-label part — its VISIBLE text label. The variant
 * changes layout only: tabs size to their content
 * (flex 0 1 auto, 10px over and 16px at the sides, each at
 * least 44px tall) on a wrapping row of min-height 44px, so
 * a labelled row never widens its page, and a label that
 * cannot fit wraps inside its own tab (min-width 0,
 * overflow-wrap: anywhere — the Squish rule for unbreakable
 * tokens; a file name is the first consumer's case).
 * SELECTION IS STILL TOLD BY INK ALONE: the variant adds no
 * indicator, border, background or pseudo-element in any
 * state — the base aria-selected colour split paints the
 * tab and therefore its label, and the selected tab's
 * label steps from 600 to 700 weight. While labels is
 * declared, the label part is the tab's accessible name;
 * the tablist semantics above are untouched (roles, roving
 * tabindex, automatic activation, tab-change — tide.js
 * reads the same [data-tab] buttons and needs no variant
 * code). Absent the attribute, the row is the corpus icon
 * row, unchanged.
 * ================================================================ */
@layer c29.components {
c29-tabrow { display: flex; box-sizing: border-box; min-width: 0;
  height: 44px; margin: 0; padding: 0; color: var(--ink); }
c29-tabrow [data-tab] { flex: 1 1 0; display: flex; align-items: center;
  justify-content: center; gap: 3px;
  box-sizing: border-box; min-width: 0; max-width: 100%;
  height: 44px; margin: 0; padding: 0; border: 0;
  background: transparent; color: var(--ink-mute);
  font: inherit; cursor: pointer;
  -webkit-tap-highlight-color: transparent; touch-action: manipulation;
  transition: color var(--d2) var(--ease), box-shadow var(--d2) var(--ease); }
c29-tabrow [data-tab][aria-selected="true"] { color: var(--ink); }
c29-tabrow [data-tab]:not([aria-selected="true"]):not(:disabled):hover { color: var(--ink-soft); }
c29-tabrow [data-tab]:focus-visible { box-shadow: inset 0 0 0 2px var(--focus-ring); outline: none; }
c29-tabrow [data-tab]:disabled { opacity: .55; cursor: default; }
c29-tabrow [data-tab-glyph] { flex: none; display: inline-flex;
  align-items: center; justify-content: center; line-height: 1; }
c29-tabrow [data-tab-glyph] svg,
c29-tabrow [data-tab-glyph] img { height: 20px; width: auto; max-width: 24px; display: block; }
c29-tabrow [data-tab-caret] { flex: none; display: inline-flex;
  align-items: center; justify-content: center; line-height: 1; }
c29-tabrow [data-tab-caret] svg { height: 6px; width: auto; max-width: 12px; display: block; }
c29-tabrow[labels] { flex-wrap: wrap; height: auto; min-height: 44px; }
c29-tabrow[labels] [data-tab] { flex: 0 1 auto; height: auto; min-height: 44px;
  padding: 10px 16px; }
c29-tabrow [data-tab-label] { flex: none; display: inline-block;
  box-sizing: border-box; min-width: 0; max-width: 100%;
  overflow-wrap: anywhere; text-align: center;
  font-size: 14px; line-height: 20px; font-weight: 600; }
c29-tabrow[labels] [data-tab][aria-selected="true"] [data-tab-label] { font-weight: 700; }
html[data-motion="reduced"] c29-tabrow [data-tab] { transition: none; }
}
/* ---------- end tabrow (c29-tabrow) ---------- */
/* ================================================================
 * BOTTOMNAV — c29-bottomnav (TikTok Pattern Library Round 22,
 * 2026-10-27 — canonized from the Round 5 teardown, corpus
 * frame 03, the profile screen's app-level bottom bar.)
 *
 * THE BAR: five slots spread full width on a white bar with a
 * hairline on top and safe-area padding below (the teardown:
 * height ≈50pt + safe area; the hairline exists ONLY above
 * this bar — the tab row above it has none). Slots in the
 * frame: Home, Friends, Create, Inbox, Profile. Labels ≈10pt
 * under ≈24pt icons.
 *
 * INK — THE CORRECTED READING BINDS: the INACTIVE slots
 * (Home, Friends, Inbox — icons AND labels) render BLACK, not
 * grey (the teardown's v1 grey reading was struck on its
 * confirmation round). Canon therefore paints EVERY slot in
 * --ink, current or not. The current slot (Profile in the
 * frame) is distinguished by GLYPH FORM alone: a FILLED
 * silhouette icon where the inactive slots carry black LINE
 * icons. No colour split, no indicator, no weight change.
 *
 * SEMANTICS — CSS-ONLY, HOST-NATIVE DOORS, argued in the
 * registry (contract 2): a bottom-nav slot NAVIGATES — it is
 * a door to a destination, not a tab that swaps panels in
 * place — so the c29-tabrow JS tablist precedent does NOT
 * carry and the c29-profstats / c29-tilegrid native-door
 * precedent does: each slot is the host's own native <a> or
 * <button type="button">, the component adds no JS, no
 * listener, no event and no routing, and the doors work
 * identically with scripting disabled. WHAT THE COMPONENT
 * DOES NOT DO, stated plainly: it does not route (the host's
 * links navigate by their own hrefs; its buttons fire the
 * host's own handlers), it never sets or moves the current
 * marking, and it does not own the centre Create action's
 * behaviour (what Create opens is the host's business — the
 * component paints its construction only).
 *
 * CURRENT STATE IS THE HOST'S: the host/router marks the
 * current destination's slot with aria-current="page" (the
 * host declares it in markup, and moves it when its router
 * navigates — exactly as a server-rendered page would). The
 * CSS keys the glyph swap on that attribute: a slot declaring
 * BOTH glyph parts (data-nav-glyph, the line drawing, and
 * data-nav-glyph-on, the filled silhouette) shows the line
 * drawing normally and the filled silhouette while it
 * carries aria-current (any value other than "false"); a
 * slot declaring only the line glyph keeps it in both
 * states. Both glyphs are the host's own drawings — the
 * component generates no glyph and cannot turn a line
 * drawing into a silhouette.
 *
 * CREATE, THE CENTRE SLOT: a black rounded rect CORE
 * ≈37×29pt, radius ≈10pt, with cyan and pink offset layers
 * peeking ≈4.3pt left/right — total footprint ≈46pt wide
 * (the teardown's confirmation corrected v1's conflation of
 * footprint with core). Canon: the data-nav-create part is
 * the 46×29px footprint; its ::before / ::after are the two
 * offset layers (37×29px, radius 10px, on the --nav-create-a
 * / --nav-create-b tokens, filed in tokens.css with this
 * round and deliberately not theme-flipped — the mark does
 * not flip); the data-nav-create-core part is the 37×29px
 * core centred over them, so each layer peeks 4.5px (canon's
 * rounding of the measured ≈4.3pt), its ground --ink and its
 * glyph --bg (the house translation of the corpus's black
 * core with a white plus; in dark the pair inverts on the
 * same tokens). The Create slot carries NO visible label in
 * the frame — its name is the host's aria-label on the slot.
 *
 * BADGE DISCIPLINE: the frame badges THIS bar exactly once —
 * a red circular count "1" on the Inbox slot (the teardown
 * struck v1's "Create is the only coloured object" on
 * exactly this evidence). The data-nav-badge part is that
 * treatment and no other: the designed --bad / --on-bad
 * pair (the c29-profhead count-badge precedent), a pill of
 * min-width 16px × 16px, numeral 700 at 11px, riding at its
 * slot's glyph top-right. Numeric counts only, host-declared
 * where the host's own pattern carries one — the component
 * generates no badge and no count, and the rig badges the
 * Inbox slot only, as the frame does. (The white "46" count
 * TAG in the frame rides the single circular photo thumbnail
 * in the header's TOP UTILITY ROW — profhead's pattern, not
 * this bar's.)
 *
 * CHROME, STATED COMPLETELY: the host's ONE declared border
 * is its top hairline, 1px solid var(--edge); its ground is
 * --raised. A slot's background is transparent (it takes
 * the bar's ground); a <button> slot's native border is
 * removed with border: 0 (button slots compute a 0px border
 * in every state — the exact mechanism, per the Round 17
 * lesson) and link slots take text-decoration none. The
 * create part's ::before / ::after layers above are the
 * component's only pseudo-elements; no other pseudo-element
 * is declared on the host or any part. FOCUS: a slot on
 * :focus-visible takes an inset ring of 0 0 0 2px in
 * --focus-ring with outline none (the family precedent — an
 * inset ring on the slot itself). HOVER and PRESSED are
 * HOUSE DESIGN (a still proves neither): a slot's ground
 * washes to --wash on hover and --wash-2 while :active.
 * POSITION IS THE HOST'S: the component declares no
 * position — a host pins the bar (fixed, sticky, or in flow
 * at its surface's foot, as the rig composes it); the
 * safe-area padding below the slots is the component's own.
 * ================================================================ */
@layer c29.components {
c29-bottomnav { display: flex; box-sizing: border-box; min-width: 0;
  width: 100%; margin: 0; padding: 0 0 var(--safe-b);
  background: var(--raised); color: var(--ink);
  border-top: 1px solid var(--edge); }
c29-bottomnav [data-nav-slot] { position: relative; flex: 1 1 0;
  display: flex; flex-direction: column; align-items: center;
  justify-content: center; gap: 2px;
  box-sizing: border-box; min-width: 0; max-width: 100%;
  min-height: 50px; margin: 0; padding: 4px 2px; border: 0;
  background: transparent; color: var(--ink);
  font: inherit; text-decoration: none; cursor: pointer;
  -webkit-tap-highlight-color: transparent; touch-action: manipulation;
  transition: background var(--d2) var(--ease); }
c29-bottomnav [data-nav-slot]:hover { background: var(--wash); }
c29-bottomnav [data-nav-slot]:active { background: var(--wash-2); }
c29-bottomnav [data-nav-slot]:focus-visible { box-shadow: inset 0 0 0 2px var(--focus-ring); outline: none; }
c29-bottomnav [data-nav-glyph],
c29-bottomnav [data-nav-glyph-on] { flex: none; display: inline-flex;
  align-items: center; justify-content: center; height: 24px; line-height: 1; }
c29-bottomnav [data-nav-glyph-on] { display: none; }
c29-bottomnav [data-nav-slot][aria-current]:not([aria-current="false"]) [data-nav-glyph-on] { display: inline-flex; }
c29-bottomnav [data-nav-slot][aria-current]:not([aria-current="false"]):has([data-nav-glyph-on]) [data-nav-glyph] { display: none; }
c29-bottomnav [data-nav-glyph] svg,
c29-bottomnav [data-nav-glyph] img,
c29-bottomnav [data-nav-glyph-on] svg,
c29-bottomnav [data-nav-glyph-on] img { height: 24px; width: auto; max-width: 28px; display: block; }
c29-bottomnav [data-nav-label] { flex: 0 1 auto; min-width: 0; max-width: 100%;
  font-size: 10px; line-height: 12px; font-weight: 500;
  text-align: center; overflow-wrap: anywhere; }
c29-bottomnav [data-nav-create] { position: relative; flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  width: 46px; height: 29px; }
c29-bottomnav [data-nav-create]::before,
c29-bottomnav [data-nav-create]::after { content: ""; position: absolute;
  top: 0; width: 37px; height: 29px; border-radius: 10px; }
c29-bottomnav [data-nav-create]::before { left: 0; background: var(--nav-create-a); }
c29-bottomnav [data-nav-create]::after { right: 0; background: var(--nav-create-b); }
c29-bottomnav [data-nav-create-core] { position: relative; z-index: 1;
  display: inline-flex; align-items: center; justify-content: center;
  width: 37px; height: 29px; border-radius: 10px;
  background: var(--ink); color: var(--bg); line-height: 1; }
c29-bottomnav [data-nav-create-core] svg,
c29-bottomnav [data-nav-create-core] img { height: 16px; width: auto; max-width: 16px; display: block; }
c29-bottomnav [data-nav-badge] { position: absolute; top: 3px;
  left: calc(50% + 3px); z-index: 2;
  box-sizing: border-box; min-width: 16px; height: 16px; padding: 0 4px;
  border-radius: 999px; background: var(--bad); color: var(--on-bad);
  font-size: 11px; line-height: 16px; font-weight: 700;
  text-align: center; white-space: nowrap; }
html[data-motion="reduced"] c29-bottomnav [data-nav-slot] { transition: none; }
}
/* ---------- end bottomnav (c29-bottomnav) ---------- */

/* ================================================================
 * HISTCARD — c29-histcard (TikTok Pattern Library Round 23,
 * 2026-10-27; CSS-ONLY — no tide.js block. Source spec: Round 6
 * teardown, corpus frame 05, the account-history screen.)
 * THE PATTERN, from the teardown (pixel-measured, its node and
 * fill spot-verified in Round 6's re-check): date groups — a
 * relative group ("Yesterday") then dated groups — each group
 * a bold ≈20pt header left over a white card (radius ≈12pt,
 * inset 16pt from the screen edges) holding history entries.
 * Entry counts proven in the frame: 1, 1 and 2 — and only the
 * two-entry card shows the timeline connector. An entry is a
 * circular NODE ≈20pt in diameter (spot-measured 19.2pt), fill
 * sampled at exactly RGB 112 with a WHITE glyph, right of it a
 * title ≈17pt semibold, a body ≈15pt grey wrapping up to 3
 * lines, and a timestamp ≈15pt grey beneath. Entry vertical
 * padding ≈16pt; node-to-text gap ≈12pt. The connector is a
 * vertical hairline (1pt, the frame's rgba(0,0,0,0.12) class)
 * descending from the first entry's node to the second's at
 * the node's centre x. Every entry is neutral in colour — a
 * security log that does not editorialize.
 * BUILDER'S FRAME RE-CHECK (this round, pixel scan at the
 * frame's 3.02x scale, filed in the round report): card inset
 * 15.9-16.2pt; node 19.8pt across, its left edge 11.9pt inside
 * the card; node top 16.2pt below the card top in BOTH entries
 * of the two-entry card; title text begins ≈44pt from the
 * card's left edge (12 entry inset + 20 node + 12 gap); the
 * thread rides x = node centre exactly. The header-to-card
 * visible gap measures ≈10-13pt and the card-to-next-header
 * distance ≈21pt to the glyph top; Round 6's "≈24pt above its
 * card" names no gap in the frame (the verifier's Round 23
 * scan at 3.023px/pt: glyph bottom 12.6-12.9pt above its card,
 * previous card's bottom 20.5pt above the next glyph top,
 * total empty card-to-card interval 45.7pt, glyph top to card
 * top 25.1pt — the probable origin of the 24 figure).
 * FIX ROUND 23 (verifier E1): the first build carried the
 * glyph measurements into box values one-for-one (24px
 * stacking + 24px line box + 12px gap = 60px, rendering glyph
 * gaps ≈28px / ≈17px) — too loose. The corrected rhythm is a
 * 16px sibling stacking margin + the header's 24px line box
 * (700 20px/24px, unchanged) + a 7px header-to-card margin =
 * a 47px box interval in the rig, rendering glyph gaps of
 * ≈20px (card bottom to next glyph top) and ≈12px (glyph
 * bottom to card top) by glyph pixel scan in both engines —
 * within ≈1px of the frame's 20.5pt / 12.6-12.9pt. The rig
 * box total (47px) sits above the frame's 45.7pt by the rig
 * font's glyph height (15px at 20px, pixel-scanned) against
 * the frame font's 12.6pt: the box total equals the two glyph
 * gaps plus the glyph height in any font, so the glyph gaps
 * are the frame-faithful quantities and the total follows
 * the font. The reading is stated in the registry.
 * SCOPE — THE R24 BOUNDARY (registry contract 1): this
 * component owns the GROUPED CARD and its entry rows, with the
 * node and its thread segments as entry parts IN SITU. Round
 * 24's c29-timeline owns the rail as a reusable standalone —
 * its own geometry contract, usable outside history cards.
 * No general rail API is built here: the thread below is one
 * pseudo-element segment per non-last entry, scoped to this
 * component's entries, and nothing outside a histcard card
 * can consume it.
 * THE CARD-CHROME RULING (R17-style, registry contract 2):
 * this card does NOT consume c29-formgroup. The two cards
 * share token VALUES (ground --raised, radius 12px, the
 * --hair ring) but they are different surfaces: formgroup
 * insets 12px, its label is 600 15px in --ink-mute on an 8px
 * gap with 28px stacking, and — decisively — formgroup admits
 * ONLY c29-formrow children and clears any other child on
 * build, so a history entry placed inside it would be
 * destroyed, not composed. This card insets 16px, its header
 * is 700 20px in --ink, and its content is node entries, not
 * form rows. Histcard therefore owns its own chrome here on
 * the same tokens — shared tokens, no forked values, no
 * consumed wrapper that cannot hold the content.
 * SEMANTICS — RECORDS, NOT DOORS (registry contract 3): the
 * frame's entries carry no door affordance — no chevron, no
 * wash, no pressed state — and Round 6 proves no collapse and
 * no motion ("groups don't visibly collapse"). Entries are
 * host-authored records (any element the host chooses, in
 * practice an article or div), the header is the host's own
 * heading element carrying the part attribute, and the whole
 * component is CSS-only: no tide.js block, no built stamp, no
 * listener, no event, no method. A host that needs an entry
 * detail view composes its own link inside the entry's text
 * stack, outside this component's parts. CSS-only means
 * JS-dead is not a degraded mode: geometry, thread and type
 * render identically with scripting disabled — there is no
 * build step to lose.
 * THE THREAD, AS A CONSTRUCTION: each entry that is NOT its
 * card's last entry carries one ::after segment on the entry
 * element itself: 1px wide on --hist-thread, its left edge at
 * the node centre minus half its width (entry left inset 12px
 * + node radius 10px - 0.5px = 21.5px from the entry's left
 * edge, the entry spanning the card's full width), running
 * from the node's bottom (entry top padding 16px + node top
 * offset 1px + node 20px = 37px) to the NEXT entry's node top
 * (17px past this entry's bottom edge: the next entry's 16px
 * top padding + the node's 1px top offset). The card clips
 * nothing between entries (the entries carry no overflow), so
 * the segments meet the next node exactly — continuity is a
 * measurement, probe-proven, not an assertion. The last entry
 * of a card and every entry of a single-entry card declare no
 * segment: the frame proves the connector ONLY between
 * entries of one card, never below the last entry, never
 * across cards. This ::after on the non-last entries is the
 * component's only pseudo-element.
 * CHROME, STATED COMPLETELY, SCOPED PER ELEMENT: the HOST
 * declares no border, no padding beyond its 16px horizontal
 * inset (0 16px, border-box), and no position. The CARD part
 * carries background var(--raised), border-radius 12px,
 * box-shadow var(--hair) and overflow hidden, and the card
 * part itself declares no border and no padding (each entry
 * carries its own padding). A card part with no children at
 * all is display none (a bare empty card is a host error made
 * invisible, never a ring sliver); a group whose period is
 * empty renders the host's data-hist-empty part inside its
 * card instead. The header part declares no border. Reduced
 * motion: the component declares no transition or animation
 * anywhere, so there is nothing to gate.
 * TOKENS ONLY: no colour literal exists in this section's
 * rules (the literals above are corpus samples quoted in this
 * comment, the house precedent); the node's subject fill
 * enters through --hist-node and the thread through
 * --hist-thread (both tokens.css, Round 23); the glyph rides
 * --on-image (the not-theme-flipped white); dark flips the
 * surface tokens through [data-theme] while the node fill
 * deliberately does not flip (the tokens.css note).
 * ================================================================ */
@layer c29.components {
c29-histcard { display: block; box-sizing: border-box; min-width: 0;
  padding: 0 16px; margin: 0; color: var(--ink); }
c29-histcard + c29-histcard { margin-top: 16px; }
c29-histcard [data-hist-head] { display: block; min-width: 0;
  max-width: 100%; overflow-wrap: anywhere; margin: 0 0 7px;
  font: 700 20px/24px var(--font-ui); color: var(--ink); }
c29-histcard [data-hist-card] { display: block; box-sizing: border-box;
  min-width: 0; background: var(--raised); border-radius: 12px;
  box-shadow: var(--hair); overflow: hidden; }
c29-histcard [data-hist-card]:empty { display: none; }
c29-histcard [data-hist-entry] { position: relative; display: flex;
  align-items: flex-start; gap: 12px;
  box-sizing: border-box; min-width: 0; margin: 0;
  padding: 16px 16px 16px 12px; color: var(--ink); }
c29-histcard [data-hist-entry]:not(:last-child)::after { content: "";
  position: absolute; left: 21.5px; top: 37px; bottom: -17px;
  width: var(--rail-thread-width); background: var(--hist-thread); }
c29-histcard [data-hist-node] { flex: none; display: inline-flex;
  align-items: center; justify-content: center;
  width: var(--rail-node-size); height: var(--rail-node-size); margin-top: 1px; border-radius: 999px;
  background: var(--hist-node); color: var(--on-image); line-height: 1; }
c29-histcard [data-hist-node] svg,
c29-histcard [data-hist-node] img { height: 12px; width: auto;
  max-width: 12px; display: block; }
c29-histcard [data-hist-main] { flex: 1 1 auto; min-width: 0;
  display: flex; flex-direction: column; gap: 4px; }
c29-histcard [data-hist-title] { display: block; min-width: 0;
  overflow-wrap: anywhere; font: 600 17px/22px var(--font-ui);
  color: var(--ink); }
c29-histcard [data-hist-detail] { display: block; min-width: 0;
  overflow-wrap: anywhere; font: 400 15px/21px var(--font-ui);
  color: var(--ink-mute); }
c29-histcard [data-hist-time] { display: block; min-width: 0;
  overflow-wrap: anywhere; margin-top: 4px;
  font: 400 15px/20px var(--font-ui); color: var(--ink-mute);
  font-variant-numeric: tabular-nums; }
c29-histcard [data-hist-empty] { display: block; min-width: 0;
  overflow-wrap: anywhere; padding: 16px 16px 16px 12px;
  font: 400 15px/21px var(--font-ui); color: var(--ink-mute); }
}
/* ---------- end histcard (c29-histcard) ---------- */

@layer c29.components {
/* ---------- video preview (c29-videopreview) ----------
   Pacesetter 250 Round 11 (Media Core I). Paint from canon
   tokens only, both themes; the label row is the c29-prose
   row grammar — the label is the defended prose column
   (.c29-prose-main: flex 1 1 45%, min-width 160px) and the
   state word + time readout are shrinkable side furniture
   (.c29-prose-side: flex 0 1 auto, min-width 0) that wrap
   BELOW the label, never crush it (Squish Law). Long names
   wrap inside the defended column (overflow-wrap: anywhere).
   The transport is inline on the picture surface only —
   never docked, never a mini bar. */
c29-videopreview { display: block; background: var(--surface); border-radius: var(--corner);
  box-shadow: var(--hair); padding: var(--s4); }
c29-videopreview .vp-frame { position: relative; overflow: hidden;
  border-radius: calc(var(--corner) - 6px); background: var(--sunken);
  aspect-ratio: 16 / 9; transition: background-color var(--d2) var(--ease); }
c29-videopreview .vp-media { position: absolute; inset: 0; }
c29-videopreview .vp-media video { display: block; width: 100%; height: 100%;
  object-fit: contain; background: var(--sunken); }
c29-videopreview .vp-line { position: absolute; inset: 0; display: grid; place-items: center;
  padding: var(--s4); text-align: center; font: 400 15px/21px var(--font-ui); color: var(--ink-mute); }
c29-videopreview .vp-line[hidden] { display: none; }
c29-videopreview .vp-errline { color: var(--bad-text); font-weight: 600; }
c29-videopreview .vp-row { margin-top: var(--s3); }
c29-videopreview .vp-label { font: 600 15px/21px var(--font-ui); color: var(--ink);
  overflow-wrap: anywhere; }
c29-videopreview .vp-side { font: 400 13px/18px var(--font-ui); color: var(--ink-mute);
  font-variant-numeric: tabular-nums; }
c29-videopreview .vp-transport { display: flex; align-items: center; gap: var(--s3);
  margin-top: var(--s3); }
c29-videopreview .vp-transport[hidden] { display: none; }
c29-videopreview .vp-toggle { flex: none; width: 48px; height: 48px; min-width: 48px; min-height: 48px;
  display: grid; place-items: center; border: 0; border-radius: 50%; cursor: pointer;
  background: var(--accent); color: var(--on-accent); }
c29-videopreview .vp-toggle .t-icon { width: 24px; height: 24px; }
c29-videopreview .vp-toggle:active { background: var(--wash-2); color: var(--ink); }
c29-videopreview .vp-mute { flex: none; width: 44px; height: 44px; min-width: 44px; min-height: 44px;
  display: grid; place-items: center; border: 0; border-radius: 50%; cursor: pointer;
  background: var(--surface); box-shadow: var(--hair); color: var(--ink-soft); }
c29-videopreview .vp-mute[hidden] { display: none; }
c29-videopreview .vp-mute .t-icon { width: 22px; height: 22px; }
c29-videopreview .vp-toggle:focus-visible, c29-videopreview .vp-mute:focus-visible,
c29-videopreview .vp-range:focus-visible { outline: none; box-shadow: var(--focus); }
c29-videopreview .vp-range { --p: 0; flex: 1 1 auto; min-width: 0; min-height: 44px;
  -webkit-appearance: none; appearance: none; background: transparent; cursor: pointer; margin: 0; }
c29-videopreview .vp-range::-webkit-slider-runnable-track { height: 8px; border-radius: 99px;
  background: linear-gradient(to right, var(--accent) calc(var(--p) * 100%), var(--sunken) calc(var(--p) * 100%)); }
c29-videopreview .vp-range::-webkit-slider-thumb { -webkit-appearance: none; appearance: none;
  width: 20px; height: 20px; margin-top: -6px; border-radius: 50%;
  background: var(--accent); border: 0; box-shadow: var(--hair); }
c29-videopreview .vp-range::-moz-range-track { height: 8px; border-radius: 99px; background: var(--sunken); }
c29-videopreview .vp-range::-moz-range-progress { height: 8px; border-radius: 99px; background: var(--accent); }
c29-videopreview .vp-range::-moz-range-thumb { width: 18px; height: 18px; border-radius: 50%;
  background: var(--accent); border: 0; }
c29-videopreview .vp-toggle:disabled, c29-videopreview .vp-mute:disabled { cursor: default;
  background: var(--sunken); color: var(--ink-mute); box-shadow: none; }
c29-videopreview .vp-range:disabled { cursor: default; }
@media (prefers-reduced-motion: reduce) {
  c29-videopreview .vp-frame, c29-videopreview .vp-toggle, c29-videopreview .vp-mute { transition: none !important; }
}
}
/* ---------- end video preview (c29-videopreview) ---------- */

/* ================================================================
 * TIMELINE — c29-timeline (TikTok Pattern Library Round 24,
 * 2026-10-27; CSS-ONLY — no tide.js block. Source spec: Round 6
 * teardown, corpus frame 05, as corrected and re-measured by the
 * Round 23 verifier: node 19.85pt across at exactly RGB 112,
 * thread ≈1pt wide at the node's centre x, thread only between
 * entries, never after the last.)
 * THE PATTERN: a standalone vertical rail — node markers
 * threaded on a continuous line, one slotted content row beside
 * each node. This is the rail OUTSIDE any card: no date header,
 * no card chrome, no grouped-card semantics. The host supplies
 * each row's content entire (any markup inside the row's main
 * part: prose, a label, a timestamp, a host link — the rail
 * styles none of it beyond the column's wrap defence);
 * timestamps and labels beside a node are HOST content, not
 * rail chrome.
 * ★ THE DUPLICATION RULING (registry contract 1): c29-histcard
 * already renders nodes + thread segments as entry parts IN
 * SITU inside a grouped history card. This component is NOT a
 * second implementation of that geometry: it is the standalone
 * rail for content that is NOT history entries (generic slotted
 * content beside nodes), and the geometry has ONE numeric home
 * shared by both components — tokens.css defines
 * --rail-node-size (20px), --rail-thread-width (1px),
 * --hist-node (the fill, exactly RGB 112) and --hist-thread
 * (the thread colour) once, and BOTH sections consume them
 * through var() (histcard's node size and thread width were
 * migrated onto the rail tokens in this round; its probes were
 * re-run green). What remains literal in each section is only
 * the DERIVED offsets, enumerated as sums in the contracts:
 * here left 9.5px = 0px row left inset + 10px node radius -
 * 0.5px half thread, top 37px = 16px row top padding + 1px
 * node offset + 20px node, bottom -17px = the next row's 16px
 * top padding + its node's 1px offset, past this row's bottom
 * edge. Histcard's own sums differ only in its entry inset
 * (left 21.5px = 12px + 10px - 0.5px); the quantities the sums
 * are built from are the shared tokens.
 * SCOPE: the rail owns the nodes, the thread segments and the
 * rows' alignment only. It owns no card, no header, no text
 * stack and no type of its own for row content. A dated,
 * grouped history stream uses c29-histcard; any other threaded
 * sequence (steps, deliveries, milestones, a plain record
 * list) uses this rail. Refactoring histcard onto this rail
 * wholesale was considered and refused in the registry: the
 * card's entries are card parts with card insets, and the
 * shared-token ruling already gives the geometry one home
 * without disturbing a proven component.
 * SEMANTICS — PRESENTATIONAL RECORDS (the histcard
 * precedent): the frame's threaded entries carry no door
 * affordance and Round 6 proves no motion, so a row is a
 * host-authored record (any element, in practice an article
 * or div), the component is CSS-only (no tide.js block, no
 * built stamp, no listener, no event, no method), and the
 * rail gives a row no cursor, no handler and no activation
 * of its own. A host that needs an action composes its own
 * link or button inside the row's main part, outside these
 * parts. CSS-only means JS-dead is not a degraded mode:
 * geometry and thread render identically with scripting
 * disabled — there is no build step to lose.
 * THE THREAD, AS A CONSTRUCTION: each row that is NOT the
 * host's last child row carries one ::after segment on the
 * row element itself: var(--rail-thread-width) wide on
 * var(--hist-thread), its left edge at 9.5px from the row's
 * left edge (the node's centre x: the row carries no left
 * inset, so the node sits at the row's left edge), running
 * from the node's bottom (row top padding 16px + node top
 * offset 1px + node size = 37px) to the NEXT row's node top
 * (17px past this row's bottom edge: the next row's 16px
 * top padding + the node's 1px offset). Segments therefore
 * meet the next node with no gap and no overlap at ANY row
 * height — tall rows stretch their segment by the same
 * construction (probe-measured across short, tall and
 * medium rows). The last row and every row of a single-row
 * rail declare no segment, and no segment ever leaves the
 * rail. This ::after on the non-last rows is the component's
 * only pseudo-element.
 * CHROME, STATED COMPLETELY, SCOPED PER ELEMENT: the HOST
 * element itself declares no border and no padding (a host
 * supplies its own inset outside the rail, or none); the
 * host element with no children at all is display none,
 * scoped to the host element itself (a bare empty rail
 * renders nothing). The ROW part is position relative
 * solely to anchor its own thread segment. The component
 * declares no z-index anywhere, and no transition or
 * animation on any part, so there is no motion to gate.
 * THE EMPTY RAIL: instead of rows, a host renders the
 * data-tl-empty part as its only child — a muted line on
 * the row's own vertical padding, with no node and no
 * thread anywhere in that rail.
 * TOKENS ONLY: no colour literal exists in this section's
 * rules (the literals above are corpus samples quoted in
 * this comment, the house precedent); the node's fill enters
 * through --hist-node, the thread colour through
 * --hist-thread, the node size through --rail-node-size and
 * the thread width through --rail-thread-width (all
 * tokens.css; the size/width pair is the Round 24 shared
 * rail pair), the glyph rides --on-image, and row text
 * inherits var(--ink) from the host part. Dark flips the
 * ink through [data-theme] while the node fill deliberately
 * does not flip (the tokens.css note).
 * ================================================================ */
@layer c29.components {
c29-timeline { display: block; box-sizing: border-box; min-width: 0;
  padding: 0; margin: 0; color: var(--ink); }
c29-timeline:empty { display: none; }
c29-timeline [data-tl-row] { position: relative; display: flex;
  align-items: flex-start; gap: 12px;
  box-sizing: border-box; min-width: 0; margin: 0;
  padding: 16px 0; color: var(--ink); }
c29-timeline [data-tl-row]:not(:last-child)::after { content: "";
  position: absolute; left: 9.5px; top: 37px; bottom: -17px;
  width: var(--rail-thread-width); background: var(--hist-thread); }
c29-timeline [data-tl-node] { flex: none; display: inline-flex;
  align-items: center; justify-content: center;
  width: var(--rail-node-size); height: var(--rail-node-size); margin-top: 1px; border-radius: 999px;
  background: var(--hist-node); color: var(--on-image); line-height: 1; }
c29-timeline [data-tl-node] svg,
c29-timeline [data-tl-node] img { height: 12px; width: auto;
  max-width: 12px; display: block; }
c29-timeline [data-tl-main] { flex: 1 1 auto; min-width: 0;
  display: block; overflow-wrap: anywhere; }
c29-timeline [data-tl-empty] { display: block; min-width: 0;
  overflow-wrap: anywhere; padding: 16px 0;
  font: 400 15px/21px var(--font-ui); color: var(--ink-mute); }
}
/* ---------- end timeline (c29-timeline) ---------- */

/* ================================================================
 * PINCOMPOSER — c29-pincomposer (TikTok Pattern Library Round 25,
 * 2026-10-27; JS: tide.js, PINCOMPOSER block. Source spec:
 * rounds/ROUND-25-CRITIC.md, from the Round 6 teardown Part C,
 * corpus frames 02 + 08.)
 * The pinned comment composer in its two corpus forms, switched
 * by the host's layout attribute (bar | stacked) — the keyboard
 * correlation is INF from two stills, so the host declares the
 * form; this section only paints the declared one.
 * BAR (frame 02): one row — avatar 38px, the field a full-radius
 * pill exactly 44px tall on --wash-2 (the token translation of
 * the frame's #F1F1F2 field ground, labelled), the host's action
 * buttons INSIDE the field at its right end (the field wrap is
 * the positioning context; the textarea's right padding reserves
 * exactly the actions' room — 8px field inset + 40px per action
 * + 2px gaps, enumerated in the registry contract), no toolbar
 * row and no send control anywhere in this form.
 * STACKED (frame 08): a field row (avatar, field min-height
 * 52px, radius 16px, on --wash — the translation of the frame's
 * #F6F6F6 EST ground) over a toolbar row: the same host actions
 * at the left, and at the right the SEND pill, 51x32px, radius
 * full, on --composer-send (the frame's sampled RGB 254,170,186)
 * with a white 16px up-arrow — present while the field is empty
 * because it is a natively DISABLED button there, its disabled
 * paint being exactly this pale pole. With text it enables on
 * --composer-send-active (HOUSE DESIGN — the active pole is NOT
 * IN CORPUS; the token is chosen for a >=4.5:1 white arrow).
 * THE ACTIONS' TWO HOMES: one .pc-actions container serves both
 * forms — in bar it is absolutely positioned inside the field
 * wrap; in stacked it is a static flex row inside the toolbar.
 * The buttons are the host's own nodes in both forms; switching
 * the layout attribute moves no node, it only re-parents the
 * container's box by CSS (flex order + position), so identity
 * and listeners survive the switch by construction.
 * GEOMETRY: host ground --bg with the hairline on --edge-soft
 * across the top (the bar rides a sheet's bottom edge, frame
 * 02); padding 8px 12px (bar) / 12px (stacked), the bottom
 * padding carrying the safe-area inset (the c29-flowfoot
 * precedent). PINNING IS HOST LAYOUT: this section declares no
 * position on the host — a c29-sheet footer slot or a host bar
 * pins it. Rows gap 8px; the field column is flex 1 1 auto with
 * min-width 0 and the textarea wraps inside it
 * (overflow-wrap:anywhere) — avatar and actions are flex none
 * (Squish Law). Action buttons are 40x40 (HOUSE DESIGN: the
 * corpus measures only their glyphs, at 19–22pt; the glyph is
 * sized 20px here and the target takes the house 40px floor).
 * FLEX BASIS 0, BOTH FIELD AND TEXTAREA: a textarea is a
 * replaced element whose intrinsic width (its cols width PLUS
 * its horizontal padding — and the bar form's in-field action
 * reservation is a wide right padding) otherwise becomes the
 * field's max-content contribution, so a host that sizes a
 * track by content (a grid column, a flex row) is blown wider
 * than its container by the composer's own reservation
 * (measured in Round 25: a 390px rig track sized to 421px
 * before this rule). Basis 0 on both flex items makes the
 * field's content-size contribution independent of the
 * reservation; the field still grows to fill by flex-grow.
 * The JS build ALSO bounds the textarea's intrinsic width
 * at the source (cols=1 — see the tide.js PINCOMPOSER
 * header): WebKit was satisfied by basis 0 alone, Chromium
 * still counted the cols width, so both bounds are canon.
 * FIELD GROWTH: bar fixes the textarea at the pill's 44px
 * whatever the text (extra lines scroll inside the pill);
 * stacked auto-grows the textarea from 52px to a 120px cap,
 * then scrolls (growth + cap HOUSE DESIGN — the corpus shows a
 * single 52pt still; the JS performs the sizing, the cap and
 * the bar's fixed height are stated here so the CSS carries
 * the bounds: stacked max-height 120px, bar height 44px).
 * The empty state's voice (reply context or placeholder) is the
 * textarea's placeholder text, in --ink-mute, one grey for both
 * corpus frames.
 * MOTION: paint only — the send's enable cross-fade at --d1 on
 * --ease; no travel anywhere; the global reduced-motion gate
 * governs the fade as it governs every colour change.
 * JS-DEAD: the field and the send are JS-built; without JS the
 * host's avatar and action buttons render in plain flow on the
 * host (this section styles the built structure; unbuilt host
 * children keep their own rendering). No state changes without
 * JS — disclosed in the registry, never faked.
 * TOKENS ONLY in the rules below: field grounds --wash /
 * --wash-2, send pair --composer-send / --composer-send-active
 * (tokens.css, Round 25; NOT theme-flipped, subject marks),
 * arrow --on-image, host ground --bg, hairline --edge-soft,
 * focus --focus. The literals above are corpus samples quoted
 * in this comment, the house precedent.
 * ================================================================ */
@layer c29.components {
c29-pincomposer { display: block; box-sizing: border-box; min-width: 0;
  background: var(--bg); color: var(--ink);
  border-top: 1px solid var(--edge-soft);
  padding: 8px 12px calc(8px + var(--safe-b)); }
c29-pincomposer[layout="stacked"] { padding: 12px 12px calc(12px + var(--safe-b)); }
c29-pincomposer .pc-row { display: flex; align-items: center; gap: 8px;
  box-sizing: border-box; min-width: 0; }
c29-pincomposer .pc-ava { flex: none; display: inline-flex;
  align-items: center; justify-content: center;
  width: 38px; height: 38px; border-radius: 999px; overflow: hidden;
  background: var(--wash-2); color: var(--ink-soft);
  font: 600 13px/1 var(--font-ui); }
c29-pincomposer .pc-ava img { width: 100%; height: 100%; object-fit: cover; display: block; }
c29-pincomposer .pc-ava svg { height: 20px; width: auto; max-width: 20px; display: block; }
c29-pincomposer .pc-field { position: relative; flex: 1 1 0; min-width: 0;
  display: flex; align-items: center; box-sizing: border-box;
  border-radius: 999px; background: var(--wash-2); }
c29-pincomposer[layout="stacked"] .pc-field { border-radius: 16px; background: var(--wash); }
c29-pincomposer .pc-input { flex: 1 1 0; min-width: 0; display: block;
  box-sizing: border-box; margin: 0; border: 0; background: none; outline: none;
  resize: none; overflow-y: auto; overflow-wrap: anywhere;
  height: 44px; padding: 11px 16px;
  font: 400 15px/22px var(--font-ui); color: var(--ink); }
c29-pincomposer[layout="stacked"] .pc-input { min-height: 52px; max-height: 120px;
  padding: 15px 16px; }
c29-pincomposer .pc-input::placeholder { color: var(--ink-mute); opacity: 1; }
c29-pincomposer .pc-input:focus-visible { box-shadow: var(--focus); border-radius: inherit; }
c29-pincomposer .pc-actions { display: flex; align-items: center; gap: 2px; flex: none; }
c29-pincomposer:not([layout="stacked"]) .pc-actions { position: absolute;
  right: 4px; top: 50%; transform: translateY(-50%); }
c29-pincomposer .pc-actions > button { flex: none; width: 40px; height: 40px;
  display: inline-flex; align-items: center; justify-content: center;
  margin: 0; padding: 0; border: 0; border-radius: 999px;
  background: none; color: var(--ink-soft); cursor: pointer;
  -webkit-tap-highlight-color: transparent; touch-action: manipulation; }
c29-pincomposer .pc-actions > button svg,
c29-pincomposer .pc-actions > button img { height: 20px; width: auto;
  max-width: 20px; display: block; }
c29-pincomposer .pc-actions > button:focus-visible { box-shadow: var(--focus); outline: none; }
c29-pincomposer .pc-toolbar { display: none; }
c29-pincomposer[layout="stacked"] .pc-toolbar { display: flex; flex-wrap: wrap;
  align-items: center; gap: 8px; box-sizing: border-box; min-width: 0;
  margin-top: 8px; }
c29-pincomposer .pc-toolbar .pc-actions { margin-right: auto;
  /* Narrow-width wrap (Round 32 adversary): in the stacked
     toolbar the actions container may shrink and wrap its
     40px buttons onto a second line — at 200% zoom (195px)
     five actions plus the send cannot hold one row, and the
     unwrapped row forced its host to 291px. Wrapping keeps
     every action at its full 40px target; the send keeps the
     first row's right end whenever it fits there, else wraps
     whole. The bar form is untouched (its actions are
     absolutely positioned in-field and contribute no width). */
  flex: 1 1 auto; min-width: 0; flex-wrap: wrap; }
c29-pincomposer .pc-send { flex: none; width: 51px; height: 32px;
  display: inline-flex; align-items: center; justify-content: center;
  margin: 0 0 0 auto; padding: 0; border: 0; border-radius: 999px;
  background: var(--composer-send-active); color: var(--on-image);
  cursor: pointer; -webkit-tap-highlight-color: transparent; touch-action: manipulation;
  transition: background-color var(--d1) var(--ease); }
c29-pincomposer .pc-send svg { height: 16px; width: auto; max-width: 16px; display: block; }
c29-pincomposer .pc-send:disabled { background: var(--composer-send); cursor: default; }
c29-pincomposer .pc-send:focus-visible { box-shadow: var(--focus); outline: none; }
/* Narrow bar form (Round 32 adversary, HOUSE DESIGN): the bar
   pill's in-field reservation (16px + 8px + 40px per action)
   is a hard floor under the textarea's border box — with
   three actions the field cannot shrink below 156px, so
   below a ~238px host the textarea painted past the pill's
   right edge (measured at a 195px / 200%-zoom viewport: the
   field's right edge at 214px). At <=280px viewport the pill
   grows a second storey instead: the field gains 44px of
   bottom padding, the actions sit at the pill's bottom-right
   at their full 40px, and the reservation is released (the
   !important overrides the JS-set inline padding for this
   arrangement only). Every target and every character of
   text stays visible; at wider widths the corpus one-storey
   form is untouched. */
@media (max-width: 280px) {
  c29-pincomposer:not([layout="stacked"]) .pc-field { padding-bottom: 44px; }
  c29-pincomposer:not([layout="stacked"]) .pc-actions { top: auto; bottom: 4px; transform: none; }
  c29-pincomposer:not([layout="stacked"]) .pc-input { padding-right: 16px !important; }
}
}
/* ---------- end pincomposer (c29-pincomposer) ---------- */

/* ================================================================
 * QUICKEMOJI — c29-quickemoji (TikTok Pattern Library Round 26,
 * 2026-10-27; JS: tide.js, QUICKEMOJI block. Source spec:
 * rounds/ROUND-26-CRITIC.md, from the Round 6 teardown Part C,
 * corpus frame 08.)
 * The quick-emoji row that rides above the stacked composer:
 * a row of light pills, each a repeated-emoji cluster declared
 * by the host as a native button's own text. Frame measures:
 * four pills, height ≈36pt, radius full, ground #F8F8F8.
 * GEOMETRY: the host is a bare flex row — gap 8px (EST, the
 * frame does not measure it), no padding and no ground of its
 * own (the host's inset positions the row, the c29-chipwall
 * precedent). Pills: height exactly 36px, radius 999px, ground
 * --wash (the token TRANSLATION of the frame's #F8F8F8,
 * labelled — an ink-mix token, so dark flips it by
 * construction; the corpus is light only), flex 1 1 0 with
 * min-width 0: an equal split (CONSTRUCTION, labelled — the
 * frame's four clusters are equal-length, so equal split and
 * natural widths are indistinguishable in it), which also
 * makes any declared count fit the row by shrinking, the
 * cluster centred inside its pill and clipped by the pill at
 * extreme counts (the one deliberate truncation, named in
 * the registry). Basis 0 + min-width 0 on every pill is the
 * Round 25 rule: no pill's content can size a host track.
 * The glyph text is 16px/1 in the UI face — emoji fall to the
 * platform emoji font; the component sets no emoji styling of
 * its own and hardcodes no cluster anywhere: clusters are
 * host data, in the buttons' own text.
 * JS-DEAD: this section paints the row completely — the pills
 * are the host's native buttons and stay pressable; only the
 * quickemoji-pick announcement (tide.js) is absent. That is
 * the component's whole no-JS truth, stated in the registry.
 * TOKENS ONLY in the rules below: pill ground --wash, focus
 * --focus, ink inherited. The literal above is the corpus
 * sample quoted in this comment, the house precedent.
 * ================================================================ */
@layer c29.components {
c29-quickemoji { display: flex; gap: 8px; align-items: stretch;
  box-sizing: border-box; min-width: 0; margin: 0; padding: 0; }
c29-quickemoji [data-qe-pick] { flex: 1 1 0; min-width: 0; height: 36px;
  box-sizing: border-box; margin: 0; padding: 0 8px;
  display: inline-flex; align-items: center; justify-content: center;
  text-align: center; overflow: hidden; overflow-wrap: anywhere;
  border: 0; border-radius: 999px; background: var(--wash);
  color: var(--ink); font: 400 16px/1 var(--font-ui); cursor: pointer;
  -webkit-tap-highlight-color: transparent; touch-action: manipulation; }
c29-quickemoji [data-qe-pick]:focus-visible { box-shadow: var(--focus); outline: none; }
}
/* ---------- end quickemoji (c29-quickemoji) ---------- */

/* ================================================================
 * SHAREWALL — c29-sharewall (TikTok Pattern Library Round 27,
 * 2026-10-27; JS: tide.js, SHAREWALL block. Source spec:
 * rounds/ROUND-27-CRITIC.md, from the Round 6 teardown Part D
 * and the Round 1 teardown, corpus frame 08.)
 * The app-icon share row inside the floating share card: one
 * row of share targets, each a brand circle with a small label
 * beneath, overflowing the card's right edge — the cut target
 * at the edge is the scroll affordance (no arrow, no fade).
 * Frame measures: circles ≈47.5pt (pixel-measured), labels
 * ≈12pt centred and wrapping to two lines where a name needs
 * it, item pitch ≈65.5pt centre-to-centre (measured). The
 * card, its title and its close behaviour are the HOST's —
 * this component is the row only.
 * GEOMETRY: the host is a flex row with overflow-x auto and
 * no padding or ground of its own (the host card's inset
 * positions the row). Each target is a fixed column of
 * exactly 65.5px (the measured pitch; gap 0, so centre-to-
 * centre pitch is 65.5px by construction) carrying a 48px
 * circle (the ≈47.5pt measure rounded to the nearest px,
 * labelled) and a 12px/15px centred label 4px below it (the
 * circle-to-label gap is EST — the frame does not isolate
 * it). Fixed basis + the host's own overflow is the Round 25
 * rule in its scroll form: no target's content can widen the
 * page — the row scrolls instead. The host also carries
 * contain: inline-size — the containment bound the round's
 * own probe isolated: without it, Chromium sized a
 * content-sized ancestor track by the row's full scroll
 * width (393px), the row stopped scrolling and the page
 * widened to 449px; with it the row's content contributes
 * nothing to any ancestor's inline size in either engine.
 * Canon imposes NO line
 * clamp on the label: a long name wraps further inside its
 * column (overflow-wrap anywhere, min-width 0); the frame's
 * truncated last label is cut by the row's overflow edge, a
 * position effect, not a label rule.
 * BRAND IS HOST DATA: the glyph carrier's ground and ink ride
 * the host-declared --sw-glyph-bg / --sw-glyph-ink custom
 * properties — canon hardcodes no network, no brand colour
 * and no glyph, and a declared brand ground does NOT flip in
 * dark (the subject-mark precedent). A target that declares
 * no brand ground falls back to --wash (an ink-mix token, so
 * dark flips it by construction); labels ride the ink token
 * --ink-soft and flip by token. Glyph content (host SVG,
 * image or character) is centred in the circle, clipped by
 * it, and capped at 26x26px (CONSTRUCTION — no glyph-ink
 * measure is proven for this row beyond the circles).
 * SCROLLBAR: hidden — the overflow-cut is the affordance,
 * per the frame (no arrow, no fade is frame fact; that the
 * row scrolls at all is INF from the overflow).
 * JS-DEAD: this section paints the row completely and the
 * host still scrolls — targets are the host's native buttons
 * or links and stay working; only the sharewall-pick
 * announcement (tide.js) is absent. That is the component's
 * whole no-JS truth, stated in the registry.
 * TOKENS ONLY in the rules below: --wash fallback, --focus,
 * --ink-soft, plus the two host-data custom properties. The
 * literals above are corpus measures quoted in this comment,
 * the house precedent.
 * ================================================================ */
@layer c29.components {
c29-sharewall { display: flex; gap: 0; align-items: flex-start;
  box-sizing: border-box; min-width: 0; max-width: 100%; margin: 0; padding: 0;
  contain: inline-size;
  overflow-x: auto; overflow-y: hidden; scrollbar-width: none;
  -webkit-overflow-scrolling: touch; }
c29-sharewall::-webkit-scrollbar { display: none; }
c29-sharewall [data-sw-target] { flex: 0 0 65.5px; min-width: 0;
  box-sizing: border-box; margin: 0; padding: 0;
  display: inline-flex; flex-direction: column; align-items: center;
  background: transparent; border: 0; border-radius: 12px;
  color: var(--ink); font: 400 12px/15px var(--font-ui);
  text-align: center; text-decoration: none; cursor: pointer;
  -webkit-tap-highlight-color: transparent; touch-action: manipulation; }
c29-sharewall [data-sw-glyph] { width: 48px; height: 48px; flex: 0 0 48px;
  box-sizing: border-box; border-radius: 999px; overflow: hidden;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--sw-glyph-bg, var(--wash)); color: var(--sw-glyph-ink, var(--ink));
  font: 400 20px/1 var(--font-ui); }
c29-sharewall [data-sw-glyph] svg, c29-sharewall [data-sw-glyph] img {
  width: 26px; height: 26px; max-width: 26px; max-height: 26px; display: block; }
c29-sharewall [data-sw-label] { margin-top: 4px; min-width: 0; max-width: 100%;
  color: var(--ink-soft); font: 400 12px/15px var(--font-ui);
  text-align: center; overflow-wrap: anywhere; }
c29-sharewall [data-sw-target]:focus-visible { box-shadow: var(--focus); outline: none; }
}
/* ---------- end sharewall (c29-sharewall) ---------- */

/* ================================================================
 * COACHMARK — c29-coachmark (TikTok Pattern Library Round 28,
 * 2026-10-27; JS: tide.js, COACHMARK block. Source spec:
 * rounds/ROUND-28-CRITIC.md, from the Round 6 teardown
 * Part C, corpus frame 08.)
 * The contextual coachmark bubble that rides on the stacked
 * composer: a white bubble carrying one short bold hint,
 * overlapping the composer field's lower edge, its tail on
 * the bubble's BOTTOM edge pointing DOWN at the + icon in
 * the toolbar below. Frame tokens (EST): background #FFF,
 * radius 10pt, shadow 0 4pt 16pt rgba(0,0,0,0.12). The
 * frame's copy is the subject's; the hint text here is
 * always the host's own text content, and canon hardcodes
 * no copy.
 * GEOMETRY: the bubble is inline-block with position
 * relative — the tail's anchor only; where the bubble sits
 * on the page (including the frame's overlap of the field
 * edge) is HOST LAYOUT, as pinning was for c29-pincomposer.
 * Radius exactly 10px; ground --surface and shadow --lift
 * (the token TRANSLATIONS of the frame's white and its EST
 * soft shadow, labelled — --surface flips in dark by
 * construction while the corpus is light only); padding
 * 10px 14px (EST — the frame does not isolate it); copy
 * 600 15px/20px in --ink; max-width 100% with overflow-wrap
 * anywhere, so a long hint wraps inside its host-given
 * width rather than widening the page. The TAIL is the
 * host's ::after: a 12px square (EST), rotated 45deg,
 * radius 2px, ground --surface, sitting on the bottom edge
 * (bottom -5px, EST) with its horizontal centre at
 * var(--cm-tail-x, 50%) — a host-data custom property, so
 * the host that positions the bubble also aims the tail at
 * its target. The tail carries no shadow of its own
 * (CONSTRUCTION, labelled). Direction: DOWN only — the one
 * direction the corpus proves; no other direction is built.
 * DISMISSAL is HOUSE DESIGN (the corpus is silent): a click
 * on the bubble sets data-cm-dismissed (display none in the
 * rule below) and announces coachmark-dismiss — see the
 * tide.js COACHMARK block. The pointer cursor in the rule
 * below is part of that labelled design, not a corpus claim.
 * JS-DEAD: this section paints the bubble and its tail
 * completely — only the click dismissal (tide.js) is
 * absent, and the host hides the bubble by its own means.
 * That is the component's whole no-JS truth, stated in the
 * registry.
 * TOKENS ONLY in the rules below: --surface, --lift, --ink,
 * plus the host-data --cm-tail-x custom property. The
 * literals above are corpus measures quoted in this
 * comment, the house precedent.
 * ================================================================ */
@layer c29.components {
c29-coachmark { display: inline-block; position: relative;
  box-sizing: border-box; max-width: 100%; margin: 0; padding: 10px 14px;
  border-radius: 10px; background: var(--surface); box-shadow: var(--lift);
  color: var(--ink); font: 600 15px/20px var(--font-ui);
  overflow-wrap: anywhere; cursor: pointer;
  -webkit-tap-highlight-color: transparent; }
c29-coachmark::after { content: ""; position: absolute;
  width: 12px; height: 12px; bottom: -5px;
  left: var(--cm-tail-x, 50%); transform: translateX(-50%) rotate(45deg);
  border-radius: 2px; background: var(--surface); }
c29-coachmark[data-cm-dismissed="1"] { display: none; }
}
/* ---------- end coachmark (c29-coachmark) ---------- */

/* ================================================================
 * THREAD — c29-thread (TikTok Pattern Library Round 29,
 * 2026-10-27; JS: tide.js, THREAD block. Source spec:
 * rounds/ROUND-29-CRITIC.md, from the Round 6 teardown
 * Part B, corpus frame 02.)
 * The threaded comment list inside the sheet (the sheet's
 * own header — context line, count, sort, close — is the
 * host sheet's chrome, never this component's). Frame
 * measures: top-level avatar ≈36pt, reply avatar ≈24pt
 * (both pixel-measured); name 15pt semibold grey; body
 * 16/21 black wrapping freely; meta 13pt grey with "Reply"
 * semibold and the like + share actions right-aligned;
 * reply rows indent ONE level, deeper replies keep the
 * SAME indent; a collapsed thread announces itself with a
 * "View N replies" expander preceded by a short hairline
 * segment; long bodies truncate behind a "… more" expander.
 * GEOMETRY: a comment is a wrapping flex row (avatar +
 * main on the first row, the replies wrapper and the
 * expander as full-width rows beneath) with a 12px column
 * gap (EST) and 16px between comments (EST). The replies
 * wrapper is a full-width row of the ARTICLE (never inside
 * the main column) whose padding-left 48px (EST — the 36px
 * avatar + the 12px gap) starts the reply row under the
 * parent text column exactly once; a wrapper nested inside
 * a reply takes padding-left 0, so deeper replies hold the
 * SAME indent (the frame's rule) at any depth. The
 * expander indents with its comment's text column (48px at
 * top level, 36px inside replies — the 24px avatar + the
 * gap). Replies sit 12px below their parent's meta (EST).
 * (The wrapper-inside-main shape was this round's first
 * build and its own probe killed it: the indent stacked to
 * 96px — the restructure is recorded in the round report.)
 * The expander's flex basis is 100% for the same reason in
 * miniature: it must hold its own row even when its thread
 * is collapsed and the hidden wrapper claims no row — the
 * round's squish gate caught the collapsed comment's main
 * column squeezed to 90px by an expander sharing its row. Name ink and meta ink are the --ink-mute
 * TRANSLATION of the frame's greys (labelled — an ink-mix
 * token, so dark flips them by construction; the corpus
 * is light only). The expander's hairline segment is 24px
 * (EST) on --edge-soft. The clamp is 3 lines (EST — the
 * frame does not count the truncated body's lines).
 * BEHAVIOUR SUMMARY (full text in the tide.js THREAD
 * header and the registry): the expander toggles its
 * thread's collapsed state + label (state is truth), the
 * "… more" button unclamps its body once, and the meta
 * actions only ANNOUNCE — Reply / like / share behaviour
 * is the host's wiring and is not in the corpus. The
 * component builds no comment structure.
 * JS-DEAD: this section paints the whole list — a
 * host-declared collapsed thread stays hidden (its state
 * is an attribute the CSS reads) and a clamped body stays
 * clamped with its more button visible but inert; only
 * the toggling, the announcements and aria-expanded are
 * absent. That is the component's whole no-JS truth,
 * stated in the registry.
 * TOKENS ONLY in the rules below: --ink, --ink-mute,
 * --edge-soft, --wash, --focus, plus the host-data
 * --th-ground custom property. The literals above are
 * corpus measures quoted in this comment, the house
 * precedent.
 * ================================================================ */
@layer c29.components {
c29-thread { display: block; box-sizing: border-box; min-width: 0;
  margin: 0; padding: 0; }
c29-thread [data-th-comment] { display: flex; flex-wrap: wrap;
  gap: 0 12px; min-width: 0; }
c29-thread [data-th-comment] + [data-th-comment] { margin-top: 16px; }
c29-thread [data-th-avatar] { flex: 0 0 auto; width: 36px; height: 36px;
  border-radius: 999px; overflow: hidden; box-sizing: border-box;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--wash); color: var(--ink-mute);
  font: 600 13px/1 var(--font-ui); }
c29-thread [data-th-replies] [data-th-avatar] { width: 24px; height: 24px;
  font-size: 11px; }
c29-thread [data-th-avatar] img { width: 100%; height: 100%;
  object-fit: cover; display: block; }
c29-thread [data-th-main] { flex: 1 1 0; min-width: 0; }
c29-thread [data-th-name] { margin: 0; color: var(--ink-mute);
  font: 600 15px/20px var(--font-ui); overflow-wrap: anywhere; }
c29-thread [data-th-bodywrap] { position: relative; }
c29-thread [data-th-body] { margin: 0; color: var(--ink);
  font: 400 16px/21px var(--font-ui); overflow-wrap: anywhere; }
c29-thread [data-th-body][data-th-clamped] { display: -webkit-box;
  -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
c29-thread [data-th-more] { position: absolute; right: 0; bottom: 0;
  margin: 0; padding: 0 0 0 12px; border: 0; cursor: pointer;
  background: var(--th-ground, var(--surface)); color: var(--ink-mute);
  font: 600 16px/21px var(--font-ui); -webkit-tap-highlight-color: transparent; }
c29-thread [data-th-meta] { display: flex; align-items: center; gap: 12px;
  margin-top: 6px; color: var(--ink-mute); font: 400 13px/18px var(--font-ui); }
c29-thread [data-th-action] { margin: 0; padding: 0; border: 0;
  background: transparent; color: inherit; cursor: pointer;
  display: inline-flex; align-items: center; gap: 4px;
  font: 400 13px/18px var(--font-ui); -webkit-tap-highlight-color: transparent; }
c29-thread [data-th-action="reply"] { font-weight: 600; }
c29-thread [data-th-side] { margin-left: auto; display: inline-flex;
  align-items: center; gap: 16px; }
c29-thread [data-th-replies] { flex: 1 1 100%; box-sizing: border-box;
  padding-left: 48px; margin-top: 12px; min-width: 0; }
c29-thread [data-th-replies] [data-th-replies] { padding-left: 0; }
c29-thread [data-th-replies][data-th-collapsed="1"] { display: none; }
c29-thread [data-th-expander] { flex: 1 1 100%; box-sizing: border-box;
  margin: 8px 0 0 48px; padding: 0; border: 0;
  background: transparent; color: var(--ink-mute); cursor: pointer;
  display: inline-flex; align-items: center; gap: 8px;
  font: 600 13px/18px var(--font-ui); -webkit-tap-highlight-color: transparent; }
c29-thread [data-th-replies] [data-th-expander] { margin-left: 36px; }
c29-thread [data-th-expander]::before { content: ""; width: 24px;
  height: 1px; background: var(--edge-soft); flex: 0 0 24px; }
c29-thread [data-th-action]:focus-visible,
c29-thread [data-th-expander]:focus-visible,
c29-thread [data-th-more]:focus-visible { box-shadow: var(--focus); outline: none; }
}
/* ---------- end thread (c29-thread) ---------- */

/* ================================================================
 * UPSELL-SHEET — c29-upsell-sheet (TikTok Pattern Library
 * Round 30, 2026-10-27; JS: tide.js, UPSELL-SHEET block.
 * Source spec: rounds/ROUND-30-CRITIC.md, from the Round 1
 * teardown Anatomy B, corpus frame 04.)
 * The single-CTA promo card — a FLOATING CARD, not a bottom
 * sheet (the R1 verification's correction; the tag carries
 * the slate's word only): artwork bleeding across the top,
 * one centred headline, one centred subcopy, ONE full-width
 * dark CTA, and a circled X close floating on the artwork.
 * The card's screen placement and the scrim beneath it are
 * the HOST overlay's — this component is the card only.
 * Frame measures: artwork the card's top ≈55%; close circle
 * ≈32pt on sampled rgba(255,255,255,0.85); headline 27/32
 * bold centred; subcopy 16/21 centred; CTA height ≈52pt,
 * radius ≈10pt (EST), inset ≈28pt from the card's edges
 * (measured); card corners EST ≈28pt top / ≈24pt bottom.
 * GEOMETRY + PAINT: radius 28px top / 24px bottom (the EST
 * pair, labelled); ground --surface and the CTA pair
 * --ink-on---bg (the TRANSLATIONS of the frame's white card
 * and black button + white label, labelled — the tokens
 * flip the card, and the CTA pair together, in dark by
 * construction; the corpus is light only); shadow --lift;
 * the artwork zone is aspect-ratio 3/2 (CONSTRUCTION —
 * derived from the frame's ≈55% artwork of a ≈52%-of-screen
 * card, labelled) with host content cover-clipped; the
 * close ground is the color-mix TRANSLATION of the sampled
 * 0.85 white at 85% of --surface; headline/subcopy/CTA ride
 * the frame's 28px content inset. Gaps the frame does not
 * isolate (close offset 12px, headline top 20px, subcopy
 * gap 8px, CTA gaps 20px/28px, CTA label 600 16px, the
 * close glyph cap 14px) are EST / CONSTRUCTION, labelled
 * here and in the registry.
 * THE COERCION STRIP (the charter's Round 30 term): this
 * section and its block provide exactly ONE action slot
 * and ONE close — and no slot, part or state for a
 * countdown, a scarcity badge, a preselected plan, a
 * second offer step, or a guilt-worded dismiss link. The
 * close cannot be hidden, shrunk, delayed or disabled by
 * anything in the component, and nothing here dismisses
 * or converts on a timer. The craft is canon; the pressure
 * is refused, by construction (the R33 audit ratifies).
 * JS-DEAD: this section paints the whole card — the slots
 * are the host's own elements and its buttons stay working;
 * only the upsell-close / upsell-action announcements
 * (tide.js) are absent. That is the component's whole
 * no-JS truth, stated in the registry.
 * TOKENS ONLY in the rules below: --surface, --lift, --ink,
 * --bg, --ink-soft, --focus. The literals above are corpus
 * measures quoted in this comment, the house precedent.
 * ================================================================ */
@layer c29.components {
c29-upsell-sheet { display: block; position: relative; overflow: hidden;
  box-sizing: border-box; min-width: 0; margin: 0; padding: 0;
  border-radius: 28px 28px 24px 24px; background: var(--surface);
  box-shadow: var(--lift); color: var(--ink); }
c29-upsell-sheet [data-us-art] { display: block; aspect-ratio: 3 / 2;
  overflow: hidden; background: var(--wash); }
c29-upsell-sheet [data-us-art] img, c29-upsell-sheet [data-us-art] svg {
  width: 100%; height: 100%; object-fit: cover; display: block; }
c29-upsell-sheet [data-us-close] { position: absolute; top: 12px; right: 12px;
  width: 32px; height: 32px; box-sizing: border-box; margin: 0; padding: 0;
  border: 0; border-radius: 999px; cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
  background: color-mix(in srgb, var(--surface) 85%, transparent);
  color: var(--ink); -webkit-tap-highlight-color: transparent; }
c29-upsell-sheet [data-us-close] svg, c29-upsell-sheet [data-us-close] img {
  width: 14px; height: 14px; max-width: 14px; max-height: 14px; display: block; }
c29-upsell-sheet [data-us-headline] { margin: 20px 28px 0; text-align: center;
  color: var(--ink); font: 700 27px/32px var(--font-ui);
  overflow-wrap: anywhere; }
c29-upsell-sheet [data-us-sub] { margin: 8px 28px 0; text-align: center;
  color: var(--ink-soft); font: 400 16px/21px var(--font-ui);
  overflow-wrap: anywhere; }
c29-upsell-sheet [data-us-cta] { display: block; width: calc(100% - 56px);
  height: 52px; box-sizing: border-box; margin: 20px 28px 28px; padding: 0 16px;
  border: 0; border-radius: 10px; cursor: pointer;
  background: var(--ink); color: var(--bg);
  font: 600 16px/1 var(--font-ui); text-align: center;
  -webkit-tap-highlight-color: transparent; }
c29-upsell-sheet [data-us-close]:focus-visible,
c29-upsell-sheet [data-us-cta]:focus-visible { box-shadow: var(--focus); outline: none; }
}
/* ---------- end upsell-sheet (c29-upsell-sheet) ---------- */

@layer c29.components {
/* ---------- waveform (c29-waveform) ----------
   Pacesetter 250 Round 12 (Media Core I). Paint from canon
   tokens only, both themes; the label row is the c29-prose
   row grammar — the label is the defended prose column
   (.c29-prose-main: flex 1 1 45%, min-width 160px) and the
   duration readout is shrinkable side furniture
   (.c29-prose-side: flex 0 1 auto, min-width 0) that wraps
   BELOW the label, never crushes it (Squish Law). Long clip
   names wrap inside the defended column
   (overflow-wrap: anywhere). The wave canvas holds no
   prose and is laid out independently of the label row.
   The wave surface is one focusable control at least 44px
   tall with the canon focus ring. No transition animates
   the playhead or the selection: under reduced motion, and
   in fact in every motion setting, position changes render
   as steps to the fed position. */
c29-waveform { display: block; background: var(--surface); border-radius: var(--corner);
  box-shadow: var(--hair); padding: var(--s4); }
c29-waveform .wf-row { margin-bottom: var(--s3); }
c29-waveform .wf-label { font: 600 15px/21px var(--font-ui); color: var(--ink);
  overflow-wrap: anywhere; }
c29-waveform .wf-label[hidden], c29-waveform .wf-empty[hidden],
c29-waveform .wf-surface[hidden], c29-waveform .wf-host[hidden] { display: none; }
c29-waveform .wf-side, c29-waveform .wf-readout { font: 400 13px/18px var(--font-ui);
  color: var(--ink-mute); font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
c29-waveform .wf-empty { font: 400 15px/21px var(--font-ui); color: var(--ink-mute);
  overflow-wrap: anywhere; margin: 0; padding: var(--s3) 0; }
c29-waveform .wf-surface { position: relative; height: 96px; min-height: 44px;
  border-radius: calc(var(--corner) - 6px); background: var(--sunken);
  cursor: pointer; touch-action: none; }
c29-waveform .wf-canvas { display: block; width: 100%; height: 100%; }
c29-waveform .wf-surface:focus-visible { outline: none; box-shadow: var(--focus); }
c29-waveform[data-state="inert"] .wf-surface { cursor: default; }
c29-waveform .wf-host { margin-top: var(--s3); font: 400 15px/21px var(--font-ui); color: var(--ink); }
@media (prefers-reduced-motion: reduce) {
  c29-waveform .wf-surface, c29-waveform .wf-canvas { transition: none !important; }
}
}
/* ---------- end waveform (c29-waveform) ---------- */
@layer c29.components {
/* ---------- clip timeline (c29-cliptimeline) ----------
   Pacesetter 250 Round 13 (Media Core I). Paint from canon
   tokens only, both themes; the project label row is the
   c29-prose row grammar — the project label is the defended
   prose column (.c29-prose-main: flex 1 1 45%, min-width
   160px) and the total-span readout is shrinkable side
   furniture (.c29-prose-side: flex 0 1 auto, min-width 0)
   that wraps BELOW the label, never crushes it (Squish
   Law). Long clip names wrap inside the defended column in
   the selected clip's label row (overflow-wrap: anywhere).
   Inside a clip block the label never compresses: the one
   deliberate, visible truncation style is .ct-cliplabel —
   a single line with a visible ellipsis cut — with the full
   label reachable in the label row when the clip is
   selected. Every clip block is a real focusable control at
   least 44px in its smallest dimension with the canon focus
   ring. No transition animates the playhead, a trim, or a
   reorder in any motion setting; under reduced motion drag
   ghosts and reorder transitions are removed outright (the
   component builds no ghost element at all). */
c29-cliptimeline { display: block; min-width: 0; max-width: 100%;
  background: var(--surface); border-radius: var(--corner);
  box-shadow: var(--hair); padding: var(--s4); }
c29-cliptimeline .ct-row { margin-bottom: var(--s3); }
c29-cliptimeline .ct-projlabel { font: 600 15px/21px var(--font-ui); color: var(--ink);
  overflow-wrap: anywhere; }
c29-cliptimeline .ct-projlabel[hidden], c29-cliptimeline .ct-empty[hidden],
c29-cliptimeline .ct-selrow[hidden], c29-cliptimeline .ct-scroll[hidden],
c29-cliptimeline .ct-odwrap[hidden], c29-cliptimeline .ct-playhead[hidden],
c29-cliptimeline .ct-host[hidden] { display: none; }
c29-cliptimeline .ct-side, c29-cliptimeline .ct-total { font: 400 13px/18px var(--font-ui);
  color: var(--ink-mute); font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
c29-cliptimeline .ct-empty { font: 400 15px/21px var(--font-ui); color: var(--ink-mute);
  overflow-wrap: anywhere; margin: 0; padding: var(--s3) 0; }
c29-cliptimeline .ct-selrow { margin-bottom: var(--s3); min-width: 0; }
c29-cliptimeline .ct-sellabel { display: block; font: 600 14px/20px var(--font-ui);
  color: var(--ink); overflow-wrap: anywhere; min-width: 0; }
c29-cliptimeline .ct-trackwrap { border-radius: calc(var(--corner) - 6px);
  background: var(--sunken); padding: var(--s3); min-width: 0; max-width: 100%; }
c29-cliptimeline .ct-trackwrap:focus-visible { outline: none; box-shadow: var(--focus); }
c29-cliptimeline .ct-scroll { overflow-x: auto; min-width: 0; max-width: 100%; }
c29-cliptimeline .ct-inner { position: relative; min-width: 100%; }
c29-cliptimeline .ct-ruler { height: 18px; cursor: pointer; touch-action: none; }
c29-cliptimeline .ct-mainrow { display: flex; align-items: stretch; min-height: 76px; }
c29-cliptimeline .ct-odwrap { margin-top: var(--s3); }
c29-cliptimeline .ct-lanelabel { font: 600 12px/16px var(--font-ui); color: var(--ink-mute);
  margin-bottom: 4px; }
c29-cliptimeline .ct-odrow { display: flex; align-items: stretch; min-height: 56px; }
c29-cliptimeline .ct-clip { position: relative; flex: none; box-sizing: border-box;
  min-height: 44px; padding: 6px 8px; overflow: hidden; cursor: pointer;
  background: var(--surface); border-radius: 8px;
  box-shadow: inset 0 0 0 1px var(--edge); color: var(--ink); }
c29-cliptimeline .ct-clip.is-selected { box-shadow: inset 0 0 0 2px var(--accent); }
c29-cliptimeline .ct-clip.is-dragging { box-shadow: inset 0 0 0 2px var(--accent);
  background: var(--accent-soft); }
c29-cliptimeline .ct-clip:focus-visible { outline: none; box-shadow: var(--focus); }
c29-cliptimeline .ct-cliplabel { display: block; white-space: nowrap; overflow: hidden;
  text-overflow: ellipsis; font: 600 13px/18px var(--font-ui); color: var(--ink); }
c29-cliptimeline .ct-clipmeta { display: flex; gap: 6px; align-items: center;
  font: 400 12px/16px var(--font-ui); color: var(--ink-mute); margin-top: 2px; }
c29-cliptimeline .ct-capmark { font-weight: 600; color: var(--accent-text); }
c29-cliptimeline .ct-thumb { display: block; max-width: 100%; margin-top: 4px; }
c29-cliptimeline .ct-strip { margin-top: 4px; }
c29-cliptimeline .ct-strip c29-waveform { padding: var(--s3); box-shadow: none; }
c29-cliptimeline .ct-playhead { position: absolute; top: 0; width: 2px;
  background: var(--accent); pointer-events: none; }
c29-cliptimeline .ct-host { margin-top: var(--s3); font: 400 15px/21px var(--font-ui); color: var(--ink); }
@media (prefers-reduced-motion: reduce) {
  c29-cliptimeline .ct-clip, c29-cliptimeline .ct-playhead,
  c29-cliptimeline .ct-inner { transition: none !important; animation: none !important; }
}
}
/* ---------- end clip timeline (c29-cliptimeline) ---------- */


/* ============================================================
 * ASSET LIBRARY — c29-assetlibrary (APPS 800 Round 39, 2026-10-28)
 * Folders, tags, and search over a host store adapter's records.
 * Every selector is scoped to c29-assetlibrary; class prefix .al-.
 * Tokens only. The asset name is the prose column of its row:
 * it keeps its floor and the furniture wraps below it.
 * ============================================================ */
@layer c29.components {
c29-assetlibrary { display: block; position: relative; flex: 1 1 100%; width: 100%; min-width: 0; color: var(--ink); }
c29-assetlibrary .al-root { display: grid; gap: var(--s4); min-width: 0; background: var(--surface); border-radius: var(--r-md); box-shadow: var(--hair); padding: var(--s4); }
c29-assetlibrary .al-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px 10px; min-width: 0; }
c29-assetlibrary .al-label { font: 700 var(--fs-lg)/1.3 var(--font-display); color: var(--ink); flex: 0 1 auto; min-width: 0; overflow-wrap: anywhere; }
c29-assetlibrary .al-hint { font-size: var(--fs-sm); line-height: 1.45; color: var(--ink-soft); flex: 1 1 45%; min-width: 0; overflow-wrap: anywhere; }
c29-assetlibrary .al-hint[hidden] { display: none; }
c29-assetlibrary .al-search { display: grid; gap: 4px; min-width: 0; }
c29-assetlibrary .al-searchlabel { font-size: var(--fs-sm); font-weight: 650; color: var(--ink-soft); min-width: 0; overflow-wrap: anywhere; }
c29-assetlibrary .al-searchinput { width: 100%; box-sizing: border-box; min-height: 44px; border: 0; border-radius: var(--r-md);
  background: var(--sunken); box-shadow: inset 0 0 0 1px var(--edge); padding: 10px 12px; font: 400 var(--fs-body)/1.4 var(--font-ui); color: var(--ink); }
c29-assetlibrary .al-searchinput:focus-visible { box-shadow: var(--focus, 0 0 0 2px var(--focus-ring)); outline: none; }
c29-assetlibrary .al-searchinput:disabled { opacity: .5; }
c29-assetlibrary .al-folders { list-style: none; margin: 0; padding: 0; display: grid; gap: 6px; min-width: 0; }
c29-assetlibrary .al-folderrow { min-width: 0; }
c29-assetlibrary .al-folder { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 10px; width: 100%; box-sizing: border-box;
  min-height: 44px; border: 0; border-radius: var(--r-md); background: var(--sunken); color: var(--ink);
  padding: 8px 12px; font: 600 var(--fs-sm)/1.35 var(--font-ui); text-align: left; cursor: pointer; }
c29-assetlibrary .al-foldername { flex: 1 1 45%; min-width: 0; overflow-wrap: anywhere; }
c29-assetlibrary .al-count { flex: 0 1 auto; min-width: 0; font-size: var(--fs-cap); font-weight: 650; color: var(--ink-mute); font-variant-numeric: tabular-nums; }
c29-assetlibrary .al-folder.is-active { box-shadow: inset 0 0 0 2px var(--accent); }
c29-assetlibrary .al-folder:focus-visible { box-shadow: var(--focus, 0 0 0 2px var(--focus-ring)); outline: none; }
c29-assetlibrary .al-folder:active { transform: scale(.99); }
c29-assetlibrary .al-folder:disabled { opacity: .5; cursor: default; }
c29-assetlibrary .al-tags { display: flex; flex-wrap: wrap; gap: 8px; min-width: 0; }
c29-assetlibrary .al-tags:empty { display: none; }
c29-assetlibrary .al-chip { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 8px; flex: 0 1 auto; min-width: 0;
  min-height: 44px; box-sizing: border-box; border: 0; border-radius: 999px; background: var(--sunken); color: var(--ink);
  padding: 8px 14px; font: 600 var(--fs-sm)/1.35 var(--font-ui); cursor: pointer; }
c29-assetlibrary .al-chipname { flex: 1 1 45%; min-width: 0; overflow-wrap: anywhere; }
c29-assetlibrary .al-chip.is-active { box-shadow: inset 0 0 0 2px var(--accent); }
c29-assetlibrary .al-chip:focus-visible { box-shadow: var(--focus, 0 0 0 2px var(--focus-ring)); outline: none; }
c29-assetlibrary .al-chip:active { transform: scale(.98); }
c29-assetlibrary .al-chip:disabled { opacity: .5; cursor: default; }
c29-assetlibrary .al-state { margin: 0; font-size: var(--fs-sm); line-height: 1.45; color: var(--ink-mute); min-width: 0; overflow-wrap: anywhere; }
c29-assetlibrary .al-state[hidden] { display: none; }
c29-assetlibrary .al-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; min-width: 0; }
c29-assetlibrary .al-list:empty { display: none; }
c29-assetlibrary .al-row { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 10px; min-width: 0;
  background: var(--sunken); border-radius: var(--r-md); padding: 10px 12px; }
c29-assetlibrary .al-main { flex: 1 1 45%; min-width: min(160px, 100%); display: grid; gap: 2px; }
c29-assetlibrary .al-name { min-width: 0; overflow-wrap: anywhere; font: 600 var(--fs-sm)/1.35 var(--font-ui); color: var(--ink); }
c29-assetlibrary .al-meta { flex: 0 1 auto; min-width: 0; overflow-wrap: anywhere; font-size: var(--fs-cap); color: var(--ink-mute); }
c29-assetlibrary .al-rowtags { flex: 0 1 auto; min-width: 0; overflow-wrap: anywhere; font-size: var(--fs-cap); color: var(--ink-soft); }
c29-assetlibrary .al-pick, c29-assetlibrary .al-remove { flex: 0 1 auto; min-width: 44px; min-height: 44px; padding: 0 12px; border: 0; border-radius: 999px;
  background: var(--surface); box-shadow: inset 0 0 0 1px var(--edge); color: var(--ink); font: 600 var(--fs-sm)/1 var(--font-ui); cursor: pointer; }
c29-assetlibrary .al-pick:focus-visible, c29-assetlibrary .al-remove:focus-visible { box-shadow: var(--focus, 0 0 0 2px var(--focus-ring)); outline: none; }
c29-assetlibrary .al-pick:active, c29-assetlibrary .al-remove:active { transform: scale(.98); }
c29-assetlibrary .al-pick:disabled, c29-assetlibrary .al-remove:disabled { opacity: .5; cursor: default; }
c29-assetlibrary .al-live { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
c29-assetlibrary[disabled] .al-root { opacity: .75; }
}
/* ---------- end asset library (c29-assetlibrary) ---------- */
