/* ============================================================
   Anna Kemp — WEBSITE DESIGN SYSTEM
   03 / components.css — the component library.
   ------------------------------------------------------------
   Every declaration reads a token. No raw values below this line
   except structural geometry (grid counts, aspect ratios, 1px
   hairlines, percentages) which are not themeable.

   Contents
     A  Layout        container, section, band, decor, stack, grids
     B  Typography    display, heading, lead, body, eyebrow, quote
     C  Actions       button, text action, nav link, social link
     D  Navigation    site header, mobile drawer, footer
     E  Cards         intent, proof, belief, hub row, timeline, stat
     F  Media         frame, figure, avatar, diamond plate
     G  Objects       guide cover, product mock, letters band
     H  Forms         field, input, consent, messages, states
     I  Motion        the single reveal
     J  Status        content-status filtering
   ============================================================ */


/* ============================================================
   A · LAYOUT
   ============================================================ */

.container       { width: 100%; max-width: var(--content-max); margin-inline: auto; padding-inline: var(--gutter); }
.container-read  { width: 100%; max-width: var(--content-reading); margin-inline: auto; padding-inline: var(--gutter); }

/* Section — the vertical rhythm unit. One job per section. */
.section         { padding-block: var(--section-y); }
.section-compact { padding-block: var(--section-y-compact); }
.section-large   { padding-block: var(--section-y-large); }

/* Surface variants. Never more than two beyond page + white in a page. */
.surface-soft  { background: var(--color-surface-soft); border-block: 1px solid var(--color-border-soft); }
.surface-white { background: var(--color-surface); border-block: 1px solid var(--color-border-soft); }
.surface-sand  { background: var(--color-surface-sand); }
.surface-mint  { background: var(--color-surface-mint); }
.surface-brand { background: var(--color-brand); color: var(--color-text-inverse); }
.surface-dark  { background: var(--color-surface-dark); color: var(--color-text-inverse); }

/* Co-branded Termly band. Added 2026-08-24 by decision: this one section
   carries Termly's own ground and lockup. Anna's typefaces and Anna's button
   treatments are kept, so the band reads as part of her page rather than an
   embedded advert. Permitted ONCE per site. Inherits every .surface-dark
   inverse rule below, then overrides the ground and the quiet text colours
   to Termly's own documented on-blue pairings. */
.surface-termly { background: var(--color-termly-blue); color: var(--color-text-on-termly); }
.surface-termly .t-body, .surface-termly .t-body-lg { color: var(--color-text-on-termly-quiet); }
.surface-termly .t-eyebrow-dark { color: var(--color-label-on-termly); }

/* Controls on the Termly ground. .surface-termly is its own class, so it did
   NOT pick up the .surface-dark / .surface-brand inversions below: .btn-primary
   computed teal-on-blue, .btn-secondary and .action computed near-black on
   blue. All three were close to invisible. Fixed 2026-08-25.
   In this band the primary is Termly's own call to action on a white fill —
   the free guide steps down to the outline. It is the one place on the site
   where the free guide is not the primary. */
.surface-termly .btn-primary { background: var(--color-surface); color: var(--color-text-primary); }
.surface-termly .btn-primary:hover { background: var(--color-brand-soft); color: var(--color-text-primary); }
.surface-termly .btn-secondary { color: var(--color-text-inverse); border-color: var(--color-border-on-dark); }
.surface-termly .btn-secondary:hover { border-color: var(--color-text-inverse); background: rgba(255, 255, 255, .08); color: var(--color-text-inverse); }
.surface-termly .action { color: var(--color-label-on-termly); }
.surface-termly .action:hover { color: var(--color-text-inverse); }
.surface-termly .action > .arrow { color: currentColor; }
/* Termly's lockup. Never recoloured, never stretched. Minimum 182x40; clear
   space equals the logomark's width, which the stack gap provides. */
.termly-lockup { display: block; width: 200px; max-width: 60%; height: auto; margin-block: var(--space-5) var(--space-6); }

/* Decor host. Required parent for any tint circle or diamond bleed. */
.has-decor { position: relative; overflow: hidden; }
.has-decor > .container { position: relative; z-index: var(--z-body); }

/* Decor circle. Bleeds off an edge; never floats mid-layout.
   Set size and offsets inline per instance — position is composition,
   not a token. */
.decor {
  position: absolute; border-radius: var(--radius-circle);
  pointer-events: none; z-index: var(--z-decor);
}
.decor-teal     { background: var(--color-decor-teal); }
.decor-sage     { background: var(--color-decor-sage); }
.decor-mint     { background: var(--color-decor-mint); }
.decor-coral    { background: var(--color-decor-coral); }
.decor-on-brand { background: var(--color-decor-on-brand); }
.decor-on-mint  { background: var(--color-decor-on-mint); }

/* Stacks. */
.stack    > * + * { margin-block-start: var(--space-4); }
.stack-sm > * + * { margin-block-start: var(--space-3); }
.stack-lg > * + * { margin-block-start: var(--space-6); }
/* One step above Stack, for two composed GROUPS inside a single section —
   where splitting them into two sections would double the section padding.
   Group level only; never for runs of text. */
.stack-block > * + * { margin-block-start: var(--space-10); }

/* Cluster — the horizontal counterpart to Stack. A wrapping row of
   actions: primary button + underlined link, a social row, a set of
   text actions. Carries no colour and no border; it only arranges.
   Not for content, only for controls and links. */
.cluster    { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-4); }
.cluster-sm { gap: var(--space-3); }
.cluster-lg { gap: var(--space-6); }

/* Section head — eyebrow + h2 + lead. Max 620px so it never
   spans the full 1240px measure. */
.section-head { max-width: 620px; margin-block-end: var(--section-head-gap); }

/* Grids. */
.grid          { display: grid; gap: clamp(var(--space-4), 2.4vw, var(--space-6)); }
.grid-split    { grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: clamp(36px, 5vw, 72px); align-items: center; }
.grid-2        { grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); }
.grid-3        { grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); }
.grid-4        { grid-template-columns: repeat(auto-fit, minmax(228px, 1fr)); }
.grid-intent   { grid-template-columns: repeat(auto-fit, minmax(258px, 1fr)); gap: clamp(var(--space-4), 1.8vw, 22px); }
.grid-proof    { grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: clamp(22px, 2.4vw, var(--space-6)); }
.grid-belief   { grid-template-columns: repeat(auto-fit, minmax(440px, 1fr)); gap: clamp(28px, 2.8vw, 40px); }
.grid-belief-pair { display: grid; grid-template-columns: repeat(2, 1fr); gap: clamp(28px, 2.8vw, 40px); }
/* Hairlines are drawn per row, not by painting the container and letting it
   show through 1px gaps. With the container painted, a last row that does not
   fill its tracks renders those empty tracks as a solid block of border
   colour — which is what happens the moment a config-gated row drops out. */
.grid-hub      { grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1px; background: var(--color-page); }
.grid-hub > .hub-row { box-shadow: 0 0 0 1px var(--color-border); }

@media (min-width: 600px) and (max-width: 899px) {
  .grid-intent, .grid-proof, .grid-3, .grid-4 { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 599px) {
  /* components.md: "All collapse to one column below 600px." .grid-split (320px),
     .grid-hub (300px) and .grid-belief (440px) were missing here, so their track
     minimums exceeded the content box at 320px and pushed the page 20px wide. */
  .grid-intent, .grid-proof, .grid-2, .grid-3, .grid-4, .grid-belief-pair,
  .grid-split, .grid-hub, .grid-belief { grid-template-columns: 1fr; }
}
@media (min-width: 1280px) {
  .grid-intent { grid-template-columns: repeat(4, 1fr); }
  /* `.grid-3` means three columns, and until 2026-09-08 it could render four.
     `auto-fit, minmax(280px, 1fr)` fits four 280px tracks inside a 1240px
     container, and with exactly three items the fourth track collapsed — so
     every existing use looked correct and the fault was invisible. The moment a
     fourth or fifth card arrived it would have laid out 4, then 4+1: one card
     alone in a row of four, which is the worst case a card grid has.

     Stating the count changes nothing for a three-item grid (a collapsed track
     and a stated track are the same width) and makes a five-item grid read
     3 + 2. Named components should lay out the way their name says. */
  .grid-3 { grid-template-columns: repeat(3, 1fr); }
}


/* ============================================================
   B · TYPOGRAPHY
   ============================================================ */

.t-display-xl { font: 300 var(--type-display-xl)/var(--leading-display) var(--font-display); letter-spacing: var(--tracking-display); }
.t-display-lg { font: 300 var(--type-display-lg)/1.08 var(--font-display); letter-spacing: var(--tracking-display); }
.t-h2         { font: 300 var(--type-h2)/var(--leading-heading) var(--font-display); letter-spacing: var(--tracking-heading); }
.t-h2-sm      { font: 300 var(--type-h2-sm)/1.14 var(--font-display); letter-spacing: var(--tracking-heading); }
.t-h3         { font: 400 var(--type-h3)/var(--leading-title) var(--font-display); letter-spacing: var(--tracking-title); }
.t-h3-card    { font: 400 var(--type-h3-card)/1.26 var(--font-display); letter-spacing: var(--tracking-title); }
.t-h4         { font: 600 var(--type-h4)/1.5 var(--font-ui); }

.t-lead       { font: 400 var(--type-lead)/var(--leading-body) var(--font-ui); color: var(--color-text-body); max-width: 47ch; }
.t-body-lg    { font: 400 var(--type-body-lg)/var(--leading-loose) var(--font-ui); color: var(--color-text-muted); max-width: var(--measure); }
.t-body       { font: 400 var(--type-body)/var(--leading-body) var(--font-ui); color: var(--color-text-muted); max-width: var(--measure); }
.t-body-sm    { font: 400 var(--type-body-sm)/1.65 var(--font-ui); color: var(--color-text-muted); }
.t-label      { font: 500 var(--type-label)/1.4 var(--font-ui); color: var(--color-text-secondary); }
.t-caption    { font: 400 var(--type-caption)/1.5 var(--font-ui); color: var(--color-text-quiet); }

/* ProseList. Added 2026-08-26 with Ed's agreement, because base.css strips
   every list to no bullets and the legal pages, /resources and the FAQ blocks
   all need real lists — semantic ones, so a screen reader announces "list,
   seven items" rather than reading seven unrelated paragraphs.

   The marker is a diamond rather than a disc: the diamond is already the
   site's motif, and at this size a rotated square reads as deliberate where a
   disc reads as a browser default. Drawn with ::marker's own content on the
   ordered variant and a pseudo-element on the unordered one, because ::marker
   accepts no transform. */
.t-list       { display: grid; gap: var(--space-3); max-width: var(--measure); font: 400 var(--type-body)/var(--leading-body) var(--font-ui); color: var(--color-text-muted); }
.t-list > li  { position: relative; padding-inline-start: var(--space-5); list-style: none; }
.t-list > li::before {
  content: ""; position: absolute; inset-inline-start: var(--space-1);
  inset-block-start: .62em; width: 5px; height: 5px;
  background: var(--color-brand); transform: rotate(45deg);
}
/* Ordered: the number carries the meaning, so it is shown rather than a mark.
   Tabular figures keep two-digit items aligned with single-digit ones. */
.t-list-ordered { counter-reset: t-list; }
.t-list-ordered > li { counter-increment: t-list; }
.t-list-ordered > li::before {
  content: counter(t-list) ".";
  position: absolute; inset-inline-start: 0; inset-block-start: 0;
  width: auto; height: auto; background: none; transform: none;
  font-variant-numeric: tabular-nums; color: var(--color-brand);
}
/* Nested list: one level only, and it steps in rather than restarting the
   visual language. */
.t-list .t-list { margin-block-start: var(--space-3); gap: var(--space-2); }

/* SpecTable. Added 2026-08-26 with Ed's agreement. A three-column factual
   table — the cookie list is the first use.

   Below 560px it stops being a table and becomes stacked labelled rows: the
   column headings are repeated per cell from data-label, because three
   columns of cookie names on a 320px phone is either a sideways scroll or
   unreadable type, and neither is acceptable on a page someone reads to
   understand what is being collected about them.

   The <table> element and its header cells stay in the markup at every width,
   so the change is presentational only and assistive technology still gets a
   real table. */
.spec-table { width: 100%; border-collapse: collapse; font: 400 var(--type-body-sm)/1.6 var(--font-ui); color: var(--color-text-muted); }
.spec-table caption { text-align: start; padding-block-end: var(--space-3); font: 500 var(--type-label)/1.4 var(--font-ui); color: var(--color-text-secondary); }
.spec-table th,
.spec-table td { text-align: start; vertical-align: top; padding: var(--space-3) var(--space-4) var(--space-3) 0; border-block-end: 1px solid var(--color-border-soft); }
.spec-table thead th { font: 400 var(--type-eyebrow)/1.3 var(--font-ui); letter-spacing: var(--tracking-eyebrow); text-transform: uppercase; color: var(--color-text-quiet); border-block-end-color: var(--color-border); }
.spec-table tbody th { font: 500 var(--type-body-sm)/1.6 var(--font-ui); color: var(--color-text-primary); }
/* InlineCode. A cookie name, a domain, a config key — a literal string the
   reader may need to match character for character. Pulled out of .spec-table
   2026-08-26 because the same treatment is needed in prose, and because
   inheriting the surrounding colour put muted text on the sand chip at 4.48:1,
   two hundredths under AA. It now sets its own colour, which passes on every
   light surface the site has. */
.t-code { font: 500 13px/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; background: var(--color-surface-sand); color: var(--color-text-primary); padding: 1px var(--space-1); }
.spec-table td:last-child,
.spec-table th:last-child { padding-inline-end: 0; }
/* data-compact: the last column holds a short value — a duration, a count —
   so it should not be broken across two lines to give the middle column room
   it does not need. Without this, "12 months" wraps. Only ever on a column
   whose longest value is a few characters. */
.spec-table[data-compact] td:last-child,
.spec-table[data-compact] thead th:last-child { white-space: nowrap; }

@media (max-width: 560px) {
  .spec-table thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
  .spec-table tr { display: grid; gap: var(--space-1); padding-block: var(--space-4); border-block-end: 1px solid var(--color-border); }
  .spec-table tr:last-child { border-block-end: none; }
  .spec-table th,
  .spec-table td { display: block; padding: 0; border: none; }
  .spec-table tbody th { font: 500 var(--type-body)/1.5 var(--font-ui); }
  /* Stacked, a long literal has no column to squeeze it, so it may break
     here. Not above: in table layout a break rule collapses the column's
     min-content width and the browser then breaks a name that would have
     fitted — ak_consent came out as "ak_consen / t". */
  .spec-table .t-code { overflow-wrap: anywhere; }
  .spec-table td::before {
    content: attr(data-label); display: block;
    font: 400 var(--type-eyebrow)/1.3 var(--font-ui);
    letter-spacing: var(--tracking-eyebrow); text-transform: uppercase;
    color: var(--color-text-quiet); margin-block-end: 1px;
  }
}

/* Breadcrumb. Added 2026-08-27 at Ed's request, once the site had a route with
   a parent other than home: /home-education-resources/know-your-why.

   Shown ONLY where a page sits under something other than the home page. On a
   top-level page the header already says where you are, and a breadcrumb
   reading "Home / About" is furniture rather than navigation.

   Not 12px caps, deliberately — that is the Eyebrow's reserved treatment and a
   breadcrumb set that way reads as a section label. Sentence case at body-small
   instead, quiet, with the current page in the primary colour and not a link.

   The separator is the site's diamond, drawn as a pseudo-element on every item
   but the first, in the same colour as the text so the whole line reads as one
   thing. A real <ol> so a screen reader announces the depth, `aria-label` on
   the nav so it is announced as a breadcrumb, and `aria-current="page"` on the
   last item.

   BreadcrumbList structured data comes in Phase 4 and will read this markup. */
.crumbs { font: 400 var(--type-body-sm)/1.5 var(--font-ui); color: var(--color-text-quiet); }
.crumbs ol { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2) var(--space-3); }
.crumbs li { display: flex; align-items: center; gap: var(--space-3); min-width: 0; }
.crumbs li + li::before {
  content: ""; flex: none; width: 4px; height: 4px;
  background: currentColor; transform: rotate(45deg); opacity: .55;
}
/* Underlined at rest, not only on hover. A breadcrumb whose links look like
   plain text is a breadcrumb nobody clicks, and hover is not a state a
   touchscreen has. The current page is the only item without an underline,
   which is also the only item that is not a link — so the underline is doing
   the same job for the eye and for the pointer. */
.crumbs a { color: inherit; text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: .22em; transition: color var(--hover-duration) ease; }
.crumbs a:hover { color: var(--color-brand); }
.crumbs [aria-current="page"] { color: var(--color-text-primary); }

/* QA. Added 2026-08-27 for /home-education-questions, and the resources page's
   Questions section should move onto it next.

   A single column with a hairline above each question, rather than a grid: an
   answer here is three or four sentences that has to survive being lifted out
   of context and quoted, so it wants a reading measure and room to breathe more
   than it wants density. The hairline is what makes eight of them scannable
   without turning any of them into an accordion — which is on the forbidden
   list, and rightly: a question whose answer is hidden cannot be found by
   search, by an assistant, or by anyone using find-on-page.

   The question is a real <h3> and the answer real paragraphs, so the structured
   data can be parsed straight out of the markup rather than restated. */
.qa-list { display: grid; gap: var(--space-6); }
.qa {
  display: grid; gap: var(--space-3);
  max-width: var(--measure);
  padding-block-start: var(--space-6);
  border-block-start: 1px solid var(--color-border);
}
.qa:first-child { padding-block-start: 0; border-block-start: none; }
/* The "where to check" line under an answer that points somewhere
   authoritative rather than answering from here. */
.qa-source { font-size: var(--type-body-sm); line-height: 1.6; color: var(--color-text-quiet); }

/* Eyebrow. 12px uppercase, .2em tracking. The only place 12px is
   permitted, and only in caps. */
.t-eyebrow    { font: 400 var(--type-eyebrow)/1.3 var(--font-ui); letter-spacing: var(--tracking-eyebrow); text-transform: uppercase; color: var(--color-brand); }
.t-eyebrow-quiet { color: var(--color-text-quiet); }
.t-eyebrow-sand  { color: var(--color-text-on-sand-quiet); }
.t-eyebrow-dark  { color: var(--color-brand-on-dark); }

/* Serif aside. Anna's voice in the margin: italic Newsreader,
   never more than three lines. */
.t-aside      { font: 400 italic 17px/1.55 var(--font-display); color: var(--color-text-on-sand-quiet); max-width: var(--measure-tight); }

/* Pull quote. Sits alone in a brand or mint band, centred,
   never attributed — it is Anna speaking. */
.t-quote      { font: 300 italic var(--type-quote)/1.28 var(--font-display); letter-spacing: -.015em; max-width: var(--measure-quote); margin-inline: auto; text-align: center; }

.measure       { max-width: var(--measure); }
.measure-tight { max-width: var(--measure-tight); }

/* Emphasis inside a display heading. One clause only. */
.t-em { font-style: italic; color: var(--color-brand); }

/* Inverse text scale for dark and brand surfaces. */
.surface-dark .t-body, .surface-dark .t-body-lg,
.surface-brand .t-body, .surface-brand .t-body-lg { color: var(--color-text-on-dark); }
.surface-dark .t-caption, .surface-brand .t-caption { color: var(--color-text-on-dark); }
/* Mint and sand overrides — measured, not assumed. */
/* .t-body-sm and .t-caption were missing here. Their default colours measure
   4.38:1 and 4.19:1 on the mint ground, both under AA for text at that size —
   a latent failure waiting for the first small paragraph placed on mint.
   Found and fixed 2026-08-25. */
.surface-mint .t-body, .surface-mint .t-body-lg,
.surface-mint .t-body-sm, .surface-mint .t-caption { color: var(--color-text-on-mint); }
.surface-sand .t-body, .surface-sand .t-body-lg { color: var(--color-text-on-sand); }
/* ProseList takes the same per-surface overrides as body copy. Without them a
   list on the sand band inherits --color-text-muted at 4.48:1, which is the
   same two-hundredths AA failure .t-code had — sand is warm enough that the
   cool muted grey stops clearing the threshold on it. Added 2026-08-26 when
   the guide licence on /terms landed on a sand section. */
.surface-sand .t-list { color: var(--color-text-on-sand); }
.surface-mint .t-list { color: var(--color-text-on-mint); }


/* ============================================================
   C · ACTIONS
   One primary button per viewport. Everything else is secondary
   or a text action.
   ============================================================ */

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--space-2);
  font: 500 var(--type-body)/1 var(--font-ui);
  padding: var(--space-4) 26px;
  border: 1px solid transparent; border-radius: var(--radius-pill);
  cursor: pointer; text-decoration: none; white-space: nowrap;
  min-height: 44px;
  transition: background var(--hover-duration) ease, border-color var(--hover-duration) ease;
}
.btn-primary { background: var(--color-brand); color: var(--color-text-inverse); }
.btn-primary:hover, .btn-primary:active { background: var(--color-brand-hover); color: var(--color-text-inverse); }

/* Ink primary. Used where the surface is already brand-adjacent
   (sand panels) and teal would compete. */
.btn-ink { background: var(--color-surface-dark); color: var(--color-text-inverse); }
.btn-ink:hover { background: var(--color-brand); color: var(--color-text-inverse); }

.btn-secondary { background: transparent; color: var(--color-text-primary); border-color: var(--color-border-strong); }
.btn-secondary:hover { border-color: var(--color-text-primary); background: rgba(22, 35, 42, .05); }

.btn[disabled], .btn-disabled { background: var(--color-disabled-bg); color: var(--color-disabled-text); cursor: not-allowed; }
.btn[aria-busy='true'] { cursor: progress; }

/* On dark: the mint fill becomes primary, white outline secondary. */
.surface-dark .btn-primary, .surface-brand .btn-primary { background: var(--color-brand-soft); color: var(--color-text-primary); }
.surface-dark .btn-primary:hover, .surface-brand .btn-primary:hover { background: var(--color-surface); color: var(--color-text-primary); }
.surface-dark .btn-secondary, .surface-brand .btn-secondary { color: var(--color-text-inverse); border-color: var(--color-border-on-dark); }
.surface-dark .btn-secondary:hover { border-color: var(--color-text-inverse); background: rgba(255, 255, 255, .08); color: var(--color-text-inverse); }

/* Compact button — header CTA only. min-height was 40px, under the system's
   own 44px target floor; raised 2026-08-24. */
.btn-sm { padding: 12px 20px; font-size: var(--type-body-sm); min-height: 44px; }

/* Full-width on mobile. */
@media (max-width: 599px) { .btn-block-mobile { width: 100%; } }

/* Text action. Rule under the words only; the arrow stays clear of
   it and shifts 2px on hover. */
.action { display: inline-flex; align-items: baseline; gap: var(--space-2); font: 400 var(--type-body-sm)/1.4 var(--font-ui); color: var(--color-text-primary); text-decoration: none; }
.action > span:first-child { border-bottom: 1px solid currentColor; padding-bottom: var(--space-1); }
.action > .arrow { color: var(--color-brand); transition: transform var(--hover-duration) ease; }
.action:hover { color: var(--color-brand); }
.action:hover > .arrow { transform: translateX(var(--hover-shift)); }
.surface-dark .action, .surface-brand .action { color: var(--color-brand-on-dark); }
.surface-dark .action:hover { color: var(--color-text-inverse); }
.surface-dark .action > .arrow { color: currentColor; }

/* Underlined inline link — used beside a primary button. */
.link-underline { font-size: var(--type-body); color: var(--color-text-primary); text-decoration: none; border-bottom: 1px solid var(--color-border-strong); padding-block-end: 2px; }
.link-underline:hover { color: var(--color-brand); border-bottom-color: var(--color-brand); }

/* ============================================================
   TOUCH TARGETS — Phase 8, 2026-08-27
   ------------------------------------------------------------
   Under a coarse pointer only. The desktop design is not touched: every rule
   in here is inside `@media (pointer: coarse)`, which is a pointer query and
   not a width breakpoint — there are still four of those.

   Why it is needed. `.action`, `.link-quiet` and `.link-underline` are
   standalone controls: a footer column, a contents list, a row of next steps.
   They are 21–25px tall. That clears WCAG 2.5.8 AA, which asks for 24×24, and
   the brief explicitly asked for 44 anyway — "including the ones that currently
   pass on 24px because that is only the accessibility floor rather than a usable
   target." tools/touch-audit.mjs found 87 of them across three devices.

   Why the containers change too, and this is the part worth understanding: in a
   list with a 12px gap you CANNOT have 44px hit areas without them overlapping,
   and overlapping hit areas are worse than small ones — you tap and the wrong
   link opens. 24px box + 12px gap is a 36px pitch; 44px targets need a 44px
   pitch. So the box grows to 44 and the gap goes to zero, which leaves the
   visible text pitch at 44px instead of 36 and the hit areas exactly adjacent.
   Adjacent is fine: WCAG's spacing rule is an ALTERNATIVE to the size rule, not
   an addition to it, and these now pass on size.

   Inline links inside running prose are deliberately untouched. Making a link
   in the middle of a sentence 44px tall puts holes in the paragraph, and 2.5.8
   exempts them for that reason.
   ============================================================ */
@media (pointer: coarse) {

  .action, .link-quiet {
    min-height: 44px;
    display: inline-flex; align-items: center;
  }

  /* `.link-underline` draws its underline with `border-bottom`, which sits at
     the bottom of the box — grow the box and the rule detaches from the words
     and floats 20px below them. So on touch it becomes a real text underline
     instead, matching what `.crumbs a` already does in this system, and the
     border is switched off. Same appearance, attached to the text at any box
     height. */
  .link-underline {
    min-height: 44px;
    display: inline-flex; align-items: center;
    border-bottom: none;
    padding-block-end: 0;
    text-decoration: underline;
    text-decoration-thickness: 1px;
    /* .26em rather than the .22em `.crumbs` uses. A border-bottom sat below the
       whole line box and cleared every descender for free; a text underline
       does not, and at .22em it ran through the g in "How long we keep it" and
       both descenders in "Changes to this page". skip-ink is stated rather than
       left to the default, because it is the thing doing the work. */
    text-underline-offset: .26em;
    text-decoration-skip-ink: auto;
  }
  .link-underline:hover { border-bottom-color: transparent; }

  /* An ordered list's marker is absolutely positioned at the top of its `<li>`.
     Grow the link inside to 44px and centre its text, and the number stays at
     the top while the words move to the middle — so the two stop lining up.
     Centre the marker to match. */
  .t-list-ordered > li:has(> .link-underline)::before {
    inset-block-start: 50%;
    transform: translateY(-50%);
  }

  /* Gaps to zero where the boxes now provide the pitch themselves. */
  .footer-col { gap: 0; }

  /* "Terms" in the footer base row measured 40px wide — four short of the
     minimum, because the word is short. A minimum on the inline axis too. */
  .link-quiet { min-width: 44px; justify-content: center; }
  .footer-base .link-quiet { justify-content: flex-end; }

  /* The breadcrumb's parent link was 22px tall. Its separators are drawn by
     `li + li::before`, which is unaffected by the link's own height. */
  .crumbs a { min-height: 44px; display: inline-flex; align-items: center; }
  .crumbs li { display: flex; align-items: center; }

  /* The wordmark measured 153×42 — two pixels short, which is exactly the kind
     of thing nobody finds by eye. */
  .wordmark { min-height: 44px; }
}

/* Quiet inline link — hero social row, footer columns. */
.link-quiet { font-size: var(--type-body-sm); color: var(--color-text-secondary); text-decoration: none; }
.link-quiet:hover { color: var(--color-brand); }

.social-link {
  display: grid; place-items: center; width: 44px; height: 44px;
  background: var(--color-surface-dark); color: var(--color-text-inverse);
  border: 1px solid var(--color-surface-dark); border-radius: var(--radius-md);
  text-decoration: none;
}
.social-link:hover { background: var(--color-surface); color: var(--color-text-primary); }


/* ============================================================
   D · NAVIGATION
   ============================================================ */

.site-header {
  position: sticky; top: 0; z-index: var(--z-header);
  background: var(--color-header-veil);
  backdrop-filter: blur(12px);
  border-bottom: 1px solid rgba(22, 35, 42, .1);
}
/* Sticky, except when the viewport is too short to afford it.

   The header is 73px. On an 800px screen that is 9% and it earns its keep. At
   400% browser zoom the CSS viewport is 320×200, and 73px becomes 37% of the
   screen — more than a third of everything a person can see, permanently, on
   every page. The fluid sweep's zoom pass found it on all thirteen routes.

   A height query rather than a width one, because this is a height problem: a
   landscape phone at 390px tall gives the header 19% and sticky is still right
   there. 320px is the threshold, matching the site's own minimum width, and
   below it the header scrolls away like any other block. Nothing else changes —
   at that size a reader is scrolling regardless.

   This is the site's only height-based media query. The four width breakpoints
   are unaffected and there are still four of them.

   It sits AFTER the base rule, not before. Written above it first, and the
   later `position: sticky` at equal specificity simply won — the query matched
   and did nothing, which is the quietest kind of wrong. */
@media (max-height: 320px) {
  .site-header { position: static; backdrop-filter: none; }
}

.site-header-inner {
  max-width: var(--content-max); margin-inline: auto; padding-inline: var(--gutter);
  height: var(--header-height);
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-5);
}

/* Wordmark. Type only — there is no logo mark. */
.wordmark { display: flex; flex-direction: column; gap: 1px; text-decoration: none; color: var(--color-text-primary); }
.wordmark-name { font: 400 21px/1 var(--font-display); letter-spacing: -.01em; }
.wordmark-descriptor { font-size: var(--type-eyebrow); letter-spacing: .19em; text-transform: uppercase; color: var(--color-text-descriptor); }

.site-nav { display: flex; align-items: center; gap: clamp(18px, 2.4vw, 34px); }
.nav-link { font-size: var(--type-body-sm); color: var(--color-text-secondary); text-decoration: none; }
.nav-link:hover { color: var(--color-text-primary); }
.nav-link[aria-current='page'] { color: var(--color-text-primary); border-bottom: 1px solid var(--color-brand); padding-block-end: 2px; }

@media (max-width: 899px) { .site-nav { display: none; } }

/* Mobile menu toggle. 44px target, hairline pill, two rules. */
.menu-toggle {
  width: 44px; height: 44px; display: none; place-items: center;
  background: none; border: 1px solid rgba(22, 35, 42, .16);
  border-radius: var(--radius-pill); cursor: pointer;
}
.menu-toggle > span { display: block; width: 17px; height: 9px; border-top: 1.4px solid var(--color-text-primary); border-bottom: 1.4px solid var(--color-text-primary); }
@media (max-width: 899px) { .menu-toggle { display: grid; } }

/* Mobile drawer. Serif links at 24px, hairline separated, primary
   button last. Not a full-screen overlay. */
.menu-drawer {
  border-top: 1px solid rgba(22, 35, 42, .1);
  background: var(--color-page);
  padding: var(--space-3) var(--gutter) 26px;
  display: flex; flex-direction: column; gap: 2px;
}
.menu-drawer > a {
  font: 400 24px/1.2 var(--font-display); color: var(--color-text-primary);
  text-decoration: none; padding-block: 15px;
  border-bottom: 1px solid var(--color-border-soft);
}
.menu-drawer > a:last-of-type { border-bottom: none; }
.menu-drawer > .btn { margin-block-start: var(--space-4); }

/* Footer. Four columns collapsing to one, author module first. */
.site-footer { position: relative; overflow: hidden; background: var(--color-surface); border-top: 1px solid rgba(22, 35, 42, .1); }
.site-footer-inner { position: relative; max-width: var(--content-max); margin-inline: auto; padding: clamp(40px, 5vw, 64px) var(--gutter); }
.footer-cols { display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: var(--space-6); align-items: start; }
.footer-col { display: flex; flex-direction: column; gap: var(--space-3); }
.footer-col-head { font-size: var(--type-eyebrow); letter-spacing: var(--tracking-label); text-transform: uppercase; color: var(--color-disabled-text); margin-block-end: 2px; }
.footer-base { margin-block-start: 44px; padding-block-start: 22px; border-top: 1px solid var(--color-border-soft); display: flex; flex-wrap: wrap; gap: var(--space-3) 28px; align-items: baseline; justify-content: space-between; }

/* Author module. The photo slot is .avatar — see Group F.

   Stacked, not side by side. It was a row until 2026-08-27, and the fluid sweep
   caught why that was wrong: `.footer-cols` tracks are `minmax(210px, 1fr)`, so
   whenever the footer runs three or four columns the track lands near 210px, the
   64px avatar and its gap take 80 of that, and the bio wrapped to four or five
   lines in a 132px ribbon with two hundred pixels of empty footer beside it.

   It is a container-width problem, not a viewport one — the same viewport gives
   a different track width depending on how many columns fit — so no viewport
   media query fixes it cleanly and the site has only four breakpoints by rule.
   Stacking fixes it at every width without a query: the bio gets the whole
   track (3 lines at 768, 2 at 1280), and the module reads as a byline. */
.author { display: flex; flex-direction: column; gap: var(--space-3); align-items: flex-start; }
.author-name { font: 400 22px/1.2 var(--font-display); margin-block-end: var(--space-2); }


/* ============================================================
   E · CARDS
   No card lifts, no card has a shadow. Hover moves the border
   to brand and nothing else.
   ============================================================ */

/* Intent card. The homepage routing device: diamond plate +
   state eyebrow + question headline + reassurance + action.
   Headline and body carry min-heights so a row of four aligns. */
.card-intent {
  display: flex; flex-direction: column;
  background: var(--color-surface); border: 1px solid var(--color-border);
  padding: clamp(22px, 2.4vw, 30px);
  text-decoration: none; color: var(--color-text-primary);
  transition: border-color var(--hover-duration) ease;
}
.card-intent:hover { border-color: var(--color-brand); color: var(--color-text-primary); }
.card-intent-head { display: flex; align-items: center; gap: 14px; margin-block-end: 18px; }
.card-intent-state { font-size: var(--type-eyebrow); letter-spacing: .17em; text-transform: uppercase; color: var(--color-text-quiet); }
.card-intent-title { font: 400 var(--type-h3-card)/1.26 var(--font-display); letter-spacing: var(--tracking-title); margin-block-end: var(--space-3); min-height: 2.55em; }
.card-intent-body { font-size: 15px; line-height: 1.66; color: var(--color-text-muted); margin-block-end: 22px; }
.card-intent-foot { margin-block-start: auto; display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-3); font-size: var(--type-body-sm); color: var(--color-text-primary); }
.card-intent-foot > span:first-child { border-bottom: 1px solid currentColor; padding-bottom: var(--space-1); }
.card-intent-foot > .arrow { color: var(--color-brand); }

/* Multi-route variant. Added 2026-08-26 with Ed's agreement, for the router on
   /home-education-resources: the same four-state card, but each stage points at
   two or three places — a resource, an episode, an article — instead of one.

   That forces a <div> rather than an <a>, because an anchor cannot contain
   anchors. So the whole-card hover comes off: a border that lights up on a
   block that is not itself clickable is a promise the card does not keep. The
   links inside carry their own hover instead.

   Secondary routes use the same gating as every other optional link on the
   site, so a stage with no episode yet shows only its resource and the card
   still reads as finished rather than half-built. */
.card-intent-links { margin-block-start: auto; display: grid; gap: var(--space-3); }
.card-intent-links > .action { font-size: var(--type-body-sm); }
.card-intent-links > .link-quiet { font-size: var(--type-body-sm); }

/* Plain card. Bordered white block for anything not routed. */
.card { background: var(--color-surface); border: 1px solid var(--color-border); padding: clamp(22px, 2.4vw, 30px); transition: border-color var(--hover-duration) ease; }
.card:hover { border-color: var(--color-brand); }
.card-flush { background: var(--color-page); padding: var(--space-5); }

/* Fill modifier. Added 2026-08-26 for the product grid on
   /home-education-resources, where three cards of different length left their
   format lines and prices at three different heights — the eye reads that as
   carelessness before it reads the copy.

   Grid items already stretch to equal height; what was missing was any way to
   pin the meta to the bottom of that height. So the card becomes a flex column,
   its stack grows to fill, and the format line takes an auto top margin. The
   bullets stay at the top, the meta lines up across all three, and no
   min-height has to be guessed at — which is what makes it survive a copy
   change.

   PENDING ED'S AGREEMENT. One class, three declarations, reversible by deleting
   this block and the class from three cards.

   Static modifier for either card. Both .card and .card-intent light their
   border on hover because both are normally anchors. A card that holds several
   links, or none, is a <div> — and a border that lights up on a block you
   cannot click is a promise the card does not keep. Must sit after both
   :hover rules: the specificity is identical, so source order decides.
   Added 2026-08-26. */
.is-static { cursor: default; }
.is-static:hover { border-color: var(--color-border); }
.card-fill { display: flex; flex-direction: column; }
.card-fill > .stack { flex: 1 1 auto; display: flex; flex-direction: column; }
.card-fill .price-note { margin-block-start: auto; padding-block-start: var(--space-4); }

/* PriceLine. Added 2026-08-26 with Ed's agreement — the site has never shown a
   price. Newsreader, because a price is a figure the eye should land on and the
   display face is what the site uses for figures elsewhere (the stat panel, the
   timeline numerals). Tabular figures so a row of three cards aligns.

   No struck-through "was", no sale colour and no modifier for one: the research
   round found every competitor permanently discounted, which teaches a visitor
   to wait. There is deliberately nothing here to build discount theatre out of.
   The format line beside it is .t-caption, which already exists. */
.price { font: 400 clamp(21px, 1.9vw, 25px)/1 var(--font-display); letter-spacing: var(--tracking-title); color: var(--color-text-primary); font-variant-numeric: tabular-nums; }
.price-note { font-size: var(--type-caption); color: var(--color-text-quiet); }

/* Proof item. Hairline above, diamond plate, serif label, one
   paragraph. Three across: Parent / Educator / Founder. */
.proof { border-top: 1px solid rgba(22, 35, 42, .16); padding-block-start: 22px; }
.proof-head { display: flex; align-items: center; gap: 13px; margin-block-end: var(--space-4); }
.proof-title { font: 400 21px/1.2 var(--font-display); letter-spacing: -.01em; }
.proof-body { font-size: var(--type-body); line-height: var(--leading-body); color: var(--color-text-muted); }

/* Belief card. 16:9 image, numeral, headline bottom-aligned to a
   fixed min-height, body on a 34ch measure. Four in two pairs. */
.belief-media { position: relative; width: 100%; aspect-ratio: 16/9; margin-block-end: var(--space-5); overflow: hidden; background: var(--color-tint-mint); }
/* Same treatment .frame gives its image — .belief-media had no img rule, so a
   photograph placed in a belief card sat unsized instead of covering. */
.belief-media > img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.belief-num { font-size: var(--type-eyebrow); letter-spacing: var(--tracking-eyebrow); color: var(--color-brand); margin-block-end: var(--space-2); }
.belief-title { font: 400 clamp(20px, 1.75vw, 23px)/1.24 var(--font-display); letter-spacing: -.015em; margin-block-end: var(--space-3); min-height: 2.6em; display: flex; align-items: flex-end; }
.belief-body { font-size: var(--type-body); line-height: var(--leading-body); color: var(--color-text-on-mint); max-width: 34ch; min-height: 6.72em; }

/* Hub row. The destinations table: hairline grid, page-coloured
   cells that go white on hover. Icon, title, one line, arrow. */
.hub-row {
  background: var(--color-page); padding: 26px var(--space-5);
  text-decoration: none; color: var(--color-text-primary);
  display: flex; align-items: flex-start; justify-content: space-between; gap: 18px;
  transition: background var(--hover-duration) ease;
}
.hub-row:hover { background: var(--color-surface); color: var(--color-text-primary); }
.hub-icon { flex: 0 0 auto; display: grid; place-items: center; width: 26px; height: 26px; margin-block-start: 6px; color: var(--color-brand); }
.hub-text { flex: 1 1 auto; }
.hub-title { display: block; font: 400 21px/1.2 var(--font-display); margin-block-end: 7px; }
.hub-desc { display: block; font-size: var(--type-body-sm); line-height: 1.6; color: var(--color-text-muted); max-width: 34ch; }
.hub-arrow { font-size: var(--type-body-lg); color: var(--color-brand); }

/* Timeline step. Serif numeral in quiet brand + title + body,
   hairline between steps. Used for the story sequence. */
.step { display: grid; grid-template-columns: auto 1fr; gap: clamp(var(--space-4), 2vw, 30px); padding-block: 30px; border-top: 1px solid var(--color-border); }
.step:first-child { padding-block-start: 0; border-top: none; }
.step-num { font: 300 var(--type-step)/1 var(--font-display); color: var(--color-brand-quiet); }
.step-title { font: 400 var(--type-h3)/var(--leading-title) var(--font-display); margin-block-end: 11px; }
.step-body { font-size: var(--type-body); line-height: var(--leading-loose); color: var(--color-text-muted); max-width: 52ch; }

/* Sticky story heading — engages at 900px and up only. */
.story-aside { position: static; }
@media (min-width: 900px) { .story-aside { position: sticky; top: 112px; } }

/* Stat panel. Two figures on a sand ground with an italic close.
   Extra-light Newsreader numerals; never more than two. */
.stat-panel { flex: 1 1 280px; max-width: 420px; background: var(--color-surface-sand); padding: clamp(26px, 3vw, 36px) clamp(22px, 2.6vw, 32px); display: flex; flex-direction: column; justify-content: space-between; }
.stat { display: flex; align-items: baseline; gap: var(--space-4); }
.stat-figure { font: 200 var(--type-numeral)/.85 var(--font-display); letter-spacing: -.03em; color: var(--color-text-primary); }
.stat-label { font-size: var(--type-body); line-height: 1.5; color: var(--color-text-body); max-width: 16ch; }
.stat + .stat { padding-block-start: var(--space-5); }
/* Wide variant. The panel was designed as a 420px sidebar; as a full-width
   closing strip under a proof row it holds its two figures side by side in a
   .grid-2, so the stacked spacing and the max-width both come off. */
.stat-panel-wide { max-width: none; }
.stat-panel-wide .stat + .stat { padding-block-start: 0; }
.stat-divide { padding-block-end: var(--space-5); border-bottom: 1px solid rgba(22, 35, 42, .14); }


/* ============================================================
   F · MEDIA
   Photographs are square-cornered, documentary, never rounded and
   never filtered. Five ratios only.
   ============================================================ */

.frame            { position: relative; width: 100%; overflow: hidden; background: var(--color-surface-sand); }
.frame-portrait   { aspect-ratio: 4/5; max-height: 640px; }   /* hero, author */
.frame-landscape  { aspect-ratio: 3/2; }                       /* story, in-page */
.frame-wide       { aspect-ratio: 16/9; }                      /* belief cards */
.frame-square     { aspect-ratio: 1/1; }                       /* social, grid */
.frame-social     { aspect-ratio: 1200/630; }                  /* og:image */
.frame > img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }

/* Photo placeholder. Every unfilled slot states its own brief, so a page in
   review reads as a set of commissions rather than a set of blank rectangles.
   Renders nothing once a real <img> is present, so it retires itself. */
.photo-brief {
  position: absolute; inset: 0; z-index: var(--z-body);
  display: flex; flex-direction: column; justify-content: space-between;
  padding: clamp(var(--space-4), 2vw, var(--space-5));
  font: 400 var(--type-caption)/1.55 var(--font-ui);
  color: var(--color-text-on-sand-quiet);
}
:is(.frame, .belief-media):has(> img) .photo-brief { display: none; }
.photo-brief-label { font-size: var(--type-eyebrow); letter-spacing: var(--tracking-label); text-transform: uppercase; }
.photo-brief-consent { color: var(--color-error); }

.figure-caption { margin-block-start: var(--space-3); font-size: var(--type-caption); color: var(--color-text-quiet); letter-spacing: .02em; }

/* Play target. The one thing that makes a 16:9 Frame read as something to
   watch rather than something to look at. It is the existing Diamond plate
   holding the existing 1.6px icon, positioned inside a Frame — no new shape,
   no new colour, no red. Decorative: the whole card is the link, so this is
   always aria-hidden and never the click target itself.
   Only on a frame that is, or will be, a video. */
.play {
  position: absolute; z-index: var(--z-body);
  inset-block-end: var(--space-4); inset-inline-end: var(--space-4);
  pointer-events: none;
  transition: transform var(--hover-duration) ease;
}
a:hover .play, a:focus-visible .play { transform: scale(1.06); }

/* Round avatar. The only circular crop on the site: every other photograph
   stays square-cornered. Two permitted uses, both 64px — the Quote band (add
   .avatar-center) and the footer author module. It replaced .author-photo on
   2026-08-25, so the two round slots are one component rather than two
   near-identical ones. Do not reach for it in the hero or the belief cards. */
.avatar { flex: 0 0 auto; position: relative; display: block; width: 64px; height: 64px; overflow: hidden; border-radius: var(--radius-circle); background: var(--color-surface-sand); }
.avatar-center { margin-inline: auto; }
.avatar > img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }

/* Resource thumbnail. A 72px cover of a printable resource, shown beside a
   link to it — used at the foot of an answer on /home-education-questions,
   where a full-width product image would turn a page of honest answers into
   a shop. Added 2026-08-27, agreed with Ed before it was written.

   Portrait 4:5 because that is the cover's own proportion; square-cornered
   like every photograph on the site; the paper shadow because it is a picture
   of a sheet of paper. Never a link itself — the link beside it names the
   resource, and two links to one place is a duplicate for a screen reader. */
.res-thumb {
  flex: 0 0 auto; display: block; width: 72px; aspect-ratio: 4/5;
  object-fit: cover; background: var(--color-surface-sand);
  box-shadow: var(--shadow-paper);
}

/* Diamond plate — the Termly motif. An icon container, nothing
   more. Limited to Termly-adjacent areas, intent routing and proof
   items. Never the default container for every icon. */
/* Every tint plate is light, so the icon colour cannot be inherited. On
   .surface-dark or .surface-brand currentColor is white, which renders the
   1.6px stroke invisible on its own plate. */
.diamond { display: grid; place-items: center; flex: 0 0 auto; width: 44px; height: 44px; border-radius: var(--radius-diamond); transform: rotate(45deg); color: var(--color-text-primary); }
.diamond > * { transform: rotate(-45deg); display: grid; place-items: center; }
.diamond-sm { width: 42px; height: 42px; }
.diamond-mint    { background: var(--color-tint-mint); }
.diamond-teal    { background: var(--color-tint-teal); }
.diamond-mustard { background: var(--color-tint-mustard); }
.diamond-coral   { background: var(--color-tint-coral); }
.diamond-sage    { background: var(--color-tint-sage); }

/* Large diamond bleed behind the hero photograph. Decorative. */
.diamond-bleed { position: absolute; aspect-ratio: 1; background: var(--color-tint-sage); border-radius: var(--radius-diamond); transform: rotate(45deg); pointer-events: none; }

/* Icons. 24px viewBox, 1.6px stroke, round caps and joins,
   currentColor. No filled icons, no icon fonts. */
.icon { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }
.icon-sm { width: 18px; height: 18px; }


/* ============================================================
   G · OBJECTS
   Three composed objects. Each appears once per page at most.
   ============================================================ */

/* Guide cover. A 5:7 dark board with a sand shadow-sheet behind,
   both slightly rotated. The only rotated object on the site and
   the only place --shadow-float is used. */
/* 80vw was the wrong second term and the fluid sweep found both symptoms.
   A grid track is not the viewport: inside a two-column `.grid-split` or
   `.hec-grid` the track lands near 302px while 80vw is still 588px, so the
   330px stack overflowed its own track — by 27px on the Collective page at
   679–689px, where it sat on top of the column beside it, and on /about at
   exactly 736px, where the rotated `.cover-sheet` behind it then reached 2px
   past the viewport and the whole document scrolled sideways.

   `100%` asks the container instead, which is what was meant. The sheet's
   rotated bleed (about 21px to the right) then stays inside the container's
   own gutter at every width. */
.cover-stack { position: relative; width: min(330px, 100%); }
.cover-sheet { position: absolute; inset: 0; transform: rotate(3deg) translate(10px, 8px); background: var(--color-cover-sheet); box-shadow: var(--shadow-paper); }
.cover {
  position: relative; aspect-ratio: 5/7; background: var(--color-surface-dark);
  padding: 34px 30px; display: flex; flex-direction: column; justify-content: space-between;
  box-shadow: var(--shadow-float); transform: rotate(-1.4deg);
}
.cover-kicker { font-size: var(--type-eyebrow); letter-spacing: .22em; text-transform: uppercase; color: var(--color-brand-on-dark); margin-block-end: 22px; }
.cover-title { font: 300 clamp(26px, 3.4vw, 32px)/1.12 var(--font-display); letter-spacing: var(--tracking-heading); color: var(--color-text-inverse); }
.cover-rule { display: block; width: 40px; height: 1px; background: rgba(255, 255, 255, .35); margin-block-end: 14px; }
.cover-author { font: 400 15px/1.4 var(--font-display); color: rgba(255, 255, 255, .82); }
.cover-meta { font-size: var(--type-eyebrow); letter-spacing: .14em; text-transform: uppercase; color: rgba(255, 255, 255, .62); margin-block-start: var(--space-1); }

/* Real screenshot inside the device frame. Present only when
   termly.appScreenshot holds a value; when it does, it replaces the drawn
   interface rather than sitting beside it. Added 2026-08-25. */
.mock-shot { display: block; width: 100%; height: auto; }
.mock-shot:not([hidden]) ~ .mock-screen { display: none; }

/* Landscape aperture. Termly's own screenshots are 569x429, and a 3:2 image
   cannot sit in a 252px portrait phone without either letterboxing or losing
   most of the interface — so when a real screenshot is present the device
   widens and its corners relax. Ed's call, 2026-08-25.

   Keyed on :has() rather than a modifier class deliberately. The frame is
   landscape exactly when there is a landscape screenshot in it: no screenshot
   in the config, or an image that fails to load and is dropped by links.js,
   and the phone with its drawn interface comes straight back. One decision,
   expressed once, and no class to remember to remove later.

   Everything else about the component is untouched — same stage, same
   In development status line, same fade mask, same caption. Only the
   aperture changes, which is why this is a state of ProductMock and not a
   second component. */
.mock-stage:has(.mock-shot:not([hidden])) { max-width: 520px; }
.mock-stage:has(.mock-shot:not([hidden])) .mock-device {
  width: 100%;
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
}

/* Product mock. The only rounded-large object, the only place
   Termly teal appears, and the only place a fade mask is used.
   Always captioned "an early look" — it must never read as shipped. */
.mock-stage { position: relative; width: 100%; max-width: 360px; margin-inline: auto; background: var(--color-mock-stage); border: 1px solid rgba(255, 255, 255, .1); border-radius: var(--radius-lg); padding: clamp(22px, 2.6vw, 30px) 20px 0; overflow: hidden; }
.mock-status { display: flex; align-items: center; gap: 9px; margin-block-end: 18px; font-size: 10.5px; letter-spacing: .15em; text-transform: uppercase; color: var(--color-brand-on-dark); }
.mock-status-dot { width: 7px; height: 7px; border-radius: var(--radius-pill); background: var(--color-brand-on-dark); }
.mock-device { width: 252px; max-width: 100%; margin-inline: auto; padding: var(--space-2) var(--space-2) 0; border-radius: 34px 34px 0 0; background: var(--color-mock-device); border: 1px solid rgba(255, 255, 255, .16); border-bottom: none; box-shadow: var(--shadow-stage); }
/* The screen is a light surface nested inside .surface-dark, which sets
   color: var(--color-text-inverse). Without resetting it here, any component
   that does not declare its own colour (.t-h4, headings) renders white on
   page and disappears. */
/* Padding belongs to the screen. Nesting a card inside it to get an inset
   leaves the 252px device too narrow for a title and a meta line. */
.mock-screen { border-radius: 28px 28px 0 0; overflow: hidden; background: var(--color-page); max-height: 330px; color: var(--color-text-primary); padding: var(--space-4); }
.mock-fade { position: relative; margin-block-start: -56px; padding-block-start: 56px; background: linear-gradient(to bottom, rgb(from var(--color-mock-stage) r g b / 0) 0%, var(--color-mock-stage) 62%); }
.mock-note { padding: 0 var(--space-1) 18px; text-align: center; font-size: var(--type-caption); line-height: 1.45; color: rgba(255, 255, 255, .62); }
.mock-chip { font-size: 10px; border-radius: var(--radius-pill); padding: 5px 9px; white-space: nowrap; background: var(--color-mock-chip); color: var(--color-text-secondary); }
.mock-chip-on { background: var(--color-termly-teal); color: var(--color-text-inverse); }
.mock-tile { display: flex; gap: 11px; padding: 10px; background: var(--color-surface); border: 1px solid rgba(22, 35, 42, .09); border-radius: var(--radius-md); }
.mock-thumb { flex: 0 0 42px; height: 42px; border-radius: var(--space-2); }

/* Letters band. Anna's owned audience. Framed as staying connected,
   never as a competing signup, and never given a primary button
   in the same viewport as the free guide. */
.letters { display: flex; flex-wrap: wrap; gap: clamp(28px, 4vw, 64px); align-items: flex-start; }
.letters-copy { flex: 1 1 340px; }
.letters-form { flex: 1 1 320px; max-width: 420px; }
.letters-row { display: flex; flex-wrap: wrap; gap: 10px; }
/* The input must share the row with the submit — .input is width:100%, which
   without this pushes the button onto its own line at every width. */
.letters-row > .input { flex: 1 1 210px; width: auto; }
.letters-note { margin-block-start: var(--space-3); font-size: var(--type-label); line-height: 1.6; color: var(--color-text-on-sand-quiet); }


/* ============================================================
   H · FORMS
   Guide delivery consent and newsletter consent are separate.
   The newsletter box is never pre-checked.
   ============================================================ */

.field { display: block; margin-block-end: var(--space-4); }
.field > label { display: block; font: 500 var(--type-label)/1.4 var(--font-ui); color: var(--color-text-secondary); margin-block-end: var(--space-2); }
.field-hint { font-size: var(--type-label); line-height: 1.6; color: var(--color-text-quiet); margin-block-start: var(--space-2); }

.input {
  width: 100%; font: 400 var(--type-body)/1.4 var(--font-ui); color: var(--color-text-primary);
  background: var(--color-surface); border: 1px solid var(--color-border-input);
  border-radius: var(--radius-sm); padding: 14px 15px; min-height: 48px;
}
.input::placeholder { color: var(--color-disabled-text); }
.input:focus { border-color: var(--color-focus); outline: 2px solid var(--color-focus-ring); outline-offset: 2px; }
.input[aria-invalid='true'] { border-color: var(--color-error); }
.input[disabled] { background: var(--color-page); color: var(--color-disabled-text); cursor: not-allowed; }

.consent { display: flex; gap: var(--space-3); align-items: flex-start; cursor: pointer; padding-block: var(--space-2); font: 400 var(--type-body-sm)/1.6 var(--font-ui); color: var(--color-text-secondary); }
/* 24px, not 18px: WCAG 2.2 target size (minimum) is 24x24 CSS px.

   It stays 24 rather than growing to 44 under a coarse pointer, and that is a
   decision rather than an oversight. The box is inside its `<label>`, so the
   whole label toggles it — the tappable region containing this checkbox is the
   full-width label, which is comfortably over 44px in both axes. Growing the
   drawn box to 44px would make a tick mark the size of a heading to solve a
   problem the label has already solved. tools/touch-audit.mjs knows this and
   says why. */
.consent input { margin-block-start: 1px; flex: 0 0 24px; width: 24px; height: 24px; accent-color: var(--color-brand); }

.form-message { font: 400 var(--type-body-sm)/1.6 var(--font-ui); }
.form-message-error   { color: var(--color-error); }
.form-message-success { background: var(--color-surface-mint); color: var(--color-text-on-mint); font: 400 19px/1.5 var(--font-display); padding: var(--space-5); }
.form-message-failure { background: var(--color-surface); border: 1px solid var(--color-border); border-left: 3px solid var(--color-error); padding: var(--space-4) 18px; color: var(--color-text-secondary); }


/* ============================================================
   I · MOTION
   One gesture. Apply data-reveal to blocks, never to individual
   words, and never to more than one nesting level.
   ============================================================ */

[data-reveal] { opacity: 0; transform: translateY(var(--reveal-distance)); transition: opacity var(--reveal-duration) ease, transform var(--reveal-duration) var(--reveal-ease); }
[data-reveal].is-in { opacity: 1; transform: none; }

@media (prefers-reduced-motion: reduce) {
  [data-reveal] { opacity: 1; transform: none; transition: none; }
  .action:hover > .arrow { transform: none; }
  .btn, .card, .card-intent, .hub-row, .arrow, .play { transition: none; }
  a:hover .play, a:focus-visible .play { transform: none; }
}


/* ============================================================
   J · CONTENT STATUS
   Filter on status server-side before render. CSS is the safety
   net, not the mechanism — a draft must never reach the DOM.
   ============================================================ */

[data-status='draft'], [data-status='archived'] { display: none; }
.status-badge { display: inline-block; font-size: var(--type-eyebrow); letter-spacing: var(--tracking-label); text-transform: uppercase; color: var(--color-text-quiet); border: 1px solid var(--color-border); border-radius: var(--radius-pill); padding: 4px 10px; }


/* ============================================================
   K · CONSENT
   ------------------------------------------------------------
   ConsentBar — component 37, added 2026-09-08 on Ed's yes.

   The whole thing is built and inserted by js/consent.js and appears only
   while `analytics.ga4` holds a measurement ID. No page's markup mentions it,
   so there is nothing here to keep in step with thirteen files.

   Two deliberate choices, both of which look like details and are not:

   · Both buttons are the same component at the same size. A decline rendered
     smaller, quieter or as a text link than the accept is the standard dark
     pattern, and this bar exists to ask an honest question.

   · It is a region, not a dialog. No overlay, no focus trap, no scroll lock.
     A reader who wants to ignore it can read the entire site with it sitting
     there, and nothing is set while it is unanswered.
   ============================================================ */

.consent-bar {
  position: fixed;
  inset-inline: 0;
  inset-block-end: 0;
  z-index: var(--z-consent);
  background: var(--color-surface);
  border-block-start: 1px solid var(--color-border-strong);
  box-shadow: var(--shadow-float);
}

.consent-bar-inner {
  max-width: var(--content-max);
  margin-inline: auto;
  padding: var(--space-4) var(--gutter);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3) var(--space-5);
}

/* The text takes the space and the buttons keep theirs. `flex: 1 1 320px`
   rather than a percentage: below about 700px the basis stops being satisfiable
   and the row wraps to two lines on its own, which is the layout the phone
   wants, with no breakpoint written for it. */
.consent-text {
  flex: 1 1 320px;
  margin: 0;
  max-width: var(--measure);
  font: 400 var(--type-body-sm)/var(--leading-body) var(--font-ui);
  color: var(--color-text-secondary);
}

.consent-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
  margin-inline-start: auto;
}

/* The site's one gesture, borrowed rather than reinvented: the same 8px rise
   and fade as [data-reveal], on the same tokens. The bar is inserted with
   `data-consent-enter` and the attribute is removed on the next frame. */
.consent-bar[data-consent-enter] {
  opacity: 0;
  transform: translateY(var(--reveal-distance));
}
.consent-bar {
  transition: opacity var(--reveal-duration) ease, transform var(--reveal-duration) var(--reveal-ease);
}

/* The footer control that reopens it. `.link-quiet` was written for an anchor;
   this one performs an action rather than navigating, so it has to be a real
   button — see amendment 26. Everything here is a reset back to the anchor's
   appearance, and the touch-target rules for `.link-quiet` already cover it. */
button.link-quiet {
  background: none;
  border: 0;
  padding: 0;
  font-family: inherit;
  line-height: inherit;
  cursor: pointer;
}

@media (prefers-reduced-motion: reduce) {
  .consent-bar { transition: none; }
  .consent-bar[data-consent-enter] { opacity: 1; transform: none; }
}
