/* ═══════════════════════════════════════════════════════════════════════════
   militar — warm memorial
   Paper base, olive and brown accents, camo only as texture.
   Phone-first: this is opened by tapping a phone to an NFC tag.
   ═══════════════════════════════════════════════════════════════════════════ */

/* Self-hosted, latin subset, SIL Open Font License. Embedded in the binary so
   the page makes zero third-party requests and keeps working offline once
   loaded — which also means no font CDN gets a log of who opened this. */
@font-face {
  font-family: 'Fraunces';
  src: url('/fonts/fraunces-latin.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter-latin.woff2') format('woff2');
  font-weight: 400 500;
  font-style: normal;
  font-display: swap;
}
/* Used for the signature only — a real handwriting face, so the closing line
   reads as something someone actually signed rather than as more typography. */
@font-face {
  font-family: 'Caveat';
  src: url('/fonts/caveat-latin.woff2') format('woff2');
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}

:root {
  /* The design commits to the warm paper look in both system themes rather
     than shipping a half-considered dark variant. */
  color-scheme: light;

  --paper:      #F2EADA;
  --sand:       #E4D9BF;
  --sand-deep:  #D3C4A3;
  --ink:        #221E17;
  --ink-soft:   #5A5243;
  --olive:      #4A5A38;
  --moss:       #6E844B;
  --brown:      #6B5439;
  --signal:     #8A2F1E;

  /* A warm transitional serif is what makes this read as memorial rather than
     military; a condensed stencil would fight the whole direction. */
  --serif: 'Fraunces', ui-serif, Iowan Old Style, Palatino Linotype, Georgia, serif;
  --sans:  'Inter', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
  --script: 'Caveat', 'Segoe Script', 'Bradley Hand', cursive;

  /* Reading column. Deliberately modest: at 17px this is ~64 characters per
     line, which is where prose reads best. Widening it a lot on a big screen
     would look "fuller" and read worse. */
  --measure: 34rem;
  /* The roll call is a list, not prose, so it gets its own width and is
     allowed to break out past the reading column on larger screens. */
  --roll-measure: 34rem;
  --radius: 2px;
  --shadow: 0 1px 2px rgba(34,30,23,.06), 0 8px 24px rgba(34,30,23,.08);

  --pad-inline: max(1.25rem, env(safe-area-inset-left));

  /* Motion. Everything animates transform and opacity only — those are the
     two properties the compositor can handle without re-laying-out the page,
     which is what keeps this smooth on an older phone. */
  --ease:      cubic-bezier(.2,.7,.3,1);   /* general purpose, gentle overshoot-free */
  --ease-out:  cubic-bezier(.16,1,.3,1);   /* entrances: fast start, long settle */
  --ease-in:   cubic-bezier(.7,0,.84,0);   /* exits: slow start, quick departure */
  --ease-back: cubic-bezier(.34,1.56,.64,1); /* small pops, slight overshoot */
}

/* Bigger screens get more room. The reading column grows only a little — the
   roll call, which has no such constraint, grows a lot. */
@media (min-width: 64rem) {
  /* The reading column stops growing here, at ~68 characters per line. Past
     that the eye starts losing its place returning to the next line, so the
     extra screen width goes to the roll call instead. */
  :root { --measure: 36rem; --roll-measure: 48rem; }
}
@media (min-width: 90rem) {
  :root { --measure: 36rem; --roll-measure: 56rem; }
}

*, *::before, *::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  min-height: 100dvh;              /* dvh, not vh: the mobile keyboard must not
                                      push the submit button off screen */
  background: var(--paper);
  color: var(--ink);
  font: 400 clamp(1rem, .96rem + .2vw, 1.0625rem)/1.65 var(--sans);
  overflow-x: hidden;              /* nothing may scroll sideways at 320px */
  padding: env(safe-area-inset-top) 0 env(safe-area-inset-bottom);
}

/* ── camo texture ──────────────────────────────────────────────────────────
   Organic blobs in olive/brown/sand at very low opacity, plus a fine grain.
   You should feel warmth and tooth, not see a pattern. */
.texture {
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  opacity: .055;
  background-image:
    url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='560' height='560' viewBox='0 0 560 560'><g fill='none'><path fill='%234A5A38' d='M40 70c40-34 96-18 132 6 30 20 22 60-6 84-34 30-92 34-118 4-24-28-16-70-8-94z'/><path fill='%236B5439' d='M330 30c46-16 104 6 118 46 12 34-18 66-56 78-46 14-100-6-110-44-8-32 12-66 48-80z'/><path fill='%234A5A38' d='M420 260c44 10 78 54 66 96-12 40-64 62-108 50-40-10-68-52-58-92 10-38 58-64 100-54z'/><path fill='%236E844B' d='M120 300c40-8 84 20 90 60 6 38-30 74-72 76-44 2-84-30-84-70 0-38 28-58 66-66z'/><path fill='%236B5439' d='M250 430c38-14 86 4 98 40 10 32-16 64-54 74-40 10-86-10-94-46-8-34 14-56 50-68z'/><path fill='%234A5A38' d='M480 430c30-6 58 16 60 44 2 26-22 48-52 48s-56-20-56-46 20-40 48-46z'/></g></svg>"),
    url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='160' height='160'><filter id='g'><feTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='3'/></filter><rect width='160' height='160' filter='url(%23g)' opacity='.5'/></svg>");
  background-size: 560px 560px, 160px 160px;
  background-blend-mode: multiply;
  /* A very slow drift — one full pass takes two minutes. You should never
     catch it moving; it just stops the paper feeling like a dead flat plane. */
  animation: camo-drift 120s linear infinite;
}
@keyframes camo-drift {
  from { transform: translate3d(0, 0, 0) scale(1.06); }
  50%  { transform: translate3d(-1.5%, 1.2%, 0) scale(1.06); }
  to   { transform: translate3d(0, 0, 0) scale(1.06); }
}

/* ── layout ────────────────────────────────────────────────────────────────*/
#app {
  position: relative;
  z-index: 1;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 2rem var(--pad-inline);
}

/* The reveal is a scrolling document, not a centred card. */
#app.is-revealed {
  justify-content: flex-start;
  padding-top: clamp(2.5rem, 8vh, 5rem);
}

.gate { width: 100%; max-width: var(--measure); text-align: center; }

/* ── type ──────────────────────────────────────────────────────────────────*/
.dateline {
  margin: 0 0 1.25rem;
  font-size: .8125rem;
  letter-spacing: .22em;
  text-transform: uppercase;
  color: var(--ink-soft);
}

.greeting {
  margin: 0 0 2rem;
  font: 400 clamp(1.75rem, 1.3rem + 2.2vw, 2.75rem)/1.15 var(--serif);
  letter-spacing: -.01em;
  text-wrap: balance;
}

/* ── the question card ─────────────────────────────────────────────────────*/
.card {
  background: var(--sand);
  border: 1px solid var(--sand-deep);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: clamp(1.25rem, 1rem + 2vw, 2rem);
  text-align: left;
}

.prompt {
  margin: 0 0 1.25rem;
  font: 400 clamp(1.125rem, 1rem + .7vw, 1.375rem)/1.4 var(--serif);
  text-wrap: pretty;
}

.help {
  margin: 0 0 .75rem;
  font-size: .8125rem;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--ink-soft);
}

/* ── controls ──────────────────────────────────────────────────────────────*/
.field {
  width: 100%;
  min-height: 3rem;                /* ≥44px tap target */
  padding: .7rem .85rem;
  background: var(--paper);
  border: 1px solid var(--sand-deep);
  border-radius: var(--radius);
  color: var(--ink);
  font: inherit;
  accent-color: var(--olive);
}
.field::placeholder { color: #9C907A; }
.field:focus-visible,
.btn:focus-visible,
.option:focus-visible,
.key:focus-visible,
.link:focus-visible,
.lang-toggle:focus-visible {
  outline: 2px solid var(--moss);
  outline-offset: 2px;
}

.btn {
  display: block;
  width: 100%;
  min-height: 3rem;
  margin-top: 1.25rem;
  padding: .8rem 1.25rem;
  background: var(--olive);
  color: #F6F2E6;
  border: 1px solid var(--olive);
  border-radius: var(--radius);
  font: 500 1rem/1 var(--sans);
  letter-spacing: .04em;
  cursor: pointer;
  transition: background .15s var(--ease);
}
.btn:hover:not(:disabled) { background: var(--moss); }
.btn:disabled { opacity: .45; cursor: default; }

.link {
  display: inline-block;
  margin-top: 1.5rem;
  padding: .5rem .25rem;           /* keeps the tap target honest */
  background: none;
  border: 0;
  color: var(--ink-soft);
  font: inherit;
  font-size: .9375rem;
  text-decoration: underline;
  text-underline-offset: .2em;
  text-decoration-color: var(--sand-deep);
  cursor: pointer;
}
.link:hover { color: var(--olive); }

.nudge {
  min-height: 1.5rem;
  margin: 1rem 0 0;
  color: var(--signal);
  font-size: .9375rem;
}
.nudge:empty { margin-top: 0; }

/* ── choice cards ──────────────────────────────────────────────────────────*/
.options { display: grid; gap: .625rem; }

.option {
  display: block;
  width: 100%;
  min-height: 3rem;
  padding: .85rem 1rem;
  background: var(--paper);
  border: 1px solid var(--sand-deep);
  border-radius: var(--radius);
  color: var(--ink);
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition: border-color .15s var(--ease), background .15s var(--ease);
}
.option:hover { border-color: var(--moss); }
.option[aria-pressed="true"] {
  border-color: var(--olive);
  border-width: 2px;
  background: #EDE4CE;
}

/* ── image grid ────────────────────────────────────────────────────────────*/
.options--image {
  grid-template-columns: repeat(2, 1fr);
}
.option--image { padding: .375rem; }
.option--image img {
  display: block;
  width: 100%;
  aspect-ratio: 1;
  object-fit: cover;
  border-radius: 1px;
}

/* ── numeric keypad ────────────────────────────────────────────────────────*/
.cells {
  display: flex;
  justify-content: center;
  gap: .5rem;
  margin-bottom: 1.25rem;
}
.cell {
  width: clamp(2.5rem, 12vw, 3.25rem);
  height: clamp(3rem, 14vw, 3.75rem);
  display: grid;
  place-items: center;
  background: var(--paper);
  border: 1px solid var(--sand-deep);
  border-radius: var(--radius);
  font: 400 clamp(1.375rem, 5vw, 1.75rem)/1 var(--serif);
  font-variant-numeric: tabular-nums;
}
.cell--filled { border-color: var(--olive); }

.keypad {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: .5rem;
}
.key {
  min-height: 3.25rem;
  background: var(--paper);
  border: 1px solid var(--sand-deep);
  border-radius: var(--radius);
  color: var(--ink);
  font: 400 1.375rem/1 var(--serif);
  font-variant-numeric: tabular-nums;
  cursor: pointer;
  transition: background .1s var(--ease);
}
.key:active { background: var(--sand-deep); }
.key--wide { grid-column: span 2; font-family: var(--sans); font-size: .9375rem; }

/* ── date ──────────────────────────────────────────────────────────────────
   The one system control in the design. Browsers permit almost no styling of
   the picker itself; accent-color is respected by modern iOS, Android and
   desktop, and the indicator can be tinted. The opened picker stays the OS's. */
input[type="date"].field {
  min-height: 3.25rem;
  font-family: var(--serif);
  font-size: 1.125rem;
  accent-color: var(--olive);
}
input[type="date"].field::-webkit-calendar-picker-indicator {
  filter: invert(29%) sepia(18%) saturate(900%) hue-rotate(45deg);
  cursor: pointer;
}

/* ── language toggle ───────────────────────────────────────────────────────*/
.lang-toggle {
  position: fixed;
  z-index: 3;
  right: max(1rem, env(safe-area-inset-right));
  bottom: max(1rem, env(safe-area-inset-bottom));
  min-width: 2.75rem;
  min-height: 2.75rem;
  padding: .5rem .75rem;
  background: var(--sand);
  border: 1px solid var(--sand-deep);
  border-radius: var(--radius);
  color: var(--ink-soft);
  font: 500 .8125rem/1 var(--sans);
  letter-spacing: .1em;
  cursor: pointer;
}
.lang-toggle:hover { color: var(--olive); border-color: var(--moss); }

/* ═══ the reveal ═══════════════════════════════════════════════════════════*/
.reveal { width: 100%; max-width: var(--measure); padding-bottom: 6rem; }

.reveal h1 {
  margin: 0 0 .5rem;
  font: 400 clamp(2rem, 1.4rem + 3vw, 3.25rem)/1.1 var(--serif);
  letter-spacing: -.015em;
}
.reveal h2 {
  margin: 2.5rem 0 .75rem;
  font: 400 clamp(1.25rem, 1.1rem + .8vw, 1.5rem)/1.25 var(--serif);
}
.reveal .rule {
  height: 1px;
  margin: 1.5rem 0 2rem;
  background: var(--olive);
  opacity: .35;
}
.reveal p { margin: 0 0 1.25rem; text-wrap: pretty; }
.reveal a { color: var(--olive); }

/* ~~struck-through~~ text. Muted rather than shouted: it reads as an aside
   you decided to leave visible, which is the whole point of striking it out
   instead of deleting it. */
.reveal del {
  color: var(--ink-soft);
  text-decoration-line: line-through;
  text-decoration-color: rgba(107, 84, 57, .6);
  text-decoration-thickness: 1px;
}

/* Pull-quote: a plain Markdown blockquote. */
.reveal blockquote {
  margin: 2rem 0;
  padding-left: 1.25rem;
  border-left: 2px solid var(--olive);
  font: 400 clamp(1.25rem, 1.1rem + 1vw, 1.625rem)/1.35 var(--serif);
  color: var(--olive);
  text-wrap: balance;
}
.reveal blockquote p { margin: 0; }

/* ── inline photos: taped polaroids ────────────────────────────────────────*/
.photo { margin: 2.5rem 0; }

.photo--polaroid {
  /* The tilt lives in a custom property so the scroll-in animation can
     interpolate *to* it instead of fighting it. Setting transform in two
     places would mean the last rule silently wins and the tilt disappears. */
  --tilt: -1.6deg;

  /* Narrower than the column on purpose: a rotated box is wider than its
     unrotated self, and without this slack the tilt clips at phone widths.
     The alternative — dropping the tilt on small screens — would remove the
     scrapbook feel on exactly the devices this site is built for. */
  width: min(100% - 1.5rem, 22rem);
  margin-inline: auto;
  padding: .75rem .75rem 0;
  background: #FBF8F1;
  border: 1px solid var(--sand-deep);
  box-shadow: 0 2px 4px rgba(34,30,23,.08), 0 12px 28px rgba(34,30,23,.10);
  transform: rotate(var(--tilt));
  transition: transform .4s var(--ease), box-shadow .4s var(--ease);
}
/* Alternate the tilt down the page so it reads as a scrapbook, not a glitch. */
.photo--polaroid:nth-of-type(even) { --tilt: 1.4deg; }

/* Straightens up and lifts when you point at it, like picking a photo off a
   table. Hover only — on a phone there is no hover state to trigger it. */
@media (hover: hover) and (pointer: fine) {
  .photo--polaroid:hover {
    transform: rotate(0deg) translateY(-4px) scale(1.015);
    box-shadow: 0 4px 8px rgba(34,30,23,.10), 0 20px 44px rgba(34,30,23,.16);
  }
}

.photo--polaroid img {
  display: block;
  width: 100%;
  height: auto;
  background: var(--sand);
}
.photo--polaroid figcaption {
  padding: .75rem .25rem .9rem;
  font: italic 400 .9375rem/1.4 var(--serif);
  color: var(--ink-soft);
  text-align: center;
}

/* Full-bleed: breaks out of the text column to touch both edges. */
.photo--full {
  width: 100vw;
  margin-inline: calc(50% - 50vw);
}
.photo--full img { display: block; width: 100%; height: auto; }
.photo--full figcaption {
  max-width: var(--measure);
  margin: .75rem auto 0;
  padding-inline: var(--pad-inline);
  font: italic 400 .9375rem/1.4 var(--serif);
  color: var(--ink-soft);
}

/* An image that shares a paragraph with prose stays inline and modest. */
.reveal p img { max-width: 100%; height: auto; vertical-align: middle; }

/* ── personal line ─────────────────────────────────────────────────────────*/
.personal {
  margin: 2.5rem 0;
  padding: 1.5rem;
  background: rgba(74,90,56,.07);
  border: 1px solid rgba(74,90,56,.22);
  border-radius: var(--radius);
  font: italic 400 clamp(1.0625rem, 1rem + .5vw, 1.25rem)/1.5 var(--serif);
  color: var(--olive);
  text-wrap: pretty;
}

/* ── roll call ─────────────────────────────────────────────────────────────*/
.rollcall {
  margin: 0;
  padding: 0;
  list-style: none;
  columns: 1;
}

/* Break the roll call (and its heading) out of the reading column.
   --roll-w resolves to the parent's own width on small screens, so the
   negative margin computes to zero and nothing moves.
   Both selectors are qualified with .reveal so they outrank `.reveal h2`,
   whose margin shorthand would otherwise reset margin-left to 0 and leave the
   heading stranded over the narrower column. */
.reveal .rollcall,
.reveal .rollcall-title {
  --roll-w: min(var(--roll-measure), 100vw - 2 * var(--pad-inline));
  width: var(--roll-w);
  margin-left: calc(50% - var(--roll-w) / 2);
  margin-right: 0;
}
.reveal .rollcall-title { margin-top: 2.5rem; margin-bottom: .75rem; }
.reveal .rollcall { margin-top: 0; margin-bottom: 0; }

@media (min-width: 34rem) { .rollcall { columns: 2; column-gap: 2.5rem; } }
@media (min-width: 52rem) { .rollcall { columns: 3; column-gap: 3rem; } }

.rollcall li {
  display: grid;
  /* Insignia, rank, name. The rank column is fixed so every name starts at
     the same x — a ragged left edge is exactly what makes a roll look untidy.
     ch units track the font, so it stays aligned if you change the type. */
  grid-template-columns: 1.25rem 8.5ch 1fr;
  align-items: center;
  gap: .65rem;
  padding: .45rem 0;
  border-bottom: 1px solid var(--sand-deep);
  break-inside: avoid;
  font-size: .9375rem;
}

/* The official Tenue B patches are 1:2 portrait, so the box has to be too —
   a square box would letterbox them into a stripe of empty space. */
.rank-badge {
  width: 1.25rem;
  aspect-ratio: 1 / 2;
  height: auto;
  object-fit: contain;
  border-radius: 1px;
  /* Held back a little so fifteen patches do not out-shout fifteen names. */
  opacity: .9;
  transition: opacity .2s var(--ease), transform .2s var(--ease);
}
.rollcall li:hover .rank-badge { opacity: 1; transform: scale(1.1); }

/* Soldat and Recrue have no insignia at all; the blank patch still holds the
   column so the names stay aligned. */
.rank-badge--none { display: block; }

.rank {
  color: var(--ink-soft);
  font-size: .8125rem;
  letter-spacing: .03em;
  white-space: nowrap;
}

.who { font-weight: 500; }

/* "Sergent-major chef" and "Adjudant sous-officier" abbreviate to eight-plus
   characters, which is what sizes the column on the narrowest phones. */
@media (max-width: 22rem) {
  .rollcall li { grid-template-columns: 1.1rem 8.5ch 1fr; gap: .5rem; font-size: .875rem; }
  .rank-badge { width: 1.1rem; }
}

/* With the extra width on a computer, the patches and names can both grow —
   at 160px per entry they were only legible, not comfortable. */
@media (min-width: 64rem) {
  .rollcall li {
    grid-template-columns: 1.6rem 9ch 1fr;
    gap: .85rem;
    padding: .6rem 0;
    font-size: 1rem;
  }
  .rank-badge { width: 1.6rem; }
  .rank { font-size: .875rem; }
}

/* ── gallery ───────────────────────────────────────────────────────────────*/
.gallery {
  display: flex;
  gap: .75rem;
  margin: 0 calc(-1 * var(--pad-inline));
  padding: 0 var(--pad-inline) .5rem;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: thin;
}
.gallery figure {
  flex: 0 0 auto;
  width: min(72vw, 18rem);
  margin: 0;
  scroll-snap-align: center;
}
.gallery img {
  display: block;
  width: 100%;
  aspect-ratio: 3/2;
  object-fit: cover;
  background: var(--sand);
  border: 1px solid var(--sand-deep);
}
.gallery figcaption {
  padding-top: .5rem;
  font: italic 400 .875rem/1.4 var(--serif);
  color: var(--ink-soft);
}

/* The closing signature. Centred, with room to breathe on both sides so it
   reads as the end of a letter rather than one more paragraph.
   Qualified with .reveal so it outranks `.reveal p`, which would otherwise win
   on specificity and quietly reimpose its own margin and text-wrap. */
.reveal .signature {
  margin: 0;
  padding: 4.5rem 0 3.5rem;
  text-align: center;
  font: 500 clamp(1.875rem, 1.4rem + 2.2vw, 2.75rem)/1.25 var(--script);
  /* Olive rather than plain ink: the colour of the stamp, so the page opens
     and closes in the same ink. */
  color: var(--olive);
  /* Handwriting sits low in its box; a hair of letter-spacing stops the
     joined strokes from crowding at large sizes. */
  letter-spacing: .01em;
  text-wrap: balance;
}

/* A short rule above it, like the line you sign over. */
.reveal .signature::before {
  content: "";
  display: block;
  width: 3.5rem;
  height: 1px;
  margin: 0 auto 2rem;
  background: var(--sand-deep);
}

/* ═══════════════════════════════════════════════════════════════════════════
   MOTION
   Transform and opacity only. The unlock is still the loudest moment; the
   rest is small feedback that makes the thing feel alive under a thumb.
   Every block here is neutralised by the reduced-motion rules at the bottom.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── entrance: the lock screen assembles itself ────────────────────────────*/
.enter > * {
  opacity: 0;
  animation: enter-rise .55s var(--ease-out) forwards;
}
.enter > *:nth-child(1) { animation-delay: .05s; }
.enter > *:nth-child(2) { animation-delay: .14s; }
.enter > *:nth-child(3) { animation-delay: .24s; }
.enter > *:nth-child(4) { animation-delay: .34s; }
@keyframes enter-rise {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: none; }
}

/* ── wrong answer: a short shake, not a long one ───────────────────────────*/
.card--wrong { animation: shake .4s var(--ease); }
@keyframes shake {
  0%,100% { transform: translateX(0); }
  15% { transform: translateX(-7px); }
  30% { transform: translateX(6px); }
  45% { transform: translateX(-4px); }
  60% { transform: translateX(3px); }
  80% { transform: translateX(-1px); }
}

.nudge { transition: opacity .25s var(--ease); }
.nudge--in { animation: nudge-in .3s var(--ease-out); }
@keyframes nudge-in {
  from { opacity: 0; transform: translateY(-4px); }
  to   { opacity: 1; transform: none; }
}

/* ── question swap on skip ─────────────────────────────────────────────────*/
.card--out { animation: card-out .26s var(--ease-in) forwards; pointer-events: none; }
.card--in  { animation: card-in  .38s var(--ease-out); }
@keyframes card-out {
  to { opacity: 0; transform: translateX(-22px) scale(.97); }
}
@keyframes card-in {
  from { opacity: 0; transform: translateX(26px) scale(.97); }
  to   { opacity: 1; transform: none; }
}

/* ── keypad feedback ───────────────────────────────────────────────────────*/
.key {
  transition: background .12s var(--ease), transform .1s var(--ease);
}
.key:active { transform: scale(.93); }

.cell { transition: border-color .2s var(--ease); }
.cell--pop { animation: cell-pop .28s var(--ease-back); }
@keyframes cell-pop {
  0%   { transform: scale(1); }
  40%  { transform: scale(1.16); }
  100% { transform: scale(1); }
}

/* ── option feedback ───────────────────────────────────────────────────────*/
.option { transition: border-color .15s var(--ease), background .15s var(--ease), transform .12s var(--ease); }
.option:active { transform: scale(.985); }
.option--picked { animation: pick .35s var(--ease-back); }
@keyframes pick {
  0%   { transform: scale(1); }
  45%  { transform: scale(1.03); }
  100% { transform: scale(1); }
}

.btn { transition: background .15s var(--ease), transform .12s var(--ease); }
.btn:active:not(:disabled) { transform: scale(.985); }

/* ── scroll reveal ─────────────────────────────────────────────────────────
   Applied by an IntersectionObserver as each block comes into view. The
   staggered variants let a list of names arrive one after another. */
.rise {
  opacity: 0;
  transform: translateY(22px);
  transition: opacity .7s var(--ease-out), transform .7s var(--ease-out);
  transition-delay: var(--delay, 0s);
}
.rise.is-visible { opacity: 1; transform: none; }

/* Photos settle into their tilt rather than starting there. */
.photo--polaroid.rise { transform: translateY(26px) rotate(0deg) scale(.97); }
.photo--polaroid.rise.is-visible { transform: translateY(0) rotate(var(--tilt)) scale(1); }

.photo--full.rise { transform: translateY(28px) scale(1.02); }
.photo--full.rise.is-visible { transform: none; }

/* ── the unlock ────────────────────────────────────────────────────────────
   Still the loudest moment: the card lifts, falls away, an olive ink stamp
   presses onto the paper, and the message rises underneath it. */
.card--leaving {
  animation: lift-away .55s var(--ease) forwards;
  pointer-events: none;
}
@keyframes lift-away {
  0%   { transform: translateY(0) scale(1);        opacity: 1; }
  30%  { transform: translateY(-8px) scale(1.012); opacity: 1; }
  100% { transform: translateY(28px) scale(.97);   opacity: 0; }
}

.stamp {
  position: absolute;
  top: clamp(1rem, 6vh, 3rem);
  right: max(1rem, env(safe-area-inset-right));
  z-index: 2;
  width: clamp(6rem, 26vw, 9rem);
  pointer-events: none;
  color: var(--olive);
  opacity: 0;
  animation: stamp-press .5s var(--ease) .35s forwards;
}
@keyframes stamp-press {
  0%   { opacity: 0;   transform: rotate(-16deg) scale(1.7); }
  55%  { opacity: .95; transform: rotate(-11deg) scale(.95); }
  72%  { opacity: .9;  transform: rotate(-12.6deg) scale(1.03); }
  88%  { opacity: .85; transform: rotate(-11.7deg) scale(.99); }
  100% { opacity: .82; transform: rotate(-12deg) scale(1); }
}

.fade-up { animation: fade-up .6s var(--ease) .5s backwards; }
@keyframes fade-up {
  from { opacity: 0; transform: translateY(12px); }
  to   { opacity: 1; transform: none; }
}

/* ── reduced motion ────────────────────────────────────────────────────────
   Someone who has asked their device for less movement gets a site that
   simply appears. Content must still be VISIBLE — the trap here is disabling
   an animation whose `from` state was opacity:0, which would leave the page
   permanently blank. Hence the explicit opacity:1 / transform:none. */
@media (prefers-reduced-motion: reduce) {
  .texture { animation: none; }

  .enter > *,
  .card--in,
  .card--wrong,
  .nudge--in,
  .cell--pop,
  .option--picked,
  .fade-up {
    animation: none;
    opacity: 1;
    transform: none;
  }

  .rise,
  .rise.is-visible,
  .photo--polaroid.rise,
  .photo--full.rise {
    opacity: 1;
    transition: none;
  }
  .rise { transform: none; }
  .photo--polaroid.rise,
  .photo--polaroid.rise.is-visible { transform: rotate(var(--tilt)); }
  .photo--full.rise { transform: none; }

  .card--out,
  .card--leaving { animation: none; opacity: 0; }

  .stamp { animation: none; opacity: .82; transform: rotate(-12deg); }

  .key:active,
  .option:active,
  .btn:active:not(:disabled) { transform: none; }

  @media (hover: hover) and (pointer: fine) {
    .photo--polaroid:hover { transform: rotate(var(--tilt)); }
  }

  * { scroll-behavior: auto !important; }
}

/* Only on genuinely tiny screens (≤320px) is there no room for the tilt. */
@media (max-width: 20rem) {
  .photo--polaroid,
  .photo--polaroid:nth-of-type(even) { transform: none; }
}
