/**
 * THE TOKEN LAYER — DESIGN.md §4. One source of truth, consumed by the app and by tilas.xyz.
 *
 * Two spellings of every colour, deliberately:
 *
 *   --color-surface-1   Tailwind's namespace. Generates the utilities (bg-surface-1, text-muted…)
 *                       and is the variable a utility resolves to, so overriding it under
 *                       [data-theme] is what makes theming work at all.
 *   --surface-1         The name DESIGN.md §4.1 specifies, aliased below. Hand-written CSS and
 *                       the marketing site use these, and a reviewer citing §4.1 finds the name
 *                       the document actually names.
 *
 * Both resolve to the same value; neither can drift from the other because one is defined in
 * terms of the other. Values are declared exactly once, in @theme.
 *
 * Contrast is verified by tokens.test.ts rather than by eye — every text/background pair here
 * meets WCAG AA (4.5:1) in every theme, and the test fails the build if a value is edited to
 * something that does not.
 *
 * --- the drafting turn (2026-08-28) ----------------------------------------------------------
 *
 * The register is a **drafting table**: cool ground, hairline rules, pencil rather than phosphor.
 * It replaces the instrument retune's workshop lamp, which had replaced a generic dark theme —
 * and the machinery that made both of those safe is unchanged, which is the reason a change this
 * total is a day's work: tokens.test.ts discovers `[data-theme]` blocks rather than listing them,
 * so all seven palettes were contrast-checked the moment the values landed.
 *
 * **The neutrals went cool**, reversing the warm turn exactly: B−R of 3/6/8/10 where the warm
 * ramp ran R−B of 3/6/8/10, at luminance steps of 3.6/5.5/9.1 against its 3.5/5.6/9.1. The warm
 * argument was never wrong, and it is worth stating what was traded rather than pretending it was
 * a mistake: --surface-0 *is* the viewport background, so the theme is the grey card a filament is
 * judged against, and a cool ground pushes a warm filament green exactly as a warm ground pushes
 * a cool one. What makes the trade payable is that both sides are still on the menu — `graphite`
 * is the absence of any cast, and `paper` is now the warm ramp, carrying alone the job the light
 * theme used to share with it.
 *
 * **The accent went blue, and stayed pale.** The chroma argument from M4c is unchanged and is
 * still what makes an accent legal at all: sixteen filament slots cover the wheel, so the accent
 * cannot be told from the user's data by hue — only by chroma. The retune rejected blue for
 * "borrowing the same corner of the wheel as filament-07 and filament-08", and that objection is
 * answered here by measuring instead of eyeballing. Chroma as max−min channel: the accent is 35,
 * filament-07 is 173, filament-08 is 190. Four to five times less saturated is what "the one
 * thing no saturated filament reads as" means, and hue proximity was never the test — if it were,
 * no accent could exist, because sixteen slots leave no empty corner to move to.
 *
 * On the *light* ramps the accent is dark rather than pale (paling it drops below 4.5:1 on white
 * — the constraint that has always made light's accent dark). It is a steel blue, and one small
 * knot comes undone with it: the old light accent had to lean teal to avoid reading as a second
 * green beside --ok, and a blue has no such collision to dodge.
 */



/**
 * --- §4.2 the typeface, shipped rather than borrowed ------------------------------------
 *
 * Four `woff2` files in `./fonts`, 69 KB together — a third smaller than the pair they replace.
 * The product asked for `system-ui` until M4 and got San Francisco, Segoe and whatever Linux had
 * configured, which is three different products depending on where it was opened. A tool whose
 * interface is 11px labels and tabular millimetres cannot leave its metrics to the host.
 *
 * **Space Grotesk and JetBrains Mono**, replacing IBM Plex. Plex was chosen for the
 * instrumentation register and is good at it; what it is not is *particular*. It is the corporate
 * humanist sans a great many technical products already use, so it made the product look
 * competent and anonymous at the same time. Space Grotesk is drawn from a monospace skeleton —
 * squared bowls, flat terminals, a narrow set — so its proportions already agree with a readout,
 * and it has enough character at 20px to carry a wordmark without one being drawn. JetBrains Mono
 * pairs with it for the same reason Plex Mono paired with Plex Sans: shared vertical proportions,
 * so a label and the number beside it sit on one baseline without anybody nudging them. Its zero
 * is dotted and its digits are unmistakable at 11px, which is the size most of them are set at.
 *
 * **Local `url()`, never a CDN.** The desktop build has no network by design and the web build
 * claims to work offline; a font that only arrives online would break both quietly, in the one way
 * a screenshot cannot show. `tokens.test.ts` asserts every `src` here is a relative path.
 *
 * **latin *and* latin-ext, and latin-ext is headroom.** MARKETING.md §3.3 ships EN, FR and ES,
 * and all three sit inside latin as Fontsource cuts it (`é à ç ù œ`, `ñ á í ¿ ¡`). latin-ext ships
 * anyway — 2 of 112 KB, already built, and the next language added is likely to need it
 * (DESIGN.md §4.2). What the subsets guard against is unchanged: a missing one renders a language
 * in a fallback face, which appears only after the launch it was meant for and only for the users
 * it was meant for.
 *
 * Sans is variable (`font-weight: 300 700` from one file) so the three sanctioned weights below
 * cost nothing extra. Mono is a static 400: no readout in the product is bold, and a second mono
 * weight would be bytes shipped for a style that has no call site.
 */

@font-face {
  font-family: "Space Grotesk Variable";
  font-style: normal;
  font-weight: 300 700;
  /* `swap`, not `block`: the files are local, so the swap period is theoretical — but if the
     bundle is ever served over a slow link, text a user can read beats a blank panel. */
  font-display: swap;
  src: url("./fonts/space-grotesk-latin-wght-normal.woff2") format("woff2-variations");
  unicode-range:
    U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304,
    U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "Space Grotesk Variable";
  font-style: normal;
  font-weight: 300 700;
  font-display: swap;
  src: url("./fonts/space-grotesk-latin-ext-wght-normal.woff2") format("woff2-variations");
  unicode-range:
    U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308,
    U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113,
    U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: "JetBrains Mono";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("./fonts/jetbrains-mono-latin-400-normal.woff2") format("woff2");
  unicode-range:
    U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304,
    U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "JetBrains Mono";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("./fonts/jetbrains-mono-latin-ext-400-normal.woff2") format("woff2");
  unicode-range:
    U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308,
    U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113,
    U+2C60-2C7F, U+A720-A7FF;
}

:root {
  /* Rewritten from @theme by apps/site/src/build.tsx. */
  /* --- §4.1 colour: dark is the default theme -------------------------------------- */

  /*
   * Viewport background → chrome → panel → raised.
   *
   * Cool carbon. The cast runs B−R of 3/6/8/10 across the ramp — the same magnitude the warm
   * version ran in the other direction, and reversed on purpose: the register is a drafting
   * table rather than a workshop lamp. Enough that the ground is not dead beside a lit 3D scene,
   * little enough that it is still a neutral and still a grey card.
   *
   * The warm ramp's argument is not wrong, it is a trade: a cool ground pushes a warm filament
   * green, and a warm one pushes a cool filament. `graphite` is what exists for a user who wants
   * neither, and `paper` is now the warm ramp for a user who wants the other side of this trade.
   *
   * Relative luminance between adjacent surfaces is 3.6/5.5/9.1 (×1000), holding the property the
   * previous retune bought: at the 3.9 this ramp used to run, the panel-against-chrome edge was
   * carried entirely by --border-subtle, and removing the border made the two surfaces one
   * colour. A surface should be legible as a surface.
   */
  --color-surface-0: #0d0d10;
  --color-surface-1: #121618;
  --color-surface-2: #1a1f22;
  --color-surface-3: #242a2e;

  /* Two text weights. There is no third. */
  --color-text-primary: #dfe6ec;
  --color-text-muted: #8e9ba4;

  /* Separation, and focus rings. */
  --color-border-subtle: #252d31;
  /* Lifted until it clears 3:1 against surface-3, the lightest thing it is ever drawn on. */
  --color-border-strong: #6a767c;

  /*
   * Exactly one accent: current selection and the primary action.
   *
   * Pale, low-chroma ice. The rule it obeys is M4c's and is unchanged: the sixteen filament slots
   * cover the whole wheel, so the accent cannot be distinguished from the user's data by *hue* —
   * there is no hue left to claim — and can only be distinguished by *chroma*.
   *
   * An earlier pass rejected a blue accent for sitting "in the same neighbourhood as filament-07
   * and filament-08", and that objection is answered by measurement rather than waved away.
   * Chroma, as max−min channel: this accent is **35**, filament-07 is 173, filament-08 is 190,
   * filament-09 is 155. It is four to five times less saturated than the nearest thing a user
   * paints with, which is the whole of what the rule asks. Hue proximity was never the test — if
   * it were, no accent could exist at all, because sixteen slots leave no empty corner.
   *
   * It reads as pencil on drafting paper, which is the register this identity is for.
   */
  --color-accent: #b6ccd9;

  /*
   * Text and glyphs drawn *on* an accent fill.
   *
   * Its absence is why the product had no filled primary button: `controls.tsx` recorded that it
   * "has no on-accent text colour, and inventing one here is the raw-hex tokenSources.test.ts
   * exists to catch", and settled for an accent border with accent text. That was the right call
   * against a missing token and the wrong shape for a primary action — a Cut button that looks
   * like every other button is not a primary action. So the token exists now, once, per theme,
   * and tokens.test.ts checks it clears 4.5:1 against the accent it sits on.
   */
  --color-accent-contrast: #0b1116;

  /* Destructive, at-risk, verified. */
  --color-danger: #ff9384;
  --color-warning: #e2b25c;
  --color-ok: #5cd98e;

  /* --- §4.1 filaments: the user's data, never UI meaning ---------------------------- */

  /*
   * Sixteen AMS slots. These are deliberately outside the theme system: a user's slot 3 red is
   * the same red in light mode, dark mode and in the exported 3MF, because it describes physical
   * plastic rather than our interface. No [data-theme] block below touches them, and no
   * interface element may borrow one.
   *
   * Untouched by the retune, for exactly that reason. A redesign may repaint every surface in
   * the product; it may not repaint the user's spools.
   */
  --color-filament-01: #e8402d;
  --color-filament-02: #f57c1f;
  --color-filament-03: #f5c518;
  --color-filament-04: #8cc973;
  --color-filament-05: #4caf50;
  --color-filament-06: #1fa87c;
  --color-filament-07: #22b8cf;
  --color-filament-08: #2f80ed;
  --color-filament-09: #3d4fd8;
  --color-filament-10: #7b4fd8;
  --color-filament-11: #b84fd8;
  --color-filament-12: #e8409b;
  --color-filament-13: #8a5a3c;
  --color-filament-14: #f2f2f2;
  --color-filament-15: #9aa0a6;
  --color-filament-16: #1a1a1a;

  /* --- §4.2 type -------------------------------------------------------------------- */

  /*
   * The shipped face first, the platform stack behind it. The tail is not decoration: it is what
   * renders during the swap period, and what renders at all if a `woff2` ever fails to decode.
   */
  --font-sans: "Space Grotesk Variable", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --font-mono: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, monospace;

  /*
   * Three weights. The same argument as the four type sizes: a variable font hands you 300–700
   * continuously, which is not a range but an invitation to pick a different one per component,
   * and `tokenSources.test.ts` exists because that is exactly what happened to colour.
   *
   * `semibold` is the wordmark and a panel heading, `medium` is a group label, `normal` is
   * everything a user reads. Mono ships only 400, so a bold number would be synthesised by the
   * browser; there is no such number today.
   */
  --font-weight-normal: 400;
  --font-weight-medium: 500;
  --font-weight-semibold: 600;

  /*
   * Four sizes. Adding a fifth requires deleting one.
   *
   * Expressed in rem against the platform root size and never in px: the incumbent shipped a
   * beta with broken 4K font scaling, and a pixel font size anywhere is how that happens.
   *
   * Only `title` moved in the retune, 20 → 22. It is the one size used at a moment when the user
   * is being addressed rather than working — the canvas empty state, a renderer failure — and at
   * 20px it sat too close to `panel` to read as a change of voice.
   */
  --text-status: 0.6875rem; /* 11px at a 16px root */
  --text-ui: 0.8125rem; /* 13 — the default */
  --text-panel: 0.9375rem; /* 15 — panel headings */
  --text-title: 1.375rem; /* 22 — the empty state, an error card */

  /*
   * Two letterspacings, each named for the one thing it is for. There is no scale here on
   * purpose: a tracking *scale* is an invitation to pick a value per component, which is how the
   * wordmark ends up 0.14em in one place and 0.12em in another.
   *
   * `label` is the uppercased 11px group heading. At that size the default tracking closes the
   * letters into a block you read as a texture rather than as a word, which is why every
   * instrument-register interface — Fusion, Ableton, a scope's menu — opens its small caps up.
   * Opened 0.06 → 0.08 with the retune: Space Grotesk sets tighter than Plex at the same size.
   *
   * `wordmark` is TILAS, and it is wider because it is a mark rather than a heading. It lives
   * here rather than at the call site because tilas.xyz sets the same word from the same file.
   */
  --tracking-label: 0.08em;
  --tracking-wordmark: 0.18em;

  /* --- §4.3 space, radius, motion ---------------------------------------------------- */

  /* The 4px scale: 4 8 12 16 24 32 come out as spacing-1 … spacing-8. Nothing off-scale. */
  --spacing: 0.25rem;

  /*
   * Machined, not softened. The viewport has no radius at all.
   *
   * 4/8 → 3/4 with the control layer, and 3/4 → 1/2 with the retune. The earlier note argued that
   * eight pixels "reads as another product's chrome beside a build plate"; the same sentence
   * applies again one step down. At 1px a control corner is a chamfer — the corner a milled part
   * has because a cutter has a radius, not because someone chose a friendly one — and at 2px a
   * panel still reads as a plate rather than a card.
   */
  --radius-control: 1px;
  --radius-panel: 2px;

  /*
   * Elevation, as named values rather than a Tailwind `shadow-lg` per site.
   *
   * `overlay` is a surface that has left the layout; `hud` is a control cluster lying on the
   * canvas, and it is shallower because the thing underneath it is the user's model, not our
   * chrome. Both now lead with a hairline ring rather than a soft drop: a floating panel in this
   * register is a plate sitting on the surface with a machined edge, and a blurred shadow alone
   * reads as a card lifted off a page.
   *
   * `etch` is the third and is the retune's one addition — an inset top-light on a raised
   * control, which is what makes a button read as a physical key rather than a filled rectangle.
   * It is a highlight rather than a shadow, so it is the same value on every palette; on the
   * light ramps it simply does very little, which is correct.
   *
   * Deliberately not themed. A shadow is cast light, so it is dark under a light palette too, and
   * the alternative — a `--color-shadow` token — would have to carry alpha, which the 6-digit
   * contrast parser in tokens.test.ts is right to refuse.
   */
  --shadow-overlay: 0 0 0 1px rgb(0 0 0 / 0.5), 0 12px 32px -12px rgb(0 0 0 / 0.5);
  --shadow-hud: 0 0 0 1px rgb(0 0 0 / 0.4), 0 2px 6px -2px rgb(0 0 0 / 0.35);
  --shadow-etch: inset 0 1px 0 0 rgb(255 255 255 / 0.04);

  /*
   * Motion is functional only — it shows where a thing went. §4.3 caps it at 150ms and
   * `tokens.test.ts` holds every duration here to that ceiling.
   *
   * The retune added the easings and the second duration, because "150ms" on its own was not a
   * motion system: it said how long, never how, so every transition in the product was the
   * browser's default ease and the only choice a component could make was to opt out. `out` is
   * for a thing arriving or changing under the pointer — fast at the start, settling — and is the
   * default. `in-out` is for something that moves between two places a user is watching, which in
   * practice is the splitters. Nothing entrance-animates; §4.3 has not changed its mind.
   */
  --default-transition-duration: 120ms;
  --duration-instant: 60ms;
  --ease-out: cubic-bezier(0.2, 0, 0, 1);
  --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);

  /* --- the shell, DESIGN.md §3 ------------------------------------------------------- */

  /*
   * One bar, 44px, holding identity, the pipeline and the verbs — see `TopBar`.
   *
   * It replaces two: a 36px chrome (itself tightened from 44 on 2026-07-30) and the 32px stage
   * strip that the 48px vertical rail folded down into on 2026-08-09. 68px of stacked horizontal
   * rules became 44, which is 24px back to the canvas in an application judged on how much of the
   * window is the model. `--spacing-chrome` and `--spacing-stages` are gone; tilas.xyz's nav sets
   * its own height from this one, so the two products' chrome still measures the same.
   */
  --spacing-topbar: 44px;
  --spacing-inspector: 320px;
  --spacing-statusbar: 26px;

  /*
   * --- the density rhythm ------------------------------------------------------------
   *
   * One height per control size, so a slider, a number field and a button standing in the same
   * property row line up without anyone measuring. Before this every control picked its own
   * `h-6` or `h-8` at the call site and a row of three of them was three heights.
   *
   * All on §4.3's 4px scale (5/6/7 × 4). `control` is the default and is the §6 floor for an
   * inspector target; `control-lg` is what a primary action and a chrome button get, because a
   * button you are meant to find should not be the same size as the checkbox beside it.
   */
  --spacing-control-sm: 20px;
  --spacing-control: 24px;
  --spacing-control-lg: 28px;
  /* Browser and property rows. Matches --spacing-control so a row can hold one without growing. */
  --spacing-row: 24px;
  /* The on-canvas HUD's bar height — taller than a panel control because it is a pointer target
     over a 3D scene, where §6's 24px floor is the wrong reference and the rail's 32px is right. */
  --spacing-hud: 32px;
  /* The label column of a property grid. One width, so every panel's controls share an edge. */
  --spacing-label: 104px;
}

/**
 * Every non-default palette, declared once.
 *
 * The light values have to be applied from two places — an explicit choice, and an unstated
 * preference that follows the platform — and CSS cannot share one declaration block across a
 * media-query boundary. So all palettes live here and the blocks below only map them.
 * tokens.test.ts asserts the two light mappings stay identical: a colour added to one and not the
 * other is a bug nobody notices until they toggle the theme.
 *
 * Why more than light and dark, when §4.1 asks for exactly one accent and no decoration: the
 * viewport background *is* `--surface-0`, so the theme is the backdrop a user judges filament
 * colour against. A warm model over a cool near-black reads differently than over neutral
 * graphite, the same reason a photographer picks a grey card. These are working surfaces, not
 * skins — which is also why every one of them is a neutral ramp rather than a hue, and why the
 * filament palette above is untouched by all of them.
 *
 * The seven ids did not change in the retune even though every value did. A theme id is
 * persisted in `tilas.theme` and validated on read (`theme.ts`), so renaming one silently drops a
 * user back to the default — which reads as the product forgetting a preference, for no gain.
 *
 * Adding one is cheap and safe by construction: tokens.test.ts discovers `[data-theme]` blocks
 * rather than listing them, so a new palette is contrast-checked the moment it exists. That is
 * §4.1's "a theme added later is a theme that never gets tested" turned into a mechanism instead
 * of a warning.
 */
:root {
  /* Cool near-white — blueprint paper. The default when the platform asks for light. */
  --light-surface-0: #e6e9ee;
  --light-surface-1: #f9fbfd;
  --light-surface-2: #eef1f6;
  --light-surface-3: #dadee6;
  --light-text-primary: #12151a;
  --light-text-muted: #4c545d;
  --light-border-subtle: #ccd3dc;
  --light-border-strong: #6c757e;
  /* Dark steel. Dark because paling it would put it under 4.5:1 against a white panel — the
     constraint that has always made light's accent dark, and the reason the pale-accent argument
     above is a *dark*-ramp decision. The old teal existed to stay clear of --ok's forest green;
     a blue accent has no such collision, so the fork in that argument closes. */
  --light-accent: #0d3f55;
  /* Light's accent is dark, so what sits on it is near-white — the one polarity where the
     on-accent colour flips. That inversion is the whole reason this is a token and not a rule. */
  --light-accent-contrast: #f5fafd;
  --light-danger: #a8291a;
  --light-warning: #6f4d05;
  --light-ok: #166534;

  /*
   * Hue-free dark: judges a warm filament without any cast at all pushing it.
   *
   * Its reason for existing survives every change of direction above it, which is the point of
   * having it: the default has a frank cast — cool now, warm before — and graphite offers the
   * *absence* of one. B−R is 0 at every step here against the default's 3/6/8/10. To a user
   * matching a grey filament that is the difference between a match and a near-match, and the
   * direction the default leans decides only which way the near-match is wrong.
   */
  --graphite-surface-0: #0d0d0d;
  --graphite-surface-1: #161616;
  --graphite-surface-2: #202020;
  --graphite-surface-3: #2b2b2b;
  --graphite-text-primary: #ededed;
  --graphite-text-muted: #a3a3a3;
  --graphite-border-subtle: #303030;
  --graphite-border-strong: #757575;
  /* The ice with its own cast all but removed, so the one hue in the interface does not
     reintroduce what the ramp exists to take away. */
  --graphite-accent: #ced3d6;
  --graphite-accent-contrast: #111314;
  --graphite-danger: #ff8f84;
  --graphite-warning: #ebb75f;
  --graphite-ok: #66dc98;

  /* Deeper than the default, for a dark room and a bright panel.
     It used to be told apart by *cast* — the one blue ramp while everything else ran warm — and
     that distinction is gone now the default is cool. What is left is the one that actually
     matters to a user in the dark: **depth**. surface-0 here is #04060b against the default's
     #0d0d10, roughly a third of the luminance, and its blue is frank where the default's is a
     whisper. Kept rather than retired, because "darker" is a real preference and this is the
     ramp that serves it. */
  --midnight-surface-0: #04060b;
  --midnight-surface-1: #0b0f18;
  --midnight-surface-2: #141a26;
  --midnight-surface-3: #1e2634;
  --midnight-text-primary: #eef2f9;
  --midnight-text-muted: #a4b0c2;
  --midnight-border-subtle: #232c3c;
  --midnight-border-strong: #74809a;
  --midnight-accent: #a8c8dc;
  --midnight-accent-contrast: #06101a;
  --midnight-danger: #ff8f84;
  --midnight-warning: #f0bf62;
  --midnight-ok: #68e29e;

  /* Mid-dark: a lighter model does not glare against it the way it does against near-black.
     Cool, like the default, and a full step lighter at every stop. */
  --slate-surface-0: #171a1b;
  --slate-surface-1: #1f2325;
  --slate-surface-2: #292e31;
  --slate-surface-3: #33393d;
  --slate-text-primary: #e5ecef;
  --slate-text-muted: #a8b4ba;
  --slate-border-subtle: #373e42;
  --slate-border-strong: #839097;
  --slate-accent: #c0d5e0;
  --slate-accent-contrast: #161d21;
  --slate-danger: #ffa79b;
  --slate-warning: #f5cf76;
  --slate-ok: #7ce8ac;

  /* The warm ramp, and now the only one. It used to be "warmer light still" beside a light theme
     that was already warm; with the default cool it inherits that whole job on its own — a
     warm-lit desk, and the user judging a warm filament who would be pushed green by every other
     surface here. Deliberately not cooled with the rest: a palette where every option leans one
     way is a palette with no option in it. */
  --paper-surface-0: #f0e9db;
  --paper-surface-1: #fffdf8;
  --paper-surface-2: #f9f4e9;
  --paper-surface-3: #e8e0ce;
  --paper-text-primary: #1a1712;
  --paper-text-muted: #5d5548;
  --paper-border-subtle: #ded5c6;
  --paper-border-strong: #837968;
  --paper-accent: #0d4a52;
  /* Warm, like the rest of paper — a pure white chip on a warm ink reads as a hole in the page. */
  --paper-accent-contrast: #fffdf8;
  --paper-danger: #a32a12;
  --paper-warning: #6b4a05;
  --paper-ok: #15602f;

  /* Maximum separation. Not a style choice — an accessibility one. */
  --contrast-surface-0: #000000;
  --contrast-surface-1: #0a0a0a;
  --contrast-surface-2: #141414;
  --contrast-surface-3: #1f1f1f;
  --contrast-text-primary: #ffffff;
  --contrast-text-muted: #d4d4d4;
  --contrast-border-subtle: #2e2e2e;
  --contrast-border-strong: #949494;
  --contrast-accent: #cfe6f2;
  --contrast-accent-contrast: #000000;
  --contrast-danger: #ffa096;
  --contrast-warning: #ffd36b;
  --contrast-ok: #85f2b0;
}

/*
 * No explicit choice yet: follow the platform, so the first paint is not a flash of the wrong
 * theme. The selector is `:not([data-theme])` — "the user has chosen nothing" — and not
 * `:not([data-theme="dark"])` as it once was. That older spelling matched every theme except
 * dark, so the moment a third palette existed it would have poured light values into graphite and
 * left the winner to source order. Absence of the attribute is the condition actually meant.
 */
@media (prefers-color-scheme: light) {
  :root:not([data-theme]) {
    --color-surface-0: var(--light-surface-0);
    --color-surface-1: var(--light-surface-1);
    --color-surface-2: var(--light-surface-2);
    --color-surface-3: var(--light-surface-3);

    --color-text-primary: var(--light-text-primary);
    --color-text-muted: var(--light-text-muted);

    --color-border-subtle: var(--light-border-subtle);
    --color-border-strong: var(--light-border-strong);

    --color-accent: var(--light-accent);
    --color-accent-contrast: var(--light-accent-contrast);

    --color-danger: var(--light-danger);
    --color-warning: var(--light-warning);
    --color-ok: var(--light-ok);
  }
}

/*
 * The explicit themes. Same token names, different values, resolved by [data-theme] exactly as
 * §4.1 requires. They come after the media block so an explicit choice always beats the platform
 * preference at equal specificity.
 */
:root[data-theme="light"] {
  --color-surface-0: var(--light-surface-0);
  --color-surface-1: var(--light-surface-1);
  --color-surface-2: var(--light-surface-2);
  --color-surface-3: var(--light-surface-3);

  --color-text-primary: var(--light-text-primary);
  --color-text-muted: var(--light-text-muted);

  --color-border-subtle: var(--light-border-subtle);
  --color-border-strong: var(--light-border-strong);

  --color-accent: var(--light-accent);
  --color-accent-contrast: var(--light-accent-contrast);

  --color-danger: var(--light-danger);
  --color-warning: var(--light-warning);
  --color-ok: var(--light-ok);
}

:root[data-theme="graphite"] {
  --color-surface-0: var(--graphite-surface-0);
  --color-surface-1: var(--graphite-surface-1);
  --color-surface-2: var(--graphite-surface-2);
  --color-surface-3: var(--graphite-surface-3);

  --color-text-primary: var(--graphite-text-primary);
  --color-text-muted: var(--graphite-text-muted);

  --color-border-subtle: var(--graphite-border-subtle);
  --color-border-strong: var(--graphite-border-strong);

  --color-accent: var(--graphite-accent);
  --color-accent-contrast: var(--graphite-accent-contrast);

  --color-danger: var(--graphite-danger);
  --color-warning: var(--graphite-warning);
  --color-ok: var(--graphite-ok);
}

:root[data-theme="midnight"] {
  --color-surface-0: var(--midnight-surface-0);
  --color-surface-1: var(--midnight-surface-1);
  --color-surface-2: var(--midnight-surface-2);
  --color-surface-3: var(--midnight-surface-3);

  --color-text-primary: var(--midnight-text-primary);
  --color-text-muted: var(--midnight-text-muted);

  --color-border-subtle: var(--midnight-border-subtle);
  --color-border-strong: var(--midnight-border-strong);

  --color-accent: var(--midnight-accent);
  --color-accent-contrast: var(--midnight-accent-contrast);

  --color-danger: var(--midnight-danger);
  --color-warning: var(--midnight-warning);
  --color-ok: var(--midnight-ok);
}

:root[data-theme="slate"] {
  --color-surface-0: var(--slate-surface-0);
  --color-surface-1: var(--slate-surface-1);
  --color-surface-2: var(--slate-surface-2);
  --color-surface-3: var(--slate-surface-3);

  --color-text-primary: var(--slate-text-primary);
  --color-text-muted: var(--slate-text-muted);

  --color-border-subtle: var(--slate-border-subtle);
  --color-border-strong: var(--slate-border-strong);

  --color-accent: var(--slate-accent);
  --color-accent-contrast: var(--slate-accent-contrast);

  --color-danger: var(--slate-danger);
  --color-warning: var(--slate-warning);
  --color-ok: var(--slate-ok);
}

:root[data-theme="paper"] {
  --color-surface-0: var(--paper-surface-0);
  --color-surface-1: var(--paper-surface-1);
  --color-surface-2: var(--paper-surface-2);
  --color-surface-3: var(--paper-surface-3);

  --color-text-primary: var(--paper-text-primary);
  --color-text-muted: var(--paper-text-muted);

  --color-border-subtle: var(--paper-border-subtle);
  --color-border-strong: var(--paper-border-strong);

  --color-accent: var(--paper-accent);
  --color-accent-contrast: var(--paper-accent-contrast);

  --color-danger: var(--paper-danger);
  --color-warning: var(--paper-warning);
  --color-ok: var(--paper-ok);
}

:root[data-theme="contrast"] {
  --color-surface-0: var(--contrast-surface-0);
  --color-surface-1: var(--contrast-surface-1);
  --color-surface-2: var(--contrast-surface-2);
  --color-surface-3: var(--contrast-surface-3);

  --color-text-primary: var(--contrast-text-primary);
  --color-text-muted: var(--contrast-text-muted);

  --color-border-subtle: var(--contrast-border-subtle);
  --color-border-strong: var(--contrast-border-strong);

  --color-accent: var(--contrast-accent);
  --color-accent-contrast: var(--contrast-accent-contrast);

  --color-danger: var(--contrast-danger);
  --color-warning: var(--contrast-warning);
  --color-ok: var(--contrast-ok);
}

/*
 * DESIGN.md §4.1 spellings. Defined in terms of the Tailwind namespace rather than repeating a
 * value, so the two can never disagree and a theme override reaches both.
 */
:root {
  --surface-0: var(--color-surface-0);
  --surface-1: var(--color-surface-1);
  --surface-2: var(--color-surface-2);
  --surface-3: var(--color-surface-3);

  --text-primary: var(--color-text-primary);
  --text-muted: var(--color-text-muted);

  --border-subtle: var(--color-border-subtle);
  --border-strong: var(--color-border-strong);

  --accent: var(--color-accent);
  --accent-contrast: var(--color-accent-contrast);
  --danger: var(--color-danger);
  --warning: var(--color-warning);
  --ok: var(--color-ok);

  --filament-01: var(--color-filament-01);
  --filament-02: var(--color-filament-02);
  --filament-03: var(--color-filament-03);
  --filament-04: var(--color-filament-04);
  --filament-05: var(--color-filament-05);
  --filament-06: var(--color-filament-06);
  --filament-07: var(--color-filament-07);
  --filament-08: var(--color-filament-08);
  --filament-09: var(--color-filament-09);
  --filament-10: var(--color-filament-10);
  --filament-11: var(--color-filament-11);
  --filament-12: var(--color-filament-12);
  --filament-13: var(--color-filament-13);
  --filament-14: var(--color-filament-14);
  --filament-15: var(--color-filament-15);
  --filament-16: var(--color-filament-16);
}

/*
 * §4.3 — reduced motion disables all of it, and the app stays fully usable. Written as a
 * blanket override rather than per-component so a new component cannot forget to honour it.
 *
 * The !important is load-bearing and biome's noImportantStyles is turned off for this file in
 * biome.json rather than suppressed inline. This rule has to beat utility classes and inline
 * styles alike — a user who has asked the platform to stop moving things has not asked for it
 * to mostly stop, and specificity is not a negotiation we should be having with them.
 */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}
