/* =============================================================================
   combo — base.css
   THE NEUTRAL SKELETON STYLESHEET. One file, both writing directions, every locale.

   ---------------------------------------------------------------------------
   0. WHAT CHANGED, AND WHY — read this before editing
   ---------------------------------------------------------------------------
   The first version of this file was STRUCTURALLY correct and VISUALLY a wireframe.
   Sections, hierarchy, conversion order, RTL and contrast were all right; the surface
   was flat colour blocks, 1px-bordered white cards in even grids, no photography, no
   depth, no material. An operator looked at four generated directions and said "they
   look horrible … basic … why no images". He was right, and the diagnosis was precise:
   the layer that was missing was CRAFT, not structure.

   For a local trade business — plumbing, drains, facade cleaning, pest control — the
   photograph IS the trust signal. A customer standing in a flooded kitchen does not
   read a value proposition; they look for evidence that these are real people with real
   vans and a real phone number, and they press call. So this file now treats
   photography as the primary material and typography as the primary voice, and it
   ships an elevation system rather than one generic box-shadow.

   Everything below is still a token, still logical-property-only, still one file.
   See website-starter/10-visual-craft.md for the reasoning, the elevation ladder, the
   type ramp, the composition patterns and the scrim arithmetic.

   ---------------------------------------------------------------------------
   0b. WHAT THE DESIGN LAW CHANGED, AND WHERE
   ---------------------------------------------------------------------------
   website-starter/00-design-law.md is the translation of the `design-taste-frontend`
   skill into a system with no React, no Tailwind, no motion library and no build step
   at request time. It outranks every other TRACK 0 document. This pass brought this
   file into line with it. The additions, and the section of the law each answers:

     THE THREE DIALS (1)          `:root[data-variance|data-motion|data-density]`
                                  blocks, written onto <html> by partials/head from
                                  project.config.json. One template set, ten levels.
     HERO FIT (4.7)               --hero-min-block-size, the capped hero padding, the
                                  `.hero__panel` plate and its 48rem collapse, the
                                  hero bullet caps at 2 (phone) and 4 (anywhere).
     SPLIT-HEADER CAP (4.7)       documented on `.section__head--split`; four of the six
                                  templates that carried it moved to `--start`.
     EYEBROW RESTRAINT (4.7)      documented on `.section__eyebrow`. NO shipped template
                                  emits one. The recommended number is zero.
     CELL COUNT (4.7, 9.C)        `.card-grid--trio` for the N=3 case, and the mod-4
                                  arithmetic written out over `--editorial`.
     IMAGE STRATEGY (4.8)         --media-placeholder behind every media container, and
                                  `.work-evidence__*`: the proof band that replaces the
                                  logo wall this category cannot have.
     MATERIALITY (4.4, 4.2)       the elevation ladder re-derived as a BRAND-TINTED ink
                                  in the @supports block. No pure-black drop shadows.
     MOTION (5, 5.C, 5.D)         `.reveal`, scroll-driven, four gates, no listener, no
                                  library, removed rather than shortened under `reduce`.
     ORPHANS (9.B)                `text-wrap: balance` on the four heading classes,
                                  `pretty` on the running-text ones.

   THE SHAPE LOCK (4.4) IS NOT ENFORCED HERE AND CANNOT BE. This file ships seven radius
   tokens because a project's chosen family needs three plus the pill. The rule is that
   `design/chosen-direction.md` names, in a table, which token each of `.card`, `.panel`,
   `.btn`, `.pill`, `.icon-tile`, `.media-frame`, `.hero__panel` and `.faq__item` uses,
   and that AT MOST THREE distinct tokens appear in that table plus `--radius-pill`. A
   page using five radii has no radius system, it has five decisions. The defaults below
   already comply: `--radius-lg` on the containers, `--radius-md` on the icon tile,
   `--radius-pill` on the pills and buttons.

   ---------------------------------------------------------------------------
   1. LOGICAL PROPERTIES ONLY
   ---------------------------------------------------------------------------
   There is no RTL fork of this file and there must never be one. Direction comes
   from a single `dir` attribute on <html>, set from `locales[].dir`, and every rule
   below is written in flow-relative terms so the whole page mirrors itself:

        margin-left            ->  margin-inline-start
        padding-right          ->  padding-inline-end
        border-left            ->  border-inline-start
        text-align: left       ->  text-align: start
        right: 1rem            ->  inset-inline-end: 1rem
        bottom: 1rem           ->  inset-block-end: 1rem

   orig: the reference implemented RTL by hand-mirroring physical properties INSIDE
   inline style="" attributes — a border-right here, a margin-left there — across six
   forked templates, plus a mirrored SVG arrow it maintained as a second copy of the
   path data. Every one of those pairs was a chance for the two directions to drift,
   and they did.

   Where a physical property is genuinely unavoidable it is marked
   `/* PHYSICAL: <reason> * /`. There are exactly six such places in this file:
   box-shadow offsets, the directional-icon flip, carousel overflow, the viewport-width
   media queries, `object-position` on photographs, and the full-bleed viewport-width
   escape. Each is explained where it appears. GRADIENTS ARE ALL `to bottom` OR RADIAL —
   never `to right` — so not one of them needs a direction override.

   ---------------------------------------------------------------------------
   2. EVERY VALUE IS A TOKEN
   ---------------------------------------------------------------------------
   No rule below contains a colour except the `:root` block and the `@supports` upgrade
   that follows it, which are the skeleton default layer. `theme.tokens` from
   project.config.json is emitted by partials/theme-vars into an inline <style> AFTER
   this file loads, so a project's real palette overrides these defaults by source order
   at identical specificity.

   The defaults are deliberately ACHROMATIC — zero saturation, greys only. A skeleton
   that shipped with a blue would look like a decision somebody made, and a client
   whose config was never filled in would quietly go live wearing combo's opinion.
   A grey page reads unmistakably as "the palette has not been set yet".
   They are written in hsl() rather than hex so that an audit rule forbidding raw hex
   literals can be applied to templates AND stylesheets without an exception list.

   EVERY NEW TOKEN IS REACHABLE FROM CONFIG. `theme.tokens.custom["shadow-lg"]` emits
   `--shadow-lg`, so the elevation ladder, the scrim, the type ramp and the icon weight
   are all re-themeable without editing this file. That is the contract in
   website-starter/03-design-system.md §1.4 and this file does not break it.

   orig: ~300 raw hex literals across six templates, plus more written into inline
   styles by a post-build repair script, with var(--color-*) used inconsistently for
   the same palette entries.

   ---------------------------------------------------------------------------
   3. WHAT A PROJECT SHOULD DO WITH THIS FILE
   ---------------------------------------------------------------------------
   Copy it to <project>/assets/css/, then ADD a stylesheet of your own rather than
   rewriting this one, or override tokens in project.config.json and change nothing
   here at all. The class names below are the section contract that the
   required-sections audit rule checks; renaming one silently breaks that rule.
   See templates/README.md.
   ========================================================================== */

/* -----------------------------------------------------------------------------
   TOKENS — skeleton defaults. Overridden by partials/theme-vars.
   Naming contract: theme.tokens.colors.brandDark -> --color-brand-dark
                    theme.tokens.fonts.body       -> --font-body
                    theme.tokens.spacing.md       -> --space-md
                    theme.tokens.radii.lg         -> --radius-lg
                    theme.tokens.maxWidth         -> --max-width
                    theme.tokens.custom.anything  -> --anything
   -------------------------------------------------------------------------- */
:root {
  /* Lets a project opt into dark UA form controls via theme.tokens.custom. */
  color-scheme: var(--color-scheme, light);

  /* Surfaces and text. */
  --color-surface: hsl(0 0% 100%);
  --color-surface-alt: hsl(0 0% 96.5%);
  --color-surface-inverse: hsl(0 0% 11%);
  --color-text: hsl(0 0% 13%);
  --color-text-muted: hsl(0 0% 40%);
  --color-text-inverse: hsl(0 0% 100%);
  --color-border: hsl(0 0% 88%);

  /* Brand and accent. Achromatic on purpose — see the note above. */
  --color-brand: hsl(0 0% 13%);
  --color-brand-dark: hsl(0 0% 6%);
  --color-brand-contrast: hsl(0 0% 100%);
  --color-accent: hsl(0 0% 30%);
  --color-accent-contrast: hsl(0 0% 100%);
  --color-focus: hsl(0 0% 13%);

  /* ---------------------------------------------------------------------
     TYPE
     `system-ui` first so a project that ships no webfont still looks current,
     and so a locale whose script the chosen webfont does not cover falls
     through to something the operating system can actually render.
     --------------------------------------------------------------------- */
  --font-body: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --font-heading: var(--font-body);

  /* Fluid type ramp. clamp() means no breakpoint-by-breakpoint font sizing.
     step-6 is the DISPLAY step and it exists because the old ramp topped out at
     3.6rem, which is a large paragraph rather than a headline. A hero heading has
     to dominate the first screen — that is its entire job — and 3.6rem next to a
     1.3rem standfirst is a 2.8x jump that reads as "big text", not as hierarchy.
     step-6 lands near 5rem on a desktop and still fits a 375px phone. */
  --step--1: clamp(0.84rem, 0.82rem + 0.1vw, 0.9rem);
  --step-0: clamp(1rem, 0.97rem + 0.15vw, 1.06rem);
  --step-1: clamp(1.15rem, 1.09rem + 0.3vw, 1.3rem);
  --step-2: clamp(1.35rem, 1.24rem + 0.55vw, 1.65rem);
  --step-3: clamp(1.6rem, 1.4rem + 1vw, 2.1rem);
  --step-4: clamp(1.9rem, 1.55rem + 1.75vw, 2.8rem);
  --step-5: clamp(2.2rem, 1.65rem + 2.75vw, 3.6rem);
  --step-6: clamp(2.55rem, 1.6rem + 4.75vw, 5rem);

  /* Weights as tokens, so a variable font with an unusual axis range can be
     retuned from config instead of by editing thirty declarations. */
  --weight-body: 400;
  --weight-medium: 500;
  --weight-semibold: 600;
  --weight-bold: 700;
  --weight-display: 800;

  /* PER-SCRIPT METRICS. These three are what make Arabic typography correct rather
     than "Latin with different glyphs". They are overridden per script further down
     — never per locale, because it is the SCRIPT that has the metrics. */
  --root-scale: 100%;
  --leading-body: 1.65;
  --leading-heading: 1.14;
  --leading-display: 1.05;
  --tracking-display: -0.02em;
  --tracking-label: 0.08em;
  --label-transform: uppercase;
  /* Keeps x-height stable when a fallback face substitutes mid-page. `none` by
     default because the correct number depends on the chosen face. */
  --font-size-adjust: none;

  /* ---------------------------------------------------------------------
     SPACING
     --------------------------------------------------------------------- */
  --space-3xs: 0.25rem;
  --space-2xs: 0.5rem;
  --space-xs: 0.75rem;
  --space-sm: 1rem;
  --space-md: 1.5rem;
  --space-lg: 2rem;
  --space-xl: 3rem;
  --space-2xl: 4rem;
  --space-3xl: 6rem;

  /* Section rhythm. A page whose every band has identical padding reads as a
     spreadsheet. These three are the rhythm control, and `.section--tight` /
     `.section--loose` are the two deviations the layout is allowed. */
  --section-space: clamp(3rem, 2rem + 4.5vw, 5.5rem);
  --section-space-tight: clamp(2rem, 1.5rem + 2.5vw, 3.25rem);
  --section-space-loose: clamp(4rem, 2.5rem + 6vw, 7.5rem);

  /* ---------------------------------------------------------------------
     RADII — one decision, held. xs/xl/2xl are additive; sm..pill are unchanged
     so an existing project's theme.tokens.radii keeps working untouched.
     --------------------------------------------------------------------- */
  --radius-xs: 0.25rem;
  --radius-sm: 0.375rem;
  --radius-md: 0.625rem;
  --radius-lg: 1rem;
  --radius-xl: 1.5rem;
  --radius-2xl: 2rem;
  --radius-pill: 999px;

  /* ---------------------------------------------------------------------
     LAYOUT
     --------------------------------------------------------------------- */
  --max-width: 75rem;
  --max-width-narrow: 46rem;
  --max-width-measure: 38rem;
  --gutter: var(--space-md);
  --header-height: 4rem;

  /* THE HERO'S HEIGHT, AND WHY IT IS NOT `100vh`.
     00-design-law.md 9.C tell 6 bans a bare viewport block-size, and 07 4.2 explains
     why for this category: the hero is budgeted at about 453px of a 560px first screen
     so that roughly 107px of the next band stays visible. That leftover IS the scroll
     affordance, which is why no scroll cue is needed and why 9.F bans one. `62vh` is
     safe only because it is trapped between a rem floor and a rem ceiling: the floor
     keeps the band usable on a short landscape phone, the ceiling stops a 4K monitor
     turning the first screen into an empty field. */
  --hero-min-block-size: clamp(26rem, 62vh, 40rem);

  /* ---------------------------------------------------------------------
     ELEVATION — a real ladder, not one generic shadow.

     Five steps, each a TWO-LAYER shadow: a tight contact shadow that anchors the
     element to the surface, plus a wide soft shadow that carries the height. One
     blurred layer alone reads as a smudge; the pair reads as an object. Every
     offset is vertical only.

     PHYSICAL: box-shadow offsets are defined in physical x/y by the CSS spec; there
     is no flow-relative form. Both offsets in every step below are vertical, so a
     mirrored page renders identical elevation and nothing has to be maintained twice.

     --shadow-ring is the EDGE token: a hairline drawn as an inset shadow rather than
     a border, so it costs no layout box and can sit under a photograph without
     clipping it. It is what replaces `border: 1px solid` on cards. A 1px grey border
     on a white card is the single most reliable way to make a page look unfinished,
     because it is what every wireframe tool emits by default.
     --------------------------------------------------------------------- */
  --shadow-color: 0 0% 0%;
  --shadow-xs: 0 1px 1px hsl(var(--shadow-color) / 0.04), 0 1px 2px hsl(var(--shadow-color) / 0.06);
  --shadow-sm: 0 1px 2px hsl(var(--shadow-color) / 0.05), 0 2px 6px hsl(var(--shadow-color) / 0.07);
  --shadow-md: 0 2px 4px hsl(var(--shadow-color) / 0.05), 0 8px 20px hsl(var(--shadow-color) / 0.09);
  --shadow-lg: 0 4px 8px hsl(var(--shadow-color) / 0.06), 0 18px 40px hsl(var(--shadow-color) / 0.12);
  --shadow-xl: 0 8px 16px hsl(var(--shadow-color) / 0.08), 0 32px 64px hsl(var(--shadow-color) / 0.16);
  --shadow-ring: inset 0 0 0 1px hsl(var(--shadow-color) / 0.07);
  --shadow-ring-strong: inset 0 0 0 1px hsl(var(--shadow-color) / 0.14);
  /* A coloured lift under the primary action. Re-derived from --color-brand in the
     @supports block below; this literal is the fallback for engines without
     color-mix() and is deliberately neutral. */
  --shadow-action: 0 2px 4px hsl(var(--shadow-color) / 0.12), 0 10px 24px hsl(var(--shadow-color) / 0.18);

  /* ---------------------------------------------------------------------
     SCRIM — the contrast guarantee over photography.

     THE ARITHMETIC, because "put a dark overlay on it" is how pages fail WCAG in
     production. Compositing happens in non-linear sRGB. A black veil at alpha `a`
     over the WORST CASE photograph (pure white) leaves sRGB value (1 - a). White
     text on that has contrast ratio 1.05 / (L + 0.05) where
     L = ((1 - a + 0.055) / 1.055) ^ 2.4.

         a = 0.40  ->  2.9:1   fails everything
         a = 0.50  ->  4.0:1   fails AA body text
         a = 0.55  ->  4.8:1   PASSES AA for all text
         a = 0.58  ->  5.3:1   the floor used here, with headroom
         a = 0.78  -> 11.7:1   the anchor stops

     So the rule is simple and checkable: NO STOP IN A SCRIM TOKEN MAY DROP BELOW
     0.55 ALPHA. Every stop below is 0.58 or darker, which means --color-text-inverse
     over --hero-scrim clears AA over ANY photograph, including a white sky, without
     anyone having to test the specific image. That is the difference between a
     specified treatment and a hoped-for one.

     `to bottom` on every gradient: a vertical axis is direction-neutral, so no
     [dir="rtl"] override exists or is needed (03-design-system.md §7.4).
     --------------------------------------------------------------------- */
  --scrim-strong: hsl(var(--shadow-color) / 0.82);
  --scrim-mid: hsl(var(--shadow-color) / 0.66);
  --scrim-floor: hsl(var(--shadow-color) / 0.58);
  --hero-scrim: linear-gradient(
    to bottom,
    var(--scrim-strong) 0%,
    var(--scrim-floor) 38%,
    var(--scrim-mid) 68%,
    var(--scrim-strong) 100%
  );
  /* Shorter bands — a CTA over a photo, a card whose title sits on its image. */
  --band-scrim: linear-gradient(to bottom, var(--scrim-mid) 0%, var(--scrim-strong) 100%);
  --caption-scrim: linear-gradient(to bottom, hsl(var(--shadow-color) / 0) 0%, var(--scrim-strong) 72%);

  /* The no-photograph fallback. It must look DELIBERATE, not broken: two offset
     radial washes over the brand fill, plus a hairline diagonal texture, all derived
     from tokens. A flat rectangle of brand colour is what "we had no image" looks
     like; this is what "we chose not to use one" looks like. */
  --hero-gradient: radial-gradient(120% 120% at 50% 0%, hsl(0 0% 100% / 0.14) 0%, hsl(0 0% 100% / 0) 55%),
    radial-gradient(90% 90% at 50% 100%, hsl(var(--shadow-color) / 0.35) 0%, hsl(var(--shadow-color) / 0) 60%),
    linear-gradient(to bottom, var(--color-brand) 0%, var(--color-brand-dark) 100%);
  --hero-texture: repeating-linear-gradient(
    135deg,
    hsl(0 0% 100% / 0.035) 0 1px,
    hsl(0 0% 100% / 0) 1px 7px
  );
  --hero-texture-rtl: repeating-linear-gradient(
    45deg,
    hsl(0 0% 100% / 0.035) 0 1px,
    hsl(0 0% 100% / 0) 1px 7px
  );

  /* Soft surface tints. Fallbacks first; re-derived from the brand in @supports. */
  --tint-brand: var(--color-surface-alt);
  --tint-brand-strong: var(--color-surface-alt);
  --tint-accent: var(--color-surface-alt);
  --tint-inverse-raise: hsl(0 0% 100% / 0.07);
  --tint-inverse-line: hsl(0 0% 100% / 0.16);
  --tint-inverse-line-soft: hsl(0 0% 100% / 0.1);
  --glass-surface: var(--color-surface);

  /* ---------------------------------------------------------------------
     MEDIA
     --------------------------------------------------------------------- */
  --media-ratio-hero: 16 / 9;
  --media-ratio-wide: 3 / 2;
  --media-ratio-card: 4 / 3;
  --media-ratio-portrait: 4 / 5;
  --media-radius: var(--radius-lg);
  --media-zoom: 1.045;
  /* THE EMPTY FRAME, DESIGNED. Every media container paints this behind its <img>, so
     the moment before a lazy image decodes shows a deliberate brand-toned plate rather
     than a grey hole, and a frame whose file is missing degrades to something that still
     looks composed. 00-design-law.md 4.8 is explicit that a `<div>` dressed up as a
     photograph is banned; this is the opposite move, a container that reads as a
     container. It is derived from --color-brand in the @supports block below. */
  --media-placeholder: linear-gradient(to bottom, var(--color-surface-alt) 0%, var(--color-border) 100%);
  /* PHYSICAL: object-position names a point inside a raster image, which has no
     writing direction. A photograph must never mirror (03-design-system.md §7.2). */
  --media-position: 50% 50%;

  /* ---------------------------------------------------------------------
     ICONS
     The sprite in partials/icon-sprite is a real icon set on one 24x24 grid at one
     stroke weight, generated from assets/icons/sprite.svg — not shapes improvised
     inside a template, which is what the first pass shipped and what read as clip art.

     SIZES ARE IN `em`, ON PURPOSE. An icon beside a word should be measured against
     that word, not against the root: 1.25em next to `--step-0` body text and the same
     class next to a `--step-2` card title stay optically consistent without either call
     site knowing a pixel value.

     `--icon-stroke` is the weight of EVERY icon, shipped or project-added. That works
     only because no <symbol> in the sprite declares a stroke-width of its own: an SVG
     presentation attribute on a symbol beats a value inherited across the <use> shadow
     boundary, so one that carried `stroke-width="2"` would be one this token could not
     touch. `combo/scripts/icons.js --check` enforces both halves — no width in the
     sprite, a width here. Do not push `--icon-size-xl` far past 1.75em: past that a
     fixed stroke reads heavy, and the way to give an icon presence is `.icon-tile`,
     not scale.
     --------------------------------------------------------------------- */
  --icon-stroke: 1.75;
  --icon-size: 1.25em;
  --icon-size-lg: 1.5em;
  --icon-size-xl: 1.75em;
  --icon-tile-size: 3rem;
  --icon-tile-size-lg: 3.5rem;
  --icon-tile-radius: var(--radius-md);

  /* ---------------------------------------------------------------------
     MOTION AND FOCUS
     --------------------------------------------------------------------- */
  --ease-out: cubic-bezier(0.22, 0.61, 0.36, 1);
  --ease-in-out: cubic-bezier(0.65, 0.05, 0.36, 1);
  --transition: 160ms var(--ease-out);
  --transition-slow: 280ms var(--ease-out);
  --lift-sm: -2px;
  --lift-md: -4px;
  --focus-ring-width: 3px;
  --focus-ring-offset: 2px;
  /* Scroll reveal, and nothing else, moves on the block axis. A block-axis translation
     is direction-neutral, so there is no RTL counterpart to maintain; an inline-axis
     reveal would need one and is therefore not offered. 00-design-law.md 5.C. */
  --reveal-rise: 1rem;
  --reveal-range: entry 10% cover 30%;

  /* Derived. Not tokens — a project changes these by changing the tokens above. */
  --border-hairline: 1px solid var(--color-border);
  --breadcrumb-separator: "/";
}

/* -----------------------------------------------------------------------------
   TOKEN UPGRADE — colour-derived values, where the engine can compute them.

   color-mix() lets the tint, glass and action-shadow tokens be DERIVED from the
   client's own brand colour instead of being a second set of literals somebody has
   to keep in step. Guarded by @supports so an engine without it keeps the neutral
   fallbacks declared above and the page degrades to "less tinted", never to "broken".

   This block is part of the skeleton default layer, so partials/theme-vars still
   wins on anything a project declares explicitly.
   -------------------------------------------------------------------------- */
@supports (color: color-mix(in srgb, red 50%, blue)) {
  :root {
    --tint-brand: color-mix(in srgb, var(--color-brand) 6%, var(--color-surface));
    --tint-brand-strong: color-mix(in srgb, var(--color-brand) 12%, var(--color-surface));
    --tint-accent: color-mix(in srgb, var(--color-accent) 10%, var(--color-surface));
    --glass-surface: color-mix(in srgb, var(--color-surface) 82%, transparent);
    --shadow-action: 0 2px 4px color-mix(in srgb, var(--color-brand) 22%, transparent),
      0 10px 24px color-mix(in srgb, var(--color-brand) 28%, transparent);

    /* THE SHADOWS ARE TINTED, NOT BLACK. 00-design-law.md 4.2: "tint shadows to the
       background hue, no pure-black drop shadows". A pure-black shadow over a coloured
       surface reads as dirt, because nothing in the physical world casts a shadow that
       is more neutral than the light around it.

       CSS cannot read a hue out of --color-brand, so the tint is made the other way
       round: mix a little brand into near-black and use the result as the shadow ink.
       22% is the most that can be mixed in before a saturated brand starts showing as
       a coloured halo at the widest layer. Every alpha below is IDENTICAL to the
       achromatic fallback declared above, so this block changes the hue of the ladder
       and never its weight.

       A project that wants a different ink sets --shadow-color (an `H S% L%` triple)
       from theme.tokens.custom, which the fallback ladder reads; or sets any of the
       --shadow-* steps outright, which theme-vars emits after this file and therefore
       wins over both layers. */
    --shadow-ink: color-mix(in srgb, var(--color-brand) 22%, hsl(var(--shadow-color)));
    --shadow-xs: 0 1px 1px color-mix(in srgb, var(--shadow-ink) 4%, transparent),
      0 1px 2px color-mix(in srgb, var(--shadow-ink) 6%, transparent);
    --shadow-sm: 0 1px 2px color-mix(in srgb, var(--shadow-ink) 5%, transparent),
      0 2px 6px color-mix(in srgb, var(--shadow-ink) 7%, transparent);
    --shadow-md: 0 2px 4px color-mix(in srgb, var(--shadow-ink) 5%, transparent),
      0 8px 20px color-mix(in srgb, var(--shadow-ink) 9%, transparent);
    --shadow-lg: 0 4px 8px color-mix(in srgb, var(--shadow-ink) 6%, transparent),
      0 18px 40px color-mix(in srgb, var(--shadow-ink) 12%, transparent);
    --shadow-xl: 0 8px 16px color-mix(in srgb, var(--shadow-ink) 8%, transparent),
      0 32px 64px color-mix(in srgb, var(--shadow-ink) 16%, transparent);
    --shadow-ring: inset 0 0 0 1px color-mix(in srgb, var(--shadow-ink) 8%, transparent);
    --shadow-ring-strong: inset 0 0 0 1px color-mix(in srgb, var(--shadow-ink) 15%, transparent);

    /* The empty frame, re-derived so it carries the client's hue instead of a second
       grey. Still a plate and still obviously not a photograph. */
    --media-placeholder: radial-gradient(
        120% 120% at 15% 0%,
        color-mix(in srgb, var(--color-brand) 14%, var(--color-surface)) 0%,
        color-mix(in srgb, var(--color-brand) 4%, var(--color-surface)) 60%
      ),
      linear-gradient(to bottom, var(--color-surface-alt) 0%, var(--tint-brand-strong) 100%);
  }
}

/* -----------------------------------------------------------------------------
   THE THREE DIALS — 00-design-law.md 1, expressed as CSS.

   taste-skill sets VARIANCE, MOTION and DENSITY as global variables and lets the agent
   decide what they change while it writes components from scratch. combo has no agent
   at render time: it has ONE template set that must serve every project, so the dial
   cannot be "the agent picks a different class". It has to be something a static
   template can carry and a stylesheet can read.

   So the dials are three attributes on <html>, written by partials/head from
   `design.variance`, `design.motion` and `design.density` in project.config.json, and
   every dial-dependent lever in this file is an attribute selector on the root. One
   template set, ten variance levels, no per-project fork of a partial.

   ONLY THE OUTER BANDS ARE DECLARED. 4-7 is the shipped default on all three dials, so
   there is deliberately no `[data-variance="5"]` block: at the default the tokens above
   stand untouched and partials/theme-vars still has the last word on every one of them.
   A dial only speaks when it is asked to deviate.

   SPECIFICITY, ON PURPOSE. `:root[data-motion="2"]` is (0,2,0) and beats the `:root`
   block that partials/theme-vars emits after this file at (0,1,0). That is the correct
   precedence: a project that declares `design.motion: 2` and also sets a --media-zoom
   token has contradicted itself, and the law is the half that wins. To keep a token, do
   not set the dial to a band that overrides it.

   EVERY LEVER IS A TOKEN OR A MODIFIER THAT ALREADY EXISTS. Nothing below invents a
   component, and nothing below is a new class a template has to know about. That is
   1.C's rule: "if a variance level seems to need a new class, it does not".

   `design.motion` 8-10 is not a band here because it is a validation error
   (VALIDATION_MOTION_UNSUPPORTED). Bands 8-10 mean scroll hijack, pinning and scrub,
   which mean a motion library on a page whose entire performance argument is that it is
   static files on a CDN.
   -------------------------------------------------------------------------- */

/* MOTION 1-3, STATIC. Hover and focus still change colour and shadow, so every
   affordance survives; nothing moves. This is the same end state as
   `prefers-reduced-motion: reduce`, reached from the brief instead of from the OS. */
:root[data-motion="1"],
:root[data-motion="2"],
:root[data-motion="3"] {
  --transition: 120ms var(--ease-out);
  --transition-slow: 120ms var(--ease-out);
  --lift-sm: 0px;
  --lift-md: 0px;
  --media-zoom: 1;
  --reveal-rise: 0px;
}

/* MOTION 7, SCROLL-AWARE. Adds nothing but the `.reveal` band treatment further down,
   which is gated on this attribute, on @supports and on prefers-reduced-motion. */

/* VARIANCE 1-3, PREDICTABLE. The promotions switch off: the editorial grid becomes a
   plain auto-fill grid, the why-us lead panel becomes an ordinary panel, the proof row
   stops staggering, and a split section head lays out as a start-aligned stack. The
   selectors below UNDO rules that only ever exist inside `@media (min-width: 60rem)`,
   so they are written inside the same query — a phone is already at variance 1 and has
   nothing to undo. */
@media (min-width: 60rem) {
  :root[data-variance="1"] .card-grid--editorial,
  :root[data-variance="2"] .card-grid--editorial,
  :root[data-variance="3"] .card-grid--editorial {
    grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
    grid-auto-rows: auto;
  }

  :root[data-variance="1"] .card-grid--editorial > li:first-child,
  :root[data-variance="2"] .card-grid--editorial > li:first-child,
  :root[data-variance="3"] .card-grid--editorial > li:first-child {
    grid-column: auto;
    grid-row: auto;
  }

  :root[data-variance="1"] .card-grid--editorial > li:first-child .card__media,
  :root[data-variance="2"] .card-grid--editorial > li:first-child .card__media,
  :root[data-variance="3"] .card-grid--editorial > li:first-child .card__media {
    position: relative;
  }

  :root[data-variance="1"] .card-grid--editorial > li:first-child .card__media img,
  :root[data-variance="2"] .card-grid--editorial > li:first-child .card__media img,
  :root[data-variance="3"] .card-grid--editorial > li:first-child .card__media img {
    block-size: auto;
    aspect-ratio: var(--media-ratio-card);
  }

  :root[data-variance="1"] .card-grid--editorial > li:first-child .card__media::after,
  :root[data-variance="2"] .card-grid--editorial > li:first-child .card__media::after,
  :root[data-variance="3"] .card-grid--editorial > li:first-child .card__media::after {
    content: none;
  }

  :root[data-variance="1"] .card-grid--editorial > li:first-child .card__body,
  :root[data-variance="2"] .card-grid--editorial > li:first-child .card__body,
  :root[data-variance="3"] .card-grid--editorial > li:first-child .card__body {
    color: inherit;
  }

  :root[data-variance="1"] .why-us__grid > .why-us__point:first-child,
  :root[data-variance="2"] .why-us__grid > .why-us__point:first-child,
  :root[data-variance="3"] .why-us__grid > .why-us__point:first-child {
    grid-column: auto;
    grid-row: auto;
    grid-template-columns: auto minmax(0, 1fr);
    padding: var(--space-lg);
    background-image: none;
  }

  :root[data-variance="1"] .social-proof__grid > .social-proof__item:nth-child(even),
  :root[data-variance="2"] .social-proof__grid > .social-proof__item:nth-child(even),
  :root[data-variance="3"] .social-proof__grid > .social-proof__item:nth-child(even) {
    margin-block-start: 0;
  }

  /* A split head at low variance is a two-column head, which is the one thing 4.7 calls
     "banned as default". It collapses to the start-aligned stack rather than to the
     centred one, because the centred head survives only on `.cta` and the 404. */
  :root[data-variance="1"] .section__head--split,
  :root[data-variance="2"] .section__head--split,
  :root[data-variance="3"] .section__head--split {
    display: block;
  }
}

/* DENSITY 1-3, ART GALLERY. The top of the spacing scale opens up and the measure
   narrows. --space-3xs through --space-sm NEVER move with the dial at any level: they
   are component-internal, they size tap targets, and shrinking them breaks the 44px
   floor. 1.C.3. */
:root[data-density="1"],
:root[data-density="2"],
:root[data-density="3"] {
  --space-3xl: 9rem;
  --section-space: clamp(4rem, 2.5rem + 6vw, 7.5rem);
  --section-space-loose: clamp(5rem, 3rem + 7vw, 9rem);
  --max-width-narrow: 42rem;
}

/* DENSITY 8-10, COCKPIT. Bands tighten, and the generic card container is withdrawn:
   4.4 and taste-skill both say that above a density of 7 a card stops communicating
   hierarchy and becomes furniture. The replacement is already in this file — a hairline
   that separates rather than a box that encloses — so `.card`, `.panel`,
   `.why-us__point` and `.social-proof__item` are flattened into ruled rows here instead
   of a second component being invented. */
:root[data-density="8"],
:root[data-density="9"],
:root[data-density="10"] {
  --space-3xl: 4.5rem;
  --section-space: clamp(2rem, 1.5rem + 2.5vw, 3.25rem);
  --section-space-loose: clamp(3rem, 2rem + 4.5vw, 5.5rem);
  --max-width-narrow: 48rem;
}

:root[data-density="8"] .card,
:root[data-density="9"] .card,
:root[data-density="10"] .card,
:root[data-density="8"] .panel,
:root[data-density="9"] .panel,
:root[data-density="10"] .panel,
:root[data-density="8"] .why-us__point,
:root[data-density="9"] .why-us__point,
:root[data-density="10"] .why-us__point,
:root[data-density="8"] .social-proof__item,
:root[data-density="9"] .social-proof__item,
:root[data-density="10"] .social-proof__item {
  background-color: transparent;
  border-radius: 0;
  border-block-end: var(--border-hairline);
  box-shadow: none;
}

:root[data-density="8"] .card__body,
:root[data-density="9"] .card__body,
:root[data-density="10"] .card__body,
:root[data-density="8"] .card__content,
:root[data-density="9"] .card__content,
:root[data-density="10"] .card__content,
:root[data-density="8"] .why-us__point,
:root[data-density="9"] .why-us__point,
:root[data-density="10"] .why-us__point {
  padding-inline: 0;
  padding-block: var(--space-sm);
}

/* Column alignment for figures. taste-skill asks for a monospaced face at this density;
   combo self-hosts by unicode-range and a whole extra webfont for four numbers is not a
   trade worth making, so the intent is met with a font feature that costs nothing and
   works in whatever face is loaded. 1.C.3. */
:root[data-density="8"] .stat__value,
:root[data-density="9"] .stat__value,
:root[data-density="10"] .stat__value,
:root[data-density="8"] .social-proof__rating,
:root[data-density="9"] .social-proof__rating,
:root[data-density="10"] .social-proof__rating {
  font-variant-numeric: tabular-nums;
}

/* -----------------------------------------------------------------------------
   PER-SCRIPT TYPOGRAPHY

   03-design-system.md §1.5: `theme` is global, one palette and one ramp for every
   locale. Per-SCRIPT metrics are real and they live here, in CSS, keyed on `:lang()`
   — which prefix-matches, so `:lang(ar)` covers ar-KW, ar-SA and ar-AE from one rule.

   WHAT ARABIC ACTUALLY NEEDS, encoded rather than noted:

   1. MORE SIZE. Arabic letterforms carry their identity below the baseline and in the
      dots; at a Latin body size the same nominal point size reads noticeably smaller
      and the diacritics turn to noise. `--root-scale` lifts the ROOT font size, which
      scales the entire rem-based system at once — type, spacing, tap targets and the
      measure — so the line length in CHARACTERS is preserved instead of the line
      length in centimetres. It is a percentage, not a pixel value, so the visitor's
      own browser font-size setting still wins.
   2. MORE LEADING. Diacritics collide with the line above at Latin leading. 1.85 body
      and 1.4 heading, per the table in 03-design-system.md §6.3.
   3. NO TRACKING. letter-spacing breaks the joins in a cursive script — it is not a
      style choice there, it is a rendering defect. Setting the tracking tokens to
      `normal` disarms every rule in this file that uses them, with no per-rule
      exceptions to remember.
   4. NO CASE TRANSFORM. Arabic has no letter case, so `text-transform: uppercase` is
      a no-op at best. `--label-transform: none` removes the pointless declaration and,
      more usefully, stops a mixed Latin brand word inside an Arabic label being
      shouted.

   Hebrew and Yiddish are RTL but not joining: they take the leading and the case rule,
   and they keep normal tracking off too because there is no case to track.
   -------------------------------------------------------------------------- */
:root:lang(ar),
:root:lang(fa),
:root:lang(ur),
:root:lang(ps),
:root:lang(ckb),
:root:lang(sd) {
  --root-scale: 106.25%;
  --leading-body: 1.85;
  --leading-heading: 1.4;
  --leading-display: 1.28;
  --tracking-display: 0;
  --tracking-label: 0;
  --label-transform: none;
}

:root:lang(he),
:root:lang(yi) {
  --leading-body: 1.75;
  --leading-heading: 1.3;
  --leading-display: 1.2;
  --tracking-display: 0;
  --tracking-label: 0;
  --label-transform: none;
}

/* A gradient ANGLE is physical and does not mirror (03-design-system.md §7.4). Every
   gradient in this file is `to bottom` or radial for exactly that reason — except the
   hero texture, whose whole point is a diagonal. Rather than waive the rule, the
   diagonal is swapped for its mirror image in one place, so the texture rakes the same
   way relative to the reading direction in both. Nothing else needs a direction fork. */
[dir="rtl"] {
  --hero-texture: var(--hero-texture-rtl);
}

/* CJK: no italics, no tracking, a little more leading. */
:root:lang(zh),
:root:lang(ja),
:root:lang(ko) {
  --leading-body: 1.8;
  --leading-heading: 1.35;
  --leading-display: 1.2;
  --tracking-display: 0;
  --tracking-label: 0;
}

/* Devanagari, Thai and other scripts with marks above and below the line. */
:root:lang(hi),
:root:lang(mr),
:root:lang(ne),
:root:lang(th) {
  --leading-body: 1.9;
  --leading-heading: 1.45;
  --leading-display: 1.3;
  --tracking-display: 0;
  --tracking-label: 0;
}

/* -----------------------------------------------------------------------------
   RESET
   -------------------------------------------------------------------------- */
*,
*::before,
*::after {
  box-sizing: border-box;
}

* {
  margin: 0;
}

html {
  /* Percentage, never px: this scales the whole rem system per script while still
     inheriting the visitor's own browser font-size preference. */
  font-size: var(--root-scale);
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  /* Anchor targets clear the sticky header instead of hiding behind it. */
  scroll-padding-block-start: calc(var(--header-height) + var(--space-sm));
}

body {
  /* NOT the banned full-screen section height (00-design-law.md 9.C tell 6). This is a
     MINIMUM on <body>: the idiom that keeps a short page's footer at the bottom of the
     window. It constrains nothing, clips nothing, and there is no bare viewport
     block-size on any section in this file. `dvh` second so a phone measures the window
     it actually gives you once the URL bar has collapsed, with `vh` first as the
     fallback for an engine that does not know the unit. */
  min-block-size: 100vh;
  min-block-size: 100dvh;
  font-family: var(--font-body);
  font-size: var(--step-0);
  font-weight: var(--weight-body);
  line-height: var(--leading-body);
  font-size-adjust: var(--font-size-adjust);
  color: var(--color-text);
  background-color: var(--color-surface);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  /* Nothing on a static page should ever scroll the page sideways. Full-bleed
     sections use viewport width, and this is the guard that keeps a rounding error
     from turning into a horizontal scrollbar on a 375px phone. */
  overflow-x: clip;
}

h1,
h2,
h3,
h4 {
  font-family: var(--font-heading);
  font-weight: var(--weight-bold);
  line-height: var(--leading-heading);
  text-wrap: balance;
}

p,
li {
  text-wrap: pretty;
}

img,
picture,
svg,
iframe,
video {
  display: block;
  max-inline-size: 100%;
}

img {
  block-size: auto;
  /* A photograph that has not arrived yet is a token-coloured plate rather than a
     transparent hole, so a slow connection never shows a broken-looking gap. */
  background-color: var(--color-surface-alt);
}

a {
  color: inherit;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.18em;
}

button {
  font: inherit;
  color: inherit;
  background: none;
  border: 0;
  cursor: pointer;
}

ul,
ol {
  padding-inline-start: 0;
  list-style: none;
}

/* FOCUS IS A DESIGNED STATE, NOT A DEFAULT.
   Three px so it survives on a photograph, offset so it never touches the glyphs,
   and a `currentColor`-independent token so a project can raise it on a dark theme.
   Interactive elements that sit on imagery re-point --color-focus locally rather
   than removing the outline — there is no `outline: none` anywhere in this file. */
:focus-visible {
  outline: var(--focus-ring-width) solid var(--color-focus);
  outline-offset: var(--focus-ring-offset);
  border-radius: var(--radius-sm);
}

/* -----------------------------------------------------------------------------
   UTILITIES
   -------------------------------------------------------------------------- */
.container {
  inline-size: 100%;
  max-inline-size: var(--max-width);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.container--narrow {
  max-inline-size: var(--max-width-narrow);
}

.visually-hidden {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.skip-link {
  position: absolute;
  inset-inline-start: var(--space-sm);
  inset-block-start: calc(-1 * var(--space-3xl));
  z-index: 100;
  padding: var(--space-2xs) var(--space-sm);
  font-weight: var(--weight-semibold);
  color: var(--color-brand-contrast);
  background-color: var(--color-brand);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-lg);
  transition: inset-block-start var(--transition);
}

.skip-link:focus {
  inset-block-start: var(--space-sm);
}

/* -----------------------------------------------------------------------------
   ICONS

   The sprite draws every symbol on one 24x24 grid in currentColor with round caps and
   joins, and deliberately declares NO stroke-width. The `.icon` rule below is therefore
   the only place icon weight is decided, for the shipped set and for anything a project
   adds — one knob, no exceptions. Remove `stroke-width` here and all 71 icons drop to
   the SVG default of 1 and render hairline-thin, silently; `combo/scripts/icons.js
   --check` fails if that happens.

   orig: the reference pasted full <svg> path data inline at every call site — one
   messaging glyph appeared four times per template across six templates — so a weight
   change meant editing twenty-four places and two of them ended up different.
   -------------------------------------------------------------------------- */
.icon {
  inline-size: var(--icon-size);
  block-size: var(--icon-size);
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: var(--icon-stroke);
  stroke-linecap: round;
  stroke-linejoin: round;
}

.icon--lg {
  inline-size: var(--icon-size-lg);
  block-size: var(--icon-size-lg);
}

.icon--xl {
  inline-size: var(--icon-size-xl);
  block-size: var(--icon-size-xl);
}

/* PHYSICAL: transform is a physical operation and this is the one place the design
   genuinely must know which way the text runs. Arrows and chevrons that point "forward"
   have to point the other way when the flow reverses; every other mirroring in this
   file is handled by logical properties. Only icons carrying this class flip, so a
   clock or a pin — which must NOT mirror — never does. */
[dir="rtl"] .icon--directional {
  transform: scaleX(-1);
}

.icon-sprite {
  position: absolute;
  inline-size: 0;
  block-size: 0;
  overflow: hidden;
}

/* THE ICON TILE — why the icons stopped looking like clip art.

   A stroke glyph floating on a white card has no relationship to anything around it;
   it reads as an afterthought, which is exactly how the first pass looked. Give it a
   plate — a tinted square with a hairline ring and a soft inner highlight — and the
   same glyph reads as a designed component. This is the single highest-yield rule in
   the file for the "basic icons" complaint, and it costs one element per icon. */
.icon-tile {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  inline-size: var(--icon-tile-size);
  block-size: var(--icon-tile-size);
  color: var(--color-brand);
  background-color: var(--tint-brand-strong);
  border-radius: var(--icon-tile-radius);
  box-shadow: var(--shadow-ring), inset 0 1px 0 hsl(0 0% 100% / 0.55);
  transition: background-color var(--transition), color var(--transition),
    box-shadow var(--transition), transform var(--transition);
}

.icon-tile--lg {
  inline-size: var(--icon-tile-size-lg);
  block-size: var(--icon-tile-size-lg);
}

.icon-tile--round {
  border-radius: var(--radius-pill);
}

.icon-tile--solid {
  color: var(--color-brand-contrast);
  background-color: var(--color-brand);
  box-shadow: var(--shadow-sm);
}

/* On an inverse band the tile inverts too, or it punches a light hole in the dark. */
.section--inverse .icon-tile,
.cta .icon-tile,
.site-footer .icon-tile {
  color: var(--color-text-inverse);
  background-color: var(--tint-inverse-raise);
  box-shadow: inset 0 0 0 1px var(--tint-inverse-line);
}

/* -----------------------------------------------------------------------------
   SECTIONS AND PAGE RHYTHM

   A page where every band has the same padding, the same background and the same
   centred four-column grid is a page that reads as a template. The rhythm controls
   are here: three vertical sizes, three surfaces, a full-bleed escape and an
   asymmetric split. Templates choose; this file supplies.
   -------------------------------------------------------------------------- */
.page__main {
  display: block;
}

.section {
  position: relative;
  padding-block: var(--section-space);
}

.section--tight {
  padding-block: var(--section-space-tight);
}

.section--loose {
  padding-block: var(--section-space-loose);
}

.section--alt {
  background-color: var(--color-surface-alt);
}

.section--tint {
  background-color: var(--tint-brand);
}

.section--inverse {
  color: var(--color-text-inverse);
  background-color: var(--color-surface-inverse);
}

/* A hairline that separates two same-coloured bands without drawing a box. */
.section--ruled {
  border-block-start: var(--border-hairline);
}

/* TWO ADJACENT BANDS MAY NOT SHARE A SURFACE (00-design-law.md 4.7), AND AN INVERSE
   BAND MAY NEVER SIT NEXT TO A TINT ONE (4.11).

   The shipped home order satisfies both. The problem is that four of its eleven bands
   are conditional — intro, work-evidence, social-proof and areas-strip all render only
   when their gate is true — and no static assignment of surfaces is collision-free
   across all sixteen presence combinations. A brand-new client with no reviews is
   exactly the case that collides, and it is also the most likely one.

   These two rules close the two collisions that a real reduction produces. They are
   written as ADJACENT-SIBLING pairs on the class, which has a known limit worth stating
   rather than discovering: the selector reads the CLASS of the previous sibling, not
   its computed background, so THREE consecutive bands of the same modifier would demote
   the second and the third alike and leave those two matching. Three consecutive `--alt`
   bands cannot occur in the shipped order (services-grid always separates `intro` from
   `process`), and a project that introduces a third owns the result. This is a floor
   under a reduction, not a licence to stop choosing surfaces.

   `section-head-variety` still reports the adjacency from the `data-surface` attribute
   each band declares, so the finding is visible even where the paint has been corrected. */
.section--alt + .section--alt {
  background-color: var(--color-surface);
}

.section--inverse + .section--tint {
  /* To `surface` rather than to `alt`: the band after a demoted tint is `--alt` in the
     shipped order, and demoting to `alt` would trade a banned adjacency for a repeated
     one. */
  background-color: var(--color-surface);
}

/* FULL BLEED. PHYSICAL: 100vw is a viewport measure and the viewport has no writing
   direction, so this is symmetric and needs no mirror. `overflow-x: clip` on body is
   what keeps the scrollbar out of the calculation. */
.section--bleed > .container {
  max-inline-size: none;
  padding-inline: 0;
}

.bleed {
  margin-inline: calc(50% - 50vw);
  inline-size: 100vw;
}

.section__head {
  max-inline-size: var(--max-width-narrow);
  margin-inline: auto;
  margin-block-end: var(--space-xl);
  text-align: center;
}

/* THE ASYMMETRIC HEAD. Not everything is centred. A section whose heading sits at the
   inline start, with its standfirst beside it rather than beneath it, breaks the
   centred-stack monotony without inventing a new component. */
.section__head--start {
  max-inline-size: none;
  margin-inline: 0;
  text-align: start;
}

/* THE SPLIT HEAD, AND ITS CAP.

   00-design-law.md 4.7 bans "left big headline plus right small explainer paragraph"
   as a DEFAULT, and 9.F softens it exactly once: the clean two-column header with an
   aligned body is permitted; the tiny floating corner paragraph is the tell. This is
   the permitted form — two columns, baseline-aligned at `align-items: end`, with a real
   60-190 character standfirst rather than filler.

   IT APPEARS AT MOST TWICE PER PAGE. Six uses is not a device, it is the page's
   template, and it fails the layout-repetition ban at the same time. On the shipped
   home page the two are `services-grid` and `why-us`; `gallery`, `process`,
   `areas-strip` and `social-proof` were moved to `--start` for exactly this reason.
   `section-head-variety` counts them and fails at three. */
@media (min-width: 60rem) {
  .section__head--split {
    display: grid;
    grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr);
    gap: var(--space-xl);
    align-items: end;
    max-inline-size: none;
    margin-inline: 0;
    text-align: start;
  }
}

/* THE EYEBROW, AND WHY NO SHIPPED TEMPLATE USES ONE.

   taste-skill calls eyebrow overuse "the #1 violated rule in production tests", and
   this class is where combo is most exposed: it is fully styled and the moment it is
   added to one `section__head` it will be on all of them within a week. So the rule is
   arithmetic and it is enforced on the rendered page:

     count(.hero__badge) + count(.section__eyebrow) <= ceil(sections / 3)

   On the shipped ten-section home page that is 4, and `hero_badge` spends one. THE
   RECOMMENDED NUMBER OF `section__eyebrow` ELEMENTS IS ZERO, and the shipped templates
   emit zero: not one partial in this repository contains the class. A section's
   position on the page already categorises it, and a page that needs a label above
   every heading to explain what the heading is has a heading problem.

   The style stays because the one permitted use has to look designed when a project
   makes it. Two forms are banned outright regardless of budget: section-number eyebrows
   (`01 / SERVICES`) and version or status labels (`BETA`, `NEW`).

   Tracking and case are tokens, so an Arabic page silently gets neither — see the
   per-script block. */
.section__eyebrow {
  display: block;
  margin-block-end: var(--space-2xs);
  font-size: var(--step--1);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-label);
  text-transform: var(--label-transform);
  color: var(--color-accent);
}

.section--inverse .section__eyebrow,
.cta .section__eyebrow {
  color: inherit;
  opacity: 0.75;
}

.section__title {
  /* orig: step-3, which topped out at 2.1rem — barely larger than the standfirst
     beneath it. A section heading has to win its band. */
  font-size: var(--step-4);
  letter-spacing: var(--tracking-display);
  /* One word alone on the last line of a heading is the most visible typographic
     failure a page can have, and it is the one nobody catches because it depends on the
     viewport. `balance` costs one declaration and degrades to nothing on an engine that
     lacks it. 00-design-law.md 9.B tell 5. */
  text-wrap: balance;
}

.section__intro {
  margin-block-start: var(--space-sm);
  max-inline-size: var(--max-width-measure);
  font-size: var(--step-1);
  color: var(--color-text-muted);
  /* `pretty` rather than `balance` on running text: balance is for two or three lines,
     and applying it to a paragraph costs layout work for no gain. */
  text-wrap: pretty;
}

.section__head:not(.section__head--start):not(.section__head--split) .section__intro {
  margin-inline: auto;
}

.section--inverse .section__intro {
  color: inherit;
  opacity: 0.82;
}

/* -----------------------------------------------------------------------------
   BUTTONS

   The primary action on a trade site is the most important object on the page, so it
   gets the most material: a coloured lift, a top-edge highlight that reads as a light
   source, a press state that actually moves, and a hover that is guarded behind
   `@media (hover: hover)` so a phone never leaves a button stuck in its hover skin
   after a tap.
   -------------------------------------------------------------------------- */
.btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2xs);
  padding-block: var(--space-xs);
  padding-inline: var(--space-md);
  min-block-size: 2.75rem;
  font-size: var(--step-0);
  font-weight: var(--weight-semibold);
  line-height: 1.25;
  text-align: center;
  text-decoration: none;
  border: 1px solid transparent;
  border-radius: var(--radius-pill);
  transition: background-color var(--transition), color var(--transition),
    border-color var(--transition), box-shadow var(--transition), transform var(--transition);
}

.btn--lg {
  min-block-size: 3.5rem;
  padding-inline: var(--space-lg);
  font-size: var(--step-1);
}

.btn--block {
  inline-size: 100%;
}

.btn--primary {
  color: var(--color-brand-contrast);
  background-color: var(--color-brand);
  /* A one-pixel light edge along the top. Direction-neutral (`to bottom`), and it is
     what stops a flat fill reading as a coloured rectangle. */
  background-image: linear-gradient(to bottom, hsl(0 0% 100% / 0.14) 0%, hsl(0 0% 100% / 0) 45%);
  box-shadow: var(--shadow-action);
}

.btn--secondary {
  color: var(--color-accent-contrast);
  background-color: var(--color-accent);
  background-image: linear-gradient(to bottom, hsl(0 0% 100% / 0.12) 0%, hsl(0 0% 100% / 0) 45%);
  box-shadow: var(--shadow-sm);
}

.btn--ghost {
  color: var(--color-text);
  background-color: var(--color-surface);
  border-color: var(--color-border);
  box-shadow: var(--shadow-xs);
}

/* A ghost button standing on a photograph or an inverse band. */
.btn--onphoto {
  color: var(--color-text-inverse);
  background-color: hsl(0 0% 100% / 0.1);
  border-color: hsl(0 0% 100% / 0.5);
  backdrop-filter: blur(6px);
}

.btn__meta {
  font-weight: var(--weight-body);
  opacity: 0.85;
  /* A phone number is an LTR run inside RTL text: isolate it or the plus sign and the
     surrounding punctuation migrate to the wrong end (03-design-system.md §7.3). */
  unicode-bidi: isolate;
}

@media (hover: hover) {
  .btn:hover {
    transform: translateY(var(--lift-sm));
  }

  .btn--primary:hover {
    background-color: var(--color-brand-dark);
    box-shadow: var(--shadow-lg), var(--shadow-action);
  }

  .btn--secondary:hover {
    filter: brightness(0.94);
    box-shadow: var(--shadow-md);
  }

  .btn--ghost:hover {
    background-color: var(--color-surface-alt);
    border-color: var(--color-text-muted);
    box-shadow: var(--shadow-sm);
  }

  .btn--onphoto:hover {
    background-color: hsl(0 0% 100% / 0.2);
    border-color: hsl(0 0% 100% / 0.8);
  }
}

/* PRESS. Every button moves under the finger and loses its lift, which is the whole
   of what makes a control feel physical. Applied on `:active` so it fires on touch. */
.btn:active {
  transform: translateY(1px);
  box-shadow: var(--shadow-xs);
}

/* -----------------------------------------------------------------------------
   HEADER / NAV
   -------------------------------------------------------------------------- */
.site-header {
  position: sticky;
  inset-block-start: 0;
  z-index: 20;
  background-color: var(--color-surface);
  border-block-end: 1px solid transparent;
  transition: box-shadow var(--transition), border-color var(--transition),
    background-color var(--transition);
}

/* Frosted rather than opaque once the page has scrolled: content passing underneath
   stays faintly visible, which is what tells the eye the bar is floating above the
   page instead of being a lid on it. Falls back to the solid surface colour. */
@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
  .site-header {
    background-color: var(--glass-surface);
    backdrop-filter: saturate(1.6) blur(14px);
    -webkit-backdrop-filter: saturate(1.6) blur(14px);
  }
}

.site-header.is-scrolled {
  border-block-end-color: var(--color-border);
  box-shadow: var(--shadow-md);
}

.site-header__inner {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  min-block-size: var(--header-height);
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2xs);
  text-decoration: none;
}

.brand__logo {
  block-size: auto;
  max-block-size: 2.5rem;
  inline-size: auto;
  /* A logo is artwork and must never mirror (03-design-system.md §7.2). Nothing here
     transforms it; this comment exists so nobody adds one. */
  background-color: transparent;
}

.brand__name {
  font-family: var(--font-heading);
  font-size: var(--step-1);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-display);
}

.nav-toggle {
  display: none;
  align-items: center;
  justify-content: center;
  /* margin-inline-start:auto pushes it to the trailing edge in BOTH directions. */
  margin-inline-start: auto;
  inline-size: 2.75rem;
  block-size: 2.75rem;
  border-radius: var(--radius-md);
  transition: background-color var(--transition);
}

.nav-toggle .icon {
  inline-size: 1.6rem;
  block-size: 1.6rem;
}

@media (hover: hover) {
  .nav-toggle:hover {
    background-color: var(--color-surface-alt);
  }
}

.nav-toggle__close,
.nav-toggle[aria-expanded="true"] .nav-toggle__open {
  display: none;
}

.nav-toggle[aria-expanded="true"] .nav-toggle__close {
  display: block;
}

.site-nav {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  margin-inline-start: auto;
}

.site-nav__list,
.site-nav__langs {
  display: flex;
  align-items: center;
  gap: var(--space-md);
}

/* The underline grows from the element rather than appearing whole: a 2px border that
   toggles is a state change, an underline that draws itself is an interaction. */
.site-nav__link {
  position: relative;
  display: inline-block;
  padding-block: var(--space-2xs);
  font-weight: var(--weight-medium);
  text-decoration: none;
  transition: color var(--transition);
}

.site-nav__link::after {
  content: "";
  position: absolute;
  inset-inline: 0;
  inset-block-end: 0;
  block-size: 2px;
  background-color: var(--color-brand);
  border-radius: var(--radius-pill);
  transform: scaleX(0);
  transform-origin: center;
  transition: transform var(--transition);
}

.site-nav__link.is-current::after {
  transform: scaleX(1);
}

@media (hover: hover) {
  .site-nav__link:hover::after {
    transform: scaleX(1);
  }
}

.site-nav__lang {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3xs);
  padding-block: var(--space-2xs);
  font-size: var(--step--1);
  font-weight: var(--weight-medium);
  text-decoration: none;
  color: var(--color-text-muted);
  transition: color var(--transition);
}

@media (hover: hover) {
  .site-nav__lang:hover {
    color: var(--color-text);
  }
}

.site-header__cta {
  flex: none;
}

/* PHYSICAL: a viewport media query is measured in physical pixels because a viewport
   is a physical thing. There is no logical equivalent and none is needed — the layout
   inside the query is still written in logical properties. */
@media (max-width: 60rem) {
  .nav-toggle {
    display: inline-flex;
  }

  .site-header__cta .btn__label {
    display: none;
  }

  .site-nav {
    position: absolute;
    inset-inline: 0;
    inset-block-start: 100%;
    display: none;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    padding: var(--space-sm) var(--gutter) var(--space-md);
    background-color: var(--color-surface);
    border-block-end: var(--border-hairline);
    box-shadow: var(--shadow-lg);
  }

  .site-nav.is-open {
    display: flex;
  }

  .site-nav__list,
  .site-nav__langs {
    flex-direction: column;
    align-items: stretch;
    gap: 0;
  }

  .site-nav__link,
  .site-nav__lang {
    padding-block: var(--space-xs);
    border-block-end: var(--border-hairline);
  }

  .site-nav__link::after {
    display: none;
  }

  .site-header__inner {
    position: relative;
  }
}

/* -----------------------------------------------------------------------------
   BREADCRUMB
   -------------------------------------------------------------------------- */
.breadcrumb {
  padding-block: var(--space-xs);
  font-size: var(--step--1);
  background-color: var(--color-surface);
  border-block-end: var(--border-hairline);
}

.breadcrumb__list {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2xs);
  color: var(--color-text-muted);
}

/* The separator is generated content: never in the DOM, never announced by a screen
   reader, never in need of translation, and mirrored automatically because it sits at
   the inline start of the following item. */
.breadcrumb__item + .breadcrumb__item::before {
  content: var(--breadcrumb-separator);
  margin-inline-end: var(--space-2xs);
  opacity: 0.55;
}

.breadcrumb__link {
  text-decoration: none;
  transition: color var(--transition);
}

@media (hover: hover) {
  .breadcrumb__link:hover {
    color: var(--color-text);
    text-decoration: underline;
  }
}

.breadcrumb__current {
  color: var(--color-text);
  font-weight: var(--weight-medium);
}

/* -----------------------------------------------------------------------------
   HERO — THE PHOTOGRAPHIC FIRST SCREEN

   This is the change that answers "why no images? website with no images looks
   horrible". The hero is now a full-bleed photograph with the copy set over it,
   because on a local trade site the photograph of real work IS the proof, and a flat
   colour field with a heading on it is a wireframe.

   THREE TREATMENTS FROM ONE PIECE OF MARKUP:

     .hero              no photograph — a layered, textured gradient built from brand
                        tokens. It has to look CHOSEN, not broken, because plenty of
                        clients hand over no usable photography on day one.
     .hero--photo       a photograph behind the copy, veiled by --hero-scrim. The
                        partial adds this class only inside the has_hero_image gate,
                        so it can never be on without an image behind it.
     .hero--split       photograph beside the copy, for a client whose imagery is
                        portrait or whose brand wants a lighter first screen. Opt-in
                        by overriding the partial; the markup does not change.

   CONTRAST IS GUARANTEED, NOT HOPED FOR. See the scrim arithmetic in the token block:
   every stop in --hero-scrim is at least 0.58 alpha, which puts --color-text-inverse
   above 5:1 over a pure-white photograph — the worst case that exists. No per-image
   testing, no "it looked fine on the one we tried".

   orig: the reference set a --hero-bg custom property containing a url() inside a
   style attribute on this section — an un-auditable URL nested in an attribute inside
   a CSS declaration, invisible to every link and asset checker. The image here is an
   <img> the browser can discover, prioritise and lazy-load by its own rules.
   -------------------------------------------------------------------------- */
.hero {
  position: relative;
  isolation: isolate;
  padding-block: var(--section-space-loose);
  color: var(--color-text-inverse);
  background-color: var(--color-brand-dark);
  background-image: var(--hero-texture), var(--hero-gradient);
}

/* A page that is not selling — a policy page, a 404 — reuses this same partial as a
   plain page header, with has_hero_actions false. It should not wear the full-height
   first screen of a landing page, so the band collapses to ordinary section rhythm
   when there is no action in it. One rule, no second partial, no context key. */
.hero:not(:has(.hero__actions)) {
  padding-block: var(--section-space);
}

/* THE FIRST-SCREEN BUDGET, AS A RULE RATHER THAN AS A HOPE.

   00-design-law.md 4.7: the hero fits 375 x 560 CSS px, and the copy panel's top edge
   sits in the UPPER 40% of the first screen. taste-skill caps hero top padding at about
   6rem; combo's hero is a full-bleed photograph with the copy sitting inside the padded
   band rather than floating down an empty viewport, so the cap that matters is not the
   padding number but where the panel lands.

   `--section-space` rather than `--section-space-loose`, plus a min block size with rem
   bounds, plus `align-items: end` below, is what produces it: the band is at least
   26rem tall, the copy sits at the bottom of it, and on a 560px screen the panel's top
   edge is around 45% down — with the trust strip's first hairline still visible, which
   is the scroll affordance 9.F bans a scroll cue for.

   The no-photograph hero keeps the looser rhythm, because a gradient band with a
   headline on it needs the air that a photograph supplies for free. */
.hero--photo:not(.hero--split) {
  min-block-size: var(--hero-min-block-size);
  padding-block: var(--section-space);
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
}

.hero__inner {
  position: relative;
  z-index: 1;
  display: grid;
  gap: var(--space-xl);
  align-items: center;
  /* The container's auto inline margins survive `inline-size: 100%` from the flex
     parent; `width: 100%` here would fight it. */
  inline-size: 100%;
}

/* THE PHOTOGRAPH LAYER. Absolutely positioned so it is a background in effect and an
   <img> in fact: discoverable, preloadable, lazy-loadable, and visible to every asset
   checker in the audit. */
.hero--photo .hero__media {
  position: absolute;
  inset: 0;
  z-index: 0;
  margin: 0;
  /* What is behind the photograph for the milliseconds before it decodes, and what is
     there if the file is ever missing. A brand-toned plate, not a grey hole. */
  background-color: var(--color-brand-dark);
  background-image: var(--media-placeholder);
}

.hero--photo .hero__media img {
  inline-size: 100%;
  block-size: 100%;
  border-radius: 0;
  box-shadow: none;
  object-fit: cover;
  /* PHYSICAL: a point inside a raster image. Photographs never mirror. */
  object-position: var(--media-position);
}

/* The veil. A pseudo-element rather than a wrapper div: no extra DOM, nothing for a
   screen reader to skip, and it cannot be forgotten by a template that reorders. */
.hero--photo .hero__media::after {
  content: "";
  position: absolute;
  inset: 0;
  background-image: var(--hero-scrim);
}

.hero--photo {
  background-image: none;
  background-color: var(--color-surface-inverse);
}

/* On the photographic hero the copy column is deliberately NARROW and set to the inline
   start. A headline that runs the full width of a 1200px photograph has no shape; one
   that occupies about 60% of it has a silhouette, and the remaining 40% is where the
   photograph gets to be a photograph.

   THE CONSTRAINT IS ON THE TEXT, NOT ON THE CONTAINER. `.hero__inner` also carries
   `.container`, which is what aligns the hero's copy with every other section's gutter
   down the page. Narrowing the container itself would pin the headline to the viewport
   edge on a wide screen and break that alignment — the copy would start in a different
   place from the section heading directly beneath it, which is the kind of half-degree
   error that reads as "cheap" without anyone being able to say why. */
.hero--photo .hero__text,
.hero:not(.hero--split) .hero__text {
  max-inline-size: 52rem;
  text-align: start;
}

/* THE COPY PANEL — the bounded plate the hero's words stand on.

   00-design-law.md 10.A names this as the shipped pattern ("full-bleed photographic
   hero with an offset copy panel"), and 5.E states the condition it exists under: THE
   CONTRAST GUARANTEE NEVER DEPENDS ON THE BLUR. The plate is a solid rectangle; the
   blur is decoration that an engine may drop without costing legibility.

   THE ARITHMETIC, because two scrims stack and somebody will otherwise assume they
   cancel. The band veil's LIGHTEST stop is --scrim-floor at 0.58 alpha. The plate is
   also --scrim-floor. Composited in sRGB the two leave (1 - 0.58) x (1 - 0.58) = 0.176
   of the photograph, which is an effective alpha of 0.82 — exactly --scrim-strong,
   which is what 10.A specifies for this panel. So the copy area lands on the strong
   scrim, the other 40% of the band stays at the band veil and gets to be a photograph,
   and every stop involved is at or above the 0.55 floor `scrim-check.mjs` enforces. No
   token below the floor was invented to make this look better.

   BELOW 48rem THE PLATE IS NOT DRAWN. On a 375 x 560 first screen the panel's own
   padding is 48px of a 453px budget, and it buys nothing: at that width the copy
   already spans the band, so a plate around it is a rectangle with no silhouette. The
   band veil alone carries AA there, which it was built to do. This is the declared
   single-column form 4.7 requires beside every rule inside a min-width query. */
@media (min-width: 48rem) {
  .hero--photo:not(.hero--split) .hero__panel {
    padding: var(--space-lg);
    background-color: var(--scrim-floor);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-ring);
  }

  @supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
    .hero--photo:not(.hero--split) .hero__panel {
      backdrop-filter: blur(10px) saturate(1.05);
      -webkit-backdrop-filter: blur(10px) saturate(1.05);
    }
  }
}

.hero__eyebrow,
.hero__badge {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3xs);
  margin-block-end: var(--space-sm);
  padding-block: var(--space-3xs);
  padding-inline: var(--space-xs);
  font-size: var(--step--1);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-label);
  text-transform: var(--label-transform);
  color: var(--color-text-inverse);
  background-color: hsl(0 0% 100% / 0.14);
  border: 1px solid hsl(0 0% 100% / 0.28);
  border-radius: var(--radius-pill);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}

.hero__title {
  /* THE DOMINANT ELEMENT ON THE PAGE. step-6, tight leading, negative tracking —
     the three things that separate a headline from large body text. */
  font-size: var(--step-6);
  font-weight: var(--weight-display);
  line-height: var(--leading-display);
  letter-spacing: var(--tracking-display);
  text-wrap: balance;
  /* Legibility insurance where a bright patch of photograph meets a light glyph.
     PHYSICAL: a text-shadow offset, kept vertical for the same reason box-shadow is. */
  text-shadow: 0 1px 2px hsl(var(--shadow-color) / 0.35);
}

.hero__subtitle {
  margin-block-start: var(--space-md);
  max-inline-size: var(--max-width-measure);
  font-size: var(--step-2);
  font-weight: var(--weight-body);
  line-height: 1.45;
  color: inherit;
  opacity: 0.92;
  text-wrap: pretty;
  /* MAX 20 WORDS AND MAX 4 LINES (4.7). Deliberately NOT enforced here with
     `-webkit-line-clamp`. A CSS clamp would truncate a client's sentence mid-word and
     hide the violation instead of reporting it, and the thing that pushes the primary
     action off a 560px screen is a copy defect that has to be fixed in the copy.
     `hero-stack-budget` measures the words with Intl.Segmenter (correct for Arabic and
     CJK) and fails the build. The measure above is what keeps four lines the normal
     outcome: --max-width-measure is 38rem, which is 60-75 Latin characters a line. */
}

.hero__rating {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3xs);
  margin-block-start: var(--space-md);
  padding-block: var(--space-3xs);
  padding-inline: var(--space-xs);
  font-size: var(--step--1);
  font-weight: var(--weight-medium);
  background-color: hsl(0 0% 100% / 0.12);
  border-radius: var(--radius-pill);
}

.hero__rating a {
  color: inherit;
}

.hero__bullets {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2xs) var(--space-md);
  margin-block-start: var(--space-md);
  font-size: var(--step-0);
}

.hero__bullets li {
  display: flex;
  align-items: center;
  gap: var(--space-2xs);
  padding-block: var(--space-3xs);
  padding-inline: var(--space-xs) var(--space-sm);
  background-color: hsl(0 0% 100% / 0.1);
  border: 1px solid hsl(0 0% 100% / 0.2);
  border-radius: var(--radius-pill);
}

.hero__bullets .icon {
  color: inherit;
  opacity: 0.9;
}

/* THE HERO'S PROOF LINE IS ONE LINE. 07 4.2 budgets two chips on a 375px screen and
   three or four from 48rem, and 4.7 caps the list at four anywhere. Both halves are
   drawn here rather than left to whoever fills `hero_bullets`, because the failure mode
   is a fifth chip wrapping to a second row and pushing the primary action below the
   fold on the one screen that pays for the site.

   `display: none` rather than `visibility` or a clip: a chip that is not shown must not
   be announced either, or a screen-reader user hears a claim a sighted user cannot see. */
.hero__bullets li:nth-child(n + 5) {
  display: none;
}

@media (max-width: 48rem) {
  .hero__bullets li:nth-child(n + 3) {
    display: none;
  }
}

.hero__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
  margin-block-start: var(--space-lg);
}

/* On a photograph the ghost button has no light surface to sit on, so it becomes the
   frosted variant automatically. One rule; the template does not have to know. */
.hero .btn--ghost {
  color: var(--color-text-inverse);
  background-color: hsl(0 0% 100% / 0.1);
  border-color: hsl(0 0% 100% / 0.5);
  box-shadow: none;
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}

@media (hover: hover) {
  .hero .btn--ghost:hover {
    background-color: hsl(0 0% 100% / 0.2);
    border-color: hsl(0 0% 100% / 0.8);
  }
}

/* The focus ring has to be visible against a dark photograph, so it inverts here
   rather than being switched off. */
.hero :focus-visible {
  outline-color: var(--color-text-inverse);
}

/* THE SPLIT TREATMENT — photograph beside the copy, on a light band.

   The markup is IDENTICAL to the photographic hero: the media wrapper is a sibling of
   the container, not a grid cell inside it. So the split is built by pinning the media
   to the trailing half of the band rather than by changing the DOM, which is what
   allows one partial to serve three treatments. `inset-inline-end` and the logical
   corner radii mean the photograph lands on the correct side in both writing
   directions, with no mirrored copy of this rule anywhere.

   Below 60rem the split collapses: the photograph returns to normal flow beneath the
   copy, because a 46% image column on a 375px screen is a thumbnail. This flow-state
   rule is declared BEFORE the query that pins it, so the pinned rule's `margin: 0` is
   the later declaration and wins — the reverse order would leave a stray top margin
   pushing an absolutely-positioned photograph off its own band. */
.hero--split .hero__media {
  margin-block-start: var(--space-xl);
}

@media (min-width: 60rem) {
  .hero--split .hero__media {
    position: absolute;
    inset-block: var(--space-lg);
    inset-inline-end: 0;
    inline-size: 46%;
    margin: 0;
  }

  /* `--radius-xl` here and below rather than `--radius-2xl`: the shape lock (4.4) caps
     a page at three radius tokens plus the pill, and the shipped set is lg, md and
     pill. The split hero spends the third on xl; a fourth would mean the page has no
     radius system, only five separate decisions. */
  .hero--split .hero__media img {
    block-size: 100%;
    aspect-ratio: auto;
    border-start-start-radius: var(--radius-xl);
    border-end-start-radius: var(--radius-xl);
    border-start-end-radius: 0;
    border-end-end-radius: 0;
  }

  .hero--split .hero__text {
    max-inline-size: 48%;
  }
}

.hero--split {
  color: var(--color-text);
  background-color: var(--color-surface-alt);
  background-image: none;
}

.hero--split .hero__title {
  text-shadow: none;
}

.hero--split .hero__subtitle {
  color: var(--color-text-muted);
  opacity: 1;
}

.hero--split .hero__eyebrow,
.hero--split .hero__badge {
  color: var(--color-accent-contrast);
  background-color: var(--color-accent);
  border-color: transparent;
}

.hero--split .hero__bullets li {
  background-color: var(--color-surface);
  border-color: var(--color-border);
}

.hero--split .hero__media img {
  inline-size: 100%;
  aspect-ratio: var(--media-ratio-wide);
  border-radius: var(--radius-xl);
  box-shadow: var(--shadow-xl), var(--shadow-ring);
  object-fit: cover;
  object-position: var(--media-position);
}

.hero--split .btn--ghost,
.hero--split :focus-visible {
  color: var(--color-text);
  background-color: var(--color-surface);
  border-color: var(--color-border);
  outline-color: var(--color-focus);
  backdrop-filter: none;
}

/* -----------------------------------------------------------------------------
   TRUST BADGES — the strip under the hero.

   Not a bordered card grid. A single low band of icon-tile-plus-label pairs, divided
   by hairlines, sitting tight under the hero so it reads as part of the first screen
   rather than as the first of nine identical sections.
   -------------------------------------------------------------------------- */
.trust {
  padding-block: var(--space-lg);
  background-color: var(--color-surface);
  border-block-end: var(--border-hairline);
}

.trust__list {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
  gap: var(--space-sm) var(--space-lg);
}

.trust__item {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  padding-inline-end: var(--space-sm);
  font-weight: var(--weight-medium);
  line-height: 1.35;
}

/* A rule between items, suppressed on the first, so the divider never dangles at the
   start of a wrapped row's leading edge. Logical, so it mirrors. */
.trust__item + .trust__item {
  border-inline-start: var(--border-hairline);
  padding-inline-start: var(--space-lg);
}

@media (max-width: 48rem) {
  .trust__item + .trust__item {
    border-inline-start: 0;
    padding-inline-start: 0;
  }
}

/* The tile carries the colour, not the glyph inside it: a rule on the glyph would beat
   the tile's inherited value and quietly undo the component. */
.trust__item .icon-tile {
  color: var(--color-accent);
  background-color: var(--tint-accent);
}

/* -----------------------------------------------------------------------------
   STATS — the figures band.

   Full-bleed inverse, hairline-divided, numerals at display size. The old version was
   four centred columns of ordinary text on a dark rectangle; this one is a band.
   -------------------------------------------------------------------------- */
.stats {
  background-image: radial-gradient(120% 140% at 50% 0%, hsl(0 0% 100% / 0.08) 0%, hsl(0 0% 100% / 0) 60%);
}

.stats__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
  gap: var(--space-lg);
  text-align: center;
}

.stat {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2xs);
  padding-inline: var(--space-sm);
}

.stat + .stat {
  border-inline-start: 1px solid var(--tint-inverse-line-soft);
}

@media (max-width: 48rem) {
  .stat + .stat {
    border-inline-start: 0;
  }
}

.stat__value {
  font-family: var(--font-heading);
  font-size: var(--step-5);
  font-weight: var(--weight-display);
  line-height: 1;
  letter-spacing: var(--tracking-display);
  /* A figure is an LTR run even in RTL text; isolate it so an adjacent + or % does
     not migrate across the number. */
  unicode-bidi: isolate;
}

.stat__label {
  font-size: var(--step--1);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-label);
  text-transform: var(--label-transform);
  opacity: 0.72;
}

/* -----------------------------------------------------------------------------
   PROCESS — a connected sequence, not four boxes.

   The old version put every step in an identical bordered panel, which loses the one
   thing a process has that a list does not: order. Here the steps are joined by a rule
   that runs behind the numbered discs, so the sequence is visible before a word of it
   is read. The rule is drawn with a logical inset pair, so it runs the correct way in
   both directions with no override.
   -------------------------------------------------------------------------- */
.process__grid {
  position: relative;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
  gap: var(--space-xl) var(--space-lg);
}

.step {
  position: relative;
  padding-block-start: var(--space-2xs);
}

/* The connector. Only between items on a row, never trailing off the last one.
   `inset-inline-start: 50%` plus `inset-inline-end: -50%` spans the gap to the next
   disc and mirrors automatically. */
@media (min-width: 48rem) {
  .process__grid > .step::before {
    content: "";
    position: absolute;
    inset-block-start: 1.375rem;
    inset-inline-start: calc(2.75rem + var(--space-2xs));
    inset-inline-end: calc(-1 * var(--space-lg));
    block-size: 2px;
    background-image: linear-gradient(to bottom, var(--color-border), var(--color-border));
    border-radius: var(--radius-pill);
  }

  .process__grid > .step:last-child::before {
    display: none;
  }
}

.step__number {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: 2.75rem;
  block-size: 2.75rem;
  margin-block-end: var(--space-sm);
  font-family: var(--font-heading);
  font-size: var(--step-1);
  font-weight: var(--weight-bold);
  line-height: 1;
  color: var(--color-brand-contrast);
  background-color: var(--color-brand);
  border-radius: var(--radius-pill);
  box-shadow: var(--shadow-md), 0 0 0 6px var(--color-surface);
  unicode-bidi: isolate;
}

.section--alt .step__number {
  box-shadow: var(--shadow-md), 0 0 0 6px var(--color-surface-alt);
}

.step__title {
  font-size: var(--step-2);
  letter-spacing: var(--tracking-display);
  text-wrap: balance;
}

.step__body {
  margin-block-start: var(--space-2xs);
  color: var(--color-text-muted);
  text-wrap: pretty;
}

/* -----------------------------------------------------------------------------
   PANEL — the generic material surface.

   This is what replaces `border: 1px solid var(--color-border)` everywhere it was
   doing the work of a card. A panel has a surface, a ring, a shadow and a radius; a
   bordered rectangle has a line. The difference is the whole complaint.
   -------------------------------------------------------------------------- */
.panel {
  position: relative;
  padding: var(--space-lg);
  background-color: var(--color-surface);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm), var(--shadow-ring);
  transition: box-shadow var(--transition), transform var(--transition);
}

.panel--flush {
  padding: 0;
  overflow: hidden;
}

.panel--raised {
  box-shadow: var(--shadow-lg), var(--shadow-ring);
}

.section--inverse .panel,
.cta .panel {
  background-color: var(--tint-inverse-raise);
  box-shadow: inset 0 0 0 1px var(--tint-inverse-line);
}

/* -----------------------------------------------------------------------------
   FAQ — native <details>, no script required
   -------------------------------------------------------------------------- */
.faq__list {
  display: grid;
  gap: var(--space-2xs);
}

.faq__item {
  padding-inline: var(--space-md);
  background-color: var(--color-surface);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-xs), var(--shadow-ring);
  transition: box-shadow var(--transition), background-color var(--transition);
}

.faq__item[open] {
  box-shadow: var(--shadow-md), var(--shadow-ring);
}

@media (hover: hover) {
  .faq__item:hover {
    box-shadow: var(--shadow-sm), var(--shadow-ring-strong);
  }
}

.faq__summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm);
  padding-block: var(--space-md);
  cursor: pointer;
  list-style: none;
}

/* Two vendor forms of the same suppression; neither has a standard alternative. */
.faq__summary::-webkit-details-marker {
  display: none;
}

.faq__summary::marker {
  content: "";
}

.faq__summary:focus-visible {
  outline-offset: calc(-1 * var(--focus-ring-width));
  border-radius: var(--radius-lg);
}

.faq__question {
  font-size: var(--step-1);
  font-weight: var(--weight-semibold);
  line-height: 1.35;
}

.faq__marker {
  flex: none;
  color: var(--color-text-muted);
  transition: transform var(--transition), color var(--transition);
}

.faq__item[open] .faq__marker {
  /* A chevron rotating about its own centre is direction-neutral. */
  transform: rotate(180deg);
  color: var(--color-brand);
}

.faq__answer {
  padding-block-end: var(--space-md);
  max-inline-size: var(--max-width-measure);
  color: var(--color-text-muted);
}

/* -----------------------------------------------------------------------------
   CTA — the closing ask.

   Full-bleed brand band with a light wash across the top, so it terminates the page
   rather than being the ninth panel on it.
   -------------------------------------------------------------------------- */
.cta {
  color: var(--color-text-inverse);
  background-color: var(--color-brand);
  background-image: radial-gradient(100% 140% at 50% 0%, hsl(0 0% 100% / 0.12) 0%, hsl(0 0% 100% / 0) 62%),
    linear-gradient(to bottom, var(--color-brand) 0%, var(--color-brand-dark) 100%);
}

.cta__inner {
  text-align: center;
}

.cta__title {
  font-size: var(--step-4);
  letter-spacing: var(--tracking-display);
  text-wrap: balance;
}

.cta__body {
  margin-block-start: var(--space-sm);
  font-size: var(--step-1);
  opacity: 0.85;
  text-wrap: pretty;
}

.cta__actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-xs);
  margin-block-start: var(--space-lg);
}

/* On the brand band the primary button inverts: the page's strongest colour is now the
   background, so the strongest button is the one made of the contrast colour. */
.cta .btn--primary {
  color: var(--color-brand);
  background-color: var(--color-brand-contrast);
  background-image: none;
  box-shadow: var(--shadow-lg);
}

.cta .btn--ghost {
  color: var(--color-text-inverse);
  background-color: hsl(0 0% 100% / 0.08);
  border-color: hsl(0 0% 100% / 0.55);
  box-shadow: none;
}

.cta :focus-visible {
  outline-color: var(--color-text-inverse);
}

@media (hover: hover) {
  .cta .btn--primary:hover {
    background-color: var(--color-brand-contrast);
    filter: brightness(0.95);
  }

  .cta .btn--ghost:hover {
    background-color: hsl(0 0% 100% / 0.18);
    border-color: var(--color-text-inverse);
  }
}

/* -----------------------------------------------------------------------------
   CARDS — the services grid, and every other linked tile.

   THE EDITORIAL GRID. `.card-grid--editorial` promotes the first card to a large
   feature spanning two columns and two rows, with its title set over the photograph.
   It is pure CSS keyed on :first-child, so it needs no new context key and no change
   to the loop — the template stays a plain list and the layout stops being four
   identical boxes.

   orig: `.card` was a white fill, a one-pixel grey border and a radius. Four
   of those in a row is the default output of every wireframe tool ever made, which is
   exactly why it reads as unfinished.
   -------------------------------------------------------------------------- */
.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
  gap: var(--space-md);
}

/* THE CELL COUNT IS ARITHMETIC, AND IT IS THE ONE THING THIS GRID GETS WRONG QUIETLY.

   `--editorial` is four columns with the first card spanning 2 x 2. Auto-placement is
   sparse, so the feature takes rows 1-2 of columns 1-2, cards 2-5 fill rows 1-2 of
   columns 3-4, and cards 6 onward fill whole four-column rows. THE GRID FILLS EXACTLY
   WHEN N is 5, 9 or 13 — N ≡ 1 (mod 4). Six cards, which is
   `seo.interlinking.siblingServicesMax`'s shipped default, leaves one quarter-width
   card alone in row 3 with three empty cells beside it. taste-skill: "if your grid has
   an empty cell in the middle or at the end, you planned wrong."

   The fix is a config default, not CSS: siblingServicesMax becomes 5. `grid-cell-count`
   fails the build otherwise. Any other count must drop `--editorial` for plain
   `.card-grid`, which is auto-fill and therefore never holes.

   THE TRIO, AND WHY IT IS AUTOMATIC. Plain `.card-grid` at exactly three items produces
   three equal feature cards, which is 9.C tell 1 — the single most recognisable AI
   layout there is, and the one the operator saw. `.card-grid--trio` is the composed
   answer: one card takes the full-height lead column and the other two stack beside it.

   A template cannot count its own list, so a class alone would mean every context
   builder in three tracks remembering to add it at N=3 and remove it at N=4. THE
   SELECTOR COUNTS INSTEAD: the `@supports selector(:has(*))` block at the end of this
   group applies the trio to ANY `.card-grid` whose third child is also its last, which
   makes the banned three-across row unreachable rather than merely discouraged. An
   engine without `:has()` skips the block and gets three equal cards, which is the
   pre-existing behaviour and not a regression.

   The `:has()` rules are kept in their OWN block and never grouped in a selector list
   with the plain ones. A comma list containing a selector an engine cannot parse is
   invalid as a whole in most engines, so grouping them would take the editorial grid
   down with them on exactly the old browsers that need it most. */
@media (min-width: 60rem) {
  .card-grid--trio {
    grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
    grid-auto-rows: minmax(9rem, auto);
  }

  .card-grid--trio > li:first-child {
    grid-row: span 2;
  }

  .card-grid--trio > li:first-child .card {
    min-block-size: 100%;
  }

  .card-grid--trio > li:first-child .card__media {
    position: absolute;
    inset: 0;
  }

  .card-grid--trio > li:first-child .card__media img {
    block-size: 100%;
    aspect-ratio: auto;
  }

  .card-grid--trio > li:first-child .card__media::after {
    content: "";
    position: absolute;
    inset: 0;
    background-image: var(--caption-scrim);
  }

  .card-grid--trio > li:first-child .card__body {
    position: relative;
    z-index: 1;
    margin-block-start: auto;
    color: var(--color-text-inverse);
  }

  .card-grid--trio > li:first-child .card__title {
    font-size: var(--step-2);
  }

  .card-grid--trio > li:first-child .card__arrow {
    color: inherit;
  }
}

@media (min-width: 60rem) {
  .card-grid--editorial {
    grid-template-columns: repeat(4, minmax(0, 1fr));
    grid-auto-rows: minmax(11rem, auto);
  }

  .card-grid--editorial > li:first-child {
    grid-column: span 2;
    grid-row: span 2;
  }

  .card-grid--editorial > li:first-child .card {
    /* The feature card's photograph fills the whole tile and the label sits on it. */
    min-block-size: 100%;
  }

  .card-grid--editorial > li:first-child .card__media {
    position: absolute;
    inset: 0;
  }

  .card-grid--editorial > li:first-child .card__media img {
    block-size: 100%;
    aspect-ratio: auto;
  }

  .card-grid--editorial > li:first-child .card__media::after {
    content: "";
    position: absolute;
    inset: 0;
    background-image: var(--caption-scrim);
  }

  .card-grid--editorial > li:first-child .card__body {
    position: relative;
    z-index: 1;
    margin-block-start: auto;
    color: var(--color-text-inverse);
  }

  .card-grid--editorial > li:first-child .card__title {
    font-size: var(--step-2);
  }

  .card-grid--editorial > li:first-child .card__arrow {
    color: inherit;
  }
}

/* THE AUTO-TRIO. Exactly three cards in any card grid, editorial or plain, become the
   composed trio. Declared last and separately, for the two reasons in the comment
   above: source order plus higher specificity means it beats the editorial promotion on
   a three-card grid, and the @supports guard means an engine without `:has()` never
   sees a selector it cannot parse. */
@media (min-width: 60rem) {
  @supports selector(:has(*)) {
    .card-grid:has(> li:nth-child(3):last-child) {
      grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
      grid-auto-rows: minmax(9rem, auto);
    }

    .card-grid:has(> li:nth-child(3):last-child) > li:first-child {
      grid-column: auto;
      grid-row: span 2;
    }

    .card-grid:has(> li:nth-child(3):last-child) > li:first-child .card {
      min-block-size: 100%;
    }

    .card-grid:has(> li:nth-child(3):last-child) > li:first-child .card__media {
      position: absolute;
      inset: 0;
    }

    .card-grid:has(> li:nth-child(3):last-child) > li:first-child .card__media img {
      block-size: 100%;
      aspect-ratio: auto;
    }

    .card-grid:has(> li:nth-child(3):last-child) > li:first-child .card__media::after {
      content: "";
      position: absolute;
      inset: 0;
      background-image: var(--caption-scrim);
    }

    .card-grid:has(> li:nth-child(3):last-child) > li:first-child .card__body {
      position: relative;
      z-index: 1;
      margin-block-start: auto;
      color: var(--color-text-inverse);
    }

    .card-grid:has(> li:nth-child(3):last-child) > li:first-child .card__title {
      font-size: var(--step-2);
    }

    .card-grid:has(> li:nth-child(3):last-child) > li:first-child .card__arrow {
      color: inherit;
    }
  }
}

.card {
  position: relative;
  display: flex;
  flex-direction: column;
  block-size: 100%;
  overflow: hidden;
  text-decoration: none;
  background-color: var(--color-surface);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm), var(--shadow-ring);
  transition: box-shadow var(--transition), transform var(--transition);
}

.card__media {
  position: relative;
  overflow: hidden;
  background-color: var(--color-surface-alt);
  background-image: var(--media-placeholder);
}

.card__media img {
  inline-size: 100%;
  aspect-ratio: var(--media-ratio-card);
  object-fit: cover;
  object-position: var(--media-position);
  transition: transform var(--transition-slow);
}

/* THE NO-PHOTOGRAPH CARD. A client rarely has a photograph for every service on day
   one, and a card with an empty media box is worse than one that was designed without
   it. `.card--plate` is emitted by the template inside an inverted section on the
   image key, so the fallback is a decision in the markup rather than a hole. */
.card--plate {
  color: var(--color-text-inverse);
  background-color: var(--color-brand);
  background-image: radial-gradient(90% 120% at 100% 0%, hsl(0 0% 100% / 0.16) 0%, hsl(0 0% 100% / 0) 55%),
    var(--hero-texture);
  min-block-size: 9rem;
}

.card--plate .card__body {
  margin-block-start: auto;
}

.card--plate .card__arrow {
  color: inherit;
}

.card__body {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2xs);
  padding: var(--space-md);
}

.card__title {
  font-family: var(--font-heading);
  font-size: var(--step-1);
  font-weight: var(--weight-semibold);
  line-height: 1.3;
  text-wrap: balance;
}

.card__meta {
  display: block;
  margin-block-start: var(--space-3xs);
  font-size: var(--step--1);
  font-weight: var(--weight-body);
  color: var(--color-text-muted);
}

/* THE CONTENT CARD, as distinct from the link card above.

   `.card__body` is a ROW: a title on one side and an arrow on the other, which is what a
   card that is entirely a link needs. The interior tracks build a different object — an
   icon, a heading and a paragraph of prose, stacked — and they were reaching for
   `card__body` because it was the only padded box on offer. In a flex row, that laid the
   icon, the heading and the paragraph out side by side.

   `.card__content` is the stack. Everything below is expressed in the same tokens as the
   row, so the two card kinds share padding, rhythm and type scale and read as one family
   in a grid that mixes them. */
.card__content {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-2xs);
  padding: var(--space-md);
}

/* The icon sits above the heading at the size the sprite was drawn for. `flex: none` so
   a long heading cannot squeeze it, and the brand colour so a grid of these reads as
   deliberate rather than as grey furniture. */
.card__icon {
  flex: none;
  inline-size: var(--icon-size-xl);
  block-size: var(--icon-size-xl);
  margin-block-end: var(--space-3xs);
  color: var(--color-brand);
}

.card__text {
  margin: 0;
  color: var(--color-text-muted);
  line-height: var(--leading-body);
  text-wrap: pretty;
}

.card__arrow {
  flex: none;
  color: var(--color-text-muted);
  transition: transform var(--transition), color var(--transition);
}

@media (hover: hover) {
  .card:hover {
    transform: translateY(var(--lift-md));
    box-shadow: var(--shadow-lg), var(--shadow-ring);
  }

  /* The photograph pushes in slightly under the fixed frame. This is the one motion
     in the file that people notice and nobody can name, and it is what separates a
     card that responds from a card that merely changes colour. */
  .card:hover .card__media img {
    transform: scale(var(--media-zoom));
  }

  .card:hover .card__arrow {
    color: var(--color-brand);
    /* Travelling along the reading direction, so it must mirror. `translate` with a
       logical value does not exist; the RTL rule below is the flip. */
    transform: translateX(3px);
  }

  /* PHYSICAL: the paired mirror of the hover nudge above. Grouped with the other
     directional transform rather than left to chance. */
  [dir="rtl"] .card:hover .card__arrow {
    transform: translateX(-3px);
  }

  .card--plate:hover .card__arrow {
    color: inherit;
  }
}

.card:active {
  transform: translateY(0);
  box-shadow: var(--shadow-sm), var(--shadow-ring);
}

.card:focus-visible {
  outline: var(--focus-ring-width) solid var(--color-focus);
  outline-offset: var(--focus-ring-offset);
}

/* The single-dash spellings `card-media` / `card-body` / `card-title` / `card-link` /
   `card-icon` / `section-title` were aliased here so the two generated tracks would pick
   up the design at all. They have been renamed to the BEM spelling at the source in
   service-pages/templates/ and service-area-pages/templates/, so the aliases are gone.
   The section contract in templates/README.md section 8 is the one spelling. */

/* -----------------------------------------------------------------------------
   PILLS AND CAROUSEL
   -------------------------------------------------------------------------- */
.pills {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2xs);
}

.pill {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3xs);
  min-block-size: 2.5rem;
  padding-block: var(--space-2xs);
  padding-inline: var(--space-sm);
  font-size: var(--step-0);
  font-weight: var(--weight-medium);
  text-decoration: none;
  white-space: nowrap;
  background-color: var(--color-surface);
  border-radius: var(--radius-pill);
  box-shadow: var(--shadow-xs), var(--shadow-ring);
  transition: background-color var(--transition), color var(--transition),
    box-shadow var(--transition), transform var(--transition);
}

@media (hover: hover) {
  .pill:hover {
    color: var(--color-brand-contrast);
    background-color: var(--color-brand);
    box-shadow: var(--shadow-md);
    transform: translateY(var(--lift-sm));
  }
}

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

.carousel {
  display: flex;
  align-items: center;
  gap: var(--space-2xs);
}

.carousel__track {
  flex: 1;
  flex-wrap: nowrap;
  /* PHYSICAL: `overflow-inline` exists in the spec but is not implemented widely
     enough to rely on. `overflow-x` is the physical axis, which happens to coincide
     with the inline axis for every horizontal writing mode — including RTL, where the
     browser handles the scroll origin itself. assets/js/site.js is the one place that
     has to know the difference, and it asks getComputedStyle rather than guessing. */
  overflow-x: auto;
  scroll-snap-type: x proximity;
  scroll-padding-inline: var(--space-2xs);
  padding-block: var(--space-2xs);
  padding-inline: var(--space-3xs);
  scrollbar-width: thin;
  overscroll-behavior-inline: contain;
}

.carousel__track > li {
  scroll-snap-align: start;
}

.carousel__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  inline-size: 2.75rem;
  block-size: 2.75rem;
  background-color: var(--color-surface);
  border-radius: var(--radius-pill);
  box-shadow: var(--shadow-sm), var(--shadow-ring);
  transition: background-color var(--transition), box-shadow var(--transition);
}

.carousel__btn[hidden] {
  display: none;
}

@media (hover: hover) {
  .carousel__btn:hover {
    background-color: var(--color-surface-alt);
    box-shadow: var(--shadow-md);
  }
}

/* -----------------------------------------------------------------------------
   GALLERY — proof that the work is real.

   For a trade business this band is worth more than any adjective on the page: a
   photograph of the crew, the van, the equipment, the drain before and the drain
   after. It is deliberately NOT a uniform grid — the first frame is large and the rest
   step down, which is what makes it read as a portfolio rather than a contact sheet.

   The before/after pair is one figure holding two images side by side with a hairline
   between them and a label on each. No slider, no JavaScript: a slider hides half the
   evidence behind an interaction most visitors never perform.
   -------------------------------------------------------------------------- */
.gallery__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: var(--space-md);
}

@media (min-width: 60rem) {
  .gallery__grid--editorial {
    grid-template-columns: repeat(6, minmax(0, 1fr));
  }

  .gallery__grid--editorial > .gallery__item {
    grid-column: span 2;
  }

  .gallery__grid--editorial > .gallery__item:first-child {
    grid-column: span 4;
    grid-row: span 2;
  }

  .gallery__grid--editorial > .gallery__item:first-child .gallery__figure {
    block-size: 100%;
  }

  .gallery__grid--editorial > .gallery__item:first-child img {
    block-size: 100%;
    aspect-ratio: auto;
  }
}

.gallery__item {
  min-inline-size: 0;
}

/* THE MEDIA FRAME — the generic framed photograph, and the gallery's own figure, from
   ONE declaration.

   00-design-law.md 10.A lists "media frame with hover zoom under a fixed frame" as a
   shipped pattern. It was shipped, but only as `.gallery__figure`, which meant any band
   other than the gallery that wanted a framed photograph had to either borrow a gallery
   class or write a second one that would drift. `.media-frame` is the primitive; the
   gallery figure is the same object under its own name, declared here together so the
   two cannot diverge.

   The zoom is the one motion on the page that people notice and nobody can name: the
   image pushes in slightly under a frame that does not move, which reads as depth
   rather than as an animation. --media-zoom is 1 at `design.motion` 1-3. */
.media-frame,
.gallery__figure {
  position: relative;
  overflow: hidden;
  margin: 0;
  background-color: var(--color-surface-alt);
  background-image: var(--media-placeholder);
  border-radius: var(--media-radius);
  box-shadow: var(--shadow-md), var(--shadow-ring);
}

.media-frame img,
.gallery__figure img {
  inline-size: 100%;
  aspect-ratio: var(--media-ratio-wide);
  object-fit: cover;
  object-position: var(--media-position);
  transition: transform var(--transition-slow);
}

.media-frame--card img {
  aspect-ratio: var(--media-ratio-card);
}

.media-frame--portrait img {
  aspect-ratio: var(--media-ratio-portrait);
}

@media (hover: hover) {
  .media-frame:hover img,
  .gallery__figure:hover img {
    transform: scale(var(--media-zoom));
  }
}

/* The caption sits ON the photograph, under a scrim, so the band stays a wall of
   imagery instead of alternating image and text bars. */
.gallery__caption {
  position: absolute;
  inset-inline: 0;
  inset-block-end: 0;
  padding: var(--space-lg) var(--space-md) var(--space-md);
  font-size: var(--step--1);
  font-weight: var(--weight-medium);
  color: var(--color-text-inverse);
  background-image: var(--caption-scrim);
}

/* BEFORE / AFTER. Two frames, one hairline, two labels. */
.gallery__pair {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 2px;
}

.gallery__pair-half {
  position: relative;
  overflow: hidden;
  background-color: var(--color-surface-alt);
  background-image: var(--media-placeholder);
}

.gallery__pair-half img {
  inline-size: 100%;
  aspect-ratio: var(--media-ratio-card);
  object-fit: cover;
  object-position: var(--media-position);
}

.gallery__label {
  position: absolute;
  inset-inline-start: var(--space-2xs);
  inset-block-start: var(--space-2xs);
  padding-block: var(--space-3xs);
  padding-inline: var(--space-xs);
  font-size: var(--step--1);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-label);
  text-transform: var(--label-transform);
  color: var(--color-text-inverse);
  background-color: var(--scrim-strong);
  border-radius: var(--radius-pill);
}

/* -----------------------------------------------------------------------------
   WORK EVIDENCE — the proof band, and the rung the trust hierarchy was missing.

   Every landing-page source combo read recommends a "trusted by" logo wall. A domestic
   trade has no client logos; a commercial one usually may not publish its clients'
   marks, because a company logo is its trademark. 00-design-law.md 4.8 deletes the
   section outright and names the replacement: PHOTOGRAPHIC EVIDENCE OF THE WORK. That
   is this band, and PRECEDENCE row 3 promotes it from a design idea to `required: when`,
   because without it a new client's page ships with no proof band at all, which the
   same section calls incomplete work.

   IT IS A DISTINCT LAYOUT FAMILY, WHICH IS THE POINT. The page already has an
   asymmetric card grid, an asymmetric value grid and a drawn sequence. This is paired
   frames on a common baseline: same aspect, labelled, no panel around them, on the
   band's own inverse ground. 4.7 requires at least four families across a page and
   forbids two bands sharing one; this is the family the shipped page was short of.

   ONE INVERSE BAND, AND THIS IS IT. 4.11 permits `.section--inverse` on the footer plus
   at most one other band. Spending it here is deliberate: a full-bleed dark band under
   photographs is the composition the page has been building toward, it is where a
   contrasting ground actually helps the images, and it is never adjacent to
   `.section--tint` in the shipped order.

   A pair and a single frame sit in the same grid because they are the same object at
   two widths — a pair is one piece of evidence told in two frames, not two pieces. */
.work-evidence__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr));
  gap: var(--space-md);
  align-items: start;
}

@media (min-width: 60rem) {
  /* A pair carries twice the information and takes twice the width, so a row of three
     singles and a row of one pair plus one single both read as one composed band rather
     than as a contact sheet. */
  .work-evidence__item--pair {
    grid-column: span 2;
  }
}

.work-evidence__figure {
  margin: 0;
}

.work-evidence__frame {
  position: relative;
  overflow: hidden;
  background-color: var(--color-surface-inverse);
  background-image: var(--media-placeholder);
  border-radius: var(--media-radius);
  /* On an inverse ground a drop shadow is invisible and a border is a wireframe, so the
     edge is a light inset hairline: the same --shadow-ring idea, inverted. 10 2.3. */
  box-shadow: inset 0 0 0 1px var(--tint-inverse-line);
}

.work-evidence__frame img {
  inline-size: 100%;
  aspect-ratio: var(--media-ratio-wide);
  object-fit: cover;
  object-position: var(--media-position);
  transition: transform var(--transition-slow);
}

.work-evidence__item--pair .work-evidence__frame img {
  aspect-ratio: var(--media-ratio-card);
}

@media (hover: hover) {
  .work-evidence__frame:hover img {
    transform: scale(var(--media-zoom));
  }
}

/* THE CAPTION SITS BELOW THE FRAME, NOT ON IT. 9.F bans pills, tags and photo-credit
   captions overlaid on images; the one permitted text-on-image on this page is the
   editorial card's title, and the one permitted label is the before/after pair's, where
   the label IS the evidence. Everything else is captioned underneath, where it can be
   read and where it does not compete with the photograph. */
.work-evidence__caption {
  margin-block-start: var(--space-2xs);
  font-size: var(--step--1);
  color: inherit;
  opacity: 0.8;
  text-wrap: pretty;
}

/* -----------------------------------------------------------------------------
   WHY-US — the differentiation band, asymmetric on purpose.

   The lead point gets a wide panel and a larger tile; the rest sit in a column beside
   it as rows divided by hairlines. Same loop, same markup, no new context key —
   the asymmetry is entirely in :first-child.
   -------------------------------------------------------------------------- */
.why-us__grid {
  display: grid;
  gap: var(--space-md);
}

.why-us__point {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: var(--space-sm) var(--space-md);
  align-items: start;
  padding: var(--space-lg);
  background-color: var(--color-surface);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm), var(--shadow-ring);
  transition: box-shadow var(--transition), transform var(--transition);
}

.why-us__point .step__title,
.why-us__point .step__body {
  grid-column: 2;
}

.why-us__point .step__title {
  margin-block-start: var(--space-3xs);
  font-size: var(--step-1);
}

@media (hover: hover) {
  .why-us__point:hover {
    transform: translateY(var(--lift-sm));
    box-shadow: var(--shadow-md), var(--shadow-ring);
  }

  .why-us__point:hover .icon-tile {
    color: var(--color-brand-contrast);
    background-color: var(--color-brand);
  }
}

@media (min-width: 60rem) {
  .why-us__grid {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }

  .why-us__grid > .why-us__point:first-child {
    grid-column: span 2;
    grid-row: span 2;
    grid-template-columns: minmax(0, 1fr);
    align-content: start;
    padding: var(--space-xl);
    background-image: radial-gradient(80% 120% at 100% 0%, var(--tint-brand) 0%, transparent 62%);
  }

  .why-us__grid > .why-us__point:first-child .step__title,
  .why-us__grid > .why-us__point:first-child .step__body {
    grid-column: 1;
  }

  .why-us__grid > .why-us__point:first-child .step__title {
    font-size: var(--step-3);
  }

  .why-us__grid > .why-us__point:first-child .step__body {
    font-size: var(--step-1);
  }

  .why-us__grid > .why-us__point:first-child .icon-tile {
    inline-size: var(--icon-tile-size-lg);
    block-size: var(--icon-tile-size-lg);
  }
}

/* -----------------------------------------------------------------------------
   SOCIAL PROOF — real reviews only.

   Staggered rather than aligned: the second column drops by a step so the band has a
   horizon line that is not perfectly flat. The quote mark is an oversized watermark
   behind the text rather than a small icon floating above it.

   NOTHING HERE INVENTS PROOF. This file styles slots; trust.reviews fills them, and
   every entry there carries verified true or the business loader drops it.
   -------------------------------------------------------------------------- */
.social-proof__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr));
  gap: var(--space-md);
  align-items: start;
}

.social-proof__item {
  position: relative;
  overflow: hidden;
  padding: var(--space-lg);
  background-color: var(--color-surface);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm), var(--shadow-ring);
}

@media (min-width: 60rem) {
  .social-proof__grid > .social-proof__item:nth-child(even) {
    margin-block-start: var(--space-lg);
  }
}

.social-proof__mark {
  position: absolute;
  inset-inline-end: var(--space-2xs);
  inset-block-start: var(--space-2xs);
  inline-size: 5rem;
  block-size: 5rem;
  color: var(--color-brand);
  opacity: 0.07;
  pointer-events: none;
}

.social-proof__quote {
  position: relative;
  font-size: var(--step-1);
  line-height: 1.55;
}

.social-proof__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2xs) var(--space-sm);
  margin-block-start: var(--space-md);
  padding-block-start: var(--space-md);
  font-size: var(--step--1);
  border-block-start: var(--border-hairline);
}

.social-proof__author {
  font-style: normal;
  font-weight: var(--weight-semibold);
}

.social-proof__rating {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3xs);
  color: var(--color-accent);
  unicode-bidi: isolate;
}

.social-proof__source {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3xs);
  margin-inline-start: auto;
  color: var(--color-text-muted);
  text-decoration: none;
}

@media (hover: hover) {
  .social-proof__source:hover {
    color: var(--color-text);
    text-decoration: underline;
  }
}

/* -----------------------------------------------------------------------------
   CONTACT PANEL
   -------------------------------------------------------------------------- */
.contact {
  display: grid;
  gap: var(--space-xl);
}

@media (min-width: 60rem) {
  .contact:has(.contact__map) {
    grid-template-columns: 1fr 1fr;
    align-items: start;
  }
}

.contact__list {
  display: grid;
  gap: var(--space-sm);
}

.contact__item {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  padding: var(--space-md);
  background-color: var(--color-surface);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-xs), var(--shadow-ring);
}

.contact__value {
  font-size: var(--step-1);
  font-weight: var(--weight-semibold);
  unicode-bidi: isolate;
}

.map-embed {
  inline-size: 100%;
  block-size: 22rem;
  border: 0;
  border-radius: var(--radius-lg);
  background-color: var(--color-surface-alt);
  box-shadow: var(--shadow-md), var(--shadow-ring);
}

/* -----------------------------------------------------------------------------
   404
   -------------------------------------------------------------------------- */
.notfound {
  text-align: center;
}

.notfound__title {
  font-size: var(--step-5);
  letter-spacing: var(--tracking-display);
}

.notfound__body {
  margin-block-start: var(--space-sm);
  font-size: var(--step-1);
  color: var(--color-text-muted);
}

.notfound__links {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-2xs);
  margin-block-start: var(--space-lg);
}

/* -----------------------------------------------------------------------------
   PROSE — authored body_html
   -------------------------------------------------------------------------- */
.prose {
  max-inline-size: var(--max-width-measure);
}

.prose > * + * {
  margin-block-start: var(--space-sm);
}

.prose h2 {
  margin-block-start: var(--space-xl);
  font-size: var(--step-3);
  letter-spacing: var(--tracking-display);
}

.prose h3 {
  margin-block-start: var(--space-lg);
  font-size: var(--step-2);
}

.prose ul,
.prose ol {
  padding-inline-start: var(--space-md);
}

.prose ul {
  list-style: disc;
}

.prose ol {
  list-style: decimal;
}

.prose li + li {
  margin-block-start: var(--space-3xs);
}

.prose blockquote {
  padding-inline-start: var(--space-md);
  border-inline-start: 3px solid var(--color-accent);
  color: var(--color-text-muted);
}

.prose img {
  border-radius: var(--media-radius);
  box-shadow: var(--shadow-md), var(--shadow-ring);
}

.prose table {
  inline-size: 100%;
  border-collapse: collapse;
}

.prose th,
.prose td {
  padding: var(--space-2xs);
  text-align: start;
  border-block-end: var(--border-hairline);
}

.prose--legal {
  font-size: var(--step--1);
}

/* -----------------------------------------------------------------------------
   FOOTER
   -------------------------------------------------------------------------- */
.site-footer {
  padding-block-start: var(--space-3xl);
  color: var(--color-text-inverse);
  background-color: var(--color-surface-inverse);
  background-image: radial-gradient(90% 120% at 50% 0%, hsl(0 0% 100% / 0.05) 0%, hsl(0 0% 100% / 0) 55%);
}

.site-footer a {
  color: inherit;
  text-decoration: none;
  transition: opacity var(--transition);
}

@media (hover: hover) {
  .site-footer a:hover {
    text-decoration: underline;
    opacity: 1;
  }
}

.site-footer :focus-visible {
  outline-color: var(--color-text-inverse);
}

.site-footer__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: var(--space-xl);
  padding-block-end: var(--space-2xl);
}

/* The brand column carries more weight than the link columns, so it takes more space
   rather than the same. Asymmetry, from one line. */
@media (min-width: 60rem) {
  .site-footer__grid {
    grid-template-columns: minmax(0, 1.6fr) repeat(auto-fit, minmax(11rem, 1fr));
  }
}

.site-footer__about {
  margin-block-start: var(--space-sm);
  max-inline-size: var(--max-width-measure);
  opacity: 0.72;
}

.footer-title {
  margin-block-end: var(--space-sm);
  font-size: var(--step--1);
  font-weight: var(--weight-semibold);
  /* Both of these are per-script tokens: an Arabic footer gets neither, because
     Arabic has no case and letter-spacing breaks the joins. */
  text-transform: var(--label-transform);
  letter-spacing: var(--tracking-label);
  opacity: 0.62;
}

.footer-links li + li,
.footer-contact li + li {
  margin-block-start: var(--space-2xs);
}

.footer-contact {
  margin-block-start: var(--space-lg);
}

.footer-contact li {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2xs);
  opacity: 0.85;
}

.footer-contact a {
  unicode-bidi: isolate;
}

.social {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2xs);
  margin-block-start: var(--space-lg);
}

.social__link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: 2.75rem;
  block-size: 2.75rem;
  border: 1px solid var(--tint-inverse-line);
  border-radius: var(--radius-pill);
  transition: background-color var(--transition), border-color var(--transition),
    transform var(--transition);
}

@media (hover: hover) {
  .social__link:hover {
    background-color: var(--tint-inverse-raise);
    border-color: var(--color-text-inverse);
    transform: translateY(var(--lift-sm));
  }
}

.site-footer__bottom {
  padding-block: var(--space-md);
  font-size: var(--step--1);
  text-align: center;
  border-block-start: 1px solid var(--tint-inverse-line-soft);
  opacity: 0.62;
}

/* -----------------------------------------------------------------------------
   FLOAT CTAs — AND THE STICKY MOBILE CALL BAR

   THIS IS THE HIGHEST-CONVERTING ELEMENT ON A TRADE SITE, and the phone is where it
   has to be right. A customer with water on the floor is holding a phone in one hand.
   So below 40rem this stops being two small round buttons in a corner and becomes a
   full-width bar pinned to the bottom of the viewport, with a real label and a real
   number on it — the pattern every emergency-trade site that converts uses.

   THE FIVE THINGS THAT MAKE IT RIGHT AT 375px, none of which are optional:

   1. It spans the full width and the two actions split it, so neither is a small
      target next to a large one. Minimum 3.5rem tall — comfortably past the 44px
      floor even before the safe-area padding.
   2. `env(safe-area-inset-bottom)` keeps it clear of the iPhone home indicator. A bar
      that ends exactly at the viewport edge has its bottom third unpressable on
      hardware nobody tested on.
   3. The page reserves the bar's height at the end of the document, so the bar never
      covers the last line of the footer. `:has()` scopes that to pages that actually
      render the bar.
   4. The label is REAL TEXT, not an aria-label on an icon. On the corner variant it is
      clipped visually but still in the DOM, so the accessible name is identical in
      both layouts and there is one string to translate.
   5. It sits above everything except the skip link and never animates in on scroll —
      a bar that arrives late is a bar that gets tapped by accident.
   -------------------------------------------------------------------------- */
.float-ctas {
  position: fixed;
  /* Trailing corner in both directions, from one rule. orig: `right: 20px` plus a
     hand-mirrored duplicate for the other locale. */
  inset-inline-end: var(--space-md);
  inset-block-end: var(--space-md);
  z-index: 30;
  display: flex;
  flex-direction: column;
  gap: var(--space-2xs);
}

.float-cta {
  /* Positioned so the clipped label below anchors to the button, not to the fixed bar. */
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2xs);
  inline-size: 3.5rem;
  block-size: 3.5rem;
  text-decoration: none;
  border-radius: var(--radius-pill);
  box-shadow: var(--shadow-xl);
  transition: transform var(--transition), box-shadow var(--transition),
    background-color var(--transition);
}

.float-cta--primary {
  color: var(--color-brand-contrast);
  background-color: var(--color-brand);
  background-image: linear-gradient(to bottom, hsl(0 0% 100% / 0.14) 0%, hsl(0 0% 100% / 0) 45%);
}

.float-cta--secondary {
  color: var(--color-accent-contrast);
  background-color: var(--color-accent);
  background-image: linear-gradient(to bottom, hsl(0 0% 100% / 0.12) 0%, hsl(0 0% 100% / 0) 45%);
}

/* In the corner layout the label is present for assistive technology and clipped for
   the eye. Same markup, same accessible name, two shapes. */
.float-cta__label,
.float-cta__meta {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

@media (hover: hover) {
  .float-cta:hover {
    transform: translateY(var(--lift-sm)) scale(1.04);
    box-shadow: var(--shadow-xl), var(--shadow-action);
  }
}

.float-cta:active {
  transform: scale(0.97);
}

/* PHYSICAL: a viewport-width query, for the same reason as every other one here. */
@media (max-width: 40rem) {
  .float-ctas {
    inset-inline: 0;
    inset-block-end: 0;
    flex-direction: row;
    gap: 1px;
    padding-block-end: env(safe-area-inset-bottom, 0px);
    background-color: var(--color-border);
    box-shadow: 0 -1px 0 hsl(var(--shadow-color) / 0.08), 0 -8px 24px hsl(var(--shadow-color) / 0.18);
  }

  .float-cta {
    flex: 1;
    flex-direction: column;
    gap: 0;
    inline-size: auto;
    min-block-size: 3.75rem;
    padding-block: var(--space-2xs);
    padding-inline: var(--space-2xs);
    border-radius: 0;
    box-shadow: none;
  }

  .float-cta .icon {
    /* One clear glyph above the label, not a decoration beside it. */
    inline-size: 1.35rem;
    block-size: 1.35rem;
    margin-block-end: 2px;
  }

  .float-cta__label {
    position: static;
    inline-size: auto;
    block-size: auto;
    overflow: visible;
    clip-path: none;
    font-size: var(--step--1);
    font-weight: var(--weight-semibold);
    line-height: 1.15;
  }

  .float-cta__meta {
    position: static;
    inline-size: auto;
    block-size: auto;
    overflow: visible;
    clip-path: none;
    font-size: var(--step--1);
    font-weight: var(--weight-body);
    line-height: 1.15;
    opacity: 0.82;
    unicode-bidi: isolate;
  }

  .float-cta:hover,
  .float-cta:active {
    transform: none;
  }

  .float-cta:active {
    filter: brightness(0.92);
  }

  /* Reserve the bar's height so it cannot cover the end of the page. Scoped with
     :has() so a project that switches the bar off gets no phantom gap; an engine
     without :has() simply keeps the footer's normal padding. */
  body:has([data-float-ctas]) .site-footer__bottom {
    padding-block-end: calc(4.25rem + env(safe-area-inset-bottom, 0px));
  }
}

/* -----------------------------------------------------------------------------
   SCROLL REVEAL — the only motion mechanism 00-design-law.md adds, and the only one
   that is not already in the file.

   taste-skill reaches for Motion's `whileInView` or GSAP ScrollTrigger. combo has no
   framework and no render loop, so the translation is CSS first and script second, and
   the ban that comes with it is absolute: NOTHING IN THIS SYSTEM LISTENS TO SCROLL.
   `window.addEventListener('scroll')` runs unbatched on every frame and forces layout
   on a mid-range phone, which is the device this page is opened on.

   FOUR GATES, ALL LOAD-BEARING, ALL AND-ed:

   1. `[data-motion="7"]` — the dial. 5.A permits reveals at motion 7 only. At 1-6 this
      block does not match and the page is finished HTML that never moved.
   2. `prefers-reduced-motion: no-preference` — the reveal is REMOVED under `reduce`,
      not shortened. A 0.01ms animation to an opacity of 1 still flashes, and "jumps
      instantly" is worse for a vestibular disorder than "moves smoothly".
   3. `@supports (animation-timeline: view())` — this is the important one. THE
      NO-SUPPORT STATE IS THE FINISHED PAGE. An engine without scroll-driven animation
      shows content, not blank space, which is the exact opposite of a JS reveal that
      shows blank space when the script fails or is slow.
   4. `transform` and `opacity` only. Never `top`, `width` or `height`: those reflow,
      and these pages are judged on Cumulative Layout Shift.

   `translateY` is a BLOCK-axis movement, so it is direction-neutral and there is no RTL
   counterpart to maintain. An inline-axis reveal would need one, which is why it is not
   offered.

   AT MOST THREE BANDS PER PAGE carry `.reveal`. It is a class a template opts into, not
   a rule on `.section`, precisely so that "every band fades up" is not the default —
   that pattern is what makes a page feel like it is being performed at the reader.

   THE OPTIONAL FALLBACK. If `assets/js/site.js` ever gains an IntersectionObserver, it
   adds `is-revealed` on intersect and unobserves immediately. The `.reveal:not(...)`
   split below is what lets the two coexist: the scroll-driven path animates, and a
   scripted path only ever ADDS the finished state. Neither can leave content hidden,
   because the hidden state exists only inside the @supports block.
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {
    :root[data-motion="7"] .reveal:not(.is-revealed) {
      animation: reveal-rise linear both;
      animation-timeline: view();
      animation-range: var(--reveal-range);
    }

    @keyframes reveal-rise {
      from {
        opacity: 0;
        transform: translateY(var(--reveal-rise));
      }

      to {
        opacity: 1;
        transform: none;
      }
    }
  }
}

/* -----------------------------------------------------------------------------
   MOTION

   Everything above transitions; nothing above moves layout. The reduced-motion path
   is not a courtesy — a page that animates through a vestibular disorder is a page
   that hurt somebody. It kills duration AND the transforms themselves, because a
   0.01ms transition to a scaled state still scales.
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }

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

  /* The state still changes — colour, shadow, the underline's presence — so every
     affordance survives. Only the movement stops. */
  .btn:hover,
  .btn:active,
  .card:hover,
  .card:active,
  .pill:hover,
  .pill:active,
  .why-us__point:hover,
  .float-cta:hover,
  .float-cta:active,
  .social__link:hover {
    transform: none;
  }

  .card:hover .card__media img,
  .gallery__figure:hover img,
  .media-frame:hover img,
  .work-evidence__frame:hover img {
    transform: none;
  }

  /* Belt and braces on the reveal. The @supports block above is already inside
     `prefers-reduced-motion: no-preference` and therefore cannot match here, but a
     project that adds its own scroll-driven animation in brand.css will forget this
     line, and an author whose own machine has motion enabled never finds out. */
  .reveal {
    animation: none !important;
    opacity: 1 !important;
    transform: none !important;
  }

  .card:hover .card__arrow,
  [dir="rtl"] .card:hover .card__arrow {
    transform: none;
  }

  .site-nav__link::after {
    transition: none;
  }
}

/* -----------------------------------------------------------------------------
   PRINT
   -------------------------------------------------------------------------- */
@media print {
  .site-header,
  .float-ctas,
  .carousel__btn,
  .skip-link,
  .map-embed,
  .social-proof__mark {
    display: none !important;
  }

  .faq__item {
    /* Every answer prints, whether or not it was expanded on screen. */
    display: block;
  }

  .faq__answer {
    display: block !important;
  }

  /* A scrim over a photograph prints as a black rectangle on a monochrome printer,
     so the veil is removed and the hero prints as ordinary dark text on white. */
  .hero,
  .hero--photo,
  .hero__panel,
  .cta,
  .section--inverse,
  .site-footer {
    color: hsl(0 0% 0%) !important;
    background: none !important;
  }

  .hero--photo .hero__media {
    display: none !important;
  }

  body {
    color: hsl(0 0% 0%);
    background: none;
  }

  .card,
  .panel,
  .faq__item,
  .why-us__point,
  .social-proof__item,
  .gallery__figure,
  .media-frame,
  .work-evidence__frame {
    box-shadow: none !important;
    border: 1px solid hsl(0 0% 80%);
  }
}
