/* ═══════════════════════════════════════════════════════════
   Yağmur & Barbaros · Düğün Davetiyesi
   One page, no dependencies, no build step. Only transform and
   opacity are animated.

   The whole thing is drawn from one photograph the couple sent:
   a wedding suite lying on pale sage toile wallpaper — a deep
   oxblood envelope, a deckled cotton invitation, a gold wax seal.
   The screen is that table: the envelope opens and the invitation
   is drawn out from between its two leaves.

   Rules that are not negotiable:
   - Easing is --ease. --ease-in only for the first half of a page
     turn, --ease-fall only for the envelope leaving frame.
     ease-in-out is banned.
   - Multi-stage keyframes need a stop about every 10 %: --ease is
     extremely front-loaded and CSS applies it to every keyframe
     interval separately.
   - Everything printed on the card is sized in cqw/cqh, never
     vw/vh. .letter__paper is the size container.
   - Neither face scrolls. Content fits or it is cut off.
   - Turkish is never uppercased by CSS — İ would become I.
   ═══════════════════════════════════════════════════════════ */

:root {
  /* Not a decorative colour — it is what the browser paints in the
     strips it keeps for itself. Outside the page's own viewport
     (the status bar on a phone, the bar the in-app browser draws
     over the bottom) nothing of ours reaches: not .wall, which is
     fixed to the viewport, and not the mural, which is an image and
     never propagates to the canvas. Only the body's background
     COLOUR does, and that colour was the mural's flat average —
     greener and bluer than either end of the painting, which is why
     a band showed above and below it on a phone.
     Measured off the rendered page at 393x852: the top three pixels
     are #908262 and the bottom three #867A5A. This is between them,
     within five units of each, and it is the pre-paint colour too. */
  --ground:     #8B7E5E;
  --ground-2:   #BAC0B4;

  /* Chrome laid straight over the mural. The wall is darker than
     the card on purpose — the paper is the lit thing — so every
     label outside the card has to be light, not ink. */
  --chrome:      rgba(247, 243, 232, .88);
  --chrome-soft: rgba(247, 243, 232, .74);
  --chrome-sh:   0 1px 3px rgba(22, 26, 16, .5), 0 0 12px rgba(22, 26, 16, .28);
  --oxblood:    #5A1E24;   /* the envelope */
  --oxblood-in: #3E1116;
  --oxblood-bk: #38121A;
  --card:       #F2EBDC;   /* handmade cotton, never white */
  --card-warm:  #E8DFCC;
  --ink:        #33291F;
  --ink-soft:   #6A5C4E;
  --ink-faint:  #9C8D7C;
  --sepia:      #6B5340;
  --sage:       #7E8566;
  --sage-pale:  #A8AE95;
  --gold:       #B58E4A;
  --gold-pale:  #D8BC85;

  --serif: 'Cormorant Garamond', 'Iowan Old Style', Palatino, Georgia, serif;
  --caps:  'Cormorant SC', 'Cormorant Garamond', Georgia, serif;
  --hand:  'Great Vibes', 'Snell Roundhand', cursive;

  --ease:      cubic-bezier(0.32, 0.72, 0, 1);
  --ease-in:   cubic-bezier(1, 0, 0.68, 0.28);
  --ease-fall: cubic-bezier(0.33, 0, 0.67, 0.33);

  --pad: clamp(16px, 4.5vw, 34px);

  /* Two zoom factors, and every size in this file that is meant to
     follow them says so by multiplying. They exist because the
     scene and the page's own labels were drawn to different
     scales: at a 700 px envelope the masthead and the countdown
     were nine-pixel type, small enough to read as a browser's own
     furniture rather than part of the invitation.

     --ui   the labels around the scene: DÜĞÜN DAVETİYESİ, the
            prompt under the envelope, the countdown.
     --zoom the invitation itself: envelope, card and everything
            printed on it.

     They multiply a clamp() rather than sit inside one, so the
     whole ramp moves together — floor, slope and ceiling — and a
     phone that was pinned to a floor is still pinned to the same
     floor, 110 % of it. */
  --ui:   1.75;
  --zoom: 1.10;

  /* The countdown's own height, written out of the three type
     ramps that build it — label line and its margin, the numeral,
     the unit label and its margin. It is needed twice, and both
     times as a number rather than as something measured after the
     fact: the footbar reserves it before the countdown exists, and
     the envelope's size is what is left of the window once it is
     taken out. */
  --cd-h: calc((clamp(8px,   2.1vw, 9.5px) * 2.2
              + clamp(16px,  4.6vw, 22px)  * 1.1
              + clamp(6.5px, 1.7vw, 7.5px) * 1.98) * var(--ui));

  /* The band under the scene. Its own ramp is a share of the
     window's height; the countdown's is a share of its width. On a
     wide, short window the second wins, which is why this is a
     max() and not just the band. */
  --foot-h: max(calc(clamp(54px, 11vh, 86px) * var(--ui)), var(--cd-h));

  /* Paper fibre: one high-frequency layer for the fibre itself,
     one very low one for the pulp waving under it. A repeating
     gradient reads as tulle, not paper.

     The pulp layers carry `viewBox` + `preserveAspectRatio='none'`
     so they can be stretched over whatever they are painted on.
     Every rule that uses one gives it `background-size: 100% 100%`
     — see the note there. The fibre layers keep their own pixel
     size and tile, which is invisible at a feature size of about
     one pixel. */
  --fibre:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='150' height='150'%3E%3Cfilter id='a'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.95' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='matrix' values='0 0 0 0 0.34 0 0 0 0 0.27 0 0 0 0 0.18 0 0 0 0.8 0'/%3E%3C/filter%3E%3Crect width='150' height='150' filter='url(%23a)' opacity='0.5'/%3E%3C/svg%3E"),
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='130' height='130'%3E%3Cfilter id='b'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.75' numOctaves='2' seed='7' stitchTiles='stitch'/%3E%3CfeColorMatrix type='matrix' values='0 0 0 0 1 0 0 0 0 0.99 0 0 0 0 0.95 0 0 0 0.55 0'/%3E%3C/filter%3E%3Crect width='130' height='130' filter='url(%23b)' opacity='0.38'/%3E%3C/svg%3E"),
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='420' height='420' viewBox='0 0 420 420' preserveAspectRatio='none'%3E%3Cfilter id='c'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.035' numOctaves='4' seed='3'/%3E%3CfeColorMatrix type='matrix' values='0 0 0 0 0.4 0 0 0 0 0.33 0 0 0 0 0.22 0 0 0 0.45 0'/%3E%3C/filter%3E%3Crect width='420' height='420' filter='url(%23c)' opacity='0.22'/%3E%3C/svg%3E");

  /* Dyed stock. The cream fibre above carries a near-white octave
     that turns oxblood to plum, so dark pieces get their own. */
  --fibre-dark:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='150' height='150'%3E%3Cfilter id='a'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='matrix' values='0 0 0 0 0.05 0 0 0 0 0.02 0 0 0 0 0.03 0 0 0 0.5 0'/%3E%3C/filter%3E%3Crect width='150' height='150' filter='url(%23a)' opacity='0.5'/%3E%3C/svg%3E"),
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='300' height='300' viewBox='0 0 300 300' preserveAspectRatio='none'%3E%3Cfilter id='b'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.055' numOctaves='4' seed='9'/%3E%3CfeColorMatrix type='matrix' values='0 0 0 0 0.62 0 0 0 0 0.42 0 0 0 0 0.4 0 0 0 0.3 0'/%3E%3C/filter%3E%3Crect width='300' height='300' filter='url(%23b)' opacity='0.35'/%3E%3C/svg%3E");

  /* The wallpaper. Small sprigs on a 168px tile, drawn at three
     angles so the repeat does not read as a grid. */
  /* Two cuts of the same wall, painted as a pair. Neither is
     tiled: each is composed for its own shape and covers, so
     there is no join to hide anywhere. */
  --duvar: url('assets/duvar.webp');
  --duvar-dik: url('assets/duvar-dik.webp');
  --toile: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='168' height='168'%3E%3Cg fill='none' stroke='%238A9179' stroke-width='1.5' stroke-linecap='round' opacity='0.34'%3E%3Cpath d='M22 30c8-6 14-14 16-24M22 30c-2-7 0-13 3-17M22 30c6 1 12-1 16-4M30 18c-1-5 0-9 2-12M31 24c5 0 9-2 12-5'/%3E%3Cpath d='M96 24c-9 4-16 11-20 20M96 24c4 5 5 11 4 16M96 24c-6-2-12-2-16-1M86 34c-4-3-8-4-12-4M89 40c1 5 0 9-2 12'/%3E%3Cpath d='M138 62c2 10 8 18 17 23M138 62c-5 3-8 8-9 13M138 62c1-6 4-11 7-14M129 75c-5-1-9 0-12 2M132 82c3 4 4 8 4 12'/%3E%3Cpath d='M44 96c9-3 15-10 18-19M44 96c-4-4-6-10-6-15M44 96c6 3 12 4 17 3M55 87c4 3 9 4 13 3M52 80c-2-5-2-9-1-13'/%3E%3Cpath d='M108 112c-3 10-1 19 4 27M108 112c-6 1-11 4-14 8M108 112c4-4 9-7 14-8M97 124c-5 2-8 5-10 9M104 129c0 5-2 9-5 12'/%3E%3Cpath d='M20 140c10 1 18-2 25-9M20 140c1 6 4 11 8 14M20 140c-3-5-4-11-3-16M32 134c-1-5 0-9 3-13M39 137c5 0 9 2 12 5'/%3E%3C/g%3E%3C/svg%3E");
}

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

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

body {
  /* Only the ground colour, so nothing flashes before the mural
     paints. The painting itself is on .wall — see below. */
  background: var(--ground);
  color: var(--ink);
  font-family: var(--serif);
  font-size: 16px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  -webkit-text-size-adjust: 100%;
  overflow: hidden;
  overscroll-behavior: none;
}

/* ── The wall ───────────────────────────────────────────────
   A fixed layer, not the body's background. On the body it is
   painted into the root element's box, and inside an in-app
   browser on a phone that box is the small viewport: the strip
   under the status bar and the strip over the toolbar fell
   outside the covered area and came out as flat --ground, with
   the painting starting below them. Fixed and inset: 0 is the
   layout viewport, which with viewport-fit=cover is the whole
   screen — and which does not shrink when the browser's own bars
   slide away. */
.wall {
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background-color: var(--ground);
  background-image:
    /* Top and bottom are drawn down, not lifted. The mural is the
       lit thing and the frame goes to shadow — that shadow is what
       carries the chrome type, whatever the painting happens to be
       doing behind it. The middle third is left alone: the card
       lands there. */
    radial-gradient(126% 58% at 50% -10%, rgba(24,28,17,.44) 0%, rgba(24,28,17,0) 64%),
    radial-gradient(118% 56% at 50% 110%, rgba(24,28,17,.48) 0%, rgba(24,28,17,0) 66%),
    var(--duvar);
  background-size: auto, auto, cover;
  background-repeat: no-repeat;
  background-position: center;
}

/* Portrait screens get the painting composed for them. Covering the
   wide cut into a tall frame would keep a narrow column of its
   middle and throw both stands of trees away. Only the image
   changes — everything else about the wall is the same. */
@media (max-aspect-ratio: 1 / 1) {
  .wall {
    background-image:
      radial-gradient(126% 58% at 50% -10%, rgba(24,28,17,.44) 0%, rgba(24,28,17,0) 64%),
      radial-gradient(118% 56% at 50% 110%, rgba(24,28,17,.48) 0%, rgba(24,28,17,0) 66%),
      var(--duvar-dik);
  }
}

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

/* ── Depth ──────────────────────────────────────────────────
   One ladder for the whole page, and every z-index in this file
   belongs to it:

     100  grain      film over the finished image
      20  stage      the table — envelope, wax, card
      10  chrome     masthead, hint, countdown
      -1  wall       the mural, edge to edge of the screen

   The scene is above the labels because it is the subject: a
   sheet of paper must never be printed over by a caption. The
   stage isolates so the ordering *inside* it — the flap folding
   behind the card at z-index -2 — stays inside it. Without that
   the -2 escaped to .shell and the opened flap swung up behind
   DÜĞÜN DAVETİYESİ while the card that came out of the same
   envelope stayed in front of it: two halves of one object on
   opposite sides of the same line of type. */

/* Fixed and pointer-transparent — on a scrolling box it costs frames. */
.grain {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 100;
  opacity: .06;
  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='.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* ── Frame ──────────────────────────────────────────────── */
.shell {
  position: relative;
  z-index: 1;
  height: 100dvh;
  display: grid;
  grid-template-rows: auto minmax(0, 1fr) auto;
  padding: max(12px, env(safe-area-inset-top)) var(--pad)
           max(14px, env(safe-area-inset-bottom));
  gap: clamp(8px, 2vh, 20px);
}

.masthead {
  position: relative;
  z-index: 10;
  text-align: center;
  transition: opacity 700ms var(--ease) 380ms;
}
.masthead__kicker {
  margin: 0;
  font-family: var(--caps);
  font-size: calc(clamp(9px, 2.4vw, 11px) * var(--ui));
  letter-spacing: .38em;
  text-indent: .38em;
  line-height: 1.6;
  color: var(--chrome-soft);
  text-shadow: var(--chrome-sh);
}
body.is-open .masthead { opacity: .55; }

/* The band is reserved, not grown into. The prompt and the
   countdown share one grid cell and swap two seconds apart; if the
   taller of the two set the height, the card that has just been
   laid on the table would be nudged when the countdown arrived.
   min-height has to stay above the countdown at every width. */
.footbar {
  position: relative;
  z-index: 10;
  min-height: var(--foot-h);
  display: grid;
  place-items: center;
  text-align: center;
}
.footbar > * { grid-area: 1 / 1; }

.hint {
  margin: 0;
  font-family: var(--caps);
  /* One line, always. It is a single instruction and it reads as
     one; broken after MÜHRE it leaves DOKUNUN sitting alone under
     the envelope. Twenty-four tracked capitals are wider than a
     phone at the full --ui, so the window has the last word — the
     same guard the countdown's gap carries.

     3.8vw is measured, not guessed: set solid, the line is 23.3
     times its own font size, and the narrowest phone in the list
     leaves 288 px between the shell's padding. That puts the
     ceiling at 12.36 px there, and 3.8vw asks for 12.16. */
  font-size: min(calc(clamp(9px, 2.5vw, 11px) * var(--ui)), 3.8vw);
  letter-spacing: .34em;
  text-indent: .34em;
  line-height: 1.6;
  color: var(--chrome);
  text-shadow: var(--chrome-sh);
  transition: opacity 420ms var(--ease), transform 420ms var(--ease);
}
body.is-open .hint { opacity: 0; transform: translateY(6px); pointer-events: none; }

.countdown {
  opacity: 0;
  transform: translateY(8px);
  transition: opacity 700ms var(--ease) 2300ms, transform 700ms var(--ease) 2300ms;
}
body.is-open .countdown { opacity: 1; transform: none; }
/* In flow from the first paint, not display:none. It is revealed
   two seconds after the envelope goes, and if it were out of flow
   until then the footbar would grow at that moment — under a card
   whose width is a share of what is left of the window, so the
   invitation would be nudged and resized while it lay on the
   table. Invisible, but its space is already spoken for. */
.countdown[hidden] { display: block; visibility: hidden; }
.countdown__label {
  margin: 0 0 .6em;
  font-family: var(--caps);
  font-size: calc(clamp(8px, 2.1vw, 9.5px) * var(--ui));
  letter-spacing: .38em;
  text-indent: .38em;
  line-height: 1.6;
  color: var(--chrome-soft);
  text-shadow: var(--chrome-sh);
}
.countdown__grid {
  display: flex;
  justify-content: center;
  /* The gap follows --ui like everything else here, but four
     tracked labels at 175 % already fill a 320 px screen, so it is
     also held to a share of the window. On a phone the window term
     is what applies; on a desktop it never gets close. */
  gap: min(calc(clamp(15px, 5vw, 32px) * var(--ui)), 5.5vw);
}
.unit { display: block; text-align: center; }
.unit b {
  display: block;
  font-size: calc(clamp(16px, 4.6vw, 22px) * var(--ui));
  font-weight: 400;
  line-height: 1.1;
  font-variant-numeric: tabular-nums;
  color: var(--chrome);
  text-shadow: var(--chrome-sh);
}
.unit i {
  display: block;
  margin-top: .45em;
  font-family: var(--caps);
  font-style: normal;
  font-size: calc(clamp(6.5px, 1.7vw, 7.5px) * var(--ui));
  letter-spacing: .28em;
  text-indent: .28em;
  color: var(--chrome-soft);
  text-shadow: var(--chrome-sh);
}

/* ═══════════════════════════════════════════════════════════
   STAGE — the table. Every layer shares one grid cell.
   ═══════════════════════════════════════════════════════════ */
.stage {
  display: grid;
  place-items: center;
  min-height: 0;
  /* Both, and min-width is not decoration. A grid item's automatic
     minimum size is its content's min-content width, and the
     envelope is wider than the shell's padding box — so the column
     could not shrink to the shell, the stage overflowed it to the
     right, and the page ran 10 px past the screen with the wall
     showing 19 px on the left and nothing on the right.

     It also caused the jolt at the end of the opening: at 2050 ms
     the three envelope layers are taken out of the DOM, the widest
     thing in the column goes with them, and everything still on the
     stage was laid out again — the card, mid-landing, jumped
     sideways. Nothing about that is animated; it is the column
     resizing. */
  min-width: 0;
  /* And the cell has to be the screen, not the screen less the
     shell's padding. That padding is for the two labels; the scene
     is measured against the window. An item wider than its cell is
     aligned to the start rather than centred — browsers refuse to
     centre an overflowing box, on the grounds that it would cut
     both ends instead of one — so with the padding in the way the
     envelope sat 17 px from the left and ran off the right. */
  margin-inline: calc(var(--pad) * -1);
  /* See the depth ladder above. isolate is the whole point: the
     scene's own ordering must not be able to reach the page. */
  position: relative;
  z-index: 20;
  isolation: isolate;
  /* Every measure inside the envelope is in em and the envelope's
     font-size comes from its width, so one variable scales the
     whole composition. dvh reads 0 mid-resize in some browsers —
     hence the max() floor.

     There is no fixed ceiling on purpose. The scene used to stop
     at 400 px and the desktop tiers took it to 520, which left the
     card holding a quarter of a 1440-wide window — and since every
     measure printed on the card is a fraction of the card's width,
     a small card is small type. What is left of the window after
     the masthead and the footer is the only real limit. */
  /* The envelope's shape, and how much of its width the card takes.
     They belong together: the two aspects are not the same — the
     card is squarer than the envelope — so the gap the eye reads
     round the paper is not one number but two, and tightening one
     without the other closes the wrong side.

     At .70 / .92 the envelope stood 62 px wider than the card and
     15 px taller, at a 770 px envelope. Vertically it was already
     as snug as it can be with the card rotated inside it, so the
     narrowing is all horizontal: .945 takes the side gap from 31 px
     to 20 px, and the aspect goes up in step so the envelope keeps
     the height it had rather than losing it along with the width. */
  --env-ar: .72;
  --kap: .945;

  /* What the page's own furniture takes out of the window, written
     out of the same tokens that draw it instead of a measured
     constant. It used to be a flat 200 px with about 35 px of slack
     in it, and at --ui 1.75 that number was simply wrong — the
     countdown alone grew by more than the slack. Spelling the band
     out means it stays right whatever --ui becomes:

       masthead   one line of the kicker, line-height 1.6
       footbar    the reserved band, which is min-height
       padding    the shell's own, safe areas included
       gaps       two, between the three rows */
  --mast-h: calc(clamp(9px, 2.4vw, 11px) * var(--ui) * 1.6);
  --chrome-h: calc(
      var(--mast-h)
    + var(--foot-h)
    + max(12px, env(safe-area-inset-top))
    + max(14px, env(safe-area-inset-bottom))
    + clamp(8px, 2vh, 20px) * 2
  );

  /* The envelope is drawn at -1.4°, so what reaches the edge of the
     screen is not the box the layout reserved but the corner of a
     tilted one: w·cos(1.4°) + h·sin(1.4°), about 1.7 % more. 97vw
     was set on w, and on a phone that put the corner through the
     side of the screen — 5.8 px past it at 393 wide. The cap
     belongs on the width the eye sees, so it is written that way
     and divided back out.

     95.5vw of screen, then, whatever the tilt costs: about 9 px of
     wall either side of the envelope on a phone. It is the one term
     --zoom cannot move — every phone in the list is already at it. */
  --egim: 1.0173;   /* cos(1.4deg) + --env-ar × sin(1.4deg) */
  --env-w: max(calc(240px * var(--zoom)),
               min(calc(95.5vw / var(--egim)),
                   calc(644px * var(--zoom) / var(--kap)),
                   calc((100dvh - var(--chrome-h)) / var(--env-ar))));
  --env-fs: calc(var(--env-w) / 38);
  --card-ar: .74;                  /* A6 landscape */

  /* The scene sits in the row left between the two labels, and that
     row is not centred in the window: the band under it reserves the
     countdown and the strip above it is one line of type. Centred in
     the row, the envelope reads high — it is nearly touching DÜĞÜN
     DAVETİYESİ while a hand's width of wall sits under the prompt.

     So the scene is moved down inside its own cell. .28 rather than
     the half that would centre it geometrically, because the band is
     not empty: the countdown has weight of its own and pulling the
     scene all the way down would leave the two crowding each other.

     Capped at the slack that actually exists — on a phone held
     sideways the envelope already fills its row to the pixel, and
     any offset there would push it under the prompt. */
  --kaydir: max(0px, min(
      calc((var(--foot-h) - var(--mast-h)) * .28),
      calc((100dvh - var(--chrome-h) - var(--env-w) * var(--env-ar)) / 2)
  ));
}
/* Centring is of the margin box, so a top margin of 2x shifts the
   border box by x and leaves the row's height alone. All four layers
   take the same one: the card has to stay inside the envelope. */
.stage > * { grid-area: 1 / 1; margin-top: calc(var(--kaydir) * 2); }

/* Only the floor drops on a short window. The reserve above is
   already exact, so there is nothing left to shave off it. */
@media (max-height: 620px) {
  .stage { --env-w: max(calc(220px * var(--zoom)),
                        min(calc(95.5vw / var(--egim)), calc(644px * var(--zoom) / var(--kap)),
                            calc((100dvh - var(--chrome-h)) / var(--env-ar)))); }
}
/* A phone turned on its side. Here, and only here, the labels come
   back down: --ui applies to the countdown's own type, the
   countdown is what the footbar reserves, and on a 360 px-tall
   window that reserve at 1.75 is 105 px — nearly a third of the
   screen, taken out of the one dimension the envelope is short of.
   Upright phones are limited by their width and pay nothing for
   the full 1.75, so the relief is keyed to height alone.

   Note this is not the 620 px tier: a 320 x 568 phone is short but
   still width-bound, and shrinking its labels would buy nothing. */
@media (max-height: 480px) {
  :root { --ui: 1.25; }
  .stage { --env-w: max(calc(200px * var(--zoom)),
                        min(calc(95.5vw / var(--egim)), calc(644px * var(--zoom) / var(--kap)),
                            calc((100dvh - var(--chrome-h)) / var(--env-ar)))); }
}

/* ═══════════════════════════════════════════════════════════
   THE ENVELOPE — three layers: back leaf, invitation, front leaf.
   The card comes out from between two of them. A single-piece
   envelope cannot show that: the card stays either always in
   front of it or always behind.
   ═══════════════════════════════════════════════════════════ */
.envelope {
  --w: var(--env-w);
  font-size: var(--env-fs);
  width: var(--w);
  max-height: 100%;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  display: block;
  perspective: 90em;
  z-index: 0;
  transition: transform 900ms var(--ease-fall) 900ms;
  -webkit-tap-highlight-color: transparent;
}
.envelope:focus-visible { outline: 2px solid var(--gold); outline-offset: 10px; }
/* The envelope is not faded out, it is taken away. Something let
   go of does not slow to a stop, it accelerates. */
body.is-open .envelope {
  transform: translateY(calc(60vh + 26em)) rotate(-3.6deg);
  pointer-events: none;
}

.envelope__body {
  position: relative;
  display: block;
  width: 100%;
  aspect-ratio: 1 / var(--env-ar);
  transform: rotate(-1.4deg);
  transform-style: preserve-3d;
  background: var(--oxblood);
  background-image:
    var(--fibre-dark),
    linear-gradient(166deg, rgba(255,214,196,.16) 0%, rgba(255,214,196,0) 44%,
                            rgba(0,0,0,.32) 100%);
  /* The pulp layer covers the leaf once instead of tiling. At its
     own 300 px it fitted twice across a 643 px envelope and you
     could see the same blotches come round again — measurably: the
     column profile correlates with itself at exactly 300 px. The
     fibre layer above it stays at 150 px and keeps tiling, which
     nobody can see at a feature size of about a pixel. */
  background-size: 150px 150px, 100% 100%, auto;
  border-radius: .06em;
  box-shadow: 0 20px 32px -18px rgba(48, 30, 16, .5),
              0 2px 6px -3px rgba(48, 30, 16, .34),
              inset 0 .08em 0 rgba(255,216,198,.14);
}

.envelope__inside {
  position: absolute;
  inset: 0 0 auto 0;
  height: 49%;
  border-radius: .3em .3em 0 0;
  background: linear-gradient(180deg, var(--oxblood-in) 0%, #4E181E 52%, #62232A 100%);
  box-shadow: inset 0 .6em 1.3em rgba(18, 4, 6, .62);
}
/* A fixed darkness says the light never changes and the scene
   reads as a drawing. It retreats as the flap lifts. */
.envelope__inside::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: linear-gradient(180deg,
    rgba(10,2,4,.66) 0%, rgba(10,2,4,.34) 54%, rgba(10,2,4,.08) 100%);
  pointer-events: none;
}
body.is-open .envelope__inside::after {
  animation: inside-lighten 920ms var(--ease) 140ms both;
}
@keyframes inside-lighten {
  0%, 12% { opacity: 1; }
  100%    { opacity: .18; }
}

/* ── Flap ───────────────────────────────────────────────────
   Not one clean rotation. The flap does not lift before the seal
   breaks; the moment it does it resists for an instant, falls
   back under its own weight, hits and springs back a little. The
   easing does not change — the physics is between the frames. */
.envelope__flap {
  position: absolute;
  inset: 0 0 auto 0;
  height: 49%;
  transform-origin: top center;
  transform: rotateX(0deg);
  backface-visibility: hidden;
  border-radius: .3em .3em 0 0;
  background: #5E2127;
  background-image:
    var(--fibre-dark),
    linear-gradient(174deg, rgba(255,214,196,.2) 0%, rgba(255,214,196,0) 56%,
                            rgba(0,0,0,.26) 100%);
  /* 204 % — the flap is 49 % of the envelope, so the pulp is drawn
     at the envelope's scale and stays in phase with the body under
     it rather than being squashed into the flap's own box. */
  background-size: 150px 150px, 100% 204%, auto;
  /* No box-shadow here. The flap layer sits above the front leaf,
     so a shadow on it fell across the lower wax shard — and only
     the lower one, because the upper shard is the flap's own child
     and paints above its shadow. That put a soft dark step across
     the middle of a seal that is one unbroken pour: measured, half
     of the discontinuity at the fracture was this and nothing else.
     The shadow now lives on .pocket__face, under the wax, which is
     where a shadow cast onto paper belongs. */
}
body.is-open .envelope__flap { animation: flap-open 920ms var(--ease) 140ms both; }
/* Direction is not arbitrary. Negative tips the flap away from the
   viewer and the envelope's own inside covers it — the flap does
   not open, it vanishes. */
@keyframes flap-open {
  0%   { transform: rotateX(0deg); }
  8%   { transform: rotateX(2.4deg); }
  20%  { transform: rotateX(13deg); }
  68%  { transform: rotateX(181.5deg); }
  85%  { transform: rotateX(167.5deg); }
  100% { transform: rotateX(172.4deg); }
}

.flap__sheen {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: linear-gradient(180deg, rgba(255,216,198,.28) 0%, rgba(255,216,198,0) 3%);
  pointer-events: none;
}

/* The back of the flap — the side seen past 90°. Without it the
   flap disappears mid-rotation and the movement stops being a
   sheet of paper. Same hinge, same curve, flipped on the vertical
   axis; both are backface-hidden, so exactly one is ever visible. */
.flap__back {
  position: absolute;
  inset: 0 0 auto 0;
  height: 49%;
  display: block;
  transform-origin: top center;
  transform: rotateX(0deg) rotateY(180deg);
  backface-visibility: hidden;
  border-radius: .3em .3em 0 0;
  background: var(--oxblood-bk);
  background-image:
    var(--fibre-dark),
    linear-gradient(186deg, rgba(0,0,0,.34) 0%, rgba(0,0,0,.1) 44%,
                            rgba(255,214,196,.12) 100%);
  background-size: 150px 150px, 100% 204%, auto;
  box-shadow: inset 0 0 1.4em rgba(10, 2, 4, .5);
}
.flap__back::before {
  content: '';
  position: absolute;
  left: 7%; right: 7%; bottom: 5%;
  height: 15%;
  background: linear-gradient(180deg,
    rgba(190,164,124,0) 0%, rgba(190,164,124,.16) 72%, rgba(190,164,124,.08) 100%);
  border-radius: .16em;
}
body.is-open .flap__back { animation: flap-open-back 920ms var(--ease) 140ms both; }
@keyframes flap-open-back {
  0%   { transform: rotateX(0deg)     rotateY(180deg); }
  8%   { transform: rotateX(2.4deg)   rotateY(180deg); }
  20%  { transform: rotateX(13deg)    rotateY(180deg); }
  68%  { transform: rotateX(181.5deg) rotateY(180deg); }
  85%  { transform: rotateX(167.5deg) rotateY(180deg); }
  100% { transform: rotateX(172.4deg) rotateY(180deg); }
}

/* ── Flap layer ─────────────────────────────────────────────
   As a child of the envelope the flap is trapped in the
   envelope's stacking context and can never go under the card.
   But a flap folds BEHIND the envelope, and the card comes
   towards the viewer — in front of the flap. So it lives at the
   stage's own level and its depth changes in time. */
.flap-layer {
  --w: var(--env-w);
  font-size: var(--env-fs);
  width: var(--w);
  max-height: 100%;
  display: block;
  perspective: 90em;
  pointer-events: none;
  z-index: 3;
  transition: transform 900ms var(--ease-fall) 900ms;
}
.flap-layer__box {
  position: relative;
  display: block;
  width: 100%;
  aspect-ratio: 1 / var(--env-ar);
  transform: rotate(-1.4deg);
  transform-style: preserve-3d;
}
body.is-open .flap-layer {
  transform: translateY(calc(60vh + 26em)) rotate(-3.6deg);
  animation: flap-depth 920ms var(--ease) 140ms forwards;
}
/* One frame, no interpolation: z-index must not take an
   in-between value, so the two keyframes are adjacent. */
@keyframes flap-depth {
  0%, 37%   { z-index: 3; }
  38%, 100% { z-index: -2; }
}

.envelope__crease {
  position: absolute;
  left: 0; right: 0;
  top: 48.4%;
  height: 1px;
  transform: rotate(.05deg);
  background: linear-gradient(90deg,
    rgba(20,6,8,0) 0%, rgba(20,6,8,.5) 10%,
    rgba(20,6,8,.5) 90%, rgba(20,6,8,0) 100%);
  box-shadow: 0 1px 0 rgba(255,214,196,.12);
}

/* ── Front leaf ─────────────────────────────────────────────
   The box is an exact copy of the envelope body; only the part
   below the mouth is painted, so the fibre and the light stay in
   phase with the body instead of being repositioned. */
.pocket {
  --w: var(--env-w);
  font-size: var(--env-fs);
  width: var(--w);
  max-height: 100%;
  display: block;
  pointer-events: none;
  z-index: 2;
  transition: transform 900ms var(--ease-fall) 900ms;
}
body.is-open .pocket {
  transform: translateY(calc(60vh + 26em)) rotate(-3.6deg);
}
/* Promoted once, up front: doing it on the first frame of the
   movement is where the stumble comes from. Temporary — a
   permanent will-change keeps the card in a 3D context. */
body.is-open .envelope,
body.is-open .flap-layer,
body.is-open .pocket,
body.is-open .envelope__flap,
body.is-open .flap__back { will-change: transform; }
body.is-open .letter,
body.is-open .letter__paper { will-change: transform; }

.pocket__box {
  position: relative;
  display: block;
  width: 100%;
  aspect-ratio: 1 / var(--env-ar);
  transform: rotate(-1.4deg);
}
.pocket__face {
  position: absolute;
  inset: 0;
  border-radius: .06em;
  background: var(--oxblood);
  background-image:
    var(--fibre-dark),
    linear-gradient(166deg, rgba(255,214,196,.16) 0%, rgba(255,214,196,0) 44%,
                            rgba(0,0,0,.32) 100%);
  background-size: 150px 150px, 100% 100%, auto;
  /* The flap's lower edge is at 49 %; the front leaf starts a hair
     higher so the two overlap. Meeting at the same percentage
     leaves a one-pixel gap and the card shows through the middle
     of a closed envelope. */
  clip-path: inset(48.4% 0 0 0);
}
/* The flap's shadow, moved off the flap — see the note there. It
   starts at the leaf's own visible edge, so the parent's clip holds
   it, and it retreats on the same curve the envelope's inside does:
   a shadow of a flap that has lifted away should not stay on the
   paper. */
.pocket__face::before {
  content: '';
  position: absolute;
  left: 0; right: 0;
  top: 48.4%;
  height: .95em;
  background: linear-gradient(180deg, rgba(30, 12, 14, .42) 0%, rgba(30, 12, 14, 0) 100%);
}
body.is-open .pocket__face::before {
  animation: inside-lighten 920ms var(--ease) 140ms both;
}
/* Written straight onto the envelope, as on the reference: no
   label, no panel, just the hand. */
.pocket__hand {
  position: absolute;
  left: 0; right: 0;
  top: 70%;
  text-align: center;
  font-family: var(--hand);
  font-size: 2.5em;
  line-height: 1.25;
  color: rgba(246, 238, 224, .9);
  text-shadow: 0 .02em .04em rgba(0,0,0,.3);
}
/* The shadow the mouth casts on the card. Without this band the
   card does not sit inside the envelope, it sits on top of it. */
.pocket__lip {
  position: absolute;
  left: 0; right: 0;
  top: 48.4%;
  height: 2.4em;
  transform: translateY(-100%);
  background: linear-gradient(180deg,
    rgba(14,3,5,0) 0%, rgba(14,3,5,.2) 55%, rgba(14,3,5,.42) 100%);
}

/* ── Wax ────────────────────────────────────────────────────
   Not a gold disc laid on the paper. Wax is poured — it spreads
   into an uneven pool, the die presses a field into the middle
   and pushes a raised lip up around it, and the monogram stands
   proud of that field. All of it is one pour bridging the flap's
   edge, which is the only reason a seal exists: it holds the two
   leaves shut.

   So it is drawn twice and clipped either side of one fracture.
   The upper shard is a child of the flap, the lower one of the
   front leaf. Both boxes are the same size at the same point —
   the flap's bottom edge is at 49 % of the envelope, which is
   .seal--up's top:100% and .seal--dn's top:49% — so while the
   envelope is shut the two halves register exactly and read as
   one seal. */
.seal {
  position: absolute;
  left: 50%;
  width: 6.6em;
  height: 6.6em;
  transform: translate(-50%, -50%);
  pointer-events: none;
  /* Nothing here pulses. Wax that has set is a hard little disc of
     resin and it does not move until something breaks it; a
     breathing seal was the one thing on the closed envelope that
     announced it was a web page. What invites the tap is the line
     under it, which says so in words. */
}
.seal--up { top: 100%; }
.seal--dn { top: 49%; }
/* The cast shadow is drawn inside each shard, as the whole pour
   clipped to that shard's half, so the two halves meet into one
   shadow under one seal. A CSS drop-shadow on the shards cannot
   do this: the upper shard would throw its shadow down onto the
   lower one and lay a dark seam across a seal that has not broken
   yet. Which is why the svg has to spill past its viewBox. */
.seal svg { width: 100%; height: 100%; display: block; overflow: visible; }

/* Whole while it is whole.

   The two shards are one pour cut in two, and at the cut each edge
   is antialiased against nothing: half-covered pixels on the upper
   shard laid over half-covered pixels on the lower one never come
   back to solid, so a hairline ran across a seal that had not been
   broken yet. Overlapping them made it worse, not better — the
   grain and the cast shadow each half carries are semi-transparent,
   and in the overlap they were drawn twice. And the two are on
   different layers, one of them inside a 3D context, so even a
   clean abutment left the two sides resampled off each other.

   So neither is cut until the envelope opens. Each draws the whole
   seal; the upper one is on the flap, above, and is simply what you
   see. The lower one is hidden underneath it, in register, waiting.

   Both cuts land the instant `is-open` does — 140 ms before the
   flap starts to move, and 60 ms before the crack begins to show
   through. Whatever the join looks like in those few frames, it is
   under the fracture that is about to be drawn over it. */
.seal .wax-kesim { clip-path: none; }
body.is-open .seal--up .wax-kesim { clip-path: url(#wx-u); }
body.is-open .seal--dn .wax-kesim { clip-path: url(#wx-d); }
.wax__cast { fill: rgba(32, 15, 2, .5); }

.wax__lip   { stroke-width: 2.4; }
.wax__grain { fill: #000; opacity: .55; }

/* Raised, not engraved: the die is cut into, so the wax comes out
   of it standing up. Shadow cast down-right, the lit edge caught
   up-left, the face itself between them. */
.wax__mark { fill: none; stroke-linecap: round; stroke-linejoin: round; }
.wax__mark .mk-sh { stroke: rgba(46, 29, 3, .62);     stroke-width: 4.6; }
.wax__mark .mk-hi { stroke: rgba(255, 244, 214, .55); stroke-width: 3.9; }
.wax__mark .mk-fc { stroke-width: 3.3; }

/* Bubbles left in the pour. Same logic, one size down. */
.pt-sh { fill: rgba(48, 30, 4, .3); }
.pt-hi { fill: rgba(255, 238, 200, .3); }

/* The broken face. Fresh wax is paler and matte inside; on the
   lower shard that face tilts up into the light, on the upper one
   it points down and away.

   It is not there until the wax breaks. Drawn at rest it puts a
   pale seam across a seal that is supposed to be one piece, and
   the whole thing reads as two halves stuck together — which is
   what it is, but not until the flap lifts. */
.wax__break {
  fill: none;
  stroke-width: 3.4;
  stroke-linecap: round;
  stroke-linejoin: round;
  opacity: 0;
}
.wax__break--up { stroke: rgba(88, 60, 18, .78); }
.wax__break--dn { stroke: rgba(240, 222, 176, .7); }
body.is-open .wax__break { animation: wax-face 920ms var(--ease) 140ms both; }
@keyframes wax-face {
  0%        { opacity: 0; }
  6%        { opacity: .5; }
  16%, 100% { opacity: 1; }
}

/* The break. Both shards start moving in the flap's first 40 ms,
   before the flap has turned two degrees — the wax gives, then
   the paper follows, which is the order it happens in. After that
   neither is animated again: the upper shard is on the flap and
   turns with it, disappearing at 90° because by then its face is
   pointing away; the lower one rides the envelope out of frame.
   No opacity anywhere. Wax does not fade, it breaks and it goes
   with whatever it was stuck to. */
body.is-open .seal--up { animation: wax-up 920ms var(--ease) 140ms both; }
body.is-open .seal--dn { animation: wax-dn 920ms var(--ease) 140ms both; }
/* The two shards barely travel. Almost all of the gap that opens
   is the flap's own rotation carrying the upper piece away, and
   that is the honest source of it — a break in set wax parts by
   a hairline, not by a finger's width. What these keyframes add
   is the tilt: each piece turns a degree and a half against the
   other about the seal's centre, so the crack opens as a wedge
   rather than sliding apart in parallel. */
@keyframes wax-up {
  0%        { transform: translate(-50%, -50%)   rotate(0deg); }
  4%        { transform: translate(-50%, -50.4%) rotate(-.6deg); }
  12%       { transform: translate(-50%, -50.8%) rotate(-1.2deg); }
  22%       { transform: translate(-50%, -51%)   rotate(-1.5deg); }
  30%, 100% { transform: translate(-50%, -51.1%) rotate(-1.6deg); }
}
@keyframes wax-dn {
  0%        { transform: translate(-50%, -50%)   rotate(0deg); }
  4%        { transform: translate(-50%, -49.7%) rotate(.55deg); }
  12%       { transform: translate(-50%, -49.35%) rotate(1.1deg); }
  22%       { transform: translate(-50%, -49.2%) rotate(1.4deg); }
  30%, 100% { transform: translate(-50%, -49.15%) rotate(1.5deg); }
}

/* ═══════════════════════════════════════════════════════════
   THE INVITATION
   Flat, landscape, slightly narrower than the envelope so it
   never pokes past the edge while inside. Not folded: it sits
   between the two leaves and is drawn straight up.
   ═══════════════════════════════════════════════════════════ */
.letter {
  --w: calc(var(--env-w) * var(--kap));
  width: var(--w);
  aspect-ratio: 1 / var(--card-ar);
  display: block;
  z-index: 1;
  transform: rotate(-2.25deg);
  pointer-events: none;
}
/* Drawn out and dropped on the table: it drags inside first,
   clears the mouth, overshoots and settles. No opacity — paper
   does not fade in. Fill is backwards, not both: with both the
   last frame stays applied and the card keeps a 3D context. */
body.is-open .letter {
  transform: none;
  pointer-events: auto;
  animation: letter-out 1060ms var(--ease) 880ms backwards;
}
/* Percentages of the card's own height, not calc() on a custom
   property. A keyframe whose value reads a var() has to be
   resolved by the style engine on every frame, which keeps the
   whole animation on the main thread; the card is the heaviest
   element in the scene (torn mask, drop shadow, three noise
   layers, all the type) and animating it there is what people saw
   as a stutter while the envelope was clearing the frame. With
   literal percentages it runs on the compositor.

   The card is 1 : 0.74, so a distance of k x width is k / 0.74 of
   its height — that is where these numbers come from. */
@keyframes letter-out {
  0%   { transform: translateY(0)        rotate(-2.25deg); }
  14%  { transform: translateY(-2.43%)   rotate(-2.1deg); }
  55%  { transform: translateY(-56.76%)  rotate(-.75deg); }
  80%  { transform: translateY(3.51%)    rotate(.2deg); }
  92%  { transform: translateY(-.95%)    rotate(-.05deg); }
  100% { transform: none; }
}
.letter:focus { outline: none; }

.letter__stage { position: relative; height: 100%; transform: rotate(.85deg); }

/* The dark inside the envelope. Once the flap lifts the card is
   visible but still inside — the light on it is not table light.

   It sits inside .letter__paper, above everything printed there,
   so the deckle mask cuts it to the sheet's own torn silhouette.
   One level up it was a rectangle laid over handmade paper and
   the whole pull-out ran inside a visible hard-edged box. */
.letter__dim {
  position: absolute;
  inset: 0;
  z-index: 10;
  pointer-events: none;
  opacity: 0;
  background: linear-gradient(180deg,
    rgba(30,10,12,.54) 0%, rgba(30,10,12,.36) 46%, rgba(30,10,12,.54) 100%);
}
body.is-open .letter__dim { animation: dim-out 1060ms var(--ease) 880ms backwards; }
@keyframes dim-out {
  0%   { opacity: 1; }
  46%  { opacity: .55; }
  100% { opacity: 0; }
}

/* ── The sheet ──────────────────────────────────────────── */
.letter__flip { position: absolute; inset: 0; perspective: 170em; }
.letter__paper {
  position: absolute;
  inset: 0;
  /* The sheet is the unit of measure. Tie type to the viewport and
     a wide-but-short window drives every clamp() to its maximum
     while the card stays small. */
  container-type: size;
  container-name: sheet;
  transform-origin: 50% 50%;
  background: var(--card);
  background-image:
    var(--fibre),
    linear-gradient(168deg, rgba(255,252,244,.4) 0%, rgba(255,252,244,0) 46%,
                            rgba(90,70,44,.07) 100%);
  background-size: 150px 150px, 130px 130px, 100% 100%, auto;
  overflow: hidden;
  transition: transform 380ms var(--ease), filter 380ms var(--ease);
  /* Deckled handmade cotton. box-shadow is painted outside the
     border box and a mask erases it, so the shadow has to come
     from a filter — which follows the torn silhouette. */
  -webkit-mask-image: var(--deckle);
          mask-image: var(--deckle);
  -webkit-mask-size: 100% 100%;
          mask-size: 100% 100%;
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  filter: drop-shadow(0 14px 22px rgba(60, 44, 28, .34))
          drop-shadow(0 2px 4px rgba(60, 44, 28, .22));
  --deckle: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='400' height='296' viewBox='0 0 400 296' preserveAspectRatio='none'%3E%3Cfilter id='d'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.028 0.05' numOctaves='4' seed='11' result='t'/%3E%3CfeDisplacementMap in='SourceGraphic' in2='t' scale='8' xChannelSelector='R' yChannelSelector='G'/%3E%3C/filter%3E%3Crect x='5' y='5' width='390' height='286' fill='%23fff' filter='url(%23d)'/%3E%3C/svg%3E");
}
body.is-open .letter__paper { cursor: pointer; -webkit-tap-highlight-color: transparent; }
body.is-open .letter__paper a { cursor: pointer; }
/* Standing on its edge the sheet lifts off the table a little. */
body.is-turning .letter__paper {
  backface-visibility: hidden;
  will-change: transform;
  filter: drop-shadow(0 26px 36px rgba(60, 44, 28, .4))
          drop-shadow(0 4px 8px rgba(60, 44, 28, .26));
}

/* ── Faces ──────────────────────────────────────────────── */
.pages { position: absolute; inset: 0; overflow: hidden; }
.pages__track { position: absolute; inset: 0; }
/* Neither face scrolls. Inside a card, text fits or it is cut.
   The hidden face is visibility:hidden, never display:none — a
   zero-sized box measures nothing and every container query
   inside it would have to re-resolve on the frame it turns. */
.page {
  position: absolute;
  inset: 0;
  display: flex;
  visibility: hidden;
  overflow: hidden;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  text-align: center;
  padding: clamp(14px, 5.6cqw, 42px) clamp(15px, 6.2cqw, 46px);
}
.page.is-on { visibility: visible; }
.page > * { margin: 0; }

/* ── Printed on the sheet ───────────────────────────────────
   Two-level headings, as in the reference: a small italic word
   above a tracked line of capitals. Capitals are written into the
   markup — text-transform would turn "için" into "IÇIN". */
/* Face 2 only. It is set larger than face 1's equivalents on
   purpose — see the note above .cols. */
.eyebrow {
  font-style: italic;
  font-size: clamp(9.5px, 2.8cqw, 21px);
  line-height: 1.5;
  color: var(--ink-soft);
}

.families {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: clamp(7px, 3cqw, 20px);
  margin-top: clamp(3px, 1.4cqh, 9px) !important;
}
.family { margin: 0; line-height: 1.45; }
.family__given {
  display: block;
  font-style: italic;
  font-size: clamp(10px, 2.5cqw, 19px);
  color: var(--ink-soft);
}
.family__name {
  display: block;
  font-family: var(--caps);
  font-size: clamp(10.8px, 2.75cqw, 21px);
  letter-spacing: .18em;
  text-indent: .18em;
  line-height: 1.5;
  color: var(--ink);
}
.families__bar {
  width: 1px;
  align-self: stretch;
  background: linear-gradient(180deg,
    rgba(107,83,64,0), rgba(107,83,64,.4) 30%, rgba(107,83,64,.4) 70%, rgba(107,83,64,0));
}

.names {
  display: flex;
  align-items: baseline;
  justify-content: center;
  gap: clamp(4px, 1.8cqw, 12px);
  margin-top: clamp(4px, 2cqh, 13px) !important;
  white-space: nowrap;
}
.name {
  font-size: clamp(19px, 5.4cqw, 40px);
  letter-spacing: .13em;
  text-indent: .13em;
  line-height: 1.36;
  color: var(--ink);
}
.amp {
  font-style: italic;
  font-size: clamp(19.5px, 5.6cqw, 41px);
  line-height: 1;
  color: var(--gold);
}

/* The venue, drawn by hand inside a mission arch. */
.ornament {
  flex: none;
  margin-top: clamp(2px, 1.2cqh, 8px);
  height: clamp(28px, 17cqh, 106px);
}
.arch { display: block; height: 100%; width: auto; margin: 0 auto; }
.arch .f1 { fill: none; stroke: rgba(107,83,64,.24); stroke-width: 1.6; }
.arch .f2 { fill: none; stroke: rgba(107,83,64,.55); stroke-width: 1.1; }
.arch .wash { opacity: .5; }
.arch .s1, .arch .s2, .arch .s3, .arch .s4 {
  fill: none;
  stroke: var(--sepia);
  stroke-linecap: round;
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
}
.arch .s1 { stroke-width: 1.15; }
.arch .s2 { stroke-width: .85; opacity: .9; }
.arch .s3 { stroke-width: .65; opacity: .78; }
.arch .s4 { stroke-width: .5;  opacity: .5; }

.verse {
  max-width: 34ch;
  margin-top: clamp(3px, 1.6cqh, 10px) !important;
  font-style: italic;
  font-size: clamp(11px, 2.85cqw, 22px);
  line-height: 1.62;
  color: var(--ink-soft);
}

.when {
  margin-top: clamp(4px, 1.9cqh, 12px) !important;
  font-family: var(--caps);
  font-size: clamp(12px, 3cqw, 23.5px);
  letter-spacing: .24em;
  text-indent: .24em;
  line-height: 1.5;
  color: var(--ink);
}
.venue {
  margin-top: clamp(2px, 1.1cqh, 7px) !important;
  font-family: var(--caps);
  font-size: clamp(13px, 3.5cqw, 26.5px);
  letter-spacing: .16em;
  text-indent: .16em;
  line-height: 1.45;
  color: var(--ink);
}
.place {
  margin-top: clamp(1px, .6cqh, 4px) !important;
  font-size: clamp(10.5px, 2.65cqw, 20px);
  line-height: 1.5;
  color: var(--ink-soft);
}

/* The reference's hairline stadium button. */
.cta {
  display: inline-block;
  margin-top: clamp(4px, 1.8cqh, 12px);
  padding: .55em 1.7em;
  border: 1px solid rgba(107, 83, 64, .42);
  border-radius: 999px;
  color: var(--ink-soft);
  text-decoration: none;
  font-style: italic;
  font-size: clamp(10.5px, 2.05cqw, 15.5px);
  line-height: 1.5;
  white-space: nowrap;
  transition: transform 320ms var(--ease), border-color 320ms var(--ease),
              color 320ms var(--ease);
}
.cta:hover { border-color: var(--gold); color: var(--sepia); }
.cta:active { transform: scale(.97); }
.cta:focus-visible { outline: 2px solid var(--gold); outline-offset: 3px; }

/* ── Face 2 ─────────────────────────────────────────────────
   Face 2 carries five short lines in two columns where face 1
   carries nine across the full width, so at face 1's scale it
   filled about a third of the sheet and sat in the middle of it
   like a caption. Its ratios are therefore larger than face 1's,
   and that is not a break of the type-scale rule: each column is
   its own measure, a little under half the card, so a 3.5cqw
   heading inside one is set to a narrower line than face 1's
   3.05cqw venue is across the whole sheet.

   The floors are untouched, so none of this reaches a phone —
   where the card is already two thirds full and there is nothing
   to give. The divider grows on its own: .cols is align-items:
   stretch, so the bar is as tall as the taller column. */
/* Face 2's own margins. The sheet's default padding is set for
   face 1, which runs one column the full width; here it is taken
   off both columns twice over and the address — the longest line
   on the card — hits the column edge long before the type is big
   enough. Narrower margins buy the width that lets it grow. */
.page.page--two { padding-left: clamp(12px, 4.2cqw, 32px);
                  padding-right: clamp(12px, 4.2cqw, 32px); }
.cols {
  display: flex;
  align-items: stretch;
  justify-content: center;
  gap: clamp(10px, 5.2cqw, 40px);
  width: 100%;
}
.col {
  flex: 1 1 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
}
.col > * { margin: 0; }
.col__head {
  font-family: var(--caps);
  font-size: clamp(11.5px, 4.6cqw, 34px);
  letter-spacing: .2em;
  text-indent: .2em;
  line-height: 1.5;
  color: var(--ink);
  margin-bottom: clamp(3px, 3cqh, 22px) !important;
}
.cols__bar {
  width: 1px;
  background: linear-gradient(180deg,
    rgba(107,83,64,0), rgba(107,83,64,.34) 22%, rgba(107,83,64,.34) 78%, rgba(107,83,64,0));
}

.program { list-style: none; margin: 0; padding: 0; }
.program li {
  display: flex;
  align-items: baseline;
  justify-content: center;
  gap: .7em;
  line-height: 2.1;
}
.program b {
  min-width: 3.4em;
  text-align: right;
  font-weight: 400;
  font-family: var(--caps);
  font-size: clamp(11px, 3.2cqw, 24px);
  letter-spacing: .1em;
  color: var(--ink);
}
.program span {
  flex: 1;
  text-align: left;
  font-style: italic;
  font-size: clamp(11px, 3.2cqw, 24px);
  color: var(--ink-soft);
}

.venue--sm { font-size: clamp(11.5px, 3.5cqw, 26px); margin-top: 0 !important; }
.address {
  margin-top: clamp(2px, 2.4cqh, 16px) !important;
  font-size: clamp(10.5px, 2.7cqw, 20px);
  line-height: 1.6;
  color: var(--ink-soft);
}
/* Only face 2's pill grows. Face 1's is on a full sheet that is
   already tight and is the first thing dropped on a phone. */
.page--two .cta {
  margin-top: clamp(4px, 3.6cqh, 28px);
  font-size: clamp(10.5px, 2.9cqw, 21px);
}

/* The rest of face 2's height goes into air, not type. Past this
   point the address — the longest line on the card — would reach
   the column edge, and the two headings are already as large as
   face 1's names. Gated on the card's own height so a phone, which
   is at five sixths full with nothing to give, never sees it. */
@container sheet (min-height: 340px) {
  .page--one .ornament    { height: clamp(28px, 22cqh, 123px); }
  .page--two .col__head  { margin-bottom: clamp(3px, 4.2cqh, 30px) !important; }
  .page--two .program li { line-height: 2.5; }
  .page--two .address    { margin-top: clamp(2px, 3.2cqh, 22px) !important; }
  .page--two .cta        { margin-top: clamp(4px, 5cqh, 38px); }
}

/* Every phone held upright is a tight card — a landscape card is
   as wide as the phone and no wider, and the height follows from
   the width. The pill on face 1 goes there: the same link is on
   face 2, and losing it buys a third of a line of body copy. */
@container sheet (max-height: 275px) {
  .page--one .verse { max-width: 30ch; }
}

/* Tighter still: a small phone, or a phone held sideways. Below
   about this height every line on the card is already sitting on
   its floor, so the card gets shorter while the type does not —
   and the type is not what gives. The air between the lines goes
   first, then the arch comes down to a mark rather than a
   drawing. */
@container sheet (max-height: 245px) {
  .page--one .ornament { height: 26px; margin-top: 2px; }
  .page--one .names    { margin-top: 4px !important; }
  .page--one .verse    { margin-top: 3px !important; }
  .page--one .when     { margin-top: 5px !important; }
  .page--one .venue    { margin-top: 2px !important; }
  .page--one .place    { margin-top: 1px !important; }
}

/* Smaller than that and the arch goes. It is decoration, it is
   down to twenty pixels of smudge by here, and it is the only
   thing on the face that can leave without taking information
   with it. The type keeps its floor. */
@container sheet (max-height: 226px) {
  .page--one .ornament { display: none; }
}

/* ── Turn note ──────────────────────────────────────────────
   Not a button, only a note printed on the sheet: tapping
   anywhere turns the card. It says what to do rather than naming
   the gesture, because a guest who has never seen the card does
   not know there is a second side — that is the thing the note
   has to tell them, and one word could not. */
.turn {
  position: absolute;
  /* Clear of the deckle: the torn edge wanders about eight
     pixels in from the border box, and the note was sitting on
     it. */
  right: clamp(11px, 3.7cqw, 25.5px);
  bottom: clamp(8px, 3.2cqh, 19px);
  z-index: 6;
  display: flex;
  align-items: center;
  gap: .55em;
  font-style: italic;
  font-size: clamp(10px, 2.3cqw, 16.5px);
  line-height: 1.4;
  white-space: nowrap;
  color: var(--ink-soft);
  opacity: 0;
  pointer-events: none;
  transition: opacity 600ms var(--ease);
}
body.is-open .turn { opacity: .85; animation: turn-nudge 2.6s var(--ease) 2.1s 3; }
body.has-turned .turn { opacity: 0; animation: none; }
.turn__icon { width: 1.75em; height: auto; }
@keyframes turn-nudge {
  0%, 72%, 100% { transform: translateX(0); }
  80%           { transform: translateX(2.5px); }
  88%           { transform: translateX(0); }
}

/* ── Reduced motion ─────────────────────────────────────────
   The choreography is not removed but collapsed: by the time the
   card is there the envelope is gone. script.js takes the same
   branch and skips every timer. */
@media (prefers-reduced-motion: reduce) {
  .envelope, .flap-layer, .pocket, .letter,
  .envelope__flap, .flap__back, .letter__dim,
  .envelope__inside::after, .seal, .wax__break {
    transition-duration: 1ms !important;
    animation-duration: 1ms !important;
    animation-delay: 0ms !important;
  }
  .letter__paper { transition-duration: 1ms !important; }
  .turn { animation: none !important; }
}
