/* ═══════════════════════════════════════════════════════════
   MARISA SAINT CLAIRE
   ═══════════════════════════════════════════════════════════ */

:root {
  --ink: #f2f0ec;
  --ink-dim: rgba(242, 240, 236, 0.55);
  --ink-faint: rgba(242, 240, 236, 0.22);
  --iridescent: #fff;
  --dot-iridescent: rgba(255, 255, 255, 0.96);
  --bg: #000;

  /* The stage's own axes, not the screen's. Everywhere a size should
     track "across the picture" or "down the picture" it uses these
     rather than vw/vh — on a phone held upright the stage is turned a
     quarter turn, and the two swap over (see "Sideways" below). */
  --vw: 1vw;
  --vh: 1vh;

  --gut: clamp(1.25rem, calc(4 * var(--vw)), 3.5rem);
  --dot: 8.75px;
  --hit: 44px;
  /* How far each dot row's hit box sits in from its edge of the stage.
     Shared with the photo layer, which stops exactly where the dots
     start — change it here and both move together. */
  --dot-inset: clamp(1.1rem, calc(3 * var(--vh)), 2rem);

  /* What's left down the middle once both dot rows are accounted for:
     the strip the photo tiles get to themselves. ~279px on a phone,
     ~758px on a laptop. */
  --band: calc(100 * var(--vh) - 2 * (var(--dot-inset) + var(--hit)) - var(--safe-b));

  --ease: cubic-bezier(0.22, 0.61, 0.36, 1);
  --slow: 900ms;

  --sans: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Helvetica, Arial, sans-serif;
  --serif: 'Times New Roman', Times, Georgia, serif;

  --safe-b: env(safe-area-inset-bottom, 0px);
  --safe-l: env(safe-area-inset-left, 0px);
  --safe-r: env(safe-area-inset-right, 0px);
}

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

html, body {
  margin: 0;
  height: 100%;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--sans);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

body {
  overflow: hidden;               /* home is fixed; inner pages scroll */
  overscroll-behavior: none;
  -webkit-tap-highlight-color: transparent;
  touch-action: none;             /* fixed viewport — kills double-tap zoom */
  -webkit-text-size-adjust: 100%; /* no reflow when the phone is turned */
  text-size-adjust: 100%;
}
/* Inner pages still scroll, so give both axes back — anything other than
   `none` re-enables pinch, which is why home stays at `none`. */
body.is-scrollable { overflow: auto; touch-action: pan-x pan-y; }

button { font: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: pointer; }

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

:focus-visible { outline: 1px solid var(--ink); outline-offset: 6px; }


/* ── Film layer ──────────────────────────────────────────── */

.film {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 100%;
  /* Large viewport — the height with browser chrome collapsed. The film
     keeps covering while Safari's bars animate away, so no black band
     flashes in at the bottom. UI below still positions against the
     *current* viewport, so the dots stay reachable either way. */
  height: 100lvh;
  z-index: 0;
  background: #000;
  /* Stand-in while there is no footage — a slow drift of headlight colour. */
  background-image:
    radial-gradient(120% 90% at 20% 15%, #1b1d24 0%, transparent 60%),
    radial-gradient(100% 80% at 85% 80%, #241c1a 0%, transparent 55%),
    linear-gradient(160deg, #0a0a0c 0%, #000 100%);
}

.film video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 1;
}

.film__grain {
  position: absolute;
  inset: -200%;
  pointer-events: none;
  opacity: 0.045;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)'/%3E%3C/svg%3E");
  animation: grain 700ms steps(4) infinite;
}
@keyframes grain {
  0%   { transform: translate(0, 0); }
  25%  { transform: translate(-3%, 2%); }
  50%  { transform: translate(2%, -3%); }
  75%  { transform: translate(-2%, -2%); }
  100% { transform: translate(0, 0); }
}

/* Blur the film behind reading pages so type stays crisp. */
body.is-inner .film video { filter: blur(14px) saturate(0.6); transform: scale(1.06); }


/* ── Views ───────────────────────────────────────────────── */

#main {
  position: relative;
  z-index: 1;
}

.view[hidden] { display: none; }

.view {
  animation: viewIn 700ms var(--ease) both;
}
@keyframes viewIn {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: none; }
}

.view--home {
  position: fixed;
  inset: 0;
}


@keyframes fadeUp {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: none; }
}


/* ── The eight dots ──────────────────────────────────────── */
/* Four top, four bottom, evenly spaced edge to edge — a comfort-rating
   scale rather than a nav cluster. Colour needs no JS: each row carries
   mix-blend-mode: difference and its dots are painted near-white, so
   they invert against whatever is directly behind them, live and per
   pixel — split a dot across a light/dark edge and the two halves render
   as two colours. The slight translucency lets the moving texture keep
   reading through the mark without washing the result into grey. */

.dots {
  position: absolute;
  left: 0;
  right: 0;
  z-index: 2;
  display: flex;
  justify-content: space-between;
  padding-left: max(var(--gut), var(--safe-l));
  padding-right: max(var(--gut), var(--safe-r));
  pointer-events: none;
  mix-blend-mode: difference;
}
body.is-inner .dots { display: none; }

.dots--top {
  top: var(--dot-inset);
}

.dots--bottom {
  bottom: calc(var(--dot-inset) + var(--safe-b));
}

.dot {
  pointer-events: auto;
  position: relative;
  width: var(--hit);
  height: var(--hit);
  display: grid;
  place-items: center;
  border-radius: 50%;
  transition: transform 400ms var(--ease);
}
.dot--placeholder { pointer-events: none; }

.dot:hover,
.dot:focus-visible { transform: scale(1.35); }

.dot__mark {
  width: var(--dot);
  height: var(--dot);
  border-radius: 50%;
  background: var(--dot-iridescent);
  /* The direct-child row performs the blend so it can see past #main
     to the film; blending the mark inside #main leaves it plain white. */
  mix-blend-mode: normal;
  animation: wiggle 6s var(--ease) infinite;
}

/* Small, slow, per-dot drift — never in lockstep. Negative delays start
   each dot mid-cycle instead of all breathing in from the same frame. */
.dots--top .dot:nth-child(1) .dot__mark { animation-duration: 5.5s; animation-delay: 0s; }
.dots--top .dot:nth-child(2) .dot__mark { animation-duration: 6.2s; animation-delay: -1.2s; }
.dots--top .dot:nth-child(3) .dot__mark { animation-duration: 5.8s; animation-delay: -2.7s; }
.dots--top .dot:nth-child(4) .dot__mark { animation-duration: 6.6s; animation-delay: -0.6s; }
.dots--bottom .dot:nth-child(1) .dot__mark { animation-duration: 6.0s; animation-delay: -3.4s; }
.dots--bottom .dot:nth-child(2) .dot__mark { animation-duration: 5.6s; animation-delay: -1.8s; }
.dots--bottom .dot:nth-child(3) .dot__mark { animation-duration: 6.4s; animation-delay: -4.1s; }
.dots--bottom .dot:nth-child(4) .dot__mark { animation-duration: 5.9s; animation-delay: -2.3s; }

@keyframes wiggle {
  0%, 100% { transform: translate(0, 0)       scale(1); }
  25%      { transform: translate(-1px, 1px)  scale(1.05); }
  50%      { transform: translate(1px, -1px)  scale(0.95); }
  75%      { transform: translate(-1px, -1px) scale(1.03); }
}



/* ── Inner pages ─────────────────────────────────────────── */

.view--page {
  min-height: 100dvh;
  padding: clamp(4.5rem, calc(12 * var(--vh)), 8rem) var(--gut) clamp(6rem, calc(14 * var(--vh)), 9rem);
  padding-left: max(var(--gut), var(--safe-l));
  padding-right: max(var(--gut), var(--safe-r));
}

.page { max-width: 46rem; margin: 0 auto; }
.page--wide { max-width: 76rem; }

.page__heading {
  margin: 0 0 clamp(2rem, 6vh, 3.5rem);
  font-family: var(--serif);
  font-weight: 400;
  font-size: clamp(1rem, 1.5vw, 1.2rem);
  letter-spacing: 0.42em;
  text-transform: uppercase;
}

.page__body p {
  margin: 0 0 1.6em;
  font-family: var(--serif);
  font-size: clamp(1.15rem, 2.2vw, 1.6rem);
  line-height: 1.62;
  letter-spacing: 0.005em;
  text-wrap: pretty;
}
.page__body p:first-child { font-size: clamp(1.35rem, 3vw, 2.1rem); line-height: 1.45; }

.page__sig,
.page__lede {
  margin: clamp(2.5rem, 7vh, 4rem) 0 0;
  font-size: 0.68rem;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: var(--ink-dim);
}
.page__lede { margin: -1.5rem 0 clamp(2rem, 6vh, 3rem); }


/* ── Moments grid ────────────────────────────────────────── */

/* Three across, always — the row of three is the point.
   Add or remove entries in config.js and the rows re-flow. */
.moments {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: clamp(0.5rem, 1.4vw, 1.1rem);
}

.moment {
  position: relative;
  display: block;
  width: 100%;
  aspect-ratio: 3 / 4;
  overflow: hidden;
  background: #0d0d0f;
  text-align: left;
  isolation: isolate;
}

.moment img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: grayscale(1) contrast(1.05);
  transform: scale(1.02);
  transition: transform var(--slow) var(--ease), filter var(--slow) var(--ease), opacity 600ms var(--ease);
}

.moment__n {
  position: absolute;
  z-index: 2;
  top: 0.9rem;
  left: 1rem;
  font-size: 0.6rem;
  letter-spacing: 0.24em;
  color: var(--ink);
  mix-blend-mode: difference;
  transition: opacity 400ms var(--ease);
}

.moment__veil {
  position: absolute;
  inset: 0;
  z-index: 1;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  gap: 0.6rem;
  padding: clamp(1rem, 2vw, 1.6rem);
  background: linear-gradient(to top, rgba(0,0,0,0.88) 0%, rgba(0,0,0,0.55) 45%, rgba(0,0,0,0.1) 100%);
  opacity: 0;
  transition: opacity var(--slow) var(--ease);
}

.moment__title {
  font-family: var(--serif);
  font-size: 0.72rem;
  letter-spacing: 0.3em;
  text-transform: uppercase;
}

.moment__text {
  margin: 0;
  font-size: clamp(0.82rem, 1.05vw, 0.95rem);
  line-height: 1.55;
  color: rgba(242, 240, 236, 0.88);
  transform: translateY(8px);
  transition: transform var(--slow) var(--ease);
  text-wrap: pretty;
}

/* Reveal — hover on desktop, .is-open (tap) on touch. */
@media (hover: hover) and (pointer: fine) {
  .moment:hover img { transform: scale(1.06); filter: grayscale(0.15) contrast(1); }
  .moment:hover .moment__veil { opacity: 1; }
  .moment:hover .moment__text { transform: none; }
  .moment:hover .moment__n { opacity: 0; }
}
.moment.is-open img { transform: scale(1.06); filter: grayscale(0.15) contrast(1); }
.moment.is-open .moment__veil { opacity: 1; }
.moment.is-open .moment__text { transform: none; }
.moment.is-open .moment__n { opacity: 0; }

.moment:focus-visible .moment__veil { opacity: 1; }
.moment:focus-visible .moment__text { transform: none; }

/* Placeholder tone while an image is missing */
.moment.is-empty img { opacity: 0; }
.moment.is-empty { background: repeating-linear-gradient(135deg, #0d0d0f 0 14px, #131316 14px 28px); }


/* ── Back control ────────────────────────────────────────── */

.back {
  position: fixed;
  z-index: 40;
  top: clamp(1.1rem, calc(3 * var(--vh)), 2rem);
  left: max(var(--gut), var(--safe-l));
  display: inline-flex;
  align-items: center;
  gap: 0.7rem;
  font-size: 0.62rem;
  letter-spacing: 0.28em;
  text-transform: uppercase;
  color: var(--ink-dim);
  transition: color 300ms var(--ease);
  animation: fadeUp 700ms var(--ease) both;
}
.back:hover { color: var(--ink); }
.back[hidden] { display: none; }

.back__mark {
  width: 22px;
  height: 1px;
  background: currentColor;
  transition: width 400ms var(--ease);
}
.back:hover .back__mark { width: 32px; }


/* ── Radio ───────────────────────────────────────────────── */

/* Audio still plays — see app.js — the chip itself stays fully
   invisible so the only thing on screen is ever the dots. Station
   name/frequency remain in the DOM as the stream's attribution;
   `display: none` doesn't stop the <audio> element playing. */
.radio { display: none; }

/* Soft pool of shade so the chip stays readable over photography. */
.radio::before {
  content: '';
  position: absolute;
  inset: -1.4rem -1.6rem;
  z-index: -1;
  pointer-events: none;
  background: radial-gradient(closest-side, rgba(0, 0, 0, 0.75), transparent);
}

.radio__toggle {
  display: inline-flex;
  align-items: center;
  gap: 0.65rem;
  min-height: var(--hit);
  padding: 0 0.2rem;
  color: var(--ink-dim);
  transition: color 400ms var(--ease);
}
.radio__toggle:hover { color: var(--ink); }
.radio__toggle[aria-pressed="true"] { color: var(--ink); }

.radio__meta { display: flex; flex-direction: column; gap: 0.18rem; text-align: right; }
.radio__name { font-size: 0.62rem; letter-spacing: 0.26em; text-transform: uppercase; }
.radio__freq { font-size: 0.55rem; letter-spacing: 0.2em; color: var(--ink-faint); }

/* Four bars — flat when muted, dancing when live. */
.radio__bars { display: flex; align-items: flex-end; gap: 2px; height: 13px; }
.radio__bars i {
  display: block;
  width: 2px;
  height: 2px;
  background: currentColor;
  transition: height 300ms var(--ease);
}
.radio__toggle[aria-pressed="true"] .radio__bars i { animation: eq 1s var(--ease) infinite; }
.radio__toggle[aria-pressed="true"] .radio__bars i:nth-child(2) { animation-delay: 0.18s; }
.radio__toggle[aria-pressed="true"] .radio__bars i:nth-child(3) { animation-delay: 0.42s; }
.radio__toggle[aria-pressed="true"] .radio__bars i:nth-child(4) { animation-delay: 0.28s; }
@keyframes eq {
  0%, 100% { height: 2px; }
  50%      { height: 13px; }
}

/* Waiting on the visitor's first gesture to start audio. */
.radio.is-awaiting .radio__toggle { animation: nudge 2.4s var(--ease) infinite; }
@keyframes nudge {
  0%, 100% { opacity: 0.45; }
  50%      { opacity: 1; }
}

/* ── Radio note ──────────────────────────────────────────── */
/* Three seconds of "101.1 Radio Classique" over the radio dot each time
   the sound is flipped, struck through when that flip was a mute. The
   dot is the first in the bottom row, so the note is centred on that
   row's padding edge plus half a hit box, and sits one hit box up.
   Everything is in the stage's own units, never raw vw/vh — it turns
   with the stage on a phone. Near-white through the same difference
   blend as the dots and the statement, so it reads over any footage
   without a plate behind it; kept small enough to stay a whisper. */

.radio-note {
  position: fixed;
  z-index: 46;
  bottom: calc(var(--dot-inset) + var(--safe-b) + var(--hit) + 0.4rem);
  left: calc(max(var(--gut), var(--safe-l)) + var(--hit) / 2);
  transform: translateX(-50%);
  margin: 0;
  padding: 0;
  pointer-events: none;
  white-space: nowrap;
  font-family: 'Shree Devanagari 714', 'Shree Devanagari 71', 'Helvetica Neue', Helvetica, Arial, sans-serif;
  font-weight: 400;
  font-size: clamp(0.58rem, calc(1.1 * var(--vw)), 0.8rem);
  letter-spacing: 0.02em;
  line-height: 1;
  color: var(--iridescent);
  mix-blend-mode: difference;
  opacity: 0;
  transition: opacity 420ms var(--ease);
}
.radio-note.is-visible { opacity: 1; }
.radio-note.is-muted {
  text-decoration: line-through;
  text-decoration-thickness: 1px;
}
body.is-inner .radio-note { display: none; }


/* ── Photo pop-ups ───────────────────────────────────────── */
/* Angela / Events dots. Each click drops the next photo at a shuffled
   position over the film. Photos stack up rather than replacing one
   another; clicking a photo doubles it, clicking it again shrinks it
   back. No drop shadow — the tile sits flat on the footage. */

/* The layer stops short of both dot rows, so a tile placed inside it
   cannot land on a button — the clearance is geometry, not a per-spot
   collision test that can fail. Tiles are `absolute`, not `fixed`, so
   that this inset is what their % positions resolve against; the layer
   itself stays fixed, so on a phone it is the turned stage it hangs
   off. app.js §4 keeps the *doubled* footprint inside this box too. */
.photo-layer {
  position: fixed;
  left: 0;
  right: 0;
  top: calc(var(--dot-inset) + var(--hit));
  bottom: calc(var(--dot-inset) + var(--hit) + var(--safe-b));
  z-index: 45;
  pointer-events: none;
}

/* Tile width tracks the *band*, not the stage's width. Sized off the
   width (the old 17vw ≈ 149px) a tile is over half the height of the
   279px strip a phone leaves between the dot rows, so the six spots in
   app.js §4 get squeezed together and the photos pile onto each other.
   Against the band the same spots land as the reference collage does,
   with no two tiles touching. The 200px ceiling is unchanged, so a
   desktop looks exactly as it did. */
.photo-pop {
  position: absolute;
  transform: translateX(-50%);
  width: clamp(80px, calc(0.32 * var(--band)), 200px);
  pointer-events: auto;
  animation: photoReveal 160ms linear both;
}
.photo-pop.is-large { z-index: 1; }

.photo-pop__inner {
  width: 100%;
  cursor: pointer;
  transform: scale(1);
  transition: transform 420ms var(--ease);
}
.photo-pop.is-large .photo-pop__inner { transform: scale(2); }

.photo-pop img {
  display: block;
  width: 100%;
  height: auto;
}

@keyframes photoReveal {
  from { opacity: 0; }
  to   { opacity: 1; }
}


/* ── Brand statement overlay ─────────────────────────────── */
/* Toggled by the bottom statement dot. Same trick as the dots —
   translucent near-white type with mix-blend-mode: difference inverts
   per pixel against whatever footage sits behind each letter, live.
   No backdrop-filter here: on an area this size the contrast boost
   that keeps a small dot decisive would instead crush the whole block
   into a solid card, which isn't the look. */

.brand-statement {
  position: fixed;
  top: 50%;
  left: 50%;
  width: min(calc(48 * var(--vw)), 46rem);
  z-index: 45;
  transform: translate(-50%, -50%);
  margin: 0;
  padding: 0;
  pointer-events: none;
  font-family: 'Shree Devanagari 714', 'Shree Devanagari 71', 'Helvetica Neue', Helvetica, Arial, sans-serif;
  font-weight: 400;
  font-size: clamp(1rem, calc(2.25 * var(--vw)), 2rem);
  line-height: 1.42;
  letter-spacing: 0.01em;
  color: var(--iridescent);
  mix-blend-mode: difference;
}
.brand-statement[hidden] { display: none; }

/* One line of the spread type: word groups pushed apart to fill the
   block edge to edge. The correspondence overlay below sets its own
   lines the same way, so both live on this rule. */
.brand-statement__line,
.correspondence__line {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  width: 100%;
  white-space: nowrap;
  font-weight: 400;
}

.brand-statement__line + .brand-statement__line {
  margin-top: 0.08em;
}

.brand-statement__line--name {
  font-weight: 700;
}

.brand-statement__name {
  display: block;
  font-weight: 700;
}

.brand-statement__text {
  display: block;
  font-weight: 400;
}


/* ── Correspondence overlay ──────────────────────────────── */
/* Toggled by the bottom email dot. Same block, same spread type and
   the same difference blend as the brand statement above — the email
   dot "expands into text on the screen", it does not open a panel, so
   there is deliberately no plate, no border and no backdrop-filter
   behind it. A filter here would crush the whole rectangle into a
   card, exactly as it does on the statement.

   The one departure: this takes clicks and keystrokes, so the block
   cannot be pointer-events: none outright. It stays transparent to
   the mouse and hands events back only on the field, the enter and
   the write line — everywhere else the dots underneath stay
   reachable. */

.correspondence {
  position: fixed;
  top: 50%;
  left: 50%;
  /* Wider than the statement's 48. Its longest line is four groups;
     the heading here is six, and at the statement's width the words
     run out of room, the spread collapses and they touch — the line
     is `nowrap` by design, so it cannot fall back to two rows. */
  width: min(calc(62 * var(--vw)), 52rem);
  z-index: 45;
  transform: translate(-50%, -50%);
  margin: 0;
  padding: 0;
  pointer-events: none;
  text-align: center;
  font-family: 'Shree Devanagari 714', 'Shree Devanagari 71', 'Helvetica Neue', Helvetica, Arial, sans-serif;
  font-weight: 400;
  font-size: clamp(1rem, calc(2.25 * var(--vw)), 2rem);
  line-height: 1.42;
  letter-spacing: 0.01em;
  color: var(--iridescent);
  mix-blend-mode: difference;
}
.correspondence[hidden] { display: none; }

/* label — field — enter, on one line, sitting on a shared baseline. */
.correspondence__field {
  display: flex;
  align-items: baseline;
  justify-content: center;
  gap: 0.55em;
  margin: 0.7em 0 0;
  white-space: nowrap;
}
.correspondence__field[hidden] { display: none; }

.correspondence__input {
  pointer-events: auto;
  flex: 0 1 10em;
  min-width: 0;
  padding: 0 0.25em 0.1em;
  background: none;
  border: 0;
  border-bottom: 1px solid currentColor;
  border-radius: 0;
  color: inherit;
  caret-color: currentColor;
  font: inherit;
  letter-spacing: inherit;
  text-align: center;
  -webkit-appearance: none;
  appearance: none;
}
/* The thickened rule is the focus state — an outline box would read as
   the panel this overlay is at pains not to be. */
.correspondence__input:focus,
.correspondence__input:focus-visible {
  outline: none;
  border-bottom-width: 2px;
}
.correspondence__input::placeholder { color: inherit; opacity: 0.4; }

/* Chrome paints autofilled fields with a solid background of its own,
   which under a difference blend comes back as a block of colour. The
   absurd transition delay is the only way to keep it from landing. */
.correspondence__input:-webkit-autofill,
.correspondence__input:-webkit-autofill:focus {
  -webkit-text-fill-color: currentColor;
  transition: background-color 9999s ease-in-out 0s;
}

.correspondence__enter { pointer-events: auto; }

/* In flight. Fading the row is the whole feedback — a spinner or a
   "sending…" would be the first piece of interface copy on the site
   that nobody wrote. */
.correspondence.is-sending .correspondence__field { opacity: 0.45; }
.correspondence__input:disabled,
.correspondence__enter:disabled { cursor: default; }

.correspondence__note {
  margin: 0.7em 0 0;
  font-size: 0.62em;
  letter-spacing: 0.06em;
}
.correspondence__note[hidden] { display: none; }

/* The line that still opens the visitor's mail app. */
.correspondence__write {
  pointer-events: auto;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  width: 100%;
  margin-top: 1.15em;
  white-space: nowrap;
}


/* ── Small screens ───────────────────────────────────────── */

@media (max-width: 640px) {
  :root { --dot: 7.5px; }
}

/* Three go to one, never two — a 2 + 1 wrap leaves an orphan. */
@media (max-width: 560px) {
  .moments { grid-template-columns: 1fr; }
}

/* Phone actually turned, lock off — the two dot rows already hug the
   top/bottom edges, so nothing to relocate. */
@media (max-height: 480px) and (orientation: landscape) {
  .view--page { padding-top: 4rem; padding-bottom: 4rem; }
  .moments { grid-template-columns: repeat(3, 1fr); }
}


/* ── Sideways (phone held upright) ───────────────────────── */
/* Nobody is ever asked to rotate. A phone in portrait gets the film
   laid across the screen the long way instead: `.stage` is built as a
   landscape rectangle — as wide as the screen is tall — and given a
   quarter turn clockwise to fit. Turn the phone (or don't, with the
   rotation lock on) and it reads as widescreen either way.
   Rotate the phone with the lock *off* and the viewport becomes
   landscape, this block stops matching, and the stage sits flat at the
   same proportions — the picture never changes shape, only what the
   browser had to do to get there.
   Outside this block `.stage` carries no styles at all: it is a plain
   wrapper, and the layers inside it stay fixed to the viewport as
   before. Here its transform makes it their containing block, so
   `position: fixed` resolves against the stage and the whole interface
   turns as one piece — which is also why the film has to drop 100lvh
   for the stage's own height.
   Last block in the file on purpose: it has to outrank the small-screen
   rules above, which read the *screen's* width and would otherwise
   narrow a stage that is in fact the wide way round. */

@media (orientation: portrait) and (max-width: 500px) {
  :root {
    /* Across the picture is now down the screen, and vice versa. */
    --vw: 1dvh;
    --vh: 1vw;

    /* Insets follow the turn too: the stage's left edge is the top of
       the screen (the notch), its right edge the bottom (the home
       indicator), and its bottom edge the screen's left. */
    --safe-l: env(safe-area-inset-top, 0px);
    --safe-r: env(safe-area-inset-bottom, 0px);
    --safe-b: env(safe-area-inset-left, 0px);
  }

  .stage {
    position: fixed;
    top: 50%;
    left: 50%;
    width: 100dvh;
    height: 100vw;
    transform: translate(-50%, -50%) rotate(90deg);
    transform-origin: 50% 50%;
    /* Deliberately not `hidden` — the film below hangs past the stage
       on purpose, and clipping it is what lets black through. */
    overflow: visible;
  }

  /* Inner pages are taller than the stage — they scroll inside it,
     since the body itself no longer moves. */
  body.is-scrollable .stage { overflow-y: auto; overflow-x: hidden; }

  /* The stage measures the viewport as it is *right now* (100dvh), so
     the dots land inside Safari's chrome instead of under it. That
     leaves it short of the glass: on an iPhone the status bar strip is
     outside it, and shows through as a black band. So the film alone
     opts out and takes the largest height the viewport can ever be,
     centred on the stage — it runs past both ends, under the status bar
     and the toolbar, and there is nothing left for black to come
     through. The other axis already spans the full screen width
     (`height: 100%` of a stage that is exactly 100vw across). */
  .film {
    left: 50%;
    right: auto;
    width: 100lvh;
    margin-left: -50lvh;
    height: 100%;
  }
  .view--page { min-height: 100%; }
  .moments { grid-template-columns: repeat(3, 1fr); }
}


/* ── Motion & data preferences ───────────────────────────── */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
  .film__grain { display: none; }
}
