/* ═══════════════════════════════════════════════════════════════════════════
   03-base.css — reset, layout and every component both portals share.
   BYTE-IDENTICAL in licence-server-portal and client-portal.

   Nothing in here may name a portal-specific colour or class. Where the two
   portals differ on purpose, this file consumes a token from 02-accent.css
   (--accent, --grad-hero, --shadow-card …); where they differ by accident, it
   consumes one of the four "pending reconciliation" tokens instead, so the
   divergence is visible in one place and Phase 7 can close it.

   Load order is 01 → 02 → 03 → 04 and is load-bearing. Two consequences:

     • Every @media in THIS file must override only selectors declared in this
       file, and sits at the end, after those base rules. A narrow-screen
       override placed above the rule it overrides loses at equal specificity.
     • 04-portal.css loads last, so it must not re-declare any property that
       one of the @media blocks below sets — it would win and silently kill the
       responsive override. Checked mechanically; see the Phase 2 commit.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── Reset + page ────────────────────────────────────────────────────────── */
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
body{font-family:var(--font-sans);background:var(--bg-page);
  color:var(--text);line-height:1.6;font-size:var(--text-lg);transition:background var(--dur-slow),color var(--dur-slow)}
.container{max-width:1280px;margin:0 auto;padding:0 var(--space-7)}
.hidden{display:none !important}

/* ── Sign-in gate ────────────────────────────────────────────────────────── */
.sso-gate{min-height:100vh;display:flex;align-items:center;justify-content:center;
  position:relative;background:var(--bg-page);padding:var(--space-7)}
.sso-gate > .sso-gate-toggle{position:absolute;top:20px;right:24px}
/* Fixed dark gradient, same as .hero, regardless of the light/dark toggle below —
   the logo's wordmark is white-on-dark by design and disappears on a light card. */
.sso-gate-card{background:var(--grad-hero);
  border-radius:var(--radius-md);padding:var(--space-9);max-width:380px;width:100%;
  text-align:center;box-shadow:var(--shadow-md)}
.sso-gate-logo{height:38px;margin-bottom:var(--space-7)}
.sso-gate-title{font-size:var(--text-xl);font-weight:700;color:#fff;margin-bottom:var(--space-7);line-height:1.15}
.sso-gate-title span{color:#7EC8E3}
.sso-gate-card .btn{text-decoration:none;display:inline-flex;align-items:center;
  justify-content:center;padding:var(--space-3) var(--space-7);font-size:var(--text-base)}

/* ── Utilities ───────────────────────────────────────────────────────────── */
.muted{color:var(--text-muted)} .small{font-size:var(--text-sm)}
/* `code` had a font-family but no box styling, so inline code was
   indistinguishable from surrounding text. (`.mono` lives in 04-portal.css —
   the two portals still disagree about whether it wraps.) */
code{font-family:var(--font-mono);font-size:var(--text-sm);
  background:var(--bg-surface2);padding:var(--space-1) var(--space-2);border-radius:var(--radius-xs)}
.mono{font-family:var(--font-mono);font-size:var(--text-sm)}
/* nowrap only inside a table, which is the reconciliation: the operator side
   applied it to every .mono so IDs and fingerprints could not break
   mid-token, but BOTH portals also use .mono on a <dd> inside a .kv grid,
   whose value column is 1fr — where nowrap forces the whole card to overflow
   instead of wrapping. A table has overflow-x to absorb it; a card does not. */
.table .mono{white-space:nowrap}

/* ── Hero ────────────────────────────────────────────────────────────────────
   Brand lockup: corporate mark, a divider, then the product name. Without the
   divider and the deliberate size/weight contrast, the logo's own "cognizant"
   wordmark and the product title sat at nearly the same optical size 12px apart
   and read as one run-on line. Brand / product / edition are three levels of
   information, so they get three levels of typographic treatment. */
.hero{background:var(--grad-hero);color:#fff;padding:var(--space-8) 0 var(--space-7);position:relative}
.hero::before{content:'';position:absolute;inset:0;opacity:.12;
  background-image:radial-gradient(circle at 1px 1px,#fff 1px,transparent 0);
  background-size:26px 26px}
.hero-inner{position:relative}
.hero-brand{display:flex;align-items:center;gap:var(--space-5);margin-bottom:var(--space-2)}
.hero-logo{height:28px;width:auto;display:block;flex-shrink:0}
.hero-rule{width:1px;height:36px;flex-shrink:0;background:rgba(255,255,255,.26)}
.hero-titles{display:flex;flex-direction:column;gap:var(--space-1);min-width:0}
.eyebrow{font-size:var(--text-xs);font-weight:600;letter-spacing:2px;text-transform:uppercase;
  color:#7EC8E3;margin-bottom:var(--space-2)}
.hero-eyebrow{font-size:var(--text-2xs);font-weight:700;letter-spacing:2.4px;text-transform:uppercase;
  color:rgba(126,200,227,.95);line-height:1.1}
.hero h1{font-size:var(--text-hero);font-weight:700;letter-spacing:-.2px;
  line-height:1.15;display:flex;align-items:center;gap:var(--space-3);flex-wrap:wrap}
.hero h1 span{color:#7EC8E3}
.hero h1 .hero-edition{font-size:var(--text-2xs);font-weight:700;letter-spacing:1.3px;
  text-transform:uppercase;color:#fff;background:rgba(255,255,255,.15);
  border:1px solid rgba(255,255,255,.3);border-radius:var(--radius-pill);padding:var(--space-2) var(--space-4);
  white-space:nowrap;position:relative;top:1px}
.hero-sub{font-size:var(--text-base);color:rgba(255,255,255,.75);max-width:620px;margin-top:var(--space-3)}
.hero-tagline{font-size:var(--text-sm);font-weight:600;letter-spacing:.4px;
  color:rgba(255,255,255,.6);margin-top:var(--space-3)}
.hero-actions{position:absolute;top:0;right:0;display:flex;align-items:center;gap:var(--space-3)}

/* ── Identity menu ───────────────────────────────────────────────────────── */
.who-am-i{position:relative}
.avatar-btn{display:flex;align-items:center;gap:var(--space-3);padding:var(--space-2) var(--space-4) var(--space-2) var(--space-2);border-radius:var(--radius-pill);
  border:1px solid rgba(255,255,255,.28);background:rgba(255,255,255,.1);cursor:pointer}
.avatar-btn:hover{background:rgba(255,255,255,.18)}
.avatar-circle{width:28px;height:28px;border-radius:50%;overflow:hidden;flex-shrink:0;
  display:flex;align-items:center;justify-content:center;background:rgba(255,255,255,.2);
  color:#fff;font-size:var(--text-sm);font-weight:700}
.avatar-img{width:100%;height:100%;object-fit:cover}
.avatar-name{font-size:var(--text-base);font-weight:700;color:#fff;white-space:nowrap;max-width:140px;
  overflow:hidden;text-overflow:ellipsis}
.avatar-menu{position:absolute;top:calc(100% + 10px);right:0;min-width:230px;
  background:var(--bg-surface);border:1px solid var(--border);border-radius:var(--radius-md);
  box-shadow:var(--shadow-md);padding:var(--space-3);z-index:var(--z-dropdown)}
.avatar-menu-header{padding:var(--space-3)}
.avatar-menu-name{font-size:var(--text-md);font-weight:700;color:var(--text)}
.avatar-menu-email{font-size:var(--text-sm);color:var(--text-muted);margin-top:var(--space-1);word-break:break-all}
/* Teal in both portals — the role pill reads as a Cognizant-issued fact, not a
   portal-local accent, so it deliberately does not follow --accent. Needs no
   dark patch now that --teal and --teal-light both theme. */
.avatar-menu-role{display:inline-block;margin-top:var(--space-3);padding:var(--space-1) var(--space-3);border-radius:var(--radius-pill);
  font-size:var(--text-xs);font-weight:700;letter-spacing:.3px;text-transform:uppercase;
  background:var(--teal-light);color:var(--teal)}
.avatar-menu-divider{height:1px;background:var(--border);margin:var(--space-1) 0 var(--space-2)}
.avatar-menu-item{display:flex;align-items:center;gap:var(--space-3);font-size:var(--text-base);font-weight:700;
  color:var(--text-muted);padding:var(--space-3);text-decoration:none;border-radius:var(--radius-sm)}
.avatar-menu-item:hover{background:var(--bg-surface2);color:var(--text)}
/* The identity menu holds links (Sign out) and actions (the timezone switch)
   side by side. A <button> arrives with its own chrome and centred text, so it
   needs stripping back to sit flush with the anchors next to it. */
button.avatar-menu-item{width:100%;background:none;border:none;font-family:inherit;
  text-align:left;cursor:pointer}
.avatar-menu-icon{flex-shrink:0}
.avatar-menu-signout{color:#E5484D}
.avatar-menu-signout:hover{background:color-mix(in srgb, #E5484D 12%, transparent);color:#E5484D}
.dm-btn{background:rgba(255,255,255,.12);
  border:1px solid rgba(255,255,255,.25);color:#fff;border-radius:var(--radius-pill);padding:var(--space-3) var(--space-5);
  font-size:var(--text-sm);font-weight:700;cursor:pointer;display:flex;align-items:center;gap:var(--space-2)}
.dm-btn:hover{background:rgba(255,255,255,.2)}

/* ── Tabs ────────────────────────────────────────────────────────────────── */
.tabs{background:color-mix(in srgb, var(--bg-surface) 88%, white 12%);
  border-bottom:1px solid var(--border);position:sticky;top:0;z-index:var(--z-sticky);
  box-shadow:var(--elev-bar) var(--shadow-bar-tint);transition:background var(--dur-slow)}
[data-theme="dark"] .tabs{background:var(--bg-surface);box-shadow:var(--shadow-sm)}
.tabs-inner{display:flex;align-items:center;gap:var(--space-1);overflow-x:auto}
.tab{background:none;border:none;border-bottom:2px solid transparent;padding:var(--space-4) var(--space-5);
  font-size:var(--text-base);font-weight:700;color:var(--text-muted);cursor:pointer;font-family:inherit;
  white-space:nowrap;transition:color var(--dur-fast),border-color var(--dur-fast)}
.tab:hover{color:var(--accent)}
.tab.is-active{color:var(--accent);border-bottom-color:var(--accent)}
.conn{margin-left:auto;display:flex;align-items:center;gap:var(--space-3);font-size:var(--text-sm);
  color:var(--text-muted);white-space:nowrap;padding-left:var(--space-4)}
.conn .dot{width:8px;height:8px;border-radius:50%;background:var(--text-muted);flex-shrink:0}
.conn.is-ok .dot{background:var(--green)} .conn.is-bad .dot{background:var(--red)}
/* A failed refresh leaves every table, seat count and licence card on screen
   looking current, so "offline" has to read as more than an 8px dot. */
.conn.is-bad{background:var(--red-light);color:var(--red);font-weight:700;
  border-radius:var(--radius-pill);padding:var(--space-1) var(--space-4)}

/* ── Layout ──────────────────────────────────────────────────────────────── */
.section{padding:var(--space-7) 0 var(--space-10)}
.subtabs{display:flex;gap:var(--space-3);flex-wrap:wrap;margin:0 0 var(--space-5) 0}
.subtab{background:var(--bg-surface);border:1px solid var(--border);color:var(--text-muted);
  border-radius:var(--radius-pill);padding:var(--space-3) var(--space-5);font-size:var(--text-sm);font-weight:700;cursor:pointer;
  transition:all var(--dur-fast) var(--ease);font-family:inherit}
.subtab:hover{border-color:var(--accent);color:var(--accent)}
.subtab.is-active{background:var(--accent);border-color:var(--accent);color:var(--on-fill)}
.subview{animation:fadein var(--dur) var(--ease)}
.grid-2{display:grid;grid-template-columns:1fr 1fr;gap:var(--space-5);margin-bottom:var(--space-6)}
.card{background:var(--bg-surface);border:1px solid color-mix(in srgb, var(--border) 90%, #fff 10%);
  border-radius:var(--radius-md);padding:var(--space-6);
  box-shadow:var(--elev-card) var(--shadow-card-tint);margin-bottom:var(--space-6);
  transition:background var(--dur-slow),box-shadow var(--dur)}
/* Hover raises the shadow only. The old rule also lifted the card 1px, which
   made a pointer sweep across a dense page twitch row by row. */
.card:hover{box-shadow:var(--elev-card-hover) var(--shadow-card-hover-tint)}
/* In dark, a shadow against a near-black page is invisible, so the surface is
   separated by its 1px border and the --bg-surface step above --bg-page. */
[data-theme="dark"] .card,[data-theme="dark"] .card:hover{box-shadow:none}
.card-head{display:flex;align-items:center;gap:var(--space-4);flex-wrap:wrap;margin-bottom:var(--space-5);
  padding-bottom:var(--space-4);border-bottom:1px solid var(--border)}
.card-head h2{font-size:var(--text-md);font-weight:700;color:var(--heading)}
.head-actions{margin-left:auto;display:flex;align-items:center;gap:var(--space-3)}
/* All three get the auto-margin: at one point only .muted did, so a .badge or
   .btn in a card-head didn't right-align. */
.card-head>.badge,.card-head>.muted,.card-head>.btn{margin-left:auto}
.card-head .head-actions>*{margin-left:0}
/* Explanatory paragraph under a card-head. Replaces inline style attributes,
   which are also the reason style-src would otherwise need 'unsafe-inline'. */
.card-intro{font-size:var(--text-base);margin-bottom:var(--space-4)}

/* ── Callouts ────────────────────────────────────────────────────────────── */
.callout{border-left:4px solid var(--orange);background:var(--orange-light);color:var(--orange);
  border-radius:0 var(--radius-sm) var(--radius-sm) 0;padding:var(--space-4) var(--space-6);margin-bottom:var(--space-6);
  font-size:var(--text-base)}
.callout-blue{background:var(--light-blue);border-left-color:var(--mid-blue);color:var(--heading)}

/* ── Form controls ───────────────────────────────────────────────────────────
   One rule for every text-entry control, in both portals. These used to live
   in 04-portal.css because the two sides disagreed about whether textarea was
   covered — server included it, client did not.

   The types are named rather than using a bare `input` so checkboxes keep the
   native control. 400-weight, not 600: user-entered data at semibold read
   like a heading rather than like an editable value, and it also resolves the
   divergence, since client textareas were already 400 (their .token-box never
   declared a weight and the base rule never reached them).

   .mini must stay after the base rule: `input.mini` and `input[type=text]`
   are both specificity (0,1,1), a tie broken only by source order. */
input[type=text],input[type=number],input[type=password],select,textarea{
  border:1px solid var(--border);
  border-radius:var(--radius-sm);padding:var(--space-3) var(--space-4);
  font-size:var(--text-lg);font-family:inherit;font-weight:400;
  background:var(--bg-surface);color:var(--text)}
input[type=text]:focus,input[type=number]:focus,input[type=password]:focus,select:focus,
textarea:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-ring)}
select.mini,input.mini{padding:var(--space-2) var(--space-3);font-size:var(--text-sm);font-weight:700}
/* Field label. Was 12px/700 UPPERCASE with letter-spacing — the treatment that
   made every form read like a tax return. Sentence case at 13px/600 carries
   the same hierarchy without shouting. */
label{display:flex;flex-direction:column;gap:var(--space-2);font-size:var(--text-base);
  font-weight:600;color:var(--text-muted)}
.chk{flex-direction:row;align-items:center;gap:var(--space-3);font-size:var(--text-sm);
  color:var(--text-muted);font-weight:600}

/* ── Field help ──────────────────────────────────────────────────────────────
   One class for every hint and validation message under a control, replacing
   .field-note, .id-hint, .code-hint and .trust-warn — four spellings of the
   same thing, split across both portals, with three different state
   vocabularies between them.

   State is carried by a custom property rather than by overriding colour, so
   adding a state means declaring one value instead of restating the rule.
   min-height reserves the line so a message appearing or clearing does not
   shift the form under the pointer. margin-top only bites on the one block
   usage; the others are inline spans, where it does not apply. */
.field-help{font-size:var(--text-sm);line-height:1.4;min-height:16px;
  margin-top:var(--space-2);color:var(--field-state,var(--text-muted))}
.field-help.is-ok{--field-state:var(--green)}
.field-help.is-warn{--field-state:var(--orange)}
.field-help.is-err{--field-state:var(--red)}
/* A standing note, not a validation result: no reserved line, and italic to
   separate guidance from feedback. */
.field-help.is-note{font-style:italic;min-height:0}

/* ── Buttons ─────────────────────────────────────────────────────────────── */
.btn{border-radius:var(--radius-sm);padding:var(--space-3) var(--space-5);font-size:var(--text-sm);font-weight:700;
  cursor:pointer;font-family:inherit;border:1px solid var(--border);background:var(--bg-surface);
  color:var(--text-muted);white-space:nowrap;
  transition:background var(--dur-fast),color var(--dur-fast),border-color var(--dur-fast)}
/* Hover is a background shift and nothing else. It used to lift 1px and cast
   an accent-tinted shadow, which read as a card rather than a control, and
   dark mode had to switch both back off — so the dark override is gone too. */
.btn:hover{background:var(--accent-soft);color:var(--accent-ink);border-color:var(--accent)}
.btn:disabled{opacity:.5;cursor:not-allowed}
.btn-primary{background:var(--accent-primary-bg);border-color:var(--accent-primary-bg);
  color:var(--on-fill)}
.btn-primary:hover{background:var(--accent-primary-hover);
  border-color:var(--accent-primary-hover);color:var(--on-fill)}
.btn-danger{color:var(--red);border-color:var(--red)}
.btn-danger:hover{background:var(--red);color:var(--on-fill)}
.btn-sm{padding:var(--space-2) var(--space-3);font-size:var(--text-xs)}
.btn-ghost{background:transparent}
/* Pressed feedback — now the only transform on a button, and it fires in both
   themes. Dark used to switch it off, which was collateral from suppressing
   the hover lift rather than a decision about the press. */
.btn:active{transform:scale(.96)}

/* ── Tables ──────────────────────────────────────────────────────────────────
   Cells are top-aligned because any cell here can wrap (middle-aligning the
   rest looks unbalanced when one does), and action buttons wrap rather than
   forcing the whole table into horizontal scroll. */
/* The wrap carries the border and the radius so the grid reads as one object
   rather than as loose rows. overflow-x:auto already establishes a clipping
   context, so the rounded corners crop the header fill without needing
   overflow:hidden — which would have killed horizontal scrolling outright. */
.table-wrap{overflow-x:auto;border:1px solid var(--border);border-radius:var(--radius-md)}
.table{width:100%;border-collapse:collapse;min-width:860px;
  font-variant-numeric:tabular-nums}
/* Flat header. This was a white-on-navy linear-gradient, the single most dated
   element in either portal: it framed the data as a report banner instead of
   letting the data lead. --bg-surface2 themes itself, so the dark override it
   used to need is gone. */
.table thead th{background:var(--bg-surface2);color:var(--text-muted);padding:var(--space-4);
  font-size:var(--text-sm);font-weight:700;text-align:left;white-space:nowrap;
  border-bottom:2px solid var(--border)}
.table tbody td{padding:var(--space-4);font-size:var(--text-base);line-height:1.5;border-bottom:1px solid var(--border);
  color:var(--text);vertical-align:top}
/* Zebra striping is gone: with a hairline under every row it was doing the
   same job twice, and the tint fought the status badges for attention. The
   last row drops its rule so it does not double up with the wrap's border. */
.table tbody tr:last-child td{border-bottom:none}
.table tbody tr:hover td{background:var(--accent-soft)}
.table .empty-row td{text-align:center;color:var(--text-muted);font-style:italic;padding:var(--space-6)}
.table td.actions{white-space:normal;text-align:right;display:flex;flex-wrap:wrap;
  justify-content:flex-end;align-items:flex-start;gap:var(--space-2)}

.pager{display:flex;align-items:center;justify-content:space-between;gap:var(--space-3);
  padding-top:var(--space-4);margin-top:var(--space-2);border-top:1px solid var(--border)}
.pager-controls{display:flex;align-items:center;gap:var(--space-3)}

/* ── Badges ──────────────────────────────────────────────────────────────────
   Base pill plus the lifecycle states both portals render. Portal-only states
   (order workflow here, seat outcomes there) are in 04-portal.css. */
.badge{display:inline-block;padding:var(--space-1) var(--space-3);border-radius:var(--radius-pill);font-size:var(--text-xs);font-weight:700;
  text-transform:uppercase;letter-spacing:.4px;background:var(--bg-surface2);color:var(--text-muted);
  white-space:nowrap;border:1px solid color-mix(in srgb, currentColor 32%, transparent)}
/* Leading dot and border, both derived from the state's own colour via
   currentColor — so every state below gets them for free by setting `color`,
   and a new state cannot forget them. States now read by hue + outline + tint
   instead of by fill saturation alone, which is what made a row of eight
   pastel pills hard to scan.

   Kept as inline-block with an inline-block dot rather than switching to
   inline-flex: a flex container takes its baseline from its first flex item,
   which would be the empty dot, and badges sit inline beside text in table
   cells and card heads where that shift would be visible. */
.badge::before{content:'';display:inline-block;width:6px;height:6px;border-radius:50%;
  background:currentColor;margin-right:var(--space-2);vertical-align:middle}
.badge-active,.badge-ok{background:var(--green-light);color:var(--green)}
.badge-renewal_due,.badge-renewal_urgent{background:var(--orange-light);color:var(--orange)}
.badge-locked{background:var(--red-light);color:var(--red)}
.badge-suspended{background:var(--purple-light);color:var(--purple)}
/* app.js renders badge-${lifecycle.state}, and LIFECYCLE.PENDING is 'pending',
   so this class was always reachable — it just had no rule, leaving a
   not-yet-started licence with the grey default while .banner-pending existed
   and said otherwise. */
.badge-pending{background:var(--light-blue);color:var(--heading)}

/* ── Key/value ───────────────────────────────────────────────────────────────
   The value column read ragged when left-aligned against an auto-width label
   column — right-aligning gives the pairs a consistent right edge. (`.kv dd`
   is in 04-portal.css: the portals use two different wrap idioms.) */
.kv{display:grid;grid-template-columns:auto 1fr;gap:var(--space-3) var(--space-5);font-size:var(--text-base)}
.kv dt{color:var(--text-muted);font-weight:600;white-space:nowrap}
/* word-break:normal + overflow-wrap is the standard spelling of what
   client-portal wrote as the deprecated word-break:break-word alias. Same
   behaviour, one idiom. */
.kv dd{color:var(--text);font-weight:600;text-align:right;word-break:normal;
  overflow-wrap:break-word;font-variant-numeric:tabular-nums}

/* ── Token box ───────────────────────────────────────────────────────────── */
/* No background of its own: it inherits var(--bg-surface) from the shared
   control rule, like every other input. The two portals used to disagree here
   — grey on the operator side, white on the buyer side — for no reason. */
.token-box{width:100%;font-family:var(--font-mono);font-size:var(--text-sm);
  line-height:1.5;word-break:break-all;resize:vertical;
  color:var(--text);border:1px solid var(--border);border-radius:var(--radius-sm);
  padding:var(--space-3)}
.token-box:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-ring)}

/* ── Results ─────────────────────────────────────────────────────────────── */
.result{margin-top:var(--space-4);border-radius:var(--radius-sm);padding:var(--space-4) var(--space-5);font-size:var(--text-base);
  border-left:3px solid}
.result-ok{background:var(--green-light);border-left-color:var(--green);color:var(--green)}
.result-warn{background:var(--orange-light);border-left-color:var(--orange);color:var(--orange)}
.result-err{background:var(--red-light);border-left-color:var(--red);color:var(--red)}
.result strong{display:block;margin-bottom:var(--space-1)}
.result ul{margin:var(--space-2) 0 0 var(--space-5)}

/* ── Activity feed ───────────────────────────────────────────────────────── */
.feed{list-style:none;max-height:340px;overflow-y:auto}
.feed li{padding:var(--space-3) 0;border-bottom:1px solid var(--border);font-size:var(--text-base);display:flex;
  gap:var(--space-4);align-items:baseline}
.feed li:last-child{border-bottom:none}
.feed .f-time{color:var(--text-faint);font-size:var(--text-sm);white-space:nowrap;min-width:112px;
  font-variant-numeric:tabular-nums}
.feed .f-act{font-weight:700;color:var(--heading);min-width:170px}
.feed .f-detail{color:var(--text-muted);font-size:var(--text-sm);flex:1;word-break:break-word}
.feed .f-empty{color:var(--text-muted);font-style:italic}

/* ── Toasts — click-to-dismiss (see toast() in app.js) ───────────────────── */
.toasts{position:fixed;bottom:20px;right:20px;display:flex;flex-direction:column;gap:var(--space-3);z-index:var(--z-toast)}
.toast{background:var(--bg-surface);border:1px solid var(--border);border-left:3px solid var(--accent);
  border-radius:var(--radius-sm);padding:var(--space-4) var(--space-5);font-size:var(--text-base);box-shadow:var(--shadow-md);
  max-width:420px;color:var(--text);animation:slidein var(--dur) var(--ease);cursor:pointer}
.toast-ok{border-left-color:var(--green)} .toast-err{border-left-color:var(--red)}
/* toast('...', 'warn') is called from app.js; without this rule warnings
   silently rendered with the default accent instead of orange. */
.toast-warn{border-left-color:var(--orange)}
@keyframes slidein{from{transform:translateX(20px);opacity:0}to{transform:none;opacity:1}}
@keyframes fadein{from{opacity:.28;transform:translateY(3px)}to{opacity:1;transform:none}}

/* ── Confirmation dialogs ────────────────────────────────────────────────────
   Both portals ask for a decision the same way (see askModal in app.js). */
.modal-backdrop{position:fixed;inset:0;z-index:var(--z-modal);background:rgba(9,17,28,.52);
  backdrop-filter:blur(4px);display:grid;place-items:center;padding:var(--space-6)}
.modal-card{width:min(520px,100%);background:var(--bg-surface);border:1px solid var(--border);
  border-radius:var(--radius-lg);box-shadow:var(--shadow-md);padding:var(--space-5)}
.modal-title{font-size:var(--text-xl);line-height:1.25;color:var(--heading);font-weight:700}
.modal-message{margin-top:var(--space-3);color:var(--text-muted);font-size:var(--text-md);word-break:break-word}
.modal-body{margin-top:var(--space-5)}
.modal-input-label{display:flex;flex-direction:column;gap:var(--space-2);
  font-size:var(--text-base);font-weight:600;color:var(--text-muted)}
/* .modal-input carries no styling of its own: every type askModal can create
   (text, number, password) is covered by the shared control rule above, which
   out-specifies a bare class. The class is still set in app.js as a hook. */
.modal-actions{margin-top:var(--space-5);display:flex;justify-content:flex-end;gap:var(--space-3)}
.modal-field{margin-top:var(--space-4)}
.modal-field-hint{font-weight:500;font-size:var(--text-sm);color:var(--text-faint);line-height:1.45}
/* Before/after panel for a multi-field decision (see askModal's `summary`).
   The grid lives on the container, not on each row, and rows are
   display:contents -- otherwise every row is its own grid and the from/arrow/to
   columns size independently, so nothing lines up down the panel. The cost is
   that rows cannot paint a border, hence the row gap doing that job. */
.modal-summary{margin-top:var(--space-5);display:grid;
  grid-template-columns:1fr auto auto auto;gap:var(--space-3);
  border:1px solid var(--border);border-radius:var(--radius-sm);
  background:var(--bg-surface2);padding:var(--space-4);font-size:var(--text-base)}
.modal-summary-row{display:contents}
.modal-summary .ms-label{color:var(--text-muted);font-weight:600}
.modal-summary .ms-from,.modal-summary .ms-arrow{color:var(--text-faint);
  font-variant-numeric:tabular-nums}
.modal-summary .ms-to{text-align:right;font-variant-numeric:tabular-nums}
/* Rows that are not changing stay quiet, so the ones that are carry the eye. */
.modal-summary-row .ms-to{color:var(--text-muted);font-weight:600}
.modal-summary-row.is-changed .ms-to{color:var(--accent);font-weight:700}
.modal-error{margin-top:var(--space-4);color:var(--red);font-size:var(--text-sm);line-height:1.45}

/* ── Footer. ~3.7:1 for the copyright line on --dark-blue (now 11px). ───── */
.footer{background:var(--footer-bg);color:rgba(255,255,255,.72);padding:var(--space-6) 0;font-size:var(--text-sm);
  text-align:center}
.footer strong{color:rgba(255,255,255,.92)}
.footer-copyright{margin-top:var(--space-3);font-size:var(--text-xs);color:rgba(255,255,255,.68)}

/* ── Polish: focus visibility, scrollbars, loading state ─────────────────── */

::selection{background:var(--accent);color:var(--on-fill)}

/* Keyboard-only focus ring — mouse/touch users keep relying on the :hover
   states above this; :focus-visible only engages for keyboard navigation. */
:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:var(--radius-xs)}
.dm-btn:focus-visible,.avatar-btn:focus-visible{outline-color:#fff}

/* Themed scrollbars for the two scrolling regions that otherwise fall back
   to the raw OS scrollbar, which looks out of place in dark mode. */
.feed,.table-wrap{scrollbar-width:thin;scrollbar-color:var(--accent) transparent}
.feed::-webkit-scrollbar,.table-wrap::-webkit-scrollbar{height:8px;width:8px}
.feed::-webkit-scrollbar-thumb,.table-wrap::-webkit-scrollbar-thumb{
  background:var(--border);border-radius:var(--radius-pill)}
.feed:hover::-webkit-scrollbar-thumb,.table-wrap:hover::-webkit-scrollbar-thumb{
  background:var(--accent)}

/* Shown by default (see index.html) for however long boot()'s /config.json +
   /auth/me calls take, so that wait is a branded moment rather than a blank
   white page — hidden by the same boot() once it knows whether to show the
   SSO gate or the app shell, so it never flashes alongside either. */
.page-loader{min-height:100vh;display:flex;flex-direction:column;align-items:center;
  justify-content:center;gap:var(--space-5);background:var(--bg-page)}
.page-loader-logo{height:34px;opacity:.9}
.page-loader-spinner{width:26px;height:26px;border-radius:50%;
  border:3px solid var(--border);border-top-color:var(--accent);
  animation:spin .8s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}

/* ── Conditional at-rules ────────────────────────────────────────────────────
   All of this file's, in one place, at the very end. Ordered widest-first so
   the narrower override wins, and placed after every base rule they override —
   e.g. .card's 640px padding must come after .card itself. Portal-only
   components have their own at-rules at the end of 04-portal.css. */

/* Compact density tier. A laptop viewport lands just under the container's
   max-width, so a full table hit its min-width floor and scrolled sideways
   even though the columns would nearly have fitted. In this band the floor is
   released and the cells tighten by one spacing step, which is usually enough
   to fit. Bounded at the bottom by min-width:861px on purpose: below that the
   floor and the horizontal scroll stay exactly as they are today, because
   removing the floor at phone widths squeezes nine columns into mush. */
@media (min-width:861px) and (max-width:1100px){
  .table{min-width:0}
  .table thead th,.table tbody td{padding:var(--space-3)}
  .table tbody td{font-size:var(--text-sm)}
}

@media(max-width:900px){
  .grid-2{grid-template-columns:1fr}
}

@media(max-width:640px){
  .hero-brand{gap:var(--space-4)}
  .hero-rule{display:none}
  .hero-logo{height:22px}
  /* Absolutely positioned at top:0/right:0, so it overlapped the brand block
     — worse now the title is two lines. Put it back in flow instead. */
  .hero-actions{position:static;margin-top:var(--space-5);justify-content:flex-start}
  /* .f-time (112px) + .f-act (170px) + gaps exceeds the width available
     inside a card at this size, so rows squashed instead of wrapping. */
  .feed li{flex-wrap:wrap;gap:var(--space-1) var(--space-4)}
  .feed .f-time,.feed .f-act{min-width:0}
  /* A fixed 420px toast pinned 20px from the right overflows a 360px screen. */
  .toasts{left:12px;right:12px;bottom:12px}
  .toast{max-width:none}
  .container{padding:0 var(--space-5)}
  .card{padding:var(--space-5)}
  .section{padding:var(--space-6) 0 var(--space-8)}
}

@media (prefers-reduced-motion: reduce){
  .card,.btn{transition:none}
  .btn:hover,.btn:active{transform:none}
  .subview,.toast{animation:none}
  .page-loader-spinner{animation-duration:1.6s}
}
