/* Prism — layout foundation.
 *
 * SERVED FROM ONE PLACE (requirements.md R7). A change here reaches every
 * tenant on its next load, and so does a mistake (R8).
 *
 * WHAT IS IN HERE: the layout primitives, the foundation (colour, mode, type,
 * borders, the one interactive rule) and the atoms. **This header said "layout
 * only, nothing here sets a colour or a font" for three commits after both
 * stopped being true.** A comment that describes the previous version is worse
 * than no comment, because the next reader trusts it.
 *
 * WHY THE ELEMENTS HAVE NO SHADOW DOM. A layout primitive lays out its
 * CHILDREN, and a shadow root puts a <slot> between the grid and the things it
 * must place. Encapsulation is the right default for a component that OWNS its
 * internals; these own their children's positions instead, so they style light
 * DOM and the agent's content stays exactly where the agent put it.
 *
 * WHY GAP AND WIDTH ARE PURE CSS AND min/threshold ARE NOT. A fixed set of
 * values can be selected on (`[gap="5"]`), so those keep working with
 * JavaScript disabled or still loading. `min` and `threshold` take an arbitrary
 * length, which CSS cannot read off an attribute -- prism.js reflects those
 * into custom properties, and the defaults below are what renders until it
 * does. **Nothing is invisible or unlaid-out while the script is pending.**
 */

/* === BEGIN GENERATED @font-face -- tools/build-fonts.py === */
/* Anybody, Newsreader and Instrument Serif — all SIL Open Font License 1.1.
   The OFL requires the licence to travel with the fonts: fonts/OFL.txt.

   Vendored from Google Fonts, latin subset, VARIABLE axes.
   Self-hosted so the system has ONE origin (R7) and no tenant carries a
   third-party dependency the platform cannot see, measure or fix (R18).

   `font-display: swap` on purpose: text is readable in a fallback while the
   face loads. A logo or a heading that is INVISIBLE for 3 seconds is worse
   than one that reflows once. */
@font-face {
  font-family: 'Anybody';
  font-style: normal;
  font-weight: 500 800;
  font-display: swap;
  src: url('fonts/anybody-500-800-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Instrument Serif';
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url('fonts/instrument-serif-400-italic-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Instrument Serif';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('fonts/instrument-serif-400-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Newsreader';
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url('fonts/newsreader-400-italic-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Newsreader';
  font-style: normal;
  font-weight: 400 800;
  font-display: swap;
  src: url('fonts/newsreader-400-800-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* === END GENERATED @font-face === */
/* **THE CONTROL FACE: NEWSREADER, CENTRED ON ITS CAPITALS.** The file's own
 * ascent and descent (0.735 and 0.265 of the em) set a word 0.1em above the
 * middle of its line, so a button's label sat high beside its centred icon.
 * Here ascent minus descent is the cap height (0.67), which puts the capitals
 * in the exact middle of any line height. Same file, so nothing more is
 * fetched. Measured with fontTools on newsreader-400-800-latin.woff2. */
@font-face {
  font-family: 'Newsreader Control';
  font-style: normal;
  font-weight: 400 800;
  font-display: swap;
  src: url('fonts/newsreader-400-800-latin.woff2') format('woff2');
  ascent-override: 83.5%;
  descent-override: 16.5%;
  line-gap-override: 0%;
}

@layer prism.tokens, prism.layout, prism.atoms, prism.molecules, prism.compositions;

@layer prism.tokens {
  :root {
    /* --- spacing -------------------------------------------------------
     * STEP NAMES, NOT VALUE NAMES. A step's value CHANGES at a breakpoint,
     * so `--space-120` would be a token that renders 56px on a phone and
     * lies to the agent reading it. The step is the stable thing.
     *
     * NOTHING HERE IS CALCULATED. No clamp(), no vw. A calculated value is
     * never a round number, so nothing else in the system can align to it,
     * and no two elements using "the same" spacing agree at any width but
     * one.
     *
     * STEPS 1-6 ARE CONSTANT at every breakpoint. They are the spacing
     * INSIDE a component, and 4px that becomes 2px is a hairline nobody
     * asked for. Steps 7-10 are SECTION scale and step down, because
     * DESIGN.md 2.10's 120px is unreadable on a phone.
     */
    /* **STATUS IS A SCALE OF URGENCY, NOT FOUR CATEGORIES.** Two of the four
     * carry colour, and both mean something to do. Never a brand hue: green
     * sits between mint and orange, yellow between orange and mint, red between
     * pink and orange; grey has no hue. The same values in both modes. The -ink
     * pair is the words on a fill. */
    --status-neutral: #3F9E3A;
    --status-info: #9A9890;
    --status-warning: #F2CF2E;
    --status-critical: #E5262B;
    --status-warning-ink: #5A4400;
    --status-critical-ink: #F7F4E9;
    /* How much of the status colour goes into the surface for a state's
     * ground. One number, every intent, both modes. */
    --status-tint: 15%;
    --space-1: 4px;
    --space-2: 8px;
    --space-3: 12px;
    --space-4: 16px;
    --space-5: 24px;
    --space-6: 32px;

    /* Section scale and the page gutter, at breakpoint `s`. MOBILE FIRST:
     * these are the small-screen values and the media queries below raise
     * them, so a browser that matches no query still gets a whole set. */
    --space-7: 32px;
    --space-8: 40px;
    --space-9: 48px;
    --space-10: 56px;

    /* --- the gutter ----------------------------------------------------
     * THE PAGE EDGE, LEFT AND RIGHT. *The owner's word for it.* Fixed per
     * breakpoint, never calculated.
     *
     * **ZERO AT `s`.** A phone has no width to spend on a margin, and a
     * collection reaching the screen edge is the whole reason to be dense
     * there.
     *
     * The space BETWEEN columns is not this and has no token of its own:
     * it is `gap`, the same attribute every other primitive already takes,
     * off the same scale. One word, one thing.
     */
    /* **`0px`, NEVER A BARE `0`.** The gutter is added to lengths in
     * calc(), and a unitless 0 plus a length is invalid there: the
     * declaration dies at computed-value time and falls back to its initial
     * value, silently. */
    --gutter: 0px;

    /* --- measures, from DESIGN.md, named for the first time -------------
     * prose  3.5, the 700px long-form read width
     * wide   3.6, the 1100px broad composition
     * app    2.10, the 1280px container
     */
    --measure-prose: 700px;
    --measure-wide: 1100px;
    --measure-app: 1280px;

    /* --- the column grid ------------------------------------------------
     * THE PAGE GRID: the columns a composition is laid out ON. Distinct from
     * prism-grid, which flows a COLLECTION. Twelve because it divides by
     * 2, 3, 4 and 6, so halves, thirds and quarters are all whole columns.
     *
     * The space between columns is `gap`, off the shared spacing scale --
     * a grid with its own gutter numbers is a second spacing system, and
     * `--gutter` in this file means the PAGE EDGE and nothing else.
     */
    --columns: 12;

    /* Defaults every primitive falls back to when the agent says nothing.
     * **A primitive with no attributes must still be correct.** */
    /* Lucide is drawn for a 2px stroke on a 24 grid. One token, so the
     * whole set thickens or thins together and never one icon at a time. */
    --icon-stroke: 2;

    /* **THE EDGE IS THE OBJECT'S OWN SOLID COLOUR, PUSHED TOWARD THE
     * TEXT.** Toward the text is darker on paper and lighter on ink, so
     * one rule holds in both modes and on every surface — the argument
     * --surface-control already makes. An object with NO fill mixes from
     * transparent, which composites to the ground it sits on pushed the
     * same way. One number.
     *
     * **WHERE THE FILL IS A RAMP STEP, THE RAMP SUPPLIES THE EDGE INSTEAD**:
     * the next step toward the text, which is --interactive-edge on an
     * interactive fill and --interactive-tint-edge on a tint. The mix stands
     * only for a fill that is a BASE -- a control's ground, a card, the
     * secondary button -- because a base has no ramp. */
    --edge-mix: 15%;

    --grid-min: 280px;
    --reflow-threshold: 45rem;
    --sidebar-width: 20rem;
    --sidebar-content-min: 60%;
  }
}

/* --- THE BREAKPOINTS, AND THEY ARE THE ONLY ONES ---------------------------
 *
 * Four bands. 600 and 1280 raise the section scale, the page gutter and the
 * display type. 900 is where shape changes — the wall takes its fourth column,
 * the nav goes inline — and it raises neither the section scale, nor the
 * gutter, nor h1, nor h2; only the display steps --type-xl and --type-l
 * still move there.
 *
 * **NO PRIMITIVE EXPOSES A BREAKPOINT IN ITS API, AND THAT IS UNCHANGED.**
 * The listing tenant writes `type="listing"` and nothing else; which width
 * gets which count is the system's. R11 forbids a breakpoint in the API, not
 * a breakpoint inside the system.
 *
 * WHY VIEWPORT AND NOT CONTAINER, WHEN EVERYTHING ELSE IS CONTAINER-BASED.
 * A page edge is a fact about the SCREEN, not about the box that happens to
 * hold it. These are the system's only viewport-aware rules, and they are
 * here together so that stays countable rather than becoming a habit.
 *
 * AND THE AGENT NEVER SEES THEM. requirements.md R11: the consumer has no
 * eyes and cannot judge a width. It writes `gap="7"` and the system decides
 * what step 7 is worth at this size. **Breakpoints inside the system are not
 * the thing R11 forbids; breakpoints in the API are.**
 */
@layer prism.tokens {
  /*  m — AND IT HOLDS THROUGH l. The l band
   *  carried its own gutter (40) and its own section scale, which moved the
   *  brand 16px off the wall's constant 24px edge and grew the top margin at
   *  exactly the width the wall adds its fourth column. The section scale and
   *  the gutter now step at 600 and 1280 only. */
  @media (min-width: 600px) {
    :root {
      --space-7: 40px;  --space-8: 48px;  --space-9: 64px;  --space-10: 72px;
      --gutter: 24px;
    }
  }
  /*  xl -- DESIGN.md 2.10's own numbers: a 64px page edge, 120px section  */
  @media (min-width: 1280px) {
    :root {
      --space-7: 48px;  --space-8: 64px;  --space-9: 96px;  --space-10: 120px;
      --gutter: 64px;
    }
  }
}

@layer prism.layout {

  /* Gap is one mechanism, and `gap` is not an HTML attribute, so the
   * selector needs no element list at all. */
  [gap="0"]  { --_gap: 0 }
  [gap="1"]  { --_gap: var(--space-1) }
  [gap="2"]  { --_gap: var(--space-2) }
  [gap="3"]  { --_gap: var(--space-3) }
  [gap="4"]  { --_gap: var(--space-4) }
  [gap="5"]  { --_gap: var(--space-5) }
  [gap="6"]  { --_gap: var(--space-6) }
  [gap="7"]  { --_gap: var(--space-7) }
  [gap="8"]  { --_gap: var(--space-8) }
  [gap="9"]  { --_gap: var(--space-9) }
  [gap="10"] { --_gap: var(--space-10) }

  prism-stack, prism-grid, prism-columns,
  prism-cluster, prism-sidebar, prism-reflow, prism-switcher {
    gap: var(--_gap, var(--space-5));
  }

  /* --- the document itself ---------------------------------------------
   *
   * **BORDER-BOX, EVERYWHERE, AND THE SYSTEM OWNS IT.** A field's control is
   * `width: 100%` with 14px of padding each side and a 1px border — under the
   * default `content-box` that is 100% PLUS 30px, so it overflowed its column
   * by exactly 30px every time.
   *
   * **This is the white-frame defect again, in a second place.** Two proof
   * pages set `box-sizing` in their own scaffolding, so the gap was invisible
   * on both; `atoms.html`, which is where the fields are, did not.
   * *A fixture that does the system's job is a test that cannot fail*, and it
   * hid this one for the whole build.
   *
   * R6 again, in the smallest possible way: a line the system requires, does
   * not supply, and cannot check. It is LAYERED, so a tenant that genuinely
   * needs content-box somewhere can still say so — unlayered CSS wins.
   */
  *, *::before, *::after {
    box-sizing: border-box;
  }

  /* **THE SYSTEM OWNS THE DOCUMENT GROUND, BECAUSE R6 SAYS THE TENANT BUILDS
   * NOTHING.** Without this every tenant writes `body { margin: 0 }` forever,
   * which is the rule failing in the smallest possible way: a line the system
   * requires, does not supply, and cannot check.
   *
   * `<body>` carries a default 8px margin and the browser's white ground;
   * the system resets both, and no proof page resets `body` itself. *A test
   * fixture doing the system's job is a test that cannot fail.*
   *
   * `html` takes the ground rather than `body`, so overscroll and the area
   * beyond a short page are the mode's colour and not white. The `:has()`
   * pair propagates an explicit mode UP from prism-page, which is where a
   * tenant declares it; with no mode stated, the root default already follows
   * prefers-color-scheme.
   */
  html {
    background: var(--surface-default);
    color-scheme: dark light;
    /* **AN ANCHOR LANDS BELOW THE STICKY BAR**, not under it: the bar's
     * tallest floor (56px) and a step of air. */
    scroll-padding-block-start: calc(56px + var(--space-5) + env(safe-area-inset-top, 0px));
  }
  html:has(prism-page[mode="light"]) { background: var(--_paper); color-scheme: light }
  html:has(prism-page[mode="dark"])  { background: var(--_ink);   color-scheme: dark }

  body {
    margin: 0;
    /* **CLEAR OF THE SCREEN'S SIDES.** With viewport-fit=cover (prism.js) a
     * phone on its side puts the camera over the page's edge; the inset is 0
     * everywhere else. */
    padding-inline: env(safe-area-inset-left, 0px) env(safe-area-inset-right, 0px);
    background: var(--surface-default);
    color: var(--text-default);
    font: 400 var(--type-body) / 1.65 var(--font-body);
    -webkit-font-smoothing: antialiased;
  }
  body:has(prism-page[mode="light"]) {
    --surface-default: var(--_paper);
    --text-default: var(--_paper-text);
  }
  body:has(prism-page[mode="dark"]) {
    --surface-default: var(--_ink);
    --text-default: var(--_ink-text);
  }

  /* **`hidden` HIDES, ON EVERY ELEMENT.** The platform's own attribute, and
   * the browser's rule for it is the weakest in the cascade: every prism
   * element that sets a `display` — which is nearly all of them — would
   * silently keep showing. So the system restates it, important so no layer
   * above can outvote it. One button taking another's place is `hidden` on
   * each, and nothing else. */
  [hidden]:not([hidden="until-found"]) { display: none !important }

  /* --- prism-page, -header, -content, -footer --------------------------
   * THE PAGE SHELL. Four elements, and between them they do exactly two
   * things: stack the page top to bottom, and keep the footer at the
   * bottom when the content is too short to push it there.
   *
   * **THEY ARE FULL WIDTH AND CONSTRAIN NOTHING.** A header whose measure
   * was built in cannot carry a background band across the viewport, which
   * is most of what a header is for. So the measure stays where it already
   * lives -- nest a prism-container inside. One mechanism, used everywhere,
   * instead of a second one hidden in three elements.
   */
  prism-page {
    display: flex;
    flex-direction: column;
    min-height: 100dvh;
  }
  prism-header,
  prism-footer { flex: none }
  /* The one line that pins the footer down on a short page. */
  /* **THE CONTENT STANDS CLEAR OF THE BAR, AND OF THE FOOTER.** The proof
   * pages had padded their own first block, which hid it — the shell owns
   * this air, a step of --space-8 (40, 48, 64px by width). */
  prism-content { flex: 1; min-inline-size: 0; padding-block: var(--space-8) }

  /* --- prism-header ----------------------------------------------------
   * **IT ALWAYS STICKS.** The `sticky` attribute is accepted and does
   * nothing: sticking is the behaviour, and removing an attribute a tenant
   * may have written is the one thing R20 forbids.
   *
   * **A BLUR AND NO COLOUR.** The bar has no ground of its own; what it
   * holds carries the page's colour as a drop shadow instead (below, with
   * the mark's). Its tint is one layer, below (R38). It sits below the
   * contents (the header is a stacking context), so the brand and the
   * menu stay sharp. The dropdown panel keeps its own ground,
   * `.prism-nav-items`'.
   */
  prism-header {
    position: sticky;
    top: 0;
    z-index: 10;
    isolation: isolate;
  }
  /* **THE PAGE REACHES THE TOP OF THE SCREEN; THE BAR HAS NO GROUND.** The
   * system sets viewport-fit=cover (prism.js), so the bar starts at the
   * screen's top edge, behind the clock, and keeps its contents below it by
   * the safe-area inset. — What scrolls behind passes the bar untouched; the
   * items keep their shadows. */
  prism-header { padding-block-start: env(safe-area-inset-top, 0px) }

  /* --- prism-container -------------------------------------------------
   * Centres a measure and holds the gutter. The max-width ADDS the gutter
   * so `width="prose"` means 700px OF PROSE, not 700px minus padding --
   * otherwise every measure in DESIGN.md silently narrows by 128px.
   *
   * **CONTENT THAT FLOATS ON THE PAGE — text, buttons, fields — HAS 8px TO THE
   * PAGE SIDES WHERE THE GUTTER IS 0. A SURFACE DOES NOT: a card sits at 0.**
   * So on a phone the container pads 8px and the surfaces pull back out by the
   * same 8px; from 600px there is a gutter and neither happens. A surface
   * inside another surface, a grid or a row is placed by that, and is left
   * alone.
   */
  prism-container {
    --_measure: var(--measure-app);
    /* The pull, INHERITED by what the container holds: -8px on a phone, 0 at
     * every width with a gutter. One property, so the breakpoint lives here
     * and the surface rule below never needs a second, weaker copy. */
    --_pull: calc(var(--space-2) * -1);
    display: block;
    margin-inline: auto;
    padding-inline: var(--space-2);
    max-width: calc(var(--_measure) + (var(--space-2) * 2));
  }
  @media (min-width: 600px) {
    prism-container {
      --_pull: 0px;
      padding-inline: var(--gutter);
      max-width: calc(var(--_measure) + (var(--gutter) * 2));
    }
  }
  prism-container:not([width="bleed"]) :is(
    prism-card, prism-image-card, prism-image, prism-video, prism-player,
    prism-grid:has(> :is(prism-card, prism-image-card))
  ):not(:is(prism-card, prism-image-card, prism-player, prism-grid, prism-columns,
            prism-cluster, prism-sidebar, prism-reflow, prism-switcher, prism-container[width="bleed"]) *) {
    /* A video or player that narrows to fit the screen is centred instead
     * (--_centre, at A15); everything else has none and is pulled. */
    margin-inline: max(var(--_pull), var(--_centre, var(--_pull)));
  }
  prism-container[width="prose"] { --_measure: var(--measure-prose) }
  prism-container[width="wide"]  { --_measure: var(--measure-wide) }
  prism-container[width="app"]   { --_measure: var(--measure-app) }
  prism-container[width="full"]  { max-width: none }
  /* Edge-to-edge: no gutter, no measure. For a full-bleed image or a bar. */
  prism-container[width="bleed"] { max-width: none; padding-inline: 0 }

  /* --- prism-stack -----------------------------------------------------
   * Vertical rhythm, and THE most common primitive. It also strips the
   * margins browsers put on headings and paragraphs, because two spacing
   * systems fighting is how vertical rhythm dies.
   */
  prism-stack {
    display: flex;
    flex-direction: column;
  }
  prism-stack > * {
    margin-block: 0;
  }
  /* An agent that wants one child pushed to the bottom -- a card footer,
   * an action row -- says so on the CHILD, which is where it is true. */
  prism-stack > [data-push="end"] {
    margin-block-start: auto;
  }

  /* --- prism-grid ------------------------------------------------------
   * THE COLLECTION PRIMITIVE: any set of like items, of unknown length,
   * where the ITEM has a natural size and the column count falls out of it.
   * The agent states one thing -- how narrow an item may get -- and every
   * screen is covered without a breakpoint.
   *
   * `min()` against 100% is load-bearing. `minmax(280px, 1fr)` OVERFLOWS a
   * container narrower than 280px, which is a phone with a sidebar open.
   * Clamping the minimum to the container is what makes "any screen" true.
   *
   * auto-fill, NOT auto-fit, is the default: auto-fit collapses the empty
   * tracks, so a set holding two items renders them half a screen wide
   * each. `stretch` opts into that where it is wanted.
   */
  prism-grid {
    display: grid;
    grid-template-columns: repeat(
      auto-fill,
      minmax(min(var(--grid-min), 100%), 1fr)
    );
  }
  prism-grid[stretch] {
    grid-template-columns: repeat(
      auto-fit,
      minmax(min(var(--grid-min), 100%), 1fr)
    );
  }
  /* --- prism-grid[type="listing"] · THE IMAGE WALL
   * ---------------------- **THE COLUMN COUNT IS THE SYSTEM'S, PER
   * BAND.**
   *
   *   band  width       gutter  gap  cols   thumb
   *   s     0-599         0      2    3     105 -> 198
   *   m     600-899      24      4    3     181 -> 281
   *   l     900-1279     24      4    4     210 -> 305
   *   xl    1280+        24      8    4     302 -> 314
   *
   * **THE s GAP IS A HAIRLINE 2px, OFF THE SCALE ON PURPOSE.** The spacing
   * scale starts at 4; a wall on a phone wants near-seamless thumbs, and 2px
   * is a value the scale does not carry — a literal, like the nav's 46 and
   * the slider's 12, with its reason attached.
   *
   * **WHY A COUNT PER BAND RATHER THAN THE INTRINSIC min.** A wall wants a
   * FIXED number of big items, not a constant item size with more of them: at
   * `min="104px"` a 1280px desktop renders eleven 110px thumbs, which is a
   * contact sheet. The tenant states what the grid is FOR and the system holds
   * the density, so two apps in the suite cannot drift.
   *
   * **THE ONE VISIBLE STEP IS 900px AND IT IS ARITHMETIC, NOT A CHOICE.**
   * Adding a column always shrinks the item — four columns are three quarters
   * of three at the same width — so 3 -> 4 costs 25% wherever it is put.
   * Earlier makes the absolute drop smaller and later makes it larger; the
   * ratio is fixed. The alternative is many small steps, which is a denser
   * wall; the owner's is the fewer, bigger items.
   *
   * **IT CARRIES ITS OWN EDGES, AND THAT IS WHY IT IS NOT JUST TRACKS.** The
   * wall's gutter is 0 then a constant 24, where the PAGE's gutter runs
   * 0/24/40/64. They are different measurements: the page edge answers to the
   * screen, the wall's answers to the wall. So the listing sets its own
   * max-width and inline padding and belongs in a `prism-container
   * width="bleed"`, or straight in a `prism-surface`. Nested in a padded
   * container it is simply narrower, which is visible rather than silent.
   *
   * **NEVER WIDER THAN THE ASSET.** The derivative is 480px, and the container
   * cap is what keeps the promise — four columns of a 1280px content box is
   * 314px, and no monitor makes it bigger. A full-bleed wall at four columns is
   * 634px on a 2560px screen, which is a 480px file upscaled.
   */
  prism-grid[type="listing"] {
    /* The measure PLUS its own padding, so the content box is exactly the
     * measure at the top band and four columns land on 314px. */
    max-inline-size: calc(var(--measure-app) + var(--space-5) * 2);
    margin-inline: auto;
    padding-inline: 0;
    gap: 2px;
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
  @media (min-width: 600px) {
    prism-grid[type="listing"] { padding-inline: var(--space-5); gap: var(--space-1) }
  }
  @media (min-width: 900px) {
    prism-grid[type="listing"] { grid-template-columns: repeat(4, minmax(0, 1fr)) }
  }
  @media (min-width: 1280px) {
    prism-grid[type="listing"] { gap: var(--space-2) }
  }

  /* An item that takes the whole row rather than one track -- a heading
   * inside the set, or an empty state. */
  prism-grid > [data-span="all"] {
    grid-column: 1 / -1;
  }

  /* --- prism-columns ---------------------------------------------------
   * THE PAGE GRID. Twelve columns; a child states how many it spans.
   *
   * NOT THE SAME THING AS prism-grid, and the difference is which side
   * decides. Here the GRID is fixed and the item says how much of it to
   * take -- that is composition. In prism-grid the ITEM's minimum decides
   * and the column count falls out of it -- that is a collection. An
   * agent picking the wrong one gets a collection that will not align to
   * the page, or a page that reflows when one item is added.
   *
   * THE STACK IS A CONTAINER QUERY, NOT A MEDIA QUERY. Every other
   * primitive here answers to its container; a column grid that answered
   * to the viewport would stay twelve columns wide inside a narrow panel
   * on a wide screen, which is the exact failure the rest of this file
   * avoids. `container-type: inline-size` is what makes @container read
   * this element's own width.
   */
  prism-columns {
    container-type: inline-size;
    display: grid;
    grid-template-columns: repeat(var(--columns), minmax(0, 1fr));
  }
  prism-columns[columns="2"]  { --columns: 2 }
  prism-columns[columns="3"]  { --columns: 3 }
  prism-columns[columns="4"]  { --columns: 4 }
  prism-columns[columns="6"]  { --columns: 6 }
  prism-columns[columns="12"] { --columns: 12 }

  /* **A child with no span takes the whole row**, not one column. An
   * unspanned element is an element whose author said nothing about
   * width, and one twelfth is never what that means. */
  prism-columns > * { grid-column: 1 / -1 }
  prism-columns > [span="1"]  { grid-column: span 1 }
  prism-columns > [span="2"]  { grid-column: span 2 }
  prism-columns > [span="3"]  { grid-column: span 3 }
  prism-columns > [span="4"]  { grid-column: span 4 }
  prism-columns > [span="5"]  { grid-column: span 5 }
  prism-columns > [span="6"]  { grid-column: span 6 }
  prism-columns > [span="7"]  { grid-column: span 7 }
  prism-columns > [span="8"]  { grid-column: span 8 }
  prism-columns > [span="9"]  { grid-column: span 9 }
  prism-columns > [span="10"] { grid-column: span 10 }
  prism-columns > [span="11"] { grid-column: span 11 }
  prism-columns > [span="12"] { grid-column: 1 / -1 }

  /* An item placed at an explicit column rather than flowed. */
  prism-columns > [start="1"]  { grid-column-start: 1 }
  prism-columns > [start="2"]  { grid-column-start: 2 }
  prism-columns > [start="3"]  { grid-column-start: 3 }
  prism-columns > [start="4"]  { grid-column-start: 4 }
  prism-columns > [start="5"]  { grid-column-start: 5 }
  prism-columns > [start="6"]  { grid-column-start: 6 }
  prism-columns > [start="7"]  { grid-column-start: 7 }
  prism-columns > [start="8"]  { grid-column-start: 8 }
  prism-columns > [start="9"]  { grid-column-start: 9 }
  prism-columns > [start="10"] { grid-column-start: 10 }
  prism-columns > [start="11"] { grid-column-start: 11 }
  prism-columns > [start="12"] { grid-column-start: 12 }

  /* **BELOW 45rem OF ITS OWN WIDTH, EVERY SPAN GOES FULL WIDTH.**
   *
   * **IT MATCHES ON `[span]` AND `[start]`**, at the specificity of the
   * rules that set them, so a spanned child stacks too.
   *
   * **SELECTING ON `[span]` AND `[start]` TIES THE SPECIFICITY**, and source
   * order decides — which is why this block sits below both of them and must
   * stay there. *A rule whose correctness depends on where it is written is a
   * rule that will be moved; `tools/render-check.py` measures the outcome so
   * the move is caught rather than trusted.*
   *
   * **A CONTAINER QUERY, NOT A MEDIA QUERY**, so it stacks inside a narrow
   * panel on a wide screen — which is the promise the manifest already makes.
   *
   * **AND THE NUMBER IS A LITERAL BECAUSE IT HAS TO BE:** a container query
   * condition cannot read a custom property, and a token that cannot be used
   * is a second truth. */
  @container (max-width: 45rem) {
    prism-columns > [span],
    prism-columns > [start] { grid-column: 1 / -1 }
  }

  /* **THE GAP SET FOR A COLUMN GRID IS SMALLER THAN THE SPACING SET,
   * AND THIS IS WHY.** At the 1280px container, `gap="8"` puts 11 x
   * 64px of gap against 576px of column -- 48px each, so the space
   * between two columns is wider than either column. It rendered, and
   * rendering something indefensible without complaining is the failure
   * CLAUDE.md names.
   *
   * Steps 7-10 are SECTION rhythm, not column gaps. They clamp to step 6
   * here so the grid still lays out, and prism.js says so out loud -- an
   * agent cannot see a grid it has ruined. */
  prism-columns[gap="7"],
  prism-columns[gap="8"],
  prism-columns[gap="9"],
  prism-columns[gap="10"] { --_gap: var(--space-6) }

  /* --- prism-cluster ---------------------------------------------------
   * Inline things that wrap: tags, filter chips, an action row. Wrapping
   * is the default because a row of unknown-length agent-supplied labels
   * WILL overflow otherwise, and silently.
   */
  prism-cluster {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
  }
  prism-cluster[justify="end"]     { justify-content: flex-end }
  prism-cluster[justify="center"]  { justify-content: center }
  prism-cluster[justify="between"] { justify-content: space-between }
  prism-cluster[align="start"]     { align-items: flex-start }
  prism-cluster[align="end"]       { align-items: flex-end }
  prism-cluster[align="stretch"]   { align-items: stretch }

  /* --- prism-sidebar ---------------------------------------------------
   * THE APP SHELL, and it needs no media query. Two children: the sidebar
   * keeps its width; the content takes the rest until it would be squeezed
   * past `--sidebar-content-min`, at which point the pair wraps to stacked.
   *
   * The flex-grow: 999 is what forces that wrap rather than a slow squeeze:
   * the content cannot shrink below its min, so when it would have to, the
   * line breaks instead.
   */
  prism-sidebar {
    display: flex;
    flex-wrap: wrap;
  }
  prism-sidebar > :first-child {
    flex-basis: var(--sidebar-width);
    flex-grow: 1;
  }
  prism-sidebar > :last-child {
    flex-basis: 0;
    flex-grow: 999;
    min-inline-size: var(--sidebar-content-min);
  }
  /* side="end" puts the sidebar last. The roles swap; nothing else does. */
  prism-sidebar[side="end"] > :first-child {
    flex-basis: 0;
    flex-grow: 999;
    min-inline-size: var(--sidebar-content-min);
  }
  prism-sidebar[side="end"] > :last-child {
    flex-basis: var(--sidebar-width);
    flex-grow: 1;
    min-inline-size: auto;
  }

  /* --- prism-reflow ----------------------------------------------------
   * Side by side until the CONTAINER cannot hold it, then stacked. Note
   * that it answers to the container and not the viewport, so a reflow
   * inside a sidebar's content column stacks at the right moment rather
   * than at the moment the window happens to be narrow.
   *
   * **`prism-switcher` IS THE SAME ELEMENT UNDER ITS FIRST NAME, KEPT FOR
   * GOOD.** R20 — no system change may require a tenant action — so a
   * tenant that wrote the first name keeps a working layout. Every rule
   * below names both, and a rule naming one is a rule the other silently
   * lacks.
   */
  :is(prism-reflow, prism-switcher) {
    display: flex;
    flex-wrap: wrap;
  }
  :is(prism-reflow, prism-switcher) > * {
    flex-grow: 1;
    flex-basis: calc((var(--reflow-threshold) - 100%) * 999);
  }
  /* Past this count, stacking is the only readable answer whatever the
   * width -- five things side by side are five columns nobody can read. */
  :is(prism-reflow, prism-switcher) > :nth-last-child(n+5),
  :is(prism-reflow, prism-switcher) > :nth-last-child(n+5) ~ * {
    flex-basis: 100%;
  }
}


/* ===========================================================================
 * F1-F5 · THE FOUNDATION.  DESIGN.md 2.1, 2.2, 2.5, 2.6, 2.8, 2.9.
 * Transcribed from the owner's specification. Where a value was not in it,
 * the comment says so and says it is a choice.
 * ======================================================================== */

@layer prism.tokens {
  :root {
    /* --- F1 · colour, DESIGN.md 2.1 ------------------------------------
     * Structural bases. The underscore means INTERNAL: a component rule
     * never names these, only the semantic variables below. */
    --_ink: #0D0F18;
    --_paper: #F4F2EC;
    --_ink-text: #F7F4E9;
    --_paper-text: #15171F;
    --_ink-text-75: #BCBBB5;
    --_paper-text-75: #4D4E52;
    --_shadow: #1E2130;
    --_mist: #D6D3C9;

    /* **FOUR HUES, TWO COMPLEMENTARY PAIRS.** Pink and mint are the emblem's
     * two lenses; blue is where they overlap; orange is the overlap's
     * complement, blue rotated 180 degrees at the same saturation and
     * lightness. Each hue is a RAMP of seven steps, 100 lightest to 700
     * darkest, and the brand value is one named step of it -- not the same step
     * in every hue: mint is a tint, pink a mid-tone, blue already an ink. The
     * brand step is never dimmed, tinted or blended; the ramp around it is what
     * a role reads. */
    --pink-100: #FFE0F2;
    --pink-200: #FFB0DB;
    --pink-300: #FF76C4;
    --pink-400: #FF1DA5;  /* brand */
    --pink-500: #D4008A;
    --pink-600: #A3006A;
    --pink-700: #6B0046;
    --mint-100: #D9FFEC;
    --mint-200: #6BFCB8;  /* brand */
    --mint-300: #35E39A;
    --mint-400: #14C180;
    --mint-500: #0E9A66;
    --mint-600: #0A7257;
    --mint-700: #084E3D;
    --blue-100: #DCEBFC;
    --blue-200: #A9CDF8;
    --blue-300: #6CAAF0;
    --blue-400: #3B92EC;
    --blue-500: #0F7AE5;  /* brand */
    --blue-600: #0A5FB4;
    --blue-700: #063E78;
    --orange-100: #FDEBD8;
    --orange-200: #FAC99A;
    --orange-300: #F5A35A;
    --orange-400: #EF8A2E;
    --orange-500: #E67A0F;  /* brand */
    --orange-600: #B35E0A;
    --orange-700: #7A3F06;

    /* The brand step of each hue, under the hue's own name. */
    --pink: var(--pink-400);
    --mint: var(--mint-200);
    --blue: var(--blue-500);
    --orange: var(--orange-500);

    /* **THE DATA COLOURS: THE 400 STEP OF EVERY HUE, IN BOTH MODES.** Measured
     * on paper, where the card is in dark mode: pink 3.14, mint 2.09, blue
     * 2.88, orange 2.25; on ink: 5.44, 8.16, 5.92, 7.60. */
    --data-pink: var(--pink-400);
    --data-blue: var(--blue-400);
    --data-mint: var(--mint-400);
    --data-orange: var(--orange-400);

    /* **HOW FULL, AS A TEMPERATURE.** Five stops, the colours the system
     * already has: the data blue, the status green and yellow, the data orange,
     * the status red. prism.js mixes the two around a bar's share in oklab. */
    --_heat-0: var(--data-blue);
    --_heat-1: var(--status-neutral);
    --_heat-2: var(--status-warning);
    --_heat-3: var(--data-orange);
    --_heat-4: var(--status-critical);

    /* Semantic roles. */
    --chromatic: var(--pink);
    --technical: var(--blue);
    --warmth: var(--mint);
    /* **A ROLE NAMES A STEP, NEVER A HEX**, and the step is the mode's --
     * so --interactive and its tint, text and edge steps are set per mode
     * below, one line per role. */

    /* --- F3 · type, DESIGN.md 2.5 --------------------------------------
     * Three faces, three voices. Every stack ends in a real fallback: a
     * font that fails to load must not take the layout with it. */
    /* **THE DISPLAY VOICE IS NEWSREADER.** Headings, captions, buttons and
     * menus now speak the body's face at their own weights, 500 to 700, which
     * Newsreader's variable file carries. Anybody is still vendored and
     * declared, and the browser never fetches a face nothing uses. */
    --font-display: "Newsreader", Georgia, "Times New Roman", serif;
    --font-body: "Newsreader", Georgia, "Times New Roman", serif;
    --font-meta: "Instrument Serif", Georgia, serif;
    /* The words of a control, centred on their capitals: buttons, chips and
     * linked tags, whose labels stand beside a centred icon. */
    --font-control: "Newsreader Control", "Newsreader", Georgia, "Times New Roman", serif;
  }

  /* --- F2 · the mode system, DESIGN.md 2.2 -----------------------------
   * Every semantic variable is remapped per mode; at a mode change the
   * whole system resolves again.
   *
   * DESIGN CHOICE, NOT A RULE -- flagged, and the owner took it: 2.2 puts
   * the mode on a `<body>` class. `<body>` is the TENANT'S markup and R6
   * says the app builds nothing, so it lives on prism-page instead. The
   * body class is kept working beside it, because DESIGN.md specifies it
   * and preview_prism_11.html uses it.
   */
  :root,
  prism-page[mode="dark"],
  .mode-dark {
    --surface-default: var(--_ink);
    --surface-inverted: var(--_paper);
    --text-default: var(--_ink-text);
    --text-inverted: var(--_paper-text);
    --text-dimmed: var(--_ink-text-75);
    /* The secondary button's fill: ink lifted 5% toward the text, the dark
     * counterpart of white on paper. See A3. */
    --_secondary-fill: color-mix(in srgb, var(--text-default) 5%, var(--surface-default));
    --border-default: var(--_shadow);
    --border-inverted: var(--_mist);
    /* The dimmed level AS SEEN ON THE INVERTED SURFACE. 2.6's dimmed is a
     * pre-mixed value against ITS OWN ground, so it needs one per ground. */
    --text-dimmed-inverted: var(--_paper-text-75);
    /* On ink a role reads: ink 300, tint 700, text on tint 200. The
     * interactive hue is pink, and its ink step here is the brand step itself,
     * 400: pink carries on ink and blue 300 is the weaker of the two. Pink 400
     * on ink 5.44:1. An interactive fill's edge is the next step toward the
     * text. */
    --interactive: var(--pink-400);
    --interactive-edge: var(--pink-300);
    --interactive-tint: var(--pink-700);
    --interactive-tint-text: var(--pink-200);
    --interactive-tint-edge: var(--pink-600);
    /* And the set an INVERTED ground inside this mode reads: the other
     * ground's steps, because the ground decides the reaction, not the mode. */
    --_interactive-inverted: var(--blue-600);
    --_interactive-inverted-edge: var(--blue-700);
    --_interactive-inverted-tint: var(--blue-100);
    --_interactive-inverted-tint-text: var(--blue-600);
    --_interactive-inverted-tint-edge: var(--blue-200);
    /* THE EMBLEM'S GRADIENT STOPS. DESIGN.md 3.1, read out of
     * preview_prism_11.html. The tops are constant in both modes; only the
     * bottoms change, and in dark they go DARKER so the mark keeps its
     * weight against ink instead of glowing off it. **The almond, the
     * overlap, goes to a saturated blue, not toward black**; */
    --emblem-mint-top: #6BFCB8;    --emblem-mint-bottom: #0E8A9E;
    --emblem-pink-top: #FF1DA5;    --emblem-pink-bottom: #7A1052;
    --emblem-almond-top: #6D1A76;  --emblem-almond-bottom: #2450D0;
  }
  prism-page[mode="light"],
  .mode-light {
    --surface-default: var(--_paper);
    --_secondary-fill: #FFFFFF;
    --surface-inverted: var(--_ink);
    --text-default: var(--_paper-text);
    --text-inverted: var(--_ink-text);
    --text-dimmed: var(--_paper-text-75);
    --border-default: var(--_mist);
    --border-inverted: var(--_shadow);
    --text-dimmed-inverted: var(--_ink-text-75);
    /* On paper a role reads: ink 600, tint 100, text on tint 600. The
     * interactive hue is blue. Blue 600 on paper 5.67:1. */
    --interactive: var(--blue-600);
    --interactive-edge: var(--blue-700);
    --interactive-tint: var(--blue-100);
    --interactive-tint-text: var(--blue-600);
    --interactive-tint-edge: var(--blue-200);
    --_interactive-inverted: var(--pink-400);
    --_interactive-inverted-edge: var(--pink-300);
    --_interactive-inverted-tint: var(--pink-700);
    --_interactive-inverted-tint-text: var(--pink-200);
    --_interactive-inverted-tint-edge: var(--pink-600);
    --emblem-mint-top: #6BFCB8;    --emblem-mint-bottom: #15D2EF;
    --emblem-pink-top: #FF1DA5;    --emblem-pink-bottom: #FFD6EE;
    --emblem-almond-top: #6D1A76;  --emblem-almond-bottom: #10C3EE;
    --_halo: color-mix(in srgb, var(--_ink) 50%, transparent);
    --_halo-near: 1.5px;  --_halo-far: 6px;
    --_text-outline: drop-shadow(.5px 0 0 #FFFFFF) drop-shadow(-.5px 0 0 #FFFFFF)
      drop-shadow(0 .5px 0 #FFFFFF) drop-shadow(0 -.5px 0 #FFFFFF);
  }
  /* No mode stated: follow the reader. DESIGN.md 3.5 says the switch's
   * initial state follows prefers-color-scheme; this is that, one level up,
   * so a tenant that never sets a mode is still correct. */
  @media (prefers-color-scheme: light) {
    :root:not(:has(prism-page[mode="dark"])) {
      --surface-default: var(--_paper);
      --_secondary-fill: #FFFFFF;
      --surface-inverted: var(--_ink);
      --text-default: var(--_paper-text);
      --text-inverted: var(--_ink-text);
      --text-dimmed: var(--_paper-text-75);
      --border-default: var(--_mist);
      --border-inverted: var(--_shadow);
      --text-dimmed-inverted: var(--_ink-text-75);
      --interactive: var(--blue-600);
      --interactive-edge: var(--blue-700);
      --interactive-tint: var(--blue-100);
      --interactive-tint-text: var(--blue-600);
      --interactive-tint-edge: var(--blue-200);
      --_interactive-inverted: var(--pink-400);
      --_interactive-inverted-edge: var(--pink-300);
      --_interactive-inverted-tint: var(--pink-700);
      --_interactive-inverted-tint-text: var(--pink-200);
      --_interactive-inverted-tint-edge: var(--pink-600);
      --emblem-mint-top: #6BFCB8;    --emblem-mint-bottom: #15D2EF;
      --emblem-pink-top: #FF1DA5;    --emblem-pink-bottom: #FFD6EE;
      --emblem-almond-top: #6D1A76;  --emblem-almond-bottom: #10C3EE;
    --_halo: color-mix(in srgb, var(--_ink) 50%, transparent);
    --_halo-near: 1.5px;  --_halo-far: 6px;
    --_text-outline: drop-shadow(.5px 0 0 #FFFFFF) drop-shadow(-.5px 0 0 #FFFFFF)
      drop-shadow(0 .5px 0 #FFFFFF) drop-shadow(0 -.5px 0 #FFFFFF);
    }
  }
}

/* --- F3 · the type scale, DESIGN.md 2.5 --------------------------------
 * The sizes are the owner's. THE BREAKPOINT STEPS ARE A DESIGN CHOICE and
 * are flagged: 2.5 gives ONE value per step, and `type-xl` at a flat 96px
 * overflows a phone. Nothing is calculated (the owner's rule), so the four
 * display steps get fixed values per breakpoint. The xl column is DESIGN.md's
 * own number, unchanged. Steps below h2 do not scale -- 18px body is 18px
 * everywhere, which is what a reading size is for.
 */
@layer prism.tokens {
  :root {
    --type-xl: 48px;  --type-l: 40px;  --type-h1: 32px;  --type-h2: 26px;
    --type-h3: 24px;
    --type-body-l: 24px;
    --type-body: 18px;
    --type-caption: 18px;
    /* The larger caption: the date line's, and a large button's
     * label. */
    --type-caption-l: 24px;
    /* **CONTROL TEXT: THE CAPTION, EXCEPT ON TOUCH, WHERE IT IS 16px.** iOS
     * Safari zooms the page when a control under 16px takes focus, and the
     * zoom moves the field while its own picker is opening — *the owner: on
     * an iPhone the date picker closes before a date can be picked.* 16px is
     * the threshold, not a design step; with a mouse, controls are the
     * caption. Read by prism-input and prism-picker only. */
    --type-control: var(--type-caption);
    /* **ONE HEIGHT FOR EVERY CONTROL IN A ROW.** A field's own sum — its text
     * at line-height 1.5, 12px padding twice, a 1px border twice — named, so a
     * button beside it is set to the same height: 53px with 18px text. */
    --control-block: calc(var(--type-control) * 1.5 + 26px);
    /* ANYBODY TRACKING. **0.02em, and it applies to EVERY Anybody setting,
     * not only to controls.**
     *
     * This REPLACES the per-step letter-spacing in DESIGN.md 2.5's table,
     * which runs -0.035em at xl, -0.03em at l, -0.02em at h1, -0.015em at
     * h2 and -0.005em at caption. Those are the conventional direction for
     * display type -- tighter as it gets bigger -- and this is the opposite
     * of them at every step.
     *
     * Chosen after five values were measured side by side at 14px. **The
     * consequence at the display sizes is much larger and was not in that
     * test:** at 96px, 0.02em is 1.9px per glyph against DESIGN.md's
     * -3.4px, a swing of 5.3px per letter. The proof page shows the four
     * display steps both ways so it can be judged where it is biggest. */
    --anybody-tracking: 0.02em;
    --type-meta: 15px;
    --type-micro: 13px;
  }
  @media (pointer: coarse) { :root { --type-control: max(16px, var(--type-caption)) } }
  @media (min-width: 600px)  { :root { --type-xl: 64px; --type-l: 52px; --type-h1: 38px; --type-h2: 28px } }
  /* No --type-h1 or --type-h2 at 900: — h1 holds 38px, h2 holds 28px (the
   * wordmark rides h2 through prism-brand, so this is also what keeps the brand
   * one size across the nav's shape switch), and both take their next step at
   * 1280. Only the two display steps still move here. */
  @media (min-width: 900px)  { :root { --type-xl: 80px; --type-l: 64px; --type-body-l: 26px; --type-caption-l: 26px } }
  @media (min-width: 1280px) { :root { --type-xl: 96px; --type-l: 72px; --type-h1: 48px; --type-h2: 32px } }
}

/* ===========================================================================
 * A1-A5 · THE ATOMS.  DESIGN.md 2.3, 2.5, 2.6, 2.8, 2.9, 3.5.
 * ======================================================================== */

@layer prism.atoms {

  /* --- A1 · prism-surface ------------------------------------------------ Two
   * grounds: the mode's own and its opposite. a brand step is not a ground, and
   * nothing from its own ramp reads as text on pink. How sparingly the inverted
   * ground is used stays the composition's business.
   */
  /* **A SURFACE REMAPS THE VARIABLES FOR EVERYTHING INSIDE IT, not just
   * its own two colours.**
   *
   * The bug was that `prism-text` set `color: var(--text-default)` and
   * `--text-default` still meant the MODE's text -- near-white ink-text
   * sitting on a paper-coloured inverted surface. Same for a primary button,
   * whose fill is `--text-default`. Changing the surface's own `color` could
   * never fix that, because every atom inside was naming the variable
   * directly and winning.
   *
   * So the surface redefines what `default` MEANS for its subtree. Every atom
   * keeps naming the semantic variable and is correct on any ground, which is
   * what a semantic variable was supposed to buy. */
  prism-surface {
    display: block;
    background: var(--surface-default);
    color: var(--text-default);
  }
  prism-surface[tone="inverted"],
  :is(prism-card, prism-image-card)[ground="inverted"],
  prism-hero:not([src]):not(:has(> prism-image)) {
    --_secondary-fill: transparent;
    --surface-default: var(--surface-inverted);
    --text-default: var(--text-inverted);
    --text-dimmed: var(--text-dimmed-inverted);
    --border-default: var(--border-inverted);
    --interactive: var(--_interactive-inverted);
    --interactive-edge: var(--_interactive-inverted-edge);
    --interactive-tint: var(--_interactive-inverted-tint);
    --interactive-tint-text: var(--_interactive-inverted-tint-text);
    --interactive-tint-edge: var(--_interactive-inverted-tint-edge);
  }
  prism-surface[tone="inverted"] {
    background: var(--surface-default);
    color: var(--text-default);
  }
  /* **A CARD MAY STAND ON THE INVERTED GROUND**, The card's own rules mix
   * its ground and its hairline from the variables above, so remapping them
   * is the whole of it: the card follows, and so does everything in it. */
  /* The page shell carries the mode, so it carries the ground colour too. */
  prism-page {
    background: var(--surface-default);
    color: var(--text-default);
  }

  /* --- A2 · prism-text, DESIGN.md 2.5 and 2.6 --------------------------
   * The type scale as one element. `level` picks a step; `tone` picks one
   * of 2.6's three contrast levels.
   *
   * NO text-transform. DESIGN.md 2.5: Anybody
   * is strong enough to carry a statement in mixed case, and capitals hide
   * the face.
   */
  prism-text {
    display: block;
    font: var(--_weight, 400) var(--_size, var(--type-body)) / var(--_leading, 1.65)
          var(--_family, var(--font-body));
    letter-spacing: var(--_tracking, normal);
    /* **INHERIT, never re-assert.** Naming --text-default here would
     * override the ground the atom sits on. The ground sets the colour;
     * text carries it. */
    color: inherit;
    margin: 0;
    /* **A word longer than its box breaks rather than escapes.** A filename, a
     * URL or an id is exactly the kind of unbroken string a tenant renders
     * without thinking, and it would otherwise widen a grid track and push the
     * row out. */
    overflow-wrap: anywhere;
  }
  /* **THE HEADINGS TAKE NEWSREADER'S OWN SPACING, 0.** +0.02em was Anybody's;
   * the captions and buttons keep it. */
  /* xl and l tracked 0.05em. */
  prism-text[level="xl"]      { --_family: var(--font-display); --_weight: 700; --_size: var(--type-xl);      --_leading: 1.02; --_tracking: 0.05em }
  prism-text[level="l"]       { --_family: var(--font-display); --_weight: 700; --_size: var(--type-l);       --_leading: 1.06; --_tracking: 0.05em }
  /* h1 to h3 at 500. */
  prism-text[level="h1"]      { --_family: var(--font-display); --_weight: 500; --_size: var(--type-h1);      --_leading: 1.12; --_tracking: 0 }
  prism-text[level="h2"]      { --_family: var(--font-display); --_weight: 500; --_size: var(--type-h2);      --_leading: 1.2;  --_tracking: 0 }
  prism-text[level="h3"]      { --_family: var(--font-body);    --_weight: 500; --_size: var(--type-h3);      --_leading: 1.4;  --_tracking: -0.01em }
  prism-text[level="body-l"]  { --_family: var(--font-body);    --_weight: 400; --_size: var(--type-body-l);  --_leading: 1.5 }
  prism-text[level="body"]    { --_family: var(--font-body);    --_weight: 400; --_size: var(--type-body);    --_leading: 1.65 }
  /* caption and caption-l at 700. */
  prism-text[level="caption"] { --_family: var(--font-display); --_weight: 700; --_size: var(--type-caption); --_leading: 1.5;  --_tracking: var(--anybody-tracking) }
  prism-text[level="caption-l"] { --_family: var(--font-display); --_weight: 700; --_size: var(--type-caption-l); --_leading: 1.5; --_tracking: var(--anybody-tracking) }
  prism-text[level="meta"]    { --_family: var(--font-meta);    --_weight: 400; --_size: var(--type-meta);    --_leading: 1.4;  --_tracking: 0.015em }
  prism-text[level="micro"]   { --_family: var(--font-meta);    --_weight: 400; --_size: var(--type-micro);   --_leading: 1.4;  --_tracking: 0.015em; font-style: italic }
  prism-text[italic]          { font-style: italic }
  /* Every digit one width, so a value that changes in place does not move. */
  prism-text[tabular]         { font-variant-numeric: tabular-nums }

  /* 2.6's three levels. `dimmed` is a PRE-MIXED colour and not an opacity:
   * the mix holds its contrast on inverted and mixed grounds, where
   * opacity on an inheriting element does not. */
  prism-text[tone="dimmed"] { color: var(--text-dimmed) }
  prism-text[tone="faint"]  { color: inherit; opacity: .5 }

  /* --- A3 · prism-button ----------------------------------------------- **A
   * BUTTON IS A BOX OF A STATED HEIGHT, AND ITS CONTENT IS CENTRED IN IT.**
   * The height is the control's, --control-block, so a button stands exactly
   * as tall as the field beside it -- and an icon-only button stands exactly
   * as tall as the text button beside it, because neither is measured from
   * its content. Flex, not inline-block: a flex box has no strut, so nothing
   * hangs under it.
   *
   * The caption's text, the control voice. Variants with a box of their own
   * -- link, item, tile, float -- set it. */
  prism-button:not([variant="link"], [variant="item"], [variant="tile"]) { display: inline-flex }
  prism-button { display: inline-block }
  prism-button > button {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font: 500 var(--type-caption) / 1.5 var(--font-control);
    letter-spacing: var(--anybody-tracking);
    block-size: var(--control-block);
    padding: 0 22px;
    /* A label is one line. A row of buttons wraps BETWEEN buttons, in its
     * cluster; a label that broke inside its box was the defect above. */
    white-space: nowrap;
    /* Room between an icon and its label: the tenant writes the two side by
     * side and the system spaces them. *The owner's page: the icon sat tight
     * against "Download original".* */
    gap: var(--space-2);
    border: 1px solid transparent;
    /* **7px.** Primary, secondary and the menu's item and tile take it;
     * link keeps its own 0 because it is an underline, and float keeps its
     * circle and pill. */
    border-radius: 7px;
    cursor: pointer;
    background: var(--text-default);
    color: var(--surface-default);
    transition: background-color .2s ease, color .2s ease, border-color .2s ease, opacity .2s ease;
  }
  /* **WHITE ON PAPER.** Paper is #F4F2EC, so transparent read as a grey fill.
   * **AND LIFTED ON INK.** 5% of the text into the ground: white sits 4-7.5%
   * per channel above paper, and this is that step taken the other way up —
   * above the card's 3%, below the field's 6% and clear of disabled's 10%. The
   * inverted ground keeps transparent. */
  prism-button[variant="secondary"] > button {
    background: var(--_secondary-fill, transparent);
    border-color: color-mix(in srgb, var(--_secondary-fill, transparent), var(--text-default) var(--edge-mix));
    color: var(--text-default);
  }
  prism-button[variant="link"] > button {
    display: inline-block;
    block-size: auto;
    background: none;
    color: inherit;
    border: none;
    border-bottom: 1px solid currentColor;
    border-radius: 0;
    padding: 0 0 2px;
  }
  /* **SQUARE WHEN THE BUTTON HOLDS AN ICON AND NOTHING ELSE**, at the same
   * height: 12px each side of a 20px icon is a lozenge, and a row of lozenges
   * reads as tabs. The script marks it, because a stylesheet cannot see text:
   * `:has(> prism-icon:only-child)` matched a button holding an icon AND a
   * label, which is one element child and some text, and squeezed "Download
   * original" into a square. */
  prism-button:is(:not([variant]), [variant="primary"], [variant="secondary"]) > button[data-prism-icon-only] {
    padding: 0;
    inline-size: var(--control-block);
  }
  /* The reference marks the arrow up by hand. Here it is an attribute, so
   * the agent asks for it rather than writing a glyph into the label. */
  prism-button[arrow] > button::after {
    content: "\2192";
    display: inline-block;
    margin-left: 6px;
    transition: transform .2s ease;
  }
  prism-button[arrow] > button:hover::after { transform: translateX(4px) }

  /* --- F5 · the one interactive rule ---------------------------------------
   * Everything that reacts, reacts in --interactive. One place. */
  prism-button > button:hover {
    background: var(--interactive);
    /* The words on an interactive fill are the mode's surface: paper on blue
     * 600, ink on pink 400 -- the pair that reads in each mode. */
    color: var(--surface-default);
  }
  prism-button:is([variant="secondary"], [variant="link"]) > button:hover {
    background: transparent;
    color: var(--interactive);
    border-color: var(--interactive);
  }
  /* The white stays under the hover: only the edge and the text react. */
  prism-button[variant="secondary"] > button:hover { background: var(--_secondary-fill, transparent) }
  /* **ground="surface": AN OPAQUE SQUARE OF THE MODE'S OWN SURFACE**, held
   * under the hover, for a button standing where nothing else is opaque — a
   * transparent bar over content. Colour and edge still react. */
  prism-button[ground="surface"] > button,
  prism-button[ground="surface"] > button:hover { background: var(--surface-default) }
  /* **A SECOND SIZE: THE TOOLBAR.** The same button at the same weight and
   * colour in less room -- 38px, the height a 20px icon with 8px around it
   * gives, which is the nav bar's floor in both of its shapes. A row of
   * full-height buttons under a 480px video is a control bar taller than a
   * paragraph. Text and icon-only stand at the one height; icon-only is
   * square. */
  prism-button[size="s"] > button { block-size: 38px; padding: 0 12px }
  prism-button[size="s"] > button[data-prism-icon-only] { padding: 0; inline-size: 38px }

  /* --- variant="float" · THE FLOATING CONTROL ------------------------------
   * Its ground is the secondary button's. The circle joins the radio, the
   * chip and the thumb as the fourth place radius is not 0, and like them the
   * shape is the meaning: a round control floats over content. 44px: a 16px
   * icon in the smallest circle a thumb can hit reliably. */
  prism-button[variant="float"] > button {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    inline-size: 44px;
    block-size: 44px;
    padding: 0;
    /* **THE BUTTONS' GROUND**, ruled by the owner: the secondary button's
     * fill — white in light, near black in dark — the words and icon the
     * text colour, the edge that ground pushed toward the text, as every edge
     * is. */
    border: 1px solid color-mix(in srgb, var(--_secondary-fill, var(--surface-default)), var(--text-default) var(--edge-mix));
    border-radius: 50%;
    background: var(--_secondary-fill, var(--surface-default));
    color: var(--text-default);
  }
  prism-button[variant="float"] prism-icon { --_icon-size: 16px }
  /* Restated: the variant's own background rule is more specific than the
   * shared hover, so without this the circle would not react at all. */
  /* The hover is the hamburger's: the ground stays, the edge and the words
   * turn --interactive. */
  prism-button[variant="float"] > button:hover {
    background: var(--_secondary-fill, var(--surface-default));
    border-color: var(--interactive);
    color: var(--interactive);
  }
  prism-button[variant="float"] > button:focus-visible { border-radius: 50% }
  /* **WITH WORDS IT IS A PILL**, with or without an icon beside them — a
   * chevron before "Tillbaka", ruled by the owner. The same 44px height and
   * fill; the width is the label's. The circle is for an icon alone. A
   * leading icon sits 12px from the edge, so the words look centred. */
  prism-button[variant="float"] > button:not([data-prism-icon-only]),
  prism-button[variant="float"] > button:not([data-prism-icon-only]):focus-visible {
    inline-size: auto;
    min-inline-size: 44px;
    padding-inline: var(--space-4);
    border-radius: 22px;
  }
  prism-button[variant="float"] > button:not([data-prism-icon-only]):has(> prism-icon:first-child) {
    padding-inline-start: var(--space-3);
  }

  /* --- variant="item" and variant="tile" · THE ACTIONS OF A MENU ----------
   * An `item` is a row: the icon leading, the label after it, the full width
   * of the menu. A `tile` is the icon over its label, and a run of them
   * shares one row. Neither has a ground or an edge — the menu's plate is
   * the ground — so they react the way secondary does: the text goes
   * --interactive. */
  prism-button:is([variant="item"], [variant="tile"]) { display: block }
  prism-button:is([variant="item"], [variant="tile"]) > button,
  prism-button:is([variant="item"], [variant="tile"]) > button:hover {
    background: transparent;
    border: none;
  }
  prism-button:is([variant="item"], [variant="tile"]) > button { color: var(--text-default) }
  prism-button:is([variant="item"], [variant="tile"]) > button:hover { color: var(--interactive) }
  prism-button[variant="item"] > button {
    display: flex;
    align-items: center;
    gap: var(--space-4);
    inline-size: 100%;
    block-size: auto;
    padding: var(--space-3) var(--space-5);
    text-align: start;
  }
  prism-button[variant="item"] prism-icon { --_icon-size: 20px }
  prism-button[variant="tile"] > button {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-2);
    inline-size: 100%;
    padding: var(--space-4) var(--space-2);
    text-align: center;
    /* **A LABEL LONGER THAN ITS THIRD BREAKS INSIDE IT.** A tile is a third of
     * a 240px menu, and one word can be wider than that. It is hyphenated where
     * the label's language allows — `lang` on the menu or the page — and broken
     * anywhere where it does not, so it never overlaps. */
    hyphens: auto;
    overflow-wrap: anywhere;
  }
  prism-button[variant="tile"] { min-inline-size: 0 }
  prism-button[variant="tile"] prism-icon { --_icon-size: 24px }

  /* --- variant="bare" · AN ICON ON A PLATE THAT IS ITS GROUND
   * ----------- A 44px square hit area, no ground and no edge; the icon
   * in the text colour, --interactive on the hover, as the hamburger's
   * words turn. */
  prism-button[variant="bare"] > button,
  prism-button[variant="bare"] > button:hover {
    background: transparent;
    border: none;
  }
  prism-button[variant="bare"] > button {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    inline-size: 44px;
    block-size: 44px;
    padding: 0;
    color: var(--text-default);
  }
  prism-button[variant="bare"] > button:hover { color: var(--interactive) }
  prism-button[variant="bare"] prism-icon { --_icon-size: 20px }
  /* **ON A PICTURE THE BARE ICON IS LIGHT, WITH AN INK HALO**, in either
   * mode: the photograph is the ground, not the page. No plate, so it hides
   * almost nothing of what it stands on; the icon grows to 24px to carry
   * alone. The hover keeps it light — the picture decides nothing. */
  prism-button[variant="bare"][ground="picture"] > button,
  prism-button[variant="bare"][ground="picture"] > button:hover {
    color: var(--_ink-text);
    filter: drop-shadow(0 0 2px color-mix(in srgb, var(--_ink) 60%, transparent))
            drop-shadow(0 0 8px color-mix(in srgb, var(--_ink) 35%, transparent));
  }
  prism-button[variant="bare"][ground="picture"] prism-icon { --_icon-size: 24px }

  /* --- tone="danger" · THE DESTRUCTIVE ACTION -------------------------------
   * The critical intent's colour, the one the system already uses for what
   * is wrong: not a colour of its own. It is carried as the button's
   * --interactive, so every variant's reaction follows it with no rule of
   * its own — a float's hover edge and words go to it — and the
   * resting colour is set only where a variant has one. */
  prism-button[tone="danger"] { --interactive: var(--status-critical) }
  prism-button[tone="danger"]:is(:not([variant]), [variant="primary"]) > button {
    background: var(--status-critical);
    color: var(--status-critical-ink);
  }
  prism-button[tone="danger"]:is([variant="secondary"], [variant="link"], [variant="item"], [variant="tile"]) > button {
    color: var(--status-critical);
  }
  prism-button[tone="danger"][variant="secondary"] > button { border-color: var(--status-critical) }

  prism-button > button:disabled { opacity: .4; cursor: not-allowed }
  prism-button > button:disabled:hover {
    background: var(--text-default); color: var(--surface-default);
  }
  /* **DISABLED IS GREY.** 10% of the text into the ground, the text at 45%,
   * both solid and mixed on the control so they hold on every surface; the edge
   * is that grey, pushed. Opacity faded the edge with everything else, so it is
   * not used. Link, item and tile are text and keep the fade. After the danger
   * tone, so it wins there too. */
  prism-button:not([variant="link"], [variant="item"], [variant="tile"]) > button:disabled,
  prism-button:not([variant="link"], [variant="item"], [variant="tile"]) > button:disabled:hover {
    opacity: 1;
    background: color-mix(in srgb, var(--text-default) 10%, var(--surface-default));
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    color: color-mix(in srgb, var(--text-default) 45%, var(--surface-default));
    border-color: color-mix(in srgb, color-mix(in srgb, var(--text-default) 10%, var(--surface-default)), var(--text-default) var(--edge-mix));
  }

  /* --- prism-chip ------------------------------------------------------
   * **A FILTER, NOT AN ACTION.** Owns one `<button aria-pressed>` — R21 —
   * because a filter is STATE, and a button is the only control the platform
   * has that carries a pressed state natively. A row of them is a
   * `prism-cluster`.
   *
   * *`prism-cluster` documented "filter chips" while the system had none;
   * `prism-tag` is a label and has no state.*
   *
   * **A PILL, WHERE EVERYTHING ELSE IS RADIUS 0.** It is the second place in
   * this system where a shape carries the distinction rather than a colour —
   * the radio is round for the same reason, and a square radio reads as a
   * checkbox. **A filter and an action are different things and now look it
   * before either is pressed.**
   *
   * On is the interactive TINT -- the ground, the words and the edge from
   * one ramp -- off is an outline, so the pressed state reads without a tick
   * or a colour anybody has to learn.
   */
  prism-chip { display: inline-block }
  prism-chip > button {
    display: inline-flex;
    align-items: center;
    font: 500 var(--type-caption) / 1.5 var(--font-control);
    letter-spacing: var(--anybody-tracking);
    padding: 6px 16px;
    border: 1px solid color-mix(in srgb, transparent, var(--text-default) var(--edge-mix));
    /* A pill: large enough that the radius is always half the height,
     * whatever the type size does at a breakpoint. */
    border-radius: 999px;
    background: transparent;
    color: var(--text-default);
    cursor: pointer;
    transition: background-color .2s ease, color .2s ease, border-color .2s ease;
  }
  prism-chip > button:hover { border-color: var(--interactive); color: var(--interactive) }
  prism-chip > button[aria-pressed="true"] {
    background: var(--interactive-tint);
    border-color: var(--interactive-tint-edge);
    color: var(--interactive-tint-text);
  }
  prism-chip > button[aria-pressed="true"]:hover { opacity: .85; color: var(--interactive-tint-text) }
  prism-chip > button:disabled,
  prism-chip > button:disabled:hover {
    opacity: 1; cursor: not-allowed;
    background: color-mix(in srgb, var(--text-default) 10%, var(--surface-default));
    color: color-mix(in srgb, var(--text-default) 45%, var(--surface-default));
    border-color: color-mix(in srgb, color-mix(in srgb, var(--text-default) 10%, var(--surface-default)), var(--text-default) var(--edge-mix));
  }
  /* The ring follows the shape. A square outline on a pill is two shapes. */
  prism-chip > button:focus-visible {
    outline: 2px solid var(--interactive);
    outline-offset: 2px;
    border-radius: 999px;
  }

  /* --- A4 · prism-link, DESIGN.md 3.5 ---------------------------------- */
  prism-link > a {
    color: inherit;
    text-decoration: none;
    border-bottom: 1px solid currentColor;
    transition: color .12s, border-color .12s;
  }
  prism-link > a:hover { color: var(--interactive); border-color: var(--interactive) }
  /* **cover: THE PRESS AREA COVERS WHAT HOLDS IT** — the nearest positioned
   * ancestor, a card. Pressing anywhere on it opens the link; the pointer
   * anywhere on it is on the link, so the words turn --interactive. The whole
   * holder is the target, so there is no underline, and the focus ring is
   * drawn around the holder. */
  prism-link[cover] > a {
    border-bottom: none;
    transition: color .2s ease;
  }
  prism-link[cover] > a::after {
    content: "";
    position: absolute;
    inset: 0;
  }

  /* --- prism-nav-item ----------------------------------------------------
   * **ONE ITEM OF THE NAV, IN BOTH OF ITS SHAPES.** It owns one <a>. Below
   * 900px it is a row of the dropdown, aligned to the end: room to press, and a
   * leading chevron — present on every row and VISIBLE on the current one,
   * so the labels line up; it leads because a trailing chevron promises a
   * second level. From 900px it is an inline link with an underline, and
   * "you are here" is the item WITHOUT one. The current item is
   * --interactive in both, and says so to a reader (aria-current).
   *
   * **IT SPEAKS THE CONTROL VOICE — ANYBODY 500 AT CAPTION SIZE — AND SAYS
   * SO**, the voice of every button in the bar, so the header speaks one voice
   * and a tenant's body font cannot dress it. */
  prism-nav-item > a {
    display: flex;
    align-items: center;
    /* Aligned to the end, under the hamburger that opened it. The chevron
     * still leads its label, and the labels' ends line up. */
    justify-content: flex-end;
    gap: var(--space-2);
    padding: var(--space-3) var(--space-4);
    font: 500 var(--type-caption) / 1.65 var(--font-display);
    letter-spacing: var(--anybody-tracking);
    color: inherit;
    text-decoration: none;
    border-bottom: none;
    transition: color .12s, border-color .12s;
  }
  prism-nav-item > a:hover { color: var(--interactive); border-color: var(--interactive) }
  prism-nav-item > a > prism-icon { visibility: hidden }
  prism-nav-item[current] > a > prism-icon { visibility: visible }
  prism-nav-item[current] > a { color: var(--interactive) }
  prism-nav-item > a:focus-visible {
    outline: 2px solid var(--interactive);
    outline-offset: 2px;
  }
  @media (min-width: 900px) {
    prism-nav-item > a {
      display: inline;
      padding: 0;
      border-bottom: 1px solid currentColor;
    }
    prism-nav-item[current] > a { border-bottom: none }
    prism-nav-item > a > prism-icon { display: none }
  }

  /* --- A5 · prism-label, A6 · prism-input -----------------------------
   * **THE TWO ATOMS A FIELD IS MADE OF.** They exist because `prism-field`
   * was an ASSEMBLY filed as an atom: a label and a control are two things,
   * and a labelled field is the textbook molecule. R19 said what had to
   * happen — *when an assembly needs a part that is not there, that is an
   * atom request* — and this is that request answered.
   *
   * **AN ATOM OWNS AT MOST ONE PLATFORM ELEMENT.** That is the line that makes
   * the taxonomy checkable rather than a matter of taste: `prism-button` owns
   * a `<button>`, `prism-image` an `<img>`, `prism-label` a `<label>`,
   * `prism-input` an `<input>`. Two or more, and it is an assembly.
   */
  prism-label { display: block }
  prism-label > label {
    display: block;
    font: italic 400 var(--type-meta) / 1.4 var(--font-meta);
    letter-spacing: .015em;
    color: inherit;
    opacity: .5;
  }
  /* **level="caption": THE CONTROL VOICE.** A label beside a checkbox or a
   * radio speaks the way the control does — Anybody 500 at caption size, at
   * full strength — and pressing it toggles the control. Next to a disabled
   * control it dims and says so. */
  prism-label[level="caption"] > label {
    font: 500 var(--type-caption) / 1.5 var(--font-display);
    letter-spacing: var(--anybody-tracking);
    font-style: normal;
    opacity: 1;
    color: var(--text-default);
    cursor: pointer;
  }
  :is(prism-check, prism-switch):has(input:disabled) ~ prism-label[level="caption"] > label {
    color: color-mix(in srgb, var(--text-default) 45%, var(--surface-default));
    cursor: not-allowed;
  }

  /* --- prism-fact ------------------------------------------------------
   * **ONE FACT PER LINE.** The label in the metadata voice prism-label and
   * prism-tag already speak — Instrument Serif italic at 50% — and the value
   * in body text after it, on the baseline. A value too long for the line
   * wraps under itself rather than escaping. */
  prism-fact {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    column-gap: var(--space-3);
    font: 400 var(--type-body) / 1.5 var(--font-body);
    color: inherit;
    overflow-wrap: anywhere;
  }
  .prism-fact-label {
    font: italic 400 var(--type-meta) / 1.4 var(--font-meta);
    letter-spacing: .015em;
    opacity: .5;
  }

  /* --- the control ground -----------------------------------------------
   * **A CONTROL CARRIES ITS OWN GROUND, ONE STEP OFF THE PAGE'S.** Mixed, not
   * named, the way the slider's track tones are: 6% of the text colour into
   * the surface is lighter on ink and darker on paper by the same rule,
   * because the text colour already sits on the opposite side of the ground
   * in both modes.
   *
   * DECLARED ON THE CONTROL, NOT IN :root. A custom property resolves its
   * mix where it is declared, so a :root value would freeze the page's pair
   * and be wrong inside an inverted or chromatic surface. Here it reads
   * whatever `default` means on the ground the control actually sits on. */
  prism-input, prism-picker, prism-check, prism-switch {
    --surface-control: color-mix(in srgb, var(--text-default) 6%, var(--surface-default));
  }

  prism-input { display: block }
  prism-input :is(input, textarea) {
    width: 100%;
    font: 500 var(--type-control) / 1.5 var(--font-display);
    letter-spacing: var(--anybody-tracking);
    color: var(--text-default);
    background: var(--surface-control);
    border: 1px solid color-mix(in srgb, var(--surface-control), var(--text-default) var(--edge-mix));
    border-radius: 0;
    padding: 12px 14px;
    transition: border-color .2s ease;
  }
  prism-input :is(input, textarea):hover { border-color: var(--text-dimmed) }
  /* **ON FOCUS THE BORDER HOLDS; THE RING IS THE ONE INTERACTIVE LINE.** The
   * border stays, so a keyboard focus draws one line, the focus-visible ring,
   * not two a ring's width apart. `outline: none` stays: it kills
   * the UA default, and the focus-visible block below re-adds the system's
   * ring on keyboard focus. */
  prism-input :is(input, textarea):focus { outline: none }
  prism-input :is(input, textarea)::placeholder { color: inherit; opacity: .4 }
  prism-input textarea { resize: vertical; min-height: 6em }
  prism-input :is(input, textarea):disabled {
    cursor: not-allowed;
    background: color-mix(in srgb, var(--text-default) 10%, var(--surface-default));
    color: color-mix(in srgb, var(--text-default) 45%, var(--surface-default));
    border-color: color-mix(in srgb, color-mix(in srgb, var(--text-default) 10%, var(--surface-default)), var(--text-default) var(--edge-mix));
  }

  /* --- the date field's icon ------------------------------------------
   * **THE BROWSER DRAWS ITS OWN AND IT IS THE WRONG ONE.**
   * `::-webkit-calendar-picker-indicator` is a fixed dark glyph: it does not
   * follow the mode, so on ink it is nearly invisible, and it is not the icon
   * this system uses anywhere else.
   *
   * **A MASK, NOT AN ELEMENT.** The icon is Lucide's `calendar` — the same
   * path the sprite serves — painted with `background-color: currentColor`
   * through a mask. So it inherits the text colour like everything else and
   * `prism-input` still owns exactly one platform element (R21). *`prism-icon`
   * here would be a second element and this is an atom; `prism-picker` draws
   * its chevron for the same reason.*
   *
   * The native indicator is kept and made transparent rather than removed:
   * it is what opens the picker, and it is stretched over the icon so the
   * click target is the icon a person aims at.
   */
  prism-input:has(input[type="date"]) { position: relative }
  prism-input input[type="date"] { padding-inline-end: 42px; min-inline-size: 0 }
  /* **ON TOUCH, THE DATE CONTROL DROPS ITS NATIVE APPEARANCE, OR IT DOES NOT
   * FIT.** iOS Safari draws it as a native control with an intrinsic minimum
   * width that `width: 100%` does not override, so it stuck out past its
   * panel while the fields beside it fitted. *A tenant, from the
   * owner's iPhone.* With the appearance gone iOS centres the value and
   * collapses an EMPTY field to no text height, so both are put back on the
   * value part. The picker still opens natively on a tap. Touch only: with a
   * mouse the control is untouched. */
  @media (pointer: coarse) {
    prism-input input[type="date"] { -webkit-appearance: none; appearance: none }
    prism-input input[type="date"]::-webkit-date-and-time-value {
      text-align: start;
      min-block-size: 1.5em;
    }
  }
  prism-input input[type="date"]::-webkit-calendar-picker-indicator {
    position: absolute;
    inset-block: 0;
    inset-inline-end: 0;
    inline-size: 42px;
    block-size: 100%;
    opacity: 0;
    cursor: pointer;
  }
  prism-input:has(input[type="date"])::after {
    content: "";
    position: absolute;
    inset-inline-end: 14px;
    inset-block-start: 50%;
    transform: translateY(-50%);
    inline-size: 18px;
    block-size: 18px;
    background-color: currentColor;
    mask: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M8%202v3%22%20%2F%3E%20%3Cpath%20d%3D%22M16%202v3%22%20%2F%3E%20%3Crect%20x%3D%223%22%20y%3D%223%22%20width%3D%2218%22%20height%3D%2218%22%20rx%3D%222%22%20%2F%3E%20%3Cpath%20d%3D%22M3%209h18%22%20%2F%3E%3C%2Fsvg%3E") center / contain no-repeat;
    pointer-events: none;
  }
  prism-input:has(input[type="date"]:focus)::after { background-color: var(--interactive) }


  /* --- M3 · prism-field ------------------------------------------------
   * **A MOLECULE, AND IT ASSEMBLES THE TWO ABOVE.** It owns nothing: the gap
   * between label and control is a `prism-stack`'s.
   */
  prism-field { display: block }

  /* **Focus must never be invisible.** 2.8 changes the rule's colour on
   * focus, which is a colour-only signal; a keyboard user on a chromatic
   * ground, or anyone who cannot separate those hues, gets nothing. The
   * ring is added for keyboard focus only, so the designed look stands for
   * the mouse. DESIGN CHOICE, flagged -- 2.8 does not specify it. */
  /* `prism-picker select`, NOT `prism-select select`: the atom owns the
   * <select>, so this reaches a picker used bare as well as one inside the
   * prism-select molecule. It said prism-select and a bare picker had no ring
   * — invisible only while the border still changed on focus, which it no
   * longer does. */
  prism-input :is(input, textarea):focus-visible,
  prism-picker select:focus-visible,
  prism-button > button:focus-visible,
  prism-link > a:focus-visible {
    outline: 2px solid var(--interactive);
    outline-offset: 2px;
  }
  prism-link[cover] > a:focus-visible { outline: none }
  prism-link[cover] > a:focus-visible::after {
    outline: 2px solid var(--interactive);
    outline-offset: -2px;
  }
}

/* ===========================================================================
 * A6-A12 · THE REST OF THE ATOMS.
 *
 * A6-A8 are TRANSCRIBED: DESIGN.md and preview_prism_11.html carry real
 * values for them. A9-A12 are DERIVED -- neither file has a checkbox, a
 * radio, a select, an image or a spinner anywhere, so each one says which
 * established rule it is built from rather than pretending to be a
 * transcription. *The buttons were invented once and the owner caught it.*
 * ======================================================================== */

@layer prism.atoms {

  /* --- A6 · prism-divider, DESIGN.md 2.9 -------------------------------
   * Transcribed from `hr.divider`. 2.9: dividers go BETWEEN sections of the
   * same surface type. Between different surfaces the change of ground is
   * itself the break and a divider is noise. Not enforceable -- the element
   * cannot see what is above and below it -- so it is documented. */
  prism-divider {
    display: block;
    height: 1px;
    width: 100%;
    background: var(--border-default);
  }
  prism-divider[tone="inverted"] { background: var(--border-inverted) }

  /* --- A7 · prism-terminator, DESIGN.md 2.7 ----------------------------
   * The chromatic dot that ends a display line. Transcribed exactly:
   * 0.32em circle, baseline aligned, 0.08em to the left.
   *
   * **RENAMED, AND THIS IS A DELIBERATE BREAK WITH DESIGN.md's TABLE.**
   * 2.7 calls them `--magenta`, `--cyan` and `--amber`, and the section
   * itself says the names do not describe the colour they render: `--amber` renders MINT. R12 requires one canonical name
   * per thing, and an agent matching on strings will ask for amber and get
   * green. The modifiers here are the palette's own names.
   *   magenta -> pink     cyan -> blue     amber -> mint
   *
   * 2.7: never on H1 or smaller. Display levels only. prism.js says so. */
  prism-terminator {
    display: inline-block;
    width: .32em;
    height: .32em;
    border-radius: 50%;
    vertical-align: baseline;
    margin-left: .08em;
    background: var(--pink);
  }
  prism-terminator[tone="pink"] { background: var(--pink) }
  prism-terminator[tone="blue"] { background: var(--blue) }
  prism-terminator[tone="mint"] { background: var(--mint) }

  /* --- A8 · prism-tag, DESIGN.md 3.5 -----------------------------------
   * Transcribed from `.article-tag`: meta italic at 50%. **It is a LABEL,
   * not a chip** -- no border, no fill, no pill. That is what the reference
   * does and it is why metadata reads as metadata rather than as a control
   * somebody can press. */
  prism-tag {
    display: inline-flex;
    align-items: center;
    gap: .35em;
    /* The reference's .article-tag is 16px; --type-meta is 15px. One step,
     * not two: a second near-identical size is a scale nobody can hold. */
    font: italic 400 var(--_tag-size, var(--type-meta)) / 1.4 var(--font-meta);
    letter-spacing: .015em;
    color: inherit;
    opacity: .5;
  }
  /* **THREE SIZES, AND `m` IS WHAT A TAG ALREADY WAS.** Off the existing scale
   * rather than a set of its own: micro, meta, body. `s`, `m` and `l` because
   * that is how `prism-button`, `prism-icon` and `prism-brand` already spell it
   * — R25, an agent learns an attribute once. A tag with no size is unchanged. */
  prism-tag[size="s"] { --_tag-size: var(--type-micro) }
  prism-tag[size="m"] { --_tag-size: var(--type-meta) }
  prism-tag[size="l"] { --_tag-size: var(--type-body) }
  /* **ground: WHAT IT STANDS ON.** Without it a tag is metadata beside prose
   * at half strength. With one it is the only thing carrying its fact — over
   * a picture — so it is at full strength on a ground of its own.
   *   surface      an opaque square of the mode's own surface, the mark in
   *                --interactive: a hole punched in the picture, inside which
   *                the mode means what it means everywhere else.
   *   interactive  a disc of --interactive, the mark in ink: a state, round
   *                because the shape is the meaning, as the radio is. */
  prism-tag[ground="surface"] {
    display: grid;
    place-items: center;
    padding: var(--space-1);
    border-radius: 0;
    opacity: 1;
    color: var(--interactive);
    background: var(--surface-default);
  }
  prism-tag[ground="interactive"] {
    display: grid;
    place-items: center;
    padding: 0;
    border-radius: 50%;
    opacity: 1;
    color: var(--surface-default);
    background: var(--interactive);
  }
  prism-tag[ground="interactive"] prism-icon { --_icon-size: 14px }
  /* **WITH href IT IS A MARKER YOU CAN FOLLOW**: the chip's shape and size,
   * always filled in the chip's on colours, because it is not a filter that
   * could be off. A row may hold chips and these side by side. */
  prism-tag[href] {
    opacity: 1;
    font: 500 var(--_tag-size, var(--type-caption)) / 1.5 var(--font-control);
    letter-spacing: var(--anybody-tracking);
  }
  prism-tag[href] > a {
    display: inline-flex;
    align-items: center;
    gap: .35em;
    padding: 6px 16px;
    border: 1px solid var(--interactive-tint-edge);
    border-radius: 999px;
    background: var(--interactive-tint);
    color: var(--interactive-tint-text);
    text-decoration: none;
    transition: opacity .2s ease;
  }
  prism-tag[href] > a:hover { opacity: .85 }
  prism-tag[href] > a:focus-visible {
    outline: 2px solid var(--interactive);
    outline-offset: 2px;
  }

  /* --- A7 · prism-check -------------------------------------------------
   * **THE CONTROL ONLY.** Owns one `<input type=checkbox|radio>` and nothing
   * else — R21. The label that names it is a `prism-label`, and the two
   * together are `prism-choice`, which is a molecule.
   *
   * DERIVED from prism-input's box: 1px
   * --border-default edge, radius 0, --interactive on focus and when checked.
   * The one departure is the radio, which is ROUND: a square radio reads as a
   * checkbox and the shape IS the affordance.
   */
  prism-check { display: inline-flex; flex: none }
  prism-check input {
    appearance: none;
    margin: 0;
    /* A dimension, not a type size: sized to sit on the caption's line. */
    inline-size: 18px;
    block-size: 18px;
    border: 1px solid color-mix(in srgb, var(--surface-control), var(--text-default) var(--edge-mix));
    border-radius: 0;
    background: var(--surface-control);
    cursor: pointer;
    display: grid;
    place-content: center;
    transition: border-color .2s ease, background-color .2s ease;
  }
  prism-check input[type="radio"] { border-radius: 50% }
  prism-check input:hover { border-color: var(--text-dimmed) }
  prism-check input:checked {
    background: var(--interactive);
    border-color: var(--interactive);
  }
  /* The mark is drawn in the SURFACE colour, so it reads on whatever
   * --interactive turns out to be in this mode and on this ground. */
  prism-check input[type="checkbox"]:checked::after {
    content: "";
    inline-size: 10px; block-size: 6px;
    border-left: 2px solid var(--surface-default);
    border-bottom: 2px solid var(--surface-default);
    transform: translateY(-1px) rotate(-45deg);
  }
  prism-check input[type="radio"]:checked::after {
    content: "";
    inline-size: 8px; block-size: 8px;
    border-radius: 50%;
    background: var(--surface-default);
  }
  prism-check input:focus-visible { outline: 2px solid var(--interactive); outline-offset: 2px }
  /* Disabled is grey here too: the grey ground, the mark in the grey text. */
  prism-check input:disabled {
    cursor: not-allowed;
    background: color-mix(in srgb, var(--text-default) 10%, var(--surface-default));
    border-color: color-mix(in srgb, color-mix(in srgb, var(--text-default) 10%, var(--surface-default)), var(--text-default) var(--edge-mix));
  }
  prism-check input[type="checkbox"]:disabled::after { border-color: color-mix(in srgb, var(--text-default) 45%, var(--surface-default)) }
  prism-check input[type="radio"]:disabled::after { background: color-mix(in srgb, var(--text-default) 45%, var(--surface-default)) }

  /* --- A7b · prism-switch
   * ----------------------------------------------- **THE ON/OFF CONTROL,
   * AND IT IS ROUND.** Owns one `<input type=checkbox role=switch>` — R21
   * — so it submits, labels and announces the way a checkbox does, and a
   * reader hears "switch, on" rather than "checkbox, checked". The label
   * that names it is a `prism-label`, as for prism-check.
   *
   * It joins the radio, the chip, the thumb and the float control as a place
   * radius is not 0, and for their reason: the shape is the meaning. A square
   * track reads as a field and a square knob as a checkbox.
   *
   * DERIVED from prism-check: the same 1px --border-default edge and
   * --surface-control ground when off, --interactive when on, the knob in
   * the surface colour so it reads on whatever --interactive is on this
   * ground. 36×20 sits on the caption's line beside an 18px check. */
  prism-switch { display: inline-flex; flex: none }
  prism-switch input {
    appearance: none;
    margin: 0;
    inline-size: 36px;
    block-size: 20px;
    border: 1px solid color-mix(in srgb, var(--surface-control), var(--text-default) var(--edge-mix));
    border-radius: 10px;
    background: var(--surface-control);
    cursor: pointer;
    display: flex;
    align-items: center;
    padding: 2px;
    transition: border-color .2s ease, background-color .2s ease;
  }
  /* The knob. Off it is the dimmed text colour, so an off switch is still
   * visibly a control on a pale ground; on it is the surface colour. */
  prism-switch input::after {
    content: "";
    inline-size: 14px;
    block-size: 14px;
    border-radius: 50%;
    background: var(--text-dimmed);
    transition: transform .2s ease, background-color .2s ease;
  }
  prism-switch input:hover { border-color: var(--text-dimmed) }
  prism-switch input:checked {
    background: var(--interactive);
    border-color: var(--interactive);
  }
  prism-switch input:checked::after {
    background: var(--surface-default);
    transform: translateX(16px);
  }
  /* A right-to-left page throws the knob the other way: on is at the end. */
  prism-switch input:checked:dir(rtl)::after { transform: translateX(-16px) }
  prism-switch input:focus-visible { outline: 2px solid var(--interactive); outline-offset: 2px }
  prism-switch input:disabled {
    cursor: not-allowed;
    background: color-mix(in srgb, var(--text-default) 10%, var(--surface-default));
    border-color: color-mix(in srgb, color-mix(in srgb, var(--text-default) 10%, var(--surface-default)), var(--text-default) var(--edge-mix));
  }
  prism-switch input:disabled::after { background: color-mix(in srgb, var(--text-default) 45%, var(--surface-default)) }
  /* **Reduced motion jumps.** The state is the colour and the side, and
   * neither needs the slide to be read. */
  @media (prefers-reduced-motion: reduce) {
    prism-switch input, prism-switch input::after { transition: none }
  }

  /* --- A8 · prism-picker ------------------------------------------------
   * **THE CONTROL ONLY.** Owns one `<select>`. The dropdown itself stays the
   * platform's: a re-implemented listbox is a keyboard trap and a
   * screen-reader problem in exchange for a chevron.
   */
  prism-picker { display: block; position: relative }
  prism-picker select {
    width: 100%;
    appearance: none;
    font: 500 var(--type-control) / 1.5 var(--font-display);
    letter-spacing: var(--anybody-tracking);
    color: var(--text-default);
    background: var(--surface-control);
    border: 1px solid color-mix(in srgb, var(--surface-control), var(--text-default) var(--edge-mix));
    border-radius: 0;
    padding: 12px 40px 12px 14px;
    cursor: pointer;
    transition: border-color .2s ease;
  }
  /* The chevron, drawn rather than an icon: prism-icon would be a second
   * element and this atom owns one. */
  prism-picker::after {
    content: "";
    position: absolute;
    right: 16px; top: 50%;
    inline-size: 8px; block-size: 8px;
    border-right: 1px solid var(--text-default);
    border-bottom: 1px solid var(--text-default);
    transform: translateY(-75%) rotate(45deg);
    pointer-events: none;
  }
  prism-picker select:hover { border-color: var(--text-dimmed) }
  /* Same ruling as prism-input: on focus the border holds and the ring is the
   * one interactive line — the picker had the identical double edge. */
  prism-picker select:focus { outline: none }
  prism-picker select:disabled {
    cursor: not-allowed;
    background: color-mix(in srgb, var(--text-default) 10%, var(--surface-default));
    color: color-mix(in srgb, var(--text-default) 45%, var(--surface-default));
    border-color: color-mix(in srgb, color-mix(in srgb, var(--text-default) 10%, var(--surface-default)), var(--text-default) var(--edge-mix));
  }

  /* --- A16 · prism-panel -------------------------------------------------
   * **THE CENTERED PANEL.** The fifth place a radius is not 0 (radio, chip
   * pill, thumb 3px, the float circle), and like the others the shape carries
   * the meaning: a rounded plate floating free of the page's hard-edged flow.
   *
   * It owns one <dialog>: the PLATFORM centres it, traps focus, closes on
   * Escape; prism.js reflects those closings into the `open` attribute and
   * closes on a backdrop click.
   *
   * **The PAGE's colour at 75% transparency, over the float control's
   * blur** — a frosted plate, the
   * card border kept. Browsers without backdrop-filter get the thin fill
   * alone over the ink scrim, which still reads. */
  prism-panel { display: block }
  prism-panel > dialog {
    background: color-mix(in srgb, var(--surface-default) 25%, transparent);
    -webkit-backdrop-filter: blur(12px);
    backdrop-filter: blur(12px);
    color: var(--text-default);
    border: 1px solid color-mix(in srgb, var(--surface-default), var(--text-default) var(--edge-mix));
    border-radius: 8px;
    padding: var(--space-6);
    /* Percentages, not viewport units: a top-layer dialog's containing block
     * IS the viewport, and the checker keeps viewport units countable. */
    max-inline-size: min(var(--measure-prose), calc(100% - var(--space-5) * 2));
    max-block-size: calc(100% - var(--space-8) * 2);
  }
  /* The scrim is INK at 45% in both modes — a dark theatre behind the plate.
   * A literal, not the token: ::backdrop cannot read the mode's custom
   * properties in every engine this system serves. */
  prism-panel > dialog::backdrop {
    background: rgba(13, 15, 24, .45);
  }

  /* --- M7 · prism-thumb ------------------------------------------------
   * **A PICTURE AND NOTHING ELSE.** A listing is thumbs; a card is a card.
   *
   * **3px, and no border.** The third place a radius is not 0, after the round
   * radio and the pill chip — a dense grid of hard corners reads as a table,
   * and the thumb is the one element that appears forty at a time.
   *
   * It assembles `prism-image` and owns nothing.
   */
  prism-thumb {
    display: block;
    position: relative;
    border-radius: 3px;
    overflow: hidden;
    min-inline-size: 0;
  }

  /* **THE TYPE MARKER.** A still frame is indistinguishable from a photograph,
   * and in a grid of forty the difference decides whether pressing it opens a
   * picture or starts sound.
   *
   * **A `prism-tag`, NOT A `prism-chip`.** The
   * marker is not clickable. A chip owns a `<button aria-pressed>` and its
   * pill is reserved to mean press me; a badge built as one would announce to
   * a reader as a control with a state that does nothing. `prism-tag` is
   * already "a label with no state", and it is `size="s"` because a marker on
   * a 104px thumb has to be compact.
   *
   * **Lucide `film`, not `play`.** *The owner's choice.* A play triangle means
   * press me too, and the whole thumb is already the control. `film` says what
   * the item IS; `video` is a camcorder and reads as "record".
   *
   * **IT CARRIES THE MODE'S OWN GROUND, SO IT NAMES THE SEMANTIC VARIABLES.**
   * An OPAQUE square of the page's own surface is a hole punched in the
   * photograph, and inside that hole the mode means what it means everywhere
   * else.
   *
   * **BLUE ON LIGHT AND PINK ON DARK IS `--interactive`**, by its own two
   * values. Named that way rather than written twice, so the marker follows
   * the day either is amended — which has happened once already.
   *
   * **A SQUARE, AND RADIUS 0.** *The owner's word.* A square with rounded
   * corners is not one, and 0 is what everything in this system is except the
   * three places a shape carries meaning.
   *
   * `pointer-events: none` because the thumb underneath is the link. */
  prism-thumb > prism-tag {
    position: absolute;
    inset-block-end: var(--space-2);
    inset-inline-start: var(--space-2);
    aspect-ratio: 1;
    pointer-events: none;
  }
  /* **THE SELECTION DISC.** The marker's atom in the opposite corner: a CIRCLE of
   * --interactive with the check in the mode's surface colour — the rule every
   * interactive fill follows — and no ring. Round, because the shape is the
   * meaning: the radio is round for the same reason. */
  prism-thumb > prism-tag.prism-thumb-check {
    inset-inline-start: auto;
    inset-inline-end: var(--space-2);
    inline-size: 24px;
    block-size: 24px;
  }
  prism-thumb[selectable] { cursor: pointer }
  prism-thumb[selectable]:focus-visible { outline: 2px solid var(--interactive); outline-offset: 2px }
  prism-thumb a { display: block; border-radius: inherit }
  prism-thumb[href]:hover { opacity: .85 }
  prism-thumb a:focus-visible { outline: 2px solid var(--interactive); outline-offset: 2px }

  /* --- M4 · prism-choice, M5 · prism-select ---------------------------
   * **BOTH WERE ASSEMBLIES FILED AS ATOMS.** prism-choice built an `<input>`,
   * a `<label>` AND a `<span>`; prism-select built a `<label>` and a
   * `<select>`. Each owns nothing now: the control is an atom, the name is a
   * `prism-label`, and clicking the label works because the label points at
   * the control with `for` — which is what `for` is, and is why no wrapper
   * element is needed.
   */
  prism-choice { display: flex; align-items: center; gap: var(--space-3) }

  prism-select { display: block }

  /* --- A11 · prism-image -----------------------------------------------
   * DERIVED. **The ratio is the point.** An image of unknown proportions
   * dropped into a grid reflows the whole row when it loads; declaring the
   * box up front is what stops the page moving under the reader. */
  prism-image {
    display: block;
    overflow: hidden;
    background: var(--border-default);
    aspect-ratio: var(--_ratio, auto);
    /* **NEVER TALLER THAN THE SCREEN.** The box keeps its width and is at most
     * the window under the bar high, the same height prism-video takes; the
     * picture inside keeps its own ratio by `fit` — contain shows it whole and
     * centred, cover crops to the shorter box. A wide picture never reaches it. */
    max-block-size: calc(100dvh - 56px - env(safe-area-inset-top, 0px) - var(--space-5) * 2);
  }
  prism-image[ratio="1/1"]  { --_ratio: 1 / 1 }
  prism-image[ratio="4/3"]  { --_ratio: 4 / 3 }
  prism-image[ratio="3/4"]  { --_ratio: 3 / 4 }
  prism-image[ratio="16/9"] { --_ratio: 16 / 9 }
  prism-image[ratio="3/2"]  { --_ratio: 3 / 2 }
  prism-image img {
    inline-size: 100%;
    block-size: 100%;
    object-fit: var(--_fit, cover);
    display: block;
  }
  prism-image[fit="contain"] { --_fit: contain }

  /* --- the frame strip ------------------------------------------------- **ONE
   * FILE, N FRAMES SIDE BY SIDE, ONE SHOWN AT A TIME.** A video thumbnail, a
   * burst, a before/after pair and a scan sequence are all this shape, and a
   * still frame of a video is the one thing a video card should not be.
   *
   * `ratio` is the FRAME's, not the file's. The image is laid out N boxes
   * wide and one box tall, so its shape matches the strip's and nothing is
   * cropped when the two ratios agree.
   *
   * **STEPPED IN CSS, NOT IN SCRIPT.** A grid of forty of these would
   * otherwise be forty timers or forty observers. `steps()` moves the strip
   * one box per frame and the animation is the whole mechanism.
   *
   * **IT PLAYS ON HOVER, AND ON FOCUS SO A KEYBOARD IS NOT SHUT OUT.** On a
   * touch screen there is no hover and the strip rests on frame one — stated
   * rather than worked around, because the alternative is forty cards moving
   * at once.
   */
  prism-image[frames] { overflow: hidden }
  prism-image[frames] img {
    inline-size: calc(var(--_frames, 1) * 100%);
    block-size: 100%;
    object-fit: cover;
    animation: prismFrames 1.2s steps(2) 1 paused;
  }
  prism-image[frames="2"] img { --_frames: 2; animation-timing-function: steps(2) }
  prism-image[frames="3"] img { --_frames: 3; animation-timing-function: steps(3) }
  prism-image[frames="4"] img { --_frames: 4; animation-timing-function: steps(4) }
  prism-image[frames="5"] img { --_frames: 5; animation-timing-function: steps(5) }
  prism-image[frames="6"] img { --_frames: 6; animation-timing-function: steps(6) }
  prism-image[frames="7"] img { --_frames: 7; animation-timing-function: steps(7) }
  prism-image[frames="8"] img { --_frames: 8; animation-timing-function: steps(8) }

  /* The strip travels its own full width, and `steps()` lands it on each
   * frame boundary on the way. */
  /* --- state · FAILED AND WAITING ----------------------------------------
   * No src is PENDING and draws the empty box; `state` says what happened
   * instead, in the status pattern -- a tint of the status colour, its edge,
   * its icon, the words in the text colour. The words are the tenant's, in
   * `message`.
   *
   *   failed   the critical icon, on prism-thumb as the marker, on
   *            prism-image centred in the box with the words under it
   *            and on prism-player as a plate over the box, the controls
   *            hidden. There is no waiting: a video is playable or failed. */
  prism-image[state="failed"] {
    display: grid;
    place-content: center;
    justify-items: center;
    gap: var(--space-2);
    padding: var(--space-4);
    background: color-mix(in srgb, var(--_st-fill) var(--status-tint), var(--surface-default));
    border: 1px solid var(--_st-fill);
    color: var(--text-default);
    font: 500 var(--type-caption) / 1.5 var(--font-display);
    letter-spacing: var(--anybody-tracking);
    text-align: center;
  }
  prism-image[state="failed"] > img { display: none }
  prism-image[state="failed"]::before {
    content: "";
    inline-size: 32px;
    block-size: 32px;
    background: var(--_st-fill);
    -webkit-mask: var(--_st-icon) center / contain no-repeat;
    mask: var(--_st-icon) center / contain no-repeat;
  }
  prism-image[state="failed"]::after { content: attr(message) }
  /* An icon-only status mark, for the thumb's corner: the tag's own shape
   * with no words, centred. */
  prism-tag[intent]:empty { padding: .3em; align-items: center; justify-content: center }
  prism-tag[intent]:empty::before { translate: none }

  @keyframes prismFrames { to { transform: translateX(-100%) } }

  /* The hover target is whatever holds the picture: the image alone, or the
   * element it sits in. Focus inside that holder counts too — a card's link
   * is in its title. No holder is named: the image answers to its parent. */
  prism-image[frames]:hover img,
  prism-image[frames]:focus-within img,
  :hover > prism-image[frames] img,
  :focus-within > prism-image[frames] img {
    animation-play-state: running;
  }

  /* **Reduced motion rests on frame one.** A strip that cannot move is a
   * still, and a still of frame one is what the card shows without this. */
  @media (prefers-reduced-motion: reduce) {
    prism-image[frames] img { animation: none }
  }

  /* --- A17 · prism-avatar
   * ---------------------------------------------- **A PARTY, NOT AN
   * ITEM.** Round is the sixth place a radius is not 0, and for the
   * reason the others are: a 3px square picture reads as a prism-thumb,
   * which is something you open.
   *
   * The picture is always 1/1 and cover; the letters sit on a grey of the
   * text mixed 10% into the surface — darker on paper, lighter on ink, one
   * step deeper than the field box's 6% — rather than a colour that means
   * nothing. NO EDGE. */
  prism-avatar {
    --_ground: color-mix(in srgb, var(--text-default) 10%, var(--surface-default));
    --_size: 48px;
    display: inline-grid;
    place-items: center;
    flex: none;
    inline-size: var(--_size);
    block-size: var(--_size);
    border-radius: 50%;
    overflow: hidden;
    background: var(--_ground);
    color: var(--text-default);
    font: 600 var(--_type, var(--type-body)) / 1 var(--font-display);
    letter-spacing: var(--anybody-tracking);
    user-select: none;
  }
  prism-avatar[size="s"] { --_size: 32px; --_type: var(--type-caption) }
  prism-avatar[size="l"] { --_size: 96px; --_type: 40px }
  prism-avatar > img {
    inline-size: 100%;
    block-size: 100%;
    object-fit: cover;
    display: block;
  }

  /* --- A12 · prism-spinner ---------------------------------------------
   * DERIVED. An arc in --interactive, so "busy" is the same colour as
   * "this reacts" everywhere else in the system.
   *
   * It respects prefers-reduced-motion by stopping, not by disappearing:
   * a reader who cannot take motion still needs to know the thing is busy. */
  prism-spinner {
    display: inline-block;
    inline-size: var(--_size, 20px);
    block-size: var(--_size, 20px);
    border: 2px solid var(--border-default);
    border-top-color: var(--interactive);
    border-radius: 50%;
    animation: prismSpin .7s linear infinite;
  }
  prism-spinner[size="large"] { --_size: 40px; border-width: 3px }
  /* camelCase on purpose: `prism-spin` is indistinguishable from an
   ELEMENT name, and the check that keeps prism.css, prism.js and
   manifest.json agreeing reads that namespace. A keyframe that can be
   mistaken for an element makes the guard cry wolf. */
  @keyframes prismSpin { to { transform: rotate(360deg) } }
  @media (prefers-reduced-motion: reduce) {
    prism-spinner { animation: none; border-top-color: var(--interactive); opacity: .7 }
  }
}

@layer prism.atoms {

  /* --- A13 · prism-icon ------------------------------------------------
   * Lucide, ISC, curated to 60 and served as one sprite from this origin
   * (R7, no CDN). `tools/build-icons.py` generates it.
   *
   * **THE SYMBOLS CARRY NO ATTRIBUTES AND THE STROKE LIVES HERE.** Lucide is
   * stroke art -- `fill:none`, `stroke:currentColor`, width 2, round caps.
   * Baking that onto 60 symbols would state one fact 60 times and freeze the
   * weight; on `<use>` it is inherited, so the stroke is one token and an
   * icon takes the colour of whatever surface it sits on for nothing.
   *
   * DEFAULT SIZE IS 1.25em, SO AN ICON FOLLOWS ITS TEXT. An icon beside a
   * caption should not be the size of one beside a heading, and an agent
   * with no eyes cannot notice that it is. The named sizes are for the cases
   * where it must be exact.
   */
  prism-icon {
    display: inline-flex;
    inline-size: var(--_icon-size, 1.25em);
    block-size: var(--_icon-size, 1.25em);
    flex: none;
    color: inherit;
    /* Optically centred on the text baseline rather than sitting on it. */
    vertical-align: -.18em;
  }
  prism-icon[size="s"]  { --_icon-size: 16px }
  prism-icon[size="m"]  { --_icon-size: 20px }
  prism-icon[size="l"]  { --_icon-size: 24px }
  prism-icon[size="xl"] { --_icon-size: 32px }

  prism-icon svg {
    inline-size: 100%;
    block-size: 100%;
    fill: none;
    stroke: currentColor;
    stroke-width: var(--icon-stroke, 2);
    stroke-linecap: round;
    stroke-linejoin: round;
  }

  /* The one icon that moves. It reuses the spinner's keyframes rather than
   * declaring a second rotation, and stops under reduced-motion for the
   * same reason: a reader who cannot take motion still needs to know. */
  prism-icon[spin] svg { animation: prismSpin .7s linear infinite }
  @media (prefers-reduced-motion: reduce) {
    prism-icon[spin] svg { animation: none }
  }

  /* --- A14 · prism-slider ------------------------------------------------
   * DERIVED. Owns one `<input type="range">`.
   *
   * **THE TRACK IS ONE GRADIENT, NOT THREE BARS.** A played portion and a
   * buffered portion drawn as their own elements would be two parts the atom
   * owns beside the input, which is an assembly (R21). Three colour stops on
   * one background are the same picture and still one element.
   *
   * `--_played` and `--_buffered` are percentages written from script,
   * because CSS cannot read a range's value. A scrubber that does not show
   * where it is is a scrubber nobody can use.
   *
   * **THE HANDLE IS ROUND AND THE TRACK A LONG PILL.** The bars join the
   * floats, the radio, the chip and the thumb as places the radius is not 0,
   * and like them the shape is the meaning: a pill runs.
   */
  prism-slider {
    display: block;
    --_played: 0%;
    --_buffered: 0%;
    /* **THE TWO UNPLAYED TONES ARE MIXED, NOT NAMED.** The named ones do not
     * work here in both modes: `--border-default` is `--_shadow` on ink, which
     * against ink is 1.3:1 and the empty track disappears; `--text-dimmed` on
     * paper is near-black, and the buffered bar then reads heavier than the
     * played one it sits behind. Mixing one variable at two strengths gives
     * played, buffered and empty in that order of weight, in both modes. */
    --_track-buffered: color-mix(in srgb, var(--text-dimmed) 65%, transparent);
    --_track-rest: color-mix(in srgb, var(--text-dimmed) 28%, transparent);
  }
  prism-slider > input {
    appearance: none;
    -webkit-appearance: none;
    display: block;
    inline-size: 100%;
    block-size: 12px;
    margin: 0;
    padding: 0;
    background: transparent;
    cursor: pointer;
  }
  prism-slider > input:disabled { opacity: .4; cursor: not-allowed }
  /* **THE TRACK IS WRITTEN TWICE ON PURPOSE.** One selector list holding both
   * a -webkit- and a -moz- pseudo-element is dropped WHOLE by both engines:
   * neither parses the other's name, and an unknown selector in a list
   * invalidates the rule. Two rules is the only form that reaches both. */
  prism-slider > input::-webkit-slider-runnable-track {
    block-size: 4px;
    border-radius: 2px;
    background: linear-gradient(to right,
      var(--interactive) 0 var(--_played),
      var(--_track-buffered) var(--_played) var(--_buffered),
      var(--_track-rest) var(--_buffered) 100%);
  }
  prism-slider > input::-moz-range-track {
    block-size: 4px;
    border-radius: 2px;
    background: linear-gradient(to right,
      var(--interactive) 0 var(--_played),
      var(--_track-buffered) var(--_played) var(--_buffered),
      var(--_track-rest) var(--_buffered) 100%);
  }
  prism-slider > input::-webkit-slider-thumb {
    -webkit-appearance: none;
    inline-size: 12px;
    block-size: 12px;
    border: none;
    border-radius: 50%;
    background: var(--text-default);
    /* Centre the handle on a 4px track: half the handle less half the track. */
    margin-block-start: -4px;
  }
  prism-slider > input::-moz-range-thumb {
    inline-size: 12px;
    block-size: 12px;
    border: none;
    border-radius: 50%;
    background: var(--text-default);
  }
  prism-slider > input:hover::-webkit-slider-thumb { background: var(--interactive) }
  prism-slider > input:hover::-moz-range-thumb     { background: var(--interactive) }
  /* The ring goes on the CONTROL, not on the handle. A ring around a 12px
   * square that moves is a ring nobody finds; a ring around the slider says
   * which control the arrow keys now belong to. */
  prism-slider > input:focus { outline: none }
  prism-slider > input:focus-visible {
    outline: 2px solid var(--interactive);
    outline-offset: 4px;
  }

  /* --- prism-progress ----------------------------------------------------
   * **HOW FAR, AS A BAR AND A FIGURE.** The slider's track — 4px, the filled
   * part --interactive, the rest the slider's empty tone — so the scrubber and
   * the bar read as one family. The figure is the control voice in equal-width
   * digits, drawn from data-text and hidden from a reader, who hears the bar's
   * own value instead. */
  prism-progress {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    --_track-rest: color-mix(in srgb, var(--text-dimmed) 28%, transparent);
  }
  prism-progress > progress {
    appearance: none;
    -webkit-appearance: none;
    flex: 1 1 auto;
    min-inline-size: 0;
    block-size: 4px;
    margin: 0;
    border: none;
    border-radius: 2px;
    overflow: hidden;
    background: var(--_track-rest);
    color: var(--interactive);
  }
  /* One rule per engine: a list holding both pseudo-elements is dropped whole. */
  prism-progress > progress::-webkit-progress-bar { background: var(--_track-rest); border-radius: 2px }
  prism-progress > progress::-webkit-progress-value { background: var(--_heat, var(--interactive)); border-radius: 2px }
  prism-progress > progress::-moz-progress-bar { background: var(--_heat, var(--interactive)); border-radius: 2px }
  prism-progress::after {
    content: attr(data-text);
    content: attr(data-text) / "";
    min-inline-size: 4ch;
    text-align: end;
    white-space: nowrap;
    font: 500 var(--type-caption) / 1.5 var(--font-display);
    letter-spacing: var(--anybody-tracking);
    font-variant-numeric: tabular-nums;
    color: var(--text-default);
  }

  /* --- prism-status, and intent on prism-tag -------------------------------
   * **STATUS IS A SCALE OF URGENCY.** The hue says how urgent; the shape is
   * the same for every intent, below. The icon is drawn from a mask, so colour
   * is never the only signal and the atom builds nothing: neutral
   * circle-check, info info, warning triangle-alert, critical circle-x. */
  [intent="neutral"] {
    --_st-fill: var(--status-neutral);
    --_st-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'/%3E%3Cpath d='m16 9-5.5 5.5L8 12'/%3E%3C/svg%3E");
  }
  [intent="info"] {
    --_st-fill: var(--status-info);
    --_st-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'/%3E%3Cpath d='M12 16v-4'/%3E%3Cpath d='M12 8h.01'/%3E%3C/svg%3E");
  }
  [intent="warning"] {
    --_st-fill: var(--status-warning);
    --_st-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3'/%3E%3Cpath d='M12 9v4'/%3E%3Cpath d='M12 17h.01'/%3E%3C/svg%3E");
  }
  [intent="critical"],
  :is(prism-image, prism-thumb, prism-player)[state="failed"] {
    --_st-fill: var(--status-critical);
    --_st-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'/%3E%3Cpath d='m15 9-6 6'/%3E%3Cpath d='m9 9 6 6'/%3E%3C/svg%3E");
  }
  prism-status {
    font: 500 var(--type-caption) / 1.5 var(--font-display);
    letter-spacing: var(--anybody-tracking);
  }
  prism-status:not([intent]) {
    --_st-fill: var(--status-neutral);
    --_st-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'/%3E%3Cpath d='m16 9-5.5 5.5L8 12'/%3E%3C/svg%3E");
  }
  /* **ONE TREATMENT FOR EVERY INTENT.** So every state is the same shape,
   * and only the hue says which: a TINT of the status colour as the ground,
   * the status colour as the edge and the icon, the words in the mode's text
   * colour. The tint is the status colour at --status-tint into the surface
   * it sits on -- the pressed chip's shape, which is what a tint is for. */
  :is(prism-status, prism-tag[intent]) {
    display: inline-flex;
    /* On the baseline, then the icon dropped by half of what it is taller
     * than a capital — centred on the CAPITALS, not on the line box, which
     * Anybody's tall ascent sets higher than its letters (prism-brand's
     * emblem, the same move). */
    align-items: baseline;
    gap: .4em;
    padding: .25em .7em .25em .4em;
    background: color-mix(in srgb, var(--_st-fill) var(--status-tint), var(--surface-default));
    border: 1px solid var(--_st-fill);
    color: var(--text-default);
    opacity: 1;
    font-style: normal;
  }
  prism-tag[intent] {
    font: 500 var(--_tag-size, var(--type-meta)) / 1.4 var(--font-display);
    letter-spacing: var(--anybody-tracking);
  }
  :is(prism-status, prism-tag[intent])::before {
    content: "";
    flex: none;
    inline-size: 1.15em;
    block-size: 1.15em;
    translate: 0 .22em;
    translate: 0 calc((1.15em - 1cap) / 2);
    background: var(--_st-fill);
    -webkit-mask: var(--_st-icon) center / contain no-repeat;
    mask: var(--_st-icon) center / contain no-repeat;
  }

  /* --- A15 · prism-video --------------------------------------------------
   * DERIVED, and the argument is prism-image's. Owns one `<video>`.
   *
   * **THE RATIO IS THE POINT, AND MORE SO THAN FOR AN IMAGE.** A video's
   * proportions are not known until its METADATA loads, and metadata is not
   * fetched at all under `preload="none"` — which is the default here. Without
   * a stated ratio the box is zero-high until the viewer presses play.
   *
   * **NO `controls` ATTRIBUTE, AND THAT IS THE DESIGN CHOICE.** The platform's
   * own control bar is the browser's chrome and looks nothing like this
   * system; exposing it in one word is how a tenant ships it by accident. A
   * video that can be operated is `prism-player`. This is the picture.
   */
  prism-video {
    display: block;
    overflow: hidden;
    background: var(--border-default);
    aspect-ratio: var(--_ratio, auto);
  }
  :is(prism-video, prism-player)[ratio="1/1"]  { --_ratio: 1 / 1 }
  :is(prism-video, prism-player)[ratio="4/3"]  { --_ratio: 4 / 3 }
  :is(prism-video, prism-player)[ratio="3/4"]  { --_ratio: 3 / 4 }
  :is(prism-video, prism-player)[ratio="16/9"] { --_ratio: 16 / 9 }
  :is(prism-video, prism-player)[ratio="3/2"]  { --_ratio: 3 / 2 }
  /* Upright phone video. */
  :is(prism-video, prism-player)[ratio="9/16"] { --_ratio: 9 / 16 }

  /* **NEVER TALLER THAN THE SCREEN.** The box keeps its ratio and is at most
   * the window under the bar high, so it narrows and is centred, the page's own
   * ground beside it. --_centre is the margin that centres it; the surface pull
   * below takes the larger of the two, so a video as wide as its column still
   * sits at the page edge on a phone. In full screen the screen is the limit,
   * and the browser's backdrop is the black beside it. */
  :is(prism-video, prism-player) {
    --_vmax: calc(var(--_vh, (100dvh - 56px - env(safe-area-inset-top, 0px) - var(--space-5) * 2)) * var(--_ratio));
    --_centre: calc((100% - var(--_vmax)) / 2);
    margin-inline: max(0px, var(--_centre));
  }
  prism-player > prism-video { margin-inline: 0 }
  /* **THE LIMIT IS INHERITED, SO THE VIDEO TAKES THE SCREEN'S TOO.** The
   * height the player and the video both use is --_vh, which the player
   * sets on itself in full screen and the video inherits, so the controls
   * sit on the picture. */
  prism-player:fullscreen {
    display: grid;
    grid-template: minmax(0, 1fr) / minmax(0, 1fr);
    place-items: center;
    --_vh: 100dvh;
  }
  prism-player:fullscreen > prism-video { inline-size: min(100%, var(--_vmax)) }
  prism-video video {
    inline-size: 100%;
    block-size: 100%;
    object-fit: var(--_fit, cover);
    display: block;
    background: var(--border-default);
  }
  prism-video[fit="contain"] { --_fit: contain }
}

@layer prism.atoms {

  /* --- A9 · prism-emblem, A10 · prism-wordmark -------------------------
   * **THE TWO PARTS OF THE LOGO, EACH AN ATOM.** `prism-brand` built an
   * `<svg>` and two `<span>`s and was filed as an atom; it is a molecule and
   * these are what it assembles.
   *
   * prism-emblem owns ONE `<svg>`. prism-wordmark owns NOTHING AT ALL — the
   * prefix is a `::before` reading an attribute and the name is the element's
   * own text, so the wordmark is one word with two weights and no elements.
   *
   * **THE PREFIX IS STYLED, NOT ADDED.** A name beginning with `My` gets it
   * set lighter; a name that does not have one renders whole. **The suite's
   * apps are My-something and the system that serves them is not one of
   * them** — it is what they all consume.
   */
  /* **THE MARK CARRIES A DROP SHADOW IN THE PAGE'S OWN COLOUR.** On plain
   * ground it is page colour on page colour and invisible; where content
   * passes behind the header it is the halo that keeps the mark legible.
   * `drop-shadow`, not box-shadow, so it follows the emblem's shape and the
   * letterforms rather than their boxes. On both atoms rather than on
   * prism-brand, so a mark used alone carries it too. */
  /* **AND SO DOES EVERYTHING THE BAR HOLDS.** The bar has no ground now, so the
   * shadow is the only thing between them and what scrolls behind. On the atoms
   * themselves, as the mark's is, and words by ground="picture" — the bar's
   * date line. **A button carries none**: the float, the hamburger, and the
   * menu's box. */
  prism-emblem,
  prism-wordmark,
  prism-text[ground="picture"] {
    /* **STRONGER.** Two shadows in the page's own colour, solid: a tight 3px
     * one that holds the edge of every letter and a 12px one that carries
     * the glow. */
    filter: drop-shadow(0 0 var(--_halo-near, 3px) var(--_halo, var(--surface-default))) drop-shadow(0 0 var(--_halo-far, 12px) var(--_halo, var(--surface-default)));
  }  /* **IN LIGHT MODE THE WORDS ARE OUTLINED IN WHITE**, .5px on each side;
   * the emblem keeps the shadow. */
  prism-wordmark,
  prism-text[ground="picture"] { filter: var(--_text-outline, drop-shadow(0 0 var(--_halo-near, 3px) var(--_halo, var(--surface-default))) drop-shadow(0 0 var(--_halo-far, 12px) var(--_halo, var(--surface-default)))) }


  prism-emblem {
    display: inline-block;
    inline-size: .95em;
    block-size: .95em;
    flex: none;
  }
  prism-emblem svg { inline-size: 100%; block-size: 100%; display: block }
  /* **CENTRED ON THE CAPITALS, NOT ON THE LINE BOX.** prism-brand sits the
   * emblem's foot on the wordmark's baseline, and this drops it by half of what
   * it is taller than a capital — so its middle is the capitals' middle in any
   * face, whatever the face's own ascent and descent do to the line box. `cap`
   * is the face's capital height; a browser without it takes the fixed
   * fallback, which is close for a serif. */
  prism-brand prism-emblem {
    translate: 0 .13em;
    translate: 0 calc((.95em - 1cap) / 2);
  }

  /* **NEWSREADER.** The system serves it at 400-500, so the prefix is
   * lighter again the way the rule says: the name at 500, the prefix at
   * 400. Synthesis is off so no weight outside the served range is faked. */
  prism-wordmark {
    display: inline-block;
    white-space: nowrap;
    font-family: "Newsreader", Georgia, serif;
    letter-spacing: normal;
    font-weight: 500;
    font-synthesis: none;
  }

  /* --- M6 · prism-brand -------------------------------------------------
   * The logo: emblem then wordmark, `() MyDesign`. Assembles the two above
   * and owns nothing. **THE WORDMARK'S FACE IS NAMED, NOT TAKEN FROM --font-body**: a
   * wordmark is an identity asset, and the day somebody changes what
   * --font-display means, every heading should follow and the logo should not.
   */
  prism-brand {
    display: inline-flex;
    align-items: baseline;
    gap: .34em;
    color: inherit;
    line-height: 1.1;
    font-size: var(--_brand-size, var(--type-h2));
  }
  prism-brand[size="s"] { --_brand-size: var(--type-caption) }
  prism-brand[size="m"] { --_brand-size: var(--type-h2) }
  prism-brand[size="l"] { --_brand-size: var(--type-h1) }
  prism-brand a {
    color: inherit; text-decoration: none;
    display: inline-flex; align-items: baseline; gap: .34em;
  }

}

/* ===========================================================================
 * A18-A19 · DATA. prism-table and prism-plot.
 *
 * **AIR AND HAIRLINES.** DESIGN.md has no
 * table; its "funktion före form" is the rule applied — no stripes, one
 * hairline per row, a grey header band, typography carrying the hierarchy. The plot is
 * tracks in the palette's order, gridlines in the border colour. **A data
 * area is always a card on the page's own ground**, below.
 * ======================================================================== */

@layer prism.atoms {

  /* --- A18 · prism-table -------------------------------------------------
   * The element is the scroller: a table wider than its column scrolls
   * sideways inside itself and never widens the page. Every cell is inset,
   * so the text sits inside the header's grey band and the rows align to it. */
  prism-table {
    display: block;
    max-inline-size: 100%;
    overflow-x: auto;
  }
  prism-table > table {
    inline-size: 100%;
    border-collapse: collapse;
    font: 400 var(--type-body) / 1.5 var(--font-body);
    color: var(--text-default);
  }
  prism-table caption {
    caption-side: top;
    text-align: start;
    padding-block-end: var(--space-4);
    font: 500 var(--type-caption) / 1.5 var(--font-display);
    letter-spacing: var(--anybody-tracking);
    color: var(--text-default);
  }
  /* **THE HAIRLINE IS THE TEXT MIXED 20% INTO THE SURFACE**, a step above
   * the border colour: 1.76:1 on ink and 1.52:1 on paper, where
   * --border-default is 1.20 and 1.34. The system's 15% edge would be 1.36
   * on paper, no visible change. */
  prism-table :is(th, td) {
    padding: var(--space-3) var(--space-4);
    text-align: start;
    vertical-align: baseline;
    font-weight: 400;
    border-block-end: 1px solid color-mix(in srgb, var(--text-default) 20%, var(--surface-default));
  }
  /* **THE LAST ROW HAS NO HAIRLINE.** The card's edge closes the table; a
   * rule above it would be a second line. */
  prism-table tbody tr:last-child > :is(th, td) { border-block-end: none }
  /* The header is the metadata voice: it names, it does not shout. Its
   * ground is the field box's grey, the text mixed 6% into the surface. */
  prism-table thead th {
    background: color-mix(in srgb, var(--text-default) 6%, var(--surface-default));
    font: italic 400 var(--type-meta) / 1.4 var(--font-meta);
    letter-spacing: 0.015em;
    color: var(--text-dimmed);
    white-space: nowrap;
  }
  /* A number column is set by its `type`: right-aligned, equal-width figures,
   * so the digits of one magnitude stand in one column. */
  prism-table [data-type="number"] {
    text-align: end;
    font-variant-numeric: tabular-nums lining-nums;
    white-space: nowrap;
  }
  /* A progress column: the row's number as prism-progress's track and
   * figure, the bar leading, the figure at the end of the cell. */
  prism-table [data-type="progress"] {
    text-align: end;
    font-variant-numeric: tabular-nums lining-nums;
    white-space: nowrap;
  }
  prism-table td[data-type="progress"]::before {
    content: "";
    display: inline-block;
    vertical-align: middle;
    inline-size: 6em;
    block-size: 4px;
    border-radius: 2px;
    margin-inline-end: var(--space-3);
    background: linear-gradient(to right,
      var(--_heat, var(--interactive)) 0 var(--_p, 0%),
      color-mix(in srgb, var(--text-dimmed) 28%, transparent) var(--_p, 0%) 100%);
  }

  /* A status column: the cell's words in prism-status's shape — the intent's
   * tint as the ground, its colour as the edge and the icon. */
  prism-table :is(td, th)[data-type="status"] { white-space: nowrap }
  prism-table :is(td, th)[data-type="status"] > span {
    display: inline-flex;
    align-items: baseline;
    gap: .4em;
    padding: .25em .7em .25em .4em;
    background: color-mix(in srgb, var(--_st-fill) var(--status-tint), var(--surface-default));
    border: 1px solid var(--_st-fill);
    color: var(--text-default);
    font: 500 var(--type-caption) / 1.5 var(--font-display);
    letter-spacing: var(--anybody-tracking);
  }
  prism-table :is(td, th)[data-type="status"] > span::before {
    content: "";
    flex: none;
    inline-size: 1.15em;
    block-size: 1.15em;
    translate: 0 calc((1.15em - 1cap) / 2);
    background: var(--_st-fill);
    -webkit-mask: var(--_st-icon) center / contain no-repeat;
    mask: var(--_st-icon) center / contain no-repeat;
  }

  /* --- A19 · prism-plot ---------------------------------------------------
   * prism.js draws the SVG at the width the element has. No colour is in
   * the SVG: every mark carries a track number and this maps it. */
  prism-plot { display: block }
  prism-plot > svg { display: block; overflow: visible }
  /* The tracks read the DATA colours, the 400 step of each hue, not
   * the brand steps. */
  prism-plot [data-track="1"] { --_track: var(--data-pink) }
  prism-plot [data-track="2"] { --_track: var(--data-blue) }
  prism-plot [data-track="3"] { --_track: var(--data-mint) }
  prism-plot [data-track="4"] { --_track: var(--data-orange) }
  prism-plot :is([data-part="bar"], [data-part="dot"], [data-part="swatch"]) { fill: var(--_track) }
  /* 1.5, one step down from 2.5. */
  prism-plot [data-part="line"] {
    fill: none;
    stroke: var(--_track);
    stroke-width: 1.5;
    stroke-linejoin: round;
    stroke-linecap: round;
  }
  prism-plot stop { stop-color: var(--_track) }
  prism-plot [data-part="arc"] { fill: none; stroke: var(--_track) }
  prism-plot [data-part="ring"] { fill: none; stroke: var(--border-default) }
  prism-plot [data-part="axis"] { stroke: var(--border-default); stroke-width: 1 }
  prism-plot [data-part="grid"] { stroke: var(--border-default); stroke-width: 1; stroke-dasharray: 2 4 }
  prism-plot text {
    fill: var(--text-dimmed);
    font: italic 400 var(--type-micro) var(--font-meta);
    letter-spacing: 0.015em;
  }
  prism-plot [data-part="legend"] { fill: var(--text-default) }
  prism-plot [data-part="caption"] {
    fill: var(--text-default);
    font: 500 var(--type-caption) var(--font-display);
    letter-spacing: var(--anybody-tracking);
  }
  prism-plot [data-part="number"] {
    fill: var(--text-default);
    font-family: var(--font-display);
    font-style: normal;
    font-weight: 600;
    letter-spacing: var(--anybody-tracking);
  }

  /* **A DATA AREA IS A CARD, ALWAYS, ON THE PAGE'S OWN GROUND.** A table or a
   * chart carries the card's own ground, hairline and padding itself, so a
   * tenant writes the data element and gets the card; it never wraps one. The
   * values are the card's: ground 3% of the text into the surface, the edge
   * that ground pushed, --space-5 inside. On the two ATOMS: a chart is a
   * carded plot and a hidden table, and a molecule restyles nothing it holds
   * (R29). */
  :is(prism-table, prism-plot) {
    background: color-mix(in srgb, var(--text-default) 3%, var(--surface-default));
    border: 1px solid color-mix(in srgb, color-mix(in srgb, var(--text-default) 3%, var(--surface-default)), var(--text-default) var(--edge-mix));
    color: var(--text-default);
    padding: var(--space-5);
  }
}

/* ===========================================================================
 * M1-M2 · THE MOLECULES.
 *
 * **THE LAYER BETWEEN ATOMS AND LAYOUT, AND THE RULE IS THE OWNER'S:
 * A MOLECULE IS AN ASSEMBLY OF ATOMS AND NEVER OWNS COMPONENTS.**
 *
 * It composes what exists. It never introduces a visual part of its own --
 * no bespoke inner element, no new vocabulary. The moment an assembly needs a
 * part that is not there, THAT IS AN ATOM REQUEST, not a molecule, which is
 * what stops this layer becoming a back door for unnamed components.
 *
 * So every child a molecule renders is a prism- element that already existed:
 * prism-image, prism-stack, prism-tag, prism-text, prism-cluster. The padded
 * text block is a prism-stack rather than a `<div class="card-body">` for
 * exactly this reason. `tools/check-prism.py` asserts it.
 *
 * *This rule cannot be applied backwards without a look: prism-brand owns its
 * internals and could not be a molecule. It stays an atom -- one indivisible
 * mark -- and that is named rather than quietly excepted.*
 * ======================================================================== */

@layer prism.molecules {

  /* --- M1 · prism-card, M2 · prism-image-card --------------------------
   * TWO COMPONENTS, RULED BY THE OWNER, against one with an optional image.
   * **They share this block on purpose.** The risk of two names is that they
   * drift; sharing the rules for tag, title, body and meta means they cannot
   * drift VISUALLY, and the only real difference stays the one the owner
   * named: whether there is a picture.
   *
   * Transcribed from DESIGN.md 3.5 and the reference's `.article`:
   * ground --surface-default, 48px, the title going --interactive on hover,
   * and the whole card a pointer.
   */
  prism-card,
  prism-image-card {
    display: flex;
    flex-direction: column;
    position: relative;
    /* **A CARD'S GROUND SITS BETWEEN THE PAGE'S AND A FIELD'S.** The field is
     * 6% of the text colour into the surface (--surface-control); the card is
     * the halfway point, 3%. Mixed on the card, so it follows whatever ground
     * the card sits on — the same argument as the control ground and the
     * slider's track tones. */
    background: color-mix(in srgb, var(--text-default) 3%, var(--surface-default));
    color: var(--text-default);
    /* **THE HAIRLINE IS THE CARD'S.** A grid whose ground showed through a 1px
     * gap drew rules between cards and a grey BLOCK wherever the last row was
     * short. A border belongs to the thing it outlines. */
    border: 1px solid color-mix(in srgb, color-mix(in srgb, var(--text-default) 3%, var(--surface-default)), var(--text-default) var(--edge-mix));
    transition: background-color .2s ease;
    /* **A grid item's default minimum is its CONTENT.** Without this a long
     * unbroken word makes the card wider than its track and pushes the whole
     * row out of the grid. */
    min-inline-size: 0;
    /* The card measures ITSELF, so the rules below answer to the card's own
     * width and not to the screen's. A card in a narrow sidebar on a wide
     * monitor is narrow. */
    container-type: inline-size;
    /* **CARDS ON ONE ROW ARE ONE HEIGHT.** A grid cell stretches to its row;
     * the card fills its cell, also when something of the tenant's sits
     * between them. Where the parent has no height of its own this is auto,
     * and the card is as tall as its text. */
    block-size: 100%;
  }

  /* **A CARD IS NEVER A TILE.** A thumb is `prism-thumb`, its own molecule.
   * The padding comes down before a card gets cramped: 32px each side of a
   * 200px card is two thirds of it.
   */
  @container (max-width: 320px) {
    :is(prism-card, prism-image-card) > prism-stack { padding: var(--space-4) }
  }
  :is(prism-card, prism-image-card):has(a[href]) { cursor: pointer }

  /* **THE STRUCTURE IS THE SAME WHETHER OR NOT THE CARD IS A LINK.** Every
   * rule here is a DIRECT-CHILD selector, and *`display: contents` removes
   * the box, never the DOM position.*
   *
   * The link lives inside the title and stretches over the card with a
   * pseudo-element. One structure, every selector matches, the whole card is
   * the target, and the accessible name is the title rather than the entire
   * contents read aloud. */
  :is(prism-card, prism-image-card) > prism-stack {
    padding: var(--space-5);
    min-inline-size: 0;
  }
  prism-image-card > prism-stack:empty { display: none }

  /* **THE STEP CONTROLS STAND ON THE PICTURE.** `prev` and `next` share the
   * picture's row, centred on its height, 4px in from its edges: bare
   * buttons on ground="picture", ruled by the owner. The bar is a prism-cluster; its gap and wrap give way here,
   * because two controls at opposite edges are not a row. */
  prism-image-card:has(> [data-prism-part="step"]) {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: auto 1fr;
    align-content: start;
  }
  prism-image-card:has(> [data-prism-part="step"]) > :is(prism-image, prism-video, prism-player) { grid-area: 1 / 1 }
  prism-image-card > [data-prism-part="step"] {
    grid-area: 1 / 1;
    align-self: center;
    z-index: 1;
    display: flex;
    flex-wrap: nowrap;
    padding-inline: var(--space-1);
    pointer-events: none;
  }
  prism-image-card > [data-prism-part="step"] > * { pointer-events: auto }
  prism-image-card > [data-prism-part="step"] > [data-prism-step="next"] { margin-inline-start: auto }

  :is(prism-card, prism-image-card) > prism-image,
  prism-image-card > :is(prism-video, prism-player) {
    inline-size: 100%;
  }
  /* A moving picture fills the card's width as the still does; the screen
   * limit is for a video that stands alone. */
  prism-image-card > :is(prism-video, prism-player) { margin-inline: 0 }

  /* A linked card's title is a prism-link cover: its press area, hover and
   * focus ring are the link's own. A card with nothing to open builds no link
   * and does not react. */

  /* --- M9 · prism-nav · THE HEADER BAR
   * ------------------------------------ **BRAND LEFT, MENU RIGHT, NOTHING
   * ELSE.**
   *
   * **ONE COMPONENT FOR BOTH SHAPES.** Above 900px the items are inline text
   * links; below it they are a hamburger and a dropdown. Same element, same
   * children, same markup — a tenant writes neither shape and never sees the
   * width. Same boundary the wall takes its fourth column at.
   *
   * **IT SITS IN THE PAGE'S OWN MEASURE.** The owner's rule was left and right
   * spacing "like the gutter", and on a 2560px screen a gutter-only bar leaves
   * the brand at x=64 while the content below it is centred in 1280 — brand
   * and page visibly out of line. So the bar takes the measure AND the gutter,
   * the way `prism-container` does, and lines up with what it heads.
   *
   * **16px INSIDE THE GUTTER, AT EVERY WIDTH.** `--space-4` added to the
   * gutter. The box is the gutter's and the 16px is inside it, so centring on a
   * wide screen keeps it.
   *
   * It assembles prism-brand, prism-cluster, prism-stack, prism-button,
   * prism-icon and prism-link, and owns nothing.
   */
  prism-nav {
    display: block;
    position: relative;
    /* It kept the shadows off the strip behind the clock while the bar
     * started under it; with viewport-fit=cover (R38) the bar's contents sit
     * the safe-area inset below the screen's edge. */
    margin-inline: auto;
    padding-inline: calc(var(--gutter) + var(--space-4));
    max-inline-size: calc(var(--measure-app) + var(--gutter) * 2);
  }
  /* **OVER THE WALL, 16px INSIDE THE WALL'S EDGE, AT EVERY WIDTH.** The wall
   * carries its own edges (0, then 24) and measure, not the page gutter's, so
   * on a page that holds one the bar takes the wall's, and the brand and the
   * date line stand 16px in from the pictures at every width. */
  prism-page:has(prism-grid[type="listing"]) prism-nav {
    --_edge: 0px;
    padding-inline: calc(var(--_edge) + var(--space-4));
    max-inline-size: calc(var(--measure-app) + var(--_edge) * 2);
  }
  @media (min-width: 600px) {
    prism-page:has(prism-grid[type="listing"]) prism-nav { --_edge: var(--space-5) }
  }
  /* **AS THIN AS IT GOES — AND ONE HEIGHT IN BOTH SHAPES.** — that made the bar
   * ~5px shorter the moment the hamburger left, and every page under the sticky
   * bar jumped up at exactly the width the nav changes shape. The floor is the
   * hamburger's in BOTH shapes: 20px icon + 8px button padding + 1px border
   * each side = 38px, plus 8px bar padding above and 4px below = 50px,
   * border-box. The brand (~33px) rides inside it at every width. */
  prism-nav > prism-cluster {
    padding-block: var(--space-2) var(--space-1);
    min-block-size: 50px;
  }
  /* **THE FLOAT STATE SEATS A 44px CIRCLE, SO ITS FLOOR IS 56.** The menu
   * bar's cap is 50 — the hamburger's row — and variant="float" is a 44px
   * circle: 44 + 8 + 4 = 56. The collision the ask named, settled by the bar
   * growing in THIS state rather than the circle shrinking under its hit
   * size. A tenant's header is one state or the other, so nothing jumps. */
  prism-nav[float] > prism-cluster { min-block-size: 56px }
  /* **THE DATE LINE SITS UNDER THE ROW**, in the bar's own inset, so it lines
   * up with the brand. Built by the nav; hidden when the page has no dated
   * thumb, so a bar without one is the height it always was. */
  prism-nav > .prism-nav-date { margin-block-end: var(--space-2) }
  .prism-nav-floats { flex-wrap: nowrap }

  /* **THE TWO SHAPES ARE ONE LIST, POSITIONED DIFFERENTLY.** Mobile first, so
   * the dropdown is what a browser matching no query gets.
   *
   * **THE PANEL IS SOLID.** The bar's fade is for content passing underneath
   * it; a dropdown that faded would put its last row on top of that content. */
  .prism-nav-items {
    display: none;
    position: absolute;
    inset-inline: 0;
    top: 100%;
    z-index: 1;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    /* A tad grey: the card's ground, 3% of the text into the page — darker
     * on paper, lighter on ink — so the open menu lifts off what it covers. */
    background: color-mix(in srgb, var(--text-default) 3%, var(--surface-default));
    border-block-end: 1px solid var(--border-default);
    padding: var(--space-2) 0 var(--space-4);
  }
  prism-nav[open] .prism-nav-items { display: flex }
  @media (min-width: 900px) {
    .prism-nav-burger { display: none }
    /* The same element, back in the bar. */
    .prism-nav-items {
      display: flex;
      position: static;
      flex-direction: row;
      align-items: center;
      gap: var(--space-5);
      padding: 0;
    }
    /* **THE INLINE MENU STANDS IN THE HAMBURGER'S BOX**: the page's own
     * ground, opaque, its edge, its corners and its height. */
    .prism-nav-items {
      block-size: 38px;
      padding-inline: var(--space-4);
      background: var(--surface-default);
      border: 1px solid color-mix(in srgb, var(--surface-default), var(--text-default) var(--edge-mix));
      border-radius: 7px;
    }
  }

  /* --- M8 · prism-player
   * ------------------------------------------------- **THE CONTROLS SIT
   * OVER THE VIDEO AND COME AND GO.**
   *
   * They sat under it, and the reason was the scrim: text on an arbitrary
   * video has no contrast guarantee, which is what kept text off the face of a
   * card. **Auto-hide is what changes that argument.** A scrim that is only
   * there while the pointer is answers the objection — the picture is
   * uncovered whenever nobody is reaching for a control — and it is the whole
   * reason to hide chrome at all. Chrome below the frame is already out of the
   * picture's way; hiding it buys nothing and costs either a permanent empty
   * strip or a jump on every pointer crossing.
   *
   * **THE BAR CARRIES ITS OWN GROUND, so it names the internal tokens.** Same
   * exception `prism-thumb`'s marker makes, and for the same reason: a bar on
   * an arbitrary frame has no surface to inherit meaning from. The scrim is
   * ink in both modes, so `--interactive` is the reaction that answers to ink
   * — pink — rather than the mode's: the ground decides the reaction, not
   * the mode.
   *
   * It assembles prism-video, prism-cluster, prism-button, prism-icon,
   * prism-slider and prism-text, and owns nothing.
   */
  prism-player {
    display: block;
    position: relative;
    /* The bar answers to the PLAYER's width, not the screen's. A player in a
     * narrow column on a wide monitor is narrow. */
    container-type: inline-size;
  }
  /* **A FAILED PLAYER IS A PLATE OVER THE BOX**, the status pattern in
   * --status-critical with the critical icon, which the intent rule sets.
   * There is no waiting: a video is playable or it failed. */
  prism-player[state="failed"]::before {
    content: attr(message);
    position: absolute;
    inset: 0;
    z-index: 1;
    display: grid;
    place-content: center;
    padding: 56px var(--space-4) 0;
    background: color-mix(in srgb, var(--_st-fill) var(--status-tint), var(--surface-default));
    border: 1px solid var(--_st-fill);
    color: var(--text-default);
    font: 500 var(--type-caption) / 1.5 var(--font-display);
    letter-spacing: var(--anybody-tracking);
    text-align: center;
  }
  prism-player[state="failed"]::after {
    content: "";
    position: absolute;
    z-index: 2;
    inset-inline-start: calc(50% - 16px);
    inset-block-start: calc(50% - 20px);
    inline-size: 32px;
    block-size: 32px;
    background: var(--_st-fill);
    -webkit-mask: var(--_st-icon) center / contain no-repeat;
    mask: var(--_st-icon) center / contain no-repeat;
  }
  /* **THE SURFACE IS A PLAY BUTTON.** The button in the bar is still the one a
   * keyboard reaches and a reader hears announced; this is the gesture, and it
   * is why hiding the bar leaves nothing unreachable. */
  prism-player > prism-video { cursor: pointer }

  /* **A PILL IN THE FLOATS' COLOURS.** — it is now the floats' plate: the
   * page's own colour, opaque, the text colour, the hairline edge, 44px high,
   * inset 12px from the video's sides and foot. Its buttons are bare icons,
   * so the pill is the only shape. The slider and the time take the page's
   * own colours from it. */
  prism-player > prism-cluster {
    position: absolute;
    inset-inline: var(--space-3);
    inset-block-end: var(--space-3);
    align-items: center;
    min-block-size: 44px;
    padding-inline: var(--space-2) var(--space-4);
    border: 1px solid color-mix(in srgb, var(--surface-default), var(--text-default) var(--edge-mix));
    border-radius: 23px;
    background: var(--surface-default);
    color: var(--text-default);
    transition: opacity .2s ease, visibility .2s ease;
  }
  /* **IN FULL SCREEN THE PILL FOLLOWS THE VIDEO.** The player is the screen
   * there; the video is the screen's height times its ratio wide, or the
   * screen's width and that divided by its ratio high, centred. The pill is
   * inset 12px from that box, not from the screen. */
  /* **A PLAYER WITH NO RATIO FROM THE LIST STILL GETS ITS BAR ON THE
   * SCREEN.** Written straight into the insets, a missing --_ratio made both
   * declarations invalid, the insets fell back to auto, and the bar dropped
   * to its place in the flow — under the video. Held in custom properties
   * instead, a missing ratio makes them invalid and the var() fallback — the
   * screen's own edge — is used. */
  prism-player:fullscreen > prism-cluster {
    --_fs-side: calc((100cqi - min(100cqi, 100dvh * var(--_ratio))) / 2 + var(--space-3));
    --_fs-foot: calc((100dvh - min(100dvh, 100cqi / (var(--_ratio)))) / 2 + var(--space-3));
    inset-inline: var(--_fs-side, var(--space-3));
    inset-block-end: var(--_fs-foot, var(--space-3));
  }
  /* **`visibility`, NOT ONLY `opacity`.** A bar at zero opacity still takes
   * every click over the bottom of the frame, so the surface would be a play
   * button everywhere except the strip where the picture matters most. */
  prism-player[idle] > prism-cluster {
    opacity: 0;
    visibility: hidden;
  }
  /* **NO HOVER MEANS NO HIDING.** On a touch screen a tap that both reveals
   * the controls and toggles playback is one gesture with two meanings, and
   * whichever the reader wanted, they get the other one half the time. */
  @media (hover: none) {
    prism-player[idle] > prism-cluster { opacity: 1; visibility: visible }
  }
  @media (prefers-reduced-motion: reduce) {
    prism-player > prism-cluster { transition: none }
  }
  /* **THE VOLUME SLIDER IS THE FIRST THING TO GO.** Under 480px the bar is
   * play, scrubber, time, mute and full screen, and the scrubber is what the
   * room is for. Mute stays: it is the control that matters when sound is the
   * problem. */
  .prism-player-volume { flex: 0 0 80px }
  @container (max-width: 479.98px) {
    .prism-player-volume { display: none }
  }
  /* **ONE LINE, ALWAYS.** With a basis of `auto` the scrubber counted as the
   * browser's default slider width when the line was laid out, and pushed the
   * last control over. A basis of 0 and no wrapping: the scrubber takes what
   * is left, however little. */
  prism-player > prism-cluster { flex-wrap: nowrap }
  prism-player prism-slider { flex: 1 1 0; min-inline-size: 0 }
  /* **A FIXED WIDTH FOR THE TIME, BECAUSE IT CHANGES EVERY SECOND.** Left to
   * its content, the readout resizes at 0:09 -> 0:10 and drags the scrubber
   * with it once a second for the length of the video. */
  prism-player > prism-cluster > prism-text {
    min-inline-size: 10ch;
    text-align: end;
    white-space: nowrap;
  }

  /* --- prism-selection-bar ------------------------------------------------
   * Fixed to the bottom of the screen, over the content, on a fade of the
   * page's own colour so the words hold over any photograph. The controls
   * line up with the WALL's edges — the measure and a constant 24 — not with
   * the viewport's, or on a wide monitor they float far off the thing they
   * act on.
   *
   * **THE PAGE MAKES ROOM FOR IT.** While a bar is showing, the content gets
   * the bar's height at its end, so the last row of a listing is never under
   * it. A tenant writes no padding. */
  prism-selection-bar {
    position: fixed;
    z-index: 10;
    inset-inline: 0;
    inset-block-end: 0;
    display: grid;
    grid-template-columns: 1fr auto 1fr;
    align-items: center;
    gap: var(--space-4);
    padding-block: var(--space-7) calc(var(--space-5) + env(safe-area-inset-bottom, 0px));
    padding-inline: max(var(--space-5), calc((100% - var(--measure-app)) / 2));
    background: linear-gradient(to bottom, transparent,
                color-mix(in srgb, var(--surface-default) 85%, transparent) 60%);
    color: var(--text-default);
    pointer-events: none;
  }
  prism-selection-bar > * { pointer-events: auto }
  prism-selection-bar > prism-text { text-align: center; white-space: nowrap }
  prism-selection-bar[hidden] { display: none }
  prism-page:has(prism-selection-bar:not([hidden])) prism-content { padding-block-end: 112px }

  /* --- prism-menu
   * ---------------------------------------------------------- — the page's
   * colour at 75% transparency over a 12px blur, the card's border, the 8px
   * corner. A popover: the platform puts it in the top layer; prism.js sets
   * where. Hidden until it is open, including before the script has run, so
   * its actions never spill into the page. */
  prism-menu:not(:popover-open) { display: none }
  prism-menu[popover] {
    background: color-mix(in srgb, var(--surface-default) 25%, transparent);
    -webkit-backdrop-filter: blur(12px);
    backdrop-filter: blur(12px);
    color: var(--text-default);
    border: 1px solid color-mix(in srgb, var(--surface-default), var(--text-default) var(--edge-mix));
    border-radius: 8px;
    padding: var(--space-2) 0;
    min-inline-size: 240px;
    max-inline-size: min(320px, calc(100% - var(--space-5) * 2));
    max-block-size: calc(100% - var(--space-5) * 2);
    overflow: auto;
  }
  prism-menu .prism-menu-tiles {
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: minmax(0, 1fr);
  }
  prism-menu prism-divider { margin-block: var(--space-2) }

  /* --- M10 · prism-chart --------------------------------------------------- A
   * prism-plot and the same numbers as a prism-table. the plot is static and
   * the table is for a reader without eyes — present to the accessibility tree,
   * absent from the screen. */
  prism-chart { display: block; position: relative }
  prism-chart > prism-table {
    position: absolute;
    inline-size: 1px;
    block-size: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }

}

@layer prism.molecules {

  /* --- prism-notice -------------------------------------------------------
   * **SOMETHING IS WAITING.** A prism-status across the content's width, its
   * words a prism-link cover when there is somewhere to go. On a status fill
   * the link keeps the fill's ink under the pointer: the notice points
   * --interactive at its own text, the channel tone="danger" already uses. */
  prism-notice {
    display: block;
    position: relative;
    --interactive: currentColor;
  }
  prism-notice > prism-status { inline-size: 100% }

}

/* ===========================================================================
 * COMPOSITIONS — R32.
 *
 * A composition takes the components a tenant writes and arranges them the
 * system's way: order, spacing, widths, the container and the title. It owns
 * no look beyond the container and title it lays down; everything else it
 * only places (R29, tools/check-prism.py).
 * ======================================================================== */
@layer prism.compositions {

  /* --- C1 · prism-form ----------------------------------------------------
   * **A FORM IS ALWAYS THE SAME SHAPE.** On the card's ground and edge, the
   * title first and above the fields, the fields in one column at full width
   * with one gap between them, and the buttons in one row at the end, the
   * primary last. Inside a prism-panel the panel is the container, so the
   * form lays down no ground of its own there. */
  prism-form {
    display: block;
    background: color-mix(in srgb, var(--text-default) 3%, var(--surface-default));
    color: var(--text-default);
    border: 1px solid color-mix(in srgb, color-mix(in srgb, var(--text-default) 3%, var(--surface-default)), var(--text-default) var(--edge-mix));
    min-inline-size: 0;
    container-type: inline-size;
  }
  prism-form > form { margin: 0 }
  prism-form > form > prism-stack { padding: var(--space-7) }
  @container (max-width: 320px) {
    prism-form > form > prism-stack { padding: var(--space-4) }
  }
  prism-panel prism-form {
    background: none;
    border: none;
  }
  prism-panel prism-form > form > prism-stack { padding: 0 }

  /* --- C3 · prism-section ---------------------------------------------------
   * **A PAGE IS SECTIONS.** Its heading, then its content, one gap between
   * them; and between two sections the widest step and a hairline. The first
   * section has none above it, so an information page's content starts right
   * under the header. Sections are written side by side in the container; in
   * a prism-stack the stack's gap is the space and the section adds none. */
  prism-section {
    display: flex;
    flex-direction: column;
    gap: var(--space-5);
    min-inline-size: 0;
  }
  prism-section > prism-stack:first-child > prism-text[level="body"] { max-inline-size: 64ch }
  /* **WHAT SITS BESIDE A SECTION IS SET OFF FROM IT** — the notiser
   * above, anything below — by the step inside a section. */
  prism-section:not(:first-child),
  prism-section + * { margin-block-start: var(--space-5) }
  /* **NO HAIRLINE BETWEEN SECTIONS, AND ONE SPACE.** The line had 48px on each
   * side; with it gone, the half that was its own is gone too: 48px between
   * sections. */
  :is(prism-section, prism-intro, prism-hero) + prism-section:not(:first-child) {
    margin-block-start: var(--space-9);
  }

  /* **TEXT ON THE PAGE LINES UP WITH TEXT IN A CARD.** Words that stand on the
   * page and not in a box — a section's heading and description, an intro, a
   * line of counts — are inset by the card's own padding, so their left edge is
   * the card text's and not the card's frame. The card pads --space-5; on a
   * phone the container already holds text 8px in from the cards (--_pull), so
   * the inset is the padding plus the pull: 24px, and 16px on a phone. */
  prism-content prism-text:not(:is(prism-card, prism-image-card, prism-hero, prism-form,
      prism-panel, prism-notices, prism-notice, prism-table, prism-plot, prism-chart,
      prism-thumb, prism-selection-bar, prism-menu, prism-cluster) *) {
    padding-inline: calc(var(--space-5) + var(--_pull, 0px));
  }
  prism-stack > prism-section:not(:first-child),
  prism-stack > prism-section + * { margin-block-start: 0 }

  /* --- C4 · prism-intro -----------------------------------------------------
   * **THE START PAGE OPENS ON ONE WORD.** The display level and its
   * terminator, one sentence under it at a reading measure. Nowhere else. */
  prism-intro { display: block }
  prism-intro > prism-stack > prism-text[level="body-l"] { max-inline-size: 60ch }

  /* --- C6 · prism-hero -------------------------------------------------------
   * **THE OPENING OF A PAGE, IN THREE SHAPES.** All three on the card's ground
   * and hairline.
   *   words    no picture: the inverted ground, xl and its terminator.
   *   over     a picture, no side: the picture fills the hero, at its ratio
   *            and never under 24rem, and the words sit at its foot on the
   *            header's own fade — the page's colour rising from transparent,
   *            with the header's blur — at l and its terminator.
   *   beside   a picture and a side: the picture on that side, the words
   *            beside it at h1; stacked, picture first, when the two cannot
   *            each have 20rem. */
  prism-hero {
    display: grid;
    position: relative;
    isolation: isolate;
    overflow: hidden;
    min-inline-size: 0;
    container-type: inline-size;
    background: color-mix(in srgb, var(--text-default) 3%, var(--surface-default));
    color: var(--text-default);
    border: 1px solid color-mix(in srgb, color-mix(in srgb, var(--text-default) 3%, var(--surface-default)), var(--text-default) var(--edge-mix));
  }
  /* **AN INVERTED CARD HAS NO HAIRLINE.** The opposite ground already sets
   * it off from the page. Transparent, not removed, so the box keeps its
   * size beside a card that has one. */
  :is(prism-card, prism-image-card)[ground="inverted"],
  prism-hero:not([src]):not(:has(> prism-image)) { border-color: transparent }
  prism-hero > prism-stack { padding: var(--space-8) var(--space-7) }
  @container (max-width: 480px) {
    prism-hero > prism-stack { padding: var(--space-6) var(--space-4) }
  }

  /* over */
  prism-hero:not([side]):has(> prism-image) {
    aspect-ratio: var(--_hero-ratio, 16 / 9);
    min-block-size: 24rem;
  }
  prism-hero:not([side]) > prism-image {
    grid-area: 1 / 1;
    block-size: 100%;
    max-block-size: none;
    inline-size: 100%;
    aspect-ratio: auto;
    z-index: -2;
  }
  prism-hero:not([side]):has(> prism-image)::before {
    content: "";
    grid-area: 1 / 1;
    z-index: -1;
    background: linear-gradient(to top, var(--surface-default) 15%, transparent 65%);
    -webkit-backdrop-filter: blur(10px);
    backdrop-filter: blur(10px);
    -webkit-mask-image: linear-gradient(to top, #000 30%, transparent 60%);
    mask-image: linear-gradient(to top, #000 30%, transparent 60%);
    pointer-events: none;
  }
  prism-hero:not([side]):has(> prism-image) > prism-stack {
    grid-area: 1 / 1;
    align-self: end;
  }
  prism-hero[ratio="1/1"]  { --_hero-ratio: 1 / 1 }
  prism-hero[ratio="4/3"]  { --_hero-ratio: 4 / 3 }
  prism-hero[ratio="3/4"]  { --_hero-ratio: 3 / 4 }
  prism-hero[ratio="3/2"]  { --_hero-ratio: 3 / 2 }

  /* beside: two panes of at least 20rem each, so under 40rem they wrap,
   * the picture first. A hero cannot query its own width, so the wrap is
   * the flex basis and not a container query. */
  /* **THE PICTURE RUNS THE HERO'S FULL HEIGHT.** Side by side the picture
   * stretches to the row and crops (cover); alone on its line, stacked, it
   * keeps its ratio. The words centre on the row. */
  prism-hero[side]:has(> prism-image) {
    display: flex;
    flex-wrap: wrap;
    align-items: stretch;
  }
  prism-hero[side="end"]:has(> prism-image) { flex-direction: row-reverse }
  prism-hero[side] > :is(prism-image, prism-stack) { flex: 1 1 20rem }
  prism-hero[side] > prism-image { max-block-size: none }
  prism-hero[side] > prism-stack { align-self: center }

  /* --- C5 · prism-notices --------------------------------------------------
   * **THE NOTISER ARE STRIPES, NO CARD.** Each notis a stripe across the
   * content's full width, and between two of them HALF A NOTICE'S HEIGHT: a
   * notice is a caption line (1.5) plus.25em above and below plus its 1px
   * edges, 2 x caption + 2px, so half is the caption plus 1px. Empty, nothing. */
  prism-notices {
    display: flex;
    flex-direction: column;
    gap: calc(var(--type-caption) + 1px);
    min-inline-size: 0;
  }
  prism-notices:not(:has(> prism-notice)) { display: none }
  /* "Allting OK" while something is wrong: hidden by the card (prism.js). */
  prism-notices > prism-notice[data-prism-quiet] { display: none }


}
