/* ==========================================================================
   Hayanezu — poster.css
   The poster collage. Replaces editorial.css, which replaced chapters.css.

   WHY THIS EXISTS
   The deck was a slide viewer bolted onto a page. The editorial run that
   replaced it fixed the shuffling but went too quiet: a headword at 5rem, a
   paragraph at 17px thirty centimetres to its right, and a great deal of
   nothing in between. Sparse is not the same as considered.

   WHAT IT IS NOW
   Full-bleed bands, one per movement, alternating ground: near-black, bone,
   and the accent at full strength with dark type on it. Body copy set LARGE —
   the density here comes from type size, not from filling space. Numerals
   outlined rather than filled, bleeding off the band edge. An extruded frame
   stack and a couple of loose dots per band. Punchlines bold inline, key links
   underlined with a drawn wave.

   Two references drove it: a poster-collage site that alternates white / grey
   / black grounds with very large warm body copy and strikethrough jokes, and
   a black-ground studio site with an iridescent gradient sheen. The sheen here
   is a CSS gradient on a long slow transform, not a video — this site claims
   static-first and 0 third-party requests, and a 4MB loop would make both a
   lie.

   MOTION RULES, UNCHANGED
   Nothing reads scrollY. Entry rides main.js's [data-reveal] machinery via the
   .stg class, so there is one reveal system and one reduced-motion early
   return. The only scroll-linked values are two custom properties written by
   poster.js from the existing HZ.scroll subscription, both spent on
   transforms.
   ========================================================================== */

/* --- 1. Band grounds ------------------------------------------------------
   Three rooms. --p-fg and --p-dim are what everything inside a band paints
   with, so a band can change ground without a single rule below it knowing.
   The accent room reuses --shade on the accent, which is the pair the solid
   button already uses and check-contrast.js already measures at 5:1. */

.scroll-chapter .story__aside,
.scroll-chapter .story-card,
.promises--run > .promise {
  --p-bg: #14161B;
  --p-fg: #FFFFFF;
  --p-dim: rgb(255 255 255 / 0.76);
  --p-line: rgb(255 255 255 / 0.20);
  --p-hot: #A8B0F0;
  --p-sheen: 0.5;
}

.is-bone {
  --p-bg: #EFECE4;
  --p-fg: #16181B;
  --p-dim: rgb(22 24 27 / 0.78);
  --p-line: rgb(22 24 27 / 0.20);
  --p-sheen: 0.22;
}

.is-pop {
  --p-bg: var(--p-pop);
  --p-fg: var(--shade);
  --p-dim: rgb(var(--shade-rgb) / 0.82);
  --p-line: rgb(var(--shade-rgb) / 0.30);
  --p-sheen: 0;
}

/* --- 2. The chapter, as a stack of bands ----------------------------------
   The two-column split is gone. A sticky heading beside a scrolling column is
   a magazine layout; this is a poster sequence, and every part of it gets the
   full width. */

.scroll-chapter { position: relative; padding: 0; }
.scroll-chapter .wrap { max-width: none; padding: 0; }
.scroll-chapter .story { display: block; }
.scroll-chapter .story-deck,
.scroll-chapter .story-deck__viewport,
.scroll-chapter .story-deck__track {
  display: block;
  position: static;
  overflow: visible;
  min-width: 0;
  height: auto;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  transform: none;
}

/* The band itself. 100vw out of a centred column, then padded back to the
   text measure — the standard full-bleed escape, safe here because <html>
   carries overflow-x: clip so the extra cannot open a scrollbar. */
.scroll-chapter .story__aside,
.scroll-chapter .story-card {
  position: relative;
  isolation: isolate;
  width: 100vw;
  margin-left: calc(50% - 50vw);
  padding: clamp(52px, 7vw, 112px) max(var(--gutter), calc(50vw - 640px));
  border: 0;
  border-radius: 0;
  box-shadow: none;
  overflow: hidden;
  background: var(--p-bg);
  color: var(--p-fg);
  /* --py and --pr are written by poster.js. */
  --py: 0;
  --pr: 0;
}

/* The iridescent veil. One wide gradient, larger than the band, moved by the
   same scroll value the numerals use — so a band lit like this shifts hue
   across its own width as it passes rather than sitting there as a static
   wash. Zero opacity in the accent room, where the ground is already the
   colour. */
.scroll-chapter .story__aside::before,
.scroll-chapter .story-card::before {
  content: "";
  position: absolute;
  z-index: -1;
  inset: -40% -20%;
  pointer-events: none;
  opacity: calc(var(--p-sheen) * 0.5);
  background:
    radial-gradient(38% 60% at 18% 30%, var(--accent), transparent 70%),
    radial-gradient(42% 55% at 78% 66%, var(--accent-2, var(--accent)), transparent 72%);
  filter: blur(38px) saturate(1.3);
  transform: translate3d(0, calc(var(--py) * 1.6px), 0);
}

/* --- 3. The chapter title band -------------------------------------------- */

.scroll-chapter .story__aside {
  --p-bg: var(--p-pop);
  --p-fg: var(--shade);
  --p-dim: rgb(var(--shade-rgb) / 0.82);
  --p-line: rgb(var(--shade-rgb) / 0.30);
  --p-sheen: 0;
  display: grid;
  gap: clamp(18px, 2.4vw, 34px);
  align-content: center;
  min-height: clamp(240px, 34vh, 400px);
}

.scroll-chapter .story__index {
  display: inline-block;
  width: fit-content;
  padding: 6px 12px 5px;
  border: 2px solid currentColor;
  font: 700 var(--t-label)/1 var(--font-mono);
  letter-spacing: 0.24em;
  text-indent: 0.24em;
  color: var(--p-fg);
}

.scroll-chapter .story__aside > .display {
  max-width: 18ch;
  margin: 0;
  color: var(--p-fg);
  font-size: clamp(2.25rem, 6.4vw, 5rem);
  font-weight: 700;
  line-height: 0.98;
  letter-spacing: -0.035em;
  text-wrap: balance;
}

/* --- 4. The movements ------------------------------------------------------
   Alternating rooms, and the last band of every chapter takes the accent so a
   sequence ends on colour rather than trailing off. */

.scroll-chapter .story-card:nth-child(even) {
  --p-bg: #EFECE4;
  --p-fg: #16181B;
  --p-dim: rgb(22 24 27 / 0.78);
  --p-line: rgb(22 24 27 / 0.20);
  --p-sheen: 0.22;
}
.scroll-chapter .story-card:last-child {
  --p-bg: var(--p-pop);
  --p-fg: var(--shade);
  --p-dim: rgb(var(--shade-rgb) / 0.82);
  --p-line: rgb(var(--shade-rgb) / 0.30);
  --p-sheen: 0;
}

.scroll-chapter .story-card__content {
  position: relative;
  z-index: 1;
  max-width: none;
}

/* The extruded frame. Two outlined rectangles stepped back from the content,
   the motif the reference uses behind every image. Drawn on the band's own
   pseudo-elements, so it costs no markup, and hidden on narrow screens where
   there is no room to step anything back. */
.scroll-chapter .story-card::after {
  content: "";
  position: absolute;
  z-index: 0;
  right: clamp(-30px, -1.6vw, -8px);
  bottom: clamp(-30px, -2vw, -10px);
  width: clamp(170px, 22vw, 340px);
  aspect-ratio: 3 / 2;
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 300 200' fill='none' stroke='%23888' stroke-width='1.4'%3E%3Crect x='60' y='6' width='233' height='150'/%3E%3Crect x='33' y='23' width='233' height='150'/%3E%3Crect x='6' y='40' width='233' height='150'/%3E%3Cpath d='M6 40 60 6M239 40 293 6M6 190 60 156M239 190 293 156'/%3E%3C/svg%3E") center / contain no-repeat;
  opacity: 0.5;
  pointer-events: none;
  transform: translate3d(0, calc(var(--py) * -0.5px), 0) rotate(calc(var(--pr) * 1deg));
}

/* --- 5. The numeral -------------------------------------------------------
   Outlined, not filled — the wireframe language the references keep returning
   to — and set to run off the top edge of its band. */

.scroll-chapter .story-card__number {
  position: absolute;
  z-index: 0;
  top: clamp(-34px, -2vw, -8px);
  right: max(var(--gutter), calc(50vw - 640px));
  margin: 0;
  font-family: var(--font-display);
  font-size: clamp(5rem, 15vw, 14rem);
  font-weight: 700;
  line-height: 0.78;
  letter-spacing: -0.06em;
  color: transparent;
  -webkit-text-stroke: 1.5px var(--p-line);
  pointer-events: none;
  user-select: none;
  will-change: transform;
  transform: translate3d(0, calc(var(--py) * 1px), 0);
}

/* The loose dots. One ring in the hot accent, one solid — the punctuation both
   references scatter through their compositions. */
.scroll-chapter .story-card__number::before,
.scroll-chapter .story-card__number::after {
  content: "";
  position: absolute;
  border-radius: 50%;
}
.scroll-chapter .story-card__number::before {
  top: 78%;
  left: -46px;
  width: 16px;
  height: 16px;
  border: 2.5px solid var(--p-hot);
}
.scroll-chapter .story-card__number::after {
  top: 96%;
  left: -22px;
  width: 8px;
  height: 8px;
  background: var(--p-fg);
  opacity: 0.7;
}

/* --- 6. The type ----------------------------------------------------------
   Large. This is the correction to the version before: the reference sets its
   running copy at something close to a heading size, and that is what makes a
   plain paragraph hold a full-bleed band on its own. */

.scroll-chapter .story-card p {
  margin: 0;
  max-width: min(40ch, 100%);
  color: var(--p-dim);
  font-size: clamp(1.1875rem, 1.9vw, 1.75rem);
  line-height: 1.48;
  letter-spacing: -0.015em;
}
.scroll-chapter .story-card p + p { margin-top: var(--s-6); }

/* The opening statement of a chapter, at poster size. */
.scroll-chapter .story-card:first-child .story-card__content > p:first-child {
  max-width: min(24ch, 100%);
  color: var(--p-fg);
  font-weight: 700;
  font-size: clamp(1.875rem, 4.6vw, 3.75rem);
  line-height: 0.98;
  letter-spacing: -0.04em;
  text-wrap: balance;
}

/* The punchline device: a bold run inside a paragraph, in full ink rather than
   the dimmed body colour. */
.scroll-chapter .story-card strong,
.promises--run .promise strong { color: var(--p-fg); font-weight: 700; }

.scroll-chapter .story-card p.micro {
  max-width: min(52ch, 100%);
  color: var(--p-dim);
  opacity: 0.72;
  font-size: var(--t-label);
  letter-spacing: 0.14em;
  line-height: 1.7;
}

.scroll-chapter .story-card h3 {
  margin: var(--s-6) 0 var(--s-3);
  color: var(--p-fg);
  font: 700 clamp(1.25rem, 1.8vw, 1.5rem)/1.2 var(--font-display);
  letter-spacing: -0.025em;
}
.scroll-chapter .story-card__content > :first-child { margin-top: 0; }

/* --- 7. Links, with the drawn wave ---------------------------------------- */

.scroll-chapter .story-card .link,
.promises--run .promise .link {
  color: var(--p-fg);
  background: none;
  font-weight: 600;
  text-decoration: none;
  padding-bottom: 6px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='6' viewBox='0 0 24 6'%3E%3Cpath d='M0 3c2-3 4-3 6 0s4 3 6 0 4-3 6 0 4 3 6 0' fill='none' stroke='%23A8B0F0' stroke-width='1.6'/%3E%3C/svg%3E");
  background-repeat: repeat-x;
  background-position: 0 100%;
  background-size: 24px 6px;
  transition: background-position 320ms var(--e-out), color 200ms var(--e-out);
}
.scroll-chapter .story-card .link:hover,
.promises--run .promise .link:hover { background-position: 12px 100%; }

.scroll-chapter .story-card :focus-visible,
.promises--run :focus-visible { outline: 3px solid var(--p-fg); outline-offset: 4px; }

/* --- 8. Lists -------------------------------------------------------------
   Questions and credentials as a ruled run at body size. */

.scroll-chapter .story-card :is(.qs, .creds) {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 0;
  margin: var(--s-6) 0 0;
  max-width: min(34ch, 100%);
}
.scroll-chapter .story-card :is(.qs, .creds) > li {
  position: relative;
  margin: 0;
  padding: var(--s-5) 0 var(--s-5) var(--s-7);
  border: 0;
  border-top: 1px solid var(--p-line);
  color: var(--p-fg);
  font-size: clamp(1.0625rem, 1.5vw, 1.3125rem);
  line-height: 1.4;
}
.scroll-chapter .story-card :is(.qs, .creds) > li:first-child { border-top: 0; padding-top: 0; }
.scroll-chapter .story-card :is(.qs, .creds) > li::before {
  content: "";
  position: absolute;
  left: 0;
  top: calc(var(--s-5) + 0.55em);
  width: 26px;
  height: 2px;
  background: var(--p-hot);
}
.scroll-chapter .story-card :is(.qs, .creds) > li:first-child::before { top: 0.55em; }

/* The refusals were briefly struck through, one clause each. It read as the
   promise being cancelled rather than the practice being refused, so the
   device is gone and the accent rule at the head of each line carries the
   emphasis on its own. */

/* --- 9. The promises, as poster panels ------------------------------------
   A deliberate composition on twelve columns rather than an auto grid: wide,
   narrow, narrow, wide, so the run reads as a laid-out page. An odd last panel
   takes the full width and closes the block. */

.promises.promises--run,
.promises.promises--run:has(> :last-child:nth-child(4)),
.promises.promises--run:has(> :last-child:nth-child(5)),
.promises.promises--run:has(> :last-child:nth-child(6)) {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  gap: clamp(10px, 1.2vw, 18px);
  margin: clamp(28px, 4vw, 56px) 0 0;
  padding: 0;
  overflow: visible;
  counter-reset: promise;
}

.promises--run > .promise {
  position: relative;
  isolation: isolate;
  display: flex;
  flex-direction: column;
  gap: clamp(12px, 1.6vw, 24px);
  min-height: clamp(190px, 20vw, 280px);
  margin: 0;
  padding: clamp(26px, 3vw, 46px);
  border: 0;
  border-radius: 0;
  box-shadow: none;
  overflow: hidden;
  background: var(--p-bg);
  color: var(--p-fg);
  counter-increment: promise;
  translate: none;
  scale: none;
  filter: none;
  opacity: 1;
  transition: background-color 420ms var(--e-out), color 420ms var(--e-out);
}

.promises--run > .promise:nth-child(4n + 1) { grid-column: span 7; }
.promises--run > .promise:nth-child(4n + 2) { grid-column: span 5; }
.promises--run > .promise:nth-child(4n + 3) { grid-column: span 5; }
.promises--run > .promise:nth-child(4n + 4) { grid-column: span 7; }
.promises--run > .promise:last-child:nth-child(odd) { grid-column: 1 / -1; }

/* The rooms cycle on three while the widths cycle on four, so the pattern does
   not repeat until the twelfth panel — which no list on this site reaches. */
.promises--run > .promise:nth-child(3n + 2) {
  --p-bg: #EFECE4;
  --p-fg: #16181B;
  --p-dim: rgb(22 24 27 / 0.78);
  --p-line: rgb(22 24 27 / 0.20);
}
.promises--run > .promise:nth-child(3n) {
  --p-bg: var(--p-pop);
  --p-fg: var(--shade);
  --p-dim: rgb(var(--shade-rgb) / 0.82);
  --p-line: rgb(var(--shade-rgb) / 0.30);
}

.promises--run > .promise::before { content: none; }

/* The outlined index, top right, from the counter — no markup, no copy. */
.promises--run .promise__num {
  position: absolute;
  z-index: 0;
  top: auto;   /* main.css pins it to the top; a grid row stretches its short
                  panels, so the figure is better used filling the foot. */
  bottom: clamp(-8px, -0.5vw, 0px);
  right: clamp(12px, 1.6vw, 26px);
  display: block;
  font: 700 clamp(3rem, 5.6vw, 5rem)/1 var(--font-display);
  letter-spacing: -0.05em;
  color: transparent;
  -webkit-text-stroke: 2px var(--p-line);
  opacity: 0.9;
  pointer-events: none;
  transition: -webkit-text-stroke-color 420ms var(--e-out);
}
.promises--run .promise__num::after { content: counter(promise, decimal-leading-zero); }

.promises--run .promise__word {
  position: relative;
  z-index: 1;
  margin: 0;
  color: var(--p-fg);
  font-family: var(--font-display);
  font-size: clamp(1.875rem, 3.4vw, 3rem);
  font-weight: 700;
  line-height: 0.96;
  letter-spacing: -0.04em;
  background: none;
  -webkit-text-fill-color: currentColor;
  /* One word, never broken: main.css sets overflow-wrap: break-word on every
     paragraph in a section, which split "Protected." before its full stop. */
  overflow-wrap: normal;
  word-break: keep-all;
  hyphens: none;
}

/* The letter shuffle. poster.js wraps each character so they can leave at
   different moments; the whole word stays in the accessibility tree because
   the spans are aria-hidden and a .visually-hidden copy carries the text. */
.promises--run .promise__word .ch {
  display: inline-block;
  transition: transform 420ms var(--e-out), color 260ms var(--e-out);
  transition-delay: calc(var(--c, 0) * 26ms);
}
.promises--run .promise__word .ch--space { width: 0.28em; }

.promises--run .promise p + p {
  position: relative;
  z-index: 1;
  margin: 0;
  max-width: min(30ch, 100%);
  color: var(--p-dim);
  font-size: clamp(1.125rem, 1.55vw, 1.4375rem);
  line-height: 1.45;
}

/* The panel answers the pointer: it flips into the accent room, the outlined
   index brightens, and the word's letters step up one after another. */
@media (hover: hover) and (pointer: fine) {
  .promises--run > .promise:hover {
    --p-bg: var(--p-pop);
    --p-fg: var(--shade);
    --p-dim: rgb(var(--shade-rgb) / 0.82);
    --p-line: rgb(var(--shade-rgb) / 0.36);
  }
  .promises--run > .promise:hover .promise__num { -webkit-text-stroke-color: rgb(var(--shade-rgb) / 0.55); }
  .promises--run > .promise:hover .promise__word .ch { transform: translateY(-0.09em); }
  .promises--run:has(.promise:hover) .promise:not(:hover) { opacity: 1; filter: none; scale: none; }
}

/* --- 10. Entry ------------------------------------------------------------
   main.js gives every band and panel .stg and adds .is-in to the section when
   it arrives. That base is a 16px rise; a poster wants to be uncovered, so the
   rise is joined by a wipe and a degree of tilt that settles out. */

.scroll-chapter .stg,
.promises--run > .promise.stg {
  clip-path: inset(0 0 100% 0);
  transform: translateY(26px) rotate(-0.5deg);
  transition: opacity 620ms var(--e-out),
              transform 760ms var(--e-soft),
              clip-path 760ms var(--e-soft);
}
.scroll-chapter.is-in .stg,
[data-reveal].is-in .promises--run > .promise.stg {
  clip-path: inset(0 0 0 0);
  transform: none;
}

/* --- 11. Narrow ----------------------------------------------------------- */

@media (max-width: 899px) {
  .scroll-chapter .story__aside,
  .scroll-chapter .story-card { padding-block: clamp(40px, 9vw, 64px); }
  .scroll-chapter .story-card::after { display: none; }
  .scroll-chapter .story-card__number {
    position: static;
    display: block;
    margin-bottom: clamp(8px, 2vw, 18px);
    font-size: clamp(3.5rem, 18vw, 6rem);
  }
  .scroll-chapter .story-card__number::before { top: 12%; left: auto; right: -4px; }
  .scroll-chapter .story-card__number::after { top: 74%; left: auto; right: -22px; }
  .scroll-chapter .story-card p { font-size: clamp(1.125rem, 4.4vw, 1.375rem); }
  .promises--run > .promise,
  .promises--run > .promise:nth-child(4n + 1),
  .promises--run > .promise:nth-child(4n + 2),
  .promises--run > .promise:nth-child(4n + 3),
  .promises--run > .promise:nth-child(4n + 4),
  .promises--run > .promise:last-child:nth-child(odd) { grid-column: 1 / -1; }
  .promises--run > .promise { min-height: 0; }
}

/* --- 12. Reduced motion, print -------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  /* main.js's reveal early-returns here, so .stg is never applied and nothing
     needs un-hiding. What is left is the parallax, which poster.js also
     declines to start, and the two gestures that are not scroll-driven. */
  .scroll-chapter .story-card__number,
  .scroll-chapter .story-card::after,
  .scroll-chapter .story__aside::before,
  .scroll-chapter .story-card::before { transform: none; }
  .promises--run .promise__word .ch { transition: none; }
}

@media print {
  .scroll-chapter .story__aside,
  .scroll-chapter .story-card,
  .promises--run > .promise {
    width: auto;
    margin-left: 0;
    padding: 12px 0;
    background: none;
    color: #000;
    break-inside: avoid;
  }
  .scroll-chapter .story-card__number,
  .scroll-chapter .story-card::after,
  .scroll-chapter .story-card::before,
  .promises--run .promise__num { display: none; }
}

/* ==========================================================================
   13. The poster title band
   Work and Contact open on one, in place of a hero that was a heading floating
   over a gradient. Same furniture as a chapter title: the boxed index, the
   headline at poster size, the whole thing full-bleed.
   ========================================================================== */

.poster-head {
  position: relative;
  isolation: isolate;
  width: 100vw;
  margin-left: calc(50% - 50vw);
  padding: clamp(88px, 12vw, 180px) max(var(--gutter), calc(50vw - 640px)) clamp(52px, 7vw, 104px);
  min-height: 0;
  background: var(--p-pop);
  color: var(--shade);
  overflow: hidden;
}
/* Contact is the inverted room — its page ground is already the accent, so a
   band in the same colour would be a band you cannot see. There the title
   takes the graphite instead and the page does the shouting. */
html.is-inverse .poster-head {
  background: #14161B;
  color: #FFFFFF;
}

.poster-head .page-hero__copy,
.poster-head .wrap { max-width: none; padding: 0; }

.poster-head .label {
  display: inline-block;
  width: fit-content;
  margin: 0 0 clamp(16px, 2.2vw, 28px);
  padding: 6px 12px 5px;
  border: 2px solid currentColor;
  color: inherit;
  font: 700 var(--t-label)/1 var(--font-mono);
  letter-spacing: 0.24em;
  text-indent: 0.24em;
}

.poster-head .display {
  max-width: 16ch;
  margin: 0;
  color: inherit;
  font-size: clamp(2.5rem, 8vw, 6.5rem);
  font-weight: 700;
  line-height: 0.94;
  letter-spacing: -0.04em;
  text-wrap: balance;
}

.poster-head .lede {
  margin-top: clamp(20px, 2.6vw, 34px);
  max-width: min(38ch, 100%);
  color: inherit;
  opacity: 0.82;
  font-size: clamp(1.125rem, 1.7vw, 1.5rem);
  line-height: 1.45;
}

/* The hero's sky, aurora and cue belong to the gradient hero this replaces. */
.poster-head .page-hero__sky,
.poster-head .shot-rule,
.poster-head .page-hero__cue { display: none; }

/* The tesseract on Contact stays, as the band's one piece of line art —
   outlined geometry is the poster language, not a departure from it. */
.poster-head .page-art {
  position: absolute;
  z-index: -1;
  right: -4%;
  top: 50%;
  translate: 0 -50%;
  width: clamp(220px, 32vw, 460px);
  opacity: 0.28;
  color: currentColor;
}
@media (max-width: 899px) { .poster-head .page-art { display: none; } }

/* ==========================================================================
   14. Work — one band per project
   The three projects were a quiet list on one ground. Each is now a band with
   its own room, the booklet on one side and the copy on the other, sides
   swapping down the page.
   ========================================================================== */

.poster-work { padding: 0; }
.poster-work > .wrap { max-width: none; padding: 0; }
.poster-work .projects { display: block; margin: 0; counter-reset: proj; }

.poster-work .project {
  --p-bg: #14161B;
  --p-fg: #FFFFFF;
  --p-dim: rgb(255 255 255 / 0.76);
  --p-line: rgb(255 255 255 / 0.20);
  --p-hot: #A8B0F0;
  --py: 0;
  --pr: 0;
  position: relative;
  isolation: isolate;
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.05fr);
  gap: clamp(28px, 4vw, 72px);
  align-items: center;
  width: 100vw;
  margin: 0 0 0 calc(50% - 50vw);
  padding: clamp(56px, 7vw, 112px) max(var(--gutter), calc(50vw - 640px));
  background: var(--p-bg);
  color: var(--p-fg);
  counter-increment: proj;
  overflow: hidden;
}
.poster-work .project:nth-child(even) {
  --p-bg: #EFECE4;
  --p-fg: #16181B;
  --p-dim: rgb(22 24 27 / 0.78);
  --p-line: rgb(22 24 27 / 0.20);
}
/* The booklet changes sides on alternate bands, so three projects read as a
   rhythm rather than three copies of one row. */
.poster-work .project:nth-child(even) .project__copy { order: -1; }

.poster-work .project__copy { position: relative; z-index: 1; }

.poster-work .project__scope {
  display: inline-block;
  width: fit-content;
  margin-bottom: clamp(14px, 1.8vw, 22px);
  padding: 6px 12px 5px;
  border: 2px solid currentColor;
  color: var(--p-fg);
  font: 700 var(--t-label)/1 var(--font-mono);
  letter-spacing: 0.24em;
  text-indent: 0.24em;
  background: none;
}

.poster-work .project__client {
  margin: 0;
  color: var(--p-fg);
  font-family: var(--font-display);
  font-size: clamp(2.25rem, 5.4vw, 4.5rem);
  font-weight: 700;
  line-height: 0.96;
  letter-spacing: -0.04em;
  overflow-wrap: normal;
  word-break: keep-all;
}

.poster-work .project__line {
  margin: clamp(16px, 2vw, 26px) 0 0;
  max-width: min(30ch, 100%);
  color: var(--p-dim);
  font-size: clamp(1.125rem, 1.6vw, 1.4375rem);
  line-height: 1.5;
}

/* The index, outlined and stepped off the top edge like a chapter numeral. */
.poster-work .project::before {
  content: counter(proj, decimal-leading-zero);
  position: absolute;
  z-index: 0;
  top: clamp(-26px, -1.6vw, -6px);
  right: max(var(--gutter), calc(50vw - 640px));
  font-family: var(--font-display);
  font-size: clamp(4.5rem, 12vw, 11rem);
  font-weight: 700;
  line-height: 0.78;
  letter-spacing: -0.06em;
  color: transparent;
  -webkit-text-stroke: 1.5px var(--p-line);
  pointer-events: none;
  transform: translate3d(0, calc(var(--py) * 1px), 0);
}

/* The booklet, framed. book.js keeps every behaviour it had; this squares the
   corner it was given and puts a rule around it so it reads as a plate on the
   band rather than a floating screenshot. */
.poster-work .project__media,
.poster-work .projects--wide .book.is-bound {
  border-radius: 0;
}
.poster-work .project__gallery {
  position: relative;
  z-index: 1;
  padding: clamp(10px, 1.2vw, 18px);
  border: 2px solid var(--p-line);
  background: rgb(var(--shade-rgb) / 0.22);
}

.poster-work .project .link { color: var(--p-fg); }

@media (max-width: 899px) {
  .poster-work .project {
    grid-template-columns: minmax(0, 1fr);
    padding-block: clamp(44px, 9vw, 72px);
  }
  .poster-work .project:nth-child(even) .project__copy { order: 0; }
  .poster-work .project::before {
    position: static;
    display: block;
    margin-bottom: clamp(6px, 1.6vw, 14px);
    font-size: clamp(3.5rem, 17vw, 6rem);
  }
}

/* ==========================================================================
   15. Contact — three panels and a band for the form
   ========================================================================== */

.poster-contact { padding: 0; }
.poster-contact > .wrap { max-width: none; padding: 0; }
.poster-contact .contact__grid { display: block; gap: 0; }

/* The three details, as poster panels on the page's own ground. */
.poster-contact .contact__details {
  background: var(--p-pop);
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(10px, 1.2vw, 18px);
  width: 100vw;
  margin-left: calc(50% - 50vw);
  padding: clamp(40px, 5vw, 76px) max(var(--gutter), calc(50vw - 640px));
}
@media (max-width: 899px) {
  .poster-contact .contact__details { grid-template-columns: minmax(0, 1fr); }
}

.poster-contact .detail {
  --p-bg: #14161B;
  --p-fg: #FFFFFF;
  --p-dim: rgb(255 255 255 / 0.78);
  --p-line: rgb(255 255 255 / 0.22);
  position: relative;
  display: flex;
  flex-direction: column;
  gap: clamp(10px, 1.2vw, 16px);
  min-height: clamp(170px, 16vw, 230px);
  margin: 0;
  padding: clamp(24px, 2.8vw, 40px);
  border: 0;
  border-radius: 0;
  background: var(--p-bg);
  color: var(--p-fg);
  box-shadow: none;
  overflow: hidden;
}
.poster-contact .detail:nth-child(2) {
  --p-bg: #EFECE4;
  --p-fg: #16181B;
  --p-dim: rgb(22 24 27 / 0.80);
  --p-line: rgb(22 24 27 / 0.22);
}
.poster-contact .detail::after { content: none; }

.poster-contact .detail h2 {
  width: fit-content;
  margin: 0;
  padding: 6px 12px 5px;
  border: 2px solid currentColor;
  color: var(--p-fg);
  font: 700 var(--t-label)/1 var(--font-mono);
  letter-spacing: 0.24em;
  text-indent: 0.24em;
}
.poster-contact .detail p,
.poster-contact .detail .footer__addr {
  margin: 0;
  color: var(--p-dim);
  font-size: clamp(1.0625rem, 1.4vw, 1.25rem);
  line-height: 1.5;
  font-style: normal;
}
.poster-contact .detail a { color: var(--p-fg); font-size: inherit; }
.poster-contact .detail .link {
  background: none;
  text-decoration: none;
  padding-bottom: 6px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='6' viewBox='0 0 24 6'%3E%3Cpath d='M0 3c2-3 4-3 6 0s4 3 6 0 4-3 6 0 4 3 6 0' fill='none' stroke='%23A8B0F0' stroke-width='1.6'/%3E%3C/svg%3E");
  background-repeat: repeat-x;
  background-position: 0 100%;
  background-size: 24px 6px;
  transition: background-position 320ms var(--e-out);
}
.poster-contact .detail .link:hover { background-position: 12px 100%; }

/* The form sits in its own graphite band, squared off. The internals — the
   icons, the tick states, the validation — are untouched; what changes is the
   geometry, which was rounded and soft in a page that is now neither. */
.poster-contact .form {
  width: 100vw;
  margin-left: calc(50% - 50vw);
  padding: clamp(44px, 6vw, 88px) max(var(--gutter), calc(50vw - 640px));
  border: 0;
  border-radius: 0;
  background: #14161B;
  box-shadow: none;
}
html .poster-contact .form.form--playful { border-radius: 0; border: 0; background: #14161B; }
html .poster-contact .form.form--playful .field input,
html .poster-contact .form.form--playful .field textarea {
  border-radius: 0;
  border-width: 2px;
  background: transparent;
  box-shadow: none;
  font-size: 1.0625rem;
}
html .poster-contact .form.form--playful .field input:focus,
html .poster-contact .form.form--playful .field textarea:focus {
  border-color: var(--form-accent);
  box-shadow: 0 0 0 3px rgb(168 176 240 / 0.34);
}
html .poster-contact .form.form--playful .checks .check {
  border-radius: 0;
  border-width: 2px;
}
.poster-contact .form .fieldset > legend,
.poster-contact .form .field > label {
  text-transform: uppercase;
  letter-spacing: 0.18em;
  font-size: var(--t-label);
  font-weight: 700;
}
.poster-contact .form .btn,
.poster-contact .form button[type="submit"] {
  border-radius: 0;
  padding: 20px 34px;
  font-size: 1.0625rem;
  letter-spacing: 0.02em;
}

/* ==========================================================================
   16. The closing call to action
   Every page ends on one. It is now the loudest band on the page.
   ========================================================================== */

.poster-cta {
  --p-pop: color-mix(in srgb, var(--accent) 76%, #171526);
  width: 100vw;
  margin-left: calc(50% - 50vw);
  padding: clamp(64px, 9vw, 140px) max(var(--gutter), calc(50vw - 640px));
  border-top: 0;
  background: var(--p-pop);
  color: var(--shade);
}
html.is-inverse .poster-cta { background: #14161B; color: #FFFFFF; }
.poster-cta > .wrap { max-width: none; padding: 0; }

.poster-cta .display {
  max-width: 16ch;
  margin: 0;
  color: inherit;
  font-size: clamp(2.25rem, 6.4vw, 5rem);
  font-weight: 700;
  line-height: 0.98;
  letter-spacing: -0.04em;
}
.poster-cta .hero__cta { margin-top: clamp(26px, 3.4vw, 44px); gap: var(--s-4); }

/* On the accent the solid button has nowhere to go, so the pair inverts: the
   primary becomes the graphite block and the secondary its outline. */
.poster-cta .btn {
  border-radius: 0;
  padding: 18px 30px;
  border: 2px solid currentColor;
  color: inherit;
  background: none;
  font-weight: 700;
}
.poster-cta .btn--solid {
  background: var(--shade);
  color: var(--accent);
  border-color: var(--shade);
}
html.is-inverse .poster-cta .btn--solid { background: var(--accent); color: var(--shade); border-color: var(--accent); }
.poster-cta .btn:hover { translate: 0 -2px; }

.poster-cta .micro { margin-top: clamp(24px, 3vw, 40px); color: inherit; opacity: 0.7; }

/* ==========================================================================
   17. The common surface
   The complaint that produced this section: the rebuilt parts and the parts
   they sit between read as two different websites. They did. The bands were
   hard-edged, flat and numbered; everything around them was a soft rounded
   card with a lit gradient and a shadow.

   Rather than restyle each component, the shared primitives change once. Every
   radius goes to zero, every card surface goes flat, and every button becomes
   a rectangle with a 2px rule. That is most of the distance, and it costs
   forty lines instead of four hundred.
   ========================================================================== */

:root,
html.is-inverse {
  --radius: 0;
  --radius-card: 0;
}

.btn {
  border-radius: 0;
  border: 2px solid currentColor;
  padding: 16px 28px;
  font-weight: 700;
  letter-spacing: 0.01em;
}
.btn--solid { border-color: transparent; }

/* The lit-card treatment — a radial highlight, a 145° sheen and a 70px
   shadow — was the single loudest difference between old and new. Flat. */
:is(.promise, .stat, .team > li, .detail, .handover, .text-detail, .pull.quote-card) {
  border-radius: 0;
  box-shadow: none;
}
:is(.stat, .team > li, .handover, .text-detail) {
  background: none;
  border: 1px solid var(--rule);
}
:is(.promise, .stat, .team > li, .detail, .handover)::after { content: none; }
.bucket__grid { border-radius: 0; box-shadow: none; }
.bucket__grid::after { content: none; }

.book, .book__page, .project__media, .project__media.is-window,
.form, .field input, .field textarea, .check, .budget-choice {
  border-radius: 0;
}

/* ==========================================================================
   18. The numbering spine
   One counter per document, and every section marker draws from it. This is
   the piece that makes a page read as one system: the chapter bands, the
   plain sections and the page heads all carry the same chip in the same place
   in the same face, and the numbers run in document order whatever the page
   is made of.
   ========================================================================== */

main { counter-reset: sect; }

.section > .wrap > .label,
.band > .wrap > .label,
.page-hero__copy > .label,
.poster-head .label,
.scroll-chapter .story__index {
  counter-increment: sect;
  display: inline-flex;
  align-items: center;
  gap: 10px;
  width: fit-content;
  margin-bottom: clamp(14px, 1.8vw, 24px);
  padding: 0;
  border: 0;
  color: var(--text-dim);
  font: 700 var(--t-label)/1 var(--font-mono);
  letter-spacing: 0.22em;
  text-transform: uppercase;
  text-indent: 0;
}
.section > .wrap > .label::before,
.band > .wrap > .label::before,
.page-hero__copy > .label::before,
.poster-head .label::before,
.scroll-chapter .story__index::before {
  content: "N." counter(sect, decimal-leading-zero);
  padding: 6px 10px 5px;
  border: 2px solid currentColor;
  letter-spacing: 0.14em;
}

/* The chapter index carries no digits of its own any more — the counter is
   the single source, so a chapter cannot drift out of step with the sections
   around it. */
.scroll-chapter .story__index { color: var(--p-fg); }
.poster-head .label { color: inherit; }

/* ==========================================================================
   19. Process, as a numbered run
   Five steps that were a soft list become the clearest expression of the
   numbering system on the site.
   ========================================================================== */

.steps {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 0;
  counter-reset: step;
  margin-top: clamp(28px, 4vw, 56px);
}
.steps > li {
  counter-increment: step;
  display: grid;
  grid-template-columns: clamp(64px, 8vw, 132px) minmax(0, 1fr);
  gap: clamp(16px, 3vw, 48px);
  align-items: start;
  margin: 0;
  padding: clamp(22px, 2.8vw, 40px) 0;
  border-top: 1px solid var(--rule);
}
.steps > li:last-child { border-bottom: 1px solid var(--rule); }
.steps > li::before {
  content: counter(step, decimal-leading-zero);
  font: 700 clamp(1.75rem, 3.4vw, 3rem)/0.9 var(--font-display);
  letter-spacing: -0.04em;
  color: transparent;
  -webkit-text-stroke: 1.5px var(--accent);
}
.steps > li > div { min-width: 0; }
.steps h3 {
  margin: 0;
  font: 700 clamp(1.25rem, 2.2vw, 1.875rem)/1.1 var(--font-display);
  letter-spacing: -0.03em;
}
.steps p {
  margin-top: var(--s-3);
  max-width: min(46ch, 100%);
  font-size: clamp(1.0625rem, 1.35vw, 1.25rem);
  line-height: 1.55;
}
.steps .steps__rail { display: none; }
@media (max-width: 599px) {
  .steps > li { grid-template-columns: minmax(0, 1fr); gap: var(--s-3); }
}

/* ==========================================================================
   20. Services, as poster panels
   Three panels on the same three-room cycle the promises use, so the two
   blocks on the home page are recognisably the same component family.
   ========================================================================== */

/* Block, not grid. As a grid the panel kept coming out at 692px inside a
   1176px track whatever was set on it; a block-level child fills its parent by
   definition and there is nothing here a grid was buying. */
.buckets { display: block; margin-top: clamp(28px, 4vw, 56px); }
.buckets > .bucket { display: block; width: auto; margin: 0 0 clamp(10px, 1.2vw, 18px); padding: 0; border: 0; }
.buckets > .bucket:last-child { margin-bottom: 0; }

.bucket__grid {
  --p-bg: #14161B;
  --p-fg: #FFFFFF;
  --p-dim: rgb(255 255 255 / 0.78);
  --p-line: rgb(255 255 255 / 0.22);
  position: relative;
  padding: clamp(26px, 3.4vw, 56px);
  border: 0;
  background: var(--p-bg);
  color: var(--p-fg);
  isolation: isolate;
}
.bucket:nth-child(2) .bucket__grid {
  --p-bg: #EFECE4;
  --p-fg: #16181B;
  --p-dim: rgb(22 24 27 / 0.80);
  --p-line: rgb(22 24 27 / 0.22);
}
.bucket:nth-child(3) .bucket__grid {
  --p-bg: var(--p-pop);
  --p-fg: var(--shade);
  --p-dim: rgb(var(--shade-rgb) / 0.84);
  --p-line: rgb(var(--shade-rgb) / 0.32);
}
.bucket__num {
  color: transparent;
  -webkit-text-stroke: 1.5px var(--p-line);
  font: 700 clamp(1.75rem, 3vw, 2.5rem)/1 var(--font-display);
  letter-spacing: -0.04em;
}
.bucket__title {
  color: var(--p-fg);
  font: 700 clamp(1.625rem, 3vw, 2.5rem)/1.02 var(--font-display);
  letter-spacing: -0.035em;
}
.bucket__desc {
  color: var(--p-dim);
  font-size: clamp(1.0625rem, 1.45vw, 1.3125rem);
  line-height: 1.5;
  max-width: min(42ch, 100%);
}
.bucket__grid .link { color: var(--p-fg); }
.bucket:hover .bucket__num { transform: none; }

/* ==========================================================================
   21. Scale
   Everything above is set in clamp(), but a clamp only protects the sizes it
   was given. These are the guards for the widths in between: nothing may sit
   on a measure it cannot fit, and no display line may be cut mid-word.
   ========================================================================== */

.poster-head .display,
.poster-cta .display,
.scroll-chapter .story__aside > .display,
.poster-work .project__client,
.bucket__title,
.steps h3 {
  overflow-wrap: break-word;   /* only ever reached by a single word longer
                                  than the column, which is better broken than
                                  spilling off the edge */
  hyphens: manual;
}

@media (max-width: 479px) {
  .poster-head { padding-top: clamp(72px, 22vw, 110px); }
  .poster-head .display { font-size: clamp(2rem, 11vw, 2.75rem); }
  .poster-cta .display,
  .scroll-chapter .story__aside > .display { font-size: clamp(1.875rem, 9.5vw, 2.5rem); }
  .scroll-chapter .story-card:first-child .story-card__content > p:first-child {
    font-size: clamp(1.625rem, 8vw, 2.125rem);
  }
  .promises--run .promise__word { font-size: clamp(1.75rem, 10vw, 2.5rem); }
  .poster-work .project__client { font-size: clamp(1.875rem, 10vw, 2.75rem); }
}

/* The pills. --radius is a token, but the header's furniture hard-codes 999px
   rather than using it, so a page of rectangles kept a row of lozenges along
   the top. These are the eight places that do it. */
.lang,
.lang a,
.lang__thumb,
.nav__list,
.nav__link,
.nav__marker,
.book__nav,
.brand,
.install-control,
.finish-control,
.sound-control { border-radius: 0; }

/* The brief's marker sits between two full-bleed bands, so it needs the same
   inline padding they carry or it would start at the viewport edge. */
.poster-contact > .wrap > .label {
  margin: clamp(30px, 4vw, 56px) 0 clamp(14px, 1.8vw, 22px);
  padding-inline: max(var(--gutter), calc(50vw - 640px));
}

/* ==========================================================================
   22. Toning down, and the services panel rebuilt
   ========================================================================== */

/* The accent room, muted. The accent at full strength was carrying a title
   band, a closing band and a page head on every page, which is more of it
   than the palette can take at that size. A quarter of the ink mixed in keeps
   the hue and drops the glare; --shade on it still measures about 5:1, and the
   fallback is the raw accent where color-mix is unsupported. */
:root,
html.is-inverse {
  --p-pop: var(--accent);
  --p-pop: color-mix(in srgb, var(--accent) 76%, #171526);
}

/* And one less of them per chapter: a sequence ending on colour was the idea,
   but with the title band already in the accent it made two accent bands in
   every chapter. The last movement takes the bone instead. */
.scroll-chapter .story-card:last-child {
  --p-bg: #EFECE4;
  --p-fg: #16181B;
  --p-dim: rgb(22 24 27 / 0.78);
  --p-line: rgb(22 24 27 / 0.20);
  --p-sheen: 0.22;
}

/* --- Services --------------------------------------------------------------
   The panel had main.css's three-column grid underneath it: a 4rem slot for
   the number, which is not enough for "(01)" at 40px, so it broke across two
   lines. The number is now a chip in the corner drawn from a counter — the
   same figure the rest of the site uses — and the panel is two real columns.
   -------------------------------------------------------------------------- */

.buckets { counter-reset: bkt; }
.bucket { counter-increment: bkt; }

.bucket__grid {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 1.06fr) minmax(0, 0.94fr);
  gap: clamp(20px, 3vw, 56px);
  align-items: center;
  padding: clamp(62px, 6.4vw, 104px) clamp(28px, 3.4vw, 60px) clamp(30px, 3.6vw, 60px);
}

/* The digits come from the counter, so the parentheses in the markup are not
   in the way and the figure cannot disagree with its position in the list. */
.bucket__num {
  position: absolute;
  top: clamp(24px, 2.6vw, 40px);
  left: clamp(28px, 3.4vw, 60px);
  z-index: 1;
  padding: 0;
  font-size: 0;
  background: none;
  -webkit-text-fill-color: currentColor;
  color: var(--p-fg);
}
.bucket__num::before {
  content: counter(bkt, decimal-leading-zero);
  display: inline-block;
  padding: 6px 11px 5px;
  border: 2px solid currentColor;
  font: 700 var(--t-label)/1 var(--font-mono);
  letter-spacing: 0.2em;
  text-indent: 0.2em;
}
.bucket__num::after { content: none; }

.bucket__desc { margin-top: clamp(12px, 1.6vw, 20px); }
.bucket__grid > div { min-width: 0; }
.bucket__media { min-width: 0; }

@media (max-width: 899px) {
  .bucket__grid {
    grid-template-columns: minmax(0, 1fr);
    padding-top: clamp(56px, 14vw, 84px);
  }
}

/* --- The form's playful accents --------------------------------------------
   The second choice lit the form in peach, which was the one warm colour left
   on the site once the dots turned periwinkle. */
html .form.form--playful:has(.check:nth-child(2) input:checked) { --form-accent: #A8B0F0; }
html .form.form--playful .checks .check:nth-child(2) { --choice-color: #A8B0F0; }

/* ==========================================================================
   23. Rounded again, and the page heads go dark
   Two corrections. Squaring every corner was my call and it split the site in
   half instead of uniting it — some components carry radii that are not
   tokenised, so "everything sharp" was never going to be true. Everything is
   rounded instead, from one scale.

   And the page head was an accent band with the hero art laid over it at 28%
   — lilac art on a lilac ground, half of it cropped by an absolute position it
   never asked for. The band takes the graphite, the art goes back into the
   flow beside the copy at full strength, and both are legible.
   ========================================================================== */

:root,
html.is-inverse {
  --radius: 999px;
  --radius-card: 20px;
  --radius-panel: 20px;
  --radius-chip: 10px;
}

/* Undoing section 17's squaring, and the eight hard-coded pills with it. */
.btn { border-radius: var(--radius); }
.lang,
.lang a,
.lang__thumb,
.nav__list,
.nav__link,
.nav__marker,
.brand,
.install-control,
.finish-control,
.sound-control { border-radius: var(--radius); }
.book__nav { border-radius: var(--radius); }

:is(.promise, .stat, .team > li, .detail, .handover, .text-detail, .pull.quote-card),
.bucket__grid,
.book, .book__page, .project__media, .project__media.is-window,
.form, .field input, .field textarea, .check, .budget-choice {
  border-radius: var(--radius-card);
}

.promises--run > .promise,
.poster-contact .detail,
.bucket__grid { border-radius: var(--radius-panel); }
.poster-work .project__gallery { border-radius: calc(var(--radius-panel) + 6px); }
.poster-work .projects--wide .book.is-bound,
.poster-work .project__media { border-radius: var(--radius-card); }

/* The index chips, the scope chips and the section markers. */
.section > .wrap > .label::before,
.band > .wrap > .label::before,
.page-hero__copy > .label::before,
.poster-head .label::before,
.scroll-chapter .story__index::before,
.bucket__num::before,
.poster-work .project__scope,
.poster-contact .detail h2 { border-radius: var(--radius-chip); }

/* --- The page head ---------------------------------------------------------
   Graphite, two columns, art in the flow. -------------------------------- */

.poster-head,
html.is-inverse .poster-head {
  display: grid;
  grid-template-columns: minmax(0, 1.08fr) minmax(0, 0.92fr);
  gap: clamp(24px, 4vw, 72px);
  align-items: center;
  background: #14161B;
  color: #FFFFFF;
}
.poster-head .page-hero__copy { grid-column: 1; }
.poster-head .display { color: #FFFFFF; }
.poster-head .label { color: rgb(255 255 255 / 0.82); }
.poster-head .lede { color: rgb(255 255 255 / 0.80); opacity: 1; }

/* Back into the flow at full strength. It was absolutely positioned at 28%
   opacity, which cropped it against the band's overflow and tinted it into
   the ground it was sitting on. */
.poster-head .page-art {
  position: relative;
  z-index: 0;
  grid-column: 2;
  right: auto;
  top: auto;
  translate: none;
  width: 100%;
  max-width: 520px;
  margin: 0 auto;
  opacity: 1;
}
@media (max-width: 899px) {
  .poster-head,
  html.is-inverse .poster-head { grid-template-columns: minmax(0, 1fr); }
  .poster-head .page-art { display: none; }
}

/* The services panel stretches its track. */


/* The staged services stack. pieces.js gives the three panels a perspective
   and pushes each to a different Z, so the one furthest back renders visibly
   narrower than the one in front. That reads as depth on borderless rows,
   which is what it was built for; on panels with a solid ground it reads as
   three boxes of three different widths, the last of them clipping its own
   heading. The Z travel and the focus dimming come off — the rooms already do
   the separating. */
.buckets--staged { perspective: none; perspective-origin: 50% 50%; }
.buckets--staged .bucket,
.buckets--staged .bucket.is-lowered,
.buckets--staged .bucket.is-raised {
  transform: none;
  opacity: 1;
  filter: none;
}

/* main.css caps every <p> and <li> in <main> at a 68ch measure, and opts a
   named list of components out of it. That is right for text and wrong for a
   list item that is now a panel with a ground: the cap was invisible while the
   buckets were borderless rows, and the moment they took a background it
   showed up as a panel that stops two thirds of the way across. Every list
   that became a surface joins the opt-out.

   (:where() contributes no specificity, so the original is (0,0,2) and this
   clears it comfortably.) */
main .buckets > li,
main .promises > li,
main .steps > li,
main .projects > li,
main .book > li { max-width: none; }

/* ==========================================================================
   24. Slabs, not bands — and a stack instead of a column
   Two changes that go together.

   ROUNDED. The bands were the last square things on the site, because a band
   that runs to both edges of the viewport has no corners to round. So they
   stop running to the edges: each one is now an inset slab on the page ground,
   the same radius as every panel, with a little air between them. The site has
   one shape language again, and 100vw stops fighting the scrollbar.

   STACKED. Reading a chapter was scrolling one long column. The slabs are
   sticky now, each pinned a few pixels below the one before it, so a chapter
   arrives as a deck that gathers under the chapter title and the edges of what
   you have already read stay visible at the top. Nothing is hijacked — it is
   position: sticky and the page scrolls at its own speed; a browser without
   sticky support gets the plain column back.
   ========================================================================== */

:root, html.is-inverse { --radius-slab: 28px; }

/* The wrap goes back to being a wrap. */
.scroll-chapter .wrap,
.poster-work > .wrap,
.poster-contact > .wrap,
.poster-cta > .wrap {
  max-width: var(--max);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

/* Every slab: in the flow, inset, rounded. */
.scroll-chapter .story__aside,
.scroll-chapter .story-card,
.poster-work .project,
.poster-contact .contact__details,
.poster-contact .form,
.poster-head,
.poster-cta {
  width: auto;
  max-width: var(--max);
  margin-left: auto;
  margin-right: auto;
  margin-block: clamp(8px, 1vw, 14px);
  padding-inline: clamp(26px, 3.6vw, 68px);
  border-radius: var(--radius-slab);
}

.poster-head,
.poster-cta {
  width: calc(100% - 2 * var(--gutter));
  padding-block: clamp(52px, 6.4vw, 104px);
}
.poster-head { margin-top: clamp(10px, 1.4vw, 20px); }

.scroll-chapter .story__aside,
.scroll-chapter .story-card { padding-block: clamp(38px, 4.6vw, 76px); }
.poster-work .project { padding-block: clamp(40px, 5vw, 84px); }
.poster-contact .contact__details { padding-block: clamp(30px, 3.4vw, 52px); }
.poster-contact .form { padding-block: clamp(36px, 4.6vw, 72px); }

/* The numeral and the section marker were positioned against the old
   full-bleed padding, which no longer exists. */
.scroll-chapter .story-card__number,
.poster-work .project::before { right: clamp(20px, 3vw, 56px); }
.poster-contact > .wrap > .label { padding-inline: 0; }

/* --- The stack ------------------------------------------------------------
   main.js already writes --i on each staggered child, which is exactly the
   index this needs, so the offsets cost no extra JavaScript. */
@media (min-width: 900px) {
  .scroll-chapter .story__aside {
    position: sticky;
    top: calc(var(--header-h) + 12px);
    z-index: 1;
  }
  .scroll-chapter .story-card {
    position: sticky;
    top: calc(var(--header-h) + 26px + var(--i, 0) * 16px);
    z-index: 2;
    box-shadow: 0 -1px 0 rgb(var(--ink-rgb) / 0.10), 0 -24px 60px -40px rgb(var(--shade-rgb) / 0.9);
  }
  /* The last slab of a chapter releases: something has to scroll away or the
     next chapter arrives underneath a permanent lid. */
  .scroll-chapter .story-card:last-child { position: relative; top: auto; }

  .poster-work .project { position: sticky; top: calc(var(--header-h) + 20px + var(--i, 0) * 16px); }
  .poster-work .project:last-child { position: relative; top: auto; }
}

/* --- Mobile ---------------------------------------------------------------
   Compact spacing is the baseline. The mobile enhancement below restores
   stacking with an offset measured for each card's actual height. */
@media (max-width: 899px) {
  :root, html.is-inverse { --radius-slab: 20px; --radius-panel: 16px; --radius-card: 16px; }

  .scroll-chapter .story__aside,
  .scroll-chapter .story-card,
  .poster-work .project,
  .poster-contact .contact__details,
  .poster-contact .form,
  .poster-head,
  .poster-cta {
    position: relative;
    top: auto;
    padding-inline: clamp(20px, 5vw, 30px);
    margin-block: 8px;
  }
  .poster-head,
  .poster-cta { padding-block: clamp(40px, 10vw, 64px); }
  .scroll-chapter .story__aside { min-height: 0; padding-block: clamp(30px, 7vw, 44px); }
  .scroll-chapter .story-card { padding-block: clamp(28px, 7vw, 44px); }
  .poster-work .project { padding-block: clamp(32px, 8vw, 48px); }

  .poster-head .display { font-size: clamp(2rem, 9vw, 3rem); }
  .poster-cta .display,
  .scroll-chapter .story__aside > .display { font-size: clamp(1.875rem, 8vw, 2.75rem); }
  .scroll-chapter .story-card:first-child .story-card__content > p:first-child {
    font-size: clamp(1.625rem, 7vw, 2.25rem);
  }
  .poster-work .project__client { font-size: clamp(1.875rem, 8.5vw, 2.75rem); }
  .bucket__title { font-size: clamp(1.5rem, 7vw, 2rem); }

  .poster-work .project::before {
    position: static;
    display: block;
    margin-bottom: clamp(6px, 2vw, 14px);
    font-size: clamp(3rem, 16vw, 5rem);
  }
  .poster-contact .contact__details { gap: 10px; }
  .steps > li::before { font-size: clamp(1.5rem, 7vw, 2rem); }
}

/* Very narrow: the decorative layer goes entirely. It costs paint and adds
   nothing at 360px, where every pixel of width is doing real work. */
@media (max-width: 559px) {
  .scroll-chapter .story-card::after,
  .scroll-chapter .story-card__number::before,
  .scroll-chapter .story-card__number::after { display: none; }
  .promises--run .promise__num { font-size: clamp(2.5rem, 12vw, 3.5rem); }
}

/* ==========================================================================
   25. The hero art on small screens, and a pass for speed
   ========================================================================== */

/* The art was hidden below 900px because it had nowhere to sit in a single
   column. It has somewhere now: under the copy, at a size that leaves the
   headline the top of the screen. pieces.js builds every one of these
   regardless of width, so nothing new is being started — this only stops
   throwing away work already done. */
@media (max-width: 899px) {
  .poster-head .page-art {
    display: block;
    grid-column: 1;
    width: min(100%, 340px);
    margin: clamp(18px, 4vw, 30px) auto 0;
    max-width: none;
    justify-self: center;
    text-align: center;
  }
  .poster-head .page-art > svg,
  .poster-head .page-art > * { max-width: 100%; height: auto; }
}
@media (max-width: 559px) {
  .poster-head .page-art { width: min(100%, 260px); }
}

/* --- Speed ----------------------------------------------------------------
   Four changes, in order of what they cost the browser.

   1. The veil dropped its blur. `filter: blur(38px)` over a band the width of
      the page is one of the most expensive things a compositor can be asked
      for, and it was running on every band on every page. Radial gradients are
      already soft; the blur was buying almost nothing visible.

   2. Off-screen panels stop being laid out and painted. A chapter page is
      thirteen thousand pixels tall and most of it is never on screen at once;
      content-visibility lets the browser skip the work until it is close,
      and contain-intrinsic-size: auto means it remembers the real height
      afterwards so the scrollbar does not jump.

   3. The numerals gave up their permanent `will-change: transform`. It forces
      a compositor layer for the life of the page whether or not anything is
      moving; the transform promotes itself while it animates anyway.

   4. On a coarse pointer the decoration that exists to answer a mouse comes
      off entirely.
   -------------------------------------------------------------------------- */

.scroll-chapter .story__aside::before,
.scroll-chapter .story-card::before {
  filter: none;
  opacity: calc(var(--p-sheen) * 0.34);
}

.promises--run > .promise,
.buckets > .bucket,
.poster-contact .detail,
.poster-work .project {
  content-visibility: auto;
  contain-intrinsic-size: auto 320px;
}
@media (max-width: 899px) {
  .scroll-chapter .story-card {
    content-visibility: auto;
    contain-intrinsic-size: auto 420px;
  }
}

.scroll-chapter .story-card__number { will-change: auto; }

@media (pointer: coarse) {
  .scroll-chapter .story-card::after,
  .scroll-chapter .story__aside::before,
  .scroll-chapter .story-card::before { display: none; }
  .promises--run .promise__word .ch { transition: none; }
}

/* ==========================================================================
   26. The numbering spine comes out, and the last square buttons
   ========================================================================== */

/* N.001 / N.002 was borrowed from a reference that uses it as the spine of a
   whole page. Here it turned out to be a second number competing with the ones
   that carry real information — the 01/02/03 on the service panels, the
   promises and the project bands, which say where you are in a list. The
   section markers keep the label; the counter goes.

   `content: none` rather than deleting the rules above, so the chips stay one
   decision in one place if they are ever wanted back. */
.section > .wrap > .label::before,
.band > .wrap > .label::before,
.page-hero__copy > .label::before,
.poster-head .label::before,
.scroll-chapter .story__index::before { content: none; }

.section > .wrap > .label,
.band > .wrap > .label,
.page-hero__copy > .label,
.poster-head .label {
  display: block;
  gap: 0;
  counter-increment: none;
}

/* Every .story__index is empty now — the digits came out of the markup when
   the counter took over, and the counter has just gone. Three of them on the
   home page alone, and one sits in a plain .story band rather than a chapter,
   so the rule cannot be scoped to .scroll-chapter. */
/* Both forms: the bare one for the index that sits in a plain .story band,
   and the scoped one to outrank section 18's inline-flex inside a chapter. */
.story__index,
.scroll-chapter .story__index { display: none; }

/* --- The buttons that stayed square ---------------------------------------
   Written back when everything was being squared, at (0,2,0), which quietly
   outranked the (0,1,0) `.btn` rule that rounded everything else. Both are the
   primary call to action, which is a poor place to leave an inconsistency. */
.poster-cta .btn,
.poster-cta .btn--solid,
.poster-contact .form .btn,
.poster-contact .form button[type="submit"],
html .poster-contact .form.form--playful .checks .check,
html .poster-contact .form.form--playful .field input,
html .poster-contact .form.form--playful .field textarea {
  border-radius: var(--radius);
}
html .poster-contact .form.form--playful .field input,
html .poster-contact .form.form--playful .field textarea,
html .poster-contact .form.form--playful .checks .check {
  border-radius: var(--radius-card);
}

/* ==========================================================================
   27. Panels arrive from the side, alternating
   Everything entered by rising sixteen pixels, which is the same gesture the
   whole web uses and reads as nothing at all. Panels now come in from the left
   and the right by turns, with a degree of rotation that settles out — so a
   run of them assembles rather than scrolls past, and the direction tells you
   the list has changed row.

   Two safeguards. The travel is in vw and <html> carries overflow-x: clip, so
   a panel waiting off to the side cannot open a horizontal scrollbar. And the
   whole block is inside prefers-reduced-motion: no-preference — main.js
   already early-returns there and never applies .stg, but a second lock on
   the door costs nothing.
   ========================================================================== */

  .scroll-chapter .story-card.stg,
  .promises--run > .promise.stg,
  .buckets > .bucket.stg,
  .poster-work .project.stg,
  .poster-contact .detail.stg,
  .steps > li.stg {
    opacity: 0;
    clip-path: none;
    transform: translate3d(var(--in-x, 0), var(--in-y, 18px), 0) rotate(var(--in-r, 0deg));
    transition: opacity 620ms var(--e-out),
                transform 880ms var(--e-soft);
    transition-delay: calc(var(--i, 0) * 90ms);
  }

  /* Odd from the left, even from the right. nth-child rather than a sum on
     --i: the index main.js writes is a plain number and CSS has no way to
     alternate a sign from one. */
  .scroll-chapter .story-card.stg:nth-child(odd),
  .promises--run > .promise.stg:nth-child(odd),
  .buckets > .bucket.stg:nth-child(odd),
  .poster-work .project.stg:nth-child(odd),
  .poster-contact .detail.stg:nth-child(odd),
  .steps > li.stg:nth-child(odd) { --in-x: -7vw; --in-r: -1.1deg; }

  .scroll-chapter .story-card.stg:nth-child(even),
  .promises--run > .promise.stg:nth-child(even),
  .buckets > .bucket.stg:nth-child(even),
  .poster-work .project.stg:nth-child(even),
  .poster-contact .detail.stg:nth-child(even),
  .steps > li.stg:nth-child(even) { --in-x: 7vw; --in-r: 1.1deg; }

  /* The steps are a tighter run, so they travel less and lean not at all —
     six rows swinging in alternate directions is a fairground, not a list. */
  .steps > li.stg:nth-child(odd) { --in-x: -3.5vw; --in-r: 0deg; }
  .steps > li.stg:nth-child(even) { --in-x: 3.5vw; --in-r: 0deg; }

  [data-reveal].is-in .scroll-chapter .story-card.stg,
  .scroll-chapter[data-reveal].is-in .story-card.stg,
  [data-reveal].is-in .promises--run > .promise.stg,
  [data-reveal].is-in .buckets > .bucket.stg,
  [data-reveal].is-in .poster-work .project.stg,
  .poster-work[data-reveal].is-in .project.stg,
  [data-reveal].is-in .poster-contact .detail.stg,
  [data-reveal].is-in .steps > li.stg {
    opacity: 1;
    transform: none;
  }

  /* Narrower screens get a shorter throw: 7vw of a phone is nothing, but the
     rotation on a full-width slab is very visible. */
  @media (max-width: 899px) {
    .scroll-chapter .story-card.stg:nth-child(odd),
    .promises--run > .promise.stg:nth-child(odd),
    .buckets > .bucket.stg:nth-child(odd),
    .poster-work .project.stg:nth-child(odd),
    .poster-contact .detail.stg:nth-child(odd) { --in-x: -12vw; --in-r: -0.6deg; }
    .scroll-chapter .story-card.stg:nth-child(even),
    .promises--run > .promise.stg:nth-child(even),
    .buckets > .bucket.stg:nth-child(even),
    .poster-work .project.stg:nth-child(even),
    .poster-contact .detail.stg:nth-child(even) { --in-x: 12vw; --in-r: 0.6deg; }
  }

/* Withdrawn under reduced motion. main.js never applies .stg there in the
   first place, so this is the second lock rather than the first. */
@media (prefers-reduced-motion: reduce) {
  .scroll-chapter .story-card.stg,
  .promises--run > .promise.stg,
  .buckets > .bucket.stg,
  .poster-work .project.stg,
  .poster-contact .detail.stg,
  .steps > li.stg { opacity: 1; transform: none; transition: none; }
}

/* The contact details are two panels now that the address has gone, so the
   grid stops being a hard three-up and sizes itself to whatever it holds. */
.poster-contact .contact__details {
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr));
}

/* ==========================================================================
   28. The hero art had no box
   Two faults, both mine, and they compounded.

   1. main.css centres the art with `transform: translateY(-50%)` against a
      `top: 50%`. My rule reset `top`, `right` and `translate` — but `translate`
      is the independent property, and this is the `transform` one. So the art
      kept being lifted by half its own height and rode up over the copy and
      the button above it.

   2. The scenes are built as absolutely positioned pieces inside a wrapper set
      to `height: 100%`. That resolved against a `.page-art` which, once it was
      in the flow rather than absolutely positioned, had no height at all — so
      the pieces laid themselves out around a box of zero height and spilled in
      every direction. A ratio gives them something to fill.
   ========================================================================== */

.poster-head .page-art {
  transform: none;
  aspect-ratio: 1 / 1;
  height: auto;
}
.poster-head .page-art > * { width: 100%; height: 100%; }

/* The tesseract is a plain SVG with its own intrinsic size and wants to keep
   it rather than be stretched to the ratio. */
.poster-head .page-art > svg {
  width: 100%;
  height: auto;
  max-width: 100%;
}

/* The word boxes the letter-split builds. poster.js wraps each word of a
   headword in a .wd so a line can only break at the spaces between them — but
   that only works if .wd is actually told not to break, and it never was. The
   JS half of that fix shipped without its CSS half, so "Visual identity" went
   on breaking after "identit" exactly as before. */
.promises--run .promise__word .wd {
  display: inline-block;
  white-space: nowrap;
}

/* ==========================================================================
   29. Tracking for Clash
   Every display size on this site was tracked between -0.035em and -0.045em,
   which was right for system-ui — SF and Segoe are drawn loose and want
   pulling in at poster sizes. Clash Display is drawn tight to begin with and
   its word space is narrow, so the same numbers closed the gaps between words
   entirely: the branding hero read as "Abrandyou canuse. Andown."

   So the tracking comes back most of the way, and a little word-spacing goes
   in to buy back the space the face does not give. Both are set here rather
   than at each rule so there is one number to turn if the face is ever
   changed again.
   ========================================================================== */

:root {
  --track-display: -0.015em;
  --track-word: 0.06em;
}

.poster-head .display,
.poster-cta .display,
.scroll-chapter .story__aside > .display,
.scroll-chapter .story-card:first-child .story-card__content > p:first-child,
.poster-work .project__client,
.promises--run .promise__word,
.bucket__title,
.steps h3,
.display,
.h-hero,
.h-1,
.h-2 {
  letter-spacing: var(--track-display);
  word-spacing: var(--track-word);
}

/* The numerals are single figures with no spaces in them, so they keep the
   tight setting that makes a large outlined number read as one shape. */
.scroll-chapter .story-card__number,
.poster-work .project::before,
.promises--run .promise__num,
.steps > li::before,
.bucket__num::before {
  letter-spacing: -0.04em;
  word-spacing: normal;
}

/* Buttons and chips are Clash Grotesk at small sizes, where the tight default
   is fine but the word gaps are still narrow. */
.btn,
.poster-work .project__scope,
.poster-contact .detail h2,
.section > .wrap > .label,
.band > .wrap > .label,
.page-hero__copy > .label,
.poster-head .label { word-spacing: 0.04em; }

/* Mobile keeps the desktop's entrances and depth, timed to the card being
   read. Native scrolling, content order and the desktop rules stay intact. */
@media (max-width: 899px) and (prefers-reduced-motion: no-preference) {
  html [data-reveal].reveal--group { opacity: 1; transform: none; }

  html [data-reveal] .stg.reveal-self {
    clip-path: none;
    transition: opacity 420ms var(--e-out), transform 620ms var(--e-soft);
    transition-delay: 0s;
  }
  html [data-reveal] .stg.reveal-self:not(.is-in):not(:focus-within) {
    opacity: 0;
    transform: translate3d(var(--in-x, 0), 20px, 0) rotate(var(--in-r, 0deg));
  }
  html [data-reveal] .stg.reveal-self:is(.is-in, :focus-within) {
    opacity: 1;
    transform: none;
  }

  .scroll-chapter .story-card::before {
    display: block;
    opacity: calc(var(--p-sheen) * 0.18);
  }

  .promises--run > .promise.is-scroll-active {
    --p-bg: var(--p-pop);
    --p-fg: var(--shade);
    --p-dim: rgb(var(--shade-rgb) / 0.82);
    --p-line: rgb(var(--shade-rgb) / 0.36);
  }
  .promises--run .promise__word .ch {
    transition: transform 420ms var(--e-out);
    transition-delay: calc(var(--c, 0) * 26ms);
  }
  .promises--run > .promise.is-scroll-active .promise__word .ch {
    transform: translateY(-0.09em);
  }
}

@media (max-width: 899px) and (min-height: 480px) and (prefers-reduced-motion: no-preference) {
  .scroll-chapter .story-card.is-mobile-stack,
  .poster-work .project.is-mobile-stack {
    content-visibility: visible;
    z-index: 2;
    box-shadow: 0 -1px 0 rgb(var(--ink-rgb) / 0.10),
                0 -20px 48px -36px rgb(var(--shade-rgb) / 0.8);
  }
  .scroll-chapter .story-card.is-mobile-stack:not(:last-child):not(:focus-within),
  .poster-work .project.is-mobile-stack:not(:last-child):not(:focus-within) {
    position: sticky;
    top: var(--mobile-stack-top, calc(var(--header-h) + 12px));
  }
}
