/* Defines the application's visual styling. */

/* Registered so the width is a typed length rather than an opaque token: an
   unregistered custom property changes discretely, which made the panel jump
   on the way out even though the grid transition was declared. */
@property --panel-width {
  syntax: "<length>";
  inherits: true;
  initial-value: 364px;
}

:root {
  --bg: #05080d;
  --bg-lift: #070c12;
  --panel: #090e15;
  --line: rgba(150, 180, 210, 0.11);
  --line-strong: rgba(150, 180, 210, 0.2);
  --text: #e7eef5;
  --text-dim: #90a4b8;
  --text-faint: #5f7286;

  /* Significance scale. Colour carries severity only. */
  --sev-low: #7f95ad;
  --sev-moderate: #d9a63c;
  --sev-elevated: #e0812f;
  --sev-high: #d8503c;
  --sev-severe: #b3243c;
  --sev-unknown: #6b7f94;

  /* Active fault motion in the explanatory figures. A role colour, deliberately
     NOT a point on the severity scale above: in a faulting diagram the accent
     means "this is the part that moved", which has to read the same whether the
     earthquake was M4 or M8. Driving it from --sev tied it to significance, so
     on every ordinary event the motion layer resolved to --sev-low — the same
     muted blue-gray as the block geometry — and the diagram lost its hierarchy.
     Structure stays muted (.fx__block, .fx__strata, .fx__face, .fx__seam,
     .fx__brow); only the motion layer takes this. */
  --fault-motion: #e2703a;

  /* Section-title colour. One notch below --text: these are structural anchors,
     not the loudest thing on screen. --text-faint remains the subordinate level
     for filter labels, so the three tiers stay distinguishable. */
  --text-title: #c3d3e2;

  /* Vertical rhythm, by RELATIONSHIP rather than by component. Three distinct
     semantic gaps, ordered — a label sits closest to the control it names, a
     section header a little further from its content, and the gap between
     sections is clearly the largest. Sections must not invent their own margins;
     if a new gap is needed, it belongs here with a name.
     Desktop overrides the values, never the model. */
  --space-label-control: 6px;
  --space-title-content: 10px;
  --space-section-gap: 22px;
  /* An editorial heading sits closer to the prose it introduces than a UI
     section title does to its controls, and lesson sections stand further
     apart than filter groups do. */
  --space-heading-body: 11px;
  --space-lesson-gap: 30px;

  /* Control system. Every toggle, and the search field, derive their box from
     these — one place to change the height, the border or the corner of the
     whole filter family. Widths are deliberately NOT here: content differs. */
  --control-border: 1px solid var(--line-strong);
  --control-radius: 2px;
  --control-pad-y: 6px;
  --control-pad-x: 11px;
  --control-selected-bg: rgba(150, 180, 210, 0.12);
  --control-selected-border: rgba(150, 180, 210, 0.42);

  --panel-width: 364px;
  /* Browsing wants a compact list; reading one event wants a longer measure.
     The detail width tracks the viewport so it does not dominate a small
     laptop screen or stop growing usefully on a large one. */
  --panel-width-detail: clamp(440px, 31vw, 520px);

  /* The sheet's own height is the tallest snap; how much of it is showing is
     `--sheet-shown`, written in pixels by app.js and declared nowhere else.
     Snap heights used to live here as dvh values and be read back with
     parseFloat, which returned 55 for "55dvh" and drove every drag from an
     origin 468px wrong. There is now exactly one declaration of the number.
     `--sheet-peek` is written by JS too; the dvh value is the pre-boot fallback
     for the two decorative surfaces that sit above the shortest sheet. */
  --sheet-height: 90dvh;
  --sheet-peek: 17dvh;
}

* {
  box-sizing: border-box;
}

html,
body {
  height: 100%;
  margin: 0;
}

body {
  background: var(--bg);
  color: var(--text);
  font-family: "IBM Plex Sans", system-ui, sans-serif;
  font-size: 14px;
  line-height: 1.45;
  -webkit-font-smoothing: antialiased;
  overflow: hidden;
}

a {
  color: #9dc4e4;
  text-decoration: none;
}
a:hover {
  color: #c7dff2;
}

.app {
  position: relative;
  height: 100dvh;
  display: grid;
  grid-template-columns: var(--panel-width) 1fr;
  transition: --panel-width 200ms cubic-bezier(0.22, 1, 0.36, 1);
}
/* The globe takes whatever is left; its canvas is observed, so the projection
   and composition offset re-derive from the new width on their own — no
   camera move, no refocus, no change to zoom or selection. */
body.is-detail .app {
  /* Literal rather than `var(--panel-width-detail)`: a registered property
     validates its computed value, and substituting another custom property
     here failed that check and silently fell back to the initial 364px. */
  --panel-width: clamp(440px, 31vw, 520px);
}

/* ---------------------------------------------------------------- stage */

.stage {
  grid-column: 2;
  position: relative;
  overflow: hidden;
  /* The overlay layers above the canvas each carry their own z-index. Without a
     stacking context here they join the root stack and paint above the panel,
     which is z-index:auto. Side by side on desktop that is invisible; on mobile
     the sheet sits over the stage and place names show through it. Isolating
     keeps that stack inside the globe. */
  isolation: isolate;

  /* ---------------------------------------------------------------------
     Stage stacking order. One scale, declared once, and every positioned
     child inside .stage takes its level from it.

       0  the globe canvas — and everything drawn into WebGL with it: the
          basemap patch, coastlines, faults, plate boundaries, the ShakeMap
          field
       1  geographic labels — map content, above the shaking field so a place
          name is never contested by the data drawn under it
       2  observation symbols — stations and felt reports for the selected
          event
       3  earthquake markers — the subject of the app, never covered by the
          geography that describes them
       4  persistent globe UI and required attribution — legend, attribution,
          the ShakeMap key, and on mobile the identity block and Legend
          control. Always above map content: a label must never sit on the
          credit line it depends on, or on a control the user is reaching for
       5  transient UI — the observation card
       6  development only

     This used to be four scattered values and four defaults. A positive
     z-index beats `z-index: auto` whatever the DOM order, so the labels and
     station symbols at 2 painted straight over the legend, the attribution and
     the mobile Legend control, all of which were sitting at auto.
     --------------------------------------------------------------------- */
  background:
    radial-gradient(
      120% 90% at 62% 44%,
      rgba(28, 52, 78, 0.5) 0%,
      rgba(8, 14, 22, 0.9) 55%,
      var(--bg) 100%
    ),
    var(--bg);
}

.stage__canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  z-index: 0;
}

.markers {
  position: absolute;
  inset: 0;
  pointer-events: none;
  /* Above the geographic labels. Label placement already reserves the space a
     marker occupies, so the two rarely meet — but markers move every frame
     while labels are only re-chosen when the view settles, so when they do
     meet the earthquake is what has to win. */
  z-index: 3;
  --mscale: 1;
}

/* ------------------------------------------------- geographic labels */

/* Labels are anchored to real coordinates and projected every frame, so they
   sit under the marker layer in the stack and never compete with it for
   attention: neutral, small, and no heavier than the linework beneath. */
.labels {
  position: absolute;
  inset: 0;
  pointer-events: none;
  /* Above the canvas, so geographic names are never contested by anything
     drawn into the globe — including the shaking field — and below the markers
     and the interface. */
  z-index: 1;
}

.geolabel {
  position: absolute;
  left: 0;
  top: 0;
  display: flex;
  align-items: center;
  gap: 6px;
  white-space: nowrap;
  opacity: calc(0.82 * var(--presence, 1));
  will-change: transform;
}

.geolabel i {
  width: 3px;
  height: 3px;
  border-radius: 50%;
  background: rgba(196, 216, 234, 0.7);
}

/* Labels carry a semantic type. Geological and physical context reads warmer
   and quieter than settlements, so the two never look like the same kind of
   fact — and none of them competes with an earthquake marker. */
.geolabel span {
  font-size: 10px;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: rgba(203, 222, 239, 0.82);
  /* A tight dark halo as well as the soft drop, so a name stays readable where
     it crosses a lit contour rather than dissolving into it. */
  text-shadow:
    0 0 3px rgba(4, 8, 12, 0.95),
    0 1px 4px rgba(4, 8, 12, 0.9);
}

.geolabel[data-kind="tectonic"] i,
.geolabel[data-kind="plate"] i {
  background: rgba(208, 160, 106, 0.75);
}
.geolabel[data-kind="tectonic"] span,
.geolabel[data-kind="plate"] span {
  color: rgba(220, 190, 152, 0.82);
  letter-spacing: 0.13em;
}
.geolabel[data-kind="plate"] span {
  font-size: 9px;
  color: rgba(206, 180, 148, 0.6);
}

.geolabel[data-kind="range"] i,
.geolabel[data-kind="plateau"] i,
.geolabel[data-kind="basin"] i,
.geolabel[data-kind="valley"] i {
  background: rgba(178, 190, 178, 0.6);
}
.geolabel[data-kind="range"] span,
.geolabel[data-kind="plateau"] span,
.geolabel[data-kind="basin"] span,
.geolabel[data-kind="valley"] span {
  color: rgba(196, 208, 196, 0.72);
  font-style: italic;
  letter-spacing: 0.07em;
  text-transform: none;
  font-size: 11px;
}

/* Settlement hierarchy. Three levels, driven by the same `weight` that decides
   ordering and collision survival, so a name is never drawn as important while
   being treated as expendable. The differences are deliberately small — size,
   luminance and one step of font weight, nothing decorative — because these
   labels sit under the earthquake markers in the stacking order and have to
   stay under them in attention too. A primary city should be findable at a
   glance; it should never look like an event. */
.geolabel[data-tier="primary"] i {
  width: 4px;
  height: 4px;
  background: rgba(214, 232, 248, 0.85);
}
.geolabel[data-tier="primary"] span {
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.1em;
  color: rgba(226, 238, 250, 0.95);
}

.geolabel[data-tier="secondary"] span {
  font-size: 10.5px;
  color: rgba(206, 224, 240, 0.84);
}

.geolabel[data-tier="local"] i {
  width: 2.5px;
  height: 2.5px;
  background: rgba(186, 206, 226, 0.55);
}
.geolabel[data-tier="local"] span {
  font-size: 9px;
  color: rgba(184, 204, 223, 0.66);
}

/* Required source credit for the tile, geography and geology layers. It owns
   the bottom band of the stage outright — every other bottom-anchored overlay
   clears it — because these credits are license obligations, not decoration,
   and a panel painted over them hides them just as effectively as removing
   them would. */
/* Mobile-only globe overlays. Desktop keeps identity in the panel header and
   the legend permanently visible, so both are absent above the sheet
   breakpoint. */
.mapid,
.legendToggle {
  display: none;
}

/* Desktop shows all three filter groups, so the mobile disclosure is absent and
   its wrappers collapse out of the box tree entirely — `.controls` keeps its
   original grid with the same three children. */
/* Done belongs to the mobile sheet, where the disclosure takes over the panel
   and has to hand it back. On desktop the expander is just an expander. */
.filters__done {
  display: none;
}

.secondary,
.secondary__inner {
  display: contents;
}

.attribution {
  position: absolute;
  left: 22px;
  right: 22px;
  bottom: 12px;
  /* Required attribution. It is never allowed to sit under map content. */
  z-index: 4;
  margin: 0;
  font-size: 9px;
  line-height: 1.5;
  letter-spacing: 0.05em;
  color: rgba(168, 190, 210, 0.42);
  opacity: 0;
  transition: opacity 220ms ease;
  pointer-events: none;
}
.attribution.is-visible {
  opacity: 1;
}

/* ------------------------------------------------------------- markers */

/* Markers are presentation only. All pointer interaction is handled by the
   globe's single gesture system on the canvas beneath them. */
.marker {
  position: absolute;
  left: 0;
  top: 0;
  width: calc(var(--d) * var(--mscale));
  height: calc(var(--d) * var(--mscale));
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  border-radius: 50%;
  pointer-events: none;
  opacity: var(--presence, 1);
}

.marker__ring {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  border: 1px solid var(--sev);
  opacity: 0.62;
}

.marker__core {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 3px;
  height: 3px;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  background: var(--sev);
  box-shadow: 0 0 6px 1px color-mix(in oklab, var(--sev) 55%, transparent);
}

.marker__fill {
  position: absolute;
  inset: 1px;
  border-radius: 50%;
  background: color-mix(in oklab, var(--sev) 20%, transparent);
}

.marker.is-hovered .marker__ring {
  opacity: 1;
}
.marker.is-hovered .marker__fill {
  background: color-mix(in oklab, var(--sev) 34%, transparent);
}
.marker.is-hovered .marker__hover {
  opacity: 1;
}

/* Hover is a preview: one thin outer ring, always weaker than selection. */
.marker__hover {
  position: absolute;
  inset: -5px;
  border-radius: 50%;
  border: 1px solid color-mix(in oklab, var(--sev) 55%, transparent);
  opacity: 0;
}
.marker.is-selected .marker__hover {
  opacity: 0;
}

/* Selected: shape and stroke, not colour alone. */
.marker.is-selected {
  opacity: 1;
  z-index: 4;
}
.marker.is-selected .marker__ring {
  opacity: 1;
  border-width: 1.5px;
}
.marker__select {
  position: absolute;
  inset: -9px;
  border-radius: 50%;
  border: 1px solid rgba(232, 242, 250, 0.75);
  opacity: 0;
}
.marker__select::before,
.marker__select::after {
  content: "";
  position: absolute;
  background: rgba(232, 242, 250, 0.8);
}
.marker__select::before {
  left: 50%;
  top: -7px;
  width: 1px;
  height: 6px;
  transform: translateX(-50%);
  box-shadow: 0 calc(100% + 8px + 2px) 0 rgba(232, 242, 250, 0.8);
}
.marker__select::after {
  top: 50%;
  left: -7px;
  height: 1px;
  width: 6px;
  transform: translateY(-50%);
  box-shadow: calc(100% + 8px + 2px) 0 0 rgba(232, 242, 250, 0.8);
}
.marker.is-selected .marker__select {
  opacity: 1;
}

.marker__label {
  position: absolute;
  left: calc(100% + 14px);
  top: 50%;
  transform: translateY(-50%);
  display: none;
  white-space: nowrap;
  font-size: 11px;
  letter-spacing: 0.04em;
  color: rgba(231, 238, 245, 0.92);
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.85);
}
.marker__label b {
  font-family: "IBM Plex Mono", monospace;
  font-weight: 500;
  margin-right: 7px;
  color: #fff;
}
.marker__depth {
  display: block;
  margin-top: 2px;
  font-family: "IBM Plex Mono", monospace;
  font-style: normal;
  font-size: 10px;
  letter-spacing: 0.06em;
  color: rgba(203, 222, 239, 0.68);
}
.marker.is-selected .marker__label {
  display: block;
}
/* Flipped to the marker's left when the label would otherwise run off the
   right edge. Right-aligned so it grows away from the screen edge. */
.marker.is-labelLeft .marker__label {
  left: auto;
  right: calc(100% + 14px);
  text-align: right;
}
/* Bounded so neither side can run off a 390px screen. It wraps rather than
   clipping: two short lines beat half a place name. */
@media (max-width: 880px) {
  .marker__label {
    max-width: 44vw;
    white-space: normal;
    line-height: 1.35;
  }
}

/* Arrival: 3 thin rings, once per cycle, then quiet. */
.marker__pulse {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  border: 1px solid var(--sev);
  opacity: 0;
  pointer-events: none;
}
.marker.is-arriving .marker__pulse {
  animation: arrival 2600ms cubic-bezier(0.16, 0.62, 0.24, 1) forwards;
}
.marker.is-arriving .marker__pulse:nth-child(2) {
  animation-delay: 500ms;
}
.marker.is-arriving .marker__pulse:nth-child(3) {
  animation-delay: 1000ms;
}

@keyframes arrival {
  0% {
    transform: scale(1);
    opacity: 0.6;
  }
  100% {
    transform: scale(4.6);
    opacity: 0;
  }
}

/* Very recent events keep a restrained trace of activity. */
.marker.is-fresh .marker__ring {
  opacity: 1;
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--sev) 12%, transparent);
}

/* -------------------------------------------------------------- legend */

/* ShakeMap legend. Contextual: it exists only while a shaking layer does, and
   lists only the bands this product actually publishes. */
.shakeLegend {
  position: absolute;
  left: 26px;
  /* Clears the attribution band (12px inset + up to two 13.5px lines). */
  bottom: 56px;
  display: flex;
  flex-direction: column;
  gap: 5px;
  padding: 10px 12px;
  border: 1px solid rgba(150, 180, 210, 0.14);
  background: rgba(6, 10, 16, 0.72);
  backdrop-filter: blur(3px);
  pointer-events: none;
  /* Globe UI: a place name must not land on the key that reads the field. */
  z-index: 4;
}
.shakeLegend b {
  font-size: 9px;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--text-faint);
}
.shakeLegend span {
  display: flex;
  align-items: center;
  gap: 7px;
  font-family: "IBM Plex Mono", monospace;
  font-size: 10px;
  color: rgba(203, 222, 239, 0.86);
}
.shakeLegend i {
  width: 10px;
  height: 2px;
  background: var(--mmi);
}
/* Debug actions occupy the same corner. */
body.is-debug .shakeLegend {
  bottom: 132px;
}

/* The class sets `display: flex`, which beats the UA `[hidden]` rule — the
   same trap `.detail` fell into. Without this the legend is an empty bordered
   box over the Earth whenever no shaking layer is active. */
.shakeLegend[hidden] {
  display: none;
}

/* Observation markers. Instruments are triangles, felt reports circles — the
   conventional seismological distinction, so the two never read as the same
   kind of fact. Neither is severity-coloured: an observation is not an
   earthquake, and the epicentre stays dominant. */
.stations {
  position: absolute;
  inset: 0;
  pointer-events: none;
  /* Above the geographic labels, below the earthquake they belong to. */
  z-index: 2;
}
.obs {
  position: absolute;
  top: 0;
  left: 0;
  width: 0;
  height: 0;
  opacity: var(--presence, 1);
}
.obs i {
  position: absolute;
  display: block;
  transform: translate(-50%, -50%);
}
.obs[data-kind="station"] i {
  width: 0;
  height: 0;
  border-left: 4.5px solid transparent;
  border-right: 4.5px solid transparent;
  border-bottom: 8px solid rgba(214, 232, 246, 0.82);
  filter: drop-shadow(0 0 2px rgba(4, 8, 12, 0.9));
  transform: translate(-50%, -60%);
}
.obs[data-kind="felt"] i {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: rgba(150, 186, 214, 0.5);
  border: 1px solid rgba(198, 222, 240, 0.7);
}
.obs.is-selected[data-kind="station"] i {
  border-bottom-color: #ffffff;
  border-left-width: 6px;
  border-right-width: 6px;
  border-bottom-width: 11px;
}
.obs.is-selected[data-kind="felt"] i {
  width: 9px;
  height: 9px;
  background: rgba(226, 240, 250, 0.85);
}

/* Compact, anchored, and deliberately not a second detail panel. */
.obscard {
  position: absolute;
  top: 0;
  left: 0;
  width: 214px;
  padding: 11px 12px 10px;
  border: 1px solid rgba(150, 180, 210, 0.2);
  background: rgba(7, 11, 17, 0.93);
  backdrop-filter: blur(4px);
  z-index: 5;
}
.obscard[hidden] {
  display: none;
}
.obscard__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}
.obscard__kind {
  font-size: 8.5px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--text-faint);
}
.obscard__close {
  appearance: none;
  border: 0;
  background: none;
  padding: 0 2px;
  font-size: 15px;
  line-height: 1;
  color: var(--text-faint);
  cursor: pointer;
}
.obscard__close:hover {
  color: var(--text);
}
.obscard__title {
  margin: 5px 0 0;
  font-family: "IBM Plex Mono", monospace;
  font-size: 14px;
  font-weight: 500;
  color: var(--text);
}
.obscard__place {
  margin: 2px 0 0;
  font-size: 11px;
  color: var(--text-faint);
}
.obscard__rows {
  margin: 9px 0 0;
  display: flex;
  flex-direction: column;
  gap: 7px;
}
.obscard__rows div {
  display: flex;
  flex-direction: column;
  gap: 1px;
}
.obscard__rows dt {
  font-size: 9px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-faint);
}
.obscard__rows dd {
  margin: 0;
  font-family: "IBM Plex Mono", monospace;
  font-size: 12px;
  color: var(--text);
}
.obscard__rows dd em {
  display: block;
  font-style: normal;
  font-size: 10px;
  color: var(--text-faint);
}
.obscard__source {
  margin: 10px 0 0;
  font-size: 9px;
  color: var(--text-faint);
}

/* Two switches, not a layer drawer. */
.obstoggles {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 10px 0 8px;
}
.obstoggles button {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 5px 9px;
  border: 1px solid rgba(150, 180, 210, 0.2);
  background: none;
  font: inherit;
  font-size: 11px;
  color: var(--text-faint);
  cursor: pointer;
}
.obstoggles button:hover {
  border-color: rgba(150, 180, 210, 0.4);
  color: var(--text);
}
.obstoggles button[aria-pressed="true"] {
  border-color: rgba(190, 216, 238, 0.5);
  color: var(--text);
}
.obstoggles button i {
  width: 7px;
  height: 7px;
  border: 1px solid currentColor;
  border-radius: 50%;
}
.obstoggles button[aria-pressed="true"] i {
  background: currentColor;
}
.obstoggles button b {
  font-weight: 500;
  font-family: "IBM Plex Mono", monospace;
  color: var(--text-faint);
}

.shakeLegend__sub {
  margin-top: 3px;
}
.shakeLegend__obs i {
  background: none;
}
.shakeLegend__obs i[data-shape="tri"] {
  width: 0;
  height: 0;
  border-left: 4px solid transparent;
  border-right: 4px solid transparent;
  border-bottom: 7px solid rgba(214, 232, 246, 0.82);
}
.shakeLegend__obs i[data-shape="dot"] {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  border: 1px solid rgba(198, 222, 240, 0.7);
}

/* PAGER impact. Alert colour lives here and nowhere else — the map already
   uses colour semantically, and an orange PAGER must not repaint the product.
   Every value is text as well as bar. */
/* Presentation cues for a resolved ShakeMap. One pulse, one settle, no loop —
   these say "the data arrived", not "waves are propagating".

   Targeted at the marker root, which is the sized circle. `marker__pulse` is
   deliberately left alone: it belongs to `is-arriving`, and the two cues must
   stay visually distinct — an arrival is an event happening, a resolve is data
   landing for an event the user chose. */
@keyframes shakeResolvePulse {
  0% {
    transform: scale(1);
    opacity: 0.55;
  }
  60% {
    transform: scale(2.6);
    opacity: 0.14;
  }
  100% {
    transform: scale(3.1);
    opacity: 0;
  }
}
.marker.is-resolving::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  border: 1px solid var(--sev, rgba(203, 222, 239, 0.8));
  animation: shakeResolvePulse 900ms var(--resolve-delay, 0ms)
    cubic-bezier(0.22, 1, 0.36, 1) both;
  pointer-events: none;
}
/* A small, brief lift on the selected epicentre while the field resolves —
   enough to lead the eye, not enough to cover a station symbol. */
.marker.is-resolving .marker__ring {
  animation: epicentreLift 1700ms var(--resolve-delay, 0ms) ease-out both;
}
@keyframes epicentreLift {
  0% {
    transform: scale(1);
  }
  18% {
    transform: scale(1.28);
  }
  100% {
    transform: scale(1);
  }
}

/* Radiating emphasis on the selected epicentre while a shaking field is up.
   Three rings on a 2.4s life, launched 800ms apart, looping — so a new wave
   leaves as the previous one is still travelling and there is never a dead
   pause. Presentation only: these are not wave fronts and are never labelled
   as any. The authoritative contour geometry is static and untouched.

   CSS-driven off a fixed pool of three spans, so there is no rAF loop, no
   timer and nothing created or destroyed while it runs.

   Exactly one marker radiates at a time — the selected one, and only while a
   shaking field is up — so the spans are attached to that marker and to no
   other. They used to exist on every marker at `opacity: 0`, which was cheap
   in markup and catastrophic in compositing: `will-change` promotes each span
   to its own layer whether or not it is animating, and a 240px square layer
   costs ~230 KB of backing store. At the 7-day window that is three layers on
   each of ~2,000 markers — over 1.4 GB of GPU memory for geometry nobody can
   see. `will-change` therefore stays inside the `is-radiating` rule, where
   there are only ever three of them. */
.marker__wave {
  position: absolute;
  left: 50%;
  top: 50%;
  width: calc(var(--wave-radius, 120px) * 2);
  height: calc(var(--wave-radius, 120px) * 2);
  margin-left: calc(var(--wave-radius, 120px) * -1);
  margin-top: calc(var(--wave-radius, 120px) * -1);
  border-radius: 50%;
  opacity: 0;
  pointer-events: none;
  /* A soft band rather than a hard-edged circle: the gradient is what keeps it
     from reading as a neon ring. */
  background: radial-gradient(
    circle closest-side,
    transparent 54%,
    color-mix(in srgb, var(--wave-tint, #e0c98a) 30%, transparent) 74%,
    color-mix(in srgb, var(--wave-tint, #e0c98a) 6%, transparent) 88%,
    transparent 100%
  );
}

.marker.is-radiating .marker__wave {
  animation: shakeRadiate 2400ms linear infinite;
  will-change: transform, opacity;
}
/* Staggered thirds, addressed as adjacent siblings rather than by nth-of-type:
   the marker carries eight other spans before these, so nth-of-type would count
   the wrong elements. Lifetimes differ slightly so the three drift apart instead
   of restarting together — there is no point where the loop visibly resets. */
.marker.is-radiating .marker__wave + .marker__wave {
  animation-delay: 800ms;
  animation-duration: 2550ms;
}
.marker.is-radiating .marker__wave + .marker__wave + .marker__wave {
  animation-delay: 1600ms;
  animation-duration: 2700ms;
}

@keyframes shakeRadiate {
  0% {
    transform: scale(0.06);
    opacity: 0;
  }
  10% {
    opacity: 0.9;
  }
  55% {
    opacity: 0.4;
  }
  100% {
    transform: scale(1);
    opacity: 0;
  }
}

/* A slow luminous lift at the epicentre as each wave leaves — one cycle per
   launch, 1.25Hz, well under any flashing threshold and nowhere near white. */
.marker.is-radiating .marker__core {
  animation: epicentreEnergy 800ms ease-out infinite;
}
@keyframes epicentreEnergy {
  0% {
    filter: brightness(1.55);
  }
  50%,
  100% {
    filter: brightness(1);
  }
}

/* Observations settle in just after the field, once. */
@keyframes obsResolve {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}
.stations.is-resolving .obs {
  animation: obsResolve 260ms calc(var(--resolve-delay, 0ms) + 1400ms) ease both;
}

@media (prefers-reduced-motion: reduce) {
  .marker.is-resolving::after {
    animation: none;
    display: none;
  }
  .marker.is-resolving .marker__ring {
    animation: none;
  }
  .stations.is-resolving .obs {
    animation: none;
  }
  /* No repeating emphasis under reduced motion. The epicentre and the static
     ShakeMap stay exactly as they are. */
  .marker.is-radiating .marker__wave {
    animation: none;
    display: none;
  }
  .marker.is-radiating .marker__core {
    animation: none;
  }
}

/* Tsunami status. The word carries the meaning; colour is a quiet second
   signal and never the only one, and it stays on the status line rather than
   colouring the panel. */
/* External references. Compact rows, no thumbnails, no cards — a list of
   places to go, subordinate to everything measured above it. */
.reporting {
  list-style: none;
  margin: 0 0 8px;
  padding: 0;
  display: flex;
  flex-direction: column;
}
.reporting li + li {
  border-top: 1px solid var(--line);
}
.reporting a {
  display: flex;
  flex-direction: column;
  gap: 3px;
  padding: 9px 0;
  text-decoration: none;
  color: inherit;
}
.reporting a:hover .reporting__title {
  color: #eaf3fa;
  text-decoration: underline;
  text-underline-offset: 3px;
}
.reporting__meta {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 8px;
  font-size: 10px;
  color: var(--text-faint);
}
.reporting__meta b {
  font-weight: 500;
  letter-spacing: 0.04em;
  color: rgba(190, 216, 238, 0.86);
}
.reporting__meta em {
  font-style: normal;
  font-size: 8.5px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: rgba(160, 186, 210, 0.55);
}
.reporting__title {
  font-size: 12px;
  line-height: 1.45;
  color: var(--text-dim);
  /* Two lines, then ellipsis — the publisher and time stay visible. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.tsunami__status {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 4px 9px;
  margin: 2px 0 8px;
  padding-left: 11px;
  border-left: 2px solid var(--tstatus, rgba(150, 180, 210, 0.4));
}
.tsunami__status b {
  font-family: "IBM Plex Mono", monospace;
  font-size: 15px;
  font-weight: 500;
  letter-spacing: 0.03em;
  color: var(--tstatus, var(--text));
}
.tsunami__status span {
  font-size: 11px;
  color: var(--text-faint);
}
.tsunami__status[data-status="warning"] {
  --tstatus: #c8443c;
}
.tsunami__status[data-status="advisory"] {
  --tstatus: #dd8235;
}
.tsunami__status[data-status="watch"] {
  --tstatus: #d3b44a;
}
.tsunami__status[data-status="information"],
.tsunami__status[data-status="cancelled"] {
  --tstatus: #8fa5b8;
}

.tsunami__headline {
  margin: 0 0 6px;
  font-size: 12px;
  line-height: 1.5;
  color: var(--text-dim);
}
.tsunami__areas {
  list-style: none;
  margin: 0 0 6px;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 3px;
}
.tsunami__areas li {
  font-size: 12px;
  color: var(--text-dim);
}

.legend {
  position: absolute;
  right: 26px;
  /* Clears the attribution band, same as the shaking legend. */
  bottom: 56px;
  z-index: 4;
  display: flex;
  gap: 26px;
  padding: 12px 16px;
  border: 1px solid var(--line);
  background: rgba(6, 10, 16, 0.62);
  border-radius: 2px;
}

.legend__group {
  display: grid;
  gap: 6px;
}/* The shaking key is written to two surfaces and shown on exactly one. Desktop
   shows the floating panel beside the contours; the group inside the Legend is
   the mobile presentation of the same markup and must not double it here. */
.legend__group--shake {
  display: none;
}

.legend__label {
  font-size: 10px;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--text-faint);
}

.legend__sizes,
.legend__colors {
  display: flex;
  align-items: center;
  gap: 9px;
  height: 24px;
}

.legend__sizes i {
  width: var(--d);
  height: var(--d);
  border-radius: 50%;
  border: 1px solid rgba(170, 196, 218, 0.7);
}

.legend__colors i {
  width: 11px;
  height: 11px;
  border-radius: 50%;
  background: var(--sev-low);
}
.legend__colors i[data-severity="moderate"] {
  background: var(--sev-moderate);
}
.legend__colors i[data-severity="elevated"] {
  background: var(--sev-elevated);
}
.legend__colors i[data-severity="high"] {
  background: var(--sev-high);
}
.legend__colors i[data-severity="severe"] {
  background: var(--sev-severe);
}

.legend__ticks {
  display: flex;
  gap: 9px;
  font-family: "IBM Plex Mono", monospace;
  font-size: 10px;
  color: var(--text-faint);
  font-style: normal;
}
.legend__ticks em {
  font-style: normal;
  width: 20px;
  text-align: center;
}
.legend__ticks--ends {
  justify-content: space-between;
}
.legend__ticks--ends em {
  width: auto;
}

/* --------------------------------------------------------------- panel */

/* Typography for the wider detail measure. Prose is capped so a 500px panel
   does not produce 90-character lines; headings and fact rows take the full
   width because they are short by nature. */
.detail .section p,
.detail .absent,
.detail .meaning,
.detail .modelled,
.detail .dx__caption,
.detail .tsunami__headline {
  max-width: 46ch;
  text-wrap: pretty;
}
.detail .detail__place,
.detail .section__label,
.detail .reporting__title,
.detail .tsunami__areas li,
.detail .obscard__title {
  text-wrap: pretty;
}

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

.panel {
  grid-column: 1;
  grid-row: 1;
  position: relative;
  display: flex;
  flex-direction: column;
  min-height: 0;
  background: linear-gradient(180deg, var(--panel) 0%, var(--bg-lift) 100%);
  border-right: 1px solid var(--line);
}

.panel__grabber {
  display: none;
}

/* One identity block: wordmark, what this is, and how fresh it is. The rhythm
   is set by SEISMOS -> subtitle (6px); the status sits only slightly looser than
   that because it is supporting metadata for the same block, not a section of
   its own. The bottom is the one deliberately larger gap — enough to close the
   block before Recent Earthquakes without leaving an empty band. */
.identity {
  padding: 30px 26px 16px;
}

.identity__mark {
  margin: 0;
  font-size: 26px;
  font-weight: 500;
  letter-spacing: 0.22em;
  color: #f2f7fb;
}

.identity__sub {
  margin: 6px 0 0;
  font-size: 13px;
  color: var(--text-dim);
}

.status {
  display: flex;
  align-items: center;
  gap: 8px;
  /* Was 20px, which read as a separate section against a 6px baseline. */
  margin: 10px 0 0;
  font-family: "IBM Plex Mono", monospace;
  font-size: 11px;
  letter-spacing: 0.08em;
  color: var(--text-dim);
}

.status__retry {
  appearance: none;
  border: 0;
  background: none;
  padding: 0 0 0 4px;
  font: inherit;
  font-size: 11px;
  letter-spacing: 0.08em;
  color: rgba(203, 222, 239, 0.9);
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
}

/* The status dot is the only place the feed's health is expressed as colour,
   and it stays quiet: a delayed feed dims rather than alarms. */
.status[data-state="error"] .status__dot,
.status[data-state="unavailable"] .status__dot {
  background: #c8913f;
  box-shadow: none;
  animation: none;
}
.status[data-state="loading"] .status__dot,
.status[data-state="idle"] .status__dot {
  background: rgba(150, 180, 210, 0.5);
  box-shadow: none;
  animation: none;
}

.source__link {
  display: inline-block;
  margin-top: 10px;
  font-size: 12px;
  color: rgba(178, 208, 234, 0.92);
  text-decoration: underline;
  text-underline-offset: 3px;
}
.source__link:hover {
  color: #eaf3fa;
}

.status__dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: #4ec98a;
  box-shadow: 0 0 8px rgba(78, 201, 138, 0.55);
}

.status__state {
  color: #cfe0ee;
}

.status__sep {
  color: var(--text-faint);
}

.controls {
  display: grid;
  /* Three filter groups now sit above the list, so the vertical rhythm is
     tighter than it was with two — the list has to keep enough of the panel to
     be a list. */
  gap: 13px;
  padding: 0 26px 14px;
}

/* Filters is the parent concept: Time window and everything behind More
   filters belong to it. The label is what makes that grouping visible instead
   of implied. */
/* SEARCH and FILTERS are section titles like DEPTH and SHAKING, so they take
   the same treatment — see `.section__label`, which they now share. The gutter
   belongs to `.sechead`, never to the label: with the label inside the header
   row, padding here was applied twice and put SEARCH 20px right of FILTERS. */
.controls__label {
  display: block;
}

.filters__chevron {
  /* An <svg> with no width/height fills its container, which turned a 14px
     chevron into a full-width arrowhead. Size it explicitly. */
  flex: 0 0 auto;
  width: 14px;
  height: 14px;
  color: var(--text-faint);
  transition: transform 160ms cubic-bezier(0.22, 1, 0.36, 1);
}
.filters__label {
  white-space: nowrap;
}
.filters[aria-expanded="true"] .filters__chevron {
  transform: rotate(180deg);
}
@media (prefers-reduced-motion: reduce) {
  .filters__chevron {
    transition: none;
  }
}

/* THE EDITORIAL heading primitive — content, not chrome. "One earthquake, many
   intensities", "Why the shaking varied", "What moved". These open a passage of
   explanation, so they carry more contrast and weight than an application title
   and a size step above the 11px utility scale. Tracking eases back a little as
   the size and weight come up, or it reads as spaced-out lettering rather than
   as a heading.

   Still uppercase and tracked: this is the Seismos language at a different
   level, not a different language. Not a display heading, and never a card. */
.lesson__heading {
  margin: 0 0 var(--space-heading-body);
  font-size: 12.5px;
  font-weight: 600;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--text);
}
/* A lesson section stands well clear of the one before it — much further than a
   heading sits from its own body, which is what makes the grouping read. */
.lesson__heading + * {
  margin-top: 0;
}
/* The opening of a lesson: heading, then the subject, then the sentence that
   introduces it, then the figure. Tight through the three text lines so they
   read as one block, and a clear step before the figure. The subject stays much
   larger than the heading — the hierarchy there is size, not another style. */
.lesson__heading + .readout {
  margin-bottom: 6px;
}
.lesson__heading + .readout + .meaning {
  margin-bottom: var(--space-lesson-gap);
}
* + .lesson__heading {
  margin-top: var(--space-lesson-gap);
}

.control__label {
  display: block;
  margin-bottom: var(--space-label-control);
  font-size: 10px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-faint);
}

.segmented {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  border: var(--control-border);
  border-radius: var(--control-radius);
  overflow: hidden;
}

.segmented button {
  appearance: none;
  border: 0;
  border-right: 1px solid var(--line);
  background: transparent;
  color: var(--text-dim);
  font-family: "IBM Plex Mono", monospace;
  font-size: 12px;
  letter-spacing: 0.06em;
  /* Same vertical box as `.chips button`, so Time Window sits at the same
     height as Data Available beneath it. The touch target is bought back with
     min-height on mobile, never with visible padding. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: var(--control-pad-y) 0;
  cursor: pointer;
  transition: background 160ms cubic-bezier(0.22, 1, 0.36, 1), color 160ms cubic-bezier(0.22, 1, 0.36, 1);
}
.segmented button:active {
  background: rgba(150, 180, 210, 0.1);
}
.segmented button:last-child {
  border-right: 0;
}
.segmented button:hover {
  background: rgba(150, 180, 210, 0.06);
  color: var(--text);
}
.segmented button[aria-pressed="true"] {
  background: var(--control-selected-bg);
  color: #f0f6fb;
}

/* Wraps, always. `nowrap` made a chip group's width a function of its LABELS —
   and Data Available's labels carry live counts, so "All 2208 / ShakeMap 98"
   pushed the row 13px past the content column at 360px and left Felt hanging
   over the gutter. Content must change the data, never the layout width. */
.chips {
  display: flex;
  flex-wrap: wrap;
  gap: 7px;
  row-gap: 7px;
}

/* One toggle box. `.chips button` and `.segmented button` differ in width and
   in how they are grouped; height, padding, border, radius and state come from
   the same tokens so they read as one family. */
.chips button {
  appearance: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  border: var(--control-border);
  background: transparent;
  color: var(--text-dim);
  font-family: "IBM Plex Mono", monospace;
  font-size: 11px;
  padding: var(--control-pad-y) var(--control-pad-x);
  border-radius: var(--control-radius);
  cursor: pointer;
  transition:
    background 160ms cubic-bezier(0.22, 1, 0.36, 1),
    color 160ms cubic-bezier(0.22, 1, 0.36, 1),
    border-color 160ms cubic-bezier(0.22, 1, 0.36, 1),
    transform 200ms cubic-bezier(0.16, 1, 0.3, 1);
}
.chips button:active {
  transform: scale(0.96);
  transition-duration: 90ms;
}
.chips button:hover {
  color: var(--text);
  border-color: rgba(150, 180, 210, 0.32);
}
.chips button[aria-pressed="true"] {
  background: var(--control-selected-bg);
  border-color: var(--control-selected-border);
  color: #f0f6fb;
}

:where(.segmented, .chips) button:focus-visible,
.row:focus-visible {
  outline: 1px solid rgba(180, 214, 240, 0.85);
  outline-offset: 2px;
}

/* ---------------------------------------------------------------- list */

.list__head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  padding: var(--space-section-gap) 26px var(--space-title-content);
  border-top: 1px solid var(--line);
}
/* Secondary metadata opposite the title, not a second title. */
.list__count {
  font-size: 10px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-faint);
}

.list__count {
  font-family: "IBM Plex Mono", monospace;
  letter-spacing: 0.06em;
}

/* Product availability chips carry a count badge; the count is what makes the
   filter worth having, since most events have no rich products at all. */
/* Historical search. A secondary action and a mode, not a second app. */
/* Underlining the chevron makes it read as part of the phrase rather than as
   the direction of travel, so only the label carries it. */

.hist {
  display: flex;
  flex-direction: column;
  margin: 6px 0 0;
  border-top: 1px solid var(--line);
}
.hist[hidden] {
  display: none;
}
.hist__bar {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 20px 26px 12px;
}
.hist__field {
  flex: 1;
  min-width: 0;
  appearance: none;
  border: 0;
  border-bottom: 1px solid var(--line-strong);
  background: none;
  padding: 6px 2px;
  font: inherit;
  font-size: 12px;
  color: var(--text);
}
.hist__field::placeholder {
  color: var(--text-faint);
}
.hist__field:focus {
  outline: none;
  border-bottom-color: rgba(190, 216, 238, 0.6);
}
.hist__go {
  appearance: none;
  border: 1px solid rgba(150, 180, 210, 0.28);
  background: none;
  padding: 5px 10px;
  font: inherit;
  font-size: 11px;
  color: var(--text-dim);
  cursor: pointer;
  transition: border-color 140ms ease, color 140ms ease;
}
.hist__go:hover {
  border-color: rgba(190, 216, 238, 0.5);
  color: var(--text);
}
/* Historical search is a destination reached from the list, not a layer over
   it, so its chrome is a back control rather than a dismiss. It reuses `.back`
   — the same control that leaves a lesson — because it is the same idea, and
   only adds the spacing that lets the field below it breathe. */
.back--hist {
  width: 100%;
  justify-content: flex-start;
}
.hist__hint {
  margin: 0 26px 10px;
  font-size: 11px;
  line-height: 1.5;
  color: var(--text-faint);
}
.hist__hint[hidden] {
  display: none;
}
.hist__results {
  padding-bottom: 4px;
}

/* Historical context in the detail panel: unmistakable, and nowhere near the
   live status line. */
.histbadge {
  /* Every other block in the panel carries its own 26px inset as padding
     rather than inheriting one from `.detail`; this follows that. */
  margin: 12px 0 4px;
  padding: 0 26px;
  font-size: 9px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: rgba(206, 180, 148, 0.86);
}

/* The live list and its filters stay in memory and in the DOM; the mode simply
   hides them, so returning costs nothing and refetches nothing. */
body.is-historical-search .controls,
body.is-historical-search .search,
body.is-historical-search .list__head,
body.is-historical-search #list {
  display: none;
}

/* Data Available is the one group whose labels carry live counts, so it is laid
   out on a grid rather than by content: equal tracks mean a control is the same
   size whatever its number says, the group can never exceed the column, and the
   count of columns falls from four to two as the width shrinks instead of the
   row spilling past the gutter. */
.chips--product {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
}
/* "All 284" is one inline group, centred as a unit. It used to align on the
   baseline with the count trailing, which read as a label anchored left and a
   number sitting wherever it landed. The count stays subordinate through colour
   and weight, not through position. */
.chips--product button {
  align-items: baseline;
}
/* The count is reserved space, not measured space: tabular figures in a fixed
   box mean a chip is the same width at "2" and at "2208", so switching the time
   window changes the numbers without reflowing the row. */
.chips--product button b {
  min-width: 4ch;
  text-align: left;
  font-weight: 500;
  font-size: 10px;
  font-family: "IBM Plex Mono", monospace;
  font-variant-numeric: tabular-nums;
  color: var(--text-faint);
  transition: color 140ms ease;
}
.chips--product button[aria-pressed="true"] b {
  color: rgba(203, 222, 239, 0.9);
}

/* Search. Restrained and part of the panel, not a navigation bar. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* An input, not a toggle — but visibly of the same system: same border token,
   same radius, same restrained focus language. It reads as a field because of
   the icon, the placeholder and its full width, not because it was the only
   control without a box. */
.search {
  position: relative;
  display: flex;
  align-items: center;
  margin: 0 26px 2px;
  border: var(--control-border);
  border-radius: var(--control-radius);
  transition: border-color 140ms ease;
}
.search:focus-within {
  border-color: var(--control-selected-border);
}
.search__icon {
  position: absolute;
  left: 11px;
  width: 13px;
  height: 13px;
  color: var(--text-faint);
  pointer-events: none;
  transition: color 140ms ease;
}
.search:focus-within .search__icon {
  color: rgba(190, 216, 238, 0.9);
}
.search__field {
  flex: 1;
  /* A flex child defaults to min-width: auto, so its intrinsic content width
     becomes a floor and a long query pushes the row wider than the sheet. */
  min-width: 0;
  appearance: none;
  border: 0;
  background: none;
  padding: 9px 30px 9px 32px;
  font: inherit;
  font-size: 12px;
  color: var(--text);
}
.search__field::placeholder {
  color: var(--text-faint);
}
.search__field:focus {
  outline: none;
}
.search__field::-webkit-search-cancel-button {
  display: none;
}
.search__clear {
  position: absolute;
  right: 8px;
  appearance: none;
  border: 0;
  background: none;
  padding: 2px 4px;
  font-size: 15px;
  line-height: 1;
  color: var(--text-faint);
  cursor: pointer;
}
.search__clear:hover {
  color: var(--text);
}
.search__clear[hidden] {
  display: none;
}

/* Filter transitions. Rows leave faster than they arrive, so a filter change
   reads as the list settling rather than as one crossfade. */
.list li[data-id] {
  transition:
    opacity 165ms cubic-bezier(0.16, 1, 0.3, 1),
    transform 165ms cubic-bezier(0.16, 1, 0.3, 1);
}
.list li.is-entering {
  opacity: 0;
  transform: translateY(-6px);
}
.list li.is-leaving {
  opacity: 0;
  transform: translateY(4px);
  pointer-events: none;
  transition-duration: 140ms;
  transition-timing-function: cubic-bezier(0.4, 0, 1, 1);
}

#listCount.is-changing {
  animation: countChange 220ms ease;
}
@keyframes countChange {
  from {
    opacity: 0.45;
  }
  to {
    opacity: 1;
  }
}

/* Marker filter transitions, deliberately unlike the arrival pulse: an event
   leaving the filter is not an event happening. Applied to the visible disc
   parts — fill, ring and core — since the marker root carries the projected
   position transform and must not be scaled. */
.marker.is-filtering-in .marker__fill,
.marker.is-filtering-in .marker__ring,
.marker.is-filtering-in .marker__core,
.marker.is-filtering-out .marker__fill,
.marker.is-filtering-out .marker__ring,
.marker.is-filtering-out .marker__core {
  transition:
    opacity 150ms ease,
    transform 150ms cubic-bezier(0.16, 1, 0.3, 1);
}
.marker.is-filtering-in .marker__fill,
.marker.is-filtering-in .marker__ring {
  opacity: 0;
  transform: scale(0.6);
}
.marker.is-filtering-in .marker__core {
  opacity: 0;
}
.marker.is-filtering-out .marker__fill,
.marker.is-filtering-out .marker__ring {
  opacity: 0;
  transform: scale(0.7);
}
.marker.is-filtering-out .marker__core {
  opacity: 0;
}

@media (prefers-reduced-motion: reduce) {
  .list li[data-id],
  .marker.is-filtering-in .marker__fill,
  .marker.is-filtering-in .marker__ring,
  .marker.is-filtering-in .marker__core,
  .marker.is-filtering-out .marker__fill,
  .marker.is-filtering-out .marker__ring,
  .marker.is-filtering-out .marker__core {
    transition: none;
  }
  #listCount.is-changing {
    animation: none;
  }
}

.list {
  margin: 0;
  padding: 0 0 8px;
  /* The master is the scroller, not the list. The list used to scroll itself
     as a flex child, which meant the fixed controls above it could consume the
     panel and leave it a few pixels tall; giving it a min-height instead only
     pushed its box below the panel and produced two competing scrollbars. As
     ordinary flow inside one scrolling parent it is always fully reachable at
     any viewport height. */
  list-style: none;
  scrollbar-width: thin;
  scrollbar-color: rgba(150, 180, 210, 0.22) transparent;
}

.row {
  display: grid;
  grid-template-columns: 46px 1fr auto;
  align-items: center;
  gap: 14px;
  width: 100%;
  padding: 11px 26px;
  border: 0;
  border-bottom: 1px solid rgba(150, 180, 210, 0.06);
  border-left: 2px solid transparent;
  background: none;
  color: inherit;
  text-align: left;
  font: inherit;
  cursor: pointer;
  transition:
    background 160ms cubic-bezier(0.22, 1, 0.36, 1),
    border-left-color 200ms cubic-bezier(0.22, 1, 0.36, 1),
    transform 240ms cubic-bezier(0.16, 1, 0.3, 1);
}

.row:active {
  transform: scale(0.994);
  transition-duration: 90ms;
}

.row:hover {
  background: rgba(150, 180, 210, 0.045);
}

.row.is-hovered {
  background: rgba(150, 180, 210, 0.045);
}

.row.is-selected {
  background: rgba(163, 199, 228, 0.09);
  border-left-color: var(--sev);
}

.row__mag {
  font-family: "IBM Plex Mono", monospace;
  font-size: 21px;
  font-weight: 400;
  letter-spacing: -0.01em;
  color: var(--sev);
  font-variant-numeric: tabular-nums;
}

.row__body {
  display: grid;
  min-width: 0;
}

.row__place {
  display: flex;
  align-items: baseline;
  gap: 8px;
  min-width: 0;
  font-size: 13px;
  color: var(--text);
}

.row__placeText {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.row__meta {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 2px;
  font-size: 11px;
  color: var(--text-faint);
  font-variant-numeric: tabular-nums;
}

.row__depth {
  text-align: right;
  font-family: "IBM Plex Mono", monospace;
  font-size: 11px;
  color: var(--text-faint);
  line-height: 1.3;
}
.row__depth b {
  display: block;
  font-weight: 400;
  color: var(--text-dim);
}

.row__tsunami {
  flex: none;
  display: inline-block;
  padding: 0 5px;
  border: 1px solid rgba(150, 180, 210, 0.24);
  border-radius: 2px;
  font-family: "IBM Plex Mono", monospace;
  font-size: 9px;
  letter-spacing: 0.09em;
  color: var(--text-dim);
  vertical-align: 1px;
}

.list__empty {
  padding: 26px;
  color: var(--text-dim);
  font-size: 13px;
}

.provenance {
  display: grid;
  gap: 3px;
  padding: 14px 26px 18px;
  border-top: 1px solid var(--line);
  font-family: "IBM Plex Mono", monospace;
  font-size: 10px;
  letter-spacing: 0.05em;
  color: var(--text-dim);
}
.provenance__line--dim {
  color: var(--text-faint);
}

/* ------------------------------------------------- master / detail stack */

.stack {
  position: relative;
  flex: 1;
  min-height: 0;
  display: flex;
}

.master {
  flex: 1;
  display: flex;
  flex-direction: column;
  min-height: 0;
  /* The single scroll container for the whole master view. */
  overflow-y: auto;
  /* The gutter is reserved whether or not a scrollbar is showing, so a result
     set that grows past the viewport cannot shift the content column sideways. */
  scrollbar-gutter: stable;
  overflow-x: hidden;
  scrollbar-width: thin;
  transition: opacity 200ms ease, transform 240ms ease;
}

/* Other rows recede as the selected event leads into detail: a short, slightly
   receding exit, so the master list reads as moving behind the detail rather
   than sliding away beside it. */
.panel[data-state="detail"] .master {
  opacity: 0;
  transform: translateX(-16px) scale(0.988);
  pointer-events: none;
  transition:
    opacity 150ms cubic-bezier(0.4, 0, 1, 1),
    transform 240ms cubic-bezier(0.4, 0, 1, 1);
}

/* Coming back, the first rows lift in behind the block motion. Only the rows
   actually in view are staggered; the rest simply arrive with the panel. */
@keyframes rowReturn {
  from {
    opacity: 0;
    transform: translateX(-9px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}
.master.is-returning .list > li:nth-child(-n + 7) .row {
  animation: rowReturn 300ms cubic-bezier(0.16, 1, 0.3, 1) backwards;
}
.master.is-returning .list > li:nth-child(2) .row {
  animation-delay: 26ms;
}
.master.is-returning .list > li:nth-child(3) .row {
  animation-delay: 48ms;
}
.master.is-returning .list > li:nth-child(4) .row {
  animation-delay: 66ms;
}
.master.is-returning .list > li:nth-child(5) .row {
  animation-delay: 80ms;
}
.master.is-returning .list > li:nth-child(6) .row {
  animation-delay: 90ms;
}
.master.is-returning .list > li:nth-child(7) .row {
  animation-delay: 96ms;
}

.detail {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  overflow-y: auto;
  /* Required, not redundant: when one axis is not `visible` the other computes
     to `auto`, so declaring only overflow-y turns this into a horizontal scroll
     container. Any detail content wider than the panel — a long place name, a
     coordinate pair, a wide figure — then pans the sheet sideways on mobile.
     `.master` carries the same guard. */
  overflow-x: hidden;
  scrollbar-width: thin;
  scrollbar-color: rgba(150, 180, 210, 0.22) transparent;
  opacity: 0;
  transform: translateX(20px);
}
/* `.detail` is absolutely positioned over the whole stack, so `display: flex`
   here would beat the UA `[hidden] { display: none }` rule and leave an
   invisible full-size layer swallowing every click meant for the master list.
   The hidden state must win — but as `visibility`, not `display`: an element
   coming out of `display: none` has not been rendered, and a CSS animation on
   it stays pending until the browser gets around to rendering it, which can be
   most of a second. Kept as a box, hidden and untouchable, the entry animation
   starts on the very next frame. `hidden` still removes it from the
   accessibility tree, and pointer events are off in master state regardless. */
.detail[hidden] {
  visibility: hidden;
  /* The pane keeps a layout box so its entry animation has something to start
     from, which means it also keeps the translateX of its exit state — and
     that pushed its right edge past the panel and produced a horizontal
     scrollbar. The entry keyframe declares its own `from`, so clearing the
     transform here costs nothing. */
  transform: none;
}
.panel:not([data-state="detail"]) .detail {
  pointer-events: none;
}
/* Entry is an animation, not a transition: the pane comes from `display:none`
   (the `[hidden]` rule below), and a newly displayed element has no previously
   rendered style for a transition to interpolate from — it would snap. An
   animation starts on the element's first rendered frame.

   BOTH ends are declared explicitly. With only a `from` keyframe the end value
   resolves against the element's underlying style, which is still the
   master-state `opacity: 0` at the moment the animation is created — the pane
   then animates 0 to 0 and jumps to 1 when the animation ends, which looks
   exactly like no animation at all. The transition further down is what plays
   on the way OUT, where the element is already rendered. */
/* Both directions are animations, and for the same reason: the pane's opacity
   is animation-driven on the way in, and a transition that starts from an
   animated value does not reliably fire. Exit is faster and accelerates away;
   entry decelerates. The asymmetry is what stops the two panes reading as one
   crossfade. */
@keyframes detailEnter {
  from {
    opacity: 0;
    transform: translateX(20px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}
.detail.is-entering {
  animation: detailEnter 360ms cubic-bezier(0.16, 1, 0.3, 1) both;
}

@keyframes detailExit {
  from {
    opacity: 1;
    transform: none;
  }
  to {
    opacity: 0;
    transform: translateX(20px);
  }
}
.detail.is-leaving {
  animation: detailExit 210ms cubic-bezier(0.4, 0, 1, 1) both;
}

.panel[data-state="detail"] .detail {
  opacity: 1;
  transform: none;
}

/* Educational navigation, one level deeper.
   The lessons sit below the selected-event overview and the swap used to be
   instantaneous, which said nothing about that. Going deeper pushes the overview
   out to the left and brings the lesson in from the right; Back reverses it.
   Written as four short keyframes rather than one parameterised by a custom
   property: custom properties inherit, and an inherited value changing on the
   pane invalidates the style of everything inside it.
   The travel is the same 18-20px the pane already uses coming in from master —
   this is navigation feedback, not a page push, and the fade does most of the
   work. Only this element moves; the sheet, the globe and the camera do not. */
@keyframes navExitDeeper {
  from { opacity: 1; transform: none; }
  to { opacity: 0; transform: translateX(-18px); }
}
@keyframes navEnterDeeper {
  from { opacity: 0; transform: translateX(18px); }
  to { opacity: 1; transform: none; }
}
@keyframes navExitBack {
  from { opacity: 1; transform: none; }
  to { opacity: 0; transform: translateX(18px); }
}
@keyframes navEnterBack {
  from { opacity: 0; transform: translateX(-18px); }
  to { opacity: 1; transform: none; }
}
.detail.is-nav-exit-deeper {
  animation: navExitDeeper 130ms cubic-bezier(0.4, 0, 1, 1) both;
}
.detail.is-nav-enter-deeper {
  animation: navEnterDeeper 200ms cubic-bezier(0.16, 1, 0.3, 1) both;
}
.detail.is-nav-exit-back {
  animation: navExitBack 130ms cubic-bezier(0.4, 0, 1, 1) both;
}
.detail.is-nav-enter-back {
  animation: navEnterBack 200ms cubic-bezier(0.16, 1, 0.3, 1) both;
}

/* Sections rise in sequence on entry only — set by the panel when it comes
   from master, so a re-render while detail is already open does not replay. */
@keyframes detailRise {
  from {
    opacity: 0;
    transform: translateY(9px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}
.detail.is-entering > * {
  animation: detailRise 340ms cubic-bezier(0.16, 1, 0.3, 1) backwards;
}
.detail.is-entering > :nth-child(1) {
  animation-delay: 30ms;
}
.detail.is-entering > :nth-child(2) {
  animation-delay: 60ms;
}
.detail.is-entering > :nth-child(3) {
  animation-delay: 86ms;
}
.detail.is-entering > :nth-child(4) {
  animation-delay: 108ms;
}
.detail.is-entering > :nth-child(5) {
  animation-delay: 126ms;
}
.detail.is-entering > :nth-child(6) {
  animation-delay: 140ms;
}
.detail.is-entering > :nth-child(n + 7) {
  animation-delay: 150ms;
}

.back {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 14px 26px;
  border: 0;
  border-bottom: 1px solid var(--line);
  background: none;
  color: var(--text-dim);
  font: inherit;
  font-size: 12px;
  text-align: left;
  cursor: pointer;
}
.back:hover {
  color: var(--text);
}
.back:hover span {
  transform: translateX(-3px);
}
.back:focus-visible {
  outline: 1px solid rgba(180, 214, 240, 0.85);
  outline-offset: -4px;
}
.back span {
  transition: transform 260ms cubic-bezier(0.16, 1, 0.3, 1);
}
.back > span:first-child {
  font-family: "IBM Plex Mono", monospace;
}
.back__short {
  display: none;
}

.detail__head {
  display: flex;
  align-items: baseline;
  gap: 12px;
  padding: 22px 26px 0;
}

.detail__mag {
  font-family: "IBM Plex Mono", monospace;
  font-size: 44px;
  line-height: 1;
  letter-spacing: -0.02em;
  color: var(--sev);
  font-variant-numeric: tabular-nums;
}
.detail__mag b {
  font-size: 20px;
  font-weight: 400;
  margin-right: 2px;
}

.detail__class {
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-faint);
}

/* Share belongs to the selected earthquake, so it lives in the event's own
   header block — not in the sheet chrome, which is the bottom sheet's own
   furniture, and not next to Back, which is navigation. It is a sibling of the
   magnitude and classification rather than part of that line, so the
   "M5.9 · MODERATE" hierarchy is untouched: `margin-left: auto` pushes it to
   the upper right of the same content area.

   44x44 is the touch target, bought with padding around an 18px glyph. The
   negative right margin is optical alignment, not a hack: it puts the icon's
   visual right edge on the same 26px content margin the place name and the
   readouts use, while the target itself stays comfortably inside the panel. */
.share {
  margin-left: auto;
  margin-right: -13px;
  align-self: center;
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 0;
  border-radius: 999px;
  background: none;
  color: var(--text-faint);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.share svg {
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.share:hover {
  color: var(--text);
}
.share:focus-visible {
  outline: 2px solid var(--sev, #7f95ad);
  outline-offset: -6px;
}
@media (hover: none) {
  /* No hover on touch, so the resting state has to carry the affordance. */
  .share {
    color: var(--text-dim);
  }
}

/* Lesson chrome: Back keeps the left edge, Share the right. The row keeps the
   border it had when Back was the only thing in it. */
.subhead {
  display: flex;
  align-items: center;
  border-bottom: 1px solid var(--line);
}
.subhead .back {
  border-bottom: 0;
}
.share--sub {
  margin-right: 13px;
  flex: 0 0 auto;
}

/* Confirmation, not a modal: it interrupts nothing and takes no dismissal.
   Above the sheet on mobile, and out of the way of the reserved globe zones. */
.toast {
  position: fixed;
  left: 50%;
  bottom: 28px;
  transform: translate(-50%, 8px);
  z-index: 60;
  padding: 9px 16px;
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  background: rgba(9, 14, 21, 0.94);
  color: var(--text);
  font-size: 13px;
  pointer-events: none;
  opacity: 0;
  transition: opacity 160ms ease, transform 160ms ease;
}
.toast.is-shown {
  opacity: 1;
  transform: translate(-50%, 0);
}
@media (prefers-reduced-motion: reduce) {
  .toast {
    transition: opacity 120ms ease;
    transform: translate(-50%, 0);
  }
}

.detail__place {
  margin: 12px 0 0;
  padding: 0 26px;
  font-size: 17px;
  font-weight: 500;
  line-height: 1.3;
  text-wrap: pretty;
  color: var(--text);
}

/* Header metadata. Time and coordinates share one compact row: coordinates are
   precision the reader may want and never the thing they came for, so they sit
   beside the timestamp rather than claiming a section of their own. */
.detail__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 14px;
  padding: 8px 26px 0;
}
.detail__time {
  font-size: 12px;
  color: var(--text-dim);
  font-variant-numeric: tabular-nums;
}
.detail__coords {
  font-size: 11px;
  color: var(--text-faint);
  font-variant-numeric: tabular-nums;
}

/* Major sections. The value inside a section is still the loudest thing in it —
   `10 km` must outweigh `DEPTH` — so the boundary is built from structure
   rather than from type size: a generous gap above, then the label, then a
   hairline that runs the width of the panel and separates the topic from its
   content. That gives two independent hierarchies at once. Scrolling reads the
   rules; reading a section reads the value. */
.section {
  padding: 26px 26px 0;
  /* The rule divides sections, not a section from its own heading. It used to
     sit under the label, which read as `DEPTH` being separated from the value
     it names; above the heading it groups the two and marks the boundary. */
  border-top: 1px solid var(--line);
  margin-top: 8px;
}
.section:last-of-type {
  padding-bottom: 30px;
}
/* The title has to dominate the secondary action beside it, so it carries a
   little more weight and luminance than before. Still the same uppercase,
   tracked language — this is emphasis, not a new type ramp. The bottom margin
   moved to `.sechead`, which now owns the gap to the content. */
/* THE APPLICATION section-title primitive — chrome. RECENT EARTHQUAKES, SEARCH,
   FILTERS and the overview section headings are the same thing: compact labels
   that name a region of the interface. Restrained by design; they must not
   compete with the content they introduce.

   This is NOT the treatment for headings inside a lesson. Those introduce prose
   and figures rather than controls, and they have their own primitive —
   `.lesson__heading`. See VISUAL_SYSTEM 41b. */
.section__label,
.controls__label,
.list__head > span:first-child {
  margin: 0;
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--text-title);
}

/* One section grammar: TITLE on the left, its secondary action on the right,
   content below. Card-like organisation with no card chrome — the divider,
   this header row and the spacing do the grouping. */
.sechead {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  margin: 0 0 var(--space-title-content);
}
.sechead__action {
  display: inline-flex;
  align-items: baseline;
  gap: 6px;
  flex: 0 0 auto;
  /* A larger pointer target than the text alone, given back to the layout with
     the negative margin, so the header row height is unchanged. Mobile widens
     this further to 44px. */
  padding: 5px 0;
  margin: -5px 0;
  appearance: none;
  border: 0;
  background: none;
  font: inherit;
  font-size: 12px;
  /* No underline: beside a heading it would compete with it, and the chevron
     already carries the affordance. */
  text-decoration: none;
  color: var(--text-dim);
  cursor: pointer;
  white-space: nowrap;
  -webkit-tap-highlight-color: transparent;
}
/* `display` on the class beats the UA `[hidden] { display: none }`, so without
   this an unsupported section still showed its Explore action — and clicking it
   navigated into a lesson with no data. */
.sechead__action[hidden] {
  display: none;
}
.sechead__chevron {
  font-size: 15px;
  line-height: 1;
  color: currentColor;
  opacity: 0.62;
}
.sechead__action:hover,
.sechead__action:focus-visible {
  color: var(--text);
}
.sechead__action:hover .sechead__chevron,
.sechead__action:focus-visible .sechead__chevron {
  opacity: 1;
}
/* Pressed mutes, in the vocabulary the chips and rows already press with — no
   underline, no full-width highlight, nothing tab-like. */
.sechead__action:active {
  color: var(--text-faint);
}

/* Provenance is the one section that should recede rather than announce
   itself, so it keeps the structure and drops the emphasis. */
.section--source .section__label {
  color: var(--text-faint);
}

/* Child labels inside a section — Fatalities, Economic loss, Origin, Review.
   Sentence-case sans, not the tracked instrument caps: the caps are what marks
   a major boundary, and reusing them one level down is what made these read as
   peers of IMPACT rather than as its contents. */
.sublabel,
.source dt {
  font-family: inherit;
  font-size: 11.5px;
  font-weight: 400;
  letter-spacing: 0.01em;
  text-transform: none;
  color: var(--text-faint);
}

/* The category, not the number, is what has to land first. "Fewer than 1" is
   unreadable until you know it estimates deaths, and this label used to be the
   dimmest element in the row — 11.5px of --text-faint under a 15px value at full
   brightness, which is precisely backwards. It now carries at least the weight
   of the value it names. */
.estimates dt {
  font-family: inherit;
  font-size: 12.5px;
  font-weight: 500;
  letter-spacing: 0.015em;
  text-transform: none;
  color: var(--text);
}
.section p {
  margin: 0;
  font-size: 13px;
  line-height: 1.5;
  color: var(--text);
  text-wrap: pretty;
}
.section p + p {
  margin-top: 8px;
}
/* A paragraph after a definition list or a figure needs the gap that `p + p`
   gives it after a paragraph. `.section p { margin: 0 }` out-specifies
   `.modelled`, so without this the caveat under a facet list sits on top of it. */
.section > dl + p,
.section > ol + p,
.section > ul + p {
  margin-top: 14px;
}
.section .muted {
  color: var(--text-dim);
}
.section .absent {
  color: var(--text-faint);
  font-size: 12px;
}

/* Shaking zones: swatch, label, intensity value. No chart. */

.zones {
  display: grid;
  gap: 7px;
  /* Row rhythm stays at 7px; the gap before the technical note below is the
     one that was too tight. */
  margin: 10px 0 14px;
  padding: 0;
  list-style: none;
}
.zones li {
  display: grid;
  grid-template-columns: 12px 1fr auto;
  align-items: center;
  gap: 10px;
  font-size: 12px;
  color: var(--text-dim);
}
.zones i {
  width: 12px;
  height: 12px;
  border-radius: 2px;
  background: var(--zone);
  opacity: 0.85;
}
.zones b {
  font-weight: 400;
  color: var(--text);
}
.zones span {
  font-family: "IBM Plex Mono", monospace;
  font-size: 11px;
  color: var(--text-faint);
}

/* ---------------------------------------------------- detail readouts */

/* The answer to a section, in one line: the number or name that the reader
   came for, then its plain-language classification. One shape reused by depth,
   shaking, faulting, impact and tsunami status, so the panel reads as one
   instrument rather than five section designs. */
.readout {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 10px;
  margin: 0 0 8px;
}
.readout b {
  font-size: 26px;
  font-weight: 500;
  letter-spacing: -0.02em;
  line-height: 1.05;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}
.readout span {
  font-size: 11px;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  color: var(--text-dim);
}
.readout i {
  width: 10px;
  height: 10px;
  border-radius: 2px;
  background: var(--peak, var(--sev, var(--text-faint)));
  align-self: center;
}
.readout--mmi b {
  font-family: "IBM Plex Mono", monospace;
  font-size: 22px;
}
.readout--alert b {
  font-size: 17px;
}
.readout--alert span {
  text-transform: none;
  letter-spacing: 0;
  font-size: 12px;
  color: var(--text-dim);
}
.readout--alert[data-level="green"] i { background: #4f9d6b; }
.readout--alert[data-level="yellow"] i { background: var(--sev-moderate); }
.readout--alert[data-level="orange"] i { background: var(--sev-elevated); }
.readout--alert[data-level="red"] i { background: var(--sev-severe); }
.readout--unknown b {
  font-size: 17px;
  color: var(--text-dim);
}
.readout--unknown span {
  text-transform: none;
  letter-spacing: 0;
  font-size: 12px;
}

/* The one sentence that says what the reading means. Body-weight and full
   colour, because it is the point of the section — not a caption. */
.meaning {
  margin: 0 0 14px;
  font-size: 13px;
  line-height: 1.55;
  color: var(--text-dim);
}

/* Provenance and modelling caveats, kept deliberately quiet and always last
   within their section. */
/* A quiet note under a value: provenance, a unit, a scope. Doubled up so it
   beats `.section p`, which is (0,1,1) and was silently winning — every one of
   these has been rendering at body size and body colour inside a section, which
   is the one thing the class exists to prevent. */
.modelled,
.section .modelled {
  margin: 14px 0 0;
  font-size: 10.5px;
  line-height: 1.5;
  color: var(--text-faint);
}

/* ------------------------------------------------------- depth diagram */

.dx {
  margin: 0;
}
.dx__svg {
  width: 100%;
  height: auto;
  overflow: visible;
}
.dx__band {
  fill: rgba(150, 180, 210, 0.03);
}
.dx__band[data-active="true"] {
  fill: rgba(150, 180, 210, 0.09);
}
.dx__rule {
  stroke: rgba(150, 180, 210, 0.14);
  stroke-width: 1;
}
.dx__surface {
  stroke: rgba(203, 222, 239, 0.5);
  stroke-width: 1.25;
}
/* The drop from the epicentre is the whole point of the diagram: it is what
   turns a marker on a scale into an earthquake underneath a place. */
.dx__stem {
  stroke: rgba(203, 222, 239, 0.35);
  stroke-width: 1;
}
.dx__drop {
  stroke: var(--sev, #7f95ad);
  stroke-width: 1.25;
}
.dx__dropHead {
  fill: none;
  stroke: var(--sev, #7f95ad);
  stroke-width: 1.25;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.dx__hypo {
  fill: var(--sev, #7f95ad);
}
.dx__hypoHalo {
  fill: none;
  stroke: var(--sev, #7f95ad);
  stroke-width: 1;
  opacity: 0.3;
}
.dx__pointLabel {
  font-family: "IBM Plex Mono", monospace;
  font-size: 8px;
  letter-spacing: 0.12em;
  fill: var(--text-faint);
}
.dx__surfaceLabel,
.dx__bandName,
.dx__tick,
.dx__unit,
.dx__hypoLabel {
  font-family: "IBM Plex Mono", monospace;
  fill: var(--text-faint);
}
.dx__surfaceLabel,
.dx__bandName {
  font-size: 8px;
  letter-spacing: 0.11em;
}
.dx__bandName[data-active="true"] {
  fill: var(--text-dim);
}
.dx__tick,
.dx__unit {
  font-size: 8px;
}
/* Knocked out of the dashed depth line rather than sitting on top of it: at
   shallow depths the marker lands within a few pixels of the surface, and the
   line would otherwise run straight through the number. */
.dx__hypoLabel {
  font-size: 11px;
  fill: var(--text);
  paint-order: stroke fill;
  stroke: var(--panel);
  stroke-width: 3px;
  stroke-linejoin: round;
}
.dx__caption {
  margin-top: 8px;
  font-size: 10.5px;
  color: var(--text-faint);
}

/* ------------------------------------------- explanatory figure motion */

/* The diagrams explain a movement, so they perform it. Everything here is a CSS
   animation on a transform: no rAF, no timers, and the browser stops the work
   itself when the tab is hidden. `app.js` adds `.is-playing` through one shared
   IntersectionObserver.

   The resting state of every figure is its finished geometry — the animations
   move away from it and come back — so a figure that never plays, or one under
   reduced motion, is exactly the static diagram that was approved. */

/* Depth. The drop is drawn from the epicentre down to the hypocentre once, and
   then the hypocentre gives three small pulses. `pathLength="1"` on the line
   makes the draw independent of how deep the earthquake actually is. */
@keyframes dxDrawDrop {
  from { stroke-dashoffset: 1; }
  to { stroke-dashoffset: 0; }
}
@keyframes dxDropHead {
  0%, 55% { opacity: 0; }
  100% { opacity: 1; }
}
@keyframes dxHypoPulse {
  0%, 100% { transform: scale(1); opacity: 1; }
  50% { transform: scale(1.55); opacity: 0.55; }
}
.dx--motion.is-playing .dx__drop {
  stroke-dasharray: 1;
  animation: dxDrawDrop 560ms cubic-bezier(0.33, 0, 0.2, 1) both;
}
.dx--motion.is-playing .dx__dropHead {
  animation: dxDropHead 620ms linear both;
}
.dx--motion.is-playing .dx__hypoHalo {
  transform-box: fill-box;
  transform-origin: center;
  animation: dxHypoPulse 700ms ease-in-out 560ms 3 both;
}

/* Faulting. The blocks carry the motion and the arrows travel with the block
   they belong to, so the arrow reinforces a movement rather than being the only
   thing that moves. Every element on the far side of the fault is marked
   `data-side="foot"`; the fault surface itself never moves.

   One pair of keyframes per mechanism, shared by the overview diagram — three
   cycles on first view — and the lesson hero, which loops while it is on screen.
   A short hold at the displaced state is what makes the offset readable. */
/* The earthquake cycle: pressure building along a fault until it gives.

   STAGING — the single most important decision here. Every diagram is drawn in
   its POST-earthquake state, because that is what a reader with reduced motion
   must see: a fault with real offset and real throw. But the animation starts by
   displacing the hanging wall back by the full slip, so the cycle opens on flat
   ground with the strata running unbroken across the fault, and the earthquake
   is what produces the offset. An earlier version animated a few pixels on top
   of an offset that was already baked into the geometry; the change was invisible
   next to the thing it was supposed to be causing.

   That is also why every dip-slip fault is planar. Sliding a block along a
   straight plane keeps the two faces exactly coincident at any slip; a curved
   ramp opens a crack the moment the displacement is large enough to see.

   MOTION DESIGN — principles, not taste.

   Easing carries the physics. Loading is ease-IN (0.55, 0, 1, 0.45): it starts
   almost imperceptibly and accelerates toward failure. Release is a hard attack
   with a long settle (0, 0.85, 0.25, 1) — nearly all of the movement happens in
   the first fraction of the segment. The two curves are mirror images on purpose.

   Spacing carries the crescendo. Keyframe intervals tighten from 6% of the cycle
   to 1.1% while amplitude climbs a power curve, so the figure shakes faster AND
   harder. Amplitude alone reads as "louder"; spacing is what reads as "sooner".

   Anticipation. Creep and three arrested micro-slips give back only 12.5% of the
   throw across 66% of the cycle, and then everything stops dead for 4%. The
   stillness is what makes the release land.

   Follow-through. The slip overshoots its final position by 8.5% and settles
   back. A movement that stops dead on its mark reads as mechanical; one that
   overshoots reads as released energy.

   Exaggeration ratio. The earthquake delivers 87.5% of the throw in a single
   2.5% segment — 7x what the entire loading phase managed, in 1/26th the time.
   The release shake peaks at 6.5px against a 2.02px smooth build and 3.3px
   micro-slip spikes: 3.2x the build, and still twice the largest interruption
   in it.

   Secondary action is staggered, not synchronised: the rupture leads the slip by
   a beat, the waves follow it, the shake trails both. Everything landing on one
   frame reads as a single object moving; offset reads as consequence.

   Staging of attention. One focal point per moment — the contact while loading,
   the slip and rupture at release, the waves afterwards.

   Monotonic. The hanging wall only ever moves toward its destination. An
   earthquake does not reverse itself, so nothing here plays backwards; the reset
   is a fade, not a rewind.

     0-6%    aligned and quiet. Flat ground, strata unbroken.
     6-66%   the crescendo, interrupted at 30, 48 and 60% by a micro-slip that
             immediately arrests. A stress halo concentrates on the contact and
             shear ticks crawl along it.
     66-70%  the tense freeze. Full load, nothing moving.
     70-77%  release: rupture, the full slip with overshoot, waves, hard shake.
     77-86%  damping, visibly returning to rest.
     86-95%  rest. `transform: none` here IS the approved static diagram.
     95-100% reset by fade. */@keyframes fxTremorReverse {
  0%, 6% { transform: none; }
  6.0% { transform: translate(0.06px, 0.1px); }
  12.0% { transform: translate(-0.07px, -0.12px); }
  17.51% { transform: translate(0.09px, 0.16px); }
  22.57% { transform: translate(-0.13px, -0.23px); }
  27.22% { transform: translate(0.18px, 0.33px); }
  30.5% { transform: translate(0.43px, 0.77px); }
  31.48% { transform: translate(-0.24px, -0.43px); }
  31.7% { transform: translate(-0.2px, -0.36px); }
  35.4% { transform: translate(0.31px, 0.55px); }
  39.0% { transform: translate(-0.38px, -0.67px); }
  42.31% { transform: translate(0.45px, 0.8px); }
  45.34% { transform: translate(-0.52px, -0.92px); }
  48.13% { transform: translate(0.59px, 1.05px); }
  48.5% { transform: translate(1.14px, 2.03px); }
  49.7% { transform: translate(-0.54px, -0.96px); }
  50.69% { transform: translate(-0.66px, -1.18px); }
  53.04% { transform: translate(0.73px, 1.3px); }
  55.2% { transform: translate(-0.8px, -1.42px); }
  57.18% { transform: translate(0.86px, 1.53px); }
  59.0% { transform: translate(-0.92px, -1.64px); }
  60.5% { transform: translate(1.85px, 3.3px); }
  60.67% { transform: translate(0.98px, 1.75px); }
  61.7% { transform: translate(-0.87px, -1.56px); }
  62.21% { transform: translate(-1.03px, -1.84px); }
  63.62% { transform: translate(1.09px, 1.94px); }
  64.91% { transform: translate(-1.13px, -2.02px); }
  66%, 70.4% { transform: none; }
  70.8% { transform: translate(3.64px, 6.5px); }
  72.2% { transform: translate(-2.58px, -4.6px); }
  73.6% { transform: translate(1.74px, 3.1px); }
  75.2% { transform: translate(-1.12px, -2.0px); }
  77.0% { transform: translate(0.7px, 1.25px); }
  79.2% { transform: translate(-0.4px, -0.72px); }
  81.6% { transform: translate(0.21px, 0.38px); }
  84.0% { transform: translate(-0.09px, -0.16px); }
  86%, 100% { transform: none; }
}

@keyframes fxTremorNormal {
  0%, 6% { transform: none; }
  6.0% { transform: translate(0.06px, 0.1px); }
  12.0% { transform: translate(-0.07px, -0.12px); }
  17.51% { transform: translate(0.1px, 0.16px); }
  22.57% { transform: translate(-0.14px, -0.23px); }
  27.22% { transform: translate(0.2px, 0.33px); }
  30.5% { transform: translate(0.46px, 0.77px); }
  31.48% { transform: translate(-0.26px, -0.43px); }
  31.7% { transform: translate(-0.22px, -0.36px); }
  35.4% { transform: translate(0.33px, 0.55px); }
  39.0% { transform: translate(-0.4px, -0.67px); }
  42.31% { transform: translate(0.48px, 0.8px); }
  45.34% { transform: translate(-0.55px, -0.92px); }
  48.13% { transform: translate(0.63px, 1.05px); }
  48.5% { transform: translate(1.22px, 2.03px); }
  49.7% { transform: translate(-0.58px, -0.96px); }
  50.69% { transform: translate(-0.71px, -1.18px); }
  53.04% { transform: translate(0.78px, 1.3px); }
  55.2% { transform: translate(-0.85px, -1.42px); }
  57.18% { transform: translate(0.92px, 1.53px); }
  59.0% { transform: translate(-0.98px, -1.64px); }
  60.5% { transform: translate(1.98px, 3.3px); }
  60.67% { transform: translate(1.05px, 1.75px); }
  61.7% { transform: translate(-0.94px, -1.56px); }
  62.21% { transform: translate(-1.1px, -1.84px); }
  63.62% { transform: translate(1.16px, 1.94px); }
  64.91% { transform: translate(-1.21px, -2.02px); }
  66%, 70.4% { transform: none; }
  70.8% { transform: translate(3.9px, 6.5px); }
  72.2% { transform: translate(-2.76px, -4.6px); }
  73.6% { transform: translate(1.86px, 3.1px); }
  75.2% { transform: translate(-1.2px, -2.0px); }
  77.0% { transform: translate(0.75px, 1.25px); }
  79.2% { transform: translate(-0.43px, -0.72px); }
  81.6% { transform: translate(0.23px, 0.38px); }
  84.0% { transform: translate(-0.1px, -0.16px); }
  86%, 100% { transform: none; }
}

@keyframes fxTremorStrikeSlip {
  0%, 6% { transform: none; }
  6.0% { transform: translateX(0.1px); }
  12.0% { transform: translateX(-0.12px); }
  17.51% { transform: translateX(0.16px); }
  22.57% { transform: translateX(-0.23px); }
  27.22% { transform: translateX(0.33px); }
  30.5% { transform: translateX(0.77px); }
  31.48% { transform: translateX(-0.43px); }
  31.7% { transform: translateX(-0.36px); }
  35.4% { transform: translateX(0.55px); }
  39.0% { transform: translateX(-0.67px); }
  42.31% { transform: translateX(0.8px); }
  45.34% { transform: translateX(-0.92px); }
  48.13% { transform: translateX(1.05px); }
  48.5% { transform: translateX(2.03px); }
  49.7% { transform: translateX(-0.96px); }
  50.69% { transform: translateX(-1.18px); }
  53.04% { transform: translateX(1.3px); }
  55.2% { transform: translateX(-1.42px); }
  57.18% { transform: translateX(1.53px); }
  59.0% { transform: translateX(-1.64px); }
  60.5% { transform: translateX(3.3px); }
  60.67% { transform: translateX(1.75px); }
  61.7% { transform: translateX(-1.56px); }
  62.21% { transform: translateX(-1.84px); }
  63.62% { transform: translateX(1.94px); }
  64.91% { transform: translateX(-2.02px); }
  66%, 70.4% { transform: none; }
  70.8% { transform: translateX(6.5px); }
  72.2% { transform: translateX(-4.6px); }
  73.6% { transform: translateX(3.1px); }
  75.2% { transform: translateX(-2.0px); }
  77.0% { transform: translateX(1.25px); }
  79.2% { transform: translateX(-0.72px); }
  81.6% { transform: translateX(0.38px); }
  84.0% { transform: translateX(-0.16px); }
  86%, 100% { transform: none; }
}

@keyframes fxTremorOblique {
  0%, 6% { transform: none; }
  6.0% { transform: translate(-0.04px, 0.09px); }
  12.0% { transform: translate(0.05px, -0.1px); }
  17.51% { transform: translate(-0.07px, 0.14px); }
  22.57% { transform: translate(0.1px, -0.2px); }
  27.22% { transform: translate(-0.14px, 0.28px); }
  30.5% { transform: translate(-0.32px, 0.65px); }
  31.48% { transform: translate(0.18px, -0.37px); }
  31.7% { transform: translate(0.15px, -0.31px); }
  35.4% { transform: translate(-0.23px, 0.47px); }
  39.0% { transform: translate(0.28px, -0.57px); }
  42.31% { transform: translate(-0.33px, 0.68px); }
  45.34% { transform: translate(0.39px, -0.79px); }
  48.13% { transform: translate(-0.44px, 0.89px); }
  48.5% { transform: translate(-0.85px, 1.73px); }
  49.7% { transform: translate(0.4px, -0.82px); }
  50.69% { transform: translate(0.49px, -1.0px); }
  53.04% { transform: translate(-0.54px, 1.1px); }
  55.2% { transform: translate(0.59px, -1.21px); }
  57.18% { transform: translate(-0.64px, 1.3px); }
  59.0% { transform: translate(0.69px, -1.4px); }
  60.5% { transform: translate(-1.37px, 2.8px); }
  60.67% { transform: translate(-0.73px, 1.48px); }
  61.7% { transform: translate(0.65px, -1.33px); }
  62.21% { transform: translate(0.77px, -1.57px); }
  63.62% { transform: translate(-0.81px, 1.65px); }
  64.91% { transform: translate(0.84px, -1.72px); }
  66%, 70.4% { transform: none; }
  70.8% { transform: translate(-2.7px, 5.52px); }
  72.2% { transform: translate(1.92px, -3.91px); }
  73.6% { transform: translate(-1.29px, 2.63px); }
  75.2% { transform: translate(0.83px, -1.7px); }
  77.0% { transform: translate(-0.52px, 1.06px); }
  79.2% { transform: translate(0.3px, -0.61px); }
  81.6% { transform: translate(-0.16px, 0.32px); }
  84.0% { transform: translate(0.07px, -0.14px); }
  86%, 100% { transform: none; }
}

@keyframes fxReverseHanging {
  0% { transform: translate(9.05px, 15.0px); opacity: 1; animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45); }
  30% { transform: translate(9.0px, 14.93px); opacity: 1; animation-timing-function: cubic-bezier(0, 0.9, 0.35, 1); }
  31% { transform: translate(8.78px, 14.55px); opacity: 1; animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45); }
  48% { transform: translate(8.73px, 14.47px); opacity: 1; animation-timing-function: cubic-bezier(0, 0.9, 0.35, 1); }
  49% { transform: translate(8.42px, 13.95px); opacity: 1; animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45); }
  60% { transform: translate(8.37px, 13.88px); opacity: 1; animation-timing-function: cubic-bezier(0, 0.9, 0.35, 1); }
  61% { transform: translate(7.96px, 13.2px); opacity: 1; animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45); }
  66% { transform: translate(7.92px, 13.12px); opacity: 1; }
  70% { transform: translate(7.92px, 13.12px); opacity: 1; animation-timing-function: cubic-bezier(0, 0.85, 0.25, 1); }
  72.5% { transform: translate(-0.77px, -1.28px); opacity: 1; }
  75% { transform: translate(0.23px, 0.38px); opacity: 1; }
  77%, 95% { transform: none; opacity: 1; }
  97% { transform: none; opacity: 0.25; }
  98% { transform: translate(9.05px, 15.0px); opacity: 0.25; }
  100% { transform: translate(9.05px, 15.0px); opacity: 1; }
}
@keyframes fxReverseHangingHero {
  0% { transform: translate(18.1px, 30.0px); opacity: 1; animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45); }
  30% { transform: translate(18.01px, 29.85px); opacity: 1; animation-timing-function: cubic-bezier(0, 0.9, 0.35, 1); }
  31% { transform: translate(17.56px, 29.1px); opacity: 1; animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45); }
  48% { transform: translate(17.47px, 28.95px); opacity: 1; animation-timing-function: cubic-bezier(0, 0.9, 0.35, 1); }
  49% { transform: translate(16.83px, 27.9px); opacity: 1; animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45); }
  60% { transform: translate(16.74px, 27.75px); opacity: 1; animation-timing-function: cubic-bezier(0, 0.9, 0.35, 1); }
  61% { transform: translate(15.93px, 26.4px); opacity: 1; animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45); }
  66% { transform: translate(15.84px, 26.25px); opacity: 1; }
  70% { transform: translate(15.84px, 26.25px); opacity: 1; animation-timing-function: cubic-bezier(0, 0.85, 0.25, 1); }
  72.5% { transform: translate(-1.54px, -2.55px); opacity: 1; }
  75% { transform: translate(0.45px, 0.75px); opacity: 1; }
  77%, 95% { transform: none; opacity: 1; }
  97% { transform: none; opacity: 0.25; }
  98% { transform: translate(18.1px, 30.0px); opacity: 0.25; }
  100% { transform: translate(18.1px, 30.0px); opacity: 1; }
}

@keyframes fxFootRest {
  0%, 95% { opacity: 1; }
  97%, 98% { opacity: 0.25; }
  100% { opacity: 1; }
}

@keyframes fxArrowReverseHanging {
  0% { transform: none; opacity: 0.25; animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45); }
  40% { transform: translate(-2.59px, -4.57px); opacity: 0.7; }
  66%, 70% { transform: translate(-4.7px, -8.3px); opacity: 1; animation-timing-function: cubic-bezier(0, 0.85, 0.25, 1); }
  74% { transform: translate(-2.35px, -4.15px); opacity: 1; }
  80% { transform: none; opacity: 0.7; }
  95% { transform: none; opacity: 0.35; }
  100% { transform: none; opacity: 0.25; }
}

@keyframes fxArrowReverseFoot {
  0% { transform: none; opacity: 0.25; animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45); }
  40% { transform: translateX(5.5px); opacity: 0.7; }
  66%, 70% { transform: translateX(10px); opacity: 1; animation-timing-function: cubic-bezier(0, 0.85, 0.25, 1); }
  74% { transform: translateX(5.0px); opacity: 1; }
  80% { transform: none; opacity: 0.7; }
  95% { transform: none; opacity: 0.35; }
  100% { transform: none; opacity: 0.25; }
}

@keyframes fxShearReverse {
  0%, 8% { stroke-dashoffset: -0.0; opacity: 0; }
  24% { stroke-dashoffset: -0.03; opacity: 0.16; }
  30% { stroke-dashoffset: -0.05; opacity: 0.24; animation-timing-function: cubic-bezier(0, 0.9, 0.35, 1); }
  31% { stroke-dashoffset: -0.1; opacity: 0.13; }
  48% { stroke-dashoffset: -0.15; opacity: 0.34; animation-timing-function: cubic-bezier(0, 0.9, 0.35, 1); }
  49% { stroke-dashoffset: -0.21; opacity: 0.2; }
  60% { stroke-dashoffset: -0.28; opacity: 0.46; animation-timing-function: cubic-bezier(0, 0.9, 0.35, 1); }
  61% { stroke-dashoffset: -0.35; opacity: 0.3; }
  66%, 70% { stroke-dashoffset: -0.46; opacity: 0.58; }
  74% { stroke-dashoffset: -1.2; opacity: 0.3; }
  80%, 100% { stroke-dashoffset: -1.2; opacity: 0; }
}

@keyframes fxNormalHanging {
  0% { transform: translate(-9.05px, -15.0px); opacity: 1; animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45); }
  30% { transform: translate(-9.0px, -14.93px); opacity: 1; animation-timing-function: cubic-bezier(0, 0.9, 0.35, 1); }
  31% { transform: translate(-8.78px, -14.55px); opacity: 1; animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45); }
  48% { transform: translate(-8.73px, -14.47px); opacity: 1; animation-timing-function: cubic-bezier(0, 0.9, 0.35, 1); }
  49% { transform: translate(-8.42px, -13.95px); opacity: 1; animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45); }
  60% { transform: translate(-8.37px, -13.88px); opacity: 1; animation-timing-function: cubic-bezier(0, 0.9, 0.35, 1); }
  61% { transform: translate(-7.96px, -13.2px); opacity: 1; animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45); }
  66% { transform: translate(-7.92px, -13.12px); opacity: 1; }
  70% { transform: translate(-7.92px, -13.12px); opacity: 1; animation-timing-function: cubic-bezier(0, 0.85, 0.25, 1); }
  72.5% { transform: translate(0.77px, 1.28px); opacity: 1; }
  75% { transform: translate(-0.23px, -0.38px); opacity: 1; }
  77%, 95% { transform: none; opacity: 1; }
  97% { transform: none; opacity: 0.25; }
  98% { transform: translate(-9.05px, -15.0px); opacity: 0.25; }
  100% { transform: translate(-9.05px, -15.0px); opacity: 1; }
}
@keyframes fxNormalHangingHero {
  0% { transform: translate(-18.1px, -30.0px); opacity: 1; animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45); }
  30% { transform: translate(-18.01px, -29.85px); opacity: 1; animation-timing-function: cubic-bezier(0, 0.9, 0.35, 1); }
  31% { transform: translate(-17.56px, -29.1px); opacity: 1; animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45); }
  48% { transform: translate(-17.47px, -28.95px); opacity: 1; animation-timing-function: cubic-bezier(0, 0.9, 0.35, 1); }
  49% { transform: translate(-16.83px, -27.9px); opacity: 1; animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45); }
  60% { transform: translate(-16.74px, -27.75px); opacity: 1; animation-timing-function: cubic-bezier(0, 0.9, 0.35, 1); }
  61% { transform: translate(-15.93px, -26.4px); opacity: 1; animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45); }
  66% { transform: translate(-15.84px, -26.25px); opacity: 1; }
  70% { transform: translate(-15.84px, -26.25px); opacity: 1; animation-timing-function: cubic-bezier(0, 0.85, 0.25, 1); }
  72.5% { transform: translate(1.54px, 2.55px); opacity: 1; }
  75% { transform: translate(-0.45px, -0.75px); opacity: 1; }
  77%, 95% { transform: none; opacity: 1; }
  97% { transform: none; opacity: 0.25; }
  98% { transform: translate(-18.1px, -30.0px); opacity: 0.25; }
  100% { transform: translate(-18.1px, -30.0px); opacity: 1; }
}


@keyframes fxArrowNormalHanging {
  0% { transform: none; opacity: 0.25; animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45); }
  40% { transform: translate(2.7px, 4.46px); opacity: 0.7; }
  66%, 70% { transform: translate(4.9px, 8.1px); opacity: 1; animation-timing-function: cubic-bezier(0, 0.85, 0.25, 1); }
  74% { transform: translate(2.45px, 4.05px); opacity: 1; }
  80% { transform: none; opacity: 0.7; }
  95% { transform: none; opacity: 0.35; }
  100% { transform: none; opacity: 0.25; }
}

@keyframes fxArrowNormalFoot {
  0% { transform: none; opacity: 0.25; animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45); }
  40% { transform: translateX(-5.5px); opacity: 0.7; }
  66%, 70% { transform: translateX(-10px); opacity: 1; animation-timing-function: cubic-bezier(0, 0.85, 0.25, 1); }
  74% { transform: translateX(-5.0px); opacity: 1; }
  80% { transform: none; opacity: 0.7; }
  95% { transform: none; opacity: 0.35; }
  100% { transform: none; opacity: 0.25; }
}

@keyframes fxShearNormal {
  0%, 8% { stroke-dashoffset: 0.0; opacity: 0; }
  24% { stroke-dashoffset: 0.03; opacity: 0.16; }
  30% { stroke-dashoffset: 0.05; opacity: 0.24; animation-timing-function: cubic-bezier(0, 0.9, 0.35, 1); }
  31% { stroke-dashoffset: 0.1; opacity: 0.13; }
  48% { stroke-dashoffset: 0.15; opacity: 0.34; animation-timing-function: cubic-bezier(0, 0.9, 0.35, 1); }
  49% { stroke-dashoffset: 0.21; opacity: 0.2; }
  60% { stroke-dashoffset: 0.28; opacity: 0.46; animation-timing-function: cubic-bezier(0, 0.9, 0.35, 1); }
  61% { stroke-dashoffset: 0.35; opacity: 0.3; }
  66%, 70% { stroke-dashoffset: 0.46; opacity: 0.58; }
  74% { stroke-dashoffset: 1.2; opacity: 0.3; }
  80%, 100% { stroke-dashoffset: 1.2; opacity: 0; }
}

@keyframes fxStrikeSlipHanging {
  0% { transform: translateX(-15.0px); opacity: 1; animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45); }
  30% { transform: translateX(-14.93px); opacity: 1; animation-timing-function: cubic-bezier(0, 0.9, 0.35, 1); }
  31% { transform: translateX(-14.55px); opacity: 1; animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45); }
  48% { transform: translateX(-14.47px); opacity: 1; animation-timing-function: cubic-bezier(0, 0.9, 0.35, 1); }
  49% { transform: translateX(-13.95px); opacity: 1; animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45); }
  60% { transform: translateX(-13.88px); opacity: 1; animation-timing-function: cubic-bezier(0, 0.9, 0.35, 1); }
  61% { transform: translateX(-13.2px); opacity: 1; animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45); }
  66% { transform: translateX(-13.12px); opacity: 1; }
  70% { transform: translateX(-13.12px); opacity: 1; animation-timing-function: cubic-bezier(0, 0.85, 0.25, 1); }
  72.5% { transform: translateX(1.28px); opacity: 1; }
  75% { transform: translateX(-0.38px); opacity: 1; }
  77%, 95% { transform: none; opacity: 1; }
  97% { transform: none; opacity: 0.25; }
  98% { transform: translateX(-15px); opacity: 0.25; }
  100% { transform: translateX(-15px); opacity: 1; }
}

@keyframes fxStrikeSlipHangingHero {
  0% { transform: translateX(-30.0px); opacity: 1; animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45); }
  30% { transform: translateX(-29.85px); opacity: 1; animation-timing-function: cubic-bezier(0, 0.9, 0.35, 1); }
  31% { transform: translateX(-29.1px); opacity: 1; animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45); }
  48% { transform: translateX(-28.95px); opacity: 1; animation-timing-function: cubic-bezier(0, 0.9, 0.35, 1); }
  49% { transform: translateX(-27.9px); opacity: 1; animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45); }
  60% { transform: translateX(-27.75px); opacity: 1; animation-timing-function: cubic-bezier(0, 0.9, 0.35, 1); }
  61% { transform: translateX(-26.4px); opacity: 1; animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45); }
  66% { transform: translateX(-26.25px); opacity: 1; }
  70% { transform: translateX(-26.25px); opacity: 1; animation-timing-function: cubic-bezier(0, 0.85, 0.25, 1); }
  72.5% { transform: translateX(2.55px); opacity: 1; }
  75% { transform: translateX(-0.75px); opacity: 1; }
  77%, 95% { transform: none; opacity: 1; }
  97% { transform: none; opacity: 0.25; }
  98% { transform: translateX(-30px); opacity: 0.25; }
  100% { transform: translateX(-30px); opacity: 1; }
}
@keyframes fxStrikeSlipFoot {
  0% { transform: translateX(15.0px); opacity: 1; animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45); }
  30% { transform: translateX(14.93px); opacity: 1; animation-timing-function: cubic-bezier(0, 0.9, 0.35, 1); }
  31% { transform: translateX(14.55px); opacity: 1; animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45); }
  48% { transform: translateX(14.47px); opacity: 1; animation-timing-function: cubic-bezier(0, 0.9, 0.35, 1); }
  49% { transform: translateX(13.95px); opacity: 1; animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45); }
  60% { transform: translateX(13.88px); opacity: 1; animation-timing-function: cubic-bezier(0, 0.9, 0.35, 1); }
  61% { transform: translateX(13.2px); opacity: 1; animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45); }
  66% { transform: translateX(13.12px); opacity: 1; }
  70% { transform: translateX(13.12px); opacity: 1; animation-timing-function: cubic-bezier(0, 0.85, 0.25, 1); }
  72.5% { transform: translateX(-1.28px); opacity: 1; }
  75% { transform: translateX(0.38px); opacity: 1; }
  77%, 95% { transform: none; opacity: 1; }
  97% { transform: none; opacity: 0.25; }
  98% { transform: translateX(15px); opacity: 0.25; }
  100% { transform: translateX(15px); opacity: 1; }
}
@keyframes fxStrikeSlipFootHero {
  0% { transform: translateX(30.0px); opacity: 1; animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45); }
  30% { transform: translateX(29.85px); opacity: 1; animation-timing-function: cubic-bezier(0, 0.9, 0.35, 1); }
  31% { transform: translateX(29.1px); opacity: 1; animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45); }
  48% { transform: translateX(28.95px); opacity: 1; animation-timing-function: cubic-bezier(0, 0.9, 0.35, 1); }
  49% { transform: translateX(27.9px); opacity: 1; animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45); }
  60% { transform: translateX(27.75px); opacity: 1; animation-timing-function: cubic-bezier(0, 0.9, 0.35, 1); }
  61% { transform: translateX(26.4px); opacity: 1; animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45); }
  66% { transform: translateX(26.25px); opacity: 1; }
  70% { transform: translateX(26.25px); opacity: 1; animation-timing-function: cubic-bezier(0, 0.85, 0.25, 1); }
  72.5% { transform: translateX(-2.55px); opacity: 1; }
  75% { transform: translateX(0.75px); opacity: 1; }
  77%, 95% { transform: none; opacity: 1; }
  97% { transform: none; opacity: 0.25; }
  98% { transform: translateX(30px); opacity: 0.25; }
  100% { transform: translateX(30px); opacity: 1; }
}



@keyframes fxArrowStrikeSlipHanging {
  0% { transform: none; opacity: 0.25; animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45); }
  40% { transform: translateX(6.05px); opacity: 0.7; }
  66%, 70% { transform: translateX(11px); opacity: 1; animation-timing-function: cubic-bezier(0, 0.85, 0.25, 1); }
  74% { transform: translateX(5.5px); opacity: 1; }
  80% { transform: none; opacity: 0.7; }
  95% { transform: none; opacity: 0.35; }
  100% { transform: none; opacity: 0.25; }
}

@keyframes fxArrowStrikeSlipFoot {
  0% { transform: none; opacity: 0.25; animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45); }
  40% { transform: translateX(-6.05px); opacity: 0.7; }
  66%, 70% { transform: translateX(-11px); opacity: 1; animation-timing-function: cubic-bezier(0, 0.85, 0.25, 1); }
  74% { transform: translateX(-5.5px); opacity: 1; }
  80% { transform: none; opacity: 0.7; }
  95% { transform: none; opacity: 0.35; }
  100% { transform: none; opacity: 0.25; }
}

@keyframes fxShearStrikeSlip {
  0%, 8% { stroke-dashoffset: 0; opacity: 0; }
  24% { stroke-dashoffset: -0.03; opacity: 0.16; }
  30% { stroke-dashoffset: -0.05; opacity: 0.24; animation-timing-function: cubic-bezier(0, 0.9, 0.35, 1); }
  31% { stroke-dashoffset: -0.1; opacity: 0.13; }
  48% { stroke-dashoffset: -0.15; opacity: 0.34; animation-timing-function: cubic-bezier(0, 0.9, 0.35, 1); }
  49% { stroke-dashoffset: -0.21; opacity: 0.2; }
  60% { stroke-dashoffset: -0.28; opacity: 0.46; animation-timing-function: cubic-bezier(0, 0.9, 0.35, 1); }
  61% { stroke-dashoffset: -0.35; opacity: 0.3; }
  66%, 70% { stroke-dashoffset: -0.46; opacity: 0.58; }
  74% { stroke-dashoffset: -1.2; opacity: 0.3; }
  80%, 100% { stroke-dashoffset: -1.2; opacity: 0; }
}

@keyframes fxObliqueFoot {
  0% { transform: translate(-11.66px, 13.19px); opacity: 1; animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45); }
  30% { transform: translate(-11.61px, 13.12px); opacity: 1; animation-timing-function: cubic-bezier(0, 0.9, 0.35, 1); }
  31% { transform: translate(-11.31px, 12.79px); opacity: 1; animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45); }
  48% { transform: translate(-11.25px, 12.72px); opacity: 1; animation-timing-function: cubic-bezier(0, 0.9, 0.35, 1); }
  49% { transform: translate(-10.84px, 12.26px); opacity: 1; animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45); }
  60% { transform: translate(-10.79px, 12.20px); opacity: 1; animation-timing-function: cubic-bezier(0, 0.9, 0.35, 1); }
  61% { transform: translate(-10.26px, 11.60px); opacity: 1; animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45); }
  66% { transform: translate(-10.20px, 11.53px); opacity: 1; }
  70% { transform: translate(-10.20px, 11.53px); opacity: 1; animation-timing-function: cubic-bezier(0, 0.85, 0.25, 1); }
  72.5% { transform: translate(1.00px, -1.13px); opacity: 1; }
  75% { transform: translate(-0.30px, 0.33px); opacity: 1; }
  77%, 95% { transform: none; opacity: 1; }
  97% { transform: none; opacity: 0.25; }
  98% { transform: translate(-11.66px, 13.19px); opacity: 0.25; }
  100% { transform: translate(-11.66px, 13.19px); opacity: 1; }
}

@keyframes fxObliqueFootHero {
  0% { transform: translate(-18.96px, 21.44px); opacity: 1; animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45); }
  30% { transform: translate(-18.87px, 21.34px); opacity: 1; animation-timing-function: cubic-bezier(0, 0.9, 0.35, 1); }
  31% { transform: translate(-18.39px, 20.80px); opacity: 1; animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45); }
  48% { transform: translate(-18.29px, 20.68px); opacity: 1; animation-timing-function: cubic-bezier(0, 0.9, 0.35, 1); }
  49% { transform: translate(-17.63px, 19.94px); opacity: 1; animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45); }
  60% { transform: translate(-17.55px, 19.84px); opacity: 1; animation-timing-function: cubic-bezier(0, 0.9, 0.35, 1); }
  61% { transform: translate(-16.69px, 18.87px); opacity: 1; animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45); }
  66% { transform: translate(-16.58px, 18.75px); opacity: 1; }
  70% { transform: translate(-16.58px, 18.75px); opacity: 1; animation-timing-function: cubic-bezier(0, 0.85, 0.25, 1); }
  72.5% { transform: translate(1.62px, -1.83px); opacity: 1; }
  75% { transform: translate(-0.48px, 0.54px); opacity: 1; }
  77%, 95% { transform: none; opacity: 1; }
  97% { transform: none; opacity: 0.25; }
  98% { transform: translate(-18.96px, 21.44px); opacity: 0.25; }
  100% { transform: translate(-18.96px, 21.44px); opacity: 1; }
}

@keyframes fxObliqueHanging {
  0% { transform: translate(11.66px, -13.19px); opacity: 1; animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45); }
  30% { transform: translate(11.61px, -13.12px); opacity: 1; animation-timing-function: cubic-bezier(0, 0.9, 0.35, 1); }
  31% { transform: translate(11.31px, -12.79px); opacity: 1; animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45); }
  48% { transform: translate(11.25px, -12.72px); opacity: 1; animation-timing-function: cubic-bezier(0, 0.9, 0.35, 1); }
  49% { transform: translate(10.84px, -12.26px); opacity: 1; animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45); }
  60% { transform: translate(10.79px, -12.20px); opacity: 1; animation-timing-function: cubic-bezier(0, 0.9, 0.35, 1); }
  61% { transform: translate(10.26px, -11.60px); opacity: 1; animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45); }
  66% { transform: translate(10.20px, -11.53px); opacity: 1; }
  70% { transform: translate(10.20px, -11.53px); opacity: 1; animation-timing-function: cubic-bezier(0, 0.85, 0.25, 1); }
  72.5% { transform: translate(-1.00px, 1.13px); opacity: 1; }
  75% { transform: translate(0.30px, -0.33px); opacity: 1; }
  77%, 95% { transform: none; opacity: 1; }
  97% { transform: none; opacity: 0.25; }
  98% { transform: translate(11.66px, -13.19px); opacity: 0.25; }
  100% { transform: translate(11.66px, -13.19px); opacity: 1; }
}
@keyframes fxObliqueHangingHero {
  0% { transform: translate(18.96px, -21.44px); opacity: 1; animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45); }
  30% { transform: translate(18.87px, -21.34px); opacity: 1; animation-timing-function: cubic-bezier(0, 0.9, 0.35, 1); }
  31% { transform: translate(18.39px, -20.80px); opacity: 1; animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45); }
  48% { transform: translate(18.29px, -20.68px); opacity: 1; animation-timing-function: cubic-bezier(0, 0.9, 0.35, 1); }
  49% { transform: translate(17.63px, -19.94px); opacity: 1; animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45); }
  60% { transform: translate(17.55px, -19.84px); opacity: 1; animation-timing-function: cubic-bezier(0, 0.9, 0.35, 1); }
  61% { transform: translate(16.69px, -18.87px); opacity: 1; animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45); }
  66% { transform: translate(16.58px, -18.75px); opacity: 1; }
  70% { transform: translate(16.58px, -18.75px); opacity: 1; animation-timing-function: cubic-bezier(0, 0.85, 0.25, 1); }
  72.5% { transform: translate(-1.62px, 1.83px); opacity: 1; }
  75% { transform: translate(0.48px, -0.54px); opacity: 1; }
  77%, 95% { transform: none; opacity: 1; }
  97% { transform: none; opacity: 0.25; }
  98% { transform: translate(18.96px, -21.44px); opacity: 0.25; }
  100% { transform: translate(18.96px, -21.44px); opacity: 1; }
}


@keyframes fxArrowObliqueHanging {
  0% { transform: none; opacity: 0.25; animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45); }
  40% { transform: translate(2.2px, -4.51px); opacity: 0.7; }
  66%, 70% { transform: translate(4.0px, -8.2px); opacity: 1; animation-timing-function: cubic-bezier(0, 0.85, 0.25, 1); }
  74% { transform: translate(2.0px, -4.1px); opacity: 1; }
  80% { transform: none; opacity: 0.7; }
  95% { transform: none; opacity: 0.35; }
  100% { transform: none; opacity: 0.25; }
}

@keyframes fxArrowObliqueFoot {
  0% { transform: none; opacity: 0.25; animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45); }
  40% { transform: translateX(4.95px); opacity: 0.7; }
  66%, 70% { transform: translateX(9px); opacity: 1; animation-timing-function: cubic-bezier(0, 0.85, 0.25, 1); }
  74% { transform: translateX(4.5px); opacity: 1; }
  80% { transform: none; opacity: 0.7; }
  95% { transform: none; opacity: 0.35; }
  100% { transform: none; opacity: 0.25; }
}

@keyframes fxShearOblique {
  0%, 8% { stroke-dashoffset: -0.0; opacity: 0; }
  24% { stroke-dashoffset: -0.03; opacity: 0.16; }
  30% { stroke-dashoffset: -0.05; opacity: 0.24; animation-timing-function: cubic-bezier(0, 0.9, 0.35, 1); }
  31% { stroke-dashoffset: -0.1; opacity: 0.13; }
  48% { stroke-dashoffset: -0.15; opacity: 0.34; animation-timing-function: cubic-bezier(0, 0.9, 0.35, 1); }
  49% { stroke-dashoffset: -0.21; opacity: 0.2; }
  60% { stroke-dashoffset: -0.28; opacity: 0.46; animation-timing-function: cubic-bezier(0, 0.9, 0.35, 1); }
  61% { stroke-dashoffset: -0.35; opacity: 0.3; }
  66%, 70% { stroke-dashoffset: -0.46; opacity: 0.58; }
  74% { stroke-dashoffset: -1.2; opacity: 0.3; }
  80%, 100% { stroke-dashoffset: -1.2; opacity: 0; }
}

/* Stress concentrating on the contact. A wide, soft stroke over the fault that
   builds with the load, dips a little each time the fault gives some back, and
   clears when it is finally released. No filter — a broad low-opacity line,
   because a blur on eight diagrams is not worth the paint. */
.fx__stress {
  fill: none;
  stroke: var(--fault-motion);
  stroke-width: 8;
  stroke-linecap: round;
  stroke-linejoin: round;
  opacity: 0;
}
@keyframes fxStress {
  0%, 8% { opacity: 0; }
  30% { opacity: 0.1; }
  31% { opacity: 0.06; }
  48% { opacity: 0.18; }
  49% { opacity: 0.12; }
  60% { opacity: 0.24; }
  61% { opacity: 0.17; }
  66%, 70% { opacity: 0.3; }
  73% { opacity: 0.38; }
  84%, 100% { opacity: 0; }
}

/* The two sides gripping. Fine ticks along the fault, crawling in the direction
   that side is trying to move. */
.fx__shear {
  fill: none;
  stroke: color-mix(in oklab, var(--fault-motion) 70%, #fff);
  stroke-width: 3.4;
  stroke-dasharray: 0.022 0.045;
  stroke-linecap: butt;
  opacity: 0;
}

/* The fault brightens as failure approaches and ends where it started, so the
   resting drawing is unchanged. */
@keyframes fxFaultLoad {
  0% { opacity: 0.6; }
  66%, 74% { opacity: 1; }
  100% { opacity: 1; }
}

/* Where the fault is about to break. Grows through the whole crescendo and
   pulses at each micro-slip, so it is a countdown of load rather than a
   decoration, and is gone before the rest phase. */
.fx__nucleus {
  fill: color-mix(in oklab, var(--fault-motion) 78%, #fff);
  stroke: none;
  opacity: 0;
  transform-box: fill-box;
  transform-origin: center;
}
@keyframes fxNucleus {
  0%, 8% { opacity: 0; transform: scale(0.5); }
  30% { opacity: 0.2; transform: scale(0.9); }
  31% { opacity: 0.1; transform: scale(0.8); }
  48% { opacity: 0.34; transform: scale(1); }
  49% { opacity: 0.2; transform: scale(0.9); }
  60% { opacity: 0.46; transform: scale(1.1); }
  61% { opacity: 0.3; transform: scale(1); }
  66% { opacity: 0.54; transform: scale(1.18); }
  70% { opacity: 0.64; transform: scale(1.22); animation-timing-function: cubic-bezier(0, 0.85, 0.25, 1); }
  71.2% { opacity: 1; transform: scale(1.75); }
  84%, 100% { opacity: 0; transform: scale(2.2); }
}

/* The rupture, travelling the fault it actually broke, at the moment it breaks.
   `pathLength="1"` normalises the geometry so the same keyframes work on a 54px
   thrust and a 284px strike-slip trace. */
.fx__rupture {
  fill: none;
  stroke: color-mix(in oklab, var(--fault-motion) 82%, #fff);
  stroke-width: 2.6;
  stroke-linecap: round;
  stroke-dasharray: 0.18 0.82;
  stroke-dashoffset: 1;
  opacity: 0;
}
@keyframes fxRupture {
  0%, 69.4% { opacity: 0; stroke-dashoffset: 1; }
  70.2% { opacity: 1; animation-timing-function: cubic-bezier(0, 0.85, 0.25, 1); }
  76% { opacity: 1; stroke-dashoffset: 0.02; }
  84%, 100% { opacity: 0; stroke-dashoffset: 0.02; }
}

/* Energy leaving the rupture. Two faint arcs expanding outward into the crust —
   outward, because that is where the waves go, and not along the fault. */
.fx__energy {
  fill: none;
  stroke: var(--fault-motion);
  stroke-width: 1;
  opacity: 0;
  transform-box: fill-box;
  transform-origin: center;
}
@keyframes fxEnergy {
  0%, 71.4% { transform: scale(0.3); opacity: 0; animation-timing-function: cubic-bezier(0, 0.85, 0.25, 1); }
  74% { opacity: 0.42; }
  92%, 100% { transform: scale(4.1); opacity: 0; }
}
@keyframes fxEnergyTrail {
  0%, 75% { transform: scale(0.3); opacity: 0; }
  80% { opacity: 0.22; }
  95%, 100% { transform: scale(3.9); opacity: 0; }
}

/* One clock for the whole figure, long enough that a 70% build is a build. */
.fx--motion.is-playing .fx__block,
.fx--motion.is-playing .fx__strata,
.fx--motion.is-playing .fx__arrow,
.fx--motion.is-playing .fx__fault,
.fx--motion.is-playing .fx__stress,
.fx--motion.is-playing .fx__shear,
.fx--motion.is-playing .fx__rupture,
.fx--motion.is-playing .fx__energy,
.fx--motion.is-playing .fx__nucleus,
.fx--motion.is-playing .fx__face,
.fx--motion.is-playing .fx__seam,
.fx--motion.is-playing .fx__brow,
.fx--motion.is-playing .fx__plates {
  animation-duration: 5600ms;
  animation-timing-function: linear;
  animation-iteration-count: 3;
}
.fx--hero.fx--motion.is-playing[data-style="reverse"] .fx__block:not([data-side]),
.fx--hero.fx--motion.is-playing[data-style="reverse"] .fx__strata:not([data-side]) {
  animation-name: fxReverseHangingHero;
}
.fx--hero.fx--motion.is-playing[data-style="normal"] .fx__block:not([data-side]),
.fx--hero.fx--motion.is-playing[data-style="normal"] .fx__strata:not([data-side]) {
  animation-name: fxNormalHangingHero;
}
.fx--hero.fx--motion.is-playing[data-style="strikeSlip"] .fx__block:not([data-side]),
.fx--hero.fx--motion.is-playing[data-style="strikeSlip"] .fx__strata:not([data-side]),
.fx--hero.fx--motion.is-playing[data-style="strikeSlip"] .fx__face:not([data-side]),
.fx--hero.fx--motion.is-playing[data-style="strikeSlip"] .fx__seam:not([data-side]),
.fx--hero.fx--motion.is-playing[data-style="strikeSlip"] .fx__brow:not([data-side]) {
  animation-name: fxStrikeSlipHangingHero;
}
.fx--hero.fx--motion.is-playing[data-style="oblique"] .fx__block[data-side="foot"],
.fx--hero.fx--motion.is-playing[data-style="oblique"] .fx__strata[data-side="foot"],
.fx--hero.fx--motion.is-playing[data-style="oblique"] .fx__face[data-side="foot"],
.fx--hero.fx--motion.is-playing[data-style="oblique"] .fx__brow[data-side="foot"] {
  animation-name: fxObliqueFootHero;
}
.fx--hero.fx--motion.is-playing[data-style="oblique"] .fx__block:not([data-side]),
.fx--hero.fx--motion.is-playing[data-style="oblique"] .fx__strata:not([data-side]),
.fx--hero.fx--motion.is-playing[data-style="oblique"] .fx__face:not([data-side]),
.fx--hero.fx--motion.is-playing[data-style="oblique"] .fx__brow:not([data-side]) {
  animation-name: fxObliqueHangingHero;
}
.fx--motion.is-playing .fx__rupture { animation-name: fxRupture; }
.fx--motion.is-playing .fx__nucleus { animation-name: fxNucleus; }
.fx--motion.is-playing /* The cut face of the crust, and the layering in it. Static: it belongs to the
   footwall, which never moves, and it is what makes the map view read as the top
   of a solid block rather than a sheet of paper. */
.fx__face {
  /* Darker than the ground it hangs from, so it recedes into shadow instead of
     advancing as a brighter stripe. */
  fill: rgba(6, 11, 20, 0.62);
  stroke: rgba(150, 180, 210, 0.22);
  stroke-width: 1;
}
/* The silhouette edge where the ground turns down into the cut face. It is the
   strongest depth cue in the figure, so it is the brightest line that is not
   the fault. */
.fx__brow {
  fill: none;
  stroke: rgba(170, 198, 226, 0.5);
  stroke-width: 1.4;
  stroke-linecap: round;
}
/* The oblique figure is the only one where two solids overlap in screen space,
   so a near face has to hide a far one. Opaque fills, scoped to it — the other
   three diagrams keep the shared translucent treatment. */
.fx[data-style="oblique"] .fx__block { fill: #2f4459; }
.fx[data-style="oblique"] .fx__face { fill: #151f2c; }
/* The cut laid bare by the slip. It is a rock surface, not a hole, and it is the
   single clearest evidence of how far the block travelled. */
.fx[data-style="oblique"] .fx__face[data-scarp="true"] {
  fill: #44586f;
  stroke: rgba(200, 222, 240, 0.7);
}
/* A long horizontal trace turns the shared 8px stress stroke into a painted bar. */
.fx[data-style="oblique"] .fx__stress { stroke-width: 5; }
.fx[data-style="oblique"] .fx__rupture { stroke-width: 2; }

.fx__seam {
  fill: none;
  stroke: rgba(150, 180, 210, 0.16);
  stroke-width: 1;
}
.fx__fault { animation-name: fxFaultLoad; }
.fx--motion.is-playing .fx__stress { animation-name: fxStress; }
.fx--motion.is-playing .fx__energy {
  animation-name: fxEnergy;
  animation-timing-function: ease-out;
}
.fx--motion.is-playing .fx__energy[data-wave="2"] {
  animation-name: fxEnergyTrail;
  animation-timing-function: ease-out;
}

.fx--motion.is-playing[data-style="reverse"] .fx__block:not([data-side]),
.fx--motion.is-playing[data-style="reverse"] .fx__strata:not([data-side]) {
  animation-name: fxReverseHanging;
}
.fx--motion.is-playing[data-style="reverse"] .fx__block[data-side="foot"],
.fx--motion.is-playing[data-style="reverse"] .fx__strata[data-side="foot"] {
  animation-name: fxFootRest;
}
.fx--motion.is-playing[data-style="reverse"] .fx__arrow:not([data-side]) {
  animation-name: fxArrowReverseHanging;
}
.fx--motion.is-playing[data-style="reverse"] .fx__arrow[data-side="foot"] {
  animation-name: fxArrowReverseFoot;
}
.fx--motion.is-playing[data-style="reverse"] .fx__shear {
  animation-name: fxShearReverse;
}
.fx--motion.is-playing[data-style="reverse"] .fx__plates {
  animation-name: fxTremorReverse;
}

.fx--motion.is-playing[data-style="normal"] .fx__block:not([data-side]),
.fx--motion.is-playing[data-style="normal"] .fx__strata:not([data-side]) {
  animation-name: fxNormalHanging;
}
.fx--motion.is-playing[data-style="normal"] .fx__block[data-side="foot"],
.fx--motion.is-playing[data-style="normal"] .fx__strata[data-side="foot"] {
  animation-name: fxFootRest;
}
.fx--motion.is-playing[data-style="normal"] .fx__arrow:not([data-side]) {
  animation-name: fxArrowNormalHanging;
}
.fx--motion.is-playing[data-style="normal"] .fx__arrow[data-side="foot"] {
  animation-name: fxArrowNormalFoot;
}
.fx--motion.is-playing[data-style="normal"] .fx__shear {
  animation-name: fxShearNormal;
}
.fx--motion.is-playing[data-style="normal"] .fx__plates {
  animation-name: fxTremorNormal;
}

.fx--motion.is-playing[data-style="strikeSlip"] .fx__block:not([data-side]),
.fx--motion.is-playing[data-style="strikeSlip"] .fx__strata:not([data-side]),
.fx--motion.is-playing[data-style="strikeSlip"] .fx__face:not([data-side]),
.fx--motion.is-playing[data-style="strikeSlip"] .fx__seam:not([data-side]),
.fx--motion.is-playing[data-style="strikeSlip"] .fx__brow:not([data-side]) {
  animation-name: fxStrikeSlipHanging;
}
.fx--motion.is-playing[data-style="strikeSlip"] .fx__block[data-side="foot"],
.fx--motion.is-playing[data-style="strikeSlip"] .fx__strata[data-side="foot"],
.fx--motion.is-playing[data-style="strikeSlip"] .fx__face[data-side="foot"],
.fx--motion.is-playing[data-style="strikeSlip"] .fx__seam[data-side="foot"],
.fx--motion.is-playing[data-style="strikeSlip"] .fx__brow[data-side="foot"] {
  animation-name: fxStrikeSlipFoot;
}
.fx--hero.fx--motion.is-playing[data-style="strikeSlip"] .fx__block[data-side="foot"],
.fx--hero.fx--motion.is-playing[data-style="strikeSlip"] .fx__strata[data-side="foot"],
.fx--hero.fx--motion.is-playing[data-style="strikeSlip"] .fx__face[data-side="foot"],
.fx--hero.fx--motion.is-playing[data-style="strikeSlip"] .fx__seam[data-side="foot"],
.fx--hero.fx--motion.is-playing[data-style="strikeSlip"] .fx__brow[data-side="foot"] {
  animation-name: fxStrikeSlipFootHero;
}
.fx--motion.is-playing[data-style="strikeSlip"] .fx__arrow:not([data-side]) {
  animation-name: fxArrowStrikeSlipHanging;
}
.fx--motion.is-playing[data-style="strikeSlip"] .fx__arrow[data-side="foot"] {
  animation-name: fxArrowStrikeSlipFoot;
}
.fx--motion.is-playing[data-style="strikeSlip"] .fx__shear {
  animation-name: fxShearStrikeSlip;
}
.fx--motion.is-playing[data-style="strikeSlip"] .fx__plates {
  animation-name: fxTremorStrikeSlip;
}

.fx--motion.is-playing[data-style="oblique"] .fx__block:not([data-side]),
.fx--motion.is-playing[data-style="oblique"] .fx__strata:not([data-side]),
.fx--motion.is-playing[data-style="oblique"] .fx__face:not([data-side]),
.fx--motion.is-playing[data-style="oblique"] .fx__brow:not([data-side]) {
  animation-name: fxObliqueHanging;
}
.fx--motion.is-playing[data-style="oblique"] .fx__block[data-side="foot"],
.fx--motion.is-playing[data-style="oblique"] .fx__strata[data-side="foot"],
.fx--motion.is-playing[data-style="oblique"] .fx__face[data-side="foot"],
.fx--motion.is-playing[data-style="oblique"] .fx__brow[data-side="foot"] {
  animation-name: fxObliqueFoot;
}
.fx--motion.is-playing[data-style="oblique"] .fx__arrow:not([data-side]) {
  animation-name: fxArrowObliqueHanging;
}
.fx--motion.is-playing[data-style="oblique"] .fx__arrow[data-side="foot"] {
  animation-name: fxArrowObliqueFoot;
}
.fx--motion.is-playing[data-style="oblique"] .fx__shear {
  animation-name: fxShearOblique;
}
.fx--motion.is-playing[data-style="oblique"] .fx__plates {
  animation-name: fxTremorOblique;
}

/* The lesson hero loops, because studying the mechanism is why the reader
   opened it. Slower, and it stops the moment the figure leaves the pane. */
.fx--hero.fx--hero.fx--motion.is-playing .fx__block,
.fx--hero.fx--motion.is-playing .fx__strata,
.fx--hero.fx--motion.is-playing .fx__arrow,
.fx--hero.fx--motion.is-playing .fx__fault,
.fx--hero.fx--motion.is-playing .fx__stress,
.fx--hero.fx--motion.is-playing .fx__shear,
.fx--hero.fx--motion.is-playing .fx__rupture,
.fx--hero.fx--motion.is-playing .fx__energy,
.fx--hero.fx--motion.is-playing .fx__nucleus,
.fx--hero.fx--motion.is-playing .fx__face,
.fx--hero.fx--motion.is-playing .fx__seam,
.fx--hero.fx--motion.is-playing .fx__brow,
.fx--hero.fx--motion.is-playing .fx__plates {
  animation-duration: 7600ms;
  animation-iteration-count: infinite;
}

/* A hidden document should cost nothing. Browsers already throttle this, but
   pausing outright is cheap and exact. */
body.is-page-hidden .fx--motion *,
body.is-page-hidden .dx--motion * {
  animation-play-state: paused !important;
}

/* Reduced motion: the static diagram, with no spatial looping at all. Not the
   same animation played faster — the movement itself is what is declined. */
@media (prefers-reduced-motion: reduce) {
  .dx--motion.is-playing .dx__drop,
  .dx--motion.is-playing .dx__dropHead,
  .dx--motion.is-playing .dx__hypoHalo,
  .fx--motion.is-playing .fx__block,
  .fx--motion.is-playing .fx__strata,
  .fx--motion.is-playing .fx__arrow,
  .fx--motion.is-playing .fx__rupture,
  .fx--motion.is-playing .fx__energy,
  .fx--motion.is-playing .fx__nucleus,
  .fx--motion.is-playing .fx__fault,
  .fx--motion.is-playing .fx__stress,
  .fx--motion.is-playing .fx__shear,
  .fx--motion.is-playing .fx__face,
  .fx--motion.is-playing .fx__seam,
  .fx--motion.is-playing .fx__brow,
  .fx--motion.is-playing .fx__svg,
  .fx--motion.is-playing .fx__plates,
  .fx--motion.is-playing [data-side="foot"] {
    animation: none !important;
    stroke-dasharray: none;
  }
  /* The sweep and the pulses exist only as motion, so with motion declined they
     are simply not drawn. */
  .fx__rupture,
  .fx__energy,
  .fx__nucleus,
  .fx__stress,
  .fx__shear {
    display: none;
  }
}

/* ----------------------------------------------------- faulting diagram */

/* Blocks and arrows, not a beachball. The reader is being told what the two
   sides of the fault did; the notation that encodes it properly belongs in the
   deeper view where there is room to teach it. */
.fx {
  margin: 0;
}
.fx__svg {
  width: 100%;
  height: auto;
}
.fx__block {
  fill: rgba(150, 180, 210, 0.09);
  stroke: rgba(150, 180, 210, 0.28);
  stroke-width: 1;
}
.fx__block[data-side="foot"] {
  fill: rgba(150, 180, 210, 0.045);
}
.fx__fault {
  fill: none;
  stroke: var(--fault-motion);
  stroke-width: 1.5;
  stroke-linejoin: round;
  stroke-linecap: round;
}
.fx__arrow {
  fill: none;
  stroke: var(--fault-motion);
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* Bedding inside the blocks. Two thin lines are enough to read as ground with
   layers in it, and the offset between the two sides is what makes the throw
   visible without an annotation. */
.fx__strata {
  fill: none;
  stroke: rgba(150, 180, 210, 0.32);
  stroke-width: 1.1;
  stroke-linejoin: round;
  stroke-linecap: round;
}

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

.exposure,
.felt {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 8px;
  margin: 14px 0 6px;
}
.exposure b,
.felt b {
  font-size: 19px;
  font-weight: 500;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}
.exposure span,
.felt span {
  font-size: 12px;
  color: var(--text-dim);
}

/* A proportion, not a meter. The segments sum to the whole exposed population,
   so length reads as "share of people at this intensity" — which is a real
   comparison. A part-filled bar would read as progress towards something. */
.exposure__strip {
  display: flex;
  width: 100%;
  height: 6px;
  border-radius: 1px;
  overflow: hidden;
  /* Visible gaps, so the row reads as separate bands rather than one filled
     track. A distribution and a progress meter are the same rectangle until
     the segmentation is legible. */
  gap: 2px;
}
.exposure__strip i {
  display: block;
  height: 100%;
  min-width: 1px;
}

.estimates {
  margin: 18px 0 0;
  display: grid;
  gap: 16px;
}
.estimates > div {
  display: grid;
  gap: 4px;
}
/* Outcome range first, likelihood under it. Side by side and baseline-aligned,
   "Fewer than 1  95% probability" reads as one broken sentence; stacked, the
   range is the answer and the probability is a qualifier on it. Stacking also
   keeps a long economic range like "$100M – $1B" off a second line at 360px. */
.estimates dd {
  margin: 0;
  display: grid;
  justify-items: start;
  gap: 2px;
}
.estimates dd b {
  font-size: 16px;
  font-weight: 400;
  /* Size carries the value now, not brightness — so it reads as the answer to
     the label above it rather than competing with it. */
  color: var(--text-dim);
  line-height: 1.25;
}
.estimates dd > span {
  font-family: "IBM Plex Mono", monospace;
  font-size: 11px;
  color: var(--text-faint);
}

/* -------------------------------------------------- event information */

.section--source {
  opacity: 0.9;
}
.source {
  margin: 0;
  display: grid;
  gap: 9px;
}
.source > div {
  display: grid;
  grid-template-columns: 96px 1fr;
  gap: 12px;
  align-items: baseline;
}
.source dd {
  margin: 0;
  font-size: 12px;
  color: var(--text-dim);
  min-width: 0;
  overflow-wrap: anywhere;
}
.source dd span {
  display: block;
  margin-top: 2px;
  font-size: 11px;
  color: var(--text-faint);
}

/* ------------------------------------------------- educational subviews */

/* An invitation, not a call to action: a quiet underlined line that sits after
   the visualisation it extends. A filled button here would outrank the value
   the section exists to report. */


/* Names the lesson without repeating the event header — the earthquake is
   already established by the panel the reader came from. */
/* The lesson heading is a new hierarchy level, not a continuation of the
   navigation above it. Sitting tight under the Back control's rule it read as
   part of it; the separation is spacing, not type size. One rule covers Depth,
   Shaking, Faulting and Impact, which all open with this element. */
.detail > .lesson__heading,
.detail > .readout,
.detail > .meaning,
.detail > .figure,
.detail > figure {
  padding-left: 26px;
  padding-right: 26px;
}
.readout--sub b {
  font-size: 17px;
}
.readout--sub span {
  text-transform: none;
  letter-spacing: 0;
  font-size: 12.5px;
  color: var(--text-dim);
}

.dx--hero,
.fx--hero {
  margin: 4px 0 8px;
}
.dx__heroLabel {
  font-family: "IBM Plex Mono", monospace;
  font-size: 9px;
  letter-spacing: 0.13em;
  fill: var(--text-dim);
}
.dx__heroNote {
  font-size: 9px;
  fill: var(--text-faint);
}
.dx__bandRange {
  font-family: "IBM Plex Mono", monospace;
  font-size: 8px;
  letter-spacing: 0.1em;
  fill: var(--text-faint);
}
.dx__epi {
  fill: rgba(226, 238, 250, 0.9);
}
.dx__brace {
  stroke: var(--sev, #7f95ad);
  stroke-width: 1;
  opacity: 0.7;
}
.dx__distance {
  font-family: "IBM Plex Mono", monospace;
  font-size: 11px;
  fill: var(--text);
  paint-order: stroke fill;
  stroke: var(--panel);
  stroke-width: 3px;
  stroke-linejoin: round;
}
.fx__note {
  font-family: "IBM Plex Mono", monospace;
  font-size: 8px;
  letter-spacing: 0.12em;
  fill: var(--text-faint);
}

/* Why depth varies, as a cross-section. See `subductionSection` in app.js for
   why this is geology rather than a scale: the horizontal bar it replaced read
   as a progress meter, and depth now reads one way everywhere — surface at the
   top, downward into the Earth. */
.dx--slab {
  margin: 6px 0 0;
}
.dx__slab {
  fill: rgba(150, 180, 210, 0.16);
}
.dx__crust {
  fill: rgba(150, 180, 210, 0.1);
  stroke: rgba(203, 222, 239, 0.3);
  stroke-width: 1;
  stroke-linejoin: round;
}
/* Seismicity, not this event. Deliberately the same neutral family as the
   diagram rather than the severity colour, which belongs to the one marker
   that really is this earthquake. */
.dx__quake {
  fill: rgba(214, 231, 246, 0.5);
}
.dx__note {
  font-family: "IBM Plex Mono", monospace;
  font-size: 8px;
  letter-spacing: 0.11em;
  fill: var(--text-faint);
}
/* Reference earthquakes. Thin, dotted and unlit, so the eye reaches the selected
   event first and finds these only when it looks for scale. They are never
   allowed to be mistaken for the thing the reader came to see. */
.dx__ref {
  stroke: rgba(150, 180, 210, 0.28);
  stroke-width: 1;
  stroke-dasharray: 1 4;
}
.dx__refDot {
  fill: none;
  stroke: rgba(150, 180, 210, 0.5);
  stroke-width: 1;
}
.dx__refLabel {
  font-family: "IBM Plex Mono", monospace;
  font-size: 7.5px;
  letter-spacing: 0.1em;
  fill: rgba(168, 190, 210, 0.62);
  paint-order: stroke fill;
  stroke: var(--panel);
  stroke-width: 3.5px;
  stroke-linejoin: round;
}

/* This earthquake's depth, drawn across the whole section. A level, never a
   position: the depth is measured, the tectonic setting is not. */
.dx__level {
  stroke: var(--sev, #7f95ad);
  stroke-width: 1.25;
  stroke-dasharray: 3 3;
}
.dx__levelLabel {
  font-family: "IBM Plex Mono", monospace;
  font-size: 9px;
  letter-spacing: 0.1em;
  fill: var(--text);
  paint-order: stroke fill;
  stroke: var(--panel);
  stroke-width: 3px;
  stroke-linejoin: round;
}

/* --------------------------------------------------------- impact lesson */

/* The four PAGER levels as one rising scale. Height carries the ordering, so it
   reads as a ladder of increasing expected impact rather than as a track being
   filled — and the colours are the source's own semantics, never decoration. */
.alertscale {
  list-style: none;
  margin: 16px 0 10px;
  padding: 0;
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  gap: 6px;
  align-items: end;
}
.alertscale li {
  display: grid;
  grid-template-rows: 58px auto auto;
  align-items: end;
  justify-items: center;
  gap: 6px 4px;
}
.alertscale i {
  display: block;
  width: 100%;
  height: var(--h);
  border-radius: 1px;
  background: var(--pager);
  opacity: 0.28;
}
.alertscale li[data-here="true"] i {
  opacity: 1;
}
.alertscale b {
  font-family: "IBM Plex Mono", monospace;
  font-size: 9px;
  font-weight: 400;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-faint);
}
.alertscale li[data-here="true"] b {
  color: var(--text);
}
/* Each level says what it means, so the scale is the explanation and no
   sentence underneath has to repeat the selected one. Recedes to a hairline of
   context on the levels this earthquake is not. */
.alertscale span {
  font-size: 9.5px;
  line-height: 1.25;
  text-align: center;
  text-wrap: balance;
  color: var(--text-faint);
  opacity: 0.55;
}
.alertscale li[data-here="true"] span {
  opacity: 1;
  color: var(--text-dim, var(--text-faint));
}
.alertscale li[data-level="green"] { --pager: #4f9d6b; }
.alertscale li[data-level="yellow"] { --pager: var(--sev-moderate); }
.alertscale li[data-level="orange"] { --pager: var(--sev-elevated); }
.alertscale li[data-level="red"] { --pager: var(--sev-severe); }

.alertscale__now {
  margin: 0;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--text-dim);
}
.alertscale__now b {
  font-weight: 500;
  color: var(--text);
}
.alertscale__now b[data-level="green"] { color: #6fc08c; }
.alertscale__now b[data-level="yellow"] { color: var(--sev-moderate); }
.alertscale__now b[data-level="orange"] { color: var(--sev-elevated); }
.alertscale__now b[data-level="red"] { color: #e2627a; }

/* One bar, one denominator: segment width is share of everyone inside the
   shaking field. The rows underneath carry the numbers, so the strip never has
   to be read for magnitude — only for proportion. */
.exposure__strip--lesson {
  height: 12px;
  margin: 14px 0 12px;
  border-radius: 1px;
}
.exposure__rows {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 8px;
}
.exposure__rows li {
  display: grid;
  grid-template-columns: 10px 1fr 46px 58px 34px;
  align-items: baseline;
  gap: 10px;
  font-size: 12.5px;
  color: var(--text-dim);
}
.exposure__rows i {
  width: 10px;
  height: 10px;
  border-radius: 2px;
  background: var(--mmi);
  align-self: center;
}
.exposure__rows b {
  font-weight: 400;
  color: var(--text);
}
.exposure__mmi,
.exposure__share {
  font-family: "IBM Plex Mono", monospace;
  font-size: 11px;
  color: var(--text-faint);
}
.exposure__count {
  font-family: "IBM Plex Mono", monospace;
  font-size: 12px;
  color: var(--text);
  text-align: right;
}
.exposure__share {
  text-align: right;
}

.sublabel--block {
  margin: 22px 0 0;
}
.facets--countries {
  margin-top: 10px;
}
.facets--countries > div {
  grid-template-columns: 1fr auto;
}
.facets--countries dd {
  font-family: "IBM Plex Mono", monospace;
  font-size: 12px;
  text-align: right;
}
.facets--countries .estimates__note {
  display: block;
  font-size: 10.5px;
}

/* A distribution, drawn as columns on a shared baseline.
   Deliberately not a segmented strip totalling 100%: on the common case, where
   one bin holds 90% of the probability, a strip is a nine-tenths-filled track
   and reads as progress however it is styled. Columns can only be read as one
   outcome being likelier than another.
   Probability is slate, never a PAGER colour — the reader must not confuse "how
   likely" with "how bad". */
.dist {
  margin: 14px 0 0;
}
.dist__plot {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  gap: 5px;
  align-items: end;
}
.dist__bin {
  display: grid;
  grid-template-rows: 14px 92px 24px;
  align-items: end;
  justify-items: center;
  gap: 5px;
  min-width: 0;
}
/* Every column the same total height, so the percentages sit on one line and
   the baseline is shared. A one-line axis label on the last bin otherwise
   shortened that column and floated it out of alignment with the rest. */
.dist__plot {
  align-items: stretch;
}
.dist__bin b {
  font-family: "IBM Plex Mono", monospace;
  font-size: 9.5px;
  font-weight: 400;
  color: var(--text-faint);
  white-space: nowrap;
}
.dist__bin i {
  display: block;
  width: 100%;
  height: var(--p);
  min-height: 2px;
  border-radius: 1px 1px 0 0;
  background: rgba(150, 180, 210, 0.24);
}
.dist__bin span {
  display: grid;
  justify-items: center;
  gap: 1px;
}
.dist__bin em {
  font-family: "IBM Plex Mono", monospace;
  font-style: normal;
  font-size: 8.5px;
  letter-spacing: 0.04em;
  color: var(--text-faint);
  white-space: nowrap;
}
.dist__bin[data-likeliest="true"] i {
  background: rgba(203, 226, 246, 0.92);
}
.dist__bin[data-likeliest="true"] b,
.dist__bin[data-likeliest="true"] em {
  color: var(--text);
}

/* The distribution's headline is a claim, not a pair of tokens: "90% chance of
   fewer than 1 death" rather than "under 1 · 90%". */
.readout--chance {
  align-items: baseline;
  margin-bottom: 2px;
}
.readout--chance b {
  font-family: "IBM Plex Mono", monospace;
  font-size: 22px;
}
.readout--chance span {
  text-transform: none;
  letter-spacing: 0;
  font-size: 12.5px;
  color: var(--text-dim);
}

/* Three inputs, one result. Magnitude is upstream of the first box and appears
   in none of the others, which is the whole lesson. */
.ix {
  margin: 16px 0 0;
}
.ix__svg {
  width: 100%;
  height: auto;
}
.ix__box {
  fill: rgba(150, 180, 210, 0.08);
  stroke: rgba(150, 180, 210, 0.22);
  stroke-width: 1;
}
.ix__label,
.ix__step {
  font-family: "IBM Plex Mono", monospace;
  font-size: 8px;
  letter-spacing: 0.11em;
  fill: var(--text-faint);
}
.ix__step {
  fill: var(--text-dim);
}
.ix__result {
  font-family: "IBM Plex Mono", monospace;
  font-size: 11px;
  letter-spacing: 0.13em;
  fill: var(--text);
}
.ix__plus {
  font-family: "IBM Plex Mono", monospace;
  font-size: 12px;
  fill: var(--text-faint);
}
.ix__flow {
  fill: none;
  stroke: rgba(150, 180, 210, 0.4);
  stroke-width: 1;
  stroke-linejoin: round;
  stroke-linecap: round;
}

/* -------------------------------------------------- shaking lesson */

/* Two figures share this namespace: the intensity arcs and the cross-section of
   why shaking varies. Both are the same kind of drawing as the depth diagrams —
   geometry carrying an explanation the prose then does not have to repeat. */
.sx {
  margin: 16px 0 0;
}
.sx__svg {
  width: 100%;
  height: auto;
  overflow: visible;
}
.sx__caption {
  margin-top: 10px;
  font-size: 10.5px;
  line-height: 1.5;
  color: var(--text-faint);
}
.sx__note {
  font-family: "IBM Plex Mono", monospace;
  font-size: 8px;
  letter-spacing: 0.11em;
  fill: var(--text-faint);
}

/* Arcs spreading from the epicentre. Colour is the one absolute MMI ramp used
   by the contours on the globe, the legend and the exposure bars, so a band
   here and the same band out there are the same colour. */
.sx__ring {
  fill: color-mix(in oklab, var(--mmi) 34%, transparent);
  stroke: color-mix(in oklab, var(--mmi) 62%, transparent);
  stroke-width: 1;
}
.sx__axis {
  stroke: rgba(203, 222, 239, 0.16);
  stroke-width: 1;
}
.sx__ringTick {
  fill: none;
  stroke: rgba(203, 222, 239, 0.4);
  stroke-width: 1;
  stroke-linejoin: round;
}
.sx__ringLabel {
  font-size: 10px;
  fill: var(--text-dim);
  paint-order: stroke fill;
  stroke: var(--panel);
  stroke-width: 3px;
  stroke-linejoin: round;
}
.sx__ringMmi {
  font-family: "IBM Plex Mono", monospace;
  font-size: 10px;
  fill: var(--text);
}
.sx__epi {
  fill: rgba(232, 242, 252, 0.95);
}
.sx__epiLabel {
  font-family: "IBM Plex Mono", monospace;
  font-size: 9px;
  letter-spacing: 0.12em;
  fill: var(--text-dim);
}
.sx__epiNote {
  font-size: 9px;
  fill: var(--text-faint);
}

/* Cross-section: rock, a sediment basin, the rupture below and what the ground
   does at three places. */
.sx__ground {
  fill: rgba(150, 180, 210, 0.07);
}
.sx__basin {
  fill: rgba(150, 180, 210, 0.16);
}
.sx__surface {
  fill: none;
  stroke: rgba(203, 222, 239, 0.5);
  stroke-width: 1.25;
  stroke-linejoin: round;
}
.sx__wave {
  fill: none;
  stroke: var(--sev, #7f95ad);
  stroke-width: 1;
  opacity: 0.3;
}
.sx__depthLine {
  stroke: var(--sev, #7f95ad);
  stroke-width: 1;
  stroke-dasharray: 2 3;
  opacity: 0.6;
}
.sx__source {
  fill: var(--sev, #7f95ad);
}
.sx__site {
  fill: rgba(232, 242, 252, 0.9);
}
.sx__trace {
  fill: none;
  stroke: rgba(226, 238, 250, 0.8);
  stroke-width: 1.25;
  stroke-linejoin: round;
  stroke-linecap: round;
}

/* Two or three short facts that want comparing rather than reading — what
   people, objects and buildings do at an intensity; instruments against
   reports. A definition list because that is what it is. */
.facets {
  margin: 14px 0 0;
  display: grid;
  gap: 12px;
}
.facets > div {
  display: grid;
  grid-template-columns: 72px 1fr;
  gap: 12px;
  align-items: baseline;
}
.facets dt {
  font-size: 11.5px;
  color: var(--text-faint);
}
.facets dd {
  margin: 0;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--text);
}

.estimates--observed {
  margin: 16px 0 4px;
}
.estimates--observed dd {
  font-size: 13px;
  color: var(--text);
}
.estimates__note {
  font-family: "IBM Plex Mono", monospace;
  font-size: 10.5px;
  color: var(--text-faint);
}

/* The Modified Mercalli scale, as a scale. Bars lengthen and darken downward so
   it reads as intensity rising; it is deliberately not a track with a filled
   portion, which would read as progress toward something. */
.mmiscale {
  list-style: none;
  margin: 16px 0 12px;
  padding: 0;
  display: grid;
  gap: 5px;
}
.mmiscale li {
  display: grid;
  grid-template-columns: 44px 68px 1fr;
  align-items: center;
  gap: 10px;
  padding: 3px 6px 3px 10px;
  border-radius: 2px;
  position: relative;
}
.mmiscale b {
  font-family: "IBM Plex Mono", monospace;
  font-size: 11px;
  font-weight: 400;
  color: var(--text-faint);
  text-align: right;
}
.mmiscale span {
  font-size: 11.5px;
  color: var(--text-faint);
}
.mmiscale i {
  display: block;
  height: 9px;
  width: var(--w);
  background: var(--mmi);
  border-radius: 1px;
  opacity: 0.55;
  max-width: 100%;
}
/* A band the globe is drawing right now. A marker rather than a colour change:
   the colour already means intensity and must not also mean presence. */
.mmiscale li[data-globe="true"]::before {
  content: "";
  position: absolute;
  left: 0;
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: var(--text-faint);
}
.mmiscale li[data-here="true"] {
  background: rgba(150, 180, 210, 0.09);
}
.mmiscale li[data-here="true"] b,
.mmiscale li[data-here="true"] span {
  color: var(--text);
}
.mmiscale li[data-here="true"] i {
  opacity: 1;
}
.mmiscale em {
  font-style: normal;
  font-family: "IBM Plex Mono", monospace;
  font-size: 9px;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  color: var(--text-dim);
  white-space: nowrap;
  /* Over the end of the bar's track rather than in a column of its own: a
     fourth column would exist on every row and shorten every bar, and the bars
     lengthening down the scale is the whole reading. Only the marked row's own
     bar is capped, so the scale still reads as rising everywhere else. */
  position: absolute;
  right: 2px;
  top: 50%;
  transform: translateY(-50%);
}
.mmiscale li[data-here="true"] i {
  max-width: calc(100% - 98px);
}
.mmiscale__dot {
  display: inline-block;
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: var(--text-faint);
  vertical-align: middle;
}

.estimates--planes {
  margin-bottom: 4px;
}
.estimates--planes .planes {
  gap: 16px;
}
.estimates--planes .planes span {
  font-family: inherit;
  font-size: 12px;
  color: var(--text-faint);
}
.estimates--planes .planes b {
  font-family: "IBM Plex Mono", monospace;
  font-size: 15px;
  color: var(--text);
  margin-right: 4px;
}

/* Physical movement, then the same earthquake as scientific shorthand. The two
   are stacked and joined, because that adjacency is the explanation — it used to
   be a paragraph after an unexplained symbol. */
.fm {
  margin: 16px 0 4px;
  display: grid;
  gap: 4px;
  justify-items: center;
}
.fm__stage {
  display: grid;
  justify-items: center;
  gap: 6px;
  width: 100%;
}
.fm__motion {
  width: min(100%, 300px);
  height: auto;
}
.fm__stageLabel {
  font-family: "IBM Plex Mono", monospace;
  font-size: 8px;
  font-weight: 400;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-faint);
}
.fm__link {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 6px 0 2px;
  font-size: 11px;
  color: var(--text-faint);
}
.fm__link span {
  font-family: "IBM Plex Mono", monospace;
  font-size: 13px;
  color: var(--text-dim);
}
.fm__stage--ball {
  grid-template-columns: auto 1fr;
  align-items: center;
  justify-items: start;
  gap: 4px;
}

/* The orientation cue: a sphere, the half we are looking into, and the
   direction we are looking from. "Lower hemisphere" is exact and teaches a
   beginner nothing; this is the same fact as a picture. */
.fm__orient {
  width: 86px;
  height: auto;
  flex: none;
}
.fm__sphere {
  fill: none;
  stroke: rgba(150, 180, 210, 0.4);
  stroke-width: 1;
}
.fm__lower {
  fill: rgba(150, 180, 210, 0.16);
}
.fm__equator {
  fill: none;
  stroke: rgba(150, 180, 210, 0.4);
  stroke-width: 1;
  stroke-dasharray: 2 3;
}
.fm__eye {
  fill: none;
  stroke: rgba(203, 222, 239, 0.55);
  stroke-width: 1.25;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.fm__orientLabel {
  font-family: "IBM Plex Mono", monospace;
  font-size: 7.5px;
  letter-spacing: 0.1em;
  fill: var(--text-faint);
}

/* The overlay owns the box and the canvas is placed inside it, so the SVG's
   0–320 coordinates land exactly on the canvas pixels the anchors were computed
   from. The extra 52 units below the disc are the margin the nodal-plane label
   needs; without it there is nowhere outside the disc to put anything, because
   the disc is already as wide as the panel. */
.beach {
  position: relative;
  margin: 0;
  justify-self: center;
  width: min(100%, 216px);
  aspect-ratio: 320 / 372;
}
.beach__canvas {
  position: absolute;
  left: 0;
  top: 1.613%;
  display: block;
  width: 100%;
  height: 86.022%;
}
/* Labels over the canvas, in the interface's own type rather than in whatever
   the canvas can manage. */
.beach__notes {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}
/* Printed on the lobe it names. The knockout is what keeps it legible over a
   hard-edged two-tone field. */
.fm__regionLabel {
  font-family: "IBM Plex Mono", monospace;
  font-size: 13px;
  letter-spacing: 0.11em;
  paint-order: stroke fill;
  stroke-width: 4px;
  stroke-linejoin: round;
}
.fm__regionLabel--push {
  fill: #fff;
  stroke: rgba(120, 34, 24, 0.85);
}
.fm__regionLabel--pull {
  fill: rgba(226, 238, 250, 0.92);
  stroke: rgba(10, 16, 24, 0.9);
}
.fm__leader {
  fill: none;
  stroke: rgba(203, 222, 239, 0.45);
  stroke-width: 1.5;
  stroke-linejoin: round;
}
.fm__leaderDot {
  fill: rgba(226, 238, 250, 0.9);
}
.fm__leaderLabel {
  font-family: "IBM Plex Mono", monospace;
  font-size: 12px;
  letter-spacing: 0.09em;
  fill: var(--text-faint);
}

.linkish {
  margin-top: 12px;
  padding: 7px 12px;
  border: 1px solid var(--line-strong);
  border-radius: 2px;
  background: none;
  color: var(--text-dim);
  font: inherit;
  font-size: 12px;
  cursor: pointer;
}
.linkish:hover {
  color: var(--text);
  border-color: rgba(150, 180, 210, 0.34);
}
.linkish[aria-pressed="true"] {
  background: rgba(163, 199, 228, 0.14);
  border-color: rgba(163, 199, 228, 0.36);
  color: #f0f6fb;
}

/* Development overlay. Present only when the app is started with ?debug=1. */
.debug {
  position: absolute;
  left: 16px;
  top: 16px;
  /* Top of the stage scale: a diagnostic readout is useless underneath the
     thing being diagnosed. */
  z-index: 6;
  margin: 0;
  padding: 10px 12px;
  border: 1px solid var(--line);
  background: rgba(4, 8, 12, 0.82);
  color: #9fd2b6;
  font-family: "IBM Plex Mono", monospace;
  font-size: 10px;
  line-height: 1.6;
  white-space: pre;
  pointer-events: none;
  display: none;
}
body.is-debug .debug {
  display: block;
}

.debugbar {
  position: absolute;
  left: 16px;
  bottom: 56px;
  z-index: 6;
  display: none;
  flex-wrap: wrap;
  gap: 6px;
  max-width: 260px;
}
body.is-debug .debugbar {
  display: flex;
}
.debugbar button {
  padding: 5px 9px;
  border: 1px solid rgba(159, 210, 182, 0.34);
  border-radius: 2px;
  background: rgba(4, 8, 12, 0.82);
  color: #9fd2b6;
  font-family: "IBM Plex Mono", monospace;
  font-size: 10px;
  cursor: pointer;
}
.debugbar button:hover {
  border-color: rgba(159, 210, 182, 0.6);
}

/* -------------------------------------------------------------- mobile */

@media (min-width: 881px) {
  /* Desktop keeps the same two groups. `display: contents` dissolves the wrapper
     so `.search` and its header stay direct children of the panel column and
     nothing about the surrounding layout changes; only the label is added. */
  .discover {
    display: contents;
  }
  /* Exactly the FILTERS grammar — it is the same rule, only positioned. The
     gutter belongs to the header ROW now, so the secondary action lands on the
     same right margin as the content instead of on the panel edge. */
  /* `.discover` is `display: contents` here, so its header needs the gutter
     itself; `.controls` is a real box that already has one, and adding it again
     put FILTERS 26px right of SEARCH. */
  /* `.discover` is `display: contents` here, so its header carries the gutter
     itself; `.controls` is a real box that already has one. Both take the
     header->content gap from the same token. */
  /* `.discover` is `display: contents` here, so a margin on IT does nothing —
     the gap has to sit on its header, which is the first real box. That is why
     the search section looked attached to the head above it. */
  .discover > .sechead {
    padding: 0 26px;
    margin: var(--space-section-gap) 0 var(--space-title-content);
  }
  /* The head's bottom padding and the next section's top margin were both
     paying for the same gap. The section token owns it. */
  .list__head {
    padding-bottom: 0;
  }
  .controls > .sechead {
    padding: 0;
    margin-bottom: var(--space-title-content);
  }
  /* FILTERS read as a caption for the search input above it — a 2px gap where
     the section break belongs. */
  .controls {
    margin-top: var(--space-section-gap);
  }

  /* SEARCH is one group: label, field, ingress. The ingress had 18px above and
     22px below, which made it read as its own section rather than as the
     secondary action of the group it belongs to. */

  /* The label/control rhythm inside FILTERS, slightly closed up. Same
     relationship, less air. */
  /* No grid gap: it would stack on the header's own margin and make the
     header->content gap 34px where the token says 10. Groups carry their own
     bottom spacing, exactly as on mobile — one model, two sets of values. */
  .controls {
    gap: 0;
    padding-bottom: 8px;
  }
  .controls > .control,
  .secondary .control {
    padding-bottom: var(--space-section-gap);
  }

  /* Time window and Data available are broad discovery controls — useful without
     the reader having formulated a geological question — so they stay out.
     Magnitude, depth and fault type are deliberate constraints and sit behind the
     disclosure. `.secondary` is `display: contents` here, so its children are
     already grid items of `.controls`; ordering and hiding them individually is
     all this takes, with no second desktop filter architecture and no change to
     what mobile does. */
  .controls { grid-auto-flow: row; }
  .controls__label      { order: 0; }
  .controls > .control  { order: 1; }
  .control--product     { order: 2; }
  .filters              { order: 3; }
  .control--magnitude   { order: 4; }
  .control--depth       { order: 5; }
  .control--fault       { order: 6; }
  .filters__done        { order: 7; }
  
  .control--magnitude,
  .control--depth,
  .control--fault {
    display: none;
  }
  .secondary.is-open .control--magnitude,
  .secondary.is-open .control--depth,
  .secondary.is-open .control--fault {
    display: block;
  }
  
  .filters {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    align-self: flex-start;
    margin: 0;
    padding: 2px 0;
    appearance: none;
    border: 0;
    background: none;
    font: inherit;
    font-size: 11px;
    letter-spacing: 0.04em;
    color: rgba(178, 208, 234, 0.72);
    cursor: pointer;
    text-align: left;
  }
  .filters__label {
    text-decoration: underline;
    text-underline-offset: 3px;
  }
  .filters__summary:empty {
    display: none;
  }
  .filters:hover {
    color: rgba(200, 222, 240, 0.95);
  }
}

@media (max-width: 880px) {
  /* One map-legend ingress on mobile. The floating shaking key used to move to
     the top-left under this breakpoint, where it sat directly on SEISMOS and the
     live status; it is now a group inside the Legend overlay instead. See
     `setShakeLegend`, which writes both surfaces. */
  .shakeLegend {
    display: none;
  }

  .app {
    grid-template-columns: 1fr;
    grid-template-rows: 1fr;
  }

  .stage {
    grid-column: 1;
    grid-row: 1;
    padding-bottom: var(--sheet-peek);
  }

  /* The sheet covers the bottom of the stage, so the attribution moves above
     it rather than sitting underneath where it could not be read. */
  /* Every required credit is retained; only its visual weight drops. It sits
     directly above the sheet so it never covers geography being read. */
  .attribution {
    left: 14px;
    right: 14px;
    bottom: calc(var(--sheet-peek) + 4px);
    font-size: 7.5px;
    line-height: 1.45;
    letter-spacing: 0.03em;
    color: rgba(168, 190, 210, 0.3);
  }

  /* Collapsed by default: the permanent key cost too much map. It opens
     under its control in the quiet upper-right corner. */
  .legend {
    left: auto;
    right: calc(env(safe-area-inset-right, 0px) + 14px);
    top: calc(env(safe-area-inset-top, 0px) + 58px);
    bottom: auto;
    display: grid;
    gap: 14px;
    padding: 12px 14px;
    max-width: min(66vw, 250px);
    max-height: 58dvh;
    overflow-y: auto;
    overscroll-behavior: contain;
    transform: scale(0.95) translateY(-4px);
    transform-origin: top right;
    opacity: 0;
    visibility: hidden;
    transition: opacity 160ms ease, transform 160ms ease, visibility 160ms;
  }
  .legend.is-open {
    opacity: 1;
    visibility: visible;
    transform: scale(0.95) translateY(0);
  }
  /* The shaking key, inside the one Legend surface. */
  .legend__group--shake {
    display: grid;
    gap: 5px;
    padding-top: 13px;
    border-top: 1px solid var(--line);
  }
  .legend__group--shake[hidden] {
    display: none;
  }
  .legend__group--shake b {
    font-size: 10px;
    font-weight: 400;
    letter-spacing: 0.13em;
    text-transform: uppercase;
    color: var(--text-faint);
  }
  .legend__group--shake span {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: 11px;
    color: var(--text-dim);
  }
  .legend__group--shake span i {
    width: 10px;
    height: 10px;
    flex: none;
    border-radius: 2px;
    background: var(--mmi, var(--text-faint));
  }
  .legend__group--shake .shakeLegend__obs i[data-shape="tri"] {
    border-radius: 0;
    background: none;
    border-left: 5px solid transparent;
    border-right: 5px solid transparent;
    border-bottom: 9px solid rgba(214, 231, 246, 0.85);
    width: 0;
    height: 0;
  }
  .legend__group--shake .shakeLegend__obs i[data-shape="dot"] {
    border-radius: 50%;
    background: rgba(214, 231, 246, 0.85);
  }

  /* Bottom sheet owns the same master information as the desktop panel.

     Height and transform are both written by app.js in pixels. CSS owns the
     transition and nothing else: the values used to be declared here as dvh
     snap points and read back with parseFloat, which returned 55 for "55dvh",
     and there is now no second declaration that can drift. The pre-boot height
     below is replaced on the first applySnap. */
  .panel {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    height: var(--sheet-height);
    max-height: var(--sheet-height);
    border-right: 0;
    border-top: 1px solid var(--line-strong);
    border-radius: 14px 14px 0 0;
    background: linear-gradient(180deg, rgba(11, 16, 23, 0.97) 0%, var(--bg-lift) 100%);
    backdrop-filter: blur(6px);
    transform: translateY(calc(var(--sheet-height) - var(--sheet-peek)));
    transition: transform 230ms cubic-bezier(0.32, 0.72, 0.2, 1);
    padding-bottom: env(safe-area-inset-bottom);
    touch-action: none;
  }

  /* The finger owns the sheet while it is down; no transition to fight it, and
     the transform is promoted for the duration rather than permanently. */
  .panel.is-dragging {
    transition: none;
    will-change: transform;
  }

  /* Content scrolls only at the tallest snap. Below it there is nothing to
     scroll to that raising the sheet would not show better, and locking the
     scroller is what removes the sheet-versus-content arbitration rather than
     tuning it — there is no state in which both want the same vertical drag. */
  .master,
  .detail {
    touch-action: pan-y;
    overflow-y: hidden;
    overscroll-behavior: contain;
  }
  .panel.is-scrollable .master,
  .panel.is-scrollable .detail {
    overflow-y: auto;
  }

  .back,
  .detail__head,
  .detail__place,
  .detail__meta,
  .detail > .lesson__heading,
  .detail > .readout,
  .detail > .meaning,
  .detail > figure,
  .section {
    padding-left: 20px;
    padding-right: 20px;
  }

  /* The sheet uses a 20px gutter, so the desktop optical offset would leave the
     44px target only 7px from the screen edge — inside the range iOS reserves
     for its own edge-back swipe. Pull it in: the icon sits a few pixels proud of
     the text margin, which is the better trade against a control that fights a
     system gesture. */
  .share {
    margin-right: -6px;
  }
  .share--sub {
    margin-right: 6px;
  }

  /* 44px of hit area for artwork that is 38x3. The handle is no longer the only
     way to move the sheet — the whole surface drags now — but it is still the
     control that advances a state on tap, and a 19px target was not reliably
     hittable. */
  /* 44px of hit area that costs 28px of layout. The compact state has to fit
     the whole of an event's identity inside a quarter of the screen, and the
     handle is chrome — it must not spend the room the place name needs. */
  .panel__grabber {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    min-height: 44px;
    margin-bottom: -16px;
    padding: 0;
    border: 0;
    background: none;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
  }
  .panel__grabber span {
    display: block;
    width: 38px;
    height: 3px;
    margin: 0 auto;
    border-radius: 2px;
    background: rgba(150, 180, 210, 0.3);
  }

  /* Identity and live status leave the sheet entirely and sit on the globe, so
     the collapsed sheet opens straight into task content. */
  .identity {
    display: none;
  }

  .mapid {
    display: block;
    position: absolute;
    top: calc(env(safe-area-inset-top, 0px) + 14px);
    left: calc(env(safe-area-inset-left, 0px) + 16px);
    z-index: 4;
    pointer-events: none;
    max-width: calc(100% - 150px);
  }
  .mapid__mark {
    display: block;
    font-size: 14px;
    font-weight: 500;
    letter-spacing: 0.22em;
    line-height: 1.15;
    color: #f2f7fb;
    /* A shadow rather than a card, so the globe stays uninterrupted. */
    text-shadow: 0 1px 4px rgba(0, 0, 0, 0.9), 0 0 14px rgba(0, 0, 0, 0.55);
  }
  .mapid__status {
    display: flex;
    align-items: center;
    gap: 6px;
    margin: 4px 0 0;
    font-family: "IBM Plex Mono", monospace;
    font-size: 10px;
    line-height: 1.2;
    letter-spacing: 0.07em;
    color: rgba(186, 206, 226, 0.74);
    text-shadow: 0 1px 4px rgba(0, 0, 0, 0.9);
    white-space: nowrap;
  }
  .mapid__dot {
    flex: none;
    width: 5px;
    height: 5px;
    border-radius: 50%;
    background: #4ec98a;
    box-shadow: 0 0 7px rgba(78, 201, 138, 0.6);
  }
  .mapid__status[data-state="error"] .mapid__dot {
    background: #e0b23c;
    box-shadow: 0 0 7px rgba(224, 178, 60, 0.6);
  }
  .mapid__status[data-state="unavailable"] .mapid__dot,
  .mapid__status[data-state="loading"] .mapid__dot,
  .mapid__status[data-state="idle"] .mapid__dot {
    background: rgba(168, 190, 210, 0.5);
    box-shadow: none;
  }
  /* Nothing to separate when the status carries no age. */
  .mapid__status[data-empty="true"] .mapid__sep {
    display: none;
  }

  /* 44px of hit area around artwork that stays small: the padding is invisible
     and the border is drawn on an inner box, so the control looks the same size
     it always did and is twice as easy to hit. */
  .legendToggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    position: absolute;
    top: calc(env(safe-area-inset-top, 0px) + 6px);
    right: calc(env(safe-area-inset-right, 0px) + 6px);
    z-index: 6;
    min-height: 44px;
    min-width: 44px;
    padding: 8px 12px;
    border: 0;
    background: none;
    font: inherit;
    font-size: 10px;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: rgba(200, 218, 234, 0.82);
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
  }
  /* Padding is on the pill, not the button: the 44px hit area and the pill's
     height are both unchanged, the text simply stops crowding the border. */
  .legendToggle::before {
    content: "";
    position: absolute;
    inset: 8px;
    z-index: -1;
    border: 1px solid rgba(150, 180, 210, 0.22);
    border-radius: 999px;
    background: rgba(9, 14, 21, 0.5);
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
    transition: border-color 140ms ease, background-color 140ms ease;
  }
  /* Restrained but unmistakable: the reader has to be able to tell at a glance
     whether the key they are looking for is already open. */
  .legendToggle.is-active {
    color: #eaf3fa;
  }
  .legendToggle.is-active::before {
    border-color: rgba(168, 200, 226, 0.55);
    background: rgba(24, 38, 54, 0.78);
  }

  /* iOS Safari zooms the layout viewport whenever a focused form control has a
     computed font size below 16px, and it does not zoom back out on blur — which
     left the page scaled, the sheet wider than the screen and pinch-out unable
     to recover it. Both fields were 12px. They are 16px on the sheet breakpoint
     and the hierarchy is carried by weight, colour and tracking instead, which
     is what it should have been doing anyway. The viewport meta is deliberately
     untouched: `maximum-scale` or `user-scalable=no` would "fix" this by
     removing pinch-to-zoom, and that is an accessibility regression, not a fix.

     16px is the threshold, not a target — anything at or above it is safe. */
  .search__field,
  .hist__field {
    font-size: 16px;
    letter-spacing: 0.005em;
  }
  /* The INPUT must stay >= 16px or iOS Safari zooms the layout viewport on
     focus and never zooms back, so that is a floor, not a preference. What is
     actually on screen at rest is the placeholder, and that can come down
     towards the filter-control scale. */
  .search__field::placeholder,
  .hist__field::placeholder {
    font-size: 14px;
    letter-spacing: 0.01em;
  }
  /* The taller text needs the row to keep its proportions rather than grow. */
  .hist__bar {
    padding-left: 20px;
    padding-right: 20px;
  }

  /* The opening state. The sheet names what it holds and starts listing it;
     search, the time window, Filters and historical search are configuration
     and wait until the sheet is pulled up. A first-time user should meet the
     Earth and a couple of real earthquakes, not a control panel. */
  body:not(.is-historical-search) .panel[data-state="master"][data-snap="compact"] .discover,
  body:not(.is-historical-search) .panel[data-state="master"][data-snap="compact"] .search,
  body:not(.is-historical-search) .panel[data-state="master"][data-snap="compact"] .controls,
  body:not(.is-historical-search) .panel[data-state="master"][data-snap="compact"] .filters,
  body:not(.is-historical-search) .panel[data-state="master"][data-snap="compact"] .secondary,
  body:not(.is-historical-search) .panel[data-state="master"][data-snap="compact"] .hist {
    display: none;
  }
  /* The head is the sheet's label at this height, so it keeps its full presence
     and the first row follows immediately. */
  .panel[data-state="master"][data-snap="compact"] .list__head {
    margin-bottom: 0;
  }

  /* Compact shows event identity and nothing else. The historical badge repeats
     the date that is already on the time line below it, so it costs 40px of a
     state that has about 215px to work with. It returns at every taller snap. */
  .panel[data-snap="compact"] .histbadge,
  .panel.is-measuring-compact .histbadge {
    display: none;
  }

  /* Touch targets. Roughly 44x44 of effective hit area on every control that
     matters, bought with invisible padding and negative margins rather than by
     making the artwork bulky: the visual weight of the interface is unchanged. */
  /* Back is a control, not a header row. Full width made the entire top of the
     sheet navigate: a user reaching for the sheet to drag it landed on Back
     instead. The hit area is still 44px tall but only as wide as the control,
     and the rest of the header stays available for the drag it was reached for.
     Desktop keeps the descriptive label, where there is room and no gesture to
     collide with. */
  .back {
    width: fit-content;
    min-height: 44px;
    padding: 6px 20px;
    border-bottom: 0;
  }
  .back__long {
    display: none;
  }
  .back__short {
    display: inline;
  }
  /* The hairline the full-width control used to draw still separates navigation
     from the event itself. */
  .back ~ .detail__head,
  .back ~ .histbadge {
    border-top: 1px solid var(--line);
    padding-top: 14px;
  }
  .back ~ .histbadge + .detail__head {
    border-top: 0;
    padding-top: 0;
  }
  /* The magnitude is still the loudest thing in the sheet; it just does not need
     to be 66px tall to be that on a 390px screen. */
  .detail__mag {
    font-size: 38px;
  }
  .detail__head {
    margin-bottom: 6px;
  }
  .obstoggles button,
  .filters__done,
  .hist__go,
  .search__clear,

  /* The ingress is navigation, so it reads as a row you can hit rather than a
     small underlined phrase among the filters. */

  /* Breathing room under the back control, per the destination model. */
  .hist__bar {
    padding: 16px 20px 12px;
  }
  .obstoggles button,
  .filters__done,
  .hist__go {
    padding-top: 10px;
    padding-bottom: 10px;
  }
  /* The ingress is a text link, so its box grows downward into its own margin
     instead of pushing the section apart. */
  .chips button,
  .segmented button {
    min-height: 44px;
  }
  .legend__group--shake span {
    min-height: 22px;
  }

  /* The sheet is for browsing earthquakes, so the list has to start early.
     `display: contents` lets the individual controls take their place in the
     master column directly, which is what makes the reordering below possible
     without touching the desktop DOM. */
  /* Not `display: contents`. The whole point of this arrangement is that
     Filters is a parent with Time window and More filters inside it, and
     flattening the box flattens the grouping with it. Children carry their own
     gutters, so the group itself adds none. */


  .master {
    display: flex;
    flex-direction: column;
  }

  /* Recent earthquakes → search → historical ingress → Filters → list.
     That is the document order, so nothing here reorders anything: discovery
     first, then the one other place to look, then the controls that narrow
     what you already have. */

  /* A comfortable row, and the separation that stops a sheet drag from landing
     on the search field. */


  /* Kept above the list: below it would mean scrolling past every row to find
     it. Compact margins so it costs the list as little as possible. */

  /* A lightweight expander, not a section header: the borders it used to carry
     made it a third top-level band competing with the group it belongs to. It
     sits inside Filters now, so it only needs to look like an action. */
  .filters {
    display: flex;
    align-items: center;
    gap: 8px;
    width: auto;
    /* Visual height stays modest; the touch target is made up with padding. */
    min-height: 44px;
    margin: 0;
    padding: 0 20px;
    appearance: none;
    border: 0;
    background: none;
    font: inherit;
    font-size: 12px;
    letter-spacing: 0.04em;
    color: rgba(190, 214, 236, 0.9);
    cursor: pointer;
    text-align: left;
  }
  .filters__summary {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-family: "IBM Plex Mono", monospace;
    font-size: 11px;
    color: var(--text-faint);
  }

  /* 0fr → 1fr collapses to exactly the content height with no magic number. */
  .secondary {
    display: grid;
    grid-template-rows: 0fr;
    opacity: 0;
    transform: translateY(-4px);
    transition:
      grid-template-rows 200ms ease,
      opacity 200ms ease,
      transform 200ms ease;
  }
  .secondary__inner {
    display: block;
    min-height: 0;
    overflow: hidden;
  }
  .secondary.is-open {
    grid-template-rows: 1fr;
    opacity: 1;
    transform: translateY(0);
  }

  .filters__done {
    display: block;
    margin: 12px 20px 14px auto;
    min-height: 44px;
    padding: 0 16px;
    appearance: none;
    border: 1px solid var(--line-strong, rgba(150, 180, 210, 0.3));
    border-radius: 999px;
    background: none;
    font: inherit;
    font-size: 12px;
    color: rgba(200, 222, 240, 0.9);
    cursor: pointer;
  }

  .list__head,
  .row,
  .provenance,
  .list__empty {
    padding-left: 20px;
    padding-right: 20px;
  }

  .provenance {
    display: none;
  }
  .panel.is-expanded .provenance {
    display: grid;
  }

  /* SEARCH and FILTERS are the two groups, with the same label/control
     relationship in each. The rhythm is tight inside a group and looser between
     them, so the hierarchy is legible from spacing alone rather than from rules
     and full-width bands. */
  .search {
    margin: 0 20px 2px;
  }

  /* The ingress is a link inside the SEARCH group, not a band between two rules.
     The 44px target stays; it is the visible padding that goes, and the chevron
     sits beside the words instead of pinned to the far edge. */

  /* The one deliberately larger gap: where SEARCH ends and FILTERS begins. No
     rule — the label and the space do that work. */

  /* One repeatable rhythm: a label sits close to the controls it names, and the
     gap to the NEXT filter group is larger, so the grouping is readable without
     rules or extra chrome. 6px inside a group, 14px between them. */
  .control__label {
    margin-bottom: 6px;
  }

  /* The list starts right after the controls; the head already separates it. */
  #list {
    margin-top: 2px;
  }

  /* The field keeps a 44px hit area through its own height; the extra padding
     was making the row 50px for a 16px input. */

  /* The ingress keeps its 44px target and gives back the space BELOW it only.
     Pulling up as well would put the target on top of the search field, and two
     overlapping tap areas at a boundary is how a mis-tap happens; below it is
     the FILTERS label, which is not interactive. */

  /* The visible control stays compact; the target is bought with padding that
     does not change the row's height, so the header rhythm is unaffected. */
  /* SEARCH / Historical events is the widest of these rows. The title holds its
     ground and the action keeps its own line-length; neither wraps into the
     other at 360px. */
  .sechead {
    gap: 10px;
  }
  .sechead .controls__label,
  .sechead .section__label {
    min-width: 0;
  }

  /* ---- consolidated mobile rhythm ------------------------------------------
     One rule per selector, all three gaps from the tokens. These selectors each
     had two or three competing declarations before, the later ones silently
     winning — which is how a "small margin fix" stops being predictable. */
  .list__head {
    padding: 4px 20px var(--space-title-content);
    min-height: 0;
    margin-bottom: 0;
    align-items: baseline;
    border-top: 0;
  }
  .discover,
  .controls {
    display: block;
    padding: 0;
    border-top: 0;
    margin-top: var(--space-section-gap);
  }
  .discover > .sechead,
  .controls > .sechead {
    padding: 0 20px;
    margin-bottom: var(--space-title-content);
  }
  .controls > .control,
  .secondary .control {
    padding: 0 20px var(--space-section-gap);
  }
  .search__field {
    padding-top: 9px;
    padding-bottom: 9px;
  }
  .sechead__action {
    min-height: 44px;
    display: inline-flex;
    align-items: center;
    margin: -14px 0;
  }
}

@media (prefers-reduced-motion: reduce) {
  * {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
  .marker.is-arriving .marker__pulse {
    opacity: 0;
  }
  .marker.is-arriving .marker__ring {
    box-shadow: 0 0 0 4px color-mix(in oklab, var(--sev) 18%, transparent);
  }
}

/* ------------------------------------- first-motion ring and tectonic map */

/* The pattern that identifies the faulting style. Direction carries the
   meaning — arrows point away from the source or back toward it — and colour
   only reinforces it, so the diagram survives being read without colour. */
.fmr {
  margin: 16px 0 0;
}
.fmr__svg {
  width: 100%;
  height: auto;
  display: block;
}
.fmr__wave {
  fill: none;
  stroke: rgba(150, 180, 210, 0.16);
  stroke-width: 1;
}
.fmr__source {
  fill: var(--sev, #7f95ad);
}
.fmr__tick {
  stroke: rgba(150, 180, 210, 0.5);
  stroke-width: 1.4;
  stroke-linecap: round;
}
.fmr__station[data-out="true"] .fmr__tick {
  stroke: var(--sev, #7f95ad);
}
.fmr__dot {
  fill: rgba(12, 18, 27, 1);
  stroke: rgba(150, 180, 210, 0.55);
  stroke-width: 1.2;
}
.fmr__station[data-out="true"] .fmr__dot {
  fill: var(--sev, #7f95ad);
  stroke: none;
}
.fmr__key {
  display: flex;
  gap: 16px;
  margin-top: 6px;
  font-size: 11px;
  color: var(--text-faint);
}
.fmr__key span {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}
.fmr__key span::before {
  content: "";
  width: 8px;
  height: 8px;
  border-radius: 50%;
}
.fmr__key span[data-out="true"]::before {
  background: var(--sev, #7f95ad);
}
.fmr__key span[data-out="false"]::before {
  background: rgba(12, 18, 27, 1);
  box-shadow: inset 0 0 0 1.2px rgba(150, 180, 210, 0.55);
}

/* Orientation first. Coastlines and national outlines establish where on Earth
   this is; the plate boundary is the tectonic setting; the earthquake is the
   strongest mark on the map and nothing is allowed to outweigh it. */
.omap {
  margin: 0 0 12px;
}
.omap__svg {
  width: 100%;
  height: auto;
  display: block;
  background: rgba(150, 180, 210, 0.045);
  border-radius: 3px;
}
.omap__coast {
  fill: none;
  stroke: rgba(150, 180, 210, 0.5);
  stroke-width: 0.9;
  stroke-linejoin: round;
  stroke-linecap: round;
}
.omap__admin {
  fill: none;
  stroke: rgba(150, 180, 210, 0.2);
  stroke-width: 0.7;
  stroke-dasharray: 2 2;
}
.omap__boundary {
  fill: none;
  stroke: rgba(232, 141, 97, 0.75);
  stroke-width: 1.5;
  stroke-linejoin: round;
  stroke-linecap: round;
}
.omap__leader {
  stroke: rgba(150, 180, 210, 0.45);
  stroke-width: 0.9;
  stroke-dasharray: 2.5 3;
}
.omap__halo {
  fill: none;
  stroke: var(--sev, #7f95ad);
  stroke-width: 1.1;
  opacity: 0.5;
}
.omap__event {
  fill: var(--sev, #7f95ad);
  stroke: rgba(6, 11, 20, 0.9);
  stroke-width: 1.2;
}
/* Three quiet tiers. Plate names name the setting, region and place names tell
   the reader where on Earth they are; neither competes with the epicentre. */
.omap__plate {
  fill: rgba(232, 141, 97, 0.6);
  font-size: 8px;
  letter-spacing: 0.14em;
  font-family: "IBM Plex Mono", monospace;
}
.omap__region {
  fill: rgba(178, 200, 222, 0.72);
  font-size: 7.5px;
  letter-spacing: 0.06em;
}
.omap__place {
  fill: rgba(150, 180, 210, 0.55);
  font-size: 7px;
}

/* Technical detail, subordinate by construction: closed until asked for. */
.tech {
  margin: 18px 0 0;
  border-top: 1px solid var(--line);
  padding-top: 14px;
}
.tech > summary {
  cursor: pointer;
  font-size: 11.5px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-faint);
  list-style: none;
  /* 44px of target bought with padding, not height alone. */
  padding: 12px 0;
  margin: -12px 0;
}
.tech > summary::-webkit-details-marker {
  display: none;
}
.tech > summary::after {
  content: " +";
  opacity: 0.7;
}
.tech[open] > summary::after {
  content: " –";
}
.tech .estimates,
.tech .source {
  margin-top: 14px;
}
