/* Swanstant webinar page, 30 September 2026.
   Copy from WEBINAR_PAGE_COPY_draft3.md only.
   No dark sections anywhere. Typography carries contrast, not colour.
   Swanstant blue #0A0C40 never signals state or emphasis at small sizes. */

@font-face { font-family: "Cormorant Garamond"; src: url("assets/CormorantGaramond.b20b7d9626.ttf") format("truetype"); font-weight: 300 700; font-display: swap; }
@font-face { font-family: "DM Sans";           src: url("assets/DMSans.8cd08d97e8.ttf")           format("truetype"); font-weight: 300 700; font-display: swap; }
@font-face { font-family: "JetBrains Mono";    src: url("assets/JetBrainsMono.48715a42ec.ttf")    format("truetype"); font-weight: 300 700; font-display: swap; }

:root {
  --serif: "Cormorant Garamond", Georgia, serif;
  --sans:  "DM Sans", system-ui, -apple-system, sans-serif;
  --mono:  "JetBrains Mono", ui-monospace, Menlo, monospace;

  --ink:   #1a1a1a;   /* headlines, body, primary */
  --body:  #4a4a4a;   /* secondary text */
  --muted: #8a8a8a;   /* labels */
  --faint: #a3a3a3;   /* source lines */
  --hair:  #d9d9d9;   /* rules */
  --rule:  #ececec;
  --rest:  #bdbdbd;   /* navigator resting tone */
  --page:  #ffffff;
  --navy:  #0A0C40;   /* links and the wordmark only. Never state, never small emphasis. */

  --gutter: 16px;
  --veil-ms: 280ms;
  --nav-w: 720px;     /* navigator needs 720px for its buffer label to read */
  /* Share of the stage a figure takes when it sits BESIDE the text rather
     than in a full-width band. page.js reads this to decide which
     composition gives a figure more area, so the two cannot drift. */
  --side-w: 74%;

  /* A stated type scale, ratio 1.125. The interpretation used to top out at
     21 px; two steps down is 16.875, and the figure title sits one step below
     that at 15. Setting them as one token and a ratio means the relationship
     cannot drift when either is nudged. */
  --step: 1.125;
  --t-figtitle: 15px;
  --t-interp: calc(var(--t-figtitle) * var(--step));
}

/* The page is white throughout. No dark section, in any theme. A forced dark
   scheme would invert the figures, which bake white in, so it is declined. */
html { color-scheme: only light; background: var(--page); }

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

html, body { margin: 0; padding: 0; background: var(--page); color: var(--ink); }
body {
  font-family: var(--sans);
  font-size: 17px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

/* NO scroll snapping. It was added to stop a presenter resting on a
   half-cleared sheet, but the slideshow made it harmful: the step positions
   are not snap points, so proximity snapping dragged every step of a frame
   back to the same place and the figures never advanced. The steep sheet
   easing and the discrete step bands already give settled states to rest on.
   Smoothness is applied per call in page.js, never globally, because a global
   smooth scroll-behavior fights programmatic scrolling in Chrome. */

a { color: var(--navy); text-underline-offset: 2px; }
a:focus-visible, button:focus-visible, [tabindex="0"]:focus-visible {
  outline: 2px solid var(--ink); outline-offset: 3px; border-radius: 2px;
}
/* A tabindex="-1" element is a PROGRAMMATIC focus target, not a control.
   Moving between frames focuses the frame's headline so a keyboard or a screen
   reader lands on the new frame, and the generic [tabindex] rule then drew a
   2 px box round the heading. The announcement is the point; the ring is not.
   The same applies to the panel, which is focused when it opens. */
h1:focus, h2:focus, h3:focus, .headline:focus, [tabindex="-1"]:focus,
h1:focus-visible, h2:focus-visible, h3:focus-visible,
.headline:focus-visible, [tabindex="-1"]:focus-visible { outline: none; }

/* ---------------------------------------------------------------- masthead */
/* The title in its top-left position and the navigator both persist, and they
   are placed so they never collide: the title sits in its own row above the
   navigator rail, not beside it. */
/* A flat white bar, not a gradient. A gradient over white content reads as a
   grey band at some scales and serves no purpose here. */
.masthead {
  position: fixed; inset: 0 0 auto 0; z-index: 60;
  display: grid; grid-template-columns: auto 1fr auto;
  align-items: center; gap: 20px;
  padding: 14px var(--gutter) 12px;
  background: var(--page);
  pointer-events: none;
}
/* The running title sits in the masthead in DM Sans, so Cormorant is reserved
   for frame headlines and the wordmark. */
/* The running head appears from frame 2. On frame 1 the headline says it. */
.masthead-title {
  font-family: var(--sans); font-size: 14px; line-height: 1.2;
  color: var(--body); margin: 0; white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis;
  opacity: 0; transition: opacity 240ms linear;
}
body.past-one .masthead-title { opacity: 1; }
/* The frame counter lives here, not in the stage. It used to sit in each
   frame's own eyebrow row, inside the band the navigator occupies, which is
   why that band could not be made opaque: an opaque navigator would have
   hidden the counter. In the masthead it is a running head with a page count,
   which is what it always was. */
.masthead-count {
  font-family: var(--sans); font-size: 11px; letter-spacing: .06em;
  text-transform: uppercase; color: var(--faint); margin: 0;
  justify-self: end; white-space: nowrap;
}
.masthead > * { pointer-events: auto; }
/* The wordmark is now the brand mark rather than set text. There is no SVG
   master and no PNG master in the brand files, only an 890 by 284 JPEG, so
   these PNGs are DOWNSCALED from it, and trimmed to the mark's ink bounding
   box so the surrounding white does not eat the masthead's height: at 26, 52
   and 78 px tall that is an 8.0x, 4.0x and 2.7x reduction, which suppresses the
   JPEG's artefacts rather than carrying them. A vector master should replace
   this before the page is hosted. */
.wordmark {
  display: inline-flex; align-items: center;
  text-decoration: none; line-height: 0;
}
.wordmark img { height: 26px; width: auto; display: block; }
/* The partnership credit belongs to the TITLE CARD, not to the frame. It sits
   at the foot of the opening state, small and quiet, and it lifts with the
   title on the first scroll rather than following the figures down the frame.
   It sits above the sheet so it is visible while the sheet is up, and it is
   absolutely placed, so it never takes height from the figure and the rest of
   Frame 1 needs no adjustment to clear it. */
.context {
  position: absolute; left: var(--gutter); right: var(--gutter); bottom: 14px;
  z-index: 6; margin: 0;
  font-family: var(--sans); font-weight: 300; font-size: 12px; line-height: 1.4;
  color: var(--faint);
}
/* No CSS transition: page.js drives the opacity from the sheet's own easing,
   so the credit and the title move together at any scroll speed. The reduced
   motion view has no sheet, so the credit is simply shown there. */
.reduced .context { opacity: 1 !important; visibility: visible !important; }


/* ------------------------------------------------------------- navigator */
/* Persistent from frame 2. Active stage marked by TONE, black against light
   grey, never by colour: #0A0C40 sits about 1.06 to 1 against black and would
   not read as different. */
/* AN OPAQUE BAND, FULL WIDTH. The navigator's inline SVG carries a white plate
   across its whole 720 by 50 canvas, and that canvas is centred, so it starts
   at x 396 and ends at 1116 on a 1512 px screen. A departing headline runs to
   about x 674, so the plate erased the MIDDLE of the line and left both ends
   showing: Frame 5 read "Critical buffers stand between t" for the length of a
   viewport of scrolling at every frame boundary. A band that covers the full
   width instead means the line passes behind chrome and reads as chrome, the
   same way it already does under the masthead. The shadow fills the six pixel
   gap between this band and the masthead's, with no blur and no layout cost,
   so the two read as one piece. */
.navrail {
  position: fixed; top: 58px; left: 0; right: 0; z-index: 58;
  display: flex; justify-content: center;
  padding: 0 var(--gutter);
  background: var(--page);
  box-shadow: 0 -6px 0 var(--page);
  opacity: 0; transition: opacity var(--veil-ms) linear;
  pointer-events: none;
}
.navrail.on { opacity: 1; }
.navrail img, .navrail > .navwrap > svg { width: min(var(--nav-w), 100%); height: auto; display: block; }
/* Below 606px the stage labels fall under 8px and stop reading, so the rail is
   withdrawn rather than shown illegibly. */
@media (max-width: 660px) { .navrail { display: none; } }

/* ---------------------------------------------------------------- frames */
.frame { position: relative; }
.frame--tall { height: 190svh; }   /* svh, not vh: a mobile browser's vh is the
     tall viewport, so a stage sized in vh is taller than the screen whenever
     the address bar is showing, and the choreography loses its last step. */
/* Two rows, not three. The interpretation OVERLAYS the visual on a white
   plate at bottom left, which is what the design briefing specifies, and it is
   also the only way the arithmetic works: with a third row the visual was 349
   to 415 px tall in a 799 px viewport, so figures rendered at a quarter scale
   and their labels fell to about 8 CSS px against an 11 px target. Giving the
   figure the whole stage below the headline roughly doubles the row. */
.stage {
  position: sticky; top: 0; height: 100svh;
  display: grid;
  grid-template-rows: auto auto 1fr;
  row-gap: 8px;
  padding: 76px var(--gutter) 12px;
  overflow: hidden;
}

/* The eyebrow is now a RESERVE, not a line of type. Its content moved to the
   masthead; the element stays so the stage's three grid rows keep the heights
   the figures were measured against. Removing it would give every figure 27 px
   more and move every headline up, which is a change to the whole page rather
   than to its header. Reclaiming that height is listed for after the webinar. */
.frame-eyebrow {
  font-family: var(--sans); font-size: 11px; letter-spacing: .06em;
  text-transform: uppercase; color: var(--muted); margin-bottom: 10px;
  display: flex; gap: 14px; align-items: baseline;
  visibility: hidden;
}
/* Briefing 3: a visible frame count. */
.framecount { color: var(--faint); letter-spacing: .04em; }

/* The headline begins alone on the white layer, centred, then moves top left
   as the layer clears. Both positions are real layout, not a transform hack,
   so it stays selectable and screen readers see one element. */
 /* NEVER transition font-size or max-width. Animating them reflows the text on
   every frame, so line breaks land in the wrong places part way through and a
   character can sit outside the clipped stage for a few frames. It reads as
   truncation. Transform and opacity only. */
.headline {
  font-family: var(--serif); color: var(--ink);
  font-size: clamp(19px, 2.1vw, 30px); line-height: 1.14;
  margin: 0; max-width: min(46ch, 66%);
}
.headline .desc {
  display: block; font-family: var(--sans); font-weight: 300; color: var(--body);
  font-size: clamp(14px, 1.5vw, 19px); line-height: 1.5; margin-top: 16px; max-width: 46ch;
}
/* The headline is laid out ONCE, at its settled size and position, and is
   scaled up while the sheet is over the figures. Scale is a transform, so the
   text never reflows and line breaks cannot move part way through. */
.headline { position: relative; z-index: 6; transform-origin: 0 0; will-change: transform; }
.stage[data-cleared="1"] .headline .desc { display: none; }

/* The white sheet. It carries no text of its own: there is ONE headline per
   frame and it sits above the sheet, so the two can never duplicate. */
.sheet {
  position: absolute; inset: 0; background: var(--page); z-index: 5;
  /* Its own compositing layer. page.js writes this element's opacity on every
     scroll frame, and without a layer that is a full-viewport repaint of the
     stage behind it each time; with one it is a compositor property change and
     the main thread does no paint work at all. */
  will-change: opacity;
}

/* The figure's title is PAGE TEXT, not drawn on the artefact, so one line sits
   above the visual and changes with the step. Its height is reserved whether or
   not the current step has a title, because a row that appears and disappears
   would resize the figure under the reader on every step. Two figures carry no
   title by decision: the framework diagram, whose title is the frame headline,
   and the two-timescale water figure, whose point the short copy makes. */
.figtitle {
  font-family: var(--sans); font-size: var(--t-figtitle); line-height: 1.35;
  color: var(--ink); margin: 0 0 6px; min-height: 1.35em;
}
/* A frame where NO figure has a title gives the row back rather than reserving
   an empty line: Frame 2's diagram is titled by the frame headline, and holding
   30 px for a line that never fills cost it 8.7 points of the stage. The class
   is set once per frame, from the markup, so the reserve still cannot appear
   and disappear between steps within a frame.
   It collapses, it does NOT display:none. The stage is a three-row grid and the
   visual is the 1fr row; taking the title out of flow moved the visual up into
   an auto row, which content-sized it and collapsed Frame 2's figure to 300 by
   105 px, 3 per cent of the stage. */
.stage:not(.has-figtitle) .figtitle {
  min-height: 0; height: 0; margin: 0; overflow: hidden;
}
@media (max-width: 900px) { :root { --t-figtitle: 14px; } }

/* -------------------------------------------------------------- visuals */
 /* The visual occupies the stage's 1fr row. Its figures are sized BY THAT ROW,
   not by the viewport: sizing them at 58vh while the row was as little as 303px
   let them overflow downward and render on top of the interpretation.

   align-content must NOT be `center` here. That makes the grid rows
   content-sized, so a percentage height on a figure becomes circular and
   resolves to zero, which silently collapsed two of Frame 6's three figures.
   The row stretches; the figure fills it; the media is capped inside it. */
.visual {
  display: grid; gap: 18px; justify-items: center; align-items: stretch;
  min-height: 0; overflow: hidden; padding: 4px 0 0;
}
/* Reserve the interpretation plate's band so a figure never renders under it.
   A figure's own source line has to stay readable; the plate may sit over
   empty stage, never over type. */
/* Reserve the interpretation plate. It is now ONE row, about 56 px, not the
   156 px stacked block it replaced, so the evidence gets that height back. */
@media (min-width: 901px) { .visual { padding-bottom: 72px; } }
.visual--three { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.visual--two   { grid-template-columns: repeat(2, minmax(0, 1fr)); }
/* The triptych keeps its three columns at every width. It used to hide the
   second and third tile below 900 px, because the slideshow was desktop only
   and the stacked fallback would otherwise have shown three maps in a column.
   The slideshow now runs on touch, so the tiles stay and simply get narrower. */
@media (max-width: 900px) {
  .visual--three { gap: 10px; }
}
.visual figure {
  margin: 0; min-width: 0; min-height: 0; max-width: 100%; height: 100%;
  display: flex; align-items: center; justify-content: center;
}
.visual img, .visual video, .visual > figure > svg {
  max-width: 100%; max-height: 100%; width: auto; height: auto;
  object-fit: contain; display: block;
}
/* An INLINED svg has no intrinsic pixel size the way an <img> does: it sizes
   from its viewBox and needs to be told to fill the figure. Without this the
   three hand-built figures rendered at zero height once they were inlined to
   pick up the page's @font-face. */
.visual > figure > svg { width: 100%; height: 100%; }
/* An SVG served as an <img> has no intrinsic size in a grid track, so it needs
   a width; it still must not exceed the row. */
.visual img[src$=".svg"] { width: 100%; max-height: 100%; }
.visual svg { max-width: 100%; height: auto; }

/* A media element that must read correctly when still: it shows its final
   frame as a poster, and carries a visible control. The player's own rules are
   further down, in one place. An earlier duplicate pair sat here and was
   overridden silently, which made the live behaviour unreadable from the file.
*/
.placeholder {
  border: 1px dashed var(--hair); border-radius: 3px;
  padding: 28px 20px; text-align: center; width: 100%; max-width: 560px;
  font-family: var(--sans); font-size: 12px; letter-spacing: .01em;
  color: var(--muted); background: #fcfcfc;
}
.placeholder b { display: block; font-weight: 400; color: var(--body); margin-top: 8px;
  font-family: var(--sans); font-size: 13px; letter-spacing: 0; text-transform: none; }

/* -------------------------------------------------- short copy and control */
/* Briefing 3D: the one-sentence interpretation sits bottom left on a white
   surface, with its expand control on the same surface rather than across the
   page. Stacked, so the control reads as belonging to the sentence. */
/* The interpretation on its own white plate, bottom left, over the visual.
   Nearly opaque so it stays legible over any figure, and it never sits over a
   busy map because the figures are centred above it. */
/* One row: the interpretation, then the step dots, then the expand control.
   A stacked block took 156 px of stage height that the evidence needed. */
.baseline {
  position: absolute; left: var(--gutter); right: var(--gutter); bottom: 12px; z-index: 4;
  display: flex; flex-direction: row; align-items: baseline; gap: 20px;
  background: rgba(255,255,255,.94);
  padding: 10px 0 2px;
  border-top: 1px solid var(--rule);
}
.baseline .shortcopy { flex: 1 1 auto; max-width: 74ch; }
.baseline .stepdots { flex: 0 0 auto; }
.baseline .expand { flex: 0 0 auto; }
@media (max-width: 900px) {
  .baseline { position: static; max-width: none; background: none; padding-top: 12px; }
}
 /* The one-sentence interpretation is explanatory copy, so DM Sans, not the
   headline serif. */
.shortcopy {
  font-family: var(--sans); font-size: var(--t-interp); line-height: 1.45;
  color: var(--ink); margin: 0; max-width: 58ch;
}
.expand {
  font-family: var(--sans); font-size: 13px; letter-spacing: .01em;
  background: none; border: 0; border-bottom: 1px solid var(--hair);
  color: var(--body); padding: 2px 0; cursor: pointer; white-space: nowrap;
}
.expand:hover { color: var(--ink); border-bottom-color: var(--ink); }

/* ----------------------------------------------------------------- panel */
.panel {
  position: fixed; z-index: 70; background: var(--page);
  border-left: 1px solid var(--hair);
  top: 0; right: 0; bottom: 0; width: min(560px, 92vw);
  transform: translateX(101%); transition: transform 260ms ease;
  /* No top padding: the sticky bar supplies it. A bar pulled up into the
     padding by a negative margin does not work, because sticky clamps the
     element to the scrollport and the margin then only moves what FOLLOWS it,
     so the bar sat 22 px low and the panel title ran underneath it. */
  overflow-y: auto; padding: 0 26px 60px;
  box-shadow: none;
}
/* The shadow renders only while the panel is on screen. Off-screen it bled a
   grey band down the right edge, and the mobile sheet did the same along the
   bottom. */
.panel.open { box-shadow: -18px 0 40px rgba(0,0,0,.06); }
.panel.open { transform: none; }
.panel h3 { font-family: var(--sans); font-size: 21px; line-height: 1.12; margin: 6px 0 4px; font-weight: 400; }
.panel p  { color: var(--body); margin: 0 0 13px; }
/* The two timescales are a numbered list in the copy, so they are one here
   too. Emphasis is not used in body text, so the labels are not bolded. */
.panel ol { color: var(--body); margin: 0 0 13px; padding-left: 20px; }
.panel li { margin: 0 0 7px; }
/* .openitems no longer exists on the page; check-page.sh refuses it. */
.panel .sources {
  font-size: 13px; color: var(--muted); line-height: 1.5;
  border-top: 1px solid var(--rule); padding-top: 12px; margin-top: 18px;
}
/* Full citations, for the sources checked against the primary document. A
   list like the panel's existing ol, inheriting the sources block's 13 px. The
   URLs wrap ANYWHERE because a Legifrance or AEMET path is longer than the
   560 px panel and would otherwise push a horizontal scrollbar across it. The
   links take the body colour rather than the navy, so a references block does
   not turn the panel blue. */
.panel .refs { color: var(--body); margin: 0 0 13px; padding-left: 20px; }
.panel .refs li { margin: 0 0 7px; overflow-wrap: anywhere; }
.panel .refs a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }
.panel .refs a:hover { color: var(--ink); }
.panel .sources h4 {
  font-family: var(--sans); font-size: 11px; letter-spacing: .06em; text-transform: uppercase;
  color: var(--muted); margin: 0 0 8px; font-weight: 400;
}
/* A STICKY STRIP, NOT A FLOATING PILL. The close control used to be a small
   sticky button floated beside the panel title. A float is only got out of the
   way by the text that wraps around it, which is the first paragraph and
   nothing else, so once the panel was scrolled every line that passed the
   button went straight under it: at the foot of Frame 6 the sentence read
   "the four gra[   ]levels" with the word punched out by the button. Making it
   a full-width strip means text passes behind chrome and reads as chrome. */
.panel-bar {
  position: sticky; top: 0; z-index: 3;
  display: flex; justify-content: flex-end;
  margin: 0 -26px 12px;              /* out to the panel's own side padding */
  padding: 16px 26px 10px;
  background: var(--page);
  border-bottom: 1px solid var(--rule);
}
.panel-close {
  font-family: var(--sans); font-size: 12px; letter-spacing: .01em;
  background: var(--page); border: 1px solid var(--hair); border-radius: 2px;
  color: var(--ink); padding: 6px 10px; cursor: pointer;
}
.pending {
  font-family: var(--sans); font-size: 12px; letter-spacing: .01em;
  background: #f6f6f6; border: 1px solid var(--hair); border-radius: 2px;
  color: var(--body); padding: 2px 6px; white-space: nowrap;
}

/* On a phone AND on a tablet in portrait the panel is a bottom sheet. */
@media (max-width: 900px) {
  .panel {
    top: auto; left: 0; right: 0; bottom: 0; width: auto;
    height: auto; max-height: 82svh;
    border-left: none; border-top: 1px solid var(--hair);
    border-radius: 12px 12px 0 0;
    transform: translateY(101%);
    box-shadow: none;
  }
  .panel.open { box-shadow: 0 -18px 40px rgba(0,0,0,.08); }
  .panel.open { transform: none; }
  .stage { padding-top: 96px; }
  .baseline { grid-template-columns: 1fr; }
  .expand { justify-self: start; }
}

.scrim {
  position: fixed; inset: 0; z-index: 69; background: rgba(255,255,255,.55);
  opacity: 0; pointer-events: none; transition: opacity 200ms linear;
}
.scrim.on { opacity: 1; pointer-events: auto; }

/* ---------------------------------------------------------------- footer */
.footer {
  border-top: 1px solid var(--hair); padding: 44px var(--gutter) 60px;
  max-width: 820px; margin: 0 auto; color: var(--body);
}
.footer .mark { font-family: var(--serif); font-size: 24px; color: var(--ink); margin: 0 0 8px; }
.footer .cutoff { font-family: var(--sans); font-size: 13px; color: var(--muted); }

.skip {
  position: absolute; left: -9999px; top: 0; z-index: 100;
  background: var(--page); color: var(--ink); padding: 10px 14px; border: 1px solid var(--ink);
}
.skip:focus { left: var(--gutter); top: 10px; }

/* THERE IS NO MOTION CONTROL. The page had a "Reduced motion" toggle at the
   top right of every frame. It wrote a preference to localStorage, so a
   browser used for testing stayed in the static layout afterwards with no
   obvious way back, and it put a control in the corner of every frame to buy
   something the operating system already offers. Full motion is now the
   default at 901 px and wider, the static layout is what 900 px and below get,
   and prefers-reduced-motion is honoured silently. The .a11y-bar rules that
   were here are gone with it; the print rule below still names the class,
   harmlessly, so it keeps working if the bar ever returns. */

/* -------------------------------------------------------- reduced motion */
/* The whole choreography is withdrawn: no sticky stages, no white layer, no
   autoplay. Every frame is a plain block in document order. */
.reduced .frame--tall { height: auto; }
.reduced .stage { position: static; height: auto; overflow: visible; padding-top: 118px; }
.reduced .veil { display: none; }
.reduced .headline { font-size: clamp(26px, 3.4vw, 40px); max-width: 30ch; }
.reduced .headline .desc { display: block; }
.reduced .visual img, .reduced .visual video { max-height: none; }

@media (prefers-reduced-motion: reduce) {
  .frame--tall { height: auto; }
  .stage { position: static; height: auto; overflow: visible; padding-top: 118px; }
  .veil { display: none; }
  .headline { font-size: clamp(26px, 3.4vw, 40px); max-width: 30ch; }
  .headline .desc { display: block; }
  .visual img, .visual video { max-height: none; }
  .panel, .scrim, .runtitle, .navrail { transition: none; }
}

@media print { .masthead, .navrail, .a11y-bar, .panel, .scrim, .veil { display: none !important; }
  .frame--tall { height: auto; } .stage { position: static; height: auto; } }

/* ------------------------------------------- navigator, clickable stages */
/* Briefing 3: "make it clickable for public-site navigation if feasible".
   The overlay is positioned from the SVG's own geometry, a 720 by 76 viewBox
   with stage boxes at x = 40, 173, 306, 439 and 572, each 106 wide, and the
   production-buffer label at x = 250, 84 wide. */
.navwrap { position: relative; width: min(var(--nav-w), 100%); }
.navwrap a {
  position: absolute; display: block; text-indent: -9999px; overflow: hidden;
  border-radius: 2px;
}
.navwrap a:hover { background: rgba(0,0,0,.05); }
.navwrap a:focus-visible { outline: 2px solid var(--ink); outline-offset: 1px; }
.navrail { pointer-events: none; }
.navrail.on { pointer-events: auto; }
/* Percentages of the RAIL's box, which is the navigator cropped to "0 22 720
   50" by inline-svg.sh: stage boxes run y 22 to 48, the buffer label y 58 to
   70. Re-derive these if that crop changes. */
.nv-stage { top: 0%; height: 52%; }
.nv1 { left: 5.56%;  width: 14.72%; }
.nv2 { left: 24.03%; width: 14.72%; }
.nv3 { left: 42.50%; width: 14.72%; }
.nv4 { left: 60.97%; width: 14.72%; }
.nv5 { left: 79.44%; width: 14.72%; }
.nv-buffer { left: 34.72%; width: 11.67%; top: 72%; height: 24%; }

/* ------------------------------------------------------- the slideshow */
/* Figures are steps. Only the current one is shown, except a final "all"
   state. Hidden steps are moved out of the flow so the visible one has the
   whole stage, and they are faded and nudged by transform only. */
.visual > figure {
  grid-area: 1 / 1;
  opacity: 0; transform: translateY(8px); pointer-events: none;
  transition: opacity 320ms ease, transform 320ms ease;
  /* The crossfade animates opacity and transform together, which is exactly
     what a compositor does without touching the main thread, provided the
     element has a layer to do it on. */
  will-change: opacity, transform;
}
.visual > figure.is-on { opacity: 1; transform: none; pointer-events: auto; }
.visual:not(.is-all) { grid-template-columns: 1fr !important; }
/* The final state of Frame 1: three maps side by side at equal size, because
   equal weight IS the argument. */
/* Equal weight IS the argument on Frame 1, so the final state gives the three
   maps equal columns rather than letting intrinsic sizes decide. */
.visual.is-all { grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr); }
.visual.is-all > figure { grid-area: auto; opacity: 1; transform: none; pointer-events: auto; }
/* The navigator is the one figure whose own type ratio pins its display
   width: stage labels are 9.5 units and the bracket type 7.5, so 1061 px
   puts them at 14.0 and 11.1, the two ends of the page band. Wider and the
   stage labels leave the band at the top; narrower and the brackets leave
   it at the bottom. */
.visual > figure.full { max-width: 1061px; }

/* ------------------------------------------- the opening triptych, Frame 1 */
/* Frame 1 opens on the three maps side by side. Each gets a 481 px column
   against the 888 to 1200 px their builders target, so their own labels would
   render at 4.5 to 6.0 CSS px. The owner has exempted this ONE state from the
   page's 11 px floor, on the condition that it shows no figure labels at all.
   So the triptych shows a label-free still of each map and the page supplies
   the caption. The full size steps that follow show the labelled figures.

   A figure that has no plain still yet is marked .noplain by the img's own
   error handler and keeps its labelled media, so a missing asset degrades to
   the previous behaviour instead of to an empty column. */
.visual > figure > .plain,
.visual > figure > .tripcap { display: none; }
.visual.is-all > figure:not(.noplain) > .plain { display: block; }
.visual.is-all > figure:not(.noplain) > .player { display: none; }
.visual.is-all > figure > .tripcap {
  display: block; margin-top: 10px;
  font-family: var(--sans); font-size: 13px; line-height: 1.3;
  color: var(--body); text-align: center; flex: 0 0 auto;
}
/* The three tiles are now EQUAL SQUARES, cut from the label-free stills, so
   they are the same shape as well as the same width. The shared two-row grid
   stays: it is what keeps the three captions on one line, and it still earns
   its place, because a square tile whose source is short leaves the media row
   taller than its own picture. It also means this row survives a tile changing
   shape again without the captions stepping.
   The squares are cut by webinar/crop_triptych.py, which reads whatever
   label-free still is on disk. Re-run it if any of the three is re-rendered. */
/* minmax(0, 1fr), not 1fr. A grid row's default minimum is its content, so a
   tile taller than the space available pushed the caption row out of the band
   and under the interpretation plate. That only showed once the tiles became
   squares: the landscape stills were short enough to fit by luck. The min-height
   floors on the figure and the image are the same guard one level down. */
.visual.is-all { grid-template-rows: minmax(0, 1fr) auto; align-content: center; }
.visual.is-all > figure {
  display: grid; grid-row: span 2; grid-template-rows: subgrid;
  justify-items: center; align-items: end;
  min-height: 0; min-width: 0;
}
/* A hairline, because a label-free still has no frame of its own and the page
   is white. The Pacific and France tiles happen to carry colour or grey land to
   their edges, so they read as bounded; the Hormuz tile is mostly white water
   and its extent would otherwise be guessed from where the land happens to
   fall. The rule tone, not the hair tone, so it contains without competing. */
.visual.is-all > figure > .plain {
  grid-row: 1; align-self: end; border: 1px solid var(--rule);
  max-width: 100%; max-height: 100%; width: auto; height: auto;
  min-height: 0; min-width: 0;
  object-fit: contain;
}
.visual.is-all > figure > .tripcap { grid-row: 2; align-self: start; }
/* A figure still showing its labelled media in this state has a .player, not a
   .plain, and it must land in the same media row. */
.visual.is-all > figure > .player { grid-row: 1; align-self: end; }
.visual.is-all > figure.noplain > .player { display: block; }

@media (prefers-reduced-motion: reduce) {
  .visual > figure { transition: none; }
}

/* Step indicator: tone, never colour, and never the only signal. */
.stepdots { display: flex; gap: 7px; align-items: center; margin: 2px 0 4px; }
.stepdots[hidden] { display: none; }
.stepdot {
  width: 7px; height: 7px; padding: 0; border-radius: 50%;
  border: 1px solid var(--hair); background: var(--page); cursor: pointer;
}
.stepdot[aria-current="true"] { background: var(--body); border-color: var(--body); }
.stepdot:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }

/* The play control sits clear of a figure's own source line rather than on
   top of it: below the media, aligned left, in the flow.
   The player must pass its height constraint DOWN. Without min-height:0 and an
   explicit max-height the video's `max-height:100%` resolved against an
   auto-height player and the media overflowed its stage row, cropping the
   figure's own colour bar and source line. */
/* The control sits inside a corner of its figure rather than taking a row of
   stage height. Bottom left, on a white plate so it reads over any field. */
/* Full height so the player can pass the height constraint DOWN to the video;
   without that the media overflowed its stage row and was cropped. But full
   height also put the Play control at the bottom of the PLAYER rather than of
   the picture, so it floated 35 to 61 px below the video with white between.
   A grid with content-sized, centred tracks keeps the constraint and makes the
   cell the video's own box, so anything in that cell is inside the picture. */
.player {
  position: relative; display: grid; place-content: center;
  min-height: 0; max-height: 100%; height: 100%;
}
.player video { grid-area: 1 / 1; min-height: 0; max-height: 100%; }
/* The control sits in the bottom row beside the step dots, in the SAME place on
   every frame, and appears only on a step that has an animation. It used to sit
   inside the picture, in a corner measured per figure to avoid the ink; that
   kept it off the artwork but moved it between six different corners as the
   reader stepped, so it read as having disappeared. A control that moves is a
   control nobody finds. */
.playctl {
  font-family: var(--sans); font-size: 12px; letter-spacing: .01em;
  background: var(--page); border: 1px solid var(--hair); border-radius: 2px;
  color: var(--body); padding: 4px 11px; cursor: pointer;
  min-width: 62px; text-align: center;
}
.playctl[hidden] { display: none; }
.playctl:hover { color: var(--ink); border-color: var(--body); }
.baseline .playctl { flex: 0 0 auto; }

.reduced .visual > figure { opacity: 1; transform: none; pointer-events: auto; }
.reduced .visual { grid-template-columns: 1fr !important; grid-auto-flow: row; }
.reduced .stepdots { display: none; }
/* Reduced motion shows every figure at once, so ONE shared title line above
   them would label the wrong figure or, as it did, none of them. Each figure
   carries its own title here instead, drawn from the same data-title the
   slideshow uses, so the two cannot say different things. */
.reduced .stage .figtitle { display: none; }
.reduced .visual > figure {
  flex-direction: column; align-items: flex-start; justify-content: flex-start;
  gap: 6px;
}
.reduced .visual > figure { justify-self: stretch; }
.reduced .visual > figure[data-title]:not([data-title=""])::before {
  content: attr(data-title);
  font-family: var(--sans); font-size: 15px; line-height: 1.35; color: var(--ink);
  /* width and text-align, not align-self: the figure is a grid item under
     justify-items: center, so it is content-sized and the pseudo-element was
     centring inside it rather than sitting over the figure's left edge. */
  width: 100%; text-align: left;
}

/* ------------------------------------------------ composition by shape */
/* A figure squarer than about 2 to 1 is height-capped in a full-width band and
   wastes 400 to 600 px of width there. It takes the right --side-w of the
   stage at FULL stage height instead, with the headline and the interpretation
   in a left column beside it. A wider figure spans the band. page.js decides
   per figure by comparing the fitted area in the two boxes, so this share and
   that rule stay in step. The stage follows the figure currently shown. */
.stage[data-shape="tall"] {
  grid-template-rows: 1fr;
  /* everything sits in row 1 here: headline and interpretation left, title and
     figure right, so the title is placed rather than given a row of its own */
  grid-template-columns: 1fr var(--side-w);
  column-gap: 34px;
}
/* The headline block must be PLACED, not auto-placed. The visual and the
   baseline are both explicitly in row 1, so the only free cell left for an
   auto-placed headline was an implicit SECOND row, and the stage's single 1fr
   row silently became two: 494 px for the figure and 209 px for the headline.
   That cost every side-composition figure about 200 px of height, and it cost
   more the longer the headline, so widening the column made the figure smaller
   rather than larger. Computed grid-template-rows is the tell: it reported two
   tracks where the rule asks for one. */
.stage[data-shape="tall"] > div:first-child {
  grid-column: 1; grid-row: 1; align-self: start;
}
/* The figure takes the full stage height here, so its top edge sits at the
   stage's 76 px padding, which is inside the navigator rail's 58 to 108 band.
   The rail is 720 px wide and centred, so it crosses this column and clipped
   the first panel header of the positioning stack. 34 px of reserve clears it.
   It costs nothing on the four figures that are width-bound in this column and
   about 30 px of height on the one that is not. */
.stage[data-shape="tall"] .figtitle { grid-column: 2; grid-row: 1; align-self: start; margin-top: 34px; }
.stage[data-shape="tall"] .visual {
  grid-column: 2; grid-row: 1; padding-bottom: 0; padding-top: 34px;
}
/* The title sits in the rail's reserve, so the figure keeps its own padding and
   the two do not both pay for the rail. */
.stage[data-shape="tall"] .visual { padding-top: calc(34px + 1.35em + 6px); }
.stage[data-shape="tall"] .baseline {
  position: static; grid-column: 1; grid-row: 1; align-self: end;
  flex-direction: column; align-items: flex-start; gap: 12px;
  right: auto; background: none; margin-bottom: 8px;
}
.stage[data-shape="tall"] .baseline .shortcopy { max-width: 42ch; }
@media (max-width: 900px) {
  .stage[data-shape="tall"] { grid-template-columns: 1fr; grid-template-rows: auto 1fr; }
  .stage[data-shape="tall"] .visual { grid-column: 1; grid-row: 2; }
  .stage[data-shape="tall"] .baseline { grid-column: 1; grid-row: 3; align-self: start; }
}


/* ------------------------------------------- tablet and phone, hosted version */
/* The scroll choreography, the slideshow and the panel all stay. What changes is
   that a figure built for 1480 CSS px cannot shrink to 390 and stay readable:
   its labels would fall to about 3 px. So below 900 px the figure is shown at
   the width it was BUILT for and the reader pans it sideways, which keeps every
   label inside the 11 to 14 px band at any screen size. The alternative, fitting
   the figure to the screen, silently breaks the one guarantee the whole type
   pass exists to make. */
/* Panning is driven by the attribute page.js sets when a figure is narrower
   than the width it was built for, at ANY viewport, not by a breakpoint. */
.visual > figure[data-pannable] {
  overflow-x: auto; overflow-y: hidden;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: thin;
  justify-content: flex-start;
}
@media (max-width: 900px) {
  .visual.is-all > figure { overflow: visible; }
  .stage { padding: 88px var(--gutter) 12px; row-gap: 6px; }
  .headline { max-width: none; font-size: clamp(21px, 5.2vw, 30px); }
  .headline .desc { font-size: clamp(14px, 3.4vw, 17px); }
  .frame-eyebrow { margin-bottom: 6px; }
  /* The interpretation, the dots and the control stop sharing a row: at 390 px
     the sentence was squeezed into about nine characters a line beside them. */
  .baseline {
    position: static; margin-top: 8px; background: none; border-top: none;
    flex-direction: column; align-items: flex-start; gap: 10px;
  }
  .baseline .shortcopy { max-width: none; }
  .visual { padding-bottom: 0; }
  .context { font-size: 11px; }
}

@media (max-width: 560px) {
  .stage { padding-top: 76px; }
  .stepdots { gap: 9px; }
  .stepdot { width: 11px; height: 11px; }
}
/* A pannable figure says so once, quietly, wherever it overflows. */
.visual > figure[data-pannable]::after {
  content: "Scroll sideways to read the figure";
  font-family: var(--sans); font-size: 11px; color: var(--faint);
  position: sticky; left: 0; align-self: flex-end; padding-top: 4px;
  white-space: nowrap;
}


/* ------------------------------------------------ the small-screen notice */
/* A line, not an overlay. It sits in the flow at the top of the page, so it
   pushes the content down rather than covering it, and it goes when dismissed.
   Quiet weight, because it is an apology for the layout, not a message. */
.smallnote {
  display: flex; align-items: flex-start; gap: 12px;
  padding: 10px var(--gutter);
  border-bottom: 1px solid var(--rule); background: var(--page);
}
.smallnote p {
  margin: 0; flex: 1 1 auto;
  font-family: var(--sans); font-weight: 300; font-size: 13px; line-height: 1.4;
  color: var(--body);
}
.smallnote button {
  flex: 0 0 auto; background: none; border: 0; padding: 2px 0;
  font-family: var(--sans); font-size: 12px; color: var(--muted);
  text-decoration: underline; text-underline-offset: 2px; cursor: pointer;
}
.smallnote button:hover { color: var(--ink); }

/* In the reduced layout a figure fits the width, full stop. */
.reduced .visual > figure { overflow: visible !important; }
.reduced .visual img, .reduced .visual video, .reduced .visual > figure > svg {
  min-width: 0 !important; max-width: 100%; width: 100%; height: auto;
}
.reduced .visual > figure[data-pannable]::after { content: none; }

/* The reduced layout shows every figure at once, each with its own title, so
   the triptych's label-free tiles and their captions have no job there: the
   caption sat on top of the figure title, and a poster tile is the wrong thing
   to read from when the labelled figure is what the page is stacking. Frame 1
   shows its three labelled maps like every other frame. */
.reduced .visual.is-all > figure > .plain { display: none; }
.reduced .visual.is-all > figure > .tripcap { display: none; }
.reduced .visual.is-all > figure > .player { display: block; }
.reduced .visual.is-all { grid-template-rows: none; }
.reduced .visual.is-all > figure { grid-row: auto; display: flex; }

/* ============================================================================
   THE REDUCED LAYOUT, AS PLAIN DOCUMENT FLOW
   ============================================================================
   Served to every screen 900 CSS px wide or narrower, or under 600 tall,
   and to
   anyone who asks for reduced motion. Until this block it was the desktop
   layout with pieces switched off, which is why a real phone showed text over
   text: the masthead, the navigator, the motion bar, the interpretation row
   and the partnership credit were all still taken OUT of the flow by
   position: fixed or absolute, and every figure still sat in one grid cell, so
   the seven figures of a frame were drawn on top of each other.

   The rule here is single and blunt: in the reduced layout nothing is taken
   out of the flow except the masthead, which is sticky rather than fixed so it
   keeps its own height and nothing can pass under it, and the panel, which is
   a sheet over the page by definition. Everything else is a block in document
   order: eyebrow, headline, figure title, figure, interpretation, credit.

   It is written as one block at the end of the file so it wins on order
   without !important flags, and so the whole phone layout can be read in one
   place instead of being inferred from a dozen overrides.
   ==========================================================================*/

/* iOS inflates text in a way that depends on the block's width, which moves
   type the layout was measured against. The page sets its own sizes. */
html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }

/* A compact masthead that keeps its place in the flow. Sticky, not fixed:
   a fixed bar covers the first line of the page and everything that scrolls
   beneath it, which is exactly what the audit found at 360, 390, 430 and 844
   px wide. */
body.reduced .masthead {
  position: sticky; inset: auto; z-index: 60;
  padding: 10px var(--gutter);
  border-bottom: 1px solid var(--rule);
}
body.reduced .wordmark img { height: 22px; }
body.reduced .masthead-title { font-size: 13px; }

/* The navigator rail needs 720 px to read and is fixed over the page. Neither
   is available here. */
body.reduced .navrail { display: none; }

/* Nothing else is taken out of the flow. */
body.reduced .stage {
  position: static; height: auto; overflow: visible;
  padding: 10px var(--gutter) 30px;
}
body.reduced .sheet,
body.reduced .veil { display: none; }
body.reduced .baseline {
  position: static; background: none; max-width: none;
  display: block; padding: 12px 0 0; margin-top: 14px;
}
body.reduced .baseline .shortcopy { max-width: none; margin: 0 0 10px; }
body.reduced .expand { display: inline-block; }
/* Every figure is shown at once here, so a control that plays "the current
   one" has nothing to point at. Each video carries its own controls instead. */
body.reduced .playctl { display: none !important; }
body.reduced .context {
  position: static; margin: 14px 0 0; opacity: 1; visibility: visible;
}

/* Figures stack. Without this they share one grid cell and are drawn over each
   other; .visual is a grid whose figures all declare grid-area 1 / 1. */
body.reduced .visual { display: block; padding-bottom: 0; }
body.reduced .visual > figure {
  grid-area: auto; position: static;
  display: block; width: 100%; height: auto; max-width: none;
  margin: 0 0 26px;
}
body.reduced .visual > figure:last-child { margin-bottom: 0; }
/* The title belongs above its own figure and the interpretation below the
   set, which is the document order already; this only restores the spacing
   the flex centring was providing. */
body.reduced .visual > figure[data-title]:not([data-title=""])::before {
  display: block; margin: 0 0 8px;
}

/* The panel is a bottom sheet at every size in this layout, and its height is
   measured in svh so a phone's address bar cannot push the close control off
   the screen. The close control is a block above the title rather than a
   floated one beside it, so it cannot sit over the text it labels, and it is
   sticky inside the sheet so scrolling never takes it away. */
body.reduced .panel {
  top: auto; left: 0; right: 0; bottom: 0; width: auto;
  height: auto; max-height: 82svh;
  border-left: none; border-top: 1px solid var(--hair);
  border-radius: 12px 12px 0 0;
  transform: translateY(101%);
  padding: 14px 18px calc(28px + env(safe-area-inset-bottom, 0px));
}
body.reduced .panel.open { transform: none; }
/* The sheet has its own padding, so the strip is pulled out to that instead. */
body.reduced .panel { padding: 0 18px calc(28px + env(safe-area-inset-bottom, 0px)); }
body.reduced .panel-bar { margin: 0 -18px 12px; padding: 12px 18px 10px; }

/* The notch and the rounded corners. The gutter is kept as it is and the
   insets are added to the page, so no rule has to know about both. */
@supports (padding: env(safe-area-inset-left)) {
  body.reduced {
    padding-left: env(safe-area-inset-left);
    padding-right: env(safe-area-inset-right);
  }
}
