  * { box-sizing: border-box; }
  html, body { height: 100%; }
  body { margin: 0; background: var(--surface-0); color: var(--text-primary);
         font: var(--fs-base)/var(--lh-base) var(--font-sans); }
  /* Every link reads in both themes (mvp116). There was no base rule, so a
     link nothing styled (Explore's "See the full per-listing funnel", a
     finding's "Too early to tell") drew in the browser's #0000EE: fine on
     white, about 2:1 on the dark surface. `:where()` keeps it at zero
     specificity, so every styled link (nav, buttons, banners) still wins,
     visited or not. */
  :where(a), :where(a:visited) { color: var(--accent-text); text-underline-offset: 2px; }

  /* --------------------------------------------------------- onboarding gate */
  .onboarding[hidden] { display: none; }
  .onboarding { position: fixed; inset: 0; z-index: 40; background: var(--surface-0);
                display: flex; align-items: center; justify-content: center; padding: var(--sp-5); }
  .onboarding-card { max-width: 460px; width: 100%; text-align: center;
                background: var(--surface-1); border: 1px solid var(--border);
                border-radius: var(--r-lg); box-shadow: var(--shadow-2);
                padding: var(--sp-7) var(--sp-6); }
  .onboarding-mark { display: flex; align-items: center; justify-content: center;
                gap: var(--sp-2);
                font: 600 var(--fs-lg)/1 var(--font-display);
                letter-spacing: .04em; color: var(--accent-text); margin-bottom: var(--sp-5); }
  .onboarding-card h1 { font-size: var(--fs-xl); margin: 0 0 var(--sp-3); }
  .onboarding-card p { color: var(--text-secondary); margin: 0 0 var(--sp-5); }
  /* The way back for a seller who ADDED a shop (mvp113 review). */
  .onboarding-alt { display: flex; flex-wrap: wrap; gap: var(--sp-2); justify-content: center;
                    margin-top: var(--sp-4); padding-top: var(--sp-4); border-top: 1px solid var(--border); }
  .onboarding-alt .danger { color: var(--down-text); }
  .onboarding-card button { width: 100%; margin-top: var(--sp-2); }

  /* ------------------------------------------------------------ app shell
     Breakpoints (spec system): sm <640, md 640-1024, lg >1024. Mobile-first:
     base rules below are the sm layout — topbar + bottom tab bar + a slide-in
     nav drawer. The lg override further down replaces that with an
     always-visible sidebar (the grouped nav "drawer" content, just pinned). */
  .shell { display: flex; flex-direction: column; min-height: 100vh; }

  .topbar { display: flex; align-items: center; gap: var(--sp-3);
            padding: var(--sp-3) var(--sp-4); background: var(--surface-nav);
            border-bottom: 1px solid var(--border); position: sticky; top: 0; z-index: 15; }
  .topbar .wordmark { flex: 1; padding: 0; }
  .icon-btn { position: relative; display: flex; align-items: center;
              justify-content: center;
              width: 36px; height: 36px; padding: 0; flex: none;
              border: 1px solid var(--border); border-radius: var(--r-sm);
              background: var(--surface-1); color: var(--text-secondary);
              cursor: pointer; transition: background var(--t-fast), color var(--t-fast); }
  .icon-btn::after { content: ""; position: absolute; inset: -4px; }  /* 44px touch target */
  .icon-btn:hover { background: var(--surface-inset); color: var(--text-primary); }
  .icon-btn:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
  .theme-toggle { position: relative; display: flex; align-items: center;
                   justify-content: center;
                   width: 32px; height: 32px; margin: 0 var(--sp-2); padding: 0;
                   border: 1px solid var(--border); border-radius: var(--r-sm);
                   background: var(--surface-1); color: var(--text-secondary);
                   cursor: pointer; transition: background var(--t-fast), color var(--t-fast); }
  .theme-toggle::after { content: ""; position: absolute; inset: -6px; }  /* 44px touch target */
  .theme-toggle:hover { background: var(--surface-inset); color: var(--text-primary); }
  .theme-toggle:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
  .topbar select { max-width: 140px; }

  aside#sidenav { display: none; } /* lg-only sidebar; hidden below lg */

  /* Mark + wordmark: two rows of text beside one full-height mark. The mark
     is injected by app.js (js/icons.js::logoMark) into every .wordmark, so
     the grid has to work with and without the .tag row. */
  .wordmark { display: grid; grid-template-columns: auto 1fr;
              column-gap: var(--sp-3); align-items: center; }
  /* Explicit placement, not auto-flow: `grid-row: 1 / -1` resolves against
     the EXPLICIT grid (one row here), so the tag wrapped underneath the mark
     instead of beside it. */
  .wordmark .logo-mark { grid-column: 1; grid-row: 1 / span 2; flex: none; }
  .wordmark .name { grid-column: 2; grid-row: 1; }
  .wordmark .tag { grid-column: 2; grid-row: 2; }
  .wordmark .name { font: 600 var(--fs-xl)/1.1 var(--font-display);
                    letter-spacing: .01em; }
  .wordmark .tag { font-size: var(--fs-xs); color: var(--text-muted);
                   text-transform: uppercase; letter-spacing: .14em; margin-top: 3px; }

  nav { display: flex; flex-direction: column; gap: 2px; }
  nav a { display: flex; align-items: center; gap: 10px; padding: 9px var(--sp-2);
          border-radius: var(--r-sm); color: var(--text-secondary); cursor: pointer;
          text-decoration: none; font-size: var(--fs-base);
          transition: background var(--t-fast), color var(--t-fast); }
  nav a:hover { background: var(--surface-inset); color: var(--text-primary); }
  nav a.active { background: var(--accent-tint); color: var(--accent-text);
                 font-weight: 600; }
  nav a svg { flex: none; }
  .nav-group { margin-bottom: var(--sp-4); }
  .nav-group-label { font-size: var(--fs-xs); color: var(--text-muted);
                      text-transform: uppercase; letter-spacing: .12em;
                      padding: 0 var(--sp-2); margin-bottom: 4px; }
  /* ------------------------------------------------------- account (mvp43)
     The identity + shop + settings + sign-out menu that replaced the two
     duplicated shop <select>s. One trigger per breakpoint, one menu. */
  .account-trigger { display: flex; align-items: center; gap: var(--sp-2);
                     width: 100%; background: transparent; border: 0;
                     border-radius: var(--r-sm); padding: var(--sp-2);
                     color: var(--text-primary); cursor: pointer; text-align: left; }
  .account-trigger:hover { background: var(--surface-2); }
  .account-trigger.sm { width: auto; padding: 4px; }
  .account-trigger[hidden] { display: none; }  /* the demo has no account (mvp100) */
  .avatar { flex: none; width: 28px; height: 28px; border-radius: 50%;
            display: grid; place-items: center; background: var(--accent);
            color: var(--on-accent); font: 600 var(--fs-sm)/1 var(--font-sans); }
  .avatar.lg { width: 40px; height: 40px; font-size: var(--fs-md); }
  /* The shop name is allowed to truncate; the EMAIL is not the headline. */
  .account-who { min-width: 0; display: flex; flex-direction: column; }
  .account-shop { font: 600 var(--fs-sm)/1.3 var(--font-sans);
                  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .account-email { font-size: var(--fs-xs); color: var(--text-muted);
                   overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

  .accountmenu { position: fixed; inset: 0; z-index: 40; display: none; }
  .accountmenu.open { display: block; }
  .accountmenu-panel { position: absolute; background: var(--surface-1);
            border: 1px solid var(--border); border-radius: var(--r-md);
            box-shadow: var(--shadow-3); padding: var(--sp-3);
            width: min(300px, calc(100vw - var(--sp-4) * 2));
            /* Phone: anchored under the topbar trigger, top-right. */
            right: var(--sp-4); top: calc(var(--sp-4) + 40px);
            animation: menu-in var(--t-fast); }
  @keyframes menu-in { from { transform: translateY(-6px); opacity: 0; }
                       to { transform: none; opacity: 1; } }
  .menu-head { display: flex; gap: var(--sp-3); align-items: center;
               padding: var(--sp-2); }
  .menu-head-text { min-width: 0; }
  .menu-email { font: 600 var(--fs-sm)/1.3 var(--font-sans);
                overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .menu-sub { font-size: var(--fs-xs); color: var(--text-muted); }
  .menu-section { border-top: 1px solid var(--border); margin-top: var(--sp-2);
                  padding-top: var(--sp-2); }
  .menu-label { font-size: var(--fs-xs); color: var(--text-muted);
                text-transform: uppercase; letter-spacing: .1em;
                padding: 0 var(--sp-2); margin-bottom: 4px; }
  .menu-item, .menu-shop { display: flex; align-items: center; gap: var(--sp-2);
            width: 100%; background: transparent; border: 0; cursor: pointer;
            padding: var(--sp-2); border-radius: var(--r-sm);
            color: var(--text-primary); font: var(--fs-sm)/1.3 var(--font-sans);
            text-align: left; }
  .menu-item:hover, .menu-shop:hover { background: var(--surface-2); }
  .menu-shop > span:not(.menu-tick) { flex: 1; overflow: hidden;
            text-overflow: ellipsis; white-space: nowrap; }
  .menu-shop.on { font-weight: 600; }
  .menu-tick { color: var(--accent); display: inline-flex; }
  .menu-item.danger { color: var(--down-text); }
  .menu-item.danger:hover { background: var(--down-tint); }
  .menu-item svg, .menu-shop svg { flex: none; }
  /* Left-aligned like the shop rows (mvp113): a <button> centres its content
     by default, so a menu item with no flex:1 child sat in the middle. */
  .menu-item { justify-content: flex-start; }
  .menu-item > span:not(.menu-meta) { flex: 1; }
  .menu-meta { font-size: var(--fs-xs); color: var(--text-muted);
               border: 1px solid var(--border); border-radius: 999px; padding: 1px 8px; }
  .account-shop-row { display: flex; align-items: center; gap: 6px; min-width: 0; }
  .account-shop-row .account-email { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .account-plan { flex: none; font: 600 10px/1.6 var(--font-sans); letter-spacing: .02em;
                  color: var(--accent-text); background: var(--accent-tint, var(--surface-2));
                  border-radius: 999px; padding: 0 7px; }

  /* Settings' section list. Below lg it is a horizontal scroller above the
     content; the lg block turns it into a column beside the content.

     `flex-direction: row` and `display: inline-flex` are NOT redundant, for
     exactly the reason `#bottombar` states three rules down: this IS a <nav>,
     so `nav { flex-direction: column }` and `nav a { display: flex }` above
     apply to it. Omitting them is what made this render as a stack of
     full-width buttons from mvp43 until mvp55 — a column container whose
     default `align-items: stretch` sized every pill to the page. */
  .settings-nav { display: flex; flex-direction: row; gap: var(--sp-2);
                  overflow-x: auto; margin-bottom: var(--sp-4);
                  padding-bottom: 2px; scrollbar-width: none; }
  .settings-nav::-webkit-scrollbar { display: none; }
  .settings-nav a { flex: none; display: inline-flex; align-items: center;
                    width: auto; font-size: var(--fs-sm); color: var(--text-secondary);
                    background: var(--surface-1); border: 1px solid var(--border);
                    border-radius: var(--r-full); padding: 6px var(--sp-3);
                    text-decoration: none; cursor: pointer; white-space: nowrap; }
  .settings-nav a:hover { color: var(--text-primary); border-color: var(--border-strong); }
  .settings-nav a.active { background: var(--accent-tint); color: var(--accent-text);
                           border-color: transparent; font-weight: 600; }
  .settings-body { min-width: 0; }

  .content { padding: var(--sp-4);
             padding-bottom: calc(var(--bottombar-h) + var(--sp-5)); min-width: 0; }

  /* How fresh the whole app is (mvp36). Above the content, quiet: it is a
     standing fact, not a warning — until it isn't, and then it is the only
     thing on screen that explains why the numbers look wrong. */
  .freshness { margin: 0 auto var(--sp-3); max-width: 1180px; }
  .freshness p { margin: 0; }
  .page { max-width: 1180px; margin: 0 auto; }
  .page.narrow { max-width: 880px; }
  .page-head { margin-bottom: var(--sp-5); }
  .page-head.with-control { display: flex; align-items: flex-start;
               justify-content: space-between; gap: var(--sp-4); }
  .page-head select { font-size: var(--fs-sm); flex-shrink: 0; }

  /* ------------------------------------------------------- range control
     Shared analysis-period picker (components.js::rangeSelect). Wraps rather
     than overflowing: the custom branch adds two date inputs and a button,
     which do not fit beside the preset select on a phone. */
  .range-control { display: flex; flex-wrap: wrap; align-items: center;
                   gap: var(--sp-2); font-size: var(--fs-sm); }
  .range-control select { font-size: var(--fs-sm); }
  .range-custom { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2); }
  .range-custom[hidden] { display: none; }
  .range-custom input[type=date] { background: var(--surface-1);
      color: var(--text-primary); border: 1px solid var(--border-strong);
      border-radius: var(--r-sm); padding: 7px 9px; font: inherit;
      font-size: var(--fs-sm); max-width: 100%; }
  .range-custom input[type=date]:focus { outline: none; border-color: var(--accent);
      box-shadow: 0 0 0 3px color-mix(in srgb, var(--focus-ring) 25%, transparent); }
  .page-head h1 { font: 600 var(--fs-2xl)/var(--lh-tight) var(--font-display);
                  margin: 0; }
  .page-head p { margin: 6px 0 0; color: var(--text-muted); font-size: var(--fs-sm); }

  /* -------------------------------------------------------- bottom tab bar
     sm/md only: the 4 daily surfaces, thumb-reachable. */
  /* `flex-direction: row` is not redundant: #bottombar IS a <nav>, and the
     generic `nav { flex-direction: column }` rule above it wins on the
     property it doesn't set. The bar had therefore been stacking its four
     tabs vertically on every phone -- ~200px of chrome instead of ~56 --
     invisible in every desktop screenshot because the bar is hidden at lg. */
  #bottombar { position: fixed; left: 0; right: 0; bottom: 0; z-index: 15;
               display: flex; flex-direction: row; background: var(--surface-nav);
               border-top: 1px solid var(--border);
               padding: 2px 0 max(4px, env(safe-area-inset-bottom)); }
  #bottombar a { flex: 1 1 0; min-width: 0; display: flex; flex-direction: column;
                 align-items: center; justify-content: center; gap: 2px;
                 padding: 6px 2px; color: var(--text-muted);
                 font-size: var(--fs-xs); text-decoration: none;
                 white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  #bottombar a.active { color: var(--accent-text); }

  /* ------------------------------------------------------------ nav drawer
     sm/md: slide-in overlay opened by the topbar Menu button. */
  .navdrawer { position: fixed; inset: 0; background: var(--scrim);
               z-index: 30; display: none; }
  .navdrawer.open { display: block; }
  .navdrawer-panel { position: absolute; left: 0; top: 0; bottom: 0;
                      width: min(300px, 85vw); background: var(--surface-1);
                      padding: var(--sp-5) var(--sp-4); overflow-y: auto;
                      box-shadow: var(--shadow-3);
                      animation: slidein-left var(--t-slow); }
  @keyframes slidein-left { from { transform: translateX(-16px); opacity: 0; }
                            to { transform: none; opacity: 1; } }
  .navdrawer-panel .close { position: absolute; top: var(--sp-3); right: var(--sp-3);
               background: transparent; border: 0; color: var(--text-muted); padding: 6px; }

  /* ------------------------------------------------ signed out (mvp43)
     Full-page, above everything, because a dead session is not a state you
     can usefully see the app behind. Split layout: brand on one side, the one
     action on the other. Stacks to a single column on a phone, brand first. */
  .signedout { position: fixed; inset: 0; z-index: 60; display: grid;
               grid-template-rows: auto 1fr; background: var(--surface-0);
               overflow-y: auto; }
  .signedout-brand { background: var(--accent-tint); padding: var(--sp-6) var(--sp-5);
                     display: flex; flex-direction: column; gap: var(--sp-3);
                     align-items: flex-start; justify-content: center; }
  .signedout-word { font: 600 var(--fs-xl)/1.1 var(--font-display);
                    color: var(--text-primary); }
  .signedout-tag { color: var(--text-secondary); max-width: 42ch; margin: 0; }
  .signedout-panel { display: grid; place-items: center;
                     padding: var(--sp-6) var(--sp-5); }
  .signedout-form { width: min(360px, 100%); text-align: left; }
  .signedout-form h1 { font: 600 var(--fs-xl)/1.15 var(--font-display);
                       margin: 0 0 var(--sp-2); }
  .signedout-form > p { color: var(--text-secondary); margin: 0 0 var(--sp-5); }
  .signedout-form button { width: 100%; }
  .signedout-home { margin: var(--sp-5) 0 0; font-size: var(--fs-sm); }
  .signedout-home a { color: var(--accent-text); text-decoration: none; }
  .signedout-home a:hover { text-decoration: underline; }
  .signedout-alt { font-size: var(--fs-sm); color: var(--text-secondary);
                   margin: var(--sp-3) 0 0; }
  .signedout-alt a { color: var(--accent-text); }
  .signedout-fine { font-size: var(--fs-xs); color: var(--text-muted);
                    margin: var(--sp-4) 0 0; }
  @media (min-width: 768px) {
    .signedout { grid-template-rows: none; grid-template-columns: 1fr 1fr; }
    .signedout-brand { padding: var(--sp-8) var(--sp-8); }
  }

  /* ------------------------------------------------------------ ask (mvp49) */
  /* The FAB is the one-tap entry from any screen; `#ask` is the page it leads
     to, so the button hides itself there (app.js::syncAskFab). */
  .ask-fab { position: fixed; right: var(--sp-4);
             bottom: calc(var(--bottombar-h) + var(--sp-3));
             z-index: 20; width: 52px; height: 52px; border-radius: 50%;
             display: flex; align-items: center; justify-content: center;
             background: var(--accent); color: var(--on-accent); border: 0;
             box-shadow: var(--shadow-3); cursor: pointer; }
  .ask-fab[hidden] { display: none; }
  /* One reading column. The log grows and the composer follows it down the
     page rather than being pinned to the viewport: a fixed composer over a
     scrolling log needs its own scroll container, and on a phone that fights
     the address bar for the last 60px. */
  .ask-page { max-width: 46rem; }
  .ask-page[hidden] { display: none; }
  .ask-page .chat-log { max-height: none; overflow: visible; }
  .ask-starters { display: flex; flex-wrap: wrap; gap: var(--sp-2);
                  margin: var(--sp-3) 0; }
  .ask-starters .chip { display: inline-flex; align-items: center; gap: 6px;
                  background: var(--surface-1); border: 1px solid var(--border);
                  color: var(--text-secondary); border-radius: var(--r-full);
                  padding: 7px 12px; font-size: var(--fs-sm); cursor: pointer;
                  text-align: left; }
  .ask-starters .chip:hover { border-color: var(--border-strong);
                  color: var(--text-primary); }
  .ask-composer { position: sticky; bottom: var(--sp-3);
                  background: var(--surface-0); padding-top: var(--sp-2); }
  .ask-composer .note { margin: var(--sp-2) 0 0; }
  /* A chart is a message, so it sits in the log with the same left edge as
     the bubbles rather than in a card of its own. */
  .msg-visual { margin: var(--sp-3) 0; padding: 0; }
  .msg-visual figcaption { font: 600 var(--fs-sm)/1.3 var(--font-display);
                  color: var(--text-primary); margin-bottom: var(--sp-2); }
  .msg-visual figcaption .sub { font-weight: 400; color: var(--text-muted); }
  .msg-visual .note { margin: var(--sp-1) 0 0; }

  /* ------------------------------------------------ locked-with-preview */
  /* Both children occupy the SAME grid cell, so the container is as tall as
     whichever is taller. An absolutely-positioned note clipped its own copy
     the moment the preview shape was shorter than the sentence. */
  /* ONE locked state (mvp82 S13). Benchmarks and Ask used the same helper and
     read as two different things: Benchmarks as a teaser with structure behind
     the message, Ask as a large pale empty box with the message adrift in it,
     because its `shape` was a single 180px block. So the frame is fixed here
     rather than per caller: the blurred shape is CAPPED, the note is a bounded
     block centred in it, and its body copy is primary text. It was
     `--text-secondary` under a blur, which is the lowest-contrast sentence in
     the app and the one being asked to sell something. */
  .locked { display: grid; border-radius: var(--r-md); overflow: hidden; }
  .locked > * { grid-area: 1 / 1; }
  .locked-shape { filter: blur(3px); opacity: .5; pointer-events: none;
              user-select: none; max-height: 220px; overflow: hidden; }
  .locked-note { display: flex; gap: var(--sp-2);
              align-items: center; justify-content: center; text-align: left;
              padding: var(--sp-3); background: color-mix(in srgb, var(--surface-1) 78%, transparent); }
  .locked-note > div { max-width: 46ch; }
  .locked-note b { display: block; }
  .locked-note .body-sm { color: var(--text-primary); }
  .locked-note a { color: var(--accent-text); font-size: var(--fs-sm);
              display: inline-block; margin-top: var(--sp-1); }
  .locked-ic { flex: none; color: var(--accent-text); }

  /* ------------------------------------------------------ demo banner (mvp100)
     In normal flow above the shell rather than sticky: the topbar is already
     sticky at top 0, and two sticky bars there would overlap. It is on every
     screen of the demo and cannot be dismissed, which is what "persistent"
     has to mean for a label that says the numbers are not a real seller's. */
  .demo-banner[hidden] { display: none; }
  .demo-banner { display: flex; flex-wrap: wrap; align-items: center;
                 justify-content: center; gap: var(--sp-1) var(--sp-4);
                 padding: var(--sp-2) var(--sp-4); text-align: center;
                 background: var(--accent-tint); color: var(--text-primary);
                 border-bottom: 1px solid var(--border); font-size: var(--fs-sm); }
  .demo-banner a { color: var(--accent-text); font-weight: 600; white-space: nowrap; }
  /* The way out sits at the leading edge, where "back" is looked for; the
     message and the signup link stay centred beside it. */
  .demo-banner .demo-back { font-weight: 500; color: var(--text-secondary); }
  .demo-banner .demo-back:hover { color: var(--accent-text); }
  @media (min-width: 640px) {
    .demo-banner:not([hidden]) { display: grid; grid-template-columns: auto 1fr auto; }
  }

  /* ---------------------------------------------------------- legal footer */
  .legal-footer { text-align: center; font-size: var(--fs-xs); color: var(--text-muted);
                  padding: var(--sp-4); }

  /* lg starts at 1024, not 1025 (mvp12): a 1024px iPad in landscape used to
     fall one pixel short and get the phone bottom-bar with no sidebar. */
  @media (min-width: 1024px) {
    .shell { flex-direction: row; }
    .topbar, #bottombar { display: none; }
    /* Ask exists on desktop too (mvp14): the FAB was `display: none` above
       lg, so the whole feature was phone-only. Same control, parked clear of
       the bottom bar it no longer has to avoid. */
    .ask-fab { bottom: var(--sp-5); right: var(--sp-5); }
    /* The sidebar is a fixed-height column with ONE scrolling part. It used
       to lay every group out at natural height inside a 100vh box, so on a
       laptop the SETTINGS group ran off the bottom of the screen with no way
       to reach it. The wordmark and the shop switcher are pinned; only the
       nav list scrolls. */
    /* `--border-strong`, not `--border` (mvp65). Dark mode's chrome-to-page
       step tops out at 1.083 — pure black against the page is only 1.086,
       because the contrast formula compresses at low luminance, and lifting the
       planes far enough to do better pushes `cat-1` under the 3:1 graphic
       floor. So the luminance does what it can and the EDGE carries the rest. */
    aside#sidenav { display: flex; background: var(--surface-nav);
            border-right: 1px solid var(--border-strong);
            padding: var(--sp-5) var(--sp-4); flex-direction: column;
            gap: var(--sp-4); position: sticky; top: 0; height: 100vh;
            width: 232px; flex: none; overflow: hidden; }
    aside#sidenav > nav { flex: 1 1 auto; min-height: 0; overflow-y: auto;
            /* Room for the scrollbar so it never sits on the labels, and a
               little bleed so a scrolled list doesn't look clipped mid-item. */
            margin-right: calc(var(--sp-2) * -1); padding-right: var(--sp-2);
            scrollbar-width: thin; }
    aside#sidenav > .account-trigger { flex: none; border-top: 1px solid var(--border);
            border-radius: 0; padding-top: var(--sp-3); margin-top: var(--sp-2); }
    /* On lg the menu rises from the sidebar foot, not the top-right corner. */
    .accountmenu-panel { left: var(--sp-4); right: auto;
                         top: auto; bottom: var(--sp-4); }
    aside#sidenav .nav-group:last-child { margin-bottom: 0; }
    .content { flex: 1; padding: var(--sp-6) var(--sp-6) var(--sp-8); }

    /* Settings' sections move beside the content. Sticky, because the page is
       long enough that a list pinned to the top scrolls out of reach on
       exactly the screens where it is most useful. */
    .settings-layout { display: grid; grid-template-columns: 184px minmax(0, 1fr);
            gap: var(--sp-6); align-items: start; }
    .settings-layout > .settings-nav { flex-direction: column; overflow: visible;
            position: sticky; top: var(--sp-6); margin-bottom: 0; gap: 2px; }
    .settings-layout > .settings-nav a { display: flex; width: 100%;
            border-color: transparent; background: transparent;
            border-radius: var(--r-sm); padding: 8px var(--sp-3);
            white-space: normal; }
    .settings-layout > .settings-nav a:hover { background: var(--surface-inset);
            border-color: transparent; }
    .settings-layout > .settings-nav a.active { background: var(--accent-tint); }
  }

  /* Cards whose visibility is decided in JS use a class, not inline
     `style.display`: a hidden inline style still counts for :nth-child, so a
     grid can't tell which cards are actually on screen. */
  .is-hidden { display: none !important; }

  /* ---------------------------------------------------------------- cards */
  .card { background: var(--surface-1); border: 1px solid var(--border);
          border-radius: var(--r-md); padding: var(--sp-5); box-shadow: var(--shadow-1);
          margin-bottom: var(--sp-4); }
  .card > h2 { font: 600 var(--fs-md)/var(--lh-tight) var(--font-display);
               color: var(--text-primary); margin: 0 0 var(--sp-4); }
  .card > h3 { font: 600 var(--fs-base)/var(--lh-tight) var(--font-display);
               color: var(--text-primary); margin: 0 0 var(--sp-2); }
  /* `:is(h2, h3)`, because a card can carry sub-sections and its sub-headings
     need the same two-line treatment. While this said `h2` only, the Google
     Analytics <h3> in Settings matched nothing: its `.sub` stayed inline in
     the bold display font and the whole heading read as one run-on sentence
     (mvp55). A sub-line that silently becomes part of a heading is the kind
     of defect that survives review because the markup is right. */
  .card > :is(h2, h3) .sub { display: block;
                    font: var(--fs-sm)/var(--lh-base) var(--font-sans);
                    color: var(--text-muted); font-weight: 400; margin-top: 2px; }

  /* ------------------------------------------------------------ KPI tiles */
  .tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(168px, 1fr));
           gap: var(--sp-3); margin-bottom: var(--sp-4); }
  .tiles:last-child { margin-bottom: 0; }
  /* Tiles inside a card would otherwise draw a second border inside the
     first. Inside a card they are plain panels on the card's own surface. */
  /* DESCENDANT, not `>` (mvp68). A tile inside a card must not be the same
     token as the card — `--surface-1` on `--surface-1` is a contrast ratio of
     1.000, and the shadow ends up doing all the work, which is exactly how the
     listing drawer's tiles were reported.
     The `>` version required `.tiles` to be a direct child of `.card`, and the
     drawer nests one level deeper (`.card > #d-buyers > .tiles`) because the
     card declares an id'd container its renderer fills. So the rule silently
     applied only to the screens that happen to nest shallowly. A `>` combinator
     is a claim about DOM depth, and DOM depth is what a renderer changes
     without anyone thinking about the stylesheet.
     Tiles with no `.card` ancestor — Today's KPI row, Money's profit tiles —
     keep the raised treatment, which is correct on the page plane. */
  .card .tiles .tile { background: var(--surface-inset); border-color: transparent;
           box-shadow: none; }
  .tile { background: var(--surface-1); border: 1px solid var(--border);
          border-radius: var(--r-md); padding: var(--sp-4); box-shadow: var(--shadow-1); }
  .tile .label { color: var(--text-muted); font-size: var(--fs-xs);
                 text-transform: uppercase; letter-spacing: .1em; }
  .tile .value { font-size: var(--fs-xl); font-weight: 650; margin-top: 4px;
                 font-variant-numeric: tabular-nums; letter-spacing: -.01em; }
  .tile .foot { margin-top: 8px; min-height: 20px; font-size: var(--fs-xs);
                color: var(--text-muted); }
  /* A tile with nothing to say in its footer says nothing, rather than
     reserving a blank strip: the reserve only exists so tiles in the SAME row
     line up, and :empty rules that out. */
  .tile .foot:empty { display: none; }
  .pill-delta { display: inline-flex; align-items: center; gap: 3px;
                padding: 2px 9px 2px 7px; border-radius: var(--r-full);
                font-size: var(--fs-xs); font-weight: 600;
                font-variant-numeric: tabular-nums; }
  .pill-delta.pos { background: var(--up-tint); color: var(--up-text); }
  .pill-delta.neg { background: var(--down-tint); color: var(--down-text); }
  .pill-delta .vs { font-weight: 400; opacity: .85; }
  .tile { position: relative; overflow: hidden; }
  .tile-spark { margin-top: 8px; opacity: .8; }
  .tile-spark svg { display: block; width: 100%; height: 26px; }

  /* -------------------------------------------------- sales-by-product mix */
  .mix-row { display: grid; grid-template-columns: 1fr 84px; align-items: center;
             gap: var(--sp-3); padding: 7px 0; }
  /* Label ABOVE the bar, not inside it. Printed over the fill, a label was
     half on the fill colour and half on the track — so a short bar cut the
     word in two ("Paid Sh|opping") and read as a rendering fault rather than
     a design. Same markup (track > fill + label); the track is now a two-row
     grid: the name on top, a slim measure underneath, with the track itself
     drawn by ::after so the fill can keep its inline percentage width. */
  .mix-bar-track { position: relative; display: grid; grid-template-rows: auto 8px;
                   grid-template-columns: 100%; row-gap: 5px; }
  .mix-bar-label { grid-row: 1; grid-column: 1; font-size: var(--fs-sm);
                   color: var(--text-primary); white-space: nowrap; overflow: hidden;
                   text-overflow: ellipsis; min-width: 0; }
  .mix-bar-track::after { content: ""; grid-row: 2; grid-column: 1; height: 8px;
                   border-radius: var(--r-full); background: var(--surface-inset); }
  /* No minimum width: a zero value must draw NOTHING. A 3px stub on a
     0-count row (the rating histogram's empty stars) reads as "a little",
     which is the one thing it isn't. */
  .mix-bar-fill { position: relative; z-index: 1; grid-row: 2; grid-column: 1;
                  height: 8px; background: var(--chart-line);
                  border-radius: var(--r-full); }
  /* Grid children default to min-width:auto, so a fixed-width sparkline in
     the value cell widened its track and painted outside the card. Same fix
     .movers-grid got in mvp12 Phase 1; .mix-row was missed. */
  .mix-row > * { min-width: 0; }
  .mix-val { display: flex; align-items: center; justify-content: flex-end;
             gap: var(--sp-2); text-align: right; font-size: var(--fs-sm);
             font-variant-numeric: tabular-nums; color: var(--text-secondary); }
  /* The sparkline scales to whatever the cell can spare rather than
     asserting sparkSvg's fixed 120px. */
  .mix-spark { flex: 1 1 auto; min-width: 0; display: flex; }
  .mix-spark svg { width: 100%; height: 22px; }
  @media (max-width: 639px) { .mix-spark { display: none; } }
  /* Change before/after (mvp22): the shop-control bar sits beside the
     listing's own, so the two must be distinguishable at a glance without
     reading the labels. */
  .mix-bar-fill.is-shop { background: var(--text-muted); }
  .mix-bar-fill.is-listing { background: var(--chart-line); }
  /* A change that HURT drew a teal bar, on the panel whose headline says
     Hurt (mvp83). Invariant #17: the loop reports losses as plainly as wins,
     and colour is the fastest thing a seller reads. */
  .mix-bar-fill.is-listing.is-down { background: var(--down-text); }
  .mix-row.rowlink { cursor: pointer; border-radius: var(--r-sm); }
  .mix-row.rowlink:hover { background: var(--surface-inset); }
  /* Two-up only once there's genuinely room for two charts side by side.
     720 was an ad-hoc boundary that didn't line up with the documented
     sm/md/lg system and put two ~340px donut cards on an iPad (mvp12). */
  .movers-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-5);
                 align-items: stretch; }
  @media (max-width: 1023px) { .movers-grid { grid-template-columns: 1fr; } }
  /* A lone trailing card spans the full width instead of leaving a hole beside
     itself. An odd number of cards in a two-up grid is the "half the row is
     empty" look; the card is better full-width than orphaned. */
  @media (min-width: 1024px) {
    .movers-grid > .card:last-child:nth-child(odd) { grid-column: 1 / -1; }
  }
  /* Cards in a two-up row are the same height by default (align-items:
     stretch), so their INTERNAL parts have to line up too, or the ring in one
     card floats while the other sits low. Each card is a column; the chart
     block takes the slack and the legend/footnote settle at the bottom. */
  .movers-grid > .card { display: flex; flex-direction: column; }
  .movers-grid > .card > .chart-square,
  .movers-grid > .card > .chart { margin-top: auto; margin-bottom: auto; }
  .movers-grid > .card > .chart-legend { margin-top: auto; margin-bottom: 0; }
  .movers-grid > .card > .chart-legend + p { margin-bottom: 0; }
  /* Grid items default to min-width:auto, so anything inside that refuses to
     shrink (a wide legend, a long title) widens the TRACK rather than
     overflowing its own box -- which is how one unwrapped legend put the
     whole page into horizontal scroll. */
  .movers-grid > *, .actions-grid > * { min-width: 0; }
  .movers-grid h3 { font-size: var(--fs-sm); text-transform: uppercase;
                    letter-spacing: .06em; color: var(--text-muted);
                    margin: 0 0 var(--sp-2); }

  /* --------------------------------------------------------------- charts */
  svg.chart text { fill: var(--chart-axis-text); font-size: 11px;
                   font-family: var(--font-sans); }
  .grid-line { stroke: var(--chart-grid); stroke-width: 1; }
  .rev-line { stroke: var(--chart-line); stroke-width: 2; fill: none;
              stroke-linejoin: round; }
  /* The fill is set on the element (a gradient url), so this only has to
     carry the fallback for anything that renders the path without defs. */
  .rev-fill { fill: var(--chart-area-from); }
  .rev-line { stroke-linecap: round; }
  .prior-line { stroke: var(--text-muted); stroke-width: 1.5; fill: none;
                stroke-linejoin: round; stroke-linecap: round;
                stroke-dasharray: 4 3; opacity: .7; }
  .yoy-line { stroke: var(--chart-line-2); stroke-width: 1.5; fill: none;
              stroke-linejoin: round; stroke-linecap: round;
              stroke-dasharray: 2 3; opacity: .85; }
  .visits-line { stroke: var(--chart-line-2); stroke-width: 1.5; fill: none;
                 stroke-linejoin: round; stroke-dasharray: 4 2; }
  .rev-bar { fill: var(--chart-line); }
  .rev-bar-2 { fill: var(--chart-line-2); }
  .crosshair { stroke: var(--text-muted); stroke-dasharray: 3 3; }
  /* Wraps: a categorical legend can carry up to 9 entries (8 hues + Other)
     and each one names a listing or fee type. Unwrapped, its min-content
     width (~660px) propagated up through the grid track and pushed the
     whole page into horizontal scroll on a phone. */
  .chart-legend { display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-4);
                  font-size: var(--fs-sm);
                  color: var(--text-muted); margin: 0 0 var(--sp-2); }
  .chart-legend span { display: inline-flex; align-items: center; gap: 6px; }
  .chart-legend .sw { width: 12px; height: 2px; display: inline-block; }
  .chart-legend .sw.revenue { background: var(--chart-line); }
  .chart-legend .sw.visits { background: var(--chart-line-2);
                              border-top: 2px dashed var(--chart-line-2); height: 0; }
  .chart-legend .sw.bar { width: 10px; height: 10px; border-radius: 2px; }
  .chart-legend .sw.bar.this-year { background: var(--chart-line); }
  .chart-legend .sw.bar.last-year { background: var(--chart-line-2); }
  /* Actions' ranked-impact bars (mvp82 S5). Colour encodes the horizon and
     whether the money is a recovered leak or an upside, and until now nothing
     said so — invariant #11 requires a labelled legend wherever colour
     carries meaning. */
  .chart-legend .sw.bar.this-week { background: var(--chart-line); }
  .chart-legend .sw.bar.next-two-weeks { background: var(--chart-line-2); }
  .chart-legend .sw.bar.upside { background: var(--up-text); }
  /* Change-marker overlay toggle (mvp22). Sits in the legend row because it
     is a legend entry that happens to be switchable, not a control. */
  .chart-toggle { display: inline-flex; align-items: center; gap: 6px;
                  cursor: pointer; }
  .chart-toggle input { accent-color: var(--chart-line-2); margin: 0; }

  /* Categorical swatches (donut/stack/hbar legends) — .sw.cat-N picks up the
     fixed-order token; never generate a hue, always one of these 8 + other. */
  .chart-legend .sw.cat { width: 10px; height: 10px; border-radius: 2px; }
  .chart-legend .sw.cat-1 { background: var(--cat-1); }
  .chart-legend .sw.cat-2 { background: var(--cat-2); }
  .chart-legend .sw.cat-3 { background: var(--cat-3); }
  .chart-legend .sw.cat-4 { background: var(--cat-4); }
  .chart-legend .sw.cat-5 { background: var(--cat-5); }
  .chart-legend .sw.cat-6 { background: var(--cat-6); }
  .chart-legend .sw.cat-7 { background: var(--cat-7); }
  .chart-legend .sw.cat-8 { background: var(--cat-8); }
  .chart-legend .sw.cat-other { background: var(--cat-other); }

  /* Donut/scatter host: scales its fixed square/landscape viewBox to the
     container instead of being measured in device pixels the way the
     axis-aligned charts are (js/chart.js's sizeChart). A ring and a bubble
     field are the two marks whose meaning depends on true aspect ratio. */
  /* Capped, not fluid. The square host scales its 240-unit viewBox to the
     container, so on a wide desktop card a donut was drawn at ~2x — which
     scales the center total with it and printed a ~47px figure where the type
     scale says 25px. The cap keeps the ring (and its text) at their designed
     size, and keeps two donuts in a row identical even when their cards are
     different widths. */
  .chart-square { display: block; margin: 0 auto; width: 100%;
                  max-width: 280px; height: auto; }

  /* ------------------------------------------------------------ thumbnails */
  .cell-thumb { width: 44px; }
  /* A lazy thumbnail paints nothing until it loads, so a 28-row catalogue
     read as 28 blank white tiles for several seconds and looked broken
     (mvp82 S12). The tile now carries the SAME shimmer every other loading
     surface in the app uses, and the image covers it once it arrives. */
  .thumb { width: 40px; height: 40px; object-fit: cover; border-radius: var(--r-sm);
           display: block; background: var(--surface-inset);
           position: relative; overflow: hidden; }
  img.thumb { background-image: linear-gradient(90deg, transparent,
                color-mix(in srgb, var(--surface-1) 60%, transparent), transparent);
              background-size: 200% 100%; background-repeat: no-repeat;
              animation: shimmer-bg 1.4s infinite linear; }
  img.thumb.is-loaded { animation: none; background-image: none; }
  @keyframes shimmer-bg { from { background-position: -100% 0; }
                          to { background-position: 200% 0; } }
  .thumb-fallback { display: flex; align-items: center; justify-content: center;
                     color: var(--text-muted); border: 1px solid var(--border); }
  .tooltip { position: fixed; pointer-events: none; background: var(--surface-2);
             border: 1px solid var(--border); border-radius: var(--r-sm);
             padding: 7px 11px; font-size: var(--fs-sm); box-shadow: var(--shadow-2);
             display: none; z-index: 30; font-variant-numeric: tabular-nums; }

  /* ------------------------------------------------- diagnosis + findings */
  .headline { font: 400 var(--fs-lg)/1.45 var(--font-display);
              color: var(--text-primary); margin: 0 0 var(--sp-4); max-width: 62ch; }
  .finding, .action-card { display: flex; gap: var(--sp-3);
              border: 1px solid var(--border); border-radius: var(--r-md);
              padding: var(--sp-4); margin: var(--sp-3) 0;
              background: var(--surface-1); }
  .finding .ic, .action-card .ic { flex: none; width: 32px; height: 32px;
              display: grid; place-items: center; border-radius: var(--r-sm);
              background: var(--accent-tint); color: var(--accent-text); }
  .finding .head, .action-card .head { display: flex; gap: 10px;
              align-items: baseline; flex-wrap: wrap; }
  /* A completed action reads as settled, not as another thing to do. */
  .action-card.action-done { background: var(--surface-0); }
  .action-card.action-done .ic { background: var(--up-tint); color: var(--up-text); }
  .action-card .form-row .ghost svg { margin-right: 5px; }
  .hypo { font-weight: 650; }
  .meta { color: var(--text-muted); font-size: var(--fs-xs); }
  .body-sm { color: var(--text-secondary); font-size: var(--fs-sm); margin-top: 3px; }
  .rec { color: var(--text-primary); font-size: var(--fs-sm); margin-top: 8px;
         display: flex; gap: 6px; align-items: baseline; }
  .rec .lead { color: var(--accent-text); font-weight: 600; flex: none; }

  /* --------------------------------------------------------------- tables */
  /* container-type makes this the query container for the card-collapse rule
     further down. A viewport media query is the wrong proxy here: at a
     1024px viewport the lg sidebar takes 232px, leaving the table ~662px --
     the same squeeze as a 768px phone-layout viewport, but on the other side
     of any viewport threshold you'd pick (mvp12). */
  .table-wrap { overflow-x: auto; container-type: inline-size; }
  table { border-collapse: collapse; width: 100%; font-size: var(--fs-sm); }
  th, td { text-align: left; padding: 9px 12px;
           border-bottom: 1px solid var(--border); }
  th { color: var(--text-muted); font-weight: 600; white-space: nowrap;
       font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: .07em; }
  td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; }
  tbody tr:last-child td { border-bottom: 0; }
  tbody tr:nth-child(even) td { background: color-mix(in srgb,
       var(--surface-inset) 34%, transparent); }
  th.sortable { cursor: pointer; }
  th.sortable:hover { color: var(--text-primary); }
  tr.rowlink { cursor: pointer; }
  tr.rowlink:hover td { background: var(--surface-inset); }
  .cell-title { max-width: 340px; overflow: hidden; text-overflow: ellipsis;
                white-space: nowrap; color: var(--text-primary); }
  .muted { color: var(--text-muted); }
  .delta-cell.pos { color: var(--up-text); font-weight: 600; }
  .delta-cell.neg { color: var(--down-text); font-weight: 600; }
  .pill { display: inline-flex; align-items: center; gap: 4px; padding: 2px 9px;
          border-radius: var(--r-full); font-size: var(--fs-xs); font-weight: 600;
          background: var(--surface-inset); border: 1px solid var(--border);
          color: var(--text-secondary); }
  .pill.rising, .pill.rising_fast { background: var(--up-tint);
          color: var(--up-text); border-color: transparent; }
  .pill.falling, .pill.falling_fast { background: var(--down-tint);
          color: var(--down-text); border-color: transparent; }
  /* Run-status tones (mvp80, Admin). The status WORD is always printed inside
     the pill, so colour is never the only signal — invariant #11's rule, which
     applies to any categorical mark and not only to a chart. `warn` is the one
     that matters: `degraded` means the lake moved and something downstream did
     not, which is a seller reading stale numbers under a success toast. */
  .pill.ok { background: var(--up-tint); color: var(--up-text);
          border-color: transparent; }
  .pill.warn { background: var(--surface-inset); color: var(--text-secondary);
          box-shadow: inset 3px 0 0 var(--cat-4); border-color: transparent; }
  .pill.bad { background: var(--down-tint); color: var(--down-text);
          border-color: transparent; }
  .chips { display: flex; flex-wrap: wrap; gap: var(--sp-2); }
  .footnote { font-size: var(--fs-xs); color: var(--text-muted);
              margin: var(--sp-3) 0 0; }

  /* ----------------------------------------------------- inputs + buttons */
  .form-row { display: flex; align-items: flex-end; gap: var(--sp-4); flex-wrap: wrap; }
  .form-row label { display: flex; flex-direction: column; gap: 6px;
                     min-width: 0; max-width: 100%;
                     font-size: var(--fs-xs); color: var(--text-muted); }
  .form-row input[type=text] { width: 140px; }
  /* max-width caps a <select> at its container: a select sizes itself to its
     longest <option>, and Settings' shop-archetype options are full
     sentences, which pushed the page into horizontal scroll on a phone. */
  input[type=text], textarea, select { background: var(--surface-1);
      color: var(--text-primary); border: 1px solid var(--border-strong);
      border-radius: var(--r-sm); padding: 8px 11px; font: inherit; max-width: 100%;
      transition: border-color var(--t-fast), box-shadow var(--t-fast); }
  input[type=text]:focus, textarea:focus, select:focus { outline: none;
      border-color: var(--accent);
      box-shadow: 0 0 0 3px color-mix(in srgb, var(--focus-ring) 25%, transparent); }
  button { display: inline-flex; align-items: center; justify-content: center;
           gap: 8px; background: var(--accent); color: var(--on-accent); border: 0;
           border-radius: var(--r-md); padding: 9px 16px; font: inherit;
           font-weight: 600; cursor: pointer;
           transition: background var(--t-fast), transform var(--t-fast); }
  button:hover { background: var(--accent-hover); }
  button:active { background: var(--accent-press); transform: translateY(1px); }
  button:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
  button:disabled { opacity: .45; cursor: default; transform: none; }
  button.ghost { background: transparent; color: var(--text-secondary);
                 border: 1px solid var(--border-strong); font-weight: 500; }
  button.ghost:hover { background: var(--surface-inset); color: var(--text-primary); }
  /* The one irreversible action (mvp69). OUTLINED rather than filled, and not
     because a destructive control should be quiet — because a filled
     `--down-text` cannot carry legible text in both themes. White on the light
     value (#be123c) is 6.3:1 and white on the dark one (#ff506b) is 3.2:1,
     under AA, and the dark value has no headroom to darken: mvp66 pinned it
     where it is precisely so red stays readable ON the card. An outline uses
     that same already-validated pair, so the button is unmistakable in both
     themes without a per-theme text colour. */
  button.danger { background: transparent; color: var(--down-text);
                  border: 1px solid var(--down-text); }
  button.danger:hover { background: var(--down-tint); }
  button.danger:active { background: var(--down-tint); }
  .spinner { width: 14px; height: 14px; border-radius: 50%; flex: none;
             border: 2px solid color-mix(in srgb, currentColor 30%, transparent);
             border-top-color: currentColor; animation: spin .7s linear infinite; }
  @keyframes spin { to { transform: rotate(360deg); } }

  /* ------------------------------------------------------------ skeletons */
  .skel { border-radius: var(--r-sm); background: var(--surface-inset);
          position: relative; overflow: hidden; }
  .skel::after { content: ""; position: absolute; inset: 0;
          background: linear-gradient(90deg, transparent,
            color-mix(in srgb, var(--surface-1) 60%, transparent), transparent);
          animation: shimmer 1.4s infinite; }
  @keyframes shimmer { from { transform: translateX(-100%); }
                       to { transform: translateX(100%); } }

  /* Ask's "thinking" bubble (mvp82 S12). Three dots on the same 1.4s beat as
     the skeleton shimmer, so the app has ONE loading tempo. */
  .msg.is-thinking { display: flex; align-items: center; gap: var(--sp-2);
                     color: var(--text-secondary); }
  .thinking-dots { display: inline-flex; gap: 4px; }
  .thinking-dots i { width: 5px; height: 5px; border-radius: 50%;
                     background: var(--text-muted); animation: thinking 1.4s infinite; }
  .thinking-dots i:nth-child(2) { animation-delay: .18s; }
  .thinking-dots i:nth-child(3) { animation-delay: .36s; }
  @keyframes thinking { 0%, 60%, 100% { opacity: .25; }
                        30% { opacity: 1; } }

  /* ---------------------------------------------------------- empty state */
  .empty { text-align: center; padding: var(--sp-8) var(--sp-5); }
  .empty .art { font-size: 0; margin-bottom: var(--sp-4); color: var(--accent-text); }
  .empty h2 { font: 600 var(--fs-lg) var(--font-display); margin: 0 0 var(--sp-2); }
  .empty p { color: var(--text-secondary); max-width: 46ch; margin: 0 auto; }

  /* ---------------------------------------------------------- error state
     Same geometry as .empty (a failure is a designed state, not a thrown
     message) but the icon reads as a warning, not neutral, and a retry
     button is always present (docs/plans/archive/mvp11.md F3). */
  .error-state { text-align: center; padding: var(--sp-8) var(--sp-5); }
  .error-state .art { font-size: 0; margin-bottom: var(--sp-4); color: var(--down-text); }
  .error-state h2 { font: 600 var(--fs-lg) var(--font-display); margin: 0 0 var(--sp-2); }
  .error-state p { color: var(--text-secondary); max-width: 46ch; margin: 0 auto var(--sp-4); }

  /* --------------------------------------------------------------- drawer */
  #drawer { position: fixed; inset: 0; background: var(--scrim);
            display: none; z-index: 20; }
  /* An analysis panel, not a sidebar. At 680px on a 1500px screen the drawer
     showed one column of cards over a half-empty page while the listing's own
     numbers wrapped; it now takes the width it needs and lays its cards out
     two-up when there is room (a container query, so it responds to the PANEL,
     not the viewport). */
  #drawer .panel { position: absolute; right: 0; top: 0; bottom: 0;
            width: min(1120px, 96vw); container-type: inline-size;
            background: var(--surface-0);
            border-left: 1px solid var(--border); box-shadow: var(--shadow-3);
            overflow-y: auto; padding: var(--sp-5);
            border-radius: var(--r-lg) 0 0 var(--r-lg);
            animation: slidein var(--t-slow); }
  /* Enters VERTICALLY (mvp44). It used to start at `translateX(24px)` on a
     panel anchored `right: 0`, which puts the panel's right 24px outside the
     viewport for the length of the animation — the rightmost tile of
     `#d-tiles` is genuinely off-screen and unreachable while it plays, and if
     the animation ever stalls (a throttled tab, a background window) it stays
     that way. Measured: with the transform removed the panel fits its viewport
     exactly at 1200/1400/1600px, so the overhang was the whole defect.

     An entrance animation must never place content outside the viewport. A
     vertical offset reads the same and cannot clip, because the panel is not
     anchored on that axis. */
  @keyframes slidein { from { transform: translateY(8px); opacity: 0; }
                       to { transform: none; opacity: 1; } }
  #drawer .close { float: right; background: transparent; border: 0;
            color: var(--text-muted); padding: 6px; border-radius: var(--r-sm); }
  #drawer .close:hover { background: var(--surface-inset);
            color: var(--text-primary); }
  /* Panel title block: the listing's own image beside its name, so a seller
     arriving from a finding chip can SEE which of forty similarly-titled
     portraits this is. Stacks below ~520px of panel width. */
  .d-hero { display: grid; grid-template-columns: 96px 1fr; gap: var(--sp-4);
            align-items: start; margin: 0 0 var(--sp-4);
            padding-right: var(--sp-6); }
  .d-hero-img { width: 96px; height: 96px; border-radius: var(--r-md);
            overflow: hidden; background: var(--surface-inset);
            border: 1px solid var(--border); display: grid; place-items: center; }
  .d-hero-img img { width: 100%; height: 100%; object-fit: cover; display: block; }
  .d-hero-img .thumb-fallback { color: var(--text-muted); }
  .d-hero-text { min-width: 0; }
  #d-title { font: 600 var(--fs-lg)/var(--lh-tight) var(--font-display);
             margin: 0; }
  .d-hero-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
            margin-top: 6px; font-size: var(--fs-sm); color: var(--text-muted); }
  .d-hero-meta a { color: var(--accent-text); }
  .d-hero-sep { color: var(--border-strong); }
  @container (min-width: 880px) {
    .d-hero { grid-template-columns: 120px 1fr; }
    .d-hero-img { width: 120px; height: 120px; }
  }
  @container (max-width: 520px) {
    .d-hero { grid-template-columns: 1fr; }
  }
  /* Two-up card grid inside the panel, above ~880px of PANEL width. Cards
     that own a wide chart or a table opt back out with .drawer-wide. */
  .drawer-grid { display: grid; gap: var(--sp-4); }
  /* Grid items default to min-width:auto, so a wide table or legend widens
     the TRACK instead of scrolling inside its own card -- the panel itself
     ended up 641px wide inside a 374px phone. Same fix `.movers-grid` got in
     mvp12; the drawer grid never had it. */
  .drawer-grid > * { min-width: 0; }
  @container (min-width: 880px) {
    .drawer-grid { grid-template-columns: 1fr 1fr; align-items: stretch; }
    .drawer-grid > .drawer-wide { grid-column: 1 / -1; }
    .drawer-grid > .card { display: flex; flex-direction: column; }
    .drawer-grid > .card > .chart { margin-top: auto; margin-bottom: auto; }
  }

  /* ------------------------------------------------- change timeline (mvp22) */
  /* A vertical list of edits, each a tap target: the timeline IS the
     navigation for the before/after panel below it, so rows are <button>s
     (keyboard-reachable, 44px tall on a phone) rather than styled divs. */
  /* ------------------------------------------ chart room on a phone (mvp44)
     Measured, not guessed. On a 390px viewport `.content` (16px x2) and
     `.card` (24px x2) spend 80px — 20% of the screen — before a chart starts,
     and the y-axis gutter takes another 55. The obvious suspect was the
     gutter, and it was innocent: `axisPadLeft` has measured the label since
     mvp12. The padding was the bigger half all along.

     Cards keep their padding for TEXT and hold their own edge; the chart alone
     is allowed to bleed into it, which buys ~24px without making the card
     look unpadded. Below 400px the page gutter narrows too. */
  @media (max-width: 639px) {
    .card > .chart, .card > .chart-scroll > .chart {
      margin-left: calc(var(--sp-5) * -1);
      margin-right: calc(var(--sp-5) * -1);
      width: calc(100% + var(--sp-5) * 2);
      max-width: none;
    }
  }
  @media (max-width: 399px) {
    .content { padding-left: var(--sp-3); padding-right: var(--sp-3); }
  }

  /* --------------------------------------------- row magnitude bar (mvp44)
     Borrowed from Plausible: a tinted bar behind the row gives a table the
     shape of its own distribution without spending a chart on it. Painted as a
     hard-stopped gradient rather than a pseudo-element so it sits under the
     text with no stacking-context fights, and so `hidden` rows (the pager)
     cost nothing.

     `--bar` is set per row by `dataList`. With no `--bar` the gradient is a
     no-op, so an un-barred table is unaffected. */
  tr[data-bar] { --bar: 0%;
               background-image: linear-gradient(to right,
                 var(--row-bar) 0 var(--bar), transparent var(--bar)); }
  tr[data-bar="2"] { background-image: linear-gradient(to right,
                 var(--row-bar-2) 0 var(--bar), transparent var(--bar)); }
  /* Hover still has to win, or the bar makes rows look permanently selected. */
  tbody tr[data-bar]:hover { background-image: none; }
  @media (prefers-reduced-motion: no-preference) {
    tr[data-bar] { transition: background-image var(--t-base); }
  }

  /* ------------------------------------------- change timeline (mvp43)
     A dotted rail with one entry per edit — the property-history shape, for
     the same reason: a sequence of events on ONE thing reads as a story, and a
     flat filtered list of everything does not. Shared by the Changes page and
     the listing drawer (`js/changetimeline.js`). */
  .chg-card { padding-bottom: var(--sp-3); }
  .chg-card + .chg-card { margin-top: var(--sp-4); }
  .chg-card-head { display: flex; align-items: baseline; gap: var(--sp-3);
        width: 100%; background: none; border: 0; padding: 0 0 var(--sp-3);
        text-align: left; cursor: pointer; color: var(--text-primary); }
  .chg-card-title { flex: 1; min-width: 0; font: 600 var(--fs-md)/1.3 var(--font-display);
        overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .chg-card-count { flex: none; font-size: var(--fs-xs); color: var(--text-muted); }

  .chg-tl { list-style: none; margin: 0; padding: 0; }
  .chg-tl-row { display: grid; grid-template-columns: 16px 1fr; gap: var(--sp-3); }
  /* The rail is one continuous line drawn by the cell, with the dot on top.
     Drawing it per-item with a border would leave a gap at every row edge. */
  .chg-tl-rail { position: relative; }
  .chg-tl-rail::before { content: ""; position: absolute; left: 7px; top: 0;
        bottom: 0; width: 1px; background: var(--border); }
  .chg-tl-row:first-child .chg-tl-rail::before { top: 14px; }
  .chg-tl-row:last-child .chg-tl-rail::before { bottom: auto; height: 14px; }
  .chg-tl-dot { position: absolute; left: 3px; top: 11px; width: 9px; height: 9px;
        border-radius: 50%; background: var(--surface-1);
        border: 2px solid var(--chart-line); }
  .chg-tl-body { display: block; width: 100%; text-align: left; background: none;
        border: 0; border-radius: var(--r-sm); cursor: pointer; color: inherit;
        font: inherit; padding: var(--sp-2) var(--sp-2) var(--sp-3); }
  .chg-tl-body:hover { background: var(--surface-inset); }
  .chg-tl-body.is-active { background: var(--surface-inset); }
  .chg-tl-top { display: flex; align-items: center; gap: var(--sp-2);
                flex-wrap: wrap; }
  .chg-tl-date { font-size: var(--fs-sm); color: var(--text-muted); }
  .chg-tl-what { display: flex; align-items: center; gap: 6px;
                 margin-top: 4px; font-size: var(--fs-sm); }
  .chg-tl-what svg { flex: none; color: var(--text-muted); }
  .chg-tl-diff { display: block; margin-top: 2px; font-size: var(--fs-sm);
                 color: var(--text-secondary); }
  .chg-from { color: var(--text-muted); }
  .chg-arrow { margin: 0 4px; color: var(--text-muted); }

  .chg-timeline { display: flex; flex-direction: column; gap: 2px; }
  .chg-item { display: grid; grid-template-columns: 92px 1fr auto;
              align-items: center; gap: var(--sp-3);
              width: 100%; min-height: 44px; padding: var(--sp-2) var(--sp-2);
              background: none; border: 0; border-left: 2px solid var(--border);
              border-radius: var(--r-sm); text-align: left; cursor: pointer;
              color: var(--text-primary); font: inherit; }
  .chg-item:hover { background: var(--surface-inset); }
  .chg-item.is-active { border-left-color: var(--chart-line);
                        background: var(--surface-inset); }
  .chg-when { font-size: var(--fs-sm); color: var(--text-muted);
              font-variant-numeric: tabular-nums; }
  .chg-what { font-size: var(--fs-sm); min-width: 0; overflow: hidden;
              text-overflow: ellipsis; white-space: nowrap; }
  .chg-verdict { justify-self: end; }
  .chg-detail { margin-top: var(--sp-4); padding-top: var(--sp-4);
                border-top: 1px solid var(--border); }
  .chg-detail-head { display: flex; flex-wrap: wrap; align-items: center;
                     gap: var(--sp-3); margin-bottom: var(--sp-3); }
  .chg-before-after { display: grid; gap: var(--sp-4); }
  .chg-metric h3 { font-size: var(--fs-sm); text-transform: uppercase;
                   letter-spacing: .06em; color: var(--text-muted);
                   margin: 0 0 var(--sp-2); }
  @media (min-width: 768px) {
    .chg-before-after { grid-template-columns: 1fr 1fr; }
    .chg-before-after > * { min-width: 0; }
  }
  @media (max-width: 520px) {
    /* Date over the summary rather than a 92px column squeezing both. */
    .chg-item { grid-template-columns: 1fr auto; }
    .chg-when { grid-column: 1 / -1; }
  }

  /* ----------------------------------------------------------------- chat */
  .chat-log { max-height: 300px; overflow-y: auto; margin-bottom: var(--sp-2); }
  .msg { padding: 9px 13px; border-radius: var(--r-md); margin: 6px 0;
         font-size: var(--fs-sm); white-space: pre-wrap; }
  .msg.user { background: var(--accent-tint); color: var(--text-primary);
              margin-left: 15%; }
  .msg.bot { background: var(--surface-1); border: 1px solid var(--border);
             margin-right: 10%; }
  .msg .tools { font-size: var(--fs-xs); color: var(--text-muted); margin-top: 6px; }
  /* Assistant answers are rendered Markdown (ask.js::renderMarkdown), so the
     bubble carries real block elements and must NOT also keep pre-wrap —
     that would add the source newlines back on top of the <p>/<ul> spacing. */
  .msg.md { white-space: normal; }
  .msg.md p { margin: 0 0 var(--sp-2); }
  .msg.md ul, .msg.md ol { margin: 0 0 var(--sp-2); padding-left: 1.25em; }
  .msg.md li { margin: 2px 0; }
  .msg.md > :last-child { margin-bottom: 0; }
  .msg.md strong { font-weight: 600; color: var(--text-primary); }
  .msg.md code { font-family: var(--font-mono);
                 font-size: 0.92em; background: var(--surface-2);
                 border-radius: var(--r-sm); padding: 0 4px; }
  .chat-row { display: flex; gap: var(--sp-2); align-items: flex-end; }
  .chat-row input, .chat-row textarea { flex: 1; }
  .chat-row textarea { resize: none; min-height: 64px; max-height: 160px;
                       line-height: 1.45; overflow-y: auto; font: inherit; }

  /* --------------------------------------------------------------- create */
  /* Create is two sections shown one at a time beside a Settings-style
     section list (mvp119); the old two-column `.create-grid` is gone. */
  .dropzone { border: 1.5px dashed var(--border-strong); border-radius: var(--r-md);
              padding: var(--sp-5); text-align: center; cursor: pointer;
              color: var(--text-secondary); background: var(--surface-1);
              transition: border-color var(--t-fast), background var(--t-fast); }
  .dropzone:hover, .dropzone.drag { border-color: var(--accent);
              background: var(--accent-tint); color: var(--accent-text); }
  .dropzone .hint { font-size: var(--fs-xs); color: var(--text-muted);
              margin-top: 4px; }
  .dz-file { display: flex; align-items: center; gap: var(--sp-3);
             border: 1px solid var(--border); border-radius: var(--r-md);
             padding: var(--sp-2) var(--sp-3); background: var(--surface-1); }
  .dz-file img { width: 56px; height: 56px; object-fit: cover;
                 border-radius: var(--r-sm); border: 1px solid var(--border); }
  .dz-file .fname { flex: 1; min-width: 0; overflow: hidden;
                 text-overflow: ellipsis; white-space: nowrap;
                 font-size: var(--fs-sm); }
  /* Mockup scenes (mvp119): the shop's own list. A row, not a pill — the
     seller wrote the prompt, so it is shown, and each row carries its own
     edit/delete. Ticked rows take the accent tint, as the old pills did. */
  /* Three numbered stages (upload, scenes, generate). Each after the first
     opens with room and a hairline, so the card reads as steps rather than
     one long form. */
  .create-step { font-size: var(--fs-base); margin: var(--sp-5) 0 var(--sp-3); }
  .create-step ~ .create-step { margin-top: var(--sp-6); padding-top: var(--sp-5);
                 border-top: 1px solid var(--border); }
  .scene-list { display: flex; flex-direction: column; gap: var(--sp-2); }
  .scene-row { display: flex; align-items: flex-start; gap: var(--sp-2);
               border: 1px solid var(--border); border-radius: var(--r-md);
               padding: var(--sp-2) var(--sp-3); background: var(--surface-1);
               transition: border-color var(--t-fast), background var(--t-fast); }
  .scene-row.sel { background: var(--accent-tint); border-color: var(--accent); }
  .scene-pick { display: flex; gap: var(--sp-3); align-items: flex-start;
                flex: 1; min-width: 0; cursor: pointer; padding: 4px 0; }
  .scene-pick input { margin-top: 3px; width: 18px; height: 18px; flex: none;
                accent-color: var(--accent); }
  .scene-pick b { display: block; font-size: var(--fs-sm); }
  .scene-pick small { display: block; color: var(--text-muted);
                font-size: var(--fs-xs); margin-top: 2px; overflow-wrap: anywhere; }
  .scene-acts { display: flex; gap: 2px; flex: none; }
  .scene-acts button { padding: 6px 10px; font-size: var(--fs-xs); }
  .scene-tools { display: flex; flex-wrap: wrap; gap: var(--sp-2);
                 align-items: center; margin-top: var(--sp-2); }
  .scene-form { display: flex; flex-direction: column; gap: 6px;
                margin-top: var(--sp-3); padding: var(--sp-3);
                border: 1px solid var(--border); border-radius: var(--r-md);
                background: var(--surface-inset); }
  .scene-form[hidden] { display: none; }   /* display:flex above would beat [hidden] */
  .scene-form label { font-size: var(--fs-xs); color: var(--text-secondary); }
  .scene-form input, .scene-form textarea { width: 100%; }
  .scene-form-foot { display: flex; gap: var(--sp-2); align-items: center;
                flex-wrap: wrap; justify-content: flex-end; }
  .scene-form-foot .note { margin-right: auto; }
  @media (max-width: 639px) {
    .scene-row { flex-wrap: wrap; }
    .scene-pick { flex-basis: 100%; }
    .scene-acts { margin-left: calc(18px + var(--sp-3)); margin-top: -4px; }
  }
  .mockups { display: grid;
             grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
             gap: var(--sp-3); margin-top: var(--sp-4); }
  .mockups figure { margin: 0; }
  .mockups img { width: 100%; border-radius: var(--r-sm);
             border: 1px solid var(--border); display: block; }
  .mockups figcaption { font-size: var(--fs-xs); color: var(--text-muted);
             margin-top: 5px; }
  .copy-out { background: var(--surface-inset); border-radius: var(--r-md);
              padding: var(--sp-4); margin-top: var(--sp-3);
              font-size: var(--fs-sm); }
  .copy-out b { font-size: var(--fs-base); }
  .tagchip { display: inline-block; background: var(--surface-1);
             border: 1px solid var(--border); border-radius: var(--r-full);
             padding: 2px 10px; margin: 2px; font-size: var(--fs-xs); }
  .warn { color: var(--down-text); font-size: var(--fs-sm); }
  /* Callouts carry a severity, not one alarm colour. Every callout used to be
     --down-tint red, so a GA data-quality caveat and "you are losing money on
     4 listings" shouted with the same voice. --danger is the default so an
     unmarked callout still reads as it did; --info/--warn are opt-in.
     Colour is never the only signal: each variant ships a leading glyph
     (js/icons.js) from the same 16px set as the rest of the chrome. */
  .callout { display: flex; align-items: flex-start; gap: var(--sp-2);
             background: var(--down-tint); color: var(--down-text);
             border-radius: var(--r-md); padding: var(--sp-3) var(--sp-4);
             font-size: var(--fs-sm); margin: 0 0 var(--sp-4); }
  .callout svg { flex: none; margin-top: 2px; }
  .callout--danger { background: var(--down-tint); color: var(--down-text); }
  .callout--warn { background: var(--surface-inset); color: var(--text-secondary);
             box-shadow: inset 3px 0 0 var(--cat-4); }
  .callout--info { background: var(--accent-tint); color: var(--accent-text); }
  button.callout-link { width: 100%; text-align: left; border: 0;
             cursor: pointer; font-family: inherit; font-size: var(--fs-sm); }
  button.callout-link:hover { filter: brightness(1.08); }
  tr.row-current { background: var(--accent-tint); }
  tr.row-warn { background: var(--down-tint); }
  tr.row-warn:hover { background: var(--down-tint); }
  .note { font-size: var(--fs-xs); color: var(--text-muted); }

  /* Data-quality checklist: the pass/fail/not-applicable mark is a column of
     its own so the statements line up, and a failed check is legible as a
     failure rather than differing from a pass by one glyph. */
  .check-list { list-style: none; margin: 0; padding: 0;
                display: flex; flex-direction: column; gap: 6px; }
  .check-list li { display: flex; gap: var(--sp-2); align-items: baseline; }
  .check-list .up { color: var(--up-text); font-weight: 600; }
  .plan-lead { font-weight: 600; color: var(--text-secondary); }
  .check-list .down { color: var(--down-text); font-weight: 600; }
  /* Two lines per check (mvp55): the claim, then what it means. The claim
     used to be dropped and only the explanation drawn, which is how a seller
     came to read a threshold constant instead of a sentence. */
  .check-list .check-label { display: block; font-weight: 600;
                             color: var(--text-primary); }
  .check-list .muted .check-label { color: var(--text-muted); font-weight: 500; }
  .check-list .check-detail { display: block; color: var(--text-muted); }
  .status-pill { display: inline-flex; align-items: center; gap: 5px;
             font-size: var(--fs-xs); color: var(--text-muted); }

  /* A link that reads as a button (the digest opens in a new tab, so it has
     to be a real <a>, not a button with a JS navigation). */
  .btn-link { display: inline-flex; align-items: center; min-height: 38px;
              padding: 0 var(--sp-4); border-radius: var(--r-md);
              background: var(--accent); color: var(--on-accent);
              text-decoration: none; font-size: var(--fs-sm); font-weight: 600; }
  .btn-link:hover { background: var(--accent-hover); }
  .btn-link:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }

  /* The shop-level fee line under Money's per-listing table (mvp15). */
  .shop-fee-line { display: flex; gap: var(--sp-4); align-items: flex-start;
              justify-content: space-between; flex-wrap: wrap;
              border-top: 1px solid var(--border); padding-top: var(--sp-3);
              margin-top: var(--sp-3); font-size: var(--fs-sm); }
  .shop-fee-line .num { font-variant-numeric: tabular-nums; color: var(--text-muted);
              white-space: nowrap; }

  /* ------------------------------------------------------------ plan card */
  .plan-grid { display: grid; gap: var(--sp-3);
               grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); }
  .plan-tier { border: 1px solid var(--border); border-radius: var(--r-md);
               padding: var(--sp-4); background: var(--surface-1);
               display: flex; flex-direction: column; gap: var(--sp-2); }
  .plan-tier.plan-current { border-color: var(--accent);
               background: var(--accent-tint); }
  .plan-tier-head { display: flex; align-items: center;
               justify-content: space-between; gap: var(--sp-2); }
  .plan-tier .check-list { margin: var(--sp-1) 0 var(--sp-2); }
  .plan-tier button { margin-top: auto; }

  /* -------------------------------------------------------------- actions */
  .actions-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-4);
                  align-items: start; }
  @media (max-width: 1023px) { .actions-grid { grid-template-columns: 1fr; } }
  .chips-row { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 8px; }
  .chip-meta { font-size: var(--fs-xs); border: 1px solid var(--border);
               border-radius: var(--r-full); padding: 1px 9px;
               color: var(--text-muted); background: var(--surface-inset); }
  /* Inline in a table cell (Money's thin-order / estimated-COGS markers) —
     .chip-meta is otherwise only used inside the .chips-row flex parent. */
  .datalist .chip-meta { margin-left: 6px; vertical-align: middle;
               white-space: nowrap; }
  /* Drillable listing-name chip (mvp13 F19): a real <button>, so it has to
     shed the UA button styling and keep .chip-meta's look, plus a 44px touch
     target and a hover/focus affordance saying it goes somewhere. */
  button.chip-link { font: inherit; font-size: var(--fs-xs); cursor: pointer;
               position: relative; color: var(--accent-text);
               max-width: 100%; overflow: hidden; text-overflow: ellipsis;
               white-space: nowrap;
               transition: background var(--t-fast), border-color var(--t-fast); }
  button.chip-link::after { content: ""; position: absolute; inset: -8px; }
  button.chip-link:hover { background: var(--accent-tint);
               border-color: var(--accent); }
  button.chip-link:focus-visible { outline: 2px solid var(--focus-ring);
               outline-offset: 2px; }
  /* Modelled-vs-measured marker (mvp13 F21). Deliberately quiet: it qualifies
     a number, it doesn't compete with it. Dotted underline = "there's a
     tooltip here", the same signal the est./no COGS chips carry. */
  .chip-modelled { border-style: dashed; cursor: help; }
  code.key { font: var(--fs-xs) var(--font-mono); background: var(--surface-inset);
             border-radius: 4px; padding: 1px 5px; }

  /* ---------------------------------------------------------- LowNGate */
  .lown { display: flex; align-items: center; gap: 6px; color: var(--text-muted);
          font-size: var(--fs-sm); padding: var(--sp-3); background: var(--surface-inset);
          border-radius: var(--r-md); }
  .lown-ic { flex: none; opacity: .8; }

  /* -------------------------------------------------- revenue-bridge waterfall */
  .wf-total { fill: var(--text-muted); opacity: .55; }
  .wf-up { fill: var(--chart-up); }
  .wf-down { fill: var(--chart-down); }

  /* ------------------------------------------------ money-stat framing tint */
  .tile .pill-delta:not(.pos):not(.neg) { background: var(--accent-tint); color: var(--accent-text); }

  /* -------------------------------------------------------------- Today */
  .briefing { font: 400 var(--fs-lg)/1.45 var(--font-display);
              color: var(--text-primary); margin: 0 0 var(--sp-4); max-width: 62ch; }
  /* The answer to the subtitle's promise, directly under the headline
     (mvp82 S15). Same "Do this:" vocabulary as a finding card, so a seller
     meets the phrase once and it means the same thing everywhere. */
  .briefing-do { display: flex; flex-wrap: wrap; gap: var(--sp-2);
                 align-items: baseline; font-size: var(--fs-sm);
                 color: var(--text-primary); margin: 0 0 var(--sp-4); }
  .briefing-do .lead { color: var(--accent-text); font-weight: 600; flex: none; }
  .briefing-do a { color: var(--accent-text); white-space: nowrap; }

  /* ------------------------------------------------------------ sm tiles
     ONE adaptive grid at every width, in every place tiles appear (Today,
     Money, Diagnosis, Customers, and the four blocks inside the listing
     drawer -- they all share this class, so there is nothing per-screen to
     keep in sync).

     This replaces the sm snap-scroll row the spec offered as one of two
     options ("a horizontal snap-scroll row (or 2-col grid)"). At `flex: 0 0
     76%` the second tile was always clipped and the row carried a horizontal
     scrollbar, so the KPIs a seller opens the app for were hidden behind a
     swipe. Two columns fit them all; the type steps down so nothing
     truncates. */
  @media (max-width: 639px) {
    .tiles { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .tiles > .tile, .tiles > .lown { min-width: 0; }
    /* An odd tile out fills the row rather than sitting beside a hole --
       same rule .movers-grid uses for a lone trailing card. */
    .tiles > :last-child:nth-child(odd) { grid-column: 1 / -1; }
    .tile { padding: var(--sp-3); }
    .tile .value { font-size: var(--fs-lg); }
  }
  /* Below ~360px two columns of a currency figure genuinely don't fit. */
  @media (max-width: 359px) {
    .tiles { grid-template-columns: 1fr; }
  }

  /* Funnel's per-listing sparkline: once the datalist row is a stacked card
     (thumb/listing/sessions/orders/trend/verdict) a 120px SVG trend line
     adds width without adding a reading, so it's dropped rather than
     forcing horizontal scroll. */
  @media (max-width: 1023px) {
    .funnel-spark-col { display: none; }
  }

  /* --------------------------------------- DataList: narrow container
     table -> stacked card-list (spec §2: a data table renders as cards
     rather than a horizontal-scroll table).

     Container query, not a media query (mvp12). `md` previously had no
     rules at all, so a tablet got a real 8-column table in a ~690px column;
     but the same squeeze recurs at a 1024px viewport once the lg sidebar
     claims 232px, and no single viewport threshold covers both. 780px is
     just above what Money's per-listing table needs (~825px measured), so
     the table renders as a table exactly when it fits and as cards when it
     doesn't -- at any viewport, with or without the sidebar. */
  /* Between the card-collapse threshold and the width the widest table
     actually needs, drop the columns that carry least. Listings is the
     widest in the app (11 columns, ~1280px natural) and would otherwise
     scroll inside its wrapper on any container between 780 and 1280 --
     e.g. a 1024px viewport. Category is near-constant for most shops,
     Avg order is Revenue/Orders, and Trend restates Momentum as a picture,
     so all three are recoverable from what remains on screen. */
  @container (max-width: 1180px) {
    table.datalist .col-optional { display: none; }
    /* The title is the one elastic column left; letting it give back some
       width is what closes the last gap. It already ellipsises and carries
       the full text in a title attribute. */
    table.datalist .cell-title { max-width: 220px; }
  }

  @container (max-width: 780px) {
    /* Cards have room for everything again — each cell is its own labelled
       row, so nothing competes for horizontal space. */
    table.datalist .col-optional { display: revert; }
    table.datalist thead { display: none; }
    table.datalist, table.datalist tbody { display: block; width: 100%; }
    table.datalist tr { display: flex; flex-wrap: wrap; align-items: flex-start;
                 gap: 2px var(--sp-3); border: 1px solid var(--border);
                 border-radius: var(--r-md); margin-bottom: var(--sp-3);
                 padding: var(--sp-3); background: var(--surface-1); }
    table.datalist tr:nth-child(even) { background: var(--surface-1); } /* undo zebra striping */
    table.datalist td { border: 0; padding: 2px 0; }
    table.datalist td[data-label=""] { flex: 0 0 auto; }
    /* min-width:0 so a long listing title truncates (it already has
       overflow/ellipsis) rather than widening the cell past the card, and
       flex-wrap so a long label ("LTV per customer") and its value stack
       onto two lines instead of overflowing side by side. */
    table.datalist td:not([data-label=""]) { flex: 1 1 45%; min-width: 0; display: flex;
                 flex-wrap: wrap; justify-content: space-between; gap: 0 var(--sp-2);
                 text-align: right; overflow-wrap: anywhere; }
    table.datalist td:not([data-label=""])::before { content: attr(data-label);
                 color: var(--text-muted); font-size: var(--fs-xs);
                 text-transform: uppercase; letter-spacing: .06em; text-align: left;
                 flex: 0 1 auto; }
    /* The desktop 340px cap is wider than a phone card; let it fill instead. */
    table.datalist .cell-title { max-width: 100%; }
  }

  /* The support sentence under a finding or action (mvp98 A07). Quieter than
     the claim above it and never a badge: it is what the claim rests on, not
     a second claim. */
  .evidence-note { margin-top: var(--sp-2); font-size: var(--fs-sm);
                   line-height: 1.5; }

  /* ------------------------------------------------ Etsy search card (mvp101)
     Ported from `docs/design/mvp101-search-card.css`, Astra's handoff. One
     claim per labelled row, because the chip bag it replaced made a price
     band, a competition count and a read date read as three interchangeable
     badges. The history chart's marks are shapes, not colours — a hollow
     triangle for "scanned, outside the top N" is legible to a reader who
     cannot distinguish the stroke (invariant #11's legend rule). */
  /* On the app's tokens, not the handoff's pixels (fixed 2026-09-27). The
     port kept the design file's standalone sizes — 16px body, 24px heading,
     24px padding, 16px radius — so in the drawer this card read a size
     larger than every card beside it. And the drawer already wraps it in a
     `.card`, so its own border and padding drew a card inside a card. The
     content block is frameless now; the heading matches `.card > h2`, the
     subtitle matches `.sub`. Standalone (no `.card` around it) it still
     reads, because the frame was the only thing it lost. */
  .search-card { color:var(--text-primary); font:var(--fs-base)/var(--lh-base) var(--font-sans); }
  .search-card * { box-sizing:border-box; }
  .search-card h2,.search-card h3,.search-card p { margin:0; }
  .search-card h2 { font:600 var(--fs-md)/var(--lh-tight) var(--font-display); }
  .search-card h3 { font:600 var(--fs-base)/var(--lh-tight) var(--font-display); }
  .search-card .search-subtitle { font-size:var(--fs-sm); color:var(--text-muted); margin-top:2px; }
  .search-snapshot { display:flex; align-items:flex-start; gap:var(--sp-3); padding:var(--sp-4) 0; }
  .search-position { flex:none; border-radius:var(--r-sm); padding:var(--sp-1) var(--sp-3); background:var(--accent-tint); color:var(--accent-text); font-weight:600; font-variant-numeric:tabular-nums; }
  .search-position.is-outside { color:var(--text-secondary); background:var(--surface-2); }
  .search-phrase { min-width:0; overflow-wrap:anywhere; }
  .search-phrase span { display:block; color:var(--text-muted); font-size:var(--fs-xs); margin-bottom:2px; }
  .search-phrase strong { font-weight:600; font-size:var(--fs-base); }
  .search-facts { margin:0; }
  .search-facts > div { display:grid; grid-template-columns:minmax(120px,.7fr) minmax(0,1.3fr); gap:var(--sp-4); padding:var(--sp-2) 0; border-top:1px solid var(--border); }
  .search-facts dt { color:var(--text-secondary); font-size:var(--fs-sm); }
  .search-facts dd { margin:0; font-weight:600; font-variant-numeric:tabular-nums; overflow-wrap:anywhere; }
  .search-facts dd small { display:block; font-size:var(--fs-sm); font-weight:400; color:var(--text-muted); margin-top:2px; }
  .search-card-section { border-top:1px solid var(--border); padding-top:var(--sp-4); margin-top:var(--sp-4); }
  .search-tags { list-style:none; display:flex; flex-wrap:wrap; gap:var(--sp-2); padding:0; margin:var(--sp-2) 0 0; }
  .search-tags li { border:1px solid var(--border); background:var(--surface-2); padding:2px var(--sp-2); border-radius:var(--r-sm); font-size:var(--fs-sm); }
  .search-card .search-context-note { margin-top:var(--sp-1); color:var(--text-muted); font-size:var(--fs-sm); }
  .search-history-chart { margin:var(--sp-3) 0 var(--sp-2); width:100%; height:auto; display:block; color:var(--accent-text); overflow:visible; }
  .search-history-chart text { fill:var(--text-secondary); font:12px var(--font-sans); }
  .search-history-chart .guide { stroke:var(--border); }
  .search-history-chart .trend { fill:none; stroke:currentColor; stroke-width:2.5; }
  .search-history-chart .point { fill:var(--surface-1); stroke:currentColor; stroke-width:2; }
  .search-history-chart .gap { stroke:var(--text-secondary); stroke-dasharray:3 4; opacity:.6; }
  .search-history-chart .outside { fill:var(--surface-1); stroke:var(--text-secondary); stroke-width:1.5; }
  .search-history-key { display:flex; flex-wrap:wrap; gap:var(--sp-2) var(--sp-4); font-size:var(--fs-xs); color:var(--text-muted); }
  .search-card details { margin-top:var(--sp-3); }
  .search-card summary { color:var(--accent-text); cursor:pointer; font-size:var(--fs-sm); }
  .search-card summary:focus-visible { outline:2px solid var(--focus-ring); outline-offset:2px; }
  .search-card table { width:100%; border-collapse:collapse; font-size:var(--fs-sm); margin-top:var(--sp-2); }
  .search-card th,.search-card td { text-align:left; border-bottom:1px solid var(--border); padding:var(--sp-2) var(--sp-1); }
  .search-card .search-policy { border-top:1px solid var(--border); padding-top:var(--sp-3); margin-top:var(--sp-4); color:var(--text-muted); font-size:var(--fs-xs); }
  .search-card .search-empty { padding-top:var(--sp-3); color:var(--text-secondary); }
  @media(max-width:480px) {
   .search-snapshot { flex-direction:column; gap:var(--sp-2); }
   .search-facts > div { grid-template-columns:1fr; gap:2px; }
   .search-history-key { flex-direction:column; }
   .search-history-chart text { font-size:24px; }
  }

  /* -------------------------------------------------- Listing Vitals (mvp117)
     Traffic lights, scorecard by area, its own drawer. Every tone is a word the
     server decided (good/fair/poor/none); colour always sits beside a number
     or a word, never alone. */
  .tone-good { --tone-text: var(--up-text); --tone-tint: var(--up-tint); }
  .tone-fair { --tone-text: var(--caution-text); --tone-tint: var(--caution-tint); }
  .tone-poor { --tone-text: var(--down-text); --tone-tint: var(--down-tint); }
  .tone-none { --tone-text: var(--text-muted); --tone-tint: var(--surface-inset); }
  .vpill { display: inline-flex; align-items: center; white-space: nowrap;
           font-size: var(--fs-xs); font-weight: 600; padding: 2px 10px;
           border-radius: var(--r-full); background: var(--tone-tint); color: var(--tone-text); }
  td .vpill { font-variant-numeric: tabular-nums; }
  .vdot { display: inline-block; flex: none; width: 10px; height: 10px;
          border-radius: var(--r-full); background: var(--tone-text);
          vertical-align: middle; }
  .vdot.tone-none { background: transparent; border: 2px solid var(--border-strong); }
  .vring .vring-track { fill: none; stroke: var(--surface-inset); stroke-width: 6; }
  .vring .vring-fill { fill: none; stroke: var(--tone-text); stroke-width: 6;
                       stroke-linecap: round; }
  .vring .vring-num { fill: var(--text-primary); font: 700 18px var(--font-display); }
  .vhead { display: flex; gap: var(--sp-4); align-items: center; margin: 0 0 var(--sp-3); }
  .vhead-text { min-width: 0; }
  .vhead-row { display: flex; gap: var(--sp-2); align-items: center; flex-wrap: wrap; }
  .vhead-line { margin: var(--sp-2) 0 var(--sp-1); font-size: var(--fs-md);
                color: var(--text-primary); line-height: 1.35; }
  .vhead-text p.body-sm { margin: 0; }
  .vitals-sub { font-size: var(--fs-xs); font-weight: 600; margin: var(--sp-5) 0 var(--sp-2);
                color: var(--text-muted); text-transform: uppercase; letter-spacing: .06em; }
  .vfirst { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--sp-2); }
  .vfirst-item { display: flex; gap: var(--sp-3); padding: var(--sp-3);
                 background: var(--surface-1); border: 1px solid var(--border);
                 border-left: 4px solid var(--tone-text); border-radius: var(--r-md); }
  .vfirst-n { flex: none; width: 24px; height: 24px; border-radius: var(--r-full);
              display: grid; place-items: center; font-size: var(--fs-xs); font-weight: 700;
              background: var(--tone-tint); color: var(--tone-text); }
  .vcheck-body { min-width: 0; flex: 1; }
  .vparts { list-style: none; margin: var(--sp-1) 0 0; padding: 0; color: var(--text-muted); }
  .vparts li { padding: 2px 0 2px var(--sp-3); position: relative; }
  .vparts li::before { content: ""; position: absolute; left: 0; top: .7em; width: 4px; height: 4px;
                       border-radius: var(--r-full); background: var(--border-strong); }
  .vparts-src { white-space: nowrap; }
  .vcheck-title { font-weight: 600; color: var(--text-primary); }
  .vcheck-meta { margin-top: 2px; color: var(--text-muted); }
  .vareas { border: 1px solid var(--border); border-radius: var(--r-md);
            background: var(--surface-1); overflow: hidden; }
  .varea + .varea { border-top: 1px solid var(--border); }
  .varea summary { display: grid; grid-template-columns: minmax(0, 1.3fr) minmax(0, 1fr) auto;
                   gap: var(--sp-3); align-items: center; padding: var(--sp-3);
                   cursor: pointer; list-style: none; }
  .varea summary::-webkit-details-marker { display: none; }
  .varea summary:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: -2px; }
  .varea[open] summary { background: var(--surface-inset); }
  .varea-label { display: flex; gap: var(--sp-2); align-items: center; font-weight: 600;
                 color: var(--text-primary); min-width: 0; }
  .varea-bar { height: 8px; border-radius: var(--r-full); background: var(--surface-inset);
               overflow: hidden; }
  .varea[open] .varea-bar { background: var(--surface-1); }
  .varea-fill { display: block; height: 100%; border-radius: var(--r-full);
                background: var(--tone-text); }
  .varea-count { color: var(--text-muted); white-space: nowrap; text-align: right; }
  .vchecks { list-style: none; margin: 0; padding: 0 var(--sp-3); }
  .varea .vchecks { padding-bottom: var(--sp-2); }
  .vcheck { display: flex; gap: var(--sp-3); align-items: baseline;
            padding: var(--sp-2) 0; border-bottom: 1px solid var(--border); }
  .vcheck:last-child { border-bottom: 0; }
  .vchecks-tight { padding: 0; margin: var(--sp-2) 0; }
  .vchecks-tight .vcheck { border: 0; padding: 3px 0; }
  .vlegend { display: flex; gap: var(--sp-2); align-items: center; flex-wrap: wrap;
             margin: var(--sp-2) 0 0; }
  .vlegend .vdot { margin-left: var(--sp-2); }
  .vlegend .vdot:first-child { margin-left: 0; }
  .vnone { display: flex; gap: var(--sp-2); align-items: center; }
  .vlink { background: none; border: 0; padding: 0; font: inherit; cursor: pointer;
           color: var(--accent-text); text-decoration: underline; }
  .vopen { display: inline-block; margin-top: var(--sp-2); font-weight: 600;
           color: var(--accent-text); }
  .vitals-breakdown { margin: var(--sp-4) 0 0; }
  .vitals-breakdown summary { cursor: pointer; font-size: var(--fs-sm); color: var(--accent-text); }
  .vitals-breakdown summary:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
  .vitals-points { list-style: none; margin: var(--sp-2) 0; padding: 0; font-size: var(--fs-sm); }
  .vitals-points li { display: flex; justify-content: space-between; gap: var(--sp-3);
                      padding: 2px 0; border-bottom: 1px solid var(--border); }
  .vitals-points .num { font-variant-numeric: tabular-nums; white-space: nowrap; }
  .vitals-readonly { margin-top: var(--sp-4); }
  .vitals-draft-body { margin-top: var(--sp-2); padding: var(--sp-3);
                       background: var(--surface-inset); border-radius: var(--r-sm); }
  .vitals-draft-body pre { white-space: pre-wrap; margin: var(--sp-2) 0; }
  /* Uniform rows (mvp118 review): the thumb never shrinks — as a flex item it
     was squeezed into a portrait strip beside a long title — the column has a
     floor, and a title longer than two lines ends in an ellipsis (the full
     title is its tooltip and the audit's heading). */
  /* The four shop tiles (mvp118 review): a label that wraps to two lines
     pushed its number down, so the numbers sat on two baselines. The value
     sits at the foot of every tile instead. */
  #vitals-tiles .tile { display: flex; flex-direction: column; }
  #vitals-tiles .tile .value { margin-top: auto; padding-top: 4px; }
  .vtile-score { color: var(--tone-text); }
  .vitals-row-title { display: flex; align-items: center; gap: var(--sp-2); min-width: 0; }
  .vitals-row-title .thumb { flex: none; }
  @media (min-width: 1024px) { .vitals-row-title { min-width: 220px; } }
  .vitals-row-title span, .vitals-clamp { min-width: 0; overflow: hidden; display: -webkit-box;
                           -webkit-box-orient: vertical; -webkit-line-clamp: 2;
                           line-clamp: 2; overflow-wrap: anywhere; }
  /* The Listing Insights summary: diagnosis left, what to do right, when the
     drawer is wide enough (a container query on the panel, like the grid). */
  .vsum-sub { margin-top: 0; }
  @container (min-width: 760px) {
    .vitals-summary { display: grid; grid-template-columns: minmax(0, 3fr) minmax(0, 2fr);
                      gap: var(--sp-5); align-items: start; }
    .vitals-summary .vhead { margin: 0; }
  }
  /* Listing Vitals' own drawer: the listing drawer's shell, narrower — a
     checklist read top to bottom, not an analysis grid. */
  #vdrawer { position: fixed; inset: 0; background: var(--scrim); display: none; z-index: 21; }
  #vdrawer .panel { position: absolute; right: 0; top: 0; bottom: 0;
            width: min(760px, 100vw); background: var(--surface-0);
            border-left: 1px solid var(--border); box-shadow: var(--shadow-3);
            overflow-y: auto; padding: var(--sp-5);
            border-radius: var(--r-lg) 0 0 var(--r-lg); animation: slidein var(--t-slow); }
  #vdrawer .close { float: right; background: transparent; border: 0;
            color: var(--text-muted); padding: 6px; border-radius: var(--r-sm); }
  #vdrawer .close:hover { background: var(--surface-inset); color: var(--text-primary); }
  @media (max-width: 480px) {
    #vdrawer .panel { padding: var(--sp-4); border-radius: 0; }
    .varea summary { grid-template-columns: minmax(0, 1fr) auto; }
    .varea-bar { grid-column: 1 / -1; grid-row: 2; }
    .vhead { align-items: flex-start; }
  }

  /* --------------------------------------------------------- FunnelVerdict */
  .badge { display: inline-flex; align-items: center; font-size: var(--fs-xs);
           font-weight: 600; padding: 2px 10px; border-radius: var(--r-full); }
  .badge-healthy { background: var(--up-tint); color: var(--up-text); }
  .badge-watch { background: var(--accent-tint); color: var(--accent-text); }
  .badge-act { background: var(--down-tint); color: var(--down-text); }
  /* MarketPosition (mvp96). One line in a table cell, always: "Not in top
     100" wrapping to two lines inside a red pill reads as an error state
     rather than as the measurement it is. */
  td .badge { white-space: nowrap; }
  .market-pos .chips-row { flex-wrap: wrap; }
  .verdict { padding: var(--sp-3) 0; }
  .verdict-head { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; }
  .verdict-sentence { font-size: var(--fs-base); color: var(--text-primary); }
  .verdict-detail { margin-top: var(--sp-3); }
  .verdict-detail summary { cursor: pointer; font-size: var(--fs-sm);
                            color: var(--text-muted); }
  .quad2x2 { display: grid; grid-template-columns: 1fr 1fr; gap: 2px;
             margin: var(--sp-3) 0; max-width: 420px; }
  .quad-cell { padding: var(--sp-3); background: var(--surface-inset);
               font-size: var(--fs-xs); color: var(--text-muted);
               border-radius: var(--r-sm); }
  .quad-active { background: var(--accent-tint); color: var(--accent-text);
                 font-weight: 600; }

  /* Wilson interval bar (mvp8) -- reuses the .mix-bar-track geometry rather
     than a new chart type. Scaled to [0, max(hi,ref) x 1.3] by the caller
     (components.js::ciBar), never 0-100%, or a 4% conversion is invisible. */
  .ci-bar-track { position: relative; height: 16px; border-radius: var(--r-sm);
                  background: var(--surface-inset); margin: var(--sp-2) 0; overflow: visible; }
  .ci-bar-range { position: absolute; top: 0; height: 100%;
                  background: var(--chart-line); opacity: .55; border-radius: var(--r-sm); }
  .ci-bar-ref { position: absolute; top: -2px; width: 2px; height: 20px;
                background: var(--text-primary); }

  /* ------------------------------------------------------------ funnelBars */
  .funnel-bar { fill: var(--chart-line); }
  .funnel-label { font-size: 13px; fill: var(--text-primary); dominant-baseline: middle; }
  .funnel-drop { font-size: 12px; fill: var(--down-text); }

  /* ------------------------------------------------------------- hbarChart */
  .hbar-track { fill: var(--surface-inset); }
  .hbar-fill { opacity: .92; }
  .hbar-label { font-size: 12.5px; fill: var(--text-primary); dominant-baseline: middle; }
  .hbar-value { font-size: 12.5px; fill: var(--text-secondary);
                font-variant-numeric: tabular-nums; dominant-baseline: middle; }

  /* -------------------------------------------------------------- donutChart */
  /* Sizes are USER UNITS in a 240-unit viewBox that renders at most 280px
     (1.17x), so 21u lands at ~25px on screen — the type scale's --fs-xl.
     Using the token directly here printed ~31px, because the ring scales and
     the token doesn't know it. */
  .donut-center-value { font: 600 21px/1 var(--font-display);
                         fill: var(--text-primary); text-anchor: middle;
                         dominant-baseline: middle; }
  .donut-center-label { font-size: 9.5px; fill: var(--text-muted);
                         text-anchor: middle; dominant-baseline: middle;
                         text-transform: uppercase; letter-spacing: .06em; }
  .donut-slice { stroke: var(--surface-1); stroke-width: 2; }

  /* --------------------------------------------------------- stackedBarChart /
     stackedAreaChart -- 2px surface gap between adjacent segments (dataviz
     skill mark spec) via the stroke below, not a margin, since segments are
     drawn as one continuous rect run. */
  .stack-seg { stroke: var(--surface-1); stroke-width: 1; }
  .stack-area { opacity: .82; }

  /* -------------------------------------------------------------- comboChart */
  /* Slim rounded bars, a curved line and quiet dashed gridlines (mvp108):
     the chart is read for its shape, so the ink goes to the data. */
  .combo-bar { fill: var(--chart-line); }
  .combo-line { stroke: var(--chart-line-2); stroke-width: 2.5; fill: none;
                stroke-linejoin: round; stroke-linecap: round; }
  .combo-dot { fill: var(--chart-line-2); stroke: var(--surface-1); stroke-width: 2; }
  .chart.combo .grid-line { stroke-dasharray: 3 4; opacity: .8; }

  /* ------------------------------------------------------------ scatterChart */
  .scatter-dot { opacity: .82; stroke: var(--surface-1); stroke-width: 1; }
  .scatter-quad-line { stroke: var(--chart-grid); stroke-width: 1;
                        stroke-dasharray: 3 3; }
  .scatter-quad-label { font-size: var(--fs-xs); fill: var(--text-muted);
                         text-transform: uppercase; letter-spacing: .04em; }
  /* Direct labels on the largest bubbles — without them the scatter says
     nothing until you hover, which on a phone never happens (mvp12). */
  .scatter-dot-label { font-size: 12px; fill: var(--text-secondary);
                        paint-order: stroke; stroke: var(--surface-1);
                        stroke-width: 3px; stroke-linejoin: round; }
  .scatter-gap { fill: var(--down-tint); opacity: .5; } /* GA data-gap shading */

  /* Change markers (mvp22): where the seller edited a listing, drawn on the
     Explore sessions chart. Muted on purpose — it annotates the series, it
     doesn't compete with it. Existing tokens only, so both themes follow. */
  .chg-flag { stroke: var(--text-muted); stroke-width: 1; stroke-dasharray: 2 4;
              opacity: .65; }
  .chg-flag-head { fill: var(--chart-line-2); opacity: .9; }

  /* ------------------------------------------------------- KPI accent rail */
  .tile.tile-rail { border-left: 3px solid var(--rail-color, var(--chart-line));
                     padding-left: calc(var(--sp-4) - 3px); }
  .tile-ic { color: var(--rail-color, var(--chart-line)); margin-bottom: var(--sp-2); }

  /* --------------------------------------------------------- DataList pager */
  .list-pager { display: flex; align-items: center; justify-content: space-between;
                gap: var(--sp-3); margin-top: var(--sp-2); font-size: var(--fs-sm);
                color: var(--text-muted); }
  .list-pager .pager-btns { display: flex; gap: 4px; }
  /* 32px visual, 44px hit area via the ::after overlay (mvp12). These are
     the primary paging control on every table in the app and were 28x28 --
     well under the 44px touch-target floor. */
  /* **`padding: 0` is load-bearing.** The generic `button` rule sets
     `padding: 9px 16px` for text buttons, and with `* { box-sizing: border-box }`
     a 32px-wide button minus 32px of padding and 2px of border leaves a
     NEGATIVE content box — the 14px chevron is squeezed to zero and the control
     renders as an empty square. It shipped that way, was reported, and was
     waved off twice before anyone measured it (mvp61). `.icon-btn` and
     `.theme-toggle` set it for the same reason: any fixed-size button in this
     stylesheet must.
     `--surface-inset` and `--border-strong` because the pager sits ON a card,
     and at `--surface-1` it was the same colour as the thing behind it. */
  .list-pager button { position: relative; display: inline-flex;
                        align-items: center; justify-content: center;
                        padding: 0; border: 1px solid var(--border-strong);
                        background: var(--surface-inset);
                        color: var(--text-primary); border-radius: var(--r-sm);
                        width: 32px; height: 32px; cursor: pointer; line-height: 1; }
  .list-pager button::after { content: ""; position: absolute; inset: -6px; }
  .list-pager button:hover:not(:disabled) { background: var(--surface-inset); }
  .list-pager button:disabled { opacity: .35; cursor: default; }
  table.datalist th.sortable { cursor: pointer; user-select: none; }
  table.datalist th.sortable:hover { color: var(--text-primary); }
  table.datalist th.sortable .sort-ic { opacity: .5; margin-left: 3px;
             display: inline-flex; vertical-align: middle; }
  table.datalist th.sortable.sort-active .sort-ic { opacity: 1; }

/* Sync progress (mvp39). Real progress off the pipeline's own step list, not
   an elapsed-time animation — see settings.js::renderProgress for why that
   distinction is load-bearing. Below the button row, because at 380px a bar
   beside two buttons has nowhere to go. */
.sync-progress-host { margin-top: var(--sp-2); }
.sync-progress { display: flex; flex-direction: column; gap: var(--sp-1); max-width: 420px; }
.sync-progress-track {
  height: 6px;
  border-radius: 3px;
  background: var(--surface-2);
  overflow: hidden;
}
.sync-progress-fill {
  height: 100%;
  background: var(--accent);
  border-radius: 3px;
  /* Eases only when the width CHANGES, i.e. when a step actually completed —
     never a continuous crawl that implies progress nothing is making. */
  transition: width 240ms ease-out;
}
@media (prefers-reduced-motion: reduce) {
  .sync-progress-fill { transition: none; }
}
