/* ==========================================================================
   Echoes Editions — Teaser
   Design system locked in DESIGN.md. Values here are not to be improvised:
   colours, spacing and type roles are the brand, not styling preferences.
   ========================================================================== */

/* --- Faces ---------------------------------------------------------------
   Jost     SIL Open Font License. Stands in for Futura PT: letterspaced
            UPPERCASE only — eyebrows, labels, buttons. Never running text.
   Seriffo  Titles and reading body, in place of Artifex CF. One weight (400)
            and no italic file, so the CSS must never ask for bold or italic:
            the browser would synthesise both, and a slanted, smeared serif is
            worse than not having the emphasis at all.
   Optima   Wordmark only, and the wordmark ships as outlined SVG — so no
            Optima file is ever loaded.
   ------------------------------------------------------------------------- */

@font-face {
  font-family: "Jost";
  src: url("../fonts/jost-VF.woff2") format("woff2-variations");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Seriffo";
  src: url("../fonts/seriffo-text.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

/* --- Tokens -------------------------------------------------------------- */

:root {
  /* Colour — never pure #000 or #FFF. Target ratio ~70% ivory/chalk,
     25% ink/graphite/stone, <=5% sienna. */
  --chalk: #f6f4ee;
  --ivory: #ece9e2;
  --stone: #d6d2c9;
  --greige: #8a867c;   /* muted text — 18px and above only, 3.1:1 on ivory */
  --graphite: #494640;
  --ink: #3a3532;
  --sienna: #8a4429;   /* accent only: eyebrows, rules, hover. Never body. */

  /* Spacing — base-8. Nothing off this scale. */
  --s8: 8px;
  --s16: 16px;
  --s24: 24px;
  --s32: 32px;
  --s48: 48px;
  --s64: 64px;
  --s96: 96px;
  --s128: 128px;
  --s160: 160px;

  --font-serif: "Seriffo", Georgia, "Times New Roman", serif;
  --font-label: "Jost", "Century Gothic", "Avenir Next", sans-serif;

  --edge: var(--s24);  /* page gutter, overridden at mobile */

  /* Semantic layer. Everything on the page paints from these four, never
     from the raw palette above — which is what lets the whole thing flip to
     ink-on-beige at the end by reassigning them in one place. Sienna is
     deliberately absent: the accent does not invert. */
  --bg: var(--ivory);
  --fg: var(--ink);
  --muted: var(--graphite);
  --rule: var(--stone);
}

/* The end of the page reverses the page. Transitioned rather than switched,
   so arriving at the subscription feels like the light changing. */
html.is-inverted {
  --bg: var(--ink);
  --fg: var(--ivory);
  --muted: var(--stone);
  --rule: var(--greige);
}

/* The subscription screen holds the grain at 0.25 on every breakpoint,
   against the 0.325 / 0.5 used through the rest of the page. Kept as its own
   class rather than folded into .is-inverted: the two happen at the same
   moment today, but they are separate decisions and moving one should not
   silently drag the other with it. */
html.is-cta { --grain: 0.25; }

html,
body,
.narrative,
.screen--hero,
.mark,
.meta-bar a,
.chapter,
.chapter p,
.block__eyebrow,
.edition__title,
.subscribe__input,
.subscribe__submit,
.consent span,
.subscribe__success,
.shot__no {
  transition: background-color 600ms ease, color 600ms ease,
              border-color 600ms ease;
}

/* --- Reset --------------------------------------------------------------- */

*,
*::before,
*::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background-color: var(--bg);
  color: var(--fg);
  font-family: var(--font-serif);
  font-weight: 400;
  font-size: 17px;
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow-x: hidden;
}

h1, p { margin: 0; }
img, svg { display: block; max-width: 100%; }

/* Analog grain, matched to the Figma frame's Noise effect: Multi, size 0.5,
   density 100%, opacity 25%.

   "Multi" is coloured noise, so unlike the earlier pass there is no saturate
   filter here — the speckle carries its own colour, which is what gives the
   warm palette its slight instability. Size 0.5 is fine grain, hence the high
   baseFrequency, and stitchTiles keeps the 200px tile from showing a seam.

   Both effects are dialled per breakpoint, not globally: a phone screen is
   held closer and its pixels are smaller, so the same blur reads as heavier
   and the same grain reads as lighter than on a desktop display. Hence the
   softer blur and stronger noise under 768px. */
:root {
  --grain: 0.325;
  --blur: 0.35px;
}

body::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 200;
  pointer-events: none;
  opacity: var(--grain);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23g)'/%3E%3C/svg%3E");
  background-size: 200px 200px;
}

/* Layer blur, from the Figma frame's Uniform blur.

   Applied to <main> and not to <body>: a filter makes the element a
   containing block for fixed-position descendants, which would tear the
   wordmark and the meta line out of the viewport and dump them into the
   document flow. Both live outside <main> precisely so this can be done. */
main { filter: blur(var(--blur)); }

/* --- Persistent frame ----------------------------------------------------
   The wordmark and the meta line do not scroll away. They are the frame the
   three screens pass through — the "held emptiness" of DESIGN.md §Layout.
   ------------------------------------------------------------------------- */

.brand-bar {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 100;
  padding: var(--edge) var(--edge) 0;
  pointer-events: none;
}

/* The wordmark is painted as a mask rather than an <img> so its colour is a
   CSS property. That is the only reason the inversion at the end of the page
   can reach it — an <img> carries its fill baked into the file. */
.mark {
  display: block;
  width: 100%;
  aspect-ratio: 353 / 27;
  background-color: var(--fg);
  -webkit-mask-image: url("../img/wordmark.svg");
  mask-image: url("../img/wordmark.svg");
  -webkit-mask-size: 100% 100%;
  mask-size: 100% 100%;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  margin-left: auto;
  margin-right: auto;
}

/* The large mark is clipped by JS to the hero's bottom edge: the edge slides
   up over it and the wordmark disappears behind it, rather than shrinking.
   clip-path, not opacity — the point is a hard moving boundary, not a fade. */
.brand-bar--large { z-index: 101; }

/* The small mark is a cross-fade tied to the first chapter, sized to 60% of
   the reading column by narrative.js. Hidden by default so a failed script
   leaves the large one visible rather than nothing at all. */
.brand-bar--small { opacity: 0; }
.brand-bar--small .mark { width: 60%; }

/* Without JS neither wipe nor fade can run, so keep one mark and drop the
   duplicate. */
.no-js .brand-bar--small { display: none; }

.meta-bar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 100;
  display: flex;
  justify-content: space-between;
  align-items: flex-end;
  gap: var(--s24);
  padding: var(--edge);
}

.meta-bar__right {
  display: flex;
  align-items: center;
  gap: 10px;
}

/* Eyebrow: the brand's structural layer. Jost caps, wide tracking, never
   lowercase. Graphite not greige — greige fails contrast below 18px. */
.eyebrow {
  font-family: var(--font-label);
  font-weight: 500;
  font-size: 11px;
  letter-spacing: 0.16em;
  line-height: 1.2;
  text-transform: uppercase;
  color: var(--muted);
}

.meta-bar a {
  color: var(--muted);
  text-decoration: none;
  transition: color 240ms ease;
}

.meta-bar a:hover { color: var(--sienna); }
.meta-bar__sep { color: var(--rule); }

/* --- Screens -------------------------------------------------------------
   svh, not vh: on iOS Safari 100vh includes the address bar and crops the
   bottom of every screen. The @supports block below covers older engines.
   ------------------------------------------------------------------------- */

.screen {
  position: relative;
  width: 100%;
  min-height: 100svh;
  display: flex;
  align-items: center;
  justify-content: center;
}

@supports not (height: 100svh) {
  .screen { min-height: 100vh; }
}

/* Screen 1 — hero */

.screen--hero {
  background-color: var(--bg);
  background-image: url("../img/hero.webp");
  background-size: cover;
  /* Anchored to the bottom edge: the landscape crop is taller than most
     desktop viewports, and centring it cuts the feet off the pieces. */
  background-position: 50% 100%;
  background-repeat: no-repeat;
}

/* The hero photograph is pale and busy exactly where the meta line sits.
   A warm ivory scrim lifts the type without darkening the image —
   the reverse-text rule from DESIGN.md §Color. */
.screen--hero::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 22%;
  pointer-events: none;
  background-image: linear-gradient(
    to top,
    rgba(236, 233, 226, 0.78),
    rgba(236, 233, 226, 0)
  );
}

/* --- The narrative -------------------------------------------------------
   A tall section whose only job is to give the sticky stage room to travel.
   Its height is set by JS from the number of images present, so it is not
   declared here — leaving it unset means a failed script degrades to a
   normal, readable page rather than a blank one.
   ------------------------------------------------------------------------- */

.narrative {
  position: relative;
  background-color: var(--bg);
}

.stage {
  position: sticky;
  top: 0;
  height: 100svh;
  overflow: hidden;
}

@supports not (height: 100svh) {
  .stage { height: 100vh; }
}

/* Text layer, pinned to the centre for the whole chapter. */
.stage__texts {
  position: absolute;
  inset: 0;
  z-index: 1;
  display: grid;
  place-items: center;
  padding: var(--s96) var(--s32);
}

.chapter {
  grid-area: 1 / 1;
  width: 100%;
  /* Widened with the type: at 20px a 600px column runs about 50 characters,
     well under the 65–75 that DESIGN.md §Layout calls for. */
  /* At 35px even a 700px column ran barely 28 characters a line. 960 brings
     it back towards the measure DESIGN.md §Layout asks for. The small
     wordmark is sized from this number in narrative.js — keep them in step. */
  max-width: 960px;
  text-align: center;
  will-change: opacity, transform;
}

/* Image layer, above the text — the overlay is the point of the sequence. */
.stage__shots {
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
}

/* top stays 0: the vertical position is the travel itself, written per frame
   as a transform. --x sets the column the image passes through, --w its size.
   --y is left in the markup as a note of the Figma placement but is no longer
   read — nothing is parked at a fixed height any more. */
.shot {
  position: absolute;
  left: var(--x);
  top: 0;
  width: var(--w);
  /* --x is the left edge, so anything wider than the space remaining to the
     right of it would hang off the canvas. This makes that impossible at any
     viewport, which matters most on phones where the widths are scaled up. */
  max-width: calc(96% - var(--x));
  margin: 0;
  opacity: 0;
  will-change: opacity, transform;
}

.shot img {
  width: 100%;
  height: auto;
  box-shadow: 0 8px 40px rgba(58, 53, 50, 0.12);
}

/* The index number beside each image, as drawn in the Figma. Sits just off
   the top-right corner so it reads as an annotation on the plate rather than
   a label inside the photograph. */
.shot__no {
  position: absolute;
  top: 10%;
  left: calc(100% + 6px);
  font-family: var(--font-label);
  font-weight: 500;
  font-size: 10px;
  line-height: 1;
  letter-spacing: 0.08em;
  color: var(--muted);
  white-space: nowrap;
}

/* The subscription, rising last. */
.stage__cta {
  position: absolute;
  inset: 0;
  z-index: 3;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  padding: var(--s96) var(--s32);
  text-align: center;
  opacity: 0;
  will-change: opacity, transform;
}

.stage__cta .subscribe { width: 100%; max-width: 420px; }
.stage__cta .consent { justify-content: center; text-align: left; }

/* Fallback: reduced motion, or no JS. Everything stacks and reads as a normal
   page. The words are the point; the choreography is not load-bearing. */
.narrative.is-static .stage,
.no-js .narrative .stage {
  position: static;
  height: auto;
  overflow: visible;
}

.narrative.is-static .stage__texts,
.no-js .narrative .stage__texts {
  position: static;
  display: block;
}

.narrative.is-static .chapter,
.no-js .narrative .chapter {
  margin: 0 auto var(--s128);
  opacity: 1;
}

.narrative.is-static .stage__shots,
.no-js .narrative .stage__shots {
  position: static;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--s32);
  max-width: 960px;
  margin: 0 auto var(--s128);
}

.narrative.is-static .shot,
.no-js .narrative .shot {
  position: static;
  width: auto;
  opacity: 1;
}

.narrative.is-static .stage__cta,
.no-js .narrative .stage__cta {
  position: static;
  opacity: 1;
  padding-bottom: var(--s160);
}

.block__eyebrow {
  font-family: var(--font-label);
  font-weight: 500;
  font-size: 12px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--sienna);
  margin-bottom: var(--s32);
}

.chapter p {
  font-size: 35px;
  line-height: 1.35;
  color: var(--fg);
}

.chapter p + p { margin-top: var(--s24); }

.edition__title {
  font-family: var(--font-serif);
  font-weight: 400;
  font-size: 80px;
  line-height: 1.05;
  letter-spacing: -0.015em;
  color: var(--fg);
}

/* Russet on both palettes: the accent is the one thing that does not
   invert with the rest of the page. */
.edition__eyebrow {
  font-family: var(--font-label);
  font-weight: 500;
  font-size: 12px;
  letter-spacing: 0.34em;
  text-transform: uppercase;
  color: var(--sienna);
  margin-top: var(--s16);
}

/* --- Subscribe ----------------------------------------------------------- */

.subscribe { margin-top: var(--s96); }

/* Desktop carries the invitation inside the field, as drawn. Mobile promotes
   it to a label — see the 767px block. */
.subscribe__label { display: none; }

.subscribe__row {
  display: flex;
  align-items: center;
  gap: var(--s16);
  padding-bottom: var(--s16);
  border-bottom: 1px solid var(--rule);
  transition: border-color 240ms ease;
}

.subscribe__row:focus-within { border-bottom-color: var(--fg); }

.subscribe__input {
  flex: 1;
  min-width: 0;
  padding: 0;
  border: 0;
  background: transparent;
  font-family: var(--font-label);
  font-weight: 500;
  font-size: 12px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--fg);
}

.subscribe__input:focus { outline: none; }
.subscribe__input::placeholder { color: var(--muted); opacity: 1; }

.subscribe__submit {
  flex: none;
  width: 32px;
  height: 20px;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--fg);
  cursor: pointer;
  transition: color 240ms ease, transform 240ms ease;
}

.subscribe__submit:hover {
  color: var(--sienna);
  transform: translateX(4px);
}

.subscribe__submit svg { width: 20px; height: auto; }
.subscribe__submit:disabled { opacity: 0.4; cursor: default; transform: none; }

/* Marketing consent is not optional under GDPR, and an address collected
   without it cannot legally be mailed. */
.consent {
  display: flex;
  align-items: flex-start;
  gap: var(--s8);
  margin-top: var(--s16);
  cursor: pointer;
}

/* Aligned to the centre of the FIRST line of the disclaimer, not to the
   block: the text wraps to two lines, so centring against the whole thing
   would leave the box floating between them.

   The offset is derived from the label's own metrics rather than typed as a
   number — font-size here matches the span, so 1.45em resolves to the same
   line box the text uses and the alignment survives a change of type size. */
.consent input {
  flex: none;
  width: 13px;
  height: 13px;
  font-size: 11px;
  margin: calc((1.45em - 13px) / 2) 0 0;
  accent-color: var(--sienna);
}

/* 11px, not 10. The grain now runs at 0.45 across the whole page, and the
   speckle sits inside the counters of small uppercase type. This line carries
   the marketing consent — it is the one piece of text on the page that has to
   survive the effect. */
.consent span {
  font-family: var(--font-label);
  font-weight: 400;
  font-size: 11px;
  line-height: 1.45;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}

.consent a { color: var(--muted); }

.subscribe__note {
  margin-top: var(--s16);
  font-family: var(--font-label);
  font-size: 10px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--sienna);
  min-height: 15px;
}

/* Says "check your inbox", not "here is the chapter": with double opt-in on,
   the first email is the confirmation. Promising the chapter would be a lie
   the visitor can measure. */
.subscribe__success {
  display: none;
  margin-top: var(--s96);
  font-size: 14px;
  color: var(--muted);
}

.is-sent .subscribe { display: none; }
.is-sent .subscribe__success { display: block; }

/* --- Motion --------------------------------------------------------------
   The narrative sets opacity and transform inline from the scroll position,
   so there is nothing to transition here: a CSS transition would fight the
   per-frame writes and lag behind the scroll. This block only handles the
   case where the visitor has asked for no motion at all.
   ------------------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* --- Responsive ---------------------------------------------------------- */

@media (max-width: 767px) {
  :root {
    --edge: var(--s16);
    --grain: 0.5;    /* doubled: fine noise disappears on a dense phone screen */
    --blur: 0.25px;  /* halved: the same radius reads far heavier up close */
  }

  /* The portrait crop. Without this the phone downloads the 2560px landscape
     and lets background-size:cover throw most of it away. Back to centre now
     the 9:16 crop matches the shape of a phone viewport — the off-centre
     anchor was compensating for a source that was too wide. */
  .screen--hero {
    background-image: url("../img/hero-mobile.webp");
    background-position: 50% 50%;
  }

  .chapter p { font-size: 16px; }
  .stage__texts { padding: var(--s64) var(--s32); }
  .stage__cta { padding: var(--s64) var(--s32); }
  .subscribe { margin-top: var(--s48); }
  .subscribe__success { margin-top: var(--s48); }

  /* The scatter is tuned for a wide canvas, so on a phone every shot is
     scaled up — 2.21 is the old 1.7 plus 30%. min-width lifts only the
     smallest plates, which were arriving too slight to register; the cap
     stays exactly where it was, so nothing grows at the top end. */
  .shot { width: calc(var(--w) * 2.21); min-width: 24%; }

  /* The subscription box, tightened. */
  /* Scoped to .stage__cta to outrank the 420px cap set on the same element
     outside this media query — specificity beats source order, so a bare
     .subscribe here would silently lose. */
  .stage__cta .subscribe {
    max-width: 260px;
    margin-left: auto;
    margin-right: auto;
    margin-top: 0;
    padding-top: 60px;
  }

  .edition__title { font-size: 60px; }
  .edition__eyebrow { margin-top: 5px; }

  .subscribe__label {
    display: block;
    margin-bottom: var(--s8);
    font-family: var(--font-label);
    font-weight: 500;
    font-size: 12px;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--muted);
  }

  /* 10px is a deliberate choice, not an oversight: iOS Safari zooms the
     whole page when a focused input is under 16px, so tapping this field
     will nudge the layout on an iPhone. Raise it back to 16px to stop that. */
  .subscribe__input {
    font-size: 10px;
    letter-spacing: 0.06em;
  }

  .subscribe__row { padding-bottom: var(--s8); gap: var(--s8); }
  .subscribe__submit { width: 24px; }
  .subscribe__submit svg { width: 16px; }
  .consent span { font-size: 10px; line-height: 1.4; }
  .consent input { width: 12px; height: 12px; font-size: 10px; margin-top: calc((1.4em - 12px) / 2); }
}

@media (max-width: 767px) {
  /* The two links stack in the bottom-right corner, Instagram above the
     address. column-reverse rather than reordering the markup: the source
     order stays email-then-Instagram, which is the order a screen reader
     should read them in and the order the desktop line uses. */
  .meta-bar__right {
    flex-direction: column-reverse;
    align-items: flex-end;
    gap: 4px;
  }

  .meta-bar__sep { display: none; }

  /* Bottom left breaks to two lines rather than running as one long string
     across a narrow screen. */
  .meta-bar__left span { display: block; }
}

@media (max-width: 479px) {
  .meta-bar { font-size: 9px; }
  .eyebrow { font-size: 9px; letter-spacing: 0.1em; }
}
