/* quake-monitor frontend styles -- Phase 15.6 packet L visual rebuild (DESIGN.md §P, operator
   decision 2026-09-07). Rewritten from the PM-authored mock kit (`kit.css`, md5
   40e89d65a40239e5ab4c76c9df4ccff2, byte-identical tokens/type-scale/sheet/segment/pill rules
   below) plus `kit-ext.css`'s map/marker/legend additions, both re-targeted at this app's real
   class names (components.js/map-section.js/map.js) instead of the static mock markup. Two
   grounds (map sea, sheet), one sheet surface, no cards, no box-shadows, hairlines only
   (§P.1/§P.6). Light by default, dark via `prefers-color-scheme` only -- no toggle (§P.2).
   System fonts only; no `style=` attribute or inline `<style>` anywhere in authored JS (CSP,
   scripts/verify-frontend.py enforces this mechanically). Colour never carries a distinction
   alone -- every state also repeats as text or shape (AGENTS.md / decision 0040). */

[hidden] {
  display: none !important;
}

/* ============================================================ §P.2 tokens (kit.css, both themes) */
:root {
  color-scheme: light dark;

  /* palette (§P.2 light) */
  --ground: #DCE9EC;       /* 白藍 mix -- sea */
  --land: #FFFFFB;         /* 胡粉 */
  --pref-stroke: #A3A3A2;  /* 薄墨 */
  --sheet: rgba(252, 250, 242, 0.94);
  --sheet-solid: #FCFAF2;  /* 白練 */
  --text: #1C1C1A;         /* 墨 */
  --text-2: #4A4643;       /* 消炭 */
  --hairline: #DCD3B2;     /* 砂色 */
  --accent: #165E83;       /* 藍 -- structure, links, selection, location */
  /* DESIGN.md §P.12 R6: recent-list selected-row background, "a faint tint of the
     accent". `color-mix()` must live inside a `:root` block (check-frontend-contrast.py's
     own literal-colour scan) -- tokenised here, referenced as `var(--accent-tint)`
     elsewhere, rather than inlined at the one rule that uses it. One definition covers
     BOTH themes: the token stream is `var(--accent)` unresolved, so it re-resolves
     against whichever `--accent` is in scope (light here, dark's own override below) at
     the point a selected row is actually painted -- no separate dark-mode override
     needed. */
  --accent-tint: color-mix(in srgb, var(--accent) 10%, transparent);
  --focus: #00A3AF;        /* 浅葱 -- focus ring only in light */
  --alert: #EB6101;        /* 朱 */
  --critical: #C9171E;     /* 深緋 */
  --stale: #9E3D3F;        /* 蘇芳 */
  --live: #007B43;         /* 常磐 */
  --lagging: #B98600;
  --choropleth-opacity: 0.55;
  /* White text on a saturated 深緋/critical fill -- same in both themes (kit.css's own
     `color: #fff` on `.is-severe .primary`/`.pill.cat-warning`, tokenised here so the
     literal-colour scan in check-frontend-contrast.py stays meaningful). */
  --on-critical: #fff;
  /* Packet L implementation repair: kit.css's raw --alert/--lagging/--critical (§P.2's
     table, unchanged above -- still what every RULE bar, DOT and pill FILL uses, exactly
     as approved) do not all clear WCAG 4.5:1 when rendered as small TEXT directly on the
     sheet, which the no-cards/no-tinted-background composition (§P.1/§P.6) now does in
     several places DESIGN.md §O.4 requires to be coloured text (the lagging caution line,
     the stale headline). These four are TEXT-SAFE derived variants -- same hue family,
     adjusted lightness only -- used ONLY where this file sets `color:` for body-ish text;
     every rule bar/dot/pill-fill keeps reading the raw --alert/--lagging/--critical
     tokens above, unchanged. check-frontend-contrast.py's DECLARED_PAIRS checks these
     variants, not the raw tokens, for every text role that uses them. */
  --alert-text: #BC4E01;
  --lagging-text: #946B00;
  --critical-text: #C9171E; /* light: identical to --critical (already >=4.5 on --sheet-solid) */
  --critical-fill: #C9171E; /* light: identical to --critical (white text already >=4.5) */

  /* type scale (§P.3), rem */
  --fs-micro: 0.6875rem;
  --fs-body: 1rem;
  --fs-lead: 1.125rem;
  --fs-title: 1.5rem;
  --fs-display: 2.75rem;
  --fs-hero: 4.5rem;
  --lh-body: 1.6;
  --lh-tight: 1.1;
  --track-micro: 0.12em;

  /* spacing (px scale, §G) */
  --s1: 4px; --s2: 8px; --s3: 12px; --s4: 16px; --s5: 24px; --s6: 32px; --s7: 48px;
  --radius-sheet: 16px;
  --radius-button: 10px;
  /* Phase 15.8 packet B (residual from A): a plain `60vh` left too little room below it
     (the mobile mode-toggle row + `.sheet`'s own reserved padding, ~260px together) at
     short viewports -- 320x568 measured only ~160px left over for the detail panel's own
     first section, not enough for its heading AND first data row both to land inside the
     first viewport. `clamp()` keeps 60vh as the PREFERRED value at every ordinary phone
     height and only engages the upper bound once 100vh minus the reserved budget below
     would leave less room than 60vh already gives.
     Phase 15.8 packet C (DESIGN.md §R.6 alternative 5, binding condition 2): the budget
     grows from ~260px to ~340px -- alternative 5 adds `#area-control`'s own row (~52px:
     its 44px segment row plus the gap ahead of the mode toggle, the alternatives doc's
     own "Cost" figure) to the settings band under the map, on top of what the band
     already reserved. The floor drops from 320px to 240px (`.map-stage`'s own
     `min-height` below, lowered to match) -- condition 3's own acceptance widths (320x568
     down to 430x932) need the SHORTEST viewports to hand back more of that space to the
     sheet than a 320px floor would leave, and a phone using this page is never narrower
     than a genuinely tiny map remains usable at (the map keeps its own internal
     zoom/reset controls regardless of how short it gets). */
  --sheet-top-mobile: clamp(240px, 60vh, calc(100vh - 340px));
  --column-w: clamp(360px, 32%, 460px);
  --tap: 44px;

}

@media (prefers-color-scheme: dark) {
  :root {
    --ground: #0F1114;
    --land: #26282B;
    --pref-stroke: #4A4D52;
    --sheet: rgba(26, 28, 31, 0.92);
    --sheet-solid: #1A1C1F;
    --text: #F3EFE6;
    --text-2: #B5B0A6;
    --hairline: #33363B;
    --accent: #00A3AF;
    --focus: #F3EFE6;
    --alert: #F07A2A;
    --critical: #E0323A;
    --stale: #C96A6C;
    --live: #5CC98A;
    --lagging: #E0B93C;
    --choropleth-opacity: 0.70;
    /* dark: --alert/--lagging already clear 4.5:1 as text on --sheet-solid unchanged, so
       the "-text" variant is the same value; --critical needs LIGHTENING for text (a dark
       critical red is too close to the dark sheet), while the pill/button FILL needs the
       opposite -- DARKENING, so white text stays readable on it. */
    --alert-text: #F07A2A;
    --lagging-text: #E0B93C;
    --critical-text: #E65B61;
    --critical-fill: #CA2D35;
  }
}

* {
  box-sizing: border-box;
}

html {
  font-size: 100%; /* 1rem == 16px; body text never drops under this */
  color-scheme: light dark;
  /* No horizontal page overflow at any required width (1920/1440/768/390/360): long words
     (long Japanese place names, long English guidance sentences) always wrap. Declared on
     HTML, not body: `<body>`'s own `overflow` (when body is the ROOT SCROLLING ELEMENT,
     i.e. `<html>` has no overflow of its own) PROPAGATES to the viewport per the CSS spec,
     which also makes `<body>` itself a new scroll container/containing block for every
     `position: sticky`/`fixed` descendant -- proven wrong by a real scroll-and-measure
     check (check-frontend-responsive.py) before this moved here: `.map-stage`
     (position: sticky, §P.1) scrolled away with the page instead of staying pinned, and
     `getComputedStyle(document.body).overflowY` read back `auto` even after an explicit
     `overflow-y: visible` was declared directly on body (the propagation quirk, not a
     specificity bug). `<html>` is the element the spec actually names for this -- body is
     left with NO overflow declaration at all, so it stays the ordinary root scrolling
     element sticky/fixed positioning already expects. */
  overflow-x: hidden;
}

html, body {
  margin: 0;
  padding: 0;
  background: var(--ground);
  color: var(--text);
  font: var(--fs-body) / var(--lh-body) system-ui, -apple-system, "Hiragino Sans", "Noto Sans JP", sans-serif;
  font-variant-numeric: tabular-nums;
  -webkit-font-smoothing: antialiased;
}

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

button {
  font: inherit;
  color: inherit;
}

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

:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 2px;
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
}

/* ============================================================ type roles (§P.3, kit.css verbatim) */
.t-micro { font-size: var(--fs-micro); color: var(--text-2); }
/* PM review repair (P2-3, 2026-09-08): tracked uppercase is for SHORT LABELS (kit's own
   convention -- "Latest"/"Your area"/"Governing earthquake"), never a full sentence at
   micro size (the JMA class description, a stale "last checked ..." phrase, a caution
   sentence) -- rendering a sentence in tracked uppercase reads as shouting and is
   materially harder to read. `.no-track` is the escape hatch a caller adds to keep the
   micro SIZE (§P.8 #2 explicitly demotes the hero's class description to micro) while
   opting out of the transform; both selector branches exclude it identically. */
.t-micro.en:not(.no-track), html[lang="en"] .t-micro:not(.no-track) { text-transform: uppercase; letter-spacing: var(--track-micro); }
.t-body { font-size: var(--fs-body); }
.t-lead { font-size: var(--fs-lead); }
.t-title { font-size: var(--fs-title); line-height: var(--lh-tight); }
.t-display { font-size: var(--fs-display); line-height: var(--lh-tight); font-weight: 600; letter-spacing: -0.01em; }
.t-hero { font-size: var(--fs-hero); line-height: 1; font-weight: 600; letter-spacing: -0.02em; }
.num { font-variant-numeric: tabular-nums; }

/* JA labels stay their own size, no uppercase/tracking (§P.3) -- only EN micro labels get
   the tracked-uppercase treatment above; scope it to the JA document so a JA page never
   picks up the English rule by accident. */
html[lang="ja"] .t-micro {
  text-transform: none;
  letter-spacing: normal;
}

/* ============================================================ site header (thin bar over the map) */
/* §P.8 #3: "the mobile site bar (wordmark, language) is micro-sized and unobtrusive" --
   reskins the EXISTING Header/SiteHeaderNav component (nav links, hamburger, language
   button all unchanged in markup/behaviour) as a thin, transparent bar floating over the
   map ground, matching the kit's `.site-bar`. */
.site-header {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 30;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s3);
  padding: var(--s3) var(--s4);
  pointer-events: none;
}
.site-header > * {
  pointer-events: auto;
}

/* Phase 15.8 packet B (DESIGN.md §R.3): transparent at scroll 0 (unchanged, above); once
   the sheet has scrolled beneath it, `.is-scrolled` (components.js `useHeaderScrolled`,
   toggled from `window.scrollY`) gives the bar an opaque ground + a hairline. Scoped to
   <1200px only -- R.3 names "< 1200 px" explicitly, and at >=1200px the desktop `.sheet`
   column never reaches under the bar in the first place (it is a narrow right-docked
   `position: fixed` box, not full-width), so this rule would have no visible effect there
   even unscoped; the media query makes that a byte-identical guarantee rather than an
   incidental one, and keeps `check_scroll_stability_desktop`'s own before/after `.site-
   header` rect assertion (scripts/check-frontend-responsive.py) trivially unaffected --
   the class this rule reads never has any effect at the widths that check runs at. No
   transition is declared, so the swap is instant and identical whether or not motion is
   reduced (DESIGN.md §R.3's own "identical under reduced motion"). */
@media (max-width: 1199px) {
  .site-header.is-scrolled {
    background: var(--sheet-solid);
    border-bottom: 1px solid var(--hairline);
  }
  /* The sheet's own FIRST section must clear the now-opaque bar -- `.sheet > section:
     first-child` (below, "the sheet" comment block) already zeroes its own top padding;
     this raises it back to (a measured, safe bound on) the bar's own height, only while
     the bar is both scrolled AND at a width where it can actually cover sheet content.
     `80px` -- the site title wraps to two lines at 320-390px (PM rendered-review repair,
     2026-09-11: truncating/shrinking it was rejected), measured 75.2px there; this stays
     >= that with a small margin, the same convention `#urgent-strip`'s own `top`/
     `#area-control`'s urgent-mode push below reuse (never three independently-guessed
     numbers for the same "header's real height" fact). */
  .site-header.is-scrolled + main .sheet > section:first-child {
    padding-top: 80px;
  }
}

.site-title-group {
  display: flex;
  align-items: baseline;
  gap: var(--s2);
}
.site-header h1 {
  margin: 0;
  font-size: var(--fs-body);
  font-weight: 600;
  color: var(--text);
  /* Phase 15.8 packet B (DESIGN.md §R.3), PM rendered-review repair (2026-09-11): the site
     name must NEVER be truncated. Two earlier attempts were both rejected by direct
     measurement: `text-overflow: ellipsis` silently clipped "Earthquake Information" to
     "Earthquake Informat…" at 320-430px; shrinking the font small enough to fit one line
     at 320px (empirically, below ~9.5px) reads as illegibly small. The title wraps to (at
     most) two lines instead, exactly as it did before this packet touched it -- the
     header's own real height is measured live by every rule below that needs it (72px
     compensation, the strip's own `top`, `#area-control`'s urgent-mode push), never
     assumed to be the single-line figure. */
}
.site-region {
  font-size: var(--fs-micro);
  color: var(--text-2);
  white-space: nowrap;
}

/* DESIGN.md §P.14 item 5: one flex group for the nav toggle + language button, replacing
   `.site-header`'s own old THREE-way `space-between` spread -- see the matching comment
   on components.js's `Header`. A small `gap` (not `--s4`) is deliberate: "immediately
   left of the language toggle" is the item's own wording, checked by the responsive
   assertion as "within 56px of the language toggle's own left edge". */
.site-header-actions {
  display: flex;
  align-items: center;
  gap: var(--s2);
}

.site-nav {
  display: flex;
  align-items: center;
}
.nav-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: var(--tap);
  min-height: var(--tap);
  border: 0;
  background: transparent;
  cursor: pointer;
}
.nav-toggle-icon,
.nav-toggle-icon::before,
.nav-toggle-icon::after {
  display: block;
  width: 18px;
  height: 2px;
  background: var(--text);
}
.nav-toggle-icon {
  position: relative;
}
.nav-toggle-icon::before,
.nav-toggle-icon::after {
  content: "";
  position: absolute;
  left: 0;
}
.nav-toggle-icon::before { top: -6px; }
.nav-toggle-icon::after { top: 6px; }

/* DESIGN.md §P.14 item 5: at mobile widths the OPEN menu is a full-width sheet directly
   under the site bar -- rounded BOTTOM corners only (it visually continues the site bar
   above it, which has none of its own), a hairline TOP border only (not a full border,
   which would draw a redundant line against the site bar's own bottom edge). `position:
   absolute` here resolves against `.site-header`, its nearest POSITIONED ancestor
   (`.site-header` is itself `position: fixed`, which establishes a containing block for
   every absolutely-positioned descendant regardless of the extra `.site-nav`/
   `.site-header-actions` wrapper levels between them, per the CSS positioning spec) --
   `left: 0; right: 0` therefore spans exactly `.site-header`'s own full width, which
   itself spans the full viewport (`inset: 0 0 auto 0`, above), giving "menu width equals
   viewport width" without this rule needing to know the viewport size itself. */
.nav-list {
  display: none;
  list-style: none;
  margin: 0;
  padding: var(--s3) var(--s4);
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  width: 100%;
  background: var(--sheet-solid);
  border-radius: 0 0 var(--radius-sheet) var(--radius-sheet);
  border-top: 1px solid var(--hairline);
}
.nav-list.is-open {
  display: block;
}
.nav-link {
  display: block;
  padding: var(--s2) var(--s3);
  min-height: var(--tap);
  color: var(--text);
  font-size: var(--fs-body);
  white-space: nowrap;
}
.nav-link[aria-current="page"] {
  color: var(--accent);
  font-weight: 600;
}

.lang-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  /* PM render review repair (Phase 15.7 packet C): as a flex item inside
     .site-header-actions, a shrinkable <a> lets Gecko break the CJK label
     ("日本語") between characters onto two lines once the flex row is tight
     (observed at 390px: "日本" / "語") -- a <button> never exhibited this because
     it always got the same implicit UA minimum inline size the min-width repair
     above now sets explicitly, but that minimum did not by itself stop wrapping.
     white-space: nowrap keeps the label on one line at every width this header
     supports; flex-shrink: 0 stops the flex row from trying to compress it below
     its content size in the first place. */
  white-space: nowrap;
  font-size: var(--fs-micro);
  letter-spacing: var(--track-micro);
  text-transform: uppercase;
  min-height: var(--tap);
  /* Phase 15.7 packet C (decision 0072 section 5) repair: a <button> gets an
     implicit UA-stylesheet minimum inline size in Gecko that a plain <a> does not --
     found by scripts/check-frontend-responsive.py's own touch-target check
     (#lang-switch measured 38.3x70.8px at 768px, height satisfied by min-height alone
     but width was not) once the toggle became a real link. min-width makes the 44px
     floor explicit on BOTH axes regardless of element type, rather than depending on
     a browser default this element no longer gets.
  */
  min-width: var(--tap);
  padding: var(--s2) var(--s3);
  border: 1px solid var(--hairline);
  border-radius: 999px;
  background: var(--sheet);
  color: var(--text);
  cursor: pointer;
  /* Phase 15.7 packet C (decision 0072 section 5): now a real <a href> to the
     counterpart document (was a <button>) -- same position/appearance either way, so the
     global `a`/`a:hover` rules (accent colour, underline-on-hover) are explicitly
     overridden here rather than relied on to happen to look right. */
  text-decoration: none;
}
.lang-btn:hover {
  text-decoration: none;
}

@media (min-width: 768px) {
  .nav-list {
    display: flex;
    position: static;
    width: auto;
    background: none;
    border: 0;
    padding: 0;
    gap: var(--s3);
  }
  .nav-toggle {
    display: none;
  }
}

/* ============================================================ freshness / alert bar (non-home pages) */
.freshness {
  display: flex;
  align-items: center;
  gap: var(--s2);
  padding: var(--s3) var(--s4);
  font-size: var(--fs-micro);
  border-bottom: 1px solid var(--hairline);
}
.freshness-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  flex: none;
}
.freshness.is-live .freshness-dot { background: var(--live); }
.freshness.is-lagging .freshness-dot { background: var(--lagging); }
.freshness.is-stale .freshness-dot,
.freshness.is-unknown .freshness-dot { background: var(--stale); }
.freshness-label { font-weight: 600; }
.freshness-detail { color: var(--text-2); }

.alert-bar {
  padding: var(--s3) var(--s4);
  border-bottom: 1px solid var(--hairline);
  font-size: var(--fs-lead);
  font-weight: 600;
}
.alert-bar-link {
  display: block;
  color: inherit;
}
.alert-bar.is-alert { color: var(--alert-text); }
.alert-bar.is-critical { color: var(--critical-text); }

/* Non-home pages have no map ground -- the header bar sits in normal flow instead of
   floating fixed, so it needs its own solid background and normal padding-top compensation
   further down (`.layout-grid` rule, below). */
.layout-grid:not(.layout-grid-home) .site-header,
body:not([data-page="home"]) .site-header {
  position: static;
  background: var(--sheet-solid);
  border-bottom: 1px solid var(--hairline);
}

/* ============================================================ map stage (§P.1/§P.5) */
/* Packet L implementation repair: the map is now the page's full-bleed ground/canvas at
   EVERY width (§P.1), not only >=1200px as the pre-packet-L Package B composition had it
   -- #map-section's own h2 heading (map-section.js, unchanged markup, kept in the
   accessibility tree as a real landmark name) is therefore sr-only UNCONDITIONALLY now,
   never a visible heading pushing .map-stage/the sticky offset down at any width (a
   regression this packet's own responsive checker caught: an unstyled heading pushed
   .map-stage ~70px down from the true viewport top on mobile, and inflated the document's
   scrollable height enough to put a vertical scrollbar between .map-stage's fixed rect and
   the full desktop viewport width). Reuses the exact .sr-only clip-rect technique. */
.map-heading {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.map-stage {
  position: sticky;
  top: 0;
  height: var(--sheet-top-mobile);
  /* Phase 15.8 packet C (DESIGN.md §R.6 alternative 5, binding condition 2): lowered from
     320px to match `--sheet-top-mobile`'s own new clamp floor (above) -- the two must
     agree, or the clamp's own floor would be unreachable. */
  min-height: 240px;
  z-index: 0;
  overflow: hidden;
}
.map-container,
.map-stage .map-svg,
.map-stage svg {
  width: 100%;
  height: 100%;
  display: block;
}
.map-container {
  position: relative;
  width: 100%;
  height: 100%;
  background: var(--ground);
}
.map-svg {
  cursor: grab;
  touch-action: none;
}
.map-svg:active {
  cursor: grabbing;
}

.map-error {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--s4);
  text-align: center;
  color: var(--text-2);
  background: var(--ground);
}

@media (min-width: 1200px) {
  .map-stage {
    position: fixed;
    inset: 0;
    /* PM render review repair (Phase 15.7 packet C, BLOCKING 1, PM-directed bisection):
       `inset: 0` alone sizes a `position: fixed` box against the CSS "layout viewport"
       (`document.documentElement.clientWidth` in Gecko), which EXCLUDES a vertical
       scrollbar's own track once one exists -- proven directly: at 1440x900 with
       #site-about present (decision 0072's static lead, below #app in body flow, making
       the document taller than one viewport for the first time -- main, without it,
       never needed this scrollbar and so never hit this), `.map-stage` measured
       1428x814 against a real 1440x814 window, a 12px dead strip at the right edge
       where the map no longer painted. `width: 100vw` is the standard technique for
       exactly this case (the one CSS keeps distinct from `100%`/`inset:0` sizing for
       this reason): viewport-percentage units are defined against the INITIAL viewport,
       which does NOT shrink for a scrollbar, so `100vw` here reliably equals
       `window.innerWidth` whether or not a scrollbar is present (verified: 1440 in both
       cases) -- `right: 0` from the `inset: 0` shorthand above is legitimately
       over-constrained and dropped in favour of the explicit `width` (CSS2.1 §10.3.7,
       LTR case), `left: 0` is unaffected. This is the map's own full-bleed background
       canvas correctly extending under the scrollbar gutter, not new map.js/
       map-section.js behaviour -- neither file is touched. */
    width: 100vw;
    height: 100vh;
    min-height: 0;
  }
}

/* Land / prefecture strokes (§P.5: 0.75px). `fill` lives ONLY on the `:not(.prefecture-filled)`
   variant -- an ordinary class rule on the base selector would always beat map.js's
   `setPrefectureFills` presentation attribute regardless of specificity (a presentation
   attribute is the lowest-priority cascade origin), which is exactly the choropleth defect
   Phase 15.6 packet E fixed; `scripts/check-frontend-render.py` asserts the computed fill on
   a filled path still traces back to the JMA ramp, never this base rule. */
.prefecture-path {
  stroke: var(--pref-stroke);
  stroke-width: 0.75;
  vector-effect: non-scaling-stroke;
}
.prefecture-path:not(.prefecture-filled) {
  fill: var(--land);
}
.prefecture-path.prefecture-filled {
  stroke: var(--pref-stroke);
  stroke-width: 1;
  vector-effect: non-scaling-stroke;
}

/* Coastline emphasis (decision 0069 §7, DESIGN.md §O.6/§O.10 item 8). `stroke` on
   `.coast-emphasis-line` is a PRESENTATION ATTRIBUTE set by map.js's `setCoastEmphasis`
   (category colour, never a literal here); the casing is always the sheet's own solid tone
   so the line reads over sea and land alike. */
#map-coast-emphasis .coast-emphasis-casing {
  stroke: var(--sheet-solid);
  stroke-width: 5px;
  stroke-linecap: round;
  vector-effect: non-scaling-stroke;
}
#map-coast-emphasis .coast-emphasis-line {
  stroke-width: 3px;
  stroke-linecap: round;
  vector-effect: non-scaling-stroke;
}

#map-markers circle {
  cursor: pointer;
}

/* ---- DESIGN.md §P.11 marker system C: the pulsing latest-epicentre dot (supersedes
   §O.11's reticle for the latest epicentre ONLY -- identity/favicon is the separate §P.10
   spike mark). ONE animated ring (r LATEST_PULSE_MIN_R->18, opacity 0.9->0, ease-out,
   infinite, 1.6s -- well under the 3-flashes-per-second seizure threshold) plus a small
   JMA-coloured dot; under `prefers-reduced-motion: reduce` the ring animation stops and a
   static thin halo ring (r 9.5, 0.6 opacity) takes over instead, so a still (a screenshot,
   or a reduced-motion visitor) can still tell "latest" apart from history. Firefox/Chromium
   both support animating the SVG geometry property `r` as a CSS property (SVG2); no JS
   per-frame fallback is needed.
   PM review repair (R3, 2026-09-08 -- operator real-device review #1, second pass): the
   ring's initial radius was 6 (map.js LATEST_PULSE_MIN_R, then just "6") -- barely bigger
   than the 5.5px dot it surrounds, so on a real render (`normal-1440`/`normal-450`) the two
   merged into one heavy blue blob at the animation's early phase. The keyframe's own 0%
   value is kept in lockstep with map.js's `LATEST_PULSE_MIN_R` constant (dot radius + 3px)
   -- both must change together if either does; `check-frontend-marker-a11y.py` asserts the
   SVG attribute's own initial `r` (read before any animation has had a chance to run) is
   >= dot radius + 3, which is the value actually painted at t=0 regardless of which of the
   two places is the "true" source for a reader of this comment alone. */
@keyframes marker-latest-pulse {
  0% { r: 8.5; stroke-opacity: 0.9; }
  100% { r: 18; stroke-opacity: 0; }
}
#map-markers .marker-latest .latest-dot {
  stroke: var(--text);
  stroke-width: 1.4px;
  vector-effect: non-scaling-stroke;
}
#map-markers .marker-latest .latest-pulse-ring {
  fill: none;
  stroke: var(--text);
  stroke-width: 1.5px;
  stroke-opacity: 0.9;
  vector-effect: non-scaling-stroke;
  animation: marker-latest-pulse 1.6s ease-out infinite;
}
/* Reduced-motion static halo: present in the DOM unconditionally (map.js) but hidden by
   default -- shown ONLY under prefers-reduced-motion, which also stops (and hides) the
   animated ring below it. */
#map-markers .marker-latest .latest-halo {
  display: none;
  fill: none;
  stroke: var(--text);
  stroke-width: 1px;
  stroke-opacity: 0.6;
  vector-effect: non-scaling-stroke;
}
@media (prefers-reduced-motion: reduce) {
  #map-markers .marker-latest .latest-pulse-ring {
    animation: none;
    display: none;
  }
  #map-markers .marker-latest .latest-halo {
    display: block;
  }
}
#map-markers .marker-latest .latest-selected-ring {
  display: none;
  fill: none;
  stroke: var(--accent);
  stroke-width: 3px;
  vector-effect: non-scaling-stroke;
}
#map-markers .marker-latest.marker-selected .latest-selected-ring {
  display: block;
}

/* LATEST/最新 label, >=1200px only (map.js buildLatestLabel). */
#map-markers .marker-latest .latest-label {
  display: none;
  pointer-events: none;
}
@media (min-width: 1200px) {
  #map-markers .marker-latest .latest-label {
    display: block;
  }
}
#map-markers .marker-latest .latest-label-bg {
  fill: var(--sheet-solid);
  fill-opacity: 0.92;
  stroke: var(--pref-stroke);
  stroke-width: 1px;
  vector-effect: non-scaling-stroke;
}
#map-markers .marker-latest .latest-label-text {
  fill: var(--text-2);
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  dominant-baseline: middle;
}

#map-markers circle.marker-selected {
  stroke: var(--accent);
  stroke-width: 3px;
  vector-effect: non-scaling-stroke;
}

/* History dots (kit-ext `.hist-dot`, this app's `.map-marker-epicentre`): JMA ramp fill (a
   presentation attribute, unchanged), thin theme-text stroke, age-fade opacity (also a
   presentation attribute, floor 35% -- unchanged map.js behaviour). */
.map-marker-epicentre {
  stroke: var(--text);
  stroke-width: 0.6;
  vector-effect: non-scaling-stroke;
}

/* DESIGN.md §P.13 item 1 (operator real-device review #4): Chromium paints CSS
   `outline` on SVG elements in the ELEMENT's OWN user coordinate system, not screen
   pixels -- Firefox paints it in screen pixels, which is why no Tier-1 checker (Firefox
   only, decision 0001) ever saw this. The site-wide `:focus-visible { outline: 3px
   solid var(--focus); outline-offset: 2px; }` rule (top of this file) therefore becomes
   a roughly (3px / the element's own internal scale) DISC once it lands on something
   inside a scaled `<g>` -- confirmed as the exact mechanism: `.marker-latest`'s own
   internal `scale(...)` transform (e.g. `scale(0.033)`, i.e. an inverse blow-up of
   ~30x) turns a nominal 3px outline into the operator's own reported "~100px disc".
   The ordinary epicentre circles already had `outline: none` (this packet's own
   predecessor, packet K a11y) -- but NOTHING previously suppressed it on
   `.marker-latest` ITSELF (only shown/hidden the SEPARATE `.latest-selected-ring`
   element's ring), which is the ACTUAL element that receives focus for the latest
   marker and is exactly the one wrapped in that scaling transform. `outline: none` on
   plain `:focus` (not only `:focus-visible`) covers every focus state uniformly -- a
   `:focus-visible` match is ALWAYS also a `:focus` match by definition, so this
   suppresses the outline unconditionally regardless of which heuristic a given browser
   uses to decide whether a particular focus event counts as "visible". The RING itself
   (a real geometry element in the SVG's own viewBox coordinate system, not a CSS
   outline) is unaffected by any of this and stays the sole focus/selection indicator,
   `vector-effect="non-scaling-stroke"` (packet L4) keeping its own stroke width correct
   regardless of zoom. Every focusable element inside the map SVG gets the SAME
   `outline: none` treatment, including ones not currently focusable (the self-location
   group, the `<svg>` itself) as a defensive measure against this exact class of defect
   recurring if either ever becomes focusable later. */
#map-markers circle.map-marker-epicentre:focus,
#map-markers .marker-latest:focus,
#map-markers .marker-self-location:focus,
.map-marker-self:focus,
#map:focus {
  outline: none;
}
/* The ring itself stays gated on `:focus-visible` specifically (unchanged from
   before this packet) -- a plain mouse-click focus should not paint the SAME loud
   keyboard-navigation ring a Tab press does; only the OUTLINE suppression above needs
   to be unconditional. */
#map-markers circle.map-marker-epicentre:focus-visible {
  stroke: var(--focus);
  stroke-width: 3px;
  vector-effect: non-scaling-stroke;
}
#map-markers .marker-latest:focus-visible .latest-selected-ring {
  display: block;
  stroke: var(--focus);
}
#map-markers circle.map-marker-epicentre {
  transition: opacity 0.15s ease;
}
#map-markers.has-selection circle.map-marker-epicentre:not(.marker-selected) {
  opacity: 0.35;
}
@media (prefers-reduced-motion: reduce) {
  #map-markers circle.map-marker-epicentre {
    transition: none;
  }
}

/* Current-location marker (dot + accuracy disc), drawn above every event marker. */
.map-marker-self {
  pointer-events: none;
}
circle.user-accuracy-disc {
  fill: var(--accent);
  fill-opacity: 0.14;
  stroke: var(--accent);
  stroke-width: 0.8;
  stroke-opacity: 0.5;
  vector-effect: non-scaling-stroke;
}
/* DESIGN.md §P.11: r shrunk to ~4px (map.js USER_DOT_R) and stroke to 1.6px -- smaller than
   the latest-epicentre dot, one of the discrimination cues (never hue alone). */
circle.user-dot {
  fill: var(--accent);
  stroke: var(--sheet-solid);
  stroke-width: 1.6;
  vector-effect: non-scaling-stroke;
}

/* ============================================================ map-docked controls (013-style
   equal outlined segments, §P.4/§P.5, kit-ext `.map-top-controls`/`.map-bottom-controls`) */
/* Phase 15.8 packet C (DESIGN.md §R.6 alternative 5, PM choice record
   docs/evidence/phase-15.8/p15.8-packet-c-alternatives.md, binding condition 1): below
   1200px `#area-control` is no longer docked over the map -- it is an ordinary row in
   `.map-settings-band` (map-section.js), in normal document flow directly under
   `.map-stage`. The old unconditional `position: absolute; top: 64px` (which docked it
   over the map's top edge at EVERY width) is retired along with the mode-gated
   `top: 80px`/`top: 160px` pushes that used to keep it clear of the site bar and the
   urgent strip (both were needed only because this control used to overlay the map;
   neither applies to an in-flow row) -- see the `@media (min-width: 1200px)` block below
   for the desktop re-dock, which reproduces the exact pre-packet-C geometry. */
/* Phase 15.8 packet C follow-up (PM rendered review of c34cfec, 2026-09-11): the band
   measured 145px (65 = a 44px segment row + 21px of `.area-control`'s own padding; 80 =
   a 44px toggle row + 36px of `.map-mode-mobile-wrap`'s own padding), against a 104px
   bound (44px rows + an 8px gap between them + <=8px top/bottom padding, nothing else).
   `.area-control`'s own vertical padding drops to a single 8px top value (0 bottom); the
   8px row-to-row gap moves to `.map-settings-band`'s own `gap` (below) instead of living
   inside either child's padding; `.map-mode-mobile-wrap`'s OWN vertical padding drops to
   0 (see its own rule) -- the sheet-overlap clearance that padding used to also provide
   moves to `.map-settings-band`'s own `margin-bottom` (an external margin, never counted
   in either child's own measured height, so it does not reopen the 104px budget). */
.area-control {
  padding: var(--s2) var(--s4) 0;
}
@media (min-width: 1200px) {
  .area-control {
    position: fixed;
    top: 64px;
    left: var(--s3);
    right: var(--s3);
    padding: 0;
    z-index: 5;
  }
}
/* Phase 15.8 packet C follow-up: the 8px gap between the two settings-band rows lives
   here (a flex `gap`, never a child's own padding) -- see `.area-control`'s own comment
   above for the full 104px budget accounting. `margin-bottom: var(--radius-sheet)`
   (16px) exactly cancels `.sheet`'s own `margin-top: calc(-1 * var(--radius-sheet))`
   pull (below, "the sheet" section) -- an EXTERNAL margin on the band itself, so the
   sheet's rounded top edge lands precisely at the band's own bottom border and never
   overlaps real content (the toggle row's own buttons), without needing any of the
   internal clearance padding `.map-mode-mobile-wrap` used to carry for the same reason
   (see that rule's own comment). At >=1200px the band is already empty (AreaControl is
   `position: fixed`, out of flow; the toggle is JS-gated off entirely) -- the margin is
   zeroed there too, so it can never add stray vertical space to the desktop column. */
.map-settings-band {
  display: flex;
  flex-direction: column;
  gap: var(--s2);
  margin-bottom: var(--radius-sheet);
}
@media (min-width: 1200px) {
  .map-settings-band {
    margin-bottom: 0;
  }
}
.area-segments {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  gap: var(--s2);
  max-width: 420px;
}
.segment,
.area-segment {
  min-height: var(--tap);
  min-width: var(--tap);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-button);
  background: var(--sheet);
  color: var(--text);
  font-size: var(--fs-body);
  padding: 0 var(--s3);
  cursor: pointer;
}
.segment[aria-pressed="true"],
.area-segment[aria-pressed="true"] {
  border-color: var(--accent);
  color: var(--accent);
  font-weight: 600;
}
/* Phase 15.8 packet C follow-up: below 1200px (the SAME breakpoint the settings band
   itself is scoped to -- AreaControl is an in-flow row only there; at >=1200px it is a
   fixed-position overlay with its own 420px max-width container, where this row never
   wraps at all) "Japan-wide"/"Use my location" wrap to a second line, which (because
   `.area-segments` is a CSS grid, whose implicit row sizes to its TALLEST cell)
   stretches EVERY segment in the row to match, well past the 44px target -- first
   measured at 390px ("Japan-wide" only) and again at 430px ("Use my location", a longer
   string, still wraps there even though "Japan-wide" no longer does) -- rather than
   picking two separate ad-hoc breakpoints for two different strings, one rule covers the
   row's entire in-flow range. Scoped to `.area-segment` only (never the shared `.segment`
   class `#map-controls`' own zoom/focus buttons also use) so the map's own controls are
   untouched. Declared AFTER the base `.segment, .area-segment` rule above (same
   specificity; source order decides, and a media query does not raise it) so this
   override actually wins. */
@media (max-width: 1199px) {
  .area-segment {
    font-size: 13px;
  }
}
.area-manual {
  margin-top: var(--s2);
}
.area-manual-label {
  display: block;
  font-size: var(--fs-micro);
  color: var(--text-2);
  margin-bottom: var(--s1);
}
.area-manual-select {
  min-height: var(--tap);
  width: 100%;
  max-width: 320px;
  border: 1px solid var(--hairline);
  border-radius: var(--radius-button);
  background: var(--sheet-solid);
  color: var(--text);
  font-size: var(--fs-body);
  padding: 0 var(--s3);
}
.area-status {
  margin: var(--s2) 0 0;
  font-size: var(--fs-micro);
  color: var(--text-2);
  background: var(--sheet);
  display: inline-block;
  padding: var(--s1) var(--s2);
  border-radius: var(--radius-button);
}

#map-controls,
.map-controls {
  position: absolute;
  bottom: 24px;
  left: var(--s3);
  right: var(--s3);
  z-index: 5;
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-start;
  align-items: flex-end;
  gap: var(--s2);
}
/* Phase 15.8 packet C follow-up round 2 (PM decision, comparing the implementer's own
   synthetic-fixture trade-off table against the PM's own live-snapshot measurements): the
   first attempt docked bottom-right at EVERY width below 1200px -- correct only at the
   SHORTEST maps. At the 240px mobile map height (320px width, below `--sheet-top-mobile`'s
   own clamp floor) the zoom/reset row's own bottom-LEFT dock sits directly over Kyūshū --
   but from 360px up the map is already tall enough (`--sheet-top-mobile`'s `60vh`/
   `calc(100vh - 340px)` branches take over) that Kyūshū sits ABOVE the row there, and
   docking bottom-right at those widths instead covers the Izu-Ogasawara arc's own southern
   tip for no offsetting benefit -- exactly the net-negative trade the implementer's own
   evidence flagged as unproven for 360-430px. The dock is therefore bottom-RIGHT ONLY
   below 360px (`left: auto` so `right` alone anchors it, `justify-content: flex-end` so
   the segments still hug that edge as the row wraps/grows); 360px and up keep the
   ORIGINAL, unconditional bottom-LEFT rule above unmodified. "Reset view" keeps its own
   visible text label unconditionally either way (app.css never hides it; only the
   desktop-only `.focus-btn`, below, is width-gated). Desktop (>=1200px) is UNCHANGED. */
@media (max-width: 359px) {
  #map-controls,
  .map-controls {
    left: auto;
    right: var(--s3);
    justify-content: flex-end;
  }
}
.map-controls .segment {
  padding: 0 var(--s2);
}
.map-controls .focus-btn {
  display: none;
  white-space: nowrap;
}
.map-mode {
  display: flex;
  gap: var(--s2);
}

@media (min-width: 1200px) {
  #map-controls,
  .map-controls {
    bottom: var(--s3);
    max-width: calc(100% - var(--column-w) - var(--s5) * 2);
  }
  .map-controls .focus-btn {
    display: inline-flex;
    align-items: center;
  }
}

/* Mobile-only recent/all toggle row directly under the map (a real functional control the
   static mock never depicted, kept in normal flow -- never overlaying the map).
   PM re-review repair (2026-09-08): before the map-mode-note was made sr-only (§P.8/P2-6
   repair), its own trailing text (margin-top var(--s2) + a line of micro text) supplied
   enough bottom space that .sheet's negative top margin (calc(-1 * var(--radius-sheet)),
   below) only ever ate into THAT text, never the segment buttons above it. Removing the
   note from flow (sr-only has zero layout height) shortened this block by that same
   amount, so the sheet's -16px pull now reached up into the segment row itself --
   verified in the -light-tall screenshots (normal/stale/significant-severe), the sheet's
   rounded top visibly overlapping the bottom half of "最近5時間 / 過去7件". The sheet's
   overlap must only ever cover MAP GROUND, never a control (§P.1).
   Phase 15.8 packet C follow-up: the bottom-padding clearance this comment used to
   describe (radius-sheet + s2, 24px) moves to `.map-settings-band`'s own `margin-bottom`
   (above) -- the PM's 104px band budget has no room for internal clearance padding on
   top of the 44px row it would otherwise inflate. This element carries NO vertical
   padding of its own any more (horizontal only, for the toggle buttons' own left/right
   inset); the claim above ("the sheet's overlap must only ever cover map ground, never a
   control") still holds -- it is now proven by the parent's external margin exactly
   cancelling the pull, rather than by padding here absorbing it. */
.map-mode-mobile-wrap {
  padding: 0 var(--s4) 0;
}
.map-mode-mobile-wrap .map-mode {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
}
.map-mode-mobile-wrap .map-mode button {
  min-height: var(--tap);
  border: 1px solid var(--hairline);
  background: var(--ground);
  color: var(--text);
  border-radius: var(--radius-button);
}
.map-mode-mobile-wrap .map-mode button[aria-pressed="true"] {
  border-color: var(--accent);
  color: var(--accent);
  font-weight: 600;
}
.map-mode-note {
  margin: var(--s2) 0 0;
  font-size: var(--fs-micro);
  color: var(--text-2);
}
@media (min-width: 1200px) {
  .map-mode-mobile-wrap {
    display: none;
  }
}

/* ============================================================ urgent status strip
   (DESIGN.md §R.4/§R.5, Phase 15.8 packet B) */
/* `position: fixed`, pinned directly under the site bar (56px -- the same assumed bar
   height used above and by `.sheet.page-sheet`, below), over the map: components.js only
   ever mounts this section for SIGNIFICANT/TSUNAMI at <1200px (`UrgentStrip`'s own
   `useMobileStripWidth`), so the `@media (min-width: 1200px)` rule below is a belt-and-
   braces guarantee, never the ONLY thing keeping it off the desktop column. `z-index: 20`
   sits above `.map-stage` (0) and its docked controls (5, `.area-control`/`.map-controls`
   above) but below `.site-header` itself (30), so the bar always paints over this strip,
   never under it, if the two ever visually met. */
.urgent-strip {
  position: fixed;
  top: 76px;
  left: 0;
  right: 0;
  z-index: 20;
  background: var(--sheet-solid);
  border-bottom: 1px solid var(--hairline);
}
@media (min-width: 1200px) {
  .urgent-strip {
    display: none;
  }
}
.urgent-strip-tap {
  display: flex;
  flex-direction: column;
  justify-content: center;
  width: 100%;
  min-height: var(--tap);
  border: 0;
  background: transparent;
  color: var(--text);
  text-align: left;
  padding: 3px var(--s3);
  font-family: inherit;
  cursor: pointer;
}
.urgent-strip-term {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--s2);
  line-height: var(--lh-tight);
}
/* PM rendered-review repair (2026-09-11): the JA+EN bilingual pair (`-ja`/`-en`) is now
   used ONLY for the official tsunami category term (decision 0070) -- components.js's own
   `UrgentStrip` comment records why a second, always-present EN span for the SIGNIFICANT
   hero term and the stale/unknown wording pushed the term row itself to two lines for a
   long EN string (measured: the `stale` fixture's own row, 76px total against the 56px
   target). `-single` is the current-document-language-only term those two cases use
   instead -- same visual weight as the bilingual pair's own JA span, one line. Each
   bilingual span stays non-wrapping itself; the flex row above may still wrap the EN span
   onto its own new line when the OFFICIAL term's own EN name does not fit beside its JA
   name (measured residual: `major_warning`, 320px only -- see the evidence file). */
.urgent-strip-term-ja,
.urgent-strip-term-en,
.urgent-strip-term-single {
  white-space: nowrap;
}
.urgent-strip-term-ja,
.urgent-strip-term-single {
  font-size: var(--fs-body);
  font-weight: 600;
}
.urgent-strip-term-en {
  font-size: var(--fs-micro);
  color: var(--text-2);
}
/* PM rendered-review repair (2026-09-11, round 3): the action line must never be
   silently truncated (an earlier single-line `text-overflow: ellipsis` dropped the
   actual instruction, e.g. "...to safer places ..." with the rest of the sentence
   gone). It wraps normally, at a compact size with a tight line-height -- the box is
   never `overflow: hidden`/height-clamped, so a longer source sentence grows the strip
   rather than losing text. Dropped from 12px to 11px in this round specifically to
   bring tsunami-warning's/tsunami-major's own category action line (a single
   ~118-character JMA sentence with no natural break) under the >=360px 56px target
   (measured 53.1px at 360/390px after this change, term-policy fix included); at
   320px that same line still takes 3 lines (measured 66.85px, inside the PM-approved
   72px exception for that one width) -- documented, not silently hidden. Padding
   dropped from `var(--s4)` to `var(--s3)` on `.urgent-strip-tap` (below) for the
   same reason; combined, neither change alone was sufficient (see the evidence file
   for the isolated deltas). */
.urgent-strip-action {
  display: block;
  margin-top: 1px;
  font-size: 11px;
  color: var(--text-2);
  line-height: 1.25;
}
/* DESIGN.md §R.5: 朱 on the TSUNAMI strip's own term, warning/major_warning only -- the
   SAME (--alert-text, --sheet-solid) pair `.tsunami-region-pill.is-alert` already declares
   in scripts/check-frontend-contrast.py's DECLARED_PAIRS (this strip's own background is
   the same --sheet-solid token), so no new declared pair is needed. */
.urgent-strip.is-alert .urgent-strip-term-ja,
.urgent-strip.is-alert .urgent-strip-term-en {
  color: var(--alert-text);
}

/* Phase 15.8 packet C (DESIGN.md §R.6 alternative 5, binding condition 1): the mode-gated
   `#area-control` push this comment used to document (`top: 80px` calm / `top: 160px`
   SIGNIFICANT-TSUNAMI, clearing the site bar and the urgent strip respectively) is
   RETIRED, not merely left at zero -- `#area-control` no longer overlays the map or the
   bar at any width below 1200px (it is an ordinary row in `.map-settings-band`, under the
   map, map-section.js), so there is nothing left for either offset to clear. The strip
   now overlays the map ONLY (DESIGN.md §R.4, unchanged); the two never occupy the same
   screen region any more, calm or urgent. See `.area-control`'s own rule above (the "map-
   docked controls" section) for the current, width-gated geometry. */

/* ============================================================ the sheet (§P.1/§P.4) */
.sheet {
  position: relative;
  z-index: 10;
  margin-top: calc(-1 * var(--radius-sheet));
  background: var(--sheet);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  border-radius: var(--radius-sheet) var(--radius-sheet) 0 0;
  padding: var(--s5) var(--s4) var(--s7);
}
.sheet > section,
.sheet > div.hero,
.sheet > footer {
  padding: var(--s4) 0;
  border-top: 1px solid var(--hairline);
}
.sheet > section:first-child {
  border-top: 0;
  padding-top: 0;
}

/* Non-home pages: a plain `.sheet` on the flat ground, no map, normal document flow (§P.1's
   "sheet on a plain ground, no map"). */
.sheet.page-sheet {
  position: static;
  margin: 0;
  border-radius: 0;
  max-width: 760px;
  margin: 0 auto;
  padding-top: calc(56px + var(--s5));
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  background: transparent;
}
body:not([data-page="home"]) {
  background: var(--ground);
}

@media (min-width: 1200px) {
  /* Phase 15.7 packet C repair: `position: absolute` here resolves against the initial
     containing block (no ancestor of `.sheet` between it and `<body>` is positioned --
     `#app`, `.layout-grid`/`.layout-grid-home` and `main` are all `position: static`), which
     is the FULL scrollable canvas, not the viewport -- so the column scrolled WITH the
     page once `#site-about` (decision 0072's static lead, below `#app` in body flow) gave
     the document a scroll range for the first time at >=1200px. `.site-header` and
     `.map-stage` never had this problem because they are themselves `position: fixed`,
     which is always viewport-relative. `position: fixed` here is the matching fix: same
     containing block as those two (the viewport), so `top`/`right`/`width`/`max-height`/
     `overflow` below are unchanged and the column's rect is now identical before and after
     scroll, proven by scripts/check-frontend-responsive.py's scroll-stability assertion. */
  .sheet:not(.page-sheet) {
    position: fixed;
    top: 72px;
    right: var(--s5);
    width: var(--column-w);
    margin: 0;
    border-radius: var(--radius-sheet);
    padding: var(--s5);
    max-height: calc(100vh - 96px);
    overflow: auto;
  }
  .sheet.page-sheet {
    padding-top: calc(72px + var(--s5));
  }
}

/* ============================================================ state header (§P.3/§P.4/§P.8 #1) */
.state {
  display: grid;
  gap: var(--s1);
}
.state .rule {
  height: 3px;
  margin-bottom: var(--s3);
  background: var(--text-2);
}
.state.is-attention .rule { background: var(--alert); }
.state.is-severe .rule,
.state.is-tsunami .rule,
.tsunami-block .rule { background: var(--critical); }
.state.is-stale .rule { background: var(--stale); }
.state-headline-word.word {
  display: flex;
  align-items: baseline;
  gap: var(--s3);
  margin: 0;
}
.state-headline-glyph.glyph {
  font-size: var(--fs-title);
  line-height: 1;
}
.state .meta {
  display: flex;
  gap: var(--s3);
  align-items: center;
}
.state .dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  display: inline-block;
}
.state .dot.live { background: var(--live); }
.state .dot.lagging { background: var(--lagging); }
.state .dot.stale, .state .dot.unknown { background: var(--stale); }
.state-headline-caution {
  margin: var(--s2) 0 0;
  color: var(--lagging-text);
}

/* Stale/unknown overlay (0068 rule 4, DESIGN.md §O.10 item 5) -- kept a full header in
   every mode, unaffected by §P.8 #1 (which names NORMAL/ORDINARY only). */
.state.is-stale .state-headline-word {
  color: var(--stale);
  margin: 0;
}
.state-headline-last-known-label {
  margin: var(--s2) 0 0;
}
.state-headline-secondary {
  margin: var(--s1) 0 0;
}

/* §P.8 #1: NORMAL/ORDINARY show NO glyph/word -- one lead line instead (StateHeadline's
   `NewestLine` branch). Same `#state-headline` id, no `.state` wrapper class at all. */
.newest-line p {
  margin: var(--s1) 0;
}

/* ============================================================ hero row (§P.3/§P.4/§P.8 #2) */
.hero {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--s4);
  align-items: end;
  padding-top: var(--s4);
}
.hero .shindo {
  display: flex;
  align-items: baseline;
  gap: var(--s2);
}
.hero .shindo .t-hero {
  color: var(--text);
}
.hero .shindo .unit {
  font-size: var(--fs-lead);
  color: var(--text-2);
}
.hero .now-decision-lead {
  margin: 0 0 var(--s2);
}
.hero .hero-class-desc {
  margin: 0;
}

/* ============================================================ tsunami line / block */
.tsunami-line .section-heading {
  margin: 0 0 var(--s1);
}
.tsunami-text,
.tsunami-attribution {
  margin: var(--s1) 0;
}
.tsunami-line.is-micro .tsunami-text,
.tsunami-line.is-micro .tsunami-attribution {
  font-size: var(--fs-micro);
  color: var(--text-2);
}
.tsunami-line:not(.is-micro) .tsunami-text {
  font-size: var(--fs-body);
}
/* Packet L: kit.css never tints .tsunami-line text by tone (only the 3px .rule bar and
   pills carry colour, DESIGN.md §O.4's "colour role: ... on headline rule and action
   button", never body text) -- the category NAME/wording already carries the
   distinction, so this stays plain --text/--text-2, matching the mock exactly. */
.tsunami-line.is-active .tsunami-text,
.tsunami-line.is-forecast .tsunami-text { font-weight: 600; }

.tsunami-block .tsunami-block-category {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--s2);
  margin: 0 0 var(--s1);
}
.tsunami-block-glyph {
  font-size: var(--fs-title);
}
.tsunami-block-ja {
  font-size: var(--fs-display);
  line-height: var(--lh-tight);
  font-weight: 600;
  /* Phase 15.8 packet B: the JA term ("津波警報" etc.) must never wrap mid-word -- the
     flex row above already allows the (longer) EN term to wrap onto its own line instead
     once the row is too narrow for both (the `flex-wrap` just added). */
  white-space: nowrap;
}
.tsunami-block-en {
  font-size: var(--fs-lead);
  color: var(--text-2);
}
/* Phase 15.8 packet B: at the narrowest required width the four-kanji "大津波警報" plus
   the glyph can still crowd close to the edge once `.sheet`'s own horizontal padding is
   subtracted from 390px -- one step down in size keeps a visible margin without touching
   any other `.section-heading`-scale text on the page. */
@media (max-width: 399px) {
  .tsunami-block-ja {
    font-size: var(--fs-title);
  }
}
/* Packet L: kit-ext.css never tints .tsunami-block-ja either -- same reasoning as the
   tsunami-line rule above; plain --text (the display size and the JA/EN term wording
   carry the meaning) for advisory ("keeps its existing colour", DESIGN.md §R.5). Phase
   15.8 packet B: WARNING/MAJOR WARNING now get --alert-text instead (components.js's
   `toneClass`, TsunamiBlock) -- the SAME (alert-text, sheet-solid) pair already declared
   in scripts/check-frontend-contrast.py (`.tsunami-region-pill.is-alert`'s own entry), so
   no new DECLARED_PAIRS entry is needed; the selector below also satisfies this packet's
   own "every `var(--alert)`-family rule lives under a tsunami/urgent-strip selector" grep
   rule (`.tsunami-block...` contains "tsunami" literally). */
.tsunami-block.is-alert .tsunami-block-ja,
.tsunami-block.is-alert .tsunami-block-en {
  color: var(--alert-text);
}
.tsunami-block-unconfirmed {
  color: var(--stale);
  font-weight: 600;
}
.tsunami-block-action {
  font-weight: 600;
  margin: var(--s3) 0;
}
.tsunami-block-issued,
.tsunami-block-pref-relation,
.tsunami-block-link {
  font-size: var(--fs-micro);
  color: var(--text-2);
  margin: var(--s1) 0;
}
.tsunami-compact-eq {
  margin-top: var(--s3);
  font-size: var(--fs-body);
}

.tsunami-region-groups {
  margin: var(--s3) 0;
}
.tsunami-region-note {
  font-size: var(--fs-micro);
  color: var(--text-2);
}
.tsunami-region-group-heading {
  font-size: var(--fs-lead);
  margin: var(--s3) 0 var(--s1);
}
.tsunami-region-list {
  list-style: none;
  margin: 0;
  padding: 0;
}
.tsunami-region-row {
  display: grid;
  grid-template-columns: 1fr auto auto;
  gap: var(--s3);
  align-items: baseline;
  padding: var(--s2) 0;
  border-top: 1px solid var(--hairline);
}
.tsunami-region-row:first-child { border-top: 0; }
.tsunami-region-pill {
  display: inline-flex;
  align-items: center;
  padding: 0 var(--s2);
  height: 1.6em;
  border-radius: 999px;
  font-weight: 600;
  border: 1px solid transparent;
}
.tsunami-region-pill.is-alert { border-color: var(--alert); color: var(--alert-text); }
.tsunami-region-pill.is-critical,
.tsunami-region-pill.is-forecast { background: var(--critical-fill); color: var(--on-critical); }
.tsunami-region-height {
  color: var(--text-2);
  font-variant-numeric: tabular-nums;
}

.map-legend-row-coast .map-legend-swatch,
.coast-line-swatch {
  vertical-align: -2px;
}

/* ============================================================ your area (§P.4 item 4) */
.area-line-heading {
  font-size: var(--fs-micro);
  color: var(--text-2);
  margin: 0 0 var(--s1);
}
.area-line-place {
  font-size: var(--fs-lead);
  margin: 0 0 var(--s1);
}
.area-line-value {
  font-size: var(--fs-body);
  margin: 0 0 var(--s1);
}
.area-line-class-desc {
  color: var(--text-2);
  margin: 0 0 var(--s2);
}
.area-line-class-desc.hero-class-desc {
  margin: 0;
}
.area-line-class-source {
  margin-left: var(--s1);
}
.area-line-areas-heading {
  font-size: var(--fs-micro);
  color: var(--text-2);
  margin: var(--s2) 0 0;
}
.area-line-areas-list {
  list-style: none;
  margin: 0;
  padding: 0;
}
.area-line-areas-list li {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: var(--s3);
  align-items: baseline;
  padding: var(--s2) 0;
  border-top: 1px solid var(--hairline);
}
.area-line-areas-list li:first-child { border-top: 0; }

/* ============================================================ legend (§P.5/§P.8 #3) */
.map-legend {
  display: flex;
  flex-direction: column;
  gap: var(--s2);
  margin: 0;
  font-size: var(--fs-micro);
  color: var(--text-2);
}
.map-legend-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s3);
}
.map-legend-label {
  font-weight: 600;
  color: var(--text);
}
.map-legend-chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s1);
}
.map-legend-chip {
  min-width: 1.6rem;
  font-size: var(--fs-micro);
  padding: 0.05rem 0.4rem;
}
.map-legend-item,
.map-legend-item-self {
  display: inline-flex;
  align-items: center;
  gap: var(--s1);
  /* PM render review repair (Phase 15.7 packet C, BLOCKING 1): `white-space: nowrap`
     here predates this packet and was never a regression in itself -- it was written
     against short Japanese legend labels, the only language check-public-states.sh
     (no --lang flag) ever actually rendered before decision 0072 made English the
     document root's default (initialLanguage() now reads document.documentElement.lang
     instead of navigator.language/localStorage detection, which fell back to "ja" in
     this exact harness). Longer English category labels (e.g. a tsunami-warning
     category name) now overflow their fixed-width legend item at 360/390px because
     nowrap forbids the text itself from wrapping even though the legend ROW
     (.map-legend-row, flex-wrap: wrap) already reflows separate items normally. Pure
     CSS on an existing rule, not a map.js/map-section.js change -- allowing the text to
     wrap onto a second line (swatch stays put via align-items: center on the first
     line) is strictly better than a page that scrolls horizontally. */
  white-space: normal;
}
.map-legend-swatch {
  flex: none;
  display: block;
}
/* DESIGN.md §P.11: the legend's static mid-cycle ring + dot (map-section.js LatestSwatch
   never animates -- see that component's own comment). `.latest-dot` uses --accent here
   (never a literal), unlike the real marker's own per-event JMA colour, since this swatch
   represents the SHAPE, not any one earthquake's intensity. */
.map-legend-swatch.latest-swatch .latest-pulse-ring {
  fill: none;
  stroke: var(--text);
  stroke-width: 1.6px;
}
.map-legend-swatch.latest-swatch .latest-dot {
  fill: var(--accent);
  stroke: var(--text);
  stroke-width: 1.4px;
}
.selected-swatch .marker-selected {
  stroke: var(--accent);
  stroke-width: 3px;
}
.earlier-swatch .map-marker-epicentre {
  fill: var(--text-2);
}
.coast-line-swatch-casing {
  stroke: var(--sheet-solid);
  stroke-width: 5px;
}
.coast-line-swatch-line {
  stroke-width: 3px;
}
.coast-line-swatch-line.is-alert { stroke: var(--alert); }
.coast-line-swatch-line.is-critical { stroke: var(--critical); }
.map-legend-note {
  margin: 0;
}

/* ============================================================ primary action (§P.4 item 5) */
.primary,
.primary-action button {
  display: block;
  width: 100%;
  min-height: 52px;
  border: 0;
  border-radius: var(--radius-button);
  background: var(--accent);
  color: var(--sheet-solid);
  font-size: var(--fs-lead);
  font-weight: 600;
  cursor: pointer;
}
main[data-mode="TSUNAMI"] .primary,
main[data-confidence="stale"] .primary,
main[data-confidence="unknown"] .primary {
  background: var(--critical-fill);
  color: var(--on-critical);
}
/* SIGNIFICANT severe also gets the critical tone (0068's own severe/critical split, mirrored
   from ActionsNow's own `is-critical`/`is-alert` bands via the sibling `.actions-now`). */
.sheet:has(.actions-now.is-critical) .primary {
  background: var(--critical-fill);
  color: var(--on-critical);
}

/* ============================================================ now strip (§P.4 item 6) */
.actions-now .section-heading {
  font-size: var(--fs-micro);
  text-transform: uppercase;
  letter-spacing: var(--track-micro);
  color: var(--text-2);
  margin: 0 0 var(--s2);
}
html[lang="ja"] .actions-now .section-heading {
  text-transform: none;
  letter-spacing: normal;
}
.now-strip-cards {
  display: block;
}
.situation-card {
  padding: var(--s3) 0;
  border-top: 1px solid var(--hairline);
}
.situation-card:first-child { border-top: 0; }
.situation-card-heading {
  font-size: var(--fs-micro);
  color: var(--text-2);
  margin: 0 0 var(--s1);
  font-weight: 400;
}
.situation-decision {
  font-size: var(--fs-title);
  margin: 0 0 var(--s1);
}
.situation-actions {
  list-style: none;
  margin: var(--s2) 0 0;
  padding: 0;
}
.situation-action {
  padding: var(--s1) 0;
}
.situation-note {
  color: var(--alert-text);
  margin: var(--s2) 0 0;
}
.situation-terms {
  margin: var(--s3) 0 0;
}
.situation-terms-heading {
  font-size: var(--fs-micro);
  color: var(--text-2);
  margin: 0 0 var(--s1);
}
.situation-terms-list {
  list-style: none;
  margin: 0;
  padding: 0;
}
.situation-term {
  padding: var(--s1) 0;
}
.situation-term-label {
  font-size: var(--fs-micro);
  color: var(--text-2);
  margin-left: var(--s1);
}
.guidance-source {
  font-size: var(--fs-micro);
  margin-left: var(--s1);
}
.guidance-lang-note {
  font-size: var(--fs-micro);
  color: var(--text-2);
}
.now-strip-more {
  margin-top: var(--s3);
}
.now-strip-more-summary {
  cursor: pointer;
  color: var(--accent);
  font-size: var(--fs-body);
  min-height: var(--tap);
  display: inline-flex;
  align-items: center;
}
.now-strip-more-list {
  list-style: none;
  margin: var(--s2) 0 0;
  padding: 0;
}
.now-strip-more-list li {
  padding: var(--s1) 0;
}
.actions-now-full-guide {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap);
  margin-top: var(--s3);
  font-size: var(--fs-body);
}
.safety-route {
  display: inline-block;
  color: var(--accent);
  font-size: var(--fs-body);
  min-height: var(--tap);
  display: inline-flex;
  align-items: center;
}

/* Tone (0068/§O.4: attention 朱, severe alert 朱, critical 深緋; TSUNAMI advisory 朱,
   warning/major 深緋) -- colour on the decision line only, never the sole carrier (the
   card's own heading/text state the situation in words too). */
.actions-now.is-attention .situation-decision,
.actions-now.is-alert .situation-decision { color: var(--alert-text); }
.actions-now.is-critical .situation-decision { color: var(--critical-text); }

/* ============================================================ emergency numbers (§P.4 item 7) */
.emergency-strip {
  padding: var(--s4) 0;
}
.emergency-strip-caption {
  font-size: var(--fs-micro);
  color: var(--text-2);
  margin: 0 0 var(--s2);
}
.emergency-strip-buttons {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  gap: var(--s3);
}
.emergency-strip-btn {
  font-size: var(--fs-title);
  font-weight: 600;
  color: var(--text);
  text-align: center;
  min-height: var(--tap);
  display: flex;
  align-items: center;
  justify-content: center;
  /* PM review repair (P2-5, 2026-09-08): each button's text is ONE string, number and
     label together ("119 Fire · Ambulance"/"JNTO 24 h · English"/"119 消防・救急") --
     default CJK line-breaking allows a break between ANY two adjacent characters with
     no regard for the lexical word they form together, which split "24時間" into
     "24時"/"間" and "消防・救急" mid-phrase inside a cramped three-column cell at 450px.
     `word-break: keep-all` (unlike the default `normal`) respects CJK word boundaries --
     it still allows wrapping at real spaces/punctuation (・, spaces), just never inside
     what the language treats as one word. */
  word-break: keep-all;
  overflow-wrap: break-word;
}
/* PM review repair (P2-5): below 560px (matching the governing-stats threshold, P2-4),
   three squeezed columns become one full-width column -- each button gets the sheet's
   whole content width, comfortably fitting its text on one or two natural lines instead
   of forcing a three-way split of an already-narrow row. */
@media (max-width: 559px) {
  .emergency-strip-buttons {
    grid-auto-flow: row;
    grid-template-columns: 1fr;
  }
}

/* ============================================================ governing event (§P.4 item 8, 079 stat row) */
.governing-card-eyebrow {
  font-size: var(--fs-micro);
  color: var(--text-2);
  margin: 0 0 var(--s1);
  display: flex;
  align-items: center;
  gap: var(--s1);
}
.governing-card-eyebrow-swatch {
  flex: none;
}
.governing-card-place {
  font-size: var(--fs-title);
  margin: 0 0 var(--s1);
}
.governing-card-region-hint {
  color: var(--text-2);
  margin: 0 0 var(--s2);
}
.governing-card-value {
  font-size: var(--fs-body);
}
.governing-card-stats {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  gap: var(--s4);
  margin: var(--s3) 0 0;
}
.governing-card-stat {
  display: flex;
  flex-direction: column;
}
.governing-card-stat dt {
  font-size: var(--fs-micro);
  color: var(--text-2);
  text-transform: uppercase;
  letter-spacing: var(--track-micro);
}
html[lang="ja"] .governing-card-stat dt {
  text-transform: none;
  letter-spacing: normal;
}
/* PM review repair (R1, 2026-09-08 -- operator real-device review #1, second pass): even
   after dropping the redundant " JST" suffix (components.js), a real render still wrapped
   this value ("09/07" / "13:12") at 1440px -- direct comparison against an automated
   Marionette read of the SAME box, at the SAME instant in real time, found the check
   consistently reporting single-line while the SEPARATE screenshot driver's own (longer,
   full-page) settle path reported wrapped: a few pixels of late layout narrowing (most
   likely a vertical scrollbar appearing once the page's content height finally exceeds the
   viewport, which the `.sheet` column's own `clamp()` width is computed against) that a
   quick read right after `wait_settled` does not yet see. `white-space: nowrap` makes
   wrapping structurally impossible regardless of exactly when or why the available width
   narrows by a few px -- the DESIGN.md §P.9 item 2 requirement is "does not wrap", not "is
   comfortably narrower than its column"; a value overflowing a few px into the column
   gap on an extreme case is a strictly better failure mode than a 2-line value.
*/
.governing-card-stat dd {
  margin: 0;
  font-size: var(--fs-lead);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
/* DESIGN.md §P.13 item 2: the secondary "最新:"/"24時間以内で最も強い地震:" micro line
   may wrap but must never be clipped -- explicit `overflow: visible` and no fixed
   height (neither was ever set here, but stated explicitly so the intent is testable,
   not merely an unstated default any future rule could accidentally shadow). */
/* PM review repair (P2-a, 2026-09-09): DESIGN.md §P.14's own "ONE MICRO line" wording --
   the `t-micro` class (components.js) already gives --fs-micro size and --text-2 colour;
   tabular numerals for the relative-time/magnitude values are added here (the SAME
   `font-variant-numeric: tabular-nums` every other numeric stat value on this page
   already carries -- .event-time-abs, .governing-card-stat dd). */
.governing-card-secondary {
  overflow: visible;
  height: auto;
  font-variant-numeric: tabular-nums;
}
/* PM review repair (P2-a, 2026-09-09): each meaningful segment of the secondary line is
   its own unbreakable atomic unit (components.js) so the line can wrap only at a real
   " · " separator, never mid-token -- the SAME technique already established for the
   recent-list "M · depth" cell's own `.event-stat-half` (app.css, above). */
/* PM render review repair (Phase 15.7 packet C, BLOCKING 1, PM-directed bisection):
   `white-space: nowrap` made a hard assumption that "label + hypocentre name" always
   fits one line -- true for the short labels this rule was calibrated against, but the
   `list-overflow-stress` fixture exists specifically to stress arbitrarily long
   hypocentre names, and pairs one here with the English "Latest:" label (decision
   0072: English is now the document root's default; the label word itself is also
   wider than JA's "最新:"/"24時間以内で最も強い地震:", a combination this rule's own
   real-headless-browser calibration was never taken against before) -- confirmed via
   direct measurement: this rule alone pushed page scrollWidth to 377px at a 360px
   viewport (17px of real, non-SVG overflow, `.governing-card-secondary-part` itself
   the widest offending box). `overflow-wrap: anywhere` on the two-word ATOMIC join
   above already establishes that "may wrap but must never be clipped" is this line's
   own actual DESIGN.md §P.13 item 2 intent (see that comment) -- allowing a SINGLE part
   to wrap internally, as a last resort when it alone cannot fit the line, is the same
   robustness guarantee, not a new relaxation: the " · " gap between DIFFERENT parts
   remains the first, preferred wrap point (an ordinary space in normal flow always
   breaks before a same-size in-token break does), so short content (the common case)
   renders byte-for-byte identically to before. */
.governing-card-secondary-part {
  display: inline-block;
  white-space: normal;
  overflow-wrap: anywhere;
}
/* PM review repair (P2-4, 2026-09-08): the old 430px threshold left the FOUR-column row
   active at 450px (the packet's own mobile screenshot width), squeezing the time value
   into four wrapped lines ("09/07 / 04:19 JST / (21時間 / 前)"). Four independent stat
   columns each need real room for their own value; 560px is comfortably past every
   phone-class width this site verifies (360/390/450), so the 2x2 grid now covers the
   whole mobile range, not just a narrow sliver of it. */
@media (max-width: 559px) {
  .governing-card-stats {
    grid-auto-flow: row;
    grid-template-columns: 1fr 1fr;
  }
}

/* ============================================================ recent list (§P.4 item 9, 048 index rows) */
.list-section .section-heading {
  font-size: var(--fs-micro);
  text-transform: uppercase;
  letter-spacing: var(--track-micro);
  color: var(--text-2);
  margin: 0 0 var(--s2);
}
html[lang="ja"] .list-section .section-heading {
  text-transform: none;
  letter-spacing: normal;
}
/* Packet L2 (DESIGN.md §P.9 item 3, operator real-device review #1): the intensity pill is
   now a real leading column (`<th scope="row">`, components.js) at every width -- the old
   <768px `display: block` card layout (which ended by `float: right`-ing the pill toward
   the row's own bottom rule -- the operator's actual complaint) is retired along with the
   per-width column reshuffle it required; ONE table layout now serves every width, which
   is also what "the SAME structure on every width" (item 3) asks for. */
/* DESIGN.md §P.13 item 2 (operator real-device review #4): "the recent list never
   overflows horizontally" -- `table-layout: fixed` makes every column a genuinely FIXED
   share of the table's own 100% width (from the `<colgroup>` below, components.js),
   regardless of content -- this REPLACES the earlier `.event-table-wrap` horizontal-
   scroll-container repair (packet L2 F2 follow-on): that fix let the table itself grow
   wider than its column and scroll internally, satisfying "no PAGE overflow" but not
   "every cell's text box lies inside the sheet" (a cell's own text could still exceed
   ITS column, just hidden by the wrapper's scrollbar) -- `table-layout: fixed` plus
   `overflow-wrap: anywhere` on the one cell that needs it (the place cell, below) makes
   overflow structurally impossible at the CELL level too, not merely at the page level. */
.event-table {
  width: 100%;
  table-layout: fixed;
  border-collapse: collapse;
}
.event-col-pill {
  width: 2.5em;
}
/* DESIGN.md §P.13 item 2: the time cell's own content ("MM/DD HH:MM（N時間前）", micro
   size, `white-space: nowrap`, see `.event-time-cell` below) is a near-FIXED-length
   string -- it does not shrink with the column, unlike the place/stat cells (both
   deliberately wrap-tolerant: `overflow-wrap: anywhere` and the "·"-only break,
   respectively). A percentage share of this list's own narrow rendered column (measured
   as low as ~324px even at a 1200px viewport -- the list is one card in a grid, not the
   full page width) previously gave the time cell as little as ~74px against a real
   ~126px content need, which does not grow the column under `table-layout: fixed` --
   it silently paints past the cell's own box into the neighbouring column instead. A
   fixed px width sized to that real content need (measured via a real headless-browser
   render, not guessed) removes the conflict structurally; `.event-col-place` is left
   with NO declared width so it absorbs whatever remains (any width, including a narrow
   one) via its own already-established wrap tolerance.
   PM render review repair (Phase 15.7 packet C, BLOCKING 1): 134px was measured only
   against Japanese relative-time text ("N時間前"/"N分前"). English "ago" phrases
   (events.js RELATIVE_EN, unchanged/pre-packet-C) are longer for the "min" case in
   particular -- "09/09 13:04（15 min ago）" measured 140px, 6px over -- because English
   is now the document root's default language (decision 0072), a combination this
   column's own real-headless-browser measurement was never taken against before. 150px
   covers the worst case ("59 min ago") with margin; still a fixed px width, same
   structural technique, `.event-col-place` still absorbs whatever remains. */
.event-col-time {
  width: 150px;
}
.event-col-stat {
  width: 30%;
}
.event-table th[scope="col"] {
  text-align: left;
  font-size: var(--fs-micro);
  color: var(--text-2);
  font-weight: 400;
  padding: var(--s1) var(--s2) var(--s1) 0;
}
.event-table td,
.event-table th[scope="row"] {
  padding: var(--s2) var(--s2) var(--s2) 0;
  border-top: 1px solid var(--hairline);
  font-size: var(--fs-body);
  font-weight: 400;
  text-align: left;
  vertical-align: top;
}
.event-table tbody tr:first-child td,
.event-table tbody tr:first-child th[scope="row"] {
  border-top: 0;
}
.event-table tbody tr {
  cursor: pointer;
}
.event-table tbody tr.is-selected td {
  color: var(--accent);
}
/* DESIGN.md §P.12 R6 (marker activation selects the row too): "an accent-coloured 3px
   left rule and a faint tint of the accent, both themes". The left rule is a `box-shadow`
   on the row's own LEADING cell (`#event-table .event-pill-cell` -- ID-scoped for the
   SAME specificity reason every other per-cell rule in this file already needs, see the
   R5/R6 comments above) rather than a `border-left`: `border-collapse: collapse` makes a
   real border interact with the adjacent cell's own border in ways a `box-shadow` (which
   paints entirely inside the cell's own box, never collapses, never shifts column width
   even without `box-sizing: border-box`) does not. The tint uses the `--accent-tint`
   token (defined once, in `:root`, see that declaration's own comment for why) rather
   than an inline colour-mix function here (check-frontend-contrast.py's own literal-
   colour scan requires that inline form to live inside a `:root` block). */
.event-table tbody tr.is-selected td,
.event-table tbody tr.is-selected th[scope="row"] {
  background: var(--accent-tint);
}
#event-table tbody tr.is-selected .event-pill-cell {
  box-shadow: inset 3px 0 0 0 var(--accent);
}
/* PM review repair (R5, 2026-09-08 -- operator real-device review #1, fourth pass): every
   one of the four per-cell rules below (`.event-pill-cell`/`.event-time-cell`/
   `.event-place-cell`/`.event-stat-cell`) is a SINGLE class selector, specificity (0,1,0)
   -- LOWER than the base rule just above it matches through for a `<td>`
   (`.event-table td`, one class + one element = (0,1,1)). CSS resolves by specificity
   first, source order only as the tie-breaker for EQUAL specificity, so the base rule's
   `font-size: var(--fs-body)` was silently winning over `.event-time-cell`'s own
   `font-size: var(--fs-micro)` the WHOLE time (verified directly:
   `getComputedStyle(.event-time-cell).fontSize` was `16px`/body, never the intended
   `11px`/micro) -- §P.9 item 3 says the time is micro, and it never actually rendered
   micro, which is what left it wide enough to squeeze the place column into mid-word
   breaks. Scoped every one of the three `<td>` rules under `.event-table` (specificity
   (0,2,0), which beats (0,1,1) regardless of the third component) so they reliably win
   from now on, not just by the coincidence of matching the base value. The PILL cell is a
   `<th scope="row">`, not a `<td>` -- the base rule matches it through its OTHER branch,
   `.event-table th[scope="row"]`, where `[scope="row"]` is an ATTRIBUTE selector and so
   ALSO counts in the specificity's middle column: (0,2,1), one column higher than
   `.event-table .event-pill-cell`'s (0,2,0) -- caught only by testing the ACTUAL computed
   `padding-bottom` after first "fixing" it with the `.event-table` prefix alone and
   finding it unchanged. `#event-table .event-pill-cell` (an ID plus a class, (1,1,0))
   trivially outranks (0,2,1) on the ID column alone, so it is used for the pill cell
   specifically instead of the plain `.event-table` prefix the other three use. */
#event-table .event-pill-cell {
  /* ~2.5em fixed width (§P.9 item 3), never wider than its own pill content needs --
     `white-space: nowrap` keeps a two-character shindo code ("5+") from ever wrapping and
     growing the column, which is what would eventually push the pill toward the row's
     bottom rule again. */
  width: 2.5em;
  white-space: nowrap;
}
/* PM review repair (R6, 2026-09-08 -- operator real-device review #1, fifth pass):
   freeing the time cell's width (R5) also shrank most rows down to a single content line
   (~42px) -- short enough that the top-aligned intensity pill (a fixed ~25.6px chip,
   unrelated to any of this row's own text sizing) no longer fits ENTIRELY within the
   row's own top HALF while also staying >=4px clear of the bottom rule (both requirements
   from the pill-position check this packet's own earlier pass added): a 42px single-line
   row only leaves 21px of "top half" budget, less than the pill's own 8px padding-top +
   25.6px height. `min-height` on the shared base rule (tried first) computed correctly
   (`getComputedStyle` reported "70px") but did NOT change the rendered row height at all
   -- verified directly, Gecko's table row-height algorithm does not treat `min-height` on
   a `display: table-cell` box as a real per-cell height floor the way it does for
   ordinary block boxes. `padding-bottom` on the pill cell specifically DOES reliably
   inflate a table cell's (and so its row's) rendered height in every engine, without
   moving the pill itself (which stays anchored by its OWN, unchanged `padding-top` +
   `vertical-align: top`) -- it just reveals a bit of empty column space below the pill,
   consistent with "top-aligned" already meaning "not vertically centred" for this column.
   40px clears the pill's own worst case (padding-top 8px + height 25.6px + >=4px
   clearance needs >=67.2px total row height; 8+25.6+40=73.6px) with a safety margin, so
   this cannot resurface as a regression from some future, even shorter row. Needs the
   SAME `#event-table` ID-selector strength as the width/white-space rule above it (see
   that rule's own comment) -- a `.event-table`-only prefix was tried first and, like the
   width/white-space case, silently lost to the base rule's `th[scope="row"]` branch. */
#event-table .event-pill-cell {
  padding-bottom: 40px;
}
/* PM review repair (R2, 2026-09-08): ONE compact time format at every width (components.js
   `formatEventRowTime`) -- "MM/DD HH:MM（相対）", micro size, tabular numerals (the `.num`
   class on the cell already gives tabular-nums; kept here too for direct clarity) --
   retires the old two-span full/short responsive-alt-text toggle and its own media query
   entirely, along with `.event-time-full`/`.event-time-short`. */
/* PM review repair (R2, 2026-09-08 -- operator real-device review #1, second pass): a real
   render showed the single compact format itself wrapping mid-parenthesis ("09/08 01:15
   （9時間" / "前）") once the header row's removal (above) freed up vertical space and the
   list column narrowed slightly -- the SAME "must not wrap" standard R1 applies to the
   governing-card stat row applies here (this cell is even narrower). `white-space: nowrap`
   for the same reason: wrapping is structurally impossible regardless of exactly how many
   px are available -- the place cell next to it is what absorbs any narrow-column
   pressure instead (R5: via `word-break: keep-all`, not `overflow-wrap: anywhere` any
   more -- see that rule's own comment for why). Actually rendering at MICRO size (R5, the
   specificity fix above) is what frees the ~30% of the row's width this cell no longer
   needs to claim for itself. */
.event-table .event-time-cell {
  font-size: var(--fs-micro);
  color: var(--text-2);
  white-space: nowrap;
}
.event-time-abs {
  font-variant-numeric: tabular-nums;
}
.event-time-relative {
  color: var(--text-2);
}
/* PM review repair (R2): place at BODY weight (was `--fs-lead`, louder than an index row
   calls for -- DESIGN.md refs 026/048).
   PM review repair (R5, 2026-09-08 -- operator real-device review #1, fourth pass): a
   real render broke Japanese place names MID-CHARACTER ("福島県" / "沖", "熊本県" /
   "熊本地" / "方") once the (still-body-size) place cell was squeezed by the
   (incorrectly body-size, see above) time cell -- `overflow-wrap: anywhere` is a LAST-
   RESORT rule that explicitly permits breaking at ANY character once a run of text does
   not fit, which is exactly what was firing. Replaced with `word-break: keep-all`: CJK
   text runs with no natural break point (no space, no `<wbr>`) now stay on ONE line and
   let the row grow, instead of breaking arbitrarily -- `components.js` inserts a real
   `<wbr>` after each occurrence of 県/都/道/府 in the place name (the natural
   prefecture-suffix boundary), which IS a real, deliberate break opportunity this rule
   still honours; the row only ever grows (never breaks) for a place name with no such
   character in it at all (e.g. an outlying-island hypocentre name). */
/* DESIGN.md §P.13 item 2: `word-break: keep-all` (packet L2 R5) + a real `<wbr>` per
   県/都/道/府 boundary is RETIRED -- under `table-layout: fixed` the place cell has a
   genuinely fixed pixel width, so "never break mid-word, grow the row instead" would
   overflow the cell's own box for a long compound name with no such boundary at all.
   `overflow-wrap: anywhere` is the new rule: text wraps at a natural word boundary when
   one exists, and as a LAST RESORT at any character when it does not -- this is exactly
   the item's own "the place cell wraps at any point" instruction, and (unlike the
   scrollable-wrapper approach it replaces) guarantees the text NEVER exceeds its own
   cell's fixed-width box, which a horizontal scroll container could not guarantee at
   the cell level, only at the page level. */
.event-table .event-place-cell {
  font-size: var(--fs-body);
  overflow-wrap: anywhere;
}
/* DESIGN.md §P.13 item 2: the "M · depth" cell may now wrap, but ONLY at the " · "
   separator, never inside "M3.2" or "50 km" themselves -- each half is its own
   `inline-block` span (components.js `.event-stat-half`) with `white-space: nowrap`,
   so a browser's own normal line-breaking algorithm can only insert a break BETWEEN the
   two inline-block boxes (where the literal " · " text sits), never inside either one.
   The cell itself no longer needs `white-space: nowrap` at all -- replaces the packet L2
   R4 "never wraps, ever" rule now that `table-layout: fixed` (rather than an unbounded
   row width) is what keeps this cell from ever pushing the table wider than its column. */
.event-table .event-stat-cell {
  color: var(--text-2);
  font-variant-numeric: tabular-nums;
}
.event-stat-half {
  display: inline-block;
  white-space: nowrap;
}
/* DESIGN.md §P.13 item 2: the horizontal-scroll wrapper (packet L2 F2 follow-on) is
   retired now that `table-layout: fixed` makes the table itself structurally unable to
   grow wider than its own 100% column -- `overflow: visible` (rather than deleting the
   wrapper element from components.js, which would touch more call sites for no
   behavioural gain) makes this explicitly a plain, non-scrolling block again. */
.event-table-wrap {
  overflow: visible;
}
.event-empty,
.event-load-error {
  color: var(--text-2);
  margin: var(--s2) 0 0;
}
.recent-list-toggle {
  margin-top: var(--s3);
  min-height: var(--tap);
  color: var(--accent);
  background: none;
  border: 0;
  cursor: pointer;
  font-size: var(--fs-body);
}

/* ============================================================ detail panel */
/* Phase 15.8 packet A (decision 0075 items 1, 3): the detail panel is now the FIRST
   `.sheet` section (app.js) -- `.sheet > section:first-child` (above) already zeroes its
   top border/padding, so no extra top margin is added here: at mobile scroll 0 the site bar
   (`.site-header`, transparent, `position: fixed`) sits over `.map-stage` (>= 320px tall,
   `.sheet`'s own negative top margin included), well above where `.sheet`'s content -- and
   therefore this section's own first line -- ever renders; verified by direct measurement
   (evidence screenshots), not assumed. */
.detail-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s3);
}
/* Phase 15.8 packet A (decision 0075 item 3): replaces `.detail-reset` -- a real >=44px
   (both axes) tap target, right-aligned by `.detail-header`'s own `space-between` (the
   heading is the only other flex child). The "×" glyph is a separate `aria-hidden` span
   (components.js) with a small negative margin so the visible "× Close"/"× 閉じる" pairing
   does not read as extra padding on one side only. */
.detail-close {
  display: inline-flex;
  align-items: center;
  gap: 0.25em;
  min-width: var(--tap);
  min-height: var(--tap);
  background: none;
  border: 1px solid var(--hairline);
  border-radius: var(--radius-button);
  color: var(--text);
  padding: 0 var(--s3);
  cursor: pointer;
  font-size: var(--fs-body);
}
.detail-close span[aria-hidden] {
  font-size: 1.1em;
  line-height: 1;
}
/* Phase 15.8 packet B (residual from A): at 320px, "#detail-heading"'s EN text
   ("Selected earthquake") plus the Close button both competing for `.detail-header`'s
   `space-between` row wrapped the heading onto two lines, pushing the first `<dl>` row
   below the first viewport at 320x568. One step down in heading size, and a more compact
   (still >=44px both axes -- unchanged) Close button, both only below 360px so nothing
   at 390px and above (already fitting) is touched. */
@media (max-width: 359px) {
  #detail-heading {
    font-size: var(--fs-lead);
  }
  .detail-close {
    padding: 0 var(--s2);
    gap: 0.15em;
  }
}
.detail-body {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--s1) var(--s3);
  margin: var(--s3) 0 0;
}
.detail-body dt {
  color: var(--text-2);
  font-size: var(--fs-micro);
}
.detail-body dd {
  margin: 0;
  font-variant-numeric: tabular-nums;
}
.detail-no-map,
.detail-region-hint {
  color: var(--text-2);
  font-size: var(--fs-micro);
}
/* Phase 15.8 packet A (decision 0075 item 8): the per-prefecture intensity disclosure --
   `--text`/`--text-2`/`--hairline` are already theme-resolved custom properties (app.css's
   own light table + the `prefers-color-scheme: dark` block above), so no separate dark-mode
   rule is needed for colour; only the native `<details>`/`<summary>` marker/spacing is
   styled here, identically in both themes. */
.detail-regions {
  margin-top: var(--s3);
}
.detail-regions > summary {
  cursor: pointer;
  min-height: var(--tap);
  display: flex;
  align-items: center;
  color: var(--text);
  font-size: var(--fs-body);
  font-weight: 600;
}
.detail-regions-list {
  list-style: none;
  margin: var(--s2) 0 0;
  padding: 0;
  display: grid;
  gap: var(--s1);
}
.detail-regions-list li {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s3);
  padding: var(--s1) 0;
  border-top: 1px solid var(--hairline);
}
.detail-regions-list li:first-child {
  border-top: 0;
}
.detail-regions-name {
  color: var(--text);
}

/* ============================================================ intensity chips / pills */
.intensity {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 2.4em;
  height: 1.6em;
  padding: 0 var(--s2);
  border-radius: 999px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  background: var(--text-2);
  color: var(--sheet-solid);
}
.intensity-unknown {
  font-size: var(--fs-micro);
}
.intensity-1 { background: rgb(242, 242, 255); color: #1C1C1A; }
.intensity-2 { background: rgb(0, 170, 255); color: #FCFAF2; }
.intensity-3 { background: rgb(0, 65, 255); color: #FCFAF2; }
.intensity-4 { background: rgb(250, 230, 150); color: #1C1C1A; }
.intensity-5m { background: rgb(255, 230, 0); color: #1C1C1A; }
.intensity-5p { background: rgb(255, 153, 0); color: #FCFAF2; }
.intensity-6m { background: rgb(255, 40, 0); color: #FCFAF2; }
.intensity-6p { background: rgb(165, 0, 33); color: #FCFAF2; }
.intensity-7 { background: rgb(180, 0, 104); color: #FCFAF2; }
/* Decision 0040: the ramp (fill AND its own chosen foreground) is exempt from theming --
   "never restyled, never blended into a site colour" (DESIGN.md §O.5) -- so it is
   deliberately the SAME nine rules in both themes, unlike every other coloured role on the
   page. The foreground here is reported by check-frontend-contrast.py, not independently
   graded (0040's own "foreground is chosen per level" mechanism). */

/* ============================================================ footer */
.site-footer {
  padding: var(--s4) 0 0;
  font-size: var(--fs-micro);
  color: var(--text-2);
}
.footer-attribution,
.footer-about,
.connection-status {
  margin: 0 0 var(--s2);
}
.footer-links {
  display: flex;
  gap: var(--s4);
  flex-wrap: wrap;
  margin: var(--s2) 0 0;
}

/* ==================================================== static lead (Phase 15.7 packet C)
   decision 0072 section 7 / DESIGN.md section Q. Static HTML, OUTSIDE #app (a sibling in
   the document, never touched by Preact's render into #app), so it survives hydration
   unconditionally -- ordinary page content in footer position, below the fold on every
   viewport by construction (it follows the app's own full-height content in DOM order). */
#site-about {
  border-top: 1px solid var(--hairline);
  margin: var(--s6) 0 0;
  /* PM render review repair (Phase 15.7 packet C): #site-about is a BODY-level
     sibling of #app (never inside .sheet), so it inherited none of .sheet's own
     horizontal padding and its text sat flush at x=0 -- the one element on the
     page that ignored the content inset every other element (.site-header,
     .sheet) respects. var(--s4) matches BOTH of those exactly. */
  padding: var(--s4) var(--s4) var(--s5);
  font-size: var(--fs-micro);
  color: var(--text-2);
  max-width: 72ch;
}

/* Phase 15.7 packet E item 3 (residual from packet C §12 item 6): on the four secondary
   pages (/safety/, /official/, /support/, /privacy/, both languages) the page's own
   content column is `.sheet.page-sheet` (max-width: 760px; margin: 0 auto -- see that
   rule above), but `#site-about` above kept its own independent `max-width: 72ch` with
   no centering, so at any viewport wide enough for the difference to show, the static
   lead's left/right edges did not line up with the content above it. Home is explicitly
   EXCLUDED (`body:not([data-page="home"])`) and left unchanged -- its own `#site-about`
   sizing is tuned against the fixed map-controls clearance below, a completely different
   layout (see the >=1200px home-only rule below), not the page-sheet column. Matching
   `.sheet.page-sheet`'s own two properties exactly (same max-width, same auto-centering)
   is deliberately the ONLY change: no new max-width value invented, so a future edit to
   `.sheet.page-sheet`'s column width cannot silently drift the two apart again. */
body:not([data-page="home"]) #site-about {
  max-width: 760px;
  margin-left: auto;
  margin-right: auto;
}
.site-about-heading {
  font-size: var(--fs-body);
  font-weight: 600;
  margin: 0 0 var(--s2);
  color: var(--text);
}
.site-about-lead {
  margin: 0 0 var(--s2);
}
.site-about-links {
  margin: 0;
  display: flex;
  gap: var(--s2);
  flex-wrap: wrap;
}

/* Phase 15.7 packet C repair: at >=1200px `.map-stage` is `position: fixed; inset: 0`
   (the map's own full-bleed ground, above) covering the ENTIRE viewport regardless of
   scroll, so `#site-about` -- an ordinary, non-positioned body-flow sibling of `#app` --
   painted underneath it once scrolled into view (CSS2.1 painting order: in-flow
   non-positioned block boxes paint BEFORE positioned descendants with z-index 0, and
   `.map-stage` is exactly that, at the root stacking context, regardless of `#app`'s own
   DOM position ahead of it). `position: relative` plus a `z-index` above `.map-stage`'s 0
   is what actually makes it paint on top; it stays a normal in-flow, non-fixed box (DESIGN
   §Q's "ordinary page content", still below the fold at scrollY 0, still reached only by
   scrolling) so this is a paint-order fix, not a repositioning of the element in the page.
   Once visible over the map it needs the sheet's own surface (background/radius/border) to
   read as the small panel DESIGN §Q/§P.1 describes rather than bare text laid over live map
   colour -- `border-top` above becomes a full hairline `border` here (radius on all four
   corners), matching `.sheet`'s own token and corner radius (§P.1 "exactly two tokens per
   theme -- ground and sheet"). `margin-left` gives it the same `var(--s5)` clearance from
   the viewport edge that `.sheet` keeps from the opposite edge; `margin-bottom` is sized
   from the map's own OWN tokens for `.map-controls`' rendered footprint at this width --
   `var(--s3)` (its `bottom` offset from the viewport edge) + `var(--tap)` (its rendered
   row height, one line of 44px segments, measured directly: 1440x900 and 1920x1080 both
   render `.map-controls` at x=12/y=(innerHeight-56)/h=44) + the required 24px clearance --
   so the two never overlap (checked by scripts/check-frontend-responsive.py) with no more
   scroll range than that minimum requires. Scoped to `[data-page="home"]` (packet E item
   3): this whole rule exists for clearance against home's own FIXED `.map-stage`/
   `.map-controls`, which do not exist on the four secondary pages -- those pages get the
   page-sheet-matching centering rule above instead, and must keep their plain in-flow
   `border-top` look rather than this fixed-panel card treatment. */
@media (min-width: 1200px) {
  body[data-page="home"] #site-about {
    position: relative;
    z-index: 1;
    margin-left: var(--s5);
    margin-bottom: calc(var(--s3) + var(--tap) + 24px);
    border: 1px solid var(--hairline);
    border-radius: var(--radius-sheet);
    background: var(--sheet);
  }
}

/* ============================================================ secondary-page sections
   (/safety/, /official/, /support/, /privacy/ -- same kit, plain ground, no map, §P.1/§P.6) */
.section-heading {
  font-size: var(--fs-title);
  font-weight: 600;
  margin: var(--s5) 0 var(--s2);
}
.sheet.page-sheet > :first-child,
.sheet.page-sheet .section-heading:first-child {
  margin-top: 0;
}

.guidance-eyebrow {
  font-size: var(--fs-micro);
  color: var(--text-2);
  margin: 0;
}
.guidance-state-label {
  font-size: var(--fs-lead);
  font-weight: 600;
  margin: var(--s2) 0;
}
.guidance-section.is-caution .guidance-state-label { color: var(--lagging-text); }
.guidance-section.is-alert .guidance-state-label { color: var(--alert-text); }
.guidance-section.is-critical .guidance-state-label { color: var(--critical-text); }
.situation-cards {
  margin-top: var(--s3);
}
.guidance-disclaimer {
  margin-top: var(--s4);
  padding-top: var(--s3);
  border-top: 1px solid var(--hairline);
  color: var(--text-2);
  font-size: var(--fs-micro);
}

.official-links,
.emergency-numbers {
  list-style: none;
  margin: 0 0 var(--s4);
  padding: 0;
}
.official-links li,
.emergency-numbers li {
  padding: var(--s2) 0;
  border-top: 1px solid var(--hairline);
}
.official-links li:first-child,
.emergency-numbers li:first-child { border-top: 0; }
.official-links a {
  display: block;
  min-height: var(--tap);
  display: flex;
  align-items: center;
}
.emergency-numbers li {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
  align-items: center;
}
.emergency-number {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap);
  font-weight: 700;
  color: var(--accent);
  font-size: var(--fs-title);
  min-width: 8ch;
}
.emergency-label {
  color: var(--text-2);
}
.emergency-caption {
  font-size: var(--fs-micro);
  color: var(--text-2);
}

.magnitude-vs-intensity,
.intensity-table-note,
.intensity-table-source {
  font-size: var(--fs-micro);
  color: var(--text-2);
}
.intensity-table {
  width: 100%;
  border-collapse: collapse;
  margin: var(--s3) 0;
}
.intensity-table th,
.intensity-table td {
  text-align: left;
  padding: var(--s2) var(--s3) var(--s2) 0;
  border-top: 1px solid var(--hairline);
}
.intensity-table thead th {
  border-top: 0;
  font-size: var(--fs-micro);
  color: var(--text-2);
  font-weight: 400;
}

.about-data p {
  margin: var(--s1) 0;
  font-size: var(--fs-micro);
  color: var(--text-2);
}

/* Phase 15.7 packet E item 1: the calm-day tsunami-category section on /official/,
   directly below the intensity table (IntensityTable's own CSS is just above). A plain
   description list -- term (JA + official EN name, decision 0070 §4's "always both"
   convention) then the sourced JMA action line, one pair per category, same body-text
   size as ordinary page content since this is guidance text a visitor needs to read
   fully, not a caption. */
.tsunami-category-list {
  margin: var(--s3) 0;
}
.tsunami-category-item {
  padding: var(--s3) 0;
  border-top: 1px solid var(--hairline);
}
.tsunami-category-item:first-child {
  border-top: 0;
  padding-top: 0;
}
.tsunami-category-name {
  font-weight: 600;
  margin: 0 0 var(--s1);
}
.tsunami-category-en {
  margin-left: var(--s1);
  font-weight: 400;
  color: var(--text-2);
}
.tsunami-category-description {
  margin: 0;
}
.tsunami-category-link {
  font-size: var(--fs-micro);
  color: var(--text-2);
  margin: var(--s2) 0 0;
}

/* ------------------------------------------------------------ support / donation */
.support-purpose {
  font-size: var(--fs-lead);
}
.support-not-relief {
  color: var(--stale);
  font-weight: 600;
}
.donation-grid {
  display: grid;
  gap: var(--s5);
  margin-top: var(--s4);
}
@media (min-width: 640px) {
  .donation-grid {
    grid-template-columns: 1fr 1fr;
  }
}
.donation-panel {
  padding-top: var(--s3);
  border-top: 1px solid var(--hairline);
}
.donation-currency {
  font-size: var(--fs-lead);
  margin: 0 0 var(--s2);
}
.support-address {
  display: block;
  word-break: break-all;
  font-size: var(--fs-micro);
  background: var(--ground);
  padding: var(--s2);
  border-radius: var(--radius-button);
}
.support-copy {
  margin-top: var(--s2);
  min-height: var(--tap);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-button);
  background: var(--sheet-solid);
  padding: 0 var(--s3);
  cursor: pointer;
}
.support-copy-status {
  margin-left: var(--s2);
  font-size: var(--fs-micro);
  color: var(--live);
}
.support-qr {
  display: block;
  width: 160px;
  height: 160px;
  margin-top: var(--s3);
  border-radius: var(--radius-button);
  overflow: hidden;
}
.donation-status,
.support-qr-unavailable {
  color: var(--text-2);
  font-size: var(--fs-micro);
}
.support-ledger-note {
  margin-top: var(--s4);
  font-size: var(--fs-micro);
  color: var(--text-2);
}

/* ============================================================ layout / page skeleton glue */
.layout-grid {
  min-height: 100vh;
}
.layout-grid-home {
  padding-top: 0;
}
.layout-grid:not(.layout-grid-home) {
  padding-top: 56px;
}
@media (min-width: 768px) {
  .layout-grid:not(.layout-grid-home) {
    padding-top: 64px;
  }
}

/* Phase 15.8 packet D, PM rendered-review repair (2026-09-11): the primary EN sentence is
   its own block (`.guidance-site-translation-primary`); the label + middle dot + JA
   original + Source link are ONE micro block line below it (`.guidance-site-translation-
   meta`), never run inline into the primary sentence (the original all-inline layout read
   as one garbled run). Reading order is still primary -> label -> JA original -> source
   (DOM order, components.js `GuidanceLineBody`). The guidance cards this renders into
   (0071 §1) carry a "fits one 390px screen without scrolling" per-card height budget, and
   the evacuation card alone carries six translated leaves at once -- kept to exactly TWO
   lines per leaf (never three), with the `.now-strip-cards` economy below absorbing the
   rest of the budget pressure. */
.guidance-site-translation-primary {
  display: block;
}
.guidance-site-translation-meta {
  display: block;
  margin-top: var(--s1);
  font-size: var(--fs-micro);
  color: var(--text-2);
}
.guidance-site-translation-meta .guidance-source {
  margin-left: var(--s1);
}
/* Scoped to `.now-strip-cards` ONLY (the home page's #actions-now container, decision
   0071's per-card budget) -- never `.situation-cards` (/safety/'s own, unbudgeted, full-
   page list, decision 0071 §1's explicit exception: "/safety/ keeps the full sourced
   set"). The TSUNAMI "Now" strip's evacuation card carries six translated leaves at once
   (more than any other card), and 0076's mandatory translation+label+JA-original content
   on each leaf otherwise pushes that ONE card past the existing 0071 §1 fit budget at
   narrow widths -- this is spacing/sizing economy only, never a change to WHICH text
   renders or its DOM order.
   */
.now-strip-cards .situation-card { padding: var(--s2) 0; }
.now-strip-cards .situation-card-heading { margin-bottom: 2px; }
.now-strip-cards .situation-decision { font-size: var(--fs-body); margin-bottom: 2px; }
.now-strip-cards .situation-actions { margin-top: var(--s1); }
.now-strip-cards .situation-terms { margin-top: var(--s2); }
.now-strip-cards .situation-terms-heading { margin-bottom: 2px; }
.now-strip-cards .situation-action,
.now-strip-cards .situation-term { padding: 2px 0; }
.now-strip-cards .situation-decision,
.now-strip-cards .situation-action,
.now-strip-cards .situation-term { line-height: 1.35; }
