/* ReSMap project page. Hand-rolled, no framework.
   Latin is set in Inter; Hangul in Freesentation. Inter sits first in the
   stack and carries no Hangul glyphs, so Korean resolves to Freesentation
   on its own. */

/* Freesentation, pinned to a commit so the URL cannot shift under us.
   Only the four weights the page uses, and only over the Korean ranges: the
   files are ~470 KB each and unsubsetted, so restricting unicode-range means
   a page with no Hangul on it downloads none of them. */
@font-face {
  font-family: "Freesentation";
  src: url("https://cdn.jsdelivr.net/gh/Freesentation/freesentation@98f6b841fca55a23fbb0f9eb91acda0e16a676ea/woff2/Freesentation-4Regular.woff2") format("woff2");
  font-weight: 400; font-display: swap;
  unicode-range: U+1100-11FF, U+3000-303F, U+3130-318F, U+A960-A97F,
                 U+AC00-D7A3, U+D7B0-D7FF, U+FF01-FF60;
}
@font-face {
  font-family: "Freesentation";
  src: url("https://cdn.jsdelivr.net/gh/Freesentation/freesentation@98f6b841fca55a23fbb0f9eb91acda0e16a676ea/woff2/Freesentation-5Medium.woff2") format("woff2");
  font-weight: 500; font-display: swap;
  unicode-range: U+1100-11FF, U+3000-303F, U+3130-318F, U+A960-A97F,
                 U+AC00-D7A3, U+D7B0-D7FF, U+FF01-FF60;
}
@font-face {
  font-family: "Freesentation";
  src: url("https://cdn.jsdelivr.net/gh/Freesentation/freesentation@98f6b841fca55a23fbb0f9eb91acda0e16a676ea/woff2/Freesentation-6SemiBold.woff2") format("woff2");
  font-weight: 600; font-display: swap;
  unicode-range: U+1100-11FF, U+3000-303F, U+3130-318F, U+A960-A97F,
                 U+AC00-D7A3, U+D7B0-D7FF, U+FF01-FF60;
}
@font-face {
  font-family: "Freesentation";
  src: url("https://cdn.jsdelivr.net/gh/Freesentation/freesentation@98f6b841fca55a23fbb0f9eb91acda0e16a676ea/woff2/Freesentation-7Bold.woff2") format("woff2");
  font-weight: 700; font-display: swap;
  unicode-range: U+1100-11FF, U+3000-303F, U+3130-318F, U+A960-A97F,
                 U+AC00-D7A3, U+D7B0-D7FF, U+FF01-FF60;
}

:root {
  --paper:      #ffffff;
  --surface:    #f4f5f7;
  --bar:        #fbfbfc;
  --row-hover:  rgba(17, 19, 23, .04);
  --ink:        #15171b;
  --ink-soft:   #4c515a;
  --ink-faint:  #848a93;
  --rule:       #e2e4e8;
  --rule-dark:  #b4b9c1;
  --accent:     #0e4a84;
  --ours-bg:    #eef3f9;
  --drop:       #b23a30;
  --sign:       #12512f;
  --go:         #1f8a4c;
  --stop:       #b53228;
  --sign-ink:   #ffffff;
  --figure-bg:  #ffffff;

  /* The carriageway and the markings painted on it. A road that does not read
     as a road is just a progress bar, so these are their own tokens rather
     than borrowed surface greys. drive.js reads both. */
  --road-surface: #d6d9df;
  --road-line:    #fbfcfd;
  /* Map element classes, as on the road rail (drive.js CLASS). */
  --map-boundary: #3f9c63;
  --map-divider:  #c2831f;
  --map-crossing: #3277bd;

  --sans: "Inter", "Freesentation", -apple-system, BlinkMacSystemFont,
          system-ui, "Segoe UI", Roboto, "Apple SD Gothic Neo",
          "Malgun Gothic", sans-serif;
  /* The display face the rest of dehsurc.github.io sets its names in, so the
     two pages read as the same hand. Inter Tight is Inter drawn for headline
     sizes, so it is the same voice rather than a second one. */
  --display: "Inter Tight", "Freesentation", "Inter", system-ui, sans-serif;
  --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

  --col:  52rem;   /* prose measure */
  --wide: 64rem;   /* the content box: figures, tables, the bar, the stage.
                      Tables were at 90rem and none of them needs it: the ten
                      column one is about 620px of content, so the width was
                      spent spreading the numbers apart. */
  --gutter: 3rem;

  /* The road is a rail down the left-hand edge. --rail-w is the strip itself
     and --rail-gutter is what the body gives up for it: the strip, plus room
     for the guide signs to stand beside it and be read. Reserved rather than
     floated over the margin, so a sign can never land on a figure however
     narrow the window is. */
  --rail-w: 68px;
  --rail-gutter: 200px;
  --chrome: 2.6rem;  /* sticky chrome a pinned section has to clear */

  color-scheme: light;
}

:root[data-theme="dark"] {
  --paper:      #0f1114;
  --surface:    #191c21;
  --bar:        #14171b;
  --row-hover:  rgba(255, 255, 255, .05);
  --ink:        #e5e7ea;
  --ink-soft:   #a0a6ae;
  --ink-faint:  #6f757d;
  --rule:       #262a30;
  --rule-dark:  #424852;
  --accent:     #7ab2e8;
  --ours-bg:    #172029;
  --drop:       #ef8b7f;
  --sign:       #17663c;
  --go:         #2ba85e;
  --stop:       #dd5e4e;
  --sign-ink:   #f2fbf5;
  --figure-bg:  #f5f6f7;

  --road-surface: #262c35;
  --road-line:    #858d99;
  --map-boundary: #63c98c;
  --map-divider:  #e0a94a;
  --map-crossing: #6aa6e8;

  color-scheme: dark;
}

* { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; scroll-padding-top: 4.5rem;
  /* keep the scrollbar's width reserved: the PDF reader, the lightbox and the full-screen viewer set
     overflow: hidden on html, and without this the page widened and every line re-wrapped behind them */
  scrollbar-gutter: stable; }

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font: 400 18px/1.7 var(--sans);
  letter-spacing: -.004em;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; text-underline-offset: 2px; }

.skiplink {
  position: absolute; left: -999px; top: 0;
  background: var(--ink); color: var(--paper); padding: .6rem 1rem; z-index: 100;
}
.skiplink:focus { left: 0; }

.sr-only {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap;
}

:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 2px; }

/* ---------- pinned camera-failure walkthrough ---------- */

/* The walkthrough leads the page, so it earns a shorter run. failure.js sets
   the track's height (the stage plus the scroll it is pinned for) and the
   stage's sticky offset; these are only the fallbacks without it.

   The stage is as tall as what is in it, and pins centred in the window. It
   used to be a whole window tall with the content centred inside, which put
   a couple of hundred pixels of nothing above and below it on the way in and
   on the way out. */
.track { position: relative; height: 260vh; }

.stage {
  position: sticky;
  top: var(--stage-top, var(--chrome));
}

/* The teaser sits above the numbered document, so its heading carries no
   number and no rule: it is the way in, not section one. */
/* The camera-failure walkthrough, inside the abstract: set off by space and
   its own subheading rather than a section of its own. */
.walk { margin-top: 3.2rem; }
/* One line of air between the walkthrough intro and the stage it introduces. */
.walk > .col:first-child { margin-bottom: 1.7rem; }
/* The prose after the walkthrough reads Table 2 back: a clear break from the stage above it. */
.walk-after { margin-top: 3rem; }
.walk-h {
  margin: 0 0 .9rem;
  font-family: var(--display);
  font-size: 1.32rem; font-weight: 700; line-height: 1.25;
  letter-spacing: -.018em;
}

.stage-head {
  display: flex; align-items: baseline; gap: .6rem;
  margin-bottom: 1.4rem;
}
.stage-step {
  font-size: 1.1rem; font-weight: 600; letter-spacing: -.015em;
}

/* The four levels, as somewhere to click rather than scroll to. They sit in
   the head because that is where the level is already named. Not `.steps`:
   that is the numbered list in section 3.2, and it is defined further down,
   so sharing the name silently un-aligned these and turned that list into a
   flex row. */
.stage-steps {
  margin-left: auto;
  display: flex; gap: .3rem;
  align-self: center;
  flex: none;
}
.step-pip {
  padding: .3rem .55rem;
  border: 1px solid var(--rule-dark);
  border-radius: 3px;
  background: none;
  color: var(--ink-faint);
  font: inherit; font-size: 11px; font-weight: 600;
  letter-spacing: .03em;
  white-space: nowrap;
  cursor: pointer;
  transition: color .15s ease, border-color .15s ease, background .15s ease;
}
.step-pip:hover { color: var(--ink); border-color: var(--ink); }
.step-pip[aria-current="step"] {
  color: var(--paper);
  background: var(--ink);
  border-color: var(--ink);
}
.stage-sub { font-size: 13.5px; color: var(--ink-faint); }

.stage-grid {
  display: grid;
  grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr);
  gap: 2.6rem;
  align-items: center;
}

.stage-note {
  margin: 1.5rem 0 0;
  font-size: 15px; line-height: 1.6;
  color: var(--ink-soft);
  max-width: 52rem;
  min-height: 4.5em;
}

/* the rig */

.rig { margin: 0; }
.rig svg { display: block; width: 100%; max-width: 21rem; margin-inline: auto; }

#fail-rig { --sat: .22; color: var(--ink); }
.sat-frame { opacity: var(--sat); }
.sat-grid { stroke: currentColor; stroke-width: .6; opacity: calc(var(--sat) * .38); }
.sat-label {
  font-family: var(--sans); font-size: 9px; font-weight: 500;
  fill: currentColor; opacity: calc(var(--sat) * .75);
  letter-spacing: .04em;
}

.cam { fill: var(--accent); transition: fill .35s ease; }
.cam.down { fill: var(--ink-faint); }
.ego { fill: var(--ink); }
.ego-cabin { fill: var(--paper); }

/* the bars */

.bars { list-style: none; margin: 0; padding: 0; }
.bar {
  display: grid;
  grid-template-columns: 10.5rem 1fr 3rem;
  align-items: center;
  gap: .75rem;
  padding: .34rem 0 .42rem;
}
.bar-name { font-size: 13.5px; white-space: nowrap; }
.bar-name small { font-size: 11px; color: var(--ink-faint); }
.bar-lane {
  position: relative;
  height: 13px;
  background: var(--row-hover);
  border-radius: 2px;
  /* Visible, so the measured-level ticks can hang below the lane. The fill
     carries its own radius, so there is nothing here that needed clipping. */
  overflow: visible;
}
.bar-lane i {
  display: block; height: 100%; width: 0;
  background: var(--ink-faint);
  border-radius: 2px;
  /* The chart steps between measured levels; this is what makes the step read
     as motion instead of a jump. The scroll no longer sets the width, so the
     easing is the only thing between one measurement and the next. */
  transition: width .42s cubic-bezier(.4, 0, .2, 1);
}

/* This method's four measured drop levels, as ticks under its own lane.
   Under, not on: a tick drawn on the lane has to be legible against the empty
   track and against the bar filling over it, which are opposite ends of the
   palette and swap over between the two themes, so any flat colour disappears
   into one of the four and differencing a grey turns the tick orange over the
   blue bar. Below the lane there is one background to contrast with, and the
   bar is left clean to read a length off. */
.bar-tick {
  position: absolute;
  top: 100%; margin-top: 1.5px;
  width: 1px; height: 3px;
  margin-left: -.5px;
  background: var(--ink-faint);
  opacity: .55;
  pointer-events: none;
}
.bar.ours .bar-name { font-weight: 600; }
.bar.ours .bar-lane i { background: var(--accent); }
.bar-val {
  font-size: 13px; text-align: right;
  font-variant-numeric: lining-nums tabular-nums;
  color: var(--ink-soft);
}
.bar.ours .bar-val { color: var(--ink); font-weight: 600; }

/* Narrow screens stack the stage but keep it pinned, because a walkthrough
   that plays while it slides off the top of the screen is a walkthrough you
   do not see. Everything in it shrinks to fit one phone screen. */
@media (max-width: 62rem) {
  .stage-grid { grid-template-columns: 1fr; gap: 1rem; }
  .rig svg { max-width: 10.5rem; }
  .rig figcaption { display: none; }
  /* The sub-caption is the first thing to go: the level's name is on the
     button that is lit, so it is said twice. */
  .stage-head { margin-bottom: .9rem; padding-bottom: .45rem; flex-wrap: wrap; }
  .stage-step { font-size: 1rem; }
  .stage-sub { display: none; }
  .stage-steps { gap: .25rem; }
  .step-pip { padding: .28rem .4rem; font-size: 10px; }
  .bar { grid-template-columns: 7.5rem 1fr 2.6rem; gap: .5rem; padding: .22rem 0; }
  .bar-name { font-size: 12px; }
  .bar-name small { display: none; }
  .bar-lane { height: 11px; }
  .bar-val { font-size: 12px; }
  .chart .tcap { font-size: 11px; }
  .stage-note { margin-top: 1rem; font-size: 13.5px; }
}

/* Too short to pin anything: let it stack and run off its own passage
   through the viewport, which failure.js falls back to on its own. */
@media (max-width: 62rem) and (max-height: 34rem) {
  .stage { position: static; padding: 1.5rem 0; }
  .track { height: auto !important; }
}

/* ---------- the road ---------- */

/* The rail reserves its own gutter, the way a scrollbar does, so it never
   sits over the content. It replaces the top bar, and with nothing sticky
   left at the top of the window a pinned section gets the screen back: the
   walkthrough used to start a road's height down from the top of every
   viewport for no reason of its own. */
/* The same gutter on both sides. Reserved on the left alone, it centred the
   text in what was left of the window, which put it half a gutter to the
   right of the window's own centre, and it read as lopsided. The right-hand
   gutter is not wasted: the cockpit and the corner buttons live in it. */
.has-road body { padding-inline: var(--rail-gutter); }
.has-road .topbar { display: none; }
.has-road { scroll-padding-top: 1rem; }
.has-road { --chrome: 1rem; }

.nextup {
  grid-column: 1 / -1;
  margin: .1rem 0 0;
  font-size: 11px;
  color: var(--ink-soft);
  font-variant-numeric: lining-nums tabular-nums;
  white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis;
}
.nextup:empty { display: none; }

/* A band across the top had to get out of the way at the top of the page or
   the first screen was all chrome. A rail covers nothing, so it is simply
   always there. */
#road {
  position: fixed;
  top: 0; bottom: 0; left: 0;
  width: var(--rail-w);
  z-index: 55;
  display: flex;
  flex-direction: column;
  border-right: 1px solid var(--rule);
  background: var(--bar);
}
#road[hidden] { display: none; }

.road-view { position: relative; flex: 1 1 auto; min-height: 0; }
#road-map { display: block; }

/* The signs stand beside the rail rather than on it, which is the whole
   reason the road is down here: on a band across the top they were capped at
   160px and truncated, and out here they have the gutter to themselves. */
#road-signs { position: absolute; inset: 0; pointer-events: none; }

/* Guide signs: green plate, white legend, white inner border, section number
   set the way an exit number sits on the real thing. They stand at their true
   position on the road and slide in from the right. */
.sign {
  position: absolute;
  /* Flush with the rail. The posts are drawn on the rail's canvas and end at
     its edge, so any gap here is a plate floating off its own legs. */
  left: 100%;
  transform: translateY(-50%);
  display: flex; align-items: baseline; gap: .3rem;
  max-width: 118px;
  padding: .26rem .5rem;
  border: 1px solid var(--sign);
  border-radius: 3px;
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .5);
  background: var(--sign);
  color: var(--sign-ink);
  font: inherit; font-size: 12px; line-height: 1.2;
  letter-spacing: .005em;
  white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis;
  opacity: .8;
  cursor: pointer;
  pointer-events: auto;
  transition: opacity .15s ease;
}
.sign[hidden] { display: none; }

.sign b {
  flex: none;
  font-size: 10px; font-weight: 700;
  font-variant-numeric: lining-nums;
  color: rgba(255, 255, 255, .68);
}
.sign:hover { opacity: 1; }
.sign.passed { opacity: .34; }
.sign.here {
  opacity: 1;
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .78), 0 0 0 2px var(--bar);
}
.sign.here b { color: rgba(255, 255, 255, .85); }


/* The route overview: the whole trip down the outer verge, a pip per
   junction and the car's place, and the itinerary card beside the rail
   while the pointer is on it. Built by drive.js from the same stops. */
#route { position: absolute; inset: 0; pointer-events: none; z-index: 2; }
.route-line {
  position: absolute; left: 1px; top: 14px; bottom: 14px; width: 9px;
  pointer-events: auto;
}
.route-line::before {
  content: ""; position: absolute; left: 50%; top: 0; bottom: 0; width: 1.5px;
  transform: translateX(-50%);
  background: var(--sign); opacity: .35; border-radius: 1px;
}
.route-pip {
  position: absolute; left: 50%;
  width: 7px; height: 7px; padding: 0;
  transform: translate(-50%, -50%);
  border: 1.5px solid var(--sign); border-radius: 50%;
  background: var(--bar);
  cursor: pointer;
  transition: transform .15s ease, background-color .15s ease;
}
.route-pip::after { content: ""; position: absolute; inset: -6px; }   /* a target a finger can find */
.route-pip.passed { background: var(--sign); opacity: .5; }
.route-pip.here { background: var(--sign); transform: translate(-50%, -50%) scale(1.35); }
.route-pip:hover { transform: translate(-50%, -50%) scale(1.5); opacity: 1; }
.route-you {
  position: absolute; left: 50%; width: 9px; height: 3px;
  transform: translate(-50%, -50%);
  border-radius: 2px; background: var(--accent);
  pointer-events: none;
}

.route-card {
  position: absolute; left: calc(100% + 10px); top: 12px;
  width: 14.5rem;   /* fixed, so "Passed" turning into "You are here" does not resize it */
  padding: .5rem .35rem .4rem;
  border-radius: 6px;
  background: var(--sign); color: var(--sign-ink);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .45), 0 6px 22px rgba(0, 0, 0, .16);
  opacity: 0; visibility: hidden; transform: translateX(-6px);
  transition: opacity .16s ease, transform .16s ease, visibility 0s linear .16s;
}
#route.open .route-card {
  opacity: 1; visibility: visible; transform: none; pointer-events: auto;
  transition: opacity .16s ease, transform .16s ease, visibility 0s;
}
.route-head {
  margin: 0 .55rem .3rem;
  font-size: 10px; font-weight: 700; letter-spacing: .14em; text-transform: uppercase;
  opacity: .66;
}
.route-list { list-style: none; margin: 0; padding: 0; }
.route-list button {
  display: grid; grid-template-columns: 1rem 1fr auto; gap: .55rem; align-items: baseline;
  width: 100%; padding: .34rem .55rem;
  border: 0; border-radius: 4px; background: none;
  color: inherit; font: inherit; font-size: 13px; line-height: 1.25; text-align: left;
  cursor: pointer;
}
.route-list button:hover, .route-list button:focus-visible { background: rgba(255, 255, 255, .13); outline: none; }
.route-list b { font-size: 11px; font-variant-numeric: lining-nums; opacity: .7; }
.route-list small { font-size: 11px; font-variant-numeric: tabular-nums; opacity: .72; white-space: nowrap; }
.route-list .passed { opacity: .55; }
.route-list .here { background: rgba(255, 255, 255, .17); }
.route-list .here b, .route-list .here small { opacity: 1; }
.route-list .here small { font-weight: 600; }
@media (prefers-reduced-motion: reduce) { .route-card, #route.open .route-card { transition: none; } }

/* ---------- the cockpit ---------- */

#cockpit {
  position: fixed;
  right: 1rem; bottom: 1rem;
  z-index: 55;
  width: 13.5rem;
  padding: 1.15rem .65rem .55rem;
  border: 1px solid var(--rule-dark);
  border-radius: 5px;
  background: var(--bar);
  box-shadow: 0 6px 22px rgba(0, 0, 0, .10);
  transition: transform .3s cubic-bezier(.2, .7, .3, 1), opacity .25s ease;
}
#cockpit[hidden] { display: none; }
/* Folded, the cockpit is one small button and nothing else. It used to slide
   down and leave its top 1.9rem on screen, which was the top of the gauge cut
   off mid-arc: a panel half hidden rather than put away. */
#cockpit.shut { width: auto; padding: 0; }
#cockpit.shut > :not(#cockpit-toggle) { display: none; }
#cockpit-toggle {
  position: absolute; top: .35rem; right: .4rem;
  width: 1.2rem; height: 1.2rem;
  display: grid; place-items: center;
  padding: 0;
  background: none; border: 0; border-radius: 3px;
  color: var(--ink-faint);
  cursor: pointer;
}
#cockpit-toggle:hover { color: var(--ink); }
#cockpit-toggle svg {
  width: 13px; height: 13px;
  fill: none; stroke: currentColor; stroke-width: 1.7;
  stroke-linecap: round; stroke-linejoin: round;
  transition: transform .2s ease;
}
#cockpit.shut #cockpit-toggle svg { transform: rotate(180deg); }
.toggle-label { display: none; }
#cockpit.shut #cockpit-toggle {
  position: static;
  width: auto; height: auto;
  display: flex; align-items: center; gap: .4rem;
  padding: .45rem .7rem;
  color: var(--ink-soft);
  font: inherit; font-size: 12px; font-weight: 600; letter-spacing: .04em;
}
#cockpit.shut #cockpit-toggle:hover { color: var(--ink); }
#cockpit.shut .toggle-label { display: inline; }
/* First visit, folded: the button lights twice the way the accelerator does
   when the cockpit is open, so it reads as something to press. */
#cockpit.shut.nudge { animation: cockpit-nudge 1.5s ease-in-out 2; }
@keyframes cockpit-nudge {
  0%, 100% { border-color: var(--rule-dark); }
  45%      { border-color: var(--go); box-shadow: 0 6px 22px rgba(0, 0, 0, .10), 0 0 0 3px color-mix(in srgb, var(--go) 18%, transparent); }
}
#cockpit.shut.nudge #cockpit-toggle { animation: cockpit-nudge-ink 1.5s ease-in-out 2; }
@keyframes cockpit-nudge-ink {
  0%, 100% { color: var(--ink-soft); }
  45%      { color: var(--go); }
}

.cluster {
  display: flex; align-items: center; gap: .5rem;
  margin-bottom: .45rem;
}

/* Analogue, because a needle sweeping a scale reads as speed and a number
   changing does not. Ticks, rev counter and gear are all drawn in drive.js. */
#gauge {
  display: block;
  width: 7.5rem; height: 7.5rem;
  margin-left: -.15rem;
}

.readout {
  margin: 0;
  font-size: 10px;
  letter-spacing: .03em;
  font-variant-numeric: lining-nums tabular-nums;
  color: var(--ink-faint);
}

.pedals { display: grid; grid-template-columns: 1fr 1fr; gap: .35rem; }

.pedal {
  display: flex; flex-direction: column; align-items: center; gap: .1rem;
  padding: .4rem .3rem .32rem;
  border: 1px solid var(--rule-dark);
  border-radius: 3px;
  background: none;
  color: var(--ink-soft);
  font: inherit; font-size: 10px; font-weight: 600;
  letter-spacing: .05em; text-transform: uppercase;
  cursor: pointer;
  transition: color .15s ease, border-color .15s ease, background .15s ease;
}
.pedal svg {
  width: 20px; height: 20px; fill: currentColor;
  /* The pedal arm swings on its top hinge, so travel is a rotation, and the
     amount of it is however far the model says the pedal is down. */
  transform: perspective(46px) rotateX(calc(var(--travel, 0) * 38deg));
  transform-origin: 50% 12%;
}
.pedal .floor { stroke: currentColor; stroke-width: 1.4; stroke-linecap: round; fill: none; opacity: .5; }
.pedal {
  --travel: 0;
  position: relative;
  overflow: hidden;
}
/* How hard it is down, behind the glyph. */
.pedal::after {
  content: "";
  position: absolute; left: 0; right: 0; bottom: 0;
  height: calc(var(--travel, 0) * 100%);
  background: currentColor;
  opacity: .13;
  pointer-events: none;
}

/* The first-visit nudge: the accelerator lights the way it does under a
   cursor, twice, and then the hint is gone for good. */
.hinting .pedal.gas { animation: pedal-nudge 1.5s ease-in-out 2; }
@keyframes pedal-nudge {
  0%, 100% { color: var(--ink-soft); border-color: var(--rule-dark); }
  45%      { color: var(--go);       border-color: var(--go); }
}
/* Faint grey at 11px, it was missed by the person who asked for it. A callout
   rule and the body ink: still small, but findable. */
#drive-hint {
  margin: 0 0 .4rem;
  padding-left: .4rem;
  border-left: 2px solid var(--go);
  font-size: 11.5px; line-height: 1.35;
  color: var(--ink-soft);
}
#drive-hint[hidden] { display: none; }

.pedal.gas:hover, .pedal.gas:active { color: var(--go); border-color: var(--go); }
.pedal.brake:hover, .pedal.brake:active { color: var(--stop); border-color: var(--stop); }

#cockpit.moving .pedal.gas { color: #fff; background: var(--go); border-color: var(--go); }


/* ---------- section reveal ---------- */

.js .reveal > .col { opacity: 0; transform: translateY(7px); }
.js .reveal.in > .col {
  opacity: 1; transform: none;
  transition: opacity .55s ease, transform .55s cubic-bezier(.2, .7, .3, 1);
}
.js .reveal.in > .col:nth-child(2) { transition-delay: .06s; }
.js .reveal.in > .col:nth-child(3) { transition-delay: .11s; }

/* ---------- layout ---------- */

.col { width: min(100% - var(--gutter), var(--col)); margin-inline: auto; }
.col.wide { width: min(100% - var(--gutter), var(--wide)); }

/* Sections are separated by space and by their headings, not by rules. There
   used to be a line above every section and another under every heading, so
   each boundary drew two, and the equation drew two more of its own. */
section { padding: 3.4rem 0; }

/* ---------- top bar ---------- */

.topbar {
  position: sticky; top: 0; z-index: 50;
  background: var(--bar);
  border-bottom: 1px solid var(--rule);
}
.topbar-inner {
  width: min(100% - var(--gutter), var(--wide));
  margin-inline: auto;
  display: flex; align-items: center; gap: 1.25rem;
  height: 2.6rem;
}

.nav-scroll { flex: 1 1 auto; overflow-x: auto; scrollbar-width: none; }
.nav-scroll::-webkit-scrollbar { display: none; }

.topbar ul {
  display: flex; gap: 1.15rem;
  list-style: none; margin: 0; padding: 0;
  font-size: 13.5px; line-height: 2.6rem;
  white-space: nowrap;
}
.topbar li { flex: none; }
/* five sections, one now "Quantitative": tighter on a phone so the last one is not cut */
@media (max-width: 430px) { .topbar ul { gap: .8rem; font-size: 13px; } }
.topbar ul a { color: var(--ink-soft); }
.topbar ul a:hover { color: var(--ink); text-decoration: none; }
.topbar ul a.current { color: var(--ink); box-shadow: inset 0 -2px 0 var(--accent); }

/* ---------- theme toggle ---------- */

/* Off the top bar and into the corner opposite the cockpit, so the bar can be
   the links and nothing else. */
.corner {
  position: fixed;
  left: 1.1rem; bottom: 1.1rem;
  z-index: 46;
  display: flex; gap: .4rem;
}
/* With the rail down the left edge, the left-hand gutter belongs to the road
   and its guide signs, and the bottom-right corner to the cockpit. The top
   right is the one corner nothing else wants: there is no top bar while the
   rail is up. Stacked, so they take as little of the margin as they can. */
.has-road .corner {
  left: auto; bottom: auto;
  top: 1.1rem; right: 1.1rem;
  flex-direction: column;
}
#to-top, #theme-toggle {
  width: 2.25rem; height: 2.25rem;
  display: grid; place-items: center;
  padding: 0;
  background: var(--bar);
  border: 1px solid var(--rule-dark);
  border-radius: 999px;
  color: var(--ink-soft);
  cursor: pointer;
  transition: color .2s ease, border-color .2s ease;
}
#to-top:hover, #theme-toggle:hover { color: var(--ink); border-color: var(--ink); }
#to-top svg { width: 15px; height: 15px; }

#theme-toggle svg { width: 17px; height: 17px; overflow: visible; }

#theme-toggle .disc {
  fill: currentColor;
  transition: r .45s cubic-bezier(.4, 0, .2, 1);
}
#theme-toggle .rays {
  stroke: currentColor; stroke-width: 1.7; stroke-linecap: round;
  transform-origin: 12px 12px;
  transition: opacity .3s ease, transform .45s cubic-bezier(.4, 0, .2, 1);
}
#theme-toggle #crescent circle {
  transition: cx .45s cubic-bezier(.4, 0, .2, 1),
              cy .45s cubic-bezier(.4, 0, .2, 1);
}

/* Dark: pull the mask circle in to carve a crescent, retract the rays. */
:root[data-theme="dark"] #theme-toggle .disc { r: 8.5; }
:root[data-theme="dark"] #theme-toggle #crescent circle { cx: 18.5; cy: 6.5; }
:root[data-theme="dark"] #theme-toggle .rays { opacity: 0; transform: rotate(-45deg) scale(.4); }

/* ---------- theme transition ---------- */

/* Browsers with the View Transitions API get the circular wipe driven from
   main.js; everything else just cross-fades the palette. */
@supports not (view-transition-name: none) {
  body, .topbar, .figure img, .placeholder, .bibtex pre,
  table, tr.ours, .button, .scene-picker button {
    transition: background-color .35s ease, color .35s ease, border-color .35s ease;
  }
}

::view-transition-old(root),
::view-transition-new(root) {
  animation: none;
  mix-blend-mode: normal;
}
::view-transition-old(root) { z-index: 1; }
::view-transition-new(root) { z-index: 2; }

/* Contracting wipe: the outgoing palette has to sit on top to shrink away. */
:root[data-wipe="out"]::view-transition-old(root) { z-index: 2; }
:root[data-wipe="out"]::view-transition-new(root) { z-index: 1; }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  * { animation-duration: .01ms !important; transition-duration: .01ms !important; }
  .js .reveal > .col { opacity: 1; transform: none; }
  /* A driving model is motion by definition. drive.js declines to start under
     this preference; this is the belt to that pair of braces. */
  #road, #cockpit { display: none !important; }
  .has-road body { padding-inline: 0; }
  .has-road .topbar { display: block; top: 0; }
  .has-road .nav-scroll { display: block; }
}



/* ---------- hero ---------- */

.hero { padding: 3rem 0 1.6rem; }

/* The venue, in the accent and at reading size rather than as a filled pill.
   A chip with letterspaced capitals in it is a component; this is a line of
   type that happens to be the one coloured thing above the fold. */
.venue {
  margin: 0 0 .9rem;
  font-size: 15px; font-weight: 600; line-height: 1;
  letter-spacing: -.005em;
  color: var(--accent);
}

h1 { margin: 0 0 1.4rem; }
.h1-name {
  display: block;
  font-family: var(--display);
  font-size: clamp(2.8rem, 6.4vw, 4.1rem); font-weight: 700;
  line-height: 1;
  letter-spacing: -.035em;
  color: var(--ink);
}
/* The name as its own outlines. At rest it is the solid letters and looks
   exactly as the text did. Under the pointer a round lens follows it, and
   inside the lens the letters are shown as the HD map a model would build
   from them: road boundaries along their edges, lane dividers down the
   middle of every stroke, a pedestrian crossing across a few of the stems,
   in the class colours the road rail uses. The lens
   covers the solid letters with page-coloured copies of them rather than
   cutting them, so there is no seam at rest, and nothing outside the letters
   is ever covered. */
.name-svg { display: block; height: 1em; width: auto; overflow: visible; }
.name-svg .nm-solid { fill: currentColor; }
.name-svg .nm-map { opacity: 0; transition: opacity .3s ease; }
.name-svg.has-roi .nm-map { opacity: 1; }
/* Page colour over the solid letters, a pixel wider than they are so no
   anti-aliased rim of the ink shows through inside the lens. */
.name-svg .paper { fill: var(--paper); stroke: var(--paper); stroke-width: 2px; vector-effect: non-scaling-stroke; }
.name-svg .ghost { fill: currentColor; fill-opacity: .07; }
.name-svg .boundary, .name-svg .divider, .name-svg .crossing {
  fill: none; stroke-width: 1.7px; vector-effect: non-scaling-stroke;
  stroke-linejoin: round; stroke-linecap: round;
}
.name-svg .boundary { stroke: var(--map-boundary); }
.name-svg .divider { stroke: var(--map-divider); }
.name-svg .crossing { stroke: var(--map-crossing); }
@media (prefers-reduced-motion: reduce) {
  .name-svg .nm-map { transition: none; }
}
.h1-sub {
  display: block;
  margin-top: .45rem;
  font-family: var(--display);
  font-size: clamp(1.02rem, 2.3vw, 1.42rem); font-weight: 600;
  line-height: 1.3;
  letter-spacing: -.014em;
  color: var(--ink-soft);
}


.authors { margin: 0 0 .35rem; line-height: 1.8; }
.authors a { color: var(--ink); }
.authors a:hover { color: var(--accent); }
.authors sup { font-size: .68em; color: var(--ink-faint); }

.affil { margin: 0; color: var(--ink-soft); }
/* The marks under the author list, on a line of their own under the lab
   rather than trailing it, where they read as part of the affiliation. */
.author-notes { margin: .2rem 0 .55rem; font-size: 13px; }
/* No underline, like the author names above it; it darkens under the cursor. */
.affil a { color: inherit; text-decoration: none; }
.affil a:hover { color: var(--ink); }

.notes, .small { font-size: 14px; line-height: 1.6; color: var(--ink-faint); margin: 0; }
.small { margin-top: 1rem; }

.links { margin: 1.6rem 0 0; display: flex; flex-wrap: wrap; gap: .5rem; }

.button {
  display: inline-block;
  font-size: 14px; font-weight: 500;
  color: var(--ink);
  border: 1px solid var(--rule-dark);
  padding: .58rem .95rem;
  border-radius: 3px;
  transition: border-color .15s ease, color .15s ease;
}
.button:hover { border-color: var(--ink); text-decoration: none; }
.button[aria-disabled="true"] { color: var(--ink-faint); border-style: dashed; cursor: default; }
.button[aria-disabled="true"]:hover { border-color: var(--rule-dark); }
.button .soon { font-weight: 400; color: var(--ink-faint); }

/* ---------- headings and prose ---------- */

/* A section heading is a title in the display face with its number on the
   same line, in the accent. Hierarchy comes from size and the face, so it
   needs no rule under it to say where a section starts. (The number sat on a
   line of its own for a while; a lone digit over a heading reads as a stray.) */
h2 {
  font-family: var(--display);
  font-size: 1.72rem; font-weight: 700; line-height: 1.2;
  letter-spacing: -.022em;
  margin: 0 0 1.3rem;
}
h2 .num {
  margin-right: .5em;
  font-weight: 600;
  color: var(--accent);
  font-variant-numeric: lining-nums tabular-nums;
}

h3 { font-size: 1.02rem; font-weight: 600; letter-spacing: -.012em; margin: 2.6rem 0 .7rem; }

/* A subheading directly over a table stays with it. The heading ends its
   prose column and the table opens the wide one, so the two margins collapsed
   to the table's 1.9rem: the heading sat further from its own table than from
   the paragraph above it. */
.col:has(> h3:last-child) + .col.wide > :first-child { margin-top: .8rem; }

p { margin: 0 0 1.05rem; }
p:last-child { margin-bottom: 0; }

b, strong { font-weight: 600; }

/* Real MathML, so the browser sets it in its own maths face: no library, no
   webfont, and subscripts that sit where a typesetter would put them. */
.eq { margin: 1.7rem 0; }
/* A display equation wider than a phone scrolls on its own instead of
   widening the page: on a 320-390 px screen the fusion fraction ran to 397 px
   and the whole document shifted sideways under a thumb. */
.eq { overflow-x: auto; overflow-y: hidden; -webkit-overflow-scrolling: touch; }
@media (max-width: 30rem) { .eq math[display="block"] { font-size: 1.02em; } }
math { font-size: 1.04em; }
math[display="block"] { font-size: 1.22em; }
.eq math { color: var(--ink); }

.steps { margin: 0 0 1.05rem; padding-left: 1.4rem; }
.steps li { margin-bottom: .6rem; }

/* ---------- figures ---------- */

.figure { margin: 1.9rem 0; }
.figure img, .figure video {
  display: block;
  width: 100%; height: auto;
  border: 1px solid var(--rule);
  border-radius: 3px;
  background: var(--figure-bg);
}
figcaption, .tcap {
  font-size: 14px; line-height: 1.6;
  color: var(--ink-soft);
  margin-top: .75rem;
  margin-left: max(0px, calc((100% - var(--col)) / 2));
  max-width: var(--col);
}

.placeholder {
  display: flex; align-items: center; justify-content: center;
  min-height: 13rem;
  border: 1px dashed var(--rule-dark);
  border-radius: 3px;
  background: var(--surface);
  font-size: 13.5px; line-height: 1.6;
  color: var(--ink-faint);
  text-align: center;
  padding: 1.5rem;
}

/* Two rows of tabs over the clip: which scene, and which of the paper's three
   settings. Labelled, because two bare rows of buttons read as one list. */
.picker-row { display: flex; align-items: baseline; gap: .9rem; margin-top: .55rem; }
.picker-row:first-child { margin-top: 1.9rem; }
.picker-row[hidden] { display: none; }
.picker-label {
  flex: none; width: 3.8rem;
  font-size: 11px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase;
  color: var(--ink-faint);
}
.picker-row + .figure { margin-top: 1rem; }

.scene-picker, .table-tabs { display: flex; flex-wrap: wrap; gap: .4rem; }
/* The dataset switch over a results table: the same buttons as the clip
   picker, so the page has one kind of tab. */
.table-tabs { margin-bottom: .9rem; }
.table-tabs[hidden], .tpanel[hidden] { display: none; }
.table-tabs button[data-draft]::after { content: " \00b7 draft"; font-weight: 400; opacity: .7; }
/* A row still waiting on its numbers. Only ever seen in draft. */
tr.pending th, tr.pending td { color: var(--ink-faint); font-style: normal; }

/* Where a clip will be. Dashed and quiet, the same voice as the disabled
   Paper and Code buttons. Sized like a clip in draft; one line on the live
   page, where there is nothing to lay out yet. */
.video-pending {
  display: flex; align-items: center; justify-content: center;
  aspect-ratio: 16 / 9;
  padding: 1.5rem;
  border: 1px dashed var(--rule-dark);
  border-radius: 3px;
  background: var(--surface);
  font-size: 13.5px; line-height: 1.6;
  color: var(--ink-faint);
  text-align: center;
}
.video-pending.quiet { aspect-ratio: auto; min-height: 5.5rem; }
video[hidden], figcaption[hidden] { display: none; }

/* Playback speed under a clip: the picker's buttons, smaller, and gone with
   the clip when there is no clip. */
.speed { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: .3rem; margin-top: .55rem; }
video[hidden] + .speed { display: none; }

/* The teaser's seek bar, over its buttons: a range input with the played part
   filled in the accent, chapter gaps cut into it, the time at its end. */
.vcontrols { margin-top: .55rem; }
.vcontrols .speed { margin-top: .3rem; }
.seek { display: flex; align-items: center; gap: .75rem; }
.seek-bar { position: relative; flex: 1 1 auto; min-width: 0; --th: 4px; --tw: 14px; }
/* Under the pointer, or while dragged, the bar thickens as on YouTube and a
   label over the pointer names the time and chapter it would jump to. */
.seek-bar.live { --th: 7px; --tw: 17px; }
.seek-tip {
  position: absolute; bottom: calc(100% - 2px); left: 0;
  transform: translateX(-50%);
  display: flex; flex-direction: column; align-items: center; gap: .1rem;
  padding: .35rem .6rem;
  border-radius: 3px;
  background: var(--ink); color: var(--paper);
  font-size: 12px; line-height: 1.35; white-space: nowrap;
  font-variant-numeric: tabular-nums;
  pointer-events: none;
  opacity: 0;
  transition: opacity .12s ease;
}
.seek-tip b { font-weight: 600; }
.seek-bar.live .seek-tip { opacity: 1; }
.lightbox.player .seek-tip { background: #fff; color: #0a0c0f; }
/* Chapter boundaries: page-coloured gaps in the track itself (--gaps, set
   by main.js), so the thumb always passes over them. */
.seek-bar { --gap-c: var(--paper); }
.lightbox.player .seek-bar { --gap-c: #0a0c0f; }
.seek input[type="range"] {
  --p: 0%; --h: 0%;
  display: block; width: 100%;
  height: 26px; margin: 0;
  background: transparent;
  -webkit-appearance: none; appearance: none;
  cursor: pointer;
  touch-action: none;
}
/* Played part in the accent; up to the pointer, a lighter fill. */
.seek input[type="range"]::-webkit-slider-runnable-track {
  height: var(--th); border-radius: 3px;
  background: var(--gaps, none),
              linear-gradient(to right, var(--accent) var(--p), var(--ink-faint) var(--p),
                              var(--ink-faint) var(--h), var(--rule-dark) var(--h));
  transition: height .12s ease;
}
.seek input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: var(--tw); height: var(--tw); margin-top: calc((var(--th) - var(--tw)) / 2);
  border: 0; border-radius: 50%;
  background: var(--accent);
  transition: width .12s ease, height .12s ease, margin-top .12s ease;
}
.seek input[type="range"]::-moz-range-track { height: var(--th); border-radius: 3px; background: var(--gaps, none), var(--rule-dark); }
.seek input[type="range"]::-moz-range-progress { height: var(--th); border-radius: 3px; background: var(--accent); }
.seek input[type="range"]::-moz-range-thumb { width: var(--tw); height: var(--tw); border: 0; border-radius: 50%; background: var(--accent); }
.seek input[type="range"]:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.seek-time {
  flex: none;
  font-size: 12px; font-variant-numeric: tabular-nums;
  color: var(--ink-soft);
}
.speed button {
  font: inherit; font-size: 12.5px; font-weight: 500;
  font-variant-numeric: tabular-nums;
  color: var(--ink-soft);
  background: none;
  border: 1px solid var(--rule-dark);
  border-radius: 3px;
  padding: .4rem .65rem;
  cursor: pointer;
  transition: color .15s ease, border-color .15s ease, background .15s ease;
}
.speed button:hover { color: var(--ink); border-color: var(--ink); }
.speed button[aria-pressed="true"] { color: var(--paper); background: var(--ink); border-color: var(--ink); }
/* Play/pause at the start of the row, full screen at the end. */
.speed .icon { display: grid; place-items: center; padding: .35rem .5rem; }
.speed .icon.play { margin-right: auto; }
.speed .icon svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 2;
  stroke-linecap: round; stroke-linejoin: round; }
.speed .icon .i-play { fill: currentColor; stroke: none; }
.speed .icon .i-replay { fill: none; }
.speed .play[data-state="playing"] .i-play, .speed .play[data-state="playing"] .i-replay,
.speed .play[data-state="paused"] .i-pause, .speed .play[data-state="paused"] .i-replay,
.speed .play[data-state="ended"] .i-play, .speed .play[data-state="ended"] .i-pause { display: none; }

/* End card, over a clip that has played through. */
.video-end {
  position: absolute; inset: 0; z-index: 3;
  display: flex; flex-wrap: wrap; align-items: center; justify-content: center; align-content: center;
  gap: .6rem;
  padding: 1rem;
  border-radius: 3px;
  background: rgba(10, 12, 15, .62);
}
.video-end[hidden] { display: none; }
/* The end card has its own way to the next clip; the pill and zoom mark step aside. */
.vwrap:has(.video-end:not([hidden])) .reel-peek,
.vwrap:has(.video-end:not([hidden])) .video-open-icon { display: none; }
.ve-btn {
  display: inline-flex; align-items: center; gap: .4rem;
  padding: .6rem 1.05rem;
  border: 1px solid rgba(255, 255, 255, .55); border-radius: 999px;
  background: rgba(10, 12, 15, .4); color: #fff;
  font: inherit; font-size: 14px; font-weight: 600; text-decoration: none; white-space: nowrap;
  cursor: pointer;
}
.ve-btn:hover { border-color: #fff; background: rgba(10, 12, 15, .7); }
.ve-btn.primary { background: #fff; color: #0a0c0f; border-color: #fff; }
.ve-btn.primary:hover { background: #e9ebee; }
.ve-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
@media (max-width: 46rem) {
  .video-end { gap: .4rem; }
  .ve-btn { padding: .45rem .8rem; font-size: 12.5px; }
}

/* ---------- the teaser reel ----------
   Two clips side by side, one swipe apart. The track scrolls sideways and
   snaps to a clip; the tabs over it and the pill on each clip move it too. */
.reel { position: relative; margin: 1.9rem 0; }   /* the figure's own margin, which the slides drop */
.reel-tabs { margin-bottom: .75rem; }
.reel-track {
  position: relative;
  display: flex; gap: 1.5rem;
  overflow-x: auto; overflow-y: hidden;
  scroll-snap-type: x mandatory;
  overscroll-behavior-x: contain;
  scrollbar-width: none;
}
.reel-track::-webkit-scrollbar { display: none; }
.reel-slide { flex: 0 0 100%; min-width: 0; margin: 0; scroll-snap-align: start; scroll-snap-stop: always; }
.reel.single .reel-tabs, .reel.single .reel-peek { display: none; }
/* The one-time hint: the clips lean left far enough to show the next one's edge. */
.reel-track.nudge .reel-slide { animation: reel-nudge .95s cubic-bezier(.45, 0, .25, 1) 1; }
@keyframes reel-nudge {
  0%, 100% { transform: none; }
  45% { transform: translateX(min(-72px, -9%)); }
}

/* The pill on a clip's edge that says there is another one. It sits in the
   white margin the clips leave beside their panels; on a phone that margin
   is narrow, so it shrinks to the arrow. */
.reel-peek {
  position: absolute; top: 50%; z-index: 2;
  transform: translateY(-50%);
  display: inline-flex; align-items: center; gap: .35rem;
  padding: .45rem .8rem;
  border: 0; border-radius: 999px;
  background: rgba(10, 12, 15, .74); color: #fff;
  font: inherit; font-size: 13px; font-weight: 600; white-space: nowrap;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.reel-peek:hover { background: rgba(10, 12, 15, .9); }
.reel-peek:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.reel-peek span { font-size: 1.25em; line-height: 1; }
.reel-peek.next { right: .6rem; }
.reel-peek.prev { left: .6rem; }
.reel.hint .reel-peek.next { animation: peek-nudge 1.2s ease-in-out 3; }
@keyframes peek-nudge {
  0%, 100% { transform: translateY(-50%) translateX(0); }
  50% { transform: translateY(-50%) translateX(7px); }
}
@media (max-width: 46rem) {
  .reel-peek { padding: 0; width: 34px; height: 34px; justify-content: center; font-size: 0; }
  .reel-peek span { font-size: 22px; }
  .reel-peek.next { right: .3rem; }
  .reel-peek.prev { left: .3rem; }
}
@media (prefers-reduced-motion: reduce) { .reel.hint .reel-peek.next, .reel-track.nudge .reel-slide { animation: none; } }

/* The teaser opens in the figure viewer: a button lies over the clip, with
   the expand mark in its corner so a phone reader knows it can be tapped. */
.vwrap { position: relative; }
.video-open {
  position: absolute; inset: 0; z-index: 1;
  display: flex; align-items: flex-end; justify-content: flex-end;
  padding: 0; border: 0; background: none;
  cursor: zoom-in;
  -webkit-tap-highlight-color: transparent;
}
.video-open[hidden] { display: none; }
.video-open-icon {
  display: grid; place-items: center;
  width: 36px; height: 36px; margin: .6rem;
  border-radius: 999px;
  background: rgba(10, 12, 15, .55);
  color: #fff;
  opacity: .85;
  transition: opacity .15s ease;
}
.video-open:hover .video-open-icon, .video-open:focus-visible .video-open-icon { opacity: 1; }
.video-open:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
.video-open svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 2;
  stroke-linecap: round; stroke-linejoin: round; }
.video-holder { border-radius: 3px; background: var(--surface); }
.lightbox.player .lightbox-stage video {
  display: block;
  width: min(96vw, 1800px, calc((100dvh - 12rem) * 16 / 9));
  height: auto;
  border-radius: 3px;
  background: #000;
  box-shadow: 0 12px 40px rgba(0, 0, 0, .35);
  cursor: pointer;
}
.lightbox.player .vcontrols {
  width: min(96vw, 1800px, calc((100dvh - 12rem) * 16 / 9));
  margin-top: 0;
}
.lightbox.player .seek input[type="range"]::-webkit-slider-runnable-track {
  background: var(--gaps, none),
              linear-gradient(to right, #fff var(--p), rgba(255, 255, 255, .55) var(--p),
                              rgba(255, 255, 255, .55) var(--h), rgba(255, 255, 255, .28) var(--h));
}
.lightbox.player .seek input[type="range"]::-webkit-slider-thumb { background: #fff; }
.lightbox.player .seek input[type="range"]::-moz-range-track { background: rgba(255, 255, 255, .28); }
.lightbox.player .seek input[type="range"]::-moz-range-progress,
.lightbox.player .seek input[type="range"]::-moz-range-thumb { background: #fff; }
.lightbox.player .seek-time { color: rgba(235, 238, 242, .86); }
.lightbox.player .speed button { color: rgba(235, 238, 242, .86); border-color: rgba(255, 255, 255, .3); }
.lightbox.player .speed button:hover { color: #fff; border-color: #fff; }
.lightbox.player .speed button[aria-pressed="true"] { color: #0a0c0f; background: #fff; border-color: #fff; }
/* A phone turned sideways: the clip takes the height, the caption goes. */
@media (orientation: landscape) and (max-height: 34rem) {
  .lightbox.player { gap: .4rem; padding-top: max(.5rem, env(safe-area-inset-top)); padding-bottom: max(.5rem, env(safe-area-inset-bottom)); }
  .lightbox.player .lightbox-cap, .lightbox.player .lightbox-hint { display: none; }
  .lightbox.player .lightbox-stage video,
  .lightbox.player .vcontrols { width: min(86vw, calc((100dvh - 7.5rem) * 16 / 9)); }
}
.scene-picker button, .table-tabs button {
  font: inherit; font-size: 13.5px; font-weight: 500;
  color: var(--ink-soft);
  background: none;
  border: 1px solid var(--rule-dark);
  border-radius: 3px;
  padding: .5rem .8rem;
  cursor: pointer;
  transition: color .15s ease, border-color .15s ease, background .15s ease;
}
.scene-picker button:hover, .table-tabs button:hover { color: var(--ink); border-color: var(--ink); }
.scene-picker button[aria-selected="true"], .table-tabs button[aria-selected="true"] {
  color: var(--paper); background: var(--ink); border-color: var(--ink);
}

/* ---------- tables ---------- */

.table-block { margin: 1.9rem 0; }
.scroll { overflow-x: auto; }

table {
  border-collapse: collapse;
  width: 100%;
  font-size: 13.5px; line-height: 1.45;
  font-variant-numeric: lining-nums tabular-nums;
  white-space: nowrap;
}

thead th {
  font-weight: 600;
  text-align: left;
  padding: .5rem .7rem;
  border-top: 1.5px solid var(--ink);
  border-bottom: 1px solid var(--rule-dark);
  vertical-align: bottom;
}
tbody th, tbody td { padding: .42rem .7rem; }
tbody th { font-weight: 400; text-align: left; }
tbody tr:last-child th, tbody tr:last-child td { border-bottom: 1.5px solid var(--ink); }

th.n, td.n { text-align: right; }
td.b, .n.b { font-weight: 600; }

tr.group th {
  font-size: 12px; font-weight: 600;
  letter-spacing: .08em; text-transform: uppercase;
  color: var(--ink-faint);
  padding: 1.05rem .7rem .35rem;
  border-bottom: 1px solid var(--rule);
}
tbody tr.group:first-child th { padding-top: .55rem; }

tbody tr:not(.group):hover th,
tbody tr:not(.group):hover td { background-color: var(--row-hover); }

tr.ours { background: var(--ours-bg); }
tbody tr.ours th:first-child { box-shadow: inset 3px 0 0 var(--accent); }
tr.ours th { font-weight: 600; }

span.dn {
  display: inline-block;
  margin-left: .35rem;
  font-size: 11.5px;
  color: var(--drop);
}

.two-up {
  display: grid;
  gap: 1.6rem 2.2rem;
  grid-template-columns: 1fr 1fr;
  align-items: start;
}
.two-up .table-block { margin: 0; }

/* ---------- bibtex ---------- */

.bibtex { position: relative; margin: 1.5rem 0 0; }
/* The note under it had no margin of its own and sat flush on the box. */
.bibtex + .small { margin-top: .9rem; }
.bibtex pre {
  margin: 0;
  padding: 1.1rem 1.2rem;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: 3px;
  overflow-x: auto;
  font: 400 12.5px/1.65 var(--mono);
  color: var(--ink-soft);
}
.bibtex button {
  position: absolute; top: .55rem; right: .55rem;
  font: inherit; font-size: 12.5px; font-weight: 500;
  color: var(--ink-soft);
  background: var(--paper);
  border: 1px solid var(--rule-dark);
  border-radius: 3px;
  padding: .35rem .6rem;
  cursor: pointer;
}
.bibtex button:hover { color: var(--ink); border-color: var(--ink); }

/* ---------- footer ---------- */

footer {
  border-top: 1px solid var(--rule);
  padding: 2rem 0 3.5rem;
  font-size: 14px; line-height: 1.6;
  color: var(--ink-soft);
}
footer p { margin: 0; }
footer .small { margin-top: .4rem; }

/* ---------- narrow screens ---------- */

@media (max-width: 46rem) {
  :root { --gutter: 1.75rem; }
  body { font-size: 16.5px; }
  h1 { font-size: 1.6rem; }
  h2 { font-size: 1.45rem; }
  .hero { padding-top: 2.4rem; }
  .two-up { grid-template-columns: 1fr; }
}

@media print {
  .topbar, .scene-picker, .bibtex button, #road, #cockpit, .corner { display: none; }
  /* The rail reserves a gutter on screen; on paper there is nothing to reserve. */
  .has-road body { padding-inline: 0; }
  body { font-size: 10.5pt; }
  .col, .col.wide { width: 100%; }
}

/* ---------- figures, full screen ---------- */

.figure img.zoomable { cursor: zoom-in; }
.figure img.zoomable:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }

.lightbox {
  position: fixed; inset: 0;
  z-index: 200;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: .9rem;
  padding: max(3.2rem, env(safe-area-inset-top)) max(1rem, env(safe-area-inset-right))
           max(1.2rem, env(safe-area-inset-bottom)) max(1rem, env(safe-area-inset-left));
  background: #0a0c0f;
  opacity: 0;
  transition: opacity .18s ease;
  overscroll-behavior: contain;
}
.lightbox[hidden] { display: none; }
.lightbox.shown { opacity: 1; }
/* The page behind stays where it was. */
:root[data-lightbox] { overflow: hidden; }

.lightbox-stage {
  display: flex; align-items: center; justify-content: center;
  max-width: 100%; max-height: 100%;
  min-height: 0;
  flex: 0 1 auto;
}
.lightbox-stage img {
  display: block;
  max-width: min(96vw, 1800px);
  max-height: calc(100vh - 9rem);
  max-height: calc(100dvh - 9rem);
  width: auto; height: auto;
  object-fit: contain;
  /* Figures are drawn on white; they stay on white in the dark theme too. */
  background: #fff;
  border-radius: 3px;
  box-shadow: 0 12px 40px rgba(0, 0, 0, .35);
  cursor: zoom-in;
  transform: scale(.97);
  transition: transform .18s ease;
  touch-action: pinch-zoom;
}
.lightbox.shown .lightbox-stage img { transform: none; }

/* Natural size: the stage scrolls and the image is as big as it was drawn. */
.lightbox.actual .lightbox-stage {
  align-self: stretch;
  overflow: auto;
  justify-content: flex-start; align-items: flex-start;
  flex: 1 1 auto;
}
.lightbox.actual .lightbox-stage img { max-width: none; max-height: none; cursor: zoom-out; margin: auto; }

.lightbox-cap {
  max-width: min(52rem, 100%);
  max-height: 22vh;
  overflow-y: auto;
  margin: 0;
  font-size: 14px; line-height: 1.6;
  color: rgba(235, 238, 242, .86);
  text-align: left;
}
.lightbox-cap b { color: #fff; }
/* Phones: wide figures fit the width and are small; say they can be pinched. */
.lightbox-hint {
  display: none;
  margin: 0;
  font-size: 12px; letter-spacing: .02em;
  color: rgba(235, 238, 242, .55);
}
@media (hover: none) and (pointer: coarse) { .lightbox-hint { display: block; } }

.lightbox-close {
  position: absolute;
  top: max(.6rem, env(safe-area-inset-top)); right: max(.6rem, env(safe-area-inset-right));
  width: 44px; height: 44px;
  display: grid; place-items: center;
  padding: 0;
  border: 0; border-radius: 999px;
  background: rgba(255, 255, 255, .12);
  color: #fff;
  cursor: pointer;
}
.lightbox-close:hover { background: rgba(255, 255, 255, .22); }
.lightbox-close svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; }

@media (max-width: 46rem) {
  .lightbox { gap: .6rem; padding-top: max(3.6rem, env(safe-area-inset-top)); }
  .lightbox-stage img { max-width: 100%; max-height: calc(100dvh - 11rem); }
  .lightbox-cap { font-size: 13px; max-height: 26vh; }
}
@media (prefers-reduced-motion: reduce) {
  .lightbox, .lightbox-stage img { transition: none; }
}

/* ---------- §4 qualitative viewer (qual.js) ----------
   Scene cards, then cameras, then the satellite tile and one panel per
   method, all on the same frame. The panels are canvases so the range, the
   camera condition and the zoom change without loading anything. */
.qv { --qv-ground: var(--surface); --qv-gt: #c3c8cf; outline: none; }
:root[data-theme="dark"] .qv { --qv-gt: #4b525c; }

/* scene cards: the front camera mid-scene, the scene and city under it, in
   one row that scrolls sideways, with an end at each side */
.qv-scenebar { display: flex; align-items: flex-start; gap: .5rem; margin-bottom: 1.1rem; }
.qv-step {
  flex: none; width: 2rem; height: calc(7.6rem * 9 / 16); margin-top: 0;
  font-size: 20px; line-height: 1; padding: 0;
}
.qv-step:hover:not(:disabled) { color: var(--ink); border-color: var(--ink); }
.qv-step:disabled { opacity: .3; cursor: default; }
.qv-scenes {
  flex: 1; min-width: 0; display: flex; gap: .6rem; overflow-x: auto;
  scroll-snap-type: x proximity; scrollbar-width: none; padding: 3px 3px 2px;
}
.qv-scenes::-webkit-scrollbar { display: none; }
.qv-scenes button {
  position: relative; display: flex; flex-direction: column; gap: .05rem; flex: none; width: 7.6rem;
  scroll-snap-align: start;
  font: inherit; text-align: left; color: var(--ink-soft);
  background: none; border: 0; padding: 0; cursor: pointer;
}
.qv-scenes img {
  display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover;
  border-radius: 4px; background: var(--surface); margin-bottom: .35rem;
  filter: saturate(.7) brightness(.92); opacity: .78;
  outline: 2px solid transparent; outline-offset: 2px;
  transition: opacity .15s ease, filter .15s ease, outline-color .15s ease;
}
.qv-scenes em {
  position: absolute; top: .3rem; left: .3rem; font-style: normal;
  font-size: 10.5px; font-weight: 600; letter-spacing: .02em; color: #fff;
  background: rgba(10, 12, 15, .62); border-radius: 2px; padding: .1rem .35rem;
}
.qv-card-name { font-size: 12.5px; font-weight: 600; color: var(--ink-soft); line-height: 1.2; }
.qv-scenes small { font-size: 11px; color: var(--ink-faint); line-height: 1.25; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.qv-scenes button:hover img { opacity: 1; filter: none; }
.qv-scenes button:hover .qv-card-name { color: var(--ink); }
.qv-scenes button[aria-selected="true"] img { opacity: 1; filter: none; outline-color: var(--accent); }
.qv-scenes button[aria-selected="true"] .qv-card-name { color: var(--accent); }
.qv-scenes button:focus-visible { outline: none; }
.qv-scenes button:focus-visible img { outline-color: var(--ink); }

.qv-step, .qv-seg button {
  font: inherit; font-size: 13px; font-weight: 500; color: var(--ink-soft);
  background: none; border: 1px solid var(--rule-dark); border-radius: 3px; cursor: pointer;
  transition: color .15s ease, border-color .15s ease, background .15s ease;
}
.qv-seg button:hover:not(:disabled):not([aria-pressed="true"]) { color: var(--ink); border-color: var(--ink); }

.qv-controls { display: flex; flex-wrap: wrap; gap: .6rem 1rem; margin-bottom: .9rem; }
.qv-seg { display: inline-flex; }
.qv-seg button { padding: .45rem .8rem; border-radius: 0; margin-left: -1px; }
.qv-seg button:first-child { border-radius: 3px 0 0 3px; margin-left: 0; }
.qv-seg button:last-child { border-radius: 0 3px 3px 0; }
.qv-seg button[aria-pressed="true"] { color: var(--paper); background: var(--ink); border-color: var(--ink); position: relative; }
.qv-cond button[aria-pressed="true"]:not([data-cond="clean"]) { background: var(--drop); border-color: var(--drop); color: var(--paper); }
.qv-seg button:disabled { opacity: .35; cursor: not-allowed; }

.qv-cams {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 4px;
  max-width: 42rem; margin: 0 auto 1.1rem;
}
.qv-cam {
  position: relative; aspect-ratio: 16 / 9; background-color: var(--surface);
  background-size: 300% 200%; border-radius: 2px; overflow: hidden;
}
.qv-cam:nth-child(1) { background-position: 0 0; }
.qv-cam:nth-child(2) { background-position: 50% 0; }
.qv-cam:nth-child(3) { background-position: 100% 0; }
.qv-cam:nth-child(4) { background-position: 0 100%; }
.qv-cam:nth-child(5) { background-position: 50% 100%; }
.qv-cam:nth-child(6) { background-position: 100% 100%; }
.qv-cam span {
  position: absolute; left: 4px; bottom: 4px; z-index: 1;
  font-size: 10.5px; font-weight: 600; line-height: 1; color: #fff;
  background: rgba(10, 12, 15, .64); border-radius: 2px; padding: .2rem .35rem;
}
.qv-cam::after {
  content: "no signal"; position: absolute; inset: 0; display: grid; place-items: center;
  background: #0d0f12; color: #5d636c; font-size: 11px; opacity: 0; transition: opacity .35s ease;
}
.qv-cam.off::after { opacity: 1; }
.qv-cam.off span { background: #b23a30; }

.qv-panels { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 1.6rem .9rem; touch-action: pan-y; }
.qv.zoomed .qv-panels { touch-action: none; }
.qv.zoomed .qv-box { cursor: grab; }
.qv.zoomed .qv-box:active { cursor: grabbing; }
.qv-box { position: relative; aspect-ratio: 1 / 2; background: var(--qv-ground); overflow: hidden; border-radius: 2px; }
.qv-box canvas, .qv-box img { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
.qv-box img { object-fit: cover; transform-origin: 50% 50%; user-select: none; -webkit-user-drag: none; }
.qv-note {
  position: absolute; left: 0; right: 0; top: 38%; text-align: center; padding: 0 .4rem;
  font-size: 11.5px; color: var(--ink-faint); pointer-events: none;
}
.qv-name { margin-top: .5rem; font-size: 14px; font-weight: 600; color: var(--ink); line-height: 1.25; }
.qv-mod { font-size: 12px; color: var(--ink-soft); }
/* ours: an accent ring round the panel and the name in the accent */
.qv-panel.ours .qv-box { box-shadow: 0 0 0 2px var(--paper), 0 0 0 4px var(--accent); }
.qv-panel.ours .qv-name { color: var(--accent); font-weight: 700; }
.qv-panel.missing canvas { opacity: .6; }
.qv.loading .qv-panels, .qv.loading .qv-cams { opacity: .5; transition: opacity .2s; }

/* transport: the teaser's seek bar and button row (.seek, .speed) */
.qv-transport { margin-top: 1.2rem; }
.qv-seek .seek-tip { flex-direction: row; }
.qv-speed { margin-top: .3rem; align-items: center; }
.qv-sep { width: 1px; align-self: stretch; background: var(--rule); margin: .15rem .25rem; }
.qv-speed .icon svg circle { fill: none; }
.qv-speed .icon:disabled { opacity: .35; cursor: default; }
.qv-speed .icon:disabled:hover { color: var(--ink-soft); border-color: var(--rule-dark); }
.qv-play .i-replay, .qv-play[data-state="ended"] .i-play, .qv-play[data-state="playing"] .i-play,
.qv-play[data-state="paused"] .i-pause, .qv-play[data-state="ended"] .i-pause { display: none; }
.qv-play[data-state="ended"] .i-replay { display: inline; }
.qv-max .i-close, .qv.max .qv-max .i-open { display: none; }
.qv.max .qv-max .i-close { display: inline; }

.qv-foot { display: flex; flex-wrap: wrap; justify-content: space-between; gap: .5rem 1rem; margin-top: .9rem; font-size: 12.5px; color: var(--ink-soft); }
.qv-legend i, .qv-gt i { display: inline-block; width: 20px; height: 3px; border-radius: 2px; vertical-align: middle; margin: 0 .4rem 0 .9rem; }
.qv-legend i:first-child { margin-left: 0; }
.qv-legend .c0 { background: var(--map-crossing); }
.qv-legend .c1 { background: var(--map-divider); }
.qv-legend .c2 { background: var(--map-boundary); }
.qv-gt { cursor: pointer; user-select: none; }
.qv-gt i { background: var(--qv-gt); margin-left: .3rem; }
.qv-caption { font-size: 12.5px; color: var(--ink-faint); margin: .7rem 0 0; }
.qv-caption b { color: var(--ink-soft); font-weight: 600; }

/* full window: the viewer alone, panels as tall as the screen allows */
html.qv-max-open { overflow: hidden; }
.qv.max {
  position: fixed; inset: 0; z-index: 1000; overflow: auto;
  background: var(--paper);
  padding: max(1rem, env(safe-area-inset-top)) max(1.25rem, env(safe-area-inset-right))
           max(1rem, env(safe-area-inset-bottom)) max(1.25rem, env(safe-area-inset-left));
  --pw: calc((100dvh - 19rem) / 2);
}
.qv.max > * { max-width: calc(6 * var(--pw) + 5 * .9rem); margin-inline: auto; }
.qv.max .qv-scenebar { display: none; }
.qv.max .qv-caption { display: none; }
.qv.max .qv-controls { margin-bottom: .6rem; }
.qv.max .qv-cams { margin-bottom: .7rem; max-width: none; grid-template-columns: repeat(6, minmax(0, 1fr)); }
.qv.max .qv-transport { margin-top: .8rem; }

@media (max-width: 720px) {
  .qv-step { display: none; }
  .qv-scenes { margin-inline: calc(var(--gutter) / -2); padding-inline: calc(var(--gutter) / 2); }
  .qv-cams { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .qv-panels { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1.2rem .6rem; }
  .qv-seg button { padding: .45rem .55rem; font-size: 12.5px; }
  .qv-speed [data-rate="0.5"], .qv-speed [data-rate="2"] { display: none; }
  .qv-name { font-size: 13px; }
  .qv.max > * { max-width: none; }
  .qv.max .qv-panels { gap: .9rem .5rem; }
  .qv.max .qv-foot { display: none; }
}
/* a phone on its side: one row of six, as on a desktop */
@media (max-height: 34rem) and (orientation: landscape) {
  .qv.max { --pw: calc((100dvh - 13rem) / 2); }
  .qv.max > * { max-width: calc(6 * var(--pw) + 5 * .5rem); }
  .qv.max .qv-cams, .qv.max .qv-panels { grid-template-columns: repeat(6, minmax(0, 1fr)); }
  .qv.max .qv-panels { gap: .5rem; }
  .qv.max .qv-cams { margin-bottom: .4rem; }
  .qv.max .qv-cam span { font-size: 9px; padding: .12rem .25rem; }
  .qv.max .qv-name { font-size: 11px; margin-top: .25rem; }
  .qv.max .qv-mod, .qv.max .qv-foot { display: none; }
  .qv.max .qv-controls { margin-bottom: .4rem; }
  .qv.max .qv-seg button { padding: .3rem .5rem; font-size: 12px; }
  .qv.max .qv-transport { margin-top: .4rem; }
}

/* ---------- §5 paper card and reader (paper.js) ---------- */
.paper-card { display: flex; gap: 1.6rem; align-items: center; margin: 0 0 1.8rem; }
.paper-cover {
  position: relative; flex: none; width: 9.5rem; aspect-ratio: 8.5 / 11; padding: 0;
  border: 1px solid var(--rule); border-radius: 2px; background: #fff; cursor: pointer;
  box-shadow: 0 1px 2px rgba(0, 0, 0, .06), 0 6px 18px rgba(0, 0, 0, .07);
  transition: transform .18s ease, box-shadow .18s ease; overflow: hidden;
}
.paper-cover:disabled { cursor: default; }
.paper-card.ready .paper-cover:hover { transform: translateY(-2px); box-shadow: 0 2px 4px rgba(0, 0, 0, .08), 0 12px 26px rgba(0, 0, 0, .12); }
.paper-cover canvas { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; transition: opacity .3s ease; }
.paper-card.covered .paper-cover canvas { opacity: 1; }
/* a blank page, lines for text, until the real first page is drawn */
.paper-cover-blank { position: absolute; inset: 14% 12%; display: flex; flex-direction: column; gap: 9%; }
.paper-cover-blank i { display: block; height: 4%; background: #e3e6ea; border-radius: 1px; }
.paper-cover-blank i:first-child { width: 80%; margin: 0 auto 6%; height: 6%; background: #cfd3d9; }
.paper-cover-blank i:nth-child(3) { width: 92%; }
.paper-cover-blank i:last-child { width: 60%; }
.paper-card.covered .paper-cover-blank { display: none; }
.paper-title { font-family: var(--display); font-size: 1.12rem; font-weight: 700; line-height: 1.3; margin: 0 0 .25rem; color: var(--ink); }
.paper-venue { margin: 0 0 .6rem; font-size: 14px; color: var(--ink-soft); }
.paper-wait { margin: 0 0 .8rem; font-size: 13.5px; color: var(--ink-faint); }
.paper-card.ready .paper-wait { display: none; }
.paper-actions { display: flex; flex-wrap: wrap; gap: .5rem; margin: 0; }
.paper-actions button.button { font: inherit; font-size: 14px; font-weight: 500; background: none; cursor: pointer; }
.paper-actions button.button:disabled { cursor: default; }
@media (max-width: 560px) {
  .paper-card { gap: 1rem; align-items: flex-start; }
  .paper-cover { width: 6.5rem; }
  .paper-title { font-size: 1rem; }
}

html.pdf-open { overflow: hidden; }
.pdfbox {
  position: fixed; inset: 0; z-index: 210; display: flex; flex-direction: column;
  background: #2a2d33; opacity: 0; transition: opacity .18s ease; overscroll-behavior: contain;
}
.pdfbox[hidden] { display: none; }
.pdfbox.shown { opacity: 1; }
.pdf-bar {
  flex: none; display: flex; align-items: center; gap: 1rem;
  padding: max(.55rem, env(safe-area-inset-top)) max(1rem, env(safe-area-inset-right)) .55rem max(1rem, env(safe-area-inset-left));
  background: #15171b; color: rgba(235, 238, 242, .9); font-size: 13px;
}
.pdf-name { font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pdf-pos { margin: 0 auto; font-variant-numeric: tabular-nums; color: rgba(235, 238, 242, .7); white-space: nowrap; }
.pdf-tools { display: flex; align-items: center; gap: .3rem; }
.pdf-tools button, .pdf-tools a {
  display: grid; place-items: center; min-width: 2rem; height: 2rem; padding: 0 .45rem;
  font: inherit; font-size: 15px; color: #fff; background: none;
  border: 1px solid rgba(255, 255, 255, .22); border-radius: 3px; cursor: pointer; text-decoration: none;
}
.pdf-tools .pdf-zoom-label { font-size: 12px; font-variant-numeric: tabular-nums; min-width: 3.2rem; }
.pdf-tools button:hover, .pdf-tools a:hover { border-color: #fff; }
.pdf-tools svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.pdf-tools .pdf-close { font-size: 20px; margin-left: .3rem; }
.pdf-pages {
  flex: 1; overflow: auto; display: flex; flex-direction: column; align-items: center; gap: 14px;
  padding: 16px 16px max(16px, env(safe-area-inset-bottom)); outline: none; -webkit-overflow-scrolling: touch;
}
.pdf-page { flex: none; max-width: none; background: #fff; box-shadow: 0 2px 10px rgba(0, 0, 0, .35); }
.pdf-page canvas { display: block; width: 100%; height: 100%; }
.pdf-fail { color: #fff; margin: auto; }
.pdf-fail a { color: #9cc8f5; }
@media (max-width: 560px) {
  .pdf-name { display: none; }
  .pdf-pos { margin-left: 0; }
  .pdf-pages { padding: 8px 4px; gap: 8px; }
}
