/* ashore. — one screen in two colours and a warm light.
   By night the page ground is --bg (the brand's deep, #262B5C), the words and everything drawn on
   it --fg (its cream, #FDF6E3), --glow is the pointer's light on the words and the flowers, and
   --gold (#F2C14E) the lit windows and the smoke. A click on the moon (assets/weather.js) sets
   html[data-mode="day"]: --bg a pale sky built from the cream and the tide (#8C9AD0), --fg the
   deep, --glow a deeper gold that reads on the sky, and --lo, the dark end of the scene's six
   tones t0–t5, a softer blue. A paper grain is baked into the page ground. The scene,
   assets/scenes/pixel.svg, fills the bottom of the screen, to its last row (assets/scene.js, which
   paints it again in the day's tones). The weather is a canvas behind it all and a second for the
   leaves, petals, smoke and flowers in front of the scene (assets/weather.js).

   --bg is always the page ground and --fg the strongest contrast to it.

   The rules the layout keeps:
   - Type: four sizes, the 16 px base, a 4:3 step down and up from it, and one display size
     (13, 16, 21, 48 at 1440 px), a tight
     leading on the big line (1.1) and an easy one on text (1.5), the measure under 66ch, and
     the tracking opened only on the small capitals.
   - Space: every gap is a multiple of --s (8 px at 1440 px).
   - Alignment: one spine down the middle for the words, one left and right edge (--edge) for
     the corner marks.
   - Shape: every control is a pill, drawn with an inset shadow rather than a border, so the
     line takes the colour behind it.
   - Scale: everything is sized from --u, one CSS pixel up to a 1440 px window and growing with
     it, so a big screen shows the same page larger rather than the same small form. */

/* The font: the variable Hanken Grotesk cut to Latin as WOFF2 (31 KB, the README says how), with
   the full TTF (133 KB) only for a browser without WOFF2. */
@font-face { font-family: "Hanken Grotesk"; src: url("fonts/HankenGrotesk-VF-latin.woff2") format("woff2"), url("fonts/HankenGrotesk-VF.ttf") format("truetype"); font-weight: 100 900; font-display: swap; }

:root {
  --ink: #262B5C;
  --paper: #FDF6E3;
  --gold: #F2C14E;
  --sky: #D3D9EC;
  --bg: var(--ink);
  --fg: var(--paper);
  --fg1: color-mix(in oklab, var(--fg) 80%, var(--bg));
  --fg2: color-mix(in oklab, var(--fg) 60%, var(--bg));
  --lo: var(--ink);
  --glow: var(--gold);
  /* The grain: fractal noise at 30% alpha, one 240 px tile, overlay-blended once into the page's
     static background (html, below). Nothing blends over the animated canvases or the scene. */
  --grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='240'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix values='.83 .83 .83 0 -.75 .83 .83 .83 0 -.75 .83 .83 .83 0 -.75 0 0 0 0 .3'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  /* Six even steps from paper to --lo: the scene's classes f0–f5 and s0–s5. */
  --t0: var(--paper);
  --t1: color-mix(in oklab, var(--paper) 80%, var(--lo));
  --t2: color-mix(in oklab, var(--paper) 60%, var(--lo));
  --t3: color-mix(in oklab, var(--paper) 40%, var(--lo));
  --t4: color-mix(in oklab, var(--paper) 20%, var(--lo));
  --t5: var(--lo);
  --sans: "Hanken Grotesk", system-ui, sans-serif;

  --u: clamp(1px, 0.0695vw, 2.2px);
  --s: calc(8 * var(--u));
  --edge: max(20px, calc(32 * var(--u)));
  --bar: calc(72 * var(--u));
  --fs-s: max(11px, calc(13 * var(--u)));
  --fs-m: max(13px, calc(16 * var(--u)));
  --fs-l: calc(21 * var(--u));
  --fs-xl: clamp(30px, 8vw, calc(48 * var(--u)));
  /* The scene's box, 5:2 like the scene: on tall screens it takes 44% of the height and the
     scene is cropped at the sides; it never takes more than two thirds, and never so much that
     the words (456 units tall from the top, to the platforms' last row) meet the hills: its
     top is cropped before that, from the sky. The words may run down over its top 18%, which
     every scene keeps as sky in the middle. */
  --ground: clamp(340px, min(max(40vw, 44vh), calc((100vh - 456 * var(--u)) / 0.82)), 66vh);
  --ground: clamp(340px, min(max(40vw, 44svh), calc((100svh - 456 * var(--u)) / 0.82)), 66svh);
}
:root[data-mode="day"] { --bg: var(--sky); --fg: var(--ink); --lo: #4A4F8C; --glow: #C28100; }
/* The change of mode is a cross-fade of the page (a view transition, assets/weather.js). */
::view-transition-old(root), ::view-transition-new(root) { animation-duration: 0.6s; }
/* On a desktop the unit also follows the window's height, so that a wide, short window shows
   the same page smaller rather than the words over the hills. At 16:10 it is the same as by the
   width. */
@media (min-width: 700px) and (min-height: 560px) {
  :root { --u: clamp(0.8px, min(0.0695vw, 0.1111vh), 2.2px); }
}

*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; background: var(--bg) var(--grain); background-blend-mode: overlay; }
body {
  margin: 0;
  overflow-x: hidden;
  font: 400 var(--fs-m)/1.5 var(--sans);
  color: var(--fg);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}
/* The grain lifts the ground: it is part of the page's background, so the browser blends it once
   into that layer, not over the canvases each frame. The scene's own fills stay flat. */

/* The weather: two canvases (assets/weather.js), each on a layer of its own and contained, so
   that what they draw never repaints the page, nor the page them. The first is rain, moon and
   birds, behind the scene; the second (inside .screen) is in front of the scene and behind the
   words, so the smoke, the leaves and the ferry pass behind the line, the form and the logos. */
.weather, .leaves { position: fixed; inset: 0; width: 100%; height: 100%; pointer-events: none; contain: strict; will-change: transform; }
.weather { z-index: 0; }
/* The front canvas is pixel art at one pixel per pixel of the scene, and on a touch screen of 2x
   or more the sky is at half its pixels (assets/weather.js): both are shown upscaled by a whole
   number without smoothing, so they stay crisp. */
.leaves, .lowres .weather { image-rendering: pixelated; }
.leaves { z-index: 1; }

/* The page is one screen: on a desktop (wide and tall enough) exactly the window's height, with
   nothing to scroll; the words over the sky, the scene across the bottom to the window's last
   row, the footer over the meadow. A phone, or a window too short for the words, lets the page
   grow and scroll instead. */
.screen {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  min-height: 100svh;
}
.hero {
  flex: 1 0 auto;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  position: relative;
  z-index: 2;
  padding: calc(var(--bar) + var(--s)) var(--edge) var(--s);
  text-align: center;
}
@media (min-width: 700px) and (min-height: 560px) {
  .screen { height: 100vh; height: 100svh; overflow: hidden; }
  .hero { flex: 1 1 0; min-height: 0; justify-content: safe center; }
}

/* The brand, top left, as in the wiki's top bar: the stone 28 px tall, "ashore." beside it, the
   stop in the quieter tone. The stone is the brand's white mark used as a mask, so it takes
   --paper. The bar is --bar tall. */
.brand {
  position: absolute;
  top: 0;
  left: max(var(--edge), env(safe-area-inset-left));
  z-index: 2;
  display: flex;
  align-items: center;
  gap: calc(10 * var(--u));
  height: var(--bar);
  color: var(--fg);
  text-decoration: none;
  border-radius: 999px;
}
.mark {
  display: block;
  height: calc(28 * var(--u));
  aspect-ratio: 54 / 88;
  background: var(--fg);
  -webkit-mask: url("brand/ashore-mark-white.svg") center / contain no-repeat;
  mask: url("brand/ashore-mark-white.svg") center / contain no-repeat;
}
.wordmark { display: block; width: calc(72 * var(--u)); height: auto; overflow: visible; }
.wordmark .letters { fill: var(--fg); }
.wordmark .stop { fill: var(--fg2); }
.brand:focus-visible { outline: 2px solid var(--glow); outline-offset: calc(6 * var(--u)); }

/* The one line: the slogan. */
.slogan {
  max-width: 15em;
  margin: 0;
  font-size: var(--fs-xl);
  font-weight: 500;
  line-height: 1.1;
  letter-spacing: -0.02em;
  text-wrap: balance;
}

/* The one line under the slogan, on the form's width so the words keep one spine. */
.about {
  max-width: calc(480 * var(--u));
  margin: calc(var(--s) * 2) 0 0;
  font-size: var(--fs-m);
  font-weight: 500;
  line-height: 1.5;
  text-wrap: balance;
}

/* The quiet line over the platforms. */
.label {
  margin: 0;
  font-size: var(--fs-s);
  font-weight: 500;
  line-height: 1.2;
  letter-spacing: 0.02em;
  color: color-mix(in srgb, var(--fg) 62%, transparent);
}

/* The form: a paper-ruled pill with the button nested in it. data-state is idle, sending, sent,
   error or invalid (assets/waitlist.js). The status line says the last three, under the field
   and out of the flow, in the gap above the platforms, so the form is as tall in every state;
   sent replaces the field with it. */
.join { position: relative; width: min(100%, calc(480 * var(--u))); margin-top: calc(var(--s) * 4); }
.join > label { display: block; margin-bottom: calc(var(--s) * 1.5); font-size: var(--fs-m); font-weight: 600; line-height: 1.2; }
.field {
  display: flex;
  height: calc(var(--s) * 7);
  min-height: 52px;
  padding: calc(var(--s) / 2);
  border-radius: 999px;
  box-shadow: inset 0 0 0 max(1px, var(--u)) color-mix(in srgb, var(--fg) 55%, transparent);
}
.field:focus-within { outline: 2px solid var(--glow); outline-offset: calc(var(--s) / 2); box-shadow: inset 0 0 0 max(1px, var(--u)) var(--fg); }
.field input {
  flex: 1;
  min-width: 0;
  padding: 0 calc(var(--s) * 2) 0 calc(var(--s) * 2.5);
  border: 0;
  border-radius: 999px;
  font: inherit;
  color: var(--fg);
  background: transparent;
  outline: none;
}
.field input::placeholder { color: var(--fg2); }
/* On a touch screen the field is never under 16 px, or iOS and iPadOS zoom into it on focus. */
@media (pointer: coarse) { .field input { font-size: max(16px, var(--fs-m)); } }
.field input:-webkit-autofill { -webkit-text-fill-color: var(--fg); box-shadow: inset 0 0 0 48px var(--bg); }
.field button {
  display: flex;
  align-items: center;
  gap: var(--s);
  padding: 0 calc(var(--s) * 3);
  border: 0;
  border-radius: 999px;
  font: inherit;
  font-weight: 600;
  color: var(--bg);
  background: var(--fg);
  cursor: pointer;
}
.field button svg { width: calc(16 * var(--u)); min-width: 14px; height: auto; }
@media (hover: hover) { .field button:hover { background: var(--fg1); } }
.field button:focus-visible { outline: 2px solid var(--glow); outline-offset: calc(var(--s) / 2); }
.field button:disabled { cursor: progress; opacity: 0.55; }
.status { position: absolute; top: 100%; left: 0; right: 0; margin: var(--s) 0 0; font-size: var(--fs-s); line-height: 1.5; color: var(--fg1); }
.join[data-state="sent"] .field, .join[data-state="sent"] > label { display: none; }
/* Sent, the block mirrors the form: "You're on the list." in the label's place and type, and
   "Save Ashore art" in the field's place and shape, so the page keeps its rhythm. */
.join[data-state="sent"] .status { position: static; display: block; margin: 0; color: var(--fg); }
.join[data-state="sent"] .status .say { display: block; margin-bottom: calc(var(--s) * 1.5); }
.status .done { font-size: var(--fs-m); font-weight: 600; line-height: 1.2; }
.hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }

/* Where it's coming: a quiet line and the platforms' own logos (Simple Icons, assets/platforms/)
   as one group, centred on one spine. Each logo is the ink's own shape (its viewBox cropped to
   it) at its own optical height (--k of 16 px), so the full squares of Windows and Switch do not
   outweigh the narrow Apple and the wide PlayStation, and the gaps between them are even, as
   the eye sees them. They are the words' colour at 72%. */
.coming { display: flex; flex-direction: column; align-items: center; gap: calc(var(--s) * 1.5); margin-top: calc(var(--s) * 4); }
.platforms { display: flex; align-items: center; justify-content: center; gap: calc(var(--s) * 2.5); margin: 0; padding: 0; list-style: none; }
.platforms li { display: flex; align-items: center; }
.logo {
  display: block;
  height: calc(var(--s) * 2 * var(--k, 1));
  aspect-ratio: var(--ar, 1);
  background: color-mix(in srgb, var(--fg) 72%, transparent);
  -webkit-mask: center / 100% 100% no-repeat;
  mask: center / 100% 100% no-repeat;
}
.logo.apple { --k: 1; --ar: 19.55 / 24; -webkit-mask-image: url("platforms/apple.svg"); mask-image: url("platforms/apple.svg"); }
.logo.windows { --k: 0.8; -webkit-mask-image: url("platforms/windows.svg"); mask-image: url("platforms/windows.svg"); }
.logo.steam { --k: 0.95; -webkit-mask-image: url("platforms/steam.svg"); mask-image: url("platforms/steam.svg"); }
.logo.playstation { --k: 0.83; --ar: 24 / 18.81; -webkit-mask-image: url("platforms/playstation.svg"); mask-image: url("platforms/playstation.svg"); }
.logo.xbox { --k: 0.95; -webkit-mask-image: url("platforms/xbox.svg"); mask-image: url("platforms/xbox.svg"); }
@media (max-width: 560px) {
  .platforms { gap: calc(var(--s) * 2); }
}

/* The scene: an inline SVG whose shapes carry only classes (these give them the page's colours),
   or for a pixel scene one canvas upscaled without smoothing, cropped like the SVG (slice, the
   bottom middle kept), with a hidden SVG of its canopies and moonflower spots over it
   (assets/scene.js). It ends on the window's last row, as solid meadow, with no fade. It is a
   layer of its own and contained, so the weather animating behind and in front of it never
   repaints it. */
.ground {
  position: relative;
  height: var(--ground);
  margin-top: calc(var(--ground) * -0.24);
  pointer-events: none;
}
#scene, #scene svg, #scene canvas { display: block; width: 100%; height: 100%; }
#scene {
  position: relative;
  contain: layout paint;
  will-change: transform;
}
#scene canvas { object-fit: cover; object-position: 50% 100%; image-rendering: pixelated; }
#scene .emit { position: absolute; inset: 0; visibility: hidden; }
#scene .f0 { fill: var(--t0); }
#scene .f1 { fill: var(--t1); }
#scene .f2 { fill: var(--t2); }
#scene .f3 { fill: var(--t3); }
#scene .f4 { fill: var(--t4); }
#scene .f5 { fill: var(--t5); }
#scene .s0 { stroke: var(--t0); }
#scene .s1 { stroke: var(--t1); }
#scene .s2 { stroke: var(--t2); }
#scene .s3 { stroke: var(--t3); }
#scene .s4 { stroke: var(--t4); }
#scene .s5 { stroke: var(--t5); }
#scene .fg { fill: var(--gold); }

/* The footer, small, over the meadow at the foot of the screen. */
.foot {
  position: absolute;
  right: 0;
  bottom: 0;
  left: 0;
  z-index: 2;
  margin: 0;
  padding: 0 var(--edge) max(calc(var(--s) * 2), env(safe-area-inset-bottom));
  font-size: var(--fs-s);
  letter-spacing: 0.04em;
  text-align: center;
  font-weight: 600;
  color: var(--ink);
  pointer-events: none;
}

/* Top right in the bar, the brand's counterpart: the tools. On a phone, night and day as a small
   switch of two pixel icons (the moon and the sun; the sky's own moon is not drawn there), and on
   every screen the sound. */
.tools {
  position: absolute;
  top: 0;
  right: max(var(--edge), env(safe-area-inset-right));
  z-index: 3;
  display: flex;
  align-items: center;
  gap: 4px;
  height: var(--bar);
  margin-right: calc(var(--s) * -1.25);
}
.modes { display: none; }
.mode {
  display: grid;
  place-items: center;
  width: 30px;
  height: 26px;
  padding: 0;
  border: 0;
  border-radius: 999px;
  color: var(--fg2);
  background: none;
  cursor: pointer;
  transition: color 0.3s, background-color 0.3s;
  -webkit-tap-highlight-color: transparent;
}
.mode svg { display: block; width: 13px; height: 13px; fill: currentColor; }
.mode[aria-pressed="true"] { color: var(--bg); background: var(--fg); }
.mode:focus-visible { outline: 2px solid var(--glow); outline-offset: 2px; }

/* The sound (assets/sound.js): a speaker in pixels, on the scene's own grid of 2 px, a step
   quieter than the brand. Off, its two waves are faint; on, it takes the brand's colour and the
   waves come and go, the near one and then the far, slowly, in steps, like the moon's shimmer.
   It shows once the script is ready. */
.sound {
  display: grid;
  place-items: center;
  width: calc(var(--s) * 5);
  min-width: 40px;
  height: var(--bar);
  padding: 0;
  border: 0;
  color: var(--fg2);
  background: none;
  cursor: pointer;
  transition: color 0.3s;
  -webkit-tap-highlight-color: transparent;
}
.sound .speaker { display: block; width: calc(22 * var(--u)); min-width: 22px; height: auto; fill: currentColor; overflow: visible; }
.sound .w1, .sound .w2 { opacity: 0.3; transition: opacity 0.4s; }
.sound:hover, .sound.waiting, .sound[aria-pressed="true"] { color: var(--fg); }
.sound.waiting .w1, .sound[aria-pressed="true"] .w1 { animation: wave 2.4s steps(1) infinite; }
.sound.waiting .w2, .sound[aria-pressed="true"] .w2 { animation: wave 2.4s steps(1) 0.6s infinite; }
.sound[aria-pressed="true"] :is(.w1, .w2) { opacity: 1; }
@keyframes wave { 0%, 70% { opacity: 1; } 71%, 100% { opacity: 0.35; } }
.sound:focus-visible { outline: 2px solid var(--glow); outline-offset: -4px; border-radius: 6px; }
@media (prefers-reduced-motion: reduce) { .sound .w1, .sound .w2 { animation: none !important; } }

/* The words turn to the warm glow near the pointer (assets/weather.js). Each of the headline,
   the paragraph and the labels carries a copy of itself over its own letters: the same text laid
   out the same way, in --glow, with no bloom or shadow, shown only inside a radial mask of about
   200 px that the script moves to the light (--mx, --my, in the copy's own box) and fades with
   --lit. So the letters themselves go the lit raindrops' colour at the pointer and back to
   their own with distance. */
.slogan, .about, .label, .join > label { position: relative; }
.glowcopy {
  position: absolute;
  inset: 0;
  pointer-events: none;
  user-select: none;
  opacity: calc(var(--lit, 0) * 0.8);
  -webkit-mask-image: radial-gradient(circle calc(200 * var(--u)) at var(--mx, -999px) var(--my, -999px), #000, rgba(0, 0, 0, 0.45) 45%, transparent);
  mask-image: radial-gradient(circle calc(200 * var(--u)) at var(--mx, -999px) var(--my, -999px), #000, rgba(0, 0, 0, 0.45) 45%, transparent);
}
.hero .glowcopy, .hero .glowcopy * { color: var(--glow); }

/* A phone held sideways (or any short, narrow window): one screen, nothing to scroll. A slimmer
   bar (the brand; night and day; the sound), and under it the words, compact, over the scene's
   sky: the line on one row, the description, the field without its label (the button says it;
   the label stays for screen readers), and "Coming to" on the logos' row. The scene takes the
   whole width at its own 5:2, set an eighth of its height below the window's foot (only meadow
   is lost), so the hills stay under the words. The input keeps 16 px, so iOS does not zoom. */
@media (orientation: landscape) and (max-height: 520px) and (max-width: 1000px) {
  :root { --u: 1px; --bar: 52px; --fs-xl: clamp(20px, 2.9vw, 26px); --ground: min(40vw, 100svh - 40px); }
  .screen { height: 100vh; height: 100svh; overflow: hidden; }
  .hero {
    flex: 0 0 auto;
    justify-content: flex-start;
    padding: var(--bar) max(var(--edge), env(safe-area-inset-right)) 0 max(var(--edge), env(safe-area-inset-left));
  }
  .slogan { max-width: none; }
  .about { margin-top: 2px; }
  .join { width: min(100%, 400px); margin-top: 10px; }
  .join > label { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
  .field { height: 46px; min-height: 0; }
  .coming { flex-direction: row; gap: 14px; margin-top: 10px; }
  .ground { position: absolute; right: 0; bottom: calc(var(--ground) * -0.12); left: 0; height: var(--ground); margin: 0; }
  .foot { padding-bottom: max(10px, env(safe-area-inset-bottom)); }
  /* Once you are on the list: the line, and "Get the wallpaper" beside it, as tall as the field. */
  .join[data-state="sent"] { width: min(100%, 460px); }
  .join[data-state="sent"] .status { display: flex; align-items: center; gap: 16px; }
  .join[data-state="sent"] .status .say { flex: none; margin: 0; }
  .join[data-state="sent"] .status .keep { flex: 1; height: 46px; min-height: 0; font-size: var(--fs-m); }
}

/* A phone upright: the words a little lower, further into the scene's sky. */
@media (orientation: portrait) and (max-width: 700px) {
  .ground { margin-top: calc(var(--ground) * -0.44); }
}
/* A phone, upright or sideways: night and day are the switch in the bar, beside the sound.
   --phone tells assets/weather.js (it draws no moon in the
   sky then), so the page has one definition of a phone. */
@media (max-width: 700px), (orientation: landscape) and (max-height: 520px) and (max-width: 1000px) {
  :root { --phone: 1; }
  .modes { display: inline-flex; padding: 2px; border-radius: 999px; box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--fg) 30%, transparent); }
  .moonbtn { display: none; }
}

/* Once you are on the list, "Get the wallpaper" in the field's place and shape, its words a size
   larger, opens the wallpaper. */
.status .keep[hidden] { display: none; }
.status .keep {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.55em;
  width: 100%;
  height: calc(var(--s) * 7);
  min-height: 52px;
  padding: 0 calc(var(--s) * 3);
  border: 0;
  border-radius: 999px;
  font: 600 var(--fs-l) / 1 var(--sans);
  letter-spacing: -0.01em;
  color: var(--bg);
  background: var(--fg);
  cursor: pointer;
  transition: background-color 0.25s;
}
.status .keep svg { width: 0.9em; height: 0.9em; fill: currentColor; }
@media (hover: hover) { .status .keep:hover { background: var(--fg1); } }
.status .keep:focus-visible { outline: 2px solid var(--glow); outline-offset: 4px; }

/* The wallpaper sheet (assets/weather.js), over the dimmed page, in the brand's cream and ink.
   It is sized on the page's own scale (--u, --s and the four type sizes), so it grows with the
   window as the page does, in every browser: a card in the middle of a screen, a sheet rising
   from the foot of a phone held upright, and on a phone held sideways the wallpaper beside its
   words and buttons. It holds the title, the two wallpapers in a row that swipes, a pager
   (previous, a dot each, next), and Share and Save. --stage is the wallpapers' height: what the
   card's width allows at 16:9, and no more than the window's height leaves for it. */
.keepsake {
  width: 100%;
  max-width: none;
  height: 100%;
  max-height: none;
  margin: 0;
  padding: 0;
  border: 0;
  color: var(--ink);
  background: transparent;
}
.keepsake[open] { display: grid; place-items: center; }
.keepsake::backdrop { background: rgba(18, 20, 48, 0.55); -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px); }
.sheet {
  --pad: calc(var(--s) * 4.5);
  --card: min(100vw - 32px, 880 * var(--u));
  --stage: min((var(--card) - 2 * var(--pad)) * 9 / 16, 100svh - 32px - 280 * var(--u));
  position: relative;
  width: var(--card);
  max-height: calc(100svh - 16px);
  overflow-y: auto;
  padding: var(--pad);
  border-radius: calc(var(--s) * 4);
  text-align: center;
  background: var(--paper);
  box-shadow: 0 24px 80px rgba(10, 12, 40, 0.4);
  animation: sheet-in 0.42s cubic-bezier(0.2, 0.85, 0.25, 1);
}
@keyframes sheet-in { from { transform: translateY(24px) scale(0.98); opacity: 0; } }
.sheet:focus { outline: none; }   /* it takes the focus on opening (autofocus), so no control shows a ring until Tab */
.sheet .grab { display: none; width: 36px; height: 5px; margin: 0 auto 14px; border-radius: 3px; background: color-mix(in srgb, var(--ink) 18%, transparent); }
.sheet .close, .pager .step {
  display: grid;
  place-items: center;
  width: max(36px, calc(var(--s) * 5));
  height: max(36px, calc(var(--s) * 5));
  padding: 0;
  border: 0;
  border-radius: 50%;
  color: var(--ink);
  background: color-mix(in srgb, var(--ink) 7%, transparent);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: opacity 0.2s, background-color 0.2s;
}
.sheet .close { position: absolute; top: calc(var(--s) * 2); right: calc(var(--s) * 2); }
.sheet .close svg, .pager .step svg { width: 40%; height: 40%; }
@media (hover: hover) { .sheet .close:hover, .pager .step:not(:disabled):hover { background: color-mix(in srgb, var(--ink) 14%, transparent); } }
.sheet h2 { margin: 0 0 calc(var(--s) * 2.5); font-size: var(--fs-l); font-weight: 600; line-height: 1.2; letter-spacing: -0.01em; }
.slides {
  display: flex;
  margin: 0 calc(-1 * var(--pad));
  padding-bottom: 6px;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
}
.slides::-webkit-scrollbar { display: none; }
.slides.dragging { scroll-snap-type: none; cursor: grabbing; user-select: none; }
@media (hover: hover) { .slides { cursor: grab; } }
.preview {
  display: flex;
  flex: 0 0 100%;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: calc(var(--s) * 1.25);
  height: calc(var(--stage) + var(--fs-s) * 1.3 + var(--s) * 1.25);
  margin: 0;
  padding: 0 var(--pad);
  scroll-snap-align: center;
}
.preview figcaption { font-size: var(--fs-s); font-weight: 500; line-height: 1.3; letter-spacing: 0.02em; color: color-mix(in srgb, var(--ink) 60%, transparent); }
.frame { position: relative; display: block; overflow: hidden; background: var(--ink); box-shadow: 0 6px 20px rgba(38, 43, 92, 0.2), 0 0 0 1px rgba(38, 43, 92, 0.08); }
.preview[data-shape="phone"] .frame { height: var(--stage); aspect-ratio: 2160 / 4680; border-radius: calc(var(--stage) * 0.07); }
.preview[data-shape="screen"] .frame { width: min(100%, var(--stage) * 16 / 9); aspect-ratio: 16 / 9; border-radius: calc(var(--s) * 1.5); }
.frame img { display: block; width: 100%; height: 100%; object-fit: cover; opacity: 0; transition: opacity 0.4s; }
.frame img.ready { opacity: 1; }
.frame .clock { position: absolute; top: 9%; left: 0; right: 0; display: flex; flex-direction: column; align-items: center; color: rgba(253, 246, 227, 0.92); pointer-events: none; }
[data-mode="day"] .frame .clock { color: rgba(38, 43, 92, 0.85); }
.frame .day { font-size: calc(var(--stage) * 0.026); font-weight: 500; letter-spacing: 0.02em; }
.frame .time { font-size: calc(var(--stage) * 0.11); font-weight: 600; line-height: 1.05; letter-spacing: -0.02em; }
.pager { display: flex; align-items: center; justify-content: center; gap: var(--s); margin: calc(var(--s) * 2) 0 calc(var(--s) * 2.5); }
.pager .step:disabled { opacity: 0.3; cursor: default; }
.dots { display: flex; }
.dots button { display: grid; place-items: center; width: max(22px, calc(var(--s) * 2.75)); height: max(22px, calc(var(--s) * 2.75)); padding: 0; border: 0; background: none; cursor: pointer; }
.dots button::before { content: ""; width: max(7px, var(--s)); height: max(7px, var(--s)); border-radius: 50%; background: color-mix(in srgb, var(--ink) 22%, transparent); transition: background-color 0.25s, transform 0.25s; }
.dots button[aria-current="true"]::before { background: var(--ink); transform: scale(1.2); }
.sheet .actions { display: grid; grid-template-columns: 1fr 1fr; gap: calc(var(--s) * 1.5); max-width: calc(560 * var(--u)); margin: 0 auto; }
.sheet .actions button {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--s);
  height: max(52px, calc(var(--s) * 7));
  padding: 0 calc(var(--s) * 2);
  border: 0;
  border-radius: 999px;
  font: 600 var(--fs-m) / 1 var(--sans);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: opacity 0.3s;
}
.sheet .actions svg { width: 1.15em; height: 1.15em; }
.sheet .share { color: var(--ink); background: none; box-shadow: inset 0 0 0 1.5px color-mix(in srgb, var(--ink) 28%, transparent); }
.sheet .save { color: var(--paper); background: var(--ink); }
/* While the 4K file in view is still being made, the buttons wait, dimmed. */
.sheet .actions.making button { opacity: 0.45; pointer-events: none; }
.sheet button:focus-visible, .slides:focus-visible { outline: 2px solid #C28100; outline-offset: 3px; }
/* A phone upright: a sheet from the foot, the full width, its wallpapers half the window tall. */
@media (max-width: 700px) and (orientation: portrait) {
  .keepsake[open] { place-items: end center; }
  .sheet {
    --pad: 20px;
    --card: 100vw;
    --stage: min(50svh, 440px);
    padding: 12px var(--pad) max(20px, env(safe-area-inset-bottom));
    border-radius: 28px 28px 0 0;
    animation-name: sheet-up;
  }
  .sheet .grab { display: block; }
  .sheet .close { top: 14px; right: 14px; }
  .sheet .actions { max-width: none; }
}
@keyframes sheet-up { from { transform: translateY(100%); } }
/* A phone sideways: the wallpaper on the left, as tall as the window allows; the title, pager and
   the buttons (one above the other) on the right. */
@media (orientation: landscape) and (max-height: 520px) {
  .sheet {
    --pad: 20px;
    --card: min(100vw - 32px, 780px);
    --stage: calc(100svh - 24px - 2 * var(--pad) - var(--fs-s) * 1.3 - var(--s) * 1.25);
    display: grid;
    grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr);
    grid-template-rows: 1fr auto auto 1fr;
    column-gap: 28px;
    max-height: calc(100svh - 24px);
    text-align: left;
  }
  .slides { grid-column: 1; grid-row: 1 / -1; }
  .sheet h2 { grid-column: 2; grid-row: 1; align-self: end; margin: 0; }
  .pager { grid-column: 2; grid-row: 2; justify-content: flex-start; margin: 10px 0 14px -8px; }
  .sheet .actions { grid-column: 2; grid-row: 3; grid-template-columns: 1fr; gap: 10px; max-width: none; margin: 0; }
  .sheet .actions button { height: 46px; }
}
@media (prefers-reduced-motion: reduce) { .sheet { animation: none; } }

/* A short phone upright (an iPhone SE, 375 x 667): the line a step smaller, with the gaps drawn
   in, so the cottage and its meadow still fit on the one screen. */
@media (orientation: portrait) and (max-width: 560px) and (max-height: 720px) {
  :root { --fs-xl: 28px; }
  .hero { padding-top: calc(var(--bar) + var(--s) / 2); }
  .about { margin-top: calc(var(--s) * 1.25); }
  .join { margin-top: calc(var(--s) * 3); }
  .coming { margin-top: calc(var(--s) * 3); }
}

/* Typing on a phone (data-typing, assets/waitlist.js, while the field has focus): what is below
   the field fades, and sideways (where the keyboard leaves a strip) the line and the description
   go too, so the field sits clear above the keyboard with the brand over it. */
[data-typing] .coming { opacity: 0; transition: opacity 0.3s; }
@media (orientation: landscape) and (max-height: 520px) and (max-width: 1000px) {
  [data-typing] .slogan, [data-typing] .about, [data-typing] .coming { display: none; }
}

/* ?fps: the frame times of the last two seconds, bottom left (assets/weather.js). */
.fps {
  position: fixed;
  left: max(var(--edge), env(safe-area-inset-left));
  bottom: calc(var(--s) * 2);
  z-index: 5;
  padding: 4px 8px;
  border-radius: 4px;
  font: 500 12px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace;
  color: #fff;
  background: rgba(0, 0, 0, 0.7);
  pointer-events: none;
}

/* The moon, a button over its sprite (assets/weather.js): clear, as big as the disc. */
.moonbtn {
  position: fixed;
  z-index: 3;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: transparent;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.moonbtn:focus-visible { outline: 2px solid var(--glow); outline-offset: 2px; }
