/* =========================================================================
   Ludex — desktop web. Shared design system.
   1 tokens · 2 base · 3 shell · 4 controls · 5 card tiles · 6 panels
   7 tables · 8 forms · 9 chart · 10 dialogs · 11 empty · 12 responsive
   ========================================================================= */

/* ---- 1. Tokens ---------------------------------------------------------
   Every value below is lifted from the Ludex .fig, not chosen. The navy
   ladder and the text ladder are the file's own; mint is its accent —
   #2AFDB5 sits on Tab-item, #22B080 on icons and buttons.
   ------------------------------------------------------------------------ */
:root {
  --bg:        #001129;   /* app canvas — 91% of the live product's pixels */
  --bg-deep:   #000B1C;   /* backdrop behind sheets and dialogs */
  --surface:   #001E39;   /* cards, panels, rail */
  --surface-2: #023050;   /* raised rows, inputs */
  --surface-3: #044266;   /* icon tiles, pressed / selected */
  --fg:        #E6E8EA;
  --bright:    #FFFFFF;
  --muted:     #9EB3C7;   /* body text */
  --faint:     #7A8FA5;   /* micro labels — 5.7:1 on the canvas, 5.1:1 on a card */
  --border:    #023050;
  --border-str:#044266;

  /* An empty checkbox is a solid block, not an outlined ring — but a block
     still has to be perceivable as a control, so this clears 3:1 against the
     surfaces it sits on (WCAG 1.4.11). --surface-3 only managed 1.6:1. */
  --pick-rest: #3D7096;

  /* the product's action colour is a light pill with dark text, not a
     saturated brand fill. Emphasis comes from brightness and weight;
     hue is reserved for status. */
  --accent:    #9EB3C7;
  --accent-2:  #044266;
  --on-accent: #001129;
  --ok:        #2A8142;   /* connected dot, non-text fills */
  --up:        #45B96A;   /* lifted from --ok so numbers clear 4.5:1 */
  --down:      #E06A6B;   /* lifted from the product's #D05051 for the same reason */
  --danger:    #D05051;   /* destructive pill, as shipped */
  --info:      #124C6E;   /* inactive / coming-soon pill */

  /* --up and --down were lifted to clear 4.5:1 against the CANVAS. Inside a
     pill or badge the text sits on a wash of its own hue, which lifts the
     background toward the text and eats the ratio: --down on --down-wash over
     a raised row measured 3.58:1. These two are the same hues carried further
     up, and they are only for text on its own wash. Sized against the WORST
     backdrop the wash actually lands on — --surface-3, which is a selected
     channel row, not the canvas. Measured; see the scorecard. */
  --up-ink:    #8CDDA5;
  --down-ink:  #F9AFB0;

  --acc-wash:  color-mix(in oklch, var(--accent) 16%, transparent);
  --up-wash:   color-mix(in oklch, var(--up) 20%, transparent);
  --down-wash: color-mix(in oklch, var(--down) 16%, transparent);
  --info-wash: color-mix(in oklch, var(--info) 22%, transparent);

  /* marketplace brand colours, taken from each brand's published palette.
     The glyph in the tile is a MONOGRAM PLACEHOLDER — production drops in the
     official SVG from each press kit. See brand-spec.md for the links. */
  --ch-ebay:        #FFFFFF;   /* eBay's mark is specified on white */
  --ch-ebay-ink:    #E53238;   /* eBay red */
  --ch-ebay-dot:    #0064D3;   /* eBay blue — the dot avoids reading as an error */
  --ch-walmart:     #0071CE;   /* Walmart True Blue */
  --ch-walmart-ink: #FFC220;   /* Walmart Spark Yellow */
  --ch-whatnot:     #FFF351;   /* Whatnot Gorse yellow */
  --ch-whatnot-ink: #0B0B0B;
  --ch-shopify:     #95BF47;   /* Shopify green */
  --ch-shopify-ink: #FFFFFF;

  /* category tints — 3px spine and 6px dot only, never a background.
     Kept low-chroma so they read as index tabs against the navy. */
  --c-basketball: #C87A4E;
  --c-baseball:   #6E97C9;
  --c-football:   #9BB86A;
  --c-soccer:     #B87FC0;
  --c-mtg:        #8489CE;
  --c-pokemon:    #D4B45C;

  --font-display: 'Patua One', Georgia, serif;
  --font-body:    'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
  --font-mono:    'IBM Plex Mono', ui-monospace, Menlo, monospace;

  --r-panel: 14px;   /* card radius as shipped */
  --r-ctl:   8px;    /* inputs stay rectangles — a pill-shaped text field is unreadable */
  --r-pill:  999px;  /* every button and chip in the live product is a full pill */
  --r-chip:  4px;    /* badges are labels, not controls */
  --r-card:  6px;

  --rail: 220px;
  --nav-h: 56px;
}

/* ---- 2. Base ----------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
  margin: 0;
  background: var(--bg);
  color: var(--fg);
  font: 400 13px/1.5 var(--font-body);
  font-variant-numeric: tabular-nums;
  -webkit-font-smoothing: antialiased;
  text-wrap: pretty;
}
h1, h2, h3, h4 { margin: 0; font-weight: 600; letter-spacing: -0.01em; }
p { margin: 0; }
a { color: inherit; text-decoration: none; }
button, input, select, textarea { font: inherit; color: inherit; }
button { cursor: pointer; background: none; border: 0; padding: 0; }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 3px; }
svg { display: block; }
[hidden] { display: none !important; }

.dsp { font-family: var(--font-display); font-weight: 400; letter-spacing: 0; }
.num { font-family: var(--font-mono); font-variant-numeric: tabular-nums; letter-spacing: -0.01em; }
.up { color: var(--up); } .down { color: var(--down); }
.mut { color: var(--muted); } .faint { color: var(--faint); }
.micro {
  font-family: var(--font-mono); font-size: 10px; font-weight: 500;
  letter-spacing: 0.08em; text-transform: uppercase; color: var(--faint);
}
.sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
/* --- icon stroke compensation -------------------------------------------
   Every glyph is drawn on a 24-unit grid, so one stroke-width renders at a
   different number of real pixels at every size: 2 units is 1.33px at 16px but
   0.92px at 11px — a hairline. Weight is therefore paired with size, here,
   and each pair is tuned to land near 1.35px so the set holds one optical
   weight from 11px to 26px.

   It has to live in CSS rather than on the sprite: <use> clones a symbol into
   a shadow tree that inherits from the <use> element's ancestors, so an
   inherited value reaches the glyph but page selectors cannot. Anything set
   directly on the symbol would outrank every rule below.

   svg:has(use) is the floor. Its specificity is (0,0,2), under every class
   below, so it catches any icon that lands outside a sized component without
   ever overriding one. */
svg:has(use) { stroke-width: 2; }
.i { width: 16px; height: 16px; flex: none; stroke-width: 2; }      /* 1.33px */
.i-sm { width: 13px; height: 13px; stroke-width: 2.5; }             /* 1.35px */
.i-lg { width: 20px; height: 20px; stroke-width: 1.6; }             /* 1.33px */
/* A 13px glyph next to 12.5px text reads as a smudge, so buttons override the
   inline size. Stated once here rather than edited into 29 call sites. */
.btn > svg { width: 15px; height: 15px; stroke-width: 2.15; }       /* 1.34px */
.btn-lg > svg { width: 17px; height: 17px; stroke-width: 1.9; }     /* 1.35px */
.iconbtn > svg { width: 16px; height: 16px; stroke-width: 2; }      /* 1.33px */
.row { display: flex; align-items: center; gap: 8px; }
.grow { flex: 1; min-width: 0; }
.push { margin-left: auto; }

/* ---- 3. App shell ------------------------------------------------------ */
.app { display: grid; grid-template-columns: var(--rail) minmax(0, 1fr); min-height: 100vh; }

.rail {
  position: sticky; top: 0; height: 100vh; overflow-y: auto;
  border-right: 1px solid var(--border);
  padding: 14px 12px; display: flex; flex-direction: column; gap: 4px;
  background: var(--bg);
}
/* lockup as shipped: white rounded tile holding the mark, then the wordmark
   in uppercase Patua */
.brand {
  display: flex; align-items: center; gap: 9px;
  padding: 2px 8px 16px; font-family: var(--font-display); font-size: 16px;
  letter-spacing: 0.03em; text-transform: uppercase;
}
.brand .tile {
  width: 28px; height: 28px; flex: none; border-radius: 8px;
  background: #FFFFFF; display: grid; place-items: center;
}
.brand .lx { width: 17px; height: 11.5px; flex: none; color: var(--bg); }
.rail-grp { margin-top: 14px; }
.rail-grp > .micro { display: block; padding: 0 8px 6px; }
.rail a, .rail button.navitem {
  display: flex; align-items: center; gap: 9px; width: 100%;
  padding: 7px 8px; border-radius: var(--r-ctl);
  color: var(--muted); font-size: 12.5px; text-align: left;
}
.rail a:hover, .rail button.navitem:hover { background: var(--surface-2); color: var(--fg); }
.rail a[aria-current="page"] { background: var(--surface-3); color: var(--fg); font-weight: 500; }
.rail .cnt { margin-left: auto; font-family: var(--font-mono); font-size: 11px; color: var(--faint); }

/* ---- tier ---------------------------------------------------------------
   Collection is the basic surface, Inventory the Pro one. The rail is what
   states the tier — there is no tier switch, because a product has none. */
.rail .locked { color: var(--faint); }
.rail .locked:hover { color: var(--muted); }
.rail .locked .lock { margin-left: auto; width: 12px; height: 12px; opacity: .8;
                      stroke-width: 2.7; }                          /* 1.35px */
.rail .locked .cnt { margin-left: 0; }
.tierbadge {
  display: inline-flex; align-items: center; height: 16px; padding: 0 5px;
  border-radius: 3px; border: 1px solid var(--border-str);
  font-family: var(--font-mono); font-size: 9px; font-weight: 500;
  letter-spacing: .09em; text-transform: uppercase; color: var(--muted);
}
.brand .tierbadge { margin-left: 2px; }
/* the Pro badge on the lockup already says which tier this is —
   tinting the group labels too would be the same signal twice */

/* ---- channels ------------------------------------------------------------
   eBay and Walmart are named in text; no third-party logos are recreated. */
.chan {
  display: flex; align-items: center; gap: 10px; width: 100%;
  padding: 11px 13px; border-radius: var(--r-ctl);
  background: var(--surface-2); border: 1px solid var(--border); text-align: left;
  transition: border-color .12s, background .12s;
}
.chan:hover:not(.is-locked) { border-color: var(--border-str); }
/* the tick carries the accent; the card stays neutral so two selected
   channels don't out-shout the primary action */
.chan[aria-pressed="true"] { border-color: var(--border-str); background: var(--surface-3); }
/* Picking which channels to publish to is a selection, not an endorsement —
   same neutral language as the table and the tiles. The green on this screen
   stays on the CONNECTED status pill, which is the thing that really means yes. */
.chan .tick {
  width: 18px; height: 18px; border-radius: 5px; flex: none; display: grid; place-items: center;
  background: var(--pick-rest); color: transparent; transition: background .1s;
}
.chan[aria-pressed="true"] .tick { background: var(--fg); color: var(--bg); }
/* Both are <span>, so both were inline: the channel name and its sub-line ran
   together on one line and the margin-top below did nothing. Every other
   name-over-detail pair in the product stacks; this one only looked like it
   did because the strings were short enough to get away with it. */
.chan .nm, .chan .sub { display: block; }
.chan .nm { font-size: 13px; font-weight: 500; }
/* --faint is documented at 5.7:1 on the canvas and 5.1:1 on a card. This row is
   --surface-2, one step brighter again, where it drops to 4.09:1 — under AA for
   an 11.5px line. --muted is 6.31:1 on the same surface and still reads as the
   quieter of the two lines. */
.chan .sub { font-size: 11.5px; color: var(--muted); margin-top: 1px; }
.chan.is-locked { opacity: .62; cursor: default; }
/* A locked channel can't be ticked, so its box is hollowed out rather than
   solid — there is no target here to aim at. */
.chan.is-locked .tick { background: none; border: 1px dashed var(--border-str); }
.chanlist { display: flex; flex-direction: column; gap: 8px; }

/* ---- marketplace marks --------------------------------------------------
   Monogram placeholders on the real brand colour. Swap for official SVGs. */
.blogo {
  flex: none; display: grid; place-items: center;
  width: 26px; height: 26px; border-radius: 7px;
  font-family: var(--font-display); font-size: 13px; line-height: 1;
}
.blogo-lg { width: 42px; height: 42px; border-radius: 11px; font-size: 20px; }
.blogo-sm { width: 18px; height: 18px; border-radius: 5px; font-size: 9.5px; }
.b-ebay    { background: var(--ch-ebay);    color: var(--ch-ebay-ink); }
.b-walmart { background: var(--ch-walmart); color: var(--ch-walmart-ink); }
.b-whatnot { background: var(--ch-whatnot); color: var(--ch-whatnot-ink); }
.b-shopify { background: var(--ch-shopify); color: var(--ch-shopify-ink); }
/* an unconnected channel is drained of its colour until it is switched on */
.is-off .blogo, .is-soon .blogo, .is-locked .blogo { filter: grayscale(1); opacity: .55; }

/* channel pills inside dense tables */
.pill-ch::before { content: ''; width: 7px; height: 7px; border-radius: 2px; flex: none; }
.pill-ebay::before    { background: var(--ch-ebay-dot); }
.pill-walmart::before { background: var(--ch-walmart-ink); }
.pill-whatnot::before { background: var(--ch-whatnot); }
.pill-shopify::before { background: var(--ch-shopify); }
.pill {
  display: inline-flex; align-items: center; gap: 4px; height: 19px; padding: 0 6px;
  border-radius: 3px; border: 1px solid var(--border);
  font-family: var(--font-mono); font-size: 9.5px; letter-spacing: .05em;
  text-transform: uppercase; color: var(--muted); white-space: nowrap;
}
.pill-live { border-color: color-mix(in oklch, var(--up) 45%, transparent); background: var(--up-wash); color: var(--up-ink); }
/* the negative half of the pair — .pill-live existed alone, so anything needing
   attention was being inline-styled at the call site */
.pill-down { border-color: color-mix(in oklch, var(--down) 45%, transparent); background: var(--down-wash); color: var(--down-ink); }
.pill-off  { color: var(--faint); }

/* ---- inventory table ----------------------------------------------------- */
.meter { height: 5px; border-radius: 3px; background: var(--surface-3); overflow: hidden; min-width: 54px; }
.meter > i { display: block; height: 100%; background: var(--up); }
.meter.is-thin > i { background: var(--down); }
.agebar { display: flex; gap: 3px; align-items: flex-end; height: 22px; }
.agebar span { width: 9px; border-radius: 2px; background: var(--surface-3); }
.agebar span.on { background: var(--accent-2); }
/* ---- integrations -------------------------------------------------------
   Card shape follows the shipped Integrations screen: icon tile, name, a
   status dot top-right, description, action bottom-right. What the shipped
   version lacks — which account, how many listings, when it last synced, and
   what disconnecting would cost — is added, because a connection you can't
   inspect isn't manageable. */
/* align-items:start rather than the grid's default stretch. Stretching made
   every card as tall as the tallest, and a connected card carries the least
   content — so pinning its footer to the bottom (which is right, and is what
   keeps the actions lined up inside a card) opened 57px of nothing above it.
   Sized to content that gap is 11px, and the two connected cards still match
   each other exactly because they hold the same things. */
/* Three things keep the four channel cards one shape.

   stretch, not start: the footer below already says margin-top:auto, but a card
   that only grows to its content has no slack for auto to consume, so the four
   ran to four different heights and the actions never lined up.

   272px, not 292px: at 1440 the content column is 1176px, and four 292s plus
   three gaps need 1210 — the fourth channel wrapped to a row of its own. 272
   fits four across at the width this is designed for.

   grid-auto-rows: 1fr so that when it does wrap on a narrower window, the lone
   card still matches the row above rather than shrinking to its content. */
.intgrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(272px, 1fr));
           grid-auto-rows: 1fr; gap: 14px; align-items: stretch; }
.intcard {
  position: relative; display: flex; flex-direction: column; gap: 11px;
  padding: 16px; background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-panel);
}
/* Blanket opacity on the coming-soon card multiplied with .btn:disabled's own
   0.45 and rendered the Connect button at 0.32 — illegible. Both unusable
   states now recess the surface instead; the grey status dot and the
   desaturated brand mark already carry the meaning. */
.intcard.is-off,
.intcard.is-soon { background: color-mix(in oklch, var(--surface) 60%, var(--bg)); }
/* Status sits in the header with the channel it describes. It used to be
   absolutely positioned in the top-right corner while the facts it summarised
   sat bottom-left, so reading a card meant a diagonal. */
.intcard .hd { display: flex; align-items: flex-start; gap: 11px; }
.intcard .hd .nm { flex: 1; min-width: 0; }
/* At 272px the subtitle wraps; balance it so it breaks between phrases rather
   than orphaning a word ("Auctions and / Buy It Now", not "…and Buy / It Now"). */
.intcard .hd .hint { text-wrap: balance; }
.intcard h3 { font-size: 14px; }
.intcard .desc { font-size: 12.5px; color: var(--muted); line-height: 1.55; }
/* The footer pins to the bottom so every card in the row lines its actions up,
   whatever sits above them. Growing .desc only worked on cards that have one —
   a connected card carries a metadata block instead, so its footer floated. */
.intcard .ft { display: flex; align-items: center; gap: 8px; margin-top: auto; padding-top: 3px; }

.cstat {
  display: inline-flex; align-items: center; gap: 6px; flex: none; margin-top: 3px;
  font-family: var(--font-mono); font-size: 9.5px; letter-spacing: .07em;
  text-transform: uppercase; color: var(--faint);
}
.cstat i { width: 8px; height: 8px; border-radius: 50%; background: var(--faint); }
.cstat.on  { color: var(--up); }
.cstat.on  i { background: var(--ok); box-shadow: 0 0 0 3px color-mix(in oklch, var(--ok) 25%, transparent); }
.cstat.off i { background: var(--border-str); }

/* What a connection carries.
   This was a tinted box sunk into the card holding three key/value rows —
   the heaviest object on the card wrapped around its least interesting
   content, and a second surface nested inside a surface, which nothing else
   in this product does. It also set the account id, the listing count and the
   sync clock at identical weight, so the one number a seller acts on was
   hidden among two they never read.

   Now: one figure, because a connection is judged on how much is live on it;
   the fee beside it because that is what differs between channels; and the
   account and clock as one quiet mono line underneath. */
.intfig { display: flex; align-items: baseline; gap: 9px; }
/* The facts under the hero number used to be one mono sentence — account, sync
   time and feed depth run together with middots — which wrapped at a different
   point on every card and made the three unrelated facts read as one. They are
   a labelled ledger now: label left, value right, one fact per line, so a
   channel with a fourth fact adds a row instead of lengthening a sentence. */
.intstat { display: grid; grid-template-columns: auto 1fr; gap: 6px 14px; margin: 0; font-size: 12px; }
.intstat dt { color: var(--faint); }
.intstat dd { margin: 0; text-align: right; font-family: var(--font-mono); font-size: 11.5px; color: var(--muted); }
.intstat dd.is-hot { color: var(--down); }
.intfig .fig {
  font-family: var(--font-display); font-weight: 400; font-size: 26px;
  line-height: 1; letter-spacing: 0; color: var(--bright);
}
.intfig .lab { font-size: 12.5px; color: var(--muted); }

/* Disconnect had been a ghost button the same size as Sync now. A destructive
   action should not sit at parity with a routine one, and --danger as a fill
   measures 4.2:1, so it stays a quiet text action that only reddens on intent. */
.lnk { font-size: 12px; color: var(--faint); }
.lnk:hover { color: var(--down); }

/* ---- tooltips ------------------------------------------------------------
   For terms, not for purpose. A tooltip is the right home for "what does this
   word mean" — it is the wrong home for anything you have to read to understand
   the screen, because a hover is not a place and cannot be found.

   The trigger is the term itself, marked with a dotted underline so it reads as
   askable before you point at it. It is a <button> rather than a <span>: hover
   alone would put the definition out of reach of a keyboard. */
.tip {
  font: inherit; color: inherit; padding: 0; cursor: help;
  border-bottom: 1px dotted var(--faint);
}
.tip:hover, .tip[aria-expanded="true"] { border-bottom-color: var(--fg); color: var(--fg); }
.tipbox {
  position: fixed; z-index: 90; max-width: 300px; padding: 9px 11px;
  background: var(--surface-3); border: 1px solid var(--border-str);
  border-radius: var(--r-ctl); box-shadow: 0 14px 34px -12px oklch(0% 0 0 / .8);
  font-size: 12px; line-height: 1.5; color: var(--fg);
  opacity: 0; transform: translateY(2px); pointer-events: none;
  transition: opacity .12s, transform .12s;
}
.tipbox[data-on] { opacity: 1; transform: none; }

/* what you lose by connecting nothing */
.blocked { display: flex; gap: 8px; align-items: flex-start; font-size: 12px; color: var(--muted); line-height: 1.5; }
.blocked svg { color: var(--faint); margin-top: 2px; flex: none; }

/* ---- plans ---------------------------------------------------------------
   Three columns, but only the tiers with a real price carry one. The current
   plan is marked rather than sold — this page is read by someone already
   paying as often as by someone deciding to. */
.plangrid { display: grid; grid-template-columns: repeat(3, minmax(0,1fr)); gap: 14px; align-items: stretch; }
.plan {
  display: flex; flex-direction: column; gap: 12px; padding: 18px;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-panel);
}
.plan.is-current { border-color: var(--border-str); background: var(--surface-2); }
.plan .nm { display: flex; align-items: center; gap: 8px; }
.plan .nm h3 { font-family: var(--font-display); font-weight: 400; font-size: 19px; letter-spacing: .01em; }
.plan .pitch { font-size: 12.5px; color: var(--muted); line-height: 1.5; min-height: 2.9em; }
.plan .price { display: flex; align-items: baseline; gap: 7px; padding: 12px 0 2px; }
.plan .price .amt { font-family: var(--font-display); font-weight: 400; font-size: 34px; letter-spacing: -0.01em; }
.plan .price .per { font-size: 12.5px; color: var(--faint); }
.plan .billnote { font-size: 11.5px; color: var(--faint); min-height: 1.4em; }
.plan .cta { margin-top: auto; padding-top: 4px; }
.plan ul { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: 7px; }
.plan li { display: flex; gap: 8px; align-items: flex-start; font-size: 12.5px; color: var(--muted); line-height: 1.45; }

/* billing period — a segmented pill, with the saving stated not implied */
/* ---- plan switch --------------------------------------------------------
   The same control type as the billing toggle below: one solid pill group,
   selected segment filled with the action colour. Sharing the type is the
   point — the two controls do the same kind of job (pick one of two plans,
   pick one of two billing periods) and now read as one component.

   Sized one step under the toolbar buttons (28px against 34px) so that in a
   topbar that already carries a filled primary button, the switch reads as
   state rather than as a second call to action. */
.tierswitch {
  display: inline-flex; align-items: center; gap: 3px; padding: 3px;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-pill);
}
.tierswitch button {
  height: 28px; padding: 0 13px; border-radius: var(--r-pill);
  font-size: 12.5px; font-weight: 500; color: var(--muted); white-space: nowrap;
}
.tierswitch button:hover { color: var(--fg); }
.tierswitch button[aria-pressed="true"] {
  background: var(--accent); color: var(--on-accent); font-weight: 600;
}
/* The topbar holds two clusters — actions, then account. This control opens
   the account cluster, and its selected segment is the same light pill as the
   primary button, so without a divider and real space the two run together as
   one bright shape. The rule is scoped to .topbar because the same component
   needs neither when it sits anywhere else. */
.topbar .tierswitch { position: relative; }
.topbar .tierswitch:not(:first-child) { margin-left: 7px; }
.topbar .tierswitch:not(:first-child)::before {
  content: ''; position: absolute; left: -11px; top: 50%; translate: 0 -50%;
  width: 1px; height: 20px; background: var(--border-str);
}

/* .viewtoggle shares this type — a segmented pill group is now how this product
   says "pick one of two views", whether that is a billing period, a plan, or
   live listings against the Walmart feed. */
.billtoggle, .viewtoggle { display: inline-flex; gap: 3px; padding: 3px; background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-pill); }
.billtoggle button, .viewtoggle button { height: 30px; padding: 0 15px; border-radius: var(--r-pill); font-size: 12.5px; font-weight: 500; color: var(--muted); }
.billtoggle button[aria-pressed="true"], .viewtoggle button[aria-pressed="true"] { background: var(--accent); color: var(--on-accent); font-weight: 600; }
.viewtoggle button { height: 28px; padding: 0 13px; font-size: 12px; display: inline-flex; align-items: center; gap: 6px; }
.viewtoggle .n {
  font-family: var(--font-mono); font-size: 10px; padding: 1px 5px; border-radius: 3px;
  background: var(--surface-2); border: 1px solid var(--border); color: var(--muted);
}
.viewtoggle button[aria-pressed="true"] .n { background: color-mix(in oklch, var(--on-accent) 14%, transparent); border-color: transparent; color: var(--on-accent); }
/* a queue with something wrong in it says so on the tab that leads to it */
.viewtoggle .n.is-hot { border-color: color-mix(in oklch, var(--down) 45%, transparent); background: var(--down-wash); color: var(--down); }
.billtoggle .save { font-family: var(--font-mono); font-size: 9.5px; letter-spacing: .06em; text-transform: uppercase; color: var(--up); margin-left: 6px; }
.billtoggle button[aria-pressed="true"] .save { color: var(--on-accent); opacity: .75; }

/* the position: Ludex charges for the tool, not for the sale */
.nofee {
  display: flex; align-items: flex-start; gap: 12px; padding: 15px 17px;
  border-radius: var(--r-panel); background: var(--up-wash);
  border: 1px solid color-mix(in oklch, var(--up) 35%, transparent);
}
.nofee .big { font-family: var(--font-display); font-weight: 400; font-size: 22px; color: var(--up); white-space: nowrap; }
.nofee p { font-size: 12.5px; color: var(--muted); line-height: 1.55; }

/* comparison — each row stated once, one cell per tier */
.cmp { width: 100%; border-collapse: collapse; font-size: 12.5px; }
.cmp th, .cmp td { padding: 9px 14px; border-bottom: 1px solid var(--border); text-align: left; }
.cmp thead th {
  position: sticky; top: var(--nav-h); z-index: 5; background: var(--bg);
  font-family: var(--font-mono); font-size: 10px; font-weight: 500;
  letter-spacing: .07em; text-transform: uppercase; color: var(--faint);
}
/* ---- comparison table: four backgrounds on one ladder -------------------
   These four states used to collide. A group heading and a hovered row were
   both --surface-2, so hovering a row made it read as a section heading; and
   .c.is-now (0,3,1) outranked the hover rule (0,2,2), so the current-plan
   column punched a hole in the hover band exactly where the eye was going.

   Now they step:  bg < surface (heading) < surface-2 (hover) < surface-3
   (hover on the current-plan column). Hover is the strongest because it is the
   transient one that has to be noticed; the heading can be quiet because its
   mono uppercase label is already doing the work. */
.cmp .grp td {
  background: var(--surface); font-family: var(--font-mono); font-size: 10px;
  letter-spacing: .07em; text-transform: uppercase; color: var(--muted); padding-block: 7px;
}
/* :not(.grp) both excludes the headings and lifts this to (0,3,3), which is
   what lets it beat .c.is-now instead of losing to it */
.cmp tbody tr:not(.grp):hover td { background: var(--surface-2); }
/* the current-plan column keeps its edge under the hover rather than being
   erased by it — it is the column you are comparing against */
.cmp tbody tr:not(.grp):hover td.is-now { background: var(--surface-3); }
.cmp .feat { color: var(--fg); width: 34%; }
.cmp .c { width: 22%; color: var(--muted); }
.cmp .c.is-now { background: color-mix(in oklch, var(--surface) 55%, var(--bg)); }
.cmp .cellval { display: inline-flex; align-items: center; gap: 7px; }
.cmp .dash { color: var(--faint); }

/* ---- settings rows ------------------------------------------------------- */
.setrow {
  display: flex; align-items: center; gap: 12px; padding: 13px 0;
  border-bottom: 1px solid var(--border);
}
.setrow:last-child { border-bottom: 0; }
.setrow .ic {
  width: 34px; height: 34px; flex: none; border-radius: 9px;
  background: var(--surface-2); display: grid; place-items: center; color: var(--muted);
}
.setrow .nm { font-size: 13px; font-weight: 500; }
.setrow .sub { font-size: 11.5px; color: var(--faint); margin-top: 2px; }
.copyfield {
  display: inline-flex; align-items: center; gap: 8px; padding: 4px 8px 4px 10px;
  background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--r-ctl);
  font-family: var(--font-mono); font-size: 12px;
}

/* ---- deadlines ----------------------------------------------------------
   A seller's screen is driven by clocks, so the clock has to be visible. */
.due { font-family: var(--font-mono); font-size: 11.5px; color: var(--muted); white-space: nowrap; }
.due.is-soon { color: var(--accent); }
.due.is-late { color: var(--down); }
.due .lbl { color: var(--faint); }

/* ---- selection ----------------------------------------------------------
   Selection is NOT an affirmative. Ticking three rows to reprice them says
   nothing good about those rows, so it must not borrow the green that this
   same table already spends on positive margin and live channels — three
   green rows in a margin table read as "these three are doing well".

   So the split is: hue carries status, contrast carries selection. Picked is
   the brightest neutral on the darkest neutral and no hue at all. The green
   stays where it means yes — the .yes disc, connected dots, margin, up ticks.

   Form follows the same rule at rest: a solid recessed block rather than an
   outlined ring, so an empty checkbox is a real target instead of a hairline. */
/* The picked-row background lives with the table hover rule in section 7, so
   the two sit on one ladder instead of colliding on the same token. */
.rowpick {
  width: 17px; height: 17px; border-radius: 5px; display: grid; place-items: center;
  background: var(--pick-rest); color: transparent;
  transition: background .1s;
}
tr:hover .rowpick { background: color-mix(in oklch, var(--pick-rest) 62%, var(--fg)); }
tr.is-picked .rowpick { background: var(--fg); color: var(--bg); }

.topbar {
  position: sticky; top: 0; z-index: 30; height: var(--nav-h);
  display: flex; align-items: center; gap: 14px; padding: 0 22px;
  background: color-mix(in oklch, var(--bg) 86%, transparent);
  backdrop-filter: blur(14px); border-bottom: 1px solid var(--border);
}
.search {
  flex: 1; max-width: 420px; display: flex; align-items: center; gap: 8px;
  height: 34px; padding: 0 11px; color: var(--faint);
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-ctl);
}
.search input { flex: 1; min-width: 0; background: none; border: 0; outline: 0; font-size: 12.5px; }
.search input::placeholder { color: var(--faint); }
.avatar {
  width: 28px; height: 28px; border-radius: 50%; flex: none;
  background: var(--surface-3); border: 1px solid var(--border);
  display: grid; place-items: center; font-size: 10.5px; font-weight: 600;
}
/* ---- account menu --------------------------------------------------------
   Profile and Sign Out are account, not product, so they live under the avatar
   rather than at the bottom of a rail that otherwise names places in the app.
   The avatar was a <div> — decorative — on all 17 pages; it is the button now. */
.acct { position: relative; }
button.avatar { cursor: pointer; position: relative; }
/* The avatar reads at 28px and should not grow — but 28px is a poor target, so
   the hit area is widened to 40px with a transparent slop ring instead. The
   menu is positioned off .acct, which is unaffected, so nothing moves. */
button.avatar::after { content: ""; position: absolute; inset: -6px; border-radius: 50%; }
button.avatar[aria-expanded="true"] { border-color: var(--border-str); background: var(--accent-2); }
.acctmenu {
  position: absolute; top: calc(100% + 8px); right: 0; z-index: 80; min-width: 226px;
  background: var(--surface); border: 1px solid var(--border-str);
  border-radius: var(--r-panel); box-shadow: 0 18px 44px -14px oklch(0% 0 0 / .8);
  padding: 5px; overflow: hidden;
}
.acctwho { padding: 10px 11px 11px; border-bottom: 1px solid var(--border); margin-bottom: 5px; }
.acctwho .nm { font-size: 13px; font-weight: 600; }
.acctwho .hint { margin-top: 2px; }
.acctwho .badge { margin-top: 8px; }
.acctmenu a, .acctmenu button {
  display: flex; align-items: center; gap: 9px; width: 100%;
  padding: 8px 11px; border-radius: var(--r-ctl);
  font-size: 12.5px; color: var(--muted); text-align: left;
}
.acctmenu a:hover, .acctmenu button:hover { background: var(--surface-2); color: var(--fg); }
.acctmenu [data-signout]:hover { color: var(--down); }

/* --main-x is named because the bulk bar has to subtract it to work out how
   far the viewport-fixed launcher overhangs the content column. */
:root { --main-x: 22px; }
.main { padding: 20px var(--main-x) 48px; }
.wrap { max-width: 1480px; margin: 0 auto; }

/* page head */
.phd { display: flex; align-items: flex-end; gap: 16px; flex-wrap: wrap; margin-bottom: 18px; }
/* page titles are set uppercase in the live product */
.phd h1 {
  font-family: var(--font-display); font-weight: 400; font-size: 25px;
  letter-spacing: 0.02em; text-transform: uppercase;
}
.phd .sub { font-size: 12.5px; color: var(--faint); margin-top: 3px; }

/* ---- 4. Controls -------------------------------------------------------
   Affirmative state has one colour. A checkbox that is ticked, a toggle that
   is on, a scope that is granted and a feature that is included all read
   green; excluded things get a faint cross, never a grey tick. The light
   pill stays the action colour and never doubles as a state. */

/* Every tick shares one size so the mark is framed by its container rather
   than filling it edge to edge — the glyph is already inset in its viewBox,
   and this leaves the fill visible around it. */
.tick svg, .rowpick svg, .pick svg { width: 11px; height: 11px; stroke-width: 2.9; } /* 1.33px */

/* ---- affirmative / excluded markers in prose lists ----------------------
   A hairline tick floating in space reads as an afterthought. These sit in a
   tinted disc so the mark has the same weight as the sentence beside it. */
.yes, .nope {
  width: 17px; height: 17px; flex: none; border-radius: 50%;
  display: grid; place-items: center; margin-top: 1px;
}
/* A shade heavier than the 1.35px baseline: this mark sits on a tinted disc
   rather than on the page, so it needs the extra weight to hold against it. */
.yes svg, .nope svg { width: 11px; height: 11px; stroke-width: 3.1; }  /* 1.42px */
.yes  { background: var(--up-wash);   color: var(--up-ink); }
.nope { background: color-mix(in oklch, var(--faint) 20%, transparent); color: var(--faint); }
.nope.is-warn { background: var(--down-wash); color: var(--down-ink); }
/* solid variant for a confirmed row, where the statement is the point */
.yes.is-solid { background: var(--up); color: var(--bg); }
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  height: 34px; padding: 0 15px; border-radius: var(--r-pill);
  background: var(--surface-2); border: 1px solid var(--border);
  font-size: 12.5px; font-weight: 500; white-space: nowrap;
  transition: background .12s, border-color .12s, color .12s;
}
.btn:hover { background: var(--surface-3); border-color: var(--border-str); }
/* A channel's integration button. It carries the mark, the name and the state,
   so it answers "is eBay connected?" without being clicked — the status sits
   behind a hairline so the button still reads as one control, not three. */
.chbtn { gap: 8px; padding-left: 8px; }
.chbtn .cstat {
  margin-top: 0; padding-left: 8px;
  /* --border is the same value as --surface-2, which is this button's own
     background — the divider has to step up a rung to exist at all. */
  border-left: 1px solid var(--border-str);
}
.chbtn:hover .cstat { border-left-color: color-mix(in oklch, var(--border-str) 60%, var(--fg)); }
.btn-primary { background: var(--accent); border-color: var(--accent); color: var(--on-accent); font-weight: 600; }
.btn-primary:hover { background: #B8C8D8; border-color: transparent; }
/* the product's destructive pill is #D05051 with white text, which measures
   4.2:1 — below AA. Not shipped here until the pair is resolved; --danger is
   kept for borders and status text, where it passes. */
.btn-ghost { background: none; border-color: transparent; color: var(--muted); }
.btn-ghost:hover { background: var(--surface-2); color: var(--fg); }
.btn-sm { height: 28px; padding: 0 10px; font-size: 12px; }
.btn-lg { height: 42px; font-size: 14px; }
.btn-block { width: 100%; }
.btn:disabled { opacity: .45; cursor: not-allowed; }
.iconbtn {
  width: 34px; height: 34px; border-radius: var(--r-ctl); display: grid; place-items: center;
  color: var(--muted); border: 1px solid transparent;
}
.iconbtn:hover { background: var(--surface-2); color: var(--fg); }
.iconbtn[aria-pressed="true"] { background: var(--surface-3); color: var(--fg); border-color: var(--border); }

.chiprow { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.chip {
  display: inline-flex; align-items: center; gap: 6px;
  height: 30px; padding: 0 12px; border-radius: var(--r-pill);
  background: var(--surface); border: 1px solid var(--border);
  color: var(--muted); font-size: 12.5px; font-weight: 500; white-space: nowrap;
}
.chip:hover { color: var(--fg); border-color: var(--border-str); }
.chip[aria-pressed="true"], .chip.is-on { background: var(--surface-3); border-color: var(--border-str); color: var(--fg); }
.chip .dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
.dot-basketball { color: var(--c-basketball); } .dot-baseball { color: var(--c-baseball); }
.dot-football  { color: var(--c-football); }   .dot-soccer   { color: var(--c-soccer); }
.dot-mtg       { color: var(--c-mtg); }        .dot-pokemon  { color: var(--c-pokemon); }
/* Other has no colour of its own — the card art uses the neutral rule for it too. */
.dot-other     { color: var(--border-str); }

.badge {
  display: inline-flex; align-items: center; gap: 5px; padding: 2px 7px; border-radius: var(--r-chip);
  font-family: var(--font-mono); font-size: 10px; font-weight: 500;
  letter-spacing: 0.06em; text-transform: uppercase;
  border: 1px solid var(--border); color: var(--muted); white-space: nowrap;
}
.badge-accent { border-color: color-mix(in oklch, var(--accent) 45%, transparent); background: var(--acc-wash); color: var(--accent); }
.badge-up   { border-color: color-mix(in oklch, var(--up) 45%, transparent);   background: var(--up-wash);   color: var(--up-ink); }
.badge-down { border-color: color-mix(in oklch, var(--down) 45%, transparent); background: var(--down-wash); color: var(--down-ink); }
.badge-info { border-color: var(--info); background: var(--info); color: var(--fg); }

.tabs { display: flex; gap: 2px; border-bottom: 1px solid var(--border); }
.tab {
  padding: 9px 13px; font-size: 12.5px; font-weight: 500; color: var(--muted);
  border-bottom: 2px solid transparent; margin-bottom: -1px; white-space: nowrap;
}
.tab:hover { color: var(--fg); }
.tab[aria-selected="true"] { color: var(--fg); border-bottom-color: var(--accent); }

/* ---- 5. Card tiles ----------------------------------------------------- */
.sheet {
  display: grid; gap: 14px;
  grid-template-columns: repeat(auto-fill, minmax(158px, 1fr));
}
.tile { display: flex; flex-direction: column; gap: 7px; text-align: left; position: relative; }

/* the 5:7 face — type, not photography */
.face {
  position: relative; aspect-ratio: 5 / 7; overflow: hidden;
  border-radius: var(--r-card); border: 1px solid var(--border);
  background: var(--surface-2);
  display: flex; flex-direction: column; justify-content: flex-end;
  padding: 10px; transition: border-color .14s, transform .14s;
}
.tile:hover .face { border-color: var(--border-str); transform: translateY(-2px); }
/* The face is --surface-2, where --faint measures 4.09:1 — under AA, same gap
   as .chan .sub and .tpl .sub. --muted is 6.31:1 there and still reads as the
   quiet line under the player's name, which is set 15px in the display face. */
.face .setline { font-family: var(--font-mono); font-size: 9.5px; letter-spacing: .06em; text-transform: uppercase; color: var(--muted); }
.face .who {
  font-family: var(--font-display); font-size: 15px; line-height: 1.1;
  margin-top: 4px; text-wrap: balance;
}
.face .no { position: absolute; top: 9px; right: 10px; font-family: var(--font-mono); font-size: 10px; color: var(--faint); }
.face .catbar { position: absolute; left: 0; top: 0; bottom: 0; width: 3px; }

/* foil sheen — ONLY on cards that really carry a parallel.
   Mint into periwinkle reads as holo without becoming a gradient wash. */
.face.is-foil::after {
  content: ''; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(115deg,
    transparent 28%,
    color-mix(in oklch, var(--bright) 16%, transparent) 44%,
    transparent 52%,
    color-mix(in oklch, var(--c-mtg) 22%, transparent) 66%,
    transparent 78%);
}
/* graded card = slab label across the top */
.face.is-slab { padding-top: 34px; }
.face.is-slab .slab {
  position: absolute; left: 0; right: 0; top: 0; height: 26px;
  display: flex; align-items: center; gap: 6px; padding: 0 9px;
  background: var(--surface-3); border-bottom: 1px solid var(--border);
  font-family: var(--font-mono); font-size: 10px; letter-spacing: .05em; text-transform: uppercase;
}
.face.is-slab .slab b { margin-left: auto; color: var(--fg); font-size: 11px; }
/* The card number is pinned to the top-right corner, which on a graded card is
   the same corner the grade occupies — #136 was printing on top of the 10. On a
   slab the number drops below the band instead of fighting it for the corner. */
.face.is-slab .no { top: 35px; }
/* The pick sits in this bar's left corner, so on a graded card it lands on top
   of the grading label. The label steps aside while the box is showing rather
   than being covered by it — otherwise selecting a slab hides its grade. */
.face.is-slab .slab { transition: padding-left .12s; }
.tile:hover .face.is-slab .slab,
.tile.is-picked .face.is-slab .slab { padding-left: 32px; }

.tile .meta { display: flex; align-items: baseline; gap: 8px; }
.tile .nm { font-size: 12.5px; font-weight: 500; line-height: 1.3; }
.tile .val { margin-left: auto; font-family: var(--font-mono); font-size: 12.5px; font-weight: 500; }
.tile .sub { font-size: 11px; color: var(--faint); }

/* selection */
/* Same selection language as the table. This one sits over card artwork, so
   the resting block is opaque rather than tinted — it has to hold against
   whatever the card happens to be. */
.tile .pick {
  position: absolute; top: 8px; left: 8px; z-index: 2;
  width: 20px; height: 20px; border-radius: 6px;
  background: color-mix(in oklch, var(--bg) 88%, transparent);
  display: grid; place-items: center;
  opacity: 0; transition: opacity .12s, background .1s;
}
.tile:hover .pick, .tile.is-picked .pick { opacity: 1; }
.tile.is-picked .pick { background: var(--fg); color: var(--bg); }
.tile.is-picked .face { border-color: var(--fg); }
.pick svg { opacity: 0; }
.tile.is-picked .pick svg { opacity: 1; }

/* List view is a real table now (see #sheet-list in collection.html), not the
   contact-sheet tiles turned on their side — a tile laid down sideways gave a
   thinner contact sheet, not a second way of reading the same 18 cards. The
   category cell carries the same coloured dot the filter chips use, so the
   filter and the column speak one vocabulary. */
.catcell { display: inline-flex; align-items: center; gap: 7px; white-space: nowrap; }
.catcell .dot { width: 7px; height: 7px; border-radius: 50%; flex: none; background: currentColor; }

/* sticky bulk bar */
.bulkbar {
  position: sticky; bottom: 16px; z-index: 20; margin-top: 16px;
  /* Inventory's bar carries four actions inside a column narrowed by the side
     panel, and ran 88px past its own right edge at 1265px. Wrapping keeps the
     controls inside the bar; the bar grows a row instead of leaking. */
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap; padding: 10px 14px;
  background: var(--surface-3); border: 1px solid var(--border-str); border-radius: var(--r-panel);
  box-shadow: 0 16px 40px -14px oklch(0% 0 0 / .75);
}

/* ---- 6. Panels --------------------------------------------------------- */
.panel { background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-panel); }
.panel-hd { display: flex; align-items: center; gap: 10px; padding: 12px 15px; border-bottom: 1px solid var(--border); }
.panel-hd h3 { font-size: 13px; }
.panel-bd { padding: 15px; }
.stack { display: flex; flex-direction: column; gap: 16px; }

/* stat strip — the one hero figure per screen */
.stats { display: flex; gap: 0; flex-wrap: wrap; }
.stats > div { padding: 13px 18px; border-right: 1px solid var(--border); flex: 1; min-width: 150px; }
.stats > div:last-child { border-right: 0; }
/* A tier variant leaves its unused twin in the DOM as [hidden], so :last-child
   stops naming the cell the eye actually sees last and the row ends on a
   divider drawn against nothing. Ask instead whether any later sibling is
   still visible. */
.stats > div:not(:has(~ div:not([hidden]))) { border-right: 0; }
.stats .v { font-family: var(--font-mono); font-size: 17px; font-weight: 600; margin-top: 3px; letter-spacing: -0.02em; }
.stats .v.hero { font-family: var(--font-display); font-weight: 400; font-size: 30px; color: var(--bright); letter-spacing: 0; }

.rowline { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; font-size: 12.5px; }
.rowline .k { color: var(--muted); }
.rowline .v { font-family: var(--font-mono); font-weight: 500; text-align: right; }

/* ---- 7. Tables --------------------------------------------------------- */
table.t { width: 100%; border-collapse: collapse; font-size: 12px; }
table.t th {
  text-align: left; padding: 8px 13px; font-family: var(--font-mono);
  font-size: 10px; font-weight: 500; letter-spacing: .07em; text-transform: uppercase;
  color: var(--faint); border-bottom: 1px solid var(--border); white-space: nowrap;
}
table.t td { padding: 9px 13px; border-bottom: 1px solid var(--border); vertical-align: middle; }
table.t tbody tr:last-child td { border-bottom: 0; }
/* Hover and selection step, because they mean different things: hover is the
   light transient one, selection is stronger because it persists, and a hovered
   selection is stronger still so it still answers the pointer.

   The hover step used to be `var(--surface)` — which IS the panel colour, so
   every table in the product had an invisible hover. Same class of mistake as
   drawing a --border divider on a --surface-2 control. The three steps are now
   measured against each other: 1.118 : 1.108 : 1.227, each perceptible, none
   loud enough to compete with the selection tick.

   All are scoped identically so the cascade is decided by order, not by an
   accident of specificity. */
table.t tbody tr:hover td { background: color-mix(in srgb, var(--surface-2) 55%, var(--surface)); }
table.t tbody tr.is-picked td { background: var(--surface-2); }
table.t tbody tr.is-picked:hover td { background: color-mix(in srgb, var(--fg) 8%, var(--surface-2)); }
/* --faint is 5.06:1 on a panel but only 4.09:1 on --surface-2, and lower again
   on a hovered selection — so every .hint sub-line in a selected row was under
   AA. The sub-line lifts with the row it is in. */
table.t tbody tr.is-picked .hint,
table.t tbody tr.is-picked .sku,
table.t tbody tr.is-picked .mut { color: var(--muted); }

/* A row that opens something says so before it is clicked. .is-openable is set
   per row by the shared layer, not by a blanket selector, because most of these
   tables render their rows from JS and a row with nothing to open must not
   claim a pointer. The checkbox keeps the default cursor: it selects, it does
   not navigate. */
table.t tbody tr.is-openable { cursor: pointer; }
table.t tbody tr .rowpick, table.t tbody tr .lname { cursor: pointer; }
table.t .n, table.t th.n { font-family: var(--font-mono); text-align: right; }

/* ---- 8. Forms ---------------------------------------------------------- */
.field { display: flex; flex-direction: column; gap: 6px; }
.field > label { font-family: var(--font-mono); font-size: 10px; font-weight: 500;
  letter-spacing: .07em; text-transform: uppercase; color: var(--faint);
  display: flex; justify-content: space-between; align-items: baseline; }
.inp {
  display: flex; align-items: center; gap: 8px; min-height: 36px; padding: 0 11px;
  background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--r-ctl);
}
.inp:focus-within { border-color: var(--border-str); }
.inp input, .inp select, .inp textarea {
  flex: 1; min-width: 0; background: none; border: 0; outline: 0; font-size: 13px; padding: 8px 0;
}
.inp textarea { resize: vertical; min-height: 62px; font-family: inherit; }
.inp .pre, .inp .unit { font-family: var(--font-mono); font-size: 12px; color: var(--faint); }
.inp.is-num input { font-family: var(--font-mono); }
/* dense two-column data form — replaces stacked mobile rows */
.datagrid { display: grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: 12px 16px; }
.hint { font-size: 11.5px; color: var(--faint); }
.err { font-size: 11.5px; color: var(--down); }

.sw { position: relative; width: 34px; height: 20px; border-radius: 10px; flex: none;
  background: var(--surface-3); border: 1px solid var(--border); transition: background .14s; }
.sw::after { content: ''; position: absolute; top: 2px; left: 2px; width: 14px; height: 14px;
  border-radius: 50%; background: var(--muted); transition: transform .14s, background .14s; }
.sw[aria-pressed="true"] { background: var(--up-wash); border-color: color-mix(in oklch, var(--up) 50%, transparent); }
.sw[aria-pressed="true"]::after { transform: translateX(14px); background: var(--up); }

.drop {
  display: flex; flex-direction: column; align-items: center; gap: 8px;
  padding: 34px 20px; text-align: center;
  border: 1.5px dashed var(--border-str); border-radius: var(--r-panel); background: var(--surface);
}
.drop.is-over { border-color: var(--accent); background: var(--acc-wash); }

/* ---- 9. Price chart ---------------------------------------------------- */
.chart-svg { width: 100%; height: 200px; display: block; }
.chart-hd { display: flex; align-items: flex-start; gap: 12px; padding: 14px 15px 4px; }

/* ---- 10. Dialogs ------------------------------------------------------- */
dialog {
  width: min(560px, calc(100vw - 32px)); padding: 0; color: var(--fg);
  background: var(--surface); border: 1px solid var(--border-str); border-radius: 12px;
  box-shadow: 0 28px 64px -18px oklch(0% 0 0 / .75);
}
dialog::backdrop { background: oklch(0% 0 0 / .62); backdrop-filter: blur(2px); }
.dlg-ft { display: flex; gap: 8px; justify-content: flex-end; padding: 13px 15px; border-top: 1px solid var(--border); }

.toast {
  position: fixed; right: 20px; bottom: 20px; z-index: 60;
  display: flex; align-items: flex-start; gap: 9px; max-width: 340px;
  padding: 11px 13px; border-radius: var(--r-panel);
  background: var(--surface-3); border: 1px solid var(--border-str);
  box-shadow: 0 14px 34px -12px oklch(0% 0 0 / .7);
  opacity: 0; transform: translateY(8px); transition: opacity .18s, transform .18s;
}
.toast.is-on { opacity: 1; transform: none; }

/* ---- Ludex Assistant — launcher + chat panel ----------------------------
   The assistant used to be a ghost button in the topbar, which put a
   conversation behind the same affordance as Export and Sync. It is now a
   persistent launcher in the bottom-right carrying the Ludex mark on the same
   white tile as the rail lockup, so the brand marks the one place in the
   product that talks back.

   Three things want this corner — the launcher, the toast and the sticky bulk
   bar — so the offset is a variable they all read rather than three hard-coded
   bottoms that drift apart. */
:root { --fab: 52px; --fab-b: 22px; --fab-r: 22px; }
/* The sticky bulk bar reaches into this corner and its last controls — Select
   all, Clear — sat under the launcher. It ends short of the launcher instead of
   the launcher moving: the bar is only stuck to the bottom while there is more
   list below it, so lifting the launcher fixed the stuck case and still covered
   Clear once the page was scrolled to the end. Shortening the bar holds at
   every scroll position. */
body:has(.chatfab) .bulkbar { padding-right: calc(var(--fab) + var(--fab-r) - var(--main-x) + 6px); }

.chatfab {
  position: fixed; right: var(--fab-r); bottom: var(--fab-b); z-index: 70;
  width: var(--fab); height: var(--fab); border-radius: 50%;
  background: var(--bright); display: grid; place-items: center;
  box-shadow: 0 12px 30px -10px oklch(0% 0 0 / .8);
  transition: bottom .18s, transform .12s;
}
.chatfab:hover { transform: translateY(-2px); }
.chatfab svg { color: var(--bg); }
.chatfab .lx { width: 23px; height: 15.6px; }
.chatfab .x { width: 19px; height: 19px; display: none; }
.chatfab[aria-expanded="true"] .lx { display: none; }
.chatfab[aria-expanded="true"] .x { display: block; }

/* the toast shares this corner, so it always clears the launcher */
body:has(.chatfab) .toast { bottom: calc(var(--fab-b) + var(--fab) + 12px); }
/* …and steps aside entirely while the panel occupies that space */
body:has(.chatwin:not([hidden])) .toast { right: calc(var(--fab-r) + 376px); }
@media (max-width: 900px) { body:has(.chatwin:not([hidden])) .toast { right: var(--fab-r); } }

.chatwin {
  position: fixed; right: var(--fab-r); z-index: 70;
  bottom: calc(var(--fab-b) + var(--fab) + 12px);
  width: 364px; max-width: calc(100vw - 44px);
  height: 470px; max-height: calc(100vh - var(--fab-b) - var(--fab) - 34px);
  display: flex; flex-direction: column; overflow: hidden;
  background: var(--surface); border: 1px solid var(--border-str);
  border-radius: var(--r-panel);
  box-shadow: 0 24px 60px -18px oklch(0% 0 0 / .85);
  transition: bottom .18s;
}
.chatwin .hd {
  display: flex; align-items: center; gap: 9px; flex: none;
  padding: 12px 13px; border-bottom: 1px solid var(--border);
}
.chatwin .hd h3 { font-size: 13px; }
.chatlog { flex: 1; overflow-y: auto; padding: 14px 13px; display: flex; flex-direction: column; gap: 9px; }
.msg { max-width: 86%; padding: 9px 11px; font-size: 12.5px; line-height: 1.5; border-radius: 12px; }
.msg.bot { align-self: flex-start; background: var(--surface-2); border-bottom-left-radius: 4px; }
.msg.me  { align-self: flex-end; background: var(--accent); color: var(--on-accent);
           border-bottom-right-radius: 4px; }
.msg.is-typing { color: var(--faint); letter-spacing: .12em; }
/* the openers are a cold start, so they leave once the thread has begun */
.chatsug { flex: none; display: flex; flex-direction: column; gap: 6px; padding: 0 13px 12px; }
.chatsug .chip { height: auto; padding: 7px 10px; font-size: 12px; text-align: left; }
.chatft { flex: none; border-top: 1px solid var(--border); padding: 11px 13px;
          display: flex; flex-direction: column; gap: 7px; }

/* ---- 11. Empty states -------------------------------------------------- */
.empty { display: flex; flex-direction: column; align-items: center; gap: 7px; padding: 40px 20px; text-align: center; }
.empty .ic { width: 36px; height: 36px; border-radius: 9px; display: grid; place-items: center;
  background: var(--surface-2); border: 1px solid var(--border); color: var(--faint); margin-bottom: 3px; }
.empty h4 { font-size: 13.5px; }
.empty p { font-size: 12.5px; color: var(--faint); max-width: 320px; }

/* ---- 11a0. Batch review tile --------------------------------------------
   The same 5:7 face the Collection contact sheet uses, because it is the same
   card and it should look like one. What is added is the only thing a batch
   asks of it: has this been dealt with. Three states, and the face itself
   carries them so a wall of 61 can be read without moving the eye down to a
   caption:

     unresolved  — plain face, waiting for a yes
     confirmed   — affirmative border and a tick, the same green and the same
                   glyph the ticks use everywhere else
     needs work  — down-coloured border and a persistent chip, because this one
                   cannot be confirmed until it is named, and dimming it would
                   send it to the back when it is the row that needs you most

   Clicking a tile confirms it. That is the fast path through a review, and it
   is the same act as ticking the row's checkbox in the table view. */
.rtile .face { cursor: pointer; }
/* a committed batch is read: its tiles are a record, so they promise nothing */
#rgrid.is-ro .rtile .face { cursor: default; }
.rtile.is-ok .face   { border-color: color-mix(in oklch, var(--up) 60%, transparent); }
.rtile.is-need .face { border-color: color-mix(in oklch, var(--down) 62%, transparent); }
/* the tick sits where the Collection tile's selection box sits, so the two
   never argue about which corner means "chosen" */
.rtile .mark {
  position: absolute; top: 8px; left: 8px; z-index: 2;
  width: 20px; height: 20px; border-radius: 6px; display: grid; place-items: center;
  background: color-mix(in oklch, var(--bg) 88%, transparent);
  opacity: 0; transition: opacity .12s, background .1s;
}
.rtile .mark svg { width: 12px; height: 12px; stroke-width: 3.1; opacity: 0; }
.rtile:hover .mark { opacity: 1; }
.rtile.is-ok .mark { opacity: 1; background: var(--up); color: var(--bg); }
.rtile.is-ok .mark svg { opacity: 1; }
/* a slab's grading band steps aside for the tick, exactly as it does for the
   Collection tile's picker */
.rtile:hover .face.is-slab .slab,
.rtile.is-ok .face.is-slab .slab { padding-left: 32px; }
/* the state chip, pinned to the bottom of the face over the card's own type */
.rtile .rchip {
  position: absolute; left: 8px; right: 8px; bottom: 8px; z-index: 2;
  display: flex; align-items: center; justify-content: center; gap: 5px;
  height: 22px; border-radius: 5px;
  font-family: var(--font-mono); font-size: 9.5px; letter-spacing: .05em;
  text-transform: uppercase;
}
.rtile.is-need .rchip {
  background: var(--down); color: var(--bg); font-weight: 500; cursor: pointer;
}
/* Dropping is destructive and rare, so it is not competing with Confirm for
   the same square inch — it waits on hover in the opposite corner. */
.rtile .drop {
  position: absolute; top: 8px; right: 8px; z-index: 2;
  width: 20px; height: 20px; border-radius: 6px; display: grid; place-items: center;
  background: color-mix(in oklch, var(--bg) 88%, transparent); color: var(--muted);
  opacity: 0; transition: opacity .12s, color .1s, background .1s;
}
.rtile:hover .drop { opacity: 1; }
.rtile .drop:hover { background: var(--down); color: var(--bg); }
.rtile .drop svg { width: 11px; height: 11px; stroke-width: 2.6; }
/* The chip covers the bottom of the face, which is where the card's own name
   is set. The face makes room for it rather than printing through it. */
.rtile.is-need .face { padding-bottom: 38px; }
/* match reading and review state on one line under the tile, so a dense sheet
   stays three lines tall per card whatever the state */
.rtile .mtline { display: flex; align-items: baseline; gap: 8px; margin-top: -2px; }
.rtile .mt { font-family: var(--font-mono); font-size: 10.5px; color: var(--faint); white-space: nowrap; }
.rtile .mt.st { margin-left: auto; }
.rtile .mt.is-low { color: var(--down-ink); }
.rtile.is-ok .mt.st { color: var(--up-ink); }
.rtile.is-need .mt.st { color: var(--down-ink); }

/* ---- 11a. Batch tile ----------------------------------------------------
   A batch is a stack of cards, so it is drawn as one: the same tile shape the
   Collection contact sheet uses — 5:7 face on top, name and figure underneath —
   with a pile of card silhouettes as the art. The index and the collection now
   read as one product rather than two card idioms side by side.

   What a batch is asked from across the room is which one it is and how much of
   it is left. The name takes the display face, exactly where a player's name
   sits on a card. The progress runs as a hairline along the bottom edge of the
   face, filled by what is identified with the remainder left dark — "not done
   yet" is an absence, not a second colour. The band at the top carries the one
   thing that needs a word. */
.bgrid { display: grid; gap: 14px; grid-template-columns: repeat(auto-fill, minmax(176px, 1fr)); }
.btile { display: flex; flex-direction: column; gap: 7px; text-align: left; position: relative; }
.bface {
  position: relative; aspect-ratio: 5 / 7; overflow: hidden;
  border-radius: var(--r-card); border: 1px solid var(--border);
  background: var(--surface-2);
  display: flex; flex-direction: column; justify-content: flex-end;
  padding: 34px 10px 14px; transition: border-color .14s, transform .14s;
}
.btile:hover .bface { border-color: var(--border-str); transform: translateY(-2px); }
/* The pile — three 5:7 planes fanned behind one another, so the face reads as a
   stack of cards before a single word is. The two behind are rotated and set
   back in tone; the front one is the brightest plane on the face and carries
   the count, because the number belongs ON the top card of the pile rather than
   floating in a corner. */
.bstack { position: absolute; inset: 40px 0 54px; pointer-events: none; }
/* The two behind RECEDE — mixed toward the canvas, not toward the face. Mixing
   --surface-3 into --surface-2 put them within a few percent of the face they
   sit on and the pile disappeared; a card behind another card is in shadow. */
.bstack i {
  position: absolute; top: 0; bottom: 0; left: 50%;
  aspect-ratio: 5 / 7; border-radius: 5px;
  border: 1px solid color-mix(in srgb, var(--bg) 40%, var(--border-str));
  background: color-mix(in srgb, var(--bg) 62%, var(--surface-2));
}
.bstack i:nth-child(1) { transform: translateX(-50%) translateX(-26px) rotate(-9deg); }
.bstack i:nth-child(2) { transform: translateX(-50%) translateX(14px) rotate(6deg);
                         background: color-mix(in srgb, var(--bg) 34%, var(--surface-2)); }
.bstack i:nth-child(3) {
  transform: translateX(-50%);
  background: color-mix(in srgb, var(--surface-3) 92%, var(--bright));
  border-color: color-mix(in srgb, var(--border-str) 60%, var(--bright));
  display: grid; place-items: center;
}
/* the count, set on the front plane in the display face */
.bstack i:nth-child(3)::after {
  content: attr(data-n);
  font-family: var(--font-display); font-weight: 400; font-size: 30px; line-height: 1;
  color: var(--bright);
}
.btile:hover .bstack i:nth-child(1) { transform: translateX(-50%) translateX(-30px) rotate(-11deg); }
.btile:hover .bstack i:nth-child(2) { transform: translateX(-50%) translateX(18px) rotate(8deg); }
.bstack i { transition: transform .16s; }
/* the state band, in the slot a graded card gives its slab label */
.bface .bband {
  position: absolute; left: 0; right: 0; top: 0; height: 26px;
  display: flex; align-items: center; gap: 6px; padding: 0 9px;
  background: var(--surface-3); border-bottom: 1px solid var(--border);
  font-family: var(--font-mono); font-size: 9.5px; letter-spacing: .05em;
  text-transform: uppercase; color: var(--muted);
}
.bface .bband.is-need { background: var(--down-wash); color: var(--down-ink);
                        border-bottom-color: color-mix(in oklch, var(--down) 40%, transparent); }
.bface .bband.is-ready { background: var(--up-wash); color: var(--up-ink);
                         border-bottom-color: color-mix(in oklch, var(--up) 40%, transparent); }
.bface .bband b { margin-left: auto; color: inherit; }
.bface .catbar { position: absolute; left: 0; top: 0; bottom: 0; width: 3px; z-index: 2; }
.bface .setline { position: relative; z-index: 2;
  font-family: var(--font-mono); font-size: 9.5px; letter-spacing: .06em;
  text-transform: uppercase; color: var(--muted); }
.bface .who { position: relative; z-index: 2;
  font-family: var(--font-display); font-size: 15px; line-height: 1.1;
  margin-top: 4px; text-wrap: balance; }
/* progress along the bottom edge of the thing it describes */
.bface .bprog { position: absolute; left: 0; right: 0; bottom: 0; height: 3px;
                background: color-mix(in srgb, var(--bg) 55%, transparent); z-index: 2; }
.bface .bprog > i { display: block; height: 100%; background: var(--up); }
.btile .meta { display: flex; align-items: baseline; gap: 8px; }
.btile .nm { font-size: 12.5px; font-weight: 500; line-height: 1.3; }
.btile .val { margin-left: auto; font-family: var(--font-mono); font-size: 12.5px; font-weight: 500; white-space: nowrap; }
.btile .sub { font-size: 11px; color: var(--faint); }
/* the grid's own group heading — the table bands with a row, the grid cannot */
.bhead {
  display: flex; align-items: center; gap: 10px; margin: 20px 0 10px;
  font-family: var(--font-mono); font-size: 9.5px; letter-spacing: .07em;
  text-transform: uppercase; color: var(--faint);
}
.bhead::after { content: ''; flex: 1; height: 1px; background: var(--border-str); }
.bhead:first-child { margin-top: 0; }

/* ---- 11b. Stepped review ------------------------------------------------
   A step is not a wizard page. Everything stays on one screen, in one scroll,
   because the seller has to be able to look back at what they picked while
   answering the next question — a wizard that hides step 1 to show step 2 is
   why people publish 40 listings to the wrong channel.

   What a step gates is whether it can be ANSWERED yet, not whether it can be
   SEEN. A locked step keeps its number, its title and its full height, and
   states the one thing that would unlock it. The gate then reads as an order
   of operations rather than as a broken panel. */
.stepp .panel-hd { gap: 11px; }
.stepn {
  width: 24px; height: 24px; border-radius: 50%; flex: none; display: grid; place-items: center;
  font-family: var(--font-mono); font-size: 11.5px; font-weight: 600;
  background: var(--surface-3); color: var(--faint); border: 1px solid var(--border-str);
}
/* The step you are on is the only filled number on the page. Done steps go
   green because "answered" is the same affirmative the ticks use everywhere
   else; steps still waiting stay the neutral raised chip. */
.stepp.is-now  .stepn { background: var(--accent); color: var(--on-accent); border-color: var(--accent); }
.stepp.is-done .stepn { background: var(--up-wash); color: var(--up-ink);
                        border-color: color-mix(in oklch, var(--up) 45%, transparent); }
.stepp.is-done .stepn svg { width: 12px; height: 12px; stroke-width: 3.1; }
.stepp .steptitle { display: flex; align-items: baseline; gap: 9px; }
.stepp .steptitle .answer { font-size: 12px; color: var(--faint); }
.stepp.is-done .steptitle .answer { color: var(--muted); }
/* A locked body is dimmed, not hidden: its inputs are inert, and pointer-events
   is what makes that true rather than just decorative. */
.stepp.is-locked .panel-bd { opacity: .38; pointer-events: none; user-select: none; }
.stepp.is-locked .steptitle h3 { color: var(--muted); }
.steplock {
  display: flex; align-items: center; gap: 8px;
  padding: 10px 14px; border-top: 1px solid var(--border);
  font-size: 12px; color: var(--faint);
}
.steplock svg { width: 13px; height: 13px; flex: none; }

/* one answer written down every row of the selection */
.applybar {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 11px 13px; border-radius: var(--r-ctl);
  background: var(--surface-2); border: 1px solid var(--border-str); margin-bottom: 12px;
}
.applybar .inp { height: 30px; }
.applybar select { font-size: 12px; }

/* template picker — the same row shape as .chan, because picking a template is
   the same kind of act as picking a channel, one step apart. */
.tpl {
  display: flex; align-items: center; gap: 11px; width: 100%;
  padding: 10px 13px; border-radius: var(--r-ctl); text-align: left;
  background: var(--surface-2); border: 1px solid var(--border);
  transition: border-color .12s, background .12s;
}
.tpl:hover { border-color: var(--border-str); }
.tpl[aria-pressed="true"] { border-color: var(--border-str); background: var(--surface-3); }
/* radio, not checkbox: one template per pass, so the marker is round */
.tpl .dotmark {
  width: 18px; height: 18px; border-radius: 50%; flex: none; display: grid; place-items: center;
  background: var(--pick-rest); transition: background .1s;
}
.tpl .dotmark::after { content: ''; width: 7px; height: 7px; border-radius: 50%; background: transparent; }
.tpl[aria-pressed="true"] .dotmark { background: var(--fg); }
.tpl[aria-pressed="true"] .dotmark::after { background: var(--bg); }
.tpl .nm, .tpl .sub { display: block; }
.tpl .nm { font-size: 13px; font-weight: 500; }
.tpl .sub { font-size: 11.5px; color: var(--muted); margin-top: 1px; }   /* --surface-2: see .chan .sub */
.tpllist { display: flex; flex-direction: column; gap: 8px; }

/* the held-back list in the publish summary */
.blockrow { display: flex; gap: 8px; align-items: flex-start; padding: 7px 0; font-size: 12px; }
.blockrow + .blockrow { border-top: 1px solid var(--border); }
.blockrow .who { font-weight: 500; color: var(--fg); }
.blockrow .why { color: var(--faint); margin-top: 1px; }

/* ---- 11c. Browse: price guide, wishlist, recently viewed ----------------
   Three screens that talk about cards the collection does not necessarily
   contain, so none of them can lean on the tile grid — a wishlist card has no
   slab you own and a price-guide row is a market, not an object. What they do
   share is that every row still has to be recognisably a CARD, and that the
   numbers have to line up under their headings. Hence a miniature face for the
   first cell and ordinary table columns for everything else. */

/* The face at table scale. Same 5:7 and the same category spine as .face, with
   everything that needs room to be legible removed rather than shrunk — a
   9.5px set line reduced to 6px is not a smaller design, it is an unreadable
   one. What survives is the silhouette and the colour, which is all a row
   needs to say "this is that card". */
.mface { position: relative; width: 30px; aspect-ratio: 5 / 7; flex: none;
         border-radius: 3px; overflow: hidden; border: 1px solid var(--border);
         background: var(--surface-2); }
.mface .catbar { position: absolute; left: 0; top: 0; bottom: 0; width: 2px; }
.mface.is-slab::before { content: ""; position: absolute; left: 0; right: 0; top: 0;
                         height: 7px; background: var(--surface-3); }
/* Wanted cards are not owned, so the face is drawn as an outline rather than a
   solid — the same distinction the product already makes between a listing and
   a draft. */
.mface.is-want { background: none; border-style: dashed; }
.cardcell { display: flex; align-items: center; gap: 10px; min-width: 0; }
.cardcell > div { min-width: 0; }

/* A grade ladder. One card is not one price: the same Aaron Judge is three
   different markets in PSA 10, PSA 9 and raw, and a guide that prints a single
   number for it is not a guide. The ladder is the child-row pattern from the
   selling desk — real table rows so Market, 30d and Comps stay under their own
   headings — recessed the same way, and opened by the card name for the same
   reason there is no chevron there either. */
.grow-in > td { background: var(--bg); font-size: 12px; padding-top: 7px; padding-bottom: 7px; }
.grow-in .gname { display: inline-flex; align-items: center; gap: 7px; margin-left: 40px;
                  font-family: var(--font-mono); font-size: 11.5px; }
.grow-in:hover > td { background: color-mix(in oklch, var(--bg) 70%, var(--surface)); }
/* The grade that carries the headline price. A guide quotes the best available
   grade, and the row has to say which one it quoted. */
.gtop { font-family: var(--font-mono); font-size: 9.5px; letter-spacing: .06em;
        text-transform: uppercase; color: var(--faint); }

/* Target vs market. The whole point of a wishlist is the distance between what
   a card costs and what you are willing to pay, so that distance is a drawn
   quantity and not another number in a row of numbers. The bar fills from the
   left toward your target; at or past it, the fill takes the affirmative
   colour and the row stops being a wish and becomes a decision. */
.gapbar { width: 84px; height: 5px; border-radius: 3px; background: var(--surface-3);
          overflow: hidden; }
.gapbar > i { display: block; height: 100%; background: var(--muted); border-radius: 3px; }
.gapbar.is-hit > i { background: var(--up); }
.gapcell { display: flex; align-items: center; gap: 9px; justify-content: flex-end; }
.gapn { font-family: var(--font-mono); font-size: 11.5px; color: var(--muted); min-width: 52px;
        text-align: right; }
.gapn.is-hit { color: var(--up-ink); font-weight: 500; }
/* A row that has come to your price is the reason the page was opened, so it
   is marked on the row itself and not only in the gap column. */
tr.is-hit td { background: color-mix(in srgb, var(--up) 7%, transparent); }
tr.is-hit:hover td { background: color-mix(in srgb, var(--up) 12%, transparent); }

/* A day heading inside a table. Recently Viewed is a log, and a log without
   days is a pile — but the entries under each day still belong to the same
   columns, so the heading is a row in the same table rather than a new table
   per day. */
tr.dayrow td { background: var(--bg); padding-top: 15px; padding-bottom: 6px;
               border-bottom: 1px solid var(--border); }
tr.dayrow:first-child td { padding-top: 8px; }
.dayhd { display: flex; align-items: baseline; gap: 9px; }
.dayhd b { font-family: var(--font-mono); font-size: 9.5px; letter-spacing: .06em;
           text-transform: uppercase; color: var(--muted); font-weight: 600; }
.dayhd .n { font-size: 11px; color: var(--faint); }
/* Where the view came from. Not decoration: it is the difference between a card
   you sought out and one you brushed past in a feed. */
.src { font-family: var(--font-mono); font-size: 9.5px; letter-spacing: .05em;
       text-transform: uppercase; color: var(--faint); }

/* ---- 12. Responsive ---------------------------------------------------- */
@media (max-width: 1180px) {
  :root { --rail: 68px; }
  .rail .label, .rail-grp > .micro, .brand span, .rail .cnt,
  .rail .locked .lock, .brand .tierbadge { display: none; }
  .rail a, .rail button.navitem { justify-content: center; padding: 9px 0; }
  .brand { justify-content: center; padding-inline: 0; }
}
@media (max-width: 860px) {
  /* minmax(0, 1fr), not 1fr. A bare 1fr track has a min of auto, so a wide
     table inside it grows the column instead of scrolling in its panel and the
     whole page picks up a horizontal scrollbar. The desktop rule already said
     minmax(0, 1fr); this one quietly dropped it, which is why every page with a
     wide table scrolled sideways below 860. */
  .app { grid-template-columns: minmax(0, 1fr); }
  .rail {
    position: sticky; top: 0; height: auto; z-index: 35;
    flex-direction: row; overflow-x: auto; gap: 2px;
    border-right: 0; border-bottom: 1px solid var(--border); padding: 8px 12px;
  }
  .rail-grp { margin-top: 0; display: contents; }
  .rail a, .rail button.navitem { width: auto; padding: 8px 11px; }
  .brand { padding: 0 8px 0 0; }
  .main { padding: 16px 14px 40px; }
  .topbar { padding-inline: 14px; }
  .datagrid { grid-template-columns: 1fr; }
  .stats > div { border-right: 0; border-bottom: 1px solid var(--border); min-width: 46%; }
  /* wide tables scroll inside their panel instead of blowing out the page */
  .panel { overflow-x: auto; }
  table.t { min-width: 560px; }
}
@media (max-width: 520px) {
  .sheet { grid-template-columns: repeat(auto-fill, minmax(132px, 1fr)); gap: 10px; }
  .phd h1 { font-size: 21px; }
  .stats > div { min-width: 100%; }
  .search { max-width: none; }
}
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: .001ms !important; transition-duration: .001ms !important; }
}
