/* Glass House Design Lab, V4
   Black void and cool greys (the mist). Archivo display, Inter Tight UI, JetBrains Mono chrome.
   There is no vignette, no CRT warp and no darkening of screen edges or corners anywhere in
   this file. The only full-screen overlay is uniform film grain.

   Sections:
     1  fonts + tokens            6  #work shell (canvas, HUD, list / fallback row)
     2  base + utilities          7  (none: the page ends with #work)
     3  header                    8  contact panel: the poster + form
     4  the 3D mark               9  project sheet             10 index view
     5  hero                      11 grain, focus, reduced motion                              */

/* ======================================================================= 1. fonts + tokens */

@font-face {
  font-family: 'Archivo';
  src: url('../fonts/Archivo.woff2') format('woff2');
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Inter Tight';
  src: url('../fonts/InterTight.woff2') format('woff2');
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'JetBrains Mono';
  src: url('../fonts/JetBrainsMono.woff2') format('woff2');
  font-weight: 400 800;
  font-style: normal;
  font-display: swap;
}

:root {
  --black: #000;
  --void: #050505;
  --charcoal: #1a1a1a;
  --slate: #71737d;
  --mist: #d9dcdf;
  --ink: #111;
  --panel: var(--mist); /* the contact panel's ground: the project sheet's mist (its ink is --ink) */
  --rule: rgba(217, 220, 223, 0.28);
  --rule-ink: rgba(17, 17, 17, 0.55);

  --display: 'Archivo', 'Helvetica Neue', Arial, sans-serif;
  --ui: 'Inter Tight', 'Helvetica Neue', Arial, sans-serif;
  --mono: 'JetBrains Mono', ui-monospace, 'SFMono-Regular', Menlo, Consolas, monospace;

  /* (viewport-fit=cover: in landscape the gutter also clears the notch, on both sides) */
  --gutter: max(20px, 3vw, env(safe-area-inset-left), env(safe-area-inset-right));
  --safe-b: env(safe-area-inset-bottom, 0px);
  --hdr-h: 72px;
  --slot-h: 36px;
  --r-card: 18px;
  --r-sheet: 20px;

  --expo-out: cubic-bezier(0.16, 1, 0.3, 1);
  --expo-in-out: cubic-bezier(0.87, 0, 0.13, 1);
  --flight: cubic-bezier(0.16, 0.84, 0.24, 1);

  /* Header ink: light over the dark sections, dark over the contact panel's mist (body.ink-dark). */
  --hdr-ink: var(--mist);
  --hdr-contrast: #000;
}

@media (max-width: 699px) {
  :root {
    --hdr-h: 60px;
    --slot-h: 30px;
    --r-card: 16px;
    --r-sheet: 14px;
  }
}

body.ink-dark {
  --hdr-ink: var(--ink);
  --hdr-contrast: var(--panel);
}

/* ======================================================================= 2. base + utilities */

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

html {
  background: var(--black);
  color-scheme: dark;
  overscroll-behavior-x: none; /* no browser back-swipe from a sideways swipe (the case's spin, the no-WebGL row) */
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
  scrollbar-width: none;
}
html::-webkit-scrollbar {
  display: none;
}

body {
  margin: 0;
  background: var(--black);
  color: var(--mist);
  font-family: var(--ui);
  font-size: 16px;
  line-height: 1.4;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
}

h1, h2, h3, p, ul, ol, figure {
  margin: 0;
}
ul, ol {
  padding: 0;
  list-style: none;
}
a {
  color: inherit;
  text-decoration: none;
}
button {
  font: inherit;
  color: inherit;
  background: none;
  border: 0;
  padding: 0;
  margin: 0;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
img,
svg,
canvas {
  display: block;
}
/* The reset above would otherwise beat the UA's [hidden] rule (an unset <img> then paints
   Chrome's empty-image border). */
[hidden] {
  display: none !important;
}

::selection {
  background: var(--mist);
  color: var(--ink);
}

/* Lenis */
html.lenis,
html.lenis body {
  height: auto;
}
.lenis.lenis-smooth {
  scroll-behavior: auto !important;
}
.lenis.lenis-smooth [data-lenis-prevent] {
  overscroll-behavior: contain;
}
html.is-locked {
  overflow: hidden;
}
/* Touch (html.has-snap): the page scrolls natively under the finger, as any page does: Safari's
   toolbar shrinks as it goes, pull-to-refresh works at the top, and nothing scripted moves the
   page while a finger is down. Once the finger lifts, main.js (the pager) steers the page onto
   a section. Neither CSS scroll-snap (Safari re-snapped the page under the finger as its toolbar
   came and went, and kept a "current" section that went stale after any scripted scroll) nor a
   page moved by script while the finger is down (it trailed the finger and shook on a slow
   drag, and Safari neither shrinks its toolbar nor refreshes for a scripted scroll). The only
   custom touch-action left is the case's grip's (section 6). */

/* The page's edge colours. Safari 26 paints its status-bar and toolbar strips from a fixed bar
   at each edge of the screen (theme-color is ignored there): the .edge bars below tell it black,
   or the contact panel's mist under it (the -mist ones). Each paints nothing itself (its colour is clipped to an empty content box), and main.js swaps which one of each pair is displayed,
   since Safari re-reads the colour when a fixed bar comes or goes, not when one is recoloured.
   Beyond the page (the rubber-band overscroll) shows the root background, which follows the
   bottom edge; theme-color (Chrome) follows the top one. */
html.edge-bottom-mist,
html.edge-bottom-mist body {
  background-color: var(--panel);
}
.edge {
  position: fixed;
  z-index: 200; /* topmost at the edge, or Safari reads whatever is above it */
  left: 0;
  right: 0;
  height: 16px; /* (Safari skips bars 10px or thinner) */
  padding-top: 16px;
  background: var(--black) content-box;
  pointer-events: none;
}
.edge--top {
  top: 0;
}
.edge--bottom {
  bottom: 0;
}
.edge--mist {
  display: none;
  background-color: var(--panel);
}
html.edge-top-mist .edge--top,
html.edge-bottom-mist .edge--bottom {
  display: none;
}
html.edge-top-mist .edge--top.edge--mist,
html.edge-bottom-mist .edge--bottom.edge--mist {
  display: block;
}

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

.sprite {
  position: absolute;
  width: 0;
  height: 0;
  overflow: hidden;
}

/* Inline icons: 1.5px strokes with round caps at any size. */
.i {
  width: 1em;
  height: 1em;
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
  overflow: visible;
}
.i path {
  vector-effect: non-scaling-stroke;
}

.mono {
  font-family: var(--mono);
  text-transform: uppercase;
  letter-spacing: -0.02em;
}

/* Masks for line reveals. Lines rise from 100% inside an overflow:hidden window; the small
   padding / negative margin keeps ascenders and descenders from being shaved by the mask. */
.mask,
.line {
  display: inline-block;
  overflow: hidden;
  vertical-align: top;
  padding: 0.14em 0;
  margin: -0.14em 0;
}
.line {
  display: block;
}
.mask__inner,
.line__inner {
  display: inline-block;
  will-change: transform;
}
.line__inner {
  display: block;
}
.js [data-hero-lines]:not(.is-split) {
  opacity: 0;
}
.js .hero .mask__inner {
  transform: translateY(110%);
}
.js [data-hud-fade] {
  opacity: 0;
}
/* Safety net: if the modules never boot, show everything. */
.boot-failed .mask__inner,
.boot-failed .line__inner,
.boot-failed [data-hero-lines],
.boot-failed [data-hud-fade],
.boot-failed .logo3d__stage {
  opacity: 1 !important;
  transform: none !important;
}

.skip {
  position: fixed;
  z-index: 95;
  left: var(--gutter);
  top: calc(var(--hdr-h) + 12px); /* just under the header line, clear of the wordmark */
  padding: 0.7em 1.2em;
  border-radius: 999px;
  background: var(--mist);
  color: var(--ink);
  font: 400 12px/1 var(--mono);
  text-transform: uppercase;
  letter-spacing: -0.02em;
  transform: translateY(calc(-100% - var(--hdr-h) - 24px)); /* parked above the viewport */
  transition: transform 0.3s var(--expo-out);
}
.skip:focus {
  transform: none;
}

/* ======================================================================= 3. header */

.hdr {
  position: fixed;
  inset: 0 0 auto;
  z-index: 40;
  height: var(--hdr-h);
  color: var(--hdr-ink);
  pointer-events: none; /* only its controls take the pointer */
  transition: color 0.2s ease, opacity 0.45s ease;
}
/* One hairline in the current ink at ~28%. */
.hdr::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1px;
  background: currentColor;
  opacity: 0.28;
  pointer-events: none;
}
body.contact-open .hdr {
  z-index: 80; /* above the contact panel, with dark ink and "Close" */
}
/* While the contact wipe sweeps past the header (~0.1s), the ink switches outright at the flood's
   contrast crossover (contact.js) instead of through a .2s mid-grey. */
body.ink-snap .hdr {
  transition-duration: 0s, 0.45s;
}
body.ink-snap .hud-quote,
body.ink-snap .hud-quote__label,
body.ink-snap .flood {
  transition-duration: 0s;
}

.hdr__inner {
  height: 100%;
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  padding: 0 var(--gutter);
}

.hdr__brand {
  justify-self: start;
  display: block;
  width: clamp(112px, 11vw, 176px);
  pointer-events: auto;
  border-radius: 4px;
}
.hdr__wordmark {
  width: 100%;
  height: auto;
  aspect-ratio: 744 / 103;
  fill: currentColor;
}

/* The docked 3D mark lands here: slot height = the docked mark height. */
.hdr__slot {
  position: relative;
  display: block;
  width: calc(var(--slot-h) * 1.91);
  height: var(--slot-h);
  border-radius: 999px;
}
body:has(.logo3d:focus-visible) .hdr__slot {
  outline: 2px solid var(--mist);
  outline-offset: 5px;
}
body.ink-dark:has(.logo3d:focus-visible) .hdr__slot {
  outline-color: var(--ink);
}

.hud-quote {
  justify-self: end;
  position: relative;
  overflow: hidden;
  isolation: isolate;
  pointer-events: auto;
  padding: calc(0.72em + 1px) calc(1.3em + 1px);
  /* The outline is an inset shadow under the flood, not a border outside its clip: the fill covers
     it right out to the edge, with no ring or seam (as .pill; the extra 1px of padding keeps the
     size the border made). */
  box-shadow: inset 0 0 0 1px var(--hdr-ink);
  border-radius: 999px;
  color: var(--hdr-ink);
  font: 400 12px/1 var(--mono);
  text-transform: uppercase;
  letter-spacing: -0.02em;
  white-space: nowrap;
  transition: box-shadow 0.2s ease, color 0.2s ease;
}
/* Hover fill (header.js toggles .is-flooded): it fades in over the whole pill at once, as the
   contact form's Send does. (Its colour re-inks with the label, not ahead of it.) */
.flood {
  position: absolute;
  z-index: -1;
  inset: 0;
  border-radius: inherit;
  background: var(--hdr-ink);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.22s ease, background-color 0.2s ease;
}
.is-flooded > .flood {
  opacity: 1;
}
.hud-quote__label {
  display: grid;
  transition: color 0.25s ease;
}
.hud-quote.is-flooded .hud-quote__label {
  color: var(--hdr-contrast);
}
.hud-quote__face {
  grid-area: 1 / 1;
  display: block;
  text-align: center;
}
/* "Close" waits folded away (header.js flips it in): also before the modules boot, or without them. */
.hud-quote__face[data-face='b'] {
  opacity: 0;
  transform: scaleY(0);
}
@media (max-width: 699px) {
  .hud-quote {
    font-size: 11px;
    padding: calc(0.75em + 1px) calc(1.25em + 1px);
  }
}

/* ======================================================================= 4. the 3D mark */

/* The mark's layer runs the page's full height (body is its box), over the page and the header as
   the mark did when it lived in the header (z 40 there), under the sheet (60). The mark inside it
   is sticky:
   it sits in the hero and scrolls with it, natively, until it reaches the header slot, then stays
   there. logo3d.js sets its size, its place in the hero (margins) and the slot's height (top).
   Only the shrink (and any sideways step to the slot) is a transform, run on a scroll timeline
   where the browser has them (logo3d.js; the script sets it frame by frame elsewhere). Nothing
   about its position waits on script, so it never trails a finger's scroll (iOS scrolls on its
   own thread, a fixed mark placed by script lagged it), and it follows a pull-to-refresh. */
body {
  position: relative;
}
.mark-layer {
  position: absolute;
  z-index: 41;
  inset: 0;
  pointer-events: none;
  transition: opacity 0.45s ease; /* (as .hdr) */
}
/* With the header, over the contact panel (70): the mark leaves its place to stand on the panel
   (logo3d.js contact mode, below), over everything on it. */
body.contact-open .mark-layer {
  z-index: 81;
}
/* With the header, the mark steps aside under the sheet (section 9, body.sheet-open .hdr). */
body.sheet-open .mark-layer {
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.3s ease, visibility 0s linear 0.3s;
}
body.sheet-open.contact-open .mark-layer {
  opacity: 1;
  visibility: visible;
  transition: opacity 0.3s ease;
}
.logo3d {
  position: sticky;
  display: block;
  top: 0;
  width: 0;
  height: 0;
  margin: 0;
  padding: 0;
  transform-origin: 50% 0;
  pointer-events: none;
  cursor: default;
  outline: none;
  will-change: transform;
}
.logo3d.is-docked {
  pointer-events: auto;
  cursor: pointer;
}
/* On the contact panel (logo3d.js contact mode): the box leaves its sticky place for a fixed one
   (its layer has no transform: the screen's), placed by the flight's translate + scale from its
   top-left corner, with the dock's scroll-driven shrink off. Never a control there. */
.logo3d.is-contact {
  position: fixed;
  left: 0;
  top: 0 !important; /* (logo3d.js sets top and the margins inline, for the sticky place) */
  margin: 0 !important;
  transform-origin: 0 0;
  animation: none !important;
  pointer-events: none !important;
}
/* (One synchronous read of its sticky place, untransformed: logo3d.js homeRect.) */
.logo3d.is-measure {
  animation: none !important;
  transform: none !important;
}
/* Out of sight (.2s) while the panel is scrolled off its top (a keyboard up: it would hang over
   a field), or while the panel is up and the mark could not come to it. */
.logo3d.is-away .logo3d__canvas,
.logo3d.is-away .logo3d__img {
  opacity: 0;
}
.logo3d__stage {
  position: absolute;
  inset: 0;
  display: block;
}
.js .logo3d__stage {
  opacity: 0; /* revealed once the mark is built */
}
/* The canvas holder is the box plus, below it, the hero reflection region (logo3d.js sets
   its height); the reflection is composited in WebGL, so there is no second canvas. */
.logo3d__canvas {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  transition: opacity 0.2s ease;
}
.logo3d__canvas canvas {
  width: 100%;
  height: 100%;
}
.logo3d__img {
  position: absolute;
  left: 10%;
  top: 10%;
  width: 80%;
  height: 80%;
  transition: filter 0.2s ease, opacity 0.2s ease;
}
body.ink-dark .logo3d__img {
  filter: brightness(0);
}

/* ======================================================================= 5. hero */

.hero {
  position: relative;
  height: 100svh;
  min-height: 560px;
  background: var(--black);
}
/* The flat mark, set in the hero only if the 3D mark's module cannot load (main.js). Same
   place and size as the 3D mark: centred at 46% of the hero, ~34svh tall (phones ~26svh). */
.hero__mark-static {
  position: absolute;
  left: 50%;
  top: 46%;
  height: 34svh;
  width: auto;
  max-width: 60vw;
  object-fit: contain;
  transform: translate(-50%, -50%);
  pointer-events: none;
}
@media (max-width: 699px) {
  .hero__mark-static {
    height: 26svh;
    max-width: 78vw;
  }
}

.hero__foot {
  position: absolute;
  left: var(--gutter);
  right: var(--gutter);
  bottom: max(28px, 3vw);
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 12px 32px;
  font: 400 12px/1.5 var(--mono);
  text-transform: uppercase;
  letter-spacing: -0.02em;
  color: var(--mist);
}
.hero__tagline {
  max-width: 40ch;
}
.hero__cue {
  display: inline-flex;
  flex: none;
  border-radius: 4px;
}
.hero__cue .mask__inner {
  display: inline-flex;
  align-items: center;
  gap: 0.7em;
}
.hero__cue .i {
  transition: transform 0.6s var(--expo-out);
}
@media (hover: hover) {
  .hero__cue:hover .i {
    transform: translateY(3px);
  }
}
@media (max-width: 699px) {
  .hero__foot {
    flex-wrap: wrap;
    font-size: 11px;
    bottom: calc(20px + var(--safe-b)); /* clear of the home indicator, never flush on it */
  }
  .hero__tagline {
    flex: 1 1 100%;
    max-width: 46ch;
  }
  .hero__cue {
    margin-left: auto;
  }
}

/* ======================================================================= 6. #work */

.work {
  /* (the 62vw cap keeps a tall, narrow screen such as a portrait tablet a carousel: both
     neighbours show, rather than one card filling the width) */
  --card-h: min(46svh, 560px, calc(62vw / 1.6));
  --card-w: calc(var(--card-h) * 1.6); /* covers are 16:10 */
  --card-gap: 14px;
  position: relative;
  height: 100svh;
  /* The large viewport: once a phone's toolbar collapses, #work still fills the screen (at
     100svh the last section would fall short of it, and the hero's foot show above it at the
     end of the page). lvh never changes with the toolbar. */
  height: 100lvh;
  min-height: 600px;
  background: var(--black);
  overflow: hidden;
}
/* The page ends where the screen does at #work's frame, whatever a phone's toolbar is doing.
   #work stays 100lvh (nothing in it re-lays out as the toolbar comes and goes), but what of it
   runs on under a toolbar that is up (lvh - dvh) is left out of the page's height: main holds the
   margin that takes it off (flow-root: it does not collapse through) and clips what runs on past
   it. Else the page had a toolbar's worth of scroll past #work's frame, and a pull past the end,
   which brings Safari's toolbar back, sprang back to that end (the page pushed up under the
   header, the docked mark with it) before gliding down into the frame. Now the spring lands on
   the frame. (0 once the toolbar has gone, and on desktops.) */
@supports (height: 100dvh) {
  main {
    display: flow-root;
    overflow-y: clip;
  }
  .work {
    margin-bottom: calc(100dvh - 100lvh);
  }
}
@media (max-width: 699px) {
  .work {
    /* Phones show each site's real mobile layout: m1.webp cover-fit to 3:4. */
    --card-h: min(52svh, calc(78vw * 4 / 3));
    --card-w: calc(var(--card-h) * 0.75);
    --card-gap: 12px;
    min-height: 560px;
  }
}
/* With WebGL the section leaves its gestures to the browser (a swipe scrolls the page natively, a
   pinch zooms), all but those that begin on the case: its grip (folio.js, below) is where a finger
   spins it. */
.gl .work {
  touch-action: pan-y pinch-zoom;
  -webkit-user-select: none;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
}
/* The case's grip: a box over the case at rest (folio.js places it), where a drag spins the case
   (folio/input.js). The browser never pans the page for a touch that begins on it (a two-finger
   pinch still zooms, where the browser knows pinch-zoom alone); a tap on it opens the project. */
.work-grip {
  position: absolute;
  z-index: 2;
  touch-action: none;
  touch-action: pinch-zoom;
}
/* Pinch-zoomed in (main.js html.is-zoomed), one finger pans the zoomed view every way. */
html.is-zoomed .work,
html.is-zoomed .work-grip {
  touch-action: auto;
}
/* The case's cursors (folio.js): grab over it (a drag spins it, a click opens it), grabbing
   mid-spin. */
@media (hover: hover) and (pointer: fine) {
  .gl .work[data-mode='featured'].is-over-card {
    cursor: grab;
  }
  .gl .work.is-grabbing,
  .gl .work.is-grabbing * {
    cursor: grabbing !important;
  }
}

.folio-gl {
  position: absolute;
  z-index: 1;
  inset: 0;
  width: 100%;
  height: 100%;
}
.no-gl .folio-gl,
.no-js .folio-gl {
  display: none;
}

/* --- work list: visually hidden but focusable with WebGL, the visible row without --- */

.work-list {
  z-index: 2;
}
/* Until main.js knows which of the two it is: not laid out, so its lazy images stay unloaded
   (a hidden but laid-out list sits right under the hero and would fetch every cover at boot). */
.js:not(.work-ready) .work-list {
  display: none;
}
/* With WebGL the list is only an accessibility proxy for the ribbon: its images never load. It
   keeps to one pixel at the section's top, its buttons too: a focused button is then inside the
   framed section, so the browser has nothing to scroll into view (a full-size card hanging below
   #work scrolled the page, or #work's own clipped content), and from the hero it scrolls a pixel
   before workhud.js glides the section into frame. */
.gl .work-card__media {
  display: none;
}
.gl .work-list {
  position: absolute;
  left: 50%;
  top: 0;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.gl .work-list .work-card {
  width: 1px;
  height: 1px;
}

.no-gl .work-list,
.no-js .work-list {
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  transform: translateY(-50%);
  display: flex;
  gap: var(--card-gap);
  padding: 16px calc(50% - var(--card-w) / 2);
  overflow-x: auto;
  overflow-y: hidden;
  scroll-snap-type: x mandatory;
  overscroll-behavior-x: contain;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
}
.no-gl .work-list::-webkit-scrollbar {
  display: none;
}
.work-list.is-dragging {
  scroll-snap-type: none;
  cursor: grabbing;
}
.work-list.is-dragging .work-card {
  cursor: grabbing;
}
.work-list__item {
  flex: none;
  scroll-snap-align: center;
}

.work-card {
  position: relative;
  display: block;
  width: var(--card-w);
  height: var(--card-h);
  border-radius: var(--r-card);
  overflow: hidden;
  background: #111;
  color: #fff;
  text-align: left;
  isolation: isolate;
  transform: translateZ(0);
}
.work-card__media,
.work-card__media picture,
.work-card__media img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}
.work-card__media img {
  object-fit: cover;
  object-position: 50% 0;
  transition: transform 1.2s var(--expo-out);
  -webkit-user-drag: none;
  user-select: none;
}
.work-card {
  -webkit-user-select: none;
  user-select: none;
}
/* Per-card caption scrim: darkens only the bottom of the card, never the screen. */
.work-card::after {
  content: '';
  position: absolute;
  z-index: 1;
  left: 0;
  right: 0;
  bottom: 0;
  height: 46%;
  background: linear-gradient(
    to top,
    rgba(0, 0, 0, 0.6) 0%,
    rgba(0, 0, 0, 0.36) 12%,
    rgba(0, 0, 0, 0.16) 24%,
    rgba(0, 0, 0, 0.04) 36%,
    rgba(0, 0, 0, 0) 100%
  );
  pointer-events: none;
}
/* Glass edge: a bright rim and a faint inner bevel line (as the GL panes). */
.work-card::before {
  content: '';
  position: absolute;
  z-index: 3;
  inset: 0;
  border-radius: inherit;
  box-shadow: inset 0 0 0 1.5px rgba(255, 255, 255, 0.26), inset 0 0 0 3.5px rgba(255, 255, 255, 0.07);
  pointer-events: none;
}
.work-card__cap {
  position: absolute;
  z-index: 2;
  left: 18px;
  right: 64px;
  bottom: 16px;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.work-card__name {
  font: 600 20px/1 var(--display);
  letter-spacing: -0.02em;
  color: #fff;
}
.work-card__line {
  position: absolute !important;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.work-card__go {
  position: absolute;
  z-index: 2;
  right: 16px;
  bottom: 14px;
  width: 30px;
  height: 30px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: #fff;
  color: #000;
  font-size: 14px;
  transition: transform 0.6s var(--expo-out);
}
@media (hover: hover) and (pointer: fine) {
  .no-gl .work-list {
    cursor: grab;
  }
  .no-gl .work-list.is-still {
    cursor: auto; /* (one card: nothing to drag; workhud.js syncSingle) */
  }
  .work-card:hover .work-card__media img {
    transform: scale(1.035);
  }
  .work-card:hover .work-card__go {
    transform: rotate(-45deg);
  }
}
@media (max-width: 699px) {
  /* Phones show m1 framed as the GL card frames it (data.js mobileCrop: workhud.js sets the
     vars), so no site headline sits under the caption; the caption band is frosted glass. */
  .work-card__media img {
    object-position: calc(var(--crop-x, 0.5) * 100%) calc(var(--crop-y, 0) * 100%);
    scale: var(--crop-zoom, 1);
    transform-origin: calc(var(--crop-x, 0.5) * 100%) calc(var(--crop-y, 0) * 100%);
  }
  .work-card::after {
    height: 28%;
    background: rgba(0, 0, 0, 0.55);
    -webkit-backdrop-filter: blur(14px);
    backdrop-filter: blur(14px);
  }
  .work-card__name {
    font-size: 17px;
  }
  .work-card__cap {
    left: 14px;
    bottom: 14px;
  }
  .work-card__go {
    right: 12px;
    bottom: 12px;
  }
}

/* --- HUD --- */

.work-hud {
  position: absolute;
  z-index: 4;
  inset: 0;
  pointer-events: none;
  color: var(--mist);
  font: 400 12px/1.2 var(--mono);
  text-transform: uppercase;
  letter-spacing: -0.02em;
  /* Steps aside while a pane flies to / sits as the project sheet (--hud-a, tweened by
     folio.js). */
  opacity: var(--hud-a, 1);
}
.work.is-sheet .work-hud,
.work.is-sheet .work-hud button {
  pointer-events: none;
}
.work-hud button {
  pointer-events: auto;
  text-transform: inherit;
  letter-spacing: inherit;
}
/* Well below the mask's own padding: nothing peeks out before the reveal. */
.js .work-hud .mask__inner {
  transform: translateY(130%);
}
/* Secondary HUD text (the slash, the view not shown) is dimmed, but at .62 of the mist it still
   reads 5.6:1 on black (WCAG AA for small text). */
.work-hud__slash {
  opacity: 0.62;
}

.work-hud__eyebrow {
  position: absolute;
  top: calc(var(--hdr-h) + 22px);
  left: var(--gutter);
  font: inherit;
}

.work-hud__bar {
  position: absolute;
  left: var(--gutter);
  right: var(--gutter);
  bottom: max(26px, 3vh);
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 16px;
}
/* #work is 100lvh: while a phone or tablet shows its bottom toolbar, the section runs on under
   it, so the row rides above the toolbar (lvh - dvh is 0 once it has gone, and on desktops).
   The toolbar now shrinks and comes back as the page scrolls, and Safari updates dvh in one step
   when it has: the row eases to its new place rather than jumping. */
@supports (height: 100dvh) {
  .work-hud__bar {
    bottom: calc(100lvh - 100dvh + max(26px, 3vh, 12px + var(--safe-b)));
    transition: bottom 0.4s var(--expo-out);
  }
}

.work-hud__modes {
  justify-self: start;
  display: flex;
  align-items: center;
  gap: 0.5em;
}
.work-hud__modes[hidden] {
  display: none;
}
.work-hud__modes button {
  position: relative;
  opacity: 0.62;
  border-radius: 3px;
  transition: opacity 0.5s ease;
}
.work-hud__modes button[aria-pressed='true'] {
  opacity: 1;
}
@media (hover: hover) {
  .work-hud__modes button:hover {
    opacity: 1;
  }
}
/* Touch: each word takes a 44px-tall target without moving the layout. */
.is-touch .work-hud__modes button::after {
  content: '';
  position: absolute;
  inset: -15px -8px;
}

.work-hud__centre {
  justify-self: center;
  grid-column: 2;
  position: relative;
  display: grid;
  place-items: center;
  min-width: 0;
  max-width: 100%;
}
.work-hud__hint,
.work-hud__focus {
  grid-area: 1 / 1;
  max-width: 100%;
  white-space: nowrap;
}
.work-hud__hint .mask__inner {
  display: inline-flex;
  align-items: center;
  gap: 0.8em;
}
.work-hud__hint .i {
  opacity: 0.6;
}
.work-hud__hint-touch {
  display: none;
}
.is-touch .work-hud__hint-mouse {
  display: none;
}
.is-touch .work-hud__hint-touch {
  display: inline;
}
@media (max-width: 699px) {
  .work-hud__hint-mouse {
    display: none;
  }
  .work-hud__hint-touch {
    display: inline;
  }
}
/* "deer + almond — Enter to open" for a keyboard-focused card (the name only on phones). */
.work-hud__focus {
  overflow: hidden;
  text-overflow: ellipsis;
  opacity: 0;
  transform: translateY(6px);
  transition: opacity 0.35s ease, transform 0.6s var(--expo-out);
}
.work-hud.is-focus .work-hud__focus {
  opacity: 1;
  transform: none;
}
.work-hud.is-focus .work-hud__hint {
  opacity: 0 !important;
}

.work-hud__nav {
  justify-self: end;
  grid-column: 3;
  display: flex;
  align-items: center;
  gap: 8px;
  transition: opacity 0.5s ease;
}
.work-hud__btn {
  width: 36px;
  height: 36px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  border: 1px solid var(--rule);
  font-size: 14px;
  transition: background-color 0.35s ease, color 0.35s ease, border-color 0.35s ease;
}
.is-touch .work-hud__btn {
  width: 44px;
  height: 44px;
}
@media (hover: hover) {
  .work-hud__btn:hover {
    background: var(--mist);
    border-color: var(--mist);
    color: #000;
  }
}
.work-hud__btn:active {
  transform: scale(0.94);
}
/* The Index view has no "current card": the arrows step away (and are inert). */
.work[data-mode='index'] .work-hud__nav,
.work[data-mode='index'] .work-hud__centre {
  opacity: 0;
  pointer-events: none;
}
.work[data-mode='index'] .work-hud__nav button {
  pointer-events: none;
}

/* Phones: two groups in the row, Featured / Index on the left and the arrows on the right; the
   hint (and a keyboard focus label) floats centred just above the row. */
@media (max-width: 699px) {
  .work-hud {
    font-size: 11px;
  }
  .work-hud__eyebrow {
    top: calc(var(--hdr-h) + 18px);
  }
  .work-hud__bar {
    grid-template-columns: auto 1fr auto;
    bottom: calc(16px + var(--safe-b));
  }
  @supports (height: 100dvh) {
    .work-hud__bar {
      bottom: calc(100lvh - 100dvh + 16px + var(--safe-b));
    }
  }
  .work-hud__centre {
    position: absolute;
    grid-column: 1 / -1; /* (an absolute grid child: the whole row is its box) */
    left: 0;
    right: 0;
    bottom: calc(100% + 16px);
  }
  .work-hud__focus-hint {
    display: none;
  }
  .work-hud__nav {
    gap: 10px;
  }
}

/* ======================================================================= 8. contact panel
   #contact[role=dialog]: fixed, z 70, under the header (80, dark ink and "Close") and the mark's
   layer (81). A poster, one screen at every size, its hero object the spinning 3D mark, which
   leaves the header to stand on it (logo3d.js flies it onto .contact__mark-stage): on tablets and
   desktops alone, centred in its row; on phones at the top of a row of its own. Around it: on
   tablets and desktops only, the wordmark with its fan-down (fan.js: in a box of its own above
   the barcode); everywhere, the enquiry form as a card, the
   email row and the barcode stamp with the © line; the header row carries the sheet's meta (the
   band). The ground is the project sheet's mist (--panel). Cards sit on solid mist with 1px ink
   outlines, so the fan shows only between them; any label over the fan sits on a mist chip.
   Secondary text is .72 of the ink (5.8:1 on the mist, AA).
   One screen: .contact__grid fills .contact__scroll exactly, and one row gives way to whatever
   grows (a field's message, a long status): the mark's on phones and tablets, the details pad's on
   desktops and phones on their side. (On the shortest phones and in short desktop windows, once
   the form has something to say, the pad gives up all but one line too.) The scroller stays a
   scroller only as a safety net (and for a touch screen's keyboard, below).
   Layouts: phones (portrait: the base rules), tablets (portrait, 700-899), desktops (900+) and
   phones on their side (landscape, 500px tall or less). The phone and tablet rules are scoped so
   that none of them reaches a phone on its side. */

.contact {
  position: fixed;
  z-index: 70;
  inset: 0;
  color: var(--ink);
}
.contact[hidden] {
  display: none;
}
/* (Laid out once, unseen, for fan.js to measure the wordmark: shown and hidden in one task.) */
.contact.is-measuring {
  visibility: hidden;
}
.contact ::selection {
  background: var(--ink);
  color: var(--panel);
}
.contact__wipe {
  position: absolute;
  inset: 0;
  overflow: hidden;
}
/* The liquid wipe. Layer i has depth --d = 3 - i: it reaches 1 + d*.13 of the box, is blurred
   d*4px and is 1 - d*.18 opaque, so the deep layers lead as a soft smear and the crisp front
   layer lands last. The layers TRANSLATE up rather than scaleY, which keeps the blur (and the
   soft leading edge) the same width at every moment instead of squashing it into hard bands.
   JS sets --d and the y.
   The blur is baked into the gradient: a layer is a flat mist field whose only edge on screen
   is its leading one (its sides and bottom hang 64px outside the box), and blurring a d*48px
   ramp by d*4px gives exactly this profile (stops every 6d px; it starts 3 blur radii, 12d px,
   above the ramp, so the box reaches that much higher). A live blur filter on four full-screen
   layers cost a GPU shader compile per radius on the first wipe, and a re-filter per frame. */
.contact__wipe > i {
  --d: 0;
  position: absolute;
  left: -64px;
  right: -64px;
  bottom: -64px; /* hang below the box so the soft bottom edge is clipped */
  height: calc(100% * (1 + var(--d) * 0.13) + 64px + var(--d) * 12px);
  background: linear-gradient(
    to bottom,
    rgba(217, 220, 223, 0) 0,
    rgba(217, 220, 223, 0.002) calc(var(--d) * 6px),
    rgba(217, 220, 223, 0.033) calc(var(--d) * 12px),
    rgba(217, 220, 223, 0.127) calc(var(--d) * 18px),
    rgba(217, 220, 223, 0.25) calc(var(--d) * 24px),
    rgba(217, 220, 223, 0.75) calc(var(--d) * 48px),
    rgba(217, 220, 223, 0.873) calc(var(--d) * 54px),
    rgba(217, 220, 223, 0.967) calc(var(--d) * 60px),
    rgba(217, 220, 223, 0.998) calc(var(--d) * 66px),
    var(--panel) calc(var(--d) * 72px)
  );
  opacity: calc(1 - var(--d) * 0.18);
  transform: translateY(100%);
  will-change: transform;
}
/* The mist prints the poster: while the wipe runs (contact.js: body.contact-wiping) the band, the
   backdrop and the grid show only where the crisp front layer has passed (--flood-top in panel px,
   --flood-in in the scroller's content), never dark over the black page; on the way out they are
   wiped away from the top down as that layer drops first. */
body.contact-wiping .contact__band {
  clip-path: inset(var(--flood-top, 100vh) 0 0 0);
}
body.contact-wiping .contact__backdrop,
body.contact-wiping .contact__grid {
  clip-path: inset(var(--flood-in, 100vh) 0 0 0);
}

/* --- the header band: the sheet's meta in the header row, between the header's wordmark and
   its (empty) dock (aria-hidden). From 760px; none below or on a phone on its side. Under the
   header (inside the panel's z 70), clear of its controls. --- */
.contact__band {
  position: absolute;
  z-index: 1;
  top: 0;
  left: 0;
  right: 0;
  height: var(--hdr-h);
  display: none;
  pointer-events: none;
  font: 400 10px / 1.35 var(--mono);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.contact__band-l {
  position: absolute;
  top: 0;
  bottom: 0;
  left: calc(var(--gutter) + clamp(112px, 11vw, 176px) + 28px); /* (past .hdr__brand) */
  right: calc(50% + var(--slot-h) * 1.91 / 2 + 28px);
  display: flex;
  align-items: center;
  gap: 24px;
  overflow: hidden;
}
.contact__cell {
  display: grid;
  flex: none;
  padding-left: 12px;
  border-left: 1px solid var(--rule-ink);
  white-space: nowrap;
  overflow: hidden;
}
@media (min-width: 760px) and (min-height: 501px) {
  .contact__band {
    display: block;
  }
}

/* --- the scroller: the panel under the header (which stays on top with "Close"); the wipe
   layers still cover the whole screen. Its size is the container every measure below is cut
   from (cq units). --- */
.contact__scroll {
  position: absolute;
  inset: var(--hdr-h) 0 0;
  overflow-x: hidden;
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  container: panel / size;
  --pad-t: clamp(10px, 2.2cqh, 24px);
  --pad-b: max(clamp(10px, 2.2cqh, 24px), var(--safe-b));
  --gap-y: clamp(8px, 1.9cqh, 18px);
  --gap-x: clamp(14px, 2vw, 32px);
  --micro: 10px;
  --label: 11px;
  --ann-x: 12px; /* air kept clear beside the stage */
  --ann-b: 24px; /* and under it */
  --area-h: clamp(56px, 10.5cqh, 128px); /* the details pad (in whole lines: below) */
  --area-min: 56px; /* (two ruled lines) */
}
.contact__scroll::-webkit-scrollbar {
  display: none;
}
/* Touch: an on-screen keyboard covers the lower half; room below the poster lets the panel
   scroll the field being typed in, and Send, above it (the mark steps aside meanwhile:
   contact.js). A spacer after the grid rather than padding on it: the grid keeps its geometry. */
.is-touch .contact__scroll:has(.quoteForm__input:focus)::after {
  content: '';
  display: block;
  height: 45svh;
}

/* --- the backdrop: the rails (decoration, clipped). Its own stacking context, so all of it paints
   under the grid. --- */
.contact__backdrop {
  position: absolute;
  z-index: 0;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
}
/* --- the wordmark and its fan-down: a block of the grid, under the cards and the mark, on tablets
   and desktops only (phones go without it: the header's own wordmark is there). It fills the box
   above the barcode, the wordmark the box's width, and the fan is fitted to the box (fan.js). --- */
.contact__brand {
  display: none;
  position: relative;
  z-index: 0;
  min-width: 0;
  min-height: 0;
  pointer-events: none;
}
.contact__brand-wm {
  position: relative;
  width: 100%;
  aspect-ratio: 744 / 103;
}
.contact__wm {
  position: absolute;
  z-index: 2;
  inset: 0;
  width: 100%;
  height: 100%;
  fill: currentColor;
}
/* The fan-down: copies behind the wordmark, each a step further down, longer, more blurred and
   fainter than the last (--trail-fade, set per copy by fan.js), opened by the panel's opening
   (--trail-progress, 0 shut .. 1 open, contact.js). Copy i falls i steps of the wordmark's height
   and stretches to 1 + i * --trail-stretch of it from its top, so the letters smear downward; fan.js
   sets the stretch that takes the farthest copy to the foot of its box. */
.contact__fan {
  --trail-progress: 0;
  --trail-step: 0.2;
  --trail-stretch: 0.6; /* (fan.js) */
  --trail-blur: 12px; /* the farthest copy's blur (fan.js bakes the same) */
  position: absolute;
  z-index: 1;
  inset: 0;
}
.contact__trail {
  --u: calc(var(--trail-index) / 6); /* 0 at the front .. 1 for the farthest */
  --s: calc(var(--trail-progress) * var(--trail-index));
  position: absolute;
  inset: 0;
  transform-origin: 50% 0;
  transform: translateY(calc(var(--s) * var(--trail-step) * 100%)) scaleY(calc(1 + var(--s) * var(--trail-stretch)));
  opacity: calc(var(--trail-fade, 0.3) * var(--trail-progress));
  /* The live blur stands in until fan.js has baked the levels below (it costs the GPU a shader
     program per radius met, and a re-filter of every copy on every frame of the fan-out). */
  filter: blur(calc(var(--trail-progress) * var(--u) * var(--trail-blur)));
  will-change: transform, opacity, filter;
}
.contact__trail-sharp {
  display: block;
  width: 100%;
  height: 100%;
  fill: currentColor;
}
/* The baked blur: level k of LEVELS (3) is the copy blurred by u * --trail-blur * k/3, so the live
   blur(p * u * --trail-blur) sits at x = 3p along sharp (0), 1, 2, 3. The two levels either side
   of x crossfade, added together (plus-lighter, inside the copy's own isolated group) so the mix is
   linear. At full progress only level 3 shows and plain blending does. --pad is each canvas's blur
   room. */
.contact__trail-level {
  display: none;
  position: absolute;
  left: calc(var(--pad, 0px) * -1);
  top: calc(var(--pad, 0px) * -1);
  width: calc(100% + var(--pad, 0px) * 2);
  height: calc(100% + var(--pad, 0px) * 2);
  mix-blend-mode: plus-lighter;
}
.contact__fan.is-baked .contact__trail {
  --x: calc(var(--trail-progress) * 3);
  isolation: isolate;
  filter: none;
  will-change: transform, opacity;
}
.contact__fan.is-baked .contact__trail-sharp {
  opacity: max(0, 1 - var(--x));
}
.contact__fan.is-baked .contact__trail-level {
  display: block;
  opacity: max(0, min(var(--x) - var(--k) + 1, var(--k) + 1 - var(--x)));
}
.contact__fan.is-baked.is-full .contact__trail-level {
  mix-blend-mode: normal;
}
/* Rails: a rotated running head in each side gutter, at the foot of its run from the top to the
   bottom. */
.contact__rail {
  position: absolute;
  z-index: 3;
  top: calc(var(--pad-t) + 18px);
  bottom: calc(var(--pad-b) + 18px);
  width: var(--gutter);
  display: flex;
  align-items: center;
  writing-mode: vertical-rl;
  font: 400 10px / 1 var(--mono);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  white-space: nowrap;
  overflow: hidden;
  container: rail / size; /* (its size is all insets and the gutter: never its heads') */
}
.contact__rail--l {
  left: 0;
  transform: rotate(180deg);
}
.contact__rail--r {
  right: 0;
  justify-content: flex-end;
}
.contact__rail i {
  font-style: normal;
  opacity: 0.6;
}
/* A rail too short for its head (in a short window: the left's runs 224px at 10px, the right's
   116px) leaves it out rather than cut it off. */
@container rail (max-height: 225px) {
  .contact__rail--l > span {
    display: none;
  }
}
@container rail (max-height: 117px) {
  .contact__rail--r > span {
    display: none;
  }
}
/* --- the grid: exactly one screen. Phones (portrait): one column, the mark takes what is left
   (never so little that its stage drops under 56px tall). --- */
.contact__grid {
  position: relative;
  z-index: 1;
  min-height: 100%;
  /* (Blocks rising into place from below the panel's edge never make it scrollable meanwhile.) */
  overflow: clip;
  display: grid;
  gap: var(--gap-y) var(--gap-x);
  padding: var(--pad-t) var(--gutter) var(--pad-b);
  align-items: stretch;
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: minmax(max(0px, 56px + var(--ann-b)), 1fr) auto auto;
  grid-template-areas:
    'mark'
    'form'
    'foot';
}

/* --- shared micro type: mono, uppercase, open --- */
.contact__micro,
.contact__formhead,
.contact__copy,
.contact__mail-k {
  font: 400 var(--micro) / 1.2 var(--mono);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

/* --- the 3D mark's stand. The stage is the rect of the visible mark (aspect 107:56): logo3d.js
   lands the live mark on it, its box overhanging by 12.5% a side (transparent, never a target).
   It is as large as its row lets it be, kept clear of what is below (its box's overhang included)
   and, on desktops, of the cards either side. --- */
.contact__mark {
  grid-area: mark;
  position: relative;
  z-index: 2;
  min-height: 0;
  container: mark / size;
  pointer-events: none;
}
.contact__mark-stage {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  margin: 0 auto;
  width: max(
    107px,
    min(100cqw - 2 * var(--ann-x), (100cqh - var(--ann-b)) * 107 / 56, 100cqh / 1.125 * 107 / 56, var(--mark-max, 640px), var(--mark-cap, 9999px))
  );
  aspect-ratio: 107 / 56;
}
/* The stand-in: the flat mark in the ink. Only while the live mark is not there (logo3d.js:
   html.has-mark3d; contact.js: .is-standin). */
.contact__mark-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  filter: brightness(0);
}
html.has-mark3d .contact:not(.is-standin) .contact__mark-img {
  visibility: hidden;
}

/* --- title + close. The dialog's title ("Contact us") is for screen readers only: the poster is
   the mark, not a headline. (contact.js still splits and focuses it.) --- */
.contact__intro {
  position: absolute;
  top: 0;
  left: 0;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.contact__title {
  margin: 0;
  font-size: 16px;
  outline: none;
}
/* A close control inside the dialog (aria-modal hides the header's Close pill from screen
   readers), on the form card's top-right corner: an ink octagon (::before: a clip-path takes no
   border), outlined on hover as Send is (::after: the mist inside, faded in). Touch tablets show
   it; with a mouse and keyboard, and on phones, it appears only when focused (the header pill is
   right above it). */
.contact__close {
  grid-area: form;
  justify-self: end;
  align-self: start;
  position: relative;
  z-index: 4;
  isolation: isolate;
  width: 44px;
  height: 44px;
  display: grid;
  place-items: center;
  margin: -16px -16px 0 0; /* (on a phone's 20px gutter it would reach the screen's edge centred) */
  border: 0;
  background: none;
  color: var(--panel);
  font-size: 16px;
  transition: color 0.22s ease;
}
.contact__close::before,
.contact__close::after {
  content: '';
  position: absolute;
  z-index: -1;
  clip-path: polygon(30% 0, 70% 0, 100% 30%, 100% 70%, 70% 100%, 30% 100%, 0 70%, 0 30%);
}
.contact__close::before {
  inset: 0;
  background: var(--ink);
}
.contact__close::after {
  inset: 1.5px;
  background: var(--panel);
  opacity: 0;
  transition: opacity 0.22s ease;
}
.contact__close .i {
  transition: transform 0.7s var(--expo-out);
}
@media (hover: hover) {
  .contact__close:hover {
    color: var(--ink);
  }
  .contact__close:hover::after {
    opacity: 1;
  }
  .contact__close:hover .i {
    transform: rotate(90deg);
  }
}
html:not(.is-touch) .contact__close:not(:focus-visible) {
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* --- the form: a dense card pasted over the fan. Its markup is the form's own (contact.js reads
   it by its ids and classes); the Req / Opt tags are CSS. --- */
.quoteForm {
  grid-area: form;
  position: relative;
  z-index: 2;
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 0;
  align-self: end;
  container: qf / inline-size;
}
/* Its frame, a HUD panel: the mist card under a 1.5px ink outline with two corners cut at 45
   degrees (top left, bottom right), a second hairline inside each cut. A 9-slice of one 96px
   drawing (border-image): the corners keep their true size at any card size, only the straight
   runs stretch. */
.quoteForm::before {
  content: '';
  position: absolute;
  z-index: -1;
  inset: 0;
  border: 32px solid transparent;
  border-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='96' height='96' viewBox='0 0 96 96'%3E%3Cpath d='M15 .75H95.25V81L81 95.25H.75V15Z' fill='%23d9dcdf' stroke='%23111111' stroke-width='1.5'/%3E%3Cpath d='M5.5 18.5 18.5 5.5M90.5 77.5 77.5 90.5' fill='none' stroke='%23111111'/%3E%3C/svg%3E")
    32 fill / 32px stretch;
  pointer-events: none;
}
/* Its head: an ink tab with a slanted end ("Project enquiry"). Inside the frame, clear of its cut
   corner (its right margin keeps the row clear of the close disc on the card's corner, wherever
   and whenever that shows). */
.contact__formhead {
  display: flex;
  align-items: center;
  margin: 8px 34px 0 19px;
  color: var(--ink);
  white-space: nowrap;
}
.contact__formhead > span {
  padding: 0.45em 1.7em 0.45em 0.7em;
  background: var(--ink);
  color: var(--panel);
  clip-path: polygon(0 0, 100% 0, calc(100% - 0.9em) 100%, 0 100%);
}
.quoteForm__form {
  position: relative;
  flex: 1;
  min-height: 0;
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  grid-template-rows: auto auto minmax(0, 1fr) auto;
  grid-template-areas:
    'name phone'
    'email email'
    'details details'
    'send status';
  gap: var(--f-gap, 10px) 14px;
  padding: var(--f-pad, 10px) var(--f-pad-x, 12px) var(--f-pad, 12px);
}
/* A narrow card: one field to a row. (Under 320px a half-width field runs its label into its tag,
   and Send overhangs its half.) */
@container qf (max-width: 319px) {
  .quoteForm__form {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: auto auto auto minmax(0, 1fr) auto auto;
    grid-template-areas: 'name' 'phone' 'email' 'details' 'send' 'status';
  }
  /* The status: a row of its own under Send, the card's width, nothing to keep clear of. (Scoped
     to the form: the status's own rules come later.) */
  .quoteForm__form .quoteForm__status {
    text-align: left;
  }
  .quoteForm__form .quoteForm__status:not(:empty)::before {
    content: none;
  }
  /* (The name runs the card's width now: its message fits beside it.) */
  .quoteForm__field[data-field='name'] .quoteForm__error {
    grid-area: tag;
    align-self: baseline;
    margin: 0;
    text-align: right;
  }
}
/* A field: its label row (LABEL ........ REQ) over its rule, the input pinned to the field's
   foot. The form's grid stretches two fields side by side to their row's height, so their rules
   stay on one line whatever their messages run to. A message takes the tag's place beside the
   label in a field the card's width; in a half-width one it takes a line of its own under the
   label (beside it, it would wrap down a column a few words wide). */
.quoteForm__field {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  grid-template-rows: auto auto minmax(0, 1fr) auto;
  grid-template-areas:
    'label tag'
    'msg   msg'
    '.     .'
    'input input';
  column-gap: 0.8em;
  row-gap: 0;
  min-width: 0;
  margin: 0;
}
.quoteForm__field[data-field='name'] {
  grid-area: name;
}
.quoteForm__field[data-field='phone'] {
  grid-area: phone;
}
.quoteForm__field[data-field='email'] {
  grid-area: email;
}
.quoteForm__field[data-field='details'] {
  grid-area: details;
}
.quoteForm__label {
  grid-area: label;
  align-self: baseline;
  font: 400 var(--label) / 1.3 var(--mono);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  white-space: nowrap;
}
/* "optional" is for screen readers (the Opt tag says it on screen, and is silent). */
.quoteForm__optional {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.quoteForm__field::after {
  content: 'Req';
  content: 'Req' / ''; /* (the field says it is required itself) */
  grid-area: tag;
  justify-self: end;
  align-self: baseline;
  font: 400 var(--micro) / 1 var(--mono);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  opacity: 0.72;
}
.quoteForm__field[data-field='details']::after {
  content: 'Opt';
  content: 'Opt' / '';
}
.quoteForm__field.is-invalid::after {
  display: none;
}
/* (The messages are the form's most important lines: never under 12px.) */
.quoteForm__error {
  grid-area: msg;
  margin: 3px 0 0; /* (clear of a focused number's second box) */
  font: 500 max(12px, var(--label)) / 1.3 var(--mono);
  text-align: left;
}
.quoteForm__field[data-field='email'] .quoteForm__error {
  grid-area: tag;
  align-self: baseline;
  margin: 0;
  text-align: right;
}
.quoteForm__error[hidden] {
  display: none;
}
.quoteForm__input {
  grid-area: input;
  width: 100%;
  margin: 2px 0 0;
  padding: var(--in-pad, 0.45em) 0;
  border: 0;
  border-bottom: 1px solid var(--ink);
  border-radius: 0;
  /* (a tick up from each end of its rule, as a HUD's field has) */
  background:
    linear-gradient(var(--ink), var(--ink)) left bottom / 1px 6px no-repeat,
    linear-gradient(var(--ink), var(--ink)) right bottom / 1px 6px no-repeat;
  color: var(--ink);
  font: 400 16px/1.4 var(--mono); /* >= 16px: no iOS zoom */
  letter-spacing: -0.01em;
  outline: none;
  transition: box-shadow 0.25s ease;
  -webkit-appearance: none;
  appearance: none;
}
/* A message: the field's rule goes to 2px. Focus: to 3px (a clear indicator without boxing an
   underline-only field in), and over a message's. */
.quoteForm__field.is-invalid .quoteForm__input {
  box-shadow: 0 1px 0 0 var(--ink);
}
.quoteForm .quoteForm__input:focus,
.quoteForm .quoteForm__input:focus-visible {
  outline: none;
  box-shadow: 0 2px 0 0 var(--ink);
}
.quoteForm__input:-webkit-autofill {
  -webkit-text-fill-color: var(--ink);
  box-shadow: 0 0 0 100px var(--panel) inset;
  transition: background-color 9999s;
}
/* Project details (no message): the label row, then the pad, which can fill its row. */
.quoteForm__field[data-field='details'] {
  grid-template-rows: auto minmax(0, 1fr);
  grid-template-areas:
    'label tag'
    'input input';
}
/* The pad: a set height (or what the card has left), ruled like a pad, what is typed scrolling
   inside it on the rules (one per line of text, riding with the text; a whole-px tile, as a
   fractional repeating gradient can render blank). No grabber, and no growing with the text:
   the poster is one screen. Never under two whole lines (--area-min; one ruled line reads as a
   one-line input), and where it has a set height, a whole number of them, so no line is cut
   through its middle. */
.quoteForm__input--area {
  --rule-lh: 24px;
  align-self: stretch;
  resize: none;
  field-sizing: fixed;
  height: var(--area-h);
  min-height: var(--area-min, 0px);
  max-height: none;
  overflow-y: auto;
  /* (The air over the first line is a clear border, not padding: padding scrolls with the text,
     and a long brief showed the descenders of the line above in it.) */
  padding: 0 0 4px;
  border-top: 4px solid transparent;
  line-height: var(--rule-lh);
  background: linear-gradient(to bottom, transparent calc(var(--rule-lh) - 1px), rgba(17, 17, 17, 0.22) 0) 0 0 / 100% var(--rule-lh) local padding-box;
}
@supports (height: calc(round(down, 50px, 24px) + 8px)) {
  .quoteForm__input--area {
    height: calc(round(down, var(--area-h) - 8px, var(--rule-lh)) + 8px);
  }
}
.quoteForm .buttons {
  grid-area: send;
  position: relative;
  z-index: 1; /* (over the status's box, which runs under it: below) */
  margin: 0;
  align-self: start;
  container: send / inline-size;
}
.button__pill {
  display: inline-flex;
  align-items: center;
  gap: 0.8em;
  min-height: 44px;
  padding: 0.8em 1.5em;
  border: 1px solid var(--ink);
  border-radius: 999px;
  background: var(--ink);
  color: var(--panel);
  font: 400 13px/1 var(--mono);
  text-transform: uppercase;
  letter-spacing: -0.02em;
  white-space: nowrap;
  transition: background-color 0.22s ease, color 0.22s ease;
}
@media (hover: hover) {
  .button__pill:hover {
    background: transparent;
    color: var(--ink);
  }
}
/* On the contact panel's HUD card Send is a square-cornered ink bar, which on hover goes to its
   outline, as the pill goes from filled to outlined. The outline is a real border (snapped to the
   screen's pixels, so it is as thick on every side; 1px, which every engine draws as asked), its
   width taken out of the side air so Send keeps its size. */
.quoteForm .button__pill {
  padding-inline: calc(1.9em - 1px);
  border-width: 1px;
  border-radius: 0;
}
.quoteForm__form.is-busy .button__pill {
  opacity: 0.5;
  pointer-events: none;
}
/* A cell narrower than Send's own 164px (two fields to a row on a card not much wider than the
   narrow one, a small phone): Send closes up its arrow and gives up as much of its side air as it
   must to fit it (50cqw - 57px: half the cell, less half its text, arrow, gap and border, and a
   pixel to spare). */
@container send (max-width: 164px) {
  .quoteForm .button__pill {
    gap: 0.5em;
    padding-inline: calc(clamp(0.9em, 50cqw - 57px, 1.5em) - 1px);
  }
}
/* The status runs the card's width from under Send: a float the size of Send's cell keeps its first
   lines in the right-hand column, beside the button (which always fits its cell: a narrow card
   gives it a row of its own, a narrower cell trims its air, above), and a word too long for that
   column (the failure message's address, wider than half a card at most sizes) drops below the
   button, whole, to the card's full width, where it always fits. (So no overflow-wrap: with one,
   the address was broken mid-word beside the button instead.) */
.quoteForm__status {
  grid-row: status;
  grid-column: send-start / status-end;
  align-self: center;
  margin: 0;
  min-height: 0;
  font: 400 var(--micro) / 1.4 var(--mono);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  text-align: right;
}
/* (the address in a "not sent" message: it opens the visitor's mail app, the message written) */
.quoteForm__mailto {
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 2px;
}
.quoteForm__status:not(:empty)::before {
  content: '';
  float: left;
  width: calc(50% + 7px); /* (Send's column and the gap: 50% is the column and half the gap) */
  height: 44px; /* (Send's height: .button__pill's min-height) */
}

/* --- the foot: the email row, the barcode and the © line --- */
.contact__foot {
  grid-area: foot;
  position: relative;
  z-index: 2;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  row-gap: var(--gap-y);
  min-width: 0;
}
.contact__stamp {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 14px;
  min-width: 0;
  position: relative;
  z-index: 1; /* (over the wordmark's box, which it shares on tablets) */
}
.contact__mail {
  display: flex;
  align-items: center;
  gap: 0.8em;
  min-width: 0;
  padding: 0.62em 0.7em;
  border-top: 1px solid var(--ink);
  border-bottom: 1px solid var(--ink);
  /* (a HUD bar: its rules thicken to 4px for a run at the top right and the bottom left) */
  background:
    linear-gradient(var(--ink), var(--ink)) right top / 64px 3px no-repeat,
    linear-gradient(var(--ink), var(--ink)) left bottom / 64px 3px no-repeat,
    var(--panel);
  font: 400 var(--mail, 14px) / 1.1 var(--mono);
  letter-spacing: -0.02em;
  white-space: nowrap;
  transition: background-color 0.2s ease, color 0.2s ease;
}
.contact__mail-k {
  flex: none;
  padding: 0.3em 1.2em 0.3em 0.45em;
  background: var(--ink);
  color: var(--panel);
  line-height: 1;
  clip-path: polygon(0 0, 100% 0, calc(100% - 0.7em) 100%, 0 100%);
}
.contact__mail-v {
  flex: 1;
  min-width: 0;
}
@media (hover: hover) {
  .contact__mail:hover {
    background: var(--ink);
    color: var(--panel);
  }
  .contact__mail:hover .contact__mail-k {
    background: var(--panel);
    color: var(--ink);
  }
}
/* (a sticker's corner brackets round it) */
.contact__barcode {
  --b: linear-gradient(var(--ink), var(--ink));
  display: block;
  flex: none;
  align-self: flex-start;
  padding: 5px 6px 3px;
  background:
    var(--b) left top / 8px 1px no-repeat,
    var(--b) left top / 1px 8px no-repeat,
    var(--b) right top / 8px 1px no-repeat,
    var(--b) right top / 1px 8px no-repeat,
    var(--b) left bottom / 8px 1px no-repeat,
    var(--b) left bottom / 1px 8px no-repeat,
    var(--b) right bottom / 8px 1px no-repeat,
    var(--b) right bottom / 1px 8px no-repeat;
}
.contact__barcode svg {
  display: block;
}
.contact__barcode svg {
  width: var(--code-w, 60px);
  height: auto;
}
.contact__barcode text {
  font-family: var(--mono);
}
/* (on a chip of the mist: it can sit over the fan) */
.contact__copy {
  padding: 0.2em 0 0.2em 0.4em;
  background: var(--panel);
  color: rgba(17, 17, 17, 0.72); /* (AA on the mist at this size) */
  text-align: right;
  white-space: nowrap;
}

/* ---------------------------------------------------- phones, portrait (and their short tiers) */
@media (max-width: 699px) and (min-height: 501px), (max-width: 699px) and (orientation: portrait) {
  /* The card's close octagon only when focused, as with a mouse (the header's Close is right above
     it). */
  .contact__close:not(:focus-visible) {
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
  /* The foot, one row: the barcode (with its number) on the left, the email over the © line on
     the right, one hairline above. The address keeps one line at any phone's width. */
  .contact__foot {
    grid-template-columns: 64px minmax(0, 1fr);
    grid-template-rows: auto auto;
    column-gap: 12px;
    row-gap: 3px;
    align-items: end;
    padding-top: 8px;
    border-top: 1px solid var(--ink);
    background: var(--panel);
  }
  .contact__stamp {
    display: contents;
  }
  .contact__barcode {
    grid-column: 1;
    grid-row: 1 / 3;
    align-self: center;
  }
  .contact__barcode svg {
    width: 52px;
  }
  .contact__mail {
    grid-column: 2;
    grid-row: 1;
    padding: 0;
    border: 0;
    background: none; /* (no HUD bar here: its rules are the foot's hairline) */
    font-size: min(13px, (100cqw - 2 * var(--gutter) - 76px) / 17.6);
  }
  .contact__mail-k {
    display: none;
  }
  .contact__copy {
    grid-column: 2;
    grid-row: 2;
    padding: 0;
    text-align: left;
  }
}
/* Short phones (an SE, a phone with its toolbars showing): tighter form, no tag. */
@media (max-width: 699px) and (max-height: 700px) and (min-height: 501px), (max-width: 699px) and (max-height: 700px) and (orientation: portrait) {
  .contact__scroll {
    --f-gap: 6px;
    --f-pad: 8px;
    --in-pad: 0.3em;
    --area-h: clamp(56px, 7cqh, 80px);
  }
}
/* Very short ones (an SE with Safari's bars showing): tighter still, so the mark stays a mark. */
@media (max-width: 699px) and (max-height: 600px) and (min-height: 501px), (max-width: 699px) and (max-height: 600px) and (orientation: portrait) {
  .contact__scroll {
    --ann-b: 16px;
    --pad-t: 8px;
    --pad-b: max(8px, var(--safe-b));
    --gap-y: 6px;
    --f-gap: 5px;
  }
  /* Once the form has something to say, its messages get the room: the details pad gives up its
     second line (what is typed scrolls in the one). */
  .contact__scroll:has(.quoteForm__field.is-invalid) {
    --area-h: 32px;
    --area-min: 32px;
  }
}

/* ---------------------------------------------------- tablets, portrait (700-899) */
@media (min-width: 700px) and (max-width: 899px) and (min-height: 501px) {
  .contact__scroll {
    --micro: 11px;
    --label: 12px;
    --mail: 18px;
    --code-w: 150px;
    --f-gap: 14px;
    --f-pad: 16px;
    --f-pad-x: 18px;
    --area-h: clamp(90px, 12cqh, 170px);
    --mark-max: 600px;
    --ann-x: 22px;
    --ann-b: 38px;
  }
  /* The mark stands alone across the top, centred in its row; under it the wordmark's box (the
     barcode stamp at its foot) beside the form card; the email across the bottom. */
  .contact__grid {
    grid-template-columns: minmax(0, 0.8fr) minmax(0, 1.2fr);
    grid-template-rows: minmax(max(0px, 56px + var(--ann-b)), 1fr) auto auto;
    grid-template-areas:
      'mark  mark'
      'brand form'
      'mail  mail';
  }
  .contact__mark-stage {
    top: 50%;
    translate: 0 -50%;
  }
  .contact__brand {
    display: block;
    grid-area: brand;
  }
  /* (centred on the card's corner: the gutter has room for it from here up) */
  .contact__close {
    margin: -22px -22px 0 0;
  }
  /* The foot splits: the email runs across the bottom, the barcode stamp sits at the foot of the
     wordmark's box (fan.js stops the fan above it). */
  .contact__foot {
    display: contents;
  }
  .contact__mail {
    grid-area: mail;
  }
  .contact__stamp {
    grid-area: brand;
    align-self: end;
    flex-direction: column;
    align-items: stretch;
    gap: 8px;
  }
  .contact__copy {
    padding: 8px 0 0;
    border-top: 1px solid var(--ink);
    text-align: left;
  }
}

/* ---------------------------------------------------- desktops: three columns on one axis
   The mark (centred in its column) and the email share the centre; the wordmark's box over the
   barcode stamp holds the left, the form card the right, top to bottom. */
@media (min-width: 900px) and (min-height: 501px) {
  .contact__scroll {
    --micro: 10.5px;
    --label: 11.5px;
    --mail: clamp(14px, 1.2vw, 22px);
    --f-gap: clamp(10px, 1.8cqh, 20px);
    --f-pad: clamp(12px, 1.9cqh, 22px);
    --f-pad-x: clamp(14px, 1.3vw, 24px);
    --area-min: 4.2em;
    --mark-max: 760px;
    --ann-x: 22px;
    --ann-b: 38px;
    /* (the mark's box kept between the cards either side of it: resolved on the stage, whose
       100cqw is the centre column) */
    --mark-cap: calc((100cqw + 2 * var(--gap-x) - 4px) * 0.8);
  }
  .contact__grid {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1.25fr) minmax(0, 1fr);
    grid-template-rows: minmax(max(0px, 56px + var(--ann-b)), 1fr) auto;
    grid-template-areas:
      'brand mark form'
      'stamp mail form';
  }
  .contact__mark-stage {
    top: 50%;
    translate: 0 -50%;
  }
  .contact__brand {
    display: block;
    grid-area: brand;
  }
  /* (centred on the card's corner, but kept 6px below the scroller's top edge, which clips: the
     card starts in the first row here, under a top padding of 24px at most, and the focus ring
     needs its 6px) */
  .contact__close {
    margin: max(-22px, 6px - var(--pad-t)) -22px 0 0;
  }
  .quoteForm {
    align-self: stretch;
  }
  /* The pad fills what the card has left: out of flow in its own grid row, so its rows="4" never
     props the card open. */
  .quoteForm__field[data-field='details'] {
    position: relative;
    grid-template-rows: auto minmax(var(--area-min), 1fr);
  }
  .quoteForm__input--area {
    position: absolute;
    left: 0;
    top: 2px;
    width: 100%;
    height: calc(100% - 2px);
    min-height: 0;
    margin: 0;
  }
  /* (In whole lines where the browser can round, once the row holds two: what it has over them
     stays clear under the pad's rule, and a scrolled brief shows no line cut through its middle.
     Short of two, a short window's pad keeps all the row has.) */
  @supports (height: calc(round(down, 100% - 10px, 24px) + 8px)) {
    .quoteForm__input--area {
      height: max(round(down, 100% - 10px, var(--rule-lh)) + 8px, min(100% - 2px, 2 * var(--rule-lh) + 8px));
    }
  }
  /* (The right rail starts clear of the close disc on the card's corner.) */
  .contact__rail--r {
    top: calc(var(--pad-t) + 34px);
  }
  /* The foot splits: the email under the mark, the barcode stamp under the wordmark's box. */
  .contact__foot {
    display: contents;
  }
  .contact__mail {
    grid-area: mail;
    align-self: end;
  }
  .contact__stamp {
    grid-area: stamp;
    align-self: end;
    flex-direction: column;
    align-items: stretch;
    gap: 10px;
  }
  /* (A stamp, not a second headline: it stays a tag's size on a big screen.) */
  .contact__barcode svg {
    width: min(16cqh * 2.076, clamp(130px, 10vw, 180px)); /* (no %: its bracketed box is as wide as it) */
  }
  .contact__copy {
    padding: 8px 0 0;
    border-top: 1px solid var(--ink);
    text-align: left;
  }
  /* A short window (700px of panel or less: a laptop's browser with its bars): once the form has
     something to say (a field's message, the status), the details pad gives it all but one whole
     ruled line (the line and the pad's 8px of air, 2px down its row). */
  @container panel (max-height: 700px) {
    .contact__grid:has(.quoteForm__field.is-invalid),
    .contact__grid:has(.quoteForm__status:not(:empty)) {
      --area-min: 34px;
    }
  }
  /* Shorter (600px of panel or less): a smaller barcode, a details pad
     that may be shorter at rest too, and on a narrow card (one field to a row: from 900 to ~1150px
     wide) less air between the fields and in the inputs, which keeps its extra rows (and a
     three-line status under Send) on the screen with the pad still a line or more. */
  @container panel (max-height: 600px) {
    .contact__barcode svg {
      width: min(12cqh * 2.076, clamp(130px, 10vw, 180px));
    }
    .contact__grid {
      --area-min: 2.6em;
    }
    @container qf (max-width: 319px) {
      .quoteForm__form {
        --f-gap: 6px;
        --in-pad: 0.3em;
      }
      /* (Messages up, or the status, which runs two or three lines under Send here: the card, the
         fields and the inputs give up a little more, as on a phone on its side.) */
      .quoteForm__form:has(.quoteForm__field.is-invalid),
      .quoteForm__form:has(.quoteForm__status:not(:empty)) {
        --f-gap: 4px;
        --f-pad: 7px;
        --in-pad: 0.15em;
      }
    }
  }
  /* Shorter again (515px of panel or less: a window under ~590px tall) with the wide card (two
     fields to a row): once the form has something to say, less air between the fields and in the
     inputs, as the narrow card has above, so the messages fit with the pad still a whole line. */
  @container panel (max-height: 515px) {
    @container qf (min-width: 320px) {
      .quoteForm__form:has(.quoteForm__field.is-invalid),
      .quoteForm__form:has(.quoteForm__status:not(:empty)) {
        --f-gap: 6px;
        --in-pad: 0.3em;
      }
    }
  }
}
/* Very large screens: the small type grows with the poster. Not in a short window (600px of panel
   or less): there the poster is no taller than a laptop's, and the larger type's messages did not
   fit it. */
@media (min-width: 1800px) and (min-height: 673px) {
  .contact__scroll {
    --micro: 12px;
    --label: 13.5px;
  }
  .quoteForm__input {
    font-size: 19px;
  }
  .quoteForm__input--area {
    --rule-lh: 28px;
  }
  .quoteForm .button__pill {
    font-size: 15px;
  }
}

/* ---------------------------------------------------- phones on their side: two columns
   The mark and the foot down the left; the form down the right. */
@media (orientation: landscape) and (max-height: 500px) {
  .contact__scroll {
    --gap-x: 20px;
    --pad-t: 8px;
    --pad-b: max(8px, var(--safe-b));
    --gap-y: 6px;
    --f-gap: 4px;
    --f-pad: 7px;
    --f-pad-x: 10px;
    --in-pad: 0.3em;
    --area-min: 34px; /* (one whole ruled line: the pad stands 2px down its row) */
    --micro: 9.5px;
    --label: 10px;
    --mail: 12px;
    --code-w: 48px;
    /* (the mark's box kept off the form's column: resolved on the stage, as on desktops) */
    --mark-cap: calc((100cqw + 2 * var(--gap-x) - 4px) * 0.8);
  }
  .contact__grid {
    grid-template-columns: minmax(0, 0.46fr) minmax(0, 0.54fr);
    grid-template-rows: minmax(max(0px, 56px + var(--ann-b)), 1fr) auto;
    grid-template-areas:
      'mark form'
      'foot form';
  }
  .quoteForm {
    align-self: stretch;
  }
  /* The pad fills what the card has left (as on desktops). */
  .quoteForm__field[data-field='details'] {
    position: relative;
    grid-template-rows: auto minmax(var(--area-min), 1fr);
  }
  .quoteForm__input--area {
    position: absolute;
    left: 0;
    top: 2px;
    width: 100%;
    height: calc(100% - 2px);
    min-height: 0;
    margin: 0;
  }
  @supports (height: calc(round(down, 100% - 10px, 24px) + 8px)) {
    .quoteForm__input--area {
      height: max(round(down, 100% - 10px, var(--rule-lh)) + 8px, min(100% - 2px, 2 * var(--rule-lh) + 8px));
    }
  }
  .contact__mail-k,
  .contact__rail {
    display: none;
  }
  /* (as on desktops: the card starts in the first row, under 8px of top padding; and shown only
     when focused, as on phones upright) */
  .contact__close {
    width: 38px;
    height: 38px;
    margin: max(-19px, 6px - var(--pad-t)) -19px 0 0;
  }
  .contact__close:not(:focus-visible) {
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
  /* (Messages up: the inputs and the card give up some of their air, so the pad keeps its one
     line, a failure's address dropped under Send included.) */
  .contact__scroll:has(.quoteForm__field.is-invalid) {
    --in-pad: 0.15em;
    --f-pad: 5px;
  }
}

/* ======================================================================= 9. SHEET
   The project sheet: #sheet[role=dialog], fixed, z 60, a mist rounded panel. No scrim and no
   darkening around it: the margins show the page itself (the black #work behind). During a GL
   flight the panel is transparent (.is-flying) so the content reveals over the flying plane;
   DOM flights clip the panel from the source rect with a washing image copy (.sheet__wash). */

.sheet {
  --sheet-pad: 40px;
  --sheet-ink-soft: rgba(17, 17, 17, 0.7); /* (5.8:1 on the mist: AA for the small mono labels) */
  --sheet-rule: rgba(17, 17, 17, 0.16);
  position: fixed;
  z-index: 60;
  /* (clear of a notch / home indicator on a phone on its side: env() is 0 elsewhere) */
  inset: max(2.2svh, env(safe-area-inset-top)) max(3vw, env(safe-area-inset-right)) max(2.2svh, env(safe-area-inset-bottom))
    max(3vw, env(safe-area-inset-left));
  color: var(--ink);
  touch-action: pan-y pinch-zoom; /* horizontal swipes are prev / next */
}
.sheet[hidden] {
  display: none;
}
/* Pinch-zoomed in with a project open, iOS pans the page itself under the locked sheet as the
   zoomed view moves, taking #work out from behind the sheet (and the rubber band below scale 1
   on the way out shows more of the page than the screen). For the whole
   gesture the sheet's surround is the black it shows at rest (and so are the edges: main.js):
   sheet.js .sheet-covered (two fingers down, the scale off 1, or the page off #work) puts #work
   back once the fingers are off and the scale is 1, and this fades only then. Only while the sheet
   is settled (sheet.js .sheet-settled): never over a flight in or out (the plane flies over the
   page, under the sheet), and it goes at once when a close starts (#work is already back behind). */
body.sheet-open::before {
  content: '';
  position: fixed;
  z-index: 59; /* (under the sheet, over the page) */
  inset: -50vh -50vw; /* (past the edges too: a zoomed pan's rubber band) */
  background: var(--black);
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
}
body.sheet-settled::before {
  transition: opacity 0.3s ease 0.15s, visibility 0s linear 0.45s; /* (the zoom undone) */
}
html.is-zoomed body.sheet-settled::before,
body.sheet-settled.sheet-covered::before {
  opacity: 1;
  visibility: visible;
  transition: opacity 0.2s ease, visibility 0s;
}
/* Laid out but unseen while the content is built and measured. (Opacity, not visibility: the
   reduced-motion rule gives every property a .01ms transition, and a visibility that is still
   "hidden" for that instant would refuse the h1's focus.) */
.sheet.is-measuring {
  opacity: 0;
  pointer-events: none;
}
.sheet ::selection {
  background: var(--ink);
  color: var(--mist);
}
/* The header steps aside under the sheet (it would otherwise sit over the flying plane); the
   contact panel opened from the sheet brings it back on top with "Close". */
body.sheet-open .hdr {
  opacity: 0;
  visibility: hidden;
  transition: color 0.2s ease, opacity 0.3s ease, visibility 0s linear 0.3s;
}
body.sheet-open.contact-open .hdr {
  opacity: 1;
  visibility: visible;
  transition: color 0.2s ease, opacity 0.3s ease;
}
body.sheet-open.contact-open.ink-snap .hdr {
  transition-duration: 0s, 0.3s; /* (see body.ink-snap .hdr) */
}
.sheet__panel {
  position: absolute;
  inset: 0;
  overflow: hidden;
  isolation: isolate;
  border-radius: var(--r-sheet);
  background: var(--mist);
}
.sheet.is-flying .sheet__panel {
  background: transparent;
}
.sheet__wash {
  position: absolute;
  z-index: 2;
  display: none;
  overflow: hidden;
  pointer-events: none;
}
.sheet.is-washing .sheet__wash {
  display: block;
}
.sheet__wash img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 0;
}

.sheet__body,
.sheet__media,
.sheet__copy {
  touch-action: pan-y pinch-zoom; /* (each scroller decides its own touch behaviour) */
}
.sheet__body {
  position: absolute;
  z-index: 1;
  inset: 0;
  display: grid;
  grid-template-columns: minmax(320px, 34%) minmax(0, 1fr);
}

/* --- left column: copy (stays put while the image column scrolls) --- */

.sheet__copy {
  min-height: 0;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: 32px;
  padding: var(--sheet-pad);
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: none;
}
.sheet__copy::-webkit-scrollbar {
  display: none;
}
.sheet__title {
  margin: 0 0 clamp(16px, 2.2vh, 26px) -0.045em;
  font: 600 clamp(40px, 5.2vw, 88px) / 0.92 var(--display);
  letter-spacing: -0.035em;
  outline: none;
  overflow-wrap: break-word;
  hyphens: manual;
}
/* The h1 takes programmatic focus on open (for screen readers): no ring around the title. */
.sheet__title:focus,
.sheet__title:focus-visible {
  outline: none;
}
/* Room for descenders inside the line masks at this tight line height. */
.sheet__title .line {
  padding: 0.04em 0 0.12em;
  margin: -0.04em 0 -0.12em;
}
.sheet__line {
  max-width: 36ch;
  font: 400 18px/1.35 var(--ui);
  letter-spacing: -0.012em;
}
.sheet__line .line {
  padding: 0.1em 0;
  margin: -0.1em 0;
}
/* (the client's own lines, data.js caption, a block each: "Making memories since 2012" over "No.
   34 on Canada's 100 Best 2025"), each kept to one line: where the column is too narrow for it at
   its size (from ~900px wide to a tablet's portrait), its type comes down to fit. (The longer of
   deer + almond's runs 14.5em: a line longer than that would want a smaller divisor.) */
.sheet__line {
  container-type: inline-size;
}
.sheet__line-part {
  display: block;
  white-space: nowrap;
  font-size: min(1em, 100cqw / 14.7);
}
/* Under it, the location ("Winnipeg, MB"), small, in the mono. */
.sheet__place {
  margin-top: clamp(12px, 1.8vh, 18px);
  font: 400 12px/1.2 var(--mono);
  text-transform: uppercase;
  letter-spacing: -0.02em;
  color: var(--sheet-ink-soft);
}

.sheet__pills {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: clamp(22px, 3.2vh, 34px);
}
.pill {
  position: relative;
  overflow: hidden;
  isolation: isolate;
  display: inline-flex;
  align-items: center;
  gap: 0.6em;
  height: 32px;
  padding: 0 calc(1.15em + 1px);
  /* The 1px outline is an inset shadow, not a border: the flood is clipped to the padding box, so a
     border stayed outside it, a ring of the pill's own colour round the fill with a light seam
     where the two edges met. Painted under the flood, it is covered right out to the edge. (The
     extra 1px of padding keeps the pill the size the border made it.) */
  box-shadow: inset 0 0 0 1px var(--ink);
  border-radius: 999px;
  color: var(--ink);
  font: 400 11px/1 var(--mono);
  text-transform: uppercase;
  letter-spacing: -0.02em;
  white-space: nowrap;
  transition: color 0.25s ease;
  transform-origin: 30% 50%;
}
.pill > :not(.flood) {
  position: relative;
}
.pill .flood {
  background: var(--ink);
}
.pill.is-flooded {
  color: var(--mist);
}
.pill .i {
  font-size: 13px;
}
/* (Visit site, .pill--visit, is a pill like the rest: outlined in the ink, as the Instagram link
   beside it is.) */
/* An icon-only link (the client's Instagram): a 32px circle. */
.pill--icon {
  justify-content: center;
  width: 32px;
  padding: 0;
}
.pill--icon .i {
  font-size: 14px;
}

.sheet__notes {
  max-width: 40ch;
  margin-top: clamp(22px, 3.4vh, 36px);
  font: 400 11px/1.35 var(--mono);
  text-transform: uppercase;
  letter-spacing: -0.02em;
}
.sheet__notes li {
  padding: 10px 0 9px;
  border-top: 1px solid var(--sheet-rule);
}
.sheet__notes li:last-child {
  border-bottom: 1px solid var(--sheet-rule);
}
/* (the copy in the image stack, under the first screen, is a phone's: sheet.js) */
.sheet__notes--stack {
  display: none;
}
.sheet__start {
  display: inline-flex;
  align-items: center;
  gap: 0.7em;
  margin-top: clamp(18px, 2.6vh, 26px);
  padding: 0.35em 0;
  border-bottom: 1px solid currentColor;
  font: 400 12px/1.2 var(--mono);
  text-transform: uppercase;
  letter-spacing: -0.02em;
}
.sheet__start .i {
  transition: transform 0.6s var(--expo-out);
}
@media (hover: hover) {
  .sheet__start:hover .i {
    transform: translateX(4px);
  }
}

.sheet__foot {
  display: flex;
  align-items: center;
  gap: 18px;
  font: 400 12px/1 var(--mono);
  text-transform: uppercase;
  letter-spacing: -0.02em;
}
.sheet__ring {
  width: 28px;
  height: 28px;
  flex: none;
  transform: rotate(-90deg);
  overflow: visible;
}
.sheet__ring circle {
  fill: none;
  stroke: var(--ink);
  stroke-width: 2;
}
.sheet__ring-track {
  opacity: 0.16;
}
.sheet__nav {
  display: flex;
  align-items: center;
  gap: 0.75em;
}
.sheet__nav button {
  display: inline-flex;
  align-items: center;
  gap: 0.6em;
  padding: 0.5em 0;
  border-radius: 3px;
  text-transform: inherit;
  letter-spacing: inherit;
  transition: opacity 0.3s ease;
}
.sheet__nav .i {
  transition: transform 0.6s var(--expo-out);
}
@media (hover: hover) {
  .sheet__nav button:hover .i {
    transform: translateX(3px);
  }
  .sheet__prev:hover .i {
    transform: translateX(-3px) !important;
  }
}
.sheet__slash {
  opacity: 0.4;
}

/* --- right column: the image stack scrolls inside the sheet --- */

.sheet__media {
  position: relative;
  min-height: 0;
  overflow-x: hidden;
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: none;
  padding: var(--sheet-pad) 104px var(--sheet-pad) 8px;
  outline: none;
}
.sheet__media::-webkit-scrollbar {
  display: none;
}
.sheet__media:focus-visible {
  box-shadow: inset 0 0 0 2px var(--ink);
  border-radius: 12px;
}
.sheet__stack {
  display: flex;
  flex-direction: column;
  gap: clamp(24px, 4vw, 64px);
}
.sheet__frame,
.sheet__phone {
  position: relative;
  overflow: hidden;
  border: 1px solid rgba(17, 17, 17, 0.12);
  background: #c8cbcf;
}
.sheet__frame {
  aspect-ratio: 16 / 10;
  border-radius: 10px;
}
.sheet__frame img,
.sheet__phone img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 0;
  opacity: 0;
  transition: opacity 0.6s ease;
  -webkit-user-drag: none;
  user-select: none;
}
.sheet__frame.is-loaded img,
.sheet__phone.is-loaded img {
  opacity: 1;
}
/* Loading: a soft light sweep across the empty frame (per-frame; stops once the image is in). */
.sheet__frame:not(.is-loaded)::after,
.sheet__phone:not(.is-loaded)::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(100deg, rgba(255, 255, 255, 0) 35%, rgba(255, 255, 255, 0.32) 50%, rgba(255, 255, 255, 0) 65%) 0 0 / 250% 100% no-repeat;
  animation: sheet-sheen 1.6s ease-in-out infinite;
}
@keyframes sheet-sheen {
  from {
    background-position: 130% 0;
  }
  to {
    background-position: -30% 0;
  }
}
.is-reduced .sheet__frame::after,
.is-reduced .sheet__phone::after {
  animation: none;
}
.sheet__phones {
  display: flex;
  justify-content: center;
  gap: clamp(16px, 2.2vw, 36px);
}
.sheet__phone {
  flex: 1 1 0;
  max-width: 260px;
  aspect-ratio: 540 / 1169;
  border-radius: 22px;
}
.sheet__nextrow {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 14px;
  width: 100%;
  padding: 26px 0 8px;
  border-top: 1px solid rgba(17, 17, 17, 0.2);
  text-align: left;
  color: var(--ink);
}
.sheet__nextrow-k {
  font: 400 12px/1 var(--mono);
  text-transform: uppercase;
  letter-spacing: -0.02em;
  color: var(--sheet-ink-soft);
}
.sheet__nextrow-name {
  display: inline-flex;
  align-items: center;
  gap: 0.35em;
  font: 600 40px/0.95 var(--display);
  letter-spacing: -0.03em;
}
.sheet__nextrow-name .i {
  font-size: 0.7em;
  transition: transform 0.7s var(--expo-out);
}
@media (hover: hover) {
  .sheet__nextrow:hover .sheet__nextrow-name .i {
    transform: translateX(8px);
  }
}

/* --- close: a round black button whose fill hollows to a ring on hover, the X turning 90° --- */

.sheet__close {
  position: absolute;
  z-index: 3;
  top: 24px;
  right: 24px;
  width: 48px;
  height: 48px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  box-shadow: inset 0 0 0 1.5px var(--ink);
  color: var(--mist);
  font-size: 16px;
  transition: color 0.35s ease;
}
.sheet__close::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: var(--ink);
  transition: transform 0.6s var(--expo-out);
}
.sheet__close .i {
  position: relative;
  transition: transform 0.7s var(--expo-out);
}
@media (hover: hover) {
  .sheet__close:hover {
    color: var(--ink);
  }
  .sheet__close:hover::before {
    transform: scale(0);
  }
  .sheet__close:hover .i {
    transform: rotate(90deg);
  }
}
.sheet__close:active .i {
  transform: rotate(90deg) scale(0.9);
}
.sheet :focus-visible {
  outline-color: var(--ink);
}

/* Mid widths: a narrower copy column and smaller gutters so long names still fit. */
@media (max-width: 1180px) and (min-width: 700px) {
  .sheet {
    --sheet-pad: 32px;
  }
  .sheet__body {
    grid-template-columns: minmax(300px, 36%) minmax(0, 1fr);
  }
  .sheet__title {
    font-size: clamp(36px, 4.8vw, 88px);
  }
  .sheet__media {
    padding-right: 92px;
  }
}

/* Very large screens: the copy column scales with the sheet instead of leaving its upper
   half of type marooned at the top of a tall, wide column. */
@media (min-width: 1800px) {
  .sheet__body {
    grid-template-columns: minmax(320px, 30%) minmax(0, 1fr);
  }
  .sheet__title {
    font-size: clamp(88px, 5.6vw, 150px);
  }
  .sheet__line {
    font-size: 22px;
  }
}

/* Compact: one column, the whole sheet scrolls, close fixed at the bottom-right. */
@media (max-width: 699px) {
  .sheet {
    inset: max(12px, env(safe-area-inset-top)) max(12px, env(safe-area-inset-right)) max(12px, env(safe-area-inset-bottom))
      max(12px, env(safe-area-inset-left));
  }
  /* The work comes first on a phone: the notes come after its first screen, in the image stack,
     not in the copy above it. */
  .sheet__copy .sheet__notes {
    display: none;
  }
  .sheet__notes--stack {
    display: block;
    max-width: none;
    margin: 0;
    font-size: 12px;
  }
  .sheet__body {
    display: block;
    overflow-x: hidden;
    overflow-y: auto;
    /* none, not contain: no bounce at the top, so a drag down there moves only the sheet
       (sheet.js pulls it down to close) and never the content inside it as well. */
    overscroll-behavior: none;
    scrollbar-width: none;
  }
  .sheet__body::-webkit-scrollbar {
    display: none;
  }
  .sheet__copy {
    overflow: visible;
    gap: 28px;
    padding: 30px 22px 8px;
  }
  .sheet__title {
    font-size: clamp(34px, 10vw, 48px);
  }
  .sheet__line {
    font-size: 16px;
  }
  .sheet__ring {
    display: none;
  }
  /* (One project: no prev / next either, so the foot would be an empty row taking a gap.) */
  .sheet.is-single .sheet__foot {
    display: none;
  }
  .sheet__media {
    overflow: visible;
    padding: 28px 12px 100px;
  }
  .sheet__stack {
    gap: 24px;
  }
  .sheet__frame {
    border-radius: 8px;
  }
  .sheet__phones {
    flex-wrap: wrap;
    justify-content: space-between;
    row-gap: 14px; /* (a % row gap resolves against an indefinite height: 0) */
    column-gap: 0;
  }
  .sheet__phones--3 {
    justify-content: center;
    column-gap: 4%;
  }
  .sheet__phone {
    flex: 0 0 48%;
    max-width: none;
    border-radius: 18px;
  }
  .sheet__nextrow {
    padding-top: 22px;
  }
  .sheet__nextrow-name {
    font-size: 30px;
  }
  .sheet__close {
    top: auto;
    bottom: 14px;
    right: 14px;
    width: 52px;
    height: 52px;
  }
}

/* ======================================================================= 10. INDEX VIEW
   nav#work-index inside #work (z 2: above the canvas, below the HUD). Names centred and
   wrapped with superscript indices and mist dots in the gaps; a cursor-following preview card
   sits behind them on desktop (indexview.js drives its transform). */

.work-index {
  --ix-gap: clamp(30px, 3.4vw, 60px);
  position: absolute;
  z-index: 2;
  inset: 0;
  display: grid;
  place-items: center;
  padding: calc(var(--hdr-h) + 64px) var(--gutter) 128px;
  pointer-events: none;
}
.work-index[hidden] {
  display: none;
}
.work-index__list {
  position: relative;
  z-index: 1;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: baseline;
  column-gap: var(--ix-gap);
  row-gap: 0.12em;
  max-width: min(1240px, 100%);
  margin-top: -6vh; /* sit a little above centre: the floor and its reflections own the lower half */
  font: 600 clamp(28px, 4.4vw, 68px) / 1.06 var(--display);
  letter-spacing: -0.03em;
  color: var(--mist);
  text-align: center;
}
.work-index__item {
  position: relative;
  white-space: nowrap;
}
.work-index__link {
  display: inline-block;
  border-radius: 6px;
  pointer-events: auto;
  transition: opacity 0.35s ease, color 0.35s ease;
}
.work-index__mask {
  display: inline-block;
  overflow: hidden;
  vertical-align: top;
  padding: 0.06em 0.05em 0.12em;
  margin: -0.06em -0.05em -0.12em;
}
.work-index__inner {
  display: inline-block;
  will-change: transform;
}
.work-index__dot {
  position: absolute;
  left: 100%;
  top: 54%;
  width: 7px;
  height: 7px;
  margin-left: calc(var(--ix-gap) / 2);
  border-radius: 50%;
  background: var(--mist);
  translate: -50% -50%;
  pointer-events: none;
}
.work-index__item.no-dot .work-index__dot {
  display: none;
}
.work-index__list.has-hover .work-index__link {
  opacity: 0.35;
}
.work-index__list.has-hover .work-index__link.is-active {
  opacity: 1;
  color: #fff;
  text-shadow: 0 0 24px rgba(0, 0, 0, 0.45); /* (a soft lift off the preview card behind it) */
}
.work-index__dot {
  transition: opacity 0.35s ease;
}
.work-index__list.has-hover .work-index__dot {
  opacity: 0.35;
}
.js .work-index__inner {
  transform: translateY(135%); /* hidden until show() reveals it (clear of the mask padding) */
}

.work-index__preview {
  position: fixed;
  z-index: 0;
  left: 0;
  top: 0;
  width: 320px;
  height: 200px;
  visibility: hidden;
  opacity: 0;
  pointer-events: none;
  will-change: transform, opacity;
}
.work-index__card {
  position: relative;
  width: 100%;
  height: 100%;
  overflow: hidden;
  border-radius: 14px;
  background: #111;
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.14);
}
.work-index__card img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 0;
  opacity: 0;
  filter: brightness(0.55) saturate(0.9); /* per-surface: keeps the white name readable over a bright cover */
  transition: opacity 0.35s ease;
}
.work-index__card img.is-on {
  opacity: 1;
}

/* No WebGL: the fallback row steps away while the Index shows, and while a sheet is up (its
   neighbours would otherwise peek into the sheet's margins). */
.no-gl .work-list {
  transition: opacity 0.5s ease, visibility 0s linear 0s;
}
.no-gl body.sheet-cover .work-list {
  opacity: 0;
  transition: opacity 0.35s ease;
}
.no-gl .work[data-mode='index'] .work-list {
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.4s ease, visibility 0s linear 0.4s;
}

@media (max-width: 699px) {
  .work-index {
    --ix-gap: 26px;
    padding: calc(var(--hdr-h) + 56px) 20px 110px;
  }
  .work-index__list {
    row-gap: 0.2em;
    margin-top: -4vh;
  }
  .work-index__dot {
    width: 6px;
    height: 6px;
  }
}

/* ======================================================================= short landscape
   A phone on its side (wider than "compact", but under 500px tall): a slimmer header, the
   sections exactly one screen each, and cards sized to the height, so the ribbon, its floor
   and the HUD row all fit on screen with #work framed. */

@media (orientation: landscape) and (max-height: 500px) {
  :root {
    --hdr-h: 52px;
    --slot-h: 26px;
  }
  .hero,
  .work {
    min-height: 0;
  }
  .work {
    --card-h: min(50svh, 300px);
  }
  .hero__foot {
    bottom: calc(14px + var(--safe-b));
  }
  .work-hud__eyebrow {
    top: calc(var(--hdr-h) + 10px);
  }
  .work-hud__bar {
    bottom: max(12px, var(--safe-b));
  }
  @supports (height: 100dvh) {
    .work-hud__bar {
      bottom: calc(100lvh - 100dvh + max(12px, var(--safe-b)));
    }
  }
  .work-index {
    padding-top: calc(var(--hdr-h) + 36px);
    padding-bottom: 64px;
  }
  /* (Every section is one screen here, so the page pages like a tall phone's: main.js pager.) */
}

/* ======================================================================= 11. grain, focus, motion */

/* Film grain: a uniform overlay that jitters in steps. Not an edge effect: same everywhere. */
.grain {
  position: fixed;
  z-index: 100;
  top: -10rem;
  left: -10rem;
  width: calc(100% + 20rem);
  height: calc(100% + 20rem);
  background: url('../img/noise.png') 50% / 100px 100px repeat;
  mix-blend-mode: overlay;
  opacity: 0.22;
  pointer-events: none;
  animation: grain 0.5s steps(1) infinite;
}
@keyframes grain {
  0% { transform: translate3d(0, 9rem, 0); }
  10% { transform: translate3d(-1rem, -4rem, 0); }
  20% { transform: translate3d(-8rem, 2rem, 0); }
  30% { transform: translate3d(9rem, -9rem, 0); }
  40% { transform: translate3d(-2rem, 7rem, 0); }
  50% { transform: translate3d(-9rem, -4rem, 0); }
  60% { transform: translate3d(2rem, 6rem, 0); }
  70% { transform: translate3d(7rem, -8rem, 0); }
  80% { transform: translate3d(-9rem, 1rem, 0); }
  90% { transform: translate3d(6rem, -5rem, 0); }
  100% { transform: translate3d(-7rem, 0, 0); }
}
@media (max-width: 699px), (hover: none) {
  .grain {
    display: none; /* desktop only */
  }
}

:focus-visible {
  outline: 2px solid var(--mist);
  outline-offset: 3px;
}
:focus:not(:focus-visible) {
  outline: none;
}
.contact :focus-visible,
body.ink-dark .hdr :focus-visible {
  outline-color: var(--ink);
}
.work-card:focus-visible {
  outline-offset: 4px;
}
/* Sections receive programmatic focus after a skip / in-page jump: no ring around a whole screen. */
main > section[tabindex='-1']:focus {
  outline: none;
}

@media (prefers-reduced-motion: reduce) {
  .grain {
    animation: none;
  }
  *,
  *::before,
  *::after {
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
  /* (Not the mark's box, which has no transition of its own: under the rule above every property
     it has changes over 0.01ms, so the read straight after a class swap (logo3d.js homeRect, as
     the panel closes) found the fixed place it was leaving. The mark faded back in there, off its
     dock, and the locked page under the panel slipped (20px on a phone) and stayed.) */
  .logo3d {
    transition: none !important;
  }
}
.is-reduced .grain {
  animation: none;
}
