/* PremadeIQ — shared stylesheet.
 *
 * CASCADE CONTRACT — do not reorder.
 *   base.html loads this file via <link>, then opens its own style block
 *   holding {% block style %}. Per-page rules therefore come AFTER these and
 *   win at equal specificity. Several pages rely on it (landing/.card,
 *   dashboard/.tier). Move the link below the block and every per-page
 *   override silently stops applying. Pinned by test_static_icons.py.
 *
 * Caching: nginx gives /static/ a 5-minute must-revalidate window and
 * /static/fonts/ a year (see deploy/nginx.conf). Font filenames carry their
 * weight and subset, so a changed file gets a changed name — a long cache on
 * a stable name is how you strand people on an old asset (incident
 * 2026-04-26).
 */

/* ---------------------------------------------------------------------
   1. Typefaces
   ---------------------------------------------------------------------
   Russo One — display only: the h1, the hero, the one number that matters.
   It is a heavy wide grotesque drawn for Cyrillic first, which is why it
   carries a Russian headline better than it carries an English one, and the
   site defaults to Russian. One weight, so it never does body work.
   Golos Text — section headings and running text. Paratype, real Cyrillic.
   IBM Plex Mono — numbers, GUIDs, versions, labels. Tabular by design.

   Latin and Cyrillic are separate files with unicode-range intact: a Russian
   visitor pulls 24 KB of fonts, not 71 KB.
   --------------------------------------------------------------------- */

@font-face {
  font-family: 'Russo One';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/static/fonts/russo-one-400-cyrillic.woff2') format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
  font-family: 'Russo One';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/static/fonts/russo-one-400-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Golos Text';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/static/fonts/golos-text-400-cyrillic.woff2') format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
  font-family: 'Golos Text';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/static/fonts/golos-text-400-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Golos Text';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('/static/fonts/golos-text-600-cyrillic.woff2') format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
  font-family: 'Golos Text';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('/static/fonts/golos-text-600-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/static/fonts/ibm-plex-mono-400-cyrillic.woff2') format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/static/fonts/ibm-plex-mono-400-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* ---------------------------------------------------------------------
   2. Tokens
   ---------------------------------------------------------------------
   Ground and surfaces are UNCHANGED from the pre-redesign site on purpose.
   Swapping #0f0f12 for some other near-black is self-deception: the contrast
   between the two is 1.009, invisible. What changes is rank (gold stops
   painting everything), faction legibility, and a semantic layer that never
   existed.

   Every colour used as TEXT clears WCAG AA (4.5:1) on BOTH surfaces — the
   page and a raised card. Verified before the markup, not after.
   --------------------------------------------------------------------- */

:root {
  color-scheme: dark;

  /* Surfaces */
  --ink:        #0f0f12;
  --raised:     #1a1a1f;
  --rule:       #2a2a30;
  --rule-soft:  #232329;

  /* Text */
  --text:       #e8e8ea;   /* 15.64 / 14.17 */
  --muted:      #9a9aa0;   /*  6.84 /  6.19 */
  --dim:        #6a6a70;   /* decorative only — never carries meaning alone */

  /* Brand, now ranked. Gold is a promotion, not a default. */
  --gold:       #ffd700;   /* crown, King of EBG tier, one number per page */
  --brass:      #d9a441;   /* the working brand colour: rules, ranks, labels */
  /* Text ON a brass/gold fill. Measured 8.42:1 against --brass in the
     browser. Dark by necessity: white on brass scores 2.1 and fails. */
  --on-brass:   #14100a;

  /* Faction — data, not decoration. Three roles each. */
  --horde:          #ef6e63;   /* 6.45 / 5.84 — text */
  --alliance:       #7ab8ff;   /* 9.23 / 8.36 — text */
  /* Fills carry bars, meters and badge backgrounds. The badge case is why
     alliance is a shade darker than the flat-UI blue it used to be: white
     11px text on #2980b9 measured 4.30 and failed AA. #2775ab gives 4.98
     with white, and still clears 3:1 against the page as a meter. */
  --horde-fill:     #c0392b;   /* white on this: 5.44 */
  --alliance-fill:  #2775ab;   /* white on this: 4.98 */
  --horde-tint:     #2a1512;   /* row background */
  --alliance-tint:  #101f2e;

  /* Confidence ladder — the signature. Mirrors /how and the match page. */
  --c-leader:    #ffd700;
  --c-confirmed: #e0b354;   /* 9.79 / 8.87 */
  --c-likely:    #b9a48c;   /* 7.98 / 7.22 */

  /* Semantic layer. Separate from the brand accent. */
  --success: #8fd18a;
  --warn:    #ffb84d;
  --danger:  #ff8080;
  --warn-bg:    #2a1f0a;
  --warn-line:  #6a5220;
  --danger-bg:  #2a0f0f;
  --danger-line:#6a2020;

  /* Type */
  --font-display: 'Russo One', 'Segoe UI', system-ui, sans-serif;
  --font-body:    'Golos Text', 'Segoe UI', system-ui, sans-serif;
  --font-data:    'IBM Plex Mono', 'Cascadia Code', 'Consolas', monospace;

  --step-0: 13px;
  --step-1: 15px;
  --step-2: 17px;
  --step-3: 21px;
  --step-4: 28px;
  --step-5: 40px;

  --measure: 68ch;
}

/* ---------------------------------------------------------------------
   3. Base
   --------------------------------------------------------------------- */

/* touch-action kills the 300ms double-tap delay; the tap highlight is set
   deliberately rather than left as the browser's blue flash. */
html { -webkit-tap-highlight-color: rgba(217, 164, 65, .25); }
body {
  touch-action: manipulation;
  background: var(--ink);
  color: var(--text);
  font-family: var(--font-body);
  font-size: var(--step-1);
  margin: 0 auto;
  padding: 24px 32px;
  max-width: 1120px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}
body.wide { max-width: 1400px; }

/* Russo One is the display face and it is LOUD. It gets the page title and
   nothing else by default; section headings stay on Golos so a page full of
   h2s does not shout. */
h1 {
  font-family: var(--font-display);
  font-size: var(--step-4);
  color: var(--text);
  font-weight: 400;
  line-height: 1.12;
  letter-spacing: .005em;
  text-wrap: balance;
  margin: 0 0 8px;
}
h1 .kb { color: var(--gold); }

h2 {
  scroll-margin-top: 24px;
  font-size: var(--step-2);
  color: var(--text);
  font-weight: 600;
  letter-spacing: .01em;
  margin: 30px 0 10px;
  text-wrap: balance;
}
h3 {
  font-size: var(--step-1);
  color: var(--muted);
  font-weight: 600;
  margin: 20px 0 8px;
}
p { margin: 8px 0; }

code {
  background: var(--raised);
  padding: 1px 6px;
  border-radius: 3px;
  color: var(--brass);
  font-size: 12px;
  font-family: var(--font-data);
}
pre {
  background: var(--raised);
  border: 1px solid var(--rule);
  border-radius: 4px;
  padding: 12px 14px;
  overflow-x: auto;
  font-size: 12px;
  font-family: var(--font-data);
}

a { color: var(--brass); }
a:hover { color: var(--text); }

ul, ol { padding-left: 24px; }
li { margin: 4px 0; }

/* Keyboard users could not see where they were: the site had zero focus
   styling and one :focus rule that set outline:none. */
:where(a, button, select, input, textarea, [tabindex]):focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: 2px;
  border-radius: 2px;
}

/* Off-screen until focused. Without it a keyboard user tabbed through nine
   navigation links on every single page before reaching the content. */
.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  background: var(--brass);
  color: var(--on-brass);
  padding: 10px 16px;
  border-radius: 0 0 3px 0;
  font-weight: 600;
  text-decoration: none;
  z-index: 10;
}
.skip-link:focus {
  left: 0;
}

nav.site {
  display: flex;
  gap: 14px;
  font-size: var(--step-0);
  margin-bottom: 20px;
  border-bottom: 1px solid var(--rule);
  padding-bottom: 10px;
  align-items: center;
  flex-wrap: wrap;
}
nav.site a { color: var(--muted); text-decoration: none; }
nav.site a:hover { color: var(--text); }
nav.site a.here { color: var(--brass); }
nav.site a.brand { display: inline-flex; align-items: center; padding: 0; }
nav.site .brand-logo { height: 28px; width: 28px; border-radius: 6px; display: block; }
nav.site .nav-spacer { flex: 1; }
nav.site .lang-label { color: var(--dim); font-size: 12px; }
nav.site select {
  background: var(--raised);
  color: var(--text);
  border: 1px solid var(--rule);
  border-radius: 3px;
  padding: 3px 6px;
  font-size: 12px;
  font-family: var(--font-body);
}

.sub { color: var(--muted); margin-bottom: 24px; font-size: var(--step-1); max-width: var(--measure); }

/* A card is for a distinct entity — a leader, a player, a match. Data lives
   in tables on the page ground, not in a box. */
.card {
  background: var(--raised);
  border: 1px solid var(--rule);
  border-radius: 4px;
  padding: 18px 20px;
  margin: 14px 0;
}
.warn, .danger {
  /* Standalone box: /install applies these without .card, so they carry
     their own border/padding rather than only recolouring one. */
  border: 1px solid var(--rule);
  border-radius: 4px;
  padding: 12px 14px;
  margin: 14px 0;
}
.warn   { background: var(--warn-bg);   border-color: var(--warn-line); }
.danger { background: var(--danger-bg); border-color: var(--danger-line); }
/* .card already supplies the box; don't double the padding when combined. */
.card.warn, .card.danger { padding: 18px 20px; margin: 14px 0; }

/* Shared table base. admin.html has 26 <th> and admin_user.html 25, with no
   table rules anywhere — they were rendering as default centred browser
   headers. Pages that want something denser override below this file. */
table { border-collapse: collapse; width: 100%; }
th {
  background: var(--rule-soft);
  color: var(--muted);
  text-align: left;
  padding: 6px 10px;
  font-family: var(--font-data);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
}
td { padding: 6px 10px; border-top: 1px solid var(--rule-soft); }

/* Form controls inherit nothing by default — buttons and inputs were still
   rendering in the UA's Arial next to Russo One headings. */
button, input, select, textarea { font-family: var(--font-body); }
input, textarea { font-size: var(--step-0); }

button.primary {
  background: var(--brass);
  color: var(--on-brass);
  border: 0;
  border-radius: 3px;
  padding: 9px 16px;
  font-size: var(--step-0);
  font-weight: 600;
  cursor: pointer;
}
button.primary:hover { background: var(--gold); }

/* .muted was defined three times over (match, player, dashboard) and NOT at
   all on the admin pages, where nine elements use it and rendered at full
   strength. It carries real content — "no data", counters, dashes — so it
   takes --muted (5.58-6.84), not --dim, which the token block reserves for
   decoration precisely because it measures 2.91-3.56 on these surfaces. */
.muted { color: var(--muted); }

/* Digits line up in columns everywhere on this site. */
.num, td.num, th.num, .tv, .stat-cell .num {
  font-variant-numeric: tabular-nums;
}

footer {
  margin-top: 48px;
  padding-top: 18px;
  border-top: 1px solid var(--rule);
  color: var(--dim);
  font-size: 12px;
}

/* ---------------------------------------------------------------------
   4. Confidence ladder — the signature
   ---------------------------------------------------------------------
   ★ leader / ⛓ confirmed member / ≈ likely premade already exist in
   content/how_it_works.*.md and on the match page, and are pinned by
   test_how.py. They are the one visual vocabulary no comparable site has,
   because no comparable site names people.

   Level is carried three ways at once — mark, rail and weight — so it
   survives greyscale and colour-blindness. The "likely" rail is broken
   rather than solid: the one level we are not sure about looks unsure.
   --------------------------------------------------------------------- */

.lvl { position: relative; padding-left: 12px; }
.lvl::before {
  content: "";
  position: absolute;
  left: 0; top: 0; bottom: 0;
  width: 3px;
}
.lvl-leader::before    { background: var(--c-leader); }
.lvl-confirmed::before { background: var(--c-confirmed); }
.lvl-likely::before {
  background: repeating-linear-gradient(
    to bottom, var(--c-likely) 0 3px, transparent 3px 7px);
}
.lvl-leader    .lvl-name { color: var(--text); font-weight: 600; }
.lvl-confirmed .lvl-name { color: var(--text); }
.lvl-likely    .lvl-name { color: var(--muted); }

.lvl-mark { font-family: var(--font-data); }
.lvl-leader    .lvl-mark { color: var(--c-leader); }
.lvl-confirmed .lvl-mark { color: var(--c-confirmed); }
.lvl-likely    .lvl-mark { color: var(--c-likely); }

/* ---------------------------------------------------------------------
   4b. Install page
   ---------------------------------------------------------------------
   /install exists once per language (install.<lang>.html — five files), and
   each shipped its own identical copy of these rules. One copy here; the
   templates keep only their prose. Prefixed .inst-* because the landing page
   already owns .step for its own three-column sequence.
   --------------------------------------------------------------------- */

.inst-step {
  display: flex;
  gap: 18px;
  align-items: flex-start;
  margin: 18px 0;
  padding: 18px 20px;
  background: var(--raised);
  border: 1px solid var(--rule);
  border-radius: 4px;
}
.inst-step .inst-num {
  font-family: var(--font-data);
  font-size: 12px;
  letter-spacing: .1em;
  color: var(--brass);
  min-width: 26px;
  padding-top: 4px;
}
.inst-step .body { flex: 1; min-width: 0; }
.inst-step h3 { margin-top: 0; color: var(--text); font-size: var(--step-2); }

.dl-row { display: flex; gap: 10px; flex-wrap: wrap; margin: 14px 0; }
.dl-row a {
  background: var(--brass);
  color: var(--on-brass);
  padding: 10px 18px;
  border-radius: 3px;
  text-decoration: none;
  font-weight: 600;
  font-size: var(--step-1);
}
.dl-row a:hover { background: var(--gold); color: var(--on-brass); }
.dl-row a.secondary {
  background: transparent;
  color: var(--muted);
  border: 1px solid var(--rule);
  font-weight: 400;
}
.dl-row a.secondary:hover { border-color: var(--brass); color: var(--text); }

.hint { color: var(--muted); font-size: var(--step-0); margin-top: 6px; }

/* The "you need to sign in" button inside the install gate. Was five copies
   of the same style attribute, one per language file.
   Selector is a.gate-btn, not .gate-btn: this button lives INSIDE .warn, and
   `.warn a` above is (0,1,1) — it beat a bare class and painted the label
   brass on a brass fill, i.e. an invisible button on the page whose whole job
   is getting people to sign in. Matching specificity plus being later in the
   file is what wins here. */
a.gate-btn {
  display: inline-block;
  margin-top: 10px;
  background: var(--brass);
  color: var(--on-brass);
  padding: 8px 16px;
  border-radius: 3px;
  text-decoration: none;
  font-weight: 600;
}
a.gate-btn:hover { background: var(--gold); color: var(--on-brass); }

/* .warn/.danger bodies live in section 3; these are the inline bits that
   only long-form pages use. */
.warn b { color: var(--warn); }
.warn code { background: var(--ink); color: var(--warn); }
/* :not(.gate-btn) states the exception instead of relying on source order.
   The gate's sign-in button is an <a> inside .warn, so this rule was
   repainting its label brass — on a brass fill. Raising a.gate-btn to equal
   specificity was not enough: .warn a comes later in the file and won the
   tie. Encode the intent, not the ordering. */
.warn a:not(.gate-btn) { color: var(--brass); }

/* ---------------------------------------------------------------------
   5. Atmosphere
   ---------------------------------------------------------------------
   The subject is a battleground at night, so the ground should not be a
   flat digital slab. Two very quiet layers do it: a vignette that pulls
   attention inward, and a fine grain so large dark areas have texture
   instead of banding.

   Everything here is drawn by us. No Blizzard art, no faction logos — the
   marks below are our own geometry (Horde: a cleaved chevron; Alliance: a
   symmetrical keep-shield), which also keeps them legible at 12px where a
   real crest would turn to mud.
   --------------------------------------------------------------------- */

body::before {
  content: "";
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 0;
  background:
    radial-gradient(120% 90% at 50% -10%, rgba(217,164,65,.055), transparent 55%),
    radial-gradient(100% 70% at 50% 110%, rgba(41,128,185,.045), transparent 60%);
}
/* Grain. An inline SVG turbulence is cheaper than shipping a texture and
   scales to any viewport without tiling seams. */
body::after {
  content: "";
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 0;
  opacity: .035;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)'/%3E%3C/svg%3E");
}
body > * { position: relative; z-index: 1; }

/* Faction marks — our own shapes, inheriting currentColor. */
.mark-faction { width: 1em; height: 1em; vertical-align: -.12em; fill: currentColor; }

/* --- Radar sweep (landing only) --------------------------------------
   The brand mark already carries a radar behind the crown, and the product
   IS reconnaissance, so the ground gets a slow sweep. Opt-in per page via
   body.has-radar — no other page pays for it.

   Contacts are not decoration: landing.html builds them from the SAME
   numbers the alert shows (one leader, N confirmed, M likely) using the
   site's own glyphs, so the background is another view of the same
   sighting. Angle sets both position and flash timing — the fraction of the
   turn to a contact equals the fraction of the period — so echo always
   coincides with the beam passing over it. Period lives in one variable;
   beam and contacts cannot drift apart.
   --------------------------------------------------------------------- */
:root { --sweep: 24s; }

.radar {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 0;
  --radar-x: 68%;
  --radar-y: 40%;
  background: repeating-radial-gradient(circle at var(--radar-x) var(--radar-y),
    transparent 0 118px, rgba(217, 164, 65, .045) 118px 119px);
}
.radar::after {
  content: "";
  position: absolute;
  left: var(--radar-x); top: var(--radar-y);
  width: 150vmax; height: 150vmax;
  margin: -75vmax 0 0 -75vmax;
  background: conic-gradient(from 0deg,
    rgba(217, 164, 65, .10) 0deg, rgba(217, 164, 65, .02) 26deg, transparent 60deg);
  animation: piq-sweep var(--sweep) linear infinite;
}
@keyframes piq-sweep { to { transform: rotate(360deg); } }

.blip {
  position: absolute;
  font-family: var(--font-data);
  line-height: 1;
  opacity: 0;
  animation: piq-blip var(--sweep) linear infinite;
}
@keyframes piq-blip {
  0%   { opacity: 0; transform: scale(.6); }
  2%   { opacity: 1; transform: scale(1); }   /* the beam just passed */
  30%  { opacity: .35; }
  70%  { opacity: 0; transform: scale(1); }
  100% { opacity: 0; }
}
.blip::after {
  content: "";
  position: absolute;
  inset: -7px;
  border-radius: 50%;
  border: 1px solid currentColor;
  opacity: 0;
  animation: piq-blip-ring var(--sweep) linear infinite;
  animation-delay: inherit;
}
@keyframes piq-blip-ring {
  0%   { opacity: 0; transform: scale(.5); }
  2%   { opacity: .55; transform: scale(.8); }
  12%  { opacity: 0; transform: scale(2.4); }
  100% { opacity: 0; }
}
.blip.lead      { color: var(--c-leader);    font-size: 15px; }
.blip.confirmed { color: var(--c-confirmed); font-size: 11px; }
.blip.likely    { color: var(--c-likely);    font-size: 11px; }

/* A hairline brass frame with cut corners: a nod to the game's panel
   chrome without any of its ornament. Used once per page, on the hero. */
.framed { position: relative; }
.framed::before,
.framed::after {
  content: "";
  position: absolute;
  width: 14px; height: 14px;
  border: 1px solid var(--brass);
  opacity: .55;
  pointer-events: none;
}
.framed::before { top: -1px; left: -1px; border-right: 0; border-bottom: 0; }
.framed::after  { bottom: -1px; right: -1px; border-left: 0; border-top: 0; }

/* Gold is the only colour allowed to glow, and only barely. */
.glow { text-shadow: 0 0 18px rgba(255, 215, 0, .28); }

/* ---------------------------------------------------------------------
   6. Motion
   ---------------------------------------------------------------------
   One orchestrated moment — the alert arriving, the way it arrives in game
   — plus quiet feedback on things you touch. Scattered effects are what
   make a page feel machine-made; a single well-timed sequence does not.

   Everything is opt-in via .is-in, added by an IntersectionObserver, so
   content is fully visible with JavaScript off.
   --------------------------------------------------------------------- */

@media (prefers-reduced-motion: no-preference) {
  /* Gated on .piq-js, which base.html's script sets on <html> before paint.
     Without it these rules never apply — so if the script is blocked, fails
     or simply hasn't run, the content is plain visible instead of being
     hidden forever by an animation that never starts. */
  .piq-js .rise { opacity: 0; transform: translateY(10px); }
  .rise.is-in {
    opacity: 1;
    transform: none;
    transition: opacity .5s ease, transform .5s cubic-bezier(.2, .7, .3, 1);
  }
  .rise-1.is-in { transition-delay: .06s; }
  .rise-2.is-in { transition-delay: .12s; }
  .rise-3.is-in { transition-delay: .18s; }

  /* The alert does what the in-game one does: lands, flares once, settles.
     Once — a looping pulse turns a warning into decoration. */
  @keyframes piq-alert-land {
    0%   { opacity: 0; transform: translateY(-8px) scale(.995); }
    60%  { opacity: 1; transform: none; }
    100% { opacity: 1; transform: none; }
  }
  @keyframes piq-alert-flare {
    0%   { box-shadow: 0 0 0 0 rgba(255, 128, 128, .40); }
    100% { box-shadow: 0 0 0 18px rgba(255, 128, 128, 0); }
  }
  .alert-land { animation: piq-alert-land .55s cubic-bezier(.2,.7,.3,1) both; }
  .alert-land::after {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: inherit;
    pointer-events: none;
    animation: piq-alert-flare 1.1s ease-out .35s both;
  }

  /* Meters and rails draw themselves in rather than snapping. Same .piq-js
     gate: a rail scaled to zero with no script to restore it is an invisible
     rail, and the rail is what carries the confidence level. */
  .meter-fill { transition: width .9s cubic-bezier(.2, .7, .3, 1); }
  .piq-js .lvl::before { transform-origin: top; transform: scaleY(0); }
  .lvl.is-in::before { transform: scaleY(1); transition: transform .45s ease .1s; }

  tbody tr, .rowlink { transition: background-color .12s ease; }
}

tbody tr:hover, .rowlink:hover { background: var(--rule-soft); }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
    scroll-behavior: auto !important;
  }
  /* Reveal-on-scroll must never leave content invisible when motion is off. */
  .rise { opacity: 1 !important; transform: none !important; }
  .lvl::before { transform: none !important; }
  /* The radar stops turning, but the contacts stay readable: someone who
     turned motion off still sees a radar with marks, not an empty ground. */
  .radar::after, .blip, .blip::after { animation: none !important; }
  .blip { opacity: .55 !important; }
}
