/* ns.css — Enclave's design system. One file, hand-written, no build step.
 *
 * It styles valid, semantic HTML by element first, by attribute and ARIA role
 * second, and by a small closed set of `ns-` classes last. Drop it into any
 * HTML page and the elements are styled; add a class only where an element or
 * an attribute cannot say what a thing is.
 *
 * Structure: every component and app block is wrapped in `@block name` /
 * `@end name` markers, contains only selectors for that block, and depends on
 * nothing outside the `tokens` and `base` layers. Deleting a block between its
 * markers removes that component cleanly — that is what "tree-shakeable by
 * structure" means, and `design:prune --report` reads these markers.
 *
 * The closed class registry is the table in the brief's Modifiers section:
 * 112 classes, and a test fails the build on any class a view uses that this
 * file does not define. Do not add a class here without adding it there.
 */

@layer reset, tokens, base, layout, components, modifiers, app;

/* ============================================================ @block reset */
@layer reset {
  *, *::before, *::after { box-sizing: border-box }

  /* Margins come from the flow rule in `base`, never from the element's own
   * default, so a heading against a container edge is the container's choice. */
  :where(body, h1, h2, h3, h4, h5, h6, p, figure, blockquote, dl, dd, ol, ul,
         fieldset, legend, pre) { margin: 0 }
  :where(ul, ol) { padding-inline-start: 0; list-style: none }

  html {
    /* `interpolate-size` lets <details> animate to `height: auto`; without it
     * the accordion has to be told a pixel height it cannot know. */
    interpolate-size: allow-keywords;

    /* Every in-page link glides rather than jumps. This is the browser's own
     * smooth scroll, so it costs nothing and honours the reduced-motion
     * setting below without a second implementation. */
    scroll-behavior: smooth;
    -webkit-text-size-adjust: 100%;
  }

  /* Media are block-level and never overflow their container. */
  :where(img, svg, video, canvas, audio, iframe, embed, object) {
    display: block; max-inline-size: 100%;
  }
  :where(img, svg, video, canvas) { block-size: auto }

  /* Forms do not inherit typography by default; make them. */
  :where(input, button, textarea, select) { font: inherit; color: inherit }
  :where(textarea) { resize: vertical }

  /* A cell that could hold a URL is the only place breaking mid-word is right.
   * The aggressive value is never used: it breaks Thai and Chinese wrongly. */
  :where(td, th) { overflow-wrap: anywhere }

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

/* =========================================================== @block tokens */
@layer tokens {
  /* Typed custom properties. The type matters: `--gap` declared as <length>
   * animates and inherits as a length instead of as an unparsed string, and a
   * bad value falls back to the initial rather than poisoning the cascade. */
  @property --gap   { syntax: "<length>"; inherits: true; initial-value: 16px }
  @property --pad   { syntax: "<length>"; inherits: true; initial-value: 16px }
  @property --m     { syntax: "<length>"; inherits: true; initial-value: 0px }
  @property --dense { syntax: "<integer>"; inherits: true; initial-value: 0 }
  @property --font-scale { syntax: "<number>"; inherits: true; initial-value: 1 }
  @property --tone-color { syntax: "<color>"; inherits: true; initial-value: #1f6b5a }
  @property --border { syntax: "<length>"; inherits: true; initial-value: 1px }
  @property --radius { syntax: "<length>"; inherits: true; initial-value: 6px }

  /* The palette is deliberately NOT registered with @property. A registered
   * <color> is resolved at computed-value time, and that resolution loses
   * light-dark(): every role would freeze at its initial value and the dark
   * theme would render light. The kit reads a role's current colour off a
   * probe element instead, which costs four lines of ui.js and nothing here. */

  :root {
    color-scheme: light dark;

    /* Colours are ROLES, never hues. One declaration per role carries both
     * schemes; `light-dark()` picks by `color-scheme`, which `data-theme`
     * below pins when the person has chosen one. */
    --bg:        light-dark(#f7f8f6, #141715);
    --surface:   light-dark(#ffffff, #1b201d);
    --ink:       light-dark(#1b1f1c, #e9ece8);
    --muted:     light-dark(#5c665f, #9aa59d);
    --outline:   light-dark(#d9ded9, #2a302c);
    --primary:   light-dark(#1f6b5a, #6fb89f);
    --secondary: light-dark(#5c665f, #9aa59d);
    --cta:       light-dark(#b5561d, #e8905a);
    --highlight: light-dark(#e8f0ec, #1e2a25);
    --danger:    light-dark(#a33a2a, #e07a6b);
    --success:   light-dark(#2f7a3e, #7ec98d);
    --warn:      light-dark(#9a6b00, #e0b64a);
    --info:      var(--primary);
    --disabled:  color-mix(in oklab, var(--muted) 45%, var(--bg));

    /* The five shape knobs. Every border width, corner and shadow in the file
     * reads one of these and nothing hard-codes its own, so changing one here
     * changes the whole system — which is what the kit's controls do. */
    --radius: 6px;
    --border: 1px;
    --shadow: 0 1px 2px color-mix(in oklab, black 8%, transparent);
    --shadow-lg: 0 8px 24px color-mix(in oklab, black 18%, transparent);
    --shadow-xl: 0 16px 48px color-mix(in oklab, black 28%, transparent);

    --s-1: 4px; --s-2: 8px; --s-3: 12px; --s-4: 16px;
    --s-5: 24px; --s-6: 32px; --s-7: 48px; --s-8: 64px;

    /* No web font is ever named. Every OS ships a CJK and a Thai face, and a
     * downloaded font is the one asset a plain-HTML product should not carry;
     * a test asserts no downloaded face and no font CDN in any response. */
    --font: system-ui, sans-serif;
    --mono: ui-monospace, monospace;
    --font-scale: 1;

    /* The tone a subtree is in. Set by the tone classes, read by style queries
     * on descendants, so one class on a <fieldset> colours everything inside. */
    --tone: neutral;
    --tone-color: var(--primary);

    --ring: 2px;
    /* The height of the bar, which is sticky, so anything else that sticks
     * knows where the page actually starts. */
    --bar: 49px;
  }

  /* Theme is one attribute, written by Blade in the first bytes of the page so
   * there is no white flash in the dark theme. Absent means "system". */
  html[data-theme="light"] { color-scheme: light }
  html[data-theme="dark"]  { color-scheme: dark }
}
/* ============================================================= @end tokens */

/* =========================================================== @block themes */
@layer tokens {
  /* Eleven previewable themes and five shape knobs, with NO JavaScript.
   *
   * Each control in the kit is a radio button; `:root:has(#t-navy:checked)`
   * re-declares the token block. The browser does the work the way it does
   * for a checkbox: the theme is part of the document's state, it survives
   * a back button, and it costs nothing to run.
   *
   * A theme sets ROLES only. It never names a component, so a theme cannot
   * break a component and a new component needs no work in any theme.
   * `light`/`dark` are the product's own palette with the scheme pinned —
   * they carry no colours at all, because light-dark() already holds both. */

  :root:has(#t-light:checked) { color-scheme: light }
  :root:has(#t-dark:checked)  { color-scheme: dark }

  :root:has(#t-citylights:checked) {
    color-scheme: dark;
    --bg:#1d252c; --surface:#181f25; --ink:#b7c5d3; --muted:#718ca1; --outline:#2b3945;
    --primary:#5ec4ff; --secondary:#70e1e8; --cta:#e27e8d; --highlight:#23303b;
    --danger:#d95468; --success:#8bd49c; --warn:#ebbf83;
  }
  :root:has(#t-barbie:checked) {
    color-scheme: light;
    --bg:#fff0f6; --surface:#ffffff; --ink:#3d0a26; --muted:#96517a; --outline:#ffc9e0;
    --primary:#e0218a; --secondary:#f78fb3; --cta:#ff5fa2; --highlight:#ffe0ee;
    --danger:#c2185b; --success:#2f9e6e; --warn:#d98e00;
  }
  :root:has(#t-teal:checked) {
    color-scheme: light;
    --bg:#f0faf8; --surface:#ffffff; --ink:#0d2b28; --muted:#4b706c; --outline:#c6e5e0;
    --primary:#0f766e; --secondary:#5eada4; --cta:#d97706; --highlight:#d9f2ee;
    --danger:#b91c1c; --success:#15803d; --warn:#a16207;
  }
  :root:has(#t-navy:checked) {
    color-scheme: dark;
    --bg:#0b1220; --surface:#111a2e; --ink:#e2e8f0; --muted:#94a3b8; --outline:#1e2b45;
    --primary:#60a5fa; --secondary:#94a3b8; --cta:#fbbf24; --highlight:#16243d;
    --danger:#f87171; --success:#4ade80; --warn:#fcd34d;
  }
  /* Shoji: washi paper, sumi ink, a matcha green and the vermilion of a torii. */
  :root:has(#t-shoji:checked) {
    color-scheme: light;
    --bg:#f4f1ea; --surface:#fbf9f4; --ink:#2b2a26; --muted:#7a756a; --outline:#ddd6c6;
    --primary:#6b7a5e; --secondary:#9c8f7a; --cta:#a84b3c; --highlight:#eae5d8;
    --danger:#8c3b32; --success:#5d7052; --warn:#b8863b;
  }
  :root:has(#t-sand:checked) {
    color-scheme: light;
    --bg:#f6efe4; --surface:#fffaf2; --ink:#3b3026; --muted:#8a7862; --outline:#e2d3bd;
    --primary:#b07d4a; --secondary:#c4a884; --cta:#c2632c; --highlight:#efe3cf;
    --danger:#a63a2a; --success:#6b7f3f; --warn:#c08a00;
  }
  :root:has(#t-macos:checked) {
    color-scheme: light;
    --bg:#ececec; --surface:#ffffff; --ink:#1d1d1f; --muted:#86868b; --outline:#d2d2d7;
    --primary:#0071e3; --secondary:#86868b; --cta:#ff9500; --highlight:#e8f0fe;
    --danger:#ff3b30; --success:#34c759; --warn:#ffcc00;
  }
  :root:has(#t-gnome:checked) {
    color-scheme: light;
    --bg:#fafafa; --surface:#ffffff; --ink:#241f31; --muted:#5e5c64; --outline:#deddda;
    --primary:#3584e4; --secondary:#77767b; --cta:#e66100; --highlight:#e0ecfb;
    --danger:#e01b24; --success:#2ec27e; --warn:#f5c211;
  }
  :root:has(#t-windows:checked) {
    color-scheme: light;
    --bg:#f3f3f3; --surface:#ffffff; --ink:#1b1b1b; --muted:#616161; --outline:#e5e5e5;
    --primary:#0067c0; --secondary:#605e5c; --cta:#ca5010; --highlight:#eff6fc;
    --danger:#c42b1c; --success:#0f7b0f; --warn:#9d5d00;
  }

  /* --- Shape knobs ------------------------------------------------------ */
  :root:has(#r-0:checked) { --radius: 0px }
  :root:has(#r-1:checked) { --radius: 4px }
  :root:has(#r-2:checked) { --radius: 8px }
  :root:has(#r-3:checked) { --radius: 14px }
  :root:has(#r-4:checked) { --radius: 999px }

  :root:has(#b-0:checked) { --border: 0px }
  :root:has(#b-1:checked) { --border: 1px }
  :root:has(#b-2:checked) { --border: 2px }
  :root:has(#b-3:checked) { --border: 3px }

  :root:has(#sh-0:checked) { --shadow: none; --shadow-lg: none; --shadow-xl: none }
  :root:has(#sh-2:checked) {
    --shadow: 0 2px 6px color-mix(in oklab, black 14%, transparent);
    --shadow-lg: 0 12px 32px color-mix(in oklab, black 24%, transparent);
    --shadow-xl: 0 24px 64px color-mix(in oklab, black 34%, transparent);
  }
  :root:has(#sh-3:checked) {
    --shadow: 0 3px 10px color-mix(in oklab, black 22%, transparent);
    --shadow-lg: 0 18px 44px color-mix(in oklab, black 34%, transparent);
    --shadow-xl: 0 32px 80px color-mix(in oklab, black 46%, transparent);
  }

  :root:has(#p-1:checked) { --pad: 8px }
  :root:has(#p-2:checked) { --pad: 12px }
  :root:has(#p-3:checked) { --pad: 16px }
  :root:has(#p-4:checked) { --pad: 24px }

  /* One token changes every face on the page. No downloaded family is named:
   * each stack is a generic first, then whatever the OS happens to have. */
  :root:has(#f-serif:checked)    { --font: ui-serif, Georgia, "Times New Roman", serif }
  :root:has(#f-mono:checked)     { --font: var(--mono) }
  :root:has(#f-rounded:checked)  { --font: ui-rounded, "SF Pro Rounded", "Segoe UI Variable", system-ui, sans-serif }
  :root:has(#f-humanist:checked) { --font: Optima, "Gill Sans", "Trebuchet MS", system-ui, sans-serif }

  :root:has(#z-90:checked)  { --font-scale: .9 }
  :root:has(#z-115:checked) { --font-scale: 1.15 }
  :root:has(#z-150:checked) { --font-scale: 1.5 }
}
/* ============================================================= @end themes */

/* ============================================================= @block base */
@layer base {
  html {
    font-family: var(--font);
    /* Thai needs >= 1.7 for stacked tone marks and Chinese is comfortable at
     * 1.6, so the floor is set for the tightest script, not the loosest. */
    line-height: 1.7;
    font-size: calc(1rem * var(--font-scale));
  }

  body {
    margin: 0;
    background: var(--bg);
    color: var(--ink);
    min-block-size: 100dvh;
    /* Safe-area insets so the bar clears a notch and the bottom bar clears a
     * home indicator; harmless everywhere else. */
    padding-block-start: env(safe-area-inset-top);
    padding-block-end: env(safe-area-inset-bottom);
  }

  /* Flow: spacing between siblings belongs to the container, not to the
   * elements. One rule instead of a margin on every block element. */
  :where(main, article, section, aside, fieldset, form, dialog, li, td, th, blockquote)
    > * + * { margin-block-start: var(--s-4) }

  h1, h2, h3, h4, h5, h6 {
    line-height: 1.3;
    text-wrap: balance;
    font-weight: 600;
  }
  h1 { font-size: 2rem } h2 { font-size: 1.5rem } h3 { font-size: 1.25rem }
  h4 { font-size: 1.1rem } h5, h6 { font-size: 1rem }

  p, li, dd { text-wrap: pretty }
  small { font-size: .875em; color: var(--muted) }
  strong, b { font-weight: 600 }
  mark { background: var(--highlight); color: inherit; padding-inline: .15em }
  /* The bar is sticky, so an anchor scrolled to the very top lands underneath
   * it. Every element that can be a link target keeps the bar's height clear. */
  [id] { scroll-margin-block-start: calc(var(--bar) + var(--s-4)) }

  hr { border: 0; border-block-start: var(--border) solid var(--outline); margin-block: var(--s-5) }

  a {
    color: var(--primary);
    text-underline-offset: .2em;
    &:hover { text-decoration-thickness: 2px }
  }

  code, kbd, samp, pre { font-family: var(--mono); font-size: .9em }
  code { background: var(--highlight); padding-inline: .3em; border-radius: 3px }
  kbd {
    background: var(--surface); border: var(--border) solid var(--outline);
    border-block-end-width: 2px; border-radius: 4px; padding-inline: .35em;
  }
  pre {
    background: var(--surface); border: var(--border) solid var(--outline);
    border-radius: var(--radius); padding: var(--s-4);
    overflow-x: auto;
    & code { background: none; padding: 0 }
  }

  blockquote {
    border-inline-start: 3px solid var(--outline);
    padding-inline-start: var(--s-4); color: var(--muted);
  }

  /* One visible focus treatment everywhere, including inside components that
   * repaint their own backgrounds. Never removed, only moved. */
  :focus-visible {
    outline: var(--ring) solid var(--tone-color);
    outline-offset: 2px;
  }

  ::selection { background: color-mix(in oklab, var(--primary) 25%, transparent) }

  /* Native controls take the tone rather than the browser's blue. */
  :where(input, textarea, select, progress, meter) { accent-color: var(--tone-color) }

  [hidden] { display: none !important }
}
/* =============================================================== @end base */

/* =========================================================== @block layout */
@layer layout {
  /* Bootstrap's 12 columns at 960px, re-expressed as CSS grid and measured
   * against the CONTAINER rather than the viewport — a page may be a floating
   * window 700px wide on a 2,000px screen, and the viewport is the wrong
   * question to ask about it. */
  .ns-container {
    container-type: inline-size;
    max-inline-size: 960px;
    margin-inline: auto;
    padding-inline: var(--s-4);
    /* The flow rule only spaces a child from the sibling BEFORE it, on
     * purpose — a heading against a container edge is the container's
     * choice. This is that choice: without it, the first thing on every
     * page sits flush against the sticky bar with no breath at all. */
    padding-block: var(--s-6);
  }

  /* 960px is the reading measure, and every document screen wants it. The two
   * surfaces that are not documents — the desktop shell and this design kit —
   * are tools, and a tool is entitled to the window it was given. */
  .ns-container.ns-wide { max-inline-size: 1400px }

  .ns-grid {
    display: grid;
    gap: var(--gap, var(--s-4));
    grid-template-columns: repeat(12, minmax(0, 1fr));

    /* Each grid is its own query container, so the spans below measure the
     * grid itself. Without this a grid outside .ns-container — the bar is
     * one — has no container to ask, every @container below fails to match,
     * and all twelve columns collapse into a stack. It also makes a nested
     * grid answer for its own width instead of its grandparent's. */
    container-type: inline-size;

    /* Everything spans twelve below 640px of container, so a phone and a
     * narrow window get one column with no second set of classes. */
    & > * { grid-column: span 12 }
  }

  @container (min-width: 640px) {
    .ns-grid > .ns-span-1  { grid-column: span 1 }
    .ns-grid > .ns-span-2  { grid-column: span 2 }
    .ns-grid > .ns-span-3  { grid-column: span 3 }
    .ns-grid > .ns-span-4  { grid-column: span 4 }
    .ns-grid > .ns-span-5  { grid-column: span 5 }
    .ns-grid > .ns-span-6  { grid-column: span 6 }
    .ns-grid > .ns-span-7  { grid-column: span 7 }
    .ns-grid > .ns-span-8  { grid-column: span 8 }
    .ns-grid > .ns-span-9  { grid-column: span 9 }
    .ns-grid > .ns-span-10 { grid-column: span 10 }
    .ns-grid > .ns-span-11 { grid-column: span 11 }
    .ns-grid > .ns-span-12 { grid-column: span 12 }

    .ns-grid > .ns-start-1  { grid-column-start: 1 }
    .ns-grid > .ns-start-2  { grid-column-start: 2 }
    .ns-grid > .ns-start-3  { grid-column-start: 3 }
    .ns-grid > .ns-start-4  { grid-column-start: 4 }
    .ns-grid > .ns-start-5  { grid-column-start: 5 }
    .ns-grid > .ns-start-6  { grid-column-start: 6 }
    .ns-grid > .ns-start-7  { grid-column-start: 7 }
    .ns-grid > .ns-start-8  { grid-column-start: 8 }
    .ns-grid > .ns-start-9  { grid-column-start: 9 }
    .ns-grid > .ns-start-10 { grid-column-start: 10 }
    .ns-grid > .ns-start-11 { grid-column-start: 11 }
    .ns-grid > .ns-start-12 { grid-column-start: 12 }
  }

  /* 900, not 960: box-sizing is border-box everywhere, so a .ns-container at
   * its full 960px has a 928px content box and a grid inside it can never be
   * 960px wide. A 960px threshold would make every ns-lg- class dead. */
  @container (min-width: 900px) {
    .ns-grid > .ns-lg-span-1  { grid-column: span 1 }
    .ns-grid > .ns-lg-span-2  { grid-column: span 2 }
    .ns-grid > .ns-lg-span-3  { grid-column: span 3 }
    .ns-grid > .ns-lg-span-4  { grid-column: span 4 }
    .ns-grid > .ns-lg-span-5  { grid-column: span 5 }
    .ns-grid > .ns-lg-span-6  { grid-column: span 6 }
    .ns-grid > .ns-lg-span-7  { grid-column: span 7 }
    .ns-grid > .ns-lg-span-8  { grid-column: span 8 }
    .ns-grid > .ns-lg-span-9  { grid-column: span 9 }
    .ns-grid > .ns-lg-span-10 { grid-column: span 10 }
    .ns-grid > .ns-lg-span-11 { grid-column: span 11 }
    .ns-grid > .ns-lg-span-12 { grid-column: span 12 }
  }
}
/* ============================================================= @end layout */

/* ======================================================== @block modifiers */
@layer modifiers {
  /* Sticks to the top of whatever scrolls it. It paints its own background
   * because a transparent sticky element shows the content sliding under it,
   * which reads as a rendering fault rather than as a bar. */
  .ns-sticky {
    /* It stops below the bar, not under it, because the bar is sticky too.
     * Inside something that scrolls on its own the bar is not in the way:
     * that container sets `--stick: 0` and the offset goes away. */
    position: sticky; inset-block-start: var(--stick, var(--bar)); z-index: 3;
    background: var(--surface);
  }
  /* A sticky <thead> only works if the cells stick; the row cannot. */
  thead.ns-sticky { background: none }
  thead.ns-sticky > tr > th {
    position: sticky; inset-block-start: var(--stick, var(--bar)); z-index: 3;
    background: var(--surface);
  }

  /* --- Flow ------------------------------------------------------------- */
  .ns-row  { display: flex; flex-wrap: wrap; gap: var(--gap, var(--s-4)); align-items: center }

  /* A field is full width in a form and content width in a toolbar. Without
   * this a single search box in a row takes the whole line and pushes the
   * button it belongs with onto the next one. `ns-full` still wins. */
  .ns-row > :where(input, select, textarea):not(.ns-full) { inline-size: auto }
  .ns-stack { display: flex; flex-direction: column; gap: var(--gap, var(--s-4)) }
  /* Anything that arranges its own children owns the space between them, so
   * the flow rule in `base` must not also add some. That rule keys off the
   * ELEMENT — fieldset, form, section, li — and a `.ns-segmented` is a
   * fieldset, so every label after the first was being pushed 16px down inside
   * an inline row. The segments came out at two different heights.
   *
   * Named rather than inferred, because CSS cannot ask what an element's
   * computed `display` is. A new block that lays out its children belongs on
   * this line, and the sign that it has been forgotten is a first child
   * sitting higher than the rest. */
  :where(.ns-row, .ns-stack, .ns-grid, .ns-segmented, .ns-pills, .ns-menu,
         .ns-slider, .ns-slides, .ns-board, .ns-thread, .ns-desktop,
         .ns-chart) > * + * {
    margin-block-start: 0;
  }
  .ns-center  { justify-content: center }
  .ns-between { justify-content: space-between }
  .ns-end     { justify-content: flex-end }
  .ns-middle  { align-items: center }

  /* --- Space ------------------------------------------------------------
   * These set a custom property that descendants consume. Per-side spacing is
   * deliberately not offered: a component owns its own insides, and
   * `ns-pl-7`-style classes are how a closed set stops being closed. */
  .ns-gap-0 { --gap: 0px } .ns-gap-1 { --gap: var(--s-1) } .ns-gap-2 { --gap: var(--s-2) }
  .ns-gap-3 { --gap: var(--s-3) } .ns-gap-4 { --gap: var(--s-4) }
  .ns-gap-5 { --gap: var(--s-5) } .ns-gap-6 { --gap: var(--s-6) }

  .ns-pad-0 { --pad: 0px; padding: 0 } .ns-pad-1 { --pad: var(--s-1); padding: var(--s-1) }
  .ns-pad-2 { --pad: var(--s-2); padding: var(--s-2) } .ns-pad-3 { --pad: var(--s-3); padding: var(--s-3) }
  .ns-pad-4 { --pad: var(--s-4); padding: var(--s-4) } .ns-pad-5 { --pad: var(--s-5); padding: var(--s-5) }
  .ns-pad-6 { --pad: var(--s-6); padding: var(--s-6) }

  .ns-m-0 { --m: 0px; margin-block: 0 } .ns-m-1 { --m: var(--s-1); margin-block: var(--s-1) }
  .ns-m-2 { --m: var(--s-2); margin-block: var(--s-2) } .ns-m-3 { --m: var(--s-3); margin-block: var(--s-3) }
  .ns-m-4 { --m: var(--s-4); margin-block: var(--s-4) } .ns-m-5 { --m: var(--s-5); margin-block: var(--s-5) }
  .ns-m-6 { --m: var(--s-6); margin-block: var(--s-6) }

  /* --- Tone -------------------------------------------------------------
   * A tone sets two inherited properties. Because custom properties inherit,
   * putting `ns-danger` on a <fieldset> colours every button, outline and
   * badge inside it through the style queries in `components` — one class at
   * the top, none on the children. That is the whole mechanism. */
  .ns-primary   { --tone: primary;   --tone-color: var(--primary) }
  .ns-secondary { --tone: secondary; --tone-color: var(--secondary) }
  .ns-cta       { --tone: cta;       --tone-color: var(--cta) }
  .ns-danger    { --tone: danger;    --tone-color: var(--danger) }
  .ns-success   { --tone: success;   --tone-color: var(--success) }
  .ns-warn      { --tone: warn;      --tone-color: var(--warn) }
  .ns-muted     { --tone: muted;     --tone-color: var(--muted); color: var(--muted) }
  .ns-highlight { --tone: highlight; --tone-color: var(--primary); background: var(--highlight) }

  /* A tone on a CONTAINER is read by the style query in `components`. A tone
   * on the element ITSELF cannot be — @container always asks an ancestor, so
   * a button's own class is invisible to it — and the commonest line anyone
   * writes is <button class="ns-primary">. Without this it renders quiet,
   * which is the sort of failure a screenshot catches and a test does not.
   * `modifiers` is a later layer than `components`, so it wins the neutral
   * default with no specificity contest. Ghost and outline opt out: they are
   * shapes that already say what to do with the tone. */
  :is(.ns-primary, .ns-secondary, .ns-cta, .ns-danger, .ns-success, .ns-warn, .ns-muted):where(button, [role="button"], input[type="submit"], input[type="reset"]):not(.ns-ghost, .ns-outline) {
    background: var(--tone-color);
    color: var(--bg);
    border-color: transparent;

    &:hover:not(:disabled) { background: color-mix(in oklab, var(--tone-color) 85%, black) }
    &:active:not(:disabled) { background: color-mix(in oklab, var(--tone-color) 75%, black) }
  }

  /* --- Size -------------------------------------------------------------- */
  .ns-sm    { --size: sm; font-size: .875rem }
  .ns-lg    { --size: lg; font-size: 1.125rem }
  .ns-dense { --dense: 1 }

  /* --- Text -------------------------------------------------------------- */
  .ns-small { font-size: .875rem }
  .ns-bold  { font-weight: 600 }
  .ns-mono  { font-family: var(--mono) }
  .ns-truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap }
  .ns-nowrap   { white-space: nowrap }
  .ns-right    { text-align: end }
  .ns-balance  { text-wrap: balance }

  /* --- Shape ------------------------------------------------------------- */
  .ns-ghost   { background: none; border-color: transparent }
  .ns-outline { background: none; border: var(--border) solid var(--tone-color); color: var(--tone-color) }
  .ns-pill    { border-radius: 999px }
  /* A pill wrapping a visually-hidden radio (the Sticky Notes colour row)
   * needs to say which one is picked some other way than the hidden input
   * itself; `:has()` reads the state without a script. */
  .ns-pill:has(input:checked) { outline: 2px solid var(--ink); outline-offset: 1px }
  .ns-square  { aspect-ratio: 1; padding-inline: 0; display: grid; place-items: center }
  .ns-full    { inline-size: 100% }

  /* --- Overlays ----------------------------------------------------------
   * A closed <dialog> and a closed [popover] are hidden by the USER AGENT,
   * and a user-agent rule loses to any author rule at all. Put a layout class
   * on one — `ns-menu` on a dropdown, `ns-stack` on a panel — and its
   * `display: flex` wins: the closed thing is laid out at the top-left of the
   * viewport at opacity 0 and silently eats every click underneath it. The
   * page looks broken in a way that shows up nowhere in the markup.
   *
   * So the user agent's rule is restated here, where it cannot lose: last in
   * `modifiers`, which is the layer those classes live in, and two selectors
   * deep, so more specific than any single class. `allow-discrete` on the
   * transitions still runs — the discrete step is what it is for. */
  [popover]:not(:popover-open) { display: none }
  dialog:not([open]) { display: none }

  /* --- Visibility -------------------------------------------------------- */
  .ns-hidden { display: none }
  .ns-sr {
    position: absolute; inline-size: 1px; block-size: 1px;
    padding: 0; margin: -1px; overflow: hidden; white-space: nowrap;
    clip-path: inset(50%); border: 0;
  }
  /* `ns-print` is print-only. There is deliberately no `ns-noprint` twin: the
   * registry is closed, and screen-only content is the default already. */
  .ns-print { display: none }
  @media print { .ns-print { display: revert } }
}
/* ========================================================== @end modifiers */

/* ======================================================== @block component-button */
@layer components {
  button, [role="button"], input[type="submit"], input[type="reset"] {
    display: inline-flex; align-items: center; justify-content: center;
    gap: var(--s-2);
    /* 44px on coarse pointers is the touch target; nothing depends on hover. */
    min-block-size: 44px;
    padding-inline: var(--s-4);
    border: var(--border) solid transparent;
    border-radius: var(--radius);
    background: var(--tone-color);
    color: var(--bg);
    font-weight: 500;
    text-decoration: none;
    cursor: pointer;
    transition: background-color .12s, border-color .12s;

    &:hover:not(:disabled) { background: color-mix(in oklab, var(--tone-color) 85%, black) }
    &:active:not(:disabled) { background: color-mix(in oklab, var(--tone-color) 75%, black) }
    &:disabled, &[aria-disabled="true"] {
      background: var(--disabled); color: var(--muted); cursor: not-allowed;
    }
  }
  @media (pointer: fine) {
    button, [role="button"] { min-block-size: 36px }
  }

  /* A button with no tone set is a quiet default, not a primary action. */
  @container style(--tone: neutral) {
    button, [role="button"] {
      background: var(--surface); color: var(--ink); border-color: var(--outline);
      &:hover:not(:disabled) { background: var(--highlight) }
    }
  }

  .ns-ghost:where(button, [role="button"]) {
    color: var(--tone-color);
    &:hover:not(:disabled) { background: color-mix(in oklab, var(--tone-color) 12%, transparent) }
  }
  .ns-outline:where(button, [role="button"]):hover:not(:disabled) {
    background: color-mix(in oklab, var(--tone-color) 12%, transparent);
  }
  .ns-sm:where(button, [role="button"]) { min-block-size: 32px; padding-inline: var(--s-3) }
  .ns-lg:where(button, [role="button"]) { min-block-size: 52px; padding-inline: var(--s-5) }
}
/* ========================================================== @end component-button */

/* ========================================================= @block component-field */
@layer components {
  form { --gap: var(--s-4) }

  label {
    display: block; font-weight: 500;
    & > input[type="checkbox"], & > input[type="radio"] {
      margin-inline-end: var(--s-2); vertical-align: middle;
    }
  }

  input:not([type="checkbox"], [type="radio"], [type="range"], [type="color"], [type="file"]),
  textarea, select {
    inline-size: 100%;
    min-block-size: 44px;
    padding: var(--s-2) var(--s-3);
    background: var(--surface);
    color: var(--ink);
    border: var(--border) solid var(--outline);
    border-radius: var(--radius);

    &:focus-visible { border-color: var(--tone-color) }
    /* `:user-invalid` fires only after the person has interacted, so an empty
     * required field is not scolded before it has been visited. */
    &:user-invalid { border-color: var(--danger) }
    &:disabled { background: var(--disabled); color: var(--muted) }
  }

  /* The textarea grows with its content, which is the one thing that would
   * otherwise want a script. */
  textarea { field-sizing: content; min-block-size: calc(1.7em * 3) }

  /* Checkboxes and radios drawn AS checkboxes and radios. A switch is neither
   * — it is the same input wearing a different shape — and this rule outranks
   * `.ns-switch` on specificity inside the same layer, so it says so here
   * rather than leaving the component to win a specificity race it loses. */
  input[type="checkbox"]:not(.ns-switch), input[type="radio"] {
    inline-size: 20px; block-size: 20px; min-block-size: 0;
  }

  fieldset { border: var(--border) solid var(--outline); border-radius: var(--radius); padding: var(--s-4) }
  legend { padding-inline: var(--s-2); font-weight: 600 }

  /* A field that contains an invalid control marks itself, so the label and
   * the hint can react without a class being added by the server. */
  label:has(:user-invalid), p:has(:user-invalid) { color: var(--danger) }

  /* The validation error, rendered as <output role="alert"> beside the field. */
  output[role="alert"] { display: block; color: var(--danger); font-size: .875rem }

  /* The flashed message after a form post. There is no toast, because there is
   * no JavaScript to show one. */
  output[role="status"] {
    display: block; padding: var(--s-3) var(--s-4);
    background: var(--highlight); border-inline-start: 3px solid var(--tone-color);
    border-radius: var(--radius);
  }

  progress, meter { inline-size: 100%; block-size: var(--s-2); border-radius: 999px }
}
/* =========================================================== @end component-field */

/* ========================================================= @block component-switch */
@layer components {
  /* Always our own pill, in every browser.
   *
   * This used to hand over to the native `switch` attribute behind
   * `@supports (appearance: base-select)` — and that guard asks about a
   * customisable <select>, which is a different feature entirely. Chromium
   * answers yes to it and no to `switch`, so the override fired, `appearance`
   * went back to `auto`, and every switch in the product rendered as an
   * ordinary checkbox. There is no CSS test for `input[switch]`, so there is
   * no detection here: one shape everywhere beats a guess that is wrong in the
   * most common browser. */
  .ns-switch {
    appearance: none;
    /* `min-inline-size` and `flex-shrink: 0`, not `inline-size` alone: a flex
     * row (`.ns-row`) gives every item `min-width: auto`, which for a plain
     * element with no intrinsic size is 0 — so a row tight on space shrank
     * the switch toward nothing instead of wrapping around it. */
    inline-size: 44px; min-inline-size: 44px; flex-shrink: 0;
    block-size: 24px; min-block-size: 0;
    padding: 2px;
    background: var(--outline);
    border: 0; border-radius: 999px;
    cursor: pointer;
    transition: background-color .15s;

    &::before {
      content: ""; display: block;
      inline-size: 20px; block-size: 20px;
      background: var(--surface); border-radius: 50%;
      transition: translate .15s;
    }
    &:checked { background: var(--tone-color) }
    &:checked::before { translate: 20px 0 }
  }
}
/* =========================================================== @end component-switch */

/* ========================================================== @block component-card */
@layer components {
  .ns-card {
    background: var(--surface);
    border: var(--border) solid var(--outline);
    border-radius: var(--radius);
    padding: var(--pad, var(--s-4));

    & > header { font-weight: 600 }
    & > footer {
      margin-block-start: var(--s-4);
      padding-block-start: var(--s-3);
      border-block-start: var(--border) solid var(--outline);
      display: flex; flex-wrap: wrap; gap: var(--s-2); align-items: center;
    }
  }
  @container style(--dense: 1) { .ns-card { padding: var(--s-2) } }
}
/* ============================================================ @end component-card */

/* ======================================================= @block component-callout */
@layer components {
  .ns-callout {
    display: flex; gap: var(--s-3);
    padding: var(--s-3) var(--s-4);
    background: color-mix(in oklab, var(--tone-color) 10%, var(--surface));
    border-inline-start: 3px solid var(--tone-color);
    border-radius: var(--radius);

    & > svg { flex: none; inline-size: 1.25em; block-size: 1.25em; color: var(--tone-color) }
  }
}
/* ========================================================= @end component-callout */

/* ========================================================= @block component-badge */
@layer components {
  .ns-badge {
    display: inline-flex; align-items: center; gap: var(--s-1);
    padding-inline: var(--s-2);
    min-block-size: 22px;
    font-size: .8125rem; font-weight: 500; line-height: 1;
    background: color-mix(in oklab, var(--tone-color) 16%, var(--surface));
    color: color-mix(in oklab, var(--tone-color) 80%, var(--ink));
    border-radius: 999px;
    white-space: nowrap;
  }
}
/* =========================================================== @end component-badge */

/* ======================================================== @block component-avatar */
@layer components {
  .ns-avatar {
    position: relative;
    display: inline-grid; place-items: center;
    inline-size: 36px; block-size: 36px; min-block-size: 0;
    padding: 0;
    background: var(--highlight); color: var(--ink);
    border: 0; border-radius: 50%;
    overflow: visible;
    font-size: .8125rem; font-weight: 600;

    & > img { inline-size: 100%; block-size: 100%; border-radius: 50%; object-fit: cover }

    /* An avatar that IS the image, rather than one wrapping it. `overflow` is
     * visible on the block so the status dot can sit outside the circle, and
     * with it visible Chromium does not clip a replaced element to the radius —
     * so an <img class="ns-avatar"> rendered as a square. A picture cannot
     * carry a status dot, so it can afford to clip. */
    &:is(img) { overflow: hidden; object-fit: cover }

    /* The presence dot. `data-status` says what, the stylesheet says where. */
    &[data-status]::after {
      content: ""; position: absolute; inset-block-end: 0; inset-inline-end: 0;
      inline-size: 10px; block-size: 10px;
      border: 2px solid var(--surface); border-radius: 50%;
    }
    &[data-status="online"]::after { background: var(--success) }
    &[data-status="away"]::after   { background: var(--warn) }
    &[data-status="offline"]::after{ background: var(--muted) }
  }
  .ns-sm.ns-avatar { inline-size: 24px; block-size: 24px }
  .ns-lg.ns-avatar { inline-size: 56px; block-size: 56px; font-size: 1.125rem }
}
/* ========================================================== @end component-avatar */

/* ========================================================= @block component-pills */
@layer components {
  /* A pillbox is checkboxes that look like pills; the input stays in the DOM
   * for the keyboard and the form, and is hidden only visually. */
  .ns-pills {
    display: flex; flex-wrap: wrap; gap: var(--s-2);
    border: 0; padding: 0;

    & label {
      display: inline-flex; align-items: center; gap: var(--s-1);
      min-block-size: 36px; padding-inline: var(--s-3);
      border: var(--border) solid var(--outline); border-radius: 999px;
      cursor: pointer; font-weight: 400;
    }
    & label:has(:checked) {
      background: color-mix(in oklab, var(--tone-color) 16%, var(--surface));
      border-color: var(--tone-color);
    }
    & label:has(:focus-visible) { outline: var(--ring) solid var(--tone-color); outline-offset: 2px }
    & input { position: absolute; opacity: 0; pointer-events: none }
  }
}
/* =========================================================== @end component-pills */

/* ===================================================== @block component-segmented */
@layer components {
  .ns-segmented {
    display: inline-flex; border: var(--border) solid var(--outline);
    border-radius: var(--radius); overflow: hidden; padding: 0;

    & label, & button {
      min-block-size: 36px; padding-inline: var(--s-4);
      border: 0; border-radius: 0; background: var(--surface); color: var(--ink);
      display: inline-flex; align-items: center; cursor: pointer; font-weight: 400;
    }
    & > * + * { border-inline-start: var(--border) solid var(--outline) }
    &.ns-sm label, &.ns-sm button { min-block-size: 28px; padding-inline: var(--s-2) }
    & label:has(:checked), & button[aria-pressed="true"] {
      background: var(--tone-color); color: var(--bg);
    }
    & input { position: absolute; opacity: 0; pointer-events: none }
  }
}
/* ======================================================= @end component-segmented */

/* ========================================================== @block component-menu */
@layer components {
  /* Dropdowns and context menus are the `popover` attribute plus anchor
   * positioning. No script: the browser opens, closes on Esc and on a click
   * outside, and restores focus. */
  [popover] {
    margin: 0; padding: var(--s-2);
    /* A popover sizes to its content, and content is not always short. This
     * keeps a wide one on the screen it opened from. */
    max-inline-size: min(560px, calc(100vw - var(--s-5)));
    max-block-size: min(80dvh, calc(100dvh - var(--s-8)));
    overflow: auto;
    background: var(--surface); color: var(--ink);
    border: var(--border) solid var(--outline); border-radius: var(--radius);
    box-shadow: var(--shadow-lg);
    opacity: 0;
    transition: opacity .12s, display .12s allow-discrete, overlay .12s allow-discrete;

    &:popover-open { opacity: 1 }
    /* The entry keyframe of a discrete transition; without this the popover
     * appears at full opacity and only the exit animates. */
    @starting-style { &:popover-open { opacity: 0 } }
  }

  .ns-menu {
    min-inline-size: 200px; max-inline-size: 320px;
    display: flex; flex-direction: column;
    list-style: none;

    & li > a, & li > button, & li form button {
      display: flex; align-items: center; gap: var(--s-2);
      inline-size: 100%; justify-content: flex-start;
      min-block-size: 40px; padding-inline: var(--s-3);
      background: none; border: 0; border-radius: var(--radius);
      color: var(--ink); text-decoration: none; font-weight: 400;
      &:hover { background: var(--highlight) }
    }
    & hr { margin-block: var(--s-1) }
  }

  @supports (anchor-name: --a) {
    /* NOT a shared `anchor-name`: one name declared on every trigger means
     * every popover on the page anchors to whichever trigger comes last in
     * the document. A popover invoked by `popovertarget` already has its
     * invoker as its implicit anchor, which is per-instance and correct. */
    [popover] {
      position-anchor: auto;
      position-area: block-end span-inline-start;
      position-try-fallbacks: block-start span-inline-start, block-end span-inline-end;
      margin-block-start: var(--s-1);
    }
  }
  @supports not (anchor-name: --a) {
    /* Fixed to the viewport rather than mispositioned against the anchor. */
    [popover] { position: fixed; inset-block-start: auto; inset-block-end: var(--s-4) }
  }
}
/* ============================================================ @end component-menu */

/* ======================================================== @block component-dialog */
@layer components {
  dialog {
    max-inline-size: min(560px, calc(100vw - var(--s-8)));
    max-block-size: calc(100dvh - var(--s-8));
    padding: var(--s-5);
    background: var(--surface); color: var(--ink);
    border: var(--border) solid var(--outline); border-radius: var(--radius);
    box-shadow: var(--shadow-xl);

    &::backdrop { background: color-mix(in oklab, black 45%, transparent) }
    & > form[method="dialog"] { display: flex; justify-content: flex-end; gap: var(--s-2) }

    /* Opened by `command="show-modal"` on a button and closed by
     * `command="close"` or a dialog form, so a modal costs no script. Both
     * the box and the backdrop have to be named for the fade to cover them. */
    opacity: 0;
    transition: opacity .16s, display .16s allow-discrete, overlay .16s allow-discrete;
    &[open] { opacity: 1 }
    @starting-style { &[open] { opacity: 0 } }

    &::backdrop {
      opacity: 0;
      transition: opacity .16s, display .16s allow-discrete, overlay .16s allow-discrete;
    }
    &[open]::backdrop { opacity: 1 }
    @starting-style { &[open]::backdrop { opacity: 0 } }
  }
}
/* ========================================================== @end component-dialog */

/* ========================================================= @block component-table */
@layer components {
  /* A table scrolls inside its own <figure>; the page body never scrolls
   * sideways. */
  figure:has(> table) { overflow-x: auto; margin: 0 }

  table { inline-size: 100%; border-collapse: collapse }
  caption { text-align: start; font-weight: 600; padding-block-end: var(--s-2) }
  th, td { padding: var(--s-2) var(--s-3); text-align: start; border-block-end: var(--border) solid var(--outline) }
  thead th { font-weight: 600; white-space: nowrap }
  thead th a { color: inherit; text-decoration: none; &:hover { text-decoration: underline } }
  th[aria-sort="ascending"] a::after  { content: " \2191" }
  th[aria-sort="descending"] a::after { content: " \2193" }
  /* Numbers align right and share one figure width so columns line up. */
  td[data-kind="number"], td[data-kind="money"], th[data-kind="number"], th[data-kind="money"] {
    text-align: end; font-variant-numeric: tabular-nums;
  }
  tbody tr:hover { background: var(--highlight) }
  @container style(--dense: 1) { th, td { padding: var(--s-1) var(--s-2) } }
}
/* =========================================================== @end component-table */

/* ========================================================== @block component-nav */
@layer components {
  body > header {
    display: grid; gap: var(--s-3);
    align-items: center;
    /* The bar is sticky, not fixed: it keeps its place in the flow, so no
     * page needs a magic top padding to sit clear of it. */
    position: sticky; inset-block-start: 0; z-index: 10;
    padding: var(--s-2) var(--s-4);
    background: var(--surface);
    border-block-end: var(--border) solid var(--outline);
    view-transition-name: ns-bar;
  }

  nav a { text-decoration: none; color: var(--ink) }
  nav a[aria-current="page"] { font-weight: 600; color: var(--tone-color) }

  /* --- Sidebar ---------------------------------------------------------
   * A sidebar STAYS. That is the whole component: the page scrolls and the
   * navigation does not, because a menu that scrolls away is a menu you have
   * to scroll back for.
   *
   * It stops below the bar rather than under it, and when it is taller than
   * the window it scrolls by itself — otherwise its last entries are
   * unreachable exactly when there are enough of them to need scrolling.
   */
  .ns-sidebar {
    position: sticky;
    inset-block-start: var(--stick, var(--bar));
    max-block-size: calc(100dvh - var(--bar) - var(--s-4));
    overflow-y: auto; overscroll-behavior: contain;
    scrollbar-width: thin;

    display: flex; flex-direction: column; gap: var(--s-1);
    view-transition-name: ns-sidebar;

    & a {
      display: flex; align-items: center; gap: var(--s-2);
      min-block-size: 40px; padding-inline: var(--s-3);
      border-radius: var(--radius);
      &:hover { background: var(--highlight) }
      &[aria-current="page"] { background: var(--highlight) }
    }

    /* A sidebar that groups itself is <details> per group: the index, not a
     * stack of cards. No border, no box, and the summary reads as a heading. */
    & > details {
      border: 0; border-radius: 0; padding: var(--s-1) 0;
      & > summary { min-block-size: 32px; display: flex; align-items: center; padding-inline: var(--s-2) }
      & > a { min-block-size: 32px; padding-inline: var(--s-5); color: var(--muted) }
      & > a:hover { color: var(--ink) }
    }

    /* A rule between groups of links, without the prose spacing a hr gets. */
    & hr { margin-block: var(--s-2) }
  }

  nav[aria-label="Breadcrumb"] ol {
    display: flex; flex-wrap: wrap; gap: var(--s-2); align-items: center;
    font-size: .875rem;
    & li + li::before { content: "/"; color: var(--muted); margin-inline-end: var(--s-2) }
  }

  [role="tablist"] {
    display: flex; gap: var(--s-1); border-block-end: var(--border) solid var(--outline);
    & [role="tab"] {
      padding: var(--s-2) var(--s-3);
      border-block-end: 2px solid transparent; margin-block-end: -1px;
      &[aria-selected="true"] { border-block-end-color: var(--tone-color); font-weight: 600 }
    }
  }

  nav[aria-label="Pages"] {
    display: flex; gap: var(--s-2); align-items: center; justify-content: center;
  }

  [role="search"] { display: flex; gap: var(--s-2); align-items: center }
}
/* ============================================================ @end component-nav */

/* ===================================================== @block component-details */
@layer components {
  details {
    border: var(--border) solid var(--outline); border-radius: var(--radius);
    padding: var(--s-3) var(--s-4);
    /* `interpolate-size: allow-keywords` in the reset is what makes this
     * animate to a height the stylesheet cannot know. */
    &::details-content {
      block-size: 0; overflow: hidden;
      transition: block-size .2s, content-visibility .2s allow-discrete;
    }
    &[open]::details-content { block-size: auto }
  }
  summary { cursor: pointer; font-weight: 600; &::marker { color: var(--muted) } }
}
/* ======================================================= @end component-details */

/* ======================================================= @block component-empty */
@layer components {
  .ns-empty {
    padding: var(--s-7) var(--s-4);
    text-align: center; color: var(--muted);
    & a { display: inline-block; margin-block-start: var(--s-3) }
  }
}
/* ========================================================= @end component-empty */

/* ====================================================== @block component-slider */
@layer components {
  /* A carousel with no script: scroll-snap does the paging and the browser
   * draws the dots where it supports scroll markers. */
  .ns-slider {
    display: flex; gap: var(--gap, var(--s-4));
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    scroll-behavior: smooth;
    padding-block-end: var(--s-4);

    & > * { flex: 0 0 min(320px, 85%); scroll-snap-align: center }

    @supports (scroll-marker-group: after) {
      scroll-marker-group: after;
      &::scroll-marker-group { display: flex; gap: var(--s-2); justify-content: center }
      & > *::scroll-marker {
        content: ""; inline-size: 8px; block-size: 8px;
        background: var(--outline); border-radius: 50%;
      }
      & > *::scroll-marker:target-current { background: var(--tone-color) }
    }
  }
}
/* ======================================================== @end component-slider */

/* =============================================================== @block app-board */
@layer app {
  /* Kanban. Moving a card is a <select> inside its form, never a drag: a drag
   * needs a script, and a select works on a phone and with a keyboard. */
  @scope (.ns-board) {
    :scope {
      display: grid; gap: var(--gap, var(--s-4));
      grid-auto-flow: column; grid-auto-columns: minmax(260px, 1fr);
      overflow-x: auto; align-items: start;
    }
    ul {
      display: flex; flex-direction: column; gap: var(--s-2);
      background: var(--highlight); border-radius: var(--radius);
      padding: var(--s-3); min-block-size: var(--s-8);
    }
    ul > li { margin-block-start: 0 }
    li > article {
      background: var(--surface); border: var(--border) solid var(--outline);
      border-radius: var(--radius); padding: var(--s-3);
    }
    h2, h3 { font-size: .875rem; text-transform: uppercase; letter-spacing: .04em;
             color: var(--muted) }
  }
}
/* ================================================================= @end app-board */

/* ============================================================== @block app-thread */
@layer app {
  /* A chat thread. Realtime is <meta http-equiv="refresh"> on the open
   * conversation; nothing here needs to know that. */
  @scope (.ns-thread) {
    :scope { display: flex; flex-direction: column; gap: var(--s-3); list-style: none }
    li { margin-block-start: 0 }
    article {
      max-inline-size: 68ch;
      background: var(--surface); border: var(--border) solid var(--outline);
      border-radius: var(--radius); padding: var(--s-2) var(--s-3);
    }
    article > header {
      display: flex; gap: var(--s-2); align-items: baseline;
      font-size: .8125rem; color: var(--muted); margin-block-end: var(--s-1);
    }
    article > header strong { color: var(--ink) }
    /* The viewer's own messages sit on the other side and take the tone. */
    li[data-mine] { align-self: flex-end }
    li[data-mine] article {
      background: color-mix(in oklab, var(--tone-color) 12%, var(--surface));
      border-color: color-mix(in oklab, var(--tone-color) 30%, var(--outline));
    }
    time { white-space: nowrap }
  }
}
/* ================================================================ @end app-thread */

/* ============================================================= @block app-desktop */
@layer app {
  /* The desktop: a grid of app icons that are links, each with its count. */
  @scope (.ns-desktop) {
    :scope {
      display: grid; gap: var(--gap, var(--s-5));
      grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
      padding: var(--s-5) 0;
    }
    a {
      display: flex; flex-direction: column; align-items: center; gap: var(--s-2);
      padding: var(--s-3) var(--s-2); border-radius: var(--radius);
      text-align: center; font-size: .875rem; color: var(--ink);
      min-block-size: 44px;
    }
    a:hover, a:focus-visible { background: var(--highlight) }
    svg, img { inline-size: 40px; block-size: 40px }
    /* The unread or pending count, as <data value>. */
    data {
      position: absolute; margin-block-start: -6px; margin-inline-start: 26px;
      min-inline-size: 20px; padding-inline: 5px;
      background: var(--danger); color: var(--bg);
      border-radius: 999px; font-size: .75rem; line-height: 20px;
    }
    a { position: relative }
  }
}
/* =============================================================== @end app-desktop */

/* ============================================================ @block app-calendar */
@layer app {
  /* A month is a <table>: seven columns, one <td> per day, events as links.
   * A table because a month IS tabular — days across, weeks down. */
  @scope (table.ns-calendar) {
    :scope { table-layout: fixed }
    td {
      vertical-align: top; block-size: 6rem; padding: var(--s-1);
      border: var(--border) solid var(--outline);
    }
    td > a:first-child { display: block; font-size: .8125rem; color: var(--muted) }
    td[data-today] { background: var(--highlight) }
    td[data-today] > a:first-child { color: var(--tone-color); font-weight: 600 }
    td[data-outside] { opacity: .45 }
    ul { display: flex; flex-direction: column; gap: 2px; margin-block-start: var(--s-1) }
    li { margin-block-start: 0 }
    li a {
      display: block; font-size: .75rem; padding-inline: var(--s-1);
      border-radius: 3px; background: color-mix(in oklab, var(--tone-color) 18%, var(--surface));
      overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    }
    @container (max-width: 640px) {
      /* Below a phone's width a month grid is unreadable; the same markup
       * becomes a day list, which is what a phone should show anyway. */
      :scope, thead, tbody, tr, td { display: block }
      thead { display: none }
      td { block-size: auto; border: 0; border-block-end: var(--border) solid var(--outline) }
      td:empty, td[data-outside] { display: none }
    }
  }

  /* Month, week and day are the same block with a different shape of data in
   * it, and which one you are looking at is three radio buttons. No script
   * decides the view, the document does, so the back button restores it. */
  :root:has(#cal-month:checked) .ns-calendar[data-view]:not([data-view="month"]),
  :root:has(#cal-week:checked)  .ns-calendar[data-view]:not([data-view="week"]),
  :root:has(#cal-day:checked)   .ns-calendar[data-view]:not([data-view="day"]) {
    display: none;
  }
}
/* ============================================================== @end app-calendar */


/* ========================================================= @block component-prose */
@layer components {
  /* Rendered Markdown, and the only place in the system where a list is prose
   * rather than furniture. The reset takes markers off every list because
   * menus, navs, pills, boards and threads are all lists and none of them
   * wants a bullet; a changelog does, and without one it reads as a wall of
   * paragraphs. Everything else prose needs — headings, code, blockquotes,
   * rules, tables, the spacing between blocks — is already in `base`. */
  .ns-prose {
    & :where(ul, ol) { padding-inline-start: var(--s-5); list-style: revert }
    & li + li { margin-block-start: var(--s-2) }
    & li > :where(ul, ol) { margin-block-start: var(--s-2) }
    & a { color: var(--tone-color) }
  }
}
/* =========================================================== @end component-prose */

/* ========================================================== @block component-icon */
@layer components {
  /* One sprite of Feather outlines is inlined once per page by <x-icons>; an
   * icon is <svg class="ns-icon"><use href="#i-home"></use></svg>. Stroke, not
   * fill, so an icon inherits the text colour it sits in and needs no variant
   * per tone. Sized in `em` so it tracks the type around it. */
  .ns-icon {
    inline-size: 1.15em; block-size: 1.15em;
    flex: none; vertical-align: -.19em;
    fill: none; stroke: currentColor;
    stroke-width: 2; stroke-linecap: round; stroke-linejoin: round;
  }
  .ns-icon.ns-lg { inline-size: 1.6em; block-size: 1.6em }
  .ns-icon.ns-sm { inline-size: .95em; block-size: .95em }
}
/* ============================================================ @end component-icon */

/* ======================================================= @block component-tooltip */
@layer components {
  /* A tooltip is an attribute, not a component: `data-tip` on anything. It is
   * deliberately NOT a replacement for a label — it appears on hover and on
   * keyboard focus, and anything a person must read before acting belongs in
   * the page. `aria-hidden` content is not offered, so the tip repeats a name
   * that is already accessible somewhere. */
  [data-tip] { position: relative }

  [data-tip]::after {
    content: attr(data-tip);
    position: absolute; z-index: 5;
    inset-block-end: calc(100% + var(--s-2));
    inset-inline-start: 50%; translate: -50% 0;
    padding: var(--s-1) var(--s-2);
    background: var(--ink); color: var(--bg);
    border-radius: var(--radius);
    font: 400 .78rem/1.3 var(--font);
    white-space: nowrap; pointer-events: none;
    opacity: 0; transition: opacity .12s .25s;
  }
  [data-tip][data-tip-side="bottom"]::after {
    inset-block: calc(100% + var(--s-2)) auto;
  }
  [data-tip]:hover::after,
  [data-tip]:focus-visible::after { opacity: 1 }

  /* A tip that would be read aloud twice is worse than no tip. */
  @media (prefers-reduced-motion: reduce) { [data-tip]::after { transition: none } }
}
/* ========================================================= @end component-tooltip */

/* ======================================================== @block component-drawer */
@layer components {
  /* A drawer is a <dialog> pinned to one edge. It opens with
   * `command="show-modal"` and closes with `command="close"` or a
   * <form method="dialog"> — no script, and the browser gives us the modal
   * backdrop, the Esc key, the focus trap and focus restoration for free. */
  dialog.ns-drawer {
    max-inline-size: none; max-block-size: none;
    margin: 0; padding: var(--s-5);
    border: 0; border-radius: 0;
    box-shadow: var(--shadow-xl);
    transition: translate .24s ease, display .24s allow-discrete, overlay .24s allow-discrete;
  }

  dialog.ns-drawer[data-side="left"],
  dialog.ns-drawer[data-side="right"] {
    inline-size: min(360px, 88vw); block-size: 100dvh;
  }
  dialog.ns-drawer[data-side="top"],
  dialog.ns-drawer[data-side="bottom"] {
    inline-size: 100vw; block-size: min(320px, 70dvh);
  }

  dialog.ns-drawer[data-side="left"]   { inset: 0 auto 0 0; border-inline-end: var(--border) solid var(--outline); translate: -100% 0 }
  dialog.ns-drawer[data-side="right"]  { inset: 0 0 0 auto; border-inline-start: var(--border) solid var(--outline); translate: 100% 0 }
  dialog.ns-drawer[data-side="top"]    { inset: 0 0 auto 0; border-block-end: var(--border) solid var(--outline); translate: 0 -100% }
  dialog.ns-drawer[data-side="bottom"] { inset: auto 0 0 0; border-block-start: var(--border) solid var(--outline); translate: 0 100% }

  dialog.ns-drawer[open] { translate: 0 0 }

  /* Without a starting style the drawer is already in place on the first
   * frame and only the closing animates. */
  @starting-style {
    dialog.ns-drawer[data-side="left"][open]   { translate: -100% 0 }
    dialog.ns-drawer[data-side="right"][open]  { translate: 100% 0 }
    dialog.ns-drawer[data-side="top"][open]    { translate: 0 -100% }
    dialog.ns-drawer[data-side="bottom"][open] { translate: 0 100% }
  }

  @media (prefers-reduced-motion: reduce) {
    dialog.ns-drawer { transition-duration: 1ms }
  }
}
/* ========================================================== @end component-drawer */

/* ======================================================== @block component-slides */
@layer components {
  /* A carousel with no script at all: scroll-snap for the movement, and the
   * browser's own generated markers and buttons for the controls. Where
   * ::scroll-marker is not supported the fallback is not broken, it is just a
   * horizontally scrolling strip — which is a usable carousel. */
  .ns-slides {
    display: flex; gap: var(--gap, var(--s-4));
    overflow-x: auto; overscroll-behavior-x: contain;
    scroll-snap-type: x mandatory; scroll-behavior: smooth;
    scrollbar-width: none;
    & > * { flex: 0 0 100%; scroll-snap-align: center; scroll-snap-stop: always }
    &::-webkit-scrollbar { display: none }
  }

  @supports selector(::scroll-marker) {
    .ns-slides {
      scroll-marker-group: after;

      &::scroll-marker-group {
        display: flex; gap: var(--s-2);
        justify-content: center; padding-block-start: var(--s-3);
      }
      & > *::scroll-marker {
        content: ""; inline-size: 9px; block-size: 9px;
        border: var(--border) solid var(--outline); border-radius: 50%;
        cursor: pointer;
      }
      & > *::scroll-marker:target-current { background: var(--tone-color); border-color: var(--tone-color) }
    }
  }
}
/* ========================================================== @end component-slides */

/* ========================================================= @block component-chart */
@layer components {
  /* Charting with no library. A chart is data in attributes and one custom
   * property per datum; the shape comes from the layout the browser already
   * has. It covers the four shapes a business screen actually asks for and
   * refuses the rest — anything needing an axis library is a table instead. */
  .ns-chart { --chart-h: 180px; color: var(--tone-color) }

  /* Vertical bars. `--v` is 0…1; the label and the value are attributes so a
   * bar carries its own annotation and the markup stays one element deep. */
  .ns-chart[data-chart="bar"] {
    display: flex; align-items: flex-end; gap: var(--s-2);
    block-size: var(--chart-h);
    padding-block: 1.1em 1.5em;
    background: repeating-linear-gradient(to top, var(--outline) 0 1px, transparent 1px 25%);

    & > * {
      flex: 1; min-inline-size: 4px;
      block-size: calc(var(--v, 0) * 100%); min-block-size: 2px;
      background: linear-gradient(to top, color-mix(in oklab, currentColor 65%, transparent), currentColor);
      border-radius: var(--radius) var(--radius) 0 0;
      position: relative;
    }
    & > *::before {
      content: attr(data-value);
      position: absolute; inset: auto 0 100% 0;
      text-align: center; font-size: .7rem; color: var(--muted);
      padding-block-end: 2px;
    }
    & > *::after {
      content: attr(data-label);
      position: absolute; inset: 100% 0 auto 0;
      text-align: center; font-size: .7rem; color: var(--muted);
      padding-block-start: var(--s-1);
    }
  }

  /* Horizontal bars, for when the labels are words rather than dates. */
  .ns-chart[data-chart="hbar"] {
    display: flex; flex-direction: column; gap: var(--s-2);

    & > * {
      display: grid; grid-template-columns: 8ch 1fr auto;
      align-items: center; gap: var(--s-3);
      font-size: .82rem;
    }
    & > *::before {
      content: attr(data-label);
      color: var(--muted); text-align: end;
      overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    }
    & > *::after { content: attr(data-value); color: var(--muted); font-variant-numeric: tabular-nums }
    & > * > * {
      block-size: 10px; inline-size: calc(var(--v, 0) * 100%); min-inline-size: 2px;
      background: currentColor; border-radius: 999px;
    }
  }

  /* A ring. One conic-gradient, one mask; the slices are an inline style. */
  .ns-chart[data-chart="donut"] {
    inline-size: 148px; aspect-ratio: 1; border-radius: 50%;
    display: grid; place-items: center;
    mask: radial-gradient(closest-side, transparent 61%, black 62%);
  }

  /* Lines and areas are an inline <svg>, because a polyline IS the data and
   * nothing else expresses it as honestly. The stylesheet only colours it. */
  .ns-chart[data-chart="line"] {
    display: block; inline-size: 100%; block-size: var(--chart-h);
    & svg { inline-size: 100%; block-size: 100%; overflow: visible }
    /* The line is a <polyline> and the area under it is a <path>, which is
     * the difference between them, so neither needs a class. */
    & polyline { fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round }
    & path { fill: color-mix(in oklab, currentColor 16%, transparent); stroke: none }
    & circle { fill: var(--surface); stroke: currentColor; stroke-width: 2 }
  }

  /* A legend is the list right after the chart, and reads as one with the
   * stylesheet removed. Adjacency is the relationship, so it needs no class. */
  .ns-chart + ul {
    display: flex; flex-wrap: wrap; gap: var(--s-2) var(--s-4);
    list-style: none; padding: 0; margin-block-start: var(--s-3);
    font-size: .8rem; color: var(--muted);

    & li { display: flex; align-items: center; gap: var(--s-2) }
    & li::before {
      content: ""; inline-size: 10px; block-size: 10px;
      border-radius: 3px; background: var(--tone-color, currentColor);
    }
  }
}
/* =========================================================== @end component-chart */

/* ======================================================== @block component-swatch */
@layer components {
  /* A colour role and the control that changes it. This is the one place in
   * the kit that needs `ui.js`: a colour input's value cannot reach a custom
   * property through CSS. Everything visible here still works without it —
   * the chip shows the role's current colour either way. */
  .ns-swatch {
    display: flex; align-items: center; gap: var(--s-2);
    font: 400 .76rem/1.2 var(--mono); color: var(--muted);

    & input[type="color"] {
      inline-size: 34px; block-size: 26px; flex: none; padding: 2px;
      background: var(--surface);
      border: var(--border) solid var(--outline); border-radius: var(--radius);
      cursor: pointer;
      &::-webkit-color-swatch-wrapper { padding: 0 }
      &::-webkit-color-swatch { border: 0; border-radius: calc(var(--radius) - 1px) }
      &::-moz-color-swatch { border: 0; border-radius: calc(var(--radius) - 1px) }
    }
  }
}
/* ========================================================== @end component-swatch */

/* ====================================================== @block component-progress */
@layer components {
  progress {
    inline-size: 100%; block-size: 9px;
    appearance: none; border: 0; border-radius: 999px;
    background: var(--highlight); color: var(--tone-color);
    overflow: hidden;

    &::-webkit-progress-bar { background: var(--highlight); border-radius: 999px }
    &::-webkit-progress-value { background: var(--tone-color); border-radius: 999px }
    &::-moz-progress-bar { background: var(--tone-color); border-radius: 999px }
  }

  /* No value means the length is unknown, and the bar says so by moving. */
  progress:not([value]) {
    background: repeating-linear-gradient(105deg,
      var(--tone-color) 0 12px, var(--highlight) 12px 24px);
    background-size: 200% 100%;
    animation: ns-progress 1s linear infinite;
  }
  @keyframes ns-progress { to { background-position: -48px 0 } }
  @media (prefers-reduced-motion: reduce) { progress:not([value]) { animation: none } }
}
/* ======================================================== @end component-progress */

/* =========================================================== @block component-note */
@layer components {
  /*
   * A Sticky Notes square. `--note-color` is the one thing this block does
   * not choose: the palette is six fixed colours and a note picks one on
   * write, so the value arrives inline the same way any other per-item
   * colour in this system does — there is no `ns-bg-*` class to hold it.
   */
  .ns-note {
    --note-color: var(--highlight);
    display: flex; flex-direction: column; gap: var(--s-2);
    inline-size: 220px; min-block-size: 220px;
    padding: var(--s-3);
    background: var(--note-color);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
    color: color-mix(in oklab, black 70%, var(--note-color));

    /* The square is TWO forms — the text and its Save button, then a
     * separate one-button form to remove it — because "the only control"
     * on the first is the brief's own phrase, and a second control on the
     * same form would contradict it. The first fills the square. */
    & form:first-of-type {
      display: flex; flex-direction: column; gap: var(--s-2);
      flex: 1; min-block-size: 0;
    }

    & textarea {
      flex: 1; resize: none; border: 0; background: none; padding: 0;
      font: inherit; color: inherit;
      &:focus-visible { outline: var(--ring) solid color-mix(in oklab, black 40%, var(--note-color)) }
    }

    & fieldset { border: 0; padding: 0; margin: 0; display: flex; gap: var(--s-1) }
  }
}
/* ============================================================= @end component-note */

/* ========================================================== @block component-thumb */
@layer components {
  /* A small file preview. Rounded, not circular — `border-radius: 50%`
   * is what `ns-avatar` means, and a document is not a person. Sized
   * the same way `ns-avatar` is, so the two sit at matching heights in
   * a list that mixes people and files. */
  .ns-thumb {
    display: block; inline-size: 36px; block-size: 36px;
    border-radius: var(--radius);
    object-fit: cover;
    background: var(--highlight);
  }
  .ns-sm.ns-thumb { inline-size: 24px; block-size: 24px }
  .ns-lg.ns-thumb { inline-size: 56px; block-size: 56px }
}
/* =========================================================== @end component-thumb */

/* ============================================================ @block transitions */
@layer base {
  /* Cross-document view transitions: the bar and the sidebar keep their
   * names (assigned in `components`) so they stay put while <main> crossfades.
   * A browser without it simply navigates, which is a working page. */
  @view-transition { navigation: auto }
  main { view-transition-name: ns-main }
}
/* ============================================================== @end transitions */
