/* transformer-twin-lab: shared styles for the demo pages.
   Three skins (grey is the default, light and dark stay switchable), one accent
   colour. The ladders and the switch mechanics follow the conventions of the
   author's personal site, trimmed to what these pages actually use.

   THE ONE KNOB: the accent of the default (grey) skin lives in --accent below.
   Swap that single value (e.g. to an orange) and the segmented controls, form
   focus rings, links and every canvas contour follow, because the canvases
   re-read the computed palette at runtime. */

/* The page background, readable from inside a button (Pico redefines
   --pico-background-color on buttons, so an alias declared on html is needed). */
html {
  --page-bg: var(--pico-background-color);
}

/* === DARK THEME (cool slate + steel blue) === */
html[data-theme="dark"] {
  --pico-background-color: #14181f;
  --pico-color: #dde5ee;
  --pico-primary: #7d9bb8;
  --pico-primary-hover: #9db8d2;
  --pico-muted-color: #74808f;
  --pico-muted-border-color: #262e3a;
  --pico-card-background-color: #1a202a;

  --tool-emph:     #e1e8f0;
  --tool-text:     #bcc5cf;
  --tool-label:    #99a4af;
  --tool-note:     #778390;
  --tool-faint:    #566472;
  /* Not the fourth rung of the even ladder any more, but solved from the
     contrast requirement (2026-09-17, taken over from the author's site, where
     the same round happened on 2026-09-06). --tool-border outlines frames, the
     segmented controls, the buttons and, since this change, Pico's form fields.
     Those are user interface components, and WCAG 1.4.11 asks for 3:1 against
     every adjacent surface. The old #374656 sat at 1.65 to 1.84 against the
     backgrounds used here and was invisible on a beamer; #68798a holds 3.56 to
     3.97 against the page, the card, --tool-surface, --tool-panel and
     --tool-subtle. */
  --tool-border:   #68798a;
  --tool-hairline: #1a2a3a;
  --tool-grid:     #45596d;
  --tool-ink:      #0a1928;
  --tool-panel:    #0a1928;
  --tool-surface:  #111c28;
  --tool-subtle:   #17232f;
  --tool-shadow:   rgba(0, 0, 0, .45);
  --tool-panel-shadow: 0 12px 28px var(--tool-shadow);
  --tool-sig-good: #4ade80;
  --tool-sig-warn: #facc15;
  --tool-sig-bad:  #f87171;
  --tool-sig-info: #38bdf8;
}

/* === LIGHT THEME === */
html[data-theme="light"] {
  --pico-background-color: #ffffff;
  --pico-color: #1b2430;
  --pico-primary: #3a6a96;
  --pico-primary-hover: #27506f;
  --pico-muted-color: #5d6b7c;
  --pico-muted-border-color: #d8e0ea;
  --pico-card-background-color: #ffffff;

  --tool-emph:     #0c1c2b;
  --tool-text:     #293746;
  --tool-label:    #485563;
  --tool-note:     #687481;
  --tool-faint:    #8b96a1;
  /* See the dark skin: solved for 3:1, not a rung of the ladder. The old
     #afb8c1 sat at 1.80 to 2.01, #838b94 holds 3.09 to 3.45. */
  --tool-border:   #838b94;
  --tool-hairline: #d5dce3;
  --tool-grid:     #d5dce3;
  --tool-ink:      #ecf3fb;
  --tool-panel:    #ecf3fb;
  --tool-surface:  #ffffff;
  --tool-subtle:   #f1f5f9;
  --tool-shadow:   transparent;
  --tool-panel-shadow: var(--pico-box-shadow);
  --tool-sig-good: #15803d;
  --tool-sig-warn: #a16207;
  --tool-sig-bad:  #b91c1c;
  --tool-sig-info: #0369a1;
}

/* Couple Pico's form border to --tool-border (2026-09-17, as on the author's
   site). Pico ships #2a3140 on dark and #cfd5e2 on light, both around 1.3:1
   against the page: number fields and selects read as borderless while the
   segments and buttons next to them carry an outline. The grey skin is the
   exception and keeps its white, which is stronger still.

   The attribute is doubled on purpose: Pico's light values hang on
   `:root:not([data-theme=dark])`, specificity 0-2-0, and a plain
   html[data-theme="light"] is only 0-1-1 and would lose. Doubled it is 0-2-1.
   Grey carries data-theme="dark" as well and is therefore matched here too,
   but the block below is 0-3-1 and wins; it also sets --tool-border to white,
   so both paths would agree anyway. */
html[data-theme][data-theme] {
  --pico-form-element-border-color: var(--tool-border);
}

/* === GREY SKIN (the default here: white on mid grey, outlines instead of
   fills, exactly one accent) ===
   It is a skin, not a theme: the head bootstrap sets data-skin="grey" together
   with data-theme="dark", so the dark blocks apply first and this overrides.
   The attribute is tripled so this block wins on specificity regardless of
   where it sits in the file. */
html[data-skin="grey"][data-skin="grey"][data-skin="grey"] {
  /* THE accent. The only colour on the page besides the greys. */
  --accent: #6fe3d2;
  --accent-hover: color-mix(in srgb, var(--accent) 72%, white);

  --pico-background-color: #55595f;
  --pico-color: #f2f4f5;
  --pico-muted-color: #c9cfd2;
  --pico-muted-border-color: #9aa1a6;
  --pico-h1-color: #ffffff;
  --pico-h2-color: #ffffff;
  --pico-h3-color: #f2f4f5;
  --pico-primary: var(--accent);
  --pico-primary-background: var(--accent);
  --pico-primary-border: var(--accent);
  --pico-primary-underline: color-mix(in srgb, var(--accent) 50%, transparent);
  --pico-primary-hover: var(--accent-hover);
  --pico-primary-hover-background: var(--accent-hover);
  --pico-primary-hover-border: var(--accent-hover);
  --pico-primary-focus: color-mix(in srgb, var(--accent) 37%, transparent);
  --pico-primary-inverse: #22262b;
  --pico-card-background-color: #55595f;
  --pico-card-border-color: #ffffff;
  --pico-card-box-shadow: none;
  --pico-box-shadow: none;
  --pico-button-box-shadow: none;
  --pico-button-hover-box-shadow: none;
  --pico-form-element-background-color: #55595f;
  --pico-form-element-border-color: #ffffff;
  --pico-form-element-color: #f2f4f5;
  --pico-form-element-placeholder-color: #b0b7bb;
  --pico-form-element-active-background-color: #55595f;
  --pico-form-element-active-border-color: var(--accent);
  --pico-form-element-focus-color: var(--accent);

  /* Tool ladder, compressed: separation comes from the white border, not from
     lightness steps. */
  --tool-emph:     #ffffff;
  --tool-text:     #f2f4f5;
  --tool-label:    #dfe3e5;
  --tool-note:     #c9cfd2;
  --tool-faint:    #b0b7bb;
  --tool-border:   #ffffff;
  --tool-hairline: #9aa1a6;
  --tool-grid:     #b0b7bb;
  --tool-ink:      #22262b;
  --tool-panel:    #55595f;
  --tool-surface:  #55595f;
  --tool-subtle:   #5c6167;
  --tool-shadow:   transparent;
  --tool-panel-shadow: none;
  /* Achromatic signals: the only channel left is brightness, so it goes to
     whatever the reader must not miss. Info keeps the accent, its one job. */
  --tool-sig-good: #b3babe;
  --tool-sig-warn: #d9dde0;
  --tool-sig-bad:  #ffffff;
  --tool-sig-info: var(--accent);
}

/* === SCROLLBARS ===
   The head bootstrap on every page mirrors the polarity onto
   `element.style.colorScheme`, and theme.js keeps it in sync on a switch, so
   light and dark already get a scrollbar of the right polarity from the first
   paint. Two things are still wrong without the rules below.

   Grey is the case that needs naming. It IS data-theme="dark", so it inherits
   the browser's dark scrollbar, and that scrollbar is built for a near-black
   page: on #55595f the track comes out darker than the page and the thumb
   nearly black. That is the wrong lightness direction and the opposite of what
   this skin says everywhere else, namely that surfaces are the same grey and
   structure is the outline drawn on top. The thumb is therefore named in the
   hairline grey (#9aa1a6, the value of --tool-hairline and
   --pico-muted-border-color here), which is the weight a separator carries in
   this skin.

   In light and dark the polarity is right but the thumb is achromatic, and
   next to a cool slate page a neutral grey strip reads as foreign. Each thumb
   is therefore derived from that skin's own background: same hue, lightness
   moved away from the page (up in dark, down in light). Tinted, not coloured:
   the accent stays reserved for the data.

   The track is `transparent` rather than a colour, and that is a fix, not a
   default. A painted track is an opaque strip the full height of the scroll
   container, so on anything with a border-radius (the log panel, the popovers)
   it squares off the rounded corner. Transparent lets the element's own
   clipped background show through, and it means the track needs no per-surface
   value: a panel in --tool-subtle gets the right backdrop for free.

   Each rule names the root AND its descendants. scrollbar-color inherits, but
   a scroll container that establishes its own scrollbar does not always take
   the inherited value in Chromium, so naming the descendants is what actually
   reaches the log panel and the popovers.

   Literal hex rather than var(): these sit on the root element, where the tool
   ladder is declared in the same block, and the cascade order inside one
   declaration list is not something to rely on.

   The grey attribute is tripled for the same reason as the ladder block above,
   so that it wins regardless of where it sits in the file. Untripled its
   descendant half is 0-1-1, exactly tying the data-theme="dark" one, and the
   tie is currently resolved only by this block coming later: moving either
   rule would silently put the dark thumb on the grey page. Tripled it is
   0-3-1 and the order stops mattering.
   No ::-webkit-scrollbar pseudo-elements anywhere: one mechanism per element,
   or Firefox and Chromium would disagree on the same panel. */
html[data-theme="dark"],
html[data-theme="dark"] * {
  scrollbar-color: #50607c transparent;
}

html[data-theme="light"],
html[data-theme="light"] * {
  scrollbar-color: #829db6 transparent;
}

html[data-skin="grey"][data-skin="grey"][data-skin="grey"],
html[data-skin="grey"][data-skin="grey"][data-skin="grey"] * {
  scrollbar-color: #9aa1a6 transparent;
}

/* === SEGMENTED CONTROL (settings switchers, incl. the theme switcher) === */
.lang-seg {
  font-size: 0.72rem;
  padding: 2px 8px;
  margin: 0;
  background: transparent;
  /* --tool-border, not --pico-muted-border-color (2026-09-17). Pico's muted
     border is a hairline meant for separators; at #262e3a on dark and #d8e0ea
     on light it left the unselected segments at about 1.3:1, so the control
     read as one selected pill next to two ghosts. --tool-border is solved for
     3:1 and is what every framed element on these pages already uses. */
  border: 1px solid var(--tool-border);
  cursor: pointer;
  color: var(--pico-muted-color);
  line-height: 1.6;
  white-space: nowrap;
  transition: background 0.12s, color 0.12s, border-color 0.12s;
  border-radius: 4px;
  width: auto;
}
/* Corners and shared borders follow adjacency, so a lone button stays fully
   rounded and a group joins seamlessly. */
.lang-seg:has(+ .lang-seg) {
  border-top-right-radius: 0;
  border-bottom-right-radius: 0;
  border-right: none;
}
.lang-seg + .lang-seg {
  border-top-left-radius: 0;
  border-bottom-left-radius: 0;
}
.lang-seg.active {
  background: var(--pico-primary);
  color: var(--page-bg);
  border-color: var(--pico-primary);
}
.lang-seg:hover:not(.active):not(:disabled),
.lang-seg:focus:not(.active):not(:disabled) {
  background: transparent;
  border-color: var(--pico-primary);
  color: var(--pico-primary);
}

/* Theme switcher: sun / contrast / moon as SVG masks tinted with currentColor,
   so they need no translation and follow every skin. */
.theme-switcher {
  display: flex;
  align-items: center;
}
.theme-seg {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 4px 10px;
}
.theme-seg::before {
  content: "";
  width: 14px;
  height: 14px;
  background-color: currentColor;
  -webkit-mask: var(--icon) no-repeat center / contain;
  mask: var(--icon) no-repeat center / contain;
}
.theme-seg[data-theme-set="light"] { --icon: url('../img/icons/theme-sun.svg'); }
.theme-seg[data-theme-set="dark"]  { --icon: url('../img/icons/theme-moon.svg'); }
.theme-seg[data-theme-set="grey"]  { --icon: url('../img/icons/theme-contrast.svg'); }

/* === THEME CHANGE ANIMATION ===
   Browsers with the View Transitions API animate two page snapshots on the
   GPU: the old one holds still while the new one sweeps in from the top-right
   corner, where the switcher sits. The gradient stops approximate a smoothstep
   so the moving boundary is a soft ramp, not a clip edge. */
::view-transition-old(root) { animation: none; }
::view-transition-new(root) {
  mask-image: linear-gradient(to bottom left,
    rgb(0 0 0) 38%, rgb(0 0 0 / 0.92) 43%, rgb(0 0 0 / 0.5) 50%,
    rgb(0 0 0 / 0.08) 57%, transparent 62%);
  mask-size: 300% 300%;
  mask-repeat: no-repeat;
  animation: theme-sweep 1.2s ease-in-out both;
}
@keyframes theme-sweep {
  from { mask-position: 0% 100%; }
  to   { mask-position: 100% 0%; }
}
/* Fallback without the API: transition every element for half a second. On for
   600ms via JS and never during normal browsing. */
html.theme-morph, html.theme-morph * {
  transition: background-color 0.5s ease, color 0.5s ease, border-color 0.5s ease,
    fill 0.5s ease, stroke 0.5s ease, box-shadow 0.5s ease;
}
@media (prefers-reduced-motion: reduce) {
  html.theme-morph, html.theme-morph * { transition: none; }
  ::view-transition-old(root), ::view-transition-new(root) { animation: none; }
  ::view-transition-new(root) { mask-image: none; }
}

/* === LANGUAGE CHANGE ANIMATION ===
   The per-element fallback, driven by lang.js on browsers without view
   transitions: the new wording washes down the page instead of appearing all at
   once. Each element holds its old wording, comes apart, is retexted at the
   trough and pulls itself back together.

   Coming apart is a channel split: a red copy runs right, a cyan one left, the
   glyph stays put. Two text-shadows rather than three real layers, because the
   text nodes belong to React and cloning them would mean fighting it for them.
   For a shift of a fraction of an em that is not a cheat but the same picture:
   white where all three channels still overlap, red and cyan where they no
   longer do. The offsets are in em, so a heading comes apart further than a
   footnote and the effect keeps its proportions.

   The 40% mark must stay in step with LANG_TROUGH in lang.js: that is where the
   element is blank and the text is swapped unseen. `backwards` keeps it whole
   during its delay, so nothing stirs before its turn. */
.lang-swap { animation: lang-swap 0.42s ease-in-out backwards; }
@keyframes lang-swap {
  0%   { opacity: 1;   text-shadow: 0 0 rgba(255, 0, 0, 0),      0 0 rgba(0, 255, 255, 0); }
  28%  { opacity: 0.5; text-shadow: 0.22em 0 rgba(255, 0, 0, 0.9), -0.22em 0 rgba(0, 255, 255, 0.9); }
  40%  { opacity: 0;   text-shadow: 0.3em 0 rgba(255, 0, 0, 0.9), -0.3em 0 rgba(0, 255, 255, 0.9); }
  58%  { opacity: 0.5; text-shadow: 0.22em 0 rgba(255, 0, 0, 0.9), -0.22em 0 rgba(0, 255, 255, 0.9); }
  100% { opacity: 1;   text-shadow: 0 0 rgba(255, 0, 0, 0),      0 0 rgba(0, 255, 255, 0); }
}
@media (prefers-reduced-motion: reduce) {
  .lang-swap { animation: none; }
}

/* The same swap as a post effect on the whole rendered page, wherever view
   transitions exist. lang.js sets .lang-vt on <html> for the duration, both to
   keep the theme sweep's unscoped rules off these snapshots and to make the
   pair of rules easy to find. The old page comes apart into its channels while
   the new one wipes down over it, so rules, panels and the canvas break up
   along with the text. The wipe replaces the per-element wave: with the page
   frozen as a texture there is nothing left to stagger.

   Both snapshots run the same walk, and its band is where the wipe's edge is,
   so the page comes apart just ahead of the edge and closes up just behind it.
   The disturbance travels instead of happening everywhere at once.

   @keyframes lang-vt-wave is not here but in lang.js, which generates it
   alongside the filters it steps through: `filter` interpolates discretely, so
   the movement is one filter per band position, and the two have to agree. */
html.lang-vt::view-transition-old(root) {
  animation: lang-vt-wave 1.1s linear both;
}
/* Same construction as the theme sweep, but straight down: an oversized mask
   whose soft edge travels from the top of the viewport to the bottom.

   The numbers are all about where the soft edge is, and they are tied to each
   other. The edge has to travel from just above the viewport to just below it,
   0.2 of a screen ahead of and behind the visible run, or it appears to start
   and stop on screen. That travel has to happen while the tile's opaque end
   still covers everything above the edge: mask-position must not go below 0,
   because past that the top of the tile drops below the top of the screen, and
   with no-repeat everything above it counts as clear. The new page is then
   un-revealed in a strip at the top for the last stretch of the run, which
   looks like the header flickering back to the old language just as the wave
   finishes.

   So the overshoot is bought with tile height instead: the tile is three
   viewports tall, the 50% stop sits at 37.3% of it, which puts the edge 1.12
   screens down at position 0, and the run starts at 62%, which puts it 0.12
   screens above the top. Timing is linear, because a wave that accelerates in
   the middle and dawdles at the ends does not read as a wave at all, it reads
   as the whole page changing at once. LANG_TRAVEL in lang.js is the same -0.12
   to 1.12 and has to stay in step, or the band and the edge drift apart. */
html.lang-vt::view-transition-new(root) {
  mask-image: linear-gradient(to bottom,
    rgb(0 0 0) 34%, rgb(0 0 0 / 0.5) 37.3%, transparent 40.7%);
  mask-size: 100% 300%;
  mask-repeat: no-repeat;
  animation: lang-vt-wipe 1.1s linear both,
             lang-vt-wave 1.1s linear both;
}
@keyframes lang-vt-wipe {
  from { mask-position: 50% 62%; }
  to   { mask-position: 50% 0%; }
}

/* The language switcher sits next to the theme switcher in the page header;
   the two are separate groups, so a gap keeps them from reading as one
   six-button control. */
.lang-switcher {
  display: flex;
  align-items: center;
  margin-right: 12px;
}

/* === PAGE LAYOUT (shared by the demo pages) === */
.tw-head {
  display: flex;
  justify-content: flex-end;
  padding-top: 14px;
}
/* With a title in the same row the switchers move to the other end of it. Not
   space-between: with two switchers in the row that would push them apart
   instead of keeping them together at the right. */
.tw-head:has(h1) {
  align-items: center;
  gap: 20px;
}
.tw-head h1 { margin: 0; margin-right: auto; }
.tw-stage {
  display: block;
  width: 100%;
  background: color-mix(in srgb, var(--tool-panel) 55%, transparent);
  border: 1px solid var(--tool-border);
  border-radius: 8px;
  box-shadow: var(--tool-panel-shadow);
  touch-action: none;
  cursor: grab;
}
.tw-stage.dragging { cursor: grabbing; }
.tw-controls {
  display: flex;
  gap: 18px;
  flex-wrap: wrap;
  align-items: center;
  margin: 14px 2px 0;
  font-size: 13px;
  color: var(--tool-label);
}
.tw-controls label {
  display: flex;
  align-items: center;
  gap: 7px;
  margin: 0;
  font-size: 13px;
  color: var(--tool-label);
}
.tw-controls input {
  width: 74px;
  margin: 0;
  padding: 3px 8px;
  font-size: 14px;
  height: auto;
}
.tw-read {
  display: flex;
  gap: 18px;
  flex-wrap: wrap;
  align-items: baseline;
  font-size: 12.5px;
  color: var(--tool-note);
  margin: 10px 2px 0;
  font-variant-numeric: tabular-nums;
}
.tw-read b { color: var(--tool-text); font-weight: 600; }
.tw-foot {
  margin-top: 26px;
  font-size: 11px;
  color: var(--tool-faint);
  line-height: 1.7;
}

/* === TABS (choose a VIEW) ===
   The house pattern for choosing a VIEW, as opposed to .lang-seg above, which
   chooses a SETTING. The distinction is the point: as a segmented control the
   tab switch would look exactly like the half dozen settings under it.
   Underlined tabs say "these are the sections" instead.
   The strip carries the baseline and the active tab overpaints it thicker, so
   the underline must sit ON the strip's line, hence the negative bottom margin
   on the buttons.
   Shared here rather than per page: this block stood three times, byte for
   byte, in levels.html, moisture.html and thermal.html. */
.tabstrip {
  display: flex;
  gap: 22px;
  flex-wrap: wrap;
  border-bottom: 1px solid var(--tool-hairline);
  /* A line of air under the strip, for every tab at once. Each tab keeps its
     own margin on top of this for whatever else it needs; putting the common
     part here is what stops the four from drifting apart. */
  margin-bottom: 20px;
}
.tabstrip > button {
  margin: 0 0 -1px;
  padding: 4px 2px 6px;
  width: auto;
  background: transparent;
  border: none;
  border-bottom: 2px solid transparent;
  border-radius: 0;
  font-size: 1rem;
  font-weight: 500;
  line-height: 1.3;
  color: var(--tool-label);
  cursor: pointer;
}
/* Pico redefines colour and border on button:is(:hover,:active,:focus); both
   have to be taken back explicitly or the tab grows a blue box.
   See the shared focus rule near the end of this file for the third thing it
   sets there, the box-shadow ring. */
.tabstrip > button:hover:not(.active),
.tabstrip > button:focus:not(.active) {
  background: transparent;
  border-color: transparent;
  border-bottom-color: var(--tool-hairline);
  color: var(--tool-emph);
}
.tabstrip > button.active,
.tabstrip > button.active:hover,
.tabstrip > button.active:focus {
  background: transparent;
  color: var(--pico-primary);
  border-bottom-color: var(--pico-primary);
  font-weight: 600;
}
.tabstrip > button:disabled { opacity: 0.45; cursor: not-allowed; }

/* === ACTION BUTTONS ===
   Ported from alber.me/design-conventions.html, not imported: these pages are
   self-contained and offline by rule, so the rule body is a copy. Values are
   that page's exactly, including the 1.5px border and the radius 8, because a
   button that is nearly the site button is worse than either.

   The convention that comes with it: an icon marks a button that PERFORMS AN
   ACTION on something (play, run a treatment, export), while choices, filters
   and tabs stay bare. A row where every button has a picture is wallpaper.

   width: auto and margin: 0 are load-bearing: Pico gives button[type=button] a
   bottom margin and stretches submit buttons to full width, and either one
   wrecks a row of buttons. */
.tool-btn {
  display: inline-flex; align-items: center; justify-content: center;
  gap: 7px; width: auto; margin: 0;
  padding: 0.4rem 0.9rem;
  font-family: inherit; font-size: 0.8rem; line-height: 1.35;
  cursor: pointer;
  border: 1.5px solid var(--tool-border); border-radius: 8px;
  background: var(--tool-surface); color: var(--tool-text);
  transition: border-color 0.12s, background 0.12s, color 0.12s;
}
.tool-btn:hover:not(:disabled) { border-color: var(--tool-note); }
.tool-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.tool-btn svg { flex: 0 0 auto; }
/* Pure size modifier: it restates no colours, so a variant can never drift
   from the base. */
.tool-btn-sm { padding: 0.28rem 0.7rem; font-size: 0.74rem; border-radius: 7px; }


/* === FOCUS ============================================================
   Pico draws its focus ring as a box-shadow on plain :focus, not on
   :focus-visible. A mouse click therefore leaves a blue box sitting on
   whatever was clicked until focus moves elsewhere, which on the tab strip
   looked like a tab had got stuck (reported 2026-09-17). It only showed on
   the dark and light skins: the ring is --pico-primary-focus, a translucent
   blue that vanishes into the grey skin's own borders.

   So the ring is dropped for pointer focus and reissued as an outline under
   :focus-visible, which is the keyboard case it exists for. An outline rather
   than a box-shadow because it follows border-radius, never affects layout,
   and stays visible in forced-colours mode. Keep this together in one place:
   split across the button classes it drifted, and .tool-btn never had it at
   all. */
.tabstrip > button:focus,
.lang-seg:focus,
.tool-btn:focus,
.th-info:focus {
  box-shadow: none;
  --pico-box-shadow: 0 0 0 rgba(0, 0, 0, 0);
}
.lang-seg:focus-visible,
.tool-btn:focus-visible,
.th-info:focus-visible {
  outline: 2px solid var(--pico-primary);
  outline-offset: 2px;
}
/* The tabs sit tight against the hairline they underline, so their ring goes
   inside: an offset one would cross the strip's bottom border. */
.tabstrip > button:focus-visible {
  outline: 2px solid var(--pico-primary);
  outline-offset: -3px;
}
