/* ==========================================================================
   ElevatorBeat — one design system for the whole site.

   THE STRUCTURAL DECISION
   -----------------------
   The page has two grounds, not one. Warm white is the primary canvas: it is
   where the work is explained, catalogued and read. Midnight is reserved for
   the architecture — full-frame, edge to edge, cut like film.

   Both are expressed through THE SAME custom properties. `:root` carries the
   paper palette; `.on-dark` overrides those properties in place. Every
   component below — eyebrows, rules, captions, buttons, plates, the rail —
   reads `var(--ground)` / `var(--text)` / `var(--accent)` and therefore works
   on either ground with no duplicate rulesets. Adding a section means picking
   a ground, not writing a second theme.

   Cyan is an annotation, not a brand wash: hairlines, ticks, the compare
   handle, the rail car, focus rings. On paper the primary button is ink,
   because a cyan button on cream is a SaaS reflex, not a studio one. Copper
   comes out of the elevator lighting and appears only as rules and numerals.
   Anything else saturated on this page is product artwork.
   ========================================================================== */

/* Instrument Serif, self-hosted (SIL Open Font License 1.1) — headlines only.
   Two 15 KB files rather than a webfont service: no third-party connection on
   the critical path, and the display face is the only thing that needs one.
   Body copy uses the platform UI sans, which is already resident. */
@font-face {
  font-family: "Instrument Serif";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("assets/fonts/instrument-serif-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Instrument Serif";
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url("assets/fonts/instrument-serif-latin-italic.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

:root {
  --font-display: "Instrument Serif", Georgia, "Times New Roman", serif;
  --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
    "Helvetica Neue", Arial, sans-serif;

  /* ---- PAPER (primary) ----
     Warm white, never #fff — #fff next to these paintings reads as a hole in
     the page. --ground-sink is the one step down, for quiet panels. */
  --ground: #F7F3EC;
  --ground-sink: #EFE8DD;
  --text: #12161C;              /* 16.3:1 on --ground */
  --text-soft: #4E565F;         /*  6.6:1 on --ground */
  --accent: #096F7B;            /*  5.3:1 on --ground */
  --accent-solid: #12161C;
  --accent-solid-text: #F7F3EC;
  --accent-solid-hover: #2C3440;
  --line: rgba(18, 22, 28, .14);
  --line-strong: rgba(18, 22, 28, .30);

  /* Copper, off the elevator's amber lighting. Rules and numerals only —
     never a fill, never a button, never type at reading size. */
  --copper: #A8672F;

  /* Fixed midnight values, for the places that need dark regardless of the
     current ground (scrims, the arrival header). */
  --midnight: #08131F;
  --midnight-deep: #050C15;
  --ivory: #F3ECE1;
  --cyan: #00D8ED;

  --container: 1240px;
  --page-x: clamp(22px, 5vw, 76px);
  --section-y: clamp(96px, 11vw, 190px);
  --radius: 2px;                /* editorial, not app-store rounded */

  --ease: cubic-bezier(.22, .61, .36, 1);
}

/* ---- MIDNIGHT ----
   Same properties, different values. Everything downstream just works. */
.on-dark {
  --ground: #08131F;
  --ground-sink: #050C15;
  --text: #F3ECE1;              /* 15.9:1 on --ground */
  --text-soft: #A9BACA;         /*  9.4:1 on --ground */
  --accent: #00D8ED;            /* 10.7:1 on --ground */
  /* Ivory, NOT cyan. A big cyan rectangle over a photograph is the generic
     CTA reflex the rest of this system exists to avoid; cyan stays a hairline
     annotation on both grounds, and the solid action is simply the inverse of
     whatever it sits on. */
  --accent-solid: #F3ECE1;
  --accent-solid-text: #050C15;
  --accent-solid-hover: #FFFFFF;
  --line: rgba(243, 236, 225, .16);
  --line-strong: rgba(243, 236, 225, .30);
  --copper: #E0A86A;

  background: var(--ground);
  color: var(--text);
}

*, *::before, *::after { box-sizing: border-box; }
html { scroll-behavior: smooth; -webkit-text-size-adjust: 100%; }
[id] { scroll-margin-top: 96px; }

body {
  margin: 0;
  background: var(--ground);
  color: var(--text);
  font-family: var(--font-sans);
  /* Body copy is information, not texture. At 19/1.62 the reading sizes on this
     page sat close enough to the captions and metadata that a paragraph read as
     another band of grey; a step up in size and a longer leading is what
     separates the two. */
  font-size: 20px;
  line-height: 1.7;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  /* Nothing on this page is allowed to push the document sideways. */
  overflow-x: clip;
}
img, svg, picture { display: block; max-width: 100%; }
a { color: inherit; }
p { margin: 0 0 1.1em; }
p:last-child { margin-bottom: 0; }
button, a { -webkit-tap-highlight-color: transparent; }

/* ---------- Display tiers ----------
   Four sizes, used deliberately. Tier is chosen by the job a headline is doing,
   not by its heading level, so the semantic outline stays honest.

   The largest tier is spent ONCE per page, on the h1. Used three or four times
   it stops being emphasis and becomes the page's default voice, which is how a
   hierarchy flattens while every individual headline still looks handsome. */
h1, h2, h3, h4 {
  font-family: var(--font-display);
  font-weight: 400;
  line-height: 1.03;
  letter-spacing: -.018em;
  margin: 0 0 .34em;
  text-wrap: balance;
}
.display-xl { font-size: clamp(54px, 8.4vw, 124px); line-height: .98; letter-spacing: -.026em; }
.display-l  { font-size: clamp(40px, 5.4vw, 74px); }
.display-m  { font-size: clamp(32px, 3.5vw, 52px); }
.display-s  { font-size: clamp(23px, 2vw, 29px); line-height: 1.2; letter-spacing: -.008em; }

.lede {
  font-size: clamp(20px, 1.6vw, 25px);
  line-height: 1.5;
  color: var(--text-soft);
  max-width: 44ch;
}
/* 58ch at 19px was a comfortable measure; at 20px it is a long one, so the
   measure comes in as the size goes up. */
.body-copy { max-width: 54ch; color: var(--text-soft); }
.body-copy strong { color: var(--text); font-weight: 600; }

/* Section label. The cyan tick is the smallest instance of the rail motif —
   it marks anywhere the page starts a new floor. */
.eyebrow {
  display: flex;
  align-items: center;
  gap: 15px;
  margin: 0 0 24px;
  font-size: 13.5px;
  font-weight: 600;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--accent);
}
.eyebrow::before {
  content: "";
  flex: none;
  width: 30px;
  height: 1px;
  background: currentColor;
}
.on-dark .eyebrow::before { box-shadow: 0 0 8px rgba(0, 216, 237, .5); }

/* Gallery-label caption. One treatment for every caption on the site, so a
   drag portrait, an architectural frame and a product shot are all annotated
   the same way. */
.caption {
  margin: 14px 0 0;
  font-size: 16px;
  line-height: 1.45;
  color: var(--text-soft);
}
.caption b {
  display: block;
  font-weight: 600;
  font-size: 13.5px;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--text);
}

.container {
  width: min(calc(100% - (2 * var(--page-x))), var(--container));
  margin-inline: auto;
}
.section { padding-block: var(--section-y); }

/* ---------- Accessibility helpers ---------- */
.skip-link {
  position: fixed;
  left: 16px;
  top: 12px;
  z-index: 1000;
  transform: translateY(-180%);
  background: var(--cyan);
  color: var(--midnight-deep);
  padding: 12px 18px;
  border-radius: var(--radius);
  font-weight: 700;
  text-decoration: none;
  transition: transform .2s var(--ease);
}
.skip-link:focus { transform: translateY(0); }
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}
a:focus-visible,
button:focus-visible,
input:focus-visible,
textarea:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: var(--radius);
}

/* ==========================================================================
   THE PLATE — the one image component
   --------------------------------------------------------------------------
   Every photograph on the site goes through this. The rules are not stylistic
   preferences; each one closes a specific way that a rounded image leaks white
   pixels at its corners:

   - The radius and `overflow: hidden` are on the SAME element. Split across a
     wrapper and a child, the child's square corners poke out of the parent's
     round ones.
   - No border. A 1px stroke plus a radius is precisely where the corner seam
     comes from, because the stroke and the clip antialias on different
     subpixel boundaries. Separation comes from the ground instead.
   - The background is the current ground, so if a sub-pixel of the wrapper
     ever shows through, it is invisible rather than a bright triangle. It is
     never white on a dark section, because `--ground` follows `.on-dark`.
   - `aspect-ratio` makes the box height deterministic, so it always agrees
     with the intrinsic ratio of the file inside it and the image never has to
     letterbox.
   - `isolation: isolate` keeps the clip in its own stacking context, so a
     transform on a sibling can't promote the child out of the clip.

   Full-frame cinematic images set `--plate-radius: 0` — editorial photography
   is not a card.
   ========================================================================== */
.plate {
  position: relative;
  overflow: hidden;
  border-radius: var(--plate-radius, var(--radius));
  background: var(--ground);
  isolation: isolate;
}
.plate > img,
.plate > picture > img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: var(--focus, 50% 50%);
}
.plate > picture { width: 100%; height: 100%; }

/* A screenshot is the one kind of picture on this site that must not be
   cropped: a phone screen cut to a gallery ratio loses the row that proves it
   is an interface. So the plate takes the capture's own ratio, passed in as
   --ratio, and the radius comes up to something that reads as a device rather
   than as a print. */
.plate--screen {
  aspect-ratio: var(--ratio, 3 / 4);
  --plate-radius: 14px;
  background: var(--ground-sink);
}

/* ---------- Buttons — one system, three weights ----------
   Every action on the site is tracked uppercase at one size. That is what makes
   a button read as building signage rather than as a web control, and it is why
   the label never needs to be bigger to carry weight — the letterspacing does
   the work.

   SOLID is spent once per page, on the arrival. OUTLINE is the working action
   everywhere else: a hairline box, which is the same annotation vocabulary as
   the rail and the plan drawing. QUIET is a text action with a rule under it.

   All three invert per ground through --accent-solid / --text / --line-strong,
   so there is one button system, not a light set and a dark set. */
.button-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 16px;
  margin-top: 38px;
}
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 11px;
  min-height: 54px;
  padding: 0 28px;
  border: 1px solid transparent;
  border-radius: var(--radius);
  font-family: var(--font-sans);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: .13em;
  text-transform: uppercase;
  text-decoration: none;
  cursor: pointer;
  transition: background .18s var(--ease), border-color .18s var(--ease),
    color .18s var(--ease);
}
.btn-primary {
  background: var(--accent-solid);
  color: var(--accent-solid-text);
}
.btn-primary:hover { background: var(--accent-solid-hover); }

/* The working action. Hairline box, current ground's ink, and on hover the
   border comes up to full strength rather than the box filling in — the same
   restraint as the quiet action's rule. */
.btn-outline {
  background: none;
  border-color: var(--line-strong);
  color: var(--text);
}
.btn-outline:hover { border-color: var(--text); }

/* Row-scale actions: the directory at the ground floor and the footer bar,
   where a 54px control would out-weigh the row it sits in. */
.btn-sm {
  min-height: 42px;
  padding: 0 18px;
  font-size: 11.5px;
  letter-spacing: .12em;
}

/* The quiet action. Its underline is the rail again: a hairline that grows
   from the left rather than a border that switches on. */
.btn-quiet {
  position: relative;
  min-height: 54px;
  padding: 0 2px;
  background: none;
  border: 0;
  color: var(--text);
  font-family: var(--font-sans);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: .13em;
  text-transform: uppercase;
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  gap: 11px;
  cursor: pointer;
}
.btn-quiet::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 16px;
  height: 1px;
  background: currentColor;
  opacity: .4;
  transform: scaleX(1);
  transform-origin: left;
  transition: opacity .18s var(--ease);
}
.btn-quiet:hover::after { opacity: 1; }
.btn svg { flex: none; fill: currentColor; }
/* The Apple glyph is 384×512 — held to its own ratio so it isn't stretched. */
.apple-icon { width: 16px; height: 21.3px; margin-top: -2px; }

/* An app that hasn't shipped. A real focusable button rather than a dead link,
   swapping its label to "Coming soon" on hover and keyboard focus. Both labels
   share one grid cell, so the width is the longer of the two and nothing
   reflows. It keeps the outline the shipped app's action has — the two rooms
   must be labelled on identical terms — but goes soft and loses the hover
   brightening, because a phone has no hover and a control that looks live for
   something undownloadable is the failure to avoid. */
.btn-upcoming {
  cursor: default;
  color: var(--text-soft);
  border-color: var(--line);
}
.btn-outline.btn-upcoming:hover { border-color: var(--line); }
.button-label { display: grid; }
.button-label > span { grid-area: 1 / 1; transition: opacity .16s var(--ease); }
.button-label-hover { opacity: 0; }
.btn-upcoming:hover .button-label-rest,
.btn-upcoming:focus-visible .button-label-rest { opacity: 0; }
.btn-upcoming:hover .button-label-hover,
.btn-upcoming:focus-visible .button-label-hover { opacity: 1; }

/* ---------- Motion ----------
   Two gestures on arrival. Copy rises 12px as it enters; full-frame
   architecture settles from 1.03 to 1 — an elevator arriving at a floor, not a
   parallax effect. Everything here is disabled under prefers-reduced-motion.

   The gallery's aperture is the third and last piece of motion on the page, and
   it is not a gesture: it is a window that travels with the scroll over a wall
   that never moves. See THE APERTURE below. */
.js .reveal {
  opacity: 0;
  transform: translateY(12px);
  transition: opacity .8s var(--ease), transform .8s var(--ease);
  transition-delay: var(--delay, 0ms);
}
.js .reveal.is-visible { opacity: 1; transform: none; }

.js .chapter-media { transform: scale(1.03); transition: transform 1.6s var(--ease); }
.js .chapter.is-visible .chapter-media { transform: none; }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .js .reveal,
  .js .chapter-media,
  .js .chapter.is-visible .chapter-media {
    opacity: 1;
    transform: none;
    transition: none;
  }
  .button-label > span,
  .site-nav a::after,
  .btn-quiet::after { transition: none; }
}

/* ==========================================================================
   THE RAIL — the page's elevator floor indicator
   --------------------------------------------------------------------------
   A hairline in the window's left gutter with a lit segment tracking scroll
   position, a tick at the top of every chapter, and — above 1400px — the
   numbered floors named: 01 STUDIO, 02 GALLERY, 03 GALLERY, GF GROUND FLOOR.
   Those are the same labels the sections carry in the markup and the same ones
   the directory repeats at the end, which is the point: the page has ONE
   numbering system and it belongs to the building.

   The stops are in PAGE order, top to bottom, not in building order. A real
   panel would put the top floor at the top, but this one has a car on it that
   tracks scroll, and a car that runs backwards up a panel while the reader
   scrolls down is an indicator that lies. Ordering follows the thing being
   indicated.

   It carries nothing you can't already see, which is why it is aria-hidden, and
   it is only built above 1100px where there is gutter to spare.

   It is `position: fixed`, so it crosses both grounds as you scroll. script.js
   therefore stamps data-ground on it from whichever ground is under the probe
   line, and the hairline, ticks and labels follow. The cyan car reads on either
   ground and never changes.
   ========================================================================== */
.rail {
  position: fixed;
  left: 32px;
  top: 0;
  bottom: 0;
  z-index: 15;
  width: 1px;
  background: rgba(18, 22, 28, .18);
  pointer-events: none;
  transition: background .3s var(--ease);
}
.rail[data-ground="dark"] { background: rgba(243, 236, 225, .18); }

.rail-tick {
  position: absolute;
  left: -3px;
  width: 7px;
  height: 1px;
  background: rgba(18, 22, 28, .34);
  transition: background .3s var(--ease);
}
.rail[data-ground="dark"] .rail-tick { background: rgba(243, 236, 225, .34); }
/* A floor gets a longer tick than a plain chapter break: the panel has stops
   and it also has travel between them. */
.rail-tick.is-floor { left: -5px; width: 11px; background: rgba(18, 22, 28, .5); }
.rail[data-ground="dark"] .rail-tick.is-floor { background: rgba(243, 236, 225, .5); }

/* The floor plate: numeral over name, tracked, hairline-quiet. Only rendered
   where there is genuinely room for it. */
.rail-floor {
  position: absolute;
  left: 15px;
  transform: translateY(-50%);
  font-family: var(--font-sans);
  font-size: 9.5px;
  font-weight: 600;
  line-height: 1.5;
  letter-spacing: .16em;
  text-transform: uppercase;
  white-space: nowrap;
  color: rgba(18, 22, 28, .34);
  transition: color .3s var(--ease);
}
/* Over a photograph the plate needs a little separation to stay readable — the
   observation band is a bright rust sky right where 01 and 02 sit. */
.rail[data-ground="dark"] .rail-floor {
  color: rgba(243, 236, 225, .42);
  text-shadow: 0 1px 3px rgba(5, 12, 21, .55);
}
/* The numeral is the one copper thing in the gutter, matching every other
   numeral on the site. */
.rail-floor b { display: block; font-weight: 600; color: var(--copper); opacity: .9; }
.rail[data-ground="dark"] .rail-floor b { color: #E0A86A; }

.rail-car {
  position: absolute;
  left: 0;
  width: 1px;
  height: 78px;
  top: var(--rail-pos, 0px);
  background: linear-gradient(to bottom,
      rgba(0, 216, 237, 0), var(--cyan) 45%, rgba(0, 216, 237, 0));
  box-shadow: 0 0 14px rgba(0, 216, 237, .7);
}
@media (max-width: 1099px) { .rail { display: none; } }
/* The numeral needs the gutter to itself; below 1400px the centred container
   reaches back far enough that a floor plate would be drawn under the first
   words of every line. */
@media (max-width: 1399px) { .rail-floor { display: none; } }
/* And the NAME needs about 90px more. Between 1400 and 1575 the container is
   pushed off centre to give it that room (see the .container override in the
   responsive section); from 1576 up, a centred 1240px container clears the panel
   on its own and nothing needs shifting. */

/* ==========================================================================
   Header
   Transparent over the arrival image; on scroll it detaches into a solid
   paper bar. Subpages have no photographic arrival, so they ship .is-solid
   from the markup and start in the scrolled appearance.
   ========================================================================== */
.site-header {
  position: absolute;
  inset: 0 0 auto;
  z-index: 20;
  padding: 24px 0;
  border-bottom: 1px solid transparent;
  color: var(--ivory);
  transition: background .25s var(--ease), padding .25s var(--ease),
    border-color .25s var(--ease), color .25s var(--ease);
}
.site-header.is-scrolled,
.site-header.is-solid {
  position: fixed;
  padding: 14px 0;
  background: rgba(247, 243, 236, .90);
  backdrop-filter: blur(14px);
  border-bottom-color: rgba(18, 22, 28, .14);
  color: var(--text);
}
.site-header.is-solid { position: sticky; }

.nav-shell {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
}
.brand {
  display: inline-flex;
  align-items: center;
  gap: 14px;
  color: inherit;
  text-decoration: none;
  font-family: var(--font-display);
  font-size: 27px;
  letter-spacing: -.01em;
}
/* The car drawn as a line rather than a slab: a single cyan weight for the
   shell and the three floor indicators, so the mark reads at the same
   optical density as the serif beside it instead of shouting over it. It
   stands taller than the cap height — the car passes the floor it names. */
.brand-mark { width: 13.6px; height: 30px; flex: none; }
/* The name is one flex item, not two. .brand is a flex container, so an
   unwrapped <em> would be laid out beside "Elevator" with the 14px mark gap
   between them — the name would read as two words no matter how it is kerned. */
.brand-name { white-space: nowrap; }
/* Beat is the half of the name that moves. The italic B carries a wide left
   sidebearing, which on top of the roman r's own still opens a hairline
   word space — hence the kern. */
.brand-name em { font-style: italic; margin-left: -.045em; }

.site-nav { display: flex; align-items: center; gap: 34px; }
.site-nav a {
  position: relative;
  color: inherit;
  text-decoration: none;
  font-size: 11.5px;
  font-weight: 600;
  letter-spacing: .15em;
  text-transform: uppercase;
  opacity: .78;
  transition: opacity .18s var(--ease);
}
.site-nav a:hover,
.site-nav a[aria-current="page"] { opacity: 1; }
/* The only nav decoration: a 1px cyan underline, i.e. the rail again. */
.site-nav a::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -7px;
  height: 1px;
  background: var(--cyan);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform .22s var(--ease);
}
.site-header.is-scrolled .site-nav a::after,
.site-header.is-solid .site-nav a::after { background: var(--accent); }
.site-nav a:hover::after,
.site-nav a[aria-current="page"]::after { transform: scaleX(1); }

.menu-toggle {
  display: none;
  flex-direction: column;
  justify-content: center;
  gap: 5px;
  width: 46px;
  height: 46px;
  background: none;
  border: 1px solid currentColor;
  border-radius: var(--radius);
  color: inherit;
  opacity: .5;
  cursor: pointer;
}
.menu-toggle span {
  display: block;
  width: 20px;
  height: 1px;
  margin-inline: auto;
  background: currentColor;
}

/* ==========================================================================
   CHAPTERS — full-frame architecture
   --------------------------------------------------------------------------
   The building is not decoration behind the content; it is where the studio
   works, so it gets the whole frame. Copy sits directly on it where the
   composition leaves somewhere quiet to put it.

   Scrims are cinematic lighting, not a uniform wash. Each modifier below is
   anchored to the corner its own copy occupies and fades to transparent well
   before the architecture it must not flatten. There is deliberately no
   default scrim: a chapter with no text gets none at all.
   ========================================================================== */
.chapter {
  position: relative;
  display: flex;
  overflow: hidden;
  /* Follows the chapter's own ground, so a light chapter never flashes
     midnight before its photograph arrives. */
  background: var(--ground-sink);
  isolation: isolate;
}
.chapter-media {
  position: absolute;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
}
/* <picture> is an inline wrapper by default, so an <img> inside it resolving
   `height: 100%` measures against the picture's own content height, not the
   chapter — which left every art-directed chapter's photograph sitting at its
   intrinsic height with bare ground underneath. It has to be told to fill. */
.chapter-media picture { display: block; width: 100%; height: 100%; }
.chapter-media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: var(--focus, 50% 50%);
}

/* The arrival photograph drifts slower than the copy stacked over it (script.js
   translates it on scroll), reading as depth rather than one flat plane moving
   as a unit. This wrapper overscans top and bottom by 8% so that drift always
   has real pixels to reveal — .chapter's overflow:hidden clips the excess, and
   the translate is capped in JS well inside that 8%, so an edge can never show. */
.parallax-media { position: absolute; inset: -8% 0; height: 116%; }
/* No width here on purpose. This element also carries .container, and a
   `width: 100%` would beat it — which is what silently pushed every chapter's
   copy out to the raw viewport edge instead of onto the site grid. */
.chapter-inner { position: relative; z-index: 2; }
.chapter::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background: var(--scrim, none);
}

/* Copy lower-left, over open ground. Radial so it stays a pool of shade under
   the type rather than a band across the picture. */
.scrim-bl {
  --scrim:
    radial-gradient(120% 90% at 6% 100%,
      rgba(5, 12, 21, .92) 0%, rgba(5, 12, 21, .74) 32%,
      rgba(5, 12, 21, .30) 58%, rgba(5, 12, 21, 0) 78%),
    linear-gradient(to top, rgba(5, 12, 21, .55) 0%, rgba(5, 12, 21, 0) 34%);
}
/* THE INVERTED SCRIM. The morning frame is the one chapter on the page that
   does not carry .on-dark, because darkening a pale morning sky enough to hold
   ivory type turns the morning into another dusk — the failure where every
   photograph on a site ends up the same dark scene.
   So this scrim LIFTS instead. Same component, same corner-anchored radial,
   warm white instead of midnight, and the type over it is near-black.

   The stops are set to the weakest lift that still clears AA against the actual
   pixels of house-day, measured at each element's own measure rather than across
   the whole band: eyebrow 4.86:1, body copy 5.34:1, headline 13.8:1. Lifting
   harder passes by more and costs the sky, which is the thing this chapter
   exists to keep — so do not raise these without re-measuring what it buys. */
.scrim-lift {
  --scrim:
    radial-gradient(105% 95% at 4% 46%,
      rgba(247, 243, 236, .94) 0%, rgba(247, 243, 236, .80) 30%,
      rgba(247, 243, 236, .34) 58%, rgba(247, 243, 236, 0) 76%);
}

.chapter--arrival {
  min-height: max(660px, 100svh);
  align-items: flex-end;
  padding-block: clamp(120px, 12vw, 160px) clamp(64px, 8vw, 110px);
}
/* The arrival headline shares its frame with the moon and the tower, and at the
   full display-xl size it ran to three lines and rode straight through both.
   Smaller and on a wider measure it breaks after "collective" and sits in the
   open sky — and the moon stays the thing the composition is built around. */
/* Two notches down from the raw display-xl and on a wider measure, so the
   headline breaks after "collective" and sits in the open sky in two lines. At
   the full size it ran to three and the third line crossed the pavilion. */
.chapter--arrival h1 { font-size: clamp(42px, 5vw, 76px); max-width: 20ch; }

/* The two interludes. Both are the same component sized to hold a short passage
   over the open third of their own painting, and neither is allowed to grow into
   a second full-height arrival — the arrival is the only chapter on the page that
   gets the whole window. */
.chapter--morning,
.chapter--observation {
  min-height: clamp(440px, 56vw, 660px);
  align-items: center;
  padding-block: clamp(72px, 8vw, 120px);
}
/* The morning frame is on paper, so its headline and body take the ink values
   the rest of the page's copy takes. Stated explicitly because this is the one
   chapter where a reader of the markup would reasonably expect .on-dark. */
.chapter--morning h2 { max-width: 20ch; }
.chapter--morning .body-copy { max-width: 34ch; }
.chapter--observation h2 { max-width: 22ch; }
.chapter--observation .body-copy { max-width: 42ch; }
/* The panorama is 2.33:1 and this chapter is nearer 2.6:1, so cover crops a
   little from top and bottom; anchoring high keeps the moon and the whole
   horizon, and gives away only foreground rock. */
.chapter--observation .chapter-media img { --focus: 50% 32%; }

.on-dark .chapter-inner .lede { color: rgba(243, 236, 225, .88); }
.on-dark .chapter-inner .body-copy { color: rgba(243, 236, 225, .84); }

/* ==========================================================================
   01 — THE STUDIO
   --------------------------------------------------------------------------
   Prose in the middle, flanked by the two other languages a studio describes
   a building in: a photograph of the room, and a survey annotation with a plan.
   Three columns rather than the usual two, because the annotation is genuinely
   a third kind of thing and folding it under the picture would make it a
   caption, which it is not.
   ========================================================================== */
.studio-grid {
  display: grid;
  grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr) minmax(0, .6fr);
  gap: clamp(36px, 4.5vw, 76px);
  align-items: start;
}
.studio-grid h2 { max-width: 13ch; }
.studio-grid .body-copy { margin-top: clamp(24px, 2.4vw, 34px); max-width: 46ch; }
.studio-figure { margin: 0; }

/* THE ARCH. The plate's own radius, passed through --plate-radius as a full
   compound value, so the rounding still lives on the same element as the clip —
   which is the entire reason .plate has no border (see the component above).
   The elliptical top echoes the pavilion's curved glass; the bottom stays at
   the site's 2px, because a shape closed at both ends is a lozenge, not a
   window. */
.arch {
  aspect-ratio: 3 / 4;
  --plate-radius: 50% 50% var(--radius) var(--radius) / 27% 27% var(--radius) var(--radius);
}

/* ---------- The survey annotation ----------
   Coordinates and a plan drawing, in the same hairline-and-tracked-numeral
   vocabulary as the rail. Cyan, because on this site cyan annotates and does
   not decorate. */
.survey { display: grid; gap: clamp(22px, 2.4vw, 34px); }
.survey-line {
  margin: 0;
  padding-top: 12px;
  border-top: 1px solid var(--accent);
  font-size: 10.5px;
  font-weight: 600;
  line-height: 1.85;
  letter-spacing: .16em;
  text-transform: uppercase;
}
.survey-line b { display: block; font-weight: 600; color: var(--accent); }
.survey-line span { display: block; color: var(--text-soft); }

.plan { border: 1px solid var(--line); padding: 14px 14px 11px; }
.plan svg { width: 100%; height: auto; }
.plan-line { stroke: var(--text-soft); stroke-width: .8; opacity: .55; }
/* The roof overhang: dashed, because it is above the floor being drawn — the
   convention that keeps two concentric circles from reading as a target. */
.plan-overhang {
  stroke: var(--text-soft);
  stroke-width: .6;
  stroke-dasharray: 5 4;
  opacity: .36;
}
.plan-core { stroke: var(--copper); stroke-width: 1.1; }
.plan-dot { fill: var(--accent); }
/* The construction lines a plan carries but does not emphasise. */
.plan-axis {
  stroke: var(--text-soft);
  stroke-width: .5;
  stroke-dasharray: 3 4;
  opacity: .3;
}
.plan-label {
  margin: 10px 0 0;
  font-size: 9.5px;
  font-weight: 600;
  letter-spacing: .15em;
  text-transform: uppercase;
  color: var(--text-soft);
}
.plan-label span { color: var(--line-strong); padding-inline: 4px; }

/* ==========================================================================
   THE ROOMS — Dragify and StraightPic, on identical terms
   --------------------------------------------------------------------------
   A wall label on the left and the evidence on the right. The two apps get
   different evidence, because a collection and a transform are not
   demonstrated the same way — but the label, the claim, the tag row, the action
   and the spacing are the same furniture in both, which is what makes them read
   as two rooms in one building rather than as two products on one page.
   ========================================================================== */
.room-grid {
  display: grid;
  /* 24rem, not 22: the tag row is three items and at 22rem the third wrapped to
     a line of its own on every room. */
  grid-template-columns: minmax(0, 24rem) minmax(0, 1fr);
  gap: clamp(40px, 5vw, 88px);
  align-items: start;
}
.room-label h2 { margin-bottom: .18em; }
/* The claim: what the app is for, at display scale but a tier down from its
   name. It is the only place on the page where the serif carries a full
   sentence, which is what stops the room from being a name and a paragraph. */
.room-claim {
  /* The display face, explicitly. The .display-* classes carry size only — the
     serif is attached to h1-h4 — so a <p> at a display tier renders in the sans
     unless it asks. Without this the claim and the paragraph under it were the
     same typeface at two sizes, which is a weaker distinction than the page
     needs at the exact point where it says what the app is for. */
  font-family: var(--font-display);
  margin: 0 0 clamp(20px, 2vw, 28px);
  color: var(--text);
  text-wrap: balance;
}
.room-label .body-copy { max-width: 38ch; }

/* ---------- The tag row ----------
   Platform, kind and status, with a 14px line-drawn glyph each. It replaces a
   four-cell ruled specification table: at this size the same three facts read
   in one pass, and the row can sit inside a directory row later without being
   redrawn as a second component. */
.tags {
  display: flex;
  flex-wrap: wrap;
  gap: 10px 22px;
  margin: clamp(26px, 3vw, 36px) 0 0;
  padding: 16px 0 0;
  border-top: 1px solid var(--line);
  list-style: none;
  font-size: 11.5px;
  font-weight: 600;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--text-soft);
}
.tags li { display: inline-flex; align-items: center; gap: 8px; }
.tags svg { flex: none; width: 15px; height: 15px; }
/* Stroked, never filled: these are diagram glyphs, the same drawing language as
   the plan and the rail. */
.tags .ic {
  fill: none;
  stroke: var(--accent);
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.tags--compact { margin: 0; padding: 0; border-top: 0; gap: 8px 18px; }

/* ==========================================================================
   THE APERTURE — Dragify's evidence, behind a window that moves
   --------------------------------------------------------------------------
   NOTHING HERE IS COLOUR-CORRECTED, deliberately. These frames are what the app
   returns, so a saturation filter or a warm wash would be the site quietly
   misrepresenting the product to protect its own palette. Coherence comes from
   the ground, the gap and the grid instead.

   THE WALL DOES NOT SCROLL. `.mosaic` is a window cut in the page; the wall of
   twelve looks behind it is pinned to the VIEWPORT and never moves. So the page
   rises, the window climbs the wall, and not one queen travels a pixel — which
   is the opposite of a parallax layer, a thing that moves slower than the page
   rather than not at all. Frames are cut hard by the window's edge on purpose:
   an opening in a wall, not a carousel.

   ONE RATIO NOW. The old mosaic mixed 3:2 and 5:4 into the 2:3 so that three
   columns of different widths would finish level. Behind an aperture nothing
   has to finish level, so every frame goes back to the single ratio the app
   exports and no look is cropped at all.

   FOUR ROWS, AND THE MIDDLE TWO ARE THE POINT. Between entering at the bottom
   of the viewport and leaving at the top, the window sweeps a band of
   100vh + 2 × its own height — more than four rows of frames, so the script
   clamps at the wall's own edge and the wall drifts rather than climbing the
   whole band. Eighteen frames filled the band outright, but then no single look
   held the window for long. Twelve is the compromise: the six that carry the
   section sit in rows two and three, where the aperture is parked for most of
   the pass, and the rows above and below are never seen whole — they are cut by
   the window's edge at both ends, which is what makes the wall read as a piece
   of something larger rather than as a grid that happens to be six wide.

   The clamp is what keeps the midnight ground from ever showing through, so the
   wall must stay TALLER THAN THE APERTURE at every width. Four 2:3 frames are,
   by a wide margin.
   ========================================================================== */
.mosaic {
  position: relative;
  /* Tall enough to hold two frames and a gutter, never so tall that the wall
     stops reading as something seen THROUGH something. */
  --aperture-h: clamp(400px, 62svh, 600px);
  --wall-gap: clamp(10px, 1.2vw, 16px);
}
/* One flat grid, filled row by row — no column wrappers. Column wrappers would
   put the wall in DOM order top-to-bottom-then-across, which means the frames
   that matter land in a different place at two columns than at three. Row-major
   keeps them in the middle of the wall at every width. */
.mosaic-plane {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--wall-gap);
  align-items: start;
}
.mosaic .plate { aspect-ratio: 2 / 3; }

/* The window only exists where the script can drive it. Without JS the same
   markup is an ordinary static wall, which is also the state the aperture falls
   back to below. `translate3d` is declared rather than left to the first scroll
   so the plane starts life on its own compositor layer. */
.js .mosaic {
  height: var(--aperture-h);
  overflow: hidden;
  /* The one reveal on the page that fades without rising. The 12px rise is a
     transition, and a transition fires no scroll events — so the wall would be
     carried up with the window it is meant to be independent of, and only find
     its anchor again on the visitor's next scroll. */
  transform: none;
}
.js .mosaic-plane {
  position: absolute;
  inset: 0 0 auto;
  transform: translate3d(0, 0, 0);
}

/* THE COMPOSITOR TAKES OVER WHERE IT CAN. script.js's rAF loop reads
   getBoundingClientRect() off a scroll event, which is main-thread work — any
   frame the main thread is busy, the wall's counter-shift arrives a beat after
   the window has already scrolled, and the clip shows old-wall-through-new-window
   for that one frame. That beat is the jitter, and the mismatched slice is the
   fold.

   A view() timeline is one the compositor drives directly, no JS in the loop,
   so there is no beat to miss. Its `cover` range is exactly "subject entering
   the scrollport to subject having fully left it", which is exactly this
   sweep — and because the window's box travels linearly across that range, the
   wall's counter-shift is linear too, so the whole pass is two keyframes.

   THE TIMELINE IS DECLARED ON THE WINDOW, NOT THE WALL. The obvious spelling —
   `animation-timeline: view()` on the plane — is silently dead here: the
   `overflow: hidden` that cuts the aperture also makes `.mosaic` a scroll
   CONTAINER, so a view() resolved from inside it takes `.mosaic` as its
   scrollport rather than the document. `.mosaic` never scrolls, so that
   timeline is permanently inactive and the wall never moves at all. Naming the
   timeline on `.mosaic` makes `.mosaic` the SUBJECT instead — its scrollport is
   then the document, which is the thing actually scrolling — and descendants
   can reference the name. */
@supports (animation-timeline: view()) {
  .js .mosaic {
    view-timeline-name: --mosaic-window;
    view-timeline-axis: block;
  }
  .js .mosaic-plane {
    animation: mosaic-sweep linear both;
    animation-timeline: --mosaic-window;
    animation-range: cover;
  }
}
/* --mosaic-start is where the wall sits as the window begins its pass; script.js
   computes it, because it depends on the wall's real rendered height. `both`
   holds that value before the range and 0 after it. */
@keyframes mosaic-sweep {
  from { transform: translate3d(0, var(--mosaic-start, 0px), 0); }
  to { transform: translate3d(0, 0, 0); }
}

/* Where the window cannot travel — no script, or motion turned off — the plane
   sits still and the wall becomes an ordinary twelve-up grid, four rows deep.
   That is a longer section than the aperture would have been, which is the
   honest trade: without a window to look through, the frames are the section. */
@media (prefers-reduced-motion: reduce) {
  .js .mosaic { height: auto; overflow: visible; }
  .js .mosaic-plane { position: static; animation: none; }
}

/* ==========================================================================
   THE DEVICES — StraightPic's evidence
   --------------------------------------------------------------------------
   Two real captures from one session in the app: the handles going down, and
   the frame coming back upright. The second is dropped half a step so the pair
   reads as a sequence rather than as a comparison table — and because two phone
   screens whose bezels line up exactly look like an App Store submission.
   ========================================================================== */
.devices {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(20px, 3vw, 44px);
  align-items: start;
  max-width: 660px;
}
.devices figure { margin: 0; }
.devices figure:last-child { margin-top: clamp(20px, 3vw, 52px); }
.devices .plate { --plate-radius: 22px; }

/* ==========================================================================
   GF — THE GROUND FLOOR
   --------------------------------------------------------------------------
   Where to get the work, as a board rather than a closing statement. Both rows
   carry the same tag row they were labelled with upstairs, so the directory is
   a summary of the building and not a second description of it.
   ========================================================================== */
.ground-grid {
  display: grid;
  grid-template-columns: minmax(0, 22rem) minmax(0, 1fr);
  gap: clamp(40px, 5vw, 88px);
  align-items: start;
}
.ground-grid h2 { max-width: 15ch; }
.ground-grid .body-copy { max-width: 34ch; }
/* The signature. The only italic on the page, and the only line of display type
   at reading size — it is the studio initialling its own work. */
.signoff {
  margin-top: clamp(28px, 3vw, 40px);
  font-family: var(--font-display);
  font-style: italic;
  font-size: clamp(23px, 2vw, 28px);
  line-height: 1.2;
  color: var(--text);
}

.rooms {
  list-style: none;
  margin: 0;
  padding: 0;
  border-top: 1px solid var(--line);
}
/* DIRECT CHILDREN ONLY. Each row contains a nested <ul class="tags">, so a
   descendant `.rooms li` also matched every tag in it — handing each tag the
   four-column row grid, the row padding and a bottom hairline, which shattered
   the row. The tag row is a component that travels; anything styling rows has to
   say `> li`. */
.rooms > li {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) minmax(0, auto) auto;
  align-items: center;
  gap: clamp(16px, 2.4vw, 34px);
  padding-block: clamp(20px, 2.4vw, 30px);
  border-bottom: 1px solid var(--line);
}
.rooms .app-icon { width: 52px; height: 52px; border-radius: 12px; }
.rooms-name h3 { margin: 0 0 4px; }
.rooms-name p { margin: 0; font-size: 16.5px; color: var(--text-soft); }
.rooms-actions { display: flex; flex-wrap: wrap; gap: 8px; margin: 0; }

/* ==========================================================================
   Footer — one bar
   --------------------------------------------------------------------------
   The four-column sitemap this replaced was a larger navigation apparatus than
   the site it was navigating: seven pages, listed twice. One bar instead —
   identity, the five links that are not already in the header, and the one
   action.
   ========================================================================== */
.site-footer {
  background: var(--ground);
  border-top: 1px solid var(--line);
  padding-top: clamp(40px, 4vw, 56px);
}
.footer-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: clamp(24px, 3vw, 44px);
  padding-bottom: clamp(32px, 3.5vw, 48px);
}
.footer-brand {
  display: inline-flex;
  align-items: center;
  gap: 16px;
  color: inherit;
  text-decoration: none;
}
.monogram { width: 44px; height: 44px; flex: none; color: var(--text); }
.footer-brand-text {
  font-size: 11.5px;
  font-weight: 700;
  line-height: 1.7;
  letter-spacing: .17em;
  text-transform: uppercase;
}
.footer-brand-text span { display: block; font-weight: 500; color: var(--text-soft); }

.footer-links {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 12px 28px;
  font-size: 15.5px;
  color: var(--text-soft);
}
.footer-links a { text-decoration: none; }
.footer-links a:hover { color: var(--accent); }
.footer-action { flex: none; }

.footer-bottom {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding-block: 20px;
  border-top: 1px solid var(--line);
  color: var(--text-soft);
  font-size: 14px;
}
.footer-social { display: flex; gap: 10px; }
.footer-social a {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
}
.footer-social a:hover { border-color: var(--accent); color: var(--accent); }
.footer-social svg { width: 15px; height: 15px; fill: currentColor; }

/* ---------- App furniture that outlived the old layout ----------
   The icon, at the one size the directory uses it, and the quiet policy links.
   Dragify's terms and privacy stay canonical on dragifyapp.com and open in the
   dialog at the foot of this file, so these must never compete with the room's
   own action: one size down, soft, hairline pipes between them. */
.app-icon { flex: none; border-radius: 17px; }
.app-links {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 15px;
  margin-top: 26px;
  font-size: 15px;
  color: var(--text-soft);
}
.app-links a { text-decoration: none; border-bottom: 1px solid var(--line-strong); }
.app-links a:hover { color: var(--accent); border-bottom-color: var(--accent); }
.app-links span { color: var(--line-strong); }

/* ---------- The install action: a link on a phone, a code on a desk ----------
   Dragify is an iPhone app, so the App Store link is the finished action on a
   device that can act on it and a detour on one that cannot — a desktop visitor
   who clicks lands on Apple's web page and still has to go find the app on
   their phone. Same href either way; only the presentation swaps.

   The swap is width AND a fine pointer, never one alone and never a UA sniff.
   The failure that matters is handing a phone a code it cannot scan with
   itself, so both signals have to agree before the QR appears and anything
   ambiguous — touchscreen laptop, narrow window, a browser that reports
   nothing — falls back to the link, which works everywhere.

   No Android branch: there is no Android build. Both rooms are already
   labelled "iPhone app" in their tags, and Apple's own page is the honest
   destination for anyone who taps through on one anyway.

   The QR carries its own ink-on-paper palette baked into the SVG rather than
   currentColor (see tools/appstore-qr.py) — an ivory-on-midnight code is an
   inverted code, and not every scanner reads those. It takes the hairline and
   2px radius so it reads as the same .plate vocabulary as the artwork. */
.install-qr { display: none; }
@media (min-width: 900px) and (pointer: fine) {
  .install-btn { display: none; }
  .install-qr {
    display: inline-flex;
    align-items: center;
    gap: 15px;
    text-decoration: none;
    color: inherit;
  }
}
/* Sizes are set from the module count, not from taste. The code is 37 modules
   across including its quiet zone, and a phone camera wants roughly 4px per
   module to read a screen at desk distance — hence 148px on the arrival. The
   two furniture placements trade that down knowingly; nothing goes below 88px,
   which is about where a code stops being a code and becomes a texture. */
.install-qr img {
  width: var(--qr-size, 148px);
  height: var(--qr-size, 148px);
  flex: none;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
}
.install-qr:hover img { border-color: var(--accent); }
.install-qr-text {
  font-size: 12px;
  font-weight: 700;
  line-height: 1.65;
  letter-spacing: .17em;
  text-transform: uppercase;
}
.install-qr-text span {
  display: block;
  font-weight: 500;
  letter-spacing: .13em;
  color: var(--text-soft);
}
/* The footer's copy is one bar of furniture, not an arrival — the code drops to
   the height of the brand block beside it and loses its caption's second line. */
.footer-action.install-qr { --qr-size: 96px; gap: 13px; }
/* Directory rows are built on a 52px icon, so any scannable code sets the row's
   height instead of the icon — Dragify's row runs taller than StraightPic's and
   that is the honest trade. Shrinking it to fit the rhythm would have produced
   a code nobody could scan, which is a worse row than an uneven one. */
.rooms-actions .install-qr { --qr-size: 88px; gap: 12px; }

/* ==========================================================================
   Contact form — its own page, not the homepage.
   The homepage closes on a decision, not a five-field form; the form lives
   where somebody who has already decided to write goes looking for it.
   ========================================================================== */
.contact-form {
  margin-top: clamp(32px, 4vw, 48px);
  max-width: 580px;
  display: flex;
  flex-direction: column;
  gap: 20px;
}
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
.contact-form label {
  display: flex;
  flex-direction: column;
  gap: 9px;
  font-size: 13.5px;
  font-weight: 600;
  letter-spacing: .07em;
  text-transform: uppercase;
  color: var(--text-soft);
}
.contact-form input,
.contact-form textarea {
  font: inherit;
  font-size: 17px; /* below 16px, iOS Safari zooms the page on focus */
  font-weight: 400;
  letter-spacing: normal;
  text-transform: none;
  color: var(--text);
  background: var(--ground-sink);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  padding: 14px 16px;
  transition: border-color .18s var(--ease);
}
.contact-form textarea { resize: vertical; min-height: 160px; }
.contact-form input:hover, .contact-form textarea:hover { border-color: var(--text-soft); }
.contact-form input:focus, .contact-form textarea:focus { border-color: var(--accent); }
.contact-form button { align-self: flex-start; margin-top: 6px; }
/* Honeypot: in the DOM for bots, never shown and never focusable. */
.contact-form input[name="_honey"] {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  opacity: 0;
}

/* ==========================================================================
   Static content pages (support / privacy / terms / licenses / contact)
   ========================================================================== */
.page-hero { padding-block: clamp(120px, 12vw, 176px) 0; }
.page-hero .display-l { max-width: 17ch; }
.page-hero .lede { margin-top: 6px; }
/* The hero carries no bottom padding because a .page-body normally follows and
   supplies its own. On thanks.html the hero IS the whole page, so without this
   its button row sits flush against the footer's top hairline. */
main > .page-hero:last-child { padding-bottom: clamp(88px, 9vw, 132px); }
/* Narrower than the container, for a readable measure — but starting at the
   container's LEFT edge. These sections also carry .container, and its
   `margin-inline: auto` would centre the narrow box, leaving the body copy
   indented from the heading sitting directly above it. */
.page-body {
  width: min(calc(100% - (2 * var(--page-x))), 68ch);
  margin-inline: max(var(--page-x), calc((100% - var(--container)) / 2)) auto;
  padding-block: clamp(48px, 5vw, 76px) var(--section-y);
}
.page-body h2 {
  font-size: clamp(26px, 2.4vw, 32px);
  margin-top: 2em;
  padding-top: 1.6em;
  border-top: 1px solid var(--line);
}
.page-body h2:first-child { margin-top: 0; padding-top: 0; border-top: 0; }
.page-body h3 { font-size: clamp(21px, 1.9vw, 25px); margin-top: 1.8em; }
.page-body p, .page-body li { color: var(--text-soft); font-size: 19.5px; }
.page-body ul, .page-body ol { padding-left: 1.15em; }
.page-body li { margin-bottom: .5em; }
.page-body a { color: var(--text); text-decoration-color: var(--line-strong); }
.page-body a:hover { color: var(--accent); }

/* ==========================================================================
   Canonical-policy dialog
   Dragify's terms and privacy policy are maintained on dragifyapp.com; this
   frames that page rather than keeping a second copy here that would then need
   updating twice. Native <dialog>, so focus trapping, Esc-to-close and
   background inertness are the browser's job.
   ========================================================================== */
.legal-modal {
  width: min(940px, calc(100vw - 32px));
  height: min(86svh, 900px);
  max-width: none;
  max-height: none;
  padding: 0;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  background: var(--ground);
  color: var(--text);
  overflow: hidden;
}
.legal-modal::backdrop { background: rgba(5, 12, 21, .70); }
.legal-modal-bar {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 15px 16px 15px 24px;
  border-bottom: 1px solid var(--line);
}
.legal-modal-bar h2 { flex: 1; min-width: 0; margin: 0; font-size: 22px; }
.legal-modal-open { flex: none; font-size: 15px; color: var(--accent); text-decoration: none; }
.legal-modal-open:hover { text-decoration: underline; }
.legal-modal-close {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  padding: 0;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  background: none;
  color: var(--text);
  cursor: pointer;
}
.legal-modal-close:hover { border-color: var(--accent); }
.legal-modal-close svg { width: 19px; height: 19px; fill: currentColor; }
.legal-modal-frame {
  display: block;
  width: 100%;
  height: calc(100% - 71px);
  border: 0;
  background: #fff;
}

/* ==========================================================================
   Responsive
   Mobile is not the desktop page compressed. Headline scale is preserved, body
   copy never drops below 19px, measures shorten, section spacing stays
   generous, copy always precedes the image it is talking about, and the arrival
   frame switches to a crop that was composed for portrait rather than to
   whatever `object-fit: cover` happens to leave behind.
   ========================================================================== */
/* The floor panel's own column. Between these widths the panel is showing its
   names and a centred container would run underneath them, so the container
   stops centring and takes a fixed left inset instead. Full-bleed photography is
   untouched — the panel overlays the pictures, which is the point of a fixed
   indicator. Above 1575px the centred container already clears the panel. */
@media (min-width: 1400px) and (max-width: 1575px) {
  .container {
    width: min(calc(100% - 168px - var(--page-x)), var(--container));
    margin-left: 168px;
    margin-right: auto;
  }
}

@media (max-width: 1180px) {
  /* The studio's three columns become a passage with its two annotations side
     by side underneath. Stacking all three would put the plan drawing directly
     under the photograph, where it reads as that photograph's caption rather
     than as a second description of the building. */
  .studio-grid { grid-template-columns: minmax(0, 1fr) minmax(0, .58fr); }
  .studio-grid > .reveal:first-child { grid-column: 1 / -1; }
  .studio-grid h2 { max-width: 18ch; }
  .studio-grid .body-copy { max-width: 62ch; }

  .room-grid,
  .ground-grid { grid-template-columns: 1fr; gap: 40px; }
  .room-label h2,
  .ground-grid h2 { max-width: 18ch; }
  .room-label .body-copy,
  .ground-grid .body-copy { max-width: 54ch; }
  /* The wall label has the page's full measure once it is alone on a row, and
     the tag row and action should not stretch across all of it. */
  .room-label,
  .ground-grid > .reveal:first-child { max-width: 640px; }
}

@media (max-width: 900px) {
  .rooms > li { grid-template-columns: auto minmax(0, 1fr); row-gap: 16px; }
  .rooms .tags--compact,
  .rooms-actions { grid-column: 2; }
}

@media (max-width: 760px) {
  body { font-size: 19px; }
  .display-xl { font-size: clamp(44px, 12vw, 68px); }
  .display-l  { font-size: clamp(38px, 10vw, 54px); }
  .display-m  { font-size: clamp(31px, 8.4vw, 44px); }

  .studio-grid { grid-template-columns: 1fr; gap: 34px; }
  .studio-grid > .reveal:first-child { grid-column: auto; }
  .arch { max-width: 420px; }

  .menu-toggle { display: flex; }
  .site-nav {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    padding: 6px var(--page-x) 24px;
    background: rgba(247, 243, 236, .98);
    backdrop-filter: blur(14px);
    border-bottom: 1px solid rgba(18, 22, 28, .14);
    color: var(--text);
    transform-origin: top;
    transform: scaleY(0);
    opacity: 0;
    pointer-events: none;
    transition: transform .2s var(--ease), opacity .2s var(--ease);
  }
  .site-nav.is-open { transform: scaleY(1); opacity: 1; pointer-events: auto; }
  .site-nav a { padding: 15px 0; font-size: 14px; border-bottom: 1px solid var(--line); }
  .site-nav a::after { display: none; }

  .chapter--arrival { min-height: max(620px, 100svh); padding-top: 104px; }
  /* A phone viewport is about 0.46 wide-to-tall; even the 4:5 portrait crop has
     to give up roughly 40% of its width to fill it. The brief's own rule for
     this frame is "keep the elevator recognisable and retain either the moon or
     the rocket", so the window is pushed right: tower, lit observation deck,
     palms and the rocket's trail stay; the moon is the thing that goes. */
  .chapter--arrival .chapter-media img { --focus: 78% 68%; }

  /* Portrait leaves no open corner wide enough for a radial pool, so both scrims
     become vertical bands sized to the copy that sits on them — the dark one
     rising from the bottom, the light one falling from the top, each still
     fading out well before the architecture it must not flatten. */
  .scrim-bl {
    --scrim: linear-gradient(to top,
        rgba(5, 12, 21, .93) 0%, rgba(5, 12, 21, .78) 34%,
        rgba(5, 12, 21, .40) 64%, rgba(5, 12, 21, .10) 88%);
  }
  .scrim-lift {
    --scrim: linear-gradient(to bottom,
        rgba(247, 243, 236, .95) 0%, rgba(247, 243, 236, .82) 40%,
        rgba(247, 243, 236, .38) 70%, rgba(247, 243, 236, 0) 92%);
  }
  .chapter-inner h1 { max-width: 100%; }
  .chapter-inner .lede { max-width: 36ch; }
  /* The morning copy moves to the top of its frame, because that is the half the
     lift now covers. */
  .chapter--morning { align-items: flex-start; }
  .chapter--observation { min-height: clamp(420px, 92vw, 560px); }

  /* Two columns rather than three: a 2:3 frame at a third of a phone's measure
     is 110px wide, which is a thumbnail of a queen and not a look. The twelve
     frames simply reflow two-up into six rows — the grid is row-major, so the
     four that matter stay in the middle of the wall here as well. */
  .mosaic {
    --aperture-h: clamp(320px, 54svh, 480px);
    --wall-gap: 10px;
  }
  .mosaic-plane { grid-template-columns: repeat(2, minmax(0, 1fr)); }

  /* The pair stays side by side: the two shapes read as a sequence at a glance,
     and the captions underneath carry what the shrunken tool labels no longer
     can. The half-step offset goes, because at this width it only adds height. */
  .devices { gap: 14px; max-width: 100%; }
  .devices figure:last-child { margin-top: 0; }
  .devices .plate { --plate-radius: 16px; }
  .devices .caption { font-size: 14.5px; }
  .devices .caption b { font-size: 12px; }

  .rooms > li { grid-template-columns: 1fr; }
  .rooms .app-icon { width: 46px; height: 46px; border-radius: 11px; }
  .rooms .tags--compact,
  .rooms-actions { grid-column: auto; }

  /* The arrival's solid action goes full measure so it is a comfortable tap
     target; the quiet one beside it wraps underneath. */
  .button-row { gap: 12px; }
  .button-row .btn-primary { flex: 1 1 260px; }
  .footer-bar { flex-direction: column; align-items: flex-start; }
  .form-row { grid-template-columns: 1fr; }
  .legal-modal { width: 100vw; height: 100svh; border: 0; border-radius: 0; }
}

