/* ============================================================
 * Neocoast Design System — Tokens
 * Source: ~/.claude/skills/neocoast-design-system/design.md
 *
 * design.md is the product system (light surfaces, primary-700
 * #1445D8). It is NOT the marketing system in colors_and_type.css
 * (black canvas, #2D4EFF) — do not mix the two. The one place the
 * marketing palette is borrowed from is the dark theme at the bottom
 * of this file, because design.md has no dark ramp at all; its blue
 * (#2D4EFF) is still not used anywhere.
 *
 * Tokens marked [interpolated] are not in design.md. The doc's
 * neutral ramp is Tailwind's gray scale (900=gray-900, 500=gray-500,
 * 200=gray-200, 50=gray-50), so gaps are filled from that same
 * scale rather than invented. design.md's Input spec references
 * neutral-100, which is one of them.
 *
 * The doc's tables are transcribed in full on purpose, so this file
 * is the whole system rather than the slice the app happens to use
 * today. Several tokens (the secondary accents, radius-md and up,
 * motion-slow, some component metrics) therefore have no consumer
 * yet. They are the vocabulary, not dead code - do not prune them
 * just because grep finds no usage.
 * ============================================================ */

/* PP Neue Montreal — licensed Pangram Pangram face, Neocoast license.
 * Converted OTF -> WOFF2. Fallback: Manrope, then system sans. */
@font-face {
  font-family: "PP Neue Montreal";
  src: url("/assets/PPNeueMontreal-Book-7104be81.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "PP Neue Montreal";
  src: url("/assets/PPNeueMontreal-Italic-88ab98b0.woff2") format("woff2");
  font-weight: 400;
  font-style: italic;
  font-display: swap;
}

@font-face {
  font-family: "PP Neue Montreal";
  src: url("/assets/PPNeueMontreal-Medium-ecc30811.woff2") format("woff2");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "PP Neue Montreal";
  src: url("/assets/PPNeueMontreal-Bold-2ee6b42a.woff2") format("woff2");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}

:root {
  color-scheme: light;

  /* ── Primary ─────────────────────────────────────────────── */
  --primary-900: #0f31aa; /* hover over primary-700, max emphasis */
  --primary-700: #1445d8; /* THE primary: buttons, links, main actions */
  --primary-500: #4069ff; /* highlights, active fills, focus rings */
  --primary-200: #a7c9fc; /* icons on primary surfaces, borders */
  --primary-100: #d7eaff; /* info alert fills, badges, highlights */

  /* ── Neutrals ────────────────────────────────────────────── */
  --neutral-900: #111827; /* primary text */
  --neutral-800: #1f2937; /* [interpolated] */
  --neutral-700: #374151; /* [interpolated] */
  --neutral-500: #6b7280; /* secondary text, placeholders */
  --neutral-400: #9ca3af; /* [interpolated] decorative only — 2.4:1, never text */
  --neutral-300: #d1d5db; /* [interpolated] control borders */
  --neutral-200: #e5e7eb; /* borders, dividers */
  --neutral-100: #f3f4f6; /* [interpolated] disabled fills, subtle surfaces */
  --neutral-50: #f9fafb; /* page background, cards */
  --neutral-0: #ffffff; /* [interpolated] raised surface */

  /* ── Semantic ────────────────────────────────────────────── */
  --success-500: #22c55e;
  --warning-500: #f59e0b;
  --error-500: #ef4444;
  --info-500: #3b82f6;

  /* ── Secondary accents ───────────────────────────────────── */
  --secondary-green: #59e0bf;
  --secondary-violet: #9480ff;
  --secondary-yellow: #ffbb5a;
  --secondary-pink: #fd60ce;
  --secondary-orange: #ff8254;

  /* ── Surfaces & text (semantic roles) ────────────────────── */
  /* design.md assigns neutral-50 to both page and cards. Cards are
   * lifted to neutral-0 so they read as distinct surfaces; both stay
   * in-palette and both keep secondary text above 4.5:1. */
  --bg-canvas: var(--neutral-50);
  --bg-surface: var(--neutral-0);
  --bg-subtle: var(--neutral-100);
  --bg-inset: var(--neutral-50);

  /* Only two text colors exist in design.md. Tiering is done with
   * size and weight, not with a third, lower-contrast gray:
   * neutral-400 is 2.4:1 on the canvas and never carries text. */
  --text-primary: var(--neutral-900); /* 17.0:1 on canvas */
  --text-secondary: var(--neutral-500); /* 4.6:1 on canvas, 4.8:1 on surface */
  --text-on-primary: var(--neutral-0); /* 7.3:1 on primary-700 */

  /* The third step of emphasis, for the things that today have
   * nowhere lower to go: the meta line under a headline figure, the
   * caveat beside it.
   *
   * On LIGHT it deliberately equals --text-secondary — 4.63:1 on the
   * canvas, 4.83:1 on the surface, 4.39:1 on --bg-subtle. This is not
   * an oversight: the light ramp has no room left. The step below
   * neutral-500 in design.md's scale is neutral-400 at 2.4:1, and
   * every interpolation between them lands under 4.5:1 too (#6F7681
   * measures 4.38:1 on the canvas). Rather than invent a gray that
   * fails AA for small text, light mode buys its third step with size
   * and weight — --weight-strong above, --text-body-sm below — and
   * spends the colour step only where the ramp affords it. Consumers
   * get one token either way and never branch on the theme.
   *
   * DARK does have the room, and takes it: see the dark blocks. */
  --text-tertiary: var(--text-secondary); /* = secondary on light, by design */

  /* Known gap: no gray in design.md's ramp reaches 3:1 on a light
   * surface below neutral-500, so the resting control border is
   * 1.47:1 and dividers are 1.18:1 - under what WCAG 1.4.11 asks of
   * a component boundary. The doc specifies neutral-200 for borders,
   * so this follows the doc and compensates where it matters: hover
   * goes to border-strong (4.83:1) and focus to the ring (4.48:1),
   * both well over 3:1. Raising the resting border to neutral-500
   * is the fix if strict 1.4.11 is ever required. */
  --border-default: var(--neutral-200); /* dividers, card edges — 1.18:1 */
  --border-control: var(--neutral-300); /* inputs, buttons at rest — 1.47:1 */
  --border-strong: var(--neutral-500); /* hover on controls — 4.83:1 */

  --link-color: var(--primary-700); /* 7.3:1 on surface */
  --focus-ring: var(--primary-500); /* 4.5:1 on surface — clears 3:1 */

  /* The chevron a <select> draws once it stops drawing the platform's own.
   * It is Lucide's chevron-down at stroke 1.5, the same icon and the same
   * weight as every other chevron in the product - the settings disclosure,
   * the sidebar tree, the project node - so the one control that had the
   * operating system's triangle now has the app's.
   *
   * A token and not `currentColor`, because a data URI cannot read a CSS
   * variable: the stroke is baked into the markup and therefore has to be
   * restated per theme, like every other colour in this file. It carries
   * --text-secondary's value in each: the caret annotates the control, it is
   * not its content. Both dark blocks below restate it. */
  --select-caret: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");

  /* Primary button fill states. On light the hover darkens; the dark
   * theme overrides these because on black it has to lighten. */
  --primary-hover: var(--primary-900);
  --primary-edge: var(--primary-700); /* button's own border, checkbox accent */
  --focus-halo: var(--primary-100); /* soft ring behind a focused input */

  /* Semantic 500s all fail 4.5:1 as text on light surfaces
   * (error 3.8, info 3.7, success 2.3, warning 2.2), so status text
   * stays --text-primary and the hue lives in the border, icon and
   * fill. That also satisfies "no color as the only state cue".
   * The 500s are Tailwind's red/amber/green/blue-500, so the tint
   * fills below are the matching -50 steps: same interpolation rule
   * as the neutral ramp, no invented hues. */
  --error-border: var(--error-500);
  --error-bg: #fef2f2;
  --warning-border: var(--warning-500);
  --warning-bg: #fffbeb;
  --success-border: var(--success-500);
  --success-bg: #f0fdf4;
  --info-border: var(--primary-200);
  --info-bg: var(--primary-100);

  /* ── Type ────────────────────────────────────────────────── */
  --font-sans: "PP Neue Montreal", "Manrope", -apple-system,
    BlinkMacSystemFont, "Segoe UI", sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
    monospace;

  /* Book is the default weight for body AND headings. Medium is for
   * eyebrows only. design.md specifies no weight above 500 in the UI;
   * 700 exists for rich-text <strong> inside generated content. */
  --weight-book: 400;
  --weight-medium: 500;
  --weight-bold: 700;

  /* The same 700, under the name the UI is allowed to reach for.
   * --weight-bold stays what it has always been - the rich-text
   * <strong> role - and is not pruned; this is the second role for
   * the same face, and the two are separate tokens so that changing
   * what the product's loudest step looks like never silently
   * restyles generated markdown, or the other way round.
   *
   * Until now the Bold face was downloaded on every page load and
   * applied to exactly nothing: the only 700 in the whole stylesheet
   * directory was the @font-face above. This is the token that spends
   * what was already being paid for. Reserve it for the one figure a
   * surface exists to report - if two things on a screen carry it,
   * neither is the answer. */
  --weight-strong: 700;

  --text-h1: 3rem; /* 48px */
  --text-h2: 1.875rem; /* 30px */
  --text-h3: 1.5rem; /* 24px */
  --text-h4: 1.25rem; /* 20px */
  --text-body: 1rem; /* 16px */
  --text-body-sm: 0.875rem; /* 14px */
  --text-eyebrow: 0.8125rem; /* 13px */
  --text-caption: 0.75rem; /* 12px */

  --tracking-tight: -0.03em; /* large headings */
  --tracking-normal: 0;
  --tracking-caption: 0.04em;
  --tracking-eyebrow: 0.08em;

  --leading-heading: 1.15;
  --leading-snug: 1.3;
  --leading-body: 1.5;

  /* Reading measure. The estimate surfaces were running 100 to 178
   * characters a line, against the 45-75 that typographic practice
   * asks for, so a paragraph of model output was a paragraph nobody
   * finished. 65ch is the middle of that band in the body size.
   *
   * It belongs on PROSE only. Do not put it on a table, on the
   * roadmap, or on a block of code: those are laid out to their
   * content and a max-width there either clips them or starts a
   * horizontal scroll. The element rules in 02_base.css apply it to
   * the prose utilities by name for exactly that reason. */
  --measure: 65ch;

  /* ── Spacing (4px base) ──────────────────────────────────── */
  --space-1: 0.25rem; /* 4px */
  --space-2: 0.5rem; /* 8px */
  --space-3: 0.75rem; /* 12px */
  --space-4: 1rem; /* 16px */
  --space-5: 1.25rem; /* 20px [interpolated] */
  --space-6: 1.5rem; /* 24px */
  --space-8: 2rem; /* 32px */
  --space-10: 2.5rem; /* 40px [interpolated] */
  --space-12: 3rem; /* 48px */

  /* ── Radii ───────────────────────────────────────────────── */
  --radius-xs: 6px; /* buttons, badges, toasts, sidebar items, inputs */
  --radius-sm: 8px; /* cards, alerts, dialogs, modals */
  --radius-md: 12px; /* highlighted containers */
  --radius-lg: 16px; /* wide surfaces */
  --radius-xl: 24px; /* hero frames and cards */
  --radius-2xl: 32px; /* maximum-emphasis surfaces */
  --radius-full: 999px; /* pill badges, avatars */

  /* ── Elevation ───────────────────────────────────────────── */
  /* The brand reads flat. Shadows exist only to detach an overlay
   * from the page it floats above. */
  --scrim: rgba(17, 24, 39, 0.5); /* modal dimming — flat, no decorative blur */

  /* The surface and the edge an overlay gets, as opposed to a card sitting in
   * the page. Same shape as --text-tertiary above: on LIGHT both deliberately
   * resolve to what a card already uses, because the page is off-white and the
   * card is white - there is no rung above it to climb to, and tinting a modal
   * grey to prove it floats would make it read as disabled. DARK has the room
   * and takes it: #0D0D0D over a #0A0A0A canvas is barely a step, so the modal
   * goes to the raised surface and its stroke to the brand's second grey.
   * Consumers get one token either way and never branch on the theme. */
  --bg-overlay: var(--bg-surface);
  --border-overlay: var(--border-default);

  --shadow-popover: 0 4px 12px rgba(17, 24, 39, 0.08),
    0 1px 2px rgba(17, 24, 39, 0.06);

  /* Elevation, not a halo. This was `0 24px 60px` at 70% on dark and
   * `0 16px 48px` on light: a 1px border wrapped in a blur wider than the
   * dialog's own padding, which is the shape a detector reads as generated
   * rather than designed, and which the brand note above ("the brand reads
   * flat") already argued against.
   *
   * What separates the dialog from the page now is the near layer - a tight,
   * higher-opacity contact shadow that draws an actual edge - plus the raised
   * surface and stroke of --bg-overlay / --border-overlay. The far layer stays
   * only to keep the corners from sitting flat on the scrim, at a third of the
   * blur it used to have. */
  --shadow-dialog: 0 1px 2px rgba(17, 24, 39, 0.2),
    0 8px 20px rgba(17, 24, 39, 0.14);

  /* ── Motion ──────────────────────────────────────────────── */
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  /* Not Material's (0.4, 0, 0.2, 1): that curve eases in before it moves,
   * which delays the frame the eye is on. This one leaves immediately
   * and settles long. */
  --ease-standard: cubic-bezier(0.2, 0, 0, 1);
  --motion-fast: 100ms; /* tooltips, popovers */
  --motion-base: 200ms; /* modals, hovers */
  --motion-slow: 300ms; /* page transitions */

  /* ── Component metrics ───────────────────────────────────── */
  --control-h-sm: 36px;
  --control-h-md: 40px;
  --sidebar-w: 240px;
  --nav-item-h: 40px;
  --sidebar-item-h: 32px;
  --sidebar-item-h-2line: 48px;
  --table-header-h: 48px;
  --table-row-h: 56px;
  --badge-h: 20px;
  /* [interpolated] Not a design.md metric: it is WCAG 2.5.8's AA
   * minimum. design.md's smallest control is 36px, so anything the doc
   * specifies clears this on its own - the token exists for the things
   * it does NOT specify, the text links and inline controls that are
   * still targets. --badge-h is 20px and is not a substitute: a badge
   * is not something you point at. */
  --target-min: 24px;
  /* [interpolated] WCAG 2.5.5's enhanced target, and the size both platform
   * guidelines ask for a finger. --target-min above is the AA floor, which is
   * what an inline control in a line of text has to clear; this is what a
   * control gets when it is the only thing on screen and the pointer is a
   * thumb - the mobile sidebar's two disclosures, and the reveal button. */
  --target-touch: 44px;
  --toast-w: 388px;
  --dialog-w: 423px;
  --alert-dialog-w: 512px;
  --alert-w: 388px;

  /* ── Layout ──────────────────────────────────────────────── */
  --page-width: 1280px;
  --gutter: var(--space-8); /* 32px desktop */
  --gutter-mobile: var(--space-4); /* 16px mobile */

  /* ── Epic colour ramp ─────────────────────────────────────
   * Not from design.md - a product ramp, derived from design.md's blue.
   * It lives here rather than beside its one consumer in 05_features.css
   * because these were the last colour literals left outside this file,
   * and a set of twelve is exactly the kind of thing that gets edited one
   * value at a time until it stops being a set.
   *
   * Thirty bars in one blue, on a chart whose rows carry an epic, a risk,
   * a seniority and a dependency verdict. Epic is the one worth the hue:
   * it is what the table above is grouped by, so a colour recognised in
   * the roadmap leads back to rows.
   *
   * Twelve steps of OKLCH at a FIXED lightness (L=0.43) and a fixed chroma
   * (C=0.10), 30 degrees apart, starting at the product blue's own hue.
   * Even lightness is what makes it categorical rather than a ramp: no
   * step reads as "more" than another, which is the whole failure mode of
   * a rainbow or a gradient used for categories.
   *
   * ONE PALETTE FOR BOTH THEMES, deliberately - which is why there is no
   * copy of this block in the dark sections below. The label on a bar is
   * --epic-label in either theme, so a palette that changed with the theme
   * would have to be re-verified twice; holding it fixed makes the text
   * contrast a property of the colour rather than of the background behind
   * the chart.
   *
   * CONTRAST, measured over all twelve fills against --epic-label:
   * minimum 7.46:1 (epic 9, the teal), maximum 8.57:1 (epic 3). All twelve
   * clear WCAG AAA for body text, and the single blue bar this replaced
   * measured 7.31:1 - so every step of the ramp is at least as legible as
   * what was there.
   *
   * The edge is the same hue at L=0.66. It is what separates a dark bar
   * from the dark canvas in the dark theme, where fill alone would not -
   * and in the light theme it reads as the bar's own outline rather than
   * as a second colour. Measured against the dark canvas it runs
   * 6.03:1-6.72:1; against the light canvas 2.82:1-3.14:1, which is the
   * cost of holding one palette across both themes and is acceptable
   * because the edge is decoration around a fill that already carries the
   * category, never the only cue. */
  --epic-0-fill: #334d86;
  --epic-0-edge: #7091d6;
  --epic-1-fill: #534381;
  --epic-1-edge: #9684cf;
  --epic-2-fill: #6a3b6e;
  --epic-2-edge: #b37ab8;
  --epic-3-fill: #783653;
  --epic-3-edge: #c67597;
  --epic-4-fill: #7e3634;
  --epic-4-edge: #cd7671;
  --epic-5-fill: #793e0d;
  --epic-5-edge: #c67f4e;
  --epic-6-fill: #694a00;
  --epic-6-edge: #b28c39;
  --epic-7-fill: #505500;
  --epic-7-edge: #919a44;
  --epic-8-fill: #285d29;
  --epic-8-edge: #67a466;
  --epic-9-fill: #00614c;
  --epic-9-edge: #34a88d;
  --epic-10-fill: #005e69;
  --epic-10-edge: #12a5b1;
  --epic-11-fill: #00577e;
  --epic-11-edge: #419dcb;

  /* Always white, whatever the theme decides --text-on-primary is: the
   * twelve fills above are fixed, so the only text colour they have been
   * measured against is the one they were measured with. */
  --epic-label: #ffffff;
}

/* ============================================================
 * Dark theme
 *
 * design.md has no dark ramp - it defines four light neutrals and
 * nothing else. Rather than invent one, the surfaces and text below
 * come from Neocoast's OWN dark palette, the marketing system in the
 * design system's colors_and_type.css: canvas #0A0A0A, card #0D0D0D,
 * raised #141414, strokes #1F1F1F/#262626, text #E5E5E5/#B3B3B3.
 * That file even applies #E5E5E5 to its own h1-h4, so headings and
 * body sharing it here is the brand's own choice, not a shortcut.
 *
 * The blue stays design.md's. Only its ROLES move, because #1445D8
 * is 2.7:1 on black and cannot carry text there:
 *   - link / accent text -> primary-200 #A7C9FC (11.7:1)
 *   - focus ring         -> primary-500 #4069FF (4.4:1, over 3:1)
 *   - primary button     -> primary-700 fill, white text (7.3:1),
 *                           primary-500 border so the button's own
 *                           edge clears 3:1 against the canvas
 *
 * Dark is the easier theme for contrast: body text lands at 15.7:1
 * (light mode is 17.0:1) and secondary at 9.4:1 (light: 4.6:1).
 * It also closes light mode's one WCAG 1.4.11 gap - control borders
 * reach 3.45:1 here, where no gray in the light ramp could.
 *
 * TWO BLOCKS, KEPT IDENTICAL ON PURPOSE. The media query serves
 * everyone whose OS asks for dark, with no JavaScript at all. The
 * attribute block lets the theme toggle override that in either
 * direction. Edit one, edit the other.
 * ============================================================ */

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

    --bg-canvas: #0a0a0a; /* Neocoast black */
    --bg-surface: #0d0d0d; /* card on black */
    --bg-subtle: #141414; /* raised surface */
    --bg-inset: #000000;

    --text-primary: #e5e5e5; /* 15.7:1 on canvas */
    --text-secondary: #b3b3b3; /* 9.4:1 on canvas, 9.3:1 on surface */
    /* The third step the light ramp could not afford. Dark has the
     * room: 15.7 / 9.4 / 5.7 is a ramp with three legible rungs,
     * where light has 17.0 / 4.6 and a floor. Measured 5.7:1 on the
     * canvas, 5.6:1 on the surface and 5.3:1 on --bg-subtle, so it
     * clears 4.5:1 as small text on every ground in this theme. It
     * also sits above --border-control (#666, 3.45:1), which keeps
     * the text rung above the boundary rung where it belongs. */
    --text-tertiary: #8a8a8a; /* 5.7:1 on canvas, 5.6:1 on surface */
    --text-on-primary: #ffffff; /* 7.3:1 on primary-700 */

    --border-default: #1f1f1f; /* dividers — decorative, 1.2:1 */
    --border-control: #666666; /* controls at rest — 3.45:1 */
    --border-strong: #868689; /* hover — 5.45:1 */

    --link-color: var(--primary-200); /* 11.7:1 */
    --focus-ring: var(--primary-500); /* 4.4:1 */

    /* Same Lucide chevron-down, restated with dark's --text-secondary
     * (#b3b3b3) baked into the stroke. See the light block for why the value
     * cannot be a variable. */
    --select-caret: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23b3b3b3' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");

    /* primary-700 lightened 12% toward white: 5.7:1 for white text
     * and 3.5:1 against the canvas, so the hover state gains contrast
     * instead of losing it. Light mode darkens to primary-900; on
     * black the only sane direction is up. */
    --primary-hover: #305bdd;
    --primary-edge: var(--primary-500); /* 4.4:1 on canvas — clears 3:1 */
    --focus-halo: rgba(64, 105, 255, 0.35);

    /* Semantic 500s at 14% over the card surface — the same rule as
     * light mode's tints, applied over the dark ground. Body text on
     * each of them measures 12.5:1 or better. */
    --error-bg: #2d1515;
    --warning-bg: #2d210d;
    --success-bg: #102718;
    --info-bg: #0e1529;
    --info-border: var(--primary-500);

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

    /* The rung the light ramp had nowhere to climb to. #141414 over the
     * #0A0A0A canvas is the lift the shadow was being asked to fake, and
     * #262626 is the marketing palette's second stroke - both already in the
     * list at the top of this block. */
    --bg-overlay: var(--bg-subtle); /* #141414 */
    --border-overlay: #262626;

    --shadow-popover: 0 4px 12px rgba(0, 0, 0, 0.6),
      0 1px 2px rgba(0, 0, 0, 0.5);
    /* See the light block: elevation from the near layer and the raised
     * surface, not from a 60px halo around a 1px border. */
    --shadow-dialog: 0 1px 2px rgba(0, 0, 0, 0.85),
      0 8px 20px rgba(0, 0, 0, 0.55);
  }
}

:root[data-theme="dark"] {
  color-scheme: dark;

  --bg-canvas: #0a0a0a; /* Neocoast black */
  --bg-surface: #0d0d0d; /* card on black */
  --bg-subtle: #141414; /* raised surface */
  --bg-inset: #000000;

  --text-primary: #e5e5e5; /* 15.7:1 on canvas */
  --text-secondary: #b3b3b3; /* 9.4:1 on canvas, 9.3:1 on surface */
  /* See the media-query block above for the measurements and the
   * reason light cannot match this rung. */
  --text-tertiary: #8a8a8a; /* 5.7:1 on canvas, 5.6:1 on surface */
  --text-on-primary: #ffffff; /* 7.3:1 on primary-700 */

  --border-default: #1f1f1f; /* dividers — decorative, 1.2:1 */
  --border-control: #666666; /* controls at rest — 3.45:1 */
  --border-strong: #868689; /* hover — 5.45:1 */

  --link-color: var(--primary-200); /* 11.7:1 */
  --focus-ring: var(--primary-500); /* 4.4:1 */

  /* Same Lucide chevron-down with dark's --text-secondary in the stroke; see
   * the light block for why it cannot be a variable. */
  --select-caret: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23b3b3b3' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");

  --primary-hover: #305bdd;
  --primary-edge: var(--primary-500); /* 4.4:1 on canvas — clears 3:1 */
  --focus-halo: rgba(64, 105, 255, 0.35);

  --error-bg: #2d1515;
  --warning-bg: #2d210d;
  --success-bg: #102718;
  --info-bg: #0e1529;
  --info-border: var(--primary-500);

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

  /* See the media-query block above for why dark can afford a raised overlay
   * surface where light cannot. */
  --bg-overlay: var(--bg-subtle); /* #141414 */
  --border-overlay: #262626;

  --shadow-popover: 0 4px 12px rgba(0, 0, 0, 0.6), 0 1px 2px rgba(0, 0, 0, 0.5);
  --shadow-dialog: 0 1px 2px rgba(0, 0, 0, 0.85), 0 8px 20px rgba(0, 0, 0, 0.55);
}

/* Breakpoints (design.md): mobile < 640px · tablet 640-1024px · desktop > 1024px */

@media (prefers-reduced-motion: reduce) {
  :root {
    --motion-fast: 0ms;
    --motion-base: 0ms;
    --motion-slow: 0ms;
  }
}
