/* Relica. One stylesheet for every page.
   Palette is closed: burgundy, blush, espresso, black, white, and those five at partial alpha.
   Nothing here animates a property that triggers layout or paint: only transform, opacity,
   clip-path, and the individual translate / rotate / scale properties. */

/* ---------------------------------------------------------------- fonts */
/* Two files, 61KB together. Melodrama is the owner's display face, shipped as the variable
   font so one request covers Light 300 through Semibold 600. Only it is preloaded: the
   wordmark is the first thing anyone sees, the body face can swap in a frame later. */
@font-face {
  font-family: "Melodrama";
  src: url("../fonts/melodrama-var.woff2") format("woff2-variations"),
       url("../fonts/melodrama-var.woff2") format("woff2");
  font-weight: 300 700;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Instrument Sans";
  src: url("../fonts/instrument-sans-var.woff2") format("woff2-variations"),
       url("../fonts/instrument-sans-var.woff2") format("woff2");
  font-weight: 400 500;
  font-style: normal;
  font-display: swap;
}

/* ---------------------------------------------------------------- tokens */
:root {
  --espresso: #221815;
  --blush: #EFDCD5;
  --burgundy: #6E1F2A;
  --black: #000000;
  --white: #FFFFFF;

  /* Hairlines and surfaces. 12% is the lowest alpha that still reads as a drawn line on
     espresso at 1x; 8% disappears on a phone in daylight. */
  --hair: rgb(239 220 213 / 0.12);
  --hair-strong: rgb(239 220 213 / 0.22);
  --surface: rgb(239 220 213 / 0.045);
  --dim: rgb(239 220 213 / 0.72);

  /* Two layered black shadows, never coloured. */
  --shadow: 0 1px 2px rgb(0 0 0 / 0.35), 0 12px 32px rgb(0 0 0 / 0.32);

  --radius: 2px;
  --header-h: 56px;

  --display: "Melodrama", "Playfair Display", Georgia, "Times New Roman", serif;
  --text: "Instrument Sans", "Segoe UI", system-ui, -apple-system, Helvetica, Arial, sans-serif;

  --f-body: clamp(1rem, 0.98rem + 0.1vw, 1.0625rem);
  --f-label: 0.75rem;
  --f-h2: clamp(2rem, 5vw, 4rem);
  --f-h3: clamp(1.375rem, 2.6vw, 1.875rem);
  --f-line: clamp(1.75rem, 4.6vw, 3.25rem);

  --gutter: 20px;
  --wrap: 1280px;

  --s1: 4px;  --s2: 8px;  --s3: 12px; --s4: 16px;
  --s5: 24px; --s6: 32px; --s7: 48px; --s8: 64px;
  --s9: 96px; --s10: 128px;
}
@media (min-width: 900px) { :root { --gutter: 48px; } }

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

html {
  /* clip, not hidden: overflow-x: hidden on the root kills position: sticky everywhere. */
  overflow-x: clip;
  scroll-behavior: smooth;
}
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

body {
  margin: 0;
  background: var(--espresso);
  color: var(--blush);
  font-family: var(--text);
  font-size: var(--f-body);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  overflow-x: clip;
}

img, svg { display: block; max-width: 100%; }
a { color: inherit; }
h1, h2, h3, p, ul, ol, figure, dl { margin: 0; }
ul { padding: 0; list-style: none; }
button { font: inherit; color: inherit; }

/* The ring is an outline, never a box-shadow: a box-shadow ring is specificity 0-1-0 and any
   component that sets its own shadow later in the file silently deletes it. */
:focus-visible {
  outline: 2px solid var(--blush);
  outline-offset: 3px;
}

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

/* The wordmark symbol source. display:none would also work for <use>, but a zero sized clipped
   box is the form every browser agrees on. */
.sprite { position: absolute; width: 0; height: 0; overflow: hidden; }

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

.skip {
  position: fixed;
  z-index: 100;
  top: var(--s2);
  left: var(--s2);
  padding: var(--s3) var(--s4);
  background: var(--burgundy);
  color: var(--white);
  border-radius: var(--radius);
  translate: 0 -200%;
}
.skip:focus { translate: 0 0; }

.label {
  font-size: var(--f-label);
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--dim);
}

/* ---------------------------------------------------------------- living print canvas */
/* top + 100lvh rather than inset: 0. A phone's visible viewport grows when the URL bar hides,
   and an inset-anchored fixed layer resizes on every bar flip, which busts the sprite cache
   and reallocates the backing store mid-scroll. 100lvh is the largest of those states and
   does not move. */
#print {
  position: fixed;
  left: 0;
  top: 0;
  width: 100%;
  height: 100lvh;
  z-index: -1;
  pointer-events: none;
  contain: strict;
}
#print canvas { display: block; width: 100%; height: 100%; }

/* ---------------------------------------------------------------- header */
.site-header {
  position: fixed;
  inset-block-start: 0;
  inset-inline: 0;
  z-index: 40;
  height: var(--header-h);
  padding-block-start: env(safe-area-inset-top);
}
/* The bar is a separate painted layer so it can fade in on opacity alone. No backdrop-filter:
   it costs a full-screen blur pass on every frame of a long scroll. */
.site-header::before {
  content: "";
  position: absolute;
  inset: 0;
  background: rgb(0 0 0 / 0.92);
  border-block-end: 1px solid var(--hair);
  /* Visible by default. The transparent-over-the-hero state is the enhancement, so a visitor
     with no JS gets a header bar and a wordmark rather than an empty strip. */
  opacity: 1;
  transition: opacity 0.35s ease;
}
.has-js .site-header::before { opacity: 0; }
.has-js .site-header.is-stuck::before { opacity: 1; }

.site-header__inner {
  position: relative;
  height: var(--header-h);
  display: flex;
  align-items: center;
  gap: var(--s4);
}

.brand {
  display: flex;
  align-items: center;
  min-height: 44px;
  padding-inline-end: var(--s2);
  opacity: 1;
  transition: opacity 0.35s ease;
}
.has-js .brand { opacity: 0; }
.has-js .site-header.is-stuck .brand { opacity: 1; }
.brand__mark { display: block; width: clamp(72px, 20vw, 104px); color: var(--white); }
.brand__mark svg { width: 100%; height: auto; }
/* Keyboard users must be able to reach the home link even while it is faded out. */
.has-js .brand:focus-visible { opacity: 1; }

.site-nav {
  margin-inline-start: auto;
  display: flex;
  align-items: center;
  gap: 10px;
}
@media (min-width: 560px) { .site-nav { gap: var(--s6); } }

.site-nav a {
  position: relative;
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  font-size: 13px;
  text-transform: uppercase;
  /* 0.14em is the house tracking, but three uppercase links plus the mark plus the icon do not
     fit a 375px bar at it. The bar is the one place the label tracking gives way. */
  letter-spacing: 0.08em;
  text-decoration: none;
  color: var(--blush);
}
@media (min-width: 560px) { .site-nav a { letter-spacing: 0.14em; } }
.site-nav a[aria-current="page"] { color: var(--white); }
.site-nav a[aria-current="page"]::after {
  content: "";
  position: absolute;
  inset-block-end: 8px;
  inset-inline: 0;
  border-block-start: 1px solid var(--burgundy);
}
.site-nav .ig { width: 20px; height: 20px; }

/* ---------------------------------------------------------------- buttons */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s2);
  min-height: 48px;
  padding: var(--s3) var(--s5);
  border: 1px solid transparent;
  border-radius: var(--radius);
  font-size: 0.875rem;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  text-decoration: none;
  cursor: pointer;
  background: none;
}
.btn--primary { background: var(--burgundy); color: var(--white); }
.btn--ghost { border-color: var(--hair-strong); color: var(--blush); }
.btn--dark { background: var(--espresso); color: var(--blush); }
@media (hover: hover) and (pointer: fine) {
  .btn { transition: opacity 0.2s ease, scale 0.2s ease; }
  .btn:hover { opacity: 0.86; scale: 1.015; }
}

/* ---------------------------------------------------------------- hero */
.hero {
  position: relative;
  min-height: 100dvh;
  display: grid;
  /* minmax(0, 1fr), not the implicit auto track: an auto track takes its minimum from the
     item's min-content, so the 92vw wordmark widened the whole column past the viewport and
     the page scrolled sideways by 10px at 375. */
  grid-template-columns: minmax(0, 1fr);
  align-content: center;
  padding-block: calc(var(--header-h) + env(safe-area-inset-top) + var(--s7))
                 calc(env(safe-area-inset-bottom) + var(--s9));
}
.hero__mark {
  position: relative;
  width: min(92vw, 1180px);
  /* The slot keeps its box when the SVG inside it is taken out of the flow, so print.js can
     measure the same rectangle whichever wordmark is currently showing. */
  aspect-ratio: 3417 / 713;
  margin-inline: auto;
  color: var(--white);
}
.hero__mark svg { position: absolute; inset: 0; width: 100%; height: 100%; }
/* The SVG wordmark is what everyone sees first and what stays if the canvas never starts.
   print.js fades it out only once it has a font-measured glyph mask to put in its place. */
.hero__mark-svg { transition: opacity 0.5s ease; }
.print-live .hero__mark-svg { opacity: 0; }
/* And out of the render tree once the crossfade has finished: a full width transparent SVG
   still costs a composited layer on every scroll. */
.print-gone .hero__mark-svg { display: none; }

.hero__body {
  margin-block-start: clamp(var(--s5), 4vw, var(--s7));
  text-align: center;
}
.hero__tagline {
  max-width: 34ch;
  margin-inline: auto;
  font-size: clamp(1.0625rem, 2.4vw, 1.375rem);
  color: var(--blush);
  text-wrap: balance;
}
.hero__cta {
  margin-block-start: var(--s6);
  display: flex;
  flex-wrap: wrap;
  gap: var(--s3);
  justify-content: center;
}
.hero__cue {
  position: absolute;
  inset-inline: 0;
  inset-block-end: calc(env(safe-area-inset-bottom) + var(--s5));
  text-align: center;
}
.hero__cue span {
  display: inline-block;
  width: 1px;
  height: 40px;
  background: linear-gradient(to bottom, var(--hair-strong), transparent);
}
@media (prefers-reduced-motion: reduce) { .hero__cue { display: none; } }
.hero__sentinel {
  position: absolute;
  inset-block-end: 0;
  inset-inline-start: 0;
  width: 1px;
  height: 1px;
}

/* ---------------------------------------------------------------- sections */
.section { position: relative; padding-block: clamp(var(--s8), 11vw, var(--s10)); }
.section__head { display: grid; gap: var(--s3); margin-block-end: var(--s7); }
.section h1,
.section h2 {
  font-family: var(--display);
  font-weight: 400;
  font-size: var(--f-h2);
  line-height: 1.02;
  letter-spacing: -0.01em;
  text-wrap: balance;
}
.section__lede { max-width: 46ch; color: var(--dim); }
.em { color: var(--white); }

/* One blush section per page, no more: it is the page taking a breath, not a second theme. */
.section--blush { background: var(--blush); color: var(--espresso); }
.section--blush .section__lede { color: rgb(34 24 21 / 0.74); }
.section--blush .label { color: rgb(34 24 21 / 0.66); }
.section--blush :focus-visible { outline-color: var(--espresso); }

/* ---------------------------------------------------------------- print band */
.band {
  position: relative;
  height: 28vh;
  min-height: 210px;
  overflow: hidden;
  display: grid;
  place-items: center;
  border-block: 1px solid var(--hair);
}
@media (min-width: 900px) { .band { height: 40vh; } }

.band__layer {
  position: absolute;
  inset: 0;
  background: var(--espresso) url("../img/print-leopard.svg") 0 0 / 360px 360px repeat;
}
/* Base state is the finished wipe. An inactive view() timeline contributes nothing at all,
   so whatever is written here is what a browser without scroll-driven animations renders. */
.band__layer--zebra {
  background-image: url("../img/print-zebra.svg");
  clip-path: inset(0 0 0 0);
}
.band__line {
  position: relative;
  font-family: var(--display);
  font-weight: 300;
  font-size: clamp(1.75rem, 6vw, 4rem);
  line-height: 1;
  color: var(--blush);
  white-space: nowrap;
}

/* ---------------------------------------------------------------- catalog grid */
.chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
  margin-block-end: var(--s6);
}
.chip {
  min-height: 44px;
  padding: var(--s2) var(--s4);
  border: 1px solid var(--hair-strong);
  border-radius: var(--radius);
  background: none;
  font-size: var(--f-label);
  text-transform: uppercase;
  letter-spacing: 0.14em;
  cursor: pointer;
}
/* Colour is never the only signal: the active chip also carries a mark and aria-checked. */
.chip[aria-checked="true"] { background: var(--burgundy); color: var(--white); border-color: var(--burgundy); }
.chip[aria-checked="true"]::before { content: "\2022\00a0"; }

.grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--s4);
}
@media (min-width: 700px) { .grid { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--s5); } }
@media (min-width: 1080px) { .grid { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
.grid[hidden] { display: none; }

.piece { min-width: 0; }
.card {
  position: relative;
  display: grid;
  gap: var(--s3);
  padding-block-end: var(--s3);
  text-decoration: none;
  /* Black, not a blush tint. The living print runs behind the grid, and a card lit to the same
     value as the page it sits on reads as a hole in the page instead of an object on it. */
  background: rgb(0 0 0 / 0.42);
  border: 1px solid var(--hair);
  border-radius: var(--radius);
}
.card__frame {
  position: relative;
  aspect-ratio: 4 / 5;
  overflow: hidden;
  background: var(--black);
}
.card__frame img { width: 100%; height: 100%; object-fit: cover; }
.card__meta { display: grid; gap: 2px; padding-inline: var(--s3); }
.card__name { font-family: var(--display); font-weight: 400; font-size: 1.0625rem; line-height: 1.2; }
.card__cat { font-size: var(--f-label); text-transform: uppercase; letter-spacing: 0.14em; color: var(--dim); }
.card__price {
  justify-self: start;
  margin-block-start: var(--s1);
  padding: 2px var(--s2);
  background: var(--burgundy);
  color: var(--white);
  font-family: var(--display);
  font-size: 0.9375rem;
  border-radius: var(--radius);
}

@media (hover: hover) and (pointer: fine) {
  .card__frame img { transition: scale 0.45s ease; }
  .card:hover .card__frame img { scale: 1.04; }
  /* The frame is a pseudo element so it can fade on opacity. A border cannot be animated. */
  .card::after {
    content: "";
    position: absolute;
    inset: -1px;
    border: 1px solid var(--burgundy);
    border-radius: var(--radius);
    opacity: 0;
    transition: opacity 0.25s ease;
    pointer-events: none;
  }
  .card:hover::after, .card:focus-visible::after { opacity: 1; }
  /* rotate, not transform: the reveal below is a filled animation on transform and would
     overwrite any base transform for the life of the page. */
  .card { transition: rotate 0.3s ease; }
  .card:hover { rotate: -0.5deg; }
}

.grid__empty { color: var(--dim); padding-block: var(--s7); }

/* ---------------------------------------------------------------- scroll thread */
.thread {
  position: absolute;
  inset-block: 0;
  inset-inline-start: 0;
  width: 100%;
  height: 100%;
  z-index: 0;
  pointer-events: none;
}
.thread path {
  fill: none;
  stroke: var(--burgundy);
  stroke-width: 1.5;
  vector-effect: non-scaling-stroke;
  stroke-dasharray: 1;
  stroke-dashoffset: 0;
}
.section, .band { z-index: 1; }

/* ---------------------------------------------------------------- manifesto */
.manifesto { display: grid; gap: var(--s2); }
.manifesto p {
  font-family: var(--display);
  font-weight: 400;
  font-size: var(--f-line);
  line-height: 1.1;
  letter-spacing: -0.01em;
  padding-block: var(--s5);
  max-width: 22ch;
  text-wrap: balance;
}
.manifesto p:nth-child(2) { justify-self: end; text-align: end; }
.manifesto p:nth-child(3) { justify-self: center; text-align: center; }

/* ---------------------------------------------------------------- contact */
.contact__grid { display: grid; gap: var(--s6); }
@media (min-width: 760px) { .contact__grid { grid-template-columns: 1fr 1fr; gap: var(--s8); } }
.contact__actions { display: flex; flex-wrap: wrap; gap: var(--s3); }
.contact dl { display: grid; gap: var(--s4); margin-block-start: var(--s6); }
.contact dt { font-size: var(--f-label); text-transform: uppercase; letter-spacing: 0.14em; color: var(--dim); }
.contact dd { margin: 0; }
.contact a { text-underline-offset: 3px; }

/* ---------------------------------------------------------------- footer */
.site-footer {
  position: relative;
  border-block-start: 1px solid var(--hair);
  padding-block: var(--s8) calc(var(--s6) + env(safe-area-inset-bottom));
  background: var(--black);
}
.footer__grid { display: grid; gap: var(--s7); }
@media (min-width: 760px) { .footer__grid { grid-template-columns: 1.4fr 1fr 1fr 1.3fr; gap: var(--s6); } }
.footer__mark { display: block; width: 132px; color: var(--white); }
.footer__mark svg { width: 100%; height: auto; }
.footer__statement { margin-block-start: var(--s4); max-width: 30ch; color: var(--dim); }
.site-footer h2 {
  font-family: var(--text);
  font-size: var(--f-label);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--dim);
}
.footer__col ul { margin-block-start: var(--s4); display: grid; gap: var(--s1); }
.footer__col a, .footer__order a {
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  min-height: 44px;
  text-decoration: none;
}
.footer__col a:hover, .footer__order a:hover { text-decoration: underline; text-underline-offset: 3px; }
.footer__order { display: grid; gap: var(--s1); margin-block-start: var(--s4); }
.footer__order .ig { width: 18px; height: 18px; }
.footer__note { color: var(--dim); }
.footer__rail {
  margin-block-start: var(--s8);
  padding-block-start: var(--s5);
  border-block-start: 1px solid var(--hair);
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2) var(--s5);
  font-size: var(--f-label);
  letter-spacing: 0.06em;
  color: var(--dim);
}
.footer__rail [data-site-version]::before { content: "v"; }

/* ---------------------------------------------------------------- whatsapp pill */
.wa-pill {
  position: fixed;
  z-index: 45;
  inset-block-end: calc(env(safe-area-inset-bottom) + var(--s5));
  inset-inline-end: calc(env(safe-area-inset-right) + var(--s5));
  width: 56px;
  height: 56px;
  display: grid;
  place-items: center;
  border-radius: 999px;
  background: var(--burgundy);
  color: var(--white);
  box-shadow: var(--shadow);
  opacity: 0;
  scale: 0.6;
  pointer-events: none;
  transition: opacity 0.3s ease, scale 0.3s ease;
}
.wa-pill svg { width: 26px; height: 26px; }
.wa-pill.is-on { opacity: 1; scale: 1; pointer-events: auto; }
.wa-pill.is-off { opacity: 0; scale: 0.6; pointer-events: none; }
/* One pulse on first appearance, then it holds still. A pill that keeps pulsing reads as an
   error state and is the first thing people complain about. */
.wa-pill.is-on.is-new { animation: waPulse 0.9s ease-out 1; }
@keyframes waPulse {
  0% { scale: 1; }
  40% { scale: 1.14; }
  100% { scale: 1; }
}

/* ---------------------------------------------------------------- quick view */
.qv {
  width: min(560px, calc(100vw - 2 * var(--s4)));
  padding: 0;
  border: 1px solid var(--hair-strong);
  border-radius: var(--radius);
  background: var(--espresso);
  color: var(--blush);
  box-shadow: var(--shadow);
}
.qv::backdrop { background: rgb(0 0 0 / 0.72); }
.qv__body { display: grid; gap: var(--s5); padding: var(--s5); }
@media (min-width: 560px) { .qv__body { grid-template-columns: 200px 1fr; align-items: start; } }
.qv__img { aspect-ratio: 4 / 5; overflow: hidden; background: var(--black); }
.qv__img img { width: 100%; height: 100%; object-fit: cover; }
.qv h2 { font-family: var(--display); font-weight: 400; font-size: var(--f-h3); line-height: 1.1; }
.qv__price { font-family: var(--display); font-size: 1.25rem; }
.qv__actions { margin-block-start: var(--s5); display: grid; gap: var(--s2); }
.qv__close {
  position: absolute;
  inset-block-start: var(--s2);
  inset-inline-end: var(--s2);
  width: 44px;
  height: 44px;
  border: 0;
  background: none;
  cursor: pointer;
  font-size: 1.25rem;
  line-height: 1;
}
/* Locking the page behind a modal: overflow alone leaves iOS rubber banding the body. */
html[data-modal], html[data-modal] body { overflow: hidden; overscroll-behavior: none; }
html[data-modal] body { position: fixed; inset-inline: 0; width: 100%; }

/* ---------------------------------------------------------------- reveals */
/* Base state is VISIBLE everywhere. The hidden state lives either in the keyframes of a
   scroll-driven animation (an inactive timeline contributes nothing, so an off-screen or
   unsupported element simply stays visible) or under .io-reveal, a class an inline head
   script adds only when the observer fallback is actually going to run. */
.reveal { opacity: 1; }

.io-reveal .reveal {
  opacity: 0;
  translate: 0 24px;
  rotate: 0.6deg;
  transition: opacity 0.5s ease, translate 0.5s ease, rotate 0.5s ease;
  transition-delay: var(--rd, 0ms);
}
.io-reveal .reveal.in { opacity: 1; translate: none; rotate: none; }
/* Anything already above the fold at load reveals with no transition at all: a 500ms fade on
   content the visitor is already looking at is a flash, not a reveal. */
.io-reveal .reveal.in-now { transition: none; }

@media (prefers-reduced-motion: reduce) {
  .io-reveal .reveal { translate: none; rotate: none; transition: opacity 0.24s linear; }
  .io-reveal .reveal.in-now { transition: none; }
}

@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {
    .reveal {
      animation: revealIn linear both;
      animation-timeline: view();
      /* Ends exactly when the element has finished entering, so anything fully inside the
         viewport is fully revealed. Ranges that run into cover leave elements mid fade at a
         resting scroll position. */
      animation-range: entry calc(var(--i, 0) * 12%) entry 100%;
    }
    @keyframes revealIn {
      from { opacity: 0; translate: 0 24px; rotate: 0.6deg; }
      to { opacity: 1; translate: none; rotate: none; }
    }

    /* Hero: tagline and buttons leave 0.3x faster than the page under them. */
    .hero__body {
      animation: heroLift linear both;
      animation-timeline: scroll(root);
      animation-range: 0 100dvh;
    }
    @keyframes heroLift {
      from { translate: none; opacity: 1; }
      to { translate: 0 -30dvh; opacity: 0.15; }
    }
    .hero__cue span {
      animation: cueOut linear both;
      animation-timeline: scroll(root);
      animation-range: 0 5dvh;
    }
    @keyframes cueOut { from { opacity: 1; } to { opacity: 0; } }

    /* Band: leopard wipes to zebra as the strip crosses the viewport.
       The timeline is NAMED on the band rather than taken with view() on the layers inside it.
       .band is overflow: hidden, which makes it a scroll container, so a view() on a descendant
       measures that descendant against the band's own scrollport: the band never scrolls, so the
       wipe froze at a constant 49.9986% at every scroll position on the page. Named on .band, the
       subject's nearest scroller is the viewport, which is what was meant. */
    .band { view-timeline-name: --band; view-timeline-axis: block; }
    .band__layer--zebra {
      animation: wipe linear both;
      animation-timeline: --band;
      animation-range: cover 12% cover 88%;
    }
    @keyframes wipe {
      from { clip-path: inset(0 100% 0 0); }
      to { clip-path: inset(0 0 0 0); }
    }
    .band__line {
      animation: bandTrack linear both;
      animation-timeline: --band;
      animation-range: cover 0% cover 100%;
    }
    @keyframes bandTrack {
      from { translate: 5vw 0; }
      to { translate: -5vw 0; }
    }

    /* The thread draws itself down the page. pathLength is 1, so the dash maths is unitless. */
    .thread path {
      animation: threadDraw linear both;
      animation-timeline: scroll(root);
      animation-range: 0% 92%;
    }
    @keyframes threadDraw {
      from { stroke-dashoffset: 1; }
      to { stroke-dashoffset: 0; }
    }

    /* Manifesto lines wipe in from alternating sides. */
    .manifesto p {
      animation: wipeFromStart linear both;
      animation-timeline: view();
      animation-range: entry 0% entry 100%;
    }
    @keyframes wipeFromStart {
      from { clip-path: inset(0 100% 0 0); opacity: 0.25; }
      to { clip-path: inset(0 0 0 0); opacity: 1; }
    }
    .manifesto p:nth-child(2) { animation-name: wipeFromEnd; }
    @keyframes wipeFromEnd {
      from { clip-path: inset(0 0 0 100%); opacity: 0.25; }
      to { clip-path: inset(0 0 0 0); opacity: 1; }
    }
  }
}

/* ---------------------------------------------------------------- 404 */
.oops { min-height: 100dvh; display: grid; align-content: center; gap: var(--s5); text-align: center; }
.oops h1 { font-family: var(--display); font-weight: 400; font-size: var(--f-h2); line-height: 1.05; }
.oops__actions { display: flex; flex-wrap: wrap; gap: var(--s3); justify-content: center; }

/* ---------------------------------------------------------------- coming soon */
/* The launch panel. <html class="is-soon" data-soon-until="..."> turns it on. The inline head
   script drops the class once that instant has passed, and soon.js reloads the page when the
   countdown reaches zero, so the site opens by itself. Everything else in <body> leaves the
   render tree while the panel is up: the print canvas is never built behind it (print.js checks
   the class) and the hidden pages cost nothing. To retire it, delete the class and the attribute
   on <html>, the .soon markup and the soon.js tag in both pages, then this section. */
html.is-soon body > :not(.soon, .sprite) { display: none !important; }
html.is-soon body { overflow: hidden; }

.soon {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: grid;
  grid-template-rows: auto 1fr;
  color: var(--espresso);
  background: var(--espresso);
  /* Never scrolls. Every size below is capped against the viewport height as well as the
     width, so a phone held sideways gets a smaller card, not a longer page. */
  overflow: hidden;
}
/* The print itself: the owner's photograph, mirrored once so it repeats sideways with no seam,
   sized to exactly one copy tall so the spots keep their real scale on every screen, and set
   off centre so the mirror line never sits under the eye. */
.soon__bg {
  position: fixed;
  inset: 0;
  z-index: -1;
  background: var(--espresso) url("../img/soon-leopard.webp") 38% top / auto 100% repeat-x;
}
.soon__bg::after {
  content: "";
  position: absolute;
  inset: 0;
  background: radial-gradient(ellipse 75% 65% at 50% 46%, rgb(0 0 0 / 0) 0%, rgb(0 0 0 / 0.34) 100%);
}

.soon__top {
  display: grid;
  place-items: center;
  min-height: min(var(--header-h), 12vh);
  padding: env(safe-area-inset-top) var(--gutter) 0;
  background: rgb(255 255 255 / 0.9);
  -webkit-backdrop-filter: blur(12px);
  backdrop-filter: blur(12px);
  border-block-end: 1px solid rgb(34 24 21 / 0.18);
}
/* Live type at 700, not the 600 weight wordmark symbol: the owner wants it bold. Same tracking
   as the generated mark. */
.soon__brand {
  display: block;
  padding: min(var(--s2), 1vh) var(--s3);
  font-family: var(--display);
  font-weight: 700;
  font-size: min(clamp(1.5rem, 3.4vw, 2.1rem), 6vh);
  line-height: 1;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--burgundy);
}
.soon__brand:focus-visible { outline-color: var(--burgundy); }

.soon__main {
  display: grid;
  place-items: center;
  padding: clamp(10px, 3vh, 64px) var(--gutter) calc(clamp(10px, 3vh, 64px) + env(safe-area-inset-bottom));
}

.soon__card {
  position: relative;
  width: min(100%, 640px);
  padding: min(clamp(28px, 6vw, 56px), 4vh) clamp(16px, 5vw, 48px) min(clamp(30px, 6vw, 56px), 4vh);
  text-align: center;
  /* White glass: the print still reads through it, faintly. Espresso on 84% white over the
     blurred print is 11:1 on the average ground and 8:1 on the darkest spot. */
  background: rgb(255 255 255 / 0.84);
  -webkit-backdrop-filter: blur(22px) saturate(1.1);
  backdrop-filter: blur(22px) saturate(1.1);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  animation: soon-in 0.8s cubic-bezier(0.2, 0.7, 0.2, 1) both;
}
.soon__card::before {
  content: "";
  position: absolute;
  inset: 10px;
  border: 1px solid rgb(34 24 21 / 0.55);
  border-radius: var(--radius);
  pointer-events: none;
}
@keyframes soon-in {
  from { opacity: 0; translate: 0 18px; }
}

/* The three pieces arrive one after the other, 120ms apart, after the glass itself. Hidden
   states live in the keyframes only. */
.soon__logo, .soon__title, .soon__clock { animation: soon-rise 0.7s cubic-bezier(0.2, 0.7, 0.2, 1) both; }
.soon__logo  { animation-delay: 0.25s; }
.soon__title { animation-delay: 0.37s; }
.soon__clock { animation-delay: 0.49s; }
@keyframes soon-rise {
  from { opacity: 0; translate: 0 14px; }
}

.soon__logo {
  position: relative;
  width: min(clamp(120px, 26vw, 176px), 21vh);
  aspect-ratio: 1;
  margin-inline: auto;
}
/* A soft floor under the mark so the extrusion reads as an object sitting on the glass. */
.soon__logo::after {
  content: "";
  position: absolute;
  left: 18%; right: 18%; bottom: 4%;
  height: 12%;
  border-radius: 50%;
  background: radial-gradient(ellipse at center, rgb(0 0 0 / 0.22), rgb(0 0 0 / 0) 70%);
}
.soon__flower {
  position: absolute;
  inset: 12%;
  width: 76%;
  height: 76%;
  color: var(--burgundy);
  transition: opacity 0.5s ease;
}
.soon__gl {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  transition: opacity 0.5s ease;
}
.soon__logo.is-3d .soon__flower { opacity: 0; }
.soon__logo.is-3d .soon__gl { opacity: 1; }

.soon__title {
  margin-block-start: min(var(--s3), 1.5vh);
  font-family: var(--display);
  font-weight: 700;
  font-size: min(clamp(2.5rem, 9vw, 4.75rem), 9.5vh);
  line-height: 1;
  letter-spacing: -0.01em;
  color: var(--espresso);
}

/* One card per unit, so the four columns are equal and the labels never push them apart. The
   card width is a token because the unit takes the same width and lets its label overflow,
   centred, instead of widening the column. */
.soon__clock {
  --soon-card: min(clamp(56px, 17vw, 132px), 18vh);
  display: flex;
  justify-content: center;
  align-items: flex-start;
  gap: clamp(8px, 2.4vw, 22px);
  margin-block-start: min(clamp(24px, 5vw, 40px), 4vh);
}
.soon__unit { display: grid; justify-items: center; gap: min(var(--s3), 1.5vh); width: var(--soon-card); }
/* Split-flap cards. Two fixed halves and, while a value changes, two moving ones: each is a
   half-height box that clips with overflow and holds a full-height glyph box, anchored to the
   top for upper halves and to the bottom for lower ones, so the same pair of glyphs is drawn
   across both. Real boxes, not clip-path, because Safari clips in 2D and a half turning in 3D
   came out with the wrong glyph on it. Espresso surfaces, white glyphs: the only red on the
   page is the mark. */
.soon__d {
  position: relative;
  display: block;
  width: var(--soon-card);
  height: min(clamp(48px, 13.5vw, 100px), 12vh);
  border-radius: 6px;
  background: var(--black);
  color: var(--white);
  font-family: var(--display);
  font-weight: 700;
  font-size: min(clamp(1.75rem, 7.6vw, 4.2rem), 8vh);
  line-height: 1;
  letter-spacing: 0.03em;
  perspective: 420px;
  /* No preserve-3d here and the card clips: with it the turning half joined the clock's own 3D
     context, whose vanishing point sits at the centre of the four cards, so an off centre card
     projected the edge on half as a wedge 22px past its right edge. The halves are direct
     children with one rotateX each, so the local perspective above is all they need. */
  overflow: hidden;
  /* The solid edge under the card is its thickness; the soft one is its shadow on the glass. */
  box-shadow: 0 5px 0 -1px rgb(0 0 0 / 0.62), 0 16px 28px rgb(0 0 0 / 0.3);
}
.soon__d > b {
  position: absolute;
  left: 0;
  width: 100%;
  height: calc(50% - 0.5px);
  overflow: hidden;
  background: linear-gradient(rgb(255 255 255 / 0.12), rgb(255 255 255 / 0) 50%, rgb(0 0 0 / 0.3)) var(--espresso);
  background-size: 100% 200%;
  backface-visibility: hidden;
}
.soon__d-top, .soon__d-ft { top: 0; border-radius: 6px 6px 0 0; background-position: top; transform-origin: 50% 100%; }
.soon__d-bot, .soon__d-fb { bottom: 0; border-radius: 0 0 6px 6px; background-position: bottom; transform-origin: 50% 0; }
.soon__d > b > i {
  position: absolute;
  left: 0;
  width: 100%;
  height: 200%;
  display: grid;
  place-items: center;
  padding-inline-start: 0.03em; /* balances the tracking after the last glyph */
  font-style: normal;
}
.soon__d-top > i, .soon__d-ft > i { top: 0; }
.soon__d-bot > i, .soon__d-fb > i { bottom: 0; }
.soon__d-ft, .soon__d-fb { display: none; }
.soon__d-ft::after, .soon__d-fb::after {
  content: "";
  position: absolute;
  inset: 0;
  background: rgb(0 0 0 / 0.5);
  opacity: 0;
}
.soon__d-fb { transform: rotateX(90deg); }
.soon__d.is-flip .soon__d-ft, .soon__d.is-flip .soon__d-fb { display: block; }
.soon__d.is-flip .soon__d-ft { animation: soon-ft 0.32s ease-in forwards; }
.soon__d.is-flip .soon__d-ft::after { animation: soon-shade-in 0.32s ease-in forwards; }
.soon__d.is-flip .soon__d-fb { animation: soon-fb 0.32s 0.32s ease-out forwards; }
.soon__d.is-flip .soon__d-fb::after { opacity: 1; animation: soon-shade-out 0.32s 0.32s ease-out forwards; }
@keyframes soon-ft { to { transform: rotateX(-90deg); } }
@keyframes soon-fb { to { transform: rotateX(0); } }
@keyframes soon-shade-in { to { opacity: 1; } }
@keyframes soon-shade-out { to { opacity: 0; } }

/* The clock stands on the glass, leaning back a few degrees, so the cards read as objects. */
.soon__clock { perspective: 900px; }
.soon__unit { transform: rotateX(7deg); transform-style: preserve-3d; }

.soon__unit small {
  font-family: var(--text);
  font-weight: 500;
  font-size: var(--f-label);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  white-space: nowrap;
  color: rgb(34 24 21 / 0.85);
}

.soon.is-over { opacity: 0; transition: opacity 0.7s ease; }

@media (max-width: 480px) {
  .soon__unit small { letter-spacing: 0.1em; font-size: 0.64rem; }
}
@media (max-width: 360px) {
  .soon__clock { gap: 6px; }
}
@media (prefers-reduced-motion: reduce) {
  .soon__card { animation: soon-fade 0.24s linear both; }
  .soon__d.is-flip .soon__d-ft, .soon__d.is-flip .soon__d-fb { display: none; }
  .soon__unit { transform: none; }
  .soon__logo, .soon__title, .soon__clock { animation: soon-fade 0.24s linear both; }
  .soon.is-over { transition-duration: 0.24s; }
}
@keyframes soon-fade { from { opacity: 0; } }
/* No backdrop blur means the print shows sharp through the card, so the card goes denser. */
@supports not (backdrop-filter: blur(1px)) {
  .soon__card { background: rgb(255 255 255 / 0.96); }
  .soon__top { background: var(--white); }
}
