/* ============================================================================
 * THE DOOR — the front page's stage and the reel lockup.
 *
 * Loaded ON TOP of seam.css, never instead of it. The bar, the footer, the
 * buttons, the type stack and every token come from The Seam; this file adds
 * exactly one thing the rest of the site does not have — a full-bleed moving
 * ground with one enormous piece of type standing in it.
 *
 * TWO RULES IT INHERITS AND KEEPS
 *
 *   Colour only where separate things meet. The stage itself carries no accent
 *   at all — no tinted scrim, no glow, no wash. Colour appears in exactly three
 *   places on this page and all three are real joins: the hairline already under
 *   the bar, the 3px spectrum at the bottom edge where the moving picture meets
 *   the page, and the reel — a fixed brand word welded to one that keeps
 *   changing, which is the most literal seam on the site. Each ending gets its
 *   own stop off the spectrum; "pool" is the product's actual name rather than a
 *   joke about it, so it is the one that is not a deviation and stays ink-white.
 *   Only ever one of them is on screen at a time.
 *
 *   No photograph is presented as a guest's capture or as product output. The
 *   plates are licensed stand-ins of rooms with people in them (site/pages/
 *   home.html names each one), shown as the ROOM, never as "look what came out
 *   of PhotoPool". Nothing on this page claims to be a pool.
 *
 * WHY A NEW FILE RATHER THAN A §14 IN seam.css: seam.css is loaded by every
 * public page and this is ~300 lines that exactly one page uses, all of it
 * describing a full-bleed video stage that no other page has. Registered in
 * DESIGN_FILE_RE (src/handlers/app.js) — a new stylesheet 404s silently until
 * it is named there and the server restarts.
 *
 * Zero dependencies. Only transform and opacity are ever animated.
 * ========================================================================== */

/* ==========================================================================
 * 1. THE STAGE
 *
 * One CSS grid cell with everything stacked in it, rather than a positioning
 * context with four absolute children. Grid gives each layer a real box that
 * participates in sizing, so the stage's height is driven by its content on a
 * short window instead of collapsing to zero and needing a min-height guess.
 * ====================================================================== */

.door {
  position: relative;
  display: grid;
  grid-template-areas: "stage";
  isolation: isolate;
  overflow: hidden;

  /* svh, not vh. On iOS `100vh` is the LARGE viewport — the one with the URL bar
   * already scrolled away — so a vh-sized hero is taller than the screen on
   * arrival and the lockup sits low, then the whole stage resizes mid-scroll as
   * the chrome collapses. svh is the small viewport and never moves.
   *
   * The bar is sticky and therefore still in flow, so a full-height stage under it
   * makes the page one screen TALLER than the screen. Subtracting it is what makes
   * the hero exactly one screenful with the bar on top. 4.32rem is measured, not
   * assumed: seam.css gives .bar-brand and .theme-btn a 44px minimum for touch and
   * the bar 0.75rem of padding plus a 1px rule, which comes to 69px. The narrow
   * bar is shorter and §7 subtracts its own measured number. */
  min-height: clamp(29rem, calc(100svh - 4.32rem), 58rem);

  /* ---- THE HERO IS DARK IN BOTH THEMES ---------------------------------
   * Every other surface on this site genuinely inverts. A lit room photographed
   * at night does not: there is no light-mode version of a dark photograph, and
   * a scrim that flips to white would erase the picture it exists to sit on.
   *
   * So the stage rebinds the SEMANTIC tokens rather than overriding component
   * rules. .act, .act-live, .act-quiet and anything added later resolve against
   * these and come out correct inside the hero with no per-component work — the
   * whole point of the token layer being semantic in the first place.
   *
   * The values are the dark block from seam.css §1, copied deliberately rather
   * than imported: the hero must stay legible on photographs even if the dark
   * theme is later retuned for flat ground. Two of them are already lifted for
   * exactly that reason, and say so. */
  --bg: #0a0a0a;
  --bg-raised: #161616;
  --bg-sunk: #050505;
  --fg: #fafafa;
  --fg-muted: #cfcfcf;    /* theme dark is #a3a3a3 — lifted: this sits on moving
                             photographs, not on a flat near-black ground */
  --fg-quiet: #a8a8a8;
  --line: #3a3a3a;
  --line-strong: #8f8f8f; /* theme dark is #575757 — a 1px #575757 outline is
                             invisible against a bright frame of the montage */

  color: var(--fg-muted);
  background: #000;       /* the ground the first plate decodes over */
}

/* Every direct child occupies the single cell. Stacking order is source order,
 * which is why the markup runs stage -> scrim -> lockup -> seam. */
.door > * { grid-area: stage; }

/* ==========================================================================
 * 2. THE MOVING GROUND
 *
 * Two sources, one box. A real <video> when site/build.js found one at build
 * time, and a crossfading plate montage underneath it that is the whole
 * background when there is no file — and the poster/fallback when there is.
 * ====================================================================== */

.door-stage { position: relative; }

.door-vid,
.door-plate {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  background-size: cover;
  background-position: center;
  /* height:auto from seam.css §2 would let a cover video shrink to its intrinsic
     ratio inside the stage. Videos and plates are cropped boxes, not images in
     a text flow, so they opt out. */
  max-width: none;
}

/* The video sits above the plates and simply covers them once it can paint. It
 * starts hidden and door.js reveals it on `loadeddata`: a <video> that never
 * gets far enough to render a frame would otherwise show as a black rectangle
 * over a montage that was working fine. */
.door-vid { z-index: 1; opacity: 0; transition: opacity 600ms var(--ease-enter); }
.door-vid.is-on { opacity: 1; }

/* ---- the plate montage ---------------------------------------------------
 * Five photographs, one shared cycle, each holding a fifth of it with a
 * crossfade at every handover, plus a slow drift so a still never reads as a
 * frozen JPEG. --i orders a plate in the cycle; --dx/--dy give it its own
 * direction so five plates do not all slide the same way.
 *
 * The percentages below are literal because @keyframes cannot read a custom
 * property — they assume FIVE plates. Changing the count means changing the
 * stops (and --cycle), which is the trade for keeping this in CSS with zero
 * per-frame JavaScript. */
.door-plates { --cycle: 45s; position: absolute; inset: 0; }

.door-plate {
  opacity: 0;
  transform: scale(1.04);
  animation: door-plate var(--cycle) linear infinite;
  animation-delay: calc(var(--cycle) / 5 * var(--i, 0));
}

/* The first plate is in the markup and is the LCP candidate: it must be opaque
 * on the very first frame, before the animation's own 0% keyframe would fade it
 * in from nothing. `backwards` is what makes a delayed animation hold its 0%
 * state during the delay — without it plates 2..5 would flash at opacity 1 for
 * their entire delay window. */
.door-plate { animation-fill-mode: backwards; }
.door-plate--first { animation-delay: 0s; }

@keyframes door-plate {
  0%     { opacity: 0; transform: scale(1.04) translate(0, 0); }
  3%     { opacity: 1; }
  20%    { opacity: 1; }
  24%    { opacity: 0; transform: scale(1.14) translate(var(--dx, -1.4%), var(--dy, -1%)); }
  /* An invisible reset. The drift has to start over for the next pass, and doing
   * it here — one hundredth of a percent after the plate is fully transparent —
   * costs nothing and saves a second animation. */
  24.01% { transform: scale(1.04) translate(0, 0); }
  100%   { opacity: 0; transform: scale(1.04) translate(0, 0); }
}

/* ---- the scrim ----------------------------------------------------------
 * Three neutral blacks, no tint. The site's grays are truly neutral by policy
 * (seam.css §1) and a warm scrim over a party photograph is the exact cream
 * this brand moved away from.
 *
 * The radial is deliberately strongest where the lockup stands rather than an
 * even wash: an even wash strong enough for 8rem type flattens the picture into
 * mud, and the picture is the reason the stage exists. */
.door-scrim {
  position: absolute;
  inset: 0;
  z-index: 2;
  background:
    radial-gradient(115% 78% at 50% 50%,
      rgba(0, 0, 0, 0.74) 0%, rgba(0, 0, 0, 0.56) 42%, rgba(0, 0, 0, 0.10) 100%),
    linear-gradient(180deg, rgba(0, 0, 0, 0.55) 0, rgba(0, 0, 0, 0) 20%),
    linear-gradient(0deg,   rgba(0, 0, 0, 0.70) 0, rgba(0, 0, 0, 0) 28%);
}

/* ==========================================================================
 * 3. THE LOCKUP
 * ====================================================================== */

.door-in {
  position: relative;
  z-index: 3;
  align-self: center;
  width: min(84rem, 100% - (var(--gut) * 2));
  margin-inline: auto;
  padding-block: clamp(2.5rem, 7vh, 5rem);
  text-align: center;
}

.door-line {
  /* ONE REEL SLOT. Every box in the lockup is exactly this tall and uses it as
   * its line-height, which is what makes the lede's baseline and the reel word's
   * baseline land on the same pixel without a single magic offset: identical
   * font-size and identical line box, centre-aligned, means identical baseline.
   * 1.16 also keeps ascenders and descenders (the g in "magic", the y in
   * "frenzy", the p in "palooza") inside the clipped window. */
  --step: 1.16em;

  /* Set by door.js from the real measured words; these defaults are close enough
   * that the pre-script frame is not visibly wrong.
   *   --reel-w      the widest word, so the window never resizes and nothing in
   *                 the line ever moves horizontally.
   *   --reel-nudge  widest minus mean. Half of it shifts the lockup right, which
   *                 optically centres the AVERAGE word inside a window sized for
   *                 the longest one. See §4. */
  --reel-w: 4.15em;
  --reel-nudge: 1.05em;

  margin: 0;
  /* THE BUDGET, measured rather than guessed. In the shipped stack at weight
   * 300/600 the parts come out at:
   *
   *     "Pure" 1.79em + gap 0.26em + "Photo" 2.44em + window 3.72em  =  8.21em
   *     plus the optical nudge                                       =  0.95em
   *                                                                  ---------
   *     total painted extent                                            9.16em
   *
   * The slot is min(84rem, 100vw - 2*gut), and gut is itself 3vw in the range
   * that matters, so the slot is ~0.94 * 100vw until it caps. 0.94 / 9.16 =
   * 10.2vw is the exact fit; 9vw leaves ~12% for the fact that these widths were
   * measured in one font stack and Segoe UI and Roboto are not that stack.
   *
   * If a longer word is ever added to the drum the window grows and this has to
   * come down — door.js logs nothing about it, and the failure is the headline
   * silently clipping at the right edge on a mid-width window. */
  font-size: clamp(2.2rem, 9vw, 7.5rem);
  font-weight: 600;
  letter-spacing: -0.035em;
  line-height: var(--step);
  color: var(--fg);
  text-shadow: 0 2px 40px rgba(0, 0, 0, 0.45);
}

.door-lock {
  display: flex;
  justify-content: center;
  align-items: flex-start;
  column-gap: 0.26em;
  /* The optical correction. Flex containers drop whitespace-only text nodes
   * between items, which is also why the markup can put each span on its own
   * line without opening a space between "Photo" and the reel. */
  transform: translateX(calc(var(--reel-nudge) / 2));
}

/* Hierarchy by weight and colour, not size: "Pure" is the qualifier and the
 * brand word is the thing being said, so the qualifier gets quieter and lighter
 * while both stay on one type size. */
.door-pure {
  font-weight: 300;
  color: var(--fg-muted);
  letter-spacing: -0.02em;
}

/* "Photo" and the reel must never break apart — they are one word. */
.door-join { display: flex; align-items: flex-start; }

/* ==========================================================================
 * 4. THE REEL
 *
 * A lottery drum: a fixed ring of words behind a slot, spun hard and let down
 * onto the next one.
 *
 * The window is EXACTLY the widest word and never resizes. Transitioning its
 * width instead would be honest to the type but would drag "Pure Photo"
 * sideways by half the delta on every spin — about 90px at full size — and the
 * one thing the eye is holding on to is that the brand word does not move.
 * The cost is that a short word ("pool", "bomb") sits left of the window's
 * centre; --reel-nudge above pays for that by centring the mean instead of the
 * maximum, which spreads the error to +/- half a word either side of true.
 * ====================================================================== */

.reel {
  display: block;
  width: var(--reel-w);
  height: var(--step);
  overflow: hidden;
  text-align: left;
  flex: 0 0 auto;
}

.reel-track {
  display: block;
  /* Nothing else. The resting transform is written by door.js and the spin is a
   * Web Animations keyframe effect on this element — a CSS transform here would
   * be the thing WAAPI has to fight on every landing. */
  will-change: transform;
}

.reel-word {
  display: block;
  /* max-content, not the window's width. A plain block child fills its parent, and
   * the parent here is the fixed-width slot — so every word reported the SLOT width
   * when measured and door.js concluded they were all identical, which silently
   * zeroed the optical nudge in §3. Sized to its own ink, a word measures as its own
   * ink. Anything wider than the slot is clipped, which is the slot's job. */
  width: max-content;
  height: var(--step);
  line-height: var(--step);
  white-space: nowrap;

  /* ---- ONE COLOUR PER WORD ---------------------------------------------
   * The site's rule is that colour appears only where separate things meet, and
   * this is the most literal seam on the site: a fixed brand word welded to a
   * word that keeps changing. So the join is where the spectrum lives, and each
   * ending gets its own stop.
   *
   * "pool" is the exception and stays ink-white. It is the only ending that is
   * the actual product name rather than a joke about it, so it is the only one
   * that is not a deviation — and it is the resting state, which means a still
   * screenshot, a reduced-motion visitor and the printer all get the brand in
   * the brand's own colour.
   *
   * Stops are assigned in site/pages/home.html, one per word, deliberately NOT
   * in wheel order: adjacent hues cycling past would read as a gradient sweep,
   * which is the exact tell seam.css §1 exists to avoid. Scattered around the
   * circle it reads as a drum of separate threads.
   *
   * Every stop clears 3:1 on the scrim at this size (the WCAG floor for large
   * text) — they were tuned in seam.css to survive as 2px hairlines on both
   * grounds, so as 96px display type they have room to spare. */
  color: var(--c, var(--fg));
}

/* The cylinder. A drum's top and bottom curve away from the viewer, and fading
 * them is what separates "a reel spinning" from "a list scrolling".
 *
 * Applied ONLY while spinning. At rest the mask would eat the ascender of a "b"
 * and the descender of a "g", so the resting word is never masked at all — the
 * class is added when the animation starts and removed when it finishes. */
.reel.is-spinning {
  -webkit-mask-image: linear-gradient(180deg, transparent 0, #000 27%, #000 73%, transparent 100%);
          mask-image: linear-gradient(180deg, transparent 0, #000 27%, #000 73%, transparent 100%);
}

/* ==========================================================================
 * 5. WHAT IS SAID UNDER IT
 * ====================================================================== */

.door-sub {
  /* 45-75 characters. Centred is allowed here because it is two lines at most
   * at every width; the measure is what enforces that. */
  max-width: 46ch;
  margin: clamp(1.25rem, 3vh, 2rem) auto 0;
  font-size: clamp(1rem, 1.7vw, 1.2rem);
  line-height: 1.55;
  color: var(--fg-muted);
  text-shadow: 0 1px 24px rgba(0, 0, 0, 0.5);
}

/* .actions is seam.css §11 — flex, wrap, 0.75rem gap. Only the centring is new,
 * because everywhere else on the site actions sit under left-aligned text. */
.door-acts { justify-content: center; }

/* ==========================================================================
 * 6. THE JOIN
 *
 * The bottom edge of the stage, where the moving picture meets the page. This
 * is the one place on this page that has earned colour. 3px rather than the
 * standard 2px hairline: it is terminating a full-bleed photograph, not
 * separating two pieces of paper.
 * ====================================================================== */

.door-seam {
  align-self: end;
  z-index: 4;
  height: 3px;
  background: var(--spectrum);
}

/* ==========================================================================
 * 7. NARROW
 *
 * Below 40rem the line stacks. On one line the budget is ~9.4em, which at a
 * 390px screen forces the type down to about 35px — a hero headline the size of
 * a section heading. Stacked, the widest row is "Photo" + the window (~6.8em),
 * which buys back a third of the size. The lockup becomes a column and the
 * optical nudge moves off it onto the row that actually contains the window.
 * ====================================================================== */

@media (max-width: 40rem) {
  .door { min-height: clamp(28rem, calc(100svh - 4.02rem), 48rem); }   /* the narrow bar measures 64.2px */

  /* Stacked, the widest row is "Photo" + window + nudge = 7.11em rather than the
   * one-line 9.16em, which buys back about a quarter of the size. 11.3vw against
   * a slot of 100vw - 40px holds with room to spare down to a 320px window. */
  .door-line { font-size: clamp(2.2rem, 11.3vw, 4.6rem); }

  .door-lock {
    flex-direction: column;
    align-items: center;
    row-gap: 0.02em;
    transform: none;
  }
  .door-join { transform: translateX(calc(var(--reel-nudge) / 2)); }

  .door-scrim {
    /* Taller and narrower: the lockup is two lines here and reaches further up
     * and down, so the readable core follows it. */
    background:
      radial-gradient(135% 66% at 50% 46%,
        rgba(0, 0, 0, 0.78) 0%, rgba(0, 0, 0, 0.60) 46%, rgba(0, 0, 0, 0.15) 100%),
      linear-gradient(180deg, rgba(0, 0, 0, 0.55) 0, rgba(0, 0, 0, 0) 18%),
      linear-gradient(0deg,   rgba(0, 0, 0, 0.72) 0, rgba(0, 0, 0, 0) 30%);
  }

  /* NOT stretched to full width. Both actions together measure about 310px, which
   * fits a 375px screen side by side — and a pair of full-bleed stacked bars, one
   * of them solid white, is the loudest thing on a phone screen by a wide margin
   * and reads as a signup wall rather than a front door. They only stack on the
   * windows where they genuinely cannot sit together. */
  .door-acts .act { padding-inline: 1.1rem; }
}

/* ==========================================================================
 * 8. REDUCED MOTION
 *
 * The contract from seam.js applies: the RESOLVED state is what ships, and
 * motion is added on top only where it is welcome. Here that means the stage
 * stops entirely — one photograph, held — and the reel rests on the brand word.
 *
 * door.js does the other half (never spins, never plays the video); this block
 * covers the CSS-only montage and anything that got a class before the query
 * was read.
 * ====================================================================== */

@media (prefers-reduced-motion: reduce) {
  .door-plate { animation: none; opacity: 0; transform: scale(1.04); }
  .door-plate--first { opacity: 1; }
  .door-vid { transition: none; }
  .reel.is-spinning { -webkit-mask-image: none; mask-image: none; }
}

/* ==========================================================================
 * 9. PRINT
 *
 * seam.css §14 already hides the bar and flattens the page. A full-bleed video
 * stage prints as a black rectangle with white type knocked out of it, so the
 * stage drops to nothing and the words print as words.
 * ====================================================================== */

@media print {
  .door { min-height: 0; background: none; color: #000; }
  .door-stage, .door-scrim, .door-seam, .door-acts { display: none; }
  .door-line, .door-pure, .door-sub { color: #000; text-shadow: none; }
  .door-line { font-size: 2rem; }
  .reel { overflow: visible; height: auto; }
  .reel-word:not(:first-child) { display: none; }
}
