/* Instrument Sans is variable (400-700) in one file. Delicious Handrawn is the
   marker on the polaroid captions and is loaded for five short words, so it is
   deliberately not used anywhere else. */
@font-face {
  font-family: "Instrument Sans";
  src: url("/fonts/instrument-sans-latin.woff2") format("woff2");
  font-weight: 400 700;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Delicious Handrawn";
  src: url("/fonts/delicious-handrawn-latin.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

:root {
  /* Neutral, not cream. The previous scale was rgb(247,241,233) — visibly
     yellow at display size against the teal, which is most of what read as
     "muted". These are plain white at descending strength. */
  --ink: #ffffff;
  --ink-soft: rgb(255 255 255 / 0.90);
  --ink-dim: rgb(255 255 255 / 0.74);
  --ink-faint: rgb(255 255 255 / 0.58);
  --field-fallback: radial-gradient(120% 90% at 50% 55%, #0d5f5c 0%, #063a44 42%, #03161b 100%);
  --glass-edge: rgb(255 255 255 / 0.16);

  --sans: "Instrument Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
  --hand: "Delicious Handrawn", "Bradley Hand", "Segoe Script", cursive;
  --mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;

  /* Polaroid paper. The only light surface on the page, so it carries its own
     ink scale — the --ink tokens above are for type on the dark field. */
  --paper: #f6f1e6;
  --paper-edge: #e6dccb;
  --paper-ink: #1a2b2f;
  --paper-ink-dim: rgb(26 43 47 / 0.66);

  --ease: cubic-bezier(0.22, 1, 0.36, 1);
  --flight: 560ms;
}

* { box-sizing: border-box; }

html, body {
  height: 100%;
  margin: 0;
}

body {
  background: #03161b;
  color: var(--ink);
  font-family: var(--sans);
  /* No -webkit-font-smoothing: antialiased here on purpose. It thins light type
     on a dark field noticeably on macOS, which is most of what read as "muted"
     — the colour was already near-white. Default smoothing renders these
     glyphs denser and the contrast problem goes with it. */
  overflow: hidden;
}

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

#field {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  background: var(--field-fallback);
}

/* A soft scrim so the type keeps its contrast no matter where the field
   drifts — the light is always moving behind it. */
.page::before {
  content: "";
  position: fixed;
  inset: 0;
  background: radial-gradient(90% 74% at 50% 40%, rgb(2 14 18 / 0.60) 0%, rgb(2 14 18 / 0.40) 44%, transparent 82%);
  pointer-events: none;
}

/* ---------------------------------------------------------------- shell */

.page {
  position: relative;
  z-index: 1;
  height: 100svh;
  display: flex;
  flex-direction: column;
  padding: clamp(1.15rem, 2.6vh, 2rem) clamp(1.15rem, 3.5vw, 2.75rem);
}

.masthead {
  flex: none;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
}

.wordmark {
  font-size: clamp(1.0625rem, 1.5vw, 1.3125rem);
  letter-spacing: 0.018em;
  color: var(--ink);
  text-decoration: none;
  text-shadow: 0 1px 20px rgb(2 14 18 / 0.5);
}

.nav {
  display: flex;
  gap: clamp(1rem, 2.4vw, 1.75rem);
}

.nav-link,
.contact {
  font-family: var(--mono);
  font-size: clamp(0.625rem, 1vw, 0.75rem);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-dim);
  text-decoration: none;
  text-shadow: 0 1px 16px rgb(2 14 18 / 0.5);
  transition: color 220ms ease;
}

.nav-link:hover,
.nav-link:focus-visible,
.contact:hover,
.contact:focus-visible { color: var(--ink); }

.stage {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: clamp(1.75rem, 5.5vh, 3.5rem);
}

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

/* Width in rem, not ch: `ch` here resolves against this element's inherited
   16px, not the h1's 70-odd, so a ch-based cap is far narrower than it reads
   and broke the headline across two lines. The measure belongs on the
   paragraph, which is actually set at its own size. */
.hero {
  text-align: center;
  max-width: min(92vw, 46rem);
}

.hero h1 {
  margin: 0;
  font-weight: 500;
  font-size: clamp(2.625rem, 7vw, 5.25rem);
  line-height: 1.02;
  letter-spacing: -0.02em;
  text-wrap: balance;
  text-shadow: 0 1px 24px rgb(2 14 18 / 0.5);
}

.hero p {
  margin: clamp(0.6rem, 1.4vh, 1rem) auto 0;
  max-width: 34ch;
  font-size: clamp(1.0625rem, 2.1vw, 1.5rem);
  line-height: 1.4;
  letter-spacing: -0.005em;
  text-wrap: balance;
  color: var(--ink-soft);
  text-shadow: 0 1px 18px rgb(2 14 18 / 0.5);
}

/* ------------------------------------------------------------- polaroid */

.pola {
  display: flex;
  flex-direction: column;
  background: var(--paper);
  color: var(--paper-ink);
  border-radius: 3px;
  box-shadow:
    0 22px 44px -18px rgb(1 10 13 / 0.8),
    0 2px 6px -2px rgb(1 10 13 / 0.5);
}

.pola-photo {
  position: relative;
  display: grid;
  place-items: center;
  aspect-ratio: 1;
  overflow: hidden;
  /* Fallback first: color-mix inside a gradient stop invalidates the whole
     declaration on browsers that don't support it. */
  background: linear-gradient(168deg, #0c333c 0%, #03151a 100%);
  background:
    radial-gradient(115% 95% at 50% 118%, color-mix(in srgb, var(--accent) 62%, transparent) 0%, transparent 64%),
    linear-gradient(168deg, #0c333c 0%, #051c23 62%, #03151a 100%);
}

.sigil {
  width: 42%;
  height: 42%;
  color: var(--accent);
  color: color-mix(in srgb, var(--accent) 88%, #ffffff);
  filter: drop-shadow(0 2px 10px rgb(1 10 13 / 0.55));
}

.pola-caption {
  text-align: center;
}

.pola-name {
  display: block;
  margin: 0;
  font-family: var(--hand);
  font-weight: 400;
  line-height: 1.05;
  letter-spacing: 0.005em;
  color: var(--paper-ink);
}

/* ----------------------------------------------------------------- deck */

.deck {
  flex: none;
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  justify-content: center;
  align-items: flex-start;
  gap: clamp(0.6rem, 1.35vw, 1.15rem);
}

/* The fan is rotation and lift only — never overlap. A polaroid that covers
   its neighbour is hiding the content this page exists to show. */
.deck-item {
  position: relative;
  transform: rotate(var(--rot)) translateY(var(--lift));
  transition: transform 380ms var(--ease);
}

.deck-item:nth-child(1) { --rot: -6deg; --lift: 15px; }
.deck-item:nth-child(2) { --rot: -3deg; --lift: 5px; }
.deck-item:nth-child(3) { --rot: 0deg; --lift: 0px; }
.deck-item:nth-child(4) { --rot: 3deg; --lift: 5px; }
.deck-item:nth-child(5) { --rot: 6deg; --lift: 15px; }

.deck-item:hover,
.deck-item:focus-within { z-index: 10; }

.pola-card {
  width: clamp(9rem, 14.6vw, 13rem);
  padding: 0.55rem 0.55rem 0;
  border: 0;
  font: inherit;
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
  transition:
    transform 380ms var(--ease),
    box-shadow 380ms ease;
}

.pola-card .pola-caption {
  padding: 0.7rem 0.2rem 0.95rem;
}

.pola-card .pola-name {
  font-size: clamp(1.125rem, 1.85vw, 1.625rem);
}

/* The deal is opt-in, added by deck.js. A polaroid's resting state is visible,
   so an animation that never starts — reduced motion, a stalled timeline, no
   JS — costs the flourish and never the content. `backwards` holds the
   from-state, which is opacity: 0. */
.deck.is-dealing .pola-card {
  animation: deal 620ms var(--ease) backwards;
  animation-delay: calc(var(--i) * 75ms + 120ms);
}

@keyframes deal {
  from { opacity: 0; transform: translateY(30px) scale(0.965); }
  to { opacity: 1; transform: none; }
}

.deck-item:hover .pola-card,
.pola-card:focus-visible {
  transform: rotate(calc(var(--rot) * -1)) translateY(-11px);
  box-shadow:
    0 34px 62px -20px rgb(1 10 13 / 0.88),
    0 2px 6px -2px rgb(1 10 13 / 0.5);
}

.pola-card:focus-visible {
  outline: 2px solid rgb(255 255 255 / 0.75);
  outline-offset: 4px;
}

/* The source polaroid is hidden while its twin is flying, so the deck never
   shows the same photo twice. */
.pola-card.is-flying { visibility: hidden; }

/* ------------------------------------------------------------ colophon */

.colophon {
  flex: none;
  display: flex;
  align-items: baseline;
  justify-content: center;
  gap: 1rem;
  flex-wrap: wrap;
}

.entity {
  margin: 0;
  font-family: var(--mono);
  font-size: clamp(0.625rem, 1vw, 0.75rem);
  letter-spacing: 0.08em;
  color: var(--ink-faint);
  text-shadow: 0 1px 16px rgb(2 14 18 / 0.5);
}

/* ------------------------------------------------------------- overlay */

/* The scroll lives here, not on .pola-modal. This element IS the viewport, so
   it cannot clip a flight that starts on a card already on screen — whereas an
   overflow on .pola-modal chopped the polaroid mid-flight, worst on the
   outermost cards, which travel furthest.
   `safe center` because a centred flex child taller than its overflow container
   has its top cut off and unreachable. */
.overlay {
  position: fixed;
  inset: 0;
  z-index: 20;
  display: flex;
  align-items: safe center;
  justify-content: center;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: clamp(1rem, 4vh, 2.5rem) clamp(1rem, 4vw, 2.5rem);
}

.overlay[hidden] { display: none; }

/* Every entrance is gated on .is-entering, which deck.js removes on a TIMER.
   The resting state — no class — is the visible one.

   A running animation that never advances holds its 0% frame, so anything that
   fades opacity in leaves the content invisible outright when frames stop
   coming: a backgrounded tab, a frozen timeline. Fill-mode is not the lever and
   neither is requestAnimationFrame, which simply never fires there. Timers do,
   so the class comes off regardless and the content is handed back whether or
   not the flourish ever played. */
.overlay-backdrop {
  position: absolute;
  inset: 0;
  background: rgb(2 14 18 / 0.62);
  -webkit-backdrop-filter: blur(12px);
  backdrop-filter: blur(12px);
}

.overlay:not([hidden]) .overlay-backdrop { will-change: opacity; }

.overlay.is-entering .overlay-backdrop { animation: fade 300ms ease; }

@keyframes fade { from { opacity: 0; } }

.overlay.is-closing .overlay-backdrop {
  animation: none;
  opacity: 0;
  transition: opacity 280ms ease;
}

.overlay-close {
  position: absolute;
  top: clamp(0.9rem, 3vh, 1.75rem);
  right: clamp(0.9rem, 3vw, 1.75rem);
  z-index: 3;
  width: 2.5rem;
  height: 2.5rem;
  display: grid;
  place-items: center;
  padding: 0;
  border: 1px solid transparent;
  border-radius: 999px;
  background: transparent;
  color: var(--ink-dim);
  cursor: pointer;
  transition: color 220ms ease, border-color 220ms ease;
}

.overlay-close svg { width: 1.25rem; height: 1.25rem; }
.overlay-close:hover { color: var(--ink); border-color: var(--glass-edge); }

.panel[hidden] { display: none; }

/* -------------------------------------------------------- polaroid modal */

/* No overflow here, and nothing that establishes a clipping box. The polaroid
   flies OUT of this element's bounds on its way to and from its card, so any
   overflow value but visible chops it mid-flight — worst on the outermost
   cards, which travel furthest. The polaroid is capped at 46svh wide, so it
   fits the viewport by construction and never needed to scroll. */
.pola-modal {
  position: relative;
  z-index: 2;
  overflow: visible;
}

.pola-modal[hidden] { display: none; }

.pola-lg {
  width: min(23rem, 86vw, 46svh);
  margin: 0 auto;
  padding: 0.85rem 0.85rem 0;
  /* deck.js sets this to map the polaroid onto the card it grew from. */
  transform-origin: 50% 50%;
  backface-visibility: hidden;
}

/* Promoted only while the modal is up. Without it the browser re-rasterises the
   caption text and the photo's gradient at a new scale every frame, which is
   most of the judder on the way out of the card. */
.pola-modal:not([hidden]) .pola-lg { will-change: transform; }

.pola-lg .pola-caption {
  padding: 1rem 0.7rem 1.35rem;
}

.pola-lg .pola-name {
  font-size: clamp(1.5rem, 4.4vw, 2rem);
}

.pola-line {
  margin: 0.45rem 0 0;
  font-family: var(--mono);
  font-size: clamp(0.625rem, 1.5vw, 0.75rem);
  letter-spacing: 0.04em;
  color: var(--paper-ink-dim);
}

.pola-detail {
  margin: 0.85rem 0 0;
  font-size: clamp(0.875rem, 2vw, 1rem);
  line-height: 1.5;
  color: var(--paper-ink-dim);
}

.pola-link {
  display: inline-block;
  margin-top: 1rem;
  font-family: var(--mono);
  font-size: clamp(0.625rem, 1.5vw, 0.75rem);
  letter-spacing: 0.08em;
  color: var(--paper-ink);
  text-decoration-color: rgb(26 43 47 / 0.35);
  text-underline-offset: 0.3em;
}

.pola-link:hover { text-decoration-color: var(--paper-ink); }

/* The caption's detail arrives after the polaroid has finished growing — during
   the flight there is nothing to read anyway. The hold is inside the keyframes
   rather than an animation-delay, because a delay needs `backwards` to hold the
   from-state, and `backwards` is what leaves real copy invisible if the
   animation never starts. */
.overlay.is-entering .pola-lg .pola-line,
.overlay.is-entering .pola-lg .pola-detail,
.overlay.is-entering .pola-lg .pola-link {
  animation: fade-late 880ms ease;
}

/* Hold until the flight has landed (560ms of 880ms = 64%). An earlier version
   started this at 38% of a 520ms run — about 200ms in, dead centre of the
   flight — so three text nodes began painting while the polaroid was still
   scaling, and it read as a stutter half way through. */
@keyframes fade-late {
  0%, 64% { opacity: 0; }
  100% { opacity: 1; }
}

/* ------------------------------------------------------- studio + contact */

.sheet {
  position: relative;
  z-index: 2;
  width: min(38rem, 100%);
  max-height: 86svh;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: clamp(1.75rem, 4vw, 2.75rem);
  border-radius: 1.25rem;
  border: 1px solid var(--glass-edge);
  background: linear-gradient(158deg, rgb(9 34 40 / 0.92), rgb(4 20 26 / 0.94));
  -webkit-backdrop-filter: blur(18px) saturate(1.1);
  backdrop-filter: blur(18px) saturate(1.1);
  box-shadow: 0 40px 90px -30px rgb(1 10 13 / 0.9), inset 0 1px 0 rgb(255 255 255 / 0.13);
}

.overlay.is-entering .sheet { animation: rise 420ms var(--ease); }

.sheet[hidden] { display: none; }

@keyframes rise {
  from { opacity: 0; transform: translateY(16px) scale(0.985); }
  to { opacity: 1; transform: none; }
}

.panel h2 {
  margin: 0 0 clamp(0.85rem, 2vh, 1.25rem);
  font-weight: 400;
  font-size: clamp(1.75rem, 4vw, 2.5rem);
  line-height: 1.1;
  letter-spacing: 0.014em;
}

.panel h3 {
  margin: clamp(1.5rem, 3.5vh, 2rem) 0 clamp(0.6rem, 1.4vh, 0.9rem);
  font-weight: 400;
  font-size: 0.6875rem;
  font-family: var(--mono);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

.sheet .panel p {
  margin: 0 0 clamp(0.75rem, 1.8vh, 1.1rem);
  font-size: clamp(0.9375rem, 1.6vw, 1.0625rem);
  line-height: 1.6;
  color: var(--ink-dim);
}

.sheet .panel .lede {
  font-size: clamp(1.0625rem, 2vw, 1.3125rem);
  line-height: 1.45;
  color: var(--ink);
}

.tenets { margin: 0; }

.tenets > div { margin-bottom: clamp(0.75rem, 1.8vh, 1.1rem); }

.tenets dt {
  font-size: clamp(1rem, 1.7vw, 1.125rem);
  line-height: 1.35;
  color: var(--ink);
}

.tenets dd {
  margin: 0.15rem 0 0;
  font-size: clamp(0.875rem, 1.5vw, 1rem);
  line-height: 1.55;
  color: var(--ink-dim);
}

.panel-foot {
  margin-top: clamp(1.25rem, 3vh, 1.75rem);
  padding-top: clamp(0.9rem, 2vh, 1.25rem);
  border-top: 1px solid rgb(255 255 255 / 0.12);
  font-family: var(--mono);
  font-size: 0.6875rem !important;
  letter-spacing: 0.06em;
  line-height: 1.8 !important;
  color: var(--ink-faint) !important;
}

.sheet .panel a {
  color: var(--ink);
  text-decoration-color: rgb(244 236 226 / 0.35);
  text-underline-offset: 0.22em;
}

.sheet .panel a:hover { text-decoration-color: var(--ink); }

.sheet .panel .mailto {
  font-family: var(--mono);
  font-size: clamp(0.875rem, 1.6vw, 1rem);
  letter-spacing: 0.06em;
}

/* ------------------------------------------------- narrow + short screens */

/* Below 820 the fan becomes a fanned carousel: the polaroids keep their tilt,
   overlap a little, and you swipe through them with scroll-snap. This is the
   one scroll on the site — the page itself still never moves. Overlap is
   allowed here in a way it never is on the desktop fan, because a covered
   caption is one swipe away rather than permanently hidden. */
@media (max-width: 820px) {
  .deck {
    /* --half is half a card. The +0.5rem cancels the item's own negative
       start margin, without which the first polaroid snaps 8px left of
       centre. */
    --half: 4.75rem;
    align-self: stretch;
    flex-wrap: nowrap;
    justify-content: flex-start;
    gap: 0;
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scroll-snap-type: x mandatory;
    scrollbar-width: none;
    -ms-overflow-style: none;
    /* The tilt, the lift and the shadow all sit outside the card's box and
       would be clipped by the scroll container without this. */
    padding-block: 1.5rem 1.75rem;
    padding-inline: calc(50% - var(--half) + 0.5rem);
  }

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

  .deck-item {
    flex: 0 0 auto;
    scroll-snap-align: center;
    margin-inline: -0.5rem;
  }

  .deck-item:nth-child(1) { --rot: -7deg; --lift: 10px; }
  .deck-item:nth-child(2) { --rot: 4deg; --lift: 2px; }
  .deck-item:nth-child(3) { --rot: -3deg; --lift: 9px; }
  .deck-item:nth-child(4) { --rot: 6deg; --lift: 1px; }
  .deck-item:nth-child(5) { --rot: -5deg; --lift: 7px; }

  .pola-card { width: 9.5rem; }
  .pola-card .pola-name { font-size: 1.375rem; }

  /* A tilted polaroid lifting on tap would fight the snap. */
  .deck-item:hover .pola-card { transform: rotate(calc(var(--rot) * -1)); }
}

/* Nothing here may scroll the page. Everything above sizes on vw, which on a
   short, wide viewport (landscape phone) overshoots the height budget and runs
   the deck under the colophon — so the composition re-sizes on vh here. The
   stage still takes any remaining overflow itself; masthead and colophon stay
   put. */
@media (max-height: 660px) and (min-width: 821px) {
  .stage {
    overflow-y: auto;
    /* safe center: centred while it fits, top-aligned once it overflows, so
       the masthead end is never the part that gets clipped. */
    justify-content: safe center;
    gap: clamp(0.9rem, 3.5vh, 2rem);
  }

  .hero h1 { font-size: clamp(1.5rem, 6.5vh, 2.5rem); }
  .hero p { font-size: clamp(0.8125rem, 2.6vh, 1rem); }

  .pola-card { width: clamp(6.75rem, 26vh, 10rem); padding: 0.4rem 0.4rem 0; }
  .pola-card .pola-caption { padding: 0.5rem 0.15rem 0.65rem; }
  .pola-card .pola-name { font-size: clamp(1rem, 3.4vh, 1.375rem); }
}

@media (max-height: 660px) and (max-width: 820px) {
  .stage {
    overflow-y: auto;
    justify-content: safe center;
    gap: clamp(0.9rem, 3vh, 1.75rem);
  }

  .hero h1 { font-size: clamp(1.75rem, 8vh, 2.5rem); }
  .hero p { font-size: clamp(0.9375rem, 3vh, 1.125rem); }
  .deck { --half: 4rem; padding-block: 1rem 1.25rem; }
  .pola-card { width: 8rem; }
  .pola-card .pola-name { font-size: 1.125rem; }
}

@media (prefers-reduced-motion: reduce) {
  .pola-card,
  .overlay-backdrop,
  .sheet,
  .pola-lg,
  .pola-lg .pola-line,
  .pola-lg .pola-detail,
  .pola-lg .pola-link {
    animation: none;
    transition: none;
  }

  .deck-item,
  .nav-link,
  .contact,
  .overlay-close { transition: none; }

  .deck-item:hover .pola-card,
  .pola-card:focus-visible { transform: rotate(calc(var(--rot) * -1)); }
}

/* Short viewports: the polaroid's width is capped on svh so the photo shrinks,
   but the caption is type and does not — which put the whole thing 190px taller
   than a landscape phone. Bring the caption down to match. */
@media (max-height: 620px) {
  .pola-lg .pola-caption { padding: 0.7rem 0.55rem 0.9rem; }
  .pola-lg .pola-name { font-size: 1.375rem; }
  .pola-lg .pola-line { margin-top: 0.3rem; font-size: 0.625rem; }
  .pola-lg .pola-detail { margin-top: 0.6rem; font-size: 0.8125rem; line-height: 1.45; }
  .pola-lg .pola-link { margin-top: 0.7rem; font-size: 0.625rem; }
}
