/* tokens.css */
/* Amplify Reach — role-based brand color system.

   Colors are ROLES, not a palette of equal swatches. The page is an
   editorial warm-ivory foundation; purple and magenta own the branded
   fields; orange is reserved as the ignition accent; teal appears exactly
   once. Blush, peach and mint are quiet surface tints for cards and
   placeholders only — they never become sections, because a run of pastel
   sections is the rainbow problem this system replaces.

   There is deliberately NO light/dark toggle. The Global Brand Canvas
   already carries the page through five colour chapters as you scroll —
   ivory, lilac, sage, peach, plum — and it ends dark. Layering a theme
   inversion on top of that would only flip the ivory chapters (the brand
   colours are constants and do not invert), producing a half-inverted
   system and doubling the contrast surface for no real gain. The plum
   chapter IS the dark moment. */

:root {
  /* --- Core brand --- */
  --brand-purple: #9C2B88;
  --brand-magenta: #B52B84;
  --brand-accent: #D44C26; /* Ignition Orange — non-text use, see below */
  --brand-charcoal: #414042;
  --brand-black: #231F20;

  /* --- Foundation --- */
  --canvas-ivory: #F8F4EF;
  --canvas-white: #FFF9F4;
  --plum-deep: #35142F;
  --teal-muted: #2E625D;

  /* --- Quiet surface tints (cards and placeholders only) --- */
  --tint-blush: #F1D7E8;
  --tint-peach: #F6D7CB;
  --tint-mint: #C8DED7;

  /* Ignition Orange is an accent role, never normal-size text: it is
     3.95:1 against ivory, and near-black on it is only 3.77:1. Use it for
     rules, markers, arrows, icons, motion and focus rings. --accent-ink is
     the readable substitute for orange-flavoured TEXT — 6.45:1 on ivory
     and ≥5.2:1 on every card tint (blush, peach, mint). */
  --accent: var(--brand-accent);
  --accent-ink: #9E3517;

  /* --- Section roles --- */
  --hero-bg: var(--canvas-ivory);
  --hero-ink: var(--brand-black);
  --hero-glow: var(--brand-purple);

  --energy-bg: var(--brand-purple);   /* Services */
  --energy-ink: var(--canvas-white);

  --warmth-bg: var(--brand-magenta);  /* Featured work */
  --warmth-ink: var(--canvas-white);

  --optimism-bg: var(--canvas-ivory); /* Packages — back to ivory to read */
  --optimism-ink: var(--brand-black);

  --calm-bg: var(--teal-muted);       /* Team — the one complementary moment */
  --calm-ink: var(--canvas-white);

  --footer-bg: var(--plum-deep);
  --footer-ink: var(--canvas-white);

  /* Tier names. On the canvas system these are overridden per chapter by
     the text-safe accents in canvas.css; these values are the no-JS
     fallback, where the section is solid ivory. */
  --tier-purple: var(--brand-purple);
  --tier-magenta: var(--brand-magenta);

  --ink: var(--brand-black);
  --ink-dim: #5B5457;
  --hairline: rgba(35, 31, 32, 0.16);

  --font-display: "Big Shoulders Display", "Arial Narrow", sans-serif;
  --font-body: "Public Sans", -apple-system, "Segoe UI", sans-serif;

  --measure: 68ch;
  /* Content column: .wrap's max width and its side gutter. */
  --wrap-max: 1440px;
  --wrap-gutter: clamp(1.25rem, 4vw, 3rem);
  --radius-pill: 999px;
  --radius-card: 20px;

  color-scheme: light;
}

/* base.css */
/* Fonts are linked from base.njk's <head> (with a preconnect), not
   @imported here: an @import is only valid at the top of a stylesheet, so it
   would be dropped silently once this file is bundled into site.css. */

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

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

body {
  margin: 0;
  background: var(--hero-bg);
  color: var(--hero-ink);
  font-family: var(--font-body);
  font-size: 16px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
}

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

a {
  color: inherit;
}

/* Text links only. Buttons and statement rows are anchors too, and they
   own their own hover treatment — without :not(.btn) this recolored the
   hero's outline button label to magenta on ivory, at 4.0:1. */
a:not(.btn):not(.statement-row):hover {
  color: var(--brand-magenta);
}

.block-energy a:not(.btn):not(.statement-row):hover,
.block-warmth a:not(.btn):not(.statement-row):hover,
.block-calm a:not(.btn):not(.statement-row):hover,
.block-plum a:not(.btn):not(.statement-row):hover {
  color: var(--tint-peach);
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.skip-link {
  position: absolute;
  top: -100%;
  left: 1rem;
  background: var(--hero-ink);
  color: var(--hero-bg);
  padding: 0.75em 1.25em;
  border-radius: var(--radius-pill);
  z-index: 200;
  font-weight: 700;
  text-decoration: none;
}

.skip-link:focus {
  top: 1rem;
}

:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 3px;
}

/* Typography */

h1,
h2,
h3,
.display {
  font-family: var(--font-display);
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: -0.01em;
  line-height: 0.92;
  margin: 0;
}

h1,
.display-xl {
  font-size: clamp(2.8rem, 4.2vw + 1.6rem, 6.5rem);
}

h2,
.display-lg {
  font-size: clamp(2.1rem, 2.8vw + 1.2rem, 3.8rem);
}

h3,
.display-md {
  font-size: clamp(1.4rem, 1.2vw + 1rem, 2.1rem);
}

p {
  max-width: var(--measure);
  margin: 0 0 1em;
}

.lede {
  font-size: clamp(1.05rem, 0.4vw + 0.95rem, 1.3rem);
  font-weight: 400;
  max-width: 52ch;
}

/* Layout */

/* 1440px, up from 1240px (owner's call, 2026-10-09): content now fills
   most of a large screen. Paragraphs keep their own --measure cap, so line
   length does not grow with it. */
.wrap {
  max-width: var(--wrap-max);
  margin-inline: auto;
  padding-inline: var(--wrap-gutter);
}

section {
  position: relative;
}

.section-pad {
  padding-block: clamp(4rem, 9vw, 8rem);
}

hr.hairline {
  border: none;
  border-top: 1px solid var(--hairline);
  margin: 0;
}

/* Section fields. Each block is a flat brand field with a hard boundary,
   never a blended gradient. Roles, not a rainbow: ivory foundation,
   purple and magenta for the branded fields, teal exactly once. */

.block-hero {
  --block-bg: var(--hero-bg);
  --block-ink: var(--hero-ink);
  background: var(--block-bg);
  color: var(--block-ink);
}

.block-energy {
  --block-bg: var(--energy-bg);
  --block-ink: var(--energy-ink);
  background: var(--block-bg);
  color: var(--block-ink);
}

.block-warmth {
  --block-bg: var(--warmth-bg);
  --block-ink: var(--warmth-ink);
  background: var(--block-bg);
  color: var(--block-ink);
}

.block-optimism {
  --block-bg: var(--optimism-bg);
  --block-ink: var(--optimism-ink);
  background: var(--block-bg);
  color: var(--block-ink);
}

.block-calm {
  --block-bg: var(--calm-bg);
  --block-ink: var(--calm-ink);
  background: var(--block-bg);
  color: var(--block-ink);
}

.block-plum {
  --block-bg: var(--footer-bg);
  --block-ink: var(--footer-ink);
  background: var(--block-bg);
  color: var(--block-ink);
}

/* Ignition rule — the orange accent as a graphic marker, used to open a
   section head. Non-text, so exact #D44C26 is safe here. */
.ignite-rule {
  display: block;
  width: 3.25rem;
  height: 5px;
  background: var(--accent);
  margin-bottom: 1.25rem;
  border-radius: 999px;
}

/* Buttons — flat pills. Outline by default; the solid variant inverts
   to the section's background color so it always reads as "punching a
   hole through to the opposite tone," never a mismatched accent color. */

.btn {
  display: inline-flex;
  align-items: center;
  gap: 0.6em;
  font-family: var(--font-body);
  font-weight: 700;
  font-size: 0.95rem;
  letter-spacing: 0.01em;
  padding: 0.9em 1.6em;
  border-radius: var(--radius-pill);
  border: 1.5px solid currentColor;
  background: transparent;
  color: inherit;
  text-decoration: none;
  cursor: pointer;
  transition: transform 0.15s ease, background-color 0.15s ease, color 0.15s ease;
}

/* Order matters: this shares specificity with .btn--solid:hover below, so
   it must come FIRST or it wins on source order and replaces the solid
   button's hover fill with a translucent tint — which on the ivory hero
   left warm-white text sitting on near-ivory. :not(.btn--solid) makes that
   impossible to reintroduce by reordering. */
.btn:hover {
  transform: translateY(-1px);
}

.btn:not(.btn--solid):hover {
  background: color-mix(in oklab, currentColor 12%, transparent);
}

.btn--solid {
  background: var(--block-ink);
  color: var(--block-bg);
  border-color: transparent;
}

.btn--solid:hover {
  background: var(--brand-purple);
  color: var(--canvas-white);
}

/* On the purple field the solid button would hover INTO the field color
   and vanish, so it steps to magenta instead — the same progression the
   monogram makes. */
.block-energy .btn--solid:hover {
  background: var(--brand-magenta);
  color: var(--canvas-white);
}

.block-warmth .btn--solid:hover {
  background: var(--brand-purple);
}

/* The directional accent: a small orange arrow that steps forward on
   hover. Graphic, not text, so exact orange is safe on every field. */
.btn__arrow {
  width: 0.85em;
  height: 0.85em;
  color: var(--accent);
  transition: transform 0.15s cubic-bezier(0.16, 1, 0.3, 1);
}

.btn:hover .btn__arrow {
  transform: translateX(3px);
}

.btn--solid .btn__arrow {
  color: var(--accent);
}

.btn[disabled],
.btn[aria-disabled="true"] {
  opacity: 0.5;
  cursor: not-allowed;
  transform: none;
}

.btn-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.9rem;
  align-items: center;
}

/* nav.css */
.site-header {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 100;
  pointer-events: none;
}

.site-header__row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding-block: 1.1rem;
}

@media (max-width: 899px) {
  /* 76px header: 48px control row + 14px top and bottom. 20px side
     padding overrides the .wrap gutter so nothing touches the edge. */
  .site-header__row {
    padding-block: 14px;
    padding-inline: 20px;
    gap: 10px;
  }
}

/* Monogram-only home link — a compact container sized to the mark, not a
   wordmark-shaped capsule. The name lives in the loader, footer, title and
   the link's aria-label. */
.brand {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  text-decoration: none;
  color: var(--hero-ink);
  background: var(--hero-bg);
  border: 1.5px solid currentColor;
  padding: 0.5rem 0.7rem;
  border-radius: var(--radius-pill);
  pointer-events: auto;
  transition: background-color 0.2s ease, color 0.2s ease;
}

/* Hover is an accent ring plus motion on the real geometry, not a purple
   fill: the fill put the magenta AR body on a purple ground (muddy), and
   it never appeared at all once the canvas was running — see the
   specificity note at the end of this file. */
.brand:hover {
  border-color: var(--accent);
  color: var(--hero-ink);
}

/* The authentic AR monogram is a 1.736:1 landscape lockup, not a square
   glyph, and carries its own brand fills — no currentColor tint. */
.brand__mark {
  width: 46px;
  height: auto;
  flex: none;
}

@media (max-width: 899px) {
  /* 48x48 hit area, 38px visible mark. */
  .brand {
    min-width: 48px;
    height: 48px;
    padding: 0 5px;
  }

  .brand__mark {
    width: 38px;
  }
}

/* During the homepage opening the flying monogram stands in for the header
   mark; they cross-fade at identical position and scale so the handoff
   lands crisp and stable rather than settling into place. */
[data-intro="running"] .brand__mark {
  opacity: 0;
}

[data-intro="done"] .brand__mark {
  opacity: 1;
  transition: opacity 0.2s linear;
}

.pill-nav {
  display: none;
  align-items: center;
  gap: 0.5rem;
  pointer-events: auto;
}

.pill-nav a {
  display: inline-flex;
  align-items: center;
  position: relative;
  padding: 0.65em 1.1em;
  border-radius: var(--radius-pill);
  text-decoration: none;
  font-weight: 700;
  font-size: 0.82rem;
  letter-spacing: 0.02em;
  background: var(--pill-bg, var(--hero-bg));
  color: var(--pill-ink, var(--hero-ink));
  border: 1.5px solid transparent;
  transition: transform 0.4s cubic-bezier(0.22, 1, 0.36, 1),
    color 0.28s ease, box-shadow 0.2s ease;
  transform-origin: center;
  white-space: nowrap;
}

.pill-nav a[aria-current="page"] {
  border-color: currentColor;
}

/* 44px minimum target without changing the pill's appearance: a
   transparent overlay extends the hit region vertically past the visible
   box, so the drawn pill keeps its approved 34px height. */
.pill-nav a::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  height: 44px;
  transform: translateY(-50%);
}

/* Collapsed, the pills scale to 0.32 and the overlay would scale with
   them, leaving a ~14px target. Counter-scaling keeps the real target at
   44px while the dot stays a dot. */
.pill-nav.is-collapsed a::after {
  transform: translateY(-50%) scale(3.125);
}

/* Pill colors track the section roles they lead to: purple for services,
   magenta for work, ivory for packages, plum for the discovery call. */
.pill-nav a:nth-child(1) {
  --pill-bg: var(--brand-purple);
  --pill-ink: var(--canvas-white);
}

.pill-nav a:nth-child(2) {
  --pill-bg: var(--brand-magenta);
  --pill-ink: var(--canvas-white);
}

.pill-nav a:nth-child(3) {
  --pill-bg: var(--hero-bg);
  --pill-ink: var(--hero-ink);
  border-color: var(--hairline);
}

.pill-nav a:nth-child(4) {
  --pill-bg: var(--plum-deep);
  --pill-ink: var(--canvas-white);
}

/* These carry the same :not() chain as the generic link-hover rule in
   base.css purely to out-specify it. Without that, "Package Plans" turned
   brand magenta on its magenta pill and vanished; the pills keep their own
   ink and take the orange ring as the hover signal instead. */
.pill-nav a:not(.btn):not(.statement-row):hover {
  color: var(--pill-ink, var(--hero-ink));
  box-shadow: inset 0 0 0 2px var(--accent);
}

.mobile-nav a:not(.btn):not(.statement-row):hover {
  color: var(--hero-ink);
}

/* Same :not() chain as base.css's generic link hover, purely to
   out-specify it — otherwise the pill's currentColor border turned brand
   magenta on hover. */
.brand:not(.btn):not(.statement-row):hover {
  color: var(--hero-ink);
}

@media (min-width: 900px) {
  .pill-nav {
    display: flex;
  }
}

.header-actions {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  pointer-events: auto;
}


@media (max-width: 899px) {
  .header-actions {
    gap: 10px;
  }

  .nav-toggle {
    width: 44px;
    height: 44px;
    padding: 0;
    align-items: center;
    justify-content: center;
  }
}

.nav-toggle {
  display: inline-flex;
  border: 1.5px solid currentColor;
  background: var(--hero-bg);
  color: var(--hero-ink);
  border-radius: var(--radius-pill);
  padding: 0.55em 0.75em;
  cursor: pointer;
}

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

  /* .header-actions has no visible children above 899px (its only child,
     .nav-toggle, is hidden there), so it is taken out of the flow rather
     than left as a zero-width flex item. */
  .header-actions {
    display: none;
  }

  /* The nav sits centred in the space after the brand — the position it
     previously landed in by accident, when space-between split the slack
     around that zero-width item. Auto margins reproduce it exactly
     (l=331, r=664 at 924px) but deterministically, so it no longer
     depends on a phantom element being present to look right. */
  .pill-nav {
    margin-inline: auto;
  }
}

.mobile-nav {
  display: none;
  flex-direction: column;
  gap: 0.4rem;
  padding-block: 0.75rem 1.1rem;
  background: var(--hero-bg);
  color: var(--hero-ink);
  border-radius: var(--radius-card);
  margin-inline: clamp(1.25rem, 4vw, 3rem);
  padding-inline: 1.25rem;
  pointer-events: auto;
}

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

@media (min-width: 900px) {
  .mobile-nav {
    display: none !important;
  }
}

.mobile-nav a {
  padding: 0.7em 0.2em;
  text-decoration: none;
  color: inherit;
  font-weight: 700;
  border-bottom: 1px solid var(--hairline);
}

/* The pill nav itself collapses into a stack of small color dots once
   the visitor scrolls past the hero — same position, same links, just
   compacted — tracking which color-block section is currently in view. */

.pill-nav.is-collapsed a {
  transform: scale(0.32);
  color: transparent;
  border-color: transparent;
}

/* The active dot gets the orange ignition ring — the accent as a
   directional indicator, which is exactly its role. */
.pill-nav.is-collapsed a.is-active {
  box-shadow: 0 0 0 6px var(--accent);
}

/* Collapsed beats hover. The generic link-hover rule above carries three
   :not()s (0,4,1) and was out-ranking the plain collapsed rule (0,2,1) —
   so hovering a dot re-showed its label inside the shrunken pill and
   swapped the ring for the full-size inset one. These rules match that
   specificity and keep a dot a dot; the label lives in the cursor bubble
   instead. */
.pill-nav.is-collapsed a:not(.btn):not(.statement-row):hover,
.pill-nav.is-collapsed a:not(.btn):not(.statement-row):focus-visible {
  color: transparent;
  box-shadow: 0 0 0 3px var(--accent);
}

.pill-nav.is-collapsed a:not(.btn):not(.statement-row).is-active:hover {
  box-shadow: 0 0 0 6px var(--accent);
}


/* --- Brand monogram hover -------------------------------------------- */

/* The header logo had no hover response at all. The cause was
   specificity, not a missing rule: canvas.css sets
   `:root[data-canvas-on] .site-header .brand` (0,3,1) for background,
   colour and border-colour, which out-ranked the old `.brand:hover`
   (0,2,0) — so the moment theme-canvas.js confirmed it could run, every
   declaration in the hover rule lost. Each rule below therefore ships in
   two forms: the plain one for the no-JS/failed-script page, and a
   canvas-prefixed one that wins while the chapters are live.

   Motion obeys the Real-Geometry Rule — the three authentic paths are
   only scaled, lifted and refilled. Nothing is redrawn, stroked or split. */

.brand__marks {
  transition: transform 0.34s cubic-bezier(0.16, 1, 0.3, 1);
}

/* fill-box, or each path would scale about the shared 1000x576 viewBox
   origin and slide across the mark instead of popping in place. */
.brand__mark [data-mark-body],
.brand__mark [data-mark-purple],
.brand__mark [data-mark-accent] {
  transform-box: fill-box;
  transform-origin: center;
  transition: fill 0.28s ease;
}

/* One pixel, the same lift .btn and .statement-row use. */
.brand:hover .brand__marks,
.brand:focus-visible .brand__marks,
:root[data-canvas-on] .site-header .brand:hover .brand__marks {
  transform: translateY(-1px);
}

/* Purple steps to magenta — the monogram's own progression, and the hover
   language the service rows and pills already speak. */
.brand:hover [data-mark-purple],
:root[data-canvas-on] .site-header .brand:hover [data-mark-purple] {
  fill: var(--brand-magenta);
}

/* The orange wedge ignites: one 1.16x pop from its own centre, then
   settle — the loader's ignition beat, same factor, no elastic. */
.brand:hover [data-mark-accent],
:root[data-canvas-on] .site-header .brand:hover [data-mark-accent] {
  animation: brand-ignite 0.62s cubic-bezier(0.16, 1, 0.3, 1) 1;
}

@keyframes brand-ignite {
  0% {
    transform: scale(1);
  }
  38% {
    transform: scale(1.16);
  }
  100% {
    transform: scale(1);
  }
}

/* The ring is the one hover signal that reads on every chapter. */
:root[data-canvas-on] .site-header .brand:hover {
  border-color: var(--accent);
}

/* On plum the full-colour mark has faded out under the reversed
   one-colour copy, so a fill change there would be invisible. The white
   mark takes the ignition as a brief accent glow instead — the same
   treatment the footer monogram already uses for exactly this reason. */
:root[data-chapter="plum"][data-canvas-on] .brand:hover .brand__mark--reversed {
  animation: brand-glow 0.7s cubic-bezier(0.16, 1, 0.3, 1) 1;
}

@keyframes brand-glow {
  0%,
  100% {
    filter: none;
  }
  30% {
    filter: drop-shadow(0 0 10px rgba(212, 76, 38, 0.9));
  }
}

/* Reduced motion keeps the ring and the colour step — both are states,
   not animation — and drops the pop, the glow and the lift. */
@media (prefers-reduced-motion: reduce) {
  .brand:hover [data-mark-accent],
  :root[data-canvas-on] .site-header .brand:hover [data-mark-accent],
  :root[data-chapter="plum"][data-canvas-on] .brand:hover .brand__mark--reversed {
    animation: none;
  }

  .brand:hover .brand__marks,
  .brand:focus-visible .brand__marks,
  :root[data-canvas-on] .site-header .brand:hover .brand__marks {
    transform: none;
  }
}


/* --- "Home" label beside the monogram --------------------------------- */

/* On hover and keyboard focus a pill slides out beside the logo, so the
   logo says where it goes. It is absolutely positioned, so it never
   pushes the nav, and it moves with transform and opacity only. Inverted
   chapter colours keep it readable on every chapter, plum included. The
   homepage does not render it (see nav.njk). */
.brand {
  position: relative;
}

.brand__home {
  position: absolute;
  left: calc(100% + 0.5rem);
  top: 50%;
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
  padding: 0.45rem 0.8rem;
  border-radius: var(--radius-pill);
  background: var(--chapter-ink, var(--hero-ink));
  color: var(--canvas, var(--hero-bg));
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  white-space: nowrap;
  pointer-events: none;
  opacity: 0;
  transform: translate(-8px, -50%);
  transition: opacity 0.22s ease, transform 0.34s cubic-bezier(0.16, 1, 0.3, 1);
}

.brand__home svg {
  width: 1em;
  height: 1em;
  flex: none;
}

.brand:hover .brand__home,
.brand:focus-visible .brand__home {
  opacity: 1;
  transform: translate(0, -50%);
}

/* The arrow points the way once, just after the pill lands. */
.brand:hover .brand__home svg,
.brand:focus-visible .brand__home svg {
  animation: brand-home-nudge 0.6s cubic-bezier(0.16, 1, 0.3, 1) 0.12s 1;
}

@keyframes brand-home-nudge {
  0%,
  100% {
    transform: translateX(0);
  }
  45% {
    transform: translateX(-3px);
  }
}

/* Below 900px there is no hover, and the menu panel already lists Home. */
@media (max-width: 899px) {
  .brand__home {
    display: none;
  }
}

/* Reduced motion: the label still appears, as a plain fade. */
@media (prefers-reduced-motion: reduce) {
  .brand__home {
    transform: translate(0, -50%);
    transition: opacity 0.18s ease;
  }

  .brand:hover .brand__home svg,
  .brand:focus-visible .brand__home svg {
    animation: none;
  }
}

/* footer.css */
/* Signal Drop footer stage + site footer.

   The DEFAULT state here is the finished, usable footer: a plain wrapping
   row of chips. JS only takes over once it has confirmed it can run, by
   setting [data-physics="on"]. No JS, a blocked file, or reduced motion
   all leave the layout below intact. */

/* The footer paints its OWN plum ground. It scopes warm-white ink
   unconditionally, so it must never depend on the canvas having reached
   the plum chapter to be readable — a dropped frame, a JS error, or a
   future refactor would otherwise leave white text on a pale canvas.
   The canvas still blends behind it; this is the floor, not the effect. */
.site-footer {
  background: var(--footer-bg);
  color: var(--footer-ink);
  --ink: var(--footer-ink);
  --ink-dim: color-mix(in oklab, var(--footer-ink) 72%, transparent);
  --hero-ink: var(--footer-ink);
  --hero-bg: var(--footer-bg);
  --block-bg: var(--footer-bg);
  --block-ink: var(--footer-ink);
  --hairline: color-mix(in oklab, var(--footer-ink) 22%, transparent);
}

/* --- Closing statement --- */

/* The stage runs full-bleed so chips have the whole viewport to fall
   through; only the text above it keeps the page gutter. */
.signal-drop {
  padding-block: clamp(3.5rem, 8vw, 6rem) 0;
}

.signal-drop.wrap {
  max-width: none;
  padding-inline: 0;
}

.signal-drop__say {
  font-family: var(--font-display);
  font-weight: 800;
  text-transform: uppercase;
  line-height: 0.9;
  letter-spacing: -0.015em;
  font-size: clamp(2.5rem, 7vw + 1rem, 7rem);
  margin: 0;
  max-width: 16ch;
  text-wrap: balance;
  /* The stage is full-bleed; the text above it keeps the page gutter. */
  margin-inline: clamp(1.25rem, 4vw, 3rem);
}

.signal-drop__say em {
  font-style: normal;
  color: var(--accent);
}

.signal-drop__mark {
  width: clamp(56px, 7vw, 96px);
  height: auto;
  margin: 0 clamp(1.25rem, 4vw, 3rem) 1.5rem;
}

/* The arrow void inside the monogram lights on ignition. Because the
   arrow is negative space, the "lit" state is a brief accent glow behind
   the mark rather than a fill. */
.signal-drop__mark[data-lit] {
  animation: drop-ignite 1.1s cubic-bezier(0.16, 1, 0.3, 1) 1;
}

@keyframes drop-ignite {
  0% {
    filter: none;
  }
  22% {
    filter: drop-shadow(0 0 18px rgba(212, 76, 38, 0.95));
  }
  100% {
    filter: none;
  }
}

/* --- The stage --- */

.drop-stage {
  position: relative;
  margin-top: clamp(2rem, 5vw, 3rem);
  display: flex;
  flex-wrap: wrap;
  gap: 0.7rem;
  align-items: flex-end;
  min-height: 0;
}

/* Physics mode: the stage becomes a fixed-height box and the chips are
   absolutely positioned inside it. */
/* Pending: signal-drop.js is waiting on the fonts before it measures. The
   chips keep their flex layout (so their widths are readable) but are not
   shown, and the stage already takes its physics height, so nothing jumps
   when the chips are parked above it. The height rules are shared with
   "on" below, including the narrow-screen one at the end of this file. */
.drop-stage[data-physics="pending"] {
  height: clamp(230px, 34vw, 340px);
  overflow: hidden;
}

.drop-stage[data-physics="pending"] .drop-chip {
  visibility: hidden;
}

.drop-stage[data-physics="on"] {
  display: block;
  height: clamp(230px, 34vw, 340px);
  overflow: hidden;
  touch-action: pan-y;
}

.drop-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.55em;
  padding: 0.85em 1.3em;
  min-height: 48px;
  border-radius: var(--radius-pill);
  font-family: var(--font-body);
  font-weight: 700;
  font-size: 0.9rem;
  letter-spacing: 0.02em;
  text-decoration: none;
  white-space: nowrap;
  border: 1.5px solid transparent;
  will-change: auto;
}

.drop-stage[data-physics="on"] .drop-chip {
  will-change: transform;
  /* Matter writes a new transform every paint; a CSS transform transition
     would lag behind its rigid body and make a sleeping pile look floaty. */
  transition: box-shadow 0.18s ease;
}

.drop-chip[data-dragging] {
  cursor: grabbing;
}

/* Hover is the chip's OWN ring, never a cursor label.

   The chips used to carry data-cursor="CONNECT arrow", and the cursor
   bubble draws from the pointer position — i.e. inside the 48px-tall chip
   it was describing, covering the email address or social name you were
   reading. That label was pure duplication (the chip already says
   "info@...", and its href says the rest), so it is gone from footer.njk
   and the chip answers for itself. */
.drop-chip {
  transition: box-shadow 0.18s ease,
    transform 0.18s cubic-bezier(0.16, 1, 0.3, 1);
}

.drop-chip:hover,
.drop-chip:focus-visible {
  /* Outside the border, so it never crowds the label or shifts the text. */
  box-shadow: 0 0 0 2px var(--accent);
}

/* THE label must survive its own hover.

   base.css carries a generic `a:not(.btn):not(.statement-row):hover {
   color: var(--brand-magenta) }`, and the footer is .site-footer — not
   one of the .block-* sections that override it — so that rule reached
   these chips and repainted their text BRAND MAGENTA ON A BRAND MAGENTA
   CHIP. The email, Instagram and LinkedIn chips are all data-tone
   ="magenta", which is exactly why those three vanished on hover and the
   ivory and white ones did not: it looked like a chip-specific bug and
   was a sitewide link rule landing where it should not.

   Restating --chip-ink is therefore the fix, not the ring above. The
   doubled .drop-chip gives this (0,5,0) against base.css's (0,3,1) so it
   wins on specificity outright, rather than depending on footer.css
   continuing to load after base.css. */
.drop-chip.drop-chip:not(.btn):not(.statement-row):hover,
.drop-chip.drop-chip:not(.btn):not(.statement-row):focus-visible {
  color: var(--chip-ink, var(--canvas-white));
}

/* The lift only applies in the default flex row. In physics mode
   signal-drop.js writes an inline transform every frame, and an inline
   style always beats a stylesheet — a CSS transform here would either be
   ignored or fight the simulation. The ring alone carries hover there. */
.drop-stage:not([data-physics="on"]) .drop-chip:hover {
  transform: translateY(-1px);
}

/* Chip tones — drawn from the locked role system, no new colors. */
/* Each tone declares its ink ONCE, as --chip-ink, and both the rest and
   hover states read that variable — see the hover rule below for why
   restating it matters. */
.drop-chip {
  color: var(--chip-ink, var(--canvas-white));
}

.drop-chip[data-tone="purple"] {
  background: var(--brand-purple);
  --chip-ink: var(--canvas-white);
}

.drop-chip[data-tone="magenta"] {
  background: var(--brand-magenta);
  --chip-ink: var(--canvas-white);
}

.drop-chip[data-tone="ivory"] {
  background: var(--canvas-ivory);
  --chip-ink: var(--brand-black);
}

.drop-chip[data-tone="plum"] {
  background: var(--canvas-white);
  --chip-ink: var(--plum-deep);
}

.drop-chip[data-tone="teal"] {
  background: var(--teal-muted);
  --chip-ink: var(--canvas-white);
}

/* The ignition chip: orange is a graphic field here, and its label is
   near-black — but near-black on exact orange is 3.77:1, so the chip
   carries the accent as a ring and dot on an ivory ground instead. */
.drop-chip[data-tone="accent"] {
  background: var(--canvas-ivory);
  --chip-ink: var(--accent-ink);
  border-color: var(--accent);
}

.drop-chip[data-tone="accent"]::before {
  content: "";
  width: 0.55em;
  height: 0.55em;
  border-radius: 999px;
  background: var(--accent);
  flex: none;
}

.drop-chip[data-tone="accent"][data-lit] {
  animation: chip-ignite 0.9s cubic-bezier(0.16, 1, 0.3, 1) 1;
}

@keyframes chip-ignite {
  0%,
  100% {
    box-shadow: 0 0 0 0 rgba(212, 76, 38, 0);
  }
  30% {
    box-shadow: 0 0 0 7px rgba(212, 76, 38, 0.35);
  }
}

/* --- Manifest row --- */

.footer-manifest {
  border-top: 1px solid var(--hairline);
  margin-top: clamp(2rem, 5vw, 3rem);
  padding-block: 2.25rem;
  display: grid;
  gap: 1.5rem 2rem;
  font-size: 0.82rem;
  color: var(--ink-dim);
}

@media (min-width: 900px) {
  .footer-manifest {
    grid-template-columns: auto 1fr auto;
    align-items: center;
  }
}

.footer-manifest__links,
.footer-manifest__legal {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem 1.4rem;
  margin: 0;
  padding: 0;
}

.footer-manifest__links a,
.footer-manifest__legal a {
  text-decoration: none;
  color: var(--ink-dim);
  display: inline-flex;
  align-items: center;
  min-height: 44px;
}

.footer-manifest__links a {
  font-weight: 700;
  color: var(--footer-ink);
}

.footer-manifest__links a:hover,
.footer-manifest__legal a:hover {
  color: var(--tint-peach);
}

/* Office address and sales line. <address> is italic by default; the
   footer's type is not. */
.footer-contact {
  display: grid;
  justify-items: start;
  font-style: normal;
  line-height: 1.5;
}

.footer-contact a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  color: var(--footer-ink);
  font-weight: 700;
  text-decoration: none;
}

/* The number is what a visitor is reaching for, so it reads a size up. */
.footer-contact__phone {
  font-size: 1.05rem;
}

/* Doubled class for the same reason as the chips above: base.css's
   `a:not(.btn):not(.statement-row):hover` is (0,3,1) and would otherwise
   paint these magenta on the plum ground. */
.footer-contact.footer-contact a:hover,
.footer-contact.footer-contact a:focus-visible {
  color: var(--tint-peach);
}

.footer-manifest__brand {
  display: flex;
  align-items: center;
  gap: 0.6em;
  font-family: var(--font-display);
  text-transform: uppercase;
  color: var(--footer-ink);
  font-weight: 800;
  font-size: 1.05rem;
}

/* Single-color mark on the plum field — the full-color monogram's magenta
   and purple sit too close to the plum to read. */
.footer-manifest__mark {
  width: 2.7em;
  height: auto;
  flex: none;
}

.footer-legal-row {
  border-top: 1px solid var(--hairline);
  padding-block: 1.4rem 2.25rem;
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem 2rem;
  justify-content: space-between;
  align-items: center;
  font-size: 0.76rem;
  color: var(--ink-dim);
}

@media (max-width: 899px) {
  .drop-stage[data-physics="pending"],
  .drop-stage[data-physics="on"] {
    height: 280px;
  }
}

/* pages.css */
.page-hero {
  padding-block: clamp(7rem, 13vw, 10rem) clamp(2.5rem, 6vw, 4rem);
}

.page-hero .lede {
  margin-top: 1rem;
}

.content-section {
  padding-block: clamp(3rem, 7vw, 5rem);
}

.content-section h2 {
  margin-bottom: 1.25rem;
}

.two-col {
  display: grid;
  gap: 2.5rem;
}

@media (min-width: 860px) {
  .two-col {
    grid-template-columns: 1fr 1fr;
  }
}

/* Section heading block — shared across the homepage and inner pages */

.block-section__head {
  margin-bottom: clamp(2rem, 5vw, 3.25rem);
}

.block-section__head .lede {
  margin-top: 0.7rem;
}

/* Statement rows — large, typographic, tappable full-width rows for
   core services / add-ons. Not cards. Shared across pages. */

.statement-list {
  border-top: 1px solid var(--rule, color-mix(in oklab, currentColor 38%, transparent));
}

.statement-row {
  display: block;
  position: relative;
  padding-block: clamp(1.5rem, 4vw, 2.5rem);
  border-bottom: 1px solid var(--rule, color-mix(in oklab, currentColor 38%, transparent));
  text-decoration: none;
  color: inherit;
  transition: transform 0.28s cubic-bezier(0.16, 1, 0.3, 1),
    background-color 0.28s ease;
}

/* Services sit on purple and transition toward magenta on hover — the
   monogram's own progression, used as the interaction language. */
/* z-index 0 + relative children, NOT z-index -1: a negative index would
   drop the fill behind the section's own background and never show. */
.statement-row::before {
  content: "";
  position: absolute;
  inset: 0 -1.25rem;
  background: var(--brand-magenta);
  opacity: 0;
  transition: opacity 0.28s ease;
  z-index: 0;
}

.statement-row__top,
.statement-row__desc {
  position: relative;
  z-index: 1;
}

.statement-row:hover {
  transform: translateX(0.75rem);
}

.statement-row:hover::before {
  opacity: 1;
}

.statement-row:hover .statement-row__title {
  color: inherit;
}

/* The orange ignition marker rides in from the left edge on hover. */
.statement-row__title::before {
  content: "";
  display: inline-block;
  vertical-align: middle;
  width: 0;
  height: 0.14em;
  margin-right: 0;
  background: var(--accent);
  transition: width 0.28s cubic-bezier(0.16, 1, 0.3, 1),
    margin-right 0.28s cubic-bezier(0.16, 1, 0.3, 1);
}

.statement-row:hover .statement-row__title::before {
  width: 0.7em;
  margin-right: 0.28em;
}

.statement-row__top {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 1rem;
  flex-wrap: wrap;
}

.statement-row__title {
  font-family: var(--font-display);
  font-weight: 800;
  text-transform: uppercase;
  font-size: clamp(1.6rem, 2.4vw + 1rem, 3rem);
  letter-spacing: -0.01em;
}

.statement-row__price {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: clamp(1rem, 1vw + 0.7rem, 1.4rem);
  white-space: nowrap;
}

.statement-row__desc {
  margin-top: 0.5rem;
  max-width: 62ch;
  color: color-mix(in oklab, currentColor 82%, transparent);
}

/* Client-work carousel — shared across pages */

.work-carousel {
  display: flex;
  gap: 1.1rem;
  overflow-x: auto;
  scroll-snap-type: x proximity;
  padding-bottom: 1rem;
  margin: 0 calc(-1 * clamp(1.25rem, 4vw, 3rem));
  padding-inline: clamp(1.25rem, 4vw, 3rem);
  cursor: grab;
}

.work-carousel:active {
  cursor: grabbing;
}

/* Editorial placeholders: a quiet blush surface waiting for real imagery,
   not a striped swatch. Nothing here invents a client, a number, or a
   result — the card states plainly what it is holding space for. */
.work-card {
  scroll-snap-align: start;
  flex: 0 0 clamp(240px, 33vw, 320px);
  aspect-ratio: var(--slot-ratio, 9 / 16);
  border-radius: 4px;
  overflow: hidden;
  position: relative;
  background: var(--tint-blush);
  color: var(--brand-black);
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  padding: 1.35rem;
  transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}

.work-card:nth-child(3n + 2) {
  background: var(--canvas-ivory);
}

.work-card:nth-child(3n) {
  background: var(--tint-peach);
}

/* A hairline frame inset from the edge — an image mat, so the card reads
   as prepared space rather than an empty box. */
.work-card::after {
  content: "";
  position: absolute;
  inset: 0.8rem;
  border: 1px solid color-mix(in oklab, var(--brand-black) 22%, transparent);
  pointer-events: none;
}

/* 1.2rem/800 clears the large-text threshold and reads better as an
   editorial folio number; --accent-ink carries it on every card tint. */
.work-card__index {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 1.2rem;
  letter-spacing: 0.14em;
  color: var(--accent-ink);
  position: relative;
}

.work-card__label {
  font-family: var(--font-display);
  font-weight: 800;
  text-transform: uppercase;
  font-size: clamp(1.5rem, 2vw + 0.9rem, 2.1rem);
  line-height: 0.94;
  position: relative;
}

.work-card__meta {
  display: block;
  margin-top: 0.55rem;
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  /* A literal value, not a transparency mix: at 62% this fell to 4.1:1 on
     blush and peach. Cards are always a light tint, so it does not need to
     track the theme. */
  color: #524B4E;
  position: relative;
}

.work-card__foot {
  position: relative;
}

/* Homepage highlights with the real work in them. The media fills the
   card, a scrim keeps the caption readable over any frame, and the text
   ignores the pointer so hovering anywhere on the card reaches the media
   slot and starts its clip. Layering: media 0, scrim 1, text 2. */
/* Compound selectors: a later `.work-card { display: block }` block for
   the grid tiles would otherwise win on source order and stack the
   caption at the top, and its 4px hover lift would open a gap under the
   full-bleed media. */
.work-card.work-card--media {
  display: flex;
  text-decoration: none;
  -webkit-user-drag: none;
}

.work-card.work-card--media:hover .media-slot {
  transform: none;
}

.work-card--media .media-slot {
  position: absolute;
  inset: 0;
  z-index: 0;
  aspect-ratio: auto;
  border-radius: inherit;
}

.work-card--media::after {
  inset: 0;
  z-index: 1;
  border: 0;
  background:
    linear-gradient(to top, rgba(26, 12, 22, 0.82) 0%, rgba(26, 12, 22, 0) 48%),
    linear-gradient(to bottom, rgba(26, 12, 22, 0.5) 0%, rgba(26, 12, 22, 0) 22%);
}

.work-card--media .work-card__index,
.work-card--media .work-card__foot {
  z-index: 2;
  pointer-events: none;
}

.work-card--media .work-card__index,
.work-card--media .work-card__label,
.work-card--media .work-card__meta,
:root[data-canvas-on] .work-card--media .work-card__index {
  color: var(--canvas-white);
}

/* Packages teaser — big pull-quote style rows, not a table. Shared
   across the homepage and the Package Plans page. */

.quote-list {
  display: flex;
  flex-direction: column;
  gap: clamp(1.5rem, 4vw, 2.5rem);
}

.quote-row {
  border-bottom: 1px solid var(--hairline);
  padding-bottom: clamp(1.5rem, 4vw, 2.5rem);
}

.quote-row:last-child {
  border-bottom: none;
  padding-bottom: 0;
}

.quote-row__tier {
  font-family: var(--font-display);
  font-weight: 800;
  text-transform: uppercase;
  font-size: clamp(1.8rem, 3vw + 1rem, 3.2rem);
  display: block;
  color: var(--tier-purple);
}

.quote-row:nth-child(2) .quote-row__tier {
  color: var(--tier-magenta);
}

/* Active-plan marker. The accent stays visible as the ring and the dot —
   its actual role — while the label sits in --accent-ink on the section
   field. Near-black on exact orange is 3.77:1 and fails AA, so the badge
   is never a filled orange chip with text on it. */
.quote-row__badge {
  display: inline-flex;
  align-items: center;
  gap: 0.5em;
  margin-left: 0.6rem;
  vertical-align: 0.35em;
  background: transparent;
  color: var(--accent-ink);
  border: 1.5px solid var(--accent);
  font-family: var(--font-body);
  font-size: 0.62rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  padding: 0.4em 0.8em;
  border-radius: 999px;
}

.quote-row__badge::before {
  content: "";
  width: 0.5em;
  height: 0.5em;
  border-radius: 999px;
  background: var(--accent);
}

.quote-row__price {
  font-weight: 700;
  font-size: 1.1rem;
  color: var(--ink-dim);
}

.quote-row__desc {
  margin-top: 0.4rem;
  max-width: 60ch;
  opacity: 0.85;
}

/* Culture / about — shared, used on the homepage today */

.directory {
  display: grid;
  gap: 2rem;
  align-items: start;
}

.directory__badge {
  aspect-ratio: 3 / 4;
  max-width: 260px;
  border-radius: 4px;
  background: var(--tint-mint);
  color: var(--brand-black);
  display: flex;
  align-items: flex-end;
  padding: 1.1rem;
  position: relative;
}

/* The purple detail that ties the one teal section back to the system. */
.directory__badge::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 6px;
  background: var(--brand-purple);
}

.directory__badge::after {
  content: "";
  position: absolute;
  inset: 0.75rem;
  border: 1px solid color-mix(in oklab, var(--brand-black) 20%, transparent);
  pointer-events: none;
}

.directory__badge span {
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: color-mix(in oklab, var(--brand-black) 68%, transparent);
  position: relative;
}

@media (min-width: 840px) {
  .directory {
    grid-template-columns: 260px 1fr;
  }
}

/* FAQ accordion */

.faq-list {
  border-top: 1.5px solid currentColor;
}

.faq-item {
  border-bottom: 1.5px solid currentColor;
}

.faq-item__q {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding-block: 1.35rem;
  background: none;
  border: none;
  color: inherit;
  font-family: var(--font-body);
  font-weight: 700;
  font-size: 1.05rem;
  text-align: left;
  cursor: pointer;
}

.faq-item__icon {
  flex: none;
  width: 1.1em;
  height: 1.1em;
  transition: transform 0.25s ease;
}

.faq-item[open] .faq-item__icon {
  transform: rotate(45deg);
}

.faq-item__a {
  padding-bottom: 1.4rem;
  opacity: 0.85;
  max-width: var(--measure);
}

/* Discovery call */

.slot-card {
  border: 1.5px solid currentColor;
  border-radius: var(--radius-card);
  padding: clamp(1.5rem, 4vw, 2.5rem);
}

.step-list {
  list-style: none;
  margin: 1.25rem 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.step-list li {
  padding-bottom: 1rem;
  border-bottom: 1px solid var(--hairline);
  font-size: 1.05rem;
}

.step-list li:last-child {
  border-bottom: none;
  padding-bottom: 0;
}

/* Legal / long-form text pages */

.legal-page .content-section {
  padding-block: clamp(1.75rem, 4vw, 2.75rem);
}

.legal-page h2 {
  font-size: clamp(1.2rem, 1vw + 1rem, 1.6rem);
  text-transform: none;
  letter-spacing: 0;
  font-family: var(--font-body);
  font-weight: 700;
  margin-bottom: 0.75rem;
}

.legal-page p,
.legal-page li {
  opacity: 0.85;
}

.legal-page ul {
  padding-left: 1.2em;
}

/* Policy metadata and open questions. `legal-todo` is deliberately loud:
   it marks text the business still has to confirm, and it should be
   impossible to miss on the page if one is ever left in by accident. */
/* The 404 must be tall enough to own the viewport. theme-canvas.js gives
   the chapter to the last section whose top has crossed 42% of the
   viewport height; on a page this short the footer's plum section cleared
   that line at scrollTop 0 and the page loaded as a dark field with
   near-invisible outline buttons. Filling the viewport keeps the ivory
   hero the active chapter and makes the page read as designed rather
   than as a stub floating above the footer. */
.notfound .page-hero {
  min-height: 72vh;
  display: flex;
  align-items: center;
}

.legal-updated {
  font-size: 0.85rem;
  color: var(--ink-dim);
  margin-top: 0.75rem;
}

.legal-page .legal-todo {
  border-left: 3px solid var(--accent);
  background: var(--tint-peach);
  padding: 0.7rem 0.9rem;
  font-size: 0.9rem;
  opacity: 1;
  /* --brand-black, NOT --ink. canvas.css redefines --ink to the active
     chapter's ink on every .block-* section, so var(--ink) here resolved
     to ivory and put 1.05:1 text on this fixed peach panel. The panel's
     background does not follow the chapter, so its text must not either.
     #231F20 on #F6D7CB is 12:1. */
  color: var(--brand-black);
}


/* --- Media placeholder slots --- */
.media-slot {
  margin: 0;
  border-radius: 4px;
  background: var(--tint-blush);
  color: var(--brand-black);
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 1.5rem;
  position: relative;
  aspect-ratio: var(--slot-ratio, 9 / 16);
  overflow: hidden;
}

.media-slot::after {
  content: "";
  position: absolute;
  inset: 0.7rem;
  border: 1px solid color-mix(in oklab, var(--brand-black) 20%, transparent);
  pointer-events: none;
}

.media-slot__note {
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  color: color-mix(in oklab, var(--brand-black) 62%, transparent);
  max-width: 26ch;
  position: relative;
}

/* Every slot is 4:5. The assets are made for feeds, and cropping them to
   a widescreen box discarded 65-80% of each image. */
/* Compound with .media-slot for the same reason as the ::after rule
   below: the duplicate `.media-slot { padding: 1.25rem }` block further
   down the file is equal specificity and later in source, so an
   uncompounded selector here loses the cascade. */
.media-slot.media-slot--filled {
  padding: 0;
}

/* Compound with .media-slot so this beats the other .media-slot::after
   rules on specificity (0,2,1 vs 0,1,1) rather than on source order —
   the same duplicate-block cascade as above would otherwise repaint the
   inset border over real imagery the moment Task 8 wires a call site. */
.media-slot.media-slot--filled::after {
  content: none;
}

.media-slot__poster,
.media-slot__video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.media-slot__video {
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.4s cubic-bezier(0.16, 1, 0.3, 1);
}

.media-slot.is-playing .media-slot__video {
  opacity: 1;
}

/* --- Case-study template --- */
.cs-hero__meta {
  display: flex;
  gap: 0.6em;
  align-items: center;
  font-size: 0.8rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-dim);
  margin: 0 0 1rem;
}

.cs-hero__client {
  color: var(--brand-purple);
}

.cs-tags {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin: 1.75rem 0 0;
  padding: 0;
}

.cs-tags li {
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  padding: 0.4em 0.85em;
  border: 1px solid var(--hairline);
  border-radius: 999px;
}

.cs-media {
  margin-top: -2.5rem;
  position: relative;
  z-index: 1;
}

.cs-grid {
  display: grid;
  gap: 2.5rem;
}

@media (min-width: 800px) {
  .cs-grid {
    grid-template-columns: 1fr 1fr;
    gap: 4rem;
  }
}

.cs-gallery {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1rem;
  margin-top: 2.5rem;
}

.cs-pending {
  color: var(--ink-dim);
  font-size: 0.92rem;
}

.cs-pager__row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.5rem;
  flex-wrap: wrap;
}

.cs-pager__link {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  text-decoration: none;
  color: inherit;
  max-width: 24ch;
}

.cs-pager__link--next {
  text-align: right;
  margin-left: auto;
}

.cs-pager__link span {
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-dim);
}

.cs-pager__link strong {
  font-family: var(--font-display);
  font-weight: 800;
  text-transform: uppercase;
  font-size: 1.3rem;
  line-height: 1.05;
}

.cs-pager__all {
  font-size: 0.8rem;
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--ink-dim);
  border: 1px solid var(--hairline);
  border-radius: 999px;
  padding: 0.6em 1.2em;
  min-height: 44px;
  display: inline-flex;
  align-items: center;
}

/* --- Work index --- */
.work-intro {
  max-width: 58ch;
}

.work-filters {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
  margin: 2.5rem 0 0;
  list-style: none;
  padding: 0;
}

.work-filters button {
  font-family: var(--font-body);
  font-size: 0.8rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  padding: 0.6em 1.1em;
  min-height: 44px;
  border-radius: 999px;
  border: 1.5px solid var(--hairline);
  background: transparent;
  color: inherit;
  cursor: pointer;
}

.work-filters button[aria-pressed="true"] {
  background: var(--brand-black);
  color: var(--canvas-ivory);
  border-color: var(--brand-black);
}

/* On the plum chapter the filled pressed state would be near-black on a
   dark ground; it flips to warm white there instead. */
:root[data-chapter="plum"] .work-filters button[aria-pressed="true"] {
  background: var(--canvas-white);
  color: var(--brand-black);
  border-color: var(--canvas-white);
}

.work-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 1.25rem;
  margin-top: 2.5rem;
}

@media (min-width: 700px) {
  .work-grid {
    grid-template-columns: repeat(3, 1fr);
  }
}

.work-grid__item {
  grid-row: span 1;
}

@media (max-width: 699px) {
  .work-grid {
    grid-template-columns: repeat(2, 1fr);
  }
}

.work-card {
  display: block;
  text-decoration: none;
  color: inherit;
  height: 100%;
}

.work-card .media-slot {
  height: 100%;
  transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}

.work-card:hover .media-slot {
  transform: translateY(-4px);
}

.work-card__meta {
  margin-top: 0.75rem;
}

.work-card__title {
  font-family: var(--font-display);
  font-weight: 800;
  text-transform: uppercase;
  font-size: 1.05rem;
  line-height: 1.1;
  display: block;
}

.work-card__sub {
  font-size: 0.76rem;
  color: var(--ink-dim);
  display: block;
  margin-top: 0.2rem;
}

/* --- Featured work drag row (Work index + Home) --- */
.featured-row {
  display: flex;
  gap: 1.25rem;
  overflow-x: auto;
  padding-bottom: 0.5rem;
  margin-top: 2.5rem;
  scroll-snap-type: x proximity;
  cursor: grab;
  scrollbar-width: thin;
}

.featured-row:active {
  cursor: grabbing;
}

.featured-row__item {
  flex: 0 0 clamp(240px, 30vw, 320px);
  scroll-snap-align: start;
  text-decoration: none;
  color: inherit;
}

.featured-row__item .media-slot {
  aspect-ratio: var(--slot-ratio, 9 / 16);
}


/* --- Media placeholder slots (case studies, work index) --- */

.media-slot {
  margin: 0;
  border-radius: 4px;
  background: var(--tint-blush);
  color: var(--brand-black);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1.25rem;
  text-align: center;
  position: relative;
  /* Declared explicitly rather than relying on the first .media-slot
     block 340 lines up: correctness shouldn't depend on that block
     surviving untouched. */
  aspect-ratio: var(--slot-ratio, 9 / 16);
  overflow: hidden;
}

.media-slot::after {
  content: "";
  position: absolute;
  inset: 0.7rem;
  border: 1px solid color-mix(in oklab, var(--brand-black) 20%, transparent);
  pointer-events: none;
}

.media-slot__note {
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  max-width: 26ch;
  color: color-mix(in oklab, var(--brand-black) 62%, transparent);
  position: relative;
}

/* --- Case study --- */

.cs-hero__meta {
  display: flex;
  gap: 0.6em;
  font-size: 0.8rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-dim);
  margin: 0 0 0.9rem;
}

.cs-hero__client {
  color: var(--brand-magenta);
}

.cs-tags {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
  margin: 1.75rem 0 0;
  padding: 0;
}

.cs-tags li {
  font-size: 0.74rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  padding: 0.45em 0.9em;
  border: 1px solid var(--hairline);
  border-radius: 999px;
}

.cs-media {
  margin-top: -2.5rem;
  position: relative;
  z-index: 1;
}

/* Cap the hero so it reads as one composed image instead of a wall the
   visitor has to scroll through — at 1280px the slot otherwise renders
   1144x1430 (nearly two viewport heights) because it simply fills
   .wrap. Scoped to .cs-media so the work-grid/featured tiles, which
   still need to fill their own tracks, are untouched. Two classes give
   this (0,2,0) specificity, which beats every plain `.media-slot` and
   `.media-slot.media-slot--filled` rule above/below regardless of which
   duplicate block ends up last in source — verified in devtools with
   both `.media-slot` blocks present. Ratio stays 4:5: with width auto,
   the browser derives width from the height once max-height clamps it. */
.cs-media .media-slot {
  max-height: 80vh;
  width: auto;
  margin-inline: auto;
}

.cs-grid {
  display: grid;
  gap: 2.5rem;
}

@media (min-width: 800px) {
  .cs-grid {
    grid-template-columns: 1fr 1fr;
    gap: 4rem;
  }
}

.cs-gallery {
  display: grid;
  gap: 1rem;
  margin-top: 2.5rem;
  grid-template-columns: repeat(2, 1fr);
}

/* Below ~700px the 2-up gallery renders ~159px-wide images — too small
   to read the creative copy baked into the artwork. Full-width single
   column until there's room for the grid again. `.cs-gallery` is
   declared twice in this file (identical, unconditional 2-col, no
   media query) so scope to the `.cs` article wrapper for (0,2,0)
   specificity: that wins over either duplicate regardless of source
   order. Desktop (>=700px) is untouched. */
@media (max-width: 699px) {
  .cs .cs-gallery {
    grid-template-columns: 1fr;
  }
}

.cs-pending {
  color: var(--ink-dim);
  font-style: italic;
}

/* Three across once there's room, so a 9:16 reel sits at a readable
   size instead of filling half the page. */
@media (min-width: 900px) {
  .cs .cs-gallery {
    grid-template-columns: repeat(3, 1fr);
  }
}

/* A four-piece gallery in a three-column grid leaves one card stranded
   beside two empty cells. Four across keeps every row full and still
   gives a 9:16 reel ~315px inside the 1344px wrap — taller than the
   three-up card, not larger than it. Below 1100px the 2-column rule
   above already pairs them 2x2, which is also complete. */
@media (min-width: 1100px) {
  .cs .cs-gallery[data-count="4"] {
    grid-template-columns: repeat(4, 1fr);
  }
}

@media (min-width: 900px) and (max-width: 1099px) {
  .cs .cs-gallery[data-count="4"] {
    grid-template-columns: repeat(2, 1fr);
  }
}

.cs-process {
  list-style: none;
  counter-reset: step;
  display: grid;
  gap: 1.5rem;
  margin: 2.5rem 0 0;
  padding: 0;
}

@media (min-width: 800px) {
  .cs-process {
    grid-template-columns: repeat(4, 1fr);
  }
}

.cs-process li {
  counter-increment: step;
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  padding-top: 0.9rem;
  border-top: 1px solid var(--hairline);
}

.cs-process li::before {
  content: "0" counter(step);
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  color: var(--ink-dim);
}

.cs-process strong {
  font-family: var(--font-display);
  font-weight: 800;
  text-transform: uppercase;
  font-size: 1.3rem;
  line-height: 1;
}

.cs-process span {
  max-width: 30ch;
}

.cs-facts {
  display: flex;
  flex-wrap: wrap;
  gap: 1.5rem 3rem;
  margin: 3rem 0 0;
}

.cs-facts dt {
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-dim);
}

.cs-facts dd {
  margin: 0;
}

.cs-facts .cs-tags {
  margin-top: 0.6rem;
}

.cs-pager__row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.5rem;
  flex-wrap: wrap;
}

.cs-pager__link {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  text-decoration: none;
  color: inherit;
  max-width: 30ch;
}

.cs-pager__link--next {
  text-align: right;
  margin-left: auto;
}

.cs-pager__link span {
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-dim);
}

.cs-pager__link strong {
  font-family: var(--font-display);
  font-weight: 800;
  text-transform: uppercase;
  font-size: 1.3rem;
  line-height: 1;
}

.cs-pager__all {
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--ink-dim);
  border: 1px solid var(--hairline);
  border-radius: 999px;
  padding: 0.6em 1.3em;
  white-space: nowrap;
}

/* --- Work index --- */

.work-intro {
  max-width: 62ch;
}

.work-filters {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
  margin-top: 2rem;
  list-style: none;
  padding: 0;
}

.work-filters button {
  font-family: var(--font-body);
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  padding: 0.6em 1.2em;
  min-height: 44px;
  border-radius: 999px;
  border: 1.5px solid currentColor;
  background: transparent;
  color: inherit;
  cursor: pointer;
}

.work-filters button[aria-pressed="true"] {
  background: var(--brand-black);
  color: var(--canvas-ivory);
  border-color: var(--brand-black);
}

.work-grid {
  margin-top: 2.5rem;
  display: grid;
  gap: 1.5rem;
  grid-template-columns: repeat(2, 1fr);
}

@media (min-width: 900px) {
  .work-grid {
    grid-template-columns: repeat(3, 1fr);
  }
}

.work-tile {
  text-decoration: none;
  color: inherit;
  display: block;
}

.work-tile[hidden] {
  display: none;
}

.work-tile .media-slot {
  transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}

.work-tile:hover .media-slot {
  transform: translateY(-4px);
}

@media (min-width: 900px) {
  /* A gentle stagger, not chaos: every other tile sits a half-row lower.
     Scoped to .work-grid on purpose — the featured carousel (.work-featured)
     is a horizontal flex row whose only element children are .work-tile, so
     an unscoped rule dropped every other tile 2rem out of line. */
  .work-grid .work-tile:nth-child(even) {
    margin-top: 2rem;
  }
}

.work-tile__meta {
  display: flex;
  justify-content: space-between;
  gap: 0.75rem;
  margin-top: 0.85rem;
}

.work-tile__title {
  font-family: var(--font-display);
  font-weight: 800;
  text-transform: uppercase;
  font-size: 1.05rem;
  line-height: 1.05;
}

.work-tile__cat {
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-dim);
  white-space: nowrap;
}

/* Desktop featured row: clickable prev/next, no drag/swipe, no visible
   scrollbar — the row still scrolls under the hood so smooth-scrolling
   between tiles just works, but the interaction is buttons only. */
.work-featured {
  display: flex;
  gap: 1.25rem;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
}

.work-featured::-webkit-scrollbar {
  display: none;
}

.work-featured-row {
  margin-top: 2.5rem;
}

.work-featured .work-tile {
  flex: 0 0 clamp(240px, 28vw, 340px);
  scroll-snap-align: start;
}

/* Every featured tile is the same box regardless of the project's real
   orientation — a row that mixes portrait and landscape cards mid-scroll
   reads as broken, not intentional variety. */
.work-featured .media-slot {
  aspect-ratio: var(--slot-ratio, 9 / 16);
}

.work-featured__controls {
  display: flex;
  gap: 0.75rem;
  margin-top: 1.25rem;
}

.work-featured__btn {
  width: 48px;
  height: 48px;
  border-radius: 999px;
  border: 1.5px solid currentColor;
  background: transparent;
  color: inherit;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}

.work-featured__btn svg {
  width: 20px;
  height: 20px;
}

.work-featured__btn:hover {
  background: color-mix(in oklab, currentColor 12%, transparent);
}

.work-featured__btn:disabled {
  opacity: 0.35;
  cursor: default;
}

@media (max-width: 899px) {
  /* Mobile keeps a plain swipeable row — the buttons are a desktop
     convenience, not a mobile requirement. */
  .work-featured__controls {
    display: none;
  }
}

@media (max-width: 899px) {
  .work-grid {
    grid-template-columns: repeat(2, 1fr);
    gap: 1rem;
  }
}

/* At 390px the 2-up grid renders ~167px-wide tiles — the same
   too-small-to-read problem as the case-study gallery. Drop to a
   single full-width column below ~700px; 700-899px keeps the 2-up
   rule above, and the min-width:900px 3-up + nth-child(even) stagger
   further up is untouched. `.work-grid` is declared in two blocks in
   this file with equal specificity, so scope to the live
   `[data-work-grid]` container (only the "all projects" grids carry
   that attribute, not `.work-featured`) for (0,2,0) specificity — that
   wins over either duplicate regardless of source order. */
@media (max-width: 699px) {
  .work-grid[data-work-grid] {
    grid-template-columns: 1fr;
  }
}

/* --- Services --- */

.service-block {
  padding-block: clamp(3rem, 7vw, 5rem);
}

.service-block__head {
  display: grid;
  gap: 1.5rem;
  margin-bottom: 2.5rem;
}

@media (min-width: 900px) {
  .service-block__head {
    grid-template-columns: 2fr 1fr;
    align-items: end;
  }
}

.service-list {
  list-style: none;
  margin: 0 0 2rem;
  padding: 0;
  display: grid;
  gap: 0.6rem;
}

.service-list li {
  padding-left: 1.4em;
  position: relative;
}

.service-list li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.6em;
  width: 0.6em;
  height: 2px;
  background: var(--accent);
}

.service-who {
  font-size: 0.82rem;
  letter-spacing: 0.02em;
  color: var(--ink-dim);
  margin-top: 1.5rem;
}

.service-process {
  display: grid;
  gap: 1.25rem;
  margin-top: 2.5rem;
}

/* Four steps: two by two on tablets, one row on desktop. Three columns
   left "04 — Optimize" alone on a second row. */
@media (min-width: 700px) {
  .service-process {
    grid-template-columns: repeat(2, 1fr);
  }
}

@media (min-width: 1100px) {
  .service-process {
    grid-template-columns: repeat(4, 1fr);
  }
}

.service-process__step {
  padding: 1.4rem;
  border: 1px solid var(--hairline);
  border-radius: 4px;
}

.service-process__step b {
  display: block;
  font-family: var(--font-display);
  font-weight: 800;
  text-transform: uppercase;
  font-size: 1.4rem;
  margin-bottom: 0.5rem;
  color: var(--accent-ink);
}

.pkg-nudge {
  margin-top: 2.5rem;
  padding: 1.5rem 1.75rem;
  border: 1px solid var(--hairline);
  border-radius: 4px;
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
  align-items: center;
  justify-content: space-between;
}

.pkg-nudge p {
  margin: 0;
  max-width: 46ch;
}

/* --- Package plans (secondary framing) --- */

.pkg-frame {
  max-width: 62ch;
}

.pkg-contact-note {
  margin-top: 3rem;
  padding-top: 2rem;
  border-top: 1px solid var(--hairline);
}

/* --- About --- */

.about-team {
  display: grid;
  gap: 1.5rem;
  grid-template-columns: repeat(2, 1fr);
  margin-top: 2.5rem;
}

@media (min-width: 700px) {
  .about-team {
    grid-template-columns: repeat(4, 1fr);
  }
}

.about-team__card figure {
  margin: 0 0 0.85rem;
}

.about-team__card figure .media-slot {
  aspect-ratio: var(--slot-ratio, 9 / 16);
}

.about-team__card b {
  display: block;
  font-family: var(--font-display);
  font-weight: 800;
  text-transform: uppercase;
  font-size: 1rem;
}

.about-team__card span {
  font-size: 0.78rem;
  color: var(--ink-dim);
}

.about-values {
  display: grid;
  gap: 1.5rem;
  margin-top: 2rem;
}

@media (min-width: 700px) {
  .about-values {
    grid-template-columns: repeat(3, 1fr);
  }
}

.about-values__card h3 {
  font-family: var(--font-display);
  font-weight: 800;
  text-transform: uppercase;
  font-size: 1.2rem;
  margin: 0 0 0.6rem;
}

/* --- What sets us apart (About Us) --- */

/* A big heading that holds its place on desktop while the list runs past,
   and hairline rows with one description open at a time. */
.apart__grid {
  display: grid;
  gap: 2.5rem;
}

@media (min-width: 900px) {
  .apart__grid {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1.15fr);
    gap: 4rem;
    align-items: start;
  }

  .apart__title {
    position: sticky;
    top: 7rem;
  }
}

.apart__title {
  font-size: clamp(3rem, 5vw + 1.2rem, 6.5rem);
  line-height: 0.88;
}

.apart__list {
  list-style: none;
  margin: 0;
  padding: 0;
}

.apart__item {
  border-bottom: 1px solid var(--hairline);
}

.apart__head {
  font-size: inherit;
}

.apart__toggle {
  display: block;
  width: 100%;
  padding: 1.35rem 0;
  border: 0;
  background: none;
  color: inherit;
  text-align: left;
  cursor: pointer;
  font-family: var(--font-display);
  font-weight: 800;
  text-transform: uppercase;
  font-size: clamp(1.7rem, 1.6vw + 1.1rem, 2.7rem);
  line-height: 0.95;
  letter-spacing: -0.01em;
  transition: color 0.25s ease;
}

.apart__toggle:hover {
  color: var(--accent-magenta-text, var(--brand-magenta));
}

.apart__toggle:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 4px;
}

/* Open by default, so the descriptions are all there without JS. Once
   apart.js is running, closed rows fold to nothing: grid rows animate the
   height, the text itself only fades and settles. */
.apart__panel {
  display: grid;
  grid-template-rows: 1fr;
}

.apart__panel-inner {
  min-height: 0;
  overflow: hidden;
}

.apart__panel p {
  margin: 0;
  padding: 0 0 1.5rem;
  max-width: 44ch;
  font-size: 1.05rem;
  line-height: 1.5;
}

[data-apart-ready] .apart__panel {
  grid-template-rows: 0fr;
  transition: grid-template-rows 0.5s cubic-bezier(0.16, 1, 0.3, 1);
}

[data-apart-ready] .apart__item.is-open .apart__panel {
  grid-template-rows: 1fr;
}

[data-apart-ready] .apart__panel p {
  opacity: 0;
  transform: translateY(-6px);
  transition: opacity 0.25s ease, transform 0.5s cubic-bezier(0.16, 1, 0.3, 1);
}

[data-apart-ready] .apart__item.is-open .apart__panel p {
  opacity: 1;
  transform: none;
  transition-delay: 0.08s;
}

@media (prefers-reduced-motion: reduce) {
  [data-apart-ready] .apart__panel,
  [data-apart-ready] .apart__panel p {
    transition: none;
  }
}

/* --- Contact --- */

.contact-grid {
  display: grid;
  gap: 3rem;
}

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

.contact-form {
  display: grid;
  gap: 1.1rem;
}

.contact-form label {
  display: grid;
  gap: 0.4rem;
  font-size: 0.82rem;
  font-weight: 700;
}

.contact-form input,
.contact-form select,
.contact-form textarea {
  font-family: var(--font-body);
  font-size: 1rem;
  padding: 0.75em 0.9em;
  min-height: 48px;
  border: 1.5px solid var(--hairline);
  border-radius: 6px;
  background: var(--canvas-white);
  color: var(--brand-black);
}

/* Our own chevron instead of the browser's: the native arrow ignored the
   padding and sat on the right border, and Safari's native select drew
   its text inconsistently. Same box as the inputs, arrow inset 1rem. */
.contact-form select {
  appearance: none;
  -webkit-appearance: none;
  padding-right: 2.75rem;
  line-height: 1.3;
  text-overflow: ellipsis;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none'%3E%3Cpath d='M4 6l4 4 4-4' stroke='%23231F20' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 1rem center;
  background-size: 1rem;
  cursor: pointer;
}

.contact-form textarea {
  min-height: 140px;
  resize: vertical;
}

.contact-form input:focus-visible,
.contact-form select:focus-visible,
.contact-form textarea:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 1px;
}

.contact-form .optional {
  font-weight: 400;
  color: var(--ink-dim);
  text-transform: none;
  letter-spacing: 0;
}

/* The submit button only: a bare `button` also reached the country
   picker's own buttons and dropped the flag and dial code 8px below the
   middle of the phone field. */
.contact-form button[type="submit"] {
  margin-top: 0.5rem;
}

/* Preferred channel: pill choices, the site's own button shape. The
   native radio stays visible so the selected state never relies on colour
   alone. */
.contact-method {
  border: 0;
  padding: 0;
  margin: 0;
  min-width: 0;
}

.contact-method legend {
  padding: 0;
  margin-bottom: 0.4rem;
  font-size: 0.82rem;
  font-weight: 700;
}

.contact-method__options {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}

.contact-form .contact-method__options label {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  min-height: 44px;
  padding: 0.55rem 1rem;
  border: 1.5px solid var(--hairline);
  border-radius: var(--radius-pill);
  background: var(--canvas-white);
  color: var(--brand-black);
  font-size: 0.92rem;
  cursor: pointer;
}

.contact-form .contact-method__options label:has(input:checked) {
  border-color: var(--brand-black);
}

/* Phone field with the country picker (intl-tel-input). Its label sits
   beside the input rather than around it, so this wrapper reproduces the
   label grid the other fields get from .contact-form label. */
.contact-field {
  display: grid;
  gap: 0.4rem;
  /* The form's reveal leaves a transform on every field, which makes each
     one its own stacking layer; later fields then painted over the open
     country list. Lifting this field keeps the list on top. */
  position: relative;
  z-index: 3;
}

/* The no-assets vendor stylesheet leaves the flag sprite paths to us. */
:root {
  --iti-path-flags-1x: url("/assets/vendor/intl-tel-input/flags.webp");
  --iti-path-flags-2x: url("/assets/vendor/intl-tel-input/flags@2x.webp");
}

.contact-form .iti {
  display: block;
  --iti-border-color: var(--hairline);
  --iti-country-selector-bg: var(--canvas-white);
  --iti-hover-color: color-mix(in oklab, var(--brand-magenta) 10%, transparent);
  --iti-icon-color: var(--brand-black);
}

.contact-form .iti__selected-country {
  border-radius: 6px 0 0 6px;
}

.contact-form .iti__selected-country:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 1px;
}

/* The country list. These rules are rooted on .iti, not .contact-form, on
   purpose: on first open intl-tel-input measures the list in a temporary
   `.iti` container on <body>, outside the form, then pins that height on
   it. Anything that changed the list's size only inside the form made the
   real list taller than the pinned box, and its last row spilled over the
   next field. Same rules in both places, same size.

   The search box needs (0,2,0) to beat `.contact-form input`, which would
   otherwise give it the 48px field height and padding; the vendor border
   width is kept for the same reason. */
.iti.iti--inline-country-selector .iti__country-selector {
  overflow: hidden;
  font-family: var(--font-body);
  font-size: 0.95rem;
  font-weight: 400;
  color: var(--brand-black);
  border-radius: 6px;
  box-shadow: 0 12px 32px rgba(35, 31, 32, 0.14);
}

.iti .iti__search-input {
  min-height: 0;
  margin: 0;
  border: 0;
  border-radius: 0;
  background: transparent;
  color: var(--brand-black);
  font-family: var(--font-body);
  font-size: 1rem;
  line-height: 1.25;
  padding-top: 8px;
  padding-bottom: 8px;
  padding-left: calc(var(--iti-spacer-horizontal) + var(--iti-globe-icon-size) + var(--iti-spacer-horizontal));
  padding-right: calc(var(--iti-spacer-horizontal) + var(--iti-clear-icon-size) + var(--iti-spacer-horizontal));
}

.contact-form .contact-method__options input {
  width: 1.05rem;
  height: 1.05rem;
  min-height: 0;
  margin: 0;
  padding: 0;
  accent-color: var(--brand-magenta);
}

.contact-direct {
  border-top: 1px solid var(--hairline);
  margin-top: 2rem;
  padding-top: 2rem;
}

.contact-direct a {
  font-weight: 700;
}

.contact-response-note {
  color: var(--ink-dim);
  font-size: 0.85rem;
  margin-top: 1rem;
}

/* Submission result. Colour is never the only signal — each state also
   carries its own left rule and its text says plainly what happened — so
   it still reads for a colour-blind visitor and in forced-colours mode.
   All three inks are >=6.3:1 on the ivory field (AA at any size). */
.form-status {
  margin-top: 1rem;
  padding: 0.7rem 0 0.7rem 0.9rem;
  border-left: 3px solid currentColor;
  font-size: 0.92rem;
  line-height: 1.5;
  color: var(--ink-dim);
}

.form-status[data-state="ok"] {
  color: var(--teal-muted);
}

.form-status[data-state="error"] {
  color: var(--accent-ink);
}

.form-status a {
  color: inherit;
}

/* Insights articles: long-form reading, so the body gets the legal pages'
   calmer heading scale and a comfortable measure. */
.article-body {
  max-width: 68ch;
}

.article-body h2 {
  font-size: clamp(1.3rem, 1vw + 1.05rem, 1.75rem);
  text-transform: none;
  letter-spacing: 0;
  font-family: var(--font-body);
  font-weight: 700;
  line-height: 1.25;
  margin: 2.25rem 0 0.75rem;
}

.article-body h3 {
  font-size: 1.15rem;
  text-transform: none;
  font-family: var(--font-body);
  font-weight: 700;
  line-height: 1.3;
  margin: 1.75rem 0 0.5rem;
}

.article-body ul,
.article-body ol {
  padding-left: 1.2em;
  margin: 0 0 1em;
}

.article-body li {
  margin-bottom: 0.4em;
}

.article-cta {
  margin-top: clamp(2.5rem, 6vw, 4rem);
  padding-top: 2rem;
  border-top: 1px solid var(--hairline);
}

.article-hero {
  width: 100%;
  height: auto;
  border-radius: var(--radius-card);
}

.insight-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 0;
}

.insight-list__item {
  display: grid;
  gap: 0.35rem;
  padding-block: 1.5rem;
  border-top: 1px solid var(--hairline);
  text-decoration: none;
}

.insight-list__item time {
  font-size: 0.8rem;
  color: var(--ink-dim);
}

.insight-list__title {
  font-family: var(--font-display);
  font-weight: 800;
  text-transform: uppercase;
  font-size: clamp(1.4rem, 1.2vw + 1rem, 2rem);
  line-height: 1;
}

.insight-list__desc {
  max-width: 60ch;
  color: var(--ink-dim);
}

/* canvas.css */
/* Scroll-reactive brand canvas.

   ONE shared background behind the whole page. Sections declare a chapter
   (data-chapter) and the canvas transitions to it as that section reaches
   the reading line — instead of the page being a stack of hard-edged
   colored rectangles.

   Why CSS transitions rather than a JS tween: a CSS transition always
   interpolates from the CURRENT computed value. Scroll fast through three
   chapters and each new target simply retargets mid-flight from whatever
   color is on screen — no flash, no queue of delayed transitions to
   unwind, no rAF loop running while scrolling. The JS only ever sets an
   attribute.

   PROGRESSIVE ENHANCEMENT: sections keep their original solid .block-*
   backgrounds by default. They only go transparent under
   [data-canvas-on], which theme-canvas.js sets after confirming it can
   run. No JS, or a failed script, leaves the original readable page. */

.page-canvas {
  display: none;
}

:root[data-canvas-on] .page-canvas {
  display: block;
  position: fixed;
  inset: 0;
  z-index: -1;
  background-color: var(--canvas);
  transition: background-color 900ms cubic-bezier(0.22, 1, 0.36, 1);
}

/* --- Chapter themes ---------------------------------------------------

   Each chapter ships a full foreground set, not just a background. The
   brand purple/magenta are NOT text-safe on the pale chapters (magenta on
   peach is 3.0:1), so every chapter has separate text-accent tokens —
   the pure brand colors stay for graphic fills, rules and icons. */

:root {
  --canvas: var(--canvas-ivory);
  --chapter-ink: var(--brand-black);
  --chapter-ink-dim: #463F42;
  --chapter-rule: rgba(35, 31, 32, 0.18);
  /* Text-safe accents: ≥4.5:1 on every light chapter, peach included. */
  --accent-purple-text: #7A1F6B;
  --accent-magenta-text: #7D1C59;
  --accent-orange-text: #7F2912;
  /* Graphic-only accent — rules, dots, arrows, never normal-size text. */
  --accent-graphic: var(--brand-accent);
  --cursor-fill: var(--brand-magenta);
  --cursor-text: var(--canvas-white);
}

:root[data-chapter="ivory"] {
  --canvas: #F7F2EC;
}

:root[data-chapter="lilac"] {
  --canvas: #D8CCF0;
}

:root[data-chapter="sage"] {
  --canvas: #B8CEC4;
}

:root[data-chapter="peach"] {
  --canvas: #F2AA92;
}

/* Deep plum: the one dark chapter, so the whole foreground set flips. */
:root[data-chapter="plum"] {
  --canvas: #35142F;
  --chapter-ink: var(--canvas-white);
  --chapter-ink-dim: rgba(255, 249, 244, 0.75);
  --chapter-rule: rgba(255, 249, 244, 0.24);
  --accent-purple-text: #CE86BE;
  --accent-magenta-text: #E88FC0;
  --accent-orange-text: #F2AA92;
  --cursor-fill: var(--tint-peach);
  --cursor-text: var(--brand-black);
}

/* --- Sections become transparent, canvas shows through --------------- */

:root[data-canvas-on] .block-hero,
:root[data-canvas-on] .block-energy,
:root[data-canvas-on] .block-warmth,
:root[data-canvas-on] .block-optimism,
:root[data-canvas-on] .block-calm,
:root[data-canvas-on] .block-plum {
  background: transparent;
  color: var(--chapter-ink);
  --block-bg: var(--canvas);
  --block-ink: var(--chapter-ink);
  --ink: var(--chapter-ink);
  --ink-dim: var(--chapter-ink-dim);
  --hairline: var(--chapter-rule);
  transition: color 900ms cubic-bezier(0.22, 1, 0.36, 1);
}

:root[data-canvas-on] body {
  background: transparent;
  color: var(--chapter-ink);
  transition: color 900ms cubic-bezier(0.22, 1, 0.36, 1);
}

/* Borders and rules follow the chapter too, or they strand mid-blend. */
:root[data-canvas-on] .statement-list,
:root[data-canvas-on] .statement-row,
:root[data-canvas-on] .quote-row,
:root[data-canvas-on] .cs-tags li,
:root[data-canvas-on] .service-process__step,
:root[data-canvas-on] .pkg-nudge,
:root[data-canvas-on] .footer-manifest,
:root[data-canvas-on] .footer-legal-row,
:root[data-canvas-on] .cs-pager__all,
:root[data-canvas-on] .work-filters button,
:root[data-canvas-on] .work-featured__btn,
:root[data-canvas-on] .btn {
  transition: color 900ms cubic-bezier(0.22, 1, 0.36, 1),
    border-color 900ms cubic-bezier(0.22, 1, 0.36, 1),
    background-color 300ms ease;
}

/* Media keeps its own surface — the canvas must never tint a photograph,
   a video, a case-study asset, or a placeholder standing in for one. */
:root[data-canvas-on] .media-slot,
:root[data-canvas-on] .work-card,
:root[data-canvas-on] .directory__badge {
  color: var(--brand-black);
}

/* --- Chapter-aware accents ------------------------------------------- */

:root[data-canvas-on] .cs-hero__client,
:root[data-canvas-on] .work-card__index {
  color: var(--accent-magenta-text);
}

:root[data-canvas-on] .service-process__step b,
:root[data-canvas-on] .kicker {
  color: var(--accent-orange-text);
}

:root[data-canvas-on] .quote-row__tier {
  color: var(--accent-purple-text);
}

:root[data-canvas-on] .quote-row:nth-child(2) .quote-row__tier {
  color: var(--accent-magenta-text);
}

/* --- Header: readable on every chapter, never resized ---------------- */

:root[data-canvas-on] .site-header .brand,
:root[data-canvas-on] .theme-toggle,
:root[data-canvas-on] .nav-toggle {
  transition: color 900ms cubic-bezier(0.22, 1, 0.36, 1),
    border-color 900ms cubic-bezier(0.22, 1, 0.36, 1),
    background-color 900ms cubic-bezier(0.22, 1, 0.36, 1);
}

/* The pills need the chapter blend AND their collapse animation. This
   shorthand loads after nav.css, so leaving `transform` out of it
   silently dropped the collapse transition and the pills snapped between
   states — that was the real cause, not the collapse technique. */
:root[data-canvas-on] .pill-nav a {
  transition: color 900ms cubic-bezier(0.22, 1, 0.36, 1),
    border-color 900ms cubic-bezier(0.22, 1, 0.36, 1),
    background-color 900ms cubic-bezier(0.22, 1, 0.36, 1),
    transform 0.4s cubic-bezier(0.22, 1, 0.36, 1),
    box-shadow 0.2s ease;
}

:root[data-canvas-on] .site-header .brand,
:root[data-canvas-on] .theme-toggle,
:root[data-canvas-on] .nav-toggle {
  background: transparent;
  color: var(--chapter-ink);
  border-color: var(--chapter-rule);
}

/* Reversed monogram cross-fades in on the dark chapter. Both marks are
   stacked in the same box, so nothing resizes or shifts during the
   blend and the logo never drops below contrast mid-transition. */
.brand__marks {
  position: relative;
  display: inline-flex;
  flex: none;
}

.brand__mark--reversed {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  transition: opacity 900ms cubic-bezier(0.22, 1, 0.36, 1);
}

:root[data-chapter="plum"][data-canvas-on] .brand__mark--reversed {
  opacity: 1;
}

/* The full-color mark holds its brand fills on light chapters; on plum it
   fades under the reversed copy rather than being recolored. */
:root[data-chapter="plum"][data-canvas-on] .brand__mark {
  opacity: 0;
  transition: opacity 900ms cubic-bezier(0.22, 1, 0.36, 1);
}

/* --- Signal Drop sits in the same world as the page ------------------ */

:root[data-canvas-on] .signal-drop__say {
  color: var(--chapter-ink);
}

/* The collapsed nav dots are fixed brand colors, and purple/magenta land
   at 2.4–2.8:1 on the plum chapter — under the 3:1 WCAG floor for a
   meaningful UI component. A ring delineates them without recoloring the
   brand or breaking the scroll-position mapping. */
:root[data-chapter="plum"][data-canvas-on] .pill-nav.is-collapsed a {
  box-shadow: 0 0 0 1.5px rgba(255, 249, 244, 0.85);
}

:root[data-chapter="plum"][data-canvas-on] .pill-nav.is-collapsed a.is-active {
  box-shadow: 0 0 0 1.5px rgba(255, 249, 244, 0.85),
    0 0 0 6px var(--accent);
}

/* The root carries the chapter color too, so overscroll/rubber-banding
   past the canvas never exposes a white page underneath. */
:root[data-canvas-on] {
  background-color: var(--canvas);
  transition: background-color 900ms cubic-bezier(0.22, 1, 0.36, 1);
}

/* --- Reduced motion: change chapters, don't perform them ------------- */

@media (prefers-reduced-motion: reduce) {
  :root[data-canvas-on] .page-canvas,
  :root[data-canvas-on] body,
  :root[data-canvas-on] .block-hero,
  :root[data-canvas-on] .block-energy,
  :root[data-canvas-on] .block-warmth,
  :root[data-canvas-on] .block-optimism,
  :root[data-canvas-on] .block-calm,
  :root[data-canvas-on] .brand__mark,
  :root[data-canvas-on] .brand__mark--reversed,
  :root[data-canvas-on] .site-header .brand,
  :root[data-canvas-on] .pill-nav a,
  :root[data-canvas-on] .theme-toggle,
  :root[data-canvas-on] .nav-toggle {
    transition-duration: 180ms !important;
  }
}

/* seed.css */
/* Signal Seed Morph — a card is an idea gaining reach.

   A small brand-colored node lifts along the AR arrow's axis, opens into
   the finished card, and the media is uncovered by a diagonal wipe raked
   at the same -38° the monogram's internal arrow runs on. Labels follow.

   NOTHING HERE HIDES CONTENT BY DEFAULT. Every pre-animation state is
   gated on [data-motion-ready], which main.js sets only after confirming
   GSAP + ScrollTrigger are present and motion is allowed. No JS, a failed
   CDN, or prefers-reduced-motion all render the finished cards.

   No layout animation: the card reserves its final box up front and only
   clip-path, transform and opacity move, so nothing reflows and the grid
   never shifts. */

[data-seed] {
  position: relative;
}

/* The finished state — and the only state without JS. */
[data-seed] .media-slot {
  clip-path: inset(0% round var(--seed-radius, 20px));
}

/* --- Pre-reveal: the seed ------------------------------------------- */

:root[data-motion-ready] [data-seed]:not(.is-seeded) .media-slot {
  /* A circular node, not the card. Radius is in px so it stays a true
     circle whatever the card's aspect ratio. */
  clip-path: circle(var(--seed-size, 30px) at 50% 62%);
  background: var(--seed-tone, var(--brand-purple));
}

/* The placeholder caption and frame belong to the card, not the seed —
   they must not be visible inside the little circle. The poster and video
   belong to the card too: clip-path clips descendants, and both are
   absolutely inset with object-fit:cover, so an opaque poster inside the
   circle renders a crop of client imagery over the --seed-tone background
   above. The seed at rest is a solid brand-colour block. */
:root[data-motion-ready] [data-seed]:not(.is-seeded) .media-slot__note,
:root[data-motion-ready] [data-seed]:not(.is-seeded) .media-slot__poster,
:root[data-motion-ready] [data-seed]:not(.is-seeded) .media-slot__video,
:root[data-motion-ready] [data-seed]:not(.is-seeded) .media-slot::after {
  opacity: 0;
}

/* Scoped to the seed itself. These were written as descendants of
   .about-team__card, but that class sits ON the card — so inside a card
   the selector never matched and the portrait labels were never part of
   the reveal. */
:root[data-motion-ready] [data-seed]:not(.is-seeded) .work-tile__meta,
:root[data-motion-ready] [data-seed]:not(.is-seeded) > b,
:root[data-motion-ready] [data-seed]:not(.is-seeded) > span {
  opacity: 0;
}

/* Deterministic tones — assigned by position, never randomised per load,
   so the page looks the same every visit. */
[data-seed-tone="purple"] {
  --seed-tone: var(--brand-purple);
}

[data-seed-tone="magenta"] {
  --seed-tone: var(--brand-magenta);
}

[data-seed-tone="orange"] {
  --seed-tone: var(--brand-accent);
}

/* --- The diagonal wipe ----------------------------------------------- */

/* A brand-colored plate raked on the monogram's arrow axis, swept off
   toward the upper right to uncover the media. */
.seed-wipe {
  position: absolute;
  inset: -20%;
  background: var(--seed-tone, var(--brand-purple));
  pointer-events: none;
  z-index: 2;
  transform: rotate(-38deg) translate3d(0, 0, 0);
  transform-origin: 50% 50%;
}

[data-seed]:not([data-wiping]) .seed-wipe {
  display: none;
}

[data-seed] figure {
  position: relative;
  overflow: hidden;
  border-radius: var(--seed-radius, 20px);
}

/* --- About: warmer, softer, slower ----------------------------------- */

.about-team__card[data-seed] {
  --seed-radius: 24px;
  --seed-size: 26px;
}

/* --- Hover, after the reveal ----------------------------------------- */

[data-seed].is-seeded {
  transition: transform 0.3s cubic-bezier(0.22, 1, 0.36, 1);
}

.work-tile[data-seed].is-seeded:hover {
  transform: translateY(-5px);
}

.work-tile[data-seed] .media-slot {
  transition: scale 0.35s cubic-bezier(0.22, 1, 0.36, 1);
}

.work-tile[data-seed].is-seeded:hover .media-slot {
  scale: 1.02;
}

/* People are not products — portraits lift, they do not scale or bounce. */
.about-team__card[data-seed].is-seeded:hover {
  transform: translateY(-3px);
}

/* --- Mobile: shorter, simpler ---------------------------------------- */

@media (max-width: 899px) {
  :root[data-motion-ready] [data-seed]:not(.is-seeded) .media-slot {
    clip-path: circle(var(--seed-size-m, 24px) at 50% 60%);
  }

  [data-seed] {
    --seed-radius: 16px;
    --seed-size-m: 24px;
  }
}

/* --- Reduced motion: the finished card, immediately ------------------ */

@media (prefers-reduced-motion: reduce) {
  [data-seed] .media-slot,
  :root[data-motion-ready] [data-seed]:not(.is-seeded) .media-slot {
    clip-path: inset(0% round var(--seed-radius, 20px)) !important;
    background: var(--tint-blush);
  }

  :root[data-motion-ready] [data-seed]:not(.is-seeded) .media-slot__note,
  :root[data-motion-ready] [data-seed]:not(.is-seeded) .media-slot::after,
  :root[data-motion-ready] [data-seed]:not(.is-seeded) .work-tile__meta,
  :root[data-motion-ready] [data-seed]:not(.is-seeded) > b,
  :root[data-motion-ready] [data-seed]:not(.is-seeded) > span {
    opacity: 1 !important;
  }

  .seed-wipe {
    display: none !important;
  }

  [data-seed].is-seeded:hover,
  [data-seed].is-seeded:hover .media-slot {
    transform: none;
    scale: 1;
  }
}

/* pacing.css */
/* Amplify Cinematic Pacing — scroll runway for signature sequences.

   A chapter is a tall wrapper (the runway) containing a sticky stage (the
   composition that holds still while you scroll past it). Progress
   through the runway drives the animation, so:

     - refreshing mid-chapter lands in a valid state (state is derived
       from scroll position, not from an event that already fired),
     - resizing recalculates for free (the runway is in vh),
     - keyboard scrolling, anchors and browser history all behave
       normally — nothing is locked, nothing is slowed.

   Native sticky is used rather than a JS pin: a pinned element gets a
   generated spacer and its layout rewritten, which is exactly what breaks
   under resize and refresh.

   RUNWAY LENGTH IS CONTENT-DRIVEN. --chapter-rows sets how many card rows
   a chapter reveals; the height follows from that, so a chapter is never
   longer than it has progress to show. */

.chapter {
  --chapter-rows: 1;
  --chapter-base: 118vh;
  --chapter-per-row: 46vh;
  position: relative;
  /* Runway = the base sequence plus a little per extra row. One row lands
     around 118vh, two around 164vh — inside the 120–180vh band. */
  min-height: calc(var(--chapter-base) + (var(--chapter-rows) - 1) * var(--chapter-per-row));
}

.chapter__stage {
  position: sticky;
  /* Clears the fixed header so headings never hide behind it. */
  top: clamp(88px, 11vh, 128px);
}

/* The last 15% of a runway is release, not dead space: the stage stops
   sticking slightly early so the next chapter arrives without the visitor
   scrolling through an empty screen. */
.chapter::after {
  content: "";
  display: block;
  height: 12vh;
}

/* A chapter whose cards are all filtered out has no progress left to
   show, so it gives up its runway entirely rather than leaving an empty
   screen to scroll through. */
.chapter[data-chapter-empty] {
  min-height: 0;
}

.chapter[data-chapter-empty]::after {
  display: none;
}

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

/* The Signal Drop needs room for chips to fall and settle before the page
   ends. Applied to the footer wrapper, not the stage, so the links are
   reachable the whole way down. */
@media (min-width: 900px) {
  .signal-drop {
    min-height: 118vh;
    display: flex;
    flex-direction: column;
    justify-content: center;
  }
}

/* --- Mobile: shorter, and no sticky ----------------------------------- */

@media (max-width: 899px) {
  .chapter {
    /* ~30% shorter, and reveals run in normal document flow — a long
       pinned section on a phone means several big swipes for one card. */
    --chapter-base: 80vh;
    --chapter-per-row: 30vh;
  }

  .chapter__stage {
    position: static;
  }

  .chapter::after {
    height: 4vh;
  }

  .signal-drop {
    min-height: 0;
  }
}

/* --- Reduced motion: no runway at all --------------------------------- */

/* Nothing is animating, so the extra scroll distance would be pure cost.
   The chapter collapses to its content and the stage stops sticking. */
@media (prefers-reduced-motion: reduce) {
  .chapter {
    min-height: 0 !important;
  }

  .chapter__stage {
    position: static !important;
  }

  .chapter::after {
    display: none !important;
  }

  .signal-drop {
    min-height: 0 !important;
  }
}

/* --- No-JS: same as reduced motion ------------------------------------ */

/* Without [data-motion-ready] there is no scrubbed sequence to give the
   runway meaning, so it collapses rather than leaving the visitor
   scrolling past a static composition. */
:root:not([data-motion-ready]) .chapter {
  min-height: 0;
}

:root:not([data-motion-ready]) .chapter__stage {
  position: static;
}

:root:not([data-motion-ready]) .chapter::after {
  display: none;
}

/* cursor.css */
/* Custom cursor. Only ever active when cursor.js has confirmed a fine
   pointer and no reduced-motion preference, and it never replaces a focus
   indicator — keyboard focus rings are untouched. */

.cursor {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 300;
  opacity: 0;
  transition: opacity 0.18s ease;
}

.cursor[data-visible] {
  opacity: 1;
}

.cursor__dot,
.cursor__label {
  position: absolute;
  top: 0;
  left: 0;
  pointer-events: none;
}

.cursor__dot {
  width: 10px;
  height: 10px;
  margin: -5px 0 0 -5px;
  border-radius: 999px;
  /* Follows the active chapter: magenta on light grounds, peach on the
     dark plum chapter, so it never sinks into the background mid-blend. */
  background: var(--cursor-fill, var(--brand-magenta));
  box-shadow: 0 0 0 1.5px color-mix(in oklab, var(--canvas-white) 70%, transparent);
  transition: width 0.18s cubic-bezier(0.16, 1, 0.3, 1),
    height 0.18s cubic-bezier(0.16, 1, 0.3, 1),
    margin 0.18s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.18s ease,
    background-color 900ms cubic-bezier(0.22, 1, 0.36, 1);
}

.cursor[data-mode="grow"] .cursor__dot {
  width: 26px;
  height: 26px;
  margin: -13px 0 0 -13px;
}

.cursor[data-mode="label"] .cursor__dot {
  opacity: 0;
}

.cursor[data-down] .cursor__dot {
  width: 7px;
  height: 7px;
  margin: -3.5px 0 0 -3.5px;
}

.cursor__label {
  /* Offset clears a 48px target grabbed at its centre, so a bubble never
     sits on the label it is describing. 14px did not: on the Signal Drop
     chips it landed inside the chip. Elements whose own text says the
     same thing should carry data-cursor="" (plain dot) rather than a
     label at all. */
  margin: 26px 0 0 18px;
  padding: 0.5em 0.9em;
  border-radius: 999px;
  background: var(--cursor-fill, var(--brand-magenta));
  color: var(--cursor-text, var(--canvas-white));
  font-family: var(--font-body);
  font-weight: 700;
  font-size: 0.68rem;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  white-space: nowrap;
  opacity: 0;
  scale: 0.8;
  transform-origin: 0 0;
  transition: opacity 0.16s ease, scale 0.2s cubic-bezier(0.16, 1, 0.3, 1),
    background-color 900ms cubic-bezier(0.22, 1, 0.36, 1),
    color 900ms cubic-bezier(0.22, 1, 0.36, 1);
}

.cursor[data-mode="label"] .cursor__label {
  opacity: 1;
  scale: 1;
}

/* Hide the native pointer only over elements that declare a cursor state,
   so text selection and form fields keep their real cursors. */
:root[data-cursor-on] [data-cursor] {
  cursor: none;
}

:root[data-cursor-on] [data-cursor]:focus-visible {
  cursor: auto;
}

@media (prefers-reduced-motion: reduce), (pointer: coarse) {
  .cursor {
    display: none !important;
  }

  :root[data-cursor-on] [data-cursor] {
    cursor: auto;
  }
}

/* bloom.css */
/* Page Bloom is invisible until a just-navigated page is known to arrive.
   CSS owns the initial plum frame so the next document cannot flash through
   before deferred JavaScript opens the masked hole. */
.page-bloom {
  position: fixed;
  inset: 0;
  z-index: 400;
  visibility: hidden;
  pointer-events: none;
  opacity: 0;
}

.page-bloom svg {
  display: block;
  width: 100vw;
  height: 100vh;
}

.page-bloom[data-bloom-state="leaving"],
html[data-bloom="arriving"] .page-bloom {
  visibility: visible;
  opacity: 1;
}

/* The masked cover belongs to the next document. The outgoing document
   shows its three expanding discs first, ending in the same plum field. */
.page-bloom[data-bloom-state="leaving"] [data-bloom-cover] {
  opacity: 0;
}

/* If the deferred script is blocked or throws, this releases the arriving
   cover instead of leaving a visitor on an opaque plum screen forever. */
html[data-bloom="arriving"] .page-bloom {
  pointer-events: auto;
  animation: bloom-failsafe 2.5s steps(1, end) forwards;
}

@keyframes bloom-failsafe {
  to {
    visibility: hidden;
    pointer-events: none;
    opacity: 0;
  }
}

/* No cross-document view transition (`@view-transition`). It was added to
   hold the plum frame across the load, but while Chrome runs one it stops
   presenting the new document for ~500ms even though the page keeps
   animating — so the reveal froze part-open on every navigation. It is not
   needed: Chrome already keeps the old frame until the next page's first
   paint, and that first paint is the plum cover (html[data-bloom]). */

@media (prefers-reduced-motion: reduce) {
  .page-bloom,
  html[data-bloom="arriving"] .page-bloom {
    display: none !important;
    animation: none;
  }
}
