/* Homepage-only styles (pages/index.html).
 *
 * WHY AN EXTERNAL FILE, when every other page in this app carries its own
 * inline <style> block. Two reasons, and the second is the real one:
 *
 *   1. fa2536d ships an enforced CSP plus a stricter report-only policy we
 *      intend to promote (tfc-70). The report-only policy drops
 *      'unsafe-inline' from style-src, and the 29 inline <style> blocks
 *      already in this codebase are the inventory blocking that promotion.
 *      A file under /static is covered by `style-src 'self'` in BOTH
 *      policies, so this page's styling needs no nonce and adds nothing to
 *      the list. Moving index.html's existing block out here makes the
 *      inventory 28, not 30.
 *   2. It is now long enough that keeping it in the template would bury
 *      the markup. The per-page inline convention was fine for pages whose
 *      styling was twenty lines.
 *
 * style="--vc: #..." attributes are still used inline for the per-vertical
 * accent, and that is deliberate rather than an oversight: the value is
 * data (one of five palette entries chosen per article), it cannot be
 * expressed as a static class without hardcoding the palette a third time,
 * and `style-src-attr 'unsafe-inline'` is permanent in both policies --
 * main.py's CSP notes say so explicitly, because no nonce mechanism exists
 * for attributes at all.
 *
 * WHAT CARRIES THE PAGE. Type, scale, rule-lines and the vertical palette
 * -- not photographs. As of today ZERO published Stream A articles have a
 * hero image, and CLAUDE.md forbids using the publisher's, so any layout
 * whose confidence depends on a picture per slot looks weakest exactly
 * when the pipeline is thinnest. Images appear in one place (the lead
 * panel) and only when a licensed, credited one exists; remove every image
 * and the page below is still a designed page.
 *
 * IT MUST LOOK DELIBERATE AT THREE STORIES. Today's edition has three
 * articles across all five verticals. Every grid here is auto-fit, so one,
 * two or five cards each fill their row rather than leaving holes, and the
 * lead panel carries the hierarchy on its own when it is the only thing
 * there.
 *
 * COLOUR IS NEVER THE ONLY SIGNAL. Each vertical accent is paired with the
 * vertical's NAME in text. That is not only an accessibility rule -- four
 * of the five palette colours fall below 4.5:1 on white (Public Policy
 * #E8871E is 2.65:1, below even 3:1), so they are used as blocks and rails,
 * never as text or as a lone meaning-bearing mark.
 *
 * TYPE COMES FROM THE SCALE, NOT FROM THIS FILE (2026-08-16). Every rule
 * below takes one of the eight roles defined on :root in base.html, either
 * as `font:var(--type-<role>)` or, for a sans role, as
 * `font-size:var(--fs-<role>)` with the family inherited from `body`. This
 * file declares NO font-family at all and no literal font size but one
 * (.hp-caro-nav's arrow glyph, which says why). It used to declare
 * fourteen families and nineteen sizes, four of which -- 10, 12.5, 13.5,
 * 14.5px -- existed nowhere else on the site.
 * The published definition is docs/DESIGN-SYSTEM.md; the guard is
 * tests/test_type_scale_is_single_source.py.
 */

/* ===== Hero band ===== */
/* Ported from V1's board/homepage.html and then scaled up: the copy is
   unchanged, the type is not. clamp() rather than a breakpoint because the
   headline is the page's single loudest element and it should grow
   continuously between a 360px phone and a 1440px desktop instead of
   jumping at one width. */
.hero{background:var(--navy);color:#fff;padding:clamp(44px,7.5vw,84px) 20px;text-align:center}
/* THE KICKER, restored 2026-08-17 -- review_notes 98 removed it, Sudeep
   reversed that with the conflict in front of him ("Override this"). Values
   are design/mockups/Home.dc.html's: mono, 14px, the brand yellow,
   uppercase, line-height 1, 18px of air below.

   `text-transform` rather than typing the name in caps, so the accessible
   name and the page source both read "The First Click" -- a screen reader
   announcing "T-H-E F-I-R-S-T" is what typing caps into markup risks.

   14px IS THE LABEL ROLE NOW, and this rule no longer says so itself.
   It was an off-scale literal here from design/mockups/Home.dc.html until
   2026-09-22, when Sudeep's readability complaint about the phone menu
   moved --fs-label from 11px to 14px sitewide (cp-508). The value he set in
   the mockup turned out to be the value the role wanted; the override is
   deleted rather than kept as a no-op, and the _SIZE_EXCEPTIONS entry that
   registered it goes with it. product-page.css's 14px is a DIFFERENT case
   and stays registered -- that one overrides body-sm, not label. */
/* text-align IS DECLARED HERE, not inherited, and that is deliberate.

   `.hero` is `text-align:center` and this <p> is its direct child, so the
   kicker should centre without being told. On Sudeep's screen it did not
   -- three times, on three different days: "Alignment and top eyebrow?",
   "DAMN this misalignment again!! Starting to feel like Groundhog day",
   "Still misaligned".

   I could not reproduce it. Checked, each time, and each time it came back
   clean: the markup renders `<p class="kicker">` inside `<div class=
   "hero">`; `.hero{...;text-align:center}` is present in the file AND in
   the stylesheet actually served from production; no rule in any
   stylesheet or template sets text-align to anything but center; the
   `font:` shorthand does not touch alignment; and the kicker's OWN
   treatment (yellow, mono, uppercase) was arriving, which proves this very
   rule was matching and therefore that the .hero rule six lines above it
   was in the same served file.

   So the diagnosis stalled and the honest engineering answer is to stop
   spending his time on it: one declaration makes the element centre on its
   own account, independent of whatever is or is not reaching it through
   inheritance. It costs nothing, it cannot regress anything, and it ends a
   loop that has now cost three rounds.

   ROUND FOUR, AND THE ACTUAL DIFFERENCE (2026-08-18). Sudeep: "this isnt
   fixed so far... only centere aligned in mobile mode." Centred on a phone
   and left on a desktop, with `text-align:center` demonstrably present in
   the served stylesheet, rules out every text-align explanation -- an
   overriding declaration would not know the viewport width.

   What it points at instead is the BOX, and there the difference had been
   sitting in plain sight the whole time: this element is the ONLY child of
   .hero without automatic side margins. Its three siblings each centre
   themselves independently of any inherited alignment --

       .hero h1     max-width:16ch;  margin:0 auto
       .hero .rule  width:72px;      margin:22px auto
       .hero p.highlight   max-width:52ch;  margin:0 auto 28px

   -- and the kicker had `margin:0 0 18px`. So whenever anything made these
   children size to their content rather than fill the line, the other
   three still centred on their auto margins and this one alone went left.
   That is exactly the reported symptom, and it explains the width
   dependence that text-align never could.

   FIXED THE WAY ITS SIBLINGS ARE, therefore: `width:fit-content` plus auto
   margins, centring the box itself. text-align:center is KEPT above it --
   belt and braces, and it is what centres the text within the box if the
   box ever fills the line again. Neither declaration depends on the other
   being effective, which is the property this rule has needed for four
   rounds. */
/* THE 14px LITERAL IS GONE, not moved (cp-508, 2026-09-22). This rule was
   the only place on the site reaching 14px, by overriding --type-label with
   an off-scale number. --fs-label is now 14px, so the role delivers what
   this rule was working around and the override would be a no-op that
   outlives the reason for it. The kicker renders identically. */
.hero .kicker{font:var(--type-label);line-height:1;
  color:var(--yellow);text-transform:uppercase;text-align:center;
  width:fit-content;margin:0 auto 18px}
/* WEIGHT 300, from the mockup. design/mockups/Home.dc.html sets the hero
   headline to `font-weight:300` -- Plex Serif Light, which base.html loads
   -- against the display role's 400. At 34-56px that is a display weight,
   not the thin-serif-at-reading-size problem review_notes 150/151 raised
   about 16px body copy, so it is taken as drawn.

   THE MOCKUP'S FIXED 40px IS DELIBERATELY NOT TAKEN. A static mockup has
   one width, so it states one size; this rule's own history is that the
   headline WAS a fixed 38px and "read as a subheading on a 27-inch monitor
   and as a paragraph on a phone". Re-introducing a literal would undo that
   fix in the name of matching a file that never had to resize. The clamp
   stays; the weight is the design decision inside it. */
/* THE MEASURE FOLLOWS THE HEADLINE, and it had to move when the copy did.
   16ch was sized for "Not just headlines. Insights." -- 29 characters,
   which it broke into two lines. On 2026-08-25 Sudeep replaced the hero
   with "Everything that matters, in less time than your first coffee." /
   "A Publication for the age of AI.", and the lead line is now 61
   characters. Left at 16ch that runs to FOUR lines, and the mobile block
   below caps .hero at 40vh with overflow:hidden -- so the second line
   would not merely wrap, it would be CLIPPED OFF the page on a phone with
   nothing to indicate anything was missing.

   34ch restores the original intent: a two-line break at the display
   clamp's desktop size. A character measure rather than a pixel width,
   for the same reason the clamp above is kept -- it survives a change of
   font size, which a literal does not. */
/* THE HEADLINE ROLE, NOT THE DISPLAY ROLE, since 2026-08-25. Sudeep, on
   seeing the new copy live on desktop and phone: "we need to reduce the
   size of the font used in the homepage template hero."

   display is clamp(34-56px); headline is clamp(26-40px) -- about a
   quarter smaller at any width, and it is the next role DOWN THE EXISTING
   SCALE rather than a number invented for this element. That distinction
   is the whole reason this file has a guard: a literal here would have
   needed a new entry in the type-scale exception list, and the entry that
   used to sit there argues in as many words against growing the list for
   one element.

   It also RETIRES that entry. The mobile block used to force 30px because
   the DISPLAY clamp bottoms out at 34px and ran the headline too tall
   inside the 40vh cap. headline bottoms out at 26px, so the override has
   nothing left to correct and is gone -- the hero is now entirely inside
   the scale at every width, which it has not been since the cap was
   added.

   Line-height moves 1.1 -> 1.15 with the role. Slightly looser, and
   right: the shorter the type, the more a tight leading reads as cramped
   rather than as display. */
/* TWO-THIRDS OF THE HERO, AT MOST. Sudeep, 2026-08-25: "the width should
   not be more than 2/3rd of the hero width on desktop."

   min() rather than a bare 66.6%, because the two limits answer different
   questions and both should hold. 34ch is a MEASURE -- how many
   characters a line may run before it stops being comfortable to read,
   which is a property of the type and does not change with the window.
   66.6% is a PROPORTION -- how much of the band the headline may occupy,
   which is a property of the layout and does change. Whichever is
   tighter at a given width wins.

   In practice 34ch binds at ordinary desktop widths (~750px against a
   ~754px two-thirds on a 1131px hero, which is how close they already
   were) and the proportion takes over on very wide displays, where a
   character measure alone would let the line drift toward the middle of
   an increasingly empty band.

   OVERRIDDEN IN THE MOBILE BLOCK BELOW, and it must be: two-thirds of a
   350px phone is 233px, which would run this headline to four lines
   inside a hero that clips. */
.hero h1{font:var(--type-headline);font-weight:300;
  letter-spacing:-.5px;max-width:min(34ch,66.6%);margin:0 auto}
/* STEPPED, NOT FLUID, AND THE SIZES ARE SUDEEP'S. He gave the table on
   2026-08-25 as an instruction -- 1024px and 1280px at 36px, 1600px at
   40px -- so the headline holds one size across a whole class of screens
   instead of changing with every pixel of window width.

   That is a different intent from the role's clamp, which is why these
   override it rather than adjust it: --fs-headline reaches 40px at 1280px
   and would stay there, and he asked for 36px at that width.

   BETWEEN 641px AND 1023px the role's clamp still applies, unoverridden,
   and bridges 30px to 36px. He did not specify that range and a step
   invented for it would be a guess wearing the same authority as the
   values he did give. */
@media (min-width:1024px){ .hero h1{font-size:36px} }
@media (min-width:1600px){ .hero h1{font-size:40px} }
.hero .rule{width:72px;height:3px;background:var(--yellow);border:0;margin:22px auto}
/* SANS, NOT SERIF -- the mockup's, and the one change here a reader will
   actually notice. This was `--type-lead`, which is IBM Plex SERIF at
   17-19px; design/mockups/Home.dc.html sets the subhead in IBM Plex Sans
   at 15px. Sudeep, 2026-08-17: "Need to get the text from mockup too for
   Homepage hero."

   It is also what made the line break badly. Serif at lead size ran the
   sentence to a second line with "join." alone on it -- the orphan visible
   in his screenshot. The body-sm role is sans at 15px, which is the
   mockup's size exactly, reached through the scale rather than as a
   literal.

   The 52ch measure is kept over the mockup's `max-width:465px` for the
   same reason the clamp is kept above: a character measure is the same
   decision expressed in a unit that survives a change of font size. */
/* THE STANDFIRST IS THE KICKER'S TWIN, since 2026-08-25. Sudeep: it
   "needs to be UPPERCASE ... and in same font size and yellow colour as
   the THE FIRST CLICK".

   MOVED TO components.css SECTION 9 on 2026-08-25, the same day it was
   built, because Sudeep asked for it to be reusable: "the class
   'highlight' can be used in other templates too... like for COMING SOON
   which attracts viewer attention." Everything that makes it a highlight
   -- the yellow field, the navy ink, the weight, the case, the 12px, the
   tight box -- is defined there and applies on every page.

   WHAT STAYS HERE IS ONLY THE HERO'S SPACING, and that division is the
   point: a component that ships its own margins has to be undone at
   every second use. The hero wants it centred with room beneath; a
   COMING SOON badge inside a card wants neither.

   ITS OWN ELEMENT, NOT A VARIANT OF THE KICKER. Sudeep, 2026-08-25:
   "You need to create a new feature in the template for the highlighted
   text and not use KICKER for it."

   It briefly WAS a copy of the kicker's treatment -- same role, same
   14px, same yellow text -- so the two framed the headline as a matched
   pair. Then the colours inverted into a HIGHLIGHT BLOCK: navy text on a
   solid yellow field, bold, boxed tight to the words. At that point the
   two stopped being variations of one idea. The kicker is quiet type
   naming the publication; this is the single struck note on the page. So
   it carries its own class and its own rule, and neither can now be
   changed by editing the other -- which is exactly the coupling that
   produced the bug below.

   THE INVERSION IS ALSO THE STRONGER CONTRAST, which is worth stating
   because it is the reverse of the usual worry about yellow. Navy on
   yellow is roughly 11.9:1 -- fractionally better than the yellow-on-navy
   it replaces, and far better than the 1.47:1 that makes this same yellow
   unusable as text on white. The rule is scoped to .hero, which is always
   navy; do not lift the yellow-text version of it onto a light ground.

   12px, TWO LESS THAN THE KICKER'S 14px -- Sudeep, on seeing the block
   rendered. It is not only a proportion: at 14px the boxed line measures
   about 291px including its padding, against the 280px a 320px phone
   leaves, so it WRAPPED TO TWO LINES on the narrowest screens and the
   field broke into a ragged double bar. At 12px it measures about 250px
   and stays on one line everywhere. Padding came down 7/11 to 6/10 with
   it, so the field stays tight to the words rather than growing as the
   type shrinks.

   11px WOULD HAVE BEEN THE LABEL ROLE'S OWN SIZE and would have needed no
   exception at all -- worth knowing if this is ever revisited -- but 12px
   is what was asked for and one pixel is not worth overriding an explicit
   instruction to save a line in a list.

   THE CLASS IS ALSO A BUG FIX, NOT ONLY A NAMING CHOICE. This rule
   was `.hero p`, which matches EVERY paragraph in the hero -- including
   `<p class="kicker">`. The kicker's own rule is more specific, so it
   kept its yellow TEXT colour and won the font-size, but it sets no
   background and therefore inherited the yellow FIELD from here: yellow
   on yellow, the publication's name rendered as a blank yellow bar. Live
   on the homepage until Sudeep photographed it.

   The selector had been `.hero p` for as long as the rule existed and was
   harmless while it set only a text colour the kicker overrode. Adding a
   background is what exposed it -- a reminder that a loose selector is a
   latent bug, not a safe default, and that it will surface on whichever
   property the more specific rule happens not to set.

   width:fit-content PLUS AUTO MARGINS, not display:inline-block. The
   kicker above carries a long comment about exactly this: these children
   sit in a flex column, and anything that sizes to its content must
   centre its BOX rather than rely on text-align, or it alone drifts left
   while its siblings stay centred. That bug was reported four times.

   WAS: --type-body-sm at #c9ccd6, sentence case; then yellow text. */
.hero p.highlight{max-width:52ch;margin:0 auto 28px}
.hero a.sub{display:inline-block;background:var(--yellow);color:var(--navy);font-weight:600;
  font-size:var(--fs-body-sm);text-decoration:none;padding:14px 28px;border-radius:8px}
.hero a.sub:hover{background:#fff}

/* ===== Page shell + section headings ===== */
/* THE UNDERLINE TAKES THE VERTICAL'S COLOUR WHEN THERE IS ONE. Sudeep's
   ruling, 2026-08-16: "Vertical section-heading underlines take the
   vertical's theme colour (not yellow) site-wide; yellow rules stay for
   non-vertical section headers."

   Expressed as var(--vc, var(--yellow)) rather than as a second class,
   so the DEFAULT is unchanged and only a caller that already knows its
   vertical opts in -- the same --vc custom property the column dots and
   card rails on this page already read. A section that is not about a
   vertical has no --vc to set and keeps yellow without doing anything,
   which is the half of the ruling that is easiest to break. */

.hp{padding:34px 30px 46px;max-width:1100px;margin:0 auto}
.hp h2.sec{font-size:var(--fs-caption);line-height:var(--lh-caption);font-weight:600;letter-spacing:.14em;
  text-transform:uppercase;color:var(--navy);margin:0 0 14px;padding-bottom:9px;
  border-bottom:3px solid var(--vc,var(--yellow))}
.hp .sec-wrap{display:flex;align-items:baseline;justify-content:space-between;gap:14px;flex-wrap:wrap;
  margin:0 0 14px;padding-bottom:9px;border-bottom:3px solid var(--vc,var(--yellow))}
.hp .sec-wrap h2{font-size:var(--fs-caption);line-height:var(--lh-caption);font-weight:600;letter-spacing:.14em;
  text-transform:uppercase;color:var(--navy);margin:0;padding:0;border:0}
/* #5A5F70, not --silver (#9aa0ad) or --muted (#6b7080). --silver is 2.62:1
   on white -- it fails AA outright and was doing so in the shipped card
   meta; --muted is 4.46:1 on the warm Stream B tint, i.e. fractionally
   under. #5A5F70 clears 4.5:1 on all three backgrounds this page uses
   (5.74:1 warm, 6.15:1 cool, 6.32:1 white). */
.hp .sec-wrap a{font:var(--type-label);color:var(--color-text-caption);text-decoration:none;white-space:nowrap}
.hp .sec-wrap a:hover{color:var(--navy);text-decoration:underline}

/* The promoted call to action (review_notes 120): "READ TODAY'S
   CLICK-THROUGH" moved out from under the carousel into the section-TITLE
   slot, directly above this row's yellow rule. It therefore takes the
   section title's typography rather than .hp-go's caption scale -- but it
   keeps the .hp-go class so the two cannot drift apart if that vocabulary
   changes.

   No border-bottom of its own, unlike a normal .hp-go: .sec-wrap already
   draws a 3px yellow rule a few pixels beneath it, and a second yellow
   line stacked above the first reads as a rendering fault rather than as
   emphasis. Hover underlines instead.

   Deliberately NOT white-space:nowrap, unlike the .sec-wrap link above.
   The words are computed and variable-length -- "Read today's
   Click-through" when the edition is today's, "Read the 14 Aug 2026
   Click-through" when it is not (see _homepage_today) -- so it must be
   free to wrap on a narrow screen. nowrap would push the longer string out
   of the flex row rather than shortening it.

   AS OF review_notes 136 IT IS ALONE IN ITS ROW. The "See the whole
   edition ->" anchor that used to sit opposite it went with that note --
   same destination, same row, said twice. No rule died with it: the
   `.hp .sec-wrap a` pair above is still what styles "All of Ctrl+Shift ->"
   in the columns row, and .sec-wrap's own space-between/flex-wrap already
   had to hold a single child (the "The rest of The First Click" row has
   only ever had one), so nothing here was compensating for the second
   element's presence. */
.hp .sec-wrap h2 .hp-go--title{font-size:var(--fs-caption);line-height:var(--lh-caption);font-weight:600;
  letter-spacing:.14em;text-transform:uppercase;color:var(--navy);
  margin:0;padding:0;border:0;display:inline;white-space:normal}
.hp .sec-wrap h2 .hp-go--title:hover{text-decoration:underline;
  text-decoration-thickness:2px;text-decoration-color:var(--yellow)}
/* ===== Daily board: the lead ===== */
/* Cool ivory, per the Stream A treatment (aggregated content reads cool,
   Stream B warm). The vertical accent is a 6px rail across the top: a
   block of colour, decorative, always accompanied by the vertical's name
   in navy text below it. */
/* THE CARD COMPONENT OWNS THE BOX (2026-08-16). The ground (#fffafa,
   Stream A), the hairline, the 4px editorial radius, the 6px vertical
   rail and `overflow:hidden` are all declared once in components.css --
   `.hp-lead` is listed in the base, in `--stream-a`, in `--editorial`,
   in `--rail` and in `--flush`, so nothing here changed except where it
   is written. What stays is the only thing that is genuinely about this
   panel's place on this page: the gap under it. */
.hp-lead{margin:0 0 14px}
/* THE SAME HEIGHT AS THE CLICK-THROUGH'S HERO (review_notes 143, filed on
   `img.hp-lead-img` itself): "I understand the smaller height of hero
   image on subheros in Click-through, but why smaller on Homepage? At
   least match the hero image height from Click-through page."

   MEASURED AT HIS RECORDED VIEWPORT (the note carries viewport_width
   1080), not eyeballed. base.html's .wrap caps the page at 1080px, .hp
   spends 30px a side, .hp-lead one more for its border -- so the picture's
   box is 1018px wide. What each of the three heights actually was:

     homepage hero  height:clamp(150px,26vw,300px) -> 26% of 1080 = 280.8px
     /daily subhero .section-hero  max-height:280px -> 280px
     /daily hero    .mustread .heroimg max-height:360px -> 360px

   So the homepage hero was rendering at the SUBHERO's height, 0.8px off
   it, which is exactly the comparison he drew. The 300px ceiling was never
   even reached: `vw` is measured against the WINDOW, and the window keeps
   growing after the 1080px column has stopped, so the one number that
   decided this height was the one number that had nothing to do with the
   picture's own box.

   Hence aspect-ratio + max-height rather than a re-tuned vw coefficient:
   it is /daily's own mechanic, where the height follows the box's width
   and 360px is the ceiling. Same rule shape on both pages, so they agree
   by construction instead of by two coefficients that happen to line up --
   the same reason _story_anchor.html exists.

   1200/630 is not invented here: it is the platform's declared hero spec,
   the ratio daily.html's own .heroslot placeholder box uses and prints.

   WHAT THIS NOW RENDERS, against /daily's hero at the same width:

     1280px window ->  360px, /daily 360px    (the column is capped at 1080)
     1080px window ->  360px, /daily 360px    <- the note's viewport
      900px window ->  360px, /daily 360px
      768px window ->  360px, /daily 349.7px
      640px window ->  318px, /daily 297px
      375px window ->  179px, /daily 158px

   Below ~790px the homepage runs 10-21px TALLER, and that is arithmetic
   rather than a choice: /daily's hero sits inside .mustread's own 20px of
   padding and this one runs edge to edge inside its panel, so the same
   ratio off a wider box gives a taller picture. "At least match" is
   satisfied at every width; nothing gets shorter anywhere.

   min-height keeps the floor the old clamp() guaranteed, so a very narrow
   phone cannot shrink the picture into a strip. object-fit:cover stays --
   a hero that is not exactly 1200x630 is cropped to the box, which is what
   this image already did and is what keeps every carousel slide the same
   height as its neighbours. */
/* HERO HEIGHT IS 420px AND IT IS FIXED, NOT A CAP. Sudeep's ruling,
   2026-08-16, from design/mockups: "Hero images: fixed 420px tall (min
   420px) site-wide, object-fit cover." A max-height is a cap that a
   short picture never reaches, which is how four pages ended up with
   four different hero heights (360/340/400/280) that all looked
   deliberate. height+min-height together means every hero is the same
   height whatever the source image is, and object-fit:cover does the
   rest. SUBHEROES ARE DELIBERATELY EXCLUDED -- review_notes 143: "I
   understand the smaller height of hero image on subheros in
   Click-through". */
.hp-lead-img{display:block;width:100%;height:420px;min-height:420px;object-fit:cover}
.hp-lead-body{padding:clamp(18px,2.6vw,30px)}
/* Credit is mandatory wherever an image renders (CLAUDE.md), so it is part
   of the image, not an option beside it -- sits directly under the picture
   at caption scale.
   SIZE IS NO LONGER RESTATED HERE (review_notes 102/108). base.html sizes
   `.imgcredit` once for the whole site; this rule keeps only what is
   genuinely local to the lead panel -- a margin that sits under an image
   rather than pulling up under one. */
/* MARGIN ONLY NOW (2026-08-23). This rule also carried `color:#5A5F70`, an
   accessibility decision measured at 6.15:1 against this panel's #fffafa
   ground, and it was nearly deleted on 2026-08-15 in the name of making the
   credit "even across the site" (review_notes 145) --
   test_picture_credit_single_source.py caught that and it was right to.

   It is retired here for the OPPOSITE reason to the one that nearly took
   it: the ground it was measured against is gone. base.html now draws the
   credit as a filled chip, so every credit on the site sits on var(--sunk)
   rather than on whatever panel is behind it, and base.html carries this
   exact value -- 5.72:1 on that one ground. Keeping the line would be a
   second copy of a number that is now shared, which is the duplication note
   102 forbids. The decision survived; the local restatement of it did not. */
.hp-lead .imgcredit{margin:0 0 14px}
/* THE VERTICAL LABEL IS A PILLBOX (review_notes 109). Sudeep: "Convert to
   PILLBOX, larger font. All vertical names on ONE line -- never two, as
   'Marketing & Design' currently wraps."
 *
 * inline-flex, not the flex it was: a block-level flex row is as wide as
 * its container, so the label read as a full-width band rather than as a
 * chip. inline-flex shrink-wraps to the words, which is what makes it a
 * pill at all. Safe here because .hp-vert is never itself a flex ITEM --
 * .hp-lead-body and .hp-vcard are ordinary blocks -- and a flex item would
 * have blockified this back.
 *
 * Same vocabulary as .hp-chip in the vertical nav below (mono, 999px
 * radius, navy on --sunk, accent carried by the dot). One pill shape on
 * this page, not two that nearly match.
 *
 * ONE LINE IS SOLVED, NOT AVOIDED. white-space:nowrap alone would move the
 * failure from wrapping to overflowing its card, so the longest real name
 * is what the numbers are set from: "MARKETING AND DESIGN" is 20
 * characters. The tracking came down from .12em (26px of pure
 * letter-spacing on that name alone, the single biggest contributor) and
 * .hp-vgrid's floor went up to 260px, so the pill clears its card at the
 * narrowest column the grid will ever produce. The arithmetic is checked
 * by test_the_longest_vertical_name_fits_the_narrowest_card_it_can_land_in.
 *
 * IT IS NOW THE CAPTION ROLE -- SANS, 13px -- AND WAS MONO 12.5px
 * (2026-08-16). review_notes 154 (`p.hp-vert.hp-vert--day`, "TODAY") and
 * 155 (the vertical name itself, "PUBLIC POLICY") are the same note filed
 * twice on the same element: "Readability is poor, San Serif Font". Mono
 * uppercase at 12.5px is the least legible combination on the page and
 * this is the only element Sudeep has said so about twice.
 *
 * caption rather than label, even though a vertical name reads like a tag,
 * for two independent reasons. The scale's label role is MONO -- the exact
 * face the notes reject -- and it is 11px, while
 * test_vertical_label_font_grew pins this element ABOVE 11px on the
 * strength of review_notes 109 ("larger font"). caption is the only role
 * that satisfies both, and 12.5 -> 13 is the direction the note asked for.
 *
 * THE SWITCH COSTS NOTHING IN WIDTH, which is the part that had to be
 * checked rather than assumed, because a wider face under nowrap is how
 * note 109 gets re-filed. "MARKETING AND DESIGN" measures 11.436em in IBM
 * Plex Sans caps (summed from the font's own advance widths; 0.572em per
 * character averaged, against Plex Mono's flat 0.6em), so 13px sans is
 * ~149px of glyphs where 12.5px mono was ~150px. Plus .06em of tracking,
 * the dot, the gap and the padding: ~206px of pill against the ~227px a
 * 260px column offers inside .hp-vcard's 15/16px padding.
 *
 * line-height stays 1.1 rather than taking caption's 1.4: on a nowrap pill
 * the line box IS the pill's height, so that number is geometry here, not
 * typography. */
.hp-vert{display:inline-flex;align-items:center;gap:7px;font-size:var(--fs-caption);
  letter-spacing:.06em;text-transform:uppercase;color:var(--navy);margin:0 0 10px;
  white-space:nowrap;max-width:100%;border:1px solid var(--line);border-radius:999px;
  padding:5px 12px;background:var(--sunk);line-height:1.1}
.hp-vert .dot{width:9px;height:9px;border-radius:50%;background:var(--vc,#001234);flex:none}
/* The vertical name is a link to its page (review_notes 103). Undecorated
   at rest -- it is a label first, and an underlined mono caps line above a
   serif headline reads as noise -- and it inherits the label's navy rather
   than taking a link colour, because four of the five vertical accents
   fail 4.5:1 on white and none of them may carry meaning as text. */
.hp-vert a{color:inherit;text-decoration:none}
.hp-vert a:hover{text-decoration:underline;text-decoration-thickness:2px;text-decoration-color:var(--yellow)}
.hp-lead-head{font:var(--type-headline);
  letter-spacing:-.4px;margin:0 0 12px;max-width:22ch}
.hp-lead-head a{color:var(--navy);text-decoration:none}
.hp-lead-head a:hover{text-decoration:underline;text-decoration-thickness:2px;text-decoration-color:var(--yellow)}
/* THE LEAD STORY'S STANDFIRST -- the single worst offender in the old
   file and the element behind review_notes 150 and 151.
   It was IBM Plex Serif LIGHT (300) at clamp(15px,1.7vw,18px): a thin
   serif that pinned to its 15px FLOOR on every phone and only reached 18px
   on a desktop. Now the lead role: the same serif, at a weight IBM
   actually drew, starting at 17px on the narrowest phone.
   Sudeep's two notes on this element ask for SANS ("Need San Serif Font
   here to improve readability on mobile"). The scale answers with serif at
   400 and 17px instead, because a standfirst is what the reader looks at
   before deciding to read -- see docs/DESIGN-SYSTEM.md, "Serif is what you
   look at". If that reads no better to him than the old one did, the whole
   change is `--type-lead` -> `--type-body` on this one line. */
.hp-lead-sum{font:var(--type-lead);color:#3a3a38;margin:0;max-width:60ch}
/* THE EDITOR'S NOTE (cp-340 / tfc-208), one per vertical. Rendered by
   _parts.html's story_body and board macros directly after the summary,
   only when the article carries one; never in the rail, which carries no
   summary. product-page.css's three rules on this page's tokens: the
   vertical's --vc is the rail colour here, where the product page has
   --accent. `.hp-vcard .hp-note` outranks `.hp-vcard p` above the board,
   which would otherwise take back the margin and size on the cards. */
/* One step below each summary: .hp-lead-sum is --type-lead, so the slide
   note is --type-body-sm; .hp-vcard p is --type-body-sm, so the demoted
   card's note is --type-caption. Steel box, 6px yellow rule (cp-340). */
/* --steel IS --silver LAID OVER WHITE AT 28%, and the only COOL ground the
   site has -- every other surface here is a warm `paper`, which is the
   point: the note is the one block on the page not speaking in the
   publication's usual register.

   TOKENISED 2026-09-19 (cp-346). It shipped on 2026-09-04 as the literal
   #E3E4E8 in this file, product-page.css and vertical.html, against a
   same-morning edition deadline, because registering a new :root property
   needs a generator pass rather than a data edit and test_design_tokens.py
   rightly refused the shortcut. It is now primitive.color.slate.100 with a
   role of its own, color.surface.note, and --steel is that role's legacy
   alias in base.html -- the same mechanism --navy and --line use. The
   email still carries the literal, because a mail client cannot be relied
   on for a custom property. */
.hp-note{font:var(--type-body-sm);font-weight:300;color:#3a3a38;margin:14px 0 0;
  padding:12px 16px;border-left:6px solid var(--yellow);background:var(--steel);
  border-radius:0 3px 3px 0;max-width:74ch}
.hp-vcard .hp-note{font:var(--type-caption);padding:10px 14px;margin-top:10px}
.hp-vcard .hp-note{font:var(--type-body-sm);font-size:14px;margin-top:10px}
.pp-note-label{display:block;font:var(--type-label);letter-spacing:.06em;text-transform:uppercase;color:#6b6b66;margin-bottom:4px}
.hp-go{display:inline-block;margin-top:16px;font:var(--type-label);letter-spacing:.06em;
  text-transform:uppercase;color:var(--navy);text-decoration:none;
  border-bottom:2px solid var(--yellow);padding-bottom:3px}
.hp-go:hover{border-bottom-color:var(--navy)}

/* ===== Daily board: the other verticals ===== */
/* auto-fit, so two cards (today's likely shape) fill the row and five
   wrap to a second one. No fixed column count anywhere on this page --
   a grid with a hole in it is what "unfinished" looks like. */
/* 260px, not 240px: the floor is now set by the widest vertical pill, so
   "MARKETING AND DESIGN" can hold its one line (review_notes 109) in the
   narrowest column this grid will ever produce. See .hp-vert for the
   arithmetic. Still auto-fit, so two cards -- today's likely shape --
   still fill the row rather than leaving a hole. */
.hp-vgrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:14px;margin:0 0 18px}
/* The card component owns this box too -- `--surface` ground, hairline,
   4px radius, the 5px rail and the 15/16/17 padding, all in
   components.css. The rule that was here declared five properties and
   none of them were about a vertical card in particular. */
.hp-vcard .hp-vert{margin-bottom:8px}
/* A CARD HEADLINE IS A SUBHEAD, and that is a real change: this was IBM
   Plex Sans SemiBold at 17px, i.e. a headline set in the body face while
   the lead panel four rules up was set in serif. review_notes 152, filed
   on a headline on this page, says so from the other direction: "Serif
   Font can work here like used in section above." One rule for headlines
   across the site is the point of the scale. */
.hp-vcard h4{font:var(--type-subhead);margin:0}
.hp-vcard h4 a{color:var(--navy);text-decoration:none}
.hp-vcard h4 a:hover{text-decoration:underline;text-decoration-color:var(--yellow)}
/* CARD TEXT IS SANS. It was serif LIGHT at 13.5px, one of the seven
   independent reinventions of "serif body text" the scale replaced, and
   the direct subject of review_notes 150 ("Poor Readability; switch font
   to San Serif"). The card gets taller. That is the fix, not a regression
   -- see docs/DESIGN-SYSTEM.md. */
.hp-vcard p{font:var(--type-body-sm);color:#3a3a38;margin:8px 0 0}

/* All five verticals, always -- the publication's standing beat list, so
   it must not appear to shrink on a day when only two ran. */
.hp-vnav{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin:0 0 34px}
/* BOLD, because the mockup writes it `<b>Every vertical</b>`. It is not
   emphasis for its own sake: this label sits in a row of five chips that
   use the same 11px mono at the same weight, and without the contrast it
   reads as a sixth chip that lost its border. */
.hp-vnav .lbl{font:var(--type-label);font-weight:600;letter-spacing:.1em;
  text-transform:uppercase;color:var(--color-text-caption);margin-right:2px}
/* nowrap for the same reason .hp-vert has it (review_notes 109): "All
   vertical names on ONE line -- never two". These already shrink-wrapped,
   but the words inside them could still break, and this row is the one
   place all five names appear together. .hp-vnav wraps between chips, so a
   narrow screen stacks whole pills rather than splitting a name. */
.hp-chip{display:inline-flex;align-items:center;gap:6px;font:var(--type-label);
  color:var(--navy);text-decoration:none;border:1px solid var(--line);border-radius:999px;
  padding:5px 12px;background:var(--sunk);white-space:nowrap}
.hp-chip:hover{border-color:var(--navy);background:#EDECE6}
.hp-chip .dot{width:8px;height:8px;border-radius:50%;background:var(--vc,#001234);flex:none}

/* Uncurated day. Deliberately built from the same parts as a populated
   lead -- rail, mono label, serif line -- so an empty day reads as this
   publication on a quiet morning, not as a broken page. */
/* Box from the card component (`--stream-a`, `--editorial`, `--rail`,
   and its own `--card-pad`). The rail here is NAVY rather than a
   vertical accent -- an uncurated day belongs to no vertical -- which
   is why this name is absent from the `--card-rail: var(--vc)` group
   and takes the component's navy default. */
.hp-empty{display:flex;gap:18px;align-items:flex-start;margin:0 0 34px}
.hp-empty img{height:52px;width:auto;flex:0 0 auto}
.hp-empty .lbl{font:var(--type-label);letter-spacing:.12em;text-transform:uppercase;
  color:var(--color-text-caption);margin:0 0 8px}
.hp-empty h3{font:var(--type-subhead);color:var(--navy);margin:0 0 10px}
.hp-empty p{font:var(--type-lead);color:#3a3a38;margin:0}

/* ===== "The rest of The First Click": product cards ===== */
/* THESE WERE SHRUNK, THEN GROWN AGAIN. Both on Sudeep's instruction, and
   the history is kept because otherwise the next person reads the current
   numbers as a drift away from a stated intent.
 *
 *   2026-08-14: "smaller images and smaller title text", so these read as
 *   secondary invitations rather than competing with the lead story. Logo
 *   28->20px, name 15->12.5px, blurb 12.5->11.5px, headline 13.5->12.5px,
 *   padding 16/17->12/13px.
 *
 *   2026-08-15 (review_notes 99/100/101/110/111), having seen that:
 *   "Increase size/height to match the height used for verticals in the
 *   section above", "Sublogo and font size must increase to at least match
 *   the article title in the card/section above", and a hero image with
 *   title and summary. So the reference is now .hp-vcard, not the old
 *   deliberately-reduced scale: logo 20->32px (still under the sublogo
 *   PNGs' 52px native size -- they have no higher-resolution source yet,
 *   so they must never be scaled UP), name 12.5->17px to meet .hp-vcard
 *   h4's 17px exactly, blurb 11.5->13px, headline 12.5->15px, padding
 *   12/13->15/16/17px to match .hp-vcard's.
 *
 * THE HIERARCHY SURVIVES THE INCREASE, which is the part worth checking.
 * The Daily lead headline is clamp(24px,3.6vw,40px); a 15px card headline
 * still sits far below it. The gap narrowed, it did not close.
 *
 * minmax 180->260px: three cards on one row now need the room the larger
 * type asks for, and 260px is the same floor .hp-vgrid uses, which is what
 * "match the height used for verticals" means in a grid whose rows size
 * themselves. Still auto-fit, so a future fourth product wraps rather than
 * squeezing.
 *
 * FOUR ROWS, AND THE CARDS SHARE THEM (2026-08-16 10:52). Sudeep, on
 * p.hp-soon at 1080px: "Move this to the bottom of this card and ensure
 * alignment with two other card photos."
 *
 * The second half is the structural one. A product card is a stack of
 * blocks -- identity head, cadence line, blurb, story -- and in ordinary
 * flow each card's picture starts wherever THAT card's blurb stopped. One
 * blurb wrapping to a second line, or one card carrying a "Coming soon"
 * label the others do not, and the three photographs are off a line from
 * each other. Moving the label out from above the picture would have
 * traded one misalignment for another rather than fixing the class of
 * problem.
 *
 * So the grid owns the rows and the cards borrow them: .hp-grid declares
 * four auto rows, each card spans all four as a `subgrid`, and each of the
 * card's four parts is pinned to one of them. A row is then sized to the
 * TALLEST card's content in that row -- so row 4 begins at the same y in
 * every card in the band, and the picture at the top of row 4 does too.
 *
 * WHY NOT A HEIGHT. `min-height` on the identity block, or a margin tuned
 * until the screenshot looked right, would hold only for today's three
 * blurbs at today's width: the first product renamed, the first blurb
 * edited, the first breakpoint between 640 and 1100 where a blurb wraps
 * differently, and it is silently wrong again. The grid measures what is
 * actually there, at every width, with no number to keep in sync.
 *
 * WHAT HAPPENS WHEN A CARD HAS NO STORY. It renders no .hp-cardstory at
 * all (see index.html), so it contributes nothing to row 4 and shows no
 * reserved band -- the other cards still line up, and a product that has
 * never published still looks like a card rather than like a card with a
 * hole in it. Same for a story with a headline but no licensed picture:
 * its headline starts where the others' pictures do.
 *
 * WHEN THE ROW WRAPS -- two columns under ~870px, one under ~600px --
 * auto-placement gives the wrapped card its own band of four implicit
 * rows. Cards that sit side by side share rows; cards that do not, do not
 * need to.
 *
 * `row-gap:0` on the card is deliberate and load-bearing: a subgrid
 * inherits its parent's gutters unless it states its own, and .hp-grid's
 * 14px would otherwise open between every part INSIDE each card. The
 * spacing between the four parts stays where it always was -- the bottom
 * margins on .hp-cardhead, .hp-meta and .hp-blurb.
 *
 * A BROWSER WITHOUT SUBGRID drops that one declaration and keeps the rest:
 * the card is still a four-row grid of its own, the parts still stack in
 * the same order, and only the cross-card alignment is lost. Degraded, not
 * broken. */
.hp-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));
  grid-template-rows:repeat(4,auto);gap:14px;margin:0 0 34px}
/* THE STREAM DISTINCTION MOVED, IT DID NOT GO (2026-08-16).
   `.hp-card--a` (#fffafa, cool, curated third-party) and `.hp-card--b`
   (#F5F3F0, warm, original First Click writing) are now declared in
   components.css as `.card--stream-a` / `.card--stream-b`, with these
   two names in their selector groups -- so the markup did not change and
   the two temperatures are now a NAMED VARIANT of the card rather than a
   pair of one-line overrides that a consolidation could have merged.
   design/DESIGN.md section 5 says why that matters; a guard test asserts
   the two grounds are never the same colour.

   Still true, and still the reason .hp-stream exists: a 2% background
   shift is not a signal anyone can reliably perceive, and none at all
   for a reader who cannot see it. The tint is the reinforcement; the
   word is the signal.

   The box, the hover and the focus ring also went to the component
   (`--editorial`, `--interactive`) -- the display is what is left,
   because an <a> is inline and the component does not decide that.

   `display:grid` rather than the `display:block` that was here until
   2026-08-16: the card now borrows .hp-grid's four rows so that the three
   cards' pictures start on one line. The full argument is in .hp-grid's
   note above. The card's own padding is accommodated by the first and last
   of the borrowed tracks, and all three cards share one --card-pad, so it
   shifts every card by the same amount and the rows still meet. */
.hp-card{display:grid;grid-template-rows:subgrid;grid-row:span 4;row-gap:0}
/* review_notes 110: "Sublogo and font size must increase to at least match
   the article title in the card/section above." That title is .hp-vcard h4,
   so .hp-nm takes THE SAME ROLE rather than the same number -- which is
   what "at least match" has always meant and what a hardcoded 17px on both
   sides could only ever approximate. Both are `subhead` now, so the two
   cannot drift apart again; test_the_card_sublogo_and_name_match_the_
   vertical_cards_title still checks the >= relation.
   32px logo: up from 20px, and deliberately still short of the PNGs' 52px
   native size. Those files have no higher-resolution source, so displaying
   them above native is upscale-pixelation on any high-DPI phone.

   THE FOUR `grid-row`s BELOW ARE THE ALIGNMENT. Each part is pinned to
   its own borrowed row rather than left to auto-place, because auto
   placement puts the FIRST child in row 1 and the next in row 2 -- so a
   card with no story block would slide its blurb into the row the others
   put their pictures in, which is the misalignment this is fixing. Pinned,
   a missing part leaves its row empty and everything below it stays put. */
.hp-cardhead{display:flex;align-items:center;gap:10px;margin:0 0 7px;grid-row:1}
.hp-cardlogo{height:32px;width:auto;flex:0 0 auto}
.hp-nm{font:var(--type-subhead);color:var(--navy)}
.hp-meta{font:var(--type-label);letter-spacing:.06em;text-transform:uppercase;
  color:var(--color-text-caption);margin:0 0 8px;grid-row:2}
.hp-stream{color:var(--navy);font-weight:500}
.hp-blurb{font-size:var(--fs-body-sm);line-height:var(--lh-body-sm);color:#4a4a48;margin:0 0 11px;
  grid-row:3}
/* THE STORY BLOCK: picture, credit, headline, summary and the "Coming
   soon" label, in one row so the pictures at the top of it start on one
   line across the band. See .hp-grid's note.

   A flex COLUMN, and for one reason only: it is what lets .hp-soon take
   `order` and `margin-top:auto` below -- paint last, sit on the floor --
   without moving the element in the template, where it still labels the
   story block it introduces. Everything else in here stacks exactly as it
   did in flow, on its own margins. */
.hp-cardstory{grid-row:4;display:flex;flex-direction:column}
/* "COMING SOON" over a scheduled piece (review_notes 149). Deliberately
   the SAME treatment as edition-states.css's .es-kicker -- mono, 10px,
   .14em, uppercase -- because /ctrl-shift's teaser and this card are the
   same editorial statement about the same article, and a reader moving
   from one to the other should not meet two different labels for it.

   #5A5F70 is this file's established caption grey, and it is the reason
   the colour is stated rather than inherited: 5.74:1 on the Stream B
   card's #F5F3F0 ground, and higher still on Stream A's #fffafa. The
   label carries real information -- that the piece cannot be read yet --
   so it has to meet AA as body text, not as decoration.

   IT IS PAINTED LAST AND ON THE FLOOR (Sudeep, 2026-08-16 10:52: "Move
   this to the bottom of this card"). It used to be the first thing in the
   story block and read `margin:0 0 7px`, the gap between it and the
   picture it introduced.

   `order:1` DOES THE MOVE, NOT THE TEMPLATE. Every sibling in
   .hp-cardstory takes the initial `order:0`, so this one paints after all
   of them while staying FIRST in the document -- which is what keeps a
   screen reader saying "coming soon" before the headline it qualifies,
   the point the superseded note in index.html was making. Nothing in this
   block is focusable, so no focus order diverges from what is painted.

   `margin-top:auto` PUTS IT ON THE FLOOR rather than under the summary:
   the story block is a flex column stretched to its grid row, so the auto
   margin eats the leftover height and the label lands on the card's
   bottom edge. That distance is also what stops it being read as a
   caption belonging to the sentence above it. `padding-top` is the
   minimum gap for the card that happens to have no slack at all, where
   `auto` resolves to zero -- 11px, .hp-blurb's own bottom margin, so the
   card's internal rhythm is unchanged. */
.hp-soon{font:var(--type-label);letter-spacing:.14em;text-transform:uppercase;
  color:var(--color-text-caption);order:1;margin:auto 0 0;padding-top:11px}
/* THE CARD'S HERO (review_notes 99/100/101 "ADD Hero Image", 111 "Use a
   SMALLER hero image"). Smaller is measured against the lead panel's
   .hp-lead-img, which tops out at 360px since review_notes 143 matched it
   to the Click-through's hero; this tops out at 150px, comfortably under
   half. (It read "clamp(150px,26vw,300px)" until 143 -- the gap has only
   widened, and the relationship is pinned by
   test_homepage_layout_uat's note-111 check either way.)
   A fixed-height cover crop rather than an intrinsic ratio, so three
   cards side by side keep their headlines on the same line whatever
   proportions the editor uploaded.

   ITS TOP EDGE IS NO LONGER LEFT TO CHANCE EITHER (2026-08-16). This is
   the first thing painted in .hp-cardstory, which is one row of .hp-grid's
   shared four, so the picture starts at the same y in every card in the
   band. The fixed height keeps the headlines level BELOW the pictures; the
   subgrid keeps the pictures level to begin with. */
.hp-cardimg{display:block;width:100%;height:clamp(104px,13vw,150px);object-fit:cover;
  border-radius:3px;margin:0 0 8px}
/* The credit under a card hero. base.html draws it (review_notes 102/108);
   only the margin is local -- the shared rule leaves an 8px gap under an
   image that has its own bottom margin, and this one does not, so the chip
   sits straight under the picture here. */
.hp-card .imgcredit{margin:0 0 9px}
/* `lead`, not `subhead` -- the one card headline on this page that takes a
   smaller role than the others, and deliberately. It is the SECOND line
   inside its own card (the product name above it is the card's title), and
   test_the_lead_headline_is_still_far_louder_than_a_card_headline pins it
   under half the Daily lead headline's ceiling: 19px against 40px. subhead
   would put it at 28px and close a gap the note that widened it wanted
   open. */
.hp-hl{font:var(--type-lead);color:var(--navy);
  border-top:1px solid var(--line);padding-top:9px;margin:0}
/* The lead story's summary (review_notes 111: "title and subheading/
   summary text"). THE SAME ROLE as .hp-vcard p -- body-sm -- so a product
   card and a vertical card read as the same kind of object, which is what
   the old rule was reaching for when it said "same serif-light treatment,
   one step down in size". It no longer steps down: two card summaries that
   differ by 1px are not a hierarchy, they are a typo, and 12.5px serif
   Light was the smallest and thinnest text on the whole page.
   No border of its own -- it belongs to the headline above it, and a
   second rule inside one card reads as two cards. */
.hp-sum{font:var(--type-body-sm);color:#3a3a38;margin:7px 0 0;
  display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;line-clamp:2;overflow:hidden}

/* ===== From the columns ===== */
.hp-cols{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:12px}
/* Box from the card component: `--surface`, `--editorial`, `--rail` at
   4px (a column is the quietest of the three rails) and 14/15 padding. */
.hp-col .who{font:var(--type-label);letter-spacing:.08em;text-transform:uppercase;
  color:var(--color-text-caption);margin:0 0 8px;display:flex;align-items:center;gap:6px;flex-wrap:wrap}
.hp-col .who .dot{width:8px;height:8px;border-radius:50%;background:var(--vc,#001234);flex:none}
.hp-col .who a{color:var(--color-text-caption);text-decoration:none}
.hp-col .who a:hover{color:var(--navy);text-decoration:underline}
/* A column card's own headline, so `subhead` -- the same role as the
   vertical cards' and the product cards' names. It was 14.5px sans
   SemiBold, a size that exists nowhere else on the site. */
.hp-col h5{font:var(--type-subhead);margin:0 0 6px;color:var(--navy)}
/* LINE CLAMPS. Sudeep's ruling, 2026-08-16: "Card summaries: clamp at 4
   lines with '...'; product-card descriptions 2 lines with '...'." The
   ellipsis is the BROWSER's, drawn by -webkit-line-clamp -- not a
   character appended in Python, which would truncate at a count of
   letters rather than at what actually overflowed and would put a '...'
   on a summary that fitted. Every card in a row therefore ends at the
   same line, which is the point: a grid of auto-fit cards is only as
   tidy as its longest summary. -webkit- prefixed and unprefixed both,
   because the standard `line-clamp` is now supported and the prefixed
   pair is still what every current engine actually honours. */
.hp-col p{font:var(--type-body-sm);color:#4a4a48;margin:0;
  display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:4;line-clamp:4;overflow:hidden}

/* Keyboard reachability: every interactive element on this page gets a
   visible ring. base.html sets none, and the default UA outline is
   invisible against the navy links here. */
.hp a:focus-visible,.hero a:focus-visible{outline:2px solid var(--navy);outline-offset:3px;border-radius:2px}
.hero a:focus-visible{outline-color:var(--yellow)}

@media(max-width:640px){
  /* THE HERO IS SHORTER ON A PHONE, so the lead story is above the fold.
     Sudeep, 2026-08-18, with two screenshots: "SHORTEN the height of the
     hero in mobile version to ensure the lead story image and title text
     are visible upon first load."

     26px is design/mockups' own mobile value (`.site-hero{padding:26px
     20px}` in its 640px block), not a number invented here. The desktop
     clamp bottoms out at 44px, which on a 390px screen spent roughly a
     fifth of the first viewport on empty navy above and below a sentence
     the reader has already met in the masthead.

     PADDING ONLY. The headline keeps its clamp and the subhead keeps its
     measure -- shrinking the type would buy the same space by making the
     page's loudest element quieter, which is the opposite of what a front
     page wants on the smallest screen. */
  /* 40% OF THE VIEWPORT, AND NO MORE. Sudeep, 2026-08-18: "NEED TO SHrink
     the homepage hero in mobile viewport to 40% of the page height. In
     Desktop its height is approx 33%. But in mobile I want visitors to see
     story first... quickly gain attention."

     svh, NOT vh, and this is the whole reason the rule works in practice.
     `vh` on a phone is the LARGEST viewport -- the height the page has once
     the address bar has retracted -- so 40vh is more than 40% of what the
     reader actually sees on load, which is the exact moment being designed
     for. `svh` is the small viewport: the height while the browser chrome
     is still showing. vh is kept as the preceding declaration so a browser
     without svh gets the near-enough answer rather than nothing.

     A HARD CAP, with the content laid out to fit inside it rather than
     scaled to it. The hero becomes a centred column and the type keeps its
     own sizes -- the kicker, headline, rule and standfirst come to roughly
     315px at 390x844, inside the ~338px this allows. overflow:hidden is
     the backstop, not the mechanism: if a future line pushed past the cap
     it would be clipped, and clipping is a visible failure that gets
     fixed, where letting the hero grow is an invisible one that puts the
     story back below the fold.

     THE FLEX COLUMN IS WHY THE KICKER'S OWN FIX MATTERED. Laying children
     out as flex items sizes them to their content instead of the line, and
     that is precisely the condition under which an element without auto
     side margins goes left while its siblings stay centred -- the bug that
     took four rounds. It centres here because it now has `width:fit-content`
     and `margin:0 auto`, like the three elements below it. */
  .hero{padding:20px;display:flex;flex-direction:column;justify-content:center;
    height:40vh;height:40svh;max-height:40vh;max-height:40svh;overflow:hidden}
  /* TWO LINES ON A PHONE, WHICH IS A REQUIREMENT AND NOT A PREFERENCE.
     Sudeep, 2026-08-25: the headline "needs to fit in two lines on the
     mobile viewport."

     MEASURED IN A BROWSER WITH IBM PLEX SERIF ACTUALLY LOADED, after two
     estimates from average character width had both been wrong. The
     largest size that still fits two lines:

         320px viewport -> 21px      360px -> 23.75px      390px -> 26px

     Those points are very nearly a straight line, because the limit is
     just available width divided by the string -- so a fluid clamp fits
     the constraint exactly, instead of forcing every phone down to the
     21px that only the narrowest one needs. 6.5vw sits under all three
     (20.8px at 320, 23.4px at 360, 25.4px at 390), floored at 21px below
     320px and capped at 26px above 390px, where the extra width means
     26px is still comfortably two lines.

     PLAIN vw RATHER THAN calc(-1.86px + 7.14vw), which fits the three
     points more tightly but cannot be written here at all: the type-scale
     guard looks the value up twice, once raw and once with ALL whitespace
     stripped from the sheet, and calc() is invalid CSS without spaces
     around its operator -- so no calc value can satisfy both lookups. A
     slightly looser line that clears the limit at every width is worth
     more than a tighter one that cannot be guarded.

     WHY NOT JUST THE HEADLINE ROLE. Its clamp evaluates to 26.24px at
     390px -- a QUARTER OF A PIXEL over the 26px limit, and enough to make
     it three lines. It is three lines at every width from 320px to 390px.
     That is exactly the margin that cannot be eyeballed, and it is why
     this value is registered as a type-scale exception rather than
     rounded to a role.

     PADDING WAS CONSIDERED AND REJECTED: dropping the hero's 20px to 12px
     buys ONE pixel of headline (21px -> 22px at 320px). Not worth
     narrowing every other element in the block for.

     The cap above is still a hard 40vh with overflow:hidden, so a
     headline that outgrows it DISAPPEARS rather than wraps. Two lines at
     26px is ~60px, well inside the ~285px the cap leaves after padding.

     max-width is released here because the base rule's two-thirds
     proportion is a desktop instruction; on a phone the container is
     already the constraint. */
  .hero h1{font-size:26px;max-width:none}

  .hp{padding:22px 16px 32px}
  /* ROOM FOR THE BROWSER'S OWN FURNITURE. Sudeep, same message: "Some more
     margin at bottom for ensure the browser addressbar does not
     interfere."

     Mobile Safari and Chrome float the address bar OVER the bottom of the
     viewport, so the last ~60px of any page can sit under it until the
     reader scrolls. env(safe-area-inset-bottom) alone does not cover this
     -- that inset is the home-indicator strip, and it reads 0 on a device
     without one -- so a real value is added to it rather than relied on
     instead of it.

     On the page shell rather than the footer: the footer is navy and full
     -bleed, so padding there would just make the navy taller. This puts
     the air between the last card and the bar. */
  .hp{padding-bottom:calc(56px + env(safe-area-inset-bottom, 0px))}
  /* The only structural change on mobile. Everything else is fluid: the
     grids are auto-fit and the type is clamp()ed, so there is no second
     layout to keep in sync -- the failure mode where desktop is fixed and
     mobile inherits something cramped. */
  .hp-empty{flex-direction:column;gap:14px}
  .hp-lead-head{max-width:none}
}

/* ===== Daily board: the carousel =====
 *
 * READ THE DEFAULT RULES AS "NO JAVASCRIPT". Everything outside the
 * `.tfc-js` block below is what a reader gets when the script is off,
 * blocked, or broken: a plain vertical STACK of the same lead panels that
 * shipped this morning, controls hidden because they would do nothing.
 * That is the whole progressive-enhancement story and it is why the
 * carousel cannot produce a blank section -- there is no state in which
 * the slides are not ordinary flow content in the served HTML.
 *
 * `.tfc-js` is added to <html> synchronously by
 * static/js/homepage-carousel.js before first paint, which is what turns
 * the stack into a horizontal snap-scrolling strip with no visible reflow.
 * If that script throws it removes the class again and the stack returns.
 *
 * THE SLIDES ARE .hp-lead. Not a new component -- the same rail, serif
 * headline and serif summary as the lead panel above, rearranged. Only the
 * track and the controls are new here, which is also why a text-only slide
 * (the rung that actually renders today, see _homepage_carousel in main.py)
 * needs almost no styling of its own: it is the typographic panel this page
 * was already built around.
 */
/* 18px, not the 6px this carried until 2026-08-15: the "Read the
   Click-through" paragraph that used to sit here and supply the remaining
   gap has moved to the section title (review_notes 120), so the carousel
   now owns the space between itself and whatever follows. */
.hp-caro{position:relative;margin:0 0 18px}
.hp-caro-track{display:grid;gap:14px}
.hp-caro-slide{margin:0}
/* Equal, substantial slides. Flex stretches them to the tallest one in
   strip mode, but a two-line headline with no picture would otherwise set
   a very short floor -- and a carousel of short panels reads as an
   accident. This is the text variant's whole layout budget. */
.hp-caro--text .hp-caro-slide{display:flex;flex-direction:column;justify-content:center;
  min-height:clamp(210px,24vw,290px)}
.hp-caro--text .hp-lead-sum{max-width:52ch}

/* ===== The typographic front page (.hp-caro--text) =====
 * THE VARIANT MOST READERS ACTUALLY MEET. Four of 1,015 articles in Corpus
 * carry a hero of their own (measured on production 2026-08-17), so "no
 * pictures today" is the ordinary state of this page and not a degraded
 * one. This block is what makes that state look deliberate: it must not
 * read as the image variant with the photographs missing.
 *
 * Three moves, all of them things type can do and a photograph cannot:
 * a standing numeral, a heavier rail, and a longer measure. No substitute
 * imagery of any kind -- see the template's header for why that is a rule
 * and not an oversight.
 */
/* The counter is scoped to the track so it restarts per render and cannot
   continue across the board grid below. */
.hp-caro--text .hp-caro-track{counter-reset:hp-slide}
.hp-caro--text .hp-caro-slide{counter-increment:hp-slide;position:relative;
  border-left-width:6px}
/* THE STANDING NUMERAL. Ornament, so it is generated content rather than
   markup -- it must never reach a story's accessible name or be announced
   before the headline. aria-hidden is not available to ::before, which is
   precisely why this is a CSS counter and not a <span>.
   Set at the SUBHEAD role from the shared scale -- whole, through
   font:var(--type-subhead), rather than as a size and a family this file
   picked for itself. It is ornament, but it is ornament made of digits
   sitting directly above a headline, so it belongs to the same scale and
   the same serif the headline does. In the vertical's own colour at low
   opacity, so the palette does the work the picture used to. */
.hp-caro--text .hp-lead-body::before{content:counter(hp-slide,decimal-leading-zero);
  display:block;font:var(--type-subhead);line-height:1;
  color:var(--vc,var(--navy));opacity:.22;margin:0 0 10px;letter-spacing:-.02em}
/* A longer measure than the image variant allows: with no photograph
   competing for the eye the summary can carry more of the story, and a
   short line under a big headline is what makes an image-less panel look
   unfinished. */
.hp-caro--text .hp-caro-slide .hp-lead-sum{max-width:58ch}

/* ===== The mixed front page (.hp-caro--mixed) =====
 * One or two licensed pictures. The pictured story leads at full size and
 * the rest of the verticals follow as text slides in the same strip.
 *
 * WHAT THIS REPLACED, so nobody restores it by accident: a single
 * photograph used to make the whole section an image carousel of ONE story
 * -- a static panel with no controls -- and pushed every other lead into
 * the board grid. That is the defect reported on 2026-08-17.
 *
 * The text slides here deliberately do NOT borrow the numeral treatment
 * above. Numerals belong to the variant where every slide is type; mixing
 * them with a photographed slide would number some stories and not others.
 */
.hp-caro--mixed .hp-slide--txt{display:flex;flex-direction:column;justify-content:center;
  min-height:clamp(210px,24vw,290px)}
.hp-caro--mixed .hp-slide--txt .hp-lead-sum{max-width:58ch}
/* Controls are meaningless without the script that drives them, so they do
   not exist until it says so. */
.hp-caro-ui{display:none}

/* gap, not 0: two rounded panels butted together read as one broken box,
   and the gutter is what makes the movement legible as "the next story"
   rather than as the page sliding. Deliberately NOT a partial "peek" of
   the next slide -- with flex-basis under 100% the last slide can never
   reach its own snap point, and a carousel that lurches on the fifth press
   is worse than one that under-advertises itself. The arrows and dots are
   the affordance. */
.tfc-js .hp-caro-track{display:flex;gap:14px;overflow-x:auto;overscroll-behavior-x:contain;
  scroll-snap-type:x mandatory;scroll-behavior:smooth;scrollbar-width:none}
.tfc-js .hp-caro-track::-webkit-scrollbar{display:none}
.tfc-js .hp-caro-slide{flex:0 0 100%;min-width:0;scroll-snap-align:start}
.tfc-js .hp-caro-ui{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin:12px 0 0}

/* Controls. Mono, pill, navy-on-paper -- the same vocabulary as .hp-chip,
   because these are chrome for the board and should not compete with the
   headline they sit under. */
.hp-caro-nav,.hp-caro-toggle{font:var(--type-label);background:var(--surface);color:var(--navy);
  border:1px solid var(--line);border-radius:999px;cursor:pointer;line-height:1;padding:0}
/* THE ONE OFF-SCALE SIZE IN THIS FILE, and it is not text. The 19px sets
   the height of a single "<" / ">" GLYPH inside a 34px round button, so it
   is icon sizing -- nobody reads it, and dropping it to the label role's
   11px would leave a 34px control with a barely visible mark in the
   middle. Registered by name, with this reason, in
   tests/test_type_scale_is_single_source.py's _SIZE_EXCEPTIONS; adding a
   second one has to be an argument somebody writes down. */
.hp-caro-nav{width:34px;height:34px;font-size:19px}
/* font-weight:600 REMOVED (2026-08-16). base.html loads IBM Plex Mono at
   400 and nothing else, so a 600 here was a weight the browser SYNTHESISED
   rather than one IBM drew -- the same mechanism review_notes 141 caught
   on /feature's H2, on a smaller element. The scale's second rule: never
   name a weight the family is not loaded at. */
.hp-caro-toggle{height:34px;padding:0 15px;letter-spacing:.1em;text-transform:uppercase;
  margin-left:auto}
.hp-caro-nav:hover,.hp-caro-toggle:hover{border-color:var(--navy);background:var(--sunk)}
.hp-caro-dots{display:flex;align-items:center;gap:2px}
/* 24x24 hit areas (WCAG 2.5.8) around a 9px mark -- the dot readers see is
   small, the target they press is not. */
.hp-caro-dot{width:24px;height:24px;padding:0;border:0;background:none;cursor:pointer;
  display:inline-flex;align-items:center;justify-content:center}
.hp-caro-dot::before{content:"";width:9px;height:9px;border-radius:50%;border:1.5px solid #5A5F70;
  background:transparent}
/* Never colour alone: each dot's accessible name carries the vertical, and
   the current one differs in FILL and SIZE, not only in hue. */
.hp-caro-dot[aria-current="true"]::before{width:11px;height:11px;background:var(--navy);border-color:var(--navy)}
.hp-caro-nav:focus-visible,.hp-caro-toggle:focus-visible,.hp-caro-dot:focus-visible{
  outline:2px solid var(--navy);outline-offset:3px;border-radius:999px}

/* The carousel is the one animated thing on this page. Auto-advance is
   suppressed in JavaScript (homepage-carousel.js never starts the timer,
   and the control reads "Play"); this suppresses the SCROLL animation, so
   a reader who does press Play, or who uses the arrows, gets an instant
   jump rather than a slide. */
@media(prefers-reduced-motion:reduce){
  .tfc-js .hp-caro-track{scroll-behavior:auto}
}

/* ===== From the Columns: the Stream B carousel (cp-508) =====

   Sudeep, 2026-09-22: "REPLACE Ad Banner with Stream B Carousel... keep
   the vertical pills", then, having seen a text-only version built to his
   own "sleek layout" suggestion: "The previous carousel for Stream B
   content was nicer." The text version is gone; this is what shipped.

   IT REUSES THE CAROUSEL'S BEHAVIOUR AND ITS CHROME, not its layout. The
   track/slide contract is the one homepage-carousel.js already drives for
   the Daily strip -- a horizontally scrollable flex track whose children
   are full-width snap targets -- and the controls are literally
   _parts.controls, so .hp-caro-nav/.hp-caro-dot/.hp-caro-toggle above
   style this section too and the pause affordance cannot diverge between
   the two carousels.

   ONE SLIDE AT A TIME AT EVERY WIDTH. Three across on desktop would leave
   the arrows and dots with nothing to do on exactly the screens where they
   are most visible, and a control cluster that does nothing is worse than
   none. The desktop width is spent on the two-column split instead.

   WARM GROUND from components.css's .hp-card--b, the Stream B tint the
   product cards already use for our own writing. Declared there, not here:
   one definition of "this is ours". */
.hp-bcaro{position:relative;margin:0 0 22px}
.hp-bcaro-track{display:grid;gap:14px}
.hp-bcaro-slide{margin:0;padding:18px;border:1px solid var(--rule);border-radius:10px;
  display:grid;gap:16px;align-content:start}

/* PICTURE AND CREDIT ARE ONE ITEM. The <figure> is what keeps the credit
   under its own photograph when the slide becomes two columns below --
   as siblings they were split across the columns and the credit landed
   above the headline, which is the defect Sudeep reported. */
.hp-bcaro-fig{margin:0;display:grid;gap:6px;align-content:start}
.hp-bcaro-img{width:100%;height:auto;aspect-ratio:16/9;object-fit:cover;border-radius:6px;
  display:block}
.hp-bcaro-fig figcaption{margin:0}
.hp-bcaro-fig .imgcredit{margin:0}

.hp-bcaro-body{display:grid;gap:10px;align-content:start}
.hp-bcaro-eyebrow{font:var(--type-label);letter-spacing:.1em;text-transform:uppercase;
  margin:0;display:flex;gap:10px;align-items:baseline;flex-wrap:wrap}
.hp-bcaro-product{color:var(--navy);font-weight:600}
.hp-bcaro-vertical{color:var(--color-text-caption)}
/* THE ROLES, NOT HAND-PICKED VALUES. .hp-lead-head is --type-headline and
   .hp-lead-sum is --type-lead, and a Stream B slide is the same two things
   at the same weight -- so it takes the same two roles rather than a pair
   of px values that happened to look right. The first version of this
   block named a family and three sizes and was caught by
   test_type_scale_is_single_source, correctly. */
.hp-bcaro-hl{font:var(--type-headline);margin:0}
.hp-bcaro-hl a{color:var(--navy);text-decoration:none}
.hp-bcaro-hl a:hover{text-decoration:underline}
.hp-bcaro-sum{font:var(--type-lead);color:#3a3a38;margin:0;max-width:60ch}
.hp-bcaro-meta{font:var(--type-label);color:var(--color-text-caption)}

/* THE DESKTOP SPLIT: picture left, words right. Below this breakpoint the
   grid falls back to the stacked declaration above -- no second rule and
   no duplicated values. */
@media(min-width:760px){
  .hp-bcaro-slide{grid-template-columns:minmax(0,1.05fr) minmax(0,1fr);
    column-gap:24px;align-items:start}
  .hp-bcaro-fig{align-self:start}
  .hp-bcaro-img{aspect-ratio:4/3}
}

.tfc-js .hp-bcaro-track{display:flex;gap:14px;overflow-x:auto;overscroll-behavior-x:contain;
  scroll-snap-type:x mandatory;scroll-behavior:smooth;scrollbar-width:none}
.tfc-js .hp-bcaro-track::-webkit-scrollbar{display:none}
.tfc-js .hp-bcaro-slide{flex:0 0 100%;min-width:0;scroll-snap-align:start}

@media(prefers-reduced-motion:reduce){
  .tfc-js .hp-bcaro-track{scroll-behavior:auto}
}

/* ===== The refer box =====
   The component itself is components.css section 8; this is the one value
   the homepage mockup sets differently. Home.dc.html separates the box
   from the columns above it by 34px, the product mockups by 24px, and the
   reason is the thing directly above it: on a product page that is a
   single-column reading flow, here it is a five-across grid of column
   cards whose bottom edge is a hard line all the way across. Declared in
   the homepage's own layer rather than as a style attribute on the page,
   so the component keeps one definition and this stays a homepage fact. */
.hp .pp-promo{margin-top:34px}

/* ===== The daily board (H-2 and H-3) =====
   design/mockups' Board toggle, `.imp-board`: the day's lead on the left
   at 2fr, every other vertical stacked beside it at 1fr. Values are the
   mockup's. The 820px collapse is its own breakpoint, not one of the
   page's two, because that is the width at which a 1fr rail stops being
   readable rather than a width the rest of the page cares about. */
.hp-board{display:grid;grid-template-columns:2fr 1fr;gap:14px;margin:0 0 18px}
@media(max-width:820px){.hp-board{grid-template-columns:1fr}}
/* The lead takes the slide's own box -- surface, hairline, 6px rail in the
   vertical's colour -- so a board lead and a carousel slide are the same
   object seen in two layouts, which is what lets them share story_body(). */
.hp-board-lead{box-sizing:border-box;background:var(--surface);border:1px solid var(--line);
  border-top:6px solid var(--vc,#001234);border-radius:4px;overflow:hidden;
  display:flex;flex-direction:column}
.hp-board-rail{display:flex;flex-direction:column;gap:10px}
/* RAIL ENTRIES TAKE A LEFT RAIL, not a top one. Stacked vertically, five
   top-borders read as five separate panels; a left edge reads as one list
   of five things, which is what it is. 4px is the column card's weight --
   the quietest of the three, as on the homepage columns. */
.hp-board-card{box-sizing:border-box;background:var(--surface);border:1px solid var(--line);
  border-left:4px solid var(--vc,#001234);border-radius:4px;padding:12px 14px;flex:1}
.hp-board-card .hp-vert{margin:0 0 6px}
/* The subhead role, unmodified. The mockup writes 18px here and the
   role IS the mockup's 18px at desktop -- restating it as a literal
   would put a nineteenth hardcoded size back into a file that spent
   a whole pass removing them. */
.hp-board-card h4{font:var(--type-subhead);margin:0}
.hp-board-card h4 a{color:var(--navy);text-decoration:none}
.hp-board-card h4 a:hover{text-decoration:underline;text-decoration-color:var(--yellow)}

/* The rail entry's day. The label voice, not the lead's pill -- five
   pills down a 1fr column read as a column of pills rather than a column
   of stories. See _parts.html's daily_board() for why a rail entry states
   its day at all when the mockup's does not. */
.hp-board-day{font:var(--type-label);letter-spacing:.06em;text-transform:uppercase;
  color:var(--color-text-caption);margin:0 0 6px}

/* THE TWO NARROWER PHONE STEPS. Sudeep's table, 2026-08-25:
   320 and 360 at 21px, 375/390/393 at 24px, 414 and 430 at 26px -- so the
   boundaries fall at 375px and 414px, and the 640px block above carries
   the 26px top step.

   ORDER IS LOAD-BEARING AND IS THE ONLY REASON THIS WORKS. All three
   rules are `.hero h1` at identical specificity and every one of them
   matches on a 320px screen, so the LAST to appear wins. Narrowest last.
   Move these above the 640px block and every phone renders at 26px --
   silently, with no warning anywhere, and three lines on the small ones.

   CHECKED AGAINST THE MEASURED TWO-LINE CEILING at each width rather than
   accepted on trust, because the whole requirement is two lines:

       320 -> 21px (ceiling 21.0)     390 -> 24px (ceiling 26.0)
       360 -> 21px (ceiling 23.75)    393 -> 24px (ceiling 26.1)
       375 -> 24px (ceiling 24.9)     414 -> 26px (ceiling 27.6)
                                      430 -> 26px (ceiling 28.6)

   320px is the one with no headroom at all: 21px IS the ceiling there.
   Anything narrower than 320px, or any future edit that lengthens the
   headline, goes to three lines -- and .hero clips rather than scrolls,
   so it would vanish rather than wrap. */
@media (max-width:413px){ .hero h1{font-size:24px} }
@media (max-width:374px){ .hero h1{font-size:21px} }
