/* ==========================================================================
   Marc Mielau — Brand Transformations
   Layout pass. Figma: Carl M — Website, node 112:174 (1440 × 4200).
   Motion / transitions come later — this file is structure and type only.
   ========================================================================== */

/* --- scale ----------------------------------------------------------------
   The design is drawn at 1440 wide. --px is one of its pixels, and every
   fixed measurement in this file is written as the Figma number times that
   unit — so the numbers here still read as the numbers in the file.

   At or below 1440 --px is 1 and the breakpoints at the foot of this file do
   the work. Above it the whole page grows with the screen, instead of leaving
   a 1440-wide island stranded in the middle of a 27" or ultrawide display.

   Height is in the minimum as well as width, because the sections are as tall
   as they are wide: on a 3440 × 1440 ultrawide, scaling on width alone would
   push a section well past the top and bottom of the screen it is on. The
   ceiling stops a very large unscaled panel from turning the site into
   signage — past ~2500 × 1570 it stays put and the margins take the rest. */
@property --px {
  syntax: '<length>';
  inherits: true;
  initial-value: 1px;
}

:root {
  --px: clamp(1px, min(100vw / 1440, 100vh / 900), 1.75px);

  --mm: #090e16;          /* dark ground */
  --bg: #fffcf7;          /* cream ground */
  /* One ink scale, and it is the blue the site sits on. The ground's own
     #090e16 is so dark its blue disappears — text set in it read as plain
     black on the cream rooms while the dark rooms read as blue. The ink is
     therefore a navy: the same hue lifted just far enough that the eye can
     see the blue in it, so the cream rooms and the dark rooms belong to one
     palette. Muted ink is the same navy held back. */
  --text-primary: #182742;
  --text-muted: rgba(24, 39, 66, 0.55);   /* the navy at 55%, for cream grounds */
  --cream-muted: rgba(255, 252, 247, 0.6); /* the cream at 60%, for dark grounds */

  --gutter: calc(120 * var(--px));      /* section side padding at 1440 */
  --section-h: calc(840 * var(--px));   /* section height at 1440 */

  /* Sections stay full-bleed — the grounds still meet edge to edge — but the
     content inside them is held to the design's own width and centred, so
     everything past the ceiling becomes margin rather than a longer line. */
  --page-w: calc(1440 * var(--px));
  --pad: calc(max(0px, 100vw - var(--page-w)) / 2 + var(--gutter));
}

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

html {
  scroll-behavior: smooth;
  scrollbar-gutter: stable;  /* so holding the page still shifts nothing */
  /* iOS inflates type in landscape unless told not to, which would break the
     one relationship the whole page is built on — the tracking to the size */
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
}

/* The landing finishes arriving before the page will move. This refuses the
   user; it cannot refuse a script, since a hidden box is still a scroll
   container by spec — the scroll listener in reveal.js covers that. */
.is-held,
.is-held body {
  overflow: hidden;
}

body {
  margin: 0;
  background: var(--mm);
  color: var(--text-primary);
  font-family: "Instrument Sans", system-ui, -apple-system, "Segoe UI", sans-serif;
  -webkit-font-smoothing: antialiased;
}

/* Every picture here is part of the page, not an object to pick up. Left to
   itself the browser treats each one as a draggable thing: a drag across the
   work lifts a ghost of the image instead of moving the row, and a press held a
   moment on a phone raises the save-image sheet over it. The row is carried by
   the scroll, so a finger on a picture has to reach the scroll underneath it. */
img {
  display: block;
  max-width: 100%;
  -webkit-user-drag: none;
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
}

/* The grey box iOS paints over anything tapped is not in this palette; the
   :active states below answer the press instead. */
a, button { -webkit-tap-highlight-color: transparent; }

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

/* --- type -------------------------------------------------------------- */

/* Brand/Wordmark — the section title voice. 24/1, 500, tracked wide,
   uppercase. Stepped up from 20 so the titles lead their rooms instead of
   whispering over them — one size for every title on the site: the header
   line, the hero name, the work cards, the thought and momentum leads, the
   legal titles. */
.wordmark {
  margin: 0;
  font-size: calc(24 * var(--px));
  font-weight: 500;
  line-height: 1;
  /* 6.4 on 20 — kept as the ratio so it survives the phone's smaller mark
     instead of quietly loosening there */
  letter-spacing: 0.32em;
  text-transform: uppercase;
  color: var(--bg);
}

/* Lede — the headline voice. 40/1.4, 400, +0.01em tracking. One size for
   every headline on the site — the questions, the momentum offer, the
   footer's close — and clearly dominant over the 24 title above it. */
.lede {
  margin: 0;
  font-size: clamp(calc(26 * var(--px)), 2.8vw, calc(40 * var(--px)));
  font-weight: 400;
  line-height: 1.4;
  letter-spacing: 0.01em;
  color: var(--text-primary);
}

.lede-muted { color: var(--text-muted); }

/* Body — 16/1.5, 400, +0.01em tracking.

   One reading size for the whole page. The pacami copy found it first: 13 is
   a caption under a picture, and the moment a block runs to two sentences it
   is read rather than glanced at. The work captions run to two sentences too,
   and were the last thing still set at caption size — over the same 560 they
   were stretching to 85 characters a line where the pacami copy sits near 60.
   Raising them fixes both at once: the size and the measure.

   Tracking is a ratio rather than a pixel count, so the one relationship the
   page is built on — the tracking to the size — holds wherever the type
   lands. */
.body {
  margin: 0;
  font-size: calc(16 * var(--px));
  font-weight: 400;
  line-height: 1.5;
  letter-spacing: 0.01em;
}

.wordmark-muted { color: var(--cream-muted); }

/* Every versal line on the page is a title, and every title is the same
   title: the case studies' 24 wordmark. The header's line, the hero's name,
   the thought's and momentum's leads, the footer's close, the legal pages'
   titles — one size, one tracking, everywhere caps appear at title level.
   Only the small caps labels (the mail address, the footer links) stay at
   label size: they are UI, not titles. */
.body-muted { color: var(--text-muted); }

/* --- section shells ---------------------------------------------------- */

.header,
.hero,
.resonance,
.thought,
.momentum,
.pacami {
  width: 100%;
  min-height: var(--section-h);
  overflow: clip;
}

.header, .resonance, .momentum { background: var(--mm); }
.hero, .thought, .pacami { background: var(--bg); }

/* --- 01 header --------------------------------------------------------- */

.header {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  /* The landing owns the screen at every size, not only on the phone. 840 is
     the height it is drawn at, and it stays the floor — on a short laptop the
     opening still runs past the fold. On anything taller the frame grows to
     the screen instead of letting a band of the cream hero sit under it: the
     bar is pinned to the top, the title to the bottom, so the composition is
     the same frame however tall it gets. svh is the viewport with the browser
     bars showing, so nothing lurches when they retract mid-scroll. */
  min-height: max(var(--section-h), 100vh);
  min-height: max(var(--section-h), 100svh);
}

/* the field paints the ground; everything else in the header sits over it */
.field {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  z-index: 0;
}

.header-bar,
.header-title { position: relative; z-index: 1; }

.header-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  /* More air than the original 40 so the top doesn't read tighter than the
     title's 120 below — but not the full gutter, which sank the bar too far
     to read as a header. A bar wants to sit closer to its edge than a title
     block does; 64 keeps it a header while the frame still feels even. */
  padding: calc(64 * var(--px)) var(--pad) calc(40 * var(--px));
}

.wordmark[href] { text-decoration: none; color: var(--bg); }

/* language switch — top right of the bar, level with the wordmark */
.lang-switch {
  display: flex;
  align-items: center;
}
.lang-switch a {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  padding: calc(4 * var(--px)) calc(14 * var(--px));
  font-size: calc(13 * var(--px));
  font-weight: 400;
  line-height: 1.4;
  letter-spacing: calc(1.3 * var(--px));
  text-decoration: none;
  color: var(--cream-muted);
  transition: color 300ms var(--fade-ease, ease);
}
.lang-switch a:last-child { padding-right: 0; }
/* the line between options stays text-height however tall the tap target is */
.lang-switch a { position: relative; }
.lang-switch a + a::before {
  content: "";
  position: absolute;
  left: 0;
  top: 50%;
  transform: translateY(-50%);
  width: 1px;
  height: 1em;
  background: rgba(255, 252, 247, 0.3);
}
.lang-switch a:hover { color: rgba(255, 252, 247, 0.85); }
.lang-switch a.is-active { font-weight: 700; color: var(--bg); }
/* the bold width is reserved invisibly, so neighbours hold still
   when the selection moves */
.lang-switch a::after {
  content: attr(data-label);
  height: 0;
  overflow: hidden;
  visibility: hidden;
  font-weight: 700;
}
.lang-switch a:focus-visible {
  outline: 1px solid currentColor;
  outline-offset: 6px;
}

/* The title lines are wordmarks that can become sentences when the frame
   narrows, so they carry the wrapped-title leading at every size — the same
   1.35 the thought's lead and the momentum's bridge carry. On one line it
   changes nothing anyone can see. */
.header-title .wordmark {
  line-height: 1.35;
  overflow-wrap: break-word;
}

/* The line under the title is a sentence, not a second title. Set in the
   title's caps and tracking it ran the full width of the frame in German and
   read as a banner — two titles stacked, the lower only paler. Same face and
   size, but in its own case, weight and spacing, so the title leads and the
   line under it reads as what the title means. Three classes deep so the
   Chinese title tracking further down cannot reach it. */
.header-title .wordmark.wordmark-muted {
  font-weight: 400;
  letter-spacing: 0.01em;
  text-transform: none;
  line-height: 1.4;
}

.header-title {
  position: absolute;
  /* the same buffer on both sides, and the same one every section below
     uses — so the title sits on the page's single left edge */
  left: var(--pad);
  bottom: var(--gutter);
  display: flex;
  flex-direction: column;
  gap: calc(14 * var(--px));
}

/* --- the scroll cue -----------------------------------------------------
   Centred on the frame, low, under the title and clear of it. One chevron
   and nothing else — no word, in a page that otherwise says everything in
   words, because this one is addressed to the hand rather than to the
   reader. What it would have said is still there for a screen reader.

   The row runs the full width so the cue is centred on the frame rather
   than on a box of its own, and only the link inside it can be clicked. The
   centring cannot ride on transform: the arrival owns that property, and
   would throw the cue half a screen sideways on its way in. */

.scroll-cue {
  position: absolute;
  z-index: 1;
  left: 0;
  right: 0;
  /* Low — nearer the floor than the title, so it reads as the edge of the
     frame pointing on rather than as another line under the title. */
  bottom: calc(12 * var(--px));
  display: flex;
  justify-content: center;
  pointer-events: none;
}

.scroll-cue-link {
  pointer-events: auto;
  display: block;
  /* the arrow is a few pixels of stroke; the padding is what a finger
     actually gets, and it is the same finger at every size */
  padding: calc(12 * var(--px)) calc(16 * var(--px));
  text-decoration: none;
  /* going, once it has been obeyed */
  transition: opacity 500ms var(--fade-ease);
}

.scroll-cue.is-spent .scroll-cue-link {
  opacity: 0;
  pointer-events: none;
}

/* The arrows. Stroke only, no fill, and the stroke opts out of the scale —
   the same rule the cursor is drawn by, so the weight here is the weight
   there. Two of them rather than one: a single chevron is a direction, a
   pair is a direction being repeated, which is what the page needs said.
   The second is the fainter of the two, so the pair has a front and a back
   before either of them has moved. */
/* Bigger on a desktop frame than on a phone one — not by the page's scale,
   which would only keep the same mark the same size, but by a size of its
   own: a 20-wide chevron sits in the hand on a phone and disappears into a
   1440 frame. The weight does not grow with it; the stroke is held to one
   width at every size, so the mark gains room without gaining heft. The box
   is taller than the chevron because the hop needs somewhere to land. */
.scroll-cue-arrow {
  display: block;
  width: calc(26 * var(--px));
  height: calc(26 * var(--px));
  overflow: visible;
}

/* Stroke only, no fill, and the stroke opts out of the scale — the rule the
   cursor is drawn by. A shade heavier than the cursor's hairline, though:
   the ring is something you are already holding, and this is something you
   have to notice. */
.scroll-cue-arrow path {
  fill: none;
  stroke: var(--bg);
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
  opacity: 0.6;
  transition: opacity 300ms var(--fade-ease);
}

/* The nudge. The arrow is not there at first: one that arrives already
   moving is decoration. It waits until the page has been free to move for a
   few seconds and has not moved — the arrival takes 1.2s and the hold lifts
   as it ends, so 4s from the cue coming in is nearly three seconds of a
   still page — and only then does it appear. The fill holds the first
   keyframes through the wait, and the first keyframe is nothing.

   Two animations on one clock rather than one animation doing both jobs,
   because the light and the travel want different curves. The light
   breathes: out of nothing to full and back, eased at both ends of each
   half, so it never starts, stops or turns at speed — there is no moment in
   it the eye can catch as a step. The travel does not breathe; it drifts,
   evenly and only downward, the whole time the arrow can be seen, and goes
   back to the top while it cannot. An arrow that moves the way it points and
   never reverses in view is the smooth version of a hop. */
:root {
  /* the breath — a sine, symmetric, still at both ends */
  --breath: cubic-bezier(0.37, 0, 0.63, 1);
}

.motion .scroll-cue-arrow path {
  animation:
    cue-breathe 3600ms var(--breath) 4000ms infinite both,
    cue-drift   3600ms linear        4000ms infinite both;
}

/* The delay only counts while the animation runs, so holding it paused until
   the cue has arrived is what starts the wait at the right moment — and
   pausing it once the cue has been obeyed keeps it from nudging a reader who
   is already on their way. */
.motion .scroll-cue:not(.is-in) .scroll-cue-arrow path,
.motion .scroll-cue.is-spent .scroll-cue-arrow path {
  animation-play-state: paused;
}

/* up over the first half, down over the second, then a short dark rest
   before the next breath */
@keyframes cue-breathe {
  0%   { opacity: 0; }
  45%  { opacity: 1; }
  90%  { opacity: 0; }
  100% { opacity: 0; }
}

/* even travel across everything that is seen; the jump back to the top
   happens in the dark */
@keyframes cue-drift {
  0%   { transform: translateY(-2px); }
  90%  { transform: translateY(4px); }
  100% { transform: translateY(4px); }
}

/* Calm keeps the breath and takes the travel out of it — the same bargain
   the field makes when the machine is asked for quiet. Same wait, same
   rhythm, and the arrow stands where it is drawn. */
.motion.calm .scroll-cue-arrow path {
  animation: cue-breathe 3600ms var(--breath) 4000ms infinite both;
}

/* Under the pointer the arrow has been found, so it stops asking: the nudge
   is dropped and the arrow simply comes up to full. It has to be dropped
   rather than paused — the nudge holds the light through its whole cycle,
   waiting included, and a paused animation still would. Moving away starts
   it over from the top, wait and all, which is right: a reader who looked
   and did not go deserves the same two seconds as one who has not looked. */
@media (hover: hover) {
  .scroll-cue-link:hover .scroll-cue-arrow path {
    animation: none;
    opacity: 1;
  }
}

.scroll-cue-link:focus-visible {
  outline: 1px solid var(--cream-muted);
  outline-offset: 8px;
}

/* --- 02 hero ----------------------------------------------------------- */

.hero {
  display: flex;
  align-items: center;
  gap: calc(177 * var(--px));
  padding: 0 var(--pad);
}

.hero-portrait {
  flex: 0 0 auto;
  width: calc(494 * var(--px));
  height: calc(562 * var(--px));
  will-change: transform;  /* the scroll moves it once it is there */
}

.hero-portrait img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* The line under the name is one line. It is a single short sentence, and
   broken over two it reads as a paragraph that ran out of room rather than a
   statement. The block takes the width its own sentence needs — so every
   translation gets its own measure — and the 301 it was drawn at becomes the
   floor: where the row runs out of room the block stops shrinking and the
   line wraps exactly as it did before. */
.hero-text {
  flex: 0 1 auto;
  width: max-content;
  min-width: calc(301 * var(--px));
  display: flex;
  flex-direction: column;
  gap: calc(20 * var(--px));
}

.hero-text .wordmark { color: var(--text-primary); }

/* --- 03 resonance (horizontal rail) ------------------------------------ */

.resonance {
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: calc(120 * var(--px)) 0;
}

.rail {
  display: flex;
  align-items: flex-start;
  gap: calc(40 * var(--px));
  padding: 0 var(--pad);
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: none;
}

.rail::-webkit-scrollbar { display: none; }

/* The row travels under the finger, so a press held on a card is not someone
   reaching for a line of text — it is a scroll that has not started moving yet,
   and letting it raise a selection and a Copy menu stops the row dead. Only the
   row: everywhere else on the page holding a line still means what it usually
   means, and the footer's address stays copyable. */
.rail {
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
}

/* pinned: the section holds still while the rail travels past it */
.resonance.is-pinned {
  display: block;
  min-height: 0;
  padding: 0;
  overflow: visible;
}

.is-pinned .rail-stage {
  position: sticky;
  top: 0;
  height: 100vh;
  display: flex;
  align-items: center;
  overflow: hidden;
}

.is-pinned .rail {
  width: max-content;
  overflow: visible;
  will-change: transform;
}

/* The phone's magnet: rail.js lays one invisible point in the track per
   project, and the page snaps them to the top of the screen — the browser's
   own momentum snap, so a flick glides onto a project instead of being
   corrected onto one. Proximity, so the page above and below scrolls free. */
html.rail-snaps { scroll-snap-type: y proximity; }

.is-pinned .rail-track { position: relative; }

.rail-snap {
  position: absolute;
  left: 0;
  width: 1px;
  height: 1px;
  pointer-events: none;
  scroll-snap-align: start;
}

/* where you are in the row, on a phone — one hairline per project */
.rail-progress {
  position: absolute;
  left: 50%;
  transform: translate(-50%, -50%);
  display: flex;
  gap: 6px;
  pointer-events: none;
}

.rail-progress i {
  display: block;
  width: 18px;
  height: 1px;
  background: var(--bg);
  opacity: 0.28;
}

/* the frame is the viewport now, so the cards have to fit inside it — the
   media follows the card's width, so capping the width caps the height:
   84vh × 2/3 is the old 56vh ceiling */
.is-pinned .card { width: min(calc(780 * var(--px)), 84vh); }

/* Every frame in the rail is the same 3:2 — the classic photographic print.
   One proportion for all the work, height held at the 520 the rail always
   had, so the row reads as a series rather than a mixed feed. */
.card {
  flex: 0 0 auto;
  width: calc(780 * var(--px));
  display: flex;
  flex-direction: column;
  gap: calc(54 * var(--px));
}

.card-media {
  position: relative;
  width: 100%;
  aspect-ratio: 3 / 2;
}

/* absolute, so a picture squarer than the frame cannot stretch it — the
   frame decides and the picture fills */
.card-media img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* The words are held to three quarters of the frame they sit under: they
   share the picture's left edge and stop short of its right, so the frame
   still leads and the copy reads as the note beneath it. */
.card-content {
  width: 75%;
  display: flex;
  flex-direction: column;
  gap: calc(37 * var(--px));
}

.card-content .body {
  width: 100%;
  color: rgba(255, 252, 247, 0.75);
  /* Every caption is a different length, so left to itself each one ends on
     a different stub — one card finishing on a near-full line, the next on a
     word or two — and the row reads as uneven weights. Balanced, each block
     keeps its line count but spreads its words evenly across it, so every
     caption sits under its picture as the same even rectangle. */
  text-wrap: balance;
}

/* Chinese sets far denser than the Latin copy, and at three quarters six of
   the eight captions come to two lines while two spill a few characters onto
   a third. Held a little wider, all eight sit on two lines, and the row
   carries one weight again. */
@media (min-width: 600px) {
  /* the phone column is already full width, and stays so */
  html[lang="zh"] .card-content { width: 87%; }
}

/* --- 04 the thought ----------------------------------------------------
   The statement and the four questions, held in the middle of the frame at
   the design's own width. Figma 112:246. The order is carried by the scroll
   itself — see the staggered shifts in the markup and data-scroll-slide in
   js/scroll-motion.js — so the page never stops to deliver it. */

.thought {
  display: flex;
  align-items: center;
  padding: 0 var(--pad);
  color: var(--text-primary);
}

.thought-inner {
  flex: 1 0 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: calc(50 * var(--px));   /* the blank line the design leaves between them */
}

/* The title names the set; it is the frame around the questions, not the first
   of five. So it is set as a wordmark rather than a lede — small and tracked
   wide, the way the design has it — and the questions keep the only voice at
   reading size. It takes twice the blank line the set already leaves, and it
   resolves upward where they come in across, so the separation is carried by
   the type, the space and the motion at once. The mark is written for the dark
   sections; here it takes the page's ink.

   It is also the one wordmark on the page that is a sentence, so it wraps
   wherever the column is narrower than the line — from tablet widths down.
   Leading of 1 is right for a mark of one word and far too tight for two
   lines of this, so the title carries its own. */
.thought-lead {
  color: var(--text-primary);
  line-height: 1.35;
  margin-bottom: calc(50 * var(--px));
}

/* --- 05 momentum --------------------------------------------------------
   The dark answer to the cream questions: the same room, moving. The field
   carries the section; the copy holds the centre at the page's own measure,
   mirroring the thought's posture so the two rooms read as one exchange.
   Figma 224:71. */

.momentum {
  position: relative;
  display: flex;
  align-items: center;
  padding: 0 var(--pad);
}

/* the field paints the sky; the copy sits over it, like the header */
.momentum-copy {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  gap: calc(50 * var(--px));   /* the thought's own rhythm between lines */
  /* No cap of its own. The copy fills the section's content box the way the
     thought's does, so its right edge falls on the same gutter as its left and
     as every other section on the page. The measure is held by the page width,
     not by a width written only for this block. */
  flex: 1 0 0;
  min-width: 0;
}

/* The bridge answers the questions above and gives its weight to the offer.
   It does the same job the lead does in the thought, so it is set the same
   way — a wordmark rather than a lede, tracked wide, leaving the offer as
   the only voice at reading size in this room. It holds full ink the way the
   thought's lead does — the two titles are the same title on two grounds, so
   they carry the same strength and the same room below: the motion, not the
   color, is what steps them back once their set is up. Like the lead it is a
   wordmark that is a sentence, so it carries its own leading for the widths
   where it wraps. */
.momentum-bridge {
  color: var(--bg);
  line-height: 1.35;
  margin-bottom: calc(50 * var(--px));
}
.momentum-offer { color: var(--bg); }

/* --- 06 pacami ----------------------------------------------------------
   The reveal is typographic: the title arrives folded to PACAMI, and the
   scroll opens the word into the three names it is made of — the ink stays
   only on the letters that spell the platform. Figma 224:71. */

.pacami {
  display: flex;
  flex-direction: column;
  justify-content: center;
  /* a statement slide now — the block holds the middle of the frame */
  padding: 0 var(--pad);
}

.pacami-title {
  margin: 0;
  font-size: min(calc(96 * var(--px)), 6.7vw);
  font-weight: 500;
  line-height: 1;
  letter-spacing: 0.01em;
  white-space: nowrap;
  color: var(--text-primary);
}

.pacami-title span { color: var(--text-muted); }

/* The folded letters. js/scroll-motion.js drives their width from zero, so
   the unfolding is welded to the scroll like everything else on the page —
   without motion they are simply open, and the names are just there. */
.motion .pacami-title span {
  display: inline-block;
  overflow: hidden;
  white-space: pre;       /* the space after each name folds away with it */
  vertical-align: bottom; /* overflow:hidden moves an inline-block's baseline */
}

.pacami-copy {
  margin-top: calc(80 * var(--px));
  display: flex;
  flex-direction: column;
  /* half the air the block has under the title — the two sentences are a
     pair under the names, not two separate things */
  gap: calc(40 * var(--px));
  /* At 16 the copy is reading text rather than a caption, so it takes a
     reading measure of its own instead of the title's. The line under the
     names used to be set to their width — by tracking first (data-fit in
     js/scroll-motion.js), then by a cap — but a sentence this long only
     crosses 840 in one line up to about 14. Past that the names' measure
     stops being a measure and becomes an overhang with a word under it.

     560 is two thirds of the names and runs about 68 characters to the line,
     so both sentences break into two full lines and the block sits under the
     title as a block. It is the design's own width rather than the viewport's
     — the same fixed measure the momentum copy is held to — so the column
     stays put as the page narrows, until the page's own gutters take it. */
  max-width: calc(560 * var(--px));
  color: var(--text-primary);
}

/* This room used to set its own size, because it was the only block on the
   page that was read rather than glanced at. It is not any more — .body
   carries the 16 it found, and the work captions carry it too. What is left
   is the wrap: two lines on a 560 column is exactly the case where a last
   line can end up holding one word, and it is the only block on the page
   short enough and wide enough for that. On the phone's four-inch column the
   same setting spends its effort loosening the rag, so it stays here. */
.pacami-copy .body { text-wrap: pretty; }

/* --- 07 close / footer --------------------------------------------------
   The last section, on the same ground the page opened on. The close turned
   the footer strip into a full room: the question sits in the space above
   the bar, the bar keeps the floor. */

.site-footer {
  position: relative;
  /* a floor rather than a cap — the section clips its own canvas, so a fixed
     height would clip the content with it once the links stack */
  min-height: var(--section-h);
  display: flex;
  flex-direction: column;
  /* The same buffer every other section uses. The footer was inset 88 where
     the page runs on 120, so the closing question and the address started
     32px to the left of every line above them — the one place the page's
     single left edge broke, and the last thing the eye sees. */
  padding: calc(40 * var(--px)) var(--pad);
  background: var(--mm);
  overflow: clip;
}

/* The pair floats in the room above the bar; flex centering there lands it
   just above the section's true middle, so the block reads held, not sunk.
   The question keeps full ink and the address steps down one size — the same
   trade of prominence the hero and the momentum copy make. */
.footer-close {
  position: relative;
  z-index: 1;
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: center;
  /* the room's own rhythm — the same 50 the thought and the momentum leave
     between their title and their set */
  gap: calc(50 * var(--px));
}

/* The close is a title like every other room's — the same 24 versal voice —
   and like the leads it is a sentence, so it carries the wrapped leading. */
.footer-close .wordmark { color: var(--bg); line-height: 1.35; }

/* The invitation and the address answer the question together, so they are
   set closer to each other than the question is to either — near enough that
   the address reads as the end of the sentence above it rather than as a
   third line. */
.footer-answer {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: calc(12 * var(--px));
}

.footer-answer .body { color: var(--bg); }

/* The only action left on the page, said in the same voice as the line above
   it — the invitation and the address are one sentence in two lines, so they
   share the body's size and case. An underline drawn in on hover answers the
   pointer, since there is no brighter color for full ink to go to. */
.footer-mail {
  font-size: calc(16 * var(--px));
  font-weight: 400;
  line-height: 1.5;
  letter-spacing: 0.01em;
  color: var(--bg);
  text-decoration: none;
}

@media (hover: hover) {
  .footer-mail:hover {
    text-decoration: underline;
    text-underline-offset: calc(8 * var(--px));
    text-decoration-thickness: 1px;
  }
}

.footer-mail:active {
  text-decoration: underline;
  text-underline-offset: calc(8 * var(--px));
  text-decoration-thickness: 1px;
}

/* The links and the line of copyright are two different sizes sharing a row,
   so they are hung from their baselines rather than from their box tops —
   flex-start had the 16px strut of the list pushing the links 6px below the
   copyright they sit beside. */
.footer-content {
  position: relative;
  z-index: 1;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: calc(40 * var(--px));
  width: 100%;
}

/* UI/Label — 12/1.4, 400, +0.1em tracking, uppercase. 10 was the smallest
   type on the site and sat next to a 12px copyright line; the phone had
   already been given 12, so 12 is simply the size this label is. */
.footer-links {
  display: flex;
  gap: calc(64 * var(--px));
  margin: 0;
  padding: 0;
  list-style: none;
}

.footer-links a {
  font-size: calc(12 * var(--px));
  line-height: 1.4;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--cream-muted);
  text-decoration: none;
  white-space: nowrap;
}

/* Hover belongs to a pointer. On touch it latches after the tap and the link
   stays lit, which reads as a broken state rather than a response. */
@media (hover: hover) {
  .footer-links a:hover { color: var(--bg); }
}

/* Touch has no hover to answer with, so the press itself is the answer. */
.footer-links a:active { color: var(--bg); }

.footer-links a:focus-visible,
.footer-mail:focus-visible,
.wordmark[href]:focus-visible {
  outline: 1px solid currentColor;
  outline-offset: 6px;
}

/* UI/Breadcrumb — 12/1.4, 400, +0.24px tracking */
.footer-copy {
  margin: 0;
  font-size: calc(12 * var(--px));
  line-height: 1.4;
  letter-spacing: calc(0.24 * var(--px));
  color: var(--cream-muted);
  white-space: nowrap;
}

/* --- arrival ------------------------------------------------------------
   One easing, one distance, one duration for the whole site. Only applied
   when JS has decided motion is on, so without it nothing is ever hidden. */

:root {
  --glide: 1200ms;
  --glide-hero: 1600ms;    /* the portrait and its lines, unhurried */
  --glide-rise: calc(36 * var(--px));

  /* Two curves, because travel and light do not want the same one. Travel
     arrives and settles, so it leads hard and eases out. Light must not: the
     settle curve is already half-opaque a tenth of the way through, which
     turns any fade into a pop with a long invisible tail after it. */
  --glide-ease: cubic-bezier(0.16, 1, 0.30, 1);
  --fade-ease: cubic-bezier(0.38, 0.05, 0.25, 1);
}

.motion [data-reveal] {
  opacity: 0;
  transform: translate3d(0, var(--glide-rise), 0);
  transition:
    opacity var(--glide) var(--fade-ease),
    transform var(--glide) var(--glide-ease);
  transition-delay: var(--d, 0ms);
  will-change: opacity, transform;
}

.motion [data-reveal].is-in {
  opacity: 1;
  transform: none;
}

/* The hero is driven by the scroll rather than by a transition — see
   js/scroll-motion.js. It carries no transition at all: one would sit between
   the scroll and the element and break the connection. */
.motion [data-scroll-in] {
  opacity: 0;
  will-change: opacity, transform;
}

.motion [data-drift] { will-change: transform; }

/* calm: things still arrive, but nothing travels — fade in place */
.motion.calm [data-reveal] { transform: none; }

/* once it has arrived, stop paying for the hint */
.motion [data-reveal].is-in { will-change: auto; }

/* the field is first on screen — it has work to do before the type arrives */
.motion .field {
  opacity: 0;
  transition: opacity 500ms var(--glide-ease);
}

.motion.is-ready .field { opacity: 1; }

/* --- cursor ------------------------------------------------------------ */

/* Stroke only, no fill. Colour comes from the ground it is over, and the
   crossing between grounds is a fade, not a switch. The ring scales with the
   page; the stroke does not — a hairline is a hairline at any size.

   The box never changes size: opening and closing ride the `scale` property,
   which the compositor can carry on its own. Sizing the box instead would put
   a layout pass in every frame of the transition — and the frames are already
   spoken for, since the script places the ring on each one. The hairline
   survives the scale because the stroke opts out of it.

   Position is `translate`, and it must stay `translate`. The individual
   properties compose in a fixed order — translate, rotate, scale, transform —
   with `transform` applied to the point first and the rest multiplying what it
   returns. Carrying the position in `transform` therefore hands it to `scale`
   as well, and the ring flies a whole screen away as it opens instead of
   growing where it stands. The box is offset by half its own size so that its
   centre sits on the origin, which leaves `translate` holding the pointer
   position and nothing else, and leaves `scale` nothing to act on but the
   ring. */
.cursor {
  position: fixed;
  /* The open/close states change the ring's size rather than `scale` it: a
     scaled layer is a 28px bitmap stretched to 56 and goes soft and pixelly.
     Resized, the SVG is redrawn at every size and the hairline stays crisp. */
  --d: 28;
  top: calc(var(--d) * -0.5 * var(--px));
  left: calc(var(--d) * -0.5 * var(--px));
  width: calc(var(--d) * var(--px));
  height: calc(var(--d) * var(--px));
  overflow: visible;
  color: var(--bg);
  pointer-events: none;
  z-index: 9999;
  opacity: 0;
  translate: 0 0;
  will-change: translate;
  transition:
    color 380ms cubic-bezier(0.22, 1, 0.36, 1),
    /* eased at both ends: an ease-out alone spends half its travel in the
       first three frames and the rest creeping, which reads as a pop */
    width 360ms cubic-bezier(0.37, 0, 0.23, 1),
    height 360ms cubic-bezier(0.37, 0, 0.23, 1),
    top 360ms cubic-bezier(0.37, 0, 0.23, 1),
    left 360ms cubic-bezier(0.37, 0, 0.23, 1),
    opacity 200ms linear;
}

.cursor circle {
  fill: none;
  stroke: currentColor;
  stroke-width: 1;
  vector-effect: non-scaling-stroke;
}

.cursor.is-live { opacity: 1; }
.cursor[data-ground="dark"]  { color: var(--bg); }
.cursor[data-ground="light"] { color: var(--text-primary); }

/* 56 and 20 against the ring's own 28 */
.cursor.is-open { --d: 56; }
.cursor.is-down { --d: 20; }

@media (hover: hover) and (pointer: fine) {
  .has-cursor,
  .has-cursor a,
  .has-cursor button,
  .has-cursor textarea { cursor: none; }
}

/* --- responsive -------------------------------------------------------- */
/* Below 1440 --px rests at 1, so these are the design's own pixels. */

@media (max-width: 1439px) {
  :root { --gutter: 80px; }
  .hero { gap: clamp(48px, 12vw, 177px); }
}

@media (max-width: 1023px) {
  /* The gutter carries the notch with it in landscape, so the page keeps one
     left edge whichever way the phone is held. */
  :root {
    --gutter: calc(48px + max(env(safe-area-inset-left, 0px), env(safe-area-inset-right, 0px)));
    --section-h: 640px;
  }

  .header-bar { padding-top: calc(40px + env(safe-area-inset-top, 0px)); }
  /* On a phone column the sub-line runs two and three lines wide and reaches
     the middle of the frame, so the title steps up by the room the cue takes
     rather than standing in it. The offset is written out here rather than
     built from the gutter, so the two hold the same distance apart at both
     phone widths. The cue keeps its own room above the safe area, so the
     bounce never travels under the home indicator. */
  .header-title { bottom: calc(env(safe-area-inset-bottom, 0px) + 72px); }

  .scroll-cue { bottom: calc(env(safe-area-inset-bottom, 0px) + 4px); }
  /* 13, not 12: one arrow is shorter than the pair was, and 18 + 2 × 13 is
     the 44 a finger needs */
  .scroll-cue-link { padding: 13px 16px; }
  .scroll-cue-arrow { width: 18px; height: 18px; }

  .hero {
    flex-direction: column;
    align-items: flex-start;
    justify-content: center;
    gap: 48px;
    padding: 96px var(--pad);
  }
  .hero-portrait { width: min(100%, 494px); height: auto; aspect-ratio: 494 / 562; }
  /* stacked, so the block is the column and the floor goes with the row */
  .hero-text { width: 100%; min-width: 0; }

  .card { width: min(calc(780 * var(--px)), 82vw); }

  /* The pin is the only thing that moves the row, so the sideways scroll here
     is the motion-off fallback and nothing else. It still gets a snap, so that
     fallback settles on a project rather than between two, and it is kept from
     reaching through to the browser's back-swipe. */
  .rail {
    scroll-snap-type: x mandatory;
    scroll-padding-left: var(--pad);
    overscroll-behavior-x: contain;
  }
  .card { scroll-snap-align: start; }

  /* the stage is a screenful, and svh is the screenful that is always there */
  .is-pinned .rail-stage { height: 100svh; }

  /* one project to a screen — 72svh × 2/3 keeps the frame under 48svh */
  .is-pinned .card { width: min(calc(780 * var(--px)), 72svh, 88vw); }

  .thought { padding: calc(96 * var(--px)) var(--pad); }

  .momentum { padding: calc(96 * var(--px)) var(--pad); }

  .site-footer {
    height: auto;
    padding: calc(48 * var(--px)) var(--pad);
    padding-bottom: calc(32 * var(--px) + env(safe-area-inset-bottom, 0px));
  }
  /* the close keeps its room on small screens too — the question needs air
     above it or the ending reads as a caption on the footer */
  .footer-close { min-height: calc(280 * var(--px)); }
  .footer-content { flex-direction: column; gap: calc(20 * var(--px)); }
}

@media (max-width: 599px) {
  :root {
    --gutter: calc(24px + max(env(safe-area-inset-left, 0px), env(safe-area-inset-right, 0px)));
    --section-h: 560px;
  }

  /* only the size — the 0.32em tracking follows it down on its own */
  .wordmark { font-size: 16px; }

  /* the 3:2 frame follows the card's width, so the svh cap moves there:
     67.5svh × 2/3 keeps the picture under 45svh with room for the caption */
  .card { width: min(82vw, 67.5svh); }
  .card { gap: 28px; }
  .card-content { gap: 20px; }
  .card-content, .card-content .body { width: 100%; }
  .resonance { padding: 72px 0; }

  /* The desktop holds a 24-to-40 step between title and headline; 18-to-22
     flattened it to nearly nothing on the phone. 16-to-24 restores the
     ratio — the headline clearly leads again, and 24px over a 342px column
     still runs a line worth reading. */
  .lede { font-size: 24px; }
  .thought-inner { gap: 36px; }

  /* The desktop tracking turns the title into a ladder on this column. Pulled
     in, it holds to two lines, and it keeps twice the gap below it — the
     proportion it has on the desktop. */
  /* The titles that run to sentences wrap on this column, and the brand
     tracking would ladder them — it eases to hold each line together. */
  .thought-lead,
  .momentum-bridge,
  .footer-close .wordmark {
    letter-spacing: 2.6px;
  }

  .thought-lead { margin-bottom: 36px; }

  /* the bridge keeps the thought's rhythm: the same gap to its set, the
     same doubled room under the title */
  .momentum-copy { gap: 36px; }
  .momentum-bridge { margin-bottom: 36px; }

  /* the wordmark is a link before it is a logo — give the tap a target
     without moving the mark off the left edge of the bar */
  .header-bar .wordmark { padding: 12px 16px; margin: -12px -16px; }

  /* the language links become finger-sized taps */
  .lang-switch a { padding: 12px 10px; font-size: 14px; letter-spacing: 1px; }
  .lang-switch a:last-child { padding-right: 0; }

  /* Three links and a line of copy will not sit in a row on a phone — they were
     being clipped by the section. Stacked, each one also gets a row worth
     touching rather than 12px of type. */
  .footer-content { gap: 24px; }
  .footer-links { flex-direction: column; gap: 0; }
  .footer-links a {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
  }
  /* both labels step up together, so the pair still reads as one size */
  .footer-links a,
  .footer-copy { font-size: 13px; }

  /* the address is the page's one action — give the tap a full row */
  .footer-mail {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
    margin: -12px 0;
  }
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  /* scale keeps a transition here too — leaving it out did not remove the
     movement, only the part that made it readable */
  .cursor { transition: color 120ms linear, width 200ms linear, height 200ms linear, top 200ms linear, left 200ms linear, opacity 120ms linear; }
}

/* --- legal rooms (imprint / privacy) -------------------------------------
   One room, two pages. The sky the landing opens and closes on is pinned to
   the viewport here rather than to a section: it never moves, and the text is
   the only thing that travels. A veil between the two holds the small type up
   against the lit cloud tops — the sky still reads as sky, just held back a
   stop. Nothing arrives, nothing pins; a legal page owes the reader the text,
   not a performance. */

.legal-page { background: var(--mm); }

/* the sky, fixed to the viewport. Its own stacking context so the veil can
   sit over the canvas without either of them reaching the type. */
.legal-sky {
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
}

/* the veil — one flat stop of the page's own night over the whole sky */
.legal-sky::after {
  content: '';
  position: absolute;
  inset: 0;
  background: rgba(9, 14, 22, 0.62);
}

.legal {
  position: relative;
  z-index: 1;
  min-height: 100vh;
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  padding: 0 var(--pad);
}

/* the landing's bar, kept to the mark alone — the wordmark is the way back */
.legal-bar {
  display: flex;
  align-items: center;
  padding: calc(64 * var(--px)) 0 calc(40 * var(--px));
}

/* the text takes the room the imprint is too short to fill, so the footer
   still lands on the floor rather than halfway up a near-empty page */
.legal-body {
  flex: 1;
  width: min(calc(660 * var(--px)), 100%);
  padding: calc(56 * var(--px)) 0 calc(120 * var(--px));
}

.legal-title {
  margin: 0 0 calc(56 * var(--px));
  color: var(--bg);
}

/* Section heads take the wordmark's voice one size down. They separate by
   form — uppercase, tracked — rather than by colour, so the reading colour
   stays one colour all the way down the page. */
.legal-body h2 {
  margin: calc(64 * var(--px)) 0 calc(24 * var(--px));
  font-size: calc(13 * var(--px));
  font-weight: 500;
  line-height: 1.5;
  letter-spacing: calc(3.2 * var(--px));
  text-transform: uppercase;
  color: var(--bg);
}

.legal-body h2:first-child { margin-top: 0; }

/* The landing's reading size, since this is reading text — and a leading
   wider than its 1.5, because the eye has to hold its place through a
   numbered clause rather than a sentence. */
.legal-body p {
  margin: 0 0 calc(20 * var(--px));
  font-size: calc(16 * var(--px));
  line-height: 1.7;
  letter-spacing: 0.01em;
  color: var(--bg);
}

.legal-body p:last-child { margin-bottom: 0; }

.legal-body a {
  color: var(--bg);
  text-decoration: underline;
  text-underline-offset: calc(5 * var(--px));
  text-decoration-thickness: 1px;
}

/* the imprint is a set of labelled facts, not prose — the label steps back
   so the eye runs down the values */
.legal-row {
  margin: 0 0 calc(36 * var(--px));
}

.legal-row dt {
  margin: 0 0 calc(10 * var(--px));
  font-size: calc(12 * var(--px));
  line-height: 1.4;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--cream-muted);
}

.legal-row dd {
  margin: 0;
  font-size: calc(16 * var(--px));
  line-height: 1.7;
  letter-spacing: 0.01em;
  color: var(--bg);
}

/* the same bar the landing closes with, on the same floor */
.legal-foot {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: calc(40 * var(--px));
  width: 100%;
  padding-bottom: calc(40 * var(--px));
}

/* the page you are already on stops being an invitation */
.footer-links [aria-current="page"] { color: var(--bg); }

@media (max-width: 1023px) {
  .legal-bar { padding-top: calc(40px + env(safe-area-inset-top, 0px)); }
  .legal-body { padding: 40px 0 96px; }
  .legal-foot {
    flex-direction: column;
    gap: calc(20 * var(--px));
    padding-bottom: calc(32 * var(--px) + env(safe-area-inset-bottom, 0px));
  }
}

@media (max-width: 599px) {
  .legal-body h2 { font-size: 12px; letter-spacing: 2.6px; }
  .legal-foot { gap: 24px; }
  /* the mark is a link before it is a logo — give the tap a target */
  .legal-bar .wordmark { padding: 12px 16px; margin: -12px -16px; }
}

/* --- language ----------------------------------------------------------
   English is the markup, so English is already on screen and never waits.
   Any other language has to replace the text before it is seen, which is the
   one moment the page could show the wrong words — so it is held back for
   exactly as long as that takes. i18n.js lifts this the instant the swap is
   done, and lifts it on a timer regardless, so a script that never arrives
   costs a blink rather than a blank page. */
html.i18n-pending body { visibility: hidden; }

/* The legal pages carry the switch now too, so their bar reads like the
   landing's: mark on the left, languages on the right. */
.legal-bar { justify-content: space-between; }

/* The line under the privacy heading exists only in Chinese, where the policy
   below it is still English. Everywhere else it is empty and takes no room. */
.legal-note {
  margin: calc(-32 * var(--px)) 0 calc(56 * var(--px));
  font-size: calc(16 * var(--px));
  line-height: 1.7;
  color: var(--cream-muted);
}
.legal-note:empty { display: none; }

/* --- chinese -----------------------------------------------------------
   Instrument Sans has no Chinese, so the system's own face carries it — but
   it stays behind Instrument Sans in the stack, not in front of it, because
   the Latin that runs through the Chinese copy (pacami, BMW, DIAVON, JobRad
   Loop) is still the brand's and should still be set in the brand's face.

   The rest of this block is the tracking. The page's display sizes are drawn
   with a lot of air between letters, which is what makes a short Latin line
   read as a mark rather than a word. Chinese characters are already square and
   already evenly spaced: the same air pulls them apart into a list. So the
   tracking comes down, and the leading goes up, because a line of Chinese is
   taller than a line of Latin and wants more room under it. */
:lang(zh) {
  font-family: "Instrument Sans", "PingFang SC", "Hiragino Sans GB",
               "Microsoft YaHei", "Noto Sans SC", "Source Han Sans SC",
               system-ui, sans-serif;
}

/* 2.4 was tuned to the 20px mark; the ratio follows the title to 24 */
:lang(zh) .wordmark { letter-spacing: calc(2.9 * var(--px)); }
/* …except the header's pacami: it is the logo, not a Chinese title, so it
   keeps the tracking it has in every other language */
:lang(zh) .header-bar .wordmark[href] { letter-spacing: 0.32em; }
:lang(zh) .lede { line-height: 1.6; letter-spacing: 0; }
:lang(zh) .body { line-height: 1.75; letter-spacing: 0; }
:lang(zh) .legal-body h2 { letter-spacing: calc(1.6 * var(--px)); }
:lang(zh) .legal-body p { letter-spacing: 0; }

/* the switch's own Chinese label is two characters where the others are two
   letters — the tracking that spaces DE and EN spaces 中文 too widely */
.lang-switch a:lang(zh) { letter-spacing: calc(0.6 * var(--px)); }
