/* ==========================================================================
   GoodWork Solutions — Design System
   Palette derived from the brand wordmark (#A18C74 sampled from logo asset).
   Dark cinematic canvas; bronze is the single accent. No second hue.
   ========================================================================== */

:root {
  /* --- Surface ramp. Cards lift lighter than the page, panels recess. --- */
  --bg:            #EEEDEB;
  --bg-raised:     #F7F6F4;
  --bg-panel:      #E4E2DE;
  --bg-elevated:   #FCFBFA;

  /* --- Brand ---
     The wordmark bronze #A18C74 only reaches 2.75:1 on #EEEDEB — it fails AA
     for body text AND for large text, so it cannot carry any type on light.
     The accent re-pitches to #6E5D4B (5.39:1 on base, 4.87:1 on the recessed
     panel). #A18C74 survives only as a decorative fill / border tone. */
  --bronze:        #6E5D4B;  /* 5.39:1 on --bg */
  --bronze-lift:   #574A3B;  /* 7.33:1 — hover / emphasis */
  --bronze-deep:   #A18C74;  /* decorative only, never text */
  --bronze-wash:   rgba(110, 93, 75, 0.07);

  /* --- Ink --- */
  --ink:           #1A1714;  /* 15.25:1 on --bg */
  --ink-soft:      #4A423A;  /*  8.42:1 */
  --ink-mute:      #6B6157;  /*  5.17:1 on --bg, 4.67:1 on --bg-panel */

  /* --- Lines --- */
  --line:          rgba(26, 23, 20, 0.13);
  --line-strong:   rgba(26, 23, 20, 0.26);

  /* --- Type --- */
  --font-display: 'Instrument Serif', 'Iowan Old Style', Georgia, serif;
  --font-sans:    'Instrument Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;

  /* Fluid scale — clamp() so there are no breakpoint jumps in headline size */
  --t-hero:  clamp(2.6rem, 1.1rem + 6.4vw, 7.2rem);
  --t-xl:    clamp(2.2rem, 1.3rem + 3.9vw, 4.6rem);
  --t-lg:    clamp(1.6rem, 1.2rem + 1.9vw, 2.6rem);
  --t-md:    clamp(1.15rem, 1.05rem + 0.5vw, 1.4rem);
  --t-body:  clamp(1rem, 0.97rem + 0.16vw, 1.09rem);
  --t-sm:    0.875rem;
  --t-xs:    0.75rem;

  /* --- Space (density 4/10 — marketing pace, generous but not floaty) --- */
  --s-1: 0.5rem;  --s-2: 0.75rem; --s-3: 1rem;    --s-4: 1.5rem;
  --s-5: 2rem;    --s-6: 3rem;    --s-7: 4rem;    --s-8: 6rem;
  --s-9: clamp(4rem, 2.75rem + 4.5vw, 7.5rem);

  /* ch is the width of "0", which sits differently against the lowercase in
     every face — the same 62ch that gave Inter a 73-character line gives
     Instrument Sans 79, past the comfortable range. Retuned by measurement
     rather than carried over. */
  --measure: 57ch;
  --gutter: clamp(1.25rem, 0.6rem + 2.8vw, 4.5rem);
  --maxw: 1320px;

  /* Expo-out. Everything decelerates on the same curve or it reads as noise. */
  --ease: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-soft: cubic-bezier(0.33, 1, 0.68, 1);
  --radius: 2px;
  --radius-lg: 14px;
}

/* ========================================================================== */

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

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

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink-soft);
  font-family: var(--font-sans);
  font-size: var(--t-body);
  /* Instrument Sans starts at 400. The old value of 350 was reachable in Inter
     but clamps here, so it is stated honestly rather than left to the browser.
     The airiness the light weight used to carry is now doing its work through
     line-height and the softened ink colour instead. */
  font-weight: 400;
  line-height: 1.68;
  letter-spacing: 0.005em;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow-x: hidden;
}

h1, h2, h3, h4 {
  color: var(--ink);
  font-weight: 400;
  margin: 0;
  text-wrap: balance;
}

p { margin: 0; text-wrap: pretty; }

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

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

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

::selection { background: var(--bronze); color: var(--bg); }

/* ---------- Paper grain ----------
   A flat #EEEDEB reads as a hex value at large sizes; a hint of tooth reads
   as stock. Generated with feTurbulence inline as a data URI, so it costs no
   request and about 300 bytes rather than a texture file.

   Fixed rather than scrolling: grain belongs to the screen, like the surface
   you are looking at, not to the document sliding underneath it. Desaturated
   in the filter itself so it never tints the warm paper toward grey. */
body::before {
  content: '';
  position: fixed;
  inset: 0;
  z-index: 400;
  pointer-events: none;
  opacity: 0.032;
  background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='200' height='200'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/><feColorMatrix type='saturate' values='0'/></filter><rect width='200' height='200' filter='url%28%23n%29'/></svg>");
  background-size: 200px 200px;
}

/* Keyboard focus only — never remove the ring, just scope it. */
:focus-visible {
  outline: 2px solid var(--bronze-lift);
  outline-offset: 3px;
  border-radius: var(--radius);
}

/* ---------- Layout primitives ---------- */

.wrap {
  width: 100%;
  max-width: var(--maxw);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

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

/* The orbit column runs taller than the copy beside it, so the problem
   section already carries a lot of trailing air before its own padding even
   starts. Halving the padding either side of that seam pulls the services
   list up to sit with it, without disturbing the section rhythm elsewhere. */
.section--problem { padding-bottom: calc(var(--s-9) * 0.45); }
#services { padding-top: calc(var(--s-9) * 0.45); }

.skip {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 200;
  background: var(--bronze);
  color: var(--bg);
  padding: var(--s-2) var(--s-4);
  font-weight: 600;
}
.skip:focus { left: var(--s-3); top: var(--s-3); }

/* Announced by screen readers, invisible on screen. Not display:none —
   that would remove it from the accessibility tree entirely. */
.sr-live, .sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* ---------- Type treatment ----------
   The system is a tension: eyebrows are tracked out to echo the wordmark's
   extreme letterspacing; display type is pulled tight against it. That
   contrast is the identity — not a decorative face. */

.eyebrow {
  font-size: var(--t-xs);
  font-weight: 500;
  letter-spacing: 0.32em;
  text-transform: uppercase;
  color: var(--bronze);
  margin: 0;
}

.eyebrow--mute { color: var(--ink-mute); }

.display {
  font-family: var(--font-display);
  font-weight: 400;
  line-height: 0.98;
  letter-spacing: -0.018em;
  color: var(--ink);
}

.display em {
  font-style: italic;
  color: var(--bronze-lift);
}

.h-hero { font-size: var(--t-hero); }
.h-xl   { font-size: var(--t-xl); line-height: 1.02; }
.h-lg   { font-size: var(--t-lg); line-height: 1.12; }

.lede {
  font-size: var(--t-md);
  line-height: 1.62;
  color: var(--ink-soft);
  max-width: var(--measure);
  /* Was 300, which this family does not have. The lede still separates from
     body copy by size and colour, so it does not need a lighter weight. */
  font-weight: 400;
}

.muted { color: var(--ink-mute); }
.measure { max-width: var(--measure); }

/* ---------- Header ---------- */

.hdr {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 100;
  padding-block: var(--s-4);
  transition: background 0.4s var(--ease), padding 0.4s var(--ease),
              border-color 0.4s var(--ease), backdrop-filter 0.4s var(--ease);
  border-bottom: 1px solid transparent;
}

.hdr.is-stuck {
  background: rgba(238, 237, 235, 0.85);
  backdrop-filter: blur(14px) saturate(1.3);
  -webkit-backdrop-filter: blur(14px) saturate(1.3);
  border-bottom-color: var(--line);
  padding-block: var(--s-3);
}

.hdr__in {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-4);
}

/* Wordmark rebuilt in type rather than shipped as a PNG — stays crisp at any
   size, recolours for free, and costs no extra request. */
.mark { display: block; line-height: 1; padding-block: 9px; }  /* 44px touch target */
.mark__a {
  display: block;
  font-size: clamp(0.95rem, 0.8rem + 0.5vw, 1.15rem);
  font-weight: 600;
  letter-spacing: 0.34em;
  color: var(--bronze);
  text-indent: 0.34em;
}
/* Same bronze as the primary line — matches the real wordmark, where both
   words are one colour, and clears 4.5:1. The darker tone it used to carry
   sat at 3.1:1. */
.mark__b {
  display: block;
  font-size: clamp(0.5rem, 0.42rem + 0.28vw, 0.62rem);
  font-weight: 500;
  letter-spacing: 0.58em;
  color: var(--bronze);
  opacity: 0.85;
  text-indent: 0.58em;
  margin-top: 0.36em;
}
.mark:hover .mark__a { color: var(--bronze-lift); }
.mark__a, .mark__b { transition: color 0.3s var(--ease); }

.nav { display: flex; align-items: center; gap: clamp(1rem, 2.2vw, 2.4rem); }

.nav__link {
  font-size: var(--t-sm);
  color: var(--ink-soft);
  position: relative;
  padding-block: 0.72rem;   /* brings the hit area to 44px */
  transition: color 0.3s var(--ease);
}
.nav__link::after {
  content: '';
  position: absolute;
  left: 0; bottom: 0;
  width: 100%; height: 1px;
  background: var(--bronze);
  transform: scaleX(0);
  transform-origin: right;
  transition: transform 0.45s var(--ease);
}
.nav__link:hover { color: var(--ink); }
.nav__link:hover::after { transform: scaleX(1); transform-origin: left; }
.nav__link[aria-current='page'] { color: var(--ink); }
.nav__link[aria-current='page']::after { transform: scaleX(1); }

@media (max-width: 900px) { .nav--desk { display: none; } }

/* ---------- Mobile menu ---------- */

.menu-btn {
  display: none;
  width: 48px; height: 48px;
  align-items: center;
  justify-content: center;
  flex-direction: column;
  gap: 5px;
  margin-right: -12px;
}
@media (max-width: 900px) { .menu-btn { display: flex; } }

.menu-btn span {
  display: block;
  width: 22px; height: 1.5px;
  background: var(--ink);
  transition: transform 0.4s var(--ease), opacity 0.3s var(--ease);
}
.menu-btn[aria-expanded='true'] span:nth-child(1) { transform: translateY(6.5px) rotate(45deg); }
.menu-btn[aria-expanded='true'] span:nth-child(2) { opacity: 0; }
.menu-btn[aria-expanded='true'] span:nth-child(3) { transform: translateY(-6.5px) rotate(-45deg); }

.menu {
  position: fixed;
  inset: 0;
  z-index: 99;
  background: rgba(238, 237, 235, 0.98);
  backdrop-filter: blur(20px);
  -webkit-backdrop-filter: blur(20px);
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: var(--s-4);
  padding: var(--s-8) var(--gutter) var(--s-7);
  opacity: 0;
  transition: opacity 0.4s var(--ease);
}
.menu.is-open { opacity: 1; }
.menu[hidden] { display: none; }

.menu a {
  font-family: var(--font-display);
  font-size: clamp(1.9rem, 1.2rem + 4vw, 3rem);
  color: var(--ink);
  padding-block: 0.18em;
  border-bottom: 1px solid var(--line);
  transition: color 0.3s var(--ease), padding-left 0.4s var(--ease);
}
.menu a:hover, .menu a:focus-visible { color: var(--bronze-lift); padding-left: 0.4rem; }
.menu .btn { margin-top: var(--s-4); align-self: flex-start; font-family: var(--font-sans); font-size: var(--t-sm); }

/* ---------- Buttons ---------- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.65em;
  min-height: 48px;              /* > 44px touch floor */
  padding: 0.85em 1.7em;
  font-size: var(--t-sm);
  font-weight: 500;
  letter-spacing: 0.02em;
  border-radius: 100px;
  cursor: pointer;
  position: relative;
  overflow: hidden;
  transition: color 0.35s var(--ease), background 0.35s var(--ease),
              border-color 0.35s var(--ease), transform 0.35s var(--ease),
              box-shadow 0.35s var(--ease);
}

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

/* Light fill text on the bronze button — 6.31:1. The old #14110E was dark
   ink for a light bronze; on the darkened accent it would be near-invisible. */
.btn--primary { background: var(--bronze); color: #FCFBFA; font-weight: 600; }
.btn--primary:hover {
  background: var(--bronze-lift);
  transform: translateY(-1px);      /* one pixel — any more reads as cheap */
  box-shadow: 0 8px 22px rgba(110, 93, 75, 0.26);
}

.btn--ghost { border: 1px solid var(--line-strong); color: var(--ink); }
.btn--ghost:hover { border-color: var(--bronze); color: var(--bronze-lift); }

.btn--sm { min-height: 44px; padding: 0.6em 1.25em; }

.btn__arrow { transition: transform 0.4s var(--ease); }
.btn:hover .btn__arrow { transform: translateX(4px); }

/* ---------- Magnetic wrapper ----------
   Inserted around every button by main.js. It exists purely to carry the
   transform, keeping it off the button — the rainbow ring and halo are
   z-index:-1 pseudo-elements, and a transform on the button would create a
   stacking context and flip them in front of the label.

   inline-flex so it behaves exactly as the button did inside the flex rows
   it sits in; without it the wrapper would be inline and collapse the
   button's box. */
.magnetic {
  display: inline-flex;
  will-change: transform;
  /* Re-setting transform every frame against a live transition is what makes
     this feel damped rather than glued — the button chases the cursor with
     weight instead of tracking it exactly. */
  transition: transform 0.45s var(--ease);
}

/* Release gets its own curve with a little overshoot, so the button settles
   back rather than stopping dead. Small enough to feel like physics; any
   more and it reads as a bounce. */
.magnetic.is-releasing {
  transition: transform 0.75s cubic-bezier(0.34, 1.38, 0.64, 1);
}

/* Column layouts stretch their items, so the wrapper fills the width and the
   button fills the wrapper — full-width CTAs on phones still work. */
.hero__cta .magnetic,
.cta__row .magnetic,
.res__cta .magnetic { max-width: 100%; }

/* ---------- Rainbow border ----------
   Ported from the React component; only the effect transfers, since the
   original is a full-screen demo wrapper with a hardcoded label and no props.

   Two things about .btn had to be worked around for the original's
   z-index:-1 pseudo-element approach to survive:

     · .btn carries overflow:hidden, which clips a ring drawn outside the
       box — overridden to visible below
     · .btn:active and .btn--primary:hover both apply a transform, and a
       transform creates a stacking context, which flips a z-index:-1 pseudo
       from behind the button to in front of it — the whole button would turn
       rainbow the instant you pointed at it. Transforms are disabled on these
       buttons for that reason, and press feedback uses a shadow instead. */

/* Hues are the original five, kept exactly. Saturation 52%, lightness levelled
   to 58% — levelling matters as much as the saturation figure, because at full
   lightness the yellow shouts and the blue goes almost black, so the ring reads
   as five separate flashes rather than one band of colour.

   Conic rather than linear: the ring's start angle is driven by where the
   cursor is, so the colours orient to the pointer instead of sliding on a
   timer. The first colour is repeated at the end to close the loop without
   a visible seam at 0deg. --ang, --mx and --my are set by main.js. */
.btn--rainbow {
  --rainbow: conic-gradient(from var(--ang, 0deg),
    #CC5C9F, #5C5CCC, #5CCC5C, #CCCC5C, #CC5C5C, #CC5C9F);

  --ang: 0deg;
  --mx: 50%;
  --my: 50%;

  overflow: visible;                 /* ring and halo sit outside the box */
}

/* Ghost variant needs an opaque centre or the ring behind it shows straight
   through the middle. --bg matches the page, so it looks unchanged at rest. */
.btn--ghost.btn--rainbow { background: var(--bg); }

/* Now that the effect is hover-only it can go back to the original's
   pseudo-element approach, which the background-clip version was working
   around: opacity is transitionable, a background-image is not. This is only
   safe because transforms stay disabled below — a transform would create a
   stacking context and flip these from behind the button to on top of it. */
.btn--rainbow::before,
.btn--rainbow::after {
  content: '';
  position: absolute;
  inset: -2px;
  border-radius: inherit;
  background: var(--rainbow);
  z-index: -1;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.4s var(--ease);
}

/* No transition on --ang: it tracks the pointer 1:1.
   An eased catch-up was measured at ~340ms to settle, and on an expo curve
   the tail crawls — 88° of a 120° move lands in the first 70ms and the rest
   dribbles in over a quarter-second. That reads as the colour chasing the
   cursor rather than being attached to it. Smoothness comes from the source
   instead: pointermove is already coalesced to one sample per frame, so the
   values arriving are continuous. */
.btn--rainbow::before {
  transition: opacity 0.4s var(--ease);
}

/* Halo blooms under the cursor specifically — a radial centred on the pointer
   rather than the whole ring glowing evenly. Well short of the original's
   blur(50px), which at this size bled a cloud across the header. */
.btn--rainbow::after {
  inset: -6px;
  filter: blur(14px);
  background:
    radial-gradient(60% 90% at var(--mx) var(--my),
      rgba(204, 92, 159, 0.95), transparent 70%),
    var(--rainbow);
}

.btn--rainbow:hover::before { opacity: 1; }
.btn--rainbow:hover::after  { opacity: 0.6; }

/* Typed so the value parses as an angle once rather than being re-substituted
   as a string on every pointer sample — worth keeping now that it updates
   every frame while hovered. */
@property --ang {
  syntax: '<angle>';
  inherits: true;
  initial-value: 0deg;
}

/* No transforms on these — see the note above. Press feedback moves to a
   shadow, which does not create a stacking context. */
.btn--rainbow:hover,
.btn--rainbow:active { transform: none; }
.btn--rainbow:active { box-shadow: 0 0 0 3px rgba(204, 92, 159, 0.32); }

/* ---------- Hero ---------- */

.hero {
  position: relative;
  /* Bottom-aligned content on a very tall window dumped the entire surplus
     into one void between the header and the eyebrow. Capping the hero keeps
     the full-viewport feel on laptops and stops the gap running away on large
     displays — and letting the next section peek in is no loss. */
  min-height: min(100svh, 900px);
  display: flex;
  align-items: flex-end;
  /* Clears the fixed header without pushing the CTA below the fold on a
     720px laptop viewport. */
  padding-block: calc(var(--s-7) + 2.5rem) var(--s-6);
  overflow: hidden;
}

/* Short viewports (most laptops): drop the scroll cue and tighten, so the
   primary CTA stays above the fold rather than the hero overflowing. */
@media (max-height: 800px) {
  .scrollcue { display: none; }
  .hero { padding-block: calc(var(--s-6) + 2rem) var(--s-5); }
  .hero__title { margin-block: var(--s-2) var(--s-3); }
  .hero__cta { margin-top: var(--s-4); }
}

.hero__canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 0;
}

/* Foreground pass. Sits above .hero__in (z-index 2) so strands cross the
   type. Never intercepts the pointer — the vortex is driven from .hero.
   The canvas itself is transparent (see `transparent` in filaments.js); the
   multiply happens here, at element level, so strands darken the headline
   they cross rather than sitting on it as opaque marks. */
.hero__canvas--front {
  z-index: 3;
  pointer-events: none;
  mix-blend-mode: multiply;
}

/* Vignette + floor fade so type never sits on a busy patch of filament */
.hero::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background:
    radial-gradient(120% 80% at 50% 8%, transparent 35%, rgba(238,237,235,0.50) 100%),
    linear-gradient(to bottom, rgba(238,237,235,0.42) 0%, transparent 22%, transparent 50%, rgba(238,237,235,0.94) 92%);
}

.hero__in { position: relative; z-index: 2; width: 100%; }

.hero__title { margin-block: var(--s-3) var(--s-4); max-width: 16ch; }

/* The eyebrow settles into its tracking rather than arriving at it — the
   letters draw together over the first second. One line, one time, so the
   layout cost is irrelevant and it is the first thing that says "made". */
.hero .eyebrow[data-reveal] {
  letter-spacing: 0.54em;
  transition:
    opacity 0.85s var(--ease) var(--d, 0s),
    transform 0.85s var(--ease) var(--d, 0s),
    letter-spacing 1.5s var(--ease) var(--d, 0s);
}
.hero .eyebrow[data-reveal].is-in { letter-spacing: 0.32em; }

/* The thesis word is the handle for the field behind it. Barely marked —
   the reward is in the background, not in the text turning a colour. */
.hero__title em {
  cursor: default;
  transition: color 0.6s var(--ease);
}
.hero__title em:hover { color: var(--ink); }

.hero__row {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--s-6);
}

.hero__lede { max-width: 46ch; }

.hero__cta { display: flex; flex-wrap: wrap; gap: var(--s-3); margin-top: var(--s-5); }

.hero__note {
  font-size: var(--t-sm);
  color: var(--ink-mute);
  font-style: italic;
  margin-top: var(--s-4);
}

.scrollcue {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  font-size: var(--t-xs);
  letter-spacing: 0.26em;
  text-transform: uppercase;
  color: var(--ink-mute);
  margin-top: var(--s-7);
}
.scrollcue__line {
  width: 54px; height: 1px;
  background: linear-gradient(to right, var(--bronze), transparent);
  position: relative; overflow: hidden;
}
.scrollcue__line::after {
  content: '';
  position: absolute; inset: 0;
  background: var(--bronze-lift);
  animation: cue 2.6s var(--ease) infinite;
}
@keyframes cue {
  0%   { transform: translateX(-100%); }
  60%, 100% { transform: translateX(100%); }
}

/* ---------- Marquee ----------
   Behaviour ported from the shadcn/React <Marquee> component so the same
   knobs exist here without React, Tailwind or a build step. Set them as
   attributes on .marquee:

     data-speed="slow|normal|fast"  → --duration 120s / 40s / 10s
     data-reverse                   → opposite direction
     data-pause                     → pause on hover
     data-vertical                  → scroll on the Y axis
     repeat                         → number of .marquee__track children

   The track translates by -100% MINUS the gap, matching the original
   keyframes. Translating a flat -100% (what this used to do) leaves a
   gap-width jump at the seam on every loop. */

.marquee-stack {
  border-block: 1px solid var(--line);
  display: grid;
}

.marquee {
  --gap: clamp(1.5rem, 3vw, 3rem);
  --duration: 40s;
  display: flex;
  gap: var(--gap);
  overflow: hidden;
  white-space: nowrap;
  user-select: none;
  padding-block: var(--s-3);
}

.marquee[data-speed='slow']   { --duration: 120s; }
.marquee[data-speed='normal'] { --duration: 40s; }
.marquee[data-speed='fast']   { --duration: 10s; }

.marquee[data-vertical] { flex-direction: column; }

.marquee__track {
  display: flex;
  flex-shrink: 0;
  align-items: center;
  justify-content: space-around;
  gap: var(--gap);
  animation: marquee var(--duration) linear infinite;
  will-change: transform;
}

.marquee[data-vertical] .marquee__track {
  flex-direction: column;
  animation-name: marquee-vertical;
}

.marquee[data-reverse] .marquee__track { animation-direction: reverse; }
.marquee[data-pause]:hover .marquee__track { animation-play-state: paused; }

@keyframes marquee {
  from { transform: translateX(0); }
  to   { transform: translateX(calc(-100% - var(--gap))); }
}

@keyframes marquee-vertical {
  from { transform: translateY(0); }
  to   { transform: translateY(calc(-100% - var(--gap))); }
}

.marquee__item {
  font-family: var(--font-display);
  font-size: clamp(1.35rem, 0.9rem + 2.1vw, 2.9rem);
  color: var(--ink);
  display: flex;
  align-items: center;
  gap: var(--gap);
  line-height: 1.1;
}
.marquee__item::after {
  content: '';
  width: 7px; height: 7px;
  border-radius: 50%;
  background: var(--bronze);
  flex-shrink: 0;
}

/* Second row sits slightly back so the two lines read as foreground and
   echo rather than two competing bands of equal weight. */
.marquee--echo .marquee__item { color: var(--ink-mute); }
/* --bronze-deep is a pale decorative tone on light, so the echo dot would
   all but vanish — the muted ink keeps it readable as a separator. */
.marquee--echo .marquee__item::after { background: var(--ink-mute); }

/* ---------- Section headers ---------- */

.shead { margin-bottom: var(--s-7); }
.shead__title { margin-top: var(--s-3); max-width: 20ch; }
.shead__sub { margin-top: var(--s-4); }

.rule {
  height: 1px;
  background: var(--line);
  border: 0;
  margin: 0;
}

/* ---------- Problem / maze section ---------- */

.maze { display: grid; gap: var(--s-8); align-items: center; }
@media (min-width: 900px) { .maze { grid-template-columns: 1fr 1fr; gap: var(--s-9); } }

.maze__viz {
  aspect-ratio: 1 / 1;
  width: 100%;
  border-radius: var(--radius-lg);
  overflow: hidden;
  background: var(--bg-raised);
  border: 1px solid var(--line);
  position: relative;
}
.maze__viz canvas { width: 100%; height: 100%; }

/* ---------- Integration orbit ----------
   Rings of systems turning around a Salesforce core. Ported from the React
   component's structure — concentric rings, alternating direction, chips that
   counter-rotate so their text stays upright — but rendered as typographic
   chips rather than logo images: no CDN dependency, no trademark assets, and
   it sits inside the existing bronze-on-paper language.

   The counter-rotation is the whole trick. A chip parented to a spinning
   spoke inherits that spin and would cartwheel; running an equal, opposite
   animation on the chip itself cancels it exactly, so the pill orbits while
   its label stays level. Both animations share --dur and are linear, which
   is what keeps them locked together. */

/* No panel — the diagram sits directly on the page. overflow is deliberately
   not hidden: with no visible box there is nothing to clip against, and a
   sliced logo would look broken where a slight bleed would not. The geometry
   below keeps everything inside regardless. */
.orbit {
  position: relative;
  aspect-ratio: 1 / 1;
  width: 100%;
  background: none;
  border: 0;
}

/* Above the Salesforce mark, so particles cross in front of it. The canvas
   is cleared each frame, so only the dots occlude — the rest is transparent. */
.orbit__core {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 2;
  pointer-events: none;
}

.orbit__ring {
  position: absolute;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  border: 1px solid var(--line);
  border-radius: 50%;
  z-index: 3;        /* above the canvas, so orbiting marks are never occluded */
}
/* Ring diameters are solved, not chosen. A logo is centred on its ring, so it
   reaches ringRadius ± logoWidth/2. Two constraints bound the set:

     inner:  ring1Radius − logoHalf ≥ sphereRadius + 30px clearance
     outer:  ring3Radius + logoHalf ≤ panel/2 − 8px

   At a 538px panel with 46px logos that gives ring1 ≥ 55% for a 95px sphere,
   and ring3 ≤ 88.5%. 55/70/84 satisfies both with even ~40px spacing, and
   still holds 18px of clearance at a 335px mobile panel. Changing the sphere
   radius in main.js means re-solving these. */
.orbit__ring--1 { width: 55%; height: 55%; }
.orbit__ring--2 { width: 70%; height: 70%; }
.orbit__ring--3 { width: 84%; height: 84%; }

.orbit__spoke {
  position: absolute;
  left: 50%; top: 0;
  width: 0; height: 50%;
  transform-origin: 50% 100%;
  animation: orbit-cw var(--dur, 40s) linear infinite;
}
.orbit__ring--rev .orbit__spoke { animation-name: orbit-ccw; }

/* Static holder does the centring so the chip's transform is free to carry
   the counter-rotation alone. */
.orbit__hold {
  position: absolute;
  top: 0; left: 0;
  transform: translate(-50%, -50%);
}

/* Marks float directly on the panel — no disc.
   That costs contrast: on this surface Salesforce reads 2.71:1, HubSpot
   2.38, Xero 2.20, all under the 3:1 non-text floor. They stay recognisable
   at this size. The two that did not — Mailchimp yellow at 1.22 and Power BI
   at 1.49 — are rendered in ink instead, which is what Mailchimp's own
   guidelines specify for light backgrounds. Marks are sized up to compensate
   for losing the disc's edge definition. */
/* The badge must carry an explicit width even though it is now invisible.
   The spoke is width:0 by design — it is only a rotation arm — so the global
   `img { max-width: 100% }` reset resolves to 100% OF ZERO and collapses the
   mark to nothing. The disc used to supply that width by accident; sizing the
   badge keeps it, and also gives .orbit__hold something real to centre with
   its translate(-50%, -50%). */
.orbit__badge {
  --c: calc(-1 * var(--a, 0deg));
  display: block;
  width: clamp(30px, 3.4vw, 46px);
  aspect-ratio: 1;
  animation: orbit-counter-cw var(--dur, 40s) linear infinite;
}
.orbit__ring--rev .orbit__badge { animation-name: orbit-counter-ccw; }

.orbit__badge img {
  display: block;
  width: 100%;
  height: 100%;
  max-width: none;
  object-fit: contain;
  transition: opacity 0.4s var(--ease);
}

/* Hovering a mark names it. The logos are otherwise unlabelled — the systems
   are only spoken aloud to assistive tech — so this is the one interaction
   here that answers a real question rather than decorating one. The label
   rides on the badge, which already counter-rotates, so it stays upright. */
.orbit__badge::after {
  content: attr(data-name);
  position: absolute;
  top: calc(100% + 8px);
  left: 50%;
  transform: translateX(-50%);
  white-space: nowrap;
  font-size: 0.62rem;
  font-weight: 500;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--ink);
  background: var(--bg);
  padding: 0.3em 0.6em;
  border-radius: 100px;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.35s var(--ease);
}
.orbit__badge:hover::after { opacity: 1; }

/* Everything else recedes, so the named mark is unmistakably the one you are
   pointing at. Same focus idea as the services list — attention given by
   taking it away from the rest. */
.orbit:has(.orbit__badge:hover) .orbit__badge img { opacity: 0.25; }
.orbit__badge:hover img { opacity: 1; }
.orbit:has(.orbit__badge:hover) .orbit__core-badge img { opacity: 0.35; }

/* Salesforce sits inside the globe rather than on a ring. It is painted
   BELOW the canvas, so the sphere's front-hemisphere particles pass over the
   mark as it turns — that overlap is what makes it read as enclosed rather
   than as a badge stuck on top. */
.orbit__core-badge {
  position: absolute;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  z-index: 1;
  display: block;
  pointer-events: none;
}
.orbit__core-badge img {
  display: block;
  width: clamp(76px, 9.6vw, 126px);   /* fills the enlarged sphere */
  height: auto;
}

.orbit__chip {
  --c: calc(-1 * var(--a, 0deg));
  display: block;
  animation: orbit-counter-cw var(--dur, 40s) linear infinite;
  background: var(--bg-elevated);
  border: 1px solid var(--line-strong);
  border-radius: 100px;
  padding: 0.5em 0.85em;
  font-size: clamp(8px, 0.92vw, 10px);
  font-weight: 500;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink);
  white-space: nowrap;
  box-shadow: 0 2px 10px rgba(26, 23, 20, 0.06);
}
.orbit__ring--rev .orbit__chip { animation-name: orbit-counter-ccw; }

.orbit:hover .orbit__spoke,
.orbit:hover .orbit__chip,
.orbit:hover .orbit__badge { animation-play-state: paused; }

@keyframes orbit-cw {
  from { transform: rotate(var(--a, 0deg)); }
  to   { transform: rotate(calc(var(--a, 0deg) + 360deg)); }
}
@keyframes orbit-ccw {
  from { transform: rotate(var(--a, 0deg)); }
  to   { transform: rotate(calc(var(--a, 0deg) - 360deg)); }
}
@keyframes orbit-counter-cw {
  from { transform: rotate(var(--c, 0deg)); }
  to   { transform: rotate(calc(var(--c, 0deg) - 360deg)); }
}
@keyframes orbit-counter-ccw {
  from { transform: rotate(var(--c, 0deg)); }
  to   { transform: rotate(calc(var(--c, 0deg) + 360deg)); }
}

/* ---------- Services ---------- */

.svc { display: grid; gap: 0; border-top: 1px solid var(--line); }

/* One interaction, four expressions: the row you point at lifts out of the
   list while the rest recede. A pointer-tracked wash, a rule that draws down
   the left edge, the title stepping right, and the label warming — all on the
   same easing so they read as a single gesture rather than four effects. */
.svc__item {
  display: grid;
  gap: var(--s-3);
  padding-block: var(--s-6);
  /* Bleeds the hover wash slightly past the text edge. Kept at 1rem because
     the page gutter bottoms out at 20px — a wider pull would push past it
     and give the page a horizontal scrollbar on a phone. */
  padding-inline: var(--s-3);
  margin-inline: calc(var(--s-3) * -1);
  border-bottom: 1px solid var(--line);
  position: relative;
  isolation: isolate;
  transition: opacity 0.55s var(--ease);
}
@media (min-width: 800px) {
  .svc__item { grid-template-columns: 5rem 1fr 1.2fr; gap: var(--s-6); align-items: start; }
}

/* On About the principles sit in a wide content column with a capped measure,
   which left a band of dead space down the right. Two-up fills that band with
   content rather than just tightening the gaps, and halves the section height.
   Scoped to the label layout so the home page services list stays a single
   column of full-width rows. */
@media (min-width: 900px) {
  .cols--aside .svc { grid-template-columns: 1fr 1fr; column-gap: var(--s-7); }
  /* Two items per row means the top border of the list would otherwise be
     drawn twice over; the per-item bottom rule still separates the rows. */
  .cols--aside .svc__item { padding-block: var(--s-5); }
}

/* Wash follows the cursor within the row — --mx/--my are set by main.js. */
.svc__item::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  opacity: 0;
  pointer-events: none;
  background: radial-gradient(240px circle at var(--mx, 50%) var(--my, 50%),
              var(--bronze-wash), transparent 72%);
  transition: opacity 0.5s var(--ease);
}
.svc__item:hover::after { opacity: 1; }

/* Rule draws downward from the top edge. */
.svc__item::before {
  content: '';
  position: absolute;
  left: 0; top: 0; bottom: 0;
  width: 2px;
  background: var(--bronze);
  transform: scaleY(0);
  transform-origin: top;
  transition: transform 0.6s var(--ease);
}
.svc__item:hover::before { transform: scaleY(1); }

/* Everything else in the list recedes. This is the move that reads as
   expensive — attention is given by taking it away from the others. */
.svc.is-hovering .svc__item { opacity: 0.42; }
.svc.is-hovering .svc__item:hover { opacity: 1; }

.svc__name, .svc__num, .svc__desc {
  transition: transform 0.55s var(--ease), color 0.55s var(--ease);
  /* Widening the content column on About pushed this to ~98 characters a
     line. Capping at the shared measure keeps it readable there; on the home
     page the column is already narrower, so this is a no-op. */
  max-width: var(--measure);
}
.svc__item:hover .svc__name { transform: translateX(10px); }
.svc__item:hover .svc__desc { transform: translateX(4px); }
.svc__item:hover .svc__num  { color: var(--bronze-lift); }

.svc__num {
  font-size: var(--t-xs);
  letter-spacing: 0.2em;
  color: var(--bronze);
  padding-top: 0.55em;
  font-variant-numeric: tabular-nums;
}
.svc__name { font-size: var(--t-lg); }
.svc__desc { color: var(--ink-mute); }

.tag {
  display: inline-block;
  font-size: var(--t-xs);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--bronze);
  border: 1px solid var(--bronze-deep);
  border-radius: 100px;
  padding: 0.35em 0.85em;
  margin-top: var(--s-2);
}

/* ---------- Stats ---------- */

.stats {
  display: grid;
  gap: var(--s-6);
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
}

/* The rule draws itself before the figure starts counting. Sequencing is the
   whole move here: four numbers all arriving at once is a dashboard, the same
   four arriving in order is a considered page. The border became a pseudo
   because a border cannot be drawn — only revealed. */
.stat { position: relative; padding-top: var(--s-4); }
.stat::before {
  content: '';
  position: absolute;
  top: 0; left: 0; right: 0;
  height: 1px;
  background: var(--line-strong);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.9s var(--ease) var(--d, 0s);
}
.stat.is-in::before { transform: scaleX(1); }
.stat__n {
  font-family: var(--font-display);
  font-size: clamp(2.6rem, 1.6rem + 4vw, 5rem);
  line-height: 1;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}
.stat__l { font-size: var(--t-sm); color: var(--ink-mute); margin-top: var(--s-2); }

/* ---------- Process ---------- */

.proc { display: grid; gap: var(--s-5); counter-reset: step; }
@media (min-width: 860px) { .proc { grid-template-columns: repeat(3, 1fr); gap: var(--s-6); } }

/* Same focus behaviour as the services list, deliberately — one interaction
   language used twice reads as a system, two similar-but-different ones read
   as a collection of effects. Siblings recede, a rule draws across the top,
   and a wash follows the cursor inside the card. */
.proc__card {
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: var(--s-5);
  position: relative;
  overflow: hidden;
  transition: border-color 0.5s var(--ease), transform 0.5s var(--ease),
              opacity 0.55s var(--ease);
}
.proc__card:hover { border-color: var(--bronze-deep); transform: translateY(-4px); }

/* Content sits above the wash. Stacking is handled with a positive z-index
   rather than a negative one on the pseudo, because the card takes a
   transform on hover — and a transform creates a stacking context, which
   would flip a z-index:-1 layer in front of the text. */
.proc__card > * { position: relative; z-index: 1; }

/* Rule draws left to right rather than fading in place. */
.proc__card::before {
  content: '';
  position: absolute;
  inset: 0 0 auto 0;
  height: 1px;
  z-index: 1;
  background: linear-gradient(to right, var(--bronze), transparent);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.65s var(--ease);
}
.proc__card:hover::before { transform: scaleX(1); }

/* Cursor-tracked wash — --mx/--my are set by main.js. */
.proc__card::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 0;
  opacity: 0;
  pointer-events: none;
  background: radial-gradient(240px circle at var(--mx, 50%) var(--my, 50%),
              var(--bronze-wash), transparent 72%);
  transition: opacity 0.5s var(--ease);
}
.proc__card:hover::after { opacity: 1; }

.proc.is-hovering .proc__card { opacity: 0.45; }
.proc.is-hovering .proc__card:hover { opacity: 1; }

.proc__day { transition: color 0.55s var(--ease); }
.proc__card:hover .proc__day { color: var(--bronze-lift); }

.proc__day {
  font-size: var(--t-xs);
  letter-spacing: 0.26em;
  text-transform: uppercase;
  color: var(--bronze);
}
.proc__title { font-size: var(--t-md); color: var(--ink); margin-block: var(--s-3) var(--s-2); font-weight: 500; }
.proc__body { font-size: var(--t-sm); color: var(--ink-mute); }

/* ---------- Quote ---------- */

/* ---------- Latest from LinkedIn ----------
   Hand-curated cards rather than an embedded feed. Reuses the focus-and-
   recede language already running on the services list and the 90-day cards,
   so this reads as another instance of the same system rather than a new
   idiom bolted on. */

.linkedin__head {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--s-4);
  margin-bottom: var(--s-6);
}
.linkedin__title { margin-top: var(--s-3); max-width: 18ch; }

.linkedin__all {
  display: inline-flex;
  align-items: center;
  gap: 0.55em;
  font-size: var(--t-sm);
  color: var(--bronze);
  padding-block: 0.72rem;
  transition: color 0.35s var(--ease);
}
.linkedin__all:hover { color: var(--bronze-lift); }
.linkedin__arrow { transition: transform 0.4s var(--ease); }
.linkedin__all:hover .linkedin__arrow { transform: translateX(4px); }

.posts {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--s-4);
}
@media (min-width: 860px) { .posts { grid-template-columns: repeat(3, 1fr); } }

.post { display: flex; }

.post__link {
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
  width: 100%;
  padding: var(--s-5);
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  position: relative;
  overflow: hidden;
  transition: border-color 0.5s var(--ease), transform 0.5s var(--ease),
              opacity 0.55s var(--ease);
}
.post__link:hover { border-color: var(--bronze-deep); transform: translateY(-4px); }

/* Content above the wash. Positive z-index rather than a negative one on the
   pseudo, because the card takes a transform on hover — and a transform
   creates a stacking context that would flip a z-index:-1 layer in front. */
.post__link > * { position: relative; z-index: 1; }

.post__link::before {
  content: '';
  position: absolute;
  inset: 0 0 auto 0;
  height: 1px;
  z-index: 1;
  background: linear-gradient(to right, var(--bronze), transparent);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.65s var(--ease);
}
.post__link:hover::before { transform: scaleX(1); }

/* Cursor-tracked wash — --mx/--my are set by main.js, same as the services
   rows and the 90-day cards. */
.post__link::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 0;
  opacity: 0;
  pointer-events: none;
  background: radial-gradient(240px circle at var(--mx, 50%) var(--my, 50%),
              var(--bronze-wash), transparent 72%);
  transition: opacity 0.5s var(--ease);
}
.post__link:hover::after { opacity: 1; }

.posts.is-hovering .post__link { opacity: 0.45; }
.posts.is-hovering .post__link:hover { opacity: 1; }

/* A single fallback card should not stretch across the three-column grid. */
.post--fallback { grid-column: 1 / -1; max-width: 34rem; }

.post__meta {
  font-size: var(--t-xs);
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--bronze);
}
.post__text {
  font-size: var(--t-md);
  line-height: 1.3;
  color: var(--ink);
}
.post__cta {
  margin-top: auto;
  font-size: var(--t-sm);
  color: var(--ink-mute);
  display: inline-flex;
  align-items: center;
  gap: 0.45em;
  transition: color 0.4s var(--ease);
}
.post__link:hover .post__cta { color: var(--bronze); }
.post__arrow { transition: transform 0.4s var(--ease); }
.post__link:hover .post__arrow { transform: translateX(4px); }

/* ---------- Client review (retired) ----------
   The testimonial that used to sit here was invented, and a fabricated
   review is the single most damaging thing a trust-led site can carry.
   These rules are kept because the styles are sound — reinstate the markup
   the moment there is a real, attributed quote to put in it. */
   The label is what makes this a review rather than a pull-quote. It sits
   centred above the quote and reads as a section heading like every other
   one on the page. */
/* The section was mostly air: a short quote sitting inside full section
   padding read as a gap with words in it. Tightened, and the quote scaled up
   so it fills the space with type rather than the space swallowing it. */
.review { padding-block: calc(var(--s-9) * 0.62); }
.review__label { text-align: center; }

/* Width is set in px, not ch, because the line breaks are authored. A ch
   measure is an average across the alphabet, so it silently under-measured
   the longest line ("started working with it.") and the forced lines wrapped
   inside their own masks — which broke the wipe and put the quote on five
   lines instead of three. This is wide enough for the longest line at the
   largest size, with headroom for the fallback face before the webfont
   loads. */
.quote {
  text-align: center;
  max-width: min(640px, 100%);
  margin: var(--s-5) auto 0;
}

.quote__text {
  margin: 0;
  color: var(--ink);
  font-size: clamp(1.5rem, 0.9rem + 2.35vw, 3.05rem);
  line-height: 1.14;
  text-wrap: nowrap;          /* the breaks are set; never re-wrap them */
}
@media (max-width: 430px) {
  /* Below this the longest line cannot hold at a readable size, so let it
     wrap rather than shrink the type into illegibility. */
  .quote__text { text-wrap: wrap; }
}

/* Marks sit inline and slightly outdented so the first word still aligns
   optically with the lines beneath it. */
.quote__mark {
  color: var(--bronze);
  font-family: var(--font-display);
}
.quote__mark:first-child { margin-left: -0.44em; }

.quote__by {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--s-2);
  margin-top: var(--s-6);
  font-style: normal;
}

/* A short rule rather than a full-width one — it separates the attribution
   from the quote without cutting the section in half. It draws outward from
   the centre once the quote has landed, which is what turns four separate
   fades into one sequence.

   The default reveal transform is overridden: a rule that slides up reads as
   another element arriving, where one that draws reads as a line being
   ruled under what was just said. */
.quote__rule {
  display: block;
  width: 40px;
  height: 1px;
  background: var(--bronze);
  margin-bottom: var(--s-2);
}
[data-reveal].quote__rule { transform: scaleX(0); transform-origin: center; }
[data-reveal].quote__rule.is-in { transform: scaleX(1); }

/* Name carries more weight than role, so the person reads as a person and
   the job title as context. Flat attribution was part of why this looked
   like the site talking to itself. */
.quote__name {
  font-size: var(--t-md);
  color: var(--ink);
  font-weight: 500;
}
.quote__meta {
  font-size: var(--t-xs);
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--ink-mute);
}

/* ---------- CTA band ---------- */

.cta {
  position: relative;
  overflow: hidden;
  border-top: 1px solid var(--line);
  background:
    radial-gradient(70% 130% at 50% 100%, rgba(110,93,75,0.10), transparent 70%),
    var(--bg);
}
.cta__in { text-align: center; }
.cta__row { display: flex; flex-wrap: wrap; gap: var(--s-3); justify-content: center; margin-top: var(--s-6); }

/* ---------- Footer ---------- */

.ftr { border-top: 1px solid var(--line); padding-block: var(--s-7) var(--s-5); }
.ftr__grid {
  display: grid;
  gap: var(--s-6);
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  margin-bottom: var(--s-7);
}
.ftr__h {
  font-size: var(--t-xs);
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--ink-mute);
  margin-bottom: var(--s-4);
}
.ftr__list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--s-2); }
.ftr__list a {
  font-size: var(--t-sm);
  color: var(--ink-soft);
  display: inline-block;
  padding-block: 0.68rem;    /* 44px hit area on touch */
  transition: color 0.3s var(--ease);
}
.ftr__list a:hover { color: var(--bronze-lift); }

/* Footer and inline links borrow the nav's underline rather than inventing a
   second hover language: the rule wipes in from the left and out to the right,
   so the direction itself carries the sense of arriving and leaving. */
.ftr__list a,
.link {
  position: relative;
  background-image: linear-gradient(var(--bronze), var(--bronze));
  background-repeat: no-repeat;
  background-position: 0 100%;
  background-size: 0% 1px;
  transition: background-size 0.45s var(--ease), color 0.3s var(--ease);
  padding-bottom: 1px;
}
.ftr__list a:hover,
.ftr__list a:focus-visible,
.link:hover,
.link:focus-visible { background-size: 100% 1px; }

/* Footer links stay inline-block. Vertical padding does not contribute to the
   height of an inline box, so making them inline for the underline effect
   silently collapsed the 44px touch target the padding was there to create.
   They never wrap, so nothing is lost by keeping them a block. */
/* min-height rather than padding alone: the height of an inline-block follows
   its line box, so padding tuned against one font's metrics silently falls
   short when the face or line-height changes. This states the target directly.
   inline-flex keeps the box hugging the text, so the underline still spans the
   words rather than the whole column. */
.ftr__list a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
}

/* Prose links stay inline so the drawn underline follows them across a line
   break. These sit inside a paragraph and are hit at text size regardless. */
/* Inline links inside prose start underlined, because a link buried in a
   paragraph has to be findable without hovering. The hover swaps the static
   rule for the drawn one. */
.link {
  display: inline;
  color: var(--bronze-lift);
  background-image: linear-gradient(var(--bronze-deep), var(--bronze-deep));
  background-size: 100% 1px;
}
.link:hover, .link:focus-visible {
  background-image: linear-gradient(var(--bronze-lift), var(--bronze-lift));
}
.ftr__base {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-3);
  justify-content: space-between;
  align-items: center;
  border-top: 1px solid var(--line);
  padding-top: var(--s-5);
  font-size: var(--t-xs);
  color: var(--ink-mute);
}

/* ---------- Placeholder flag ----------
   Anything invented is visibly marked so it cannot ship by accident. */
.ph {
  /* Off by default. The marks were doing their job during the build, but they
     read as a typographic defect to anyone who does not know what they mean,
     and this page is shown to people. The tracking is untouched: the class,
     the tooltip and the counter all still work, and the counter's button
     turns the marks back on when you want to audit what is still invented. */
  border-bottom: 1px dashed transparent;
  cursor: help;
}
body.ph-on .ph { border-bottom-color: var(--bronze-deep); }

/* A dashed underline is meaningful beneath a run of text and meaningless
   beneath a logo — on the orbit it drew a line under every mark and straight
   through the globe. These keep the .ph class so they still carry their
   tooltips and stay in the placeholder count; only the underline is dropped. */
.orbit__badge.ph,
.orbit__core-badge.ph { border-bottom: 0; }

.phbar {
  position: fixed;
  right: var(--s-3);
  bottom: var(--s-3);
  z-index: 150;
  display: flex;
  align-items: center;
  gap: var(--s-2);
  background: var(--bg-elevated);
  border: 1px solid var(--line-strong);
  border-radius: 100px;
  padding: 0.5em 1em;
  font-size: var(--t-xs);
  color: var(--ink-mute);
  letter-spacing: 0.06em;
  box-shadow: 0 8px 30px rgba(26,23,20,0.14);
}
.phbar__dot { width: 7px; height: 7px; border-radius: 50%; background: var(--bronze); }
.phbar button {
  color: var(--bronze-lift);
  text-decoration: underline;
  font-size: var(--t-xs);
  min-height: 44px;
  padding-inline: 0.5em;
}

/* ---------- Reveal ----------
   transform + opacity only; both are compositor-friendly and never
   trigger layout. --d staggers without per-element JS timers. */

[data-reveal] {
  opacity: 0;
  transform: translate3d(0, 22px, 0);
  transition:
    opacity 0.85s var(--ease) var(--d, 0s),
    transform 0.85s var(--ease) var(--d, 0s);
  will-change: opacity, transform;
}

/* Fail-safe, second attempt.
   This used to be a delayed keyframe animation on [data-reveal]:not(.is-in).
   That worked as a safety net and silently destroyed every entrance: while an
   animation is attached it OWNS the property, and a property under animation
   control cannot transition. Adding .is-in removed the animation and changed
   opacity in the same style recalculation, so the browser resolved straight to
   the end value — a hard cut, never a fade.

   The real risk was only ever "JS never ran", so that is what this covers now.
   An inline script in <head> stamps .js on <html> before first paint; without
   it, every reveal is simply visible from the start. No animation involved, so
   nothing interferes with the transitions.

   The JS-ran-but-broke case is handled in main.js by a 1.2s timer that sweeps
   anything in view, independent of IntersectionObserver. */
html:not(.js) [data-reveal] { opacity: 1; transform: none; }
html:not(.js) .lines__i { transform: none; }
[data-reveal].is-in { opacity: 1; transform: none; }
[data-reveal].is-in { will-change: auto; }

/* Line-by-line headline entrance.
   The outer translate is dropped here — the inner masked slide is the whole
   effect, and doubling them reads as a lurch. */
[data-reveal].lines { transform: none; }
.lines { display: block; }

/* The display face is set at line-height 0.98, so the line box is shorter
   than the glyphs are tall — and overflow:hidden was slicing the descenders
   off, which is what cut the "g" in "navigate". The mask gains room below,
   cancelled by an equal negative margin so the layout is untouched. The
   inner slide then has to start lower than 105% or it would peek out of the
   taller mask at rest. */
.lines__l {
  display: block;
  overflow: hidden;
  padding-bottom: 0.18em;
  margin-bottom: -0.18em;
}
.lines__i {
  display: block;
  transform: translate3d(0, 135%, 0);
  transition: transform 1s var(--ease) var(--d, 0s);
}
.is-in .lines__i { transform: none; }

/* ==========================================================================
   Healthcheck quiz
   ========================================================================== */

.page-head {
  padding-block: calc(var(--s-8) + 1.5rem) var(--s-6);
  border-bottom: 1px solid var(--line);
  position: relative;
  overflow: hidden;
}
/* The signature field used to stop dead after the homepage fold and never
   return, which is why the inner pages read as bare colour. This carries it
   through at low density and part-ordered, so it is the same handwriting
   without competing with the page it sits behind. */
.page-head__canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 1;
}

/* Fades the field out from the left, so type always sits on clean paper and
   the texture reads strongest in the empty right-hand space. */
.page-head::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background:
    linear-gradient(to right, var(--bg) 0%, rgba(238,237,235,0.72) 34%, transparent 78%),
    linear-gradient(to bottom, var(--bg) 0%, transparent 26%, transparent 74%, var(--bg) 100%);
}

.page-head__in { position: relative; z-index: 2; }
.page-head__title { margin-block: var(--s-3) var(--s-4); }

.quiz { padding-block: var(--s-7) var(--s-9); }

.quiz__shell {
  max-width: 760px;
  margin-inline: auto;
}

/* Segmented progress — one bar per category. A single bar tells you how far
   through you are; five tell you what is being assessed and where you are
   inside it. The active segment lifts so the current area is unmistakable. */
.quiz__segments {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 5px;
  margin-bottom: var(--s-3);
}
.qseg {
  height: 2px;
  background: var(--line);
  border-radius: 2px;
  overflow: hidden;
  transition: height 0.45s var(--ease), background 0.45s var(--ease);
}
.qseg.is-active { height: 4px; background: var(--line-strong); }
.qseg__fill {
  display: block;
  height: 100%;
  background: var(--bronze);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.6s var(--ease);
}

.quiz__meta {
  display: flex;
  justify-content: space-between;
  font-size: var(--t-xs);
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--ink-mute);
  margin-bottom: var(--s-6);
}
.quiz__cat { color: var(--bronze); }

/* Questions arrive rather than appear. The card and its options enter on a
   short stagger, so moving between questions feels like turning a page
   instead of a swap. */
.quiz__card {
  border: 0; padding: 0; margin: 0; min-inline-size: 0;
  animation: quiz-in 0.5s var(--ease) both;
}
.quiz__card.is-back { animation-name: quiz-in-back; }

@keyframes quiz-in {
  from { opacity: 0; transform: translate3d(0, 14px, 0); }
  to   { opacity: 1; transform: none; }
}
@keyframes quiz-in-back {
  from { opacity: 0; transform: translate3d(0, -14px, 0); }
  to   { opacity: 1; transform: none; }
}

.quiz__q {
  font-family: var(--font-display);
  font-size: var(--t-lg);
  line-height: 1.15;
  color: var(--ink);
  padding: 0;
  margin-bottom: var(--s-5);
  display: block;
}

.quiz__opts { display: grid; gap: var(--s-2); }

.opt { position: relative; display: block; }

.opt input {
  position: absolute;
  opacity: 0;
  width: 1px; height: 1px;
  margin: 0;
}

.opt { animation: quiz-in 0.45s var(--ease) both; animation-delay: calc(0.06s + var(--i, 0) * 0.055s); }

.opt__box {
  display: flex;
  align-items: flex-start;
  gap: var(--s-3);
  min-height: 56px;
  padding: var(--s-3) var(--s-4);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--bg-raised);
  cursor: pointer;
  position: relative;
  overflow: hidden;
  transition: border-color 0.3s var(--ease), background 0.3s var(--ease),
              transform 0.3s var(--ease);
}
.opt__box:hover { border-color: var(--bronze-deep); background: var(--bg-panel); }

/* Choosing an answer sweeps a wash across it from the left rather than
   simply switching a colour on. It is the one moment in the quiz where the
   visitor commits to something, so it is the one worth animating. */
.opt__box::before {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--bronze-wash);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.5s var(--ease);
}
.opt input:checked + .opt__box::before { transform: scaleX(1); }

/* The number key that picks this answer. Only shown where a keyboard exists,
   because on a phone it is a promise you cannot keep. */
.opt__key {
  flex-shrink: 0;
  width: 1.55em;
  height: 1.55em;
  display: none;
  place-items: center;
  border: 1px solid var(--line-strong);
  border-radius: 5px;
  font-size: var(--t-xs);
  font-variant-numeric: tabular-nums;
  color: var(--ink-mute);
  margin-top: 0.15em;
  transition: border-color 0.3s var(--ease), color 0.3s var(--ease);
  position: relative;
}
@media (hover: hover) and (pointer: fine) { .opt__key { display: grid; } }
.opt input:checked + .opt__box .opt__key { border-color: var(--bronze); color: var(--bronze); }
.opt input:checked + .opt__box {
  border-color: var(--bronze);
  background: var(--bronze-wash);
}
/* The visually-hidden input still owns focus, so mirror its focus ring
   onto the visible box — otherwise keyboard users see nothing at all. */
.opt input:focus-visible + .opt__box {
  outline: 2px solid var(--bronze-lift);
  outline-offset: 3px;
}
.opt__box:active { transform: scale(0.99); }

.opt__dot {
  position: relative;
  width: 18px; height: 18px;
  border-radius: 50%;
  border: 1px solid var(--line-strong);
  flex-shrink: 0;
  margin-top: 0.28em;
  position: relative;
  transition: border-color 0.3s var(--ease);
}
.opt input:checked + .opt__box .opt__dot { border-color: var(--bronze); }
.opt__dot::after {
  content: '';
  position: absolute;
  inset: 3px;
  border-radius: 50%;
  background: var(--bronze);
  transform: scale(0);
  transition: transform 0.3s var(--ease);
}
.opt input:checked + .opt__box .opt__dot::after { transform: scale(1); }

.opt__text { color: var(--ink-soft); font-size: var(--t-body); position: relative; }
.opt input:checked + .opt__box .opt__text { color: var(--ink); }

/* ---------- Results: category breakdown ---------- */

.res__h { margin-top: var(--s-7); }
.res__sub { margin-top: var(--s-3); font-size: var(--t-sm); max-width: var(--measure); }
.res__item-cat {
  font-size: var(--t-xs);
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--bronze);
  margin-bottom: var(--s-1);
}

.cats { list-style: none; margin: var(--s-4) 0 0; padding: 0; display: grid; gap: var(--s-3); }

.cat {
  display: grid;
  grid-template-columns: 9.5rem 1fr auto;
  gap: var(--s-4);
  align-items: center;
  font-size: var(--t-sm);
}
@media (max-width: 560px) {
  .cat { grid-template-columns: 1fr auto; }
  .cat__track { grid-column: 1 / -1; order: 3; }
}
.cat__name { color: var(--ink); }
.cat__track { height: 3px; background: var(--line); border-radius: 3px; overflow: hidden; }

/* Bars draw in sequence, like the stat rules. The order is the point: five
   bars arriving together is a chart, five arriving in turn is a reading. */
.cat__fill {
  display: block;
  height: 100%;
  width: var(--w, 0%);
  background: var(--bronze);
  transform: scaleX(0);
  transform-origin: left;
  animation: cat-draw 0.85s var(--ease) var(--d, 0s) forwards;
}
@keyframes cat-draw { to { transform: scaleX(1); } }

.cat__score { color: var(--ink); font-variant-numeric: tabular-nums; }
.cat__of { color: var(--ink-mute); }

/* ---------- Results: optional capture ---------- */

.capture {
  margin-top: var(--s-8);
  padding-top: var(--s-6);
  border-top: 1px solid var(--line);
}
.capture__h { font-size: var(--t-lg); color: var(--ink); }
.capture__sub { margin-top: var(--s-2); font-size: var(--t-sm); max-width: 52ch; }

.capture__grid {
  display: grid;
  gap: var(--s-4);
  margin-top: var(--s-5);
}
@media (min-width: 640px) { .capture__grid { grid-template-columns: 1fr 1fr; } }
.capture__wide { grid-column: 1 / -1; }

.consent {
  display: flex;
  gap: var(--s-3);
  align-items: flex-start;
  margin-top: var(--s-4);
  font-size: var(--t-sm);
  color: var(--ink-mute);
  max-width: 62ch;
  cursor: pointer;
}
.consent input {
  flex-shrink: 0;
  width: 20px; height: 20px;
  margin-top: 0.15em;
  accent-color: var(--bronze);
  cursor: pointer;
}

.capture__actions { margin-top: var(--s-5); }

.capture__msg { margin-top: var(--s-4); font-size: var(--t-sm); min-height: 1.4em; }
.capture__msg.is-ok { color: var(--bronze); }
.capture__msg.is-error { color: var(--ink); border-left: 2px solid var(--bronze); padding-left: var(--s-3); }

/* Once sent, the fields go away — leaving a filled-in form on screen invites
   people to wonder whether it worked. */
.capture.is-sent .capture__grid,
.capture.is-sent .consent,
.capture.is-sent .capture__actions,
.capture.is-sent .capture__sub { display: none; }

.quiz__nav {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--s-3);
  margin-top: var(--s-6);
}
.quiz__back {
  font-size: var(--t-sm);
  color: var(--ink-mute);
  min-height: 44px;
  padding-inline: 0.25rem;
  transition: color 0.3s var(--ease);
}
.quiz__back:hover { color: var(--ink); }
.quiz__back[hidden] { visibility: hidden; display: block; }

/* ---- Results ---- */

.res__score {
  display: flex;
  align-items: baseline;
  gap: var(--s-3);
  margin-bottom: var(--s-5);
}
.res__n {
  font-family: var(--font-display);
  font-size: clamp(3.4rem, 2rem + 6vw, 6.5rem);
  line-height: 1;
  color: var(--bronze);
  font-variant-numeric: tabular-nums;
}
.res__of { font-size: var(--t-md); color: var(--ink-mute); }

.res__band {
  display: inline-block;
  font-size: var(--t-xs);
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--bg);
  background: var(--bronze);
  border-radius: 100px;
  padding: 0.45em 1.1em;
  margin-bottom: var(--s-4);
  font-weight: 600;
}

/* CTA under the homepage plan section. */
.plan__cta {
  margin-top: var(--s-6);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-3) var(--s-4);
}
.plan__cta-note { font-size: var(--t-sm); }

/* ==========================================================================
   HERO — orbit treatment

   Scoped under .hero--orbit. It began as a side-by-side preview and was
   promoted to the homepage on 16 Aug 2026; the scoping is kept so the plain
   .hero used by the inner pages is unaffected.

   The idea: the diagram stops being an illustration beside the argument and
   becomes the space the argument sits in. Three things carry that.

   1. Scale past the frame. The rings are sized against the viewport and run
      off both edges. A circle you can see all of is a widget; one that leaves
      the frame implies a system continuing beyond the page.
   2. A quiet centre. Ring one is held wider than the text column so no mark
      ever crosses a word, which is what lets the logos stay full colour
      without the hero turning into a partner-logo wall.
   3. Depth ordering. Filaments faintest, then rings, then the sphere, then
      type. Each layer sits in front of the last rather than beside it.
   ========================================================================== */

.hero--orbit { display: grid; place-items: center; text-align: center; }

/* Atmosphere, not subject. The flow field stays for depth but drops back so
   two moving systems are not competing for the same attention. */
.hero--orbit .hero__canvas { opacity: 0.28; }
.hero--orbit .hero__canvas--front { opacity: 0.16; }

.orbit--hero {
  position: absolute;
  top: 50%; left: 50%;
  /* Sized on the larger axis so the rings always overrun the short edge.
     min() keeps it from running away on an ultrawide display. */
  /* Solved against a 1440x900 laptop, the tightest common desktop. Two
     constraints fight each other: ring one has to sit outside the text column
     so no mark crosses a word, and ring three has to stay inside the viewport
     height so every mark is actually on screen. 102vh is where both hold —
     ring one clears the type by ~57px, ring three lands ~46px inside the fold.
     The first attempt at 190vh satisfied the first constraint and threw the
     logos off the screen entirely, which defeats the point of the treatment. */
  width: min(102vh, 92vw);
  height: min(102vh, 92vw);
  /* --core-align is written by main.js on phones, to slide the rings and the
     sphere until their centre matches the Salesforce mark. Zero by default, so
     desktop keeps the composition it was tuned with. */
  transform: translate(-50%, calc(-50% + var(--core-align, 0px)));
  aspect-ratio: auto;
  pointer-events: none;   /* the hero's buttons must stay clickable */
  z-index: 0;
}

/* Wider and further apart than the diagram's 50/70/90. The gap in the middle
   is where the headline lives. */
/* Ring one pushed out to 62%: at 56% its radius was 257px against a headline
   whose longest line is wider than that, so marks were crossing the type on
   every revolution. Two and three follow to keep the spacing even, and three
   still lands inside the viewport height. */
.orbit--hero .orbit__ring--1 { width: 62%; height: 62%; }
.orbit--hero .orbit__ring--2 { width: 76%; height: 76%; }
.orbit--hero .orbit__ring--3 { width: 90%; height: 90%; }

/* Rings recede; the marks they carry do not. A hairline that reads correctly
   at 600px is heavy-handed at 1400. */
.orbit--hero .orbit__ring { border-color: color-mix(in srgb, var(--line) 55%, transparent); }

/* Bigger than the diagram's 30px, because everything around them grew. Full
   colour, as asked — legible at this size and recognisable at a glance. */
.orbit--hero .orbit__badge { width: clamp(34px, 3vw, 52px); }
.orbit--hero .orbit__badge img { width: 100%; height: 100%; }

/* The sphere sits behind the type. Its dots are faint enough to read through,
   and the Salesforce mark on top of it stays sharp. */
/* --- The core, and the hole in the type that reveals it ---------------

   The mark sits at the centre of the rings, and the headline occupies the
   centre too, so at any size the mark was entirely behind the type. Rather
   than shrink one or move the other off-axis, the copy parts around it: the
   headline stops short, the core shows through the gap, and the lede resumes
   below. The type reads as opening around the thing it is describing.

   The mark is nudged up into that gap rather than pinned to the exact
   geometric centre. Forcing the gap onto the centre instead would mean the
   blocks above and below it matching in height, which they do not, and which
   would break the moment a line wrapped differently. The sphere is wide
   enough that a mark sitting slightly high still reads as inside it. */
.hero__core {
  /* fit-content, not block. As a full-width block, `margin: auto` has nothing
     to centre — the box already fills the column — so the mark sat hard against
     its left edge. Shrinking the box to the image is what lets auto margins
     do their job. */
  display: block;
  width: fit-content;
  margin: var(--s-5) auto;
  line-height: 0;
  pointer-events: none;
}
/* Scales with the viewport, because the sphere behind it does. At a fixed
   84px the mark was 24% of the sphere on a 1024px screen and only 18% on a
   1680px one — it appeared to shrink the wider the window got. The clamp holds
   it at roughly 30% throughout. */
.hero__core img {
  /* Kept in the layout, but not painted. The visible mark is now drawn into
     the sphere's canvas, between its back and front hemispheres, so particles
     cross in front of it. This element stays as an invisible spacer: it is
     what opens the gap in the copy, and what initCoreAlign measures to slide
     the rings until the sphere's centre lands exactly here. Remove it and the
     gap closes and the mark drifts back behind the headline. */
  width: clamp(92px, 8.4vw, 148px);
  height: clamp(92px, 8.4vw, 148px);
  display: block;
  visibility: hidden;
}

/* Type above every layer, and centred. */
.hero--orbit .hero__in { position: relative; z-index: 2; }
/* Every text element is shrunk to its content and centred. As full-width
   blocks their boxes spanned the whole wrap, which made it impossible to
   reason about how close a mark could safely pass — and left the CTAs with a
   1220px-wide hit area for two buttons. */
.hero--orbit .hero__title,
.hero--orbit .eyebrow,
.hero--orbit .hero__note,
.hero--orbit .hero__cta { width: fit-content; margin-inline: auto; max-width: 100%; }
.hero--orbit .hero__lede {
  margin-inline: auto;
  max-width: 38ch;   /* narrower than the default measure. Centred text is
                        harder to track across, and the column width is what
                        sets how close ring one can come. */
}
.hero--orbit .hero__cta { justify-content: center; }
.hero--orbit .hero__note { margin-inline: auto; }

/* Marks pass behind the type, and that is a decision rather than an oversight.

   For a mark to clear the text at every angle, the ring radius has to exceed
   the distance to the text block's CORNER, not its edge — 434px for this
   block, so 455px once the mark's own width is counted, or 910px of diameter.
   The viewport is 900px tall. There is no ring that both clears the copy and
   stays on screen, so the choice is to cut the hero copy or to let the marks
   travel behind it.

   Behind it, then — which is the same move the hero already makes with the
   filament strands crossing the headline. The difference between depth and
   interference is entirely in this wash: an opaque-centred halo, sized to the
   type, that keeps the reading surface calm while the marks stay visible
   around it. Two stops rather than one, so the falloff is gradual enough not
   to read as a plate behind the words. */
.hero--orbit .hero__in::before {
  content: '';
  position: absolute;
  /* Hugs the type rather than the section. At -14%/-10% the halo spanned
     roughly 1580x830 — most of the hero — so marks dimmed unevenly across a
     wide area and read as badly rendered rather than layered. Pulled in to
     sit just around the centre column, so everything outside it is at full
     strength and the falloff is short enough to look intentional. */
  inset: 10% 27%;
  z-index: -1;
  /* A ring, not a disc.

     The mark is now painted inside the sphere's canvas, which sits below this
     layer — so a halo that was opaque at its centre dimmed the one thing the
     composition exists to show. Measured on the rendered page: the mark
     reaches about 49% of this gradient's radius, while the headline and lede
     occupy roughly 57% outward. Clear through the middle, protective further
     out, which serves both. */
  background:
    radial-gradient(closest-side,
      transparent 0%,
      transparent 46%,
      color-mix(in srgb, var(--bg) 52%, transparent) 64%,
      color-mix(in srgb, var(--bg) 58%, transparent) 78%,
      color-mix(in srgb, var(--bg) 24%, transparent) 90%,
      transparent 100%);
  pointer-events: none;
}

/* Marks stay at full strength now; the wash alone carries the separation. */
.orbit--hero .orbit__badge img { opacity: 1; }

/* Rings brought back up too — at 55% they read as a smudge behind the type
   rather than as structure. */
.orbit--hero .orbit__ring { border-color: color-mix(in srgb, var(--line) 85%, transparent); }
.orbit--hero .orbit__core { opacity: 1; }

/* Below the split the rings would crowd the type, so the composition tightens:
   the orbit sizes off width alone and the sphere grows back to being a visible
   anchor rather than a whisper behind the headline. */
@media (max-width: 900px) {
  /* Sized off width, not height: a phone's viewport is tall and narrow, so
     102vh would make a ring set far wider than the screen and push every mark
     out of sight. 168vw bleeds the outer rings past both edges — the same
     "fragment of something larger" the desktop composition has — while the
     inner ring still clears the text column. .hero has overflow: hidden, so
     the bleed can never create a sideways scroll. */
  .orbit--hero {
    width: min(168vw, 125vh);
    height: min(168vw, 125vh);
  }
  /* Bigger than the old 30px. Everything around them grew, and a 30px mark on
     a 375px screen is smaller in proportion than a 43px mark on a laptop. */
  .orbit--hero .orbit__badge { width: 34px; }
  .orbit--hero .orbit__core { opacity: 0.9; }

  /* Smaller mark and tighter margins, so the mark costs less of a phone's
     first screen. No offset to tune: being in flow, it is in the gap by
     construction at every width. */
  .hero__core { margin-block: var(--s-4); }
  .hero__core img { width: 84px; height: 84px; }
}

/* A flow field filling a whole section. The canvas sits behind the content,
   the content is lifted into its own stacking level, and the field is held
   back far enough that body copy never has to fight it. */
.section--field { position: relative; overflow: hidden; isolation: isolate; }
.section--field > .wrap { position: relative; z-index: 1; }
.panel-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 0;
  /* Was 0.5, which halved a field that was already faint. The strength now
     comes from the field's own alpha and saturation rather than from dimming
     a stronger image, so the depth range survives. */
  opacity: 0.85;
  pointer-events: none;
  mix-blend-mode: multiply;   /* ink settling into paper, not paint on top */
}

/* Above the copy. Fainter still, and never intercepting a click. */
.panel-canvas--front { z-index: 2; opacity: 0.5; }

/* ---------- Line-masked headings ----------
   Each word sits in its own overflow box and rises from beneath it. Words
   sharing a line share a delay, so the effect reads as lines lifting rather
   than a ripple across words. main.js does the splitting. */

.w {
  display: inline-block;
  overflow: hidden;
  /* An overflow box sized exactly to the text would shear descenders off the
     g and y. The padding gives them room and the negative margin takes the
     space back out of the layout. */
  padding-bottom: 0.14em;
  margin-bottom: -0.14em;
  vertical-align: baseline;
}
.w__i {
  display: inline-block;
  /* Travel has to clear the mask completely — anything less than 100% starts
     the word clipped through the middle of its glyphs, which reads as broken
     rather than subtle. Restraint comes from the timing instead. */
  transform: translateY(105%);
  transition: transform 0.72s var(--ease);
  will-change: transform;
}
[data-split].is-in .w__i { transform: translateY(0); }

/* The heading itself must not also fade, or the mask is pointless — the words
   would simply appear behind it. Once split, the block-level reveal is handed
   over to the words. */
[data-split].is-split { opacity: 1; transform: none; }

/* ---------- The generated 90-day plan ----------
   A spine, not a card grid. The plan's whole argument is that the work has an
   order, so it reads as one continuous vertical line with phases hung off it —
   the sequence is the design. Cards would say "pick one"; a spine says "then". */

.pln { margin-top: var(--s-6); }

.pln__phase {
  position: relative;
  padding-left: var(--s-6);
  padding-bottom: var(--s-6);
}
.pln__phase:last-child { padding-bottom: 0; }

/* The spine. Runs the full height of each phase and stops at the last one,
   so the line ends with the plan rather than trailing into whitespace. */
.pln__phase::before {
  content: '';
  position: absolute;
  left: 0;
  top: 0.55em;
  bottom: 0;
  width: 1px;
  background: var(--line);
}
.pln__phase:last-child::before { bottom: auto; height: 100%; }

/* Node on the spine, aligned to the cap-height of the phase label. */
.pln__phase::after {
  content: '';
  position: absolute;
  left: -3px;
  top: 0.45em;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--bronze);
}

.pln__day {
  font-size: var(--t-xs);
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--bronze);
  font-weight: 500;
}

.pln__items {
  list-style: none;
  margin: var(--s-4) 0 0;
  padding: 0;
  display: grid;
  gap: var(--s-3);
}

.pln__item {
  padding: var(--s-4);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--bg-raised);
}

.pln__cat {
  font-size: var(--t-xs);
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--ink-mute);
  margin-bottom: var(--s-1);
}
.pln__t { font-size: var(--t-md); color: var(--ink); font-weight: 500; }
.pln__b { font-size: var(--t-sm); color: var(--ink-mute); margin-top: var(--s-2); }

/* Effort and owner. Quiet by default — they are reference, not argument. */
.pln__meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-1) var(--s-4);
  margin-top: var(--s-3);
  padding-top: var(--s-3);
  border-top: 1px solid var(--line);
  font-size: var(--t-xs);
  color: var(--ink-mute);
  letter-spacing: 0.04em;
}
.pln__meta span:first-child { color: var(--bronze); }

.pln__note {
  margin-top: var(--s-5);
  padding-left: var(--s-6);
  font-size: var(--t-sm);
  color: var(--ink-mute);
  max-width: var(--measure);
}

/* On paper the spine survives, but phases must not split across a page —
   a plan cut in half mid-month reads as two plans. */
@media print {
  .pln__phase { break-inside: avoid; page-break-inside: avoid; }
  .pln__item  { break-inside: avoid; page-break-inside: avoid; }
}

.res__list { list-style: none; padding: 0; margin: var(--s-6) 0 0; display: grid; gap: var(--s-3); }
.res__item {
  display: grid;
  gap: var(--s-2);
  padding: var(--s-4);
  border: 1px solid var(--line);
  border-left: 2px solid var(--bronze);
  border-radius: var(--radius-lg);
  background: var(--bg-raised);
}
.res__item h3 { font-size: var(--t-md); color: var(--ink); font-weight: 500; }
.res__item p { font-size: var(--t-sm); color: var(--ink-mute); }

.res__cta { display: flex; flex-wrap: wrap; gap: var(--s-3); margin-top: var(--s-7); }

/* ---------- Contact form ---------- */

.form { display: grid; gap: var(--s-4); max-width: 560px; }
.field { display: grid; gap: var(--s-2); }
.field label { font-size: var(--t-sm); color: var(--ink); }
.field input, .field textarea, .field select {
  font: inherit;
  font-size: var(--t-body);
  color: var(--ink);
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: 0.85em 1em;
  min-height: 52px;
  width: 100%;
  transition: border-color 0.3s var(--ease), background 0.3s var(--ease);
}
.field textarea { min-height: 140px; resize: vertical; }
.field input:hover, .field textarea:hover { border-color: var(--line-strong); }
.field input:focus, .field textarea:focus {
  border-color: var(--bronze);
  background: var(--bg-panel);
  outline: none;
}

/* The field is where a visitor is most committed, so it gets the most
   considered response. The label warms and tightens, and a rule draws along
   the bottom edge from the left — the same gesture as the nav underline and
   the card rules, arriving here rather than a new idea. */
.field label {
  transition: color 0.35s var(--ease), letter-spacing 0.35s var(--ease);
  width: fit-content;
}
.field:focus-within label { color: var(--bronze-lift); letter-spacing: 0.01em; }

/* A drawn rule was the first instinct here, to echo the nav and the cards,
   but it reads badly across a rounded border — the line runs straight past
   the corner radius. These fields already carry a full border, so the focus
   response belongs on the border itself: it warms, and a soft ring expands
   just outside it. */
.field input, .field textarea, .field select {
  box-shadow: 0 0 0 0 rgba(110, 93, 75, 0);
  transition: border-color 0.3s var(--ease), background 0.3s var(--ease),
              box-shadow 0.4s var(--ease);
}
.field input:hover, .field textarea:hover { border-color: var(--bronze-deep); }
/* Restates border-color as well as the ring. A focused field is almost always
   hovered too, and the hover rule above would otherwise win on source order
   and hold the border at the paler decorative bronze. */
.field input:focus, .field textarea:focus, .field select:focus {
  border-color: var(--bronze);
  box-shadow: 0 0 0 3px rgba(110, 93, 75, 0.08);
}

.field__hint { transition: color 0.35s var(--ease); }
.field:focus-within .field__hint { color: var(--ink-soft); }
.field input:focus-visible, .field textarea:focus-visible {
  outline: 2px solid var(--bronze-lift);
  outline-offset: 2px;
}
.field__hint { font-size: var(--t-xs); color: var(--ink-mute); }

.form__note {
  font-size: var(--t-sm);
  color: var(--ink-mute);
  border-left: 2px solid var(--bronze-deep);
  padding-left: var(--s-3);
}

/* ---------- Simple two-column prose ---------- */

.cols { display: grid; gap: var(--s-7); }
@media (min-width: 900px) { .cols { grid-template-columns: 1fr 1.15fr; gap: var(--s-8); } }

/* Label-and-content variant. The plain .cols split is for two columns that
   both carry weight (the contact page's details beside its form). Where the
   left column holds only a section label, an equal split strands it in half a
   screen of nothing — so the label gets a narrow measure and sticks alongside
   the prose as it scrolls. The space beside it then reads as a deliberate
   margin rather than a hole, which is the difference between generous and
   empty. */
@media (min-width: 900px) {
  .cols--aside { grid-template-columns: minmax(0, 14rem) minmax(0, 1fr); gap: var(--s-8); }
  .cols--aside > :first-child {
    position: sticky;
    top: calc(var(--s-8) + var(--s-3));
    align-self: start;
  }
}

.prose { display: grid; gap: var(--s-4); }
.prose p { max-width: var(--measure); }

/* ==========================================================================
   Phone — layout decisions, not a compressed desktop.

   Everything below is a different arrangement rather than the same one made
   smaller. The orbit is swapped for a grid, the stats and footer become 2×2
   instead of a single tall stack, the marquee drops to one row, and the CTAs
   go full width. Each is a choice about what a 375px screen should do.
   ========================================================================== */

.orbit-grid { display: none; }

@media (max-width: 720px) {

  /* --- Density. On a phone the page ran to 8.8 screens, and roughly 3.8 of
     those were the services list and the 90-day cards. Nothing is removed;
     the padding that reads as generous at 1440px simply reads as dead space
     at 375px, where the eye travels by scrolling rather than by scanning. --- */

  /* --s-9 already bottoms out at its 4rem minimum by this width, which is
     exactly --s-7 — so stepping down to --s-7 here would change nothing.
     --s-6 is the first value that actually moves. */
  .section { padding-block: var(--s-6); }
  .shead { margin-bottom: var(--s-6); }

  /* (.svc__item padding is set further down this same block, where the row
     layout is rearranged. Setting it here as well would just be two rules
     fighting over one value.) */

  .proc__card { padding: var(--s-4); }
  .proc { gap: var(--s-3); }

  .post__link { padding: var(--s-4); }
  .posts { gap: var(--s-3); }
  .linkedin__head { margin-bottom: var(--s-5); }

  /* The diagram sits directly under the prose it illustrates; 96px between
     them on a narrow screen reads as a break rather than a pairing. */
  .maze { gap: var(--s-5); }

  /* --- Two across beats one tall. A single column turned four stats into
     four full screens of scrolling for four short facts. --- */
  .stats { grid-template-columns: 1fr 1fr; gap: var(--s-5) var(--s-4); }
  .stat__n { font-size: clamp(2rem, 9vw, 2.8rem); }

  .ftr__grid { grid-template-columns: 1fr 1fr; gap: var(--s-5) var(--s-4); }

  /* --- One marquee row on a phone. Two competing lines of large serif in a
     375px viewport is noise, not rhythm. --- */
  .marquee--echo { display: none; }

  /* --- Full-width CTAs. Thumb-reachable and unambiguous, rather than two
     small pills sharing a line. --- */
  .hero__cta, .cta__row { flex-direction: column; align-items: stretch; }
  .hero__cta .btn, .cta__row .btn { width: 100%; }

  /* --- Service rows: the category label sits inline with the name rather
     than stacked above it, which was spending a whole line on one word. --- */
  .svc__item { gap: var(--s-2); padding-block: var(--s-5); }
  .svc__num {
    padding-top: 0;
    font-size: 0.68rem;
    letter-spacing: 0.24em;
  }
  .svc__name { font-size: 1.45rem; }

  /* --- Quiz: bigger answer targets and a question that does not need
     scrolling past to reach the options. --- */
  .quiz__q { font-size: 1.35rem; margin-bottom: var(--s-4); }
  .opt__box { min-height: 62px; padding: var(--s-3); }
  .quiz { padding-block: var(--s-5) var(--s-8); }

  /* --- Inner page heads carry less top padding; there is no wide screen to
     balance against. --- */
  .page-head { padding-block: calc(var(--s-7) + 2rem) var(--s-5); }

  /* --- The placeholder badge sits above the thumb zone rather than over the
     bottom CTA. --- */
  .phbar { right: var(--s-2); bottom: var(--s-2); font-size: 0.66rem; }
}



/* ==========================================================================
   Print — the healthcheck result as a document

   The printed sheet is not the web page with the chrome removed; it is a
   different artefact with a different job. It leaves the site entirely, so
   it has to identify itself, carry the full answers, and print the booking
   link as readable text rather than as an unclickable underline.

   Nothing here depends on background colours surviving. Browsers strip them
   by default and users override print-colour settings freely, so anything
   that would become invisible without its background is redrawn with borders
   and text instead.
   ========================================================================== */

/* Screen-only by default; the print block turns these on. */
.res__printhead,
.res__printfoot,
.answers { display: none; }

@media print {

  /* Zero page margin so the warm paper runs edge to edge in the PDF —
     @page margins would leave a white border around a coloured sheet. The
     margin becomes body padding instead. */
  @page { size: A4; margin: 0; }

  /* Everything that is site rather than result. */
  .hdr, .menu, .ftr, .phbar, .page-head, .marquee-stack,
  .quiz__segments, .quiz__meta, .quiz__nav, .res__cta,
  .capture, .skip, .sr-live, canvas { display: none !important; }

  /* print-color-adjust is inherited, so declaring it here carries the brand
     through every descendant. Chrome honours it when saving to PDF; a
     physical printer with "background graphics" switched off will drop the
     fills, which is why nothing below relies on a fill alone to be legible. */
  html, body {
    background: var(--bg) !important;
    -webkit-print-color-adjust: exact;
    print-color-adjust: exact;
  }
  body {
    color: var(--ink);
    font-size: 10.5pt;
    line-height: 1.55;
    padding: 15mm 16mm 14mm;
  }
  body::before { display: none; }          /* screen grain — paper has its own */

  .quiz { padding: 0; }
  .quiz__shell, .wrap { max-width: none; margin: 0; padding: 0; }

  .res__printhead, .res__printfoot, .answers { display: block; }

  /* ---- Masthead: the real wordmark lockup, not a line of text ---- */
  .res__printhead {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    gap: 12pt;
    padding-bottom: 9pt;
    border-bottom: 0.75pt solid var(--bronze);
    margin-bottom: 20pt;
  }
  .res__printmark-a,
  .res__printmark-b { display: block; color: var(--bronze); line-height: 1; }
  .res__printmark-a {
    font-size: 10pt;
    font-weight: 600;
    letter-spacing: 0.34em;
    text-indent: 0.34em;
  }
  .res__printmark-b {
    font-size: 5pt;
    font-weight: 500;
    letter-spacing: 0.58em;
    text-indent: 0.58em;
    margin-top: 3pt;
    opacity: 0.85;
  }
  .res__printmeta {
    text-align: right;
    font-size: 7.5pt;
    line-height: 1.6;
    letter-spacing: 0.2em;
    text-transform: uppercase;
    color: var(--ink-mute);
  }

  /* ---- Band and score: the site's own treatment ---- */
  .res__band {
    background: var(--bronze) !important;
    color: #FCFBFA !important;
    border: 0;
    padding: 3.5pt 10pt;
    font-size: 7.5pt;
    letter-spacing: 0.24em;
  }
  .res__score { margin: 10pt 0 6pt; align-items: baseline; gap: 8pt; }
  .res__n {
    font-family: var(--font-display);
    font-size: 46pt;
    color: var(--ink);
    line-height: 1;
  }
  .res__of { font-size: 11pt; color: var(--ink-mute); }
  .lede { font-size: 11.5pt; color: var(--ink-soft); max-width: 62ch; }

  .eyebrow, .res__h {
    color: var(--bronze) !important;
    font-size: 7.5pt;
    letter-spacing: 0.3em;
    margin-top: 20pt;
  }
  .res__sub { font-size: 9.5pt; color: var(--ink-mute); margin-top: 5pt; }
  .res__sub strong { color: var(--ink); }

  /* ---- Category bars: bronze on the page's own line colour ---- */
  .cats { gap: 7pt; margin-top: 12pt; }
  .cat { grid-template-columns: 8.5rem 1fr auto; gap: 12pt; font-size: 9.5pt; }
  .cat__name { color: var(--ink); }
  .cat__track {
    height: 4pt;
    background: rgba(26, 23, 20, 0.12) !important;
    border-radius: 4pt;
  }
  .cat__fill {
    background: var(--bronze) !important;
    animation: none !important;
    transform: scaleX(1) !important;
  }
  .cat__score { font-size: 9.5pt; }

  /* ---- Recommendations: the raised card, flattened for paper ---- */
  .res__list { gap: 8pt; margin-top: 12pt; }
  .res__item {
    background: var(--bg-raised) !important;
    border: 0.5pt solid var(--line);
    border-left: 2pt solid var(--bronze);
    border-radius: 3pt;
    padding: 9pt 11pt;
  }
  .res__item-cat {
    color: var(--bronze) !important;
    font-size: 6.5pt;
    letter-spacing: 0.22em;
  }
  .res__item h3 { font-size: 10.5pt; color: var(--ink); margin-top: 2pt; }
  .res__item p { color: var(--ink-mute); font-size: 9pt; }

  /* ---- Answers ---- */
  .answers {
    list-style: none;
    padding: 14pt 0 0;
    margin: 22pt 0 0;
    border-top: 0.75pt solid var(--bronze);
    columns: 2;
    column-gap: 14pt;
  }
  .answers::before {
    content: 'Your answers';
    display: block;
    column-span: all;
    color: var(--bronze);
    font-weight: 500;
    letter-spacing: 0.3em;
    text-transform: uppercase;
    font-size: 7.5pt;
    margin-bottom: 10pt;
  }
  .ans { margin-bottom: 8pt; }
  .ans__cat {
    font-size: 6.5pt;
    letter-spacing: 0.2em;
    text-transform: uppercase;
    color: var(--bronze);
  }
  .ans__q { font-size: 8.5pt; color: var(--ink); margin-top: 1pt; }
  .ans__a { font-size: 8.5pt; color: var(--ink-mute); }
  .ans__score { color: var(--bronze); }

  /* ---- Footer ---- */
  .res__printfoot {
    margin-top: 18pt;
    padding-top: 8pt;
    border-top: 0.5pt solid var(--line-strong);
    display: flex;
    justify-content: space-between;
    gap: 12pt;
    font-size: 8pt;
  }
  .res__printfoot-cta { color: var(--ink-soft); }
  .res__printfoot-tag {
    font-family: var(--font-display);
    font-size: 9.5pt;
    color: var(--bronze);
  }

  /* Never split a card, a bar or an answer across a page break. */
  .res__item, .cat, .ans, .res__printhead,
  .res__printfoot { break-inside: avoid; page-break-inside: avoid; }
  .eyebrow, .res__h { break-after: avoid; page-break-after: avoid; }

  /* Links print as text; the URLs that matter are spelled out already. */
  a { text-decoration: none; color: inherit; }
}

/* ==========================================================================
   Reduced motion — honour it properly: kill movement, keep the site legible
   and fully revealed. Never leave reveal elements stuck at opacity 0.
   ========================================================================== */
/* ---------- Cross-document transitions ----------
   Native browser navigation transitions. Where unsupported, the whole block
   is ignored and navigation stays exactly as it is today, so there is no
   fallback to maintain. Kept to a short cross-fade: anything with movement
   in it fights the reveal animations that run immediately after arrival. */
@view-transition { navigation: auto; }

::view-transition-old(root) {
  animation: vt-out 0.22s var(--ease) both;
}
::view-transition-new(root) {
  animation: vt-in 0.32s var(--ease) both;
}
@keyframes vt-out { to { opacity: 0; } }
@keyframes vt-in  { from { opacity: 0; } }

/* The header persists across pages, so it should not cross-fade with the
   body — it gets its own name and simply stays put. */
.hdr { view-transition-name: site-header; }
::view-transition-old(site-header),
::view-transition-new(site-header) { animation: none; mix-blend-mode: normal; }

/* ---------- Cursor ring ----------
   A single ring that lags a few frames behind the pointer and swells over
   anything interactive. Pointer-events are off, so it can never intercept a
   click. Hidden entirely on touch and for reduced motion — both are handled
   in JS by not creating it at all, but the guard is here too. */
.cursor {
  position: fixed;
  top: 0; left: 0;
  width: 20px; height: 20px;
  margin: -10px 0 0 -10px;
  /* Semi-transparent rather than a solid bronze hairline. At full strength the
     ring competes with the cursor it is following. */
  border: 1px solid rgba(110, 93, 75, 0.4);
  border-radius: 50%;
  pointer-events: none;
  z-index: 90;
  opacity: 0;
  transition: opacity 0.3s var(--ease), width 0.3s var(--ease),
              height 0.3s var(--ease), margin 0.3s var(--ease),
              background 0.3s var(--ease), border-color 0.3s var(--ease);
  will-change: transform;
}
.cursor.is-live { opacity: 0.8; }
/* Over a link or a card the ring grows and fills faintly. Over a magnetic
   button it disappears instead: that button already responds by moving toward
   the pointer, and two responses to one gesture reads as noise. */
.cursor.is-over {
  width: 34px; height: 34px; margin: -17px 0 0 -17px;
  background: rgba(110, 93, 75, 0.04);
  border-color: rgba(110, 93, 75, 0.45);
}
.cursor.is-hidden { opacity: 0; }

@media (hover: none), (pointer: coarse) { .cursor { display: none; } }

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

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

  /* Kill the fail-safe outright. Its `both` fill would apply the from-state
     (opacity 0) for the whole 4s delay, and animation fill wins over normal
     declarations regardless of source order — so leaving it on would blank
     the page for four seconds for precisely the people who opted out of
     motion. The rules below then reveal everything immediately. */
  [data-reveal]:not(.is-in),
  [data-reveal]:not(.is-in) .lines__i { animation: none !important; }

  [data-reveal] { opacity: 1; transform: none; }
  .lines__i { transform: none; }
  /* Land the eyebrow on its final tracking rather than the wide start value. */
  .hero .eyebrow[data-reveal] { letter-spacing: 0.32em; }

  /* Nothing to suppress here any more — the ring only moves in direct
     response to the pointer, which is not ambient motion. Left documented so
     it is clear the omission is deliberate rather than an oversight. */

  /* Words land in place; the split is skipped in JS, but if a stale split
     survives a preference change mid-session this keeps the text visible. */
  .w__i { transform: none !important; }
  .cursor { display: none !important; }
  ::view-transition-old(root), ::view-transition-new(root) { animation: none !important; }

  /* Keep the focus effect — dimming is not motion — but drop the shifts. */
  .svc__item:hover .svc__name,
  .svc__item:hover .svc__desc { transform: none; }
  .svc__item::before { transition: none; }
  .proc__card:hover { transform: none; }
  .proc__card::before { transition: none; transform: scaleX(1); }

  /* Rules and bars land drawn rather than drawing themselves. Without this
     the review rule would sit at scaleX(0) — an invisible line, not a still
     one. */
  .stat::before { transform: scaleX(1); }
  [data-reveal].quote__rule { transform: scaleX(1); }
  .cat__fill { animation: none; transform: scaleX(1); }
  .quiz__card, .opt { animation: none; }
  .opt input:checked + .opt__box::before { transition: none; }
  .scrollcue__line::after { animation: none; }

  /* Freeze the orbit at its authored angles. The global animation-duration
     override alone is not enough: without fill-forwards the elements revert
     to no transform when the animation ends, collapsing every chip onto the
     twelve o'clock position. Setting the transforms explicitly holds the
     ring's shape while removing all movement. */
  .orbit__spoke { animation: none; transform: rotate(var(--a, 0deg)); }
  .orbit__chip,
  .orbit__badge { animation: none; transform: rotate(calc(-1 * var(--a, 0deg))); }

  /* Stop the scroll and drop the duplicate tracks — repeated copies only
     exist to hide the loop seam, and frozen in place they read as the same
     phrase stuttered four times. One static row is the honest fallback. */
  .marquee__track { animation: none; }
  .marquee__track:not(:first-child) { display: none; }
  .marquee { overflow-x: auto; }
  .marquee--echo { display: none; }
}
