/* ==========================================================================
   Bunn Design Co. — design system
   Fonts: Space Grotesk (display/UI) · Manrope (body) · Instrument Serif (accent)
   ========================================================================== */

/* ---------- tokens ---------- */
:root {
  --paper:      #F5F3ED;
  --paper-2:    #E9E7E1;
  --white:      #FFFFFF;
  --ink:        #111111;
  --ink-70:     rgba(17, 17, 17, .70);
  /* muted label colour — .64 keeps small uppercase text above WCAG AA (4.5:1)
     on both --paper and --white. Do not lower it. */
  --ink-55:     rgba(17, 17, 17, .64);
  --ink-14:     rgba(17, 17, 17, .14);
  --ink-08:     rgba(17, 17, 17, .08);
  /* TRYING: olive/luxury green swapped in for the blue accent, to preview it.
     Names kept as --blue/--blue-ink/--blue-dark on purpose — everything in the
     file already points at these tokens, so this is a 3-line, fully reversible
     change. If green sticks, rename the tokens properly in a follow-up pass. */
  --blue:       #4B5D34;   /* fills, borders, large display type — passes AA (6.5:1) on paper */
  --blue-ink:   #3C4A29;   /* small text/borders — extra depth + margin (8.6:1 on paper) */
  --blue-dark:  #333F22;   /* hover-darken state */
  --night:      #0D0D0D;

  --sans:    "Manrope", ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
  --display: "Space Grotesk", "Manrope", ui-sans-serif, system-ui, -apple-system, "Segoe UI", Arial, sans-serif;
  --serif:   "Instrument Serif", ui-serif, Georgia, "Times New Roman", serif;

  /* fluid type scale */
  --t-xs:      clamp(.72rem, .70rem + .10vw, .80rem);
  --t-sm:      clamp(.82rem, .79rem + .14vw, .92rem);
  --t-body:    clamp(1rem, .965rem + .18vw, 1.09rem);
  --t-lead:    clamp(1.08rem, 1.01rem + .34vw, 1.32rem);
  --t-h3:      clamp(1.3rem, 1.13rem + .72vw, 1.85rem);
  --t-h2:      clamp(2rem, 1.42rem + 2.5vw, 3.7rem);
  --t-display: clamp(2.55rem, 1.25rem + 5.9vw, 6.6rem);
  --t-mega:    clamp(3.6rem, 1.1rem + 11vw, 11rem);

  /* fluid spacing */
  --s-1:   clamp(.35rem, .3rem + .2vw, .5rem);
  --s-2:   clamp(.6rem, .55rem + .3vw, .85rem);
  --s-3:   clamp(1rem, .9rem + .5vw, 1.4rem);
  --s-4:   clamp(1.5rem, 1.3rem + .9vw, 2.2rem);
  --s-5:   clamp(2.2rem, 1.8rem + 1.6vw, 3.4rem);
  --s-6:   clamp(3.2rem, 2.5rem + 2.8vw, 5.4rem);
  --s-7:   clamp(4.4rem, 3.3rem + 4.6vw, 8rem);
  --gutter: clamp(1.15rem, .6rem + 3.2vw, 4rem);
  --maxw:   1440px;

  /* floating header */
  --nav-h:      clamp(64px, 5.2vw, 82px);   /* height of the bar itself */
  --nav-gap:    clamp(.5rem, 1.1vw, 1rem);  /* breathing room around the bar */
  --brand-size: clamp(40px, 3.6vw, 56px);   /* logo mark */
  --nav-radius:       clamp(16px, 1.6vw, 22px);
  --nav-radius-panel: clamp(14px, 1.4vw, 20px);
  /* total vertical space the fixed bar occupies — everything below offsets by this */
  --nav-space:  calc(var(--nav-h) + var(--nav-gap) * 2);

  /* One glass recipe shared by the bar AND the mobile dropdown panel, so they
     read as the same material. Midpoint between the first pass (too light —
     rgba(224,223,219,.62), nearly matched --paper and disappeared) and the
     second (too dark — rgba(213,211,204,.78)). Composites to ~227,225,220 over
     --paper: ~10-11pt separation, visibly a panel without going slab-opaque. */
  --nav-glass-bg:     rgba(219, 217, 212, .70);
  --nav-glass-blur:   blur(20px) saturate(190%);
  --nav-glass-border: rgba(255, 255, 255, .5);
  --nav-glass-fallback: #E3E1DC; /* solid stand-in when backdrop-filter isn't supported */

  /* The open mobile dropdown gets its OWN, much darker/more opaque fill. It's
     the same warm grey family as the bar (so it still reads as one material),
     but the bar only ever sits over the hero/header area while this panel can
     drop over any section — busy photos included — so it needs far less
     see-through to stay legible. */
  --nav-glass-panel-bg:       rgba(203, 201, 195, .95);
  --nav-glass-panel-fallback: #CBC9C3;

  /* ---- motion system ----
     Two curves and four durations, and nothing on the site is allowed a
     fifth. Before this block existed the same two beziers were re-typed
     literally in five stylesheets and the durations were picked per
     component (.2s here, .35s there, .7s somewhere else), which is exactly
     why the site read as a pile of separate effects rather than one
     directed system.

     --ease-glide is the workhorse: symmetrical, used for anything that
     MOVES while you watch it (hover shifts, parallax, drawing lines).
     --ease-entry decelerates hard at the end, so anything ARRIVING (a
     reveal, a mask, a magnetic release) settles instead of stopping. Nothing
     overshoots or bounces anywhere on the site — the settle comes from the
     curve, never from an elastic keyframe.

     Durations are deliberately at the slow end. The brief is editorial, not
     energetic: --dur-2 is the default for a reveal, --dur-1 for a hover
     response (which has to keep up with the pointer), --dur-3 and
     --dur-line for the big single-shot entrances that carry a section.

     The two --about-ease names that the About/Services/studio layers
     already reference everywhere are the same two curves and stay valid;
     they're just no longer the definition. */
  --ease-glide: cubic-bezier(.2, .7, .2, 1);
  --ease-entry: cubic-bezier(.16, 1, .3, 1);
  --dur-1:    .45s;   /* hover / pointer response */
  --dur-2:    .8s;    /* standard reveal */
  --dur-3:    1.1s;   /* section-carrying entrance */
  --dur-line: 1.15s;  /* a rule or divider drawing across */
  --stagger:  90ms;   /* one step of a staggered group */
}

/* ---------- reset / base ---------- */
*, *::before, *::after { box-sizing: border-box; }
html {
  -webkit-text-size-adjust: 100%; scroll-behavior: smooth;
  /* keep #anchor jumps clear of the fixed header */
  scroll-padding-top: calc(var(--nav-space) + var(--s-2));
}
html, body { overflow-x: clip; }
body {
  margin: 0;
  /* the header is fixed, so reserve its space here */
  padding-top: var(--nav-space);
  background: var(--paper);
  color: var(--ink);
  font-family: var(--sans);
  font-size: var(--t-body);
  line-height: 1.62;
  font-weight: 400;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}
img, svg, video { display: block; max-width: 100%; height: auto; }
h1, h2, h3, h4 { margin: 0; font-family: var(--display); font-weight: 700; line-height: .96; letter-spacing: -.03em; }
p { margin: 0; }
ul, ol { margin: 0; padding: 0; list-style: none; }
a { color: inherit; text-decoration: none; }
button, input, select, textarea { font: inherit; color: inherit; }
:focus-visible {
  outline: 3px solid var(--blue);
  outline-offset: 3px;
  border-radius: 2px;
}
::selection { background: var(--blue); color: #fff; }

.skip {
  position: absolute; left: var(--s-3); top: -100px; z-index: 200;
  background: var(--ink); color: #fff; padding: .7rem 1.1rem; font-weight: 700;
  transition: top .15s;
}
.skip:focus { top: var(--s-3); }
/* Screen-reader-only text. clip-path is the safety net: `clip` only applies to
   absolutely positioned elements, so if another rule ever wins on `position`
   or `width`, clip-path still keeps this visually hidden. */
.sr {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%);
  white-space: nowrap; border: 0;
}

/* ---------- layout ---------- */
.wrap { width: 100%; max-width: var(--maxw); margin-inline: auto; padding-inline: var(--gutter); }
.section { padding-block: var(--s-7); }
.section--tight { padding-block: var(--s-6); }
.section--neutral { background: var(--paper-2); }
.rule { height: 1px; background: var(--ink-14); border: 0; margin: 0; }

/* section heading block */
.eyebrow {
  font-family: var(--display);
  font-size: var(--t-xs);
  font-weight: 600;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--ink-55);
  display: flex; align-items: center; gap: .7rem;
}
.eyebrow::before {
  content: ""; width: 26px; height: 1px; background: var(--ink); flex: none;
}
.eyebrow--blue { color: var(--blue-ink); }
.eyebrow--blue::before { background: var(--blue); }
/* breadcrumb link inside an eyebrow needs a real tap target */
.eyebrow a {
  display: inline-flex; align-items: center; min-height: 44px;
  text-decoration: underline; text-underline-offset: 3px; text-decoration-thickness: 1px;
}
.eyebrow a:hover { color: var(--blue-ink); }
.eyebrow--light { color: rgba(255,255,255,.92); }
.eyebrow--light::before { background: rgba(255,255,255,.8); }

.sec-head {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: var(--s-3) var(--s-4);
  align-items: end;
  margin-bottom: var(--s-5);
}
.sec-head__title { font-size: var(--t-h2); max-width: 22ch; margin-top: var(--s-2); }
.sec-head__aside { max-width: 34ch; color: var(--ink-70); font-size: var(--t-sm); }
@media (max-width: 800px) {
  .sec-head { grid-template-columns: 1fr; align-items: start; }
}

.serif { font-family: var(--serif); font-weight: 400; font-style: italic; letter-spacing: -.01em; }
.lead { font-size: var(--t-lead); line-height: 1.5; color: var(--ink-70); max-width: 48ch; }
.muted { color: var(--ink-70); }
.blue { color: var(--blue); }

/* ---------- buttons ----------
   Plain colour change, not a wipe — a sliding fill read as too much motion
   for a small control someone hovers dozens of times a session. The state
   change is entirely background/text/border colour, transitioning together
   on the same clock, plus a soft green glow that blooms outward as a halo
   rather than travelling across the button. No pseudo-element, no direction:
   the whole face responds at once, evenly.

   transform stays on its own short duration: a magnetic button
   (studio.js/about.js/services.js) writes an inline transform every frame
   while hovered, and a long transition on that property would fight the
   pointer instead of following it. */
.btn {
  --btn-bg: var(--ink); --btn-fg: #fff; --btn-bd: var(--ink);
  --btn-bg-hover: var(--blue); --btn-fg-hover: #fff; --btn-bd-hover: var(--blue);
  position: relative;
  display: inline-flex; align-items: center; justify-content: center; gap: .55rem;
  min-height: 48px; padding: .78rem 1.5rem;
  background: var(--btn-bg); color: var(--btn-fg);
  border: 1.5px solid var(--btn-bd); border-radius: 999px;
  font-family: var(--display); font-size: var(--t-sm); font-weight: 600;
  letter-spacing: .02em; cursor: pointer;
  box-shadow: 0 0 0 0 var(--about-green-16, rgba(75, 93, 52, 0));
  transition: background-color var(--dur-1) var(--ease-glide),
              color var(--dur-1) var(--ease-glide),
              border-color var(--dur-1) var(--ease-glide),
              box-shadow var(--dur-1) var(--ease-glide),
              transform .22s var(--ease-glide);
}
.btn:hover, .btn:focus-visible {
  background: var(--btn-bg-hover); color: var(--btn-fg-hover); border-color: var(--btn-bd-hover);
  box-shadow: 0 0 0 6px var(--about-green-16, rgba(75, 93, 52, .16));
  transform: translateY(-2px);
}
.btn__arrow { transition: transform var(--dur-1) var(--ease-entry); }
.btn:hover .btn__arrow, .btn:focus-visible .btn__arrow { transform: translateX(5px); }
.btn--blue  { --btn-bg: var(--blue); --btn-bd: var(--blue); --btn-bg-hover: var(--ink); --btn-bd-hover: var(--ink); }
.btn--ghost { --btn-bg: transparent; --btn-fg: var(--ink); --btn-bd: var(--ink); --btn-bg-hover: var(--ink); --btn-fg-hover: #fff; --btn-bd-hover: var(--ink); }
.btn--onblue { --btn-bg: var(--ink); --btn-bd: var(--ink); --btn-fg: #fff;
               --btn-bg-hover: #fff; --btn-fg-hover: var(--ink); --btn-bd-hover: #fff; }
.btn--sm { min-height: 44px; padding: .6rem 1.15rem; }
.btn-row { display: flex; flex-wrap: wrap; gap: var(--s-2); }

/* text link with animated underline.
   Two background layers, not one: a permanent hairline at 14% ink sits under
   the label at rest, and the green rule draws across it on hover. So the
   link is legibly a link before you touch it, and hovering DRAWS rather than
   conjures — the same left-to-right gesture the rules and buttons use. */
.tlink {
  display: inline-flex; align-items: center; gap: .45rem;
  font-family: var(--display); font-weight: 600; font-size: var(--t-sm);
  padding-block: .35rem; min-height: 44px; align-self: start;
  background-image: linear-gradient(var(--blue), var(--blue)),
                    linear-gradient(var(--ink-14), var(--ink-14));
  background-size: 0% 1.5px, 100% 1px;
  background-repeat: no-repeat;
  background-position: 0 100%, 0 100%;
  transition: background-size var(--dur-1) var(--ease-entry), color .2s var(--ease-glide);
}
.tlink:hover, .tlink:focus-visible { color: var(--blue-ink); background-size: 100% 1.5px, 100% 1px; }
.tlink svg { transition: transform var(--dur-1) var(--ease-entry); }
.tlink:hover svg, .tlink:focus-visible svg { transform: translateX(5px); }

/* ---------- navigation — floating glass bar ---------- */
/* Pinned: position:fixed, so it never scrolls away. Because it is out of flow,
   body gets --nav-space of top padding (see the base layer) and every sticky
   thing below it offsets by --nav-space too. */
.nav {
  position: fixed;
  top: var(--nav-gap); left: 0; right: 0;
  z-index: 100;
  padding-inline: var(--gutter);
  pointer-events: none;          /* let clicks through the gutter beside the bar */
}
.nav__inner {
  pointer-events: auto;
  position: relative;
  max-width: var(--maxw); margin-inline: auto;
  min-height: var(--nav-h);
  padding-inline: clamp(.85rem, 1.6vw, 1.5rem);
  display: flex; align-items: center; gap: var(--s-4);
  border-radius: var(--nav-radius);
  /* translucent grey: picks up whatever section is passing underneath */
  background: var(--nav-glass-bg);
  backdrop-filter: var(--nav-glass-blur);
  -webkit-backdrop-filter: var(--nav-glass-blur);
  border: 1px solid var(--nav-glass-border);
  box-shadow: 0 1px 2px rgba(17,17,17,.055), 0 9px 26px rgba(17,17,17,.10);
  transition: background .3s ease, box-shadow .3s ease;
}
/* no blur support (or a browser that fakes it badly) — go opaque so text stays legible */
@supports not ((backdrop-filter: blur(4px)) or (-webkit-backdrop-filter: blur(4px))) {
  .nav__inner { background: var(--nav-glass-fallback); border-color: var(--ink-14); }
}
.brand {
  display: flex; align-items: center; gap: clamp(.5rem, .9vw, .8rem);
  margin-right: auto; min-height: 44px;
  min-width: 0; /* let the wordmark actually shrink/ellipsize instead of forcing
                   the flex row wider than the bar and pushing the burger off */
}
.brand img {
  width: var(--brand-size); height: var(--brand-size); flex: none;
  /* no border-radius: this is a transparent letterform mark, not a tile/badge —
     rounding a transparent PNG's bounding box does nothing but risks clipping
     the letters themselves if the crop is ever tightened later */
}
.brand__name {
  font-family: var(--display); font-weight: 700;
  font-size: clamp(1.02rem, .92rem + .42vw, 1.32rem);
  letter-spacing: -.03em; white-space: nowrap;
  min-width: 0; overflow: hidden; text-overflow: ellipsis;
}
/* Below ~360px there isn't room for the full wordmark next to a 46px burger
   without crowding or overflow. The mark alone is enough to identify the site
   once a menu button is already visible next to it. */
@media (max-width: 359.98px) {
  .brand__name { display: none; }
}
/* Header CTA: dead still until you're on it, then it drifts. The drift is driven
   from site.js, not from keyframes here — CSS animations always retrace the same
   path, and this one is meant to wander differently every time. JS owns
   `transform` on this button; CSS only handles the colour fill and the ring.
   `transform` is deliberately NOT in the transition list below, so it can't
   fight the script for control of the same property. */
.nav__inner .btn {
  transition: background .3s cubic-bezier(.2, .7, .2, 1),
              border-color .3s ease, color .3s ease,
              box-shadow .3s cubic-bezier(.2, .7, .2, 1);
}
.nav__inner .btn:hover,
.nav__inner .btn:focus-visible {
  /* `transform: none` is the resting value the script animates back toward, and
     the thing that suppresses the global .btn:hover lift. The colour swap
     itself (background/text/border) still comes from the base .btn:hover
     rule — only the ring is overridden here, to an ink tone rather than the
     green every other button glows with, since this one is already dark. */
  transform: none;
  box-shadow: 0 0 0 5px rgba(17, 17, 17, .13);
}
.nav__inner .btn:active {
  transform: none;
  box-shadow: 0 0 0 3px rgba(17, 17, 17, .2);
}

.nav__links { display: flex; align-items: center; gap: clamp(.9rem, 1.8vw, 2rem); }
.nav__link {
  font-family: var(--display); font-weight: 600; font-size: var(--t-sm);
  display: inline-flex; align-items: center; min-height: 44px;
  position: relative;
}
/* The underline DRAWS in from the left and, on leaving, keeps travelling
   the same way out to the right rather than retracting back to where it came
   from. One extra line of CSS, and it's the difference between a rule that
   was drawn and one that was scaled. The current page's link keeps it
   permanently, so origin only ever matters on the hover transition. */
.nav__link::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 8px; height: 1.5px;
  background: var(--blue); transform: scaleX(0); transform-origin: right;
  transition: transform .34s var(--ease-entry);
}
.nav__link:hover::after, .nav__link:focus-visible::after,
.nav__link[aria-current="page"]::after { transform: scaleX(1); transform-origin: left; }
.nav__link[aria-current="page"] { color: var(--blue-ink); }
.nav__cta { flex: none; }
.nav__burger {
  display: none; width: 46px; height: 46px; flex: none;
  align-items: center; justify-content: center;
  background: none; border: 1.5px solid var(--ink); border-radius: 999px; cursor: pointer;
  padding: 0;                                  /* kill the UA button padding */
  -webkit-tap-highlight-color: transparent;    /* no grey flash on tap */
  touch-action: manipulation;                  /* no 300ms double-tap delay */
  transition: background .2s ease;
}
.nav__burger:hover { background: rgba(17, 17, 17, .06); }
.nav__burger:active { background: rgba(17, 17, 17, .12); }
/* The bars are ONE span styled with two pseudo-elements. `:not(.sr)` is load
   bearing — the button also contains <span class="sr">Menu</span> for screen
   readers, and without it that label gets styled as a second set of bars. */
.nav__burger span:not(.sr) {
  display: block; position: relative;
  width: 18px; height: 2px; border-radius: 2px;
  background: var(--ink);
  transition: background .18s ease;
}
.nav__burger span:not(.sr)::before,
.nav__burger span:not(.sr)::after {
  content: ""; position: absolute; left: 0;
  width: 18px; height: 2px; border-radius: 2px;
  background: var(--ink);
  transition: transform .3s cubic-bezier(.2, .7, .2, 1);
}
.nav__burger span:not(.sr)::before { top: -6px; }
.nav__burger span:not(.sr)::after  { top:  6px; }
/* open — collapse to a clean X */
.nav__burger[aria-expanded="true"] span:not(.sr) { background: transparent; }
.nav__burger[aria-expanded="true"] span:not(.sr)::before { transform: translateY(6px) rotate(45deg); }
.nav__burger[aria-expanded="true"] span:not(.sr)::after  { transform: translateY(-6px) rotate(-45deg); }

/* ---- NAV BREAKPOINT ----------------------------------------------------
   768px is the one number that decides how the menu behaves:
     above  -> inline word links + Get Started button (desktop / laptop)
     at or below -> hamburger + floating glass panel (tablet / phone)
   Change it here, in the min-width query just below, AND in the resize
   handler in site.js — all three must agree.
   ----------------------------------------------------------------------- */
@media (max-width: 767.98px) {
  .nav__burger { display: inline-flex; }
  /* the mobile menu is its own floating glass panel, hung under the bar */
  .nav__links {
    position: absolute; top: calc(100% + var(--nav-gap)); left: 0; right: 0;
    flex-direction: column; align-items: stretch; gap: 0;
    padding: clamp(.5rem, 1.4vw, .9rem) clamp(1rem, 2.4vw, 1.4rem);
    border-radius: var(--nav-radius-panel);
    /* deliberately darker/more opaque than the bar — see --nav-glass-panel-bg */
    background: var(--nav-glass-panel-bg);
    backdrop-filter: var(--nav-glass-blur);
    -webkit-backdrop-filter: var(--nav-glass-blur);
    border: 1px solid var(--nav-glass-border);
    box-shadow: 0 1px 2px rgba(17,17,17,.06), 0 12px 32px rgba(17,17,17,.16);
    transform: translateY(-8px); opacity: 0; pointer-events: none;
    /* visibility keeps the closed menu out of the tab order and a11y tree */
    visibility: hidden;
    transition: opacity .22s ease, transform .22s ease, visibility .22s ease;
    max-height: calc(100dvh - var(--nav-h) - var(--nav-gap) * 3); overflow-y: auto;
  }
  @supports not ((backdrop-filter: blur(4px)) or (-webkit-backdrop-filter: blur(4px))) {
    .nav__links { background: var(--nav-glass-panel-fallback); }
  }
  .nav__links[data-open="true"] {
    transform: none; opacity: 1; pointer-events: auto; visibility: visible;
  }
  .nav__link { min-height: 56px; font-size: 1.1rem; border-bottom: 1px solid var(--ink-08); }
  /* the item directly above the Get Started button doesn't need a divider */
  .nav__links li:has(+ .nav__cta--mobile) .nav__link { border-bottom: 0; }
  /* same underline as the desktop menu (::after is defined once, outside this
     query, with the blue fill/scaleX animation) — just re-anchored higher so it
     clears the item's own bottom divider on these taller touch-sized rows */
  .nav__link::after { bottom: 14px; }
  .nav__links .btn { margin-top: var(--s-2); margin-bottom: .35rem; width: 100%; }
  .nav__cta--desktop { display: none; }
}
@media (min-width: 768px) { .nav__cta--mobile { display: none; } }

/* ---------- opening panel ----------
   Full-viewport brand moment at the top of the homepage. Not a splash screen —
   it's a normal section in document flow; scrolling past it is how visitors
   move into the rest of the site, same as scrolling past any hero. The nav
   stays fixed on top of it exactly as it already sits on top of .hero below. */
.intro-panel {
  position: relative;
  isolation: isolate; /* keeps the canvas's stacking scoped to this section */
  /* shorter than a full viewport on purpose — enough to feel like a real
     opening moment without making visitors scroll a full screen's worth to
     reach the actual homepage */
  min-height: 66vh;
  min-height: 66svh; /* accounts for mobile browser chrome; overrides the line above where supported */
  display: flex; align-items: center; justify-content: center;
  overflow: hidden;
  background: var(--paper);
  text-align: center;
  padding-inline: var(--gutter);
}
.intro-panel__canvas {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  display: block;
}
.intro-panel__content {
  position: relative; z-index: 1;
  display: flex; flex-direction: column; align-items: center; gap: var(--s-4);
}
.intro-panel__eyebrow { justify-content: center; }
.intro-panel__title {
  font-size: var(--t-mega);
  line-height: .92;
  letter-spacing: -.045em;
  color: var(--ink);
  max-width: 16ch;
  text-wrap: balance;
  animation: introTitleIn 1.1s cubic-bezier(.2,.7,.2,1) both; /* same curve as .reveal — one motion language */
  animation-delay: .1s;
}
@keyframes introTitleIn {
  from { opacity: 0; transform: scale(.94); letter-spacing: -.01em; }
  to   { opacity: 1; transform: scale(1);   letter-spacing: -.045em; }
}
.intro-panel__scroll {
  position: absolute; z-index: 1; left: 50%; bottom: clamp(1.5rem, 4vh, 3rem);
  transform: translateX(-50%);
  display: flex; flex-direction: column; align-items: center; gap: .4rem;
  font-family: var(--display); font-weight: 600; font-size: var(--t-xs);
  letter-spacing: .16em; text-transform: uppercase; color: var(--ink-55);
  animation: introScrollBob 2.6s ease-in-out infinite, introScrollIn 1s cubic-bezier(.2,.7,.2,1) both;
  animation-delay: 0s, .9s;
}
/* a soft paper-coloured halo behind the label — the weave and the drifting
   smoke blocks both run underneath this indicator, and either can land
   right behind the small uppercase text and blend it away */
.intro-panel__scroll::before {
  content: ""; position: absolute; inset: -10px -18px; z-index: -1;
  background: radial-gradient(closest-side, var(--paper) 60%, transparent 100%);
}
@keyframes introScrollBob {
  0%, 100% { transform: translate(-50%, 0); }
  50%      { transform: translate(-50%, 8px); }
}
@keyframes introScrollIn {
  from { opacity: 0; } to { opacity: 1; }
}

/* ---------- hero ---------- */
.hero { padding-top: var(--s-6); padding-bottom: var(--s-6); overflow: hidden; }
.hero__grid {
  display: grid;
  grid-template-columns: minmax(0, 1.02fr) minmax(0, .98fr);
  gap: var(--s-6) var(--s-5);
  align-items: center;
}
.hero__title {
  font-size: var(--t-display);
  margin-top: var(--s-3);
  letter-spacing: -.045em;
  text-wrap: balance;
}
.hero__title .serif { letter-spacing: -.02em; }
.hero__lead { margin-top: var(--s-4); max-width: 42ch; }
.hero__actions { margin-top: var(--s-5); }
.hero__meta {
  margin-top: var(--s-5); padding-top: var(--s-3);
  border-top: 1px solid var(--ink-14);
  display: flex; flex-wrap: wrap; gap: var(--s-2) var(--s-4);
  font-family: var(--display); font-size: var(--t-xs); font-weight: 600;
  letter-spacing: .12em; text-transform: uppercase; color: var(--ink-55);
}

/* overlapping hero composition — same frame language as
   .about-designer__photo (green resting border, radius, blue hover with a
   soft green glow) so the collage reads as part of the same site rather than
   a one-off. [data-magnetic] gives each piece the same light pointer-follow
   as the CTA buttons; see [data-magnetic] in studio.css/js for the mechanism
   and the transition that eases it back to rest. */
.compose { position: relative; }
.compose__main {
  position: relative; width: 100%; aspect-ratio: 4 / 3.15;
  overflow: hidden; background: var(--paper-2);
  border-radius: 20px;
  border: 1.5px solid var(--about-green-35);
  transition: border-color .3s ease, box-shadow .35s ease;
}
.compose__main:hover { border-color: var(--blue); box-shadow: 0 20px 40px var(--about-green-16); }
.compose__main img { width: 100%; height: 100%; object-fit: cover; }
.compose__float {
  position: absolute; overflow: hidden; background: var(--white);
  border-radius: 16px;
  border: 1.5px solid var(--about-green-35);
  box-shadow: 0 8px 20px rgba(17, 17, 17, .1);
  transition: border-color .3s ease, box-shadow .35s ease;
}
.compose__float:hover { border-color: var(--blue); box-shadow: 0 14px 28px var(--about-green-16); }
.compose__float img { width: 100%; height: 100%; object-fit: cover; }
.compose__float--a {
  width: 42%; aspect-ratio: 1 / 1;
  right: -4%; bottom: -8%;
}
.compose__float--b {
  width: 27%; aspect-ratio: 1 / 1;
  left: -6%; top: -6%; background: var(--white);
  display: grid; place-items: center; padding: 4%;
}
.compose__float--b img { object-fit: contain; }
/* positioning only — the visual treatment (border, colour, type) comes from
   .tag-concept itself, the same "Concept Project" mark used on every work
   card, so the hero collage doesn't invent its own one-off label style.
   `.tag-concept.compose__tag` (not just `.compose__tag`) so this wins over
   .tag-concept's own margin regardless of source order. */
.tag-concept.compose__tag {
  position: absolute; left: var(--s-3); bottom: var(--s-3);
  margin: 0; background: var(--paper); /* backing plate — it sits on a photo, not a card */
}
@media (max-width: 900px) {
  .hero__grid { grid-template-columns: 1fr; }
  .hero__visual { order: 2; }
  .compose__float--a { width: 38%; right: 0; bottom: -6%; }
  .compose__float--b { width: 24%; left: 0; top: -5%; }
  .tag-concept.compose__tag { bottom: auto; top: var(--s-3); left: auto; right: var(--s-3); }
}
@media (max-width: 520px) {
  .compose__float--b { display: none; }
  .compose__float--a { width: 42%; }
}

/* The .work-grid / .work-card card system that used to live here was only
   ever used by work.html, and that page is now the captionless .pf-gallery
   in studio.css. .tag-concept below stays — the case-study pages and the
   homepage all still use it. */
.tag-concept {
  display: inline-block; margin-top: .5rem;
  font-family: var(--display); font-size: var(--t-xs); font-weight: 600;
  letter-spacing: .1em; text-transform: uppercase;
  color: var(--blue-ink); border: 1px solid var(--blue-ink);
  padding: .18rem .5rem;
}

/* ---------- services editorial index ----------
   No canvas texture behind this section any more — the hover states below
   (a background tint, a coloured divider) need to read against plain paper,
   the same way the About page's comparison/process rows do, not compete
   with a weave running underneath them. */
.sec-head--wide { align-items: end; }
.sec-head__aside--wide {
  margin-top: var(--s-3);
  max-width: 68ch; font-size: var(--t-sm); line-height: 1.6;
}

.svc-index { border-top: 1px solid var(--ink-14); }

/* ---------- service row entrance: a sequence, not a fade ----------
   The row itself carries .about-reveal so it gets an .is-in class from the
   shared observer (and so the divider below still knows when to draw), but
   the row's OWN fade/rise/blur is switched off here and the motion is handed
   to its four children instead. The order is the reading order — numeral,
   title, copy, the Explore arrow — each a step behind the last, on top of
   whatever --d the group stagger already gave this row. So four rows enter
   as four short sequences offset from one another rather than four
   identical blocks fading up together, which is what the section did before.

   Three classes deep on purpose: .about-reveal.is-in in studio.css is two,
   and studio.css loads after this file, so two would lose. */
.svc-index .svc-index__row.about-reveal {
  opacity: 1; transform: none; filter: none;
  transition: background-color .55s var(--ease-glide);
}
/* The entrance rides `translate`, NOT `transform`, and that is the whole
   trick that lets this coexist with the hover states. The numeral, title and
   arrow all nudge on `transform` while hovered; if the entrance used the same
   property one would have to win, and the more specific selector (this one)
   would silently kill the hover nudge once the row had arrived. `translate`
   is a separate, independently-animatable property that composes with
   `transform`, so entrance and hover never touch each other. */
.svc-index .svc-index__row.about-reveal > * {
  --rise: 16px;
  opacity: 0; translate: 0 var(--rise);
  transition: opacity var(--dur-2) var(--ease-entry) var(--d, 0ms),
              translate var(--dur-2) var(--ease-entry) var(--d, 0ms);
}
.svc-index .svc-index__row.about-reveal.is-in > * { opacity: 1; translate: none; }
/* The numeral leads, then the title, then the description block, then the
   Explore arrow — 110ms apart, slow enough to read as a sequence rather than
   a blur, and the title travels furthest because it is the thing being
   announced.

   That extra distance is set through --rise rather than by re-declaring
   `translate` here, and the reason is worth recording: a per-child override
   of the property itself is MORE specific than the rules below that undo the
   motion for reduced-motion and no-JS readers, so it beat them and left the
   title sitting permanently 24px low for exactly the people who had opted
   out of the animation. Varying an inherited custom property keeps the
   property itself declared in one place, where the resets can reach it. */
.svc-index .svc-index__row.about-reveal > .svc-index__title {
  --rise: 24px;
  transition-delay: calc(var(--d, 0ms) + 110ms);
}
.svc-index .svc-index__row.about-reveal > .svc-index__copy { transition-delay: calc(var(--d, 0ms) + 220ms); }
.svc-index .svc-index__row.about-reveal > .svc-index__go   { transition-delay: calc(var(--d, 0ms) + 330ms); }
.no-js .svc-index__row.about-reveal > * { opacity: 1; translate: none; }
@media (prefers-reduced-motion: reduce) {
  .svc-index .svc-index__row.about-reveal > * { opacity: 1; translate: none; }
}

.svc-index__row {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 3rem) minmax(0, 1.15fr) minmax(0, 1.4fr) auto;
  gap: var(--s-3) var(--s-5);
  align-items: center;
  padding-block: var(--s-6);
  padding-inline: var(--s-3);
  margin-inline: calc(var(--s-3) * -1);
  border-radius: 20px;
  overflow: hidden;
  color: inherit; text-decoration: none;
  transition: background-color .55s var(--about-ease);
}
/* muted green wash, not white/glow — and rounded, so the tint reads as a
   deliberate panel rather than a flat colour swap */
.svc-index__row:hover,
.svc-index__row:focus-visible,
.svc-index__row.is-active { background: var(--about-green-08); }
/* the divider: drawn left-to-right on entrance (transform), then recoloured
   grey->green on hover (background-color) — one element handles both, since
   both are declared on the same base transition and only their trigger
   differs (.is-in vs :hover). */
.svc-index__row::after {
  content: ""; position: absolute; left: var(--s-3); right: var(--s-3); bottom: 0; height: 1px;
  background: var(--ink-14);
  transform-origin: left; transform: scaleX(0);
  transition: transform 1.1s var(--about-ease-out) var(--d, 0ms), background-color .6s ease;
  pointer-events: none;
}
.svc-index__row.is-in::after { transform: scaleX(1); }
.svc-index__row:hover::after,
.svc-index__row:focus-visible::after,
.svc-index__row.is-active::after { background-color: var(--blue); }
.svc-index__num {
  font-family: var(--display); font-weight: 600; font-size: var(--t-xs);
  letter-spacing: .1em; color: var(--ink-55);
  transition: transform .5s var(--about-ease);
}
.svc-index__title {
  font-family: var(--display); font-weight: 700;
  font-size: clamp(2rem, 1.5rem + 2.4vw, 3.4rem);
  letter-spacing: -.03em; line-height: 1;
  transition: transform .5s var(--about-ease);
}
.svc-index__row:hover .svc-index__num, .svc-index__row:focus-visible .svc-index__num, .svc-index__row.is-active .svc-index__num,
.svc-index__row:hover .svc-index__title, .svc-index__row:focus-visible .svc-index__title, .svc-index__row.is-active .svc-index__title { transform: translateX(7px); }
.svc-index__copy { display: grid; gap: var(--s-2); position: relative; z-index: 1; }
.svc-index__desc { color: var(--ink-70); font-size: var(--t-sm); max-width: 42ch; }
/* the included-services list stays quiet until the row is worked, then
   settles in — "gently reveal", not always-on clutter */
.svc-index__list {
  font-family: var(--display); font-weight: 600; font-size: var(--t-xs);
  letter-spacing: .08em; text-transform: uppercase; color: var(--ink-55);
}
/* The reveal-on-hover is gated on viewport width as well as pointer type. It
   was previously pointer-only, which had a subtle cost: the list still
   occupies its space at opacity:0, so on a narrow screen with a mouse (a
   half-width desktop window) every row carried a band of reserved blank space
   above its Explore link with nothing ever appearing in it. Below the stacking
   breakpoint the list is simply always visible — which is what a touch device
   needed regardless, and it fills the space the row has already reserved. */
@media (hover: hover) and (pointer: fine) and (min-width: 561px) {
  .svc-index__list {
    opacity: 0; transform: translateY(5px);
    transition: opacity .55s var(--about-ease), transform .55s var(--about-ease);
  }
  .svc-index__row:hover .svc-index__list,
  .svc-index__row:focus-visible .svc-index__list { opacity: 1; transform: translateY(0); }
}
.svc-index__go {
  position: relative; z-index: 1;
  display: inline-flex; align-items: center; gap: .6em; justify-self: end;
  font-family: var(--display); font-weight: 600; font-size: var(--t-sm);
  letter-spacing: .04em; white-space: nowrap;
}
.svc-index__arrow { transition: transform .5s var(--about-ease); }
.svc-index__row:hover .svc-index__arrow,
.svc-index__row:focus-visible .svc-index__arrow,
.svc-index__row.is-active .svc-index__arrow { transform: translate(5px, -3px); }
@media (max-width: 900px) {
  .svc-index__row {
    grid-template-columns: minmax(0, 2.4rem) 1fr;
    row-gap: var(--s-3);
    padding-block: var(--s-5);
  }
  .svc-index__num { grid-column: 1; grid-row: 1; }
  .svc-index__title { grid-column: 2; grid-row: 1; font-size: clamp(1.7rem, 1.3rem + 3vw, 2.6rem); }
  .svc-index__copy { grid-column: 1 / -1; grid-row: 2; }
  .svc-index__go { grid-column: 1 / -1; grid-row: 3; justify-self: start; }
}
@media (prefers-reduced-motion: reduce) {
  .svc-index__row::after { transition: background-color .3s ease; transform: scaleX(1); }
}

/* ---------- section numbering (shared) ----------
   One numeral treatment used everywhere a section counts its own parts —
   services overview, process, reasons. Previously process used a bold
   stroke-outline numeral while services used this serif-italic one; two
   different "systems" for the same job. Standardised on this one. */
.num {
  font-family: var(--serif); font-style: italic;
  font-size: clamp(2rem, 1.4rem + 2.2vw, 3.4rem); line-height: .85;
  color: var(--blue);
}

/* ---------- process ---------- */
.process { border-top: 1px solid var(--ink-14); }
.process__row {
  display: grid; grid-template-columns: minmax(0, 5.5rem) minmax(0, 15ch) minmax(0, 1fr);
  gap: var(--s-3) var(--s-5); align-items: baseline;
  padding-block: var(--s-4);
  border-bottom: 1px solid var(--ink-14);
  transition: background .3s ease, border-color .3s ease;
}
/* hover (and its touch equivalent, .is-active, toggled briefly by site.js as a
   row scrolls into view on devices without real hover) — soft green glow on
   the number, the divider brightens, the title nudges right. Shared by the
   homepage's process section too, since it's the same component. */
.process__row:hover,
.process__row.is-active { background: var(--white); border-bottom-color: var(--blue); }
.process__row .num { transition: text-shadow .3s ease; }
.process__row:hover .num,
.process__row.is-active .num { text-shadow: 0 0 20px rgba(75, 93, 52, .35); }
.process__name { display: inline-block; transition: transform .3s cubic-bezier(.2,.7,.2,1); }
.process__row:hover .process__name,
.process__row.is-active .process__name { transform: translateX(6px); }
.process__body { color: var(--ink-70); font-size: var(--t-sm); max-width: 52ch; }
@media (max-width: 700px) {
  .process__row { grid-template-columns: 3.5rem 1fr; }
  .process__body { grid-column: 1 / -1; }
}

/* ---------- reasons ---------- */
.reasons { border-top: 1px solid var(--ink-14); }
.reasons__row {
  display: grid; grid-template-columns: minmax(0, 5.5rem) minmax(0, 1fr);
  gap: var(--s-3) var(--s-5); align-items: baseline;
  padding-block: var(--s-4);
  border-bottom: 1px solid var(--ink-14);
  transition: background .3s ease;
}
.reasons__row:hover { background: var(--white); }
.reasons__name { font-size: var(--t-h3); }
.reasons__body { color: var(--ink-70); font-size: var(--t-sm); max-width: 52ch; margin-top: .35rem; }
@media (max-width: 700px) {
  .reasons__row { grid-template-columns: 3.5rem 1fr; }
}

/* ---------- evidence ---------- */
.evidence__grid {
  display: grid;
  grid-template-columns: minmax(0, .8fr) minmax(0, 1.2fr);
  gap: var(--s-6);
  align-items: start;
}
/* sticky only as long as the right column (three point rows) runs taller
   than the card — position:sticky naturally releases at the bottom of this
   grid row on its own, no scroll listener needed */
.evidence__stat-wrap { position: sticky; top: calc(var(--nav-space) + var(--s-3)); }
.evidence__stat-card {
  position: relative;
  aspect-ratio: 1 / 1; max-width: 360px; margin-inline: auto;
  border-radius: 28px;
  background: var(--paper-2);
  border: 1px solid var(--ink-08);
  display: grid; place-items: center; text-align: center;
  padding: var(--s-5);
  transition: background-color .5s var(--about-ease), border-color .5s var(--about-ease);
}
.evidence__stat-card:hover { background: var(--about-green-08); border-color: var(--blue); }
.evidence__ring { position: absolute; inset: 0; width: 100%; height: 100%; transform: rotate(-90deg); }
.evidence__ring-track { stroke: var(--ink-08); }
/* The arc is the slowest single move on the page — 2.2s — because it is
   drawing a measurement, and a number that arrives instantly reads as
   decoration while one that takes its time reads as evidence. pathLength=100
   on the circle makes the dash units percentage points exactly, so this stops
   at 46.1 and not at "roughly there". */
.evidence__ring-fill {
  stroke: url(#evidenceRingGrad);
  stroke-dashoffset: var(--ring-offset, 100);
  transition: stroke-dashoffset 2.2s var(--ease-entry);
}
@media (prefers-reduced-motion: reduce) { .evidence__ring-fill { transition: none; } }
.evidence__stat-value {
  position: relative; z-index: 1;
  font-family: var(--display); font-weight: 700;
  font-size: clamp(2.6rem, 2rem + 3vw, 4rem);
  letter-spacing: -.03em; color: var(--ink); line-height: 1;
}
/* the figure rises out of a clipping mask a beat AFTER the arc starts
   drawing, so the sequence reads arc-then-number — the measurement, then its
   value — rather than both landing at once */
.evidence__stat-value .about-mask__inner { transition-delay: .32s; }
/* class, not a bare `span`: the figure now sits inside two wrapper spans for
   the clipping mask, and a bare descendant selector shrank those too */
.evidence__stat-pct { font-size: .5em; vertical-align: .35em; }
.evidence__stat-label {
  position: relative; z-index: 1;
  margin-top: var(--s-2); max-width: 19ch; margin-inline: auto;
  font-family: var(--display); font-weight: 600; font-size: var(--t-xs);
  letter-spacing: .06em; text-transform: uppercase; color: var(--ink-55);
  /* and the caption follows the figure, on the same reveal the rest of the
     page uses, so the card is a three-step sequence of its own */
  opacity: 0; translate: 0 10px;
  transition: opacity var(--dur-2) var(--ease-entry) .78s, translate var(--dur-2) var(--ease-entry) .78s;
}
.evidence__stat-card.is-in .evidence__stat-label { opacity: 1; translate: none; }
.no-js .evidence__stat-label { opacity: 1; translate: none; }
@media (prefers-reduced-motion: reduce) {
  .evidence__stat-label { opacity: 1; translate: none; }
}

.evidence__title { font-size: var(--t-h2); max-width: 20ch; }

.evidence__points { margin-top: var(--s-6); border-top: 1px solid var(--ink-14); }
/* title stacked above its description rather than a fixed side-by-side
   column — a title-width column sized in `ch` for a bold display font either
   wraps unpredictably or, sized wide enough not to, leaves the description
   column cramped; stacking sidesteps that regardless of how long any one
   title runs. */
.evidence__point {
  padding-block: var(--s-5);
  padding-inline: var(--s-3);
  margin-inline: calc(var(--s-3) * -1);
  border-radius: 20px;
  border-bottom: 1px solid var(--ink-14);
  display: grid; gap: var(--s-2);
  transition: background-color .5s var(--about-ease);
}
.evidence__point:hover { background: var(--about-green-08); }
.evidence__point-name { font-family: var(--display); font-weight: 700; font-size: var(--t-h3); letter-spacing: -.02em; }
.evidence__point-body { color: var(--ink-70); font-size: var(--t-sm); max-width: 52ch; }
@media (max-width: 900px) {
  .evidence__grid { grid-template-columns: 1fr; }
  .evidence__stat-wrap { position: static; margin-bottom: var(--s-5); }
}

/* ---------- contact + studio info ----------
   Homepage-only, sitting between the closing dark CTA and the footer. Stays
   on the page's own paper surface rather than going dark a second time —
   the CTA above already made the "this page is ending" statement in black;
   this one's job is purely practical (the actual way to reach the studio),
   so it reads as reference information, not a second big finish. Same
   ink/green/paper palette as every other section, same reveal language
   (mask heading, drawn lines, staggered rows) as the rest of the site —
   only the layout (two columns) marks it as slightly more considered than
   a plain paragraph would be. */
.contact-cta { padding-block: var(--s-7); border-top: 1px solid var(--ink-14); }
.contact-cta__grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, .9fr);
  gap: var(--s-6) var(--s-7);
  align-items: start;
}
.contact-cta__title { margin-top: var(--s-2); max-width: 14ch; }
.contact-cta__body { margin-top: var(--s-4); max-width: 42ch; color: var(--ink-70); font-size: var(--t-lead); text-wrap: pretty; }

/* the primary action reads as a link, not another pill button — the page
   already has several of those, and one large editorial line closes the
   section the same way .tlink closes a paragraph elsewhere on the site.
   Two background layers: a faint permanent hairline at rest, a green one
   that draws over it on hover — identical mechanism to .tlink, just set in
   the larger display type this section's heading already established. */
.contact-cta__link {
  margin-top: var(--s-5);
  display: inline-flex; align-items: center; gap: .6em;
  font-family: var(--display); font-weight: 700;
  font-size: clamp(1.2rem, 1.02rem + .9vw, 1.55rem);
  color: var(--ink);
  background-image: linear-gradient(var(--blue), var(--blue)), linear-gradient(var(--ink-14), var(--ink-14));
  background-size: 0% 2px, 100% 1px;
  background-repeat: no-repeat;
  background-position: 0 100%, 0 100%;
  padding-bottom: .2em;
  transition: background-size var(--dur-1) var(--ease-entry), color .2s var(--ease-glide);
}
.contact-cta__link:hover, .contact-cta__link:focus-visible {
  color: var(--blue-ink);
  background-size: 100% 2px, 100% 1px;
}
.contact-cta__link-arrow { transition: transform var(--dur-1) var(--ease-entry); }
.contact-cta__link:hover .contact-cta__link-arrow,
.contact-cta__link:focus-visible .contact-cta__link-arrow { transform: translate(4px, -4px); }

/* ---------- studio info table ----------
   Same visual language as .evidence__point: an ink hairline that draws in
   (via .line-host, applied in site.js), a rounded green tint on hover, dt/dd
   pairs rather than a definition list styled like prose. */
.contact-cta__facts { border-top: 1px solid transparent; }
.contact-cta__fact {
  position: relative;
  display: grid; grid-template-columns: minmax(0, 11ch) minmax(0, 1fr);
  gap: var(--s-3);
  padding-block: var(--s-4);
  padding-inline: var(--s-3);
  margin-inline: calc(var(--s-3) * -1);
  border-radius: 20px;
  border-bottom: 1px solid transparent;
  transition: background-color .5s var(--ease-glide);
}
.contact-cta__fact:hover { background: var(--about-green-08); }
.contact-cta__fact dt {
  font-family: var(--display); font-weight: 600; font-size: var(--t-xs);
  letter-spacing: .08em; text-transform: uppercase; color: var(--ink-55);
}
.contact-cta__fact dd {
  margin: 0; font-family: var(--display); font-weight: 600; font-size: var(--t-sm);
  color: var(--ink);
}
.contact-cta__fact dd a { text-decoration: underline; text-underline-offset: 3px; text-decoration-thickness: 1px; }
.contact-cta__fact dd a:hover, .contact-cta__fact dd a:focus-visible { color: var(--blue-ink); }
/* The email and Instagram rows are the two things a visitor on a phone is most
   likely to actually tap, and as inline text they measured 16.7px tall — well
   under half a fingertip. Given a real tap box only where there is no precise
   cursor, so the desktop table keeps its exact existing rhythm. */
@media (hover: none), (pointer: coarse) {
  .contact-cta__fact dd a { display: inline-flex; align-items: center; min-height: 44px; }
}
@media (max-width: 900px) {
  .contact-cta__grid { grid-template-columns: 1fr; }
  .contact-cta__facts { margin-top: var(--s-2); }
  /* label above value, and the vertical padding comes down: stacked rows are
     twice as tall as the two-column version, so keeping the desktop padding
     turned this table into a very long scroll on a phone */
  .contact-cta__fact { grid-template-columns: 1fr; gap: .2rem; padding-block: var(--s-3); }
  /* the 44px link box already provides the row's lower breathing room */
  .contact-cta__fact:has(dd a) { padding-bottom: var(--s-2); }
}

/* ---------- about ---------- */
.about__grid {
  display: grid; grid-template-columns: minmax(0, 1.15fr) minmax(0, .85fr);
  gap: var(--s-6) var(--s-5); align-items: start;
}
.about__title { font-size: var(--t-h2); max-width: 26ch; }
.about__copy { margin-top: var(--s-4); display: grid; gap: var(--s-3); max-width: 52ch; color: var(--ink-70); }
.about__facts { border-top: 1px solid var(--ink-14); }
.about__fact {
  display: flex; justify-content: space-between; gap: var(--s-3);
  padding-block: var(--s-3); border-bottom: 1px solid var(--ink-14);
  font-family: var(--display); font-size: var(--t-sm);
}
.about__fact dt { color: var(--ink-55); letter-spacing: .1em; text-transform: uppercase; font-size: var(--t-xs); font-weight: 600; }
.about__fact dd { margin: 0; font-weight: 600; text-align: right; }
.about__fact dd a {
  display: inline-flex; align-items: center; min-height: 44px;
  text-decoration: underline; text-underline-offset: 3px; text-decoration-thickness: 1px;
}
.about__fact dd a:hover { color: var(--blue-ink); }
@media (max-width: 900px) { .about__grid { grid-template-columns: 1fr; } }

/* ---------- blue CTA ---------- */
.cta {
  background: var(--blue); color: #fff;
  padding-block: var(--s-7);
}
.cta__grid {
  display: grid; grid-template-columns: minmax(0, 1fr) auto;
  gap: var(--s-5); align-items: end;
}
.cta__title {
  font-size: var(--t-h2); max-width: 20ch; margin-top: var(--s-3);
  color: #fff;
}
.cta__body { margin-top: var(--s-3); max-width: 44ch; color: rgba(255,255,255,.95); }
@media (max-width: 800px) { .cta__grid { grid-template-columns: 1fr; align-items: start; } }

/* outlined button for use on the green/blue CTA background — same hover
   behaviour as .btn--onblue (fills solid white, ink text), just a different
   resting state, so the interaction language matches exactly */
.btn--ghost-onblue {
  --btn-bg: transparent; --btn-fg: #fff; --btn-bd: rgba(255, 255, 255, .55);
  --btn-bg-hover: #fff; --btn-fg-hover: var(--ink); --btn-bd-hover: #fff;
}

/* ---------- footer ---------- */
.footer { background: var(--night); color: rgba(255,255,255,.72); padding-block: var(--s-6) var(--s-4); }
.footer__grid {
  display: grid; grid-template-columns: minmax(0, 1.4fr) repeat(2, minmax(0, 1fr));
  gap: var(--s-5);
  padding-bottom: var(--s-5); border-bottom: 1px solid rgba(255,255,255,.16);
}
.footer__brand { display: flex; align-items: center; gap: .6rem; }
.footer__brand img { width: 40px; height: 40px; }
.footer__brand span { font-family: var(--display); font-weight: 700; font-size: 1.1rem; color: #fff; letter-spacing: -.02em; }
.footer__tag { margin-top: var(--s-3); max-width: 34ch; font-size: var(--t-sm); }
.footer h2 {
  font-family: var(--display); font-size: var(--t-xs); font-weight: 600;
  letter-spacing: .16em; text-transform: uppercase; color: rgba(255,255,255,.5);
  margin-bottom: var(--s-3);
}
.footer__list { display: grid; gap: .1rem; }
.footer__list a {
  display: inline-flex; align-items: center; min-height: 44px;
  font-family: var(--display); font-weight: 500; font-size: var(--t-sm); color: rgba(255,255,255,.82);
  transition: color .2s ease;
}
.footer__list a:hover { color: #fff; }
.footer__list a:hover { text-decoration: underline; text-underline-offset: 4px; }
.footer__bottom {
  padding-top: var(--s-4);
  display: flex; flex-wrap: wrap; gap: var(--s-2) var(--s-4); justify-content: space-between;
  font-size: var(--t-xs); color: rgba(255,255,255,.5);
}
@media (max-width: 760px) { .footer__grid { grid-template-columns: 1fr; gap: var(--s-4); } }

/* ---------- page header (inner pages) ---------- */
.phead { padding-top: var(--s-6); padding-bottom: var(--s-5); }
.phead__title { font-size: var(--t-display); letter-spacing: -.045em; margin-top: var(--s-3); max-width: 20ch; }
.phead__lead { margin-top: var(--s-4); }

/* ---------- services page ---------- */
.cat-nav {
  position: sticky; top: var(--nav-space); z-index: 50;
  background: var(--paper); border-block: 1px solid var(--ink-14);
}
.cat-nav__inner {
  max-width: var(--maxw); margin-inline: auto; padding-inline: var(--gutter);
  display: flex; gap: var(--s-3); overflow-x: auto; scrollbar-width: none;
}
.cat-nav__inner::-webkit-scrollbar { display: none; }
.cat-nav a {
  flex: none; display: inline-flex; align-items: center; min-height: 52px;
  font-family: var(--display); font-weight: 600; font-size: var(--t-sm);
  color: var(--ink-55); border-bottom: 2px solid transparent;
  transition: color .2s, border-color .2s;
}
.cat-nav a:hover, .cat-nav a.is-active { color: var(--ink); border-bottom-color: var(--blue); }

.cat { padding-block: var(--s-6); border-bottom: 1px solid var(--ink-14); scroll-margin-top: calc(var(--nav-space) + 64px); }
.cat__head { display: grid; grid-template-columns: minmax(0,1fr) minmax(0,1.1fr); gap: var(--s-4); align-items: end; margin-bottom: var(--s-5); }
.cat__title { font-size: var(--t-h2); }
@media (max-width: 800px) { .cat__head { grid-template-columns: 1fr; } }

.svc-list { display: grid; gap: var(--s-3); }
.svc {
  background: var(--white); border: 1px solid var(--ink-14);
}
.svc > summary {
  list-style: none; cursor: pointer;
  display: grid; grid-template-columns: minmax(0,1fr) auto auto;
  gap: var(--s-3) var(--s-4); align-items: center;
  padding: var(--s-4);
  min-height: 64px;
}
.svc > summary::-webkit-details-marker { display: none; }
.svc__name { font-family: var(--display); font-weight: 700; font-size: var(--t-h3); letter-spacing: -.03em; }
.svc__desc { color: var(--ink-70); font-size: var(--t-sm); margin-top: .35rem; max-width: 52ch; }
.svc__price { font-family: var(--display); font-weight: 700; font-size: clamp(1.15rem,1rem+.55vw,1.5rem); white-space: nowrap; }
.svc__price small { display: block; font-size: var(--t-xs); font-weight: 600; letter-spacing: .1em; text-transform: uppercase; color: var(--ink-55); }
.svc__chev {
  width: 40px; height: 40px; flex: none; border: 1.5px solid var(--ink); border-radius: 999px;
  display: grid; place-items: center; transition: transform .3s ease, background .2s, color .2s;
}
.svc[open] .svc__chev { transform: rotate(45deg); background: var(--blue); border-color: var(--blue); color: #fff; }
.svc__panel { padding: 0 var(--s-4) var(--s-4); }
.svc__panel-inner {
  border-top: 1px solid var(--ink-14); padding-top: var(--s-4);
  display: grid; grid-template-columns: minmax(0,1fr) auto; gap: var(--s-4); align-items: end;
}
.svc__incl { display: grid; gap: .5rem; }
.svc__incl h3 {
  font-family: var(--display); font-size: var(--t-xs); font-weight: 600;
  letter-spacing: .14em; text-transform: uppercase; color: var(--ink-55);
}
.svc__incl li { position: relative; padding-left: 1.4rem; font-size: var(--t-sm); color: var(--ink-70); }
.svc__incl li::before {
  content: ""; position: absolute; left: 0; top: .62em;
  width: 8px; height: 8px; background: var(--blue);
}
@media (max-width: 700px) {
  .svc > summary { grid-template-columns: minmax(0,1fr) auto; }
  .svc__price { grid-column: 1; text-align: left; }
  .svc__chev { grid-row: 1; grid-column: 2; }
  .svc__panel-inner { grid-template-columns: 1fr; align-items: start; }
}

.note {
  background: var(--paper-2); border-left: 3px solid var(--blue);
  padding: var(--s-4); font-size: var(--t-sm); color: var(--ink-70);
  max-width: 70ch;
}

/* ---------- case study ---------- */
.cs__meta {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 170px), 1fr));
  gap: var(--s-4);
  border-top: 1px solid var(--ink-14); border-bottom: 1px solid var(--ink-14);
  padding-block: var(--s-4); margin-block: var(--s-5);
}
.cs__meta dt {
  font-family: var(--display); font-size: var(--t-xs); font-weight: 600;
  letter-spacing: .14em; text-transform: uppercase; color: var(--ink-55); margin-bottom: .5rem;
}
.cs__meta dd { margin: 0; font-family: var(--display); font-weight: 600; font-size: var(--t-sm); }
.cs__cols { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 30ch), 1fr)); gap: var(--s-5); }
.cs__cols h2 { font-size: var(--t-h3); margin-bottom: var(--s-3); }
.cs__cols p { color: var(--ink-70); }

.gallery { display: grid; grid-template-columns: repeat(12, 1fr); gap: var(--s-4); align-items: start; }
.gitem { grid-column: span 6; }
.gitem--full { grid-column: span 12; }
.gitem--third { grid-column: span 4; }
.gitem--two-thirds { grid-column: span 8; }
.gitem figure { margin: 0; }
.gitem__media { overflow: hidden; background: var(--paper-2); aspect-ratio: var(--ar, 4 / 3); }
.gitem__media img { width: 100%; height: 100%; object-fit: cover; }
.gitem__media--contain { background: var(--white); border: 1px solid var(--ink-14); }
.gitem__media--contain img { object-fit: contain; padding: 8%; }
.gitem figcaption {
  margin-top: var(--s-2); font-family: var(--display); font-size: var(--t-xs);
  font-weight: 600; letter-spacing: .1em; text-transform: uppercase; color: var(--ink-55);
}
@media (max-width: 820px) {
  .gitem, .gitem--third, .gitem--two-thirds { grid-column: span 6; }
}
@media (max-width: 560px) {
  .gitem, .gitem--third, .gitem--two-thirds, .gitem--full { grid-column: span 12; }
}

.nextproj {
  display: flex; flex-wrap: wrap; justify-content: space-between; gap: var(--s-3);
  align-items: center; padding-block: var(--s-5);
  border-top: 1px solid var(--ink-14);
}

/* ---------- filter bar (work page) ---------- */
.filters { display: flex; flex-wrap: wrap; gap: .5rem; margin-bottom: var(--s-5); }
.filters button {
  min-height: 44px; padding: .55rem 1.1rem;
  background: transparent; border: 1.5px solid var(--ink-14); border-radius: 999px;
  font-family: var(--display); font-weight: 600; font-size: var(--t-sm); cursor: pointer;
  transition: background .2s, color .2s, border-color .2s;
}
.filters button:hover { border-color: var(--ink); }
.filters button[aria-pressed="true"] { background: var(--ink); border-color: var(--ink); color: #fff; }

/* ---------- before / after ---------- */
.ba { display: grid; grid-template-columns: 1fr 1fr; gap: var(--s-2); }
.ba figure { margin: 0; position: relative; }
.ba__media { overflow: hidden; aspect-ratio: 3 / 2; background: var(--paper-2); }
.ba__media img { width: 100%; height: 100%; object-fit: cover; }
.ba__label {
  position: absolute; left: 0; top: 0;
  background: var(--ink); color: #fff;
  font-family: var(--display); font-size: var(--t-xs); font-weight: 600;
  letter-spacing: .12em; text-transform: uppercase; padding: .35rem .6rem;
}
.ba figure:last-child .ba__label { background: var(--blue); }

/* ---------- form ---------- */
.form-shell {
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 330px);
  gap: var(--s-6);
  align-items: start;
}
@media (max-width: 960px) { .form-shell { grid-template-columns: 1fr; } }

.fset { border: 0; padding: 0; margin: 0 0 var(--s-5); }
.fset > legend {
  font-family: var(--display); font-size: var(--t-xs); font-weight: 600;
  letter-spacing: .16em; text-transform: uppercase; color: var(--ink-55);
  padding: 0 0 var(--s-3); width: 100%; border-bottom: 1px solid var(--ink-14); margin-bottom: var(--s-4);
}
.fgrid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--s-3) var(--s-4); }
.fgrid--1 { grid-template-columns: 1fr; }
.f-full { grid-column: 1 / -1; }
@media (max-width: 640px) { .fgrid { grid-template-columns: 1fr; } }

.field { display: grid; gap: .45rem; }
.field > label {
  font-family: var(--display); font-size: var(--t-sm); font-weight: 600;
}
.field > label .opt { color: var(--ink-55); font-weight: 500; }
.field .hint { font-size: var(--t-xs); color: var(--ink-55); }
.field input[type="text"], .field input[type="email"], .field input[type="tel"],
.field input[type="date"], .field input[type="number"], .field select, .field textarea {
  width: 100%; min-height: 50px; padding: .7rem .9rem;
  background: var(--white); color: var(--ink);
  border: 1.5px solid var(--ink-14); border-radius: 0;
  transition: border-color .2s, box-shadow .2s;
  appearance: none;
}
.field textarea { min-height: 148px; resize: vertical; line-height: 1.55; }
.field select {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='9' viewBox='0 0 14 9'%3E%3Cpath d='M1 1l6 6 6-6' fill='none' stroke='%23111' stroke-width='1.8'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 1rem center; padding-right: 2.6rem;
}
.field input:hover, .field select:hover, .field textarea:hover { border-color: rgba(17,17,17,.3); }
.field input:focus, .field select:focus, .field textarea:focus {
  outline: none; border-color: var(--blue); box-shadow: 0 0 0 3px rgba(75,93,52,.20);
}
.field input[aria-invalid="true"], .field textarea[aria-invalid="true"], .field select[aria-invalid="true"] {
  border-color: #C02626; box-shadow: 0 0 0 3px rgba(192,38,38,.14);
}
.field .err { font-size: var(--t-xs); color: #A81F1F; font-weight: 600; min-height: 0; }
.field input[type="file"] {
  width: 100%; padding: .85rem; background: var(--white);
  border: 1.5px dashed var(--ink-14); font-size: var(--t-sm);
}
.field input[type="file"]::file-selector-button {
  margin-right: .8rem; min-height: 38px; padding: .4rem .9rem;
  background: var(--ink); color: #fff; border: 0; border-radius: 999px;
  font-family: var(--display); font-weight: 600; font-size: var(--t-xs); cursor: pointer;
}

.checks { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 210px), 1fr)); gap: .6rem; }
.check {
  display: flex; align-items: center; gap: .7rem;
  min-height: 52px; padding: .6rem .9rem;
  background: var(--white); border: 1.5px solid var(--ink-14); cursor: pointer;
  font-family: var(--display); font-size: var(--t-sm); font-weight: 500;
  transition: border-color .2s, background .2s;
}
.check:hover { border-color: rgba(17,17,17,.35); }
.check input { width: 20px; height: 20px; accent-color: var(--blue); flex: none; margin: 0; }
.check:has(input:checked) { border-color: var(--blue); background: color-mix(in srgb, var(--blue) 7%, var(--white)); }
.check:has(input:focus-visible) { outline: 3px solid var(--blue); outline-offset: 2px; }

.form-actions {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-3);
  padding-top: var(--s-4); border-top: 1px solid var(--ink-14);
}
.form-aside {
  background: var(--white); border: 1px solid var(--ink-14); padding: var(--s-4);
  position: sticky; top: calc(var(--nav-space) + var(--s-3));
}
.form-aside h2 { font-size: var(--t-h3); margin-bottom: var(--s-3); }
.form-aside dl { display: grid; gap: var(--s-3); margin: 0; }
.form-aside dt {
  font-family: var(--display); font-size: var(--t-xs); font-weight: 600;
  letter-spacing: .14em; text-transform: uppercase; color: var(--ink-55);
}
.form-aside dd { margin: .25rem 0 0; font-size: var(--t-sm); }
.form-aside a { text-decoration: underline; text-underline-offset: 3px; text-decoration-thickness: 1px; }
.form-aside a:hover { color: var(--blue-ink); }
/* standalone contact links (not links inside a sentence) get a full tap target */
.form-aside .dd-link { display: inline-flex; align-items: center; min-height: 44px; }

.confirm {
  background: var(--white); border: 1px solid var(--ink-14);
  border-left: 4px solid var(--blue);
  padding: var(--s-5);
}
.confirm__mark {
  width: 54px; height: 54px; border-radius: 999px; background: var(--blue); color: #fff;
  display: grid; place-items: center; margin-bottom: var(--s-3);
}
.confirm h2 { font-size: var(--t-h2); max-width: 22ch; }
.confirm p { margin-top: var(--s-3); color: var(--ink-70); max-width: 52ch; }
.confirm .btn-row { margin-top: var(--s-4); }
[hidden] { display: none !important; }

/* ---------- reveal animation ---------- */
.reveal { opacity: 0; transform: translateY(22px); }
.reveal.is-in {
  opacity: 1; transform: none;
  transition: opacity .7s cubic-bezier(.2,.7,.2,1), transform .7s cubic-bezier(.2,.7,.2,1);
  transition-delay: var(--d, 0ms);
}
.no-js .reveal { opacity: 1; transform: none; }

/* Horizontal divider that draws in on scroll instead of just appearing.
   Rides the existing .reveal + IntersectionObserver plumbing — add this class
   alongside .reveal and it just works, no JS changes. Only the transform
   differs from a normal .reveal (scaleX instead of translateY); the duration
   and easing are the exact same values as everything else on the site. */
.rule--draw.reveal { opacity: 1; transform: scaleX(0); transform-origin: left; }
.rule--draw.reveal.is-in { transform: scaleX(1); }

/* ==========================================================================
   SHARED MOTION PRIMITIVES

   Four reusable behaviours, defined once here because site.css is the only
   stylesheet every page loads. studio.css, about.css and services.css each
   carry their own copy of the .about-reveal family — that duplication is
   pre-existing and not worth unpicking now — but everything NEW below lives
   here only, so there is exactly one definition of each to change.

   Each one is driven by the same .is-in class the existing observers already
   add, so nothing here needs its own script or its own observer.
   ========================================================================== */

/* ---------- 1. page entry and exit ----------
   A paper-coloured sheet the exact shade of the page, faded out on arrival
   and back in on the way to the next page, so navigating between pages reads
   as one continuous surface rather than a white flash and a hard cut.

   It is pointer-events:none for its whole life, and site.js injects it — so
   it never blocks a click even mid-fade, and a reader with JS off never has a
   veil at all. Deliberately short: 420ms out, 260ms in. Any longer and it
   stops being a transition and starts being a loading screen. */
.page-veil {
  position: fixed; inset: 0; z-index: 400;
  background: var(--paper);
  pointer-events: none;
  opacity: 1;
  transition: opacity .42s var(--ease-glide);
}
.page-veil.is-clear { opacity: 0; }
html.is-leaving .page-veil { opacity: 1; transition-duration: .26s; }

/* ---------- 2. any hairline draws itself ----------
   site.css already had .rule--draw for hand-marked dividers, but almost every
   .rule on the site is a plain one and just appeared. site.js now tags them
   all with this class and observes them, so every dividing line on every page
   draws left-to-right as it enters — one gesture, applied consistently,
   instead of being a feature of three sections that remembered to ask. */
.rule--auto {
  transform: scaleX(0); transform-origin: left;
  transition: transform var(--dur-line) var(--ease-glide) var(--d, 0ms);
}
.rule--auto.is-in { transform: scaleX(1); }
.no-js .rule--auto { transform: none; }
@media (prefers-reduced-motion: reduce) { .rule--auto { transform: none; } }

/* ---------- 2b. component hairlines that draw ----------
   Almost every divider on this site is a `border` on a component rather than
   a .rule element, and a border cannot be animated directionally — which is
   why "lines draw themselves" was previously true of the service rows only.
   .line-host hands a component's own hairline to a pseudo-element that draws
   left-to-right on entry instead, and site.js applies it to the ones
   carrying real structural weight. The border stays in the box model and
   just goes transparent, so nothing moves by a pixel when this is applied.

   The inset on the Evidence rows is deliberate: it matches
   .svc-index__row::after exactly, so the two stacked sections read as the
   same ruled grid rather than two different ones. */
.line-host { position: relative; }
.line-host::before {
  content: ""; position: absolute; left: 0; right: 0; height: 1px;
  /* --line-color lets a dark-background host (the closing contact section)
     draw a light line instead of the default ink one, without a second
     copy of this rule */
  background: var(--line-color, var(--ink-14));
  transform: scaleX(0); transform-origin: left;
  transition: transform var(--dur-line) var(--ease-glide) var(--d, 0ms);
  pointer-events: none;
}
.line-host.is-in::before { transform: scaleX(1); }
.svc-index.line-host, .evidence__points.line-host { border-top-color: transparent; }
.svc-index.line-host::before, .evidence__points.line-host::before { top: 0; }
.evidence__point.line-host { border-bottom-color: transparent; }
.evidence__point.line-host::before { bottom: 0; left: var(--s-3); right: var(--s-3); }
.no-js .line-host::before { transform: none; }
@media (prefers-reduced-motion: reduce) { .line-host::before { transform: none; } }

/* ---------- 3. lateral reveal variants ----------
   The brief's "avoid repeating the exact same entrance animation in every
   section" problem, solved at the token level: a second and third direction
   for the shared .about-reveal, so a section can enter from the side where
   that suits its composition (an aside beside a headline, a sticky card
   beside a list) instead of everything on the site rising by 18px.

   Two classes deep, so these beat the single-class .about-reveal base
   wherever it happens to be defined in the load order. */
.about-reveal.about-reveal--left  { transform: translateX(-26px); filter: blur(2px); }
.about-reveal.about-reveal--right { transform: translateX(26px);  filter: blur(2px); }
.about-reveal.about-reveal--left.is-in,
.about-reveal.about-reveal--right.is-in { transform: none; filter: blur(0); }
@media (prefers-reduced-motion: reduce) {
  .about-reveal.about-reveal--left,
  .about-reveal.about-reveal--right { transform: none; filter: none; }
}

/* ---------- 4. masked media reveal ----------
   For photographs and screenshots: the frame wipes open from the bottom edge
   while the image inside settles back from a slight over-scale, so the
   picture is uncovered rather than faded in. The over-scale is what stops the
   wipe reading as a blind coming up — there is real movement behind the mask,
   so the two together read as one camera move.

   `round var(--media-radius)` keeps rounded corners intact through the wipe;
   components that are square just leave the variable at 0. */
.media-reveal {
  --media-radius: 0px;
  clip-path: inset(0 0 100% 0 round var(--media-radius));
  transition: clip-path var(--dur-3) var(--ease-entry) var(--d, 0ms);
}
.media-reveal.is-in { clip-path: inset(0 0 0 0 round var(--media-radius)); }
.media-reveal img { transition: transform 1.35s var(--ease-entry) var(--d, 0ms); }
.media-reveal:not(.is-in) img { transform: scale(1.07); }
.no-js .media-reveal { clip-path: none; }
@media (prefers-reduced-motion: reduce) {
  .media-reveal { clip-path: none; }
  .media-reveal:not(.is-in) img { transform: none; }
}

/* .reveal-handoff marks a wrapper whose entrance has been given to the framed
   image inside it — site.js takes .about-reveal off it and puts this on
   instead, so there is no fade left to override and no specificity fight with
   a stylesheet that loads later. The wrapper still gets .is-in, which is all
   its caption needs: the caption arrives just after the picture it describes,
   rather than with it. */
.reveal-handoff figcaption {
  opacity: 0; translate: 0 8px;
  transition: opacity var(--dur-2) var(--ease-entry) .3s, translate var(--dur-2) var(--ease-entry) .3s;
}
.reveal-handoff.is-in figcaption { opacity: 1; translate: none; }
/* before, then after — the comparison uncovers in the order it reads in */
.ba figure:last-child .media-reveal { --d: 150ms; }
.no-js .reveal-handoff figcaption { opacity: 1; translate: none; }
@media (prefers-reduced-motion: reduce) {
  .reveal-handoff figcaption { opacity: 1; translate: none; }
}

/* ---------- 5. cursor-position depth on media ----------
   site.js writes --tilt-x / --tilt-y on a hovered [data-tilt] host; the
   component decides what moves. Always applied to the IMAGE inside the frame
   and never to the frame itself, which is the whole point: the picture drifts
   a few pixels inside a stationary border, so the card gains depth without
   anything shifting in the layout around it. Capped at a handful of pixels —
   any more and it stops being depth and becomes a gimmick. */
   The per-component transform composition lives in each component's own
   stylesheet (studio.css for .pf-item, casestudy.css for .cs-portfolio) so
   the hover scale and the tilt are declared in ONE rule per component and
   can't end up in a specificity fight across two files. */
[data-tilt] { --tilt-x: 0px; --tilt-y: 0px; --tilt-s: 1; }
@media (prefers-reduced-motion: reduce) {
  [data-tilt] { --tilt-x: 0px; --tilt-y: 0px; }
}

/* ---------- 6. section handoff ----------
   Not an animation, but it is why the scroll starts to feel continuous: the
   homepage's paper -> paper-2 -> paper sequence butted together as two hard
   horizontal edges, and every entrance inside the tinted band was fighting
   that seam. Feathering both of its edges means the sections dissolve into
   one another and the motion inside them is the only thing with a hard start.

   Scoped to this one band rather than the shared .section--neutral utility:
   other pages put tinted sections against dark ones, where a paper-coloured
   fade would be a visible smudge rather than a blend. z-index:-1 plus
   isolation keeps the gradients above the section's own fill and below every
   bit of its content. */
.svc-index-sec { position: relative; isolation: isolate; }
.svc-index-sec::before,
.svc-index-sec::after {
  content: ""; position: absolute; left: 0; right: 0; z-index: -1;
  height: var(--s-5); pointer-events: none;
}
.svc-index-sec::before { top: 0; background: linear-gradient(var(--paper), transparent); }
.svc-index-sec::after { bottom: 0; background: linear-gradient(transparent, var(--paper)); }

/* ==========================================================================
   TOUCH AND SMALL-SCREEN PASS

   Everything below is additive and gated behind either a coarse pointer or a
   narrow viewport, so the approved desktop rendering above is untouched. It
   exists because a handful of things that are perfectly fine with a mouse are
   not fine with a thumb, and because a few desktop compositions need a
   different arrangement rather than a smaller one.
   ========================================================================== */

/* ---------- lateral reveals go vertical on narrow screens ----------
   .about-reveal--left / --right slide in 26px horizontally. On a wide screen
   that reads as a considered sideways settle, because the element has room
   either side. At 320-430px the element is already the full width of the
   column, so those same 26px are 26px of the element hanging past the viewport
   edge for the duration of the entrance — a transient horizontal overflow that
   overflow-x:clip hides but which still shows as a visible sideways lurch on a
   phone. Same reveal, vertical direction, no lurch. */
@media (max-width: 700px) {
  .about-reveal.about-reveal--left,
  .about-reveal.about-reveal--right { transform: translateY(20px); }
  .about-reveal.about-reveal--left.is-in,
  .about-reveal.about-reveal--right.is-in { transform: none; }
}

/* ---------- 44px floor on coarse pointers ----------
   Keyed to the pointer, not the width: a small laptop window still has a
   precise cursor and does not need padded-out links, while a large tablet
   does. These are the few elements measured under 44px that aren't already
   handled by their own component rule. Padding rather than height, so nothing
   in the surrounding layout is pushed around. */
@media (hover: none), (pointer: coarse) {
  .btn, .btn--sm { min-height: 48px; }
  .tlink { min-height: 44px; }
  .dd-link { min-height: 44px; }
  /* the page's primary call to action, at the small end of its own clamp() */
  .contact-cta__link { min-height: 44px; }
  .filters button { min-height: 44px; padding-block: .6rem; }
  .footer__bottom a { display: inline-flex; align-items: center; min-height: 44px; }
  /* the swatch chips and gallery tiles are not links, so they are left alone */
}

/* ---------- services index on phones ----------
   The desktop row is a four-track grid: numeral, title, description, Explore.
   Below 900px it already restacks (numeral+title on one line, then copy, then
   the link), which is the right reading order — the problem was that it kept
   the desktop's generous row padding and its 3rem numeral gutter, so each row
   was mostly empty space and four of them filled a phone screen and a half.
   This tightens the arrangement without changing the hierarchy: the numeral
   sits close to the title it belongs to, the rule still closes each row, and
   the Explore link keeps its own line so it stays an obvious tap target. */
@media (max-width: 560px) {
  .svc-index__row {
    grid-template-columns: auto minmax(0, 1fr);
    column-gap: var(--s-2);
    row-gap: var(--s-2);
    padding-block: var(--s-4);
  }
  .svc-index__num { font-size: var(--t-xs); align-self: start; padding-top: .5em; }
  .svc-index__title { font-size: clamp(1.55rem, 1.2rem + 3.4vw, 2.2rem); }
  .svc-index__desc { font-size: var(--t-sm); max-width: none; }
  .svc-index__list { font-size: var(--t-xs); line-height: 1.7; }
  .svc-index__go { min-height: 44px; }
}

/* ---------- evidence card on phones ----------
   The ring is a fixed square that was sized for a sidebar; full-bleed on a
   phone it becomes a very large empty circle. Capped, and the sticky
   behaviour is already dropped at 900px in the component's own rule. */
@media (max-width: 560px) {
  .evidence__stat-card { max-width: 260px; padding: var(--s-4); }
  .evidence__point { padding-block: var(--s-4); }
}

/* ---------- form ergonomics on touch ----------
   16px is the exact threshold below which iOS Safari zooms the viewport on
   focus, which then leaves the page zoomed and horizontally scrollable — the
   single most common way a well-built form feels broken on an iPhone. The
   text inputs on this site are already at or above it via --t-body; these are
   the two that were not (the file picker and its button), plus a full-width
   submit so the primary action is unmistakable. */
@media (max-width: 700px) {
  .field input[type="file"],
  .field input[type="file"]::file-selector-button { font-size: 16px; }
  .form-actions .btn { width: 100%; }
  .form-actions { flex-direction: column; align-items: stretch; gap: var(--s-3); }
  .checks { grid-template-columns: 1fr; }
  .check { min-height: 48px; }
}

/* ---------- reduced motion ---------- */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
    /* Delays have to go too, not just durations. Zeroing duration alone
       still leaves every staggered reveal on the site (--d on .reveal,
       .about-rule, the hero cascades) waiting out its full delay before
       snapping in — so a reader who asked for less motion instead got a
       page that appeared in silent, unexplained instalments. */
    animation-delay: 0s !important;
    transition-delay: 0s !important;
    scroll-behavior: auto !important;
  }
  .reveal { opacity: 1; transform: none; }
  .work-card:hover .work-card__media img { transform: none; }
}

/* ---------- print ---------- */
@media print {
  .nav, .cta, .footer, .cat-nav, .intro-panel { display: none; }
  body { background: #fff; }
}
