/* ============================================================
   LAYOUT SYSTEM — the layer V2 never shipped.
   ============================================================

   V2 gave four container WIDTHS under a heading named LAYOUT and nothing else:
   no container class, no bleed escape, no measure token, no page grid, no
   section rhythm. So every surface invented its own — 11 distinct hardcoded
   >=400px max-widths across 25 declarations, and 26 of 54 templates hand-copy
   the whole document shell. This file is the missing layer.

   ── LOAD POSITION IS LOAD-BEARING ────────────────────────────
   Linked from partials/head.ejs AFTER tailwind.output.css and BEFORE pageCss.
   Both halves matter and neither is cosmetic:

   ⚠️ THE FIRST VERSION OF THIS NOTE GAVE THE WRONG REASON, and an adversarial
   pass caught it. It said: load after Tailwind or Preflight wins and the sheet
   goes dead. That is FALSE for this file. Every selector here is class-based
   (0-1-0); Preflight resets at element specificity (0-0-1). A class beats an
   element at any load position, so nothing in this file could ever lose to
   Preflight. Repeating a plausible-sounding rule in three places does not make
   it true, and a comment that argues is a comment that will be wrong.

   The real reasons, both narrower and both load-bearing:
   · AFTER Tailwind, because Tailwind's UTILITIES are class-based too (0-1-0),
     so a contest between `.eng-main{display:grid}` and `.grid`/`.flex`/`.hidden`
     is a specificity TIE and resolves purely on source order. There is no
     @layer anywhere in this repo (`grep -rn "@layer" public/css src/css` = 0),
     so the two cannot be interleaved; one of them wins wholesale. This layer
     defines page structure and must not be overridden by a stray utility left
     on a converted shell.
   · BEFORE pageCss, because pageCss is injected at head.ejs after Tailwind and
     43 of 52 templates pass one. A page kit must still be able to override the
     system without !important — that is the escape hatch.

   ⚠️ AND THE TIE HAS A PRICE, PAID EXPLICITLY BELOW. Winning against utilities
   means `.eng-figure{display:flex}` also beats `.hidden{display:none}` — this
   repo uses `class="hidden"` 215 times and `el.hidden = true` 93 times, so a
   primitive that cannot be hidden is a live footgun, not a curiosity. Every
   display-setting rule in this file therefore ships its own hide escape.

   Pinned by tests/views/layout-load-order.test.js. A position that is correct
   today and unpinned is a position that drifts.

   ── WHY A NAMED GRID AND NOT A vw BREAKOUT ───────────────────
   The obvious full-bleed recipe is `width:100vw; margin-inline:calc(50% - 50vw)`.
   It was rejected after measurement, not on taste:
     · 100vw includes the scrollbar gutter -> horizontal overflow on desktop,
       which then needs an overflow guard on an ancestor, which then needs a
       rule about WHICH ancestor;
     · a negative-margin breakout is defeated by any ancestor that establishes a
       containing block or clips. This repo ships plenty: nine dashboard cards
       hold a permanent inline transform (GSAP without clearProps), four
       container classes carry backdrop-filter (including .lf-card, the runner
       card for ~420 lessons), .art-vehicle__markwrap carries an unconditional
       will-change+translate3d, and five selectors clip with overflow.
   A named grid uses NO viewport unit and NO negative margin, so it is immune to
   both the scrollbar gutter and to every containing block above <main>. The
   hazard stops being a rule anyone has to remember, which is the only kind of
   fix that survives contact with a codebase this size.

   ── OPT-IN BY DESIGN ─────────────────────────────────────────
   .eng-main does nothing unless a page asks for it (layouts/main.ejs emits the
   class only when `mainClass` is passed, and emits NOTHING when it is absent so
   the other 27 pages stay byte-identical). Shells convert one at a time; a
   half-converted shell double-pads, which is visible — the good kind of failure.
   ============================================================ */

/* ---------- the page grid ----------
   Three columns: a gutter, the content column, a gutter. Children land in
   `content` by default; anything that wants the full width asks for `full`.
   `min(--container, 100% - 2*--gutter)` keeps the column inside the viewport at
   every width without a media query. */
.eng-main {
  display: grid;
  /* ⚠️ align-content, or the rows stretch. <main> is `flex-1` inside a
     `min-h-screen flex flex-col` body, so on any surface shorter than the
     viewport it is TALLER THAN ITS CONTENT BY CONSTRUCTION — and a grid whose
     align-content computes to `normal` behaves as `stretch`, pulling auto-sized
     rows apart to fill it. The article page hides this (it is long); the --form
     tier, the shortest surface in the repo, is exactly where it would bite. */
  align-content: start;
  grid-template-columns:
    [full-start] minmax(var(--gutter), 1fr)
    [content-start] min(var(--eng-container, var(--container-lg)), 100% - 2 * var(--gutter)) [content-end]
    minmax(var(--gutter), 1fr) [full-end];
}

.eng-main > * { grid-column: content; }

/* The one missing primitive. No vw, no negative margin — see the note above.
   Scoped `.eng-main > .eng-bleed` (0-2-0) rather than a bare `.eng-bleed`
   (0-1-0) DELIBERATELY: the bare form ties with `.eng-main > *` and would win
   only on source order inside this file — a correctness that survives only as
   long as nobody reorders two rules. It is also the honest scope: grid-column
   means nothing on an element that is not a grid item. */
.eng-main > .eng-bleed { grid-column: full; }

/* ---------- the reading measure ----------
   The MIRROR of .eng-bleed: a block that stays on the reading measure inside a
   column wider than it. --container-flow (560px) has existed since the tokens
   landed and NOTHING used it; this is what it was for.

   ⭐ WHY 560, ARGUED IN EMS RATHER THAN CHARACTERS. The gate was written as
   "<=80 characters per line", which is a LATIN convention (45-75 is the classic
   band). Thai has no inter-word spaces and runs roughly 25% more characters in
   the same width, so measuring a Thai page in characters imports the wrong
   yardstick: the article's hero lead sits at a textbook 35.1em and holds 87 Thai
   characters — typographically ideal, and a literal 80-char reading calls it a
   failure. The invariant behind the rule is the MEASURE. 560px gives 35.0em at
   16px, 32.9em at 17px and 40.0em at 14px — the whole comfortable band, at every
   size this page uses. Measured, not asserted:
   review-shots/article-inner/source/cpl.mjs prints both units.

   ⚠️ LEFT-ALIGNED, NOT CENTRED, AND THAT IS THE DECISION. Centring the measure
   inside a 736 column insets prose 88px while cards stay full — so the LEFT edge
   alternates 0 / 88 / 0 / 88 down the page. The left edge is the scan line;
   ragged-right is already what prose does. Keeping one left rule is strictly
   calmer, and it is the edge a reader returns to on every line.

   ⚠️ UNSCOPED, unlike `.eng-main > .eng-bleed`, and the asymmetry has a reason.
   `grid-column` means nothing on an element that is not a grid item, so bleed
   MUST be scoped to be honest. `max-width` is meaningful anywhere, and the
   blocks that need the measure are not all direct grid children — the FAQ answer
   sits inside a <details> inside a section, the rule body inside a card. A
   scoped version would silently do nothing on exactly those. */
.eng-measure { max-width: var(--container-flow); }

/* ---------- tier selection ----------
   A page picks a ROLE, not a number. That is the actual failure being fixed:
   four widths existed and nobody could tell which one their surface wanted, so
   they guessed — 1140, 1152, 1240 and 1392 were each invented independently
   against a declared 1120. */
.eng-main--form   { --eng-container: var(--container-sm); }     /* auth forms          */
.eng-main--flow   { --eng-container: var(--container-flow); }   /* single-column task  */
/* ⚠️ THE READING TIER SUBTRACTS ITS OWN GUTTERS, and that is not a rounding
   preference. The shipped article is `max-w-eng-md mx-auto px-4` = a 768 box
   with 16px of padding INSIDE it, so the text measure is 736 and the crossover
   to full-width happens at a 768 viewport. In the grid the gutters live OUTSIDE
   the column, so a bare 768 container would widen the measure by 32px. 736 with
   a 16px gutter reproduces BOTH numbers exactly: min(736, 100% - 32) is 736 at
   1280 and 358 at 390 — the two widths the shipped page renders today. */
.eng-main--read   { --gutter: var(--gutter-read);               /* reading shell       */
                    --eng-container: calc(var(--container-md) - 2 * var(--gutter-read)); }
.eng-main--app    { --eng-container: var(--container-lg); }     /* app multi-column    */
.eng-main--market { --eng-container: var(--container-xl); }     /* marketing           */
.eng-main--dense  { --eng-container: var(--container-dense); }  /* dense admin tables  */

/* ---------- section rhythm ----------
   Promoted verbatim from .lp-sec (landing.css:275) — the only working section
   band in the repo, built once under pressure on the one surface that could not
   survive as a text column, and never promoted out of a stylesheet only landing
   loads.
   ONE DELIBERATE CHANGE: .lp-sec's separator was a raw rgba(255,255,255,0.06),
   which is dark-surface-only and invisible on white. It becomes the hairline
   token, so the band works on both surfaces. That is 0.06 -> 0.10 on landing
   when landing converts — a small visible change, stated rather than smuggled. */
.eng-sec {
  padding: clamp(56px, 10vw, 96px) var(--gutter);
  border-top: 1px solid var(--hairline);
}

.eng-bg-space .eng-sec,
.eng-sec--dark { border-top-color: var(--hairline-dark); }

/* ---------- figure ----------
   A content figure at reading scale. There was no such thing: <figure> appears
   0 times in views/, object-fit is used 4 times and every one is an avatar
   <=56px, and aspect-ratio is used once on an 88px badge. */
/* ⚠️ flex-direction: column, AND IT SHIPPED WITHOUT IT. The first version was
   `display:flex; justify-content:center` with no direction, so the diagram and
   its <figcaption> laid out as a ROW — measured on the live page: card right
   edge 583, caption left edge 583, side by side. The build report had recorded
   the figure's WIDTH and the caption's TEXT and never asked where the caption
   WAS, which is the same error this whole arc is a ledger of: measuring the
   artefact instead of the encounter. A caption beside its figure is not a
   caption. */
.eng-figure {
  margin: 22px 0 26px;
  display: flex;
  flex-direction: column;
  align-items: center;
}

/* The hide escape this layer owes (see the load-position note above): loading
   after Tailwind means `.eng-figure{display:flex}` wins the tie against both
   `.hidden` and Preflight's `[hidden]` rule. Verified before this line existed —
   setting either left computed display at `flex`. 0-2-0 beats both. */
.eng-figure[hidden],
.eng-figure.hidden { display: none; }

.eng-figure > svg { width: 100%; height: auto; max-width: 520px; display: block; }

/* ⚠️ AN EXPLICIT COLOUR, NOT AN OPACITY, AND THE FIRST DRAFT PROVED WHY.
   This rule shipped as `opacity: 0.72` with no colour. The figure mounts on the
   article's dark plate, so the caption inherited the LIGHT page's black ink and
   rendered black-on-#0B0716 — 1.04:1, measured by audit:contrast across four
   surface×viewport passes, on the exact pass that was written to catch it.
   Opacity on an inherited colour is the fourth defect of this family in this
   codebase; the standing rule is an explicit value on a known background. The
   dark-surface value lives with the surface that creates it (.art-diagram-plate
   in learner.css), because only the surface knows it is dark. */
.eng-figure__caption {
  margin-top: 10px;
  text-align: center;
  font-size: var(--fs-body-sm);
  color: var(--fg-secondary);
}
