@charset "UTF-8";
/* ==========================================================================
   EasyCalculatorSmart — Design system

   Table of contents
   1.  Tokens
   2.  Reset & base
   3.  Typography
   4.  Layout
   5.  Header & navigation
   6.  Buttons
   7.  Forms
   8.  Cards
   9.  Result panel
   10. Tables
   11. Disclosure & accordion
   12. Breadcrumbs
   13. Article & prose
   14. Table of contents
   15. Callouts & disclaimer
   16. Footer
   17. Search
   18. Utilities
   19. Print
   20. Theme toggle, motion & interaction
   21. Tools platform
   22. Index pages & homepage polish
   23. Exact-arithmetic calculators
   30. Platform layout: listing cards, homepage, hubs
   31. Blox Fruits trade calculator
   32. Find the zeros calculator
   33. Grow a Garden calculator
   34. Grow Garden Calculator (real gardens)
   ========================================================================== */

/* ==========================================================================
   1. TOKENS
   ========================================================================== */

:root {
  /* Brand — a deep teal reads as precise and utilitarian without the
     generic SaaS-indigo look. */
  --brand-50:  #eefbf8;
  --brand-100: #d3f4ed;
  --brand-200: #a9e8dd;
  --brand-300: #75d5c8;
  --brand-400: #42bcaf;
  --brand-500: #269e94;
  --brand-600: #1a7f79;
  --brand-700: #196662;
  --brand-800: #1a5250;
  --brand-900: #1a4543;

  /* Tool hue - the second half of the brand.
   *
   * The calculators and the tools are one site and two products. Teal
   * alone could not say that: a calculator card and a converter card were
   * the same object in the same colour, and the only way to tell them
   * apart was to read them.
   *
   * Violet sits far enough from teal to separate the two at a glance and
   * close enough in temperature that they still read as one palette. It
   * is used ONLY in /tools/ and in the places the homepage points there.
   * Nothing in a calculator ever takes this hue. */
  --violet-50:  #f4f2fe;
  --violet-100: #e8e3fd;
  --violet-200: #d2c8fb;
  --violet-300: #b0a0f6;
  --violet-400: #8f78f0;
  --violet-500: #7455e7;
  --violet-600: #5f3dcc;
  --violet-700: #4d31a4;
  --violet-800: #402a83;
  --violet-900: #352367;

  /* Neutrals — warm, faintly brown greys. The dark theme is a charcoal
     with a little brown in it rather than a blue-black, which is what
     makes the teal and the violet read as two accents on one ground
     instead of two neon signs. Only the token blocks read this scale;
     every component reads a role. */
  --ink-50:  #faf8f5;
  --ink-100: #f2eee9;
  --ink-200: #e5dfd7;
  --ink-300: #cfc6bb;
  --ink-400: #a69c91;
  --ink-500: #7a6f65;
  --ink-600: #5a5048;
  --ink-700: #463e37;
  --ink-800: #2b2520;
  --ink-900: #211c18;
  --ink-950: #171310;

  /* Semantic */
  --ok-bg:    #e8f7ee;   --ok-fg:    #10653a;   --ok-line:   #a7dcbe;
  --warn-bg:  #fdf4e3;   --warn-fg:  #7d5307;   --warn-line: #f0d49a;
  --bad-bg:   #fdecec;   --bad-fg:   #97231f;   --bad-line:  #f3bdbb;
  --info-bg:  #eaf3fb;   --info-fg:  #17527e;   --info-line: #b6d5ec;

  /* Applied roles — LIGHT (the default)
   *
   * The site ships two themes. Every colour used anywhere in this file is a
   * role token, never a raw hex, so a theme is defined entirely by the two
   * blocks below and nothing else has to change.
   *
   * How the theme is chosen, in order:
   *   1. An explicit choice, stored in localStorage and written to
   *      <html data-theme="light|dark"> by a tiny inline script in the
   *      document head. Inline and synchronous so the correct colours are
   *      applied before first paint — no flash of the wrong theme.
   *   2. Failing that, the operating-system preference, resolved by the
   *      same script and NOT persisted, so the page keeps following the
   *      system until the visitor picks something.
   *   3. Failing both (JavaScript off), light — this block.
   *
   * There is deliberately no prefers-color-scheme media query. The head
   * script resolves the system preference into the attribute instead,
   * which keeps every dark value in exactly one place.
   *
   * Text contrast against --surface (#ffffff):
   *   --text       #211c18  ~16.8:1   headings and body
   *   --text-soft  #463e37  ~10.6:1   supporting copy
   *   --text-faint #685d53  ~6.4:1    labels and captions
   * All three clear WCAG AA for body text; the faint tone is deliberately
   * darker than a typical placeholder grey so it stays readable on white.
   */
  --bg:            var(--ink-50);
  --surface:       #ffffff;
  --surface-sunk:  var(--ink-100);
  --surface-alt:   #fbfcfd;
  --line:          var(--ink-200);
  --line-strong:   var(--ink-300);
  --text:          var(--ink-900);
  --text-soft:     var(--ink-700);
  --text-faint:    #685d53;
  --accent:        var(--brand-600);
  --accent-hover:  var(--brand-700);
  --accent-soft:   var(--brand-50);
  --accent-line:   var(--brand-200);
  --on-accent:     #ffffff;
  --focus:         var(--brand-500);

  /* Surfaces that are not plain --surface and must still flip with the
     theme: the hero wash and the toast, which is deliberately inverted. */
  /* A deeper accent for text that sits ON --accent-soft, where the
     standard accent only just clears AA. And the meter gradient, which
     needs different ends in each theme to stay visible. */
  --accent-strong: var(--brand-700);
  --meter-from:    var(--brand-400);
  --meter-to:      var(--brand-600);

  --brand-mark-bg: var(--brand-600);
  --brand-mark-fg: #ffffff;

  --hero-wash:     var(--brand-50);
  --toast-bg:      var(--ink-900);
  --toast-fg:      #ffffff;

  /* Type */
  --font-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto,
               "Helvetica Neue", Arial, "Noto Sans", sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
               "Liberation Mono", monospace;

  --step--1: clamp(0.82rem, 0.80rem + 0.08vw, 0.86rem);
  --step-0:  clamp(0.98rem, 0.95rem + 0.12vw, 1.05rem);
  --step-1:  clamp(1.12rem, 1.06rem + 0.28vw, 1.28rem);
  --step-2:  clamp(1.32rem, 1.20rem + 0.55vw, 1.62rem);
  --step-3:  clamp(1.56rem, 1.34rem + 0.95vw, 2.10rem);
  --step-4:  clamp(1.84rem, 1.48rem + 1.55vw, 2.75rem);

  /* Space — 4px base */
  --s1: 0.25rem;  --s2: 0.5rem;   --s3: 0.75rem;  --s4: 1rem;
  --s5: 1.5rem;   --s6: 2rem;     --s7: 2.75rem;  --s8: 3.5rem;
  --s9: 5rem;

  /* Shape */
  --r-sm: 6px;  --r-md: 10px;  --r-lg: 14px;  --r-xl: 20px;  --r-full: 999px;

  /* Depth — restrained; borders do most of the separation work. */
  --shadow-sm: 0 1px 2px rgba(13, 17, 22, 0.06);
  --shadow-md: 0 2px 4px rgba(13, 17, 22, 0.05), 0 4px 12px rgba(13, 17, 22, 0.05);
  --shadow-lg: 0 8px 28px rgba(13, 17, 22, 0.10);


  /* Tool roles - LIGHT
   *
   * Contrast on --surface (#ffffff):
   *   --tool-accent  #5f3dcc  ~7.5:1
   *   --tool-strong  #4d31a4  ~9.8:1
   * Both clear AA for body text, so a tool label is never the weak link.
   */
  --tool-accent:        var(--violet-600);
  --tool-accent-hover:  var(--violet-700);
  --tool-strong:        var(--violet-700);
  --tool-soft:          var(--violet-50);
  --tool-soft-2:        var(--violet-100);
  --tool-line:          var(--violet-200);
  --on-tool:            #ffffff;
  --tool-wash:          var(--violet-50);

  /* Calculator roles that do NOT follow a scope. Inside the tools half
     --accent-* is remapped to violet, but a link from there back to a
     calculator still has to wear teal, so it reads these instead.
     brand-700 on brand-50 is ~6.4:1. */
  --calc-soft:          var(--brand-50);
  --calc-line:          var(--brand-200);
  --calc-strong:        var(--brand-700);

  /* The two identities, fixed. --calc-accent is the teal a calculator
     label or link wears anywhere on the site, including inside the violet
     tools scope; the tiles are the filled icon squares every listing card,
     category card and section mark uses. The glyph on a tile is always
     white, so the gradient stops are chosen dark enough to carry it. */
  --calc-accent:        var(--brand-600);
  --tile-calc:          linear-gradient(145deg, var(--brand-500) 0%, var(--brand-700) 100%);
  --tile-tool:          linear-gradient(145deg, var(--violet-500) 0%, var(--violet-700) 100%);
  --on-tile:            #ffffff;
  --grad-btn-calc:      linear-gradient(135deg, var(--brand-600) 0%, var(--brand-700) 100%);
  --on-calc:            #ffffff;

  /* Soft washes behind the homepage hero, the hub heads and the two
     "worlds" cards. Tints of the two accents, never a surface a word sits
     on without its own background. */
  --glow-calc-soft:     rgba(38, 158, 148, .10);
  --glow-tool-soft:     rgba(116, 85, 231, .09);
  --world-calc-bg:      linear-gradient(140deg, #dcf3ee 0%, #f5fbf9 100%);
  --world-tool-bg:      linear-gradient(140deg, #ebe6fd 0%, #f8f6fe 100%);

  --r-card:             16px;

  /* Gradients.
   *
   * Every gradient on the site is one of these. They are tokens rather
   * than inline stops for the same reason the colours are: a theme has to
   * be able to change one and have the whole site follow, and a gradient
   * tuned for white is wrong on ink.
   *
   * Used for: icon tiles, primary buttons, hero decoration, selected
   * states and result emphasis. Not for text, not for large surfaces, and
   * never for anything a word has to sit on top of. */
  --grad-calc:      linear-gradient(135deg, var(--brand-500) 0%, var(--brand-700) 100%);
  --grad-calc-soft: linear-gradient(135deg, var(--brand-50) 0%, var(--brand-100) 100%);
  --grad-tool:      linear-gradient(135deg, var(--violet-500) 0%, var(--violet-700) 100%);
  --grad-tool-soft: linear-gradient(135deg, var(--violet-50) 0%, var(--violet-100) 100%);
  --grad-btn:       linear-gradient(135deg, var(--brand-600) 0%, var(--brand-700) 100%);
  --grad-btn-tool:  linear-gradient(135deg, var(--violet-600) 0%, var(--violet-700) 100%);

  /* The homepage mesh. Three offset radials rather than one linear ramp,
     so the wash has a centre of gravity instead of a seam. */
  --mesh-1: rgba(38, 158, 148, .16);
  --mesh-2: rgba(116, 85, 231, .14);
  --mesh-3: rgba(66, 188, 175, .12);

  /* Glow. A tinted shadow, not a neon ring: it reads as the card lifting
     off the page toward its own colour. */
  --glow-calc: 0 14px 34px -14px rgba(26, 127, 121, .50);
  --glow-tool: 0 14px 34px -14px rgba(95, 61, 204, .48);

  /* One more step on the radius scale. Tools use it; calculators do not,
     which is half of how the two are told apart without reading them. */
  --r-2xl: 26px;

  --container: 1140px;
  --measure: 68ch;
  --measure-article: 72ch;
  --header-h: 64px;

  /* Motion. Every transition and animation references these, so the
     reduced-motion block near the end of section 2 can flatten the whole
     site by overriding four values rather than hunting for keyframes. */
  --dur-fast: .14s;
  --dur:      .22s;
  --dur-slow: .45s;
  --dur-reveal: .62s;
  --ease:     cubic-bezier(.22, .61, .36, 1);
  /* A longer tail than --ease: the element decelerates into place, which
     is what makes an entrance readable at a glance. Used by the scroll
     reveal and the hero, not by hovers, where a slow tail feels laggy. */
  --ease-out: cubic-bezier(.16, .84, .30, 1);

  /* Moon phase calculator.

     A moon disc only reads as lit against a dark sky, so this one panel
     keeps its own small palette instead of following the light/dark
     surfaces — the same reason a photograph of the night sky is dark in a
     brightly lit room. Everything outside the panel uses the ordinary
     roles, and these values are repeated in the dark block only where the
     surrounding page changes around them. */
  --moon-sky:        #141b30;
  --moon-sky-edge:   #222c52;
  --moon-on-sky:     #eceffa;
  --moon-on-sky-dim: #a9b2d2;
  --moon-lit:        #f6f3e7;
  --moon-dark:       #262e4f;
  --moon-rim:        rgba(236, 239, 250, 0.30);
  --moon-glow:       rgba(132, 150, 255, 0.35);
  --moon-track:      rgba(236, 239, 250, 0.16);
  --moon-ring:       #6fd3c5;

  /* Twin flame calculator. Shares the moon panel’s sky so the two dark
     panels on this site are the same dark, and adds two very low-opacity
     glows and its own ring colour. Nothing pink, nothing heart-shaped: the
     romantic register is a soft light, and the page still has to read as a
     calculator. */
  --tf-ring:         #7fd7ca;
  --tf-ring-glow:    rgba(127, 215, 202, 0.45);
  --tf-glow-a:       rgba(122, 138, 255, 0.22);
  --tf-glow-b:       rgba(214, 140, 190, 0.16);

  color-scheme: light;
}

/* --------------------------------------------------------------------------
   Applied roles — DARK

   Contrast against --surface (#211c18):
     --text       #f2eee9  ~15.0:1   headings and body
     --text-soft  #cfc6bb  ~10.4:1   supporting copy
     --text-faint #a69c91  ~6.3:1    labels and captions
     --accent     #42bcaf  ~7.4:1    links and emphasis

   The accent lightens from brand-600 to brand-400 here: the deep teal that
   reads as precise on white is close to unreadable on a dark ground, and a
   theme that keeps one accent for both is always wrong in one of them.
   Because the accent is now light, --on-accent flips to a near-black so
   filled buttons stay legible.
   -------------------------------------------------------------------------- */

:root[data-theme="dark"] {
  --bg:            var(--ink-950);
  --surface:       var(--ink-900);
  --surface-sunk:  #1c1814;
  --surface-alt:   #29231e;
  --line:          #362f29;
  --line-strong:   var(--ink-700);
  --text:          var(--ink-100);
  --text-soft:     var(--ink-300);
  --text-faint:    var(--ink-400);
  --accent:        var(--brand-400);
  --accent-hover:  var(--brand-300);
  --accent-soft:   #12302e;
  --accent-line:   var(--brand-800);
  --on-accent:     #06201f;
  --focus:         var(--brand-300);

  /* A pale mint wash is invisible on ink, and the toast cannot use ink-900
     in dark mode because that IS the card surface — it would stop reading
     as an overlay. Both are inverted here. */
  /* Inverted: on a dark ground the DEEP teal is the unreadable one, so
     the strong accent lightens instead of darkening. */
  --accent-strong: var(--brand-300);
  --meter-from:    var(--brand-500);
  --meter-to:      var(--brand-300);

  --brand-mark-bg: var(--brand-400);
  --brand-mark-fg: #06201f;

  --hero-wash:     #14302e;
  --toast-bg:      var(--ink-100);
  --toast-fg:      var(--ink-950);

  /* Semantic pairs, re-tuned rather than reused. The light versions are
     pale tints designed to sit on white and become invisible on ink. */
  --ok-bg:    #10281c;   --ok-fg:    #7fd8a6;   --ok-line:   #1e5237;
  --warn-bg:  #2a2110;   --warn-fg:  #f0c674;   --warn-line: #574117;
  --bad-bg:   #2b1516;   --bad-fg:   #f3a6a3;   --bad-line:  #5c2a29;
  --info-bg:  #10202e;   --info-fg:  #96c7ec;   --info-line: #1f4767;

  /* Tool roles - DARK
   *
   * The violet lightens the same way the teal does, and for the same
   * reason: violet-600 on ink is a bruise, not an accent. Contrast on
   * --surface (#161b21): --tool-accent #8f78f0 ~6.4:1, --tool-strong
   * #b0a0f6 ~9.0:1.
   *
   * --tool-soft cannot be violet-50 here - a near-white tile on a dark
   * card is a hole punched in the page - so it becomes a deep violet ink
   * mixed toward the surface, the same move --accent-soft makes. */
  --tool-accent:        var(--violet-400);
  --tool-accent-hover:  var(--violet-300);
  --tool-strong:        var(--violet-300);
  --tool-soft:          #1e1840;
  --tool-soft-2:        #262052;
  --tool-line:          var(--violet-800);
  --on-tool:            #120b2c;
  --tool-wash:          #1b1638;

  /* brand-300 on #12302e is ~8:1. */
  --calc-soft:          #12302e;
  --calc-line:          var(--brand-800);
  --calc-strong:        var(--brand-300);

  --calc-accent:        var(--brand-400);
  --tile-calc:          linear-gradient(145deg, var(--brand-500) 0%, var(--brand-800) 100%);
  --tile-tool:          linear-gradient(145deg, var(--violet-500) 0%, var(--violet-800) 100%);
  --grad-btn-calc:      linear-gradient(135deg, var(--brand-300) 0%, var(--brand-500) 100%);
  --on-calc:            #06201f;

  --glow-calc-soft:     rgba(66, 188, 175, .11);
  --glow-tool-soft:     rgba(143, 120, 240, .11);
  --world-calc-bg:      linear-gradient(140deg, #123a36 0%, #1d2320 100%);
  --world-tool-bg:      linear-gradient(140deg, #2b2253 0%, #221d27 100%);

  /* Gradients, re-tuned rather than reused. The light pair runs 500->700
     because it sits on white; on ink that same ramp goes muddy at the
     dark end, so both ends lift. */
  --grad-calc:      linear-gradient(135deg, var(--brand-400) 0%, var(--brand-600) 100%);
  --grad-calc-soft: linear-gradient(135deg, #12302e 0%, #0f2523 100%);
  --grad-tool:      linear-gradient(135deg, var(--violet-400) 0%, var(--violet-600) 100%);
  --grad-tool-soft: linear-gradient(135deg, #221b47 0%, #1a1536 100%);
  --grad-btn:       linear-gradient(135deg, var(--brand-300) 0%, var(--brand-500) 100%);
  --grad-btn-tool:  linear-gradient(135deg, var(--violet-300) 0%, var(--violet-400) 100%);

  /* The mesh needs more opacity to register against ink at all. */
  --mesh-1: rgba(66, 188, 175, .20);
  --mesh-2: rgba(143, 120, 240, .18);
  --mesh-3: rgba(38, 158, 148, .16);

  --glow-calc: 0 16px 40px -14px rgba(66, 188, 175, .40);
  --glow-tool: 0 16px 40px -14px rgba(143, 120, 240, .38);

  /* Shadows need more opacity to register against a dark ground, and a
     hairline top highlight so raised surfaces still read as raised. */
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, .40);
  --shadow-md: 0 2px 4px rgba(0, 0, 0, .38), 0 4px 14px rgba(0, 0, 0, .34);
  --shadow-lg: 0 10px 34px rgba(0, 0, 0, .50);

  color-scheme: dark;
}

/* ==========================================================================
   2. RESET & BASE
   ========================================================================== */

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  /* Anchor links must clear the sticky header. */
  scroll-padding-top: calc(var(--header-h) + var(--s4));
}

body {
  margin: 0;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  background-color: var(--bg);
  color: var(--text);
  font-family: var(--font-sans);
  font-size: var(--step-0);
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

img, svg, video { max-width: 100%; height: auto; display: block; }

/* Every number on this site should line up in columns. */
.num, td.num, .result-value, .stat-value, table.data td, table.data th {
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}

::selection { background: var(--accent-soft); color: var(--text); }

/* One consistent, high-visibility focus ring everywhere. */
:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 2px;
  border-radius: var(--r-sm);
}
:focus:not(:focus-visible) { outline: none; }

/* The hidden attribute must win.

   [hidden] is a UA-stylesheet rule, so ANY author rule that sets display
   silently beats it. The unsupported-browser warning on the date tools
   is a .callout, which is display:flex, so it rendered in full on every
   browser that supports the calendars — the exact opposite of what it
   says. Three components had already grown their own
   `.x[hidden] { display: none !important }` patch; this replaces the
   need for a fourth.

   !important is right here: hidden means hidden, and no layout rule
   should be able to argue with it. */
[hidden] { display: none !important; }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

/* Keyboard users get a way past the navigation. */
.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 200;
  padding: var(--s3) var(--s4);
  background: var(--accent);
  color: var(--on-accent);
  font-weight: 600;
  border-radius: 0 0 var(--r-md) 0;
}
.skip-link:focus { left: 0; }

/* ==========================================================================
   3. TYPOGRAPHY
   ========================================================================== */

h1, h2, h3, h4, h5, h6 {
  margin: 0 0 var(--s3);
  line-height: 1.22;
  font-weight: 700;
  letter-spacing: -0.014em;
  color: var(--text);
  text-wrap: balance;
}

h1 { font-size: var(--step-4); letter-spacing: -0.024em; }
h2 { font-size: var(--step-3); letter-spacing: -0.019em; }
h3 { font-size: var(--step-2); }
h4 { font-size: var(--step-1); }
h5, h6 { font-size: var(--step-0); }

p { margin: 0 0 var(--s4); }
p:last-child { margin-bottom: 0; }

a { color: var(--accent); text-decoration-thickness: 1px; text-underline-offset: 0.16em; }
a:hover { color: var(--accent-hover); }

strong, b { font-weight: 650; }
small { font-size: var(--step--1); }

code, kbd, samp {
  font-family: var(--font-mono);
  font-size: 0.9em;
  background: var(--surface-sunk);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  padding: 0.1em 0.36em;
}

hr { border: 0; border-top: 1px solid var(--line); margin: var(--s6) 0; }

.lede {
  font-size: var(--step-1);
  color: var(--text-soft);
  line-height: 1.58;
  max-width: var(--measure);
}

.eyebrow {
  display: inline-block;
  font-size: var(--step--1);
  font-weight: 650;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: var(--s2);
}

/* ==========================================================================
   4. LAYOUT
   ========================================================================== */

.container {
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: var(--s4);
}
@media (min-width: 768px) { .container { padding-inline: var(--s5); } }

/* Also used on its own inside a .container (homepage FAQ, tools page), where
   there is no inherited auto margin to centre it. */
.container-narrow { max-width: 860px; margin-inline: auto; }

main { flex: 1 0 auto; }

.section { padding-block: var(--s7); }
.section-tight { padding-block: var(--s5); }
@media (min-width: 768px) { .section { padding-block: var(--s8); } }

.stack > * + * { margin-top: var(--s4); }
.stack-lg > * + * { margin-top: var(--s6); }

.grid { display: grid; gap: var(--s4); }

/* auto-fit collapses unused tracks, so a section holding one card
   stretches it across the full width and looks broken. auto-fill keeps
   the track sizing, so a lone card stays card-sized. grid-2 keeps
   auto-fit because its users (result cards, feature pairs) are always
   full and should share the row evenly. */
.grid-2 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr)); }
.grid-3 { grid-template-columns: repeat(auto-fill, minmax(min(100%, 260px), 1fr)); }
.grid-4 { grid-template-columns: repeat(auto-fill, minmax(min(100%, 190px), 1fr)); }

/* Exactly two columns above 720px, one below. Used where the pair is a
   deliberate composition — a feature grid that reads as a 2x2 block —
   rather than a flow of cards that should fill the row. auto-fit would
   put four 285px columns on a 1280px screen and squeeze each card into a
   narrow, over-tall strip. */
.grid-halves { grid-template-columns: 1fr; }
@media (min-width: 720px) { .grid-halves { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

.section-head { margin-bottom: var(--s5); }
.section-head p { color: var(--text-soft); max-width: var(--measure); margin-bottom: 0; }
.section-head h2 { margin-bottom: var(--s2); }

/* Heading on the left, "View all" on the right, stacking on narrow screens. */
.section-head-row {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--s4);
  flex-wrap: wrap;
}

.section-more {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  flex-shrink: 0;
  min-height: 44px;
  font-size: 0.92rem;
  font-weight: 650;
  color: var(--accent);
  text-decoration: none;
  white-space: nowrap;
}
.section-more:hover { text-decoration: underline; }
.section-more svg { width: 15px; height: 15px; transition: transform .16s; }
.section-more:hover svg { transform: translateX(3px); }

/* ==========================================================================
   5. HEADER & NAVIGATION
   ========================================================================== */

/* One bar on every page: the logo, then Calculators · Tools · Categories ·
   About with the site search after them, then language, theme and (on a
   small screen) the menu button.

   A solid page-coloured ground rather than a translucent blur: it keeps
   navigation text at full contrast whatever scrolls underneath, and
   renders identically in every browser. */
.site-header {
  position: sticky;
  top: 0;
  z-index: 100;
  background: var(--bg);
  border-bottom: 1px solid var(--line);
}

.header-inner {
  display: flex;
  align-items: center;
  gap: var(--s3);
  min-height: var(--header-h);
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  font-weight: 750;
  font-size: 1.08rem;
  letter-spacing: -0.02em;
  color: var(--text);
  text-decoration: none;
  white-space: nowrap;
  flex-shrink: 0;
}
.brand:hover { color: var(--text); }

/* The mark is pinned to the calculator teal, not the page accent. The
   tools half remaps --accent inside its main; the logo sits outside that
   scope, and pinning it here means it stays the same mark on every page.
   One brand, one colour. */
.brand-mark {
  width: 34px; height: 34px;
  display: grid; place-items: center;
  border-radius: 10px;
  background-color: var(--brand-600);
  background-image: var(--tile-calc);
  color: #ffffff;
  box-shadow: 0 8px 18px -10px rgba(38, 158, 148, .8), inset 0 1px 0 rgba(255, 255, 255, .2);
  flex-shrink: 0;
}
.brand-mark svg { width: 21px; height: 21px; }
.brand-name em { font-style: normal; color: var(--calc-accent); }

@media (max-width: 399px) {
  .brand { gap: 8px; font-size: 1rem; }
  .brand-mark { width: 30px; height: 30px; border-radius: 9px; }
  .brand-mark svg { width: 19px; height: 19px; }
}

/* The nav holds the links and the search. It takes the free width, the
   links sit in the middle of it and the search box at its far end. */
.nav {
  flex: 1 1 auto;
  display: flex;
  align-items: center;
  gap: var(--s4);
  min-width: 0;
}

.nav-list {
  display: flex;
  align-items: center;
  gap: 2px;
  list-style: none;
  margin: 0 auto;
  padding: 0;
}

.nav-link {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: var(--s2) var(--s3);
  border-radius: var(--r-md);
  color: var(--text-soft);
  text-decoration: none;
  font-weight: 550;
  font-size: 0.94rem;
  white-space: nowrap;
  transition: background-color .14s, color .14s;
}
.nav-link:hover { background: var(--surface-sunk); color: var(--text); }

/* The current page is marked in weight, colour and the underline drawn in
   section 20 — and by aria-current, so never by colour alone. */
.nav-link[aria-current="page"],
.navcats-trigger[data-current="true"] {
  color: var(--text);
  font-weight: 650;
}

/* Header search. A compact field with live suggestions; see section 17
   for the field itself. It sits at the far end of the nav on a wide
   screen and first in the menu panel on a small one. */
.header-search {
  flex: 0 1 270px;
  min-width: 170px;
  margin-inline-start: auto;
}

/* Categories: a disclosure holding two lists of real category pages. */
.nav-cats { position: relative; }

.navcats > summary {
  list-style: none;
  cursor: pointer;
}
.navcats > summary::-webkit-details-marker { display: none; }
.navcats > summary::marker { content: ''; }
.navcats > summary:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; }

.navcats-chevron {
  width: 15px; height: 15px;
  transition: transform var(--dur-fast) var(--ease);
}
.navcats[open] .navcats-chevron { transform: rotate(180deg); }
.navcats[open] > summary { background: var(--surface-sunk); color: var(--text); }

.navcats-panel {
  position: absolute;
  top: calc(100% + 10px);
  left: 50%;
  z-index: 70;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--s4);
  width: min(580px, calc(100vw - 2 * var(--s4)));
  padding: var(--s4);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-xl);
  box-shadow: var(--shadow-lg);
  transform: translateX(-50%);
}

.navcats-heading {
  margin: 0 0 var(--s2);
  padding-inline: var(--s2);
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--text-faint);
}

.navcats-group ul {
  display: grid;
  gap: 2px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.navcats-group a {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 44px;
  padding: 6px var(--s2);
  border-radius: var(--r-md);
  color: var(--text);
  text-decoration: none;
  font-size: 0.92rem;
  font-weight: 550;
  line-height: 1.3;
  transition: background-color .14s, color .14s;
}
.navcats-group a:hover,
.navcats-group a[aria-current="page"] { background: var(--surface-sunk); }
.navcats-calc a:hover { color: var(--calc-strong); }
.navcats-tool a:hover { color: var(--tool-strong); }
.navcats-group a:focus-visible { outline: 3px solid var(--focus); outline-offset: -2px; }

.navcats-icon {
  flex-shrink: 0;
  width: 30px; height: 30px;
  display: grid; place-items: center;
  border-radius: 9px;
  color: var(--on-tile);
}
.navcats-calc .navcats-icon { background-color: var(--brand-600); background-image: var(--tile-calc); }
.navcats-tool .navcats-icon { background-color: var(--violet-600); background-image: var(--tile-tool); }
.navcats-icon svg { width: 16px; height: 16px; }

/* Mobile menu button */
.menu-btn {
  display: none;
  width: 42px; height: 42px;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  color: var(--text);
  cursor: pointer;
}
.menu-btn svg { width: 20px; height: 20px; }
.menu-btn .icon-close { display: none; }
.menu-btn[aria-expanded="true"] .icon-open { display: none; }
.menu-btn[aria-expanded="true"] .icon-close { display: block; }

/* Below 1080px the links, the search and the controls no longer fit one
   bar at a readable size, so the nav becomes the menu panel: search
   first, then the links, with Categories opening in place. The theme
   toggle and the language switcher stay on the bar — switching either is
   something people do on arrival. */
@media (max-width: 1079px) {
  .menu-btn { display: flex; }
  .brand { margin-inline-end: auto; }

  .nav {
    display: none;
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    flex-direction: column;
    align-items: stretch;
    gap: var(--s3);
    padding: var(--s4);
    background: var(--surface);
    border-bottom: 1px solid var(--line);
    box-shadow: var(--shadow-lg);
    max-height: calc(100vh - var(--header-h));
    overflow-y: auto;
  }
  .nav[data-open="true"] { display: flex; }

  .header-search {
    order: -1;
    flex: 0 0 auto;
    width: 100%;
    min-width: 0;
    margin: 0;
  }

  .nav-list { flex-direction: column; align-items: stretch; gap: 2px; margin: 0; }
  .nav-link { display: flex; padding: var(--s3); font-size: 1rem; }
  .navcats-trigger { justify-content: space-between; width: 100%; }

  .navcats-panel {
    position: static;
    width: auto;
    grid-template-columns: 1fr;
    padding: var(--s2) 0 var(--s2) var(--s2);
    background: transparent;
    border: 0;
    box-shadow: none;
    transform: none;
  }
}

@media (min-width: 560px) and (max-width: 1079px) {
  .navcats-panel { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* ==========================================================================
   6. BUTTONS
   ========================================================================== */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s2);
  /* 44px minimum height keeps taps comfortable. */
  min-height: 44px;
  padding: var(--s3) var(--s5);
  font: inherit;
  font-weight: 600;
  font-size: 0.96rem;
  line-height: 1.2;
  text-align: center;
  text-decoration: none;
  border: 1px solid transparent;
  border-radius: var(--r-md);
  cursor: pointer;
  transition: background-color .14s, border-color .14s, color .14s, transform .06s;
  -webkit-user-select: none;
  user-select: none;
}
.btn:active { transform: translateY(1px); }
.btn svg { width: 17px; height: 17px; flex-shrink: 0; }

/* The primary button is the one place a gradient earns its keep on a
   small element: it gives the main action a light source that a flat fill
   cannot, and every stop of --grad-btn clears AA against --on-accent, so
   the label is as readable at the top-left corner as at the bottom-right.

   The sheen is a second, transparent layer over the gradient rather than a
   different gradient, so hover changes one value instead of re-declaring
   the fill. */
.btn-primary {
  background-color: var(--accent);
  background-image: var(--grad-btn);
  color: var(--on-accent);
  border-color: transparent;
  box-shadow: var(--shadow-sm);
}
.btn-primary:hover {
  background-color: var(--accent-hover);
  border-color: transparent;
  color: var(--on-accent);
  box-shadow: var(--glow-calc);
}
.btn-primary:active { box-shadow: var(--shadow-sm); }

/* The calculator button, fixed to teal. .btn-primary follows the page
   accent, which the tools scope remaps to violet; a link from the tools
   half back to the calculators has to stay teal, so it uses this. */
.btn-calc {
  background-color: var(--brand-500);
  background-image: var(--grad-btn-calc);
  color: var(--on-calc);
  border-color: transparent;
  box-shadow: var(--shadow-sm);
}
.btn-calc:hover {
  background-color: var(--brand-600);
  border-color: transparent;
  color: var(--on-calc);
  box-shadow: 0 14px 34px -14px rgba(38, 158, 148, .55);
}
.btn-calc:active { box-shadow: var(--shadow-sm); }

/* The same button, in the tools half of the site. It is the ONLY thing a
   tool page changes about the button: same size, same weight, same focus
   ring, different hue. */
.btn-tool {
  background-color: var(--tool-accent);
  background-image: var(--grad-btn-tool);
  color: var(--on-tool);
  border-color: transparent;
  box-shadow: var(--shadow-sm);
}
.btn-tool:hover {
  background-color: var(--tool-accent-hover);
  border-color: transparent;
  color: var(--on-tool);
  box-shadow: var(--glow-tool);
}
.btn-tool:active { box-shadow: var(--shadow-sm); }


.btn-secondary { background: var(--surface); color: var(--text); border-color: var(--line-strong); }
.btn-secondary:hover { background: var(--surface-sunk); color: var(--text); border-color: var(--text-faint); }

.btn-ghost { background: transparent; color: var(--text-soft); border-color: transparent; }
.btn-ghost:hover { background: var(--surface-sunk); color: var(--text); }

.btn-sm { min-height: 36px; padding: var(--s2) var(--s3); font-size: 0.87rem; }
.btn-block { width: 100%; }

/* The hero pair. Large enough to read as the primary action on a wide
   screen without becoming a slab on a phone, where the type stays at the
   normal size and only the target grows. */
.btn-lg {
  min-height: 52px;
  padding: var(--s3) var(--s6);
  font-size: 1rem;
  border-radius: var(--r-lg);
}
.btn-lg svg { width: 18px; height: 18px; }

.btn:disabled, .btn[aria-disabled="true"] {
  opacity: .55;
  cursor: not-allowed;
  transform: none;
}

.btn-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s3);
}
.btn-row .btn { flex: 1 1 auto; }
@media (max-width: 479px) {
  .btn-row { flex-direction: column; }
  .btn-row .btn { width: 100%; }
}

/* ==========================================================================
   7. FORMS
   ========================================================================== */

.field { margin-bottom: var(--s4); }
.field:last-child { margin-bottom: 0; }

.label {
  display: block;
  margin-bottom: var(--s2);
  font-weight: 600;
  font-size: 0.92rem;
  color: var(--text);
}
.label .req { color: var(--bad-fg); margin-left: 2px; }
.label .opt { color: var(--text-faint); font-weight: 450; margin-left: 4px; }

.hint {
  display: block;
  margin-top: var(--s2);
  font-size: 0.85rem;
  color: var(--text-faint);
  line-height: 1.5;
}

/* A compact input for editing a value in place inside a table cell —
   used by the editable grade-point scales. Kept to the 36px control
   height rather than shrinking further, so it stays a comfortable
   touch target on a phone. */
.input-sm {
  min-height: 36px;
  max-width: 7.5rem;
  margin-left: auto;
  padding: var(--s1) var(--s2);
  font-size: 0.9rem;
  text-align: right;
}

.input, .select, .textarea {
  display: block;
  width: 100%;
  min-height: 46px;
  padding: var(--s3);
  font: inherit;
  /* 16px minimum stops iOS Safari zooming on focus. */
  font-size: max(16px, 0.98rem);
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-md);
  transition: border-color .14s, box-shadow .14s;
  -webkit-appearance: none;
  appearance: none;
}
.textarea { min-height: 96px; resize: vertical; line-height: 1.6; }

.input:hover, .select:hover, .textarea:hover { border-color: var(--text-faint); }

.input:focus, .select:focus, .textarea:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
}
.input:focus-visible, .select:focus-visible, .textarea:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 1px;
}

.input::placeholder, .textarea::placeholder { color: var(--text-faint); opacity: 1; }

/* A select is a fixed-width control holding text of any length, so at
   320px a long option is cut mid-word. The text is never lost — the open
   dropdown sizes itself to its content — but an ellipsis says "there is
   more" instead of looking like a rendering fault. */
.select {
  text-overflow: ellipsis;
  padding-right: var(--s7);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='%236b7885' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 8l5 5 5-5'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right var(--s3) center;
  background-size: 18px;
}

/* Native controls follow the ACTIVE THEME, not the operating system.
   `color-scheme` is set on :root in both token blocks, and these controls
   inherit it — so a date picker or number spinner opens light inside the
   light theme and dark inside the dark one, even when the OS disagrees
   with the visitor's choice on this site.

   Previously these were pinned to `light` because the site had no dark
   theme; leaving that pin would now put a white date picker inside a dark
   field, which is the same bug in the other direction. */
input[type="date"], input[type="time"], input[type="number"] {
  color-scheme: inherit;
}

/* The select chevron is a background SVG, so its stroke cannot inherit
   currentColor. Two versions rather than one grey that is wrong in one
   theme. */
:root[data-theme="dark"] .select {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='%2398a3af' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 8l5 5 5-5'/%3E%3C/svg%3E");
}

/* Invalid state. Marked with a border, an icon and a text message —
   never colour alone. */
.input[aria-invalid="true"], .select[aria-invalid="true"], .textarea[aria-invalid="true"] {
  border-color: var(--bad-fg);
  background-color: var(--bad-bg);
}
.input[aria-invalid="true"]:focus, .select[aria-invalid="true"]:focus {
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--bad-fg) 18%, transparent);
}

.error-msg {
  display: none;
  align-items: flex-start;
  gap: var(--s2);
  margin-top: var(--s2);
  font-size: 0.87rem;
  font-weight: 500;
  color: var(--bad-fg);
  line-height: 1.45;
}
.error-msg::before {
  content: "!";
  flex-shrink: 0;
  width: 16px; height: 16px;
  margin-top: 1px;
  display: grid; place-items: center;
  border-radius: 50%;
  background: var(--bad-fg);
  color: var(--surface);
  font-size: 11px;
  font-weight: 700;
  line-height: 1;
}
.error-msg.is-visible { display: flex; }

/* Field groups */
.field-row {
  display: grid;
  gap: var(--s4);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 200px), 1fr));
}

fieldset {
  margin: 0 0 var(--s5);
  padding: var(--s5);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  background: var(--surface-alt);
  /* Browsers give a fieldset an implicit min-width of min-content, so it
     refuses to shrink below whatever the widest thing inside it needs. Put
     a wide table in one and the fieldset — and the page with it — scrolls
     sideways, even though the table sits in an overflow-auto wrapper. This
     is the standard fix and it has no other effect. */
  min-width: 0;
}
legend {
  padding-inline: var(--s2);
  margin-left: calc(var(--s2) * -1);
  font-weight: 650;
  font-size: 0.95rem;
  color: var(--text);
}

/* Checkbox and radio */
.check {
  display: flex;
  align-items: flex-start;
  gap: var(--s3);
  min-height: 44px;
  padding: var(--s2) 0;
  cursor: pointer;
  font-size: 0.94rem;
  line-height: 1.5;
}
.check input[type="checkbox"], .check input[type="radio"] {
  flex-shrink: 0;
  width: 20px; height: 20px;
  margin: 2px 0 0;
  accent-color: var(--accent);
  cursor: pointer;
}

/* Segmented control, used for calculator modes */
.segmented {
  display: flex;
  flex-wrap: wrap;
  gap: 3px;
  padding: 3px;
  background: var(--surface-sunk);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
}
.segmented input { position: absolute; opacity: 0; pointer-events: none; }
.segmented label {
  flex: 1 1 auto;
  min-width: 100px;
  min-height: 40px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--s2) var(--s3);
  border-radius: calc(var(--r-md) - 3px);
  font-size: 0.9rem;
  font-weight: 600;
  color: var(--text-soft);
  text-align: center;
  cursor: pointer;
  transition: background-color .14s, color .14s;
}
.segmented label:hover { color: var(--text); }
.segmented input:checked + label {
  background: var(--surface);
  color: var(--accent);
  box-shadow: var(--shadow-sm);
}
.segmented input:focus-visible + label {
  outline: 3px solid var(--focus);
  outline-offset: 2px;
}

/* Range slider, used for the questionnaire calculators */
.range-field { display: grid; gap: var(--s2); }
.range-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--s3);
}
.range-value {
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--accent);
  font-size: 1.05rem;
}
input[type="range"] {
  width: 100%;
  height: 44px;
  background: transparent;
  cursor: pointer;
  accent-color: var(--accent);
}

/* --------------------------------------------------------------------
   Denomination grid

   Notes and coins are grouped separately, because that is how physical
   cash is counted and how a till is reconciled.
   ------------------------------------------------------------------ */

.denom-group + .denom-group { margin-top: var(--s5); }

.denom-group-title {
  margin-bottom: var(--s3);
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--text-faint);
}

.denom-grid {
  display: grid;
  gap: var(--s3);
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 130px), 1fr));
}

.denom-item { min-width: 0; }

.denom-label {
  display: flex;
  align-items: center;
  gap: var(--s2);
  margin-bottom: var(--s2);
  font-weight: 650;
  font-size: 0.95rem;
  color: var(--text);
}

.denom-face { font-variant-numeric: tabular-nums; }

.denom-tag { font-size: 0.66rem; padding: 2px 6px; }

/* Quantity fields are short by nature; centring the digits makes a long
   row of them far easier to scan for a mistake. */
.denom-item .input { text-align: center; padding-inline: var(--s2); }

/* --------------------------------------------------------------------
   Experience timeline

   Each spell drawn as a proportional bar on a shared axis, so overlaps
   and gaps are visible at a glance rather than inferred from dates.
   ------------------------------------------------------------------ */

.tl-row {
  display: grid;
  grid-template-columns: minmax(90px, 22%) 1fr;
  align-items: center;
  gap: var(--s3);
  margin-bottom: var(--s2);
}

.tl-label {
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--text-soft);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.tl-track {
  position: relative;
  display: block;
  height: 22px;
  background: var(--surface-sunk);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  overflow: hidden;
}

.tl-bar {
  position: absolute;
  top: 0;
  bottom: 0;
  min-width: 3px;
  border-radius: 3px;
  background: linear-gradient(90deg, var(--meter-from), var(--meter-to));
}

.tl-axis {
  display: flex;
  justify-content: space-between;
  margin-top: var(--s2);
  padding-left: calc(22% + var(--s3));
  font-size: 0.78rem;
  font-variant-numeric: tabular-nums;
  color: var(--text-faint);
}

@media (max-width: 559px) {
  .tl-row { grid-template-columns: 1fr; gap: var(--s1); }
  .tl-axis { padding-left: 0; }
}

/* Resume-ready output lines */
.resume-line {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s3);
  padding: var(--s3) var(--s4);
  background: var(--surface-alt);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  font-size: 0.95rem;
}
.resume-line + .resume-line { margin-top: var(--s2); }
.resume-line span { font-weight: 600; }

/* --------------------------------------------------------------------
   Questionnaire sliders
   ------------------------------------------------------------------ */

.q-item {
  padding: var(--s4);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
}
.q-item + .q-item { margin-top: var(--s3); }

.q-title {
  margin: 0 0 var(--s3);
  font-weight: 600;
  font-size: 0.96rem;
  color: var(--text);
}

.q-cols {
  display: grid;
  gap: var(--s4);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr));
}
.q-col[hidden] { display: none !important; }

.q-who {
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--text-soft);
}

.q-scale {
  display: flex;
  justify-content: space-between;
  font-size: 0.78rem;
  color: var(--text-faint);
}

/* --------------------------------------------------------------------
   Radar chart

   Inline SVG, drawn by hand. Eight points on a polygon does not justify
   a charting library, and inline SVG inherits the theme tokens.
   ------------------------------------------------------------------ */

.radar-wrap {
  display: grid;
  gap: var(--s5);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr));
  align-items: center;
}

.radar { width: 100%; max-width: 340px; margin-inline: auto; display: block; }
.radar-ring  { fill: none; stroke: var(--line); stroke-width: 1; }
.radar-spoke { stroke: var(--line); stroke-width: 1; }
.radar-shape {
  /* fill + fill-opacity rather than color-mix(): it needs no @supports
     fallback and, unlike a baked rgba(), it follows the accent token so
     the shape stays visible when the theme flips. */
  fill: var(--accent);
  fill-opacity: .22;
  stroke: var(--accent);
  stroke-width: 2;
  stroke-linejoin: round;
}
.radar-dot { fill: var(--accent); }

.radar-legend { list-style: none; margin: 0; padding: 0; }
.radar-key {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s3);
  padding: var(--s2) 0;
  border-bottom: 1px dashed var(--line);
  font-size: 0.9rem;
}
.radar-key:last-child { border-bottom: 0; }
.radar-key span { color: var(--text-soft); }
.radar-key strong { font-variant-numeric: tabular-nums; }

/* Dynamic rows (add subject / add job / add player) */
.rows { display: grid; gap: var(--s3); }

.row-item {
  display: grid;
  gap: var(--s3);
  padding: var(--s4);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
}
.row-item .row-head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--s3);
}
.row-index {
  font-size: 0.8rem;
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--text-faint);
}

.row-remove {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px; height: 36px;
  background: transparent;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  color: var(--text-faint);
  cursor: pointer;
  transition: color .14s, border-color .14s, background-color .14s;
}
.row-remove:hover { color: var(--bad-fg); border-color: var(--bad-line); background: var(--bad-bg); }
.row-remove svg { width: 16px; height: 16px; }
.row-remove:disabled { opacity: .35; cursor: not-allowed; }

/* Rows that nest — a semester holding its own list of courses (the CGPA
   calculators). The outer block sits on the sunk surface so the inner
   course rows, which keep the normal white surface, stay legible as the
   foreground layer rather than reading as a second identical box. */
.row-group {
  display: grid;
  gap: var(--s3);
  padding: var(--s4);
  background: var(--surface-sunk);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
}
.row-group > .rows { gap: var(--s2); }
.row-group .row-item { padding: var(--s3) var(--s4); }
.row-group-head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--s3);
}
.row-group-title {
  font-size: 0.95rem;
  font-weight: 650;
  color: var(--text);
}
.row-group-meta {
  font-size: 0.82rem;
  color: var(--text-faint);
  font-variant-numeric: tabular-nums;
}

/* Trend chart (SRM CGPA). Inline SVG built in JS — two polylines, a grid
   and labels. No charting library: it would be a network dependency and a
   large one for something this small. Every value drawn here is also in
   the table directly beneath the chart, which is what screen-reader and
   print users get.

   Colours are the accent for the cumulative line and the faint text tone
   for the semester line, so the cumulative figure — the one the page is
   about — reads as the foreground series. Both are distinguished by dot
   size and position as well as hue, not by colour alone. */
.chart-wrap {
  margin-block: var(--s4);
  padding: var(--s3);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
}
.trend-chart { display: block; width: 100%; height: auto; }

.chart-grid { stroke: var(--line); stroke-width: 1; }
.chart-label { fill: var(--text-faint); font-size: 12px; font-family: var(--font-sans); }
.chart-empty { fill: var(--text-faint); font-size: 14px; font-family: var(--font-sans); }

.chart-line-cgpa { fill: none; stroke: var(--accent); stroke-width: 2.5; stroke-linejoin: round; stroke-linecap: round; }
.chart-line-cgpa-dot { fill: var(--accent); }
.chart-line-sgpa { fill: none; stroke: var(--text-faint); stroke-width: 2; stroke-dasharray: 5 4; stroke-linejoin: round; }
.chart-line-sgpa-dot { fill: var(--surface); stroke: var(--text-faint); stroke-width: 2; }

/* Function curve (Zeros Calculator). Same inline-SVG-in-JS approach as the
   trend chart above, reusing .chart-wrap/.trend-chart/.chart-grid for the
   frame and axes so a second chart style was not needed for one page. */
.chart-line-zeros { fill: none; stroke: var(--accent); stroke-width: 2.5; stroke-linejoin: round; stroke-linecap: round; }
.chart-zero-dot { fill: var(--surface); stroke: var(--accent); stroke-width: 2.5; }

.chart-key {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s4);
  margin-bottom: var(--s4);
  font-size: 0.85rem;
  color: var(--text-soft);
}
.chart-key-item { display: inline-flex; align-items: center; gap: var(--s2); }
.chart-swatch { width: 18px; height: 3px; border-radius: 2px; flex: 0 0 auto; }
.chart-swatch-cgpa { background: var(--accent); }
.chart-swatch-sgpa {
  background: repeating-linear-gradient(90deg, var(--text-faint) 0 5px, transparent 5px 9px);
}

/* Grade-swap rows (VIT GPA). One line per subject: the name on the left,
   a grade dropdown on the right. Deliberately more compact than a normal
   .row-item, because this is a comparison list rather than a form the
   visitor is filling in for the first time. */
.swap-row { padding: var(--s2) var(--s3); }

.swap-line {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s3);
  flex-wrap: wrap;
}
.swap-name {
  flex: 1 1 12rem;
  min-width: 0;
  font-size: 0.95rem;
  font-weight: 600;
  color: var(--text);
}
.swap-credits {
  font-weight: 500;
  color: var(--text-faint);
  font-variant-numeric: tabular-nums;
}
.select-inline { width: auto; min-width: 8.5rem; flex: 0 0 auto; }

@media (max-width: 420px) {
  .swap-line { flex-wrap: nowrap; }
  .select-inline { min-width: 7rem; }
}

.swap-summary { margin-top: var(--s4); }

/* Marks the visitor's own input where it sits inside a table of computed
   values — the starting price in the Gann level ladder. Uses a left border
   as well as a tint, so it is not signalled by colour alone. */
tr.row-highlight > th,
tr.row-highlight > td { background: var(--accent-soft); font-weight: 650; }
tr.row-highlight > th:first-child { box-shadow: inset 3px 0 0 var(--accent); }

@media (max-width: 767px) {
  table.stacked tr.row-highlight {
    background: var(--accent-soft);
    border-radius: var(--r-sm);
    box-shadow: inset 3px 0 0 var(--accent);
  }
}

/* A small qualifier inside a table header — "supplied" on the one Shadbala
   column the page fills in itself. Block so it sits under the heading
   rather than widening an already-tight column. */
.th-note {
  display: block;
  font-size: 0.72rem;
  font-weight: 500;
  letter-spacing: 0;
  text-transform: none;
  /* --accent-strong, not --accent: at 11.5px on the sunk table-header
     ground the plain accent measures 4.22:1, a shade under AA. */
  color: var(--accent-strong);
}

/* A fieldset nested inside another, for a group of related toggles that
   needs its own label without breaking out of the parent's box. */
.fieldset-inner {
  margin-top: var(--s4);
  padding: var(--s3) var(--s4) var(--s4);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--surface-sunk);
}
.legend-sm {
  padding-inline: var(--s2);
  font-size: 0.8rem;
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--text-soft);
}

/* Checkbox chips — a multi-select that reads as a row of toggles rather
   than a stack of checkboxes. The input stays in the accessibility tree
   and keeps keyboard focus; only its default box is hidden. */
.chip-row-form { justify-content: flex-start; margin-top: var(--s2); }

.chip-toggle { cursor: pointer; user-select: none; }
.chip-toggle input {
  position: absolute;
  width: 1px; height: 1px;
  opacity: 0;
  pointer-events: none;
}
.chip-toggle input:checked + span {
  color: var(--accent);
  font-weight: 650;
}
.chip-toggle:has(input:checked) {
  border-color: var(--accent);
  background: var(--accent-soft);
  color: var(--accent);
}
.chip-toggle:has(input:focus-visible) {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

/* A hint used as a sub-heading inside a fieldset, where a second <legend>
   is not valid HTML. */
.hint-head {
  margin-top: var(--s5);
  margin-bottom: var(--s3);
  font-size: 0.8rem;
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--text-soft);
}

/* ==========================================================================
   8. CARDS
   ========================================================================== */

.card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  padding: var(--s5);
}
.card-flush { padding: 0; overflow: hidden; }

/* The listing cards — calculator, tool and category — live together in
   section 30, where both identities are defined side by side. */

/* --------------------------------------------------------------------
   Calculator page head

   Icon, title and one-sentence description, immediately under the
   breadcrumb. The icon is decorative and repeats the one on the tool
   card, so the page a visitor arrives on looks like the card they
   clicked. It is aria-hidden: the h1 already names the page.

   The lede keeps the reading measure of an article even though the
   calculator below it runs the full content width — a description is
   prose, and prose set to 1100px is unreadable.
   ------------------------------------------------------------------ */

.calc-head {
  display: flex;
  align-items: flex-start;
  gap: var(--s4);
  padding-block: var(--s5) var(--s6);
}

.calc-head-icon {
  display: grid;
  place-items: center;
  flex-shrink: 0;
  width: 52px; height: 52px;
  margin-top: 4px;
  background-color: var(--accent);
  background-image: var(--grad-calc);
  border: 0;
  border-radius: var(--r-lg);
  color: var(--on-accent);
  box-shadow: var(--glow-calc);
}
.calc-head-icon svg { width: 26px; height: 26px; }

.calc-head-text { min-width: 0; }
.calc-head h1 { margin-bottom: var(--s3); }
.calc-head .lede { margin-bottom: 0; }

/* Side by side costs about 70px of line length, which a phone cannot
   spare, so the icon moves above the title. */
@media (max-width: 479px) {
  .calc-head { flex-direction: column; gap: var(--s3); }
  .calc-head-icon { width: 44px; height: 44px; margin-top: 0; }
  .calc-head-icon svg { width: 22px; height: 22px; }
}

/* ==========================================================================
   9. RESULT PANEL
   ========================================================================== */

/* Hidden until a calculation succeeds. */
.result[hidden] { display: none !important; }

/* The result is the reason the page exists, so it is the one panel that
   is allowed to be emphatic: a gradient cap along the top edge, a tinted
   wash behind the number and a glow that lifts it out of the form above.
   Everything else on a calculator page stays quiet so this can be loud.

   The cap is a ::before rather than a border-image so it survives the
   overflow:hidden clip and follows --grad-calc, which the tools scope
   remaps - a date tool's result is violet without a second rule. */
.result {
  position: relative;
  margin-top: var(--s5);
  border: 1px solid var(--accent-line);
  border-radius: var(--r-lg);
  background: var(--surface);
  box-shadow: var(--glow-calc);
  overflow: hidden;
}
.result::before {
  content: "";
  position: absolute;
  inset-block-start: 0;
  inset-inline: 0;
  height: 3px;
  background: var(--grad-calc);
}

.result-primary {
  padding: var(--s6) var(--s5);
  text-align: center;
  background:
    linear-gradient(180deg, var(--accent-soft), transparent 78%),
    var(--surface);
  border-bottom: 1px solid var(--line);
}

/* The number itself gets a short count-in: it scales up from 98% as the
   panel arrives, which reads as the figure settling rather than as an
   effect. Tabular numerals keep the width stable while it moves. */
.result-value { font-variant-numeric: tabular-nums; }
.result:not([hidden]) .result-value {
  animation: ecs-result-num var(--dur-slow) var(--ease) both;
  animation-delay: .06s;
}
@keyframes ecs-result-num {
  from { opacity: 0; transform: scale(.98); }
  to   { opacity: 1; transform: none; }
}

.result-label {
  display: block;
  font-size: 0.82rem;
  font-weight: 650;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-faint);
  margin-bottom: var(--s2);
}

.result-value {
  display: block;
  font-size: clamp(2rem, 1.3rem + 3.2vw, 3.1rem);
  font-weight: 750;
  line-height: 1.05;
  letter-spacing: -0.035em;
  color: var(--accent);
  word-break: break-word;
}
.result-value .unit {
  font-size: 0.42em;
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--text-soft);
  margin-left: 0.15em;
}

.result-sub {
  margin-top: var(--s3);
  font-size: 1rem;
  color: var(--text-soft);
  max-width: 46ch;
  margin-inline: auto;
}

.result-body { padding: var(--s5); }
.result-body > * + * { margin-top: var(--s5); }

.result-section-title {
  font-size: 0.82rem;
  font-weight: 700;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--text-faint);
  margin-bottom: var(--s3);
  padding-bottom: var(--s2);
  border-bottom: 1px solid var(--line);
}

/* Stat grid used across result panels */
.stats {
  display: grid;
  gap: var(--s3);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 140px), 1fr));
}
.stat {
  padding: var(--s3) var(--s4);
  background: var(--surface-alt);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
}
.stat-label {
  display: block;
  font-size: 0.78rem;
  font-weight: 600;
  color: var(--text-faint);
  margin-bottom: var(--s1);
  line-height: 1.35;
}
.stat-value {
  display: block;
  font-size: 1.2rem;
  font-weight: 700;
  letter-spacing: -0.02em;
  color: var(--text);
  line-height: 1.25;
}
.stat-value .unit {
  font-size: 0.7rem;
  font-weight: 600;
  color: var(--text-faint);
  margin-left: 3px;
}

/* Meter for percentage-style results */
.meter {
  height: 12px;
  background: var(--surface-sunk);
  border: 1px solid var(--line);
  border-radius: var(--r-full);
  overflow: hidden;
}
.meter-fill {
  height: 100%;
  width: 0;
  border-radius: var(--r-full);
  background: linear-gradient(90deg, var(--meter-from), var(--meter-to));
  transition: width .7s cubic-bezier(.22,.9,.3,1);
}
@media (prefers-reduced-motion: reduce) { .meter-fill { transition: none; } }

.bar-list { display: grid; gap: var(--s3); }
.bar-item { display: grid; gap: 5px; }
.bar-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--s3);
  font-size: 0.88rem;
}
.bar-name { font-weight: 550; color: var(--text-soft); }
.bar-val { font-weight: 700; font-variant-numeric: tabular-nums; color: var(--text); }

/* Empty state shown before the first calculation */
.result-empty {
  margin-top: var(--s5);
  padding: var(--s6) var(--s5);
  text-align: center;
  border: 1px dashed var(--line-strong);
  border-radius: var(--r-lg);
  background: var(--surface-alt);
  color: var(--text-faint);
}
.result-empty svg {
  width: 30px; height: 30px;
  margin: 0 auto var(--s3);
  color: var(--text-faint);
  opacity: .7;
}
.result-empty p { margin: 0; font-size: 0.93rem; }

/* Form-level error summary, focused on submit failure */
.form-error {
  display: none;
  gap: var(--s3);
  margin-bottom: var(--s4);
  padding: var(--s4);
  background: var(--bad-bg);
  border: 1px solid var(--bad-line);
  border-left: 3px solid var(--bad-fg);
  border-radius: var(--r-md);
  color: var(--bad-fg);
}
.form-error.is-visible { display: block; }
.form-error strong { display: block; margin-bottom: var(--s1); }
.form-error ul { margin: 0; padding-left: var(--s5); font-size: 0.9rem; }
.form-error a { color: inherit; }

/* ==========================================================================
   10. TABLES
   ========================================================================== */

.table-wrap {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--surface);
}

table.data {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.9rem;
}
table.data caption {
  padding: var(--s3) var(--s4);
  text-align: left;
  font-size: 0.85rem;
  color: var(--text-faint);
  border-bottom: 1px solid var(--line);
}
table.data th, table.data td {
  padding: var(--s3) var(--s4);
  text-align: left;
  border-bottom: 1px solid var(--line);
  vertical-align: top;
}
table.data thead th {
  background: var(--surface-sunk);
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.045em;
  text-transform: uppercase;
  color: var(--text-soft);
  white-space: nowrap;
}
table.data tbody tr:last-child td { border-bottom: 0; }
table.data tbody tr:hover { background: var(--surface-alt); }
table.data td.num, table.data th.num { text-align: right; white-space: nowrap; }

/* Right-aligned like .num, but for values that are phrases rather than
   figures ("13 years, 5 months and 21 days"). nowrap on those forces the
   table far wider than a phone screen. */
table.data td.num-wrap, table.data th.num-wrap {
  text-align: right;
  white-space: normal;
}
table.data tfoot td {
  background: var(--surface-sunk);
  font-weight: 700;
  border-top: 2px solid var(--line-strong);
  border-bottom: 0;
}

/* Below 640px, stacked tables become label/value pairs so nothing is
   cut off and no horizontal scroll is needed. */
@media (max-width: 639px) {
  /* The table's own box must become a block too. Left as a table it keeps
     its min-content width and the rows never collapse, however the cells
     are styled. */
  table.stacked,
  table.stacked tbody,
  table.stacked tfoot { display: block; width: 100%; }

  table.stacked caption { display: block; }

  table.stacked thead {
    position: absolute;
    width: 1px; height: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
  }

  table.stacked tbody tr,
  table.stacked tfoot tr {
    display: block;
    padding: var(--s3) 0;
    border-bottom: 1px solid var(--line);
  }
  table.stacked tbody tr:last-child { border-bottom: 0; }

  /* A row header becomes the heading for its group of values. Styling
     only td left this as a table-cell, which is what kept the whole
     table from collapsing. */
  table.stacked th[scope="row"] {
    display: block;
    border: 0;
    padding: 0 var(--s4) var(--s2);
    text-align: left;
    font-size: 0.92rem;
    font-weight: 650;
    color: var(--text);
    white-space: normal;
  }

  table.stacked td {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: var(--s4);
    border: 0;
    padding: var(--s1) var(--s4);
    text-align: right;
    white-space: normal;
  }
  table.stacked td::before {
    content: attr(data-label);
    flex-shrink: 0;
    font-size: 0.8rem;
    font-weight: 650;
    color: var(--text-faint);
    text-align: left;
  }
  /* Rows built without a data-label have nothing to show on the left. */
  table.stacked td:not([data-label])::before { content: none; }

  table.stacked tfoot td { border-top: 1px solid var(--line-strong); }
}

/* ==========================================================================
   11. DISCLOSURE & ACCORDION
   ========================================================================== */

.disclosure {
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--surface);
  overflow: hidden;
}
.disclosure + .disclosure { margin-top: var(--s2); }

.disclosure > summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s3);
  padding: var(--s4);
  min-height: 52px;
  font-weight: 600;
  font-size: 0.95rem;
  cursor: pointer;
  list-style: none;
  background: var(--surface);
  transition: background-color .14s;
}
.disclosure > summary::-webkit-details-marker { display: none; }
.disclosure > summary:hover { background: var(--surface-alt); }
.disclosure > summary:focus-visible { outline: 3px solid var(--focus); outline-offset: -3px; }

.disclosure > summary::after {
  content: "";
  flex-shrink: 0;
  width: 10px; height: 10px;
  border-right: 2px solid var(--text-faint);
  border-bottom: 2px solid var(--text-faint);
  transform: rotate(45deg) translate(-2px, -2px);
  transition: transform .18s;
}
.disclosure[open] > summary::after { transform: rotate(-135deg) translate(-2px, -2px); }
.disclosure[open] > summary { border-bottom: 1px solid var(--line); }

.disclosure-body { padding: var(--s4); }
.disclosure-body > *:last-child { margin-bottom: 0; }

/* "How was this calculated?" audit trail */
.audit { font-size: 0.89rem; }
.audit dl { display: grid; gap: 0; margin: 0; }
.audit-row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--s4);
  padding: var(--s2) 0;
  border-bottom: 1px dashed var(--line);
}
.audit-row:last-child { border-bottom: 0; }
.audit-row dt { color: var(--text-soft); }
.audit-row dd {
  margin: 0;
  font-weight: 650;
  font-variant-numeric: tabular-nums;
  text-align: right;
}
.audit-formula {
  margin: var(--s3) 0;
  padding: var(--s3) var(--s4);
  background: var(--surface-sunk);
  border-left: 3px solid var(--accent);
  border-radius: var(--r-sm);
  font-family: var(--font-mono);
  font-size: 0.85rem;
  line-height: 1.65;
  overflow-x: auto;
  /* The formula is written one step per line in the markup. Without this
     the lines collapse into a single unreadable run-on. pre-wrap rather
     than pre so long lines still fold on a narrow screen. */
  white-space: pre-wrap;
  tab-size: 2;
}
.audit-total {
  margin-top: var(--s3);
  padding-top: var(--s3);
  border-top: 2px solid var(--line-strong);
  font-weight: 700;
}

/* FAQ list */
.faq-list { display: grid; gap: var(--s2); }
.faq-list .disclosure + .disclosure { margin-top: 0; }
.faq-list summary { font-size: 1rem; }

/* ==========================================================================
   12. BREADCRUMBS
   ========================================================================== */

.breadcrumb { padding-block: var(--s4) 0; }
.breadcrumb ol {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s1);
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: 0.85rem;
  color: var(--text-faint);
}
.breadcrumb li { display: flex; align-items: center; gap: var(--s1); }
.breadcrumb li + li::before {
  content: "/";
  color: var(--line-strong);
  margin-right: var(--s1);
}
.breadcrumb a { color: var(--text-soft); text-decoration: none; }
.breadcrumb a:hover { color: var(--accent); text-decoration: underline; }
.breadcrumb [aria-current="page"] { color: var(--text); font-weight: 550; }

/* ==========================================================================
   13. ARTICLE & PROSE
   ========================================================================== */

/* The long-form article is a CENTRED reading column, not the full
   calculator width.

   The calculator above it deliberately uses the whole 1140px container —
   it needs the room for input rows, result tables and comparisons. The
   article must not. Capping only `.prose` left the column hard against
   the left edge of the container with roughly 500px of empty gutter
   beside it, which is what this rule fixes: the cap moves up to the
   <article> so the heading, the table of contents and the prose are all
   inside one centred column.

   The article measure is a little wider than `--measure` so that the
   comparison tables inside have room to breathe, while still keeping
   lines short enough to read comfortably. */
article.section {
  max-width: var(--measure-article);
  margin-inline: auto;
}
/* Its own cap would re-introduce a left-aligned block inside the column
   that now sets the width. */
article.section > .prose { max-width: none; }

.prose {
  max-width: var(--measure);
  font-size: 1.02rem;
  line-height: 1.75;
  color: var(--text);
}
.prose > * + * { margin-top: var(--s4); }

.prose h2 {
  margin-top: var(--s7);
  padding-top: var(--s5);
  border-top: 1px solid var(--line);
  font-size: var(--step-2);
  scroll-margin-top: calc(var(--header-h) + var(--s4));
}
.prose h2:first-child { margin-top: 0; padding-top: 0; border-top: 0; }

.prose h3 {
  margin-top: var(--s6);
  font-size: var(--step-1);
  scroll-margin-top: calc(var(--header-h) + var(--s4));
}
.prose h4 {
  margin-top: var(--s5);
  font-size: 1.02rem;
  font-weight: 650;
  color: var(--text);
}

.prose ul, .prose ol { margin: 0 0 var(--s4); padding-left: var(--s5); }
.prose li { margin-bottom: var(--s2); }
.prose li:last-child { margin-bottom: 0; }
.prose li > ul, .prose li > ol { margin-top: var(--s2); }

.prose blockquote {
  margin: var(--s5) 0;
  padding: var(--s3) var(--s5);
  border-left: 3px solid var(--accent);
  background: var(--accent-soft);
  border-radius: 0 var(--r-md) var(--r-md) 0;
  color: var(--text-soft);
  font-size: 0.98rem;
}
.prose blockquote p:last-child { margin-bottom: 0; }

.prose .table-wrap { margin-block: var(--s5); max-width: none; }

/* Worked examples get a distinct, slightly technical treatment. */
.example {
  margin: var(--s5) 0;
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--surface);
  overflow: hidden;
}
.example-head {
  padding: var(--s3) var(--s4);
  background: var(--surface-sunk);
  border-bottom: 1px solid var(--line);
  font-size: 0.8rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-soft);
}
.example-body { padding: var(--s4); font-size: 0.96rem; }
.example-body > *:last-child { margin-bottom: 0; }
.example-body p { margin-bottom: var(--s3); }

.step-list { counter-reset: step; list-style: none; padding-left: 0; }
.step-list > li {
  counter-increment: step;
  position: relative;
  padding-left: var(--s7);
  margin-bottom: var(--s4);
}
.step-list > li::before {
  content: counter(step);
  position: absolute;
  left: 0;
  top: 1px;
  width: 26px; height: 26px;
  display: grid; place-items: center;
  border-radius: 50%;
  background: var(--accent-soft);
  border: 1px solid var(--accent-line);
  color: var(--accent);
  font-size: 0.8rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

/* ==========================================================================
   14. TABLE OF CONTENTS
   ========================================================================== */

.toc {
  margin-bottom: var(--s6);
  padding: var(--s4) var(--s5);
  background: var(--surface-alt);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
}
.toc > summary {
  font-weight: 650;
  cursor: pointer;
  list-style: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s3);
}
.toc > summary::-webkit-details-marker { display: none; }
.toc > summary::after {
  content: "";
  width: 9px; height: 9px;
  border-right: 2px solid var(--text-faint);
  border-bottom: 2px solid var(--text-faint);
  transform: rotate(45deg) translate(-2px, -2px);
  transition: transform .18s;
}
.toc[open] > summary::after { transform: rotate(-135deg) translate(-2px, -2px); }

.toc ol {
  margin: var(--s4) 0 0;
  padding-left: var(--s5);
  columns: 2;
  column-gap: var(--s6);
  font-size: 0.92rem;
}
@media (max-width: 639px) { .toc ol { columns: 1; } }
.toc li { margin-bottom: var(--s2); break-inside: avoid; }
.toc a { color: var(--text-soft); text-decoration: none; }
.toc a:hover { color: var(--accent); text-decoration: underline; }

/* ==========================================================================
   15. CALLOUTS & DISCLAIMER
   ========================================================================== */

.callout {
  display: flex;
  gap: var(--s3);
  padding: var(--s4);
  border-radius: var(--r-md);
  border: 1px solid var(--info-line);
  background: var(--info-bg);
  color: var(--info-fg);
  font-size: 0.94rem;
  line-height: 1.6;
}
.callout > svg { flex-shrink: 0; width: 19px; height: 19px; margin-top: 2px; }
.callout p:last-child { margin-bottom: 0; }
.callout strong { color: inherit; }
.callout a { color: inherit; font-weight: 600; }

.callout-warn { border-color: var(--warn-line); background: var(--warn-bg); color: var(--warn-fg); }
.callout-ok   { border-color: var(--ok-line);   background: var(--ok-bg);   color: var(--ok-fg); }
.callout-bad  { border-color: var(--bad-line);  background: var(--bad-bg);  color: var(--bad-fg); }

/* Per-calculator disclaimer. Deliberately visible, not buried. */
.disclaimer {
  margin-top: var(--s6);
  padding: var(--s5);
  border: 1px solid var(--warn-line);
  border-left: 4px solid var(--warn-fg);
  border-radius: var(--r-md);
  background: var(--warn-bg);
  color: var(--warn-fg);
  font-size: 0.93rem;
  line-height: 1.65;
}
.disclaimer h2, .disclaimer h3 {
  margin: 0 0 var(--s2);
  font-size: 0.96rem;
  font-weight: 700;
  color: inherit;
  border: 0;
  padding: 0;
}
.disclaimer p:last-child { margin-bottom: 0; }

/* ==========================================================================
   16. FOOTER
   ========================================================================== */

.site-footer {
  margin-top: var(--s8);
  padding-top: var(--s7);
  background: var(--surface-sunk);
  border-top: 1px solid var(--line);
  font-size: 0.92rem;
}

/* The brand spans the top on a phone and a tablet, then takes the first
   column beside the five link columns from 1024px. */
.footer-grid {
  display: grid;
  gap: var(--s6) var(--s5);
  grid-template-columns: repeat(2, minmax(0, 1fr));
  padding-bottom: var(--s6);
}
.footer-about { grid-column: 1 / -1; max-width: 40ch; }

@media (min-width: 720px) {
  .footer-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@media (min-width: 1024px) {
  .footer-grid { grid-template-columns: 1.3fr repeat(5, minmax(0, 1fr)); }
  .footer-about { grid-column: auto; }
}

.footer-about p {
  margin-top: var(--s3);
  color: var(--text-soft);
  font-size: 0.9rem;
  line-height: 1.6;
}

.footer-social {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
  margin: var(--s4) 0 0;
  padding: 0;
  list-style: none;
}
.footer-social a {
  display: inline-flex;
  align-items: center;
  min-height: 36px;
  padding: 4px var(--s3);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-full);
  color: var(--text-soft);
  font-size: 0.85rem;
  font-weight: 600;
  text-decoration: none;
}
.footer-social a:hover { color: var(--text); border-color: var(--text-faint); }

.footer-col h2 {
  margin-bottom: var(--s3);
  font-size: 0.95rem;
  font-weight: 700;
  letter-spacing: -0.01em;
  color: var(--text);
}
.footer-col ul {
  display: grid;
  gap: 4px;
  margin: 0;
  padding: 0;
  list-style: none;
}
/* Padded rather than tightly set, so every footer link is a comfortable
   target on a phone without the column looking spaced out. */
.footer-col a {
  display: inline-block;
  padding-block: 3px;
  color: var(--text-soft);
  text-decoration: none;
  font-size: 0.9rem;
  line-height: 1.45;
}
.footer-col a:hover { color: var(--text); text-decoration: underline; }
.footer-col-calc a:hover { color: var(--calc-strong); }
.footer-col-tool a:hover { color: var(--tool-strong); }
.footer-all a { font-weight: 650; }
.footer-col-calc .footer-all a { color: var(--calc-strong); }
.footer-col-tool .footer-all a { color: var(--tool-strong); }

.footer-bottom {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--s3);
  padding-block: var(--s4);
  border-top: 1px solid var(--line);
  color: var(--text-faint);
  font-size: 0.85rem;
}
.footer-bottom ul {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2) var(--s4);
  list-style: none;
  margin: 0;
  padding: 0;
}
.footer-bottom a {
  display: inline-block;
  padding-block: 3px;
  color: var(--text-faint);
  text-decoration: none;
}
.footer-bottom a:hover { color: var(--text); text-decoration: underline; }

/* ==========================================================================
   17. SEARCH
   ========================================================================== */

.search-hero {
  position: relative;
  max-width: 620px;
}

/* Input and button sit on one row; the field flexes, the button does not. */
.search-row { display: flex; gap: var(--s2); }
.search-field { position: relative; flex: 1 1 auto; min-width: 0; }

.search-hero .input {
  min-height: 52px;
  padding-left: var(--s8);
  padding-right: var(--s4);
  font-size: max(16px, 1.02rem);
  border-radius: var(--r-md);
  border-color: var(--line-strong);
  box-shadow: var(--shadow-sm);
  text-align: left;
}
.search-hero .search-icon {
  position: absolute;
  left: var(--s4);
  top: 50%;
  transform: translateY(-50%);
  width: 20px; height: 20px;
  color: var(--text-faint);
  pointer-events: none;
  z-index: 1;
}
.search-hero .btn { min-height: 52px; flex-shrink: 0; }

/* The search field is the fastest route to a calculator, so it behaves
   like a control rather than a box: it lifts and warms on focus. The
   glow follows the page accent, which means the tools hub search glows
   violet with no extra rule. */
.search-hero .input {
  transition: border-color var(--dur) var(--ease),
              box-shadow var(--dur) var(--ease),
              transform var(--dur) var(--ease);
}
.search-hero .input:hover { border-color: var(--text-faint); }
.search-hero .input:focus {
  transform: translateY(-1px);
  box-shadow: 0 0 0 3px var(--accent-soft), var(--glow-calc);
}
.search-hero .input:focus ~ .search-icon,
.search-hero .input:focus + .search-icon { color: var(--accent); }

/* The hero search is the primary action on the page, so it gets a
   slightly larger field and a visible submit button. */
.search-hero-lg { max-width: 640px; margin-inline: auto; }
.search-hero-lg .input { min-height: 60px; font-size: max(16px, 1.06rem); }
.search-hero-lg .btn { min-height: 60px; padding-inline: var(--s6); }

@media (max-width: 559px) {
  .search-hero-lg .btn { padding-inline: var(--s4); }
  .search-hero-lg .btn span { display: none; }
}

/* Live suggestion list.
   Each suggestion is a small card: the icon tile of the half of the site
   it belongs to, the name and one line, and the kind in words. */
.search-results {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  right: 0;
  z-index: 80;
  max-height: min(420px, 70vh);
  overflow-y: auto;
  padding: var(--s2);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-lg);
  list-style: none;
  margin: 0;
}
.search-results[hidden] { display: none; }
.search-results li + li { margin-top: 2px; }
.search-results a {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: 10px;
  padding: var(--s2) 10px;
  border-radius: var(--r-md);
  color: var(--text);
  text-decoration: none;
  line-height: 1.35;
}
.search-results a:hover,
.search-results a:focus,
.search-results [aria-selected="true"] a {
  background: var(--surface-sunk);
  color: var(--text);
}
.search-results .r-icon {
  width: 34px; height: 34px;
  display: grid; place-items: center;
  border-radius: 9px;
  background-color: var(--brand-600);
  background-image: var(--tile-calc);
  color: var(--on-tile);
}
.search-results .r-tool .r-icon { background-color: var(--violet-600); background-image: var(--tile-tool); }
.search-results .r-icon svg { width: 17px; height: 17px; }
.search-results .r-body { min-width: 0; }
.search-results .r-name { display: block; font-weight: 650; font-size: 0.93rem; }
.search-results .r-desc {
  display: block;
  margin-top: 1px;
  overflow: hidden;
  font-size: 0.8rem;
  color: var(--text-faint);
  text-overflow: ellipsis;
  white-space: nowrap;
}
.search-results .r-kind {
  padding: 2px 7px;
  border: 1px solid var(--calc-line);
  border-radius: var(--r-full);
  background: var(--calc-soft);
  font-size: 0.66rem;
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--calc-strong);
  white-space: nowrap;
}
.search-results .r-tool .r-kind {
  border-color: var(--tool-line);
  background: var(--tool-soft);
  color: var(--tool-strong);
}
.search-empty { padding: var(--s4); text-align: center; color: var(--text-faint); font-size: 0.9rem; }

/* The header variant: a pill-shaped field sized for the bar. It never
   lifts on focus — a moving control in a sticky bar reads as a glitch —
   and its suggestion list opens wider than the field, anchored to the
   end edge so it stays on screen. */
.search-compact { max-width: none; }
.search-compact .input {
  min-height: 40px;
  padding-left: 38px;
  padding-right: var(--s3);
  font-size: 0.9rem;
  border-color: var(--line);
  border-radius: var(--r-full);
  background: var(--surface-sunk);
  box-shadow: none;
}
.search-compact .search-icon { left: 13px; width: 16px; height: 16px; }
.search-compact .input:hover { border-color: var(--line-strong); }
.search-compact .input:focus {
  transform: none;
  background: var(--surface);
  border-color: var(--calc-line);
  box-shadow: 0 0 0 3px var(--calc-soft);
}
.search-compact .search-results {
  left: auto;
  right: 0;
  width: min(440px, calc(100vw - 2 * var(--s4)));
}

@media (max-width: 1079px) {
  /* 16px so iOS does not zoom the page when the field takes focus. */
  .search-compact .input { min-height: 46px; font-size: 16px; }
  .search-compact .search-results { left: 0; right: 0; width: auto; }
}

@media (max-width: 399px) {
  .search-results .r-kind { display: none; }
  .search-results a { grid-template-columns: auto minmax(0, 1fr); }
}

.chip-row {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  gap: var(--s2);
  margin-top: var(--s5);
}
.chip {
  display: inline-flex;
  align-items: center;
  min-height: 34px;
  padding: 6px var(--s3);
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-full);
  font-size: 0.86rem;
  font-weight: 550;
  color: var(--text-soft);
  text-decoration: none;
  transition: border-color .14s, color .14s, background-color .14s;
}
.chip:hover {
  border-color: var(--accent);
  color: var(--accent);
  background: var(--accent-soft);
}

/* ==========================================================================
   18. UTILITIES
   ========================================================================== */

.visually-hidden {
  position: absolute !important;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

.text-center { text-align: center; }
.text-soft { color: var(--text-soft); }
.text-faint { color: var(--text-faint); }
.measure { max-width: var(--measure); }
.mt-0 { margin-top: 0 !important; }
.mb-0 { margin-bottom: 0 !important; }
.mb-1 { margin-bottom: var(--s1); }
.mb-2 { margin-bottom: var(--s2); }
.mt-3 { margin-top: var(--s3); }
.mt-4 { margin-top: var(--s4); }
.mt-5 { margin-top: var(--s5); }
.mt-6 { margin-top: var(--s6); }

.badge {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 3px var(--s2);
  border-radius: var(--r-full);
  font-size: 0.76rem;
  font-weight: 650;
  letter-spacing: 0.02em;
  background: var(--surface-sunk);
  border: 1px solid var(--line);
  color: var(--text-soft);
}
.badge-ok  { background: var(--ok-bg);  border-color: var(--ok-line);  color: var(--ok-fg); }
.badge-bad { background: var(--bad-bg); border-color: var(--bad-line); color: var(--bad-fg); }
.badge-warn{ background: var(--warn-bg);border-color: var(--warn-line);color: var(--warn-fg); }

/* Transient "Copied" confirmation */
.toast {
  position: fixed;
  left: 50%;
  bottom: var(--s6);
  transform: translate(-50%, 12px);
  z-index: 300;
  padding: var(--s3) var(--s5);
  background: var(--toast-bg);
  color: var(--toast-fg);
  border-radius: var(--r-full);
  font-size: 0.9rem;
  font-weight: 550;
  box-shadow: var(--shadow-lg);
  opacity: 0;
  pointer-events: none;
  transition: opacity .2s, transform .2s;
}
.toast.is-visible { opacity: 1; transform: translate(-50%, 0); }

/* 404 */
.err-page { padding-block: var(--s9); text-align: center; }
.err-code {
  font-size: clamp(3.5rem, 2rem + 7vw, 6rem);
  font-weight: 800;
  letter-spacing: -0.05em;
  line-height: 1;
  color: var(--accent);
  opacity: .28;
}

/* ==========================================================================
   19. PRINT
   ========================================================================== */

@media print {
  .site-header, .site-footer, .breadcrumb, .btn-row, .toc,
  .menu-btn, .toast, .result-empty, .no-print { display: none !important; }

  body { background: #fff; color: #000; font-size: 11.5pt; }
  main { padding: 0; }
  .container { max-width: none; padding: 0; }

  .result { border: 1.5pt solid #000; break-inside: avoid; }
  .result-primary { background: none !important; }
  .result-value { color: #000; }

  .card, .stat, table.data { border-color: #999 !important; }
  .disclosure { border-color: #999; }
  .disclosure > summary::after { display: none; }
  /* Expand every collapsed section so a printed page is complete. */
  details { display: block !important; }
  details > *:not(summary) { display: block !important; }

  a[href^="http"]::after { content: " (" attr(href) ")"; font-size: 9pt; color: #555; }
  h2, h3 { break-after: avoid; }
  table, .example, .audit { break-inside: avoid; }
}

/* ==========================================================================
   20. THEME TOGGLE, MOTION & INTERACTION
   ========================================================================== */

/* --------------------------------------------------------------------------
   Theme toggle
   -------------------------------------------------------------------------- */

/* A two-position switch: the sun and the moon side by side, with the
   active theme's glyph on a raised thumb. It is still one button with one
   state (aria-pressed = dark is on), so nothing about how it is operated
   or announced changed. Which glyph is raised follows the resolved theme
   rather than aria-pressed, so it is right even before app.js runs. */
.theme-btn {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  flex: 0 0 auto;
  height: 38px;
  padding: 3px;
  background: var(--surface-sunk);
  border: 1px solid var(--line);
  border-radius: var(--r-full);
  color: var(--text-faint);
  cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease),
              color var(--dur-fast) var(--ease);
}
.theme-btn:hover { border-color: var(--line-strong); color: var(--text-soft); }
.theme-btn:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 2px;
}

.theme-btn-opt {
  display: grid;
  place-items: center;
  width: 30px; height: 30px;
  border-radius: 50%;
  transition: background-color var(--dur-fast) var(--ease),
              color var(--dur-fast) var(--ease),
              box-shadow var(--dur-fast) var(--ease);
}
.theme-btn svg { width: 16px; height: 16px; }

:root:not([data-theme="dark"]) .theme-btn-light,
:root[data-theme="dark"] .theme-btn-dark {
  background: var(--surface);
  color: var(--text);
  box-shadow: var(--shadow-sm);
}
:root[data-theme="dark"] .theme-btn-dark { color: var(--calc-strong); }

/* On a phone the bar is short of room, so the switch folds to a single
   round button showing the current theme's glyph. Same button, same
   label and state; only the inactive glyph is hidden. */
@media (max-width: 479px) {
  .theme-btn {
    justify-content: center;
    width: 40px; height: 40px;
    padding: 0;
  }
  .theme-btn-opt { display: none; }
  :root:not([data-theme="dark"]) .theme-btn-light,
  :root[data-theme="dark"] .theme-btn-dark {
    display: grid;
    background: transparent;
    box-shadow: none;
  }
  .theme-btn svg { width: 18px; height: 18px; }
}

/* --------------------------------------------------------------------------
   Theme swap — deliberately instant, and why

   The first attempt here cross-faded the colours. It produced a real bug:
   elements that declare their own `transition: background-color` for hover
   — .disclosure > summary, .chip, .nav-link, .row-remove, .tool-card-icon —
   do not reliably re-resolve when the custom property behind that colour
   changes. They kept painting the PREVIOUS theme background, so toggling
   to dark left white FAQ headers sitting in a dark page. A full page load
   was always correct, which is exactly why it only appeared when using the
   toggle. Re-declaring the transition during the swap did not fix it.

   So the swap now suppresses every transition for one frame, applies the
   theme, forces a reflow and releases. Every element takes its new value
   directly, with no chance of a stale one surviving.

   The trade-off is honest: the change is instantaneous rather than a fade.
   A correct instant swap beats a smooth one that leaves half the page in
   the wrong theme, and at this speed the eye reads it as clean rather than
   abrupt. Hover, reveal and result animations are untouched.
   -------------------------------------------------------------------------- */

:root.theme-instant *,
:root.theme-instant *::before,
:root.theme-instant *::after {
  transition: none !important;
  animation-duration: 0s !important;
}

/* --------------------------------------------------------------------------
   Entrance and reveal
   -------------------------------------------------------------------------- */

@keyframes ecs-rise {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: none; }
}

/* Hero content, staggered. Runs on load rather than on scroll because it
   is above the fold, where an observer would fire immediately anyway. */
.hero-enter > * {
  animation: ecs-rise var(--dur-slow) var(--ease) both;
}
.hero-enter > *:nth-child(1) { animation-delay: .02s; }
.hero-enter > *:nth-child(2) { animation-delay: .08s; }
.hero-enter > *:nth-child(3) { animation-delay: .14s; }
.hero-enter > *:nth-child(4) { animation-delay: .20s; }
.hero-enter > *:nth-child(5) { animation-delay: .26s; }
.hero-enter > *:nth-child(6) { animation-delay: .32s; }

/* The calculator page head gets the same rise, unstaggered: it is one
   block, and the calculator itself must not be delayed behind it. */
.calc-head { animation: ecs-rise var(--dur-slow) var(--ease) both; }

/* Scroll reveal. The hidden state applies only when JS is running: the
   .js class is set by app.js, so with scripting off every [data-reveal]
   element is simply visible. Content must never depend on an effect. */
/* The offset is declared on the NOT-yet-revealed state, and the revealed
   state declares no transform at all.
 *
 * It used to be the other way round: the hidden state set the offset and
 * `.is-revealed` parked the element at `transform: none`. That parking
 * rule is specificity 0,3,0 — higher than `.tool-card:hover` (0,2,0),
 * `.cat-card:hover` and every other hover lift on the site — so the
 * moment a card scrolled into view its hover transform stopped applying.
 * The lift, the icon nudge and the 3D tilt were all being written,
 * cascaded over, and silently discarded. Only cards that had never been
 * revealed still moved, which is why it looked like it worked.
 *
 * Inverting it fixes every one of them at once: `:not(.is-revealed)`
 * stops matching the instant the class lands, so a revealed element
 * carries no transform from this system and hover rules apply normally.
 * The transition still animates the change, because the transition lives
 * on the base selector below. */
.js [data-reveal]:not(.is-revealed) {
  opacity: 0;
  /* 18px and a plain fade was close to invisible on a laptop screen: by
     the time the eye reached the card it had already arrived. The longer
     travel plus the slight scale reads as the card coming forward, which
     is what makes the entrance legible rather than merely present. */
  transform: translateY(30px) scale(.975);
  will-change: transform, opacity;
}
.js [data-reveal] {
  transition: opacity var(--dur-reveal) var(--ease-out),
              transform var(--dur-reveal) var(--ease-out);
}

/* Six steps, not three. A 3-column grid runs 1-2-3 across the first row
   and 4-5-6 across the second, so the wave crosses the block on a
   diagonal instead of two rows arriving as two blocks. */
.js [data-reveal-delay="1"] { transition-delay: .05s; }
.js [data-reveal-delay="2"] { transition-delay: .13s; }
.js [data-reveal-delay="3"] { transition-delay: .21s; }
.js [data-reveal-delay="4"] { transition-delay: .29s; }
.js [data-reveal-delay="5"] { transition-delay: .37s; }
.js [data-reveal-delay="6"] { transition-delay: .45s; }

/* Results appearing after a calculation. */
@keyframes ecs-result-in {
  from { opacity: 0; transform: translateY(10px) scale(.995); }
  to   { opacity: 1; transform: none; }
}
.result:not([hidden]) {
  animation: ecs-result-in var(--dur) var(--ease) both;
}

/* --------------------------------------------------------------------------
   Hover and focus

   Deliberately restrained: a small lift, a border that warms to the
   accent, and a shadow already in the token set. No gradients, no scaling
   text, nothing that moves layout.
   -------------------------------------------------------------------------- */

.card {
  transition: transform var(--dur) var(--ease),
              box-shadow var(--dur) var(--ease),
              border-color var(--dur) var(--ease);
}

.btn {
  transition: transform var(--dur-fast) var(--ease),
              background-color var(--dur-fast) var(--ease),
              border-color var(--dur-fast) var(--ease),
              color var(--dur-fast) var(--ease),
              box-shadow var(--dur-fast) var(--ease);
}
.btn:hover  { transform: translateY(-2px); }
.btn:active { transform: translateY(0); }
.btn-primary:hover { box-shadow: var(--shadow-md); }

/* The arrow leaves before the button does. Same idea as .section-more.
   Scoped to the chevron by name: a button can just as easily hold a
   download, copy or refresh glyph, and sliding one of those sideways on
   hover would be meaningless. */
.btn svg[data-icon="arrow-right"] { transition: transform var(--dur) var(--ease); }
.btn:hover svg[data-icon="arrow-right"] { transform: translateX(3px); }

/* Nav links grow a short underline from the centre, drawn with a
   pseudo-element so nothing in the layout moves. */
.nav-link { position: relative; }
.nav-link::after {
  content: "";
  position: absolute;
  left: 50%; right: 50%; bottom: 3px;
  height: 2px;
  border-radius: 2px;
  background: var(--accent);
  opacity: 0;
  transition: left var(--dur) var(--ease), right var(--dur) var(--ease),
              opacity var(--dur) var(--ease);
}
.nav-link:hover::after,
.nav-link[aria-current="page"]::after {
  left: var(--s2); right: var(--s2);
  opacity: 1;
}
@media (max-width: 1079px) {
  /* In the stacked mobile menu an underline reads as a stray rule. */
  .nav-link::after { display: none; }
}

/* Icons that lean into a hover without dragging their label along. */
.section-more svg {
  transition: transform var(--dur) var(--ease);
}
.section-more:hover svg { transform: translateX(5px); }

/* --------------------------------------------------------------------------
   Reduced motion

   The global block in section 2 already flattens durations. These are the
   transforms and fixed end-states it cannot reach on its own: an element
   left mid-animation at opacity 0 would be invisible, not merely still.
   -------------------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  .hero-enter > *,
  .calc-head,
  .result:not([hidden]) { animation: none !important; }

  .result:not([hidden]) .result-value { animation: none !important; }
  .dropzone[data-dragover="true"] .dropzone-icon { animation: none !important; }
  .dropzone[data-dragover="true"] { transform: none !important; }
  .dropzone:hover .dropzone-icon,
  .dropzone:focus-visible .dropzone-icon { transform: none !important; }

  .js [data-reveal] { opacity: 1 !important; transform: none !important; }

  .btn:hover, .btn:active { transform: none !important; }
  .search-hero .input:focus { transform: none !important; }

  .nav-link::after { transition: none !important; }
  .section-more:hover svg { transform: none !important; }
}

/* ==========================================================================
   21. TOOLS PLATFORM

   The /tools/ section is a different kind of product from the calculators:
   you arrive with a file or a job rather than a set of numbers, and the
   working surface matters more than the explanation. So it gets its own
   visual language — a tinted section ground, category-coloured tool cards,
   a large working panel — while using the SAME role tokens as everything
   else, so both themes keep working with no extra rules.

   Nothing here defines a raw colour. Every value is a token, which is what
   makes the dark theme free rather than a second stylesheet.
   ========================================================================== */

/* --------------------------------------------------------------------
   Section ground

   The same --bg as the rest of the site, deliberately.

   The first version used --surface-sunk here, one step darker, so you
   could tell at a glance you were in the tools section. It failed the
   contrast sweep: --accent on ink-100 measures 4.22:1, just under AA for
   normal text, which quietly broke every accent-coloured link and label
   on these pages — "All tools" on the category page was the one the audit
   caught, and an in-article link would have been the next.

   Promoting each of those rules to --accent-strong would have worked, but
   it means every future rule on a tools page has to remember a different
   colour role than the same rule elsewhere. Matching the site ground
   removes the whole class of bug instead.

   The tools section still reads as its own thing: the radial hero, the
   filter chips, the taller cards with icon tiles and badges, and the
   workspace panel do that work — and none of them cost any contrast.
   ------------------------------------------------------------------ */

/* --------------------------------------------------------------------
   The tool hue, applied once

   This is the whole mechanism that makes /tools/ a different product.
   Every tool page already carries body_class "tools-page", so the accent
   ROLES are remapped to the violet ones inside its <main>. Buttons,
   inputs, focus rings, badges, chips, drop zones, progress bars and
   result panels all read those roles already, so the entire tools half
   changes hue without a single component being redeclared.

   Scoped to main, deliberately. The header, the logo and the footer sit
   outside it and stay teal on every page in the site - one brand, and
   the mark never changes colour depending on which half you are in.

   Adding a component to /tools/ needs no work here: if it uses --accent
   it is already correct.
   ------------------------------------------------------------------ */

.tools-page main {
  --accent:        var(--tool-accent);
  --accent-hover:  var(--tool-accent-hover);
  --accent-strong: var(--tool-strong);
  --accent-soft:   var(--tool-soft);
  --accent-line:   var(--tool-line);
  --on-accent:     var(--on-tool);
  --focus:         var(--tool-accent);
  --hero-wash:     var(--tool-wash);

  /* The gradient and glow roles follow, so a primary button on a tool
     page is the tool button without needing the .btn-tool class. */
  --grad-btn:      var(--grad-btn-tool);
  --grad-calc:     var(--grad-tool);
  --grad-calc-soft: var(--grad-tool-soft);
  --glow-calc:     var(--glow-tool);

  --meter-from:    var(--violet-400);
  --meter-to:      var(--violet-600);
}

:root[data-theme="dark"] .tools-page main {
  --meter-from:    var(--violet-500);
  --meter-to:      var(--violet-300);
}

.tools-page { background: var(--bg); }

/* --------------------------------------------------------------------
   Format badges

   Two real extension codes for a file-to-file tool, read from its
   registry record. Used on the tool cards and the homepage spotlight.
   ------------------------------------------------------------------ */

.format-badge {
  display: inline-flex;
  align-items: center;
  padding: 5px var(--s3);
  background: var(--surface-sunk);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-sm);
  font-family: var(--font-mono);
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.02em;
  color: var(--text-soft);
}
.format-badge-to {
  background-color: var(--tool-soft);
  background-image: var(--grad-tool-soft);
  border-color: var(--tool-line);
  color: var(--tool-strong);
}

/* --------------------------------------------------------------------
   Category chips — the quick filter row under the hero
   ------------------------------------------------------------------ */

.tool-chips {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--s2);
  margin: 0;
  padding: 0;
  list-style: none;
}

.tool-chip {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  min-height: 40px;
  padding: 6px var(--s4);
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-full);
  font-size: 0.9rem;
  font-weight: 600;
  color: var(--text-soft);
  text-decoration: none;
  cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease),
              color var(--dur-fast) var(--ease),
              background-color var(--dur-fast) var(--ease);
}
.tool-chip svg { width: 16px; height: 16px; }
.tool-chip:hover { border-color: var(--accent); color: var(--accent-strong); }
.tool-chip[aria-pressed="true"],
.tool-chip[aria-current="page"] {
  background: var(--accent-soft);
  border-color: var(--accent-line);
  color: var(--accent-strong);
}
.tool-chip .n { font-weight: 500; color: var(--text-faint); font-variant-numeric: tabular-nums; }

/* --------------------------------------------------------------------
   Tool search and filter bar
   ------------------------------------------------------------------ */

.tool-filter {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s3);
  margin-bottom: var(--s5);
}
.tool-filter .search-field { flex: 1 1 260px; }
.tool-filter .select { flex: 0 0 auto; min-width: 150px; }

/* The filter reuses .search-field and .search-icon from the site search,
   but not .search-hero — which is where those two get their size and
   position. Without this the inline SVG has no intrinsic size and paints
   as a 300px circle in the middle of the page. */
.tool-filter .search-icon {
  position: absolute;
  left: var(--s4);
  top: 50%;
  transform: translateY(-50%);
  width: 18px; height: 18px;
  color: var(--text-faint);
  pointer-events: none;
  z-index: 1;
}
.tool-filter .input {
  min-height: 48px;
  padding-left: var(--s8);
  font-size: max(16px, 0.98rem);
}

/* How these tools work: three short statements, each with a gradient tile.
   The hover warms the border and glow only - nothing moves, so there is no
   motion here for prefers-reduced-motion to cancel. */
.tool-why {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr));
  gap: var(--s4);
  margin: 0;
  padding: 0;
  list-style: none;
}
.tool-why-item {
  padding: var(--s5);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-xl);
  transition: border-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}
.tool-why-item:hover { border-color: var(--tool-line); box-shadow: var(--glow-tool); }
.tool-why-icon {
  display: grid;
  place-items: center;
  width: 40px; height: 40px;
  margin-bottom: var(--s3);
  background-color: var(--tool-accent);
  background-image: var(--grad-btn-tool);
  border-radius: var(--r-md);
  color: var(--on-tool);
}
.tool-why-icon svg { width: 20px; height: 20px; }
.tool-why-item h3 { margin-bottom: var(--s2); font-size: 1rem; }
.tool-why-item p { margin: 0; font-size: 0.92rem; line-height: 1.6; color: var(--text-soft); }

/* Every tool, by category: the plain-link index. */
.tool-index {
  display: grid;
  gap: var(--s5) var(--s6);
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 210px), 1fr));
}
.tool-index-group h3 {
  margin-bottom: var(--s2);
  font-size: 0.98rem;
}
.tool-index-group h3 a { color: var(--text); text-decoration: none; }
.tool-index-group h3 a:hover { color: var(--accent-strong); }
.tool-index-group ul {
  display: grid;
  gap: 6px;
  margin: 0;
  padding: 0;
  list-style: none;
}
.tool-index-group li a {
  font-size: 0.9rem;
  color: var(--text-soft);
  text-decoration: none;
}
.tool-index-group li a:hover { color: var(--accent-strong); text-decoration: underline; }

/* A cross-link back to the calculators wears teal even inside the violet
   half: it reads --calc-*, which no scope remaps. */
.tool-index-calc {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
}
.tag-calc {
  padding: 1px 7px;
  background: var(--calc-soft);
  border: 1px solid var(--calc-line);
  border-radius: var(--r-full);
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--calc-strong);
}
/* .select is width:100% by default, which makes it a flex item wider than
   the row and forces the filter to wrap onto two lines. */
.tool-filter .select { min-height: 48px; width: auto; }

.tool-count {
  font-size: 0.88rem;
  color: var(--text-faint);
  font-variant-numeric: tabular-nums;
}

.tool-empty {
  padding: var(--s7) var(--s5);
  text-align: center;
  background: var(--surface);
  border: 1px dashed var(--line-strong);
  border-radius: var(--r-lg);
  color: var(--text-soft);
}
.tool-empty h3 { margin-bottom: var(--s2); font-size: 1.05rem; }
.tool-empty p { margin-bottom: 0; font-size: 0.92rem; }
.tool-empty .btn-row { justify-content: center; margin-top: var(--s4); }

/* --------------------------------------------------------------------
   Tool page — the working surface

   The tool itself sits in a raised panel directly under the title, above
   the article. That order is the point: people came to use it, not to
   read about it.
   ------------------------------------------------------------------ */

.tool-workspace {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-sm);
  overflow: hidden;
}
.tool-workspace-body { padding: var(--s5); }
@media (min-width: 768px) { .tool-workspace-body { padding: var(--s6); } }

.tool-workspace-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--s3);
  padding: var(--s4) var(--s5);
  background: var(--surface-sunk);
  border-bottom: 1px solid var(--line);
}
.tool-workspace-head h2 {
  margin: 0;
  font-size: 0.95rem;
  font-weight: 650;
  letter-spacing: 0;
}

/* Where the file goes, or where it would go. Dashed until something is
   dropped, so the affordance is obvious without an instruction. */
/* THE DROP ZONE - the centre of gravity of every tool page.
 *
 * Three states, each doing a different job:
 *   rest      quiet, dashed, clearly a target and clearly empty
 *   hover     the hue arrives and the icon rises
 *   dragover  the dashes become solid and the whole zone lights up,
 *             because at that moment the visitor is holding a file over
 *             it and needs an unambiguous yes
 *
 * The state change is deliberately large. A drop target that responds
 * subtly is a drop target people miss.
 */
.dropzone {
  position: relative;
  isolation: isolate;
  display: grid;
  place-items: center;
  gap: var(--s3);
  padding: var(--s7) var(--s5);
  background: var(--surface-sunk);
  border: 2px dashed var(--line-strong);
  border-radius: var(--r-xl);
  text-align: center;
  cursor: pointer;
  transition: border-color var(--dur) var(--ease),
              background-color var(--dur) var(--ease),
              box-shadow var(--dur) var(--ease),
              transform var(--dur) var(--ease);
}

/* A soft pool of the tool hue under the icon, absent at rest. */
.dropzone::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: inherit;
  background: radial-gradient(56% 70% at 50% 34%, var(--mesh-2) 0%, transparent 72%);
  opacity: 0;
  transition: opacity var(--dur) var(--ease);
  pointer-events: none;
}

.dropzone:hover,
.dropzone:focus-visible,
.dropzone:focus-within {
  border-color: var(--accent);
  background: var(--accent-soft);
}
.dropzone:hover::before,
.dropzone:focus-visible::before,
.dropzone:focus-within::before { opacity: .7; }

.dropzone:hover .dropzone-icon,
.dropzone:focus-visible .dropzone-icon {
  transform: translateY(-3px);
  background-image: var(--grad-calc);
  border-color: transparent;
  color: var(--on-accent);
  box-shadow: var(--glow-calc);
}

/* Holding a file over it. */
.dropzone[data-dragover="true"] {
  border-color: var(--accent);
  border-style: solid;
  background: var(--accent-soft);
  box-shadow: var(--glow-calc);
  transform: scale(1.006);
}
.dropzone[data-dragover="true"]::before { opacity: 1; }
.dropzone[data-dragover="true"] .dropzone-icon {
  background-image: var(--grad-calc);
  border-color: transparent;
  color: var(--on-accent);
  animation: ecs-dz-pulse 1.1s var(--ease) infinite;
}

@keyframes ecs-dz-pulse {
  0%, 100% { transform: translateY(-3px) scale(1); }
  50%      { transform: translateY(-6px) scale(1.05); }
}

.dropzone-icon {
  display: grid;
  place-items: center;
  width: 56px; height: 56px;
  background-color: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-full);
  color: var(--accent-strong);
  transition: transform var(--dur) var(--ease),
              background-image var(--dur) var(--ease),
              border-color var(--dur) var(--ease),
              color var(--dur) var(--ease),
              box-shadow var(--dur) var(--ease);
}
.dropzone-icon svg { width: 25px; height: 25px; }
.dropzone-title { font-weight: 650; }
.dropzone-hint { font-size: 0.86rem; color: var(--text-faint); margin: 0; }

/* Per-file rows once something is queued. */
.file-list { list-style: none; margin: var(--s4) 0 0; padding: 0; display: grid; gap: var(--s2); }
.file-row {
  display: flex;
  align-items: center;
  gap: var(--s3);
  padding: var(--s3);
  background: var(--surface-sunk);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
}
.file-row-name { flex: 1 1 auto; min-width: 0; font-size: 0.9rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.file-row-size { flex: 0 0 auto; font-size: 0.82rem; color: var(--text-faint); font-variant-numeric: tabular-nums; }
.file-row-state { flex: 0 0 auto; font-size: 0.8rem; font-weight: 600; }
.file-row-state[data-state="done"]  { color: var(--ok-fg); }
.file-row-state[data-state="error"] { color: var(--bad-fg); }

/* Progress. Only rendered when a real operation reports real progress. */
.progress {
  height: 6px;
  background: var(--surface-sunk);
  border: 1px solid var(--line);
  border-radius: var(--r-full);
  overflow: hidden;
}
.progress-fill {
  height: 100%;
  width: 0;
  background: linear-gradient(90deg, var(--meter-from), var(--meter-to));
  transition: width var(--dur) var(--ease);
}

/* The privacy line under every tool. It states what the tool actually
   does, and the registry's 'processing' field is what decides which
   wording appears — so it cannot drift away from the truth. */
.tool-privacy {
  display: flex;
  align-items: flex-start;
  gap: var(--s3);
  margin-top: var(--s4);
  padding: var(--s3) var(--s4);
  background: var(--ok-bg);
  border: 1px solid var(--ok-line);
  border-radius: var(--r-md);
  font-size: 0.86rem;
  line-height: 1.5;
  color: var(--ok-fg);
}
.tool-privacy svg { width: 17px; height: 17px; flex-shrink: 0; margin-top: 2px; }
.tool-privacy p { margin: 0; }

/* Small fact strip under the workspace: formats in, format out. */
.tool-facts {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2) var(--s5);
  margin-top: var(--s4);
  font-size: 0.85rem;
  color: var(--text-soft);
}
/* Wraps, and can shrink below its content width. A tool that accepts
   seven formats renders seven <code> chips inside one fact; without
   both of these the row refused to fold and pushed a 320px phone into
   horizontal scroll. */
.tool-fact {
  display: inline-flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 7px;
  min-width: 0;
  max-width: 100%;
}
.tool-fact svg { width: 15px; height: 15px; color: var(--accent); flex-shrink: 0; }
.tool-fact code {
  overflow-wrap: anywhere;
  font-size: 0.82rem;
  padding: 1px 5px;
  background: var(--surface-sunk);
  border: 1px solid var(--line);
  border-radius: 4px;
}

/* --------------------------------------------------------------------
   Reduced motion
   ------------------------------------------------------------------ */

@media (prefers-reduced-motion: reduce) {
  .utool-fav:hover { transform: none !important; }
  .progress-fill { transition: none !important; }
}

/* --------------------------------------------------------------------
   Tasbih counter

   The dial is the whole interface. It is sized for a thumb rather than a
   cursor — people use this one-handed, on a phone, often without looking
   at the screen — so the tap target is far larger than anything else on
   the site and the number inside it is the largest type anywhere.
   ------------------------------------------------------------------ */

.tasbih { display: grid; justify-items: center; gap: var(--s4); }

.tasbih-dial {
  position: relative;
  /* Sized against the container, not the viewport. 78vw knew nothing
     about the card's padding, so on a 320px screen the dial came out
     250px wide inside a 223px box and painted over its own card. */
  width: min(280px, 100%);
  aspect-ratio: 1;
  display: grid;
  place-items: center;
}

/* Rotated so the ring starts at twelve o'clock rather than three. */
.tasbih-ring {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  transform: rotate(-90deg);
}
.tasbih-ring circle {
  fill: none;
  stroke-width: 6;
  stroke-linecap: round;
}
.tasbih-ring-track { stroke: var(--line); }
.tasbih-ring-fill {
  stroke: var(--accent);
  /* Set from JS; declared here so the first paint is not a full ring. */
  stroke-dasharray: 339.29;
  stroke-dashoffset: 339.29;
  transition: stroke-dashoffset var(--dur) var(--ease);
}

.tasbih-tap {
  position: relative;
  display: grid;
  place-items: center;
  gap: 2px;
  width: 78%;
  aspect-ratio: 1;
  padding: 0;
  background: var(--accent-soft);
  border: 1px solid var(--accent-line);
  border-radius: 50%;
  color: var(--accent-strong);
  cursor: pointer;
  -webkit-user-select: none;
  user-select: none;
  /* Stops the double-tap-to-zoom delay and the grey flash on Android. */
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  transition: transform .08s var(--ease), background-color var(--dur-fast) var(--ease);
}
.tasbih-tap:hover { background: var(--accent-soft); }
.tasbih-tap:active { transform: scale(0.96); }
.tasbih-tap:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 4px;
}

.tasbih-num {
  font-size: clamp(3rem, 2rem + 8vw, 4.6rem);
  font-weight: 750;
  line-height: 1;
  letter-spacing: -0.04em;
  font-variant-numeric: tabular-nums;
  color: var(--text);
}
.tasbih-tap-hint {
  font-size: 0.8rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-faint);
}

.tasbih-remaining {
  margin: 0;
  font-size: 0.92rem;
  font-weight: 600;
  color: var(--text-soft);
}

.tasbih-actions { justify-content: center; width: 100%; max-width: 460px; }
.tasbih-actions .btn { flex: 0 1 auto; }

.tasbih-settings { width: 100%; max-width: 560px; }
.tasbih-presets {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
  margin-bottom: var(--s4);
}
.tasbih-presets .tool-chip { min-width: 60px; justify-content: center; font-variant-numeric: tabular-nums; }

@media (prefers-reduced-motion: reduce) {
  .tasbih-tap:active { transform: none !important; }
  .tasbih-ring-fill { transition: none !important; }
}

/* Options between the dropzone and the Convert button. Spaced away from
   both so the working surface reads top to bottom: choose files, choose
   how, convert, collect. */
.tool-options { margin-block: var(--s5); }
.tool-options fieldset { margin-bottom: var(--s4); }

/* The queue and results lists sit inside the workspace, so their heading
   is smaller than a section heading. */
.tool-workspace .result-section-title { margin-top: var(--s5); }
.tool-workspace .file-list { margin-bottom: var(--s2); }
.file-row .btn-sm { flex-shrink: 0; }

/* A result row carries more than a queue row: name, size change, which
   path it took, and a download. Below 560px that is too much for one
   line, so it wraps into two with the name on its own row. */
@media (max-width: 559px) {
  .file-row { flex-wrap: wrap; }
  .file-row-name { flex: 1 1 100%; }
}

/* --------------------------------------------------------------------
   Date tools

   The converters answer with two dates, not one. Neither is the "real"
   answer — they are the same day written two ways — so they are given
   equal weight rather than a headline and a footnote.
   ------------------------------------------------------------------ */

.today-strip {
  display: grid;
  gap: var(--s2);
  padding: var(--s4) var(--s5);
  background: var(--accent-soft);
  border: 1px solid var(--accent-line);
  border-radius: var(--r-lg);
  margin-bottom: var(--s5);
}
.today-strip .label {
  margin: 0;
  font-size: var(--step--1);
  font-weight: 650;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--accent-strong);
}
.today-strip .today-primary {
  margin: 0;
  font-size: var(--step-2);
  font-weight: 700;
  letter-spacing: -0.02em;
  color: var(--text);
}
.today-strip .today-secondary { margin: 0; font-size: 0.92rem; color: var(--text-soft); }

.date-pair {
  display: grid;
  gap: var(--s4);
  grid-template-columns: 1fr;
}
@media (min-width: 620px) { .date-pair { grid-template-columns: 1fr 1fr; } }

.date-card {
  padding: var(--s4) var(--s5);
  background: var(--surface-sunk);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
}
.date-card.is-primary {
  background: var(--accent-soft);
  border-color: var(--accent-line);
}
.date-card .label {
  display: block;
  margin-bottom: var(--s1);
  font-size: var(--step--1);
  font-weight: 650;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-faint);
}
.date-card.is-primary .label { color: var(--accent-strong); }
.date-card .value {
  display: block;
  font-size: var(--step-2);
  font-weight: 700;
  letter-spacing: -0.02em;
  line-height: 1.15;
  color: var(--text);
}

/* Key/value rows. Bare dt/dd in a two-column grid, which stacks below
   480px so a long value is never squeezed into a sliver. */
.kv {
  display: grid;
  grid-template-columns: minmax(0, auto) minmax(0, 1fr);
  gap: var(--s2) var(--s4);
  margin: 0;
  font-size: 0.92rem;
}
.kv dt { color: var(--text-soft); }
.kv dd { margin: 0; font-weight: 600; overflow-wrap: anywhere; }
@media (max-width: 479px) {
  .kv { grid-template-columns: 1fr; gap: 0; }
  .kv dt { margin-top: var(--s3); }
  .kv dt:first-child { margin-top: 0; }
}

/* The three date inputs sit on one row from 480px up. */
.date-fields {
  display: grid;
  gap: var(--s3);
  grid-template-columns: 1fr;
}
@media (min-width: 480px) {
  .date-fields { grid-template-columns: 1fr 1.6fr 1fr; align-items: end; }
}

/* --------------------------------------------------------------------
   Slide preview (the PowerPoint tools)

   Shown between the dropzone and the download, because these conversions
   lose layout and images by nature. Seeing what was actually extracted
   before you download is what stops that being a surprise afterwards.
   ------------------------------------------------------------------ */

.slide-list {
  display: grid;
  gap: var(--s2);
  max-height: 320px;
  overflow-y: auto;
  padding: var(--s3);
  background: var(--surface-sunk);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
}

.slide-row {
  display: flex;
  align-items: flex-start;
  gap: var(--s3);
  padding: var(--s2) var(--s3);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
}

.slide-row-num {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  min-width: 26px;
  height: 26px;
  padding-inline: 6px;
  background: var(--accent-soft);
  border: 1px solid var(--accent-line);
  border-radius: var(--r-sm);
  font-size: 0.78rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--accent-strong);
}

.slide-row-text { flex: 1 1 auto; min-width: 0; }
.slide-row-text strong { display: block; font-size: 0.92rem; line-height: 1.35; }
.slide-row-text strong.is-empty { color: var(--text-faint); font-weight: 500; font-style: italic; }
.slide-row-text p {
  margin: 2px 0 0;
  font-size: 0.84rem;
  line-height: 1.45;
  color: var(--text-soft);
  overflow: hidden;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}

.slide-row-meta {
  flex: 0 0 auto;
  font-size: 0.78rem;
  color: var(--text-faint);
  white-space: nowrap;
}

@media (max-width: 519px) {
  .slide-row { flex-wrap: wrap; }
  .slide-row-meta { width: 100%; padding-left: 38px; }
}

/* --------------------------------------------------------------------
   Text and data converters

   Input and output side by side above 900px, stacked below. Seeing both
   at once is the point of converting as you type: you adjust the input
   against the output rather than converting blind and checking after.
   ------------------------------------------------------------------ */

.io-pair {
  display: grid;
  gap: var(--s4);
  grid-template-columns: 1fr;
}
@media (min-width: 900px) { .io-pair { grid-template-columns: 1fr 1fr; } }

.io-box {
  min-height: 260px;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.86rem;
  line-height: 1.55;
  /* Long unbroken lines are normal in CSV and JSON, and wrapping them
     hides the structure. Scroll instead. */
  white-space: pre;
  overflow-wrap: normal;
  overflow-x: auto;
  tab-size: 4;
  resize: vertical;
}
@media (max-width: 899px) { .io-box { min-height: 190px; } }

/* The output is read-only but not disabled: it must stay selectable and
   reachable by keyboard so it can be copied without the button. */
.io-box[readonly] { background: var(--surface-sunk); }

/* A slimmer dropzone, because here the file picker is the secondary way
   in — most people paste. */
.dropzone-slim {
  margin-top: var(--s5);
  padding: var(--s4);
  gap: var(--s2);
}
.dropzone-slim .dropzone-icon { width: 38px; height: 38px; }
.dropzone-slim .dropzone-icon svg { width: 18px; height: 18px; }
.dropzone-slim .dropzone-title { font-size: 0.94rem; }

/* Column mapping (CSV to vCard). Built by JS from the file's header. */
.map-grid {
  display: grid;
  gap: var(--s3);
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 200px), 1fr));
}
.map-grid .field { margin-bottom: 0; }


/* ===========================================================================
   22. LANGUAGE

   The switcher, and the two direction-isolation utilities that shared
   markup carries in every language. The mirror itself is a separate file,
   assets/css/rtl.css, served only to right-to-left locales.
   =========================================================================== */

/* Left-to-right content inside a page of any direction: filenames, file
   extensions, URLs, code, version numbers, technical identifiers.

   Inert on an English page and that is fine — the markup that carries it
   is shared between languages, so the class has to be safe to apply
   unconditionally. `isolate` is the part that does the work: `direction`
   alone still lets the bidirectional algorithm resolve the run against
   its neighbours, which is how `photo10.png` ends up as `png.photo10`. */
.ltr { direction: ltr; unicode-bidi: isolate; }

/* Direction taken from the content's own first strong character — for a
   filename the visitor chose, which may be in either script. */
.bidi { unicode-bidi: isolate; }

.langswitch { position: relative; flex-shrink: 0; }

/* The summary carries the whole control, so the disclosure works with no
   JavaScript at all: the browser gives keyboard operation, focus and the
   escape key, and every entry inside is a real link to a real URL. */
.langswitch-details > summary {
  display: inline-flex;
  align-items: center;
  gap: var(--s1);
  height: 40px;
  padding: 0 var(--s2);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: transparent;
  color: var(--text-muted);
  font-size: 0.85rem;
  font-weight: 600;
  cursor: pointer;
  list-style: none;
  transition: color .14s, border-color .14s, background-color .14s;
}
.langswitch-details > summary::-webkit-details-marker { display: none; }
.langswitch-details > summary::marker { content: ''; }

.langswitch-details > summary:hover {
  color: var(--text);
  border-color: var(--text-faint);
  background: var(--surface-sunk);
}
.langswitch-details > summary:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 2px;
}

.langswitch-icon svg { width: 18px; height: 18px; }
.langswitch-code { font-variant-numeric: tabular-nums; letter-spacing: .02em; }
.langswitch-caret svg { width: 14px; height: 14px; }
.langswitch-details[open] .langswitch-caret svg { transform: rotate(180deg); }

.langswitch-panel {
  position: absolute;
  top: calc(100% + var(--s2));
  right: 0;
  z-index: 60;
  min-width: 15rem;
  max-height: min(70vh, 26rem);
  overflow-y: auto;
  padding: var(--s3);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-lg);
}

.langswitch-head {
  margin: 0 0 var(--s2);
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--text-faint);
}

.langswitch-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 2px; }

.langswitch-item {
  display: flex;
  align-items: baseline;
  gap: var(--s2);
  padding: var(--s2) var(--s3);
  border-radius: var(--r-sm);
  color: var(--text);
  text-decoration: none;
  font-size: 0.92rem;
  transition: background-color .14s, color .14s;
}
.langswitch-item:hover { background: var(--surface-sunk); }
.langswitch-item:focus-visible { outline: 3px solid var(--focus); outline-offset: -2px; }

/* The current language is marked with a tick AND aria-current, not with
   colour alone — the whole point of the control is to be usable by
   someone who cannot read the page they are on. */
.langswitch-item[aria-current="true"] { font-weight: 700; }

.langswitch-name { flex: 1 1 auto; }
.langswitch-en { font-size: 0.78rem; color: var(--text-faint); }
.langswitch-tick { flex: 0 0 auto; color: var(--accent-strong); }
.langswitch-tick svg { width: 15px; height: 15px; }

@media (max-width: 600px) {
  .langswitch-code { display: none; }
  .langswitch-details > summary { padding: 0 var(--s2); }
  /* Anchored to the viewport edge rather than the button, so a long
     language name cannot push the panel off screen at 320px. */
  .langswitch-panel { right: 0; min-width: min(15rem, calc(100vw - var(--s6))); }
}

/* Header crowding.
 *
 * The bar holds a brand, the nav, the language switcher, the theme toggle
 * and the menu button. `.brand` was `flex-shrink: 0` and `white-space:
 * nowrap`, so it took 195px of a 288px content box at 320px and nothing
 * could give — adding the switcher pushed the whole page into horizontal
 * scroll. Found by the sweep on a right-to-left fixture page, but it was
 * never a direction bug: it is what happens when one more control joins a
 * row whose widest member refuses to shrink.
 *
 * So the brand yields first, and the switcher goes icon-only on a phone. */
.brand { flex-shrink: 1; min-width: 0; }
.brand-mark { flex-shrink: 0; }
.brand-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; }

@media (max-width: 600px) {
  .header-inner { gap: var(--s2); }
  /* The globe alone is the control. The caret said "this opens" to people
     who could already see it was a disclosure, and cost 18px on the one
     screen size that had none to spare. */
  .langswitch-caret { display: none; }
  .langswitch-details > summary { padding: 0 var(--s1); gap: 0; }
}

/* Long unbreakable strings in running text.
 *
 * A Windows registry path, a URL, a German compound or a technical
 * identifier can be one token wider than a 320px viewport, and a token
 * that cannot break pushes the whole page sideways. Found on the Russian
 * JFIF page, where HKEY_CLASSES_ROOT\MIME\Database\Content Type\image/jpeg
 * is a single word 69px wider than its column — but nothing about it is
 * Russian, and the same sentence in English would do the same thing.
 *
 * `break-word` rather than `anywhere`: it only breaks a word that would
 * otherwise overflow, so ordinary prose is unaffected and the ragged edge
 * stays where the language wants it. */
.prose p,
.prose li,
.prose dd,
.callout p,
.disclosure-body p,
.tool-empty p,
.lede { overflow-wrap: break-word; }

/* A titled callout stacks; an icon callout does not.
 *
 * `.callout` is a flex row built for an icon beside a paragraph. The
 * disclaimer blocks put an <h2> beside the paragraph instead, which at
 * 320px left the heading and the text fighting over 271px — and once a
 * heading grew (the Russian ones are longer) it pushed the paragraph out
 * of the box entirely.
 *
 * Allowing the heading to shrink was worse: it squeezed to a 15px sliver
 * on English pages too. So the heading takes a row of its own and the
 * body follows underneath, which is what a titled callout should have
 * looked like at narrow widths all along. An icon callout has no <h2>,
 * so nothing about it changes.
 *
 * `min-width: 0` stays, so a long unbreakable token in the body wraps
 * rather than widening the box. */
.callout { flex-wrap: wrap; }
.callout > *  { min-width: 0; }
.callout > h2 { flex: 0 0 100%; margin-bottom: var(--s2); }

/* =====================================================================
   IMAGE TO WORD (/jpg-to-word/)

   Two modes on one page, so the mode picker has to read as a choice
   between two different products rather than as an on/off switch. That
   is why each option carries its own explanatory line inside the label
   and why they are radios in a fieldset, not a toggle.

   Every colour here is a role token. The confidence chip reuses the
   ok/warn/bad trio, which is already defined in both themes, so the
   dark palette needs no rule of its own below.
   ===================================================================== */

.imgword-modes {
  display: grid;
  gap: var(--s3);
  margin: 0 0 var(--s5);
}

.imgword-mode {
  display: flex;
  gap: var(--s3);
  align-items: flex-start;
  padding: var(--s4);
  background: var(--surface-sunk);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  cursor: pointer;
}

.imgword-mode:hover { border-color: var(--line-strong); }

/* The selected mode is marked by the accent border AND the radio itself.
   Colour alone would leave the choice invisible to anyone who cannot see
   the difference between the two borders. */
.imgword-mode:has(input:checked) {
  border-color: var(--accent);
  background: var(--accent-soft);
}

.imgword-mode:has(input:focus-visible) {
  outline: 3px solid var(--focus);
  outline-offset: 2px;
}

.imgword-mode input { margin-top: 0.2em; flex: none; }

.imgword-mode-body { display: grid; gap: var(--s1); }
.imgword-mode-body strong { font-weight: 650; }
.imgword-mode-body .hint { margin: 0; }

.imgword-note { margin: 0 0 var(--s5); }

/* Queue rows carry a thumbnail, so the row needs a fixed leading box. */
.imgword-row { align-items: center; }

.imgword-thumb {
  flex: none;
  width: 44px;
  height: 44px;
  object-fit: cover;
  border-radius: var(--r-sm);
  border: 1px solid var(--line);
  background: var(--surface);
}

/* Progress. Only OCR reports any, and it reports real stages rather than
   an animation that runs regardless of what is happening. */
.imgword-progress { margin: var(--s4) 0 0; }

.imgword-progress-track {
  height: 8px;
  border-radius: 99px;
  background: var(--surface-sunk);
  border: 1px solid var(--line);
  overflow: hidden;
}

.imgword-progress-bar {
  display: block;
  height: 100%;
  width: 0;
  background: var(--accent);
  transition: width var(--dur) var(--ease);
}

@media (prefers-reduced-motion: reduce) {
  .imgword-progress-bar { transition: none; }
}

.imgword-progress .hint { margin: var(--s2) 0 0; }

/* Recognised text, editable before it becomes a document. */
.imgword-results { margin: var(--s6) 0 0; }

.imgword-result {
  margin: var(--s4) 0 0;
  padding: var(--s4);
  background: var(--surface-sunk);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
}

.imgword-result-head {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2) var(--s3);
  align-items: baseline;
  justify-content: space-between;
  margin-bottom: var(--s3);
}

.imgword-result-head h4 { margin: 0; font-size: var(--step-0); }

.imgword-conf {
  font-size: 0.82rem;
  font-weight: 650;
  padding: 0.15em 0.6em;
  border-radius: 99px;
  border: 1px solid;
}

.imgword-conf[data-level="ok"]  { background: var(--ok-bg);   color: var(--ok-fg);   border-color: var(--ok-line); }
.imgword-conf[data-level="low"] { background: var(--warn-bg); color: var(--warn-fg); border-color: var(--warn-line); }
.imgword-conf[data-level="bad"] { background: var(--bad-bg);  color: var(--bad-fg);  border-color: var(--bad-line); }

.imgword-doubtful {
  margin: 0 0 var(--s3);
  font-size: 0.88rem;
  color: var(--warn-fg);
}

.imgword-text {
  width: 100%;
  font-family: var(--font-mono);
  font-size: 0.9rem;
  line-height: 1.55;
  resize: vertical;
}

/* =====================================================================
   JPG TO SVG (/jpg-to-svg/)

   The two panels are the honesty mechanism of this page: the traced
   result sits beside the original at the same size so the visitor judges
   the trade before downloading. They must therefore stay side by side
   wherever there is room, and stack rather than shrink when there is not.

   The canvas ground is a checkerboard built from two role tokens, so
   that "leave the background out" produces a visibly transparent result
   in both themes instead of white-on-white or black-on-black.
   ===================================================================== */

.svgtool-panels {
  display: grid;
  gap: var(--s4);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr));
  margin: var(--s5) 0 var(--s4);
}

.svgtool-panel { margin: 0; }

.svgtool-panel figcaption {
  font-size: 0.86rem;
  font-weight: 650;
  color: var(--text-soft);
  margin-bottom: var(--s2);
}

.svgtool-canvas {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 140px;
  padding: var(--s3);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  overflow: auto;

  /* Transparency has to be visible or "leave the background out" looks
     like it did nothing. Both squares are role tokens, so the board
     follows the theme instead of being a fixed grey. */
  background-color: var(--surface);
  background-image:
    linear-gradient(45deg, var(--surface-sunk) 25%, transparent 25%),
    linear-gradient(-45deg, var(--surface-sunk) 25%, transparent 25%),
    linear-gradient(45deg, transparent 75%, var(--surface-sunk) 75%),
    linear-gradient(-45deg, transparent 75%, var(--surface-sunk) 75%);
  background-size: 16px 16px;
  background-position: 0 0, 0 8px, 8px -8px, -8px 0;
}

/* The two previews are rendered at the working size, which can be wider
   than the panel. Fit them rather than letting one panel scroll and the
   other not — the comparison only works at matching scale. */
.svgtool-canvas > img,
.svgtool-canvas > canvas {
  display: block;
  max-width: 100%;
  height: auto;
}

.svgtool-warning { margin: var(--s4) 0 0; }

/* The live value beside a slider's label. */
.svgtool-out {
  float: inline-end;
  font-weight: 650;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
}

/* --------------------------------------------------------------------
   Small layout utilities

   These replace the handful of inline style attributes that had
   accumulated on page templates. An inline style cannot be themed, cannot
   be overridden by a media query and does not show up when you grep the
   stylesheet for a value, so a design system with seven of them has seven
   holes in it.
   ------------------------------------------------------------------ */

.search-inline-lg { max-width: 600px; margin-top: var(--s4); }
.card-spaced { margin-bottom: var(--s6); }
.h-plain { margin-top: 0; border: 0; padding: 0; font-size: 1.1rem; }
.text-fine { font-size: 0.88rem; }


/* ==========================================================================
   Moon phase compatibility calculator

   The page is an ordinary calculator page until the result appears; the
   result itself sits on a night panel, because two moon discs and a score
   read better against a sky than against a white card, and because it makes
   the one part people screenshot look like something.

   The discs are drawn from SVG paths — a semicircular limb closed with a
   half-ellipse — generated by app/includes/moon-visual.php for the eight in
   the article and by assets/js/lib/moon.js for the two in the result. There
   are no images to load and no phase sprite sheet to keep in step.
   ========================================================================== */

.moon-privacy {
  display: flex;
  align-items: flex-start;
  gap: var(--s2);
  margin-top: var(--s4);
}

.moon-privacy svg {
  flex: none;
  width: 1.05em;
  height: 1.05em;
  margin-top: 0.15em;
}

/* Discs ------------------------------------------------------------------ */

.moon-disc { display: block; width: 100%; height: auto; }
.moon-disc-dark { fill: var(--moon-dark); }
.moon-disc-lit  { fill: var(--moon-lit); }
.moon-disc-rim  { fill: none; stroke: var(--moon-rim); stroke-width: 1.5; }

.moon-disc-lg {
  width: clamp(96px, 24vw, 132px);
  filter: drop-shadow(0 0 16px var(--moon-glow));
}

.moon-disc-sm { width: 52px; flex: none; }

/* Result ----------------------------------------------------------------- */

.moon-pair {
  display: grid;
  gap: var(--s5);
  justify-items: center;
  padding: var(--s6) var(--s5);
  border-radius: var(--r-xl);
  background: radial-gradient(120% 120% at 50% 0%, var(--moon-sky-edge), var(--moon-sky));
  color: var(--moon-on-sky);
  text-align: center;
}

.moon-card {
  display: grid;
  justify-items: center;
  gap: var(--s2);
  max-width: 20rem;
}

.moon-card-who {
  font-size: 0.82rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--moon-on-sky-dim);
}

.moon-card-phase { font-size: 1.2rem; font-weight: 650; }

.moon-card-meta {
  font-size: 0.9rem;
  color: var(--moon-on-sky-dim);
  font-variant-numeric: tabular-nums;
}

.moon-card-blurb {
  font-size: 0.92rem;
  color: var(--moon-on-sky-dim);
  max-width: 17rem;
}

.moon-score { display: grid; justify-items: center; gap: var(--s2); }

.moon-ring {
  grid-area: 1 / 1;
  width: clamp(140px, 34vw, 172px);
  height: auto;
  transform: rotate(-90deg);
}

.moon-ring-track { fill: none; stroke: var(--moon-track); stroke-width: 9; }

.moon-ring-fill {
  fill: none;
  stroke: var(--moon-ring);
  stroke-width: 9;
  stroke-linecap: round;
  stroke-dashoffset: 327;
}

.moon-score-value {
  grid-area: 1 / 1;
  align-self: center;
  font-size: clamp(1.9rem, 7vw, 2.5rem);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

.moon-score-label {
  grid-area: 2 / 1;
  font-size: 0.82rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--moon-on-sky-dim);
}

.moon-band {
  margin: var(--s5) 0 0;
  text-align: center;
  font-size: 1.25rem;
  font-weight: 650;
}

.moon-summary {
  margin: var(--s2) auto 0;
  max-width: 44rem;
  text-align: center;
  color: var(--text-soft);
}

.moon-approx { margin-top: var(--s4); }

.moon-reading-title { font-weight: 650; margin-bottom: var(--s2); }

.moon-points {
  display: grid;
  gap: var(--s4);
  margin-top: var(--s3);
}

.moon-points-col h4 {
  margin: 0 0 var(--s2);
  font-size: 0.82rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-faint);
}

.moon-points-col ul {
  margin: 0;
  padding-inline-start: var(--s5);
  display: grid;
  gap: var(--s2);
}

.moon-reflection {
  margin-top: var(--s4);
  padding: var(--s3) var(--s4);
  border-inline-start: 3px solid var(--accent-line);
  background: var(--surface-sunk);
  border-radius: var(--r-sm);
  color: var(--text-soft);
}

/* The eight phases in the article ---------------------------------------- */

.moon-phase-grid {
  display: grid;
  gap: var(--s4);
  margin: var(--s5) 0;
}

.moon-phase-card {
  padding: var(--s4);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  background: var(--surface);
}

.moon-phase-card-head {
  display: flex;
  align-items: center;
  gap: var(--s3);
  margin-bottom: var(--s3);
}

.moon-phase-card-head h3 { margin: 0; font-size: 1.1rem; }

.moon-phase-sky { color: var(--text-soft); font-size: 0.94rem; }

.moon-phase-facts { margin: var(--s3) 0 0; display: grid; gap: var(--s2); }

.moon-phase-facts > div {
  display: grid;
  gap: 2px;
  padding-top: var(--s2);
  border-top: 1px solid var(--line);
}

.moon-phase-facts dt {
  font-size: 0.76rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-faint);
}

.moon-phase-facts dd { margin: 0; font-size: 0.94rem; }

@media (min-width: 640px) {
  .moon-points { grid-template-columns: 1fr 1fr; gap: var(--s6); }
  .moon-phase-grid { grid-template-columns: 1fr 1fr; }
}

@media (min-width: 900px) {
  .moon-pair {
    grid-template-columns: 1fr auto 1fr;
    gap: var(--s6);
    padding: var(--s7) var(--s6);
  }
}

/* The ring fills from empty when a result appears. JavaScript sets the
   transition and skips it outright under reduced motion, so there is
   nothing to cancel here; this rule is the resting state. */
@media (prefers-reduced-motion: reduce) {
  .moon-ring-fill { transition: none !important; }
  .moon-disc-lg { filter: none; }
}

/* Printing a result: the form, the article and the controls are noise on
   paper — the two moons, the score and the reading are the point. */
@media print {
  .moon-pair {
    background: none;
    color: var(--text);
    border: 1px solid var(--line);
    page-break-inside: avoid;
  }

  .moon-card-who, .moon-card-meta, .moon-card-blurb, .moon-score-label { color: var(--text-soft); }
  .moon-disc-dark { fill: none; stroke: var(--line); }
  .moon-disc-lit { fill: var(--text-soft); }
  .moon-disc-lg { filter: none; }
  .moon-ring-track { stroke: var(--line); }
  .moon-ring-fill { stroke: var(--text); }

  #moon-form, .moon-privacy, .moon-phase-grid { display: none !important; }
}

/* ==========================================================================
   TWIN FLAME CALCULATOR

   The same night-sky panel the moon page uses, and for the same reason: a
   score presented as a glowing ring needs a dark ground to read against,
   and this page is about constellations and cycles rather than about
   hearts. It deliberately borrows the moon palette rather than inventing a
   second one — two dark panels on one site should be the same dark.

   What it is NOT: pink, glittery, hearted or otherwise decorated. The
   romantic register here is a soft glow and a thin orbit line, nothing
   more. This is a calculator.
   ========================================================================== */

.tf-person { display: grid; gap: var(--s4); }

.tf-precision { margin-top: var(--s2); }
.tf-precision > summary { font-size: 0.92rem; font-weight: 600; }

.tf-privacy {
  display: flex;
  align-items: flex-start;
  gap: var(--s2);
  margin-top: var(--s4);
}

.tf-privacy svg {
  flex: none;
  width: 1.05em;
  height: 1.05em;
  margin-top: 0.15em;
}

/* Headline ---------------------------------------------------------------- */

.tf-headline {
  display: grid;
  gap: var(--s5);
  justify-items: center;
  padding: var(--s6) var(--s5);
  border-radius: var(--r-xl);
  /* Two light sources, off-centre and low-contrast: an orbit rather than a
     spotlight. The radial pair is what keeps the panel from reading as a
     flat dark rectangle at phone width. */
  background:
    radial-gradient(90% 120% at 18% 0%, var(--tf-glow-a), transparent 60%),
    radial-gradient(90% 120% at 84% 12%, var(--tf-glow-b), transparent 58%),
    radial-gradient(130% 130% at 50% 0%, var(--moon-sky-edge), var(--moon-sky));
  color: var(--moon-on-sky);
  text-align: center;
}

.tf-score {
  display: grid;
  justify-items: center;
  gap: var(--s2);
}

.tf-ring {
  grid-area: 1 / 1;
  width: clamp(148px, 38vw, 184px);
  height: auto;
  transform: rotate(-90deg);
}

.tf-ring-track { fill: none; stroke: var(--moon-track); stroke-width: 8; }

.tf-ring-fill {
  fill: none;
  stroke: var(--tf-ring);
  stroke-width: 8;
  stroke-linecap: round;
  stroke-dashoffset: 327;
  filter: drop-shadow(0 0 6px var(--tf-ring-glow));
  transition: stroke-dashoffset var(--dur-reveal) var(--ease-out);
}

.tf-score-value {
  grid-area: 1 / 1;
  align-self: center;
  font-size: clamp(2rem, 8vw, 2.7rem);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

.tf-score-label {
  grid-area: 2 / 1;
  font-size: 0.78rem;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--moon-on-sky-dim);
}

.tf-headline-text { max-width: 40rem; }

.tf-band {
  margin: 0;
  font-size: 0.78rem;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--tf-ring);
}

.tf-band-title {
  margin: var(--s2) 0 0;
  font-size: 1.2rem;
  font-weight: 650;
}

.tf-band-reading {
  margin: var(--s3) 0 0;
  color: var(--moon-on-sky-dim);
  font-size: 0.95rem;
}

/* Profiles ---------------------------------------------------------------- */

.tf-profiles { display: grid; gap: var(--s5); }

.tf-profile h4 {
  margin: 0 0 var(--s3);
  font-size: 1rem;
  /* Long names are user input and must never widen the layout. */
  overflow-wrap: anywhere;
}

.tf-note {
  display: block;
  margin-top: 2px;
  font-size: 0.84rem;
  color: var(--text-faint);
}

/* Reading ----------------------------------------------------------------- */

.tf-highlight { margin: 0 0 var(--s2); }
.tf-highlight strong { color: var(--text); }

.tf-points { display: grid; gap: var(--s5); margin-top: var(--s4); }

.tf-points-col h4 { margin: 0 0 var(--s2); font-size: 0.95rem; }

.tf-points-col ul,
.tf-reflection { margin: 0; padding-inline-start: 1.2em; }

.tf-points-col li,
.tf-reflection li { margin-bottom: var(--s2); }

.tf-show-work { margin-top: var(--s5); }

.tf-show-work h4 {
  margin: var(--s4) 0 var(--s2);
  font-size: 0.95rem;
}

.tf-show-work h4:first-child { margin-top: 0; }

.tf-working-list {
  margin: 0;
  padding-inline-start: 1.2em;
  font-size: 0.92rem;
  color: var(--text-soft);
}

.tf-working-list li { margin-bottom: var(--s2); overflow-wrap: anywhere; }

@media (min-width: 640px) {
  .tf-profiles { grid-template-columns: 1fr 1fr; gap: var(--s6); }
  .tf-points { grid-template-columns: 1fr 1fr; gap: var(--s6); }
}

@media (min-width: 900px) {
  .tf-headline {
    grid-template-columns: auto 1fr;
    justify-items: start;
    gap: var(--s6);
    padding: var(--s7) var(--s6);
    text-align: start;
  }
}

@media (prefers-reduced-motion: reduce) {
  .tf-ring-fill { transition: none !important; filter: none; }
}

/* On paper the panel becomes a plain bordered block: a dark rectangle
   costs a cartridge and reads worse than the text it contains. */
@media print {
  .tf-headline {
    background: none;
    color: var(--text);
    border: 1px solid var(--line);
    page-break-inside: avoid;
  }

  .tf-score-label, .tf-band-reading { color: var(--text-soft); }
  .tf-band { color: var(--text); }
  .tf-ring-track { stroke: var(--line); }
  .tf-ring-fill { stroke: var(--text); filter: none; }

  #tf-form, .tf-privacy { display: none !important; }

  /* The working is the most useful thing to have on paper, so it is
     forced open rather than printed as a collapsed summary. */
  .tf-show-work[open] > .disclosure-body,
  .tf-show-work > .disclosure-body { display: block !important; }
}

/* ==========================================================================
   30. PLATFORM LAYOUT — LISTING CARDS, HOMEPAGE, HUBS

   One design system for both halves of the site. Calculators and tools
   share every geometry here — the card, the icon tile, the pill, the hub
   head — and differ in exactly one thing: the hue. Teal is the calculator
   half, violet the tools half.

   The hue is carried by a handful of per-component custom properties
   (--card-*, --hub-*, --spot-*) set once from the FIXED identity roles
   (--calc-*, --tool-*, --tile-*). Those roles are never remapped by the
   tools scope, so a calculator card stays teal on a tools page and a tool
   card stays violet on the homepage.

   Motion is small: a two-pixel lift, a warmed border, an arrow that leans
   forward. Every transform is cancelled at the end of this section under
   prefers-reduced-motion. No keyframes are defined here.
   ========================================================================== */

/* --------------------------------------------------------------------
   Grids

   Three columns on a desktop, two on a tablet, one on a phone — set
   explicitly rather than falling out of a minimum width, so a hub reads
   as the same grid at every laptop size.
   ------------------------------------------------------------------ */

.card-grid {
  display: grid;
  gap: var(--s4);
  grid-template-columns: minmax(0, 1fr);
}
@media (min-width: 640px) {
  .card-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (min-width: 1024px) {
  .card-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
/* A short rail (the homepage's popular picks) fits four across. */
@media (min-width: 1200px) {
  .card-grid-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}
/* Category cards are shorter than listing cards, so a desktop row takes
   four unless the set divides evenly by three — seven calculator
   categories read 4 + 3, six tool categories 3 + 3. */
@media (min-width: 1024px) {
  .card-grid-cats { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  .card-grid-cats.card-grid-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

/* The older grid names, still used by the 404 page and the calculator
   pages' related rails. */
.grid-3,
.tool-grid {
  display: grid;
  gap: var(--s4);
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 17.5rem), 1fr));
}

/* --------------------------------------------------------------------
   Listing cards — calculator (.tool-card) and tool (.utool)

   Icon tile, then a column holding the name, one line of description
   and a foot row with the category and the onward arrow. The calculator
   card is itself the link; the tool card is an article whose title link
   is stretched over it, so its favourite button can be a real button.
   ------------------------------------------------------------------ */

.tool-card,
.utool,
.cat-card {
  --card-strong: var(--calc-strong);
  --card-soft:   var(--calc-soft);
  --card-line:   var(--calc-line);
  --card-tile:   var(--tile-calc);
  --card-base:   var(--brand-600);

  position: relative;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: start;
  gap: 14px;
  min-height: 100%;
  padding: var(--s4);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-card);
  box-shadow: var(--shadow-sm);
  color: var(--text);
  text-decoration: none;
  transition: border-color var(--dur) var(--ease),
              box-shadow var(--dur) var(--ease),
              transform var(--dur) var(--ease);
}

.utool,
.cat-card-tool {
  --card-strong: var(--tool-strong);
  --card-soft:   var(--tool-soft);
  --card-line:   var(--tool-line);
  --card-tile:   var(--tile-tool);
  --card-base:   var(--violet-600);
}

.tool-card:hover,
.tool-card:focus-visible,
.utool:hover,
.utool:focus-within,
.cat-card:hover,
.cat-card:focus-visible {
  border-color: var(--card-line);
  box-shadow: var(--shadow-md);
  color: var(--text);
  transform: translateY(-2px);
}

/* The icon tile: filled with the half's gradient, glyph in white. */
.tool-card-icon,
.utool-icon,
.cat-card-icon {
  flex-shrink: 0;
  display: grid;
  place-items: center;
  width: 46px; height: 46px;
  margin: 0;
  border: 0;
  border-radius: 13px;
  background-color: var(--card-base);
  background-image: var(--card-tile);
  color: var(--on-tile);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .18);
  transition: transform var(--dur) var(--ease);
}
.tool-card-icon svg,
.utool-icon svg,
.cat-card-icon svg { width: 23px; height: 23px; }

.tool-card:hover .tool-card-icon,
.tool-card:focus-visible .tool-card-icon,
.utool:hover .utool-icon,
.utool:focus-within .utool-icon,
.cat-card:hover .cat-card-icon,
.cat-card:focus-visible .cat-card-icon { transform: scale(1.05); }

.tool-card-body,
.utool-body,
.cat-card-body {
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-width: 0;
  min-height: 100%;
}

.tool-card h3,
.utool h3,
.cat-card h3 {
  margin: 0;
  font-size: 1rem;
  font-weight: 700;
  letter-spacing: -0.01em;
  line-height: 1.3;
  color: var(--text);
}

.tool-card p,
.utool p,
.cat-card p {
  margin: 0;
  font-size: 0.875rem;
  line-height: 1.5;
  color: var(--text-soft);
}

/* The tool card's title link covers the whole card. */
.utool-link { color: inherit; text-decoration: none; }
.utool-link:hover { color: inherit; }
.utool-link::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: var(--r-card);
}
/* Keyboard focus rings the card, not the few words of the title. */
.utool-link:focus-visible { outline: none; }
.utool-link:focus-visible::after {
  outline: 3px solid var(--focus);
  outline-offset: 2px;
}

.card-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s2);
  margin-top: auto;
  padding-top: 10px;
}
.card-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  min-width: 0;
}
.card-foot-end {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  flex-shrink: 0;
}

/* The category, in words and in the half's colour. */
.card-cat {
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--card-strong);
}

.card-arrow {
  display: grid;
  place-items: center;
  width: 30px; height: 30px;
  flex-shrink: 0;
  border-radius: 50%;
  color: var(--text-faint);
  transition: color var(--dur) var(--ease),
              background-color var(--dur) var(--ease),
              transform var(--dur) var(--ease);
}
.card-arrow svg { width: 16px; height: 16px; }

.tool-card:hover .card-arrow,
.tool-card:focus-visible .card-arrow,
.utool:hover .card-arrow,
.utool:focus-within .card-arrow,
.cat-card:hover .card-arrow,
.cat-card:focus-visible .card-arrow {
  color: var(--card-strong);
  background: var(--card-soft);
  transform: translateX(3px);
}

.cat-card .count {
  font-size: 0.78rem;
  font-weight: 650;
  color: var(--card-strong);
}

/* The status badge's final size and case. It used to live with the tool
   cards; the cards no longer carry badges, but the calculators' status
   badges (target planning, eligibility, denomination tags) have always
   rendered with this override on top of the base rule in section 18, so
   it stays exactly as it was. */
.badge {
  display: inline-flex;
  align-items: center;
  padding: 3px 8px;
  border-radius: var(--r-full);
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

/* Favourite. Above the stretched card link, so it stays clickable. */
.utool-fav {
  position: relative;
  z-index: 1;
  display: grid;
  place-items: center;
  width: 36px; height: 36px;
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--r-full);
  color: var(--text-faint);
  cursor: pointer;
  transition: color var(--dur-fast) var(--ease),
              background-color var(--dur-fast) var(--ease),
              border-color var(--dur-fast) var(--ease),
              transform var(--dur-fast) var(--ease);
}
.utool-fav svg { width: 18px; height: 18px; }
.utool-fav .icon-on { display: none; }
.utool-fav:hover {
  color: var(--tool-strong);
  background: var(--tool-soft);
  border-color: var(--tool-line);
  transform: scale(1.08);
}
.utool-fav[aria-pressed="true"] { color: var(--tool-strong); }
.utool-fav[aria-pressed="true"] .icon-on  { display: block; }
.utool-fav[aria-pressed="true"] .icon-off { display: none; }
.utool-fav[aria-pressed="true"] .icon-on path { fill: currentColor; }

@media (max-width: 399px) {
  .tool-card, .utool, .cat-card { gap: var(--s3); padding: var(--s3); }
  .tool-card-icon, .utool-icon, .cat-card-icon { width: 40px; height: 40px; border-radius: 11px; }
  .tool-card-icon svg, .utool-icon svg, .cat-card-icon svg { width: 20px; height: 20px; }
}

/* --------------------------------------------------------------------
   Filter pills — the category row above a hub grid

   Real buttons with aria-pressed, driven by assets/js/tools.js. The
   pressed pill is filled with the page accent: teal on the calculator
   hub, violet on the tools hub, where the tools scope remaps --accent.
   On a phone the row scrolls sideways instead of wrapping into a block.
   ------------------------------------------------------------------ */

.filter-pills {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
  margin: 0;
  padding: 0;
  list-style: none;
}

.filter-pill {
  display: inline-flex;
  align-items: center;
  min-height: 40px;
  padding: 7px var(--s4);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-full);
  font: inherit;
  font-size: 0.9rem;
  font-weight: 600;
  line-height: 1.2;
  color: var(--text-soft);
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease),
              color var(--dur-fast) var(--ease),
              background-color var(--dur-fast) var(--ease);
}
.filter-pill:hover { border-color: var(--line-strong); color: var(--text); }
.filter-pill[aria-pressed="true"] {
  background-color: var(--accent);
  background-image: var(--grad-btn);
  border-color: transparent;
  color: var(--on-accent);
}

.hub-toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--s3) var(--s4);
  margin-bottom: var(--s5);
}
.hub-toolbar .filter-pills { flex: 1 1 480px; min-width: 0; }
.hub-toolbar-end {
  display: flex;
  align-items: center;
  gap: var(--s3);
  margin-inline-start: auto;
}
.hub-toolbar .tool-count { margin: 0; white-space: nowrap; }
.hub-toolbar .tool-filter { margin: 0; }
.hub-toolbar .select { min-height: 40px; border-radius: var(--r-full); }

@media (max-width: 639px) {
  .hub-toolbar { gap: var(--s3); }
  .hub-toolbar .filter-pills {
    flex: 1 1 100%;
    flex-wrap: nowrap;
    overflow-x: auto;
    margin-inline: calc(var(--s4) * -1);
    padding: 2px var(--s4) var(--s2);
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
  }
  .hub-toolbar .filter-pills::-webkit-scrollbar { display: none; }
  .hub-toolbar-end { width: 100%; justify-content: space-between; margin: 0; }
  /* A translated sort label ("Сортировка: рекомендуемые") is wider than
     the English one; the select shrinks to the row instead of pushing the
     page sideways. */
  .hub-toolbar .tool-count { flex-shrink: 0; }
  .hub-toolbar .tool-filter { flex: 0 1 auto; min-width: 0; }
  .hub-toolbar .select { width: 100%; min-width: 0; max-width: 100%; text-overflow: ellipsis; }
}

/* --------------------------------------------------------------------
   Hub head — /calculators/, /tools/ and both kinds of category page

   Breadcrumb, h1, one line and a count, with the half's mark on the far
   side. The mark is aria-hidden and carries no words, so it never needs
   translating and never repeats the heading to a screen reader.
   ------------------------------------------------------------------ */

.hub-head {
  --hub-glow:   var(--glow-calc-soft);
  --hub-tile:   var(--tile-calc);
  --hub-base:   var(--brand-600);
  --hub-strong: var(--calc-strong);
  --hub-soft:   var(--calc-soft);
  --hub-line:   var(--calc-line);

  position: relative;
  isolation: isolate;
  overflow: hidden;
  padding-bottom: var(--s6);
  border-bottom: 1px solid var(--line);
}
.hub-head-tool {
  --hub-glow:   var(--glow-tool-soft);
  --hub-tile:   var(--tile-tool);
  --hub-base:   var(--violet-600);
  --hub-strong: var(--tool-strong);
  --hub-soft:   var(--tool-soft);
  --hub-line:   var(--tool-line);
}
.hub-head::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: radial-gradient(50% 120% at 88% 0%, var(--hub-glow) 0%, transparent 70%);
  pointer-events: none;
}

.hub-head-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  align-items: center;
  gap: var(--s5);
  padding-top: var(--s5);
}
@media (min-width: 768px) {
  .hub-head-grid { grid-template-columns: minmax(0, 1fr) auto; }
}

.hub-head h1 {
  margin-bottom: var(--s3);
  font-size: clamp(2rem, 1.45rem + 2.2vw, 3rem);
  font-weight: 800;
  letter-spacing: -0.03em;
  line-height: 1.1;
}
.hub-head .lede { margin-bottom: var(--s4); max-width: 62ch; }
.hub-head .eyebrow { margin-bottom: var(--s1); color: var(--hub-strong); }

.hub-head-count {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  margin: 0;
  padding: 5px var(--s3);
  background: var(--hub-soft);
  border: 1px solid var(--hub-line);
  border-radius: var(--r-full);
  font-size: 0.85rem;
  font-weight: 650;
  color: var(--hub-strong);
}
.hub-head-count svg { width: 15px; height: 15px; }

.hub-head-art {
  position: relative;
  display: none;
  width: 190px; height: 150px;
  margin-inline-end: var(--s4);
}
@media (min-width: 768px) {
  .hub-head-art { display: block; }
}
.hub-art-tile {
  position: absolute;
  inset: 12px 36px;
  display: grid;
  place-items: center;
  border-radius: 28px;
  background-color: var(--hub-base);
  background-image: var(--hub-tile);
  color: var(--on-tile);
  box-shadow: 0 26px 40px -22px rgba(0, 0, 0, .6), inset 0 1px 0 rgba(255, 255, 255, .22);
  transform: rotate(-8deg);
}
.hub-art-tile svg { width: 62px; height: 62px; stroke-width: 1.5; }
.hub-art-chip {
  position: absolute;
  display: grid;
  place-items: center;
  width: 48px; height: 48px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 14px;
  box-shadow: var(--shadow-md);
  color: var(--hub-strong);
}
.hub-art-chip svg { width: 22px; height: 22px; }
.hub-art-chip-a { top: 0; inset-inline-end: 0; transform: rotate(9deg); }
.hub-art-chip-b { bottom: 0; inset-inline-start: 0; transform: rotate(-7deg); }

/* The category page variant: an icon tile beside the heading. */
.hub-head-compact .hub-head-copy { padding-top: var(--s5); }
.hub-head-with-icon {
  display: flex;
  align-items: flex-start;
  gap: var(--s4);
}
.hub-head-icon {
  flex-shrink: 0;
  display: grid;
  place-items: center;
  width: 58px; height: 58px;
  margin-top: 6px;
  border-radius: 17px;
  background-color: var(--hub-base);
  background-image: var(--hub-tile);
  color: var(--on-tile);
  box-shadow: 0 16px 30px -18px rgba(0, 0, 0, .55), inset 0 1px 0 rgba(255, 255, 255, .2);
}
.hub-head-icon svg { width: 29px; height: 29px; }
.hub-head-compact h1 { font-size: clamp(1.8rem, 1.4rem + 1.6vw, 2.6rem); }

@media (max-width: 479px) {
  .hub-head-with-icon { flex-direction: column; gap: var(--s3); }
  .hub-head-icon { width: 46px; height: 46px; margin-top: 0; border-radius: 13px; }
  .hub-head-icon svg { width: 23px; height: 23px; }
}

/* The category's intro paragraph, under the head. */
.cat-intro {
  max-width: 78ch;
  margin-top: var(--s5);
  padding-inline-start: var(--s4);
  border-inline-start: 3px solid var(--hub-line, var(--accent-line));
}
.cat-intro p { margin-bottom: 0; color: var(--text-soft); }

.hub-section { padding-top: var(--s6); }
.hub-section:first-child { padding-top: var(--s5); }
.hub-section-end { padding-top: var(--s5); }
.hub-section .section-head h2,
.hub-section .home-head h2 { font-size: clamp(1.3rem, 1.12rem + .7vw, 1.7rem); }

/* Anchor targets sit under a sticky header; a jump must stop short of it. */
[id^="all-"],
#categories { scroll-margin-top: calc(var(--header-h) + var(--s5)); }

.chip-row-start { justify-content: flex-start; }

.cat-hub-link { margin-bottom: 0; }
.cat-hub-link a {
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  font-weight: 600;
  text-decoration: none;
}
.cat-hub-link a:hover { text-decoration: underline; }
.cat-hub-link svg {
  width: 14px;
  height: 14px;
  flex-shrink: 0;
  transition: transform var(--dur) var(--ease);
}
.cat-hub-link a:hover svg { transform: translateX(3px); }

.chip {
  transition: border-color var(--dur-fast) var(--ease),
              color var(--dur-fast) var(--ease),
              background-color var(--dur-fast) var(--ease);
}
.chip:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 2px;
}

/* Across to the other half of the site, at the foot of a hub. */
.crosslink {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s4);
  padding: var(--s5);
  border: 1px solid var(--calc-line);
  border-radius: 20px;
  background: var(--world-calc-bg);
}
.crosslink-tool { border-color: var(--tool-line); background: var(--world-tool-bg); }
.crosslink-icon {
  flex-shrink: 0;
  display: grid;
  place-items: center;
  width: 50px; height: 50px;
  border-radius: 14px;
  background-color: var(--brand-600);
  background-image: var(--tile-calc);
  color: var(--on-tile);
}
.crosslink-tool .crosslink-icon { background-color: var(--violet-600); background-image: var(--tile-tool); }
.crosslink-icon svg { width: 25px; height: 25px; }
.crosslink-body { flex: 1 1 260px; min-width: 0; }
.crosslink h2 { margin: 0 0 var(--s1); font-size: 1.25rem; }
.crosslink p { margin: 0; color: var(--text-soft); font-size: 0.93rem; }

/* --------------------------------------------------------------------
   Homepage — hero
   ------------------------------------------------------------------ */

.home-hero {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  padding-block: var(--s7) var(--s6);
}
.home-hero::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background:
    radial-gradient(38% 60% at 78% 40%, var(--glow-tool-soft) 0%, transparent 70%),
    radial-gradient(42% 60% at 8% 0%, var(--glow-calc-soft) 0%, transparent 70%);
  pointer-events: none;
}

.home-hero-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  align-items: center;
  gap: var(--s6);
}
@media (min-width: 960px) {
  .home-hero-grid { grid-template-columns: minmax(0, 1.08fr) minmax(0, .92fr); gap: var(--s7); }
}

.home-eyebrow {
  margin: 0 0 var(--s3);
  font-size: 0.8rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--calc-strong);
}

.home-hero h1 {
  margin: 0 0 var(--s4);
  font-size: clamp(2.5rem, 1.6rem + 3.8vw, 4.2rem);
  font-weight: 800;
  line-height: 1.04;
  letter-spacing: -0.038em;
}
.home-hero h1 .hl-calc { color: var(--calc-accent); }
.home-hero h1 .hl-tool { color: var(--tool-accent); }
.home-hero h1 .hl-plain { display: block; color: var(--text); }
/* The eyebrow line lives inside the h1 (so the heading names the site's
   purpose) but must look exactly as it did as a paragraph above it: its own
   block, body line-height, and the h1's tight tracking and balance undone. */
.home-hero h1 .home-eyebrow { display: block; line-height: 1.65; text-wrap: wrap; }

.home-hero-sub {
  max-width: 54ch;
  margin: 0 0 var(--s5);
  font-size: clamp(1rem, .95rem + .3vw, 1.15rem);
  line-height: 1.6;
  color: var(--text-soft);
}

.home-hero-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s3);
  margin-bottom: var(--s5);
}
@media (max-width: 479px) {
  .home-hero-actions { flex-direction: column; }
  .home-hero-actions .btn { width: 100%; }
}

.home-facts {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2) var(--s5);
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: 0.9rem;
  color: var(--text-soft);
}
.home-facts li { display: inline-flex; align-items: center; gap: 8px; }
.home-facts svg { width: 17px; height: 17px; flex-shrink: 0; color: var(--calc-accent); }

/* The two halves as two stacked cards on a loose orbit. Static: the tilt
   is a resting pose, not an animation. */
.home-hero-art {
  position: relative;
  width: 100%;
  max-width: 460px;
  aspect-ratio: 460 / 380;
  margin-inline: auto;
}
@media (max-width: 959px) { .home-hero-art { max-width: 400px; } }
@media (max-width: 479px) { .home-hero-art { max-width: 330px; } }

.home-orbit {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
}
.home-orbit ellipse { fill: none; stroke: var(--line-strong); stroke-width: 1.2; }
.home-orbit .dot-calc { fill: var(--calc-accent); }
.home-orbit .dot-tool { fill: var(--tool-accent); }
.home-orbit .spark { fill: var(--tool-accent); opacity: .85; }
.home-orbit .spark-sm { fill: var(--calc-accent); }

.art-card {
  position: absolute;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  width: 44%;
  aspect-ratio: 1 / 1.08;
  padding: var(--s3);
  border-radius: 26px;
  color: #ffffff;
  text-align: center;
  box-shadow: 0 34px 60px -30px rgba(0, 0, 0, .65), inset 0 1px 0 rgba(255, 255, 255, .22);
}
.art-card-calc {
  top: 12%;
  left: 9%;
  background-color: var(--brand-600);
  background-image: linear-gradient(150deg, var(--brand-500) 0%, var(--brand-800) 100%);
  transform: rotate(-9deg);
}
.art-card-tool {
  top: 30%;
  right: 7%;
  background-color: var(--violet-600);
  background-image: linear-gradient(150deg, var(--violet-500) 0%, var(--violet-800) 100%);
  transform: rotate(8deg);
}
.art-card-icon {
  display: grid;
  place-items: center;
  width: 30%;
  aspect-ratio: 1;
  margin-bottom: 4px;
  border-radius: 16px;
  background: rgba(255, 255, 255, .16);
  border: 1px solid rgba(255, 255, 255, .3);
}
.art-card-icon svg { width: 58%; height: 58%; }
.art-card-title {
  font-size: clamp(1.05rem, .85rem + .9vw, 1.45rem);
  font-weight: 750;
  letter-spacing: -0.02em;
  line-height: 1.1;
}
.art-card-sub { font-size: clamp(.68rem, .62rem + .25vw, .8rem); opacity: .88; }

.art-note {
  position: absolute;
  right: 0;
  bottom: 2%;
  font-family: "Segoe Print", "Bradley Hand", "Snell Roundhand", cursive;
  font-size: clamp(.85rem, .75rem + .4vw, 1.05rem);
  color: var(--text-soft);
  white-space: nowrap;
  transform: rotate(-7deg);
}
/* On a phone the note sits under the cards, centred, so the hero's
   overflow clip never cuts it off. */
@media (max-width: 559px) {
  .art-note { right: auto; left: 50%; bottom: -2%; transform: translateX(-50%) rotate(-4deg); }
}

/* --------------------------------------------------------------------
   Homepage — section heads
   ------------------------------------------------------------------ */

.home-section { padding-block: var(--s5); }

.home-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s3);
  margin-bottom: var(--s4);
}
.home-head-icon {
  flex-shrink: 0;
  display: grid;
  place-items: center;
  width: 40px; height: 40px;
  border-radius: 12px;
  color: var(--on-tile);
}
.home-head-icon svg { width: 21px; height: 21px; }
.home-head-icon-calc { background-color: var(--brand-600); background-image: var(--tile-calc); }
.home-head-icon-tool { background-color: var(--violet-600); background-image: var(--tile-tool); }
.home-head-icon-hot {
  background: var(--warn-bg);
  border: 1px solid var(--warn-line);
  color: var(--warn-fg);
}
.home-head-text { flex: 1 1 240px; min-width: 0; }
.home-head h2 {
  margin: 0;
  font-size: clamp(1.35rem, 1.15rem + .8vw, 1.8rem);
  font-weight: 750;
  letter-spacing: -0.022em;
}
.home-head p { margin: 2px 0 0; font-size: 0.93rem; color: var(--text-soft); }
.home-head .section-more { margin-inline-start: auto; }
.home-head-center { justify-content: center; text-align: center; }
.home-head-center .home-head-text { flex: 0 1 auto; }

.section-more-tool { color: var(--tool-accent); }
.section-more-tool:hover { color: var(--tool-strong); }

/* --------------------------------------------------------------------
   Homepage — the two halves
   ------------------------------------------------------------------ */

.worlds {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--s4);
}
@media (min-width: 860px) {
  .worlds { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

.world {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  display: flex;
  align-items: flex-start;
  gap: var(--s5);
  padding: var(--s6);
  background: var(--world-calc-bg);
  border: 1px solid var(--calc-line);
  border-radius: 22px;
}
.world-tool { background: var(--world-tool-bg); border-color: var(--tool-line); }

.world-icon {
  flex-shrink: 0;
  display: grid;
  place-items: center;
  width: 66px; height: 66px;
  border-radius: 19px;
  background-color: var(--brand-600);
  background-image: var(--tile-calc);
  color: var(--on-tile);
  box-shadow: 0 18px 30px -18px rgba(0, 0, 0, .55), inset 0 1px 0 rgba(255, 255, 255, .2);
}
.world-tool .world-icon { background-color: var(--violet-600); background-image: var(--tile-tool); }
.world-icon svg { width: 34px; height: 34px; }

.world-body { position: relative; z-index: 1; min-width: 0; }
.world h2 {
  margin: 0 0 var(--s2);
  font-size: clamp(1.5rem, 1.25rem + 1vw, 2rem);
  font-weight: 800;
  letter-spacing: -0.025em;
}
.world p { max-width: 44ch; margin-bottom: var(--s3); color: var(--text-soft); }
.world .world-count {
  margin-bottom: var(--s4);
  font-size: 0.85rem;
  font-weight: 650;
  color: var(--calc-strong);
}
.world-tool .world-count { color: var(--tool-strong); }

.world-art {
  position: absolute;
  inset-inline-end: -26px;
  bottom: -34px;
  z-index: 0;
  width: 190px; height: 190px;
  color: var(--calc-accent);
  opacity: .12;
  transform: rotate(-12deg);
  pointer-events: none;
}
.world-tool .world-art { color: var(--tool-accent); }
.world-art svg { width: 100%; height: 100%; stroke-width: 1.2; }

@media (max-width: 559px) {
  .world { flex-direction: column; gap: var(--s4); padding: var(--s5) var(--s4); }
  .world-icon { width: 52px; height: 52px; border-radius: 15px; }
  .world-icon svg { width: 27px; height: 27px; }
}

/* --------------------------------------------------------------------
   Homepage — spotlights (featured calculator, featured tool)
   ------------------------------------------------------------------ */

.spotlight {
  --spot-glow:   var(--glow-calc-soft);
  --spot-strong: var(--calc-strong);
  --spot-soft:   var(--calc-soft);
  --spot-line:   var(--calc-line);
  --spot-tile:   var(--tile-calc);
  --spot-base:   var(--brand-600);

  position: relative;
  isolation: isolate;
  overflow: hidden;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  align-items: center;
  gap: var(--s5);
  padding: var(--s6) var(--s5);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 22px;
}
.spotlight-tool {
  --spot-glow:   var(--glow-tool-soft);
  --spot-strong: var(--tool-strong);
  --spot-soft:   var(--tool-soft);
  --spot-line:   var(--tool-line);
  --spot-tile:   var(--tile-tool);
  --spot-base:   var(--violet-600);
}
.spotlight::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: radial-gradient(55% 90% at 100% 50%, var(--spot-glow) 0%, transparent 72%);
  pointer-events: none;
}
@media (min-width: 900px) {
  .spotlight {
    grid-template-columns: minmax(0, 1.1fr) minmax(0, .9fr);
    gap: var(--s7);
    padding: var(--s7);
  }
}

.spotlight-eyebrow {
  display: inline-block;
  margin-bottom: var(--s2);
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--spot-strong);
}
.spotlight h2 {
  margin-bottom: var(--s3);
  font-size: clamp(1.6rem, 1.3rem + 1.2vw, 2.3rem);
  font-weight: 800;
  letter-spacing: -0.028em;
}
.spotlight-copy > p { max-width: 54ch; color: var(--text-soft); }

.spotlight-list {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 10px var(--s4);
  margin: 0 0 var(--s5);
  padding: 0;
  list-style: none;
}
@media (min-width: 560px) {
  .spotlight-list { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
.spotlight-list li {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  font-size: 0.92rem;
  line-height: 1.45;
  color: var(--text-soft);
}
.spotlight-list strong { color: var(--text); font-weight: 650; }
.spotlight-tick {
  flex-shrink: 0;
  display: grid;
  place-items: center;
  width: 22px; height: 22px;
  margin-top: 1px;
  background: var(--spot-soft);
  border: 1px solid var(--spot-line);
  border-radius: 50%;
  color: var(--spot-strong);
}
.spotlight-tick svg { width: 13px; height: 13px; stroke-width: 2.5; }

/* The worked example. A figure, captioned as an example, with the inputs
   the calculator's own preset uses and the figures its library returns. */
.spotlight-preview {
  margin: 0;
  padding: var(--s4);
  background: var(--surface-sunk);
  border: 1px solid var(--line);
  border-radius: 18px;
  box-shadow: var(--shadow-md);
}
.preview-head {
  display: flex;
  align-items: center;
  gap: var(--s3);
  margin-bottom: var(--s3);
}
.preview-icon {
  flex-shrink: 0;
  display: grid;
  place-items: center;
  width: 40px; height: 40px;
  border-radius: 12px;
  background-color: var(--spot-base);
  background-image: var(--spot-tile);
  color: var(--on-tile);
}
.preview-icon svg { width: 20px; height: 20px; }
.preview-title { display: block; font-size: 0.98rem; font-weight: 700; color: var(--text); }
.preview-sub { display: block; font-size: 0.78rem; color: var(--text-faint); }

.preview-inputs {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(88px, 1fr));
  gap: 6px;
  margin: 0 0 var(--s3);
}
.preview-inputs div {
  padding: 6px 10px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 10px;
}
.preview-inputs dt {
  font-size: 0.68rem;
  font-weight: 650;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--text-faint);
}
.preview-inputs dd {
  margin: 0;
  font-size: 0.9rem;
  font-weight: 650;
  font-variant-numeric: tabular-nums;
}

.preview-results {
  display: grid;
  grid-template-columns: 1.3fr 1fr 1fr;
  gap: 6px;
}
.preview-result {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 10px var(--s3);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 12px;
}
.preview-result-main { background: var(--spot-soft); border-color: var(--spot-line); }
.preview-k { font-size: 0.72rem; font-weight: 600; color: var(--text-faint); }
.preview-result-main .preview-k { color: var(--spot-strong); }
.preview-v {
  font-size: 1.15rem;
  font-weight: 750;
  font-variant-numeric: tabular-nums;
  color: var(--text);
}
.preview-result-main .preview-v { font-size: 1.5rem; color: var(--spot-strong); }

@media (max-width: 419px) {
  .preview-results { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .preview-result-main { grid-column: 1 / -1; }
}

/* The featured tool's conversion, drawn. */
.convert-visual {
  display: grid;
  grid-template-columns: auto minmax(40px, 1fr) auto;
  align-items: center;
  gap: var(--s3);
  padding: var(--s5);
  background: var(--surface-sunk);
  border: 1px solid var(--line);
  border-radius: 18px;
}
.file-tile {
  display: grid;
  justify-items: center;
  align-content: center;
  gap: var(--s2);
  width: clamp(92px, 24vw, 124px);
  aspect-ratio: .82;
  padding: var(--s3);
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: 14px 26px 14px 14px;
  color: var(--text-soft);
  box-shadow: var(--shadow-sm);
}
.file-tile svg { width: 36px; height: 36px; }
.file-ext {
  padding: 2px var(--s2);
  background: var(--surface-sunk);
  border: 1px solid var(--line);
  border-radius: 6px;
  font-family: var(--font-mono);
  font-size: 0.85rem;
  font-weight: 750;
  color: var(--text);
}
.file-tile-to {
  background: var(--tool-soft);
  border-color: var(--tool-line);
  color: var(--tool-strong);
}
.file-tile-to .file-ext {
  background-color: var(--violet-600);
  background-image: var(--tile-tool);
  border-color: transparent;
  color: var(--on-tile);
}
.convert-arrow {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  height: 42px;
}
.convert-track {
  position: absolute;
  inset-inline: 0;
  top: 50%;
  border-top: 2px dashed var(--tool-line);
}
.convert-arrow svg {
  position: relative;
  width: 38px; height: 38px;
  padding: 9px;
  border-radius: 50%;
  background-color: var(--violet-600);
  background-image: var(--tile-tool);
  color: var(--on-tile);
  box-shadow: var(--shadow-md);
}
.convert-caption {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--s2);
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--text-soft);
}
.convert-caption svg { width: 16px; height: 16px; color: var(--tool-accent); }

/* Below 360px the two 92px tiles, the arrow, the gaps and the padding add
   up to ~296px inside a ~232px box, so the right tile and the caption were
   cut off by the spotlight's overflow: hidden. Tighten only there; every
   wider screen keeps the approved sizes. */
@media (max-width: 359px) {
  .convert-visual { padding: var(--s4); gap: var(--s2); }
  .convert-visual .file-tile { width: 72px; padding: var(--s2); }
  .convert-caption { flex-wrap: wrap; text-align: center; }
}

/* --------------------------------------------------------------------
   Homepage — why this site
   ------------------------------------------------------------------ */

.why-panel {
  padding: var(--s6) var(--s5);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 22px;
}
.why-head { margin-bottom: var(--s5); text-align: center; }
.why-head h2 {
  margin-bottom: var(--s2);
  font-size: clamp(1.4rem, 1.2rem + .8vw, 1.9rem);
  font-weight: 800;
  letter-spacing: -0.022em;
}
.why-head p { margin: 0 auto; color: var(--text-soft); }

.why-list {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--s5) var(--s4);
  margin: 0;
  padding: 0;
  list-style: none;
}
@media (min-width: 560px) {
  .why-list { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (min-width: 1024px) {
  .why-list { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}
.why-item {
  display: flex;
  align-items: flex-start;
  gap: var(--s3);
  font-size: 0.9rem;
  line-height: 1.5;
  color: var(--text-soft);
}
.why-item strong { display: block; margin-bottom: 2px; font-size: 1rem; color: var(--text); }
.why-icon {
  flex-shrink: 0;
  display: grid;
  place-items: center;
  width: 48px; height: 48px;
  border-radius: 50%;
  color: var(--on-tile);
}
.why-icon svg { width: 22px; height: 22px; }
.why-icon-calc { background-color: var(--brand-600); background-image: var(--tile-calc); }
.why-icon-tool { background-color: var(--violet-600); background-image: var(--tile-tool); }

/* --------------------------------------------------------------------
   Reduced motion — everything in section 30

   The page is complete with all of it off: cards sit where they are,
   icons keep their tile, arrows keep their colour without travelling.
   ------------------------------------------------------------------ */

@media (prefers-reduced-motion: reduce) {
  .tool-card:hover,
  .tool-card:focus-visible,
  .utool:hover,
  .utool:focus-within,
  .cat-card:hover,
  .cat-card:focus-visible,
  .tool-card:hover .tool-card-icon,
  .tool-card:focus-visible .tool-card-icon,
  .utool:hover .utool-icon,
  .utool:focus-within .utool-icon,
  .cat-card:hover .cat-card-icon,
  .cat-card:focus-visible .cat-card-icon,
  .tool-card:hover .card-arrow,
  .tool-card:focus-visible .card-arrow,
  .utool:hover .card-arrow,
  .utool:focus-within .card-arrow,
  .cat-card:hover .card-arrow,
  .cat-card:focus-visible .card-arrow,
  .cat-hub-link a:hover svg { transform: none !important; }

  /* Not `none` — in RTL the chevron rests mirrored, and none would
     unflip it on hover. rtl.css restores the mirror; this stops the
     travel only. */
  .btn:hover svg[data-icon="arrow-right"] { transform: translateX(0) !important; }
}

/* ==========================================================================
   28. UPLOAD PROGRESS AND RESULT THUMBNAILS

   Both belong to the shared converter workspace. The bar is a plain
   width transition — no animation loop, no script running between
   files — and the thumbnail reuses the object URL the download link
   already holds, so neither adds a network request or a second copy of
   the file in memory.
   ========================================================================== */

.tool-progress {
  height: 6px;
  margin-bottom: var(--s4);
  background: var(--surface-sunk);
  border: 1px solid var(--line);
  border-radius: var(--r-full);
  overflow: hidden;
}

.tool-progress-bar {
  display: block;
  width: 0;
  height: 100%;
  background-image: var(--grad-calc);
  border-radius: inherit;
  /* Width only. The bar moves once per finished file, so a short ease
     reads as progress rather than as a jump. */
  transition: width var(--dur) var(--ease);
}

/* A tool page is violet, so its bar is too. */
.tools-page .tool-progress-bar { background-image: var(--grad-tool); }

/* The converted-file thumbnail. A fixed box with object-fit means a
   portrait and a landscape result line up in the same column and
   nothing reflows as each one decodes. */
.file-row-thumb {
  flex-shrink: 0;
  width: 40px;
  height: 40px;
  border-radius: var(--r-sm);
  border: 1px solid var(--line);
  background: var(--surface-sunk);
  object-fit: cover;
}

@media (prefers-reduced-motion: reduce) {
  .tool-progress-bar { transition: none !important; }
}

/* ==========================================================================
   29. CALCULATION HISTORY AND FEEDBACK

   Both blocks sit at the end of a calculator or tool page. They are
   quiet by design: this is a record of something the visitor already
   did and a single yes/no question, neither of which should compete
   with the calculator above them.
   ========================================================================== */

.calc-extras {
  border-top: 1px solid var(--line);
  padding-top: var(--s6);
}

.calc-extras h2 {
  margin: 0;
  font-size: 1.05rem;
  font-weight: 650;
  letter-spacing: -0.01em;
}

.calc-extras .hint {
  margin-top: var(--s2);
  font-size: 0.82rem;
  color: var(--text-faint);
}

/* --------------------------------------------------------------------
   History
   ------------------------------------------------------------------ */

.calc-history { margin-bottom: var(--s6); }

.calc-extras-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s3);
  flex-wrap: wrap;
}

.calc-history-list {
  list-style: none;
  margin: var(--s4) 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--s2);
}

.calc-history-item {
  display: flex;
  align-items: flex-start;
  gap: var(--s3);
  padding: var(--s3);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
}

.calc-history-body { flex: 1 1 auto; min-width: 0; }

.calc-history-when {
  display: block;
  font-size: 0.76rem;
  font-variant-numeric: tabular-nums;
  color: var(--text-faint);
}

.calc-history-text {
  margin: 2px 0 0;
  font-size: 0.875rem;
  line-height: 1.5;
  color: var(--text-soft);
  /* A summary can be long. Two lines is enough to recognise which
     calculation this was, which is all history has to do. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* 32px is the visible tile; the padding takes the real target past 40px
   without pushing the row taller. */
.calc-history-remove {
  flex-shrink: 0;
  display: grid;
  place-items: center;
  width: 32px;
  height: 32px;
  padding: 4px;
  background: transparent;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  color: var(--text-faint);
  font-size: 1.05rem;
  line-height: 1;
  cursor: pointer;
  transition: color var(--dur-fast) var(--ease),
              border-color var(--dur-fast) var(--ease),
              background-color var(--dur-fast) var(--ease);
}
.calc-history-remove:hover {
  color: var(--bad-fg);
  border-color: var(--bad-fg);
  background: var(--bad-bg);
}

/* --------------------------------------------------------------------
   Helpful / not helpful
   ------------------------------------------------------------------ */

.calc-feedback .btn-row { margin-top: var(--s3); }

.calc-feedback-done {
  margin: var(--s3) 0 0;
  font-size: 0.9rem;
  font-weight: 600;
  color: var(--accent-strong);
}

/* A tool page is violet, so its confirmation is too. */
.tools-page .calc-feedback-done { color: var(--tool-strong); }

@media (max-width: 479px) {
  .calc-extras-head { align-items: flex-start; }
  .calc-history-text { -webkit-line-clamp: 3; }
}

/* ==========================================================================
   23. EXACT-ARITHMETIC CALCULATORS

   Fraction, ratio, rounding, discount and subnet pages. Only the pieces
   the shared form and result components do not already cover: a stacked
   fraction for input and output, a colon-separated ratio row, and a
   monospaced binary row. Role tokens only.
   ========================================================================== */

/* Fraction input: whole part beside a numerator-over-denominator stack. */
.frac-form-grid {
  display: grid;
  gap: var(--s4);
  align-items: center;
  grid-template-columns: 1fr;
}
@media (min-width: 760px) {
  .frac-form-grid { grid-template-columns: 1fr auto 1fr; }
}

.frac-operand { min-width: 0; }
.frac-operand legend { font-weight: 650; margin-bottom: var(--s2); }

.frac-inputs {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.2fr);
  gap: var(--s3);
  align-items: center;
}

.frac-stack {
  display: grid;
  gap: var(--s1);
}
.frac-stack .field,
.frac-inputs .field { margin-bottom: 0; }

.frac-bar {
  display: block;
  height: 2px;
  border-radius: 2px;
  background: var(--text-soft);
}

.frac-op { margin-bottom: 0; }
@media (min-width: 760px) {
  .frac-op { align-self: center; min-width: 9rem; }
}

/* Fraction output: the answer as a real stacked fraction. */
.frac-result {
  display: inline-flex;
  align-items: center;
  gap: 0.18em;
  direction: ltr;
}
.frac {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  vertical-align: middle;
  line-height: 1.1;
  font-size: 0.72em;
}
.frac-num {
  padding: 0 0.2em 0.08em;
  border-bottom: 0.08em solid currentColor;
}
.frac-den { padding: 0.08em 0.2em 0; }
.frac-whole-part { margin-inline-end: 0.08em; }

/* Ratio row: A : B : C with the separators vertically centred on the
   inputs rather than on the labels above them. */
.ratio-row {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--s2) var(--s3);
}
.ratio-row > .field {
  flex: 1 1 7rem;
  min-width: 0;
  margin-bottom: 0;
}
.ratio-colon {
  flex: 0 0 auto;
  padding-bottom: 0.7rem;
  font-size: 1.25rem;
  font-weight: 700;
  color: var(--text-soft);
}
@media (max-width: 479px) {
  .ratio-row-4 > .field { flex-basis: 40%; }
}

.hint-inline {
  font-weight: 400;
  font-size: 0.85em;
  color: var(--text-faint);
}

.mono {
  font-family: var(--font-mono);
  font-size: 0.9em;
  letter-spacing: 0.02em;
  word-break: break-all;
}

/* The row matching the rule the reader chose, in a comparison table. */
table.data tr.is-current th,
table.data tr.is-current td {
  background: var(--accent-soft);
  color: var(--accent-strong);
  font-weight: 650;
}

/* --------------------------------------------------------------------
   Grade average calculator — one compact row per mark

   A term can hold twenty marks, so each row is a single line on a wide
   screen (number · name · mark · weight · remove) instead of the card
   used for fewer, larger items. Below 560px the name takes its own line
   and mark and weight sit side by side, so nothing becomes a thin target.
   ------------------------------------------------------------------ */

.marks-rows {
  display: grid;
  gap: var(--s2);
}

.marks-row {
  display: grid;
  grid-template-columns: 2rem minmax(0, 1fr) 6.5rem 6.5rem 36px;
  gap: var(--s2) var(--s3);
  align-items: start;
  padding: var(--s3);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
}
.marks-row .field { margin-bottom: 0; min-width: 0; }
.marks-row .label { font-size: 0.8rem; }

.marks-row-index {
  align-self: center;
  padding-top: 1.2rem;
  font-size: 0.85rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--text-faint);
  text-align: center;
}

.marks-remove { align-self: end; margin-bottom: 2px; }

.marks-quick { margin-top: var(--s4); }
.marks-quick-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
}
.marks-quick-row .input { flex: 1 1 12rem; min-width: 0; }

@media (max-width: 559px) {
  .marks-row {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) 36px;
  }
  .marks-row-index { display: none; }
  .marks-row .marks-name { grid-column: 1 / -1; }
}

/* --------------------------------------------------------------------
   Body fat calculator

   .unit-suffix: the "(in)"/"(cm)" tag next to a measurement label,
   swapped by page script when the unit toggle changes — kept as its own
   span so the script never has to touch the label's own text.

   .measure-diagram: the inline neck/waist/hip SVG. Sized as a small,
   fixed-aspect block rather than full width, since it is a reference
   diagram next to a paragraph, not a hero image.
   ------------------------------------------------------------------ */
.unit-suffix {
  font-weight: 400;
  color: var(--text-faint);
}

.measure-diagram {
  width: 100%;
  max-width: 180px;
  margin: var(--s4) auto;
  color: var(--text-soft);
}
.measure-diagram svg {
  display: block;
  width: 100%;
  height: auto;
}
.measure-diagram text { fill: var(--text-soft); font-family: var(--font-sans); }

/* --------------------------------------------------------------------
   Body fat calculator — method cards, measurement helpers, range
   indicator, comparison and print summary. Role tokens only, so the
   light, dark and system themes need no second rule.
   ------------------------------------------------------------------ */
.bf-method-block {
  margin-bottom: var(--s5);
  padding: var(--s5);
  border: 1px solid var(--accent-line);
  border-radius: var(--r-lg);
  background: var(--surface-alt);
}
.bf-method-title { font-size: var(--step-1); margin: 0 0 var(--s2); }
.bf-method-intro { margin: 0 0 var(--s4); color: var(--text-soft); }

.method-cards {
  display: grid;
  gap: var(--s3);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 170px), 1fr));
}
@media (max-width: 520px) {
  .bf-method-block { padding: var(--s4); }
  .method-cards { grid-template-columns: 1fr 1fr; gap: var(--s2); }
  .method-card { padding: var(--s3); }
}
.method-cards input { position: absolute; opacity: 0; width: 1px; height: 1px; pointer-events: none; }
.method-card {
  display: flex;
  flex-direction: column;
  gap: var(--s1);
  min-height: 44px;
  padding: var(--s3) var(--s4);
  background: var(--surface);
  border: 1.5px solid var(--line-strong);
  border-radius: var(--r-md);
  cursor: pointer;
  transition: border-color .14s, background-color .14s;
}
.method-card:hover { border-color: var(--accent-line); }
.method-card-title { font-weight: 700; color: var(--text); }
.method-card-desc { font-size: 0.86rem; color: var(--text-faint); line-height: 1.4; }
.method-cards input:checked + .method-card {
  border-color: var(--accent);
  background: var(--accent-soft);
  box-shadow: inset 0 0 0 1px var(--accent);
}
.method-cards input:checked + .method-card .method-card-title { color: var(--accent-strong); }
.method-cards input:checked + .method-card .method-card-title::before { content: "\2713  "; }
.method-cards input:focus-visible + .method-card { outline: 3px solid var(--focus); outline-offset: 2px; }

.bf-presets {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s2);
  margin-top: var(--s4);
}
.bf-presets-label { font-size: 0.88rem; font-weight: 600; color: var(--text-soft); }
.bf-presets-note { flex-basis: 100%; margin-top: 0; }

.field-tools { display: flex; flex-wrap: wrap; gap: var(--s1) var(--s3); margin-top: var(--s2); }
.mini-disclosure { font-size: 0.86rem; min-width: 0; }
.mini-disclosure > summary {
  cursor: pointer;
  color: var(--accent);
  font-weight: 600;
  padding: 2px 0;
  list-style-position: inside;
}
.mini-disclosure > summary:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; border-radius: var(--r-sm); }
.mini-disclosure[open] { flex-basis: 100%; }
.mini-disclosure-body {
  margin: var(--s2) 0 0;
  padding: var(--s3);
  background: var(--surface-sunk);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  color: var(--text-soft);
  line-height: 1.5;
}
.avg-grid {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: center;
  gap: var(--s2) var(--s3);
}
.input.input-compact { min-height: 40px; padding: var(--s2); }
.avg-out { margin: var(--s2) 0; font-weight: 600; color: var(--text); }

.bf-compare { display: grid; gap: var(--s2); margin: 0 0 var(--s3); }
.bf-compare > div {
  display: flex;
  justify-content: space-between;
  gap: var(--s3);
  padding: var(--s2) var(--s3);
  background: var(--surface-alt);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
}
.bf-compare dt { font-weight: 600; }
.bf-compare dd { margin: 0; font-weight: 700; text-align: right; }
.bf-compare-why { font-size: 0.95rem; margin: var(--s3) 0 var(--s1); }

.bf-range { position: relative; padding-top: 30px; }
.bf-range-track {
  display: flex;
  height: 16px;
  border-radius: var(--r-full);
  overflow: hidden;
  border: 1px solid var(--line);
}
.bf-range-seg { height: 100%; border-right: 2px solid var(--surface); }
.bf-range-seg:last-child { border-right: 0; }
.bf-range-seg-1 { background: color-mix(in srgb, var(--accent) 18%, var(--surface-sunk)); }
.bf-range-seg-2 { background: color-mix(in srgb, var(--accent) 34%, var(--surface-sunk)); }
.bf-range-seg-3 { background: color-mix(in srgb, var(--accent) 50%, var(--surface-sunk)); }
.bf-range-seg-4 { background: color-mix(in srgb, var(--accent) 68%, var(--surface-sunk)); }
.bf-range-seg-5 { background: color-mix(in srgb, var(--accent) 86%, var(--surface-sunk)); }
.bf-range-marker {
  position: absolute;
  top: 0;
  bottom: -4px;
  width: 0;
  border-left: 2px solid var(--text);
  transition: left .5s cubic-bezier(.22,.9,.3,1);
}
.bf-range-marker span {
  position: absolute;
  top: 0;
  left: 0;
  transform: translateX(-50%);
  padding: 1px 6px;
  font-size: 0.78rem;
  font-weight: 700;
  white-space: nowrap;
  color: var(--surface);
  background: var(--text);
  border-radius: var(--r-sm);
}
.bf-range-marker.is-start span { transform: none; }
.bf-range-marker.is-end span { transform: translateX(-100%); }
@media (prefers-reduced-motion: reduce) { .bf-range-marker { transition: none; } }
.bf-range-scale {
  display: flex;
  justify-content: space-between;
  margin-top: var(--s1);
  font-size: 0.75rem;
  color: var(--text-faint);
}
.bf-range-text { margin: var(--s3) 0 var(--s2); font-weight: 600; }
.bf-range-legend { list-style: none; padding: 0; margin: 0; display: grid; gap: 2px; font-size: 0.88rem; }
.bf-range-legend li {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
  padding: var(--s1) var(--s2);
  border-radius: var(--r-sm);
  color: var(--text-soft);
}
.bf-range-legend li span:first-child { min-width: 7.5rem; }
.bf-range-legend li.is-current { background: var(--accent-soft); color: var(--accent-strong); font-weight: 650; }

.bf-ffmi-line { margin: 0 0 var(--s2); font-size: 1.1rem; }

.step-list > li.step-group {
  counter-increment: none;
  padding-left: 0;
  margin-top: var(--s4);
  font-weight: 700;
  color: var(--text);
}
.step-list > li.step-group::before { content: none; }
.step-list > li { overflow-wrap: anywhere; }

.bf-print-summary { display: none; }
.bf-summary { display: grid; gap: 2px; margin: 0; }
.bf-summary > div { display: flex; gap: var(--s3); }
.bf-summary dt { min-width: 9rem; font-weight: 600; }
.bf-summary dd { margin: 0; }

.bf-terms dt { font-weight: 700; margin-top: var(--s3); }
.bf-terms dd { margin: var(--s1) 0 0; }

.measure-diagram-wide { max-width: 300px; }

@media print {
  .bf-page #bf-form,
  .bf-page .bf-article,
  .bf-page .result-body > section:not(.bf-print-summary):not(#bf-compare),
  .bf-page #bf-advanced,
  .bf-page .related-tools-line,
  .bf-page > section.section-tight,
  body:has(.bf-page) .calc-extras { display: none !important; }
  .bf-print-summary { display: block; }
  .bf-range-marker span { color: #fff; background: #000; }
}

/* ==========================================================================
   31. BLOX FRUITS TRADE CALCULATOR

   The two sides of a trade, the rows inside them, and the two verdicts.
   Every colour is a role token, so both themes and the tools scope keep
   working. No animation is declared here, so there is nothing for the
   reduced-motion block to cancel.
   ========================================================================== */

.bf-sides {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--s4);
  margin-bottom: var(--s4);
}
@media (min-width: 860px) {
  .bf-sides { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

.bf-side {
  margin: 0;
  padding: var(--s4);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
}
.bf-side > legend {
  padding-inline: var(--s2);
  font-size: 1rem;
  font-weight: 700;
  color: var(--text);
}
.bf-side > .hint { margin-top: 0; }

/* One item: the fruit, its form, how many, and the visitor's own value.
   Six controls is too many for one line on a phone, so the row is laid
   out by area and folds in two steps rather than wrapping arbitrarily. */
.bf-row {
  display: grid;
  gap: var(--s2) var(--s3);
  align-items: start;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas:
    "fruit  fruit"
    "form   qty"
    "value  demand"
    "remove remove";
  padding-block: var(--s3);
  border-top: 1px solid var(--line);
}
.bf-row:first-child { border-top: 0; padding-top: 0; }

.bf-row-fruit  { grid-area: fruit; }
.bf-row-form   { grid-area: form; }
.bf-row-qty    { grid-area: qty; }
.bf-row-value  { grid-area: value; }
.bf-row-demand { grid-area: demand; }
.bf-row-remove { grid-area: remove; justify-self: start; }

/* The row folds on the width of its own card, not the viewport: from
   860px the two sides sit next to each other and each card is narrower
   than a phone screen, so a viewport query pushed Qty and the remove
   button out of the card between roughly 860 and 920px. Without
   container query support the row simply keeps the stacked layout. */
.bf-side [data-rows] { container-type: inline-size; }

@container (min-width: 30rem) {
  .bf-row {
    /* Four columns: the fruit spans the two flexible ones, so the row
       reads as fruit / form / qty on top and value / demand under it.
       The form column is sized by its longest option, which is what
       keeps "Перманентный" whole where a fixed 8.5rem clipped it. */
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) auto 4.5rem;
    grid-template-areas:
      "fruit fruit  form   qty"
      "value demand remove remove";
  }
  .bf-row-remove { justify-self: end; align-self: center; }
}

/* The chevron is 18px wide and sits 12px in; the site-wide 44px of
   right padding leaves more dead space than a narrow row can spare. */
.bf-row .select { padding-right: 2.25rem; }

.bf-row .label {
  font-size: 0.78rem;
  margin-bottom: 4px;
}
.bf-row .hint { margin-top: 4px; font-size: 0.8rem; }

/* Short numeric fields read better centred: a column of them can be
   scanned for a wrong figure without reading each one. */
.input-qty { text-align: center; padding-inline: var(--s2); }

.bf-side-totals {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--s2);
  margin: var(--s4) 0 0;
  padding-top: var(--s3);
  border-top: 1px solid var(--line);
}
.bf-side-totals dt {
  font-size: 0.72rem;
  font-weight: 650;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--text-faint);
}
.bf-side-totals dd {
  margin: 2px 0 0;
  font-size: 1.02rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--text);
}

.bf-settings { margin-bottom: var(--s4); }
.bf-settings .btn { margin-top: var(--s2); }

/* The verdict word itself, and the pills beside the two section
   headings. Tone is carried by the semantic role pairs, which are tuned
   for both themes, and never by colour alone: each pill says what it
   means in words. */
.bf-verdict-win  { color: var(--ok-fg); }
.bf-verdict-loss { color: var(--bad-fg); }
.bf-verdict-fair { color: var(--text); }

.bf-badge {
  display: inline-flex;
  align-items: center;
  margin-inline-start: var(--s2);
  padding: 3px var(--s3);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-full);
  background: var(--surface-sunk);
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-soft);
  vertical-align: middle;
}
.bf-badge-win  { background: var(--ok-bg);   border-color: var(--ok-line);   color: var(--ok-fg); }
.bf-badge-loss { background: var(--bad-bg);  border-color: var(--bad-line);  color: var(--bad-fg); }
.bf-badge-fair { background: var(--info-bg); border-color: var(--info-line); color: var(--info-fg); }

.stat-sub {
  display: block;
  margin-top: 2px;
  font-size: 0.76rem;
  font-variant-numeric: tabular-nums;
  color: var(--text-faint);
}

/* The price table's filter sits beside its live count. */
.bf-table-controls {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--s3);
  margin-bottom: var(--s4);
}
.bf-table-controls .field { flex: 1 1 18rem; margin: 0; }
.bf-table-controls .hint { margin: 0 0 var(--s2); }

/* ==========================================================================
   32. FIND THE ZEROS CALCULATOR

   Maths output is long and unpredictable — a surd, a complex pair, a
   degree-10 polynomial — so every block here wraps or scrolls inside its
   own box rather than widening the page. Colours are role tokens only, so
   both themes work unchanged, and a confirmed zero is marked with a tick
   and a word as well as a tint, never by colour alone.
   ========================================================================== */

.zc-input { font-family: var(--font-mono); font-size: 1rem; }
.zc-formats { margin: var(--s3) 0 0; }
.zc-formats code { overflow-wrap: anywhere; }
.zc-try { font-size: 0.82rem; }
.zc-chip-label { font-weight: 700; color: var(--text); margin-inline-end: 2px; }

/* The zeros themselves can be long ("(−1 + i√3)/2, …"), so the headline
   figure is smaller than a single-number result and allowed to wrap. */
.zc-primary {
  font-size: clamp(1.35rem, 1.1rem + 1.6vw, 2rem);
  letter-spacing: -0.01em;
  overflow-wrap: anywhere;
}

.zc-groups {
  display: grid;
  gap: var(--s2);
  margin: 0 0 var(--s3);
}
@media (min-width: 640px) {
  .zc-groups { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
.zc-group {
  padding: var(--s3);
  background: var(--surface-sunk);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
}
.zc-group dt {
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--text-faint);
  margin-bottom: var(--s1);
}
.zc-group dd {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s1) var(--s2);
  margin: 0;
  font-weight: 650;
}
.zc-val {
  padding: 2px var(--s2);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  font-variant-numeric: tabular-nums;
  overflow-wrap: anywhere;
}

.zc-view label { min-width: 84px; }

.zc-table th[scope="row"] { font-weight: 700; white-space: nowrap; }
.zc-table td.num { white-space: nowrap; }
.zc-check { font-size: 0.85rem; color: var(--text-soft); min-width: 12rem; }

.zc-factor {
  margin: 0 0 var(--s3);
  padding: var(--s3) var(--s4);
  background: var(--surface-sunk);
  border-left: 3px solid var(--accent);
  border-radius: var(--r-sm);
  font-size: 1.05rem;
  font-weight: 650;
  overflow-wrap: anywhere;
}
.zc-factor-list { margin: 0 0 var(--s2); padding-left: var(--s5); }
.zc-factor-list li { margin-bottom: var(--s1); overflow-wrap: anywhere; }

.zc-facts {
  display: grid;
  gap: var(--s1) var(--s4);
  margin: 0 0 var(--s3);
}
@media (min-width: 640px) {
  .zc-facts { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
.zc-facts > div { display: flex; flex-wrap: wrap; gap: var(--s2); }
.zc-facts dt { color: var(--text-soft); }
.zc-facts dd { margin: 0; font-weight: 650; overflow-wrap: anywhere; }

.zc-sub {
  margin: var(--s4) 0 var(--s2);
  font-size: 0.95rem;
  font-weight: 700;
  color: var(--text);
}
.zc-cands { margin-top: 0; margin-bottom: var(--s2); }
.zc-cands .chip { min-height: 30px; padding: 3px var(--s2); font-variant-numeric: tabular-nums; }
.zc-hit {
  border-color: var(--ok-line);
  background: var(--ok-bg);
  color: var(--ok-fg);
  font-weight: 700;
}
.zc-hit::after { content: " \2713"; }
.zc-test td, .zc-test th { white-space: nowrap; }
.zc-hit-row th, .zc-hit-row td { background: var(--ok-bg); color: var(--ok-fg); font-weight: 650; }

/* Synthetic division: the textbook tableau — c on the left, a rule under
   the products, and the remainder boxed. */
.zc-synth { margin-bottom: var(--s3); }
.zc-synth-table {
  border-collapse: collapse;
  font-family: var(--font-mono);
  font-size: 0.9rem;
  font-variant-numeric: tabular-nums;
}
.zc-synth-table th,
.zc-synth-table td {
  padding: 4px 12px;
  text-align: right;
  white-space: nowrap;
}
.zc-synth-table th {
  border-right: 2px solid var(--text-soft);
  color: var(--accent);
  font-weight: 700;
}
.zc-synth-table .zc-sum th,
.zc-synth-table .zc-sum td { border-top: 2px solid var(--text-soft); font-weight: 700; }
.zc-synth-table .zc-rem { outline: 2px solid var(--accent); outline-offset: -3px; border-radius: 3px; }
.zc-synth-static { margin: var(--s3) 0; }

.zc-steps > li > strong { display: block; margin-bottom: 2px; }
.zc-step-line {
  display: block;
  color: var(--text-soft);
  overflow-wrap: anywhere;
}

@media print {
  .zc-view { display: none; }
  .zc-group, .zc-factor { background: none; }
}

/* ==========================================================================
   33. Grow a Garden calculator
   34. Grow Garden Calculator (real gardens)

   The only piece of UI here that the site did not already have is the
   mutation picker: 195 options is too many for a plain checkbox list and
   too many for a <select multiple>, so it is a search box over a capped,
   scrollable field of chips with the chosen ones pinned above it.
   ========================================================================== */

.gag-picker { margin-top: var(--s5); }

/* Chosen mutations, above the search rather than lost inside it. Hidden
   entirely when nothing is chosen so the gap does not sit there empty. */
.gag-picked { margin-top: var(--s3); }
.gag-picked[hidden] { display: none; }

.gag-chip-picked {
  border-color: var(--accent);
  background: var(--accent-soft);
  color: var(--accent);
  font-weight: 650;
  cursor: pointer;
}
.gag-chip-picked:hover { border-color: var(--accent-strong); }
.gag-chip-x {
  margin-inline-start: var(--s2);
  font-size: 1.05em;
  line-height: 1;
  opacity: 0.7;
}

/* The option field. Capped in height and scrollable, because a search
   that matches eighty mutations must not push the rest of the form off
   the screen. The border makes the scroll boundary visible rather than
   leaving the list looking truncated by accident. */
.gag-options {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
  max-height: 210px;
  overflow-y: auto;
  margin-top: var(--s3);
  padding: var(--s3);
  background: var(--surface-sunk);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  /* Keeps a focused chip from being scrolled half out of view when it is
     reached by keyboard rather than by pointer. */
  scroll-padding: var(--s3);
}
.gag-options:empty { display: none; }

.gag-notes {
  margin: 0;
  padding-inline-start: 1.15rem;
  display: grid;
  gap: var(--s2);
  color: var(--text-soft);
}
.gag-notes li { line-height: 1.55; }

/* The formula block. `mono` already sets the face; this only stops a long
   formula forcing a horizontal scroll on a phone, which is the one thing
   that must not happen on a 375px screen. */
.audit-formula {
  overflow-wrap: anywhere;
  line-height: 1.8;
}

@media (max-width: 560px) {
  .gag-options { max-height: 168px; }
}

@media print {
  /* A printed result is the answer and its working, not the form it was
     assembled in — and the result restates every input it used, so nothing
     is lost by dropping the controls. The mutation and crop reference
     tables sit outside the form and still print. */
  #gag-form { display: none !important; }
}

/* Compare mode: two sides, and a row compact enough that six of them fit
   on a phone. The row is a grid rather than a flex wrap so the columns
   line up down the side instead of staggering. */
.gag-sides {
  display: grid;
  gap: var(--s4);
  grid-template-columns: 1fr;
}
@media (min-width: 900px) {
  .gag-sides { grid-template-columns: 1fr 1fr; }
}

.gag-side {
  padding: var(--s4);
  background: var(--surface-alt);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
}
.gag-side > legend {
  padding: 0 var(--s2);
  font-weight: 700;
  color: var(--accent);
}
.gag-side > .hint { margin-top: 0; }

.gag-row {
  display: grid;
  gap: var(--s2) var(--s3);
  grid-template-columns: repeat(2, minmax(0, 1fr));
  align-items: end;
  padding: var(--s3) 0;
  border-top: 1px solid var(--line);
}
.gag-row:first-child { border-top: 0; padding-top: 0; }
.gag-row-crop { grid-column: 1 / -1; }
.gag-row-remove { grid-column: 1 / -1; justify-self: start; }

@media (min-width: 560px) {
  .gag-row { grid-template-columns: repeat(3, minmax(0, 1fr)) auto; }
  .gag-row-crop { grid-column: 1 / -1; }
  .gag-row-remove { grid-column: auto; justify-self: end; align-self: end; }
}

.gag-side-totals {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s4);
  margin: var(--s4) 0 0;
  padding-top: var(--s3);
  border-top: 1px solid var(--line);
}
.gag-side-totals dt {
  font-size: 0.78rem;
  font-weight: 600;
  color: var(--text-faint);
}
.gag-side-totals dd {
  margin: 0;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--text);
}

/* ==========================================================================
   34. GROW GARDEN CALCULATOR (REAL GARDENS)

   The real-garden planner at /grow-garden-calculator/. Not the Roblox page
   above (section 33) — it borrows only .gag-notes, a plain list style.

   .gg-layout: the bed drawn from above, one dot per plant. The SVG scales
   to its box; the bed outline uses a non-scaling stroke so a 60 ft field
   and a 2 ft pot draw with the same line weight. Colours are tokens, so
   it follows the theme and prints in black on white.
   ========================================================================== */

.gg-disambig { margin: calc(var(--s2) * -1) 0 var(--s4); }

.gg-toolbar {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s3);
  margin-bottom: var(--s4);
}
.gg-units { max-width: 22rem; }
.gg-tabs label { min-width: 7.5rem; }

.gg-span { grid-column: 1 / -1; margin-top: 0; }

.gg-area-line {
  margin: 0 0 var(--s3);
  font-weight: 600;
  color: var(--text-soft);
  font-variant-numeric: tabular-nums;
}
.gg-area-line:empty { display: none; }

.gg-layout {
  margin: var(--s4) 0 0;
  padding: var(--s3);
  background: var(--surface-alt);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
}
.gg-layout-svg svg {
  display: block;
  width: 100%;
  height: auto;
  max-height: 22rem;
}
.gg-layout .gg-bed {
  fill: var(--surface);
  stroke: var(--accent);
  stroke-width: 2;
}
.gg-layout .gg-plant { fill: var(--accent); }
.gg-layout figcaption { margin: var(--s2) 0 0; }

.gg-sources {
  margin: var(--s4) 0 var(--s3);
  padding-left: 1.25rem;
  display: grid;
  gap: var(--s2);
}
.gg-sources li { overflow-wrap: anywhere; }

@media print {
  .gg-toolbar, .gg-tabs, #gg-form .btn-row { display: none !important; }
  .gg-layout { background: none; border-color: #000; }
  .gg-layout .gg-bed { fill: #fff; stroke: #000; }
  .gg-layout .gg-plant { fill: #000; }
}

/* ==========================================================================
   36. UK UNIVERSITY GRADE CALCULATOR

   One component the other calculators do not have: a 0-100 strip showing
   the classification bands with the student's average marked on it. Built
   from role tokens only, so both themes work unchanged.

   Colour is never the only signal. The strip is aria-hidden; the same
   information is written out in .ukg-scale-text and listed in
   .ukg-scale-legend, and the current band carries an inset ring and
   aria-current as well as a tint. Widths are set from the bands the
   student is actually using, which may be custom, so nothing here assumes
   70 / 60 / 50 / 40.
   ========================================================================== */

/* padding-top makes room for the marker's label; .ukg-scale-range below
   depends on that 30px and on the 20px track height, so the three numbers
   are kept together. */
.ukg-scale { position: relative; padding-top: 30px; }

.ukg-scale-track {
  display: flex;
  height: 20px;
  border-radius: var(--r-full);
  overflow: hidden;
  border: 1px solid var(--line);
}
.ukg-seg { height: 100%; border-right: 2px solid var(--surface); }
.ukg-seg:last-child { border-right: 0; }

/* A higher class is a stronger accent, so the strip reads left to right
   the way the bands do. */
.ukg-seg-1 { background: color-mix(in srgb, var(--accent) 10%, var(--surface-sunk)); }
.ukg-seg-2 { background: color-mix(in srgb, var(--accent) 26%, var(--surface-sunk)); }
.ukg-seg-3 { background: color-mix(in srgb, var(--accent) 42%, var(--surface-sunk)); }
.ukg-seg-4 { background: color-mix(in srgb, var(--accent) 58%, var(--surface-sunk)); }
.ukg-seg-5 { background: color-mix(in srgb, var(--accent) 74%, var(--surface-sunk)); }
.ukg-seg-6 { background: color-mix(in srgb, var(--accent) 88%, var(--surface-sunk)); }
.ukg-seg.is-current { box-shadow: inset 0 0 0 2px var(--accent-strong); }

/* Target mode only: the span of final averages still available. Dashed
   rather than merely tinted, so it survives a greyscale print. */
.ukg-scale-range {
  position: absolute;
  top: 30px;
  height: 20px;
  border: 2px dashed var(--accent-strong);
  border-radius: var(--r-sm);
  background: color-mix(in srgb, var(--accent) 16%, transparent);
  pointer-events: none;
}

.ukg-scale-marker {
  position: absolute;
  top: 0;
  bottom: -4px;
  width: 0;
  border-left: 2px solid var(--text);
  transition: left .5s cubic-bezier(.22,.9,.3,1);
}
.ukg-scale-marker span {
  position: absolute;
  top: 0;
  left: 0;
  transform: translateX(-50%);
  padding: 1px 6px;
  font-size: 0.78rem;
  font-weight: 700;
  white-space: nowrap;
  color: var(--surface);
  background: var(--text);
  border-radius: var(--r-sm);
}
/* At the ends the label would hang off the strip and force a horizontal
   scrollbar on a 320px screen. */
.ukg-scale-marker.is-start span { transform: none; }
.ukg-scale-marker.is-end span { transform: translateX(-100%); }
@media (prefers-reduced-motion: reduce) { .ukg-scale-marker { transition: none; } }

/* Boundary numbers, positioned at their own percentage rather than spread
   evenly — an evenly spread row would put 40, 50, 60 and 70 in the wrong
   places and quietly misdescribe the bands. */
.ukg-scale-ticks {
  position: relative;
  height: 1.15rem;
  margin-top: var(--s1);
  font-size: 0.72rem;
  color: var(--text-faint);
  font-variant-numeric: tabular-nums;
}
.ukg-tick { position: absolute; top: 0; transform: translateX(-50%); white-space: nowrap; }
.ukg-tick.is-start { transform: none; }
.ukg-tick.is-end { transform: translateX(-100%); }

.ukg-scale-text { margin: var(--s3) 0 var(--s2); font-weight: 600; }

.ukg-scale-legend {
  list-style: none;
  padding: 0;
  margin: 0;
  display: grid;
  gap: 2px;
  font-size: 0.88rem;
}
.ukg-scale-legend li {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: var(--s2);
  padding: var(--s1) var(--s2);
  border-radius: var(--r-sm);
  color: var(--text-soft);
}
.ukg-scale-legend li.is-current {
  background: var(--accent-soft);
  color: var(--accent-strong);
  font-weight: 650;
}

.ukg-sources {
  margin: var(--s4) 0 var(--s3);
  padding-left: 1.25rem;
  display: grid;
  gap: var(--s2);
}
.ukg-sources li { overflow-wrap: anywhere; }

@media print {
  #uk-form .btn-row, #uk-rules { display: none !important; }
  .ukg-scale-marker span { color: #fff; background: #000; }
  .ukg-scale-range { border-color: #000; background: none; }
}

/* ==========================================================================
   38. CBM CALCULATOR

   The carton rows, the working box and the quick converter. Every colour
   is a role token, so both themes and the post scope in sections 35 and
   37 keep working with no extra rules here. Nothing animates, so there is
   nothing for the reduced-motion block to cancel.
   ========================================================================== */

.cbm-fieldset { border: 0; padding: 0; margin: 0 0 var(--s4); }
.cbm-fieldset > legend {
  padding: 0;
  margin-bottom: var(--s1);
  font-size: 0.95rem;
  font-weight: 650;
  color: var(--text);
}

.cbm-rows { display: grid; gap: var(--s2); }

/* index | item | L | W | H | unit | qty | kg | remove */
.cbm-row {
  display: grid;
  grid-template-columns:
    1.6rem minmax(0, 1.3fr) repeat(3, minmax(0, 4.6rem))
    minmax(0, 6.5rem) minmax(0, 4rem) minmax(0, 5rem) 72px;
  gap: var(--s2);
  align-items: start;
  padding: var(--s3);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
}
.cbm-row .field  { margin-bottom: 0; min-width: 0; }
.cbm-row .label  { font-size: 0.78rem; }
.cbm-row .input,
.cbm-row .select { min-width: 0; width: 100%; }

/* The unit select sits in a column about five characters wide, and the
   default chevron gutter (--s7) leaves no room for "cm" inside it. Same
   control, tighter arrow. rtl.css mirrors this pair. */
.cbm-row .select {
  padding-left: var(--s2);
  padding-right: var(--s5);
  background-position: right var(--s2) center;
  background-size: 15px;
}

.cbm-row-index {
  align-self: center;
  padding-top: 1.2rem;
  font-size: 0.85rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--text-faint);
  text-align: center;
}

.cbm-row-actions {
  display: flex;
  gap: var(--s1);
  align-self: end;
  margin-bottom: 2px;
}
.cbm-row-actions .row-remove { flex: 0 0 auto; }

/* Duplicate borrows .row-remove's shape but not its red hover — it adds
   a row rather than destroying one. */
.cbm-copy:hover:not(:disabled) {
  color: var(--accent);
  border-color: var(--accent-line);
  background: var(--accent-soft);
}

.cbm-add { margin-top: var(--s3); }
.cbm-add .btn { width: auto; }

.cbm-try { align-self: center; font-size: 0.85rem; }
.cbm-chip-label { font-weight: 700; color: var(--text); margin-inline-end: 0.35em; }

.cbm-options { margin: var(--s4) 0; }
.cbm-caps { border: 0; padding: 0; margin: var(--s4) 0 0; }
.cbm-caps > legend { padding: 0; margin-bottom: var(--s2); }
.cbm-caps-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 9rem), 1fr));
  gap: var(--s3);
  margin-bottom: var(--s2);
}
.cbm-caps-row .field { margin-bottom: 0; }

/* The working: a formula line, then the substitution for each carton
   type. Each step's lines stack so a long one wraps instead of pushing
   the list sideways on a phone. */
.cbm-formula {
  padding: var(--s3);
  background: var(--accent-soft);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  font-weight: 600;
  text-align: center;
}

.cbm-work .step-list { margin-top: var(--s3); }
.cbm-step-line {
  display: block;
  font-variant-numeric: tabular-nums;
  color: var(--text-soft);
}

.cbm-cost {
  font-size: 1.05rem;
  font-weight: 650;
  font-variant-numeric: tabular-nums;
}

.cbm-convert {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 11rem), 1fr));
  gap: var(--s3);
  align-items: start;
}
.cbm-convert .field { margin-bottom: 0; }

.cbm-convert-out {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--s1) var(--s3);
  align-content: start;
  margin: 0;
  padding: var(--s3);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
}
.cbm-convert-out:empty { display: none; }
.cbm-convert-out dt { font-weight: 600; color: var(--text-soft); }
.cbm-convert-out dd { margin: 0; font-variant-numeric: tabular-nums; }

/* Tablet: the item name takes its own row so the six controls below it
   keep a usable width instead of being squeezed to three characters.
   The row number goes with it — the rows are separated visually and the
   remove button carries its own numbered label for screen readers, so
   the digit is decoration once the layout is stacked. */
@media (max-width: 899px) {
  .cbm-row {
    grid-template-columns:
      repeat(3, minmax(0, 1fr)) minmax(0, 6.5rem) minmax(0, 4.5rem)
      minmax(0, 5rem) 72px;
  }
  .cbm-row-index { display: none; }
  .cbm-row .cbm-name { grid-column: 1 / -1; }
}

/* Phone: four rows — name, the three dimensions, the unit, then quantity
   and weight beside the remove button.
 *
 * The unit takes a line of its own rather than a third of one. Squeezed
 * into a narrow column it fits "cm" and nothing else, and the option
 * text is a translated word — "дюйм", "インチ", "بوصة" — on four of the
 * five languages this page is published in. An ellipsised unit on a
 * volume calculator is not a cosmetic problem, so the row gets taller
 * instead.
 *
 * The row number goes: the rows are already separated visually, and the
 * remove button carries its own numbered label for screen readers. */
@media (max-width: 599px) {
  .cbm-row {
    grid-template-columns: repeat(3, minmax(0, 1fr)) 72px;
    gap: var(--s2);
  }
  .cbm-row-index       { display: none; }
  .cbm-row .cbm-name   { grid-column: 1 / -1; }
  .cbm-row .cbm-unit   { grid-column: 1 / 4; grid-row: 3; }
  .cbm-row .cbm-qty    { grid-column: 1 / 2; grid-row: 4; }
  .cbm-row .cbm-weight { grid-column: 2 / 4; grid-row: 4; }
  .cbm-row .cbm-row-actions { grid-column: 4; grid-row: 4; }
  .cbm-formula { text-align: start; }
}


/* Print: the measurements stay — they are the record of what was
   measured — and everything that only exists to be clicked goes. The
   options panel is a <details>, which the global print block expands, so
   it is hidden here rather than left to unfold into a printed page of
   empty fields. */
@media print {
  #cbm-form .chip-row,
  .cbm-add,
  .cbm-options,
  .cbm-convert,
  #conv-heading { display: none !important; }

  .cbm-row { border-color: #999; break-inside: avoid; }
  .cbm-formula { background: none; border-color: #999; }
}
/* ==========================================================================
   35. POST PAGES — the shared design system for a single calculator or tool

   One design for all thirty-eight calculator pages and all twenty tool
   pages, and not one line of it per page.

   WHAT IT IS SCOPED TO, AND WHY THAT MATTERS
   -----------------------------------------------------------------------
   Everything here is under `body.is-post`, which header.php sets from
   the ROUTE — 'calc' or 'tool' — and from nothing else. The homepage,
   the two directories, the category hubs, search and the legal pages
   never carry it, so no rule in this section can reach a page whose
   design is already signed off. That is the whole containment story: one
   class, set in one place, on the two route types that are a post.

   WHY IT IS ALL IN HERE RATHER THAN IN THE TEMPLATES
   -----------------------------------------------------------------------
   Every post template already has the same skeleton: a .calc-head, one
   working surface (a <form> on a calculator, a .tool-workspace on a
   tool), an <article> with the guide, then a related rail. That
   regularity is the only hook this section needs, so a page ships with
   the design the day it ships — no template remembers to opt in, and no
   template can drift out.

   Which also means the rule this section keeps: it changes PRESENTATION
   ONLY. Nothing here renames a control, reorders a field, touches a
   formula or edits a word of an article. A calculator's markup, its
   script and its copy are exactly what they were; this gives them a page
   worth sitting on.

   THE HUE TAKES CARE OF ITSELF
   -----------------------------------------------------------------------
   `.tools-page main` (section 21) already remaps --accent, --grad-calc
   and --glow-calc to the violet roles. Every rule below that sits inside
   <main> reads those roles, so a tool post is violet and a calculator
   post is teal with no second set of rules. The two properties that are
   read OUTSIDE main — the page wash and the head tile — carry their own
   pair, set once at the top of 35.1.

   Page-specific sections (31-34) load after this one and still win, so
   the Blox Fruits grid, the zeros graph and the two garden planners keep
   their bespoke layouts.
   ========================================================================== */

/* --------------------------------------------------------------------
   35.1  Scope

   The rail is the width of the "On this page" column on a wide screen;
   the split is the width at which the article earns a second column at
   all. Below it there is not enough room for a 300px rail AND a
   readable measure, so the rail goes back above the article rather than
   squeezing the text.
   ------------------------------------------------------------------ */

body.is-post {
  --post-rail:  300px;
  --post-split: 1120px;
  --post-gap:   var(--s7);

  /* The two hue values used outside <main>, where the tools scope does
     not reach. Everything else reads --accent and friends. */
  --post-tile: var(--tile-calc);
  --post-wash: var(--glow-calc-soft);
}
body.is-post.tools-page {
  --post-tile: var(--tile-tool);
  --post-wash: var(--glow-tool-soft);
}

/* A wash, not a background: two very low-opacity radials fixed behind
   the top of the page, so the head and the working surface sit in a
   faint accent light and the article below scrolls out of it into plain
   --bg.

   position: fixed rather than absolute for two reasons — an absolutely
   positioned child WOULD be a flex item of <body>, and fixed also means
   the gradient never stretches to the full height of a long article,
   which is where a wash turns into a tint. z-index -1 puts it above the
   canvas (which is where body's background-color actually paints) and
   below every in-flow element. */
body.is-post::before {
  content: "";
  position: fixed;
  inset-block-start: 0;
  inset-inline: 0;
  block-size: min(76vh, 760px);
  z-index: -1;
  pointer-events: none;
  background:
    radial-gradient(44% 62% at 12% 0%, var(--post-wash) 0%, transparent 68%),
    radial-gradient(40% 54% at 88% 4%, var(--post-wash) 0%, transparent 70%);
}

/* --------------------------------------------------------------------
   35.2  Post head

   The same three parts .calc-head has always had — icon, h1, one
   sentence — given the room to read as a title block rather than a line
   of text with a square next to it. Calculators and tools share the
   partial and now share the treatment; only the tile hue differs.
   ------------------------------------------------------------------ */

body.is-post .calc-head {
  gap: var(--s5);
  padding-block: var(--s6);
}

body.is-post .calc-head-icon {
  inline-size: 58px;
  block-size: 58px;
  margin-block-start: 3px;
  border-radius: 18px;
  background-image: var(--post-tile);
  color: var(--on-tile);
  box-shadow: var(--glow-calc);
}
body.is-post .calc-head-icon svg { inline-size: 28px; block-size: 28px; }

body.is-post .calc-head h1 {
  margin-block-end: var(--s3);
  letter-spacing: -0.035em;
  text-wrap: balance;
}

body.is-post .calc-head .lede {
  max-inline-size: var(--measure);
  font-size: 1.06rem;
  line-height: 1.68;
  color: var(--text-soft);
}

@media (max-width: 479px) {
  body.is-post .calc-head { gap: var(--s4); padding-block: var(--s5); }
  body.is-post .calc-head-icon {
    inline-size: 48px; block-size: 48px;
    margin-block-start: 0;
    border-radius: 15px;
  }
  body.is-post .calc-head-icon svg { inline-size: 24px; block-size: 24px; }
  body.is-post .calc-head .lede { font-size: 1rem; }
}

/* Tool facts, as pills.

   tool-meta.php already renders these — what a tool accepts, what it
   returns, and whether it is free — generated from the registry's own
   'processing' and 'accepts' fields, so they can never say something
   the tool does not do. Nothing here adds a claim; it only gives the
   three existing facts the compact, scannable shape a phone rewards,
   in place of the plain text line the base rule (section 21) draws for
   every other context .tool-facts appears in. */
body.is-post .tool-facts { gap: var(--s2); }

body.is-post .tool-fact {
  padding: 6px var(--s3);
  border: 1px solid var(--line);
  border-radius: var(--r-full);
  background: var(--surface-alt);
  font-size: 0.82rem;
  font-weight: 550;
  color: var(--text-soft);
}
body.is-post .tool-fact code {
  background: transparent;
  border: 0;
  padding: 0;
  font-size: inherit;
  font-weight: 650;
  color: var(--text);
}

/* --------------------------------------------------------------------
   35.3  The working surface

   The one element on the page that is allowed to look expensive.

   On a CALCULATOR it is selected structurally — the section that
   directly contains the form — rather than by a class, because a class
   would mean editing thirty-eight templates to say something their
   markup already says. :has() carries that, and the @supports wrapper
   means a browser without it renders precisely the page that shipped
   yesterday: the form and the result panel, unstyled by this block and
   perfectly usable. Nothing inside is positioned, resized or reordered
   by these rules; the shell is drawn AROUND the calculator.

   On a TOOL the surface is already a card — .tool-workspace, section 21
   — so it is matched by name and only lifted to the same weight.

   The top edge is a background layer rather than a ::before so that
   border-radius clips it for free, with no overflow property that could
   cut off a focus ring or a sticky header inside a result table.
   ------------------------------------------------------------------ */

body.is-post .tool-workspace {
  max-inline-size: 980px;
  margin-inline: auto;
  border-radius: var(--r-xl);
  box-shadow: var(--shadow-md);
  background:
    var(--grad-calc) center top / 100% 3px no-repeat,
    var(--surface);
}

@supports selector(:has(*)) {

  body.is-post main > .container > section:has(> form) {
    max-inline-size: 980px;
    margin-inline: auto;
    margin-block-end: var(--s7);
    padding: var(--s5);
    border: 1px solid var(--line);
    border-radius: var(--r-xl);
    background:
      var(--grad-calc) center top / 100% 3px no-repeat,
      linear-gradient(180deg, var(--accent-soft) 0%, transparent 220px),
      var(--surface);
    box-shadow: var(--shadow-md);
  }

  @media (min-width: 768px) {
    body.is-post main > .container > section:has(> form) { padding: var(--s6); }
  }

  /* The shell's padding is the gap below the controls now. */
  body.is-post main > .container > section:has(> form) > form { margin-block-end: 0; }

  body.is-post main > .container > section:has(> form) > .result-empty,
  body.is-post main > .container > section:has(> form) > .result {
    margin-block-start: var(--s5);
  }

  /* An empty state inside a card should read as a waiting area, not as
     a second card. */
  body.is-post main > .container > section:has(> form) > .result-empty {
    background: var(--surface-sunk);
    border-style: dashed;
    border-color: var(--line);
  }
}

/* Controls, wherever they are on the post. Slightly taller and rounder
   than the site default, sunk rather than flush, which is what makes a
   form look considered rather than merely correct. Values only — every
   control keeps its type, its name, its label and its behaviour. */
/* A sunk fill, so a field reads as something to type INTO on a raised
   card. The border keeps --line-strong from section 7 untouched: it is
   the only thing giving the control a boundary, and a card this quiet
   cannot afford to lose it.

   background-COLOR, never the shorthand. A .select carries its chevron
   as a background-image with its own repeat, position and size, and the
   shorthand resets all three — which tiles the chevron across the whole
   control. */
body.is-post .input,
body.is-post .select,
body.is-post .textarea {
  min-block-size: 48px;
  border-radius: var(--r-md);
  background-color: var(--surface-sunk);
  transition: border-color var(--dur-fast), box-shadow var(--dur-fast),
              background-color var(--dur-fast);
}
body.is-post .textarea { min-block-size: 96px; }

/* Focus lifts the field to the card's own surface — the clearest "this
   one is live" a filled control can give without changing its size. */
body.is-post .input:focus,
body.is-post .select:focus,
body.is-post .textarea:focus { background-color: var(--surface); }

body.is-post .label {
  font-size: 0.88rem;
  font-weight: 650;
  color: var(--text-soft);
}

/* The buttons need no restyling: .btn-primary already carries
   --grad-btn and --glow-calc, and .tools-page main already remaps both
   to violet.

   What they need is air. The last .field in a .field-row has its bottom
   margin removed — correctly, it is the end of that row — and nothing
   replaces it, so an action row directly under one sat flush against
   the input above it. Only where a button row actually follows inputs,
   so a row of buttons inside a result panel or a section foot is not
   pushed around. */
body.is-post .field + .btn-row,
body.is-post .field-row + .btn-row,
body.is-post fieldset + .btn-row { margin-block-start: var(--s5); }

/* --------------------------------------------------------------------
   35.4  Result panel

   Already the loudest thing on the page — this only trues it up with
   the shell it now sits inside.
   ------------------------------------------------------------------ */

body.is-post .result { border-radius: var(--r-card); }
body.is-post .result-primary { padding-block: var(--s6); }
body.is-post .result-label { font-size: 0.78rem; letter-spacing: 0.1em; }
body.is-post .result-section-title { letter-spacing: 0.09em; }

body.is-post .stat {
  background: var(--surface-sunk);
  border-radius: var(--r-md);
}

/* --------------------------------------------------------------------
   35.5  The article column and the "On this page" rail

   app.js builds the rail out of the headings the article already has —
   it invents no heading, writes no text and links to nothing that is
   not on the page — and marks the article `has-rail`. Everything below
   is what that class turns on.

   ABOVE THE SPLIT the rail is a second grid column, sticky beside the
   text for the whole scroll. It is placed by grid, not by DOM order, so
   it stays the first thing inside the article for a screen reader and a
   keyboard while sitting to the right on screen.

   `grid-row: 1 / span 120` is the span trick: the rail has to own every
   row so its grid area is as tall as the article, which is what gives
   position: sticky something to travel inside. The empty rows it forces
   cost nothing because row-gap is 0 — the prose spaces itself with
   margins, as it always has.
   ------------------------------------------------------------------ */

body.is-post main > .container > article { padding-block: var(--s7); }

body.is-post main > .container > article .prose,
body.is-post main > .container > article.prose {
  font-size: 1.05rem;
  line-height: 1.78;
  max-inline-size: var(--measure-article);
}

/* The rail card is a <details>: collapsed on a phone, where ten links
   between the reader and the first paragraph is a wall rather than a
   shortcut, and open from tablet up, where it is a map. app.js sets the
   attribute from the viewport. */
.toc-card {
  margin-block-end: var(--s6);
  border: 1px solid var(--line);
  border-radius: var(--r-card);
  background: var(--surface);
  box-shadow: var(--shadow-sm);
}

.toc-card > summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s3);
  padding: var(--s4) var(--s5);
  min-block-size: 52px;
  font-size: 0.76rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-faint);
  cursor: pointer;
  list-style: none;
}
.toc-card > summary::-webkit-details-marker { display: none; }
.toc-card > summary:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: -3px;
  border-radius: var(--r-card);
}

.toc-card > summary::after {
  content: "";
  flex-shrink: 0;
  inline-size: 9px;
  block-size: 9px;
  border-inline-end: 2px solid var(--text-faint);
  border-block-end: 2px solid var(--text-faint);
  transform: rotate(45deg) translate(-2px, -2px);
  transition: transform var(--dur);
}
.toc-card[open] > summary::after { transform: rotate(-135deg) translate(-2px, -2px); }

.toc-nav {
  padding: var(--s4) var(--s5) var(--s5);
  border-block-start: 1px solid var(--line);
}

.toc-links {
  counter-reset: toc;
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: 0.92rem;
}
.toc-links li { margin: 0; }

.toc-links a {
  counter-increment: toc;
  display: flex;
  align-items: baseline;
  gap: var(--s3);
  padding: 7px var(--s3);
  margin-inline: calc(var(--s3) * -1);
  border-radius: var(--r-sm);
  color: var(--text-soft);
  text-decoration: none;
  line-height: 1.45;
  transition: color var(--dur-fast), background-color var(--dur-fast);
}
.toc-links a::before {
  content: counter(toc);
  flex-shrink: 0;
  inline-size: 1.4em;
  font-size: 0.78rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--text-faint);
}
.toc-links a:hover { color: var(--accent); background: var(--accent-soft); }
.toc-links a:hover::before { color: var(--accent); }

/* Set by app.js as the reader passes each section. Colour alone would
   be a weak signal on a list this quiet, so the marker takes the accent
   too and the label gains weight. */
.toc-links a.is-current {
  color: var(--accent-strong);
  background: var(--accent-soft);
  font-weight: 650;
}
.toc-links a.is-current::before { color: var(--accent); }

/* Two columns of links while the card is full width and there is room —
   a ten-item list is a lot of vertical space to spend above an article. */
@media (min-width: 640px) and (max-width: 1119.98px) {
  .toc-links { columns: 2; column-gap: var(--s7); }
  .toc-links li { break-inside: avoid; }
  /* The negative inline margin that lets a link's highlight bleed past
     the text goes, because here it would bleed into the column gap and
     leave one column's row touching the next column's number. */
  .toc-links a {
    display: block;
    margin-inline: 0;
    padding-inline: var(--s2);
    text-indent: -1.8em;
    padding-inline-start: calc(var(--s2) + 1.8em);
  }
  .toc-links a::before { display: inline-block; }
}

@media (min-width: 1120px) {

  /* The reading measure is the COLUMN, not a cap on each child.

     The obvious version — let column 1 take the free space and cap the
     children inside it — does not work in a grid: an auto inline margin
     on a grid item makes it shrink to its content and centre, so every
     short heading and list ended up its own width, floating in the
     middle of a ragged column. Sizing the track instead gives one edge
     that every child shares, whichever shape the article has: a
     calculator's <article><div class="prose">, or a translated page
     where the <article> IS the prose and its children are the grid
     items.

     space-between then pins the rail to the far edge and lets the slack
     fall between the two, where it reads as the gutter it is. */
  body.is-post main > .container > article.has-rail {
    max-inline-size: none;
    display: grid;
    grid-template-columns: minmax(0, var(--measure-article)) var(--post-rail);
    justify-content: space-between;
    column-gap: var(--post-gap);
    row-gap: 0;
    align-items: start;
  }

  body.is-post main > .container > article.has-rail > * {
    grid-column: 1;
    min-inline-size: 0;
  }

  body.is-post main > .container > article.has-rail > .toc-card {
    grid-column: 2;
    grid-row: 1 / span 120;
    position: sticky;
    inset-block-start: calc(var(--header-h) + var(--s4));
    margin-block-end: 0;
  }

  /* A twenty-entry list is taller than the viewport, so the links scroll
     inside a card that stays put.

     The cap is on the NAV, and both halves of that are deliberate.

     Not on the card, made a flex column: the content of a <details>
     sits in a UA slot, and that slot — not anything selectable here — is
     the flex item that refuses to shrink below its content, so the list
     overflowed straight out through the card's rounded corner.

     Not on the list either: each link has a negative inline margin so
     its highlight can bleed past the text, which is overflow against the
     bare <ol> and earned the card a horizontal scrollbar. The nav has
     the padding those margins bleed into, so against the nav they
     overflow nothing.

     The subtrahend is the chrome above and below: the sticky offset, the
     summary, and room at the bottom of the screen. */
  body.is-post main > .container > article.has-rail > .toc-card .toc-nav {
    max-block-size: calc(100vh - var(--header-h) - 110px);
    overflow-y: auto;
    overscroll-behavior: contain;
  }
}

/* Whatever follows the rail in the source is the first thing in the
   reading column, so it starts at the top of it. */
body.is-post article > .toc-card + * { margin-block-start: 0; }

/* --------------------------------------------------------------------
   35.6  Prose

   Headings, formula blocks, worked examples, callouts and tables. Every
   one of these components already existed and is already in use across
   the articles — .example in nineteen calculator templates, .callout in
   twenty-four, table.data in thirty-three, and all three on the tool
   pages too. So this is the cheapest kind of upgrade there is: restyle
   what is there, and every article gets it without a word being
   touched.
   ------------------------------------------------------------------ */

/* A short accent rule above each section instead of a full-width line
   across the column. Same separation, far less furniture. */
body.is-post .prose > h2 {
  margin-block-start: var(--s8);
  padding-block-start: 0;
  border-block-start: 0;
  letter-spacing: -0.02em;
  text-wrap: balance;
}
body.is-post .prose > h2::before {
  content: "";
  display: block;
  inline-size: 40px;
  block-size: 3px;
  margin-block-end: var(--s4);
  border-radius: var(--r-full);
  background: var(--grad-calc);
}
body.is-post .prose > h2:first-child { margin-block-start: 0; }
body.is-post article > .toc-card + h2 { margin-block-start: 0; }

body.is-post .prose h3 {
  margin-block-start: var(--s6);
  letter-spacing: -0.012em;
}

body.is-post .prose li { margin-block-end: var(--s3); }
body.is-post .prose strong { font-weight: 680; color: var(--text); }

/* An accented marker is the cheapest way to make a list look drawn
   rather than defaulted, and it costs no markup — which matters when the
   lists in question are fifty-eight finished articles nobody is going to
   re-tag. ::marker is ignored where a list sets its own counter
   (.step-list), so the numbered steps keep their pills. */
body.is-post .prose ul > li::marker { color: var(--accent); }
body.is-post .prose ol > li::marker { color: var(--text-faint); font-weight: 700; }

/* An inline expression should look like an expression. */
body.is-post .prose code {
  padding: 0.12em 0.42em;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  background: var(--surface-sunk);
  font-family: var(--font-mono);
  font-size: 0.88em;
  color: var(--accent-strong);
}

/* A standalone formula or sample. `pre` in one of these articles is
   always a formula, a worked line or a few rows of data — never a code
   listing — so it is framed rather than terminal-styled. */
body.is-post .prose pre:not(.visually-hidden) {
  margin-block: var(--s5);
  padding: var(--s4) var(--s5);
  border: 1px solid var(--line);
  border-inline-start: 3px solid var(--accent);
  border-radius: var(--r-md);
  background: var(--surface-sunk);
  font-family: var(--font-mono);
  font-size: 0.92rem;
  line-height: 1.7;
  overflow-x: auto;
  white-space: pre-wrap;
}
body.is-post .prose pre code {
  padding: 0;
  border: 0;
  background: none;
  font-size: 1em;
  color: inherit;
}

/* Worked examples. The head keeps its small uppercase label and gains
   the accent, so an example reads as a worked block at a glance while
   scrolling past. */
body.is-post .example {
  margin-block: var(--s6);
  border-radius: var(--r-card);
  border-color: var(--line);
  background: var(--surface);
  box-shadow: var(--shadow-sm);
}
body.is-post .example-head {
  padding: var(--s3) var(--s5);
  background: var(--accent-soft);
  border-block-end-color: var(--accent-line);
  color: var(--accent-strong);
  letter-spacing: 0.09em;
}
body.is-post .example-body { padding: var(--s5); }

body.is-post .callout {
  border-radius: var(--r-card);
  padding: var(--s4) var(--s5);
  line-height: 1.65;
}
/* A callout used as a section — the tool pages' "About this converter"
   — carries a real heading, which should not inherit the prose rule. */
body.is-post .callout h2,
body.is-post .callout h3 {
  margin: 0 0 var(--s2);
  padding: 0;
  border: 0;
  font-size: 1rem;
  font-weight: 700;
  color: inherit;
}
body.is-post .callout h2::before,
body.is-post .callout h3::before { content: none; }

body.is-post .disclaimer {
  border-radius: var(--r-card);
  border-inline-start-width: 4px;
}

/* Numbered steps, in an article and in a result panel. */
body.is-post .step-list > li::before {
  background: var(--accent-soft);
  border-color: var(--accent-line);
  color: var(--accent-strong);
}

/* Tables. The wrapper already scrolls on a narrow screen and the
   stacked variant already folds to label/value pairs below 640px —
   both stay exactly as they are. This is the frame around them. */
body.is-post .table-wrap {
  border-radius: var(--r-card);
  box-shadow: var(--shadow-sm);
}
body.is-post table.data thead th {
  background: var(--surface-alt);
  color: var(--text-faint);
}
body.is-post table.data tbody tr:nth-child(even) { background: var(--surface-alt); }
body.is-post table.data tbody tr:hover { background: var(--accent-soft); }

/* --------------------------------------------------------------------
   35.7  FAQ

   Same questions, same answers, same <details> element and the same
   FAQPage markup describing them. A bigger target, a quieter border and
   a marker that answers rather than decorates.
   ------------------------------------------------------------------ */

body.is-post .faq-list { gap: var(--s3); }

body.is-post .faq-list .disclosure {
  border-radius: var(--r-card);
  border-color: var(--line);
  background: var(--surface);
  box-shadow: var(--shadow-sm);
  transition: border-color var(--dur-fast), box-shadow var(--dur-fast);
}
body.is-post .faq-list .disclosure:hover { border-color: var(--accent-line); }
body.is-post .faq-list .disclosure[open] {
  border-color: var(--accent-line);
  box-shadow: var(--shadow-md);
}

body.is-post .faq-list .disclosure > summary {
  gap: var(--s4);
  padding: var(--s4) var(--s5);
  min-block-size: 60px;
  font-size: 1.01rem;
  font-weight: 650;
  line-height: 1.5;
}
body.is-post .faq-list .disclosure[open] > summary {
  color: var(--accent-strong);
  border-block-end-color: var(--line);
}

/* A plus that becomes a minus: two background bars closed, one open.
   The half-turn is what animates — the bar it lands on is symmetric, so
   the marker spins into a minus rather than stopping at a diagonal. It
   replaces the inherited chevron, which is why it is built on the same
   ::after rather than added beside it. */
body.is-post .faq-list .disclosure > summary::after {
  content: "";
  flex-shrink: 0;
  inline-size: 26px;
  block-size: 26px;
  border: 0;
  border-radius: 50%;
  transform: none;
  background:
    linear-gradient(currentColor, currentColor) center / 11px 2px no-repeat,
    linear-gradient(currentColor, currentColor) center / 2px 11px no-repeat,
    var(--accent-soft);
  color: var(--accent);
  transition: transform var(--dur) var(--ease);
}
body.is-post .faq-list .disclosure[open] > summary::after {
  transform: rotate(180deg);
  background:
    linear-gradient(currentColor, currentColor) center / 11px 2px no-repeat,
    var(--accent-soft);
}

body.is-post .faq-list .disclosure-body {
  padding: var(--s4) var(--s5) var(--s5);
  color: var(--text-soft);
  line-height: 1.72;
}

/* --------------------------------------------------------------------
   35.8  Everything under the article

   The related rail, and the two "helpful" blocks footer.php adds to
   every post. Section 30 already draws the cards and they are the same
   cards the directories use, which is the point — the end of a post
   should look like the page it sends you to. This is the spacing and
   the section heads around them.
   ------------------------------------------------------------------ */

body.is-post main > .container > section[aria-labelledby="related-heading"] {
  margin-block-start: var(--s6);
  padding-block-start: var(--s7);
  border-block-start: 1px solid var(--line);
}

body.is-post .section-head h2,
body.is-post .calc-extras h2 { letter-spacing: -0.02em; }

body.is-post .related-tools-line { color: var(--text-soft); }

/* --------------------------------------------------------------------
   35.9  Narrow screens

   The article is one column below --post-split, so nothing here has to
   undo a grid. What is left is padding: a card that is comfortable at
   1280px is cramped at 375px for the opposite reason — the padding is
   eating the line length rather than framing it.
   ------------------------------------------------------------------ */

@media (max-width: 599px) {
  @supports selector(:has(*)) {
    body.is-post main > .container > section:has(> form) {
      padding: var(--s4);
      border-radius: var(--r-card);
    }
  }

  body.is-post .tool-workspace { border-radius: var(--r-card); }

  body.is-post main > .container > article { padding-block: var(--s6); }
  body.is-post main > .container > article .prose,
  body.is-post main > .container > article.prose {
    font-size: 1.02rem;
    line-height: 1.72;
  }

  body.is-post .prose > h2 { margin-block-start: var(--s7); }

  .toc-card > summary { padding: var(--s3) var(--s4); }
  .toc-nav { padding: var(--s3) var(--s4) var(--s4); }

  body.is-post .faq-list .disclosure > summary { padding: var(--s4); font-size: 0.98rem; }
  body.is-post .faq-list .disclosure-body { padding: var(--s4); }
  body.is-post .example-head,
  body.is-post .example-body { padding-inline: var(--s4); }
  body.is-post .callout { padding: var(--s4); }
}

/* --------------------------------------------------------------------
   35.10  Print and reduced motion
   ------------------------------------------------------------------ */

@media print {
  body.is-post::before { display: none; }
  .toc-card { display: none !important; }
  body.is-post .tool-workspace { box-shadow: none; background: none; }
  @supports selector(:has(*)) {
    body.is-post main > .container > section:has(> form) {
      box-shadow: none;
      background: none;
      border-color: #999;
    }
  }
}

@media (prefers-reduced-motion: reduce) {
  body.is-post .faq-list .disclosure > summary::after,
  .toc-card > summary::after { transition: none; }
}

/* --------------------------------------------------------------------
   35.11  DARK SURFACES — NAVY-BLACK

   Requested for a post page specifically: the rest of the site's dark
   theme is a warm charcoal on purpose (see the note at the DARK token
   block above) — a post page wants a cooler, bluer near-black instead.

   Rather than reskin every card, input and table rule a second time,
   this overrides the SIX role tokens everything already reads —
   --bg, --surface, --surface-sunk, --surface-alt, --line,
   --line-strong — on body.is-post itself. Custom properties inherit,
   so every descendant already painting itself with `var(--surface)` or
   `var(--line)` repaints navy with no second rule. --accent, --accent-
   soft and the two hue scales are untouched, which is what keeps the
   teal-vs-violet identity intact ON these new surfaces.

   Dark theme only: light theme is not part of this request and keeps
   its existing pale surfaces, on posts exactly as everywhere else.

   Four steps, one hue family, each one step lighter than the last —
   the ordering IS the depth cue, so it stays deliberately subtle
   rather than four unrelated colours:
     canvas < sunk (inputs) < surface (cards, article, shell) < alt
     (hover tint, zebra rows) < line < line-strong
   ------------------------------------------------------------------ */

:root[data-theme="dark"] body.is-post {
  --bg:            #05060c;
  --surface-sunk:  #090b16;
  --surface:       #0c0f1c;
  --surface-alt:   #141a2e;
  --line:          #212a44;
  --line-strong:   #2c3856;
}

/* --------------------------------------------------------------------
   35.12  HOVER ACCENTS

   The card hover in section 30 (border, shadow, a 2px lift) is shared
   with the calculator and tool DIRECTORIES and stays exactly as it is
   there. This adds one more step — a background tint, plus the card's
   own hue on its heading and icon — ON A POST PAGE ONLY, so a related-
   calculator card here reads teal and the equivalent card on /tools/
   still reads exactly as it did before this change.

   (hover: hover) and (pointer: fine) is the guard the brief asks for by
   name: a tap on a touchscreen matches :hover in most browsers and
   would otherwise leave a card visibly "stuck" highlighted after the
   finger lifts. Gating on real hover support means a touch tap gets
   only the existing :active/:focus-visible state, never this one.
   ------------------------------------------------------------------ */

@media (hover: hover) and (pointer: fine) {

  body.is-post .tool-card:hover,
  body.is-post .utool:hover,
  body.is-post .cat-card:hover {
    background: var(--surface-alt);
  }

  body.is-post .tool-card:hover .tool-card-body h3,
  body.is-post .utool:hover .utool-body h3,
  body.is-post .cat-card:hover h3 {
    color: var(--card-strong);
  }

  body.is-post .faq-list .disclosure:hover {
    background: var(--surface-alt);
  }

  body.is-post .example:hover,
  body.is-post .stat:hover {
    border-color: var(--accent-line);
  }
}

/* ==========================================================================
   37. POST PAGES — navy canvas, red hover

   Scoped to body.is-post, exactly like section 35, so it reaches the 41
   calculator pages and the 20 tool pages and nothing else. The homepage,
   both directories, the category hubs, search and the legal pages never
   carry the class and are untouched.

   WHY THIS IS A TOKEN OVERRIDE AND NOT A PILE OF BACKGROUND RULES
   -----------------------------------------------------------------------
   Sections 1-36 read role tokens and never raw hex, so a post page can be
   recoloured completely by redefining the roles once, here, on <body>.
   Every card, table, input, callout and chart inside inherits without a
   single extra selector.

   DARK IN BOTH THEMES — DELIBERATE, AND WHAT IT COSTS
   -----------------------------------------------------------------------
   A post page is navy whichever theme the visitor picked. That means the
   theme toggle has no visible effect on these pages, and it means the
   LIGHT theme's roles can never be allowed to reach them: --text there is
   near-black and --accent is a deep teal, both of which are invisible on
   navy. So the full light-on-dark set is restated below rather than just
   --bg, and `color-scheme: dark` goes with it so native select menus,
   scrollbars and autofill follow.

   Contrast against --surface (#0d1a2e), all AA or better for body text:
     --text        #eef3fa  ~15.0:1
     --text-soft   #c6d3e4  ~11.0:1
     --text-faint  #95a7bf  ~6.9:1
     --accent      #42bcaf  ~7.6:1   (teal, calculators)
     --tool-accent #8f78f0  ~6.4:1   (violet, tools)
   ========================================================================== */

body.is-post {
  /* Native UI — menus, scrollbars, autofill — must follow the page, not
     the theme attribute, or a light-theme visitor gets white dropdowns
     punched into a navy form. */
  color-scheme: dark;

  /* ---- neutrals ----
     These six are the values section 35.11 already gives a post page in
     DARK theme. They are repeated verbatim rather than re-picked: 35.11
     is signed-off work, it outranks this block whenever the theme
     attribute is dark, and two navies a shade apart depending on a
     setting nobody can see would be a bug, not a design. Change one, and
     change it in both places. */
  --bg:            #05060c;
  --surface-sunk:  #090b16;
  --surface:       #0c0f1c;
  --surface-alt:   #141a2e;
  --line:          #212a44;
  --line-strong:   #2c3856;

  /* ---- everything else the DARK theme defines ----
     Restated here so a post page in LIGHT theme behaves exactly as it
     already does in dark. The values are copied from
     :root[data-theme="dark"] unchanged — this block makes a post page
     use the dark theme, it does not invent a third one.

     Leaving any of these out is the failure mode that matters: they do
     not error, they silently resolve to the light theme's value and put
     a deep teal heading on a navy card at about 2:1. */
  --text:          var(--ink-100);
  --text-soft:     var(--ink-300);
  --text-faint:    var(--ink-400);

  --accent:        var(--brand-400);
  --accent-hover:  var(--brand-300);
  --accent-strong: var(--brand-300);
  --accent-soft:   #12302e;
  --accent-line:   var(--brand-800);
  --on-accent:     #06201f;
  --focus:         var(--brand-300);
  --meter-from:    var(--brand-500);
  --meter-to:      var(--brand-300);

  --calc-accent:   var(--brand-400);
  --calc-strong:   var(--brand-300);
  --calc-soft:     #12302e;
  --calc-line:     var(--brand-800);
  --on-calc:       #06201f;

  --tool-accent:       var(--violet-400);
  --tool-accent-hover: var(--violet-300);
  --tool-strong:       var(--violet-300);
  --tool-soft:         #1e1840;
  --tool-soft-2:       #262052;
  --tool-line:         var(--violet-800);
  --tool-wash:         #1b1638;
  --on-tool:           #120b2c;

  --ok-bg:    #10281c;   --ok-fg:    #7fd8a6;   --ok-line:   #1e5237;
  --warn-bg:  #2a2110;   --warn-fg:  #f0c674;   --warn-line: #574117;
  --bad-bg:   #2b1516;   --bad-fg:   #f3a6a3;   --bad-line:  #5c2a29;
  --info-bg:  #10202e;   --info-fg:  #96c7ec;   --info-line: #1f4767;

  --hero-wash:     #14302e;
  --toast-bg:      var(--ink-100);
  --toast-fg:      var(--ink-950);
  --brand-mark-bg: var(--brand-400);
  --brand-mark-fg: #06201f;

  --tile-calc:      linear-gradient(145deg, var(--brand-500) 0%, var(--brand-800) 100%);
  --tile-tool:      linear-gradient(145deg, var(--violet-500) 0%, var(--violet-800) 100%);
  --glow-calc-soft: rgba(66, 188, 175, .11);
  --glow-tool-soft: rgba(143, 120, 240, .11);
  --glow-calc:      0 16px 40px -14px rgba(66, 188, 175, .40);
  --glow-tool:      0 16px 40px -14px rgba(143, 120, 240, .38);

  --grad-btn:       linear-gradient(135deg, var(--brand-300) 0%, var(--brand-500) 100%);
  --grad-btn-calc:  linear-gradient(135deg, var(--brand-300) 0%, var(--brand-500) 100%);
  --grad-btn-tool:  linear-gradient(135deg, var(--violet-300) 0%, var(--violet-400) 100%);
  --grad-calc:      linear-gradient(135deg, var(--brand-400) 0%, var(--brand-600) 100%);
  --grad-calc-soft: linear-gradient(135deg, #12302e 0%, #0f2523 100%);
  --grad-tool:      linear-gradient(135deg, var(--violet-400) 0%, var(--violet-600) 100%);
  --grad-tool-soft: linear-gradient(135deg, #221b47 0%, #1a1536 100%);

  --mesh-1: rgba(66, 188, 175, .20);
  --mesh-2: rgba(143, 120, 240, .18);
  --mesh-3: rgba(38, 158, 148, .16);

  /* Shadows fall on navy now, so they need the dark theme's weight — one
     tuned for white is invisible here. */
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, .40);
  --shadow-md: 0 2px 4px rgba(0, 0, 0, .38), 0 4px 14px rgba(0, 0, 0, .34);
  --shadow-lg: 0 10px 34px rgba(0, 0, 0, .50);

  --world-calc-bg: linear-gradient(140deg, #123a36 0%, #1d2320 100%);
  --world-tool-bg: linear-gradient(140deg, #2b2253 0%, #221d27 100%);

  /* ---- the hover pair ----
     A light red block with near-black red text on it, 12.9:1. Tokens so
     the whole treatment can be softened from one place: set
     --post-hover-bg to #2b1516 and --post-hover-fg to #f3a6a3 for a
     tinted hover instead of a solid one. */
  --post-hover-bg:   #ffd9dc;
  --post-hover-fg:   #3b0d13;
  --post-hover-line: #ef9ea6;
}

/* The violet meter needs its light-on-dark ends on a tool post in either
   theme. Section 21 only does this under [data-theme="dark"]. */
body.is-post.tools-page main {
  --meter-from: var(--violet-500);
  --meter-to:   var(--violet-300);
}

/* --------------------------------------------------------------------
   37.1  Red hover

   Cards, result table rows and the panel blocks — the same set section
   35 already gave a hover, re-pointed at the red pair. Buttons, text
   links, focus rings and error states are deliberately NOT included:
   buttons keep the teal/violet identity, and red stays meaningful on an
   error because nothing else claims it permanently.

   Still behind (hover: hover) and (pointer: fine). A tap matches :hover
   in most mobile browsers, and a card left stuck bright red after a
   finger lifts would be worse here than anywhere else on the site.
   ------------------------------------------------------------------ */

@media (hover: hover) and (pointer: fine) {

  body.is-post .tool-card:hover,
  body.is-post .utool:hover,
  body.is-post .cat-card:hover,
  body.is-post .faq-list .disclosure:hover,
  body.is-post .example:hover,
  body.is-post .stat:hover,
  body.is-post table.data tbody tr:hover {
    background: var(--post-hover-bg);
    border-color: var(--post-hover-line);
    color: var(--post-hover-fg);
  }

  /* Everything inside the hovered block follows it. Without this the
     light-on-navy text stays light and lands on a light red ground,
     which is the one way this treatment could fail badly. `inherit`
     rather than a second colour so icons drawn with currentColor come
     along too. */
  body.is-post .tool-card:hover *,
  body.is-post .utool:hover *,
  body.is-post .cat-card:hover *,
  body.is-post .faq-list .disclosure:hover *,
  body.is-post .example:hover *,
  body.is-post .stat:hover *,
  body.is-post table.data tbody tr:hover > * {
    color: inherit;
  }

  /* A highlighted result row already carries a background of its own, so
     it needs the hover stated again to win. */
  body.is-post table.data tbody tr.row-highlight:hover {
    background: var(--post-hover-bg);
  }
}

/* --------------------------------------------------------------------
   37.2  Hover rules that section 35 and section 30 would otherwise win

   These exist only because the earlier sections colour parts of a hovered
   card individually, at a specificity the `:hover *` rule above cannot
   reach. Listed one by one rather than fought with !important, so the
   cascade still reads honestly.

   Without them the failure is silent and ugly: a teal heading and a dark
   teal arrow pill sitting on the light red card, and a navy summary bar
   painted over a red FAQ row.
   ------------------------------------------------------------------ */

@media (hover: hover) and (pointer: fine) {

  /* Card heading: section 35 pins it to --card-strong. */
  body.is-post .tool-card:hover .tool-card-body h3,
  body.is-post .utool:hover .utool-body h3,
  body.is-post .cat-card:hover h3 {
    color: var(--post-hover-fg);
  }

  /* The arrow pill carries its own background as well as its own colour. */
  body.is-post .tool-card:hover .card-arrow,
  body.is-post .utool:hover .card-arrow,
  body.is-post .cat-card:hover .card-arrow {
    color: var(--post-hover-fg);
    background: rgba(59, 13, 19, .10);
  }

  /* A category card's count chip, same story. */
  body.is-post .cat-card:hover .count {
    color: var(--post-hover-fg);
  }

  /* <summary> covers almost all of a collapsed FAQ row and has its own
     hover background, so the row would read navy rather than red. */
  body.is-post .faq-list .disclosure:hover > summary,
  body.is-post .faq-list .disclosure:hover > summary:hover {
    background: transparent;
    color: var(--post-hover-fg);
  }

  /* The icon tile inside a hovered card keeps its gradient on purpose —
     it is the one piece that should stay branded — so its glyph must not
     follow the red text colour. */
  body.is-post .tool-card:hover .tool-card-icon,
  body.is-post .utool:hover .utool-icon,
  body.is-post .cat-card:hover .cat-card-icon,
  body.is-post .tool-card:hover .tool-card-icon svg,
  body.is-post .utool:hover .utool-icon svg,
  body.is-post .cat-card:hover .cat-card-icon svg {
    color: var(--on-tile);
  }
}
/* ==========================================================================
   39. THE SEPTEMBER 2026 EXPANSION

   Nine calculators, and between them six new result components. They
   share the surface, line and radius tokens with everything above, so
   nothing here defines a colour of its own: a `.ms-card` and a
   `.pl-finding` are the same card at different densities, and both
   inherit the navy override in section 37 without being named there.

   The status colours (ok / warn / bad) are the existing four-token
   sets, used through their variables, so every component below changes
   together in dark mode.
   ========================================================================== */

/* --------------------------------------------------------------------
   39.1 Gold — the piece row

   Reuses .cbm-row's mechanics with its own grid template, because a
   gold row carries five controls (weight, unit, purity, fineness,
   quantity) where a carton row carries four. Inheriting the carton
   template put the quantity field under the index column.
   -------------------------------------------------------------------- */

.gold-row {
  grid-template-columns:
    1.6rem            /* index */
    minmax(0, 1.1fr)  /* weight */
    minmax(0, 1.2fr)  /* unit */
    minmax(0, 1.3fr)  /* purity */
    minmax(0, 0.9fr)  /* quantity */
    auto;             /* remove */
}

/* The fineness box exists only when "other" is chosen, and when it does
   it takes a row under the first five columns rather than squeezing a
   sixth column in. */
.gold-row .gold-fineness { grid-column: 2 / -1; }

/* A unit name is long in every language ("pennyweight (dwt)"), so the
   select gets the tighter padding the CBM unit picker uses rather than
   the default, which ellipsised it to a single letter. */
.gold-row .select {
  padding-inline-end: 1.9rem;
  background-position: right 0.5rem center;
}

@media (max-width: 899px) {
  .gold-row { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) auto; }
  .gold-row .cbm-row-index { display: none; }
  .gold-row .gold-fineness { grid-column: 1 / -1; }
}

@media (max-width: 599px) {
  .gold-row { grid-template-columns: minmax(0, 1fr) auto; }
  .gold-row .field { grid-column: 1 / -1; }
  .gold-row .cbm-row-actions { grid-column: 2; grid-row: 1; }
}

/* --------------------------------------------------------------------
   39.2 Birthday milestones — one card per named milestone

   Each card carries the rule that produced it and, where the tradition
   disagrees with itself, a note saying so. That note is the point of
   the component, so it is styled to be read rather than skimmed.
   -------------------------------------------------------------------- */

.ms-cards {
  display: grid;
  gap: var(--s4);
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
}

.ms-card {
  padding: var(--s4);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--surface);
}

.ms-card-title {
  margin: 0 0 var(--s2);
  font-size: 0.82rem;
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--text-soft);
}

.ms-card-date {
  margin: 0;
  font-size: 1.22rem;
  font-weight: 700;
  line-height: 1.25;
  color: var(--text);
}

.ms-card-meta {
  margin: var(--s1) 0 var(--s3);
  font-size: 0.9rem;
  color: var(--text-soft);
}

.ms-card-rule,
.ms-card-note {
  margin: 0;
  font-size: 0.86rem;
  line-height: 1.5;
  color: var(--text-soft);
}

.ms-card-note {
  margin-top: var(--s2);
  padding-top: var(--s2);
  border-top: 1px dashed var(--line);
}

/* A contested definition is marked, not hidden. */
.ms-card-note.is-contested {
  color: var(--warn-fg);
  border-top-color: var(--warn-line);
}

.ms-card.is-today {
  border-color: var(--ok-line);
  background: var(--ok-bg);
}
.ms-card.is-today .ms-card-date { color: var(--ok-fg); }
.ms-card.is-past .ms-card-date { color: var(--text-soft); }

/* Table rows for milestones already behind you. */
tr.is-past th,
tr.is-past td { color: var(--text-soft); }

/* --------------------------------------------------------------------
   39.3 Attendance — the verdict line

   One sentence, before any table, coloured by which side of the line
   the reader is on. The colour comes from the existing status tokens,
   so it survives dark mode and the post-page override untouched.
   -------------------------------------------------------------------- */

.at-verdict {
  margin-bottom: var(--s5);
  padding: var(--s4) var(--s5);
  border: 1px solid var(--line);
  border-inline-start: 4px solid var(--text-soft);
  border-radius: var(--r-md);
  background: var(--surface-sunk);
}

.at-verdict-text {
  margin: 0;
  font-size: 1.02rem;
  font-weight: 600;
  line-height: 1.5;
}

.at-verdict.is-ok {
  border-color: var(--ok-line);
  border-inline-start-color: var(--ok-fg);
  background: var(--ok-bg);
  color: var(--ok-fg);
}
.at-verdict.is-warn {
  border-color: var(--warn-line);
  border-inline-start-color: var(--warn-fg);
  background: var(--warn-bg);
  color: var(--warn-fg);
}
.at-verdict.is-bad {
  border-color: var(--bad-line);
  border-inline-start-color: var(--bad-fg);
  background: var(--bad-bg);
  color: var(--bad-fg);
}

/* Cells that have crossed a threshold, in a projection table. */
.is-ok-text  { color: var(--ok-fg);  font-weight: 650; }
.is-bad-text { color: var(--bad-fg); font-weight: 650; }

/* --------------------------------------------------------------------
   39.4 Resin parts, and the glass layer builder
   -------------------------------------------------------------------- */

.rc-parts {
  display: grid;
  gap: var(--s4);
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  margin-bottom: var(--s4);
}

.rc-part {
  padding: var(--s4);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--surface-sunk);
  text-align: center;
}

.rc-part-title {
  margin: 0 0 var(--s2);
  font-size: 0.82rem;
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--text-soft);
}

.rc-part-value {
  margin: 0;
  font-size: 1.5rem;
  font-weight: 750;
  line-height: 1.1;
  font-variant-numeric: tabular-nums;
  color: var(--text);
}

.rc-part-sub {
  margin: var(--s1) 0 0;
  font-size: 0.88rem;
  color: var(--text-soft);
  font-variant-numeric: tabular-nums;
}

/* The dimension fields are rebuilt per shape, so this row has to cope
   with one, two or three of them without reflowing oddly. */
.rc-dims { align-items: start; }
.rc-dims[hidden] { display: none; }

.gw-layers {
  display: grid;
  gap: var(--s3);
  margin-bottom: var(--s4);
}

.gw-layer {
  display: grid;
  grid-template-columns: minmax(7rem, auto) minmax(0, 1fr) minmax(0, 1fr) auto;
  gap: var(--s3);
  align-items: end;
  padding: var(--s3);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--surface-sunk);
}

.gw-layer-tag {
  align-self: center;
  font-size: 0.86rem;
  font-weight: 650;
  color: var(--text-soft);
}

/* A cavity is drawn as an absence: dashed border, italic label. */
.gw-layer[data-kind="cavity"] { border-style: dashed; }
.gw-layer[data-kind="cavity"] .gw-layer-tag { font-style: italic; }

.gw-layer-actions { align-self: center; }

@media (max-width: 719px) {
  .gw-layer { grid-template-columns: minmax(0, 1fr) auto; }
  .gw-layer-tag { grid-column: 1 / -1; }
  .gw-layer .field { grid-column: 1 / -1; }
  .gw-layer-actions { grid-column: 2; grid-row: 1; justify-self: end; }
}

/* --------------------------------------------------------------------
   39.5 Rucking — the term breakdown

   Four bars on the shared .meter, each in its own colour so a reader
   can see at a glance whether the pack or the hill is the expensive
   part. Every colour is an existing token.
   -------------------------------------------------------------------- */

.rk-bar-standing { background: var(--text-soft); }
.rk-bar-load     { background: var(--warn-fg); }
.rk-bar-walking  { background: linear-gradient(90deg, var(--meter-from), var(--meter-to)); }
.rk-bar-grade    { background: var(--bad-fg); }

/* --------------------------------------------------------------------
   39.6 Miscarriage statistics — a finding with its source attached

   The component exists so a figure can never be rendered without the
   study and the population that produced it. The source line is
   deliberately not small print: it is the part that makes the number
   mean anything.
   -------------------------------------------------------------------- */

.pl-urgent { margin: var(--s5) 0; }

.pl-findings {
  display: grid;
  gap: var(--s5);
  margin-bottom: var(--s5);
}

.pl-finding {
  padding: var(--s5);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--surface);
}

.pl-finding.is-muted { background: var(--surface-sunk); }

.pl-finding-title {
  margin: 0 0 var(--s2);
  font-size: 0.92rem;
  font-weight: 700;
  color: var(--text-soft);
}

.pl-finding-figure {
  margin: 0 0 var(--s3);
  font-size: 2rem;
  font-weight: 750;
  line-height: 1.05;
  font-variant-numeric: tabular-nums;
  color: var(--text);
}

.pl-finding-measure {
  margin: 0 0 var(--s3);
  font-size: 0.95rem;
  line-height: 1.55;
  color: var(--text);
}

/* The condition attached to the week figures. It is the single
   most-dropped sentence on this subject, so it is boxed rather than
   run into the paragraph above it. */
.pl-finding-condition {
  margin: 0 0 var(--s3);
  padding: var(--s3) var(--s4);
  border-inline-start: 3px solid var(--warn-fg);
  border-radius: var(--r-sm);
  background: var(--warn-bg);
  color: var(--warn-fg);
  font-size: 0.92rem;
  line-height: 1.5;
}

.pl-finding-note {
  margin: 0 0 var(--s2);
  font-size: 0.9rem;
  line-height: 1.55;
  color: var(--text-soft);
}

.pl-finding-source {
  margin: var(--s3) 0 0;
  padding-top: var(--s3);
  border-top: 1px solid var(--line);
  font-size: 0.86rem;
  line-height: 1.55;
  color: var(--text-soft);
  overflow-wrap: anywhere;
}

.pl-finding-source cite { font-style: italic; }

/* --------------------------------------------------------------------
   39.7 Shared odds and ends
   -------------------------------------------------------------------- */

/* A number field with no business being as wide as a name field. */
.input-narrow { max-width: 12rem; }

@media print {
  #gold-form .btn-row,
  #bm-form .btn-row,
  #dm-form .btn-row,
  #at-form .btn-row,
  #gc-form .btn-row,
  #rc-form .btn-row,
  #gw-form .btn-row,
  #rk-form .btn-row,
  #pl-form .btn-row { display: none !important; }

  /* A finding's source has to survive the printout: it is the part a
     reader would take to a clinician. */
  .pl-finding { break-inside: avoid; border-color: #000; }
  .pl-finding-source { color: #000; }
  .ms-card { break-inside: avoid; }
}

/* ==========================================================================
   UNIVERSAL FILE INPUT (app/includes/file-input.php, assets/js/fileinput.js)

   The whole .fi is the visible box — dashed edge, sunk surface — and the
   <label> inside it (the engines' [data-drop]) is borderless and fills the
   top, so the entire box reads as one control. The "Add files" pill is the
   obvious action; provider buttons sit in the same box, formats under.
   Accent follows the page (violet on tool pages via .tools-page main).
   ========================================================================== */
.fi {
  position: relative;
  display: grid;
  gap: var(--s3);
  padding: var(--s5) var(--s4) var(--s4);
  background: var(--surface-sunk);
  border: 2px dashed var(--line-strong);
  border-radius: var(--r-xl);
  text-align: center;
  transition: border-color var(--dur) var(--ease), background-color var(--dur) var(--ease);
}
.fi:hover,
.fi:focus-within,
.fi[data-over="true"],
.fi:has(.dropzone[data-dragover="true"]) {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 7%, var(--surface-sunk));
}
.fi .fi-zone {
  padding: var(--s3) var(--s2);
  background: transparent;
  border: 0;
  border-radius: var(--r-lg);
  gap: var(--s2);
}
.fi .fi-zone::before { display: none; }
.fi .fi-zone:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; }
.fi .fi-zone[data-dragover="true"] { transform: none; }

.fi-add {
  display: inline-flex;
  align-items: center;
  gap: .55rem;
  min-block-size: 52px;
  padding: .7rem 1.6rem;
  font-weight: 700;
  font-size: 1.02rem;
  color: var(--on-accent, #fff);
  background: var(--accent);
  border-radius: var(--r-lg);
  box-shadow: 0 10px 22px -12px var(--accent);
  transition: transform var(--dur) var(--ease), filter var(--dur) var(--ease);
}
.fi-add svg { inline-size: 1.25rem; block-size: 1.25rem; }
.fi .fi-zone:hover .fi-add { transform: translateY(-1px); filter: brightness(1.06); }
.fi .fi-title { font-weight: 600; color: var(--text-soft); }
.fi .fi-hint { color: var(--text-faint); font-size: var(--step--1); }
.fi[data-over="true"] .fi-title,
.fi:has(.dropzone[data-dragover="true"]) .fi-title { color: var(--accent-strong); }

.fi-sources { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: .6rem; }
.fi-sources-label { flex-basis: 100%; font-size: var(--step--1); color: var(--text-faint); }
.fi-source {
  display: inline-flex;
  align-items: center;
  gap: .5rem;
  min-block-size: 44px;
  padding: .4rem .95rem .4rem .6rem;
  font: inherit;
  font-size: var(--step--1);
  font-weight: 600;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-full);
  cursor: pointer;
}
.fi-source svg { inline-size: 1.5rem; block-size: 1.5rem; }
.fi-source:hover { border-color: var(--accent-line); }
.fi-source:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; }
.fi-source[aria-busy="true"] { opacity: .6; cursor: progress; }
.fi-msg { flex-basis: 100%; margin: 0; font-size: var(--step--1); color: var(--bad-fg); }

.fi-formats { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: .4rem; font-size: .78rem; color: var(--text-faint); }
.fi-formats-label { font-weight: 600; }
.fi-fmt {
  padding: .15rem .55rem;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  font-weight: 600;
  letter-spacing: .02em;
}
.fi-more { display: contents; }
.fi-more summary {
  padding: .15rem .55rem;
  font-weight: 700;
  color: var(--accent-strong);
  border-radius: var(--r-sm);
  cursor: pointer;
  list-style: none;
}
.fi-more summary::-webkit-details-marker { display: none; }
.fi-more summary:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; }
.fi-more[open] summary { display: none; }
.fi-more-list { display: contents; }

/* Compact variant under the text tools: a single tidy row. */
.fi-slim { padding: var(--s3) var(--s4); gap: var(--s2); }
.fi-slim .fi-zone { grid-auto-flow: row; padding: var(--s2); }
.fi-slim .fi-add { min-block-size: 44px; padding: .5rem 1.2rem; font-size: .95rem; }

@media (max-width: 480px) {
  .fi { padding: var(--s4) var(--s3); }
  .fi-add { inline-size: 100%; justify-content: center; }
  .fi-source { flex: 1 1 calc(50% - .6rem); justify-content: center; }
}
@media (prefers-reduced-motion: reduce) {
  .fi, .fi-add { transition: none; }
  .fi .fi-zone:hover .fi-add { transform: none; }
}
