/* public/shared/head.css — the merchant top bar and account menu.
 *
 * THE FIRST SHARED ASSET IN THIS REPO, and that is the point. Eight pages hand
 * rolled their own topbar in three different flavours, and this bar has to be
 * the same on four of them. api/_flags.js says why in its own words: "a second
 * copy of this decision is a copy that drifts, and the day it drifts the thing
 * that leaks is a signup form on the live site." A header copied four times is
 * the same bet.
 *
 * TOKENS ARE NOT DECLARED HERE — AND THE SENTENCE THAT USED TO SIT HERE WAS
 * THE REASON THE PALETTE NEVER GOT CENTRALISED. It said every page already
 * declares :root, so declaring it again here would be a second copy. Measured
 * 2026-08-23 (docs/UX-AUDIT-2.0.md §F1): there are SIXTEEN copies, one per
 * page, and the fallbacks below were the seventeenth. Sixteen copies were
 * never the avoidance of a second copy. They were the disease, and the file
 * that warned about it was one of them.
 *
 * public/shared/tokens.css IS THAT ONE PLACE, and it landed 2026-08-23 (7️⃣ M5,
 * on the Arbiter's ruling that the palette is the marketing pages' set). Every
 * page links it before its own stylesheet and declares no brand colour of its
 * own; tests/brand-palette.mjs walks public/ and fails if one starts to again.
 * This file USES the tokens and may not invent a value for one — the fallbacks
 * below are the last resort for a page that somehow ships without the link, and
 * they are asserted against docs/BRAND-GUIDE.md's own table rather than typed.
 *
 * THE SKELETON IS IN THE HTML, NOT IN JS. Everything below styles markup the
 * page ships with. head.js only upgrades it. If the script never loads the bar
 * is still a bar and the links still work, which is the whole reason /diy and
 * /m are allowed to carry it at all. */

/* EVERY var() HERE CARRIES A FALLBACK, and that is new as of 2026-08-17.
 * The bar used to land only on pages that declare the full palette. It now
 * lands on all of them, and public/404.html declares none of it — a var() with
 * no fallback resolves to nothing, which would have been an invisible wordmark
 * on a transparent bar on the one page a lost visitor sees.
 *
 * THE VALUES WERE WRONG FROM THE DAY THEY WERE WRITTEN, corrected 2026-08-23.
 * "The palette's own" was copied off /app, and /app's copy has drifted:
 * --dim #5C5349, --chili #C81E1E and --pandan #166B49 appear nowhere in
 * docs/BRAND-GUIDE.md:61-69. Two pages read these rather than their own :root,
 * and both were wearing a colour the brand does not have — public/404.html,
 * which declares no palette at all, and public/m.html, which declares --sub,
 * --red and --green instead of --dim, --chili and --pandan. So the one page a
 * lost visitor sees and the console a merchant manages his campaign from were
 * the two carrying it. Contrast after the correction, all still AA: chili on
 * white 5.01, white on chili 5.01, dim on white 6.05, white on pandan 5.29. */
.svbar { border-bottom: 2.5px solid var(--ink, #191410); background: #fff; }
.svbar .svin {
  display: flex; align-items: center; gap: 14px;
  padding: 11px 22px; max-width: 980px; margin: 0 auto;
}

/* THE RED TICKET BLOCK — 正本 :967 (markup) and :63-67 (rule), and it is not a
 * new mark. public/index.html:52-54, terms, privacy, refund, thanks and viral
 * have shipped these three rules BYTE-IDENTICALLY for as long as they have
 * existed; the six merchant pages carried a plain two-tone wordmark instead.
 * So the product has always had two front doors wearing different faces, and
 * the comment that used to sit here is the reason: "deliberately the /app +
 * /account treatment ... because this bar lands on pages that use both and the
 * plain one survives being squeezed next to four links on a phone."
 *
 * BOTH HALVES OF THAT ARGUMENT ARE GONE. It lands on pages that use both
 * because the merchant half had the plain one — circular. And the four links it
 * had to survive being squeezed next to were removed in this same change (see
 * .svnav's obituary below), so the block now sits alone against a spacer.
 *
 * The declarations are COPIED from public/index.html rather than re-derived, so
 * a visitor who crosses from the marketing site into his workspace does not
 * watch the mark change shape. tests/brand-palette.mjs owns the two colours. */
.svbar .block {
  background: var(--chili, #D62828); color: #fff; padding: 8px 11px 10px;
  display: inline-block; min-width: 86px; flex: 0 0 auto;
  border: 2.5px solid var(--ink, #191410); border-radius: 10px; text-decoration: none;
}
.svbar .block .w1, .svbar .block .w2 {
  font-family: 'Bricolage Grotesque', sans-serif; font-weight: 800; font-size: 12.5px;
  line-height: 1.1; display: flex; justify-content: space-between; letter-spacing: .06em;
}
.svbar .block .dash { border-top: 2px dashed rgba(255, 255, 255, .85); margin: 4px 0; }

/* THE SPACER — 正本 :62, `.topbar .sp{flex:1}`. It is what puts the actions on
   the right now that nothing lives in the middle. A `margin-left:auto` on
   .svright would do the same thing and say less. */
.svbar .sp { flex: 1 1 auto; min-width: 0; }

/* ── WHAT WAS HERE AND WHY IT IS NOT ────────────────────────────────────────
 * AND `.svret` BELOW IS NOT THIS COMING BACK. The pill described here was on
 * every page unconditionally; `.svret` appears only when a page a merchant was
 * BUILDING on is behind him in this tab, and it points at that page rather than
 * at the workspace. Same corner, different promise — the ruling below still
 * stands.
 *
 * `.svback` (a back pill beside the wordmark) and `.svnav` (Home · Build a
 * voucher · My vouchers · Assets) were both removed 2026-08-25 on the Arbiter's
 * "i want exactly the same": the 正本's bar is a block, a spacer, and four
 * actions, and it has neither.
 *
 * THIS REVERSES A FIX, AND SAYING SO IS THE POINT. docs/UX-AUDIT-2.0.md §A
 * measured twelve renders with zero back links and `.svback` was the answer to
 * it. What replaces it is not nothing: the 正本 collects "where can I go" into
 * the avatar menu, which goes from four rows to five destinations in this same
 * change, and the red Campaign pill on this bar is the way back to /app. (The
 * block was the way back for one day: head.js repointed it at /app for a
 * signed-in merchant until the Arbiter ruled on 2026-08-26 that the wordmark
 * means swipevoucher.com from everywhere. The menu's My Campaign row is what
 * carries the workspace below 620px, where this pill is hidden.)
 * The one affordance genuinely lost is one-level-up on
 * /app/assets/library, which used to point at /app/assets; that page is now a
 * menu row instead. Recorded in BUILD-STATUS.md rather than argued away.
 *
 * `.svnav`'s min-width:0 lesson is not lost with it — the flex item that can
 * afford to shrink is now `.sp`, and it carries the same declaration for the
 * same reason. */
.svbar .svright { display: flex; align-items: center; gap: 9px; flex: 0 0 auto; }
/* AND THE SLOT ITSELF IS A ROW. [data-svright] ships as a <span>, which is
   inline — so the four controls head.js draws into it laid out as inline content
   and WRAPPED, dropping the avatar onto a second line and making the bar two
   rows tall. It held one link before this round and the difference never showed.
   SCOPED TO THE SLOT, NOT TO .svbar, AND EVERY RULE BELOW FOLLOWS IT. See the
   block at the foot of this file: SIX MORE PAGES host this same slot in a topbar
   of their own (`[data-svbar]`) — the landing page, /viral and the four legal
   ones. Anything scoped `.svbar` simply does not reach them, and all three
   reviewers of 2026-08-25 measured what that cost: the three new controls
   rendered as underlined inline text, 0px tall, no border, no collapse, and the
   bar ran 106px past a 390px phone on /terms and 59px past it on the landing
   page — where Chromium answers by shrinking the whole document to fit, so every
   word of the Terms rendered at 79% of its intended size. head.js draws into
   [data-svright]; so does this. */
[data-svright] { display: flex; align-items: center; gap: 9px; }
/* ONE LANGUAGE CONTROL ON SCREEN, NEVER TWO — Arbiter ruling C, 2026-08-25:
   signed in it lives in the account menu, signed out it lives on the bar. Both
   halves are right about different screens; public/shared/i18n.js has the whole
   argument for the second one, and getting it wrong once is why that comment is
   as long as it is. head.js stamps data-svin on the bar in the same pass that
   builds the menu, and leaves an empty <li data-svlang-slot> where the row goes.
   THE ROW ITSELF IS ②'s WORK, IN i18n.js, THIS ROUND. Between this branch
   landing and that one, a signed-in merchant has no switcher — the slot is
   ready, the rule is ready, and BUILD-STATUS.md names the gap rather than
   letting it be discovered. */
[data-svin] .svlang { display: none; }
/* AN UNFILLED SLOT IS NOT A GAP IN THE MENU. :empty rather than a height of 0,
   so the day it is filled nothing has to be undone. */
.svmenu li[data-svlang-slot]:empty { display: none; }

/* ── THE FOUR ACTIONS — 正本 :1674-1688 ──────────────────────────────────────
 * + Campaign, Buy credits, the cart chip and the avatar. head.js draws them
 * into [data-svright] when it knows who he is; a signed-out bar keeps the
 * Sign in link that ships in the markup and none of this applies to it.
 *
 * ANCHORS, NOT BUTTONS, FOR THE FIRST THREE. The 正本's are <button> because it
 * is a hash router with no addresses. These navigate, and this repo has already
 * settled that argument once, in the menu's credit row: "a control that
 * navigates is a link — a button here would be a keyboard user told 'press'
 * and a right-click with nothing in it." */
[data-svright] .svact {
  display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  font-family: 'Bricolage Grotesque', sans-serif; font-weight: 800; font-size: 13.5px;
  text-decoration: none; white-space: nowrap; cursor: pointer;
  color: var(--ink, #191410); background: #fff;
  border: 2.5px solid var(--ink, #191410); border-radius: 999px;
  padding: 8px 15px; min-height: 44px; box-shadow: 0 4px 0 var(--ink, #191410);
  transition: transform .14s cubic-bezier(.34, 1.56, .64, 1), box-shadow .14s;
}
[data-svright] .svact:hover { transform: translateY(-3px); box-shadow: 0 7px 0 var(--ink, #191410); }
[data-svright] .svact:active { transform: translateY(2px); box-shadow: 0 2px 0 var(--ink, #191410); }
[data-svright] .svact.red { background: var(--chili, #D62828); color: #fff; }
[data-svright] .svact.topup { background: var(--cream, #FFF6E8); }
/* ── THE GREEN ONE, AND IT IS THE ONLY GREEN ON THIS BAR ────────────────────
 * Arbiter, 2026-09-03: a word and a box, beside Campaign, for the merchant who
 * still has his first film. Green because the two colours already on this bar
 * mean something — red is the primary action, cream is the wallet — and a third
 * pill wearing either would read as a second one of those.
 * --pandan IS THE BRAND'S GREEN (tokens.css), not a hex chosen here: this file
 * declaring a tenth colour is the exact disease docs/UX-AUDIT-2.0.md §F1
 * measured and tokens.css exists to end. White type on it is 5.28:1, which
 * clears AA and is what qa/gate-a11y.mjs measures on every page carrying it.
 * The fallback is the token's own value for the same reason every other
 * fallback in this file carries one — a browser that cannot resolve the
 * variable gets the brand colour, not a browser default. */
[data-svright] .svact.free { background: var(--pandan, #1F7A54); color: #fff; }
/* The chip is round-ish and carries a count rather than a word — 正本 `.chip2`.
   Its number is BUILT, never hidden: display:none still leaves the figure in the
   accessibility tree, so "he has nothing waiting" would be true on screen and
   false to a screen reader. */
[data-svright] .svact.svcart { padding: 8px 13px; gap: 6px; font-size: 14px; }
[data-svright] .svact .svic { flex: 0 0 auto; }
/* BELOW 560 THE WORDS GO AND THE ICONS STAY. The label leaves the EYE and not
   the accessibility tree — a display:none here would leave two controls named
   nothing at all, which is what .svback's own comment warned about before it
   was deleted.
   HALF OF THIS IS THE 正本'S AND HALF IS NOT, and the sentence that used to be
   here claimed all of it. Driven at 390/360/320, the 正本 does two different
   things: `.btn.topup .lbl{display:none}` at ≤820 — words go, icon stays, which
   is what this rule copies — and `#btnPrices{display:none}` at ≤620, which
   removes + CAMPAIGN ENTIRELY. Its mobile bar is three controls, not four.
   KEEPING IT IS A DELIBERATE DEPARTURE: starting a campaign is the one thing
   this product exists for and the phone is where the founder actually builds,
   so the primary action does not leave the bar on the screen it matters most
   on. Measured after the language pill moved into the menu, all four fit 320
   with room — see the 380 step below. */
@media (max-width: 560px) {
  [data-svright] .svact { padding: 8px 11px; gap: 0; }
  [data-svright] .svact .svlb {
    position: absolute; width: 1px; height: 1px; margin: -1px;
    overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
  }
}

/* ── + CAMPAIGN LEAVES THE BAR ON A PHONE — Arbiter, 2026-08-25 ─────────────
 *
 * THIS REVERSES THE PARAGRAPH ABOVE, and the paragraph above was wrong for a
 * reason worth keeping rather than deleting. It argued that the primary action
 * must not leave the bar on the screen it matters most on, and it kept the
 * button while the rule right above it hid every action's WORDS at 560. The `+`
 * had already been taken off this one control (head.js, same day), so it was
 * the one action with no icon to fall back on: measured on the real bar at
 * 390px, it rendered as a red pill 26px wide with nothing written on it at all.
 * "Words go, icon stays" is a fine rule for a control that has an icon.
 *
 * SO THE 正本 WAS RIGHT AND WE WERE NOT. It does `#btnPrices{display:none}` at
 * ≤620 and shows three actions on a phone, and the reason it can is the reason
 * this can: the workspace itself carries a full-width red band with a large
 * "+ Campaign" in it, and THAT is the entry point on a phone. The bar was
 * offering a second door to a room he is already standing in front of.
 *
 * ≤620, NOT ≤560, and the seam between them is deliberate. From 561 to 620 this
 * button is gone while the other three still have their words — that is the
 * 正本's own arrangement, and qa/browser-head.mjs §Z measures 620 and 561 (the
 * tight end of that band, where the labels are longest and the room least) as
 * well as 560 and below.
 *
 * BY IDENTITY, NOT BY COLOUR — see head.js, where the attribute is set. */
/* ── AND THE MENU ROW THAT COVERS FOR IT IS THE EXACT COMPLEMENT ───────────
 * Arbiter, 2026-09-01: "remove My Campaign cuz this is already at the bar, it's
 * repeating." True above 620, where the pill is drawn — and false at or below,
 * where the rule under this comment takes the pill away and that row becomes the
 * only door to his workspace from any page in the product (it was deleted once
 * on the same argument, 2026-08-25, and had to come back the next day).
 *
 * ONE BREAKPOINT, TWO RULES, WRITTEN TOGETHER. Split across two files or two
 * numbers, the day somebody nudges one is the day a width exists that shows
 * NEITHER — and that width is a phone with no way home. */
.svmenu li.svphoneonly { display: none; }
@media (max-width: 620px) {
  [data-svright] .svact[data-svaction="campaign"] { display: none; }
  .svmenu li.svphoneonly { display: block; }
}

/* ── AND THE GREEN CHIP TAKES THE WIDTH CAMPAIGN JUST GAVE UP ───────────────
 * The paragraph at the top of this file that ends "the next control anyone adds
 * to this bar will hit the same wall" is right, and this is how this one gets
 * past it rather than around it: below 620 the Campaign pill leaves, so a bar
 * carrying the free-film chip is FOUR controls wide on a phone exactly as it
 * was before the chip existed. Measured at 390 / 360 / 320 in all three
 * languages by qa/browser-head.mjs §S, which fails on a sideways scroll.
 * IT IS THE ONE THAT STAYS, and the rule this file already uses decides it: the
 * action that stands down is the one with a second door that costs nothing.
 * Buy credits has the menu row, Campaign has the menu row this breakpoint puts
 * back — the free film has neither, and it is the one control on this bar that
 * expires. A merchant who never sees it never learns it was his. */


/* A PAGE HOSTING THE SLOT IN ITS OWN BAR (`data-svbar`).
 *
 * Six pages have a topbar that IS their identity — the block wordmark and their
 * own CTA — so they keep it and take only the account half. Their `.in` is
 * `justify-content: space-between` with two children; appending a third would
 * strand the CTA in the middle of the bar. Pinning only the FIRST child left and
 * letting the rest pack right keeps every one of them reading the way it did,
 * with the avatar on the end. One rule, because six copies of it is six chances
 * to drift.
 *
 * EVERYTHING head.js BUILDS IS UNSCOPED, AND THAT IS A RULE RATHER THAN AN
 * OBSERVATION. .svavatar, .svmenuwrap and .svmenu were already; the two rules
 * below were scoped to .svbar and had to learn the second home; and v18 added
 * three more — + Campaign, Buy credits and the cart chip — which were scoped to
 * .svbar and did NOT learn it, which is the defect all three reviewers of
 * 2026-08-25 opened with. They are scoped `[data-svright]` now: the slot head.js
 * actually draws into, and the one thing both bar flavours share.
 * ANYTHING ADDED HERE LATER GOES THE SAME WAY. A rule that names `.svbar` is a
 * rule that works on twelve pages out of eighteen. */
[data-svbar] .in { display: flex; align-items: center; gap: 10px; }
[data-svbar] .in > :first-child { margin-right: auto; min-width: 0; }
[data-svbar] .svright { display: flex; align-items: center; gap: 10px; flex: 0 0 auto; }

/* THE LANGUAGE SWITCH — built by public/shared/i18n.js, styled here.
 *
 * A SEGMENTED PAIR, not a dropdown. There are two languages; a <select> would
 * make the merchant open a thing to discover that, and on a phone it opens a
 * native sheet over the page. Two buttons say what the choice is by existing.
 *
 * aria-pressed CARRIES THE STATE, and the CSS reads it rather than a class of its
 * own: one source for "which language am I in", and a screen reader hears it for
 * free. A class would be a second copy of the same fact.
 *
 * Small on purpose. This is a control a merchant uses once, ever, and it sits
 * beside the thing he came to the bar for. */
/* ONE BUTTON, and its width is the reason — see markSwitch() in i18n.js. The
   two-button version put .svright at 247px and /diy scrolled sideways at 390px.
   It must NOT be inside the nav: a merchant who cannot read the page cannot find
   a control he has to scroll to reach. */
.svlang {
  font-family: 'DM Sans', sans-serif; font-weight: 800; font-size: 11.5px;
  flex: 0 0 auto; border: 2px solid var(--ink, #191410); border-radius: 999px;
  background: #fff; color: var(--ink, #191410); cursor: pointer;
  padding: 7px 10px; min-height: 34px; line-height: 1; white-space: nowrap;
}
.svlang:hover { background: var(--cream, #FFF6E8); }
@media (max-width: 560px) { .svlang { padding: 6px 8px; font-size: 11px; min-height: 32px; } }
[data-svbar] .svin-link {
  font-weight: 800; font-size: 14px; text-decoration: none; color: var(--ink, #191410);
  border: 2.5px solid var(--ink, #191410); border-radius: 999px; background: #fff;
  padding: 8px 15px; min-height: 40px; display: flex; align-items: center;
}

/* The page's own badge, beside the account slot rather than inside it — head.js
   empties the slot when it draws the menu, and "DIY · FREE" is not ours to
   empty. That word is what keeps the free road legible on the page that earns
   money without an account. On a narrow phone the badge goes before the nav
   does: the avatar is the thing he came to the bar for. */
.svbar .svbadge {
  font-family: 'Bricolage Grotesque', sans-serif; font-weight: 800; font-size: 12px;
  border: 2px solid var(--ink, #191410); border-radius: 999px; padding: 6px 12px;
  background: var(--pandan, #1F7A54); color: #fff; white-space: nowrap;
}
/* It SHRINKS on a phone, it does not disappear. Hiding it on the smallest
   screen would be backwards: the phone is where the merchant actually builds,
   and "FREE" is the sentence that road is selling. */
@media (max-width: 620px) { .svbar .svbadge { font-size: 10.5px; padding: 5px 9px; } }
/* EXCEPT FOR THE ONE PERSON IT IS NOT ADDRESSED TO. Measured on /diy at 390px
   after v18 filled the right-hand side: block 94 + badge + four actions came to
   426, and the avatar sat from x=386 to x=426 — half of it off the screen, and
   Playwright could not click it because a merchant could not tap it. The
   sentence above stays true for the audience it was written for; "DIY · FREE"
   argues the free road to somebody who has not signed up, and a merchant who is
   signed in has already taken it. So it goes only once the bar knows who he is,
   which is also the only state in which there is anything else competing for the
   room. /m's badge is the same shape and follows the same rule. */
[data-svin] .svbadge { display: none; }

/* The signed-out state. AN INVITATION, NOT A GATE — see head.js. Ghost
   styling on purpose: a red primary button here would read as "you must",
   and the road this bar sits on earns money without an account. */
.svbar .svin-link {
  font-weight: 800; font-size: 14px; text-decoration: none; color: var(--ink, #191410);
  border: 2.5px solid var(--ink, #191410); border-radius: 999px; background: #fff;
  padding: 8px 15px; min-height: 40px; display: flex; align-items: center;
}

/* ══ THE WAY BACK TO A HALF-BUILT THING ═══════════════════════════════════
   Arbiter, 2026-09-01: a merchant halfway through a video in /viral/build or
   /viral/studio taps a row of the account menu, the page navigates, and the
   build is behind him with nothing on screen pointing back at it. head.js
   remembers the page he left and draws this chip top-left on the one he lands
   on. Its full argument — including why the three action pills are excluded —
   is in head.js under the same heading.

   GHOST, NOT RED, AND SMALLER THAN THE WORDMARK BESIDE IT. It is an offer to
   go back, not the errand he came here for: the reason he tapped the row is
   still the most important thing on the page, and a red pill at the top-left
   would out-shout it.

   min-width: 0 AND A TRUNCATING LABEL because this bar has no slack. Everything
   in .svin except .sp is a fixed pill; a chip that could not shrink would push
   the whole row wider on the first narrow screen. */
.svbar .svret, [data-svbar] .svret {
  display: inline-flex; align-items: center; gap: 6px; flex: 0 1 auto; min-width: 0;
  font-weight: 800; font-size: 13px; text-decoration: none; color: var(--ink, #191410);
  border: 2.5px solid var(--ink, #191410); border-radius: 999px; background: #fff;
  padding: 7px 13px; min-height: 36px;
}
.svbar .svret:hover, [data-svbar] .svret:hover { background: var(--cream, #FFF6E8); }
.svret .svretar { font-size: 15px; line-height: 1; }
.svret .svretw { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* THE WORDS GO, THE CONTROL STAYS. Measured in head.js's own note below: the
   signed-in bar is four fixed pills and already overran 360px by 9px before
   anything was added on the left. An arrow alone is still the control every
   phone reads as back, and the link keeps the whole sentence as its accessible
   name — see head.js, where aria-label is set from the same key. */
@media (max-width: 560px) {
  .svret .svretw { display: none; }
  .svbar .svret, [data-svbar] .svret { padding: 7px 10px; }
}

/* The avatar button. A real <button> with aria-expanded — the menu has to be
   reachable and dismissable from a keyboard, and qa/gate-a11y.mjs is an axe
   run on every page this lands on. */
.svavatar {
  /* 56px AND A 14px RING — 正本 :`.avatar.gauge`, and the Arbiter's own words on
     2026-08-25: "image 1 most right is the profile size and the color circle
     wide i want". It shipped at 40px with a 3px ring, which is where "小" and
     "细" came from: the ring was almost five times too thin to read as a gauge
     at all, so the one control that reports his balance reported it in a hairline.

     6px SINCE 2026-08-26, AND THE 56 IS UNCHANGED. Arbiter, shown four weights
     side by side at true size and asked to pick: "D" — the thinnest. The ring
     above stays a gauge, it just stops being the loudest thing on the control;
     the face it gives back goes from 23px to 39px, which is what "the profile
     icon bigger" asked for. ONE NUMBER DID BOTH, because the ring is padding:
     there is no separate size to grow.

     THE COLOUR IS NOT COPIED FROM THE 正本 AND THAT IS DELIBERATE. Its ring is
     green because it is decoration; this one is a credit gauge, and green /
     amber / red is it SAYING something about his money. Same geometry, different
     job. */
  width: 56px; height: 56px; border-radius: 999px; border: 2.5px solid var(--ink, #191410);
  background: #fff; cursor: pointer; overflow: hidden;
  display: flex; align-items: center; justify-content: center; flex: 0 0 auto;
  /* ── THE THICKNESS IS DECIDED HERE, ONCE, AND NOWHERE ELSE ────────────────
     The ring IS this padding: the face sits inside it and the button's own
     background — plain or an arc — shows through all the way round.

     head.js USED TO CARRY `RING.width: 3` BESIDE IT, and both files' comments
     said the two had to stay equal. Measured before this change: that constant
     had ZERO readers — the only other mentions of it in the repo were the two
     comments claiming the coupling. So it was not a second source of truth, it
     was a decoy that would have sent the next person to change the number in
     the file that does not draw anything. Deleted there, named here, with the
     custom property so that anything which really does need it can read it off
     the element rather than restate it. */
  --svring-w: 6px;
  padding: var(--svring-w);
  /* THE FACE IS WHATEVER THE RING GIVES BACK, NAMED so the letter can be sized
     from it instead of from a second copy of this arithmetic. Every page sets
     `box-sizing: border-box`, so the 56 already includes the 2.5px border on
     each side: 56 − 5 − two rings. At 6px that is a 39px face, against 23px
     when the ring was 14. */
  --svface: calc(56px - 5px - 2 * var(--svring-w));

  /* ── IT ANSWERS THE FINGER, AND IT DID NOT BEFORE ────────────────────────
     A real <button> that gives nothing back when pressed reads as a picture of
     a button. Press: the face sinks and the whole control shrinks a hair, so
     the ring reads THICKER under the thumb — the gauge is padding, so taking
     size off the button takes it off the face first. Release: back out, with
     the overshoot in the curve rather than in a keyframe, which is why the
     resting transition is a back-ease and the pressed one is not. An overshoot
     on the way DOWN would be the control bouncing away from his finger.

     THE ARC'S TRANSITION LIVES HERE TOO, in the same declaration, because
     `transition` is one property and a second rule naming it on a more specific
     selector silently drops whatever the first one said. That is how the press
     would have quietly cancelled the growth. */
  transition: transform 220ms cubic-bezier(.34, 1.56, .64, 1),
              --svring-deg 720ms cubic-bezier(.05, .7, .1, 1);
}
.svavatar .svface { transition: transform 220ms cubic-bezier(.34, 1.56, .64, 1); }
.svavatar:hover { transform: translateY(-1px); }
.svavatar:active { transform: scale(.965); transition: transform 130ms cubic-bezier(.2, 0, 0, 1); }
.svavatar:active .svface { transform: scale(.94); transition-duration: 130ms; }

/* ── THE ARC GROWS TO WHERE HE IS ──────────────────────────────────────────
   @property is what makes a conic-gradient animatable at all: to the engine an
   unregistered custom property is a STRING, and a string cannot be
   interpolated, so the transition above without this registration is a jump
   with extra words. Registered, the same declaration draws the month — 0° on
   the first paint up to today's reading — and afterwards carries the arc
   between two real readings instead of teleporting.

   `inherits: false` because the arc is painted on the button's own background
   and nothing below it reads the angle. Where a browser has no @property the
   arc simply appears at its value, which is exactly what shipped before this:
   the drawing is lost, the reading is not. */
@property --svring-deg { syntax: "<angle>"; inherits: false; initial-value: 0deg; }

/* ── THE LAST STRETCH ──────────────────────────────────────────────────────
   TWICE, THEN SILENCE. A ring that pulses forever is a ring nobody looks at;
   this fires when the arc crosses into red and then stops, so motion on this
   control always means something new. head.js writes the attribute once and
   takes it off when he leaves the band — see paintRing(), which also explains
   why it is not written on every repaint.

   THE COLOUR IS THE BAND'S OWN, mixed down to a wash rather than restated as
   an rgba() nobody would think to update the day chili moves. */
@keyframes svring-beat {
  0%   { box-shadow: 0 0 0 0 color-mix(in srgb, var(--chili, #D62828) 34%, transparent); }
  55%  { box-shadow: 0 0 0 8px color-mix(in srgb, var(--chili, #D62828) 0%, transparent); }
  100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--chili, #D62828) 0%, transparent); }
}
.svavatar[data-svring-hot] { animation: svring-beat 1100ms cubic-bezier(.37, 0, .63, 1) 2; }
/* ── THE CREDIT RING ────────────────────────────────────────────────────────
   The avatar reports the merchant's balance as a filled arc: `credits ÷ denom`,
   green through amber to red. public/shared/head.js owns the arithmetic and
   hands this stylesheet two custom properties; the shape — including how thick
   the arc is — lives here and only here. See --svring-w above.

   TWO STATES, ONE DIAMETER, AND THAT IS THE WHOLE REASON THE FALLBACK IS A RULE
   RATHER THAN AN ABSENCE. `data-svring="off"` is what head.js sets when there
   is nothing honest to measure — no plan, no recorded top-up, or a balance that
   has not loaded — and it paints the SAME ring in a flat colour. An avatar that
   changed size or thickness the moment a merchant's first top-up landed would
   read as a glitch on the control he taps most, and an EMPTY arc would read as
   "you have run out", which is a sentence about his money that we would be
   making up. */
.svavatar[data-svring="off"] { background: var(--ink, #191410); }
.svavatar[data-svring="on"] {
  /* Two stops, one angle: filled to --svring-deg, track the rest of the way.
     `0` as the track's start means "wherever the last stop ended", so there is
     no second copy of the angle to fall out of step.

     ── THE TRACK HAS TO BE VISIBLE, AND FOR TWO DAYS IT WAS NOT ──────────────
     It was --cream, on a bar that is white. Measured: 1.07 against the bar
     behind it, where 1.0 is the same colour. That was survivable while the arc
     drained — a merchant saw an almost-full ring on his first day and the track
     was a sliver nobody needed to find. Filling changes who sees it: an empty
     gauge is now what EVERY new merchant looks at, and the Arbiter looked at his
     and said the circle did not come out.

     WARM GREY OUT OF --dim, NOT A NEW COLOUR AND NOT WCAG'S 3:1. The track is
     not what carries the reading — the arc's colour does, and the button's
     aria-label says it in words for anyone who cannot see either. What the
     track has to do is prove the gauge EXISTS at zero, and a mid grey dark
     enough for 3:1 stops being a track and starts being a second arc. This is
     2.08 against white: plainly there, still behind. */
  background: conic-gradient(
    var(--svring-col, var(--pandan, #1F7A54)) var(--svring-deg, 0deg),
    color-mix(in srgb, var(--dim, #6B6156) 45%, var(--cream, #FFF6E8)) 0
  );
}
/* THE FACE — the white disc the logo or the initial sits on, covering the
   middle of the arc so only the rim shows. */
.svavatar .svface {
  width: 100%; height: 100%; border-radius: 999px; overflow: hidden; background: #fff;
  display: flex; align-items: center; justify-content: center;
}
.svavatar img { width: 100%; height: 100%; object-fit: contain; display: block; }
/* No logo yet: his initial on the brand colour. Not a blank circle and not a
   broken image — both of those read as "something went wrong". */
.svavatar .svinitial {
  font-family: 'Bricolage Grotesque', sans-serif; font-weight: 800;
  /* SIZED FROM THE FACE, NOT TYPED. It was a flat 16px, which was right for the
     23px face the 14px ring left behind and would be lost in a 39px one. 0.696
     is that same proportion kept — 16 ÷ 23 — so the letter follows --svring-w
     rather than needing a second edit nobody would remember to make. That is
     the exact trap the deleted `RING.width` constant used to set. */
  font-size: calc(var(--svface, 39px) * 0.696); color: #fff; background: var(--chili, #D62828);
  width: 100%; height: 100%; display: flex; align-items: center; justify-content: center;
}

.svmenuwrap { position: relative; }
.svmenu {
  position: absolute; right: 0; top: calc(100% + 9px); z-index: 80;
  min-width: 232px; background: #fff; color: var(--ink, #191410);
  border: 2.5px solid var(--ink, #191410); border-radius: 18px;
  box-shadow: 0 7px 0 var(--ink, #191410); padding: 8px; list-style: none;
}
.svmenu[hidden] { display: none; }
.svmenu li { list-style: none; }
.svmenu a, .svmenu button {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  width: 100%; text-align: left; background: none; border: 0; cursor: pointer;
  font-family: inherit; font-size: 14.5px; font-weight: 700; color: var(--ink, #191410);
  padding: 11px 12px; border-radius: 12px; text-decoration: none; min-height: 44px;
}
.svmenu a:hover, .svmenu button:hover { background: var(--cream, #FFF6E8); }
.svmenu .svsep { height: 2.5px; background: var(--ink, #191410); opacity: .12; margin: 6px 4px; }

/* THE ROWS CARRY ICONS, and they are the product's first — docs/HANDOFF.md
   §9.1a records that as a decision. The measurements here are the approved
   prototype's own (docs/prototype/index.html:174-176): a 20px glyph in --dim, a
   label that takes the rest of the row, and a trailing value in --dim at 12px.
   The row was already laid out for this — justify-content: space-between with
   nothing on the right — which is what made the gap so easy to miss. */
.svmenu .svic { flex: 0 0 auto; color: var(--dim, #6B6156); }
.svmenu .svlb { flex: 1 1 auto; }
/* The trailing count. 12px/700 in --dim is the prototype's own
   (docs/prototype/index.html:176). :empty is what makes "we do not know" render
   as nothing at all rather than as a gap where a number should be. */
/* ── THE SHELF, AS BLOCKS ───────────────────────────────────────────────────
   One block per slot he is allowed, filled for the ones in use. `:empty` is
   still what hides it, and it still means the same thing — head.js writes no
   children when it could not ask — so "we do not know" stays silent rather than
   drawing three empty blocks, which would say he owns none. The `font-size` and
   `font-weight` stay for the fallback reading past SLOT_BLOCKS_MAX. */
.svmenu .svvl { flex: 0 0 auto; font-size: 12px; font-weight: 700; color: var(--dim, #6B6156); }
.svmenu .svvl:empty { display: none; }
/* THE BLOCKS ARE INLINE, AND .svvl IS NOT TOUCHED. This span is shared: the
   language control in public/shared/i18n.js builds its own row out of head.css's
   parts — .svic, .svlb, .svvl — and puts the current language's NAME in one of
   these. Turning .svvl into a flex container to space the blocks would have
   re-laid-out that text too, for a row that has no blocks in it and no reason to
   move. So the spacing lives on the blocks: inline-block for the size to apply
   at all, and the margin on the sibling combinator so a lone block has none. */
.svmenu .svvl .svblk {
  display: inline-block; vertical-align: middle;
  width: 9px; height: 14px; border-radius: 3px;
  border: 2px solid var(--ink, #191410); background: var(--cream, #FFF6E8);
}
.svmenu .svvl .svblk + .svblk { margin-left: 3px; }
.svmenu .svvl .svblk.on { background: var(--pandan, #1F7A54); }

/* THE CREDIT LINE IS A DOOR NOW, and this comment is the corrected version of
   the one that used to sit here. It said the line is not a button because "a
   reading that pretends to be tappable is a lie about what happens next" — and
   that was right about a reading that goes NOWHERE. It goes to the place the
   number is changed, which is the question a merchant is asking when he reaches
   for it. The prototype points its credits block and its Plan/Credit row at one
   address for the same reason.
   Left as its own rule rather than folded into .svmenu a: this row is a big
   number and a small label sharing a baseline, and the generic row is a
   centred line of 14.5px text. The arrow opts back into centre on its own. */
.svmenu .svcredit {
  display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
  /* 6px AT THE BOTTOM, NOT 12: the usage bar below is what closes this block
     now, and it carries the 12. Left at 12 the number and the bar about it sat
     24px apart and read as two unrelated things. */
  padding: 10px 12px 6px;
}
/* ── THE USAGE BAR — Arbiter, 2026-08-29 ─────────────────────────────────────
   "make a bar under the credit, with credit usage. (Green > Yellow > Orange >
   Red) from left to right."

   THE SHAPE IS HERE, THE READING IS NOT. head.js's paintUse() writes the width
   and the fill colour and nothing else; this file never learns what 50% means.
   That division is the same one .svavatar's ring already keeps, and it is what
   stops the ramp existing twice — a `linear-gradient(green, yellow, orange,
   red)` written here would be a second copy of RING's three thresholds in a
   file that cannot be told when they move. There is one ramp, in ringColour().

   THE TRACK IS THE SAME OBJECT AS THE SLOT BLOCKS — 2px ink border, cream
   inside, fully rounded — because they are the same kind of reading eight rows
   apart, and two different treatments of "how much of N" in one menu is two
   things to learn instead of one. */
.svmenu .svuse { display: block; margin: 0 12px 12px; }
/* ABSENT, NOT EMPTY. head.js sets "off" when there is no denominator or the
   balance has not landed — and an empty track drawn on either says "you have
   spent nothing" on the evidence of never having asked. Same rule as
   .svvl:empty above and the credit figure's em-dash. */
.svmenu .svuse[data-svuse="off"] { display: none; }
.svmenu .svuse {
  height: 12px; border-radius: 999px;
  border: 2px solid var(--ink, #191410);
  background: var(--cream, #FFF6E8);
  overflow: hidden;
}
.svmenu .svuse .svusef {
  display: block; height: 100%; width: 0;
  background: var(--pandan, #1F7A54);
  transition: width .35s ease, background-color .35s ease;
}
/* A BAR THAT GROWS IS A BAR THAT MOVED WHILE HE WAS READING IT. The width is
   the only animated thing in this menu; anyone who has asked not to be moved
   gets the same bar, drawn. */
@media (prefers-reduced-motion: reduce) {
  .svmenu .svuse .svusef { transition: none; }
}
.svmenu .svcredit .svic { align-self: center; }
.svmenu .svcredit b {
  font-family: 'Bricolage Grotesque', sans-serif; font-weight: 800; font-size: 22px;
}
.svmenu .svcredit span { font-size: 11.5px; font-weight: 700; color: var(--dim, #6B6156); letter-spacing: .06em; }
/* THE SHOP, THEN THE ADDRESS — 正本 :1704-1707. This was one line of dim grey
   holding the raw email and nothing else; the name a merchant gave his own shop
   was not on the screen anywhere in the chrome. Two weights, and only the
   address gets break-all: a shop name broken mid-word reads as a rendering
   fault, and a long email with nowhere to break pushes the menu wider. */
.svmenu .svwho { padding: 2px 12px 8px; }
.svmenu .svwho b {
  display: block; font-family: 'Bricolage Grotesque', sans-serif; font-weight: 800;
  font-size: 14.5px; color: var(--ink, #191410);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.svmenu .svwho span {
  display: block; font-size: 12.5px; color: var(--dim, #6B6156); word-break: break-all;
}
/* Empty unless a sign-out failed. :empty keeps it from reserving space for a
   message that is not there — a menu with a permanent gap in it looks broken. */
.svmenu .svouterr { padding: 2px 12px 8px; font-size: 12.5px; color: var(--chili, #D62828); font-weight: 700; }
.svmenu .svouterr:empty { display: none; }

@media (max-width: 560px) {
  .svbar .svin { padding: 10px 14px; gap: 10px; }
}

/* THE NARROWEST PHONES GET TIGHTER CHROME, and this step exists because measuring
 * said 391px against a 390px screen — one pixel over, with the nav already
 * shrunk to nothing and the language toggle already halved.
 *
 * A ONE-PIXEL FIX WOULD HAVE BEEN A LIE. The bar was at its exact limit: a longer
 * wordmark, a wider sign-in word in any other language, a fatter default font on
 * some Android build, and /diy scrolls sideways again for the next person to
 * discover. This buys ~14px of headroom instead, which is the difference between
 * "passes today" and "fits".
 *
 * 420px rather than 560px on purpose: the 560 step above is for the nav's text,
 * and squeezing the sign-in pill on a 540px phone that has room for it would be
 * cramping a screen to fix a different screen's problem.
 *
 * ── AND THEN THE WIDER SIGN-IN WORD ARRIVED, exactly as the paragraph above
 *    said it would. docs/HANDOFF.md §14 recorded /diy overflowing 8px at 360.
 *    That was the ENGLISH number. Measured across all three languages
 *    (7️⃣, 2026-08-23), Bahasa Melayu's "Log masuk" is 95px against "Sign in"'s
 *    69px, and the real figures are:
 *
 *      /diy @360    en +8    ms +26
 *      /terms /privacy /refund /thanks @360    en fits    ms +13
 *
 *    FIVE PAGES, NOT ONE, AND FOUR OF THEM INVISIBLE IN ENGLISH. One language is
 *    not a measurement — it is a sample of one, and this bar carries three.
 *
 *    Sized by binary-searching the narrowest viewport the bar survives, in the
 *    WIDEST language, because a spare-pixels reading saturates: the middle of
 *    the bar is `flex: 1 1 auto` — .svnav then, `.sp` now — so whenever anything
 *    fits at all it expands and pins .svright against the padding, and every
 *    passing layout reads the same.
 *    Before: 386px. After: 348px. 360 therefore has 12px of real slack rather
 *    than the 0 it had.
 *
 * ── AND THEN THE BAR ITSELF CHANGED, 2026-08-25. Every figure above was
 *    measured against a signed-OUT bar: a wordmark, a nav that could shrink to
 *    nothing, and one Sign in pill. v18 gave the signed-IN bar four fixed pills
 *    and no shrinkable middle, and it overran by 9px at 360 and 17px at 320. The
 *    step below is what answers that, and the numbers above still describe the
 *    signed-out case, which is why they are still here.
 *
 *    THE WORDMARK IS SIZED FROM THE BRAND'S OWN FLOOR, not from taste:
 *    docs/BRAND-GUIDE.md:147 sets the on-screen minimum at 88px. 0.8rem renders
 *    it at 105px here, which keeps 17px in hand — and section S below asserts
 *    that floor by reading the number OUT OF the guide, so shrinking this
 *    further to buy room for some future control goes red instead of quietly
 *    breaking the mark.
 *
 *    STILL BROKEN AT 320px (old iPhone SE, old Androids): -66 before, ~-28
 *    after. That is not a padding problem — the bar cannot hold a wordmark, a
 *    language switch, a badge and a sign-in word at 320. Recorded as debt in
 *    BUILD-STATUS.md rather than papered over here. */
@media (max-width: 380px) {
  .svbar .svin { padding: 10px 12px; gap: 6px; }
  .svbar .svin-link, [data-svbar] .svin-link { padding: 8px 11px; font-size: 13px; }
  /* ── THE FOURTH ACTION STANDS DOWN, AND IT IS THE ONE WITH ANOTHER DOOR ──
   * Measured, not guessed: at 360 the bar overran by 9px and at 320 by 17, with
   * every control already collapsed to its icon. Nothing here had slack — the
   * bar this replaced fitted 320 because `.svnav` was `flex:1 1 auto;
   * min-width:0` and could shrink to nothing; four fixed pills cannot.
   *
   * WHAT MAY NOT YIELD, AND WHY IT IS NOT THE BLOCK. docs/BRAND-GUIDE.md:147
   * puts the wordmark's on-screen floor at 88px and §S in qa/browser-head.mjs
   * reads that number out of the guide; an earlier cut of this rule shrank the
   * block to 84 and 76, which is through the floor rather than near it. The
   * language switch may not go either — head.css argues that one at length
   * below: a merchant who cannot read the page cannot find a control he has to
   * hunt for.
   *
   * SO BUY CREDITS LEAVES THE BAR, AND ONLY THE BAR. It is the one action of
   * the four with a second door that costs nothing: buildMenu() carries the row
   * again (it was there until this round moved it up here), so the address is
   * always one tap away and never disappears. + Campaign, the cart and the
   * avatar are each the only way to what they open.
   *
   * ── AND THEN IT DID NOT HAVE TO. The Arbiter's own measurement of the 正本
   *    (2026-08-25) put the language switch inside the account menu, and moving
   *    it there took ~46px off the signed-in bar — more than the 17px that was
   *    over at 320. All four actions fit at all three widths, so nothing stands
   *    down and the rule that used to be here is gone rather than kept "just in
   *    case". The paragraphs above are left standing because they are the record
   *    of what the constraint actually was: four fixed pills and a block at its
   *    brand floor have no slack, and the next control anyone adds to this bar
   *    will hit the same wall. */
  /* BOTH BAR FLAVOURS. The legal pages and /thanks host the slot in their own
     topbar (`[data-svbar]`), so their wordmark is theirs and the rules above do
     nothing for them — .svright is the only part of those bars this file owns,
     and it is the part that was overflowing. */
  .svbar .svright, [data-svbar] .svright, [data-svright] { gap: 6px; }
}

/* ── 320px, BECAUSE THE ARBITER SAID IT IS SUPPORTED (2026-08-23) ────────────
 *
 * The step above fits 360 with 12px in hand and still left 320 short by 28px in
 * Bahasa Melayu. This is the third and last step, and it engages below 360 so a
 * phone that already had room keeps it.
 *
 * WHAT YIELDS, AND WHAT DOES NOT. The badge is "the sentence that road is
 * selling" and shrinks rather than disappearing; the sign-in link is "an
 * invitation, not a gate"; the language switch may not be hidden or moved,
 * because "a merchant who cannot read the page cannot find a control he has to
 * scroll to reach". Everything that stays gives up horizontal padding and the
 * gaps tighten. ONE THING IS HIDDEN — Buy credits, one step up, and only
 * because buildMenu() carries the same address as a permanent row.
 *
 * THE WORDMARK STOPS AT 92px, NOT AT 88. docs/BRAND-GUIDE.md:147 sets the
 * on-screen minimum at 88px, and sitting exactly on a floor is how the next
 * squeeze goes through it. Four pixels of daylight is the difference between a
 * limit and a limit that has already been spent — §S in qa/browser-head.mjs
 * reads that 88 out of the guide and fails if this ever crosses it. The block
 * spends PADDING to get there, never type size: shrinking the letters is what
 * takes a wordmark through its floor.
 *
 * Measured by binary-searching the narrowest surviving viewport in the widest
 * language: 348px before this block, 312px after. 320 therefore has 8px of real
 * slack. Tap targets are untouched — sign-in stays 40px tall, the switch 32px.
 *
 * 340px rather than 360px: 360 already fits with room, and squeezing a phone
 * that has room to fix a narrower one's problem is the mistake the 420 step
 * above already names. */
@media (max-width: 340px) {
  .svbar .svin { gap: 4px; padding-left: 10px; padding-right: 10px; }
  .svbar .svright, [data-svbar] .svright, [data-svright] { gap: 4px; }
  /* THE BLOCK SPENDS PADDING, NEVER TYPE. Shrinking the letters is what takes
     the mark through docs/BRAND-GUIDE.md:147's 88px floor; 9px of side padding
     lands it at 92, which is the four pixels of daylight the paragraph above
     asks for rather than a limit already spent. */
  .svbar .block { padding: 7px 9px 9px; min-width: 92px; }
  [data-svright] .svact { padding: 8px 9px; }
  [data-svright] .svact.svcart { padding: 8px 9px; gap: 5px; }
  .svbar .svin-link, [data-svbar] .svin-link { padding: 8px 8px; }
  .svbar .svbadge { padding: 5px 7px; font-size: 10px; }
  /* ── BUY CREDITS LEAVES THE LANDING BAR, AND ONLY THE LANDING BAR ─────────
   * MEASURED, 2026-08-26: with the landing page opting into the whole workspace
   * chrome (head.js, `data-svfull`), its bar came to 341px against 320 —
   * 21px over, with every action already collapsed to its icon by the ≤560
   * rule and the Campaign pill already gone by the ≤620 one. 390 and 360 both
   * fit; only 320 does not.
   *
   * WHICH ONE YIELDS IS NOT A NEW ARGUMENT. The ≤380 block above already made
   * it, in this file, for this bar: the action that stands down is the one with
   * a second door that costs nothing. Buy credits opens /app#hCredits, and the
   * account menu behind the avatar — on this page as on every other — carries
   * Plan / Credit onto the same wallet, permanently. The cart and the avatar
   * are each the only way to what they open, and the block is at its brand
   * floor.
   *
   * SCOPED TO THE SLOT THAT ASKED FOR THE CHROME, so the twelve workspace bars
   * are untouched: they fit 320 today and squeezing a bar that has room to fix
   * a different bar's problem is the mistake the 420 step above names by name.
   * Measured after: 303 against 320. */
  [data-svright][data-svfull] .svact.topup { display: none; }
}

/* ── EVERY MOTION ON THIS BAR IS OPTIONAL ───────────────────────────────────
   Each one above lands on the state it was animating toward, so switching them
   off costs a merchant nothing but the drawing: the arc is still at his
   reading, the button is still pressed, the red band is still red. A merchant
   who has asked his phone to stop moving things has asked this bar too. */
@media (prefers-reduced-motion: reduce) {
  .svavatar,
  .svavatar .svface,
  .svavatar:active { transition: none; }
  .svavatar[data-svring-hot] { animation: none; }
}
