/* Shared values and portable controls. Pages own layout; opt-in author pages
   use these same bytes without inheriting an application-wide reset. */
:root {
    --native-color-scheme: dark;
    --neutral-rgb: 255, 255, 255;
    --shell-rgb: 15, 15, 17;
    --composer-rgb: 26, 27, 28;
    --modal-rgb: 20, 21, 22;
    --menu-rgb: 17, 18, 19;
    --media-rgb: 9, 9, 10;
    --wizard-panel-rgb: 21, 22, 23;
    --wizard-rgb: 9, 9, 10;
    --code-bg: #0b0b0c;
    --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
    --control-disabled-opacity: 0.55;
    --bg-primary: #0f0f11;
    --bg-secondary: #1a1b1c;
    /* Type scale + foreground semantics: docs/DESIGN.md. Four sizes, no fifth. */
    --type-meta: 12px;
    --type-body: 14px;
    --type-section: 16px;
    --type-page: 24px;
    /* Not UI sizes: the chat reading ladder, relative to the bubble's reading
       text (20px and 18px over the 16px bubble; DESIGN.md §1). */
    --md-heading-major: 1.25em;
    --md-heading-minor: 1.125em;
    --line-meta: 1.35;
    --line-body: 1.5;
    --line-title: 1.3;
    --text-primary: #efeff2;
    --text-secondary: rgba(var(--neutral-rgb), 0.58);
    --text-meta: rgba(var(--neutral-rgb), 0.7);
    --text-disabled: rgba(var(--neutral-rgb), 0.46);
    --text-muted: var(--text-disabled);  /* legacy alias */
    --accent: #f45152;
    --accent-light: #fe7d85;
    /* Accent alpha tokens; keep future tweaks off hardcoded rgba literals. */
    --accent-08: rgba(201, 53, 69, 0.08);
    --accent-10: rgba(201, 53, 69, 0.10);
    --accent-12: rgba(201, 53, 69, 0.12);
    --accent-14: rgba(201, 53, 69, 0.14);
    --accent-18: rgba(201, 53, 69, 0.18);
    --accent-22: rgba(201, 53, 69, 0.22);
    --accent-25: rgba(201, 53, 69, 0.25);
    --accent-35: rgba(201, 53, 69, 0.35);
    --accent-45: rgba(201, 53, 69, 0.45);
    --accent-55: rgba(201, 53, 69, 0.55);
    /* Project identity (vivid fuchsia): a converted task card, its sidebar marker,
       and the project panel header share this so "this is a project" reads as a
       deliberate state — never the red task accent (looked like an error, P3) and
       never the blue owner voice. The old calm-indigo sat next to the user-message
       blue and the two blurred together; fuchsia is unmistakably its own role.
       Base is a light fuchsia that reads on the room and under its veil on a garnet or sapphire voice; in light
       every tint shares the SAME hue (C7.3), in dark the fills are neutral veils carrying the fuchsia ink (a dark
       fuchsia tint reads as mauve). */
    --project: #ee90fa;
    --project-08: rgba(255, 255, 255, 0.07);
    --project-12: rgba(255, 255, 255, 0.1);
    --project-18: rgba(255, 255, 255, 0.14);
    /* User / owner voice: the owner's chat bubble + sender accents. Tokenized so
       the hue lives in one place and stays clear of the project fuchsia (C7.2). */
    --user-bubble-from: rgba(71, 128, 210, 0.24);
    --user-sender: #a0c6ff;
    --user-text: #efeff2;
    /* System / owner-control voice (system bubble + sender), tokenized so it tracks one source (C7.2): in dark a
       neutral body with a gold name, since gold goes olive as a dark fill; in light an amber tint. */
    --system-bubble-bg: rgba(255, 255, 255, 0.055);
    --system-sender: #f7cb58;
    --system-text: #efeff2;
    /* Assistant / agent voice: the assistant bubble's sender + body tints,
       tokenized like the user/system voices above (C7.2). */
    --assistant-sender: #feaaae;
    --assistant-text: #efeff2;
    /* A reply quote's author ink when Ouroboros is quoted: its sender colour, and the voice's readable word inside
       the dark Ouroboros voice (its material, after the light block). */
    --quote-assistant-ink: var(--assistant-sender);
    --blue: #3b82f6;
    --amber: #f7cb58;
    --amber-dim: rgba(255, 255, 255, 0.07);
    /* The saturated ok hue of dots, tracks and switches (status text reads --status-ok-fg); light restates its own. */
    --green: #2fd359;
    --green-dim: rgba(255, 255, 255, 0.07);
    --red: #ef4444;
    --red-dim: rgba(255, 255, 255, 0.07);
    /* A rule only where a table or a section needs one; a field's hairline (the one edge the bible keeps, principle 2). */
    --divider: rgba(var(--neutral-rgb), 0.06);
    --field-border: rgba(var(--neutral-rgb), 0.08);
    /* Chart chrome (grid lines, tick/legend text) and the four default series of a
       chart the host draws (widgets, living cards), in the owner's order; a fifth
       series takes the first again. Chart.js paints to a canvas and cannot read CSS,
       so these tokens are the SSOT that theme_palette.js copies into every mounted
       instance — the canvas twin of --diagram-*. */
    --chart-grid: rgba(var(--neutral-rgb), 0.10);
    --chart-text: #a4a4a4;
    --chart-series-1: #ee6780;
    --chart-series-2: #60a5fa;
    --chart-series-3: #3fdca3;
    --chart-series-4: #fbbf24;
    /* The native select arrow is a data-URI, so its stroke cannot read a token
       at paint time; the whole image is the token instead. Painting a near-white
       arrow on the light surface is what made the arrows look washed out. */
    --select-arrow: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 14 14'%3E%3Cpath d='M3 5l4 4 4-4' fill='none' stroke='%23efeff2' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
    --diagram-font: Inter, system-ui, sans-serif;
    --diagram-bg: #131415;
    --diagram-primary: #212223;
    --diagram-primary-text: #efeff2;
    --diagram-border: #717171;
    --diagram-line: #989898;
    --diagram-secondary: #28292a;
    --diagram-tertiary: #151617;
    --ui-modal-bg: rgba(var(--modal-rgb), 0.98);
    /* Status triples: fg/bg/border named together, never derived
       (docs/DESIGN.md "Status and chips"). The fg tints are readable at 12px on the dark room, where a status body
       is a neutral veil carrying the hue's ink (a dark tint of green, amber or red goes olive or brown); inside the
       dark Ouroboros voice the body is that voice's opaque capsule step and the red ink its readable word (the
       material, after the light block). The red ink is a step lighter than the crimson word; the saturated
       --green/--amber/--red stay for dots, tracks and other large indicators. */
    --status-ok-fg: #83e499;
    --status-ok-bg: rgba(255, 255, 255, 0.08);
    --status-ok-border: rgba(255, 255, 255, 0.1);
    --status-warn-fg: #f7cb58;
    --status-warn-bg: rgba(255, 255, 255, 0.08);
    --status-warn-border: rgba(255, 255, 255, 0.1);
    --status-error-fg: #fe9498;
    --status-error-bg: rgba(255, 255, 255, 0.08);
    --status-error-border: rgba(255, 255, 255, 0.1);
    --status-info-fg: #a0c6ff;
    /* A warning or a failure on a row whose body cannot show it: on dark the body stays a neutral veil, so the row's
       words (a field: its hairline) take the hue's ink. The light block unsets both: there the tinted body says it,
       and each reader falls back to its own ink. */
    --status-warn-word: var(--status-warn-fg);
    --status-error-word: var(--status-error-fg);
    --status-neutral-fg: var(--text-meta);
    --status-neutral-bg: rgba(var(--neutral-rgb), 0.06);
    --status-neutral-border: rgba(var(--neutral-rgb), 0.12);
    /* A work's live word and its chip body (the Working chip): the crimson word on its tint, and inside the dark
       Ouroboros voice a pulse ink on the voice's capsule step. Panic's word: the crimson word on dark, the failure
       ink on light. */
    --status-live-fg: var(--accent-light);
    --status-live-bg: var(--accent-10);
    --panic-ink: var(--accent-light);
    /* Panic's body (style.css .chat-header-btn.danger): on dark the red tone is already the capsule's neutral veil, and
       stacking it would wash out Panic's ink, so the capsule's own fill; light lays the red tone over it. */
    --panic-fill: var(--chip-float);
    --panic-fill-hover: var(--chip-float-hover);
    --focus-accent-border: #fe7d85;
    --focus-accent-ring: rgba(211, 45, 67, 0.28);
    --page-header-padding-y: 10px;
    --page-header-padding-x: 20px;
    --page-header-padding-bottom: 6px;
    --pill-padding-x: 14px;
    --pill-radius: 999px;
    /* The control box (DESIGN.md principle 4; owner 06.11.2026, Q1 = A): one height for a control at rest, one for a
       control inside a row, one for floating chrome (the header's capsules; a coarse pointer raises it to the touch
       minimum, below). A page picks a size by setting --control-h on a scope, never by writing the box: the lock
       after the control recipes makes a page's box declaration on a shared control do nothing. The inset, the corner
       and the text are tokens the same way; the text stays one token until the board picks 13 or 14 px. */
    --control-h: 34px;
    --control-h-sm: 28px;
    --control-h-float: 30px;
    --control-pad-x: var(--space-4);
    --control-pad-y: 0px;
    --control-radius: var(--pill-radius);
    --button-font-size: var(--type-body);
    /* A floating group's inset, which its negative margin cancels so grouping moves no control. */
    --chrome-inset: calc(var(--space-1) + 1px);
    /* Airy dopamine seed (DESIGN.md design bible): the dark values; the light theme's follow in its block. One set of
       recipes reads them in both themes. The dark theme draws the same roles its own way: a deep graphite room, each
       neutral height a white veil over what lies under it, the voices as deep opaque sapphire and cherry at one
       weight (a tint over black turns brown), gold only as a rail and a word, controls as glass over what scrolls
       under them; inside a voice its own opaque steps (the voice materials, after the light block). */
    --elev-rest: 0 1px 2px rgba(0, 0, 0, 0.4), 0 6px 20px rgba(0, 0, 0, 0.28);
    --elev-float: 0 2px 8px rgba(0, 0, 0, 0.45), 0 18px 44px rgba(0, 0, 0, 0.5);
    /* The one veil under every layer: dialogs, the Project panel, the file dialog and the reader, the feed under a row
       lifted by a long press. */
    --scrim: rgba(0, 0, 0, 0.45);
    --radius-object: 18px;
    --radius-inner: 12px;
    --radius-sheet: 24px;
    /* One inset for every card on the room (block, inline; the board picks 16/24 or 16/18). */
    --card-pad: 16px 24px;
    --surface-object: rgba(255, 255, 255, 0.055);
    /* The object's colour as one opaque paint (its veil over --bg-primary), for marks that must cover what they
       cross: a chart's label halos and dot rings. */
    --surface-object-opaque: #1c1c1f;
    --surface-capsule: rgba(255, 255, 255, 0.09);
    --surface-capsule-hover: rgba(255, 255, 255, 0.135);
    --chip-float: linear-gradient(var(--surface-capsule), var(--surface-capsule)), rgba(var(--shell-rgb), 0.98);
    --chip-float-hover: linear-gradient(var(--surface-capsule-hover), var(--surface-capsule-hover)), rgba(var(--shell-rgb), 0.98);
    /* The opaque neutral sheet (DESIGN principle 2): the capsule's step over the room's own ink, fully opaque,
       for a list of rows that must stay readable over any bubble. Both themes read it through these two tokens. */
    --sheet-float: linear-gradient(var(--surface-capsule), var(--surface-capsule)), rgb(var(--shell-rgb));
    /* The popups' backing, the same way opaque: the float colour over the room's ink, for menus and dialogs. */
    --popup-float: linear-gradient(var(--surface-float), var(--surface-float)), rgb(var(--shell-rgb));
    --surface-float: rgba(32, 32, 34, 0.92);
    --surface-tray: rgba(var(--neutral-rgb), 0.045);
    --surface-side: #0f0f11;
    --glass: blur(18px);
    --voice-owner: #103e7f;
    --voice-ouroboros: #76162b;
    --voice-ouroboros-card: #76162b;
    /* A note between works (DESIGN "Replies"): the voice one quiet step back toward the room, its hover a step less;
       the cards peeking below a stack's front card are that material flattened toward the room again, each level
       more. Flat colours: the ledges carry the rest shadow, which an opacity would fade with them. Written as values
       (the voice at 74 % / 84 % over the room; the note at 84 % / 66 %), since older WebKit has no color-mix(). */
    --voice-ouroboros-note: #5b1424;
    --voice-ouroboros-note-hover: #661527;
    --relay-ledge-1: #4f1321;
    --relay-ledge-2: #41121e;
    --tone-crimson: #d32d43;
    --tone-crimson-hover: #c32a3f;
    /* The crimson tone's ink: white on the solid crimson. Its hover deepens rather than lightens, and so does its
       press (--press-solid), so the white keeps 4.5:1 at rest, on hover and pressed. */
    --tone-crimson-ink: #ffffff;
    /* The soft crimson: a chosen option or card (a filter, a segment, a switched-on capsule, the current step of
       setup), Material 3's tonal step below the filled crimson of the primary action, the chosen place and a count. */
    --tone-crimson-soft: #872236;
    --tone-crimson-soft-ink: #ffdfe1;
    /* The crimson tone without its fill, for a chosen control that has no body (Read aloud while it speaks, the
       Official mark): on dark that is the crimson word, since the tone's own ink is white. */
    --tone-crimson-word: var(--accent-light);
    /* The chosen place's left mark: on the solid crimson it is the fill's white ink (the accent would vanish into
       the fill). */
    --place-mark: var(--tone-crimson-ink);
    --tone-green: rgba(255, 255, 255, 0.16);
    --tone-green-hover: rgba(255, 255, 255, 0.2);
    --tone-red: rgba(255, 255, 255, 0.09);
    --tone-red-hover: rgba(255, 255, 255, 0.135);
    --tone-amber: rgba(255, 255, 255, 0.09);
    --tone-amber-hover: rgba(255, 255, 255, 0.135);
    --tone-quiet-hover: rgba(var(--neutral-rgb), 0.08);
    /* The owner's recorded answer (DESIGN "Quiz card"): on dark the chosen option of a settled question is the owner's
       sapphire under white ink, keeps its rest lift and shows the `Your choice` word (--chosen-answer-cue-display; its
       accessible name says so in every theme); the `You answered` word and its dot take the owner's blue. The asker's
       recommendation is a word of its own. On dark the controls held while an answer is sent (--quiz-held-opacity)
       and a message still waiting for its echo (--message-pending-opacity) keep full ink: their words carry the
       state. */
    --tone-chosen-answer: #235193;
    --tone-chosen-answer-ink: #ffffff;
    --tone-chosen-answer-detail: #dce9fc;
    --tone-chosen-answer-shadow: var(--elev-rest);
    --chosen-answer-cue-display: inline;
    --tone-recommended: #ffc2c6;
    --tone-recorded-answer: var(--user-sender);
    --tone-recorded-mark: var(--user-sender);
    --quiz-held-opacity: 1;
    --message-pending-opacity: 1;
    /* A state changes colour, never height: hover and press deepen a capsule's own fill (a press lightens on dark). */
    --press: brightness(1.12);
    /* A press on a solid fill under white ink (the primary action, the chosen tab) deepens instead. */
    --press-solid: brightness(0.9);
    --motion-tone: background 0.12s ease, box-shadow 0.12s ease, color 0.12s ease, filter 0.12s ease;
    /* The one curve anything that moves rides: the iOS sheet curve (DESIGN §10 "Motion"; shell_motion.js reads it). */
    --ease-sheet: cubic-bezier(0.32, 0.72, 0, 1);
    /* How long it runs: a surface coming in, a surface going out, a fade (a finger's release sets its own speed). */
    --motion-open: 340ms;
    --motion-close: 280ms;
    --motion-fade: 180ms;
    --widget-fault-status-max-width: min(240px, 35vw);
    /* App shell: persistent desktop sidebar + optional project detail pane. */
    --sidebar-width: 280px;
    --project-panel-width: clamp(340px, 36vw, 440px);
    /* One hover fill for an item in a popup menu or a picker list. Six menus
       used five different fills (0.04 / 0.06 / 0.08 / an accent tint / an
       undeclared --surface-2), so the same gesture looked like a different
       control depending on which menu you opened. */
    --menu-item-hover: rgba(var(--neutral-rgb), 0.07);
    /* 8pt spacing scale (Apple HIG / common 8pt grid) — use these instead of
       ad-hoc pixel values so spacing stays consistent across components. */
    --space-1: 4px;
    --space-2: 8px;
    --space-3: 12px;
    --space-4: 16px;
    --space-5: 24px;
    /* Smallest result measure before a neighbouring action wraps below it. */
    --result-copy-min-width: 12rem;
    /* Bounded scroll region for the Updates restore list (DESIGN section 5:
       a new visual dimension becomes a variable before a page-local literal). */
    --updates-list-max-height: 320px;
    /* The delivered-document reader (DESIGN "Document reading"): the dialog's
       largest side on a wide screen, and its one reading measure. */
    --document-reader-size: 920px;
    --document-reader-measure: 76ch;
    /* Bounded scroll window for the sidebar project list (v6.32.0 geometry): a
       share of the visual viewport height below, so the navigation column keeps
       its height whatever the project count. Distinct from the fixed Updates
       window above: one is a page-local list, this one follows the viewport. */
    /* Chat media (DESIGN "Chat attachments"): a whole single photo keeps its own
       ratio up to this height, a bubble carrying several media stops at this
       width, and a staged composer file shows a thumbnail of this size. */
    --chat-photo-max-height: min(340px, calc(var(--vvh) * 0.5));
    /* A single photo's smallest box: its `•••` (a touch target at the actions'
       7px inset on each side) stays whole inside however small the photo is. */
    --chat-photo-min-box: calc(var(--touch-target-min) + 14px);
    --chat-media-bubble-max: 520px;
    --chat-attach-thumb-size: 28px;
    /* Message fields (DESIGN §3, "Quiz card"): the composer's control height, which a question's Send shares; the one
       cap a message field grows to before it scrolls (chat_input_sizing.js reads it as the field's max-height); and
       the row a question's voice line and head share, its × as tall as the row. */
    --composer-control-height: 32px;
    --message-field-max-height: 140px;
    --quiz-row-height: 28px;
    /* The smallest comfortable touch target; a coarse pointer (a phone) raises it to a finger's 44px (below). */
    --touch-target-min: 36px;
    /* The hit extension's inset (the recipe below): out only on the sides that fall short of the touch minimum. The
       percentage resolves where it is used, against the control's own box. */
    --ui-hit-inset: min(0px, calc((100% - var(--touch-target-min)) / 2));
    --vvh: 100dvh;
    /* The shell's insets (DESIGN "Shell"): the system's safe area, reported by the browser as env() and inside
       Telegram as --tg-safe-area-inset-* (the SDK writes them inline; host_telegram.js mirrors its object; the same
       inset, so the larger wins, never the sum), plus Telegram's own controls over the content
       (--tg-content-safe-area-inset-*). Outside Telegram the Telegram tokens rest at 0. */
    --tg-safe-area-inset-top: 0px;
    --tg-safe-area-inset-bottom: 0px;
    --tg-content-safe-area-inset-top: 0px;
    --tg-content-safe-area-inset-bottom: 0px;
    --inset-top: calc(max(env(safe-area-inset-top, 0px), var(--tg-safe-area-inset-top)) + var(--tg-content-safe-area-inset-top));
    --inset-bottom: calc(max(env(safe-area-inset-bottom, 0px), var(--tg-safe-area-inset-bottom)) + var(--tg-content-safe-area-inset-bottom));
    /* The shell's stacking order (DESIGN "Shell"), lowest first: Main's floating header, the sidebar column (on a
       phone the drawer, which lies under the room), the room (on a phone the card the drawer pushes aside), the
       sidebar's resize handle, the Project panel over the content area (its scrim beneath it; the stack layout's
       full-width Project a step above), the reply mark of a message being swiped, a sent draft rising into its feed
       (send_flight.js), the drawer's way back (its clear
       scrim over the room's visible quarter); popovers (.ui-popup) stay the top of everything. */
    --z-page-header: 5;
    --z-sidebar: 10;
    --z-room: 15;
    --z-resize-handle: 20;
    --z-project-scrim: 55;
    --z-project-panel: 58;
    --z-project-layer: 60;
    --z-row-reply-mark: 62;
    --z-send-flight: 64;
    --z-drawer-scrim: 68;
    --z-popover: 1200;
    /* Quote panel glass (DESIGN "Replies"): the shell colour at one alpha, so a quote reads lighter than the bubble it sits in. */
    --quote-glass: rgba(var(--shell-rgb), 0.7);
}

:root[data-theme="light"] {
    --native-color-scheme: light;
    --neutral-rgb: 32, 35, 43;
    --shell-rgb: 255, 255, 255;
    --composer-rgb: 244, 245, 248;
    --modal-rgb: 255, 255, 255;
    --menu-rgb: 250, 251, 253;
    --media-rgb: 244, 245, 248;
    --wizard-panel-rgb: 255, 255, 255;
    --wizard-rgb: 255, 255, 255;
    --code-bg: #f2f4f7;
    --bg-primary: #ffffff;
    --bg-secondary: #f5f6f8;
    --text-primary: #20232b;
    --text-meta: #555d69;
    --text-secondary: #626977;
    --text-disabled: #89909b;
    --accent-light: #bf2f40;
    --project: #8f2499;
    --user-sender: #24558d;
    --user-text: #233b58;
    --system-sender: #805000;
    --system-text: #58451f;
    --assistant-sender: #bf2f40;
    --assistant-text: #35272c;
    --status-ok-fg: #176b49;
    --status-warn-fg: #805300;
    --status-error-fg: #b42332;
    --status-info-fg: #24558d;
    --focus-accent-border: #bf2f40;
    --chart-grid: rgba(var(--neutral-rgb), 0.14);
    --chart-text: #4a515d;
    --chart-series-1: #c0284c;
    --chart-series-2: #2c68d0;
    --chart-series-3: #1e9c76;
    --chart-series-4: #bd6a00;
    --select-arrow: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 14 14'%3E%3Cpath d='M3 5l4 4 4-4' fill='none' stroke='%2320232b' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
    --diagram-bg: #ffffff;
    --diagram-primary: #f3e8ec;
    --diagram-primary-text: #20232b;
    --diagram-border: #77717d;
    --diagram-line: #66606e;
    --diagram-secondary: #edf0f5;
    --diagram-tertiary: #f5f6f8;
    /* Airy dopamine seed, light values (the dark ones are in :root): white objects and capsules on two soft shadows. */
    --elev-rest: 0 1px 2px rgba(28, 33, 51, 0.05), 0 6px 18px rgba(28, 33, 51, 0.06);
    --elev-float: 0 2px 6px rgba(28, 33, 51, 0.06), 0 18px 44px rgba(28, 33, 51, 0.11);
    --scrim: rgba(16, 18, 28, 0.24);
    --surface-object: #ffffff;
    --surface-object-opaque: var(--surface-object);
    --surface-capsule: #ffffff;
    --surface-capsule-hover: #f5f6f8;
    --chip-float: var(--surface-capsule);
    --chip-float-hover: var(--surface-capsule-hover);
    --surface-float: #ffffff;
    --surface-tray: rgba(var(--neutral-rgb), 0.03);
    --surface-side: #fafbfc;
    --glass: none;
    --voice-owner: #e9eef9;
    --voice-ouroboros: #fdf2f4;
    --voice-ouroboros-card: #fff8f9;
    /* The note between works: the work card's rose as a value under the note's own name (the card's token stays the
       card's), hovering to the voice; on white a card behind lies in the front card's shadow, a deeper step of the
       accent ladder, not a paler one. */
    --voice-ouroboros-note: #fff8f9;
    --voice-ouroboros-note-hover: var(--voice-ouroboros);
    --relay-ledge-1: #faebec;  /* the accent at 10 % over white */
    --relay-ledge-2: #f7e3e5;  /* the accent at 14 % over white */
    /* The crimson tone is the accent ladder's; its hover stops at 14 % so the ink keeps 4.5:1 on it. */
    --tone-crimson: var(--accent-10);
    --tone-crimson-hover: var(--accent-14);
    /* One step deeper than the accent text, so a tonal capsule keeps 4.5:1 on a voice-tinted backing too. */
    --tone-crimson-ink: #b52b3b;
    /* One pale crimson for both strengths: on white a tint is already quiet. */
    --tone-crimson-soft: var(--tone-crimson);
    --tone-crimson-soft-ink: var(--tone-crimson-ink);
    --tone-crimson-word: var(--tone-crimson-ink);
    --place-mark: var(--accent);
    --tone-green: rgba(34, 197, 94, 0.14);
    --tone-green-hover: rgba(34, 197, 94, 0.22);
    --tone-red: rgba(239, 68, 68, 0.10);
    --tone-red-hover: rgba(239, 68, 68, 0.16);
    --tone-amber: rgba(245, 158, 11, 0.18);
    --tone-amber-hover: rgba(245, 158, 11, 0.26);
    --tone-quiet-hover: rgba(28, 33, 51, 0.06);
    --press: brightness(0.95);
    --press-solid: var(--press);
    /* The light theme's own values of tokens the dark seed in :root sets differently (hues, status triples, the
       System voice, the owner's quote tint, the Project's tints). */
    --user-bubble-from: rgba(80, 120, 190, 0.14);
    --system-bubble-bg: rgba(245, 158, 11, 0.12);
    --accent: #c93545;
    --focus-accent-ring: var(--accent-10);
    --amber: #f59e0b;
    --green: #22c55e;
    --amber-dim: rgba(245, 158, 11, 0.15);
    --status-warn-bg: var(--amber-dim);
    --status-warn-border: rgba(245, 158, 11, 0.30);
    --red-dim: rgba(239, 68, 68, 0.15);
    --status-error-bg: var(--red-dim);
    --status-error-border: rgba(239, 68, 68, 0.30);
    --green-dim: rgba(34, 197, 94, 0.15);
    --status-ok-bg: var(--green-dim);
    --status-ok-border: rgba(34, 197, 94, 0.28);
    --status-warn-word: initial;
    --status-error-word: initial;
    --project-08: rgba(232, 121, 249, 0.08);
    --project-12: rgba(232, 121, 249, 0.12);
    --project-18: rgba(232, 121, 249, 0.18);
    /* The answer roles and Panic's word on white: the ok pair flat for the chosen answer (only its accessible name says
       `Your choice`), meta words and the ok dot for `You answered`, the faded held controls and pending message. */
    --panic-ink: var(--status-error-fg);
    --panic-fill: linear-gradient(var(--tone-red), var(--tone-red)), var(--chip-float);
    --panic-fill-hover: linear-gradient(var(--tone-red-hover), var(--tone-red-hover)), var(--chip-float);
    --tone-chosen-answer: var(--tone-green);
    --tone-chosen-answer-ink: var(--status-ok-fg);
    --tone-chosen-answer-detail: var(--text-meta);
    --tone-chosen-answer-shadow: none;
    --chosen-answer-cue-display: none;
    --tone-recommended: var(--status-ok-fg);
    --tone-recorded-answer: var(--text-meta);
    --tone-recorded-mark: var(--green);
    --quiz-held-opacity: .75;
    --message-pending-opacity: .78;
}

/* The dark voices' materials (DESIGN principle 2 and the seed table). Inside the Ouroboros voice (its answers and its
   work cards, finished ones too) trays and capsules are opaque lighter steps of that voice under opaque readable ink,
   and the tones and status bodies take those steps, so a word keeps its contrast at rest, hover and press. A custom
   property resolves where it is declared, so the aliases :root computed (the legacy muted ink, the card bodies, the
   crimson word, the failure word) are restated here. Inside the owner's voice its links, focus and quiet words take
   the owner's readable blue. A light document keeps every inherited value. */
:root:not([data-theme="light"]) :is(.chat-bubble.assistant, .chat-live-card) {
    --text-primary: #fcf9f9;
    --assistant-text: var(--text-primary);
    --text-secondary: #ebd9da;
    --text-meta: #f2e4e5;
    --text-disabled: #e1cbcc;
    --text-muted: var(--text-disabled);
    --surface-capsule: #8f263a;
    --surface-capsule-hover: #9e3044;
    --surface-tray: #832336;
    --status-neutral-bg: var(--surface-capsule);
    --status-neutral-fg: var(--text-secondary);
    --tone-quiet-hover: var(--surface-capsule);
    --tone-green: var(--surface-capsule);
    --tone-green-hover: var(--surface-capsule-hover);
    --tone-red: var(--surface-capsule);
    --tone-red-hover: var(--surface-capsule-hover);
    --tone-amber: var(--surface-capsule);
    --tone-amber-hover: var(--surface-capsule-hover);
    --status-ok-bg: var(--surface-capsule);
    --status-warn-bg: var(--surface-capsule);
    --status-error-bg: var(--surface-capsule);
    --status-live-fg: #ffb3b7;
    --status-live-bg: var(--surface-capsule);
    --accent-light: #ffc2c6;
    --accent-08: var(--surface-capsule);
    --accent-10: var(--surface-capsule);
    --accent-18: var(--surface-capsule-hover);
    --tone-crimson-word: var(--accent-light);
    --quote-assistant-ink: var(--accent-light);
    --focus-accent-border: var(--accent-light);
    --status-error-fg: var(--accent-light);
    --status-error-word: var(--status-error-fg);
    --project: #f6c4fc;
    --project-08: var(--surface-capsule);
    --project-12: var(--surface-capsule-hover);
    --project-18: var(--surface-capsule-hover);
    --status-info-fg: #b6d4ff;
}
:root:not([data-theme="light"]) .chat-bubble.user {
    --accent-light: var(--user-sender);
    --tone-crimson-word: var(--accent-light);
    --focus-accent-border: var(--accent-light);
    --text-disabled: var(--user-sender);
    --text-muted: var(--text-disabled);
}
/* Code stays on the room's neutral material inside either voice. */
.md-code-block { --tone-quiet-hover: var(--menu-item-hover); }

.ouro-ui { font-family: var(--font-sans); color-scheme: var(--native-color-scheme); }
.muted { color: var(--text-meta); }
/* Appearance tri-state (Light / Dark / System), rendered by theme.js. It lives in
   ui.css, not settings.css, because standalone onboarding loads ui.css only and
   must offer the identical control — one look, one keyboard model, both surfaces. */
.theme-choice-group {
    display: inline-grid;
    grid-auto-flow: column;
    grid-auto-columns: minmax(0, 1fr);
    gap: 8px;
}
.theme-choice {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    text-align: center;
    box-sizing: border-box;
    height: var(--control-h);
    line-height: 1.2;
    border: 1px solid transparent;
    border-radius: var(--control-radius);
    background: var(--surface-capsule);
    color: var(--text-secondary);
    box-shadow: var(--elev-rest);
    padding: var(--control-pad-y) var(--control-pad-x);
    cursor: pointer;
    font-family: var(--font-sans);
    font-size: var(--button-font-size);
    font-weight: 600;
    transition: var(--motion-tone);
}
.theme-choice:hover { color: var(--text-primary); background: var(--surface-capsule-hover); }
.theme-choice[aria-checked="true"] { background: var(--tone-crimson-soft); color: var(--tone-crimson-soft-ink); }
.theme-choice:not(:disabled):active { filter: var(--press); }
.theme-choice:focus-visible { outline: 2px solid var(--focus-accent-border); outline-offset: 2px; }
.theme-choice-label {
    display: block;
    color: var(--text-meta);
    font-size: var(--type-meta);
    line-height: var(--line-meta);
}
.theme-status { color: var(--text-meta); font-size: var(--type-meta); line-height: var(--line-meta); }
.theme-status:empty { display: none; }

/* ---- One control, three tones (DESIGN.md design bible) ---------------------------------------------------------
   Everything pressable with a body is a capsule standing on the rest shadow, in every state and either theme:
   neutral (the capsule surface), tonal (its hue's fill and that fill's ink: the crimson for the primary action, the
   chosen tab or place and a count, the soft crimson for a chosen option or card, green for save, red for danger, amber
   for the owner's turn). A quiet control (ghost, link, filter) has no body and no shadow. A state changes colour,
   never height: hover deepens the fill a step, a press deepens it again (--press), a chosen one takes its tone; what
   cannot be pressed now loses its shadow. Page styles map their controls onto these tones and add no fourth recipe
   and no outline; an author frame may override any of it with its own CSS after this sheet. */
.btn {
    box-sizing: border-box;
    height: var(--control-h);
    padding: var(--control-pad-y) var(--control-pad-x);
    border-radius: var(--control-radius);
    border: 1px solid transparent;
    /* Without a tone a button is quiet (no body, the primary ink), never the engine's own grey. */
    background: transparent;
    color: var(--text-primary);
    font-size: var(--button-font-size);
    font-weight: 600;
    font-family: var(--font-sans);
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    text-align: center;
    transition: var(--motion-tone);
}

.btn:focus-visible {
    outline: 2px solid var(--focus-accent-border);
    outline-offset: 2px;
}

.btn:not(:disabled):active { filter: var(--press); }
.btn:disabled {
    cursor: not-allowed;
    opacity: var(--control-disabled-opacity);
    box-shadow: none;
}

/* The solid crimson under white ink deepens when pressed (--press-solid), so its words keep 4.5:1. */
.btn-primary { background: var(--tone-crimson); color: var(--tone-crimson-ink); box-shadow: var(--elev-rest); --press: var(--press-solid); }
.btn-primary:not(:disabled):hover { background: var(--tone-crimson-hover); }
/* An action the owner can take is legible at rest (docs/DESIGN.md section 6): a neutral button carries the primary
   ink, since a label parked at secondary ink reads as disabled next to the one beside it. */
.btn-default { background: var(--surface-capsule); color: var(--text-primary); box-shadow: var(--elev-rest); }
.btn-default:not(:disabled):hover { background: var(--surface-capsule-hover); }
.btn-ghost { background: transparent; color: var(--text-primary); box-shadow: none; }
.btn-ghost:not(:disabled):hover { background: var(--tone-quiet-hover); }
.btn-danger { background: var(--tone-red); color: var(--status-error-fg); box-shadow: var(--elev-rest); }
.btn-danger:not(:disabled):hover { background: var(--tone-red-hover); }
.btn-save { background: var(--tone-green); color: var(--status-ok-fg); box-shadow: var(--elev-rest); }
.btn-save:not(:disabled):hover { background: var(--tone-green-hover); }
/* A capsule over content uses the same geometry, focus and disabled state, on glass. */
.btn-float {
    background: var(--surface-float); color: var(--text-primary); box-shadow: var(--elev-float);
    -webkit-backdrop-filter: var(--glass); backdrop-filter: var(--glass);
}
@media (hover: hover) {
    .btn-float:not(:disabled):hover { filter: var(--press); }
}
.btn-float:not(:disabled):active { filter: var(--press) var(--press); }
/* A control inside a row is the small capsule: the row height, a tighter inset, the meta text size. */
.btn-sm { --control-h: var(--control-h-sm); --control-pad-x: var(--space-3); --button-font-size: var(--type-meta); }

/* Tabs (DESIGN.md design bible): a tab is a neutral capsule with the secondary ink; the chosen tab (a place) takes the
   crimson tone and stays standing; a press deepens the colour, nothing changes height. The strip that scrolls them is
   the page's (style.css .app-tab-strip). */
.app-tab {
    --control-pad-x: var(--pill-padding-x);
    flex: 0 0 auto;
    white-space: nowrap;
    box-sizing: border-box;
    height: var(--control-h);
    border: 1px solid transparent;
    background: var(--surface-capsule);
    color: var(--text-secondary);
    box-shadow: var(--elev-rest);
    border-radius: var(--control-radius);
    padding: var(--control-pad-y) var(--control-pad-x);
    font: inherit;
    font-size: var(--button-font-size);
    font-weight: 600;
    cursor: pointer;
    transition: var(--motion-tone);
}
.app-tab:hover { background: var(--surface-capsule-hover); box-shadow: var(--elev-rest); color: var(--text-primary); }
.app-tab.active,
.app-tab.is-active { background: var(--tone-crimson); color: var(--tone-crimson-ink); box-shadow: var(--elev-rest); --press: var(--press-solid); }
.app-tab:not(:disabled):active { filter: var(--press); }
.app-tab:disabled { box-shadow: none; }
.app-tab:focus-visible { outline: 2px solid var(--focus-accent-border); outline-offset: 2px; }
/* A tab's count (Skills: the jobs in flight) is a neutral count pill on the shared type scale: it says how many,
   while amber would say the owner has a move to make. */
.app-tab-pill {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    margin-left: 6px;
    padding: 0 6px;
    min-width: 18px;
    height: 18px;
    border-radius: var(--pill-radius);
    background: var(--status-neutral-bg);
    color: var(--status-neutral-fg);
    font-size: var(--type-meta);
    font-weight: 600;
    line-height: 1;
    font-variant-numeric: tabular-nums;
}

/* Filters are quiet controls: no body, so no shadow; a chosen filter takes the soft crimson. Their words carry the
   controls' weight (DESIGN type line), as .btn and .app-tab do. */
.filter-chip {
    --control-pad-x: var(--space-3);
    --button-font-size: var(--type-meta);
    box-sizing: border-box;
    height: var(--control-h);
    padding: var(--control-pad-y) var(--control-pad-x);
    border-radius: var(--control-radius);
    border: 1px solid transparent;
    background: transparent;
    color: var(--text-secondary);
    box-shadow: none;
    font-size: var(--button-font-size);
    font-weight: 600;
    font-family: inherit;
    cursor: pointer;
    transition: var(--motion-tone);
}
.filter-chip:hover { background: var(--tone-quiet-hover); }
.filter-chip.active { background: var(--tone-crimson-soft); color: var(--tone-crimson-soft-ink); }
.filter-chip:not(:disabled):active { filter: var(--press); }
.filter-chip:focus-visible { outline: 2px solid var(--focus-accent-border); outline-offset: 2px; }

/* A segmented choice (one option of a single-select group, page_header.js renderSegmentedField) is the one capsule
   control: neutral options standing on the rest shadow, the chosen one in the soft crimson and still standing, no
   outline. Hover and press change colour, never height. A word longer than its column (a translated label) breaks
   rather than overflowing, so a segment keeps a minimum height, not a fixed one. A setting's value is never amber. */
.ui-segment {
    --control-pad-x: var(--space-3);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    text-align: center;
    box-sizing: border-box;
    min-height: var(--control-h);
    line-height: 1.2;
    border: 1px solid transparent;
    border-radius: var(--control-radius);
    background: var(--surface-capsule);
    color: var(--text-secondary);
    box-shadow: var(--elev-rest);
    padding: var(--space-1) var(--control-pad-x);
    cursor: pointer;
    font-size: var(--button-font-size);
    font-weight: 600;
    overflow-wrap: anywhere;
    transition: var(--motion-tone);
}
.ui-segment:not(:disabled):not(.active):hover { background: var(--surface-capsule-hover); color: var(--text-primary); }
.ui-segment.active { background: var(--tone-crimson-soft); color: var(--tone-crimson-soft-ink); }
.ui-segment:not(:disabled):active { filter: var(--press); }
.ui-segment:disabled { cursor: not-allowed; opacity: var(--control-disabled-opacity); box-shadow: none; }
.ui-segment:focus-visible { outline: 2px solid var(--focus-accent-border); outline-offset: 2px; }

/* An object on the room (DESIGN.md design bible): one flat fill on the rest shadow with the object radius, no edge
   (the transparent 1px border only keeps the box's size). Inside an object a group or row is a tray
   (--surface-tray, --radius-inner, no shadow); fills never stack. */
.ui-card {
    border: 1px solid transparent;
    background: var(--surface-object);
    border-radius: var(--radius-object);
    box-shadow: var(--elev-rest);
    padding: var(--card-pad);
}
/* On a phone a card keeps 18 px at its sides, so a three-way choice fits its row instead of wrapping two and one
   (owner, board of 06.11: 24 px on a desk, 18 px on a phone). */
html[data-layout="stack"] { --card-pad: var(--space-4) 18px; }

/* ---- The control box lock (owner decision 06.11.2026, Q1 = A) ---------------------------------------------------
   Why it exists: one button had eight heights (18–39 px) because any page sheet could write height, padding, radius,
   border or font-size on a shared control and win by specificity, and nothing but a screenshot noticed. The 06.11
   cleanup deleted those rules; this lock keeps them from returning by construction: in the product documents a page
   declaration on the box of these roles does nothing, so the next author reaches for the token. It holds only the
   box; material, words and places are held by deletion, review and the owner's eye (his 2C of 05.11).
   How to pick a size: set a token on a scope, never the property —
     --control-h: var(--control-h-sm)      a control inside a row (.btn-sm does this)
     --control-h: var(--control-h-float)   floating chrome (the docked header toolbar scope does this)
     --control-h: auto                     a control that carries a paragraph (style.css .chat-file-card), with
     --control-pad-y                       its block inset, --control-pad-x its inline inset (one or two values),
     --control-radius                      its corner (the pill, or --radius-inner for a card),
     --button-font-size                    its text (the body size at rest; .btn-sm takes the meta size).
   How to add a role: give its class the token values it needs (as .btn-sm does), add it to the :is() lists below and
   to the touch-reach family, and name it in DESIGN.md principle 4.
   Scope: the product documents only (their root is .ouro-ui: the app, setup, login). An author frame loads this sheet
   under its own root and keeps the overridable recipes above. */
:root:where(.ouro-ui) :is(.btn, .app-tab, .filter-chip, .theme-choice) {
    box-sizing: border-box !important;
    height: var(--control-h) !important;
    min-height: 0 !important;
    padding-block: var(--control-pad-y) !important;
    padding-inline: var(--control-pad-x) !important;
    border: 1px solid transparent !important;
    border-radius: var(--control-radius) !important;
    font-size: var(--button-font-size) !important;
}
:root:where(.ouro-ui) .ui-segment {
    box-sizing: border-box !important;
    height: auto !important;
    min-height: var(--control-h) !important;
    padding-block: var(--space-1) !important;
    padding-inline: var(--control-pad-x) !important;
    border: 1px solid transparent !important;
    border-radius: var(--control-radius) !important;
    font-size: var(--button-font-size) !important;
}

/* ---- The shared parts (DESIGN.md principles 6 and 7; the 06.11 convergence) -------------------------------------
   Five decisions each have one owner here and screens compose them: the result of an action (.ui-action-row +
   .ui-result, ui_primitives.js setResult / errorWords), main vs Details (details.ui-disclosure), a row of one fact
   (.ui-row), the length of a list (.ui-list-more), and the one primary action of a place (the [data-primary] slot).
   The contract of each part is its comment here and its home test (web/tests/ui_primitives.test.js). */

/* The action row: the trigger, then its result as the next sibling, 8px apart. The result keeps one meta line of
   height from the start (reserved, never display: none), so a late word never moves the control or its neighbours;
   in a narrow container the result wraps under the trigger into that same reserved line. */
.ui-action-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2) var(--space-3);
    min-width: 0;
}
.ui-action-row > .ui-result { flex: 1 1 var(--result-copy-min-width); }
/* The result: a status sentence (the meta size, a dot and a word in the tone's ink) with the raw text, if any, behind
   one disclosure. ui_primitives.js setResult writes it; a plain acknowledgement ends by itself, a warning, a needed
   restart or a failure stays until the next result or an edit in the same subject replaces it (DESIGN "Status and
   chips"). */
.ui-result {
    display: block;
    min-width: 0;
    min-height: calc(var(--type-meta) * var(--line-meta));
    overflow-wrap: anywhere;
}
.ui-result > .ui-disclosure { margin-top: var(--space-1); }
.ui-result-details {
    margin: var(--space-1) 0 0;
    padding: var(--space-2) var(--space-3);
    border-radius: var(--radius-inner);
    background: var(--surface-tray);
    color: var(--text-secondary);
    font-family: 'SF Mono', 'Fira Code', Consolas, monospace;
    font-size: var(--type-meta);
    line-height: var(--line-meta);
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}

/* The one disclosure look: a quiet summary with one caret, the secondary ink, the meta size; what it opens keeps its
   own layout. A section's lead stays in the main view; the rest, a paragraph or a record, lives behind this. */
details.ui-disclosure > summary {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    min-width: 0;
    list-style: none;
    cursor: pointer;
    color: var(--text-secondary);
    font-size: var(--type-meta);
    line-height: var(--line-meta);
    font-weight: 500;
}
details.ui-disclosure > summary::-webkit-details-marker { display: none; }
details.ui-disclosure > summary::before {
    content: "";
    flex: none;
    width: 6px;
    height: 6px;
    margin-inline: 2px var(--space-1);
    border-right: 1.5px solid currentColor;
    border-bottom: 1.5px solid currentColor;
    transform: rotate(-45deg);
    transition: transform var(--motion-fade) var(--ease-sheet);
}
details.ui-disclosure[open] > summary::before { transform: rotate(45deg); }
details.ui-disclosure > summary:hover { color: var(--text-primary); }
details.ui-disclosure > summary:focus-visible { outline: 2px solid var(--focus-accent-border); outline-offset: 2px; border-radius: var(--radius-inner); }
details.ui-disclosure > :not(summary) { margin-top: var(--space-2); }

/* The row: one fact as a tray inside its object — a title the CSS clamps to two lines, one meta line under it, and
   its actions at the end; in a narrow row the actions move under the words (a container query, never a viewport
   one). The words for titles, meta and counts come from the caller. */
.ui-row {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas: "words actions" "meta actions";
    align-items: center;
    column-gap: var(--space-3);
    row-gap: var(--space-1);
    min-width: 0;
    padding: var(--space-3);
    border: 1px solid transparent;
    border-radius: var(--radius-inner);
    background: var(--surface-tray);
    container-type: inline-size;
}
.ui-row-title {
    grid-area: words;
    min-width: 0;
    margin: 0;
    color: var(--text-primary);
    font-size: var(--type-body);
    line-height: var(--line-title);
    font-weight: 500;
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    overflow: hidden;
    overflow-wrap: anywhere;
}
.ui-row-meta {
    grid-area: meta;
    min-width: 0;
    margin: 0;
    color: var(--text-meta);
    font-size: var(--type-meta);
    line-height: var(--line-meta);
    overflow-wrap: anywhere;
}
.ui-row-meta:empty { display: none; }
.ui-row-actions {
    grid-area: actions;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: flex-end;
    gap: var(--space-2);
    min-width: 0;
}
/* Narrow row: the children query the row itself (its container-type); a rule on .ui-row inside the query would match an
   ancestor container instead, because an element never queries its own size. */
@container (max-width: 28rem) {
    .ui-row > :is(.ui-row-title, .ui-row-meta, .ui-row-actions) { grid-column: 1 / -1; }
    .ui-row > .ui-row-title { grid-row: 1; }
    .ui-row > .ui-row-meta { grid-row: 2; }
    .ui-row > .ui-row-actions { grid-row: 3; justify-content: flex-start; }
}
.ui-row + .ui-row { margin-top: var(--space-2); }

/* The list's one length control: "Show all (N)" is a quiet small capsule at the list's start edge; the label and its
   count come from the caller. */
.ui-list-more { align-self: flex-start; justify-self: start; margin-top: var(--space-2); }

/* The primary slot (DESIGN principle 4: the crimson is the one primary action of a place). The containers that have
   one — the page header's actions, a section's actions, an action row, a dialog's foot — paint their [data-primary]
   control crimson; a second [data-primary] in the same slot is neutral, so two crimsons in one place cannot be
   expressed. Any other crimson there is a page-level choice the review sees. */
.ui-section-actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); min-width: 0; }
:is(.app-page-actions, .ui-section-actions, .ui-action-row, .marketplace-modal-actions, .ui-dialog-foot) > [data-primary] {
    background: var(--tone-crimson);
    color: var(--tone-crimson-ink);
    box-shadow: var(--elev-rest);
    --press: var(--press-solid);
}
:is(.app-page-actions, .ui-section-actions, .ui-action-row, .marketplace-modal-actions, .ui-dialog-foot) > [data-primary]:not(:disabled):hover {
    background: var(--tone-crimson-hover);
}
:is(.app-page-actions, .ui-section-actions, .ui-action-row, .marketplace-modal-actions, .ui-dialog-foot) > [data-primary] ~ [data-primary] {
    background: var(--surface-capsule);
    color: var(--text-primary);
    --press: brightness(1.12);
}
:is(.app-page-actions, .ui-section-actions, .ui-action-row, .marketplace-modal-actions, .ui-dialog-foot) > [data-primary] ~ [data-primary]:not(:disabled):hover {
    background: var(--surface-capsule-hover);
}


.ui-chip,
:where(.status-badge, .mcp-server-status) {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    border-radius: 999px;
    white-space: nowrap;
}

/* The chip primitive reserves its border so a tone can paint its fill without changing the chip's size; a chip and
   a status badge never show an edge (a status chip lies flat). The aliases above keep their own geometry. */
.ui-chip {
    border: 1px solid transparent;
}

.ui-status,
.ui-result,
:where(.settings-inline-status, .wizard-runtime-status) {
    color: var(--text-secondary);
    font-size: var(--type-meta);
    line-height: var(--line-meta);
}

/* Tone primitive (docs/DESIGN.md "Status and chips"). A status states a fact
   with an EXPLICIT pair; it never derives its foreground from whatever generic
   opacity happens to sit on the element.
   Two shapes, because a status appears as two things:
     - `.ui-status[data-tone]` — a status SENTENCE: foreground only, rendered as
       dot + text. Giving it a fill would turn every inline status into a badge
       and make the loudest thing on the panel the one that says "connected".
     - `.ui-chip[data-tone]` — a status CHIP: the full triple, because the chip
       itself is the status and has nothing else to carry it.
   Tone is a STATE and must outrank the `.muted` utility: a status that carries
   both classes would lose to it under a :where()-wrapped tone (specificity 0). */
.ui-chip[data-tone="ok"] {
    color: var(--status-ok-fg);
    background: var(--status-ok-bg);
}
.ui-chip[data-tone="warn"] {
    color: var(--status-warn-fg);
    background: var(--status-warn-bg);
}
.ui-chip[data-tone="danger"],
.ui-chip[data-tone="error"] {
    color: var(--status-error-fg);
    background: var(--status-error-bg);
}
.ui-chip[data-tone="neutral"],
.ui-chip[data-tone="muted"] {
    color: var(--status-neutral-fg);
    background: var(--status-neutral-bg);
}

.ui-status[data-tone="ok"],
.ui-result[data-tone="ok"],
.settings-inline-status[data-tone="ok"],
.settings-test-result[data-tone="ok"] { color: var(--status-ok-fg); }
.ui-status[data-tone="warn"],
.ui-result[data-tone="warn"],
.settings-inline-status[data-tone="warn"],
.settings-test-result[data-tone="warn"] { color: var(--status-warn-fg); }
.ui-status[data-tone="danger"],
.ui-status[data-tone="error"],
.ui-result[data-tone="danger"],
.ui-result[data-tone="error"],
.settings-inline-status[data-tone="danger"],
.settings-inline-status[data-tone="error"],
.settings-test-result[data-tone="danger"],
.settings-test-result[data-tone="error"] { color: var(--status-error-fg); }
/* `muted` is a tone the code emits but no rule named. */
.ui-status[data-tone="neutral"],
.ui-status[data-tone="muted"],
.ui-result[data-tone="neutral"],
.ui-result[data-tone="muted"],
.settings-inline-status[data-tone="neutral"],
.settings-inline-status[data-tone="muted"] { color: var(--status-neutral-fg); }
.ui-status[data-tone="info"],
.ui-result[data-tone="info"] { color: var(--status-info-fg); }

/* A status sentence's own dot, one anatomy in every consumer (docs/DESIGN.md "Status marks"). CSS draws it, not
   status_mark.js, because ui.css and ui_primitives.js reach skill authors on their own; it takes the marks' fact
   size and the saturated hues (ok green, warn amber, error red, any other tone the neutral meta ink) while the
   sentence keeps its word tone. */
.ui-status[data-tone]:not(:empty)::before,
.ui-result[data-tone]:not(:empty)::before,
.settings-inline-status[data-tone]:not(:empty)::before,
.wizard-runtime-status[data-tone]:not(:empty)::before {
    content: "";
    display: inline-block;
    flex: 0 0 auto;
    width: var(--mark-fact);
    height: var(--mark-fact);
    margin-right: 7px;
    border-radius: 999px;
    background: var(--text-meta);
    vertical-align: middle;
    position: relative;
    top: -1px;
}
:is(.ui-status, .ui-result, .settings-inline-status, .wizard-runtime-status)[data-tone="ok"]:not(:empty)::before { background: var(--green); }
:is(.ui-status, .ui-result, .settings-inline-status, .wizard-runtime-status)[data-tone="warn"]:not(:empty)::before { background: var(--amber); }
:is(.ui-status, .ui-result, .settings-inline-status, .wizard-runtime-status):is([data-tone="danger"], [data-tone="error"]):not(:empty)::before {
    background: var(--red);
}


/* One field anatomy; existing widget/form classes remain layout hooks only. */
.ui-field {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    min-width: 0;
    color: var(--text-meta);
    font-size: var(--type-meta);
    line-height: var(--line-meta);
}
.ui-field-inline { flex-direction: row; align-items: center; flex-wrap: wrap; }
.ui-field-help { color: var(--text-meta); font-size: var(--type-meta); line-height: var(--line-meta); }
.ui-control {
    box-sizing: border-box;
    width: 100%;
    min-width: 0;
    max-width: 100%;
    padding: var(--space-2) var(--space-3);
    border: 1px solid var(--field-border);
    border-radius: var(--radius-inner);
    background-color: var(--bg-secondary);
    color: var(--text-primary);
    font-family: var(--font-sans);
    font-size: var(--type-body);
    line-height: var(--line-body);
    outline: none;
    transition: border-color 0.2s, box-shadow 0.2s;
}
.ui-control::placeholder { color: var(--text-disabled); }
.ui-control:focus {
    border-color: var(--focus-accent-border);
    box-shadow: 0 0 0 3px var(--focus-accent-ring);
}
.ui-control:disabled { color: var(--text-disabled); cursor: not-allowed; }
/* WebKit (the packaged desktop shell) computes `overflow: visible` on a native
   select, so a long selected option paints past the control and turns the page
   scroller into a horizontal one; the clip keeps it inside the control's box. */
select.ui-control {
    -webkit-appearance: none;
    appearance: none;
    overflow: hidden;
    text-overflow: ellipsis;
    padding-inline-end: calc(var(--space-3) * 3);
    background-image: var(--select-arrow);
    background-repeat: no-repeat;
    background-position: right var(--space-3) center;
}
textarea.ui-control { resize: vertical; }
.ui-checkbox {
    box-sizing: border-box;
    width: 14px;
    height: 14px;
    min-width: 14px;
    margin: 0;
    padding: 0;
    flex: 0 0 auto;
    accent-color: var(--accent);
}
.ui-checkbox:focus-visible { outline: 2px solid var(--focus-accent-border); outline-offset: 2px; }
.ui-status:empty { display: none; }
@media (max-width: 640px), (pointer: coarse) {
    .ui-control { font-size: var(--type-section); }
}
@media (prefers-reduced-motion: reduce) {
    .btn, .ui-control { transition: none; }
}

/* Measured popup placement and its floating material are shared; menu/listbox interaction stays separate. The paint
   belongs to the popup itself, so a menu, a dialog and a listbox are one backed sheet on
   the float shadow: a role below chooses layout, never paint. */
.ui-popup {
    position: fixed;
    margin: 0;
    right: auto;
    bottom: auto;
    top: var(--ui-popup-top);
    left: var(--ui-popup-left);
    box-sizing: border-box;
    max-width: var(--ui-popup-max-width);
    max-height: var(--ui-popup-max-height);
    overflow-y: auto;
    overscroll-behavior: contain;
    z-index: var(--z-popover);
    border: 1px solid transparent;
    border-radius: var(--radius-object);
    background: var(--popup-float);
    color: var(--text-primary);
    box-shadow: var(--elev-float);
    -webkit-backdrop-filter: var(--glass);
    backdrop-filter: var(--glass);
}
.ui-model-chooser { display: block; min-width: 0; }
.ui-model-chooser > .ui-control { width: 100%; }
.ui-model-options {
    width: var(--ui-popup-anchor-width);
    margin: 0;
    padding: var(--space-1);
    list-style: none;
}
.ui-model-option, .ui-model-empty {
    padding: var(--space-2) var(--space-3);
    border-radius: var(--radius-inner);
    font-size: var(--type-body);
    line-height: var(--line-body);
    overflow-wrap: anywhere;
}
.ui-model-option { cursor: pointer; }
.ui-model-option:hover { background: var(--menu-item-hover); }
.ui-model-option[aria-selected="true"] { color: var(--accent-light); background: var(--accent-12); }
.ui-model-empty { color: var(--text-meta); }

/* One menu layout on that sheet; callers retain only domain meaning such as danger. */
.ui-popup[role="menu"] {
    display: flex;
    flex-direction: column;
    width: max-content;
    min-width: min(160px, var(--ui-popup-max-width));
    padding: var(--space-1);
}
.ui-popup [role^="menuitem"] {
    flex: 0 0 auto;
    width: 100%;
    margin: 0;
    padding: var(--space-2) var(--space-3);
    border: none;
    border-radius: var(--radius-inner);
    background: transparent;
    color: var(--text-primary);
    font-family: var(--font-sans);
    font-size: var(--type-body);
    line-height: var(--line-body);
    text-align: start;
    white-space: normal;
    cursor: pointer;
}
.ui-popup [role^="menuitem"]:hover { background: var(--menu-item-hover); }
.ui-popup [role^="menuitem"]:focus-visible { outline: 2px solid var(--focus-accent-border); outline-offset: -2px; }
.ui-popup [role^="menuitem"]:disabled,
.ui-popup [role^="menuitem"][aria-disabled="true"] { color: var(--text-disabled); background: transparent; cursor: default; }
.ui-popup [role^="menuitem"]:is(.danger, .files-context-item-danger) { color: var(--status-error-fg); }

.ui-control[aria-invalid="true"] { border-color: var(--status-error-word, var(--status-error-border)); }
.ui-field-help[data-tone="error"] { color: var(--status-error-fg); }

.ui-control-code {
    font-family: 'SF Mono', 'Fira Code', Consolas, monospace;
    white-space: pre;
    tab-size: 4;
}

.ui-popup[hidden], dialog.ui-popup:not([open]) { display: none; }
/* A popup grows a little from where it was opened as it fades in (--ui-popup-origin, ui_interactions.js measures it),
   and its picture leaves the same way when it closes. Reduced motion: at once. */
.ui-popup:not([hidden]) {
    transform-origin: var(--ui-popup-origin, 50% 0);
    animation: ui-popup-in var(--motion-fade) var(--ease-sheet);
}
@keyframes ui-popup-in { from { opacity: 0; transform: scale(0.96); } }
@media (prefers-reduced-motion: reduce) {
    .ui-popup:not([hidden]) { animation: none; }
}

/* ---- The one hint (DESIGN "Hints", ui_hint.js) ---------------------------------------------------------------------
   What a control says about itself, on the popups' floating material: it stands above its control and over the page,
   out of the flow, so showing it, changing its words or hiding it moves nothing. A line of meaning in the meta ink (the
   value's word primary), a rule line in the secondary ink, and at most one action in the accent ink. */
.ui-hint {
    position: absolute;
    z-index: 1;
    bottom: calc(100% + var(--space-2));
    inset-inline-start: var(--ui-hint-shift, 0px);
    box-sizing: border-box;
    width: max-content;
    max-width: min(28rem, calc(100vw - 4 * var(--space-2)));
    max-width: min(28rem, 100cqi);
    max-height: var(--ui-hint-room, none);
    padding: var(--space-2) var(--space-3);
    border: 1px solid transparent;
    border-radius: var(--radius-object);
    background: var(--popup-float);
    box-shadow: var(--elev-float);
    -webkit-backdrop-filter: var(--glass);
    backdrop-filter: var(--glass);
    color: var(--text-meta);
    font-family: var(--font-sans);
    font-size: var(--type-meta);
    font-weight: 500;
    line-height: var(--line-meta);
    letter-spacing: 0;
    text-align: start;
    white-space: normal;
}
.ui-hint[hidden], .ui-hint [hidden] { display: none; }
.ui-hint[data-side="below"] { bottom: auto; top: calc(100% + var(--space-2)); }
.ui-hint[data-cramped="true"] { overflow-y: auto; overscroll-behavior: contain; }
.ui-hint-line, .ui-hint-rule { margin: 0; }
.ui-hint-word { margin-inline-end: var(--space-2); color: var(--text-primary); font-weight: 600; }
.ui-hint-rule { margin-top: 2px; color: var(--text-secondary); }
.ui-hint-action {
    display: block;
    margin-top: var(--space-1);
    padding: 0;
    border: 0;
    background: none;
    color: var(--accent-light);
    font: inherit;
    font-weight: 600;
    cursor: pointer;
}
.ui-hint-action:focus-visible { outline: 2px solid var(--focus-accent-border); outline-offset: 2px; border-radius: var(--radius-inner); }
.ui-hint:not([hidden]) { animation: ui-hint-in var(--motion-fade) var(--ease-sheet); }
@keyframes ui-hint-in { from { opacity: 0; transform: translateY(3px); } }
@media (prefers-reduced-motion: reduce) {
    .ui-hint:not([hidden]) { animation: none; }
}

/* ---- Floating chrome ----------------------------------------------------------------------------------------------
   Controls that float over scrolling content (Main's header group) share one box: height, baseline and the pill
   corner; words, width, weight and tone stay the control's own (DESIGN §5). The group is invisible at rest and its
   padding is cancelled by an equal negative margin, so grouping moves no control. A room's separate capsules keep
   their own reading backing, and what a bar discloses hangs under its capsules as one work list (room_work.css),
   never as a box glued to one control or one group. */
@media (pointer: coarse) {
    /* A coarse pointer (a phone) raises the shared touch minimum to a finger's 44px, and the whole family with it. */
    :root { --touch-target-min: 44px; --control-h-float: var(--touch-target-min); }
}
/* ---- Touch reach --------------------------------------------------------------------------------------------------
   A small control keeps its look and, under a finger, gains an invisible hit area centred on it and at least the
   touch minimum each way: the pseudo-element reaches out only on the sides that fall short (a 34px-tall capsule
   gains 5px above and below, nothing sideways), so the one-height capsules need no 44px body on a phone. Composition
   keeps at least that overshoot between neighbouring controls, so hit areas never overlap (the composer rows part by
   --space-2). The one recipe, inset by the one --ui-hit-inset: the shared control families carry it by selector, a
   page's own small control adds .ui-hit-area, style.css gives a page's links the same extension, and in the product
   documents every disclosure's summary has it (a summary that draws its own ::after caret keeps it: that rule is as
   specific and later). Where a reach would cover a neighbour or be clipped, the minimum is the control's own height
   instead (DESIGN "Touch and containment"): a row that wraps on a phone and a strip that scrolls set it through the
   control tokens on their scope (--control-h, and --control-h-sm for the small capsules in it; the lock writes the
   box); a disclosure's summary, words without a body, takes it below, and so does a field. */
.ui-hit-area, .btn, .app-tab, .filter-chip, .theme-choice, .ui-segment,
:root:where(.ouro-ui) summary { position: relative; }
@media (pointer: coarse) {
    :is(.ui-hit-area, .btn, .app-tab, .filter-chip, .theme-choice, .ui-segment)::after,
    :root:where(.ouro-ui) summary::after {
        content: "";
        position: absolute;
        inset: var(--ui-hit-inset);
    }
    /* A disclosure stacked under controls or under another one: its line of words is the minimum tall, so its reach
       covers neither (its look stays a line: the summary has no body). A menu's items stack the same way. A field is a
       target too, and a native select cannot carry the ::after. */
    details.ui-disclosure > summary,
    .ui-popup [role^="menuitem"],
    .ui-control { min-height: var(--touch-target-min); }
}

.ui-chrome-group {
    position: relative;
    box-sizing: border-box;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    margin: calc(-1 * var(--chrome-inset));
    padding: var(--space-1);
    border: 1px solid transparent;
    border-radius: var(--radius-object);
}
.ui-chrome-control {
    box-sizing: border-box;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
    height: var(--control-h-float);
    padding: 0 var(--pill-padding-x);
    border-radius: var(--pill-radius);
    line-height: 1;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
}

/* ---- Status marks (docs/DESIGN.md "Status marks") --------------------------------------------------------------
   One small family for every surface, created and painted by web/modules/status_mark.js and drawn only here. The
   shape says what the work does (three moving dots, the same dots still, the pause's two bars, a faint ring, one dot);
   the hue says whether there is something for the owner. Dots are 4px, a single fact 7px; no halos, shadows or glyphs.
   Only the moving dots animate. The ink hue is the surface's own ink (a chosen row's selection colour included);
   --mark-ink is the hook a surface sets to give its marks another ink, and --mark-ok the one for its ok dot (a settled
   question's recorded answer, style.css "Quiz card"). */
:root {
    --mark-dot: 4px;
    --mark-gap: 3px;
    --mark-fact: 7px;
    --mark-wave: status-mark-bounce;  /* a sheet that hides marks rests their wave with none */
    --mark-ink: currentColor;
    --mark-unread: var(--accent-light);
    --mark-owner: var(--amber);
    --mark-ok: var(--green);
}
:root[data-theme="light"] { --mark-unread: var(--accent); }
.status-mark {
    display: inline-flex;
    flex: none;
    align-items: center;
    justify-content: center;
    gap: var(--mark-gap);
    min-height: var(--mark-fact);
    color: var(--mark-ink);
    line-height: 0;
    pointer-events: none;
}
.status-mark[hidden] { display: none; }
.status-mark > i {
    display: block;
    flex: none;
    width: var(--mark-dot);
    height: var(--mark-dot);
    border-radius: 50%;
    background: currentColor;
}
/* The moving dots type: a wave 150 ms apart that lifts each dot 4px and a little larger (owner 20.10 2A). A sheet
   that hides marks rests their wave with `--mark-wave: none` (the closed phone drawer), never by painting them. */
.status-mark[data-shape="moving"] > i { animation: var(--mark-wave) 1.15s cubic-bezier(0.45, 0, 0.55, 1) infinite; }
.status-mark[data-shape="moving"] > i:nth-child(2) { animation-delay: 0.15s; }
.status-mark[data-shape="moving"] > i:nth-child(3) { animation-delay: 0.3s; }
.status-mark[data-shape="still"] { opacity: 0.4; }
.status-mark[data-shape="still"][data-hue="unread"] { opacity: 0.6; }
.status-mark[data-shape="pause"] { opacity: 0.7; }
.status-mark[data-shape="pause"][data-hue="unread"] { opacity: 1; }
.status-mark[data-shape="ring"] { opacity: 0.55; }
.status-mark[data-shape="ring"][data-hue="unread"] { opacity: 0.6; }
.status-mark[data-shape="pause"] > i { width: 2px; height: 8px; border-radius: 1px; }
.status-mark:is([data-shape="pause"], [data-shape="ring"], [data-shape="dot"]) > i:nth-child(3),
.status-mark:is([data-shape="ring"], [data-shape="dot"]) > i:nth-child(2) { display: none; }
.status-mark:is([data-shape="ring"], [data-shape="dot"]) > i { width: var(--mark-fact); height: var(--mark-fact); }
.status-mark[data-shape="ring"] > i { box-sizing: border-box; background: transparent; border: 1px solid currentColor; }
.status-mark[data-hue="unread"] { color: var(--mark-unread); }
.status-mark[data-hue="owner"] { color: var(--mark-owner); }
.status-mark[data-hue="ok"] { color: var(--mark-ok); }
.status-mark[data-hue="failed"] { color: var(--red); }
.status-mark[data-hue="neutral"] { color: var(--text-meta); }
@keyframes status-mark-bounce {
    0%, 52%, 100% { transform: translateY(0) scale(1); opacity: 0.55; }
    26% { transform: translateY(-4px) scale(1.18); opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
    .status-mark[data-shape="moving"] > i { animation: none; }
}
