/* Rural Gas Supply Co. — Azle, Texas
   Art direction: Aria 2026-09-05. "The counter on Main Street."
   Palette MEASURED from the 5000x5000 mark (scripts/measure-logo.py), not eyeballed.
   Logo top-bin blue #2060B0 · red median #B02119 · cylinder median #151011.
   Paper is a warm shop-wall off-white. Never #000 on #fff. Red is ONE job: the call.
   Notices are posted notices with hairline rules — never a card grid.
*/

:root {
  --blue: #2060B0;
  --blue-ink: #184A86;
  --blue-deep: #123A6A;
  --red: #B02119;
  --red-deep: #8C1A14;
  --cylinder: #151011;
  --paper: oklch(97.2% 0.012 85);
  --paper-sunk: oklch(94.4% 0.016 82);
  --paper-rule: oklch(88.5% 0.022 250);
  --ink: oklch(27% 0.028 250);
  --ink-soft: oklch(42% 0.024 250);
  --focus: oklch(48% 0.14 250);
  /* Type. Display = Bitter (slab, OFL) — Aria asked for something painted on the
     side of a truck, not a magazine Didone. Body = Public Sans (OFL), a Franklin-
     lineage American gothic with a 0.52 x-height for a 55-year-old reading an old
     Android in sun. Fallbacks are metric-matched in assets/fonts/fonts.css. */
  --font-display: "Bitter", "Bitter Fallback Georgia", "Bitter Fallback Times", Georgia, "Times New Roman", serif;
  --font-body: "Public Sans", "Public Sans Fallback Segoe", "Public Sans Fallback Arial", "Segoe UI", Arial, system-ui, sans-serif;

  /* Modular scale. Anchor 1.125rem (18px body, Aria's floor), ratio ~1.25 at
     1280 / ~1.15 at 375, emitted as clamp() so there are no breakpoint jumps.
     Every clamp carries a rem term so the scale still grows when a visitor
     raises their browser font size — a pure-vw clamp ignores that (WCAG 1.4.4).
     Components use these tokens. Nothing sets a font-size by hand. */
  --text-3xs: 0.72rem;
  --text-2xs: 0.78rem;
  --text-xs: 0.88rem;
  --text-sm: 0.95rem;
  --text-base: 1.125rem;
  --text-md: clamp(1.2rem, 0.72vw + 1.03rem, 1.4rem);
  --text-lg: clamp(1.25rem, 1.08vw + 1.0rem, 1.55rem);
  --text-xl: clamp(1.6rem, 1.72vw + 1.18rem, 2.15rem);
  --text-2xl: clamp(1.65rem, 2.48vw + 1.06rem, 2.35rem);
  --text-3xl: clamp(2.05rem, 4.6vw + 0.98rem, 3.35rem);
  --text-year: clamp(5rem, 12vw + 0.5rem, 9rem);

  /* Rhythm + tracking */
  --leading-body: 1.55;
  --leading-lead: 1.4;
  --leading-display: 1.1;
  --track-display: -0.02em;
  --track-display-tight: -0.03em;
  --track-caps: 0.12em;
  --track-caps-wide: 0.16em;
  --wt-display: 700;
  --wt-display-loud: 800;
  --s1: 4px; --s2: 8px; --s3: 12px; --s4: 16px; --s5: 24px;
  --s6: 32px; --s7: 48px; --s8: 72px; --s9: 112px; --s10: 160px;
  --r-btn: 2px;
  --r-img: 0;
  --r-input: 0;
  --maxw: 1180px;
  --pad-inline: clamp(16px, 4.5vw, 64px);
  /* MEASURE — the token every running-copy cap reads. Measured, not assumed:
     Public Sans's "0" is 0.612em, so 1ch = 11.02px at the 18px body, while the
     average character in this copy renders at ~8.1px. The old 64ch cap therefore
     measured 83-93 REAL characters per line, not the "~74" the note beside it
     claimed — counted line by line with Range rects on 2026-09-08, not estimated.
     48ch = 529px = 63-68 real characters, inside Aria's 60-70. Change the token,
     never a component's own number. */
  --prose: 48ch;

  /* ── MOTION TOKENS — Mira, MIRA-MOTION-SPEC-20260906.md ──────────────────
     Near-still register (Aria). Ease-OUT only; no bounce, no elastic, nothing
     over 400ms. transform + opacity ONLY — never a layout property, so motion
     cannot move CLS. Components read these; nothing writes a duration by hand.
     Every rule that spends one sits inside `prefers-reduced-motion:
     no-preference`, so `reduce` is the DEFAULT state, not a reset applied after
     the fact. */
  --ease-out: cubic-bezier(0.22, 0.61, 0.36, 1);     /* entrances, reveals */
  --ease-settle: cubic-bezier(0.16, 0.84, 0.44, 1);  /* the one signature moment */
  --dur-press: 120ms;    /* :active depress */
  --dur-hover: 160ms;    /* hover colour / lift */
  --dur-status: 180ms;   /* form-status swap, the Texas punch */
  --dur-nav: 180ms;      /* cross-document view transition */
  --dur-drawer: 220ms;   /* mobile drawer + its overlay (unchanged) */
  --dur-reveal: 300ms;   /* scroll reveal */
  --dur-resolve: 380ms;  /* hero cylinder resolve */

  /* ── TOKEN RULE — RED MEANS THE EMERGENCY ────────────────────────────────
     (Io spec §1 "Safety phone treatment"; closes Kassia fix #8, four reds on /.)

     --emergency        is spent ONLY on the emergency strip and the leak / 911
                        instruction. It does not re-theme per route, it is never
                        an A/B'd CTA, and no component outside `.emergency` may
                        name it.
     --accent-call      is the ordinary "call us" accent: every CTA button and
                        every non-emergency phone number, on EVERY route. It is
                        blue ink, sitewide, with no per-route override — so the
                        only red on any page of this site is the emergency.
                        (Kassia R3 fix 1: red was still resolving to 15 solid-red
                        CTA buttons across the seven sales routes, competing with
                        the one red that is supposed to mean emergency.)

     Components read --accent-call. No component names --red directly any more.
     Form validation reads --form-error (same hue family, a rust, not the alarm).
     `.emergency` is the only component that may spend --red / --emergency.

     There is no route switch for this pair any more. If a CTA on some future
     route needs to be red, it does not — do not hand-recolour a button, and do
     not reintroduce a `.route-*` override. Measured AA, all pairings:
     #fff on #184A86 8.89:1 · #184A86 on paper 8.20:1 · on footer sunk 7.54:1 ·
     #fff on #123A6A 11.42:1 · emergency #8C1A14 on its 7% wash 7.68:1. */
  --emergency: var(--red);
  --emergency-deep: var(--red-deep);
  --form-error: #9A3D2E;
  --form-error-deep: #722E23;
  --accent-call: var(--blue-ink);
  --accent-call-deep: var(--blue-deep);
}

*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; overflow-x: clip; max-width: 100%; }
/* WCAG 2.2 SC 2.4.11 Focus Not Obscured (Minimum).
   Measured: .site-header (sticky, top:0) is 69px at 375, 77px at 768/1280, 86px at 960;
   .mobile-callbar (fixed, bottom:0) is 74.6px up to 959px, plus the iOS safe-area inset.
   Without this, every scroll-into-view lands the focused element flush against the
   viewport edge and UNDER one of those bars. Tabbing forward on contact.html at 375
   put the submit button 0/25 sample points visible behind the call bar; shift-tabbing
   put footer links 0/25 visible behind the header.
   scroll-padding on the scroll container covers EVERY path that scrolls something into
   view — sequential focus, shift-tab, :target hash landings, scrollIntoView() — which is
   why it replaces the old `:target { scroll-margin-top }`. The two must not both be set:
   scroll-padding and scroll-margin ADD, which would overshoot a hash target by 2x. */
html { scroll-padding-top: 96px; scroll-padding-bottom: var(--s5); }
@media (max-width: 959px) {
  html { scroll-padding-bottom: calc(88px + env(safe-area-inset-bottom)); }
}
@media (prefers-reduced-motion: no-preference) {
  html { scroll-behavior: smooth; }
}
body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: var(--text-base);
  line-height: var(--leading-body);
  overflow-wrap: break-word;
  max-width: 100%;
  -webkit-font-smoothing: antialiased;
}
img, svg { display: block; max-width: 100%; height: auto; }
/* The cylinder-with-Texas silhouette is a STRUCTURAL device, not a decoration
   (Aria). Paired with a hairline it reads as a masthead rule instead of a
   stray icon floating above the eyebrow. */
.motif { display: block; width: 56px; height: 76px; color: var(--cylinder); margin: 0 0 var(--s5); }
.motif svg { width: 56px; height: 76px; max-width: 56px; }
.hero-copy .motif {
  display: flex; align-items: center; gap: var(--s5);
  width: auto; height: auto; margin-bottom: var(--s5);
}
.hero-copy .motif::after {
  content: ""; flex: 1 1 auto; height: 1px; background: var(--blue);
}
a { color: var(--blue-ink); text-decoration-thickness: 1px; text-underline-offset: 0.18em; }
a:hover { color: var(--blue-deep); }
:focus-visible { outline: 3px solid var(--focus); outline-offset: 3px; }
/* A blue focus ring on a blue panel is not a focus ring. */
.nav-close:focus-visible, .skip-link:focus-visible { outline-color: var(--paper); outline-offset: -6px; }
/* :target scroll-margin-top removed — html scroll-padding-top above covers hash
   landings too, and the two would stack to a 192px overshoot. */

/* Hierarchy is size + weight + colour + space, never size alone:
   h1 800 / h2 700 / h3 700 at body-adjacent size / eyebrow = body face, caps,
   +0.12em, brand blue. A heading never ends on a widow — balance on the short
   display lines, pretty on running copy. */
h1, h2, h3, h4 {
  font-family: var(--font-display);
  font-weight: var(--wt-display);
  letter-spacing: var(--track-display);
  line-height: var(--leading-display);
  color: var(--blue-deep);
  overflow-wrap: break-word;
  text-wrap: balance;
  margin: 0 0 var(--s4);
}
h1 { font-size: var(--text-3xl); font-weight: var(--wt-display-loud); line-height: 1.06; }
h2 { font-size: var(--text-2xl); }
h3 { font-size: var(--text-lg); letter-spacing: -0.01em; line-height: 1.2; }
h4 { font-size: var(--text-md); letter-spacing: -0.01em; }
p { margin: 0 0 var(--s4); text-wrap: pretty; }
/* Measure. Every running-copy cap on this site reads --prose (see the token, and
   the measurement that set it). Components that need a NARROWER column still set
   their own (.lead 46ch, .year-say .lead 40ch); nothing sets a wider one. */
main p, main li, main dd { max-width: min(var(--prose), 100%); }
.lead {
  font-size: var(--text-md);
  line-height: var(--leading-lead);
  max-width: 46ch;
}
.eyebrow {
  font-family: var(--font-body);
  font-weight: 700;
  font-size: var(--text-2xs);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--blue);
  margin: 0 0 var(--s3);
}
.tnum { font-variant-numeric: tabular-nums; font-feature-settings: "tnum"; }
.muted { color: var(--ink-soft); }

.skip-link {
  position: absolute; left: -999px; top: 0; z-index: 400;
  background: var(--cylinder); color: var(--paper);
  padding: var(--s3) var(--s4); font-family: var(--font-body);
}
.skip-link:focus { left: var(--s4); top: var(--s4); }

.container { width: 100%; max-width: var(--maxw); margin-inline: auto; padding-inline: var(--pad-inline); }
/* Vertical rhythm is deliberately UNEVEN (Aria: identical gaps are a slop tell).
   tall opening -> dense practical block -> air -> the 1946 block -> air. */
.section { padding-block: clamp(56px, 10vw, 140px); }
.section-tight { padding-block: clamp(40px, 6vw, 80px); }
.section-dense { padding-block: clamp(28px, 4vw, 52px) clamp(48px, 7vw, 96px); }
.section-air { padding-block: clamp(64px, 12vw, 156px) clamp(48px, 8vw, 104px); }
.section-sunk { background: var(--paper-sunk); }
.section-1946 { padding-block: clamp(72px, 11vw, 132px); }

/* ---- Header ---- */
.site-header {
  position: sticky; top: 0; z-index: 220;
  background: var(--paper);
  border-bottom: 1px solid var(--paper-rule);
}
/* The header separates from the page once you leave the top. This used to run
   off a JS scroll listener; it is now a CSS scroll-driven animation on the
   compositor (Kassia fix 1, Aria's Motion register). Zero main-thread work,
   zero listeners, so it cannot cost INP. The range is 1px wide on purpose —
   this is a binary state flip, not a fade. Unsupported browser: no shadow,
   the 1px bottom border still separates the header. Reduced motion: the
   shadow is simply always on. */
@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: scroll()) {
    .site-header {
      animation-name: headerSink;
      animation-fill-mode: both;
      animation-timeline: scroll(root block);
      animation-range: 8px 9px;
    }
  }
}
@media (prefers-reduced-motion: reduce) {
  .site-header { box-shadow: 0 1px 0 var(--paper-rule); }
}
@keyframes headerSink { to { box-shadow: 0 1px 0 var(--paper-rule); } }
.nav {
  display: flex; align-items: center; gap: var(--s3);
  padding-block: 10px; min-width: 0; width: 100%;
}
.brand { display: flex; align-items: center; gap: 10px; color: var(--blue-deep); text-decoration: none; min-width: 0; flex: 1; }
.brand:hover { color: var(--blue-deep); }
.brand img { width: 56px; height: 56px; object-fit: contain; background: var(--paper); }
.brand .wm {
  display: flex; flex-direction: column; line-height: 1.05;
  font-family: var(--font-display); font-weight: var(--wt-display);
}
.brand .wm-1 { font-size: var(--text-base); letter-spacing: var(--track-display); }
.brand .wm-2 { font-family: var(--font-body); font-size: var(--text-3xs); letter-spacing: var(--track-caps-wide); text-transform: uppercase; color: var(--blue); font-weight: 700; margin-top: 3px; }
.nav-links {
  display: flex; gap: var(--s4); margin-left: auto; align-items: center;
  list-style: none; padding: 0; margin-block: 0;
}
.nav-links a:not(.btn):not(.nav-phone) {
  font-size: var(--text-sm); font-weight: 600; color: var(--ink);
  text-decoration: none; padding-block: 10px; white-space: nowrap;
}
.nav-links a:not(.btn):not(.nav-phone)[aria-current="page"] { color: var(--blue-deep); box-shadow: inset 0 -2px 0 var(--blue); }
.nav-phone {
  display: inline-flex; align-items: center; gap: 8px;
  min-height: 48px; min-width: 48px;
  font-weight: 700; font-variant-numeric: tabular-nums;
  color: var(--accent-call); text-decoration: none; white-space: nowrap;
}
.nav-phone:hover { color: var(--accent-call-deep); }
.nav-toggle, .nav-close { display: none; }
.nav-backdrop { display: none; }

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  font-family: var(--font-body); font-weight: 700; font-size: var(--text-base);
  padding: 14px 22px; min-height: 48px; min-width: 48px;
  border-radius: var(--r-btn); border: 1.5px solid transparent;
  cursor: pointer; text-decoration: none; text-align: center;
}
.btn-call {
  background: var(--accent-call); color: #fff; border-color: var(--accent-call);
  font-variant-numeric: tabular-nums;
}
.btn-call:hover { background: var(--accent-call-deep); color: #fff; border-color: var(--accent-call-deep); }
.btn-ink {
  background: var(--blue); color: #fff; border-color: var(--blue);
}
.btn-ink:hover { background: var(--blue-deep); color: #fff; }
.btn-ghost {
  background: transparent; color: var(--blue-deep); border-color: var(--blue);
}
.btn-ghost:hover { background: var(--blue); color: #fff; }
.btn-block { width: 100%; }
.btn-row { display: flex; flex-wrap: wrap; gap: var(--s3); align-items: center; }

/* ---- Hero / the counter ----
   FIX-RGS-R9B: Running Horse puts the photograph first under the header
   (rh-reference/index.html:161 `.hero` > `.hero-bg-slider`, then `.hero-copy`
   in the same section at :177). Same pattern, RGS tokens: the figure is the
   first box in `.hero-stage`, full content width; headline / lede / phone CTA
   share that row on a paper panel so they stay readable without borrowing RH
   colours. The notice wall is the next row, not a competing column that
   pushed the photograph below the fold. */
.hero {
  display: grid; gap: var(--s7);
  padding-block: 0 clamp(48px, 8vw, 96px);
  align-items: start;
}
.hero-stage {
  display: grid;
  grid-template-columns: 1fr;
  grid-template-rows: auto auto;
  align-items: start;
}
.hero-stage > .photo-band {
  display: contents;
}
.hero-stage > .photo-band > picture {
  grid-row: 1;
  grid-column: 1;
  display: block;
}
.hero-stage > .photo-band > figcaption {
  grid-row: 2;
  grid-column: 1;
}
.hero-stage > .hero-copy {
  grid-row: 1;
  grid-column: 1;
  align-self: end;
  justify-self: start;
  z-index: 1;
  width: min(36rem, calc(100% - 2 * var(--s5)));
  margin: var(--s5);
  padding: var(--s5) var(--s6);
  /* Bill 2026-09-14: opaque --paper covered the customer's bobtail. Same paper
     token, alpha 0.55 behind the headline → 0.42 over the truck (0.35–0.55). */
  background-color: color-mix(in oklab, var(--paper) 55%, transparent);
  background-image: linear-gradient(
    105deg,
    color-mix(in oklab, var(--paper) 55%, transparent) 0%,
    color-mix(in oklab, var(--paper) 42%, transparent) 100%
  );
  border: 1px solid var(--paper-rule);
}
@media (max-width: 719px) {
  /* Kassia R3: at 390px the panel's own width (~100% - 2*s5) plus the 4:3
     hero crop meant the scrim buried the truck. Lighten it and drop the
     padding so more of the photo shows through and the panel itself is
     shorter top-to-bottom. */
  .hero-stage > .hero-copy {
    padding: var(--s4) var(--s5);
    background-color: color-mix(in oklab, var(--paper) 38%, transparent);
    background-image: linear-gradient(
      105deg,
      color-mix(in oklab, var(--paper) 38%, transparent) 0%,
      color-mix(in oklab, var(--paper) 26%, transparent) 100%
    );
  }
}
@media (min-width: 900px) {
  .hero {
    /* One column: composed photograph, then the notice wall. The old
       1.15fr/0.85fr split sat copy beside notices and parked the photo on
       row two — Bill's "big photo near the top" that was not at the top. */
    grid-template-columns: 1fr;
    row-gap: var(--s6);
    padding-block: 0 var(--s7);
  }
}
.hero-copy h1 { margin-bottom: var(--s5); }
.hero-copy .lead { margin-bottom: var(--s6); }
/* FIX-RGS-R9B / RH tip: CTA under the hero copy (rh-reference/index.html:184–189
   — `.btn-row` with Become a Customer + Call, immediately after `.lead`,
   composed on the photograph). Same order here: lede, then `.hero-phone`,
   then the secondary office link. */
/* KASSIA R8 #8 (Uma R9): home ran the site's WEAKEST pair - a bare link plus a
   ghost outline - on the site's most important route, while all 30 other routes
   run a filled .btn-call. The number is not demoted to that button (the hero
   number is a deliberately larger device than a page-head CTA); it is PROMOTED
   to a filled one at hero scale, and "Write the office" drops to the text link
   it always should have been. One primary, one secondary, same order as every
   other route. Same token pair as .btn-call, so contrast is the pair the
   check-contrast gate already clears. */
.hero-phone {
  display: inline-flex; align-items: center; gap: 12px;
  font-size: var(--text-xl);
  font-weight: 700; font-variant-numeric: tabular-nums;
  letter-spacing: var(--track-display); text-decoration: none;
  min-height: 56px; line-height: 1.1;
  background: var(--accent-call); color: #fff;
  border: 1.5px solid var(--accent-call); border-radius: var(--r-btn);
  padding: 14px 24px;
}
.hero-phone:hover { background: var(--accent-call-deep); border-color: var(--accent-call-deep); color: #fff; }
.hero-second {
  display: inline-block; font-weight: 700; padding-block: 6px;
  color: var(--blue-ink);
}
.hero-phone svg { width: 28px; height: 28px; flex: 0 0 auto; }

/* The wall of notices — NOT cards */
.notice-wall {
  display: flex; flex-direction: column;
  border-top: 1px solid var(--blue);
}
.notice {
  padding: var(--s5) 0;
  border-bottom: 1px solid var(--blue);
}
.notice .k {
  font-size: var(--text-3xs); font-weight: 700; letter-spacing: var(--track-caps);
  text-transform: uppercase; color: var(--blue); margin: 0 0 6px;
}
.notice .v {
  font-family: var(--font-display); font-weight: var(--wt-display);
  font-size: var(--text-md); letter-spacing: var(--track-display); color: var(--blue-deep);
  margin: 0; font-variant-numeric: tabular-nums;
}
.notice .v + .v { margin-top: 0.35em; }
.notice .v a { color: var(--blue-ink); text-decoration: none; }
.notice .v a { text-decoration-color: color-mix(in oklab, var(--blue) 45%, transparent); }
.notice .v a:hover { color: var(--blue-deep); text-decoration: underline; text-decoration-color: currentColor; }
.notice .v a[href^="tel"] { color: var(--accent-call); font-variant-numeric: tabular-nums; }
.notice .v a[href^="tel"]:hover { color: var(--accent-call-deep); }
.notice p { margin: 6px 0 0; font-size: var(--text-base); color: var(--ink-soft); }

/* Cylinder motif — original SVG, not a crop of the mark. Homepage only.
   ── THE ONE SIGNATURE MOMENT (Mira spec §1). ────────────────────────────
   Re-cut from a fade-up to a RESOLVE (Kassia fix 1). The old 900ms
   opacity+translateY(8px)+scale(.92) was the default AI entrance and read as
   "another element fading in". This reads as the mark coming into register:
   the cylinder arrives 6% oversize and SETTLES down into register in 380ms,
   and the Texas cutout punches through the black body 200ms in — so the last
   thing that happens is Texas appearing, which is the whole point of the
   motif. Both parts land together at 380ms.
   THE BODY NEVER STARTS AT OPACITY 0. That is the difference between a resolve
   and a fade-up, and it is also the safe engineering choice: the mark is
   painted solid on the first frame, so a throttled or starved timeline (real
   on an old Android, and reproducible in headless Chromium, where the document
   timeline can sit at 0 until the first frame is demanded) can never leave the
   hero holding an invisible element. Only the decorative Texas punch-out uses
   opacity, and its worst case is a plain black cylinder.
   transform + opacity only. The <span class="motif"> reserves 56x76 at all
   times, so nothing here can shift layout (CLS stays 0).
   Reduced motion: no animation at all, resolved state at first paint. */
@media (prefers-reduced-motion: no-preference) {
  .hero .motif svg { animation: motifSettle var(--dur-resolve) var(--ease-settle) both; }
  .hero .motif .tx { animation: txPunch var(--dur-status) var(--ease-out) 200ms both; }
}
@keyframes motifSettle {
  from { transform: scale(1.06) translateY(-4px); }
  to { transform: none; }
}
@keyframes txPunch {
  from { opacity: 0; }
  to { opacity: 1; }
}

/* Almanac rows (services) — not a 3-up card grid */
.almanac { border-top: 1px solid var(--blue); }
.almanac-row {
  display: grid; gap: var(--s4);
  padding-block: var(--s6);
  border-bottom: 1px solid var(--blue);
  align-items: start;
}
@media (max-width: 719px) {
  /* The numeral on its own line read as an orphan; set it as a running index
     beside the heading instead. */
  .almanac-row { grid-template-columns: 2.4rem 1fr; column-gap: var(--s3); row-gap: var(--s3); }
  .almanac-row .go { grid-column: 2; }
}
@media (min-width: 720px) {
  .almanac-row { grid-template-columns: 4.5rem 1fr auto; gap: var(--s6); }
}
.almanac-row .numeral {
  font-family: var(--font-display); font-weight: var(--wt-display);
  font-variant-numeric: tabular-nums; color: var(--blue);
  font-size: var(--text-md); letter-spacing: var(--track-display-tight);
}
.almanac-row h3 { margin: 0 0 8px; }
.almanac-row p { margin: 0; max-width: min(var(--prose), 100%); }
.almanac-row .go {
  font-weight: 700; white-space: nowrap; align-self: center;
  display: inline-flex; align-items: center; gap: 6px;
  min-height: 44px; text-decoration: none;
  color: var(--blue-ink);
  box-shadow: inset 0 -1px 0 color-mix(in oklab, var(--blue) 45%, transparent);
}
/* Neither webfont subset carries U+2192, so name the stack that draws the arrow
   rather than letting it land on whatever the UA picks. */
.almanac-row .go::after {
  content: "\2192"; font-size: 1.1em; line-height: 1;
  font-family: "Segoe UI Symbol", "Noto Sans Symbols 2", system-ui, sans-serif;
}
.almanac-row .go:hover { color: var(--blue-deep); box-shadow: inset 0 -2px 0 var(--blue); }

/* 1946 block. The numeral is cap-aligned to the H2, not vertically centred
   against the column — centred, it floated free of everything it belongs to. */
.year-block { display: grid; gap: var(--s6); align-items: start; }
@media (min-width: 860px) {
  .year-block {
    grid-template-columns: auto 1fr;
    column-gap: var(--s8); row-gap: var(--s7);
  }
  .year-figure { grid-column: 1 / -1; }
  .year { margin-top: -0.09em; }
}
.year {
  font-family: var(--font-display); font-weight: var(--wt-display-loud);
  font-size: var(--text-year); line-height: 0.82;
  letter-spacing: var(--track-display-tight); color: var(--blue-deep);
  font-variant-numeric: tabular-nums; margin: 0;
}
.year-say .lead { max-width: 40ch; }
.year-figure { margin: 0; }
.page-head > .year-figure { margin-block: var(--s5) var(--s6); }
.year-figure img {
  width: 100%; height: auto; border-radius: var(--r-img);
  aspect-ratio: 21 / 9; object-fit: cover; object-position: center 58%;
}
/* A 21:9 band is ~150px tall on a phone — the archive photo is the emotional
   anchor of this block and needs presence there, so it crops taller. */
@media (max-width: 719px) { .year-figure img { aspect-ratio: 4 / 3; } }

/* ── PHOTO BAND (Dex, DEX-IMAGERY-R9) ────────────────────────────────────────
   The house photo component. Same register as .year-figure — a posted print on
   a shop wall, not a card: square corners (--r-img: 0), a hairline rule under
   the caption, no shadow, no lift, no rounded "media object". One rule for
   every photograph on the site so the band cannot drift page to page.
   Aspect is declared in CSS AND width/height ship on the <img>, so CLS is 0
   before the bytes land. 21:9 is ~150px tall on a phone, which is a strip, not
   a photograph — so it crops to 3:2 there, exactly as .year-figure does. */
.photo-band { margin: 0 0 var(--s6); }
/* Uma R9 + R9B: inside the hero stage the row gap is the spacing, not a margin.
   display:contents on the figure (so the copy can share the photograph's row
   without covering the caption) means the figure has no box to margin. */
.hero-stage > .photo-band > figcaption { margin-bottom: 0; }
.photo-band img {
  width: 100%; height: auto; border-radius: var(--r-img);
  aspect-ratio: 21 / 9; object-fit: cover; object-position: center 55%;
  background: var(--paper-sunk);
}
@media (max-width: 719px) { .photo-band img { aspect-ratio: 3 / 2; } }
/* The hero band carries the page and gets more height on every screen. */
.photo-band--hero img { aspect-ratio: 16 / 9; object-position: center 52%; }
@media (max-width: 719px) { .photo-band--hero img { aspect-ratio: 4 / 3; } }
/* A detail crop is read close, so it is not cropped to a letterbox. */
.photo-band--detail img { aspect-ratio: 16 / 9; object-position: center 50%; }
/* R4 (Bill, in Chrome): the bottle-refill photo's native frame is portrait
   (2:3) and forcing it into 16:9 cut the man's head off. One-off portrait
   band: the full source frame, no cropping (object-fit still "cover" but the
   ratio matches the file exactly, so it is a no-op). Capped narrower than a
   landscape band so a 2:3 image does not run absurdly tall on desktop. */
.photo-band--portrait { max-width: 32rem; margin-inline: auto; }
.photo-band--portrait img { aspect-ratio: 2 / 3; object-position: center top; }
.photo-band figcaption { max-width: 46ch; }
figcaption {
  font-size: var(--text-xs); color: var(--ink-soft); margin-top: var(--s3);
  font-style: normal; letter-spacing: 0.02em;
  padding-top: var(--s2); border-top: 1px solid var(--paper-rule);
  max-width: 32ch;
}

/* Quote */
.quote {
  max-width: 40rem;
  border-left: 2px solid var(--blue);
  padding-left: var(--s5);
  margin-inline: 0;
}
.quote p {
  font-family: var(--font-display); font-size: var(--text-lg);
  line-height: 1.35; letter-spacing: -0.015em; margin: 0 0 var(--s4);
  text-wrap: pretty;
}
.quote footer { font-size: var(--text-sm); color: var(--ink-soft); font-style: normal; }

/* People list */
.people { list-style: none; padding: 0; margin: 0; border-top: 1px solid var(--blue); }
.people li {
  display: grid; gap: 4px 24px;
  padding-block: var(--s4);
  border-bottom: 1px solid var(--blue);
}
@media (min-width: 640px) {
  .people li { grid-template-columns: 10rem 1fr; align-items: baseline; }
}
/* KASSIA R8 #7 (Uma R9). The list's top hairline ran the full 1,052px content
   column while every row inside it stopped at 529px - the prose measure cap that
   applies to every li on the site - so a full-width rule promised a band and the
   right half of it stayed empty paper.
   The rows FILL the promise instead of the rule retreating to them: two named
   people, two columns, one continuous hairline across both. auto-fit with a 24rem
   floor means the column count follows the roster - a third name would need
   1,152px to sit beside the other two and otherwise stacks, so nothing here
   breaks when Colton gets a mailbox (ASK-JACKSON #4). Rows, not cards: no box,
   no radius, no shadow, same hairline the ledger uses. */
@media (min-width: 820px) {
  .people { display: grid; grid-template-columns: repeat(auto-fit, minmax(24rem, 1fr)); }
  /* The gutter is padding, not gap, so the row hairlines meet and the list closes
     on ONE continuous rule instead of two with a 72px bite out of the middle. */
  .people li { max-width: none; }
  .people li + li { padding-inline-start: var(--s8); }
}
.people strong { color: var(--blue-deep); }

/* Town list. Thirteen posted rows, two columns from 560 up — the copy asked for
   two columns and the single 28rem column left the right two-thirds of a desktop
   /service-area empty with thirteen hairlines running under nothing.
   Multicol, not grid: an odd count fills column one and stops, which reads as a
   list that ran out rather than a grid with a hole in it. */
/* Kassia R7 #9: /blog and /404 held their link lists as raw ::marker discs while
   every other link list on the site is a hairline row with an arrow. The
   component was already here under a route-specific name; .link-rows is the same
   component, named for what it does. One alias, no second implementation. */
:is(.town-list, .link-rows) {
  list-style: none; padding: 0; margin: 0;
  border-top: 1px solid var(--blue);
  max-width: 46rem;
}
@media (min-width: 560px) {
  :is(.town-list, .link-rows) { columns: 2; column-gap: var(--s8); }
}
:is(.town-list, .link-rows) li {
  border-bottom: 1px solid var(--blue);
  break-inside: avoid;
  font-family: var(--font-display); font-size: var(--text-md);
  font-weight: var(--wt-display); color: var(--blue-deep);
}
/* WCAG 2.2 SC 2.5.8 — the a11y floor measured these thirteen links at 40x23 and
   45x23 at 375/390 (the only FAIL in the 2026-09-08 run). The link is now the
   whole row: full column width, >=44px tall. The arrow gives the hairline a job
   and marks the row as somewhere to go, not just a name on a list. */
:is(.town-list, .link-rows) a {
  display: flex; align-items: center; justify-content: space-between; gap: var(--s4);
  min-height: 44px; padding-block: var(--s3);
  color: var(--blue-deep); text-decoration: none;
}
:is(.town-list, .link-rows) a::after {
  content: "\2192"; color: var(--blue); font-size: 0.85em; line-height: 1;
  font-family: "Segoe UI Symbol", "Noto Sans Symbols 2", system-ui, sans-serif;
}
:is(.town-list, .link-rows) a:hover { color: var(--blue-ink); text-decoration: underline; text-underline-offset: 0.2em; }

/* WCAG 2.2 SC 2.5.8. Links that sit on their own line get padded to >=24px.
   Links genuinely inside a sentence keep the inline exception. */
.breadcrumb a,
.notice .v a,
.footer-nap .phone,
.page-head > p > a { display: inline-block; padding-block: 5px; }
/* Scoped to main: the header .nav-phone is also an li > a[href^=tel] and it
   sets its own inline-flex. Unscoped, this rule flipped it to inline-block
   and stacked the handset icon above the number in every desktop header. */
main :is(p, li, dd) > a[href^="tel:"]:not(.btn) { display: inline-block; padding-block: 5px; }
/* A link that IS the whole paragraph is a link on its own line, not a link
   inside a sentence, so 2.5.8's inline exception does not cover it. Measured
   21-23px unpadded across the safety section, /propane-delivery and the people
   list; 5px of block padding clears 24. */
/* A list item that IS a link is a link on its own line, so 2.5.8's inline
   exception does not cover it either. Measured 21px tall on the four source
   links in /safety/duty-to-warn's "Where this comes from" list, at all four
   widths — the only FAIL left once the floor was widened from 11 pages to all
   31 on 2026-09-08. Scoped to .plain-list so the town list, whose anchors set
   their own 44px flex row, is untouched. */
.plain-list > li > a:only-child,
main p > a:only-child,
.people a,
.counter-facts dd a,
.map-figure .directions,
.emergency .e-link { display: inline-block; padding-block: 5px; }

/* Breadcrumb */
.breadcrumb { font-size: var(--text-xs); color: var(--ink-soft); margin: 0 0 var(--s4); }
.breadcrumb .sep { margin-inline: 8px; color: var(--blue); }
.page-head { padding-block: clamp(40px, 7vw, 88px) var(--s5); }

/* Form */
.contact-grid { display: grid; gap: var(--s8); }
@media (min-width: 860px) { .contact-grid { grid-template-columns: 1.2fr 0.8fr; } }
.quote-form label { display: block; font-weight: 700; font-size: var(--text-sm); margin-bottom: 6px; }
.quote-form input, .quote-form textarea, .quote-form select {
  width: 100%; font: inherit; font-size: var(--text-base);
  padding: 12px 12px; border: 1px solid var(--blue);
  border-radius: var(--r-input); background: var(--paper); color: var(--ink);
  min-height: 48px;
}
.quote-form textarea { min-height: 140px; resize: vertical; }
.quote-form input:hover, .quote-form textarea:hover { border-color: var(--blue-deep); }
.quote-form input[aria-invalid="true"], .quote-form textarea[aria-invalid="true"] {
  border-color: var(--form-error); border-width: 2px; background: color-mix(in oklab, var(--form-error) 4%, var(--paper));
}
.quote-form .field-error {
  display: none; margin: 6px 0 0; font-size: var(--text-xs); font-weight: 700; color: var(--form-error-deep);
}
.quote-form .field-error.is-on { display: block; }
.quote-form button[disabled] { opacity: 0.55; cursor: not-allowed; }
.form-field { margin-bottom: var(--s4); }
.form-row.two { display: grid; gap: var(--s4); }
@media (min-width: 560px) { .form-row.two { grid-template-columns: 1fr 1fr; } }
.hp-field { position: absolute; left: -9999px; height: 0; overflow: hidden; }
/* "(required)" is a label hint, not an error. Words, not a red asterisk — a lone
   glyph is not a name for anything (Io §4: required marked in words). */
.req { color: var(--ink-soft); font-weight: 400; }
.big-phone {
  display: inline-flex; align-items: center; gap: 10px;
  font-size: var(--text-xl); font-weight: 700;
  font-variant-numeric: tabular-nums; color: var(--accent-call); text-decoration: none;
  min-height: 48px;
}
.big-phone:hover { color: var(--accent-call-deep); }
address { font-style: normal; }

/* Footer — lightened panel so the full-colour mark stays on a light field */
.site-footer {
  background: var(--paper-sunk);
  color: var(--ink);
  padding-block: var(--s8) var(--s5);
  border-top: 3px solid var(--blue);
  font-size: var(--text-base);
}
.site-footer .foot-h {
  font-family: var(--font-body); font-weight: 700; font-size: var(--text-3xs); letter-spacing: var(--track-caps);
  text-transform: uppercase; color: var(--blue); margin: 0 0 var(--s3); line-height: 1.2;
}
.site-footer .foot-h + .footer-links + .foot-h { margin-top: var(--s5); }
/* ONE footer, on all 31 routes. There were SIX different footers in the build on
   2026-09-08: the nine core pages carried People + Follow and no safety links,
   while the thirteen town pages, the seven safety pages, /about and /blog carried
   the safety links and no People. No page showed a visitor everything, and which
   one you got depended on which page you happened to land on.
   Four groups now, everywhere: brand + NAP · On this site · Propane safety ·
   People + Follow. Three columns from 760 (the fourth wraps under the brand),
   four from 1040 where 245px per column still holds "Storms, power outages,
   high water" on two lines. */
.footer-grid { display: grid; gap: var(--s7); }
@media (min-width: 760px) { .footer-grid { grid-template-columns: 1.4fr 1fr 1fr; } }
/* R6, Kassia #6 (three widows on all 31 footers). The widows were not a
   text-wrap problem, they were a COLUMN-WIDTH problem: the fr ratios were set
   by eye, so the column holding the SHORTEST links got 164px and the column
   holding the longest — "Storms, power outages, high water", 33 characters —
   got 212px. Measured at 1440: "Tanks & bottle / refills", "Your tank and your
   / bottles", "Storms, power outages, / high water", "Tank-install quotes: call
   / (817) 444-4613". Sizing each column to what it actually holds costs
   nothing and fixes all four in one rule, on all 31 footers, with no per-link
   nowrap span to maintain in 31 files. The minmax floors are the measured
   ink widths at 15px Public Sans + 8px of slack. */
/* 1100, not 1040: the four content-sized floors plus three gaps need 968px of
   grid, and the container only reaches that at ~1062px of viewport. Below it
   the three-column footer is correct and the fourth group wraps to a new row. */
@media (min-width: 1100px) {
  .footer-grid {
    grid-template-columns:
      minmax(210px, 1fr)     /* mark + NAP: the address wraps by design */
      minmax(180px, 0.74fr)  /* "Tanks & bottle refills" = 172px of ink */
      minmax(244px, 1.06fr)  /* "Your tank and your bottles" = 219px */
      minmax(262px, 1fr);    /* "jacksonb@ruralgassupply.com" = 254px, and it
                                is one unbreakable token — under 254 it does not
                                wrap, it CLIPS. This floor is the hard one. */
    column-gap: var(--s5);
  }
}
/* Two links cannot fit on one row at any sane column width in a four-column
   footer: "Tank-install quotes: call (817) 444-4613" (330px of ink) and
   "Storms, power outages, high water" (290px). They get a DELIBERATE two-line
   break instead of an accidental one — balance splits them 3/3 and 2/3 rather
   than dropping "refills" or "(817) 444-4613" alone onto a second line — and
   the phone number never breaks away from the word that introduces it. */
.footer-links li { text-wrap: balance; }
.footer-links li .tnum { white-space: nowrap; }
.footer-brand img { width: 96px; height: 96px; object-fit: contain; margin-bottom: var(--s3); }
.footer-nap { line-height: 1.7; }
.footer-nap .phone { color: var(--accent-call); font-weight: 700; font-variant-numeric: tabular-nums; text-decoration: none; }
.footer-links { list-style: none; padding: 0; margin: 0; line-height: 1.5; }
/* WCAG 2.2 target size: these read 16-19px tall unpadded. Pad to >=24. */
.footer-links a { display: inline-block; padding-block: 5px; text-decoration: none; color: var(--ink); }
.footer-links a:hover { color: var(--blue-deep); text-decoration: underline; }
.footer-bottom {
  border-top: 1px solid var(--paper-rule); margin-top: var(--s7);
  padding-top: var(--s4); font-size: var(--text-xs); color: var(--ink-soft);
  display: flex; flex-wrap: wrap; gap: var(--s3) var(--s4); justify-content: space-between;
  align-items: baseline;
}
.footer-bottom a { display: inline-block; padding-block: 5px; }

/* Sticky call bar — the one red job. Runs to 959px, i.e. everywhere the
   desktop nav phone is collapsed behind the hamburger. */
.mobile-callbar { display: none; }
@media (max-width: 959px) {
  .mobile-callbar {
    display: flex; gap: var(--s3);
    position: fixed; left: 0; right: 0; bottom: 0; z-index: 180;
    background: var(--paper); border-top: 1px solid var(--blue);
    box-shadow: 0 -6px 18px -14px color-mix(in oklab, var(--cylinder) 60%, transparent);
    padding: 10px 14px; padding-bottom: max(10px, env(safe-area-inset-bottom));
  }
  /* "Call (817) 444-4613" wrapped and broke the number across two lines
     ("444-" / "4613") at 375. A phone number must never wrap. */
  .mobile-callbar .btn { flex: 1 1 0; min-width: 0; padding-inline: var(--s3); }
  .mobile-callbar .btn-call { flex-grow: 1.7; white-space: nowrap; font-size: var(--text-sm); font-variant-numeric: tabular-nums; }
  .mobile-callbar .btn-ghost { font-size: var(--text-sm); white-space: nowrap; }
  body { padding-bottom: 92px; }

  /* ── R8, Kassia R7 #7. Measured with elementsFromPoint: at scroll 0 the 75px
     bar sat over live content on every route — part of the emergency strip on
     /service-area, part of "The six steps, in order" on /safety/if-you-smell-gas.
     A bar that covers the emergency instruction is worse than no bar.

     The bar now arrives once the reader has scrolled past the in-page phone CTA,
     on the same `scroll(root block)` timeline the header already runs (:242) —
     compositor-only, transform-only, zero JS, zero listeners, so it cannot cost
     INP. 220px to 300px is measured: the shortest in-page call CTA on any route
     sits at y=213 at 375 (/safety hub), so the bar is still off-screen while any
     of them is in view and fully seated 300px in.

     THREE WAYS IT STAYS SAFE, all deliberate:
     - transform only. `body` keeps its 92px padding, so nothing reflows and CLS
       stays 0.00 whether the bar is up or down.
     - :focus-within drops the animation, so the base state (seated) applies the
       instant a keyboard reaches it — the bar can never be focused off-screen.
       That is why the hide is a transform and not visibility/display: the two
       buttons stay in the tab order at every scroll position.
     - reduced motion and any browser without scroll timelines never enter this
       block, so they get today's always-visible bar. The phone is never removed
       by a progressive enhancement. */
  @media (prefers-reduced-motion: no-preference) {
    @supports (animation-timeline: scroll()) {
      .mobile-callbar {
        animation-name: callbarSeat;
        animation-fill-mode: both;
        animation-timing-function: var(--ease-out);
        animation-timeline: scroll(root block);
        animation-range: 220px 300px;
      }
      .mobile-callbar:focus-within { animation-name: none; }
    }
  }
}
@keyframes callbarSeat {
  from { transform: translateY(110%); }
  to { transform: none; }
}

/* Mobile nav */
@media (max-width: 520px) {
  .brand .wm { display: none; }
  .brand img { width: 48px; height: 48px; }
}
@media (max-width: 959px) {
  .nav-toggle {
    display: inline-flex; align-items: center; justify-content: center;
    margin-left: auto; width: 48px; height: 48px;
    border: 1px solid var(--blue); background: var(--paper); color: var(--blue-deep); cursor: pointer;
  }
  .nav-toggle svg { width: 24px; height: 24px; }
  .nav-phone-mobile { margin-left: auto; }
  .nav-links {
    position: fixed; inset: 0 0 0 auto; width: min(340px, 92vw);
    flex-direction: column; align-items: stretch; gap: 0;
    background: var(--paper); border-left: 1px solid var(--blue);
    padding: 0; margin: 0; z-index: 230;
    transform: translateX(100%);
    /* A closed drawer must be GONE, not merely off to the right: otherwise its
       8 links stay in the tab order, stay readable to a screen reader, and
       render into a full-page screenshot. */
    visibility: hidden; pointer-events: none;
  }
  .nav-links.open { transform: translateX(0); visibility: visible; pointer-events: auto; }
  @media (prefers-reduced-motion: no-preference) {
    .nav-links { transition: transform var(--dur-drawer) var(--ease-out), visibility 0s linear var(--dur-drawer); }
    .nav-links.open { transition: transform var(--dur-drawer) var(--ease-out), visibility 0s; }
  }
  .nav-close {
    display: inline-flex; align-items: center; justify-content: flex-end;
    width: 100%; height: 56px; padding-inline: var(--s5);
    background: var(--blue-deep); color: var(--paper); border: 0; cursor: pointer;
  }
  .nav-close svg { width: 26px; height: 26px; }
  .nav-links a:not(.btn) {
    display: flex; align-items: center; width: 100%; min-height: 48px;
    padding: 0 var(--s5); border-bottom: 1px solid var(--paper-rule);
  }
  .nav-links li:last-child { margin-top: auto; padding: var(--s5); }
  .nav-links .nav-cta { width: 100%; }
  .nav-backdrop {
    display: block; position: fixed; inset: 0; background: color-mix(in oklab, var(--cylinder) 45%, transparent);
    z-index: 225; opacity: 0; visibility: hidden;
  }
  .nav-backdrop.open { opacity: 1; visibility: visible; }
  /* The overlay fades on the same 220ms clock as the panel it belongs to, so
     the two read as ONE object arriving rather than two. visibility is stepped,
     never eased, so a closed backdrop is genuinely gone and never eats a tap. */
  @media (prefers-reduced-motion: no-preference) {
    .nav-backdrop { transition: opacity var(--dur-drawer) var(--ease-out), visibility 0s linear var(--dur-drawer); }
    .nav-backdrop.open { transition: opacity var(--dur-drawer) var(--ease-out), visibility 0s; }
  }
}
@media (min-width: 960px) {
  .nav-phone-mobile { display: none; }
  /* Red does ONE job. The desktop header already carries the number in red;
     a second red filled button beside it doubled the accent and split the
     focal point. The button survives inside the mobile drawer only. */
  .nav-cta { display: none; }
}

.empty-blog {
  border-top: 1px solid var(--blue); border-bottom: 1px solid var(--blue);
  padding: var(--s7) 0; max-width: 40rem;
}

/* The drawer close button lives inside an <li> so the <ul> only ever has
   <li> children (axe rule `list`, serious, was firing on all 10 pages at 390). */
.nav-close-item { list-style: none; }

/* ═══ SAFETY SECTION + STOREFRONT (Uma, 2026-09-06, to Io's IA spec) ═══════ */

/* The emergency strip. The ONLY component allowed to spend --emergency.
   Not a CTA: it is never split-tested, never counted as a conversion, and it
   sits ABOVE the sales content in reading order on every page that carries it.
   A tinted field under a heavy top rule instead of a red flood — the red stays
   bounded enough to still read as an alarm next to the ordinary blue CTAs.
   PERTA R3 NOTE 8 (record, not a change): the 7% tint is 1.11:1 against paper —
   it is decoration. The 4px top rule and the .emergency-mark (both 5.96:1) are
   the ONLY things that make this read as a block, so a later "soften the strip"
   edit must not drop either one. Delete the rule and the component disappears
   for anyone who cannot see a 1.11:1 wash. */
.emergency {
  border-top: 4px solid var(--emergency);
  border-bottom: 1px solid color-mix(in oklab, var(--emergency) 30%, transparent);
  background: color-mix(in oklab, var(--emergency) 7%, var(--paper));
  padding: var(--s5) clamp(16px, 4.5vw, 40px);
  margin-block-start: 3rem;
  display: grid; gap: var(--s3) var(--s5); align-items: start;
}
@media (min-width: 720px) { .emergency { grid-template-columns: 40px 1fr; } }
.emergency-mark { color: var(--emergency); width: 40px; height: 40px; }
.emergency h2, .emergency .e-h {
  font-family: var(--font-display); font-weight: var(--wt-display-loud);
  font-size: var(--text-lg); line-height: 1.15; letter-spacing: var(--track-display);
  color: var(--emergency-deep); margin: 0 0 var(--s3); text-wrap: balance;
}
.emergency p { color: var(--ink); margin: 0 0 var(--s3); max-width: min(var(--prose), 100%); font-weight: 600; }
.emergency p:last-child { margin-bottom: 0; }
.emergency a[href^="tel:"] {
  color: var(--emergency-deep); font-weight: 800; font-variant-numeric: tabular-nums;
  display: inline-block; padding-block: 3px;
}
.emergency a[href^="tel:"]:hover { color: var(--emergency); }
.emergency .e-link { font-weight: 700; display: inline-block; padding-block: 4px; }
/* In the footer the strip is a band across the column, not a card inside one. */
.site-footer .emergency { margin-bottom: var(--s7); }
/* EDGE STATE — /404 and /410 end on the emergency strip, so its bottom hairline
   was landing flush against the footer's 3px blue rule: two rules touching, and
   the strip read as part of the footer instead of the last thing on the page.
   Measured 0px between them before, 104px after (48px at 375). Scoped to a
   .container that IS the last child of main, so it can only ever hit a page that
   ends on a bare block — no other route matches. */
main > .container:last-child { padding-bottom: clamp(48px, 8vw, 104px); }

/* Hairline rows to the child pages. Hub, home teaser and footer share it —
   never a 3-up card grid, never a nested card. */
.safety-rows { list-style: none; padding: 0; margin: 0; border-top: 1px solid var(--blue); }
.safety-rows li { border-bottom: 1px solid var(--blue); padding-block: var(--s4); }
.safety-rows a {
  font-family: var(--font-display); font-weight: var(--wt-display);
  font-size: var(--text-md); letter-spacing: var(--track-display);
  color: var(--blue-deep); text-decoration: none; display: block; min-height: 28px;
}
.safety-rows a:hover { text-decoration: underline; text-underline-offset: 0.2em; }
.safety-rows p { margin: 2px 0 0; color: var(--ink-soft); font-size: var(--text-sm); max-width: 44ch; }

.emergency .plain-list > li { font-weight: 600; margin-bottom: var(--s2); }

/* Right-gutter teaser (Io §8) — fills the desktop gutter beside the testimonial
   instead of stacking one more full-width band. Same 1.15/0.85 split as the
   hero, so the page has two columns in the same proportion, not three widths. */
.gutter-split { display: grid; gap: var(--s7); align-items: start; }
@media (min-width: 900px) { .gutter-split { grid-template-columns: 1.15fr 0.85fr; gap: var(--s8); } }
.safety-teaser { border-top: 3px solid var(--blue); padding-top: var(--s5); }
.safety-teaser h2 { font-size: var(--text-lg); max-width: 22ch; }

/* Kassia #6: at 375 the hero phone link sat at y=497 with a 56px box, i.e. on
   the fold line of a 667px handset. Tighten the stack ~56px so the number is
   fully above it. Only the gaps move — no type size is touched. */
@media (max-width: 430px) {
  .hero { padding-block: 0 32px; gap: var(--s6); }
  .hero-copy .motif { margin-bottom: var(--s4); }
  .hero-copy h1 { margin-bottom: var(--s4); }
  .hero-copy .lead { margin-bottom: var(--s5); }
  .hero-stage > .hero-copy { margin: var(--s4); padding: var(--s4) var(--s5); width: calc(100% - 2 * var(--s4)); }
}

/* Kassia R3 #13, re-cut R6 (Kassia R5 #7: "a solid black lozenge with the Texas
   knockout invisible" at 375 — the site's one identity moment, at the width
   most visitors use). The R3 pass treated the symptom with a 1.2px paper stroke
   around the state; the cause was that the path's own bbox sat right of and
   above the cylinder body, so the shape ran off the black and read as a notch.
   That is fixed in the geometry now (index.html, transform on .tx), so the
   stroke comes off — at 88% it was fattening a 28px silhouette by 9% and
   rounding the panhandle and the Gulf coast back into a blob. What stays is the
   SIZE: below 480 the cylinder goes to 70x96, which puts the state at ~28 CSS
   px, i.e. ~56 device px on the 2x screens this width implies. Geometry only —
   motifSettle and txPunch are untouched, and .motif still reserves its box, so
   CLS stays 0. */
@media (max-width: 480px) {
  .hero-copy .motif svg { width: 70px; height: 96px; max-width: 70px; }
}

/* "Do this first" — deliberately the largest type on the site. */
.do-first {
  font-family: var(--font-display); font-weight: var(--wt-display-loud);
  font-size: clamp(2.1rem, 6vw + 0.6rem, 3.6rem); line-height: 1.02;
  letter-spacing: var(--track-display-tight); color: var(--emergency-deep);
  margin: 0 0 var(--s6); max-width: 16ch;
}
.do-first span { display: block; }

/* The six steps. Kept a real <ol> — ::marker is styled, list semantics are not
   thrown away with list-style: none. */
.steps { padding-left: 2.4rem; margin: 0; border-top: 1px solid var(--blue); }
.steps > li { border-bottom: 1px solid var(--blue); padding-block: var(--s5); }
.steps > li::marker {
  font-family: var(--font-display); font-weight: var(--wt-display-loud);
  color: var(--blue); font-size: 1.25em; font-variant-numeric: tabular-nums;
}
.steps h3 { margin: 0 0 var(--s2); }
.steps p { margin: 0; }
.steps p + p { margin-top: var(--s3); }

/* Ordinary numbered / bulleted safety copy */
.plain-list { padding-left: 1.4rem; margin: 0 0 var(--s4); }
.plain-list > li { margin-bottom: var(--s3); max-width: min(var(--prose), 100%); }
.plain-list > li::marker { color: var(--blue); }
.plain-list > li:last-child { margin-bottom: 0; }
/* Kassia R7 #9, the third list she named: /propane-delivery's "Before we come
   out" is a set of parallel physical checks, not prose and not links — the one
   place on the site a disc was doing a hairline's job. Same rule metrics as
   .steps, no arrow (nothing here goes anywhere), list semantics kept: the disc
   is dropped by padding the marker out of the box, not by list-style: none.
   Scoped to .ruled so the twelve prose lists on the safety routes are untouched
   — those pages are already 4,500px of ruled prose and do not need more rules. */
.plain-list.ruled {
  padding-left: 0; list-style: none;
  border-top: 1px solid var(--paper-rule);
}
.plain-list.ruled > li {
  margin-bottom: 0; padding-block: var(--s4);
  border-bottom: 1px solid var(--paper-rule);
}

/* Call block on the leak page: 911 first, us second — stated, not styled away. */
.call-ladder { border-top: 1px solid var(--blue); margin-bottom: var(--s5); }
.call-ladder div {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--s2) var(--s4);
  padding-block: var(--s4); border-bottom: 1px solid var(--blue);
}
.call-ladder .num {
  font-family: var(--font-display); font-weight: var(--wt-display-loud);
  font-size: var(--text-xl); letter-spacing: var(--track-display);
  color: var(--emergency-deep); font-variant-numeric: tabular-nums; text-decoration: none;
  min-height: 44px; display: inline-flex; align-items: center;
}
.call-ladder .num:hover { color: var(--emergency); }
.call-ladder .who { color: var(--ink-soft); font-size: var(--text-sm); }

/* A safety route and the interior sales routes are READING COLUMNS, not landing pages. A 64ch measure inside
   the 1180px marketing container left the whole right half of a 1440 desktop
   empty and the text hugging the left edge. The column is centred instead, and
   the section rhythm tightens with a hairline between sections so it reads as
   one article rather than a stack of floating blocks. Header, footer and the
   emergency strip keep the full site width. */
:is(.route-safety, .route-article) main .container { max-width: 748px; }
:is(.route-safety, .route-article) main > section.section-tight { padding-block: clamp(28px, 4vw, 48px); }
:is(.route-safety, .route-article) main > section.section-tight + section.section-tight {
  /* Not border-top: a border on a .container spans its padding box, so every
     section divider ran var(--pad-inline) (57.6px at 1280) past the text on both
     sides while every other rule on this site — notice wall, almanac, town list,
     page-note, prev/next — stops exactly at the copy. One inset 1px rule drawn
     across the CONTENT box, no extra markup, no magic number. */
  background-image: linear-gradient(var(--paper-rule), var(--paper-rule));
  background-size: calc(100% - 2 * var(--pad-inline)) 1px;
  background-position: top center;
  background-repeat: no-repeat;
}
:is(.route-safety, .route-article) .page-head { padding-bottom: var(--s4); }

/* The price answer (Quill R3 #4). Sits under a block that already ends in a
   rule (.almanac last row, .steps last li), so it carries no rule of its own --
   only the space that separates it from the list it is not part of. */
.market-note { margin-top: var(--s5); }

/* Shared safety-page foot (Lex edit 3). Sits directly above the strip. */
.page-note {
  border-top: 1px solid var(--paper-rule);
  padding-top: var(--s4); margin-top: var(--s7);
  font-size: var(--text-sm); color: var(--ink-soft); max-width: min(var(--prose), 100%);
}

/* Prev / next inside the safety section */
.prevnext {
  display: grid; gap: var(--s4); border-top: 1px solid var(--blue);
  padding-top: var(--s5); margin-top: var(--s6);
}
@media (min-width: 640px) { .prevnext { grid-template-columns: 1fr 1fr; gap: var(--s6); } }
.prevnext a { display: block; text-decoration: none; padding-block: 4px; }
.prevnext .k {
  display: block; font-size: var(--text-3xs); font-weight: 700;
  letter-spacing: var(--track-caps); text-transform: uppercase; color: var(--blue); margin-bottom: 4px;
}
.prevnext .t {
  font-family: var(--font-display); font-weight: var(--wt-display);
  font-size: var(--text-md); color: var(--blue-deep); letter-spacing: var(--track-display);
}
.prevnext a:hover .t { text-decoration: underline; text-underline-offset: 0.2em; }
@media (min-width: 640px) { .prevnext .next { text-align: right; } }

/* A fact we do not have yet. Visible on purpose so nobody ships a guess. */
.pending {
  display: inline-block; font-family: var(--font-body); font-weight: 700;
  font-size: var(--text-2xs); letter-spacing: 0.04em; color: var(--ink-soft);
  background: color-mix(in oklab, var(--blue) 7%, var(--paper));
  border: 1px dashed color-mix(in oklab, var(--blue) 40%, transparent);
  padding: 3px 8px; margin-block: 2px;
}
/* SC 1.3.1. The dashed border is the ONLY thing that says "we do not have this
   fact yet", and a border is not information a screen reader can reach — the
   chip on /safety read out as "Read the safety booklet, PERC duty-to-warn
   booklet, ask Jackson K", i.e. as a fact. This puts the word in the a11y tree
   for every chip, present and future, without touching the frozen copy or the
   generators that emit it. Absolutely positioned + clipped, so it is in the
   tree and out of the picture; verified in the Chrome AX tree, not assumed. */
.pending::before {
  content: "Pending: ";
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}
/* Kassia R5 #4. Seven chips were printing build notes at the visitor ("owed to
   Quill", "the office knows, the site does not"). The fact is still held and the
   note still lives in an HTML comment beside each one — what changed is who the
   sentence is addressed to. Once a chip reads as a plain ask ("Ask us … call the
   number"), it states no fact we lack, so the placeholder dress and the
   "Pending: " announcement are both wrong: it is body copy now, and a screen
   reader must not hear a fact called pending when nothing pending is spoken.
   data-copy="pending-quill" is the handle Quill reviews these seven by. */
.pending[data-copy="pending-quill"] {
  display: inline; font-size: inherit; letter-spacing: normal;
  font-weight: 600; color: var(--blue-deep);
  background: none; border: 0; padding: 0; margin-block: 0;
}
.pending[data-copy="pending-quill"]::before { content: none; }
/* The number inside the ask is an inline link in a sentence, so SC 2.5.8's
   inline exception covers it — but it is also the whole point of the sentence,
   so give it a hit box over 24px anyway. Inline padding-block grows the target
   without touching the line box: measured 0 layout shift. */
.pending[data-copy="pending-quill"] a[href^="tel:"] { padding-block: 5px; }

/* The counter / storefront block */
.counter-split { display: grid; gap: var(--s6); align-items: start; }
/* KASSIA R8 #3 (Uma R9). The map used to live in the 0.9fr column - 464px at
   1440 - and an 800-unit artboard at 464px renders its 12-unit county and
   attribution labels at 7.0 css px. Measured, not estimated: 464.2px rendered,
   464.2 / 800 * 12 = 7.0.
   The figure now takes a row of its own at the full content column (1,052px at
   1440 -> 15.8 css px for the same label, 28.9 for a town name), which is the
   same treatment /service-area got in R8. The ledger moves into the column the
   map vacated, so the row above stays a two-column composition and the widening
   adds no empty paper. The <img> keeps width=800 height=600 and aspect-ratio
   4/3, so the box is still reserved from the first byte and CLS stays 0. */
@media (min-width: 820px) {
  .counter-split { grid-template-columns: 1fr 0.9fr; gap: var(--s8); }
  .counter-split > .map-figure { grid-column: 1 / -1; }
  /* FIX-RGS-R9B: this ledger used to be `position: sticky; top: 112px`. While
     stuck it was painted over the map row (`grid-column: 1 / -1`) on /,
     /contact and every page that reuses .counter-split — Bill's WHERE / HOURS
     / PHONE / CALL / JACKSON / DARREN overlay. It is in-flow again: short
     column beside the copy, map on its own row underneath, nothing travels. */
  .counter-split > .counter-facts { margin-top: 0; }
}
.counter-facts { border-top: 1px solid var(--blue); margin: var(--s5) 0 0; }
.counter-facts div {
  display: grid; gap: 2px var(--s5); padding-block: var(--s4);
  border-bottom: 1px solid var(--blue);
}
@media (min-width: 480px) { .counter-facts div { grid-template-columns: 7.5rem 1fr; align-items: baseline; } }
/* A heading straight after the facts list was sitting on the list's own closing
   hairline, so the rule read as an underline for the heading instead of the
   bottom of the table. Measured 0px gap before, 32px after. */
.counter-facts + h3 { margin-top: var(--s6); }
.counter-facts dt {
  font-size: var(--text-3xs); font-weight: 700; letter-spacing: var(--track-caps);
  text-transform: uppercase; color: var(--blue);
}
.counter-facts dd {
  margin: 0; font-family: var(--font-display); font-weight: var(--wt-display);
  font-size: var(--text-md); color: var(--blue-deep); letter-spacing: var(--track-display);
  font-variant-numeric: tabular-nums;
}
.counter-facts dd a[href^="tel:"] { color: var(--accent-call); text-decoration: none; }
.counter-facts dd a[href^="tel:"]:hover { color: var(--accent-call-deep); }

/* Static map. An <img> with width/height, never an iframe: an embed costs a
   third-party connection and a layout shift for a one-tap job. */
/* FIX-RGS-R9B / RH tip: the map is a column citizen, not a sticky overlay.
   Running Horse sizes a feature figure to the column it lives in
   (rh-reference/index.html:312 `.feature-photo` beside copy, not over it).
   min-width:0 lets the grid shrink the figure; width:100% fills that column. */
.map-figure { margin: 0; min-width: 0; max-width: 100%; width: 100%; }
.map-figure img {
  width: 100%; height: auto; aspect-ratio: 4 / 3;
  border: 1px solid var(--blue); background: var(--paper-sunk); object-fit: cover;
}
.map-figure figcaption { max-width: 44ch; }
.map-figure .directions { display: inline-block; font-weight: 700; padding-block: 6px; margin-top: var(--s3); }
/* Uma R9. Two rules, both about the artboard-to-container fit the widening opened.
   (1) The 560-unit phone board is a REDRAW for a small slot, not a zoom. Between
   768 and 959 the content column is 699-873px, and blowing a 560 board up to 873
   makes a 22-unit town name land at 34 css px - large-print, coarse against the
   body text. Cap it at its own natural 560 and centre it: the labels then render
   at exactly the 20/22/29 px they were drawn at.
   (2) At >=960 the 800 board takes the full column and the caption sits beside
   "Get directions" instead of leaving the right half of the band empty. */
@media (max-width: 959px) {
  .map-figure picture { display: block; max-width: 560px; margin-inline: auto; }
}
@media (min-width: 960px) {
  .map-figure { display: grid; grid-template-columns: 1fr auto; align-items: baseline; column-gap: var(--s6); }
  .map-figure picture { grid-column: 1 / -1; }
  .map-figure .directions { justify-self: end; }
}

/* Form states (Io §4). Idle is the default; the rest are toggled by main.js and
   every one of them keeps the phone on screen. */
.storefront {
  font-family: var(--font-display); font-weight: var(--wt-display);
  font-size: var(--text-lg); line-height: 1.3; letter-spacing: var(--track-display);
  color: var(--blue-deep); margin: 0 0 var(--s3);
}
.hours-line { font-weight: 700; margin-bottom: var(--s3); }
.quote-form select { appearance: auto; }
.form-status { display: none; border-top: 3px solid var(--blue); padding-top: var(--s5); margin-top: var(--s5); }
.form-status.is-on { display: block; }
.form-status.is-error { border-top-color: var(--form-error); }
.form-status h2 { margin-bottom: var(--s3); }
.form-note { font-size: var(--text-sm); color: var(--ink-soft); }

/* ═══ MOTION — Mira, MIRA-MOTION-SPEC-20260906.md (2026-09-06) ═════════════
   Register: near-still. ONE signature moment (the hero cylinder resolve,
   above), and below this line nothing but state changes and one restrained
   scroll reveal on the home page.

   THREE HARD EXCLUSIONS, and they are the reason the selectors below look
   fussy. Nothing here may ever animate:
     1. `.emergency` — the strip, on every page it appears. It is the one red
        thing on the page and a person reading it may be in a live emergency.
     2. `body.route-safety` — all six safety routes, including
        /safety/if-you-smell-gas and every safety instruction list. Hover and
        press feedback are scoped `body:not(.route-safety)` for exactly this.
     3. Any layout property. transform and opacity only, everywhere.

   The drawer (above) is the one exception to rule 2: it is a nav affordance,
   it predates this spec, Perta certified it, and a drawer that teleports is a
   worse experience on a safety page, not a better one. */

/* ── Scroll reveals — HOME PAGE ONLY, two groups, and that is the whole list.
   CSS scroll-driven (`animation-timeline: view()`), not IntersectionObserver:
   it runs on the compositor, costs zero JS and zero main-thread work, and so
   cannot regress INP. Aria's register asks for scroll-driven animation by
   name; Kassia deducted for its absence.
   CONTENT IS VISIBLE AT REST. The parked opacity:0 state exists ONLY inside
   `@supports (animation-timeline: view())`, so a browser without scroll
   timelines never sees it and there is no observer path that can fail and
   leave a section blank. An element already in view at first paint evaluates
   its timeline at 100% and paints resolved.
   Stagger is not authored — the three almanac rows enter the viewport at
   different moments, so the timeline staggers them for free, under 60ms
   apart at a normal scroll rate.
   `screen` is load-bearing: a print stylesheet has no scroll timeline, so
   without it a printed home page would come out with three blank rows. */
@media screen and (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {
    /* FIX-RGS-R9B: `#the-counter .counter-facts > div` used to ride `rise`
       (translateY 10px on a view() timeline). Combined with the sticky ledger
       it slid the WHERE/HOURS/PHONE rows onto the map. Almanac rows stay;
       they are in-flow and do not share a cell with a figure. */
    .almanac-row {
      animation-name: rise;
      animation-fill-mode: both;
      animation-timing-function: var(--ease-out);
      animation-duration: var(--dur-reveal);
      animation-timeline: view(block);
      animation-range: entry 8% cover 18%;
    }
  }
}
/* TRANSFORM ONLY — no opacity, deliberately (Uma 2026-09-08).
   The parked `opacity: 0` was safe for a person scrolling, and it was still
   wrong. Any full-page capture (Chrome's captureBeyondViewport, which resizes
   the viewport to the whole document) re-evaluates a view() timeline at entry
   0%, so the three almanac rows — "Three lines of work", the section that says
   what this company sells — rendered as a BLANK BAND in every full-page
   screenshot of the home page. Verified live: opacity is 1 in a real 1280x900
   viewport at every scroll position, and 0 in the capture.
   A reveal that can hide content in any rendering path is a reveal with a
   failure mode. A 10px settle reads the same to a person scrolling and cannot
   blank anything, in any path, ever. Mira's register (near-still, ease-out,
   compositor-only) is unchanged. */
@keyframes rise {
  from { transform: translateY(10px); }
  to { transform: none; }
}

/* ── THE ONE SIGNATURE MOMENT (Kassia R8 #5, Mira's register). One page, one
   element, one moment: the 9rem 1946 on the home page and nothing else on the
   site. Constitution Art. 5 licenses exactly one per key page; R8 spent zero,
   and "every element arrives with the same 10px settle" was the deduction.

   What it does: the numeral is set at 0.88 and grows to full size from its own
   left edge as the band crosses, so 1946 STAMPS itself onto the page against
   the fixed left margin every heading on the site stands on. The date is the
   whole argument of the company and now it is the only thing on the site that
   behaves differently from everything else.

   Why it is safe:
   - transform only, on the compositor, no opacity anywhere, so no rendering
     path (including Chrome's captureBeyondViewport full-page capture, which
     re-evaluates a view() timeline at entry 0%) can leave it invisible - the
     failure mode that cost us the almanac band in R7;
   - transform does not participate in layout, so the reserved box never moves
     and CLS stays 0.0000;
   - scoped to .section-1946, which exists on index.html and nowhere else, so
     the "one moment, one page" rule is enforced by the selector, not by
     discipline;
   - inside prefers-reduced-motion: no-preference AND @supports, so a reduced
     motion reader and a browser without scroll timelines both get the finished
     state at rest, never the parked one;
   - `screen` because a print stylesheet has no scroll timeline. */
@media screen and (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {
    .section-1946 .year {
      transform-origin: left center;
      animation-name: yearStamp;
      animation-fill-mode: both;
      animation-timing-function: var(--ease-out);
      animation-duration: var(--dur-reveal);
      animation-timeline: view(block);
      animation-range: entry 15% cover 30%;
      will-change: transform;
    }
  }
}
@keyframes yearStamp {
  from { transform: scale(0.88); }
  to { transform: none; }
}

/* ── Interior title/rail `rise` removed (FIX-RGS-R9B). A view() translateY
   on the opening block — and, worse, on a sticky h2/.rail — is what slid
   section titles over maps and "Emergency numbers" over 911. Almanac rows on
   the home page still settle; safety routes were already still; sales-route
   titles are in-flow and unanimated. Reduced-motion and no-@supports were
   already the rest state, so there is nothing parked to undo. */

/* ── Hover / press feedback. Sales routes only (see exclusion 2).
   A press is the only motion on this site a person triggers on purpose, so it
   is the fastest thing here: 120ms, 1px down, no shadow — the button behaves
   like a physical key. transform only, so it cannot cost INP. */
/* Written flat, not with CSS nesting, on purpose: this audience is on old
   Android and a browser that cannot parse a nested style rule would drop the
   whole block. */
@media (prefers-reduced-motion: no-preference) {
  body:not(.route-safety) .btn {
    transition: background-color var(--dur-hover) var(--ease-out),
                border-color var(--dur-hover) var(--ease-out),
                color var(--dur-hover) var(--ease-out),
                box-shadow var(--dur-hover) var(--ease-out),
                transform var(--dur-press) var(--ease-out);
  }
  body:not(.route-safety) .btn-call:hover { transform: translateY(-1px); box-shadow: 0 3px 8px -4px color-mix(in oklab, var(--cylinder) 55%, transparent); }
  body:not(.route-safety) .btn:active { transform: translateY(1px); box-shadow: none; }
  body:not(.route-safety) .hero-phone,
  body:not(.route-safety) .big-phone,
  body:not(.route-safety) .nav-phone { transition: color var(--dur-hover) var(--ease-out), transform var(--dur-press) var(--ease-out); }
  body:not(.route-safety) .hero-phone:active,
  body:not(.route-safety) .big-phone:active,
  body:not(.route-safety) .nav-phone:active { transform: translateY(1px); }
  body:not(.route-safety) .almanac-row .go,
  body:not(.route-safety) .notice .v a,
  body:not(.route-safety) .footer-links a { transition: color var(--dur-hover) var(--ease-out), box-shadow var(--dur-hover) var(--ease-out); }
  /* Never the emergency strip. Belt and braces — the strip carries no .btn
     today and this keeps it still if one is ever added. */
  .emergency, .emergency * { transition: none !important; animation: none !important; }
}

/* ── .form-status swap. The success and error blocks previously appeared with
   no transition at all, which on a slow connection reads as a page glitch
   rather than an answer. 180ms fade + a 4px rise, on the box that is arriving.
   No parked opacity: the box is display:none until its class lands, so an
   unsupported or failed animation still shows it.
   ARIA (Perta's floor): both boxes are role="status" — polite. Focus moves to
   the heading on ERROR ONLY (assets/main.js), because an error needs the
   reader's cursor put where the recovery instructions are; a success does not,
   and yanking focus after a successful submit is a known a11y annoyance. */
@media (prefers-reduced-motion: no-preference) {
  .form-status.is-on { animation: statusIn var(--dur-status) var(--ease-out) both; }
}
@keyframes statusIn {
  from { opacity: 0; transform: translateY(4px); }
  to { opacity: 1; transform: none; }
}

/* ── Page-to-page. Cross-document View Transitions, opt-in, 0 bytes of JS.
   Same-origin static site, so the browser does the whole thing on the
   compositor; unsupported browsers get today's hard cut and lose nothing.
   This is a navigation state change, NOT a second signature moment: 180ms,
   root cross-fade, no shared-element choreography.
   The header and the emergency strip are pinned by name and given
   `animation: none` so they hold PERFECTLY STILL across a navigation — which
   is both the nicer effect and the thing that keeps the emergency strip
   motionless, as the rail requires. Reduced motion: the whole opt-in is
   inside `no-preference`, so navigation is a plain hard cut. */
@media (prefers-reduced-motion: no-preference) {
  @view-transition { navigation: auto; }
}
::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: var(--dur-nav);
  animation-timing-function: var(--ease-out);
}
.site-header { view-transition-name: siteheader; }
.emergency { view-transition-name: emergencystrip; }
::view-transition-old(siteheader),
::view-transition-old(emergencystrip) { display: none; }
::view-transition-new(siteheader),
::view-transition-new(emergencystrip) { animation: none; mix-blend-mode: normal; }

/* ═══ INTERIOR STRUCTURE — Uma R5, Kassia #4 #5 (2026-09-08) ═══════════════
   The measured complaint: 30 of 31 routes were ONE ~565px column inside a 1440
   canvas, H2 → body → hairline repeated 5 to 14 times, both margins dead. The
   reading measure was not the problem (48ch is right, and it does not move
   here); the problem was that nothing else on the page had a job.

   The home page already owns the device that fixes it — the almanac: a LABEL on
   the left, its VALUE on the right, a hairline under the pair. Interiors now use
   the same grammar at the page scale. The heading (and its eyebrow) sit in a
   19rem rail; the copy runs at its unchanged 48ch beside it; the section rule is
   drawn across BOTH, so a 14-section page reads as a ledger of headings rather
   than a stack of floating blocks. No new markup, no new font size, no figure
   invented — one grid, applied where the reading column already was. */
@media (min-width: 1200px) {
  :is(.route-safety, .route-article) main .container { max-width: 1060px; }
  :is(.route-safety, .route-article) main :is(section.container, section > .container) {
    display: grid;
    grid-template-columns: 19rem minmax(0, 1fr);
    column-gap: var(--s7);
    align-items: start;
  }
  /* Copy runs in the right-hand track; the heading rail takes the left. Items
     are placed in source order, so a section that opens with an eyebrow gets
     eyebrow-over-heading in the rail and its first paragraph on the heading's
     own row. */
  :is(.route-safety, .route-article) main :is(section.container, section > .container) > * { grid-column: 2; }
  :is(.route-safety, .route-article) main :is(section.container, section > .container) > :is(h2, .eyebrow, .breadcrumb) { grid-column: 1; }
  /* The heading SPANS the rows its copy occupies instead of owning one of them.
     Measured why: a two-line rail heading beside a two-line opening paragraph
     made row 1 82px tall against a 44px paragraph, and every following
     paragraph inherited a 38px hole above it. Spanning hands row height back to
     the copy, so paragraph rhythm is the same 16px it is everywhere else. */
  :is(.route-safety, .route-article) main :is(section.container, section > .container) > h2 {
    grid-row: auto / span 99;
    align-self: start;
    /* Optical: Bitter's cap-height rides high, so the rail heading sits ~3px
       above the first line it labels without this. */
    margin-top: -0.14em;
  }
  /* Blocks that ARE the full width of the page, not a label or a value: the
     boxed index, the prev/next pair, the closing rail, a band's own figure.
     Kassia R9 #1: .photo-band / .town-map-figure / .year-figure were landing in
     column 2 of this grid (measured x:606 w:580 in the 1060px container at 1440)
     because `> * { grid-column: 2 }` above beat the default auto-placement. The
     rail only exists inside this 1200px query — there is no two-column grid at
     900px to span — so the same `1 / -1` the map already uses is the fix. */
  :is(.route-safety, .route-article) main section.container.toc-block > *,
  :is(.route-safety, .route-article) main section.container > :is(.prevnext, .page-note, .call-ladder, .town-list, .link-rows, .map-figure, .photo-band, .town-map-figure, .year-figure) { grid-column: 1 / -1; }
  /* R6: .counter-facts was named in the comment above as "the closing rail" and
     then left out of the list, so the page's closing ledger sat at 48ch in the
     copy track with 367px of empty rail beside it on all 13 town pages. It is a
     ledger, not a value — it spans. */
  :is(.route-safety, .route-article) main section.container > .counter-facts { grid-column: 1 / -1; }

  /* ── R6, Kassia #4: "the rail is EMPTY on nearly every section — one eyebrow,
     then 200-350px of dead column beside four paragraphs." Three fixes, in
     descending order of the dead pixels each removes. ───────────────────────

     (1) THE PAGE HEAD IS NOT A RAIL SECTION. This was the eyebrow Kassia
     photographed. The rule above sends .breadcrumb and .eyebrow into the 19rem
     rail and leaves the h1, the lead and the call button in the copy track — so
     every interior route opened with a 12px kicker alone above 246-360px of
     empty paper (measured on all 24 rail routes: town pages 215-272, /about 84,
     /propane-delivery 313, duty-to-warn 360). A page title, its kicker and its
     crumb are ONE block. They were never a label and a value.

     Cap the h1, NOT the section: .container carries margin-inline:auto, so
     narrowing the section re-centres it and the title drifts off the spine that
     every h2 below it stands on. */
  :is(.route-safety, .route-article) main section.page-head.container { display: block; }
  :is(.route-safety, .route-article) main section.page-head > h1 { max-width: 44rem; }
  /* Kassia R9 #3: the 1938 aerial is the first image on /about and must read at
     the site content column (1180 - 2×64 pad = 1052px at 1440), not the 1060px
     railed measure. :has() keeps every other page-head on the 1060 spine. */
  :is(.route-safety, .route-article) main section.page-head.container:has(> .year-figure) {
    max-width: var(--maxw);
  }

  /* (2) WHERE THE PAGE HAS RAIL CONTENT, THE RAIL TAKES IT. .rail-facts is the
     home page's almanac grammar at section scale: label left, value right,
     hairline under the pair. The 13 town pages already state county, road and
     distance in the opening sentence — the rail restates those same three
     facts, so it carries no claim the page did not already make. When a section
     has one, the heading and the facts move together.

     A .rail div, not two loose siblings: grid auto-placement is source-order
     dependent, so a bare dl after the h2 lands in whichever row the copy column
     has reached — a different row on every page. One wrapper pins the pair.
     Below 1200px the div is an ordinary block and the reading order is heading,
     facts, copy, which is the right order on a phone and needs no second
     markup. */
  :is(.route-safety, .route-article) main :is(section.container, section > .container) > .rail {
    grid-column: 1;
    grid-row: auto / span 99;
    align-self: start;
  }
  :is(.route-safety, .route-article) main .rail > h2 { margin-top: -0.14em; }

  /* (3) Headings stay in flow at the top of the section they label.
     FIX-RGS-R9B: they used to be `position: sticky; top: 112px`. Because the
     rail shares a grid with full-width `.map-figure` / `.call-ladder` /
     `.town-map-figure` children (`grid-column: 1 / -1`), a stuck title was
     painted over the map and over the 911 / office numbers. Bill photographed
     that on /contact, /, /service-area and /safety/. Static, start-aligned,
     in the rail column — above their copy, never over a sibling. */

  /* FIX-RGS-R9B / RH tip: static headings, no floating numbers.
     Running Horse sets an eyebrow then an h2 then the body in one column
     (rh-reference/index.html:203–205, 234–236, 296–297). Numerals live in-flow
     inside almanac/step rows (RH:238), never as a sticky `01`/`02` in the
     margin. The railsec ::before counter is gone. When the section's other
     child is a full-width map or the emergency ladder, the heading spans
     above that child instead of occupying the left track those blocks also
     paint — "Emergency numbers" sits over 911 no longer. */
  :is(.route-safety, .route-article) main :is(section.container, section > .container):has(> :is(.map-figure, .call-ladder, .town-map-figure)) > :is(h2, .rail) {
    grid-column: 1 / -1;
    grid-row: auto;
  }

  /* The empty part of a text rail still gets a hairline. A printed almanac
     rules its margin — the rule is why the white beside a short heading reads
     as margin instead of as a hole. Sections are contiguous, so the ::after
     hairlines stack into one continuous line running the length of the route.

     Absolutely positioned, not a grid item: a pseudo-element placed in the grid
     would have to be pinned to a row span, and it would then fight the h2 for
     column 1. Out of flow it costs no track, no row and no reflow.

     Sits in the middle of the 19rem/1fr gap (--s7), so it touches neither the
     heading's right edge nor the copy's left. Same --paper-rule the section's own
     horizontal hairline uses: one rule weight, one rule colour, two directions.

     A SECTION EARNS THE RULE ONLY IF IT HAS A MARGIN. The blocks listed in the
     `1 / -1` span rule above — the map, the town list, the closing ledger, the
     prev/next, the boxed index — are the full width of the page, so those
     sections have no rail/copy split and nothing to rule; a rule drawn through
     one crossed the service-area map and ran down the middle of the town list.
     `:not(:has())` states that condition once, off the same component list, so
     the rule cannot fall out of step with the span rule. Verified live at 1440:
     rule on 6 of 6 sections on /propane-delivery, 3 of 5 on /service-area — the
     two it skips are the two whose gutter is already occupied.

     Inside `@supports selector(:has(*))` deliberately. On a browser without
     :has() the whole block is dropped and the page falls back to R7's plain
     rail — never to a hairline drawn across the map. */
  @supports selector(:has(*)) {
    :is(.route-safety, .route-article) main :is(section.container, section > .container) { position: relative; }
    :is(.route-safety, .route-article) main :is(section.container, section > .container):not(.page-head):not(:has(> :is(.town-list, .link-rows, .map-figure, .counter-facts, .prevnext, .call-ladder, .page-note, .toc, .photo-band, .town-map-figure, .year-figure)))::after {
      content: "";
      position: absolute;
      top: 0; bottom: 0;
      left: calc(var(--pad-inline) + 19rem + (var(--s7) / 2));
      width: 1px;
      background: var(--paper-rule);
      pointer-events: none;
    }
  }
}

/* The rail almanac. Same label/value/hairline as .counter-facts, set one step
   quieter because it annotates a section rather than closing a page. Below the
   1200px rail breakpoint it is an ordinary definition list under the heading,
   which is the correct reading order on a phone and needs no separate markup. */
.rail-facts {
  margin: var(--s4) 0 0;
  font-size: var(--text-xs);
  border-top: 1px solid var(--paper-rule);
}
.rail-facts > div {
  display: grid;
  grid-template-columns: 7.5rem minmax(0, 1fr);
  gap: var(--s2) var(--s3);
  padding-block: 7px;
  border-bottom: 1px solid var(--paper-rule);
}
.rail-facts dt {
  color: var(--ink-soft);
  text-transform: uppercase;
  letter-spacing: var(--track-caps);
  font-size: var(--text-3xs);
  font-weight: 700;
  line-height: 1.5;
}
/* Kassia R7 #11: one component, two voices — the home page sets its almanac
   values in Bitter (.notice .v) and the 13 town pages set the same rail in body
   sans. Same face, same tracking, same blue-deep as .notice .v; only the size
   stays where it was, because this rail annotates a section rather than opening
   a page. */
.rail-facts dd {
  margin: 0;
  font-family: var(--font-display); font-weight: var(--wt-display);
  letter-spacing: var(--track-display);
  color: var(--blue-deep);
  font-variant-numeric: tabular-nums;
  line-height: 1.4;
}
@media (min-width: 1200px) {
  /* In the rail the column is 19rem, so the pair stacks rather than sitting
     side by side — a 7.5rem label beside a 4-word value would have set the
     value at 12ch. */
  :is(.route-safety, .route-article) .rail-facts > div { grid-template-columns: 1fr; gap: 2px; }
  :is(.route-safety, .route-article) .rail-facts { max-width: 19rem; }
}

/* Full-bleed band. ONE per interior route, so the page has a horizon line
   instead of an unbroken field of paper. box-shadow + clip-path rather than
   `margin-inline: calc(50% - 50vw)`: 100vw includes the scrollbar and would
   have cost the 0-overflow-at-320px result Perta certified. A shadow cannot
   affect layout at all, and the inset clip stops it bleeding up or down into
   the sections either side. */
.band {
  background: var(--paper-sunk);
  box-shadow: 0 0 0 100vmax var(--paper-sunk);
  clip-path: inset(0 -100vmax);
  padding-block: clamp(44px, 6vw, 84px);
}
/* The tone change separates the band from its neighbours; the hairline that
   normally does that job would double up. */
:is(.route-safety, .route-article) main > section.section-tight.band,
:is(.route-safety, .route-article) main > section.section-tight.band + section.section-tight { background-image: none; }
/* the tightened interior rhythm (28-48px) is set at a higher specificity than
   .band, so the band re-states its own padding here rather than !important */
:is(.route-safety, .route-article) main > section.section-tight.band { padding-block: clamp(40px, 5vw, 72px); }

/* The closing rail ends on a hairline; the call button needs its own air under
   it or the two read as one stuck-together block. */
.counter-facts + .btn-row { margin-top: var(--s5); }

/* Boxed index (Kassia #4). Two columns from 620px, 44px rows so the target
   floor the town list set is not undercut on the one page most likely to be
   opened on a phone at 2 a.m. */
.toc-block { padding-block: var(--s5) var(--s4); }
.toc { border-top: 2px solid var(--blue); border-bottom: 1px solid var(--paper-rule); padding-top: var(--s4); }
.toc .eyebrow { margin-bottom: var(--s2); }
.toc-list { list-style: none; margin: 0; padding: 0; display: grid; column-gap: var(--s7); }
@media (min-width: 620px) { .toc-list { grid-template-columns: 1fr 1fr; } }
.toc-list > li { margin: 0; border-top: 1px solid var(--paper-rule); }
.toc-list > li:first-child { border-top: 0; }
@media (min-width: 620px) { .toc-list > li:nth-child(2) { border-top: 0; } }
.toc-list a {
  display: flex; align-items: center; min-height: 44px;
  padding-block: 6px; font-size: var(--text-sm); font-weight: 600;
  color: var(--ink); text-decoration: none;
}
.toc-list a:hover { color: var(--blue-deep); box-shadow: inset 0 -2px 0 var(--blue); }

/* ═══ PRINT — the safety pages get taped to a wall (Uma, 2026-09-08) ════════
   The build had no @media print at all. These six safety routes are the pages
   an operator actually prints: the leak steps, the CO steps, what to do after
   a storm. On paper they were coming out with a sticky header band, a call bar,
   a prev/next pair and a 30-link footer wrapped around the instructions.

   Two rules govern everything here:
   1. Nothing that only works by tapping survives to paper. Nav, the call bar,
      buttons, prev/next and the footer link lists are all gone; the phone
      number stays, in words, everywhere it already was.
   2. The emergency strip must still read as an alarm in BLACK AND WHITE. Its
      7% wash is 1.11:1 and most printers drop backgrounds by default, so the
      strip carries a 3px black-red box of its own on paper rather than relying
      on a tint that may never print. Same reason the strip keeps its heading
      weight instead of inheriting a lighter print body. */
@media print {
  /* --prose is set in ch, so it re-resolves against the 11pt print body on its
     own: 48ch there is 4.5in, which left a third of a Letter sheet empty beside
     a wall notice. 52ch = 5.1in = ~71 characters — the sheet is the column. */
  :root { --paper: #fff; --paper-sunk: #fff; --ink: #111; --ink-soft: #333; --prose: 52ch; }
  html { scroll-padding-top: 0; }
  body { font-size: 11pt; line-height: 1.45; padding-bottom: 0; background: #fff; }

  .site-header, .mobile-callbar, .skip-link, .nav-backdrop, .nav-toggle,
  .prevnext, .btn, .btn-row, .breadcrumb,
  .site-footer .footer-links, .site-footer .foot-h, .footer-brand img,
  .footer-bottom a { display: none !important; }

  /* The reading column is the paper. */
  .container, :is(.route-safety, .route-article) main .container { max-width: none; padding-inline: 0; }
  /* R8: the rail grid, its numerals and its ledger rule are a screen device at
     >=1200px. Print reflows to one column with padding-inline: 0, so the rule
     would land inside the copy and the numerals would print a second, silent
     numbering beside a set of headings that are no longer in a margin. */
  :is(.route-safety, .route-article) main :is(section.container, section > .container)::after { content: none; }
  :is(.route-safety, .route-article) main :is(section.container, section > .container) > h2::before,
  :is(.route-safety, .route-article) main .rail > h2::before { content: none; }
  .section, .section-tight, .section-dense, .section-air, .section-1946 { padding-block: 0 18pt; }
  .page-head { padding-block: 0 12pt; }

  h1 { font-size: 22pt; }
  h2 { font-size: 15pt; }
  h3 { font-size: 13pt; }
  /* The three-line instruction is the whole point of the printed sheet — the
     thing somebody reads from across a kitchen. It stays the largest type on the
     page on paper exactly as it is on screen. */
  .do-first { font-size: 20pt; max-width: none; margin-bottom: 12pt; }
  h1, h2, h3, .do-first, .eyebrow { break-after: avoid; page-break-after: avoid; }
  .steps > li, .plain-list > li, .call-ladder div, .safety-rows li, .town-list li, .link-rows li,
  figure, .emergency { break-inside: avoid; page-break-inside: avoid; }

  /* An alarm that survives a black-and-white printer. */
  .emergency {
    border: 3px solid #8C1A14; background: #fff;
    padding: 10pt; margin-bottom: 14pt;
  }
  .emergency-mark { width: 24pt; height: 24pt; }

  /* A wall sheet needs to say who to call and where it came from. */
  .site-footer { border-top: 1px solid #111; padding-block: 10pt 0; }
  .footer-bottom { border: 0; margin-top: 6pt; padding-top: 4pt; }
  .footer-nap { line-height: 1.4; }

  /* A link is not blue underlined text on paper; it is just the sentence. */
  a { color: #111; text-decoration: none; }
  .emergency a, .call-ladder .num, .footer-nap .phone { color: #8C1A14; font-weight: 700; }
  :is(.town-list, .link-rows) a::after, .almanac-row .go::after { content: ""; }
}

/* ── DRAWN PLATE (Dex, DEX-IMAGERY-R10) ──────────────────────────────────────
   The house component for a drawn asset: the six safety plates, the thirteen town
   locators and the colophon mark. Same register as .photo-band and .map-figure — a
   plate posted on paper, square corners, a hairline under the key, no card, no
   shadow. The artboard is 560 units wide and is CAPPED at 560 px so its type lands
   at 20 css px on a desktop and 12.7 at 390; the key sits in the second column as
   REAL TEXT rather than lettering baked into the picture (WCAG 1.4.5), which is also
   what fills the desktop margin. width/height ship on every <img>, so CLS is 0
   before the bytes land. */
:is(.diagram-figure, .town-map-figure) {
  margin: 0 0 var(--s6);
  display: flex; flex-wrap: wrap; align-items: flex-start; gap: var(--s5);
  min-width: 0; max-width: 100%;
}
:is(.diagram-figure, .town-map-figure) > img {
  flex: 0 1 560px; width: 100%; max-width: 560px; height: auto;
  border-radius: var(--r-img); background: var(--paper-sunk);
}
:is(.diagram-figure, .town-map-figure) > figcaption { flex: 1 1 20rem; }
.town-map-figure > img { border: 1px solid var(--blue); }
.pressmark-figure > img { background: none; }
:is(.diagram-figure, .town-map-figure) figcaption { max-width: 42ch; }
:is(.diagram-figure, .town-map-figure) figcaption p { margin: 0; }
.dia-key { list-style: none; margin: var(--s3) 0 0; padding: 0; }
.dia-key li {
  display: grid; grid-template-columns: 1.9rem 1fr; gap: var(--s2);
  padding-block: 7px; border-top: 1px solid var(--paper-rule);
  font-size: var(--text-sm); color: var(--ink); line-height: 1.4;
}
.dia-key b {
  font-family: var(--font-display); font-weight: var(--wt-display);
  color: var(--blue-deep); font-variant-numeric: tabular-nums;
}
.dia-src { font-size: var(--text-2xs); color: var(--ink-soft); margin-top: var(--s3) !important; }
/* NO MEDIA QUERY, DELIBERATELY. The plate sits in the CONTENT column, and that column
   is ~705 px on a railed route and ~1052 px on an unrailed one — a `min-width: 900px`
   query asks the VIEWPORT and gets the wrong answer on the railed ones, which is how
   the first cut put the key in a column one character wide. flex-wrap asks the
   CONTAINER: 560 + 24 + 320 = 904, so the key sits beside the drawing where there is
   room for it and drops underneath where there is not. */
