/* CL Growth Academy — shared styles
   Palette, type, and voice per brand snapshot (Aug 2026) */

/* ---------------------------------------------------------------
   1. TOKENS
   --------------------------------------------------------------- */

:root {
  /* Palette — proportions from the brand snapshot */
  --ink: #1A1310;      /* base, 60% */
  --oxblood: #9E2D24;  /* hero, 20% */
  --paper: #F2EADB;    /* type surface, 10% */
  --mustard: #D4A038;  /* accent, 6% — never wall-to-wall */
  --cream: #F2DFA8;    /* soft alt on oxblood, 3% */
  --ink-2: #221A14;    /* surface, 1% */

  /* RGB triplets, so alpha variants stay tied to the tokens above */
  --paper-rgb: 242, 234, 219;
  --ink-rgb: 26, 19, 16;

  /* Dimmed text as real colors rather than stacked opacity,
     so contrast stays measurable and passes WCAG AA */
  --paper-muted: #C9BFAE;
  --paper-faint: #A89C88;
  --ink-muted: #5C4E42;

  /* Type */
  --font-display: 'Alfa Slab One', serif;
  --font-script: 'Yellowtail', cursive;
  --font-body: 'Work Sans', sans-serif;

  --fs-display: clamp(2.6rem, 6vw, 4.2rem);
  --fs-h1: clamp(2.1rem, 4.6vw, 3.2rem);
  --fs-h2: clamp(1.7rem, 3.4vw, 2.4rem);
  --fs-h3: clamp(1.05rem, 1.6vw, 1.3rem);
  /* Card-scale headings. Set in Work Sans 800, not Alfa Slab One — see the
     .card-title rule in §3 for why the display face is wrong at this size. */
  --fs-card-title: clamp(1rem, 1.5vw, 1.15rem);
  --fs-lead: clamp(1rem, 1.4vw, 1.15rem);
  --fs-body: 1rem;
  --fs-small: 0.875rem;
  --fs-micro: 0.75rem;

  /* Article subheads. Deliberately smaller than --fs-h2/--fs-h3: a service
     page carries four or five headings, an article carries a dozen, and the
     display face at 2.4rem twelve times over reads as a shouting match. */
  --fs-article-h2: clamp(1.5rem, 2.6vw, 1.9rem);
  --fs-article-h3: clamp(1.15rem, 1.8vw, 1.35rem);

  --tracking-wide: 1px;
  --tracking-wider: 1.5px;

  /* Spacing — 4px base */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 24px;
  --space-6: 32px;
  --space-7: 48px;
  --space-8: 64px;
  --space-9: 96px;
  --space-10: 128px;

  /* Form */
  --radius-sm: 4px;
  --radius-md: 6px;
  --radius-lg: 10px;
  --radius-pill: 999px;

  --border-hairline: 1px solid rgba(var(--paper-rgb), 0.12);
  --border-hairline-ink: 1px solid rgba(var(--ink-rgb), 0.15);

  --shadow-sm: 0 1px 2px rgba(var(--ink-rgb), 0.2);
  --shadow-md: 0 6px 18px rgba(var(--ink-rgb), 0.28);
  --shadow-lg: 0 16px 40px rgba(var(--ink-rgb), 0.35);

  --ease: cubic-bezier(0.2, 0.7, 0.3, 1);
  --t-fast: 0.15s;
  --t-base: 0.25s;
  --t-slow: 0.5s;

  --max-width: 1120px;

  /* Height of the sticky .site-header, for scroll-margin-top on anchor
     targets. Measured, not derived, because CSS cannot read it: .nav has
     var(--space-4) padding top and bottom around a .brand with
     min-height 44px, plus the header's 1px border-bottom.
     16 + 44 + 16 + 1 = 77. If either the nav padding or the brand tap
     target changes, this has to follow or every anchor jump lands under
     the header again. */
  --header-h: 77px;

  /* Workshop paper grain — inline SVG, no network request */
  --grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23g)' opacity='0.5'/%3E%3C/svg%3E");
}

/* ---------------------------------------------------------------
   2. BASE
   --------------------------------------------------------------- */

* { box-sizing: border-box; }

html {
  scroll-behavior: smooth;
  -webkit-text-size-adjust: 100%;
}

/* A backstop, not the fix. Every known overflow source is dealt with at its
   origin (the .form-pill stack in the 480px tier, the gallery's hidden overflow,
   overflow-wrap on headings and long email tokens). This catches the next
   one, so a single wide child cannot rock the whole document sideways — a
   failure mode the site had on every page.

   clip, NOT hidden. `overflow-x: hidden` computes overflow-y to auto, which
   makes body a scroll container and breaks position:sticky on .site-header
   (:309). `clip` suppresses the overflow without establishing one, so the
   header keeps sticking. Safari <16 ignores clip and falls back to visible:
   that loses the backstop on old iOS but never breaks the header, which is
   the right way round for a rule that should not be doing any work. */
html,
body {
  overflow-x: clip;
  max-width: 100%;
}

body {
  margin: 0;
  font-family: var(--font-body);
  font-size: var(--fs-body);
  font-weight: 400;
  color: var(--paper);
  background: var(--ink);
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
}

img { max-width: 100%; display: block; }

a { color: inherit; text-decoration: none; }

p { margin: 0 0 var(--space-4) 0; }
p:last-child { margin-bottom: 0; }

/* Visible focus ring — the site had none, and the UA default is
   effectively invisible against --ink */
:focus-visible {
  outline: 3px solid var(--mustard);
  outline-offset: 3px;
  border-radius: var(--radius-sm);
}

.skip-link {
  position: absolute;
  left: var(--space-4);
  top: var(--space-4);
  z-index: 100;
  background: var(--mustard);
  color: var(--ink);
  font-weight: 700;
  font-size: var(--fs-small);
  text-transform: uppercase;
  letter-spacing: var(--tracking-wide);
  padding: var(--space-3) var(--space-5);
  border-radius: var(--radius-sm);
  transform: translateY(-200%);
  transition: transform var(--t-base) var(--ease);
}

.skip-link:focus { transform: translateY(0); }

/* Announced but not shown. Used for the new-tab warning on outbound
   links, which are otherwise a silent surprise for screen reader users. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.wrap {
  max-width: var(--max-width);
  margin: 0 auto;
  padding: 0 var(--space-5);
}

/* ---------------------------------------------------------------
   3. TYPE
   --------------------------------------------------------------- */

h1, h2, h3, h4 {
  font-family: var(--font-display);
  text-transform: uppercase;
  line-height: 1.12;
  margin: 0;
  letter-spacing: 0.5px;
  text-wrap: balance;
  /* Alfa Slab One uppercase is very wide, and every clamp below bottoms out
     on a phone, so a single long word can exceed the 272px of content a
     320px viewport offers. Breaking mid-word is ugly; overflowing the
     viewport is worse. */
  overflow-wrap: break-word;
}

h1 { font-size: var(--fs-h1); }
h2 { font-size: var(--fs-h2); }
h3 { font-size: var(--fs-h3); }

/* Card-scale headings — the display face does not survive a narrow column.
   At 5-up in .grid-5 a card has ~157px of measure, where Alfa Slab One at
   --fs-h3 wraps "Outsourced NDIS Marketing Manager" to four lines while
   "NDIS SEO" takes one: the row reads ragged and the cards fight each other.

   Work Sans 800 uppercase is ~35% narrower and is not a new voice — it is
   the .eyebrow treatment (:186) one step up the scale. Alfa Slab One keeps
   every h1/h2, the .way cards, .channel-row h3 and the pull-quotes, i.e.
   everywhere it has the measure to behave like a display face. */
.card h3,
.post-card h3,
.channel-note h3,
.nudge-title,
.faq summary {
  font-family: var(--font-body);
  font-size: var(--fs-card-title);
  font-weight: 800;
  letter-spacing: var(--tracking-wide);
  line-height: 1.25;
}

.script {
  font-family: var(--font-script);
  text-transform: none;
  font-weight: 400;
  letter-spacing: normal;
}

/* Holds a phrase on one line so a headline breaks where it reads best.
   Replaces welding words with &nbsp;, which cannot be undone at any width:
   "newsletter from me." in Alfa Slab One runs 480-505px as one unbreakable
   token, and it was setting the minimum width of the entire newsletter hero
   grid — which is 320px + 1fr, so the row needed roughly 900px of wrap
   before the phrase fit. Below that it pushed the hero past the viewport,
   measured at 768px and 1024px as well as on every phone.

   Written as opt-in above 1200px rather than opt-out below some width. The
   phrase is not a fixed size — --fs-h1 keeps growing with the viewport up
   to 51px, so the phrase grows too (~13x the font size), and a hand-picked
   max-width release has to be re-derived every time the type scale moves.
   Defaulting to normal means the failure mode is a line breaking somewhere
   slightly less pretty, not a hero pushed off the screen. */
.keep-together { white-space: normal; }

@media (min-width: 1200px) {
  .keep-together { white-space: nowrap; }
}

.eyebrow {
  display: inline-block;
  font-family: var(--font-body);
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: var(--tracking-wider);
  font-size: var(--fs-micro);
  color: var(--mustard);
}

/* Eyebrow sitting on a cream/paper surface */
.eyebrow-light { color: var(--oxblood); }

.lead {
  font-size: var(--fs-lead);
  max-width: 62ch;
}

.note {
  font-size: var(--fs-small);
  color: var(--paper-faint);
  margin-top: var(--space-4);
}

.section-paper .note { color: var(--ink-muted); }

.breadcrumb {
  font-size: var(--fs-micro);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: var(--tracking-wide);
  color: var(--paper-muted);
  margin-bottom: var(--space-4);
}

/* 12px type gives a 14px-tall inline box. These are real navigation on the
   five channel pages, so they get a tappable box rather than the line box.

   min-width matters as much as min-height once the label is short: the
   article pages point back to "Blog", which is 37px wide at this size and
   was the only tap target on the site under 44px. The box grows rightwards
   from a flex-start default, so the label stays where it was and the extra
   space lands before the separator. */
.breadcrumb a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  min-width: 44px;
}

.breadcrumb a:hover { color: var(--cream); }

/* Section heading pairing — replaces the ad-hoc 10/14/16/20/22px
   inline margins that were doing this job */
.section-head { margin-bottom: var(--space-7); }
.section-head .eyebrow { margin-bottom: var(--space-3); }

/* ---------------------------------------------------------------
   4. BUTTONS
   --------------------------------------------------------------- */

.btn {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-family: var(--font-body);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: var(--tracking-wide);
  font-size: var(--fs-small);
  padding: var(--space-4) var(--space-6);
  border-radius: var(--radius-sm);
  border: 2px solid transparent;
  cursor: pointer;
  transition: transform var(--t-fast) var(--ease),
              box-shadow var(--t-fast) var(--ease),
              background var(--t-fast) var(--ease);
}

.btn:hover {
  transform: translateY(-2px);
  box-shadow: var(--shadow-md);
}

.btn:active { transform: translateY(0); }

.btn-mustard { background: var(--mustard); color: var(--ink); }
.btn-paper { background: var(--paper); color: var(--ink); }
.btn-ink { background: var(--ink); color: var(--paper); }

.btn-ghost {
  background: transparent;
  border-color: rgba(var(--paper-rgb), 0.4);
  color: var(--paper);
}

.btn-ghost:hover { border-color: var(--paper); }

.btn-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
  align-items: center;
  margin-top: var(--space-6);
}

/* ---------------------------------------------------------------
   5. BRAND DEVICE
   --------------------------------------------------------------- */

.brand-strip {
  display: flex;
  gap: var(--space-2);
  flex-wrap: wrap;
  padding: var(--space-3) var(--space-5);
  max-width: var(--max-width);
  margin: 0 auto;
}

.brand-strip span {
  width: 14px;
  height: 14px;
  background: var(--oxblood);
  flex: 0 0 auto;
}

/* ---------------------------------------------------------------
   6. HEADER / NAV
   --------------------------------------------------------------- */

.site-header {
  position: sticky;
  top: 0;
  z-index: 50;
  background: var(--ink);
  border-bottom: var(--border-hairline);
  transition: box-shadow var(--t-base) var(--ease),
              background var(--t-base) var(--ease);
}

.site-header.is-scrolled {
  background: var(--ink-2);
  box-shadow: var(--shadow-md);
}

.nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-5);
  padding: var(--space-4) var(--space-5);
  max-width: var(--max-width);
  margin: 0 auto;
}

/* Logo lockup: oxblood disc holding "CL", wordmark stacked beside it.
   Built from type and geometry rather than an image so it stays crisp
   at any size and remains real text. --brand-scale lets the footer
   render the same lockup larger without duplicated rules. */

.brand {
  --brand-scale: 1;
  display: inline-flex;
  align-items: center;
  gap: calc(10px * var(--brand-scale));
  white-space: nowrap;
  /* The 40px mark left the whole lockup 4px short of a 44px tap target,
     and it is the home link on every page */
  min-height: 44px;
}

.brand-mark {
  flex: 0 0 auto;
  width: calc(40px * var(--brand-scale));
  height: calc(40px * var(--brand-scale));
  border-radius: var(--radius-pill);
  background: var(--oxblood);
  color: var(--paper);
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-display);
  font-size: calc(0.92rem * var(--brand-scale));
  line-height: 1;
  /* Alfa Slab One sits slightly high in its box; nudge for optical centre */
  padding-top: calc(2px * var(--brand-scale));
  letter-spacing: 0.5px;
}

.brand-word {
  display: flex;
  flex-direction: column;
  font-family: var(--font-display);
  text-transform: uppercase;
  color: var(--paper);
  font-size: calc(0.78rem * var(--brand-scale));
  line-height: 0.98;
  letter-spacing: 0.6px;
}

.nav-links {
  position: relative;
  display: flex;
  gap: var(--space-6);
  align-items: center;
  font-weight: 600;
  font-size: var(--fs-small);
}

.nav-links a { transition: color var(--t-fast) var(--ease); }
.nav-links a:hover { color: var(--mustard); }

/* Active page indicator — the nav used to look identical everywhere.
   On desktop .nav-indicator takes over and this underline is suppressed
   (see below); the rule stays because the mobile drawer still uses it. */
.nav-links a[aria-current="page"],
.nav-links .nav-sub-trigger[aria-current] {
  color: var(--mustard);
  box-shadow: inset 0 -2px 0 var(--mustard);
}

/* The CTA is a filled mustard button, so the active-page rule above was
   painting its label in its own background colour and the words vanished
   entirely on contact.html, the one page where it IS the current page.
   It keeps the button's own ink, and needs no underline because the fill
   already marks it out. */
.nav-links a.nav-cta[aria-current="page"] {
  color: var(--ink);
  box-shadow: none;
}

/* ---- Sliding indicator ----
   A single bar positioned by JS from each item's offsetLeft/offsetWidth,
   rather than a per-item pseudo-element, so it can travel between items
   instead of cross-fading. Desktop only: inside the mobile drawer the
   items are stacked full-width and the left-bar marker reads better. */

.nav-indicator {
  position: absolute;
  bottom: -6px;
  left: 0;
  height: 2px;
  width: 0;
  border-radius: var(--radius-pill);
  background: var(--mustard);
  opacity: 0;
  pointer-events: none;
  transform: translateX(0);
  transition: transform var(--t-base) var(--ease),
              width var(--t-base) var(--ease),
              opacity var(--t-fast) var(--ease);
}

.nav-indicator.is-visible { opacity: 1; }

/* The travelling bar replaces the static underline, or the current page
   would carry two markers at once */
@media (min-width: 801px) {
  .nav-links a[aria-current="page"],
  .nav-links .nav-sub-trigger[aria-current] { box-shadow: none; }
}

/* ---- NDIS Marketing dropdown ---- */

.nav-sub { position: relative; }

.nav-sub-trigger {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: 0;
  border: none;
  background: none;
  color: inherit;
  font: inherit;
  font-weight: 600;
  cursor: pointer;
  transition: color var(--t-fast) var(--ease);
}

.nav-sub-trigger:hover,
.nav-sub-trigger[aria-expanded="true"] { color: var(--mustard); }

.nav-sub-trigger svg {
  width: 12px;
  height: 12px;
  transition: transform var(--t-base) var(--ease);
}

.nav-sub-trigger[aria-expanded="true"] svg { transform: rotate(180deg); }

/* Opaque on purpose. On the homepage .site-header is a transparent
   gradient over the masterclass photograph, so an inherited background
   would drop this menu's text straight onto the projector screen. */
.nav-panel {
  position: absolute;
  top: calc(100% + var(--space-4));
  left: 50%;
  z-index: 60;
  /* min(), not a flat 600px: at the 801px breakpoint where this panel first
     appears, 600px plus the wrap's gutters would overhang the viewport. */
  width: min(600px, calc(100vw - 2 * var(--space-5)));
  padding: var(--space-3);
  border-radius: var(--radius-lg);
  background: var(--ink-2);
  border: var(--border-hairline);
  box-shadow: var(--shadow-lg);
  opacity: 0;
  transform: translate(-50%, -6px);
  visibility: hidden;
  transition: opacity var(--t-base) var(--ease),
              transform var(--t-base) var(--ease),
              visibility 0s linear var(--t-base);
}

.nav-panel.open {
  opacity: 1;
  transform: translate(-50%, 0);
  visibility: visible;
  transition-delay: 0s;
}

/* Bridges the gap between trigger and panel so a diagonal mouse path
   does not cross dead space and close the menu */
.nav-panel::before {
  content: '';
  position: absolute;
  top: calc(var(--space-4) * -1);
  left: 0;
  right: 0;
  height: var(--space-4);
}

.nav-panel a {
  display: flex;
  gap: var(--space-3);
  padding: var(--space-3);
  border-radius: var(--radius-md);
  transition: background var(--t-fast) var(--ease);
}

.nav-panel a:hover,
.nav-panel a:focus-visible { background: rgba(var(--paper-rgb), 0.07); }

.nav-panel-num {
  flex: 0 0 auto;
  font-family: var(--font-display);
  font-size: var(--fs-micro);
  color: var(--mustard);
  padding-top: 2px;
}

.nav-panel-title { display: block; color: var(--paper); }

.nav-panel-desc {
  display: block;
  margin-top: 2px;
  font-weight: 400;
  font-size: var(--fs-micro);
  line-height: 1.4;
  color: var(--paper-faint);
}

.nav-panel-overview {
  margin-bottom: var(--space-2);
  padding-bottom: var(--space-3);
  border-bottom: var(--border-hairline);
}

/* ---- Two-column panel ----
   The dropdown lists seven channels across two engines plus the managed
   tier. Nine rows in a 320px column runs past the fold on a laptop, so the
   groups sit side by side and the panel widens to suit.

   Each group is its own wrapper rather than the panel being a two-column
   grid: a grid flows row-wise, which would interleave Marketing and Growth
   entries. Wrappers keep DOM order equal to reading order, which is also
   what initNavIndicator's arrow-key walk over querySelectorAll('a')
   depends on. */
.nav-panel-cols {
  display: flex;
  gap: var(--space-2);
}

.nav-panel-col { flex: 1 1 0; min-width: 0; }

/* A label, not a heading: it must not enter the document outline, and the
   panel sits inside a <nav> where an h-level would be noise. */
.nav-panel-group {
  display: block;
  padding: var(--space-2) var(--space-3) var(--space-1);
  font-weight: 800;
  text-transform: uppercase;
  font-size: var(--fs-micro);
  letter-spacing: var(--tracking-wider);
  color: var(--mustard);
}

.nav-panel-group + .nav-panel-group,
.nav-panel-col a + .nav-panel-group { margin-top: var(--space-3); }

/* 44px square. It was 39x34 — the smallest hit area on the site and also
   the most-used control on a phone, since it gates the whole nav. The glyph
   still renders at 1.6rem; the box around it is what changed. */
.nav-toggle {
  display: none;
  background: none;
  border: none;
  color: var(--paper);
  font-size: 1.6rem;
  line-height: 1;
  cursor: pointer;
  width: 44px;
  height: 44px;
  padding: 0;
  align-items: center;
  justify-content: center;
}

/* ---------------------------------------------------------------
   7. SECTIONS
   --------------------------------------------------------------- */

section { padding: var(--space-9) 0; }

.section-oxblood { background: var(--oxblood); color: var(--paper); }
.section-ink2 { background: var(--ink-2); }

.section-paper {
  background: var(--paper);
  color: var(--ink);
  background-image: var(--grain);
  background-blend-mode: multiply;
}

.section-paper h1, .section-paper h2,
.section-paper h3, .section-paper h4 { color: var(--ink); }

/* Centered closing CTA band — replaces the inline
   text-align/margin pairs repeated on 7 pages */
.section-cta { text-align: center; }
.section-cta .lead { margin: var(--space-4) auto 0; }
.section-cta .btn-row { justify-content: center; }

/* ---------------------------------------------------------------
   8. HERO
   --------------------------------------------------------------- */

.hero {
  background: var(--oxblood);
  padding: var(--space-10) 0 var(--space-9);
}

/* Interior pages run a shorter hero — this was 9 inline
   `padding: 60px 0` overrides */
.hero-compact { padding: var(--space-8) 0; }

.hero .eyebrow { color: var(--cream); }

.hero-top {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  flex-wrap: wrap;
  gap: var(--space-3);
}

.hero-top .script {
  font-size: 1.5rem;
  color: var(--cream);
}

.hero h1 {
  margin-top: var(--space-5);
  font-size: var(--fs-display);
  max-width: 20ch;
}

.hero-compact h1 {
  font-size: var(--fs-h1);
  margin-top: var(--space-4);
}

.hero .lead {
  margin-top: var(--space-6);
  border-top: 1px solid rgba(var(--paper-rgb), 0.35);
  padding-top: var(--space-5);
}

/* Written for a hero that spread two items to the edges, then never used by
   any page. The article pages use it as a byline, where three items pushed
   apart across 1072px stop reading as one line, so it starts from the left
   and the gap does the separating. Nothing else consumes it. */
.hero-meta {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--space-3) var(--space-5);
  margin-top: var(--space-6);
  font-weight: 700;
  text-transform: uppercase;
  font-size: var(--fs-micro);
  letter-spacing: var(--tracking-wide);
  color: var(--cream);
}

/* ---- Split hero: a photograph beside the headline ----
   The four article heroes under blog/ were type on a flat oxblood band with
   nothing to look at, so nothing pulled a reader in off a search result.

   Not full-bleed like §18. The event photographs are 760px wide on disk, and
   a full-bleed hero would upscale them about 1.9x, which is the mistake the
   760px cap on .wrap > .channel-media in §27 exists to prevent.

   A modifier on the section rather than a `.article .hero .wrap` descendant
   rule, for the same reason .hero-compact and .showcase-wide are modifiers:
   it travels with the markup, it does not depend on an ancestor class two
   elements up, and `grep hero-split` finds exactly the pages using it.
   Scoping it this way is also what keeps the other fourteen heroes provably
   untouched. Named .hero-plate rather than .hero-photo on purpose: §18 is six
   hundred lines of .photo-hero and .photo-frame, and the two names side by
   side in one stylesheet is a transposition bug waiting to happen. */
.hero-split .wrap {
  display: grid;
  /* minmax(0, ...) on both tracks. A bare 1fr is minmax(auto, 1fr) and takes
     its floor from min-content: "participants" in Alfa Slab One at --fs-h1 is
     a ~250px word with nowhere to break. That is the bug §25 documents twice,
     where one long word held the newsletter hero at 481px on every phone.

     0.75fr is measured, not picked. The sharpness this site accepts is 0.87x
     of the DPR2 requirement: the channel pages feed 900px sources into a
     520px slot, and 900/1040 = 0.865. For a 760px source that means a 439px
     slot. Across 1072px of wrap content less the 48px gap, 439/1024 needs
     0.75fr against 1fr, which also leaves the headline 585px. Raise this
     number and the photograph starts upscaling. */
  grid-template-columns: minmax(0, 1fr) minmax(0, 0.75fr);
  gap: var(--space-7);
  /* end, not center. Every one of the four headlines makes the copy column
     taller than the 293px plate, so centring leaves a symmetric band of empty
     oxblood above and below the photograph, which reads as a mistake rather
     than as space. Flushed down, the plate's lower edge and the byline row
     form one line across the hero and the slack lands at top right, where
     nobody is looking. If a much shorter headline ever arrives, check this. */
  align-items: end;
}

.hero-plate {
  width: 100%;
  /* height:auto is load-bearing, and this is the fifth instance of the trap in
     this file (.post-thumb §10, .founder-photo §13, .showcase-layer img §29,
     .article figure img §30). The img carries height="570", which lands on
     the CSS height property as a presentational hint; with width and height
     both definite the aspect-ratio below is silently ignored, the three 4:3
     frames render 4:3, and the four heroes stop matching each other. */
  height: auto;
  /* 3:2 is not a new ratio on these pages: .showcase-wide in §29 already crops
     these same event photographs to it, further down the same articles. It
     also normalises the set, which matters because the hero is the one band a
     reader meets on all four. masterclass-slide is 760x507, natively 3:2, and
     loses nothing. The other three are 760x570 and lose 11% of their height,
     5.6% off each edge, which is ceiling at the top and carpet at the bottom
     in all three. No face is near either edge, so no object-position is
     needed. */
  aspect-ratio: 3 / 2;
  object-fit: cover;
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  /* Holds the box while the file loads. Not var(--ink-2) like the figures in
     §30: those sit on ink, this sits on oxblood, and a near-black plate
     flashing inside a red band is louder than the photograph arriving. */
  background: rgba(var(--ink-rgb), 0.22);
}

/* Below 900 the pair stops working rather than merely getting tight: the copy
   column is a five-element stack against a photograph shrunk to ~345px, so
   most of the hero is empty oxblood at top right. Same collapse point as
   .founder and the card grids in §22. */
@media (max-width: 900px) {
  .hero-split .wrap {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-6);
  }

  /* 470px is .showcase-media's cap from §29, reused rather than invented.
     Uncapped, the plate runs the full 852px of wrap at the top of this query
     and upscales a 760px source 1.12x. Left aligned, not centred: the whole
     hero is left aligned, and a centred plate under left-aligned type reads
     as a caption. */
  .hero-plate { max-width: 470px; }
}

@media (max-width: 700px) {
  /* .hero.hero-split, not .hero-split. §22 sets `.hero { padding:
     var(--space-8) 0 }` inside its own 700px block, later in this file and as
     a shorthand, so a (0,1,0) selector here would be reset straight back to
     64px at both ends. (0,2,0) wins on specificity whatever the order. The
     reason for the change: a photograph's lower edge does not need the air
     that a line of 12px uppercase type does, and this is 16px straight off
     the phone hero, where height is the whole problem. */
  .hero.hero-split { padding-bottom: var(--space-7); }
}

/* ---------------------------------------------------------------
   9. GRIDS
   --------------------------------------------------------------- */

/* margin-top is load-bearing, not decoration. This site spaces blocks by
   putting a top margin on the FOLLOWING element, and .section-head is the only
   thing that owns a margin-bottom (48px). A .grid with no top margin therefore
   looked fine only because a .section-head almost always sat above it: where
   one did not, the grid butted straight against the previous block and the
   first card's border landed on the last line of body copy. That shipped on
   all eight channel pages, under every `.channel-row` followed by a
   `.grid.grid-2`.

   Sibling margins collapse to the larger of the two, and no .grid on this site
   is ever a grid or flex item, so the collapse is never suppressed. That makes
   this safe rather than sweeping: after a .section-head it is max(48, 48) and
   nothing moves, which is 18 of the 29 grids on the site. It only bites where
   the gap was wrong: 0px after a .channel-row, 16px after a bare .lead.

   tools/spacing.js exists to catch the next one of these. */
.grid {
  display: grid;
  gap: var(--space-5);
  margin-top: var(--space-7);
}

/* contact.html opens a section with a .grid and no .section-head above it. A
   first child's top margin has nothing to collapse with and cannot collapse
   through the section's padding, so it would simply add 48px to the 96px
   already there. */
.wrap > .grid:first-child { margin-top: 0; }

/* grid-5 forces five columns where grid-auto will not: five 210px minimums
   plus four 24px gaps is 1146px, over the 1120px wrap, so auto-fit drops to
   four and strands the fifth card on a row of its own. */
.grid-5 { grid-template-columns: repeat(5, minmax(0, 1fr)); }
.grid-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.grid-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.grid-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }

/* Flows any count sensibly — used for the 5 service cards, which
   previously needed a second grid-4 to hold the 5th card alone */
.grid-auto { grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); }

/* ---------------------------------------------------------------
   10. CARDS
   --------------------------------------------------------------- */

/* Column, not block, for the same reason .post-card.has-thumb is one (:800):
   the grid equalises card heights, so without it the "Learn more" line
   trails whatever length its own copy happens to be and the row's five
   links land at five different heights. */
.card {
  display: flex;
  flex-direction: column;
  background: var(--paper);
  color: var(--ink);
  border-radius: var(--radius-md);
  padding: var(--space-6) var(--space-5);
  border: 2px solid transparent;
  transition: transform var(--t-base) var(--ease),
              box-shadow var(--t-base) var(--ease),
              border-color var(--t-base) var(--ease);
}

/* auto beats the .card-more margin-top on specificity; padding-top restores
   a minimum gap for the tallest card in the row, where auto resolves to 0.
   Same pairing as .has-thumb .card-more (:836). */
.card > .card-more { margin-top: auto; padding-top: var(--space-5); }

/* Only cards that are actually links get an interactive affordance */
a.card:hover,
a.card:focus-visible {
  transform: translateY(-4px);
  box-shadow: var(--shadow-lg);
  border-color: var(--oxblood);
}

.card-number {
  width: 36px;
  height: 36px;
  border-radius: var(--radius-pill);
  background: var(--oxblood);
  color: var(--paper);
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 800;
  font-size: var(--fs-small);
  margin-bottom: var(--space-4);
}

.card h3 { margin-bottom: var(--space-3); }
.card p { color: var(--ink); }

/* "Read more →" / "Learn more →" — 15 inline styles collapsed */
.card-more {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-4);
  margin-bottom: 0;
  font-size: var(--fs-small);
  font-weight: 700;
  color: var(--mustard);
}

.card-more::after {
  content: '→';
  transition: transform var(--t-base) var(--ease);
}

a.post-card:hover .card-more::after,
a.post-card:focus-visible .card-more::after,
a.card:hover .card-more::after { transform: translateX(4px); }

.post-card {
  display: block;
  background: var(--ink-2);
  border-radius: var(--radius-md);
  padding: var(--space-6);
  border: 2px solid transparent;
  transition: transform var(--t-base) var(--ease),
              box-shadow var(--t-base) var(--ease),
              border-color var(--t-base) var(--ease);
}

a.post-card:hover,
a.post-card:focus-visible {
  transform: translateY(-4px);
  box-shadow: var(--shadow-lg);
  border-color: var(--mustard);
}

.post-card .eyebrow { margin-bottom: var(--space-3); }

/* Thumbnail variant. The card's own padding moves to .post-body so the
   artwork can bleed to the edge, and overflow clips it back to the radius
   — neither is needed by the base .post-card, so both live here.

   The resting border matches .is-upcoming above for the same reason it
   exists there: on index.html these cards sit in a .section-ink2 band whose
   background is the card's own --ink-2, so without an edge the body of the
   card dissolves into the band. Held at 2px so the mustard hover swaps
   colour without shifting layout. */
.post-card.has-thumb {
  padding: 0;
  overflow: hidden;
  border-color: rgba(var(--paper-rgb), 0.12);
  /* Column so .post-body can stretch to the grid-equalised card height,
     which is what lets the Read more line sit on a common baseline
     across cards whose excerpts run to different lengths */
  display: flex;
  flex-direction: column;
}

.post-thumb {
  width: 100%;
  /* height:auto is load-bearing, the same trap documented on .founder-photo:
     the img carries height="500", which lands on the CSS height property as
     a presentational hint. Width and height would then both be definite and
     aspect-ratio would be ignored. The attributes stay for CLS reservation. */
  height: auto;
  aspect-ratio: 16 / 10;
  object-fit: cover;
  background: var(--ink);
}

.post-body {
  padding: var(--space-6);
  display: flex;
  flex-direction: column;
  flex: 1;
}

/* Pushed to the foot of the card rather than trailing the excerpt */
.has-thumb .card-more { margin-top: auto; padding-top: var(--space-5); }

/* Dimmed like .faq p rather than sitting at full --paper brightness, so the
   title keeps the hierarchy */
.post-excerpt {
  margin-top: var(--space-3);
  font-size: var(--fs-small);
  color: var(--paper-muted);
}

/* Outbound arrow in place of the in-site →.
   Still in use, though the four featured posts now have pages here: the
   second band on blog.html lists the four articles that are still only on
   thegrowthacademy.com.au. Delete this rule only if that band goes. */
.post-card.is-external .card-more::after { content: '↗'; }

/* ---------------------------------------------------------------
   11. SOCIAL PROOF
   --------------------------------------------------------------- */

/* Sliding logo wall. The track holds the logo set twice so a
   -50% translate loops seamlessly; the second copy is aria-hidden
   and untabbable so it doesn't duplicate content or tab stops. */

.marquee {
  position: relative;
  overflow: hidden;
  padding: var(--space-3) 0;
  /* Fade the ends rather than clipping logos mid-tile */
  mask-image: linear-gradient(to right, transparent 0, #000 7%, #000 93%, transparent 100%);
  -webkit-mask-image: linear-gradient(to right, transparent 0, #000 7%, #000 93%, transparent 100%);
}

/* Default is a static wrapping wall, so the logos are all visible
   with JS off or motion reduced. JS adds .is-animating to opt in. */
.marquee-track {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  width: 100%;
  gap: var(--space-4);
}

.marquee-track.is-animating {
  flex-wrap: nowrap;
  justify-content: flex-start;
  width: max-content;
  animation: marquee 48s linear infinite;
}

@keyframes marquee {
  from { transform: translateX(0); }
  to { transform: translateX(-50%); }
}

/* Pauses on hover so a logo can be read rather than chased.

   Honest about the limit: the tiles are plain divs with nothing focusable
   inside, so :focus-within cannot fire, and there is no touch equivalent —
   on a phone the wall never stops. Accepted rather than fixed, because the
   alternative is making thirteen decorative logos focusable and adding
   thirteen stops to the tab order of every page. Nothing is only available
   here; the client names appear in the copy too. The :focus-within half is
   kept for the day a tile becomes a real link. */
.marquee:hover .marquee-track,
.marquee:focus-within .marquee-track { animation-play-state: paused; }

.logo-tile {
  flex: 0 0 auto;
  width: 136px;
  height: 98px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-3);
  /* Pure white, not --paper: these logos are JPEGs with baked-in
     white backgrounds, so any off-white would show a visible edge */
  background: #FFFFFF;
  border: 2px solid transparent;
  border-radius: var(--radius-md);
  font: inherit;
  transition: transform var(--t-base) var(--ease),
              box-shadow var(--t-base) var(--ease),
              border-color var(--t-base) var(--ease);
}

.logo-tile img {
  max-width: 100%;
  max-height: 100%;
  width: auto;
  object-fit: contain;
}

/* The tiles are plain divs, not buttons: there is nothing to click. The
   lift on hover stays, because the wall pauses under the pointer and a
   little response keeps that from reading as a stall. On touch the lift is
   suppressed in §28, where :hover cannot be trusted. */
.logo-tile:hover {
  transform: scale(1.08);
  box-shadow: var(--shadow-lg);
  border-color: var(--mustard);
}

/* ---------------------------------------------------------------
   12. STAT PANEL
   --------------------------------------------------------------- */

.stat-panel {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 2px;
  border-radius: var(--radius-md);
  overflow: hidden;
  background: rgba(var(--ink-rgb), 0.25);
}

.stat { padding: var(--space-7) var(--space-5); }

.stat .num {
  font-family: var(--font-display);
  font-size: clamp(2.2rem, 4vw, 2.8rem);
  line-height: 1;
}

.stat .label {
  margin-top: var(--space-3);
  font-weight: 700;
  text-transform: uppercase;
  font-size: var(--fs-micro);
  letter-spacing: var(--tracking-wide);
}

.stat-paper { background: var(--paper); color: var(--ink); }
.stat-mustard { background: var(--mustard); color: var(--ink); }
.stat-oxblood { background: var(--oxblood); color: var(--paper); }

/* ---------------------------------------------------------------
   13. FOUNDER
   --------------------------------------------------------------- */

.founder {
  display: grid;
  grid-template-columns: 260px 1fr;
  gap: var(--space-7);
  align-items: center;
}

.founder-photo {
  width: 100%;
  /* height:auto is load-bearing. The img carries height="660", which maps to
     the CSS height property as a presentational hint; without this the box
     renders 260x660 and aspect-ratio is ignored, because width and height are
     then both definite. The attributes stay for aspect-ratio/CLS reservation. */
  height: auto;
  aspect-ratio: 1;
  object-fit: cover;
  border-radius: var(--radius-md);
  /* Holds the square while the file loads, matching the section behind it */
  background: var(--ink-2);
}

.founder-quote {
  font-size: var(--fs-lead);
  font-weight: 500;
}

.founder-sign {
  font-family: var(--font-script);
  font-size: 1.6rem;
  color: var(--mustard);
  margin-top: var(--space-5);
}

/* ---------------------------------------------------------------
   14. LEAD MAGNET
   --------------------------------------------------------------- */

.lead-panel {
  background: var(--paper);
  color: var(--ink);
  border-radius: var(--radius-lg);
  padding: var(--space-7);
  background-image: var(--grain);
  background-blend-mode: multiply;
}

.lead-panel h2 {
  color: var(--ink);
  margin-top: var(--space-3);
}

.chip-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: var(--space-5) 0;
  padding: 0;
  list-style: none;
}

.chip {
  background: var(--ink);
  color: var(--paper);
  border-radius: var(--radius-sm);
  padding: var(--space-3) var(--space-4);
  font-size: var(--fs-micro);
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: var(--tracking-wide);
}

/* ---------------------------------------------------------------
   15. FORMS
   --------------------------------------------------------------- */

.form-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-top: var(--space-5);
}

.form-row.stack { flex-direction: column; }

input, select, textarea {
  font-family: var(--font-body);
  font-size: var(--fs-body);
  padding: var(--space-4);
  border: 1px solid rgba(var(--paper-rgb), 0.25);
  border-radius: var(--radius-sm);
  background: var(--ink-2);
  color: var(--paper);
  flex: 1 1 200px;
  width: 100%;
  transition: border-color var(--t-fast) var(--ease);
}

/* Vertical only: the default `both` lets the drag handle widen a textarea
   past its container, which pushes the consult dialog into horizontal
   overflow and does the same to the contact form. */
textarea { resize: vertical; }

/* The only <select> on the site. appearance:none strips the UA chrome so it
   matches the text fields: left native it renders as system grey on the cream
   contact page and as a white slab on the dark consult dialog, neither of
   which is a field on this site. The right padding clears the chevron. */
select {
  appearance: none;
  -webkit-appearance: none;
  min-height: 44px;
  padding-right: var(--space-8);
  cursor: pointer;
}

/* The chevron needs a wrapper: a <select> cannot carry a pseudo-element, and
   drawing it as a background-image would be wiped by the `background:`
   shorthand in the light-surface override below. The wrapper hugs the control
   on its own, so 50% here is the control's own middle. currentColor means the
   one rule serves both the dark dialog and the cream page. */
.form-select { position: relative; display: block; }

.form-select::after {
  content: '';
  position: absolute;
  top: 50%;
  right: var(--space-4);
  width: 9px;
  height: 9px;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  /* -65%, not -50%: the mass of a chevron sits below the box it is drawn in,
     so centring the box leaves it looking low. */
  transform: translateY(-65%) rotate(45deg);
  opacity: 0.55;
  pointer-events: none;
}

/* The popup list is drawn by the OS and inherits nothing, so it has to be
   told the surface it belongs to or the dark dialog opens a white menu. */
select option { background: var(--ink-2); color: var(--paper); }

input::placeholder, textarea::placeholder { color: var(--paper-faint); }
input:hover, select:hover, textarea:hover { border-color: rgba(var(--paper-rgb), 0.5); }

/* Inputs on light surfaces. Scoped to the surface itself rather than
   the section — the homepage panel sits inside .section-oxblood, so a
   .section-paper-scoped rule missed it and rendered a near-black
   field on cream. */
.section-paper input, .section-paper select, .section-paper textarea,
.lead-panel input, .lead-panel select, .lead-panel textarea {
  background: #FFFDF8;
  color: var(--ink);
  border-color: rgba(var(--ink-rgb), 0.2);
}

.section-paper select option,
.lead-panel select option { background: #FFFDF8; color: var(--ink); }

.section-paper input::placeholder, .lead-panel input::placeholder,
.section-paper textarea::placeholder { color: var(--ink-muted); }

.section-paper input:hover, .lead-panel input:hover,
.section-paper select:hover, .lead-panel select:hover,
.section-paper textarea:hover { border-color: rgba(var(--ink-rgb), 0.45); }

label {
  font-size: var(--fs-micro);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.5px;
  display: block;
  margin-bottom: var(--space-2);
}

/* Dark surface by default, light surfaces overridden below — the same way
   round as every input rule above, and the way round the markup actually
   uses it: most of these sit under the footer signup, on --ink.

   It had --ink-muted as its base, which is the LIGHT-surface colour. On
   the footer that put #5C4E42 on #221A14: 2.14:1, against the 4.5:1 that
   small text needs, and the small print under every newsletter form on
   the site was effectively invisible. */
.form-note {
  font-size: var(--fs-micro);
  color: var(--paper-muted);
  margin-top: var(--space-4);
}

/* Scoped to the surface rather than the section, for the same reason the
   input rules are: the homepage panel sits inside .section-oxblood, so a
   .section-paper-scoped rule misses it. */
.section-paper .form-note,
.lead-panel .form-note { color: var(--ink-muted); }

/* ---- Waitlist pill ----
   One rounded surface holding the field and its button, which on submit
   fills mustard in place rather than being swapped out for a separate
   message. Swapping (the old .newsletter-form.is-submitted display:none)
   cannot be animated — the box has to survive for the morph to happen. */

.form-pill {
  position: relative;
  display: flex;
  margin-top: var(--space-5);
  padding: var(--space-2);
  border-radius: var(--radius-pill);
  background: var(--ink-2);
  border: 1px solid rgba(var(--paper-rgb), 0.25);
  transition: background var(--t-base) var(--ease),
              border-color var(--t-base) var(--ease);
}

/* Light surfaces, scoped to the surface rather than the section for the
   same reason as the input rule above — the homepage panel sits inside
   .section-oxblood, so a section-scoped rule misses it */
.lead-panel .form-pill,
.section-paper .form-pill {
  background: #FFFDF8;
  border-color: rgba(var(--ink-rgb), 0.2);
}

.form-pill:focus-within { border-color: var(--mustard); }

/* Button-only variant, for the contact form — a four-field form has no
   single field to sit in the pill, so only its submit morphs */
.form-pill-compact { display: inline-flex; margin-top: 0; }

.form-pill-fields {
  display: flex;
  flex: 1 1 auto;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
  transition: opacity var(--t-base) var(--ease),
              transform var(--t-base) var(--ease);
}

/* Borderless and transparent: the pill wrapper is the visible surface now,
   so the field must not draw a second one inside it */
.form-pill input {
  flex: 1 1 auto;
  width: auto;
  min-width: 0;
  min-height: 44px;   /* padding alone left it at 43 */
  border: none;
  background: transparent;
  padding: var(--space-3) var(--space-4);
}

.form-pill input:hover,
.form-pill input:focus { border: none; outline: none; }

.form-pill .btn {
  flex: 0 0 auto;
  border-radius: var(--radius-pill);
  padding: var(--space-3) var(--space-5);
}

.form-pill-success {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-3);
  margin: 0;
  border-radius: var(--radius-pill);
  color: var(--ink);
  font-weight: 800;
  font-size: var(--fs-small);
  text-transform: uppercase;
  letter-spacing: var(--tracking-wide);
  opacity: 0;
  transform: translateY(6px);
  pointer-events: none;
  transition: opacity var(--t-base) var(--ease),
              transform var(--t-base) var(--ease);
}

.form-pill.is-submitted { background: var(--mustard); border-color: var(--mustard); }

.form-pill.is-submitted .form-pill-fields {
  opacity: 0;
  transform: scale(0.96);
  pointer-events: none;
}

.form-pill.is-submitted .form-pill-success {
  opacity: 1;
  transform: translateY(0);
  transition-delay: 0.12s;
}

.form-tick { flex: 0 0 auto; }

/* Total path length is about 13.1 units, so 14 covers it with a margin */
.form-tick-path { stroke-dasharray: 14; stroke-dashoffset: 14; }

.form-pill.is-submitted .form-tick-path {
  animation: tick-draw var(--t-base) var(--ease) 0.2s both;
}

@keyframes tick-draw {
  from { stroke-dashoffset: 14; }
  to { stroke-dashoffset: 0; }
}

/* ---------------------------------------------------------------
   16. FAQ
   --------------------------------------------------------------- */

.faq { display: grid; gap: var(--space-3); }

/* No padding here — it moved onto summary and .faq p so the summary's own
   box is the full-width tap target. */
.faq details {
  background: var(--ink-2);
  border: var(--border-hairline);
  border-radius: var(--radius-md);
}

.faq details[open] { border-color: rgba(var(--paper-rgb), 0.3); }

/* Family, size and tracking come from the card-title rule in §3.
   The padding lives here rather than on .faq details so the whole tap
   target is the summary box itself — on details it sat outside the
   summary, leaving a ~22px-tall target on a phone. */
.faq summary {
  text-transform: uppercase;
  padding: var(--space-5) var(--space-6);
  cursor: pointer;
  list-style: none;
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--space-4);
}

.faq summary::-webkit-details-marker { display: none; }

.faq summary::after {
  content: '+';
  font-family: var(--font-body);
  color: var(--mustard);
  flex: 0 0 auto;
  transition: transform var(--t-base) var(--ease);
}

.faq details[open] summary::after { transform: rotate(45deg); }

/* Picks up the horizontal gutter that details gave up, and the bottom one.
   margin-top is 0 because summary's own padding already separates them. */
.faq p {
  margin: 0 var(--space-6) var(--space-5);
  color: var(--paper-muted);
  max-width: 68ch;
}

/* ---------------------------------------------------------------
   17. FOOTER
   --------------------------------------------------------------- */

.site-footer {
  background: var(--ink-2);
  padding: var(--space-8) 0 var(--space-6);
  font-size: var(--fs-small);
  border-top: var(--border-hairline);
}

/* Four columns now: brand, Services, Company, Contact. The service list
   went from one hub link to eight channel pages, and a flat single-line
   list does not hold at that count. */
.footer-grid {
  display: grid;
  grid-template-columns: 1.4fr 1fr 0.8fr 1.1fr;
  gap: var(--space-6);
}

/* ---- Footer newsletter capture ----
   The footer is where people land who have read the page and are not ready
   to book, so it offers the lower-commitment step. Reuses .form-pill and
   .js-waitlist with the live Beehiiv action, so initWaitlistForms picks it
   up with no JS change and it genuinely submits. */
.footer-signup {
  margin-top: var(--space-7);
  padding-top: var(--space-6);
  border-top: var(--border-hairline);
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.1fr);
  gap: var(--space-6);
  align-items: center;
}

/* A <p>, not a heading, for the same reason .footer-label is not one: this
   block repeats on every page and an h2 in the footer would sit in the
   outline below whatever the page's own h2s are. */
.footer-signup-title {
  margin: 0 0 var(--space-2);
  font-family: var(--font-body);
  font-size: var(--fs-card-title);
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: var(--tracking-wide);
  color: var(--paper);
}

.footer-signup .form-pill { margin-top: 0; }

.footer-signup .form-note { margin-top: var(--space-3); }

@media (max-width: 900px) {
  .footer-grid { grid-template-columns: 1fr 1fr; }
}

@media (max-width: 700px) {
  .footer-signup { grid-template-columns: minmax(0, 1fr); gap: var(--space-4); }
}

.site-footer .brand { --brand-scale: 1.15; }

.footer-tagline {
  margin-top: var(--space-4);
  color: var(--paper-muted);
  max-width: 320px;
}

/* Visual labels, not headings — keeping them out of the heading
   outline avoids a level skip on pages that stop at h2 */
.footer-label {
  font-family: var(--font-body);
  font-weight: 800;
  text-transform: uppercase;
  font-size: var(--fs-micro);
  letter-spacing: var(--tracking-wide);
  color: var(--mustard);
  margin: 0 0 var(--space-4) 0;
}

.footer-grid ul { list-style: none; padding: 0; margin: 0; }
.footer-grid li { margin-bottom: var(--space-2); color: var(--paper-muted); }
.footer-grid li a { transition: color var(--t-fast) var(--ease); }
.footer-grid li a:hover { color: var(--mustard); }

.footer-bottom {
  margin-top: var(--space-7);
  padding-top: var(--space-5);
  border-top: var(--border-hairline);
  color: var(--paper-faint);
  font-size: var(--fs-micro);
}

/* ---------------------------------------------------------------
   18. PHOTO HERO — full-viewport, homepage only

   NOTE: do not put aspect-ratio and max-height on .photo-frame
   together. When max-height caps the height, aspect-ratio shrinks
   the WIDTH to preserve the ratio and the box collapses to roughly
   half the viewport. Height is set directly here for that reason;
   object-fit does the cropping.
   --------------------------------------------------------------- */

.photo-hero-section {
  position: relative;
  padding: 0;
  background: var(--ink-2);
}

.photo-frame {
  position: relative;
  width: 100%;
  min-height: 100vh;  /* fallback for browsers without svh */
  min-height: 100svh; /* avoids extending behind mobile browser chrome */
  display: flex;
  align-items: flex-end;
  overflow: hidden;
}

/* The image is wrapped in <picture> for art direction, so the positioning
   lives on the wrapper — <picture> is display:inline by default and would
   otherwise collapse. Do not re-target these rules at `.photo-frame > img`;
   the img is a grandchild now and the rules would silently stop matching,
   dropping the absolute positioning and object-fit together. */
.photo-frame > picture {
  display: block;
  position: absolute;
  inset: 0;
}

.photo-frame picture > img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Holds Chris and the projector screen in frame on the landscape source */
  object-position: 55% 38%;
}

/* Optional video layer over the photo, injected by initHeroVideo() on wide
   viewports only. The photo underneath stays the base layer, the poster and
   the fallback, so this can fail to load or be skipped entirely with no
   visible gap.

   The source is 1600x598 (2.68:1), authored for the newsletter's wide band
   rather than a full-viewport hero: on a phone only 17% of its width would
   show, upscaled 2.8x, for 2.89MB. Hence wide viewports only. It fades in
   rather than popping, since the photo is already on screen. It must stay
   BELOW ::after so the whole scrim, and every contrast measurement behind
   it, still applies. */
.photo-frame > video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 45%;   /* holds Chris, who sits mid-frame */
  opacity: 0;
  transition: opacity var(--t-slow) var(--ease);
}

.photo-frame > video.is-ready { opacity: 1; }

/* Two-axis scrim: heavy along the bottom where the type sits, plus a
   left wash so the headline never competes with the curtain texture */
.photo-frame::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    linear-gradient(to top,
      rgba(var(--ink-rgb), 0.96) 0%,
      rgba(var(--ink-rgb), 0.86) 20%,
      rgba(var(--ink-rgb), 0.68) 42%,
      rgba(var(--ink-rgb), 0.38) 66%,
      rgba(var(--ink-rgb), 0.55) 100%),
    /* The headline measured 2.85:1 against the projector screen, under AA
       for large text. Most of the correction goes into this left wash
       rather than the vertical one: the type is left-aligned inside the
       wrap, so darkening leftwards buys contrast where the glyphs are
       without dimming the right of the frame, where the room reads. */
    linear-gradient(to right,
      rgba(var(--ink-rgb), 0.86) 0%,
      rgba(var(--ink-rgb), 0.42) 52%,
      rgba(var(--ink-rgb), 0) 100%);
}

/* padding-bottom is small because .hero-proof now occupies the band that
   used to be empty photograph. Raising it back to --space-9 without also
   removing the strip re-opens the gap it was added to fill. */
.photo-hero-content {
  position: relative;
  z-index: 1;
  width: 100%;
  padding-top: var(--space-10);
  padding-bottom: var(--space-5);
}

/* Mustard on a photograph cannot reach AA wherever the projector screen
   shows through — its luminance needs a near-black backdrop, and no scrim
   light enough to keep the room visible provides one. Measured at 1.90:1
   over the phone crop and 2.40:1 at a square window. So the eyebrow carries
   its own ground: contrast becomes a fixed property of the chip instead of
   a gamble on what the photo is doing behind it. At 0.9 alpha it measures
   7.0:1 or better at every breakpoint, against the brightest pixel the
   projector slide produces. Keep it a solid fill — a mustard border sits
   on top of this background and drags the ratio back down to ~3.1:1. */
.photo-hero-content .eyebrow {
  padding: var(--space-2) var(--space-4);
  border-radius: var(--radius-pill);
  background: rgba(var(--ink-rgb), 0.9);
}

.photo-hero-content h1 {
  margin-top: var(--space-5);
  font-size: var(--fs-display);
  max-width: 22ch;
}

.photo-hero-content .lead {
  margin-top: var(--space-5);
  color: var(--paper);
}


/* Homepage only: the nav sits over the photo so the hero can fill
   the viewport. fixed (not sticky) keeps it out of the layout flow,
   which avoids the jump a sticky-to-fixed swap would cause. */
body.photo-hero .site-header {
  position: fixed;
  left: 0;
  right: 0;
  border-bottom: none;
  /* Not fully transparent: the nav sits over the bright projector
     screen, where paper-on-white would fail. This guarantees
     legibility without reading as a solid bar. */
  background: linear-gradient(
    to bottom,
    rgba(var(--ink-rgb), 0.92) 0%,
    rgba(var(--ink-rgb), 0.72) 60%,
    rgba(var(--ink-rgb), 0) 100%
  );
}

body.photo-hero .site-header.is-scrolled {
  background: var(--ink-2);
  border-bottom: var(--border-hairline);
  box-shadow: var(--shadow-md);
}

/* Proof bar filling the hero's lower band, and lifting the headline block
   off the bottom edge in the process.

   This was a logo strip. The logos are the Trusted by wall's own content
   (§11), so running them here too put the same 13 marks on screen twice on
   one page. Figures occupy the same band, say something the copy above does
   not, and need no JS — the strip needed cloning to loop seamlessly. */

.hero-proof {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-5) var(--space-8);
  margin-top: var(--space-6);
  padding: 0;
  list-style: none;
}

.hero-stat { max-width: 22ch; }

/* Mustard is safe here where the eyebrow chip above needed its own opaque
   ground: the scrim's bottom stop is rgba(ink, 0.96), so this end of the
   frame is near-black whatever the photograph is doing behind it. */
.hero-stat-figure {
  display: block;
  font-family: var(--font-display);
  font-size: var(--fs-h3);
  line-height: 1.1;
  color: var(--mustard);
}

.hero-stat-label {
  display: block;
  margin-top: var(--space-2);
  font-size: var(--fs-micro);
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: var(--tracking-wide);
  color: var(--paper-muted);
}

/* ---------------------------------------------------------------
   19b. CURVED GALLERY — one row on a shallow concave curve
   --------------------------------------------------------------- */

/* Knobs, read by initGallery(). They live here so a breakpoint can retune
   the band in CSS without touching the script.

   --turn and --depth are what make the curve: a card's rotation runs
   linearly with how far it sits from the middle of the band, and its
   depth with the square of it, which is a parabola standing in for a
   shallow circular arc. Both reach their stated value at the band's edge.
   A card is FLAT — it is turned, never bent — which is the only reason a
   single radius can round all four of its corners. */
.gallery {
  --card-w: 300px;  /* a card's width; its height follows the 4:3 below */
  --gap: 14px;      /* space between cards */
  --turn: 42;       /* degrees a card is turned by at the band's edge */
  --depth: 130;     /* pixels a card comes forward by at the band's edge */
  --speed: 26;      /* pixels per second the row travels */
  --fade: 7;        /* width of each end fade, % of the band */
  position: relative;
  height: 330px;
  margin-top: var(--space-7);
  overflow: hidden;
  /* Horizontal drags belong to the row, vertical ones to the page. Without
     this a swipe down the page is swallowed by the card under the thumb. */
  touch-action: pan-y;
  cursor: grab;
  /* A drag across photographs would otherwise sweep a text selection along
     with it, and the browser would offer to drag the image itself. */
  user-select: none;
  -webkit-user-select: none;
}

.gallery img {
  -webkit-user-drag: none;
  user-drag: none;
}

.gallery.is-dragging { cursor: grabbing; }

/* Held back until the first frame is placed, so a row laid out at its
   unplaced default never gets on screen. */
.gallery-track {
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity var(--t-slow) var(--ease);
  /* The perspective belongs HERE, not on .gallery. It only ever applies to
     an element's direct children, and the cards are children of the track,
     so from one level up it reached nothing: every card stayed exactly its
     laid-out size however far forward it was pushed, and the curve read as
     a row of flat rectangles leaning slightly.

     Near enough that coming forward is worth a fifth in size and a turn
     foreshortens visibly; far enough that nothing balloons. */
  perspective: 900px;
  perspective-origin: 50% 50%;
}

.gallery.is-live .gallery-track { opacity: 1; }

.gallery-card {
  position: absolute;
  top: 50%;
  left: 0;
  width: var(--card-w);
  aspect-ratio: 4 / 3;
  border-radius: 16px;
  overflow: hidden;
  background: var(--ink-2);
  box-shadow: var(--shadow-lg);
  /* JS writes the transform every frame; nothing here may add one, or the
     two will fight and the card will jump. */
  will-change: transform;
}

/* height:auto is load-bearing. With width and height attributes present on
   the img, aspect-ratio is silently ignored without it. */
.gallery-card img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Softens the two edges so a card slides out of the band rather than being
   cut off against it. */
.gallery::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    linear-gradient(to right,
      var(--ink) 0%,
      rgba(var(--ink-rgb), 0) calc(var(--fade) * 1%),
      rgba(var(--ink-rgb), 0) calc(100% - var(--fade) * 1%),
      var(--ink) 100%);
}

/* ---------------------------------------------------------------
   19c. GRAIN CTA — animated closing band
   --------------------------------------------------------------- */

.grain-cta {
  position: relative;
  overflow: hidden;
  background: var(--ink);
  text-align: center;
}

/* The first radial-gradients in this stylesheet; everything else is linear.
   Blurred hard so the two blooms read as one drifting field rather than two
   discs, which is what keeps the grain on top looking like texture. */
.grain-cta::before {
  content: '';
  position: absolute;
  inset: -25%;
  background:
    radial-gradient(38% 42% at 28% 32%, var(--oxblood) 0%, transparent 68%),
    radial-gradient(32% 36% at 74% 68%, var(--mustard) 0%, transparent 68%),
    radial-gradient(30% 34% at 60% 20%, var(--cream) 0%, transparent 70%);
  filter: blur(70px);
  opacity: 0.85;
  animation: grain-drift 26s ease-in-out infinite alternate;
}

@keyframes grain-drift {
  from { transform: translate3d(-4%, -3%, 0) scale(1); }
  to { transform: translate3d(5%, 4%, 0) scale(1.12); }
}

/* --grain's first use as an overlay on a dark ground; the three paper
   surfaces multiply it instead */
.grain-cta::after {
  content: '';
  position: absolute;
  inset: 0;
  background-image: var(--grain);
  opacity: 0.55;
  mix-blend-mode: overlay;
  pointer-events: none;
}

.grain-cta .wrap { position: relative; z-index: 1; }

/* Paper on the mustard bloom is the contrast failure case, so the copy
   carries its own shadow rather than relying on where the drift happens
   to be when someone lands on the page */
.grain-cta h2,
.grain-cta .lead {
  color: var(--paper);
  text-shadow: 0 2px 18px rgba(var(--ink-rgb), 0.85);
}

.grain-cta .lead { max-width: 54ch; margin: var(--space-5) auto 0; }
.grain-cta .btn-row { justify-content: center; }

/* ---------------------------------------------------------------
   19d. CONSULT DIALOG

   Native <dialog>, so focus trapping, Escape and the backdrop come
   from the platform rather than being hand-rolled. Markup is built
   in JS — it is identical on every page, and eleven copies is how
   the nav drifted.
   --------------------------------------------------------------- */

/* The height chain here is load-bearing and easy to break.

   max-height + overflow:hidden alone does NOT give you a scrollable modal.
   The grid inside used to size its row to content (813px desktop, 1058px
   phone) no matter the viewport, so .consult-form's overflow-y:auto was
   inert — a grid item stretched to a content-sized row has
   scrollHeight === clientHeight, nothing to scroll — and overflow:hidden
   then blocked user scrolling entirely. Anything past the fold was gone:
   on a 1366x768 laptop that meant Chris's email and phone, and once the
   form is wired it would mean the submit button.

   So: flex column here, a row bounded to 1fr on the grid, and min-height:0
   at both levels because flex and grid items will not shrink below their
   content without it. */
.consult {
  position: relative;   /* anchors .consult-close, which sits outside the grid */
  display: flex;
  flex-direction: column;
  width: min(920px, calc(100vw - 2 * var(--space-5)));
  /* svh, not vh: with the iOS URL bar showing, vh measures the LARGE
     viewport, so the dialog is sized taller than what is actually visible
     and its bottom — the submit button, or Chris's email and phone in the
     un-wired state — sits under the browser chrome. Same pairing as
     .photo-frame and .newsletter-hero. vh first as the fallback. */
  max-height: calc(100vh - 2 * var(--space-6));
  max-height: calc(100svh - 2 * var(--space-6));
  padding: 0;
  border: var(--border-hairline);
  border-radius: var(--radius-lg);
  background: var(--ink-2);
  color: var(--paper);
  box-shadow: var(--shadow-lg);
  overflow: hidden; /* keeps the gradient panel inside the rounded corners */
}

.consult::backdrop {
  background: rgba(var(--ink-rgb), 0.78);
  backdrop-filter: blur(3px);
}

.consult-grid {
  display: grid;
  grid-template-columns: 1.1fr 0.9fr;
  /* minmax(0, 1fr), not the default auto: an auto row is max-content, so the
     items would stretch past the dialog again even with the grid bounded. */
  grid-template-rows: minmax(0, 1fr);
  flex: 1;
  min-height: 0;
}

.consult-form {
  position: relative;
  padding: var(--space-7) var(--space-6);
  overflow-y: auto;
  min-height: 0;                /* without this the grid item never shrinks */
  overscroll-behavior: contain;  /* don't hand the scroll to the page behind */
}

.consult-form h2 { margin: var(--space-3) 0 0; font-size: var(--fs-h2); }

.consult-sub {
  margin: var(--space-3) 0 0;
  font-size: var(--fs-small);
  color: var(--paper-muted);
}

/* Anchored to .consult, not .consult-form. On a phone .consult-visual takes
   order:-1 and leads the stack, so a form-anchored X started several hundred
   pixels down the scroll container — off-screen at the moment the dialog
   opened, when it is the only control the user is looking for.

   The trade is that it now sits over .consult-visual's gradient rather than
   inside the form, so it carries its own ground to stay legible against the
   mustard bloom, and .consult-visual-copy reserves room for it below. */
.consult-close {
  position: absolute;
  top: var(--space-4);
  right: var(--space-4);
  z-index: 2;
  width: 44px;
  height: 44px;
  background: rgba(var(--ink-rgb), 0.55);
  backdrop-filter: blur(2px);
  display: flex;
  align-items: center;
  justify-content: center;
  border: var(--border-hairline);
  border-radius: var(--radius-pill);
  color: var(--paper);
  font-size: 1.4rem;
  line-height: 1;
  cursor: pointer;
  transition: color var(--t-fast) var(--ease),
              border-color var(--t-fast) var(--ease);
}

.consult-close:hover { color: var(--paper); border-color: var(--paper); }

/* A bare email address has no break opportunity in it and is wider than
   the dialog's content box on a phone. anywhere, not break-word: break-word
   only breaks a word that would overflow on its own line, which leaves the
   address to push the box wider first. */
.form-pill-success,
.footer-grid li a { overflow-wrap: anywhere; }

/* Same drifting-bloom treatment as the closing CTA band, so the two read
   as one system rather than two takes on the same idea */
.consult-visual {
  position: relative;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  padding: var(--space-7) var(--space-6);
  background: var(--ink);
}

.consult-visual::before {
  content: '';
  position: absolute;
  inset: -30%;
  background:
    radial-gradient(40% 44% at 30% 28%, var(--oxblood) 0%, transparent 68%),
    radial-gradient(34% 38% at 72% 72%, var(--mustard) 0%, transparent 68%);
  filter: blur(60px);
  opacity: 0.9;
  animation: grain-drift 26s ease-in-out infinite alternate;
}

.consult-visual::after {
  content: '';
  position: absolute;
  inset: 0;
  background-image: var(--grain);
  opacity: 0.55;
  mix-blend-mode: overlay;
  pointer-events: none;
}

/* The blurred oxblood/mustard gradient paints on ::before behind these, so
   they need both the stacking context and the shadow to stay readable as it
   drifts. */
.consult-visual-copy,
.consult-visual-points,
.consult-visual-meta {
  position: relative;
  z-index: 1;
  text-shadow: 0 2px 18px rgba(var(--ink-rgb), 0.85);
}

/* Clears the close button, which is anchored to the dialog corner and so
   overlaps this panel's first line. 44px control + its 16px offset. */
.consult-visual-copy { padding-right: 60px; }

/* Fills the middle of the panel. Without it .consult-visual's
   space-between left a large empty band between headline and meta line. */
.consult-visual-points {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  max-width: 30ch;
  font-size: var(--fs-small);
  color: var(--paper);
}

.consult-visual-points li {
  padding-left: var(--space-5);
  position: relative;
}

/* Drawn, not typed. This was a literal em-dash in content:, which rendered
   on every page and was the site's only remaining breach of the no-em-dash
   rule. Same mustard bar .showcase-points uses, so the two lists match. */
.consult-visual-points li::before {
  content: '';
  position: absolute;
  left: 0;
  top: 0.62em;
  width: 14px;
  height: 2px;
  background: var(--mustard);
}

.consult-visual-copy {
  margin: 0;
  font-family: var(--font-display);
  text-transform: uppercase;
  font-size: var(--fs-h2);
  line-height: 1.1;
  color: var(--paper);
}

.consult-visual-copy .script {
  font-family: var(--font-script);
  text-transform: none;
  color: var(--cream);
}

.consult-visual-meta {
  margin: var(--space-5) 0 0;
  font-size: var(--fs-micro);
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: var(--tracking-wide);
  color: var(--paper-muted);
}

/* ---------------------------------------------------------------
   20. WAYS TO WORK — notched cards

   The punched-out corner is a badge ringed by a thick box-shadow in
   the SECTION's background colour. --notch-bg must therefore match
   whatever surface the section sits on, or the illusion collapses.
   Done this way rather than with clip-path or mask compositing,
   which is where inverted radii usually break.
   --------------------------------------------------------------- */

.ways {
  --notch-bg: var(--ink);
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--space-5);
}

.way {
  position: relative;
  display: flex;
  flex-direction: column;
  /* room at the bottom-right so the badge never sits on the text */
  padding: var(--space-6) var(--space-5) var(--space-8);
  border-radius: var(--radius-lg);
  overflow: hidden;
  isolation: isolate;
  transition: transform var(--t-base) var(--ease),
              box-shadow var(--t-base) var(--ease);
}

/* Slow sheen sweeping the surface on hover */
.way::before {
  content: '';
  position: absolute;
  inset: -40% -60%;
  z-index: -1;
  background: linear-gradient(
    75deg,
    transparent 38%,
    rgba(var(--paper-rgb), 0.16) 50%,
    transparent 62%
  );
  transform: translateX(-70%);
  opacity: 0;
  transition: transform 0.9s var(--ease), opacity var(--t-base) var(--ease);
}

.way:hover, .way:focus-visible {
  transform: translateY(-6px);
  box-shadow: var(--shadow-lg);
}

.way:hover::before, .way:focus-visible::before {
  transform: translateX(70%);
  opacity: 1;
}

/* Surfaces — one brand colour each, so the row has rhythm and no
   single surface pairs mustard with oxblood */
.way-oxblood { background: var(--oxblood); color: var(--paper); }
/* ink-2 sits only one step off the page background, so this one needs
   a real outline to read as a card at all — an intentional fourth
   treatment rather than a washed-out version of the others */
.way-ink2 {
  background: var(--ink-2);
  color: var(--paper);
  border: 2px solid rgba(var(--paper-rgb), 0.28);
}
.way-mustard { background: var(--mustard); color: var(--ink); }
.way-paper   { background: var(--paper); color: var(--ink); background-image: var(--grain); background-blend-mode: multiply; }

.way-mustard h3, .way-paper h3 { color: var(--ink); }
.way-oxblood h3, .way-ink2 h3 { color: var(--paper); }

/* Inherits each surface's text colour through currentColor on the stroke,
   which is the whole reason these icons are inline SVG rather than files
   under img/ — an <img>-loaded SVG is a separate document and would need
   four hardcoded colour variants, one per .way surface. */
.way-icon {
  width: 40px;
  height: 40px;
  margin-bottom: var(--space-4);
  color: inherit;
}

.way h3 { margin-bottom: var(--space-3); }

.way p {
  font-size: var(--fs-small);
  margin-bottom: 0;
}

.way-eyebrow {
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: var(--tracking-wider);
  font-size: var(--fs-micro);
  margin-bottom: var(--space-3);
  opacity: 0.8;
}

/* The CTA sits at the card foot regardless of copy length */
.way-cta {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  margin-top: auto;
  padding-top: var(--space-5);
  font-weight: 700;
  font-size: var(--fs-small);
  text-transform: uppercase;
  letter-spacing: var(--tracking-wide);
}

.way-cta::after {
  content: '→';
  transition: transform var(--t-base) var(--ease);
}

.way:hover .way-cta::after,
.way:focus-visible .way-cta::after { transform: translateX(5px); }

/* The numbered disc, echoing the CL logo mark */
.way-badge {
  position: absolute;
  right: calc(-1 * var(--space-2));
  bottom: calc(-1 * var(--space-2));
  width: 56px;
  height: 56px;
  border-radius: var(--radius-pill);
  background: var(--oxblood);
  color: var(--paper);
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-display);
  font-size: 1rem;
  /* This ring is what reads as the corner being punched out */
  box-shadow: 0 0 0 10px var(--notch-bg);
  transition: transform var(--t-base) var(--ease);
}

/* On the oxblood card the disc would vanish — flip it to ink */
.way-oxblood .way-badge { background: var(--ink); color: var(--paper); }

.way:hover .way-badge,
.way:focus-visible .way-badge { transform: scale(1.1) rotate(-8deg); }

/* Staggered entrance — CSS only, riding the existing .reveal observer */
.reveal .way { transition-delay: 0ms; }
.reveal.is-visible .way:nth-child(1) { transition-delay: 0ms; }
.reveal.is-visible .way:nth-child(2) { transition-delay: 80ms; }
.reveal.is-visible .way:nth-child(3) { transition-delay: 160ms; }
.reveal.is-visible .way:nth-child(4) { transition-delay: 240ms; }

/* ---------------------------------------------------------------
   21. SCROLL REVEAL
   --------------------------------------------------------------- */

.reveal {
  opacity: 0;
  transform: translateY(18px);
  transition: opacity var(--t-slow) var(--ease),
              transform var(--t-slow) var(--ease);
}

.reveal.is-visible {
  opacity: 1;
  transform: none;
}

/* ---------------------------------------------------------------
   22. RESPONSIVE
   --------------------------------------------------------------- */

@media (max-width: 900px) {
  .grid-5, .grid-4, .grid-3 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .founder { grid-template-columns: 200px 1fr; gap: var(--space-6); }
  .ways { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (max-width: 800px) {
  /* inline-flex, not block — the base rule centres the glyph in its 44px
     box with align-items/justify-content, which need a flex container */
  .nav-toggle { display: inline-flex; }

  .nav-links {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    background: var(--ink-2);
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    max-height: 0;
    overflow: hidden;
    box-shadow: var(--shadow-md);
    /* visibility, not just max-height: a 0-height overflow:hidden drawer
       still leaves its links in the tab order, so a keyboard user could
       tab into an invisible menu. Delayed so it does not cut the collapse
       animation short. */
    visibility: hidden;
    transition: max-height var(--t-base) var(--ease),
                visibility 0s linear var(--t-base);
  }

  /* 85vh with its own scroll, not 60vh: the drawer now nests a six-item
     submenu and would otherwise clip it against overflow: hidden.

     svh follows vh for the reason given on .consult: with the URL bar
     showing, 85vh measures the large viewport, so a fully expanded drawer
     runs past the visible bottom edge. overscroll-behavior stops the scroll
     handing off to the page once the drawer hits either end, which
     otherwise moves the page underneath the open menu. */
  .nav-links.open {
    max-height: 85vh;
    max-height: 85svh;
    overflow-y: auto;
    overscroll-behavior: contain;
    visibility: visible;
    transition-delay: 0s;
  }

  /* Inline disclosure rather than a floating panel on small screens */
  .nav-indicator { display: none; }

  .nav-sub { display: flex; flex-direction: column; }

  .nav-sub-trigger {
    width: 100%;
    justify-content: space-between;
    padding: var(--space-4) var(--space-5);
    border-top: 1px solid rgba(var(--paper-rgb), 0.08);
  }

  .nav-panel {
    position: static;
    width: auto;
    padding: 0;
    border: none;
    border-radius: 0;
    box-shadow: none;
    background: rgba(var(--ink-rgb), 0.45);
    transform: none;
    display: none;
  }

  .nav-panel.open { transform: none; display: block; }

  /* Back to one column inside the drawer, which is already narrow and
     already scrolls. Two columns here would halve the measure for no gain. */
  .nav-panel-cols { flex-direction: column; gap: 0; }

  .nav-panel-group { padding: var(--space-3) var(--space-5) var(--space-1); }

  .nav-panel::before { display: none; }

  .nav-panel a { padding: var(--space-3) var(--space-5); border-radius: 0; }

  .nav-panel-overview { margin: 0; border-bottom: none; }

  .nav-links a {
    padding: var(--space-4) var(--space-5);
    width: 100%;
    border-top: 1px solid rgba(var(--paper-rgb), 0.08);
  }

  .nav-links a[aria-current="page"],
  .nav-links .nav-sub-trigger[aria-current] { box-shadow: inset 3px 0 0 var(--mustard); }

  .nav-links a.nav-cta {
    margin: var(--space-4) var(--space-5) var(--space-5);
    width: auto;
    justify-content: center;
    border-top: none;
  }

  /* The footer list is the site's secondary nav, and once .footer-grid goes
     single column it is the main one on a phone. As inline links they were
     16px tall on a ~30px pitch — two adjacent targets inside one thumb.
     Scoped to this query rather than applied globally so the desktop footer
     does not grow by 24px per row for no benefit. */
  .footer-grid li { margin-bottom: 0; }

  /* block, not inline-block: "Blog" is a 29px-wide word, and a target
     sized to its text is a target you have to aim at. The row is the
     target instead, which is the ordinary phone-nav convention. */
  .footer-grid li a {
    display: block;
    padding: var(--space-3) 0;
  }
}

@media (max-width: 700px) {
  section { padding: var(--space-8) 0; }
  .hero { padding: var(--space-8) 0; }
  .stat-panel { grid-template-columns: 1fr; }
  .founder { grid-template-columns: 1fr; }
  .founder-photo { max-width: 220px; }
  .footer-grid { grid-template-columns: 1fr; }
  .lead-panel { padding: var(--space-6) var(--space-5); }
  .hero-meta { gap: var(--space-2); }

  /* Tighter vertical padding so the headline, lead, both CTAs and the
     proof bar still fit inside one viewport at phone height */
  .photo-hero-content {
    padding-top: var(--space-9);
    padding-bottom: var(--space-5);
  }

  /* The portrait source is already framed on Chris, so this centres
     rather than compensating. At a phone's aspect the 9:16 source
     crops only from the sides, so the Y value matters solely at the
     600px edge of this query, where it keeps the slide in view. */
  .photo-frame picture > img { object-position: 50% 45%; }

  /* The copy block covers roughly the lower three-quarters of a phone
     viewport, and the projector screen is bright, so some scrim is
     unavoidable — cream on white fails outright. But a flat 0.94 wash
     erases the photograph. Graded instead: opaque where the type sits,
     opening up towards the top so the room is actually visible. These
     stops were set by measuring rendered luminance behind the eyebrow,
     headline and lead — not by eye. Lightening the upper stops further
     starts failing the header, which composites its own gradient on top
     of this one. */
  .photo-frame::after {
    background: linear-gradient(to top,
      rgba(var(--ink-rgb), 0.95) 0%,
      rgba(var(--ink-rgb), 0.92) 42%,
      rgba(var(--ink-rgb), 0.86) 66%,
      rgba(var(--ink-rgb), 0.62) 84%,
      rgba(var(--ink-rgb), 0.30) 100%);
  }


  .hero-proof { margin-top: var(--space-5); gap: var(--space-4) var(--space-6); }

  /* Wider cards on a narrower band means fewer of them reach the frame,
     which is the point: three small photographs read as a filmstrip, two
     larger ones read as photographs. The curve eases off with the band,
     because the same turn over a shorter span foreshortens much harder. */
  .gallery {
    --card-w: 260px;
    --gap: 12px;
    --turn: 30;
    --depth: 90;
    height: 290px;
  }

  /* Side by side leaves neither panel usable at this width */
  .consult-grid {
    grid-template-columns: 1fr;
    /* Stacked, so the whole thing scrolls as one column rather than the
       form scrolling inside a fixed panel — on a phone, scrolling the
       intro away is the expected behaviour. Rows go back to auto: the
       base minmax(0,1fr) would squeeze both into a single track. */
    grid-template-rows: auto auto;
    overflow-y: auto;
    overscroll-behavior: contain;
  }
  .consult-visual { order: -1; padding: var(--space-6); }
  .consult-visual-copy { font-size: var(--fs-h3); }
  .consult-form {
    padding: var(--space-6) var(--space-5);
    overflow-y: visible; /* the grid is the scroll container here */
  }
}

@media (max-width: 600px) {
  .grid-5, .grid-4, .grid-3, .grid-2 { grid-template-columns: 1fr; }
  .ways { grid-template-columns: 1fr; }
}

/* ---- Small phones ----
   Everything above was written against 600px as the floor, so a 360px
   viewport used to get rules identical to a 599px one. This is the tier
   where the assumptions actually stop holding. */
@media (max-width: 480px) {
  /* 16px gutters buy back 16px of measure, which several rules below
     and the headline clamps all need at 320px */
  .wrap { padding: 0 var(--space-4); }

  /* The pill stacks. Side by side it could not work at this width: the
     button is flex:0 0 auto so its max-content width is the floor of the
     whole control, which left the email field 32px wide on the newsletter
     page — measured, not estimated — and pushed the pill past the
     viewport. Stacked, the button gets a full line and the field gets the
     rest, so both stay usable. */
  .form-pill {
    border-radius: var(--radius-md);
    padding: var(--space-3);
  }

  .form-pill-fields {
    flex-direction: column;
    align-items: stretch;
    gap: var(--space-3);
  }

  .form-pill input {
    padding: var(--space-3);
    text-align: center;
  }

  .form-pill .btn {
    justify-content: center;
    padding: var(--space-3) var(--space-4);
  }

  /* Shrink-wrapped on wider screens because it holds only a button; at this
     width it should span the column like every other stacked control */
  .form-pill-compact { display: flex; }

  /* Gentler again, and down to about two cards across. */
  .gallery {
    --card-w: 230px;
    --gap: 10px;
    --turn: 22;
    --depth: 66;
    height: 250px;
  }

  /* Cards carry the same padding at 320px as at 1440px, which spends 48px
     of a 288px measure on air */
  .card { padding: var(--space-5) var(--space-4); }
  .post-body { padding: var(--space-5); }
  .faq summary { padding: var(--space-4) var(--space-5); }
  .faq p { margin: 0 var(--space-5) var(--space-4); }
  .channel-note { padding: var(--space-6) var(--space-5); }
}

/* ---------------------------------------------------------------
   23. MOTION PREFERENCES
   --------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }

  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }

  .reveal { opacity: 1; transform: none; }
  .btn:hover, a.card:hover, a.post-card:hover { transform: none; }

  /* The blanket rule above zeroes transition-duration but NOT
     transition-delay, so each of these would still lag. The states must
     stay reachable — only the travel is removed. */
  .nav-indicator { transition: none; }
  .nav-panel { transition: none; }
  .nav-sub-trigger svg { transition: none; }

  .form-pill.is-submitted .form-pill-success { transition-delay: 0s; transform: none; }
  .form-pill.is-submitted .form-tick-path { animation: none; stroke-dashoffset: 0; }
  .form-pill.is-submitted .form-pill-fields { transform: none; }

  .grain-cta::before { animation: none; }

  /* Static wrapping wall — JS also skips cloning under reduced
     motion, but belt and braces in case the class is already set */
  .marquee { mask-image: none; -webkit-mask-image: none; }
  .marquee-track.is-animating {
    animation: none;
    flex-wrap: wrap;
    width: 100%;
    justify-content: center;
  }
  .logo-tile:hover, .logo-tile:focus-visible { transform: none; }


  /* Notched cards: keep the surfaces and badge, drop the movement */
  .way:hover, .way:focus-visible { transform: none; }
  .way::before { display: none; }
  .way:hover .way-badge, .way:focus-visible .way-badge { transform: none; }
  .way:hover .way-cta::after { transform: none; }
  .reveal.is-visible .way:nth-child(1),
  .reveal.is-visible .way:nth-child(2),
  .reveal.is-visible .way:nth-child(3),
  .reveal.is-visible .way:nth-child(4) { transition-delay: 0ms; }

  /* Newsletter cards: land them in place, and zero the stagger — the
     blanket transition-duration override above does not touch
     transition-delay, so card 03 would otherwise pop in 180ms late */
  .newsletter-page .reveal .card { opacity: 1; transform: none; }
  .newsletter-page .reveal.is-visible .card:nth-child(1),
  .newsletter-page .reveal.is-visible .card:nth-child(2),
  .newsletter-page .reveal.is-visible .card:nth-child(3) { transition-delay: 0ms; }
}

/* ---------------------------------------------------------------
   24. PRINT
   --------------------------------------------------------------- */

@media print {
  .site-header, .nav-toggle, .brand-strip, .skip-link { display: none; }
  /* The table of contents is navigation for a scrolling page, and the rule
     below would print "(#what-it-costs)" beside every entry. The reading
     progress bar needs no rule of its own: it lives inside .site-header,
     which is already hidden. */
  .article-toc { display: none; }
  /* A 7.5in page is about 720px, wide enough that the 900px query never
     fires, so without these the split hero prints its grid: a full-colour
     room photograph beside a headline squeezed into 57% of the page. The
     oxblood band is a background and browsers strip those by default, but a
     foreground <img> prints whatever the user's setting. */
  .hero-plate { display: none; }
  .hero-split .wrap { display: block; }
  body { background: #fff; color: #000; }
  section { padding: 12pt 0; }
  a::after { content: ' (' attr(href) ')'; font-size: 9pt; }
}

/* ---------------------------------------------------------------
   25. NEWSLETTER PAGE
   --------------------------------------------------------------- */

.newsletter-hero { padding: var(--space-8) 0 var(--space-8); }

.newsletter-hero .wrap {
  display: grid;
  /* minmax(0, …) so the text track can shrink below its widest word. A bare
     1fr is minmax(auto, 1fr) and takes its floor from min-content, which is
     how one long headline word pushed this whole row past the viewport. */
  grid-template-columns: 320px minmax(0, 1fr);
  gap: var(--space-8);
  /* Top-aligned rather than centered: the text column is much taller
     than the 320px photo, so centering left a large empty gap above it */
  align-items: start;
}

/* Alfa Slab One's tall, blocky glyphs need more room between lines
   than the sitewide 1.12 once a headline wraps to four lines */
.newsletter-hero h1 { line-height: 1.3; }

/* ---- Page-load entrance animation ----
   Reduced motion is already handled by the sitewide
   `@media (prefers-reduced-motion: reduce) { *, *::before, *::after {
   animation-duration: 0.01ms !important } }` rule further down this
   file, so no extra override is needed here. */

@keyframes hero-in {
  from { opacity: 0; transform: translateY(16px); }
  to   { opacity: 1; transform: translateY(0); }
}

.load-in { animation: hero-in 0.7s var(--ease) both; }

.load-in-1 { animation-delay: 0ms; }
.load-in-2 { animation-delay: 90ms; }
.load-in-3 { animation-delay: 180ms; }
.load-in-4 { animation-delay: 280ms; }
.load-in-5 { animation-delay: 380ms; }

@media (max-width: 700px) {
  /* minmax(0,1fr), not 1fr: a bare 1fr track is minmax(auto, 1fr) and cannot
     shrink below its widest child's min-content, so one long word held the
     whole hero at 481px on every phone regardless of viewport. */
  .newsletter-hero .wrap { grid-template-columns: minmax(0, 1fr); }
  .newsletter-hero .founder-photo { max-width: 260px; margin: 0 auto; }
}

/* Mustard accent under this page's section headings — signature-accent
   use per the brand snapshot, scoped here rather than on the shared
   .section-head so the other four pages are untouched */
.newsletter-page .section-head h2 {
  display: inline-block;
  padding-bottom: var(--space-3);
  border-bottom: 4px solid var(--mustard);
}

.card-num { color: var(--oxblood); }

/* The old display:none swap between form and message is gone — the pill
   morphs in place instead. See .form-pill in section 15. */

.newsletter-footer { padding: var(--space-7) 0; text-align: center; }

.newsletter-footer p {
  margin: 0 0 var(--space-2) 0;
  color: var(--paper-muted);
  font-size: var(--fs-small);
}

.newsletter-footer p:last-child { margin-bottom: 0; }

/* The site URL is a paragraph of its own, so it is a standalone target
   rather than an inline one and needs a tappable box. overflow-wrap
   because it is a 36-character unbreakable string. */
.newsletter-footer p > a:only-child {
  display: inline-block;
  padding: var(--space-3) 0;
  overflow-wrap: anywhere;
}

/* Pulled out of card 03 and given its own weight — feedback flagged this
   line as the strongest copy on the page and asked that it not stay
   buried inside a card */
.section-callout {
  margin: var(--space-7) auto 0;
  max-width: 46ch;
  text-align: center;
  font-family: var(--font-display);
  text-transform: uppercase;
  font-size: var(--fs-h3);
  line-height: 1.4;
  color: var(--mustard);
}

/* ---- Staggered card entrance ----
   The section itself already fades in via .reveal; these give each card
   a starting offset of its own so the row arrives in sequence instead of
   as one block. Scoped to this page so the .card grids on index,
   how-we-help, blog and ndis-marketing are untouched.

   Overriding .card's base transition costs nothing here — these cards are
   plain divs, and only a.card gets a hover affordance. */
.newsletter-page .reveal .card {
  opacity: 0;
  transform: translateY(24px);
  transition: opacity var(--t-slow) var(--ease),
              transform var(--t-slow) var(--ease);
}

.newsletter-page .reveal.is-visible .card {
  opacity: 1;
  transform: none;
}

/* 90ms steps, matching this page's .load-in cadence */
.newsletter-page .reveal.is-visible .card:nth-child(1) { transition-delay: 0ms; }
.newsletter-page .reveal.is-visible .card:nth-child(2) { transition-delay: 90ms; }
.newsletter-page .reveal.is-visible .card:nth-child(3) { transition-delay: 180ms; }

/* ---- Video hero variant ----
   Modelled on the homepage .photo-frame: media absolutely positioned and
   cover-fitted, a scrim over it, content layered above. min-height rather
   than height so a cramped mobile viewport can grow to fit the form
   instead of clipping it. */
.newsletter-hero.has-video {
  position: relative;
  overflow: hidden;
  display: flex;
  align-items: center;
  min-height: 65vh;
  min-height: 65svh; /* keeps clear of mobile browser chrome */
}

.newsletter-hero .hero-video {
  position: absolute;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* The montage is encoded at 2.68:1, near enough to this band's ratio
     that cover-fitting crops only a few pixels on a desktop viewport —
     so centre is correct and no vertical bias is wanted. It earns its
     keep on narrow viewports, where the band turns portrait and the crop
     goes horizontal: centre keeps the middle panel of the split-screen
     shots in view rather than slicing between two of them. */
  object-position: center;
  pointer-events: none;
}

/* Two-axis scrim, weighted left — the headline and form are left-aligned
   inside the wrap, so contrast is bought where the glyphs are rather than
   by dimming the whole frame. Same approach as .photo-frame::after. */
.newsletter-hero.has-video::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  /* The two layers composite, so these read lighter than they look.
     The vertical one is deliberately monotonic — darkest at the bottom,
     lifting toward the top — because the small type stacks downward: the
     lead, the form and its note need more cover than the display-size
     headline, which only has to clear AA for large text. A gradient that
     went light in the middle would starve the lead of contrast exactly
     where it sits.

     Measured against the brightest thing this footage contains (the white
     wall): composite ~0.77 behind the headline gives 3.6:1 (passes large
     text), ~0.81 behind the lead gives 4.3:1, and ~0.86 at the form note
     gives 5:1. Falls to ~0.54 at the right edge so the room still reads
     as footage rather than texture.

     This is the dial to turn if the footage changes brightness — recut
     material may need the stops moved. */
  background:
    linear-gradient(to right,
      rgba(var(--ink-rgb), 0.72) 0%,
      rgba(var(--ink-rgb), 0.66) 45%,
      rgba(var(--ink-rgb), 0.44) 78%,
      rgba(var(--ink-rgb), 0.20) 100%),
    linear-gradient(to top,
      rgba(var(--ink-rgb), 0.62) 0%,
      rgba(var(--ink-rgb), 0.42) 50%,
      rgba(var(--ink-rgb), 0.18) 100%);
}

/* .wrap is a flex item now, so it needs its width back for the
   max-width + auto margins to keep centring the grid */
.newsletter-hero.has-video .wrap {
  position: relative;
  z-index: 2;
  width: 100%;
}

/* Must sit after the base ::after rule above, not up in the earlier
   700px block — media queries add no specificity, so the later of two
   identical selectors wins and the base wash would override this. */
@media (max-width: 700px) {
  /* Stacked, the type spans the full width, so a left-weighted wash
     leaves the right end of every line unprotected. Flatten it. */
  .newsletter-hero.has-video::after {
    background:
      linear-gradient(to top,
        rgba(var(--ink-rgb), 0.88) 0%,
        rgba(var(--ink-rgb), 0.76) 50%,
        rgba(var(--ink-rgb), 0.82) 100%);
  }
}

/* ---------------------------------------------------------------
   26. NEWSLETTER NUDGE

   Timed slide-in, injected by initNewsletterNudge() ten seconds after
   load. Deliberately NOT a dialog: it arrives unprompted, so trapping or
   stealing focus would interrupt whatever the visitor was reading or
   typing. An aside with a close button, nothing more.

   z-index 70 puts it above the header (50) and nav panel (60) and below
   the skip link (100), so it can never cover the first thing a keyboard
   user reaches.
   --------------------------------------------------------------- */

.nudge {
  position: fixed;
  right: var(--space-5);
  bottom: var(--space-5);
  z-index: 70;
  width: min(340px, calc(100vw - 2 * var(--space-5)));
  padding: var(--space-6) var(--space-5) var(--space-5);
  border-radius: var(--radius-lg);
  background: var(--ink-2);
  border: 1px solid rgba(var(--paper-rgb), 0.14);
  box-shadow: var(--shadow-lg);
  overflow: hidden;
  isolation: isolate;
  transform: translateX(calc(100% + var(--space-7)));
  opacity: 0;
  transition: transform var(--t-slow) var(--ease),
              opacity var(--t-slow) var(--ease);
}

.nudge.is-open { transform: translateX(0); opacity: 1; }

/* Same drifting bloom as .grain-cta, reused rather than reinvented so the
   two read as one system. Sits behind the content via z-index, not order. */
.nudge::before {
  content: '';
  position: absolute;
  inset: -40%;
  z-index: -2;
  background:
    radial-gradient(40% 44% at 26% 28%, var(--oxblood) 0%, transparent 66%),
    radial-gradient(34% 38% at 76% 74%, var(--mustard) 0%, transparent 66%);
  filter: blur(58px);
  opacity: 0.7;
  animation: grain-drift 26s ease-in-out infinite alternate;
}

.nudge::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  background-image: var(--grain);
  opacity: 0.4;
  mix-blend-mode: overlay;
  pointer-events: none;
}

.nudge-eyebrow {
  margin: 0 0 var(--space-2);
  font-size: var(--fs-micro);
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: var(--tracking-wider);
  color: var(--mustard);
}

/* Family, size and tracking come from the card-title rule in §3 */
.nudge-title {
  margin: 0 0 var(--space-3);
  text-transform: uppercase;
  color: var(--paper);
}

.nudge-body {
  margin: 0 0 var(--space-5);
  font-size: var(--fs-small);
  color: var(--paper-muted);
}

.nudge-cta {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  background: var(--mustard);
  color: var(--ink);
  font-weight: 700;
  font-size: var(--fs-small);
  text-transform: uppercase;
  letter-spacing: var(--tracking-wide);
  padding: var(--space-3) var(--space-5);
  border-radius: var(--radius-sm);
  transition: transform var(--t-fast) var(--ease);
}

.nudge-cta:hover { transform: translateY(-2px); }

/* 44px. It was 30px — the smallest target on the site, and on a phone it is
   the only way to dismiss the bar: there is no swipe and no tap-outside,
   and Escape is not a phone gesture. */
.nudge-close {
  position: absolute;
  top: var(--space-2);
  right: var(--space-2);
  width: 44px;
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: var(--border-hairline);
  border-radius: var(--radius-pill);
  background: rgba(var(--ink-rgb), 0.5);
  color: var(--paper-muted);
  font-size: 1.2rem;
  line-height: 1;
  cursor: pointer;
  transition: color var(--t-fast) var(--ease),
              border-color var(--t-fast) var(--ease);
}

.nudge-close:hover { color: var(--paper); border-color: var(--paper); }

/* A card pinned to one corner reads as an interruption on a phone; full
   width along the bottom reads as a bar, and slides from the nearest edge */
@media (max-width: 700px) {
  .nudge {
    left: var(--space-4);
    right: var(--space-4);
    /* Clear of the iPhone home-indicator strip. At a flat 16px the CTA sat
       inside the system's swipe-up zone, so a tap near it was ambiguous
       between pressing the button and going home. env() resolves to 0 on
       every device without an inset, so the fallback needs no query. */
    bottom: calc(var(--space-4) + env(safe-area-inset-bottom, 0px));
    width: auto;
    transform: translateY(calc(100% + var(--space-6)));
  }
  .nudge.is-open { transform: translateY(0); }

  /* The drawer wins while it is open. The nudge outranks the header (70 vs
     50) so it can float over an expanded drawer's lower links — including
     the "Get In Touch" CTA the drawer puts last. Nothing dismisses the
     nudge to get at them, so it has to yield. */
  body:has(.nav-links.open) .nudge {
    opacity: 0;
    pointer-events: none;
  }
}

/* The content is useful; only the movement is the problem. So it appears
   in place rather than being suppressed. */
@media (prefers-reduced-motion: reduce) {
  .nudge {
    transform: none;
    transition: opacity var(--t-base) linear;
  }
  .nudge.is-open { transform: none; }
  .nudge::before { animation: none; }
}

/* ---------------------------------------------------------------
   27. SERVICE PAGE LAYOUT

   Built for the channel pages, which run long (1,200 words plus) and
   would read as an undifferentiated column without a rhythm to them.
   Two devices only: an alternating copy/photo row, and a timeline.
   Everything else on those pages reuses the existing card, grid, faq
   and section-colour components.
   --------------------------------------------------------------- */

.channel-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-7);
  align-items: center;
  margin-top: var(--space-8);
}

/* Alternate which side the photo sits on, so a run of these reads as a
   rhythm rather than a list. Set explicitly per row rather than with
   :nth-of-type(even): that counts EVERY sibling div, so a .section-head
   above the rows shifts the whole alternation by one and the numbering
   ends up reading 01, 02, 04, 03. */
.channel-row.is-flipped .channel-media { order: -1; }

.channel-num {
  display: block;
  font-family: var(--font-display);
  font-size: var(--fs-h2);
  line-height: 1;
  color: var(--mustard);
  margin-bottom: var(--space-3);
}

.channel-row h3 {
  font-size: var(--fs-h2);
  margin-bottom: var(--space-4);
}

.channel-row p { color: var(--paper-muted); }

/* Used standalone on the hub, not just inside a .channel-row, where it
   needs its own spacing since there is no grid gap to inherit. Capped at
   760px because that is the source's real width: stretched to the full
   1072px wrap it was upscaling 1.4x and showing it. */
.wrap > .channel-media {
  margin: var(--space-7) auto 0;
  max-width: 760px;
}

.channel-media img {
  width: 100%;
  height: auto;
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
}

/* No honest photograph exists for some of these sections (technical
   work, AI search). Rather than reach for a stock stand-in on a site
   that promises never to, those get type on a brand surface. */
.channel-note {
  padding: var(--space-7) var(--space-6);
  border-radius: var(--radius-lg);
  background: var(--ink-2);
  border: 1px solid rgba(var(--paper-rgb), 0.12);
}

.channel-note .eyebrow { margin-bottom: var(--space-3); }

/* These panels are used both inside a .channel-row and standalone in a
   grid, so they cannot rely on .channel-row h3 for their spacing. Family
   and size come from the card-title rule in §3. */
.channel-note h3 {
  margin-bottom: var(--space-4);
}

.channel-note p:last-child { margin-bottom: 0; }

/* An ordered process reads better against a spine than as another card
   grid; these pages already carry two grids. */
.timeline {
  list-style: none;
  margin: var(--space-7) 0 0;
  padding: 0;
}

.timeline li {
  position: relative;
  padding: 0 0 var(--space-6) var(--space-7);
  border-left: 2px solid rgba(var(--paper-rgb), 0.16);
}

.timeline li:last-child { padding-bottom: 0; border-left-color: transparent; }

.timeline li::before {
  content: '';
  position: absolute;
  left: -7px;
  top: 4px;
  width: 12px;
  height: 12px;
  border-radius: var(--radius-pill);
  background: var(--mustard);
}

.timeline-when {
  display: block;
  font-size: var(--fs-micro);
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: var(--tracking-wider);
  color: var(--mustard);
  margin-bottom: var(--space-2);
}

.timeline p { margin: 0; color: var(--paper-muted); }

.section-paper .timeline li { border-left-color: rgba(var(--ink-rgb), 0.18); }

/* The paragraph that closes a timeline's argument, as opposed to the
   one-line cross-link footnote .note is sized for. Full body colour,
   because it is the point rather than an aside. */
.timeline-summary {
  margin-top: var(--space-6);
  max-width: 68ch;
  color: var(--paper-muted);
}

.section-paper .timeline-summary { color: var(--ink-muted); }
.section-paper .timeline p,
.section-paper .channel-row p { color: var(--ink-muted); }

@media (max-width: 700px) {
  .channel-row { grid-template-columns: 1fr; gap: var(--space-5); }
  /* Photo first on a phone: it orients the reader before the copy */
  .channel-row .channel-media { order: -1 !important; }
  .channel-note { padding: var(--space-6) var(--space-5); }
}

/* ---------------------------------------------------------------
   28. POINTER CAPABILITY
   --------------------------------------------------------------- */

/* A touch device fires :hover on tap and then holds it until the next tap
   lands somewhere else. So every affordance meant to say "this is
   clickable" instead says "this is stuck": a card sitting 4px off the page
   with a shadow under it, a badge frozen mid-spin, all through the
   navigation away and often on return via the back button.

   Neutralised here rather than by wrapping a dozen scattered rules in
   (hover: hover), which would mean restructuring §4, §10, §11, §20 and §26
   for one concern. The shape mirrors the reduced-motion block in §23 — same
   selectors, same reason to exist, different trigger.

   :focus-visible is deliberately untouched: a keyboard user on a
   touchscreen laptop still gets the full affordance, and it cannot stick,
   because focus moves on. */
@media (hover: none) {
  .btn:hover,
  a.card:hover,
  a.post-card:hover,
  .nudge-cta:hover {
    transform: none;
    box-shadow: none;
  }

  a.card:hover .card-more::after,
  a.post-card:hover .card-more::after { transform: none; }

  .logo-tile:hover { transform: none; border-color: transparent; }

  .way:hover { transform: none; box-shadow: none; }
  .way:hover::before { opacity: 0; }
  .way:hover .way-badge { transform: none; }
  .way:hover .way-cta::after { transform: none; }
}

/* ---------------------------------------------------------------
   29. SHOWCASE — copy beside two photographs that drift apart

   A vanilla rebuild of a React/framer-motion component. The reference
   used whileInView + staggerChildren; this rides the .reveal
   IntersectionObserver that already exists (initReveals in main.js), so
   the section costs no JavaScript at all — the same trick the .way
   stagger in §20 uses.

   Its palette was black / #868f97 / white glassmorphism. Mapped to this
   brand: --ink ground, --paper-muted body, an oxblood-tinted --ink-2
   back plate, and for the front card a hairline paper border with
   --shadow-lg in place of the frosted glass, which does not read over an
   ink ground the way it does over pure black.
   --------------------------------------------------------------- */

.showcase {
  position: relative;
  background: var(--ink);
  overflow: hidden;   /* the back layer is deliberately outside the column */
}

.showcase-grid {
  display: grid;
  /* minmax(0, …) on BOTH tracks. A bare 1fr floors at min-content, which is
     how one long word once held the newsletter hero at 481px on every phone. */
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--space-8);
  align-items: center;
}

.showcase-copy { max-width: 34rem; }

.showcase-copy h2 { margin-top: var(--space-3); }

.showcase-copy .lead { margin-top: var(--space-4); color: var(--paper-muted); }

.showcase-points {
  list-style: none;
  margin: var(--space-6) 0 var(--space-7);
  padding: 0;
  display: grid;
  gap: var(--space-3);
}

/* The same list used outside the showcase, where a section needs a set of
   items rather than a paragraph. Generalised rather than copied so there is
   one mustard-rule list on the site, not three. */
.points {
  list-style: none;
  /* The bottom margin matters because a bare <p> and a .prose block both
     declare no top margin, so a paragraph following one of these lists sat at
     exactly 0px from the last item. On managed.html that reads as a broken
     list item rather than as a new paragraph: .points li carries 24px of left
     padding and a mustard dash, so the paragraph appears 24px further left
     with no dash and no vertical offset at all. */
  margin: var(--space-4) 0 var(--space-5);
  padding: 0;
  display: grid;
  gap: var(--space-3);
  font-size: var(--fs-small);
  color: var(--paper-muted);
}

/* Six of the ten lists on the site are the last thing inside a card or a
   column, where a trailing margin only pads the bottom of the box. Same
   last-child guard the bare `p` rule uses in §2. */
.points:last-child { margin-bottom: 0; }

.section-paper .points { color: var(--ink-muted); }

/* The rule above sets the colour on the container, and the .points li rule
   below sets it directly on the item. A declaration that matches the element
   always beats an inherited value, whatever the specificity of the selector
   it came from, so the light-surface variant never reached the text. Both
   existing uses (managed.html) sit on a section-ink2 band, so this changes
   nothing already shipped. It matters for the article pages, which put a
   .points list on paper. */
.section-paper .points li { color: var(--ink-muted); }

/* Same mustard rule as the consult panel's points, so the two read as one
   system rather than two takes on a list */
.showcase-points li,
.points li {
  position: relative;
  padding-left: var(--space-6);
  color: var(--paper-muted);
  font-size: var(--fs-small);
}

.showcase-points li::before,
.points li::before {
  content: '';
  position: absolute;
  left: 0;
  top: 0.62em;
  width: 14px;
  height: 2px;
  background: var(--mustard);
}

/* ---- The two layers ----
   The back plate sits up and left of the front card and is the thing that
   makes this read as depth rather than as one photo. It is pulled outside
   the column on purpose; .showcase clips it. */

.showcase-media {
  position: relative;
  justify-self: center;
  width: 100%;
  max-width: 470px;
  /* Reserves the height both absolutely-positioned layers need, plus the
     travel, so the drift never reflows the section under itself */
  padding-block: var(--space-7);
}

.showcase-layer {
  border-radius: var(--radius-lg);
  overflow: hidden;
  /* 1.2s is far slower than --t-slow and deliberately so: this is the
     section's one signature move, and at --t-slow it reads as a jump
     rather than a drift. */
  transition: transform 1.2s var(--ease), opacity 1.2s var(--ease);
}

.showcase-layer img {
  width: 100%;
  /* height:auto is load-bearing — the same trap documented on .post-thumb
     and .founder-photo. The img carries height="600", which lands on the CSS
     height property as a presentational hint; with width and height both
     definite, aspect-ratio is ignored and the crop never happens. The
     attributes stay for CLS reservation. */
  height: auto;
  aspect-ratio: 4 / 5;
  object-fit: cover;
}

/* Landscape variant, used by the article pages. The 4:5 box above needs a
   portrait source: feed it a landscape frame and the browser downloads the
   whole file and object-fit throws away 47% of it, which is how a change
   estimated at 70KB once shipped 176KB. Only two 4:5 crops exist on disk and
   both are on the homepage, so rather than cut more, the article version runs
   3:2 — which fits the 800x500 brand SVGs and the ~760x570 event photographs
   with nothing discarded. */
.showcase-wide .showcase-layer img { aspect-ratio: 3 / 2; }

.showcase-back {
  position: absolute;
  top: 0;
  left: -14%;
  width: 78%;
  z-index: 0;
  opacity: 0.55;
  filter: blur(1px);
  border: 1px solid rgba(var(--paper-rgb), 0.1);
  /* Tints the plate towards oxblood so the pair reads as brand rather than
     as the same photograph printed twice */
  background: var(--oxblood);
}

.showcase-back img { opacity: 0.72; }

.showcase-front {
  position: relative;
  z-index: 1;
  margin-left: 12%;
  border: 1px solid rgba(var(--paper-rgb), 0.18);
  box-shadow: var(--shadow-lg);
  background: var(--ink-2);
}

/* ---- Entrance ----
   Copy and media fade up together, media 200ms behind (the reference's
   staggerChildren: 0.2). Then the two layers pull apart. */

/* The section carries .reveal only to collect is-visible from the observer.
   Its own 18px fade is suppressed so the staggered children ARE the
   animation — running both means the copy fades twice at different rates
   and the whole band reads as mush. */
.showcase.reveal { opacity: 1; transform: none; transition: none; }

.showcase .showcase-copy,
.showcase .showcase-media {
  opacity: 0;
  transform: translateY(40px);
  transition: opacity var(--t-slow) var(--ease),
              transform var(--t-slow) var(--ease);
}

.showcase.is-visible .showcase-copy,
.showcase.is-visible .showcase-media { opacity: 1; transform: none; }

.showcase.is-visible .showcase-media { transition-delay: 200ms; }

/* At rest the two layers sit together; on reveal they separate. Starting
   them apart and closing would fight the fade-up above, which is already
   travelling downward. */
.showcase.is-visible .showcase-back { transform: translateY(-30px); }
.showcase.is-visible .showcase-front {
  transform: translateY(30px);
  transition-delay: 100ms;
}

/* The reference's hairline under the section. Radial rather than linear so
   it fades at both ends instead of butting into the page edge. */
.showcase::after {
  content: '';
  position: absolute;
  left: 0;
  bottom: 0;
  width: 100%;
  height: 1px;
  background: radial-gradient(50% 50% at 50% 50%,
    rgba(var(--paper-rgb), 0.24) 0%,
    rgba(var(--paper-rgb), 0) 100%);
}

@media (max-width: 900px) {
  .showcase-grid { gap: var(--space-7); }
}

@media (max-width: 700px) {
  /* Photo first, matching .channel-row in §27 — it orients the reader
     before the copy does */
  .showcase-grid { grid-template-columns: minmax(0, 1fr); }
  .showcase-media { order: -1; }
  .showcase-copy { max-width: none; }

  /* Shorter travel: 30px each way is a third of the visible band on a
     phone, where it reads as a glitch rather than as depth */
  .showcase.is-visible .showcase-back { transform: translateY(-14px); }
  .showcase.is-visible .showcase-front { transform: translateY(14px); }
}

@media (prefers-reduced-motion: reduce) {
  /* The blanket rule in §23 zeroes transition-duration but never
     transition-delay, so without these the section would still stagger in
     — the trap this stylesheet documents three times. Layers rest where
     they land; the composition is the point, not the travel. */
  .showcase .showcase-copy,
  .showcase .showcase-media { opacity: 1; transform: none; }
  .showcase.is-visible .showcase-media,
  .showcase.is-visible .showcase-front { transition-delay: 0ms; }
  .showcase-layer { transition: none; }
  .showcase.is-visible .showcase-back { transform: translateY(-30px); }
  .showcase.is-visible .showcase-front { transform: translateY(30px); }
}

/* ---------------------------------------------------------------
   29b. CREW — centred headline with photographs scattered round it
   Homepage only. Each photo is rounded on three corners and square on the
   one its tag sits against, so the tag reads as attached to the frame.
   Positions are percentages of the stage, so the scatter holds its shape
   from 900px up; below that it becomes a two-column grid under the copy.
   --------------------------------------------------------------- */

.crew { overflow: hidden; }

.crew-stage {
  position: relative;
  min-height: 680px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.crew-copy {
  position: relative;
  z-index: 2;
  max-width: 30rem;
  text-align: center;
}

.crew-copy h2 { margin-top: var(--space-3); font-size: clamp(2rem, 4.4vw, 3.2rem); }
.crew-copy .lead { margin: var(--space-4) auto var(--space-6); color: var(--paper-muted); }

.crew-photo {
  position: absolute;
  z-index: 1;
  margin: 0;
}

.crew-photo img {
  display: block;
  width: 100%;
  height: auto;
  object-fit: cover;
  border-radius: 28px;
  box-shadow: var(--shadow-lg);
}

.crew-a { left: 0; top: 8%; width: 21%; }
.crew-a img { aspect-ratio: 4 / 5; border-bottom-left-radius: 0; }

.crew-b { right: 2%; top: 3%; width: 24%; }
.crew-b img { aspect-ratio: 3 / 2; border-top-left-radius: 0; }

.crew-c { right: 6%; bottom: 4%; width: 25%; }
.crew-c img { aspect-ratio: 3 / 2; border-bottom-left-radius: 0; }

.crew-d { left: 23%; bottom: 3%; width: 12%; }
.crew-d img { aspect-ratio: 1; border-top-right-radius: 0; }

.crew-tag {
  position: absolute;
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-5);
  border-radius: var(--radius-lg);
  font-family: var(--font-body);
  font-weight: 800;
  font-size: var(--fs-small);
  text-transform: uppercase;
  letter-spacing: var(--tracking-wide);
  white-space: nowrap;
  box-shadow: var(--shadow-md);
}

.crew-tag svg { flex: none; }

.crew-tag-mustard { background: var(--mustard); color: var(--ink); }
.crew-tag-cream { background: var(--cream); color: var(--ink); }
.crew-tag-oxblood { background: var(--oxblood); color: var(--paper); }

/* Each tag hangs off the frame's square corner */
.crew-a .crew-tag { left: -6%; bottom: 10%; }
.crew-b .crew-tag { right: 62%; top: 30%; }
.crew-c .crew-tag { right: 60%; bottom: 12%; }

/* Entrance: the photos settle in behind the copy, staggered. Rides the
   existing .reveal observer. */
.crew.reveal { opacity: 1; transform: none; transition: none; }

.crew .crew-copy,
.crew .crew-photo {
  opacity: 0;
  transform: translateY(24px);
  transition: opacity var(--t-slow) var(--ease), transform var(--t-slow) var(--ease);
}

.crew.is-visible .crew-copy,
.crew.is-visible .crew-photo { opacity: 1; transform: none; }

.crew.is-visible .crew-a { transition-delay: 120ms; }
.crew.is-visible .crew-b { transition-delay: 220ms; }
.crew.is-visible .crew-c { transition-delay: 320ms; }
.crew.is-visible .crew-d { transition-delay: 420ms; }

@media (max-width: 1100px) {
  .crew-tag { padding: var(--space-2) var(--space-4); font-size: var(--fs-micro); }
  .crew-a .crew-tag { left: 0; }
}

@media (max-width: 900px) {
  .crew-stage {
    min-height: 0;
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-5) var(--space-4);
  }
  .crew-copy { grid-column: 1 / -1; max-width: none; margin-bottom: var(--space-4); }
  .crew-photo { position: relative; inset: auto; width: auto; }
  .crew-photo img { border-radius: 20px; }
  .crew-a img, .crew-b img, .crew-c img, .crew-d img { aspect-ratio: 4 / 5; }
  .crew-a img { border-bottom-left-radius: 0; }
  .crew-b img { border-bottom-left-radius: 0; border-top-left-radius: 20px; }
  .crew-a .crew-tag, .crew-b .crew-tag, .crew-c .crew-tag {
    left: 0; right: auto; top: auto; bottom: 0;
    max-width: 100%;
    white-space: normal;
    border-radius: 0 var(--radius-md) 0 0;
  }
}

@media (prefers-reduced-motion: reduce) {
  .crew .crew-copy, .crew .crew-photo { opacity: 1; transform: none; transition: none; }
}

/* ---------------------------------------------------------------
   30. ARTICLE PAGE — long-form posts under blog/

   Four posts that used to live only on thegrowthacademy.com.au now have
   pages here. They run 1,300 to 2,800 words each, three to five times a
   service page, and nothing in §1 to §29 was built for that: there was no
   prose measure, no figure, no pull-quote, no contents list. This section
   adds those and nothing else. Everything a channel page already provides
   (the hero, .section-head, .points, .timeline, .faq, .post-card, the
   closing CTA band) is reused as it stands.

   The one component borrowed and modified is §29's .showcase, through the
   .showcase-wide modifier, declared up there beside the rule it overrides.

   No max-width on .article itself, on purpose: `section` is a bare element
   selector carrying var(--space-9) of block padding, and the surface
   colours have to bleed the full width of the viewport. The narrow measure
   belongs to .prose inside the .wrap, which is the only thing that should
   be 68ch.
   --------------------------------------------------------------- */

/* ---- Prose ----
   68ch is not a new number: it is the measure .faq p and .timeline-summary
   already use, so the site has one reading width rather than three. */
.article .prose { max-width: 68ch; }

/* Spaced with the owl rather than a margin on p, so a figure, list or
   blockquote dropped into the flow spaces identically without its own rule */
.article .prose > * + * { margin-top: var(--space-5); }

/* margin-bottom rather than margin: 0. The global p rule in §2 sets a bottom
   margin that has to go, but zeroing all four sides here would also beat the
   owl above: `.article .prose p` is (0,2,1) and `.article .prose > * + *` is
   (0,2,0), so a shorthand wins and every paragraph closes up against the
   one before it. */
.article .prose p { margin-bottom: 0; color: var(--paper-muted); }
.section-paper .prose p { color: var(--ink-muted); }

/* A subhead needs more air above it than a paragraph does, and it has to
   out-specify the owl to get it */
.article .prose > * + h3 { margin-top: var(--space-7); }

/* Emphasis lifts to full brightness, which is the only contrast step left
   once the body text is already muted */
.article .prose strong { color: var(--paper); font-weight: 700; }
.section-paper .prose strong { color: var(--ink); }

.article .prose a { text-decoration: underline; text-underline-offset: 3px; }

/* ---- Subheads ----
   Same escape hatch as the card titles in §3. The display face is uppercase
   at every size, and a dozen uppercase Alfa Slab headings down a 2,800-word
   page reads as shouting. Work Sans 800 keeps the weight without the volume.

   The selectors are narrow on purpose. `.article h3` would be (0,1,1), the
   same specificity as the .card h3 / .post-card h3 / .channel-note h3 /
   .faq summary rule in §3, and it sits later in the file, so it would win
   and silently retint every card, post card and FAQ summary on the page.
   There are three such regions on every article. .article .prose h3 cannot
   reach one, because a card is never inside .prose. */
.article .section-head h2 {
  font-family: var(--font-body);
  font-size: var(--fs-article-h2);
  font-weight: 800;
  letter-spacing: var(--tracking-wide);
  line-height: 1.2;
}

.article .prose h3 {
  font-family: var(--font-body);
  font-size: var(--fs-article-h3);
  font-weight: 800;
  /* Sentence case, one clear level below the uppercase h2, without adding a
     fourth typeface to say so */
  text-transform: none;
  letter-spacing: normal;
  line-height: 1.3;
  color: var(--paper);
}

.section-paper .prose h3 { color: var(--ink); }

/* ---- Figures ----
   Values deliberately identical to .channel-media img in §27 and the 760px
   cap on .wrap > .channel-media: 760px is the real pixel width of these
   sources and anything wider upscales. The two devices are meant to look
   the same, so they are built the same. */
.article figure {
  margin: var(--space-7) 0;
  max-width: 760px;
}

.article figure img {
  width: 100%;
  /* height:auto again. The img carries width and height attributes for CLS
     reservation, and with both definite the aspect-ratio would be ignored.
     Fourth instance of the trap in this file. */
  height: auto;
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  background: var(--ink-2);
}

/* The caption carries the meaning of the diagram above it. The artwork is
   decorative (alt="") because text inside an img-loaded SVG is invisible to
   search and unreadable to a screen reader, so this is where the idea gets
   said in words. */
.article figcaption {
  margin-top: var(--space-4);
  max-width: 62ch;
  padding-left: var(--space-4);
  border-left: 2px solid var(--mustard);
  font-size: var(--fs-small);
  color: var(--paper-faint);
}

.section-paper figcaption { color: var(--ink-muted); }

/* ---- Pull-quote ----
   Mixed-case display face with a mustard rule. It has to read as a different
   device from .section-callout in §25, which already owns
   Alfa-Slab-uppercase-mustard as the site's emphasis block.

   No content: declaration and no decorative quote glyph. tools/rules.js
   walks the computed content of every ::before and ::after on every page
   because an em-dash lived in one of them for weeks, and every string added
   there is one more thing that has to be audited forever. */
.article blockquote {
  position: relative;
  margin: var(--space-8) 0;
  max-width: 62ch;
  padding: var(--space-2) 0 var(--space-2) var(--space-6);
  font-family: var(--font-display);
  font-size: var(--fs-h3);
  /* Alfa Slab One is uppercase globally in §3. At pull-quote length that is
     unreadable, so this is one of the two places that opts out. */
  text-transform: none;
  line-height: 1.45;
  color: var(--cream);
}

.section-paper blockquote { color: var(--oxblood); }

/* The rule is a pseudo-element rather than a border so it can animate
   independently of the text (see the entrance below) */
.article blockquote::before {
  content: '';
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 3px;
  background: var(--mustard);
}

.article blockquote p { margin: 0; }
.article blockquote p + p { margin-top: var(--space-4); }

.article blockquote cite {
  display: block;
  margin-top: var(--space-4);
  font-family: var(--font-body);
  font-size: var(--fs-micro);
  font-weight: 800;
  font-style: normal;
  text-transform: uppercase;
  letter-spacing: var(--tracking-wider);
  color: var(--mustard);
}

/* ---- Anchor targets ----
   .site-header is sticky, so without this every jump from the contents list
   lands with its heading hidden underneath the header. */
.article section[id] { scroll-margin-top: calc(var(--header-h) + var(--space-5)); }

/* Pre-existing on all fourteen pages, not only these four: the skip link
   points at #main, and #main had no scroll margin either. */
#main { scroll-margin-top: calc(var(--header-h) + var(--space-4)); }

/* ---- Contents ---- */
.article-toc { max-width: 68ch; }
.article-toc .eyebrow { margin-bottom: var(--space-4); }

.article-toc ol {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--space-1);
  counter-reset: toc;
}

.article-toc li { counter-increment: toc; }

.article-toc a {
  display: flex;
  align-items: baseline;
  gap: var(--space-4);
  /* Standalone navigation rather than a link inside a sentence, so it needs
     a real tap target, the same reason .breadcrumb a has one in §3 */
  min-height: 44px;
  font-size: var(--fs-small);
  color: var(--paper-muted);
  transition: color var(--t-fast) var(--ease);
}

/* A counter, so this is the one new content: string on the site and it can
   never hold a dash of any kind */
.article-toc a::before {
  content: counter(toc, decimal-leading-zero);
  flex: 0 0 auto;
  font-size: var(--fs-micro);
  font-weight: 800;
  letter-spacing: var(--tracking-wide);
  color: var(--mustard);
}

.article-toc a:hover,
.article-toc a:focus-visible { color: var(--cream); }

/* ---- Reading progress ----
   Sits inside .site-header so it travels with it and needs no stacking
   context of its own. Driven from initHeader's existing scroll listener. */
.article-progress {
  height: 3px;
  background: rgba(var(--paper-rgb), 0.08);
}

.article-progress span {
  display: block;
  height: 100%;
  background: var(--mustard);
  /* scaleX rather than width: transform only, so none of the sixty frames a
     second triggers layout */
  transform: scaleX(0);
  transform-origin: 0 50%;
}

/* ---- Entrance ----
   On every other page .reveal sits on the <section>. Here it must not.
   initReveals fires at threshold 0.1 and then unobserves, so a 2,500px
   article band counts as visible as soon as its top 250px are on screen,
   and every stagger inside it would finish while the reader is still in the
   first paragraph. So .reveal goes on the effect-bearing element itself:
   each .prose, each figure, each blockquote, each list, the .faq, the
   showcase band, the closing CTA. Same observer, more targets, no new JS.

   Do not tidy this by hoisting .reveal back onto the sections. It breaks
   every stagger below, and it breaks them invisibly. */

/* Where an element both fades itself and staggers its children, the parent's
   own fade is suppressed so the children ARE the animation. Same reasoning
   as .showcase.reveal in §29: running both reads as mush. */
.article ul.points.reveal,
.article ol.timeline.reveal,
.article .faq.reveal,
.article-toc ol.reveal { opacity: 1; transform: none; transition: none; }

/* Contents entries */
.article-toc ol.reveal li {
  opacity: 0;
  transform: translateY(10px);
  transition: opacity var(--t-slow) var(--ease), transform var(--t-slow) var(--ease);
}
.article-toc ol.reveal.is-visible li { opacity: 1; transform: none; }
.article-toc ol.reveal.is-visible li:nth-child(1) { transition-delay: 0ms; }
.article-toc ol.reveal.is-visible li:nth-child(2) { transition-delay: 45ms; }
.article-toc ol.reveal.is-visible li:nth-child(3) { transition-delay: 90ms; }
.article-toc ol.reveal.is-visible li:nth-child(4) { transition-delay: 135ms; }
.article-toc ol.reveal.is-visible li:nth-child(5) { transition-delay: 180ms; }
.article-toc ol.reveal.is-visible li:nth-child(6) { transition-delay: 225ms; }
.article-toc ol.reveal.is-visible li:nth-child(7) { transition-delay: 270ms; }
.article-toc ol.reveal.is-visible li:nth-child(8) { transition-delay: 315ms; }

/* Points lists arrive from the left, so they read as items being set down
   beside their mustard rule rather than as another block fading up */
.article ul.points.reveal li {
  opacity: 0;
  transform: translateX(-8px);
  transition: opacity var(--t-slow) var(--ease), transform var(--t-slow) var(--ease);
}
.article ul.points.reveal.is-visible li { opacity: 1; transform: none; }
.article ul.points.reveal.is-visible li:nth-child(1) { transition-delay: 0ms; }
.article ul.points.reveal.is-visible li:nth-child(2) { transition-delay: 60ms; }
.article ul.points.reveal.is-visible li:nth-child(3) { transition-delay: 120ms; }
.article ul.points.reveal.is-visible li:nth-child(4) { transition-delay: 180ms; }
.article ul.points.reveal.is-visible li:nth-child(5) { transition-delay: 240ms; }
.article ul.points.reveal.is-visible li:nth-child(6) { transition-delay: 300ms; }

/* Figures settle rather than fade: the artwork comes to rest from very
   slightly oversized, and the caption lands just behind it. 1.2s is §29's
   signature duration, reused so the page speaks one motion vocabulary. */
.article figure.reveal { opacity: 1; transform: none; transition: none; }
.article figure.reveal img {
  opacity: 0;
  transform: scale(1.03);
  transition: opacity 1.2s var(--ease), transform 1.2s var(--ease);
}
.article figure.reveal.is-visible img { opacity: 1; transform: none; }

.article figure.reveal figcaption {
  opacity: 0;
  transform: translateY(8px);
  transition: opacity var(--t-slow) var(--ease), transform var(--t-slow) var(--ease);
}
.article figure.reveal.is-visible figcaption {
  opacity: 1;
  transform: none;
  transition-delay: 120ms;
}

/* The pull-quote's mustard rule draws downward, then the words arrive */
.article blockquote.reveal { opacity: 1; transform: none; transition: none; }
.article blockquote.reveal::before {
  transform: scaleY(0);
  transform-origin: 50% 0;
  transition: transform 0.8s var(--ease);
}
.article blockquote.reveal.is-visible::before { transform: scaleY(1); }

.article blockquote.reveal > * {
  opacity: 0;
  transition: opacity var(--t-slow) var(--ease);
}
.article blockquote.reveal.is-visible > * {
  opacity: 1;
  transition-delay: 150ms;
}

/* Timeline steps arrive down the spine */
.article ol.timeline.reveal li {
  opacity: 0;
  transform: translateY(12px);
  transition: opacity var(--t-slow) var(--ease), transform var(--t-slow) var(--ease);
}
.article ol.timeline.reveal.is-visible li { opacity: 1; transform: none; }
.article ol.timeline.reveal.is-visible li:nth-child(1) { transition-delay: 0ms; }
.article ol.timeline.reveal.is-visible li:nth-child(2) { transition-delay: 70ms; }
.article ol.timeline.reveal.is-visible li:nth-child(3) { transition-delay: 140ms; }
.article ol.timeline.reveal.is-visible li:nth-child(4) { transition-delay: 210ms; }
.article ol.timeline.reveal.is-visible li:nth-child(5) { transition-delay: 280ms; }
.article ol.timeline.reveal.is-visible li:nth-child(6) { transition-delay: 350ms; }

/* FAQ rows. Opacity and transform only: a <details> has to stay clickable
   and keyboard-operable the whole way through its entrance. */
.article .faq.reveal details {
  opacity: 0;
  transform: translateY(10px);
  transition: opacity var(--t-slow) var(--ease), transform var(--t-slow) var(--ease);
}
.article .faq.reveal.is-visible details { opacity: 1; transform: none; }
.article .faq.reveal.is-visible details:nth-child(1) { transition-delay: 0ms; }
.article .faq.reveal.is-visible details:nth-child(2) { transition-delay: 40ms; }
.article .faq.reveal.is-visible details:nth-child(3) { transition-delay: 80ms; }
.article .faq.reveal.is-visible details:nth-child(4) { transition-delay: 120ms; }
.article .faq.reveal.is-visible details:nth-child(5) { transition-delay: 160ms; }
.article .faq.reveal.is-visible details:nth-child(6) { transition-delay: 200ms; }
.article .faq.reveal.is-visible details:nth-child(7) { transition-delay: 240ms; }
.article .faq.reveal.is-visible details:nth-child(8) { transition-delay: 280ms; }

/* The hero deliberately carries no .reveal. Its h1 is the LCP element on a
   type-only hero, and putting the largest text on the page behind a fade
   delays that measurement for no gain. The channel-page heroes do the same. */

/* ---- Responsive ---- */
@media (max-width: 700px) {
  .article figure { margin: var(--space-6) 0; max-width: none; }
  .article blockquote {
    margin: var(--space-7) 0;
    padding-left: var(--space-5);
  }
  .article .prose { max-width: none; }
  .article-toc { max-width: none; }
}

@media (prefers-reduced-motion: reduce) {
  /* §23 zeroes transition-duration and never transition-delay. Every stagger
     above therefore needs its delay zeroed by hand, and every animated
     element needs a resting state, or it stays invisible forever. */
  .article-toc ol.reveal li,
  .article ul.points.reveal li,
  .article ol.timeline.reveal li,
  .article .faq.reveal details,
  .article figure.reveal figcaption,
  .article blockquote.reveal > * { opacity: 1; transform: none; }

  .article figure.reveal img { opacity: 1; transform: none; }
  .article blockquote.reveal::before { transform: none; }

  .article-toc ol.reveal.is-visible li:nth-child(1),
  .article-toc ol.reveal.is-visible li:nth-child(2),
  .article-toc ol.reveal.is-visible li:nth-child(3),
  .article-toc ol.reveal.is-visible li:nth-child(4),
  .article-toc ol.reveal.is-visible li:nth-child(5),
  .article-toc ol.reveal.is-visible li:nth-child(6),
  .article-toc ol.reveal.is-visible li:nth-child(7),
  .article-toc ol.reveal.is-visible li:nth-child(8),
  .article ul.points.reveal.is-visible li:nth-child(1),
  .article ul.points.reveal.is-visible li:nth-child(2),
  .article ul.points.reveal.is-visible li:nth-child(3),
  .article ul.points.reveal.is-visible li:nth-child(4),
  .article ul.points.reveal.is-visible li:nth-child(5),
  .article ul.points.reveal.is-visible li:nth-child(6),
  .article ol.timeline.reveal.is-visible li:nth-child(1),
  .article ol.timeline.reveal.is-visible li:nth-child(2),
  .article ol.timeline.reveal.is-visible li:nth-child(3),
  .article ol.timeline.reveal.is-visible li:nth-child(4),
  .article ol.timeline.reveal.is-visible li:nth-child(5),
  .article ol.timeline.reveal.is-visible li:nth-child(6),
  .article .faq.reveal.is-visible details:nth-child(1),
  .article .faq.reveal.is-visible details:nth-child(2),
  .article .faq.reveal.is-visible details:nth-child(3),
  .article .faq.reveal.is-visible details:nth-child(4),
  .article .faq.reveal.is-visible details:nth-child(5),
  .article .faq.reveal.is-visible details:nth-child(6),
  .article .faq.reveal.is-visible details:nth-child(7),
  .article .faq.reveal.is-visible details:nth-child(8),
  .article figure.reveal.is-visible figcaption,
  .article blockquote.reveal.is-visible > * { transition-delay: 0ms; }

  /* Redundant with the scrollbar, and decoration rather than information.
     initHeader also skips the writes under reduced motion, so this costs
     nothing at runtime. */
  .article-progress { display: none; }
}

/* ---------------------------------------------------------------
   31. ENGINE PANEL — circle photo beside copy, homepage Engine 02
   A paper panel set into the section-ink2 band: grayscale photo in an
   oxblood-ringed circle, a mustard badge overlapping its top-right edge,
   and the two growth services as links rather than a second card row.
   --------------------------------------------------------------- */

.engine-panel {
  display: grid;
  /* minmax(0, …) on both tracks, same reason as .showcase-grid */
  grid-template-columns: minmax(0, 5fr) minmax(0, 6fr);
  gap: var(--space-8);
  align-items: center;
  background: var(--paper);
  background-image: var(--grain);
  background-blend-mode: multiply;
  color: var(--ink);
  border-radius: var(--radius-lg);
  padding: var(--space-8) var(--space-7);
}

.engine-media {
  position: relative;
  justify-self: center;
  width: 100%;
  max-width: 440px;
  /* Own stacking context so the ring below sits behind the photo, not
     behind the panel */
  isolation: isolate;
}

/* The oxblood ring is a separate layer because the photo's grayscale
   filter would grey a border set on the img itself */
.engine-media::before {
  content: '';
  position: absolute;
  inset: -10px;
  z-index: -1;
  border-radius: 50%;
  background: var(--oxblood);
}

.engine-media img {
  display: block;
  width: 100%;
  /* height:auto is load-bearing — see .showcase-layer img */
  height: auto;
  aspect-ratio: 1;
  object-fit: cover;
  border-radius: 50%;
  filter: grayscale(1) contrast(1.05);
}

.engine-badge {
  position: absolute;
  top: 2%;
  right: 0;
  width: 30%;
  aspect-ratio: 1;
  border-radius: 50%;
  background: var(--mustard);
  color: var(--ink);
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: var(--shadow-md);
}

.engine-badge svg { width: 50%; height: auto; }

.engine-copy h2 { margin-top: var(--space-3); color: var(--ink); }

.engine-copy .lead { margin-top: var(--space-4); color: var(--ink); }

.engine-links {
  list-style: none;
  margin: var(--space-6) 0 0;
  padding: 0;
  display: grid;
  gap: var(--space-3);
}

.engine-links a {
  display: flex;
  gap: var(--space-4);
  align-items: flex-start;
  padding: var(--space-4);
  border-radius: var(--radius-md);
  border: var(--border-hairline-ink);
  color: var(--ink-muted);
  font-size: var(--fs-small);
  transition: border-color var(--t-base) var(--ease),
              background-color var(--t-base) var(--ease);
}

.engine-links a:hover,
.engine-links a:focus-visible {
  border-color: var(--oxblood);
  background-color: rgba(var(--ink-rgb), 0.04);
}

.engine-links .card-number { flex: none; margin-bottom: 0; }

.engine-links strong {
  display: block;
  margin-bottom: var(--space-1);
  font-size: var(--fs-card-title);
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: var(--tracking-wide);
  color: var(--ink);
}

.engine-links .card-more { display: flex; color: var(--oxblood); margin-top: var(--space-2); }

.engine-links a:hover .card-more::after,
.engine-links a:focus-visible .card-more::after { transform: translateX(4px); }

@media (max-width: 900px) {
  .engine-panel { gap: var(--space-7); padding: var(--space-7) var(--space-6); }
}

@media (max-width: 700px) {
  /* Photo first, matching .showcase and .channel-row */
  .engine-panel {
    grid-template-columns: minmax(0, 1fr);
    padding: var(--space-6) var(--space-4);
  }
  .engine-media { max-width: 280px; }
  .engine-media::before { inset: -6px; }
}

/* ---------------- Enquiry forms (Web3Forms) ---------------- */

/* Web3Forms' honeypot. Off-screen rather than display:none, so a bot
   that skips hidden inputs still sees a box to tick. */
.form-botcheck {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  opacity: 0;
}

/* Shown only when Web3Forms does not confirm the send */
.form-error {
  margin: var(--space-3) 0 0;
  font-size: var(--fs-small);
  font-weight: 600;
  color: var(--oxblood);
}
.form-error a { color: inherit; }
.consult .form-error { color: var(--mustard); }
