/* ═══════════════════════════════════════════════════════════════════════════
   NirnAI design system — vanilla port
   Source of truth: nirnai-ui/src/styles/{tokens,base,dark,motion}.css

   Same three layers, same rule about which one components may touch:
     1. primitives  — raw ramps (--n-0 … --n-1000, brand hues). Never used directly.
     2. semantic    — meaning-carrying aliases (--surface, --ink, --accent).
     3. scales      — type, space, radius, elevation, motion, state, density.

   Both themes define the same semantic names, so a component is written once and
   theming is an attribute swap. The brand hues come from the Capillary glyph
   gradients, which is why the accent is a green with a warm-to-cool range rather
   than a single flat hex.

   Every value that carries text was checked against the surface it actually
   lands on: body text clears WCAG AA 4.5:1, borders and status dots clear 3:1.

   ── Deliberate differences from NirnAI ─────────────────────────────────────
   • dark.css is folded in. Upstream splits the dark theme across two files
     (tokens.css declares it, dark.css corrects it) purely so the token file
     stays readable. There is one file here, so the dark block below carries the
     *merged, final* values — dark.css wins wherever the two disagreed.
   • No @import, no @font-face, no font package. See --font below.
   • The layout shell is not ported. See the omissions note at the bottom.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ═══════════════════════════════════════════════════════════════════════════
   THEMING — three triggers, one result

   NirnAI only ever has to satisfy one mechanism, because main.jsx stamps
   data-theme on <html> before first paint. Here we have to satisfy three:

     1. <html data-theme="dark">   — NirnAI's own mechanism, kept so a page can
                                     be moved between the two products verbatim.
     2. <body class="dark-mode">   — what sidebar-shared.js already toggles.
     3. prefers-color-scheme: dark — the "system" case, for pages that never
                                     took an explicit choice.

   Trigger 2 is the one that makes this delicate: it declares the tokens on
   <body>, one level below where the light defaults are declared. Custom
   properties resolve on the element that *declares* them, not on the element
   that uses them, so any semantic token whose value is `var(--another-semantic)`
   would resolve against <html>'s light value and silently stay light. Two
   tokens are in that position — --focus-ring and --selection-ink — and both are
   re-declared in every dark block below rather than left to inherit. Anything
   pointing at a *primitive* is safe, because primitives are theme-independent.

   Explicit light always wins over the OS preference. Say so with either
   <html data-theme="light"> or <body class="light-mode">; the media query below
   excludes both. A page that takes no position gets the OS preference — which
   means a user who picked light in a page that leaves no marker will still be
   flipped. The one-line fix in sidebar-shared.js is to write the choice out:
       document.body.classList.toggle('light-mode', !isDark);
   ═══════════════════════════════════════════════════════════════════════════ */

:root {
  /* ── 1. Primitives ─────────────────────────────────────────────────────── */

  /* Neutrals: very slightly cool, so large surfaces read as "quiet" not "grey".
     The steps are intentionally uneven in a fixed shape: 0…200 stay within ~4 L*
     of one another so stacked surfaces never fight, then 300…800 step ~9–15 L*
     because every stop from there down carries a border or a text tier that has
     to be separable at a glance, and the last stops tighten again as they run
     out of headroom toward black. */
  --n-0: #ffffff;
  --n-25: #fcfcfd;
  --n-50: #f8f9fb;
  --n-100: #f1f3f7;
  --n-150: #e8ebf1;
  --n-200: #dde1e9;
  --n-300: #b0b6c2;
  --n-400: #868e9e;
  --n-500: #697284;
  --n-600: #545d6a;
  --n-700: #414855;
  --n-800: #2b3140;
  --n-850: #212632;
  --n-900: #181c26;
  --n-950: #10131b;
  --n-1000: #0a0c12;

  /* Brand, lifted from the Capillary mark. */
  --brand-green: #2f9e4f;
  --brand-green-deep: #17784a;
  --brand-green-lift: #45b862;
  --brand-blue: #2081c4;
  --brand-yellow: #f5c518;
  --brand-orange: #e25226;
  --brand-magenta: #b3418f;

  /* Status hues come in pairs: 500 is the light-theme text weight (AA on white
     and on its own soft tint), 400 is the dark-theme weight and the dot fill. */
  --red-500: #ce1f46;
  --red-400: #ef5573;
  --amber-500: #945c00;
  --amber-400: #e0a325;
  --green-500: #00793d;
  --green-400: #37b26c;
  --blue-500: #1e6fb8;
  --blue-400: #4d9fdd;

  /* ── 3. Scales (theme-independent) ─────────────────────────────────────── */

  --space-1: 2px;
  --space-2: 4px;
  --space-3: 6px;
  --space-4: 8px;
  --space-5: 12px;
  --space-6: 16px;
  --space-7: 20px;
  --space-8: 24px;
  --space-9: 32px;
  --space-10: 40px;
  --space-11: 56px;
  --space-12: 72px;

  /* Type runs in 1px steps up to --text-lg because this is a dense reading
     surface where a 6% size change is the difference between two ranks; above
     that it switches to ratio steps (~1.16–1.27) where the jumps are display.
     The floor is deliberately 11px and the middle of the scale is opened up:
     the earlier 10.5/11.5/12.5/13.5 run was 8% apart, which reads as one size
     with rounding errors rather than four ranks. */
  --text-2xs: 11px;
  --text-xs: 12px;
  --text-sm: 13px;
  --text-base: 14px;
  --text-md: 15px;
  --text-lg: 16.5px;
  --text-xl: 19px;
  --text-2xl: 22px;
  --text-3xl: 28px;
  --text-4xl: 34px;

  --leading-none: 1;
  --leading-tight: 1.22;
  --leading-snug: 1.36;
  --leading-normal: 1.55;
  --leading-relaxed: 1.68;

  /* Off-integer weights: these are variable-font axis positions, and they only
     land exactly when Inter Variable is present. With a static fallback the
     browser rounds to the nearest available weight, which is why the ranks are
     spaced far enough apart to survive that rounding. */
  --weight-normal: 450;
  --weight-medium: 530;
  --weight-semibold: 620;
  --weight-bold: 700;
  --weight-black: 800;

  --tracking-tight: -0.014em;
  --tracking-snug: -0.006em;
  --tracking-wide: 0.02em;
  --tracking-caps: 0.06em;

  /* Agents read customer IDs, ARNs and log lines all day: cv05/cv08 give Inter's
     tailed l and serifed I so l/1/I stop colliding, and code drops ligatures so
     != and => are read as the characters that are actually in the log. Both are
     no-ops on the system fallback, which is harmless. */
  --font-features-ui: "cv05" 1, "cv08" 1, "calt" 1;
  --font-features-code: "liga" 0, "clig" 0, "calt" 0;

  /* DELIBERATE DIFFERENCE FROM NIRNAI. Upstream self-hosts Inter through
     @fontsource-variable/inter and imports it from main.jsx. There is no
     bundler here and no @font-face, so the stack is the whole mechanism: Inter
     is used when the machine already has it (or when a page chooses to ship its
     own @font-face for it), and the system UI stack is used otherwise. No
     Google Fonts <link> and no @import on purpose — this deploys somewhere
     egress is not guaranteed, and a font that silently fails to load is worse
     than a fallback that was designed for.
     Consequence to know about: the type scale was measured against Inter's
     metrics, so the system stack renders very slightly larger on Windows. */
  --font: "Inter Variable", "Inter var", "Inter", -apple-system, BlinkMacSystemFont,
    "Segoe UI Variable Text", "Segoe UI", "Helvetica Neue", Arial, sans-serif;
  --mono: ui-monospace, "SF Mono", "JetBrains Mono", "Cascadia Code", Menlo, Consolas, monospace;

  --radius-xs: 4px;
  --radius-sm: 6px;
  --radius-md: 9px;
  --radius-lg: 12px;
  --radius-xl: 16px;
  --radius-2xl: 22px;
  --radius-pill: 999px;

  --ease-out: cubic-bezier(0.22, 1, 0.36, 1);
  --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
  --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
  --ease-exit: cubic-bezier(0.4, 0, 1, 1);
  --dur-1: 90ms;
  --dur-2: 140ms;
  --dur-3: 220ms;
  --dur-4: 340ms;
  --dur-5: 520ms;

  /* Intent aliases: reach for these first. Things leaving the screen move faster
     than things arriving, and pointer feedback is faster than either. */
  --dur-hover: var(--dur-1);
  --dur-exit: var(--dur-2);
  --dur-enter: var(--dur-3);

  /* Layout metrics. Kept for name parity with NirnAI — the shell rules that
     consumed them are not ported, so these are inert until something asks. */
  --thread-max: 100%;
  --rail-w: 72px;

  /* ── 2. Semantic — light theme ─────────────────────────────────────────── */
  color-scheme: light;

  --canvas: var(--n-50);
  --surface: var(--n-0);
  --surface-soft: var(--n-100);
  --surface-sunken: var(--n-150);
  --surface-raised: var(--n-0);
  --surface-overlay: rgba(255, 255, 255, 0.82);
  --stripe: var(--n-25);

  /* Four text ranks. --faint is the quietest rank that is still text, so it sits
     at the AA floor rather than below it; it and --muted read apart by size and
     weight as much as by value, because AA leaves them only ~9 L* of room. */
  --ink: #171b24;
  --ink-soft: var(--n-700);
  --muted: var(--n-600);
  --faint: var(--n-500);

  --line: var(--n-200);
  --line-soft: var(--n-150);
  --line-strong: var(--n-400);
  /* NirnAI declares --line-hairline in dark.css only, where it is a light wash
     over a dark chrome. A light-theme value has to exist here or every use of
     it falls back to the initial value and the hairline disappears; this is the
     same idea inverted — a dark wash over a light chrome, same alpha. */
  --line-hairline: rgba(16, 19, 27, 0.07);

  --accent: var(--brand-green-deep);
  --accent-hover: #14653f;
  --accent-ink: #ffffff;
  --accent-soft: rgba(47, 158, 79, 0.1);
  --accent-line: rgba(47, 158, 79, 0.32);
  --accent-glow: rgba(47, 158, 79, 0.22);
  --accent-gradient: linear-gradient(135deg, var(--brand-green-lift), var(--brand-green-deep) 78%);
  /* --accent-gradient runs *up* into the lifted green, whose lightest stop is
     only 2.5:1 against white — correct for a fill that carries no text, wrong
     for the chat banner and the user bubble, which do. This variant runs down
     instead, so white clears 5.5:1 at every stop, and it holds the same value
     in both themes because a banner is chrome (same argument as --sidebar-bg).
     The dark end is mixed from primitives rather than written as a hex so the
     brand hue stays the single source. */
  --accent-gradient-chrome: linear-gradient(135deg,
      color-mix(in srgb, var(--brand-green-deep) 76%, var(--n-1000)) 0%,
      var(--brand-green-deep) 100%);

  --link: var(--blue-500);
  --danger: var(--red-500);
  --danger-soft: rgba(217, 45, 78, 0.09);
  --danger-line: rgba(217, 45, 78, 0.28);
  --warn: var(--amber-500);
  --warn-soft: rgba(224, 163, 37, 0.13);
  --warn-line: rgba(224, 163, 37, 0.34);
  --ok: var(--green-500);
  --ok-soft: rgba(31, 146, 84, 0.11);
  --info-soft: rgba(30, 111, 184, 0.1);

  --ok-dot: #169d59;
  --prod-dot: var(--red-500);

  /* State layers. Overlays are mixed from --ink and --accent so one definition
     inverts correctly when the theme flips. */
  --state-hover: color-mix(in srgb, var(--ink) 6%, transparent);
  --state-active: color-mix(in srgb, var(--ink) 11%, transparent);
  --state-selected: color-mix(in srgb, var(--accent) 12%, transparent);
  --opacity-disabled: 0.45;

  --focus-ring: var(--accent);
  --focus-ring-width: 2px;
  --focus-ring-offset: 2px;
  /* The rail and sidebar are dark under both themes, where the deep-green ring
     nearly disappears — anything drawn on them focuses with the lifted green. */
  --focus-ring-inverse: var(--brand-green-lift);
  /* Same problem, same shape, for destructive text drawn on that dark chrome:
     the light-theme --danger (#ce1f46) is a text weight tuned for white, and it
     only reaches 3.2:1 on --sidebar-bg — under AA for the 11px Logout link that
     is the one place it appears. --red-400 is the dark-chrome weight of the same
     hue and clears 5.0:1 on both sidebar values. Points at a primitive, so it
     is theme-independent and needs no dark re-declaration (see the theming note
     at the top of this file). */
  --danger-inverse: var(--red-400);
  --danger-inverse-soft: rgba(239, 85, 115, 0.16);

  --selection-bg: rgba(47, 158, 79, 0.24);
  --selection-ink: var(--ink);

  /* Rail and sidebar stay dark in both themes: it anchors the app's identity
     and keeps the reading surface visually dominant. */
  --rail-bg: linear-gradient(180deg, #14181f 0%, #0e1116 100%);
  --sidebar-bg: #171b23;
  --sidebar-text: rgba(255, 255, 255, 0.72);
  --sidebar-text-strong: #ffffff;
  --sidebar-text-dim: rgba(255, 255, 255, 0.46);
  --sidebar-hover: rgba(255, 255, 255, 0.07);
  --sidebar-active: rgba(69, 184, 98, 0.16);
  --sidebar-active-line: var(--brand-green-lift);
  --sidebar-line: rgba(255, 255, 255, 0.08);

  /* Resting composer border is a visible hairline that --line-strong can then
     escalate on hover; the card's own elevation is what identifies the control.
     Reused here as the resting border of any focus-glow control. */
  --composer-line: var(--n-300);
  --composer-glow: 0 0 0 4px rgba(47, 158, 79, 0.11);

  /* Elevation. Each level is one direct shadow (tight, offset, tracks the light)
     plus one ambient shadow (roughly 3× the offset and blur, lower alpha). Both
     roughly double per level, so a lifted surface reads as a real height rather
     than a heavier blur. */
  --shadow-1: 0 1px 1px rgba(16, 19, 27, 0.04), 0 1px 3px -1px rgba(16, 19, 27, 0.07);
  --shadow-2: 0 2px 3px -1px rgba(16, 19, 27, 0.05), 0 4px 10px -3px rgba(16, 19, 27, 0.09);
  --shadow-3: 0 4px 6px -2px rgba(16, 19, 27, 0.06), 0 10px 24px -6px rgba(16, 19, 27, 0.11);
  --shadow-4: 0 8px 12px -4px rgba(16, 19, 27, 0.07), 0 22px 48px -12px rgba(16, 19, 27, 0.15);
  --shadow-5: 0 16px 24px -8px rgba(16, 19, 27, 0.08), 0 44px 96px -24px rgba(16, 19, 27, 0.22);

  --scrollbar: rgba(23, 27, 36, 0.3);
  --scrollbar-hover: rgba(23, 27, 36, 0.46);
  --skeleton: linear-gradient(90deg, var(--n-100) 25%, var(--n-150) 45%, var(--n-100) 65%);

  --jump-bg: var(--n-900);
  --jump-fg: #ffffff;
  --kbd-bg: var(--n-100);
  --kbd-line: var(--n-200);

  /* ── Density (see the compact override further down) ───────────────────── */
  --row-pad-y: 10px;
  --thread-gap: 6px;
  --msg-gap: 14px;
  --control-pad-y: 8px;
  --control-pad-x: 14px;
}

/* ═══ Dark theme ═══════════════════════════════════════════════════════════
   tokens.css and dark.css merged; dark.css wins on every token the two both
   declared. The comments explaining each correction are kept, because the
   corrections are the part that is not obvious from the values.

   Both triggers live in one selector list on purpose. Nothing exotic goes in
   this list — an unrecognised selector invalidates the *entire* rule in CSS, so
   :has() is quarantined into its own rule further down where failing to parse
   costs one declaration instead of the whole theme.
   ═══════════════════════════════════════════════════════════════════════════ */

:root[data-theme="dark"],
body.dark-mode {
  color-scheme: dark;

  /* ── Elevation ladder ───────────────────────────────────────────────────
     Contrast ratio is a poor guide near black — every step from #0b to #24
     lands between 1.03 and 1.13 — so the ladder is spaced by CIE L* instead.
     L*: sunken 3.9 · canvas 5.4 · surface 9.6 · stripe 11.6 · soft 14.0 ·
     raised 17.4. Roughly 2–4 L* per step is the smallest move that survives a
     dim room and an uncalibrated panel. */
  --canvas: #0f1218;
  --surface-sunken: #0b0e13;
  --surface: #161b23;
  --stripe: #1a1f28;
  --surface-soft: #1e242e;
  --surface-raised: #242b37;
  --surface-overlay: rgba(22, 27, 35, 0.9);

  /* ── Text ───────────────────────────────────────────────────────────────
     Dark type needs to sit higher than light-theme intuition suggests: the
     same grey that reads as "quiet" on white reads as "broken" on near-black.
     Every step clears 4.5:1 on the lightest surface it can land on
     (--surface-raised), not just on the canvas. */
  --ink: #e9ecf2;
  --ink-soft: #c8cedb;
  --muted: #a0aaba;
  --faint: #8c96a7;

  /* ── Boundaries ─────────────────────────────────────────────────────────
     Hairlines are the first thing a dark theme loses. --line-strong and
     --composer-line carry control identity, so they clear 3:1; --line and
     --line-soft group content rather than identify it and stay quiet on
     purpose — pushing table gridlines to 3:1 turns a dense answer into a
     wireframe. */
  --line-soft: #272e3a;
  --line: #3a4353;
  --line-strong: #6b7689;
  --line-hairline: rgba(255, 255, 255, 0.07);
  --composer-line: #5d687c;

  /* ── Accent, and halation ───────────────────────────────────────────────
     A saturated light green on near-black scatters in the eye and smears the
     edge it is meant to define. Large accent fills lose ~8% saturation and the
     glows drop to a deep green, which tints without blooming. */
  --accent: var(--brand-green-lift);
  --accent-hover: #57cc74;
  --accent-ink: #07130c;
  --accent-soft: rgba(69, 184, 98, 0.15);
  --accent-line: rgba(69, 184, 98, 0.34);
  --accent-glow: rgba(45, 125, 72, 0.4);
  --accent-gradient: linear-gradient(135deg, #4bb46a, #2c8f4b 82%);
  --composer-glow: 0 0 0 3px rgba(69, 184, 98, 0.1);

  --link: var(--blue-400);
  /* #ef5573 sat only 4.55:1 on --danger-soft, under AA for the PROD badge. */
  --danger: #f2637f;
  --danger-soft: rgba(239, 85, 115, 0.13);
  --danger-line: rgba(239, 85, 115, 0.3);
  --warn: var(--amber-400);
  --warn-soft: rgba(224, 163, 37, 0.13);
  --warn-line: rgba(224, 163, 37, 0.3);
  --ok: var(--green-400);
  --ok-soft: rgba(55, 178, 108, 0.14);
  --info-soft: rgba(77, 159, 221, 0.12);

  --ok-dot: var(--green-400);
  /* red-500 is too dark to read as a dot against a dark chrome. */
  --prod-dot: var(--red-400);

  /* Overlays carry a little further in the dark, where a 6% white wash on a
     near-black surface moves fewer perceptual steps than it does on white. */
  --state-hover: color-mix(in srgb, var(--ink) 9%, transparent);
  --state-active: color-mix(in srgb, var(--ink) 15%, transparent);
  --state-selected: color-mix(in srgb, var(--accent) 18%, transparent);

  /* Re-declared, not inherited. Both point at another semantic token, and under
     the body.dark-mode trigger an inherited copy would still be resolving
     against <html>'s light --accent / --ink. See the theming note at the top. */
  --focus-ring: var(--accent);
  --selection-ink: var(--ink);

  /* Selection at 15% is invisible on dark, but a saturated green wash over
     prose glows; more alpha on a desaturated tint is the better trade.
     (NirnAI spends a whole ::selection override on this; the token carries it
     here so there is still exactly one ::selection rule in the file.) */
  --selection-bg: rgba(58, 150, 84, 0.42);

  /* ── Chrome seam ────────────────────────────────────────────────────────
     The rail and sidebar are dark in both themes, so in dark mode they risk
     dissolving into the canvas. They sit clearly *below* it in L* (3.0 / 3.9
     against the canvas 5.4) and the seam itself is lifted. */
  --sidebar-bg: #0a0e14;
  --rail-bg: linear-gradient(180deg, #080b10 0%, #05080c 100%);
  --sidebar-line: rgba(255, 255, 255, 0.1);

  /* Same geometry as light, higher alphas: there is no lighter ground for the
     ambient layer to fall on, so the shadow has to do all of the work. */
  --shadow-1: 0 1px 1px rgba(0, 0, 0, 0.3), 0 1px 3px -1px rgba(0, 0, 0, 0.34);
  --shadow-2: 0 2px 3px -1px rgba(0, 0, 0, 0.34), 0 4px 10px -3px rgba(0, 0, 0, 0.42);
  --shadow-3: 0 4px 6px -2px rgba(0, 0, 0, 0.38), 0 10px 24px -6px rgba(0, 0, 0, 0.5);
  --shadow-4: 0 8px 12px -4px rgba(0, 0, 0, 0.42), 0 22px 48px -12px rgba(0, 0, 0, 0.58);
  --shadow-5: 0 16px 24px -8px rgba(0, 0, 0, 0.46), 0 44px 96px -24px rgba(0, 0, 0, 0.68);

  --scrollbar: rgba(255, 255, 255, 0.18);
  --scrollbar-hover: rgba(255, 255, 255, 0.32);
  --skeleton: linear-gradient(90deg, #1e242e 25%, #2a3240 45%, #1e242e 65%);

  /* A near-white pill blooms; a step down costs nothing at 14:1. */
  --jump-bg: #dfe4ec;
  --jump-fg: #12161d;
  --kbd-bg: #232a35;
  --kbd-line: #3a4353;
}

/* Trigger 3 (the OS preference) is deliberately NOT implemented.

   It was here, and it was the right instinct in isolation — but this theme is
   loaded into an app whose other pages (dashboard, portal, war room, eval) are
   light-only or only partly dark-aware. Honouring prefers-color-scheme made a
   themed page render dark while the page next to it rendered light on the same
   machine, which reads as a broken product rather than a considerate one.

   So dark is an explicit choice only: data-theme="dark" on <html>, or
   body.dark-mode, which is what sidebar-shared.js's Theme toggle sets across
   every page. Restore the media query once the remaining pages are on tokens. */

/* body.dark-mode declares its tokens on <body>, so <html> itself never learns
   the theme — and color-scheme is one of the few things that has to be known at
   the root, because it is what tells the UA to paint the document canvas and
   the window scrollbar dark. Quarantined for the :has() reason above; if it
   does not parse the page is still correctly themed, just with a light-painted
   scrollbar gutter behind it. */
html:has(body.dark-mode) {
  color-scheme: dark;
}
/* There is deliberately no mirror rule for body.light-mode. That class is only
   a guard on the media query above — it never declares tokens of its own,
   because excluding it from the query is enough to leave the :root light
   defaults standing, color-scheme included. Adding a second definition of light
   would just be a second thing to keep in sync. */

/* ═══ Density ══════════════════════════════════════════════════════════════
   Compact trims vertical rhythm only — never type size or line-height, which
   would hurt legibility of dense investigation output. It buys about one extra
   row per screen by taking padding out of rows and controls. Both triggers
   again, for the same reason as the theme.
   ═══════════════════════════════════════════════════════════════════════════ */

:root[data-density="compact"],
body.density-compact {
  --row-pad-y: 5px;
  --thread-gap: 2px;
  --msg-gap: 10px;
  --control-pad-y: 5px;
  --control-pad-x: 12px;
}

/* ═══ Base ═════════════════════════════════════════════════════════════════
   NOT PORTED, on purpose: `html, body, #root { height: 100% }`,
   `body { overflow: hidden }`, `#root { display: flex }`, `.shell`, `.chatcol`.
   Those exist to make NirnAI a fixed-height app frame with its own internal
   scroll containers. Our pages are ordinary scrolling documents and every one
   of those rules would break them — a page inside `overflow: hidden` on <body>
   simply stops scrolling. If a page ever wants the app-frame behaviour it
   should opt in locally rather than inherit it from the theme.
   ═══════════════════════════════════════════════════════════════════════════ */

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

body {
  margin: 0;
  font-family: var(--font);
  font-size: var(--text-md);
  font-weight: var(--weight-normal);
  line-height: var(--leading-normal);
  color: var(--ink);
  background: var(--canvas);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
  font-optical-sizing: auto;
  font-variant-numeric: tabular-nums;
  font-feature-settings: var(--font-features-ui);
}

/* Icons are laid out as boxes, not as letters: no baseline gap, and they never
   get squeezed when a flex row runs out of room. Zero specificity, so any
   component rule wins without a fight. */
:where(svg, img, video) {
  display: block;
  flex: none;
}

button,
input,
textarea,
select {
  font-family: inherit;
  font-size: inherit;
  letter-spacing: inherit;
  color: inherit;
}

button {
  -webkit-tap-highlight-color: transparent;
  user-select: none;
}

/* Firefox dims placeholders by default, which drops them below the contrast
   floor that --faint is chosen to sit on. */
::placeholder {
  color: var(--faint);
  opacity: 1;
}

h1,
h2,
h3,
h4 {
  margin: 0;
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-tight);
  line-height: var(--leading-tight);
  color: var(--ink);
  text-wrap: balance;
}

/* Identifiers, ARNs and log lines: fixed advance, slashed zero, and no
   ligatures so `!=` and `=>` read as the characters that are in the record. */
code,
kbd,
samp,
pre {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums slashed-zero;
  font-feature-settings: var(--font-features-code);
}

a {
  color: var(--link);
  text-decoration-thickness: 1px;
  text-underline-offset: 0.18em;
}

/* ── Focus and selection ──────────────────────────────────────────────────── */

/* One visible focus treatment for the whole app. Keyboard users get a clear
   ring; pointer users never see it. */
:focus {
  outline: none;
}
:focus-visible {
  outline: var(--focus-ring-width) solid var(--focus-ring);
  outline-offset: var(--focus-ring-offset);
}
/* The rail and sidebar stay dark under both themes, where the light theme's
   deep-green ring all but vanishes. */
.rail :focus-visible,
.sidebar :focus-visible,
.sb-nav :focus-visible {
  outline-color: var(--focus-ring-inverse);
}

::selection {
  background: var(--selection-bg);
  color: var(--selection-ink);
}

/* ── Utilities ────────────────────────────────────────────────────────────── */

/* Reachable by a screen reader, absent for everyone else. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  white-space: nowrap;
  border: 0;
  clip-path: inset(50%);
}

/* ═══ Shared bits ══════════════════════════════════════════════════════════ */

.dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  flex: none;
  box-shadow: 0 0 0 3px color-mix(in srgb, currentColor 12%, transparent);
}
.dot.uat {
  background: var(--ok-dot);
  color: var(--ok-dot);
}
.dot.prod {
  background: var(--prod-dot);
  color: var(--prod-dot);
}

/* kbd is the one place --mono is wrong: a key cap is a label, not a record, and
   the UI face keeps the cap narrow enough to sit inside a line of prose. */
kbd {
  font-family: var(--font);
  font-size: var(--text-2xs);
  font-weight: var(--weight-semibold);
  font-feature-settings: var(--font-features-ui);
  line-height: var(--leading-none);
  padding: 3px 5px;
  min-width: 18px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 2px;
  border-radius: var(--radius-xs);
  background: var(--kbd-bg);
  border: 1px solid var(--kbd-line);
  color: var(--muted);
  user-select: none;
}

.env-badge {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 1px 6px;
  border-radius: var(--radius-xs);
  font-size: var(--text-2xs);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  white-space: nowrap;
}
.env-badge.uat {
  background: var(--ok-soft);
  color: var(--ok);
}
.env-badge.prod {
  background: var(--danger-soft);
  color: var(--danger);
}

.btn-primary,
.btn-secondary {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  border-radius: var(--radius-sm);
  padding: var(--control-pad-y) var(--control-pad-x);
  font-size: var(--text-base);
  font-weight: var(--weight-semibold);
  white-space: nowrap;
  cursor: pointer;
  transition: background var(--dur-hover) var(--ease-out),
    border-color var(--dur-hover) var(--ease-out), color var(--dur-hover) var(--ease-out),
    transform var(--dur-hover) var(--ease-out), box-shadow var(--dur-2) var(--ease-out);
}
.btn-primary {
  background: var(--accent);
  color: var(--accent-ink);
  border: 1px solid transparent;
  box-shadow: var(--shadow-1);
}
.btn-primary:hover {
  background: var(--accent-hover);
  box-shadow: var(--shadow-2);
}
.btn-primary:active,
.btn-secondary:active {
  transform: translateY(1px);
}
.btn-secondary {
  background: var(--surface-raised);
  color: var(--ink-soft);
  border: 1px solid var(--line);
}
.btn-secondary:hover {
  background: var(--surface-soft);
  border-color: var(--line-strong);
  color: var(--ink);
}
/* Disabled has to beat every :hover and :active above it, hence the explicit
   restatements rather than one opacity rule. */
.btn-primary:disabled,
.btn-secondary:disabled {
  opacity: var(--opacity-disabled);
  cursor: default;
  box-shadow: none;
}
.btn-primary:disabled:hover {
  background: var(--accent);
  box-shadow: none;
}
.btn-secondary:disabled:hover {
  background: var(--surface-raised);
  border-color: var(--line);
  color: var(--ink-soft);
}
.btn-primary:disabled:active,
.btn-secondary:disabled:active {
  transform: none;
}

/* Segmented control */
.seg {
  display: inline-flex;
  padding: 3px;
  gap: 2px;
  background: var(--surface-sunken);
  border: 1px solid var(--line-soft);
  border-radius: var(--radius-md);
}
.seg-btn {
  border: none;
  background: transparent;
  color: var(--muted);
  cursor: pointer;
  border-radius: var(--radius-sm);
  /* Two px shorter than a standalone button: the track's own 3px padding is
     already carrying that height, and the control has to stay on one row. */
  padding: calc(var(--control-pad-y) - 2px) var(--control-pad-x);
  font-size: var(--text-base);
  font-weight: var(--weight-semibold);
  transition: color var(--dur-hover) var(--ease-out), background var(--dur-hover) var(--ease-out);
}
.seg-btn:hover {
  background: var(--state-hover);
  color: var(--ink);
}
.seg-btn.active {
  background: var(--surface-raised);
  color: var(--ink);
  box-shadow: var(--shadow-1);
}
.seg-btn:active {
  background: var(--state-active);
}

/* ═══ Form controls ════════════════════════════════════════════════════════
   NOT IN NIRNAI. Its only text entry is the composer and one settings modal, so
   there is no general input primitive to port; these are built from the same
   tokens and copied from the settings modal's geometry, so a form here and a
   form there are the same object. The 9px/11px padding is that modal's, kept
   literal rather than switched to --control-pad-*: those are density-linked and
   a text field that changes height under compact mode reflows the whole form.

   Wrapped in :where() so the whole thing has zero specificity. Our pages carry
   inline <style> blocks with their own `input {…}` and `.card {…}` rules; at
   equal specificity the later inline block would win anyway, but at zero the
   theme never has to be fought at all — it is a floor, not a claim.

   Checkbox, radio, range, file and colour inputs are excluded: they are UA
   widgets, and a border plus padding turns them into small grey rectangles.
   accent-color handles them instead.
   ═══════════════════════════════════════════════════════════════════════════ */

:where(
    input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="file"]):not(
        [type="color"]
      ):not([type="submit"]):not([type="button"]):not([type="reset"]),
    select,
    textarea
  ) {
  width: 100%;
  padding: 9px 11px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  font-size: var(--text-base);
  line-height: var(--leading-snug);
  color: var(--ink);
  transition: border-color var(--dur-2) var(--ease-out), box-shadow var(--dur-2) var(--ease-out);
}

/* The glow replaces the outline ring here rather than adding to it: a 2px
   offset outline around a full-width field crowds every neighbour in a stacked
   form, and the accent border plus the 4px wash is already the loudest thing on
   the page at that moment. */
:where(input, select, textarea):focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: var(--composer-glow);
}
:where(input, select, textarea):hover:not(:focus):not(:disabled) {
  border-color: var(--line-strong);
}
:where(input, select, textarea):disabled {
  background: var(--surface-soft);
  color: var(--faint);
  cursor: default;
}
:where(input, select, textarea)[aria-invalid="true"] {
  border-color: var(--danger);
}

:where(textarea) {
  min-height: 84px;
  resize: vertical;
  font-family: inherit;
}

/* UA widgets keep their own shape and just borrow the brand hue. */
:where(input[type="checkbox"], input[type="radio"], input[type="range"], progress) {
  accent-color: var(--accent);
}

/* ── Field ────────────────────────────────────────────────────────────────── */

.field {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}
/* The label is --ink and bold, not a muted caps micro-label: in a long form the
   label is the thing being scanned, and the value below it is what should be
   quieter, not the other way round. */
.field > label,
.field-label {
  font-size: var(--text-base);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-snug);
  color: var(--ink);
}
.field > label .req,
.field-label .req {
  margin-left: 2px;
  color: var(--danger);
}
/* Help text sits *above* the control when it changes how the control should be
   filled in, and below when it only explains the result. Either way it is one
   rank down and snug, so it never competes with the label. */
.field-hint {
  margin: 0;
  font-size: var(--text-sm);
  line-height: var(--leading-snug);
  color: var(--muted);
}
.field-error {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  line-height: var(--leading-snug);
  color: var(--danger);
}

/* ── Card ─────────────────────────────────────────────────────────────────── */

/* --line-soft rather than --line: a card is already separated from the canvas
   by its fill and its shadow, so the border is only there to keep the edge
   crisp on a surface that happens to match. --radius-lg is the container radius
   throughout the system; --radius-sm belongs to controls. */
.card {
  padding: var(--space-6);
  background: var(--surface);
  border: 1px solid var(--line-soft);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-1);
}
.card-title {
  margin: 0 0 var(--space-2);
  font-size: var(--text-md);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-tight);
  line-height: var(--leading-tight);
  color: var(--ink);
}
.card-sub {
  margin: 0 0 var(--space-5);
  font-size: var(--text-sm);
  line-height: var(--leading-snug);
  color: var(--muted);
}
.card.warn {
  border-color: var(--warn-line);
  background: var(--warn-soft);
}
.card.danger {
  border-color: var(--danger-line);
  background: var(--danger-soft);
}
.card.ok {
  border-color: var(--accent-line);
  background: var(--ok-soft);
}

/* ═══ Scrollbars ═══════════════════════════════════════════════════════════
   Thin, and only visible where content actually scrolls. NirnAI names its own
   scroll containers here; we cannot, because every page has different ones — so
   the document scroller gets it plus a .thin-scroll opt-in for anything with
   its own overflow.

   The 3px transparent border with background-clip is what makes the thumb read
   as thin without giving the track a thin hit area: the track stays 10px wide
   and grabbable, only the paint is inset.
   ═══════════════════════════════════════════════════════════════════════════ */

body,
.thin-scroll {
  scrollbar-width: thin;
  scrollbar-color: var(--scrollbar) transparent;
}
body::-webkit-scrollbar,
.thin-scroll::-webkit-scrollbar {
  width: 10px;
  height: 10px;
}
body::-webkit-scrollbar-track,
.thin-scroll::-webkit-scrollbar-track {
  background: transparent;
}
body::-webkit-scrollbar-thumb,
.thin-scroll::-webkit-scrollbar-thumb {
  background: var(--scrollbar);
  border-radius: var(--radius-pill);
  border: 3px solid transparent;
  background-clip: content-box;
  transition: background-color var(--dur-2) var(--ease-out);
}
body::-webkit-scrollbar-thumb:hover,
.thin-scroll::-webkit-scrollbar-thumb:hover {
  background: var(--scrollbar-hover);
  background-clip: content-box;
}
/* Anything drawn on the always-dark chrome needs the inverted thumb, or it is a
   dark smear on a dark panel. */
.side-list,
.sb-nav {
  --scrollbar: rgba(255, 255, 255, 0.16);
  --scrollbar-hover: rgba(255, 255, 255, 0.28);
}

/* ═══ Motion ═══════════════════════════════════════════════════════════════
   Only the OS-preference guard is ported. NirnAI's keyframes all belong to
   components that do not exist here, and shipping thirteen unused @keyframes
   would just be dead weight in a file that every page loads.
   Respect the preference: kill decorative motion, keep state legible.
   ═══════════════════════════════════════════════════════════════════════════ */

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