/*
Theme Name: londonly
Theme URI: https://londonly.ca
Author: Hey Master
Description: London, Ontario city publication — a hey-master child theme. Layout, templates and behaviour are inherited from the hey-master parent; site identity (name, social, analytics) is provided at provisioning time via the wpultra_site_config option. Add London-only CSS below.
Version: 1.2.2
Requires at least: 6.0
Tested up to: 6.7
Requires PHP: 8.0
License: GNU General Public License v2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html
Text Domain: hey-child-londonly
Template: hey-master
*/

/* ═══════════════════════════════════════════════════════════════════════════
   LONDONLY — "Twilight Canopy"
   ---------------------------------------------------------------------------
   Londonly's identity is asserted HERE: hey-master hard-codes its palette/fonts
   and does NOT read branding.* from config (verified). "Less techy" version of
   londonly.ca's Forest-City look — the neon mint deepened to an editorial canopy
   green, on hey-master's cream layout with one bark-green dark section carried
   over for mood; slab "Londonly" wordmark + bronze kept; no neon, no mono.

   Maintainable channels ONLY (no parent CSS copied, no layout change):
     1. self-hosted @font-face (+ alias the parent's hard-coded 'DM Serif
        Display' → Fraunces; DM Sans body + Bebas kickers left UNTOUCHED = 0 reflow)
     2. :root design-token overrides (re-point --red → brand green)
     3. the logo wordmark restyle + a few named dark-section backgrounds
   The house CATEGORY / mega-menu colours are deliberately NOT touched (no
   inline-accent flips) — they are the section colour-coding, not the brand.
   ═══════════════════════════════════════════════════════════════════════════ */

/* 1 ── Type — self-hosted (no external CDN; deploy-safe) ──────────────────── */
/* 2026-08-05: LATIN-EXT ADDED. The shipped `-latin` subsets carry 222/223
   codepoints and only 3 of the 128 Latin Extended-A slots (ı Œ œ). Full Latin-1 is
   present, so French is safe — "Montréal" renders in Fraunces. But ř ł š ā are
   NOT, and before the token repoint those glyphs came from the parent's Google
   subset. A name like "Dvořák" in a restaurant write-up would render one glyph in
   Georgia mid-word — different x-height, different stroke contrast, and a third
   face entirely on clients without Georgia.

   EVERY face now declares a `unicode-range`, including the pre-existing ones. That
   is load-bearing, not tidiness: a face WITHOUT unicode-range is a candidate for
   every codepoint, so the `-latin` file could win the match for ř, find no glyph,
   and fall straight through to Georgia — the latin-ext file would never be
   consulted. Ranges are Google's own, verbatim, so the split matches the subsets.

   Both new files are the same variable font as the originals (opsz 9–144,
   wght 100–900), so nothing about the existing rendering changes.

   KNOWN RESIDUAL: U+2192 (→) is in neither range — Google's latin subset carries
   U+2191 and U+2193 but not U+2192 — so a right-arrow inside a serif element still
   falls back. U+2020 (†) IS covered, by latin-ext. */
@font-face{font-family:'Fraunces';font-style:normal;font-weight:400 900;font-display:swap;src:url('fonts/fraunces-latin.woff2') format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;}
@font-face{font-family:'Fraunces';font-style:normal;font-weight:400 900;font-display:swap;src:url('fonts/fraunces-latinext.woff2') format('woff2');unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;}
@font-face{font-family:'Fraunces';font-style:italic;font-weight:400 900;font-display:swap;src:url('fonts/fraunces-italic-latin.woff2') format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;}
@font-face{font-family:'Fraunces';font-style:italic;font-weight:400 900;font-display:swap;src:url('fonts/fraunces-italic-latinext.woff2') format('woff2');unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;}
@font-face{font-family:'Hepta Slab';font-style:normal;font-weight:600 800;font-display:swap;src:url('fonts/hepta-slab-latin.woff2') format('woff2');}
/* Alias hey-master's hard-coded headline family → Fraunces (global, refactor-proof,
   no per-selector overrides). Body (DM Sans) + kickers (Bebas Neue) untouched. */
/* 2026-08-04 — WIDENED 600 → `400 900`. The previous comment here claimed that
   declaring a SINGLE weight makes every requested weight clamp to it. That is true
   of a family you own outright; it was FALSE in this codebase, and the difference
   was live on every page.

   hey-master `@import`s the REAL Google 'DM Serif Display' at weight 400
   (`hey-master/style.css:16`). So the family had TWO normal-style faces — Google's
   at 400 and this alias at 600 — and CSS weight matching sent every request at
   400 or 500 to GOOGLE's face, not Fraunces. Two different serifs in one article:
   `.article-body p` is --font-serif with no weight (400 → Google DM Serif Display)
   while `.article-body blockquote p` is italic, and the italic alias below already
   declared `400 600`, so pull-quotes DID get Fraunces italic.

   Widening to a 400–900 variable range fixes weights 500–900 — measured on :8092,
   'DM Serif Display'@500…@900 now equal 'Fraunces'@500…@900 exactly, where before
   the widen 500 fell through to Google's 400 face.

   IT DOES NOT FIX 400, AND 400 IS WHERE THE BODY PROSE LIVES. At exactly 400 both
   faces are an EXACT match — Google's static 400 and this range's lower bound — and
   Chrome resolves that tie to the earlier-declared face, i.e. the parent's. Proved
   on :8092: with this alias loaded (`document.fonts` status `loaded`) and Google's
   face UNLOADED on the homepage, `'DM Serif Display'@400` still rendered Times New
   Roman rather than falling to this face. Declaration order does not break the tie;
   the child sheet already loads last. See the token repoint below, which does. */
/* 2026-08-05: the alias gets the same latin-ext treatment as 'Fraunces' above.
   Nothing in the parent names this family any more (the token repoint below is what
   renders), so these four rules are belt-and-braces only — but an un-extended
   belt-and-braces would reintroduce the exact glyph gap it exists to guard against
   the moment it ever engages. Extended for symmetry, not because it is reachable. */
@font-face{font-family:'DM Serif Display';font-style:normal;font-weight:400 900;font-display:swap;src:url('fonts/fraunces-latin.woff2') format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;}
@font-face{font-family:'DM Serif Display';font-style:normal;font-weight:400 900;font-display:swap;src:url('fonts/fraunces-latinext.woff2') format('woff2');unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;}
@font-face{font-family:'DM Serif Display';font-style:italic;font-weight:400 600;font-display:swap;src:url('fonts/fraunces-italic-latin.woff2') format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;}
@font-face{font-family:'DM Serif Display';font-style:italic;font-weight:400 600;font-display:swap;src:url('fonts/fraunces-italic-latinext.woff2') format('woff2');unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;}

/* 1a ── THE ROOT-CAUSE FIX — repoint the token, don't fight the font matcher ──
   Runbook §5(a), the PREFERRED method. `'DM Serif Display'` occurs exactly ONCE in
   the whole parent — this token's definition at `hey-master/style.css:49`. There is
   not one literal per-selector use in `style.css` or `extra.css`, and not one in any
   PHP template or JS file (grepped). So the token is the single choke point for the
   entire serif role, and repointing it means nothing asks for the aliased family any
   more: the 400 tie-break above simply stops existing, at every weight, for every
   consumer, including ones this session never enumerated.

   Generic family included deliberately — runbook §3: every parent consumer is a bare
   `font-family:var(--font-serif)` with no appended fallback, so a token without a
   generic removes the site's last-resort fallback.

   The alias @font-face above is KEPT as belt-and-braces: it still serves anything
   that names the family directly (and would catch a future parent literal).

   ACCEPTED CONSEQUENCE (operator decision, not a bug): body prose lightens site-wide
   in one step — Fraunces 400 is ~12% narrower than DM Serif Display 400 (measured
   2730.00px → 2335.75px on a real article paragraph). Chosen over a 500 middle path.
   Reverting is this one line, but the ~17 automatic title fixes revert with it. */
:root{ --font-serif:'Fraunces', Georgia, serif; }

/* 1b ── Weight guards — surfaces that DECLARE 400 and would go light ──────────
   Widening the alias means each selector finally renders at its DECLARED weight.
   A few declare 400 explicitly and were only reading as substantial because the
   clamp-that-wasn't happened to land them on a heavier face. Pinned to 600.

   Deliberately NOT blanket-pinned: an `<h2>`/`<h3>` with no author `font-weight`
   already computes to the UA stylesheet's bold (700) and resolves correctly —
   `.pulse-feature__title` and `.pulse-discover__heading` were both wrongly flagged
   as light on a first pass and need no rule. Only selectors carrying an explicit
   `font-weight:400` in the parent are listed here.

   2026-08-05 review wave: the spec's table had two rows; the real population is
   four. The two below were filed as "could not be checked, the pages do not
   resolve locally" — they do resolve (it is `/privacy/` that 404s, not
   `/privacy-policy/`), and more to the point both are decidable from source: each
   carries an explicit `font-weight:400`, so neither is the "h2 inherits UA bold"
   case that the do-not-blanket-pin rule is protecting. Pinned for the same reason
   as the first two. */
.about-hero__title{ font-weight:600; }        /* style.css:2094 — H1,  clamp(40px,6vw,80px)/400 */
.about-value__heading{ font-weight:600; }     /* style.css:2137 — DIV, 22px/400 */
.about-body__content h2{ font-weight:600; }   /* style.css:2113 — H2,  30px/400 */
.legal-page__body h2{ font-weight:600; }      /* style.css:3048 — H2,  24px/400 */

/* 2026-08-05 review wave — the five the table never had. Rather than trust a
   2-row table, all 71 normal-and-italic `--font-serif` rules across the THREE
   enqueued parent sheets (style.css, extra.css, directory-map.css) were swept from
   source, then every one that actually renders was resolved in the live DOM —
   because selector text cannot tell an `<h2 class="…__title">` (UA bold, fine)
   from a `<div class="…__title">` (inherits 400, goes light). Only the DOM knows.

   That vindicated the spec's warning in both directions: `.pulse-discover__heading`
   and `.error-404__title` really are h2/h1 at UA bold 700 and correctly need no
   rule — but it also found five heading-role DIVs that get no UA bold and had
   quietly dropped to body weight. Structurally identical to `.about-value__heading`
   above, so treated identically.

   NOT pinned, deliberately:
     `.evt-info__step-title` — ITALIC (style.css:3621). The italic alias already
       declared `400 600`, so italic surfaces were on Fraunces BEFORE this session
       and are unchanged by it. Nothing lightened; nothing to guard.
     `.evt-info__step-title` — see above; italic, unchanged by this session.

   2026-08-05 — FOUR MORE PINNED, after code review refuted the reasoning below.
   The previous note declined to pin three of these because "their ELEMENT is
   unknown … if any is an h2 it is already 700 and a 600 pin would make it
   LIGHTER". That was answerable from source in seconds and the answer is that all
   three are DIVs, so all three had silently dropped to body weight:
     `.newsletter-strip__heading`      template-parts/newsletter-strip.php:13  DIV
     `.newsletter-strip__done-title`   template-parts/newsletter-strip.php:25  DIV
     `.event-card-featured__title`     functions.php:1959                      DIV
   "No local fixture" was true but irrelevant — absence from a 6-post fixture is not
   absence from the site. `.newsletter-strip__heading` renders on every category
   archive (`category.php:98`) and on author archives past the 6th post.

   `.hood-empty__heading` was never enumerated at all — a silent omission rather
   than a decision. It is a `<p>` (taxonomy-neighbourhood.php:206,
   taxonomy-place.php:97) consuming --font-serif with no declared weight, i.e.
   structurally identical to `.events-empty__title`, which WAS pinned. londonly has
   51 neighbourhood pages and most are thin at launch, so the empty state is the
   common case, not the edge case.

   `.article-card--wide .article-card__title` was previously defended here as "a
   deliberate variant". It is DEAD CODE: `template-parts/article-card.php:10` is the
   only consumer of the `wide` arg and its only caller passes `'wide' => false`
   (`template-parts/front/feed.php:52`). No pin, because nothing renders it — but
   the reason is "unreachable", not "deliberate". */
.newsletter-strip__heading{ font-weight:600; }    /* style.css:1013 — DIV, 25px/400 */
.newsletter-strip__done-title{ font-weight:600; } /* style.css:1057 — DIV, 18px/400 */
.event-card-featured__title{ font-weight:600; }   /* style.css:1560 — DIV, 19px/400 */
.hood-empty__heading{ font-weight:600; }          /* extra.css:1355 — P,   18px/400 */
.rail-newsletter__heading{ font-weight:600; }     /* extra.css:142  — DIV, 19px/400 */
.rail-submit-event__heading{ font-weight:600; }   /* extra.css:840  — DIV, 17px/400 */
.events-empty__title{ font-weight:600; }          /* extra.css:865  — DIV, 22px/400 */
.hood-rail__nl-heading{ font-weight:600; }        /* extra.css:1491 — DIV, 17px/400 */
.hood-newsletter__heading{ font-weight:600; }     /* extra.css:5730 — DIV, 25px/400 */

/* 2 ── Palette — re-point hey-master's design tokens ──────────────────────── */
:root{
  --brand:#1B7A50;        /* Canopy Green — softened from londonly.ca's neon mint */
  --brand-deep:#16311F;   /* bark green — the one dark section that carries the mood */
  --bronze:#A9885A;       /* retained radar-ring warmth */

  --red:var(--brand);         /* THE brand token → flips logo/buttons/nav/brand chrome */
  --accent-news:var(--brand); /* news/default category chrome that reads as the brand   */

  --black:#15221B;        /* warm forest ink (text + var-based dark) — off pure #0d0d0d */
  --grey-bg:#EEF1EC;      /* cool-cream panel tint */
  --grey-text:#5f6b62;    /* sage-grey secondary text */
  /* --white (#fafaf8 cream) kept · category accents (culture/food/best-of/…) untouched */
}

/* 3 ── Dark sections → bark green (hero + footer). Named backgrounds only. ── */
.site-footer,
.hero-banner{ background:var(--brand-deep); }

/* 4 ── Logo — the Hepta Slab "Londonly" wordmark ─────────────────────────── */
/* Parent now renders  <logo-hey>London</logo-hey><logo-city>ly</logo-city>  (no dot). */
.logo-hey,.mobile-logo__hey,.site-footer__logo-hey{
  font-family:'Hepta Slab',Georgia,serif;font-weight:800;font-style:normal;
  color:var(--black);letter-spacing:-.01em;text-transform:none;margin:0;
}
.logo-city,.mobile-logo__city,.site-footer__logo-city{
  font-family:'Hepta Slab',Georgia,serif;font-weight:800;font-style:italic;
  color:var(--brand);letter-spacing:0;text-transform:none;margin:0;
}
.site-footer__logo-hey{color:#eef3ee;}          /* on the dark footer, "London" goes cream */
.logo-dot,.mobile-logo__dot,.site-footer__logo-dot{display:none;}  /* separator retired */
/* Join "London" + "ly": the retired dot left a 3px flex gap on the logo row. */
.logo,.mobile-logo,.site-footer__logo{gap:0;}

/* 5 ── Keep-house exceptions (wayfinding colours, NOT brand green) ────────── */
/* Events keeps its house red (matches the reference site); the --red→green flip
   would otherwise turn the top-nav Events link green. */
.nav-btn.is-events,.nav-btn.is-events .nav-btn__ico,.nav-btn.is-events svg{color:#e8341c;}

/* 6 ── Rounded card imagery (matches the mockup). Only border-radius — the
   parent's aspect-ratio (3/2) is untouched, so the one image aspect is preserved.
   2026-08-04: 14px → 12px, the mockup's `.card .ph` value (PARITY.md row 9.2).
   2026-08-04: `.mm-hero-card > div` narrowed to `:first-child`. The mega hero
   card has TWO direct div children — the image wrapper and `.mm-hero-body` —
   so the bare `> div` was rounding the text block too. Inert (no background or
   clip on the body) but the selector did not say what it meant. Both the
   thumbnail branch and the `hm_placeholder_img()` branch emit the image
   wrapper first, so `:first-child` hits the same element on either path. */
.article-card__thumb,
.event-card-featured__thumb,
.event-row__thumb,
.dir-card__thumb,
.mm-hero-card > div:first-child{border-radius:12px;}

/* 7 ── Subscribe button in the brand green (parent ships it dark) ─────────── */
.btn-sub{background:var(--brand);}
.btn-sub:hover{background:var(--brand-deep);}

/* 8 ── Make the big serif headlines read bold/warm like the mockup (not the
   hairline default).

   2026-08-04 (typography pass): the Fraunces-italic hero deck that used to sit
   here has been REMOVED, not moved. Last session added it to hit the mockup's
   `.hsub` parity; direction C puts decks on DM Sans instead, so the rule is gone
   and `.hero-banner__sub` is restated in full in group F below. See spec §7
   divergence 2 — this reverses last session deliberately.

   ⚠ ORDERING TRAP — DO NOT TIDY THIS LINE UPWARD. It carries three selectors with
   two opposite requirements. `.article-hero__title` genuinely wants 600 (and, now
   that it is Fraunces rather than Bebas, that weight finally does something — it
   was inert while the family was Bebas). `.hero-banner__title` wants 700, and the
   hero rule in group F beats this one ONLY because it sits LATER in the file at
   the same (0,1,0) specificity. Move this line down, or the hero rule up, and the
   hero silently drops to 600. Do not remove `.hero-banner__title` from here
   either without re-checking group F. */
.hero-banner__title,.article-hero__title,.article-card__title{font-weight:600;}

/* ═══════════════════════════════════════════════════════════════════════════
   2026-08-04 — MOCKUP-PARITY PASS (home · masthead · menus · logo)
   ---------------------------------------------------------------------------
   Closes the gaps between :8092 and the embedded homepage panel of
   C:\temp\londonly-mockup\londonly-mockup.html (Twilight Canopy direction).
   Values are read from the mockup's OWN source CSS, not picked off pixels.
   Full row-by-row justification: C:\temp\hey-refactor\restyle\PARITY.md

   OPERATOR ORDER HONOURED THROUGHOUT: every category / section colour stays
   byte-identical — the editorial-nav dots, the mega-panel accents, the hero
   and card category pills, the Events/Directory wayfinding accents. Where the
   mockup disagreed with one, the CURRENT colour won and the row is logged
   KEPT-BY-ORDER. Nothing below sets a category colour.

   The mockup's three type roles, for reference:
     eyebrow / kicker / label / button  → Bebas Neue   (var(--font-display))
     headline / deck                    → Fraunces     (via the DM Serif alias)
     body / UI                          → DM Sans      (var(--font-body))
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── A · TOPBAR ────────────────────────────────────────────────────────────
   Mockup `.util`: bark-green bar, Bebas 13px/.1em, pale-canopy text.
   The bar is the one dark surface still on --black; the mockup puts all three
   dark surfaces (topbar, hero, footer) on --brand-deep. */
.topbar{ background:var(--brand-deep); }
.tb-chip,
.tb-link{ font-family:var(--font-display); font-size:13px; font-weight:400; letter-spacing:.1em; }
.tb-chip{ color:#cfe0d5; }
.tb-link{ color:rgba(207,224,213,.8); }
.tb-link:hover{ color:#fff; }
/* #2e2e2e was a near-invisible hairline once the bar went bark-green. */
.tb-sep{ background:rgba(207,224,213,.25); }

/* ── B · WORDMARK ──────────────────────────────────────────────────────────
   Mockup `.logo`: both halves 32px / line-height 1 / -.01em, and the FOOTER
   wordmark is deliberately smaller (26px) than the masthead's. The parent
   sizes its two spans for a different wordmark shape (26 + 29 masthead,
   32 + 36 footer), which inverts that hierarchy and leaves the halves
   mismatched within each wordmark. */
.logo-hey,
.logo-city{ font-size:32px; line-height:1; letter-spacing:-.01em; }
.site-footer__logo-hey,
.site-footer__logo-city{ font-size:26px; line-height:1; letter-spacing:-.01em; }

/* ── C · UTILITY NAV ───────────────────────────────────────────────────────
   Mockup `.nav` 15px/500 · `.pilltrend` outlined, not filled · `.btn` in Bebas.
   Scoped to the utility list so the editorial row (row 2) is untouched —
   `.nav-btn.ed-root` out-specifies a bare `.nav-btn` anyway, but naming the
   list keeps the intent readable.
   NOT TOUCHED: .nav-btn.is-events / .is-dir colours (wayfinding accents,
   KEPT-BY-ORDER). */
.nav-list--utility .nav-btn{ font-size:15px; font-weight:500; }

.btn-trending{
  background:transparent; border:1px solid var(--border);
  font-size:13px; font-weight:500; color:var(--grey-text);
}

.btn-sub{
  font-family:var(--font-display); font-size:16px; font-weight:400;
  letter-spacing:.08em; color:#fff;
}

/* ── D · EDITORIAL NAV ROW ─────────────────────────────────────────────────
   Mockup `.catbar` is a WHITE band under the cream masthead — on --white
   (#FAFAF8) the row and the masthead are the same colour and the band reads
   as one slab. Item type 13px -> 14.5px.
   NOT TOUCHED: .ed-dot / --ac (the section colour-coding), KEPT-BY-ORDER. */
.nav-editorial{ background:#fff; }
.nav-btn.ed-root{ font-size:14.5px; }

/* ── E · MEGA / DROP-DOWN PANELS — TYPE ONLY (constraint 2) ────────────────
   The mockup has no drop-downs, so nothing here is colour work; these are the
   two places where the panels fall out of the house type system once the rest
   of the page adopts it.
   1. `.mm-col-title` is the same eyebrow ROLE as "MORE STORIES" and the footer
      column headers — the mockup renders that role in Bebas.
   2. `.mm-hero-title-ed` asks for --font-serif at weight 400. The child's
      alias only redeclares 'DM Serif Display' at weight 600, so weight 400
      matches the PARENT's real Google DM Serif Display face and this headline
      silently escapes Fraunces. Measured on :8092: 400/40px 'DM Serif
      Display' = 686.6px advance vs Fraunces@400 = 723.28px (different faces);
      at 600 both = 747.97px (the alias resolves). Pinning the weight to 600
      puts it back on Fraunces without widening the alias globally. */
.mm-col-title{
  font-family:var(--font-display); font-size:15px; font-weight:400;
  letter-spacing:.14em;
}
.mm-hero-title-ed{ font-weight:600; }

/* ── F · HERO PANEL ────────────────────────────────────────────────────────
   The bark-green field already matched; everything ON it did not. The parent
   paints the whole hero in white-alpha ramps (.35 / .32 / .30 / .18), which
   composite to muddy greens on #16311F — the mockup uses opaque, deliberately
   warm values instead, and it is the only place --bronze is spent.
   NOT TOUCHED: `.hero-banner__cat-pill` colour / background (the category
   pill), KEPT-BY-ORDER — only its type moves. */
.hero-banner__latest{
  font-family:var(--font-display); font-size:14px; font-weight:400;
  letter-spacing:.16em; color:var(--bronze);
}
/* Type only — the pill's colour and tint are the category's, and stay. */
/* `.hero-banner__hood` joins the kicker family for the same reason
   `.article-card__hood` did in Wave 1: without it the new neighbourhood label
   stays on the parent's 10px DM Sans beside a 13px Bebas category pill, and the
   pair stops reading as one object - which is the whole premise of the
   outline/filled pairing. Measured on the local stack: 19px against 23px
   before, 23px against 23px after. No new declarations, one selector. */
.hero-banner__cat-pill,
.hero-banner__hood,
.hero-banner__cat-label{
  font-family:var(--font-display); font-size:13px; font-weight:400;
  letter-spacing:.1em;
}
/* Hero headline — direction C (serif headline, DM Sans deck). 42px/600 → 46px/700.
   Reaches weight 700 through `var(--font-serif)` rather than naming 'Fraunces'
   directly, which only became possible once the serif role was fixed in group 1a.
   This rule MUST stay below the `font-weight:600` line in group 8 — same (0,1,0)
   specificity, so later wins. See the ordering trap noted there.
   `max-width:560px` is load-bearing, not decoration: the parent sets 500px, and
   every number in the spec's fit table was measured at 560. Omit it and the
   headline wraps earlier and produces more lines than were measured. */
.hero-banner__title{
  font-family:var(--font-serif); font-size:46px; font-weight:700;
  line-height:1.0; letter-spacing:-.022em; color:#fdfcf7; max-width:560px;
}
/* Deck → DM Sans (spec §7 divergence 2, reversing last session's Fraunces italic).
   Restated in full because the group-8 rule that set family/style/size is gone;
   `font-style:normal` is explicit so nothing inherits the italic it replaced.
   Colour is the mockup's `.hsub` #cddccf, unchanged from last session. */
.hero-banner__sub{
  font-family:var(--font-body); font-style:normal; font-size:16px;
  font-weight:400; line-height:1.55; color:#cddccf;
}
.hero-banner__meta{ font-size:13px; letter-spacing:.02em; color:#9fb3a6; }
.hero-banner__author{ font-size:13px; font-weight:700; color:#eef3ee; }
.hero-banner__meta-sep{ color:#9fb3a6; }

/* ── F2 · HERO IMAGE — the half of the hero that ISN'T text ────────────────
   Three parent declarations that a child repointing --black silently
   desynchronises (runbook §11 trap 6: a literal stays literal):

   1. `.hero-banner__image-fade` ramps from `#0d0d0d` — a hard-coded copy of the
      parent's DEFAULT --black. londonly's hero is bark green, so the overlay
      was smearing near-BLACK across the image's left edge instead of
      dissolving the green panel into the photo. Re-ramped on --brand-deep.
      The end stop is `rgba(22,49,31,0)` rather than `transparent`: a gradient
      to `transparent` interpolates through rgba(0,0,0,0) and picks up a grey
      cast in the middle of the ramp.
   2. `.hero-banner__image` sits on `#1a1a2e` (navy) wherever the photo does not
      cover — visible on any hero whose image is missing or still loading.
   3. The image stopped at the 1200px rail, leaving a 37px bark-green strip
      between it and the viewport edge; the mockup's hero image bleeds to the
      frame edge. Pulled out by the gutter. Desktop only — below 1280 the
      parent stacks the hero and the image is already full-width. `.hero-banner`
      keeps `overflow:hidden`, so the 100vw/scrollbar overshoot is clipped
      rather than producing a horizontal scrollbar.
      This is the second deliberate layout move in the pass. */
.hero-banner__image{ background:var(--brand-deep); }
/* The parent ramps the overlay across 52% of the image because its text column
   used to run 48px into the photo. Once the image bleeds right (below) that
   overlap is 8px — geometry, not content, so it holds for every hero — and a
   262px ramp green-washes a quarter of the photo for an 8px seam. The mockup
   has NO fade at all: a hard vertical edge between panel and image. Kept as a
   narrow feather instead of removing it outright, so the seam stays clean on a
   photo whose left edge happens to be dark. */
.hero-banner__image-fade{
  background:linear-gradient(to right,
    var(--brand-deep) 0%,
    rgba(22,49,31,.35) 3%,
    rgba(22,49,31,0) 8%);
}
/* Desktop hero geometry. Two things the first attempt at this got wrong:

   (a) BLEEDING TO THE VIEWPORT EDGE IS WRONG ON WIDE SCREENS. Pushing the
       image out by the full gutter glues it to the viewport and DETACHES it
       from the content column — at 1920 that left a 312px empty green void
       between the end of the text and the start of the image, and 632px at
       2560. The image must GROW toward the edge, not just move: its left edge
       stays with the text, its right edge extends. Capped so the hero content
       tops out around an 1800px envelope (right edge at centre + 900px)
       instead of tracking the viewport forever.

   (b) THE TEXT LOST ITS BREATHING ROOM. The hero's height is locked to the
       image's `aspect-ratio:3/2`, so it cannot grow with its text — and the
       text block is absolutely positioned, so it silently eats the padding
       instead of pushing the box. Measured at 1280: before this session the
       hero gave the text 44px top / 44px bottom, which is the mockup's 40/45.
       The type scale from group F grew the content 247px -> 317px (the 42px
       title takes this headline to three lines where 40px took two), leaving
       9px / 9px. So the box has to get taller to carry the mockup's type.
       Height is now explicit and the photo covers it — `hm-hero` is 1200x800
       and the parent relies on that 3:2 matching the container exactly, so
       once the container stops being 3:2 the image needs `object-fit:cover`
       or it would letterbox against the background.

   (c) 2026-08-04 — AN EXPLICIT HEIGHT ONLY MOVED THE CEILING; IT DID NOT REMOVE
       IT. Fixing the height at 420px still leaves the box unable to grow, so a
       long headline goes back to eating its own padding. Measured against the six
       real post titles (median 73 chars — the 46-char title used in the first mock
       is the SHORTEST on the site): the live 42px hero squeezed padding from the
       intended 40px to 8px on a 94-char headline, and direction C at 52px in a
       fixed box would have CUT 71px of it off.

       So the hero now has no fixed height at all. `.hero-banner__text` goes back
       to `position:static` — in normal flow it drives the box, instead of being
       absolutely positioned and silently overflowing into `overflow:hidden` — and
       the image is absolutely positioned and stretched top-to-bottom to whatever
       height the text produced. `min-height:420px` keeps a one-line headline from
       collapsing the photo. Result: a constant 44px of padding at every headline
       length rather than a shrinking one. This is the third deliberate layout move
       in this stream.

       The image-envelope cap formula from the previous session is UNCHANGED — it
       is what stops the photo detaching from the text column on wide screens
       (312px void at 1920, 632px at 2560; see (a)). Only the height/position half
       of the rule moves. `.hero-banner__inner` is `position:relative` in the
       parent, so it is the containing block for the absolute image.

   2026-08-05 review wave, two corrections to the above:

   (d) STACKING. Taking the text out of `position:absolute` also made the parent's
       `z-index:2` on it INERT — z-index does nothing on a static element. The
       image is `position:absolute`, and a positioned element paints above a
       non-positioned one whatever the DOM order says, so the photo was covering
       the text column's right 48px. Latent rather than visible (the headline's
       560px measure wraps well before that padding), but one long unbroken word
       exposes it. `position:relative` restores a paint context so `z-index:2`
       means something again.

   (e) VERTICAL CENTRING. `min-height` belonged on `.hero-banner__text`, not on
       `.hero-banner__inner`. The parent's `justify-content:center` lives on the
       TEXT, and once the text shrink-wraps its content there is nothing for it to
       centre within — so a floor on the inner just opened bark-green space below
       the copy (8px on the shortest real title, ~95px on a one-liner). With the
       floor on the text itself the flex centring works again, and the inner still
       inherits the same 420px minimum because the text is what drives its height. */
@media (min-width:1280px){
  .hero-banner__inner{ overflow:visible; }
  .hero-banner__text{
    position:relative; z-index:2; min-height:420px;
    width:62%; padding:44px 48px 44px 32px;
  }
  .hero-banner__image{
    position:absolute; top:0; right:0; bottom:0; height:auto; aspect-ratio:auto;
    width:calc(42% + min(300px, (100vw - 1200px) / 2));
    margin:0 calc(-1 * min(300px, (100vw - 1200px) / 2)) 0 0;
  }
  .hero-banner__image img{ width:100%; height:100%; object-fit:cover; }
}

/* ── G · "MORE STORIES" HEADER ─────────────────────────────────────────────
   Mockup `.feedhdr`: Bebas eyebrow, and the live-ticker line is BRAND GREEN,
   not another grey. Label colour left on --grey-text: the mockup's --soft
   (#6b736c) differs by (12,8,10)/255, which is sub-perceptual, and --grey-text
   has 116 consumers so a global nudge for this would be disproportionate. */
.feed-header__label{
  font-family:var(--font-display); font-size:15px; font-weight:400;
  letter-spacing:.14em;
}
.feed-header__live{ font-size:12.5px; color:var(--brand); }

/* ── H · STORY CARDS ───────────────────────────────────────────────────────
   Mockup `.card`: image, then text straight onto the cream — no card box.
   The parent's fill is `var(--white)`, which IS the page colour, so the only
   thing the box actually draws is the hairline; dropping the border removes
   the box and leaves the mockup's flat card. Radius/overflow stay so the
   image keeps its rounded corners.
   NOT TOUCHED: `.article-card__cat` colour + tint pill (the category chip),
   KEPT-BY-ORDER — only its type moves.
   Titles are the mockup's second Fraunces surface and were DM Sans. The
   `--wide` variant keeps the parent's serif-at-400 treatment (higher
   specificity, and no wide card appears on the homepage). */
.article-card{ background:transparent; border-color:transparent; }
/* The 16/18/18 body inset was the card BOX's interior padding. With the box
   gone it stops reading as padding and starts reading as a misalignment
   against the image edge — the mockup runs text flush to the image. This is
   the one deliberate layout move in this pass; it exists only because the
   border above removed the thing the padding was for. Desktop card only:
   `.article-card--mobile .article-card__body` out-specifies this. */
.article-card--desktop .article-card__body{ padding:12px 0 0; }
/* An elevation shadow with no card to elevate. The hover affordance survives
   via the parent's `.article-card:hover .article-card__title` accent flip. */
.article-card:hover{ box-shadow:none; }

/* `.article-card__hood` joins the kicker family here for the same reason the
   other two are in it. Without it the neighbourhood chip stays on the parent's
   9.5px DM Sans next to a 13.5px Bebas category chip — measured 16px against
   22px — and the pair stops reading as one object, which is the whole premise
   of the outline/filled chip pairing. */
.article-card__cat,
.article-card__hood,
.article-card__label{
  font-family:var(--font-display); font-size:13.5px; font-weight:400;
  letter-spacing:.1em;
}
.article-card__title{
  font-family:var(--font-serif); font-size:19px;
  line-height:1.18; letter-spacing:-.01em;
}
.article-card__meta{ font-size:12.5px; }

/* ── I · FOOTER ────────────────────────────────────────────────────────────
   Same white-alpha problem as the hero: column headers at .3 and links at .45
   composite to rgb(90,101,93) / rgb(127,142,132) on the bark green, roughly
   three times too dim against the mockup's opaque #fff / #a9bbad.
   NOT TOUCHED: the bottom bar (© + Privacy/Terms/…) — the mockup panel has no
   counterpart for it, so there is nothing to adjudicate against. It now reads
   dimmer relative to the lifted columns; flagged for the operator rather than
   invented. */
.site-footer__col-title{
  font-family:var(--font-display); font-size:15px; letter-spacing:.12em;
  color:#fff;
}
.site-footer__col-link{ font-size:13.5px; color:#a9bbad; }

/* ── J · 390 SANITY ───────────────────────────────────────────────────────
   Two things the desktop pass exposes at mobile width. */

/* 1. The mobile wordmark carries the same half-mismatch the masthead had
      (22 + 24) — now conspicuous because both desktop wordmarks match.
      Levelled at the larger of the two so the 54px bar height is unchanged. */
.mobile-logo__hey,
.mobile-logo__city{ font-size:24px; line-height:1; letter-spacing:-.01em; }

/* 2. The mobile card pins its kicker to 9px (`.article-card--mobile
      .article-card__cat` out-specifies the 13.5px above). Bebas has a much
      smaller x-height than DM Sans at the same size, so inheriting the face
      at 9px cost real legibility. Sized up to hold the pre-change reading
      weight; still smaller than the desktop kicker, as the parent intends. */
.article-card--mobile .article-card__cat,
.article-card--mobile .article-card__hood{ font-size:11.5px; }

/* 3. THE CHILD SHEET LOADS LAST, SO AN UNCONDITIONAL CHILD RULE BEATS AN
      EQUAL-SPECIFICITY PARENT RULE **INSIDE A MEDIA QUERY**. The parent tunes
      `.hero-banner__title` down to 24px at `@media (max-width:1279px)`; the
      42px above was silently winning there and blowing the mobile hero up by
      ~83px. Restated at the parent's own breakpoint and value — the mockup has
      no mobile spec, so the parent's responsive intent stands. The leading and
      tracking above are deliberately NOT restated: those should follow the
      type treatment at every width.
      Audited the other direction too (`mediaclash.py`): this is the ONLY
      selector where a parent @media rule and a rule added by this pass set the
      same property. The parent's other responsive rules on restyled selectors
      (`.topbar{display}`, `.event-row__thumb{width}`,
      `.event-card-featured__thumb{aspect-ratio}`) touch properties this pass
      never sets. */
@media (max-width:1279px){
  .hero-banner__title{ font-size:24px; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   2026-08-04 — K · STORY HEADLINES: BEBAS → FRAUNCES  (spec §3.4)
   ---------------------------------------------------------------------------
   The reported symptom was that the homepage reads as a serif publication while
   post pages read as bold condensed. Page-level titles are split across two of
   the parent's three type roles: the homepage hero is --font-serif, while post
   headlines, category archives and every directory surface are --font-display
   (Bebas). Last session's mockup-parity pass widened the gap by moving the
   homepage further into serif while interior pages were out of scope.

   THE EDITORIAL / WAYFINDING LINE, which is what decides who moves:
     a title a reader reads as a STORY HEADLINE  → Fraunces  (these four)
     a title that NAMES A PLACE — directory, category archive, events,
     neighbourhoods, utility pages                → stays Bebas (untouched)

   Sizes come DOWN because Bebas is condensed and effectively all-caps while
   Fraunces is neither. For the same reason the caps line-heights (.88–.93) come
   UP past 1.05, and the caps tracking (+.02–.04em) goes to 0 or negative — caps
   tracking applied to lower-case serif reads as broken word-spacing.

   CALIBRATION: the hero is 700 and these are 600 on purpose. The hero sits on
   dark bark green and needs the extra weight to hold against the field; these sit
   on cream, where 700 reads heavy. Verified on screen before committing.
   ═══════════════════════════════════════════════════════════════════════════ */

/* 1. Standard / best-of / digest single-post H1.
      `!important` on all THREE of family, size and line-height is not defensive
      clutter — `extra.css:3706` sets exactly those three with `!important`, and
      its 1279 / 639 steps are `!important` too, so both restatements below need
      it as well or extra.css wins back at those widths. `letter-spacing` is the
      one property it does NOT protect, so that one needs no `!important`.
      (`style.css` also has non-important 44/44/32px steps; the child beats those
      on load order alone.) */
.article-hero__title{
  font-family:var(--font-serif)!important; font-weight:600;
  font-size:44px!important; letter-spacing:-.01em; line-height:1.06!important;
}
@media (max-width:1279px){ .article-hero__title{ font-size:38px!important; } }
@media (max-width:639px){ .article-hero__title{ font-size:29px!important; } }

/* 2. Image-first single-post H1. No `!important` anywhere — extra.css:244 uses
      none, and the child sheet loads last so an equal-specificity tie goes to the
      child. Its 1023 and 639 steps ARE restated though: they are non-important,
      which means an unconditional child `font-size` would silently kill them (a
      media query adds no specificity — runbook §2 trap). Breakpoint is 1023 here,
      not 1279: that is extra.css's own breakpoint for this selector, and inventing
      a different one would leave a gap. */
.article-header__title{
  font-family:var(--font-serif); font-weight:600; font-size:46px;
  letter-spacing:-.01em; line-height:1.08;
}
@media (max-width:1023px){ .article-header__title{ font-size:38px; } }
@media (max-width:639px){ .article-header__title{ font-size:29px; } }

/* 3. In-body subheads.
      ⚠ THE `:not()` SCOPING IS LOAD-BEARING — DO NOT SIMPLIFY IT TO `.article-body
      h2`. A bare rule is (0,1,1) and matches two CLASSED headings that also live
      inside `.article-body`:
        `.bestof-entry__name`  — extra.css protects its family, size and
          line-height with `!important`, but NOT `letter-spacing:.02em`. A bare
          rule would therefore strip the caps tracking off 25 live 48px Bebas
          headings and leave them cramped, while the `!important`s kept everything
          else Bebas — the worst of both. It is also deliberately out of scope:
          structurally it is a venue name, not a story headline.
        `.digest-item__title` — `single-digest.php:117`, inside the `.article-body`
          container opened at line 69. The CLASS has zero CSS in either parent
          stylesheet, but the element is NOT unstyled: `style.css:1347`
          `.article-body h2` (0,1,1) already gives it Bebas 28px, tracking .03em,
          `padding-top:22px` and a 1px hairline. (An earlier note here called it "a
          bare UA h2" — wrong, and worth correcting because the deferred follow-up
          reads this comment.) Excluding it keeps that inherited treatment rather
          than silently converting a digest item headline to a content subhead.
      Real content subheads on this site are UNCLASSED h2 — verified against live
      markup across all six posts (25 `h2.bestof-entry__name`, 5 unclassed).
      `:not(.class)` is preferred over `:not([class])` because it survives
      Gutenberg adding `wp-block-heading` to future content.
      The parent's top border and padding on these are deliberately left alone —
      they are a layout device, not type. */
.article-body h2:not(.bestof-entry__name):not(.digest-item__title){
  font-family:var(--font-serif); font-weight:600; font-size:23px;
  letter-spacing:0; line-height:1.25;
}
/* 4. The image-first variant restates the same subhead at 30px/.04em
      (`extra.css:410`, specificity (0,2,1)). The compound below is (0,4,1) so it
      wins on specificity rather than order. */
.image-first-body .article-body h2:not(.bestof-entry__name):not(.digest-item__title){
  font-family:var(--font-serif); font-weight:600; font-size:24px;
  letter-spacing:0; line-height:1.25;
}

/* ═══════════════════════════════════════════════════════════════════════════
   2026-08-06 — STORY-CARD TITLES: the tail of the Fraunces conversion
   ---------------------------------------------------------------------------
   Five surfaces that render a STORY HEADLINE but were left on DM Sans when
   `.article-card__title` moved to Fraunces. Each is the same editorial object as
   that card title — a post title a reader reads — so leaving them split gave
   londonly two headline voices for one role. `.mm-compact-title` is the most
   visible: it sits inside the same mega panel as the Fraunces `.mm-hero-title-ed`.

   FAMILY ONLY — deliberately no `font-weight`. Every one of these already
   DECLARES its weight (500 or 600) and Fraunces resolves both as real variable
   instances, so nothing goes light. The §1b weight pins exist only to rescue
   surfaces declaring 400; adding a pin here would be an undisclosed +100 to the
   parent's authored weight, not a rescue. Three independent reviews flagged the
   bundled 600 as unjustified — it is dropped.

   No `!important` and no breakpoint restatements: for all five, a CSSOM sweep of
   every loaded sheet found exactly one font-bearing rule, at normal priority,
   with no @media rule touching the selector. Verified per selector, not assumed.

   NOT converted, on purpose:
     `.mosaic-card__title` — ONE class, THREE editorial objects
       (taxonomy-neighbourhood.php: 'Local Pick' = a PLACE name, 'Story' = an
       article title, 'Event'). A bare family rule would drag place names and
       events into the story voice, and `.mosaic-card--event` declares font-size
       only, so it would be caught too. Needs per-variant scoping, not a rule.
     `.digest-item__title` — no digest content exists to judge it against.
   ═══════════════════════════════════════════════════════════════════════════ */
.rail-related__title{ font-family:var(--font-serif); }     /* extra.css:128  — DIV, 12.5px/500, sidebar "Related Stories" */
.dir-feat-article__title{ font-family:var(--font-serif); } /* extra.css:2696 — DIV, 13.5px/600, directory "Featured In" (line-clamp:2 — do not resize) */
.mm-compact-title{ font-family:var(--font-serif); }        /* style.css:541  — DIV, 13px/500, mega-panel compact cards */
.sidebar-item-title{ font-family:var(--font-serif); }      /* style.css:1248 — DIV, 12.5px/500, category "Most read" rail */
.so-article-title{ font-family:var(--font-serif); }        /* JS-built DIV (site.js:203), 14px/500, search overlay */
