/* ============================================================
   Vendoor — premium component classes
   ------------------------------------------------------------
   The shadcn-style light theme, plus its dark counterpart:
     stark white cards, crisp slate-200 hairlines, slate-50
     ground, the brand accent, Figtree, FLAT depth.

   These are plain CSS classes rather than @apply because the
   Tailwind browser build compiles at runtime and @apply in a
   runtime-compiled sheet is a needless dependency. Utilities
   still work everywhere; this file is for the handful of things
   repeated on every page.

   FLAT DEPTH is the rule that matters. A resting surface gets a
   1px border and NO shadow. Shadow is spent only on things that
   genuinely float above the page (menus, drawers). Putting a
   shadow on every card is the single change that makes a UI read
   as a template instead of as a product.
   ============================================================ */

/* ---------- type ----------
   --font-sans is declared ONCE, in vendoor.css; this file only binds it.
   It used to name its own "Inter" here, which left the family a page
   rendered in decided by whichever stylesheet loaded last:
   index/portal/compare resolved Geist and downloaded Inter for nothing,
   while dashboard.html linked Inter, resolved Geist, and fell through to
   the system stack because Geist was never fetched on that page. Three
   typefaces, one product, no error anywhere.

   It has to be declared in CSS either way: the Tailwind v4 browser build
   does NOT read window.tailwind.config (that is the v3 Play-CDN API), so
   a font named there is silently dropped. */
:root {
  /* ONE content width, used by the top bar AND every page body. The bar
     was 1400, the directory toolbar 1180 and the directory content 1120,
     so three bands on one page each began at a different x and nothing
     lined up with anything above it. Pages must use this, not their own. */
  --page-max: 1280px;
  --page-gutter: clamp(12px, 2.2vw, 24px);
}
html, body, .font-sans { font-family: var(--font-sans); }

/* Proportional figures make a column of prices jitter as it updates.
   Everything numeric here is meant to be read down a column. */
body { letter-spacing: -0.006em; }
.tnum, .metric-v, table td, table th { font-variant-numeric: tabular-nums; }

/* ---------- shell ----------
   The 260px sidebar rail (.app-shell/.app-side/.app-header/.app-scrim) was
   here and is GONE. The navigation became one horizontal top bar, which is
   a single form at every width instead of a rail plus a drawer that had to
   be kept in agreement — and by the time these were deleted NO page carried
   any of the classes. They were dead CSS that still had to be kept in step
   with every palette change. The bar's own styles are under "topbar" below.
   The drawer's `translate`-not-`transform` note went with it; if a drawer
   ever comes back, Tailwind v4 implements its translate utilities with the
   CSS `translate` property, so `transform: none` will not move it. */

.app-body { flex: 1 1 auto; padding: clamp(16px, 2.4vw, 32px); }

/* ---------- nav item ---------- */
.nav-item {
  display: flex; align-items: center; gap: 12px; width: 100%;
  padding: 9px 12px;
  border: 0; background: transparent; text-align: left;
  border-radius: 6px;
  font-size: 14px; font-weight: 500; line-height: 1.4;
  color: var(--muted-foreground);
  text-decoration: none;
  transition: background .16s ease, color .16s ease;
}
.nav-item svg { width: 18px; height: 18px; flex: 0 0 auto; color: var(--subtle-foreground); transition: color .16s ease; }
.nav-item:hover { background: var(--muted); color: var(--foreground); }
.nav-item:hover svg { color: var(--brand-600); }
.nav-item.is-active { background: var(--brand-50); color: var(--brand-700); font-weight: 600; }
.nav-item.is-active svg { color: var(--brand-600); }

.nav-count {
  margin-left: auto; padding: 1px 7px; border-radius: 999px;
  background: var(--muted); color: var(--muted-foreground);
  font-size: 11px; font-weight: 600; font-variant-numeric: tabular-nums;
}
.nav-count[hidden] { display: none; }

/* ---------- surfaces ---------- */
/* Flat by default. No shadow on a resting card, ever. */
.card {
  background: var(--card);
  border: 1px solid var(--border-color);
  border-radius: 10px;
}

.card-pad { padding: clamp(14px, 1.6vw, 20px); }

.card-hover { transition: border-color .16s ease, background .16s ease; }
.card-hover:hover { border-color: var(--brand-200); }

/* ---------- metric card ---------- */
.metric { background: var(--card); border: 1px solid var(--border-color); border-radius: 10px; padding: 18px 20px; transition: border-color .16s ease; }
.metric:hover { border-color: var(--brand-200); }
.metric-k { font-size: 12px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; color: var(--muted-foreground); }
.metric-v { margin-top: 8px; font-size: 30px; font-weight: 700; line-height: 1.05; letter-spacing: -.025em; color: var(--foreground); font-variant-numeric: tabular-nums; }
.metric-v.accent { color: var(--brand-600); }
.metric-h { margin-top: 5px; font-size: 12px; color: var(--muted-foreground); }

/* ---------- data table ----------
   The buyer's list-of-things component: a row per subject, an initials
   chip, a filter strip above and a pager below. It lives HERE rather than
   in one page's <style> because it is the shape both My Tenders and any
   future list want, and a second copy is a second thing to keep in step.
   Nothing in it is named after tenders or quotes for the same reason. */

/* A table gets a scroll container at EVERY width, not just on a phone: the
   panel clips, so an oversized table is silently CUT rather than scrolled,
   which is the worst of the three options. The negative margin lets the
   scroll run to the panel's edges instead of stopping inside its padding. */
.dt-scroll { overflow-x: auto; margin-inline: calc(clamp(14px,1.7vw,20px) * -1); padding-inline: clamp(14px,1.7vw,20px); }
.dt { width: 100%; min-width: 680px; border-collapse: collapse; }
.dt th {
  padding: 0 12px 10px 0; text-align: left; white-space: nowrap;
  font-size: 10.5px; font-weight: 700; letter-spacing: .09em; text-transform: uppercase;
  color: var(--muted-foreground); border-bottom: 1px solid var(--border-color);
}
.dt td { padding: 13px 12px 13px 0; border-bottom: 1px solid var(--border-color); font-size: 13px; vertical-align: middle; }
.dt tr:last-child td { border-bottom: 0; }
.dt th:last-child, .dt td:last-child { padding-right: 0; text-align: right; }

/* Initials, not an avatar image: there is no logo in the database, and a
   different colour per subject would be a palette we do not have. */
.dt-who { display: flex; align-items: center; gap: 11px; min-width: 0; }
.dt-ini {
  flex: 0 0 auto; width: 34px; height: 34px; border-radius: 50%; display: grid; place-items: center;
  background: var(--brand-50); color: var(--brand-700);
  font-size: 11.5px; font-weight: 700; letter-spacing: .02em;
}
.dt-nm { font-weight: 640; color: var(--foreground); }
.dt-sub { margin-top: 1px; font-size: 11.5px; color: var(--muted-foreground); }
.dt-num { font-weight: 700; color: var(--foreground); font-variant-numeric: tabular-nums; white-space: nowrap; }
.dt-num.soft { font-weight: 500; color: var(--muted-foreground); }
.dt-link { font-size: 12.5px; font-weight: 620; color: var(--brand-600); text-decoration: none; white-space: nowrap;
           background: none; border: 0; padding: 0; font-family: inherit; cursor: pointer; }
.dt-link:hover { text-decoration: underline; }

/* A meter inside a cell: fixed track, so the column does not jump width as
   the numbers beside it change. */
.dt-meter { display: flex; align-items: center; gap: 9px; }
/* "0 of 15" must never wrap into a three-line column beside the bar. */
.dt-meter .dt-sub { white-space: nowrap; }
.dt-bar { flex: 0 0 64px; height: 5px; border-radius: 99px; background: var(--muted); overflow: hidden; }
.dt-bar > span { display: block; height: 100%; border-radius: 99px; background: var(--brand-600); }

.chips { display: flex; flex-wrap: wrap; gap: 7px; margin-bottom: 14px; }
.chip {
  padding: 6px 14px; border-radius: 999px; border: 1px solid var(--border-color); background: var(--card);
  font-size: 12.5px; font-weight: 620; color: var(--muted-foreground); cursor: pointer; font-family: inherit;
}
.chip:hover { border-color: var(--border-strong); color: var(--foreground); }
.chip.on { background: var(--brand-600); border-color: var(--brand-600); color: #fff; }

.dt-foot {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  margin-top: 14px; padding-top: 13px; border-top: 1px solid var(--border-color);
  font-size: 12px; color: var(--muted-foreground);
}
.dt-pager { margin-left: auto; display: flex; gap: 7px; }
.dt-pager button {
  padding: 6px 13px; border-radius: 7px; border: 1px solid var(--border-color); background: var(--card);
  font-size: 12.5px; font-weight: 600; color: var(--foreground); cursor: pointer; font-family: inherit;
}
.dt-pager button:disabled { color: var(--subtle-foreground); cursor: default; }
.dt-pager button:not(:disabled):hover { border-color: var(--brand-400); }

/* ---- .dt on a narrow device ----
   Six columns and a 680px floor cannot be made to work on a phone, and
   side-scrolling the list that answers 'where is my tender?' means swiping
   past the name to reach Compare on every row. So below this width each row
   stops being a row and becomes a card, carrying its column header down as a
   label on each field (a data-label attribute, set in the row builder). It is
   the same mechanism admin.html uses for the pipeline table, deliberately: one
   technique for one problem. A NEW COLUMN NEEDS A NEW data-label or it
   renders unlabelled here. A cell labelled '' is a title or an action row,
   and shows its value with no label beside it.

   900, matching admin: an iPad in portrait is the width the six columns stop
   fitting, not 560. */
/* THE TABLE IS THE IPAD AND LAPTOP FORM (601px up). It scrolls sideways
   inside .dt-scroll exactly as it does in a narrow laptop window, and the
   FIRST COLUMN PINS so the tender's name never leaves the screen while the
   figures scroll past — the same mechanism compare.html's matrix uses, for
   the same reason: a row you can't tell whose it is answers nothing. Both
   sticky cells need their own background or the scrolling columns show
   through them; --n-0 is the page ground AND the card in both themes.
   Below 600px the row goes VERTICAL instead — see the card block further
   down, and the founder call recorded on it. */
.dt th:first-child, .dt td:first-child {
  position: sticky; left: 0; z-index: 2; background: var(--n-0);
}
.dt thead th:first-child { z-index: 3; }
@media (max-width: 720px) {
  /* Laptop anatomy, phone-scaled: tighter cell padding and a narrower pinned
     column, so at least two figure columns ride beside the name. The name
     wraps instead of running under the pinned edge. */
  .dt th, .dt td { padding: 11px 10px 11px 0; }
  .dt td:first-child, .dt th:first-child { max-width: 46vw; }
  .dt td:first-child .dt-link { white-space: normal; text-align: left; }
}

/* ON THE PHONE THE ROW GOES VERTICAL (founder call, 2026-08-21, from the
   device): the pinned-column table at 390px showed status pills sliding
   half-under the pinned edge and figures wrapping three lines tall — his
   words were "if not possible on horizontal, do vertical". So ≤600 each row
   is a composed card — name with the status chip beside it, the three
   figures as a row of mini-stats under small captions, the action on a
   hairline floor. The table, with its pinned first column, is the iPad and
   laptop form (601px up). Placement is BY POSITION for the six My Tenders
   columns (who · Suppliers · Quotes in · Sent · Status · action); a second
   .dt table needs its own placement or it inherits this one wrongly. */
@media (max-width: 600px) {
  .dt-scroll { overflow-x: visible; margin-inline: 0; padding-inline: 0; }
  .dt { min-width: 0; }
  .dt thead { display: none; }
  .dt, .dt tbody { display: block; width: 100%; }
  .dt tr {
    display: grid;
    grid-template-columns: minmax(0, .9fr) minmax(0, 1.1fr) auto;
    column-gap: 14px; row-gap: 13px; align-items: start;
    border: 1px solid var(--border-color); border-radius: 12px;
    margin: 0 0 10px; padding: 15px 16px 12px;
  }
  .dt td { display: block; width: auto; min-width: 0; border: 0; padding: 0; }
  /* The card mode must undo the pinned-column rules above. */
  .dt td:first-child, .dt th:first-child { position: static; max-width: none; background: transparent; }
  .dt td::before {
    content: attr(data-label); display: block; margin-bottom: 3px;
    font-size: 9.5px; font-weight: 700; letter-spacing: .09em; text-transform: uppercase;
    color: var(--muted-foreground);
  }
  .dt td[data-label=""]::before { display: none; }
  .dt td:nth-child(1) { grid-column: 1 / 3; grid-row: 1; }
  .dt td:nth-child(5) { grid-column: 3; grid-row: 1; justify-self: end; }
  .dt td:nth-child(5)::before { display: none; }
  .dt td:nth-child(2) { grid-column: 1; grid-row: 2; }
  .dt td:nth-child(3) { grid-column: 2; grid-row: 2; }
  .dt td:nth-child(4) { grid-column: 3; grid-row: 2; justify-self: end; text-align: right; }
  .dt td:last-child {
    grid-column: 1 / -1; grid-row: 3; text-align: right;
    border-top: 1px solid var(--border-color); padding-top: 11px;
  }
  .dt-bar { flex: 0 0 48px; }
  .dt-meter { flex-wrap: wrap; }
  .dt td:nth-child(1) .dt-link { white-space: normal; text-align: left; }
  .dt-who > div { min-width: 0; }
}

@media (max-width: 560px) { .dt-ini { display: none; } }

/* ---------- badge ---------- */
.badge {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 3px 9px; border-radius: 999px;
  font-size: 11.5px; font-weight: 600; line-height: 1.5; white-space: nowrap;
  border: 1px solid transparent;
}
/* Five states, ONE hue. They are told apart by fill and border weight,
   which is the only way a restricted palette can carry meaning — and the
   only way any palette carries it to a colour-blind reader or a printer.
   Loudest first: filled brand (the outcome you wanted), tinted brand (in
   the buyer's hands), heavy ink (the bad outcome), outline (waiting),
   flat grey (nothing has happened). */
.badge-neutral { background: var(--muted); color: var(--muted-foreground); border-color: var(--border-color); }
.badge-brand   { background: var(--brand-50); color: var(--brand-700); border-color: var(--brand-100); }
.badge-success { background: var(--brand-600); color: #fff; border-color: var(--brand-600); font-weight: 650; }
.badge-warning { background: transparent; color: var(--n-700); border-color: var(--border-strong); }
.badge-danger  { background: var(--muted); color: var(--foreground); border-color: var(--n-400); font-weight: 650; }

/* ---------- buttons ---------- */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  padding: 9px 15px; border-radius: 6px;
  font-size: 14px; font-weight: 600; line-height: 1.3;
  border: 1px solid transparent; text-decoration: none; white-space: nowrap;
  transition: background .16s ease, border-color .16s ease, color .16s ease;
}
.btn-primary { background: var(--brand-600); color: #fff; }
.btn-primary:hover { background: var(--brand-700); }
.btn-secondary { background: var(--card); color: var(--n-700); border-color: var(--border-color); }
.btn-secondary:hover { background: var(--muted); border-color: var(--border-strong); }
.btn-ghost { background: transparent; color: var(--muted-foreground); }
.btn-ghost:hover { background: var(--muted); color: var(--foreground); }

/* ---------- focus ----------
   Visible for keyboards only, but never removed: a page with no focus
   ring cannot be operated without a mouse. */
:focus-visible { outline: 2px solid var(--brand-600); outline-offset: 2px; border-radius: 4px; }

/* ---------- motion ----------
   Content is visible by default; .reveal only hides once JS confirms it
   is running (html.js-motion), so a failed script can never leave a
   blank page. */
.js-motion .reveal { opacity: 0; transform: translateY(8px); }
.js-motion .reveal.in {
  opacity: 1; transform: none;
  transition: opacity .38s cubic-bezier(.16,1,.3,1), transform .38s cubic-bezier(.16,1,.3,1);
  transition-delay: calc(var(--i, 0) * 45ms);
}
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important; transition-duration: .01ms !important;
  }
  .js-motion .reveal, .reveal { opacity: 1 !important; transform: none !important; }
}

/* ---------- skeleton ---------- */
.skeleton { position: relative; overflow: hidden; background: var(--muted); border-radius: 6px; }
.skeleton::after {
  content: ''; position: absolute; inset: 0; transform: translateX(-100%);
  background: linear-gradient(90deg, transparent, rgba(255,255,255,.6), transparent);
  animation: sk 1.4s infinite;
}
.dark .skeleton::after { background: linear-gradient(90deg, transparent, rgba(255,255,255,.06), transparent); }
@keyframes sk { to { transform: translateX(100%); } }

/* ---------- misc ---------- */
.tnum { font-variant-numeric: tabular-nums; }
.table-scroll { width: 100%; overflow-x: auto; -webkit-overflow-scrolling: touch; }
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }

/* ============================================================
   TOP BAR — horizontal navigation
   ------------------------------------------------------------
   Replaced the vertical rail. Five destinations did not justify
   260px of every page, and a rail needs a second form (a drawer)
   on a phone — so the same nav existed twice. This is one form at
   every width: the tab strip scrolls sideways when it runs out of
   room, which is deliberately NOT a breakpoint, because the
   overflow point moves every time a tab is added.

   Written against vendoor.css tokens, not hardcoded slate/indigo,
   so it inherits dark mode with everything else.
   ============================================================ */
.topbar {
  position: sticky; top: 0; z-index: 50;
  background: color-mix(in srgb, var(--n-0) 88%, transparent);
  backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--n-200);
}
/* BRAND (left) · NAV (centred) · ACTIONS (right).

   A THREE-COLUMN GRID, not flex, and `1fr auto 1fr` is the whole point: the
   centre column is then centred against the BAR rather than against whatever
   the two sides happen to weigh. Under flex with `margin-left:auto`, a wide
   account cluster on the right pushes the tabs left by exactly half its own
   width, so "centred" navigation drifts every time a badge appears or the
   avatar loads — it reads as a rendering bug rather than as a layout.

   The two `1fr` side columns are equal by construction, so the centre stays
   put no matter what lands in them. */
.topbar-inner {
  /* minmax(0, …) on ALL THREE tracks. A bare `auto` centre track sizes to
     max-content and refuses to shrink, so on a phone the bar pushed the whole
     DOCUMENT 39px wider than the screen instead of letting the tabs scroll
     inside themselves — and the two 1fr sides then stopped being equal, which
     dragged the "centred" nav 33px off centre at ~600px. Every track has to be
     allowed to shrink or the grid solves the overflow by growing the page. */
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, auto) minmax(0, 1fr);
  align-items: center;
  gap: clamp(12px, 2vw, 26px);
  max-width: var(--page-max); margin-inline: auto;
  padding: 0 var(--page-gutter);
  height: 64px;
}

/* The brand is the first child and the route home. It went out of the bar
   once (the logo moved to the hero) and is back because the reference layout
   puts navigation in the centre, which needs both edges anchored — and a
   centred nav with an empty left corner reads as unbalanced, not as minimal. */
.brand { display: flex; align-items: center; gap: 9px; flex: 0 0 auto; text-decoration: none; }
.brand-mark {
  width: 32px; height: 32px; display: grid; place-items: center;
  border-radius: 8px; background: var(--brand-600); color: #fff;
  font-size: 17px; font-weight: 700; line-height: 1;
  transition: transform .25s cubic-bezier(.16,1,.3,1);
}
.brand:hover .brand-mark { transform: rotate(-8deg) scale(1.06); }
.brand-word {
  font-size: 18px; font-weight: 700; letter-spacing: -.02em;
  text-transform: lowercase; color: var(--brand-600);
}
/* NEVER hidden. This used to be `display: none` below 560px, which was safe
   while `.brand-mark` sat beside it — the mark carried the lockup. The mark is
   gone, so hiding the word leaves an EMPTY <a> in the corner: no logo, and no
   route home on a phone. It is eight characters; it stays. */

/* ---------- the hero's floating tender card ----------
   Real CSS, not Tailwind arbitrary values. `shadow-[0_10px_40px_-12px_rgba(...)]`
   silently did not compile in the v4 browser build — the class was on the
   element and the computed box-shadow came back fully transparent, which is the
   worst kind of failure: the card looked flat and nothing anywhere said why.
   Same reasoning as the rest of this file: the runtime-compiled utility layer is
   for layout, and anything load-bearing is written out.

   This is one of the few places a shadow is spent. The FLAT DEPTH rule at the
   top of this file says a resting surface gets a hairline and no shadow — but
   this card genuinely floats above a photograph, which is exactly the exception
   the rule names.

   The lift is transform + shadow ONLY, with no colour change: the card sits on
   an image, and a tint under the cursor reads as the photo shifting rather than
   as the card responding. */
.hero-tender {
  box-shadow: 0 10px 40px -12px rgba(0, 0, 0, .45);
  transition: transform .2s cubic-bezier(.16, 1, .3, 1), box-shadow .2s ease;
}
.hero-tender:hover,
.hero-tender:focus-visible {
  transform: translateY(-4px);
  box-shadow: 0 22px 60px -14px rgba(0, 0, 0, .55);
}
/* A logo that will not sit still is an accessibility problem, and so is a card
   that jumps. Keep the shadow cue, drop the movement. */
@media (prefers-reduced-motion: reduce) {
  .hero-tender { transition: box-shadow .2s ease; }
  .hero-tender:hover, .hero-tender:focus-visible { transform: none; }
}

/* ---- tabs ---- */
/* The CENTRE grid column. It sizes to its content (the `auto` track) rather
   than growing, which is what keeps it centred; the two 1fr columns either
   side do the centring for it.

   `min-width: 0` stays load-bearing — without it a grid/flex item never
   shrinks below its content, and on a narrow window the nav pushes the whole
   DOCUMENT wider than the screen instead of scrolling inside itself. The
   symptom of getting that wrong is the entire page sliding sideways, which
   reads as "the layout is broken" rather than "the nav is too wide". */
.tabs {
  display: flex; align-items: center; gap: 10px;
  min-width: 0; justify-self: center;
  overflow-x: auto; scrollbar-width: none;
}
.tabs::-webkit-scrollbar { display: none; }

.tab {
  position: relative;
  display: inline-flex; align-items: center; gap: 7px;
  padding: 8px 12px; border-radius: 7px;
  font-size: 14px; font-weight: 500; white-space: nowrap;
  color: var(--n-600); text-decoration: none;
  transition: color .16s ease, background .16s ease;
}
.tab svg { width: 16px; height: 16px; flex: 0 0 auto; opacity: .75; transition: opacity .16s ease; }

/* Below 760px the tabs are ICONS ONLY. Four labelled tabs plus the brand and
   the account cluster do not fit a phone, and the alternative — letting the
   strip scroll sideways — hides destinations behind a gesture nobody knows is
   there. The icons are why this works: they were added to make the bar feel
   considered, and they double as the small-screen form of it.

   `aria-label` is not needed: the text is still in the DOM, only clipped to a
   1px box, so a screen reader reads the tab exactly as before. `display: none`
   would have taken the name away from assistive tech along with the pixels. */
@media (max-width: 760px) {
  .tab { padding: 8px 10px; }
  /* .tab-label, not `svg + *`: the label used to be a bare TEXT NODE, which no
     selector can reach — the adjacent-sibling would have hit the count badge
     instead and hidden the one thing that must stay visible. Wrapping it in a
     span is what makes this rule addressable at all. */
  .tab-label { position: absolute; width: 1px; height: 1px; overflow: hidden;
               clip-path: inset(50%); white-space: nowrap; }
}
.tab:hover { color: var(--n-900); }
.tab:hover svg { opacity: 1; }
.tab.is-active { color: var(--brand-600); font-weight: 600; }
.tab.is-active svg { opacity: 1; }

/* The underline slides in rather than appearing, which is what makes a
   tab strip feel like one control instead of five links. */
.tab::after {
  content: ''; position: absolute; left: 10px; right: 10px; bottom: -1px;
  height: 2px; border-radius: 2px 2px 0 0;
  background: var(--brand-600);
  transform: scaleX(0); transform-origin: center;
  transition: transform .26s cubic-bezier(.16,1,.3,1);
}
.tab.is-active::after { transform: scaleX(1); }

.tab-count {
  padding: 1px 6px; border-radius: 999px;
  background: var(--n-0); border: 1px solid var(--n-200); color: var(--n-600);
  font-size: 11px; font-weight: 600; font-variant-numeric: tabular-nums;
}
.tab.is-active .tab-count { background: var(--brand-50); color: var(--brand-600); }
.tab-count[hidden] { display: none; }

/* ---- actions ---- */
/* The theme toggle, the WhatsApp button, the account avatar and the CTA are
   four separate controls, so they get separated: at gap 8 the avatar looked
   attached to the WhatsApp button and both looked attached to the CTA. */
/* The controls sit 10px apart from each other; the brand is separated by
   .topbar-inner's own 26px gap, so the cluster reads as one group beside the
   logo rather than four things in a row ending in a wordmark. Deliberately
   no extra margin here — that gap is already the separation, and adding to
   it is how one seam ends up wider than every other for no stated reason. */
/* The right-hand grid column. `justify-self: end` replaces the old
   `margin-left: auto` — under grid that margin would fight the track and
   leave the cluster floating in the middle of its own column. */
.topbar-actions { display: flex; align-items: center; gap: 10px; justify-self: end; min-width: 0; }

.topbar-search { position: relative; }
.topbar-search svg {
  position: absolute; left: 10px; top: 50%; transform: translateY(-50%);
  width: 15px; height: 15px; color: var(--n-400); pointer-events: none;
}
.topbar-search input {
  width: 170px; padding: 8px 12px 8px 31px;
  font: inherit; font-size: 13.5px;
  color: var(--n-900); background: var(--n-0);
  border: 1px solid var(--n-200); border-radius: 7px;
  transition: width .22s cubic-bezier(.16,1,.3,1), border-color .16s ease, box-shadow .16s ease;
}
.topbar-search input::placeholder { color: var(--n-400); }
.topbar-search input:focus {
  outline: none; width: 230px;
  border-color: var(--brand-400); background: var(--n-0);
  box-shadow: 0 0 0 3px var(--ring);
}
@media (max-width: 1080px) { .topbar-search { display: none; } }

/* ---- the right-hand control cluster ----
   Theme toggle, WhatsApp and the account avatar are ONE ROW OF EQUAL
   CONTROLS: same 34px box, same radius, aligned on the same centre line.
   They were 34px square, 30px circle and 30px circle respectively, which is
   what made the corner look unorganised — three sizes and two shapes in
   three adjacent controls reads as three things that arrived separately.
   --ctl-size is the single knob; auth.js's injected styles read the same
   variables, so its two buttons cannot drift from this one. */
:root { --ctl-size: 34px; --ctl-radius: 9px; }

.icon-btn {
  display: grid; place-items: center;
  width: var(--ctl-size); height: var(--ctl-size); flex: 0 0 auto;
  border: 1px solid var(--n-200); border-radius: var(--ctl-radius);
  background: var(--n-0); color: var(--n-500);
  transition: color .16s ease, background .16s ease, border-color .16s ease;
}
.icon-btn svg { width: 17px; height: 17px; }
.icon-btn:hover { color: var(--n-900); border-color: var(--n-300); }
.is-hidden { display: none !important; }

/* auth.js appends the WhatsApp button and the avatar here; the gap is the
   same 10px they need from the theme toggle, so the three sit evenly rather
   than the pair looking glued together. */
.va-host { display: flex; align-items: center; gap: 10px; }

.topbar-cta {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 8px 13px; border-radius: 7px;
  background: var(--brand-600); color: #fff;
  font-size: 13.5px; font-weight: 600; text-decoration: none; white-space: nowrap;
  transition: background .16s ease, transform .12s ease;
}
.topbar-cta svg { width: 15px; height: 15px; }
.topbar-cta:hover { background: var(--brand-700); }
.topbar-cta:active { transform: translateY(1px); }
@media (max-width: 720px) { .topbar-cta span { display: none; } .topbar-cta { padding: 8px 10px; } }

/* ---- THE BAR TAKES TWO ROWS ON A PHONE ----
   Below ~600px the brand, the tab strip and the account cluster genuinely do
   not fit one 64px row: the side 1fr tracks squeeze below their clusters'
   fixed-size children (flex: 0 0 auto controls cannot shrink), and both
   clusters painted OVER the centred tabs — 15–39px of overlap signed out,
   ~100px signed in, on EVERY page. An overflow check never sees it because
   nothing widens the document; it took an element-overlap probe to catch.
   So: brand + actions share the first row, and the tabs get a full-width
   second row, centred — the same trade the admin console made at 1279px for
   the same reason. MUST SIT AFTER every base topbar rule in this sheet: it
   overrides same-specificity declarations (justify-self on .tabs) and wins
   only by source order. */
@media (max-width: 600px) {
  /* FIXED, SOLID, and space-reserved — the phone bar must not move a pixel.
     Sticky + backdrop-blur is janky on iOS Safari: the blurred layer
     repaints a frame late during scroll, so the bar (and the toolbar under
     it) visibly bounced and seemed to vanish mid-scroll — the founder
     reported it twice. position: fixed paints on its own layer and does
     not lag; the background goes SOLID (--n-0, right in both themes)
     because the blur was the expensive part; and the body reserves the
     bar's measured height via --topbar-h, which layout.js keeps current.
     :has(.topbar) guards intro, which loads this sheet but carries its own
     header and must not inherit a phantom padding. */
  .topbar {
    position: fixed; top: 0; left: 0; right: 0;
    background: var(--n-0);
    backdrop-filter: none; -webkit-backdrop-filter: none;
  }
  body:has(.topbar) { padding-top: var(--topbar-h, 116px); }
  /* The LAST bounce: iOS rubber-band overscroll drags the whole page —
     fixed bar included — past its edges. overscroll-behavior kills the
     elastic where the engine supports it, and the html ground matches the
     bar so any overscroll that survives shows the page's own colour
     instead of a flashing seam above the bar. */
  html { overscroll-behavior-y: none; background: var(--n-0); }
  .topbar-inner {
    grid-template-columns: minmax(0, 1fr) auto;
    height: auto; row-gap: 5px;
    /* The founder asked for air here: at 9px the brand and controls sat
       tight against the status bar and the tab row pressed the hairline. */
    padding-top: 13px;
  }
  .brand { grid-column: 1; grid-row: 1; }
  .topbar-actions { grid-column: 2; grid-row: 1; }
  /* THE STRIP IS STATIC: equal-width tracks, one per tab, no scroll. The
     flex+overflow version was draggable — signed in, the tenders count badge
     pushed it a few px past the row and the whole strip wiggled under a
     thumb, which the founder called out. grid-auto-flow makes every tab an
     equal column at ANY tab count (the fallback nav included), so the row
     always exactly fills its width and there is nothing to drag. */
  .tabs {
    grid-column: 1 / -1; grid-row: 2;
    justify-self: stretch;
    margin-top: 2px;
    display: grid; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr);
    gap: 0;
    overflow-x: hidden;
  }
  /* THE LABELS COME BACK on the phone's own tab row (founder call,
     2026-08-21: the phone is the laptop scaled, not a different app — a row
     of bare icons reads as a different product). The ≤760 clip exists for
     the 601–760 band, where the tabs share one row with the brand and the
     account cluster and genuinely have no room for words. Same specificity
     as the clip rule — order is what wins. The count badge is hidden here,
     deliberately: with it, the longest tab needs more than a quarter of a
     390px row, and an ellipsized label to make room for a number the page
     itself already shows is the wrong trade. */
  /* ICON OVER LABEL, the app tab-bar pattern. Side by side, icon + longest
     label need ~103px against an 89px quarter-row, so words truncated;
     words alone fit but the founder missed the icons ("the tab icon not
     there in phone"). Stacked, each needs ~24px of height and ~70px of
     width — both fit with room, so the phone keeps the icons AND the full
     words at every width down to ~330px. */
  .tab {
    flex-direction: column; justify-content: center; align-items: center;
    min-width: 0; padding: 7px 2px 9px; gap: 4px;
  }
  .tab svg { width: 19px; height: 19px; }
  .tab-label {
    position: static; width: auto; height: auto; max-width: 100%;
    overflow: hidden; clip-path: none;
    text-overflow: ellipsis; white-space: nowrap;
    font-size: 12px;
  }
  .tab-count { display: none; }
}

/* ---- THE PHONE RUNS THE LAPTOP AT 88% ----
   The founder's own framing: don't restructure for the phone, DECREASE THE
   SIZE so the laptop layout genuinely fits the width. `zoom` is the one
   property that scales every hardcoded px on these pages — type, padding,
   radii, the lot — uniformly, which px-based sheets cannot do via rem.
   Media queries keep evaluating against the REAL viewport, so every ≤600
   rule still applies; the content simply lays out at ~443 CSS px inside a
   390px screen, which is the extra air that was missing. The edge gutters
   also come UP (they bottomed out at 12–14px, so content touched the screen
   edge — the opposite of what a smaller rendering is for). `body .wrap`
   outranks vendoor.css's `.wrap` by specificity, not order — vendoor loads
   after this sheet. Admin never loads this sheet and is untouched. */
@media (max-width: 600px) {
  /* The zoom is on the CONTENT, never on body: body-level zoom scaled the
     sticky top bar too, which (a) rendered the tab labels tiny beside the
     unscalable-looking brand and controls, and (b) mis-anchored the sticky
     bar on iOS Safari — the founder's phone showed the tab row half-clipped
     under the brand row. The bar stays laptop-size; only the page under it
     scales. `.page-body` covers seven pages, `main#view` is the directory,
     `body > main` is intro. */
  .page-body, main#view, body > main { zoom: 0.88; }
  :root { --page-gutter: 16px; }
  body .wrap { padding-inline: 18px; }
}

/* ---- page body under the bar ---- */
.page-body {
  max-width: var(--page-max); margin-inline: auto;
  padding: clamp(16px, 2.4vw, 30px) var(--page-gutter) clamp(40px, 5vw, 64px);
}

/* ============================================================
   Motion
   ============================================================ */
@keyframes v-rise { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
@keyframes v-fade { from { opacity: 0; } to { opacity: 1; } }
@keyframes v-pop  { from { opacity: 0; transform: scale(.97); } to { opacity: 1; transform: none; } }

/* Page entrance. Applied to the whole body region rather than to each
   card, so one animation covers a page instead of forty. */
.page-body { animation: v-fade .3s ease both; }

/* Staggered entrance for a group. --i is set per item by the page. */
.rise { animation: v-rise .42s cubic-bezier(.16,1,.3,1) both; animation-delay: calc(var(--i, 0) * 55ms); }
.pop  { animation: v-pop  .3s cubic-bezier(.16,1,.3,1) both; animation-delay: calc(var(--i, 0) * 40ms); }

/* Hover lift, kept to 1px: 3px is theatrical and reads as a template. */
.lift { transition: transform .16s ease, border-color .16s ease, box-shadow .16s ease; }
.lift:hover { transform: translateY(-1px); border-color: var(--brand-300); }

/* A number that has just changed, marked so the eye finds it. */
@keyframes v-flash { 0% { background: var(--brand-100); } 100% { background: transparent; } }
.flash { animation: v-flash 1s ease both; border-radius: 4px; }

@media (prefers-reduced-motion: reduce) {
  .page-body, .rise, .pop, .flash { animation: none !important; }
  .tab::after { transition: none; }
}


/* ============================================================
   auth.js chrome inside the top bar
   ------------------------------------------------------------
   auth.js styles .va-* for admin's DARK navy nav: translucent white
   fills and white icons. Dropped into this bar they vanish — on a
   white bar a white button is invisible, which is not a styling nit
   but a missing feature, because the WhatsApp button is the only way
   back for anyone who pressed "Not now".

   Scoped to .topbar so admin.html keeps its own treatment.
   ============================================================ */
.topbar .va-nav-btn {
  background: var(--brand-600); color: #fff;
  padding: 8px 14px; border-radius: 7px;
  font-size: 13.5px; font-weight: 600;
  transition: background .16s ease;
}
.topbar .va-nav-btn:hover { background: var(--brand-700); }

.topbar .va-wa {
  width: 34px; height: 34px; margin-right: 0;
  background: var(--n-0);
  border: 1px solid var(--n-200);
  border-radius: 7px;
  transition: background .16s ease, border-color .16s ease;
}
.topbar .va-wa:hover { background: var(--brand-50); border-color: var(--brand-200); }
.topbar .va-wa svg { width: 17px; height: 17px; fill: var(--n-500); transition: fill .16s ease; }
/* auth.js styles .va-nav-btn for admin's DARK bar (white text on translucent
   white) — on the buyer shell's white topbar that made the signed-out Sign in
   button invisible. Here it is the primary action, so it wears the brand. */
.topbar .va-nav-btn { background: var(--brand-600); color: #fff; }
.topbar .va-nav-btn:hover { background: var(--brand-700); }
.topbar .va-wa:hover svg { fill: var(--brand-600); }
/* WhatsApp's own green is a third party's brand, not ours. "Connected" is
   told the way every other positive state is told here: filled brand. */
.topbar .va-wa.on { background: var(--brand-600); border-color: var(--brand-600); }
.topbar .va-wa.on svg { fill: #fff; }
/* The dot sits ON the button, so its ring must match the bar, not navy. */
.topbar .va-wa-dot { border-color: var(--n-0); background: var(--brand-600); }

.topbar .va-avatar {
  width: 34px; height: 34px;
  border-radius: 50%;
  background: var(--brand-600);
  font-size: 13px; font-weight: 700; color: #fff;
}

/* The account menu is a floating panel: it is one of the few things
   that genuinely should carry a shadow. auth.js hardcodes it light. */
/* auth.js's own styles read the tokens now, so the dark counterparts it
   used to need here would only restate what the tokens already flip. */
