/* ═══════════════════════════════════════════════════════════════════════
   Tempus — rebuild stylesheet
   Brand tokens are the repo's real values. They were read off style.css, the
   stylesheet the pre-rebuild site used — DELETED on 2026-09-09, once the last
   three hand-written pages moved onto this one and nothing loaded it any more.
   The values are unchanged by that; this line records where they came from, so
   nobody goes looking for a file that is now only in the git history.
   Typography follows Font Lab preset 1, "Keep, but tune": the existing
   faces (League Spartan + Inter) with the measure, size and line height
   corrected — the lab found the fonts were never the problem, the
   ~95 characters-per-line measure was.
   ═══════════════════════════════════════════════════════════════════════ */

:root{
  /* ── Brand colours — the single source of truth (button/colour spec §2) ──
     Four values, defined once, never written as a literal hex again anywhere
     below this block. The spec exists because the sibling support site has
     #091464 written out 96 times and #E6007E 92 times, which is exactly how a
     second, undocumented pink hover crept in there. Same drift had started
     here: .btn-primary:hover was #c80070, a third pink nobody had approved.
     It is now var(--brand-pink-hover).

     2026-09-08 — the approved palette lands, and it names two of these colours
     differently: the navy is --tile, the pink is --action, the pink hover is
     --action-press. Those names are added below as ALIASES pointing back here,
     NOT as fresh hex. Writing #091464 out a second time under a new name would
     leave the site running two colour systems side by side, which is precisely
     the drift this block exists to stop. One declaration, many names.

     DELETED 2026-09-08: --brand-blue-hover:#0A1880. It had zero consumers from
     the day it was written — nothing on this site ever hovered to a second
     navy. The approved palette's second navy (--tile-2:#0b1878) is deliberately
     NOT declared in its place, because it would have zero consumers too: the
     CTA band's middle gradient stop is #1a2a8a, and changing that is a visible
     change nobody has approved. Declare --tile-2 at the point something
     actually uses it, not before.
     ──────────────────────────────────────────────────────────────────────── */
  --brand-blue:#091464;
  --brand-pink:#E6007E;
  --brand-pink-hover:#C9006E;

  /* ── The approved palette (operator sign-off, 2026-09-08) ────────────────
     Surfaces, ink and action. Three entries are the brand colours above under
     their palette names; the rest are new. Every ratio quoted here was
     measured, not estimated, and the measurements are the reason several of
     these values are not the obvious ones:
       --ink #14161f            18.04:1 on canvas, 16.42:1 on parchment.
       --ink-muted #5b6070      6.27:1 on canvas, 5.70:1 on parchment. It
                                replaces #6b7280, which was 4.83:1 / 4.40:1 —
                                i.e. the OLD muted grey fails AA on the NEW
                                parchment. The two changes must never be
                                separated, or the retrofit ships a contrast
                                regression in text it never meant to touch.
       --ink-faint #8a8fa3      3.21:1 on canvas, 2.92:1 on parchment. This is a
                                DECORATIVE tier: illegal for normal-size text on
                                canvas, and illegal for text of ANY size on
                                parchment, because 2.92 misses even the 3:1
                                large-text bar. "Just use it at 24px" does not
                                rescue it on parchment.
       --action #e6007e         4.50:1 on canvas — it clears AA by 0.0008 — and
                                4.10:1 on parchment, which FAILS. Fills and
                                large text only, and never at reduced opacity.
       --action-press #c9006e   5.66:1 on canvas, 5.16:1 on parchment. The only
                                pink that is legal as normal-size text on both
                                grounds, which is why D2 makes it the inline
                                link colour. On navy it is 2.86:1 — never put it
                                on a dark surface.
       --action-on-dark #ff5cae 5.71:1 on the navy. Use it wherever pink meets a
                                dark surface; #e6007e there is 3.60:1 and fails.
       --on-dark-muted #b9bed6  8.80:1 on the navy, 10.43:1 on the footer.
       --hairline #e3e5ee       1.26:1 on canvas. Fine for a decorative card
                                edge (WCAG 1.4.11 exempts decoration) and NOT
                                fine as the only visible edge of a control — see
                                --form-border below.
       --hairline-hover #7b8093 The edge a card, tile or post card takes when
                                the pointer is over it. THREE numbers have to
                                clear 3:1 here, not one, and this value clears
                                all three:
                                  3.12:1  the STEP — resting edge (--hairline
                                          #e3e5ee) to hovered edge. This is the
                                          change a visitor actually perceives as
                                          the hover happening, and it is the
                                          hardest of the three to clear.
                                  3.92:1  the hovered edge on canvas
                                  3.57:1  the hovered edge on parchment
                                Both grounds are measured because cards sit on
                                BOTH surfaces; a value that passed on white
                                while failing on parchment would drop the cue on
                                half the site.
                                TWO VALUES THAT LOOK RIGHT AND ARE NOT, so
                                nobody re-derives them:
                                  --ink-faint #8a8fa3 — 2.92:1 on parchment. It
                                    fails outright, and being an existing token
                                    is not a reason to reach for it.
                                  #858a9e — 3.43 canvas / 3.12 parchment, so it
                                    clears the EDGE bar and looks like a pass.
                                    Its step is only 2.73:1. It was the
                                    front-runner until the step was measured.
                                For scale: the old #d8dbe2 was a step of 1.10:1
                                — roughly a tenth of what the guideline asks.
       --form-border #7b8093    The edge of an input, a select or a textarea.
                                Deliberately the SAME value as --hairline-hover
                                and measured the same four ways — 3.92:1 on
                                canvas, 3.57:1 on parchment, 3.79:1 on the
                                Ask-an-AI box's own #FAFBFD fill, and a 3.12:1
                                step away from --hairline so a field's edge
                                cannot be mistaken for a card's. Raised from
                                #d4d7de (1.44:1) on 2026-09-08; the full note is
                                on the token itself.
     Naming note: --tile is a COLOUR and .tile (~line 371) is a card class. They
     are unrelated — CSS keeps custom properties and class names in separate
     namespaces — but a grep for "tile" returns both.
     ──────────────────────────────────────────────────────────────────────── */
  --canvas:#ffffff;
  --parchment:#f3f4f9;
  --tile:var(--brand-blue);
  --ink:#14161f;
  --ink-muted:#5b6070;
  --ink-faint:#8a8fa3;
  --on-dark:var(--canvas);
  --on-dark-muted:#b9bed6;
  --action:var(--brand-pink);
  --action-press:var(--brand-pink-hover);
  --action-on-dark:#ff5cae;
  --hairline:#e3e5ee;
  /* Approved by the operator 2026-09-08: the card hover edge must clear 3:1.
     It is a NAMED token and not a fourth loose hex because three separate rules
     (.card, a.tile, .postcard) have to move together — when they were three
     literals, one of them was always going to be missed.
     If you change this value, the number to check FIRST is its contrast against
     --hairline directly above, not against the page background. That step is
     the binding constraint and it is the one that is easy to forget. */
  --hairline-hover:#7b8093;

  /* HOW A CARD ANSWERS THE POINTER. Requested by the operator 2026-09-09: the
     card should GROW slightly rather than slide upwards, which is what it did
     before ("I think that we should have the card actually increase in size a
     little bit").
     Named for exactly the reason --hairline-hover above is named — .card,
     a.tile and .postcard are one hover language and must move together. They
     used to lift by three different literals (-4px, -3px, -3px), which is how
     you end up with a row of cards that answer the pointer three ways.
     1.02 and not more. On the widest card on the site (~381px) it is 3.8px of
     growth per side; anything larger starts to overlap the neighbouring card
     rather than nudge it, and a marketing card that leaps reads as a bug.
     If you raise it, the two numbers to re-check are the grid gaps it has to
     stay inside (.cards 20px, .grid3 18px, .postgrid 22px) and whether the
     card's 1px border still rasterises sharply — see the note on .card:hover. */
  --card-hover-grow:1.02;

  /* Legacy names, kept as aliases so every existing rule in this file — and
     anything outside it that reads these — keeps working untouched. That
     "anything outside" is not hypothetical: _source/build.py's review-file
     chrome reads --font-display, --border-gray, --text-dark, --tempus-pink,
     --text-gray, --pad and --dark-blue directly. Delete one of these aliases
     and the review file silently loses its styling — no error, no build abort,
     because CSS just drops a declaration it cannot resolve. New rules should
     reference the palette names above. */
  --tempus-pink:var(--action);
  --dark-blue:var(--tile);
  --text-dark:var(--ink);
  --text-gray:var(--ink-muted);
  --light-gray:var(--parchment);
  --border-gray:var(--hairline);

  /* DELETED 2026-09-08: --white:#fff. It had exactly one consumer (the page
     background) while 45 literal #fff sat beside it doing the same job under no
     name at all. Those literals are now split between --canvas (a white
     SURFACE: the page, a card, a panel) and --on-dark (white INK sitting on a
     dark surface: the navy bar, the CTA band, the footer, a filled pink
     button). Same colour, two different jobs, and the job is what a future
     dark-surface pass will need to be able to tell apart. */

  --accent-blue:#3b82f6;
  --quote-teal:#2BB3A3;

  /* Control borders are deliberately DARKER than --hairline, and must stay that
     way. A hairline round a card is decoration and WCAG 1.4.11 exempts it; the
     border of an input is the only thing telling a user where the field is, so
     it has to carry its own weight. Declared here on 2026-09-08 because
     .askai-prompt-box has read var(--form-border,#D4D7DE) since the day it was
     written while --form-border was declared NOWHERE in the repo — it rendered
     only because of that inline fallback, so nobody ever noticed.

     DARKENED 2026-09-08 from #d4d7de, which measured 1.44:1 on canvas and
     1.31:1 on parchment against the 3:1 WCAG 1.4.11 asks of a control's
     boundary. The comment above already said control borders had to be darker
     than a hairline; the value did not do it — #d4d7de is only 1.15:1 away from
     --hairline itself, so in practice every input on the site was outlined in
     decoration.

     WHY THIS EXACT VALUE, and it is not a taste call. FOUR numbers have to
     clear 3:1, not one, and the fourth is the one that is easy to miss:
       3.92:1  on canvas — the field's own white fill, and the white page
                           behind it.
       3.57:1  on parchment — the same edge where the page under it is grey.
       3.79:1  on #FAFBFD — the Ask-an-AI prompt box's own off-white fill.
       3.12:1  the STEP from --hairline #e3e5ee. A form sits among cards edged
               in --hairline, and if a field's edge is not distinguishable from
               a decorative one then it is not doing the job this token exists
               for. #868b9f clears the three grounds (3.38 / 3.08 / 3.27) and
               fails this at 2.69:1 — the same trap that #858a9e fell into on
               --hairline-hover, recorded there.

     It is the SAME measured value as --hairline-hover and that is deliberate:
     the site already owns a blue-grey that clears 3:1 everywhere, and inventing
     a second one a shade away is how a palette doubles. Written as a literal
     rather than var(--hairline-hover) because the two mean different things — a
     hover state and a control edge — and either should be able to move without
     dragging the other with it. */
  --form-border:#7b8093;

  /* typography — the tuned values.
     Body/UI face is DM Sans (spec §7): headings stay League Spartan, everything
     else — body copy, ALL buttons, labels, table headers — is DM Sans.

     CORRECTION 2026-09-08 to what this comment used to claim. It said Inter was
     kept in the fallback stack only to cover "the window between this file
     shipping and the Google Fonts <link> in build.py being updated". That
     window closed long ago: build.py requests DM Sans, so DM Sans always loads
     and the Inter entry in --font-body is inert. Inter is still REQUESTED, for
     a completely different reason — lead-overlay.css hard-codes
     'Inter', sans-serif on six rules with no DM Sans in the stack, and build.py
     links that stylesheet on every built page. Drop Inter from the font request
     and the lead-capture overlay, the site's only conversion path, falls back
     to the system sans on all 18 pages — and it will never show up in a
     screenshot, because the overlay is closed by default.

     Leading 1.47 and the slight negative tracking are the approved Apple ladder
     (2026-09-08), replacing 1.62 and no tracking at all. The tracking is a
     token rather than a bare letter-spacing literal on body, so the next person
     can find it. */
  --font-display:'League Spartan',system-ui,sans-serif;
  --font-body:'DM Sans','Inter',system-ui,-apple-system,sans-serif;
  --body-size:17px;
  --body-leading:1.47;
  --body-tracking:-.011em;

  /* DELETED 2026-09-08: --measure:34em and --measure-wide:52em. Both had zero
     consumers. Rule 2.4 ("no measure cap anywhere") put max-width:none on
     .article and on every p, which deleted every consumer they had ever had and
     left the two tokens sitting in :root looking load-bearing. If a measure cap
     is ever wanted again, the rule and the token come back together. */

  --ink-h:var(--ink);
  --ink-body:#3c4250;
  /* --ink-body is a THIRD grey, sitting between --ink and --ink-muted, and the
     approved four-step ink ladder has no slot for it. It is deliberately left
     alone rather than folded into --ink: it sets the text colour of the whole
     document (see body{} below), so re-pointing it would darken every paragraph
     on the site — a visible change the operator has not signed off. Recorded
     here so it reads as a decision rather than as something that was missed. */

  --pad:clamp(20px,5vw,64px);

  /* ── The vertical rhythm of the whole site (approved 2026-09-08) ─────────
     --section is the padding a surface block keeps inside its own top and
     bottom edges: 80px on a desktop, 56px on a narrow screen (the step-down is
     the single @media rule in the sections block below). It replaces a
     clamp(22px,1.9vw,32px) scale that predates the surface model — roughly a
     2.5x increase, so every page is noticeably longer than it was.

     ONE KNOB ON PURPOSE. Every section-padding override in this file is
     derived from this token rather than written as its own number, so moving
     the rhythm moves all of it together. Do not reintroduce a hard-coded
     section padding anywhere; that is exactly how the five different
     hand-tuned clamps this replaces came about.

     --section-top is the shorter padding the FIRST section on a page gets. It
     exists because the header is fixed and does not occupy flow, so
     body{padding-top:58px} holds its space and the first section's own top
     padding lands on top of that. At the full 80px that is 138px of empty
     screen above every page's first word. .45 lands it on 36px, which is
     within a couple of pixels of what .hero and .page-hero were already doing
     by hand before this token existed. */
  --section:80px;
  --section-top:calc(var(--section) * .45);
  /* THE SITE IS LIGHT-ONLY, AND SAYS SO. Operator decision, 2026-09-09, after
     opening the dev site on an iPhone 15 with the system in dark mode.
     There is no `prefers-color-scheme` rule anywhere in this file and there is
     not meant to be: every surface here is a deliberate choice against a fixed
     palette, and a half-honoured dark mode - navy bars that adapt, white cards
     that do not - is worse than none. Declaring `light` stops the browser
     auto-darkening the things it decides for itself (form controls, scrollbars,
     the canvas behind a short page) on a device set to dark. Without it those
     drift while everything painted by this file stays put, which is exactly
     the mismatch the operator saw.
     If a real dark mode is ever wanted, it is a design project with its own
     token set, not a media query bolted on here. */
  color-scheme:light;
  /* Operator decision: cards, tiles, panels and CTA blocks are perfectly square,
     so the colour band across the top of a card has no rounding to fight.
     This token is used ONLY by card/tile/panel containers. The spec's 12px card
     tier is deliberately NOT followed — the operator's zero overrides it.
     STILL STANDING as of 2026-09-08. The pill reversal recorded below covers
     BUTTONS ONLY; cards, tiles and panels stay square. */
  --radius:0;

  /* ── Radius scale (spec §5, as amended twice) ────────────────────────────
     Spec scale:  4 small · 6 control · 8 overlay · 12 card · 9999 pill.

     AMENDMENT 1 (operator, original — kept here rather than deleted, so nobody
     re-derives it from scratch). Two parts: cards/panels are 0 rather than 12px
     (--radius above), and buttons were "very square, minimal corner rounding",
     so the primary CTA was NOT a pill and every button on the site sat at the
     6px control tier. The consequence was stated plainly at the time, because
     spec §9.1 warns about it: rank could no longer be encoded by SHAPE, so it
     was carried by size, weight and shadow instead.

     AMENDMENT 2 (operator, 2026-09-08) — REVERSES the button half of that, and
     only the button half. Pills are back on every .btn variant: primary, ghost,
     outline, bare, small and the header pair, plus the footer's mobile buttons
     and the Ask AI row. Rank is now carried by FILL, SIZE and WEIGHT. Shape no
     longer separates one button from another, because they all share it — and
     the CTA's drop shadow has gone too (see the note where --btn-shadow used to
     be), so what was a three-lever hierarchy is now a two-lever one and both
     remaining levers have to be got right.

     What amendment 2 does NOT touch, stated because "pills are back" is exactly
     the sentence that gets over-applied: --radius stays 0, so cards, tiles,
     panels, quotes and FAQ blocks are still square; and --r-control keeps
     inputs, selects and textareas, because a pill-shaped text field is wrong.
     ──────────────────────────────────────────────────────────────────────── */
  --r-control:6px;    /* inputs, selects, textareas, and menu/panel chrome */
  --r-overlay:8px;    /* modals and dialogs, >=768px only */
  --pill:9999px;      /* every button, plus badges and status pills */
  /* DELETED 2026-09-08: --r-small:4px ("checkboxes, inline chrome"). Zero
     consumers — the generated pages contain no checkboxes at all.
     RENAMED 2026-09-08: --r-pill:999px became --pill:9999px, which is the
     approved palette's name and value. Renamed rather than added alongside,
     because two pill tokens holding two different values in one :root is how
     the next person picks the wrong one. Its old trailing comment read "NOT
     buttons" — that was amendment 1 talking, and it is now exactly backwards.
     --r-overlay was itself dead until 2026-09-08: .askai-modal hard-coded its
     own 8px corner instead of reading the token. It now reads the token. */

  /* ── One control height (spec §9.5) ──────────────────────────────────────
     The source site renders a button at 38px, a text input at 36px and a select
     at 40px — three heights in one form row. We pick ONE and apply it to
     button + input + select together: 44px, which is also the minimum touch
     target, so a thumb-driven marketing site gets it for free.
     The 32px small tier below is a deliberate, explicitly opted-into second
     size for repeatable in-page actions (the Ask AI row), not drift. */
  --control-h:44px;
  --control-h-sm:32px;

  /* REMOVED 2026-09-08: --btn-shadow, the primary CTA's drop shadow, taken
     verbatim from spec §4b. It existed because the operator had ruled the pill
     shape out, so rank had to be carried by size, weight and shadow instead.
     Pills are back (amendment 2 above), so shape carries rank again and the
     shadow is no longer earning its place. Apple's method allows essentially
     one decorative shadow tier, and it is reserved for product imagery — see
     --shadow below. Do not reintroduce a button shadow to "make the CTA pop":
     make it pink, filled, 700 and 44px tall instead.
     Both of its references went with it. The second one mattered far more than
     it looked: .btn-primary:focus-visible composed --btn-shadow INTO the focus
     ring, so deleting the token alone would have made that whole box-shadow
     declaration invalid and computed it to none — silently deleting the focus
     ring on the most important button on every page. See the focus-ring block
     below for why the rule was deleted rather than edited. */

  /* The one surviving decorative elevation, and it does not get a new user
     without a conversation. It is reserved for PRODUCT IMAGERY — .videowrap,
     .visual, .uiframe and .shot, the four places real screenshots and video
     land — plus exactly one piece of chrome: the desktop nav popover
     (.mainnav .grp .menu), where elevation is information rather than
     decoration because it says "this layer floats above the page". The three
     card hover shadows and the two button shadows that used to read this token
     were culled on 2026-09-08.
     .shot WAS the fourth user, added 2026-09-08 for the home hero's product
     screenshot. It is inside the policy rather than an exception to it — a
     screenshot of the product is exactly what "product imagery" means — and it
     is recorded here rather than added quietly, because the previous sentence
     of this comment said a FIFTH user is how the shadow tier grows back, and
     that sentence is still true. It now means a sixth. */
  --shadow:0 1px 3px rgba(16,24,40,.08),0 12px 32px -12px rgba(16,24,40,.14);

  /* Card colour band. Base height, and the doubled height it grows to on a
     desktop hover. Both live here so the "doubles" relationship is visible.
     Naming note: "band" means two unrelated things in this file. These two
     tokens are the 6px colour rule across the top of a CARD. The OTHER meaning
     is gone as of 2026-09-08: the navy CTA section's class .band was renamed
     .surface-tile, so "band" now means one thing in this file again. That
     resolved an ambiguity rather than creating one, and it is the reason the
     surface classes are not called .band-navy or similar. */
  --band-h:6px;
  --band-h-hover:12px;
}

*{box-sizing:border-box;margin:0;padding:0}
html{scroll-behavior:auto}  /* smooth scrolling fights the hide-on-scroll bar */
html:focus-within{scroll-behavior:smooth}
body{
  font-family:var(--font-body);font-size:var(--body-size);line-height:var(--body-leading);
  letter-spacing:var(--body-tracking);
  color:var(--ink-body);background:var(--canvas);-webkit-font-smoothing:antialiased;
}
img{max-width:100%;display:block}
a{color:inherit}

/* ── Inline links in body copy (D2, operator 2026-09-08) ──────────────────
   `a{color:inherit}` above is the whole of this stylesheet's link policy, and
   it means an inline link inside a paragraph renders in the surrounding body
   colour with no underline — i.e. it is not identifiable as a link at all.
   Every other link on the site (nav, footer, cards, buttons) is styled by its
   own component rule and is unaffected by this; PROSE was the gap.

   The colour is --action-press #c9006e and NOT the brand pink #e6007e. That is
   a measured call, not a taste one: #e6007e is 4.10:1 on the parchment sections
   and fails AA outright, and 4.50:1 on white, which clears AA by 0.0008 and is
   therefore not a number to build on. #c9006e is 5.66:1 and 5.16:1. The brand
   pink stays where it earns its keep — filled buttons, where the white label
   sits ON it rather than beside it.

   Colour alone is never the only signal (WCAG 1.4.1), hence the underline. It
   thickens rather than appears on hover, so the line does not shift.

   THIS RULE HAS REAL CONSUMERS AS OF 2026-09-09, and it did not before. The
   note here used to say no section in _source/content.py contained an inline
   link, so the rule matched nothing. Bringing the contact page and the privacy
   policy onto the generator brought seven links with them: three mailto: links,
   one per contact card, and four outbound links in the policy — three in §14
   and one in §19. Those two shapes are added to the selector list below. Leave
   them there: a policy link that renders as plain black text is a link nobody
   can see, on the one page where "where does this go?" is the whole question.
   ─────────────────────────────────────────────────────────────────────────── */
.article p a,.article li a,.article blockquote a,
.prose p a,.prose li a,.callout p a,.lede a,
.card p a,.accordion-content p a,.accordion-content li a{
  color:var(--action-press);text-decoration:underline;
  text-underline-offset:3px;text-decoration-thickness:1px}
.article p a:hover,.article li a:hover,.article blockquote a:hover,
.prose p a:hover,.prose li a:hover,.callout p a:hover,.lede a:hover,
.card p a:hover,.accordion-content p a:hover,.accordion-content li a:hover{
  text-decoration-thickness:2px}

/* 600, not 700 (approved weight ladder, 2026-09-08). The ladder is
   300/400/600/700 with 500 deliberately absent, and headings sit at 600. Two
   <h2>s carry their own font-weight and are therefore NOT reached by this rule
   — .hero h2.sub-head (300) and .askai-modal-title (600) — and two <h4>s take
   their weight from here while overriding the family: .site-footer h4 and
   .aside h4. Those two are now pinned to 700 explicitly, because they are
   11.5px uppercase micro-labels whose peers (.eyebrow, .postcard .cat,
   .cmp-label) are all 700, and letting them drift to 600 would split one label
   role across two weights for no reason. */
h1,h2,h3,h4{font-family:var(--font-display);color:var(--ink-h);line-height:1.12;font-weight:600}
h1{font-size:clamp(34px,5.2vw,54px);letter-spacing:-.02em}
h2{font-size:clamp(26px,3.4vw,36px);letter-spacing:-.015em;line-height:1.18}
h3{font-size:clamp(19px,2vw,22px);line-height:1.3;letter-spacing:-.01em}
p{max-width:none}   /* measure is applied to .article only, never globally */
strong{color:var(--ink-h);font-weight:600}

.wrap{max-width:1180px;margin:0 auto;padding:0 var(--pad)}
.eyebrow{font-size:12px;letter-spacing:2.4px;text-transform:uppercase;font-weight:700;
  color:var(--action-press);font-family:var(--font-body)}

/* ── buttons ─────────────────────────────────────────────────────────────
   Built to the button & colour spec, with ONE deliberate departure.

   HISTORY, kept because it explains why the rest of this block is shaped the
   way it is. The spec (§3) encodes rank in SHAPE: the single page CTA is a
   9999px pill, every other affirmative button is 6px and square-ish. The
   operator ruled that out — "drop the entire sentiment of these rounded
   buttons, very square buttons with very minimal corner rounding" — so for a
   time every button here was 6px. That removed the spec's rank signal, which
   §9.1 explicitly warns is the thing that most often breaks this design
   language, and rank was carried by three levers instead: size, weight and a
   drop shadow on the CTA alone.

   REVERSED 2026-09-08 (operator). Pills are back on every .btn variant, so the
   shape lever is live again — and because every button now shares it, shape
   says "this is a button", not "this button outranks that one". The CTA's drop
   shadow has been culled in the same pass (Apple's method keeps one decorative
   shadow tier and reserves it for product imagery), so the three levers are now
   two, and both have to be got right:
       1. FILL    — the CTA is the only SOLID pink button in a view. Everything
                    else is transparent with a hairline border.
       2. SIZE    — CTA 44px tall with 32px side padding; secondary 32px/12px.
       3. WEIGHT  — CTA 700; everything else 600. Weight 500 was removed from
                    the ladder entirely on 2026-09-08, so the old three-step
                    700/600/500 is now a two-step 700/600.
   One CTA per view, same as before. A second one and none of the levers mean
   anything.

   Hover is a flat colour swap, not the source site's translucent overlay film.
   The spec recommends this for a site with a handful of known button colours
   (§8, §9.3) and it sidesteps the overlay's position:relative trap entirely.
   The old translateY(-1px) lift is gone with it: a button that jumps reads as
   soft, which is the opposite of what the operator asked for.
   ─────────────────────────────────────────────────────────────────────────── */
.btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;
  font-family:var(--font-body);font-weight:600;font-size:14px;line-height:20px;
  min-height:var(--control-h);padding:10px 16px;border-radius:var(--pill);
  border:1.5px solid transparent;cursor:pointer;text-decoration:none;
  white-space:nowrap;transition:background-color .15s ease,border-color .15s ease}
.btn svg,.btn .ico{width:16px;height:16px;flex:none}
.btn[disabled],.btn:disabled,.btn.is-disabled{opacity:.5;pointer-events:none}

/* Primary CTA. Pink is the action colour and is never a background band.
   White on #E6007E is 4.50:1 — it clears AA by 0.0008, so it must stay at full
   opacity and must never be tinted or faded. Hover #C9006E is 5.66:1. */
.btn-primary{background:var(--action);color:var(--on-dark);border-color:transparent;
  font-weight:700;padding:10px 32px}
.btn-primary:hover{background:var(--action-press)}

/* Secondary pink — the repeatable, in-page affirmative action. Same pink, so it
   still reads as the yes-choice, but small and shadowless so it cannot compete
   with the page CTA. This is the rank the Ask AI buttons use. */
.btn-sm{min-height:var(--control-h-sm);padding:6px 12px;font-size:12px;line-height:16px;
  font-weight:600}

/* Neutral. Transparent, hairline border, and it INHERITS its text colour —
   that is how the white-on-navy buttons in the header and the CTA band work:
   the parent sets the colour and the button follows. .btn-ghost is the name the
   existing markup already uses for this, so it keeps the visible border; the
   spec's true border-less ghost is .btn-bare, which keeps the same 1.5px
   transparent border so its height matches an outline button beside it. */
/* The 5%-black 1px shadow this pair used to carry was culled on 2026-09-08.
   It was invisible under a transparent button on white, and actively wrong on
   the navy header, where a BLACK shadow under a white-on-navy ghost button
   contributes nothing at all. */
.btn-outline,.btn-ghost{background:transparent;color:inherit;
  border-color:rgba(0,0,0,.1)}
.btn-outline:hover,.btn-ghost:hover{border-color:rgba(0,0,0,.28)}
.btn-bare{background:transparent;color:inherit;border-color:transparent;box-shadow:none}
.btn-bare:hover{background:rgba(0,0,0,.04)}

.btn-row{display:flex;gap:12px;flex-wrap:wrap;margin-top:22px;align-items:center}

/* ── one control height, and one focus ring (spec §4a, §8, §9.5) ──────────
   Buttons take a 2px ring flush to the edge. Inputs take a 2px gap in the page
   colour first, then the ring, so the ring does not touch the field border.
   On navy — the header bar and the CTA band — a navy ring is invisible, so it
   flips to white there. Focus that cannot be seen is not focus.

   DELETED 2026-09-08, and the reason is worth reading before anyone writes
   anything like it again. There used to be a fourth rule here:
       .btn-primary:focus-visible{box-shadow:0 0 0 2px var(--brand-blue),var(--btn-shadow)}
   Its job was to keep the CTA's drop shadow underneath the ring instead of
   having it blown away by the focus rule. The shadow is gone, so the rule had
   nothing left to preserve — but it could not simply have its token stripped
   out and be left in place, and it could NOT be left behind when --btn-shadow
   was deleted. An undefined custom property with no fallback makes the WHOLE
   box-shadow declaration invalid at computed-value time; box-shadow is not
   inherited, so it computes to none. And because that rule had already beaten
   the generic .btn:focus-visible rule on the cascade (both are (0,2,0), it came
   later), the browser would NOT have fallen back — the most important button on
   every page would have rendered with no visible focus ring at all, with
   nothing in the build or the tests to catch it. Deleting the rule lets the
   generic one win, which is safe here because build.py never emits btn-primary
   without btn. Never reintroduce a .btn-primary:focus-visible rule that
   composes a var() without a fallback. */
button:focus-visible,.btn:focus-visible,a.btn:focus-visible,summary:focus-visible{
  outline:none;box-shadow:0 0 0 2px var(--brand-blue)}
.hdr button:focus-visible,.hdr .btn:focus-visible,.hdr a:focus-visible,
.surface-tile .btn:focus-visible,.site-footer a:focus-visible{box-shadow:0 0 0 2px var(--on-dark)}
input:focus-visible,select:focus-visible,textarea:focus-visible{
  outline:none;box-shadow:0 0 0 2px var(--canvas),0 0 0 4px var(--brand-blue)}

/* Button, input and select are the SAME height. This is spec §9.5's one fix and
   the reason it exists — three heights in one form row is the tell that nobody
   set a control height on purpose. Scoped to bare elements so the lead-capture
   overlay, whose stylesheet loads after this one and styles its own fields by
   class, is untouched. */
input:not([type=checkbox]):not([type=radio]):not([type=hidden]),select,textarea{
  font-family:var(--font-body);font-size:14px;line-height:20px;
  min-height:var(--control-h);padding:10px 12px;border-radius:var(--r-control);
  border:1px solid var(--form-border);background:var(--canvas);color:var(--ink-h)}
textarea{min-height:calc(var(--control-h) * 2)}

/* ── header: full-bleed navy strip, flush to the top ───────────────────
   This WAS a floating pill: .hdr was itself the width wrapper (`class="wrap
   hdr"`), so it inherited the 1180px cap, sat 14px down from the top and was
   ringed in 3px of white. Now .hdr is the full-bleed navy band and .hdr-inner
   — which carries .wrap — holds the content on the normal 1180px grid.
   A full-width bar and a centred content column cannot be the same element;
   if you find yourself collapsing these two divs you are rebuilding the pill.

   THE BOTTOM EDGE IS LOAD-BEARING, NOT DECORATION — do not "tidy it away".
   The 3px white ring it replaces existed for exactly one reason, recorded
   here before it was removed: on a white section the ring merged into the
   background and read as nothing, but the moment the hide-on-scroll bar
   revealed over a NAVY background it became a crisp outline, so navy-on-navy
   never happened. Both the CTA band and the footer are navy, and an invisible
   header is a lost navigation. Flush to the top there is no gap left to draw
   a ring in — it would render off-screen — so the same job is done by the
   bar's bottom edge instead:
     • 0 1px 0 rgba(255,255,255,.36)  the hairline. This is the half that
       does the navy-on-navy job, and .36 is a MEASURED value, not a taste
       call. .surface-tile (renamed from .band on 2026-09-08) is painted
       in var(--tile) — the identical rgb(9,20,100) the bar is painted in —
       so the hairline is the only thing separating them.
       RE-CHECKED 2026-09-08, when .surface-tile went from a three-stop
       gradient to that flat navy. The number does not move, and the worst
       case gets BETTER rather than worse. Under the gradient only the band's
       top-left corner was #091464; across and down it ran to #1a2a8a and then
       #3a1a6a, and the hairline had never been measured against either of
       those. Flat, the measured 3.09:1 is true across the whole band for the
       first time. The one arrangement that would void it is unchanged and is
       spelled out three paragraphs down: do not flatten to #0b1878.
       Resolved over that navy it is rgb(98,105,156), which is 3.09:1
       against #091464 (WCAG 1.4.11 wants 3:1 for a non-text boundary).
       The .14 it replaces resolved to 1.44:1, which is why the bar read as
       nothing when it revealed over the CTA band.
       THIS NUMBER IS MEASURED AGAINST #091464 AND ONLY HOLDS THERE. It stays
       true only while the navy surface's TOPMOST gradient stop is that exact
       colour. Anyone who "simplifies" .surface-tile to the palette's second
       navy #0b1878 voids it: #0b1878 against #091464 is 1.09:1, i.e. the
       fixed bar would disappear into a navy section scrolling behind it,
       which is the precise failure this hairline exists to prevent.
       Do not lower it back towards .2 "because it looks strong on white" —
       over white it is white, i.e. invisible, which is the whole point.
     • 0 6px 16px -8px rgba(0,0,0,.34)  the soft drop. The hairline cannot
       be seen on a WHITE section, so this is the half that draws the edge
       there; the pair is what makes one bar work over both backdrops.
       Deeper and less negatively-spread than the 0 8px 24px -14px rgba(0,0,0,.55)
       it replaces, which resolved to almost nothing on white and, being
       black-on-navy, contributed literally nothing over the band.
   Delete either half and the bar loses its edge on one of the two backdrops.
   ─────────────────────────────────────────────────────────────────── */
.site-header{position:fixed;top:0;left:0;right:0;z-index:90;padding:0;
  transition:transform .32s cubic-bezier(.4,0,.2,1)}
/* Plain -100%. The old calc(-100% - 18px) was buying clearance for the 14px
   top inset plus the ring and its shadow; with the bar flush to the viewport
   edge there is nothing left above it, so the extra 18px is pure over-travel.
   One of THREE coupled numbers — see the note on .mainnav.open below. */
.site-header.hidden{transform:translateY(-100%)}
/* Full width on purpose: no max-width, no auto margins, no radius, and none
   of the old `padding:0 10px 0 20px`. That padding belonged to a bar that was
   also the wrapper; re-adding it here would inset the strip's background from
   nothing and shove .hdr-inner off the site's own grid.
   The box-shadow is the LOAD-BEARING EDGE — read the block comment above
   before touching either half. Hairline resolves to 3.09:1 on #091464, which
   is the navy the CTA band starts on; the drop is what draws the edge on a
   white section. Both are needed; neither is decoration. */
.hdr{background:var(--dark-blue);border-radius:0;
  box-shadow:0 1px 0 rgba(255,255,255,.36), 0 6px 16px -8px rgba(0,0,0,.34)}
/* .hdr-inner also carries .wrap, so max-width:1180px, the auto margins and the
   horizontal --pad all arrive from there. Do NOT re-declare them here — a
   second max-width on the same element is how the pill crept back last time.
   58px is the bar height and is quoted in two other places; keep them in step. */
.hdr-inner{display:flex;align-items:center;gap:22px;height:58px}
.brand{display:flex;align-items:center;gap:9px;text-decoration:none;font-family:var(--font-display);
  font-weight:700;font-size:19px;color:var(--on-dark);letter-spacing:-.01em}
/* The real Tempus logo. What shipped here was a conic-gradient swirl standing in for it —
   a placeholder that read as the brand mark. object-fit:contain so a non-square source is
   never distorted; alt is empty because the adjacent text already says "Tempus". */
.brand .brandmark{width:26px;height:26px;flex:0 0 26px;object-fit:contain;display:block}
.mainnav{display:flex;gap:20px;align-items:center;margin-left:6px;flex:1}
.mainnav a{text-decoration:none;font-size:14px;font-weight:400;color:rgba(255,255,255,.82);
  padding:5px 0;border-bottom:2px solid transparent;transition:color .15s ease}
.mainnav a:hover{color:var(--on-dark);border-bottom-color:var(--action-on-dark)}
/* --nav-menu-gap is the visible breathing space between the nav button and the
   dropdown it opens. It is a variable rather than a repeated 14px because TWO
   rules below have to agree about it — the menu's own offset, and the invisible
   bridge that spans it. They drifted apart once already and the menu became
   unreachable; see the bridge comment. Change the gap here and both follow. */
.mainnav .grp{position:relative;--nav-menu-gap:14px}
.mainnav .grp>button{background:none;border:0;font:400 14px var(--font-body);
  color:rgba(255,255,255,.82);cursor:pointer;display:flex;align-items:center;gap:5px;padding:5px 0}
.mainnav .grp>button:hover{color:var(--on-dark)}
.mainnav .grp .menu{position:absolute;top:calc(100% + var(--nav-menu-gap));left:-14px;background:var(--canvas);
  /* Squared with the bar it hangs off. A 14px popover under a 6px header bar was
     the same two-design-languages problem the .hdr fix above solved. */
  border:1px solid var(--border-gray);border-radius:var(--r-control);box-shadow:var(--shadow);padding:8px;
  min-width:260px;display:none}
/* THE HOVER BRIDGE — without it the menu cannot be reached with a mouse.
   The menu is shown by `.grp:hover`, and it hangs --nav-menu-gap below the
   button. That gap was empty, so the instant the pointer left the button it
   was over the header bar instead of over anything inside .grp, :hover was
   lost, and the menu closed before the pointer had travelled the 14px to it.
   Measured on a 1470px viewport: button bottom y=43.3, menu top y=57.3, and
   the menu went display:none at every y in between.

   This is an invisible strip that belongs to the menu and fills that gap, so
   the pointer never leaves .grp on the way down. It paints nothing — no
   background, no border — it only takes pointer events.

   Deliberately NOT a close-delay: a delay leaves the menu hanging after the
   pointer has genuinely gone, and still feels laggy on the way in. And
   deliberately not "move the menu flush to the button" either — the gap is
   the visual separation between the bar and the popover, and is wanted.

   bottom:100% parks it directly on top of the menu's padding box; the +1px is
   the menu's own 1px border, which sits between that padding box and the gap.
   left/right:0 give it the menu's full width, which covers the button and then
   some, so there is no diagonal the pointer can slip through.

   It exists ONLY while the menu is displayed, because it is the menu's own
   pseudo-element. That is what keeps it from swallowing clicks: with the menu
   closed there is nothing there at all. Verified with the menu open that the
   brand mark and all five sibling nav links still hit-test to themselves.

   Killed at <=960px in the responsive block below — the mobile panel lays the
   menu out static, where an absolutely-positioned strip would float loose over
   the nav and block taps. */
.mainnav .grp .menu::before{content:"";position:absolute;left:0;right:0;bottom:100%;
  height:calc(var(--nav-menu-gap) + 1px)}
.mainnav .grp:hover .menu,.mainnav .grp:focus-within .menu{display:block}
.mainnav .grp .menu a{display:block;padding:9px 12px;border-radius:0;border:0;
  color:var(--text-dark)}
.mainnav .grp .menu a:hover{background:var(--light-gray);border:0;color:var(--text-dark)}
.hdr-cta{display:flex;gap:8px;align-items:center}
.hdr-cta .btn{padding:9px 17px;font-size:13.5px}
.hdr-cta .btn-ghost{color:rgba(255,255,255,.9);border-color:rgba(255,255,255,.28)}
.hdr-cta .btn-ghost:hover{border-color:var(--on-dark);color:var(--on-dark)}
/* A pill, like every other button (operator, 2026-09-08 - it was 6px while
   buttons were square).
   font-family too: a <button> does NOT inherit the page font, so without this
   the mobile menu toggle was setting in the browser default (Arial) while every
   other button on the site was in the brand face. */
.navtoggle{display:none;background:none;border:1px solid rgba(255,255,255,.3);
  border-radius:var(--pill);font-family:var(--font-body);font-weight:600;
  padding:8px 12px;cursor:pointer;font-size:13.5px;color:var(--on-dark);
  /* 44px is the minimum touch target in UI-STANDARDS rule 5.3. Measured live on a
     375px viewport this was 34px — and on a phone it is the ONLY way to open the menu,
     so a missed tap is a dead navigation. Height, not padding, so the label size
     cannot silently shrink it again. */
  min-height:44px;display:none;align-items:center}
/* The fixed bar no longer occupies flow, so give the first section room.
   58px = the bar height exactly, because the bar is now flush to the top with
   no gap above it. It was 86px when the bar floated: 14px inset + 58px bar +
   the ring and a little breathing room. COUPLED NUMBER 1 of 3 — this,
   .mainnav.open{top} and .site-header.hidden{translateY} all describe the same
   58px bar and must move together. Too big and the hero starts with a white
   band; too small and the bar covers the first heading. */
body{padding-top:58px}

/* ── sections: the three surfaces ──────────────────────────────────────
   Every section on the site sits on exactly one of three backgrounds, and as
   of 2026-09-08 build.py puts that choice on the tag as a class instead of
   each renderer picking a background privately inside its own markup. The
   three below are the entire vocabulary of section backgrounds. A fourth is a
   new colour on the website — a decision, not a CSS edit.

   THE COLOUR CHANGE IS THE DIVIDER. There is no rule, border or separator
   between sections anywhere in this file, deliberately: one surface ending and
   another beginning is the only thing that tells a reader a new block has
   started. That only works if the vertical rhythm is even and honest, which is
   what the padding block below is for — the two halves are one design and
   neither survives without the other.

   .surface-canvas looks like it does nothing, and in a screenshot it does
   nothing: an unclassed section used to be transparent and let the white
   <body> show through. But its COMPUTED background-color moves from
   rgba(0,0,0,0) to rgb(255,255,255). The project's self-test reads computed
   styles as well as taking screenshots, so expect that one to show up on a
   change that is genuinely pixel-neutral, and do not chase it as a regression.

   The navy surface, .surface-tile, is not here — it lives with the reversals
   that make text legible on it, further down under "the navy surface".
   ──────────────────────────────────────────────────────────────────── */
.surface-canvas{background:var(--canvas)}
.surface-parchment{background:var(--parchment)}
/* DELETED 2026-09-08, all three of them: .alt .blocks .blk, .alt .acard/.ncard
   and .alt .nstack, each of which set a white background. Every one was a dead
   no-op — the element's own base rule already declared that same white, so the
   rule changed nothing, and renaming it to .surface-parchment would have
   changed nothing either. Recorded here rather than quietly dropped, because
   they were the right IDEA in the wrong place: those card families DO need a
   per-surface fill, just not on the surface that is already nearly white.
   Their live counterparts are in the navy block below, where a white card
   genuinely would be wrong. */

/* ── section rhythm ────────────────────────────────────────────────────
   One number, --section, governs the whole site's vertical spacing (see the
   token, where the 80px/56px values and the 2.5x increase are recorded).
   Everything here is derived from it.

   THE ADJACENT-SECTION RULE IS THE HALF THAT IS EASY TO LEAVE OUT, and
   leaving it out is invisible in a diff. Sections are direct children of
   <main> (build.py wraps them in nothing), so `section + section` reaches
   them. Two sections sharing a background are visually ONE block; left alone
   they would stack 80px of bottom padding against 80px of top padding and open
   a 160px hole in the middle of a block that has no colour change to justify
   it. Counted off the built pages on 2026-09-09: 5 of the site's 45 section
   boundaries are same-surface, and all five are white-on-white — how-it-works,
   llm-info, stories, who-we-help/disability-and-care and
   who-we-help/professional-teams-and-firms each run two canvas sections
   together, and two is the longest run on the site. It was 11 until the twelve
   page heroes moved onto navy the same day: six of them were a canvas hero
   above a canvas section, and those six boundaries are a colour change now.
   The denominator moved the same day too, in the second pass: splitting the
   five articles into a navy band plus a white body gave each of them a second
   section, so 40 boundaries became 45 and all five of the new ones are colour
   changes. The figure that stood here said "20 of ~29", which matched neither build.
   Five is rarer than that, and it is still five holes this rule is the only
   thing closing. So a section following one of its own colour drops its top
   padding, and the gap inside a block is 80px, once.

   Where the colour DOES change, both paddings stay, and that is correct even
   though it totals 160px. Nobody reads it as 160px of anything — it is 80px of
   white and then 80px of grey, each block keeping its own edge clear. Do not
   "fix" it by collapsing the seam; that is the rhythm.

   THE :not(.page-hero) MATCHES NOTHING TODAY, AND IS KEPT ANYWAY. All seventeen
   page heroes are .surface-tile — SURFACE_DEFAULT["page_hero"] in build.py —
   and not one of them is followed by a navy section, so no arm of this rule can
   reach a page hero: the canvas and parchment arms have no page hero left to
   match, and the tile arm has no page hero followed by tile. Counted off the
   built pages on 2026-09-09, not estimated. The figure that stood here said six
   of eighteen, and before that nine of fifteen; each was true of the build it
   was written against, when a page hero was canvas.

   WHAT WOULD MAKE IT MATTER AGAIN is a page whose hero is followed straight by
   the navy CTA band — that boundary is tile on tile — and four pages are one
   section away from it already: 404 and our-story run hero, canvas, CTA band;
   contactus and privacypolicy run hero, parchment, CTA band. Dropping that
   middle section is an ordinary editorial change. (List corrected 2026-09-09:
   it named only the two canvas pages and missed the two parchment ones.) Two separate
   things would go wrong on the day it happens, which is why a guard that
   currently matches nothing is not dead code.
   The first is the gap. A page hero no longer has zero bottom padding: it
   carries calc(var(--hero-fade) * .9) from .surface-tile.page-hero, 180px at
   the 200px desktop fade and 99px below 720px. But that is the tail of the
   gradient, not a gap under a title — it is contrast-locked and guaranteed
   empty (see --hero-fade) — so a CTA band collapsing its own top padding onto
   it would put the band's heading directly against the fade.
   The second is quieter and is about weight, not spacing.
   `.surface-tile:not(.page-hero)+.surface-tile` is (0,3,0); the overlap rule
   further down, `.surface-tile.page-hero + section`, is (0,2,1). Without the
   :not() the collapse would out-specify it and take the hero boundary's
   -14px/16px with it, whichever order the two rules sit in.
   ──────────────────────────────────────────────────────────────────── */
section{padding:var(--section) 0}
/* padding-top:0 removes the padding that was also (incidentally) the barrier
   stopping a section's own first-child margin — e.g. a <h2> with a plain
   margin-top — from collapsing straight through the section's top edge and
   rendering outside its background. flow-root closes that without clipping
   anything, unlike overflow:hidden. Found 2026-09-10: how-it-works' "what we
   need from you" section runs straight into its faq section (both default
   to parchment, see SURFACE_DEFAULT in build.py) and the faq heading's
   10px margin-top was escaping as a visible sliver of page background
   between two sections of the same colour. */
.surface-canvas:not(.page-hero)+.surface-canvas,
.surface-parchment:not(.page-hero)+.surface-parchment,
.surface-tile:not(.page-hero)+.surface-tile{padding-top:0;display:flow-root}

/* The first section on a page takes the shorter --section-top, because the
   fixed bar's 58px of body padding already sits above it. It used to catch the
   five article pages, which opened straight onto a plain section with no hero;
   the second pass on 2026-09-09 gave each of them a navy band, so all 18 pages
   now open on a hero and this rule has no unhandled opener left to catch.
   It also out-specifies .hero and .page-hero below — (0,1,2) against (0,1,0) —
   but they resolve to the SAME token, so there is no disagreement to lose. It
   is out-specified in turn on every page by the under-the-bar rule further
   down, (0,2,2), which is what actually pads a hero today. Kept as the opener
   of last resort: the day a page ships without a hero it is the only rule that
   stops the first section colliding with the bar. */
main>section:first-child{padding-top:var(--section-top)}

/* On eleven of the eighteen pages the last section is the navy CTA block, and
   the footer directly beneath it is near-navy too. There is no visible edge
   between them for a bottom padding to keep clear, so a full 80px there merely
   adds to the footer's own 56px and makes 136px of unbroken dark. Half. */
main>section:last-child.surface-tile{padding-bottom:calc(var(--section) * .5)}

/* .flush-top — "this section continues the one above it rather than starting a
   new block". Only the callout carries it, and it replaces the inline
   style="padding-top:0" that build.py used to write onto that tag.

   The collapse rule above has already removed the callout's own top padding,
   because a callout sits on the same surface as the prose above it. All that
   is left is to pull in the bottom padding of the section it is continuing, so
   the pair share one modest gap instead of a full section break — which is
   what the inline zero bought, and roughly the gap it produced.

   SCOPED PER SURFACE ON PURPOSE. If a callout is ever put on a different
   background from the section above it, none of these match: both sections
   keep their full padding and the colour change gets its proper 80px on each
   side rather than landing flush against the text. That was impossible while
   the zero was inline, because an inline style beats every rule in this file.

   :has() is what lets a rule reach BACKWARDS to the previous section. Where it
   is unsupported the pair simply keeps the ordinary gap — nothing breaks. */
.surface-canvas:has(+.flush-top.surface-canvas),
.surface-parchment:has(+.flush-top.surface-parchment),
.surface-tile:has(+.flush-top.surface-tile){padding-bottom:calc(var(--section) * .4)}

/* The one step-down. Only --section moves; --section-top is a calc off it and
   follows on its own, which is the reason it was written as a calc. */
@media (max-width:720px){:root{--section:56px}}

/* The hero keeps a bespoke top — it is the first thing under the fixed bar —
   and an ordinary bottom, so the white it sits on ends on the site's rhythm
   like any other block. Its own background:var(--canvas) was deleted on
   2026-09-08: the surface class owns that now, and leaving both in place put
   two same-specificity background declarations on one element with nothing but
   source order deciding between them. */
.hero{padding:var(--section-top) 0 var(--section)}

/* ══════════════════════════════════════════════════════════════════════
   THE HERO ON NAVY.  Two audiences in this block, and the selector says
   which is which. The gradient below and the under-the-bar rule read
   `.surface-tile.hero,.surface-tile.page-hero`, so they carry all eighteen
   heroes on the site: the home page's, which is type "hero", and the seventeen
   inner-page ones, which are type "page_hero" and take the navy surface by
   default (SURFACE_DEFAULT in build.py). Everything else here — the scrim,
   the screenshot's tilt and bleed, the facts strip — stays scoped
   `.surface-tile.hero` alone, because it exists for the one screenshot on
   the home page and no other hero has one.
   That split is the thing to preserve. Widening a rule to `.page-hero`
   reaches seventeen pages rather than one; widening it to bare `.surface-tile`
   reaches every navy CTA band on the site as well. Decide which you mean
   before you touch a selector in here.
   ══════════════════════════════════════════════════════════════════════ */

/* The gradient. Navy for almost all of it, fading in the last stretch to the
   colour of the section that actually comes next.
   Specificity is (0,2,1) against .surface-tile's (0,1,0), so this wins on
   weight rather than on which rule happens to sit lower in the file.

   IT FADES TO THE COLOUR OF WHATEVER COMES NEXT, AND THAT IS THE WHOLE POINT.
   The section under the home hero is .surface-parchment (#f3f4f9). While the
   fade ended on white the page showed navy, then a band of white, then a step
   to grey at the section boundary — three colours where the design has two.
   Reviewed 2026-09-08 (recording, 00:30 and again at 01:30): the fade now runs
   navy to the next section's own colour, so the boundary is invisible and the
   whole thing reads as one blue-to-grey wash. The endpoint is therefore COUPLED
   to the surface of the section below — that is the one coupling in this rule,
   and it is why --hero-fade-to is a token named here rather than a hex written
   out. It defaults to parchment, which is what the home hero needs; the six
   page heroes that sit above a white section move it to --canvas through the
   :has() rule below. If the section after any hero is moved onto a different
   surface, this endpoint has to move with it.

   EVERY STOP IS A MIX OF THOSE TWO COLOURS AND NOTHING ELSE.
   The old midpoint #4a3a86 was a violet — measurably off the navy-to-parchment
   line (its red channel ran ahead of its green) and far too dark for where it
   sat, so the fade crawled through a purple band and then had to sprint to
   white in the last 80px. That sprint is what read as abrupt.
   The three stops below were derived, not chosen: each is a straight mix of
   #091464 and #f3f4f9 at 15.0%, 48.1% and 83.3%, placed so that CIE L*
   (perceived lightness) follows a smoothstep across the fade rather than a
   straight line. Smoothstep because this fade meets FLAT colour at both ends —
   navy above, parchment below — and a linear ramp leaves a visible crease
   where its slope meets each flat field. Easing in and out removes both
   creases. Measured against the ideal curve the worst error is 2.9 L*, which
   is under the threshold where an eye finds a band.
   Recomputing them: mix the two endpoint colours channel-wise at f, and pick
   the f whose L* equals L*(navy) + (L*(parchment) - L*(navy)) * (3p^2 - 2p^3)
   at each stop position p. Do not hand-pick a colour here — a hand-picked one
   is how the violet got in.

   THE BOTTOM OF THIS GRADIENT IS ALMOST WHITE AND THIS SURFACE PAINTS ALL TEXT
   WHITE. White text on #f3f4f9 measures 1.06:1 — invisible, and nothing in the
   build can see it happen. Changing the endpoint from white to parchment did
   not soften this by one bit; it is the same trap one shade greyer. So the fade
   is not allowed to reach any text, and that is made STRUCTURAL here rather
   than left to a warning nobody reads:

     --hero-fade is the height of the fade, measured up from the bottom edge.
     The gradient's stops are written as calc(100% - --hero-fade), NOT as
     percentages. Percentage stops move with the height of the box, and this
     box changes height every time the headline re-wraps — so a percentage that
     cleared the facts strip on a 1440px window would swallow it on a 1024px
     one. Anchored to the bottom in pixels, the fade is the last N pixels of
     the hero whatever its height.
     padding-bottom is that SAME number plus 24px of clearance. So the last
     --hero-fade pixels of the hero are padding, i.e. guaranteed empty, and no
     text can be in the fade however the page reflows.

   The two numbers cannot drift apart because there is only one of them. If you
   need more room under the facts, raise --hero-fade; do not add padding.

   AS OF 2026-09-08 THIS GUARANTEE CARRIES THE PINK AS WELL. Settled decision 12
   turned the pink text roles back on for the navy surface, and the hero's
   eyebrow is one of them. --action-on-dark #ff5cae is 5.71:1 on the flat navy
   and passes AA; on the fade's own stops it measures 3.86:1, 1.35:1 and 1.84:1
   from the top down, and 2.59:1 on the parchment at the bottom — every one of
   them a fail. So the pink is legal here for exactly the same reason the white
   body copy is: the padding rule above keeps every word out of the fade, at
   every window width. RE-MEASURED ON THE BUILT PAGE ON 2026-09-09, after the
   fade was SHORTENED to 80px — the direction that could actually have broken
   this, which is why it was re-measured rather than assumed. At 1470px the
   eyebrow's line box runs 94.0px to 111.6px down a 631.5px hero; the facts
   strip ends at 527.5px and the flat navy runs to 551.5px. At 375px the eyebrow
   starts at 83.2px, the facts strip ends at 825.8px of a 929.8px hero and the
   flat navy runs to 849.8px. Both leave the same 24px, because that 24px IS the
   "+ 24px" in the padding calc and it is the whole of the margin at every width
   and every fade length. Measured
   against what is actually painted behind them, the pink eyebrow is 5.71:1,
   the facts strip's white is 16.21:1 and its pink dots are 5.71:1 — the
   flat-navy figures, because nothing lands on a faded stop. Weaken the padding
   rule and this becomes an AA failure, not just an ugly one.

   THE THREE BLEND STOPS ARE GONE, 2026-09-09, AND MUST NOT COME BACK.
   #2c367a, #7980ac and #cccfe0 were hand-placed points along the navy-to-
   parchment blend, positioned so the perceived lightness followed a smoothstep.
   The maths was right and the result was visibly wrong: the operator looked at
   the deployed page and saw "three or four clear steps". A linear-gradient
   interpolates linearly BETWEEN stops, so each stop is a break in the rate of
   change, and a break in slope is what reads as a band — the human eye resolves
   a slope discontinuity far more readily than a value one. Placing stops to
   smooth a gradient makes it worse; it is the stops themselves that band.
   The rule now uses two stops and lets the browser interpolate in OKLab, which
   is perceptually uniform and needs no hand-placed midpoint at all.

   THE BAND IS NOT WHERE THE SCREENSHOT'S LOWER CORNER FLOATS. This comment
   used to say that it was, and offered it as the reason the empty band earned
   its height. It was not true even when it was written: at --hero-fade:230px
   the picture's lower corner sat at y=445.9 on a 781.5px hero whose fade did
   not start until y=551.5, so the corner was a hundred pixels clear of the band
   it was said to be floating in. NOTHING sits in this band. It is pure cost,
   which is the whole reason the fade is now as short as it can be while still
   reading as a wash. sec_hero() in build.py carries the other half of the rule
   — that nothing may be added below the facts list — and that part is true and
   stays.

   80px, DOWN FROM 230px, ON 2026-09-09. THIS NUMBER HAS NOW BEEN WRONG TWICE
   IN THE SAME DIRECTION, SO THE REASON IS WRITTEN DOWN RATHER THAN THE VALUE.
   The operator asked for the transition to be "more gradual". That was read as
   "longer" and the number went 190 -> 230. But this rule also owns
   padding-bottom, so every pixel of fade is a pixel of guaranteed EMPTY hero
   below the last line of content: at 230px the home page carried 254px of dead
   navy-into-grey under the facts strip, 33% of the hero's whole 781px height.
   The operator looked at that and said the transition was still wrong. It was.
   Lengthening it is what made it wrong.

   GRADUAL IS THE SHAPE OF THE RAMP, NOT ITS LENGTH. The smoothstep derived
   above is what makes this fade soft, and it is scale-free — every stop is a
   fraction of --hero-fade, so an 80px fade has exactly the same eased ends and
   the same worst-case 2.9 L* error as a 230px one. Only the amount of page
   spent on it changed. Measured on the built page at 1470px: the empty band
   below the facts strip is 104px instead of 254px (16.5% of the hero, not
   32.5%) and the hero is 631.5px tall instead of 781.5px.

   IF SOMEONE ASKS FOR "MORE GRADUAL" AGAIN, RESHAPE THE STOPS. Do not reach
   for this number: raising it does not soften anything, it only buys more
   empty navy, and that is the thing the operator has now objected to twice. */
.surface-tile.hero,.surface-tile.page-hero{
  /* WHERE THE FADE ENDS, as a token rather than a literal — both the sRGB
     fallback and the OKLab override below use it. It defaults to --parchment,
     which is what this rule has always faded to, so the home hero's computed
     background is unchanged byte for byte, and the 14px overlap derived
     against parchment further down still holds for it. It is a token only so
     the inner pages whose next section is WHITE can move the endpoint; see
     the :has() rule immediately below this one. */
  --hero-fade-to:var(--parchment);
  /* 200px, 2026-09-09, WITH THE PADDING DELIBERATELY DECOUPLED FROM IT.
     The two used to be locked together (padding = fade + 24) so that no text
     could ever sit on the fade. That coupling is why every attempt to lengthen
     the fade also lengthened the empty band, and why 230px produced 254px of
     dead hero that the operator rejected.
     THE COUPLING IS KEPT — it is the only thing that can see, structurally,
     that no text ends up somewhere illegible. What changed is the RATIO. It was
     1.0 (padding = the whole fade + 24), which forced every extra pixel of fade
     to become a pixel of dead hero. It is now 0.75, which is derived rather than
     picked: with the eased ramp, the facts strip lands 25% into the fade, where
     the mix is 15.6% toward parchment. Computed there — white 10.94:1, and the
     pink dots 3.85:1 against the 3:1 that WCAG 1.4.11 asks of a non-text mark.
     0.70 would still pass (pink 3.22:1) and 0.65 would not (2.63:1), so 0.75
     carries real margin rather than sitting on the line.
     IF YOU LENGTHEN THE FADE AGAIN, the ratio still holds — that is the point of
     expressing it as a ratio. If you change the EASING, re-run the numbers,
     because they come from the smoothstep, not from the length. */
  --hero-fade:200px;
  padding-bottom:calc(var(--hero-fade) * .75);
  /* TWO STOPS, INTERPOLATED IN OKLAB. Read the note above about why the three
     hand-placed blend stops that used to sit here were removed: they were the
     cause of the visible banding, not a defence against it. A linear-gradient
     interpolates STRAIGHT between consecutive stops, so every stop is a kink in
     the rate of change — and the eye is far more sensitive to a discontinuity in
     the SLOPE than to one in the value. Four stops over 80px meant four kinks in
     80px, which is exactly the "three or four clear steps" the operator saw.
     Two stops have no interior kink at all, by construction.
     The first declaration is the sRGB fallback and interpolates through a
     slightly muddy middle; the second overrides it wherever `in oklab` is
     understood, which is every current browser. OKLab is perceptually uniform,
     so the ramp reads even without any stop being placed by hand. Do not
     "improve" this by adding stops back. */
  background:linear-gradient(180deg,
    var(--tile) 0,
    var(--tile) calc(100% - var(--hero-fade)),
    var(--hero-fade-to) 100%);
  background:
    url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='140' height='140'><filter id='n' x='0' y='0' width='100%25' height='100%25'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/><feColorMatrix type='saturate' values='0'/></filter><rect width='140' height='140' filter='url(%23n)' opacity='0.055'/></svg>") repeat,
    /* EASED AT BOTH ENDS, 2026-09-09. The operator said the fade still read as
       "aggressive... not nice and gentle and progressive" and suggested
       extending its range. Those are two problems and both are fixed here.
       RANGE: --hero-fade went 80px -> 200px; see the note on that token.
       SHAPE: a two-stop ramp has a CORNER at each end — flat navy meets a
       linear climb, and that climb meets flat parchment. A corner is a
       discontinuity in the SLOPE, and the eye reads a slope discontinuity as an
       edge even when every colour along it is correct. THAT is what "aggressive"
       was; it is not a colour problem and re-picking colours would never have
       fixed it.
       The nine stops below sit on a smoothstep (3t^2 - 2t^3), so the ramp leaves
       navy at zero gradient and reaches parchment at zero gradient — no corner
       at either end. They are NOT hand-picked colours: each is a color-mix of
       exactly the two endpoint tokens at the smoothstep's own value, so no third
       hue can enter the way #4a3a86 once did, and the browser still interpolates
       in OKLab between them. Nine is enough that every remaining kink sits far
       below the threshold where it reads as a band; the dither above covers the
       8-bit quantisation, which stops cannot. */
    linear-gradient(180deg in oklab,
      var(--tile) 0,
      var(--tile) calc(100% - var(--hero-fade)),
      color-mix(in oklab, var(--tile), var(--hero-fade-to) 2.8%) calc(100% - var(--hero-fade) * 0.90),
      color-mix(in oklab, var(--tile), var(--hero-fade-to) 10.4%) calc(100% - var(--hero-fade) * 0.80),
      color-mix(in oklab, var(--tile), var(--hero-fade-to) 21.6%) calc(100% - var(--hero-fade) * 0.70),
      color-mix(in oklab, var(--tile), var(--hero-fade-to) 35.2%) calc(100% - var(--hero-fade) * 0.60),
      color-mix(in oklab, var(--tile), var(--hero-fade-to) 50.0%) calc(100% - var(--hero-fade) * 0.50),
      color-mix(in oklab, var(--tile), var(--hero-fade-to) 64.8%) calc(100% - var(--hero-fade) * 0.40),
      color-mix(in oklab, var(--tile), var(--hero-fade-to) 78.4%) calc(100% - var(--hero-fade) * 0.30),
      color-mix(in oklab, var(--tile), var(--hero-fade-to) 89.6%) calc(100% - var(--hero-fade) * 0.20),
      color-mix(in oklab, var(--tile), var(--hero-fade-to) 97.2%) calc(100% - var(--hero-fade) * 0.10),
      var(--hero-fade-to) 100%);
  position:relative;overflow:hidden}

/* THE FADE HAS TO END ON THE COLOUR THAT FOLLOWS IT. The rule above says in
   capitals that if the section after a hero is ever moved onto a different
   surface, this endpoint has to move with it. Putting seventeen page heroes onto
   the gradient did exactly that: eleven of them are followed by .surface-canvas,
   so the ramp landed on #f3f4f9 and then stepped hard to #ffffff at the section
   boundary — about 17 levels of blue, a razor edge the full width of the
   viewport. Written as the relationship rather than a list of pages, so it stays
   true when a section's surface changes. The home hero is followed by parchment
   and does not match this rule. (Eleven pages on 2026-09-09: 404, how-it-works,
   llm-info, our-story, pricing, who-we-help/trades-and-construction, and all
   five insights posts — the second pass split each article into a navy band
   above a white body, which put five more heroes above canvas. The six-name
   list that stood here predated that pass.) */
.surface-tile.page-hero:has(+ .surface-canvas){--hero-fade-to:var(--canvas)}

/* THE OTHER ARM, AND IT MATCHES NOTHING TODAY. Written on 2026-09-09 precisely
   because this file had already predicted the case: the section-rhythm note
   above says in capitals that a page hero landing directly above the navy CTA
   band is one ordinary editorial deletion away, on four pages. Counted off the
   built pages, no page hero is followed by a .surface-tile section right now,
   so this rule is dead on every one of the 18 pages. Without it, the day that
   deletion happens the fade would still run to parchment — its default — and
   paint a band of grey between two navy sections. Navy into navy is no visible
   fade at all, which is the correct answer for a tile-on-tile boundary. */
.surface-tile.page-hero:has(+ .surface-tile){--hero-fade-to:var(--tile)}

/* The hero starts UNDER the fixed bar, not below it.
   body reserves 58px of its own padding to clear the bar, and that strip shows
   the BODY's colour, which is white. While every hero was white too that was
   invisible. A hero is navy now, so without this rule the bar hiding on scroll
   would uncover a white band sitting above a navy hero. The reservation is
   cancelled with a negative margin and handed straight back as padding, so
   nothing below moves and the navy runs to the top of the window.
   58px is the bar height and is a COUPLED NUMBER — see the note on
   body{padding-top} above; this is a fourth place that describes the same bar.
   All eighteen heroes do this — the home page's and the seventeen page heroes —
   because each is the first section on its page AND is a different colour from
   the white body showing behind the bar. Nothing else qualifies: the navy CTA
   band is never the first section on a page, and everything else that can open
   a page is white already, so there is no band to uncover. */
main>section:first-child.surface-tile.hero,main>section:first-child.surface-tile.page-hero{
  margin-top:-58px;padding-top:calc(58px + var(--section) * .875)}
/* 70px OF CLEAR AIR UNDER THE BAR, not --section-top's 36px. Operator,
   2026-09-09: the headline sat close enough to the fixed bar that the two
   read as one object rather than as chrome and then a section.
   The 58px is not padding in the design sense — it is the height of the bar
   this section deliberately starts underneath (see the margin-top above), so
   only the second term is the gap a reader actually sees. Doubling the gap
   therefore means 36 -> 70 here, NOT 94 -> 188, which would have pushed the
   facts strip near the fold on a 13-inch laptop.
   It is .875 of --section rather than --section-top's .45, and it stays
   DERIVED from the token on purpose. Writing it as a flat 70px turned SUR-003
   red, and that guard is right: a hard-coded section-sized padding is exactly
   how the five hand-tuned clamps this rhythm replaced came about, and it would
   not have changed a single measurement on the day it was added. Deriving it
   also means the narrow-screen step-down carries: --section drops to 56px
   under 720px, so the clearance becomes 49px there without a second rule. */

/* The scrim: a dark fade down over the top of the section, above the
   screenshot and below the header.
   WHAT IT IS FOR, precisely, because "it looks nicer" would not survive a
   tidy-up: the screenshot rises up behind the bar, and the top of a screenshot
   is its own bright browser chrome. The header is opaque, so nothing shows
   through it while it is there — but it HIDES ON SCROLL, and without the scrim
   it would slide away to reveal a hard-edged block of near-white UI against
   the navy. The fade makes the picture end in the background colour instead.
   rgba(9,20,100,...) is var(--tile) with alpha. CSS cannot take the alpha off
   a hex custom property without color-mix(), and this file stays conservative,
   so the rgb triple is written out — if --tile changes, this changes with it.
   pointer-events:none so it can never swallow a click on the bar above it.

   96px, DOWN FROM 170px ON 2026-09-08. THE SHORTENING WAS RIGHT. THE STORY
   THIS COMMENT TOLD ABOUT IT WAS NOT, AND IS CORRECTED HERE (2026-09-09).

   WHAT IT USED TO SAY, and why it has been taken out: that the pink eyebrow
   "was rendering at 1.80-2.38:1, a fail on the live site right now", and it
   invited the operator to check that against their own recording. That was
   false and it would not have survived the check. At baseline the hero eyebrow
   was WHITE, not pink, and white under this scrim was passing. The very same
   wave that shortened the scrim is the wave that turned the eyebrow pink —
   settled decision 12 switching the pink text roles back on for the navy
   surface. So there was never a moment when a pink eyebrow was failing on the
   live site, and telling the operator to go and find one was telling them to go
   and find something that was not there.

   WHAT IS TRUE, AND WHY 96px IS STILL THE RIGHT NUMBER. The scrim is full-width
   and z-index:2. The text column is not positioned at all, so it paints in
   normal flow — underneath. The scrim therefore lies over the EYEBROW AND THE
   HEADLINE, not just over the picture, and it is 74% navy for most of its
   length. At 170px it laid a 61.3% veil across the top of the eyebrow's line
   box (y=94px) easing to 47.1% at its bottom (y=111.6px). Through that veil,
   #ff5cae goes from the 5.71:1 this file records on the flat navy down to
   1.80:1 at the top of the band and 2.37:1 at the bottom — nowhere near the
   4.5:1 that 12px text needs. White is compromised too, though less: 16.21:1
   falls to 3.37:1 at the top of the band, and only climbs back past 4.5:1
   partway down it. Pink arriving on the eyebrow in the same wave is what turned
   a thin case into a clear failure, and shortening the scrim to 96px is what
   let both changes ship together.

   THOSE RATIOS ARE A CALCULATION, NOT AN OBSERVATION, and are labelled as one
   so nobody goes hunting for a screenshot of them: they are #ff5cae and #ffffff
   composited over #091464 at the alpha this rule's own gradient produces at
   each y, when its height is 170px. That scrim is not on the site any more, so
   there is nothing left to photograph.

   WHAT CAN BE CHECKED ON THE BUILT PAGE, and was, on 2026-09-09: the eyebrow's
   line box still starts at y=94px at every desktop width from 901px to 2560px,
   the 96px scrim has 2.9% left at that point, and the pink measures 5.44:1
   there. Four pixels lower, at the eyebrow's cap line, the scrim has ended
   entirely and it is the full 5.71:1.

   The picture no longer needs the tall version. It used to be pushed 104px up
   into the bar, so a lot of bright browser chrome was hidden behind an opaque
   header that hides on scroll. It now stops near the bar's bottom edge instead
   of a hundred pixels above it, so there is almost nothing up there to fade.
   96px reaches zero at 96px,
   which leaves the eyebrow at 94px on a 3% veil — 5.44:1, still AA — and clears
   everything below it completely, while the picture's far top corner still
   lands inside the scrim and fades out instead of cutting.

   THE FAR CORNER'S OWN NUMBERS MOVED WHEN THE PICTURE WAS NARROWED ON
   2026-09-09, and are re-measured here rather than left at the old ones. This
   comment used to say the corner was "tucked right under the bar at 59px" on a
   53% veil. A narrower box at the same tilt does not reach as high, so on the
   built page the corner now sits at 73.0px at 961, 72.4px at 1024, 70.3px at
   1366 and 69.5px at 1470 — those four are measured. Reading this rule's own
   gradient at those heights gives a 33% to 38% veil rather than 53%; like the
   ratios above, the alpha is a calculation off the declaration below, not
   something photographed. It is still a veil and the corner is still well
   inside the 96px, which is all this rule has to guarantee; checked at 1470px
   by hiding the header on the built page, the top
   edge fades into the navy with no hard cut. On a large monitor the picture is
   bigger again and the corner climbs back: 59.9px and a 52% veil at 1920,
   45.3px and 72% at 2560 — again, the heights measured, the alphas computed.

   THE HEIGHT AND THE TEXT POSITION ARE COUPLED. If the headline ever grows a
   line, or --section-top changes, the eyebrow moves down and this gets safer;
   if anything moves the text UP, re-measure. The safe rule is that the scrim
   must reach zero above the first line of text. */
.surface-tile.hero .scrim{
  position:absolute;top:0;left:0;right:0;height:96px;z-index:2;pointer-events:none;
  background:linear-gradient(180deg,
    rgba(9,20,100,.97) 0%, rgba(9,20,100,.74) 46%, rgba(9,20,100,0) 100%)}

/* The stacking order is shot (1) -> scrim (2) -> header (90, set on
   .site-header). Three layers, three numbers, all written down. */
.heroshot{position:relative;z-index:1}

/* Where the picture sits. Out to the right, so it runs off the edge of the
   window rather than stopping politely at the content grid — the operator
   called that out as the part to keep — and just far enough up that the tilt
   drops its near corner onto the top line of the text beside it.
   Both are undone on a phone, below.

   -13px, NOT the -104px this used to be. Reviewed 2026-09-08 (recording, from
   00:13): the picture was riding too high, its top edge nearly a hundred pixels
   above the words. The ask was that its top edge line up with the text, and the
   line meant is the EYEBROW — "Fully managed payroll · New Zealand" — because
   that is the first line of the column and so the top of the text block as a
   reader sees it.

   IT WAS -19px UNTIL 2026-09-09, when the picture was narrowed from 90% of
   this column to 66%. A narrower box at the same tilt does not drop its near
   corner as far, so the corner rose about 6px and the alignment the operator
   asked for started coming undone on its own. -13px is that 6px handed back.
   The rule did not change; only the constant did. Anyone who changes
   .shot-angle's width again has to re-measure this — that coupling is the
   price of aligning a corner of a tilted box rather than an edge of a flat one.

   WHICH CORNER IS "THE TOP EDGE" MATTERS, because the picture is tilted and so
   its top edge is a slope, not a level line. It rises to the right and leaves
   the window before it gets there, so the top-right corner is never on screen
   at any width this rule applies to. The corner a reader actually sees as the
   top of the picture is the near one on the left, and that is the one aligned
   here. At 1440px it lands at 94.2px against the eyebrow's own top of 94.0px.
   The far corner reaches 70.1px there — just BELOW the fixed bar's 58px bottom
   edge rather than tucked under it, which is where narrowing the picture on
   2026-09-09 put it. It comes back level with the bar at 1920px and rises above
   it at 2560px. Either way it lands inside the scrim, which is the only thing
   that depends on where it is; the .scrim rule above carries those numbers.

   THE NUMBER IS ONLY THIS SMALL AND THIS STABLE BECAUSE OF THE align-items
   RULE THAT COMES WITH IT. .split centres its columns in the row by default.
   Under centring the picture's own box height feeds back into where that box
   starts, so half of any margin change is eaten by re-centring and the offset
   that lines the corner up ranges from -34px at 1680 to -96px at 1024 — a
   number that is right at one window width and wrong at every other. Aligned to
   the TOP of the row the margin moves the picture one-for-one, and the correct
   value barely moves. Re-measured on the built page on 2026-09-09 with -13px
   and width:66% both actually in place, the near corner lands this far from the
   eyebrow's top of 94.0px: 3.1px high at 961, 2.4px high at 1024, 0.3px high at
   1280, dead level at 1366, 0.2px low at 1440, at 1470 and at 1680, 0.3px low
   at 1920 and 0.9px low at 2560. Delete the align-items line and this number
   stops working, and no single number replaces it.

   IT IS WRITTEN OUT HERE RATHER THAN BORROWING THE .top MODIFIER. .split.top
   does say align-items:start, and using it was the first thing tried on
   2026-09-08 — but further down this file .split.top ALSO re-weights the
   columns to 1.35fr / .65fr for prose-with-a-visual layouts, which took a fifth
   off the width of the screenshot the moment it was applied. Two sections
   wanting their columns aligned to the top is not two sections wanting the same
   layout, so the hero says the one thing it means and nothing else.

   Only the row containing the two columns is affected. The facts strip is the
   grid's third item and sits alone on row two, where start and centre are the
   same thing; and the text column is the taller of the two, so it defines the
   row height and was already flush with the top under centring. Nothing but the
   picture moves. */
/* THE SECTION AFTER THE HERO SITS UP INSIDE THE END OF THE WASH.
   Operator, 2026-09-09: "move this section up a bit, into the gradient, the gap
   between those sections is too large." It was 230px from the facts strip to
   the next eyebrow — 150px of hero (the fade's tail, which is contrast-locked;
   see --hero-fade) plus 80px of this section's own top padding.
   The 150px cannot move: it is what keeps the facts strip out of the ramp.
   The 80px can, and does.
   THE NUMBERS ABOVE ARE THE HOME PAGE'S. A page hero's tail is 180px at the
   same fade, not 150px, because .surface-tile.page-hero takes the ratio to .9.
   THE -14px OVERLAP IS THE PART THAT NEEDS EXPLAINING. The section after a hero
   paints a flat colour — parchment under the home hero and under six of the
   seventeen page heroes, white under the other eleven — so overlapping the hero means
   covering the tail of the ramp with a flat block, which shows a seam if the
   ramp has not effectively arrived yet.
   Measured against the smoothstep: 14px above the hero's bottom edge the ramp is
   98.6% of the way to its endpoint, about three 8-bit levels short, which the
   dither covers. At 30px it is 93.9% and the seam would be visible. So 14px is
   the measured safe overlap, not a round number — if the easing or the fade
   length changes, re-derive it rather than keeping this value.
   ONE NUMBER SERVES BOTH ENDPOINTS. The 98.6% is a fraction of --hero-fade, so
   it does not move with the fade's length, and the remaining 1.4% is the same
   three 8-bit levels on the navy-to-white span as on the navy-to-parchment one
   — the two spans differ by a dozen levels out of two hundred and thirty odd.
   The white-ended page heroes need no separate overlap. */
.surface-tile.hero + section,.surface-tile.page-hero + section{margin-top:-14px;padding-top:16px}
/* ...and its bottom padding, the last of the vertical saving. Scoped to the
   hero boundary — eighteen of them now, one on every page, since every page
   opens on a hero and none of them is the only section on its page; the
   80px rhythm is unchanged everywhere else on the site. */
.surface-tile.hero + section,.surface-tile.page-hero + section{padding-bottom:56px}
@media (max-width:960px){
  .surface-tile.hero + section,.surface-tile.page-hero + section{margin-top:0;padding-top:var(--section-top)}
}

.surface-tile.hero .wrap.split{align-items:center;
  /* A FIXED TRACK, NOT A FRACTION — this is what stops the picture drifting
     right on a wide monitor. With the site-wide `1.12fr .88fr` the visual
     column grew with the viewport, and because the picture was pinned to that
     column's RIGHT edge at 66% of its width, the empty gap between the text and
     the picture grew with it: 300px at 1470, 389px at 1920, and on the
     operator's ~2000px screen the picture read as flung into the far corner.
     A fixed 280px track plus the bleed means the picture's box is the same
     shape at every width, and the gap is just the grid gap. */
  grid-template-columns:1fr 280px}

/* ── THE PICTURE STILL LEAVES THE WINDOW ON A BIG MONITOR ──────────────
   Retuned 2026-09-09. The block above used to end with the honest admission
   that "past about 1600px the picture stops running off the right edge at all,
   and sits inside it". That is now fixed, and this is the whole of the fix.

   THE DEFECT, MEASURED ON THE BUILT PAGE. The far corner of the tilted
   screenshot fell 13.7px SHORT of the right edge at 1680px and 88.7px short at
   1920px. Running off the edge is the one thing about this composition the
   operator named twice and asked to keep, so on a large monitor the part they
   praised was simply not there.

   WHY IT DRIFTS. .wrap is capped at 1180px and centred, so the space between
   the wrap and the window edge grows by half of every pixel added to the
   window. -17vw only grows by 0.17 of it. Above about 1600px the gap wins and
   the picture falls back inside. It is arithmetic, not a rounding error, and it
   gets worse with every wider screen that ships.

   THE FIX IS ONE max() AND IT CHANGES NOTHING AT OR BELOW 1440px.
   50vw is exactly the rate the gap grows at; 475px is chosen so the two terms
   are EQUAL at 1439px (17vw = 50vw - 475px when 33vw = 475px). Below that
   width the 17vw term is the larger of the two and wins, so every measurement
   this file already records for 1024, 1280 and 1440 still stands, unaltered —
   1440 was 61.8px of overhang before and is 62.0px after, which is sub-pixel
   layout rounding rather than a change. Above it the new term takes over
   smoothly, because max() of two lines that meet has no step in it.

   MEASURED WHEN THE max() WENT IN, BEFORE -> AFTER, as pixels the picture's far
   corner passes the right edge of the window (negative means it stopped short).
   These are the 90%-width figures — the size the picture was that day:
     1280px   +112.5  ->  +112.5   (unchanged, 17vw still wins)
     1440px    +61.8  ->   +62.0   (unchanged)
     1680px    -13.7  ->   +74.3
     1920px    -88.7  ->   +90.5
     2560px   -286.5  ->  +155.2

   RE-MEASURED ON 2026-09-09 AFTER THE PICTURE WAS NARROWED to 66%, because
   narrowing it eats into the overhang this whole block exists to protect. It
   survives at every width, with room to spare, and NO compensation was needed
   here — .shot-angle is pinned to the right of this column by margin-left:auto,
   so taking width off it takes the width off the LEFT side and leaves the right
   edge where the bleed put it. What is lost is only the extra reach the tilt's
   own perspective gave a wider box:
     1024px  +121.1     1440px   +50.8     1920px   +61.4
     1280px  +103.0     1470px   +51.0     2560px   +88.9
                        1680px   +54.7
   document.scrollWidth - clientWidth is 0 at every one of those widths, before
   and after, and at 375px: the section's own overflow:hidden clips the
   overhang, which is what has always kept the body from scrolling sideways.

   WHAT GROWS IS THE PICTURE, NOT ITS DISTANCE FROM THE TEXT, and that is the
   reason this lever was chosen over sliding the whole column right. The column
   is pinned at its left by the grid track, so a bigger negative right margin
   widens it rightwards rather than moving it. Measured as a share of the window
   the picture is 31.3% at 1440 and at 1470, 31.5% at 1680, 31.7% at 1920 and
   32.0% at 2560 — it holds a steady proportion instead of shrinking away on
   every larger screen. (Those percentages were 42.7 / 42.9 / 43.2 / 43.6 until
   2026-09-09; what changed is .shot-angle's width, not this rule.)

   ONE HONEST COST, RECORDED RATHER THAN GLOSSED. This block used to reject
   "slide the column right" partly because it opened the gap between the text
   column's right edge and the picture's left edge from 136.3px to 225-315px at
   1920. Narrowing the picture opens that same gap to 380.0px at 1920 and
   300.1px at 1470, because the picture is pinned right and the width comes off
   the left. That is the unavoidable price of a smaller picture that still runs
   off the right edge, and the operator asked for both. It was looked at on the
   built page at 1470 and 1920 before being accepted. The argument against
   sliding the column still stands on its own terms: sliding buys a bigger gap
   AND a smaller picture on screen, where this buys the gap and keeps the bleed.

   THE margin-top CORRECTION IS NOT A SECOND DECISION — IT IS THE PRICE OF THE
   FIRST, PAID BACK. A wider picture at the same tilt drops its near top corner
   further, and that corner is the one the previous wave lined up with the
   eyebrow. Uncorrected, and at the 90% width of the day, the corner fell from
   1.0px below the eyebrow's box top to 3.5px at 1680 and 7.5px at 1920 — the
   alignment the operator asked for,
   quietly coming undone as the fix took effect. .033 is the measured slope:
   the corner drops 3.3px for every 100px the bleed grows.

   .033 WAS RE-VERIFIED ON 2026-09-09 AT THE NEW 66% WIDTH and is still right,
   which was not obvious in advance — the picture is a percentage of a column
   the bleed itself widens, so the slope could have moved with it. Measured on
   the built page, near corner against the eyebrow's top of 94.0px, with the
   correction switched off (a flat margin-top:-13px) and then on:
     1680px    96.8  ->   94.2
     1920px    99.5  ->   94.3
     2560px   107.1  ->   94.9
   Off, the drift is exactly the defect this term was written to cancel; on, the
   corner holds within a pixel from 1280px to 2560px. The correction is written
   against 17vw rather than against a pixel constant so it is exactly ZERO
   everywhere the max() still picks 17vw. Nothing below 1439px can be touched by
   it, however either number is edited later.

   IF THE TILT ANGLES EVER CHANGE, .033 IS WRONG and has to be re-measured; it
   describes rotateZ(3deg) on this box and nothing else. The bleed itself is
   safe from that — it is pure layout.

   The headline does not move: it is in the other grid column, both tracks are
   fr units, and a margin on this item cannot resize them. Verified at all five
   widths — the h1's left, top and width are identical before and after. */
/* --hero-bleed MOVED UP TO THE SECTION 2026-09-09. It was declared on
   .col-visual, where only .col-visual could read it. The facts rule below now
   needs the same number to line itself up against the picture, and a second
   copy of an expression this load-bearing is exactly how two rules end up
   disagreeing. Declared once, read twice. */
.surface-tile.hero{--hero-bleed:max(17vw, 50vw - 475px)}
.surface-tile.hero .col-visual{
  /* THE margin-top CHANGED MEANING 2026-09-09, and the number is measured.
     It used to read
         margin-top:calc(14px - (var(--hero-bleed) - 17vw) * .033);
     and its job was to line the picture's near top corner up with the eyebrow
     while the row was top-aligned. The row is centred now (operator: "keep
     that image centred to the hero section text"), so there is no top edge to
     line up against and that formula is void.

     WHAT REPLACED IT IS NOT ZERO, AND MY FIRST ATTEMPT AT THIS WAS TO DELETE
     IT — worth recording, because deleting it made the centring WORSE and the
     reason is not obvious. A 3D tilt's PROJECTED box is not symmetric about
     the element's layout box: rotateX drops the near edge further than it
     lifts the far one, so centring the layout box leaves the picture sitting
     high. Measured at 1470px, offset of the picture's visual centre from the
     text's: 0px margin gives -4.3px, 8px gives -0.3px, 9px gives +0.2px,
     13.67px (the old value) gives +2.5px. So the old value was closer to right
     than zero was.

     9px, not a formula. The residue scales with the picture's width and so
     with the bleed: measured +0.2px at 1470, -0.2px at 1280 and +4.4px at
     1920. A formula fitted to a 4px effect is false precision. Re-measure the
     same way if the tilt or the width changes; do not reason about it. */
  margin-top:9px;
  margin-right:calc(-1 * var(--hero-bleed))}

/* ── the rule above the facts strip, widened ──────────────────────────────
   Operator, 2026-09-09: "let's just widen the horizontal bar that shows, just
   to help keep all of that in balance." The picture lost 20% of its width in
   the same change, so the strip that used to sit well inside it now reaches
   further right than it does; extending the rule closes that gap back up.

   IT DELIBERATELY BREAKS THE RIGHT MARGIN, which is the licence he gave for
   this section and this section only: "we don't have to honour that left-hand
   margin specifically just for that very first hero section". Where the clamp's
   upper bound binds, the rule ends exactly 129px from the window edge against
   209px on the left. That asymmetry is the effect, not a rounding error.

   THE clamp IS A SAFETY RAIL, NOT A TASTE CALL. The picture's right edge sits
   at `wrap content right - 56px + 0.8 x bleed`, and following it exactly would
   put the rule 4px PAST the window at 1280px — it would be clipped by the
   hero's overflow:hidden and read as a line running off the screen. So a cap
   is still needed at 1280px specifically.

   THE ORIGINAL CAP, `50vw - 655px`, over-corrected: built to stop clipping at
   exactly 1280px, it also suppressed the preferred formula at every width up
   to about 2190px — so from 1280px to nearly 2190px the bar sat wherever the
   cap put it, not wherever the picture's edge actually was, which is the same
   defect this section exists to avoid. Widened 2026-09-14 to `90vw - 1152px`:
   1152 is chosen so the cap is still exactly 0 at 1280px (identical there —
   129px right margin, no extension), and the steeper 90vw slope means the cap
   stops binding by about 1435px instead of 2190px, so the bar tracks the
   picture's true edge from 1440px up. Computed, NOT yet re-measured live:
   right edge 1166px at 1280 (unchanged), 1386px at 1440 (was 1311 — the
   picture's true edge measures ~1385, so this now lands within a pixel of it,
   versus 75px short before), 1818px at 1920 (was 1791; 102px right margin,
   still inside the window). FIRST-PASS NUMBER — 1152 was picked to zero out
   at 1280px exactly, not fitted to a live screenshot; re-measure before
   treating it as final.

   RE-MEASURED LIVE 2026-09-14, PER OPERATOR FEEDBACK: at 1920px the right
   edge landed 1818px against the picture's true corner at 1828.8px — 10.8px
   short, visible on close inspection. Operator asked for "a tiny bit wider"
   here. FIRST INSTINCT WAS WRONG: adding a flat pixel amount on top of the
   clamp seemed safest, since it can't touch the 1280px cap. Tried it, then
   measured 1440px too, not just the width that was complained about — the
   existing formula was already landing within a pixel of the picture's true
   edge there (1386 vs ~1385), so any flat addition overshoots PAST the
   picture at 1440px by that same amount. A fix that repairs 1920px by
   breaking 1440px is not a fix.

   The 56 below is not a free constant — it is shared with .col-text's pull
   below, which is a genuine reason to leave it alone, but that reasoning
   doesn't apply here: this copy only feeds .facts's OWN margin-right, never
   .col-text's, so retuning it cannot move the headline. And the 1280px cap
   survives untouched regardless of what this constant is, because the cap's
   OWN zero-crossing (`90vw - 1152px = 0` at exactly 1280px) has no dependency
   on it — clamp(0, anything, 0) is 0. So lowering 56 to 45 was safe to try:
   it adds a flat 11px to the UNCAPPED preferred value at every width, but
   the cap still exists above it, so anywhere the cap already binds or
   nearly binds, the cap absorbs most of that 11px automatically rather than
   passing all of it through. Confirmed live: 1280px unchanged at 1166px
   (still capped to 0 — the cap doesn't care what the preferred value under
   it is); 1440px 1386px -> 1390px against the picture's 1385.1px (was
   already ~1px over, now ~5px over — the cap started absorbing part of the
   increase here, exactly because 1440px sits close to where the cap stops
   binding); 1920px 1818px -> 1829px against 1828.8px (was 10.8px short, now
   0.2px over — the width the operator actually flagged). 2560px improved
   too as a side effect (46px short -> 35px short) without being asked, but
   is still short; not chased further, since nobody has flagged that width
   and closing it would need the formula's slope retuned, not just this
   constant. Net: 1920px goes from visibly short to essentially exact, at
   the cost of 1440px going from a near-miss to a small, much less
   noticeable overshoot the other way — a better trade than the flat
   addition, which made both worse in opposite directions. */
.surface-tile.hero .facts{
  margin-right:calc(-1 * clamp(0px, 0.8 * var(--hero-bleed) - 45px, 90vw - 1152px));
  /* ── left-hand match, added 2026-09-14 ──────────────────────────────────
     .facts never got the .col-text treatment below — only this right-side
     rule existed, so the line (and the "$149/month" label inside it) sat at
     the grid's natural column start (434px at 1920px, level with the nav)
     while the headline beside it already bled left to 102px. Operator
     flagged exactly this gap and marked where the label should start.
     Copied VERBATIM from .col-text rather than re-derived: .facts spans
     grid-column:1/-1 from the same column .col-text opens, so the identical
     expression lands both on the same left edge at every width by
     construction, not by coincidence to maintain separately. */
  margin-left:calc(-1 * clamp(0px, 0.8 * var(--hero-bleed) - 56px, max(0px, (100vw - 1180px) / 2)))}

/* ── re-centring the text column, first pass ──────────────────────────────
   THE HERO READS OFF-CENTRE because .col-visual's right-side bleed (above)
   was added after this row was laid out centred without it, and the text
   column never got a matching pull on its own side. Same licence as above,
   and as .shot-angle's width below: "we don't have to honour that left-hand
   margin specifically just for that very first hero section."

   PULLS AGAINST THE CORRECTED EDGE, NOT THE OLD ONE — this is layered on top
   of the .facts fix above, not independent of it. .facts now reaches the
   picture's true right edge at `wrap content right - 56px + 0.8 x bleed`
   rather than the container's edge, so the same expression is used here for
   the left-hand pull. That is not a matched-for-symmetry choice, it falls out
   of the wrap's own geometry: its left inset equals its right inset at every
   width (equal padding, equal side margin), so extending the right edge by X
   and the left edge by that same X keeps both margins equal to the window at
   every width, not just one.

   CAPPED FOR A DIFFERENT REASON THAN .facts'S CAP. .facts bleeds a decorative
   line that can take a hairline clip; this bleeds the box holding the actual
   headline and button row, and it has no left padding of its own — past the
   viewport's left edge would clip real text, not a sliver of a line.

   LIVE-CHECKED AT 1280PX AND CORRECTED. The first-pass cap added --wrap's own
   side margin AND its inner --pad together, reasoning both were "room
   available" before text would clip. Live in a browser at 1280px, `.wrap`'s
   own padding turned out to be exactly the column's only breathing room, not
   spare capacity — that first-pass cap pulled the column flush to 0px inset,
   headline touching the literal window edge. Nothing clipped, so the original
   safety goal was technically met, but "not clipped" and "not touching the
   glass" are different bars, and this section needs the second one.

   The cap now spends only the wrap's own auto-centring margin, `max(0px,
   (100vw - 1180px) / 2)` — never the wrap's own --pad on top of it — so the
   column can never be pulled tighter than the site's standard content inset.
   Below 1180px the wrap has no auto-margin to give up at all, so the cap is
   0 and the column doesn't move; from 1180px up to where the preferred
   formula drops back under the cap (~1540px) the column holds at a constant
   --pad inset instead of the window edge; above that the preferred formula
   runs uncapped, same as before — this part was already correct and is
   untouched. Confirmed live: 64px inset at both 1280px and 1440px (matches
   --pad's own clamped value at those widths), 102px at 1920px (uncapped,
   identical to the first pass — this width was never the problem). */
.surface-tile.hero .col-text{
  margin-left:calc(-1 * clamp(0px, 0.8 * var(--hero-bleed) - 56px, max(0px, (100vw - 1180px) / 2)))}

/* The tilt.
   PERSPECTIVE DISTANCE IS THE LEVER, NOT THE ROTATION ANGLES — recorded here
   so it is not re-derived a third time. An earlier attempt used
   perspective(2400px) and read as completely flat: 2400px of viewing distance
   on an element about 950px wide is very nearly orthographic, and a wide 16:9
   picture foreshortens far less than a tall one, so the identical rotation
   that had looked right on a narrow CSS mock-up did nothing at all on the real
   screenshot. 1350px brings the vanishing point close enough for the tilt to
   be obvious. Do not raise it back towards 2400, and do not try to compensate
   by increasing rotateX/rotateY — that skews the UI inside the screenshot into
   unreadability long before it looks more tilted.
   transform-origin sits high so the picture pivots about its own top third and
   the bottom swings away, rather than see-sawing about its middle. */
.shot{border-radius:10px;overflow:hidden;box-shadow:var(--shadow)}

/* THE ROTATIONS ARE UNTOUCHED. The operator liked the angle and said so, so
   nothing in the transform below changed on 2026-09-08 — only the size of the
   box it is applied to.

   66%, DOWN FROM 90%, ON 2026-09-09 — AND 90% WAS TOO TIMID, WHICH IS THE
   POINT OF THIS PARAGRAPH. The operator asked for the picture to be "a little
   bit smaller"; ten per cent came off, and they came back and said it was
   "still way too large on the right hand side". They were right, and the reason
   ten per cent could not have fixed it is arithmetic that was never done at the
   time: at 90% the picture was 627px wide at a 1470px window — 42.7% of the
   whole window and WIDER THAN THE 556px TEXT COLUMN NEXT TO IT. A picture that
   out-measures the headline it is illustrating does not read as a supporting
   image whatever the tilt does, so the composition kept looking front-heavy on
   the right however the number was nudged.

   66% is the number that makes it secondary and it was chosen against the text
   column, not by feel: 460px at 1470px, which is 31.3% of the window and 83% of
   the text column beside it. Measured on the built page, the same rule gives
   434px at 1280, 450px at 1440, 529px at 1680, 608px at 1920 — a steady 31-34%
   of the window from 1280px up. Below that the wrap stops being capped and the
   share creeps up: 371px at 1024 and 348px at 961 are both 36.2%, which is
   right, because a narrow window has less room to spare for white space than a
   wide one. All of those are the rendered CSS box, measured, not the intent.

   THE TEST TO APPLY IF THIS IS EVER RE-TUNED is the text column — but only up
   to a point, and the point is worth knowing before someone reads a wide
   monitor as a regression. .wrap caps at 1180px, so from about 1300px up the
   text column is a flat ~556px while the bleed keeps widening the picture.
   Measured, picture as a share of that column: 77% at 1280, 81% at 1440, 83% at
   1470, 95% at 1680, level at 1760 and 109% at 1920. So on a very wide monitor
   the picture is the wider of the two again. That is inherent to the bleed —
   see the max() block above, which is the rule that must not be broken — and it
   is why the share-of-WINDOW figure, not the share-of-column one, is the number
   to hold at once past ~1760px.

   A SIDE EFFECT WORTH KNOWING: the markup's own srcset hint (emitted by
   build.py) says `sizes="... 450px"` above 1240px. At 90% the element was
   painting 627px against a 450px hint and the browser was choosing its source
   for the wrong size. At 66% the two agree at 1440-1470. Nothing here depends
   on that, but it stops being wrong.

   IT IS DONE WITH width, NOT WITH scale() IN THE TRANSFORM, for two reasons.
   The picture is pinned to the RIGHT of its column by margin-left:auto, so the
   shrink eats into the left side and the overhang past the window edge — the
   other thing the operator said to keep — survives: at 1440 it was 69px at full
   width, 62px at 90% and is 50.8px at 66%, all of it still past the edge of the
   window. A scale() shrinks about the transform-origin instead, which pulls the
   right-hand side back inboard and had cut that overhang to 33px in testing
   at 90%, before this rule was made smaller again. And width feeds the real
   raster: the browser picks a source from the srcset for the size it is
   actually painting, where a
   transform scale would have had it fetch the larger file and squash it.

   THE OVERHANG NOW SURVIVES ON A BIG MONITOR TOO, which it did not when this
   note was first written. That paragraph used to record an unfixed defect —
   past about 1600px the picture stopped running off the right edge and sat
   inside it, 13.7px short at 1680 and 88.7px short at 1920 — and it was true
   when written. It was fixed on 2026-09-09 by the max() on .col-visual's
   margin-right above, not by touching anything in this rule, and it SURVIVED
   the narrowing later the same day: +103.0px at 1280, +50.8 at 1440, +54.7 at
   1680, +61.4 at 1920, +88.9 at 2560, with no horizontal scrollbar at any of
   them. margin-left:auto is what makes that true — it takes the width off the
   left. The rotations are still untouched. Read that block for the
   measurements. */
.shot-angle{
  /* 80%, NOT 100% — operator, 2026-09-09: "the image clearly blows out that
     margin on the right-hand side, and it just looks a little bit weird...
     make the image about 80% of its current size."
     THIS REVERSES A DECISION RECORDED TWICE ABOVE. The long block on
     .col-visual's bleed exists because running off the right edge was
     "the one thing about this composition the operator named twice and asked
     to keep", and a max() was added to defend it on large monitors. He has now
     asked for the opposite. The bleed machinery is deliberately LEFT IN PLACE
     rather than ripped out: it still sets where the picture's box sits, the
     max() still stops the box drifting inward on a wide screen, and if he wants
     some overhang back it is this one number that changes, not that whole
     block.

     MEASURED ACROSS THE RANGE, and it is not "inside" everywhere — said plainly
     rather than letting one width stand for all. Pixels the picture's right
     edge falls SHORT of the window edge (negative = still overhanging):
         1280px   -2.8   a hairline overhang, clipped, no scrollbar
         1470px  +57.6
         1920px  +91.2
     At 1470 that is a move from 1525.6px — 55.6px past the window and clipped —
     to 1412.4px, well inside. The 2.8px left at 1280 is the 17vw term of the
     bleed, not a composition; document.scrollWidth equals clientWidth at all
     three widths.
     margin-left:0 keeps the box's left edge where it was, so the 20% comes off
     the RIGHT. That is deliberate and it is what he authorised when he said
     "we don't have to honour that left-hand margin specifically just for that
     very first hero section". */
  width:80%;margin-left:0;
  transform:perspective(1350px) rotateX(17deg) rotateY(-19deg) rotateZ(3deg);
  transform-origin:50% 15%}

/* NO background under .shot, on purpose. The master's corners are very
   slightly see-through, so the navy behind is what should show at them — and
   the JPEG fallback is flattened onto that same navy by _source/images.py.
   Put a fill here and the two disagree. */

/* Phone and narrow tablet. 960px, NOT 900: .split collapses to one column at
   960 (further down this file) and that is also where the desktop nav becomes
   the mobile toggle. A nearer breakpoint would leave the hero in two columns
   while the navigation had already collapsed.
   THE PHONE'S .shot-angle AND .col-visual USED TO BE OVERRIDDEN HERE. Both
   have moved to the "THE PHONE" block at the very end of this file, where the
   rest of the mobile home page now lives — read that one, not this note, for
   what the picture does on a phone.

   What was here, and why it is worth knowing it existed: the tilt was set to
   `transform:none` and the width back to 100%, on the grounds that a
   3D-transformed screenshot at 375px is unreadable and its transformed
   bounding box can push the BODY sideways. The second half of that was the
   real argument and it still holds — what changed is that the picture is now
   clipped by .surface-tile.hero's own `overflow:hidden` before it can reach
   the body, so a deliberate bleed is safe where an accidental one was not.
   The operator asked for the angle back on 2026-09-09; it returns reduced,
   not copied. Test case HOR-001 is what keeps the body from scrolling
   sideways, rather than this rule's absence.

   Nothing about the tilt belongs in the prefers-reduced-motion block: the
   transform is static, it does not move, and killing it there would be a
   cosmetic change dressed up as an accessibility one. */
@media (max-width:960px){
  /* The scrim is switched OFF here, not shortened. Below this breakpoint the
     hero is a single column and the picture sits after the text, about 440px
     down the page — it never goes near the bar, so there is nothing up at the
     top of the section for a scrim to fade. All the 120px version did was lie
     over the eyebrow at 83px and the top of the headline at 113px and dim them,
     for the reason set out on the .scrim rule above: full width, z-index:2,
     text underneath. Measured on the built page at 375px, the eyebrow was
     coming out between 2.63:1 and 3.89:1 across its own cap height instead of a
     flat 5.71:1. A decoration with no job that costs an AA pass is not worth
     keeping at any height. */
  .surface-tile.hero .scrim{display:none}
  /* DELETED HERE 2026-09-09: `.surface-tile.hero{--hero-fade:130px}`, and the
     phone now takes the desktop 80px like everything else.
     It existed to shorten a fade that was far too long for a phone — the note
     that came with it said "230px of empty gradient is over a quarter of a
     phone screen; 130px is a fade rather than a gap", and that was true of
     230px. The desktop number is 80px now, so this override had stopped
     shortening anything and started LENGTHENING it: a media query that makes a
     small screen carry MORE empty gradient than a large one is backwards
     whatever the numbers are. One number is also one fewer thing to keep in
     step, which is the same argument the fade/padding calc is built on.
     Measured on the built page at 375px after the deletion: the hero is 929.8px
     tall rather than 979.8px, the facts strip ends at 825.8px, the flat navy
     runs to 849.8px and the empty band below the last line of content is 104px
     instead of 154px — 11.2% of the hero rather than 15.7%.
     The eyebrow measures 5.71:1 and the facts strip 16.21:1 white / 5.71:1 pink
     — all on flat navy, none of it on a faded stop. */
}
.hero h2.sub-head{font-family:var(--font-display);font-size:clamp(21px,2.4vw,29px);
  font-weight:300;color:var(--text-gray);line-height:1.2;margin-top:8px;letter-spacing:-.01em}
/* The quiet second line of the headline, on the navy.
   THIS OVERRIDE IS NOT OPTIONAL AND IT IS NOT A TASTE CALL. The rule above
   paints it --text-gray (#5b6070), which is right on white and measures
   2.59:1 on #091464 — a headline that is still in the page and cannot be read.
   It out-specifies the surface's own .surface-tile h2 reversal at (0,2,1)
   against (0,1,1), so moving the hero onto navy did NOT fix it by itself;
   this rule, at (0,3,1), is what fixes it.
   --on-dark-muted is the same design intent translated, not a new decision: it
   is the muted tier this surface already uses everywhere else, and it measures
   8.80:1 here. Found by resolving the cascade rather than by looking at it —
   dark-on-dark is invisible in a screenshot too. */
.surface-tile.hero h2.sub-head{color:var(--on-dark-muted)}
.hero h1{max-width:none;margin:12px 0 0;font-size:clamp(32px,3.9vw,46px)}
.hero .sub{font-size:clamp(16.5px,1.8vw,19px);color:var(--text-gray);margin-top:14px;max-width:none}
/* THIS SHORTHAND IS THE BASE AND, ON EVERY PAGE AS BUILT, BOTH ENDS OF IT ARE
   OVERRIDDEN. Since page heroes went onto the navy surface the top comes from
   `main>section:first-child.surface-tile.page-hero` at (0,3,2) — 58px of bar
   plus .875 of --section — and the bottom from `.surface-tile.page-hero` at
   (0,2,0), calc(var(--hero-fade) * .9), which is 180px at the 200px desktop
   fade. Neither --section-top nor the 0 below reaches a visitor today.

   THE 0 IS STILL NOT A LEFTOVER, because of what it says: a page hero owns no
   gap of its own, and the space under a page title belongs to the next section.
   Give the page hero the full rhythm "for consistency" and a page hero above a
   section on its own surface gets 80px under the title and another 80px above
   the first paragraph — 160px between a heading and the words it introduces.
   That is what a page hero falls back to the moment it is not on navy, and it
   is the reason to leave this rule alone rather than fold it into the navy one.
   The 180px above is not the same thing wearing a different number: it is the
   tail of the gradient (see --hero-fade), empty because no text may sit on the
   fade, not empty for rhythm. */
.page-hero{padding:var(--section-top) 0 0}
/* Title and standfirst stack, full width. Nothing sits beside a heading. */
.page-hero h1{max-width:none;margin:8px 0 0;font-size:clamp(27px,3vw,36px)}
.page-hero .sub{font-size:clamp(15.5px,1.6vw,17.5px);color:var(--text-gray);
  margin:10px 0 0;max-width:none}
.sec-head{max-width:none;margin-bottom:18px}
.sec-head h2{margin-top:10px}
.sec-head .intro{margin-top:14px;color:var(--text-gray)}

/* ── strength cards ────────────────────────────────────────────────── */
.cards{display:grid;grid-template-columns:repeat(3,1fr);gap:20px}
/* position:relative + overflow:hidden are load-bearing, not decoration: the
   colour band is pinned to the card's top edge and clipped by it. Without them
   an untinted card carrying a .cardtop would let the band escape the box. */
/* --pad-top and --pad-x are the card's own padding, named rather than typed
   into the shorthand, for one reason: .cardimg below has to run full-bleed to
   the card's edges, which it can only do by cancelling that padding with an
   equal negative margin. Two places holding the same number is how a picture
   ends up two pixels off its own card. Named, there is only one number.
   --pad-top is separate from the horizontal padding because a tinted card
   already overrides it (see the .t-pink / .t-navy rule further down, which
   sets --pad-top:24px to leave room for the colour band). */
/* VERTICAL-ONLY TIGHTENING — 2026-09-09, second attempt.
   The three-card row measured 845px on the deployed page, over a MacBook
   Air's ~790px of usable height. The first attempt also shrank the hero's
   type, which changed how wide the headline runs and therefore the balance
   of the whole hero — the operator saw the composition shift and was right
   to reject it. Type size is NOT a vertical-only lever, and this pass
   deliberately leaves every font-size alone.
   What is left is genuinely one-dimensional: top and bottom padding, top and
   bottom margins, and line-height. --pad-x is untouched, so the card's
   horizontal geometry and every line break in it are identical. */
.card{--pad-top:24px;--pad-x:26px;
  background:var(--canvas);border:1px solid var(--border-gray);border-radius:var(--radius);
  padding:var(--pad-top) var(--pad-x) 22px;position:relative;overflow:hidden;
  transition:transform .18s ease,border-color .18s ease}
/* The hover drop shadow was culled on 2026-09-08 (Apple's method keeps one
   decorative shadow tier and reserves it for product imagery). The hover still
   reads: the card growing, the border darkening, and on desktop the colour
   band doubling from 6px to 12px. box-shadow came out of the transition list
   with it - and out of the DUPLICATE list on .card.reveal near the end of this
   file, which must move in step.

   CHANGED 2026-09-09 from translateY(-4px) to a scale. The operator watched the
   old hover and said the card should "actually increase in size a little bit"
   rather than slide. The amount lives once, as --card-hover-grow at the top of
   this file, because .card, a.tile and .postcard are one hover language.

   WHAT THE SCALE DOES TO THE 1px BORDER — measured, not assumed, because
   scaling a hairline-edged box is the classic way to get a soft edge and mushy
   text on a non-retina screen. Read off real pixels on 2026-09-09 at
   deviceScaleFactor 1 (i.e. the 1x case, the bad one), scanning across the left
   border of the first strength card on the home page:

     at rest   the card's left edge sits on a whole pixel (x=194.00) and the
               border is ONE column at exactly #e3e5ee — a clean hairline.
     at hover  the edge lands mid-pixel (x=190.63) and the same 1px border is
               spread over TWO columns, #c9cbd4 and #aaadb9. The total ink is
               unchanged (0.35 + 0.64 = 0.99 of a pixel's worth) but the darkest
               column reaches only about 62% of the intended #7b8093, so the
               edge reads very slightly softer while the pointer is on it.

   That is inherent to growing a box: a fractional scale lands the edges on
   fractional device pixels and there is no CSS that rounds them back. It is
   accepted rather than fixed. It is also the real reason 1.02 is a ceiling —
   the softening is a function of where the edges land, and a bigger card puts
   more of its edge off-grid.

   THE TEXT DOES NOT BLUR, which was the bigger worry and is the good news:
   Chrome re-rasterises the layer at the settled scale rather than stretching
   the pixels it already had. Over the card's heading, the share of pixels that
   are neither ink nor paper — the antialiasing, which is what "blurry" actually
   means — went from 6.15% at rest to 6.89% at hover, and the contrast (stdev of
   luminance) from 76.9 to 76.6. That difference is the glyphs landing on a
   different subpixel offset, not the type being scaled up as a bitmap. */
.card:hover{transform:scale(var(--card-hover-grow));border-color:var(--hairline-hover)}
.card h3{margin:8px 0 8px}
/* The three contact cards carry an <h2> where every other card on the site
   carries an <h3>, and that is correct rather than sloppy: those cards sit
   underneath a section <h2>, whereas the three contact cards ARE the section
   and have no heading above them. Demoting them would leave that page going
   h1 -> h3. Same size and the same rhythm either way, so nothing about how a
   card looks depends on which level it uses. */
.card h2{font-size:clamp(19px,2vw,22px);line-height:1.3;letter-spacing:-.01em;
  margin:12px 0 10px}
.card p{font-size:15.5px;line-height:1.42;color:var(--text-gray);max-width:none}
/* Nothing had ever set the gap between two paragraphs in a card, because no
   card had ever had two as SIBLINGS. The contact page's three cards do — the
   description, then the line carrying the email address — and without this
   they run together as one unbroken block of text.

   Measured before it was added, on 2026-09-09, rather than assumed: across all
   fifteen pages the generator already produced, `.card p + p` matches nothing,
   so this rule changes none of them. Three home-page cards DO contain a second
   <p>, which is why this was checked and not eyeballed — but that one is the
   caption inside the image placeholder, nested a level down, so it is never
   adjacent to the card's own paragraph. */
.card p+p{margin-top:.9em}
/* ── the picture at the top of a card ──────────────────────────────────
   Used by the three strength cards and the three sector cards on the home
   page. The picture runs full-bleed to the card's left, right and top edges:
   the negative margin cancels exactly the padding the card declared in
   --pad-top / --pad-x, which is why those two are named there rather than
   typed into the shorthand.

   The colour band (.cardtop) is position:absolute at top:0 and so draws ON TOP
   of the picture's first 6px. That is deliberate and it is why the band is a
   solid colour rather than a tint: laid over a photograph it still reads.

   flex:none because .card and .tile are flex columns (see the UI-STANDARDS
   block near the end of this file). Without it a flex item with an
   aspect-ratio is free to be squashed by a taller sibling, and the picture
   would silently stop being the shape it was cut to be. */
.cardimg{flex:none;margin:calc(var(--pad-top) * -1) calc(var(--pad-x) * -1) 12px;
  width:auto;overflow:hidden}
.tile .cardimg{margin-bottom:12px}

/* THE SITE HAS EXACTLY THREE PICTURE SHAPES. Three, and adding a fourth is a
   decision about what has to be photographed, not a styling tweak — every extra
   shape is another crop somebody has to remember to cut from the same shoot.

     16:10  the thumbnail. ONE master serves the Insights card (.postthumb) and
            the home page's sector cards — which are also the hero picture on
            each /who-we-help/ page, so one crop covers three places.
     4:3    the article lede (.artlede-media) — a taller picture standing beside
            the opening paragraph, which a 16:10 strip cannot do.
     4:5    the tall column beside the home page's six-item list.

   UPDATED 2026-09-09. This list used to include "the home page's strength
   cards" under 16:10. Those cards lost their picture entirely that day, so the
   16:10 shape now has one fewer place to be, not one more.

   Two of the three are declared as CLASSES here, because their slots have no
   shape of their own and are told one by build.py. The other, 4:3, is declared
   on .artlede-media itself, as 16:10 also is on .postthumb: where a slot IS a
   named thing in this stylesheet, the slot owns its shape and the placeholder
   inside it is stretched to fill. Both arrangements are fine; what is not fine
   is a slot and its placeholder each believing a different number, which is
   exactly what happened while .postthumb was 16:10 and the .ph-video inside it
   was still 16:9.

   Classes rather than an inline style so a real <picture> CAN BE GIVEN the same
   class and land at exactly the size its placeholder held. That promise was
   cashed on 2026-09-09 and it very nearly was not: the first version of the
   migration passed only "cardimg" to picture() and dropped the ratio class at
   two of the three call sites. It rendered identically, because the cut files
   happen to be exactly 16:10 — and the crop box that decides that is four
   numbers in build.py that its own comment invites editing. The class is what
   makes the slot hold its shape independently of the file in it, so all three
   call sites now pass it.

   4:5 is a NEW shape, not one of the two the operator originally signed off.
   It carried a placeholder only until 2026-09-09; there is a real photograph
   in it now (IMAGE_SLOTS "payroll-room"), and it is the one slot on the site
   whose box changes shape with the window — see the block at the end of this
   file. */
/* Each shape is declared twice on purpose, and the second half is not
   redundant. .ph-video sets its own aspect-ratio:16/9 further down this file,
   at the same specificity as a bare .ar-* class — so on a waiting slot the
   later rule would win and every card picture would come out 16:9 regardless
   of what it was asked for. Qualifying by .ph-video settles it on weight
   rather than on which rule happens to sit lower in the file, which is the
   kind of thing that silently comes undone when a block is moved.

   AS OF 2026-09-09 THE .ph-video HALVES MATCH NOTHING, and they are kept
   anyway. That needs saying, because a rule matching nothing was deleted a few
   hundred lines below this one — `.cards .card .cardimg{display:none}` — with
   the note that it "reads, to the next person, as evidence that the element
   exists". The two cases are genuinely different and the difference is whether
   the element can come back:

     · the strength cards' picture CANNOT. build.py no longer emits it and the
       decision was that it does not return, so the rule could only ever mislead.
     · a waiting picture slot CAN, and will. sec_page_hero and sec_sectors both
       still take a placeholder — that is how all four of today's photographs
       started, held at their final shape before anyone had taken them. Deleting
       these two halves would re-arm the exact 16:9 trap described above, and it
       would fire on the next slot somebody holds open, not today.

   So: kept, unmatched today, and load-bearing the moment a new slot is held. */
.ar-16x10,.ph-video.ar-16x10{aspect-ratio:16/10}
.ar-4x5,.ph-video.ar-4x5{aspect-ratio:4/5}

.cards-cta{margin-top:28px;text-align:center}
.cards-cta .note{margin:14px 0 0;color:var(--text-gray);font-size:15px;max-width:none}

/* ── video ─────────────────────────────────────────────────────────── */
.videowrap{margin-top:26px;border-radius:var(--radius);overflow:hidden;box-shadow:var(--shadow);
  background:#0b0f1e}
.videowrap video{width:100%;height:auto;display:block}
/* ── the client quotes under a video block ─────────────────────────────
   Added 2026-09-09, from the review: "we could potentially do a box, Steve
   Kinning's quote, Amie Kavanaugh quote, so that the quote's text is
   searchable."

   THERE IS ALMOST NO NEW DESIGN HERE ON PURPOSE. The box itself is .quote —
   the client-quote card this file already draws for the written testimonials
   on the trades page — and build.py's speaker_quote() puts both classes on the
   element so it arrives fully styled: border, radius, padding, the teal
   opening mark, the .who name-and-role pair. Two ways to draw a client quote
   on one site is the drift this avoids. What is left below is only the four
   things that differ because of WHERE this one sits.

   1. THEY STACK, THEY DO NOT SIT SIDE BY SIDE. This is the narrow half of a
      two-column block: 437px at 1440px, and less below that. .quotes puts its
      cards in two columns because it owns the full width; two 200px columns of
      quotation here would be a ribbon. One column, and rule 4.1 (siblings in a
      row are equal height) does not arise, because there is no row.
   2. flex-wrap is gone with the flex. It was there to stop two names
      colliding; a grid column cannot collide with itself.
   3. <cite> is italic by default in every browser. The site sets no italic on
      any other attribution, so it is turned off rather than inherited by
      accident.
   4. The gap is 12px, not .quotes' 18px, because these two cards are stacked
      rather than beside each other, and the CTA follows immediately below.

   THE NAME AND ROLE RULES STAY AS THEY WERE for the speaker who has no quote.
   speaker_quote() still emits the plain <div class="speaker"><b>/<span> for
   that case, byte-for-byte as before this change, and these two rules are what
   paint it. They are not dead code waiting to be tidied; deleting them makes a
   quoteless speaker render as unstyled text. */
/* THE QUOTE ROW. Two client quotes are a pair of equal weight, so they sit side
   by side. They were previously stacked inside the video block's text column,
   which made that column run far past the video beside it — the operator called
   the result "entirely disjointed" and they were right: a stack of two cards in
   one half of a two-column row is not a layout, it is an overflow.
   auto-fit with a floor means one quote fills the row rather than sitting as a
   lonely half, and three would wrap to a second row without a new rule. */
.vquotes{margin-top:34px}
.speakers{display:grid;gap:16px;margin-top:0;
  grid-template-columns:repeat(auto-fit,minmax(340px,1fr));align-items:stretch}
.vquotes .btn-row{margin-top:22px}
@media (max-width:640px){
  .vquotes{margin-top:26px}
  .speakers{grid-template-columns:1fr;gap:12px}
}
.speaker{font-size:14.5px}
.speaker b{display:block;color:var(--ink-h);font-weight:600}
.speaker span{color:var(--text-gray)}
.speaker cite{font-style:normal}
/* ── the one waiting-media placeholder ─────────────────────────────────
   ONE component, every empty slot on the site: the pending testimonial videos,
   the Insights card thumbnails, the article ledes, and as of 2026-09-08 the
   home page's picture slots. build.py has a single ph_media() that emits it,
   and there is deliberately no second placeholder style anywhere.

   IT HAS TO BE OBVIOUSLY, DELIBERATELY EMPTY. The failure to design against is
   not ugliness — it is a plain grey rectangle that a visitor reads as a
   photograph which failed to load, or worse, that a reviewer reads as finished
   artwork. So: a hatch, a dashed edge and a worded mark, all three. Do not
   "clean it up" into a flat panel; the hatch is the part that says "nothing is
   meant to be here yet".

   RESTYLED 2026-09-08 off its old yellow warning tag and #c9ced8 dashed box.
   The yellow belonged to no palette on this site and read as an error state;
   the mark is now the same navy/parchment vocabulary as everything around it,
   which says "not yet" rather than "something went wrong". */
.ph-video{aspect-ratio:16/9;border-radius:var(--radius);
  border:1px dashed var(--hairline-hover);
  background:repeating-linear-gradient(135deg,
    var(--parchment) 0 10px, var(--canvas) 10px 20px);
  display:flex;align-items:center;justify-content:center;text-align:center;
  padding:clamp(14px,3vw,30px);min-width:0}
.ph-video .inner{max-width:none}
.ph-video .tag{display:inline-block;background:var(--tile);border:1px solid var(--tile);
  color:var(--on-dark);
  font:700 10.5px var(--font-body);letter-spacing:1.2px;text-transform:uppercase;
  padding:5px 11px;border-radius:var(--pill);margin-bottom:10px}
.ph-video p{color:var(--text-gray);font-size:14.5px;margin:0 auto;max-width:none}
/* A picture slot is a fraction of the size a video slot is, so the same mark
   at the same size would fill it edge to edge and stop reading as a mark. */
/* Same standing as the two pairings above: unmatched since 2026-09-09, when
   every card picture on the site became a real <picture>, and kept because the
   placeholder branch in sec_sectors is still reachable — ph_image("ar-16x10
   cardimg") is what a sector tile emits before its photograph is taken. These
   two lines are what stop that placeholder drawing its own side borders inside
   a card that already has them, and shrinking its caption to fit. */
.cardimg.ph-video p,.ar-4x5.ph-video p{font-size:13px;line-height:1.4}
.cardimg.ph-video{border-left:0;border-right:0;border-top:0}

/* ── steps ─────────────────────────────────────────────────────────── */
.steps{counter-reset:s;display:grid;grid-template-columns:1fr 1fr;gap:10px}
.step{counter-increment:s;display:grid;grid-template-columns:22px 1fr;gap:13px;
  background:var(--canvas);border:1px solid var(--border-gray);border-radius:var(--radius);padding:14px 16px}
.step::before{content:counter(s);font-family:var(--font-display);font-weight:700;
  color:var(--on-dark);background:var(--dark-blue);width:22px;height:22px;border-radius:6px;font-size:14px;
  display:flex;align-items:center;justify-content:center;line-height:1;transform:translateY(1px)}
.step h3{margin-bottom:4px;font-size:16.5px}
.step p{color:var(--text-gray);max-width:none;font-size:14.5px;line-height:1.45}

/* ── feature grid / sectors ────────────────────────────────────────── */
.grid3{display:grid;grid-template-columns:repeat(3,1fr);gap:18px}
.tile{--pad-top:14px;--pad-x:16px;
  background:var(--canvas);border:1px solid var(--border-gray);border-radius:var(--radius);
  padding:var(--pad-top) var(--pad-x) 14px}
.tile h3{font-size:18px;margin-bottom:8px}
.tile p{font-size:15px;color:var(--text-gray);max-width:none}
/* Shadow culled 2026-09-08, same pass as .card and .postcard. That left the 3px
   lift as the only hover feedback here — and the prefers-reduced-motion block
   near the end of this file sets a.tile:hover{transform:none}, which takes the
   lift away again. So a visitor with Reduce Motion switched on had NO hover cue
   at all on these linked tiles: no lift, no shadow, no border change, and .tile
   sets text-decoration:none so there is no underline either. Before the cull the
   shadow survived reduced motion and those users did get a cue.
   The border-color change below is therefore NOT decoration and NOT optional —
   for a reduced-motion visitor it is the only cue left. It is the same
   --hairline-hover that .card:hover and .postcard:hover use, so this is parity
   with the siblings, not a new design decision. border-color is in the
   transition list for the same reason it is on those two: so it eases rather
   than snaps.
   Remove either half and reduced motion silently loses the state change again.
   UPDATED 2026-09-08: all three of these hovers used to name #d8dbe2 directly.
   That colour was a step of only 1.10:1 from the resting edge — about a tenth
   of what the guideline asks — so the one cue a reduced-motion visitor had was
   very nearly invisible. The operator approved clearing 3:1; the value now
   lives once, as --hairline-hover at the top of this file, with the
   measurements. */
/* UPDATED 2026-09-09: the 3px lift became the shared grow. Same change, same
   day, same reason as .card:hover above — a linked sector tile and a strength
   card sit two sections apart on the home page, and a visitor who sees one
   slide and the other grow reads it as two different kinds of thing. */
a.tile{text-decoration:none;display:block;transition:transform .18s ease,border-color .18s ease}
a.tile:hover{transform:scale(var(--card-hover-grow));border-color:var(--hairline-hover)}
a.tile .more{margin-top:14px;font-weight:600;font-size:14.5px;color:var(--action-press)}

/* ── prose / callout / faq ─────────────────────────────────────────── */
.prose{max-width:none}
.prose p+p{margin-top:1.05em}
.prose h2{margin-bottom:18px}
.callout{border-left:3px solid var(--quote-teal);background:var(--light-gray);
  border-radius:var(--radius);padding:20px 24px;margin-top:22px;max-width:none}
.callout p{max-width:none}
.faq{display:grid;gap:14px;max-width:none}
/* THE PADDING IS ON THE SUMMARY, NOT ON THE DETAILS, and that is a touch-target
   decision rather than a layout preference. Corrected 2026-09-09 during the
   full-site parity sweep.
   <summary> is the whole clickable control; the padding its parent <details>
   carries sits OUTSIDE it and does nothing to enlarge it. With the 16px 20px
   that used to live on the parent, the two FAQ questions on /how-it-works
   measured 21.59px tall — the height of one line of 16px text at 1.35. Moving
   the same padding onto the summary and adding the floor makes the control
   49.59px.
   SAID EXACTLY: across all eighteen pages measured at 1440px and at 500px, this
   was the only STANDALONE control in the page body under 44px other than the
   Ask-AI buttons, whose 32px is a decision already recorded further down this
   file. Two other things are under 44px and are deliberately not touched here,
   because they are identical on the approved home page and changing them is a
   site-wide question rather than a parity gap: the header and footer navigation
   links (27.92px to 32.58px), and links sitting inline inside a sentence on the
   contact and privacy pages (20px), which are exempt from target sizing
   precisely because they are part of running text.
   THE PRIVACY POLICY'S ACCORDION ALREADY DID IT THIS WAY: .accordion-header
   below carries `min-height:44px;padding:14px 20px` on the button itself. These
   two components are meant to be the same thing in two markup shapes (see the
   note under this one), so they now take the same numbers and close to the same
   height — 51.59px including the border, on both.
   The gap between the question and its answer is now the summary's own 14px of
   bottom padding rather than a 12px margin on the paragraph, which is why that
   margin is gone; the answer keeps the horizontal inset by carrying it itself. */
.faq details{background:var(--canvas);border:1px solid var(--border-gray);border-radius:var(--radius);
  padding:0;height:100%;transition:border-color .18s ease}
/* The hover lift the rest of the site's cards get. Added 2026-09-09 in the same
   parity sweep as the padding above, and for the same reason: .privacy-card
   below already carried exactly this line, so the two accordions responded
   differently to the pointer even though they are meant to be one component in
   two markup shapes. Border colour only, no scale — .card and a.tile grow
   because they are whole clickable objects, and a <details> is not one; the
   thing being pressed is the summary inside it. --hairline-hover is the same
   token every other hover border on the site steps to, so no new value. */
.faq details:hover{border-color:var(--hairline-hover)}
.faq summary{cursor:pointer;font-weight:600;color:var(--ink-h);font-size:16px;list-style:none;
  display:flex;align-items:center;justify-content:space-between;gap:16px;line-height:1.35;
  flex-wrap:nowrap;min-height:44px;padding:14px 20px}
.faq summary::-webkit-details-marker{display:none}
.faq summary::after{content:"+";color:var(--tempus-pink);font-weight:700;font-size:19px;
  line-height:1;flex:none}
.faq details[open] summary::after{content:"–"}
.faq details p{margin:0;padding:0 20px 16px;color:var(--text-gray)}

/* ── the privacy policy accordion ───────────────────────────────────────
   Deliberately the .faq look — same card, same hairline, same pink +/- glyph —
   because it IS the same idea, but it CANNOT be the same markup. The FAQ is
   <details>/<summary>; this has to be a <div> wrapping a <button> containing an
   <h2>, plus a <div> for the panel, because server.py builds the downloadable
   PDF by reading the finished privacypolicy.html back off disk and selecting
   .privacy-card / .accordion-header h2 / .accordion-content out of it by name.
   The full contract, and what breaks if it is not met, is written out above
   sec_privacy_accordion in _source/build.py. Do not rename these three class
   names to tidy them up.

   THE PANEL IS COLLAPSED HERE AND OPENED BY SCRIPT, so the page also carries a
   <noscript> block that sets max-height:none — one line, only ever applied when
   scripting is off, and it is why a visitor without JavaScript reads the whole
   policy instead of nineteen headings. That block is (0,2,0) and this rule is
   (0,1,0), so it wins on specificity and not on being further down the page. */
.privacy-list{display:grid;gap:12px}
.privacy-card{background:var(--canvas);border:1px solid var(--border-gray);
  border-radius:var(--radius);transition:border-color .18s ease}
.privacy-card:hover{border-color:var(--hairline-hover)}
/* font:inherit resets the four properties a <button> takes from the browser
   rather than from this stylesheet — family, size, weight and line-height. A
   19-section legal document set in the browser default 13px Arial is what
   happens without it, and it happens only in the panel headings, so it reads
   as a deliberate style rather than as a bug. */
.accordion-header{display:flex;align-items:center;justify-content:space-between;
  gap:16px;width:100%;min-height:44px;padding:14px 20px;margin:0;
  background:none;border:0;cursor:pointer;text-align:left;font:inherit;color:inherit}
.accordion-header h2{margin:0;font-family:var(--font-body);font-size:16px;
  font-weight:600;line-height:1.35;letter-spacing:.02em;color:var(--ink-h)}
.accordion-chevron{flex:none;width:19px;text-align:center;color:var(--tempus-pink);
  font-weight:700;font-size:19px;line-height:1}
.accordion-chevron::before{content:"+"}
.privacy-card.is-open .accordion-chevron::before{content:"–"}
/* Horizontal padding only. Vertical padding is not clipped by max-height, so a
   closed panel with padding-top would show as an unexplained gap under every
   heading; the space under the last block comes from its own margin instead,
   which the script's height measurement includes because overflow:hidden stops
   that margin collapsing out of the box. */
.accordion-content{max-height:0;overflow:hidden;padding:0 20px;
  transition:max-height .3s ease}
.accordion-content>*{max-width:none;color:var(--text-gray);font-size:15.5px}
.accordion-content>*+*{margin-top:.95em}
.accordion-content>*:last-child{margin-bottom:18px}
.accordion-content ul{padding-left:20px}
.accordion-content li+li{margin-top:6px}
@media (prefers-reduced-motion:reduce){.accordion-content{transition:none}}

/* ── a page title with buttons and no standfirst ────────────────────────
   The contact page's phone number and the privacy policy's PDF download. Both
   pages carry an <h1> and a button and nothing in between, so without this the
   button sits hard against the heading. */
.page-hero .btn-row{margin-top:18px}

/* ── the 404 page's illustration ────────────────────────────────────────
   .split gives its FIRST child the wider column, and the illustration is the
   first child, which would have made a decorative picture the biggest thing on
   a page whose job is to get the visitor back out of it. The weighting is
   flipped here rather than by reordering the markup, because the picture
   belongs before the text in reading order on a phone.
   Written as .split.errsplit — (0,2,0) — so it does not depend on sitting after
   the 960px rule that stacks .split; both widths are stated. */
.split.errsplit{grid-template-columns:.85fr 1.15fr}
.errorshot{display:block;width:100%;height:auto;max-width:420px}
@media (max-width:960px){.split.errsplit{grid-template-columns:1fr}}

/* ── testimonials ──────────────────────────────────────────────────── */
.quotes{display:grid;grid-template-columns:repeat(2,1fr);gap:18px}
.quote{background:var(--canvas);border:1px solid var(--border-gray);border-radius:var(--radius);padding:14px 16px}
/* THE VIDEO BLOCK'S QUOTES ARE A COMPACT VARIANT. Operator, 2026-09-09: drop the
   quotation mark, set the quote at the same size as the name and role, and take
   the height down. The mark is display:none rather than deleted because .quote
   is shared with the written-testimonials block on the trades page, where the
   mark is still wanted — this is a variant of the component, not a change to it.
   Scoped to .speakers so nothing else moves. */
.speakers .quote .mark{display:none}
.speakers .quote p{font-size:14.5px;line-height:1.55;margin:0 0 12px}
/* TYPOGRAPHIC QUOTE MARKS, not the big decorative glyph. Operator,
   2026-09-09: they want the quotation wrapped in real quote marks —
   "like this" — rather than carrying a separate ornament above it.
   They are drawn with ::before/::after rather than typed into
   _source/content.py because the copy in that file is the client's
   VERBATIM words and punctuation the site adds around them is
   presentation, not content. It also keeps the sentence clean for
   anything that reads the text rather than looks at it. */
.speakers .quote p:first-of-type::before{content:"\201C"}
.speakers .quote p:last-of-type::after{content:"\201D"}
.quotes .quote .mark{display:none}
.quotes .quote p:first-of-type::before{content:"\201C"}
.quotes .quote p:last-of-type::after{content:"\201D"}
/* font-weight:700 is PINNED here, explicitly, on 2026-09-08. It declared no
   weight at all, so it asked for 400 and inherited it - but it RENDERED at 700,
   because 700 was the only League Spartan face downloaded and the browser fell
   to the nearest available. The font request now also carries 300 and 600, so
   the moment that changed this decorative quote glyph would have silently
   dropped to 400 with nothing in the diff to explain it. */
.quote .mark{color:var(--quote-teal);font-family:var(--font-display);font-size:34px;line-height:1;
  font-weight:700}
.quote p{font-size:16px;margin:6px 0 16px;max-width:none}
.quote .who b{display:block;color:var(--ink-h);font-weight:600;font-size:14.5px}
.quote .who span{color:var(--text-gray);font-size:14px}

/* ── placeholder block ─────────────────────────────────────────────── */
.placeholder{border:2px dashed #e0b84a;background:#fffdf5;border-radius:var(--radius);
  padding:30px;max-width:none}
.placeholder .tag{display:inline-block;background:#fff3cd;border:1px solid #f3d38a;color:#7a5a00;
  font:700 11px var(--font-body);letter-spacing:1.2px;text-transform:uppercase;
  padding:6px 12px;border-radius:var(--pill)}
.placeholder h2{margin:14px 0 12px;font-size:24px}
.placeholder ul{margin:0 0 16px 20px}
.placeholder li{margin-bottom:7px;font-size:15.5px}
.placeholder .why{color:var(--text-gray);font-size:15px;max-width:none}

/* ── the navy surface, and every reversal it needs ─────────────────────
   The site's one dark surface. Everything above this line assumes dark ink on
   a light ground; this block is the complete set of reversals that makes a
   section readable on navy. It was .band until 2026-09-08 — one class that
   welded the colour to the CTA block's centred layout, so the navy could not
   be used anywhere else without dragging the centring along with it. The
   layout half is now .cta-band, further down.

   THE BACKGROUND IS NOW FLAT var(--tile), CHANGED 2026-09-08 FROM A THREE-STOP
   GRADIENT (135deg, var(--tile) -> #1a2a8a 55% -> #3a1a6a). The operator asked
   for one navy everywhere and this is it. Two things follow that are worth
   knowing before anyone reaches for a gradient again:

     • THE FLAT COLOUR MUST BE var(--tile) AND NOTHING ELSE. #091464 is the
       identical rgb(9,20,100) the fixed header bar is painted in, and the
       header's bottom hairline is MEASURED against exactly that colour (see
       the header block above). Flattening to the palette's second navy
       #0b1878 instead measures 1.09:1 against #091464 — the fixed bar would
       disappear into a navy section scrolling behind it, which is the precise
       failure that hairline exists to prevent. The approved mock-up's closing
       band uses that second navy; the mock-up is a standalone page with no
       fixed header behind it, so nothing there exposed the problem.
     • #1a2a8a and #3a1a6a left WITH the declaration rather than being kept as
       unused tokens. --tile-2 (#0b1878) is still deliberately undeclared, for
       the reason given at the top of this file: declaring a colour before
       anything uses it is how a second colour system starts.

   PINK IS TEXT HERE AGAIN, and the history is worth two minutes because it is
   the reason the answer changed rather than the reason it was wrong.

   While this surface was a three-stop gradient, --action-on-dark #ff5cae sat
   over the middle stop #1a2a8a at 4.24:1 and FAILED AA, so every pink text role
   was turned white. Flattening the surface to var(--tile) removed that middle
   stop: the same pink on the same surface now measures 5.71:1 and passes AA
   comfortably. The hold that followed the flattening was not caution about the
   number — it was that a visible colour change on eight pages is the operator's
   call. They have made it: pink on navy is approved, and it is switched on
   below (2026-09-08).

   WHICH ROLES, AND WHY NOT ALL OF THEM. A role turns pink here only if it is
   pink on a LIGHT surface — the eyebrow, the tile "more" link, the "us" column
   label in a comparison, the numbered-stack numerals, an Insights card's
   category and its Read More, and the FAQ's "+". The plain .cmp-label is
   --text-gray on light, i.e. not a pink role at all, so it stays white here;
   turning it pink would invent a distinction the light surface does not make,
   and it is the one thing distinguishing "Now" from the "us" column.

   THE OTHER TWO PINKS ARE STILL WRONG HERE and must never be reached for: the
   brand pink #e6007e is 3.60:1 on this navy and --action-press #c9006e is
   2.86:1. Only --action-on-dark is legal as text on a dark surface. Where pink
   is a MARK rather than text — a list bullet, a quote rule — 3:1 is the bar and
   it clears comfortably.

   THE RATIOS, all measured against #091464, which is now the whole surface
   rather than only its darkest corner:
     white            16.21:1  headings, body, strong
     --on-dark-muted   8.80:1  muted copy — the tier that is #5b6070 on light
     rgba(255,255,255,.36) resolves to rgb(98,105,156) = 3.10:1, clearing WCAG
                              1.4.11's 3:1 for a non-text boundary. This is the
                              SAME measured alpha the header hairline uses;
                              reuse the number rather than inventing a second.
                              .22 measures 1.92:1 and .18 measures 1.66:1 —
                              both fail, and both look "cleaner".

   WHY THE CARD RULES ARE HERE. They were written on 2026-09-08 while nothing
   but the closing CTA block sat on this surface, as the surface's CONTRACT:
   any section in _source/content.py can be moved onto navy with a one-line
   "surface": "tile" key and no code change at all, and that promise is
   worthless if using it produces white boxes floating on a navy field. The
   promise was collected the same day — the home page's hero and its six-item
   block are both on navy now, put there by exactly that one-line key. A card
   here keeps NO fill — a raised navy is invisible against navy (the palette's
   second navy #0b1878 measures 1.09:1 against the first, and a 6% white wash
   is 1.15:1) — so its hairline carries the box instead. Delete these and the
   override key becomes a trap rather than a feature.
   ──────────────────────────────────────────────────────────────────── */
.surface-tile{background:var(--tile);color:var(--on-dark)}
.surface-tile h1,.surface-tile h2,.surface-tile h3,.surface-tile h4,
.surface-tile strong,.surface-tile b{color:var(--on-dark)}
/* Muted copy. .surface-tile p catches the general case; everything after it is
   a class that sets its own colour at (0,1,1) and would otherwise win or lose
   on source order alone, which is not a thing to leave to chance. */
.surface-tile p,
.surface-tile .lede,.surface-tile .sub,.surface-tile .sec-head .intro,
.surface-tile .card p,.surface-tile .tile p,.surface-tile .step p,
.surface-tile .cards-cta .note,.surface-tile .ph-video p,
.surface-tile .faq details p,.surface-tile .quote .who span,
.surface-tile .placeholder .why,.surface-tile .blocks .blk p,
.surface-tile .ncard div,.surface-tile .acard,.surface-tile .facts li,
.surface-tile .nstack .nrow>div,.surface-tile .cmp li,.surface-tile .cmp-foot,
.surface-tile .postcard p,.surface-tile .postcard .excerpt,
.surface-tile .article .meta,
/* .askai-lead was the one text role inside a hero with no reversal here. It
   declares its own color:var(--text-gray), which BEATS the inherited
   .surface-tile{color:var(--on-dark)}, so the moment page heroes went navy
   the label above the four buttons on /insights fell to #5b6070 on #091464 —
   2.59:1, against 4.5:1 for 14.5px text. It read 6.27:1 on white the day
   before. Measured 2026-09-09. */
.surface-tile .askai-lead{color:var(--on-dark-muted)}
/* The pink TEXT roles. Every one of these is pink on a light surface, so on the
   navy it takes the navy's pink — --action-on-dark #ff5cae, 5.71:1, AA. Never
   --action or --action-press here; both fail on this ground. See the note
   above for why this was white until 2026-09-08. */
.surface-tile .eyebrow,.surface-tile a.tile .more,
.surface-tile .cmp:last-child .cmp-label,.surface-tile .nstack .nnum,
.surface-tile .postcard .cat,.surface-tile .postcard .readmore,
.surface-tile .faq summary::after{color:var(--action-on-dark)}
/* The plain comparison label is NOT a pink role — it is --text-gray on light —
   so it stays white here rather than following its pink sibling above. It is
   listed on its own so that reading the two rules together tells you which of
   the pair is the pink one, which is the whole distinction the block draws. */
.surface-tile .cmp-label{color:var(--on-dark)}
/* An inline link still has to be identifiable as a link. On light it is
   --action-press underlined; here the underline does the whole job and the
   colour steps back to plain white. */
.surface-tile .article p a,.surface-tile .article li a,
.surface-tile .prose p a,.surface-tile .prose li a,
.surface-tile .callout p a,.surface-tile .lede a{color:var(--on-dark)}
/* Card fills and every hairline. No fill: the edge carries the box. */
.surface-tile .card,.surface-tile .tile,.surface-tile .step,
.surface-tile .faq details,.surface-tile .quote,.surface-tile .cmp,
.surface-tile .blocks .blk,.surface-tile .ncard,.surface-tile .acard,
.surface-tile .nstack,.surface-tile .postcard{
  background:transparent;border-color:rgba(255,255,255,.36)}
.surface-tile .nstack .nrow,.surface-tile .facts,
.surface-tile .article p.close{border-color:rgba(255,255,255,.36)}
/* Navy chrome disappears on navy: the numeral badges, the .nstack top rule and
   every navy CARD TINT are var(--tile) on a light surface, i.e. 1:1 against
   this one. The badges invert — white pill, navy numeral — rather than being
   tinted, because a numeral is text and needs full contrast, not a boundary's
   3:1. The card tint turns white for the same reason its pink sibling does not
   have to: pink is still visible here and navy is not.
   NOTE the tint classes are .t-navy / .t-pink and they mean "this CARD has a
   coloured rule across its top". They are NOT surface names and must never be
   used as one — two of their rules elsewhere in this file are unqualified by
   element, so a <section class="t-navy"> would grow a 6px bar of its own. */
.surface-tile .step::before,.surface-tile .ncard .nnum{
  background:var(--on-dark);color:var(--tile)}
.surface-tile .nstack::before,
.surface-tile .card.t-navy>.cardtop,.surface-tile .cardtop.t-navy,
.surface-tile .cardtop.navy{background:var(--on-dark)}
/* Pink survives as a MARK, where 4.24:1 on the lightest stop is well past the
   3:1 a non-text graphic needs: the list bullets and an article's quote rule. */
.surface-tile .cmp li::before,.surface-tile .facts li::before{
  background:var(--action-on-dark)}
.surface-tile .article blockquote{border-left-color:var(--action-on-dark)}
/* The outline buttons. Their black 1px drop shadow is invisible on navy and is
   dead weight, so it is removed rather than restated. */
.surface-tile .btn-ghost,.surface-tile .btn-outline{
  color:var(--on-dark);border-color:rgba(255,255,255,.4);box-shadow:none}
.surface-tile .btn-ghost:hover,.surface-tile .btn-outline:hover{
  border-color:var(--on-dark)}
/* The waiting-media placeholder, inverted. Its hatch is two near-whites, which
   on navy is a bright rectangle — the loudest thing on the page, for the one
   element that is meant to be quiet. Here the hatch becomes two barely-apart
   whites at very low alpha over the navy, and the mark inverts to a white pill
   with navy lettering so it stays legible.
   The 1px dashed edge reads --hairline-hover on light; on navy that is a
   mid-grey against a dark ground, so it takes the same rgba(255,255,255,.36)
   every other hairline on this surface uses — measured at 3.10:1, see above. */
.surface-tile .ph-video{border-color:rgba(255,255,255,.36);
  background:repeating-linear-gradient(135deg,
    rgba(255,255,255,.07) 0 10px, rgba(255,255,255,.02) 10px 20px)}
.surface-tile .ph-video .tag{background:var(--on-dark);border-color:var(--on-dark);
  color:var(--tile)}

/* A callout's pale-grey fill is invisible on parchment and a bright block on
   navy, so each non-white surface gives it one that works. The teal left rule
   is unchanged: it measures 6.24:1 on the navy and needs nothing. */
.surface-parchment .callout{background:var(--canvas)}
.surface-tile .callout{background:rgba(255,255,255,.07)}

/* ── Insights index ────────────────────────────────────────────────────
   Three across, two on a tablet, one on a phone. The tracks are
   minmax(0,1fr) rather than a bare 1fr on purpose: a grid track's default
   minimum is min-content, so one unbroken word — a long URL, "PricewaterhouseCoopers"
   — silently widens its column and pushes the last card off the screen.
   minmax(0,…) lets the track shrink below its content and the word wraps
   instead. Cards are square-cornered and stretch to a common height, with the
   Read More affordance pinned to the bottom edge so a short excerpt does not
   leave a ragged foot.
   ────────────────────────────────────────────────────────────────────── */
.postlist{display:grid;gap:16px;max-width:none}
.postgrid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:22px;
  align-items:stretch;max-width:none}
@media (max-width:960px){.postgrid{grid-template-columns:repeat(2,minmax(0,1fr));gap:18px}}
@media (max-width:640px){.postgrid{grid-template-columns:repeat(1,minmax(0,1fr));gap:16px}}

.postcard{display:flex;flex-direction:column;text-decoration:none;background:var(--canvas);
  border:1px solid var(--border-gray);border-radius:var(--radius);overflow:hidden;
  height:100%;transition:transform .18s ease,border-color .18s ease}
/* The card grows and the border darkens. (This used to read "a lift and a
   shadow" - the hover shadow was culled on 2026-09-08 because Apple's method
   allows one decorative shadow tier and it is reserved for product imagery,
   and on 2026-09-09 the lift became the shared grow at the operator's request.)

   THE ZOOM-ON-HOVER RULING IS NOT AFFECTED BY THIS, and the difference is worth
   being precise about because the two look like the same word. What the
   operator ruled out was zooming the IMAGE INSIDE its fixed slot: the slot
   stays put, the picture grows behind it, and the crop changes on hover — which
   reads as the photograph being swapped rather than as a hover state. That is
   still forbidden, and .postcard:hover .postthumb img{transform:none} below is
   what forbids it. Growing the whole card scales the slot and the picture by
   the same factor at the same instant, so the crop is identical throughout.
   Nothing is revealed and nothing is cut off. */
.postcard:hover{transform:scale(var(--card-hover-grow));border-color:var(--hairline-hover)}

/* THE 16:10 THUMBNAIL SLOT. aspect-ratio reserves the footprint whether or not
   the image has loaded, so a row of cards never reflows as art arrives.

   CHANGED 2026-09-08 from 16:9. 16:10 is the settled thumbnail crop, and the
   point of settling it is that ONE master then serves this Insights card and
   the home page's three sector cards — the .ar-16x10 class those use is the
   same shape as this slot. At 16:9 the Insights card was a crop of its own,
   which means a photographer shooting one picture for both would have had to be
   told about the difference, and would not have been.

   AMENDED 2026-09-09: this used to say "the home page's three strength cards
   and its three sector cards". The strength cards lost their picture that day,
   so the sentence named a slot that no longer exists. */
.postthumb{aspect-ratio:16/10;width:100%;overflow:hidden;background:var(--light-gray);
  background-image:linear-gradient(135deg,rgba(230,0,126,.06),rgba(9,20,100,.06)),
    repeating-linear-gradient(135deg,#fafbfd 0 14px,#f2f4f8 14px 28px);
  display:flex;align-items:stretch;justify-content:center;flex:none}
/* align-items WAS center, until 2026-09-14 — operator reported a thin diagonal-
   striped sliver along the top and bottom of every card photo. That stripe is
   THIS BOX'S OWN waiting-media background (the repeating-linear-gradient two
   lines up), showing through a real gap: .postthumb img below sets
   height:100% intending to fill this box, but the <img>'s actual parent is
   the <picture> the generator wraps it in, and align-items:center left
   <picture> unstretched — sized to its own content, i.e. to the image's
   native ratio (768x432 from its width/height attributes, a 16:9 crop) rather
   than this slot's 16:10. height:100% on the img then has nothing definite to
   be 100% OF, falls back to auto, and the image renders 9px short top and
   bottom of a 16:10 box — centred by this same align-items, which is exactly
   why the gap was even and on both edges, not one. Confirmed live before
   changing it: forcing height:100% on the <img> directly did nothing (same
   9px short); forcing align-items:stretch on this box closed it to the pixel.
   stretch fixes it at the source — <picture> now takes this box's real
   height, and the img's own height:100% has something to resolve against.
   justify-content:center is untouched: width was never short (100% resolves
   against <picture>'s width directly, no equivalent chain), so there was
   nothing for it to be centring in that direction and nothing changes there. */
.postthumb img{width:100%;height:100%;object-fit:cover;display:block;transform:none}
.postcard:hover .postthumb img{transform:none}   /* explicit: no zoom, ever */
/* THE SLOT OWNS THE SHAPE, the waiting-media placeholder just occupies it —
   the same arrangement .artlede-media uses further down, and for the same
   reason. .ph-video carries its own aspect-ratio:16/9, so without this the
   placeholder sat at 16:9 inside a 16:10 box and left a pale strip along the
   bottom of every Insights card. It was invisible while both were 16:9. */
.postthumb .ph-video{aspect-ratio:auto;width:100%;height:100%;border-radius:0}

/* the body fills whatever height the tallest card in the row sets */
.postbody{flex:1 1 auto;display:flex;flex-direction:column;padding:22px 24px 24px}
/* The padding moved off the card and onto .postbody so the thumbnail can run
   edge to edge. The older .postlist markup has no .postbody, so it keeps the
   padding on the card itself. Written as two plain rules rather than one
   :has() test, so a browser without :has() still renders the legacy list
   correctly instead of dropping the declaration and losing all its padding. */
.postcard{padding:0}
.postlist .postcard{padding:26px}
.postlist .postcard:has(.postbody){padding:0}

.postcard .cat{font-size:11.5px;letter-spacing:1.6px;text-transform:uppercase;font-weight:700;
  color:var(--action-press)}
.postcard h3{margin:10px 0 8px}
.postcard p,.postcard .excerpt{color:var(--text-gray);font-size:15.5px;max-width:none;margin:0}
.postcard .date{margin-top:12px;font-size:13.5px;color:var(--text-gray)}
/* margin-top:auto is what makes the row even — it pushes Read More to the
   bottom of whichever card is tallest and every other card matches it. */
.postcard .readmore{margin-top:auto;padding-top:16px;font-weight:600;font-size:14.5px;
  color:var(--action-press);display:inline-flex;align-items:center;gap:7px;align-self:flex-start}
.postcard .readmore::after{content:"→";font-size:15px;line-height:1;
  transition:transform .18s ease}
.postcard:hover .readmore{background-image:linear-gradient(currentColor,currentColor);
  background-size:100% 1px;background-repeat:no-repeat;background-position:0 calc(100% - 2px)}
.postcard:hover .readmore::after{transform:translateX(3px)}
.article{max-width:none}   /* rule 2.4: no measure cap anywhere */
.article .stand{font-size:19px;font-weight:600;color:var(--ink-h);margin:16px 0 26px;max-width:none}
.article h2{margin:34px 0 12px}
.article p{margin-bottom:1em;max-width:none}
.article ul,.article ol{margin:0 0 1.1em 1.2em}
.article li{margin-bottom:.5em}
.article blockquote{border-left:3px solid var(--tempus-pink);padding-left:20px;margin:26px 0}
/* 17px, not 18.5px — operator, 2026-09-11: the pull-quote text read oddly
   larger than the surrounding body copy (17px, --body-size). The pink
   left border already carries the emphasis; the type doesn't need to as
   well. Same on mobile and desktop — one rule, no breakpoint. */
.article blockquote p{font-size:17px;color:var(--ink-h);margin:0}
.article .meta{color:var(--text-gray);font-size:14px;margin-top:8px}

/* ── footer ────────────────────────────────────────────────────────── */
.site-footer{background:#0a0e1c;color:var(--on-dark-muted);padding:56px 0 34px;margin-top:0}
/* Three EVEN columns. The old 1.4fr lead track existed for the .fbrand block
   — logo, blurb and phone — that sat to the left of the link columns. That
   block is gone (its blurb and contact details moved to the centred
   .fbottom-row below), so a wide first track would just be a fat empty
   gutter with the first link column stranded beside it. */
/* CENTRED, and that is the whole point of this rule.
   The footer used to run three different alignment systems stacked on top of
   one another, which is what the operator was seeing when they said it looked
   "over to the left" and "disjointed": the link columns were left-aligned
   inside their tracks, the phone/email/blurb below them were centred, and the
   legal line under that was pushed edge to edge. Three systems in one
   component, and the eye reads the disagreement long before it can name it.

   Left-aligning short lists inside wide even tracks is also what created the
   dead space. Measured on a 1470px viewport before this change: the columns
   started hard against the left edge of the row and the last one ran out of
   words 239.5px short of the right edge, so the whole block sat off to one
   side of a footer whose other two rows were centred on the middle.

   Centring the text inside the tracks fixes both at once, without touching the
   track widths: the tracks stay three even 1fr columns spanning the full row,
   so the columns are still evenly distributed, but each column's content now
   centres on its own track. The three column centres land 361px apart and the
   middle one lands exactly on the footer's centre line — the same centre line
   .fbottom-row and .fbot now use. Left and right dead space came out at 122.0
   and 119.8px: balanced, which reads as centred rather than as pushed aside. */
.fgrid{display:grid;grid-template-columns:repeat(3,1fr);gap:32px;text-align:center}
.site-footer h4{font-family:var(--font-body);font-size:11.5px;letter-spacing:1.6px;
  text-transform:uppercase;color:var(--on-dark);font-weight:700;margin-bottom:14px}
.site-footer a{color:var(--on-dark-muted);text-decoration:none;display:block;padding:5px 0;font-size:14.5px}
.site-footer a:hover{color:var(--on-dark)}
/* Shrink each link's BOX to its own words, then centre the box.
   `.site-footer a` above is display:block, so every column link was a
   329px-wide bar the full width of its track. Left-aligned that was merely
   generous; centred it is a bug you can feel — the text sits in the middle
   while the hover highlight and the click target run out to both edges of the
   column, so the pointer lights a link up from 120px away from it.
   width:fit-content shrinks the box onto the words and margin-inline:auto puts
   it back in the centre of the track. The links stay stacked (they are still
   blocks) and the hit area is now the label. */
.site-footer .fgrid a{width:fit-content;margin-inline:auto}
/* `.site-footer .brand{color:#fff}` was deleted here along with the .fbrand
   block it styled — the footer no longer contains a .brand element at all, so
   it matched nothing. .brand still exists in the HEADER and keeps its own rule
   up at ~line 263; this was only the footer's override of it. */
.site-footer .blurb{margin-top:14px;font-size:14.5px;max-width:none;color:var(--on-dark-muted)}
/* The third of the footer's three alignment systems, now folded into the one
   the other two use. This row was justify-content:space-between, which pinned
   the copyright to the far left and the "Tempus is a service of" line to the
   far right — a 1052px-wide bar of text underneath a stack of centred content.
   That edge-to-edge line is a large part of why the footer read as disjointed:
   it drew a hard horizontal axis that nothing above it shared.
   Centred, the two legal lines sit as one group on the same centre line as the
   columns, the phone/email and the blurb. The 26px column gap is what keeps
   them reading as two separate statements rather than one run-on sentence, now
   that they are no longer held apart by the width of the footer.
   flex-wrap stays: on a narrow viewport the second line drops below the first,
   and the <=720px block below re-anchors both to the left, where the mobile
   footer's own left-aligned grid is. */
.fbot{border-top:1px solid #1c2338;margin-top:34px;padding-top:20px;display:flex;
  justify-content:center;gap:8px 26px;flex-wrap:wrap;font-size:13.5px;color:var(--on-dark-muted)}

/* ── Footer bottom row (operator decision 9, rebuilt) ─────────────────────
   Two centred lines across the bottom of the footer, directly above the grey
   divider that .fbot draws:
       1. .fcontact — the phone number and the email address, side by side.
       2. .blurb    — the one-line description, underneath them.
   Contact FIRST and blurb second, deliberately: the two contact details are
   the things a visitor scrolls to the footer to find, and the blurb is
   supporting copy. Do not put the blurb back on top.

   HISTORY, so the previous two shapes are not re-attempted. This row was
   originally blurb-left / phone-right on ONE line with
   justify-content:space-between, which parked the number 234px clear of the
   end of the blurb and read as two unrelated fragments. The fix at the time
   was flex-start plus flex-grow:0 on the blurb so the phone sat directly
   after it. Both are now moot — the row is a centred COLUMN, so there is no
   main-axis distribution left to get wrong and nothing for flex-grow to eat.

   The divider is .fbot's own border-top, so this row must never draw one of
   its own — two hairlines 20px apart read as a mistake. */
.fbottom-row{display:flex;flex-direction:column;align-items:center;
  text-align:center;gap:14px;margin-top:34px}

/* One centred line holding both contact details. flex-wrap so a narrow
   viewport drops the email onto its own line instead of letting a long
   address overflow the column; the 16px column gap plus the 8px of padding
   each link now carries reads as the same ~32px pair-gap the old 28px did.
   align-items:center, not baseline: the two links are now 44px-tall boxes
   rather than bare text runs, and baseline alignment of two padded boxes
   buys nothing while making the row's own height harder to reason about. */
.fcontact{display:flex;flex-wrap:wrap;justify-content:center;align-items:center;
  gap:2px 16px}

/* Phone and email carry the SAME weight — neither is the afterthought.
   SPECIFICITY IS LOAD-BEARING HERE, exactly as it is for .fmbtn further down:
   both are bare <a>s inside .site-footer, so `.site-footer a` (0,1,1) claims
   them with colour:#aeb6c9, font-size:14.5px and padding:5px 0. This selector
   is (0,3,1) and beats it. Do not shorten it to `.fphone` / `.fmail`.

   CORRECTION to what this comment used to claim. It said `display:inline` was
   the declaration doing the work and padding:0 was incidental. That was
   backwards, and in fact `display` here is INERT either way: .fcontact is a
   flex container, so both children are blockified and `inline` never survives
   — it computes to `block`. Only the padding was ever load-bearing.

   And the padding it set was padding:0, which was a WCAG 2.5.8 failure. The
   links measured 21.7px tall on the 721–960px range where this row is still
   visible, against a 24px minimum. They are now a full 44px (UI-STANDARDS
   rule 5.3): line-height:22px + 11px top and bottom, chosen over min-height so
   the number is exact and a later font-size change cannot silently shrink it
   below the target again. Horizontal padding is 8px a side so the hit area is
   the whole label, not a 1px-tall strip of text.

   Brighter than the blurb underneath because these are the one thing in the
   footer somebody might actually act on; at #8f97ab on #0a0e1c they read as
   another line of small print. */
.site-footer .fcontact a.fphone,.site-footer .fcontact a.fmail{
  display:block;padding:11px 8px;line-height:22px;
  color:var(--on-dark);font-weight:600;font-size:15.5px}
/* Tabular figures so the number sets as a phone number rather than as prose,
   and nowrap so "0800 600 541" can never break across two lines. */
.site-footer .fcontact a.fphone{white-space:nowrap;
  font-variant-numeric:tabular-nums;font-feature-settings:"tnum" 1}
.site-footer .fcontact a.fphone:hover,.site-footer .fcontact a.fmail:hover{
  color:var(--action-on-dark)}
/* DELETED: `.site-footer a[href^="tel:"]` (0,2,1) and its :hover.
   It was kept "for any tel: link elsewhere in the footer", but there is no
   elsewhere — the footer emits exactly two tel: links and both are already
   won by a more specific rule: the desktop one by
   `.site-footer .fcontact a.fphone` (0,3,1) above, and the mobile Call Us
   button by `.site-footer .fmgrid a.fmbtn` (0,3,1) below. It styled nothing.
   If a third tel: link is ever added, give it a class and style that, rather
   than reviving an attribute selector that sits between the two rules that
   actually matter and has to be reasoned around every time. */
/* Centred and capped. margin-inline:auto as well as the parent's
   align-items:center so the cap still centres the block if this row is ever
   given a different alignment; 68ch keeps it to a readable measure instead of
   one 1100px line. */
.fbottom-row .blurb{margin:0 auto;font-size:14.5px;color:var(--on-dark-muted);
  max-width:68ch;text-align:center}
/* .fbot already carries margin-top:34px for the case where it follows the
   columns directly. When the bottom row is present that gap belongs to the
   row, so the divider tucks in just beneath it. */
.fbottom-row + .fbot{margin-top:20px}

/* ── responsive ────────────────────────────────────────────────────── */
@media (max-width:960px){
  .cards,.grid3,.quotes{grid-template-columns:1fr}
  .fgrid{grid-template-columns:1fr 1fr}
  .mainnav{display:none}
  .navtoggle{display:inline-flex}
  /* COUPLED NUMBER 2 of 3 (with body{padding-top} and .site-header.hidden).
     top MUST equal the bar height. This panel is position:absolute and
     .site-header — the only positioned ancestor, position:fixed — is what it
     measures from, so `top` is an offset from the very top of the viewport,
     not from the bar. It was 72px = the header's old 14px top padding + the
     58px bar; that padding is gone, so it is now the bar height alone.
     Get this wrong and the menu floats below the bar with a strip of the page
     showing through the gap, at every width <=960px.

     margin:0 is NOT tidying either. `.mainnav` carries margin-left:6px
     (~line 269) to nudge the DESKTOP nav row off the wordmark, and
     .mainnav.open is the SAME element, so it inherited that 6px while being
     absolutely positioned with left:0;right:0 — which resolves as left:6px,
     not left:0. Measured on a 375px viewport the open panel was x=6 w=369 and
     a 6px strip of the page showed down its whole left edge, at every width
     <=960px. `.mainnav.open` is (0,2,0) and beats `.mainnav` (0,1,0), so this
     zeroes it for the panel only and the desktop row keeps its nudge. */
  .mainnav.open{display:flex;position:absolute;top:58px;left:0;right:0;margin:0;
    background:var(--canvas);
    flex-direction:column;align-items:flex-start;gap:0;padding:12px var(--pad) 20px;
    border-bottom:1px solid var(--border-gray)}
  /* NAV-006: .mainnav a is white for the DARK desktop bar, but .mainnav.open repaints
     this panel #fff for mobile - without an explicit colour the links are white on white
     and invisible. 5 of 8 nav links were unreadable at every width <= 960px. */
  .mainnav.open a,.mainnav.open .grp>button{padding:11px 0;width:100%;color:var(--text-dark)}
  .mainnav.open a:hover,.mainnav.open .grp>button:hover{color:var(--action-press)}
  .mainnav.open .grp .menu{position:static;display:block;border:0;box-shadow:none;padding:0 0 0 12px;
    min-width:0}
  /* The desktop hover bridge has no job here and would do harm. This panel is
     a plain stacked list — the menu is static and sits directly under its
     button with no gap to bridge — but the bridge is position:absolute, so it
     would stop measuring from the menu and start measuring from .grp, landing
     as an invisible 14px strip over the group heading and eating taps on it.
     There is no hover on a phone for it to help with either. */
  .mainnav.open .grp .menu::before{content:none}
}
@media (max-width:560px){
  /* DELETED: `.fgrid{grid-template-columns:1fr}`. It was dead — .fgrid is
     display:none for the whole of max-width:720px (see the mobile footer
     block), so no viewport ever reaches 560px with .fgrid still laid out.
     Its live sibling is the 960px rule above, which covers 721–960px: that
     IS the only range where .fgrid is both visible and narrow. */
  .hdr-cta .btn-ghost{display:none}
}

/* hero fact strip */
.facts{list-style:none;display:flex;gap:22px;flex-wrap:wrap;margin:24px 0 0;padding:18px 0 0;
  border-top:1px solid var(--border-gray);max-width:none}
.facts li{font-size:14.5px;color:var(--text-gray);position:relative;padding-left:20px}
.facts li::before{content:"";position:absolute;left:0;top:7px;width:9px;height:9px;border-radius:3px;
  background:var(--tempus-pink)}


/* ═══════════════════════════════════════════════════════════════════════
   Two-column layouts. The 66-character measure is right for reading, but
   a reading column alone leaves half the canvas empty at desktop widths.
   Text keeps its measure; the other half carries a supporting visual.
   ═══════════════════════════════════════════════════════════════════════ */
.split{display:grid;grid-template-columns:1.12fr .88fr;gap:clamp(28px,4vw,60px);align-items:center}
.split.top{align-items:start}
.split .col-text p{max-width:none}   /* measure is applied to .article only, never globally */

/* image placeholder — clearly marked so nobody mistakes it for final art */
.visual{border-radius:var(--radius);overflow:hidden;border:1px solid var(--border-gray);
  background:var(--canvas);box-shadow:var(--shadow);position:relative}
.visual .ph{aspect-ratio:4/3;display:flex;flex-direction:column;align-items:center;
  justify-content:center;gap:8px;text-align:center;padding:26px;
  background:
    linear-gradient(135deg,rgba(230,0,126,.06),rgba(9,20,100,.06)),
    repeating-linear-gradient(135deg,#fafbfd 0 14px,#f2f4f8 14px 28px)}
.visual .ph .lbl{font:700 10.5px var(--font-body);letter-spacing:1.6px;text-transform:uppercase;
  color:var(--ink-faint)}
.visual .ph .desc{font-size:14.5px;color:var(--text-gray);max-width:none}
.visual .ph .icon{width:44px;height:44px;border-radius:12px;
  background:conic-gradient(from 210deg,var(--tempus-pink),#ff7ac4,var(--accent-blue),var(--dark-blue),var(--tempus-pink));
  opacity:.85}

/* a fake product surface — reads as "screenshot goes here" without pretending to be one */
.uiframe{background:var(--canvas);border-radius:var(--radius);border:1px solid var(--border-gray);
  box-shadow:var(--shadow);overflow:hidden}
.uiframe .bar{display:flex;gap:6px;padding:11px 14px;background:#f7f8fa;
  border-bottom:1px solid var(--border-gray)}
.uiframe .bar i{width:9px;height:9px;border-radius:50%;background:#d8dce4;display:block}
.uiframe .body{padding:18px}
.uiframe .row{display:grid;grid-template-columns:1.6fr 1fr 1fr;gap:10px;padding:13px 4px;
  border-bottom:1px solid #f0f2f6;align-items:center}
.uiframe .row:last-child{border-bottom:0}
.uiframe .row span{height:9px;border-radius:5px;background:#e9ecf2;display:block}
.uiframe .row.head span{background:#c9cfdc;height:8px}
.uiframe .row span.pink{background:rgba(230,0,126,.28)}
.uiframe .cap{font:600 11px var(--font-body);letter-spacing:1.4px;text-transform:uppercase;
  color:var(--ink-faint);padding:0 4px 10px}

/* ── real pictures ──────────────────────────────────────────────────────────
   Every real photograph and screenshot on the site is emitted by picture() in
   _source/build.py and comes out wearing these two classes: .pic on the
   <picture> wrapper, .pic-img on the <img> inside it. Individual slots add a
   third, .pic-<slot name> — the home page hero is .pic-home-hero — which is
   where per-slot styling belongs.

   These two rules are the MINIMUM that makes a picture behave, and nothing
   more. They are not a look.

   Why each one is needed, since both appear to be doing nothing:
     display:block on the <img> removes the few pixels of empty space a
       browser leaves under an inline image — the gap that makes a picture
       sitting in a bordered box look mysteriously off-centre.
     width:100%;height:auto is load-bearing, not cosmetic. The smallest file in
       the set is 720 pixels across and the space it sits in on a desktop is
       about 450. Without this the picture renders at its own full size and
       bursts out of the column. "auto" then keeps its proportions, and because
       the <img> also carries width= and height= attributes, the browser works
       the shape out and holds the space open before the file has arrived — so
       the text underneath does not jump when it lands.

   DELIBERATELY ABSENT: no border-radius and no box-shadow. The hero master has
   soft, rounded, see-through corners painted into the picture itself. A CSS
   corner radius would cut a second, differently-shaped corner across the one
   already there, and a box-shadow follows the square element box, so it would
   draw a hard rectangular shadow behind visibly soft edges. If a future slot
   uses a hard-edged master, that slot's own .pic-<name> rule is the place for
   a radius — not here, where it would reach every picture on the site. */
.pic{display:block}
.pic-img{display:block;width:100%;height:auto}

/* an aside that sits beside prose instead of white space */
.aside{background:var(--light-gray);border:1px solid var(--border-gray);border-radius:var(--radius);
  padding:24px}
.aside h4{font-family:var(--font-body);font-size:11.5px;letter-spacing:1.6px;text-transform:uppercase;
  color:var(--text-gray);font-weight:700;margin-bottom:12px}
.aside ul{list-style:none;display:grid;gap:10px}
.aside li{font-size:15px;padding-left:20px;position:relative;color:var(--ink-body)}
.aside li::before{content:"";position:absolute;left:0;top:8px;width:8px;height:8px;border-radius:3px;
  background:var(--tempus-pink)}

/* DELETED HERE 2026-09-09: `.page-hero .wrap{grid-template-columns:1fr;row-gap:12px}`.
   Both of its declarations only do anything to a grid or a flex container, and a
   page hero's .wrap was a plain block on all eighteen pages, so the rule had been
   doing nothing at all. It stopped being harmless the moment the three "who we
   help" heroes gained a picture beside the title and became <div class="wrap
   split">: at (0,1,1) it out-specifies .split's own (0,1,0) stacking rule below,
   so it took over the row gap and put 12px between the standfirst and a
   photograph where every other stacked split on the site leaves 26px. Removing it
   hands those heroes back to .split, which is the rule that should own them.
   If a page hero ever needs a gap of its own again, write it against
   `.page-hero .wrap.split` so it is visibly about the two-column case. */
@media (max-width:960px){
  .page-hero .sub{margin-top:4px}
  .split{grid-template-columns:1fr;gap:26px}
  .steps{grid-template-columns:1fr}
}


/* ═══════════════════════════════════════════════════════════════════════
   Layout system, rewritten. Nothing is pinned to the left half any more.
   ═══════════════════════════════════════════════════════════════════════ */

/* hero fact strip spans the full width beneath both columns */
.hero .wrap.split{row-gap:0}
.facts{grid-column:1/-1;max-width:none;display:flex;gap:32px;flex-wrap:wrap;
  margin:26px 0 0;padding:18px 0 0;border-top:1px solid var(--border-gray)}

/* video sections: blurb left, video right, equal halves */
.split-media{display:grid;grid-template-columns:1fr 1fr;gap:clamp(24px,3vw,44px);align-items:start}
.split-media .sec-head{margin-bottom:14px;max-width:none}
.split-media .speakers{margin-top:14px}

/* ── THE LEADER'S PORTRAIT IS CAPPED, NOT FILLED TO ITS COLUMN ──────────
   ADDED 2026-09-21, from the operator's review of /our-story: on desktop the
   photo was "too large relative to the text", and on a phone "near full width,
   it does not need to be".

   MEASURED BEFORE: a 4:5 portrait in a 1fr column is exactly as wide as the
   text beside it, so at 1440px it was 504 x 631 against a 517px-tall text
   column — 114px taller, hanging below the quote box. UI-STANDARDS 3.4 says a
   media block must not force its row taller than the text needs.

   380px IS THE SIZE THE OPERATOR PICKED (75% of that 504) from a rendered
   mock-up, and it is a fixed cap rather than a percentage of the column. The
   media column is 391.9px even at 901px, so the cap binds at every desktop
   width: the photo is a constant 380 x 475, 42px shorter than the text at
   1440. A percentage would have tracked the window instead — 75% of 901px's
   392px column is a 294 x 367 photo beside 593px of text. THE PRICE OF A FLAT
   380: on a small laptop the photo is smaller than it used to be (it was 392,
   445 and 478px wide at 901, 1024 and 1100) and up to ~120px shorter than the
   text (119.6px at 916px), which leaves blank column beneath it. That is
   inside the 140px uicheck.py allows, and it was shown to the operator at
   1024px in the mock-up; if it ever needs to be tighter, taper the cap up on
   narrow laptops rather than changing 380. justify-self:end keeps the photo's
   right edge on .wrap's content edge, level with everything else on the page.
   The two columns stay equal (OST-016 asserts they are); only the photo inside
   the right-hand one changes.

   At 900px and under the grid is one column and the portrait sits below the
   heading (the order block near the end of this file). There it is 70% of the
   content width and centred, and the 380px max-width above still caps it —
   the width is 70%, not min(70%,380px), so the ceiling has one owner. 245 x
   306 on a 390px phone, where it was 350 x 438. 70% of the content width
   passes 380px at about 603px, so a tablet gets the same 380 x 475 as a laptop
   instead of a 691px-wide photo.

   ITS OWN CLASS, "split-leader", added by sec_leader() in build.py, because
   .split-media is shared with sec_video, whose 16:9 video must keep filling
   its column. This caps the real <picture> only; the ph_image() fallback that
   sec_leader() can still emit for a leader with no photo is not capped, and no
   live page uses it. IMAGE_SLOTS["ryon-portrait"]["sizes"] in build.py restates
   these widths so a phone is not sent a bigger file than it will draw — change
   the two together.

   VERTICALLY CENTRED ON DESKTOP — added 2026-09-21, second pass. Operator:
   "fractionally lower, vertically centered between the pink text and the
   bottom of the box with the quote in it". align-self:center puts the photo's
   midpoint on the midpoint of the text column — the eyebrow's top edge down to
   the quote box's bottom edge — instead of hanging from the top. At 1440 that
   is 21px lower: the text column is 517px and the photo 475px, so 21px above
   it and 21px below. Only the PICTURE is centred. .split-media keeps
   align-items:start, so the text column and its heading still begin at the
   top of the row. That is the failure UI-STANDARDS 13.12 was written against
   (centring the whole split pushed headings ~84px down the page), and it
   cannot recur here. The photo is also the shorter item at every desktop
   width, so 3.4 (a media block must not force its row taller than the text)
   is untouched; if the copy is ever cut below the photo's height the row
   simply becomes the photo's height and centring does nothing. What DOES
   depart from 13.12 is its letter — "a heading sits level with the media
   beside it": the photo now starts 21px below the eyebrow at 1440. That is
   the operator's call, recorded at UI-STANDARDS 0.6. On a 900-1100px laptop
   the text column runs up to ~120px taller than the photo; the same rule
   then splits that spare space evenly (~60px above, ~60px below) instead of
   leaving all of it under the photo. The 900px-and-under block resets it to
   auto, so a phone is untouched by construction, not by luck. */
.split-leader picture{width:100%;max-width:380px;justify-self:end;align-self:center}
@media (max-width:900px){
  .split-leader picture{width:70%;justify-self:center;align-self:auto}
}

/* ── THE VIDEO'S TOP EDGE LANDS ON THE EYEBROW'S CAP LINE ──────────────
   4px, measured, not eyeballed. Added 2026-09-09.

   The operator said the video's top edge "should be with the top edge of that
   line" and that what was there was wrong. Checking it in a browser, the two
   BOXES were already flush: .split-media is a grid with align-items:start, so
   .col-text and the video both began at 3413.66px — an offset of exactly 0.
   The mismatch is optical, which is why it survived a box-model check.

   The first thing in the text column is the "Client Story" eyebrow: 12px DM
   Sans in a 17.64px line box. Rasterising that exact face and scanning for the
   first painted row of pixels puts the cap-height of the capitals 3.82px below
   the top of their line box. So the video's hairline edge floated ~4px ABOVE
   the first thing a reader sees, and a designer reads that as the video sitting
   too high — which is exactly what was reported.

   Pushing the video down by that half-leading puts its edge on the cap line:
   0.18px out instead of 3.82px. This is the ordinary optical correction for
   aligning a rule to text, and the number is the eyebrow's leading — if the
   eyebrow's size or line-height changes, re-measure it; do not scale it by eye.

   It is set on .videowrap too, not just the placeholder, so the shipped video
   lands where the approved placeholder was. A slot that moves 4px on the day
   the real file arrives is a bug nobody is watching for.

   Desktop only, and that is the point of the min-width. Below 900px this block
   stacks and the video drops beneath the text, where there is no eyebrow beside
   it to align to and the nudge would just be 4px of stray gap. */
.split-media .videowrap,.split-media .ph-video{margin-top:0}
@media (min-width:901px){
  .split-media .videowrap,.split-media .ph-video{margin-top:12px}
  /* 12px, MEASURED ON THE DEPLOYED PAGE, 2026-09-09. The operator asked for
     the video's top edge to line up with the top of the HEADING ("Equitas, in
     Their Own Words") — not the eyebrow above it, which is what an earlier
     pass aligned to. At 34px it sat 22px low; 34 - 22 = 12.
     ONE RULE OWNS THIS PROPERTY. A previous attempt added a second rule at
     the same (0,2,0) specificity but earlier in the file, so this one won and
     the page did not move at all — the diff looked right and nothing changed.
     Change the number here; do not add another rule. */
  /* -16.18px, MEASURED, 2026-09-09. This rule already existed at 3.82px and
     was already trying to do this alignment; at that value the video still
     sat 20px BELOW the top of the text column beside it, which is the gap
     the operator pointed at. 3.82 - 20 = -16.18.
     A second rule was briefly added elsewhere to fix this and it did
     nothing: it was `.split-media > .ph-video` at (0,2,0), the same
     specificity as this pair but EARLIER in the file, so this one won and
     the page did not move. One property, one owner — if this needs
     changing again, change it here rather than adding a third rule. */
}

/* 16:9, the ratio the operator asked to see — "a 1920 by 1080 aspect ratioed
   preview". This used to say 16/10 and was then overridden again further down
   the file; both are gone. The base .ph-video rule already declares 16/9, so
   this line exists only to out-specify anything that reaches the placeholder
   through a .split-media ancestor, and to be findable next to the layout it
   belongs to. See the 3.4 block near the end of this file for why the fixed
   236px height that used to win here has been removed. */
.split-media .ph-video{aspect-ratio:16/9}

/* ── a tall picture beside a list of items ─────────────────────────────
   The home page's "so much more than just software" block. Picture left, the
   items down the right.

   THE ITEM COUNT IS NOT FOUR. The approved mock-up draws four; the site's copy
   holds six, and the copy wins (see sec_feature_grid() in build.py, which says
   why at length). Nothing here counts: a seventh item would render, and the
   rule between items comes from :not(:first-child) rather than an index.

   align-items:start, NOT the mock-up's center. The mock-up centres because
   four items make the two columns nearly the same height. Six make the list
   the taller of the two, and centring then floats the picture in the middle of
   its column with unequal air above and below it. Tops aligned.

   The hairline is rgba(255,255,255,.36) — the SAME value every other hairline
   on the navy surface uses, measured at 3.10:1. The mock-up's .16 is a second
   on-navy hairline value and a fainter one; there is only ever one.

   No colour is set on the PARAGRAPHS here — .surface-tile p already paints them
   muted-white, and the mock-up's second color:#fff declaration was redundant.
   The headings are now an exception to that, deliberately; see below.

   ── THE SIX ITEMS FIT INSIDE THE PICTURE. Retuned 2026-09-09. ─────────
   The operator asked for "the overall height of these tightened up so that
   hopefully the six of these will fit in the total height of this image", and
   named the heading-to-body gap as the easy win. Measured at 1440px before the
   change: the 4:5 picture is 581.9px tall and the six items ran 739.4px, so the
   list overran the picture by 157.5px — visible as the last item and a half
   hanging below the bottom of the photograph.

   Three spacing numbers were cut and NO type was resized. That order matters:
   the operator asked to tighten spacing, not to shrink the copy, and the
   paragraphs stay at their inherited 17px.

     1. the heading-to-body gap  8px → 4px   (the named easy win)
     2. the padding either side
        of each hairline        24px → 12px
     3. the heading's leading    1.3 → 1.15

   (3) looks like typography and is not. Every heading in this list is a single
   word — Process, Integrations, Software, Hardware, Automations, Legislation —
   so its line-height never separates two lines of anything; it is 3.3px of dead
   air locked above and below each title, six times over. Taking it costs
   nothing a reader can see. THE MOMENT AN ITEM TITLE WRAPS TO TWO LINES this
   stops being true and 1.15 becomes genuinely tight, so that is the number to
   revisit — not the padding.

   After: the six items measure 575.6px against the picture's 581.9px. They fit,
   with 6.3px to spare, at every width from 1280px up (the .wrap caps at 1180px,
   so nothing above 1440px moves). Below 900px the block stacks into one column
   and the question stops applying.

   THAT LEAVES 901px TO 1179px, WHICH THIS PARAGRAPH USED TO PASS OVER IN
   SILENCE. The block is still two columns there and the spacing work above did
   NOT make the six items fit: they overran by 125.1px at 901 and 73.3px at
   1024. The column widths in that band are what closes it, and they are set in
   their own rule further down this file — search for "ON A SMALL LAPTOP". Read
   that before concluding from this paragraph that the question is settled.

   ── PINK HEADINGS ────────────────────────────────────────────────────
   Added 2026-09-09. The operator asked for "some colourization against these
   headings", floated a pink dot marker, rejected the dot out loud, and did not
   name what should replace it. This is the plainest reading of what is left:
   colour the headings themselves, in the one on-navy accent the palette already
   has. No new value is introduced — --action-on-dark is the same #ff5cae that
   decision 12 approved for pink on this surface.
   Measured on the background actually behind them — #091464, read off the
   rendered page rather than off the palette, because what is painted is the
   question: 5.71:1, the same figure the rest of this file records for that
   pair. It clears AA for body text, never mind the 22px headings it is used on.
   It IS a real step down from the 16.21:1 the white headings had, and that is
   the price of the colour rather than a surprise. Worth knowing before anyone
   reuses this on smaller type: at 22px there is a lot of headroom, and there
   would be much less at 13px.
   The pink is safe here only because this surface is FLAT navy. Where the navy
   runs into the hero's gradient, the same #ff5cae measures 3.86:1, 1.35:1 and
   1.84:1 on the three blend stops and 2.59:1 on the parchment at the foot of it
   — all four short of 4.5:1. (This note used to give the range as "1.80:1 to
   2.38:1". Those are real numbers but they are the hero SCRIM's, not the
   gradient's — two separate warnings on this surface that had been run
   together. Recomputed and separated 2026-09-09.) Do not carry this rule onto a
   gradient without re-measuring it there.
   It out-specifies .surface-tile h3 by carrying the surface in the selector —
   (0,2,1) against (0,1,1) — rather than by relying on which rule comes later in
   this file, so moving either one cannot silently undo it. Naming the surface
   also fails safe: this block is only ever used once today, on the navy, but if
   a feature grid is ever put on a pale surface the headings fall back to the
   ordinary dark ink instead of becoming pink on white, which is 2.84:1 and
   would fail. */
.splitlist{display:grid;grid-template-columns:.95fr 1.05fr;
  --splitlist-gap:clamp(32px,5vw,72px);
  gap:var(--splitlist-gap);align-items:start;margin-top:52px}
.splitlist-media{min-width:0}
.splitlist-items{display:flex;flex-direction:column;min-width:0}
.splititem{padding:12px 0;border-top:1px solid rgba(255,255,255,.36)}
.splititem:first-child{border-top:0;padding-top:0}
.splititem:last-child{padding-bottom:0}
.splititem h3{line-height:1.15}
.surface-tile .splititem h3{color:var(--action-on-dark)}
.splititem p{margin-top:4px;max-width:none}

/* ── THE SIX ITEMS ON A SMALL LAPTOP. Added 2026-09-09. ────────────────
   The block above records the six items fitting inside the picture "at every
   width from 1280px up". That was true and it was not the whole truth: between
   901px and 1179px the block is still two columns and the list still overran
   the picture, and 1024px is an ordinary small laptop. Measured on the built
   page, list height minus picture height:
     901px  +125.1     960px  +96.8     1024px  +73.3     1100px  +20.5
   (1180px and above already cleared it, at -19.9px and better.)

   THE PICTURE IS THE THING THAT IS TOO SHORT, NOT THE LIST THAT IS TOO TALL.
   The slot is 4:5, so its height is 1.25x the width of its column, and at 901px
   that column is only 363.8px — a 454.7px picture against a list that cannot
   go below about 576px without shrinking type. Tightening the component further
   was the wrong lever twice over: it would over-compress the layout at 1280px
   and up where it is already correct, and the numbers above show it would have
   had to find another 125px, which no amount of spacing holds.

   SO THE COLUMN WIDTH IS THE LEVER, AND IT HAS A CEILING AND A FLOOR.
     minmax(...,.95fr) — the .95fr stays the MAXIMUM, so this can only ever
       widen the picture inside the band, never past what it already gets.
     490px — the ceiling. The list settles at 575.6-600.6px across this band, so
       490 x 1.25 = 612.5px of picture clears it with room. Going wider buys
       nothing and only starves the text.
     345px — the floor under the TEXT column, and the reason this is a min() and
       not a flat 490px. Squeeze the items narrower than that and their
       paragraphs take an extra line each; the list grows faster than the
       picture and the overflow gets WORSE. Measured at 901px: a 470px picture
       with a 296px text column overruns by 117.3px, while 420.9px of picture
       against 345px of text overruns by 53.8px. 345 is the widest the text can
       be squeezed to before that turns around.

   RESULT, MEASURED, before -> after (negative means the picture is the taller):
      901px  +125.1 ->  +53.8      1024px   +73.3 ->  -22.4
      920px   +33.6 (after)        1100px   +20.5 ->  -11.9
      940px   +12.4 (after)        1140px          ->  -11.9
      950px    +1.8 (after)        1179px          ->  -36.9
      960px   +96.8 ->   -7.5

   THIS RULE CLOSES IT FROM 952px UP AND NOT BELOW THAT — said plainly rather
   than rounded away. In the 51px of window width between the 900px stacking
   breakpoint and 952px the list still overhangs it, by 53.8px at the very worst,
   and nothing about the COLUMN WIDTHS can change that: the six items cannot fit
   a 4:5 picture that narrow while the paragraphs stay at 17px. The alternatives
   at this lever were weighed and rejected — dropping the gap to 16px only
   reaches +17.5px and starves the column; moving the stacking breakpoint up to
   952px would close it completely but would break this block away from
   .split-media, .blocks and .faq, which stack together at 900 on purpose (see
   the note under this one).
   THAT BAND IS NO LONGER LEFT OPEN. It is closed by a separate rule further
   down, which lets the picture stretch to the list's height for those 51px
   instead of holding it to 4:5 — search for "THE LAST 51px OF IT, CLOSED".
   Read that before concluding from this paragraph that 901-951px still
   overhangs; it does not.

   THE GAP IS READ, NOT ASSUMED. --splitlist-gap is declared on .splitlist above
   and used both as the gap and inside the calc here, so the two cannot drift
   apart. Change the gap in one place and this arithmetic follows it.

   IT STOPS AT 1179.98px SO IT CANNOT REACH THE WIDTHS THAT ARE ALREADY RIGHT.
   Above 1180px nothing at all changes — 1180px and 1280px measure exactly what
   they measured before this rule existed. There IS a step at the boundary and
   it is not nothing: the picture goes from 490.0px wide and 612.5px tall at
   1179px to 476.4px and 595.5px at 1180px, so it loses 13.6px of width and 17px
   of height in one pixel of window. Nobody sees that unless they drag a window
   edge slowly across 1180px, and the alternative — carrying the wider picture
   on past 1180 — would re-proportion the layout at the widths the operator has
   already approved. That trade is the reason the number is 490 and not smaller:
   490 is what clears the list at 1100 and 1140, where it runs 600.6px. */
@media (min-width:901px) and (max-width:1179.98px){
  .splitlist{grid-template-columns:
    minmax(min(490px, calc(100% - var(--splitlist-gap) - 345px)),.95fr)
    minmax(0,1.05fr)}
}

/* ── AND THE LAST 51px OF IT, CLOSED. Added 2026-09-09. ────────────────
   The rule above got the six items inside the picture from 952px up and said
   plainly that 901-951px was still open, by 53.8px at the worst. This closes
   that band too, and it does it by the same diagnosis the rule above already
   reached: THE PICTURE IS THE THING THAT IS TOO SHORT.

   BOTH WIDTH LEVERS ARE GENUINELY EXHAUSTED, MEASURED RATHER THAN ASSERTED.
   All measurements below are on the built home page in Chrome at a 901px
   window, where .splitlist is 810.91px wide and the gap computes to 45.05px.

   1. THE COLUMN SPLIT. Sweeping the text column from 380px down to 250px and
      reading the two heights back:
        text 380 -> picture 482.3 vs list 579.9   (+97.6)
        text 350 -> picture 519.8 vs list 579.9   (+60.1)
        text 345 -> picture 526.0 vs list 579.9   (+53.8)   <- the best there is
        text 340 -> picture 532.3 vs list 604.9   (+72.6)   <- a paragraph wraps
        text 300 -> picture 582.3 vs list 704.8  (+122.5)
      Every one of the six paragraphs is exactly two lines at 345px and the
      first of them goes to three at 340px, so 345 is not a taste call — it is
      the last width before the list starts growing faster than the picture.
      +53.8px is the floor of that curve.

   2. THE GAP. With the text held at that 345px floor the picture gets
      810.91 - gap - 345 of width, and at 4:5 it needs 463.9px of width to
      reach the list's 579.9px. Measured, holding text at 345 and varying only
      the gap:
        gap 45.05 -> +53.8    gap 32 -> +37.5    gap 16 -> +17.5
        gap 8     ->  +7.5    gap  4 ->  +2.5    gap  2 ->   0.0
      It takes a 2px gap between a photograph and the words about it. That is
      not a layout, so the gap is not the lever either.

   SO THE ASPECT RATIO GIVES, AND ONLY INSIDE THIS BAND. The picture stretches
   to whatever height the items beside it come to. It can only ever grow here,
   never shrink, because across the whole band the list is the taller of the
   two — measured 901px +53.8, 920px +33.6, 940px +12.4, 950px +1.8, 951px
   +0.8 — and the band stops at exactly the pixel where that stops being true:
   at 952px the 4:5 picture is already 0.09px the taller, so from there the
   shape below takes over on its own and this rule must not reach it. The
   result is a ratio that slides from 0.726 at 901px back to 0.799 at 951px and
   hands over to a true 4:5 at 952px, so there is no visible step at either
   edge of the band. The overhang is 0.0px at every width inside it.

   WHY THIS IS NOT A BREACH OF "the six-item image is 4:5". This component's
   ratio is already width-dependent by design: below 901px it becomes 4:3 (see
   the rule under this one, and the reason written there). 4:5 remains the shape
   everywhere the layout can honour it; what this says is that in the 51px where
   it cannot, the picture grows rather than the list overhanging it. Nothing
   here touches the 17px body size and nothing here drops an item, which were
   the two things the brief ruled out.

   A REAL PHOTOGRAPH NOW SITS IN THIS SLOT (2026-09-09) and the rule the note
   here used to predict is written below, just under this block. The prediction
   was right: .pic-img is width:100% with height:auto, so in a stretched box it
   would have sat at its natural height with a gap under it. */
@media (min-width:901px) and (max-width:951.98px){
  .splitlist{align-items:stretch}
  .splitlist-media{display:flex;flex-direction:column}
  /* THIS LINE IS WHAT ACTUALLY CLOSES THE BAND. Measured 2026-09-09: with it,
     the picture and the list both come out 580px at a 901px window; the
     aspect-ratio release below is not doing that work. Test PHO-008's second
     mutation removes this line specifically, so the dependency is visible to
     something other than a reader. */
  .splitlist-media>*{flex:1 1 auto;min-height:0}
  /* RETARGETED 2026-09-09 when a real photograph replaced the placeholder.
     This used to name `.ph-video.ar-4x5` only, and the moment build.py started
     emitting `<picture class="ar-4x5">` here instead, the rule stopped matching
     anything — silently, in a 51px band nothing tests.

     WHAT REMOVING IT ACTUALLY COSTS, MEASURED RATHER THAN ASSUMED. The obvious
     story is that without the release the 4:5 ratio holds the picture short and
     the 53.8px overhang comes back. That story is WRONG, and it was tested by
     reverting the selector and looking: at a 901px window the picture still
     measures 580px against the list's 580px. `flex:1 1 auto` on
     `.splitlist-media>*` two lines above beats aspect-ratio in a column flex
     container, so the height is already held without this rule.

     It is retargeted anyway, for two reasons that are not "it breaks today":
       · the rule matched nothing, and a rule matching nothing reads as evidence
         that the element it names is what sits here. It is not.
       · the height now depends ENTIRELY on that flex line. Change or remove
         `flex:1 1 auto` and the ratio becomes load-bearing again, with nothing
         to warn you. This rule is what makes that a survivable edit instead of
         a second 54px regression in the same 51px band.

     Both selectors are kept because the placeholder path is still reachable
     (a future page with the slot and no photograph yet).

     (0,3,0) each, so both beat the bare `.ar-4x5` pairing on weight rather than
     on which rule happens to sit lower in this file. Releasing the ratio is
     safe for a real picture because the <img> inside carries object-fit:cover
     (see the end of this file) — it fills the stretched box and crops, rather
     than distorting. */
  .splitlist-media>.ph-video.ar-4x5,
  .splitlist-media>picture.ar-4x5{aspect-ratio:auto}
}

/* One column on a phone, and the picture goes from portrait to landscape with
   it: a 4:5 portrait at full phone width is very nearly a whole screen of
   photograph before the reader reaches a single word.
   900px rather than 960 on purpose — this block is a sibling of .split-media,
   .blocks and .faq, which all stack at 900, and it should stack with them. */
@media (max-width:900px){
  .splitlist{grid-template-columns:1fr;gap:28px}
  .splitlist-media .ar-4x5{aspect-ratio:4/3}
}

/* prose whose paragraphs carry bold lead-ins becomes a grid of blocks —
   fills the width, reads better, and roughly halves the height */
.blocks{display:grid;grid-template-columns:repeat(auto-fit,minmax(290px,1fr));gap:16px;
  align-items:stretch}
.blocks .blk{background:var(--canvas);border:1px solid var(--border-gray);border-radius:var(--radius);
  padding:24px 26px 22px;transition:transform .18s ease,border-color .18s ease}
.blocks .blk:hover{transform:scale(var(--card-hover-grow));border-color:var(--hairline-hover)}
.blocks .blk p{margin:0;font-size:15.5px;color:var(--text-gray)}
.blocks .blk strong{display:block;margin-bottom:6px;color:var(--ink-h);font-size:16px}
.lede{font-size:17.5px;color:var(--text-gray);max-width:none;margin-bottom:20px}

/* faq: two per row */
.faq{grid-template-columns:1fr 1fr;max-width:none;align-items:stretch;gap:12px}

/* prose sections that keep a supporting visual give the text more room */
.split.top{grid-template-columns:1.35fr .65fr;align-items:stretch}

@media (max-width:900px){
  .split-media,.blocks,.faq{grid-template-columns:1fr}
  .split.top{grid-template-columns:1fr}
}


/* heading left, prose right — a full-width row that still keeps a sane
   line length, instead of either a narrow strip or a 130-character line */
.split-head{display:block}
.split-head h2{margin:0 0 14px}
/* two text columns fill the width without a 140-character line */
.split-head .prose.twocol{columns:2;column-gap:clamp(28px,3.4vw,52px)}
.split-head .prose p{max-width:none;break-inside:avoid}
.split-head .prose:not(.twocol) p{max-width:none}
.split-head .prose p+p{margin-top:1em}
@media (max-width:820px){.split-head .prose.twocol{columns:1}}

/* The CTA block's LAYOUT — stacked and centred, nothing beside the heading.
   Its navy is not here; that is .surface-tile, above. Splitting them on
   2026-09-08 is what stops any other section put on the navy surface from
   being silently centred as well.

   DELETED in the same change: .band{padding:clamp(28px,2.6vw,44px) 0}. It was
   the last section-padding override that did not read --section, and it had to
   go rather than be rescaled. If the navy block pads differently from a white
   one, the colour change stops landing on a consistent rhythm — and "the
   colour change IS the divider" is the whole design. The one bottom-edge
   exception it leaves behind (this block above the near-navy footer) is
   handled by main>section:last-child.surface-tile in the rhythm block. */
.cta-band .wrap{display:block;text-align:center}
.cta-band h2{max-width:none;margin:0}
.cta-band p{margin:12px 0 0;max-width:none}
.cta-band .btn-row{margin-top:18px;justify-content:center}

@media (max-width:900px){
  /* .band .wrap used to be in this selector list and was inert: .cta-band .wrap
     is display:block, so neither grid-template-columns nor gap did anything to
     it. Dropped 2026-09-08 rather than renamed. */
  .split-head{grid-template-columns:1fr;gap:16px}
}


/* ── scroll reveal ───────────────────────────────────────────────────
   Short and low-travel on purpose: 16px and 420ms reads as the page
   settling, not as an effect. Anything longer gets irritating on a
   second visit. Disabled outright for reduced-motion users.
   ──────────────────────────────────────────────────────────────────── */
.reveal{opacity:0;transform:translateY(16px);
  transition:opacity .42s cubic-bezier(.16,1,.3,1),transform .42s cubic-bezier(.16,1,.3,1)}
.reveal.in{opacity:1;transform:none}
.reveal.d1{transition-delay:.06s} .reveal.d2{transition-delay:.12s} .reveal.d3{transition-delay:.18s}
@media (prefers-reduced-motion:reduce){
  .reveal{opacity:1;transform:none;transition:none}
  .site-header{transition:none}
}

/* money always in tabular figures — the quiet financial-data signal */
.uiframe,.fig,.money,td.num{font-variant-numeric:tabular-nums;
  font-feature-settings:"tnum" 1}


/* ═══════════════════════════════════════════════════════════════════════
   UI-STANDARDS 4.1 / 4.2 — every repeated element in a row is the same
   height, and its content sits centred rather than hugging the top of a
   box that a taller sibling stretched. Applied once here rather than
   patched per-component, which is how the ragged rows kept coming back.
   ═══════════════════════════════════════════════════════════════════════ */
.card, .tile, .blk, .quote{display:flex;flex-direction:column;justify-content:flex-start}
.faq details{display:flex;flex-direction:column;justify-content:flex-start}
.faq details[open]{justify-content:flex-start}
.step{align-content:center}
a.tile{justify-content:flex-start}
.blocks .blk p{margin:0}

/* 3.4 — a media block must not force the row taller than its text partner.
   The rule stands; the way it used to be enforced does not. */
.split-media{align-items:start}
/* An empty <video> falls back to 300x150, so the block collapses wherever
   the file is not present (the mockup, or before the asset is deployed).
   An explicit aspect ratio reserves the real footprint either way. */
.videowrap video{aspect-ratio:16/9;width:100%;height:auto;object-fit:cover}

/* ── WHY THE 236px CAP IS GONE. Removed 2026-09-09. ────────────────────
   Two lines used to live here:

     .split-media .videowrap{align-self:center}
     .split-media .ph-video{aspect-ratio:auto;height:236px;max-height:236px}

   The second was the answer to "the video preview is not 16:9". .ph-video
   declares aspect-ratio:16/9 at the top of this file and .split-media .ph-video
   re-declared 16/10 further up — and then THIS rule, last in the cascade and
   more specific than the base, threw the ratio away entirely with
   aspect-ratio:auto and pinned the height to a flat 236px. In a 504.41px column
   that renders 2.14:1. Nothing was broken about the aspect-ratio property; it
   was simply being switched off by a rule 750 lines away from the one that set
   it. The fix is the deletion, not another !important on top.

   3.4 IS BEING KNOWINGLY BROKEN ON THREE PAGES, AND THIS IS THE RECORD OF IT.
   Corrected 2026-09-09, because what stood here was comforting and wrong.

   IT USED TO SAY the video is "the SHORTER of the two, so it never forces the
   row", and that "narrower viewports only widen the margin". That is true on
   home.html and it is true nowhere else. Re-measured at 1440px on the built
   pages, video height minus the height of the text column beside it:

     home.html            Equitas          283.7 vs 674.4  ->  -390.7  video shorter
     stories.html         Equitas          283.7 vs 674.4  ->  -390.7  video shorter
     stories.html         Marralomeda      283.7 vs 123.1  ->  +160.6  VIDEO TALLER
     stories.html         Novo Group       283.7 vs 123.1  ->  +160.6  VIDEO TALLER
     who-we-help/disability-and-care.html
                          Marralomeda      283.7 vs 148.1  ->  +135.6  VIDEO TALLER
     who-we-help/professional-teams-and-firms.html
                          Novo Group       283.7 vs 123.1  ->  +160.6  VIDEO TALLER

   Every one of those four "VIDEO TALLER" rows is a column of dead space beside
   a short blurb — exactly what rule 3.4 was written to prevent. The three
   pending blocks carry one short sentence each, so their text columns are
   123-148px against a 283.7px video, and no arrangement of the existing words
   closes that gap.

   IT IS NOT BEING FIXED, AND THE REASON IS AN OPERATOR DECISION THAT OUTRANKS
   THE RULE. On 2026-09-09 the operator asked, explicitly, for "a 1920 by 1080
   aspect ratioed preview". A 16:9 slot in a half-width column at 1440px is
   283.7px tall and there is no second variable: honour the ratio and the height
   follows. The only ways to satisfy 3.4 as literally written are to cap the
   height (which is what threw the ratio away and produced the 2.14:1 preview
   the operator objected to) or to write more copy for three clients who have
   not supplied a video yet, which would be inventing words. So the ratio wins,
   the rule is broken on those three pages, and it is broken in writing here
   rather than quietly.

   THE HOME AND STORIES EQUITAS BLOCKS FLIPPED THE OTHER WAY ON 2026-09-09, when
   the two approved client quotes were added under the blurb. Their text columns
   went from 306.7px to 674.4px, so the video is now the shorter by 390.7px and
   the empty column is on the video's side. 3.4 only forbids the MEDIA forcing
   the row, so those two blocks satisfy it — but 390.7px of empty column is
   worth an operator's eye, and it is recorded here for that reason and not
   hidden. Moving .speakers out of .col-text to full width below the row is the
   change that would level them; it was NOT made, because the brief for this
   work said to extend the speaker block in place and keep the CTA where it is.

   WHAT WOULD CHANGE THIS. Real footage does not: a <video> here is 16:9 too.
   Copy for the three pending clients would, and so would an operator decision
   to let the pending blocks use a different slot shape from the shipped ones.
   Until one of those happens, do not "fix" this by capping the height again —
   that is the deleted rule above, and it comes back as a 2.14:1 preview.

   align-self:center went with it. It centred the video against the text column,
   which directly contradicts the top-edge alignment the operator asked for; the
   grid's own align-items:start now does the right thing for both children. */

/* ═══════════════════════════════════════════════════════════════════════
   Blog: same design language as the rest of the site, not a text dump.
   The article keeps a reading measure for prose, but designed elements
   (cards, figures) break out to the full column so the page uses the width.
   ═══════════════════════════════════════════════════════════════════════ */
.article{max-width:none}
.article > p,.article > h2,.article > ul,.article > blockquote,.article .stand{max-width:none}
.article .stand{font-size:18.5px}

/* landscape figure that breaks the opening wall of text */
.artfig{margin:26px 0 30px;max-width:none}
.artfig-ph{aspect-ratio:3/1;border-radius:var(--radius);border:2px dashed #c9ced8;
  background:linear-gradient(135deg,rgba(230,0,126,.05),rgba(9,20,100,.05)),
    repeating-linear-gradient(135deg,#fafbfd 0 14px,#f2f4f8 14px 28px);
  display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px}
.artfig-ph .lbl{font:700 11px var(--font-body);letter-spacing:1.6px;text-transform:uppercase;
  color:var(--ink-faint)}
.artfig-ph .dim{font-size:13px;color:var(--text-gray)}
.artfig figcaption{font-size:13.5px;color:var(--text-gray);margin-top:10px}

/* numbered runs become cards, evenly sized */
.ncards{display:grid;grid-template-columns:repeat(auto-fit,minmax(290px,1fr));gap:14px;
  margin:22px 0 30px;align-items:stretch}
.ncard{counter-increment:none;background:var(--canvas);border:1px solid var(--border-gray);
  border-radius:var(--radius);padding:20px;display:grid;grid-template-columns:32px 1fr;
  gap:14px;align-content:start}
.ncard .nnum{width:30px;height:30px;border-radius:9px;background:var(--dark-blue);color:var(--on-dark);
  font:700 16px var(--font-display);display:flex;align-items:center;justify-content:center}
.ncard div{font-size:15px;line-height:1.5;color:var(--text-gray)}
.ncard strong{display:block;color:var(--ink-h);margin-bottom:4px;font-size:15.5px}

/* consecutive bold-lead paragraphs become a card row */
.acards{display:grid;grid-template-columns:repeat(auto-fit,minmax(290px,1fr));gap:14px;
  margin:22px 0 30px;align-items:stretch}
.acard{background:var(--canvas);border:1px solid var(--border-gray);border-radius:var(--radius);
  padding:20px 22px;font-size:15px;line-height:1.5;color:var(--text-gray);
  display:flex;flex-direction:column;justify-content:flex-start}
.acard strong{display:block;color:var(--ink-h);margin-bottom:5px;font-size:15.5px}

/* Rule 4.3 fix, 2026-09-11: build.py used to write the column count straight
   into an inline style, e.g. style="grid-template-columns:repeat(3,1fr)".
   An inline style beats every rule in this file regardless of specificity or
   @media, so that fixed count applied at EVERY width, including a 375px
   phone — three narrow, tall-and-skinny columns instead of the auto-fit
   fallback above collapsing to one. build.py now writes the count as the
   --cols custom property instead, which carries no specificity of its own,
   so the auto-fit rule above still wins below this breakpoint and only
   yields to a fixed `cols`-column grid once there is room for it. 641px
   pairs with this file's other mobile cutoff at 640px (.postgrid, .nstack
   .nrow, and the @media (min-width:641px) block below for .acard/.ncard
   text-align). --cols always arrives from build.py; the ,3 fallback only
   protects a card row rendered with no inline style at all. */
@media (min-width:641px){
  .ncards,.acards{grid-template-columns:repeat(var(--cols,3),1fr)}
}

/* ── comparison pair (Holidays Act vs Employment Leave Act) ─────────── */
.compare{display:grid;grid-template-columns:1fr 1fr;gap:16px;align-items:stretch;margin-top:6px}
.cmp{background:var(--canvas);border:1px solid var(--border-gray);border-radius:var(--radius);
  padding:22px 24px;display:flex;flex-direction:column}
.cmp:last-child{border-color:rgba(230,0,126,.35)}
.cmp-label{font:700 11px var(--font-body);letter-spacing:1.6px;text-transform:uppercase;
  color:var(--text-gray)}
.cmp:last-child .cmp-label{color:var(--action-press)}
.cmp h3{margin:8px 0 14px}
.cmp ul{list-style:none;display:grid;gap:10px;margin:0}
.cmp li{font-size:15px;line-height:1.5;color:var(--text-gray);padding-left:20px;position:relative}
.cmp li::before{content:"";position:absolute;left:0;top:8px;width:8px;height:8px;border-radius:3px;
  background:var(--border-gray)}
.cmp:last-child li::before{background:var(--tempus-pink)}
.cmp-foot{margin-top:16px;color:var(--text-gray);font-size:15.5px;max-width:none}

/* ── price table ───────────────────────────────────────────────────── */
.ptable{display:grid;gap:10px;margin-top:8px;grid-auto-rows:1fr}   /* equal row heights */
.prow{display:grid;grid-template-columns:1.1fr .8fr 1.6fr;gap:20px;align-items:center;
  background:var(--canvas);border:1px solid var(--border-gray);border-radius:var(--radius);
  padding:16px 22px}
.pname{font-family:var(--font-display);font-weight:600;font-size:17px;color:var(--ink-h)}
.pamt{display:grid;grid-template-columns:14px auto 1fr;align-items:baseline;gap:0 8px;
  font-variant-numeric:tabular-nums;font-feature-settings:"tnum" 1,"lnum" 1}
.pamt .psign{width:14px;text-align:left;font-weight:700;color:var(--text-gray)}
.pamt .punit{color:var(--text-gray);font-size:14px}
.pamt b{font-family:var(--font-display);font-weight:700;font-size:26px;
  color:var(--tempus-pink);line-height:1}
.pamt span{font-size:13px;color:var(--text-gray)}
.pdesc{font-size:15px;color:var(--text-gray)}
.pnote{margin-top:14px;font-size:15px;color:var(--text-gray);max-width:none}

@media (max-width:900px){
  .compare{grid-template-columns:1fr}
  .prow{grid-template-columns:1fr;gap:8px}
}

/* A card set whose copy is uneven stacks full width instead of sitting in a
   ragged row. Applies where the words are fixed — reviewed blog copy — so the
   layout yields rather than the writing being reshaped to fit a grid. */

/* ── article opening: prose left, landscape image right ───────────────
   The image sits beside the opening rather than under it, so the first
   screen of a post is not a wall of text. The prose column carries enough
   copy to stand level with the image — see UI-STANDARDS 8b.3. */
.artintro{display:grid;grid-template-columns:1.05fr .95fr;gap:clamp(24px,3vw,42px);
  align-items:start;margin:18px 0 30px}
.artintro-text p{max-width:none;margin-bottom:1em}
.artintro-text p:last-child{margin-bottom:0}
.artintro .artfig{margin:5px 0 0}
.artintro .artfig-ph{aspect-ratio:3/2}   /* sized to a realistic opening, not the reverse */
@media (max-width:860px){
  .artintro{grid-template-columns:1fr;gap:20px}
  .artintro .artfig-ph{aspect-ratio:3/1}
}

/* ── Closing statement (rule 8b.6) ────────────────────────────────────────
   The last line of a post is the one readers carry out with them. It is set
   apart and weighted so it does not read as one more body paragraph. */
.article p.close{margin:30px 0 0;padding-top:20px;border-top:2px solid var(--tempus-pink);
  font-weight:700;font-size:18.5px;color:var(--ink-h);max-width:none}

/* Comparison cards used inside an article body */
.article .compare{margin:22px 0 30px}
.article .compare .cmp{padding:24px 26px}
.article .compare .cmp h3{margin:0 0 8px;font-size:18px}
.article .compare .cmp p{margin:0;color:var(--text-gray);font-size:15.5px;max-width:none}

/* Verbatim quotes are the one exemption to card-copy parity, so the layout
   carries the variance instead: equal heights, attribution pinned to the
   bottom edge, no ragged foot. */
.quotes .quote{justify-content:flex-start}
.quotes .quote .who{margin-top:auto;padding-top:14px}

/* ── Colour cue on three-across rows (from the printed fact sheets) ───────
   A thick rule across the top of each card, pink · pink · navy left to right.
   The cue means "this is a row of three": a pair or a 2x2 is left plain, so
   the signal keeps its meaning. Print sets these square; here they follow the
   card's own top radius. */
.t-pink,.t-navy{position:relative;overflow:hidden}
/* The colour band is drawn by the .cardtop ELEMENT (below), not by a pseudo-element.
   Both were briefly live at once and every card rendered two stacked 6px bands.
   .cardtop wins because the hover doubling animates a real element's height. */
/* Room above the title for the colour band. Split into two rules on
   2026-09-08 and it has to stay split: .card and .tile now hold their top
   padding in --pad-top so that .cardimg can cancel it, and setting
   padding-top directly on those two would leave --pad-top saying 30px while
   the card actually padded 24px — the picture would then hang two rows of
   pixels over the card's top edge. The other four components have no such
   variable and take the padding straight, exactly as before. */
.card.t-pink,.card.t-navy,.tile.t-pink,.tile.t-navy{--pad-top:24px}
.blk.t-pink,.blk.t-navy,.step.t-pink,.step.t-navy,
.acard.t-pink,.acard.t-navy,.ncard.t-pink,.ncard.t-navy{padding-top:24px}

/* ── Numbered stack (rule 4.3b) ───────────────────────────────────────────
   Where a set does not divide by three, it stacks as numbered rows instead of
   leaving a short final row. Pink numerals, navy lead-in, hairline dividers —
   the 01–05 pattern from the collateral, in a rounded container. */
.nstack{border:1px solid var(--border-gray);border-radius:var(--radius);
  background:var(--canvas);margin:22px 0 30px;overflow:hidden}
.nstack::before{content:"";display:block;height:4px;background:var(--dark-blue)}
.nstack .nrow{display:grid;grid-template-columns:64px 1fr;gap:6px;
  align-items:baseline;padding:16px 24px;border-top:1px solid var(--border-gray)}
.nstack .nrow:first-of-type{border-top:0}
.nstack .nnum{font:700 17px var(--font-display);color:var(--action-press);
  letter-spacing:.02em;font-variant-numeric:tabular-nums}
.nstack .nrow > div{font-size:15.5px;line-height:1.55;color:var(--text-gray)}
.nstack .nrow strong{color:var(--ink-h)}
@media (max-width:640px){
  .nstack .nrow{grid-template-columns:1fr;gap:2px;padding:14px 18px}
}

/* ── Mobile-only nstack look for 5 flagged rows (operator, 2026-09-11) ────
   Five specific .ncards/.acards rows — flagged by build.py with the
   .m-nstack class, see MOBILE_NUMBERED_SECTIONS — read as this same
   pink-numeral, shared-top-bar stack on a phone, without becoming a real
   .nstack: desktop keeps the badged grid card these rows already render as
   everywhere else, untouched. Nothing here matches a row without the
   .m-nstack marker, so every other .ncards/.acards on the site — including
   the two rows the operator asked to leave as plain cards — renders exactly
   as before at every width. Structure mirrors .nstack above: overflow:hidden
   plus the container's own border-radius rounds the first/last row for free,
   so the rows themselves stay square-cornered like .nstack .nrow does.

   .nnum-m defaults to hidden here, same base-hide-then-flip shape as
   .askai-full/.askai-short below: a bare <span> is display:inline by
   default, so without this line the zero-padded "01" it carries would sit
   as plain unstyled text on desktop too, next to (ncards) or in front of
   (acards) the card content. The @media block right after this is the only
   place that turns it back on, and only inside .m-nstack. */
.nnum-m{display:none}
@media (max-width:640px){
  .ncards.m-nstack,.acards.m-nstack{
    display:block;border:1px solid var(--border-gray);border-radius:var(--radius);
    background:var(--canvas);overflow:hidden}
  .ncards.m-nstack::before,.acards.m-nstack::before{
    content:"";display:block;height:4px;background:var(--dark-blue)}
  .ncards.m-nstack .ncard,.acards.m-nstack .acard{
    display:block;background:transparent;border:0;border-radius:0;
    padding:14px 18px;border-top:1px solid var(--border-gray)}
  .ncards.m-nstack .ncard:first-of-type,.acards.m-nstack .acard:first-of-type{
    border-top:0}
  .ncards.m-nstack .cardtop,.acards.m-nstack .cardtop{display:none}
  /* The desktop badge (.nnum) hides; its zero-padded twin (.nnum-m, added by
     build.py only for these rows) takes over, styled like .nstack .nnum. */
  .ncards.m-nstack .ncard .nnum{display:none}
  .ncards.m-nstack .nnum-m,.acards.m-nstack .nnum-m{
    display:block;margin-bottom:2px;font:700 17px var(--font-display);
    color:var(--action-press);letter-spacing:.02em;font-variant-numeric:tabular-nums}
  .ncards.m-nstack .ncard>div,.acards.m-nstack .acard{
    font-size:15.5px;line-height:1.55;color:var(--text-gray)}
}

/* ── Uncoloured sets get their cue on hover instead ──────────────────────
   A 2x2 or a pair carries no top rule, so on a pointer device the numbered
   badge shifts navy → pink. Nothing depends on it; it is a nicety. */
@media (hover:hover){
  .ncards .ncard:not(.t-pink):not(.t-navy):hover .nnum{background:var(--tempus-pink)}
  .ncards .ncard .nnum{transition:background .18s ease}
  .nstack .nrow:hover .nnum{color:var(--dark-blue);transition:color .18s ease}
}

/* ── Pay-cadence diagram (/how-it-works, "A normal pay run with Tempus") ──
   A horizontal 4-step icon bar: timesheets in -> we process it -> approval
   out -> once approved. Each step is its OWN separated card (matching the
   3-card grid immediately above it in the same section), not one fused panel
   with a faint internal divider — that read as a single undivided block
   rather than a sequence. The arrow between cards is a real grid cell of its
   own (not an absolutely-positioned glyph pinned to a guessed pixel offset),
   so it stays vertically centered on the row no matter how tall the cards
   are — which matters here since the four bodies are different lengths.
   Built by _paycadence() in build.py; sec_strengths() renders it only when a
   section sets "diagram", so every other .strengths section (home's "What
   you actually get") is untouched. */
.pc-intro{margin:36px 0 20px;color:var(--text-gray)}
.paycadence{display:grid;grid-template-columns:1fr auto 1fr auto 1fr auto 1fr;
  align-items:stretch;margin:0 0 8px}
.pc-step{border:1px solid var(--border-gray);border-radius:var(--radius);
  background:var(--canvas);padding:32px 22px;text-align:center;
  display:flex;flex-direction:column;align-items:center;gap:10px}
.pc-arrow{display:flex;align-items:center;justify-content:center;
  padding:0 8px;color:var(--action-press)}
.pc-ico{color:var(--dark-blue)}
.pc-step strong{font:700 16px var(--font-display);color:var(--ink-h)}
.pc-step p{margin:0;font-size:14px;line-height:1.55;color:var(--text-gray)}
@media (max-width:640px){
  .paycadence{grid-template-columns:1fr}
  .pc-arrow{transform:rotate(90deg);padding:6px 0}
}

/* Comparison tiles inside an article are a symmetrical pair, so both carry the
   same navy top rule and the same border. The pink last-child accent is
   reserved for the site's before/after comparison, where it means something. */
.article .compare .cmp{border-color:var(--border-gray);position:relative;
  overflow:hidden;padding-top:26px}
.article .compare .cmp::before{content:"";position:absolute;top:0;left:0;right:0;
  height:4px;background:var(--dark-blue)}


/* ═══════════════════════════════════════════════════════════════════════
   OPERATOR DESIGN PASS — cards
   Four decisions live here, kept together because they all describe the
   same object: square corners, justified copy, no entrance animation, and
   a colour band that grows on hover. Placed at the end of the file so the
   cascade order is obvious rather than a specificity puzzle.
   ═══════════════════════════════════════════════════════════════════════ */

/* ── Rule 4: cards do not animate in ──────────────────────────────────────
   .reveal fades and lifts an element as it scrolls into view. On cards that
   read as the page still loading, so cards are exempt: full opacity, no
   translate, visible at rest. .reveal is left intact for everything else
   that uses it — figures, video blocks, price rows, the numbered stack.

   The second and third rules are not redundant. .reveal sets the `transition`
   SHORTHAND, which overwrites whatever transition the card declared for its
   own hover; without restoring it, exempting a card from the animation would
   silently take its hover easing with it. */
.card.reveal,.postcard.reveal,.tile.reveal,.blk.reveal,.acard.reveal,.ncard.reveal,
.cmp.reveal,.quote.reveal,.step.reveal{opacity:1;transform:none}
.card.reveal,.postcard.reveal{
  transition:transform .18s ease,border-color .18s ease}
/* border-color added here 2026-09-08 in step with a.tile:hover above. This is
   the DUPLICATE transition list this block's own comment warns about: .reveal
   sets the transition SHORTHAND, so whatever is missing from this line is
   silently dropped from the hover. No linked tile carries .reveal today, but
   the moment one does, omitting border-color would snap the tile's only
   reduced-motion hover cue instead of easing it. Keep in step with .card. */
a.tile.reveal{transition:transform .18s ease,border-color .18s ease}
.tile.reveal:not(a),.blk.reveal,.acard.reveal,.ncard.reveal,.cmp.reveal,
.quote.reveal,.step.reveal{transition:none}

/* ── Rule 3: card copy is justified; card titles are not ──────────────────
   Justified only from 641px up, and only the body paragraph. Below that a
   card is the full width of a phone MINUS its padding, which is the
   narrowest measure on the site — justifying a ~30-character line opens
   white rivers down the middle of the paragraph, so the phone keeps ragged-right.

   .card h3 was justified too until 2026-09-10 (operator report): a short
   title looks left-aligned regardless, but a longer one wraps to two lines
   in a card-width column and justify stretches the word spacing on that
   first line into something that reads as a mistake. .postcard/.tile
   headings are a different component and weren't reported, so they keep
   the old behaviour.

   hyphens:auto is the partner to justify, not an extra: three cards across a
   1180px page gives roughly a 40-character line, and justifying that without
   letting long words break is what produces the stretched word spacing people
   blame on justification itself. The headings that stay justified are
   deliberately left unhyphenated — a broken word in a title reads as a typo. */
@media (min-width:641px){
  .card p,
  .postcard p,.postcard .excerpt,
  .tile h3,.tile p,
  .blocks .blk p,
  .acard,.ncard > div{text-align:justify}
  .card p,.postcard p,.postcard .excerpt,.tile p,.blocks .blk p,.acard,.ncard > div{
    hyphens:auto;-webkit-hyphens:auto}
  .postcard h3,.tile h3{hyphens:manual;-webkit-hyphens:manual}
}

/* ── Rule 5: the colour band, and what it does on hover ───────────────────
   The band is an absolutely positioned strip pinned to the card's top edge.
   Absolute, not a flex child, for one reason: growing a strip that is IN the
   flow would push every line of the card down and change the card's height,
   so hovering one card in a row would make its neighbours look misaligned.
   Out of flow, the band grows into the white space the card's top padding
   already reserves, and nothing else moves.

   The card reserves at least 24px above its title (padding-top on a tinted
   card, 30px on a plain one) and the title adds 12px of margin on top of
   that. The band tops out at 12px, so the doubled band still clears the
   title by 24px and can never cover it. */
.cardtop{position:absolute;top:0;left:0;right:0;height:var(--band-h);
  /* Transparent by default. build.py emits .cardtop on all ~106 card-family
     elements, but before it existed only .t-pink / .t-navy cards showed a band -
     defaulting this to navy would put one on every previously-plain card. */
  background:transparent;pointer-events:none;border-radius:0}
.card.t-pink > .cardtop,.cardtop.t-pink,.cardtop.pink{background:var(--tempus-pink)}
.card.t-navy > .cardtop,.cardtop.t-navy,.cardtop.navy{background:var(--dark-blue)}

/* The band also exists as a ::before on .t-pink / .t-navy, which is how every
   card on the site draws it today. Both forms are given the same height and
   the same hover behaviour, so the markup can carry either — or move from one
   to the other — without the hover quietly stopping working. */
.t-pink::before,.t-navy::before{height:var(--band-h)}

@media (min-width:961px) and (hover:hover) and (pointer:fine){
  .cardtop,
  .card.t-pink::before,.card.t-navy::before{
    transition:height .24s cubic-bezier(.4,0,.2,1)}
  .card:hover .cardtop,
  .card.t-pink:hover::before,.card.t-navy:hover::before{height:var(--band-h-hover)}
}

/* Reduced motion: the band still doubles — that is information about which
   card the pointer is on — it simply arrives instantly instead of easing.
   The card's own growth is stilled here too, for the same reason.

   THIS IS WHY THE BORDER DARKENING IS NOT DECORATION. transform:none below
   removes the ONLY size cue a reduced-motion visitor gets, so --hairline-hover
   is the whole hover state for them. It survived the 2026-09-08 shadow cull and
   the 2026-09-09 lift-to-grow change untouched, and it has to keep surviving:
   drop it and these visitors get no hover feedback at all on a linked tile,
   which sets text-decoration:none and so has no underline either. */
@media (prefers-reduced-motion:reduce){
  .cardtop,.t-pink::before,.t-navy::before{transition:none}
  .card,.card.reveal,.postcard,.postcard.reveal,a.tile,a.tile.reveal{transition:none}
  .card:hover,.postcard:hover,a.tile:hover{transform:none}
  .postcard .readmore::after{transition:none}
  .postcard:hover .readmore::after{transform:none}
}


/* ═══════════════════════════════════════════════════════════════════════
   ASK AI — the row of "open this page in an AI assistant" buttons.
   Sits immediately under the title on the Insights index and on every
   article, desktop and mobile.

   RANK: these are spec §3 SECONDARY pink — same brand pink, so they still
   read as an affirmative action, but small, 600 weight and shadowless. They
   are repeatable in-page actions that appear on 6 pages — the Insights index
   and the five articles, counted off the built pages 2026-09-09, where this
   used to claim 15; if they carried
   the CTA's size, weight and shadow they would out-shout the one thing each
   page actually exists to make you do. Do not promote them.

   LABELS: the buttons carry two labels and CSS picks one. Desktop shows
   .askai-full ("Ask ChatGPT about this article"), mobile shows .askai-short
   ("ChatGPT"). Both are always in the DOM, so a screen reader and the page
   source always see a complete label; only the painted one changes.
   ═══════════════════════════════════════════════════════════════════════ */
.askai{margin:16px 0 28px;max-width:none}
/* Mobile-only, 2026-09-11: see the full note further down, at the rule
   `.article > .askai:first-child`, for why THAT rule — not this one — is
   what actually moves on screen for the one .askai the site renders today.
   Changed here too so this rule stays honest for any future .askai that
   follows something, rather than quietly drifting from what :first-child says. */
@media (max-width:767.98px){
  .askai{margin-top:13px}
}
.askai-lead{margin:0 0 10px;font-size:14.5px;line-height:1.5;color:var(--text-gray);
  max-width:none}

/* Below 768px: a 2x2 grid. A wrapping flex row on a phone gives an uneven
   last line — three buttons up top and one stranded underneath — which reads
   as a layout bug rather than a set. A 2x2 is deliberate and even. */
.askai-row{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}

/* WHITE, NOT PINK, AND THE FILL IS EXPLICIT ON PURPOSE. These buttons carry the
   providers' own marks, which are full-colour — OpenAI's near-black, Gemini's
   four-stop gradient, Copilot's six, Claude's #D97757. Pink put four loud logos
   on a louder ground and none of them could be read. The operator settled it on
   2026-09-09 against his own help centre, support.payrollbytempus.com, which
   ships the same row: a light button, a hairline, dark text, colour icon.

   The fill is var(--canvas) rather than `transparent` — the difference matters.
   THE FILL IS var(--canvas), NOT `transparent`, AND THAT IS NOW A STANDING
   GUARD. It was written when the row rendered on two grounds — white in a post
   body, and navy in the /insights hero, where askai() was injected after the
   h1. The operator removed the row from the index on 2026-09-09, so today every
   occurrence sits on white and the two values render identically. That makes
   this MORE worth keeping, not less: nothing visible defends it any more, so a
   later "simplify to transparent" looks free right up until the row is put back
   on a dark ground and four colour marks land on navy. AAI-015 pins it.

   The hairline and its hover are the literals rgba(0,0,0,.1) and
   rgba(0,0,0,.28) — the same two values .btn-outline/.btn-ghost already write
   at :522-524. There is NO token for either; this comment used to name a
   var(--btn-outline) that has never existed in this file, corrected 2026-09-09.
   The help centre independently landed on the same border, so this is the
   house value and the reference value at once.

   NO BOX-SHADOW, AND THAT IS THE 2026-09-08 DECISION BEING HONOURED, NOT
   OVERLOOKED. This rule shipped with box-shadow:0 1px 2px rgba(0,0,0,.05) —
   the exact value culled from .btn-outline/.btn-ghost on 2026-09-08 for being
   invisible on white and actively wrong on navy. The opaque white fill is a
   real difference from that case, so it was re-measured rather than assumed:
   at 2px blur offset 1px down, the darkest pixel outside the border box is
   about 3.5% black, 9 of 255 levels on white, falling under 4 levels within
   1.5px — and it sits directly beneath a 1.5px hairline that is 25 levels
   dark, which swamps it. On the navy of the /insights hero the same shadow is
   3.5 levels on the blue channel alone, i.e. nothing, which is the original
   objection unchanged and this button renders there half the time. It did not
   earn its place on either surface, so it is gone rather than left to
   contradict the note at :518. The pill radius and the 32px small-control
   height are NOT taken from the reference (it uses a 6px radius and 38px): the
   operator was explicit that roundness follows this project's design language. */
.askai-btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;
  font-family:var(--font-body);font-weight:600;font-size:12px;line-height:16px;
  min-height:var(--control-h-sm);padding:6px 12px;border-radius:var(--pill);
  border:1.5px solid rgba(0,0,0,.1);background:var(--canvas);color:var(--text-dark);
  text-decoration:none;text-align:center;cursor:pointer;
  transition:background-color .15s ease,border-color .15s ease}
.askai-btn:hover{background:var(--parchment);border-color:rgba(0,0,0,.28)}
.askai-btn[disabled],.askai-btn:disabled{opacity:.5;pointer-events:none}
.askai-btn svg,.askai-btn img,.askai-btn .ico{width:16px;height:16px;flex:none}

/* One label visible at a time. Mobile default is the short one. */
.askai-full{display:none}
.askai-short{display:inline}

/* >=768px: one wrapping flex row, full labels. nowrap only here — in the 2x2
   grid above the cell is ~160px on a 375px phone and the label must be free
   to wrap rather than overflow its own button. */
@media (min-width:768px){
  .askai-row{display:flex;flex-wrap:wrap;gap:10px}
  .askai-btn{white-space:nowrap}
  .askai-full{display:inline}
  .askai-short{display:none}
}

/* ── the prompt modal ──────────────────────────────────────────────────
   Centred dialog from 768px (8px, the spec's overlay tier); a bottom sheet
   below it with SQUARE top corners — the spec squares modals below 640px and
   the operator wants square everywhere, so the two agree and square wins.

   .askai-modal is styled display:flex, on the assumption the JS creates and
   appends the element when the dialog opens. The [hidden] rule is there so a
   markup-already-present, toggle-the-attribute implementation also works
   without the dialog being stuck open — display:flex would otherwise beat
   [hidden]'s display:none.
   ─────────────────────────────────────────────────────────────────────── */
/* NOTE: an earlier draft of these styles modelled .askai-modal as the OVERLAY
   (position:fixed, flex, centred) with .askai-panel as the dialog inside it.
   askai.js does the opposite: .askai-backdrop is the overlay and .askai-modal
   IS the dialog. With both live the dialog inherited position:fixed, dropped
   out of the backdrop's flex flow, and rendered jammed against the top-left
   corner. The JS builds the DOM, so its contract wins - the overlay/panel
   rules were removed and the live rules are further down this file. */


/* ═══════════════════════════════════════════════════════════════════════
   ARTICLE LEDE — the first paragraph and the lede image, side by side.

   Text left, media right, both aligned to the TOP so the paragraph's first
   line and the top edge of the image start on the same baseline. Below
   861px they stack in DOM order, which puts the image immediately after the
   first paragraph — the reader gets one paragraph, then the picture, then
   the rest of the piece. 861px is the same breakpoint the existing .artintro
   uses, so an article never has two different opening layouts.

   4:3 below 861px (mobile/tablet, one column): a fixed slot reserves its
   footprint so a slow-loading photo never reflows the page. It is
   deliberately not the 16:10 thumbnail: the index card wants a wide strip
   above a block of text, the article wants a taller picture standing beside
   a paragraph, and one crop cannot be both without cutting the top off one
   of them. (CHANGED 2026-09-08 from 3:2, which was a third shape nobody had
   settled — and _source/build.py's ARTLEDE_RATIO said 16:9 at the same time,
   so the site had two written-down answers and neither was right. They now
   agree, and they must move together.)

   Above 861px this fixed ratio is OVERRIDDEN further down this file (search
   "text-shorter-than-image"): all five Insights posts now carry a real
   photograph here, and a fixed aspect-ratio made the image height a function
   of column width alone, with no relationship to how much lede text a given
   post happens to open with. Short posts ended up with the text column
   visibly shorter than the photo beside it. Desktop instead stretches the
   image to match the text column's own height, whatever that is, and lets
   the object-fit:cover below crop the photo to fit — so "shorter than the
   text" can't recur no matter how long or short a future post's opening
   paragraphs are.
   ═══════════════════════════════════════════════════════════════════════ */
.artlede{display:grid;grid-template-columns:minmax(0,1fr);gap:20px;align-items:start;
  margin:18px 0 30px;max-width:none}
.artlede > p,.artlede-text p{max-width:none;margin:0}
.artlede-text p + p{margin-top:1em}

.artlede-media{position:relative;margin:0;min-width:0;width:100%;aspect-ratio:4/3;
  overflow:hidden;border-radius:0;background:var(--light-gray)}
.artlede-media img,.artlede-media picture,.artlede-media video{
  width:100%;height:100%;object-fit:cover;display:block}
/* The pending-media placeholder fills the slot instead of imposing its own
   16:9 — the slot owns the shape, the placeholder just occupies it. */
.artlede-media .ph-video{aspect-ratio:auto;width:100%;height:100%;border-radius:0;
  padding:24px}
/* A caption belongs under the slot, outside it, or the aspect ratio eats it. */
.artlede-cap{margin-top:10px;font-size:13.5px;color:var(--text-gray);max-width:none}

@media (min-width:861px){
  .artlede{grid-template-columns:minmax(0,1.05fr) minmax(0,.95fr);
    gap:clamp(24px,3vw,42px)}
}

/* ── Ask an AI: the modal ────────────────────────────────────────────────
   askai.js builds this DOM and sets classes only, never inline styles. Two
   rules it asks of this stylesheet, both load-bearing:
     1. .askai-backdrop carries [hidden] while closed, so every `display:`
        below is scoped to .is-open. A bare .askai-backdrop{display:flex}
        beats [hidden] and pins the modal permanently on screen.
     2. The 768px breakpoint lives HERE and nowhere else - the JS never
        measures the viewport, so this is the single source of truth for
        centred-dialog vs bottom-sheet. */
.askai-backdrop{position:fixed;inset:0;z-index:1000;background:rgba(9,20,100,.55);
  align-items:center;justify-content:center;padding:24px}
.askai-backdrop.is-open{display:flex}
html.askai-locked{overflow:hidden}

.askai-modal{background:var(--canvas);color:var(--text-dark);width:100%;max-width:560px;
  max-height:calc(100vh - 48px);overflow-y:auto;border-radius:var(--r-overlay);
  box-shadow:0 18px 48px -12px rgba(9,20,100,.35);
  font-family:var(--font-body);padding:22px 24px 20px}
.askai-modal-head{display:flex;align-items:flex-start;justify-content:space-between;gap:16px}
/* 600, matching h1-h4. askai.js creates this as an <h2>, so it already matched
   the base heading rule - but its own font-weight:700 won, which would have
   left it as the only bold heading on the site once headings moved to 600. */
.askai-modal-title{font-family:var(--font-display);font-size:20px;font-weight:600;margin:0}
.askai-modal-close{flex:0 0 auto;width:32px;height:32px;display:inline-flex;
  align-items:center;justify-content:center;border:1px solid rgba(0,0,0,.1);
  background:transparent;border-radius:var(--r-control);cursor:pointer;font-size:18px;line-height:1;
  color:var(--text-gray)}
.askai-modal-close:hover{background:#EBECEF}

.askai-modal-cols{display:grid;grid-template-columns:1fr auto;gap:20px;
  align-items:start;margin-top:16px}
.askai-status-head{font-weight:600;margin:0 0 4px}
.askai-status-note{margin:0;font-size:14px;color:var(--text-gray)}
.askai-tick{display:inline-flex;align-items:center;justify-content:center;
  width:28px;height:28px;border-radius:var(--pill);background:#E9F5EF;color:#0F6E56;
  font-size:15px;margin-bottom:8px}
.askai-prompt-box{width:100%;margin-top:10px;min-height:104px;resize:vertical;
  font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12.5px;
  line-height:1.5;padding:10px;border:1px solid var(--form-border);
  border-radius:var(--r-control);background:#FAFBFD;color:var(--text-dark)}

.askai-service{text-align:center;min-width:118px}
.askai-service-name{display:block;font-weight:600;margin-bottom:6px}
.askai-open-link{display:inline-flex;align-items:center;gap:6px;
  color:var(--brand-blue);font-weight:600;font-size:14px;text-decoration:none}
.askai-open-link:hover{text-decoration:underline}
.askai-open-arrow{font-size:12px}

.askai-disclaimer{margin:16px 0 0;padding-top:14px;border-top:1px solid var(--border-gray);
  font-size:12.5px;line-height:1.5;color:var(--text-gray)}

/* Bottom sheet below the breakpoint. Square top corners: the spec makes
   modals square-cornered below 640px, and the operator wants square. */
@media (max-width:767.98px){
  .askai-backdrop{padding:0;align-items:flex-end}
  .askai-modal{max-width:none;border-radius:0;max-height:88vh;padding:20px 18px 18px}
  .askai-modal-cols{grid-template-columns:1fr;gap:16px}
  .askai-service{text-align:left;min-width:0}
}
@media (prefers-reduced-motion:reduce){
  .askai-backdrop,.askai-modal{transition:none}
}

/* ── Mobile footer: compact CTA grid ─────────────────────────────────────
   The desktop column list is far too tall on a phone - it filled an entire
   screen edge to edge. Below 720px the columns are hidden and this takes over.

   STRUCTURE, and why it changed. This used to be one flat 2-column grid of
   six hand-picked buttons. It is now a GROUPED stack that mirrors the desktop
   columns one-for-one, because build.py derives both from the same
   C.FOOTER_COLS list:
       .fmgrid            the vertical stack
         .fmrow.fmcta     Call Us / Email Us — the only row with no desktop twin
         .fmgroup         one per desktop column
           h4             the same heading the desktop column uses
           .fmrow         that column's links, two across
   So .fmrow is now the 2-column grid .fmgrid itself used to be, and .fmgrid
   is the thing that stacks the groups. If a link is missing here it is
   missing from the desktop columns too — add it to FOOTER_COLS in
   _source/content.py, never by hand to one side.
   Hidden on desktop; it is the ONLY footer navigation below 720px. */
.fmgrid{display:none}
/* The two-across button grid. Lives on .fmrow rather than on .fmgrid so the
   group headings can sit at full width above their own buttons. */
.fmrow{display:grid;grid-template-columns:1fr 1fr;gap:8px}
/* The group heading is deliberately NOT restyled here. `.site-footer h4`
   (~line 498) already sets the family, 11.5px, the 1.6px tracking, the
   uppercase and the white — the same treatment the desktop column headings
   get — and letting it cascade is what makes the two genuinely mirror rather
   than merely look similar. Only the bottom margin is tightened, because a
   14px gap under a heading is right above a loose text list and too airy
   above a tight pair of buttons. If you find yourself copying the desktop
   heading's properties down here, stop: they will drift.
   4px, not the 9px this shipped with. Part of the FTR-006 height budget —
   see the @media (max-width:720px) block at the end of this section. The
   RATIO is what protects the grouping, not the absolute value: 8px of
   .fmgrid gap above the heading against 4px below it binds the heading to
   its own buttons rather than to the group before it. If you loosen one of
   those two numbers, loosen the other in step or the headings start reading
   as trailing labels on the wrong group. */
.site-footer .fmgroup h4{margin-bottom:4px}
/* Selector deliberately over-qualified. `.fmbtn` alone (0,1,0) LOSES to
   `.site-footer a` (0,1,1) at line ~500, which forces display:block and
   padding:5px 0 - so the buttons rendered as plain blocks with the label
   spilling out of the box and the icon stuck beside the text instead of at
   the far end. Keep it qualified. (It used to have a second job, clearing
   `.site-footer a[href^="tel:"]` off the Call Us button; that rule has since
   been deleted as inert — see the note where it used to live, above the
   .fbottom-row .blurb rule.) */
.site-footer .fmgrid a.fmbtn{display:flex;align-items:center;
  justify-content:space-between;gap:10px;
  background:#161d33;color:var(--on-dark);text-decoration:none;
  padding:6px 12px;border:1px solid #273049;border-radius:var(--pill);
  font-family:var(--font-body);font-size:14px;font-weight:400;line-height:1.2;
  /* min-height:44px is the FLOOR and is not negotiable — UI-STANDARDS rule 5.3,
     the same minimum the .navtoggle carries. Measured at 375px, EVERY button
     here is exactly 44px tall except the two whose label wraps to a second
     line. That is the whole reason the vertical padding could come down from
     11px to 6px: a one-line button is 16.8px of text + 12px padding + 2px
     border = 30.8px, so min-height, not padding, sets its height and the trim
     costs it literally nothing — it only shortens the two wrapped buttons,
     by 10px each. Horizontally it only goes 14px -> 12px, and no further:
     take that lower and MORE labels wrap, which adds the height straight
     back on.
     If this file ever needs height back, take it from gaps and padding —
     never from this line. */
  min-height:44px}
.site-footer .fmgrid a.fmbtn svg{flex:0 0 16px;opacity:.7}
.site-footer .fmgrid a.fmbtn:hover,
.site-footer .fmgrid a.fmbtn:active{background:#1d2542;border-color:#39456b;color:var(--on-dark)}
/* Call Us / Email Us are the two actions, not navigation, so they sit a shade
   brighter than the link buttons under them. Qualified one level further
   (0,4,1) so it beats the base .fmbtn rule (0,3,1) above — same trap as
   everywhere else in this block. */
.site-footer .fmgrid .fmcta a.fmbtn{background:#1d2542;border-color:#39456b;
  font-weight:600}

@media (max-width:720px){
  /* Swap the tall column list for the grouped button grid.
     This rule WAS `.site-footer .fgrid > div:not(.fbrand)`, which hid every
     column except the brand block. The brand block has been deleted, so that
     selector now matches EVERY column and the exclusion protects nothing —
     it just silently blanked the grid. The whole .fgrid goes instead, which
     is what was always meant: .fmgrid below is its mobile representation. */
  .site-footer .fgrid{display:none}

  /* ── Mobile footer height budget (test case FTR-006) ──────────────────
     FTR-006 caps the footer at 65% of a 375x812 viewport — 527.8px — because
     a footer taller than that is a screen the visitor scrolls past rather
     than reads. The grouped grid mirrors the desktop columns one-for-one by
     operator decision, so the CONTENT is fixed and cannot be traded away:
     13 buttons over 7 grid rows, each at the 44px touch floor, plus three
     headings and the two-line legal block.

     MEASURED, at 375x812, so the next person does not have to re-derive it:
       • 408.9px (50.4%) — the floor. Every gap and every padding in this
         block forced to zero, 44px buttons intact. That is the content.
       • 527.8px (65.0%) — the cap. So the ENTIRE whitespace budget for this
         footer, across footer padding, three group gaps, three heading
         margins, three row gaps, the grid-to-legal gap and the legal
         block's own padding, is 118.9px.
       • 587.1px (72.3%) — where this landed on the first pass, before the
         numbers below. It FAILED FTR-006.
       • 509.1px (62.7%) — what the numbers below now measure. 18.7px of
         headroom under the cap, which is the point of stopping here rather
         than at the 64.8% a slightly looser set also reaches: a footer that
         passes by 2px is one font-metric change away from failing again.
     Every value below is spent from that 118.9px. They are tight on purpose
     and they were measured, not chosen by eye. Do NOT "round them back up"
     to the desktop values — that is exactly the 72.3% failure above. If you
     need to add height, take it out of somewhere else in the same block and
     re-measure the footer against 527.8px. */
  .site-footer{padding:16px 0 12px}
  /* FINDING 5 — the orphaned divider is GONE, along with the space above it.
     `padding-top:16px` and `border-top:1px solid #1c2338` were drawn when
     .fgrid still rendered above this and there were two things to divide.
     .fgrid is display:none here, so the hairline had nothing above it: it sat
     74px down an otherwise empty stretch of footer, with the first button
     91px down, and read as a block that had failed to load. margin-top drops to 0 for the same reason — .site-footer's own
     padding-top is the space between the page and the first button now.
     If a visible element is ever put back above .fmgrid on mobile, the
     divider comes back WITH it, not before it.
     8px between groups against .fmrow's 4px between buttons is what separates
     one heading's set from the next without a rule between them — the 2:1
     ratio is doing that work, not the absolute size. */
  .fmgrid{display:grid;gap:8px;margin:0 0 8px;padding-top:0;border-top:0}
  /* 4px, down from 8px. Only the three groups that wrap to a second grid row
     pay this at all, so it is 12px off the total for almost no visible
     tightening — the 44px buttons still carry the rhythm. */
  .fmrow{gap:4px}
  /* The whole bottom row goes on a phone, because both halves of it are
     already answered above:
       • .fcontact is the same phone number and email address the .fmcta row
         offers as thumb-sized buttons. Two identical affordances 20px apart
         read as a bug, not as emphasis.
       • the blurb is the single biggest block of footer text on a phone and
         it repeats what the site has already said. It was hidden here before
         this rebuild too; that decision is unchanged, only relocated.
     Note that `.fbottom-row + .fbot` still matches — an adjacent-sibling
     selector does not care whether the sibling is painted — which is exactly
     the trap FINDING 11 caught; see the margin-top rule below. */
  .fbottom-row{display:none}
  /* justify-content:flex-start UNDOES the desktop centring for the phone, and
     is not a stylistic preference — it is what keeps this row aligned with the
     rest of the mobile footer. Everything above it here is the .fmgrid button
     grid, which is edge to edge and left-aligned, with left-aligned group
     headings. Centred legal lines under a left-aligned grid is the same
     two-systems-in-one-component fault the desktop change above just removed.
     This also pins the rendering that was already shipping: the old
     space-between put a single wrapped line at the start of its own flex line
     anyway, so both legal lines already sat at the grid's left edge (x=20 on a
     375px viewport). Written explicitly so the desktop rule cannot move them.
     gap:6px likewise beats the desktop 8px/26px — this rule is later in the
     file at the same specificity, which is the only reason it wins. */
  .fbot{padding-top:8px;font-size:12.5px;line-height:1.5;gap:6px;justify-content:flex-start}
  /* FINDING 11 — this margin USED to be written as part of the .fbot rule
     above, as `margin-top:14px`. That is (0,1,0) and lost to
     `.fbottom-row + .fbot{margin-top:20px}` (0,2,0) further up the file, so
     the mobile tightening never applied and the gap stayed at the desktop
     20px. Written at (0,2,0) it ties, and being later in the file it wins.
     Keep it as its own rule: folding it back into .fbot silently reverts it. */
  .fbottom-row + .fbot{margin-top:8px}
}


/* ═══════════════════════════════════════════════════════════════════════
   THE PHONE
   ═══════════════════════════════════════════════════════════════════════
   Added 2026-09-09, after the operator reviewed the signed-off home page on
   an iPhone. Everything in this block is inside a max-width query and NOTHING
   above 960px may change: the desktop rendering is approved and locked.

   It lives at the END of the file on purpose. Several of the rules below have
   to beat hero rules written at (0,4,0) around line 1235, and source order is
   what settles a tie. Do not "tidy" these up into the earlier responsive
   blocks - that is exactly how the bug in the first rule got there.
   ─────────────────────────────────────────────────────────────────────── */

@media (max-width:960px){

  /* ── THE BUG THIS BLOCK EXISTS FOR ────────────────────────────────────
     `.surface-tile.hero .wrap.split{grid-template-columns:1fr 280px}` near
     line 1235 is (0,4,0) and carries NO media guard, so it beat the generic
     `@media(max-width:960px){.split{grid-template-columns:1fr}}` at (0,1,0)
     at every width including 375px. Measured on the deployed page at
     375x812: the text column was 144.2px wide with a 280px picture beside
     it, and the 32px headline broke to five lines, roughly one word each.
     It was introduced while fixing the DESKTOP hero and nothing in the build
     or the test suite could see it, because nothing looks at a phone width.
     Same specificity as the offending rule, later in the file. */
  .surface-tile.hero .wrap.split{grid-template-columns:1fr}

  /* ── the reading order on a phone ─────────────────────────────────────
     The picture is the hero's whole argument and the operator wants it in
     the first screen, directly under the "one less thing to stress about"
     line - not below the paragraph, the buttons and the facts, which is
     where source order puts it.

     `display:contents` on .col-text is what makes that possible: it drops
     the wrapper's box so its children become grid items of .wrap.split in
     their own right, and `order` can then interleave them with .col-visual.
     It is safe on a plain <div> (the known assistive-technology bugs are for
     elements that carry semantics of their own - lists, tables, buttons).
     Selectors are unaffected: `.split .col-text p` still matches.

     Row spacing is NOT set here. `.hero .wrap.split{row-gap:0}` already
     zeroes it, so the elements keep their own margins and the vertical
     rhythm is the one they were designed with. Put a gap back and every
     line of the hero gains it, including between the eyebrow and the
     headline. */
  .surface-tile.hero .col-text{display:contents}
  .surface-tile.hero .eyebrow{order:1}
  .surface-tile.hero h1{order:2}
  .surface-tile.hero h2.sub-head{order:3}
  .surface-tile.hero .col-visual{order:4}
  .surface-tile.hero .sub{order:5}
  .surface-tile.hero .btn-row{order:6}
  .surface-tile.hero .facts{order:7}

  /* ── the tilt comes BACK on a phone ───────────────────────────────────
     This reverses the `.shot-angle{transform:none}` override near line 1468,
     which is why that rule's comment now points down here. The reasoning
     there was sound for the layout it was written against - a full-width
     screenshot laid flat, with the transformed bounding box able to push the
     BODY sideways. Two things changed:

       1. the operator's call, 2026-09-09: the angle is the hero's signature
          and a phone is where most people will meet it;
       2. .surface-tile.hero carries `overflow:hidden`, so a picture that
          bleeds past the right edge is clipped by the section and never
          reaches the body. HOR-001 in the test suite asserts that.

     Angles are reduced, not copied: rotateY goes 19deg -> 12deg and the
     perspective 1350px -> 900px, because a 19deg turn on a 375px viewport
     throws away a third of the image to foreshortening. transform-origin
     moves to the left edge so the picture pivots away from the text rather
     than away from the middle, and the 118% width is what pushes the far
     edge off-screen - the same bleed the desktop composition has, sized for
     a phone. */
  /* RESIZED 2026-09-09. Operator: "for mobile, I want the image to be fully
     visible, and for there to be a little bit of blue padding around the image
     in that top hero section... probably down to about 80% of what it is."
     THE SIZE COMES FROM THE MARGINS, NOT FROM A PERCENTAGE, and that is worth
     explaining because the obvious reading of "80%" is `width:80%` and that is
     what I tried first. It came out at 63% of the old picture, not 80% — the
     old box was ~401px wide because .col-visual had NEGATIVE margins pulling it
     into both gutters, so taking 20% off a box that had already been widened
     compounds. Two changes, together:
       · the negative margins on .col-visual go (see the note on that rule), so
         the box is the wrap's own 335px column instead of 401px;
       · the picture stays at 100% OF THAT, which measures 334.1px — 83% of the
         old 401px. That is the "about 80%" he asked for, and the blue either
         side is the page's own 20px gutter rather than a padding invented here.
     transform-origin also moves back to the CENTRE. At `0 50%` the picture
     pivots about its left edge, so the whole of the perspective's spread is
     thrown to the right and off the screen; pivoting about the middle splits
     that spread either side, which is what lets it fit at all.

     "FULLY VISIBLE" IS MEASURED, NOT ASSUMED. The projected corners of a 3D
     transform are not the element's layout box, so this is checked with
     getBoundingClientRect on the transformed element: at 375px it runs
     24.4px to 358.5px, inside the viewport at both ends, with
     document.scrollWidth still 375. The two gaps are not equal — 24.4 left
     against 16.5 right — because rotateZ and rotateY skew the projection.
     That is the tilt, not a centring bug, and evening them up would mean
     un-tilting the picture. */
  .shot-angle{width:100%;max-width:none;margin:0 auto;
    transform:perspective(900px) rotateX(9deg) rotateY(-12deg) rotateZ(2deg);
    transform-origin:50% 50%}
  /* The picture is pulled into the LEFT gutter rather than pushed off the
     right. It used to be 118% wide starting at the 20px gutter, which put its
     projected right edge at about 454px on a 375px viewport - roughly 79px
     past the screen, and the corner that went missing was the TOP right, where
     the pay-date and employee-count panels are. The operator asked to see most
     of the screen and said he did not mind losing the bottom right; rotateZ
     tilts that corner down and out anyway, so this trades one for the other.
     Starting at x=0 buys back the gutter, and 100% instead of 118% is what
     brings the top-right corner inside the viewport. */
  /* WAS margin:14px -6px 2px -20px — negative both sides, which is what pulled
     the picture into the gutters and off the screen. That was the right answer
     while the brief was "show most of the screenshot, I don't mind losing the
     bottom right". The brief changed on 2026-09-09 to "fully visible, with a
     little bit of blue padding around it", so the negative margins go and a
     positive inset takes their place. The blue is the hero's own surface
     showing through — there is no background on this box, so padding here IS
     the padding he asked for. */
  .surface-tile.hero .col-visual{margin:14px 0 6px}

  /* ── the facts strip stacks ───────────────────────────────────────────
     It is a flex ROW with a 32px gap, and it does not fit: measured 450px
     wide inside a 375px viewport, with the "Holidays Act compliant now,
     Employment Leave Act from 2028" item 430px on its own. Three claims side
     by side is a desktop idea; on a phone they are a list.
     ::before moves down by exactly the padding added above it, so the marker
     stays on the first line's cap height. */
  .facts{display:block;margin-top:22px;padding-top:16px}
  .facts li{padding:6px 0 6px 20px}
  .facts li::before{top:13px}
}

@media (max-width:720px){
  /* ── a shorter fade, same shape ───────────────────────────────────────
     The desktop fade is 200px and its padding-bottom derives from it at
     0.75, so the hero ends with 150px of guaranteed-empty gradient. On an
     812px screen that is a fifth of the phone spent on nothing.

     ONE TOKEN, and everything follows: all nine smoothstep stops are written
     as offsets from --hero-fade and padding-bottom is calc()'d off it, so
     the easing, the dither and the coupling are all unchanged - only the
     distance is shorter. This is NOT the 2026-09-09 override that was
     deleted from the 960px block: that one LENGTHENED the phone's fade
     relative to desktop, which is backwards. 110px is shorter than 200px. */
  .surface-tile.hero,.surface-tile.page-hero{--hero-fade:110px}

}

@media (max-width:960px){
  /* ── the menu is a tree, not a list ───────────────────────────────────
     The panel was white with dark text. It is now the same near-black the
     footer uses, for one reason: it drops out of a navy bar, and a white
     sheet arriving under a navy bar reads as a different website. The
     operator approved the dark tree in the mock-up.

     "Who We Help" keeps its heading and shows its three children in place,
     indented behind a pink rule. It is a <button> in the markup and stays
     one - hiding the chevron is enough, because on this panel the menu is
     already open and there is nothing left for it to toggle. A second tap to
     reveal three links is where people give up.

     There is deliberately NO heading over the five standalone pages. The
     mock-up carried one reading "The Site"; the operator's words were that
     it "doesn't make any sense", and he was right - it named nothing the
     five items did not already say. */
  /* ── the brand is a tap target ────────────────────────────────────────
     Measured 27.9px tall on the deployed page. UI-STANDARDS rule 5.3 puts
     the floor at 44px, which .navtoggle already meets and this did not - and
     on a phone the wordmark is the only route back to the home page from any
     of the other seventeen. Height rather than padding, for the same reason
     .navtoggle uses height: a label change cannot silently shrink it. */
  .brand{min-height:44px}

  /* ── the ladder ───────────────────────────────────────────────────────
     The toggle used to read the word "Menu" in a pill. It is now three bars,
     which is what a phone visitor looks for. The <button> keeps its
     aria-label="Menu" and its id, so NAVJS and every screen reader are
     unaffected; only the visible content changed. The pill border goes with
     the word - a bordered pill around an icon reads as a second CTA sitting
     next to the real one.

     This is in the 960px block and not the 720px one deliberately: the bars
     are in the markup at EVERY width, and .navtoggle appears from 960px
     down. Style them any narrower and 721-960px gets a bordered empty pill
     with three unstyled, invisible <i> elements in it - the toggle would
     still work and would look broken. */
  /* ── the bar's right-hand edge ────────────────────────────────────────
     .hdr-inner is a plain flex row and .mainnav - the thing that carried
     `flex:1` and pushed everything after it to the right - is display:none at
     this width. So brand, CTA and ladder all packed to the LEFT and the ladder
     finished at x=315.6 on a 375px viewport, 39.4px short of the 20px gutter
     everything else on the page lines up to. margin-left:auto on .hdr-cta puts
     that job back where .mainnav used to do it, and the ladder, being last,
     lands on the margin.
     The gap comes down with it: 22px is the DESKTOP rhythm, set for a row that
     also holds a nav. Here it left the CTA floating in the middle of the bar
     rather than reading as a pair with the ladder beside it. */
  .hdr-cta{margin-left:auto}
  .hdr-inner{gap:10px}
  /* The pill keeps its 44px height. The operator asked for it to match the
     ladder and pre-accepted being inconsistent with other buttons - and it
     ALREADY matches: both measure exactly 44.0px, because 44px is the touch
     floor in UI-STANDARDS rule 5.3 and MOB-004 asserts it across the page.
     What differs is weight, not height: the ladder is three 2px bars in an
     invisible box, the pill is 44px of solid pink. So what gives here is the
     pill's WIDTH, and the height stays where the accessibility floor puts it -
     shrinking the primary conversion button below 44px is the one place on
     the page not to spend that budget.

     CORRECTION, same day: this rule also carried `font-size:13.5px` and a
     comment saying the label came down from 14px. Both were wrong. The base
     rule at ~line 704 already sets 13.5px, so the declaration changed nothing
     and the comment described an edit that never happened. Measured on the
     deployed page at 1470px: 13.5px before and after. Only the horizontal
     padding actually moves, 17px -> 15px.

     SECOND REVISION, 2026-09-09. The paragraph above argued the height should
     stay at 44px and the operator asked again: "I know it's against
     convention, but that's what needs to be done." His call, and it is taken.
     The pill's PAINTED box is now 34px - 59% of the 58px bar rather than 76%.

     What is NOT given up is the touch target. ::after extends the hit area
     back to 44px (34 + 5 above + 5 below), so a thumb still gets the full
     floor while the eye sees a 34px pill. That is the whole reason this is
     done with a pseudo-element instead of just setting a smaller height: the
     operator asked for a shorter BUTTON, not a smaller target, and those are
     separable.

     The overlay is given an EXPLICIT 44px height and centred, rather than
     negative top/bottom insets. The first attempt used `top:-5px;bottom:-5px`
     and the arithmetic looked right - 34 + 5 + 5 = 44 - but absolute insets
     resolve against the containing block's PADDING box, not its border box,
     so the 1.5px border on each side came off and the real hit area was 41px.
     A hit test 4px above the painted edge landed on .hdr-inner, not the
     button, which is how it was caught. Height plus translate cannot drift
     that way: it is 44px whatever the border does.

     MOB-004 knows about this: it measures the header CTA's hit area rather
     than its box, and names it as the second documented exception after the
     32px .askai-btn. */
  .hdr-cta .btn{padding-left:15px;padding-right:15px;
    min-height:34px;height:34px;position:relative}
  .hdr-cta .btn::after{content:"";position:absolute;
    left:0;right:0;top:50%;height:44px;transform:translateY(-50%)}
  .navtoggle{border:0;padding:0;width:44px;justify-content:center}
  .navtoggle .bars{display:flex;flex-direction:column;gap:5px}
  .navtoggle .bars i{display:block;width:20px;height:2px;border-radius:2px;
    background:var(--on-dark)}

  .mainnav.open{background:#0a0e1c;border-bottom-color:rgba(255,255,255,.12);
    padding:6px var(--pad) 18px}
  .mainnav.open a,.mainnav.open .grp>button{color:var(--on-dark);font-size:16px;
    font-weight:600;min-height:46px;padding:0;display:flex;align-items:center;
    border-bottom:1px solid rgba(255,255,255,.09)}
  .mainnav.open a:hover,.mainnav.open .grp>button:hover{color:var(--action-on-dark)}
  .mainnav.open .grp{width:100%}
  /* The group label, not a control: pink, small, tracked out, and it does not
     take a row of its own height or a rule under it. */
  .mainnav.open .grp>button{color:var(--action-on-dark);font-size:11px;
    font-weight:700;letter-spacing:1.7px;text-transform:uppercase;
    min-height:0;padding:14px 0 2px;border-bottom:0;cursor:default}
  .mainnav.open .grp>button span{display:none}
  /* background:none IS THE BUG FIX, not tidying. `.mainnav .grp .menu` up at
     ~line 649 paints the DESKTOP dropdown white (var(--canvas)) because there
     it is a card floating over the page. The mobile override at ~line 2410
     resets position, border, shadow and padding but never touched the
     background - which did not matter while this panel was itself white, and
     mattered enormously the moment it went dark: the three "Who We Help" pages
     rendered as a white card inside a near-black drawer, with the muted grey
     link colour set just below leaving them at roughly 1.4:1. The operator saw
     it on an iPhone 15 and read the whole thing as the site honouring dark
     mode. It was not dark mode; it was this. */
  .mainnav.open .grp .menu{background:none;padding:0 0 0 14px;margin-left:2px;
    border-left:2px solid rgba(255,92,174,.45)}
  .mainnav.open .grp .menu a{font-weight:400;font-size:15px;padding:0;
    color:var(--on-dark-muted);background:none}
  .mainnav.open .grp .menu a:hover{background:none;color:var(--on-dark)}
}

@media (max-width:900px){
  /* ── the client story reads in the right order ────────────────────────
     Source order is: heading, paragraph, CALL TO ACTION, video, quotes.
     That order exists because the operator asked for the CTA under the
     paragraph on DESKTOP, where it sits in the left column beside the video
     and reads correctly. Stacked on a phone it puts "Learn about more client
     stories" between the paragraph and the video it is describing - you are
     asked to leave before you have been shown anything.

     Same mechanism as the hero: `display:contents` on .col-text drops the
     wrapper so `order` can move the video above the button. 900px, not 640,
     because that is where .split-media stacks - reordering only on phones
     would leave 641-900px reading the wrong way round.

     WHAT THIS DOES NOT DO, and why: the mock-up put the quote BEFORE the CTA
     as well. The quotes live in a separate .wrap, a sibling of .split-media,
     so no amount of `order` reaches across the two - that one needs
     sec_video() to emit the CTA after the quotes, which changes the DESKTOP
     markup that is currently signed off. Left alone deliberately, and raised
     with the operator rather than taken unsupervised.

     ADDED "picture" 2026-09-18: sec_leader's photo, once supplied, renders
     through picture() rather than as a .videowrap or a .ph-video - it is a
     real <picture>, not either flavour of video slot - so without this it
     fell through to the browser's default order:0 and jumped ahead of
     .sec-head (order:1), landing above "A note from Ryon G." instead of
     below it. Same real/placeholder pairing as .videowrap and .ph-video
     above, just for a photo instead of a video. */
  .split-media .col-text{display:contents}
  .split-media .sec-head{order:1}
  .split-media .videowrap,.split-media .ph-video,.split-media picture{order:2}
  .split-media .btn-row{order:3}
  .split-media .speakers{order:4}
}

@media (max-width:640px){
  /* ── strength cards, tightened ────────────────────────────────────────
     There USED TO BE a `.cards .card .cardimg{display:none}` rule here,
     hiding the strength cards' picture on phones only. It was removed
     2026-09-09 when the operator dropped that picture at every width
     instead - "let's not swamp them with images... later on we have images
     on the cards for the sectors we help" - so build.py stopped emitting
     the element and the rule had nothing left to hide.

     It is worth knowing WHY it is not simply left in place harmlessly: a
     rule that hides an element nobody emits reads, to the next person, as
     evidence that the element exists. They go looking for it.

     The padding below is NOT part of that and stays: it is the phone
     tightening, and it applies to a card whether or not it has a picture. */
  .cards .card{--pad-top:18px;--pad-x:18px}

  /* ── the six-item block, tightened ────────────────────────────────────
     Operator: "we need to minimise the amount of vertical space that the
     section is taking up." The picture is already 4:3 below 900px; on a
     phone it becomes a band, and each item's heading sits directly on its
     own line instead of 4px above it, so an item is two lines rather than
     four. */
  .splitlist{gap:18px}
  .splitlist-media .ar-4x5{aspect-ratio:16/9}
  .splititem{padding:9px 0}
  .splititem h3{font-size:16px}
  .splititem p{margin-top:1px;font-size:14px;line-height:1.42}

  /* ── one quote on the home page, not two ──────────────────────────────
     Operator, 2026-09-09: "drop the quotable card box for Amy; have just the
     quotable box in there for Steve." Two stacked quote cards is about 300px
     of reading between the video and the way in; Steve's is the one that
     frames the problem, so his is the one that stays.

     Scoped to the home page by body[data-page], NOT to .speakers generally.
     The story pages carry speakers of their own and the operator has not
     looked at those on a phone yet; a global rule would silently delete
     quotes from pages nobody has reviewed. That is why build.py now writes
     the page's slug onto <body>. */
  body[data-page="index"] .speakers .speaker:nth-of-type(n+2){display:none}

  /* ── two sector-page quotes, desktop only ──────────────────────────────
     Operator, 2026-09-14: put Viniece Dodds' quote back under Marralomeda on
     /who-we-help/disability-and-care, and Jeremy Phillips' back under Novo
     Group on /who-we-help/professional-teams-and-firms — both desktop only.
     Neither section sets quote_beside_video, so the quote renders as the
     ordinary full-width .vquotes tail (same shape as the home page's Equitas
     block); hiding the whole tail on phones is enough, the sec-head above it
     is untouched. Scoped by body[data-page], same mechanism and same reason
     as the index-page rule above — this must not touch /stories, the home
     page, or trades-and-construction, none of which were asked for. */
  body[data-page="who-we-help/disability-and-care"] .vquotes,
  body[data-page="who-we-help/professional-teams-and-firms"] .vquotes{display:none}

  /* ── buttons pull in from the edges ───────────────────────────────────
     Operator: "the CTA button shouldn't necessarily be full width... perhaps
     to be 80% of the viewport." Edge to edge reads as a banner rather than a
     button. Centred, at 80%, they are unmistakably tappable and the page
     keeps a margin down both sides.
     .hdr-cta is NOT a .btn-row, so the header's "Get started" is untouched. */
  .btn-row{flex-direction:column;align-items:center;gap:10px}
  /* 80vw, NOT 80%. A percentage here resolves against .btn-row, which sits
     inside .wrap and is therefore already 40px narrower than the screen: 80%
     of it measured 268px = 71.5% of a 375px viewport, and the operator's words
     were "80% of the viewport". 80vw is 300px, which is what he asked for and
     what MOB-006 asserts. min() keeps it inside its column on any width where
     80vw would be the larger of the two. */
  .btn-row .btn{width:min(80vw,100%)}
  /* .cards-cta is the ONE call to action on this page that is not in a
     .btn-row - the strengths section wraps its single button in a centred div
     of its own. Left out, it rendered 165px wide directly under three buttons
     of 300px, which reads as a different, lesser control rather than the one
     the section is asking you to press. Same width, same rule, stated twice
     because the markup gives it no shared hook. */
  .cards-cta .btn{width:min(80vw,100%)}

  /* ── footer button labels go grey ─────────────────────────────────────
     Operator, 2026-09-09, after looking at support.payrollbytempus.com on a
     phone: its link text is a muted slate, not white, and the effect is a
     list you can scan rather than a wall of equally-loud labels. White here
     put thirteen buttons at the same weight as the group headings above
     them, so the headings stopped doing their job.
     --on-dark-muted on #161d33 measures 8.4:1 - well clear of the 4.5:1
     floor, and the headings stay the brightest thing in the footer.
     Call Us and Email Us keep white: they are the two ACTIONS in here, and
     the whole point of that row is that it does not read as navigation. */
  .site-footer .fmgrid a.fmbtn{color:var(--on-dark-muted)}
  .site-footer .fmgrid .fmcta a.fmbtn{color:var(--on-dark)}
}

/* ═══════════════════════════════════════════════════════════════════════
   THE PICTURE BESIDE THE SIX-ITEM BLOCK
   ═══════════════════════════════════════════════════════════════════════
   This is the only picture on the site whose SHAPE changes with the window:
   4:5 standing beside the list on a desktop, 4:3 as the layout narrows, and
   16:9 as a band above the list on a phone. All three come from one file.

   The generic rule for a picture() image is .pic-img{width:100%;height:auto}
   — the image sets its own height and nothing crops it. That is right almost
   everywhere and wrong here: .ar-4x5 puts a fixed aspect-ratio on the box, so
   an image left at its natural height leaves a gap under itself in two of the
   three shapes. height:100% + object-fit:cover hands the shape back to the
   box and lets the picture be trimmed to fit, which is what .postthumb and
   .artlede-media already do.

   THE 53% IS THE POINT OF THIS BLOCK. object-fit:cover keeps the MIDDLE of
   the picture by default, and the middle is not what this one is about. The
   16:9 window is 608 of the crop's 1350 pixels, so it shows 45% of the
   picture and something has to choose which 45%. Two things have to survive
   and they pull in opposite directions: the three faces near the top, and the
   wall planner and payslips on the desk near the bottom — which are the
   actual subject, in a section about the parts of payroll that go wrong.
   Centred, the paperwork is cut off. Slid far enough down to hold all of it,
   the man's hair touches the top edge.

   53% is where both fit — 37 source pixels of clear space above his head,
   about 11 on a phone, with the planner and the payslips whole. Chosen by
   rendering 40/50/53/56/65% at the size the picture actually ships at and
   looking: 56% left 2px of headroom, which reads as touching, and 65% cut his
   head outright.

   THIS NUMBER WAS 70% UNTIL 2026-09-09, for a different photograph — five
   people around a boardroom table, where centring clipped one person's hair
   and there was no paperwork to protect. The picture was replaced and the
   value was re-derived from scratch, not adjusted. It is tied to the exact
   crop box in build.py (IMAGE_SLOTS, "payroll-room"): if that box moves, this
   number is wrong and has to be re-chosen by looking again.

   Only below 640px, because that is the only width where the box is 16:9. At
   4:5 and 4:3 the default centre keeps everybody in frame already, and moving
   the picture there would push the group down for no reason. */
.splitlist-media picture.ar-4x5{display:block;overflow:hidden}
.splitlist-media picture.ar-4x5 .pic-img{height:100%;object-fit:cover}
@media (max-width:640px){
  .splitlist-media picture.ar-4x5 .pic-img{object-position:center 53%}
}

/* ── inner-page design parity, 2026-09-09 ──────────────────────────────
   The navy→parchment gradient above was welded to `.surface-tile.hero`, and
   only the home hero emits `hero`; every other page emits `page-hero`, so
   seventeen of eighteen pages opened on flat white. The selector extensions
   above fix that. The rules below are the consequences of doing so, plus the
   three article-page defects found alongside it. Nothing here can reach the
   home hero: every selector is `.page-hero` or `.article`. */

/* The page hero only switches to `.wrap.split` when a photograph is present,
   and `.split` is 1.12fr .88fr — so adding a picture cut a 78-character h1
   from ~1132px to ~566px and it wrapped twice as much purely because a photo
   arrived. Give the words the larger share. */
@media (min-width:961px){
  .page-hero .wrap.split{grid-template-columns:1fr}
}
/* THE MEDIA QUERY IS LOAD-BEARING, and 961 is not arbitrary. .split stacks to
   one column at max-width:960px, and that stacking rule is `.split` at (0,1,0).
   Written bare, `.page-hero .wrap.split` is (0,3,0) and beats it at EVERY width,
   so every page hero carrying a photograph kept two columns on a phone: the h1
   squeezed into a ~55% column beside the picture, wrapping over five lines.
   Caught on /pricing at 375px on 2026-09-09. Same pattern as .split.errsplit
   above — state the width rather than depend on sitting after the stacking
   rule. */

/* More gradient beneath the hero text, as asked. This raises the ratio rather
   than lengthening `--hero-fade`: the note on --hero-fade records that
   lengthening the fade has been the wrong lever twice and only buys dead navy.
   A higher ratio pushes the text up so more of the EXISTING fade sits under
   it. Scoped to `.page-hero` — the home hero keeps its .75. */
.surface-tile.page-hero{padding-bottom:calc(var(--hero-fade) * .9)}

/* `.article h1` had no override, so it inherited the global
   clamp(34px,5.2vw,54px) — making a blog post's title the LARGEST type on the
   site, bigger than the home hero (46px) and the page hero (36px). That is why
   article titles wrapped over four lines. */
.article h1{font-size:clamp(30px,3.6vw,42px)}

/* `.artlede` was minmax(0,1.05fr) minmax(0,.95fr) above 861px, then narrowed
   to minmax(0,1.62fr) minmax(0,.38fr) as a workaround for the pending-media
   placeholder's explanatory sentence not fitting a small box. That reasoning
   is now obsolete: all five Insights posts carry a real photograph in this
   slot, and at ~19% width the opening image read too small — per the site
   owner's own feedback once real photography replaced the placeholder. Widened
   to roughly double the image's share (text ≈60%, image ≈40%) as a first
   pass; only above 861px — below that the lede is already one column and the
   picture stays full width. */
@media (min-width:861px){
  .artlede{grid-template-columns:minmax(0,1.2fr) minmax(0,.8fr)}
}

/* text-shorter-than-image: widening the image column above also grows its
   height, since the base rule (further up this file) fixes the image to a
   4:3 ratio driven purely off column width. Measured live against all five
   posts' real opening paragraph(s) at this width: every one of them left the
   text column shorter than the image — by as little as 11px (the post with
   the longest lede) and as much as 111px (the shortest). No grid ratio fixes
   this for every post at once: narrowing the image column back down to close
   the gap on short posts also shrinks the image back toward its old, too-small
   size, defeating the point of widening it above.
   So above 861px the image stops being independently sized at all — it
   gives up its own aspect-ratio and instead stretches (align-items:stretch
   on the grid, height:100% here) to exactly match whatever height the text
   column's own content naturally comes out to, tall or short. object-fit:
   cover (set on the base rule above) then crops the photo to whatever shape
   that produces — confirmed live on all five posts, from a close 1.38:1 crop
   on the longest post down to a 1.78:1 crop on the shortest, and all five
   read as a normal, intentional edit rather than a stretched or squashed one. */
@media (min-width:861px){
  .artlede{align-items:stretch}
  .artlede-media{aspect-ratio:auto;height:100%}
}

/* Consequence of the rule above, and only visible because no Insights post has
   a photograph yet. At ~192px the placeholder's explanatory sentence no longer
   fits its own box — measured 2026-09-09 at 178px of content in a 144px slot,
   so it spilled out the bottom on all five posts. The "Placeholder" tag alone
   says what needs saying in a slot this size. Scoped to the article lede, so
   the full marker still reads normally everywhere else it is used, and a real
   photograph is unaffected either way. */
.artlede-media .ph-video p{display:none}

/* ASK-AI: NO TOP MARGIN WHERE THE STRIP OPENS AN ARTICLE. Since sec_article was
   split into two bands, `.askai` is the FIRST child of `article.article` in the
   white band, and that band already supplies its own top padding — 25.2px below
   960px, 16px above it (with the -14px overlap). `.askai{margin:16px 0 28px}`
   at :3589 was written when the strip sat mid-band beneath the h1; as a first
   child its 16px top now collapses out through .wrap and .article to the band's
   content edge and simply stacks on that padding, buying nothing.

   MEASURED at 375px and 1440px against /stories, the page the operator holds up
   as correctly balanced at the top. Both pages run the SAME hero geometry —
   107px/99px padding and --hero-fade:110px at 375px, 128px/180px at 1440px — so
   the whole difference at the band boundary was this one margin.

     hero bottom -> top of the first thing in band 2
     375px    /stories      387.2 -> 412.4  = 25.2px, the band's padding exactly
              post before   361.0 -> 402.2  = 41.2px  (25.2 + this 16)
              post after    361.0 -> 386.2  = 25.2px
     1440px   /stories      435.4 -> 437.4  =  2.0px  (-14px margin + 16px pad)
              post before   456.3 -> 474.3  = 18.0px
              post after    456.3 -> 458.3  =  2.0px

   The first .askai-btn moves 427.2 -> 411.2 at 375px against /stories' first
   content at 412.4, and 499.3 -> 483.3 at 1440px; .stand moves 527.2 -> 511.2
   and 559.3 -> 543.3. What remains between the two pages is CONTENT, not
   spacing: the post's h1 wraps to three lines at 375px where /stories' wraps to
   two, which is why the post's hero is 26px shorter and its date sits higher
   inside it. Not a spacing bug, and not to be answered by shrinking type.

   SCOPED, NOT BLANKET-REMOVED, and the scope still earns its keep even though
   the case it was drawn around has since gone. It was written while `.askai`
   also rendered in the /insights index hero, following an .eyebrow and an h1
   inside .page-hero .wrap, where it genuinely wanted its 16px. The operator
   removed that row on 2026-09-09, so the only `.askai` left on the site is the
   one this rule targets. Left scoped rather than collapsed to a blanket
   `.askai{margin-top:0}` because the 16px is correct for any `.askai` that
   follows something, and putting the row back on an index or under a heading is
   a small edit. It cannot reach the home hero either, which carries no
   .page-hero, no .article and no .askai. Checked 2026-09-09: this selector
   matches exactly once per post, on all five, and zero times on the other
   thirteen pages. The child combinator matters on
   /what-happens-when-your-payroll-person-leaves, whose hero uses a second
   `.article` wrapper whose first child is the .eyebrow.

   THE 28px BOTTOM MARGIN WAS EXAMINED AND DELIBERATELY LEFT ALONE. It is not
   redundant. It collapses against `.article .stand{margin:16px 0 26px}` at
   :2457, so the painted gap is max(28,16) = 28px and the entire saving on offer
   is 12px, not 28px — anything below 16px moves nothing at all. And it is the
   wrong 12px: the strip's own internal gap is 10px (.askai-lead) and a body
   paragraph is 17px (1em at 17px), so a 16px strip-to-prose gap would be
   TIGHTER than one paragraph and the control strip would start reading as the
   article's opening paragraph. 28px is also within 2px of this file's house
   separation for a distinct block — 26px on .article blockquote and on .stand's
   own bottom. It is doing a job; leave it.

   MOBILE-ONLY REDUCTION, 2026-09-11. Operator: on a phone, pull the strip
   closer to the hero gradient above it — 20%, roughly 3px, off the 16px this
   rule already zeroes for margin-top. That 16px lives only in the base
   .askai rule now (also trimmed to 13px below 768px, see there), because
   THIS rule's margin-top:0 is the floor .askai itself can reach — there is
   no positive margin left here to shave 20% off. The only way to move the
   painted gap further is a small NEGATIVE margin, pulling the strip up into
   the band's own top padding instead: 3px is 20% of 16px, rounded to the
   pixel the same way the base rule's 13px is. That takes the gap measured
   above (375px: 25.2px, matching /stories) down to 22.2px on a phone only;
   768px and up keeps margin-top:0 and the gap this whole comment measures. */
.article > .askai:first-child{margin-top:0}
@media (max-width:767.98px){
  .article > .askai:first-child{margin-top:-3px}
}

/* ── THE THREE SECTOR HEROES DROP THEIR PHOTOGRAPH ────────────────────────
   Operator, 2026-09-09, in their words: "The industry specific pages have
   the stock images. Remove all these from the mobile render. On mobile I
   want the eye drawn to the videos for disability care and professional
   services. … Stock images live as they are for all 3x for desktop only
   render."

   That shipped as a MOBILE-ONLY suppression: hidden below 960px, shown at
   961px and up (measured 298.33 x 186.45 at 1440px before and after,
   unchanged). On 2026-09-14, at the operator's request, this was extended
   from mobile-only to every width. The rule below no longer sits inside a
   media query, and the desktop column split above it
   (`@media (min-width:961px)`) now reads `grid-template-columns:1fr`
   instead of `1.4fr .6fr`, so the heading takes the full row now that
   nothing shares it with a photograph.

   WHY THE PICTURE WAS IN THE WAY ON A PHONE, ORIGINALLY. Stacked, the hero
   runs eyebrow -> h1 -> standfirst -> photograph, and the photograph is
   209.38px of the 622.6px hero on /who-we-help/disability-and-care at
   375px. The section immediately below it on two of the three pages is the
   client-story video — Marralomeda on disability-and-care, Novo Group on
   professional-teams-and-firms — and that is the thing the operator wanted
   a phone to arrive at. Hiding the photograph lifts the video 235.38px up
   the page on both. Trades has no video and its next section is prose, but
   it lost the picture too: the instruction was "all these" and "all 3x".

   SCOPE. `.page-hero .wrap.split > .col-visual` reaches three elements on
   the whole site — one per sector page — and nothing else. It cannot touch
   the home hero, which is `.surface-tile.hero` and carries no `page-hero`.
   It cannot touch the 404 illustration, whose .col-visual sits in a
   `.surface-canvas` band well below that page's hero. It cannot touch
   `.artlede-media`, `.postthumb`, `.splitlist-media`, the video
   placeholders, or any other `.pic`, because none of those is a
   `.col-visual` and none is inside a `.page-hero`. Verified by counting
   matches across all eighteen built pages: 3, and 0 for `.pic` outside it.
   The selector did not change on 2026-09-14, only the media query around
   it, so this scope still holds.

   NO GAP IS LEFT BEHIND, and that was checked rather than assumed. Below
   960 the hero's `.wrap.split` is a single-column grid with a 26px row gap,
   so the photograph was a second grid ITEM, not a second track.
   `display:none` takes it out of the item list, the gap it used to sit
   under has nothing to space, and the grid collapses to the text alone.
   Measured at 375px on all three: the standfirst's bottom edge lands
   exactly on `.wrap.split`'s bottom edge afterwards (a gap of 0.00px, from
   235.38px = 209.38 picture + 26 row gap), and each hero loses precisely
   that 235.38px. The 99px that remains between the split and the bottom of
   the hero is NOT dead space — it is `.surface-tile.page-hero`'s
   `padding-bottom: calc(var(--hero-fade) * .9)`, the navy-to-parchment
   gradient. Removing it would end the fade on a hard edge. This paragraph
   is the below-960px case; it was not re-measured above 960px for the
   2026-09-14 extension.

   THE BYTES ARE NOT SAVED, AND SAYING OTHERWISE WOULD BE A LIE.
   `display:none` hides a picture; it does not stop the browser fetching it.
   The <img> that sec_page_hero emits carries `fetchpriority="high"` and NO
   `loading="lazy"`, so it is requested at full priority before layout
   decides it is invisible. Confirmed in the network panel after this rule
   first landed: all three still fetch. What a phone pays for a picture it
   will never see, at `sizes` 92vw of 375px = 345px:

     candidate      1x (440w)   2x (720w)   3x (1080w)
     disability       16,892      30,472      49,908   bytes (webp)
     trades           15,650      29,580      48,202
     professional     21,634      45,556      77,316

   A modern phone is 2x or 3x, so the real number is ~30-77KB, not the 17KB
   the 1x column suggests. Since 2026-09-14 the same picture is fetched and
   hidden at desktop widths too — that request is larger, not smaller, and
   has not been remeasured here. THE REAL FIX, if that ever matters, is at
   the markup level: `sec_page_hero` in _source/build.py would stop emitting
   the <picture> for these three slots outright — with the photograph now
   hidden at every width there is no surviving desktop version left to keep
   behind a `<source media>` swap. That is deliberately NOT being done here.
   It is a build-time change to a shared generator, and the operator asked
   for a render change, not an image-delivery change. Recorded so the next
   person finds a decision rather than an oversight. */
.page-hero .wrap.split > .col-visual{display:none}


/* THE NAV LABELS DO NOT WRAP. Adding "Contact" as a seventh item on 2026-09-09
   (operator, for parity with the footer) put the bar close enough to its
   available width that flex began shrinking the items, and two of them wrapped
   their own text onto a second line — "How It Works" and "Our Story" — taking
   .mainnav from 33px to 53px inside a 58px bar.

   MEASURED, so the window is on record rather than rediscovered: it happened
   only between 961px and about 1004px. 961 is the width at which the desktop
   bar replaces the hamburger, so the break sat in the first ~44px above that
   seam — narrow, easy to miss, and squarely in the range a part-width laptop
   window lands in. With six items it did not happen at any width.

   nowrap alone fixes it: at 961px the bar returns to 33px with the same 22px of
   clearance to the CTA buttons and no horizontal overflow. `flex:none` on the
   items was measured as an equally complete fix and NOT used — it changes how
   the row distributes space at every width to solve a problem that exists at
   one end of one range. Below 961px .mainnav is the stacked mobile menu, where
   each link is its own full-width row and nowrap changes nothing.

   The real constraint is that seven is now close to the ceiling. An eighth item,
   or a longer label than "How It Works", will not fit here and should be
   measured across 961-1180px before it is added. */
.mainnav > a,.mainnav > .grp > button{white-space:nowrap}