/* =========================================================================
   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 */

  --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); }
.chan .nm { font-size: 13px; font-weight: 500; }
.chan .sub { font-size: 11.5px; color: var(--faint); 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); }
/* 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); }
.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); }

/* 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;
}
/* --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); }
.nope { background: color-mix(in oklch, var(--faint) 20%, transparent); color: var(--faint); }
.nope.is-warn { background: var(--down-wash); color: var(--down); }
/* 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); }
.badge-down { border-color: color-mix(in oklch, var(--down) 45%, transparent); background: var(--down-wash); color: var(--down); }
.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); }
.face .setline { font-family: var(--font-mono); font-size: 9.5px; letter-spacing: .06em; text-transform: uppercase; color: var(--faint); }
.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; }
.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 were both --surface-2, so a row you were merely pointing
   at looked exactly like a row you had ticked — and hovering an already-picked
   row gave no feedback at all. They now step: 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.
   All three are scoped identically so the cascade is decided by order, not by
   an accident of specificity. */
table.t tbody tr:hover td { background: var(--surface); }
/* A row that opens something says so before it is clicked. The hover paint was
   already here; the pointer is what was missing. The checkbox keeps the default
   cursor because it does something else — it selects, it does not navigate. */
table.t[data-rowlink] tbody tr[data-href] { cursor: pointer; }
table.t[data-rowlink] tbody tr .rowpick { cursor: pointer; }
table.t tbody tr.is-picked td { background: var(--surface-2); }
table.t tbody tr.is-picked:hover td { background: color-mix(in oklch, var(--surface-2) 86%, var(--fg)); }
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; }

/* ---- 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) {
  .app { grid-template-columns: 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; }
}
