/* 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 500;
  font-display: swap;
  src: url('fonts/newsreader-400-500-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 === */

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

@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. *Ruled by the owner.* 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.
     */
    --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`.** *Ruled by the owner: on mobile width we drop the
     * gutter.* 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.
     * *Shipped once: the header's 8px inset rendered as 0 on a phone.* */
    --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.**
     * *Ruled by the owner 2026-09-28, after 12/20/30% and 10/15/20% were tried side by side: an object that has no solid fill has
     * an outline, and every edge is "the solid colour, but darker".* 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. */
    --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 *ruled by the owner 2026-09-19* 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. *This sentence used to say no
 * component changes shape here, and the wall is the exception named rather
 * than quietly added.*
 *
 * **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. *Ruled by the owner 2026-09-19.* 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. **The list used to be six tags
   * repeated across eleven lines, which meant every new primitive had to be
   * added in eleven places or its gap silently did nothing.** */
  [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.** *Found by the owner:
   * text fields flowing over their container.* 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.** `grid.html`
   * and `page.html` 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.
   *
   * *Found by the owner: a white frame around the page.* `<body>` carries a
   * default 8px margin and the browser's white ground, and `prism-page` sat
   * inside it. **Every proof page had hidden it** by resetting `body` in its
   * own scaffolding — so the defect was invisible until the front door, which
   * is the first page to consume the system with NO local CSS. *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;
  }
  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;
    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. */
  prism-content { flex: 1; min-inline-size: 0 }

  /* --- prism-header ----------------------------------------------------
   * **IT ALWAYS STICKS.** *Ruled by the owner.* The `sticky` attribute is
   * accepted and does nothing — it was opt-in and is now the behaviour, and
   * removing an attribute a tenant may have written is the one thing R20
   * forbids.
   *
   * ~~THE GRADIENT IS THE HEADER'S OWN BACKGROUND~~ **Struck by the owner
   * 2026-09-20: no background at all.** The three-stop gradient (solid to the
   * midpoint, gone at the bottom) is removed whole; the header is transparent
   * and content scrolls fully visible behind the bar's text. The dropdown
   * panel is unaffected — its solid ground is `.prism-nav-items`' own.
   */
  prism-header {
    position: sticky;
    top: 0;
    z-index: 10;
  }

  /* --- 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.
   *
   * ~~**8px INSIDE THE GUTTER, AT EVERY WIDTH.**~~ *Struck by the owner the
   * same day: it moved the cards in with the text.* **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.** *Ruled by the owner
   * 2026-09-24.* 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"]) *) {
    margin-inline: 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.** *Ruled by the owner.*
   *
   *   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.** *Ruled by the
   * owner 2026-09-20, from Apple Photos' library as reference.* 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, and the owner ruled for 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.** *Ruled by the owner: never a thumb wider
   * than its actual pixel size.* 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.**
   *
   * **THE RULE EXISTED AND LOST ON SPECIFICITY.** It was written as
   * `prism-columns > *`, which is (0,0,2), against `prism-columns > [span="6"]`
   * at (0,1,1) — so it stacked children that stated no span and left every
   * spanned one at twelve tracks, which is the only case that matters. A form
   * of `span="6"` fields rendered as two 148px columns on a phone, which is a
   * date field with 90px of room for 90px of text. *Found by the owner looking
   * at a tenant's app on a phone; every check in this repository was green.*
   *
   * **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. It was `--columns-stack-below`,
   * declared once in :root and referenced nowhere — a token that cannot be
   * used is a second truth, and this one was the whole of the bug's cover. */
  @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. *Found by the owner reading section 4 of the proof page.*
   *
   * 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.** *Ruled by the owner 2026-09-27: the word "switch" belongs to the
   * on/off atom.* 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;

    /* Chromatic primaries, one per design pillar. Constant in both modes,
     * full saturation, never dimmed or blended down. */
    --pink: #FF1DA5;
    --mint: #6BFCB8;
    --blue: #10C3EE;

    /* Secondaries -- spectrum complements, for accents and data. */
    --indigo: #4F2BD9;
    --ember: #FF842E;
    --lime: #C8FF00;

    /* Semantic roles. */
    --chromatic: var(--pink);
    --technical: var(--blue);
    --warmth: var(--mint);
    /* --interactive is NOT here: it is per mode, below. DESIGN.md 2.1 maps
     * it to --pink flat, and the owner amended that -- the reaction colour
     * answers to the ground it sits on. */

    /* --- 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. */
    --font-display: "Anybody", ui-sans-serif, system-ui, sans-serif;
    --font-body: "Newsreader", Georgia, "Times New Roman", serif;
    --font-meta: "Instrument Serif", Georgia, 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);
    --surface-chromatic: var(--chromatic);
    --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);
    /* Bold pink on ink: 5.44:1, AA for body text. Measured. */
    --interactive: var(--pink);
    /* 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. */
    --emblem-mint-top: #6BFCB8;    --emblem-mint-bottom: #0E8A9E;
    --emblem-pink-top: #FF1DA5;    --emblem-pink-bottom: #7A1052;
    --emblem-almond-top: #6D1A76;  --emblem-almond-bottom: #1B0E4D;
  }
  prism-page[mode="light"],
  .mode-light {
    --surface-default: var(--_paper);
    --_secondary-fill: #FFFFFF;
    --surface-inverted: var(--_ink);
    --surface-chromatic: var(--warmth);
    --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);
    /* CONSCIOUS BLUE. **Ruled by the owner, twice, after the measurement.**
     *
     * On the record because two of his own rulings now disagree and a later
     * reader must not have to rediscover it: #10C3EE on #F4F2EC measures
     * **1.86:1**, and DESIGN.md's "Tillgängligt" section requires 4.5:1 for
     * body text and 3:1 for UI. This is his call and it stands; the clause
     * is what needs amending, not this line. requirements.md R15 carries it.
     *
     * (For the next person who reaches for the alternative: flat pink here
     * is 3.14:1 and also fails body text, so it is not a safe fallback
     * either. Indigo at 7.00:1 was offered and refused.) */
    --interactive: var(--blue);
    --emblem-mint-top: #6BFCB8;    --emblem-mint-bottom: #15D2EF;
    --emblem-pink-top: #FF1DA5;    --emblem-pink-bottom: #FFD6EE;
    --emblem-almond-top: #6D1A76;  --emblem-almond-bottom: #10C3EE;
  }
  /* 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);
      --surface-chromatic: var(--warmth);
      --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);
      --emblem-mint-top: #6BFCB8;    --emblem-mint-bottom: #15D2EF;
      --emblem-pink-top: #FF1DA5;    --emblem-pink-bottom: #FFD6EE;
      --emblem-almond-top: #6D1A76;  --emblem-almond-bottom: #10C3EE;
    }
  }
}

/* --- 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. The owner has ruled out calculated values, 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: 22px;
    --type-body: 18px;
    --type-caption: 14px;
    /* **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 stay the
     * caption the owner ruled. Read by prism-input and prism-picker only. */
    --type-control: var(--type-caption);
    /* ANYBODY TRACKING. **Ruled by the owner: 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: 16px } }
  @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: *ruled by the owner 2026-09-19, both are
   * the same size on m and l* — 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 } }
  @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, DESIGN.md 2.3 -------------------------------
   * Three ways to render a surface, and a section renders on exactly one.
   *
   * DESIGN.md ties B and C to single uses -- inverted is the pullquote,
   * chromatic is the proof section, one interruption per composition. THOSE
   * ARE EDITORIAL RULES FOR ONE LONG-FORM PAGE and they are not enforced
   * here, because the tenants are applications with no pullquote and no
   * proof section. The surfaces exist; how sparingly they are used stays
   * the composition's business.
   */
  /* **A SURFACE REMAPS THE VARIABLES FOR EVERYTHING INSIDE IT, not just its
   * own two colours.** *Found by the owner: contrast was unreadable on every
   * surface except default.*
   *
   * 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"] {
    --_secondary-fill: transparent;
    --surface-default: var(--surface-inverted);
    --text-default: var(--text-inverted);
    --text-dimmed: var(--text-dimmed-inverted);
    background: var(--surface-default);
    color: var(--text-default);
  }
  /* Chromatic text is ALWAYS ink, in both modes: DESIGN.md 2.3 and 3.7.
   * Pink and mint are both light enough that paper text on them fails.
   *
   * Remapping --interactive here is DESIGN.md 2.8's named exception, applied
   * once instead of as a special rule per atom: pink on pink is invisible, so
   * on this ground the reaction is ink. */
  prism-surface[tone="chromatic"] {
    --_secondary-fill: transparent;
    --surface-default: var(--surface-chromatic);
    --text-default: var(--_ink);
    /* DESIGN CHOICE: 2.6 gives no dimmed level for a chromatic ground.
     * A 30% mix toward the ground, which is what dimmed means everywhere else. */
    --text-dimmed: color-mix(in srgb, var(--_ink) 70%, var(--surface-chromatic));
    --interactive: var(--_ink);
    --border-default: color-mix(in srgb, var(--_ink) 30%, var(--surface-chromatic));
    background: var(--surface-default);
    color: var(--text-default);
  }

  /* 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, except xl and l, ruled below. 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 is what broke
     * every non-default surface: the atom overrode the ground it was sitting
     * 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;
  }
  prism-text[level="xl"]      { --_family: var(--font-display); --_weight: 700; --_size: var(--type-xl);      --_leading: 1.02; --_tracking: var(--anybody-tracking) }
  prism-text[level="l"]       { --_family: var(--font-display); --_weight: 700; --_size: var(--type-l);       --_leading: 1.06; --_tracking: var(--anybody-tracking) }
  prism-text[level="h1"]      { --_family: var(--font-display); --_weight: 600; --_size: var(--type-h1);      --_leading: 1.12; --_tracking: var(--anybody-tracking) }
  prism-text[level="h2"]      { --_family: var(--font-display); --_weight: 600; --_size: var(--type-h2);      --_leading: 1.2;  --_tracking: var(--anybody-tracking) }
  /* **XL AND L IN CAPITALS.** *Ruled by the owner 2026-09-28: "XL and L
   * all caps. All H as they were."* This sets aside DESIGN.md 2.5's "no
   * capitals" for the two display levels only — the note above, and
   * check-prism's guard, stand for every other level; the guard allows this
   * one rule by its exact text. The text stays mixed
   * case in the markup, so a reader and a copy-paste get the words. */
  prism-text:is([level="xl"], [level="l"]) { text-transform: uppercase }
  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 }
  prism-text[level="caption"] { --_family: var(--font-display); --_weight: 500; --_size: var(--type-caption); --_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 }

  /* 2.6's three levels. `dimmed` is a PRE-MIXED colour and not an opacity:
   * the mix holds its contrast on chromatic 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 -----------------------------------------------
   * **TRANSCRIBED FROM preview_prism_11.html, NOT DESIGNED HERE.** *The
   * owner did not recognise the previous shape, and was right not to:
   * DESIGN.md 3.5 gives only the colour logic, so the padding, the radius
   * and the height had been invented.* The reference carries the values he
   * has already seen, and these are those values.
   *
   * Four things were wrong in the invented version, each named so the
   * transcription can be checked against `.btn` in the reference:
   *   radius        2px          -> 0
   *   size          min-height   -> padding 14px 22px, inline-block
   *   primary hover --surface-default -> --_ink
   *   link          no arrow     -> arrow, and it moves 4px on hover
   */
  prism-button { display: inline-block }
  prism-button > button {
    display: inline-block;
    font: 500 14px / 1.65 var(--font-display);
    letter-spacing: var(--anybody-tracking);
    padding: 14px 22px;
    border: 1px solid transparent;
    /* **7px.** *Ruled by the owner 2026-09-28, after 5px was tried.*
     * 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.** *The owner, 2026-09-28: secondary has a
   * white background.* Paper is #F4F2EC, so transparent read as a grey fill.
   * **AND LIFTED ON INK.** *Ruled by the owner 2026-09-28: "fix secondary
   * dark fill".* 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%. Inverted and
   * chromatic grounds keep 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 {
    background: none;
    color: inherit;
    border: none;
    border-bottom: 1px solid currentColor;
    border-radius: 0;
    padding: 0 0 2px;
  }
  /* 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, DESIGN.md 2.8 --------------------
   * Everything that reacts, reacts in --interactive. One place.
   * On a chromatic ground --interactive is already remapped to ink by
   * prism-surface, so the exception needs no rule of its own. */
  prism-button > button:hover {
    background: var(--interactive);
    color: var(--_ink);
  }
  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) }
  /* **A SECOND SIZE, AND IT IS A DESIGN CHOICE RATHER THAN A RULE.** The
   * reference gives one button: 14px 22px, which is 48px tall. A row of those
   * under a 480px video is a control bar taller than a paragraph. This is the
   * same button at the same weight and colour, in less room — a toolbar size,
   * not a second button.
   *
   * An icon-only button goes SQUARE: 12px each side of a 20px icon is a
   * lozenge, and a control bar of lozenges reads as a row of tabs. */
  prism-button[size="s"] > button { padding: 8px 12px }
  /* **FLEX, BECAUSE AN INLINE-BLOCK BUTTON IS TALLER THAN ITS ICON.** The
   * text line box (14px × 1.65 ≈ 23px) stands behind a 20px icon and sets the
   * height, so an "icon-only" button was ~41px, not 8+20+8+2 = 38 — and the
   * nav's bar rode it 3px past its floor below 900, hopping the wordmark down
   * a couple of px at the shape switch. *Found by the owner's eye; the
   * arithmetic in this file had claimed 38 all along.* Flex has no strut: the
   * button is exactly padding + icon + border. */
  prism-button[size="s"] > button:has(> prism-icon:only-child) {
    padding: 8px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }
  /* **AND THE WRAPPER, WHICH HAS A LINE BOX OF ITS OWN.** Fixing the button
   * left `prism-button` itself inline-block, whose strut — the INHERITED body
   * font, 18px × 1.65 — hangs its descent under the button's baseline: ~45px
   * around a 38px button, and the wordmark still hopped. *The owner's eye
   * again, ~2px this time.* Both boxes are flex now; an icon-only control
   * measures its icon and nothing else. */
  prism-button[size="s"]:has(> button > prism-icon:only-child) { display: inline-flex }

  /* --- variant="float" · THE FLOATING CONTROL, FIRST TRY ------------------
   * *Ruled by the owner 2026-09-20, Apple Photos' library as reference: a
   * CIRCLE — not a pill — a 16px icon inside, half-transparent fill with
   * blur, pink or blue by mode.* The fill is 50% --interactive, which IS
   * pink-on-dark / blue-on-light by the mode's own mapping; the icon is ink,
   * the rule every interactive fill already follows. 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.
   * Browsers without backdrop-filter get the 50% fill alone, which still
   * reads; the blur is enhancement, not the mechanism. */
  prism-button[variant="float"] > button {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    inline-size: 44px;
    block-size: 44px;
    padding: 0;
    /* Its fill is half-transparent, so it carries the edge: the solid
     * --interactive it is half of, pushed. */
    border: 1px solid color-mix(in srgb, var(--interactive), var(--text-default) var(--edge-mix));
    border-radius: 50%;
    background: color-mix(in srgb, var(--interactive) 50%, transparent);
    -webkit-backdrop-filter: blur(12px);
    backdrop-filter: blur(12px);
    color: var(--_ink);
  }
  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. */
  prism-button[variant="float"] > button:hover {
    background: var(--interactive);
    color: var(--_ink);
  }
  prism-button[variant="float"] > button:focus-visible { border-radius: 50% }
  /* **WITH WORDS IT IS A PILL.** *Ruled by the owner 2026-09-24: "Välj" is
   * text in a pill-shaped button.* The same 44px height, fill and blur; the
   * width is the label's. The circle is for an icon alone. */
  prism-button[variant="float"] > button:not(:has(> prism-icon)),
  prism-button[variant="float"] > button:not(:has(> prism-icon)):focus-visible {
    inline-size: auto;
    min-inline-size: 44px;
    padding-inline: var(--space-4);
    border-radius: 22px;
  }

  /* --- variant="item" and variant="tile" · THE ACTIONS OF A MENU ----------
   * *Ruled by the owner 2026-09-23 with prism-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%;
    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.** *Found by the
     * owner on a phone: "Favoritmarkera" ran into both neighbours.* 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 }

  /* --- tone="danger" · THE DESTRUCTIVE ACTION -------------------------------
   * *Ruled by the owner 2026-09-23: ember.* The palette's own secondary, not
   * a new red. It is carried as the button's --interactive, so every variant's
   * reaction follows it with no rule of its own — the float circle fills
   * ember at 50%, a hover goes ember — and the resting colour is set only
   * where a variant has one. */
  prism-button[tone="danger"] { --interactive: var(--ember) }
  prism-button[tone="danger"]:is(:not([variant]), [variant="primary"]) > button {
    background: var(--ember);
    color: var(--_ink);
  }
  prism-button[tone="danger"]:is([variant="secondary"], [variant="link"], [variant="item"], [variant="tile"]) > button {
    color: var(--ember);
  }
  prism-button[tone="danger"][variant="secondary"] > button { border-color: var(--ember) }

  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.** *Ruled by the owner 2026-09-28: a greyed ground
   * and grey text — empty read as an unpressed secondary.* 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.** *Ruled by the owner: a chip
   * must not read as a button.* 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 `--interactive` filled, off is an outline — the same relationship
   * `prism-button` has between primary and secondary, 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-display);
    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);
    border-color: var(--interactive);
    color: var(--surface-default);
  }
  prism-chip > button[aria-pressed="true"]:hover { opacity: .85; color: var(--surface-default) }
  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) }

  /* --- A5 · prism-label, A6 · prism-input -----------------------------
   * **THE TWO ATOMS A FIELD IS MADE OF.** *Ruled by the owner.* 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;
  }

  /* --- prism-fact ------------------------------------------------------
   * **ONE FACT PER LINE.** *Ruled by the owner 2026-09-24.* 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.** *Ruled by
   * the owner: slightly lighter in dark mode, slightly darker in light mode.*
   * 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.**
   * *Ruled by the owner 2026-09-20.* The border used to go --interactive too,
   * so a keyboard focus drew two pink lines a ring's width apart — the field's
   * own edge and the focus-visible ring outside it. Only the ring survives.
   * `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 ------------------------------------------------
   * **NOW A MOLECULE, AND IT ASSEMBLES THE TWO ABOVE.** It used to build a
   * `<label>` and an `<input>` itself, which made it an assembly filed as an
   * atom — *the owner read the atoms page and said so.* It owns nothing now:
   * 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;
  }
}

/* ===========================================================================
 * 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 are historical and no longer 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.** *Ruled by the owner,
   * for a marker on a thumb that has to be compact and must not read as
   * something to press.* 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) }

  /* --- 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, which the owner ruled: 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.** *Ruled by the owner 2026-09-27.*
   * 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.** *Ruled by the owner 2026-09-20: opened from the
   * float control, centred on the screen, the card's ground and border, and a
   * ROUND CORNER — 8px.* 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 ground is the card's 3% mix~~ **Amended by the owner 2026-09-20:
   * 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.** *Ruled by the owner.* A listing is thumbs;
   * a card is a card. The card used to hide its own text when it got narrow,
   * which made one element two things and neither of them well.
   *
   * **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;
  }
  prism-thumb prism-image { border-radius: inherit }

  /* **THE TYPE MARKER.** *Ruled by the owner: the thumb takes a type, and a
   * type may carry a small tag with an icon.* 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`, and the owner ruled it so.** 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.**
   * *Ruled by the owner: the actual background colour for that mode, and the
   * mark blue on light, pink on dark.* It was an ink scrim with paper text in
   * both modes — the exception `prism-surface[tone="chromatic"]` makes — and
   * it does not need to be: 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);
    display: grid;
    place-items: center;
    aspect-ratio: 1;
    padding: var(--space-1);
    border-radius: 0;
    /* A tag is metadata beside prose at half strength. Here it is the only
     * thing carrying the fact, so it is at full. */
    opacity: 1;
    color: var(--interactive);
    background: var(--surface-default);
    pointer-events: none;
  }
  /* **THE SELECTION DISC.** *Ruled by the owner 2026-09-23, Apple Photos as
   * reference.* The marker's atom in the opposite corner: a CIRCLE of
   * --interactive with the check in ink — the rule every interactive fill
   * follows — and no ring, the owner's word 2026-09-24. 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;
    padding: 0;
    border-radius: 50%;
    color: var(--_ink);
    background: var(--interactive);
  }
  prism-thumb > prism-tag.prism-thumb-check prism-icon { --_icon-size: 14px }
  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-choice prism-label > 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;
  }
  prism-choice:has(input:disabled) prism-label > label { color: color-mix(in srgb, var(--text-default) 45%, var(--surface-default)) }
  prism-choice:has(input:disabled) prism-label > label { cursor: not-allowed }

  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);
  }
  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.** *Ruled by the
   * owner, request 2.* 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. */
  @keyframes prismFrames { to { transform: translateX(-100%) } }

  /* The hover target is whatever holds the picture: the image alone, or the
   * card around it. A card's focus counts too — its link is in the title. */
  prism-image[frames]:hover img,
  prism-image[frames]:focus-within img,
  :is(prism-card, prism-image-card):hover prism-image[frames] img,
  :is(prism-card, prism-image-card):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.** *Ruled by the owner 2026-10-04: round, three
   * sizes, initials on the control surface.* 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%, ruled by the owner — rather than a
   * colour that means nothing. NO EDGE, ruled by the owner. */
  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 SQUARE.** A round one would be a fourth place a radius is
   * not 0, against three the system counts and names. A 12px square handle on
   * a 4px track is legible and it is this system's shape.
   */
  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: 0;
    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: 0;
    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: 0;
    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: 0;
    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;
  }

  /* --- 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);
  }
  prism-video[ratio="1/1"]  { --_ratio: 1 / 1 }
  prism-video[ratio="4/3"]  { --_ratio: 4 / 3 }
  prism-video[ratio="3/4"]  { --_ratio: 3 / 4 }
  prism-video[ratio="16/9"] { --_ratio: 16 / 9 }
  prism-video[ratio="3/2"]  { --_ratio: 3 / 2 }
  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.** *Ruled by the owner: the design
   * system's mark is "Design", not "MyDesign".* 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.** *Ruled by
   * the owner 2026-09-20: angle 0, colour = the page's. First cut was 25%
   * at 5px and his eye called it too subtle; it stands at 75% and 8px.*
   * On plain ground it is page colour on page colour and invisible; where
   * content passes behind the transparent 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. */
  prism-emblem,
  prism-wordmark {
    filter: drop-shadow(0 0 8px color-mix(in srgb, var(--surface-default) 75%, transparent));
  }

  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.** *Found by the owner
   * when the wordmark moved to Newsreader: mark and word no longer lined
   * up.* 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 {
    transform: translateY(.13em);
    transform: translateY(calc((.95em - 1cap) / 2));
  }

  /* **NEWSREADER.** *Ruled by the owner 2026-09-28, after Instrument Serif
   * was tried.* 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;
  }
  prism-wordmark::before {
    content: attr(prefix);
    font-weight: 400;
  }

  /* --- 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;
  }

}

/* ===========================================================================
 * 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.** *Ruled by
     * the owner 2026-09-20: a colour between the background and the text
     * 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;
  }

  /* **A CARD IS NEVER A TILE.** *A thumb is `prism-thumb` — the owner ruled it
   * its own molecule, so a card that hid its own text to imitate one had no
   * reason to exist.* What stays is the padding, which 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, AND THAT
   * IS THE FIX.** It used to wrap everything in an `<a display:contents>`, so
   * a linked card was `prism-card > a > prism-stack` — and every rule here is
   * a DIRECT-CHILD selector, so a linked card silently lost its padding and
   * its text ran to the edge. *`display: contents` removes the box, never the
   * DOM position.*
   *
   * The link now 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-7);
    min-inline-size: 0;
  }
  prism-image-card > prism-stack:empty { display: none }

  :is(prism-card, prism-image-card) > prism-image {
    inline-size: 100%;
  }

  .prism-card-title { transition: color .2s ease }
  .prism-card-title a {
    color: inherit;
    text-decoration: none;
  }
  /* The stretched link: the anchor is in the title, the hit area is the card. */
  .prism-card-title a::after {
    content: "";
    position: absolute;
    inset: 0;
  }
  :is(prism-card, prism-image-card):hover .prism-card-title { color: var(--interactive) }
  .prism-card-title a:focus-visible::after {
    outline: 2px solid var(--interactive);
    outline-offset: -2px;
  }

  /* --- M9 · prism-nav · THE HEADER BAR ------------------------------------
   * **BRAND LEFT, MENU RIGHT, NOTHING ELSE.** *Ruled by the owner.*
   *
   * **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.
   *
   * ~~**8px WHERE THERE IS NO GUTTER.**~~ **8px INSIDE THE GUTTER, AT EVERY
   * WIDTH.** *Ruled by the owner 2026-09-24: the brand and the floats sat
   * exactly on the page edge and should not.* `--space-2` added to the gutter,
   * so on a phone, where the gutter is 0, it is the 8px it already was.
   *
   * It assembles prism-brand, prism-cluster, prism-stack, prism-button,
   * prism-icon and prism-link, and owns nothing.
   */
  prism-nav {
    display: block;
    position: relative;
    margin-inline: auto;
    padding-inline: calc(var(--gutter) + var(--space-2));
    max-inline-size: calc(var(--measure-app) + (var(--gutter) + var(--space-2)) * 2);
  }
  /* **AS THIN AS IT GOES — AND ONE HEIGHT IN BOTH SHAPES.** *Ruled by the
   * owner 2026-09-19: the top must not move at 900.* ~~Above 900px there is no
   * button and the brand sets it~~ — 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. *The top went from 4 to 8, ruled by
   * the owner 2026-09-24: too tight under the glass edge.* */
  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 }
  .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.
   * *Named by the owner as the bar's background, not the menu's.* */
  .prism-nav-items {
    display: none;
    position: absolute;
    inset-inline: 0;
    top: 100%;
    z-index: 1;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    background: 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 }
  .prism-nav-items prism-link > a {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-3) var(--space-4);
    border-bottom: none;
    /* **THE MENU SPEAKS NEWSREADER, AND SAYS SO.** *Ruled by the owner
     * 2026-09-20.* The items stated no face and inherited the page's, so the
     * suite's one bar wore monospace on one proof page and plain sans on
     * another — and a tenant setting a body font would skew every menu the
     * same way. The system's body voice, stated where no ground can dress
     * the bar. */
    font: 400 var(--type-body) / 1.65 var(--font-body);
  }

  /* **"YOU ARE HERE", TWO WAYS.** *Ruled by the owner.* Inline it is the item
   * with no underline, where every other item has one — the same distinction
   * the page menu already made. In the dropdown it is a chevron, because a
   * row has no underline to remove.
   *
   * **THE CHEVRON LEADS, IT DOES NOT TRAIL.** A trailing chevron on a menu row
   * means "there is more this way", and the owner has said levels come later.
   * Leading, it points at the row and cannot be read as a promise. It is
   * present on every row and VISIBLE on one, so the labels line up. */
  prism-nav prism-link > a > prism-icon { visibility: hidden }
  prism-nav prism-link[current] > a > prism-icon { visibility: visible }
  prism-nav prism-link[current] > a {
    border-bottom: none;
    color: var(--interactive);
  }

  /* **THE HAMBURGER CARRIES A BACKGROUND — THE PAGE'S OWN.** *Ruled by the
   * owner 2026-09-20, the day the header lost its background:* the bar is
   * transparent now, so the one control in it stands on an opaque square of
   * the mode's surface — the same move `prism-thumb`'s marker makes. Hover is
   * restated or the atoms-layer secondary hover would punch the square
   * transparent under the pointer; colour and border still react from there.
   * This layer (molecules) outranks the atom's rules, which is what lets a
   * class do it without fighting the variant's specificity. */
  .prism-nav-burger > button,
  .prism-nav-burger > button:hover { background: var(--surface-default) }

  @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);
      background: none;
      border-block-end: none;
      padding: 0;
    }
    /* **THE UNDERLINE COMES BACK HERE, AND ONLY HERE.** The panel rows drop it
     * — a row has no underline to remove, which is why the chevron exists. But
     * dropping it for BOTH shapes left nothing for the current item to be
     * distinguished from, so "no underline" marked every item equally. The
     * `[current]` rule above is more specific and still wins. */
    .prism-nav-items prism-link > a {
      display: inline;
      padding: 0;
      border-bottom: 1px solid currentColor;
    }
    /* No chevrons inline: the underline is what says where the reader is. */
    prism-nav prism-link > a > prism-icon { display: none }
  }

  /* --- M8 · prism-player -------------------------------------------------
   * **THE CONTROLS SIT OVER THE VIDEO AND COME AND GO.** *Ruled by the owner.*
   *
   * 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. *`prism-surface[tone="chromatic"]` set
   * that precedent: 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;
  }
  /* **THE SURFACE IS A PLAY BUTTON.** *Ruled by the owner: play/stop on click
   * on the video surface, controls showing or not.* 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 }

  prism-player > prism-cluster {
    position: absolute;
    inset-inline: 0;
    inset-block-end: 0;
    align-items: center;
    padding: var(--space-3);

    --surface-default: var(--_ink);
    --text-default: var(--_ink-text);
    --text-dimmed: var(--_ink-text-75);
    --border-default: color-mix(in srgb, var(--_ink-text) 40%, transparent);
    --interactive: var(--pink);
    color: var(--text-default);
    /* Top stop is fully transparent INK, not the keyword: mixing to
     * `transparent` keeps the same colour, so the gradient fades out rather
     * than fading through grey. */
    background: linear-gradient(to top,
      color-mix(in srgb, var(--_ink) 88%, transparent),
      color-mix(in srgb, var(--_ink) 0%, transparent));
    transition: opacity .2s ease, visibility .2s ease;
  }
  /* **`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 {
    display: none;
    flex: 0 0 80px;
  }
  @container (min-width: 480px) {
    .prism-player-volume { display: block }
  }
  prism-player prism-slider { flex: 1 1 auto; 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;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
  }

  /* --- prism-selection-bar ------------------------------------------------
   * *Ruled by the owner 2026-09-23.* 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 ----------------------------------------------------------
   * *Ruled by the owner 2026-09-23: anchored to its opener, and the panel's
   * plate* — 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) }

}
