/*
 * ═══════════════════════════════════════════════════════════════════════
 *  THE NOON THEME STYLESHEET (08-03-PLAN.md Task 2; D-86/D-88/D-93/D-97)
 * ═══════════════════════════════════════════════════════════════════════
 *
 * The INVARIANT half of D-86's split: token VALUES ship inline in the rendered
 * document (dynamic, tiny, uncacheable); the RULES that consume them ship as
 * this one static, perfectly cacheable asset. One database read either way.
 *
 * TWO KINDS OF ABSENCE, deliberately different (the fallbacks below invite
 * conflating them):
 *   1. NO THEME DOCUMENT → NO STYLING. This file is not referenced and no token
 *      block is emitted; the page is byte-for-byte pre-Phase-8. The `<link>` is
 *      CONDITIONAL — that is what makes TOK-04 literally, not approximately,
 *      true. An absent theme adds ZERO bytes. Do NOT "simplify" the conditional
 *      link away: the switch is the DOCUMENT's existence.
 *   2. A DOCUMENT SETTING ONLY SOME KEYS → FULLY STYLED, per-key fallbacks
 *      below supplying the rest.
 *
 * NO FRAMEWORK / PREPROCESSOR / CSS BUILD STEP — because the lowest deploy tier
 * is FTP-uploaded with no shell (TOK-02's no-build promise). Plain CSS, served
 * as-is.
 *
 * GATED AGAINST THE VOCABULARY IN BOTH DIRECTIONS by
 * `src/__tests__/theme-stylesheet-coverage.test.ts`: SET EQUALITY between
 * `TOKEN_KEYS` and the `var(--noon-…)` references here — a settable token
 * nothing consumes is a silent no-op, a reference naming no token is a dead
 * default, both are build failures. EVERY TOKEN REFERENCE CARRIES AN EXPLICIT
 * FALLBACK; the `--noon-stack-*` / sidebar internal constants below are the
 * exception (not tokens — defined unconditionally here, so a fallback would be
 * unreachable).
 *
 * ✓ D-97 CLOSED (BTF-02, DOC-03). This gate proves BYTES, not computed style;
 * as of Phase 16 the headless-browser suites `theme-computed-style.test.ts`
 * (rows 1/2/3/5: cascade applies, D-93 font chain resolves, per-block style,
 * unthemed absence) and `region-computed-style.test.ts` (row 4: region
 * wrappers) carry the visual claim by machine. Coexistence: the browser layer
 * ADDS a stratum; this gate's four byte/parse properties REMAIN authoritative
 * about bytes (served 200+CSS, parses, every ref names a real key in a LIVE
 * rule, every block selector matches an emitted `data-block`). D-93 is the
 * worked proof the closed gap could have hidden a live defect — a bare
 * font-stack KEY as a family name renders nothing while passing every text
 * assertion.
 *
 * NO EXTERNAL REQUEST OF ANY KIND — no `@import`, `url()`, font CDN or image.
 * This file is served on every themed page; an external ref would be a
 * third-party dependency in every request's render path. The gate asserts their
 * absence.
 */

/* ==================================================================== */
/*  D-93 — THE THREE FONT STACKS THE SERIALIZER'S EXPANSIONS RESOLVE TO  */
/* ==================================================================== */

/*
 * THE CONSUMING HALF OF D-93'S MECHANISM — the feature is DEAD without them.
 * `packages/plugin-theme/src/serialize.ts` maps a font enum key through a
 * FROZEN three-entry table to the exact literal `var(--noon-stack-sans)` (or
 * `-serif`/`-mono`); these declarations are what those literals resolve to,
 * carrying the commas/spaces/quotes the token grammar cannot express.
 *
 * >>> IF ABSENT, the font token sets a property resolving to NOTHING, the
 * >>> `var()` fallback does NOT fire (the property IS set), and the feature is
 * >>> silently dead while every text assertion stays green — so the coverage
 * >>> gate asserts all three are defined in a live rule.
 */

/*
 * EVERY RULE BELOW IS IN THE `noon` CASCADE LAYER (260730-csc, #25).
 *
 * Operator `customCss` is emitted UNLAYERED, and unlayered beats layered
 * UNCONDITIONALLY — independent of specificity AND of source order — so an
 * operator rule wins here even at STRICTLY LOWER specificity.
 *
 * WHY: #25 claimed operator CSS won because it was EMITTED LAST; that held only
 * against THIS sheet — the three component sheets hoist AFTER the operator's
 * block and silently beat it. Re-ordering `precedence` was REJECTED: it makes
 * the contract a property of emission order again, which is how it broke.
 *
 * ONE LAYER NAME ACROSS ALL FOUR NOON SHEETS, so their order AMONG THEMSELVES is
 * unchanged (one layer, ordinary source order inside it). Rules are NOT
 * re-indented: the wrapper is the ONLY diff.
 *
 * THE TOKEN BLOCK IS DELIBERATELY *NOT* LAYERED: only `:root` properties and
 * `@font-face`, i.e. operator DATA read through `var()` — so an operator
 * declaration of the same property still beats it the old way, by source order,
 * and font matching never meets a layer. And NEVER flag a declaration important
 * here: inside a layer that flag interacts INVERSELY, handing a Noon rule
 * authority over the operator.
 *
 * Full reasoning in `src/lib/custom-css.tsx`; PROVEN by
 * `src/__tests__/custom-css-cascade-authority.test.ts` via the CSSOM.
 */
@layer noon {
  :root {
    --noon-stack-sans:
      ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial,
      sans-serif;
    --noon-stack-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
    --noon-stack-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
    /*
   * SIDEBAR INTERNAL CONSTANTS (T-1, 260722-fbg) — the same idiom as the
   * three stack properties above: an internal constant an agent never sets
   * directly, existing only so the REGIONS rules below (and the coverage
   * gate's INTERNAL_CONSTANTS set) reference ONE declaration instead of the
   * literal `16rem`/`calc(var(--noon-spacing-unit,8px)*4)` repeated four
   * times each. `--noon-region-gutter` is deliberately the SAME expression
   * as the region gap below it — the coupling between "how much room the
   * sidebar's width reservation needs" and "how big the gap between it and
   * the content column is" is structural, not two numbers that happen to
   * agree today.
   */
    --noon-sidebar-width: 16rem;
    --noon-region-gutter: calc(var(--noon-spacing-unit, 8px) * 4);

    /*
   * NOTICE INTERNAL CONSTANTS (260729-notice) — the same idiom as the sidebar
   * constants above: an internal constant an agent never sets directly (it is
   * NOT one of the 14 frozen TOKEN_KEYS), existing only so the notice rules
   * below (and the coverage gate's INTERNAL_CONSTANTS set) reference ONE
   * declaration per hue. An operator's own custom CSS MAY override these —
   * same steering level as every other `--noon-*` internal constant.
   */
    --noon-notice-success: #1a7f37;
    --noon-notice-error: #cf222e;
    --noon-notice-warning: #9a6700;
    --noon-notice-info: #0969da;
  }

  /* ==================================================================== */
  /*  PAGE — TYPOGRAPHY, COLOUR AND RHYTHM FROM THE BODY DOWN             */
  /* ==================================================================== */

  :root {
    font-size: var(--noon-base-font-size, 17px);
    /* `light`, NOT `light dark`.
   *
   * `color-scheme` is a PROMISE to the browser about which schemes this page
   * actually handles, and it was promising one it does not keep: there is not a
   * single `prefers-color-scheme` rule in this app, and every colour comes from
   * a theme token holding one fixed value. Nothing here changes in dark mode.
   *
   * What `light dark` bought was a page painted LIGHT by its tokens while the
   * user agent painted the NATIVE CONTROLS dark — on a phone in dark mode the
   * poll's checkboxes rendered as solid black squares and its text input as a
   * black bar, on a cream page. Reported from a real device 2026-08-07; curl and
   * any light-mode browser show nothing wrong, which is why it survived.
   *
   * This is not a poll bug. It is every <input>, <select> and <textarea> on
   * every Noon site, FormBlock included, for every visitor whose device is in
   * dark mode. Declaring `light` makes the promise match the stylesheet.
   *
   * WHEN A REAL DARK THEME ARRIVES this goes back to `light dark`, and that
   * change belongs in the SAME commit as the `prefers-color-scheme` token
   * overrides that make it true. */
    color-scheme: light;
  }

  body {
    margin: 0;
    background: var(--noon-background, #fbfbfd);
    color: var(--noon-text, #16161a);
    font-family: var(--noon-body-font, var(--noon-stack-sans));
    /* A real reading measure rather than the full width of a monitor. The
   * horizontal padding is derived from the spacing unit so the page breathes
   * in proportion to the rest of the rhythm. */
    line-height: 1.65;
    -webkit-text-size-adjust: 100%;
  }

  main {
    max-width: var(--noon-measure, 68ch);
    /* As a flex item inside [data-region-layout], min-width defaults to `auto`,
   * so a wide, non-wrapping child (a <pre> code block, a long token) forces the
   * column past the frame and one page renders wider than a prose-only page.
   * `min-width: 0` lets the column shrink so the <pre>'s own overflow-x:auto
   * scrolls WITHIN a consistent-width column. */
    min-width: 0;
    /* ...and `flex: 1` lets it GROW to fill the column when it IS a flex item in
   * the region layout. Without this, main's default `flex: 0 1 auto` shrinks it
   * to its content width (a narrow form collapses to ~330px) and `max-width`
   * above becomes an unreachable ceiling — the exact "max-width does nothing"
   * symptom. On a region-less page main is a plain block and this is inert. */
    flex: 1 1 auto;
    margin: 0 auto;
    padding: calc(var(--noon-spacing-unit, 8px) * 6) calc(var(--noon-spacing-unit, 8px) * 3);
  }

  /*
 * THE TYPE SCALE IS DERIVED HERE, NOT STORED AS MORE TOKENS.
 *
 * `scale-ratio` is one number and the heading sizes are computed from it by
 * repeated multiplication — CSS has no exponentiation, so `r^3` is spelled as
 * three multiplications. A second, third and fourth size token would be four
 * values an agent could set into disagreement with each other; one ratio
 * cannot be internally inconsistent.
 */
  h1,
  h2,
  h3,
  h4,
  h5,
  h6 {
    font-family: var(--noon-heading-font, var(--noon-stack-sans));
    line-height: 1.15;
    letter-spacing: -0.015em;
    margin: calc(var(--noon-spacing-unit, 8px) * 4) 0 calc(var(--noon-spacing-unit, 8px) * 2);
    text-wrap: balance;
  }

  h1 {
    font-size: calc(
      1rem *
      var(--noon-scale-ratio, 1.25) *
      var(--noon-scale-ratio, 1.25) *
      var(--noon-scale-ratio, 1.25)
    );
  }

  h2 {
    font-size: calc(1rem * var(--noon-scale-ratio, 1.25) * var(--noon-scale-ratio, 1.25));
  }

  h3 {
    font-size: calc(1rem * var(--noon-scale-ratio, 1.25));
  }

  p,
  ul,
  ol {
    margin: 0 0 calc(var(--noon-spacing-unit, 8px) * 2);
  }

  a {
    color: var(--noon-accent, #4338ca);
    text-underline-offset: 0.2em;
    text-decoration-thickness: 0.06em;
  }

  a:hover {
    text-decoration-thickness: 0.12em;
  }

  hr {
    border: 0;
    border-top: 1px solid var(--noon-border, #e3e3ea);
    margin: calc(var(--noon-spacing-unit, 8px) * 5) 0;
  }

  code,
  pre,
  kbd {
    font-family: var(--noon-stack-mono);
    font-size: 0.9em;
  }

  pre {
    background: var(--noon-surface, #ffffff);
    border: 1px solid var(--noon-border, #e3e3ea);
    border-radius: var(--noon-radius, 10px);
    padding: calc(var(--noon-spacing-unit, 8px) * 2);
    overflow-x: auto;
  }

  table {
    width: 100%;
    border-collapse: collapse;
    margin: 0 0 calc(var(--noon-spacing-unit, 8px) * 3);
  }

  th,
  td {
    border-bottom: 1px solid var(--noon-border, #e3e3ea);
    padding: var(--noon-spacing-unit, 8px) 0;
    text-align: left;
  }

  th {
    font-family: var(--noon-heading-font, var(--noon-stack-sans));
    font-size: 0.85rem;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--noon-muted, #6b6b76);
  }

  /* ==================================================================== */
  /*  BLOCKS — ONE RULE SET PER `data-block`, THE ATTRIBUTE ALREADY IN     */
  /*  PRODUCTION HTML. NO COMPONENT SOURCE CHANGE ADDS ANY HOOK HERE.      */
  /* ==================================================================== */

  /*
 * The selector list is DERIVED from the schema package's frozen component
 * tuple by the coverage gate, never hand-listed there — the renderer
 * registry's own pattern. A fifth component added to the union in Phase 9
 * fails that gate until it is either styled here or given an explicitly
 * commented empty rule. An omission must be a statement, not a gap.
 */

  [data-block="HeroSection"] {
    padding: calc(var(--noon-spacing-unit, 8px) * 5) 0 calc(var(--noon-spacing-unit, 8px) * 4);
    border-bottom: 1px solid var(--noon-border, #e3e3ea);
    margin-bottom: calc(var(--noon-spacing-unit, 8px) * 5);
  }

  [data-block="HeroSection"] h1 {
    margin-top: 0;
  }

  /* The subhead is the one paragraph in the hero, and it reads as secondary. */
  [data-block="HeroSection"] p {
    color: var(--noon-muted, #6b6b76);
    font-size: calc(1rem * var(--noon-scale-ratio, 1.25));
    max-width: var(--noon-measure, 68ch);
  }

  /* The call to action is the one place `accent-contrast` earns its keep:
 * text ON the accent rather than text IN it. */
  [data-block="HeroSection"] a {
    display: inline-block;
    margin-top: calc(var(--noon-spacing-unit, 8px) * 2);
    padding: var(--noon-spacing-unit, 8px) calc(var(--noon-spacing-unit, 8px) * 2.5);
    background: var(--noon-accent, #4338ca);
    color: var(--noon-accent-contrast, #ffffff);
    border-radius: var(--noon-radius, 10px);
    text-decoration: none;
    font-weight: 600;
  }

  [data-block="RichText"] {
    margin-bottom: calc(var(--noon-spacing-unit, 8px) * 4);
  }

  [data-block="RichText"] blockquote {
    margin: calc(var(--noon-spacing-unit, 8px) * 3) 0;
    padding-left: calc(var(--noon-spacing-unit, 8px) * 2);
    border-left: 3px solid var(--noon-accent, #4338ca);
    color: var(--noon-muted, #6b6b76);
  }

  [data-block="ImageBlock"] {
    margin: calc(var(--noon-spacing-unit, 8px) * 4) 0;
  }

  [data-block="ImageBlock"] img {
    display: block;
    width: 100%;
    height: auto;
    border-radius: var(--noon-radius, 10px);
    border: 1px solid var(--noon-border, #e3e3ea);
    background: var(--noon-surface, #ffffff);
  }

  [data-block="ImageBlock"] figcaption {
    margin-top: var(--noon-spacing-unit, 8px);
    color: var(--noon-muted, #6b6b76);
    font-size: 0.9rem;
  }

  [data-block="ProductGrid"] {
    display: grid;
    gap: calc(var(--noon-spacing-unit, 8px) * 3);
    margin: calc(var(--noon-spacing-unit, 8px) * 4) 0;
  }

  [data-block="ProductGrid"] article {
    background: var(--noon-surface, #ffffff);
    border: 1px solid var(--noon-border, #e3e3ea);
    border-radius: var(--noon-radius, 10px);
    padding: calc(var(--noon-spacing-unit, 8px) * 2.5);
    display: flex;
    flex-direction: column;
    gap: var(--noon-spacing-unit, 8px);
  }

  [data-block="ProductGrid"] article h2 {
    margin: 0;
    font-size: calc(1rem * var(--noon-scale-ratio, 1.25));
  }

  [data-block="ProductGrid"] [data-product-price] {
    margin: 0;
    color: var(--noon-muted, #6b6b76);
    font-family: var(--noon-stack-mono);
  }

  [data-block="ProductGrid"] [data-checkout-link] {
    margin-top: auto;
    align-self: flex-start;
    padding: var(--noon-spacing-unit, 8px) calc(var(--noon-spacing-unit, 8px) * 2);
    background: var(--noon-accent, #4338ca);
    color: var(--noon-accent-contrast, #ffffff);
    border-radius: var(--noon-radius, 10px);
    text-decoration: none;
    font-weight: 600;
  }

  /* ==================================================================== */
  /*  CALLOUT — ADMONITION BLOCK, VARIANT-KEYED (16-02; AMD-01)            */
  /* ==================================================================== */

  /*
 * The Callout keys on `data-variant` exactly as ProductGrid keys on
 * `data-columns` — an EXISTING attribute idiom, not a new pattern. Every
 * colour comes from an EXISTING token (surface / border / text / muted /
 * accent), each with a fallback, so the coverage gate's set-equality and
 * fallback assertions stay green and NO new token key is introduced (that
 * would be its own vocabulary amendment — Open Question 2).
 *
 * >>> THE ACCENT IS RESERVED FOR note / tip. warning and danger get NO
 * >>> dedicated hue (the 14-key vocabulary has none): they lean on the neutral
 * >>> muted / text tokens for their left-border emphasis. The variant → token
 * >>> mapping below is pinned by `callout-computed-style.test.ts`, one computed
 * >>> colour per variant, so whatever mapping ships is machine-proven visible.
 */

  [data-block="Callout"] {
    margin: calc(var(--noon-spacing-unit, 8px) * 4) 0;
    padding: calc(var(--noon-spacing-unit, 8px) * 2) calc(var(--noon-spacing-unit, 8px) * 2.5);
    background: var(--noon-surface, #ffffff);
    border: 1px solid var(--noon-border, #e3e3ea);
    border-left: calc(var(--noon-spacing-unit, 8px) * 0.5) solid var(--noon-border, #e3e3ea);
    border-radius: var(--noon-radius, 10px);
    color: var(--noon-text, #16161a);
  }

  [data-block="Callout"] [data-callout-title] {
    margin: 0 0 var(--noon-spacing-unit, 8px);
    font-family: var(--noon-heading-font, var(--noon-stack-sans));
    font-weight: 600;
    color: var(--noon-muted, #6b6b76);
  }

  /* note / tip — the accent earns its keep as the emphasis hue. */
  [data-block="Callout"][data-variant="note"] {
    border-left-color: var(--noon-accent, #4338ca);
  }

  [data-block="Callout"][data-variant="tip"] {
    border-left-color: var(--noon-accent, #4338ca);
  }

  /* warning — muted neutral emphasis, no dedicated hue. */
  [data-block="Callout"][data-variant="warning"] {
    border-left-color: var(--noon-muted, #6b6b76);
  }

  /* danger — the strongest neutral (text), no dedicated hue. */
  [data-block="Callout"][data-variant="danger"] {
    border-left-color: var(--noon-text, #16161a);
  }

  /* ==================================================================== */
  /*  FAQ — NATIVE <details> DISCLOSURE (260724-faq; tracker #32)          */
  /* ==================================================================== */

  /*
 * The FAQ keys on the SEMANTIC attributes the renderer already emits
 * (`data-block="FAQ"`, `data-faq-item`, `data-faq-question`, `data-faq-answer`)
 * — no new class contract, no inline style, no client JS. Every colour comes
 * from an EXISTING token (surface / border / text / muted / accent / background)
 * with a fallback, so the coverage gate's set-equality and fallback assertions
 * stay green and NO new token key is introduced. These are EXACTLY the six
 * locked selectors (Ruling 2); the open-state and focus-visible colours are
 * pinned by `faq-computed-style.test.ts` under a real layout engine.
 */

  [data-block="FAQ"] {
    margin: calc(var(--noon-spacing-unit, 8px) * 4) 0;
    background: var(--noon-surface, #ffffff);
    border: 1px solid var(--noon-border, #e3e3ea);
    border-radius: var(--noon-radius, 10px);
  }

  [data-faq-item] {
    border-bottom: 1px solid var(--noon-border, #e3e3ea);
  }

  [data-faq-question] {
    cursor: pointer;
    padding: calc(var(--noon-spacing-unit, 8px) * 2) calc(var(--noon-spacing-unit, 8px) * 2.5);
    font-family: var(--noon-heading-font, var(--noon-stack-sans));
    font-weight: 600;
    color: var(--noon-text, #16161a);
  }

  /* Keyboard focus gets a visible accent outline — a11y, not decoration. */
  [data-faq-question]:focus-visible {
    outline: 2px solid var(--noon-accent, #4338ca);
    outline-offset: 2px;
  }

  [data-faq-answer] {
    padding: 0 calc(var(--noon-spacing-unit, 8px) * 2.5) calc(var(--noon-spacing-unit, 8px) * 2);
    color: var(--noon-muted, #6b6b76);
  }

  /* The open item lifts onto the page background for a subtle disclosure cue. */
  [data-faq-item][open] {
    background: var(--noon-background, #fbfbfd);
  }

  /* ==================================================================== */
  /*  POSTLIST — INDEXED SERVER FEED OF CARDS (20-02; ART-02/ART-03)       */
  /* ==================================================================== */

  /*
 * PostList keys on the semantic attributes the renderer emits (`data-block`
 * plus the per-card `data-post-card`/`data-post-link`/`data-post-date`). The
 * grid geometry (`grid-template-columns` per `data-columns`) is owned by the
 * component's own `/noon-post-list.css`; this file supplies only token-driven
 * decoration, so it adds NO `data-columns` selector and leaves the D-88 column
 * contract untouched. Every colour is an EXISTING token with a fallback. The
 * empty feed still emits the `data-block` container (Ruling 2) — what (d) needs.
 */

  [data-block="PostList"] {
    margin: calc(var(--noon-spacing-unit, 8px) * 4) 0;
    gap: calc(var(--noon-spacing-unit, 8px) * 3);
  }

  [data-block="PostList"] [data-post-card] {
    background: var(--noon-surface, #ffffff);
    border: 1px solid var(--noon-border, #e3e3ea);
    border-radius: var(--noon-radius, 10px);
    padding: calc(var(--noon-spacing-unit, 8px) * 2.5);
  }

  [data-block="PostList"] [data-post-link] {
    color: var(--noon-accent, #4338ca);
    font-family: var(--noon-heading-font, var(--noon-stack-sans));
    font-weight: 600;
  }

  [data-block="PostList"] [data-post-date] {
    display: block;
    margin-top: var(--noon-spacing-unit, 8px);
    color: var(--noon-muted, #6b6b76);
    font-size: 0.9rem;
  }

  /* ==================================================================== */
  /*  SOCIALLINKS — BOUNDED ROW OF PROFILE ANCHORS (20-02; ART-04)         */
  /* ==================================================================== */

  /*
 * SocialLinks keys on `data-block="SocialLinks"` and the anchor it emits. Icon
 * dimensions (`data-size`) and the label layout are owned by the component's
 * own `/noon-social-links.css`; this file supplies only token-driven row
 * spacing and interaction colour — every colour an EXISTING token with a
 * fallback. An explicit non-empty `links` list renders the container without
 * reading site settings, which is what (d) matches.
 */

  [data-block="SocialLinks"] {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: calc(var(--noon-spacing-unit, 8px) * 2);
    margin: calc(var(--noon-spacing-unit, 8px) * 3) 0;
  }

  [data-block="SocialLinks"] a {
    display: inline-flex;
    align-items: center;
    gap: var(--noon-spacing-unit, 8px);
    color: var(--noon-muted, #6b6b76);
    text-decoration: none;
  }

  [data-block="SocialLinks"] a:hover {
    color: var(--noon-accent, #4338ca);
  }

  /* Keyboard focus gets a visible accent outline — a11y, not decoration. */
  [data-block="SocialLinks"] a:focus-visible {
    outline: 2px solid var(--noon-accent, #4338ca);
    outline-offset: 2px;
  }

  /* ==================================================================== */
  /*  FORMBLOCK — ANONYMOUS-WRITE CONTACT FORM (260728-fsc; tracker #29)   */
  /* ==================================================================== */

  /*
 * FormBlock keys on `data-block="FormBlock"` (emitted on BOTH the active form
 * and the inert fail-closed shell) and the semantic hooks the renderer already
 * emits: the per-field `<label data-noon-field>` wrapping a `<span>` caption +
 * native control, the submit `<button>`, and the `data-noon-form-status` live
 * region. Every colour comes from an EXISTING token (surface / border / text /
 * muted / accent / accent-contrast) with a fallback, and every spacing value
 * from `--noon-spacing-unit` / `--noon-radius` / `--noon-measure` — so the
 * coverage gate's set-equality and fallback assertions stay green and NO new
 * token key is introduced. The honeypot input is deliberately UNSTYLED here: it
 * is a bare input OUTSIDE any `[data-noon-field]`, so the field rules below
 * never touch it and its off-screen inline style stands.
 */

  [data-block="FormBlock"] {
    display: flex;
    flex-direction: column;
    gap: calc(var(--noon-spacing-unit, 8px) * 2);
    /* Fill the content column (governed by <main>'s own max-width) rather than
   * imposing a second, narrower reading-measure cap on top of it. */
    width: 100%;
    margin: calc(var(--noon-spacing-unit, 8px) * 4) 0;
  }

  [data-block="FormBlock"] [data-noon-field] {
    display: flex;
    flex-direction: column;
    gap: var(--noon-spacing-unit, 8px);
  }

  /* 260729-fw: responsive field-width columns. Fields that opt into `width` are
 * wrapped in `[data-noon-fields]` (a flex-wrap row); each bucket's flex-basis is
 * f·100% - (1-f)·gap, so any row summing to 100% (50+50, 33×3, 25+75, 20×5,
 * 66+33) tiles exactly. Buckets are gate-bound to FORM_FIELD_WIDTHS; gap derives
 * from --noon-spacing-unit (no new token); ≤40rem collapses to full width. */
  [data-block="FormBlock"] [data-noon-fields] {
    display: flex;
    flex-wrap: wrap;
    gap: calc(var(--noon-spacing-unit, 8px) * 2);
  }

  [data-block="FormBlock"] [data-noon-fields] [data-noon-field] {
    flex-grow: 0;
    min-width: 0;
  }

  [data-block="FormBlock"] [data-noon-field-width="20"] {
    flex-basis: calc(20% - var(--noon-spacing-unit, 8px) * 1.6);
  }

  [data-block="FormBlock"] [data-noon-field-width="25"] {
    flex-basis: calc(25% - var(--noon-spacing-unit, 8px) * 1.5);
  }

  [data-block="FormBlock"] [data-noon-field-width="33"] {
    flex-basis: calc(100% / 3 - var(--noon-spacing-unit, 8px) * 1.333);
  }

  [data-block="FormBlock"] [data-noon-field-width="50"] {
    flex-basis: calc(50% - var(--noon-spacing-unit, 8px));
  }

  [data-block="FormBlock"] [data-noon-field-width="66"] {
    flex-basis: calc(200% / 3 - var(--noon-spacing-unit, 8px) * 0.667);
  }

  [data-block="FormBlock"] [data-noon-field-width="75"] {
    flex-basis: calc(75% - var(--noon-spacing-unit, 8px) * 0.5);
  }

  [data-block="FormBlock"] [data-noon-field-width="100"] {
    flex-basis: 100%;
  }

  @media (max-width: 40rem) {
    [data-block="FormBlock"] [data-noon-fields] [data-noon-field] {
      flex-basis: 100%;
    }
  }

  [data-block="FormBlock"] [data-noon-field] > span {
    font-family: var(--noon-heading-font, var(--noon-stack-sans));
    font-weight: 600;
    color: var(--noon-text, #16161a);
  }

  [data-block="FormBlock"] [data-noon-field] input:not([type="checkbox"]),
  [data-block="FormBlock"] [data-noon-field] textarea,
  [data-block="FormBlock"] [data-noon-field] select {
    width: 100%;
    /* Include padding + border in the 100% so the control never overflows the
   * form to the right (there is no global box-sizing reset in this sheet). */
    box-sizing: border-box;
    padding: var(--noon-spacing-unit, 8px) calc(var(--noon-spacing-unit, 8px) * 1.5);
    font: inherit;
    color: var(--noon-text, #16161a);
    background: var(--noon-surface, #ffffff);
    border: 1px solid var(--noon-border, #e3e3ea);
    border-radius: var(--noon-radius, 10px);
  }

  [data-block="FormBlock"] [data-noon-field] textarea {
    min-height: calc(var(--noon-spacing-unit, 8px) * 12);
    resize: vertical;
  }

  /* A single-line checkbox row: the native box sits beside its caption rather
 * than stacked above it.
 *
 * THE RULE BELOW USED TO BE THIS COMMENT PLUS `align-self: flex-start` ALONE,
 * AND IT NEVER DID WHAT THE COMMENT SAYS. The field wrapper is
 * `flex-direction: column`, so `align-self` acts on the CROSS axis: it stopped
 * the box stretching to full width and nothing more. The caption stayed
 * stacked underneath, in the bold weight every field caption gets, so a
 * checkbox read as a heading with a stray box above it.
 *
 * It survived because no form on this site had a checkbox field until a
 * consent box landed on /academy — `/contact` is email, name and message. A
 * comment describing behaviour the rule never produced is worse than no
 * comment: it tells the next reader the case is handled.
 *
 * The fix is on the WRAPPER, because direction is the parent's property. The
 * `:has()` is the same idiom the operator CSS on this site already uses. */
  [data-block="FormBlock"] [data-noon-field]:has(> input[type="checkbox"]) {
    flex-direction: row;
    align-items: center;
    gap: calc(var(--noon-spacing-unit, 8px) * 1.25);
  }

  /* A checkbox's caption is a LABEL BESIDE A CONTROL, not a field heading, so it
 * drops the caption weight the stacked fields use. */
  [data-block="FormBlock"] [data-noon-field]:has(> input[type="checkbox"]) > span {
    font-family: inherit;
    font-weight: 400;
  }

  [data-block="FormBlock"] [data-noon-field] input[type="checkbox"] {
    /* Sized to match the Poll block's boxes. The native control is fixed at
   * roughly 13px and looks undersized beside 17px body text — the same
   * browser-default correction `noon-controls.css` exists for, applied here
   * because the two blocks are still styled independently. */
    width: 1.1em;
    height: 1.1em;
    flex: none;
    margin: 0;
    accent-color: var(--noon-accent, #4338ca);
  }

  /* Keyboard focus gets a visible accent outline — a11y, not decoration. */
  [data-block="FormBlock"] [data-noon-field] input:focus-visible,
  [data-block="FormBlock"] [data-noon-field] textarea:focus-visible,
  [data-block="FormBlock"] [data-noon-field] select:focus-visible {
    outline: 2px solid var(--noon-accent, #4338ca);
    outline-offset: 2px;
  }

  /* Matched to the Poll block's submit, which is the button a visitor sees first
 * on /academy — same weight, same border, same padding. They shared a radius
 * token already and diverged on everything else, because only `font: inherit`
 * and `accent-color` are actually shared between blocks. Making them agree by
 * hand is the narrow fix; the broad one is hoisting button appearance into
 * `noon-controls.css` so the NEXT block cannot drift, and that is a deliberate
 * refactor with a cascade order to get right, not a change to slip in here. */
  [data-block="FormBlock"] button {
    align-self: flex-start;
    padding: calc(var(--noon-spacing-unit, 8px) * 1) calc(var(--noon-spacing-unit, 8px) * 2);
    color: var(--noon-accent-contrast, #ffffff);
    background: var(--noon-accent, #4338ca);
    border: 1px solid var(--noon-border, #ddd);
    border-radius: var(--noon-radius, 10px);
    cursor: pointer;
  }

  /* The inert shell's disabled submit reads as unavailable, not clickable. */
  [data-block="FormBlock"] button:disabled {
    background: var(--noon-muted, #6b6b76);
    cursor: not-allowed;
  }

  [data-block="FormBlock"] button:focus-visible {
    outline: 2px solid var(--noon-accent, #4338ca);
    outline-offset: 2px;
  }

  /* The status region is a SCREEN-READER-ONLY live region: `role="status"
 * aria-live="polite"` announces submit progress + the result to assistive tech,
 * but the visible message is the authored `[data-noon-form-success]` /
 * `[data-noon-form-error]` paragraph — so it must NOT render a second, visible
 * copy of the same text. This is the standard visually-hidden idiom (kept in the
 * layout and readable by AT; removed from the visual flow). Pure geometry, no
 * tokens — the coverage gate's token set is unchanged. */
  [data-block="FormBlock"] [data-noon-form-status] {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
  }

  /* ==================================================================== */
  /*  NOTICE — SHARED data-noon-notice STEERING CONTRACT (260729-notice)   */
  /* ==================================================================== */

  /*
 * Notice keys on `data-noon-notice` + `data-noon-notice-kind` exactly as
 * Callout keys on `data-variant` above — an EXISTING attribute idiom, not a
 * new pattern. The base rule uses EXISTING tokens (surface / border / text /
 * radius / spacing-unit), each with a fallback, so the coverage gate's
 * set-equality assertion stays green. The four per-kind hues are the
 * `--noon-notice-*` INTERNAL CONSTANTS declared in the live top-level :root
 * above — NOT frozen theme tokens (the sidebar-constant idiom) — so
 * TOKEN_KEYS stays at exactly fourteen.
 */

  [data-noon-notice] {
    margin: calc(var(--noon-spacing-unit, 8px) * 2) 0;
    padding: calc(var(--noon-spacing-unit, 8px) * 2) calc(var(--noon-spacing-unit, 8px) * 2.5);
    background: var(--noon-surface, #ffffff);
    color: var(--noon-text, #16161a);
    border: 1px solid var(--noon-border, #e3e3ea);
    border-left: calc(var(--noon-spacing-unit, 8px) * 0.5) solid var(--noon-border, #e3e3ea);
    border-radius: var(--noon-radius, 10px);
  }

  [data-noon-notice][data-noon-notice-kind="success"] {
    border-left-color: var(--noon-notice-success, #1a7f37);
  }

  [data-noon-notice][data-noon-notice-kind="error"] {
    border-left-color: var(--noon-notice-error, #cf222e);
  }

  [data-noon-notice][data-noon-notice-kind="warning"] {
    border-left-color: var(--noon-notice-warning, #9a6700);
  }

  [data-noon-notice][data-noon-notice-kind="info"] {
    border-left-color: var(--noon-notice-info, #0969da);
  }

  /* ==================================================================== */
  /*  REGIONS — HEADER, FOOTER AND SIDEBAR SLOTS (09-04; REG-01a, REG-03)  */
  /* ==================================================================== */

  /*
 * These rules key on `data-region`, NOT `data-block`, so they are OUTSIDE the
 * coverage gate's block-selector contract by construction — a region wrapper is
 * emitted by the render seam, not by a component in the frozen tuple. They use
 * only EXISTING token keys, each with a fallback, so the token set-equality and
 * fallback assertions in `theme-stylesheet-coverage.test.ts` stay green.
 *
 * >>> AN ABSENT REGION EMITS NO WRAPPER AT ALL (the seam's zero-bytes rule), so
 * >>> none of these selectors matches anything on a region-less deployment —
 * >>> which is why the no-region baseline is byte-identical. They style the
 * >>> slots only once an agent publishes a template of that type.
 */

  /* THE HEADER IS A FULL-BLEED BAR, NOT A CENTRED CARD (260730-rgm). It carried
 * the shared content shell on the element ITSELF, so a header with presentation
 * {background, shadow, sticky} painted as a centred card with visible shadow
 * edges instead of a bar across the viewport. Paint, shadow and sticky pin now
 * span the full width: no max-width, no horizontal padding, and SYMMETRIC 2u
 * vertical padding (a tight nav bar) in place of the old asymmetric 3u/0. */
  [data-region="header"] {
    padding: calc(var(--noon-spacing-unit, 8px) * 2) 0;
  }

  /* THE FOOTER IS THE HEADER'S TWIN (260730-csc): a full-bleed bar whose RULE
 * spans the viewport while its CONTENT stays in the shell — "full-width
 * regarding the border, but not the content", staged in live customCss as a
 * stopgap and landing here where it belongs. Hence no max-width, no auto side
 * margins, vertical-only padding; `border-top` and `color` are KEPT exactly as
 * they were, because the full-width rule IS the request. */
  [data-region="footer"] {
    margin: calc(var(--noon-spacing-unit, 8px) * 6) 0 0;
    padding: calc(var(--noon-spacing-unit, 8px) * 4) 0;
    border-top: 1px solid var(--noon-border, #e3e3ea);
    color: var(--noon-muted, #6b6b76);
  }

  /* ...and BOTH bars' shells move INWARD onto the blocks wrapper `registry.tsx`
 * already emits inside every region — CSS only, no new element. ONE MERGED
 * selector list, not two rules that happen to agree today, so the header's
 * content column and the footer's are the same box BY CONSTRUCTION (260730-csc). */
  [data-region="header"] > [data-noon-blocks],
  [data-region="footer"] > [data-noon-blocks] {
    max-width: calc(
      var(--noon-sidebar-width) +
      var(--noon-measure, 68ch) +
      var(--noon-region-gutter)
    );
    margin: 0 auto;
    padding: 0 calc(var(--noon-spacing-unit, 8px) * 3);
  }

  /* THE CHROME BARS OWN THEIR VERTICAL RHYTHM (260730-rgm). A block's
 * `margin-bottom` is ARTICLE-FLOW rhythm: right between blocks in a column,
 * wrong as trailing space in a horizontal bar, whose padding IS its rhythm —
 * and since the #28 `flow-root` fix it is CONTAINED in the paint, reading as a
 * dead band under the nav. DELIBERATELY NOT the sidebar: a vertical flow column
 * where block rhythm still applies, and whose contained trailing margin keeps
 * the flow-root containment proof non-vacuous. `*:last-child`, never a selector
 * naming a component, so the coverage gate's selector contract is untouched.
 *
 * THE `display` DECLARATION WAS REMOVED (260730-csc): this selector is (0,3,0)
 * and every block shipping its OWN layout does so at (0,1,0) — SocialLinks and
 * FormBlock `flex`, ProductGrid `grid` — so a chrome bar ending in any of them
 * had its layout silently destroyed, and a social row is THE canonical way a
 * footer ends. It had already forced the operator to write a doubled
 * `[data-block="RichText"][data-block]` selector to get a header nav back. Never
 * reintroduce `display`, `overflow` (same BFC, but it clips the sticky header's
 * box-shadow), `contain`, or an important flag. */
  [data-region="header"] > [data-noon-blocks] > *:last-child,
  [data-region="footer"] > [data-noon-blocks] > *:last-child {
    margin-bottom: 0;
  }

  /* …and one level deeper, because CONTAINMENT DOES NOT DELETE SPACE (260730-csc).
 * A markdown block's own last descendant (RichText's `<h2>`/`<p>`) carries the
 * theme's trailing margin, which collapses out through the zeroed block box
 * above. `flow-root` was reached for here, and MEASUREMENT showed it merely
 * RELOCATED that margin inside the block's border box — the bar stayed 16px too
 * tall while a gap measured at that border box read tight. Zeroing the
 * descendant's own margin REMOVES it. At (0,4,0) this outranks `h1..h6` and
 * `p, ul, ol` (0,0,1), and it is scoped to the LAST block only, so rhythm
 * BETWEEN blocks in the same bar survives. */
  [data-region="header"] > [data-noon-blocks] > *:last-child > :last-child,
  [data-region="footer"] > [data-noon-blocks] > *:last-child > :last-child {
    margin-bottom: 0;
  }

  /* The sidebar shares this flow wrapper with the page's own <main>. The wrapper
 * is emitted ONLY when a sidebar template is published, so its own max-width
 * governs the pair; the nested <main>'s auto margins still center the content
 * column within the remaining space. */
  [data-region-layout] {
    display: flex;
    gap: var(--noon-region-gutter);
    align-items: flex-start;
    max-width: calc(
      var(--noon-sidebar-width) +
      var(--noon-measure, 68ch) +
      var(--noon-region-gutter)
    );
    margin: 0 auto;
    /* The SAME 3u inset the header column and footer carry, so the sidebar starts
   * where their content starts (max-width sizes the CONTENT box, so the shell
   * width is unchanged). */
    padding: 0 calc(var(--noon-spacing-unit, 8px) * 3);
  }

  [data-region="sidebar"] {
    flex: 0 0 auto;
    width: var(--noon-sidebar-width);
    /* border-box keeps the padding and the 1px rule INSIDE the 16rem reservation,
   * so the column width and the gutter to <main> are unchanged (no global
   * box-sizing reset here — the FormBlock controls set their own for the same
   * reason). 3u RIGHT so text clears `border-right`; 0 LEFT so the content
   * starts where the header/footer content column starts. */
    box-sizing: border-box;
    padding: calc(var(--noon-spacing-unit, 8px) * 6) calc(var(--noon-spacing-unit, 8px) * 3) 0 0;
    color: var(--noon-muted, #6b6b76);
    border-right: 1px solid var(--noon-border, #e3e3ea);
  }

  /* On a narrow viewport the sidebar stacks above the content rather than
 * squeezing it — the same device-reality override the product grid uses. */
  @media (max-width: 40rem) {
    [data-region-layout] {
      flex-direction: column;
    }

    [data-region="sidebar"] {
      width: auto;
      border-right: 0;
      /* Nothing left to clear once the rule becomes a border-BOTTOM. */
      padding-right: 0;
      border-bottom: 1px solid var(--noon-border, #e3e3ea);
    }
  }

  /* ==================================================================== */
  /*  D-88 — THE FOUR COLUMN RULES THAT REPLACE THE DELETED INLINE STYLE   */
  /* ==================================================================== */

  /*
 * `apps/web/src/components/product-grid.tsx` USED to carry an inline
 * `style={{ gridTemplateColumns: repeat(<columns>, 1fr) }}`, now DELETED; these
 * four rules are its exact functional replacement.
 *
 * >>> EXHAUSTIVE BY CONSTRUCTION: `packages/schema` bounds `columns` to
 * >>> `z.int().min(1).max(4)`, so four rules cover the whole finite space, and
 * >>> `data-columns` is emitted UNCONDITIONALLY (verified in rendered HTML).
 * >>> The coverage gate asserts this set against the SCHEMA'S OWN MAX, not the
 * >>> literal four — widening the bound without adding rules fails the gate
 * >>> instead of silently dropping columns. Only a prop was removed, nothing
 * >>> added, so the phase's no-component-change anti-goal holds.
 */

  [data-block="ProductGrid"][data-columns="1"] {
    grid-template-columns: repeat(1, 1fr);
  }

  [data-block="ProductGrid"][data-columns="2"] {
    grid-template-columns: repeat(2, 1fr);
  }

  [data-block="ProductGrid"][data-columns="3"] {
    grid-template-columns: repeat(3, 1fr);
  }

  [data-block="ProductGrid"][data-columns="4"] {
    grid-template-columns: repeat(4, 1fr);
  }

  /* A narrow viewport collapses every column count to one. The static rules
 * above are the authored intent; this is the device reality. */
  @media (max-width: 40rem) {
    [data-block="ProductGrid"] {
      grid-template-columns: 1fr;
    }
  }

  /* FLOATING DISPLAY-RULE ISLAND (10-05; REG-01b/REG-04, D-127) — CUT from v1.1
 * (D-131). Its CSS, client script, island component and column were removed
 * together; REG-01b/REG-04 return in v1.2 with browser-test infra. */

  /* ==================================================================== */
  /*  POLL and COUNTDOWN — the two blocks with COMPONENT-OWNED sheets        */
  /*  (260806-vote, 260807-countdown)                                      */
  /* ==================================================================== */
  /* Each keys on its `data-block` value. Their INTERNALS (bars, ballot
 * controls, the digit group) ride their own static sheets, `noon-poll.css`
 * and `noon-countdown.css`, on the post-list precedent. What the THEME owns
 * is the block's place in the page rhythm, and every block in the frozen
 * vocabulary gets that rule HERE: `theme-stylesheet-coverage.test.ts` (d)
 * asserts a selector for every rendered `data-block` value, and these two
 * were the first blocks to arrive without one (measured 2026-09-05). */
  [data-block="Poll"] {
    margin: calc(var(--noon-spacing-unit, 8px) * 4) 0;
  }

  [data-block="Countdown"] {
    margin: calc(var(--noon-spacing-unit, 8px) * 4) 0;
  }

  /* ── close of @layer noon (260730-csc) ── */
}
