/* ============================================================
   Component styles — systems/blog.
   ============================================================

   LINKED, NOT INLINED. `_page.liquid` links this file and holds no component
   rules of its own — the conclusion #69 reached in `base` after inlining
   copied 66,883 identical characters into every rendered design and left
   every committed page stale whenever a component changed. Adopted here from
   the start rather than rediscovered.

   THIS FILE IS THE SOURCE. Edit it directly.

   EVERY VALUE IS A SEMANTIC TOKEN. Not a primitive — `--space-6` where
   `--space-card` belongs passes `checks/literals.py`, because a primitive is
   still a token, and review of #49 found 50 such uses against 5 semantic ones
   in `base` after exactly that omission. `systems/blog/README.md` states the
   rule for this system: if no role fits, the missing role is a bug in
   `foundation/semantic/`, not a reason to reach one tier down. That happened
   once while writing this file — there was no `--focus-ring-width` — and the
   role was added rather than the literal.
   ============================================================ */

/* ---- box-sizing, once ------------------------------------------------
   `base` learned this the expensive way: the default `content-box` makes
   `width: 100%` plus side padding render WIDER than its container, which put
   a 263px sidebar inside a 256px nav. A reading page is mostly one column
   with padding, so it is the same trap with more surface area. */
*,
*::before,
*::after {
  box-sizing: border-box;
}

/* ---- focus ----------------------------------------------------------
   ONE RULE FOR EVERY FOCUSABLE THING, grouped as `base` groups its own after
   #71: review sees one line joining a known list rather than a fresh copy it
   has to recognise as a copy. Grouping does not move the cascade — each
   selector keeps the specificity it had alone.

   `.force-focus` rides along because `checks/surfaces.mjs` cannot fire
   `:focus-visible` in a headless render, so a ring with no forced twin is
   never measured — and an unmeasured rule cannot fail. The `create-ui` skill
   states this as a rule; it is honoured here rather than discovered later. */
.prose a:focus-visible,
.post-card-title a:focus-visible,
/* CHROME IS FOCUSABLE TOO. Added in #90 with the site header and footer, and
   `checks/keyboard.mjs` caught the omission the same run -- two footer links
   fell back to Chrome's default ring, which it reports because a UA ring is
   not measured against the surface it lands on. A grouped rule means adding a
   component is one line joining a known list, which is the shape `base`
   settled on in #71. */
.site-link:focus-visible,
.pill:focus-visible,
.prose .sc-lp-wrap a:focus-visible,
.site-footer-link:focus-visible,
.site-brand:focus-visible,
/* `.force-focus` IS NOT ELEMENT-BOUND. It is written on a non-focusable
   `<span>` specimen -- see `_post-card-body.liquid`, and `base`'s
   `button.liquid` for why the specimen must not itself be focusable -- so a
   selector requiring an `<a>` would style nothing and the ring would go on
   rendering nowhere. Which is the bug this class exists to prevent. */
.force-focus {
  outline: var(--focus-ring-width) solid var(--color-focus);
  outline-offset: var(--focus-ring-offset);
}

/* ---- page ground ----------------------------------------------------
   The body carries the reading surface's own background and body colour, so a
   component that sets neither still renders on the right ground. */
body {
  margin: 0;
  /* NO PADDING HERE. THE CHROME REACHES THE VIEWPORT EDGE.

     #89 put `padding-block` and `padding-inline` on this element for a real
     reason -- `.prose` measured `left=0`, with the first character against the
     glass, and no check asks whether text touches it. That was correct when it
     was written, because this system had no site chrome: the body WAS the page.

     #90 then added `site-header` and `site-footer`, full-width bands that carry
     their own `--space-chrome-x`, and this rule was not revisited. The result
     was chrome floating in a 24px gutter with a 56px white strip above it --
     header and footer measured `l:24 r:1401` in a 1425px viewport, anchored to
     nothing.

     Removing it costs the content column NOTHING, which is what makes this the
     right level to fix: `.content-main` already declares the identical
     `padding-block: var(--space-section-tight)` and
     `padding-inline: var(--space-chrome-x)`. The body rule had become a
     duplicate that only the chrome could feel.

     Found by BLOG-5.6 looking at the live blog, not by a check. Every check was
     green: 320px reflow passes either way, because an inset page does not
     scroll sideways -- it just does not reach the edge. */
  background: var(--color-background);
  color: var(--color-body);
  font-family: var(--text-family-body);
}

/* ============================================================
   prose — the article body
   ============================================================

   STYLED BY ELEMENT SELECTOR, under one wrapper class. Every other component
   in this repository renders a fixed structure it controls; this one styles
   markup it has never seen, because the body comes from WordPress.

   MEASURED, NOT ASSUMED. The live AI-drafted article
   (`/wp-json/wp/v2/posts/54`) is:

       {'h2': 6, 'p': 18, 'h3': 8, 'div': 1, 'a': 2}
       classes present: ['sc-lp-wrap']   <- the CTA block only

   Not one prose element carries a class. A component expecting classes would
   style nothing at all.

   THE TAG SET IS OPEN. `blog_draft_generator.py` constrains the model with a
   DENYLIST of six tags — html, head, body, script, style, iframe — so any
   other tag is legitimate output. The rules below are therefore a FLOOR, and
   the inherited defaults underneath them are the contract for everything
   else: `.prose` sets family, size, leading and colour on the wrapper, so an
   unstyled `<dl>` or `<code>` inherits a readable paragraph rather than a
   browser default at 16px Times. That is why the wrapper carries type at all
   rather than each element declaring its own. */
.prose {
  max-width: var(--text-measure);
  font-family: var(--text-family-body);
  font-size: var(--text-article-body);
  line-height: var(--text-article-body-leading);
  font-weight: var(--text-article-body-weight);
  color: var(--color-body);
  /* A URL in running text is the usual cause of horizontal scroll at 320px,
     and WCAG 1.4.10 is a hard floor here rather than a preference. `anywhere`
     rather than `break-word`: the latter does not shrink the intrinsic
     min-content width, so a long token still forces the column wider inside a
     flex or grid parent. */
  overflow-wrap: anywhere;
}

/* THE FIRST CHILD HAS NO TOP MARGIN. Real articles open on an `h2` — measured
   above, six of them, the first at the top — and a heading's `margin-block-
   start` would otherwise push the body away from the article header by
   `--space-before-heading`, opening a gap nobody chose. */
.prose > :first-child {
  margin-block-start: 0;
}

.prose p {
  margin-block: 0 var(--space-paragraph);
}

/* THE OPENING PARAGRAPH is the one place `--text-article-lead` is licensed.
   THE WEIGHT IS NOT OPTIONAL and is why the token exists: 22px REGULAR
   qualifies as neither of WCAG's large-text bars (>=24px regular, or
   >=18.66px semibold), so a lead in `--color-large-text-subtle` at the
   default weight measures 3.48:1 and fails both AA and the exception it
   appears to claim. #92 put the constraint in the colour's name and review
   found the name alone insufficient. */
.prose > p.prose-lead {
  font-size: var(--text-article-lead);
  line-height: var(--text-article-lead-leading);
  font-weight: var(--text-article-lead-weight);
  /* `--color-body-strong`, NOT `--color-large-text-subtle`.
     The subtle token was the first choice and rendering it settled the
     question: at #7a8aa8 the lead is LIGHTER than the body's #4a5878, so the
     opening paragraph -- the sentence carrying the most weight on the page --
     receded behind the text under it. Larger and fainter reads as a caption,
     not as a standfirst.

     The subtle token remains legitimate where a thing genuinely is secondary;
     `figcaption` uses it below. But it is worth recording that the token whose
     NAME says "large text" turned out to be wrong for the largest body text in
     the system, which is the one place its name most invites reaching for it.
     #92 put a size constraint in the name; what the name cannot carry is
     whether receding is what the role wants. */
  color: var(--color-body-strong);
}

.prose h2,
.prose h3 {
  font-family: var(--text-family-heading);
  font-weight: var(--text-heading-weight);
  color: var(--color-heading);
  /* Twice the paragraph gap above, a quarter of it below: a heading belongs
     to what follows it. The asymmetry is the whole reason structure reads
     before the words do. */
  margin-block: var(--space-before-heading) var(--space-after-heading);
}

.prose h2 {
  font-size: var(--text-article-h2);
  line-height: var(--text-article-h2-leading);
}

.prose h3 {
  font-size: var(--text-article-h3);
  line-height: var(--text-article-h3-leading);
}

/* LINKS ARE UNDERLINED, not colour-only. WCAG 1.4.1: colour cannot be the
   only visual means of conveying information, and a link in running text is
   the canonical instance. `--color-link` clears AA on its own (5.44:1) — the
   underline is for the reader who cannot use the hue, not a substitute for
   contrast. */
.prose a {
  color: var(--color-link);
  text-decoration: underline;
  /* Enough to clear descenders so the underline does not cut through a `g`. */
  text-underline-offset: var(--space-inline);
}

.prose a:hover,
.prose a.force-hover {
  color: var(--color-link-hover);
}

.prose ul,
.prose ol {
  margin-block: 0 var(--space-paragraph);
  padding-inline-start: var(--space-card);
}

.prose li {
  margin-block-end: var(--space-list-item);
}

/* A pull quote. The rule is on the inline-start edge rather than a box: a
   quotation inside an article is an aside in the same voice, and a bordered
   panel would read as a different kind of content. */
.prose blockquote {
  margin-block: var(--space-paragraph);
  margin-inline: 0;
  padding-inline-start: var(--space-card);
  /* `--space-1` reached through the focus-ring role would be the wrong name
     for a decorative rule, and `--border-hairline` is too thin to read as a
     quotation mark. This is the third distinct border width the system wants,
     so it gets its own role rather than borrowing one whose name means
     something else -- the same argument the two roles above were added on. */
  border-inline-start: var(--border-quote) solid var(--color-accent);
  color: var(--color-body-strong);
}

.prose figure {
  margin-block: var(--space-before-heading);
  margin-inline: 0;
}

.prose img {
  /* WCAG 1.4.10 again: an image wider than the column is the second most
     common cause of horizontal scroll after a long URL. */
  max-width: 100%;
  height: auto;
  border-radius: var(--radius-card);
}

.prose figcaption {
  margin-block-start: var(--space-inline);
  font-size: var(--text-meta);
  line-height: var(--text-meta-leading);
  /* `--color-body`, NOT `--color-large-text-subtle`, AND THIS IS THE SECOND
     TIME IN THIS FILE.

     `--text-meta` is 14px REGULAR. That is normal text, which owes 4.5:1, and
     `--color-large-text-subtle` measures 3.48:1 -- it is verified in
     `checks/expected-pairs-blog.txt` only at the 3.0 non-text threshold. A
     caption set in it fails AA outright.

     The lead paragraph four declarations above was the same defect, caught by
     rendering. This one shipped, and #94's argument was precisely that the
     rename would stop it: a component author writing 14px would see the
     contradiction on the line. That author was me, one file later, and the
     contradiction still shipped.

     WHY THE NAME WAS NOT ENOUGH. `--type-caption`'s own primitive comment
     reads "14px -- bylines, figure captions", so the size token and the colour
     token each look correct in isolation and only collide when composed. A
     naming convention is weakest exactly there, and `contrast.py` cannot help:
     it checks the PAIR, annotated large-text-only, and never the rendered
     size. That is #95, and it is no longer hypothetical.

     RAISING THE SIZE WOULD BE THE WRONG FIX. A caption is secondary by intent;
     making it large enough to qualify for the 3.0 exception would make it
     compete with the body it explains. The recession has to come from
     something other than contrast -- here, from size and position. Every other
     small-text rule in this file already uses `--color-body` (7.10:1), so this
     was the one outlier rather than a pattern. */
  color: var(--color-body);
}

.prose strong {
  font-weight: var(--text-chrome-weight);
  color: var(--color-body-strong);
}

.prose em {
  font-style: italic;
}

/* ============================================================
   article-header
   ============================================================

   A PEER of `base`'s `page-header`, not a variant. `AGENTS.md`'s test is
   whether one can be produced from the other by changing values; `page-header`
   renders an action row and this renders none, which is structure. A public
   article has no Edit, no Export and no primary action — the reader cannot act
   on it, only read it. */
.article-header {
  max-width: var(--text-measure);
  margin-block-end: var(--space-section-tight);
}

.article-header-title {
  margin-block: 0 var(--space-after-heading);
  /* WRAPS ANYWHERE, like `.prose`, and for a sharper reason: a title is ONE
     line of text at the largest size on the page, so a single long token has
     no smaller sibling to hide behind.

     Found by BLOG-5.6 rendering this design in WordPress at 320px, where the
     real published title -- "Kalau Ananda Belum Lolos Tes MAN IC: Apa Langkah
     Selanjutnya?" -- pushed the document to 323px against a 305px viewport.

     THIS PAGE MEASURED 305px AND STILL HAD THE DEFECT. `checks/reflow.mjs`
     reads `document.scrollWidth`, and the overflowing h1 sat 1px inside the
     margin that would have shown up there: h1 scrollWidth 256 against a 209px
     box, contained by luck rather than by a rule. The element-level overflow
     was there to be measured and nothing measured it.

     It became visible downstream because the THEME LOADS POPPINS AND THIS
     REPOSITORY DOES NOT -- see the note in primitives/typography.css. Every
     rendering here has been in the `system-ui` fallback, which is narrower, so
     the design was judged in a font it does not ship. */
  overflow-wrap: anywhere;
  font-family: var(--text-family-heading);
  font-size: var(--text-article-title);
  line-height: var(--text-article-title-leading);
  font-weight: var(--text-heading-weight);
  color: var(--color-heading);
}

/* Byline, date and reading time on one line, wrapping to two on a phone
   rather than truncating. */
.article-meta {
  display: flex;
  flex-wrap: wrap;
  /* THE SEPARATOR STAYS WITH THE FIELD BEFORE IT. Seen at 320px: the byline
     wrapped and left `· 6 menit baca` opening the second line with a dangling
     dot, which reads as a bullet rather than as punctuation. Each field and
     its preceding separator are therefore one flex item -- see the template,
     where the separator sits inside the following field's span group. The row
     gap is what keeps two lines legible once it does wrap. */
  gap: var(--space-inline);
  margin: 0;
  font-size: var(--text-meta);
  line-height: var(--text-meta-leading);
  color: var(--color-body);
}

/* The separator is DECORATIVE and `aria-hidden` in the template: a screen
   reader announcing "middle dot" between a name and a date is noise. It is
   drawn rather than punctuated into the text so it can be hidden at all. */
/* One field plus the separator in front of it. Inline-flex so the pair cannot
   be split across a line break, with the same inline gap the row uses so the
   spacing is identical whether the row wraps or not. */
.article-meta-item {
  display: inline-flex;
  gap: var(--space-inline);
}

.article-meta-sep {
  color: var(--color-border-strong);
}

/* ============================================================
   post-card — one article in an index
   ============================================================

   THE TITLE IS THE LINK, and the card is not. A whole-card anchor swallows any
   text selection inside it and announces the entire card as one link name;
   `base`'s `crud.md` reaches the same conclusion from the other direction for
   table rows, where a row click is the affordance precisely because the row
   holds no other target. A post card holds a title and a standfirst, and only
   one of them is the thing you follow. */
.post-card {
  /* A ROW, NOT A BOX -- #114. The review asked for a list that reads as a list:
     date, title, standfirst, separated by a rule.

     THE COMPONENT WAS EDITED RATHER THAN JOINED BY A PEER, which is the
     opposite of what `AGENTS.md`'s values-or-structure test usually implies.
     The test says a box and a row differ structurally, and they do; a peer
     exists when BOTH forms stay in use, and nothing renders a boxed post card
     after this. Keeping one for symmetry is how a component library fragments.

     THE NAME STAYS `post-card`. It names the role -- one post in a list -- not
     the border treatment. `post-row` would describe today's styling and be
     wrong again the next time it changes; #90 recorded that components are
     "named by form and function, never by product or version."

     CONSTRAINED TO THE MEASURE, still, and for the reason the box version
     recorded: on a wide viewport an unbounded title runs past the 65-75
     characters `--text-measure` exists to hold, and a one-line standfirst
     stretches into a ribbon. */
  max-width: var(--text-measure);
  padding-block: var(--space-card);
}

/* THE RULE BELONGS TO THE ROW BELOW IT, not above: a border-top on every row
   would draw a line under the masthead, where the band's own border already
   sits. `+` rather than `:not(:first-child)` so the rule is between rows by
   construction. */
.post-card + .post-card {
  border-block-start: var(--border-hairline) solid var(--color-border);
}

.post-card-title {
  margin-block: 0 var(--space-card-gap);
  font-family: var(--text-family-heading);
  font-size: var(--text-article-h3);
  line-height: var(--text-article-h3-leading);
  font-weight: var(--text-heading-weight);
}

.post-card-title a {
  color: var(--color-heading);
  text-decoration: none;
}

.post-card-title a:hover,
.post-card-title a.force-hover {
  color: var(--color-link-hover);
  text-decoration: underline;
}

.post-card-standfirst {
  /* NO TRAILING MARGIN -- it is the last field in the row since #114 reordered
     them, and the row's own `padding-block` provides the gap to the rule. */
  margin-block: 0;
  /* SET EXPLICITLY, because nothing else would. Found by measuring the
     rendered card: `fontSize=16px lineHeight=normal` -- the browser default,
     inherited from `body`, because `.prose` is what carries article type and a
     card is not inside it. It rendered smaller than the article body and with
     no leading from the scale at all.

     `--text-chrome` rather than `--text-article-body`: an index summary is
     navigational, which is the distinction `semantic/typography.css` draws
     when it says chrome and captions "happen to share a size today and need
     not tomorrow". Regular weight, though -- `--text-chrome-weight` is
     semibold for nav links, and a summary paragraph is read, not clicked. */
  font-size: var(--text-chrome);
  line-height: var(--text-chrome-leading);
  font-weight: var(--text-article-body-weight);
  color: var(--color-body);
}

.post-card-date {
  /* LEADS THE ROW since #114, so it carries the gap to the title. Semibold and
     in the subtle tier: it is a scanning handle rather than a sentence, and the
     title under it must stay the loudest thing in the row.

     `--color-body` rather than the subtle tier, deliberately -- at
     `--text-meta` (14px) this is normal-size text under WCAG, where the AA bar
     is 4.5:1. `--color-large-text-subtle` is 3.48:1 and passes only at the
     large-text threshold, which 14px does not reach; its own name says so since
     #92, and reaching for it here would write the contradiction on the line. */
  margin-block: 0 var(--space-inline);
  font-size: var(--text-meta);
  line-height: var(--text-meta-leading);
  font-weight: var(--text-chrome-weight);
  color: var(--color-body);
}

/* The empty state of an INDEX, not of one card: no posts to list. */
.post-card-note {
  margin: 0;
  padding: var(--space-card);
  color: var(--color-body);
}

/* The loading placeholder.
   A DRAWN SHAPE, not spacing: `height` here is the thickness of a bar
   standing in for a line of text, which is why `checks/literals.py` exempts
   graphic heights -- its docstring names `.skeleton` in `base` as the example.
   Routing it through a spacing token would say it is spacing.

   `--color-background-soft` rather than a border colour: the placeholder sits
   on `--color-surface` and has to be visible against it while reading as
   absent-content rather than as content. `base` records the failure mode from
   the other direction -- it used `--muted`, which on a card measured 1.23:1
   and stopped reserving layout perceptibly even though the box model still
   reserved it. */
.post-card-skeleton {
  display: block;
  height: 0.75rem;
  border-radius: var(--radius-rule);
  background: var(--color-background-soft);
}

.post-card-skeleton + .post-card-skeleton {
  margin-block-start: var(--space-list-item);
}

/* The title placeholder is shorter than a full line -- a headline rarely fills
   the column, and a skeleton that draws three identical full-width bars reads
   as a paragraph rather than as a card. */
.post-card-skeleton-title {
  width: 60%;
  /* Taller than the body bars because the title is set larger. Expressed as a
     RATIO of the element's own font-size rather than a second fixed height:
     `checks/literals.py` accepted the 0.75rem above (a graphic height, the
     exemption its docstring names) and rejected a bare 1rem here, and it was
     right to -- two unrelated magic heights in one component is the start of
     the second grid its history records. `em` ties this one to the type scale
     instead, so it follows a token without being one. */
  height: 0.8em;
  font-size: var(--text-article-h3);
}

.post-card-skeleton-date {
  width: 30%;
}

/* THE CONNECTOR'S CALL TO ACTION -- #91, AC-4.
   ============================================================

   THIS MARKUP IS NOT OURS. `blog_cta.py` appends it to every article, and it
   arrives inside `the_content()` exactly as the pipeline writes it:

       <div class="sc-lp-wrap" data-cta-page="blog" data-cta-variant="nurture">
         <a id="cta-blog-nurture" data-cta="blog_nurture" href="...">...</a>
       </div>

   GTM MATCHES THAT STATIC SELECTOR, so the design styles it and changes
   nothing else. A design that authored its own CTA -- different class, extra
   wrapper, restructured anchor -- would produce a theme that either breaks
   tracking or cannot render what the pipeline already publishes. The epic
   calls that the mismatch caught late if missed, and this is the whole of
   AC-4.

   It is styled UNDER `.prose`, because that is where it lands: the CTA is part
   of the body, not a component the design composes. Same contract as every
   other element `prose` styles by selector.

   THE ANCHOR IS THE BUTTON. There is no `<button>` in that markup and adding
   one is not available -- it navigates to a landing page, so an anchor is
   correct anyway. It takes the full hit target because it is the one action on
   the page. */
/* A BAND, NOT A BUTTON IN A PARAGRAPH -- #114.

   THE BAND IS `.sc-lp-wrap` ITSELF. `blog_cta.py` appends that wrapper INSIDE
   the post content and GTM matches the static selector, reading its variant
   from `data-cta-page` / `data-cta-variant` (PRD FR-06d). So the design cannot
   wrap it in a band element of its own: the theme adds no `the_content` filter,
   which is AC-6 of `salamcendekia-wordpress`#30, and a design that authored its
   own CTA would produce a theme that either breaks tracking or cannot render
   what the pipeline already publishes. #91 reached the same conclusion and it
   holds here -- the difference is only how much presence the block is given.

   NOT FULL-BLEED, AND THAT IS THE MEASURED ANSWER RATHER THAN THE FIRST ONE.

   `.index-head` escapes its container by cancelling `--space-chrome-x`, because
   `.content-main` is the only thing between it and the viewport. This block is
   two levels deeper: `.prose` is CENTRED AT THE MEASURE, so the same escape
   left the band at 301..1124 in a 1425px viewport -- a band that starts and
   stops in the middle of nowhere, which reads worse than one that does not try.

   Reaching the edge from here needs the viewport, and every viewport-relative
   form was measured and rejected: `100vw` includes the scrollbar,
   `calc(50% - 50vw)` resolves `50%` against the padded parent and overshot by
   8px. Both produce the sideways scroll `checks/reflow.mjs` exists to catch.

   So the band spans THE MEASURE, which is the column the article is read in,
   and gets its presence from the tint and the rule rather than from width. At
   320px the measure is the viewport, so it is full-bleed there anyway -- on the
   surface where a band matters most.

   THE HEADING AND SUPPORTING LINE ARE DESIGNED HERE, AND NOT YET EMITTED.

   This comment used to stop at "the pipeline emits neither, and CSS cannot
   author copy" and conclude that the band was all the design could give. Both
   halves are true and the conclusion was wrong, because it confused what this
   REPOSITORY publishes with what the theme may do.

   The band was shipped as a tinted, bordered box containing one button, which
   is the technique's cost without its benefit: a container around a single
   control adds visual weight and empty space and groups nothing. Raised by the
   user on the rendered proof -- "why do we need that light blue container?"
   The honest answer was that we did not, as long as it stayed empty.

   SO THE DESIGN ANSWERS THE QUESTION IT IS FOR. `AGENTS.md`: this repository
   "publishes design decisions, not implementation" -- "here is the design,
   please follow it" is the whole contract. The decision is that the band holds
   a heading and a supporting line above the action, and the rules below say how
   they are set. What renders them is a separate question for whoever owns the
   markup.

   NOTHING HERE AUTHORS COPY, and that distinction is the reason the old comment
   went wrong. Styling `.sc-lp-title` is the same act as styling `.sc-lp-wrap`
   and the anchor -- naming elements this system does not emit and saying how
   they look. AC-6 forbids the THEME rewriting the pipeline's markup; it does
   not forbid the DESIGN specifying what that markup should contain. The strings
   in the proof are Marketing's own, quoted from the landing page's final CTA
   block so the design is measured against real text rather than lorem.

   UNTIL THE PIPELINE EMITS THEM, THE LIVE BAND STILL RENDERS EMPTY. That is a
   gap between design and implementation, recorded in the design's README rather
   than hidden -- the deliverable `create-ui` asks for. */
.prose .sc-lp-wrap {
  margin-block: var(--space-section) 0;
  /* `--space-card` RATHER THAN `--space-section-tight` (56px). The taller
     padding was the only thing giving an EMPTY box any height; with a heading
     and a supporting line inside it, 56px above and below reads as a gap rather
     than as generosity. The band is a contained thing, which is the role
     `--space-card` names. */
  padding-block: var(--space-card);
  padding-inline: var(--space-chrome-x);
  border-radius: var(--radius-card);
  background: var(--color-background-soft);
  /* A BORDER ON ALL FOUR SIDES, not just the top. The top-only rule was right
     while this was a full-bleed band, where the edges leave the screen; on a
     rounded block that ends inside the column it drew a line into two corners
     and stopped. */
  border: var(--border-hairline) solid var(--color-border);
  text-align: center;
}

/* THE BAND'S HEADING. `h3` in the proof because the block lands inside
   `the_content()` beneath an article whose own sections are `h2`, and a second
   `h2` would claim equal rank in the document outline. The SIZE is the
   article's h3 role, so the band speaks at the level of a subsection rather
   than shouting over the piece it interrupts.

   Styled by class rather than by element, unlike everything else `prose` sets:
   an `h3` in the article body is a subsection and takes the running-text rules
   above, while this one is chrome inside a contained block. Same tag, different
   role, so the class is what separates them. */
.prose .sc-lp-title {
  margin-block: 0 var(--space-after-heading);
  font-family: var(--text-family-heading);
  font-size: var(--text-article-h3);
  line-height: var(--text-article-h3-leading);
  font-weight: var(--text-heading-weight);
  color: var(--color-heading);
}

/* THE SUPPORTING LINE, constrained and centred.

   `--text-measure` is the ARTICLE's measure at 68ch, which is right for a
   left-aligned column of running text and too wide here: centred text is harder
   to track back to the next line, so it wants fewer characters, not the same
   number. `--text-measure-tight` was added for this rather than a literal
   reached for -- the rule `systems/blog/README.md` states for exactly this
   case, where a component finds no role that fits. */
.prose .sc-lp-sub {
  max-width: var(--text-measure-tight);
  margin-block: 0 var(--space-card);
  margin-inline: auto;
  font-size: var(--text-article-body);
  line-height: var(--text-article-body-leading);
  color: var(--color-body);
}

/* ============================================================
   content-page — the archetype's arrangement (#91)
   ============================================================

   LAYOUT ONLY. The archetype declares no colour and no control; this is the
   centring and the vertical rhythm around components that own their own
   appearance.

   ONE WIDTH DECISION, NOT TWO. The measure lives on the components that hold
   text, so this wrapper centres them and adds page padding -- it does not set
   a second max-width. Two constraints on one column is how a measure stops
   being one. */
.content-main {
  padding-block: var(--space-section-tight);
  padding-inline: var(--space-chrome-x);
}

/* The article body and its header share one centred column. `margin-inline:
   auto` on the children rather than a wrapper width, so the measure stays the
   components' own property. */
.content-main > .article-header,
.content-main > .prose {
  margin-inline: auto;
}

/* THE BAND ESCAPES ONLY WHERE THERE IS PADDING TO ESCAPE -- see `.index-head`.
   Written against the container that owns the padding rather than on the
   component, because the component cannot know whether it has one. */
.content-main > .index-head {
  margin-inline: calc(var(--space-chrome-x) * -1);
  margin-block-start: calc(var(--space-section-tight) * -1);
}

/* An index is one card per row at every width. A grid would be a second layout
   decision this archetype does not need: the cards are already capped at the
   measure, so a second column would either break that cap or leave the page
   half empty. If a multi-column index is wanted later it is a design decision,
   not a default. */
.content-index {
  display: flex;
  flex-direction: column;
  gap: var(--space-card);
  max-width: var(--text-measure);
  margin-inline: auto;
}

/* ============================================================
   index-masthead — the title of a list of articles (#91)
   ============================================================

   A COMPONENT, because it carries type and colour. It lived in the archetype's
   layout-only block until review of #91 pointed out the contradiction: that
   block's header says "the archetype declares no colour and no control", and
   this set a family, a size, a leading, a weight and a colour nine lines
   later. `list-page` states the rule -- an archetype that grows a colour
   needed a component.

   `--text-masthead` is the only place `--type-display` is licensed, per the
   token's own declaration in #86. */
/* WRAPS FOR THE SAME REASON AS `.article-header-title`, and one case makes it
   unavoidable: BLOG-5.6 renders a search page whose masthead carries the
   reader's own query, which is arbitrary text this system cannot bound. A
   40-character query with no space in it is not a hypothetical. */
/* THE BAND IS FULL-BLEED AND ITS WORDS ARE NOT -- #114.

   `100vw` IS WRONG AND SO IS `calc(50% - 50vw)`. Both were tried and measured:

     100vw               includes the scrollbar, so the band is wider than the
                         viewport wherever one is reserved
     calc(50% - 50vw)    `50%` resolves against the PADDED parent, not the
                         viewport, so inside `.content-main` the band overshot
                         by exactly that padding -- measured -7..1433 in a
                         1425px viewport, an 8px sideways scroll

   The first draft of this rule shipped the second form with a comment claiming
   it worked, which it did not. Recorded because the comment was the failure:
   it asserted a measurement nobody had taken.

   `-100vw` margins with `100%` width would have the scrollbar problem again.
   What actually holds is cancelling the parent's padding by name -- the band
   knows which container it escapes, and `--space-chrome-x` is that container's
   own token, so the two cannot drift.

   THE ESCAPE IS SCOPED TO THAT CONTAINER, and lives beside `.content-main`'s
   other child rules rather than here. A negative margin cancelling padding that
   is not there does not sit flush -- it overflows by exactly the amount it
   tried to cancel. The component proof renders this block straight into
   `<body>`, with no `.content-main` at all, and `checks/reflow.mjs` caught it
   on the first run after the block was added: 353px in a 320px viewport, on all
   six states.

   So the component styles itself, and the container grants the escape. A band
   rendered anywhere else is a band at its container's width, which is correct
   rather than broken. */
.index-head {
  /* IT ALSO CANCELS THE TOP PADDING, so the band meets the header rather than
     floating below a 56px strip of white. `.content-main` opens with
     `padding-block: var(--space-section-tight)`, which is correct for a page
     that starts with an article title and wrong for one that starts with a
     band -- a band under a header is chrome continuing, not content beginning.

     Seen by opening the render, not by a check: the band was the right colour,
     the right width and in the right order, sitting 56px too low. */
  margin-block-end: var(--space-section-tight);
  padding-block: var(--space-section-tight);
  padding-inline: var(--space-chrome-x);
  background: var(--color-background-soft);
  border-block-end: var(--border-hairline) solid var(--color-border);
}

/* The words come back to the measure. Same shape `site-footer` uses: a band
   that spans the window holding content that does not. */
.index-head-inner {
  max-width: var(--text-measure);
  margin-inline: auto;
}

.index-masthead {
  overflow-wrap: anywhere;
  margin-block: 0;
  font-family: var(--text-family-heading);
  font-size: var(--text-masthead);
  line-height: var(--text-masthead-leading);
  font-weight: var(--text-heading-weight);
  color: var(--color-heading);
}

/* The line under the masthead. `--text-article-lead` is the one size in this
   scale between the display end and body copy, which is what a standfirst for
   a whole page is. */
.index-head-description {
  overflow-wrap: anywhere;
  margin-block: var(--space-inline) 0;
  font-size: var(--text-article-lead);
  line-height: var(--text-article-lead-leading);
  font-weight: var(--text-article-lead-weight);
  color: var(--color-body);
}

/* ============================================================
   pill — the button-shaped control, once (#116)
   ============================================================

   WHY THIS COMPONENT EXISTS. Three call sites styled a button-shaped control by
   hand, and two of them -- the header CTA and the in-article CTA -- were
   IDENTICAL except for one declaration, the background. `AGENTS.md` asks one
   question before a component is added:

       The test is whether one can be produced from the other by changing values.
       | Values -- radius, shadow, density, colour | a token set |

   One could be produced from the other by changing one value, so the answer was
   "a token set on one component" and what existed was two hand-typed copies.
   The third, the menu button, diverged on two declarations with nothing in the
   source recording whether that was a decision or a drift.

   NOTHING CHECKED THAT THE COPIES AGREED. `literals.py` sees tokens and passes;
   `contrast.py` sees pairs and passes; neither reads whether two controls share
   a shape. The only reason the two CTAs matched is that the author of #115
   copied carefully, which is not a mechanism.

   NOT A PEER OF `base`'s `button`. Different system, different scale, no shared
   stylesheet -- the same relationship `site-header` has to `nav`. `base`'s
   control is `.btn` at 14px body type on a dense application scale; this one is
   `--text-chrome` at 16px on a reading surface, and it is an `<a>` at two of
   its three call sites.

   WHY `pill` AND NOT `button` OR `action`. It is named for its form and
   function, which is what the naming rule asks for, and both alternatives are
   already spoken for in a way that would mislead:

     `button`  Two of three call sites are `<a>` elements that navigate. A class
               called `button` on an anchor invites the next author to reach for
               a `<button>` and break the link.
     `action`  `base` uses `a.action` for a TEXT-LINK action -- no fill, no
               pill, no target floor. One name for two different controls across
               two systems is the confusion the naming rule exists to prevent.

   `pill` says the shape, and the shape is the affordance -- which is the
   argument `site-header.spec.md` already made for this control in #114.

   ---- structure ----

   Everything true of every variant. Colour lives on the variants below and
   NOWHERE else, which is what makes a rebrand a token swap. */
.pill,
/* THE IN-ARTICLE CTA JOINS HERE RATHER THAN RESTATING THE SHAPE.

   `blog_cta.py` emits that anchor and GTM matches the static selector, so the
   theme adds no `the_content` filter (AC-6 of `salamcendekia-wordpress`#30) and
   there is NO PLACE TO WRITE A CLASS ATTRIBUTE. Every other call site writes
   `class="pill pill-accent"`; this one cannot.

   So the selector joins the component's own rule. That is what keeps this a
   composition rather than a fourth copy: there is exactly one list of these
   declarations in the file, and this anchor is on it. Writing the eleven
   declarations again under a comment SAYING it composes would be the defect
   #116 exists to close, wearing the fix's clothes.

   `:visited` rides along because a CTA that has been clicked is still the same
   control, and `.prose a:visited` would otherwise recolour its label. */
.prose .sc-lp-wrap a,
.prose .sc-lp-wrap a:visited {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: var(--control-target);
  padding-inline: var(--space-card);
  border-radius: var(--radius-pill);
  /* A DECLARED TRANSPARENT BORDER, so the outlined variant does not resize the
     filled ones by adding 2px of box. `base`'s `.btn` reaches the same
     conclusion for the same reason; here the hairline token carries the width
     so the outline variant only has to name a colour. */
  border: var(--border-hairline) solid transparent;
  font-family: var(--text-family-heading);
  font-size: var(--text-chrome);
  line-height: var(--text-chrome-leading);
  font-weight: var(--text-chrome-weight);
  /* A BUTTON SHAPE DOES NOT CARRY A LINK UNDERLINE -- the shape already says it
     is actionable. Set here rather than per variant because it is true of the
     control, and it must survive `.prose a`'s underline: the in-article CTA
     lands inside prose, where every anchor is underlined by element selector. */
  text-decoration: none;
  /* THE TRANSITION THIS CONTROL WANTED SINCE #116, now that #120 authored the
     tier it needed.

     It shipped without one deliberately: `base`'s `.btn` animates over 200ms,
     and copying that here would have written `200ms` as a LITERAL, because
     this system had no motion tier at all -- no primitives, no semantic role,
     and not one `transition` in this file. `systems/blog/README.md` states the
     rule for exactly that case:

         If a component reaches for a primitive because no role fits, the
         missing role is a bug in the semantic file.

     A missing TIER is that bug one level up, so the gap was recorded in
     `pill.spec.md` rather than papered over with a hardcoded duration. #120
     authored `foundation/{primitives,semantic}/motion.css`, and this is the
     first and only occupant of `--transition-control`.

     THE PROPERTIES ARE THE ONES THAT CHANGE. `background-color`, `border-color`
     and `color` are what every pill variant moves on hover and focus; `all`
     would animate layout properties nothing here changes and cost a frame
     budget for no visible result. */
  transition:
    background-color var(--transition-control),
    border-color var(--transition-control),
    color var(--transition-control);
}

/* A `<button>` element does not inherit the pointer, and the menu control is
   the one call site that is a real button. Set on the element rather than the
   class so an anchor -- which already shows a pointer -- is not told twice. */
button.pill {
  cursor: pointer;
}

/* ---- variants: colour, and nothing else ----

   THE HIERARCHY IS THE POINT, and it is why there are three rather than one.
   A reader meets at most two of these on a page and they must not compete:

     accent   the header CTA -- yellow, the loudest thing in the chrome
     primary  the in-article CTA -- blue, deliberately NOT a second yellow
     quiet    the menu control -- outlined, because opening a menu is not an
              action that should compete with the one action on the page

   `pill-primary` KEEPS BLUE RATHER THAN TAKING THE HEADER'S YELLOW, and #114
   recorded why: `foundation/semantic/color.css` makes yellow the ACCENT here
   and keeps it scarce by intent. Two yellow pills on one page spends that
   scarcity on a repeat. The shape carries the family resemblance; the colour
   keeps the hierarchy. */
.pill-accent  { background: var(--color-accent);     color: var(--color-accent-foreground);  }
.pill-primary,
.prose .sc-lp-wrap a,
.prose .sc-lp-wrap a:visited { background: var(--color-primary); color: var(--color-primary-foreground); }
.pill-quiet   {
  /* TRANSPARENT, NOT `--color-background`. It painted white-on-white before,
     and `checks/surfaces.mjs` measured that at 1:1 and failed it -- correctly,
     and it is worth being precise about what it caught. The control was legible
     to a reader the whole time, because its border does the signalling. What
     the opaque fill did was make the check UNABLE TO SEE THAT: its exemption is

         A state that paints no fill of its own is not this check's business --
         it may be signalling with a border, an outline or a colour instead.

     An outlined control repainting the page's own background is exactly such a
     control wearing a fill it does not use, so it fell outside the exemption
     written for it and was measured against a rule it was never trying to meet.

     `transparent` is also the truer statement. The old value asserted "this
     control paints white", which is only accidentally right: on
     `--color-background-soft` -- the footer's ground, and the CTA band's -- the
     white patch would have been visible as a box the design never intended.
     `base`'s `.btn-outline` reaches the same conclusion for the same reason.

     The border still carries the whole affordance: `--color-border-strong` on
     `--color-background` measures 3.48:1, over WCAG 1.4.11's 3:1 for a control
     boundary. Measured with `tools/color.py`, and unwatched -- see the spec. */
  background: transparent;
  color: var(--color-heading);
  border-color: var(--color-border-strong);
  /* THE ONE STRUCTURAL VALUE A VARIANT SETS, and it is a density decision
     rather than a colour: a menu label is one word where a CTA is a phrase, so
     the tighter step reads as deliberate rather than cramped. Kept here rather
     than as a `pill-tight` size variant, because one call site does not
     establish a scale -- the moment a second control wants it, that is the
     token set `AGENTS.md` asks for. */
  padding-inline: var(--space-card-gap);
  /* A 44px SQUARE FLOOR. The label is short enough that the padding alone can
     leave it under the target on a narrow viewport. */
  min-width: var(--control-target);
}

/* ---- hover: colour, the only state channel this system spends ----

   EVERY RULE HAS A `.force-hover` TWIN, and that is not decoration. Live
   `:hover` never fires in a headless render, so a rule with no forced story is
   never measured -- and an unmeasured rule cannot fail. Before #116 NOT ONE
   blog hover rule was rendered anywhere: `checks/surfaces.mjs
   --require-full-coverage` on the blog proof reported "no state fill rendered
   -- nothing measured" and exited 0, which is this repository's recurring
   defect shape exactly. The proof now renders each of these. */
.pill-accent:hover,
.pill-accent.force-hover  { background: var(--color-accent-strong); }

.pill-primary:hover,
.pill-primary.force-hover,
.prose .sc-lp-wrap a:hover,
.prose .sc-lp-wrap a.force-hover { background: var(--color-primary-hover); }

/* THE OUTLINED ONE MOVES ITS BORDER AND ITS INK, not a fill. It has no fill to
   deepen, so the channel it owns is the outline -- the same treatment it
   carried as `.site-menu`, preserved because it was the one part of that
   control that was a real decision. */
.pill-quiet:hover,
.pill-quiet.force-hover {
  border-color: var(--color-link);
  color: var(--color-link);
}

/* THE `prefers-reduced-motion` COUNTERPART, which the note here reserved this
   spot for: "if the motion tier described above is ever authored, this is where
   its counterpart goes." #120 authored it, so here it is.

   REMOVED, NOT SHORTENED. `base`'s `patterns/motion.md:42` is the rule -- "a
   300 ms slide shortened to 50 ms is still a slide" -- so this sets `none`
   rather than a faster duration. A user who asked for less motion asked for
   none of this, not a quicker version of it.

   THE FIRST REAL `@media` IN THIS SYSTEM, and it is a deliberate exception
   rather than a reversal. The rule this file states twice is that there is no
   `@media` switching LAYOUT -- a static page mounting two width variants ships
   a control unreachable at every width, which `keyboard.mjs` caught with 16
   failures in `base`. `prefers-reduced-motion` is a USER PREFERENCE, not a
   breakpoint: it mounts nothing, hides nothing, and changes no document. The
   constraint it sits under is about layout, and this is not layout. */
@media (prefers-reduced-motion: reduce) {
  .pill { transition: none; }
}

/* ============================================================
   site-header — BLOG-5.4
   ============================================================

   ONE WIDTH'S TRUTH PER RENDERING. There is no `@media` here switching between
   a row and a menu, because the two are different DOCUMENTS: `base`'s app shell
   records that a static page mounting both ships a control unreachable at every
   width, and `keyboard.mjs` caught exactly that with 16 failures.

   So these rules style whichever header the state produced, and the wrapping
   below is what makes each one survive its own width rather than what switches
   between them. */
.site-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-inline) var(--space-card);
  padding: var(--space-chrome-y) var(--space-chrome-x);
  border-block-end: var(--border-hairline) solid var(--color-border);
  background: var(--color-background);
}

.site-brand {
  display: inline-flex;
  /* The lockup is the link's whole content, so the anchor must not add a
     baseline gap under it. */
  line-height: 0;
}

/* AT OR ABOVE THE BRAND MINIMUM, which is 180px for `lockup-horizontal` and is
   a rule rather than a preference -- below it the wordmark drops under the
   readable floor. `width`/`height` are also on the element so the row does not
   reflow when the image arrives.

   It does not shrink on a narrow viewport, and it does not need to: 320px less
   two 24px paddings leaves 272, and 272 - 180 leaves 92 for a control whose
   target is 44. Measured before choosing the lockup. */
.site-lockup {
  /* 120px is `lockup-stacked`'s minimum, from `brand/logo/README.md`. The
     stacked wordmark wraps to two lines, so it needs LESS width than the
     horizontal one's 180 -- which is why #114 could adopt the main site's
     lockup without giving up the 320px budget #90 measured. */
  width: 120px;
  height: auto;
}

.site-links {
  min-width: 0;
}

.site-link-list,
.site-menu-list {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-inline) var(--space-card);
  margin: 0;
  padding: 0;
  list-style: none;
}

/* PADDED TO CLEAR THE 24px FLOOR. Line height alone painted these at 22px --
   under WCAG 2.5.8, on the one control every reader meets first.

   Nothing caught it for four epics: `checks/targets.mjs`'s selector list was
   entirely `systems/base` classes, so it printed "no hit targets found" and
   exited 0 on every blog surface. #115 added the blog selectors, and the check
   failed these two links on the first run -- which is what a check earning its
   keep looks like.

   `display: inline-block` plus `padding-block`, the same shape
   `.site-footer-link` already used to reach 40px. Not `min-height`: these are
   inline flex children, where a min-height does not grow the painted box
   without also changing the alignment of the row. */
.site-link {
  display: inline-block;
  padding-block: var(--space-inline);
  font-size: var(--text-chrome);
  line-height: var(--text-chrome-leading);
  font-weight: var(--text-chrome-weight);
  color: var(--color-heading);
  text-decoration: none;
}

.site-link:hover,
.site-link.force-hover {
  color: var(--color-link-hover);
  text-decoration: underline;
}

/* THE HEADER CTA IS A `pill`, NOT ITS OWN CONTROL -- #116.

   This block used to restate the eleven declarations `.prose .sc-lp-wrap a`
   already carried, differing only in the background. That is the
   values-or-structure test in `AGENTS.md` answered "values", and two hand-typed
   copies is the answer it tells you not to write:

       | The difference is | Do this   |
       | Values -- colour  | a token set |

   So the shape lives once, in `.pill` below, and this call site names the
   variant. `--color-accent` on `--color-accent-foreground` is 8.62:1 and was
   first rendered in #114; `.pill-accent` carries it now.

   WHAT REMAINS HERE IS ONLY WHAT IS TRUE OF THIS POSITION rather than of the
   control: nothing. The header CTA is a `pill-accent` and no more, which is why
   this rule no longer exists -- `_site-header-body.liquid` writes the classes
   directly. Kept as a comment rather than deleted silently, because "where did
   `.site-cta` go" is a question the next reader of the theme will ask: the
   WordPress theme vendors this file verbatim, and the class it used is gone.

   `.site-cta` IS RETIRED, NOT ALIASED. An alias would leave two names for one
   control and the fragmentation would arrive by a different door. The
   `AGENTS.md` clause for this is "when the old form is retired, edit in place":
   nothing renders the old form once the templates below are updated, the rules
   for it are removed rather than left behind, and the name was describing a
   position anyway. */

/* THE MENU CONTROL. A real `<button>`: it operates the page rather than
   navigating.

   IT IS A `pill-quiet` NOW, and #116 is where its two divergences were finally
   decided rather than left as artifacts. It used to set `--text-family-body`
   and `--radius-control` while the other two controls set heading and pill, and
   nothing in the source said whether that was a decision or a drift -- the
   question was literally unanswerable, because there was no component for it to
   be a variant of.

   BOTH DIVERGENCES ARE RESOLVED TOWARDS THE SHARED SHAPE, and the reason is
   that neither had an argument behind it:

     font-family  A menu button is chrome, and every other piece of chrome in
                  this system -- `.site-link`, `.site-footer-link`, the CTA --
                  is set in `--text-family-heading`. Body type here was the odd
                  one out, not a considered contrast.

     border-radius The control sits in the same header row as the CTA pill, at
                  the same 44px height. Two adjacent controls at one height with
                  different corner radii reads as an inconsistency rather than
                  as a hierarchy, and the hierarchy is already carried by the
                  fill -- yellow against a hairline outline.

   WHAT STAYS ITS OWN is the only thing that was ever a real difference: it is
   OUTLINED rather than filled. That is `.pill-quiet`, and it is a genuine
   variant because a control that opens a menu should not compete with the one
   action on the page.

   44px IS VERIFIED BY HAND, NOT BY A CHECK, and the first version of this
   comment claimed otherwise -- it said `checks/targets.mjs` measures this,
   which is wrong twice over. That check's `SELECTORS` list did not include
   `.site-menu`, so running it against `collapsed.html` reported "no hit targets
   found"; and its `MIN_TARGET` is 24, enforcing WCAG 2.5.8 (AA) rather than
   the 2.5.5 (AAA) figure `--control-target` carries.

   Measured in a browser instead: 74.7 x 44. Raised in review of #106, where
   the pattern was named exactly -- a check's name taken as evidence that the
   check was watching.

   `padding-inline` is the ONE declaration this call site still sets, and it is
   set on the variant rather than here: a menu label is one word where a CTA is
   a phrase, so it takes the tighter `--space-card-gap`. See `.pill-quiet`. */

/* THE OPEN MENU TAKES THE FULL ROW, which is what `flex-wrap` on the header is
   for: the panel is a third flex item and wraps beneath the lockup and button
   rather than being positioned over them. A statically rendered overlay would
   have to guess at a height this component does not know. */
.site-menu-panel {
  flex-basis: 100%;
  padding-block-start: var(--space-card-gap);
}

.site-menu-list {
  flex-direction: column;
  gap: var(--space-list-item);
}

/* THE WIDE DEFAULT COMES FIRST, AND THE ORDER IS LOAD-BEARING.

   This rule and its counterpart inside the query below have the same
   specificity, so the LAST one wins. Written after the query -- as it was
   first -- it overrode the query: below the hinge the mark was `display: none`
   from both rules and the header rendered with no brand at all, an empty 41px
   bar. Measured, not reasoned about. Declared here, the query overrides it,
   which is the way round a width query has to work.

   It is a default rather than a second `(width >= 48rem)` query so that a
   browser applying neither still renders the fuller header rather than
   nothing.

   SIZED BY ITS OWN ROLE, not by `--control-target`. An earlier version reached
   for that token because 44px was the number wanted, but it names a HIT TARGET
   -- the minimum a finger needs -- and this mark is not a control. Borrowing a
   token for its value rather than its meaning is how a rebrand stops being a
   token swap: change the hit-target floor and the brand mark would resize with
   it, for no reason anyone could reconstruct. Raised in review of #132. */
.site-brand-mark {
  display: none;
  width: var(--size-brand-mark);
  height: var(--size-brand-mark);
}

/* ---- the narrow header ------------------------------------------------

   THE FIRST LAYOUT `@media` IN THIS SYSTEM, and it reverses a recorded
   decision rather than filling a blank, so the reason belongs here.

   `systems/blog` shipped with no layout query at all. The chrome reflowed by
   `flex-wrap`, and the comment above says that wrapping "is what makes each one
   survive its own width rather than what switches between them".

   THE COST OF CHANGING THAT WAS NAMED DOWNSTREAM, in the WordPress theme's own
   spec -- `salamcendekia-wordpress`,
   `docs/spec/30-salcen-theme/01-theme.md`, under "The header renders one state,
   and ships no JavaScript":

       Adding a breakpoint and a menu button would invent both: a breakpoint
       value the system does not define, and a control the approved pages do
       not carry.

   Cited by repository and path because it is NOT in this one -- review of #132
   could not trace it and reasonably read it as invented. Half of it is now
   answered: the value is no longer invented, because it is `base`'s. The other
   half -- a control the approved pages do not carry -- is answered by
   `index-320` carrying it.

   MEASURED ON THE LIVE HEADER, which is what settled it:

       parts   lockup 120 + links 133 + CTA 165        = 418px
       gaps    24 x 2                                  =  48px
       need                                            = 466px

       480px viewport -> 432 available -> wraps, header 85px becomes 133px
       320px viewport -> 272 available -> wraps twice, header becomes 174px

   Nothing overflows -- `scrollWidth` equals the viewport at every width, so
   1.4.10 was never in question. The defect is three rows of chrome above the
   first word of content, which is what a reader meets on a phone.

   AND TIGHTENING ALONE CANNOT FIX IT. Dropping the padding to 16 and the gap
   to 12 frees 40px: enough at 480, still 84px short at 390. The only remaining
   lever is the lockup, and 120px is `brand/logo/README.md`'s floor for the
   stacked mark. Shrinking past a brand minimum to win a layout argument is the
   trade this repository exists to refuse.

   `brand/logo/README.md` already prescribes the alternative: "Below the lockup
   minimums, use the mark alone rather than shrinking further." So the narrow
   header swaps the lockup for the mark -- a substitution the brand sanctions --
   and moves the links behind the menu control the component already ships.

   THE LITERAL `48rem` IS DELIBERATE. A custom property cannot appear in an
   `@media` condition: the query is evaluated before custom properties resolve.
   `foundation/semantic/breakpoints.css` is the record of what the number means;
   this is the one place the value has to be written twice, and that file says
   so. */
@media (width < 48rem) {
  .site-header {
    /* Tighter than the desktop chrome, because at this width the padding is
       competing with the content rather than framing it. */
    padding-inline: var(--space-card-gap);
    gap: var(--space-inline) var(--space-card-gap);
  }

  /* THE MARK, NOT THE LOCKUP. `brand/logo/README.md`: "Below the lockup
     minimums, use the mark alone rather than shrinking further." The mark is
     square and reads at 44px, where the stacked lockup needs 120 and would be
     below its own floor. */
  .site-brand-lockup {
    display: none;
  }

  .site-brand-mark {
    display: block;
  }
}

/* ============================================================
   site-footer — BLOG-5.4
   ============================================================

   COLUMNS THAT STACK BY WRAPPING, not by a breakpoint. `min-width` on a group
   plus `flex-wrap` means the number of columns follows the space available and
   the number of groups, neither of which this component decides -- which links
   it carries is Marketing's call and lands in the design (BLOG-5.5). */
.site-footer {
  padding: var(--space-section-tight) var(--space-chrome-x);
  border-block-start: var(--border-hairline) solid var(--color-border);
  background: var(--color-background-soft);
}

.site-footer-brand {
  margin-block-end: var(--space-card);
}

/* A DRAWN SHAPE, sized by width with the height following. `checks/literals.py`
   exempts `width` -- its docstring names graphic sizes explicitly -- and does
   not exempt `height`, which is correct: a height is usually spacing or control
   size and this is the rare case where it is neither. `aspect-ratio` says the
   mark is square, which is a fact about the artwork rather than a second
   magic number. */
.site-footer-mark {
  width: 2rem;
  aspect-ratio: 1;
}

.site-footer-name {
  margin-block: var(--space-inline) 0;
  font-family: var(--text-family-heading);
  font-size: var(--text-chrome);
  line-height: var(--text-chrome-leading);
  font-weight: var(--text-heading-weight);
  color: var(--color-heading);
}

.site-footer-tagline {
  margin-block: var(--space-inline) 0;
  font-size: var(--text-chrome);
  line-height: var(--text-chrome-leading);
  color: var(--color-body);
}

.site-footer-groups {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-card);
}

.site-footer-group {
  /* Wide enough that a two-word link does not wrap mid-phrase, narrow enough
     that two groups still sit side by side at 320px less the chrome padding.

     `flex: 0 1 8rem` -- IT DOES NOT GROW. `1 1 8rem` let two groups share the
     full width of a wide viewport, so "Perusahaan" sat in the middle of the
     page with a column of empty space either side, reading as a layout
     accident rather than a pair. Seen by opening it; every check was green,
     because nothing measures whether a gap means anything.

     The group is as wide as its content needs and no wider; the row's `gap`
     does the spacing. That also keeps the footer's shape stable as BLOG-5.5
     adds groups, rather than re-dividing the width each time one appears. */
  min-width: 8rem;
  flex: 0 1 8rem;
}

.site-footer-heading {
  margin-block: 0 var(--space-inline);
  font-size: var(--text-meta);
  line-height: var(--text-meta-leading);
  font-weight: var(--text-chrome-weight);
  color: var(--color-heading);
}

.site-footer-list {
  margin: 0;
  padding: 0;
  list-style: none;
}

.site-footer-link {
  display: inline-block;
  padding-block: var(--space-inline);
  font-size: var(--text-chrome);
  line-height: var(--text-chrome-leading);
  color: var(--color-body);
  text-decoration: none;
}

.site-footer-link:hover,
.site-footer-link.force-hover {
  color: var(--color-link-hover);
  text-decoration: underline;
}

.site-footer-copyright {
  margin-block: var(--space-card) 0;
  font-size: var(--text-meta);
  line-height: var(--text-meta-leading);
  color: var(--color-body);
}

/* Screen-reader-only text. Used by the loading state, where a spinner that
   says nothing is invisible to a reader who cannot see it. */
.visually-hidden {
  position: absolute;
  width: var(--border-hairline);
  height: var(--border-hairline);
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
