/* ─────────────────────────────────────────────────────────────────────────────
   Sunset — the design system for this portal.

   Implements the Mistral DESIGN.md tokens: the sunset palette, cream surfaces,
   the editorial/sans type pairing, sober 8px/12px geometry, flat elevation, and
   the signature sunset stripe band.

   ── NO EXTERNAL REQUESTS, DELIBERATELY ───────────────────────────────────────
   This page is the thing you open when the system is NOT yet working: no key,
   no profile, possibly no network. A webfont from a CDN would mean the setup
   page renders in Times New Roman exactly when someone is least sure whether
   they have installed it correctly. So the DESIGN.md fallback stacks are used
   as the primary stacks.

   PP Editorial Old is a licensed commercial face and cannot be embedded here
   anyway; DESIGN.md names Georgia as its fallback, and Georgia is a genuine
   near-serif with the classical character the brand voice depends on. Inter
   falls through to system-ui, which is Segoe UI on Windows and SF on macOS —
   both close enough that the editorial/sans CONTRAST, which is the actual brand
   signal, survives intact.
   ───────────────────────────────────────────────────────────────────────────── */

:root {
  /* ── Brand & accent ──────────────────────────────────────────────────────
     Saturated orange is reserved for primary CTAs, active states and the
     sunset stripe. Using it for anything else is what turns a considered
     palette into a loud one. */
  --primary: #fa500f;
  --primary-deep: #e14504;
  --on-primary: #ffffff;

  --sunshine-300: #ffd24a;
  --sunshine-500: #ffaf20;
  --sunshine-700: #ff8205;
  --sunshine-800: #f06400;
  --sunshine-900: #d94600;
  --yellow-saturated: #ffd800;

  /* ── Cream / warm neutral ─────────────────────────────────────────────── */
  --cream: #fff4d6;
  --cream-soft: #fffaeb;
  --cream-deeper: #ffe9b0;
  --beige-deep: #ecdcae;
  --footer-cream: #fff8e3;

  /* ── Surface ──────────────────────────────────────────────────────────── */
  --canvas: #ffffff;
  --surface: #fafaf8;
  --surface-cream: #fffbf0;
  --surface-code: #14100c;

  --hairline: #e6e2da;
  --hairline-soft: #f0ece4;
  --hairline-strong: #d6d0c4;

  /* ── Text ─────────────────────────────────────────────────────────────── */
  --ink: #1a1613;
  --ink-tint: #2b2520;
  --charcoal: #3d3630;
  --slate: #5c534a;
  --steel: #7a7168;
  --stone: #9a9188;
  --muted: #b5ada4;
  --on-dark: #ffffff;
  --on-dark-muted: rgba(255, 255, 255, 0.62);
  --on-cream: var(--ink);
  --link: var(--primary);

  /* ── Semantic status ──────────────────────────────────────────────────────
     DESIGN.md says not to introduce accents beyond the sunset palette. Applied
     and failed still have to be distinguishable at a glance on a dashboard, so
     these are the minimum: a desaturated green and a red pulled toward the warm
     end so they sit inside the palette rather than fighting it. */
  --ok: #2f7d55;
  --ok-surface: #eaf4ee;
  --bad: #c2381b;
  --bad-surface: #fdeeea;
  --warn: var(--sunshine-800);
  --warn-surface: #fff2df;

  /* ── Typography ───────────────────────────────────────────────────────── */
  --font-display: 'PP Editorial Old', 'Times New Roman', Georgia, serif;
  --font-ui: 'Inter', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif;
  --font-mono: 'JetBrains Mono', 'SF Mono', Menlo, Consolas, monospace;

  /* ── Spacing — 4px base, 8px primary increment ────────────────────────── */
  --xxs: 4px;  --xs: 8px;   --sm: 12px;  --md: 16px;
  --lg: 20px;  --xl: 24px;  --xxl: 32px; --xxxl: 40px;
  --section-sm: 48px; --section: 64px; --section-lg: 96px; --hero: 120px;

  /* ── Radius — sober and editorial. Pills are for badges only. ─────────── */
  --r-xs: 4px; --r-sm: 6px; --r-md: 8px; --r-lg: 12px;
  --r-xl: 16px; --r-xxl: 20px; --r-full: 9999px;

  /* ── Elevation — predominantly flat ───────────────────────────────────── */
  --e1: rgba(0, 0, 0, 0.04) 0px 1px 2px 0px;
  --e2: rgba(0, 0, 0, 0.04) 0px 4px 12px 0px;
  --e3: rgba(0, 0, 0, 0.08) 0px 12px 24px -4px;
  --e4: rgba(0, 0, 0, 0.12) 0px 16px 48px -8px;
}

/* ── Reset ─────────────────────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; }

/* The `hidden` attribute is only `display: none` in the UA stylesheet, so ANY
   class that sets display beats it — .badge is inline-flex, which is how a
   hidden status pill rendered as a stray empty capsule. Specificity has to win
   here, not politeness. */
[hidden] { display: none !important; }

body {
  margin: 0;
  background: var(--canvas);
  color: var(--ink);
  font-family: var(--font-ui);
  font-size: 16px;
  line-height: 1.55;             /* generous leading, per the editorial brief */
  -webkit-font-smoothing: antialiased;
}

/* ── Type scale ────────────────────────────────────────────────────────────
   Negative letter-spacing progresses WITH size; smaller heads relax to 0. The
   display sizes are Editorial, everything else Inter — that contrast is the
   brand voice and is not interchangeable. */
.t-hero    { font-family: var(--font-display); font-size: 84px; font-weight: 400; line-height: 1.05; letter-spacing: -1.5px; }
.t-display { font-family: var(--font-display); font-size: 64px; font-weight: 400; line-height: 1.10; letter-spacing: -1px; }
.t-h1      { font-family: var(--font-display); font-size: 52px; font-weight: 400; line-height: 1.15; letter-spacing: -0.5px; }
.t-stat    { font-family: var(--font-display); font-size: 56px; font-weight: 400; line-height: 1.10; letter-spacing: -1px; }
.t-h2      { font-size: 36px; font-weight: 500; line-height: 1.20; letter-spacing: -0.5px; }
.t-h3      { font-size: 28px; font-weight: 500; line-height: 1.25; }
.t-h4      { font-size: 22px; font-weight: 500; line-height: 1.30; }
.t-h5      { font-size: 18px; font-weight: 500; line-height: 1.40; }
.t-subtitle{ font-size: 18px; font-weight: 400; line-height: 1.50; }
.t-body    { font-size: 16px; font-weight: 400; line-height: 1.55; }
.t-body-m  { font-size: 16px; font-weight: 500; line-height: 1.55; }
.t-sm      { font-size: 14px; font-weight: 400; line-height: 1.50; }
.t-sm-m    { font-size: 14px; font-weight: 500; line-height: 1.50; }
.t-caption { font-size: 13px; font-weight: 400; line-height: 1.40; }
.t-caption-b { font-size: 13px; font-weight: 600; line-height: 1.40; }
.t-micro   { font-size: 12px; font-weight: 500; line-height: 1.40; }
.t-eyebrow { font-size: 11px; font-weight: 600; line-height: 1.40; letter-spacing: 1px; text-transform: uppercase; }
.t-code    { font-family: var(--font-mono); font-size: 14px; font-weight: 400; line-height: 1.50; }

.c-ink { color: var(--ink); }  .c-slate { color: var(--slate); }
.c-steel { color: var(--steel); } .c-stone { color: var(--stone); }
.c-primary { color: var(--primary); }

/* ── Buttons — 8px, never pills ────────────────────────────────────────── */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--xs);
  font-family: var(--font-ui); font-size: 14px; font-weight: 500; line-height: 1.30;
  padding: 10px 20px;            /* 40-44px effective height, WCAG AAA floor */
  border-radius: var(--r-md);
  border: 1px solid transparent;
  cursor: pointer;
  transition: background 160ms ease, border-color 160ms ease, color 160ms ease;
  text-decoration: none;
  white-space: nowrap;
}
.btn:disabled { background: var(--hairline); color: var(--muted); border-color: transparent; cursor: default; }

.btn-primary   { background: var(--primary); color: var(--on-primary); }
.btn-primary:not(:disabled):hover  { background: var(--primary-deep); }
.btn-dark      { background: var(--ink); color: var(--on-dark); }
.btn-dark:not(:disabled):hover     { background: var(--charcoal); }
.btn-cream     { background: var(--cream); color: var(--ink); border-color: var(--beige-deep); }
.btn-cream:not(:disabled):hover    { background: var(--cream-deeper); }
.btn-secondary { background: transparent; color: var(--ink); border-color: var(--hairline-strong); }
.btn-secondary:not(:disabled):hover{ background: var(--surface); }
.btn-on-cream  { background: var(--canvas); color: var(--ink); border-color: var(--beige-deep); }
/* nowrap because these live inside flex rows next to badges. As a shrinkable
   flex item the label broke mid-phrase and stranded the trailing arrow on its
   own line. */
.btn-link      { background: none; border: none; color: var(--primary); padding: 0;
                 font-weight: 500; white-space: nowrap; flex-shrink: 0; }
.btn-link:hover{ text-decoration: underline; }
/* .btn-link is not a .btn, so it misses .btn:disabled entirely and a disabled
   one looked exactly like a live one — the worst possible state for a control,
   since the only feedback on clicking it is nothing at all. Hover is killed too:
   an underline appearing under a button that cannot be pressed is a promise. */
.btn-link:disabled       { color: var(--muted); cursor: default; }
.btn-link:disabled:hover { text-decoration: none; }
.btn-sm        { padding: 6px 12px; font-size: 13px; }

/* ── Cards — 12px, flat by default ─────────────────────────────────────── */
.card {
  background: var(--canvas);
  border: 1px solid var(--hairline-soft);
  border-radius: var(--r-lg);
  padding: var(--xl);
}
.card-feature { padding: var(--xxl); box-shadow: var(--e2); }
.card-cream   { background: var(--cream); border-color: var(--beige-deep); color: var(--on-cream); padding: var(--xxl); }
.card-cream-soft { background: var(--cream-soft); border-color: var(--beige-deep); padding: var(--xxl); }

/* ── Inputs — 44px, 8px radius ─────────────────────────────────────────── */
.input, select.input, textarea.input {
  width: 100%;
  background: var(--canvas);
  color: var(--ink);
  border: 1px solid var(--hairline-strong);
  border-radius: var(--r-md);
  padding: var(--sm) var(--md);
  height: 44px;
  font-family: var(--font-ui);
  font-size: 14px;
  transition: border-color 160ms ease, box-shadow 160ms ease;
}
textarea.input { height: auto; min-height: 96px; padding: var(--md); resize: vertical; }
.input:focus {
  outline: none;
  border: 2px solid var(--primary);
  padding: calc(var(--sm) - 1px) calc(var(--md) - 1px);   /* keep height stable */
}
.field-label {
  display: block;
  font-size: 13px; font-weight: 500;
  color: var(--slate);
  margin: 0 0 var(--xxs);
}

/* ── Badges — the one place pills are allowed ──────────────────────────── */
.badge {
  display: inline-flex; align-items: center; gap: 5px;
  font-size: 12px; font-weight: 600; line-height: 1.4;
  border-radius: var(--r-full);
  padding: 4px 10px;
  white-space: nowrap;
}
.badge-orange { background: var(--primary); color: var(--on-primary); }
.badge-cream  { background: var(--cream-deeper); color: var(--ink); }
.badge-dark   { background: var(--ink); color: var(--on-dark); }
.badge-quiet  { background: var(--surface); color: var(--steel); border: 1px solid var(--hairline); }

/* The owner marker on a queue row, when the apply queue is shared by more than
   one browser. Deliberately the strongest of the row's chips: on a shared board
   every row looks alike, and "which of these six are mine" is the question the
   panel now exists to answer at a glance. Someone else's rows keep .badge-quiet,
   so the eye lands on yours rather than having to read all of them. */
.badge-mine   { background: var(--primary); color: var(--on-primary); }
.badge-ok     { background: var(--ok-surface); color: var(--ok); }
.badge-bad    { background: var(--bad-surface); color: var(--bad); }
.badge-warn   { background: var(--warn-surface); color: var(--warn); }

/* ── On/off switch ───────────────────────────────────────────────────────
   For a setting that is genuinely two-state and takes effect the moment it is
   clicked — not a form control you save afterwards.

   A real <button> carrying aria-pressed, not a styled checkbox. A checkbox
   promises a form and a submit; this acts immediately, and screen readers
   should hear a pressed button rather than a field left mid-edit.

   Sized against .badge, which it sits beside in a card header, so the two
   share a baseline. */
.switch {
  display: inline-flex; align-items: center; gap: 8px;
  padding: 0; border: 0; background: none; color: var(--ink);
  font: inherit; font-size: 13px; font-weight: 500;
  cursor: pointer; white-space: nowrap;
}
.switch:disabled { cursor: default; opacity: .55; }
.switch-track {
  position: relative; flex: none;
  width: 38px; height: 22px; border-radius: var(--r-full);
  background: var(--hairline-strong);
  transition: background 180ms ease;
}
.switch-thumb {
  position: absolute; top: 3px; left: 3px;
  width: 16px; height: 16px; border-radius: 50%;
  background: var(--canvas);
  transition: transform 180ms cubic-bezier(.4, 0, .2, 1);
  box-shadow: 0 1px 2px rgba(0, 0, 0, .25);
}
.switch[aria-pressed="true"] .switch-track { background: var(--primary); }
.switch[aria-pressed="true"] .switch-thumb { transform: translateX(16px); }
.switch:focus-visible {
  outline: 2px solid var(--primary); outline-offset: 3px;
  border-radius: var(--r-sm);
}
/* Reserved width, so the label swapping between "On" and "Off" cannot shift
   whatever sits beside it. At 13px the two words differ by enough to nudge a
   badge, and a header that twitches on every toggle reads as a reflow. */
.switch-label { min-width: 24px; text-align: left; }

/* Motion is decoration here; the aria-pressed state and the label carry the
   meaning without it. */
@media (prefers-reduced-motion: reduce) {
  .switch-track, .switch-thumb { transition: none; }
}

/* ── Tabs — underline, per segmented-tab ───────────────────────────────── */
.tabs { display: flex; gap: var(--xs); border-bottom: 1px solid var(--hairline-soft); }
.tab {
  background: none; border: none; cursor: pointer;
  font-family: var(--font-ui); font-size: 14px; font-weight: 500;
  color: var(--steel);
  padding: var(--sm) var(--md);
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  transition: color 160ms ease, border-color 160ms ease;
}
.tab:hover { color: var(--ink); }
.tab.active { color: var(--primary); border-bottom-color: var(--primary); }

/* A tab that navigates to ANOTHER PAGE rather than switching a view.
   Deliberately not class `.tab`: portal.js binds every `.tab` on the dashboard
   to its hash router (`location.hash = tab.dataset.view`), and a cross-page
   link has no data-view — routing one through it would write "#undefined" into
   the URL on the way out and leave the dashboard's own tab state confused when
   the user came back. Same appearance, no shared behaviour. */
.tab-link {
  display: inline-flex; align-items: center;
  font-family: var(--font-ui); font-size: 14px; font-weight: 500;
  color: var(--steel); text-decoration: none;
  padding: var(--sm) var(--md);
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  transition: color 160ms ease, border-color 160ms ease;
}
.tab-link:hover { color: var(--ink); }
.tab-link.active { color: var(--primary); border-bottom-color: var(--primary); }

/* ── Code / log surface ────────────────────────────────────────────────── */
.code {
  background: var(--surface-code);
  color: var(--on-dark);
  font-family: var(--font-mono);
  font-size: 13px; line-height: 1.55;
  border-radius: var(--r-md);
  padding: var(--md);
  overflow: auto;
}

/* ── Tables ────────────────────────────────────────────────────────────── */
.table { width: 100%; border-collapse: collapse; font-size: 14px; }
.table th {
  text-align: left;
  font-size: 11px; font-weight: 600; letter-spacing: 1px; text-transform: uppercase;
  color: var(--stone);
  padding: var(--xs) var(--sm);
  border-bottom: 1px solid var(--hairline);
  white-space: nowrap;
}
.table td { padding: var(--sm); border-bottom: 1px solid var(--hairline-soft); vertical-align: top; }
.table tr:last-child td { border-bottom: none; }
.table tbody tr { transition: background 160ms ease; }
.table tbody tr:hover { background: var(--surface-cream); }

/* Wide content must scroll inside its own container — the page body never
   scrolls sideways. */
.table-scroll { overflow-x: auto; }

/* ── THE SIGNATURE ELEMENT ─────────────────────────────────────────────────
   The sunset stripe. Per DESIGN.md this sits at the foot of EVERY page and is
   the brand's continuity element — it is not decoration to be dropped when a
   layout gets tight. */
.sunset-stripe {
  height: 8px;
  width: 100%;
  background: linear-gradient(90deg,
      var(--primary) 0%,
      var(--sunshine-700) 28%,
      var(--sunshine-500) 52%,
      var(--yellow-saturated) 76%,
      var(--cream) 100%);
}

/* The same gradient as an atmospheric field, for the hero band. Stands in for
   the mountain-sunset photography the brand uses — same light, no asset. */
.hero-sunset {
  background:
    radial-gradient(120% 140% at 82% 8%, rgba(255, 216, 0, 0.55) 0%, transparent 55%),
    radial-gradient(90% 120% at 96% 40%, rgba(250, 80, 15, 0.42) 0%, transparent 60%),
    linear-gradient(135deg, var(--cream-soft) 0%, var(--cream) 48%, var(--cream-deeper) 100%);
  border-bottom: 1px solid var(--beige-deep);
}

/* ── Motion ────────────────────────────────────────────────────────────────
   Scroll-reveal, staggered entrances and rolling numbers.

   ── THE FAILURE THIS AVOIDS ─────────────────────────────────────────────────
   Every hidden-then-revealed pattern has the same trap: if the JavaScript that
   reveals it never runs, the content is invisible forever. So the hidden state
   is scoped under `.js-anim`, a class portal.js puts on <html> before it does
   anything else. No script, no class, no hiding — the page just renders.

   ── AND THE ONE THAT MATTERS HERE ───────────────────────────────────────────
   This dashboard re-renders its panels every three seconds. An entrance
   animation applied on render would restart on every poll, so the queue would
   strobe continuously. Entrances are therefore applied ONCE PER ITEM, keyed by
   task id — see markNew() in portal.js. Nothing animates because time passed;
   things animate because they arrived. */

.js-anim .reveal {
  opacity: 0;
  transform: translateY(18px);
}
.reveal {
  transition: opacity 620ms cubic-bezier(0.22, 1, 0.36, 1),
              transform 620ms cubic-bezier(0.22, 1, 0.36, 1);
  /* Set per element for a stagger. The easing is a soft overshoot-free
     ease-out — motion should feel like it settles, not like it bounces. */
  transition-delay: var(--d, 0ms);
}
.reveal.in { opacity: 1; transform: none; }

/* A row or card that has just appeared in a list. Shorter and smaller than the
   section reveal: this fires while you are looking at it, so it has to register
   without pulling your eye away from whatever you were reading. */
@keyframes enter {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: none; }
}
.enter { animation: enter 380ms cubic-bezier(0.22, 1, 0.36, 1) both; }

/* Cards lift very slightly on hover. 2px, not 8 — the brief calls for sober
   editorial geometry, and a card that leaps is neither. */
.card, .card-cream, .card-cream-soft {
  transition: transform 260ms cubic-bezier(0.22, 1, 0.36, 1),
              box-shadow 260ms ease,
              border-color 260ms ease;
}
.card:hover, .card-cream:hover, .card-cream-soft:hover {
  transform: translateY(-2px);
  box-shadow: var(--e2);
}

/* The hero's light drifts, very slowly. This stands in for the brand's
   mountain-sunset photography: the original gets its life from real light
   across a landscape, and a static gradient reads as flat next to it. 24s and a
   few percent of movement — noticeable only if you look for it, which is the
   point. */
@keyframes drift {
  0%   { background-position: 0% 0%,   100% 40%, 0% 0%; }
  50%  { background-position: 6% 4%,    94% 46%, 0% 0%; }
  100% { background-position: 0% 0%,   100% 40%, 0% 0%; }
}
.hero-sunset {
  background-size: 140% 140%, 130% 130%, 100% 100%;
  animation: drift 24s ease-in-out infinite;
}

/* The signature stripe gets a slow travelling sheen — the one place a little
   shine is on-brand, because the stripe IS the brand mark. */
@keyframes sheen { to { background-position: 200% 0; } }
.sunset-stripe {
  background-size: 200% 100%;
  animation: sheen 14s linear infinite;
}

/* Numbers roll rather than snap. A count that jumps from 924 to 1025 between
   polls looks like a glitch; the same change counted through reads as progress,
   which is the entire message of this dashboard. */
.rolling { font-variant-numeric: tabular-nums; }

/* ── Live pulse ────────────────────────────────────────────────────────────
   The "is it actually doing something" signal. Motion is confined to this one
   dot on purpose: a dashboard where several things move competes with itself,
   and the only genuinely live fact here is whether the agent is working. */
.pulse {
  width: 8px; height: 8px; border-radius: var(--r-full);
  background: var(--primary);
  box-shadow: 0 0 0 0 rgba(250, 80, 15, 0.55);
  animation: pulse 1900ms ease-out infinite;
  flex-shrink: 0;
}
.pulse-idle { background: var(--muted); animation: none; box-shadow: none; }
/* The one exception to "motion means working": a fault that stops the pipeline
   and needs the user. It keeps the pulse so it reads as live rather than as a
   stale error, but in --bad so it cannot be mistaken for healthy activity. */
.pulse-bad {
  background: var(--bad);
  box-shadow: 0 0 0 0 rgba(194, 56, 27, 0.55);
  animation: pulse-bad 1900ms ease-out infinite;
}
@keyframes pulse-bad {
  0%   { box-shadow: 0 0 0 0 rgba(194, 56, 27, 0.5); }
  70%  { box-shadow: 0 0 0 9px rgba(194, 56, 27, 0); }
  100% { box-shadow: 0 0 0 0 rgba(194, 56, 27, 0); }
}
@keyframes pulse {
  0%   { box-shadow: 0 0 0 0 rgba(250, 80, 15, 0.5); }
  70%  { box-shadow: 0 0 0 9px rgba(250, 80, 15, 0); }
  100% { box-shadow: 0 0 0 0 rgba(250, 80, 15, 0); }
}

/* FETCHING, as distinct from applying. Green, and a different colour is the
   whole point: a sweep and an application are separate activities that can run
   at the same time, and one orange dot for both answered "something is
   happening" when the question being asked is "is it still collecting jobs".
   Collecting produced no signal at all before this — the counts sit still for
   the first forty minutes of a sweep, because the whole-board phase writes once
   at the end and the dedup absorbs most of it, so a working sweep and a dead
   one looked identical from the dashboard. */
.pulse-fetch {
  background: var(--ok);
  box-shadow: 0 0 0 0 rgba(47, 125, 85, 0.55);
  animation: pulse-fetch 1900ms ease-out infinite;
}
@keyframes pulse-fetch {
  0%   { box-shadow: 0 0 0 0 rgba(47, 125, 85, 0.5); }
  70%  { box-shadow: 0 0 0 9px rgba(47, 125, 85, 0); }
  100% { box-shadow: 0 0 0 0 rgba(47, 125, 85, 0); }
}

/* The hero's own fetching light. The nav dot is 8px in a corner; this one sits
   beside the run status line where the user is already looking, and carries a
   word so the colour is not the only thing saying what it means. */
.fetch-live {
  display: inline-flex; align-items: center; gap: 8px;
  margin-top: var(--xs);
  font-size: 12px; font-weight: 700; letter-spacing: .02em;
  color: var(--ok);
}
.fetch-live[hidden] { display: none; }
.fetch-live.is-stopping { color: var(--warn); }
.fetch-live.is-stopping .pulse-fetch {
  background: var(--warn);
  animation-name: pulse-bad;
}

/* ── Fetch plan: the sweep as rows of ticks ────────────────────────────────
   Horizontal and wrapping, not a vertical list. The units are short — a role
   name, sometimes a portal — and stacked they would run to twenty lines for
   something the eye should take in at a glance. Wrapped chips also keep the
   ORDER visible left-to-right, which is the whole point: the green run at the
   start is what is banked, the blinking one is what would finish if you stopped
   now, and the grey tail is what is still owed. */
.plan-row + .plan-row {
  margin-top: var(--md); padding-top: var(--md);
  border-top: 1px solid rgba(0, 0, 0, .07);
}
.plan-row-head {
  display: flex; align-items: baseline; gap: var(--sm);
  flex-wrap: wrap; margin-bottom: 4px;
}
.plan-row-label { font-size: 13px; font-weight: 700; letter-spacing: .01em; }
/* Tabular so the count does not jitter sideways as it climbs on every poll. */
.plan-row-count { font-size: 12px; color: var(--steel); font-variant-numeric: tabular-nums; }
.plan-row-note { font-size: 11.5px; color: var(--stone); margin: 0 0 8px; max-width: 78ch; }

.plan-steps { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }

/* Which portal the next few chips belong to. The scraper row is portal-major —
   LinkedIn for every role, then Indeed, then Naukri — and without this the row
   is the same role names repeated three times with no way to tell the passes
   apart. */
.plan-group {
  font-size: 10.5px; font-weight: 700; letter-spacing: .07em;
  text-transform: uppercase; color: var(--stone);
  margin-left: 8px; padding-left: 8px;
  border-left: 1px solid rgba(0, 0, 0, .10);
}
.plan-group:first-child { margin-left: 0; padding-left: 0; border-left: 0; }

.plan-step {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 4px 10px; border-radius: var(--r-full);
  border: 1px solid rgba(0, 0, 0, .12);
  font-size: 12px; line-height: 1.35; white-space: nowrap;
}
/* Fixed width so a ✓ replacing a · does not shunt the label sideways when a
   chip lands — on a wrapping row that shifts everything after it.

   inline-FLEX, not a block with text-align. The active mark is not a character,
   it is a .pulse element, and .pulse sizes itself with width/height — which an
   inline child of a block box simply ignores. The dot came out as a zero-sized
   inline span with a stray box-shadow and no visible circle at all: present in
   the DOM, correct in getComputedStyle, invisible on screen. Everywhere else on
   the page .pulse happens to sit inside a flex row, which is why this was the
   first place it broke. */
.plan-step .plan-mark {
  width: 11px; height: 11px; flex-shrink: 0; font-weight: 700;
  display: inline-flex; align-items: center; justify-content: center;
}
.plan-step .plan-mark .pulse { width: 7px; height: 7px; }
/* A tighter ring than the navbar's. The animation is deliberately the same
   signal — green, same cadence — but a 9px halo around a dot inside a 26px chip
   washes over the chip's own border and reads as a rendering fault rather than
   as a heartbeat. */
.plan-step .plan-mark .pulse-fetch { animation-name: plan-pulse; }
@keyframes plan-pulse {
  0%   { box-shadow: 0 0 0 0 rgba(47, 125, 85, .55); }
  70%  { box-shadow: 0 0 0 5px rgba(47, 125, 85, 0); }
  100% { box-shadow: 0 0 0 0 rgba(47, 125, 85, 0); }
}
/* The stopping dot is --warn, not --bad. It matches .fetch-live.is-stopping,
   which makes the same distinction for the same reason: a sweep winding down on
   request is not a fault, and red for it would put a running system's ordinary
   behaviour in the same colour as a dead API key. */
.plan-step .plan-mark .pulse-bad {
  background: var(--warn);
  animation-name: plan-pulse-warn;
}
@keyframes plan-pulse-warn {
  0%   { box-shadow: 0 0 0 0 rgba(240, 100, 0, .55); }
  70%  { box-shadow: 0 0 0 5px rgba(240, 100, 0, 0); }
  100% { box-shadow: 0 0 0 0 rgba(240, 100, 0, 0); }
}

.plan-step.is-pending { color: var(--stone); }
.plan-step.is-done {
  color: var(--ok); font-weight: 600;
  border-color: rgba(47, 125, 85, .30);
  background: rgba(47, 125, 85, .08);
}
/* THE ONE IN FLIGHT. The mark is a real .pulse-fetch dot rather than a bespoke
   flash, so it is the identical green blink the navbar and the hero light use —
   one animation means one thing across the page, and the user has already
   learned it means "working right now". */
.plan-step.is-active {
  color: var(--ok); font-weight: 700;
  border-color: var(--ok);
  background: rgba(47, 125, 85, .12);
}
/* Asked to stop but not yet at a boundary. Amber, matching .fetch-live's
   is-stopping, so the chip does not go on claiming healthy progress after the
   button was pressed — it is still working, but it is winding down. */
.plan-step.is-active.is-stopping {
  color: var(--warn); border-color: var(--warn);
  background: rgba(240, 100, 0, .10);
}
/* The run ended and this never happened — a portal signed out and skipped, or a
   phase that failed. Deliberately NOT the same grey as pending: pending is a
   promise that Continue will get to it, and telling someone to wait for work
   that is never coming is worse than saying plainly that it did not happen. */
.plan-step.is-missed {
  color: var(--warn); border-color: rgba(240, 100, 0, .32);
  background: rgba(240, 100, 0, .06);
}

/* ── Role pills (Setup → Job roles) ────────────────────────────────────────
   The same chip shape the fetch plan uses, because they are the same objects
   seen at two moments: what you asked to sweep, and how that sweep is going.
   Someone who has watched the dashboard tick through these should recognise
   them instantly when they come to change them. */
.role-pills {
  display: flex; flex-wrap: wrap; gap: 8px;
  margin-top: var(--md); min-height: 38px;
}
.role-pill {
  display: inline-flex; align-items: center; gap: 8px;
  padding: 6px 6px 6px 10px; border-radius: var(--r-full);
  border: 1px solid var(--hairline);
  background: var(--surface-cream);
  font-size: 13px; line-height: 1.3;
  cursor: grab; user-select: none;
}
.role-pill:active { cursor: grabbing; }
/* The element being dragged. Faded rather than hidden so the gap it leaves is
   still legible as "this is the one moving". */
.role-pill.is-dragging { opacity: .4; }
/* Where it would land. A visible target beats guessing from cursor position on
   a wrapping row, where "before or after" is genuinely ambiguous. */
.role-pill.is-over { border-color: var(--primary); box-shadow: 0 0 0 2px rgba(250, 80, 15, .18); }

/* Sweep position. Shown because the order IS the priority — the sweep works
   through these one at a time and Stop keeps whatever is already done, so
   "which is first" is a decision, not a display detail. */
.role-pill-n {
  font-family: var(--font-display); font-size: 12px;
  color: var(--stone); min-width: 12px; text-align: right;
}
.role-pill-x {
  border: 0; background: none; cursor: pointer;
  color: var(--stone); font-size: 15px; line-height: 1;
  padding: 2px 6px; border-radius: var(--r-full);
}
.role-pill-x:hover { background: rgba(194, 56, 27, .12); color: var(--bad); }
.role-pills-empty { color: var(--stone); font-size: 13px; padding: 8px 0; }

/* Someone watching an agent work for an hour should not have a dot strobing at
   them, and vestibular triggers are a real accessibility concern.

   Everything above is decoration; none of it carries meaning that is lost by
   switching it off. So this turns ALL of it off — including the reveal's hidden
   start state, or the page would stay blank for anyone who asked for less
   motion, which is the worst possible reading of the request. */
@media (prefers-reduced-motion: reduce) {
  .pulse { animation: none; }
  /* Spelled out at the same depth as the rules above, because `.pulse` alone
     does NOT switch these off: `.plan-step .plan-mark .pulse-fetch` is a
     three-class selector and outranks it, so the chip would go on blinking at
     exactly the user who asked it not to. The meaning survives without the
     motion — the running chip still has a filled dot, a solid border and bold
     text, which is what makes it identifiable in the first place. */
  .plan-step .plan-mark .pulse,
  .plan-step .plan-mark .pulse-fetch,
  .plan-step .plan-mark .pulse-bad { animation: none; box-shadow: none; }
  .hero-sunset, .sunset-stripe { animation: none; }
  .card:hover, .card-cream:hover, .card-cream-soft:hover { transform: none; }
  .js-anim .reveal, .reveal { opacity: 1 !important; transform: none !important; }
  .enter { animation: none; }
  * { transition-duration: 1ms !important; animation-duration: 1ms !important; }
}

/* ── Layout helpers ────────────────────────────────────────────────────── */
.wrap { max-width: 1280px; margin: 0 auto; padding: 0 var(--xxl); }
.row { display: flex; align-items: center; gap: var(--sm); }
.row-between { display: flex; align-items: center; justify-content: space-between; gap: var(--sm); }
.col { display: flex; flex-direction: column; }
.grid { display: grid; gap: var(--md); }
.stack > * + * { margin-top: var(--md); }
.spacer { flex: 1; }
.hidden { display: none !important; }
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ── Top navigation — sticky, 64px, hairline base ──────────────────────── */
.topnav {
  position: sticky; top: 0; z-index: 20;
  background: var(--canvas);
  border-bottom: 1px solid var(--hairline-soft);
  height: 64px;
}
.brandmark {
  font-family: var(--font-display);
  font-size: 20px; letter-spacing: -0.3px;
}
.brandmark .accent { color: var(--primary); }

/* ── Responsive ────────────────────────────────────────────────────────────
   Hero steps 84 → 76 → 64 → 52 → 40 per the breakpoint table. The stripe stays
   full width at every size. */
@media (max-width: 1279px) { .t-hero { font-size: 76px; } }
@media (max-width: 1023px) {
  .t-hero { font-size: 64px; }
  .t-h1 { font-size: 42px; }
  .wrap { padding: 0 var(--lg); }
}
@media (max-width: 767px) {
  .t-hero { font-size: 52px; }
  .t-display { font-size: 40px; }
  .t-h1 { font-size: 34px; }
  .t-h2 { font-size: 28px; }
  .t-stat { font-size: 40px; }
}
@media (max-width: 479px) {
  .t-hero { font-size: 40px; }
  .wrap { padding: 0 var(--md); }
  .card, .card-feature, .card-cream { padding: var(--lg); }
  /* Touch targets go up, not down, on the smallest screens. */
  .tab { padding: var(--sm); min-height: 44px; }
}
