/* ============================================================================
   Pingo — 1b SPEED TEST (mid-run). Owns screens/speed.html and nothing else.

   EVERY selector in this file is scoped under .pgs-speed and EVERY @keyframes
   name is prefixed `pgsspeed`. Nothing leaks into the site, and no other screen
   in the gallery can collide with it. Two screens both declaring `pgSweep` with
   different keyframes would silently fight, so the prefix is not optional.

   Source of truth for pixels: the LOCKED design canvas
       C:/Dev/pingo/design/Pingo Screens.dc.html  ->  <div class="opt" id="1b">
   (lines 176-263). "canvas L204" citations below point into that file. Values
   are transcribed verbatim: real px, real hex/rgba, real gradients.
   Structural conventions (outer sized box + inner scaled screen, .is-offstage,
   the html:not(.motion-opt-in) reduced-motion gate) copy the already-shipped
   C:/Dev/pingo/website/mock.css so the gallery behaves as one system.

   ---------------------------------------------------------------------------
   THE NAMED MOMENT, which decides every motion decision in this file:
       A run is IN FLIGHT. Ping has finished and passed. Download is running
       with NO honest completion fraction. Upload and bufferbloat have not run.

   THE CARDINAL RULE, from C:/Dev/pingo/MASTER-CONTEXT.md:
       "Motion is a liveness cue: nothing hatched, withheld or stale may
        animate."

   How that is honoured here, concretely:
     * The two hatched pending phase chips (UP, BLOAT) have NO animation of any
       kind, in any state, and section 9 pins that with an explicit
       `animation: none !important` belt rather than trusting absence. A hatched
       chip that shimmered beside the running spinner would claim that upload is
       underway when it has not started. This is the screen's #1 failure mode.
     * The progress track is HATCHED and perfectly still. See divergence 1.
     * The three settled readings (ping / jitter / loss) do not animate. A
       settled reading that shimmers claims to still be arriving.
     * The five completed history bars do not animate. Only the sixth, which is
       the run happening now, pulses. It does not GROW: a growing bar would
       animate a claim about a figure that has not settled.
     * There is NO numeric count-up anywhere. The 52px download figure is typed
       once. The arc's `draw` is an entrance that lands on the value and stops.
     * There is no radar and no device blips. RadarField is mounted on no screen
       in the app and test/screens/home_steel_test.dart:155 asserts it stays
       absent.
     * No brand accent hue. Mint / amber / red mean pass / caution / fail and
       nothing else. The only red on this screen is Stop, because destructive
       actions get red and may not borrow the fail hue for decoration.

   THE TWO STOP STATES, both landing on HONEST FINAL FRAMES:
     * .pgs-speed.is-offstage  (section 8) — the gallery shell toggles it so only
       the ACTIVE screen animates. This is the web's TickerMode and it mirrors
       the app's IndexedStack, where offstage tabs genuinely stop ticking. It is
       deliberately NOT gated on any preference: offstage is a performance fact.
     * @media (prefers-reduced-motion: reduce)  (section 10) — byte-identical
       final frames, every selector gated on html:not(.motion-opt-in) so
       index.html's existing "Play the motion" opt-in keeps working. Exactly as
       styles.css and mock.css already do.
   Neither block ever uses `animation-play-state: paused` and neither ever
   freezes a mid-frame. What each animation rests on, and why:
     - pgsspeedRing     vanishes (display:none). Its 100% frame IS opacity 0, so
                        "no ring" is its honest final frame.
     - pgsspeedDraw     `animation: none` reverts the arc to the dasharray /
                        dashoffset ATTRIBUTES on the element, i.e. the COMPLETE
                        value. A partial arc would misstate the measurement.
     - pgsspeedBreathe  parks on its own 0% rest frame (opacity .5, scale 1),
                        matching mock.css's treatment of pgmBreathe.
     - pgsspeedPulse    holds FULLY LIT (opacity 1, its own 0%/100% frame). A
                        live thing dimmed to 28% would understate it.
     - pgsspeedBlink    holds FULLY LIT (opacity 1, its own 0% frame). Same
                        reasoning as a live dot: the download phase genuinely is
                        running, and an invisible caret understates that. Note
                        this deliberately does NOT use the 100% frame (opacity
                        0); mock.css sets the same precedent by parking
                        pgmBreathe on its 0% rest frame rather than its 100%.
     - pgsspeedSpin     holds at rotate(0), bright quadrant at the top, which is
                        the conventional static "in progress" badge. An
                        indeterminate spinner has no true final frame, and
                        HIDING it would leave the running chip indistinguishable
                        from the pending ones except for the hatch, understating
                        a phase that really is executing.

   ---------------------------------------------------------------------------
   ILLUSTRATIVE VALUES (chosen here, NOT measured). The canvas left {{mbps}},
   {{ping}} and {{jit}} as template holes that render EMPTY in a real browser:
     * DOWNLOAD 214 Mb/s — the in-flight partial reading, used in BOTH places
       the canvas templated {{mbps}} (the dial, canvas L199, and the sixth
       history bar, canvas L240) so the two agree. Deliberately NOT the 231 that
       mock.html/index.html use for Home's settled downlink: this is a DIFFERENT
       run that has not finished, and reusing 231 would imply the same
       measurement. 214 also sits inside the history range 188-255, so the
       sixth bar's height (31px, canvas L240) stays plausible against its
       neighbours.
     * PING 18 MS and JITTER 6.2 MS — reused verbatim from the site's existing
       chosen Home values (RESUME-WEBSITE.md section 4), which is correct rather
       than lazy: phase 1 of THIS run measured them, so they are the same
       readings Home is showing.
   Straight from the canvas, unchanged: LOSS 0%, "CLOUDFLARE · SYD",
   "2 OF 4 · ~40 S LEFT", the arc's stroke-dashoffset 88, and the five settled
   history runs 188 / 241 / 62 (amber) / 255 / 203 with their 34/44/12/47/37px
   heights.

   ---------------------------------------------------------------------------
   DELIBERATE DIVERGENCES from the canvas. All are documented, none are
   accidents, and each is repeated inline at its own rule.

   1. THE PROGRESS TRACK IS HATCHED, NOT PART-FILLED. The canvas (L224) fills it
      with `animation: pgFill 9s ease-in-out infinite alternate`, whose keyframes
      walk the width 8% -> 72% -> 64% -> 78% -> 71% and then reverse, forever.
      That is a fabricated completion fraction: it moves BACKWARDS, it never
      reaches 100%, and no engine produced any of those numbers. A null progress
      is not a zero and it is not a guess either, so while a run is in flight
      with no honest fraction the track is hatched and perfectly still. The
      genuinely known discrete fact ("2 OF 4") stays in the caption below, where
      it cannot be misread as a proportion of elapsed work — four phases of
      unequal duration are not four equal quarters. This is also the divergence
      the brief explicitly required. To restore the canvas exactly, this is the
      one rule to change: .pgss-progress in section 5.

   2. TIMINGS FOLLOW THE CANVAS WHERE IT AND THE HANDOVER DISAGREE. The canvas
      authors `pgBreathe 5s` (L190) and `pgDraw 2s` (L195); PINGO-STEEL-HANDOVER
      section 2's motion summary says "breathe 6s" and "draw 1.7s". The canvas
      wins because it is the pixel authority (lib/theme/tokens.dart: "Harvested
      verbatim from the LOCKED design canvas") and because the handover's own
      list is written as a range summary elsewhere ("pulse 1.6-2.4s",
      "ring 3-4s", "flowX .8-1s", "marquee 7-9s"). Everything else already sits
      inside the handover's authored envelope: ring 3.4s is inside 3-4s, and the
      spinner is exactly the specified 1s indeterminate quadrant.

   3. pgBlink IS KEPT. The canvas authors `pgBlink 1s step-end infinite` on the
      caret beside MEASURING (L201). It is not named in the handover's motion
      summary, but it is canvas-authored, it sits on a genuinely live element,
      and the handover's list is a vocabulary menu rather than a closed set (it
      also lists sweep / flowX / travel / marquee / bar, none of which have any
      subject on this screen). Dropping an authored animation from a live element
      would be the opposite error to the one the cardinal rule guards against.

   4. STOP IS A DIV, NOT A <button> (canvas L232). The whole screen sits inside
      role="img" + aria-hidden, and a focusable control inside an aria-hidden
      subtree is an accessibility fault: reachable by tab, invisible to the
      reader. mock.html made the identical call for Home's four quick tests and
      contains no <button> either. The canvas's own `border: none` followed by
      `border: 1px solid rgba(255,92,110,.4)` is resolved to the 1px border,
      which is what it renders as.

   5. THE NAV'S FOURTH LABEL IS "Diagnose", not the canvas's "Ask" (L248). The
      settled decision merges Ask and Guided Diagnose into one surface named
      Diagnose. The GLYPH is left exactly as authored so all six gallery screens
      carry an identical nav; diverging icons between screens would read as a
      defect, and no parked AI capability is named anywhere on this screen.

   6. SIZING. The screen is built at a true 375x812 with the canvas's authored
      8-8.5px mono labels INTACT, then scaled as a whole by --gal-scale
      (default .88, and .88 is the floor). Nothing was re-specced smaller. This
      is the same proven approach mock.css uses for Home, and the floor exists
      because 8px labels at .76/.68 would render at 6.08/5.44px, breaking the
      9px mono floor that PINGO-STEEL-HANDOVER section 2 exists to protect (see
      RESUME-WEBSITE.md fix #7).

   7. Two small robustness additions the canvas did not need because it is a
      static plate inside its own document: `white-space: nowrap` on the nav
      labels (so "Diagnose", the longest of the five, cannot wrap in a ~66px
      slot) and `margin-inline: auto` on the phase-chip icons (the canvas relies
      on inline SVG plus text-align:center; this file sets svg{display:block}
      defensively, as mock.css does, which would otherwise left-align them).

   ---------------------------------------------------------------------------
   PUBLIC API — the integrator should not need to open these files.
     .pgs-speed                 outer box. Occupies calc(375px * scale) x
                                calc(812px * scale) of real layout space.
     .pgs-speed--bezel          optional device bezel (padding + border sit
                                OUTSIDE the 375x812 box, so layout is unchanged)
     .pgs-speed.is-offstage     stop ALL motion on honest final frames. The
                                gallery shell MUST toggle this so only the
                                active/visible screen animates.
     --gal-scale                set on .pgs-speed or ANY ancestor. Default .88.
                                1 = authored size. Never below .88.
     [data-pgs-screen="speed"]  stable hook for the shell's querySelector
     @keyframes                 pgsspeedBreathe · pgsspeedRing · pgsspeedDraw ·
                                pgsspeedSpin · pgsspeedBlink · pgsspeedPulse
   Requires from the host page: Sora (600/700/800) + JetBrains Mono (500/700).
   index.html already loads both. There are no other external dependencies, no
   JS in this file, and no state classes beyond .is-offstage.
   ============================================================================ */


/* ---------------------------------------------------------------------------
   1. Outer box, tokens, defensive reset

   The site's body sets line-height 1.6 and its own font stack; every negative
   letter-spacing and every px height below assumes the canvas's own values, so
   the root re-establishes them rather than inheriting.
   --------------------------------------------------------------------------- */

.pgs-speed {
  /* content-box so the optional bezel's padding/border sit OUTSIDE the 375px */
  box-sizing: content-box;
  width: calc(375px * var(--gal-scale, .88));
  height: calc(812px * var(--gal-scale, .88));
  flex: none;
  position: relative;
  user-select: none;
  -webkit-user-select: none;

  font-family: var(--pgss-font);
  font-size: 16px;
  font-weight: 400;
  line-height: normal;      /* the site's body sets 1.6 — that would break the stack */
  letter-spacing: normal;
  text-align: left;
  color: var(--pgss-ink);
  -webkit-font-smoothing: antialiased;

  --pgss-font: 'Sora', system-ui, -apple-system, 'Segoe UI', sans-serif;
  --pgss-mono: 'JetBrains Mono', ui-monospace, 'SF Mono', Consolas, monospace;

  /* Colour tokens, PINGO-STEEL-HANDOVER section 2 */
  --pgss-ink: #F2F6FC;          /* primary text/numbers */
  --pgss-ink2: #D7E0EC;         /* secondary */
  --pgss-mut: #B7C4D6;          /* body copy */
  --pgss-faint: #94A2B6;        /* mono labels/units */
  --pgss-faint2: #7F8EA4;       /* tertiary captions */
  --pgss-untested: #A3B1C3;     /* the colour of ABSENCE. Never mint, never red. */
  --pgss-on-light: #0B121C;     /* text on white actions */

  --pgss-glass: rgba(9, 14, 21, .52);
  --pgss-line: rgba(255, 255, 255, .11);

  --pgss-pass-text: #6FE0B6;
  --pgss-pass-tint: rgba(53, 214, 160, .12);
  --pgss-pass-border: rgba(53, 214, 160, .32);   /* canvas L207 uses .32 here */
  --pgss-warn: #FFB454;
  --pgss-fail-text: #FF8A96;
  --pgss-fail-tint: rgba(255, 92, 110, .14);
  --pgss-fail-border: rgba(255, 92, 110, .4);

  /* "not measured yet" — a state, not a texture. 45deg 4px stripes. */
  --pgss-hatch: repeating-linear-gradient(45deg, transparent 0 4px, rgba(255, 255, 255, .06) 4px 8px);
  --pgss-hatch-line: rgba(255, 255, 255, .2);
}

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

.pgs-speed div,
.pgs-speed span { margin: 0; padding: 0; }

.pgs-speed svg { display: block; flex: none; overflow: visible; }

/* Optional device bezel. Purely canvas chrome (canvas L32). */
.pgs-speed.pgs-speed--bezel {
  background: #060B16;
  padding: calc(9px * var(--gal-scale, .88));
  border: calc(9px * var(--gal-scale, .88)) solid #03060c;
  border-radius: calc(46px * var(--gal-scale, .88));
  box-shadow: 0 0 0 2px #16223a, 0 30px 70px rgba(0, 0, 0, .6);
}

/* Type helpers = the canvas's .mono / .num / .lbl (canvas L39-41) */
.pgs-speed .pgss-mono { font-family: var(--pgss-mono); font-variant-numeric: tabular-nums; }
.pgs-speed .pgss-num { font-variant-numeric: tabular-nums; letter-spacing: -1px; }
.pgs-speed .pgss-lbl { font: 700 9.5px var(--pgss-mono); letter-spacing: 1.6px; color: var(--pgss-faint); }
.pgs-speed .pgss-grow { flex: 1 1 auto; min-width: 0; }


/* ---------------------------------------------------------------------------
   2. Screen root + the two bloom layers (canvas L33-35, L180-181)

   The scale lives HERE, on the 375x812 box, while .pgs-speed above reserves the
   post-scale footprint with calc(). That is what lets a scaled screen occupy
   correct layout space instead of overlapping its neighbours.
   --------------------------------------------------------------------------- */

.pgs-speed .pgss-screen {
  position: relative;
  width: 375px;
  height: 812px;
  flex: none;
  border-radius: 38px;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  background: linear-gradient(180deg,
    #22303F 0%, #1A2634 13%, #111A25 36%, #0A0F16 66%, #04070B 100%);
  transform: scale(var(--gal-scale, .88));
  transform-origin: top left;
}

.pgs-speed .pgss-bloom { position: absolute; top: 0; left: 0; right: 0; }
.pgs-speed .pgss-bloom--sheet {
  height: 46%;
  background: linear-gradient(180deg,
    rgba(163, 187, 214, .2), rgba(163, 187, 214, .06) 46%, transparent 100%);
}
.pgs-speed .pgss-bloom--core {
  height: 22%;
  background: radial-gradient(120% 100% at 50% 0%, rgba(190, 210, 232, .16), transparent 72%);
}


/* ---------------------------------------------------------------------------
   3. Status bar + content column (canvas L36-37, L182-183)
   --------------------------------------------------------------------------- */

.pgs-speed .pgss-sb {
  height: 44px;
  flex: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 26px;
  font: 700 12px var(--pgss-font);
  color: var(--pgss-ink2);
  position: relative;
  z-index: 3;
}
.pgs-speed .pgss-sb-icons { display: flex; gap: 6px; align-items: center; }

/* 18px screen padding, and it does NOT scroll: this is a captured moment. */
.pgs-speed .pgss-col {
  flex: 1;
  min-height: 0;
  position: relative;
  z-index: 2;
  padding: 2px 18px 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}


/* ---------------------------------------------------------------------------
   4. Header row (canvas L184-188)

   The server name is load-bearing, not decoration: a throughput figure is
   meaningless without the endpoint it was measured to, so it sits in the header.
   --------------------------------------------------------------------------- */

.pgs-speed .pgss-head {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 2px 0 8px;
  flex: none;
}
.pgs-speed .pgss-title {
  font-size: 19px;
  font-weight: 800;
  letter-spacing: -.5px;
  color: var(--pgss-ink);
}
.pgs-speed .pgss-server {
  font-size: 9px;
  letter-spacing: 1px;
  color: var(--pgss-faint);
  flex: none;
}


/* ---------------------------------------------------------------------------
   5. The dial — 236px band (canvas L189-203)

   Same SVG instrument as Home's ScoreDial: dotted tick ring (r 47, 0.7/5.6
   dash), empty track (r 40, 5px), then the value arc with a round cap.
   PINGO-STEEL-HANDOVER section 3: never use conic-gradient + a circular mask,
   it aliases badly.

   MOTION IS EARNED HERE because the run is live. Four animations, all cheap,
   all ambient, none of them touching the number itself.
   --------------------------------------------------------------------------- */

.pgs-speed .pgss-dialwrap {
  height: 236px;
  flex: none;
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* Inner glow. Rests at opacity .5 and lifts to 1 — a breath, not a throb. */
.pgs-speed .pgss-dial-glow {
  position: absolute;
  width: 206px;
  height: 206px;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(232, 238, 247, .1), transparent 68%);
  animation: pgsspeedBreathe 5s ease-in-out infinite;
}

/* One expanding ring. Ends at opacity 0, so "no ring" is its honest final
   frame and both stop states remove it entirely rather than freezing it. */
.pgs-speed .pgss-dial-ring {
  position: absolute;
  width: 222px;
  height: 222px;
  border-radius: 50%;
  border: 1px solid rgba(232, 238, 247, .18);
  animation: pgsspeedRing 3.4s ease-out infinite;
}

.pgs-speed .pgss-dial-svg {
  position: relative;
  transform: rotate(-90deg);   /* 12 o'clock start */
}

/* The value arc. stroke-dasharray / stroke-dashoffset live on the ELEMENT as
   SVG attributes (see speed.html), so `animation: none` in either stop state
   reverts to the COMPLETE honest value with no extra CSS. `both` fill-mode
   holds that same value after the entrance. It draws ONCE on appear and never
   loops: a looping arc would replay "a fresh reading just landed" forever. */
.pgs-speed .pgss-arc {
  animation: pgsspeedDraw 2s cubic-bezier(.22, .9, .24, 1) both;
}

.pgs-speed .pgss-dial-face {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}
.pgs-speed .pgss-dial-lbl { font-size: 8.5px; color: var(--pgss-faint); }
.pgs-speed .pgss-dial-num {
  font-size: 52px;
  font-weight: 800;
  color: var(--pgss-ink);
  line-height: 1.02;
  margin-top: 5px;
  /* No count-up. MASTER-CONTEXT forbids it: a rolling number claims a stream of
     readings that did not arrive. The digits are typed once and never tick. */
}
.pgs-speed .pgss-dial-unit {
  font-size: 12px;
  font-weight: 600;
  color: var(--pgss-mut);
  margin-top: 2px;
}
.pgs-speed .pgss-dial-state {
  font-size: 9px;
  letter-spacing: 1.2px;
  color: var(--pgss-faint);
  margin-top: 9px;
}
/* The caret is the liveness cue for a figure that is still moving. It is
   allowed to blink because the download phase genuinely is running. */
.pgs-speed .pgss-caret { animation: pgsspeedBlink 1s step-end infinite; }


/* ---------------------------------------------------------------------------
   6. PhaseStrip (canvas L204-226)

   Four equal chips, each carrying its own state AND its own motion budget:
     PING   done      mint tint + tick        STILL (a finished thing is not live)
     DOWN   running   white chip + spinner    the 1s indeterminate quadrant
     UP     pending   HATCHED + dashed + —    STILL
     BLOAT  pending   HATCHED + dashed + —    STILL
   --------------------------------------------------------------------------- */

/* GlassCard (canvas L38): sur + line + blur 20 */
.pgs-speed .pgss-card {
  background: var(--pgss-glass);
  border: 1px solid var(--pgss-line);
  backdrop-filter: blur(20px);
  -webkit-backdrop-filter: blur(20px);
}

.pgs-speed .pgss-phases {
  border-radius: 20px;
  padding: 12px 14px;
  flex: none;
}
.pgs-speed .pgss-phases-lbl { font-size: 8.5px; }

.pgs-speed .pgss-phase-row { display: flex; gap: 7px; margin-top: 9px; }

.pgs-speed .pgss-phase {
  flex: 1;
  border-radius: 12px;
  padding: 8px 6px;
  text-align: center;
}
/* The canvas relies on inline SVG + text-align:center to centre the chip icon.
   This file sets svg{display:block} defensively (as mock.css does), which would
   left-align it, so the icon is re-centred explicitly. */
.pgs-speed .pgss-phase-ico { margin-inline: auto; }

.pgs-speed .pgss-phase-lbl {
  font-size: 8px;
  font-weight: 700;
  letter-spacing: .8px;
  margin-top: 4px;
}

/* DONE — the only mint chip. Mint means pass and nothing else. */
.pgs-speed .pgss-phase--done {
  background: var(--pgss-pass-tint);
  border: 1px solid var(--pgss-pass-border);
}
.pgs-speed .pgss-phase--done .pgss-phase-lbl { color: var(--pgss-pass-text); }

/* RUNNING — neutral white, because "in progress" is not a verdict. */
.pgs-speed .pgss-phase--running {
  background: rgba(255, 255, 255, .1);
  border: 1px solid rgba(255, 255, 255, .2);
}
.pgs-speed .pgss-phase--running .pgss-phase-lbl { color: var(--pgss-ink); }

/* The 1s rotating quadrant. INDETERMINATE by design: it says "working" and
   deliberately does NOT imply a percentage, because the engine has no honest
   fraction for a download in progress. Never use this shape for a percentage. */
.pgs-speed .pgss-spin {
  display: inline-block;
  width: 13px;
  height: 13px;
  border-radius: 50%;
  border: 2px solid rgba(232, 238, 247, .3);
  border-top-color: var(--pgss-ink);
  animation: pgsspeedSpin 1s linear infinite;
}

/* PENDING — hatched, dashed, em-dash placeholder, and PERFECTLY STILL in every
   state. No fake zeros: these phases genuinely have not run. Section 9 pins the
   stillness with an explicit belt. */
.pgs-speed .pgss-phase--pending {
  background: var(--pgss-hatch);
  border: 1px dashed var(--pgss-hatch-line);
}
.pgs-speed .pgss-phase--pending .pgss-phase-lbl {
  color: var(--pgss-untested);
  margin-top: 5px;   /* canvas L217/L221: 5px here, vs 4px on the two live chips */
}
.pgs-speed .pgss-phase-dash {
  font-size: 13px;
  font-weight: 700;
  color: var(--pgss-untested);
  line-height: .9;
}

/* THE PROGRESS TRACK — DIVERGENCE 1, the most important one on this screen.
   The canvas (L224) part-fills this with `pgFill 9s ... infinite alternate`,
   walking the width 8% -> 72% -> 64% -> 78% -> 71% and back, forever. That is a
   fabricated completion fraction: it moves BACKWARDS, never reaches 100%, and
   no engine produced any of those numbers.
   A null progress is not a zero, and it is not a guess either. While a run is
   in flight with no honest fraction, the track is HATCHED and perfectly still.
   The genuinely known discrete fact ("2 OF 4") stays in the caption below,
   where it cannot be misread as a proportion of elapsed work: four phases of
   unequal duration are not four equal quarters.
   The canvas's own empty-track colour is kept underneath the hatch so the track
   still reads AS a track, exactly as HatchCell lays hatch over a cell surface.
   TO RESTORE THE CANVAS EXACTLY: this single rule is the only thing to change. */
.pgs-speed .pgss-progress {
  height: 5px;
  border-radius: 3px;
  margin-top: 11px;
  overflow: hidden;
  background-color: rgba(255, 255, 255, .09);
  background-image: var(--pgss-hatch);
  border: 1px dashed var(--pgss-hatch-line);
}

.pgs-speed .pgss-phases-foot {
  display: flex;
  justify-content: space-between;
  margin-top: 7px;
}
.pgs-speed .pgss-phases-note { font-size: 8.5px; color: var(--pgss-faint); }


/* ---------------------------------------------------------------------------
   7. The three settled inputs, Stop, and the run history
   --------------------------------------------------------------------------- */

/* 7a. ping / jitter / loss (canvas L227-231). `flex: 1` is `flex: 1 1 0%`, so
   the three cells are equal-width regardless of content. Nothing here animates:
   these are settled readings from phase 1, and a settled reading that shimmers
   claims to still be arriving. */
.pgs-speed .pgss-metrics {
  display: flex;
  gap: 9px;
  margin-top: 11px;
  flex: none;
}
.pgs-speed .pgss-metric {
  flex: 1;
  min-width: 0;
  border-radius: 18px;
  padding: 11px 12px;
}
.pgs-speed .pgss-metric-lbl { font-size: 8px; }
.pgs-speed .pgss-metric-val {
  display: flex;
  align-items: baseline;
  gap: 3px;
  margin-top: 4px;
}
.pgs-speed .pgss-metric-num {
  font-size: 17px;
  font-weight: 700;
  color: var(--pgss-ink);
}
/* 0% loss is a genuine pass, so it earns mint. Ping and jitter stay neutral
   ink: this screen REPORTS them, it does not grade them. */
.pgs-speed .pgss-metric-num--pass { color: var(--pgss-pass-text); }
.pgs-speed .pgss-metric-unit { font-size: 9px; color: var(--pgss-faint); }

/* 7b. Stop (canvas L232). The ONLY red thing on screen: destructive actions get
   red, never an accent, and decoration may never borrow the fail hue.
   display:flex centres the label because this is a div, not a <button>
   (divergence 4). */
.pgs-speed .pgss-stop {
  height: 50px;
  flex: none;
  margin-top: 12px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 18px;
  background: var(--pgss-fail-tint);
  border: 1px solid var(--pgss-fail-border);
  font-family: var(--pgss-font);
  font-size: 13px;
  font-weight: 700;
  color: var(--pgss-fail-text);
  cursor: pointer;
}

/* 7c. History header (canvas L233) */
.pgs-speed .pgss-hist-head {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 14px 2px 8px;
  flex: none;
}
.pgs-speed .pgss-rule { flex: 1; height: 1px; background: rgba(255, 255, 255, .09); }
.pgs-speed .pgss-hist-link { font-size: 11px; font-weight: 600; color: var(--pgss-ink2); }

/* 7d. Last-6-runs bars (canvas L234-241). Values are shown, and they carry tier
   colour, so a bad run (62 Mb/s, amber) stays visible in the record instead of
   being smoothed away. Heights come straight from the canvas. */
.pgs-speed .pgss-hist {
  display: flex;
  align-items: flex-end;
  gap: 7px;
  height: 60px;
  flex: none;
}
.pgs-speed .pgss-hist-col {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 5px;
  justify-content: flex-end;
}
.pgs-speed .pgss-hist-val { font-size: 8px; color: var(--pgss-faint2); }
.pgs-speed .pgss-hist-bar {
  width: 100%;
  border-radius: 4px 4px 0 0;
  background: linear-gradient(180deg, rgba(232, 238, 247, .5), rgba(232, 238, 247, .1));
}

/* Run 3: amber = caution. */
.pgs-speed .pgss-hist-val--warn { color: var(--pgss-warn); }
.pgs-speed .pgss-hist-bar--warn {
  background: linear-gradient(180deg, rgba(255, 180, 84, .8), rgba(255, 180, 84, .18));
}

/* Run 6 IS the run in flight: brighter, bold, and the only bar that pulses.
   It deliberately does NOT grow — a growing bar would animate a claim about a
   figure that has not settled. The pulse says "this one, now"; the height says
   nothing it has not measured. The five completed bars are perfectly still,
   because a finished run is not live. */
.pgs-speed .pgss-hist-val--live { font-weight: 700; color: var(--pgss-ink); }
.pgs-speed .pgss-hist-bar--live {
  background: linear-gradient(180deg, #E8EEF7, rgba(232, 238, 247, .18));
  animation: pgsspeedPulse 2s ease-in-out infinite;
}

/* 7e. The nav reserve (canvas L242). PINGO-STEEL-HANDOVER section 5, the LAYOUT
   CONTRACT that caused every review failure in design: the floating nav overlays
   content, so the column must reserve >= 104px at the bottom and the last
   visible control must end >= 20px above the nav's top edge (its shadow reaches
   ~20px upward). Measured here: content ends ~623px into the 768px column and
   the nav's top edge is at ~682px, leaving ~59px of clear air — inside the
   contract's own >20px / <60px band. Do NOT shrink this to fit something in;
   the contract says remove a block or shrink a chart, never nudge padding. */
.pgs-speed .pgss-navgap { height: 104px; flex: none; }


/* ---------------------------------------------------------------------------
   8. SteelNav (canvas L42-44, L244-250)
   72px glass pill, 5 items, Tools centre as a 34px white gradient tile.
   Item 4 reads "Diagnose" (divergence 5).
   --------------------------------------------------------------------------- */

.pgs-speed .pgss-nav {
  position: absolute;
  left: 16px;
  right: 16px;
  bottom: 14px;
  height: 72px;
  border-radius: 26px;
  background: rgba(9, 13, 20, .88);
  border: 1px solid var(--pgss-line);
  backdrop-filter: blur(22px);
  -webkit-backdrop-filter: blur(22px);
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 6px;
  z-index: 5;
  box-shadow: 0 14px 34px rgba(0, 0, 0, .5);
}
.pgs-speed .pgss-nvi {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  color: #7C8CA1;
}
.pgs-speed .pgss-nvi--tools { color: var(--pgss-ink); }
.pgs-speed .pgss-nvl {
  font-size: 9.5px;
  font-weight: 600;
  /* "Diagnose" is the longest of the five labels (~50px of Sora 600 at 9.5px in
     a ~66px slot). nowrap so it can never break onto a second line and push the
     72px pill out of shape. */
  white-space: nowrap;
}
.pgs-speed .pgss-nvl--tools { font-weight: 700; }
.pgs-speed .pgss-nv-tile {
  width: 34px;
  height: 34px;
  border-radius: 12px;
  background: linear-gradient(150deg, #FFFFFF, #C9D6E6);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--pgss-on-light);
  box-shadow: 0 8px 18px rgba(0, 0, 0, .45);
}


/* ---------------------------------------------------------------------------
   9. THE STILLNESS BELT — hatched things never animate, in ANY state.

   The two pending phase chips and the hatched progress track are the single
   most likely way to fail this screen, and "no animation is declared on them"
   is a weaker guarantee than it looks: a future ambient rule, an inherited
   shorthand, or a well-meaning "make the page livelier" edit could all reach
   them. !important is justified exactly here, because the cardinal rule
   ("nothing hatched, withheld or stale may animate") outranks any later
   stylistic intent. It is scoped to hatched subtrees only and touches nothing
   that is legitimately live.
   --------------------------------------------------------------------------- */

.pgs-speed .pgss-phase--pending,
.pgs-speed .pgss-phase--pending *,
.pgs-speed .pgss-progress,
.pgs-speed .pgss-progress * {
  animation: none !important;
  transition: none !important;
}


/* ---------------------------------------------------------------------------
   10. Keyframes. Every name is prefixed `pgsspeed` so no other gallery screen
   can collide with it. Canvas source names are noted for traceability.
   --------------------------------------------------------------------------- */

/* canvas pgBreathe (L47). Rest frame is 0%: opacity .5, scale 1. Both stop
   states park there, which is what mock.css does with pgmBreathe. */
@keyframes pgsspeedBreathe {
  0%, 100% { opacity: .5; transform: scale(1); }
  50%      { opacity: 1;  transform: scale(1.04); }
}

/* canvas pgRing (L48). Ends at opacity 0, so "no ring" IS its final frame and
   both stop states use display:none rather than freezing a visible ring. */
@keyframes pgsspeedRing {
  0%   { transform: scale(.6);  opacity: .55; }
  100% { transform: scale(1.55); opacity: 0; }
}

/* canvas pgDraw (L54). Draws the arc from empty to the value on appear, ONCE.
   The end value is not written here on purpose: it is the element's own
   stroke-dashoffset attribute, so the arc can only ever rest on the COMPLETE
   honest value. */
@keyframes pgsspeedDraw {
  from { stroke-dashoffset: 251.3; }
}

/* canvas pgSweep (L46), used here at 1s as the INDETERMINATE activity quadrant
   (the handover's "1s rotating quadrant"). Not the 2.8s logo sweep, which has
   no subject on this screen. Never drive a percentage with this. */
@keyframes pgsspeedSpin {
  to { transform: rotate(360deg); }
}

/* canvas pgBlink (L52). The caret beside MEASURING. Both stop states hold it
   FULLY LIT (its 0% frame), not on its 100% frame: the download phase genuinely
   is running, and an invisible caret would understate that. Same reasoning the
   handover applies to live dots. */
@keyframes pgsspeedBlink {
  0%, 55%   { opacity: 1; }
  60%, 100% { opacity: 0; }
}

/* canvas pgPulse (L51). The in-flight history bar. Its 0%/100% frame is
   opacity 1, so both stop states hold it fully lit. */
@keyframes pgsspeedPulse {
  0%, 100% { opacity: 1; }
  50%      { opacity: .28; }
}

/* DELIBERATELY ABSENT: the canvas's pgFill (L55), which fabricated a progress
   percentage. See divergence 1 and .pgss-progress. Do not reinstate it. */


/* ---------------------------------------------------------------------------
   11. HONEST FINAL FRAMES — block A: .is-offstage

   The gallery shell toggles .is-offstage on .pgs-speed whenever this screen is
   not the active tab OR has scrolled out of view. Only the ACTIVE screen may
   animate: six mocks each carrying backdrop-filter: blur(20px) and animating at
   once would drop frames on a mid-range phone, and this also reproduces the
   app's IndexedStack + TickerMode, where offstage tabs genuinely stop ticking.

   This block is NOT gated on any preference, unlike block B. Offstage is a
   performance fact, not a taste. Block B below is byte-identical apart from its
   gate — keep them in sync if you touch either.
   --------------------------------------------------------------------------- */

.pgs-speed.is-offstage .pgss-dial-ring { display: none; }
.pgs-speed.is-offstage .pgss-dial-glow { animation: none; opacity: .5; transform: scale(1); }
.pgs-speed.is-offstage .pgss-arc { animation: none; }
.pgs-speed.is-offstage .pgss-spin { animation: none; transform: none; }
.pgs-speed.is-offstage .pgss-caret,
.pgs-speed.is-offstage .pgss-hist-bar--live { animation: none; opacity: 1; }


/* ---------------------------------------------------------------------------
   12. HONEST FINAL FRAMES — block B: prefers-reduced-motion

   Every selector is gated on html:not(.motion-opt-in). That class is set ONLY
   when a visitor whose OS suppresses motion clicks index.html's "Play the
   motion" button, so the default remains: preference honoured, honest final
   frames. With JS off the class can never be set, so the OS preference always
   wins. Never `animation-play-state: paused`, never a frozen mid-frame.
   --------------------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  html:not(.motion-opt-in) .pgs-speed .pgss-dial-ring { display: none; }
  html:not(.motion-opt-in) .pgs-speed .pgss-dial-glow { animation: none; opacity: .5; transform: scale(1); }
  html:not(.motion-opt-in) .pgs-speed .pgss-arc { animation: none; }
  html:not(.motion-opt-in) .pgs-speed .pgss-spin { animation: none; transform: none; }
  html:not(.motion-opt-in) .pgs-speed .pgss-caret,
  html:not(.motion-opt-in) .pgs-speed .pgss-hist-bar--live { animation: none; opacity: 1; }
}
