/* ============================================================================
   Pingo — Home screen phone mock.  Owns mock.html and nothing else.
   Every selector is scoped under .pg-mock so nothing leaks into the site.

   Rebuilt from the LOCKED design "Pingo Home Redesign.dc.html" option 6a
   (STEEL) + turn 7a (the score/tier state matrix). All 12 animations from the
   design's motion list are here at their authored durations, easings, delays
   and iteration counts.

   ---------------------------------------------------------------------------
   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 hatched DNS cell has no animation of any kind. Neither does the
       hatched dial in .is-tier-withheld: inside that state the hero card's
       bloom, its inner glow and its expanding ring are display:none (removed,
       not dimmed, matching the app's RadarHalo, which is never painted when
       there is no live reading), the arc is removed and the number's entrance
       animation is off.
     * prefers-reduced-motion lands on each animation's HONEST FINAL FRAME.
       Never animation-play-state:paused, never a frozen mid-frame:
         - expanding rings vanish entirely (pgRing ends at opacity 0, so "no
           ring" IS its final frame)
         - live dots hold FULLY LIT (opacity 1)
         - the packet dots vanish (pgTravel ends at opacity 0)
         - the signal bars hold FULL (scaleY(1) = each rung's measured height)
         - the score arc holds its COMPLETE value; a partial arc would misstate
           the score, which is the one thing this dial must never do
         - the mint glows park at rest (pgBreathe's own 0% frame, opacity .5),
           while the lit gateway tile holds fully lit — a measured, answering
           tile dimmed to 50% would understate it
     * .is-offstage on .pg-mock applies exactly the same final-frame block. It
       is the web's equivalent of the app's TickerMode and YOUR IntersectionObserver
       must toggle it; CSS cannot see the viewport. See mock.html step 4.
     * Do NOT add the site's .motion-keep escape hatch to this subtree.

   ---------------------------------------------------------------------------
   ILLUSTRATIVE VALUES (chosen, not measured — the design file left these as
   unfilled template holes that render blank in a real browser):
     score 92 / Excellent · PING 18 MS · JITTER 6.2 MS · PKT LOSS 0% of 60 PKT
     DNS not measured yet (hatched) · DOWNLINK 231 Mb/s · SIGNAL -48 dBm, GOOD,
     WI-FI 6 · ISP Telstra · MY IP 192.168.1.14 · GATEWAY 192.168.1.1 ·
     PUBLIC 203.0.113.47 (RFC 5737 documentation range).
   The Good / Fair / Poor tiers reuse turn 7a's own published readings.

   DELIBERATE DIVERGENCES from the design file (all six are documented, none
   are accidents):
     1. Sizing follows the spec's honest option (a): the screen is built at a
        true 375x812 with the authored 8-9px mono floor intact and scaled as a
        whole by --mock-scale. Nothing was re-specced smaller.
     2. Wire 1 (You -> Gateway) carries NO chip. MASTER-CONTEXT section 5
        settled that the You-to-Gateway Mb/s figure comes off ("it is link rate
        not throughput"), and the LAN IP that replaces it does not fit: at the
        9px mono floor "192.168.1.14" needs 84px inside a 61.5px wire and
        would collide with both icon tiles. The LAN IP readout it points at is
        already in this card, in the IP row below ("MY IP"). Wire 2 keeps its
        measured chip. One wire with a number and one without is the honest
        reading of "we measured the internet leg, we are not claiming a figure
        for the gateway leg".
     3. Em dashes used as connectors were rewritten into two sentences (house
        style). Affected: the insight body and the four 7a captions. The dash
        GLYPH survives as the withheld-value placeholder, which is design
        vocabulary.
     4. The hero bloom keeps its centring translate inside its keyframes
        (pgmBreatheC). The design applies transform:translate(-50%,-50%) inline
        AND pgBreathe, whose keyframes set transform:scale() — so the animation
        wipes the translate and the mint disc renders 77px down-and-right of the
        dial, behind the metric cells. Reproducing that would put a mint (=pass)
        glow behind the hatched "not measured" cell. Same timings, same values,
        centre preserved.
     5. The second packet dot gets pgTravel's own 0% frame (left:2%; opacity:0)
        as its base state. The design gives it a 1.3s delay and no base left, so
        for the first 1.3s a lit mint dot sits parked at the middle of the wire.
        A static dot beside a live wire is exactly the wrong signal.
     6. The arc gets a transition on stroke-dashoffset so a tier switch sweeps
        instead of jumping. The design file has zero transitions, but it is also
        a static plate; the app's own ScoreDial redraws on a value change with
        exactly this duration and curve. It lands on the complete new value,
        never a partial arc, and it is switched off under reduced motion and
        offstage. To remove it, delete one declaration in .pgm-dial-arc.

   PUBLIC API — see the header of mock.html for the integration steps.
     .pg-mock                     outer box, occupies calc(375px*scale) x calc(812px*scale)
     .pg-mock--bezel              optional device bezel (adds padding+border outside the box)
     .pg-mock.is-offstage         stop all motion on honest final frames
     --mock-scale                 set on any ancestor; default .88; 1 = authored size
     [data-pg-screen]             the .pgm-screen root that carries the tier class
     .is-tier-excellent|good|fair|poor|withheld
   ============================================================================ */


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

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

  /* Type. The webfonts (Sora 400/600/700/800, JetBrains Mono 500/700) are the
     host page's job; index.html already loads both. Every negative
     letter-spacing below assumes them. */
  font-family: var(--pgm-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(--pgm-ink);
  -webkit-font-smoothing: antialiased;

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

  --pgm-ink: #F2F6FC;
  --pgm-ink2: #D7E0EC;
  --pgm-faint: #94A2B6;
  --pgm-unit: #9FAEC2;
  --pgm-cell-lbl: #A9B7C9;
  --pgm-untested: #A3B1C3;
  --pgm-on-light: #0B121C;

  --pgm-glass-hero: rgba(9, 14, 21, .58);
  --pgm-glass: rgba(9, 14, 21, .5);
  --pgm-line: rgba(255, 255, 255, .11);
  --pgm-line-hero: rgba(255, 255, 255, .12);
  --pgm-line-strong: rgba(255, 255, 255, .14);

  --pgm-pass: #35D6A0;
  --pgm-pass-bright: #4BEDBA;
  --pgm-pass-text: #6FE0B6;
  --pgm-pass-tint: rgba(53, 214, 160, .12);
  --pgm-pass-border: rgba(53, 214, 160, .34);
  --pgm-warn: #FFB454;
  --pgm-warn-text: #E7C08A;
  --pgm-warn-tint: rgba(255, 180, 84, .13);
  --pgm-warn-border: rgba(255, 180, 84, .34);
  --pgm-fail: #FF5C6E;
  --pgm-fail-text: #F0A3AC;
  --pgm-fail-tint: rgba(255, 92, 110, .14);
  --pgm-fail-border: rgba(255, 92, 110, .36);

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

.pg-mock *,
.pg-mock *::before,
.pg-mock *::after { box-sizing: border-box; }

.pg-mock div,
.pg-mock span,
.pg-mock b { margin: 0; padding: 0; }

.pg-mock svg { display: block; flex: none; overflow: visible; }

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

/* Type helpers (design's .mono / .num / .lbl) */
.pg-mock .pgm-mono { font-family: var(--pgm-mono); font-variant-numeric: tabular-nums; }
.pg-mock .pgm-num { font-variant-numeric: tabular-nums; letter-spacing: -1px; }
.pg-mock .pgm-lbl { font: 700 9.5px var(--pgm-mono); letter-spacing: 1.6px; color: #61738E; }
.pg-mock .pgm-grow { flex: 1 1 auto; min-width: 0; }


/* ---------------------------------------------------------------------------
   2. Screen root + the two bloom layers
   --------------------------------------------------------------------------- */

.pg-mock .pgm-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(--mock-scale, .88));
  transform-origin: top left;
}

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


/* ---------------------------------------------------------------------------
   3. Status bar
   --------------------------------------------------------------------------- */

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


/* ---------------------------------------------------------------------------
   4. Content column — 18px screen padding, and it does not scroll
   --------------------------------------------------------------------------- */

.pg-mock .pgm-col {
  flex: 1;
  min-height: 0;
  position: relative;
  z-index: 2;
  padding: 2px 18px 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}


/* ---------------------------------------------------------------------------
   5. Header row — radar logo · wordmark · SSID chip · avatar
   --------------------------------------------------------------------------- */

.pg-mock .pgm-head {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 2px 0 10px;
  flex: none;
}

/* The 26px logo mark is the ONLY radar the app ships on Home, and the only
   sanctioned use of mint as chrome (it is the logo's contact, not a verdict).
   Keep it at 26px: the conic-gradient inside a circular mask aliases if scaled. */
.pg-mock .pgm-logo { position: relative; width: 26px; height: 26px; flex: none; display: block; }
.pg-mock .pgm-logo-clip { position: absolute; inset: 1.6px; border-radius: 50%; overflow: hidden; }
.pg-mock .pgm-logo-sweep {
  position: absolute;
  inset: 0;
  background: conic-gradient(from 0deg, transparent 0deg 290deg,
    rgba(232, 238, 247, .1) 336deg, rgba(232, 238, 247, .6) 360deg);
  animation: pgmSweep 2.8s linear infinite;
}
.pg-mock .pgm-logo-outer { position: absolute; inset: 0; border-radius: 50%; border: 1.7px solid #E8EEF7; }
.pg-mock .pgm-logo-inner { position: absolute; inset: 6px; border-radius: 50%; border: 1.5px solid rgba(232, 238, 247, .72); }
.pg-mock .pgm-logo-dot {
  position: absolute; left: 50%; top: 50%;
  width: 4px; height: 4px; margin: -2px 0 0 -2px;
  border-radius: 50%; background: #E8EEF7;
}
.pg-mock .pgm-logo-blip {
  position: absolute; left: 70%; top: 28%;
  width: 3px; height: 3px; border-radius: 50%; background: #6FE0B6;
  animation: pgmPulse 2.8s .55s ease-in-out infinite;   /* phase-locked to the sweep */
}
.pg-mock .pgm-logo-ring {
  position: absolute; inset: 0; border-radius: 50%;
  border: 1px solid rgba(232, 238, 247, .45);
  animation: pgmRing 2.8s ease-out infinite;
}

.pg-mock .pgm-wordmark { font-size: 16.5px; font-weight: 700; letter-spacing: -.3px; color: var(--pgm-ink); }

.pg-mock .pgm-ssid {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  border-radius: 20px;
  padding: 6px 12px;
  font-size: 11.5px;
  font-weight: 600;
  color: var(--pgm-ink2);
  background: rgba(255, 255, 255, .08);
  border: 1px solid var(--pgm-line-strong);
  white-space: nowrap;
}
.pg-mock .pgm-ssid-dot {
  width: 6px; height: 6px; flex: none; border-radius: 50%;
  background: var(--pgm-pass);
  animation: pgmPulse 2.4s ease-in-out infinite;
}

.pg-mock .pgm-avatar {
  width: 34px; height: 34px; flex: none;
  border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  color: #AEBCCE;
  background: rgba(255, 255, 255, .08);
  border: 1px solid var(--pgm-line-strong);
}


/* ---------------------------------------------------------------------------
   6. Hero score card — the arc length IS the score
   --------------------------------------------------------------------------- */

.pg-mock .pgm-hero {
  flex: none;
  position: relative;
  border-radius: 32px;
  padding: 18px;
  background: var(--pgm-glass-hero);
  border: 1px solid var(--pgm-line-hero);
  /* Load-bearing: without the blur these surfaces read as flat grey plates. */
  -webkit-backdrop-filter: blur(20px);
  backdrop-filter: blur(20px);
  overflow: hidden;
  box-shadow: 0 14px 34px rgba(0, 0, 0, .45);
}

.pg-mock .pgm-hero-bloom {
  position: absolute;
  left: 16%; top: 50%;
  width: 154px; height: 154px;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(var(--pgm-tone), .26), transparent 66%);
  transform: translate(-50%, -50%);
  animation: pgmBreatheC 6s ease-in-out infinite;
}

.pg-mock .pgm-hero-row { position: relative; display: flex; gap: 14px; align-items: center; }
.pg-mock .pgm-hero-left { width: 104px; flex: none; text-align: center; }

.pg-mock .pgm-dial { position: relative; width: 78px; height: 78px; margin: 0 auto; }
.pg-mock .pgm-dial-ring {
  position: absolute; inset: -5px; border-radius: 50%;
  border: 1px solid rgba(var(--pgm-tone), var(--pgm-ring-alpha));
  animation: pgmRing var(--pgm-ring-dur) ease-out infinite;
}
.pg-mock .pgm-dial-glow {
  position: absolute; inset: 14px; border-radius: 50%;
  background: radial-gradient(circle, rgba(var(--pgm-tone), .2), transparent 72%);
  animation: pgmBreathe 6s ease-in-out infinite;
}
/* Shown only when the score is withheld. Never animated. */
.pg-mock .pgm-dial-hatch {
  display: none;
  position: absolute; inset: 10px; border-radius: 50%;
  background: var(--pgm-hatch);
  border: 1px dashed rgba(255, 255, 255, .2);
}
/* rotate(-90deg) or the arc starts at 3 o'clock. */
.pg-mock .pgm-dial-svg { position: relative; display: block; transform: rotate(-90deg); }
.pg-mock .pgm-dial-tick { stroke: rgba(232, 238, 247, .32); }
.pg-mock .pgm-dial-track { stroke: rgba(255, 255, 255, .1); }
.pg-mock .pgm-dial-arc {
  stroke: var(--pgm-arc);
  /* 2*pi*40 = 251.327 -> dashoffset = 251.3 * (1 - score/100) */
  stroke-dashoffset: calc(251.3px * (1 - var(--pgm-dial-frac)));
  animation: pgmDraw 1.7s cubic-bezier(.22, .9, .24, 1) both;
  /* Length animates on a tier switch (the measurement changed); the tone snaps
     (a verdict is not a gradient, and mint->red would cross an off-palette hue). */
  transition: stroke-dashoffset .9s cubic-bezier(.22, .9, .24, 1);
}

.pg-mock .pgm-dial-val {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
}
.pg-mock .pgm-score {
  font-size: 30px;
  font-weight: 800;
  color: var(--pgm-ink);
  letter-spacing: -1.4px;
  line-height: 1;
  /* Entrance only. The digits NEVER tick up — opacity + a 4px rise. */
  animation: pgmCount .7s .3s ease-out both;
}
.pg-mock .pgm-tier {
  font-size: 15px;
  font-weight: 800;
  letter-spacing: -.3px;
  color: var(--pgm-tier-ink);
  line-height: 1.1;
  margin-top: 8px;
}
.pg-mock .pgm-meta {
  font-size: 9px;
  color: var(--pgm-unit);
  letter-spacing: .6px;
  margin-top: 4px;
  white-space: nowrap;
}

.pg-mock .pgm-cells {
  flex: 1;
  min-width: 0;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
}
/* Cell tone is set by the tier rules in section 7 through these five props. */
.pg-mock .pgm-cell {
  --cbg: rgba(255, 255, 255, .06);
  --cbd: 1px solid var(--pgm-line);
  --clbl: var(--pgm-cell-lbl);
  --cval: var(--pgm-ink);
  --cunit: var(--pgm-unit);
  border-radius: 14px;
  padding: 9px 10px;
  background: var(--cbg);
  border: var(--cbd);
}
.pg-mock .pgm-cell-lbl { font-size: 8px; color: var(--clbl); }
.pg-mock .pgm-cell-row { display: flex; align-items: baseline; gap: 3px; margin-top: 3px; }
.pg-mock .pgm-cell-val { font-size: 15px; font-weight: 700; color: var(--cval); }
.pg-mock .pgm-cell-unit { font-size: 9px; color: var(--cunit); white-space: nowrap; }


/* ---------------------------------------------------------------------------
   7. Tier states (turn 7a). Swap ONE class on .pgm-screen.
      Bands: 90-100 Excellent · 75-89 Good · 55-74 Fair · 30-54 Poor ·
             0-29 Critical (not built, see note) · unmeasured -> withheld.
      Every per-tier string lives in the markup as a .pgm-var; CSS picks one.
   --------------------------------------------------------------------------- */

.pg-mock .pgm-var { display: none; }
.pg-mock .is-tier-excellent .pgm-var[data-tier~="excellent"],
.pg-mock .is-tier-good      .pgm-var[data-tier~="good"],
.pg-mock .is-tier-fair      .pgm-var[data-tier~="fair"],
.pg-mock .is-tier-poor      .pgm-var[data-tier~="poor"],
.pg-mock .is-tier-withheld  .pgm-var[data-tier~="withheld"] { display: inline; }

/* --pgm-tone drives the bloom, the inner glow and the expanding ring, so a
   mint (= pass) glow can never sit behind an amber or red verdict. */
.pg-mock .pgm-screen.is-tier-excellent {
  --pgm-dial-frac: .92; --pgm-arc: var(--pgm-pass-bright); --pgm-tone: 53, 214, 160;
  --pgm-tier-ink: var(--pgm-pass-text); --pgm-ring-alpha: .3; --pgm-ring-dur: 3.6s;
}
.pg-mock .pgm-screen.is-tier-good {
  --pgm-dial-frac: .78; --pgm-arc: var(--pgm-pass); --pgm-tone: 53, 214, 160;
  --pgm-tier-ink: var(--pgm-pass-text); --pgm-ring-alpha: .3; --pgm-ring-dur: 3.6s;
}
.pg-mock .pgm-screen.is-tier-fair {
  --pgm-dial-frac: .61; --pgm-arc: var(--pgm-warn); --pgm-tone: 255, 180, 84;
  --pgm-tier-ink: var(--pgm-warn); --pgm-ring-alpha: .34; --pgm-ring-dur: 3.6s;
}
/* Poor wears 7a's red alarm ring: same pgRing, 3s, at .38 */
.pg-mock .pgm-screen.is-tier-poor {
  --pgm-dial-frac: .34; --pgm-arc: var(--pgm-fail); --pgm-tone: 255, 92, 110;
  --pgm-tier-ink: var(--pgm-fail); --pgm-ring-alpha: .38; --pgm-ring-dur: 3s;
}
.pg-mock .pgm-screen.is-tier-withheld {
  --pgm-dial-frac: 0; --pgm-arc: transparent; --pgm-tone: 163, 177, 195;
  --pgm-tier-ink: var(--pgm-untested); --pgm-ring-alpha: 0; --pgm-ring-dur: 3.6s;
}

/* Cell tones. Assigned from 7a's own readings by a stated threshold, so the
   worst input is always visible and can never hide behind the others:
     PING  <=40 neutral · 41-150 caution · >150 fail
     JITTER <=7 neutral · 7.1-30 caution · >30 fail
     LOSS   0% pass · >0-2% caution · >2% fail
     DNS   <=60 neutral · 61-120 caution · >120 fail
     not measured -> hatched, whatever the tier */
.pg-mock .is-tier-excellent .pgm-cell--loss,
.pg-mock .is-tier-good .pgm-cell--loss {
  --cbg: var(--pgm-pass-tint); --cbd: 1px solid var(--pgm-pass-border);
  --clbl: #8FD9C0; --cval: var(--pgm-pass-text); --cunit: #8FD9C0;
}
.pg-mock .is-tier-good .pgm-cell--jitter,
.pg-mock .is-tier-fair .pgm-cell--ping,
.pg-mock .is-tier-fair .pgm-cell--jitter,
.pg-mock .is-tier-fair .pgm-cell--loss {
  --cbg: var(--pgm-warn-tint); --cbd: 1px solid var(--pgm-warn-border);
  --clbl: var(--pgm-warn-text); --cval: var(--pgm-warn); --cunit: var(--pgm-warn-text);
}
.pg-mock .is-tier-poor .pgm-cell--ping,
.pg-mock .is-tier-poor .pgm-cell--jitter,
.pg-mock .is-tier-poor .pgm-cell--loss,
.pg-mock .is-tier-poor .pgm-cell--dns {
  --cbg: var(--pgm-fail-tint); --cbd: 1px solid var(--pgm-fail-border);
  --clbl: var(--pgm-fail-text); --cval: var(--pgm-fail); --cunit: var(--pgm-fail-text);
}
/* The hatch is a semantic state ("not measured yet"), not a texture. */
.pg-mock .is-tier-excellent .pgm-cell--dns,
.pg-mock .is-tier-withheld .pgm-cell--dns,
.pg-mock .is-tier-withheld .pgm-cell--loss {
  --cbg: var(--pgm-hatch); --cbd: 1px dashed rgba(255, 255, 255, .2);
  --clbl: var(--pgm-untested); --cval: var(--pgm-untested); --cunit: var(--pgm-untested);
}

/* WITHHELD — a hatched dial has nothing to be live about, so every animation
   in the score card stops and the mint layers are REMOVED, not dimmed. */
.pg-mock .is-tier-withheld .pgm-hero-bloom,
.pg-mock .is-tier-withheld .pgm-dial-ring,
.pg-mock .is-tier-withheld .pgm-dial-glow,
.pg-mock .is-tier-withheld .pgm-dial-arc { display: none; }
.pg-mock .is-tier-withheld .pgm-dial-hatch { display: block; }
.pg-mock .is-tier-withheld .pgm-dial-tick { stroke: rgba(232, 238, 247, .18); }
.pg-mock .is-tier-withheld .pgm-dial-track { stroke: rgba(232, 238, 247, .2); stroke-dasharray: 4 5.5; }
/* NOTE the absence of `animation: none` here, which is load-bearing.
   Setting it changed the computed animation-name from pgmCount to none, so
   LEAVING this state changed it back and re-fired the entrance — the dial
   played "a fresh score just landed" every time the cycle passed out of
   no-score. Keeping the same animation-name means the entrance runs once on
   load and never again. pgmCount is `both`, so it stays on its final frame. */
.pg-mock .is-tier-withheld .pgm-score {
  color: var(--pgm-untested);
  letter-spacing: -1px;
}

/* Not built: the 0-29 Critical band. 7a ships no Critical plate, so its four
   cell readings would have to be invented. To add it, copy the .is-tier-poor
   blocks, use arc #C2384A (the legend swatch) and supply real numbers. */


/* ---------------------------------------------------------------------------
   8. Link strip — You -> Gateway -> Internet, and the IP readout
   --------------------------------------------------------------------------- */

.pg-mock .pgm-link {
  flex: none;
  margin-top: 11px;
  border-radius: 22px;
  padding: 13px 16px;
  background: var(--pgm-glass);
  border: 1px solid var(--pgm-line);
  -webkit-backdrop-filter: blur(20px);
  backdrop-filter: blur(20px);
}
.pg-mock .pgm-nodes { display: flex; align-items: center; }

.pg-mock .pgm-node {
  width: 58px;
  flex: none;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 5px;
}
.pg-mock .pgm-node--gw { width: 64px; position: relative; }
.pg-mock .pgm-tile {
  width: 38px; height: 38px;
  border-radius: 13px;
  background: rgba(255, 255, 255, .07);
  border: 1px solid rgba(255, 255, 255, .15);
  display: flex; align-items: center; justify-content: center;
  color: var(--pgm-ink2);
}
/* The gateway is the thing that is alive: mint tile, breathing, ringed — and
   only because this hop is genuinely answering. */
.pg-mock .pgm-tile--gw {
  width: 42px; height: 42px;
  border-radius: 14px;
  border: 0;
  background: linear-gradient(155deg, #3BE0AE, #128A66);
  color: #022019;
  animation: pgmBreathe 6s ease-in-out infinite;
}
.pg-mock .pgm-gw-ring {
  position: absolute; top: -7px;
  width: 52px; height: 52px;
  border-radius: 50%;
  border: 1px solid rgba(53, 214, 160, .45);
  animation: pgmRing 3.2s ease-out infinite;
}

/* HONESTY GATE ON THE GATEWAY, ported from the app's own rule:
     _halo => node.live && node.tone != null && node.tone != HonestyTone.untested
   (lib/widgets/ui.dart:4606-4612, fed by `live: _gatewayMs != null && _fastActive`)
   An expanding ring is the loudest "a measurement is happening" signal on the
   screen. In the withheld state there is no reading behind ANY of this, so the
   ring is REMOVED rather than dimmed — the app constructs no controller at all
   in that case — and the tile parks on pgmBreathe's own 0% frame. Without this,
   a mint ring kept expanding out of the gateway two rows below the score card
   that had just declared it has nothing to report. */
.pg-mock .is-tier-withheld .pgm-gw-ring { display: none; }
.pg-mock .is-tier-withheld .pgm-tile--gw { animation: none; opacity: .5; }
.pg-mock .pgm-node-lbl { font-size: 10px; font-weight: 600; color: var(--pgm-ink2); white-space: nowrap; }

.pg-mock .pgm-wire {
  flex: 1;
  position: relative;
  height: 38px;
  min-width: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}
/* The wire, its packet dots and the leg's ms chip all carry --pgm-tone, the
   same per-tier triple that drives the dial. They used to be hard-coded mint,
   which meant that in Fair and Poor the identical ping reading was a red
   FAILURE in the metric cell and a mint PASS on the wire, three rows apart.
   Mint means "a check passed" and nothing else, so a mint wire under a 312 ms
   ping was mint used as link chrome. The app makes the same call deliberately
   elsewhere: RadarField's sweep is steel, not mint, because "mint would read as
   'these devices passed something'" (ui.dart:5214-5215). */
.pg-mock .pgm-wire-bar {
  position: absolute; left: 0; right: 0; top: 18px; height: 2px;
  background: repeating-linear-gradient(90deg, rgba(var(--pgm-tone), .78) 0 6px, transparent 6px 14px);
  /* the 14px shift equals the dash period, so the loop is invisible */
  animation: pgmFlowX 1s linear infinite;
}
.pg-mock .pgm-wire-dot {
  position: absolute; top: 14px;
  /* pgmTravel's own 0% frame as the base state, so the half-cycle-delayed dot
     below is invisible during its 1.3s delay instead of parked mid-wire. */
  left: 2%;
  opacity: 0;
  width: 7px; height: 7px;
  border-radius: 50%;
  background: rgb(var(--pgm-tone));
  animation: pgmTravel 2.6s linear infinite;
}
.pg-mock .pgm-wire-dot--late { animation: pgmTravel 2.6s 1.3s linear infinite; } /* half a cycle */
.pg-mock .pgm-chip {
  position: relative;
  background: #080D14;
  border: 1px solid rgba(var(--pgm-tone), .34);
  border-radius: 8px;
  padding: 2px 6px;
  font-size: 9px;
  font-weight: 700;
  color: rgb(var(--pgm-tone));
  white-space: nowrap;
}

/* Withheld: no reading, so no flow and no packet in flight. Both would claim
   traffic is being measured on a screen that has just said it is not. */
.pg-mock .is-tier-withheld .pgm-wire-bar { animation: none; opacity: .45; }
.pg-mock .is-tier-withheld .pgm-wire-dot { animation: none; opacity: 0; }

.pg-mock .pgm-ips {
  display: flex;
  justify-content: space-between;
  margin-top: 11px;
  padding-top: 10px;
  border-top: 1px solid rgba(255, 255, 255, .1);
}
.pg-mock .pgm-ip { min-width: 0; }
.pg-mock .pgm-ip--end { text-align: right; }
.pg-mock .pgm-ip-lbl { font-size: 8px; color: var(--pgm-faint); white-space: nowrap; }
.pg-mock .pgm-ip-val {
  font-size: 10px;
  font-weight: 500;
  color: var(--pgm-ink2);
  margin-top: 3px;
  white-space: nowrap;
}
.pg-mock .pgm-ip-val--pub { color: var(--pgm-ink); }


/* ---------------------------------------------------------------------------
   9. Downlink + Signal pair
   --------------------------------------------------------------------------- */

.pg-mock .pgm-pair { display: flex; gap: 11px; margin-top: 11px; flex: none; }
.pg-mock .pgm-pcard {
  flex: 1;
  min-width: 0;
  border-radius: 20px;
  padding: 12px 14px;
  background: var(--pgm-glass);
  border: 1px solid var(--pgm-line);
  -webkit-backdrop-filter: blur(20px);
  backdrop-filter: blur(20px);
}
.pg-mock .pgm-phead { display: flex; align-items: center; justify-content: space-between; }
.pg-mock .pgm-phead-lbl { font-size: 8.5px; color: var(--pgm-faint); }
.pg-mock .pgm-phead-icon { color: var(--pgm-ink2); }
.pg-mock .pgm-sig-word { font-size: 8.5px; font-weight: 700; color: var(--pgm-pass-text); }
.pg-mock .pgm-pval { display: flex; align-items: baseline; gap: 4px; margin-top: 5px; }
.pg-mock .pgm-pnum { font-size: 23px; font-weight: 800; color: var(--pgm-ink); }
.pg-mock .pgm-punit { font-size: 10px; font-weight: 600; color: var(--pgm-unit); }

/* The duplicated polyline is mandatory: with one copy the trace runs out. */
.pg-mock .pgm-trace { height: 18px; overflow: hidden; margin-top: 5px; }
.pg-mock .pgm-trace-run {
  display: flex;
  width: 200%;
  height: 100%;
  animation: pgmMarq 7s linear infinite;
}
.pg-mock .pgm-trace-svg { width: 50%; height: 100%; flex: none; }

.pg-mock .pgm-ladder { display: flex; align-items: flex-end; gap: 3px; height: 18px; margin-top: 5px; }
.pg-mock .pgm-bar {
  flex: 1;
  background: var(--pgm-pass);
  border-radius: 2px;
  transform-origin: bottom;
  animation: pgmBar 2.2s ease-in-out infinite;
}
.pg-mock .pgm-bar--1 { height: 40%; animation-delay: 0s; }
.pg-mock .pgm-bar--2 { height: 60%; animation-delay: .2s; }
.pg-mock .pgm-bar--3 { height: 80%; animation-delay: .4s; }
.pg-mock .pgm-bar--4 { height: 100%; animation-delay: .6s; }
/* An unlit rung is the statement "the signal is not this strong". Animating it
   would soften a measurement into a suggestion, so it never moves. */
.pg-mock .pgm-bar--unlit {
  height: 70%;
  background: rgba(255, 255, 255, .16);
  animation: none;
}
.pg-mock .pgm-wifi { font-size: 8px; color: var(--pgm-unit); margin-left: 2px; white-space: nowrap; }


/* ---------------------------------------------------------------------------
   10. "Worth knowing" insight — amber means caution, not failure
   --------------------------------------------------------------------------- */

.pg-mock .pgm-insight {
  flex: none;
  margin-top: 11px;
  border-radius: 20px;
  padding: 12px 15px;
  background: var(--pgm-glass);
  border: 1px solid var(--pgm-line);
  -webkit-backdrop-filter: blur(20px);
  backdrop-filter: blur(20px);
}
.pg-mock .pgm-insight-head { display: flex; align-items: center; gap: 9px; }
.pg-mock .pgm-insight-icon { color: var(--pgm-warn); }
.pg-mock .pgm-insight-lbl { color: var(--pgm-warn); }
.pg-mock .pgm-conf { font-size: 9px; font-weight: 700; color: var(--pgm-unit); }
.pg-mock .pgm-insight-body {
  font-size: 12px;
  color: var(--pgm-ink2);
  line-height: 1.45;
  margin-top: 8px;
}
.pg-mock .pgm-b { color: var(--pgm-ink); font-weight: 700; }


/* ---------------------------------------------------------------------------
   11. Quick actions — exactly one white primary, no coloured buttons anywhere
   --------------------------------------------------------------------------- */

.pg-mock .pgm-actions {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
  margin-top: 11px;
  flex: none;
}
.pg-mock .pgm-act {
  border-radius: 18px;
  padding: 11px 13px;
  display: flex;
  align-items: center;
  gap: 10px;
  background: rgba(255, 255, 255, .07);
  border: 1px solid var(--pgm-line-strong);
  color: var(--pgm-ink2);
}
.pg-mock .pgm-act-lbl { font-size: 12px; font-weight: 700; color: var(--pgm-ink); white-space: nowrap; }
.pg-mock .pgm-act--primary {
  background: linear-gradient(150deg, #FFFFFF, #CFDBE9);
  border: 0;
  color: var(--pgm-on-light);
  box-shadow: 0 10px 24px rgba(0, 0, 0, .4);
}
.pg-mock .pgm-act--primary .pgm-act-lbl { color: var(--pgm-on-light); }


/* ---------------------------------------------------------------------------
   12. Nav reserve + floating tab bar
   --------------------------------------------------------------------------- */

/* The layout contract: the bar floats and its shadow reaches ~20px up, so the
   column reserves a hard 104px and no control ever sits under it. */
.pg-mock .pgm-reserve { height: 104px; flex: none; }

.pg-mock .pgm-nav {
  position: absolute;
  left: 16px; right: 16px; bottom: 14px;
  height: 72px;
  border-radius: 26px;
  background: rgba(9, 13, 20, .88);
  border: 1px solid var(--pgm-line);
  -webkit-backdrop-filter: blur(22px);
  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);
}
.pg-mock .pgm-tab {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  color: #7C8CA1;
}
.pg-mock .pgm-tab-lbl { font-size: 9.5px; font-weight: 600; }
.pg-mock .pgm-tab.is-active { color: var(--pgm-ink); }
.pg-mock .pgm-tab.is-active .pgm-tab-lbl { font-weight: 700; }
.pg-mock .pgm-tools-tile {
  width: 34px; height: 34px;
  border-radius: 12px;
  background: linear-gradient(150deg, #FFFFFF, #C9D6E6);
  display: flex; align-items: center; justify-content: center;
  color: var(--pgm-on-light);
  box-shadow: 0 8px 18px rgba(0, 0, 0, .45);
}
.pg-mock .pgm-tab--tools .pgm-tab-lbl { color: var(--pgm-ink); font-weight: 700; }


/* ---------------------------------------------------------------------------
   13. Keyframes. Names are the design's, namespaced pgm* so they cannot
       collide with anything the site declares.
         pgmSweep   = pgSweep    pgmPulse  = pgPulse    pgmRing  = pgRing
         pgmBreathe = pgBreathe  pgmDraw   = pgDraw     pgmCount = pgCount
         pgmFlowX   = pgFlowX    pgmTravel = pgTravel   pgmMarq  = pgMarq
         pgmBar     = pgBar      pgmBreatheC = pgBreathe, centre preserved
       The design's five unused keyframes (pgSpin, pgBlink, pgFloat, pgDrift,
       pgShimmer) belong to rejected turns 1-5 and are deliberately absent.
   --------------------------------------------------------------------------- */

@keyframes pgmSweep { to { transform: rotate(360deg); } }
@keyframes pgmPulse { 0%, 100% { opacity: 1; } 50% { opacity: .28; } }
@keyframes pgmRing { 0% { transform: scale(.6); opacity: .55; } 100% { transform: scale(1.55); opacity: 0; } }
/* 0%/100% sit at opacity .5 deliberately — these glows never reach full. */
@keyframes pgmBreathe { 0%, 100% { opacity: .5; transform: scale(1); } 50% { opacity: 1; transform: scale(1.04); } }
@keyframes pgmBreatheC {
  0%, 100% { opacity: .5; transform: translate(-50%, -50%) scale(1); }
  50% { opacity: 1; transform: translate(-50%, -50%) scale(1.04); }
}
/* No `to` block: CSS interpolates to the element's own dashoffset, i.e. the
   score. The arc must always finish on the complete value. */
@keyframes pgmDraw { from { stroke-dashoffset: 251.3px; } }
@keyframes pgmCount { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }
@keyframes pgmFlowX { to { background-position: 14px 0; } }
@keyframes pgmTravel {
  0% { left: 2%; opacity: 0; }
  14% { opacity: 1; }
  86% { opacity: 1; }
  100% { left: 96%; opacity: 0; }
}
@keyframes pgmMarq { to { transform: translateX(-50%); } }
@keyframes pgmBar { 0%, 100% { transform: scaleY(.45); } 50% { transform: scaleY(1); } }


/* ---------------------------------------------------------------------------
   14. HONEST FINAL FRAMES — block A: offstage.
       Your IntersectionObserver toggles .is-offstage on .pg-mock. This is the
       web's TickerMode: motion stops when the mock is not being looked at.
       ***  KEEP THIS BLOCK AND BLOCK B (section 15) IDENTICAL.  ***
   --------------------------------------------------------------------------- */

.pg-mock.is-offstage .pgm-logo-sweep { animation: none; transform: rotate(50.4deg); }
.pg-mock.is-offstage .pgm-logo-blip,
.pg-mock.is-offstage .pgm-ssid-dot { animation: none; opacity: 1; }
.pg-mock.is-offstage .pgm-logo-ring,
.pg-mock.is-offstage .pgm-dial-ring,
.pg-mock.is-offstage .pgm-gw-ring,
.pg-mock.is-offstage .pgm-wire-dot { display: none; }
.pg-mock.is-offstage .pgm-hero-bloom { animation: none; opacity: .5; transform: translate(-50%, -50%) scale(1); }
.pg-mock.is-offstage .pgm-dial-glow { animation: none; opacity: .5; transform: scale(1); }
.pg-mock.is-offstage .pgm-tile--gw { animation: none; opacity: 1; transform: none; }
.pg-mock.is-offstage .pgm-dial-arc { animation: none; transition: none; }
.pg-mock.is-offstage .pgm-score,
.pg-mock.is-offstage .pgm-wire-bar,
.pg-mock.is-offstage .pgm-trace-run,
.pg-mock.is-offstage .pgm-bar { animation: none; }


/* ---------------------------------------------------------------------------
   15. HONEST FINAL FRAMES — block B: prefers-reduced-motion.
       Not animation-play-state:paused, and not a frozen mid-frame: each
       animation's real end state. The sweep holds the app's own reduced pose
       (the beam just past the blip, RadarMark.blipBearing = 0.14 turns).
       ***  IDENTICAL TO BLOCK A ABOVE.  ***
   --------------------------------------------------------------------------- */

/* Every selector is gated on html:not(.motion-opt-in). That class is set ONLY when the visitor
   explicitly asks for motion via the "Play the motion" control, which is itself
   only rendered when the preference is detected. Default behaviour, and the
   behaviour with JS off, is unchanged: the OS preference wins and these honest
   final frames apply. Block A (.is-offstage) is deliberately NOT gated, because
   off-screen is a performance fact, not a preference. */
@media (prefers-reduced-motion: reduce) {
  html:not(.motion-opt-in) .pg-mock .pgm-logo-sweep { animation: none; transform: rotate(50.4deg); }
  html:not(.motion-opt-in) .pg-mock .pgm-logo-blip,
  html:not(.motion-opt-in) .pg-mock .pgm-ssid-dot { animation: none; opacity: 1; }
  html:not(.motion-opt-in) .pg-mock .pgm-logo-ring,
  html:not(.motion-opt-in) .pg-mock .pgm-dial-ring,
  html:not(.motion-opt-in) .pg-mock .pgm-gw-ring,
  html:not(.motion-opt-in) .pg-mock .pgm-wire-dot { display: none; }
  html:not(.motion-opt-in) .pg-mock .pgm-hero-bloom { animation: none; opacity: .5; transform: translate(-50%, -50%) scale(1); }
  html:not(.motion-opt-in) .pg-mock .pgm-dial-glow { animation: none; opacity: .5; transform: scale(1); }
  html:not(.motion-opt-in) .pg-mock .pgm-tile--gw { animation: none; opacity: 1; transform: none; }
  html:not(.motion-opt-in) .pg-mock .pgm-dial-arc { animation: none; transition: none; }
  html:not(.motion-opt-in) .pg-mock .pgm-score,
  html:not(.motion-opt-in) .pg-mock .pgm-wire-bar,
  html:not(.motion-opt-in) .pg-mock .pgm-trace-run,
  html:not(.motion-opt-in) .pg-mock .pgm-bar { animation: none; }
}
