/* DropBlockCMS — Article body and block types */

.article-body { font-family: var(--font-body); font-size: var(--text-md); line-height: var(--leading-relaxed); color: var(--color-text-primary); }
.article-body > * + * { margin-top: var(--space-6); }

.article-body p { margin-bottom: 0; }
.article-body a { color: var(--color-brand-primary); text-decoration: underline; text-underline-offset: 2px; }

/* scope-briefs/035. `.article-body h1` did not exist because no block could produce an h1 — the
   only one on the site came from the banner or a template's own page header. A homepage built
   without a banner therefore had NO top-level heading at all. */
.article-body h1 {
  font-family: var(--font-display); font-size: var(--text-3xl);
  font-weight: var(--weight-semibold); line-height: var(--leading-tight);
  margin-top: var(--space-12);
}

/* scope-briefs/035 — SIZE, decoupled from level. These deliberately do NOT reuse the .type-h*
   classes from design-tokens.css: those are in live use by five pieces of system chrome ("Latest
   articles", "You might like", "Related reading"), so redefining them would move those too. Same
   tokens, own classes, no shared fate.
   Specificity note: `.article-body .hd-hero` is (0,2,0) and beats `.article-body h2` at (0,1,1),
   which is what lets a size override its level. A heading left at its level's default carries no
   class at all, so the tag rules above still own it and nothing existing moves. */
.article-body .hd-hero   { font-size: var(--text-hero); font-weight: var(--weight-semibold); line-height: var(--leading-tight); }
.article-body .hd-large  { font-size: var(--text-3xl);  font-weight: var(--weight-semibold); line-height: var(--leading-tight); }
.article-body .hd-medium { font-size: var(--text-2xl);  font-weight: var(--weight-semibold); line-height: var(--leading-tight); }
.article-body .hd-small  { font-size: var(--text-xl);   font-weight: var(--weight-medium);   line-height: var(--leading-snug); }

.article-body h2 {
  font-family: var(--font-display); font-size: var(--text-2xl);
  font-weight: var(--weight-semibold); line-height: var(--leading-tight);
  margin-top: var(--space-12);
  padding-bottom: var(--space-2);
  border-bottom: 1px solid var(--color-border-default);
}
.article-body h3 {
  font-family: var(--font-display); font-size: var(--text-xl);
  font-weight: var(--weight-medium); line-height: var(--leading-snug);
  margin-top: var(--space-10);
}

/* --- Image --- */
.article-figure { margin: var(--space-10) 0; }
.article-figure img {
  border-radius: var(--radius-md);
  border: 1px solid var(--color-border-default);
  width: 100%;
}
.article-figure figcaption,
.figure-caption {
  font-family: var(--font-ui); font-size: var(--text-xs);
  color: var(--color-text-secondary); text-align: center;
  margin-top: var(--space-3);
}

/* --- Pull quote --- */
.pull-quote {
  border-left: 3px solid var(--color-brand-primary);
  background: var(--color-brand-light);
  padding: var(--space-6) var(--space-8);
  margin: var(--space-10) 0;
}
.pull-quote p {
  font-family: var(--font-body); font-size: var(--text-md);
  font-style: italic; color: var(--color-info-text);
  margin: 0;
}
.pull-quote cite {
  display: block; margin-top: var(--space-3);
  font-family: var(--font-ui); font-size: var(--text-xs);
  font-style: normal; color: var(--color-brand-primary);
}

/* --- Divider --- */
.article-divider { border: 0; border-top: 1px solid var(--color-border-default); margin: var(--space-12) 0; }

/* --- Columns (scope-briefs/006 §5, rebuilt from the old fixed 1fr/1fr shape — zero live content
   used the old left/right form, so no published page changes appearance from this) --- */
/* grid-template-columns is set per-instance inline (render_block, includes/functions.php) from each
   column's own admin-set width percentage. CSS Grid (not flexbox) is the right tool: a percentage
   TRACK size is resolved against the container's content box AFTER `gap` is subtracted, so widths
   that sum to exactly 100% (guaranteed by the sanitiser's normalisation) never overflow past the
   gap the way a flex-basis percentage plus a separate gap would. */
/* align-items: center — without it, grid's default (stretch) leaves each column's content sitting
   at the top of its row, so a short column (e.g. an image) visually floats above a taller one (e.g.
   a paragraph) instead of balancing against it (Martin, 2026-08-27). Harmless on the mobile
   single-column stack below — each column is then the only thing in its own row, so there's nothing
   to center against. */
.block-columns { display: grid; align-items: center; gap: var(--space-8); margin: var(--space-8) 0; }
.block-column > * + * { margin-top: var(--space-6); }
@media (max-width: 768px) { .block-columns { grid-template-columns: 1fr !important; } }

/* --- Container (scope-briefs/008) — a wrapper section: an optional backdrop behind, its own bounded
   content on top. "Standard" width deliberately gets NO breakout rule at all: the article body may
   already be narrower than 1100px depending on the site's own Page Width setting (scope-briefs/005),
   and forcing a 100vw trick even at "standard" could make it wider than intended. Wide/Full apply the
   standard CSS full-bleed technique, capped at the SAME px values as the site-wide frame preset
   (THEME_WIDTH_FRAME_PRESETS) for visual consistency, but stored per-block, not read from theme.json.

   Revised 2026-08-28 after Martin's first walk-test on a real page:
   - NO default margin/padding at all any more — 'flush' (0) is the starting point on both edges, per
     his own steer ("push right up against whatever is above and below as the starting point"). The
     old single `margin: var(--space-8) 0` fought against the spacing preset and dominated it visually
     (a constant 32px either side regardless of preset), which read as "spacing doesn't do anything".
   - spacing is now two independent fields (top/bottom, his own ask: "two options") instead of one.
   - a height option was added — a sparse-content backdrop (e.g. one heading over a background video)
     otherwise collapses to near-nothing, since the backdrop layer is `position: absolute` and
     contributes zero height of its own; the editor nudges Height to "medium" the moment a backdrop
     kind is picked (admin-editor.js), so a fresh backdrop container looks reasonable by default
     without needing a plain content-only container to carry the same tall minimum for no reason. */
.block-container { position: relative; overflow: hidden; }
/* scope-briefs/020 Coin 3: a video backdrop the visitor hasn't consented to is a placeholder over
   this fallback — the same dark gradient the youtube facade uses, so an un-activated hero still
   looks intentional rather than empty. The iframe replaces the placeholder's contents in-page
   the moment Marketing consent is granted (video.js), and the class comes off with it. */
.block-container__bg.block-container__bg--gated { background: linear-gradient(135deg, #202024, #3a3a40); }   /* two classes: must beat .block-container__bg's own default background below */
.block-container--spt-flush  { padding-top: 0; }
.block-container--spt-tight  { padding-top: var(--space-6); }
.block-container--spt-normal { padding-top: var(--space-12); }
.block-container--spt-roomy  { padding-top: var(--space-20); }
.block-container--spb-flush  { padding-bottom: 0; }
.block-container--spb-tight  { padding-bottom: var(--space-6); }
.block-container--spb-normal { padding-bottom: var(--space-12); }
.block-container--spb-roomy  { padding-bottom: var(--space-20); }
.block-container--h-small  { min-height: 40vh; }
.block-container--h-medium { min-height: 60vh; }
.block-container--h-large  { min-height: 80vh; }
.block-container--h-full   { min-height: 100vh; }
/* Flush-against-the-hero (2026-08-28, Martin's ask): "flush" (spacing_top/bottom) only ever zeroed the
   Container's OWN padding — the page template's own wrapper (`.container-text.section`, page.php) still
   carries its ambient `padding: var(--space-16) var(--space-6)` (64px top/bottom) around ALL page
   content, so a "flush" Container sitting first in the flow still showed that 64px gap under the site's
   own hero banner. Cancel that ambient padding specifically when a flush Container touches the true top
   (`:first-child`) or bottom (`:last-child`) edge of the page body — "flush" now means genuinely flush,
   not just flush against its own padding while the page's is still there underneath it. Scoped to the
   page/landing template's own wrapper shape; NOT the article template's `.article-layout` (out of scope
   for now — pages only, Martin's call, same session — the article grid's own asymmetry is a separate,
   already-flagged limitation regardless of this specific gap). */
.container-text.section > .article-body > .block-container--spt-flush:first-child {
  margin-top: calc(-1 * var(--space-16));
}
.container-text.section > .article-body > .block-container--spb-flush:last-child {
  margin-bottom: calc(-1 * var(--space-16));
}
/* Flush-against-a-NEIGHBOUR-BLOCK, not just the page edge (Martin's walk-test, 2026-08-28 —
   a Container with a video backdrop sat below a Banner block with a visible gap, even though
   both the Container's own spacing_top AND the fix above are "flush"). Different rule from the
   pair above: `.article-body > * + *` (line 4) gives EVERY block a `margin-top` from its
   PRECEDING sibling, for ordinary reading rhythm between paragraphs/headings — that's exactly
   right for text, but fights a Container/Banner that explicitly asked to sit flush against
   whatever's next to it. The :first-child/:last-child pair above only ever cancelled the PAGE
   template's own ambient padding; this cancels the DIFFERENT, smaller inter-sibling margin,
   for a flush block in ANY position, not just the very top/bottom of the page. Both rules can
   apply to the same element at once (e.g. a flush Container that's also :first-child) without
   conflict — they touch the same property but at different specificities, so the more specific
   :first-child rule above still correctly wins there; this one is what fires everywhere else. */
.article-body > .block-container--spt-flush { margin-top: 0; }
.article-body > .block-container--spb-flush + * { margin-top: 0; }
/* margin-based breakout, NOT left/transform: every block carries .fade-in (animations.css), which
   sets its OWN `transform: translateY(16px)` for the scroll-reveal slide-up — two rules setting
   `transform` on the same element collide (equal specificity, source order decides the winner), and
   `.fade-in` was winning outright, silently zeroing out the breakout's translateX entirely (confirmed
   via live DOM inspection, 2026-08-28: computed transform was translateY(16px) only, left:50% alone
   shifting the box right with nothing pulling it back — exactly Martin's "pushed right, nothing to its
   left" symptom). margin-left never touches `transform`, so it coexists with .fade-in for free — same
   reason no other .fade-in element in this codebase puts a functional transform on the SAME node
   (video-facade__play's translate lives on a separate nested element, never the .fade-in one itself). */
/* min() gate (2026-08-28, same-day second bug found on live re-test at a wide viewport): a bare
   `calc(50% - 50vw)` margin is only correct while the box is genuinely 100vw wide. Once the viewport
   exceeds the cap and `max-width` kicks in to actually shrink the rendered box, that SAME margin value
   (still computed for the full 100vw case) no longer centers the now-narrower box — it just anchors
   its left edge to 0, leaving it flush-left with a large gap on the right instead of centered
   (confirmed live: at 1920px viewport, Full width sat flush-left with a ~530px right-hand gap instead
   of centering the 1400px-capped box). Gating each half of the margin at min(50vw, capHalfPx) makes
   the SAME formula correct in both regimes: below the cap it reduces to the original flush-breakout
   value; above the cap it becomes a constant offset that centers the now-fixed-width box instead. */
.block-container--w-wide {
  width: 100vw; max-width: 1250px;
  margin-left: calc(50% - min(50vw, 625px)); margin-right: calc(50% - min(50vw, 625px));
  --container-rendered-width: min(100vw, 1250px);   /* scope-briefs/037 — see the zone-alignment note below */
  --zone-min-inset: var(--space-6);                 /* walk round 2 — a breakout never sits on the screen edge */
}
.block-container--w-full {
  width: 100vw; max-width: 1400px;
  margin-left: calc(50% - min(50vw, 700px)); margin-right: calc(50% - min(50vw, 700px));
  --container-rendered-width: min(100vw, 1400px);   /* scope-briefs/037 */
  --zone-min-inset: var(--space-6);                 /* walk round 2 */
}
/* 'edge' (2026-08-28, Martin's ask after seeing real full-bleed hero-video reference sites) — a
   genuinely uncapped tier: no `max-width` at all, so no min() gate is needed either (there's no cap
   for the formula to need to guard against — it's the plain flush-breakout case, always). Fills the
   screen edge to edge on any monitor width, which 'full' (deliberately capped at the site frame's
   1400px, D3) was never meant to do. */
.block-container--w-edge {
  width: 100vw; margin-left: calc(50% - 50vw); margin-right: calc(50% - 50vw);
  --container-rendered-width: 100vw;   /* scope-briefs/037 */
  --zone-min-inset: var(--space-6);    /* walk round 2 */
}
.block-container__bg { position: absolute; inset: 0; z-index: 0; overflow: hidden; background: var(--color-surface-secondary); }
.block-container__bg--image { background-size: cover; background-position: center; }
/* Background video (scope-briefs/008 D4/D-video): true autoplay/muted/looped, no controls — loads
   immediately on page view (a deliberate exception to the click-to-load youtube block's privacy
   stance, Martin's call 2026-08-28). Oversized + centred + clipped by the parent's overflow:hidden —
   the standard "fill and crop to 16:9 regardless of the box's own aspect ratio" technique. */
.block-container__bg--video video { object-fit: cover; }   /* scope-briefs/020 Coin 4: a plain video file fills and crops like the iframe */
.block-container__bg--video iframe, .block-container__bg--video video {
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  width: 100%; height: 100%; min-width: 177.78vh; min-height: 56.25vw;
  border: 0; pointer-events: none;
}
.block-container__content { position: relative; z-index: 1; }
.block-container__content > * + * { margin-top: var(--space-6); }
.block-container__content:empty { display: none; }
/* A heading inside a Container inherits `.article-body h2`/`h3`'s styling (2026-08-28, Martin's ask):
   those rules add a 48px top margin (h2) plus a bottom border/padding, meant for a heading that starts
   a new section partway down a normal article — not for a hero heading floating over a backdrop. Since
   `.article-body h2` is a plain descendant selector, it matched here too regardless of nesting depth,
   and because .block-container__content establishes its own block formatting context (needed for the
   content-card's `inline-block` sizing), that top margin doesn't collapse away — it sits INSIDE the
   card, trapped between the card's own top padding and the heading text, with nothing matching below
   it. Confirmed live: 72px above the text vs 24px below, a paragraph in the same slot showing neither
   because it carries none of this heading-specific chrome. Zero all three out here specifically. */
.block-container__content h1, .block-container__content h2, .block-container__content h3 {
  margin-top: 0; padding-bottom: 0; border-bottom: none;
}
/* Content padding (2026-08-28, Martin's ask, extended to all four sides same day after his next
   screenshot showed the exact same problem on the TOP edge — a card sitting right against the top of
   the video, wanted a gap there too): inset for the content from the container's own edges — 'normal'
   by default, unlike spacing_top/bottom's 'flush' default, since a breakout container's content
   usually DOES want breathing room from the true screen edges. Independent of spacing_top/bottom,
   which are vertical and control the gap to NEIGHBOURING blocks outside the container, not this.
   MARGIN, not padding — deliberately: the content-card rule below also carries its own `padding`
   (the card's internal text inset), and padding here would collide with it (same property, equal
   specificity, source order silently picks a winner — confirmed live, 2026-08-28: the card's
   shorthand padding always won, making this control a no-op whenever a card was active). margin is a
   different property entirely, so the two compose cleanly: this offsets the content (or the card, when
   one wraps it) from the container's edges; the card's own padding is the separate, unrelated inset
   between the card's edge and the text inside it. Symmetric margin narrows a block-level (no-card)
   content box exactly like padding would; for an inline-block card it offsets where the shrink-wrapped
   card sits, which is the effect actually wanted there. */
.block-container__content--pad-flush  { margin: 0; }
.block-container__content--pad-tight  { margin: var(--space-6); }
.block-container__content--pad-normal { margin: var(--space-12); }
.block-container__content--pad-roomy  { margin: var(--space-20); }
/* Content card (2026-08-28, Martin's ask, after real-photo backdrop testing showed default dark
   heading text disappearing into it): an optional translucent, rounded panel behind the WHOLE content
   block (not per inner element — far simpler, same practical readability result for the common
   heading+CTA case). inline-block so it hugs the content's own width rather than stretching across
   the full breakout width, which would look like a heavy bar across the hero rather than a card. */
.block-container__content--card-dark, .block-container__content--card-light {
  display: inline-block; border-radius: var(--radius-lg); padding: var(--space-6) var(--space-8);
}
.block-container__content--card-dark  { background: rgba(20, 20, 24, 0.55); }
.block-container__content--card-light { background: rgba(255, 255, 255, 0.82); }
/* Content position (scope-briefs/012, 2026-08-28) — pin the WHOLE content group to one of 9
   zones instead of it stretching to fill the container (the concrete need: a logo image
   forced full-width instead of sitting at its own natural size in a corner). Deliberately NO
   rule at all for 'fill' — the default stays plain block flow, byte-identical to every
   Container published before this feature existed; `display: flex` only ever applies to the
   OUTER `.block-container` for one of the 8 real zones, so `.block-container__bg`
   (`position: absolute; inset: 0`) is completely unaffected either way — it was never part of
   the flow this changes. `.block-container__content` becoming a flex ITEM here (not the flex
   container) means the browser "blockifies" its own `display` regardless of what it's set to
   (`inline-block` on the content-card variants above) — harmless: a flex item already shrinks
   to fit its content on the cross axis whenever `align-items` isn't `stretch` (true for all 9
   zones below), which is the exact same "hug the content, don't stretch" effect the card's own
   `inline-block` was going for on a non-flex parent — the two mechanisms land on the same
   result rather than fighting. */
.block-container--pos-top-left      { display: flex; align-items: flex-start; justify-content: flex-start; }
.block-container--pos-top-center    { display: flex; align-items: flex-start; justify-content: center; }
.block-container--pos-top-right     { display: flex; align-items: flex-start; justify-content: flex-end; }
.block-container--pos-middle-left   { display: flex; align-items: center; justify-content: flex-start; }
.block-container--pos-center        { display: flex; align-items: center; justify-content: center; }
.block-container--pos-middle-right  { display: flex; align-items: center; justify-content: flex-end; }
.block-container--pos-bottom-left   { display: flex; align-items: flex-end; justify-content: flex-start; }
.block-container--pos-bottom-center { display: flex; align-items: flex-end; justify-content: center; }
.block-container--pos-bottom-right  { display: flex; align-items: flex-end; justify-content: flex-end; }
/* Zone alignment follows the SITE FRAME, not the viewport (scope-briefs/037, 2026-09-07 — Martin's
   own report from his 21" monitor: the homepage hero text "sits over to the hard left where we said
   we'd be able to change that so it follows the main page layout").

   The nine rules above are flex on the OUTER .block-container, which at the 'edge' tier is the whole
   screen — so `justify-content: flex-start` meant the TRUE viewport edge, ~421px left of the nav
   logo on his monitor. The .block-container__content cap below caps how WIDE the content gets, never
   WHERE IT STARTS, which is why it never helped. Insetting the flex container by the frame's own
   gutter moves the whole zone grid — left, right AND centre stay mutually consistent — without
   touching the backdrop: .block-container__bg is `position: absolute; inset: 0`, and an absolutely
   positioned child resolves against its ancestor's PADDING box, so the image still spans edge to
   edge. That is the whole reason this is padding on the outer rather than a new wrapper element:
   no render-side change, no stored document touched, rollback is reverting this file.

   `--max-width-wide` deliberately, not a literal 1100: theme.php:143 maps the 'frame' bucket to that
   variable, so this follows the site's own Branding preset (1100/1250/1400) instead of guessing.
   D1 chose the frame line (the logo) over the body text column (~190px further in) — the codebase
   already grouped them, theme.php calling that preset "Site frame (nav, hero, footer)".

   ⚠ --container-rendered-width, NOT 100%. A percentage padding resolves against the CONTAINING
   BLOCK's inline size, never the element's own width. At the 'wide'/'full' tiers the containing
   block is the article/landing body (~712px), so `(100% - 1100px)/2` would compute against 712,
   clamp to zero, and leave both tiers silently unfixed while 'edge' looked perfect — a bug that
   passes any walk-test that only checks the homepage. Each breakout tier therefore states its own
   rendered width above (the same reason --w-wide already hard-codes its 625px half-cap rather than
   deriving it). 'standard' sets nothing: the 0px fallback makes the whole expression negative, max()
   clamps it, and that tier is a guaranteed no-op rather than a coincidence.

   D2 (Martin's call, against Forge's recommendation and on the better argument): content_padding is
   ADDITIVE on top of this, not absorbed by it — padding should mean padding, and 'flush' is the
   existing way to ask for none. So a 'flush' Container lands exactly on the logo line; his hero's
   'normal' lands 48px right of it, by choice.

   THE FLOOR IS --zone-min-inset, AND IT IS SET PER TIER — walk round 2 (v1.35.1), from Martin's own
   phone and laptop. v1.35.0 floored at a flat 0px, reasoning that below the frame cap there is no
   frame/viewport divergence left to correct. True, and beside the point: below the cap the rule
   simply stepped aside, so a 'flush' breakout sat at 0 — hard against the screen edge, and actually
   -7.5px past it once the scrollbar's share of 100vw is counted — while the logo sat at --space-6.
   He found it on a laptop narrower than the frame, then on a phone, where five of the walk fixture's
   ten tests showed it at once. Not a regression (flush did this before 037 too), but it meant no
   single padding setting could be right on both his screens, which is the thing 037 exists to fix.

   The floor is NOT global, and that distinction is the whole of this round: flooring every zone at
   --space-6 also moves the 'standard' tier, which has no breakout and was already sitting correctly
   on the page's own gutter — measured, it would have gone 24px -> 48px on every screen, quietly
   destroying the guaranteed no-op designed in above. So each BREAKOUT tier declares --zone-min-inset
   for itself and 'standard' leaves it unset, where the 0px fallback here preserves its zero exactly.
   Above the frame cap the calc() always wins, so the wide-monitor result 037 shipped is untouched —
   verified identical, not assumed.

   Only D4 zones get any of this: `fill` has no --pos- class, so it never matches, and every
   Container published at the default position renders byte-identically to before. */
.block-container--pos-top-left,
.block-container--pos-top-center,
.block-container--pos-top-right,
.block-container--pos-middle-left,
.block-container--pos-center,
.block-container--pos-middle-right,
.block-container--pos-bottom-left,
.block-container--pos-bottom-center,
.block-container--pos-bottom-right {
  padding-inline: max(var(--zone-min-inset, 0px), calc((var(--container-rendered-width, 0px) - var(--max-width-wide)) / 2 + var(--space-6)));
}
/* A positioned zone shrinks the content group to its own size (that's what makes a logo sit
   natural-size instead of stretching) — but a long paragraph in a zone would otherwise become
   one very wide, awkward line with nothing to wrap it. Cap at the same reading width already
   used everywhere else in this codebase (scope-briefs/012 D4) — a small image/logo never gets
   close to this width in practice, so it's a no-op for the case that actually motivated this
   feature, and only matters for the text case it's meant to guard. */
.block-container--pos-top-left .block-container__content,
.block-container--pos-top-center .block-container__content,
.block-container--pos-top-right .block-container__content,
.block-container--pos-middle-left .block-container__content,
.block-container--pos-center .block-container__content,
.block-container--pos-middle-right .block-container__content,
.block-container--pos-bottom-left .block-container__content,
.block-container--pos-bottom-center .block-container__content,
.block-container--pos-bottom-right .block-container__content {
  max-width: var(--max-width-content);
}

/* --- Menu block (scope-briefs/036) — a site menu placed as page CONTENT ---------------------
   Deliberately NOT reusing .top-nav/.nav-links. Those carry the site header's own identity —
   sticky, full-bleed bar, its own background and border — and a menu sitting inside an article
   is not that. Sharing them would mean every future header tweak silently restyled page content.
   The JS is shared (data-nav-root, assets/js/nav.js); the looks are not.

   THE PHONE PANEL IS IN FLOW, not absolutely positioned, and that single decision removes two
   traps at once (§7 of the brief): the header's panel is `position: absolute; top: var(--nav-height)`
   and anchors to the nearest positioned ancestor, which inside page content is somewhere
   unintended; and `.block-container` is `overflow: hidden`, which would CLIP an overlay panel on a
   nested menu. Expanding in flow pushes the content below it down instead — no anchor to get
   wrong, nothing to clip, and arguably the better behaviour for a menu that is part of the page
   rather than floating over it. */
.block-menu { position: relative; }
.block-menu__toggle {
  display: none;                       /* desktop: the panel is simply open */
  align-items: center; gap: var(--space-2);
  font-family: var(--font-ui); font-size: var(--text-sm); font-weight: var(--weight-medium);
  color: var(--color-nav-link); background: none; border: 0;
  padding: var(--space-2) 0; cursor: pointer;
}
.block-menu__toggle-icon { font-size: var(--text-lg); line-height: 1; }
.block-menu__list { list-style: none; display: flex; gap: var(--space-6); flex-wrap: wrap; }
.block-menu__list a {
  font-family: var(--font-ui); font-size: var(--text-sm); color: var(--color-nav-link);
  /* .article-body a (line 7) underlines every link in prose, which is right for prose and wrong for
     navigation — found on the walk, the block's links read as body copy while the site header's did
     not. The header escapes that rule by living OUTSIDE .article-body; a menu placed as content sits
     inside it, so the override has to be explicit. Same equal specificity (0,0,2,0), later in the
     file, which is also how the colour above already wins. Hover changes colour, never adds a rule —
     matching .nav-links exactly. */
  text-decoration: none;
}
.block-menu__list a:hover { color: var(--color-nav-link-active); text-decoration: none; }
.block-menu__coltitle {
  font-family: var(--font-ui); font-size: var(--text-xs); font-weight: var(--weight-medium);
  letter-spacing: 0.06em; text-transform: uppercase; color: var(--color-text-muted);
  margin-bottom: var(--space-3);
}

/* Row: one line of links, wrapping. Columns: headings above their own vertical lists. auto-fit
   rather than a fixed count — a placeable menu has no idea how wide the slot it lands in is. */
.block-menu--row .block-menu__panel { display: flex; flex-wrap: wrap; gap: var(--space-6); }
.block-menu--columns .block-menu__panel {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: var(--space-8);
}
.block-menu--columns .block-menu__list { flex-direction: column; gap: var(--space-3); }

/* Alignment (D6b, Martin's ask) — moves the block's contents within its own width. It does NOT
   make body text flow beside the menu; that is a Columns-block layout, banked in the brief's §9. */
.block-menu--a-left   .block-menu__panel,
.block-menu--a-left   .block-menu__list { justify-content: flex-start; }
.block-menu--a-center .block-menu__panel,
.block-menu--a-center .block-menu__list { justify-content: center; }
.block-menu--a-right  .block-menu__panel,
.block-menu--a-right  .block-menu__list { justify-content: flex-end; }
.block-menu--a-center { text-align: center; }
.block-menu--a-right  { text-align: right; }
.block-menu--a-center .block-menu__toggle { margin: 0 auto; }
.block-menu--a-right  .block-menu__toggle { margin-left: auto; }

/* Width: two of Container's four tiers (D5). 'standard' is the content column and needs no rule
   at all — same reasoning as Container's own standard tier, which deliberately has none: the body
   may already be narrower than any breakout would assume. 'edge' copies Container's uncapped
   flush-breakout verbatim (margin-based, never transform — .fade-in owns transform on this
   element, see the Container note above). */
/* scope-briefs/037 D5 — the same frame alignment as a Container's zone grid, for the same reason.
   A standalone edge-width menu breaks out to the viewport and carried a flat --space-6 gutter, so on
   a wide monitor its links sat hard against the screen edge while every other nav on the page sat on
   the frame line. This is a SEPARATE rule from the Container fix, not a freebie from it: a menu
   NESTED in a Container rides in that Container's content group and is already handled, but a
   standalone one has its own breakout and would have stayed broken. Longhands, not padding-inline,
   so the nested reset immediately below overrides it property-for-property with no logical/physical
   cascade subtlety to reason about. The --space-6 floor (not 0px, unlike the Container rule) simply
   preserves the gutter this rule already had below the frame cap. */
.block-menu--w-edge {
  width: 100vw; margin-left: calc(50% - 50vw); margin-right: calc(50% - 50vw);
  padding-left:  max(var(--space-6), calc((100vw - var(--max-width-wide)) / 2 + var(--space-6)));
  padding-right: max(var(--space-6), calc((100vw - var(--max-width-wide)) / 2 + var(--space-6)));
}
/* Nested inside a Container, the Container owns the width — a breakout inside one is nonsense and
   would burst its box. The editor hides the width control when nested; this is the render-side
   backstop for a hand-edited document, same "never trust a single layer" discipline as the
   sanitiser re-checking on every publish path. */
.block-container__content .block-menu--w-edge {
  width: auto; margin-left: 0; margin-right: 0; padding-left: 0; padding-right: 0;
}

@media (max-width: 768px) {
  .block-menu__toggle { display: flex; }
  .block-menu__panel {
    max-height: 0; overflow: hidden;
    transition: max-height 0.3s ease;
  }
  .block-menu__panel.is-open { max-height: 60vh; }
  .block-menu--row .block-menu__panel { flex-direction: column; gap: 0; }
  .block-menu--columns .block-menu__panel { grid-template-columns: 1fr; gap: var(--space-6); }
  .block-menu--row .block-menu__list { flex-direction: column; gap: 0; }
  .block-menu__list a { display: block; padding: var(--space-3) 0; }
}
@media (prefers-reduced-motion: reduce) {
  .block-menu__panel { transition: none; }
}

/* --- YouTube (click-to-load facade → youtube-nocookie player; no Google contact until click) --- */
.video-embed { position: relative; aspect-ratio: 16 / 9; margin: var(--space-10) 0; border-radius: var(--radius-md); overflow: hidden; background: #000; }
.video-embed iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }
/* Facade: a static thumbnail + play button; the gradient shows if the thumbnail fails to load. */
.video-facade { position: absolute; inset: 0; width: 100%; height: 100%; padding: 0; border: 0; cursor: pointer; display: block; background: linear-gradient(135deg, #202024, #3a3a40); }
.video-facade__thumb { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.video-facade__play { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 68px; height: 48px; border-radius: 14px; background: rgba(0, 0, 0, 0.65); transition: background 0.15s ease; }
.video-facade__play::before { content: ""; position: absolute; top: 50%; left: 50%; transform: translate(-35%, -50%); border-style: solid; border-width: 11px 0 11px 19px; border-color: transparent transparent transparent #fff; }
.video-facade:hover .video-facade__play, .video-facade:focus-visible .video-facade__play { background: #c00; }
.video-facade__title { position: absolute; left: 0; right: 0; bottom: 0; padding: 0.6rem 0.8rem; color: #fff; font-size: var(--text-sm); text-align: left; background: linear-gradient(transparent, rgba(0, 0, 0, 0.75)); }
/* scope-briefs/020 Coin 3: the plain "what pressing play means" line, only while Marketing consent is absent. */
.video-facade__note { position: absolute; top: 0.8rem; left: 0.8rem; right: 0.8rem; padding: 0.55rem 0.9rem; color: #fff; font-size: var(--text-md); font-weight: var(--weight-medium); line-height: var(--leading-snug); font-family: var(--font-ui); text-align: left; background: rgba(0, 0, 0, 0.75); border-left: 4px solid #fff; border-radius: var(--radius-sm); }
/* Sized up after Martin's own walk-test (2026-09-02): the first version was small enough to miss
   before pressing play, and a notice nobody reads is not an informed click. */
@media (max-width: 640px) { .video-facade__note { font-size: var(--text-sm); padding: 0.45rem 0.7rem; } }
/* Read-time note shown only when the article contains a video. */
.article-video-note { font-size: var(--text-sm); color: var(--color-text-secondary); font-style: italic; margin: 0 0 var(--space-6); }

/* --- Audio --- */
.audio-block {
  background: var(--color-surface-secondary);
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius-md);
  padding: var(--space-5) var(--space-6); margin: var(--space-8) 0;
}
.audio-block .audio-title { font-family: var(--font-ui); font-weight: var(--weight-medium); margin-bottom: var(--space-3); }
.audio-block .audio-duration { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--color-text-secondary); }
.audio-block audio { width: 100%; }

/* --- List (scope-briefs/006) --- */
/* reset.css strips list-style on every ul/ol site-wide, so markers must be restored explicitly
   here rather than assumed — otherwise a published list silently renders with no bullets/numbers. */
.article-list { margin-left: var(--space-6); }
ul.article-list { list-style: disc; }
ol.article-list { list-style: decimal; }
.article-list li { margin-bottom: var(--space-2); }
.article-list li:last-child { margin-bottom: 0; }

/* --- Table (scope-briefs/006) --- */
/* Wrapped in a scrollable container so a wide table never breaks the page on a narrow screen,
   rather than overflowing or forcing the whole article wider (Martin's research notes). */
.article-table-wrap { overflow-x: auto; margin: var(--space-10) 0; }
.article-table { border-collapse: collapse; width: 100%; font-family: var(--font-body); font-size: var(--text-sm); }
.article-table th, .article-table td {
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--color-border-default);
  text-align: left;
}
.article-table thead th {
  font-family: var(--font-ui); font-weight: var(--weight-semibold);
  background: var(--color-surface-secondary); color: var(--color-text-primary);
}
/* Zebra striping — a permanent style, not an admin toggle (Martin's research notes: free
   visually, no reason to make it optional). Only stripes tbody rows, never the header. */
.article-table tbody tr:nth-child(even) { background: var(--color-surface-secondary); }

/* --- Image Gallery (scope-briefs/007) --- */
/* An empty slot is deliberately just a blank cell (no placeholder icon) — Martin's D3/D4: running
   short of images, or leaving a slot blank on purpose, look identical. --gallery-cols is set inline
   per block (render_block, includes/functions.php) from the admin's own column count. */
.gallery-block { display: grid; gap: var(--space-2); grid-template-columns: repeat(var(--gallery-cols, 3), 1fr); margin: var(--space-10) 0; }
.gallery-item, .gallery-slot--empty { aspect-ratio: 1; border-radius: var(--radius-sm); }
.gallery-slot--empty { background: var(--color-surface-secondary); }
.gallery-item {
  position: relative; padding: 0; border: 0; overflow: hidden; cursor: zoom-in;
  background: var(--color-surface-secondary);
}
.gallery-item img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform 0.2s ease; }
.gallery-item:hover img, .gallery-item:focus-visible img { transform: scale(1.04); }

/* Lightbox expand (assets/js/gallery-lightbox.js). Scales to a fixed viewport percentage rather than
   any admin-set width (D6 — Martin's own steer, 2026-08-27: keep it decoupled from the unrelated
   Branding page-width presets, judge this plain version live before reaching for anything fancier).
   The [hidden] rule MUST precede .gallery-lightbox's own `display: flex` and match its specificity —
   same reset.css trap the Media Library picker hit (a class selector and [hidden] tie on specificity,
   so source order decides, and author CSS loaded after the UA sheet wins by default). */
.gallery-lightbox[hidden] { display: none; }
.gallery-lightbox {
  position: fixed; inset: 0; z-index: 300; display: flex; align-items: center; justify-content: center;
  cursor: zoom-out;
  /* Starts transparent; JS adds .is-open one frame after clearing [hidden] so the browser has a
     real "from" state to animate from — see gallery-lightbox.js's open(). Fades back out the same
     way: .is-open is removed first, [hidden] only lands once the transition has had time to finish,
     so closing never gets cut off mid-fade. */
  opacity: 0; transition: opacity 0.2s ease;
}
.gallery-lightbox.is-open { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  .gallery-lightbox { transition: none; }
}
.gallery-lightbox__backdrop { position: absolute; inset: 0; background: rgba(0, 0, 0, 0.85); }
.gallery-lightbox__img {
  position: relative; max-width: 92vw; max-height: 92vh; width: auto; height: auto;
  object-fit: contain; box-shadow: var(--shadow-lg);
}

/* --- FAQ --- */
.faq-block { margin: var(--space-10) 0; }
.faq-item { border-bottom: 1px solid var(--color-border-default); padding: var(--space-5) 0; }
.faq-item summary {
  font-family: var(--font-display); font-size: var(--text-md);
  font-weight: var(--weight-medium); cursor: pointer; list-style: none;
  display: flex; justify-content: space-between; gap: var(--space-4);
}
.faq-item summary::-webkit-details-marker { display: none; }
.faq-item summary::after { content: "+"; color: var(--color-brand-primary); font-family: var(--font-ui); }
.faq-item[open] summary::after { content: "–"; }
.faq-item .faq-answer { font-family: var(--font-body); margin-top: var(--space-3); color: var(--color-text-primary); }

/* --- Cookie register (scope-briefs/021 Coin 3) — the privacy page's cookie list --- */
.cookie-register { margin: var(--space-8) 0; }
.cookie-register__cat { border-top: 1px solid var(--color-border-default); padding: var(--space-5) 0; }
.cookie-register__cat:last-child { border-bottom: 1px solid var(--color-border-default); }
.cookie-register__name { font-family: var(--font-display); font-size: var(--text-md); font-weight: var(--weight-medium); margin: 0 0 var(--space-2); display: flex; align-items: baseline; gap: var(--space-3); flex-wrap: wrap; }
.cookie-register__always { font-family: var(--font-ui); font-size: var(--text-xs); font-weight: var(--weight-medium); color: var(--color-text-tertiary); text-transform: uppercase; letter-spacing: 0.04em; }
.cookie-register__purpose { font-family: var(--font-body); color: var(--color-text-secondary); margin: 0 0 var(--space-3); }
.cookie-register__cookies { list-style: none; margin: 0; padding: 0; font-size: var(--text-sm); line-height: var(--leading-normal); color: var(--color-text-secondary); }
.cookie-register__cookies li { margin: 0 0 var(--space-2); padding-left: var(--space-5); text-indent: calc(-1 * var(--space-5)); }
.cookie-register__cookies code { font-family: var(--font-mono); color: var(--color-text-primary); }
.cookie-register__life { color: var(--color-text-tertiary); }   /* no nowrap: a lifetime can be a sentence, and a phone must wrap it */
.cookie-register__none { font-family: var(--font-body); font-size: var(--text-sm); color: var(--color-text-tertiary); margin: 0; }

/* --- HowTo steps --- */
.howto-steps { counter-reset: howto; margin: var(--space-8) 0; display: flex; flex-direction: column; gap: var(--space-6); }
.howto-step {
  counter-increment: howto;
  position: relative;
  background: var(--color-surface-white);
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius-md);
  padding: var(--space-6) var(--space-6) var(--space-6) var(--space-16);
}
.howto-step::before {
  content: counter(howto);
  position: absolute; left: var(--space-5); top: var(--space-6);
  width: var(--space-8); height: var(--space-8);
  display: flex; align-items: center; justify-content: center;
  background: var(--color-brand-primary); color: #fff;
  font-family: var(--font-display); font-weight: var(--weight-semibold);
  border-radius: var(--radius-pill);
}
.howto-step .howto-name { font-family: var(--font-display); font-size: var(--text-lg); font-weight: var(--weight-medium); margin-bottom: var(--space-2); }
.howto-step .howto-text { font-family: var(--font-body); margin: 0; }
.howto-step img { margin-top: var(--space-4); border-radius: var(--radius-md); border: 1px solid var(--color-border-default); }

/* --- "On this page" bar (scope-briefs/048 D7, D8) --- */
/* One slim line at the start of the article; sticks under the menu for the rest of the body
   (.article-flow is its sticky range). Plain CSS sticking, so it works with no script; on-this-page.js
   only adds the current section and closes the list after a jump. The band reaches the screen edges
   with a spread shadow clipped to the bar's own height — never a transform (gotcha 12) and never a
   width, so it can't cause sideways scroll. */
.article-flow { --otp-height: 2.75rem; }
.otp {
  position: sticky; top: var(--nav-height); z-index: 90;   /* under the menu (100), over the body */
  margin-bottom: var(--space-8);
  background: var(--color-surface-page);
  box-shadow: 0 0 0 100vmax var(--color-surface-page), 0 1px 0 100vmax var(--color-border-default);
  clip-path: inset(0 -100vmax -1px -100vmax);
  font-family: var(--font-ui); font-size: var(--text-sm);
}
.otp__summary {
  display: flex; align-items: center; gap: var(--space-2);
  min-height: var(--otp-height);   /* also the touch target */
  list-style: none; cursor: pointer;
  color: var(--color-text-primary); font-weight: var(--weight-medium);
}
.otp__summary::-webkit-details-marker { display: none; }
/* The clip above would cut an outside focus ring, so the ring sits inside. */
.otp__summary:focus-visible, .otp__list a:focus-visible { outline-offset: -2px; }
.otp__current {
  min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
  color: var(--color-text-secondary); font-weight: var(--weight-regular);
}
.otp__current:not(:empty)::before { content: ": "; }
.otp__chevron { margin-left: auto; flex-shrink: 0; }
.otp__chevron::before { content: "▾"; display: inline-block; transition: transform 0.2s ease; }
.otp__details[open] .otp__chevron::before { transform: rotate(180deg); }
.otp__list {
  max-height: min(60vh, 24rem); overflow-y: auto; overscroll-behavior: contain;
  padding: 0 0 var(--space-3); border-left: 1px solid var(--color-border-default);
}
.otp__list a { display: block; padding: var(--space-2) var(--space-4); color: var(--color-text-secondary); }
.otp__list a:hover { color: var(--color-brand-primary); text-decoration: none; }
.otp__list a[aria-current] { color: var(--color-brand-primary); font-weight: var(--weight-medium); }
.otp__item--h3 a { padding-left: var(--space-8); font-size: var(--text-xs); }
/* A jumped-to heading lands below the menu and the bar, not under them. */
.article-flow .article-body :is(h1, h2, h3)[id] { scroll-margin-top: calc(var(--nav-height) + var(--otp-height) + var(--space-4)); }

/* --- Article header meta --- */
.article-meta { font-family: var(--font-ui); font-size: var(--text-sm); color: var(--color-text-secondary); display: flex; flex-wrap: wrap; gap: var(--space-4); margin-top: var(--space-4); }
.article-author { display: flex; align-items: center; gap: var(--space-3); margin: var(--space-8) 0; padding: var(--space-6); background: var(--color-surface-secondary); border-radius: var(--radius-md); }
.article-author img { width: 48px; height: 48px; border-radius: var(--radius-pill); }
.article-author .author-name { font-family: var(--font-ui); font-weight: var(--weight-medium); }
.article-author .author-bio { font-family: var(--font-ui); font-size: var(--text-sm); color: var(--color-text-secondary); }

/* --- Icon (scope-briefs/027) --- */
/* .icon sizing is repeated in admin-editor.css for the editor's own preview — change both together.
   Colour is never set on .icon itself: the sprite is stroked in currentColor and takes the colour of
   the text around it, or of the block's own colour class below (which targets the svg only, so a
   label keeps the surrounding text colour — a Container's text_color included). */
.icon { display: inline-block; width: 1em; height: 1em; vertical-align: -0.125em; flex: none; }
.icon--s { width: 24px; height: 24px; }
.icon--m { width: 40px; height: 40px; }
.icon--l { width: 64px; height: 64px; }
.block-icon { display: flex; flex-direction: column; align-items: center; gap: var(--space-2); margin: 0; }
.block-icon--left { align-items: flex-start; }
.block-icon--right { align-items: flex-end; }
.block-icon--brand .icon { color: var(--color-brand-primary); }
.block-icon--muted .icon { color: var(--color-text-secondary); }
.block-icon__label { font-family: var(--font-ui); font-size: var(--text-sm); line-height: var(--leading-snug); }
.block-icon--left .block-icon__label { text-align: left; }
.block-icon--center .block-icon__label { text-align: center; }
.block-icon--right .block-icon__label { text-align: right; }
/* Inside a column the icon is usually the column's first thing — no extra top margin needed; the
   column's own `> * + *` spacing handles what follows it. */

/* Inline icons (scope-briefs/027 coins 2 and 3): em-sized so they sit with whatever text they are in —
   a table cell, a button, a link. Declared after .icon--s so the em size wins for inline use. */
.icon--inline { width: 1.25em; height: 1.25em; vertical-align: -0.3em; }
.icon--before { margin-right: 0.4em; }
.icon--after { margin-left: 0.4em; }
.btn .icon--before, .btn .icon--after { margin: 0; }   /* .btn is inline-flex with its own gap */
.article-table .icon--inline { vertical-align: -0.25em; }

/* ============================================================
   Consent notice — scope-briefs/031 Coin 1
   What stands where a block was held back because the visitor has not allowed the
   cookie category it needs. Deliberately in CORE article.css, not in the consent
   module's own sheet: the notice must still look intentional on a site where that
   module is not installed, and a semantic style defined only in a module's stylesheet
   is how a destructive button once came out looking like a safe one (traps.md).
   Self-contained for the same reason — it borrows nothing from consent.css.
   ============================================================ */

.consent-notice {
  position: relative;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 180px;
  padding: var(--space-8) var(--space-6);
  border: 1px dashed var(--color-border-medium);
  border-radius: var(--radius-md);
  background: var(--color-surface-secondary);
  color: var(--color-text-primary);
  text-align: center;
}

/* The optional image (D5) keeps the page's shape where the embed would have been.
   Decorative only: it renders aria-hidden with an empty alt. */
.consent-notice__image {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* A scrim over the image so the words stay readable over ANY photo — contrast here
   cannot depend on which picture the operator picked. */
.consent-notice--image::after {
  content: "";
  position: absolute;
  inset: 0;
  background: rgba(255, 255, 255, 0.86);
}

.consent-notice__body {
  position: relative;   /* above the scrim */
  z-index: 1;
  max-width: 46ch;
  margin: 0 auto;
}

.consent-notice__text {
  margin: 0;
  font-family: var(--font-ui);
  font-size: var(--text-sm);
  line-height: var(--leading-relaxed);
  color: var(--color-text-secondary);
}

.consent-notice__action { margin: var(--space-5) 0 0; }

/* Styled here in full rather than reusing the consent module's .consent-reopen__btn:
   that sheet only loads when the module is installed, and a button that inherits its
   look from a file that may not be there is the trap this whole block exists to avoid. */
.consent-notice__btn {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-family: var(--font-ui);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  padding: var(--space-3) var(--space-6);
  border: 1px solid var(--color-brand-primary);
  border-radius: var(--radius-pill);
  background: var(--color-brand-primary);
  color: var(--color-surface-white);
  cursor: pointer;
  transition: background 0.2s ease;
}

.consent-notice__btn:hover { background: var(--color-brand-dark); }

.consent-notice__btn:focus-visible {
  outline: 2px solid var(--color-text-primary);
  outline-offset: 2px;
}

@media (max-width: 480px) {
  .consent-notice { min-height: 140px; padding: var(--space-6) var(--space-4); }
}

@media (prefers-reduced-motion: reduce) {
  .consent-notice__btn { transition: none; }
}
