/* ============================================================
   Quietwork × Ramp Studio structure study
   Built 2026-09-19. A LOCAL STUDY, not the live site.

   What is borrowed: the STRUCTURE and LAYOUT of
   rampstudio.framer.website, measured rather than guessed.
   What is not: its colour, its typeface, its copy, its images.

   The governing grid of the reference, measured at 1440:
   a narrow LEFT RAIL carrying a small uppercase label, and a
   WIDE RIGHT COLUMN carrying everything else. Gutter 40px.
   The rail runs 40..480 and the content column 520..1400, so
   roughly 1 : 2 with a 40px gap. EVERY section obeys it, and
   that repetition is most of what makes the reference read as
   one system rather than a stack of blocks.

   Quietwork colours are unchanged: tokens come from the real
   palettes.css, loaded first. No hex is written in this file
   except #fff on a coloured ground, same rule as site.css.
   ============================================================ */

:root {
  /* Lets height transition to `auto`. Without it the accordion would need
     a JavaScript height measurement and a resize listener. */
  interpolate-size: allow-keywords;
  --gutter: 40px;
  --wrap: 1360px;
  --rail: 420px;
  --gap: 48px;
  --hair: 1px solid var(--line);
  /* ⚠️ Ramp's ENTIRE radius scale, read off every element on the page, is
     4, 6, 8, 10 and 11px, plus one 100px pill. Its largest corner is 11.
     This build used 20px on panels and cards, roughly double the reference's
     maximum, and that is a large part of why it read as less precise: a big
     radius reads as soft, a small one reads as drawn. */
  --radius-lg: 10px;
  --radius-sm: 6px;
  /* Section corners are a different scale from component corners: the
     reference's FAQ block uses 40px where nothing else exceeds 11px. */
  --radius-section: 40px;
}

* { box-sizing: border-box; }

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

/* ⚠️ THE PAGE GROUND IS THE ACCENT, and this is the whole trick behind the
   reference's footer. Its body is the brand yellow; the content sections sit
   ON it in white and grey; and at the end the sections simply stop and the
   body shows through. The footer is not a coloured block, it is the page's
   own ground revealed. Owen, of the previous version: "theirs is sophisticated
   and interesting, ours is just a block of colour." Ours was a block of colour
   because the FAQ and the footer were both painted --bg-ink, so they fused
   into one slab with no edge between them. */
body {
  margin: 0;
  background: var(--accent);
  color: var(--text);
  font-family: Archivo, system-ui, sans-serif;
  font-size: 17px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  /* ⚠️ `overflow-x: clip` REMOVED 2026-09-20. An overflow value on `body`
     PROPAGATES TO THE VIEWPORT, and a viewport whose overflow is not visible
     is a documented way to break `position: fixed` on iOS Safari: the fixed
     bar stops being pinned to the screen and page content scrolls above it,
     un-blurred, which is exactly what Owen reported twice.
     It was safe to remove because it is no longer load-bearing: `.hero` keeps
     its own `overflow: clip` for the bleeding wordmark, and the work rail is
     its own scroller. Verified before removing, not after: with the clip
     disabled, 36 combinations (6 pages x 6 widths from 390 to 1440) reported
     ZERO horizontal overflow. If something ever does overflow again, clip it
     on that element, never on the body. */
}

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

/* ⚠️ NO max-width. Measured on the reference at 1440: the label sits at
   x=40 and the content column ends at 1400, i.e. the page is full width with
   a fixed 40px gutter and nothing is centred. A centred 1360 wrap is what put
   this build's wordmark 184px inside the screen edge on a wide display, which
   was the most obvious thing separating the two pages. Paragraphs keep their
   own measure caps, so the line length does not run away. */
.wrap {
  max-width: none;
  margin: 0;
  padding-inline: var(--gutter);
}
/* ⚠️ 40px is 18% of a 440px phone, taken off BOTH sides: the reference's
   gutter is a proportion of a desktop, not a constant. At 24px the reading
   column gains 32px, the work rail's next card peeks 49px instead of 17
   (which is the only thing telling anyone it scrolls), and the logo row
   fits five marks to a line instead of four and a half. */
@media (max-width: 620px) { :root { --gutter: 24px; } }

/* ---- The split. This is the whole system. ---------------------------- */
/* Measured on the reference at 1440: rail 40..516 (476 wide), content
   516..1400 (884 wide), and the GAP IS ZERO. The columns simply abut, and the
   label's own box provides the air. 476 : 884 is 35 : 65. */
.split {
  display: grid;
  grid-template-columns: 35fr 65fr;
  gap: 0;
  align-items: start;
}
/* The reference lets the rail run its own length and the content column
   carry the height. Sticky is what makes a long right column read as one
   section rather than as several. */
/* ⚠️ The rail needs an inset even though the grid gap is 0. The reference
   runs zero gap and gets away with it because its rail headings are two short
   words ("Learner Stories"). A longer heading here filled the rail to its
   exact edge and ran straight into the image in the next column with no air
   at all. The gap stays 0, as measured; the rail's CONTENT is inset instead,
   which is what the reference achieves by keeping its headings short. */
.split__rail { position: sticky; top: 96px; padding-right: clamp(24px, 3vw, 56px); }

/* ---- THE EYEBROW SITS ABOVE EVERYTHING IN ITS SECTION ------------------
   Owen, 2026-09-20: *"Eyebrow is supposed to be above all content in the
   section."* That is the whole rule, and it applies to **every** rail, which
   is why it lives on `.split__rail` itself rather than on a modifier class.
   A modifier was tried first and every new section would have had to remember
   it; with no build step, anything you have to remember is something that
   eventually gets forgotten.

   The rail stacks its eyebrow above its heading, so without this the eyebrow
   starts a whole eyebrow-block lower than the column beside it and lands
   level with, or under, the top of that content.

   ⚠️ **MEASURE AGAINST THE RIGHT COLUMN'S BOX, NOT ITS FIRST VISIBLE THING.**
   This was got wrong twice. `In their words` looked correct at +24px because
   the measurement found the PICTURE inside `.quote`, which sits 29px in from
   its own box top; measured against the box, the eyebrow was level with it
   like all the others. And `Where to start` briefly had the rail pushed DOWN
   to line its kicker up with the cards' kickers, which measured beautifully
   and put the eyebrow below the top of the card. Owen: *"you made it worse."*

   The offset is the eyebrow's own block, its line box plus its 14px bottom
   margin, so editing `.label` moves this with it. */
.split__rail { margin-top: calc(-1 * (1.55 * 0.875rem + 14px)); }
@media (max-width: 900px) {
  /* Stacked, the rail sits above its own content, there is nothing to clear,
     and the pull would only eat the section's top padding. */
  .split__rail { margin-top: 0; }
}

/* ⚠️ THE RAIL'S BUTTON SITS WELL CLEAR OF ITS HEADING: ~100px, not the 18px
   it shipped with (Owen, 2026-09-20). At display size a button tucked under a
   heading reads as part of it, as a subtitle would, rather than as the thing
   to do after reading it. It is a PATTERN, not one section: all three rails
   that carry a button use this (Selected work, Where to start, In their
   words), and they carried the value as an inline margin on each <p>, which
   is how the three could have drifted apart. The clamp holds 100px from about
   1250 up and eases to 48 on a phone, where the rail is static and the cards
   sit directly beneath it. */
.rail__cta { margin-top: clamp(48px, 8vw, 100px); }
@media (max-width: 900px) {
  .split { grid-template-columns: 1fr; gap: 24px; }
  .split__rail { position: static; }
}

/* ---- The label. Uppercase, tracked, small. --------------------------- */
/* The reference sets these in Azeret Mono. Quietwork has a documented
   Single Voice Rule (one face, Archivo, hierarchy from size and weight),
   so this is Archivo tracked out instead. Same job, no second typeface. */
/* Measured on the reference: 14px, weight 500, letter-spacing 2.8px which is
   0.2em. This build had it at 11px/600/0.16em, smaller and tighter, so it
   read as a caption rather than as a structural marker. */
.label {
  margin: 0 0 14px;
  font-size: 0.875rem;
  font-weight: 500;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--text-muted);
}
.label--onaccent { color: rgba(255,255,255,0.82); }

/* ---- Type ------------------------------------------------------------ */
/* ⚠️ WEIGHT 400, not 700. Measured across every heading on the reference:
   hero statement, section headings, panel titles, accordion titles and the
   footer wordmark are ALL weight 400. The only 500s are the mono section
   labels and the FAQ questions; the only 600 is the nav wordmark. This build
   set everything at 700, which is the single biggest reason the two pages
   felt different in texture. Size carries the hierarchy, not weight.
   ⚠️ Sizes were measured at a 756px viewport, the small end of the clamps,
   so they are NOT copied literally. The transferable facts are the weights
   and the tracking, neither of which scales with the viewport. */
h1, h2, h3, h4 { margin: 0; font-weight: 400; letter-spacing: -0.03em; line-height: 1.02; }
h2 { font-size: clamp(1.75rem, 2.9vw, 2.75rem); }
h3 { font-size: clamp(1.25rem, 1.8vw, 1.5rem); letter-spacing: -0.02em; line-height: 1.12; }
p  { margin: 0 0 1em; max-width: 62ch; }
p:last-child { margin-bottom: 0; }
/* Measured on the reference: 22px on 26.4px, a leading of 1.2. This build had
   it smaller (20px) and far looser (the body's 1.55), which is most of why the
   same paragraph ran to four lines here against three there. */
/* ⚠️ The floor is 1.25rem, not 1.125. At 18px against a 17px body the lead
   stopped being a lead on a phone: one pixel of difference is not a step,
   and the section then opened on two paragraphs that looked identical.
   20px restores a visible ratio and leaves the desktop 22px untouched. */
/* ⚠️ Black, not muted (Owen, 2026-09-20). The rule applied here: the muted
   colour earns its place where it separates secondary text from PRIMARY text
   sitting beside it, and does not where something else already separates
   them. A lead is 20px directly above 17px body copy, so the size is already
   doing the work and the colour was only greying the intro. Card
   descriptions, accordion bodies, captions and breadcrumbs keep it, because
   there the neighbouring text is the same size or larger. */
.lead { font-size: clamp(1.25rem, 1.5vw, 1.375rem); line-height: 1.2; color: var(--text); }
a { color: inherit; }

/* ---- The button. A RECTANGLE with a separate attached arrow box. ------
   ⚠️ Corrected 2026-09-19. These were built as 999px pills, which is the
   single most visible thing that made the first build not look like the
   reference. Ramp's buttons are near-square-cornered blocks with the arrow
   sitting in its own cell, divided by a hairline. The shape is the tell. */
.btn {
  display: inline-flex; align-items: stretch;
  border-radius: var(--radius-sm);
  background: var(--text);
  color: var(--bg);
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  text-decoration: none;
  overflow: hidden;
  transition: background .15s ease, color .15s ease;
}
.btn__t { padding: 14px 18px; }
.btn__a {
  display: grid; place-items: center;
  width: 40px; flex: none;
  border-left: 1px solid rgba(255,255,255,0.28);
}
.btn__a svg { width: 14px; height: 14px; }
.btn:hover { background: var(--accent); color: #fff; }
.btn--light { background: var(--bg); color: var(--text); }
.btn--light .btn__a { border-left-color: rgba(0,0,0,0.22); }
.btn--light:hover { background: var(--bg-ink); color: #fff; }
.btn--ghost { background: transparent; color: var(--text); box-shadow: inset 0 0 0 1px var(--text); }
.btn--ghost .btn__a { border-left-color: rgba(0,0,0,0.22); }
.btn--ghost:hover { background: var(--text); color: var(--bg); }

/* ---- Sections -------------------------------------------------------- */
/* Every section now needs its OWN ground: anything transparent lets the
   accent body through, which is the point but only where intended. */
.section { padding: clamp(72px, 9vw, 132px) 0; background: var(--bg); }
.section--alt { background: var(--bg-alt); }

/* =====================================================================
   1. HERO
   ⚠️ REBUILT 2026-09-19. The first version put a full-bleed photograph
   under a colour wash. That is not what the reference does and it was the
   reason the two pages looked nothing alike, despite the structure notes
   being right. The measurement was there from the first pass and was
   ignored: `getComputedStyle(body).backgroundColor` on the reference is a
   FLAT saturated ground, and the photograph is a small CONTAINED portrait
   card floating in it with a great deal of empty space around it.

   Three things carry the look, and none of them is the image:
     1. a flat ground, edge to edge
     2. everything hard against a 40px gutter, with NO max-width cap, so
        the wordmark and the statement sit at the very left of the screen
     3. a lot of nothing. The image occupies about 5% of the area.
   ===================================================================== */
.hero {
  position: relative;
  min-height: 100svh;
  display: grid;
  /* TWO in-flow children now: the statement and the foot. The logo/menu row
     moved out to a page-level fixed bar, and .hero__mid is absolutely
     positioned so it leaves the grid entirely. */
  grid-template-rows: auto 1fr;
  gap: clamp(20px, 2.6vw, 40px);
  padding: clamp(104px, 12vh, 150px) 0 30px;   /* clears the fixed bar */
  /* The card's resting size and float are TOKENS, not literals, because the
     phone layout has to move both and the keyframes must stay single-source.
     A second copy of `card-bleed` inside a media query is how these two
     drift apart. */
  --card-w-rest: clamp(190px, 23vw, 330px);
  --card-mb-rest: 22vh;
  --card-mb-end: 0px;
  background: var(--accent);      /* FLAT. No image behind the type. */
  color: #fff;
  overflow: clip;
}
/* The hero ignores the centred wrap: the reference runs full width with a
   fixed gutter, which is why its wordmark sits at the screen edge and mine
   sat 184px inside it on a wide display. */
.hero .wrap { max-width: none; width: 100%; padding-inline: var(--gutter); }

/* The real Quietwork mark, white on the accent. The reference sets its
   identity as type, but Quietwork has an actual logo and `brand-rules.md`
   is explicit that the SVG is used for it, never a typeset substitute. This
   is one of the places the reference is followed in structure (identity at
   the top-left, above the statement) and not in execution. */
.hero__mark { width: clamp(112px, 9vw, 150px); height: auto; display: block; }

/* ⚠️ Weight 400 and a ~12ch measure, both corrected after comparing the two
   heroes side by side. The reference sets "We turn / strategy / into growth":
   REGULAR weight, and two or three words a line. A heavier weight or a wider
   measure and it stops reading as the same idea, which is what happened at
   700/14ch and again at 500/14ch. The short line is doing as much work as
   the size is. */
.hero__statement {
  margin: 0;
  font-size: clamp(2.2rem, 5.4vw, 4.6rem);
  font-weight: 400;
  line-height: 1.06;
  letter-spacing: -0.035em;
  /* 15ch, not 12: "Design. Build. Automate." is meant to break in two,
     "Design. Build." then "Automate.", and 14 characters need the room.
     At 12ch it broke into three lines. */
  max-width: 15ch;
}

/* The image is an object IN the space, not the ground of it. */
/* ⚠️ The card is a LAYER anchored to the bottom edge of the blue, not a
   block in the flow. Owen: "image should be anchored to the bottom of the
   blue section." Previously it was centred in a flex row, so when it grew
   there was a band of blue left between its bottom edge and the copy below.
   Anchored here, the expanded image runs to the section's bottom edge and
   the label, note and button sit OVER it, which is what the reference does. */
.hero__mid {
  position: absolute; inset: 0; z-index: 1;
  pointer-events: none;
}
/* ⚠️ Positioned explicitly, NOT by grid alignment. With `align-items: end` a
   grid item 2187px tall inside a 1000px track overflowed DOWNWARD: measured
   card bottom 1387 against a hero bottom of 200. `bottom: 0` is unambiguous,
   so the excess height goes up and `overflow: clip` on .hero takes it, which
   is what keeps the subject locked as the image scales. */
.hero__card {
  position: absolute;
  bottom: 0; left: 50%;
  translate: -50% 0;
}
/* Everything else rides above the image. */
.hero > .wrap, .hero__foot { position: relative; z-index: 2; }
/* ⚠️ NO SCRIM. Owen: "lets not let the bottom soften." The reference runs its
   image crisp to the bottom edge of the section and lets the picture's own
   flat areas carry the type. A gradient here was doing the opposite: fading
   the photograph into flat colour exactly where it should be most present.

   It is safe to drop because of where the crop lands. The source is 941x1671
   into a 3:4 box, so cover shows 1255 of its 1671 rows, and `object-position
   50% 16%` puts the visible window at rows 67..1322. Row 1322 is the lower
   torso, which is black, over a ground that is the accent. White sits on
   either at 5.10:1 or better. The orange face is around rows 250..950, well
   above where the foot copy sits.
   ⚠️ Re-check this if the image is replaced: the guarantee is a property of
   THIS photograph's composition, not of the layout. */
.hero__foot { position: relative; align-self: end; }
/* The card grows to full bleed as the hero scrolls away. Scroll-driven, so
   there is no JavaScript and it cannot run away from the scroll position.
   ⚠️ .hero carries `overflow: clip`, never `hidden`: hidden would make the
   hero a scroll container and view() resolves against the nearest one, which
   would pin this at a fixed progress. */
/* ⚠️ Driven by the ROOT SCROLLER, not by view(). A view() timeline on the
   hero looked right and rendered the card full width at rest, because the
   hero is 100svh and `contain` has NO USABLE LENGTH when the subject is
   taller than the scrollport: the animation simply parks at its end state.
   scroll(root block) with an explicit range is unambiguous, and 0 to 92vh
   means the card has finished growing about the time the hero has gone. */
@supports (animation-timeline: scroll()) {
  @media (prefers-reduced-motion: no-preference) {
    .hero__card {
      animation: card-bleed linear both;
      animation-timeline: scroll(root block);
      /* ⚠️ Placement is MEASURED off the reference, not chosen. Probed at a
         1000px viewport: its image is still 328px at scrollY=120 and already
         fully 1640 at scrollY=300. The entire transition happens between
         roughly 12vh and 30vh, which is far sooner than it feels like it
         should be. A first pass used 46vh..76vh, so this build had not even
         STARTED by the point the reference had finished. Owen: "it needs to
         happen sooner."
         The hard ease-in-out stays: it is what gives the hold-then-snap. */
      animation-range: 13vh 31vh;
      animation-timing-function: cubic-bezier(.76, 0, .24, 1);
      will-change: width, margin-bottom;
    }
  }
}
/* ⚠️ THE ASPECT RATIO NEVER CHANGES. Measured on the reference at scrollY=780:
   the expanded image is 1640x2187, which is 3:4, the SAME ratio as the 288x384
   card at rest. It does not morph, it scales, and the hero clips whatever will
   not fit. This build animated 3:4 to 16:9, which re-crops the photograph as it
   grows and slides the subject down the frame. Owen: "you're shifting the focus
   down. The original locks it to the bottom as if it's a static element."
   Keeping one ratio is what makes it read as a static thing being enlarged
   rather than a frame being reshaped around a moving picture.

   The gap beneath still animates: at rest the card FLOATS with air under it,
   which is the reference's resting composition (image bottom at 642 in a 900
   viewport, ~258px below). Expanded, it sits flush to the bottom of the blue
   and the excess height overflows UPWARD, where .hero's `overflow: clip`
   takes it. */
@keyframes card-bleed {
  from { width: var(--card-w-rest); margin-bottom: var(--card-mb-rest); }
  to   { width: 100vw;              margin-bottom: var(--card-mb-end); }
}
.hero__mid img { max-height: 100%; }
.hero__card {
  width: var(--card-w-rest);
  aspect-ratio: 3 / 4;
  margin-bottom: var(--card-mb-rest);   /* resting gap, and the no-animation fallback */
  overflow: clip;
  border-radius: var(--radius-sm);
}
/* The image renders as supplied, with ONE exception recorded below. Owen,
   2026-09-19: "stop changing the image. Just use the one I'll give you." If a
   future file needs matching to the ground, that is a change to the file, not
   something to be invented here in CSS.

   No crop offset: 1082x1454 is a ratio of 0.744 against a 3:4 box, so `cover`
   has essentially nothing to trim. */
.hero__card img {
  width: 100%; height: 100%;
  object-fit: cover; object-position: 50% 50%;
}
/* ⚠️ THE ONE EXCEPTION, and it is only above the phone. Owen: "on the desktop
   version, just add a slight filter that makes it a little bit darker so we
   can see the edges."

   The two treatments want opposite things from the same file. On a phone the
   picture is FULL BLEED, so the graded top edge is the whole point: it is what
   stops the picture meeting the flat accent in a visible step. Above 760 the
   picture is a CONTAINED object with a 6px radius, and there the same grading
   dissolves the card into the ground, so the edge it was drawn to hide is the
   edge that has to read.

   0.92 is the smallest value that does it. Measured against the accent: the
   left edge goes 11.9 dE to 15.7 and the top 8.1 to 9.2. 0.96 moved the top by
   0.1 dE, which is nothing; below 0.90 the picture starts reading as graded
   rather than as itself. */
@media (min-width: 900px) {
  .hero__card img { filter: brightness(0.92); }
}

.hero__foot {
  display: flex; align-items: flex-end; justify-content: space-between; gap: 32px;
}
/* 60ch, so each sentence gets its own line as in Owen's comp. At 42ch the
   second sentence broke three ways and left "works." alone on the last. */
.hero__note { max-width: 60ch; margin: 0; font-size: 0.9375rem; color: rgba(255,255,255,0.92); }
.hero__lbl { margin: 0 0 8px; }
/* =====================================================================
   THE HERO BELOW A WIDE DESKTOP. Rebuilt 2026-09-19 after Owen: "I don't
   think we've thought about the responsive state quite well enough."

   ⚠️ TWO REAL DEFECTS, both measured rather than eyeballed.

   1. **302px of dead blue** between the statement and the card at 440x956.
      The card is `clamp(190px, 23vw, 330px)` and 23vw of a phone is 101px, so
      it sat on the 190px FLOOR: a stamp of a picture pinned 15px above the
      copy with a third of the screen empty above it. "A small object in a lot
      of space" is the composition on a desktop; at phone width the space
      stops reading as space and reads as a hole.

   2. **The expanded picture ran under the copy and the white type died on
      it.** The CSS above explains that there is no scrim because THIS
      photograph's bottom rows are dark, and it says in writing to re-check
      that if the image is replaced. The image was replaced on 2026-09-19 and
      nothing re-checked it. Sampled through a canvas (draw the natural image,
      map each text pixel back through the `cover` transform, take the white-on-it ratio
      against every pixel underneath), at 32% of a viewport height of scroll:

        width   label under 4.5:1   note under 4.5:1   worst
        440     100%                100%               1.00:1
        820      63.8%               34.1%             1.37:1
        1024     58.7%               25.0%             1.42:1
        1280     25.7%                2.4%             1.44:1
        1440      5.8%                0%               1.53:1

      It is not a phone bug. The overlap only works on a wide screen, because
      the mockups sit in the middle of the frame and only a wide viewport puts
      the bottom-left copy outside them. Below roughly 1400 the label is
      sitting on white UI.

   THE FIX IS COMPOSITION, NOT A SCRIM. Softening the picture to rescue the
   type would buy the type back by doing the thing Owen rejected outright
   ("lets not let the bottom soften"). Instead, below 1400 the hero becomes
   three bands in reading order: statement, copy, picture. The copy then sits
   on FLAT ACCENT at every scroll position, which is 5.10:1 by construction
   and cannot drift when the image is swapped again.

   `align-content: space-between` shares the leftover height between the three
   bands instead of pooling it all in one gap, which is what kills the hole.
   ===================================================================== */
@media (max-width: 899px) {
  .hero {
    grid-template-rows: auto auto auto;
    align-content: space-between;
    padding-bottom: 0;                    /* the picture runs to the edge */
    --card-mb-rest: 0px;
    /* ⚠️ ONE expression for the whole range, not a value per breakpoint. The
       vw term sizes it on a phone, the vh term stops it overrunning a short
       screen, and the 330px cap keeps it the same object it is on a desktop.
       A separate value per query gave a 40% jump in the picture between a
       tablet and a small laptop, which is the kind of thing that only shows
       up while resizing and then cannot be unseen. */
    --card-w-rest: min(66vw, 30vh, 330px);
  }
  .hero__foot { grid-row: 2; }
  /* The picture stops being a layer OVER the hero and becomes the last band
     OF it: a fixed-height window that the card grows inside. The window is
     exactly the resting card, so nothing moves at rest, and `overflow: clip`
     (never `hidden`, which would make it a scroll container and pin every
     view() timeline inside it) takes the excess as the card widens. */
  .hero__mid {
    position: relative; inset: auto; grid-row: 3;
    width: 100%;
    height: calc(var(--card-w-rest) * 4 / 3);
    overflow: clip;
  }
  /* ⚠️ Centre-anchored here, not bottom-anchored. Bottom-anchored, the growth
     spills upward only, so the window keeps the BOTTOM of the frame and the
     mockups are cut off at the chin. Centred, it spills both ways and the
     window holds the middle of the picture, which is the part worth showing.
     Measured at 440 wide: 587px of picture in a 382px window, clipped 103 top
     and 102 bottom. The ratio is still fixed, so the subject does not walk as
     it scales. */
  .hero__card { top: 50%; bottom: auto; translate: -50% -50%; }
}

/* ===== THE PHONE GOES FULL BLEED, and this is the second revision ========
   Owen, on the first version of this band: "is that the best solution?" It
   was not, and the reason is a rule he set himself about the lede wordmark:
   **the honest choices are a confident overhang or a clean fit inside the
   gutters, because "almost fits" reads as a mistake.**

   That first version put a 287px card in a 440px screen, centred, and ran it
   flush to the bottom edge. So the picture was CONTAINED on one axis and
   FULL BLEED on the other: 76px of blue either side, nothing underneath. It
   also left the hero's leftover height pooled in two 106px gaps, which is the
   hole coming back in a politer form.

   A contained card is right where it reads as contained. At 1280 the card is
   258px of a 1280px screen, 20%, and it is plainly an object floating in a
   field. At 440 it is 65% of the width, which is not an object in a field and
   not a full-bleed image either. So the phone takes the other honest option:
   the picture fills everything below the copy, edge to edge to the bottom,
   and the leftover height goes INTO it rather than around it.

   `1fr` on the third row is what does that. There is no dead blue left to
   distribute, because the picture is what absorbs it.
   ===================================================================== */
@media (max-width: 899px) {
  .hero {
    /* The picture is no longer sized by the leftover space: it is a full
       screen of its own, so the rows are all `auto` and the hero runs past
       100svh on purpose. */
    gap: clamp(28px, 6vh, 72px);
  }
  /* One column all the way down, wherever the picture is full bleed. */
  .hero__foot { flex-direction: column; align-items: flex-start; gap: 22px; }
  /* ⚠️ A FULL SCREEN of picture, not the leftover space. This is the third
     version of this band and it is the one drawn in Owen's comp: the copy
     sits at the top of the first screen, the picture starts under it and
     PEEKS, and scrolling fills the screen with it before the next section
     arrives. A band sized by the leftover height can never do that, because
     whatever is left over after the copy is by definition less than a screen.
     The hero is therefore about 1.5 screens tall on a phone. That is the
     design, not an overflow. */
  .hero__mid { height: 100svh; }
  /* ⚠️ `animation: none` on purpose, and it must stay after the @supports
     block that sets it. Growing the WIDTH is meaningless once the picture is
     already edge to edge: the keyframes would animate 440px to 440px and the
     hero would simply sit still. The gesture moves to the image inside. */
  .hero__card {
    animation: none;                    /* card-bleed grows the WIDTH; see below */
    position: absolute; top: 0; left: 0; right: 0; bottom: auto;
    width: 100%; height: 100svh;
    aspect-ratio: auto; translate: none; margin: 0;
    border-radius: 0;
  }
  /* ⚠️ THE SEAM, closed in CSS rather than in the file. Full bleed, the
     picture's top edge butts straight onto the flat accent, and the two blues
     are 8.3 dE apart (#3055E3 against #2764EC): far enough to read as a line,
     close enough that the line reads as a mistake rather than as the edge of a
     photograph.

     This is a gradient OVER the picture, not alpha inside it. Owen's first
     thought was that a transparent fade meant going back to PNG, which is why
     this is worth writing down: the fade costs nothing, the file stays a
     177KB JPEG, and because it is painted from `var(--accent)` it follows a
     palette swap. Matching the colour in the file would work equally well and
     would tie the asset to this one accent; `palettes.css` still carries
     indigo #4A5EDA, so that coupling is real.

     12% of the card, which is about 71px at rest and 115px expanded. The
     picture's own top band is flat for roughly 81px at that scale, so the
     gradient dies out before it reaches anything with detail in it: it tints
     flat colour into flat colour and never touches the mockups.

     ⚠️ Below 900 only, which is wherever the picture is full bleed. Above it
     the picture is a contained object and the edge is supposed to read; that
     is what `brightness(0.92)` is for. The two rules want opposite things and
     both are deliberate. */
  .hero__card::after {
    content: "";
    position: absolute; inset: 0 0 auto 0;
    height: 12%;
    background: linear-gradient(var(--accent), transparent);
    pointer-events: none;
  }
}
/* ⚠️ THE PHONE GROWS HEIGHT, THE DESKTOP GROWS WIDTH, and it has to be that
   way round. On a desktop the picture is a card in a field, so widening it to
   100vw is the whole gesture. On a phone it is already edge to edge at rest,
   so widening it would animate 440px to 440px and the hero would sit still.
   What changes on a phone is how much of the SCREEN it owns.

   The two end states are both exact rather than chosen:

     from  height = 100vw * 1.3438   the picture at width-fit. 1454/1082 is
                                     the source ratio, so the whole
                                     composition is visible and the mockups
                                     are small. This is the peek.
     to    height = 100svh           `cover` then crops to the middle 62% of
                                     the frame and the mockups fill the
                                     screen. This is the hero.

   `min()` on the from-value so a landscape phone never starts taller than the
   screen, and the static height is the END state, so a browser with no
   scroll-driven animation and anyone who asks for reduced motion gets the
   full-screen picture rather than a band with a hole under it.

   ⚠️ Nothing outside the band moves. The band is a fixed 100svh and the card
   is absolute inside it, so this animates no layout that anything else
   depends on: it cannot shove the page around under a thumb, and no copy can
   ever end up on top of the picture. */
@supports (animation-timeline: scroll()) {
  @media (max-width: 899px) and (prefers-reduced-motion: no-preference) {
    .hero__card {
      animation: card-fill linear both;
      animation-timeline: scroll(root block);
      /* ⚠️ The range END is not a feel, it is the moment the band's top edge
         reaches the top of the screen: that is when the picture can actually
         own the whole viewport, and finishing later means it is still growing
         while it is already full width and reads as lag. The copy block is
         about half a screen tall on every phone measured (475 of 956, 450 of
         690), so 50vh lands on it within a few pixels. */
      animation-range: 0 50vh;
      animation-timing-function: cubic-bezier(.76, 0, .24, 1);
      will-change: height;
    }
  }
}
@keyframes card-fill {
  from { height: min(calc(100vw * 1.3438), 100svh); }
  to   { height: 100svh; }
}
/* =====================================================================
   THE IN-BETWEEN WIDTHS: TABLET LANDSCAPE TO SMALL LAPTOP (900 to 1400)
   Owen: "the big desktop hero looks good, the mobile hero looks good, but the
   in-between resolutions that are going to fall into small screens and iPads
   and stuff look awful."

   He was right, and the reason is that they had the PHONE's structure at a
   DESKTOP's proportions. Measured at 1024x768: a 230px picture stranded at
   the bottom centre of a 1024px field, flush to the fold, with roughly 100px
   of dead blue above it and another 100px between the statement and the copy.
   Three stacked bands work on a phone because the screen is narrow and tall.
   At 1024 they leave a column of nothing down the middle.

   So these widths take the WIDE DESKTOP's composition instead, which is the
   other thing Owen says reads well: statement top left, picture floating in
   the field, copy along the bottom.

   ⚠️ What stopped that composition being used here was contrast, not taste.
   Above 1400 the picture expands to full bleed and the copy sits ON it, which
   measures 5.8% of the label under 4.5:1 at 1440 and 63.8% at 820. The fix is
   not to restructure, it is to CLIP: the picture lives in a region that stops
   short of the copy, so it can grow to the full width of the screen and still
   never reach the type. Same mechanism as the phone band, different position.
   ===================================================================== */
@media (min-width: 900px) and (max-width: 1400px) {
  .hero {
    /* back to the wide desktop's two rows: statement, then the foot pinned
       to the bottom of a 1fr track */
    grid-template-rows: auto 1fr;
    align-content: normal;
    padding-bottom: 30px;
    /* ⚠️ The vh term is what keeps the picture clear of the statement on a
       short laptop. At 1024x768 the vw term alone gives 246px, which puts the
       top of the picture 14px ABOVE the bottom of the statement; 30vh brings
       it to 230 and the two clear each other by 41px. */
    --card-w-rest: min(clamp(240px, 24vw, 330px), 30vh);
    --card-mb-rest: 0px;
    /* The height reserved for the copy at the bottom. The picture's region
       stops here, so the two can never meet however far it grows. */
    --hero-copy-band: clamp(170px, 22vh, 250px);
  }
  .hero__foot { grid-row: auto; }
  .hero__mid {
    inset: 0 0 var(--hero-copy-band) 0;
    overflow: clip;              /* clip, never hidden: hidden is a scroll container */
  }
  .hero__card { top: auto; bottom: 0; left: 50%; translate: -50% 0; }
}
/* ⚠️ The TYPE is the only phone-specific thing left here. The stacked foot
   moved up to 899 with the rest of the full-bleed treatment (Owen, at 768:
   "even at that resolution, the See the work button should be underneath the
   copy paragraph"). The earlier reasoning, that at 899 the foot still has room
   for its row, was the wrong test: it is not about whether the row FITS, it is
   that a button beside the copy reads as a second column, and under a
   full-bleed picture the hero should be one column all the way down. */
@media (max-width: 899px) {
  /* 35px on a 440px screen is a caption, not a hero. The desktop clamp floors
     at 2.2rem because its vw term is sized for a desktop; below 900 the type
     gets its own vw term instead of inheriting a floor meant for something
     else. */
  /* ⚠️ The cap is 3.04rem so the two clamps MEET at the boundary. The phone
     term is 11vw and the desktop term is 5.4vw, which at 900 is 48.6px, and
     3.04rem is 48.6px. Without that the headline SHRANK as the screen grew:
     measured 57.6px at 760 and 41.5px at 768, a 28% drop from eight pixels of
     extra width. Found by sweeping the range rather than by looking, because
     nobody resizes a browser across 764 to notice it. */
  .hero__statement { font-size: clamp(2.6rem, 11vw, 3.04rem); }
}

@media (max-width: 760px) and (max-height: 720px) {
  .hero { padding-top: 88px; }
}

/* =====================================================================
   2. STAT ROWS
   Hairline rows, label left, value hard right. This is the reference's
   "Marketing 30% / Social Media 50%" block, and it lands on Quietwork's
   existing primitive exactly: ruled rows, never cards.
   ===================================================================== */
/* ⚠️ THE refinement, and the thing this build got wrong. On the reference
   the stat rows are 442px inside an 884px content column: EXACTLY HALF,
   left-aligned, with 442px of air to their right. This build ran them the
   full width of the column and hard to the right edge, which is why the
   section read as unresolved. Owen: "it's kind of full width and doesn't
   quite know where it's putting stuff." A block that stops deliberately
   short reads as placed; one that fills its container reads as default. */
.stats { margin-top: clamp(40px, 5vw, 72px); width: 50%; }
@media (max-width: 1100px) { .stats { width: 100%; } }

/* Row pitch measured on the reference: label baselines 74px apart. The label
   box is 16px on 1.4 = 22.4px tall, so 74 = 22.4 + 2p + 1px rule gives p=25.3.
   At 22px the rows came out at a 67px pitch.
   ⚠️ The reference's labels are one-word categories ("Marketing"), so its rows
   are always single-line. These labels carry credentials instead, so one of
   them wraps. That is a content difference and the substance wins: the labels
   were cut to two words to force single lines and that gutted the BT title,
   which is the point of the row. The value dropping from 38px to 18px is what
   made the longer labels fit at all. */
.stat {
  display: flex; align-items: baseline; justify-content: space-between; gap: 24px;
  padding: 25px 0;
  border-top: var(--hair);
}
.stat:last-child { border-bottom: var(--hair); }
/* Same rule: the 28px numeral beside it is what separates label from figure,
   so the label does not also need to be greyed. */
.stat__k { font-size: 1rem; line-height: 1.4; color: var(--text); }   /* reference: 16px / 22.4px */
.stat__v {
  /* ⚠️ 18px, weight 400. Measured on the reference, where the value is only
     TWO pixels larger than its label and no bolder. This build ran it at up to
     38px and w500, more than twice the size, which inflated every row and
     squeezed the label column until the text wrapped. The numbers do not need
     to shout; the rule and the right alignment already make them read. */
  font-size: 1.125rem;
  font-weight: 400; letter-spacing: -0.01em; line-height: 1;   /* the reference's numerals run w500 */
  font-variant-numeric: tabular-nums lining-nums;
}
/* ⚠️ Declared AFTER the base rules, and that is load-bearing. Every selector
   here is (0,1,0), the same as the rule it overrides, so source order is the
   only thing deciding. Written above them first and it measured 18px values
   and 25px padding, i.e. nothing applied, which looks exactly like a media
   query that is not matching. Second time in this file; the quote crop was
   the first. */
/* ⚠️ THREE COLUMNS ON A PHONE, and the type sizes have to change with the
   layout, not survive it. Stacked as full-width rows the figures read fine,
   because the hairline and the hard-right alignment are what make a number
   look like a number: that is the reference's trick and it is why the value
   there is only two pixels larger than its label. Side by side, both of those
   cues are gone, so the size has to do the work instead. Hence 28px against a
   13px label here, and 18px against 16px on a desktop. Same component, two
   different jobs.
   The rule moves from each row to the group, since three columns have nothing
   to separate vertically. Column-reverse rather than reordered markup, so a
   screen reader still hears the label before the figure. */
@media (max-width: 760px) {
  .stats {
    display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 18px;
    border-top: var(--hair);
    padding-top: 22px;
  }
  .stat {
    flex-direction: column-reverse; align-items: flex-start;
    gap: 8px; padding: 0; border: 0;
  }
  .stat:last-child { border-bottom: 0; }
  .stat__v { font-size: 1.75rem; line-height: 1; }
  .stat__k { font-size: 0.8125rem; line-height: 1.35; }
}

/* =====================================================================
   3. WORK CARDS
   The reference runs a horizontal carousel of tall cards with arrow
   controls. Kept as a real overflow scroller with scroll-snap so it works
   without JavaScript and can be swiped on a phone.
   ===================================================================== */
/* ⚠️ The card width is a cue, not a preference. The right column is 892px at
   1440, and 320 + 20 gap divides into it 2.6 times, so a card is ALWAYS part
   visible at the right edge. That is what tells a visitor the row scrolls.
   The live site learned this the hard way: at 400px tiles, 3 cards plus 2
   gaps came to 1268 in a 1280 rail, the fourth sat entirely offscreen, and
   the row read as a finished three-up grid that simply would not move.
   The mask makes the cut look authored rather than clipped. */
.rail-scroll {
  display: grid; grid-auto-flow: column; grid-auto-columns: 320px;
  gap: 20px;
  overflow-x: auto; overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;
  -webkit-mask-image: linear-gradient(90deg, #000 0, #000 calc(100% - 64px), transparent 100%);
          mask-image: linear-gradient(90deg, #000 0, #000 calc(100% - 64px), transparent 100%);
}
.rail-scroll > * { scroll-snap-align: start; }
@media (max-width: 620px) { .rail-scroll { grid-auto-columns: 78vw; } }

/* ⚠️ AN INDICATOR, NOT A CONTROL, and it runs at every width (Owen, after
   seeing it on the phone: "let's have that horizontal scroll indicator on the
   desktop one as well"). A thumb whose WIDTH is the visible fraction of the
   rail and whose POSITION is how far through it you are, so it is a true map
   of the scroller rather than a decoration that happens to move. The two
   custom properties are set from the scroll handler; their defaults are a
   deliberate 30% at the left, so with no JavaScript at all the rule still
   reads as "there is more to the right" rather than disappearing.
   Measured: thumb 18.16% and 71px at 440 (392 of 2159 visible), 44.5% at 1440
   (892 of 2006), finishing with its right edge exactly on the track's. */
/* ⚠️ `min-width: 0` is not optional. A grid item whose child is an overflow
   scroller takes its AUTO minimum size from that child's content, so without
   this the wrapper is sized by six 320px cards and the column blows past its
   track. `.rail-scroll` carried this implicitly by being the grid item itself;
   wrapping it moved the requirement up a level. */
.rail { min-width: 0; }
.rail-prog {
  position: relative;
  height: 2px; margin-top: 16px;
  background: var(--line);
  border-radius: 2px; overflow: clip;
}
.rail-prog i {
  position: absolute; top: 0; bottom: 0;
  left: var(--x, 0%); width: var(--w, 30%);
  background: var(--accent);
  border-radius: inherit;
}
/* ⚠️ The native scrollbar goes, because it would sit directly under this and
   say the same thing twice. The cost is real and worth stating: on a desktop
   that scrollbar was DRAGGABLE and this is not. Everything else still scrolls
   the rail (wheel, trackpad, shift-wheel, and tabbing through the cards, which
   are links), so what is lost is dragging a 2px bar with a mouse. If that
   matters, the indicator has to become a real control with a hit area, a
   keyboard story and a role, which is a different component from this one. */
.rail-scroll { scrollbar-width: none; padding-bottom: 0; }
.rail-scroll::-webkit-scrollbar { display: none; }

.card {
  display: flex; flex-direction: column;
  text-decoration: none;
  border-radius: var(--radius-lg);
  overflow: clip;
  background: var(--tile);
}
.card__media { aspect-ratio: 4 / 5; overflow: clip; }
.card__media img { width: 100%; height: 100%; object-fit: cover; }
.card__body { padding: 18px 20px 22px; }
.card__t { font-size: 1.125rem; font-weight: 400; letter-spacing: -0.02em; }
.card__d { margin: 6px 0 0; font-size: 0.875rem; color: var(--text-muted);
  /* Same reason as the panel rows: short copy in a ~300px tile orphans its
     last word readily. Measured "part." alone on the RNBW tile at 390. */
  text-wrap: pretty; }

/* =====================================================================
   4. ACCORDION ROWS
   Same primitive as the stats: a hairline, a label, a + on the right.
   <details>/<summary> so it opens with no script at all.
   ===================================================================== */
.acc { border-top: var(--hair); }
.acc__item { border-bottom: var(--hair); }
.acc__item > summary {
  display: flex; align-items: center; justify-content: space-between; gap: 24px;
  padding: 24px 0;
  cursor: pointer;
  list-style: none;
  font-size: clamp(1.0625rem, 1.4vw, 1.25rem);
  font-weight: 500; letter-spacing: -0.02em;   /* reference: w500 */
}
.acc__item > summary::-webkit-details-marker { display: none; }
.acc__item > summary::after {
  content: "+"; flex: none;
  font-size: 1.4rem; font-weight: 400; line-height: 1;
  color: var(--text-muted);
  transition: transform .18s ease;
}
.acc__item[open] > summary::after { transform: rotate(45deg); }
.acc__body { padding: 0 0 26px; max-width: 64ch; color: var(--text-muted); }

/* ⚠️ <details> snaps open with no animation, which is what it does natively
   and is easy to leave as-is. Animated here with ::details-content, which is
   the part of the element the browser reveals.

   `interpolate-size: allow-keywords` is what makes a transition TO `auto`
   possible at all; without it the height would have to be a fixed pixel value
   and that means measuring in JavaScript and re-measuring on resize.
   `content-visibility` is transitioned with `allow-discrete` so the content is
   not simply switched on at frame one, which would make the height animate
   underneath already-visible text.

   Where ::details-content is unsupported the accordion still opens, instantly,
   exactly as it did before. Nothing depends on the animation running. */
@media (prefers-reduced-motion: no-preference) {
  .acc__item::details-content {
    block-size: 0;
    overflow: clip;              /* clip, never hidden: the standing rule here */
    transition: block-size .38s cubic-bezier(.22, 1, .36, 1),
                content-visibility .38s allow-discrete;
  }
  .acc__item[open]::details-content { block-size: auto; }
}

/* =====================================================================
   5. PLANS
   Two panels side by side, the second one filled with the accent. The
   reference fills the second with its brand yellow and keeps black type;
   ours takes white, because the accent is dark.
   ===================================================================== */
.plans { display: grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: 20px; }
@media (max-width: 760px) { .plans { grid-template-columns: 1fr; } }
.plan {
  display: flex; flex-direction: column;
  /* Widened 2026-09-20 (Owen). ⚠️ Padding does NOT fix a one-word overhang:
     it narrows the measure, so it makes wrapping MORE likely, not less.
     The orphans are handled by `text-wrap: pretty` on the rows. This is
     air at the tile edge, which is a separate thing and was tight. */
  padding: clamp(28px, 3.4vw, 44px);
  border: var(--hair);
  border-radius: var(--radius-lg);
  background: var(--bg);
}
.plan--accent { background: var(--accent); border-color: var(--accent); color: #fff; }
.plan__t { font-size: 1.5rem; font-weight: 400; letter-spacing: -0.02em; }   /* reference: 24px w400 */
/* ⚠️ THE TWO PANELS SHARE ONE ROW RHYTHM. DO NOT DISTRIBUTE THE SLACK.
   The panels are a grid, so they stretch to the taller one and the foot is
   bottom-aligned; the free-call panel runs five rows against the audit's
   seven, so a MEASURED 159px gap opens above its foot at 1280.
   That gap was closed on 2026-09-20 by letting each row grow to take an equal
   share (rows went 45px to 71px on the short side), and Owen reversed it the
   same day: *"keep the spacing consistent on each box, i dont mind if theres
   a gap on the first one."* The rows read as a pair of lists side by side, so
   a row that is 71px in one panel and 45px in the other is the more visible
   fault, and the gap is the acceptable cost. **An equal share of the slack is
   not the fix here. Leave the gap.**
   Note also that raising the row padding does NOT close it either: the
   seven-row panel grows faster than the five-row one, so the two diverge. */
.plan__list { margin: 20px 0 0; padding: 0; list-style: none; flex: 1; }
.plan__list li { padding: 11px 0; border-top: var(--hair); font-size: 0.875rem;   /* reference: 14px */
  /* Short declarative rows orphan their last word readily: measured
     "follow-up." alone at 390 and "hand." alone at 1280. */
  text-wrap: pretty; }
.plan--accent .plan__list li { border-top-color: rgba(255,255,255,0.28); }
/* ⚠️ SOLID white, not the 0.82 muted white. Measured on this accent: white at
   0.82 alpha is 3.96:1, under the 4.5:1 this 14px label needs. There is no
   usable muted white on #2764EC, which is the same finding the footer records;
   hierarchy comes from size and tracking instead. */
.plan--accent .label { color: #fff; }
.plan__foot {
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
  margin-top: 26px; padding-top: 20px; border-top: var(--hair);
}
.plan--accent .plan__foot { border-top-color: rgba(255,255,255,0.28); }
.plan__price { font-size: 1.25rem; font-weight: 500; letter-spacing: -0.02em; }   /* reference: 20px w500 */

/* The rail heading is a QUESTION AND ITS ANSWER, so the break between the two
   sentences is structural, not decorative: each is its own block and wraps
   inside itself. A <br> would break in the same place at every width, which is
   the thing the two-line composition cannot survive on a phone. */
.two-q span { display: block; }


/* =====================================================================
   6. CLIENT MARKS
   ===================================================================== */
/* ONE row, always, which is the live site's standing rule for these ten
   marks. The gap is SOLVED, and solved against a MEASURED container rather
   than an arithmetic one. The marks total 576.3px at their ink-equalised
   heights. The column is 812px, not the 892 a first pass computed: the wrap
   is max-width 1360 INCLUDING its 40px padding either side, so the content
   box is 1280 and the right column is 1280 - 420 rail - 48 gap = 812.
   That leaves 235.7px for 9 gaps, a ceiling of 26.2px each. 24px comes to
   792.3 and leaves 19.7px of headroom. 30px missed by 34px and wrapped the
   tenth mark onto a second row. Measure the container, do not derive it. */
.marks { display: flex; flex-wrap: nowrap; align-items: center; gap: 24px; }
/* ⚠️ THE WRAP BREAKPOINT IS SOLVED, NOT CHOSEN, and it was wrong: it sat at
   900 when the row stops fitting at about 1300. The ten marks need 793px
   including gaps. The column they sit in is 65% of the content width, so it is
   614px at 1024 and the row ran 139px PAST THE VIEWPORT: the whole page
   scrolled sideways at every width between roughly 900 and 1300, on the live
   site, and the hero was getting the blame for how those widths looked.
   Found by diffing element rects against the viewport, not by eye. */
@media (max-width: 1340px) { .marks { flex-wrap: wrap; gap: 14px 28px; } }
/* ⚠️ Scaled as a SET, never re-solved per mark. The ten heights are
   ink-equalised (h = 36 * (median_ink / mark_ink) ^ 0.30) and that solve is a
   property of the files, so the phone multiplies all ten by one factor and
   the relationship between them survives. At full size they wrapped 4 / 4 / 2
   and the last row hung off the left like an afterthought; at 0.8 they run
   5 / 5. Picking new heights by eye here would quietly undo the solve. */
/* Scaled as a SET below the same threshold: 0.8 brings the row to 677px, which
   fits one line again from about 1120 up and wraps to two below it. */
@media (max-width: 1340px) { .marks { --mark-scale: 0.8; gap: 16px 24px; } }
.marks img { object-fit: contain; }

/* =====================================================================
   7. QUOTE
   ===================================================================== */
/* ⚠️ A CONTAINED panel, not loose parts. Previously the image and the text
   simply sat next to each other in the open, so the photograph read as
   floating and the quote ran the full width of the column. The reference puts
   its testimonial in a card, and containment is what stops a small portrait
   next to a long line of type looking accidental. Same panel treatment as the
   plans, so the page has one card primitive rather than two. */
.quote {
  display: grid;
  grid-template-columns: 220px minmax(0, 1fr);
  gap: clamp(20px, 2.4vw, 32px);
  align-items: stretch;
  padding: clamp(20px, 2.2vw, 28px);
  border: var(--hair);
  border-radius: var(--radius-lg);
  background: var(--bg);
}
@media (max-width: 760px) { .quote { grid-template-columns: 1fr; } }
.quote__media { border-radius: var(--radius-sm); overflow: clip; aspect-ratio: 3/4; }
.quote__media img { width: 100%; height: 100%; object-fit: cover; }
/* ⚠️ The 3:4 crop is a SIDEBAR ratio. Stacked on a phone it became a 318x424
   slab, the tallest single element on the page, and it pushed the quote it is
   supposed to introduce below the fold. Landscape keeps the lock recognisable
   at a fifth of the height. The offset is the subject: the padlock sits above
   centre in the frame and a dead-centre crop takes the shackle off. */
@media (max-width: 760px) {
  .quote__media { aspect-ratio: 16 / 10; }
  .quote__media img { object-position: 50% 42%; }
}
/* ⚠️ Declared AFTER the base rule on purpose. `.quote__media` and the media
   query version are both (0,1,0), so the later one wins and an override
   placed earlier in the file does nothing at all. It was written above the
   base rule first and measured 350x467, unchanged. */
/* w400, like every other heading here. This one was missed when the rest of
   the type came down off 700, which is why it looked heavier than the page. */
.quote blockquote {
  margin: 0;
  max-width: 24ch;
  font-size: clamp(1.3rem, 2vw, 1.85rem);
  font-weight: 400; letter-spacing: -0.025em; line-height: 1.18;
}
/* A real two-column grid. Flex-wrap with a 56px gap let the two blocks drift
   to opposite ends of a wide column and read as unrelated. */
.quote__meta {
  margin-top: clamp(20px, 2.4vw, 30px);
  padding-top: clamp(16px, 1.8vw, 22px);
  border-top: var(--hair);
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 22px 32px;
}
@media (max-width: 620px) { .quote__meta { grid-template-columns: 1fr; } }
.quote__meta p:not(.label) { font-size: 0.875rem; line-height: 1.45; }
/* The text column distributes across the panel instead of stacking at the
   top. The image sets the panel's height (220px at 3:4 is 293px tall), and
   without this the quote and its meta both sat in the upper half with a band
   of empty panel beneath them. Quote to the top, attribution to the bottom,
   so the two columns finish level. */
.quote__media + div { display: flex; flex-direction: column; justify-content: space-between; }
.quote__media + div > .quote__meta { margin-top: 0; }

/* =====================================================================
   8. FAQ, on ink with a rounded top edge
   ===================================================================== */
.faq {
  background: var(--bg-ink);
  color: var(--text-onink);
  /* Rounded at the BOTTOM, not the top, so the accent ground shows through
     the corners and beneath. Measured on the reference: 0 0 40px 40px. */
  border-radius: 0 0 var(--radius-section) var(--radius-section);
  padding: clamp(64px, 8vw, 116px) 0;
}
.faq h2, .faq .acc__item > summary { color: #fff; }
.faq .label { color: rgba(255,255,255,0.7); }
.faq .acc { border-top-color: var(--line-onink); }
.faq .acc__item { border-bottom-color: var(--line-onink); }
.faq .acc__item > summary::after { color: rgba(255,255,255,0.7); }
.faq .acc__body, .faq .lead { color: var(--text-onink-muted); }

/* ---- Chrome ---------------------------------------------------------- */
.skip-link {
  position: absolute; left: -9999px;
  background: var(--bg); color: var(--text); padding: 10px 14px; z-index: 99;
}
.skip-link:focus { left: 12px; top: 12px; }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.hero :focus-visible, .plan--accent :focus-visible { outline-color: #fff; }

/* =====================================================================
   SCROLL REVEALS
   The reference brings every section in as it enters the viewport. This
   build had none at all, which is most of why it felt static beside it.
   Scroll-driven, so there is no JavaScript and nothing can desynchronise
   from the scroll position.

   ⚠️ Content is visible BY DEFAULT. The whole block sits inside
   @supports + prefers-reduced-motion, so a browser without scroll-driven
   animation, or a visitor who asks for stillness, gets the finished state
   and never an empty page.

   ⚠️ `.card` is deliberately NOT in this list. The cards live inside
   `.rail-scroll`, which has `overflow-x: auto` and is therefore a scroll
   container, and `view()` resolves against the NEAREST one. Their timeline
   would be the horizontal rail rather than the page, so they would sit
   pinned at a fixed progress and, with opacity in the keyframes, could
   stay invisible. This is the documented trap on this project and it does
   not announce itself: nothing errors, the animation reports as running.
   ===================================================================== */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .stat,
    .acc__item,
    .plan,
    .quote,
    .marks,
    .split__rail > *,
    .stats,
    .lead,
    /* the supporting paragraph under a lead, added with the 2026-09-19 copy
       rewrite: without this it is the one static element in a revealed block */
    .lead + p {
      animation: rise-in linear both;
      animation-timeline: view();
      animation-range: entry 4% entry 46%;
    }
    /* Each stat row gets its own timeline, so they arrive one after another
       as each crosses the threshold. No delays to keep in sync. */
    .stat { animation-range: entry 6% entry 40%; }
  }
}
@keyframes rise-in {
  from { opacity: 0; transform: translateY(16px); }
  to   { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  * { animation: none !important; transition: none !important; scroll-behavior: auto !important; }
}

/* =====================================================================
   PORTED FOR THE LIVE SITE, 2026-09-19
   Everything above this line is the structure study verbatim. Everything
   below it is what a live page needs and a local study did not: the about
   block, the contact form, the cookie banner and the form states that
   `js/main.js` paints.

   ⚠️ This page does NOT load `site.css`. The two stylesheets define the same
   class names (.section, .split, .btn, .lead, .card) with different values,
   so loading both would mean 1,200 lines of accidental cascade. The homepage
   is self-contained on this file plus `palettes.css`; the other 21 pages are
   untouched on `site.css`. Until they are converted the site therefore runs
   TWO chrome variants, which breaks the "one nav, one footer" rule recorded
   in CLAUDE.md. That is a known, temporary consequence of converting the
   homepage first, not an oversight.
   ===================================================================== */

/* ---- About ----------------------------------------------------------- */
/* ⚠️ The media column was a flat 420px, which left the text column 308px: about
   36 characters at 17px, well under a readable measure. It survived while the
   About was one long paragraph, because a single block reads as a column
   whatever its width. The 2026-09-20 copy is four short paragraphs, and at 36ch
   those set as a ragged stack. The video is portrait (720x876) so it does not
   need the width; trading 60px of it back puts the text at roughly 44
   characters. Measure the TEXT column after any change here, not the video. */
.about { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 360px); gap: clamp(28px, 4vw, 64px); align-items: start; }
.about > div > p { text-wrap: pretty; }
@media (max-width: 900px) { .about { grid-template-columns: 1fr; } }
.about__media { margin: 0; }
.about__video {
  width: 100%; height: auto; display: block;
  border-radius: var(--radius-lg); background: var(--bg-alt);
}
.about figcaption {
  margin-top: 12px;
  font-size: 0.8125rem; line-height: 1.45; color: var(--text-muted);
}
/* ⚠️ A real control, not a hover. The loop is 20 seconds, well past the five
   at which WCAG 2.2.2 wants a mechanism to stop moving content, and hover is
   not a mechanism for a keyboard or screen reader user. `js/main.js` unhides
   it; it stays hidden under prefers-reduced-motion, where the video never
   starts in the first place. */
/* ⚠️ `display: block` with a top margin, so it sits on its own line under the
   caption rather than running on after the full stop (Owen: "the pause button
   should be on its own line, plus it should be an icon not a word"). Inline,
   it read as part of the sentence.
   The icon is drawn here rather than taken from an icon set: the site's rule
   is one mark, a 16px chevron, and a media control is the one place that
   cannot use it, so these two glyphs are the whole exception. */
.video__toggle {
  display: block;
  margin-top: 14px;
  width: 38px; height: 38px;
  padding: 0;
  display: grid; place-items: center;
  color: var(--text); background: transparent;
  border: 1px solid var(--line-strong); border-radius: 50%;
  cursor: pointer;
}
.video__ico { width: 14px; height: 14px; fill: currentColor; display: block; }
/* One of the two is always hidden. The button carries `data-paused` when the
   video is stopped, set by js/main.js alongside the aria-label. */
.video__ico--play { display: none; }
.video__toggle[data-paused] .video__ico--pause { display: none; }
.video__toggle[data-paused] .video__ico--play  { display: block; }
.video__toggle:hover { background: var(--text); color: var(--bg); border-color: var(--text); }

/* ---- The contact form ------------------------------------------------ */
/* The markup and the handler are the live site's, unchanged, because that
   handler already carries a honeypot, a bot-window delay that DELAYS rather
   than discards, and error states that never report success from inside a
   catch. Only the paint is new. */
/* ⚠️ Capped. The form sits in the page's wide column, which is 884px at 1440,
   and a text input that wide is hard to scan and looks like a mistake next to
   a 40 character name field (Owen: "this form is way too wide, copy above it
   is fine, but the fields can be much narrower"). The COPY above it keeps the
   full column: it is prose and wants its own measure. */
.form { display: grid; gap: 18px; max-width: 34rem; }
/* A HOOK, not a style. `js/main.js` finds the form by this class, so it is
   declared here rather than left as the one class on the page with no rule:
   an undefined class and a rule that matches nothing are the same fault, and
   both are invisible. */
.contact-form { display: grid; }
.field { display: grid; gap: 8px; }
.field > span { font-size: 0.8125rem; letter-spacing: 0.14em; text-transform: uppercase; color: var(--text-muted); }
.field input, .field textarea {
  width: 100%; padding: 13px 14px;
  font: inherit; font-size: 1rem; color: var(--text);
  background: var(--bg);
  border: 1px solid var(--line-strong);   /* 3.02:1, the floor for a control edge */
  border-radius: var(--radius-sm);
}
.field textarea { resize: vertical; min-height: 128px; }
.field input::placeholder, .field textarea::placeholder { color: var(--placeholder); }
.field input:focus-visible, .field textarea:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent); }
.form-submit { margin-top: 2px; }
/* main.js reaches for `.form-submit .btn` and paints its label, so the button
   keeps the live site's class as well as this file's. */
.btn--primary { background: var(--text); color: var(--bg); border: 0; cursor: pointer; font: inherit;
  display: inline-flex; align-items: center; gap: 10px;
  padding: 14px 20px; border-radius: var(--radius-sm);
  font-size: 0.6875rem; font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase; }
.btn--primary:hover { background: var(--accent); }
.btn--primary[disabled] { opacity: 0.6; cursor: default; }
.form-success, .form-error { display: none; font-size: 0.9375rem; line-height: 1.5; }
.form-success.visible, .form-error.visible { display: block; }
.form-success { color: var(--accent); }
/* ⚠️ Not a colour alone. The failure state carries a rule and a label as well,
   because colour is the one channel a visitor may not have. */
.form-error { padding-left: 12px; border-left: 2px solid #B3261E; color: #B3261E; }
.form-error a { color: inherit; }
.hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }

/* ---- Cookie banner ---------------------------------------------------- */
/* Carried over from site.css so the consent gate looks the same on this page
   as on the other twenty-one. `js/cookies.js` is unchanged and still the only
   thing that can load GA4. */
.cookie-banner {
  position: fixed; left: 16px; right: 16px; bottom: 16px; z-index: 150;
  display: none; flex-wrap: wrap; gap: 12px 20px; align-items: center; justify-content: center;
  max-width: 720px; margin: 0 auto;
  padding: 16px 20px;
  background: var(--bg); border: 1px solid var(--line); border-radius: 14px;
  box-shadow: var(--shadow-toast);
  font-size: 0.9375rem; color: var(--text);
}
.cookie-banner p { margin: 0; max-width: none; }
.cookie-detail {
  flex-basis: 100%; margin-top: 2px; padding: 12px 14px;
  background: var(--bg-alt); border-radius: 8px;
  font-size: 0.875rem; color: var(--text-muted);
}
.cookie-detail p + p { margin-top: 8px; }
.btn--sm { padding: 9px 16px; font-size: 0.8125rem; }
.btn--secondary { background: transparent; color: var(--text); border: 1px solid var(--line-strong);
  cursor: pointer; font: inherit;
  padding: 9px 16px; border-radius: var(--radius-sm);
  font-size: 0.6875rem; font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase; }
.btn--secondary:hover { background: var(--text); color: var(--bg); }

/* ---- The marker (PROTOTYPE, 2026-09-22) ---------------------------------
   One word in a display heading, filled with the accent and set in white.
   White on #2764EC is 5.10:1. Rules: display headings only, at most one per
   section, never on an accent ground, where it disappears.

   The fill is a ::before BEHIND the text so it can be drawn independently of
   the word. --p (0..1) is how much of the box is drawn. It is 1 by default, so
   with no scroll-driven animation support, or under reduced motion, the marker
   is simply there. The draw-in only exists inside the two guards below.
   A cursor lean was tried and removed (Owen: "adds nothing"). */
@property --p { syntax: "<number>"; inherits: true; initial-value: 1; }
.hi {
  position: relative; z-index: 0;
  color: #fff;
  padding: 0 0.12em 0.04em;
  margin: 0 -0.02em;
  white-space: nowrap;
}
.hi::before {
  content: ""; position: absolute; inset: 0; z-index: -1;
  background: var(--accent);
  border-radius: 0.1em;
  transform-origin: 0 50%;
  transform: scaleX(var(--p));
}
/* Headings here run at 1.02, and the filled box is taller than its line box,
   so it would sit on the lines above and below. Open the leading only where a
   marker is used. */
h2:has(.hi) { line-height: 1.16; }

@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {
    /* Drawn left to right as the heading scrolls in, like a marker pen. The
       word stays ink (black on the accent is 4.1:1, readable) until the fill
       has almost covered it, then snaps to white, so it is never white on
       white and never passes through a muddy grey. */
    .hi {
      animation: hi-draw linear both;
      animation-timeline: view();
      animation-range: entry 60% cover 38%;
    }
    @keyframes hi-draw {
      0%   { --p: 0; color: var(--text); }
      86%  { color: var(--text); }
      92%  { color: #fff; }
      100% { --p: 1; color: #fff; }
    }
  }
}
