/* project-clementine.com
 *
 * Same tokens as the product's own interface (`cli/src/ui/app.css`) and the
 * same argument behind them (`docs/design-language.md`). Kept deliberately in
 * step: a marketing site that looks like a different company from the thing it
 * sells is a promise the product then breaks on first launch.
 *
 * Nothing is fetched here. The typeface is served from this origin, so there is
 * no webfont CDN, no icon CDN and no analytics script.
 *
 * # Two palettes, on purpose
 *
 * The **shell** -- the marketing pages, and later the frame holding a user's
 * profile, inbox and settings -- is cream, orange and black. That is the brand.
 *
 * The **tool** -- the workspace itself -- is white and near-black with thin
 * neutral rules. An engineering canvas is looked at for eight hours, and every
 * saturated pixel it spends on itself is one the model cannot spend on meaning
 * (D-26: a colour that carries no information is grey). Brand colour appears
 * there only where it is doing a job.
 */

/* ---------------------------------------------------------------- typeface */

/*
 * Inter, as one variable face.
 *
 * # What this replaces, and why it had to
 *
 * Six Barlow files, and **every one of them was the Vietnamese subset**: 113
 * glyphs, no Latin letters, no digits. So the product had never rendered a
 * single letter in Barlow. Every screen was Segoe UI through the fallback
 * stack, while the stylesheet, the design language and the brand book all
 * described a typeface that was not on screen.
 *
 * Nothing failed. The files were present, they parsed, the browser used them
 * for the two characters they had, and the fallback did the rest — which is
 * why it survived a year of people looking at the screen and saying the type
 * felt wrong without being able to say why.
 *
 * # One file, every weight
 *
 * A variable face at 100..900 is one request that answers every weight,
 * instead of a static file per step and a scale that can only hold the steps
 * somebody remembered to vendor. That constraint is what had kept emphasis at
 * 400-to-700 with nothing in between; 500 now exists because the file it lives
 * in is the same file.
 *
 * `opsz` is Inter's optical size axis and is left to the browser, which sets
 * it from the font size — the reason small text here holds up without being
 * separately tuned.
 *
 * # No italic face, deliberately
 *
 * Italic is used in about ten places, all of them short muted captions. A
 * second 117 KB face for a caption is poor value, so those get a synthesised
 * oblique. If italic ever carries meaning rather than tone, vendor it then.
 *
 * SIL Open Font License 1.1 — `fonts/inter-LICENSE.txt`, served beside it.
 */
@font-face {
  font-family: Inter;
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url("/fonts/inter.woff2") format("woff2-variations"), url("/fonts/inter.woff2") format("woff2");
}

/* ---------------------------------------------------------------- tokens */
/*
 * Material Design 3's **structure**, at Supabase's density, in Clementine's
 * colours. Set 2026-08-23 (D-71), and the reason is worth stating because it is
 * not "we wanted a refresh":
 *
 *   Every component used to choose its own font size, its own grey and its own
 *   hover colour. There were 262 `font-size` declarations and 71 raw hex values
 *   in this file. Nothing was *wrong* in a way a gate could see, and the result
 *   was that making the interface consistent meant somebody noticing each
 *   inconsistency and asking for it to be changed — one screenshot at a time.
 *
 * So the tokens below are not a palette. They are a set of **rules about what
 * goes on what**, which is the part of Material 3 that does the work:
 *
 *   - Every colour pairs. `--on-surface` is legible on `--surface`, and the
 *     accessibility gate proves it rather than trusting it. A component picks a
 *     *ground* and takes the matching ink; it never picks ink and ground apart.
 *   - Every surface is a step on one ladder. `surface-container-*` goes from
 *     recessed to raised in light, and dark inverts the direction, which is what
 *     stops dark mode being an inversion that happens to look wrong.
 *   - Every size is on one scale. `--f-body-m`, not `13.5px`.
 *   - Every interaction is the same interaction. One state layer, at Material's
 *     opacities, composited over whatever ground the component already has.
 *
 * # Brand is not the same thing as primary
 *
 * `--brand` is the artwork: #EE6A21, sampled from the wordmark, the same in
 * both themes because a logo does not change colour when the lights go out.
 * `--primary` is the *role* — the accent the interface acts with — and it is
 * lighter in dark mode because #EE6A21 on a near-black ground is not legible.
 * They are the same value in light and deliberately differ in dark.
 *
 * # Two palettes, still
 *
 * The **shell** — marketing pages, and the frame around profile and settings —
 * is cream, orange and black. That is the brand.
 *
 * The **tool** — the workspace — is neutral, because an engineering canvas is
 * looked at for eight hours and every saturated pixel it spends on itself is
 * one the model cannot spend on meaning (D-26: a colour that carries no
 * information is grey). Brand colour appears there only where it has a job.
 */

:root {
  /* -- action ----------------------------------------------------------- */
  /*
   * **The action colour is ink, not a hue.** Set 2026-09-04, at Cameron's ask
   * to drop the orange: *the orange is too abrasive*.
   *
   * # This was forced by the palette, not chosen for taste
   *
   * The obvious move was to swap #EE6A21 for Linear's indigo. It does not
   * work, and the reason is worth keeping because it will come up again:
   * **there is no free saturated hue left.** The kind palette already spends
   * violet, blue, teal, magenta and slate on *what a thing is*, and green,
   * amber and red mean *status* and cannot be borrowed for chrome in this
   * industry. An indigo action button sitting beside a violet `requirement`
   * diamond would read as a category, which is the one thing hue is not
   * allowed to be ambiguous about here.
   *
   * So the accent is the ink. **Contrast says "this is the action", not hue** —
   * which is what both references actually do: linear.app's primary buttons are
   * high-contrast neutral fills and its colour is spent on labels and status,
   * not on chrome.
   *
   * It is also, finally, the rule this file already claimed to follow. D-26:
   * *a colour that carries no information is grey.* A button is not a category
   * and was never carrying information by being orange.
   *
   * # These flip per theme, and the wordmark now does too
   *
   * The old note here said the brand is one value in both themes because a logo
   * does not change colour when the lights go out. That was true of artwork.
   * The mark is set in type now, so it takes the ink like everything else.
   */
  --brand: #16181d;
  --brand-deep: #000000;

  /* -- colour roles ----------------------------------------------------- */
  --primary: #16181d;
  --on-primary: #ffffff;
  --primary-container: #eef0f3;
  --on-primary-container: #16181d;
  /* Kept as its own token even though it now equals `--primary`. The fill and
     the ink were genuinely different values while the accent was a hue, and
     collapsing them would mean re-separating every call site the day an accent
     comes back. One alias costs nothing; unpicking 53 uses does not. */
  --primary-ink: #16181d;

  /* The surface ladder. Recessed to raised, one step at a time, and the only
     greys any component may stand on. */
  /*
   * **Cooled 2026-09-03, at Cameron's ask to go full neutral.**
   *
   * The ramp was warm — #f6f6f5, #eaeae8, #e2e2df all have R = G > B, a yellow
   * cast biased toward the orange, which is what the note above about choosing
   * neutrals asks for. The trouble is that **the ink standing on it was never
   * cooled to match**: #14161a, #52565e and #6f747c are blue-greys, and always
   * were. So the light theme had warm grounds under cool text, which is the one
   * combination that reads as *unresolved* rather than as either warm or cool.
   * The dark theme had no such problem — #08090a and #8b9098 agree.
   *
   * These are the same lightness steps, rotated to the blue-grey the ink was
   * already using. Contrast is unchanged to within a hundredth; the
   * accessibility gate checks all 24 pairs in both themes and is the reason
   * that can be said rather than hoped.
   */
  /*
   * # The light theme is a cream, and deliberately not a white
   *
   * Cameron, 2026-09-06: *"a near-white cream — only barely perceptible, but it
   * shouldn't be stark white."* Both halves of that are the specification. Warm
   * enough that a screen of it does not glare; not so warm that anybody would
   * call it beige, or notice the hue before they notice the content.
   *
   * **The lightness steps are unchanged.** These are the previous values with
   * the hue rotated from blue-grey to a warm neutral at matched relative
   * luminance — the same operation the note above describes, in the other
   * direction. Every rung lands within 0.003 of the luminance it replaces, so
   * the twenty-four contrast pairs the accessibility gate checks move by less
   * than a hundredth and the gate is what says that rather than this comment.
   *
   * The one real change is the top rung: `#ffffff` has luminance 1.0 and no
   * cream can, so it drops to 0.983. That drop *is* the request — it is the
   * difference between a card that glares and one that does not, and it is the
   * only rung where the shift is visible at all.
   */
  --surface-container-lowest: #fffdf8;
  --surface-container-low: #fdfbf6;
  --surface: #f8f6f1;
  --surface-container: #f3f1ec;
  --surface-container-high: #ecebe6;
  --surface-container-highest: #e4e2de;

  --on-surface: #14161a;
  --on-surface-variant: #52565e;
  /* Deliberately quieter than `on-surface-variant`, and held to 3:1 rather than
     4.5:1 — it is for labels that are large or bold, never for prose. */
  --on-surface-muted: #6f747c;

  /* Borders that identify a control carry a contrast requirement (WCAG 1.4.11,
     3:1); borders that divide do not. Two tokens, because one could only ever
     satisfy one of them — darkening a single token until every button edge
     passed made every divider look drawn on with a marker. */
  --outline: #757a83;
  --outline-variant: #e2e5ea;

  /* -- kind colours ------------------------------------------------------ */
  /*
   * # Hue marks what a thing *is*. It never marks that it is selected.
   *
   * Added 2026-09-04. Both of Cameron's references earn their scannability the
   * same way: linear.app puts a coloured dot on a label and Flow a coloured
   * diamond on an element, with the text beside it left neutral. Clementine's
   * tree rendered every kind in the same grey, or in orange for a definition —
   * so a screen of forty rows had no shape to it and orange, which means
   * *action* everywhere else, meant "is a definition" here.
   *
   * # Five families, not seven, and not twenty-four
   *
   * The brief was "seven hues, one per Kind". That was my framing and it was
   * wrong for this job. The spine's seven Kinds are *definitions* — and
   * `Document`, `Issue` and `Approval` are all `item def` while `Part` and
   * `Person` are both `part def`, so the tree cannot tell them apart from what
   * it actually renders, which is the raw SysML kind. There are 24 of those,
   * which is far past the number of hues anyone can hold.
   *
   * So the 24 are grouped into the distinctions a reader is actually scanning
   * for. Five carry a hue and the sixth deliberately does not:
   *
   *   requirement  requirement, constraint, concern
   *   structure    part, item, occurrence, package
   *   interface    port, connection, interface, flow
   *   behaviour    action, state, calc, case, use case, verification, analysis
   *   value        attribute, enum
   *   (neutral)    view, viewpoint, rendering, metadata, allocation
   *
   * **Verification sits with behaviour rather than taking a sixth hue**, and
   * that is an ontological claim before it is a palette one: verification and
   * analysis are *activities performed against* the model, which is what action,
   * state and case are. It is also true that a sixth saturated hue distinct
   * from these five, from the orange that means action, and from the red, amber
   * and green that mean status, is not available — and in this industry red and
   * green cannot be borrowed for a category.
   *
   * View and metadata get no hue because they are chrome *about* the model
   * rather than model content. An absence is a legitimate member of a scale.
   *
   * # These are glyph colours, and that is what makes them legible
   *
   * Held to 3:1 (WCAG 1.4.11, non-text) rather than 4.5:1, because the hue is a
   * 7px diamond and the label beside it stays `--on-surface-variant`. That was
   * measured, not assumed: as *text* on the raised container, structure came out
   * at 4.08:1 and interface at 4.18:1, both short of AA at 10px. Colouring the
   * glyph instead is the reference behaviour, and it is also the accessible one.
   *
   * The kind is written out in words next to the glyph, so colour is never the
   * only carrier (WCAG 1.4.1). It is redundant, which is the point of it.
   */
  --kind-requirement: #6a4ec9;
  --kind-structure: #2f6fd0;
  --kind-interface: #0d7d74;
  --kind-behaviour: #b0398a;
  --kind-value: #5b6b8a;

  /* -- selection --------------------------------------------------------- */
  /*
   * **A step up the surface ladder, never a tint.**
   *
   * A named alias rather than `--surface-container-high` written out nineteen
   * times, so that changing what selection looks like is one edit. It resolves
   * to a step that is darker than every light ground and lighter than every
   * dark one, so it reads as raised in both themes without a per-theme override.
   */
  --selected: var(--surface-container-high);

  /* **The rail is dark in both themes, so it needs its own pair.**
   *
   * It was `#17140f` on `#cfc7bb` — hard-typed hex, and the last living piece
   * of the "cream, orange and black shell" palette whose four tokens were
   * deleted 2026-09-03 for having zero references. They had zero references
   * because the one component that wanted them had pasted the values in
   * instead, which is also why the rail read warm against a cool application.
   *
   * `--inverse-surface` cannot serve this: it inverts *per theme*, which is its
   * job, and the rail is deliberately dark in both. Neutral now, and measured —
   * 10.6:1, against 11.0:1 before, so nothing is lost. */
  --rail-surface: #16181c;
  --on-rail: #c3c8d0;

  /* For a surface that inverts: a tooltip, a snackbar, a selected row. */
  --inverse-surface: #2a2d33;
  --on-inverse-surface: #f3f4f6;
  /* **The scrim is dark in both themes, and that is not an oversight.**
   *
   * MD3 has a scrim role and it is the one colour that does not invert: the job
   * of the wash behind a dialog is to put the page *further away*, and light
   * always reads as nearer. The two newest scrims reached for
   * `--inverse-surface` instead, which is `#e2e2df` in the dark theme — so on
   * the theme this product actually ships in, the dim behind a dialog was a
   * pale film over the screen. Cameron, 2026-08-27: *I'd like the translucent
   * background to be dark instead of light.*
   *
   * One token, so the four scrims cannot drift apart again. Two of them were
   * already hand-typed and were already slightly different from each other. */
  --scrim: rgb(9 10 12 / 62%);

  /* -- label colours ------------------------------------------------------
   * A **closed set of eight**, and they are tokens rather than literals in the
   * component for the reason the ratchet exists: a colour typed where it is
   * used is a colour nobody can audit, retheme or check the contrast of.
   *
   * Eight because a label palette is chosen by whoever makes the label and read
   * by everybody else — a free hex picker is how a board ends up with mid-grey
   * on mid-grey, and the person choosing is never the person squinting at it
   * later. These are mid-saturation on purpose so each one carries white *and*
   * near-black text, which is what lets the same value serve both themes. */
  /* Remapped onto the accent ramp below, 2026-09-13. These were eight unrelated
     hues chosen one at a time; seven of them are now steps of one ramp, so a
     label, an architecture block and a dot on the sign-in field belong to the
     same family rather than to three palettes that happen to coexist. */
  --label-slate: var(--accent-slate);
  --label-orange: var(--accent-tangerine);
  --label-amber: var(--accent-orange);
  --label-green: var(--accent-willow);
  --label-teal: var(--accent-seagrass);
  --label-blue: var(--accent-cerulean);
  /* Text you can press: a dropdown's value. The one colour in an otherwise
     monochrome control set, so what is clickable reads as clickable. */
  --interactive: #2f64d6;
  /* Named for the ramp step it takes rather than for the colour it used to be.
     It was violet, then briefly slate, which made behaviour and structure two
     neighbouring blues on the architecture diagram. Cyan separates them without
     leaving the family, which is the whole point of having a ramp. */
  --label-cyan: var(--accent-cyan);
  --label-rose: var(--accent-red);

  /* The accent palette, ten steps from red through to blue.
     ---------------------------------------------------------------------
     Cameron's palette, 2026-09-13. One ramp rather than ten unrelated hues:
     they sit at a common lightness and walk the wheel in order, so any two are
     distinguishable and the whole set reads as one family. That is what makes
     it usable for a thing that needs *n* colours and cannot choose them by
     meaning -- a dot on the sign-in field, a series on a chart, a lane.

     Named for the colour rather than the job on purpose. A token called
     --series-3 is one nobody can picture, and these are chosen as a ramp; where
     a colour does carry meaning it gets a role token (--ok, --warn, --bad) and
     those stay as they are. */
  /* Two grades of each hue, because one value cannot do both jobs.

     The ramp as published is built for a dark ground: on our near-white every
     one of the ten falls below 4.5:1 as text, and six fall below 3:1 even as a
     dot. It was not a bad palette, it was a palette being asked to sit on the
     wrong colour.

     So each hue is solved twice, once against each ground, holding the hue and
     moving only the lightness -- in OKLab, where lightness tracks the eye,
     because moving it in sRGB or HSL drags the hue along and the two themes
     stop being the same colour.

       --accent-*        text grade, >= 4.5:1. Words, and any glyph that has to
                         carry meaning on its own.
       --accent-*-fill   fill grade, >= 3:1. Bars, dots, chips -- anything read
                         as an area rather than as a shape.

     On the dark ground the fill grade *is* the published ramp for nine of the
     eleven; only slate and cerulean needed lifting off near-black. Violet does
     not: one value serves both grades there, which is why the dark blocks name
     it twice with the same hex.
     Light mode is where the work is: yellow especially, which cannot be both
     yellow and 4.5:1 on white, and lands as a dark gold. */

  /* The ink that pairs with any `--accent-*-fill`. One value for both themes
     on purpose: the fill grade stays a mid-to-light hue on either ground, so
     the ink that reads on it does not change when the theme does. Eight of
     the eleven fills clear 4.5:1 against it; slate and cerulean do not, and
     `faces.jsx` leaves those two out rather than putting text on them. */
  --on-accent-fill: #14161a;

  --accent-red: #de1e2d;
  --accent-tangerine: #bd500a;
  --accent-orange: #a56108;
  --accent-coral: #be4f01;
  --accent-sun: #8e6c0d;
  --accent-willow: #527d2d;
  --accent-seagrass: #118065;
  --accent-cyan: #387c7a;
  --accent-slate: #57748f;
  --accent-cerulean: #227a9d;
  --accent-violet: #6a4c93;

  --accent-red-fill: #f94144;
  --accent-tangerine-fill: #e9691f;
  --accent-orange-fill: #cf7b0f;
  --accent-coral-fill: #e16e32;
  --accent-sun-fill: #b38915;
  --accent-willow-fill: #6f9b4b;
  --accent-seagrass-fill: #37a081;
  --accent-cyan-fill: #4d908e;
  --accent-slate-fill: #577590;
  --accent-cerulean-fill: #277da1;
  /* Lifted off the text grade rather than reusing it. Every other hue solves
     the fill separately -- lighter, keeping more of the published hue -- and
     violet was the one exception at first, landing darker than the darkest of
     the other nine and reading as a flat plum block beside them. This sits in
     the same lightness band as slate and cerulean, at 4.5:1 where a fill needs
     3:1. */
  --accent-violet-fill: #8560b8;

  --ok: #0f7a52;
  --on-ok-container: #06301f;
  --ok-container: #d6f0e4;
  --warn: #8a5a04;
  --on-warn-container: #3a2500;
  --warn-container: #fbecd0;
  /* **A red that reads as red.** Both of these sat at hue 11 — an orange-red —
     and once the amber came off the statuses this became the only warm colour
     on the planner, so the whole screen still read as tinted. Moved to hue 0.
     It is the same lightness, so every contrast pair holds; what changes is
     that "bad" stops looking like a brand colour. */
  --bad: #bd2f2f;
  --on-bad-container: #4d1105;
  --bad-container: #fbdfd8;

  /* -- state layers ------------------------------------------------------ */
  /*
   * Material's mechanism, and the reason every control in this file used to
   * hover differently: a state is *not* a different colour, it is a translucent
   * layer of the component's own ink over whatever ground it already has. So it
   * works on a white button, an orange one and a selected row without any of
   * them naming a hover colour.
   *
   * A gradient rather than a background-color, so it composites *over* the
   * background the component already set instead of replacing it.
   */
  --st-hover: 8%;
  --st-focus: 10%;
  --st-press: 12%;
  --layer-hover: linear-gradient(color-mix(in oklab, currentColor var(--st-hover), transparent) 0 0);
  --layer-focus: linear-gradient(color-mix(in oklab, currentColor var(--st-focus), transparent) 0 0);
  --layer-press: linear-gradient(color-mix(in oklab, currentColor var(--st-press), transparent) 0 0);
  --layer-drag: linear-gradient(color-mix(in oklab, currentColor 16%, transparent) 0 0);
  /* **The one layer that was not built on `currentColor`**, and therefore the
     one that washed a selected row in accent. It is the most-seen selection in
     the product — every row of every tree — so it was also the largest single
     source of orange on screen. Neutral now, at the same 14%: the 3px inset
     bar on `.tree-row.selected` is the load-bearing signal and stays accent,
     which is the same division the rail already used. */
  --layer-select: linear-gradient(color-mix(in oklab, currentColor 14%, transparent) 0 0);

  /* -- type -------------------------------------------------------------- */
  --font: Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
  /* Kept for one job only: **raw SysML source**, where column alignment is
     information rather than decoration — a `pre`, a `textarea`, the model as
     text. It used to reach twenty-two selectors, which is why the demo looked
     like two typefaces. Inter has tabular figures behind `tabular-nums`, so a
     numeric column lines up without reaching for a second face. */
  --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

  /* **Every weight is loaded**, because the face is variable and they all live
     in the one file. That was not true before: three statics were vendored, so
     `font-weight: 600` sat in this file for years and resolved to 400 or 700
     depending on the platform — a difference nobody could name and everybody
     could see. Four are *named* below, which is a different claim: a token for
     a weight is a statement that some role uses it. */
  --w-thin: 200;
  --w-body: 400;
  /*
   * The structural weight, and the one the scale did not have.
   *
   * Everything that carries structure rather than content — a panel's name, a
   * column head, a tab, a button, a chip — used to be 700, because 400 and 700
   * were the only weights vendored. So a panel title, the column head under it
   * and the tab beside it all shouted equally at the same size as the body text
   * they sat above, and none of them read as more important than another.
   *
   * 500 is the quiet step. 700 stays, and now means something.
   */
  --w-medium: 500;
  --w-bold: 700;

  /*
   * Material's roles, at tool density. Display and headline are the shell's;
   * title, body and label are the tool's, and a workspace should almost never
   * reach above `--f-title-l`.
   *
   * The `font` shorthand, so one declaration carries weight, size, leading and
   * family together and they cannot drift apart. **It resets `font-variant`**,
   * so any rule wanting `font-variant-numeric: tabular-nums` must declare it
   * after — the style gate checks for that, because getting it backwards
   * silently un-aligns a column of numbers.
   */
  /* The two display roles are the shell's, and the shell is read on phones —
     so they carry their own `clamp()` rather than leaving every heading to
     re-derive one. `h1` used to hold `clamp(34px, 5vw, 52px)` inline, and
     lifting it into the role without the clamp would have put a fixed 44px
     headline on a 375px screen. */
  /* **Regular, which is Material's own weight for display** — not the hairline
     the weight comment below implies. The shipped `h1` was 680, resolving to
     700, so lifting it into a role at 200 would have flipped the landing page's
     headline from bold to hairline as a side effect of tidying the tokens. That
     is a design change, and it is not this one's to make. 200 stays available
     for anything that deliberately wants it. */
  /*
   * # The scale was rebuilt 2026-09-03, and the fault it fixes is an interval
   *
   * Cameron: *we are still struggling on the UI side*, with linear.app as the
   * reference. Measuring that page rather than describing it gives the
   * comparison below — every number here was read off `getComputedStyle`, at a
   * 1280px viewport, not estimated:
   *
   *   role              was      now      linear.app
   *   hero              46px     60px     64px   (weight 510, -1.408px = -.022em)
   *   section heading   32px     36px     48px   (weight 510, -1.056px)
   *   body              15px     16px     16px
   *   heading : body    2.13x    2.25x    3.0x
   *
   * **No single step here was wrong. The scale was too narrow across all of
   * them**, and that is a different fault with a different fix. Twelve roles
   * spanning 46px down to 11px put adjacent steps 1-3px apart — 17/15/13,
   * 15/14/13 — and a step a reader cannot see is not hierarchy. It is a choice
   * the next author will make differently, which is how a page ends up with
   * four sizes of the same thing and nobody able to say which is correct.
   *
   * Widening the top is what buys the intervals back, and it is also the whole
   * of what makes the reference page read as composed: Linear's h2 is three
   * times its body text, so a section heading is unmistakably a *chapter*
   * rather than a slightly larger paragraph.
   *
   * We stopped short of 48px because Barlow set larger than Inter for the same
   * optical weight — a lower x-height and narrower forms — so copying the
   * number would have given a headline that measured right and looked
   * oversized.
   *
   * **That reason no longer holds**: the face is Inter now, which is what the
   * reference was measured in. The number here has not been revisited, so it is
   * a size somebody chose for a face that is gone rather than one anybody has
   * argued for. Worth looking at with fresh eyes; not changed on the way past,
   * because the shell's headline is not what this change was about.
   *
   * # Tracking is a function of size, so it belongs to the role
   *
   * One `--k-display` served both a 46px headline and a 32px one. The right
   * value for a display face tightens as it grows, so a single token was wrong
   * for at least one of them — and was in fact slightly wrong for both, loose
   * at the top and over-tight at the bottom. The `font` shorthand cannot carry
   * `letter-spacing`, so each display role now has a `--k-` beside it and they
   * are used together.
   */
  /*
   * # One number per step, and every other form derived from it
   *
   * Set 2026-09-03, after Cameron: *I need a cohesive design language we follow
   * and right now our website and our actual app lack that.* The cause turned
   * out to be measurable rather than a matter of taste. There were **two type
   * scales**, both live, both named as though each were the design system:
   *
   *     role (a `font` shorthand)     raw (a `font-size`)
   *     --f-label-s   11px            --label-sm   12px      off by one
   *     --f-label-m   12px            --label-md   13px      off by one
   *     --f-body-s    13px            --body-sm    13px      agree
   *     --f-body-m    14px            --body-md    14px      agree
   *     --f-title-s   13px            --title-sm   14px      off by one
   *     --f-title-m   15px            --title-md   16px      off by one
   *
   * Four of six pairs disagreed by a pixel. Two components could both be "on
   * the system" and still render a pixel apart, with nothing to say which was
   * right. On top of that sat **136 hand-typed `font-size` declarations** using
   * fifteen distinct values, spread one and two at a time across ninety
   * components — no single unmigrated area, just everybody deciding separately.
   *
   * # Why the second scale existed, which is the part worth keeping
   *
   * Not carelessness. `--f-*` is a `font` shorthand, and **a shorthand cannot
   * set a size on its own.** It brings weight, leading and family with it, so a
   * component wanting only a smaller size — a caption inside a row that must
   * keep the row's weight — had no token it could reach for. The raw family was
   * invented to fill that hole and then drifted from the roles; the literals
   * filled it again wherever the raw family had no step.
   *
   * So the fix is not "migrate harder", because the system was missing a shape
   * of token that components genuinely needed. It is to make **the size itself
   * the token** and derive the shorthand from it. The two forms cannot disagree
   * now, because there is one number and both read it.
   */
  --fs-label-xs: 10px;
  --fs-label-s: 11px;
  --fs-label-m: 12px;
  --fs-label-l: 13px;
  --fs-body-s: 13px;
  --fs-body-m: 14px;
  --fs-body-l: 15px;
  --fs-title-s: 13px;
  --fs-title-m: 15px;
  --fs-title-l: 17px;
  --fs-headline-s: 19px;
  --fs-headline-m: 22px;
  --fs-headline-l: 28px;
  --fs-display-m: clamp(26px, 3.1vw, 36px);
  --fs-display-l: clamp(32px, 3.9vw, 44px);
  --fs-display-xl: clamp(38px, 5.2vw, 60px);

  /* **`--fs-label-xs` is a floor the scale did not have, and 24 components had
     already found it.** Ten pixels is where a dense tool puts an uppercase kind
     badge — PART, REQ, TYPE — and uppercase at 10px with tracking is legible
     where 10px of sentence case would not be. Adding a step because people used
     it is normally how a scale rots; what makes this one defensible is that it
     sits at the *end* of the scale rather than between two existing steps, it
     has one stated job, and the alternative was leaving 24 components with no
     token they could use. */

  --f-display-xl: var(--w-body) var(--fs-display-xl)/1.04 var(--font);
  --f-display-l: var(--w-body) var(--fs-display-l)/1.1 var(--font);
  --f-display-m: var(--w-body) var(--fs-display-m)/1.18 var(--font);
  --f-headline-l: var(--w-body) var(--fs-headline-l)/1.18 var(--font);
  --f-headline-m: var(--w-bold) var(--fs-headline-m)/1.25 var(--font);
  --f-headline-s: var(--w-bold) var(--fs-headline-s)/1.3 var(--font);
  --f-title-l: var(--w-bold) var(--fs-title-l)/1.3 var(--font);
  /* The name of a region — a panel, a dialog. Medium for the same reason the
     labels are: at 15px over 13px body, size already carries the difference and
     700 on top of it turns a name into an announcement. `--f-title-l` and the
     headlines stay bold, because they are the largest thing on a screen and
     have nothing above them to be quieter than. */
  --f-title-m: var(--w-medium) var(--fs-title-m)/1.35 var(--font);
  /* Medium, and it is the same 13px as the body text beside it — so weight is
     the only thing distinguishing them and 700 made that distinction shout. */
  --f-title-s: var(--w-medium) var(--fs-title-s)/1.35 var(--font);
  --f-body-l: var(--w-body) var(--fs-body-l)/1.55 var(--font);
  --f-body-m: var(--w-body) var(--fs-body-m)/1.5 var(--font);
  --f-body-s: var(--w-body) var(--fs-body-s)/1.45 var(--font);
  /*
   * Labels are medium, not bold.
   *
   * A label names something on screen: a column, a tab, a chip, a button. Its
   * job is to be found when looked for and to stay out of the way otherwise,
   * and 700 at 12px does the opposite — it is the loudest thing in a table of
   * numbers that are the point.
   */
  --f-label-l: var(--w-medium) var(--fs-label-l)/1.2 var(--font);
  --f-label-m: var(--w-medium) var(--fs-label-m)/1.2 var(--font);
  --f-label-s: var(--w-medium) var(--fs-label-s)/1.2 var(--font);

  /* **The shell reads at 16px; the tool reads at 15px, and that is deliberate.**
   *
   * `--f-body-l` is the workspace's, and the workspace is a dense canvas where a
   * pixel of body text is a pixel not spent on the model. A marketing page is
   * read once, at arm's length, by somebody deciding whether to care — so it
   * gets the extra pixel and the extra leading. Borrowing the tool's role for
   * the shell is what made every landing page paragraph feel cramped under a
   * heading that was itself too small to compensate.
   *
   * The lede clamps because it is the one paragraph on a page that is read
   * before the reader has decided to read anything. */
  --f-shell-body: var(--w-body) 16px/1.62 var(--font);
  --f-shell-lede: var(--w-body) clamp(17px, 1.5vw, 20px)/1.55 var(--font);

  /* Tracking, where it is not zero. Uppercase eyebrows need air; a display face
     at 60px needs the opposite, and needs more of it than one at 36px. */
  --k-display-xl: -.034em;
  --k-display-l: -.028em;
  --k-display-m: -.022em;
  /* The old single token, kept pointing at the middle role so the twenty-odd
     rules that still name it do not silently lose their tracking. It is a
     migration surface, not a fourth value. */
  --k-display: var(--k-display-m);
  --k-label: .01em;
  /* Raised from .06em. Three rules doing the same job — `.eyebrow`, `.label`,
     `.tag` — carried .12em, .08em and .06em, which is precisely the drift the
     token exists to stop; converging them needs one value, and .08em is the one
     that reads as spaced rather than as either cramped or gappy at 11-12px. */
  --k-eyebrow: .08em;

  /* -- shape ------------------------------------------------------------- */
  /* Material's scale, pulled tight. This is a dense tool, and a 28px corner on
     a 32px row is a pill nobody asked for. */
  /* **The scale's zero, and it was missing.** MD3 has a `none` role and this
     did not, so every full-bleed surface -- a sheet against a phone's edge, a
     row that runs to both margins -- wrote `border-radius: 0` by hand. The
     ratchet has now caught that three separate times and been right each time:
     a literal in the stylesheet is a literal whether it is 12px or 0, and the
     third occurrence is the one that says the scale has a hole rather than the
     author being careless. */
  --shape-none: 0;
  /* **One radius for the application's chrome**, at Cameron's ask 2026-08-27:
   * *the completely circular elements all over the UI should instead be
   * rectangles with rounded edges, and the rounded edges should all be the same
   * radius.* It is `--shape-m` under another name, and it is a name rather than
   * a second value so that changing his mind is one edit rather than forty.
   *
   * `--shape-full` survives for the three places roundness is *functional*
   * rather than decorative -- the spinner, the meter's track and the tiny
   * status dots, where an 8px radius on a 9px box is a circle drawn the long
   * way round. */
  --shape-chrome: 8px;
  --shape-xs: 4px;
  --shape-hair: 2px;
  /* One device pixel, as a token rather than as a literal.
     The cut between the slices of the priority bar, and the only place a rule
     needs a width this small. In the token block because that is where a value
     is allowed to be written once; typed into the rule it would be one more
     against the style budget for something the system should simply have. */
  --hair: 1px;
  --shape-s: 6px;
  --shape-m: 8px;
  --shape-l: 12px;
  --shape-xl: 16px;
  --shape-full: 999px;

  /* -- space ------------------------------------------------------------- */
  /* A 4px base on Material's 8dp grid, at its *compact* density — this is a
     tool, and rows a person scans a thousand times a day should not be spaced
     like a landing page. */
  /*
   * **Three half-steps at the bottom, because a 4px grid is too coarse for the
   * inside of a control.**
   *
   * Measured 2026-09-03: 408 spacing declarations used a token and 287 used a
   * literal, across **36 distinct pixel values**. The style gate never counted
   * space at all, so unlike colour and type there was nothing stopping that
   * number growing — and it had grown to 2, 3, 5, 6, 7, 9, 10, 11, 13, 14, 15,
   * 18, 20, 22, 26 and more.
   *
   * Reading what the off-grid values were actually doing says the scale was
   * partly at fault rather than only the authors. The three heaviest —
   * **2px (45 uses), 6px (28), 10px (26)** — are all *interior* measurements: a
   * badge's vertical padding, the gap between an icon and its label, a chip's
   * inset. The step from 4px to 8px is a doubling, and inside a 22px pill that
   * is the difference between cramped and burst. MD3 has the same split: a 4dp
   * layout grid, with component internals free to use 2dp.
   *
   * So the layout scale keeps its 4px grid and gains three named half-steps for
   * interiors. `--s-15` reads as "one and a half", which is what it is. This is
   * a different situation from the two *type* scales unified above: those were
   * one job with two answers, and these are two genuinely different jobs.
   */
  --s-05: 2px; --s-15: 6px; --s-25: 10px;
  --s-1: 4px; --s-2: 8px; --s-3: 12px; --s-4: 16px;
  --s-5: 24px; --s-6: 32px; --s-7: 48px; --s-8: 64px;
  /* **Two steps the tool will never use, and the shell cannot do without.**
   *
   * The scale stopped at 64px, so every section on the marketing site was
   * separated by 60-76px of hand-typed margin — which is to say, by roughly the
   * height of one heading. Sections then ran together and the page read as one
   * long column with bold bits in it.
   *
   * Measured, linear.app separates its sections by **128px** and gives each
   * about a screenful. That is the single largest difference between the two
   * pages that is not typographic, and it costs nothing but scroll. */
  --s-9: 96px; --s-10: 128px;

  /* -- container roles --------------------------------------------------- */
  /*
   * **One object, one definition.** Set 2026-09-04.
   *
   * 110 rules in this sheet independently define a box with padding, a radius
   * and a background, in **73 distinct padding-and-radius combinations** — 73
   * answers to "how big is the box". Radius was already migrated (4 raw values
   * left, all of them legitimately asymmetric or zero); spacing was not.
   *
   * The card family is the clearest case and the reason these exist. `.card`
   * was correct at `--s-5`; `.strip`, `.tier`, `.card-panel` and
   * `.feature-list article` each said 26px, `.member` and `.milestone` 22px.
   * Six rules for one object, none of them agreeing, every one of them 2-4px
   * off a 4px grid — which is exactly the drift that reads as unfinished
   * without anybody being able to point at it.
   */
  --pad-card: var(--s-5);
  --pad-tile: var(--s-3);
  /* Three chip sizes, because there genuinely are three: a tiny inline tag, a
     chip that carries a value, and a pill you can click — which needs a bigger
     target than either. Naming them stops a fourth appearing. */
  --pad-tag: var(--s-05) var(--s-15);
  --pad-chip: var(--s-1) var(--s-25);
  --pad-pill: var(--s-15) var(--s-3);

  /* Control heights. 32px is the tool's row; 40px is the shell's, which meets
     the 40x40 target size WCAG 2.2 (2.5.8) asks for. Tool controls sit inside a
     pointer-driven canvas and are exempt under the same rule's spacing
     provision, which the a11y gate accounts for. */
  --h-row: 32px;
  /* Chart heights. One figure, one sparkline: a chart that set its own would be
     the container problem again, one axis down. */
  --h-chart: 120px;
  --h-spark: 34px;
  /* A scrolling pane inside a panel -- a long table, a list. Tall enough
     that scrolling is the exception rather than the way it is read. */
  --h-pane: 520px;
  /* A panel read rather than worked in: a sign-in, a confirmation. Narrow
     enough that a line of prose in it is a comfortable measure. */
  --w-pane: 420px;
  /* A card on a marketing page that is read once, not worked in: wide enough
     that a plan's rate rows and its list sit side by side without wrapping,
     narrow enough that the eye crosses a row without losing it. Its own token
     so it cannot move when an app panel does. */
  --w-marketing-card: 630px;

  /* The workspace's three columns. The middle one is unbounded on purpose: a
     wide window should grow the thing being worked on, not the navigator. */
  --w-nav-min: 200px;
  --w-nav: 260px;
  --w-inspector-min: 260px;
  --w-inspector: 340px;
  /* What a collapsed side leaves behind. A panel that vanishes completely is a
     panel nobody can get back, so the stub is the way in rather than a
     decoration -- wide enough for a target at the size WCAG asks for. */
  --w-stub: 28px;
  /* The label column in a `.field`. Fixed, so every control in a pane forms
     one edge; see the primitive for why `auto` is wrong here. */
  --field-label-w: 104px;
  /* Every panel head is this tall, so two panels side by side line up at
     the top. They did not: the inspector's head measured 49px and the
     tree's `.tree-root` 27px, because each set its own padding. */
  --head-h: 40px;
  --h-control: 40px;

  /* -- elevation --------------------------------------------------------- */
  /* Material's six levels, earned from a rule rather than a blur. A wireframe
     aesthetic gets its depth from a hairline; only what genuinely floats above
     the page gets a shadow, and nothing gets two. */
  --e-0: none;
  --e-1: 0 0 0 1px var(--outline-variant);
  --e-2: 0 1px 2px rgb(20 22 26 / 6%), 0 0 0 1px var(--outline-variant);
  --e-3: 0 2px 6px -1px rgb(20 22 26 / 10%), 0 0 0 1px var(--outline-variant);
  --e-4: 0 6px 16px -4px rgb(20 22 26 / 14%), 0 0 0 1px var(--outline-variant);
  --e-5: 0 12px 32px -8px rgb(20 22 26 / 18%), 0 0 0 1px var(--outline-variant);

  /* **The product frame**, and it is a different recipe rather than a sixth
   * level of the same one.
   *
   * A screenshot of the application is the only element on a marketing page
   * that has to read as a *physical object* — a screen photographed, not a div
   * with a border. Measured off linear.app, that effect is three parts and none
   * of them is a big blur:
   *
   *   0 0 0 1px  rgb(0 0 0 / 20%)          a hairline ring, drawn dark
   *   inset 0 0 0 .5px rgb(255 255 255 / 8%)  a half-pixel highlight on the lip
   *   0 2px 32px rgb(0 0 0 / 25%)          one wide, weak shadow
   *
   * The inset highlight is the part everybody leaves out, and it is the part
   * doing the work: it catches the top edge the way a bezel does, which is what
   * separates a raised object from a rectangle sitting on top of another one.
   * Values are adapted here for a light ground, where the same opacities would
   * read as soot. */
  --e-frame:
    0 0 0 1px rgb(20 22 26 / 10%),
    inset 0 1px 0 rgb(255 255 255 / 70%),
    0 1px 2px rgb(20 22 26 / 4%),
    0 12px 40px -12px rgb(20 22 26 / 18%),
    0 40px 80px -32px rgb(20 22 26 / 14%);

  /* -- motion ------------------------------------------------------------ */
  --ease-out: cubic-bezier(.32, .72, 0, 1);
  --ease-spring: cubic-bezier(.34, 1.36, .64, 1);
  --ease-in: cubic-bezier(.4, 0, 1, 1);
  /* Material's standard durations: short, medium, long. */
  --t-press: 100ms;
  --t-quick: 200ms;
  --t-move: 300ms;

  /* The shell's measure. 1180px was set when the widest thing on a page was a
     two-column card grid; it now has to hold a screenshot of a three-pane
     workspace, and a 1180px frame renders that at a scale where the tree labels
     stop being readable. linear.app measures 1364px with 32px gutters. */
  --page: 1280px;

  /* -- names the rest of this file still uses ---------------------------- */
  /*
   * Aliases, not a second palette. Every one of these is a role above under an
   * older name, kept so that 2,900 lines of component CSS did not have to be
   * rewritten in one commit to adopt the scheme — which is how a design system
   * gets half-adopted and then abandoned.
   *
   * They are the migration surface. `agents/scripts/style-gate.mjs` counts what
   * still uses them and refuses to let the number grow.
   */
  --bg: var(--surface);
  /* **The card ground, named apart from `--surface`.** In Material, `surface`
     is the page; in this file it used to mean "the white thing a panel stands
     on", which is `surface-container-lowest`. Both meanings are wanted, so both
     get a name — and which container a card takes depends on the theme, because
     in dark a card is *lighter* than the page and in light it is lighter too,
     which is the same word for opposite ends of the ladder. */
  --surface-card: var(--surface-container-lowest);
  --surface-2: var(--surface-container-low);
  --surface-3: var(--surface-container-high);
  --line: var(--outline-variant);
  --line-strong: var(--surface-container-highest);
  --control-edge: var(--outline);
  --ink: var(--on-surface);
  --ink-2: var(--on-surface-variant);
  --muted: var(--on-surface-muted);
  --tint: var(--primary-container);
  --on-brand: var(--on-primary);
  --brand-ink: var(--primary-ink);
  --r: var(--shape-s);
  --r-lg: var(--shape-l);
  --shadow: var(--e-4);
}

/*
 * Dark.
 *
 * **The ladder inverts, it does not invert.** Material's rule is that a raised
 * surface in dark is *lighter* than the one under it, where in light it is
 * darker — so `surface-container-highest` is the lightest grey here and the
 * darkest there. Flipping each value individually is what produces a dark theme
 * where panels look punched into the page instead of lifted off it.
 *
 * Two selectors for one palette, and both are needed: the media query catches
 * the default "system" setting, where no attribute is stamped on the root at
 * all, and the attribute catches an explicit choice. The media query is guarded
 * so an explicit *light* choice still beats a dark operating system.
 */
/* The dark ramp was re-cut 2026-08-27, and the reason is worth keeping.
 *
 * It ran #17181b for the page and #1c1e21 for a card -- about two per cent of
 * luminance apart. On a desktop monitor in a dim room that reads as a subtle
 * elevation; on a phone in daylight it reads as **one flat surface**, which is
 * exactly what it looked like in the screenshots that prompted this. Elevation
 * that only works in ideal viewing conditions is elevation that does not work.
 *
 * The page is near-black now and a card takes a real step up. Everything else
 * in the ramp keeps its spacing, so nothing that reaches for a token has to
 * change -- which is the whole argument for having had them.
 *
 * Contrast only improves: every foreground token is light, so dropping the
 * grounds raises every ratio on the page. The a11y gate checks it. */
/**
 * Tell the browser which way round the page is.
 *
 * **This was never declared, and it is why every native control was white.**
 * `prefers-color-scheme` is a *query* — it asks what the user wants. The
 * `color-scheme` *property* is the answer: it tells the browser what this page
 * actually is, and the browser then paints everything it owns to match — date
 * and time pickers, `<select>` dropdowns, scrollbars, autofill backgrounds,
 * spinner buttons, the lot.
 *
 * Without it a themed dark application renders a white date picker on top of
 * itself, which is what Cameron photographed on 2026-08-29. No stylesheet rule
 * could have fixed it: those surfaces are drawn by the browser and are not in
 * the DOM.
 *
 * One declaration, and it reaches every native control in the product —
 * including the ones nobody has written yet.
 */
/* The select's chevron. A colour, so it lives with the colours. */
:root { --chevron: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath d='M1.1 1.4 6 6.2l4.9-4.8' fill='none' stroke='%236b7280' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); }
:root { color-scheme: light; }
:root[data-theme="dark"] { color-scheme: dark; --chevron: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath d='M1.1 1.4 6 6.2l4.9-4.8' fill='none' stroke='%23949ba7' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) { color-scheme: dark; --chevron: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath d='M1.1 1.4 6 6.2l4.9-4.8' fill='none' stroke='%23949ba7' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); }
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    /* The ladder inverts: on near-black the action is near-white. The wordmark
       comes with it, which is new -- it was artwork and is type now. */
    --primary: #f0f2f5;
    --on-primary: #0c0d0f;
    --primary-container: #22262b;
    --on-primary-container: #f0f2f5;
    --primary-ink: #f0f2f5;
    --brand: #f0f2f5;
    --brand-deep: #ffffff;

    /* **Black, and a slightly lighter black.**
     *
     * Cameron, 2026-09-05: *move away from some of the gray — dark black and a
     * slightly lighter black, kind of a crispy tech board design.*
     *
     * The old ramp was near-black for two rungs and then jumped to `#202329`,
     * which is a blue-grey and reads as one — and every panel, card and
     * selected row sat on it, so most of the product's area was grey on a theme
     * whose ground was black. **The jump was the problem rather than any one
     * value**: an eighteen-point step between two rungs of a six-rung ladder.
     *
     * Even steps of five or six points now, neutral with the faintest cool cast
     * so the two themes read as siblings rather than as two products. Nothing
     * here is pure black: `#000` under a panel removes the sense that anything
     * is layered at all, and the deepest rung is what the shallowest is
     * measured against.
     *
     * Text contrast only improves, because the ink is light and the ground
     * moved away from it — which is why the accessibility gate has less to say
     * about this than it would about the same change in the light theme.
     */
    --surface-container-lowest: #050607;
    --surface-container-low: #0f1113;
    --surface: #0a0b0d;
    --surface-container: #15171a;
    --surface-container-high: #1c1f23;
    --surface-container-highest: #262a2f;

    --on-surface: #e9eaec;
    --on-surface-variant: #b6bac1;
    --on-surface-muted: #8b9098;

    --outline: #7e848c;
    --outline-variant: #22262b;

    --inverse-surface: #e2e2df;
    --on-inverse-surface: #1c1e21;

    /* The ramp, lifted off near-black. See the light set for why there are
       two grades and how they were solved. */
    --accent-red: #f94144;
    --accent-tangerine: #f3722c;
    --accent-orange: #f8961e;
    --accent-coral: #f9844a;
    --accent-sun: #f9c74f;
    --accent-willow: #90be6d;
    --accent-seagrass: #43aa8b;
    --accent-cyan: #4d908e;
    --accent-slate: #5e7c97;
    --accent-cerulean: #2c81a5;
    --accent-violet: #a98fd4;

    --accent-red-fill: #f94144;
    --accent-tangerine-fill: #f3722c;
    --accent-orange-fill: #f8961e;
    --accent-coral-fill: #f9844a;
    --accent-sun-fill: #f9c74f;
    --accent-willow-fill: #90be6d;
    --accent-seagrass-fill: #43aa8b;
    --accent-cyan-fill: #4d908e;
    --accent-slate-fill: #577590;
    --accent-cerulean-fill: #277da1;
    --accent-violet-fill: #a98fd4;

    --ok: #3fcf8e; --ok-container: #0d3a28; --on-ok-container: #b6ecd3;
    --warn: #e9b949; --warn-container: #40300a; --on-warn-container: #fae3b0;
    --bad: #f07070; --bad-container: #4a1a11; --on-bad-container: #ffcfc4;

    --e-2: 0 1px 2px rgb(0 0 0 / 50%), 0 0 0 1px var(--outline-variant);
    --e-3: 0 2px 8px -1px rgb(0 0 0 / 55%), 0 0 0 1px var(--outline-variant);
    --e-4: 0 6px 18px -4px rgb(0 0 0 / 60%), 0 0 0 1px var(--outline-variant);
    --e-5: 0 12px 34px -8px rgb(0 0 0 / 66%), 0 0 0 1px var(--outline-variant);
    /* The product frame, dark. **The inset highlight inverts in kind, not just
       in value**: in light it is a near-white lip catching a light source above,
       and at 70% it is what makes the frame read as raised. Carried into dark
       unchanged it would be a white line drawn across a dark panel, which reads
       as a rendering fault. At 8% it becomes the bezel highlight linear.app
       uses, which is the same idea at the opacity a dark ground can carry. */
    /* Kind hues, lifted for a dark ground. Measured against the page (#0c0d0f)
       and the raised container (#202329): 6.1:1 at worst, comfortably past the
       3:1 a glyph needs. The light values sit at 4.1-5.0:1 on white and would
       be 1.5:1 here, which is the whole reason these are a second set. */
    --kind-requirement: #a894f2;
    --kind-structure: #7dabf5;
    --kind-interface: #3fc9bd;
    --interactive: #8fb4ff;
    --kind-behaviour: #f087c6;
    --kind-value: #98a6c4;
    /* Kind hues, lifted for a dark ground. Measured against the page (#0c0d0f)
     and the raised container (#202329): 6.1:1 at worst, comfortably past the
     3:1 a glyph needs. The light values sit at 4.1-5.0:1 on white and would
     be 1.5:1 here, which is the whole reason these are a second set. */
  --kind-requirement: #a894f2;
  --kind-structure: #7dabf5;
  --kind-interface: #3fc9bd;
  --interactive: #8fb4ff;
  --kind-behaviour: #f087c6;
  --kind-value: #98a6c4;
  --e-frame:
      0 0 0 1px rgb(0 0 0 / 60%),
      inset 0 1px 0 rgb(255 255 255 / 8%),
      0 2px 4px rgb(0 0 0 / 40%),
      0 16px 48px -12px rgb(0 0 0 / 66%),
      0 48px 96px -32px rgb(0 0 0 / 58%);

    /* The card ground. In light the lowest container is white and sits *above*
       the page; in dark it is the darkest and would sit below it, so a card
       takes the step up instead. The alias moves; nothing that uses it does. */
    --surface-card: var(--surface-container);
    --surface-2: var(--surface-container-low);
    --surface-3: var(--surface-container-high);
    --line-strong: var(--surface-container-highest);
  }
}

:root[data-theme="dark"] {
  --primary: #f0f2f5;
  --on-primary: #0c0d0f;
  --primary-container: #22262b;
  --on-primary-container: #f0f2f5;
  --primary-ink: #f0f2f5;
  --brand: #f0f2f5;
  --brand-deep: #ffffff;

  --surface-container-lowest: #050607;
  --surface-container-low: #0f1113;
  --surface: #0a0b0d;
  --surface-container: #15171a;
  --surface-container-high: #1c1f23;
  --surface-container-highest: #262a2f;

  --on-surface: #e9eaec;
  --on-surface-variant: #b6bac1;
  --on-surface-muted: #8b9098;

  --outline: #7e848c;
  --outline-variant: #22262b;

  --inverse-surface: #e2e2df;
  --on-inverse-surface: #1c1e21;

  /* The ramp, lifted off near-black. See the light set for why there are
     two grades and how they were solved. */
  --accent-red: #f94144;
  --accent-tangerine: #f3722c;
  --accent-orange: #f8961e;
  --accent-coral: #f9844a;
  --accent-sun: #f9c74f;
  --accent-willow: #90be6d;
  --accent-seagrass: #43aa8b;
  --accent-cyan: #4d908e;
  --accent-slate: #5e7c97;
  --accent-cerulean: #2c81a5;
  --accent-violet: #a98fd4;

  --accent-red-fill: #f94144;
  --accent-tangerine-fill: #f3722c;
  --accent-orange-fill: #f8961e;
  --accent-coral-fill: #f9844a;
  --accent-sun-fill: #f9c74f;
  --accent-willow-fill: #90be6d;
  --accent-seagrass-fill: #43aa8b;
  --accent-cyan-fill: #4d908e;
  --accent-slate-fill: #577590;
  --accent-cerulean-fill: #277da1;
  --accent-violet-fill: #a98fd4;

  --ok: #3fcf8e; --ok-container: #0d3a28; --on-ok-container: #b6ecd3;
  --warn: #e9b949; --warn-container: #40300a; --on-warn-container: #fae3b0;
  --bad: #f07070; --bad-container: #4a1a11; --on-bad-container: #ffcfc4;

  --e-2: 0 1px 2px rgb(0 0 0 / 50%), 0 0 0 1px var(--outline-variant);
  --e-3: 0 2px 8px -1px rgb(0 0 0 / 55%), 0 0 0 1px var(--outline-variant);
  --e-4: 0 6px 18px -4px rgb(0 0 0 / 60%), 0 0 0 1px var(--outline-variant);
  --e-5: 0 12px 34px -8px rgb(0 0 0 / 66%), 0 0 0 1px var(--outline-variant);
  /* Kept in step with the media-query block above; see the note there on why
     the inset highlight cannot simply carry its light-mode value across. */
  --e-frame:
    0 0 0 1px rgb(0 0 0 / 60%),
    inset 0 1px 0 rgb(255 255 255 / 8%),
    0 2px 4px rgb(0 0 0 / 40%),
    0 16px 48px -12px rgb(0 0 0 / 66%),
    0 48px 96px -32px rgb(0 0 0 / 58%);

  --surface-card: var(--surface-container);
  --surface-2: var(--surface-container-low);
  --surface-3: var(--surface-container-high);
  --line-strong: var(--surface-container-highest);
}

/* `hidden` means hidden.
 *
 * The attribute's `display: none` comes from the user-agent sheet, so *any*
 * author `display` rule outranks it regardless of specificity. `.demo-loading`
 * set `display: grid`, so the loading panel stayed on screen for every visitor
 * after the engine had finished loading -- with `element.hidden === true` and
 * the element plainly visible. Fifteen elements in the demo are hidden this
 * way, so this is a class of bug rather than one instance. */
[hidden] { display: none !important; }

* { box-sizing: border-box; }
html { scroll-behavior: smooth; }

/* Instant, not absent — the state change stays legible, it simply arrives
   without travelling. See docs/design-language.md. */
@media (prefers-reduced-motion: reduce) {
  :root { --t-press: 1ms; --t-quick: 1ms; --t-move: 1ms; }
  html { scroll-behavior: auto; }
  *, *::before, *::after { animation-duration: 1ms !important; animation-iteration-count: 1 !important; }
}

/* No transitions until after the first frame, so the page does not animate
   itself into existence while somebody is reading it. `site.js` sets this. */
html:not(.ready) *, html:not(.ready) *::before, html:not(.ready) *::after {
  transition: none !important;
}

body {
  margin: 0; background: var(--surface); color: var(--on-surface);
  font: var(--f-body-l); -webkit-font-smoothing: antialiased;
}
/* `--primary`, not `--brand`: the ring has to be visible on a near-black ground
   as well as a white one, and the wordmark's orange is not. That is the whole
   distinction between the artwork and the role. */
:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; border-radius: var(--shape-xs); }
a { color: var(--primary-ink); text-underline-offset: 2px; }
/* Block source keeps a monospace; an inline `code` does not.
 *
 * Inline `code` is used here for *element names* — `Vehicles::Axle::left` in an
 * impact list, a qualified name in the inspector. Those are identifiers a
 * person reads, not source they scan in columns, and setting them in a
 * monospace was most of what still looked like a second typeface after Barlow
 * landed. They stay visually distinct by weight and colour instead. */
pre { font-family: var(--mono); }
code {
  font-family: inherit; font-variant-numeric: tabular-nums;
  font-weight: var(--w-body); letter-spacing: -.005em;
}
pre code { font-family: var(--mono); font-weight: inherit; letter-spacing: normal; }
.muted { color: var(--muted); }
s { color: var(--muted); text-decoration-thickness: 2px; }

/* ------------------------------------------------------------------ chrome */

.topbar {
  position: sticky; top: 0; z-index: 20;
  display: flex; align-items: center; gap: var(--s-5);
  padding: var(--s-3) var(--s-5); border-bottom: 1px solid var(--line);
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  backdrop-filter: blur(10px);
}
.mark { width: 28px; height: 28px; color: var(--brand); flex: none; }
.brand b { display: block; font-size: var(--fs-body-l); font-weight: var(--w-bold); letter-spacing: -.02em; }
/* The wordmark is a tinted alpha mask, not a coloured image.
 *
 * The artwork is one flat colour, so its alpha channel *is* the shape --
 * masking lets `background` decide the colour, which means one 11 KB file
 * serves the orange-on-cream and black-on-white cases and any future one,
 * instead of a PNG per colour. It also inherits `currentColor`, so it is
 * correct in both themes without a second asset. */
/* The name, set in the product's own typeface.
 *
 * It was a script wordmark masked out of a PNG. Real text instead, for three
 * reasons that all point the same way: it is selectable and searchable, it is
 * the name rather than a picture of the name to anything reading the page, and
 * it renders at any size without a second asset — which the masked image did
 * not, hence the `.sm` variant that existed only to pick a different box.
 *
 * The wireframe C still does the icon's job. It has to work at 16px, in a
 * favicon and a collapsed rail, where a wordmark of any kind is a smudge. */
.wordmark {
  font: var(--f-title-l);
  font-weight: var(--w-medium);
  letter-spacing: -.01em;
  color: var(--brand);
  white-space: nowrap;
  flex: none;
  /* Centre the *letters* on the mark, not the line box.
   *
   * A line box is taller than the letters in it and is not centred on them:
   * it carries the font's full ascent and descent plus half the leading, and
   * `align-items: center` on `.brand` can only see that box. So the name sat
   * fractionally low beside a symbol that is centred on its own ink.
   *
   * `text-box: trim-both cap alphabetic` trims the box to exactly the cap
   * band -- top of a capital S down to the baseline -- which is the part of a
   * word the eye reads as its extent. Centring that lands the caps on the
   * mark's middle. Measured: the trimmed box is 12.36px against Inter's cap
   * height of 0.727em at 17px, and its centre sits within 0.01px of the
   * mark's.
   *
   * The alternative was a hand-tuned `translateY`, which would have been one
   * number per size -- wrong the moment the type scale moved, and wrong in the
   * footer, which sets `.sm`. This is derived from the font, so both sizes are
   * right for free. Browsers without it get the untrimmed box, which is where
   * this started and is off by under a pixel on a wide screen. */
  text-box: trim-both cap alphabetic;
}
.wordmark.sm { font: var(--f-title-s); font-weight: var(--w-medium); color: var(--brand); }
/* One definition, and it was two.
 *
 * Both set `display: flex` and the later one won outright, which is the exact
 * hazard the duplicate-selector check describes — it also silently dropped the
 * `color: inherit` the earlier one set, so the brand link took the anchor
 * colour and nobody noticed. The gate could not see it while a comment sat
 * between them; removing that comment is what surfaced it. */
.brand { display: flex; align-items: center; gap: var(--s-3); text-decoration: none; color: inherit; }
.brand .mark { width: 30px; height: 30px; flex: none; color: var(--brand); }
body > footer .mark { width: 22px; height: 22px; color: var(--brand); flex: none; }
.brand i {
  display: block; align-self: flex-end; padding-bottom: var(--s-1);
  font-style: normal; font-weight: var(--w-thin); font-size: var(--fs-label-m);
 color: var(--muted);
  white-space: nowrap;
}
@media (max-width: 720px) { .brand i { display: none; } }

.topbar nav { display: flex; gap: var(--s-1); margin-left: auto; }

/* The one control on the bar that is an action rather than a destination, so it
   is a button and not a seventh link. `margin-left: auto` moves to it when the
   nav collapses on a narrow screen, which keeps it on the bar rather than
   inside the menu — somebody looking for the way in should not have to open a
   menu to find it. */
.topbar-cta { flex: none; margin-left: var(--s-3); text-decoration: none; }
@media (max-width: 720px) {
  .topbar-cta { margin-left: auto; }
}

/* Below 520 the bar has to be measured rather than eyeballed. The brand, the
   button and the two icons have to fit a 375px phone, and the preview pane
   cannot be made narrow enough to show it going wrong.

   # The wordmark shrinks by font size, and it used to shrink by box

   This rule read `width: 92px; height: 33px`, which is how you resize an
   *image* -- and the wordmark was one, masked out of a PNG. It is real text
   now, and those two numbers did two wrong things to it.

   The width clamped a 98px word into a 92px box, so the last letters ran past
   their own boundary and into the gap before the button. The height was worse
   because it was silent: a 33px flex item holding a 22px line box leaves the
   glyphs sitting at the top of it, and `.brand` centres *the box*, so the name
   rendered about five pixels above the middle of the mark beside it. It read
   as a typo in the logo. */
@media (max-width: 520px) {
  .topbar { padding-left: var(--s-3); padding-right: var(--s-3); gap: var(--s-2); }
  .topbar .wordmark { font-size: var(--fs-title-m); }
  .brand { gap: var(--s-2); }
  .topbar-cta { margin-left: auto; padding: 0 var(--s-2); }
}
/* Below 360 the name goes and the mark stays. The button asks for access rather
   than promising a trial, which is 26px longer, and at 320 -- the width WCAG's
   reflow criterion is measured at -- the menu button went past the edge of the
   screen, clipped rather than scrollable. The mark still leads home and the link
   still says "Superposition — home" to anything reading it. */
@media (max-width: 359px) {
  .topbar .wordmark { display: none; }
}
.topbar nav a {
  padding: var(--s-2) var(--s-3); border-radius: var(--r); text-decoration: none;
  color: var(--ink-2); font-size: var(--fs-body-m); font-weight: var(--w-body);
  transition: background var(--t-quick) var(--ease-out), color var(--t-quick) var(--ease-out);
}
.topbar nav a:hover { background: var(--surface-2); color: var(--ink); }
.topbar nav a[aria-current="page"] { background: var(--surface-container-high); color: var(--on-surface); font-weight: var(--w-bold); }
/* Sign in: in the nav so it collapses with it, drawn as the action it is.
 *
 * A bordered link rather than a filled one — the filled button on this bar is
 * *Request access*, and two of them side by side is two primary actions, which
 * is none.
 *
 * That button said *Talk to us* until 2026-09-10, and this comment claimed it
 * had already changed a while before the button actually did — the sweep that
 * took the appointments out read `site/pages`, and the topbar is built in
 * `build.mjs`. **A CTA in a template is a CTA on ten pages that no grep of the
 * pages will find**, which is why a screenshot caught it and a search did not.
 *
 * # `nav-signin`, and the first name was `signin`
 *
 * There is already a `.signin` in this sheet: the sign-in *panel*, at
 * `max-width: 520px; margin: 40px auto 0`. Putting that class on a nav link
 * gave the link a 40px top margin, which pushed it 40px down its own bar and
 * dragged the header from 58px to 105px — every other link stretched to match.
 *
 * Two things wearing one name, which is the failure Cameron asked for a
 * detector for and the one I said a checker could not see, because two things
 * sharing a name is a fact about meaning. `foreignRules` in the style gate
 * catches the *module* version of this — a module sheet restyling a host
 * class — and this was the host colliding with itself, one file, forty
 * lines apart. Renaming is the fix; fighting it with `margin-top: 0` would
 * have left the collision for the next person. */
.topbar nav a.nav-signin {
  margin-left: var(--s-2); border: 1px solid var(--outline-variant);
  color: var(--ink); font-weight: var(--w-medium);
}
.topbar nav a.nav-signin:hover { border-color: var(--outline); background: var(--surface-2); }
/* In the collapsed menu it is a full-width row like the others, so the border
   would draw a box round one item in a list. */
@media (max-width: 1000px) {
  .topbar nav a.nav-signin { margin-left: 0; border: 0; font-weight: var(--w-bold); }
}
button.icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; padding: 0; flex: none; border-radius: var(--shape-s);
  border: 1px solid var(--outline-variant); background-color: var(--surface-card);
  color: var(--on-surface-variant); cursor: pointer;
  transition: color var(--t-quick) var(--ease-out),
              border-color var(--t-quick) var(--ease-out),
              transform var(--t-move) var(--ease-spring);
}
button.icon:hover { background-image: var(--layer-hover); color: var(--on-surface); border-color: var(--outline); }
button.icon:active { transform: scale(.92); transition-duration: var(--t-press); }
button.icon svg { width: 17px; height: 17px; transition: transform var(--t-move) var(--ease-spring); }
/* The theme control turns as it cycles, so three presses read as three states
   rather than one button that may or may not have done anything. */
#theme:active svg { transform: rotate(45deg); }
button.icon.menu { display: none; }

main { max-width: var(--page); margin: 0 auto; padding: 0 var(--s-5) 80px; }
main.wide { max-width: 1400px; }

/* **`body > footer`, not `footer`.** This is the site's own footer, and as a
   bare type selector it reached every `<footer>` in the product -- including the
   action row inside the planner's create sheet, which came out with a border, a
   `--surface-2` fill and 99px of height. Cameron: *remove the random black box
   inside the new item creation pop-up window.* It was this rule, and the
   element was only guilty of being semantically correct.

   The site footer is a direct child of `<body>` on all eight built pages, so
   the child combinator costs nothing and stops the leak at the source rather
   than renaming the component that got hit. */
body > footer {
  border-top: 1px solid var(--line); background: var(--surface-2);
  padding: var(--s-6) var(--s-5); display: grid; gap: var(--s-4); justify-items: center; text-align: center;
}
body > footer > div { display: flex; align-items: center; gap: var(--s-25); color: var(--ink-2); font-size: var(--fs-body-m); }
body > footer nav { display: flex; gap: var(--s-4); flex-wrap: wrap; justify-content: center; }
body > footer nav a { color: var(--ink-2); font-size: var(--fs-body-m); text-decoration: none; }
body > footer nav a:hover { color: var(--brand-ink); }
footer p { margin: 0; font-size: var(--fs-body-s); }

/* ------------------------------------------------------------------ typography */

/* Material's roles, and only those. The weights here used to be 680, 650 and
   650 — three values between the two weights Barlow actually ships, so the
   browser synthesised the difference and no two headings matched. */
/* Headings balance, body text does not orphan its last word.
 *
 * Nothing set `text-wrap`, so every heading broke wherever the line ran out.
 * The landing page's own headline did it in the most visible way available:
 * at 1400px it read *Cost, schedule, risk and / engineering / on one source of
 * truth* — one word alone on the middle line, on the first thing anybody sees.
 *
 * `balance` evens the line lengths across a short block, which is what a
 * heading is; `pretty` only pulls a word down to avoid a one-word last line,
 * which is what a paragraph needs. Using `balance` on body copy would be
 * expensive and would fight the measure, and `pretty` on a headline does not
 * fix an orphan in the *middle*.
 *
 * A `<br>` inside a heading balances each side of it independently, which is
 * what the hero wants: the break is a design decision and the wrapping either
 * side of it is not. */
h1, h2, h3, h4, .lede, .eyebrow, .tier-for { text-wrap: balance; }
p, li, dd, figcaption { text-wrap: pretty; }

h1 { font: var(--f-display-l); letter-spacing: var(--k-display-l); margin: 0 0 var(--s-4); }
h2 { font: var(--f-display-m); letter-spacing: var(--k-display-m); margin: 0 0 var(--s-3); }
h3 { font: var(--f-headline-s); margin: 0 0 var(--s-2); }

/*
 * **A chapter opener is left-aligned and it is not full measure.**
 *
 * `.prose.center` was on four of the six sections of the landing page, which
 * meant the page's whole structure was: centred heading, centred lede, grid;
 * four times. That reads as a template rather than an argument, and it is the
 * single most recognisable tell of a generated page.
 *
 * Left alignment is not a style preference here. A centred paragraph gives the
 * eye a different starting x on every line, which is why centred text is used
 * for one or two lines and almost never for a page. It also throws away the
 * hard left edge that lets a heading, its lede and the grid beneath it read as
 * one block — the alignment *is* the grouping.
 *
 * `.center` used to survive for the final call to action. It has no callers
 * now: every page ends in a `.cta-band`, which centres its own contents and
 * says what it is, so a general-purpose centring modifier was one more way to
 * do the same thing differently. Removed rather than kept "in case" — an unused
 * rule is exactly what the dead-selector check exists to catch.
 */
.prose { max-width: 62ch; margin: var(--s-10) 0 0; }

/* ------------------------------------------- a prose section, on a wide screen
 *
 * The other half of Cameron's note about whitespace. A 62ch measure is the right
 * line length and is not what changes here — what changed is that the measure sat
 * at the left edge of a 1232px band with six hundred pixels of nothing beside it,
 * so every section read as a column somebody forgot to finish.
 *
 * The heading takes its own column and the body keeps its measure. Both are
 * unchanged typographically; the band is simply used.
 *
 * **Only where the section leads with an `h2`.** A section leading with `h1` is
 * the page's masthead — a title and a lede, meant to be read as one block — and
 * splitting that across two columns would make the page's own name look like a
 * sidebar note. The `:has()` keeps that distinction in the sheet rather than in
 * four hand-applied classes that drift.
 *
 * The body column is a measure and the heading column takes the slack, so the
 * pair fills the band while the line length stays put. A fraction on the body
 * instead would give a seventy-character line at one width and a forty-character
 * one at another, and the measure is the thing that must not move. */
@media (min-width: 1001px) {
  .prose:has(> h2:first-child) {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 62ch);
    column-gap: var(--s-8);
    row-gap: var(--s-4);
    align-items: start;
    max-width: none;
  }
  .prose:has(> h2:first-child) > h2 { grid-column: 1; margin-top: 0; }
  /* Everything that is not the heading stacks in the second column, whatever it
     is — a lede, a paragraph, a list. Naming the elements instead would mean a
     section growing a `<table>` silently landing back in the first column. */
  /* **Grid does not collapse margins, and block flow does.**
   *
   * Two paragraphs in normal flow share one 16px gap. The moment this
   * section became a grid they stopped sharing it and every gap in the
   * column doubled — looser on a desktop than the same section on a phone,
   * and looser than an h1-led section beside it, for no reason anybody
   * chose. It is silent, because nothing looks wrong until the two are
   * compared.
   *
   * So the margins go and `row-gap` above is the spacing, which is the
   * pattern `.principle p` already uses for the same reason one grid over. */
  .prose:has(> h2:first-child) > :not(h2) { grid-column: 2; margin-block: 0; }
}
/*
 * **Shell prose takes the shell's body role, and the `:not()` is load-bearing.**
 *
 * `body` sets `--f-body-l`, which is the workspace's 15px, so every paragraph on
 * every marketing page was reading at the tool's density. `.prose p` is
 * specificity (0,1,1) and `.lede` is (0,1,0), so without the exclusion the bare
 * descendant selector would silently outrank the lede's own clamped role and
 * flatten the one paragraph on the page that has to be larger than the rest.
 */
.prose p:not(.lede):not(.fineprint),
.lede { font: var(--f-shell-lede); color: var(--ink-2); }
.eyebrow { font: var(--f-label-m);
  color: var(--on-surface-muted); margin: 0 0 var(--s-3); }
.label { font: var(--f-label-s); color: var(--muted); }
ul.plain { padding-left: var(--s-5); }
ul.plain li { margin: 10px 0; }

/* ------------------------------------------------------------------ buttons */

/*
 * One button, three states, no per-variant hover colours.
 *
 * Every variant below sets a *ground* and an *ink*; the state layer is the same
 * translucent wash of the ink over whatever that ground is. That is why
 * `.btn.primary` needs no `:hover` rule of its own any more — it used to have
 * one naming `--brand-deep`, which is why a filled button and an outlined one
 * darkened by visibly different amounts.
 */
/* A button and a text field were the same rectangle.
 *
 * Compare what they set: a field is `1px solid var(--outline)`, `--shape-s`,
 * `var(--s-2) var(--s-3)` of padding, on a container ground. The outlined
 * button was `1px solid var(--outline)`, `--shape-s`, `var(--s-2) var(--s-4)`
 * of padding, on a card ground. One step of horizontal padding apart.
 *
 * That is survivable while a button is content-width, because a short box with
 * words in it is obviously not somewhere you type. It stops being survivable
 * the moment one is stretched — a grid child stretches by default — and
 * `Sign in` sat above two full-width left-aligned bordered boxes reading
 * `Email me a link instead` and `Carry on without an account`, which Cameron
 * read as empty inputs, because that is what they looked like.
 *
 * **A label is centred and a value is not.** It is the one difference that
 * survives the button being any width, and it costs nothing anywhere else: a
 * content-width button has no spare room to centre anything in. */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--s-2);
  font: var(--f-label-l); letter-spacing: var(--k-label); text-decoration: none;
  padding: var(--s-2) var(--s-4); border-radius: var(--shape-s); cursor: pointer;
  border: 1px solid var(--outline); background-color: var(--surface-card); color: var(--on-surface);
  /* Down instant, up with physics. A press that eases in feels disconnected
     from the finger. */
  transition: background-color var(--t-quick) var(--ease-out),
              border-color var(--t-quick) var(--ease-out),
              transform var(--t-move) var(--ease-spring);
}
.btn:hover { background-image: var(--layer-hover); }
.btn:active { background-image: var(--layer-press); transform: scale(.97); transition-duration: var(--t-press); }
/* The arrow leans into the direction of travel on hover — the smallest
   possible hint that this button goes somewhere. */
.btn span[aria-hidden] { transition: transform var(--t-move) var(--ease-spring); }
.btn:hover span[aria-hidden] { transform: translateX(3px); }
.btn.primary { background-color: var(--primary); border-color: var(--primary); color: var(--on-primary); }
.btn.lg { font: var(--f-title-m); padding: var(--s-3) var(--s-5); min-height: var(--h-control); }
.btn.sm { font: var(--f-label-m); letter-spacing: var(--k-label); padding: var(--s-1) var(--s-3); min-height: var(--h-row); }
.btn:disabled { opacity: .5; cursor: not-allowed; }
.cta { display: flex; gap: var(--s-3); flex-wrap: wrap; margin: 28px 0 14px; }
.fineprint { font-size: var(--fs-body-s); color: var(--muted); margin: 0; }

/* ------------------------------------------------------------------ hero */

/*
 * **The hero is one column, and the product sits under it at full width.**
 *
 * It was two columns — copy left, a mock review panel right — and that shape
 * costs twice. The headline gets half a container to set in, so it wraps at
 * four words and cannot be large; and the product gets half a container to be
 * shown in, so it has to be a *mock* rather than the application, because the
 * application does not read at 560px wide.
 *
 * Stacking them fixes both at once: the headline gets the full measure, and the
 * screenshot below it gets 1280px, which is enough to show the real three-pane
 * workspace with its tree labels legible. That is the arrangement linear.app
 * uses and the reason it uses it.
 */

/* And centred, with nothing in it but the claim, who it is for and the two ways
   in (docs/marketing.md §4). The paragraph that stood beside the headline, and
   the drawn mock under it, are gone: the product under it is a capture. */
.hero { display: flex; flex-direction: column; align-items: center; gap: var(--s-4);
  padding: var(--s-10) 0 0; text-align: center; }
.hero h1 { font: var(--f-display-xl); letter-spacing: var(--k-display-xl); max-width: 20ch; margin: 0;
  text-wrap: balance; }
.hero .lede { max-width: 50ch; margin: 0; text-wrap: balance; }
.hero .cta { justify-content: center; margin: var(--s-2) 0 0; }

/* ---------------------------------------------------------- product shots */
/*
 * A capture of the running product, never a drawing of it (docs/marketing.md §5).
 *
 * Framed in the product's own container — hairline, `--r-lg`, the card ground —
 * with no device chrome, drop shadow or tilt. Where a crop cuts through the
 * interface the cut edge fades into the page instead of stopping on a hard
 * line: a hard edge reads as *broken*, a fade as *there is more*. An uncut edge
 * keeps its border.
 *
 * The fade is a mask, not an animation. Nothing on these pages moves.
 */
.product-shot {
  margin: 0;
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  background: var(--surface-card);
  overflow: hidden;
}
.product-shot img { display: block; width: 100%; height: auto; }
.product-shot.fade-bottom {
  border-bottom-color: transparent;
  mask-image: linear-gradient(to bottom, black 72%, transparent);
}
.product-shot.fade-right {
  border-right-color: transparent;
  mask-image: linear-gradient(to right, black 78%, transparent);
}
.product-shot.fade-bottom.fade-right {
  mask-image: linear-gradient(to bottom, black 72%, transparent), linear-gradient(to right, black 78%, transparent);
  mask-composite: intersect;
}
.hero + .product-shot { margin-top: var(--s-8); }

/* ---------------------------------------------------------------- feature */
/*
 * The unit every marketing page is built from: a label, a title of a few
 * words, one or two sentences, a link to the long form, and one capture.
 * Stacked on a phone; side by side from 900px, alternating with `.flip` so the
 * page has a beat rather than a column.
 */
.feature { display: grid; gap: var(--s-5); align-items: center; margin: var(--s-10) 0 0; }
.feature h2 { font: var(--f-headline-l); margin: var(--s-2) 0; text-wrap: balance; }
.feature p:not(.status-line) { margin: 0; max-width: 40ch; color: var(--ink-2); font: var(--f-shell-body); }
/* "Learn more →", the same wherever it appears — a feature or a card. A link,
   because the planner has its own `li.more` for "…and 8 more". */
a.more { display: inline-block; margin-top: var(--s-3); font: var(--f-label-l); }
@media (min-width: 900px) {
  .feature { grid-template-columns: minmax(0, 4fr) minmax(0, 7fr); gap: var(--s-8); }
  /* The capture keeps the wide column when it changes sides. */
  .feature.flip { grid-template-columns: minmax(0, 7fr) minmax(0, 4fr); }
  .feature.flip > :first-child { order: 2; }
}

.kind { font-size: var(--fs-label-xs); font-weight: var(--w-bold); }
.kind.changed { color: var(--brand-ink); }
.kind.added { color: var(--ok); }
.kind.removed { color: var(--bad); }
.kind.new { color: var(--muted); }
.kind.moved { color: var(--ink-2); }
/*
 * A state tag, and the three states are three appearances.
 *
 * `.now`, `.next` and `.later` were all written in the markup and **none of
 * them existed in CSS**, so "works today", "in progress" and "planned" rendered
 * identically on the landing page — the one section whose entire job is to
 * distinguish them. Nothing errored, and reading the HTML would tell you it was
 * handled.
 *
 * Each takes a semantic container and the ink that pairs with it, so the three
 * are legible on both grounds without any of them naming a colour.
 */
.tag { font: var(--f-label-s);
  border: 1px solid transparent; border-radius: var(--shape-xs); padding: var(--pad-tag);
  background-color: var(--surface-container-high); color: var(--on-surface-variant); }
.tag.now { background-color: var(--ok-container); color: var(--on-ok-container); }
.tag.next { background-color: var(--primary-container); color: var(--on-primary-container); }
.tag.later { background-color: transparent; border-color: var(--outline-variant);
  color: var(--on-surface-muted); }

/* ------------------------------------------------------------------ blocks */

/* A grid that follows a chapter opener belongs to it, so it sits close under
   the lede; the gap that separates *chapters* is `--s-10` on `.prose`. Card
   grids used to carry 60px of their own top margin, which put the same distance
   between a heading and its own cards as between two unrelated sections — and a
   page whose spacing does not encode grouping has to be read to be understood
   rather than scanned. */
.cards { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--s-4); margin: var(--s-6) 0 0; }
.card { border: 1px solid var(--line); border-radius: var(--r-lg); background: var(--surface-card);
  padding: var(--s-5);
  transition: border-color var(--t-quick) var(--ease-out),
              transform var(--t-move) var(--ease-out),
              box-shadow var(--t-move) var(--ease-out); }
.card:hover { border-color: var(--line-strong); transform: translateY(-2px); box-shadow: var(--shadow); }
.card p:not(.status-line) { margin: 0; color: var(--ink-2); font: var(--f-shell-body); }
/* A list in a card — /compare's who-should-buy pair — reads as the card's own
   text: the same role and ink, with markers kept because it is a list. */
.card ul { display: grid; gap: var(--s-2); margin: var(--s-2) 0 0; padding-left: var(--s-5); }
.card li { color: var(--ink-2); font: var(--f-shell-body); }

.caption { font-size: var(--fs-body-s); color: var(--muted); margin: 10px 0 0; }

/* What a capability's status is, under a hairline. On /capabilities it sits in
   a `.feature` and in cards; a card that carries one becomes a flex column so
   `margin-top: auto` pins the status to the bottom and a row of cards reads
   as a row of statuses. Only those cards: the others keep their block flow. */
.status-line {
  margin: var(--s-4) 0 0; padding-top: var(--s-3);
  border-top: 1px solid var(--line); font-size: var(--fs-label-m);
}
.card:has(.status-line) { display: flex; flex-direction: column; gap: var(--s-2); }
.card .status-line { margin-top: auto; }
/* Scoped to the status line because the application's `.pill` — a pressable
   segment, further down — shares the name and won on every property: it reset
   the weight, doubled the padding, and gave a label a pointer and a hover wash.
   Inline-block, because an inline box's padding does not make room in the line,
   so a status that wrapped on a phone drew one label over the other. A card's
   list takes the same label, one per line: /capabilities' integrations. */
:is(.status-line, .card li) .pill { display: inline-block; border: 0; cursor: default;
  font-size: var(--fs-label-xs); font-weight: var(--w-bold);
  border-radius: var(--shape-xs); padding: var(--s-05) var(--s-15); margin-right: var(--s-1); }
:is(.status-line, .card li) .pill.done { background: color-mix(in srgb, var(--ok) 15%, transparent); color: var(--ok); }
:is(.status-line, .card li) .pill.next { background: var(--tint); color: var(--brand-ink); }
/* Working: built and usable, not yet complete. It was written on /capabilities
   and styled nowhere, so it rendered as bare text beside two coloured pills;
   it takes the "works today" tag's pair rather than a colour of its own. */
:is(.status-line, .card li) .pill.now { background: var(--ok-container); color: var(--on-ok-container); }

.principles { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--s-5); margin: var(--s-10) 0 0; }
/* **The heading is a child of the grid, so it takes a cell unless told not
   to.** It was doing exactly that on /about — "What we hold to" sitting in
   column one with the first principle beside it, so the section had no title
   line and the two-column reading order started halfway down a heading. Centred
   text hid it; left alignment does not. */
.principles > h2 { grid-column: 1 / -1; margin: 0; }
.principle { border-left: 3px solid var(--brand); padding: var(--s-1) 0 var(--s-1) var(--s-4); }
.principle p { margin: 0; color: var(--ink-2); font: var(--f-shell-body); }

.team { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: var(--s-4);
  margin: 26px 0 0; }
.member { border: 1px solid var(--line); border-radius: var(--r-lg); background: var(--surface-card); padding: var(--pad-card); }
.member[data-placeholder] { border-style: dashed; }
/* **Scoped to the team card it belongs to.** It was a bare `.avatar` — 52px, a
   border and a bottom margin — which is exactly right on the about page and
   wrong everywhere else. The moment a module drew a person's face in the
   inspector it inherited a marketing card's dimensions, which is the same
   outgrown-container fault that has bitten this file repeatedly. */
.member .avatar { width: 52px; height: 52px; border-radius: var(--shape-chrome);
  background: var(--surface-2); border: 1px solid var(--line); display: grid;
  place-items: center; color: var(--muted); font-size: var(--fs-headline-s); margin-bottom: var(--s-3); }
.member .role { color: var(--on-surface-muted); font-size: var(--fs-body-s); font-weight: var(--w-bold); margin: 0 0 8px; }
.member p { font-size: var(--fs-body-m); color: var(--ink-2); }

/* ------------------------------------------------------------------ milestones */

.milestones { display: grid; gap: var(--s-4); margin: 40px 0 0; }
.milestone { display: grid; grid-template-columns: 92px 1fr; gap: var(--s-5); align-items: start;
  border: 1px solid var(--line); border-radius: var(--r-lg); background: var(--surface-card); padding: var(--pad-card); }
.milestone-id { font-variant-numeric: tabular-nums; font-size: var(--fs-headline-s); font-weight: var(--w-bold); color: var(--on-surface-muted); }
.milestone h2 { font-size: var(--fs-headline-s); margin: 0 0 4px; }
.milestone .target { font-size: var(--fs-label-m); color: var(--muted); margin: 0 0 10px; }
.milestone .gate { margin: 0; color: var(--ink-2); font-size: var(--fs-body-l); }

/* ------------------------------------------------------------------ pricing */

/* As many columns as there are tiers, rather than three.
 *
 * It was `repeat(3, 1fr)` while there were three, and dropping the Modules and
 * Deployed cards on 2026-09-13 left two cards in a three-column grid with a hole
 * where the third used to be. `auto-fit` counts them instead, so removing or
 * adding a tier is an edit to one file rather than two. The floor keeps a card
 * wide enough for its rate table before the row wraps. */
.tiers { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  gap: var(--s-4); margin: 48px 0 0; align-items: start; }
.tier { border: 1px solid var(--line); border-radius: var(--r-lg); background: var(--surface-card);
  padding: var(--pad-card); position: relative; }
/* One plan, since the flat tier was withdrawn: centred at a width its rate rows
   can be read across, rather than stretched over the whole page. */
.tier:only-child { justify-self: center; width: 100%; max-width: var(--w-marketing-card); }
.tier .price { font-size: var(--fs-headline-l); font-weight: var(--w-bold); letter-spacing: -.03em; margin: 6px 0 4px; }
.tier .price span { display: block; font-size: var(--fs-body-s); font-weight: var(--w-body); color: var(--muted); letter-spacing: 0; }
.tier-for { color: var(--ink-2); font-size: var(--fs-body-m); min-height: 44px; }
/* Two prices for one product, not two products.
   Side by side inside the same card, because the choice between them is a
   billing shape rather than a choice of what you get — two cards would say the
   opposite, and the flat plan would read as an upgrade with more in it. */
/* The rate card, as a table because it is one.
 *
 * Two prices side by side (`.tier-two`) was the old shape, when the choice was
 * between a seat price and a program price. There is one price now and three
 * bands of it, which is a row per band and a caption saying that each band
 * prices only the editors inside it — the one thing about graduated pricing that
 * people get wrong, and they get it wrong in our favour, so it is worth the
 * sentence. */
.rates { width: 100%; border-collapse: collapse; margin: var(--s-2) 0 var(--s-3); }
.rates th, .rates td { padding: var(--s-1) 0; text-align: left; font: var(--f-body-s); }
.rates th { color: var(--on-surface-muted); font-weight: var(--w-body); }
/* The guideline mapping: four columns of prose rather than two of figures, so
   the rows need separating and the cells need to start at the top of the row.
   `thin` rather than a width, because a hairline is what this wants and a
   number here is a number the ratchet has to carry. */
.guidelines td, .guidelines th { vertical-align: top; padding: var(--s-2) var(--s-3) var(--s-2) 0; }
/* A floor in characters, so the table overflows into its scroller on a phone
   instead of compressing into it.
   
   Measured at 375px before this line existed: columns of 20, 104, 149 and 54
   pixels, and a single guideline row 156px tall -- about twelve characters per
   line in the column that carries the explanation. It fitted, in the sense that
   nothing overflowed, and it was unreadable.
   
   `ch` rather than a length, and the unit is the argument: what a column of
   prose needs is a number of characters, not a number of pixels. It is also not
   a value the style ratchet counts, which is worth saying out loud rather than
   leaning on quietly -- the ratchet is there to stop magic numbers standing in
   for tokens, and a character count is neither. */
.guidelines { min-width: 58ch; }
.guidelines tbody tr { border-top: thin solid var(--outline-variant); }
.guidelines thead th { color: var(--on-surface); font-weight: var(--w-bold); }
/* Never the page. A table this wide scrolls inside its own box on a narrow
   screen; the alternative is a body that scrolls sideways and takes the
   navigation with it. */
.table-scroll { overflow-x: auto; }
.rates td { text-align: right; font-weight: var(--w-bold); font-variant-numeric: tabular-nums; }
.rates tr + tr th, .rates tr + tr td { border-top: 1px solid var(--line); }
.rates caption { caption-side: bottom; padding-top: var(--s-2); text-align: left;
  color: var(--on-surface-muted); font: var(--f-label-m); }
.tier ul { list-style: none; padding: 0; margin: 18px 0; display: grid; gap: var(--s-25); }
.tier li { font-size: var(--fs-body-m); padding-left: var(--s-5); position: relative; color: var(--ink-2); }
.tier li::before { content: "✓"; position: absolute; left: 0; color: var(--ok); font-weight: var(--w-bold); }
.tier-note { font-size: var(--fs-body-s); color: var(--muted); border-top: 1px solid var(--line); padding-top: var(--s-4); }
.callout-inline { background: var(--tint); border-left: 3px solid var(--brand); padding: var(--s-3) var(--s-4);
  border-radius: 0 var(--r) var(--r) 0; font-size: var(--fs-body-l); }

/* Left, not auto-centred. `.prose` sets the page's left edge and an
   auto-centred block beside it reads as a second, competing column — visible
   on /pricing, where a left-aligned section heading sat above a centred one. */
.faq { max-width: 760px; margin: var(--s-10) 0 0; }
.faq details { border: 1px solid var(--line); border-radius: var(--r); background: var(--surface-card);
  margin: 10px 0; }
.faq summary {
  cursor: pointer; padding: var(--s-4) var(--s-4); font-weight: var(--w-bold); font-size: var(--fs-body-l);
  transition: color var(--t-quick) var(--ease-out);
}
.faq summary:hover { color: var(--on-surface); }
@keyframes reveal { from { opacity: 0; transform: translateY(-3px); } to { opacity: 1; transform: none; } }
.faq details[open] p { animation: reveal var(--t-quick) var(--ease-out) both; }
.faq details[open] summary { border-bottom: 1px solid var(--line); }
.faq details p { margin: 0; padding: var(--s-4) var(--s-4); color: var(--ink-2); font: var(--f-shell-body); }

/* ------------------------------------------------------------------ demo */

/* --------------------------------------------------------- the workspace */

.ws { margin: 40px 0 0; }

/* Material Design 3, at density -2.
 *
 * MD3's density scale subtracts 4px per step from a component's height, and a
 * desktop modelling tool is two steps down: the 40px default becomes 32px. That
 * one token is why controls line up now. Before it, an input arrived at its
 * height through `font-size: 15px; padding: 10px 12px` (≈45px) and the button
 * beside it through `font-size: 13px; padding: 6px 11px` (≈34px) — two
 * independent routes to a number, sitting in the same row, 11px apart. They
 * were the marketing page's control sizes, inherited into a tool that never
 * declared its own density.
 *
 * The rest is MD3's own vocabulary: the shape scale, the type scale roles a
 * dense UI actually uses, and state layers — hover/focus/pressed expressed as
 * the "on" colour laid over the container at a fixed opacity, one mechanism for
 * every control rather than a bespoke hover colour per component. */
/* The tool's scale.
 *
 * On `:root`, not on `.ws`, and the move was not cosmetic. These were scoped to
 * the workspace, and everything built outside it since — the project bar, the
 * rail, the planner, the gate, the setup dialog — sits in `.app`, not in `.ws`.
 * So every `var(--s-2)` and `var(--ctl-h-sm)` in those resolved to nothing:
 * `gap` fell back to zero and `padding: 10px var(--s-4)` was dropped as invalid.
 * The project bar had *no padding at all* and looked like text jammed against
 * an edge, which is exactly how it was reported.
 *
 * A scale that only exists inside one container is a scale the next component
 * silently does without. Defined once, at the root, so scope can never be the
 * reason a measurement disappears again.
 *
 * Marketing pages are unaffected: they do not reference these, and the
 * component rules that do are still scoped where they were. */
/*
 * The workspace's older names, now aliases.
 *
 * **This block used to be a second design system.** It sat six hundred lines
 * below the first, it also called itself Material Design 3, and it declared its
 * own spacing grid (`--g1`…`--g4`), its own shape scale (`--r-xs`, `--r-sm`),
 * its own type sizes (`--label-md`, `--body-sm`) and its own state opacities —
 * every one of them a second answer to a question the block at the top of the
 * file had already answered, and in two cases a *different* answer: 8px versus
 * the 6px small radius, 38px versus the 40px control height.
 *
 * That is the real reason the interface looked unmaintained. Not a missing
 * token: two of everything, so which one a component got depended on which half
 * of the file its author had been reading. Neither was wrong and they could not
 * both be right.
 *
 * So the values are gone and the names point at the roles. Everything that uses
 * them keeps working; nothing can drift, because there is now one number.
 * `agents/scripts/style-gate.mjs` refuses to let a second scale appear again.
 */
:root {
  /* The tool's control height. Deliberately not the shell's 40px: MD3's
     density -2 (32px) reads as a pro tool and is harder to scan, and this is
     the ClickUp end of the range. It costs roughly a fifth of the rows on
     screen, which is the trade. Kept as its own value because a *tool* row and
     a *shell* button are genuinely different sizes, not two names for one. */
  --ctl-h: 38px;
  --ctl-h-sm: var(--h-row);
  --ctl-px: var(--s-3);
}

:root {
  --r-xs: var(--shape-xs);
  --r-sm: var(--shape-m);

  /*
   * **The second scale, aliased into the first rather than running beside it.**
   *
   * These predate the roles and 100 declarations still name them, so the names
   * stay. What changes is that they are no longer independent numbers: each
   * resolves to a `--fs-*` step, so the pixel-wide drift documented at the top
   * of this file cannot come back by anybody editing one list and not the other.
   *
   * The mapping was chosen so that **nothing moves except `--title-md`**, which
   * had no equivalent on the role scale at all:
   *
   *     --label-sm  12px -> --fs-label-m   unchanged
   *     --label-md  13px -> --fs-label-l   unchanged
   *     --body-sm   13px -> --fs-body-s    unchanged
   *     --body-md   14px -> --fs-body-m    unchanged
   *     --title-sm  14px -> --fs-body-m    unchanged
   *     --title-md  16px -> --fs-title-l   16 -> 17px, three module headings
   *
   * Note what the mapping exposes: `--label-md` and `--body-sm` are both 13px
   * and always were — two names for one size, 59 uses between them. They stay
   * separate here because the *roles* differ and a later change might move one
   * and not the other. They are simply no longer two independent chances to be
   * wrong about what 13px is.
   */
  --label-sm: var(--fs-label-m);
  --label-md: var(--fs-label-l);
  --body-sm: var(--fs-body-s);
  --body-md: var(--fs-body-m);
  --title-sm: var(--fs-body-m);
  /* Material's title-medium, and the step above `--title-sm`.
   *
   * It was missing, and every module's section heading reached for it anyway
   * as `var(--title-md, 18px)` — so three headings rendered at a size that is
   * on no scale, and the fallback made that look deliberate. Defining it is
   * what makes the three agree by construction rather than by everyone copying
   * the same literal.
   *
   * **It was 16px, and 16px was on no scale either** — one more step invented
   * to fix the symptom, which is how the file came to hold fifteen distinct
   * sizes. It is `--fs-title-l` now, moving three module section headings from
   * 16px to 17px. That is the only value the whole unification changes, and it
   * is written down here rather than absorbed quietly: a design system that
   * silently resizes things is one nobody believes the next time it says
   * nothing moved. */
  --title-md: var(--fs-title-l);

  /* The state opacities live with the layers that use them, at the top of the
     file. Repeating them here is exactly the duplication this block was — and
     the style gate catches it now, which it did within a minute of being
     written. */

  --g1: var(--s-1); --g2: var(--s-2); --g3: var(--s-3); --g4: var(--s-4);
}

/* On a phone, every control grows to the thumb.
 *
 * 38px is a good desktop density and below the floor for a finger. Measured in
 * the task sheet at 375px, the date inputs came out **21px** tall -- a target
 * you aim at rather than press. A pointer is accurate to a pixel; a thumb is
 * accurate to about 10mm, and no amount of care in the layout above makes up
 * for a control the size of the text inside it.
 *
 * Changed on the tokens rather than per component, so it reaches everything
 * that ever reached for them. That is what D-71 put the numbers in one place
 * for, and it is the difference between this being four lines and forty. */
@media (max-width: 720px) {
  :root {
    --ctl-h: 44px;
    --ctl-h-sm: 40px;
  }
}

/* One height for every control in the tool, taken from the token rather than
   arrived at through padding. `textarea` is deliberately excluded — it is the
   one control whose height is the point. */
.ws input[type="text"], .ws input[type="number"], .ws input:not([type]), .ws select,
.details-scrim input[type="text"], .details-scrim select {
  height: var(--ctl-h);
  padding: 0 var(--ctl-px);
  font-size: var(--body-md);
  border-radius: var(--shape-xs);
  border: 1px solid var(--control-edge);
  background: var(--surface-card);
  color: inherit;
  width: 100%;
}
.ws select, .details-scrim select { padding-right: var(--s-15); cursor: pointer; }
/* Three sizes, and each means one thing everywhere in the tool.
 *
 * This used to read `.ws .btn, .ws .btn.sm { height: var(--ctl-h) }` — `sm`
 * explicitly forced to the *full* height, so the class meant nothing inside the
 * workspace and 32px outside it. The same `.btn.sm` rendered at 38 and at 32 on
 * one screen, which is what "the buttons don't align" looks like from the
 * outside.
 *
 * The selector covers the whole application, not just `.ws`: the project bar,
 * the gate, the planner and every sheet live outside it, and a scale that stops
 * at one container is one the next component silently does without. */
.app .btn, .ws .btn, .gate .btn, .details-scrim .btn, .sheet-scrim .btn {
  height: var(--ctl-h);
  padding: 0 var(--s-4);
  font-size: var(--body-md);
  font-weight: var(--w-bold);
  border-radius: var(--shape-xs);
  gap: var(--s-2);
}
.app .btn.sm, .ws .btn.sm, .gate .btn.sm, .details-scrim .btn.sm, .sheet-scrim .btn.sm {
  height: var(--ctl-h-sm); padding: 0 var(--s-3); font-size: var(--body-sm);
}
.app .btn.xs, .ws .btn.xs, .gate .btn.xs, .details-scrim .btn.xs, .sheet-scrim .btn.xs {
  height: 26px; padding: 0 var(--s-2); font-size: var(--label-md);
}
/* The one deliberate exception: the gate's single call to action is the page's
   whole purpose, so it keeps the marketing size. */
.gate .btn.lg { height: auto; padding: var(--s-3) var(--s-5); font-size: var(--fs-body-l); }

/* MD3 state layers. `color-mix` puts the "on" colour over the container at the
   given opacity, so hover and press read the same on every surface instead of
   each component naming its own hover colour. */
.ws .btn:hover:not(:disabled) {
  background: color-mix(in srgb, var(--ink) var(--st-hover), var(--surface-card));
  border-color: var(--control-edge);
}
.ws .btn:active:not(:disabled) {
  background: color-mix(in srgb, var(--ink) var(--st-press), var(--surface-card));
}
.ws .btn.primary:hover:not(:disabled) {
  background: color-mix(in srgb, #000 var(--st-hover), var(--brand));
  border-color: transparent;
}
.ws input:focus-visible, .ws select:focus-visible, .ws .btn:focus-visible,
.details-scrim input:focus-visible, .details-scrim select:focus-visible, .details-scrim .btn:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 1px;
  border-color: var(--brand);
}
.ws .btn:disabled, .details-scrim .btn:disabled { opacity: .38; }   /* MD3 disabled state opacity */

.pill { font: inherit; font-size: var(--fs-body-s); padding: var(--pad-pill); border-radius: var(--shape-chrome);
  cursor: pointer; border: 1px solid transparent; background: none; color: var(--ink-2);
  transition: background var(--t-quick) var(--ease-out),
              color var(--t-quick) var(--ease-out),
              border-color var(--t-quick) var(--ease-out); }
/* Selected is a colour, not a container.
 *
 * It was a filled ground *and* a border, which is three changes at once — fill,
 * edge and weight — to say one thing, and in a row of six it made the chosen
 * one look like a different kind of control rather than the same control
 * chosen. Ink and weight are enough, and they leave the row reading as a row.
 *
 * Hover keeps its wash: that is the only thing left saying these are pressable
 * before you press one. */
.pill:hover { background: var(--surface-2); }
.pill[aria-pressed="true"] { color: var(--on-surface); font-weight: var(--w-bold); }
@media (prefers-reduced-motion: reduce) { .pill { transition: none; } }

.ws-dirty { font-size: var(--fs-label-m); font-variant-numeric: tabular-nums; color: var(--brand-ink);
  background: var(--tint); border-radius: var(--shape-chrome); padding: var(--s-1) var(--s-25); }

/* A caveat, not a failure: the act succeeded and the record carries a
   qualification the person needs to read. Warn rather than bad, because
   colouring it as an error teaches people to dismiss it. */

/* ---------------------------------------------------------- the four regions
   Navigator, stage, inspector, drawer (docs/workspace.md §3). Named, because a
   region you can name is one a support answer can refer to.

   Fixed regions rather than free-floating panels: that is the failure that made
   Cameo hard to use — panels lost, no two installations alike, documentation
   screenshots matching nobody's screen. Panels move between regions and hide;
   they never float. */
/* Flush, with the seam itself as the control.
 *
 * The gaps were 14px of nothing between four boxes that each already had a
 * border — three lines of separation doing one line's work, on a screen whose
 * scarcest resource is the canvas. The regions now meet, and the join is a
 * draggable splitter rather than dead space.
 *
 * Widths are custom properties so a drag can move them without rewriting the
 * template, and so a saved layout can restore one. */
.regions { display: grid; gap: 0;
  grid-template-columns: var(--w-nav, 264px) 5px minmax(0, 1fr) 5px var(--w-insp, 300px);
  grid-template-rows: minmax(0, 1fr) 5px var(--h-drawer, 200px);
  grid-template-areas:
    "nav  s1 stage s2 insp"
    "s3   s3 s3    s3 s3"
    "drawer drawer drawer drawer drawer"; }

/* The seam. Five pixels wide, with a wider invisible hit area, because a
   splitter you cannot grab is decoration. */

/* `--area` was never defined and nothing sets it, so this resolved to nothing;
   `#split-nav` and `#split-insp` below place them by id and always did. */

#split-nav { grid-area: s1; }
#split-insp { grid-area: s2; }

/* No radius and no gap: the regions tile. A rounded corner needs a background
   behind it to read as a corner, and there is not one any more. */
.region { border: 0; background: var(--surface-card);
  display: flex; flex-direction: column; min-width: 0; overflow: hidden; }

/* A region holds panels as a tabbed group. One panel today in most of them,
   and the shape is the same either way so adding a second is not a rewrite. */

/* The state layer takes the tab's own ink, so hovering a selected tab and
   hovering an unselected one are the same gesture at different weights rather
   than two different colours arrived at separately. */

/* Panels animate in, and the reduced-motion rule below turns it off. */

@keyframes ws-in { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }

/* The drawer answers questions about the whole scope rather than one element,
   so it spans the full width and is short. */

/* One column below a laptop. The drawer stays last, which is where it belongs
   in reading order as well as on screen.
 *
 * `grid-template-rows` has to be reset here, and forgetting it is how the
 * architecture disappeared on a phone: the desktop template is
 * `1fr / 5px / drawer`, those five pixels are the horizontal splitter, and with
 * four stacked areas against three row sizes the **stage** landed in the 5px
 * row. The region clips its overflow, so a 340px diagram was drawn inside a
 * five-pixel box — present in the DOM, invisible on screen, and reported by
 * nothing.
 *
 * Splitters go with it. Dragging a seam between stacked regions has no meaning,
 * and leaving them in put a live 5px control between every pair of panels. */
@media (max-width: 1100px) {
  .regions {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: auto auto auto auto;
    grid-template-areas: "nav" "stage" "insp" "drawer";
  }

  /* Each region gets a height it can actually be used at, rather than whatever
     its contents collapse to. The stage is the one that matters: a diagram is
     the reason somebody opened this on a phone. */

  .region { border-bottom: 1px solid var(--line); }
}

/* ------------------------------------------------------------ requirements */

/* Status, muted.
 *
 * These were outline-only, which is calm and reads weakly: scanning a column of
 * them, the eye has to land on each before the state registers. A filled pill
 * is what makes a status scannable — the choice is how loud the fill is.
 *
 * Muted, so: the hue at ~12% as the ground, ~30% as the edge, and the full
 * token strength reserved for the text. Meaning is legible at a glance and the
 * brand orange stays the only saturated colour on screen. A screen with forty
 * elements on it should not look like a Christmas tree, and status is the one
 * thing that would do it. */
.chip { display: inline-block; font-size: var(--label-md); font-weight: var(--w-bold);
  padding: var(--pad-chip); border-radius: var(--shape-chrome);
  border: 1px solid var(--line-strong); color: var(--ink-2); background: var(--surface-2); }
.chip.ok {
  color: var(--ok);
  background: color-mix(in srgb, var(--ok) 12%, transparent);
  border-color: color-mix(in srgb, var(--ok) 30%, transparent); }
.chip.bad {
  color: var(--bad);
  background: color-mix(in srgb, var(--bad) 12%, transparent);
  border-color: color-mix(in srgb, var(--bad) 30%, transparent); }
/* Unknown is deliberately colourless. "Not checked" is not a mild pass, and
   giving it a hue would put it on the same scale as the states that are. */
.chip.unknown { color: var(--muted); background: var(--surface-2); }
/* An error is not a violation: the design is not wrong, the requirement
   cannot be checked against it. Amber rather than red, and never green. */
.chip.err {
  color: var(--warn);
  background: color-mix(in srgb, var(--warn) 12%, transparent);
  border-color: color-mix(in srgb, var(--warn) 32%, transparent); }

.reqs { display: grid; gap: var(--s-3); grid-template-columns: repeat(auto-fit, minmax(310px, 1fr)); }
.req { border: 1px solid var(--line); border-left: 3px solid var(--line-strong);
  border-radius: var(--r-lg); background: var(--surface-card); padding: var(--s-4) var(--s-4); }
.req.ok { border-left-color: var(--ok); }
.req.bad { border-left-color: var(--bad); }
.req.unknown { border-left-color: var(--line-strong); }
.req.err { border-left-color: var(--warn); }
.req header { display: flex; align-items: center; gap: var(--s-25); margin-bottom: var(--s-2); }
.req h3 { margin: 0; font-size: var(--fs-body-m); font-variant-numeric: tabular-nums; font-weight: var(--w-bold); }
.req header .chip { margin-left: auto; flex: none; }

.method { font-variant-numeric: tabular-nums; font-size: var(--fs-label-s); letter-spacing: .04em;
  background: var(--surface-2); border: 1px solid var(--line); border-radius: var(--shape-xs); padding: var(--pad-tag); }
.method.none { color: var(--muted); font-style: italic; }

.chip.sm { font-size: var(--label-sm); padding: var(--s-1) var(--s-25); }
.meter { height: 5px; border-radius: var(--shape-full); background: var(--surface-2); margin-top: 9px;
  overflow: hidden; border: 1px solid var(--line); }
.meter span { display: block; height: 100%; background: var(--ok);
  transition: width var(--t-move) var(--ease-out); }
.req.bad .meter span { background: var(--bad); }

/* ------------------------------------------------------------ architecture */

.arch { display: block; width: 100%; min-height: 340px; touch-action: none; }

.arch-value { fill: var(--muted); font-variant-numeric: tabular-nums; font-size: var(--fs-label-s); }

/* -------------------------------------------------------------- parameters */

.budgets { display: flex; gap: var(--s-3); flex-wrap: wrap; margin-bottom: 20px; }
.budget { border: 1px solid var(--line); border-radius: var(--r-lg); background: var(--surface-card);
  padding: var(--s-4) var(--s-4); min-width: 210px; display: grid; gap: var(--s-05); }
.budget.partial { border-left: 3px solid var(--warn); }

.params { width: 100%; min-width: 460px; border-collapse: collapse; font-size: var(--fs-body-m); }
.params th { text-align: left; font-size: var(--fs-label-m);
  color: var(--muted); font-weight: var(--w-bold); padding: 0 var(--s-3) var(--s-2); border-bottom: 1px solid var(--line); }
.params td { padding: var(--s-25) var(--s-4); border-bottom: 1px solid var(--line); }
.params .num, .params th.num { text-align: right; }
.params .unit { font-variant-numeric: tabular-nums; font-size: var(--fs-label-m); color: var(--muted); width: 70px; }
.params td.num input { width: 120px; text-align: right; padding: var(--s-15) var(--s-25); font-variant-numeric: tabular-nums;
  font-size: var(--fs-body-s); font-variant-numeric: tabular-nums; }

/* ------------------------------------------------------------------ review */

.status { font-size: var(--fs-label-m); min-height: 20px; font-variant-numeric: tabular-nums; }
.status.ok { color: var(--ok); }
.status.bad { color: var(--bad); }
.status.warn { color: var(--warn); }

@keyframes change-in { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }

.change { padding: var(--s-3) var(--s-3); border: 1px solid var(--outline-variant);
  border-radius: var(--shape-s); background-color: var(--surface-container-low);
  font: var(--f-body-s); }
.change code { font-size: var(--fs-label-m); word-break: break-word; }
.change .meta { font-size: var(--fs-label-m); color: var(--muted); margin-left: var(--s-2); }
.change .pair { display: grid; gap: var(--s-15); margin-top: var(--s-2); }
.change .pair pre { margin: 0; padding: var(--s-15) var(--s-25); border-radius: var(--shape-s); font-size: var(--fs-label-m);
  font-family: var(--mono); white-space: pre-wrap; background: var(--surface-card); }
.change .pair .before { color: var(--bad); }
.change .pair .after { color: var(--ok); }
.change .affects { font-size: var(--fs-label-m); color: var(--ink-2); margin-top: var(--s-2);
  display: flex; gap: var(--s-2); flex-wrap: wrap; align-items: baseline; }
.change .affects .label { font-size: var(--fs-label-s);
  color: var(--muted); }

@media (max-width: 860px) {
  .reqs { grid-template-columns: 1fr; }
}

.spinner { width: 30px; height: 30px; border-radius: var(--shape-full); border: 3px solid var(--line);
  border-top-color: var(--brand); animation: spin 900ms linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
/* The spinner is the one thing that *should* keep moving under reduced motion —
   it is a liveness signal rather than decoration — so it is slowed rather than
   stopped, and exempted from the blanket rule above. */
@media (prefers-reduced-motion: reduce) { .spinner { animation-duration: 2.4s !important; } }

/* ------------------------------------------------- the selection, and views */

/* The bar that makes a selection visible between views. Without it a highlight
   appears on one screen and is unexplained on the next; with it, the tab change
   reads as carrying something across rather than as switching tool. */

/* A short cross-fade, so a tab change reads as a move rather than a cut. */
@keyframes view-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }

/* Selected and merely related are different states: one is what you picked,
   the other is what it touches, and painting them the same loses the answer. */
.req { cursor: pointer; transition: border-color var(--t-quick) var(--ease-out),
  box-shadow var(--t-quick) var(--ease-out); }
.req:hover { border-color: var(--line-strong); }
.req.selected { box-shadow: 0 0 0 2px var(--brand); }
.req:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

/* ------------------------------------------------- the nested canvas */

/* A box with something inside it is a container, so it reads as one: the fill
   drops back so its children sit *on* it rather than beside it in the same
   tone. Without this a parent and its child are the same colour and the nesting
   is only legible from the border, which at two levels deep is a stack of
   hairlines. */

/* The eight resize handles. Square, because round reads as a connection point
   and these are not one — the ports beside them are, and telling a handle from
   a port at a glance is the whole reason they are different shapes. */
/* Keyed off `data-handle` rather than a class per handle, and the dead-selector
   gate is why. The markup builds the class from the handle name, so `.h-nw`
   never appears literally in any source and the gate reported eight rules
   matching nothing -- correctly, on the evidence it has. An attribute selector
   is the same specificity, needs no second name for the same fact, and is
   visible to a checker reading the markup. */

/* Where a drop would land. Filled rather than outlined: an outline on a box
   that already has one is a thicker border, which reads as selection. */

/* The rectangle being dragged out before it is named. */

/* ----------------------------------------- changes from other people */

/* Bottom right, stacked, over everything.
 *
 * Not a panel somebody has to go and look at: the whole point is that a change
 * affecting your work reaches you where you already are. Not a modal either --
 * something moving in a programme is news rather than a question, and a modal
 * for news teaches people to dismiss without reading. */
.live {
  position: fixed; right: var(--s-4); bottom: var(--s-4); z-index: 60;
  display: flex; flex-direction: column; gap: var(--s-2);
  max-width: min(420px, calc(100vw - var(--s-6)));
}

/* The container survives; the item rules went with `demo.js` on 2026-09-14.
   Nothing renders these yet — `model/live.mjs` is the data model and no screen
   draws it — so what the items should look like is recorded there, including
   the contrast lesson that was learned the expensive way. */

/* ------------------------------------------------------- export control */

/* A marking has to be legible at a glance in a list somebody is scanning to
   decide what goes in a package for a supplier — which is the moment it gets
   missed. So it is a badge on the row and on the box, not only a field in the
   inspector, and it borrows the warning colour rather than inventing one: this
   is the same category of thing as an unmet requirement, not a new one. */

/* A segmented control: a small set of exclusive choices, all visible.
 *
 * Used where a dropdown would hide the options behind a click and the choice is
 * consequential enough that seeing them matters -- an export-control regime,
 * where "not assessed" and "assessed, not controlled" are the pair people
 * confuse and a closed dropdown shows neither.
 *
 * Buttons with `aria-pressed`, so it is one control to a screen reader and
 * costs nothing against the raw-control budget: the styled button already
 * exists and a `select` would have been a fifteenth hand-written one. */
.seg { display: flex; flex-wrap: wrap; gap: var(--s-1); }
.seg button {
  flex: 1 1 auto; padding: var(--s-2) var(--s-2);
  border: 1px solid var(--outline-variant); border-radius: var(--shape-s);
  background: var(--surface-card); color: var(--ink-2);
  font: var(--f-label-l); cursor: pointer;
  transition: background var(--t-quick) var(--ease-out), color var(--t-quick) var(--ease-out);
}
.seg button:hover:not(:disabled) { background: var(--surface-2); color: var(--ink); }
.seg button[aria-pressed="true"] {
  background: var(--surface-container-high); color: var(--on-surface);
  border-color: var(--outline); font-weight: var(--w-bold);
}
.seg button:disabled { opacity: .5; cursor: default; }
/* Keyed off `data-tone` rather than a class per regime, for the reason the
   resize handles give: the markup builds the class name, so `.eci-itar` appears
   nowhere literally and the dead-selector gate correctly reported three rules
   matching nothing. */

/* Not assessed is a *question*, not a verdict, and reads as one. */

/* Inherited from what is inside it. Dashed, because it is a consequence rather
   than a determination somebody made about this element. */

/* An advisory note is not a refusal, and must not be dressed as one. Where
   nobody is signed in nothing is being enforced, and saying so plainly is what
   keeps the parts that *are* enforced believable. */

.params tr.lit { background: var(--selected); }
.params tr.lit td { border-bottom-color: color-mix(in srgb, var(--brand) 30%, transparent); }
.params tr.near td:first-child { box-shadow: inset 2px 0 var(--brand); }

.change:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.change .affected { cursor: pointer; border-bottom: 1px dashed var(--line-strong); }
.change .affected:hover { color: var(--brand-ink); border-bottom-color: var(--brand); }

/* What something else has to say about this change.
   A left rule rather than a filled panel: it sits above the change cards, and a
   block of colour there would read as the most important thing in the drawer
   when it is context for the list below it. `warn` is the decision whose
   *basis* moved; `info` is the one being overridden knowingly. */
/* Explanatory prose under a field in the planner. It had no rule at all, so it
   read at full body size beside the field it was explaining — indistinguishable
   from the thing itself. `setup-hint` says the same thing elsewhere; this is the
   planner's name for it, and it now looks the same. */
.hint { margin: var(--s-1) 0 0; font: var(--f-body-s); color: var(--on-surface-variant); }
/* The planner's grouped list. Had no rule, so its groups ran together. */
.pl-list { display: grid; gap: var(--s-3); }

/* ------------------------------------------------ comparing two commits */

/* A statement about the list, above the list rather than in it. */

/* The answer, under the two picks. Its own class rather than reusing
   `.hist-body`, whose left padding is measured to line up under a commit row
   and is meaningless here — a class borrowed for its visibility rather than its
   meaning is how a layout comes to depend on something unrelated. */
/* The tick sits beside the row rather than inside the button — a checkbox
   inside a button is a control inside a control, and clicking either does the
   other one's job. */

/* The same notes, beside the element they are about. `.why` rather than a
   second `.review-notes`, because the two boxes answer different questions and
   will want different room: this one carries a rationale, which is prose. */
.why { display: grid; gap: var(--s-25); margin-top: var(--s-3); }
.why .note { border-left: 3px solid var(--outline-variant);
  padding: 6px 0 6px var(--s-3); font: var(--f-body-s); }
.why .note.info { border-left-color: var(--brand); }
/* Retired, and still on the page — the whole argument of the record. Quieter,
   never hidden: a superseded decision that disappears is exactly how today's
   answer comes to look like it was always the answer. */
/* Quieter is carried by the dashed border, not by dimming the words.
   `opacity: .72` measured 3.71:1 on the retired note's own text — below AA,
   on the one thing the record exists to keep readable. The whole argument
   above is that a superseded decision stays legible; dimming it past
   legibility to signal that it is superseded defeats the argument. */
.why .note.muted { border-left-style: dashed; }

/* A note that carries a face lays out beside it rather than under it. The
   avatar's own appearance is the module's — this only makes room for one, so a
   shell that knows nothing about people can still draw one. */

/* What a note offers to do about itself. Full width under the badge, so two
   actions do not squeeze the name they are about. */

/* A button that reads as a link, for an action inside a sentence. A real link
   would need an href it does not have, and `role="link"` on a button lies to a
   screen reader about where it goes. */
.linkish {
  padding: 0; border: 0; background: none; font: inherit; color: var(--brand-ink);
  text-decoration: underline; text-underline-offset: 2px; cursor: pointer;
}
.linkish:hover { color: var(--ink); }

/* ------------------------------------------------------------- the editor */

.editor { display: grid; grid-template-columns: minmax(0, 1.4fr) minmax(300px, 1fr);
  gap: var(--s-4); align-items: start; }
/* `min-width: 0` because a grid item's default minimum is its content, so a
   tree wider than the column pushes the column instead of scrolling inside it —
   and the page ends up scrolling sideways on a phone. */
/* ======================================================== the panel primitives
 *
 * Set 2026-09-04, at Cameron's ask, after measuring instead of restyling.
 *
 * # Why the UI read as unfinished, stated as a number
 *
 * **110 rules in this sheet independently define "a box with padding, a radius
 * and a background", and 78 independently define a control.** `.card`,
 * `.card-panel`, `.member`, `.milestone`, `.tier`, `.strip`, `.impact`,
 * `.budget`, `.change`, `.req` and `.ws-selection` are one object invented
 * eleven times, each with its own padding and its own type size. Add the 151
 * hand-typed type literals spread one to six across ninety components and the
 * picture is complete.
 *
 * `docs/components.md` already carries the rule that fixes this — *a control is
 * styled because of what it is, not because of where somebody remembered to put
 * it* — and it was applied thoroughly to form inputs and never to containers.
 * These are the containers.
 *
 * What reads as polish is not hue. It is **rhythm**: the same object having the
 * same height, the same padding and the same type wherever it appears. Linear
 * and Supabase look the way they do because roughly eight components are on
 * screen and each is used a hundred times.
 *
 * # Three bugs this replaces, all found by looking rather than by review
 *
 * **`.tree-panel` matched no markup.** It appears once in this file and nowhere
 * else in the repository — the tree's container is `<div class="panel">`. So
 * the inspector drew a bordered, rounded, white card and the tree panel beside
 * it drew nothing at all, which is measurable: 0.8px/12px/white against
 * 0px/0px/transparent.
 *
 * **A panel was a card inside a card.** `.region` already paints
 * `--surface-card`, so the inspector's border and radius nested a white box in
 * a white box. Both of Cameron's references resolve this the same way and it is
 * the resolution taken here: **the region is the container; a panel is a layout
 * region inside it, never a card.** Nested cards are most of what "unpolished"
 * means on a dense screen.
 *
 * **The row height had three values.** `--h-row` says 32px, `.tree-row` said
 * 34px, and `ROW_H` in `demo.js` says 26px — and the virtualiser computes which
 * rows are in view from its own number, so it was rendering the wrong slice and
 * sizing the spacers wrong. The comment above `.tree-row` even said the two
 * must be changed together. One token now, and the script reads it.
 *
 * # The set is closed
 *
 * `panel`, `panel-head`, `panel-body`, `row`, `field`. A screen that needs a
 * sixth should say so out loud rather than inventing it, which is what the 110
 * were.
 */

/* A panel: a layout region, not a card. The `.region` around it paints the
   ground and draws the edge. */
.panel { display: flex; flex-direction: column; min-width: 0; min-height: 0; }

/* One header, one padding, everywhere. `.inspector .panel-head` used to
   override this with a different pair, so the two panels' headers sat at
   different heights — the kind of difference nobody can name and everybody
   sees. */
.panel-head { display: flex; align-items: center; gap: var(--s-2); flex: none;
  min-height: var(--head-h); box-sizing: border-box;
  padding: 0 var(--s-3); border-bottom: 1px solid var(--outline-variant); }
.panel-head .label { font: var(--f-label-l); letter-spacing: var(--k-label);
 color: var(--on-surface-muted); }
.panel-head .muted { margin-left: auto; font: var(--f-label-m); }

/* The scrolling half. Wide content scrolls in here and never takes the page
   sideways with it, which is the rule the params table already taught us. */
.panel-body { flex: 1 1 auto; min-width: 0; min-height: 0; overflow: auto; }

/* A row. One height, from the one token.

   Selection is a neutral ground plus a 3px accent bar (the neutral-selection work):
   the bar is load-bearing, because colour alone fails anyone who cannot
   separate the orange from the grey. */
.row { display: flex; align-items: center; gap: var(--s-2); box-sizing: border-box;
  height: var(--h-row); padding: 0 var(--s-3); cursor: pointer; min-width: max-content; }
.row:hover { background-image: var(--layer-hover); }
.row.lit { background-image: var(--layer-select); }
.row.selected, .row[aria-selected="true"] {
  background-image: var(--layer-select);
  box-shadow: inset 3px 0 var(--primary), inset 0 -1px var(--outline-variant); }
.row:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; }

/* A field: label left, value right, on a fixed column so every control in a
   pane forms one edge. Fixed rather than `auto` deliberately — `auto` moves the
   edge whenever a longer name appears, which is exactly the sort of thing that
   reads as unfinished without anyone being able to say why. */
.field { display: grid; grid-template-columns: var(--field-label-w) minmax(0, 1fr);
  align-items: center; gap: var(--s-2); min-height: calc(var(--ctl-h) + var(--s-1)); }
.field > label { margin: 0; font: var(--f-body-m); color: var(--on-surface-variant);
  display: flex; align-items: baseline; gap: var(--s-1); min-width: 0; }
.field.ro > label { color: var(--on-surface-muted); }
/* Scrolls in its own box, both ways. A deep tree indents past a narrow screen,
   and letting that scroll the *page* sideways is the failure the params table
   already taught us — wide content gets its own container. */
.tree { padding: var(--s-2) 0; max-height: 560px; overflow: auto; }
/* The row height is fixed at 26px and the JavaScript knows it (`ROW_H`).
   Virtualisation needs to work out which rows are in view from the scroll
   offset alone, and it cannot do that if rows are allowed to differ in height.
   Changing this number means changing it in both places. */
/* `--body-md` used to be here and was never defined anywhere — so every tree
   row inherited whatever the panel happened to set, which is why the tree and
   the table never quite matched. */
/* The tree's row is a `.row` in the markup and keeps only what is genuinely the
   tree's: a tighter gap for the guides and twisty, and its own left padding
   because the indent guides start at the edge. Height, states, focus and the
   hairline all come from `.row` now — they used to be duplicated here, at a
   height (34px) that matched neither `--h-row` nor the virtualiser's 26px.

   A hairline between rows via `box-shadow`, not `border-bottom`: a border adds
   a pixel to a fixed-height row and pushes the last one out of the panel. */
.tree-row { gap: var(--s-15); padding: 0 var(--s-4) 0 var(--s-1); font: var(--f-body-s);
  transition: background-color var(--t-quick) var(--ease-out); }

/* Dashed elbows from parent to child.
 *
 * Indentation used to be padding. It is now these cells, so the indent and the
 * line that explains it are the same 16px and cannot drift apart. A cell draws
 * its vertical only while that ancestor still has children coming — otherwise
 * the guides run past the end of a branch and point at nothing. */
.tree-guides { display: flex; align-self: stretch; flex: none; margin-right: -2px; }
.tg { position: relative; width: 16px; align-self: stretch; flex: none; }
.tg.pipe::before, .tg.tee::before {
  content: ""; position: absolute; left: 7px; top: 0; bottom: 0;
  border-left: 1px dashed var(--tree-guide);
}
.tg.ell::before {
  content: ""; position: absolute; left: 7px; top: 0; height: 50%;
  border-left: 1px dashed var(--tree-guide);
}
.tg.tee::after, .tg.ell::after {
  content: ""; position: absolute; left: 7px; top: 50%; width: 9px;
  border-top: 1px dashed var(--tree-guide);
}
:root { --tree-guide: #cdd2da; }
:root[data-theme="dark"], :root:not([data-theme="light"]) { --tree-guide: #333941; }
@media (prefers-color-scheme: light) { :root:not([data-theme="dark"]) { --tree-guide: #cdd2da; } }

/* Stands in for the rows that are not rendered, so the scrollbar is the length
   the whole tree would be. */
.tree-spacer { min-width: 1px; }

/* The fold arrow. Its own hit target, and `tabindex="-1"` because the row is
   already focusable and left/right do this from the keyboard — a second tab
   stop per row would double the length of a keyboard walk through the tree. */
.tree-twisty { display: inline-flex; align-items: center; justify-content: center;
  width: 16px; height: 16px; flex: 0 0 16px; padding: 0; border: 0; background: none;
  color: var(--muted); cursor: pointer; border-radius: var(--shape-xs);
  transition: transform var(--t-quick) var(--ease-out), color var(--t-quick) var(--ease-out); }
.tree-twisty svg { display: block; }
.tree-row:not(.folded) .tree-twisty:not(.empty) { transform: rotate(90deg); }
.tree-twisty:hover { color: var(--ink); background: var(--surface-2); }
.tree-twisty.empty { cursor: default; }
@media (prefers-reduced-motion: reduce) { .tree-twisty { transition: none; } }

/* The element icon. Monochrome and inheriting colour, because colour carries
   meaning here (D-26) and an icon must not spend it. */
.el-icon { flex: 0 0 16px; color: var(--muted); }
.tree-row.selected .el-icon, .tree-row:hover .el-icon { color: var(--ink-2); }
/* A usage is a particular one; a definition is a kind of thing. The dot is the
   only filled part of any glyph, which is what makes the distinction readable
   at sixteen pixels. */
.el-icon .usage-dot { fill: currentColor; stroke: none; opacity: .75; }

/* How much a fold is hiding, so folding does not hide *how much*. */
.tree-folded { font-variant-numeric: tabular-nums; font-size: var(--fs-label-xs); color: var(--muted);
  border: 1px solid var(--line); border-radius: var(--shape-chrome); padding: 0 var(--s-15); }
.tree-row:hover { background-image: var(--layer-hover); }
.tree-row.lit { background-image: var(--layer-select); }
.tree-row.selected { background-image: var(--layer-select);
  box-shadow: inset 3px 0 var(--primary), inset 0 -1px var(--outline-variant); }
.tree-row:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; }
/* The kind, in the emitter's own word. Definitions read heavier than usages,
   because `part def Wheel` and `part left : Wheel` are different rows meaning
   different things and a tree that showed them alike is unreadable. */
/* The kind label, and the diamond that carries its family.
 *
 * `--kind` is set by the family classes below and read by the glyph, so there
 * is one glyph rule rather than one per kind, and a kind with no family listed
 * falls back to the muted ink instead of disappearing. */
.tree-kind { font-variant-numeric: tabular-nums; font-size: var(--fs-label-xs); letter-spacing: .04em;
  color: var(--on-surface-variant); min-width: 74px;
  display: inline-flex; align-items: center; gap: var(--s-15); }
.tree-kind::before {
  content: ""; flex: none; width: 7px; height: 7px;
  background: var(--kind, var(--on-surface-muted));
  /* A diamond rather than a dot, which is Flow's device and reads as an
     element rather than as a status light. Sharp: the style gate refused a
     1px radius here and was right to — the shape scale starts at 4px, which
     on a 7px square is a circle, so the choice was a sharp diamond or a
     dot, and the diamond is the one that carries meaning. */
  transform: rotate(45deg);
}

/* **A definition is marked by weight, not by hue.** It was `--brand-ink`, and
   orange means *action* or *changed* everywhere else in the product — so the
   loudest colour on a tree of forty rows was spent saying "this one is a
   definition", which is neither. */
.tree-kind.def { color: var(--on-surface); font-weight: var(--w-bold); }

/* The five families. Twenty-four SysML kinds, grouped by the distinction a
   reader is actually scanning for; see the token block for why five and not
   seven. View, viewpoint, rendering, metadata and allocation are deliberately
   absent and inherit the neutral fallback. */
.tree-kind.requirement, .tree-kind.constraint, .tree-kind.concern { --kind: var(--kind-requirement); }
.tree-kind.part, .tree-kind.item, .tree-kind.occurrence, .tree-kind.package { --kind: var(--kind-structure); }
.tree-kind.port, .tree-kind.connection, .tree-kind.interface, .tree-kind.flow { --kind: var(--kind-interface); }
.tree-kind.action, .tree-kind.state, .tree-kind.calc, .tree-kind.case,
.tree-kind.verification, .tree-kind.analysis { --kind: var(--kind-behaviour); }
.tree-kind.attribute, .tree-kind.enum { --kind: var(--kind-value); }
.tree-label { font-weight: var(--w-body); }
.tree-type, .tree-value { font-variant-numeric: tabular-nums; font-size: var(--fs-label-m); color: var(--muted); }
.tree-value { margin-left: auto; }
.tree-new { font-size: var(--fs-label-xs);
  color: var(--warn); border: 1px solid currentColor; border-radius: var(--shape-xs); padding: 0 var(--s-1); }

/* The impact mark. Deliberately small and deliberately rare: it is here because
   something *else* is affected, never because something changed. A mark on
   every edit is noise, and everybody learns to ignore it. */

/* `.inspector` no longer paints a border, a radius or a ground — `.region`
   already does, and the two together were a white card inside a white card.
   What is left is the pane's own bottom breathing room. */
/* The inspector reads at the tool's density, not the site's.
 *
 * Everything around it is 12px — the tree, the toolbar, the panel heads — and
 * its own controls were 14, because a form control inherits the base rule
 * written for the marketing pages, where 14 is right for prose read at arm's
 * length. In a panel beside a 12px tree it is two sizes out and reads as a
 * different application bolted on.
 *
 * Scoped to `.ws` rather than fixing the base rule: the contact form and the
 * pricing page genuinely want the larger size, and shrinking every input in the
 * product to suit one panel is the wrong direction of fix.
 */
.ws .inspector { padding-bottom: var(--s-3); font-size: var(--fs-body-s); }

.ws .inspector summary,
.ws .inspector .muted,
.ws .inspector > p,
.ws .inspector .stack > p { font-size: var(--fs-label-m); }

/* The control size, as a variable rather than an override.
 *
 * The base control rule carries ten `:not()`s, so it has a specificity of
 * (0,10,1) and sits later in the file — an override for the workspace would
 * have had to out-specify it, which is an arms race that ends with `!important`
 * three changes from now. Reading a variable instead lets a scope *ask* for a
 * density, and the ask is one declaration.
 *
 * `.ws-cols` is the React client's workspace, and it never asked, so the one
 * field in it drew at the size meant for the contact form (2026-09-23). */
.ws, .ws-cols { --f-control: var(--f-body-s); }

/* The property sheet.
 *
 * Label left, control right, on a fixed column so the controls form a single
 * edge down the pane rather than each row finding its own. The label column is
 * fixed rather than `auto` for exactly that reason: `auto` makes the edge move
 * whenever a longer property name appears, which is the sort of thing that
 * reads as unfinished without anyone being able to say why. */
.sheet { padding: var(--s-2) var(--s-3) 0; }
/*
 * One dialog, one rhythm.
 *
 * Every group in a sheet is: an uppercase rule, then its content, then air. The
 * account dialog had one group following it and one inventing its own — a
 * heading at 19px next to a heading at 12px, no rule, no space — which is what
 * makes a dialog read as unfinished even when every individual part of it is
 * fine.
 */

/* A sheet row is a `.field`. The grid, the label column, the min-height and the
   read-only label colour all come from the primitive; nothing is redeclared
   here, which is the point of having one. */

.sheet-ro.mono, .sheet input.mono { font-family: var(--mono); font-size: var(--body-sm); }

/* Why a field cannot be edited, on the field. A greyed control with no reason
   reads as a bug rather than as a rule. */

/* The `.inspector .panel-head` override that lived here is gone: it set a
   different gap and padding from every other panel head, so the two panels
   beside each other had headers of different heights. `.panel-head` is one
   rule now. */
.inspector .panel-head .btn { margin-left: var(--s-2); }

.add-actions { display: flex; gap: var(--s-2); margin-top: var(--s-2); flex-wrap: wrap; }
.btn.danger-text { color: var(--bad);
  border-color: color-mix(in srgb, var(--bad) 35%, transparent); justify-self: start; }

.impact { margin: var(--s-3) var(--s-3) 0; padding: var(--s-2) var(--s-3);
  border-radius: var(--shape-xs);
  background: var(--tint); border: 1px solid color-mix(in srgb, var(--brand) 32%, transparent);
  display: flex; flex-wrap: wrap; gap: var(--s-1) var(--s-2); align-items: baseline; }

.impact .affected { font-size: var(--body-sm); cursor: pointer;
  border-bottom: 1px dashed var(--line-strong); }
.impact .affected:hover { color: var(--brand-ink); border-bottom-color: var(--brand); }

@media (max-width: 900px) { .editor { grid-template-columns: minmax(0, 1fr); } }

/* ------------------------------------------------ configuration control */

/* The release gate's one question, as counts rather than a percentage.
   "84% approved" is a number nobody can act on; "9 not approved" is a list
   somebody can work through. */
/* The maturity strip: how much of the model has reached the floor.
 *
 * **It was called `.gate`, and so is the sign-in screen.** Two completely
 * different things under one class, four hundred lines apart, and the later rule
 * won: `min-height: 62vh` turned a one-line status into a 558-pixel block. It
 * sat inside the product tree, so the tree and the project row were pushed to
 * the bottom of the panel and the status read as a stray label.
 *
 * Two separate reports — "why is there a random label in the tree" and "the
 * project is at the bottom of the tree" — one cause. Renamed rather than made
 * more specific: a collision that costs 558 pixels should be impossible to
 * reintroduce, not merely outranked. */
.maturity { display: flex; align-items: center; gap: var(--s-2); flex-wrap: wrap; font-size: var(--fs-label-m); }
.maturity:empty { display: none; }

/* The crossfade between counts and percentages. Keyed on the mode so switching
   back re-runs it, and skipped entirely under reduced motion — where an
   instant swap is the correct behaviour rather than a degraded one. */
@keyframes maturity-in { from { opacity: 0; } to { opacity: 1; } }

/* ------------------------------------------------------------------ forms */

.card-panel { border: 1px solid var(--line); border-radius: var(--r-lg); background: var(--surface-card);
  padding: var(--pad-card); margin: var(--pad-card) 0 0; }

.signin { max-width: 520px; margin: 40px auto 0; }
form { display: grid; gap: var(--s-15); margin-top: 14px; }
label { font: var(--f-label-l); letter-spacing: var(--k-label); margin-top: var(--s-2); }
/**
 * Every text-like control, by exclusion rather than by list.
 *
 * **This was an enumeration and that was the bug.** It read
 * `input[type=email], input[type=text], input[type=password], input[type=number],
 * input:not([type])` — so `type="date"`, `type="search"` and `type="file"` matched
 * nothing here and fell through to the browser's own chrome. A native date picker
 * on a dark theme renders as a white box, which is what Cameron photographed on
 * 2026-08-29 and read, correctly, as the form being unstyled.
 *
 * It was never a styling oversight. It was a list that had to be extended every
 * time somebody used an input type nobody had used before, and nothing said so.
 *
 * Inverted, the default is *styled*: a control added tomorrow inherits the
 * system, and the exclusions are the four that genuinely need different
 * treatment. `agents/scripts/component-gate.mjs` reads this very selector to
 * decide what is covered, so the two cannot drift apart.
 */
/* **`.cell` is the tenth exclusion, and it is the first one that is not a
   type.** A table cell somebody can type into is not a form field: the rule
   below gives every input a border, a ground and a radius, which is right for a
   sheet of six fields and wrong for sixty cells in a table -- it draws a grid of
   boxes over a grid that already exists.

   Named here rather than overridden at the call site because this selector is
   nine `:not()`s deep, so no utility class can reach it, and because
   `component-gate.mjs` reads this very selector to decide what is covered. An
   exception written anywhere else would be invisible to it.

   **`.bare` is the eleventh, for an input that is part of a control that draws
   the field itself**: the planner's search pill paints the ground, the edge and
   the focus, and the input inside it is only where the typing goes. Its own
   rule said `border: 0; background: none; padding: 0` and lost to this one, so
   the pill held a second, outlined field at the form's size (2026-09-23). Not
   `.cell`, which is a value in a table or a sheet: a name that says what the
   input is keeps the next reader from guessing. The gate reads only the type
   exclusions, so nothing yet checks that a `.bare` input has a rule of its
   own; `component.test.mjs` checks the planner's.

   **The width is a variable for the same reason the font is** (`--f-control`,
   above). A field in a toolbar wants its own width, not the row's, and no
   utility class can say so past this selector. `--w-control` lets the one
   control ask; everything else stays full width. The requirements filter was
   drawn across the whole panel until it could (2026-09-23). */
input:not([type=checkbox]):not([type=radio]):not([type=file]):not([type=range]):not([type=color]):not([type=submit]):not([type=button]):not([type=image]):not([type=hidden]):not(.cell):not(.bare),
textarea, select {
  width: var(--w-control, 100%); padding: var(--s-2) var(--s-3); border-radius: var(--shape-s);
  border: 1px solid var(--outline); background-color: var(--surface-container-low);
  color: var(--on-surface); font: var(--f-control, var(--f-body-l));
  transition: border-color var(--t-quick) var(--ease-out);
}
input:not(.cell):not(.bare):hover, textarea:hover, select:hover { border-color: var(--on-surface-variant); }
/* The accent, so a focused field reads as the one being typed into rather than
   as one more field with a slightly different edge. */
input:not(.cell):not(.bare):focus-visible, textarea:focus-visible, select:focus-visible { border-color: var(--primary); }
input::placeholder, textarea::placeholder { color: var(--on-surface-muted); }
/* A monospace here and nowhere else: this is raw source, where column alignment
   is information. */
textarea { font-family: var(--mono); font-size: var(--fs-body-s); resize: vertical; }
.field-row { display: grid; grid-template-columns: 1fr 1fr; gap: var(--s-4); }
.check { display: flex; align-items: center; gap: var(--s-25); font: var(--f-body-m);
  color: var(--on-surface-variant); margin-top: var(--s-4); cursor: pointer; }

/* ------------------------------------------------------------- the check box
 *
 * Drawn, not native. A native checkbox is the one control a browser paints
 * entirely itself: it takes the operating system's blue, its corner radius and
 * its tick, and nothing in this stylesheet reaches any of them. Beside a set of
 * controls built out of these tokens it read as the thing somebody forgot —
 * which is exactly what it was.
 *
 * `appearance: none` empties the box and the rest is ours: the tool's outline
 * when off, the primary when on, and a tick clipped from the box itself so
 * there is no glyph to load and nothing to go missing.
 *
 * The input keeps being an `<input type="checkbox">`, so it is still focusable,
 * still in the tab order, still announced as a checkbox and still toggled by
 * Space. Replacing it with a styled `<span>` is how that gets thrown away.
 *
 * # Global, and it was scoped to `.check`
 *
 * These rules read `.check input[type="checkbox"]`, so a checkbox in a plain
 * `<label>` got none of it. The workspace's view menu has four of those and ten
 * native radios, all rendering as operating-system chrome on a dark theme —
 * found by `agents/scripts/component-gate.mjs` on its first run, not by looking.
 *
 * **A control should be styled because of what it is, not because of where
 * somebody remembered to put it.** `.check` is layout now: a row, a gap, a
 * label beside a box. The box looks like the box everywhere. */
input[type="checkbox"], input[type="radio"] {
  appearance: none; -webkit-appearance: none;
  width: 18px; height: 18px; flex: none; margin: 0; padding: 0; cursor: pointer;
  border: 1.5px solid var(--outline); border-radius: var(--shape-xs);
  background: none; display: grid; place-items: center;
  transition: background var(--t-quick) var(--ease-out),
              border-color var(--t-quick) var(--ease-out);
}
input[type="checkbox"]::after, input[type="radio"]::after {
  content: ""; width: 10px; height: 10px; transform: scale(0);
  background: var(--on-primary);
  transition: transform var(--t-quick) var(--ease-spring);
  /* The tick is the shape of the mask, so it inherits the colour above and
     there is no second element to keep in step with the box. */
  clip-path: polygon(13% 48%, 6% 57%, 38% 88%, 94% 24%, 86% 16%, 37% 71%);
}
input[type="checkbox"]:hover, input[type="radio"]:hover { border-color: var(--on-surface-variant); }
input[type="checkbox"]:checked, input[type="radio"]:checked { background: var(--primary); border-color: var(--primary); }
input[type="checkbox"]:checked::after, input[type="radio"]:checked::after { transform: scale(1); }
input[type="checkbox"]:focus-visible, input[type="radio"]:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
/* **A radio is round, and that is not decoration.** Round means *one of these*
   and square means *any of these* — it is the only thing telling somebody which
   they are looking at before they click, so the shape carries meaning and does
   not follow the rounded-rectangle rule the rest of the tool does. Its mark is
   a dot rather than a tick for the same reason. */
/**
 * The file picker, whose button is the browser's and is reachable.
 *
 * `::file-selector-button` is the one part of a file input a stylesheet can
 * touch, and it is the part people click — so it gets the tool's button
 * treatment and the field around it gets the tool's field treatment. The
 * filename beside it stays the browser's, because there is no way to style it
 * and pretending otherwise would mean rebuilding the control in JavaScript and
 * losing the OS file dialog.
 *
 * One rule, and every file input in the product gets it — there is one today,
 * for a profile photograph, and it was rendering as raw chrome. */
input[type="file"] {
  width: 100%; padding: var(--s-2); border-radius: var(--shape-s);
  border: 1px dashed var(--outline); background-color: var(--surface-container-low);
  color: var(--on-surface-variant); font: var(--f-body-s); cursor: pointer;
}
input[type="file"]::file-selector-button {
  margin-right: var(--s-3); padding: var(--s-2) var(--s-3);
  border: 1px solid var(--outline); border-radius: var(--shape-xs);
  background: var(--surface-container-high); color: var(--on-surface);
  font: var(--f-label-l); cursor: pointer;
}
input[type="file"]::file-selector-button:hover { border-color: var(--on-surface-variant); }
input[type="file"]:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }

/**
 * The dropdown arrow, drawn rather than inherited.
 *
 * A `<select>` took the tool's border, background and type — and then the
 * operating system drew its own arrow inside it, in its own colour, at its own
 * size. Sixteen of them across the product, each looking almost right.
 *
 * `appearance: none` empties the control and the arrow becomes a background
 * image. The element stays a `<select>`: keyboard operable, announced as a
 * listbox, and still opening the platform's own picker on a phone, which is the
 * one thing a hand-built dropdown never gets right.
 *
 * # Where the arrow's colour lives
 *
 * In `:root`, with the rest of the palette, and redefined in the dark blocks.
 * The first draft baked `%23888f9a` into the data URI — a colour the theme
 * could never reach, so the arrow stayed the same grey in both. Worse, the
 * style gate could not see it: URL-encoded, `%23` is not `#`, so the check that
 * exists to stop exactly this counted zero. `urlColours` in the gate catches
 * that now.
 *
 * `currentColor` is not available inside a data URI, and masking the element
 * would cut a hole through its text rather than draw an arrow — so a token per
 * theme is the honest mechanism, and colours in a token block are where they
 * belong.
 */
select {
  appearance: none; -webkit-appearance: none;
  padding-right: var(--s-5);
  background-image: var(--chevron);
  background-repeat: no-repeat;
  background-position: right var(--s-3) center;
  background-size: 12px 8px;
}
/* A multiple or sized select is a list, not a dropdown; it has no arrow. */
select[multiple], select[size]:not([size="1"]) { background-image: none; padding-right: var(--s-3); }
/* A dropdown reads as a choice, not a box (Cameron, 2026-09-12): no outline,
   the value in the accent colour, and any label beside it in body text.
   Everywhere, so a dropdown looks the same on every screen. A list-style
   select keeps its box: it is a list, not a dropdown. */
select:not([multiple]):not([size]), .ws select, .details-scrim select {
  border-color: transparent; background-color: transparent;
  color: var(--interactive); width: auto; max-width: 100%;
  padding-left: var(--s-1);
}
select:not([multiple]):not([size]):hover { border-color: transparent; color: var(--interactive); }
select:not([multiple]):not([size]):focus-visible { border-color: transparent; outline: 2px solid var(--interactive); outline-offset: 2px; }
/* A label and its dropdown, side by side: "Group" beside "status". */
.pl-pick { display: inline-flex; align-items: center; gap: var(--s-1); font: var(--f-label-m); color: var(--on-surface); }
.pl-pick select { font: var(--f-label-m); }

input[type="radio"] { border-radius: var(--shape-full); }
input[type="radio"]::after { clip-path: circle(50% at 50% 50%); width: 8px; height: 8px; }
@media (prefers-reduced-motion: reduce) {
  input[type="checkbox"], input[type="radio"],
  input[type="checkbox"]::after, input[type="radio"]::after { transition: none; }
}
form .btn { justify-self: start; margin-top: var(--s-4); }
.form-status { font: var(--f-body-m); margin: var(--s-3) 0 0; min-height: 20px; }
.form-status.ok { color: var(--ok); }
.form-status.bad { color: var(--bad); }
.signed-in { display: flex; align-items: center; gap: var(--s-4); margin-bottom: var(--s-15); }
.signed-in p { margin: 0; color: var(--ink-2); font-size: var(--fs-body-m); }
.signed-in .btn { margin-left: auto; }
.report { border: 1px solid var(--line); border-radius: var(--r); padding: var(--s-3) var(--s-4); margin: 10px 0;
  background: var(--surface-2); }
.report .meta { font-size: var(--fs-label-m); color: var(--muted); margin-top: var(--s-1); }

.callout { border: 1px solid var(--warn); border-left: 4px solid var(--warn);
  border-radius: var(--r); background: var(--surface-card); padding: var(--s-4) var(--s-5); margin: 30px 0 0; }
.callout p { margin: 0 0 8px; }
.callout p:last-child { margin: 0; }

/* ------------------------------------------------------------------ narrow */

@media (max-width: 1000px) {
  .compare { grid-template-columns: 1fr; }
  .tiers, .cards, .principles { grid-template-columns: 1fr; }
  .hero { padding-top: var(--s-9); }
  .topbar nav { display: none; }
  .topbar nav.open { display: flex; position: absolute; top: 100%; left: 0; right: 0;
    flex-direction: column; padding: var(--s-25); background: var(--surface-card);
    border-bottom: 1px solid var(--line); }
  button.icon.menu { display: inline-flex; }
  .topbar { position: relative; }
  .milestone { grid-template-columns: 1fr; gap: var(--s-2); }
  .field-row { grid-template-columns: 1fr; }

  /* `.prose` carries a 128px top margin, which is the gap between chapters on a
     wide page and is right there. On a phone it is a third of the viewport, and
     on a page that *opens* with a `.prose` section -- contact, trust, about --
     it was 128px of nothing under the bar before the heading. The chapter gap
     still has to read as a chapter gap, so it scales rather than collapsing;
     the one at the top of the page has no chapter above it to separate from. */
  .prose { margin-top: var(--s-7); }
  main > .prose:first-child { margin-top: var(--s-5); }
}

/* ------------------------------------------------------------- the canvas */

/* The bridge where one line passes over another. Painted in the canvas colour
   with a wider stroke underneath, so it erases the line beneath it rather than
   drawing on top of it — which is what makes it read as a bridge. */

/* Requirements carried by this element. The count, and whether any of them is
   failing — the two things worth knowing before clicking. */

/* ------------------------------------------------------- the view settings */

/* --------------------------------------------------------------- toolbar */

/* Floats over the canvas rather than sitting above it, so the diagram keeps
   the full height. Photoshop's placement, and the reason is the same: the
   drawing is the subject and the tools are visiting. */

/* The context toolbar: full width, directly under the selection bar, always in
   the same place. Its contents change with what is selected; its position never
   does, so the eye learns where to look once. Affinity's context toolbar. */
.toolbar {
  display: flex; align-items: center; gap: var(--s-05); flex-wrap: wrap;
  padding: 4px var(--s-2); min-height: 38px;
  background: var(--surface-card); border-bottom: 1px solid var(--line);
}
.toolbar:empty { display: none; }

/* Quick actions, floating beside the selection on the canvas. Elevated because
   it sits over the model rather than beside it, and small because three
   non-destructive actions is all it is allowed to be. */

.tool {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--s-2);
  /* From the token, not a number. These were left on a hardcoded 30px and so
     sat out the density change entirely — which is the failure a scale exists
     to prevent, and it showed up as the only controls under 32px on a phone. */
  height: var(--ctl-h-sm); min-width: var(--ctl-h-sm); padding: 0 var(--s-2);
  border: 0; border-radius: var(--shape-xs); background: none; color: var(--ink-2);
  font: inherit; font-size: var(--body-sm); cursor: pointer;
  transition: background var(--t-press) var(--ease-out), color var(--t-press) var(--ease-out);
}
.tool:hover { background: var(--surface-3); color: var(--ink); }
.tool:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }
.tool[aria-expanded="true"] { background: var(--surface-3); color: var(--ink); }
.tool.danger:hover { background: color-mix(in srgb, var(--bad) 12%, var(--surface-card)); color: var(--bad); }
.tool svg { flex: none; }

/* The disclosure arrow that opens the kind list. Its own hit target, because
   creating the default thing and choosing what to create are different acts. */

/* The project row hosts the same menu the toolbar does, so it has to be a
   positioning context — without this the popup escapes to the nearest ancestor
   that is one and lands somewhere else on the page entirely. */

/* Opened from a control on the right edge, so it grows leftwards rather than
   off the side of the navigator. */

/* ------------------------------------------------------------- diagram tabs */

/* A tab is a SysML `view`: opening one opens a model element, closing one
   closes a window onto it and changes nothing. That is why closing needs no
   confirmation and renaming is an edit. */

/* -------------------------------------------------------- minimap and zoom */

/* Where you are in a diagram bigger than the window. The whole diagram, drawn
   small, with the part on screen picked out — so the answer to "what am I
   looking at" costs a glance rather than a scroll. */

/* The viewport. Shaded *outside* rather than in, so the part being looked at
   stays the clearest thing in the box. */

/* The zoom cluster, at half the weight it had.
 *
 * It is chrome floating over the drawing, and it was taking the space of a
 * control somebody uses constantly rather than one they reach for occasionally.
 * The buttons drop out of `.btn.sm` — which is the tool's density step for
 * things in a row with inputs — into their own smaller box, because this row
 * has no inputs to line up with. */

/* An input, not a label. Styled to read as text until it is used, because a
   boxed field here would be the loudest thing on the canvas. */
/* `.ws .zoomer .zoom-level`, and the specificity is the whole story.
 *
 * `.ws input[type="text"]` sets the tool's control height. An **attribute
 * selector counts the same as a class**, so that rule is (0,2,1) — it beat a
 * bare `.zoom-level` and then beat `.zoomer .zoom-level` too, and the field sat
 * at 38px inside a 20px row through two attempts to shrink it.
 *
 * The density step is right for a form row and wrong for chrome floating over a
 * canvas. That is a real difference rather than an exception being carved out,
 * which is why this is scoped to the cluster rather than made `!important`. */

/* Reset reverts the model to the worked example. That is what the demo is for
   and what the application must never offer — see the note in demo.js. */
.app #reset { display: none; }
.arch { cursor: grab; }

/* A connection is clickable, which needs a target wider than the line. */

/* ------------------------------------------------------ tab management */

/* The way back from a closed tab. Sits at the end of the strip and only
   appears when there is something to reopen, because a permanently disabled
   control is one people stop seeing. */

/* ------------------------------------------------------- the details popup */

.details-scrim {
  position: fixed; inset: 0; z-index: 60;
  display: grid; place-items: center; padding: var(--s-5);
  background: var(--scrim);
}

/* ------------------------------------------------------ layouts and docking */

.layouts {
  grid-column: 1 / -1;
  display: flex; align-items: center; gap: var(--s-05);
  padding: var(--s-1) var(--s-3); border-bottom: 1px solid var(--line);
  background: var(--surface-2);
}
.layouts .btn[aria-pressed="true"] {
  background: var(--surface-card); color: var(--ink); box-shadow: var(--e-1);
}

/* A panel tab is the handle. Dragging the tab moves the panel, which is what
   every editor does — the tab *is* the panel as far as a pointer is concerned. */

/* Where it would land. Shown on the region rather than between tabs, because
   the answer to "where does this go" is a region — the order inside one is not
   information anybody needs. */

/* --------------------------------------------------------------- the dialog */

.ask {
  width: min(420px, 100%); display: grid; gap: var(--s-3);
  padding: var(--s-4); border-radius: var(--r-lg);
  background: var(--surface-card); box-shadow: var(--e-3);
}
.ask h2 { margin: 0; font-size: var(--fs-body-l); font-weight: var(--w-bold); }
.ask label { font-size: var(--fs-label-m); color: var(--muted); }
.ask input, .ask textarea { width: 100%; }
/* Several fields at once needs room to read them together; one field does not.
   Both scroll rather than growing past the window, because a dialog taller than
   the viewport has an action row nobody can reach. */
.ask.wide { max-width: 520px; max-height: 82vh; overflow-y: auto; }
.ask.wide textarea { font: var(--f-body-m); font-family: var(--font); resize: vertical; }

/* A label a screen reader announces and nothing draws. Not `display: none`,
   which removes it from the accessibility tree as well and so announces
   nothing. */
.visually-hidden {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0;
}

/* --------------------------------------------- a table you can work in */

/* Controls above the thing they work on. */

/* A sortable header is a button, so it is reachable by keyboard and announces
   itself as pressable. The arrow is drawn from `aria-sort`, not from a class —
   one source for what the table is doing and what it says it is doing. */

/* An editable cell reads as editable on hover and on focus, and not before —
   a table where every cell is outlined is a form, and this is a table. */

/* Decomposition, shown as indentation. The depth is a custom property on the
   cell so the indent and the guide that explains it are the same measurement and
   cannot drift — the same arrangement the product tree uses. */

/* A parameter is the thing an element references, so it is set as code and
   carries its qualified name in the tooltip. */

/* -------------------------------------------------- the requirements audit */

.audit { border-bottom: 1px solid var(--outline-variant); padding: var(--s-2) var(--s-3); }

/* The names, which are what makes a count actionable — truncated rather than
   wrapped, so one long finding cannot push the rest off the screen. */

/* Shown on the row being pointed at or worked in. Always-visible delete buttons
   on every row turn a table into a minefield; ones that appear only on hover are
   unreachable by keyboard, so focus counts too. */

.btn.primary.danger { background: var(--bad); border-color: var(--bad); color: #fff; }

/* A value converted into the project's unit system.
 *
 * Beside the written one rather than instead of it: the model says what it
 * says, and a screen that silently replaced a supplier's `3 lb` with `1.36 kg`
 * would be showing something nobody wrote. Lighter, so the written value stays
 * the one being read. */
.converted {
  color: var(--muted); font-size: .92em; font-variant-numeric: tabular-nums;
  margin-left: .4em; white-space: nowrap;
}

/* The canvas, while something from the tree is over it. */

.tree-row[draggable="true"] { -webkit-user-drag: element; }

/* TBD / TBR / TBV on a total.
 *
 * Warn-coloured rather than neutral: a provisional number that reads as settled
 * is the failure this marks, so it has to be visible at a glance beside the
 * figure rather than discoverable on inspection. */
.maturity {
  font-size: var(--fs-label-xs); font-weight: var(--w-bold); letter-spacing: .07em;
  padding: 1px var(--s-15); border-radius: var(--shape-xs);
  color: var(--warn); background: color-mix(in srgb, var(--warn) 13%, transparent);
}

/* The requirements table.
 *
 * Eleven columns is a lot, and it is what a requirements review actually reads
 * — so it scrolls sideways in its own container rather than being trimmed to
 * fit. Trimming would mean deciding for a reviewer which of their columns
 * matters, and the answer differs by review. */

/* A column the model cannot answer yet says so once, quietly, rather than
   showing an empty cell that reads as "no rationale" when it means "not asked". */

/* ========================================================== the application shell
 *
 * The rail is the outer container (docs/workspace.md §6): its entries are places
 * a person goes, and choosing one replaces everything to its right.
 *
 * It is dark in both themes, deliberately. The original brief asked for the app
 * to carry cream, orange and black while the tool surface stayed "apple-esque
 * white/black" — and those two only coexist if the brand has somewhere of its
 * own to live. The rail is that place: black ground, orange for where you are,
 * and the workspace beside it left plain. A rail that inverted with the theme
 * would put the brand somewhere different depending on the time of day. */
.app {
  display: grid;
  grid-template-columns: var(--w-rail, 52px) minmax(0, 1fr);
  /* No top margin. The application is not an article on a page: a band of page
     background between the site bar and the app read as a gap where something
     had failed to load. */
  margin: 0;
  border: 1px solid var(--line);
  border-radius: var(--shape-m);
  overflow: hidden;
  /* **The page ground, not the card ground**, and the difference is the whole
     reason the application looked flat.
     
     This was `--surface-card`, which is the token a *card* uses -- so every
     card in the planner sat on a ground of exactly its own colour. Measured on
     a phone: 1.15:1 between a task card and the surface behind it, which is
     no elevation at all. Cards were doing nothing but adding a hairline.
     
     Found by measuring rather than reading, which is the only way this kind of
     thing gets found: both were correct token names, and the stylesheet reads
     perfectly well with the wrong one. */
  background: var(--surface);
}
.app > .ws { margin: 0; min-width: 0; }

.rail {
  display: flex; flex-direction: column; align-items: stretch;
  background: var(--rail-surface); color: var(--on-rail);
  padding: var(--s-2) 0 var(--s-2);
  gap: var(--s-05);
  min-height: 0;
}
.rail-group { display: flex; flex-direction: column; gap: var(--s-05); }
.rail-spacer { flex: 1 1 auto; min-height: var(--s-4); }

.rail-item, .rail-profile {
  position: relative;
  display: flex; flex-direction: column; align-items: center; gap: 0;
  padding: var(--s-3) var(--s-1); margin: 0;
  border: 0; background: none; color: inherit; cursor: pointer;
  font: inherit; font-size: var(--fs-label-xs); font-weight: var(--w-body); letter-spacing: 0;
  border-radius: 0;
  transition: background var(--t-quick) var(--ease-out), color var(--t-quick) var(--ease-out);
}
.rail-item svg, .rail-profile svg { width: 21px; height: 21px; flex: none; }
/* The label is for screen readers and the flyout, not for the rail. Hidden
   visually rather than removed: an icon-only button with no accessible name is
   one nobody using a screen reader can identify. */
.rail-label {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* MD3 state layers, over a dark ground rather than the page's. */
.rail-item:hover:not([aria-disabled="true"]),
.rail-profile:hover:not([aria-disabled="true"]) {
  background: rgb(255 255 255 / 8%); color: #fff;
}
.rail-item:active:not([aria-disabled="true"]) { background: rgb(255 255 255 / 12%); }
.rail-item:focus-visible, .rail-profile:focus-visible {
  outline: 2px solid var(--brand); outline-offset: -2px;
}

/* Where you are. The bar is the load-bearing signal — colour alone would fail
   anyone who cannot separate the orange from the grey. */
.rail-item.active { color: #fff; background: rgb(255 255 255 / 12%); }
.rail-item.active::before {
  content: ""; position: absolute; left: 0; top: 50%;
  transform: translateY(-50%);
  width: 3px; height: 26px; border-radius: 0 2px 2px 0; background: var(--brand);
}

/* Not built yet, and saying so rather than being a button that does nothing.
   Still focusable, so a keyboard user can read the reason instead of tabbing
   past a hole. */
/* 0.6, not MD3's 0.38 for a disabled control, and the deviation is deliberate.
   38% puts this text at 2.6:1 on the rail's ground. WCAG exempts disabled
   controls from contrast, so that would pass — but these entries exist *to be
   read*: they are how a visitor sees the shape of the product. Dimming them
   past legibility defeats the only reason they are on screen. 0.6 measures
   4.66:1, clears AA as body text, and still reads plainly as unavailable
   against the 18.4:1 of a live entry. */
.rail-item.unbuilt, .rail-profile[aria-disabled="true"] { opacity: .6; cursor: default; }
.rail-soon {
  position: absolute; top: 5px; right: 6px;
  width: 5px; height: 5px; border-radius: var(--shape-chrome);
  border: 1px solid currentColor;
}

.rail-profile { margin-top: var(--s-1); }
.rail-avatar {
  width: 24px; height: 24px; border-radius: var(--shape-chrome);
  display: grid; place-items: center;
  background: var(--brand); color: var(--on-brand);
  font-size: var(--fs-label-xs); font-weight: var(--w-bold); letter-spacing: .02em;
}

/* Narrow: the rail lies down above the workspace and scrolls, rather than
   taking a fifth of a phone's width for navigation. */
@media (max-width: 1100px) {
  .app { grid-template-columns: minmax(0, 1fr); grid-template-rows: auto minmax(0, 1fr); }
  .rail {
    flex-direction: row; align-items: center;
    padding: var(--s-1) var(--s-2); gap: var(--s-1);
    /* No `overflow-x: auto` any more, and its removal is the fix for something
       else. A scroll container clips anything positioned outside it, so the
       flyout — which drops *below* the rail on a narrow screen — was cut off at
       the rail's own edge and unusable.
       It was there when every entry carried a label. Icons-only, the seven
       entries need 330px of a 375px screen, so there is nothing to scroll. */
    justify-content: space-between;
  }
  .rail-group { flex-direction: row; gap: var(--s-1); }
  .rail-spacer { flex: 0 0 var(--s-3); min-height: 0; }
  .rail-item, .rail-profile { margin: 0; padding: var(--s-15) var(--s-25); flex: none; }
  .rail-item.active::before {
    left: 50%; top: auto; bottom: -4px; transform: translateX(-50%);
    width: 20px; height: 3px; border-radius: var(--shape-xs) 2px 0 0;
  }
  .rail-soon { top: 3px; right: 4px; }
}

/* ================================================================== the gate
 *
 * Four screens, one at a time. Centred and narrow because each asks exactly one
 * thing, and a full-width layout around a single question reads as a page that
 * failed to load the rest of itself. */
.gate {
  min-height: 62vh;
  display: grid; place-items: center;
  padding: var(--s-4) 0 48px;
  /* The stacking context the canvas sits in. Without it the canvas would need
     a z-index against the whole page rather than against this section. */
  position: relative; isolation: isolate;
}

/* ------------------------------------------------- the moving ground
 *
 * Barely above the background, and that is the specification rather than a
 * compromise: this is the only screen with nothing to do on it, and a
 * background you can read without looking for it competes with the one form
 * that matters. If the waves are legible at a glance they are too bright.
 *
 * `--wave-ground` is what the canvas measures itself against. It is declared
 * here rather than read from `background` because the section is transparent —
 * asking the element what colour it is would return `rgba(0,0,0,0)` and the
 * lines would compute against black in both themes. */
/* The host for the field, on the screens that have one.
 *
 * `position: relative` so the canvas's `inset: 0` is measured against this and
 * not the viewport, and `isolation` so a `z-index: -1` child stays behind the
 * panel without escaping to sit behind the page background — which is what it
 * does in a stacking context it did not create, and the symptom is a backdrop
 * that renders nowhere at all. */
.gate-field { position: relative; isolation: isolate; overflow: hidden; }

.wave-bg {
  position: absolute; inset: 0; z-index: -1;
  width: 100%; height: 100%;
  --wave-ground: var(--surface);
  /* Nothing here is interactive. Pointer events are listened for on the window,
     so the canvas must not intercept a click meant for the form behind it. */
  pointer-events: none;
}
/* The panel sits on the ground rather than floating over a pattern: the waves
   pass behind it, and its own surface is what makes the form readable. */

/* A link that is a button, for an action that is not the page's main one. */
.linkish {
  font: inherit; font-size: inherit; padding: 0; border: 0; background: none;
  color: var(--brand-ink); text-decoration: underline; cursor: pointer;
}

/* The project bar is styled once, under "the project bar, again" further down.
   It was styled here too, and every property this block set was set again
   there — so this was dead the day that one was written and read as if it were
   what you see. */

.sheet-scrim {
  position: fixed; inset: 0; z-index: 60;
  background: var(--scrim);
  display: grid; place-items: center; padding: var(--s-4);
}
.sheet-scrim[hidden] { display: none; }
.sheet-modal {
  width: 100%; max-width: 560px; max-height: 82vh; overflow: auto;
  background: var(--surface-card); border: 1px solid var(--line);
  border-radius: var(--r-lg); padding: var(--s-5); box-shadow: var(--e-3);
}
.sheet-modal.narrow { max-width: 400px; }
.sheet-modal .sheet-head { display: flex; align-items: center; gap: var(--s-2); margin-bottom: 14px; }
.sheet-modal .sheet-head h2 { margin: 0; font-size: var(--fs-title-l); }
.sheet-modal .spacer { flex: 1; }

/* ================================================================ the planner
 *
 * Four views over one list. Comfortable density like the rest of the tool, and
 * status carries a muted hue rather than a saturated one — a screen of forty
 * tasks should not look like a Christmas tree. */
.planner { display: flex; flex-direction: column; min-height: 0; height: 100%; position: relative; }

.pl-bar {
  display: flex; align-items: center; gap: var(--s-2); flex-wrap: wrap;
  padding: var(--s-2) var(--s-3); border-bottom: 1px solid var(--line); background: var(--surface-card);
  /* The ground a toolbar menu drops from: its button may be anywhere once the
     bar wraps, so the menu is placed against the bar instead. */
  position: relative;
}
/* Where you are, and how it is drawn — two questions, so two controls.
 *
 * They were one row of identical pills with a hairline in the middle, which
 * put `Load` (a section) and `Gantt` (a lens on a different section) side by
 * side as peers. The switcher is enclosed on its own ground so it reads as one
 * control with five positions; the lens beside it is bare, because a lens is a
 * modifier of whatever the switcher is pointing at and should not compete with
 * it for the eye. */
.pl-sections {
  display: flex; gap: var(--s-05); padding: var(--s-05);
  border-radius: var(--shape-chrome); background: var(--surface-container-high);
}
.pl-lens { display: flex; gap: var(--s-05); }
.pl-filters { display: flex; align-items: center; gap: var(--s-2); flex-wrap: wrap; }
/* On the wide layout there is nothing to disclose: the wrapper dissolves and
   the controls are flex items of the bar exactly as they were. The toggle that
   opens it on a phone is not drawn at all. */
/* The workspace's three columns, stacked when there is no room for three.
 *
 * `!important` because the widths are computed inline in `main.jsx` — they
 * depend on which panels are open — and an inline style wins otherwise. It is
 * the narrow case overriding the general one rather than a specificity fight,
 * which is the one shape this is the right tool for.
 *
 * `height: auto` matters as much as the columns do: the row is viewport-tall so
 * its columns can scroll inside it, and three of those stacked inside one
 * viewport height gives each a third of a screen. Let the page scroll instead.
 *
 * A minimum of 60vh on the middle pane, because a canvas squeezed to its
 * content is a canvas nobody can see anything in. */
@media (max-width: 900px) {
  .ws-cols {
    grid-template-columns: minmax(0, 1fr) !important;
    height: auto !important;
    grid-auto-rows: minmax(min-content, auto);
  }
  .ws-cols > :nth-child(2) { min-height: 60vh; }

  /* ------------------------------------------------ the inspector as a sheet
   *
   * There is room for one view, so the inspector stops being a column that
   * competes for width and becomes a thing that arrives when you ask a question
   * about an element. Tapping a node in the tree opens it; the scrim, the close
   * button and Escape all put it away.
   *
   * `translateY` rather than `display`, so it slides and so the panel keeps its
   * scroll position between openings — and so a screen reader is not handed a
   * panel that appears and disappears from the tree.
   *
   * `max-height: 85vh` leaves the tree visible behind it. A full-height sheet
   * is a page, and a page is the thing you get by pressing the tree's own
   * collapse buttons; this is deliberately the lighter gesture. */
  .ws-sheet {
    position: fixed; z-index: 60;
    inset: auto 0 0 0; max-height: 85vh;
    display: flex; flex-direction: column; overflow: auto;
    border-radius: var(--r-lg) var(--r-lg) var(--shape-none) var(--shape-none);
    box-shadow: var(--e-5);
    transform: translateY(101%);
    transition: transform var(--t-quick) var(--ease-out);
  }
  .ws-sheet[data-open="true"] { transform: translateY(0); }

  .ws-scrim {
    position: fixed; inset: 0; z-index: 59;
    background: var(--scrim);
    opacity: 0; pointer-events: none;
    transition: opacity var(--t-quick) var(--ease-out);
  }
  .ws-scrim[data-open="true"] { opacity: 1; pointer-events: auto; }

  .ws-sheet-close {
    margin-left: auto; border: 0; background: none; cursor: pointer;
    color: var(--on-surface-variant); font: var(--f-title-m);
    padding: 0 var(--s-2);
  }
  .ws-sheet-close:hover { color: var(--on-surface); }
}

/* A sheet is a narrow-layout idea. Above the breakpoint the panel is a column
   and these two are inert — the close button is not drawn at all, and the scrim
   is an empty div, which is why `main.jsx` needs no width test. */
@media (min-width: 901px) {
  .ws-scrim, .ws-sheet-close { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .ws-sheet, .ws-scrim { transition: none; }
}

/* ------------------------------------------------- the dependency map
 *
 * What comes before a task, the task, and what waits on it. Blocks and arrows
 * in flex rather than SVG: three columns become three stacked rows when there
 * is no width for three, which is what makes it readable on a phone — an SVG
 * would need measuring before it could be drawn and would not reflow at all.
 *
 * Each block is a real button, so the keyboard reaches it and the text stays
 * selectable. The edge carries the *kind* (FS, SS, FF, SF) and the lag, which
 * is the half a comma-separated list of names could never say: a
 * finish-to-start and a start-to-start constrain a plan completely differently.
 */
.pl-dep {
  display: grid; gap: var(--s-3); margin: var(--s-4) 0 0;
  grid-template-columns: 1fr auto 1fr; align-items: start;
}
.pl-dep-col { min-width: 0; display: grid; gap: var(--s-2); }
.pl-dep-col h4 {
  margin: 0; font: var(--f-label-m); color: var(--on-surface-muted);
  text-transform: uppercase; letter-spacing: .04em;
}
.pl-dep-col ul { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--s-2); }
.pl-dep-col li { display: flex; align-items: center; gap: var(--s-2); min-width: 0; }
.pl-dep-self { align-self: center; }

.pl-dep-block {
  display: flex; align-items: baseline; gap: var(--s-2); min-width: 0; width: 100%;
  padding: var(--s-2) var(--s-3); text-align: left;
  border: 1px solid var(--outline-variant); border-radius: var(--r);
  background: var(--surface-card); color: var(--on-surface);
  font: var(--f-body-s); cursor: pointer;
}
.pl-dep-block:hover { background: var(--surface-2); }
.pl-dep-block:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }
/* The one thing worth knowing about a neighbour when you are looking at why a
   task cannot move. */
.pl-dep-block.critical { border-color: var(--bad); }
.pl-dep-block.done .pl-dep-name { color: var(--on-surface-muted); text-decoration: line-through; }
.pl-dep-block.self { border-color: var(--primary); background: var(--surface-container-high); cursor: default; }
.pl-dep-ref { color: var(--on-surface-muted); font-variant-numeric: tabular-nums; flex: none; }
.pl-dep-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.pl-dep-edge { display: grid; justify-items: center; flex: none; color: var(--on-surface-muted); }
.pl-dep-kind { font: var(--f-label-s); letter-spacing: .04em; }
.pl-dep-lag { font: var(--f-label-s); font-variant-numeric: tabular-nums; }
.pl-dep-none { margin: 0; font: var(--f-body-s); color: var(--on-surface-muted); }

/* Stacked, and the middle column stops being a middle. Three columns of
   ellipsised names on a phone is three unreadable columns. */
@media (max-width: 700px) {
  .pl-dep { grid-template-columns: minmax(0, 1fr); }
  .pl-dep-self { align-self: stretch; }
}

/* ------------------------------------------------------- architecture blocks
 *
 * A colour per *role* rather than per kind. The model has a dozen kinds and a
 * reader distinguishes four or five things: is this structure, is it behaviour,
 * is it a requirement, is it an interface. `BLOCK_ROLES` in `diagram.mjs` does
 * the classification; this does the palette, and the split is deliberate — one
 * file deciding both is the one place a colour and a meaning drift apart
 * without either looking wrong on its own.
 *
 * Every block was `fill-surface-container` until 2026-09-10, so a requirement
 * and a part were drawn identically and you had to click one to find out which
 * it was.
 *
 * The fill is a wash rather than the label colour itself: at full strength
 * fourteen saturated boxes are a diagram nobody can read the edges on, and the
 * edges are the content. `color-mix` against the surface keeps the hue
 * identifiable and the contrast of the text inside it unchanged. */
.arch-block { stroke-width: 1; }
.arch-block-id {
  fill: var(--on-surface-muted); font: var(--f-label-s);
  font-variant-numeric: tabular-nums;
}
.arch-block-name { fill: var(--on-surface); font: var(--f-body-s); }

.arch-structure  { fill: color-mix(in srgb, var(--label-blue) 12%, var(--surface-container)); stroke: var(--label-blue); }
.arch-behaviour  { fill: color-mix(in srgb, var(--label-cyan) 12%, var(--surface-container)); stroke: var(--label-cyan); }
.arch-requirement{ fill: color-mix(in srgb, var(--label-amber) 14%, var(--surface-container)); stroke: var(--label-amber); }
.arch-interface  { fill: color-mix(in srgb, var(--label-teal) 12%, var(--surface-container)); stroke: var(--label-teal); }
.arch-flow       { fill: color-mix(in srgb, var(--label-green) 12%, var(--surface-container)); stroke: var(--label-green); }
.arch-constraint { fill: color-mix(in srgb, var(--label-rose) 12%, var(--surface-container)); stroke: var(--label-rose); }
.arch-value      { fill: color-mix(in srgb, var(--label-orange) 12%, var(--surface-container)); stroke: var(--label-orange); }
.arch-container  { fill: var(--surface-2); stroke: var(--outline-variant); }
.arch-other      { fill: var(--surface-container); stroke: var(--outline-variant); }

/* Selection and a touched element read through the colour rather than over it:
   the role is what the block *is* and stays legible while it is selected. */
.arch-block.is-selected { stroke: var(--on-surface); }

/* The way to an element’s other diagrams.
 *
 * Hidden until the block is hovered or selected. Always-on, every box carries a
 * mark and the canvas reads as a field of buttons; hover-only, a keyboard never
 * finds it — which is why selection shows it too. An SVG has no focus ring to
 * follow a pointer-less user around.
 *
 * The hit area is its own rect rather than the arrow’s stroke: a 1.5px path is
 * not something anybody lands on, least of all on a canvas that zooms. */
/* The resize corner, on the selected block only.
 *
 * On every block it would be fourteen grab targets on a canvas whose primary
 * gesture is dragging the whole thing, and every near miss resizes something.
 * Selection is a thing somebody did on purpose — and it is the same gesture
 * that opens the inspector, so the block you are working on is the one you can
 * size. */
.arch-grip {
  fill: var(--surface-container-highest); stroke: var(--on-surface);
  stroke-width: 1; cursor: nwse-resize;
}
.arch-grip:hover { fill: var(--on-surface); stroke: var(--on-surface); }

.arch-open { opacity: 0; cursor: pointer; }
[data-node]:hover .arch-open, .arch-open.is-shown, .arch-open:focus-visible { opacity: 1; }
.arch-open-hit { fill: var(--surface-container-high); stroke: var(--outline-variant); }
.arch-open:hover .arch-open-hit { fill: var(--surface-container-highest); stroke: var(--on-surface); }
.arch-open-mark { stroke: var(--on-surface-variant); stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; }
.arch-open:hover .arch-open-mark { stroke: var(--on-surface); }
/* How many others it is on, when it is more than one. */
.arch-open-count { fill: var(--on-surface-muted); font: var(--f-label-s); }
.arch-block.is-touched { stroke-dasharray: 4 3; }

/* Ports and the connections between them (task 0224).
 *
 * A port is drawn in the interface role's colours, because that is what it is,
 * and a connection in the interface hue so it cannot be read as a composition
 * edge — those are neutral ink, and the two answer different questions. The
 * name is small and muted beside the square, outside the box, where it does
 * not compete with the block's own name. */
.arch-connection { fill: none; stroke: var(--label-teal); stroke-width: 1.5; }
/* An import in the package view (task 0298): dashed, in the neutral ink a
   containment edge is drawn in but a grade darker, with a head at what is
   imported. Shape rather than hue says it is not containment; it is not an
   interface, so it does not take the teal. `6 3`, not the `4 3` a touched
   block is dashed with, so a dash on this canvas means one thing each. */
.arch-import { fill: none; stroke: var(--on-surface-variant); stroke-width: 1.5; stroke-dasharray: 6 3; }
.arch-import-head { fill: var(--on-surface-variant); }
/* A halo in the canvas ground, the bridges' colour, so a line passing behind a
   name does not strike through it. */
.arch-port-name {
  fill: var(--on-surface-variant); font: var(--f-label-s);
  paint-order: stroke; stroke: var(--surface-container-low); stroke-width: 3px; stroke-linejoin: round;
}
/* Focus is the orange ring on the square, as everywhere; the on-surface stroke
   is selection, and a port is never selected. The group's own outline would
   ring the name too. */
.arch-port:focus { outline: none; }
.arch-port:focus-visible .arch-interface { stroke: var(--primary); stroke-width: 2; }
.arch-port:hover .arch-interface { stroke-width: 2; }

/* The sequence view (task 0296).
 *
 * A lifeline's head is an architecture block in the structure role, because
 * it is a part and should read as the same thing it is on the diagram. The
 * line under it is dashed and a step fainter than a message: it is the axis
 * time runs down, not a relationship, but it is still needed to read the
 * chart, so it keeps `--outline`'s 3:1 rather than a divider's no guarantee.
 * A message is neutral ink — a colour would say it is a kind of thing, and
 * every message is the same kind. Its label is set like a port name, with the
 * same halo so the lifelines it crosses do not strike through it, and no
 * heavier than the names of the parts it connects. */
.seq-lifeline { stroke: var(--outline); stroke-width: 1; stroke-dasharray: 4 4; }
.seq-message { fill: none; stroke: var(--on-surface-variant); stroke-width: 1.5; }
.seq-head { fill: var(--on-surface-variant); }
.seq-type { fill: var(--on-surface-muted); font: var(--f-label-s); }
.seq-label {
  fill: var(--on-surface-variant); font: var(--f-body-s);
  paint-order: stroke; stroke: var(--surface-container-low); stroke-width: 3px; stroke-linejoin: round;
}

/* Priority as something you press, not a word you go elsewhere to change.
 *
 * `none` is offered like any other level: clearing a priority happens as often
 * as setting one, and a picker that can only raise it is one that drifts
 * upward until every task is urgent and the field means nothing. */
/* A task's span, with both ends pressable.
 *
 * An empty end reads "no start date" rather than being blank: a blank is not
 * something anybody thinks to click, which is the whole reason this row was
 * three read-only lines before.
 *
 * A derived date — one the plan computed rather than one anybody typed — is
 * muted and italic. It is true, and it is not a commitment, and drawing the two
 * identically is how a computed finish gets quoted as a promise. */
/* ------------------------------------------------------- the column header
 *
 * Drawn from the same list as the rows, with the same grid template set inline
 * on both — so a header with seven tracks over rows with six is not a state
 * this can reach. Three copies of a grid definition is what produced the note
 * further up about four items stacking in one area.
 *
 * The controls appear on hover and on focus. Always-on, six columns carry two
 * extra glyphs each and the header is busier than the data under it; only on
 * hover, a keyboard never finds them. */
.pl-head-row {
  display: grid; align-items: center; gap: var(--s-2);
  padding: 0 var(--s-2); min-height: 32px;
  border-bottom: 1px solid var(--line);
  background: var(--surface-2);
}
.pl-head-cell { display: flex; align-items: center; gap: var(--s-05); min-width: 0; }
.pl-head-cell.collapsed { overflow: hidden; }
.pl-head-name, .pl-head-sort {
  font: var(--f-label-m); color: var(--on-surface-muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.pl-head-sort { border: 0; background: none; padding: 0; cursor: pointer; }
.pl-head-sort:hover { color: var(--on-surface); }
.pl-head-sort.on { color: var(--on-surface); font-weight: var(--w-bold); }
.pl-head-act {
  border: 0; background: none; padding: 0 var(--s-05); cursor: pointer;
  color: var(--on-surface-muted); font: var(--f-label-m);
  opacity: 0;
}
.pl-head-cell:hover .pl-head-act, .pl-head-act:focus-visible { opacity: 1; }
.pl-head-act:hover { color: var(--on-surface); }

/* The column menu, at the end of each header row and on the toolbar. */
.pl-head-end { justify-content: flex-end; }
.pl-head-add {
  display: inline-flex; padding: var(--s-05); border: 0; border-radius: var(--shape-xs);
  background: none; color: var(--on-surface-muted); cursor: pointer;
}
.pl-head-add:hover, .pl-head-add[aria-expanded="true"] { color: var(--on-surface); background: var(--layer-hover); }

/* Menus that open under a toolbar button or a column header. */
.pl-menu-at, .pl-head-end { position: relative; display: inline-flex; align-items: center; }
.pl-pop {
  position: absolute; top: calc(100% + var(--s-1)); right: 0; z-index: 20;
  display: grid; gap: var(--s-05); min-width: max-content; padding: var(--s-2);
  border: thin solid var(--outline-variant); border-radius: var(--r);
  background: var(--surface-card); box-shadow: var(--shadow);
}
/* The right-click menu on a task row.

   `fixed`, not `absolute`: the coordinates come from `clientX/clientY`, which
   are viewport coordinates, and an absolutely positioned menu would land at
   that offset inside whichever ancestor happened to be positioned. It also has
   to sit above the peek panel, which is why the z-index is the higher one.

   `right: auto` because `.pl-pop` pins itself to the right of its anchor and
   this one has no anchor -- without it the left the handler computed is
   ignored. Sized in ch so the list keeps a readable measure. */
.pl-rowmenu {
  position: fixed;
  right: auto;
  z-index: 60;
  min-width: 22ch;
  max-height: 80vh;
  overflow-y: auto;
}
/* Whose menu this is. A menu opened by pointing at something should say what
   it is pointing at -- five statuses look identical on two different rows. */
.pl-rowmenu-of {
  margin: 0 0 var(--s-05);
  padding: 0 var(--s-1);
  font: var(--f-label-m);
  color: var(--on-surface);
  font-weight: var(--w-bold);
  max-width: 30ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.pl-rowmenu-head {
  margin: var(--s-1) 0 0;
  padding: 0 var(--s-1);
  font: var(--f-label-m);
  color: var(--on-surface-muted);
}
.pl-rowmenu .pl-rowmenu-bad { color: var(--bad); margin-top: var(--s-1); }

.pl-bar .pl-menu-at { position: static; }
.pl-bar .pl-pop { top: calc(100% - var(--s-2)); right: var(--s-3); }
.pl-pop .pl-filters { display: grid; gap: var(--s-2); justify-items: start; }
.pl-pop .pl-filters select { width: 100%; }
.pl-pop-opt {
  display: flex; align-items: center; gap: var(--s-2); width: 100%;
  padding: var(--s-1) var(--s-2); border: 0; border-radius: var(--shape-xs);
  background: none; color: var(--on-surface); font: var(--f-label-m); text-align: left; cursor: pointer;
}
.pl-pop-opt:hover { background: var(--layer-hover); }
.pl-pop-opt[aria-pressed]::before {
  content: ""; width: var(--s-3); height: var(--s-3); flex: none;
  border: thin solid var(--outline); border-radius: var(--shape-xs);
}
.pl-pop-opt[aria-pressed="true"]::before { background: var(--interactive); border-color: var(--interactive); }

/* The same indicator for a menu where only one option can be true.
   The column menu is a set of independent toggles and gets a square, which is
   what a checkbox is. The layout and priority menus are one-of-five, so the
   mark is round -- the difference a radio has always carried. One convention,
   two shapes, rather than the second convention this first had: a full-row
   `--selected` wash, which said the same thing in a way nothing else in the
   panel family says it. */
.pl-pop-opt[aria-checked]::before {
  content: ""; width: var(--s-3); height: var(--s-3); flex: none;
  border: thin solid var(--outline); border-radius: var(--shape-full);
}
.pl-pop-opt[aria-checked="true"]::before { background: var(--interactive); border-color: var(--interactive); }

/* Toolbar buttons with a mark beside their name. */
.pl-view, .pl-tool { gap: var(--s-1); }
.pl-tool { display: inline-flex; align-items: center; }
.pl-tool[aria-pressed="true"], .pl-tool[aria-expanded="true"] { background: var(--surface-container-high); }

/* A row at the foot of each group that starts a task in it. */
.pl-add-row {
  display: flex; align-items: center; gap: var(--s-2); width: 100%;
  padding: var(--s-2); border: 0; border-radius: var(--shape-xs); background: none;
  color: var(--on-surface-muted); font: var(--f-label-m); text-align: left; cursor: pointer;
}
.pl-add-row:hover { color: var(--on-surface); background: var(--layer-hover); }

/* One cell of a row, so an empty one still holds its column. */
.pl-cell { display: inline-flex; align-items: center; min-width: 0; }
.pl-talk {
  display: inline-flex; align-items: center; gap: var(--s-05);
  padding: var(--s-05); border: 0; border-radius: var(--shape-xs); background: none;
  color: var(--on-surface-muted); font: var(--f-label-s); cursor: pointer;
}
.pl-talk.has { color: var(--on-surface-variant); }
.pl-talk:hover { background: var(--layer-hover); color: var(--on-surface); }
.pl-group-name { display: inline-flex; align-items: center; gap: var(--s-1); }
.pl-group-name .pl-state { color: inherit; }
.pl-subcount .ui-mark { margin-left: var(--s-05); vertical-align: -1px; }

/* The header is a wide-layout idea. The phone draws each task as a card with
   its own areas, and a row of column names above a stack of cards would be
   labelling something that is not there. */
@media (max-width: 720px) {
  .pl-head-row { display: none; }
}

/* ------------------------------------------------ importing a schedule
 *
 * The preview before anything is written. Four counts, a note that this is a
 * one-time import, and the list of what did not come across — which starts
 * open when nothing could be imported, because then it is the whole answer. */
.pl-import { display: grid; gap: var(--s-3); }
.pl-import h3 { margin: 0; font: var(--f-title-m); color: var(--on-surface); }
.pl-import-from { margin: 0; font: var(--f-label-m); color: var(--on-surface-muted); }
.pl-import-counts {
  display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--s-2); margin: 0;
}
.pl-import-counts > div {
  display: grid; gap: var(--s-05); padding: var(--s-2) var(--s-3);
  border: 1px solid var(--outline-variant); border-radius: var(--r); background: var(--surface-2);
}
.pl-import-counts dt { font: var(--f-label-m); color: var(--on-surface-muted); }
.pl-import-counts dd { margin: 0; font: var(--f-title-m); font-variant-numeric: tabular-nums; color: var(--on-surface); }
.pl-import-note { margin: 0; font: var(--f-body-s); color: var(--on-surface-variant); }
.pl-import-note.warn { color: var(--warn); }
.pl-import-unread > summary { cursor: pointer; font: var(--f-label-l); color: var(--on-surface-variant); }
.pl-import-unread ul {
  list-style: none; margin: var(--s-2) 0 0; padding: 0; display: grid; gap: var(--s-1);
  max-height: 40vh; overflow: auto;
}
.pl-import-unread li { font: var(--f-body-s); color: var(--on-surface-variant); }
.pl-import-more { color: var(--on-surface-muted); }
.pl-import-actions { display: flex; justify-content: flex-end; gap: var(--s-2); }
@media (max-width: 560px) {
  .pl-import-counts { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

.pl-dates { display: inline-flex; flex-wrap: wrap; align-items: baseline; gap: var(--s-2); }
.pl-dates-arrow { color: var(--on-surface-muted); }
.pl-date {
  padding: var(--s-05) var(--s-2); border-radius: var(--shape-chrome);
  border: 1px solid transparent; background: none; cursor: pointer;
  color: var(--on-surface); font: var(--f-body-s);
  font-variant-numeric: tabular-nums;
}
.pl-date:hover { background: var(--surface-2); border-color: var(--outline-variant); }
.pl-date:focus-visible { outline: 2px solid var(--primary); outline-offset: 1px; }
.pl-date.derived { color: var(--on-surface-muted); font-style: italic; }
.pl-date.empty { color: var(--on-surface-muted); }

.pl-tool-count {
  display: inline-grid; place-items: center; min-width: 16px; height: 16px;
  padding: 0 var(--s-1); border-radius: var(--shape-chrome);
  background: var(--primary); color: var(--on-primary);
  font: var(--f-label-s); font-variant-numeric: tabular-nums;
}

/* ------------------------------------------------------ the toolbar's density
 *
 * The bar was built out of the tool's default control sizes, and a *filter* bar
 * is not a form: nothing in it is typed into for long, and it is read far more
 * often than it is used. At the shared sizes it was the heaviest thing on the
 * screen, above a list of tasks set in 13px.
 *
 * One height for everything in the row, one step below the tool's, and the
 * labels a step down with it. The row still stays on a single line because
 * every control shares `--pl-ctl-h` rather than each finding its own.
 *
 * # This is deliberately under the 44px target
 *
 * Every other target raised in this stylesheet stayed at 44 and the completion
 * tick keeps a 44px hit area over a 22px mark. These do not, and the difference
 * is what a mistake costs. Mis-tapping *Board* when you meant *List* changes
 * what you are looking at and one more tap changes it back; mis-tapping the
 * tick writes to a task and tells somebody the work is finished. The controls
 * here are also wide — the narrowest is 54px — so the error they invite is
 * sideways, and 36px of height is comfortable against a target that long.
 *
 * Cameron's call, 2026-08-27: less padding, smaller type, less between them. */
.pl-bar {
  --pl-ctl-h: 36px;
  --pl-font: var(--label-md);
}
.pl-bar .pill { height: var(--pl-ctl-h); padding: 0 var(--s-25); font-size: var(--pl-font); display: inline-flex; align-items: center; }
/* Inside the switcher the height comes off its own padding, so the control is
   one row tall rather than one row plus two paddings. */
.pl-sections .pill { height: calc(var(--pl-ctl-h) - var(--s-1)); }
/* The section switcher sits on its own filled ground, so the same rule applies
   one level in: the chosen section is darker ink, not a lighter box. */
.pl-sections .pill[aria-pressed="true"] { border-color: transparent; background: none; }
.pl-sections .pill:hover { background: var(--surface-container-highest); }
.pl-sections .pill[aria-pressed="true"]:hover { background: var(--surface-container-highest); }
.pl-bar select,
.pl-bar .btn.sm { height: var(--pl-ctl-h); font-size: var(--pl-font); }
.pl-bar select { padding: 0 var(--s-2); }
.pl-bar .btn.sm { padding: 0 var(--s-25); }

/* ---------------------------------------------------------- the search field
 *
 * It was a plain `<input type="search">` taking the base form rule, which is
 * written for a form: full width, a form's border, a form's padding. In a
 * toolbar that read as an unstyled box — the one control on the row with no
 * design applied to it.
 *
 * A filled container rather than an outlined one, because it sits *inside* a
 * surface rather than on a page, and an outline here would draw a second edge
 * beside the selects' own. The magnifier is inside the field and not a button:
 * there is nothing to press, the field searches as you type. */
.pl-search-box {
  display: inline-flex; align-items: center; gap: var(--s-2); flex: none;
  height: var(--pl-ctl-h); padding: 0 var(--s-3); width: 200px;
  border-radius: var(--shape-chrome); border: 1px solid transparent;
  background: var(--surface-container-high); color: var(--on-surface-variant);
  transition: border-color var(--t-quick) var(--ease-out),
              background var(--t-quick) var(--ease-out);
}
.pl-search-box:hover { background: var(--surface-container-highest); }
.pl-search-box:focus-within { border-color: var(--primary); background: var(--surface-container-high); }
.pl-search-icon { width: 15px; height: 15px; flex: none; }
/* `.bare`, so the base form rule leaves it alone (see the eleventh exclusion
   there) and this rule is the whole of it. `font: inherit` first, because an
   input does not inherit its font and nothing else now sets one. The pill's
   `:focus-within` edge is the focus, so the ring is not drawn twice. */
.pl-bar .pl-search {
  flex: 1; min-width: 0; width: auto; height: auto; align-self: stretch;
  border: 0; background: none; padding: 0;
  font: inherit; font-size: var(--pl-font); color: var(--on-surface);
}
.pl-bar .pl-search:focus-visible { outline: none; }
.pl-search::-webkit-search-cancel-button { filter: grayscale(1) opacity(.6); cursor: pointer; }
@media (prefers-reduced-motion: reduce) { .pl-search-box { transition: none; } }
/* `.pl-bar select`, not `.pl-filters select`. Group and Sort sit directly in
   the bar rather than in the filter group, so the narrower selector missed
   them and the base `select { width: 100% }` had them — two selects each as
   wide as the whole toolbar, which on a phone is two full screens of scroll
   before you reach Save. */
.pl-bar select { width: auto; min-width: 116px; }
.pl-bar .spacer { flex: 1; }
.pl-shown { font-size: var(--label-sm); color: var(--muted); font-variant-numeric: tabular-nums; }
.pl-month { display: inline-flex; align-items: center; gap: var(--s-1); font-variant-numeric: tabular-nums; }
.check.sm { margin: 0; font-size: var(--label-md); gap: var(--s-15); }

.pl-body { flex: 1; min-height: 0; overflow: auto; padding: var(--s-3); }

/* A save that was refused, said where the person is looking.
 *
 * Not a toast. A toast is for something that happened and is over, and this is
 * a state the document is *in*: the edit is still on screen and still unsaved,
 * and it stays that way until somebody undoes it or somebody with a seat does
 * it. Reverting their typing for them would be the worse trade — losing work to
 * explain a price is not a bargain anybody would take. */
.pl-refused {
  margin: 0; padding: var(--s-2) var(--s-3);
  border-left: 3px solid var(--warn);
  background: var(--surface-2); color: var(--on-surface);
  font: var(--f-label-m);
}
.pl-refused b { font-weight: var(--w-bold); }

/* ---------------------------------------------------------------- summary
 *
 * The four counts a planner is opened to answer, each one a filter you can tap.
 *
 * # Why the numbers are the loud part
 *
 * The count is the content; the word under it is the label. So the number gets
 * the display size and the label gets the small caps, rather than both sitting
 * at body size and leaving the eye to work out which is which. A strip of four
 * evenly-weighted pairs is a strip nobody reads at a glance, which defeats the
 * point of having it above the list.
 *
 * # Tone carries meaning and is not decoration
 *
 * Overdue is `bad`, today is `warn`, the coming week is `info`, unassigned is
 * plain -- because unassigned is a fact rather than a problem, and giving it a
 * colour would make four alarms out of two. At **zero** every tile goes quiet:
 * the number is still there, dimmed, because *nothing is overdue* is the one
 * piece of good news a planner ever gives and a tile that vanished would both
 * hide it and make the strip jump about as work is finished.
 *
 * The hue sits at 12% for the ground with full-strength text -- the same rule
 * the status pills follow, and the reason a screen of these stays readable. */
.pl-summary {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--s-1); padding: var(--s-2) var(--s-3) 0;
}
.pl-tile {
  display: inline-flex; align-items: baseline; gap: var(--s-1);
  padding: var(--s-05) var(--s-25);
  border: 1px solid var(--outline-variant); border-radius: var(--shape-chrome);
  background: var(--surface-card); color: var(--on-surface);
  cursor: pointer; text-align: left;
  transition: background var(--t-quick) var(--ease-out), border-color var(--t-quick) var(--ease-out);
}
.pl-tile b { font: var(--f-label-l); font-variant-numeric: tabular-nums; }
.pl-tile span {
  font: var(--f-label-m);
  color: var(--on-surface-variant);
}
.pl-tile:hover { background: var(--surface-3); }
.pl-tile:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

.pl-tile.bad  b { color: var(--bad); }
.pl-tile.warn b { color: var(--warn); }
.pl-tile.info b { color: var(--brand-ink); }
/* Quiet at zero. Still legible -- this is an answer, not an absence. */
.pl-tile.zero b { color: var(--on-surface-muted); }

/* The active tile. A border rather than a fill, so the number keeps its own
   colour and the selection does not fight the meaning it carries. */
.pl-tile.on { border-color: var(--outline); background: var(--selected); }
.pl-tile.on span { color: var(--brand-ink); }

@media (prefers-reduced-motion: reduce) { .pl-tile { transition: none; } }

/* ------------------------------------------------------------------- list */
.pl-group + .pl-group { margin-top: var(--s-4); }
.pl-group-head { display: flex; align-items: center; gap: var(--s-2); padding: var(--s-2) 0; }
.pl-group-name { font-weight: var(--w-bold); }
.pl-count {
  font-size: var(--label-sm); font-variant-numeric: tabular-nums; color: var(--muted);
  background: var(--surface-2); border-radius: var(--shape-chrome); padding: 1px var(--s-2);
}
.pl-fold { border: 0; background: none; color: var(--muted); cursor: pointer; padding: var(--s-05);
  display: inline-flex; transition: transform var(--t-quick) var(--ease-out); }
.pl-fold[aria-expanded="true"] { transform: rotate(90deg); }

.pl-rows { border: 1px solid var(--line); border-radius: var(--r); overflow: hidden; }
.pl-row {
  display: grid; align-items: center; gap: var(--s-2);
  grid-template-columns: 28px minmax(160px, 1fr) 40px 96px 74px minmax(90px, 160px) 110px;
  padding: 0 var(--s-2); min-height: 40px;
  border-bottom: 1px solid var(--line); cursor: pointer; background: var(--surface-card);
}
/* **`display: contents` and it is load-bearing.** On the wide layout the row is
   a seven-column grid and each cell is its own column; the wrapper dissolves so
   those columns still line up. On a phone the same wrapper becomes a flex row
   that wraps — see the narrow block. One element, two layouts, and no second
   copy of the markup to keep in step. */
.pl-meta { display: contents; }
/* The blurb is a child of `.pl-meta` and hidden here, which is the whole reason
   it can exist at all.
 *
 * `.pl-row` is a seven-column grid and the phone layout assigns its areas by
 * `nth-child`, so a new *direct* child of the row shifts every one of them --
 * the failure this file already carries a note about. Inside `.pl-meta` it
 * costs nothing: the wrapper is `display: contents` here, and a child with
 * `display: none` contributes no column at all.
 *
 * Hidden on the wide layout by intent as well as by necessity. These rows are
 * 40px and dense, and a description belongs on the layout where somebody is
 * reading one task at a time rather than scanning forty. */
.pl-blurb { display: none; }
.pl-row:last-child { border-bottom: 0; }
/* --------------------------------------------------- what was said and done
 *
 * One timeline, two kinds of row. A comment is somebody's words and gets the
 * weight of body text; a recorded change is context and is quieter — they are
 * the same story, and drawing them identically would make forty automatic lines
 * bury the three somebody wrote. */
.pl-thread { margin-top: var(--s-4); border-top: 1px solid var(--line); padding-top: var(--s-3); }
.pl-thread-head {
  margin: 0 0 var(--s-2); font-size: var(--label-md); letter-spacing: .05em;
 color: var(--muted);
}
.pl-said { list-style: none; margin: 0 0 var(--s-3); padding: 0; display: grid; gap: var(--s-2); }
.pl-say, .pl-did { display: grid; grid-template-columns: auto 1fr; gap: 0 var(--s-2); align-items: baseline; }
.pl-say-who { font-size: var(--body-sm); font-weight: var(--w-bold); color: var(--on-surface); }
.pl-say-when {
  font-size: var(--label-sm); color: var(--muted); font-variant-numeric: tabular-nums;
  justify-self: start;
}
.pl-say-body { grid-column: 1 / -1; margin: 0; font-size: var(--body-sm); color: var(--on-surface); }
/* A change the system recorded. Muted and one step in, so the eye finds the
   written lines first without the recorded ones being hidden. */
.pl-did-what {
  grid-column: 1 / -1; margin: 0; padding-left: var(--s-2);
  border-left: 2px solid var(--line); font-size: var(--body-sm); color: var(--on-surface-variant);
}
.pl-say-none { margin: 0 0 var(--s-3); font-size: var(--body-sm); color: var(--muted); }
.pl-say-new { display: flex; gap: var(--s-2); align-items: center; }
.pl-say-new input { flex: 1; min-width: 0; }
.pl-say-as { margin: var(--s-2) 0 0; font-size: var(--label-sm); color: var(--muted); }

/* ------------------------------------------------------ team-defined fields */
.pl-field-why, .pl-fields { margin: 0 0 var(--s-3); }
.pl-fields { list-style: none; padding: 0; display: grid; gap: var(--s-1); }
.pl-field-why { font-size: var(--body-sm); color: var(--on-surface-variant); }
.pl-field-row {
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 2fr) auto;
  gap: var(--s-2); align-items: center;
  padding: var(--s-15) var(--s-2); border-radius: var(--shape-xs);
  background: var(--surface-container);
}
.pl-field-row > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pl-field-name { font-size: var(--body-sm); color: var(--on-surface); }
.pl-field-type { font-size: var(--body-sm); color: var(--on-surface-variant); }
.pl-field-new { display: grid; grid-template-columns: 1fr 1fr 1fr auto; gap: var(--s-2); align-items: center; }
.pl-field-new .form-status { grid-column: 1 / -1; margin: 0; }

/* ------------------------------------------------------ ordering and nesting
 *
 * A subtask is indented and nothing else: no second row style, no smaller type.
 * It is a task — it has a status, an owner and a date — and drawing it as a
 * lesser kind of thing would be a claim the record does not make. */
.pl-row.pl-child .pl-title { padding-left: var(--s-5); }
.pl-row.pl-child .pl-title::before {
  content: ""; position: absolute; margin-left: calc(var(--s-4) * -1);
  width: var(--s-3); height: 1px; background: var(--line-strong); transform: translateY(9px);
}
.pl-row .pl-title { position: relative; }

/* How much of a parent is finished. Neutral until it is all of it, on the same
   rule the rest of the product follows: a count is not a severity. */
.pl-subcount {
  margin-left: var(--s-2); padding: 0 var(--s-1); border-radius: var(--shape-chrome);
  background: var(--surface-container-high); color: var(--on-surface-variant);
  font-size: var(--label-sm); font-variant-numeric: tabular-nums;
}
.pl-subcount.all { color: var(--ok); }

/* The row being carried, and the line showing where it would land.
 *
 * **Not `opacity`.** Fading the row dims the words on it too, and the
 * accessibility gate is right to refuse that: a pair measuring 4.7:1 becomes
 * 2.1:1 and no token check can see it. What needs to look different is the
 * row's *ground*, so the ground is what changes. */
.pl-dragging {
  background: var(--surface-container-high);
  outline: 1px dashed var(--outline); outline-offset: -1px;
}
.pl-drop-after { box-shadow: inset 0 -2px 0 var(--brand); }

/* A selected row.
 *
 * Marked on its ground and on its leading edge, for the same reason the dragged
 * row is: fading or tinting the text would put a state on the words themselves,
 * and the words are what somebody is checking before they act on twelve of them
 * at once. The edge is the part that survives a row scrolled half out of view. */
.pl-row.pl-picked {
  background: var(--surface-container-high);
  box-shadow: inset 3px 0 0 var(--brand);
}
.pl-row.pl-picked:hover { background: var(--surface-container-highest); }

.pl-row:hover { background: var(--surface-2); }
.pl-row:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }
/* `.pl-head` is gone with the column-header row it styled. The dead-selector
   check flagged it the moment the markup went, which is what that check is for
   — the alternative is a rule that outlives its element and reads, months
   later, like the reason something looks the way it does. */
/* Done work recedes but stays readable. A strike-through *and* muted *and*
   lighter is three signals for one state, and the row becomes hard to read at
   the moment somebody is checking whether it was the right one. */
.pl-row.done .pl-title { color: var(--on-surface-muted); font-weight: var(--w-body); }
/* **The title is the row.** Everything beside it is metadata and is muted; the
   reference makes that separation with weight as much as colour, and without it
   a list of forty rows has no landing point for the eye. */
.pl-title { font-size: var(--body-md); font-weight: var(--w-bold); color: var(--on-surface);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Holds a drawn mark rather than a character now, so it needs to be told
   where the baseline is — an inline SVG sits on it and a glyph sits in it. */
.pl-hasnotes { color: var(--muted); margin-left: var(--s-15);
  display: inline-flex; align-items: center; vertical-align: -2px; }

.pl-check {
/* **A frame around the status glyph, not a checkbox.** It was a 17px box with
   its own border that filled solid green when pressed — which, once the glyph
   moved inside it, painted over the tick it was supposed to be showing. The
   state is the glyph now; this only has to be pressable. */
  display: inline-grid; place-items: center; width: 20px; height: 20px;
  border: 0; background: none; cursor: pointer; padding: 0;
  border-radius: var(--shape-chrome);
}
.pl-check:hover { background: var(--layer-hover); }
.pl-check:focus-visible { outline: 2px solid var(--primary); outline-offset: 1px; }

/* Muted semantic, as everywhere: hue at 12% for the ground, full strength for
   the text, so meaning is legible without shouting. */
.pl-status {
  display: inline-block; font-size: var(--label-sm); font-weight: var(--w-bold);
  padding: var(--s-1) var(--s-25); border-radius: var(--shape-chrome); white-space: nowrap;
  color: var(--ink-2); background: var(--surface-2); border: 1px solid var(--line);
}
/* A group header is both, and `.pl-status` is written later.
   `.pl-group-name` asks for `inline-flex` and a gap so the status glyph stands
   off its word; `.pl-status` says `inline-block`, wins on order alone, and the
   gap it carries has nothing to act on -- so the tick sat against the D of
   Done. Cameron, 2026-09-13: *icon and text are too close.*
   Both classes in one selector, so it wins on specificity rather than on which
   of the two happens to be further down the file. */
.pl-group-name.pl-status { display: inline-flex; align-items: center; gap: var(--s-1); }

.pl-status.ok   { color: var(--ok);   background: color-mix(in srgb, var(--ok) 12%, transparent);   border-color: color-mix(in srgb, var(--ok) 30%, transparent); }
.pl-status.info { color: var(--interactive); background: color-mix(in srgb, var(--interactive) 12%, transparent); border-color: color-mix(in srgb, var(--interactive) 30%, transparent); }
.pl-status.warn { color: var(--warn); background: color-mix(in srgb, var(--warn) 12%, transparent); border-color: color-mix(in srgb, var(--warn) 32%, transparent); }
.pl-status.bad  { color: var(--bad);  background: color-mix(in srgb, var(--bad) 12%, transparent);  border-color: color-mix(in srgb, var(--bad) 30%, transparent); }

.pl-avatar {
  display: inline-grid; place-items: center; width: 26px; height: 26px; border-radius: var(--shape-full);
  background: var(--surface-3); color: var(--ink-2);
  font-size: var(--fs-label-xs); font-weight: var(--w-bold); letter-spacing: .02em;
}
.pl-avatar.none { color: var(--muted); background: var(--surface-2); border: 1px dashed var(--line-strong); }

.pl-due { font-size: var(--body-sm); font-variant-numeric: tabular-nums; color: var(--ink-2); }
.pl-due.none { color: var(--muted); display: inline-flex; }
.pl-due.late { color: var(--bad); font-weight: var(--w-bold); }
.pl-due.today { color: var(--warn); font-weight: var(--w-bold); }

.pl-priority { font-size: var(--label-md); color: var(--muted); text-transform: capitalize; }
.pl-priority.high, .pl-priority.urgent { color: var(--bad); font-weight: var(--w-bold); }

.pl-labels { display: flex; gap: var(--s-1); flex-wrap: wrap; }
.pl-label {
  font-size: var(--label-sm); padding: 1px var(--s-2); border-radius: var(--shape-xs);
  background: var(--surface-2); border: 1px solid var(--line); color: var(--ink-2); white-space: nowrap;
}
.pl-element {
  font: inherit; font-size: var(--label-sm);
  border: thin solid var(--line); border-radius: var(--shape-xs); background: var(--surface-2);
  color: var(--ink-2); cursor: pointer; padding: 0 var(--s-2); text-decoration: none;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%;
}

/* ------------------------------------------------------------------ board */
/* ------------------------------------------------------------------ board
 *
 * **A column has to look like a column.** It was a `--surface-2` ground on a
 * `--surface-2` panel — the same colour — so the columns were invisible and the
 * cards appeared to float in a field. Five headers in three different
 * treatments finished the job: two plain, three filled colour blocks.
 *
 * So: a column is a bordered surface a step down from the cards inside it,
 * every header is a dot, a name and a count, and the cards get room. Depth goes
 * the right way round — the card is the object, the column is where it sits. */
.pl-board {
  display: flex; gap: var(--s-3); align-items: flex-start;
  overflow-x: auto; padding-bottom: var(--s-3);
}
.pl-col {
  flex: 0 0 288px; display: flex; flex-direction: column;
  background: var(--surface-container-lowest); border: 1px solid var(--outline-variant);
  border-radius: var(--shape-m); padding: var(--s-3);
  max-height: 100%;
  transition: border-color var(--t-quick) var(--ease-out);
}
/* The column a drop would land in.
 *
 * A thicker edge in the brand rather than a fill: the cards inside sit on the
 * column's ground, so tinting it changes every card's background at the moment
 * somebody is looking at one of them. The border is drawn *inside* the existing
 * one -- same box, heavier line -- so nothing shifts by a pixel as the pointer
 * crosses between two columns a thumb apart. */
.pl-col.pl-drop-into {
  border-color: var(--brand);
  box-shadow: inset 0 0 0 1px var(--brand);
}
.pl-col-head {
  display: flex; align-items: center; gap: var(--s-2);
  margin: 0 0 var(--s-3); padding-bottom: var(--s-2);
  border-bottom: 1px solid var(--outline-variant);
}
.pl-col-name { font: var(--f-label-l); letter-spacing: var(--k-label); color: var(--on-surface); }
.pl-col-head .pl-count { margin-left: auto; }
/* The only colour in the header, and only two columns have any: blocked, which
   is work that has stopped, and done, which is what people scan for. */
.pl-col-dot { width: 7px; height: 7px; border-radius: var(--shape-full); background: var(--outline); flex: none; }
.pl-col[data-tone="bad"] .pl-col-dot { background: var(--bad); }
.pl-col[data-tone="ok"] .pl-col-dot { background: var(--ok); }
.pl-cards { display: grid; gap: var(--s-2); min-height: 48px; overflow-y: auto; }
.pl-card {
  background: var(--surface-container); border: 1px solid var(--outline-variant);
  border-radius: var(--shape-s);
  padding: var(--s-3); cursor: pointer;
  transition: border-color var(--t-quick) var(--ease-out), box-shadow var(--t-quick) var(--ease-out);
}
.pl-card:hover { border-color: var(--outline); box-shadow: var(--e-1); }
.pl-card:focus-visible { outline: 2px solid var(--primary); outline-offset: -1px; }
.pl-card-title { margin: 0 0 var(--s-2); font: var(--f-body-s); color: var(--on-surface); }
.pl-card footer { display: flex; align-items: center; gap: var(--s-2); margin-top: var(--s-25); }
.pl-empty-col { margin: 0; padding: var(--s-4) var(--s-3); text-align: center; color: var(--on-surface-muted); font: var(--f-label-m); }

/* ------------------------------------------------------------------ gantt */

/* The gates, named, above the bars they cut across. Its own strip rather than
   labels on the date scale: a milestone name is words and a tick is five
   characters, and interleaving them made both unreadable. */

/* The one place the chart's geometry is written down.
 *
 * Every row is its own grid, so an `auto` column is sized per row — which meant
 * the float labels differed in width from row to row, the `1fr` track differed
 * with them, and **two bars covering the same fortnight were drawn different
 * lengths**. A timeline whose scale changes per row is not a timeline.
 *
 * Fixed on both ends, and the overlay lines below are inset by exactly the
 * same amounts, so "today" and a gate land where the bars say they do. */

/* Three columns since the critical path landed: name, track, float. */

/* `.pl-gantt-bar`, not `.pl-bar`. It was `.pl-bar` for three weeks, which is
   also the toolbar's name a hundred and eighty lines above — same specificity,
   later in the file, so **the toolbar was `position: absolute` and 17px tall on
   every screen the product has ever been opened on**. Every control in it was
   overflowing an absolutely positioned strip the colour of a gantt bar.
   Nothing errored and nothing in the source looked wrong; it was found by
   measuring the toolbar's height and getting 17. See `duplicateSelectors` in
   `agents/scripts/style-gate.mjs`, which now refuses a second definition of a
   selector that sets `position` or `height`. */

/* One tone, and it is the one that means somebody has to do something.
 *
 * There were four — green for done, brand for in progress, amber for review,
 * red for blocked — so a finished fortnight and a stuck one were equally loud
 * and the chart read as decorated rather than informative. Same rule the board
 * columns, the status glyphs and the home tiles answer to: a kind of thing is
 * not a severity. Being *on the critical path* is drawn separately, below, and
 * that one is a severity. */

/* A squared edge says the work continues past the window rather than ending. */

/* Everything drawn at a date rather than on a row lives in here.
 *
 * It exists because `left: 40%` of a row is not 40% of the timeline: the today
 * line was positioned against the full width *including the 180px of task
 * names*, so it stood a fifth of the chart to the right of the date it claimed
 * — and the same arithmetic would have put every gate in the wrong place. The
 * lines carry a percentage from the layout, so the box they sit in has to be
 * the track and nothing else.
 *
 * `pointer-events: none` because it covers every bar, and a line across the
 * chart must not be the thing that swallows a click on a task. */

/* A gate is a date the whole chart is measured against, so it crosses every
   row rather than occupying one. Dashed, so it is never mistaken for a bar. */
/* `--muted`, not `--line-strong`. That token is a *surface* — one step off the
   panel it sits on — so the three gates were drawn, measured in the right
   places, and invisible on the screen. A line nobody can see is not a line. */

/* --------------------------------------------------------------- calendar */
.pl-calendar { display: grid; gap: var(--s-05); }
.pl-cal-names, .pl-week { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: var(--s-05); }
.pl-cal-names span {
  font-size: var(--label-sm);
  color: var(--muted); padding: 0 var(--s-1) var(--s-1);
}
.pl-day {
  min-height: 92px; padding: var(--s-1); border: 1px solid var(--line);
  border-radius: var(--shape-xs); background: var(--surface-card); display: flex; flex-direction: column; gap: var(--s-05);
}
/* A day outside the month recedes through its ground alone. `opacity: .62`
   took its date number to 2.33:1 — the worst measurement in the stylesheet,
   on a number whose entire purpose is to be read. The lighter ground says
   *not this month* without touching the digits. */
.pl-day.outside { background: var(--surface-2); }
.pl-day.today { border-color: var(--brand); box-shadow: inset 0 0 0 1px var(--brand); }
.pl-daynum { font-size: var(--label-md); color: var(--muted); font-variant-numeric: tabular-nums; }
.pl-pill {
  font: inherit; font-size: var(--label-sm); text-align: left; cursor: pointer;
  padding: var(--s-05) var(--s-15); border-radius: var(--shape-xs); border: 1px solid transparent;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  background: var(--surface-2); color: var(--ink-2);
}
.pl-pill.ok   { background: color-mix(in srgb, var(--ok) 12%, transparent);   color: var(--ok); }
.pl-pill.info { background: var(--surface-container); color: var(--on-surface-variant); }
.pl-pill.warn { background: color-mix(in srgb, var(--warn) 12%, transparent); color: var(--warn); }
.pl-pill.bad, .pl-pill.late { background: color-mix(in srgb, var(--bad) 12%, transparent); color: var(--bad); }
.pl-more { font-size: var(--label-sm); color: var(--muted); padding-left: var(--s-15); }

/* ---------------------------------------------------------- the task dialog
 *
 * **It was a 320px box pinned to the bottom-right corner** of a panel that is
 * taller than the window, so on a 1000px viewport it opened at y=1013 -- off
 * the bottom of the screen. Clicking a task appeared to do nothing, and when it
 * was visible it was a cramped two-column list in the corner of the eye.
 *
 * It is a dialog now, centred on the same scrim the compose sheet uses, wide
 * enough for the field list to breathe and tall enough to scroll. A task is the
 * thing this screen is about; opening one should put it in front of you. */
/* ------------------------------------------------- acting on many at once
 *
 * The bar sits between the summary strip and the rows, in the flow rather than
 * floating over them. A floating bar covers the bottom rows, which on a short
 * list is a good fraction of the selection somebody is about to change. */
.pl-picked-bar {
  display: flex; align-items: center; gap: var(--s-2);
  padding: var(--s-2) var(--s-3); margin-bottom: var(--s-2);
  border: 1px solid var(--line); border-radius: var(--r);
  background: var(--surface-container);
}
.pl-picked-bar .spacer { flex: 1; }
/* The same `select { width: 100% }` the toolbar was caught by, twelve hundred
   lines up. Two selects each as wide as the bar pushed Clear off the end and
   wrapped the count onto two lines. */
.pl-picked-bar select { width: auto; min-width: 116px; }
.pl-picked-count {
  font: var(--f-label-m); color: var(--on-surface); font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* ------------------------------------------------------------ the command box
 *
 * Near the top rather than centred. It grows downwards as somebody types, and a
 * box centred on the viewport would walk up the screen while they do — the one
 * thing a list you are arrowing through must not do. */
.pl-palette-scrim {
  position: fixed; inset: 0; z-index: 75;
  display: flex; justify-content: center; align-items: flex-start;
  padding: 10vh var(--s-4) var(--s-4);
  background: var(--scrim);
}
.pl-palette {
  width: min(560px, 100%);
  background: var(--surface-card); border: 1px solid var(--line);
  border-radius: var(--shape-l); box-shadow: var(--e-3); overflow: hidden;
}
.pl-palette-input {
  /* Square, and it stays square because the box clips it: the parent's
     `overflow: hidden` is what makes the top corners follow the card. A radius
     of its own here would be a hand-typed shape value for no visible gain. */
  width: 100%; border: 0; border-bottom: 1px solid var(--line);
  padding: var(--s-3) var(--s-4); font: var(--f-body-m);
  background: var(--surface-card); color: var(--on-surface);
}
.pl-palette-list {
  list-style: none; margin: 0; padding: var(--s-1);
  max-height: min(52vh, 420px); overflow: auto;
}
.pl-palette-row {
  display: grid; grid-template-columns: 68px minmax(0, 1fr) auto;
  gap: var(--s-2); align-items: center;
  padding: var(--s-15) var(--s-2); border-radius: var(--shape-xs); cursor: pointer;
}
/* The highlighted row is a ground change, not a tint on the label. Same rule as
   the selected row above, and for the same reason. */
.pl-palette-row.on { background: var(--surface-container-high); }
.pl-palette-kind {
  font: var(--f-label-s);
  color: var(--on-surface-muted);
}
.pl-palette-label {
  font: var(--f-body-s); color: var(--on-surface);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.pl-palette-hint { font: var(--f-label-s); color: var(--on-surface-muted); }
.pl-palette-none { margin: 0; padding: var(--s-4); font: var(--f-body-s); color: var(--on-surface-muted); }

.pl-peek-scrim {
  position: fixed; inset: 0; z-index: 70;
  display: flex; justify-content: center; align-items: center; padding: var(--s-4);
  background: var(--scrim);
}
.pl-peek-scrim[hidden] { display: none; }
.pl-peek {
  width: min(560px, 100%); max-height: min(80vh, 720px); overflow: auto;
  background: var(--surface-card); border: 1px solid var(--line); border-radius: var(--shape-l);
  padding: var(--s-5); box-shadow: var(--e-3);
}
.pl-peek-head { display: flex; align-items: center; gap: var(--s-2); margin-bottom: var(--s-3); }
.pl-peek-head .spacer { flex: 1; }
/* A button that is a mark rather than a caption. Square, so a row of them is a
   row of equal targets, and `flex: none` because a header that ran out of room
   used to solve it by squeezing these into two lines of text. */
.btn.icon-only {
  flex: none; display: inline-flex; align-items: center; justify-content: center;
  width: var(--ctl-h-sm); height: var(--ctl-h-sm); padding: 0;
}
.pl-peek-head .btn.icon-only { width: 28px; height: 28px; }
/* The reference, which the row shows and the dialog used not to. It is what
   somebody says out loud about a task. */
.pl-peek-ref { font: var(--f-label-m); color: var(--on-surface-muted); font-variant-numeric: tabular-nums; }
.pl-peek h3 { margin: 0 0 var(--s-3); font: var(--f-title-l); }
.pl-peek-grid {
  display: grid; grid-template-columns: 108px 1fr; gap: var(--s-15) var(--s-3);
  margin: 0 0 var(--s-3); font: var(--f-body-s);
  padding-top: var(--s-3); border-top: 1px solid var(--outline-variant);
}
.pl-peek-grid dt { color: var(--on-surface-muted); }
.pl-peek-grid dd { margin: 0; min-width: 0; color: var(--on-surface); }
.pl-peek-notes {
  margin: 0; padding-top: var(--s-3); border-top: 1px solid var(--outline-variant);
  font: var(--f-body-s); color: var(--on-surface-variant); white-space: pre-wrap;
}

/* ------------------------------------------------ the task panel's shapes
 *
 * Modal is the rule above. Full screen keeps a readable measure in the middle
 * of a wide monitor rather than running a line of notes across 2,000 pixels.
 * The sidebar lets clicks through to the list beside it, which is what makes
 * it a sidebar and not a narrow dialog. It is wider than the modal rather than
 * narrower: the modal is a look you dismiss, the sidebar is where you sit and
 * work while the list stays visible, and at 480px it was the tightest of the
 * three layouts holding exactly the same content. */
.pl-peek-scrim[data-layout="full"] { padding: 0; }
.pl-peek-scrim[data-layout="full"] .pl-peek {
  width: 100%; height: 100%; max-height: none; border: 0; border-radius: var(--shape-none);
}
.pl-peek-scrim[data-layout="full"] .pl-peek > * { max-width: 880px; margin-inline: auto; }
.pl-peek-scrim[data-layout="side"] {
  justify-content: flex-end; align-items: stretch; padding: 0;
  background: transparent; pointer-events: none;
}
.pl-peek-scrim[data-layout="side"] .pl-peek {
  pointer-events: auto; width: min(640px, 100%); height: 100%; max-height: none;
  border-width: 0 0 0 1px; border-radius: var(--shape-none);
}

/* Subtasks, checklist and files: one section shape, so three lists read as
   one panel and not three widgets. */
.pl-peek-section {
  display: grid; gap: var(--s-2); margin-top: var(--s-3); padding-top: var(--s-3);
  border-top: 1px solid var(--outline-variant);
}
.pl-peek-section h4 {
  display: flex; align-items: baseline; gap: var(--s-2); margin: 0;
  font: var(--f-label-l); color: var(--on-surface-variant);
}
.pl-peek-count { font: var(--f-label-s); font-variant-numeric: tabular-nums; color: var(--on-surface-muted); }
.pl-peek-list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--s-1); }
.pl-peek-list li { display: flex; align-items: center; gap: var(--s-2); min-width: 0; font: var(--f-body-s); color: var(--on-surface); }
.pl-peek-text { flex: 1; min-width: 0; overflow-wrap: anywhere; }
.pl-check-done .pl-peek-text { color: var(--on-surface-muted); text-decoration: line-through; }
.pl-peek-sub {
  flex: 1; min-width: 0; padding: 0; border: 0; background: none; cursor: pointer; text-align: left;
  font: inherit; color: var(--on-surface); overflow-wrap: anywhere;
}
.pl-peek-sub:hover { text-decoration: underline; }
.pl-peek-sub:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.pl-check-add { display: flex; gap: var(--s-2); }
.pl-check-add input { flex: 1; min-width: 0; }
.pl-attach-size { font: var(--f-label-s); font-variant-numeric: tabular-nums; color: var(--on-surface-muted); }
.pl-attach-note { margin: 0; font: var(--f-body-s); color: var(--warn); }
.pl-peek-hint { margin: 0; font: var(--f-label-s); color: var(--on-surface-muted); }
.pl-peek-actions {
  display: flex; flex-wrap: wrap; gap: var(--s-2); margin-top: var(--s-3); padding-top: var(--s-3);
  border-top: 1px solid var(--outline-variant);
}

/* The row that makes a subtask, in the list where the subtasks are.
   Quieter than a row that names one: it is an invitation, and it sits under
   real work that should stay the loudest thing in the section. It takes the
   row's shape so the list reads as one column rather than as a list with a
   button after it. */
.pl-peek-add-row {
  display: flex; align-items: center; gap: var(--s-2);
  width: 100%; padding: var(--s-1) var(--s-05);
  border: 0; background: none; cursor: pointer; text-align: left;
  font: var(--f-body-s); color: var(--on-surface-muted);
  border-radius: var(--shape-xs);
}
.pl-peek-add-row:hover { background: var(--surface-container); color: var(--on-surface); }
.pl-peek-add-row:focus-visible { outline: 2px solid var(--primary); outline-offset: 1px; }

/* The priority, as the level and its name, pressed to change.
   A button rather than five: four of the five were always the wrong answer and
   all five were as loud as the field they described. */
.pl-prio-open {
  display: inline-flex; align-items: center; gap: var(--s-1);
  padding: var(--s-05) var(--s-2); border-radius: var(--shape-chrome);
  /* Transparent rather than none, so the hover edge costs no reflow and the
     control needs no negative margin to sit flush. `.pl-date` in the row
     below solves the same problem the same way; the first version here padded
     and then cancelled the padding, which nothing else in this file does. */
  border: 1px solid transparent; background: none; cursor: pointer;
  font: inherit; color: inherit;
}
.pl-prio-open:hover { background: var(--surface-2); border-color: var(--outline-variant); }
.pl-prio-open:focus-visible { outline: 2px solid var(--primary); outline-offset: 1px; }

/* A menu opened from a control on the left of a panel pins left.
   `.pl-pop` pins to the *right* of its anchor, which is right for a toolbar
   whose controls sit at the right edge and wrong for the priority field, which
   sits at the left: the menu hung off the panel and the first characters of
   every option were cut off by its edge. */
.pl-prio-open + .pl-pop { right: auto; left: 0; }

/* The description. A field, not a paragraph that happens to be there.
   Only the two things that are its own: it grows, and it starts tall enough for
   a paragraph. Edge, ground, radius and focus come from the shared control
   baseline, which is what `#pl-check-new` two sections up takes untouched --
   two fields in one panel looking and focusing differently is the thing worth
   avoiding, and `--outline-variant` is a divider's role rather than a control
   edge's anyway. */
textarea.pl-peek-notes { resize: vertical; min-height: var(--s-9); font: var(--f-body-s); }
/* On a phone all three shapes are the same full-width sheet, so the choice is
   noise in a header that is already short of room. */
@media (max-width: 560px) {
  [data-menu="peek-layout"] { display: none; }
}

/* ------------------------------------------------------------- the inbox
 *
 * What an assistant proposed, one card each: the change as a sentence, the
 * reason, who and when, and two buttons. The reason is set apart because it is
 * what a person is actually judging. */
.ib { display: grid; gap: var(--s-4); max-width: 880px; }
.ib-head h2 { margin: 0; font: var(--f-title-l); color: var(--on-surface); }
.ib-lede { margin: var(--s-1) 0 0; font: var(--f-body-s); color: var(--on-surface-variant); }
.ib-note {
  margin: 0; padding: var(--s-2) var(--s-3); border-radius: var(--r);
  background: var(--warn-container); color: var(--on-warn-container); font: var(--f-body-s);
}
.ib-list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--s-3); }
.ib-card {
  display: grid; gap: var(--s-2); padding: var(--s-3) var(--s-4);
  border: 1px solid var(--outline-variant); border-radius: var(--r); background: var(--surface-2);
}
.ib-top { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--s-2); }
.ib-kind { font: var(--f-label-m); color: var(--primary); }
.ib-who { font: var(--f-label-s); color: var(--on-surface-muted); }
.ib-what { margin: 0; font: var(--f-body-s); color: var(--on-surface); }
.ib-why { margin: 0; font: var(--f-body-s); color: var(--on-surface-variant); }
.ib-label { margin-right: var(--s-1); font: var(--f-label-s); color: var(--on-surface-muted); }
.ib-acts { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2); }
.ib-elsewhere { font: var(--f-body-s); color: var(--on-surface-muted); }
.ib-empty { margin: 0; font: var(--f-body-s); color: var(--on-surface-muted); }
.ib-done > summary { cursor: pointer; font: var(--f-label-l); color: var(--on-surface-variant); }
.ib-done .ib-list { margin-top: var(--s-2); gap: var(--s-1); }
.ib-decided { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--s-2); font: var(--f-body-s); color: var(--on-surface-variant); }
.ib-verdict { font: var(--f-label-s); color: var(--on-surface-muted); }
.ib-decided[data-state="accepted"] .ib-verdict { color: var(--ok); }

.pl-detail-foot { display: flex; align-items: center; gap: var(--s-2); margin-top: var(--s-4); }
.pl-detail-foot .pl-meta { margin-left: auto; font-size: var(--label-sm); color: var(--muted); }

.pl-empty { padding: 48px var(--s-3); text-align: center; color: var(--muted); }
/* What this view cannot show. The difference between a view and a view you can
   trust: without it the timeline quietly disagrees with the list. */
.pl-undated { margin: var(--s-3) 0 0; font-size: var(--label-md); color: var(--muted); font-style: italic; }

/* ---------------------------------------------------------------- narrow
 *
 * A tablet still reads as a table, so this only drops the columns that stop
 * fitting. The phone layout below is a different shape entirely, because a
 * table with its columns removed is not a phone interface -- it is a desktop
 * interface with information missing, which is what this used to be. */
@media (max-width: 900px) {
  .pl-row { grid-template-columns: 26px minmax(120px, 1fr) 36px 88px; }
  .pl-row > :nth-child(n + 5) { display: none; }
  .pl-peek { width: 100%; max-height: 88vh; padding: var(--s-4); }
  .pl-peek-grid { grid-template-columns: 1fr; gap: 2px; }
  .pl-peek-grid dt { margin-top: var(--s-2); }
}

/* ============================================================ the planner,
 *                                                              on a phone
 *
 * Rewritten 2026-08-27 from screenshots of the real thing on a real handset,
 * beside ClickUp doing the same job. The old rule was the desktop table with
 * four of its seven columns hidden, and the result was exactly what that
 * describes: 13px type, 40px rows, three filter selects stacked into a wall,
 * and the empty-state sentence colliding with the search box.
 *
 * **A row becomes a card**, which is the whole idea. The markup does not change
 * -- the same seven children are re-placed into a two-line card by grid areas,
 * so there is one list renderer and not two, and nothing here can disagree with
 * what the desktop shows.
 *
 * What the borrowed pattern actually gets right, and why each part is here:
 *
 *   a real hit target      44px is the floor a thumb needs. The old row was 40
 *                          and the checkbox inside it 17, which is a coin-flip
 *                          at arm's length and the reason a list like this
 *                          feels broken rather than dense.
 *   separation by gap      cards with space between them, not hairlines. A 1px
 *                          rule at phone contrast is nearly invisible; space is
 *                          not, and it survives a bright room.
 *   one line of meta       due, priority and labels folded onto a second line
 *                          instead of into columns. The old rule *deleted*
 *                          them; a phone has room for all of it stacked.
 *   status as a pill       carried up to the group heading where it names the
 *                          column, which is where somebody looks first.
 *
 * What is deliberately not borrowed: the saturated status colours. A screen of
 * forty tasks should not look like a Christmas tree -- the hue stays at 12% for
 * the ground with full-strength text, which is the rule the rest of the tool
 * already follows and the reason it stays readable at this size. */
@media (max-width: 720px) {
  .pl-body { padding: var(--s-3) var(--s-3) calc(var(--s-5) * 2); }

  /* Four tiles in 375px is 84px each, which is not enough for "Next 7 days"
     and a two-digit number side by side. They scroll instead, sized so the
     fourth is visibly cut off -- an edge that is obviously mid-list is what
     tells somebody there is more, where four squeezed tiles just look wrong. */
  .pl-summary {
    flex-wrap: nowrap;
    overflow-x: auto; scrollbar-width: none;
    scroll-snap-type: x proximity; scroll-padding-inline: var(--s-3);
  }
  .pl-summary::-webkit-scrollbar { display: none; }
  .pl-tile { scroll-snap-align: start; flex: none; }

  /* The toolbar scrolls sideways rather than stacking. Three selects and a
     search box wrapped onto four lines pushed the list itself below the fold,
     so the first thing somebody saw on opening their tasks was the filters. */
  .pl-bar {
    flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none;
    gap: var(--s-2); padding: var(--s-2) var(--s-3);
    scroll-padding-inline: var(--s-3);
  }
  .pl-bar::-webkit-scrollbar { display: none; }
  .pl-bar > * { flex: none; }
  .pl-search-box { width: 168px; }
  .pl-filters { flex-wrap: nowrap; }
  .pl-bar select { min-width: 0; }
  /* It pushed everything off the end of a scrolling row, so the filters could
     not be reached at all. */
  .pl-bar .spacer { display: none; }

  /* Open means the bar stops being a single scrolling line, or the menu would
     be clipped by the strip it drops out of. */
  .pl-bar:has(.pl-pop) { flex-wrap: wrap; overflow: visible; }
  .pl-bar .pl-pop { left: var(--s-3); }

  /* A group heading reads as its status, and carries its own count. */
  .pl-group + .pl-group { margin-top: var(--s-5); }
  .pl-group-head { padding: 0 0 var(--s-2); gap: var(--s-2); }
  .pl-group-name {
    font: var(--f-label-l);
    padding: var(--s-15) var(--s-3); border-radius: var(--shape-chrome);
    background: var(--surface-3); color: var(--on-surface);
  }
  .pl-count { font: var(--f-label-m); background: none; padding: 0; }
  /* The fold's target is at the end of the file, with the rest of the planner's
     phone sizes. This padding said it gave 44px and gave 26; the arithmetic is
     in the note down there. */

  /* Cards, not rows. */
  /* No radius reset needed: with no border and nothing clipped, the inherited
     one has nothing to round. Setting it to zero would also be a hand-typed
     shape value, and the ratchet is right to count it. */
  .pl-rows { border: 0; overflow: visible; display: grid; gap: var(--s-2); }

  .pl-row {
    grid-template-columns: 28px 1fr auto;
    grid-template-areas:
      "check title  who"
      "check meta   who";
    align-items: start;
    gap: var(--s-1) var(--s-3);
    min-height: 68px; padding: var(--s-3);
    border: 1px solid var(--outline-variant); border-radius: var(--r-lg);
    background: var(--surface-card);
  }
  .pl-row:last-child { border-bottom: 1px solid var(--outline-variant); }

  .pl-row > :nth-child(1) { grid-area: check; align-self: center; }
  .pl-row > :nth-child(2) { grid-area: title; }
  .pl-row > :nth-child(3) { grid-area: who; align-self: center; }
  .pl-row > :nth-child(4) { grid-area: meta; }
  /* **One item in the `meta` cell, not four.** Four items assigned to one grid
     area do not flow onto a line, they stack — the due date, the priority and
     the labels rendered on top of each other, which is what Cameron
     photographed. A wrapper makes it a real flex row that wraps. */
  .pl-meta {
    grid-area: meta; display: flex; flex-wrap: wrap; align-items: center;
    gap: var(--s-1) var(--s-2); min-width: 0;
  }
  .pl-meta > .pl-cell:empty { display: none; }

  /* Two lines of what the task is for, then an ellipsis the clamp supplies --
     so it appears only when something was actually cut. `flex-basis: 100%` to
     take its own line above the due date and the labels rather than competing
     with them for the same one. */
  .pl-blurb {
    flex: 1 0 100%; min-width: 0; order: -1;
    font: var(--f-body-s); color: var(--on-surface-muted);
    display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
    overflow: hidden;
  }

  /* Two lines, then ellipsis. A task called "Put up posters of the Leyline
     launch event week plan across town" is not one line at this width, and
     truncating it to one loses the half that distinguishes it from its
     neighbours. */
  .pl-title {
    font: var(--f-body-l); white-space: normal;
    display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
    overflow: hidden;
  }

  /* ---------------------------------------- what the meta line stops saying
   *
   * Cameron, 2026-09-13: *the mobile optimization overcrowds a lot of the text
   * where things could be made into icons or scrollable.* Three lines of chips
   * under every title, and two of them were the card repeating itself.
   *
   * **The priority word goes, the bars stay.** Five slices filled to four is
   * the same fact as "High" and it is already beside it. The bar carries
   * `aria-label="Priority: High"`, so this is a visual duplicate only — nothing
   * is lost to a screen reader, which is the test for hiding a word rather than
   * the width being tight.
   *
   * **Scoped to the two places that state a priority, not to the class.** The
   * same glyph draws the picker's five options, and there the name is not a
   * duplicate of anything — it is the only thing telling one option from
   * another. Hidden globally, choosing a priority on a phone means choosing
   * between five bars filled to different levels with no word anywhere, which
   * is the failure `docs/design-language.md` rejects for kind colours: never
   * let the mark be the only carrier.
   *
   * **The status chip goes when the heading is the status.** Grouped by status,
   * every card under "To do" said "To do". Grouped by anything else it is the
   * only place the status appears and it stays — which is why this keys off
   * `data-grouped-by` rather than the width alone. The same argument applies to
   * priority under a priority grouping. */
  .pl-cell .pl-prio-name, .pl-prio-open .pl-prio-name { display: none; }
  .pl-list[data-grouped-by="status"] .pl-cell[data-col="status"],
  .pl-list[data-grouped-by="priority"] .pl-cell[data-col="priority"] { display: none; }

  .pl-check { width: 22px; height: 22px; }
  .pl-avatar { width: 32px; height: 32px; font: var(--f-label-m); }
  .pl-due, .pl-priority, .pl-label { font: var(--f-label-m); }
  .pl-due { font-variant-numeric: tabular-nums; }
  .pl-element { font: var(--f-label-m); }

  /* It sat under the toolbar and read as part of it, which is how a message
     about an empty list ends up looking like a broken filter bar. */
  .pl-empty { padding: calc(var(--s-5) * 2) var(--s-3); font: var(--f-body-m); }

  /* One column at a time, snapped, rather than four squeezed side by side. */
  .pl-board { gap: var(--s-3); scroll-snap-type: x mandatory; }
  .pl-board > * { scroll-snap-align: start; min-width: 78vw; }
}

/* ---------------------------------------------------------------- the shell
 *
 * The rail already collapses to a bar across the top on a phone, which is
 * right. What it did not do is grow its hit targets: a flyout entry was 36px
 * tall, and 36 is the size at which a thumb starts hitting the row above the
 * one it aimed at. 44 is the floor, and it is a floor rather than a preference
 * -- below it, a list stops being usable one-handed while walking, which is
 * where a planner is actually read. */
@media (max-width: 720px) {
  /* A hair over 100% put a subpixel of horizontal scroll on the page. */
  .app { max-width: 100%; }
}

/* A phone in landscape, and a small phone. The card holds; only the meta line
   needs to be allowed to wrap onto itself. */
@media (max-width: 400px) {
  .pl-row { padding: var(--s-2) var(--s-3); gap: var(--s-1) var(--s-2); }
  .pl-row > :nth-child(n + 4) { flex-wrap: wrap; }
  .pl-search-box { width: 132px; }
}
.dest { min-height: 0; display: flex; flex-direction: column; flex: 1; }
.dest[hidden] { display: none; }

/* ============================================================ rail flyouts
 *
 * Hovering a destination opens the sections under it. The flyout **navigates**
 * — it never switches how a thing is drawn, which is the tab row's job. Two
 * controls with two jobs, so neither is a second way to do the other's. */
.rail-slot { position: relative; }

.rail-flyout {
  position: absolute; left: 100%; top: -6px; z-index: 70;
  min-width: 208px; padding: var(--s-2);
  background: var(--surface-card); color: var(--ink);
  border: 1px solid var(--line); border-radius: var(--r);
  box-shadow: var(--e-3);
  /* Not `display: none`: a hidden-by-display panel cannot animate, and a panel
     that appears instantly under a moving pointer reads as a misclick. */
  opacity: 0; visibility: hidden; transform: translateX(-6px);
  transition: opacity var(--t-quick) var(--ease-out),
              transform var(--t-quick) var(--ease-out),
              visibility 0s linear var(--t-quick);
}
.rail-slot.has-flyout:hover > .rail-flyout,
.rail-slot.open > .rail-flyout {
  opacity: 1; visibility: visible; transform: translateX(0);
  transition-delay: 0s;
}
/* A bridge across the gap. Without it the pointer leaves the rail before it
   reaches the panel and the panel closes underneath it — the classic flyout
   failure, and the reason people stab at these menus. */
.rail-slot.has-flyout::after {
  content: ""; position: absolute; left: 100%; top: 0; width: 10px; height: 100%;
}

.rail-flyout-head {
  margin: 0 0 var(--s-1); padding: 0 var(--s-2);
  font-size: var(--label-sm);
  color: var(--muted); font-weight: var(--w-bold);
}
.rail-sub {
  display: flex; align-items: center; gap: var(--s-2); width: 100%;
  padding: 7px var(--s-2); border: 0; border-radius: var(--shape-xs);
  background: none; color: var(--ink-2); cursor: pointer;
  font: inherit; font-size: var(--body-md); text-align: left;
}
.rail-sub:hover:not([aria-disabled="true"]) { background: var(--surface-2); color: var(--ink); }
.rail-sub.active { background: var(--selected); color: var(--on-surface); font-weight: var(--w-bold); }
.rail-sub:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }
/* Legible on purpose, like the unbuilt rail entries: these exist to be read. */
.rail-sub.unbuilt { color: var(--muted); cursor: default; }
.rail-soon-text {
  margin-left: auto; font-size: var(--label-sm); letter-spacing: .04em;
 color: var(--muted);
  border: 1px solid var(--line); border-radius: var(--shape-chrome); padding: 0 var(--s-15);
}

/* Narrow screens: the rail lies down, so the flyout drops below rather than
   beside — at 62px of a phone there is no "beside". */
@media (max-width: 1100px) {
  /* Anchored to the rail, not to the slot. Hanging off the slot put the panel
     wherever that entry happened to sit — Design is fifth of seven, so a 208px
     panel started around x=200 and ran 33px off a 375px screen. Spanning the
     rail is both simpler and always on screen. */
  .rail { position: relative; }
  .rail-slot { position: static; }
  .rail-flyout {
    left: var(--s-2); right: var(--s-2); top: 100%; min-width: 0;
    transform: translateY(-6px);
  }
  .rail-slot.has-flyout:hover > .rail-flyout,
  .rail-slot.open > .rail-flyout { transform: translateY(0); }
  .rail-slot.has-flyout::after { left: 0; right: 0; top: 100%; width: auto; height: 10px; }
}

/* ============================================================== view tabs
 *
 * The row that used to be two groups of pills. Tabs read as places rather than
 * as toggles, which is what they now are: a section is a destination with its
 * own state, and the tab row switches how the open one is drawn. */

/* ------------------------------------------------------------- reveal-in
 *
 * The named way across. Sections keep their own state, so moving between them
 * is a deliberate act with a name — "show where this is used" — rather than
 * state quietly following the reader around. */

/* ------------------------------------------------- the project bar, again
 *
 * It read as "PROJECTMy first project(your account)" — a label, a name and a
 * chip with nothing between them, and buttons at the full 38px control height
 * jammed against the edge. The bar is chrome: it should be quieter and smaller
 * than the thing it sits above, not the loudest row on screen. */

#project-name {
  font-size: var(--title-sm); font-weight: var(--w-bold); color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* Maturity, save state and commit: everything that describes the *model*. It
   stands down where there is no model on screen — see `app.html`. Not
   `visibility: hidden`, which would keep the gap and leave the bar looking
   like something failed to load. */

/* No sizes here any more: `.btn.sm` is the tool's small step everywhere, so the
   bar simply uses it. A component redefining a shared size is how one class
   comes to mean two things. */

/* ------------------------------------------------------------------- home
 *
 * What a new account sees. The previous version made a project called "My
 * first project" and dropped somebody into an empty model — no name, no
 * description, and nothing saying what to do next. A blank canvas is not a
 * start; it is a question with no prompt. */
.home { padding: 40px var(--s-4); overflow: auto; }
.home h1 { margin: 0 0 6px; font-size: var(--fs-headline-l); }
.home .lede { margin: 0 0 28px; font-size: var(--fs-body-l); color: var(--ink-2); }

/* --------------------------------------------------- the setup dialog
 *
 * Naming a project is not setting one up. This asks the three things that
 * matter and explains what the third one does today, rather than offering a
 * field that quietly records nothing. */
.setup-hint { margin: 4px 0 0; font-size: var(--label-md); color: var(--muted); line-height: 1.5; }

.setup-actions { display: flex; align-items: center; gap: var(--s-2); margin-top: var(--s-4); }
.setup-actions .spacer { flex: 1; }

/* =============================================================== the schedule
 *
 * The answer first, the chart second. Somebody opening this wants to know
 * whether the next review is in trouble; a timeline alone makes them work it
 * out for themselves. */
.schedule { display: flex; flex-direction: column; gap: var(--s-4); }
.pl-sep { width: 1px; align-self: stretch; margin: var(--s-1) var(--s-05); background: var(--line); }

.sc-banner {
  display: flex; align-items: center; gap: var(--s-3); flex-wrap: wrap;
  padding: var(--s-3) var(--s-4); border-radius: var(--r);
  background: var(--surface-2); border: 1px solid var(--line);
  font-size: var(--body-md);
}
.sc-banner.warn {
  background: color-mix(in srgb, var(--warn) 10%, transparent);
  border-color: color-mix(in srgb, var(--warn) 30%, transparent);
}
.sc-banner.ok {
  background: color-mix(in srgb, var(--ok) 10%, transparent);
  border-color: color-mix(in srgb, var(--ok) 28%, transparent);
}
.sc-banner .btn { margin-left: auto; }

/* What the count in the banner is counting.
 *
 * A `<details>` rather than a button and a state flag: no event wiring, it
 * survives a re-render without the planner remembering anything, and it opens
 * on a phone, which has no hover to reveal a tooltip.
 *
 * `flex-basis: 100%` so it takes its own line inside the flex banner rather
 * than squeezing in beside the flags and the button. */
.sc-outstanding { flex: 1 0 100%; margin-top: var(--s-1); }
.sc-outstanding > summary { cursor: pointer; font: var(--f-label-l); color: var(--on-surface-variant); }
.sc-outstanding-what { color: var(--on-surface-muted); font-weight: var(--w-body); }
.sc-outstanding ul { list-style: none; margin: var(--s-2) 0 0; padding: 0; display: grid; gap: var(--s-1); }
.sc-outstanding li { display: flex; align-items: baseline; gap: var(--s-2); font: var(--f-body-s); }
.sc-outstanding-ref { color: var(--on-surface-muted); font-variant-numeric: tabular-nums; }
.sc-flag {
  font-size: var(--label-sm); font-weight: var(--w-bold); padding: var(--s-05) var(--s-2); border-radius: var(--shape-chrome);
  color: var(--bad); background: color-mix(in srgb, var(--bad) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--bad) 28%, transparent);
}

/* ---------------------------------------------------------------- timeline */
.sc-timeline { padding: 26px var(--s-4) 0; }
.sc-track {
  position: relative; height: 4px; border-radius: var(--shape-xs);
  background: var(--surface-3); margin-bottom: var(--s-2);
}
.sc-today { position: absolute; top: -8px; bottom: -8px; width: 2px; background: var(--brand); opacity: .6; }

.sc-mark {
  position: absolute; top: 50%; transform: translate(-50%, -50%);
  display: flex; flex-direction: column; align-items: center; gap: var(--s-1);
  border: 0; background: none; padding: 0; cursor: pointer; font: inherit;
}
.sc-dot {
  width: 13px; height: 13px; border-radius: var(--shape-full);
  background: var(--surface-card); border: 2px solid var(--line-strong);
  transition: transform var(--t-quick) var(--ease-out);
}
.sc-mark:hover .sc-dot { transform: scale(1.25); }
.sc-mark:focus-visible { outline: 2px solid var(--brand); outline-offset: 3px; border-radius: var(--shape-xs); }
.sc-mark.ok .sc-dot   { background: var(--ok); border-color: var(--ok); }
.sc-mark.warn .sc-dot { background: var(--warn); border-color: var(--warn); }
.sc-mark.bad .sc-dot  { background: var(--bad); border-color: var(--bad); }
.sc-mark-label {
  position: absolute; top: 16px; white-space: nowrap;
  font-size: var(--label-sm); font-weight: var(--w-bold); letter-spacing: .04em; color: var(--ink-2);
}
.sc-scale {
  display: flex; justify-content: space-between; margin-top: 22px;
  font-size: var(--label-sm); color: var(--muted); font-variant-numeric: tabular-nums;
}

/* ----------------------------------------------------------------- buckets */
.sc-buckets { display: grid; gap: var(--s-3); }
.sc-bucket { border: 1px solid var(--line); border-radius: var(--r); background: var(--surface-card); overflow: hidden; }
.sc-bucket.loose { background: var(--surface-2); }
.sc-bucket.loose.warn { border-style: dashed; }
.sc-bucket-head {
  display: flex; align-items: center; gap: var(--s-2); flex-wrap: wrap;
  padding: var(--s-3) var(--s-4); border-bottom: 1px solid var(--line);
}
.sc-bucket-head .spacer { flex: 1; }
.sc-kind {
  font-size: var(--label-sm); font-weight: var(--w-bold); letter-spacing: .05em;
  padding: var(--s-05) var(--s-2); border-radius: var(--shape-xs);
  background: var(--surface-container); color: var(--on-surface-variant);
}
.sc-name { font-size: var(--body-md); font-weight: var(--w-bold); }
.sc-date { font-size: var(--body-sm); color: var(--muted); font-variant-numeric: tabular-nums; }
.sc-progress {
  font-size: var(--label-md); font-variant-numeric: tabular-nums; color: var(--ink-2);
  background: var(--surface-2); border-radius: var(--shape-full); padding: var(--s-05) var(--s-25);
}
.sc-progress.none { color: var(--muted); background: none; font-style: italic; }

/* On whose word. Stated for every milestone that names a baseline, passed or
   not — "this review freezes refs/baselines/PDR-1" matters before as well. */
.sc-evidence {
  margin: 0; padding: var(--s-2) var(--s-4);
  font-size: var(--label-md); color: var(--muted);
  background: var(--surface-2); border-bottom: 1px solid var(--line);
}
.sc-evidence.ok { color: var(--ok); background: color-mix(in srgb, var(--ok) 8%, transparent); }
.sc-evidence code { font-size: var(--label-md); }

.sc-tasks { display: grid; }
.sc-task {
  display: flex; align-items: center; gap: var(--s-3); width: 100%; text-align: left;
  padding: 9px var(--s-4); border: 0; border-bottom: 1px solid var(--line);
  background: none; cursor: pointer; font: inherit; font-size: var(--body-sm); color: var(--ink);
}
.sc-task:last-child { border-bottom: 0; }
.sc-task:hover { background: var(--surface-2); }
.sc-task:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }
.sc-task.done .sc-task-title { color: var(--muted); text-decoration: line-through; }
.sc-task-dot {
  width: 8px; height: 8px; border-radius: var(--shape-full); flex: none;
  background: var(--line-strong);
}
.sc-task-dot.done { background: var(--ok); }
.sc-task-dot.late { background: var(--bad); }
.sc-task-title { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sc-task-due { font-size: var(--label-md); color: var(--muted); font-variant-numeric: tabular-nums; }
.sc-none { margin: 0; padding: var(--s-3) var(--s-4); font-size: var(--body-sm); color: var(--muted); font-style: italic; }

.sc-empty { padding: 56px var(--s-4); text-align: center; }
.sc-empty h2 { margin: 0 0 8px; font-size: var(--fs-headline-s); }
.sc-empty p { margin: 0 auto 20px; max-width: 46ch; color: var(--ink-2); }

@media (max-width: 900px) {
  .sc-timeline { padding-left: var(--s-3); padding-right: var(--s-3); }
  .sc-mark-label { font-size: var(--fs-label-xs); }
}

/* ================================================================= history
 *
 * The commit log. The product's argument is that it can say what changed, and
 * the hosted application could record history without showing it — so this is
 * the argument, on a screen. */

/* The tip of the branch, marked. Which commit you are standing on is the first
   thing anybody looks for in a log. */

/* No bespoke change styles here: the log draws the review panel's own card, so
   `.changelist` and `.change` already cover it. A second set would be a second
   look for one thing. */

/* The commit dialog: what changed, said before the message is written. */
#commit-what { margin: 0 0 var(--s-3); font-size: var(--body-sm); }

/* Built, real, and not reachable from here — the public demo has no account, so
   Home and Planner exist and cannot be shown. Deliberately distinct from
   `.unbuilt`: "not made yet" and "sign in to use it" are different answers, and
   showing them the same way is how a control ends up lying about which it is.
   Legible rather than dimmed to nothing, for the same reason: it is an
   invitation, not a disabled control. */
.rail-item.elsewhere { opacity: .72; }
.rail-item.elsewhere:hover { opacity: 1; background: rgb(238 106 33 / 14%); }
.rail-item.elsewhere .rail-soon { display: none; }

/* ================================================================ modules
 *
 * The two states the module host owns. Everything else a module looks like
 * ships with the module — `modules/<id>/ui/` — because a stylesheet held here
 * for code that can be deleted is a stylesheet that outlives it.
 *
 * A module that fails to load says so in place and leaves the rest of the
 * application working. That is the same claim severability makes at build time,
 * and it would be odd to hold it there and drop it at runtime. */
.module-loading,
/* `--g5` was a typo — the grid stops at g4. It resolved to nothing, so this had
   no vertical padding at all, which is why a module that failed to load sat
   flush against the panel edge. */
.module-failed { margin: 0; padding: var(--s-5) var(--s-4); font-size: var(--body-sm); }
.module-loading { color: var(--muted); }
.module-failed { color: var(--bad); }

/* ==================================================================== ask
 *
 * The assistant, where there is one. Beneath the comments, in the same column:
 * both are things you do *about the thing you have selected*, and neither is a
 * place you go.
 *
 * It is often absent — hosted always, because a browser cannot start a process
 * — so nothing here reserves space or draws a frame around an empty box. */
.ask {
  border-top: 1px solid var(--line);
  padding: var(--s-3);
  display: grid;
  gap: var(--s-2);
}
.ask[hidden] { display: none; }

/* Not an error: the product works without an assistant by design, so this reads
   as information rather than as something being wrong. */

/* The *question form's* actions. `.ask-actions` is the confirm dialog's, which
   right-aligns its two buttons; this row has a note, a spacer and one button,
   and was silently taking that dialog's `justify-content` and `margin-top`. */

/* What it was answered *about*, shown rather than tucked away. An answer built
   from a truncated context is a different kind of answer, and a reader who is
   not told cannot know to check. */

/* ========================================================= the app, as an app
 *
 * *"I don't like that the height of the entire container changes depending on
 * the contents inside it… when we're in this view on the desktop we shouldn't
 * even really have the header navigation or the footer. This is the actual app
 * at that point; those things make it feel like a website."*
 *
 * Right, and the two halves are one fix. The chrome is gone from the page
 * itself (`chrome: false` in pages.json), and what is left has to fill the
 * window rather than grow with whatever is inside it — a workspace whose height
 * follows its contents is a document, and every panel inside it that wants to
 * scroll independently cannot, because there is nothing fixed to scroll within. */
main.full {
  max-width: none;
  margin: 0;
  padding: 0;
  /* `100dvh`, not `100vh`: on a phone the browser's own bars are counted out of
     `dvh` and not out of `vh`, so `vh` puts the bottom of the application
     underneath the address bar. */
  height: 100dvh;
  min-height: 0;
}
main.full > .app {
  height: 100%;
  min-height: 0;
  border: 0;
  border-radius: 0;
}
/* Every layer between the window and the panels has to pass the height down, or
   the constraint stops at the first one that does not. */

main.full .ws { min-height: 0; height: 100%; display: flex; flex-direction: column; }
main.full .dest { min-height: 0; }
main.full .dest:not([hidden]) { display: flex; flex-direction: column; flex: 1 1 auto; }

/* **The gap under the project bar.**
 *
 * `.ws { margin: 40px 0 0 }` — the workspace was written for the demo page,
 * where it sits below a paragraph of prose and needs air. Inside the
 * application it sits below a toolbar and needs none, and 40px of page
 * background between two bars reads as something having failed to load.
 *
 * Scoped rather than changed: the demo still wants its margin. */
.app .ws { margin: 0; }

/* **The rail, 20% narrower, with the icons still filling it.**
 *
 * The width is a token so the grid column and the flyout's offset cannot
 * disagree about it — they did, and the flyout overlapped the rail by the
 * difference. */
.app { --w-rail: 42px; }
/* Full width, so the hit target and the hover state are the rail rather than a
   22px square floating in it. The button was shrink-wrapping to its icon, which
   left most of the bar unclickable and made the active marker look detached.
   
   **Scoped to the wide layout, and it was not.** "Full width" means 42px in a
   vertical rail and the entire bar in a horizontal one -- so on a phone this
   made the profile button 359px wide, pushing 112px of it off the screen on
   every single destination. It sat *after* the narrow block with the same
   specificity, so the mobile rule lost the cascade and the comment above it
   described a layout that was no longer the one being styled.
   
   The kind of thing that survives review: both rules are correct, they are 1000
   lines apart, and nothing about either one looks wrong on its own. */
@media (min-width: 1101px) {
  .rail-item, .rail-profile { padding: var(--s-25) 0; width: 100%; }
}
.rail-item svg, .rail-profile svg { width: 22px; height: 22px; }

/* **"PROJECT" and "your account" are redundant.**
 *
 * The name is in the largest type in the bar and is plainly a project; the chip
 * said "your account" beside a bar that only ever appears once you are in your
 * account. Two labels that tell you what you already know, in the row that is
 * meant to be the quietest on screen. The storage chip stays where it says
 * something you cannot otherwise know — *this computer*, *this browser*. */
#storage-where[data-kind="supabase"] { display: none; }

/* ------------------------------------------------------- the project row
 *
 * *"the 'new element' add button is above the product tree, which doesn't make
 * sense; it should be a small '+' inside the product tree on the right side of
 * the first element, which should always be a 'project' element with the
 * project name."*
 *
 * Reads as the tree's first row and is deliberately not one: the tree is
 * virtualised and every row in it is an element you can select, inspect and
 * reveal elsewhere. A synthetic row inside it would look selectable and name
 * nothing in the model. */
/* The tree's head. It is a `.panel-head` in the markup now and keeps only
   the type: the flex, the gap, the padding and the rule underneath used to
   be redeclared here at a different padding, which is why this head was
   27px tall and the inspector's beside it was 49px. */

/* Quiet until the row is under the pointer. A `+` at full strength beside a
   project name reads as the most important thing in the panel, which it is not
   — but it must never be invisible to somebody who does not hover, so it is
   dimmed rather than hidden. */

/* Where you are, at the head of the tab row.
 *
 * A label rather than a button. It used to be one of several buttons that also
 * changed *where* you were, which put "which section" and "how it is drawn" in
 * one row of identical controls — and where you are is the rail's job. */

/* ----------------------------------------------------------- save status
 *
 * Three states, one glyph each, and only one drawn at a time. A status rather
 * than a button: saving is automatic now, and a control that does what happens
 * anyway invites people to press it and wonder whether it worked.
 *
 * It replaced "uncommitted" in a warning colour. That label was accurate — the
 * work really was only in the tab — and the fix was to make it untrue rather
 * than to reword it. */

/* Every glyph is in the markup and all but one is hidden, so switching state
   cannot fetch or lay out anything — the indicator must never be the reason a
   frame is late. */
.savestate[data-state="saving"] .save-dot,

/* The one thing worth animating here: work in flight. Everything else is a
   state that has already settled. */
@keyframes save-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .35; } }

/* Narrow: the glyph carries it. The word is the first thing to go in a bar that
   also holds the project name and three buttons. */

/* A setting: a checkbox, a claim, and the reason it is the default it is. */
.setting { display: flex; align-items: flex-start; gap: var(--s-3); cursor: pointer; }
.setting input { margin-top: 3px; flex: none; }
.setting b { display: block; font: var(--f-title-s); }
/* The explanation, which must not read as part of the label. It used to sit
   directly under it at nearly the same size and weight, so the two ran together
   into one paragraph of bold text. */
.setting em { display: block; margin-top: var(--s-05); font: var(--f-body-s); font-style: normal;
  color: var(--on-surface-variant); }

/* ==================================================== the workspace, tightened
 *
 * Region names, so there is one vocabulary for talking about this screen. They
 * are `docs/workspace.md` §3's and they are in the markup as `data-region` and
 * `aria-label` — deliberately not on screen, because a label naming a region is
 * a label nobody reads twice:
 *
 *   RAIL        the black bar. Where you go.
 *   PROJECT BAR the top strip: project, save state, commit, projects, account.
 *   VIEW BAR    scope · section · presentation · find. What you are looking at.
 *   SELECTION   what is selected, and the toolbar of what can be done to it.
 *   NAVIGATOR   the product tree, left.
 *   STAGE       the diagram or table, middle. The only region that changes shape.
 *   INSPECTOR   the selected element, right. Comments and Ask live under it.
 *   DRAWER      budgets, history, change review. Whole-scope answers, bottom.
 */

/* (1) The view bar loses its perimeter. The padding belongs to the things you
   click, not to the strip they sit in — a strip with its own margin reads as a
   card floating on a page, which is what the application stopped being. */

/* (5) Find, in the middle. */

/* One shape, not three.
 *
 * This was a bordered rectangle containing a bordered keyboard chip, at a
 * radius (8px on a 26px box) that reads as neither a field nor a pill -- three
 * outlines and an undecided corner, in a bar whose other controls are square.
 *
 * At rest it is now a filled pill with no border at all: the surface tint is
 * enough to say *type here* against the bar, and one less line is one less
 * thing competing with the tabs beside it. The edge appears on focus, where it
 * means something, and as an inset outline rather than a border so nothing
 * shifts by a pixel when it arrives. */

/* The hint, without a box round it. A bordered chip inside a bordered field was
   the second outline; as plain muted letterforms it still reads as a key and
   stops competing with the thing it sits inside. */

/* On a phone, 200px of minimum inside a bar that also carries a scope selector
   and the action buttons put 170px of the search box off the right edge -- so
   the thing you type into was the part you could not reach.
   
   It shrinks to whatever is left, and the keyboard hint goes: a phone has no
   Ctrl-K, and a shortcut nobody can press is 36px spent telling them so. */

/* (2) A tab strip with one tab is not a choice.
 *
 * The navigator had one — "Product tree" — with an accent underline marking it
 * as selected among no alternatives, and a header below it holding nothing but
 * a count. Two rows of chrome above a tree that wanted the height.
 *
 * Hidden by count rather than by name, so a region that gains a second panel by
 * docking gets its tabs back automatically. The strip is still the drag target
 * when there is something to drag. */

/* The count, on the project row where the name is. */

/* (2) A line under each row, so one element is visibly one row. Inset to the
   text rather than full-bleed: a rule that starts at the panel edge reads as a
   divider between groups, and these are siblings. */
/* Removed: a second `.tree-row` a thousand lines below the first, adding a
   `border-bottom` to a fixed-height row that already draws its hairline as an
   inset shadow — so rows were 1px taller than the virtualiser believed, on
   top of the 34-vs-26 disagreement. This is the exact shape `variantAfterFont`
   and the media-query check in the style gate exist to catch. */
.tree-row:last-child { border-bottom: 0; }

/* (4) Commit, as an icon. */

/* -------------------------------------------------------- the canvas toolbar
 *
 * What can be created, on the canvas where it will appear.
 *
 * Docked rather than free-floating, and to four positions rather than any. That
 * is the panels' rule (docs/workspace.md §3): free-floating chrome is how Cameo
 * became hard to use, because a thing draggable anywhere can be lost and no two
 * installations look alike. Four docks give the flexibility that was wanted
 * without the failure that comes with all of it. */
/*
 * The canvas toolbar.
 *
 * **Taller than the zoomer, deliberately.** It used to match it exactly, on the
 * argument that two things at the same distance from the drawing should be the
 * same size — which was wrong about what they are. The zoomer adjusts the view
 * and is reached for occasionally; this is where everything gets *made*, and a
 * 24px strip of 20px icons reads as a status bar somebody forgot to remove.
 */

/* While the toolbar is being dragged, the four places it can land. Without this
   a person drags it, drops it, and it either moves or does not — with no way to
   have known which edge they were aiming at. */

/* 20px in a 32px button. The glyphs were 16px in a 20px button, which is what
   "only showing icons with three dots" looked like from across a desk. */

/* Armed, and visibly so. A mode you cannot see is the thing that makes modal
   tools unpleasant — the other half being Escape, which is wired. */

/* The canvas says what will happen if you click. */

.arch.relating { cursor: alias; }

/* ------------------------------------------------- every region fills its box
 *
 * *"Not every window UI fits the fullscreen. The architecture table view is not
 * full screen vertically."*
 *
 * Three faults in one chain, and the third is why only the **table** showed it.
 *
 * `.regions` did not grow inside the workspace, so a grid whose first row is
 * `1fr` had nothing to take a fraction *of*. `.region-body` did not grow inside
 * its region, so it was the height of whatever was in it. And the diagram hid
 * both, because `.arch` carries `min-height: 340px` and props its panel open;
 * the table has no such prop and collapsed to 29 pixels — a header and two rows.
 * A `min-height` standing in for a layout that does not reach works until
 * something without one arrives.
 *
 * # Scoped to `main.full`, and the first two attempts were scoped wrong
 *
 * These say *fill the space you are given*, which is only meaningful where there
 * is a fixed amount of it. The application constrains its height; the demo page
 * scrolls. Unscoped, `.region-body { flex: 1 1 auto }` took the demo's stage
 * from 560px to **2611** — it grew to its content, and the canvas then sized
 * itself to that.
 *
 * Scoping to `.app` did not help, and the reason is worth writing down: **`.app`
 * is the shell, not the application.** The demo renders the same shell — rail
 * and all — so it carries that class too. `main.full` is the honest
 * discriminator because it is literally the class the build puts on a page that
 * has declined the site's chrome and taken the whole window.
 *
 * Caught by measuring the demo before and after rather than by looking at it.
 */
main.full .regions {
  flex: 1 1 auto;
  min-height: 0;
  /* `.layouts` is a child of this grid with no area, so it is auto-placed into
     an implicit row the template never accounted for — and that row takes its
     height out of the one meant to stretch. Invisible in the stylesheet, plain
     in the computed style: `268px 5px 200px 41px` against a template naming
     three rows. */
  grid-template-rows: auto minmax(0, 1fr) 5px var(--h-drawer, 200px);
  grid-template-areas:
    "layouts layouts layouts layouts layouts"
    "nav  s1 stage s2 insp"
    "s3   s3 s3    s3 s3"
    "drawer drawer drawer drawer drawer";
}
main.full .layouts { grid-area: layouts; }

/* The stage's own panels fill it rather than sitting at their content height. */

main.full .arch { flex: 1 1 auto; min-height: 0; height: 100%; }

/* The tree fills the navigator now, so a fixed ceiling would leave a gap under
   it on a tall window and a scroller inside a scroller on a short one. */
main.full .tree { max-height: none; }

/* ==================================================== the workspace, on a phone
 *
 * **Last in the file on purpose.** Everything below overrides a control that
 * sets its own height, and several of those definitions live in the last third
 * of this stylesheet — so a block placed with the other narrow rules loses the
 * cascade to them and silently does nothing. That is exactly what happened on
 * the first attempt here: `.tree-add` stayed 22px because its own rule is 1100
 * lines further down, which is the same collision the rail's profile button
 * had. Phone overrides go at the end, where nothing can come after them.
 *
 * # Most of this block went with the demo
 *
 * It used to raise eight controls, and six belonged to the workspace UI that
 * `demo.js` drew — deleted on 2026-09-14, and their rules with it. What is left
 * is the rail and the tree, which the React client still draws.
 *
 * **The reasoning is not lost, it moved.** `docs/design-language.md` now
 * carries the rule those measurements were arguing for, because it was never
 * about those particular controls: on a phone the scarce thing is accuracy
 * rather than vertical space, and a list you can read and cannot hit is not
 * denser, it is broken. That belongs with the design authority rather than in a
 * comment above whichever rules happen to exist this month. */
@media (max-width: 720px) {
  /* The rail's flyout entries. These were in the narrow block with the other
     rail rules and `padding` was being defeated by a later `.rail-sub` — found
     by a check, not by looking, because `min-height` survived and the row was
     the right *height* while the padding inside it was not. */
  .rail-sub { min-height: 44px; padding: var(--s-2) var(--s-3); }
  .rail-item { min-height: 44px; }

  /* How somebody navigates the model, so one of the most pressed targets in
     the product. It was 34px measured at 375px. */
  .tree-row { height: 44px; }
}

/* ======================================================= the planner, on a phone
 *
 * **Last in the file, with the workspace's block, and for the same reason.**
 *
 * The planner had its own narrow-viewport block a thousand lines up, where
 * `.pl-fold` took `padding: var(--s-2)` under a comment promising "44px of
 * thumb around a 10px glyph".
 *
 * `--s-2` is 8px. 10 + 8 + 8 is **26**, not 44, and it had said 44 since the
 * day it was written. Nothing was defeating it; the arithmetic was simply never
 * done, and the comment then stood in for having checked. Measured at 375px,
 * every control the planner draws for itself was under the target size:
 *
 *   - the completion tick, **22px**, ten of them on screen and the single most
 *     pressed control in a task list
 *   - the group fold, **26px**
 *   - the view pills, **32px**
 *   - the filter selects, **38px**
 *   - the search field, **21px** — a text input the height of its own text
 *   - the two switches, a **13px** native box
 *
 * # A tick stays 22px and its target becomes 44
 *
 * Growing the box would draw a 44px circle, which is not what anybody wants
 * beside a line of text — the tick is a mark, not a button, and at 44px it
 * competes with the task's own title. So the drawn thing keeps its size and an
 * empty pseudo-element carries the target. That is the shape to reach for
 * whenever a control is smaller than a thumb *on purpose*.
 *
 * It is `::after` on a `position: relative` parent rather than padding, because
 * both of these have a border and a background: padding would grow exactly the
 * part that must not grow. */
@media (max-width: 720px) {
  .pl-check, .pl-fold { position: relative; }
  .pl-check::after, .pl-fold::after {
    content: ""; position: absolute; top: 50%; left: 50%;
    width: var(--ctl-h); height: var(--ctl-h); transform: translate(-50%, -50%);
  }
  /* The padding hack this replaces claimed 44 and delivered 26. */
  .pl-fold { padding: var(--s-05); margin: 0; }

  /* The toolbar's own controls all take `--pl-ctl-h` now, set where the bar is
     defined — one number for the whole row instead of a rule per control here,
     each of which was a chance to miss one. Group and Sort were missed exactly
     that way. The trade against the 44px target is argued there. */

  /* Hide done and Overdue. The label is the target, not the 18px box inside
     it, so the row carries the height. */
  .pl-bar .check.sm { min-height: var(--pl-ctl-h); }
}

/* ============================================== the planner's create button
 *
 * The one control that makes something, floating over the list it adds to.
 *
 * It was the last item in a toolbar that scrolls sideways on a phone, which
 * put the planner's primary action off the end of the screen behind eight
 * filters. A bar for narrowing what you are looking at is the wrong place for
 * the thing that makes something new, and on a handset it was also the least
 * reachable pixel on the row.
 *
 * A square with rounded corners rather than a circle, at Cameron's ask, and it
 * reads better here for it: everything else in this product is a rounded
 * rectangle, and a circle would have been the only one of its kind on screen.
 *
 * # Absolute, not fixed
 *
 * `.planner` is `position: relative`, so this hangs off the planner rather than
 * the viewport. Fixed positioning would leave it floating over Design and over
 * every module the moment the destination changed, because a fixed element
 * ignores every ancestor that is not itself transformed. */
.pl-fab-wrap {
  position: absolute; right: var(--s-4); bottom: var(--s-4);
  display: flex; flex-direction: column; align-items: flex-end; gap: var(--s-2);
  z-index: 5;
}
.pl-fab {
  width: 54px; height: 54px; display: grid; place-items: center;
  border: 0; border-radius: var(--shape-l); cursor: pointer;
  background: var(--primary); color: var(--on-primary);
  box-shadow: var(--e-3);
  transition: transform var(--t-quick) var(--ease-spring),
              box-shadow var(--t-quick) var(--ease-out);
}
.pl-fab svg { width: 24px; height: 24px; transition: transform var(--t-quick) var(--ease-spring); }
.pl-fab:hover { box-shadow: var(--e-4); }
.pl-fab:active { transform: scale(.94); }
.pl-fab:focus-visible { outline: 2px solid var(--primary); outline-offset: 3px; }
/* The plus turns into a close when the menu is open, so one button says both
   what it will do and that it is currently doing it. */
.pl-fab[aria-expanded="true"] svg { transform: rotate(45deg); }

.pl-fab-menu {
  display: flex; flex-direction: column; gap: var(--s-05); padding: var(--s-1);
  border-radius: var(--shape-m); border: 1px solid var(--outline-variant);
  background: var(--surface-container-high); box-shadow: var(--e-2);
  min-width: 168px;
}
.pl-fab-menu[hidden] { display: none; }

@media (prefers-reduced-motion: reduce) {
  .pl-fab, .pl-fab svg { transition: none; }
}

/* The word on a pointer, the glyph alone on a handset — see `app.html`. The
   bar wrapped onto a second row on a phone and this was the widest thing on
   it. */
@media (max-width: 720px) {
  /* Without a word the button is a glyph in a box the width of two paddings,
     so it takes the square shape the other icon buttons in the bar have. */
  #projects-open { width: var(--ctl-h-sm); padding: 0; }

  /* **The bar is one row, or it is not chrome.** It wrapped onto two — 93px of
     a 812px screen spent on which project you are in — and taking the model
     state out of it was not enough on its own: the name, the description and
     the storage chip still came to 333px against 375 of screen.
     The description is the one of the three that answers a question nobody
     asks at a glance, and it was ellipsised to a third of itself anyway. The
     chip stays: "this browser" says the work is only on this device, which is
     worth a phone's width. */
}

/* ====================================================== the type scale, smaller
 *                                                        on a phone
 *
 * Cameron, 2026-08-27: *our mobile fonts still feel pretty big.*
 *
 * He is right, and the reason is that there was only ever one scale. It was set
 * for a dense desktop tool where 13px body text sits beside a tree, a canvas and
 * an inspector — and then rendered unchanged on a 375px screen, where the same
 * 13px is proportionally much larger because there is a third of the width to
 * put it in. A phone is not a small desktop; it is a different measure.
 *
 * # One block, because the scale is the thing that moves
 *
 * Every size here is a token, so this is the whole change: nothing per-component
 * and nothing to keep in step. A rule that shrank `.pl-row` and forgot
 * `.pl-group-name` would be back within a week.
 *
 * **Last in the file**, after both `:root` blocks and the `[data-theme]` ones.
 * A token redefined before its own definition does nothing, which is how twenty
 * of these got stranded inside a media query on the first attempt at a mobile
 * pass — valid CSS, green gates, no effect.
 *
 * # What is deliberately left alone
 *
 * `--f-display-*` and the two large headline roles. They are the landing page's
 * voice, they are already fluid through `clamp()`, and shrinking the hero on a
 * phone would be fixing the application by damaging the thing that sells it.
 * The roles below this are the ones the *tool* is built from. */
@media (max-width: 720px) {
  :root {
    /*
     * **Sixteen declarations became eleven, and the duplicate list is gone.**
     *
     * This block used to restate every `font` shorthand *and* a parallel list
     * of raw sizes, under a comment saying the two must "move together with
     * them or the two scales drift and a row ends up mixing 13px from one and
     * 12px from the other". The comment was right about the hazard and this
     * block was diligent about it — and the *base* `:root` had never been
     * reconciled, so the drift it warns against was already shipped, one pixel
     * wide, in four of six pairs.
     *
     * That is the argument for deriving rather than restating. Both forms now
     * read `--fs-*`, so redefining these numbers is the entire compact scale
     * and there is no second list to update out of step with this one.
     */
    --fs-label-xs: 9px;
    --fs-label-s: 10px;
    --fs-label-m: 11px;
    --fs-label-l: 12px;
    --fs-body-s: 12px;
    --fs-body-m: 13px;
    --fs-body-l: 14px;
    --fs-title-s: 12px;
    --fs-title-m: 14px;
    --fs-title-l: 15px;
    --fs-headline-s: 17px;
  }
}

/* ============================================================= the home screen
 *
 * Where the planner opens, at Cameron's ask 2026-08-27: the dashboard is the
 * planning screen people jump off from, not a tab beside the list.
 *
 * # The tile
 *
 * A filled rounded square holding a line glyph, the count large, the label
 * small under it. The proportion is the thing being copied: the icon is about
 * the height of the two lines of text beside it, which keeps it reading as a
 * marker for the number rather than as a button in its own right.
 *
 * The icon square carries the colour and the card does not. A card tinted by
 * its own status turns a screen of eight into a paint chart, and the number is
 * what somebody came to read. */
.hm { display: flex; flex-direction: column; gap: var(--s-5); padding: var(--s-4); }

.hm-tiles {
  display: grid; gap: var(--s-3);
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
}
.hm-tile {
  display: grid; grid-template-columns: auto 1fr; grid-template-rows: auto auto;
  align-items: center; gap: 2px var(--s-3);
  padding: var(--s-3); text-align: left; cursor: pointer;
  border: 1px solid var(--outline-variant); border-radius: var(--shape-m);
  background: var(--surface-container-low); color: var(--on-surface);
  transition: border-color var(--t-quick) var(--ease-out), background var(--t-quick) var(--ease-out);
}
.hm-tile:hover { background-image: var(--layer-hover); border-color: var(--outline); }
.hm-tile:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.hm-icon {
  grid-row: 1 / span 2; display: grid; place-items: center;
  width: 34px; height: 34px; border-radius: var(--shape-s);
  background: var(--surface-container-highest); color: var(--on-surface-variant);
}
.hm-icon svg { width: 19px; height: 19px; }
.hm-count { font: var(--f-title-l); font-variant-numeric: tabular-nums; align-self: end; }
.hm-label { font: var(--f-label-m); color: var(--on-surface-variant); align-self: start; }

/* Tone lives on the icon, and one tile has one.
 *
 * There were five hues across eight tiles, which is a row that looks like an
 * alarm panel and reads as none of them mattering. Colour marks what needs
 * attention; the only thing here that does is work that is late — and the
 * renderer sends `neutral` when the count is zero, because a tile that is red
 * whether or not anything is overdue is a false alarm every time somebody
 * glances at it. */
.hm-tile[data-tone="bad"] .hm-icon { background: var(--bad-container); color: var(--bad); }

.hm-section { display: flex; align-items: center; gap: var(--s-2); }
.hm-heading {
  margin: 0; font: var(--f-label-l); letter-spacing: var(--k-eyebrow);
 color: var(--on-surface-variant);
}
.hm-add {
  margin-left: auto; display: grid; place-items: center;
  width: var(--ctl-h-sm); height: var(--ctl-h-sm); cursor: pointer;
  border: 1px solid var(--outline-variant); border-radius: var(--shape-s);
  background: none; color: var(--on-surface-variant);
}
.hm-add svg { width: 17px; height: 17px; }
.hm-add:hover { background-image: var(--layer-hover); color: var(--on-surface); }
.hm-add:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }

.hm-lists { display: flex; flex-direction: column; }
.hm-list {
  display: flex; align-items: center; gap: var(--s-3); width: 100%;
  padding: var(--s-2) var(--s-1); min-height: var(--ctl-h); cursor: pointer;
  border: 0; border-bottom: 1px solid var(--outline-variant); background: none;
  color: var(--on-surface); text-align: left; font: var(--f-body-m);
}
.hm-list:last-child { border-bottom: 0; }
.hm-list:hover { background-image: var(--layer-hover); }
.hm-list:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; }
.hm-list-mark { display: grid; place-items: center; color: var(--on-surface-variant); flex: none; }
.hm-list-mark svg { width: 18px; height: 18px; }
/* A colour, not `opacity: .55`. Opacity multiplies the contrast of everything
   inside an element, so it dims whatever it is applied to *and* every word in
   it -- which no token check can see, because both tokens are still correct.
   The a11y gate caught this one; the muted role says the same thing and says
   it in a way the rest of the palette can be reasoned about. */
.hm-list-loose { color: var(--on-surface-muted); }
.hm-list-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hm-list-count { font: var(--f-label-m); color: var(--on-surface-variant); font-variant-numeric: tabular-nums; }
/* The late count travels beside the open one: a list of twelve with three late
   is a different thing from a list of twelve, and the row has space for both. */
.hm-list-late {
  font: var(--f-label-s); font-variant-numeric: tabular-nums;
  padding: 1px var(--s-2); border-radius: var(--shape-chrome);
  background: var(--bad-container); color: var(--bad);
}
.hm-empty { margin: 0; font: var(--f-body-s); color: var(--on-surface-variant); }

/* The practice programme, offered from a project with work already in it.
 * Quiet on purpose: it sits under the lists and must not compete with them.
 * Existing tokens throughout — a demo panel is not the place to introduce a
 * value the ratchet then has to carry for ever. */
.hm-sample { display: flex; flex-direction: column; align-items: flex-start; gap: var(--s-2); }
.hm-sample-note { margin: 0; font: var(--f-body-s); color: var(--on-surface-variant); }

/* ================================================================ the ask window
 *
 * A tab in the corner that opens a panel. Deliberately not a floating circle
 * with a speech bubble in it: that is the universal sign of a support bot people
 * have learned to dismiss, and this is meant to be read.
 *
 * It sits above the page and below a dialog, so a modal still covers it. */
.askwin { position: fixed; right: var(--s-4); bottom: var(--s-4); z-index: 30;
  display: flex; flex-direction: column; align-items: flex-end; gap: var(--s-2); }
.aw-tab {
  display: inline-flex; align-items: center; gap: var(--s-2);
  padding: var(--s-2) var(--s-3); border: 1px solid var(--line);
  border-radius: var(--r-lg); background: var(--surface-card); color: var(--on-surface);
  font: var(--f-label-l); cursor: pointer; box-shadow: var(--e-2);
}
.aw-tab:hover { border-color: var(--line-strong); }
.aw-tab svg { width: 17px; height: 17px; }
.aw-panel {
  width: min(380px, calc(100vw - var(--s-6)));
  border: 1px solid var(--line); border-radius: var(--r-lg);
  background: var(--surface-card); box-shadow: var(--e-4);
  display: flex; flex-direction: column; overflow: hidden;
}
.aw-head { display: flex; align-items: center; justify-content: space-between;
  padding: var(--s-3); border-bottom: 1px solid var(--line); font: var(--f-title-s); }
.aw-close { border: 0; background: none; color: var(--on-surface-variant); cursor: pointer;
  padding: var(--s-1); display: inline-flex; }
.aw-close svg { width: 17px; height: 17px; }
.aw-log { padding: var(--s-3); display: flex; flex-direction: column; gap: var(--s-2);
  max-height: 46vh; overflow-y: auto; }
.aw-note { margin: 0; font: var(--f-body-s); color: var(--on-surface-variant); }
.aw-openers { display: flex; flex-wrap: wrap; gap: var(--s-1); }
.aw-line { margin: 0; font: var(--f-body-s); }
/* The visitor's own words, set apart so a long exchange stays readable. */
.aw-you { color: var(--on-surface); font-weight: var(--w-medium); }
.aw-us { color: var(--on-surface-variant); }
.aw-form { display: flex; gap: var(--s-2); padding: var(--s-3);
  border-top: 1px solid var(--line); align-items: center; }
.aw-form input { flex: 1; min-width: 0; }
/* The label is for assistive technology; the placeholder carries it visually,
 * and a placeholder is not a label. */
.aw-label { position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap; }

@media (max-width: 620px) {
  /* Full width at the bottom, and the log gets shorter so the keyboard does not
   * push the input off the screen. */
  .askwin { left: var(--s-3); right: var(--s-3); bottom: var(--s-3); align-items: stretch; }
  .aw-panel { width: auto; }
  .aw-log { max-height: 38vh; }
}


/* ================================================================= call to action
 *
 * A band rather than another prose section, because a page that asks the same
 * way it explains gets scrolled past. Existing tokens only: the surface is the
 * one cards already use, so this reads as part of the page rather than as an
 * advertisement somebody dropped into it. */
.cta-band {
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  padding: var(--s-6) var(--s-5);
  background: var(--surface-card);
  text-align: center;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--s-2);
}
.cta-band h2 { margin: 0; }
.cta-band p { margin: 0; max-width: 52ch; color: var(--on-surface-variant); }
.cta-band .cta { margin-top: var(--s-2); }
/* After running prose or a card — a blog post, About — the band needs the gap a chapter gets. */
:is(.prose, .team) + .cta-band { margin-top: var(--s-10); }

/* ========================================================== mapping approval
 *
 * The screen where somebody signs their name to a mapping set (D-86). Every
 * value is a token: this is the last screen that should introduce a one-off
 * number, because it is the one an auditor reads.
 *
 * The unaudited remainder is styled to be *read*, not to be reassuring. It is
 * the least flattering count on the screen and hiding it in muted small type
 * would be the interface quietly making the claim the policy refuses to. */
.mp { display: flex; flex-direction: column; gap: var(--s-3); }
.mp-plan, .mp-section, .mp-decide { display: flex; flex-direction: column; gap: var(--s-2); }
.mp-heading { margin: 0; font: var(--f-title-s); color: var(--on-surface); }
.mp-note { margin: 0; font: var(--f-body-s); color: var(--on-surface-variant); }
.mp-remainder { margin: 0; font: var(--f-body-s); color: var(--on-surface); }
.mp-rows { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
.mp-row {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  padding: var(--s-1) 0;
  border-bottom: var(--line);
}
.mp-pair { font: var(--f-body-s); color: var(--on-surface); font-family: var(--mono); }
.mp-arrow { color: var(--on-surface-variant); }
.mp-why { font: var(--f-label-s); color: var(--on-surface-variant); }
.mp-mark { font: var(--f-label-s); color: var(--on-surface-variant); }
/* An unjudged row is the one that must not read as settled, so the judged ones
 * carry the colour and the plain row carries none. */
.mp-row[data-judged="approved"] .mp-mark { color: var(--ok); }
.mp-row[data-judged="rejected"] .mp-mark { color: var(--bad); }
.mp-acts { display: flex; gap: var(--s-1); margin-left: auto; }
.mp-blockers { margin: 0; padding-left: var(--s-3); font: var(--f-body-s); color: var(--on-surface-variant); }

/* ================================================================ the picker
 *
 * What the create button opens. Each row is an icon, a name and a line saying
 * what the thing is for — and that last line is why the picker earns its
 * screen. Task, reminder and note are three words somebody has to guess
 * between, and the difference is real: a reminder has no status and a note has
 * no date, so guessing wrong means fighting the record's shape later. */
.pk-scrim {
  position: fixed; inset: 0; z-index: 60;
  display: flex; justify-content: center; align-items: flex-start;
  padding: var(--s-6) var(--s-4);
  background: var(--scrim);
}
.pk {
  display: flex; flex-direction: column; gap: var(--s-3);
  width: min(440px, 100%); max-height: 100%; overflow: auto;
  padding: var(--s-4); border-radius: var(--shape-l);
  border: 1px solid var(--outline-variant);
  background: var(--surface-container); box-shadow: var(--e-4);
}
.pk-head { display: flex; align-items: center; }
.pk-title { margin: 0; font: var(--f-title-m); flex: 1; text-align: center; }
.pk-close {
  display: grid; place-items: center; width: var(--ctl-h-sm); height: var(--ctl-h-sm);
  border: 0; border-radius: var(--shape-chrome); cursor: pointer;
  background: var(--surface-container-highest); color: var(--on-surface-variant);
}
.pk-close svg { width: 18px; height: 18px; }
.pk-close:hover { background-image: var(--layer-hover); color: var(--on-surface); }
.pk-close:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }

.pk-find {
  display: flex; align-items: center; gap: var(--s-2); height: var(--ctl-h);
  padding: 0 var(--s-3); border-radius: var(--shape-chrome);
  background: var(--surface-container-high); color: var(--on-surface-variant);
  border: 1px solid transparent;
}
.pk-find:focus-within { border-color: var(--primary); }
.pk-find-icon { width: 16px; height: 16px; flex: none; }
.pk-find input {
  flex: 1; min-width: 0; width: auto; border: 0; background: none; padding: 0;
  font: var(--f-body-m); color: var(--on-surface);
}
.pk-find input:hover { border-color: transparent; }
.pk-find input:focus-visible { outline: none; }

.pk-rows { display: flex; flex-direction: column; gap: var(--s-05); }
.pk-row {
  display: flex; align-items: center; gap: var(--s-3); width: 100%;
  padding: var(--s-2); cursor: pointer; text-align: left;
  border: 0; border-radius: var(--shape-s); background: none; color: var(--on-surface);
}
.pk-row[hidden] { display: none; }
.pk-row:hover { background-image: var(--layer-hover); }
.pk-row:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; }
/* 38px against two lines of type — the same proportion as the home tiles, on
   purpose. An icon the size of the type reads as decoration; one much larger
   reads as the button, which it is not. The row is the button. */
.pk-icon {
  display: grid; place-items: center; flex: none;
  width: 38px; height: 38px; border-radius: var(--shape-s);
  background: var(--surface-container-highest); color: var(--on-surface-variant);
}
.pk-icon svg { width: 20px; height: 20px; }
.pk-icon[data-tone="primary"] { background: var(--primary-container); color: var(--primary-ink); }
.pk-icon[data-tone="warn"] { background: var(--warn-container); color: var(--warn); }
.pk-icon[data-tone="info"] { background: var(--primary-container); color: var(--primary-ink); }
.pk-icon[data-tone="ok"] { background: var(--ok-container); color: var(--ok); }
.pk-text { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
/* The name a clear step above the blurb. Both landed on 12px at the first
   attempt -- correct tokens, no hierarchy, and the row read as two lines of
   equal weight where one of them is the answer to "what am I making". */
.pk-name { font: var(--f-title-m); }
.pk-blurb { font: var(--f-body-s); color: var(--on-surface-variant); }
.pk-none { margin: 0; padding: var(--s-3); font: var(--f-body-s); color: var(--on-surface-variant); }

/* ========================================================== the compose sheet
 *
 * A modal on a pointer, a sheet from the bottom on a phone.
 *
 * The title is the only thing asked for; everything else is an icon that may be
 * pressed. That is what keeps the fast path one line and one tap while leaving
 * every field the record holds one press away rather than behind an edit screen
 * reached afterwards. */
.cp-scrim {
  position: fixed; inset: 0; z-index: 70;
  display: flex; justify-content: center; align-items: center; padding: var(--s-4);
  background: var(--scrim);
}
.cp {
  display: flex; flex-direction: column; gap: var(--s-3);
  width: min(520px, 100%); max-height: 100%; overflow: auto;
  padding: var(--s-4); margin: 0;
  border-radius: var(--shape-l); border: 1px solid var(--outline-variant);
  background: var(--surface-container); box-shadow: var(--e-5);
}
.cp-head { display: flex; align-items: center; gap: var(--s-2); }
.cp-where {
  display: flex; align-items: center; gap: var(--s-2); min-width: 0; flex: 1;
  font: var(--f-body-m); margin: 0;
}
.cp-where-label { color: var(--on-surface-variant); flex: none; }
.cp-where select {
  width: auto; min-width: 0; height: var(--ctl-h-sm); padding: 0 var(--s-2);
  font: var(--f-title-s);
  border-color: transparent; background: none;
}
.cp-where select:hover { border-color: var(--outline-variant); }
.cp-close {
  display: grid; place-items: center; width: var(--ctl-h-sm); height: var(--ctl-h-sm);
  border: 0; border-radius: var(--shape-chrome); cursor: pointer;
  background: var(--surface-container-highest); color: var(--on-surface-variant);
}
.cp-close:hover { background-image: var(--layer-hover); color: var(--on-surface); }
.cp-close:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }

/* The one thing being asked for, so it is the largest thing on the sheet.
 *
 * **`input.cp-title`, not `.cp-title`.** The base form rule is
 * `input[type=text], input[type=email], … { font: var(--f-body-l) }`, and an
 * attribute selector scores a class — so a bare `.cp-title` loses on
 * specificity and the headline rendered at body size. Measured 14px where 17
 * was written; nothing in either rule looks wrong on its own, which is this
 * stylesheet's whole recurring problem. */
input.cp-title {
  width: 100%; border: 0; background: none; padding: 0;
  font: var(--f-headline-s); color: var(--on-surface);
}
input.cp-title:hover { border-color: transparent; }
input.cp-title:focus-visible { outline: none; }
input.cp-title::placeholder { color: var(--on-surface-muted); }

.cp-body {
  width: 100%; border: 0; background: none; padding: 0; resize: vertical;
  font: var(--f-body-m); color: var(--on-surface-variant);
}
.cp-body:hover { border-color: transparent; }
.cp-body:focus-visible { outline: none; }

.cp-lines { display: flex; flex-direction: column; }
.cp-line {
  display: flex; align-items: center; gap: var(--s-3); width: 100%;
  min-height: var(--ctl-h); padding: 0; cursor: pointer; text-align: left;
  border: 0; border-top: 1px solid var(--outline-variant); background: none;
  font: var(--f-body-m); color: var(--on-surface-variant);
}
.cp-line:hover .cp-line-text { color: var(--on-surface); }
.cp-line:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; }
.cp-line-icon { display: grid; place-items: center; flex: none; color: var(--on-surface-variant); }
.cp-line-text.on { color: var(--on-surface); }

.cp-foot { display: flex; align-items: center; gap: var(--s-2); padding-top: var(--s-2); }
.cp-spacer { flex: 1; }
.cp-status {
  display: inline-flex; align-items: center; gap: var(--s-2);
  height: var(--ctl-h-sm); padding: 0 var(--s-3); cursor: pointer;
  border: 1px solid var(--outline-variant); border-radius: var(--shape-chrome);
  background: none; color: var(--on-surface-variant);
  font: var(--f-label-m);
}
.cp-status:hover { background-image: var(--layer-hover); }
.cp-status:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.cp-status-dot { width: 8px; height: 8px; border-radius: var(--shape-chrome); background: var(--outline); }
.cp-status[data-tone="info"] .cp-status-dot { background: var(--primary); }
.cp-status[data-tone="warn"] .cp-status-dot { background: var(--warn); }
.cp-status[data-tone="bad"] .cp-status-dot { background: var(--bad); }
.cp-status[data-tone="ok"] .cp-status-dot { background: var(--ok); }

.cp-tools { display: flex; align-items: center; gap: var(--s-05); }
.cp-tool {
  display: grid; place-items: center; width: var(--ctl-h-sm); height: var(--ctl-h-sm);
  border: 0; border-radius: var(--shape-s); background: none; cursor: pointer;
  color: var(--on-surface-variant);
}
.cp-tool:hover { background-image: var(--layer-hover); color: var(--on-surface); }
.cp-tool:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; }
/* A set field takes the accent. Nothing else on this row carries colour, so one
   coloured glyph reads as "that one has something in it" at a glance. */
.cp-tool.on { color: var(--primary); background: var(--primary-container); }
.cp-error { margin: 0; font: var(--f-body-s); color: var(--bad); }
.cp-error[hidden] { display: none; }

/* The way back out of a list, in the toolbar. */
.pl-crumb {
  display: inline-flex; align-items: center; gap: var(--s-2); flex: none;
  height: var(--pl-ctl-h, 36px); padding: 0 var(--s-3); cursor: pointer;
  border: 1px solid var(--outline-variant); border-radius: var(--shape-chrome);
  background: var(--surface-container-high); color: var(--on-surface);
  font: var(--f-label-m);
}
.pl-crumb svg { width: 13px; height: 13px; color: var(--on-surface-variant); }
.pl-crumb:hover { background-image: var(--layer-hover); }
.pl-crumb:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.pl-crumb-x { color: var(--on-surface-variant); font: var(--f-body-m); }

/* ============================== the picker and the compose sheet, on a phone
 *
 * **Last in the file**, with the other phone overrides, for the reason the
 * block above them gives at length: a narrow rule placed among the components
 * loses the cascade to definitions further down and silently does nothing.
 *
 * A centred box is the right shape on a pointer and the wrong one on a handset.
 * Cameron's screenshots are unambiguous about both: the picker fills the screen
 * from the top, and the compose sheet rises from the bottom edge.
 *
 * # Why the bottom, and not the middle
 *
 * The keyboard. Every one of these sheets exists to be typed into, and a
 * software keyboard takes the bottom half of the screen — a box centred in the
 * viewport ends up centred behind the keyboard, with the field being typed into
 * underneath it. Anchoring to the bottom means the sheet sits *on* the keyboard
 * instead, which is where every native compose screen puts it and why they all
 * look the same. The corners round only at the top for the same reason: the
 * bottom edge is the screen's, not the sheet's. */
@media (max-width: 720px) {
  .pk-scrim { padding: 0; align-items: stretch; }
  .pk {
    width: 100%; max-height: none; border-radius: var(--shape-none); border: 0;
    padding: var(--s-4) var(--s-3);
    background: var(--surface);
  }
  /* Full-bleed rows, so the tap target runs to both edges rather than stopping
     at a card's padding on a screen that has none to spare. */
  .pk-row { padding: var(--s-2) var(--s-3); border-radius: var(--shape-none); }

  .cp-scrim { padding: 0; align-items: flex-end; }
  .cp {
    width: 100%; max-height: 88%;
    border-radius: var(--shape-l) var(--shape-l) var(--shape-none) var(--shape-none);
    border-left: 0; border-right: 0; border-bottom: 0;
    /* Below the sheet is the screen edge, and a phone's home indicator sits on
       it. Padding rather than a margin, so the sheet still reaches the edge. */
    padding-bottom: var(--s-5);
  }
}

/* ================================================================ the choosers
 *
 * One shape for picking a person, an element, a status, a priority or a label —
 * and a calendar, which is genuinely a different thing.
 *
 * They replace a `prompt()` each. Cameron went through them field by field on
 * 2026-08-27: a people list with a face, a real calendar, an element list
 * filtered by type and showing what each name refers to, and menus rather than
 * a control that cycles through five states.
 *
 * # Above the compose sheet, not inside it
 *
 * `z-index` puts a chooser over the sheet that opened it, and Escape closes the
 * chooser alone. A chooser drawn *inside* the sheet would either scroll it or
 * resize it, and both lose the half-typed title somebody already has in there. */
.ch-scrim {
  position: fixed; inset: 0; z-index: 80;
  display: flex; justify-content: center; align-items: center; padding: var(--s-4);
  background: var(--scrim);
}
.ch {
  display: flex; flex-direction: column; gap: var(--s-3);
  width: min(440px, 100%); max-height: 100%; overflow: auto;
  padding: var(--s-4); border-radius: var(--shape-l);
  border: 1px solid var(--outline-variant);
  background: var(--surface-container); box-shadow: var(--e-5);
}
.ch-head { display: flex; align-items: center; }
.ch-title { margin: 0; font: var(--f-title-m); flex: 1; }
.ch-close {
  display: grid; place-items: center; width: var(--ctl-h-sm); height: var(--ctl-h-sm);
  border: 0; border-radius: var(--shape-chrome); cursor: pointer;
  background: var(--surface-container-highest); color: var(--on-surface-variant);
}
.ch-close:hover { background-image: var(--layer-hover); color: var(--on-surface); }
.ch-close:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }

.ch-find {
  display: flex; align-items: center; gap: var(--s-2); height: var(--ctl-h);
  padding: 0 var(--s-3); border-radius: var(--shape-chrome);
  background: var(--surface-container-high); color: var(--on-surface-variant);
  border: 1px solid transparent; flex: none;
}
.ch-find:focus-within { border-color: var(--primary); }
.ch-find-icon { width: 16px; height: 16px; flex: none; }
.ch .ch-q {
  flex: 1; min-width: 0; width: auto; height: auto; align-self: stretch;
  border: 0; background: none; padding: 0;
  font: var(--f-body-m); color: var(--on-surface);
}
.ch .ch-q:hover { border-color: transparent; }
.ch .ch-q:focus-visible { outline: none; }

/* The type filter. Scrolls sideways rather than wrapping: a model with a dozen
   kinds would otherwise push the list itself off the bottom of a phone. */
.ch-filters {
  display: flex; gap: var(--s-2); flex: none;
  overflow-x: auto; scrollbar-width: none; padding-bottom: var(--s-05);
}
.ch-filters::-webkit-scrollbar { display: none; }
.ch-chip {
  display: inline-flex; align-items: center; gap: var(--s-2); flex: none;
  height: var(--ctl-h-sm); padding: 0 var(--s-3); cursor: pointer;
  border: 1px solid var(--outline-variant); border-radius: var(--shape-chrome);
  background: none; color: var(--on-surface-variant); font: var(--f-label-m);
}
.ch-chip b { font: var(--f-label-s); color: var(--on-surface-muted); }
.ch-chip:hover { background-image: var(--layer-hover); }
.ch-chip[aria-pressed="true"] {
  background: var(--primary-container); color: var(--primary-ink); border-color: transparent;
}
.ch-chip[aria-pressed="true"] b { color: var(--primary-ink); }
.ch-chip:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }

.ch-rows { display: flex; flex-direction: column; gap: 1px; overflow: auto; min-height: 0; }
.ch-row {
  display: flex; align-items: center; gap: var(--s-3); width: 100%;
  min-height: var(--ctl-h); padding: var(--s-2); cursor: pointer; text-align: left;
  border: 0; border-radius: var(--shape-s); background: none; color: var(--on-surface);
}
.ch-row:hover { background-image: var(--layer-hover); }
.ch-row:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; }
.ch-row.on { background: var(--primary-container); color: var(--primary-ink); }
.ch-text { display: flex; flex-direction: column; gap: 1px; min-width: 0; flex: 1; }
.ch-name { font: var(--f-body-m); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ch-sub {
  display: flex; align-items: center; gap: var(--s-2);
  font: var(--f-body-s); color: var(--on-surface-variant);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ch-row.on .ch-sub { color: var(--primary-ink); }
.ch-tick { font: var(--f-body-m); flex: none; }
.ch-empty, .ch-more { margin: 0; padding: var(--s-3); font: var(--f-body-s); color: var(--on-surface-variant); }
.ch-foot { display: flex; justify-content: flex-end; gap: var(--s-2); padding-top: var(--s-2); flex: none; }

/* An element's kind, as a short tag rather than an icon. There are forty-seven
   kinds in the parser and no icon set distinguishes them; the word does. */
.ch-kind {
  display: grid; place-items: center; flex: none;
  min-width: 42px; height: 34px; padding: 0 var(--s-2); border-radius: var(--shape-s);
  background: var(--surface-container-highest); color: var(--on-surface-variant);
  font: var(--f-label-s);
}
.ch-type { color: var(--on-surface-variant); }
/* Small and monospaced: there to be checked against something, not read. */
/* The mono role, not a family plus a size: an id is small because it is a
   label, and `--f-label-s` already says how small a label is. */
.ch-id { font: var(--f-label-s); font-family: var(--mono); color: var(--on-surface-muted); }
.ch-row.on .ch-id { color: var(--primary-ink); }
.ch-dot { width: 9px; height: 9px; border-radius: var(--shape-full); background: var(--outline); flex: none; }
.ch-dot[data-tone="info"] { background: var(--primary); }
.ch-dot[data-tone="warn"] { background: var(--warn); }
.ch-dot[data-tone="bad"] { background: var(--bad); }
.ch-dot[data-tone="ok"] { background: var(--ok); }
.ch-dot[data-tone="neutral"] { background: var(--on-surface-variant); }

/* ------------------------------------------------------------------ a face
 *
 * Circular, and the initials are drawn rather than a stock silhouette: an
 * outline of a head says "this person has no picture" in the same language as
 * "this person is not known", and those are different things. */
.pv {
  display: grid; place-items: center; flex: none; overflow: hidden;
  border-radius: var(--shape-chrome);
  background: var(--surface-container-highest); color: var(--on-surface-variant);
  font: var(--f-label-m); letter-spacing: 0;
}
.pv img { width: 100%; height: 100%; object-fit: cover; display: block; }
.pv-sm { width: 24px; height: 24px; font: var(--f-label-s); }
.ch-row.on .pv { background: var(--surface-container); color: var(--on-surface); }

/* ============================================================== the calendar
 *
 * Both dates in one sheet, because a start after its due date is the only wrong
 * pair a person can enter here and it is only visible when both are in front of
 * them. Two separate pickers would each be individually correct and let the
 * pair be nonsense.
 *
 * No recurring switch, though the screenshot has one: nothing in this model
 * repeats, and a control that sets a field nothing reads is a promise the
 * screen makes and nothing keeps. */
.dp { width: min(400px, 100%); }
.dp-fields { display: flex; flex-direction: column; gap: var(--s-2); }
.dp-field {
  display: flex; align-items: center; gap: var(--s-3);
  height: var(--ctl-h); padding: 0 var(--s-3); cursor: pointer; text-align: left;
  border: 1px solid var(--outline-variant); border-radius: var(--shape-m);
  background: none; color: var(--on-surface-variant); font: var(--f-body-m);
}
.dp-field span.on { color: var(--on-surface); }
.dp-field:hover { background-image: var(--layer-hover); }
/* The focused field is the one a tap on the grid fills, so it says so. */
.dp-field.on { border-color: var(--primary); }
.dp-field:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }

.dp-quick { display: flex; gap: var(--s-2); overflow-x: auto; scrollbar-width: none; }
.dp-quick::-webkit-scrollbar { display: none; }

.dp-month { display: flex; align-items: center; gap: var(--s-2); }
.dp-name { font: var(--f-title-m); flex: 1; }
.dp-step {
  display: grid; place-items: center; width: var(--ctl-h-sm); height: var(--ctl-h-sm);
  border: 0; border-radius: var(--shape-s); background: none; cursor: pointer;
  color: var(--on-surface-variant);
}
.dp-step:hover { background-image: var(--layer-hover); color: var(--on-surface); }
.dp-step:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; }

.dp-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: var(--s-05); }
.dp-dow {
  display: grid; place-items: center; height: 26px;
  font: var(--f-label-s); color: var(--on-surface-variant);
}
.dp-day {
  display: grid; place-items: center; aspect-ratio: 1; cursor: pointer;
  border: 0; border-radius: var(--shape-chrome); background: none;
  color: var(--on-surface); font: var(--f-body-m); font-variant-numeric: tabular-nums;
}
.dp-day:hover { background-image: var(--layer-hover); }
.dp-day:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; }
.dp-day.is-outside { color: var(--on-surface-muted); }
/* Today is a ring, not a fill. A filled today competes with the day actually
   chosen, and the chosen one is the answer to the question being asked. */
.dp-day.is-today { box-shadow: inset 0 0 0 1px var(--outline); }
/* **A range is one shape, so its ends are rounded outward only**, at Cameron's
   ask 2026-08-27. The start rounds on the left, the finish on the right, and
   the days between are square — which is what makes five separate cells read as
   one bar rather than five chosen days.

   A day that is both start and finish rounds on both, and that rule has to be
   last or the two above would each half-apply to it. */
.dp-day.is-between { background: var(--primary-container); border-radius: var(--shape-none); }
.dp-day.is-start, .dp-day.is-due { background: var(--primary); color: var(--on-primary); }
.dp-day.is-start {
  border-radius: var(--shape-chrome) var(--shape-none) var(--shape-none) var(--shape-chrome);
}
.dp-day.is-due {
  border-radius: var(--shape-none) var(--shape-chrome) var(--shape-chrome) var(--shape-none);
}
/* One day is the whole range. Both ends, so it rounds all four corners. */
.dp-day.is-start.is-due { border-radius: var(--shape-chrome); }
/* And a start with no finish yet is not a range, so it is not half a bar. */
.dp-day.is-start:not(.is-due):last-of-type { border-radius: var(--shape-chrome); }

.dp-foot { display: flex; gap: var(--s-2); }
.dp-foot .btn { flex: 1; justify-content: center; }

/* ------------------------------------------- the choosers, on a phone
 *
 * Bottom sheets, for the reason the compose sheet's own note gives: these are
 * typed into, and a box centred in the viewport ends up centred behind the
 * software keyboard. **Last in the file**, after the definitions above. */
@media (max-width: 720px) {
  .ch-scrim { padding: 0; align-items: flex-end; }
  .ch {
    width: 100%; max-height: 88%;
    border-radius: var(--shape-l) var(--shape-l) var(--shape-none) var(--shape-none);
    border-left: 0; border-right: 0; border-bottom: 0;
    padding-bottom: var(--s-5);
  }
  .dp { width: 100%; }
}

/* ============================================================ label rows
 *
 * **The swatches and the recolour button were here.** A label wore one of eight
 * hues and this styled the picker that chose it; both are gone, and the style
 * gate is what said so — four selectors matching nothing, which is exactly what
 * a rule for a control nobody renders looks like.
 *
 * The row is still a flex container rather than a plain button: it was two
 * controls, and leaving the layout in place costs nothing while the toggle
 * inside it keeps its own hit area. */
.ch-row.ch-label { display: flex; flex-wrap: wrap; padding: 0; }
.ch-labelmain {
  display: flex; align-items: center; gap: var(--s-3); flex: 1; min-width: 0;
  min-height: var(--ctl-h); padding: var(--s-2); cursor: pointer; text-align: left;
  border: 0; border-radius: var(--shape-s); background: none; color: inherit;
}
.ch-labelmain:hover { background-image: var(--layer-hover); }
.ch-labelmain:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; }
/* The row that makes a label rather than finding one. First in the list,
   because that is where somebody who typed a new name is looking. */
.ch-make {
  display: flex; align-items: center; gap: var(--s-3); width: 100%;
  min-height: var(--ctl-h); padding: var(--s-2); cursor: pointer; text-align: left;
  border: 0; border-radius: var(--shape-s); background: none;
  color: var(--primary-ink);
}
.ch-make:hover { background-image: var(--layer-hover); }
.ch-make:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; }

/* ------------------------------------------------------- several assignees
 *
 * Faces, once anybody is on it: Cameron, 2026-08-27 — *after selecting somebody
 * as an assignee it should just show the circle with their profile picture.*
 * The overlap is deliberate; a row of separated squares reads as a list of
 * people and a slight overlap reads as a group. */
.cp-faces { display: flex; align-items: center; }
.cp-faces .pv + .pv { margin-left: -6px; box-shadow: 0 0 0 2px var(--surface-container); }
.pl-avatars { display: inline-flex; align-items: center; }
.pl-avatars .pl-avatar + .pl-avatar { margin-left: -5px; box-shadow: 0 0 0 2px var(--surface-card); }
.pl-avatar.more { background: var(--surface-container-highest); color: var(--on-surface-variant); }

/* ------------------------------------------- reminders and notes, as screens
 *
 * Plain on purpose. Giving either a board, a grouping and four views would be
 * building the task list again for records that have none of the machinery
 * that needs it, and the second copy would be the one that goes stale. */
.rm, .nt { display: flex; flex-direction: column; gap: var(--s-2); padding: var(--s-4); }
.rm-row {
  display: flex; align-items: center; gap: var(--s-3);
  min-height: var(--ctl-h); padding: var(--s-2) 0;
  border-bottom: 1px solid var(--outline-variant); font: var(--f-body-m);
}
.rm-row:last-child { border-bottom: 0; }
.rm-row.done .rm-title { color: var(--on-surface-muted); text-decoration: line-through; }
.rm-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rm-at { font: var(--f-label-m); color: var(--on-surface-variant); font-variant-numeric: tabular-nums; }
.rm-at.late { color: var(--bad); }
.rm-el {
  border: 0; background: none; cursor: pointer; padding: 0;
  font: var(--f-label-m); color: var(--primary-ink); max-width: 12ch;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.rm-el:hover { text-decoration: underline; }
.rm-empty { padding: var(--s-6) var(--s-4); text-align: center; color: var(--on-surface-variant); }
.rm-empty p { margin: 0 0 var(--s-2); font: var(--f-body-m); }
.nt-card {
  padding: var(--s-3); border: 1px solid var(--outline-variant);
  border-radius: var(--shape-m); background: var(--surface-container-low);
  display: flex; flex-direction: column; gap: var(--s-2);
}
.nt-title { margin: 0; font: var(--f-title-m); }
.nt-body { margin: 0; font: var(--f-body-m); color: var(--on-surface-variant); white-space: pre-wrap; }

/* --------------------------------------------------------- the action row
 *
 * `flex-wrap: nowrap`, because it was wrapping to three lines at 375px and
 * standing 99px tall. The tools scroll instead — the Create button is the one
 * thing on this row that must never be pushed off it. */
.cp-foot { flex-wrap: nowrap; }
.cp-tools { overflow-x: auto; scrollbar-width: none; min-width: 0; }
.cp-tools::-webkit-scrollbar { display: none; }

/* A label is a word.
 *
 * It wore one of eight hues, assigned in the order labels happened to be
 * created — so on a row already carrying a reference, a title, an assignee, a
 * priority and a due date, the colour was the loudest thing on it and the least
 * informative. What distinguishes `test` from `procurement` is that they are
 * different words. */

/* ===================================================== the critical path
 *
 * Red is the accent for *cannot slip*, and it is used nowhere else on the
 * chart. The status tones already colour the bar — a critical task can be in
 * progress, blocked or in review — so criticality is carried by the outline and
 * the dot rather than by replacing the fill, which would make the two facts
 * compete for the same pixel and lose one of them.
 *
 * The float chip is a number because the colour cannot be one. Red says *this
 * cannot slip*; it does not say the next one has three days, and "3d" is the
 * thing that changes what somebody does on a Tuesday. */

.pl-float {
  font: var(--f-label-s); color: var(--on-surface-muted); font-variant-numeric: tabular-nums;
  text-align: right; min-width: 5ch; padding-left: var(--s-2);
}
.pl-float.none { color: var(--bad); font-weight: var(--w-bold); }
/* Already impossible, which is a different statement from "cannot slip". */
.pl-float.late { color: var(--on-bad-container); background: var(--bad-container);
  border-radius: var(--shape-xs); padding: 1px var(--s-2); }

/* A ring in the network. Said out loud rather than by quietly omitting half the
   plan from a chart that still looks complete. */
.pl-cycle {
  margin: 0 0 var(--s-3); padding: var(--s-3);
  border: 1px solid var(--bad); border-radius: var(--shape-m);
  background: var(--bad-container); color: var(--on-bad-container);
  font: var(--f-body-s);
}

/* ======================================================= load, and sprints
 *
 * # Why load is a bar per day
 *
 * "Bo is at 140%" is true and useless: over what, and when? Over-commitment is
 * a shape in time — three bad days in a fortnight is a conversation, three bad
 * weeks is a plan that does not work. One scale across everybody, so two
 * people's rows can be compared; per-person scaling makes the busiest day of a
 * quiet week look like the busiest day of a terrible one. */
.ld { display: grid; gap: var(--s-4); padding: var(--s-3); }
.ld-person { display: grid; gap: var(--s-2); }
.ld-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--s-3); }
.ld-who { font: var(--f-title-s); color: var(--on-surface); }
.ld-peak { font: var(--f-label-s); color: var(--on-surface-muted); font-variant-numeric: tabular-nums; }
.ld-peak.over { color: var(--on-bad-container); background: var(--bad-container);
  border-radius: var(--shape-xs); padding: 1px var(--s-2); }
.ld-person > .plot-spark { border-bottom: 1px solid var(--outline-variant); }

.sp { display: grid; gap: var(--s-3); padding: var(--s-3); }
.sp-card {
  border: 1px solid var(--outline-variant); border-radius: var(--shape-l);
  background: var(--surface-container-low); padding: var(--s-3); display: grid; gap: var(--s-2);
}
/* The sprint you are in, marked once. A card per sprint all shouting equally is
   how somebody loses track of which fortnight it currently is. */
.sp-card.current { border-color: var(--primary); }
/* **Not `opacity`.** Dimming the card multiplies the contrast of every word in
   it — the a11y gate measured a 4.7:1 pair falling to 3.4:1, which no token
   check can see because both tokens are still correct. A finished sprint should
   read as finished, not as harder to read: it loses its fill and keeps its ink. */
.sp-card.past { background: none; border-style: dashed; }
.sp-card.past .sp-name { color: var(--on-surface-variant); }
.sp-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--s-3); flex-wrap: wrap; }
.sp-name { font: var(--f-title-s); color: var(--on-surface); }
.sp-when { font: var(--f-label-s); color: var(--on-surface-muted); font-variant-numeric: tabular-nums; }
.sp-goal { margin: 0; font: var(--f-body-s); color: var(--on-surface-variant); }
.sp-bar {
  position: relative; height: 8px; border-radius: var(--shape-xs);
  background: var(--surface-container-high); overflow: visible;
}
.sp-done { position: absolute; inset: 0 auto 0 0; background: var(--ok); border-radius: var(--shape-xs); }
/* Capacity is a line, not a colour: it says where the edge is without claiming
   anything about whether crossing it is bad in this particular fortnight. */
.sp-cap { position: absolute; top: -3px; bottom: -3px; width: 2px; background: var(--on-surface); }
.sp-figures { margin: 0; font: var(--f-label-m); color: var(--on-surface-variant); font-variant-numeric: tabular-nums; }
.sp-over { color: var(--on-bad-container); background: var(--bad-container);
  border-radius: var(--shape-xs); padding: 1px var(--s-2); margin-left: var(--s-2); }
.sp-nocap { color: var(--on-surface-muted); margin-left: var(--s-2); font-style: italic; }
.sp-tasks { margin: 0; padding: 0; list-style: none; display: grid; gap: var(--s-05); }
.sp-tasks li { font: var(--f-body-s); color: var(--on-surface-variant); }
.sp-tasks li.done { text-decoration: line-through; color: var(--on-surface-muted); }
.sp-tasks li.more { color: var(--on-surface-muted); font-style: italic; }
.sp-card.backlog { border-style: dashed; background: none; }

/* The peek panel's people row: the faces, then who they are in words.
   Both, because an avatar answers "is this mine" at a glance and a name answers
   "who do I ask", and a panel that has room for one line has room for both. */
.pl-peek-people { display: flex; align-items: center; gap: var(--s-2); margin: var(--s-2) 0 var(--s-3); }
.pl-peek-who { font: var(--f-body-s); color: var(--on-surface-variant); min-width: 0; }

/* ================================================= earned value, and rescheduling
 *
 * The indices are the headline because they are the only figures with the
 * judgement already made: "840,000 spent" is a fact nobody can act on, and
 * "CPI 0.87" is the same information saying the work costs 15% more than it was
 * budgeted to. Under 1 is bad for both, so one tone rule colours both. */
.ev { display: grid; gap: var(--s-4); padding: var(--s-3); }
.ev-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--s-3); flex-wrap: wrap; }
.ev-baseline { font: var(--f-title-s); color: var(--on-surface); }
.ev-asof { font: var(--f-label-s); color: var(--on-surface-muted); }
.ev-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: var(--s-3); }
.ev-card {
  display: grid; gap: var(--s-1); padding: var(--s-3);
  border: 1px solid var(--outline-variant); border-radius: var(--shape-l);
  background: var(--surface-container-low);
}
.ev-label { font: var(--f-label-s); letter-spacing: var(--k-label); color: var(--on-surface-muted); }
.ev-value { font: var(--f-headline-s); font-variant-numeric: tabular-nums; color: var(--on-surface); }
.ev-hint { font: var(--f-label-s); color: var(--on-surface-muted); }
.ev-value .good { color: var(--ok); }
.ev-value .warn { color: var(--warn); }
.ev-value .bad { color: var(--bad); }
.ev-grid {
  display: grid; grid-template-columns: 1fr auto; gap: var(--s-1) var(--s-4); margin: 0;
  font: var(--f-body-s);
}
.ev-grid dt { color: var(--on-surface-variant); }
.ev-grid dd { margin: 0; color: var(--on-surface); font-variant-numeric: tabular-nums; text-align: right; }
.ev-grid dd.bad { color: var(--bad); }
.ev-block { display: grid; gap: var(--s-2); }
.ev-block h4 { margin: 0; font: var(--f-label-l); letter-spacing: var(--k-label); color: var(--on-surface-muted); }
.ev-note { margin: 0; font: var(--f-body-s); color: var(--on-surface-variant); }
/* Level of effort cannot report a schedule variance, so a baseline heavy in it
   is reporting on less than it looks. Said out loud beside the number. */
.ev-warn {
  margin: 0; padding: var(--s-3); border-radius: var(--shape-m);
  border: 1px solid var(--warn); background: var(--warn-container);
  color: var(--on-warn-container); font: var(--f-body-s);
}
.ev-table { width: 100%; border-collapse: collapse; font: var(--f-body-s); }
.ev-table th {
  text-align: right; font: var(--f-label-s); letter-spacing: var(--k-label);
 color: var(--on-surface-muted); padding: var(--s-2);
}
.ev-table th:first-child, .ev-table td:first-child { text-align: left; }
.ev-table td { padding: var(--s-2); text-align: right; font-variant-numeric: tabular-nums;
  border-top: 1px solid var(--outline-variant); color: var(--on-surface); }
.ev-table tr.child td:first-child { padding-left: var(--s-5); color: var(--on-surface-variant); }
.ev-table td.good { color: var(--ok); }
.ev-table td.warn { color: var(--warn); }
.ev-table td.bad { color: var(--bad); }

/* The reschedule preview. Every row is a date somebody may have committed to,
   so the old value stays beside the new one — that is the difference between a
   preview and an announcement. */
.rs { display: grid; gap: var(--s-3); }
.rs-head { margin: 0; font: var(--f-title-s); color: var(--on-surface); }
.rs-count { margin: 0; font: var(--f-body-m); color: var(--on-surface-variant); }
.rs-none { margin: 0; font: var(--f-body-m); color: var(--on-surface); }
.rs-list { margin: 0; padding: 0; list-style: none; display: grid; gap: var(--s-2); max-height: 46vh; overflow: auto; }
.rs-row {
  display: grid; grid-template-columns: 1fr auto auto auto; gap: var(--s-2);
  align-items: baseline; font: var(--f-body-s); font-variant-numeric: tabular-nums;
}
.rs-title { color: var(--on-surface); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rs-from { color: var(--on-surface-muted); text-decoration: line-through; }
.rs-arrow { color: var(--on-surface-muted); }
.rs-to { color: var(--primary); }
.rs-foot { display: flex; justify-content: flex-end; gap: var(--s-2); }

/* A button that stands in for a field.

   The milestone date is chosen with the calendar rather than typed, so the
   control is a button — but it sits in a form beside real fields, and a button
   that looks like a button there reads as an action rather than as the value it
   is showing. It takes the field treatment and keeps the button behaviour. */
.field-btn {
  width: 100%; justify-content: flex-start; text-align: left;
  background: var(--surface-container-low); border-color: var(--outline);
  color: var(--on-surface); font: var(--f-body-l);
}
.field-btn:hover { border-color: var(--on-surface-variant); }

/* ==================================================================== the Gantt
 *
 * Task 0149. Layout only — **no new container**, deliberately.
 *
 * `docs/components.md` §0 counts the rules that declare padding, a radius and a
 * background together, and a chart is the classic place a twelfth kind of box
 * gets invented on the grounds that a chart is obviously its own thing. Nothing
 * below declares all three. The panel is `.panel`, its header `.panel-head`,
 * its scroller `.panel-body`, its rows `.row` and the impact list `.field`.
 *
 * Rows are `--h-row` from the primitive, so the task list and the timeline line
 * up without either knowing the other's height. The tree and its virtualiser
 * disagreeing by 8px is a bug this repository has already shipped once.
 */

/* One column per working day. A zoom control changes this token and nothing
   else — every bar is positioned in column units, not pixels. */
.gantt { --gantt-col: 18px; --gantt-names: 200px; }

.gantt-months { display: grid; grid-auto-flow: column; grid-auto-columns: max-content;
  margin-left: var(--gantt-names); font: var(--f-label-s); color: var(--on-surface-muted);
  letter-spacing: var(--k-label); position: sticky; top: 0; }
.gantt-months > span { width: calc(var(--span, 1) * var(--gantt-col)); overflow: hidden;
  border-left: 1px solid var(--outline-variant); }

.gantt-rows { min-width: max-content; }
.gantt-row { gap: 0; align-items: center; }
.gantt-name { width: var(--gantt-names); flex: none; font: var(--f-body-s);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* The track is the row's own width in columns; bars sit in it absolutely so a
   bar's position costs no layout when it moves. */
.gantt-track { position: relative; flex: 1 1 auto; height: 100%;
  width: calc(var(--cols, 1) * var(--gantt-col)); }

/* Bars, float tails and overrun segments all sit in the track and are all
   placed the same way. `.gantt-over` was added as its own rule and inherited
   none of this, so it rendered as a 1px inline sliver — visible in the DOM,
   invisible on the chart, and reporting four of them the whole time. */
.gantt-bar, .gantt-float, .gantt-over {
  position: absolute; top: 50%; translate: 0 -50%;
  /* Fallbacks are load-bearing, not gate appeasement: with none, a row whose
     inline style failed to render drops the whole declaration and every bar
     stacks at column zero — which looks like a layout bug, not a missing value. */
  left: calc(var(--col, 0) * var(--gantt-col));
  width: calc(var(--span, 1) * var(--gantt-col));
}
.gantt-bar { height: 14px; border-radius: var(--shape-xs);
  background: var(--on-surface-variant); display: flex; align-items: center; }
.gantt-bar[data-state="critical"] { background: var(--on-surface); }

/* A bar can be picked up, and says so before anybody tries: the same `grab`
   the architecture diagram uses for its nodes. */
.gantt-bar { cursor: grab; touch-action: none; }

/* The one being held, marked the way a held row is (`.pl-dragging`), so it
   reads as raised against the bars it is passing rather than only moving. */
.gantt-bar[data-dragging] { cursor: grabbing; box-shadow: var(--e-2);
  outline: 1px dashed var(--outline); outline-offset: 1px; }

/* **The non-colour carrier.** A notch on the critical path and a tail on
   everything else, so the two differ in shape before they differ in hue. About
   8% of men cannot separate the usual red from the bars beside it, and
   this is the most consequential readout on the screen. */
.gantt-notch { width: 3px; height: 100%; margin-left: var(--s-1); flex: none;
  background: var(--surface); }
.gantt-pin { width: 5px; height: 5px; margin-left: auto; margin-right: var(--s-1); flex: none;
  background: var(--surface); rotate: 45deg; }

/* Float: how far this could slide before the programme finishes later. Drawn,
   because a chart that knows the float and hides it sends somebody to another
   screen for the thing they came for. */
.gantt-float { height: 3px; background: var(--outline-variant); }

/* Dependency lines. One SVG over the track area of every row, drawn in columns
   and rows and stretched to the chart, so a line cannot drift from the bars it
   joins. Above the bars so it is never hidden behind one, and transparent to
   the pointer except along the strokes, so every bar stays draggable. */
.gantt-rows { position: relative; }
.gantt-links { position: absolute; top: 0; z-index: 1; overflow: visible; pointer-events: none;
  left: calc(var(--s-3) + var(--gantt-names));
  width: calc(var(--cols, 1) * var(--gantt-col)); height: calc(var(--rows, 1) * var(--h-row)); }
/* The ordinary line is the same grade as the grid and the float tail beside
   it; `--outline` on this chart is the one singled-out line, today. */
.gantt-link-line { fill: none; stroke: var(--outline-variant); vector-effect: non-scaling-stroke;
  stroke-dasharray: var(--s-1) var(--s-1); }
/* The link carrying the finish date is solid and darker; every other is
   dashed. Shape before hue, for the same reason the critical bar has a notch. */
.gantt-link[data-critical="true"] .gantt-link-line { stroke: var(--on-surface); stroke-dasharray: none; }
/* The part you click: wide, invisible, and only along the line. */
.gantt-link-hit { fill: none; stroke: transparent; stroke-width: var(--s-3);
  vector-effect: non-scaling-stroke; pointer-events: stroke; cursor: pointer; }
/* Hover carries no information, so it is not the brand colour: it steps up to
   the text grade, as the architecture diagram's handles do. */
.gantt-link:hover .gantt-link-line { stroke: var(--on-surface); }

/* Where a link is drawn from: the bar's finish. Shown when the row is
   pointed at or focused, so four hundred rows are not four hundred dots. */
.gantt-link-handle { position: absolute; right: calc(-1 * var(--s-2)); width: var(--s-3); height: var(--s-3);
  border-radius: var(--shape-full); background: var(--surface-container-high); border: 1px solid var(--outline);
  cursor: alias; opacity: 0; }
.gantt-row:hover .gantt-link-handle,
.gantt-row:focus-within .gantt-link-handle { opacity: 1; }
.gantt-link-says { margin: 0 0 var(--s-3); font: var(--f-body-m); color: var(--on-surface); }

/* Drawing a link from the keyboard: the task it is drawn from is held in the
   selected state, and the line above the chart says what to do next. */
.gantt-row[data-link-source] { background-image: var(--layer-select); }
.gantt-linking { margin: 0 0 var(--s-2); font: var(--f-label-m); color: var(--on-surface); }

/* The impact panel sits over the chart rather than beside it: the question it
   answers is about the bar somebody is holding, and putting the answer at the
   other end of the screen makes them look away from the thing they moved. */
/* The impact is a sheet (`.sheet-modal.narrow`), so it takes its place and its
   elevation from there rather than pinning itself to a corner of a scrolling
   chart, which put it off the screen on any plan longer than one. */
.gantt-impact .field + .field { margin-top: var(--s-1); }
.gantt-keys { margin: var(--s-3) 0 0; font: var(--f-label-m); }
.gantt-breach > label { color: var(--bad); }

.gantt-cycle { color: var(--on-surface); font: var(--f-body-m); }
.gantt-breach-head { font: var(--f-label-l); letter-spacing: var(--k-label);
 color: var(--on-surface-muted); margin: 0 0 var(--s-2); }
.gantt-newly-critical { font: var(--f-label-m); color: var(--warn); }
.gantt-move[data-became-critical="true"] > label { font-weight: var(--w-bold); }
.gantt-actions { display: flex; justify-content: flex-end; gap: var(--s-2); margin-top: var(--s-4); }

@media (prefers-reduced-motion: no-preference) {
  .gantt-bar { transition: left var(--t-move) var(--ease-out); }
}

/* ============================================ the list, closer to the reference
 *
 * Cameron's linear.app screenshots, 2026-09-04. Four changes, each one a rule
 * rather than a resemblance.
 *
 * **Status is a ring whose fill is the state.** Empty and dashed for to-do, a
 * quarter for in progress, three-quarters for review, a bar for blocked, a tick
 * for done. Shape first and colour second, which is the kind-diamond rule
 * applied to the other column that matters.
 *
 * **Priority is four ascending bars**, not the word "urgent". A word costs the
 * width of a word in every row and reads at the same weight as the title beside
 * it; bars are comparable down a column at a glance, which is the only question
 * anybody asks of a priority.
 *
 * **A label is a dot and a word on a quiet ground**, never coloured text. The
 * hue identifies the label and the text stays legible, so a label palette can
 * be arbitrary without any of it having to clear a contrast bar.
 *
 * **The list has no column headers.** They were a table-ism: seven words across
 * the top of every group, costing a row each time, naming columns whose content
 * is self-evident. The reference has none.
 */

/* Keyed on the tone, not the id, because it was keyed on the id and disagreed
   with `STATUSES` about what the ids mean: `review` was painted green here and
   called informational there, `doing` was amber here and informational there.
   One vocabulary now, and only the two states worth interrupting for have a
   colour — blocked, which is work that has stopped, and done. */
.pl-state { display: inline-flex; color: var(--on-surface-muted); }
.pl-state[data-tone="bad"] { color: var(--bad); }
.pl-state[data-tone="ok"] { color: var(--ok); }
.pl-state[data-tone="info"] { color: var(--interactive); }
.pl-state[data-status="review"] { color: var(--warn); }

.pl-prio { display: inline-flex; align-items: center; gap: var(--s-1); color: var(--on-surface-muted); }
.pl-prio-name { font: var(--f-label-m); color: var(--on-surface-variant); white-space: nowrap; }

/* The bar. Sized in ch rather than px so it tracks the label type beside it,
   and because the ratchet counts px and this column does not deserve five of
   the budget. */
/* One bar, cut into five, rather than five bars in a row.
   Cameron, 2026-09-13: *urgency bars should be vertical slices of a horizontal
   bar to make them more compact.* They were separate rounded pills with a 2px
   gap, which reads as five things; a bar reads as one measurement.
   The container carries the unfilled colour and the slices are transparent, so
   the hairline between them is the container showing through. That means the
   cuts appear inside the filled run -- where they do the counting -- and the
   unfilled tail stays a single quiet bar. */
.pl-prio-bar {
  display: inline-flex;
  align-items: center;
  gap: var(--hair);
  background: var(--outline-variant);
  border-radius: var(--shape-hair);
  overflow: hidden;
}
.pl-prio-bar > span {
  /* **Portrait, not landscape.** Cameron, 2026-09-13: *do you see how each bar
     is landscape? I want it portrait.* They were 1.2ch by 6px — wider than
     tall, so five of them read as five stacked blocks rather than as cuts
     across one bar. A slice taller than it is wide reads as a division; a slice
     wider than it is tall reads as a brick.

     Width in ch so it tracks the label type beside it and the ratchet keeps its
     px budget for things that need it; height on the spacing scale. */
  width: 0.55ch;
  height: var(--s-25);
  background: transparent;
}

/* Filled to the level, in the level's colour. The fill grade, not the text
   grade: a slice is an area, so it needs 3:1 and gets to keep more of the
   published hue. */
.pl-prio-bar[data-level="1"] > span.on { background: var(--accent-slate-fill); }
.pl-prio-bar[data-level="2"] > span.on { background: var(--accent-seagrass-fill); }
.pl-prio-bar[data-level="3"] > span.on { background: var(--accent-orange-fill); }
.pl-prio-bar[data-level="4"] > span.on { background: var(--accent-red-fill); }
.pl-prio-bar[data-level="5"] > span.on { background: var(--accent-violet-fill); }

/* The dot that carries the label's hue, so the word never has to. */
.pl-label { display: inline-flex; align-items: center; }

/* The first run. One offer, and the reason for it. */
.pl-firstrun { display: grid; gap: var(--s-3); justify-items: center; text-align: center; }
.pl-firstrun-note { max-width: 46ch; color: var(--on-surface-variant); font: var(--f-body-m); }

/* The sayable reference, `TVQ-14`.
 *
 * Muted and tabular, always to the left of the title. It is how somebody names
 * this task in a commit message or across a table, so it has to be findable at
 * a glance and it must never compete with the title for attention — the number
 * identifies the row, the title is what the row says. */
.pl-ref {
  /* Shorthand first. `font` resets `font-variant`, so the other order silently
     un-aligns the digits — which is the whole reason to set it on a reference
     people scan down a column. The style gate caught this on the way in. */
  font: var(--f-label-l); font-variant-numeric: tabular-nums;
  letter-spacing: var(--k-label); color: var(--on-surface-muted);
  margin-right: var(--s-2); flex: none;
}
.pl-card .pl-ref { display: block; margin: 0 0 var(--s-05); }

/* The sprint a task sits in: a label that leads to the schedule, not a button
   into a sprint screen inside the task list. Quiet, because it is context
   rather than the subject of the row — the title is the subject. */
.pl-sprint {
  margin-left: var(--s-15); padding: 0 var(--s-1);
  font: var(--f-label-m); letter-spacing: var(--k-label);
  color: var(--on-surface-muted); background: none;
  border: 1px solid var(--outline-variant); border-radius: var(--shape-xs);
  cursor: pointer;
}
.pl-sprint:hover { color: var(--on-surface); border-color: var(--outline); }

/* ------------------------------------------------- the timeline's ruler and lane
 *
 * Cameron's second reference screenshot: a month band, a week ruler under it,
 * dashed gridlines at the same ticks, and milestones as diamonds with their
 * names beneath.
 *
 * Everything is positioned in column units from the same `--gantt-col` the bars
 * use, so the ruler cannot drift from the thing it measures — which is the
 * failure that makes a chart untrustworthy rather than merely ugly.
 */

.gantt-weeks { position: relative; height: var(--s-4); margin-left: var(--gantt-names);
  width: calc(var(--cols, 1) * var(--gantt-col)); }
.gantt-weeks > span {
  position: absolute; left: calc(var(--col, 0) * var(--gantt-col));
  font: var(--f-label-s); font-variant-numeric: tabular-nums;
  color: var(--on-surface-muted); translate: -50% 0;
}

/* The plot: gridlines behind, marks and rows above. */
.gantt-plot { position: relative; }

.gantt-grid { position: absolute; inset: 0 0 0 var(--gantt-names); pointer-events: none; }
.gantt-grid > i {
  position: absolute; top: 0; bottom: 0; width: 0;
  left: calc(var(--col, 0) * var(--gantt-col));
  border-left: 1px dashed var(--outline-variant);
}
/* Today is a solid line in the ink, not a dashed one in a hue: it is the only
   line on the chart that is not a week, so it should not read as one. */
/* Today. A solid line in a stronger neutral than the dashed week lines, so it
   reads as the one line that is not a week.

   **No `opacity`.** The first version dimmed it to .55 and the accessibility
   gate refused: opacity multiplies the contrast of everything inside an
   element, and a rule that is fine on an empty `<i>` today is a trap the moment
   somebody puts a label in it. A token says what colour it is; an opacity says
   what fraction of some other colour it is, and only one of those can be
   checked. */
.gantt-grid > i.gantt-now { border-left: 1px solid var(--outline); }

.gantt-marks { position: relative; height: var(--s-8); margin-left: var(--gantt-names);
  width: calc(var(--cols, 1) * var(--gantt-col)); }
.gantt-mark {
  position: absolute; left: calc(var(--col, 0) * var(--gantt-col));
  display: grid; justify-items: center; gap: var(--s-05); translate: -50% 0;
}
.gantt-mark > i {
  width: 9px; height: 9px; rotate: 45deg;
  background: var(--on-surface-variant);
}
.gantt-mark > b {
  font: var(--f-label-m); color: var(--on-surface-muted); white-space: nowrap;
}
/* A missed milestone is hollow and dashed as well as coloured, so the state
   survives without the hue — the critical path's rule, applied again. */
.gantt-mark[data-missed="true"] > i { background: none; border: 1.5px dashed var(--bad); rotate: 45deg; }
.gantt-mark[data-missed="true"] > b { color: var(--bad); }

/* How far past its planned finish a task has run.
 *
 * The bar says where the work was meant to stop; this says where it has got to,
 * which is the difference between a chart of a plan and a chart of a programme.
 *
 * **Dashed as well as coloured**, so the state survives with the hue removed —
 * the same rule as the critical path's notch and the missed milestone's hollow
 * diamond. A red bar and a grey bar are one signal; a dashed outline and a
 * solid fill are two. */
.gantt-over {
  height: 10px; border: 1px dashed var(--bad); border-left: 0;
  border-top-right-radius: var(--shape-xs); border-bottom-right-radius: var(--shape-xs);
  background: color-mix(in srgb, var(--bad) 14%, transparent);
}

/* The progress ring.
 *
 * **Never only a ring.** An arc is a shape, and nobody reads a number off one
 * to better than about a quarter — so the count sits beside it always. The ring
 * is for scanning a column of them; the digits are for knowing. */
.pg { display: inline-flex; align-items: center; gap: var(--s-15); }
.pg-ring { flex: none; display: block; }
.pg-track { stroke: var(--outline-variant); }
.pg-arc { stroke: var(--on-surface); stroke-linecap: round; }
.pg-count {
  font: var(--f-label-l); font-variant-numeric: tabular-nums;
  color: var(--on-surface); letter-spacing: var(--k-label);
}
.pg-of { color: var(--on-surface-muted); font-weight: var(--w-body); }
.pg-label { font: var(--f-label-m); color: var(--on-surface-muted); }

/* When counting tasks and counting work disagree.
 *
 * A word, not a colour and not a shape. The disagreement is about the *number*,
 * not about the programme — drawing it as a severity would make it look like a
 * warning that the work is in trouble, which is a different claim entirely. */
.pg-split {
  font: var(--f-label-m); font-style: normal;
  color: var(--on-surface-muted); white-space: nowrap;
}

/* ------------------------------------------------- gates, scopes and the mode
 *
 * One list whichever discipline is running. There is no agile screen and no
 * waterfall screen — two of those would be two implementations of the same ring
 * and would drift the first time somebody fixed a bug in one.
 */

.pl-mode { display: flex; align-items: center; gap: var(--s-1); flex-wrap: wrap;
  margin-bottom: var(--s-3); }
.pl-mode-opt {
  font: var(--f-label-l); letter-spacing: var(--k-label);
  padding: var(--pad-tag); border: 1px solid var(--outline-variant);
  border-radius: var(--shape-chrome); background: none; color: var(--on-surface-variant);
  cursor: pointer;
}
.pl-mode-opt:hover { background-image: var(--layer-hover); }
.pl-mode-opt[aria-pressed="true"] {
  background: var(--selected); color: var(--on-surface); border-color: var(--outline);
}
/* What the mode *asks*. "Waterfall" and "Agile" are tribal words that mean
   different things in different companies; the question is the real difference,
   and a team choosing between them should be choosing between those. */
.pl-mode-says { font: var(--f-body-s); color: var(--on-surface-muted); margin-left: var(--s-2); }

/* The panel is scoped rather than global: a second rollup list elsewhere should
   not inherit this one's width by accident. */
.pl-rollups { max-width: 720px; }
.pl-roll-list { display: grid; }
.pl-roll { gap: var(--s-2); }
/* The size is the mark's own now. It was `font-size`, which set a character's
   size and does nothing for an SVG that carries its own width. */
.pl-roll-kind { flex: none; color: var(--on-surface-muted); display: grid; place-items: center; }
.pl-roll-name { flex: 1 1 auto; font: var(--f-body-m); overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; }
/* A gate's date is a commitment and a scope's is an intention. The difference
   is the whole distinction between the two disciplines, so it is weight and
   colour rather than a column header somebody has to remember. */
.pl-roll-when { font: var(--f-label-l); font-variant-numeric: tabular-nums;
  color: var(--on-surface-variant); flex: none; white-space: nowrap; }
/* On a phone the row ran out of width and the date was the thing that lost:
   it clipped to "2026-08-." with the interesting half off-screen. The name is
   what should give way — it already ellipsises, and a truncated name is still
   recognisable where half a date is not. */
/* Wrapping needs somewhere to wrap *to*.
 *
 * `.row` is a fixed `height`, and `min-width: max-content` on top of it — both
 * right for a table row and both wrong the moment this one is told to wrap. The
 * second line had nowhere to go and painted over the row beneath: Cameron
 * photographed a progress ring sitting across the words "Critical design
 * review". The max-content floor was the other half, holding the row wider than
 * the phone so the dates left the screen sideways instead of wrapping.
 *
 * So the height becomes a floor rather than a measurement, and the row is
 * allowed to be as narrow as the panel it is in. */
@media (max-width: 560px) {
  .pl-roll {
    flex-wrap: wrap; min-width: 0;
    height: auto; min-height: var(--h-row); padding-block: var(--s-2);
  }
  .pl-roll-name { flex: 1 1 60%; min-width: 0; }
  .pl-roll-when { margin-left: auto; }
}
.pl-roll-when[data-soft="true"] { color: var(--on-surface-muted); font-style: italic; }
.pl-roll-convert { font: var(--f-label-m); padding: var(--pad-tag); flex: none;
  border: 1px solid transparent; border-radius: var(--shape-chrome);
  background: none; color: var(--on-surface-muted); cursor: pointer; opacity: 0; }
.pl-roll:hover .pl-roll-convert, .pl-roll-convert:focus-visible {
  opacity: 1; border-color: var(--outline-variant); color: var(--on-surface-variant); }

.pl-unfiled { margin: var(--s-3) 0 0; font: var(--f-body-s); color: var(--on-surface-muted); }

/* The conversion dialog. The warnings are the point of it, not decoration on
   it — so they sit above the list and the confirm sits under them. */
.pl-convert { max-width: 460px; }
.pl-convert-warnings { margin: 0 0 var(--s-3); padding-left: var(--s-4);
  font: var(--f-body-s); color: var(--on-surface-variant); display: grid; gap: var(--s-2); }
.pl-convert-count { margin: 0 0 var(--s-2); font: var(--f-label-l);
  letter-spacing: var(--k-label); color: var(--on-surface-muted); }
.pl-convert-list { max-height: 200px; overflow: auto; }
.pl-convert-move > label { font-variant-numeric: tabular-nums; }
.pl-convert-acts { border-bottom: 0; border-top: 1px solid var(--outline-variant);
  justify-content: flex-end; }

/* ============================================================== scrollbars
 *
 * Narrow, floating, no arrows — the same bar on every surface.
 *
 * The platform default is a fifteen-pixel channel with a stepper at each end,
 * drawn in the operating system's colours rather than the product's. On a dark
 * interface it is the brightest thing on screen, it reserves a column of layout
 * whether or not anything scrolls, and it looks like a different application
 * every time somebody opens the page on a different machine.
 *
 * **This is design language rather than a tweak**, so it is set once for
 * everything that scrolls rather than per panel. A scrollbar somebody notices
 * is a scrollbar that has failed.
 *
 * The thumb is drawn inside a transparent border with `background-clip`, which
 * is the only way to get a *padded* thumb: the bar keeps a comfortable hit
 * target while the visible mark stays thin. It thickens on hover, so it is
 * findable when somebody goes looking for it and quiet when they do not.
 */
* {
  scrollbar-width: thin;
  scrollbar-color: var(--outline-variant) transparent;
}
::-webkit-scrollbar { width: var(--s-25); height: var(--s-25); }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-corner { background: transparent; }
::-webkit-scrollbar-button { display: none; }
::-webkit-scrollbar-thumb {
  border-radius: var(--shape-full);
  background: var(--outline-variant);
  border: var(--s-05) solid transparent;
  background-clip: content-box;
}
::-webkit-scrollbar-thumb:hover { background: var(--outline); background-clip: content-box; }

/* ================================================================== charts
 *
 * Every chart comes through here, for the same reason every box comes through
 * `.panel`: independently-drawn charts diverge in stroke weight, grid density
 * and baseline treatment within about three of them, and the result reads as
 * several tools rather than one.
 *
 * The geometry is `chart.mjs`; nothing there emits a colour or a class.
 */
/* Drawn into a unitless box and stretched to whatever width it lands in, which
   would stretch the stroke with it — so the stroke opts out. Without this a
   line is a hairline on a wide screen and a slab on a narrow one. */
.plot { display: block; width: 100%; height: var(--h-chart); overflow: visible; }
.plot-line, .plot-area, .plot-zero, .plot-mark { vector-effect: non-scaling-stroke; }
.plot-line { fill: none; stroke: var(--on-surface); stroke-width: 1.5; }
.plot-area { fill: color-mix(in oklab, var(--on-surface) var(--st-press), transparent);
  stroke: none; }
/* Below the line the balance is a deficit, and a deficit is not the same weight
   as the money above it. */
.plot-area[data-sign="under"] { fill: color-mix(in oklab, var(--bad) var(--st-press), transparent); }
/* The baseline a balance is read against, so it is drawn rather than implied. */
.plot-zero { stroke: var(--outline); stroke-width: 1; }
.plot-mark { stroke: var(--outline); stroke-width: 1; stroke-dasharray: 2 3; }
.plot-mark[data-kind="out"] { stroke: var(--bad); stroke-dasharray: none; }
/* A sparkline is the same chart at a glanceable height. */
.plot-spark { height: var(--h-spark); }
/* The line a load is read against. Drawn rather than implied, so an excursion
   above it is the thing the eye lands on. */
.plot-cap { stroke: var(--bad); stroke-width: 1; stroke-dasharray: 2 3;
  vector-effect: non-scaling-stroke; }

.plot-y { position: relative; display: grid; align-content: space-between;
  height: var(--h-chart); text-align: right;
  font: var(--f-label-m); font-variant-numeric: tabular-nums;
  color: var(--on-surface-muted); }
/* Positioned at the zero line rather than at the bottom, because on a balance
   that goes negative the bottom of the box is not zero. */
.plot-y-zero { position: absolute; right: 0; transform: translateY(-50%); }
.plot-x { display: flex; justify-content: space-between; margin: var(--s-1) 0 0;
  font: var(--f-label-m); font-variant-numeric: tabular-nums;
  color: var(--on-surface-muted); }

/* ================================================================== runway
 *
 * Built from the container primitives and adding no box of its own.
 *
 * The gap between the money going and the work landing is the headline, not the
 * runway: nine months is comfortable against six months of work and fatal
 * against twelve, so the two dates sit either side of the number that separates
 * them — and again on the chart, as the distance between the two rules.
 */
/* The panel own scope, so a class here cannot collide with one the host or a
   module chose. */
.rw { min-width: 0; }
/* The three pairwise comparisons, as a group. */
.rw-compare { margin-top: var(--s-4); }

.rw-head { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center;
  gap: var(--s-4); margin-bottom: var(--s-4); }
.rw-when { display: grid; gap: var(--s-05); }
.rw-when:last-child { text-align: right; }
.rw-when > b { font: var(--f-title-l); font-variant-numeric: tabular-nums; }
.rw-label { font: var(--f-label-l); letter-spacing: var(--k-label);
 color: var(--on-surface-muted); }

.rw-gap { display: grid; justify-items: center; gap: var(--s-05); }
/* `--f-display-s` does not exist — the scale runs headline-l then display-m.
   The style gate caught it: an undefined `var()` with no fallback drops the
   whole declaration, so this would have rendered at the inherited size and
   looked merely underwhelming rather than broken. */
.rw-gap > b { font: var(--f-display-m); font-variant-numeric: tabular-nums; }
.rw-gap > span { font: var(--f-label-l); color: var(--on-surface-muted); }
.rw-gap-none { font: var(--f-body-s); color: var(--on-surface-muted); }
/* The money going first is the case the screen exists to surface, and the one
   thing on it that takes a colour. */
.rw-head[data-short="true"] .rw-gap > b { color: var(--bad); }
.rw-warn { margin: 0 0 var(--s-3); font: var(--f-body-m); color: var(--bad); }

.rw-totals { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
  gap: var(--s-3); margin-bottom: var(--s-4); }
.rw-total { display: grid; gap: var(--s-05); }
.rw-total[data-bad="true"] .rw-n { color: var(--bad); }
.rw-n { font: var(--f-title-m); font-variant-numeric: tabular-nums; }
/* The unit, always beside the number. Money is not a dimension and nothing
   converts, so a figure without one is a figure somebody reads as their own. */
.rw-u { font: var(--f-label-m); color: var(--on-surface-muted); margin-left: var(--s-05); }

.rw-chart { display: grid; grid-template-columns: auto minmax(0, 1fr);
  grid-template-areas: "y plot" ". x"; gap: 0 var(--s-2); margin: 0 0 var(--s-4); }
.rw-chart > .plot-y { grid-area: y; }
.rw-chart > .plot { grid-area: plot; }
.rw-chart > .plot-x { grid-area: x; }

/* What is missing from the totals, as figures rather than a warning to read. */
.rw-gaps, .rw-rates { display: flex; align-items: baseline; flex-wrap: wrap;
  gap: var(--s-1) var(--s-4); margin-bottom: var(--s-4); }
.rw-flag, .rw-rate { display: inline-flex; align-items: baseline; gap: var(--s-1);
  font: var(--f-body-s); color: var(--on-surface-variant); }
.rw-flag > b { font-variant-numeric: tabular-nums; color: var(--warn); }
.rw-flag > i, .rw-rate > i { font-style: normal; color: var(--on-surface-muted); }
.rw-ratio { font: var(--f-body-s); font-variant-numeric: tabular-nums;
  color: var(--on-surface-muted); }

.rw-section { margin: 0 0 var(--s-2); font: var(--f-label-l);
  letter-spacing: var(--k-label); color: var(--on-surface-muted); }
.rw-cmp { display: grid; grid-template-columns: 110px minmax(0, 1fr) auto 56px;
  align-items: baseline; gap: var(--s-2); padding: var(--s-15) 0;
  border-bottom: 1px solid var(--outline-variant); }
.rw-cmp-name { font: var(--f-label-l); letter-spacing: var(--k-label);
 color: var(--on-surface-muted); }
.rw-cmp-nums { font: var(--f-body-s); font-variant-numeric: tabular-nums; }
.rw-cmp-delta { font: var(--f-title-m); font-variant-numeric: tabular-nums; text-align: right; }
.rw-cmp[data-over="true"] .rw-cmp-delta { color: var(--bad); }
/* The ratio, because 150k to 300k is legible as a doubling and not as a large
   number. */
.rw-cmp-ratio { font: var(--f-body-s); font-variant-numeric: tabular-nums;
  text-align: right; color: var(--on-surface-muted); }

/* The estimate own stated accuracy. A plan inside it has not overrun, which is
   the whole reason the class was recorded. */
.rw-range { display: flex; align-items: baseline; gap: var(--s-2);
  padding: var(--s-15) 0; font: var(--f-body-s); }
.rw-range > b { margin-left: auto; color: var(--bad); }
.rw-range[data-in="true"] > b { color: var(--ok); }
.rw-empty { margin: 0; font: var(--f-body-m); color: var(--on-surface-muted); }

/* ------------------------------------------------ asking before anything moves
 *
 * Reuses the dialog scrim and the card panel the rest of the product uses, so a
 * report reads as part of the application. It is deliberately tall and scrolls:
 * the itemised list of what would be sent is the whole point of the first
 * prompt, and a dialog that truncated it behind a scroll nobody notices would
 * be the "details" link this design exists to avoid. */
.sr-panel {
  width: min(560px, 100%); max-height: min(84vh, 760px); overflow: auto;
  padding: var(--s-5);
}
.sr-panel h2 { margin: 0 0 var(--s-2); font: var(--f-title-l); }
.sr-assure { margin: 0 0 var(--s-3); font: var(--f-body-s); color: var(--on-surface-variant); }
.sr-becomes { margin: var(--s-3) 0 0; font: var(--f-body-s); color: var(--on-surface-variant); }
.sr-head {
  margin: var(--s-4) 0 var(--s-2); font: var(--f-label-m);
 color: var(--on-surface-muted);
}
/* Two columns on a wide dialog, one on a narrow one. The label is the thing
   being scanned, so it keeps its own column for as long as there is room. */
.sr-items { display: grid; grid-template-columns: 13em 1fr; gap: var(--s-2) var(--s-3); margin: 0; font: var(--f-body-s); }
.sr-items dt { font-weight: var(--w-bold); color: var(--on-surface); }
.sr-items dd { margin: 0; color: var(--on-surface-variant); }
.sr-routes { list-style: none; margin: var(--s-3) 0 0; padding: 0; display: grid; gap: var(--s-3); }
/* The reason a route is unavailable sits under it and is always present. A
   disabled control with no explanation reads as broken, and somebody who thinks
   it is broken files a second bug about that instead. */
.sr-why { margin: var(--s-1) 0 0; font: var(--f-label-m); color: var(--on-surface-variant); }
/* Always in view while the disclosure scrolls.
 *
 * The itemised list is deliberately long — it is the whole point of the first
 * prompt — which put the answers below the fold. A consent dialog whose choices
 * are only findable by scrolling is one where people scroll past the reading to
 * hunt for the button, which inverts what the length was for. The choices stay
 * put; the thing being consented to is what moves. */
.sr-actions {
  display: flex; justify-content: flex-end; gap: var(--s-2);
  position: sticky; bottom: calc(var(--s-5) * -1);
  margin: var(--s-4) calc(var(--s-5) * -1) calc(var(--s-5) * -1);
  padding: var(--s-3) var(--s-5);
  background: var(--surface-card); border-top: 1px solid var(--line);
}
@media (max-width: 560px) {
  .sr-items { grid-template-columns: 1fr; gap: var(--s-05) var(--s-3); }
  .sr-items dd { margin-bottom: var(--s-2); }
}

/* ------------------------------------------------------- lanes and limits
 *
 * A lane keeps every column, including the empty ones: the value of a lane is
 * reading *across* it, and a gap in the row is the finding. */
.pl-lane { margin-bottom: var(--s-4); }
.pl-lane-head {
  display: flex; align-items: center; gap: var(--s-2);
  padding: var(--s-1) var(--s-2); margin-bottom: var(--s-2);
  border-bottom: 1px solid var(--line);
}
.pl-lane-name { font: var(--f-label-m); color: var(--on-surface-muted); }

/* **Only the breach is coloured.** A column at 3 of 5 is a count, not a state;
   colouring it teaches the eye to read past colour on that row, so the fourth
   column going over reads as more decoration. The limit itself is muted, so
   what is read is the number that moved. */
.pl-limit { color: var(--on-surface-muted); font-weight: var(--w-body); }
.pl-count.over { background: var(--bad-container); color: var(--on-bad-container); }
.pl-col.pl-over .pl-col-head { box-shadow: inset 0 -2px 0 var(--bad); }

/* ----------------------------------------------------------------- the table
 *
 * A grid rather than a `<table>`: the column widths are declared once, in
 * `table.mjs`, and passed in as a custom property, so the header and every row
 * cannot disagree about how many columns there are. */
.pl-table {
  /* The fallback is not decoration. `--pl-cols` is set inline by the renderer
     from `COLUMNS`, so a stylesheet that referenced it bare would drop the whole
     declaration the moment the inline style were missing — and a grid with no
     template collapses every row into one column, which looks like a rendering
     bug rather than a missing variable. This degrades to something readable. */
  display: grid; grid-template-columns: var(--pl-cols, repeat(auto-fit, minmax(6rem, 1fr)));
  border: 1px solid var(--line); border-radius: var(--r); overflow: auto;
  background: var(--surface-card);
}
.pl-tr { display: contents; }
.pl-tr > * {
  padding: var(--s-15) var(--s-2); border-bottom: 1px solid var(--line);
  font: var(--f-body-s); color: var(--on-surface);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  text-align: left;
}
.pl-th > * {
  font: var(--f-label-m); color: var(--on-surface-muted);
  background: var(--surface-container); position: sticky; top: 0; z-index: 1;
}
.pl-tr.done > * { color: var(--on-surface-muted); }
/* An editable cell is a button and has to look like a cell until it is
   hovered, or forty rows of buttons is forty rows of chrome. */
.pl-editable { background: none; border: 0; cursor: text; width: 100%; }
.pl-editable:hover { background: var(--surface-2); }
.pl-editable:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }
.pl-cell-input { width: 100%; }
.pl-table select { width: 100%; min-width: 0; }

/* --------------------------------------------------- setting a column's limit
 *
 * The current count sits beside the box on purpose. A limit set against nothing
 * is a guess; showing what is there now turns "what should our limit be" into
 * "is four too many", which is a question somebody can answer while looking at
 * their own board. */
.pl-wip-list { list-style: none; margin: var(--s-3) 0; padding: 0; display: grid; gap: var(--s-2); }
.pl-wip-row {
  display: grid; grid-template-columns: 1fr auto 6rem; gap: var(--s-3);
  align-items: center;
}
.pl-wip-name { font: var(--f-body-m); color: var(--on-surface); }
/* The count is muted until the column is over, and then it is the only coloured
   thing on the row. Same rule as the board: colour marks what needs attention,
   and a count that is within its limit needs none. */
.pl-wip-now { font: var(--f-label-m); color: var(--on-surface-muted); font-variant-numeric: tabular-nums; }
.pl-wip-now.over { color: var(--on-bad-container); background: var(--bad-container);
  border-radius: var(--shape-chrome); padding: var(--s-05) var(--s-2); }
.pl-wip-input { width: 100%; text-align: right; font-variant-numeric: tabular-nums; }
.pl-wip-why { margin: 0; font: var(--f-body-s); color: var(--on-surface-variant); }

/* ---------------------------------------------------------- working time
 *
 * The days a programme keeps. Same shape as the limits sheet next door — a
 * short why, the setting, the list, and a way to add to it — because they are
 * the same kind of screen and drawing them differently would make the second
 * one feel like a different product. */
.pl-cal-why { margin: 0; font: var(--f-body-s); color: var(--on-surface-variant); }
.pl-cal-sub { margin: var(--s-4) 0 var(--s-2); font: var(--f-title-s); color: var(--on-surface); }
.pl-cal-week { display: grid; gap: var(--s-2); margin: var(--s-4) 0; }
.pl-cal-week > span { font: var(--f-label-m); color: var(--on-surface-variant); }
.pl-cal-list { list-style: none; margin: 0 0 var(--s-3); padding: 0; display: grid; gap: var(--s-1); }
.pl-cal-row {
  display: grid; grid-template-columns: auto 1fr auto; gap: var(--s-3);
  align-items: center;
}
/* Tabular, because a column of dates that does not line up is a column nobody
   scans — and scanning it for the one that is wrong is the whole job. */
.pl-cal-date { font: var(--f-body-m); color: var(--on-surface); font-variant-numeric: tabular-nums; }
.pl-cal-name { font: var(--f-body-s); color: var(--on-surface-variant);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pl-cal-new { display: grid; grid-template-columns: auto 1fr auto; gap: var(--s-2); align-items: center; }
.pl-cal-bulk { display: flex; flex-wrap: wrap; gap: var(--s-2); align-items: center;
  margin: var(--s-3) 0; }
.pl-cal-added { font: var(--f-label-m); color: var(--on-surface-variant); }

/* ------------------------------------------------- promises the plan misses
 *
 * Above the chart rather than in it. Negative float already says the plan is
 * impossible; this says which promise broke, which is the half somebody can
 * take to a meeting. Nothing here offers to fix it — moving the date would make
 * the plan compute and the promise disappear. */
.pl-breaches {
  margin: 0 0 var(--s-4); padding: var(--s-3) var(--s-4);
  background: var(--bad-container); color: var(--on-bad-container);
  border-radius: var(--shape-l);
}
.pl-breaches h3 { margin: 0 0 var(--s-2); font: var(--f-title-s); }
.pl-breaches p { margin: var(--s-2) 0 0; font: var(--f-body-s); }
.pl-breaches ul { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--s-1); }
.pl-breach-row { display: grid; grid-template-columns: auto auto 1fr; gap: var(--s-2);
  align-items: baseline; }
.pl-breach-kind { font: var(--f-label-m); font-variant-numeric: tabular-nums; }
/* A hard constraint overrides the network rather than bending to it, and DCMA
   counts them for that reason. Marked, so the ones that will be asked about are
   the ones that stand out. */
.pl-breach-kind.hard { text-decoration: underline; }
.pl-breach-task { font: var(--f-body-m); }
.pl-breach-why { font: var(--f-body-s); }

/* ------------------------------------------------ the change impact assessment
 *
 * A form somebody works down, not a dashboard. A change board reads it once,
 * argues about it and files it — so the order is the order the questions get
 * asked, and the only things given colour are the ones that change the answer:
 * a subject nothing recognised, a total that understates, a date already
 * missed, and a screening question nobody has filled in. */
.ia { display: grid; gap: var(--s-4); max-width: 60rem; }
.ia-head h2 { margin: 0; font: var(--f-title-m); color: var(--on-surface); }
.ia-reason { margin: var(--s-1) 0 0; font: var(--f-body-m); color: var(--on-surface); }
.ia-detail { display: block; font: var(--f-body-s); color: var(--on-surface-variant); }
.ia-class { display: grid; gap: var(--s-2); padding: var(--s-4);
  background: var(--surface-container); border-radius: var(--shape-l); }
.ia-class h3 { margin: 0; font: var(--f-title-s); color: var(--on-surface); }
.ia-asks { margin: 0; font: var(--f-body-s); color: var(--on-surface-variant); }
.ia-authority { font-variant-numeric: tabular-nums; color: var(--on-surface-muted); }
.ia-facts { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--s-2); }
.ia-facts > li { font: var(--f-body-m); color: var(--on-surface); }
.ia-none, .ia-note { margin: 0; font: var(--f-body-s); color: var(--on-surface-variant); }

/* Numbers line up or nobody scans them, and scanning for the big one is the job. */
.ia-cost { width: 100%; border-collapse: collapse; }
.ia-cost th { text-align: left; font: var(--f-body-m); color: var(--on-surface); font-weight: inherit; }
.ia-cost td, .ia-cost th { padding: var(--s-2) 0; vertical-align: top; }
.ia-cost tfoot th, .ia-cost tfoot td { border-top: 1px solid var(--control-edge); font-weight: var(--w-bold); }
.ia-num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.ia-money { font: var(--f-body-m); color: var(--on-surface); }
.ia-hours { font: var(--f-body-m); color: var(--on-surface); }

/* "Not costed" and "partial" are the two labels that stop a number being
   believed too readily, so they travel with the number rather than sitting in a
   footnote nobody reaches. */
.ia-unpriced, .ia-partial { display: block; font: var(--f-label-m); color: var(--on-surface-variant); }

.ia-warn { color: var(--on-bad-container); background: var(--bad-container);
  border-radius: var(--shape-chrome); padding: var(--s-2) var(--s-3); }
.ia-warn .ia-detail { color: inherit; }
.ia-bad { color: var(--on-bad-container); }
.ia-unknown { margin: 0; font: var(--f-body-m);
  color: var(--on-bad-container); background: var(--bad-container);
  border-radius: var(--shape-l); padding: var(--s-3) var(--s-4); }

/* An unanswered screening question. Every other heading on the page is filled
   in by the tool, which is why a blank one needs an edge to be seen at all. */
.ia-class.ia-open { outline: 2px dashed var(--control-edge); outline-offset: -2px; }
.ia-awaiting { margin: 0; font: var(--f-body-m); color: var(--on-surface); }
.ia-optional { font: var(--f-label-m); color: var(--on-surface-variant); }
.ia-finding { margin: 0; padding: var(--s-3); border-left: 3px solid var(--primary);
  background: var(--surface); border-radius: var(--shape-none) var(--shape-chrome) var(--shape-chrome) var(--shape-none); }
.ia-finding footer { margin-top: var(--s-2); font: var(--f-label-m); color: var(--on-surface-variant); }
.ia-foot { font: var(--f-body-m); color: var(--on-surface); padding: var(--s-3) var(--s-4);
  background: var(--surface-container); border-radius: var(--shape-l); }
.ia-foot.ia-blocked { color: var(--on-bad-container); background: var(--bad-container); }

/* The screening questions a programme adds. Same shape as the limits and
   working-time sheets, because they are the same kind of screen. */
.pl-screen-list { list-style: none; margin: var(--s-3) 0; padding: 0; display: grid; gap: var(--s-3); }
.pl-screen-row { display: grid; grid-template-columns: 1fr auto; gap: var(--s-2); align-items: start; }
.pl-screen-row .ia-detail { grid-column: 1; }
.pl-screen-row button { grid-column: 2; grid-row: 1 / span 2; }
.pl-screen-name { font: var(--f-body-m); color: var(--on-surface); }
.pl-screen-new { display: grid; gap: var(--s-2); }
.pl-change { display: grid; gap: var(--s-4); padding: var(--s-4); overflow: auto; }
.pl-change-bar { display: flex; gap: var(--s-2); flex-wrap: wrap; }
.pl-finding { display: grid; gap: var(--s-3); margin-top: var(--s-3); }

/* Obligations: what somebody outside the programme has been promised. */
.pl-oblig-list { list-style: none; margin: var(--s-3) 0; padding: 0; display: grid; gap: var(--s-3); }
.pl-oblig-row { display: grid; grid-template-columns: 1fr auto; gap: var(--s-2); align-items: start; }
.pl-oblig-row .ia-detail { grid-column: 1; }
.pl-oblig-row button { grid-column: 2; grid-row: 1 / span 2; }
.pl-oblig-name { font: var(--f-body-m); color: var(--on-surface); }
.pl-oblig-new { display: grid; gap: var(--s-2); }
/* Only a certificate that actually lapses first earns colour. Unknown is drawn
   as the muted detail beside it, because reassurance the data does not support
   is the same mistake as drawing an unpriced task as free. */
.ia-lapses { display: block; font: var(--f-label-m); color: var(--on-bad-container);
  background: var(--bad-container); border-radius: var(--shape-chrome);
  padding: var(--s-05) var(--s-2); justify-self: start; }

/* ------------------------------------------------------- filling the ledger
 *
 * The structure is always on the page rather than behind a button: a programme
 * with no control accounts needs to see the shape of what the screen wants
 * before it has any of it. */
.lg { display: grid; gap: var(--s-5); padding: var(--s-4) 0; }
.lg h3 { margin: 0 0 var(--s-2); font: var(--f-title-s); color: var(--on-surface); }
.lg-baseline, .lg-accounts { display: grid; gap: var(--s-3); }
.lg-bl { margin: 0; font: var(--f-body-m); color: var(--on-surface); }
.lg-ca { display: grid; gap: var(--s-2); padding: var(--s-3);
  background: var(--surface-container); border-radius: var(--shape-l); }
.lg-ca-head { display: grid; grid-template-columns: 1fr auto; gap: var(--s-2); align-items: start; }
.lg-ca-head .ia-detail { grid-column: 1; }
.lg-ca-head button { grid-column: 2; grid-row: 1 / span 2; }
.lg-ca-name { font: var(--f-body-m); color: var(--on-surface); }
.lg-wps { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--s-2); }
.lg-wp { display: grid; grid-template-columns: 1fr auto auto; gap: var(--s-2); align-items: center; }
.lg-wp-name { font: var(--f-body-m); color: var(--on-surface); grid-column: 1; }
.lg-wp-meta { font: var(--f-body-s); color: var(--on-surface-variant); grid-column: 1;
  font-variant-numeric: tabular-nums; }
/* A planning package measures nothing, so it is marked rather than left to be
   inferred from an empty rule column. */
.lg-tag { font: var(--f-label-m); color: var(--on-surface-variant); }
.lg-progress { display: flex; gap: var(--s-2); align-items: center; flex-wrap: wrap;
  grid-column: 2; grid-row: 1 / span 2; }
/* The walkthrough panel.

   Fixed rather than in the flow, because a step names a destination and the
   panel has to survive going there. Deliberately not a modal and deliberately
   not full width: it points at the product, and everything behind it stays
   usable while it is open — a tour that drives is a tour people fight.

   Sized in ch so the reading measure is the thing that sets the width, and
   inset with spacing tokens, so it costs the style ratchet nothing. */
.tour {
  position: fixed;
  right: var(--s-4);
  bottom: var(--s-4);
  z-index: 40;
  width: min(42ch, calc(100% - var(--s-6)));
  display: grid;
  gap: var(--s-2);
  padding: var(--s-3);
  border: 1px solid var(--outline-variant);
  border-radius: var(--shape-l);
  background: var(--surface-2);
  box-shadow: var(--shadow);
}
.tour-at { font: var(--f-label-m); color: var(--on-surface-muted); margin: 0; }
.tour-title { font: var(--f-title-s); margin: 0; }
.tour-body { font: var(--f-body-m); color: var(--on-surface-variant); margin: 0; }
.tour-acts { display: flex; align-items: center; gap: var(--s-1); }

/* A person, as a disc with their initials.

   Sized in ch and em so it tracks the label type it sits beside and costs the
   style ratchet nothing. The ring is the page's own ground rather than a
   colour, so overlapping discs read as separate without a border that would
   fight whichever theme is on. */
.face {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.9ch;
  height: 2.9ch;
  border-radius: var(--shape-full);
  box-shadow: 0 0 0 2px var(--surface-container-low);
  font: var(--f-label-m);
  font-size: var(--fs-label-xs);
  color: var(--on-accent-fill);
  user-select: none;
}
/* The overflow count is not a person, so it is not given somebody's colour. */
.face-more {
  background: var(--surface-container-high);
  color: var(--on-surface-variant);
  box-shadow: none;
  font-variant-numeric: tabular-nums;
}

/* Presence (task 0303): a face in the shell strip is a button that opens what
   that person has selected. The disc stays the disc; only the state layer and
   the focus ring are added. Away takes the overflow count's neutral pair
   rather than a dimmed colour, which would take the initials below 4.5:1;
   the label says *away* in words. */
.face-button { border: 0; padding: 0; cursor: pointer; background-image: none; }
.face-button:hover { background-image: var(--layer-hover); }
.face-button:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.face-button.is-away { background-color: var(--surface-container-high); color: var(--on-surface-variant); }

/* Somebody else has this selected. A ring in their colour, at the line grade
   rather than the face's fill grade, because a ring is a line against the
   ground and the fill grade measures under 3:1 on the light surfaces. Under
   your own selection's accent bar, never over it: that bar is the
   load-bearing signal. The row also says who, in words, as its description. */
.tree-row[data-seen]:not(:focus-visible) { box-shadow: inset 0 0 0 2px var(--seen, var(--primary)); }
.tree-row.selected[data-seen]:not(:focus-visible) {
  box-shadow: inset 3px 0 var(--primary), inset 0 0 0 2px var(--seen, var(--primary)), inset 0 -1px var(--outline-variant);
}
/* A block is an SVG shape, where an outline is not reliably painted, and the
   block's own stroke already says selected and touched. Four hard shadows are
   a ring outside the shape that every engine draws. */
[data-seen] > .arch-block {
  filter: drop-shadow(2px 0 0 var(--seen, var(--primary))) drop-shadow(-2px 0 0 var(--seen, var(--primary)))
    drop-shadow(0 2px 0 var(--seen, var(--primary))) drop-shadow(0 -2px 0 var(--seen, var(--primary)));
}
/* Hide my presence is a switch; on is a ground, as selection is. */
[data-region="presence"] .btn[aria-pressed="true"] { background-color: var(--selected); }

.lg-new { display: grid; gap: var(--s-2); }

/* The contract facts: forty-six fields, so they are laid out rather than
   listed. Sized in ch and fr because the style ratchet counts px and a form
   this size does not deserve any of the budget -- and because a column
   measured in characters is the right measure for a column of text fields. */
.lg-stack { gap: var(--s-4); }
.lg-group { border: none; padding: 0; margin: 0; display: grid; gap: var(--s-2); }
.lg-group legend { font: var(--f-label-m); color: var(--on-surface-variant); padding: 0; }
.lg-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(22ch, 1fr)); gap: var(--s-2); }
.lg-field { display: grid; gap: var(--s-05); }
.lg-field-name { font: var(--f-label-m); color: var(--on-surface-variant); }
/* A toggle is a control, not a bar. Without this it is a grid item and
   stretches the full width of the fieldset, which reads as a banner. */
.lg-group > button { justify-self: start; }
.lg-money { display: grid; gap: var(--s-3); }
/* Same row as a work package: a name, what it costs, and a way to take it out. */
.lg-rate { display: grid; grid-template-columns: 1fr auto; gap: var(--s-2); align-items: center; }
.lg-rate .lg-wp-meta { grid-column: 1; }
.lg-rate button { grid-column: 2; grid-row: 1 / span 2; }

/* Presence faces on a phone (task 0303). A face opens something, so it is a
   primary target and takes 44px of hit area around the disc; the disc keeps
   its size. Last in the file, where phone overrides win. */
@media (max-width: 720px) {
  .face-button { position: relative; }
  .face-button::after { content: ""; position: absolute; inset: calc((2.9ch - 44px) / 2); }
  [data-region="presence"] > span { gap: var(--s-3); }
}
