/* ===========================================================================
   FutureToast Studios — workshop draft
   Colour-blocked bands: amber / paper / navy / ink. No JS, no third parties.

   Two systems hold the page together:
     · one spacing scale (--s1 … --s8). Every gap, pad and margin is a step on
       it, so nothing is a one-off clamp that nearly matches its neighbour.
     · three breakpoints (600 / 900 / 1200). The whole composition changes at
       each one instead of one grid at a time.
   =========================================================================== */

@font-face {
  font-family: 'Fraunces';
  src: url(assets/fraunces-latin.9ec8233c.woff2) format('woff2');
  font-weight: 600 700;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
                 U+02DC, U+2000-206F, U+20AC, U+2122, U+2212, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Figtree';
  src: url(assets/figtree-latin.8330490a.woff2) format('woff2');
  font-weight: 300 800;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
                 U+02DC, U+2000-206F, U+20AC, U+2122, U+2212, U+FEFF, U+FFFD;
}

:root {
  /* amber does three jobs and needs three values: a field you look across,
     an accent you look at, and a line on cream that has to stay visible. */
  --amber:       #F1AE48;
  --amber-press: #E08F22;
  --amber-field: #E69A37;
  --amber-line:  #B7771C;   /* 3.36:1 on paper — a hover underline is a state */
  --amber-deep:  #96570A;

  --paper:       #FAF3E6;
  --paper-edge:  #D6C7A6;
  --ink:         #2C2521;
  --ink-deep:    #0E0A06;
  --ink-soft:    #59472F;
  --amber-ink:   #2E1F0C;
  --navy:        #22355A;
  --navy-deep:   #101B2F;
  --navy-line:   #2A3856;
  --navy-soft:   #BCC7DB;
  --foot-soft:   #A89A84;
  --foot-edge:   #3A2F20;
  --foot-rule:   #5A4B37;

  --ring: var(--amber);

  --serif: 'Fraunces', 'Iowan Old Style', Georgia, 'Times New Roman', serif;
  --sans:  'Figtree', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;

  --gutter: clamp(20px, 4.4vw, 56px);

  --s1: 8px;
  --s2: 12px;
  --s3: 16px;
  --s4: 24px;
  --s5: 32px;
  --s6: clamp(40px, 4vw, 56px);
  --s7: clamp(56px, 6vw, 80px);
  --s8: clamp(72px, 9vw, 128px);
  --band-y: var(--s8);

  /* one shadow recipe for every app capture: a contact layer and an ambient
     layer, same direction, re-inked for the ground it sits on. */
  --lift-paper: 0 2px 4px -2px rgba(28, 22, 16, .30),
                0 18px 36px -18px rgba(28, 22, 16, .26);
  --lift-navy:  0 2px 4px -2px rgba(0, 0, 0, .45),
                0 18px 36px -18px rgba(0, 0, 0, .50);

  /* one easing family, two curves. Both stay inside 0..1 on the Y axis, so
     nothing on this page can overshoot, bounce or wobble. `ease` appears
     nowhere. */
  --ease-out:   cubic-bezier(.23, 1, .32, 1);     /* entrances and reveals: fast off the mark, long settle */
  --ease-quick: cubic-bezier(.32, .72, 0, 1);     /* hover, focus, press */

  --dur-micro:  120ms;   /* colour and underline colour only */
  --dur-state:  170ms;   /* button lift, nav underline, background swap */
  --dur-row:    420ms;   /* header bar, small rows, footer items */
  --dur-enter:  560ms;   /* headings, lede, prose */
  --dur-device: 640ms;   /* the slabs and the app captures — the one long one */

  /* the load sequence, in one place: a 70ms step, five steps at most */
  --d0: 0ms;
  --d1: 70ms;
  --d2: 140ms;
  --d3: 210ms;
  --d4: 280ms;

  /* travel. Small on purpose — a long slide reads cheap. */
  --rise-sm: 8px;
  --rise:    12px;
  --rise-md: 14px;
  --rise-lg: 18px;
  --settle:  .985;   /* scale from-state: images and slabs ONLY, never type */
  --lift:    -2px;   /* hover / focus lift on .btn */
  --press:   1px;    /* the existing :active depress */
}

/* --- reset ---------------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }
html {
  -webkit-text-size-adjust: 100%;
  background: var(--ink-deep);   /* what an overscroll past the footer reveals */
  /* no grey flash over every tap on iOS and Android; each control below has
     its own :active state in its place */
  -webkit-tap-highlight-color: transparent;
}
/* taps fire at once (no double-tap-zoom wait), and a long press on a control
   doesn't select its label. Body text stays selectable. */
a, button, label, summary, input, select, textarea { touch-action: manipulation; }
.btn, summary, .pills label, .cat-row, .os-switch label {
  -webkit-user-select: none; user-select: none;
}
body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--sans);
  font-size: 17px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  overflow-wrap: break-word;
  overscroll-behavior-y: none;
}
img, svg { display: block; max-width: 100%; }
img { height: auto; }
h1, h2, h3 { margin: 0; font-family: var(--serif); font-weight: 700; }
p { margin: 0; }
ol, ul, dl, dd { margin: 0; padding: 0; }
li { list-style: none; }
a { color: inherit; }
::selection { background: var(--amber); color: var(--ink); }

/* 404 only: the page is shorter than a laptop viewport, so without this the ink
   footer floats and leaves a strip of paper under it. */
body.short-page { min-height: 100svh; display: flex; flex-direction: column; }
body.short-page main { flex: 1 0 auto; display: flex; flex-direction: column; }
body.short-page main > .band { flex: 1 0 auto; }

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

/* grid children must be allowed to shrink below their intrinsic width, or a
   414px screenshot drags the whole track past the viewport on a phone. */
.hero-grid > *,
.nf-grid > *,
.work-head > * { min-width: 0; }

/* --- focus + skip --------------------------------------------------------- */
:focus-visible {
  outline: 3px solid var(--ring);
  outline-offset: 3px;
  border-radius: 2px;
}
/* the ring is declared per band rather than patched per section, so a new
   light band cannot ship with an amber ring on amber. */
.site-head, .hero, .work, .notfound, .skip { --ring: var(--ink); }

.skip {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 10;
  background: var(--ink);
  color: var(--paper);
  padding: 12px 18px;
  font-weight: 600;
  text-decoration: none;
}
.skip:focus { left: 8px; top: 8px; }

/* --- type scale ----------------------------------------------------------- */
h1 {
  font-size: clamp(44px, 6.4vw, 76px);
  line-height: 0.96;
  letter-spacing: -0.026em;
  max-width: 8.5em;
  text-wrap: balance;
}
h2 {
  font-size: clamp(34px, 4.2vw, 54px);
  line-height: 1.04;
  letter-spacing: -0.02em;
  max-width: 13em;
  text-wrap: balance;
}
h3 {
  font-size: clamp(20px, 1.6vw, 24px);
  line-height: 1.18;
  letter-spacing: -0.012em;
  text-wrap: balance;
}
.lede {
  font-size: clamp(19px, 1.45vw, 22px);
  line-height: 1.55;
  max-width: 44ch;
  text-wrap: pretty;
}
.body-lg {
  font-size: clamp(18px, 1.25vw, 20px);
  line-height: 1.6;
  max-width: 46ch;
  text-wrap: pretty;
}
.minor-note {
  font-size: 15.5px;
  max-width: 48ch;
  text-wrap: pretty;
}

/* --- buttons + small links ------------------------------------------------ */
.btn-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
  margin-top: var(--s5);
}
.btn {
  display: inline-block;
  padding: 14px 24px;
  border: 2px solid var(--ink);
  border-radius: 999px;
  font-weight: 600;
  font-size: 16px;
  line-height: 1.2;
  text-decoration: none;
  transition: background-color var(--dur-state) var(--ease-quick),
              color            var(--dur-state) var(--ease-quick),
              border-color     var(--dur-state) var(--ease-quick),
              transform        var(--dur-state) var(--ease-quick);
}
.btn-ink { background: var(--ink); color: var(--paper); }
.btn-outline { background: transparent; color: var(--ink); }
.btn-amber { background: var(--amber); color: var(--ink); }
/* 2px up on hover against 1px down on press: three pixels of total state
   travel — enough to feel like a physical control, too little to watch move.
   Focus gets the identical lift; a keyboard user is not a second-class one. */
.btn:focus-visible { transform: translateY(var(--lift)); }
/* EVERY :hover IN THIS FILE SITS BEHIND THIS QUERY. A phone fakes :hover on
   tap and keeps it until the next tap somewhere else, which left a tapped
   Download button raised and recoloured after the tap was over.
   Hover deepens the primary instead of hollowing it out — the old rule made
   "See Pantry" turn into the secondary button on hover. */
@media (hover: hover) and (pointer: fine) {
  .btn-ink:hover { background: var(--ink-deep); border-color: var(--ink-deep); }
  .btn-outline:hover { background: var(--ink); color: var(--paper); }
  .btn-amber:hover { background: var(--amber-press); }
  .btn:hover { transform: translateY(var(--lift)); }
}
.btn:active { transform: translateY(var(--press)); transition-duration: 100ms; }
/* with no hover lift to come down from, 1px of press is invisible under a
   thumb: on touch the button gives a little instead */
@media not all and (hover: hover) and (pointer: fine) {
  .btn:active { transform: scale(.97); }
}

.minor-note a {
  color: var(--ink-soft);
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
  /* no lift on inline text — a 1px nudge on 15.5px type shimmers */
  transition: color                 var(--dur-micro) var(--ease-quick),
              text-decoration-color var(--dur-micro) var(--ease-quick);
}
.minor-note a:focus-visible { color: var(--ink); }
@media (hover: hover) and (pointer: fine) { .minor-note a:hover { color: var(--ink); } }

/* --- header --------------------------------------------------------------- */
.site-head {
  background: var(--paper);
  border-bottom: 2px solid var(--ink);
}
.head-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px 28px;
  min-height: 96px;
  padding-block: var(--s3);
}
.brand {
  display: inline-flex;
  align-items: center;
  color: var(--ink);
  text-decoration: none;
  flex-shrink: 0;
  transition: opacity var(--dur-state) var(--ease-quick);
}
/* focus deliberately does NOT take the dim: lowering the contrast of the thing
   a keyboard user just landed on is the opposite of feedback. The ring is the
   stronger signal and it is already there. */
@media (hover: hover) and (pointer: fine) { .brand:hover { opacity: .78; } }
.brand-logo { width: 105px; height: 64px; }
.brand-foot .brand-logo { width: 72px; height: 44px; }
.head-nav {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 26px;
  font-size: 16px;
  font-weight: 600;
}
.head-nav a {
  position: relative;
  text-decoration: none;
  padding-block: 4px;
  border-bottom: 2px solid transparent;
}
/* the underline grows from the left rather than snapping on. The bar sits
   exactly over the transparent border it replaces, so the box model is
   untouched at both header states — including the taller tap target below
   600px, where the padding changes but the border does not. */
.head-nav a::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: -2px;
  height: 2px;
  background: var(--amber-line);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform var(--dur-state) var(--ease-quick);
}
.head-nav a:focus-visible::after { transform: scaleX(1); }
@media (hover: hover) and (pointer: fine) { .head-nav a:hover::after { transform: scaleX(1); } }

/* --- bands ---------------------------------------------------------------- */
.band { padding-block: var(--band-y); }

/* hero — amber ------------------------------------------------------------- */
.hero {
  background: var(--amber-field);
  position: relative;
  z-index: 1;
  padding-block: var(--s7) var(--s6);
}
.hero-grid {
  display: grid;
  gap: var(--s7);
  align-items: center;
}
.hero-copy .lede { margin-top: var(--s4); color: var(--amber-ink); }

/* the slab: a slice-shaped block that carries the app and dips into the
   section below it. The top padding is set so the screenshot clears the sweep
   of the arch rather than being buried in it. */
.slab {
  background: var(--navy);
  border-radius: clamp(72px, 10vw, 128px) clamp(72px, 10vw, 128px) 24px 24px;
  padding: clamp(44px, 4.4vw, 60px) clamp(24px, 2.8vw, 40px) clamp(24px, 2.6vw, 36px);
  overflow: hidden;
  max-width: 430px;
  margin-inline: 0;      /* stacked: left edge lines up with the h1 */
  width: 100%;
}
.slab img {
  width: 100%;
  max-width: 308px;
  height: auto;
  margin-inline: auto;
  border-radius: 26px;
  border: 1px solid rgba(188, 199, 219, 0.40);
  box-shadow: var(--lift-navy);
}

/* pantry — paper ----------------------------------------------------------- */

.phone {
  border-radius: clamp(16px, 2vw, 22px);
  overflow: hidden;
  box-shadow: 0 0 0 1px rgba(28, 22, 16, .14), var(--lift-paper);
}
.phone img { width: 100%; height: auto; }

/* client work — paper ------------------------------------------------------ */
.work { background: var(--paper); }
.work-head { display: grid; row-gap: var(--s6); }

/* contact + footer — ink --------------------------------------------------- */
.contact {
  background: var(--ink-deep);
  color: var(--paper);
  padding-bottom: var(--s7);
}
.contact-mark {
  width: 84px;
  height: auto;              /* 240x187 exactly; a fixed 66px squashed it 0.8% */
  margin-left: -2px;         /* seats the slice's curve on the heading's stem */
  margin-bottom: var(--s4);
}
.contact h2 { max-width: 12em; }
.contact .minor-note { margin-top: var(--s4); color: var(--foot-soft); }

.site-foot {
  background: var(--ink-deep);
  color: var(--foot-soft);
  padding-bottom: var(--s6);
}
/* the rule belongs to the content column, not to the wrap's padding box —
   every other rule on the page starts and ends at the content edge. */
.foot-grid {
  position: relative;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 20px 40px;
  padding-top: var(--s6);
}
.foot-grid::before {
  content: '';
  position: absolute;
  inset: 0 var(--gutter) auto;
  border-top: 1px solid var(--foot-edge);
}
.foot-links {
  display: flex;
  flex-wrap: wrap;
  gap: 10px 26px;
  font-size: 15px;
  margin-right: auto;
}
.foot-links a {
  color: var(--foot-soft);
  text-decoration: underline;
  text-decoration-color: var(--foot-rule);
  text-decoration-thickness: 1px;
  text-underline-offset: 4px;
  /* two properties move at once here; transitioning them together is what
     stops the text and its underline arriving at different moments. */
  transition: color                 var(--dur-state) var(--ease-quick),
              text-decoration-color var(--dur-state) var(--ease-quick);
}
.foot-links a:focus-visible { color: var(--amber); text-decoration-color: var(--amber); }
@media (hover: hover) and (pointer: fine) {
  .foot-links a:hover { color: var(--amber); text-decoration-color: var(--amber); }
}
.copy { font-size: 14.5px; }

/* --- 404 ------------------------------------------------------------------ */
.notfound {
  background: var(--amber-field);
  min-height: 74svh;
  display: flex;
  align-items: center;
  padding-block: var(--s8);
}
.nf-grid { display: grid; gap: var(--s7); align-items: center; width: 100%; }
.notfound .lede { margin-top: var(--s4); color: var(--amber-ink); }
.notfound h1 { max-width: 15ch; }
/* the 404 borrows the hero's one signature move rather than inventing a
   second one: the same navy slab, holding the mark instead of the app.
   Below 600 the copy fills the band on its own and the slab would only add
   scroll, so it starts at the same 600 threshold as the other two-up pairs. */
.nf-slab { display: none; }

/* ===========================================================================
   Three breakpoints. Each one changes the whole composition.
   =========================================================================== */

/* 600 — the two-up pairs come in ------------------------------------------- */
@media (min-width: 600px) {

  /* stacked under the copy, left edge on the h1 — the same move the hero's
     slab makes when it stacks. Without it 600-899 was a bare amber field. */
  .nf-slab {
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--navy);
    border-radius: clamp(72px, 10vw, 128px) clamp(72px, 10vw, 128px) 24px 24px;
    padding: var(--s5);
    max-width: clamp(200px, 30vw, 260px);
    width: 100%;
    margin-inline: 0;
    aspect-ratio: 17 / 20;
  }
  /* holds the mark at ~half the slab's width, the proportion it has on desktop */
  .nf-mark { width: min(140px, 74%); }
}

@media (max-width: 599px) {
  .head-bar {
    flex-direction: column;
    align-items: flex-start;
    gap: 0;
    min-height: 0;
    padding-block: 0;
  }
  .brand { padding-block: var(--s3); }
  .brand-logo { width: 84px; height: auto; }   /* 105x64 kept exactly */
  .head-nav {
    width: 100%;
    gap: 0 22px;
    padding-block: 2px;
    border-top: 1px solid var(--paper-edge);
    font-size: 15.5px;
  }
  /* 44px hit boxes on every small link at phone width */
  .head-nav a { padding-block: 12px 8px; }
  .foot-links a { display: inline-block; padding-block: 10px; }
  /* one column beats four links falling where they land */
  .foot-links { display: grid; justify-items: start; gap: 0; width: 100%; margin-right: 0; }
  .phone {
    box-shadow: 0 0 0 1px rgba(28, 22, 16, .14),
                0 1px 3px -1px rgba(28, 22, 16, .28),
                0 10px 20px -12px rgba(28, 22, 16, .24);
  }
  h1 br { display: none; }
}

/* 600-899 — still a touch width. The nav's 44px hit boxes were scoped to the
   phone block below, which left every tablet showing 36px targets: iPad
   portrait is 768-834 and is touch-first. The link's transparent 2px border
   is what ::after sits against, so the underline moves with the padding and
   nothing else in the bar does — .head-bar is held at 96px by the 64px logo
   plus its own 16px padding, and a 48px nav still clears it. */
@media (min-width: 600px) and (max-width: 899px) {
  .head-nav a { padding-block: 12px 8px; }
}

/* 900 — the page becomes two-column ---------------------------------------- */
@media (min-width: 900px) {
  .hero { padding-bottom: 0; }
  .hero-grid { grid-template-columns: minmax(0, 1.1fr) minmax(0, 0.9fr); }
  .slab { margin-bottom: -108px; margin-inline: auto 0; }

  /* the section intro and the service copy are the same voice at the same
     size, so they share a column edge. */
  .work-head {
    grid-template-columns: 68px minmax(0, 0.85fr) minmax(0, 1.45fr);
    column-gap: var(--s5);
    align-items: end;
  }
  .work-head h2 { grid-column: 1 / 3; }

  .nf-grid { grid-template-columns: minmax(0, 1.1fr) minmax(0, 0.9fr); }
  /* two-up: the slab hangs on the right content margin, like the hero's */
  .nf-slab {
    padding: var(--s6);
    max-width: 340px;
    margin-inline: auto 0;
  }
  .nf-mark { width: clamp(120px, 12.5vw, 180px); }
}

@media (max-width: 899px) {
  h1 br, h2 br { display: none; }
}

/* ===========================================================================
   MOTION
   The page settling into place, not performing. Nothing here loops, spins,
   overshoots or counts, and only transform and opacity ever move — so no
   rule below can reflow the page, shift a layout or cost a frame of CLS.

   Two mechanisms carry all of it:

     1. ABOVE THE FOLD — time-based @keyframes with `animation-fill-mode:
        backwards`. Genuinely one-way: runs once per pageview, never repeats.
     2. BELOW THE FOLD — CSS scroll-driven `view()` timelines.

   ONE RULE GOVERNS EVERY SCROLL REVEAL. It opens as the element's own top
   crosses the bottom edge of the window, and it reads as arrived about 0.3 of
   a viewport height later — 240 to 320px of scroll on a 900px window, so that
   every reveal on the page finishes with its top between 64% and 74% of the
   window. That number is the whole point of this pass. At half of it every
   reveal landed inside the last inch of the screen, several of them below it,
   and a third of the page's motion was never seen at all: measured at
   1440x900, nine elements reached opacity 1.00 before they had crossed the
   fold. Now none does — every reveal is still travelling when it appears,
   with 67% to 100% of its fade happening on screen.

   THE OTHER HALF OF THAT RULE IS WHERE IT STOPS. Landing at ~70% keeps the
   motion in the lower third, where the eye is, and keeps it out of the part
   of the screen people read from. Swept down and back up at eleven window
   sizes, nothing whose top is in the upper 60% of the window is ever below
   0.99 opacity. Between 60% and 75% the floor is 0.82, which is the honest
   price of the change and is the bottom quarter of the screen.

   WHERE THE STAGGER COMES FROM. Almost everywhere, the scroll itself: two
   stacked blocks 140px apart arrive 140px apart, which is a beat of roughly a
   third of a second at reading speed and needs no authoring. A designed offset
   is added in exactly three places, and only because the siblings there share a
   top edge and would otherwise fire as one movement: the two captures, which
   stand on a common baseline; the two facts in a row at 900+; and the heading
   and its paragraph in the work intro at 900+. Nothing on the page is a cascade
   more than three steps deep, and where a container names the timeline, each
   child's start is set to the scroll position at which that child appears —
   so a container range behaves exactly like an independent one and cannot
   push its own tail off the bottom of the screen, which is what a fixed
   stagger on a tall parent does.

   PROGRESSIVE ENHANCEMENT. Every at-rest style in this stylesheet is the
   final, VISIBLE state. Each hidden from-state lives in a @keyframes block and
   is reached only through a rule inside `@supports (animation-timeline:
   view())` — itself inside `@media (prefers-reduced-motion: no-preference)`.
   A browser with no scroll-driven animations never sees an `opacity: 0` or a
   translate, and renders this page exactly as it rendered before this pass.

   WHY THE RANGES ARE ANCHORED TO `cover`. A range written against `entry`
   scales with the subject's own height, so a 62px row would reveal over 28px
   of scroll and a 700px figure over 315px — the same percentage reading as two
   different animations. `cover 0%` starts at the same instant as `entry 0%`
   but its length is (subject + viewport), which is dominated by the viewport.
   That is also what lets one percentage hold at every window size: the travel
   it buys scales with the viewport, so a reveal lands at the same place on
   screen in a 700px window and in a 1600px one. The two blocks at the very
   bottom of the page are the exception, and the reason is written out where
   they are: a cover range needs scroll that no longer exists down there.

   A SCROLL-DRIVEN REVEAL HAS NO DURATION, and it is worth saying so rather
   than implying one. Its length is pixels of scroll, so what a visitor
   perceives is that length divided by how fast they are scrolling. At ~280px,
   a reading-speed scroll of 400-600px/s settles it in about 470-700ms, which
   is the budget these entrances are written to; a hard flick compresses it to
   ~140ms and a slow crawl stretches it past a second. Only the fast end is
   worth defending against, and the defence is length: at the old 135px the
   same flick was a ~65ms flicker across 17px prose, which is exactly what a
   cheap page looks like.

   REVERSIBILITY, stated plainly rather than papered over. Scroll-driven
   timelines are bidirectional: scrub the scroll back and the animation scrubs
   back with it. The only true one-way trigger is IntersectionObserver, which
   needs JavaScript, which the CSP here forbids — correctly. So the reversal is
   engineered out of sight instead: every range opens at the moment the element
   first appears and closes while it is still below the reading area, and
   `--ease-out` on a progress timeline front-loads the travel so it is most of
   the way arrived earlier still. Content is fully settled everywhere in the
   reading area; the only thing that can move on the way back up is already
   sliding off the bottom of the screen.
   =========================================================================== */

/* Every from-state on the page lives in these eight blocks and nowhere else.
   A keyframe nothing references is inert, so they cost nothing where the
   @supports guard below does not match. */
@keyframes ft-rise-sm { from { opacity: 0; transform: translateY(var(--rise-sm)); } to { opacity: 1; transform: none; } }
@keyframes ft-rise    { from { opacity: 0; transform: translateY(var(--rise));    } to { opacity: 1; transform: none; } }
@keyframes ft-rise-md { from { opacity: 0; transform: translateY(var(--rise-md)); } to { opacity: 1; transform: none; } }
@keyframes ft-rise-lg { from { opacity: 0; transform: translateY(var(--rise-lg)); } to { opacity: 1; transform: none; } }
/* scale belongs to images and the navy slabs only. Scaling 76px Fraunces or a
   60px email address re-rasterises the glyphs mid-flight and goes soft on
   exactly the elements that should look sharpest. */
@keyframes ft-settle    { from { opacity: 0; transform: translateY(var(--rise))    scale(var(--settle)); } to { opacity: 1; transform: none; } }
@keyframes ft-settle-lg { from { opacity: 0; transform: translateY(var(--rise-lg)) scale(var(--settle)); } to { opacity: 1; transform: none; } }
/* the one pure fade. It exists for the single element on the site that
   animates INSIDE another animating element, and it is what keeps that from
   being a special case — see .nf-mark. */
@keyframes ft-fade { from { opacity: 0; } to { opacity: 1; } }

/* the browser's own interpolation for the three nav links and the two hero
   buttons — cancellable by touching the wheel, which is what separates it
   from scroll-jacking. The margin keeps a section heading off the top edge. */
html { scroll-behavior: smooth; }
#work, #contact { scroll-margin-top: var(--s4); }

@media (prefers-reduced-motion: no-preference) {

  /* --- the load sequence -------------------------------------------------
     `backwards` fill means the from-state exists only while the animation is
     pending or running. If the animation never runs, the element sits at its
     normal cascade value, which is visible — there is no window in which a
     delayed element can be stranded. The stylesheet is render-blocking, so if
     these rules applied at all, the animations ran.

     THE HEADER IS NOT IN THIS SEQUENCE, and that is a decision rather than an
     omission. It is the frame the page settles into, so it should already be
     drawn when the settling starts; fading it in bought the least of anything
     here and cost the most, because it put the studio's own logo behind a
     420ms fade and pushed every word below it back a step. Making it static
     returns that step to the h1, which now starts at the first frame the
     stylesheet is live for — three beats to compose the hero instead of five,
     and no moment where the largest element on the screen is invisible. */

  /* the two pages share one arrival, beat for beat, which is most of what
     makes them feel like one site */
  .hero-copy h1,       .nf-copy h1       { animation: ft-rise-lg var(--dur-enter) var(--ease-out) var(--d0) backwards; }
  .hero-copy .lede,    .nf-copy .lede    { animation: ft-rise-md var(--dur-enter) var(--ease-out) var(--d1) backwards; }
  /* the row arrives as one unit — staggering two adjacent pills against each
     other is the exact texture that reads as a template */
  .hero-copy .btn-row, .nf-copy .btn-row { animation: ft-rise    520ms           var(--ease-out) var(--d2) backwards; }

  /* the page's most expensive-looking object gets the longest curve and the
     only scale. It rides with the lede rather than behind the buttons: it is
     the likeliest thing on the page to be the browser's largest paint, and
     every millisecond it spends at opacity 0 is charged to that measurement.
     The origin matters: at 900+ the hero slab hangs 108px into the paper band
     below it, and a centre origin would make that seam breathe by ~3px. At the
     bottom edge the scale contributes nothing there. Scaling the block also
     scales its --lift-navy shadow, which is the honest version of the shadow
     deepening as it lands. */
  .slab, .nf-slab {
    transform-origin: 50% 100%;
    animation: ft-settle-lg var(--dur-device) var(--ease-out) var(--d1) backwards;
  }
  /* the 404's one moment of character: the slab arrives, and the toast becomes
     present inside it a beat later. No spin — the mark has motion lines drawn
     into it already and does not need animating.
     IT TAKES OPACITY AND NOTHING ELSE, and that is the rule rather than a
     preference. This is the only element on the site with an animating
     ancestor, and a transform inside a transform multiplies: the slab's
     18px translate and .985 scale about its bottom edge already carry every
     child of it 26.8px, measured — exactly what the hero slab does to the
     screenshot it holds, which is the same move and has always looked right.
     A second travel token declared here would be added on top of that, for a
     total no token in the system authorises. So nothing nested declares its
     own travel; its ancestor's motion is its motion, and the beat that makes
     it read as a separate arrival is the delay on the fade. */
  .nf-mark { animation: ft-fade var(--dur-row) var(--ease-out) var(--d3) backwards; }
  /* on the 404 the footer is inside the first viewport, so a scroll reveal
     would never fire and it would be the one static thing on a settling page */
  .short-page .foot-grid { animation: ft-rise-sm var(--dur-row) var(--ease-out) var(--d4) backwards; }

  /* --- the scroll reveals ----------------------------------------------- */
  @supports (animation-timeline: view()) {

    /* shared: each reveal runs the length of its own range, on the entrance
       curve. `animation-duration: auto` is written as a longhand rather than
       through the shorthand because the shorthand's initial duration differs
       between CSS Animations 1 and 2, and this is not a thing to leave open. */
    .work-head > *,
.contact > .wrap > *,
body:not(.short-page) .foot-grid {
      animation-duration: auto;
      animation-fill-mode: both;
      animation-timing-function: var(--ease-out);
    }

    /* stacked at this width; the 900 rule below switches them to a container */
    .work-head > * {
      animation-name: ft-rise-md;
      animation-timeline: view();
      animation-range: cover  0% cover 31%;
    }
    .work-head h2 { animation-name: ft-rise-lg; }

    /* THE CLOSING BAND IS ANCHORED TO `entry`, NOT `cover`, AND THIS IS THE
       ONE PLACE THAT EXCEPTION IS CORRECT.

       A `cover` range is (subject + viewport) long, so reaching a quarter of
       it costs a quarter of a viewport height of scrolling. Everything above
       is far enough from the end of the document to have that scroll
       available. This band is not: the page runs out underneath it. Measured
       in Chrome, the furthest the contact band's cover timeline can ever
       progress is 27% at an 1800px-tall window, against a range that needed
       26% — and the footer's was 8% against 22%, which left the copyright line
       sitting permanently at 0.44 opacity, 4.5px low, on a tall desktop.

       An `entry` range is the subject's OWN height long, so its cost in scroll
       does not grow with the viewport. Reachable progress here is 1 + (document
       left below the band ÷ the band's height) = 168%, at any window size, which
       is why the percentages below are allowed past 100%.

       The other thing the bottom of a document cannot give you is a uniform
       landing height. There are 581px of page left under this band and 329px
       between its first element and its last, so they cannot all finish ~280px
       after they appear — the last one would need scroll that does not exist.
       Each is given the travel its own remaining scroll can pay for instead,
       longest for the mark and tapering to the closing line: measured at
       1440x900 they land with their tops at 67%, 67%, 74% and 84% of the
       window. Every one opens within ~15px of the moment it appears, so all
       four are in flight the whole time they are on screen, and none of them
       is still moving by the time it reaches the reading area. Because an
       entry range is a fixed number of pixels rather than a share of the
       viewport, a short window makes this band's travel a larger fraction of
       the screen; at 1600x700 the mark finishes at 59% rather than 67%, which
       is the one place on the page where a reveal ends above the lower third.
       It ends there at 0.99 opacity, so what is left is arithmetic, not
       something anyone can see. */
    .contact > .wrap { view-timeline-name: --tl-contact; view-timeline-axis: block; }
    .contact > .wrap > *             { animation-name: ft-rise-md; animation-timeline: --tl-contact; }
    .contact > .wrap > .contact-mark { animation-name: ft-settle;  animation-range: entry  4% entry 96%; }
    .contact > .wrap > h2            { animation-name: ft-rise-lg; animation-range: entry 34% entry 120%; }
    .contact > .wrap > .minor-note   { animation-name: ft-rise-sm; animation-range: entry 92% entry 140%; }

    /* THE FOOTER REVEALS ONLY WHERE THERE IS ROOM TO SEE IT HAPPEN — below
       900, where the links stack and .foot-grid grows to 171px at 768 and
       355px at 390, with 211px and 395px of document under it. On a desktop
       it is ~100px tall with ~156px under it, which means that even at the
       very bottom of the page its top edge is still at 81% of the window:
       there is no scroll position from which a reveal there could be watched,
       and measurement agreed — all three items completed over 45px of scroll
       with none of it in flight on screen. So on a desktop the footer is
       simply drawn. By the time it is on screen the closing statement above it
       has finished arriving, and a band that is already at rest when it
       appears is not the same thing as a band that forgot to move.
       Where it does reveal it moves as ONE block rather than three. The three
       items span the whole height of the band at these widths, so staggering
       them would put the copyright line back in exactly the starved position
       the entry anchor was introduced to get it out of. */
    @media (max-width: 899px) {
      body:not(.short-page) .foot-grid {
        animation-name: ft-rise-sm;
        animation-timeline: view();
        animation-range: entry 0% entry 88%;
      }
    }

    /* 900 — two blocks stop being stacked and become rows, and a shared
       timeline starts being the right answer for them: the members of a row
       share a top edge, so only a designed 2% offset separates them. Below
       this width each element owns its own timeline and the scroll separates
       them for free. */
    @media (min-width: 900px) {
      .work-head { view-timeline-name: --tl-work; view-timeline-axis: block; }
      .work-head > *      { animation-timeline: --tl-work; }
      .work-head h2       { animation-range: cover  3% cover 33%; }
      .work-head .body-lg { animation-range: cover  5% cover 35%; }
    }
  }
}

/* --- reduced motion: a real kill switch ------------------------------------
   `animation: none` rather than a near-zero duration, because a near-zero
   duration is not an off switch — it does not reset animation-delay, so a
   delayed entrance renders as a blank element, and it makes no defined promise
   about a progress-based timeline, where duration is not what drives the
   animation at all. The shorthand resets name, duration, delay, fill AND
   animation-timeline together, so every element lands on its at-rest cascade
   value — which, by the construction above, IS the final visible state.
   Nothing can be stranded hidden and nothing can be scrubbed by scroll.

   Transitions are held to 60ms and restricted to paint properties. That one
   declaration is what stops .head-nav a::after from sliding its scaleX and the
   buttons from sliding their lift, while leaving colour and underline feedback
   alive. Reduced motion should mean no movement, not no response.

   The transform reset is scoped to .btn on purpose: .head-nav a::after uses
   scaleX(0) as its RESTING state, and a blanket `transform: none !important`
   would pin all three nav underlines permanently on. That is a legibility
   regression dressed up as an accessibility fix. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation: none !important;
    transition-property: color, background-color, border-color,
                         text-decoration-color, opacity !important;
    transition-duration: 60ms !important;
    transition-delay: 0s !important;
    transition-timing-function: var(--ease-quick) !important;
  }
  html { scroll-behavior: auto !important; }
  .btn:hover, .btn:focus-visible, .btn:active { transform: none !important; }
}

/* dark variant ----------------------------------------------------------- */
/* Every colour above is a token, so dark mode is a token swap plus explicit
   colours for the sections that already sit on dark grounds (their text was
   "paper", which is now the dark ground). Amber fields keep dark text. */
@media (prefers-color-scheme: dark) {
  :root {
    --paper:       #14110E;
    --paper-edge:  #3A3128;
    --ink:         #F3EADB;
    --ink-deep:    #0A0806;
    --ink-soft:    #C9B99F;
    --amber-field: #D98F2E;
    --amber-line:  #F1AE48;
    --lift-paper:  0 2px 4px -2px rgba(0, 0, 0, .60), 0 14px 32px -14px rgba(0, 0, 0, .55);
  }
  .contact, .site-foot { color: #F3EADB; }
  .hero, .notfound { color: #2E1F0C; }
  .hero h1, .notfound h1 { color: #2E1F0C; }
  .hero .btn-outline, .notfound .btn-outline { color: #2E1F0C; border-color: #2E1F0C; }
  .hero .btn-ink, .notfound .btn-ink { background: #2E1F0C; color: #F3EADB; border-color: #2E1F0C; }
  .phone { box-shadow: 0 0 0 1px rgba(255, 255, 255, .10), var(--lift-paper); }
  @media (hover: hover) and (pointer: fine) {
    .hero .btn-outline:hover, .notfound .btn-outline:hover { background: #2E1F0C; color: #F3EADB; }
    .hero .btn-ink:hover, .notfound .btn-ink:hover { background: #0A0806; border-color: #0A0806; }
  }
}

/* layout round 2 (Wyatt's picks: hero 2, tiles 4, pantry 2, how-we-work 3, tail 5) */
:root { --tile: #F3EAD9; }

/* hero plate: plain soft-cornered navy, one phone that cycles through two real screens */
.slab { border-radius: 28px; padding: clamp(22px, 2.6vw, 34px); }

@media (prefers-color-scheme: dark) {
  :root { --tile: #1E1915; }
}

/* the work band and the Pantry band are both paper, so their two full paddings
   read as a hole. Tint Pantry as its own surface and close the seam. */
.work { padding-bottom: var(--s7); }

/* round 4: air under the hero copy; the animation floats on the amber instead
   of sitting in a box; nothing hangs into the next band any more */
.hero { padding-block: var(--s7) var(--s8); }
.slab { background: transparent; box-shadow: none; padding: 0; border-radius: 0; max-width: 500px; }
@media (min-width: 900px) {
  .slab { margin-bottom: 0; align-self: center; }
  .hero-grid { align-items: center; }
}

/* round 5: bigger animation */
.slab { max-width: 560px; }
@media (min-width: 900px) { .hero-grid { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); } }

/* ── sticky header ───────────────────────────────────────────────────────── */
.site-head {
  position: sticky;
  top: 0;
  z-index: 50;
}
/* the bar is 96px because the logo is a 3-line lockup; that is too much of a
   phone viewport to keep pinned, so it tightens below 900. */
@media (max-width: 899px) {
  .head-bar { padding-block: 10px; }
  .brand-logo { width: auto; height: 40px; }
}
@media (max-width: 599px) {
  .brand-logo { height: 34px; }
  .head-nav { gap: var(--s2); }
  .head-nav a { font-size: 14.5px; }
}
@media (prefers-reduced-motion: no-preference) {
  .site-head { transition: box-shadow var(--dur-state) var(--ease-quick); }
}

/* the sticky bar must stay shallow on phones: the stacked layout at <600 made
   it 136px, a fifth of a 740px viewport, once the header became sticky. */
@media (max-width: 599px) {
  .head-bar {
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
    gap: var(--s2);
    min-height: 0;
    padding-block: 8px;
  }
  .brand-logo { height: 30px; }
  .head-nav { flex-wrap: nowrap; gap: 10px; }
  .head-nav a { font-size: 13px; letter-spacing: 0; white-space: nowrap; padding-block: 10px; }
}
@media (max-width: 379px) {
  .head-nav a { font-size: 12.5px; }
  .head-nav { gap: 8px; }
}

/* ── featured work ───────────────────────────────────────────────────────── */
.featured { background: var(--paper); }
.eyebrow {
  font-size: 13px; letter-spacing: .1em; text-transform: uppercase;
  color: var(--ink-soft); margin-bottom: var(--s3);
}
.plain-link { font-weight: 600; color: var(--amber-deep); text-underline-offset: 4px; }

@media (prefers-color-scheme: dark) {
  .plain-link, .stage-n { color: var(--amber); }
  .offer-cue { color: var(--ink-soft); }
  .sig-browser { border-color: rgba(255,255,255,.18); }
}

/* ═══ hero signature: a site and an app get built, shipped and go live ═══
   One 14s loop on a single shared clock: dashed wireframes, each block painted
   in from the left, a cursor presses publish, a "Live" toast lands, the phone
   gets its notification, a short hold, then it all fades back to wireframe.
   Each block has its own generated keyframes (no animation-delay), so the
   pieces can never drift apart however long the page stays open. */
.signature { --loop: 14s; position: relative; max-width: 600px; margin-inline: auto; padding-block: var(--s5) var(--s7); }
.sig-browser {
  position: relative; width: 71%; border-radius: 14px; overflow: hidden; background: #14110E;
  border: 1px solid rgba(255,255,255,.16); box-shadow: 0 24px 50px -22px rgba(0,0,0,.65);
}
.sig-chrome { display: flex; align-items: center; gap: 6px; padding: 9px 12px; background: #1E1915; }
.sig-chrome span { width: 8px; height: 8px; border-radius: 50%; background: rgba(243,234,219,.28); }
.sig-chrome i {
  margin-left: 8px; padding: 3px 12px; border-radius: 999px; background: rgba(255,255,255,.07);
  color: rgba(243,234,219,.5); font-style: normal; font-size: 10.5px; letter-spacing: .03em;
}
.sig-page { padding: var(--s4) var(--s4) var(--s5); display: grid; gap: 11px; }

/* every block is a wireframe (::after, dashed) with a fill painted over it (::before) */
.f { position: relative; }
.f::before, .f::after { content: ""; position: absolute; inset: 0; border-radius: inherit; pointer-events: none; }
.f::before { background: var(--fill); clip-path: inset(0 100% 0 0); }
.f::after { border: 1px dashed rgba(243,234,219,.34); }
.sig-bar { height: 14px; border-radius: 5px; --fill: rgba(243,234,219,.88); }
.sig-bar.sig-thin { height: 6px; border-radius: 3px; --fill: rgba(243,234,219,.34); }
.sig-w55 { width: 55%; } .sig-w65 { width: 65%; } .sig-w70 { width: 70%; } .sig-w80 { width: 80%; }
.sig-pill { width: 92px; height: 24px; border-radius: 999px; --fill: var(--amber); }
.sig-row { position: relative; display: grid; grid-template-columns: repeat(3, 1fr); gap: 9px; overflow: hidden; border-radius: 9px; }
.sig-tile { height: 46px; border-radius: 9px; --fill: rgba(255,255,255,.1); }
.sig-tile:first-child { --fill: rgba(241,174,72,.85); }

.sig-device {
  position: absolute; right: 0; bottom: -14px; width: 32%; max-width: 172px;
  border-radius: 22px; background: #0B0A09; border: 1px solid rgba(255,255,255,.22);
  /* the ring is the hero's own field colour, so the phone sits in front of the
     browser instead of looking like it clipped a hole in it */
  box-shadow: 0 0 0 7px var(--amber-field), 0 22px 44px -16px rgba(0,0,0,.7);
  overflow: hidden;
}
.sig-notch { width: 38%; height: 5px; border-radius: 0 0 6px 6px; background: #1E1915; margin: 0 auto; }
.sig-screen { padding: 14px 12px; display: grid; gap: 8px; }
.sig-card { display: flex; align-items: center; gap: 8px; padding: 8px; border-radius: 9px; --fill: rgba(255,255,255,.08); }
.sig-card > * { position: relative; z-index: 1; }
.sig-dot { width: 13px; height: 13px; border-radius: 50%; background: var(--amber); flex: none; }
.sig-dot-mute { background: rgba(188,199,219,.55); }
.sig-card em { display: block; height: 5px; border-radius: 3px; background: rgba(243,234,219,.6); flex: 1; }
.sig-cta { height: 22px; border-radius: 999px; margin-top: 2px; --fill: var(--amber); }
.sig-tabs { display: flex; justify-content: space-around; padding: 7px 0 9px; background: rgba(255,255,255,.05); }
.sig-tabs i { width: 7px; height: 7px; border-radius: 50%; background: rgba(243,234,219,.3); }
.sig-tabs i:first-child { background: var(--amber); }

/* the moment it ships: a cursor, a press, a ripple, a toast, a notification */
.sig-cursor { position: absolute; z-index: 3; left: 62%; top: 46%; width: 16px; height: 20px; opacity: 0; filter: drop-shadow(0 2px 3px rgba(0,0,0,.45)); }
.sig-cursor svg { display: block; }
.sig-ripple { position: absolute; inset: 0; border-radius: inherit; box-shadow: 0 0 0 2px var(--amber); opacity: 0; }
.sig-toast {
  position: absolute; top: 46px; right: 12px; display: flex; align-items: center; gap: 6px;
  padding: 5px 11px 5px 8px; border-radius: 999px; background: #F3EADB; color: #14110E;
  font-size: 11px; letter-spacing: .02em; box-shadow: 0 8px 18px -8px rgba(0,0,0,.6); opacity: 0;
}
.sig-toast i { width: 7px; height: 7px; border-radius: 50%; background: #2E8B57; }
.sig-toast b { font-weight: 700; }
.sig-note {
  position: absolute; z-index: 2; top: 12px; left: 8px; right: 8px;
  display: flex; align-items: center; gap: 7px; padding: 8px; border-radius: 11px;
  background: rgba(243,234,219,.96); box-shadow: 0 8px 16px -8px rgba(0,0,0,.6); opacity: 0;
}
.sig-note em { display: block; height: 5px; border-radius: 3px; background: rgba(20,17,14,.5); flex: 1; }
.sig-row::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(100deg, transparent 30%, rgba(255,255,255,.2) 50%, transparent 70%);
  transform: translateX(-120%);
}

@media (prefers-reduced-motion: no-preference) {
  .f::before, .f::after, .o9, .o10, .o11, .sig-cursor, .sig-ripple, .sig-toast, .sig-note,
  .sig-row::after, .sig-tabs i:nth-child(-n+2), .f4 {
    animation-duration: var(--loop); animation-iteration-count: infinite;
    animation-timing-function: cubic-bezier(.4, 0, .2, 1); animation-fill-mode: both;
  }
  .f1::before { animation-name: ft-f1; } .f1::after { animation-name: ft-w1; }
  .f2::before { animation-name: ft-f2; } .f2::after { animation-name: ft-w2; }
  .f3::before { animation-name: ft-f3; } .f3::after { animation-name: ft-w3; }
  .f4::before { animation-name: ft-f4; } .f4::after { animation-name: ft-w4; }
  .f5::before { animation-name: ft-f5; } .f5::after { animation-name: ft-w5; }
  .f6::before { animation-name: ft-f6; } .f6::after { animation-name: ft-w6; }
  .f7::before { animation-name: ft-f7; } .f7::after { animation-name: ft-w7; }
  .f8::before { animation-name: ft-f8; } .f8::after { animation-name: ft-w8; }
  .f9::before { animation-name: ft-f9; } .f9::after { animation-name: ft-w9; }
  .f10::before { animation-name: ft-f10; } .f10::after { animation-name: ft-w10; }
  .f11::before { animation-name: ft-f11; } .f11::after { animation-name: ft-w11; }
  .f12::before { animation-name: ft-f12; } .f12::after { animation-name: ft-w12; }
  .o9 { animation-name: ft-o9; } .o10 { animation-name: ft-o10; } .o11 { animation-name: ft-o11; }
  .sig-cursor { animation-name: ft-cursor; }
  .sig-ripple { animation-name: ft-ripple; }
  .f4         { animation-name: ft-press; }
  .sig-toast  { animation-name: ft-toast; }
  .sig-note   { animation-name: ft-note; }
  .sig-row::after { animation-name: ft-sheen; }
  .sig-tabs i:nth-child(1) { animation-name: ft-tab-a; }
  .sig-tabs i:nth-child(2) { animation-name: ft-tab-b; }
  /* the pair arrives once, then the phone breathes */
  .sig-browser { animation: ft-sig-rise 700ms var(--ease-out) 80ms backwards; }
  .sig-device  { animation: ft-sig-rise 700ms var(--ease-out) 420ms backwards, ft-float 6s ease-in-out 1.2s infinite alternate; }
}
@keyframes ft-f1 { 0%, 5% { clip-path: inset(0 100% 0 0); opacity: 1; } 10%, 86% { clip-path: inset(0 0 0 0); opacity: 1; } 91% { clip-path: inset(0 0 0 0); opacity: 0; } 92% { clip-path: inset(0 100% 0 0); opacity: 0; } 93%, 100% { clip-path: inset(0 100% 0 0); opacity: 1; } }
@keyframes ft-w1 { 0%, 6% { opacity: 1; } 10%, 88% { opacity: 0; } 93%, 100% { opacity: 1; } }
@keyframes ft-f2 { 0%, 8% { clip-path: inset(0 100% 0 0); opacity: 1; } 13%, 86% { clip-path: inset(0 0 0 0); opacity: 1; } 91% { clip-path: inset(0 0 0 0); opacity: 0; } 92% { clip-path: inset(0 100% 0 0); opacity: 0; } 93%, 100% { clip-path: inset(0 100% 0 0); opacity: 1; } }
@keyframes ft-w2 { 0%, 9% { opacity: 1; } 13%, 88% { opacity: 0; } 93%, 100% { opacity: 1; } }
@keyframes ft-f3 { 0%, 10% { clip-path: inset(0 100% 0 0); opacity: 1; } 15%, 86% { clip-path: inset(0 0 0 0); opacity: 1; } 91% { clip-path: inset(0 0 0 0); opacity: 0; } 92% { clip-path: inset(0 100% 0 0); opacity: 0; } 93%, 100% { clip-path: inset(0 100% 0 0); opacity: 1; } }
@keyframes ft-w3 { 0%, 11% { opacity: 1; } 15%, 88% { opacity: 0; } 93%, 100% { opacity: 1; } }
@keyframes ft-f4 { 0%, 13% { clip-path: inset(0 100% 0 0); opacity: 1; } 18%, 86% { clip-path: inset(0 0 0 0); opacity: 1; } 91% { clip-path: inset(0 0 0 0); opacity: 0; } 92% { clip-path: inset(0 100% 0 0); opacity: 0; } 93%, 100% { clip-path: inset(0 100% 0 0); opacity: 1; } }
@keyframes ft-w4 { 0%, 14% { opacity: 1; } 18%, 88% { opacity: 0; } 93%, 100% { opacity: 1; } }
@keyframes ft-f5 { 0%, 16% { clip-path: inset(0 100% 0 0); opacity: 1; } 21%, 86% { clip-path: inset(0 0 0 0); opacity: 1; } 91% { clip-path: inset(0 0 0 0); opacity: 0; } 92% { clip-path: inset(0 100% 0 0); opacity: 0; } 93%, 100% { clip-path: inset(0 100% 0 0); opacity: 1; } }
@keyframes ft-w5 { 0%, 17% { opacity: 1; } 21%, 88% { opacity: 0; } 93%, 100% { opacity: 1; } }
@keyframes ft-f6 { 0%, 18% { clip-path: inset(0 100% 0 0); opacity: 1; } 23%, 86% { clip-path: inset(0 0 0 0); opacity: 1; } 91% { clip-path: inset(0 0 0 0); opacity: 0; } 92% { clip-path: inset(0 100% 0 0); opacity: 0; } 93%, 100% { clip-path: inset(0 100% 0 0); opacity: 1; } }
@keyframes ft-w6 { 0%, 19% { opacity: 1; } 23%, 88% { opacity: 0; } 93%, 100% { opacity: 1; } }
@keyframes ft-f7 { 0%, 20% { clip-path: inset(0 100% 0 0); opacity: 1; } 25%, 86% { clip-path: inset(0 0 0 0); opacity: 1; } 91% { clip-path: inset(0 0 0 0); opacity: 0; } 92% { clip-path: inset(0 100% 0 0); opacity: 0; } 93%, 100% { clip-path: inset(0 100% 0 0); opacity: 1; } }
@keyframes ft-w7 { 0%, 21% { opacity: 1; } 25%, 88% { opacity: 0; } 93%, 100% { opacity: 1; } }
@keyframes ft-f8 { 0%, 23% { clip-path: inset(0 100% 0 0); opacity: 1; } 28%, 86% { clip-path: inset(0 0 0 0); opacity: 1; } 91% { clip-path: inset(0 0 0 0); opacity: 0; } 92% { clip-path: inset(0 100% 0 0); opacity: 0; } 93%, 100% { clip-path: inset(0 100% 0 0); opacity: 1; } }
@keyframes ft-w8 { 0%, 24% { opacity: 1; } 28%, 88% { opacity: 0; } 93%, 100% { opacity: 1; } }
@keyframes ft-f9 { 0%, 26% { clip-path: inset(0 100% 0 0); opacity: 1; } 31%, 86% { clip-path: inset(0 0 0 0); opacity: 1; } 91% { clip-path: inset(0 0 0 0); opacity: 0; } 92% { clip-path: inset(0 100% 0 0); opacity: 0; } 93%, 100% { clip-path: inset(0 100% 0 0); opacity: 1; } }
@keyframes ft-w9 { 0%, 27% { opacity: 1; } 31%, 88% { opacity: 0; } 93%, 100% { opacity: 1; } }
@keyframes ft-f10 { 0%, 28% { clip-path: inset(0 100% 0 0); opacity: 1; } 33%, 86% { clip-path: inset(0 0 0 0); opacity: 1; } 91% { clip-path: inset(0 0 0 0); opacity: 0; } 92% { clip-path: inset(0 100% 0 0); opacity: 0; } 93%, 100% { clip-path: inset(0 100% 0 0); opacity: 1; } }
@keyframes ft-w10 { 0%, 29% { opacity: 1; } 33%, 88% { opacity: 0; } 93%, 100% { opacity: 1; } }
@keyframes ft-f11 { 0%, 30% { clip-path: inset(0 100% 0 0); opacity: 1; } 35%, 86% { clip-path: inset(0 0 0 0); opacity: 1; } 91% { clip-path: inset(0 0 0 0); opacity: 0; } 92% { clip-path: inset(0 100% 0 0); opacity: 0; } 93%, 100% { clip-path: inset(0 100% 0 0); opacity: 1; } }
@keyframes ft-w11 { 0%, 31% { opacity: 1; } 35%, 88% { opacity: 0; } 93%, 100% { opacity: 1; } }
@keyframes ft-f12 { 0%, 33% { clip-path: inset(0 100% 0 0); opacity: 1; } 38%, 86% { clip-path: inset(0 0 0 0); opacity: 1; } 91% { clip-path: inset(0 0 0 0); opacity: 0; } 92% { clip-path: inset(0 100% 0 0); opacity: 0; } 93%, 100% { clip-path: inset(0 100% 0 0); opacity: 1; } }
@keyframes ft-w12 { 0%, 34% { opacity: 1; } 38%, 88% { opacity: 0; } 93%, 100% { opacity: 1; } }
@keyframes ft-o9 { 0%, 29% { opacity: 0; } 32%, 86% { opacity: 1; } 91%, 100% { opacity: 0; } }
@keyframes ft-o10 { 0%, 31% { opacity: 0; } 34%, 86% { opacity: 1; } 91%, 100% { opacity: 0; } }
@keyframes ft-o11 { 0%, 33% { opacity: 0; } 36%, 86% { opacity: 1; } 91%, 100% { opacity: 0; } }
@keyframes ft-cursor {
  0%, 37% { opacity: 0; transform: translate(150px, 70px); }
  39%     { opacity: 1; transform: translate(150px, 70px); }
  46%     { opacity: 1; transform: translate(0, 0) scale(1); }
  47.5%   { transform: translate(0, 0) scale(.8); }
  49%     { transform: translate(0, 0) scale(1); }
  56%     { opacity: 1; transform: translate(42px, 34px); }
  60%     { opacity: 0; transform: translate(50px, 40px); }
  100%    { opacity: 0; transform: translate(150px, 70px); }
}
@keyframes ft-press  { 0%, 46.5% { transform: none; } 47.5% { transform: scale(.93); } 49.5%, 100% { transform: none; } }
@keyframes ft-ripple { 0%, 47% { opacity: 0; transform: none; } 47.5% { opacity: .8; transform: none; } 56%, 100% { opacity: 0; transform: scale(1.3, 2); } }
@keyframes ft-toast  { 0%, 48% { opacity: 0; transform: translateY(-8px); } 52%, 86% { opacity: 1; transform: none; } 90%, 100% { opacity: 0; transform: translateY(-4px); } }
@keyframes ft-note   { 0%, 53% { opacity: 0; transform: translateY(-140%); } 57%, 86% { opacity: 1; transform: none; } 90%, 100% { opacity: 0; transform: translateY(-140%); } }
@keyframes ft-sheen  { 0%, 62% { transform: translateX(-120%); } 74%, 100% { transform: translateX(120%); } }
@keyframes ft-tab-a  { 0%, 64% { background: var(--amber); } 66%, 78% { background: rgba(243,234,219,.3); } 80%, 100% { background: var(--amber); } }
@keyframes ft-tab-b  { 0%, 64% { background: rgba(243,234,219,.3); } 66%, 78% { background: var(--amber); } 80%, 100% { background: rgba(243,234,219,.3); } }
@keyframes ft-sig-rise { from { opacity: 0; transform: translateY(16px) scale(.985); } to { opacity: 1; transform: none; } }
@keyframes ft-float  { from { transform: translateY(0); } to { transform: translateY(-6px); } }

/* no motion: show the finished, shipped state and hold it */
@media (prefers-reduced-motion: reduce) {
  .f::before { clip-path: none; }
  .f::after { opacity: 0; }
  .sig-toast, .sig-note { opacity: 1; }
}
@media (max-width: 599px) {
  .signature { padding-block: var(--s4) var(--s6); }
  .sig-browser { width: 74%; }
  .sig-device { width: 34%; bottom: -10px; box-shadow: 0 0 0 6px var(--amber-field), 0 16px 32px -12px rgba(0,0,0,.7); }
  .sig-tile { height: 32px; }
  .sig-page { padding: var(--s3) var(--s3) var(--s4); gap: 9px; }
  .sig-toast { top: 40px; right: 8px; }
}

/* ═══ featured: one compact Pantry panel, no pitch ═══════════════════════
   The copy sets the panel's height; the two phones are positioned, so they
   add none, and they run off the bottom edge instead of stretching it. */
:root { --pantry: #043B2B; --pantry-ink: #E7F2EC; }
.featured { background: var(--paper); }
.case {
  position: relative; display: grid; gap: var(--s4);
  background: var(--pantry); color: var(--pantry-ink);
  border-radius: 24px; padding: var(--s6) var(--s5) 0; overflow: hidden;
}
.case-copy { display: grid; gap: var(--s3); align-content: start; }
.case-name { line-height: 0; margin: 0; }
.case-wordmark { width: auto; height: 38px; }
.case-what { color: rgba(231,242,236,.82); font-size: 17px; line-height: 1.6; max-width: 38ch; text-wrap: pretty; }
.case-chips { display: flex; flex-wrap: wrap; gap: 6px; list-style: none; padding: 0; margin: 0; }
.case-chips li { font-size: 13px; padding: 4px 11px; border-radius: 999px; background: rgba(231,242,236,.1); color: rgba(231,242,236,.9); }
.case-links { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s4); margin-top: var(--s2); }
.btn-pantry { background: var(--pantry-ink); color: var(--pantry); border-color: var(--pantry-ink); }
@media (hover: hover) and (pointer: fine) { .btn-pantry:hover { background: #fff; border-color: #fff; } }
.case-links .plain-link { color: rgba(231,242,236,.85); font-weight: 600; text-underline-offset: 4px; }

.case-shots { position: relative; height: 220px; }
.case-shots .phone {
  position: absolute; top: var(--s4); left: 50%; width: 44%; max-width: 188px; translate: -86% 0;
  box-shadow: 0 0 0 1px rgba(255,255,255,.1), 0 -10px 40px -14px rgba(0,0,0,.7);
  transition: transform 520ms var(--ease-out);
}
.case-shots .phone-back { width: 38%; max-width: 164px; top: calc(var(--s4) + 34px); translate: 6% 0; opacity: .9; z-index: 0; }
.case-shots .phone:not(.phone-back) { z-index: 1; }
@media (hover: hover) and (pointer: fine) {
  .case:hover .case-shots .phone { transform: translateY(-10px); }
  .case:hover .case-shots .phone-back { transform: translateY(-5px); }
}
@media (min-width: 900px) {
  .case {
    grid-template-columns: minmax(0, 1fr) minmax(0, .82fr); align-items: center;
    padding: var(--s6) var(--s7); column-gap: var(--s6);
  }
  /* stretch over the panel's own padding so the phones meet its edges */
  .case-shots { height: auto; align-self: stretch; margin-block: calc(var(--s6) * -1); min-height: 300px; }
  .case-shots .phone { top: calc(var(--s6) + var(--s3)); width: 50%; max-width: 196px; }
  .case-shots .phone-back { top: calc(var(--s6) + var(--s3) + 40px); width: 43%; max-width: 170px; }
}
@media (prefers-reduced-motion: reduce) { .case-shots .phone { transition: none; } }

/* ═══ offers: cards that open on hover (not clickable) ═════════════════ */
.offers { display: grid; gap: var(--s4); margin-top: var(--s6); list-style: none; padding: 0; }
.offer {
  position: relative; border-radius: 20px; padding: var(--s5);
  background: var(--tile); border: 1px solid var(--paper-edge);
  display: grid; align-content: start;
  transition: background-color var(--dur-state) var(--ease-quick),
              border-color var(--dur-state) var(--ease-quick),
              transform var(--dur-state) var(--ease-quick),
              box-shadow var(--dur-state) var(--ease-quick);
}
.offer h3 { margin-bottom: var(--s2); }
.offer-line { color: var(--ink-soft); max-width: 40ch; text-wrap: pretty; }

.offer-more p { padding-top: var(--s4); color: var(--ink-soft); font-size: 16px; line-height: 1.6; }
.offer-more ul { display: flex; flex-wrap: wrap; gap: 6px 8px; padding-top: var(--s4); list-style: none; }
.offer-more li {
  font-size: 13.5px; padding: 4px 11px; border-radius: 999px;
  background: rgba(44,37,33,.08); color: var(--ink-soft);
}

/* the cue: without it a hover-to-open card looks like a card with nothing in it */
.offer-cue {
  display: flex; align-items: center; gap: var(--s2);
  margin-top: var(--s4); font-size: 13px; letter-spacing: .07em;
  text-transform: uppercase; color: var(--ink-soft);
  transition: color var(--dur-state) var(--ease-quick), opacity 200ms var(--ease-out);
}
.offer-cue i {
  position: relative; width: 13px; height: 13px; flex: none;
  transition: transform 220ms var(--ease-out);
}
.offer-cue i::before, .offer-cue i::after {
  content: ""; position: absolute; inset: 50% 0 auto; height: 1.5px;
  background: currentColor; transform: translateY(-50%);
}
.offer-cue i::after { transform: translateY(-50%) rotate(90deg); }

/* no hover on a touch screen: the detail is simply shown rather than hidden
   behind a gesture nobody can make, and the cue that would then be a lie goes.
   The exact complement of the pointer query below, so every device gets one
   of the two and none gets a panel it can't open. */
@media not all and (hover: hover) and (pointer: fine) {
  .offer-cue { display: none; }
}
@media (prefers-reduced-motion: reduce) {
  .offer, .offer-more, .offer-cue, .offer-cue i { transition: none; }
}

/* ═══ process sits with the offers, not a field away ═════════════════════ */
.process { margin-top: var(--s7); padding-top: var(--s6); border-top: 2px solid var(--ink); }
.stages { display: grid; gap: var(--s5); margin-top: var(--s4); list-style: none; padding: 0; }
.stage { display: grid; gap: var(--s1); }
.stage-n { font-family: var(--serif); font-size: 14px; color: var(--amber-deep); letter-spacing: .06em; }
.stage p { color: var(--ink-soft); max-width: 40ch; font-size: 16px; text-wrap: pretty; }
.standard { margin-top: var(--s6); font-size: 15.5px; color: var(--ink-soft); max-width: 64ch; text-wrap: pretty; }
.standard strong { color: var(--ink); }

@media (min-width: 900px) {
  .offers { grid-template-columns: repeat(3, minmax(0, 1fr)); align-items: start; }
  .stages { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@media (prefers-color-scheme: dark) {
  .case { background: #062A1F; }
  .offer { border-color: #3A3128; }
  .offer-more li { background: rgba(255,255,255,.07); }
  .stage-n { color: var(--amber); }
  .offer-cue { color: var(--ink-soft); }
}

/* featured and work share a background, so two full band paddings read as a
   dead field between them. One gap is enough. */
.featured { padding-bottom: var(--s6); }
.work { padding-top: var(--s6); }
.process { margin-top: var(--s6); }
.stage { align-content: start; }
.offers { margin-top: var(--s5); }
/* the old slab clipped its photo; the signature's shadows and halo need to spill */
.hero .slab { overflow: visible; }

/* With a mouse, the detail drops over the content below instead of pushing it:
   an in-flow open on hover shoves "How it goes" down under the pointer and
   snaps it back up on leave.
   The card answers the pointer at once (colour, lift) but only OPENS after
   120ms of staying, so a pointer crossing the row on its way somewhere else
   doesn't flash panels over "How it goes"; leaving closes it instantly. The
   panel is laid out at full size from the start and wiped open with
   clip-path, so nothing reflows while it moves; the negative side and bottom
   insets leave room for its shadow. The words fade in a beat behind. */
@media (hover: hover) and (pointer: fine) {
  .offer {
    z-index: 1;
    transition: background-color var(--dur-state) var(--ease-quick),
                border-color var(--dur-state) var(--ease-quick),
                transform var(--dur-state) var(--ease-quick),
                box-shadow var(--dur-state) var(--ease-quick),
                border-bottom-color 0s, border-bottom-left-radius 0s, border-bottom-right-radius 0s;
  }
  .offer:hover {
    z-index: 4; background: var(--paper); border-color: var(--ink);
    transform: translateY(-3px); box-shadow: var(--lift-paper);
    /* the bottom edge opens up for the panel only when the panel comes */
    border-bottom-left-radius: 0; border-bottom-right-radius: 0;
    border-bottom-color: transparent;
    transition-delay: 0s, 0s, 0s, 0s, 120ms, 120ms, 120ms;
  }
  .offer-more {
    position: absolute; left: -1px; right: -1px; top: calc(100% - 1px);
    visibility: hidden;
    background: var(--paper); border: 1px solid var(--ink); border-top: 0;
    border-radius: 0 0 20px 20px;
    box-shadow: 0 22px 34px -20px rgba(28,22,16,.4);
    clip-path: inset(0 -40px 100% -40px);
    transition: clip-path 0s, visibility 0s;
  }
  .offer:hover .offer-more {
    visibility: visible; clip-path: inset(0 -40px -64px -40px);
    transition: clip-path 220ms var(--ease-out) 120ms, visibility 0s 120ms;
  }
  .offer-inner { padding-inline: var(--s5); opacity: 0; transition: opacity 0s; }
  .offer:hover .offer-inner { opacity: 1; transition: opacity 200ms var(--ease-out) 170ms; }
  .offer-more ul { padding-bottom: var(--s5); }
  .offer:hover .offer-cue { color: var(--amber-deep); }
  .offer:hover .offer-cue i { transform: rotate(135deg); transition-delay: 120ms; }
}
@media (hover: hover) and (pointer: fine) and (prefers-color-scheme: dark) {
  .offer:hover { background: #241E19; border-color: #5A4B37; }
  .offer:hover .offer-cue { color: var(--amber); }
  .offer-more { background: #241E19; border-color: #5A4B37; box-shadow: 0 22px 34px -20px rgba(0,0,0,.75); }
}

/* ═══ contact form: no JavaScript, so the browser does the validating ════ */
/* The band is near-black in both themes, so the controls use fixed colours
   rather than tokens that flip. */
.contact-grid { display: grid; gap: var(--s6); align-items: start; }
.contact > .wrap > .contact-grid { animation: none; }
.contact-intro .minor-note { max-width: 40ch; }

.enquiry {
  --f-text: #F3EADB; --f-soft: #A89A84; --f-line: #4A3F33; --f-fill: rgba(255,255,255,.045);
  display: grid; gap: var(--s4);
  background: #17120D; border: 1px solid #2E261D; border-radius: 22px;
  padding: var(--s5); color: var(--f-text);
}
.q { border: 0; margin: 0; padding: 0; min-width: 0; }
.q legend, .field-label {
  display: block; padding: 0; margin-bottom: var(--s2);
  font-size: 15px; font-weight: 600; color: var(--f-text);
}
.field-label em, .q legend em { font-style: normal; font-weight: 400; color: var(--f-soft); margin-left: 4px; font-size: 13.5px; }
.field { display: grid; min-width: 0; }
.field-hint { margin: -4px 0 var(--s2); font-size: 14px; line-height: 1.5; color: var(--f-soft); }
.pair { display: grid; gap: var(--s4); }

.field input, .field textarea, .select select {
  width: 100%; font: inherit; font-size: 16px; color: var(--f-text);   /* 16px: iOS zooms below it */
  background: var(--f-fill); border: 1px solid var(--f-line); border-radius: 12px;
  padding: 12px 14px; line-height: 1.4;
  transition: border-color var(--dur-state) var(--ease-quick), background-color var(--dur-state) var(--ease-quick);
}
.field textarea { resize: vertical; min-height: 9.5em; }
@media (hover: hover) and (pointer: fine) { .field input:hover, .field textarea:hover, .select select:hover { border-color: #6A5A44; } }
.field input:focus, .field textarea:focus, .select select:focus {
  outline: none; border-color: var(--amber); background: rgba(255,255,255,.07);
  box-shadow: 0 0 0 3px rgba(241,174,72,.22);
}
/* only after someone has actually touched a field, never on page load */
.field input:user-invalid, .field textarea:user-invalid, .select select:user-invalid { border-color: #E07A5F; }

/* selects: drawn chevron, since the CSP will not load a data: image */
.select { position: relative; display: block; }
.select select { appearance: none; -webkit-appearance: none; padding-right: 40px; cursor: pointer; }
.select select option { color: #1B1510; background: #F3EADB; }
.select::after {
  content: ""; position: absolute; right: 16px; top: 50%; width: 8px; height: 8px;
  border-right: 2px solid var(--f-soft); border-bottom: 2px solid var(--f-soft);
  transform: translateY(-70%) rotate(45deg); pointer-events: none;
}

/* the first question is chips, so the form starts with a tap, not a blank box */
.pills { display: flex; flex-wrap: wrap; gap: 8px; }
.pills label { position: relative; cursor: pointer; }
.pills input { position: absolute; inset: 0; opacity: 0; margin: 0; cursor: pointer; }
.pills span {
  display: inline-block; padding: 9px 16px; border-radius: 999px; font-size: 15px;
  border: 1px solid var(--f-line); background: var(--f-fill); color: var(--f-text);
  transition: background-color var(--dur-state) var(--ease-quick), border-color var(--dur-state) var(--ease-quick),
              color var(--dur-state) var(--ease-quick), transform var(--dur-state) var(--ease-quick);
}
@media (hover: hover) and (pointer: fine) { .pills label:hover span { border-color: #6A5A44; } }
.pills input:checked + span { background: var(--amber); border-color: var(--amber); color: #1B1510; font-weight: 600; }
.pills input:focus-visible + span { outline: 2px solid var(--amber); outline-offset: 3px; }
.pills input:active + span { transform: scale(.97); }

/* the trap: off-screen for people, still a real field for a bot */
.hp { position: absolute; left: -10000px; width: 1px; height: 1px; overflow: hidden; }

.send-row { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s3) var(--s4); margin-top: var(--s2); }
.send-row .btn { border: 0; cursor: pointer; font: inherit; font-weight: 600; }
.send-row p { font-size: 13.5px; color: var(--f-soft); }

@media (min-width: 600px) {
  .pair { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .enquiry { padding: var(--s6); }
}
@media (min-width: 900px) {
  .contact-grid { grid-template-columns: minmax(0, .8fr) minmax(0, 1.2fr); gap: var(--s7); }
  .contact-intro { position: sticky; top: 120px; }
}
@media (prefers-reduced-motion: reduce) {
  .pills span, .field input, .field textarea, .select select { transition: none; }
}

/* ── the two static pages the form lands on ── */
.landed { min-height: 62vh; display: grid; align-content: center; }
.landed h1 { font-size: clamp(40px, 6vw, 72px); max-width: 14ch; }
.landed .lede { margin-top: var(--s4); max-width: 42ch; color: var(--ink-soft); }
.landed .btn-row { margin-top: var(--s5); }
.field input::placeholder { color: #7A6C58; }

/* section labels that are real headings keep the label look */
h2.eyebrow { font-family: var(--sans); font-weight: 500; font-size: 13px; line-height: 1.4; letter-spacing: .1em; }
