/* =========================================================
   DrawIQ — main.css
   ========================================================= */

/* ----- Self-hosted fonts (2026-06-08) -----
   Inter is now served from /static (same-origin), removing the
   render-blocking third-party Google Fonts <link> that hung the page
   load on flaky mobile networks (the load bar never completed). The file
   is the Inter VARIABLE woff2 already bundled for WeasyPrint
   (wght 100..900), so one @font-face covers every UI weight (400/500/
   600/700). Relative url() resolves against this file's /static/css/
   location -> /static/fonts/inter.woff2 (robust to any static prefix).
   font-display: swap -> text renders immediately in the system fallback,
   then upgrades to Inter (no invisible-text flash, no blocking).
   Source Serif 4 was dropped from the external link too: --font-serif is
   a reserved design token with ZERO rendered usage today (the serif H1
   tier was removed after the "más seria" feedback), so its Georgia/serif
   fallback is visually identical and no self-hosted serif file is needed. */
@font-face {
  font-family: 'Inter';
  src: url('../fonts/inter.woff2') format('woff2');
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

/* ----- Reset & base ----- */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

/* The HTML `hidden` attribute should ALWAYS hide an element. The UA
   default `[hidden] { display: none }` has specificity (0,0,1), so
   any author rule like `.btn { display: inline-flex }` (specificity
   0,1,0) silently overrides it. The spec intent is that hidden means
   hidden regardless of class-level display rules, so this restores
   that semantic with !important. Without this rule the conditional-
   visibility pattern (e.g. the Step 10 Download summary button that
   toggles via [hidden] on copilot:estimate-modified) breaks for any
   styled element. */
[hidden] { display: none !important; }

/* Screen-reader-only utility: visually removes an element from the page
   while keeping it in the accessibility tree + DOM. Used to hide a native
   <input> behind a styled <label> button (comparator plan-file picker) and
   to expose sr-only <label>s for otherwise-unlabeled inputs (comparator
   search + PL select). Standard clip-rect sr-only recipe. Was referenced
   in 3 comparator spots but never defined — see Grupo 2 (2026-05-27). */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* === Global modal centering ===
   Native <dialog>:modal does NOT reliably center via the UA's auto-
   margin across browsers — empirically verified 2026-05-31 across all
   11 distinct dialog classes in the codebase. (.preview-dialog,
   .confirm-dialog, .reanalyze-dialog, .asset-create-modal,
   .upload-pdf-dialog, .comparator-pdf-dialog, .copilot-commands-dialog,
   .dup-refresh-dialog, .dup-name-dialog, .generate-dialog,
   .share-dialog — 9 of those 11 were off-center under the UA default;
   the 2 that appeared centered were inconsistent across browsers too.)
   One global rule covers every current dialog + any future dialog
   automatically. Uses [open] over :modal because [open] is set by both
   showModal() and .show() — broader future-proof. Bundle Estética/UX
   item 1, 2026-05-31. */
dialog[open] {
  margin: auto;
}

:root {
  /* =========================================================
     VISUAL DESIGN PASS — Phase 3 Step 1 (2026-05-15)

     New design-token foundation. Old tokens retained at the
     bottom of this block as ALIASES → new tokens, so existing
     CSS rules and template usages render with the new design
     until per-page/per-component migrations replace them
     directly (Phase 3 steps 2-12). Cleanup in step 13 removes
     the legacy aliases.

     Tokens follow the Matterport-aligned aesthetic confirmed
     in Phase 2 Stage A (colors + typography) and Stage B
     (spacing, buttons, forms). See context/DEFERRED_BACKLOG.md
     Visual Design Pass entry for the full spec.
     ========================================================= */

  /* ----- Font families ----- */
  --font-sans:  'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  --font-serif: 'Source Serif 4', 'Source Serif Pro', Georgia, serif;
  --font-mono:  'SF Mono', Menlo, Consolas, monospace;

  /* ----- Color foundations ----- */
  --color-bg:          #FFFFFF;
  --color-surface:     #FCFCFC;
  --color-surface-alt: #FAFAFA;

  /* ----- Borders ----- */
  --color-border-subtle:  #F0F0F0;
  --color-border-default: #E5E5E5;
  --color-border-strong:  #CCCCCC;

  /* ----- Text ----- */
  --color-text-primary:   #111111;
  --color-text-secondary: #555555;
  --color-text-muted:     #888888;
  --color-text-disabled:  #BBBBBB;
  --color-text-inverse:   #FFFFFF;

  /* ----- Primary accent (CTAs) ----- */
  --color-accent:          #111111;
  --color-accent-hover:    #000000;
  --color-accent-active:   #2A2A2A;
  --color-accent-disabled: #CCCCCC;
  --color-accent-text:     #FFFFFF;

  /* ----- Vibrant accent (coral — sparse use, ~5-10 places max)
     User-measured from matterport.com live site (RGB 255, 49, 88).
     Use cases: topbar active-nav underline, NEW/Beta pills,
     promotional banners, decorative subtitle underlines on
     marketing surfaces. NOT for primary CTAs, NOT for focus
     rings, NOT for semantic states. ----- */
  --color-accent-vibrant:    #FF3158;
  --color-accent-vibrant-bg: #FFE8EE;

  /* ----- Semantic (restrained, deep-foreground + pale-bg pattern) ----- */
  --color-success:    #16745F;
  --color-success-bg: #ECF6F1;
  --color-error:      #B42318;
  --color-error-bg:   #FEF3F2;
  --color-warning:    #B45309;
  --color-warning-bg: #FFF8EB;
  --color-info:       #1F4173;
  --color-info-bg:    #F2F5FA;
  /* Soft list-card tint (Phase F polish) — a barely-there NEUTRAL-GREY lift so
     list cards read off the page; present enough to separate, not yellow/cream.
     Single token = tunable. Applied to PL/Projects + Branding list cards. */
  --color-card-tint:  #F1F1F2;

  /* ----- Image overlays (for hero photography) ----- */
  --color-overlay-dark:   rgba(17, 17, 17, 0.40);
  --color-overlay-darker: rgba(17, 17, 17, 0.65);

  /* ----- Spacing scale (4px base, T-shirt names) ----- */
  --space-xs:   4px;
  --space-sm:   8px;
  --space-md:  12px;
  --space-lg:  16px;
  --space-xl:  24px;
  --space-2xl: 32px;
  --space-3xl: 48px;
  --space-4xl: 64px;
  --space-5xl: 96px;

  /* ----- Border-radius ----- */
  --radius-sm:    6px;    /* was 5px — buttons, inputs, chips */
  --radius:       8px;    /* unchanged — cards, panels, modals */
  --radius-lg:   12px;    /* new — hero containers, large feature blocks */
  --radius-pill: 999px;   /* new — chips, badges (replaces hardcoded 20px) */

  /* ----- Shadows (softer than prior values, Matterport-restrained) ----- */
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.04);
  --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.06);
  --shadow-lg: 0 12px 32px rgba(0, 0, 0, 0.10);
  --shadow-xl: 0 24px 56px rgba(0, 0, 0, 0.14);

  /* ----- Component sizes ----- */
  --height-input: 44px;   /* was 40px — single source of truth for input/select/textarea height */

  /* ----- Type scale (sizes / line-heights / letter-spacing / weight) -----
     Tokens are values only; component CSS applies them via font-size +
     line-height + letter-spacing + font-weight per the spec below.
     Display + h1 use serif; everything else uses sans.

     display:     48px / 56px / -0.025em / 400 / serif italic   (hero copy)
     h1:          32px / 40px / -0.02em  / 600 / serif roman    (in-app page titles)
     h2:          24px / 32px / -0.015em / 600 / sans
     h3:          20px / 28px / -0.01em  / 600 / sans
     h4:          16px / 24px /  0       / 600 / sans
     body-large:  18px / 28px /  0       / 400 / sans
     body:        16px / 24px /  0       / 400 / sans (root body)
     body-small:  14px / 20px /  0       / 400 / sans
     button:      14px / 1    /  0       / 500 / sans
     label:       14px / 20px /  0       / 500 / sans
     input:       16px / 1.5  /  0       / 400 / sans
     caption:     12px / 16px /  0.01em  / 400 / sans
     overline:    12px / 16px /  0.06em  / 600 / sans (uppercase via CSS)
     code:        13px / 1.4  /  0       / 400 / mono
   */

  /* =========================================================
     LEGACY TOKEN ALIASES — RETIRED 2026-05-27 (Copilot UX Polish).
     The Phase-3 transition aliases (--navy / --blue / --bg / --surface /
     --text / --muted / --border / --error[-bg] / --success[-bg] /
     --brand[-hover] / --font / --shadow, plus the unused --navy-dark /
     --blue-hover / --border-focus / --info-bg) were migrated to their
     canonical --color-* / --font-sans / --shadow-sm targets across
     main.css + copilot.css and removed here — a no-op rename (each alias
     already resolved to its canonical target).

     Only these three standalone values had NO canonical equivalent, so
     they are kept as real tokens (never aliases):
     ========================================================= */
  --error-border:      #FECACA;   /* no canonical equivalent */
  --info-border:       #BFDBFE;   /* no canonical equivalent */
  /* Peripheral "this just changed" highlight used by the auto-DOM-refresh
     fade — a transient signal, not a brand color. */
  --surface-highlight: #FFFBEB;
}

html { font-size: 16px; }   /* was 15px — confirmed in Phase 2 Stage A (Body base 16px) */

/* Bundle Estética/UX item 6 Layer 1 (2026-05-31) — global horizontal-
   overflow guard for mobile responsiveness. Stops any element wider
   than the viewport (wide tables, min-width:180-260px flex/grid items
   in some surfaces, long unwrapped headings) from stretching the
   whole document app-wide. The Layer 1 guard CONTAINS overflow but
   does NOT make wide content usable on mobile — Layer 2 (pending)
   will wrap wide tables in overflow-x:auto scroll containers + add
   @media restacking for min-width flex/grid items. Pair both layers
   to ship the full mobile fix; this commit is Layer 1 only. */
html, body { overflow-x: hidden; }

body {
  font-family: var(--font-sans);
  background: var(--color-surface-alt);
  color: var(--color-text-primary);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}

a { color: var(--color-accent); text-decoration: none; }
a:hover { text-decoration: underline; }

/* ----- Typography -----
   App-shell H1 default: 1.6rem (25.6px). Auth surfaces override to
   1.75rem (28px) via .auth-header h1 + .auth-split__form-header h1
   per VDP Step 3 — auth-page H1 is the focal element on its screen
   and deserves more visual weight than an in-app page title.
   Detail-page H1s use the global 1.6rem with only overflow-wrap
   overrides for long filenames (Step 10 + Step 11). #}*/
h1 { font-size: 1.6rem; font-weight: 700; letter-spacing: -.02em; line-height: 1.25; }
h2 { font-size: 1.25rem; font-weight: 600; }
h3 { font-size: 1rem; font-weight: 600; }

/* VDP Step 14: replaces the inline `style="margin-top:.25rem"` that
   the 6 form/header surfaces sprinkled on H1s following the
   .breadcrumb-back link. Adjacent-sibling combinator gives ANY h1
   immediately after a breadcrumb the 4px breathing room — zero
   template-side spacing markup needed. */
.breadcrumb-back + h1 { margin-top: var(--space-xs); }

/* VDP Step 14: global form-margin reset. Default browser <form>
   margin (Firefox: 1em-ish; Chrome/Edge: 0) was being killed by
   inline `style="margin:0"` on 6 inline-CSRF forms. The global
   reset makes the inline neutralization unnecessary across every
   form on the app. */
form { margin: 0; }

/* VDP Step 14: adjacent-sibling spacing on .panel — replaces the
   8 inline `style="margin-bottom:1rem"` (and one .85rem) on
   loop-generated panels. Between siblings, margin-collapse means
   `margin-top` on the second panel produces the same visual gap as
   `margin-bottom` on the first did. Last-panel-trailing-margin
   loss is acceptable because the following element (.summary-cards
   Step 10, .price-list-items-no-results Step 11, etc.) already
   provides its own top margin. */
.panel + .panel { margin-top: var(--space-lg); }

/* =========================================================
   AUTH LAYOUT
   Login / Invite:  centered card
   Register:        two-column (brand | form)
   ========================================================= */

/* Shared auth page reset */
.auth-page {
  /* dvh, NOT vh, so the auth shell sizes to the actually-visible
     viewport (subtracts browser chrome: tabs/bookmarks bar; OS chrome
     was already excluded). vh would anchor the shell to the LARGEST
     possible viewport height — pushing tall forms below visible
     without giving the body a scroll path. Bundle Estética/UX item 4,
     2026-05-31. */
  min-height: 100dvh;
  display: flex;
  align-items: stretch;
}

/* --- Centered card (login, forgot, reset, invite) --- */
.auth-page--centered {
  align-items: center;
  justify-content: center;
  background: var(--color-surface-alt);
  padding: var(--space-2xl) var(--space-lg);
}

.auth-card {
  background: var(--color-surface);
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-md);
  padding: var(--space-3xl) var(--space-2xl);
  width: 100%;
  max-width: 420px;
}

/* --- Split layout (register) --- */
.auth-page--split {
  background: var(--color-surface-alt);
}

.auth-split__brand {
  display: none;
  background: var(--color-text-primary);
  color: var(--color-text-inverse);
  padding: var(--space-4xl) var(--space-3xl);
  flex-direction: column;
  justify-content: space-between;
  /* dvh: see .auth-page comment above. */
  min-height: 100dvh;
}

.auth-split__form {
  flex: 1;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: var(--space-3xl) var(--space-lg);
  /* No overflow-y: auto here — that created an INTERNAL scroll
     context which was invisible on browsers that hide scrollbars
     until hover, trapping the registration form's submit button
     below the form panel's bottom edge. Removing it lets form-
     inner's intrinsic height push the parent freely.
     PRINCIPLE for this entire layout chain (.auth-page,
     .auth-split__brand, .auth-split__form, .auth-split__form-inner):
     no fixed heights anywhere. Use min-height (which grows when
     content demands) NOT height (which caps the container and
     blocks the body-scroll path). Containers grow with their
     natural content; body scrolls when content > viewport; works
     on ANY screen size (4K monitor, small laptop, phone, with/
     without browser chrome) with zero size-specific rules. dvh
     over vh so the "at least visible viewport" floor reflects
     actual chrome-eaten visible area, not the theoretical-max
     viewport. Bundle Estética/UX item 4, 2026-05-31. */
}

.auth-split__form-inner {
  background: var(--color-surface);
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-md);
  /* Vertical compaction (Bundle Estética/UX item 4, 2026-05-31):
     3xl→2xl on Y so the registration form fits within standard
     laptop usable viewports without scroll. X padding unchanged. */
  padding: var(--space-2xl) var(--space-2xl);
  width: 100%;
  max-width: 620px;
}

.auth-split__form-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-lg);
  /* Vertical compaction history (item 4, 2026-05-31):
     - Original: var(--space-xl) (24px) before compaction.
     - Compaction pass: var(--space-xl) → var(--space-lg) (16px).
     - First tweak attempt: var(--space-lg) → var(--space-3xl) (48px,
       +32) — too much. Reduced -24 to var(--space-xl) (24px, net
       +8 from compaction). Lands at the original pre-compaction
       value: enough separation between header divider and the
       "ABOUT YOU & YOUR COMPANY" section label without overshoot.
       Register-only by selector scope; register still fits without
       scroll. */
  margin-bottom: var(--space-xl);
  padding-bottom: var(--space-md);
  border-bottom: 1px solid var(--color-border-subtle);
}

/* Bundle Estética/UX item 4 tweak 2 (2026-05-31): top-bar with the
   register-page DrawIQ logo on the left + "Sign in" CTA on the right.
   Replaces the prior layout where the CTA sat inside the form-header
   and visually competed with the "Create your workspace" heading.
   Register-only — login/forgot/reset/invite use .auth-page--centered
   which has its own layout. The auth-logo's own margin-bottom is
   zeroed inside the top-bar so the gap comes from the top-bar (avoids
   double-spacing). */
.auth-split__top-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-lg);
  margin-bottom: var(--space-lg);
}
.auth-split__top-bar .auth-logo {
  margin-bottom: 0;
}

/* Register H1 — same Inter sans 700/28px treatment as .auth-header h1
   above. All 5 auth-page H1s converge on this typography per VDP Step 3
   final decision; see comment on .auth-header h1 for rationale. */
.auth-split__form-header h1 {
  margin: 0 0 var(--space-xs);
  font-family: var(--font-sans);
  font-size: 1.75rem;
  font-weight: 700;
  line-height: 1.25;
  letter-spacing: -0.015em;
  color: var(--color-text-primary);
}
.auth-split__form-header p {
  margin: 0;
  color: var(--color-text-muted);
  font-size: 0.875rem;
}

/* Top-right text + cross-flow CTA combo (replaces the prior .btn-ghost
   button). Gray descriptive text + coral CTA word. */
.auth-split__form-header-cta {
  margin: 0;
  font-size: 0.875rem;
  color: var(--color-text-secondary);
  white-space: nowrap;
  flex-shrink: 0;
}

@media (min-width: 900px) {
  /* min-height (NOT height): brand can stretch when form content
     pushes the parent past 100dvh, instead of capping the parent at
     a rigid height that blocks body scroll.
     position: sticky removed (Option A, 2026-05-31): the sticky +
     align-items: stretch + min-height: 100dvh combo created an
     html.scrollHeight vs body.scrollHeight discrepancy (~105px gap)
     that capped the document scroll before reaching the bottom of
     the form. The sticky-pinned-brand UX was already neutralized
     when height → min-height landed earlier this commit (stretched
     brand had sticky-range = 0), so no live UX is lost. Principle:
     no fixed/positioned constraints that block the natural content-
     height + body-scroll flow. Bundle Estética/UX item 4. */
  .auth-split__brand { display: flex; width: 380px; flex-shrink: 0; min-height: 100dvh; }
  /* Vertical compaction (item 4, 2026-05-31): 3rem→2rem Y. */
  .auth-split__form  { padding: 2rem 2rem; }
}

/* Register-page mobile overflow fix (2026-06-01).
   Cause: .auth-split__form-header-cta has `white-space: nowrap` —
   "Already have an account? Sign in →" is an unbreakable ~233px
   line. Combined with the logo's ~100px in the top-bar, this
   forces .auth-split__form-inner's intrinsic min-width to ~413px,
   which propagates up through .auth-split__form's `flex: 1` +
   default `min-width: auto` to ~445px. Past the 375px viewport
   = clipped Sign-in CTA. Measured live in Edge DevTools at 375px;
   see DEFERRED_BACKLOG.md "Register-page mobile — Sign-in CTA
   clipped at narrow viewport" for the offenders-query output.
   Breakpoint 520px matches the existing .form-row--2 collapse
   on this exact form (line 637-639), so mobile auth behavior
   shifts together.

   Revision (same-day): at 375px in DevTools the wrap-only Layer-1
   was enough, BUT on Gus's real phone (~360px or narrower) the
   link "Sign in →" still clipped — even with nowrap dropped, the
   CTA's available width was so narrow that the link could not
   share a line with the "Already have an account?" copy at all.
   Robust fix: put the link on its OWN LINE below the text. Then
   it never competes for inline space with the surrounding copy,
   regardless of how narrow the viewport gets — the link always
   has a full content-area line to itself. */
@media (max-width: 520px) {
  /* Layer 1 — release the content-side pressure. Allow the CTA
     copy ("Already have an account?") to wrap as needed.
     Combined with the block-link below, the <p> can wrap its
     text to multiple lines if the container is too narrow. */
  .auth-split__form-header-cta {
    white-space: normal;
  }

  /* Layer 2 — belt-and-suspenders against the flex min-width:
     auto gotcha. Same pattern as `.app-content { min-width: 0 }`
     from Estética/UX item 6 Layer 1. Allows the flex item to
     shrink below its content's intrinsic min — if any future
     nowrap creeper sneaks back in, the overflow stays contained
     within the form (visible scroll or wrap), it doesn't push
     the page wider than the viewport. */
  .auth-split__form {
    min-width: 0;
  }

  /* Layer 3 — link on its own line at mobile.
     `display: block` makes the <a> start on a new line below
     "Already have an account?", taking the CTA <p>'s content-area
     width and left-aligning. `white-space: nowrap` on the link
     itself locks "Sign in →" as one unbreakable unit (the
     arrow stays attached to "Sign in" even at extreme narrow
     widths). The link's existing visual (coral color from
     .auth-foot__cta line 1221-1224, weight 500) is preserved
     — only the flow changes, not the look. Specificity
     0,2,0 — beats the base .auth-foot__cta rule (0,1,0) and
     stays scoped to the register top-bar (won't touch login /
     forgot / reset / invite, which never enter the
     .auth-split__* cascade). */
  .auth-split__form-header-cta .auth-foot__cta {
    display: block;
    white-space: nowrap;
  }
}

/* Brand panel content (register left side, sticky black panel) */
.brand-logo {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  font-size: 1.125rem;
  font-weight: 700;
  color: var(--color-text-inverse);
}

/* Register brand-panel mark — B1 inversion per VDP Step 3: WHITE square
   with BLACK "D" inside, so it reads cleanly on the black panel. The
   centered-card .auth-logo__mark is the non-inverted variant. */
.brand-logo__mark {
  width: 32px;
  height: 32px;
  background: var(--color-text-inverse);
  border-radius: var(--radius-sm);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 0.875rem;
  font-weight: 800;
  color: var(--color-text-primary);
}

.brand-tagline {
  margin-top: var(--space-3xl);
}

/* Brand tagline display — Inter sans 700 at 32px. (VDP Step 3 first
   proposed serif italic; user feedback was that italic felt too
   "editorial" for B2B contractor audience. Sans bold at 32px reads
   solid and impactful without the magazine-y tone.) The serif tier
   stays available via --font-serif but is currently used only on the
   centered-card Login H1 ("Welcome back") where 32px gives it room. */
.brand-tagline h2 {
  font-family: var(--font-sans);
  font-style: normal;
  font-size: 2rem;
  font-weight: 700;
  line-height: 1.15;
  letter-spacing: -0.02em;
  color: var(--color-text-inverse);
  margin-bottom: var(--space-md);
}

.brand-tagline p {
  color: rgba(255, 255, 255, 0.80);
  font-size: 0.875rem;
  line-height: 1.55;
}

/* Brand features: single-column vertical stack. (VDP Step 3 originally
   tried 2-col but user clarified the 2-col preference was for the
   password requirements list, not for the marketing bullets — those
   read better stacked one per line.) */
.brand-features {
  list-style: none;
  margin-top: var(--space-2xl);
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-md);
}

.brand-features li {
  display: flex;
  align-items: flex-start;
  gap: var(--space-sm);
  color: rgba(255, 255, 255, 0.80);
  font-size: 0.875rem;
  line-height: 1.45;
}

/* Bullet ✓ marks in coral (F1 per user decision): decorative on marketing
   surface, not validation indicators. Coral usage allowed here under the
   "promotional surface" rule from the accent-restriction decision. */
.brand-features li::before {
  content: '✓';
  color: var(--color-accent-vibrant);
  font-weight: 700;
  flex-shrink: 0;
  margin-top: 0.05rem;
}

.brand-footer {
  color: rgba(255, 255, 255, 0.40);
  font-size: 0.75rem;
}

/* Mini image card in the brand panel — clickable placeholder for an
   upcoming user-provided image + link destination. Coral border = the
   single visual accent surface in the brand panel (other than the bullet
   checkmarks). Hover lifts via shadow + outline; the actual border
   stays 1px to avoid box-jump on hover. */
.brand-image-card {
  margin: var(--space-2xl) auto 0 auto;
  width: 240px;
  height: 140px;
  background: transparent;
  border: 1px solid var(--color-accent-vibrant);
  border-radius: var(--radius-lg);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: box-shadow 0.15s ease-out, border-color 0.15s ease-out;
}

.brand-image-card:hover,
.brand-image-card:focus-visible {
  border-color: color-mix(in srgb, var(--color-accent-vibrant), white 15%);
  box-shadow:
    0 0 0 1px var(--color-accent-vibrant),
    var(--shadow-sm);
  outline: none;
}

.brand-image-card__placeholder {
  font-size: 0.875rem;
  color: rgba(255, 255, 255, 0.30);
}

/* --- Auth header (centered cards: login, forgot, reset, invite) --- */
.auth-header {
  margin-bottom: var(--space-2xl);
}

.auth-logo {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  font-size: 1rem;
  font-weight: 700;
  color: var(--color-text-primary);
  /* Vertical compaction (item 4, 2026-05-31): xl→lg. */
  margin-bottom: var(--space-lg);
  text-decoration: none;
}

.auth-logo:hover { text-decoration: none; }

/* Centered-card mark — black square with white "D" (mirrors topbar mark).
   On register the brand-panel uses .brand-logo__mark instead, which is
   the inverted B1 variant (white square, black "D"). */
.auth-logo__mark {
  width: 28px;
  height: 28px;
  background: var(--color-text-primary);
  border-radius: var(--radius-sm);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 0.75rem;
  font-weight: 800;
  color: var(--color-text-inverse);
  flex-shrink: 0;
}

/* Centered-card H1 (Login, Forgot, Reset, Invite). Inter sans 700 at
   28px, matching the Register form-card H1 below. VDP Step 3 unified
   all auth H1s on sans after user feedback: "la app es más seria con
   la letra de register" — serif felt too editorial for in-product
   contractor B2B context. The --font-serif token stays in the system
   for marketing/landing surfaces where editorial tone fits. */
.auth-header h1 {
  font-family: var(--font-sans);
  font-size: 1.75rem;
  font-weight: 700;
  line-height: 1.25;
  letter-spacing: -0.015em;
  color: var(--color-text-primary);
  margin-bottom: var(--space-xs);
}
.auth-header p {
  color: var(--color-text-muted);
  font-size: 0.875rem;
}

/* =========================================================
   FORM COMPONENTS
   ========================================================= */

/* Vertical compaction (Bundle Estética/UX item 4, 2026-05-31):
   GLOBAL tightening of form spacing — applies to every form using
   .form-section / .form-section__label / .form-row / .field. Goal:
   forms (register especially, but all of them) fit within standard
   laptop usable viewports without scroll. NO font/input-height
   changes; spacing only. */
.form-section {
  margin-bottom: .75rem;
}

.form-section__label {
  display: block;
  font-size: 0.75rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-text-muted);
  margin-bottom: var(--space-sm);
  padding-bottom: var(--space-xs);
  border-bottom: 1px solid var(--color-border-subtle);
}

.form-row {
  display: grid;
  gap: .85rem;          /* horizontal gap (2-col rows) — unchanged */
  margin-bottom: .6rem; /* vertical gap between rows — tightened */
}

.form-row--2 { grid-template-columns: 1fr 1fr; }

@media (max-width: 520px) {
  .form-row--2 { grid-template-columns: 1fr; }
}

.field {
  display: flex;
  flex-direction: column;
  gap: .3rem;
}

.field label {
  font-size: 0.875rem;
  font-weight: 500;
  color: var(--color-text-primary);
  display: flex;
  justify-content: space-between;
  align-items: center;
  /* Vertical compaction (item 4, 2026-05-31): margin-bottom DELETED.
     The visible label→input gap now comes purely from .field's
     `gap: .3rem` (5px). Margins-in-flex don't collapse so prior
     8px margin + 5px gap = 13px was conspicuously loose. If the
     5px proves too tight in smoke, the easy follow-up is .field
     { gap: .35rem | .4rem } (~5.6-6.4px). */
}

.field-hint {
  font-size: 0.75rem;
  color: var(--color-text-muted);
  font-weight: 400;
  letter-spacing: 0.01em;
}

/* Inline form warning (Price List Data Integrity Sprint, Phase D).
   Surfaces a non-fatal advisory beneath the form fields — e.g. "another
   item with the same code+type already exists". Distinct from .flash --
   .flash lives at the top of the page surface, this lives next to the
   fields it warns about. Amber bg matches the warning-token convention
   without competing with coral (reserved for accent), error red, or
   success green. */
.field-inline-warning {
  margin: var(--space-sm) 0;
  padding: 0.6rem 0.85rem;
  font-size: 0.825rem;
  line-height: 1.45;
  background: #fff4d6;
  border: 1px solid #f4c95d;
  border-radius: var(--radius-sm);
  color: #6a4a00;
}
.field-inline-warning code.code-label {
  color: inherit;
}

/* Inline label link (e.g. "Forgot password?" beside the Password label).
   Same-flow link, stays gray — NOT coral. Coral is reserved for
   cross-flow login↔register transitions. */
.field-label-link {
  font-size: 0.875rem;
  font-weight: 400;
  color: var(--color-text-secondary);
}
.field-label-link:hover {
  color: var(--color-text-primary);
  text-decoration: none;
}

.field input,
.field select {
  height: var(--height-input);
  padding: 0 var(--space-lg);
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius-sm);
  font-size: 1rem;
  font-family: var(--font-sans);
  color: var(--color-text-primary);
  background: var(--color-bg);
  transition: border-color 0.15s ease-out, box-shadow 0.15s ease-out;
  width: 100%;
}

.field select {
  appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23888888' d='M6 8L1 3h10z'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right var(--space-lg) center;
  padding-right: var(--space-3xl);
}

/* Focus halo: dark monochrome ring (was blue) per Phase 2 Stage B spec. */
.field input:focus,
.field select:focus {
  outline: none;
  border-color: var(--color-text-primary);
  box-shadow: 0 0 0 3px rgba(17, 17, 17, 0.10);
}

.field input::placeholder { color: var(--color-text-muted); }

/* Batch 6 Group 1 — Company Details readability bump (scoped to this panel
   only; other settings pages keep the standard field sizing). */
.settings-company .field label { font-size: 0.95rem; }
.settings-company .field-hint { font-size: 0.8125rem; }
.settings-company .field input,
.settings-company .field select,
.settings-company .field textarea { font-size: 1rem; }

/* Password field with eye toggle */
.field-password {
  position: relative;
  display: flex;
  align-items: center;
}

.field-password input {
  padding-right: 2.75rem;
  width: 100%;
}

.pwd-toggle {
  position: absolute;
  right: .6rem;
  background: none;
  border: none;
  cursor: pointer;
  color: var(--color-text-muted);
  padding: .2rem;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 4px;
  transition: color .15s;
  flex-shrink: 0;
}

.pwd-toggle:hover { color: var(--color-text-primary); }

/* Checkbox */
.field-checkbox {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  margin-bottom: var(--space-md);
}

.field-checkbox input[type="checkbox"] {
  width: 18px;
  height: 18px;
  cursor: pointer;
  accent-color: var(--color-accent);
  flex-shrink: 0;
}

.field-checkbox label {
  font-size: 0.875rem;
  color: var(--color-text-secondary);
  cursor: pointer;
}

/* =========================================================
   BUTTONS — VDP Step 4 system

   8 variants × 3 sizes × full state matrix (default, hover, active,
   disabled, focus-visible). Focus uses :focus-visible only so mouse
   clicks don't show the keyboard ring.

   Variants:
   - .btn-primary           filled black, single primary CTA per page
   - .btn-secondary         outlined, FULL INVERSION on hover
                            (Sign out, Edit project, Refresh totals, etc.)
   - .btn-secondary--subtle modifier on .btn-secondary preserving the
                            prior gentle hover (Cancel, Select all,
                            view toggles, Settings buttons)
   - .btn-ghost             truly borderless, color-shift hover
                            (system-defined; no template usage today)
   - .btn-link              inline text link with hover fix applied
                            (Edit row actions; was broken pre-Step-4)
   - .btn-danger            filled red, irreversible CTA in confirm
                            dialogs only
   - .btn-danger-link       inline destructive link (renamed from
                            .btn-link--danger; Delete row actions)
   - .btn-white             on-dark variant for hero overlays
                            (system-defined; no template usage today)

   Sizes (second class):
   - .btn-sm   32px height, 14px font, 12px padding
   - (default) 40px height, 14px font, 16px padding
   - .btn-lg   48px height, 16px font, 24px padding

   Loading-state spinner infrastructure DEFERRED — no template needs it
   today. Existing buttons that swap text on submit (e.g. Upload)
   continue using their current text-swap pattern.
   ========================================================= */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-xs);
  height: 40px;
  padding: 0 var(--space-lg);
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  font-size: 0.875rem;
  font-weight: 500;
  font-family: var(--font-sans);
  cursor: pointer;
  transition:
    background-color 0.15s ease-out,
    color 0.15s ease-out,
    border-color 0.15s ease-out,
    box-shadow 0.15s ease-out;
  text-decoration: none;
  white-space: nowrap;
}

.btn:hover { text-decoration: none; }

.btn:disabled,
.btn[disabled] { cursor: not-allowed; }

/* Focus halo (keyboard-only via :focus-visible). Variants override the
   ring color where needed (red for .btn-danger / .btn-danger-link,
   white for .btn-white). */
.btn:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px rgba(17, 17, 17, 0.12);
}

/* ----- Sizes ----- */

.btn-sm {
  height: 32px;
  padding: 0 var(--space-md);
  font-size: 0.875rem;
}

.btn-lg {
  height: 48px;
  padding: 0 var(--space-xl);
  font-size: 1rem;
}

/* Width utility — full-width button (e.g. auth-card submit). */
.btn-full { width: 100%; }

/* ----- Variant: PRIMARY (filled black, RED hover for brand-identity
   moment on every primary CTA — Sign in, Create workspace, Save,
   Analyze, Apply changes, etc.) ----- */

.btn-primary {
  background: var(--color-accent);
  color: var(--color-accent-text);
  border-color: var(--color-accent);
}
.btn-primary:hover {
  background: var(--color-accent-vibrant);
  color: var(--color-accent-text);
  border-color: var(--color-accent-vibrant);
}
.btn-primary:active {
  background: #D62150;
  color: var(--color-accent-text);
  border-color: #D62150;
}
.btn-primary:disabled,
.btn-primary[disabled] {
  background: var(--color-accent-disabled);
  color: var(--color-accent-text);
  border-color: var(--color-accent-disabled);
}
.btn-primary:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px rgba(255, 49, 88, 0.20);
}

/* ----- Variant: SECONDARY (outlined, FULL INVERSION on hover) ----- */

.btn-secondary {
  background: transparent;
  color: var(--color-text-primary);
  border-color: var(--color-border-default);
}
.btn-secondary:hover {
  background: var(--color-text-primary);
  color: var(--color-text-inverse);
  border-color: var(--color-text-primary);
}
.btn-secondary:active {
  background: var(--color-accent-active);
  color: var(--color-text-inverse);
  border-color: var(--color-accent-active);
}
.btn-secondary:disabled,
.btn-secondary[disabled] {
  background: transparent;
  color: var(--color-text-disabled);
  border-color: var(--color-border-default);
}

/* ----- Modifier: SECONDARY--SUBTLE (overrides hover/active for buttons
   that need the prior gentle hover rather than full inversion). Stacks
   on top of .btn-secondary. Used by Cancel, Select all, view toggles,
   Settings buttons, and the Current-plan-disabled indicator. ----- */

/* Level 2 hover: clearly visible bg shift + darker border than the
   default --subtle pre-Step-4-adjustment treatment, so users get
   unmistakable feedback without the full inversion the unmodified
   .btn-secondary uses. Cancel, Select all, Settings buttons, view
   toggles all consume this. */
.btn-secondary--subtle:hover {
  background: var(--color-border-subtle);
  color: var(--color-text-primary);
  border-color: var(--color-text-muted);
}
.btn-secondary--subtle:active {
  background: var(--color-border-default);
  color: var(--color-text-primary);
  border-color: var(--color-text-secondary);
}

/* ----- Variant: GHOST (truly borderless, text-only). System-defined
   for future low-emphasis utilities; no template usage as of Step 4. */

.btn-ghost {
  background: transparent;
  color: var(--color-text-secondary);
  border-color: transparent;
}
.btn-ghost:hover {
  color: var(--color-text-primary);
}
.btn-ghost:active {
  color: var(--color-accent-hover);
}
.btn-ghost:disabled,
.btn-ghost[disabled] {
  color: var(--color-text-disabled);
}

/* ----- Variant: LINK (inline text link — Edit row actions). No fixed
   height; sized by parent context. Hover fix applied (text shift +
   underline) — fixes the Edit hover bug surfaced during VDP Step 3
   visual review. ----- */

.btn-link {
  background: none;
  border: none;
  padding: 0;
  height: auto;
  cursor: pointer;
  font-size: 0.875rem;
  font-weight: 500;
  font-family: var(--font-sans);
  color: var(--color-text-secondary);
  text-decoration: none;
}
.btn-link:hover {
  color: var(--color-text-primary);
  text-decoration: underline;
}
.btn-link:active {
  color: var(--color-accent-hover);
}
.btn-link:disabled,
.btn-link[disabled] {
  color: var(--color-text-disabled);
  text-decoration: none;
  cursor: not-allowed;
}
.btn-link:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px rgba(17, 17, 17, 0.12);
  border-radius: 2px;
}

/* ----- Variant: DANGER (filled red — irreversible CTA inside confirm
   dialogs only). Red focus ring instead of the dark default. ----- */

.btn-danger {
  background: var(--color-error);
  color: var(--color-text-inverse);
  border-color: var(--color-error);
}
.btn-danger:hover {
  background: #8B1A12;
  color: var(--color-text-inverse);
  border-color: #8B1A12;
}
.btn-danger:active {
  background: #6B1310;
  border-color: #6B1310;
}
.btn-danger:disabled,
.btn-danger[disabled] {
  background: var(--color-accent-disabled);
  color: var(--color-text-inverse);
  border-color: var(--color-accent-disabled);
}
.btn-danger:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px rgba(180, 35, 24, 0.18);
}

/* ----- Variant: DANGER-LINK (inline destructive — Delete row actions).
   Renamed from .btn-link--danger. Shares .btn-link base shape with
   error-tone palette throughout. ----- */

.btn-danger-link {
  background: none;
  border: none;
  padding: 0;
  height: auto;
  cursor: pointer;
  font-size: 0.875rem;
  font-weight: 500;
  font-family: var(--font-sans);
  color: var(--color-error);
  text-decoration: none;
}
.btn-danger-link:hover {
  color: #8B1A12;
  text-decoration: underline;
}
.btn-danger-link:active {
  color: #6B1310;
}
.btn-danger-link:disabled,
.btn-danger-link[disabled] {
  color: var(--color-text-disabled);
  text-decoration: none;
  cursor: not-allowed;
}
.btn-danger-link:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px rgba(180, 35, 24, 0.18);
  border-radius: 2px;
}

/* ----- Variant: DANGER-SECONDARY (outlined red, red-fill inversion
   on hover). Page-header-level destructive actions where the affordance
   needs button-sized visual presence (not a text-link) AND should
   communicate destructive intent via red. Distinct from .btn-danger
   (filled red default — modal-confirm-only). Today used for "Delete
   project" in projects/detail.html. ----- */

.btn-danger-secondary {
  background: transparent;
  color: var(--color-error);
  border-color: var(--color-error);
}
.btn-danger-secondary:hover {
  background: var(--color-error);
  color: var(--color-text-inverse);
  border-color: var(--color-error);
}
.btn-danger-secondary:active {
  background: #8B1A12;
  color: var(--color-text-inverse);
  border-color: #8B1A12;
}
.btn-danger-secondary:disabled,
.btn-danger-secondary[disabled] {
  background: transparent;
  color: var(--color-text-disabled);
  border-color: var(--color-border-default);
}
.btn-danger-secondary:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px rgba(180, 35, 24, 0.18);
}

/* ----- Variant: WHITE (on-dark variant for hero overlays). System-
   defined for future use on dark photo overlays; no template usage as
   of Step 4. White focus ring. ----- */

.btn-white {
  background: transparent;
  color: var(--color-text-inverse);
  border-color: rgba(255, 255, 255, 0.4);
}
.btn-white:hover {
  background: var(--color-text-inverse);
  color: var(--color-text-primary);
  border-color: var(--color-text-inverse);
}
.btn-white:active {
  background: rgba(255, 255, 255, 0.85);
  color: var(--color-text-primary);
  border-color: rgba(255, 255, 255, 0.85);
}
.btn-white:disabled,
.btn-white[disabled] {
  background: transparent;
  color: rgba(255, 255, 255, 0.4);
  border-color: rgba(255, 255, 255, 0.2);
}
.btn-white:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.30);
}

.btn-submit-row {
  /* Vertical compaction (item 4, 2026-05-31): xl(24) → lg(16) initially,
     then bumped to 1.25rem (20) when 16 read too tight. Register has
     its own +4 override below (.auth-split__form-inner .btn-submit-row). */
  margin-top: 1.25rem;
}

/* Register-only: 48px above the submit button (3xl). Global 20px on
   .btn-submit-row above reads OK on shorter forms (login / forgot /
   reset / invite / in-app forms); register's long form benefits from
   deliberate breathing between Confirm Password and the submit CTA.
   Scoped via .auth-split__form-inner so only register's --split
   layout gets the extra space. Bundle Estética/UX item 4 tweak
   (final), 2026-05-31. */
.auth-split__form-inner .btn-submit-row {
  margin-top: var(--space-3xl);
}

/* =========================================================
   FLASH MESSAGES
   ========================================================= */

.flashes {
  display: flex;
  flex-direction: column;
  gap: .5rem;
  margin-bottom: 1.25rem;
}

.flash {
  padding: var(--space-sm) var(--space-md);
  border-radius: var(--radius-sm);
  font-size: .875rem;
  line-height: 1.45;
  display: flex;
  align-items: flex-start;
  gap: var(--space-sm);
}

/* VDP Step 13 — token migration in-place. Pattern: pale --color-X-bg
   bg + deep --color-X text + 1px color-mix border (Matterport-
   restrained, matches the .invite-meta + .preview-empty-pricelist
   recipes). The new .flash--warning closes the Day-4 backlog
   (--warning was previously undefined). */
.flash--error {
  background: var(--color-error-bg);
  border: 1px solid color-mix(in srgb, var(--color-error), white 75%);
  color: var(--color-error);
}
.flash--success {
  background: var(--color-success-bg);
  border: 1px solid color-mix(in srgb, var(--color-success), white 75%);
  color: var(--color-success);
}
.flash--info {
  background: var(--color-info-bg);
  border: 1px solid color-mix(in srgb, var(--color-info), white 75%);
  color: var(--color-info);
}
.flash--warning {
  background: var(--color-warning-bg);
  border: 1px solid color-mix(in srgb, var(--color-warning), white 75%);
  color: var(--color-warning);
}

/* === BILLING FREEZE BANNER (Phase C) === */
.freeze-banner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  padding: var(--space-sm) var(--space-md);
  margin-bottom: var(--space-md);
  background: var(--color-warning-bg);
  border: 1px solid color-mix(in srgb, var(--color-warning), white 70%);
  border-radius: var(--radius-md);
  color: var(--color-warning);
}
.freeze-banner__text   { font-size: 0.9rem; line-height: 1.4; }
.freeze-banner__action { flex-shrink: 0; }

/* === FLASH TOAST (app-shell) ===
   App-shell flashes render here (layout/app.html) instead of in document
   flow, so they stay visible when the user is scrolled down a long list.
   Desktop: top-right, below the 56px topbar. Mobile: bottom-center thumb
   zone, raised to clear a sticky footer when present (same :has() offset
   trick as the Copilot launcher). z-index 1100: above the topbar (100) and
   Copilot (1001-1003), below the mobile nav drawer (1500) and native
   <dialog> (top-layer). toast.js animates entrance + auto-dismisses
   success/info; errors stay until the × is clicked (role=alert). Auth
   pages keep their inline card flash (out of scope). Colors reuse the
   .flash--* recipes verbatim. */
.toast-stack {
  position: fixed;
  top: calc(56px + var(--space-md));
  right: var(--space-md);
  z-index: 1100;
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  width: min(360px, calc(100vw - 2 * var(--space-md)));
  pointer-events: none;   /* gaps don't block the page; toasts re-enable below */
}

.toast {
  pointer-events: auto;
  display: flex;
  align-items: flex-start;
  gap: var(--space-sm);
  padding: var(--space-sm) var(--space-md);
  border-radius: var(--radius-sm);
  font-size: .875rem;
  line-height: 1.45;
  box-shadow: var(--shadow-md);
  opacity: 0;
  transform: translateY(-8px);     /* desktop: drop in from above */
  transition: opacity .2s ease-out, transform .2s ease-out;
}
.toast--in  { opacity: 1; transform: none; }
.toast--out { opacity: 0; transform: translateY(-8px); }

.toast__msg {
  flex: 1 1 auto;
  min-width: 0;
  overflow-wrap: anywhere;   /* long CSV-import error wraps, never clips/overflows */
  white-space: pre-line;     /* preserve newlines in multi-line errors */
}

.toast__close {
  flex: 0 0 auto;
  background: transparent;
  border: 0;
  cursor: pointer;
  font-size: 1.15rem;
  line-height: 1;
  padding: 0 2px;
  color: inherit;
  opacity: .65;
}
.toast__close:hover { opacity: 1; }

/* "Dismiss all" — injected by toast.js only when 2+ toasts are present.
   Understated on purpose (small, muted) so an unread error isn't cleared
   by reflex; it's a one-click safety net for accumulated errors. */
.toast-dismiss-all {
  pointer-events: auto;
  align-self: flex-end;          /* desktop: right edge, above the stack */
  /* Discoverability fix (2026-06-08): was white-bg + muted #888 text +
     light border = barely visible. Now a soft-gray filled pill (separates
     from the white page) with dark readable bold text + a defined border,
     so it clearly reads as a tappable control — yet stays SECONDARY (no
     accent/coral, not solid-black at rest) so an unread error isn't
     cleared by reflex. */
  background: var(--color-border-subtle);            /* #F0F0F0 soft-gray fill */
  color: var(--color-text-primary);                  /* #111 — readable (was #888) */
  border: 1.5px solid var(--color-border-strong);    /* #CCC — reads as a control */
  border-radius: 999px;                              /* pill — unmistakably a button */
  font-size: .8rem;
  font-weight: 600;
  line-height: 1.2;
  padding: 5px 14px;                                 /* larger tap target */
  cursor: pointer;
  box-shadow: var(--shadow-sm);
}
.toast-dismiss-all:hover {       /* full inversion (mirrors .btn-secondary) — confirms it's clickable */
  background: var(--color-text-primary);
  color: var(--color-text-inverse);
  border-color: var(--color-text-primary);
}

.toast--error {
  background: var(--color-error-bg);
  border: 1px solid color-mix(in srgb, var(--color-error), white 75%);
  color: var(--color-error);
}
.toast--success {
  background: var(--color-success-bg);
  border: 1px solid color-mix(in srgb, var(--color-success), white 75%);
  color: var(--color-success);
}
.toast--info {
  background: var(--color-info-bg);
  border: 1px solid color-mix(in srgb, var(--color-info), white 75%);
  color: var(--color-info);
}
.toast--warning {
  background: var(--color-warning-bg);
  border: 1px solid color-mix(in srgb, var(--color-warning), white 75%);
  color: var(--color-warning);
}

/* Mobile: TOP-center, below the 56px topbar (more visible than the bottom,
   which the user could barely see). Top placement means NO sticky-footer
   clearance is needed, and the base slide-from-above animation
   (.toast translateY(-8px)) applies unchanged. */
@media (max-width: 767px) {
  .toast-stack {
    top: calc(56px + var(--space-md));
    bottom: auto;
    left: 50%;
    right: auto;
    transform: translateX(-50%);   /* centers the STACK; toasts animate their own transform */
    width: min(440px, calc(100vw - 2 * var(--space-md)));
  }
  .toast-dismiss-all { align-self: center; }
}

/* Reduced motion: appear/disappear without sliding (toast.js still removes). */
@media (prefers-reduced-motion: reduce) {
  .toast      { transform: none; transition: opacity .15s ease-out; }
  .toast--in  { transform: none; }
  .toast--out { transform: none; }
}

/* =========================================================
   AUTH FOOTER
   ========================================================= */

.auth-foot {
  text-align: center;
  margin-top: var(--space-xl);
  font-size: 0.875rem;
  color: var(--color-text-secondary);
}

/* Default auth-foot link — gray, plain. Used for backward navigation
   ("Back to sign in") and same-flow links. For cross-flow CTAs
   (login↔register / invite→login) use .auth-foot__cta instead. */
.auth-foot a {
  color: var(--color-text-secondary);
  font-weight: 400;
  text-decoration: none;
}
.auth-foot a:hover {
  color: var(--color-text-primary);
  text-decoration: none;
}

/* Cross-flow CTA link — coral (#FF3158), slightly bolder. Used to drive
   the user from login → register, register → login, invite → login. */
.auth-foot__cta {
  color: var(--color-accent-vibrant);
  font-weight: 500;
}
.auth-foot__cta:hover {
  color: color-mix(in srgb, var(--color-accent-vibrant), black 12%);
  text-decoration: none;
}

/* =========================================================
   ONBOARDING WIZARD (stub)
   ========================================================= */

.wizard-page {
  min-height: 100vh;
  background: var(--color-surface-alt);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 2rem 1rem;
}

.wizard-card {
  background: var(--color-surface);
  border: 1px solid var(--color-border-default);
  border-radius: 12px;
  box-shadow: var(--shadow-md);
  padding: 3rem 2.5rem;
  width: 100%;
  max-width: 560px;
  text-align: center;
}

.wizard-card h1 { margin-bottom: .5rem; }
.wizard-card p  { color: var(--color-text-muted); margin-bottom: 2rem; }

.wizard-step-indicator {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: .5rem;
  margin-bottom: 2.5rem;
}

.wizard-step {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--color-border-default);
}

.wizard-step--active { background: var(--color-accent); width: 24px; border-radius: 4px; }

/* =========================================================
   DASHBOARD (minimal stub)
   ========================================================= */

.app-shell {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

/* App topbar — global nav surface across all authenticated pages.
   VDP Step 2 migration to design tokens (2026-05-15):
   - background → pure white via --color-bg (not the tinted --color-surface;
     the topbar sits above body content and reads better with a flat white)
   - logo wordmark → --color-text-primary, 1rem (was .95rem at old 15px base)
   - spacing → tokens (24px horizontal, 16px gap → --space-xl / --space-lg) */
.app-topbar {
  height: 56px;
  background: var(--color-bg);
  border-bottom: 1px solid var(--color-border-default);
  display: flex;
  align-items: center;
  padding: 0 var(--space-xl);
  gap: var(--space-lg);
  position: sticky;
  top: 0;
  z-index: 100;
}

.app-topbar__logo {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  font-weight: 700;
  color: var(--color-text-primary);
  font-size: 1rem;
  text-decoration: none;
}

.app-topbar__logo:hover { text-decoration: none; }

.app-topbar__spacer { flex: 1; }

.app-content {
  flex: 1;
  padding: 2rem 3rem;
  max-width: 1600px;
  margin: 0 auto;
  width: 100%;
  /* Bundle Estética/UX item 6 Layer 1 (2026-05-31): flex items
     default to min-width: auto (shrink only to content's intrinsic
     min size, NOT below). On mobile, a wide table or min-width-
     constrained flex/grid item inside .app-content would push the
     flex item wider than its container — combined with no horizontal
     scroll containment higher up, that stretched the whole document
     past viewport width. min-width: 0 lets .app-content shrink to its
     allotted flex space regardless of content; paired with the html/
     body overflow-x: hidden above. Layer 2 (pending) wraps wide
     tables in scroll containers so the user can still access wide
     content; this layer just stops the page-stretching. */
  min-width: 0;
}

@media (max-width: 768px) {
  .app-content { padding: 1.5rem 1rem; }

  /* Topbar tightening at mobile — the actual hide rules for nav /
     usage / user / signout-form live in the SECOND @media (max-width:
     768px) block after the .app-topbar__nav base rule (see 2c section
     below), so source-order cascade resolves correctly. */
  .app-topbar { padding: 0 var(--space-lg); gap: var(--space-sm); }
}

/* =========================================================
   INVITE BADGE
   ========================================================= */

.invite-meta {
  background: var(--color-info-bg);
  border: 1px solid color-mix(in srgb, var(--color-info), white 75%);
  border-radius: var(--radius-sm);
  padding: var(--space-md) var(--space-lg);
  font-size: 0.875rem;
  color: var(--color-info);
  margin-bottom: var(--space-xl);
}

.invite-meta strong { font-weight: 600; }

/* =========================================================
   APP TOPBAR — extended
   ========================================================= */

.app-topbar__mark {
  width: 26px;
  height: 26px;
  background: var(--color-text-primary);
  border-radius: var(--radius-sm);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: .75rem;
  font-weight: 800;
  color: var(--color-text-inverse);
  flex-shrink: 0;
}

.app-topbar__nav {
  display: flex;
  gap: var(--space-xs);
  margin-left: var(--space-sm);
}

.app-topbar__navlink {
  padding: var(--space-xs) var(--space-md);
  font-size: .875rem;
  font-weight: 500;
  color: var(--color-text-secondary);
  border-radius: var(--radius-sm);
  text-decoration: none;
  transition: color .15s;
}

/* Hover: text-color shift only — NO background tint per VDP Step 2
   user decision. Cleaner, more Matterport-aligned than the prior
   gray-pill hover. */
.app-topbar__navlink:hover {
  color: var(--color-text-primary);
  text-decoration: none;
}

/* Active state: 2px BLACK underline, NOT coral. Coral (#FF3158) is
   reserved for logo + promotional banners per user decision; the active
   nav item still reads clearly via the underline + text-color + weight
   combo. The prior background-pill treatment is also removed for a
   cleaner underline-only effect. */
.app-topbar__navlink--active {
  color: var(--color-text-primary);
  font-weight: 600;
  box-shadow: inset 0 -2px 0 var(--color-text-primary);
}

/* Section wayfinding icons (C / Option 1) — one sober monochrome line icon per
   section, in the nav AND the page <h1>. stroke=currentColor tracks the text
   color (so the active nav item's icon darkens with it); shape-based, so
   wayfinding never relies on color alone (colorblind-safe). em-relative align
   scales for the 16px nav icon and the 22px header icon alike. */
.section-icon { vertical-align: -0.18em; }
.app-topbar__navlink .section-icon,
.app-mobile-drawer__link .section-icon { margin-right: 6px; }
.page-header h1 .section-icon { margin-right: 8px; }

/* Per-section accent (C) — a SUBTLE tint on the ICON ONLY (the rest of the
   chrome stays sober). stroke=currentColor, so setting `color` tints the icon
   in BOTH the nav and the page header at once. Tones are from the app's
   existing curated sober palette (_WORKSPACE_ACCENTS) so section tints
   harmonize with the workspace tints; coral (#FF3158) stays reserved for CTAs.
   Shape stays the PRIMARY cue (colorblind-safe); color is reinforcement.
   The work sections are tinted; the utility sections (Help / Settings / Admin)
   stay neutral (currentColor — still tracking the active state). */
.section-icon--projects    { color: #2D6A92; }  /* steel blue */
.section-icon--price_lists { color: #118069; }  /* teal-green */
.section-icon--reports     { color: #5E4CA0; }  /* muted indigo */
.section-icon--comparator  { color: #9A5630; }  /* bronze */

/* VDP Step 5: usage indicator (Free trial / N%). Sits before the user
   name; clickable → settings.subscription. Subtle vertical separator
   from the user-name span via border-left on .app-topbar__user. */
.app-topbar__usage {
  font-size: 0.875rem;
  font-weight: 500;
  color: var(--color-text-primary);
  text-decoration: none;
  padding: var(--space-xs) var(--space-sm);
  border-radius: var(--radius-sm);
  transition: color 0.15s ease-out;
}
.app-topbar__usage:hover {
  color: var(--color-accent-vibrant);
  text-decoration: none;
}

.app-topbar__user {
  font-size: 0.875rem;
  color: var(--color-text-muted);
  padding-left: var(--space-md);
  border-left: 1px solid var(--color-border-default);
  margin-left: var(--space-xs);
}

/* =========================================================
   TOPBAR IDENTITY — active workspace label + user + avatar
   (Topbar redesign, 2026-06-10). Colored workspace label
   (the active-workspace cue for multi-cliente) + user name
   stacked, with a deterministic colored-initials avatar.
   Sober: the only color is the per-workspace accent on the
   label text + avatar fill. Collapses to avatar-only on
   mobile (see the ≤768px block below).
   ========================================================= */
.app-topbar__identity {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  padding-left: var(--space-md);
  border-left: 1px solid var(--color-border-default);
  margin-left: var(--space-xs);
  min-width: 0;
}
.app-topbar__identity-text {
  display: flex;
  flex-direction: column;
  align-items: flex-end;   /* right-align toward the avatar / corner */
  line-height: 1.25;
  min-width: 0;
}
.app-topbar__workspace {
  font-size: 0.68rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  max-width: 200px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.app-topbar__username {
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--color-text-primary);
  max-width: 200px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.app-topbar__avatar {
  /* Mirrors the proven .app-topbar__mark letter-in-a-box pattern (display:flex
     + line-height:1) so the centered initials always render. color:#fff is ALSO
     set inline on the element (see layout/app.html) as a stale-cache safeguard. */
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  color: #fff;
  font-size: 0.75rem;
  font-weight: 700;
  line-height: 1;
  letter-spacing: 0.02em;
  flex-shrink: 0;
  /* WeasyPrint/print + ensure the accent fill always renders */
  print-color-adjust: exact;
  -webkit-print-color-adjust: exact;
}

/* =========================================================
   APP MOBILE NAV — Estética/UX item 2c (2026-06-01)
   Hamburger button (topbar) + right-side slide-in drawer +
   backdrop scrim. Only rendered on authenticated pages.
   Visible only at ≤768px; desktop hides all 3 entirely.
   z-index: scrim 1500, drawer 1501 — above Copilot panel
   (1001) and edit toasts (1002-1003). Below native <dialog>
   top-layer (which renders above any z-index, so confirmation
   dialogs triggered FROM the drawer still overlay correctly).
   ========================================================= */

/* Hamburger button — hidden by default (desktop); revealed at mobile
   breakpoint via the @media (max-width: 768px) block below. Sits at
   the rightmost end of the topbar (DOM-last after signout form). */
.app-topbar__hamburger {
  display: none;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  padding: 0;
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius-sm);
  background: var(--color-bg);
  color: var(--color-text-primary);
  cursor: pointer;
  transition: background 120ms ease, border-color 120ms ease;
}
.app-topbar__hamburger:hover {
  background: var(--color-surface-alt);
  border-color: var(--color-text-secondary);
}
.app-topbar__hamburger:focus-visible {
  outline: 2px solid var(--color-text-primary);
  outline-offset: 2px;
}

/* Drawer — right-side slide-in panel. Closed state: translated off
   the right edge of viewport. Open state (.is-open): translate(0).
   width: min(280px, 85vw) keeps it readable on small phones without
   overwhelming the screen. */
.app-mobile-drawer {
  display: none;
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  width: min(280px, 85vw);
  background: var(--color-bg);
  box-shadow: -8px 0 24px rgba(0, 0, 0, 0.18);
  z-index: 1501;
  flex-direction: column;
  transform: translateX(100%);
  transition: transform 200ms cubic-bezier(0.2, 0, 0, 1);
  /* Disable iOS double-tap-zoom delay on internal taps. */
  touch-action: manipulation;
  /* Allow vertical scroll inside the drawer if its content gets tall. */
  overflow-y: auto;
}
.app-mobile-drawer.is-open { transform: translateX(0); }

/* Removing [hidden] visually-hides via display:none — overriding
   the global [hidden] !important reset from VDP for our explicit
   show-toggle. JS adds/removes `hidden` to gate display: none vs
   display: flex AND the .is-open class for the slide animation. */
.app-mobile-drawer:not([hidden]) {
  display: flex;
}

.app-mobile-drawer__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--space-md) var(--space-lg);
  border-bottom: 1px solid var(--color-border-default);
  flex-shrink: 0;
}
.app-mobile-drawer__title {
  font-weight: 600;
  font-size: 1rem;
}
.app-mobile-drawer__close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  padding: 0;
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  color: var(--color-text-primary);
  cursor: pointer;
  transition: background 120ms ease, border-color 120ms ease;
}
.app-mobile-drawer__close:hover {
  background: var(--color-surface-alt);
  border-color: var(--color-border-default);
}
.app-mobile-drawer__close:focus-visible {
  outline: 2px solid var(--color-text-primary);
  outline-offset: 2px;
}

.app-mobile-drawer__nav {
  display: flex;
  flex-direction: column;
  padding: var(--space-sm) var(--space-sm);
  gap: 2px;
}
.app-mobile-drawer__link {
  display: block;
  padding: var(--space-sm) var(--space-md);
  border-radius: var(--radius-sm);
  color: var(--color-text-primary);
  font-size: 0.9375rem;
  font-weight: 500;
  text-decoration: none;
  transition: background 120ms ease;
}
.app-mobile-drawer__link:hover {
  background: var(--color-surface-alt);
  text-decoration: none;
}
.app-mobile-drawer__link:focus-visible {
  outline: 2px solid var(--color-text-primary);
  outline-offset: -2px;
}
.app-mobile-drawer__link.is-active {
  background: var(--color-surface-alt);
  font-weight: 600;
  box-shadow: inset 2px 0 0 var(--color-text-primary);
}

.app-mobile-drawer__usage {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin: var(--space-sm);
  padding: var(--space-sm) var(--space-md);
  background: var(--color-surface-alt);
  border-radius: var(--radius-sm);
  text-decoration: none;
  color: var(--color-text-primary);
  font-size: 0.875rem;
  border-top: 1px solid var(--color-border-subtle);
}
.app-mobile-drawer__usage:hover { text-decoration: none; }
.app-mobile-drawer__usage-label { color: var(--color-text-muted); }
.app-mobile-drawer__usage-value { font-weight: 600; }

.app-mobile-drawer__footer {
  margin-top: auto;
  padding: var(--space-md) var(--space-lg);
  border-top: 1px solid var(--color-border-default);
  flex-shrink: 0;
}
/* Topbar redesign (2026-06-10) — workspace context in the drawer footer
   (mobile topbar is avatar-only, so the workspace name surfaces here). */
.app-mobile-drawer__workspace {
  font-size: 0.68rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  margin-bottom: var(--space-xs);
}
.app-mobile-drawer__user {
  font-size: 0.8125rem;
  color: var(--color-text-muted);
  margin-bottom: 2px;
}
.app-mobile-drawer__user strong { color: var(--color-text-primary); font-weight: 600; }
.app-mobile-drawer__email {
  font-size: 0.75rem;
  color: var(--color-text-muted);
  margin-bottom: var(--space-sm);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.app-mobile-drawer__signout { width: 100%; }

/* Scrim — semi-opaque backdrop behind the drawer. Closes drawer on
   click. pointerdown listener (not click) for iOS reliability. */
.app-mobile-drawer__scrim {
  display: none;
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.45);
  z-index: 1500;
  opacity: 0;
  transition: opacity 200ms cubic-bezier(0.2, 0, 0, 1);
  touch-action: manipulation;
}
.app-mobile-drawer__scrim:not([hidden]) { display: block; }
.app-mobile-drawer__scrim.is-open { opacity: 1; }

/* Body scroll-lock while drawer open — prevents underlying content
   from scrolling vertically when user touches outside the drawer. */
body.has-mobile-drawer-open { overflow: hidden; }

/* Estética/UX item 2c (2026-06-01) — mobile topbar collapses to
   [Logo] [spacer] [Bell] [Hamburger]. Placed AFTER the .app-topbar__nav
   base rule (line ~1388) so source-order cascade resolves correctly
   at same specificity (both 0,1,0). Nav links + usage chip + user
   name + sign-out form all move into the drawer; their topbar
   versions hide here. Reveals the hamburger button (display: none
   base → inline-flex at mobile). */
@media (max-width: 768px) {
  .app-topbar__nav         { display: none; }
  .app-topbar__usage       { display: none; }
  .app-topbar__user        { display: none; }
  .app-topbar__logout-form { display: none; }
  .app-topbar__hamburger   { display: inline-flex; }
  /* Help Zone Commit 12 (mobile sweep): hide the docked "?" launcher on mobile
     so the bar collapses cleanly to [Logo] … [Bell] [avatar] [Hamburger] — four
     right-side controls (?, bell, avatar, hamburger) crowd a phone bar. The
     contextual carousel stays reachable via the in-flow "?" dots on each page,
     and the full Help index via the drawer's "Help" entry. */
  .app-topbar__help        { display: none; }
  .app-topbar__feedback    { display: none; }   /* mirror help: hidden on the cramped mobile topbar */
  /* Topbar redesign (2026-06-10): collapse the identity to AVATAR-ONLY on
     mobile — the text stack hides, the colored avatar stays (gives the mobile
     topbar life + signals the workspace). The workspace NAME stays reachable in
     the drawer footer. Drop the divider/padding so the avatar sits clean. */
  .app-topbar__identity      { border-left: none; padding-left: 0; margin-left: 0; }
  .app-topbar__identity-text { display: none; }
}

/* =========================================================
   PAGE LAYOUT HELPERS
   ========================================================= */

.page-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  margin-bottom: 1.75rem;
  gap: 1rem;
  flex-wrap: wrap;
}

.page-header__actions {
  display: flex;
  align-items: center;
  gap: .75rem;
  flex-shrink: 0;
}

/* VDP Step 13 — alias → canonical token migration. */
.page-subtitle { color: var(--color-text-muted); font-size: 0.875rem; margin-top: var(--space-xs); }

.breadcrumb-back { font-size: 0.875rem; color: var(--color-text-muted); text-decoration: none; display: inline-block; }
.breadcrumb-back:hover { color: var(--color-text-primary); text-decoration: none; }

.empty-state {
  text-align: center;
  padding: var(--space-3xl) var(--space-lg);
  color: var(--color-text-muted);
}

/* =========================================================
   PANELS
   ========================================================= */

/* VDP Step 10 (2026-05-17): token migration in-place. Class names
   preserved because copilot_dom_refresh.js re-renders this shape. */
.panel {
  background: var(--color-surface);
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius);
  overflow: hidden;
}

/* Narrow panel variant — extracted from the inline style="max-width:560px"
   repeated across coming_soon / price_lists(new,item_form) / projects/new
   (2026-05-27 CSS cleanup). */
.panel--narrow { max-width: 560px; }

.panel__header {
  padding: var(--space-md) var(--space-lg);
  border-bottom: 1px solid var(--color-border-subtle);
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.panel__header--compact { padding: var(--space-sm) var(--space-lg); }

.panel__header h3 {
  font-size: 0.9375rem;
  font-weight: 600;
  color: var(--color-text-primary);
}

.panel__header-meta {
  font-size: 0.875rem;
  font-weight: 600;
  color: var(--color-text-secondary);
  font-variant-numeric: tabular-nums;
}

.panel__body { padding: var(--space-lg); }

/* =========================================================
   GROUP ORDER ARROWS (Family 3 (b) step 4 A.2, 2026-06-03)
   =========================================================
   Up/down arrow buttons on by-type panel headers that POST to
   /<surface>/<id>/type-order. Subtle, sit at the right of the
   header next to the .panel__header-meta. Disabled state hides
   visibility (preserves layout — first panel's ▲ disappears,
   last panel's ▼ disappears, without the header shifting). */
.group-order-arrows {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  margin-left: var(--space-sm);
}
.group-order-arrow {
  background: transparent;
  border: 1px solid var(--color-border-default);
  border-radius: 4px;
  width: 22px;
  height: 22px;
  padding: 0;
  font-size: 0.75rem;
  line-height: 1;
  color: var(--color-text-secondary);
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: background 0.12s ease, color 0.12s ease;
}
.group-order-arrow:hover:not(:disabled) {
  background: var(--color-border-default);
  color: var(--color-text-primary);
}
.group-order-arrow:disabled {
  visibility: hidden;
  cursor: default;
}

/* =========================================================
   FORM EXTRAS
   ========================================================= */

.form-actions { display: flex; gap: .5rem; margin-top: 1.5rem; }

.field-required { color: var(--color-error); font-weight: 400; }

.field-textarea {
  padding: var(--space-md) var(--space-lg);
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius-sm);
  font-size: 1rem;
  font-family: var(--font-sans);
  color: var(--color-text-primary);
  background: var(--color-bg);
  width: 100%;
  min-height: 96px;
  resize: vertical;
  transition: border-color 0.15s ease-out, box-shadow 0.15s ease-out;
}

.field-textarea:focus {
  outline: none;
  border-color: var(--color-text-primary);
  box-shadow: 0 0 0 3px rgba(17, 17, 17, 0.10);
}

/* (Former BUTTONS EXTRA section consolidated into the main BUTTONS
   section above in VDP Step 4. .btn-sm + .btn-link + .btn-link--danger
   now live with the rest of the button system. .btn-link--danger
   renamed to .btn-danger-link.) */

/* =========================================================
   PROJECT LIST
   ========================================================= */

.project-list { display: flex; flex-direction: column; gap: .5rem; }

/* UI CRUD sprint (2026-05-12): card is now a CONTAINER, not the click
   target itself. The inner .project-card__body-link is the anchor; the
   .project-card__actions row sits below it (border-separated) for Edit
   + Delete. Hover effect stays on the outer container so the visual unit
   reacts uniformly. The Visual Design Pass may refine spacing/borders. */
.project-card {
  background: var(--color-surface);
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius);
  transition: border-color .15s, box-shadow .15s;
}

.project-card:hover { border-color: var(--color-accent); box-shadow: var(--shadow-sm); }

.project-card__body-link {
  display: flex;
  align-items: center;
  padding: 1rem 1.25rem;
  text-decoration: none;
  color: var(--color-text-primary);
}
.project-card__body-link:hover { text-decoration: none; color: var(--color-text-primary); }

.project-card__body  { flex: 1; }
.project-card__name  { font-weight: 600; }
.project-card__meta  { font-size: .85rem; color: var(--color-text-muted); margin-top: .2rem; }
.project-card__aside { display: flex; align-items: center; gap: .75rem; }
.project-card__count { font-size: .85rem; color: var(--color-text-muted); }
.project-card__arrow { color: var(--color-text-muted); font-size: 1.2rem; }

.project-card__actions {
  display: flex;
  gap: 1rem;
  padding: .6rem 1.25rem;
  border-top: 1px solid var(--color-border-default);
}

/* =========================================================
   DATA TABLE
   ========================================================= */

/* VDP Step 10 (2026-05-17): token migration in-place — shape locked
   by copilot_dom_refresh.js, so class names must stay. Colors only. */
.data-table {
  width: 100%;
  border-collapse: collapse;
  font-size: .875rem;
  /* Bundle Estética/UX item 5 (2026-05-31): table-layout: fixed
     so columns size from the <colgroup> widths below instead of
     each panel's own content. Without this, separate per-type-
     group tables (Products / Installation / Hardware / etc. on
     estimate detail; by-code / by-type panels on PL detail) each
     auto-size their columns independently, breaking cross-panel
     alignment (Products' UNIT PRICE column wouldn't line up with
     Installation's UNIT PRICE column). The <colgroup> below + the
     fixed layout make every .data-table use identical column
     widths. Affects estimate + PL detail (every .data-table on
     those surfaces gets the colgroup). PDF templates use their
     own markup tree (reports/pdf/*.html.j2) — untouched. */
  table-layout: fixed;
}

/* Bundle Estética/UX item 5 (2026-05-31): shared column widths for
   .data-table on estimate + PL detail. Description (.col-desc)
   deliberately has NO width — table-layout: fixed makes the one
   unspecified column take all remaining table width, so description
   is always the flex column.
   Widths chosen by one-time eyeball against real content (block
   codes ~4-8 chars, prices up to $12,345.00, units like "linear ft",
   etc.). Pixel widths leave ~700-900px for description on typical
   1200-1300px desktop tables.
   row-actions is sized 0 when not editing (column collapses; pairs
   with the .row-actions cells' existing `display: none` rule) and
   expands to 75px under .is-editing so the per-row × + buttons get
   space when edit mode unlocks them. */
.data-table > colgroup > col.col-code        { width: 200px; }
.data-table > colgroup > col.col-qty         { width: 60px; }
.data-table > colgroup > col.col-unit        { width: 80px; }
.data-table > colgroup > col.col-unit-value  { width: 130px; }
.data-table > colgroup > col.col-line-value  { width: 150px; }
.data-table > colgroup > col.col-source-page { width: 60px; }
.data-table > colgroup > col.col-row-actions { width: 0; }
[data-editable-table-surface].is-editing .data-table > colgroup > col.col-row-actions {
  width: 75px;
}
/* PL-specific: the first column (Type in by-code view, Code in by-
   type view) is slightly wider on PL because type labels like
   "Installation" need more room than block codes. Scoped to the PL
   surface only. */
[data-editable-table-surface="price_list"] .data-table > colgroup > col.col-code {
  width: 220px;
}

/* Bundle Estética/UX item 5 overflow safety net (2026-05-31):
   block_code and type cells (the first column on every .data-table)
   ellipsis-truncate when content exceeds their column width. The
   column width caps (200/220px) handle typical codes (4-8 chars) +
   reasonable longer ones (10-12 chars). Beyond that, ellipsis keeps
   the cell from overlapping the description column — alignment
   stays intact regardless of code length. Full code surfaces via
   the `title` attr on the cell. Separate data-validation work
   (max-length on block_code, max-value on unit_value) tracked in
   DEFERRED_BACKLOG.md as a pre-launch follow-up; this is the
   visual safety net only. Both surfaces; both views (estimate +
   PL by-code + PL by-type). */
.data-table td[data-cell-field="block_code"],
.data-table td[data-cell-field="type"] {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.data-table th {
  text-align: left;
  padding: var(--space-sm) var(--space-lg);
  font-size: 0.6875rem;
  font-weight: 600;
  color: var(--color-text-muted);
  text-transform: uppercase;
  letter-spacing: .05em;
  background: var(--color-surface-alt);
  border-bottom: 1px solid var(--color-border-subtle);
  white-space: nowrap;
}

.data-table td {
  padding: var(--space-md) var(--space-lg);
  border-bottom: 1px solid var(--color-border-subtle);
  vertical-align: middle;
  color: var(--color-text-primary);
}

.data-table tbody tr:last-child td { border-bottom: none; }
.data-table tbody tr:hover         { background: var(--color-surface-alt); }

.col-right { text-align: right; }
.col-bold  { font-weight: 600; }

.row--unmatched td { color: var(--color-text-muted); font-style: italic; }

/* ===============================================
   Bundle B Phase 1 (2026-05-29) — inline-edit
   shell. Master Edit / Save / Cancel row + edit-
   mode visual unlock for the items region.
   editable_table.js wires the state machine and
   applies .cell-editable / .cell-locked on Edit.
   Phase 2 hooks into these classes for the
   actual cell input swap.
   =============================================== */

/* Master row sits ABOVE the items region; Edit is the only button
   visible at rest, Save + Cancel are server-rendered hidden and
   the engine toggles them on entering / exiting edit mode.
   align-items:center keeps the buttons baseline-aligned regardless
   of which is visible. */
.items-edit-master {
  display:         flex;
  align-items:     center;
  gap:             var(--space-sm);
  margin:          0 0 var(--space-md) 0;
}

/* Estimate items search (Piece 8). Reuses the .projects-search input chrome;
   this wrapper just owns spacing + the no-results line. The search box is
   constrained so it doesn't stretch full-width on desktop, and goes
   full-width on mobile (the input is width:100% within .projects-search). */
.estimate-items-toolbar {
  margin: 0 0 var(--space-md) 0;
}
.estimate-items-toolbar .projects-search {
  max-width: 22rem;
}
.estimate-items-no-results {
  margin: var(--space-sm) 0 0 0;
  color:  var(--color-text-muted);
  font-size: 0.9rem;
}
@media (max-width: 600px) {
  .estimate-items-toolbar .projects-search { max-width: none; }
}

/* Bundle Estética/UX item 6 Layer 2 sub-step 2b-1 (2026-05-31):
   Empty-state type-picker overflow on narrow viewports. When the
   estimate is empty, .items-edit-master holds 5 type-picker
   buttons (Products / Installation / Hardware / Delivery / Other)
   on a single flex row with gap: var(--space-sm) + no flex-wrap;
   total width ~400-500px overflows mobile viewports. Adding
   flex-wrap at ≤600px lets the picker wrap onto multiple rows so
   all 5 are reachable. Non-empty branches (Edit / Cancel+Save +
   sort hint — typically 2-3 visible items) wrap is harmless (no
   wrapping needed at 2-3 items unless the row genuinely overflows,
   in which case wrapping is still the right behavior). */
@media (max-width: 600px) {
  .items-edit-master { flex-wrap: wrap; }
}

/* Bundle B Phase 5 (2026-05-29) — clickable hint that sits immediately
   right of the Edit button inside .items-edit-master. The master row is
   already display:flex with gap:var(--space-sm), so the hint inherits
   the gap and aligns center with the button. JS hides the hint in edit
   mode (mirrors btnEdit.hidden) so it doesn't sit between Cancel/Save.
   The sparkle glyph matches the Copilot launcher in _copilot_panel.html
   (currentColor inherits the muted text). Rendered as a <button>;
   button chrome is reset so it reads as muted text + pointer cursor on
   hover. Click handler in editable_table.js opens (or focuses) the Ask
   DrawIQ panel and triggers the .copilot-arrival-pulse below. */
.ask-drawiq-sort-hint {
  appearance:      none;
  background:      transparent;
  border:          0;
  padding:         0;
  display:         inline-flex;
  align-items:     center;
  gap:             6px;
  margin:          0;
  font:            inherit;
  font-size:       0.8125rem;
  color:           var(--color-text-muted);
  text-align:      left;
  cursor:          pointer;
  user-select:     none;
  transition:      color 120ms ease;
}
.ask-drawiq-sort-hint:hover {
  color:           var(--color-text-primary);
}
.ask-drawiq-sort-hint:focus-visible {
  color:           var(--color-text-primary);
  outline:         2px solid var(--color-accent-vibrant);
  outline-offset:  2px;
  border-radius:   3px;
}
.ask-drawiq-sort-hint svg {
  flex-shrink:     0;
  opacity:         0.8;
}

/* Family 3 (c) (2026-06-03) — "Sorted by: <field> <direction>" label
   between the Edit button and the Ask-DrawIQ sort hint on the master
   row of both surfaces. Muted gray, similar weight to the hint above
   so it reads as tertiary chrome. Hide-in-edit-mode is JS-driven via
   sortIndicator.hidden = true in editable_table.js, matching the
   sort-hint pattern (.hidden attribute does display: none). */
.active-sort-indicator {
  font-size:       0.8125rem;
  color:           var(--color-text-muted);
  display:         inline-flex;
  align-items:     center;
  gap:             4px;
  white-space:     nowrap;
}

/* Coral arrival pulse applied to #copilot-panel for ~1.2s after the
   hint is clicked. The 0% and 100% frames keep the panel's existing
   -4px 0 16px depth shadow constant; the second comma-value ring
   (0 0 0 6px coral) fades opacity 0 → 0.55 → 0 over each 600ms cycle,
   2 cycles total. animation-iteration-count: 2 → 1.2s wall-clock,
   then JS removes the class. box-shadow does not shift layout, so the
   panel's slide-in (transform transition) and the pulse coexist
   cleanly. rgba literal matches --color-accent-vibrant (#FF3158). */
@keyframes copilot-arrival-pulse {
  0%, 100% {
    box-shadow: -4px 0 16px rgba(0, 0, 0, 0.1),
                0 0 0 0 rgba(255, 49, 88, 0);
  }
  50% {
    box-shadow: -4px 0 16px rgba(0, 0, 0, 0.1),
                0 0 0 6px rgba(255, 49, 88, 0.55);
  }
}
.copilot-arrival-pulse {
  animation: copilot-arrival-pulse 600ms ease-in-out 2;
}

/* Edit-mode visual on the items region. A 3px top accent strip
   appears (border-top color flips from transparent), background
   gets a subtle tint, and the items table's hover state is
   suppressed so it doesn't compete with the per-cell affordances
   below. The transition gives ~150ms ease so the mode change reads
   as a deliberate state shift, not a flicker. */
/* Bundle B Phase 4 (2026-05-29) — generalized the edit-mode region
   selector to [data-editable-table-surface] so the same styles apply
   to BOTH #estimate-items-region (estimate surface) AND #pl-items-
   region (price-list surface) without duplicating rules. */
[data-editable-table-surface] {
  border-top:    3px solid transparent;
  padding-top:   0;
  transition:    border-color 150ms ease, background 150ms ease;
}
[data-editable-table-surface].is-editing {
  border-top-color: var(--color-accent-vibrant, #FF3158);
  background:       var(--color-surface-alt);
}
[data-editable-table-surface].is-editing .data-table tbody tr:hover {
  background: transparent;   /* per-cell affordance takes over */
}

/* Editable cells in edit mode — hover affordance: a subtle outline
   appears + the text cursor signals "click to edit." Border-on-
   hover is the discoverability cue Phase 2 turns into an input
   swap. The current text color stays so the value remains
   readable while hovering. */
.is-editing .data-table td.cell-editable {
  cursor: text;
  transition: box-shadow 100ms ease;
}
.is-editing .data-table td.cell-editable:hover {
  box-shadow: inset 0 0 0 1px var(--color-text-primary);
  background: white;
}

/* Locked cells in edit mode — visibly dimmed + not-allowed cursor
   signals "this column can't be edited here." block_code, type,
   line_value, source_page on the estimate side; only line_value
   ends up locked on the price-list side (Phase 4). */
.is-editing .data-table td.cell-locked {
  color:      var(--color-text-muted);
  cursor:     not-allowed;
  opacity:    0.7;
}
.is-editing .data-table td.cell-locked .code-label {
  /* The code chip's background contrast looked off against the
     dimmed cell. Mute it together. */
  opacity:    0.85;
}

/* Bundle B Phase 2 (2026-05-29) — input swap when a cell is opened
   for editing. The <input> inherits cell sizing so the swap doesn't
   resize the row. Focus styling makes the input feel like a native
   edit affordance. */
.is-editing .data-table td .editable-input {
  width:        100%;
  box-sizing:   border-box;
  border:       1px solid var(--color-text-primary);
  border-radius: var(--radius-sm);
  padding:      4px 6px;
  font:         inherit;
  color:        var(--color-text-primary);
  background:   white;
  outline:      none;
}
.is-editing .data-table td .editable-input:focus {
  border-color: var(--color-accent-vibrant, #FF3158);
  box-shadow:   0 0 0 2px rgba(255, 49, 88, 0.18);
}

/* Pending-edit row — subtle background tint so the user can see at a
   glance which rows hold unsaved changes. Cleared on Save success or
   Cancel. */
.is-editing .data-table tr.row--pending {
  background: rgba(255, 49, 88, 0.06);
}

/* Server-side validation error — per-cell state set after a 400
   response from batch-update. cursor:help signals "hover for reason"
   (browser tooltip via the title attribute that editable_table.js
   sets to the error message). */
/* .cell-dup-warn (2026-06-26): the estimate in-the-moment (block_code,
   type) duplicate warning reuses the save-time cell-error red treatment
   verbatim, driven live on every edit by editable_table.js. */
.is-editing .data-table td.cell-error,
.is-editing .data-table td.cell-dup-warn {
  background:   rgba(214, 36, 36, 0.10);
  box-shadow:   inset 0 0 0 1px var(--color-error);
  cursor:       help;
}
.is-editing .data-table tr.row--has-error {
  /* Reserved for the rare case where the server returns a row-level
     error without a specific field. Hover the row to see the title. */
  cursor:       help;
}

/* Bundle B Phase 3 (2026-05-29) — per-row actions + add-line +
   pending-delete + pending-new. The row-actions <td> is server-
   rendered on every row (so Copilot DOM refresh keeps the lockstep)
   but hidden until .is-editing. Same for the per-group "+ Add line"
   row + the row-actions table header. */
.row-actions,
.row-actions-head,
.items-add-line-row {
  display: none;
}
/* Phase 4: generalized — applies to both estimate + PL surfaces. */
[data-editable-table-surface].is-editing .row-actions,
[data-editable-table-surface].is-editing .row-actions-head,
[data-editable-table-surface].is-editing .items-add-line-row {
  display: table-cell;
}
[data-editable-table-surface].is-editing .items-add-line-row {
  display: block;
  padding: var(--space-sm) var(--space-md);
  border-top: 1px solid var(--color-border-subtle);
  background: var(--color-surface-alt);
}
[data-editable-table-surface].is-editing td.row-actions {
  text-align: right;
  white-space: nowrap;
  padding-right: var(--space-md);
}


/* Per-row × and + and Undo buttons. Small + unobtrusive at rest,
   coral on hover for the destructive (×) and accent for duplicate
   (+). The Undo state is a single button replacing both. */
.row-action-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 24px;
  height: 24px;
  padding: 0 6px;
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius-sm);
  background: white;
  color: var(--color-text-muted);
  cursor: pointer;
  font-size: 1rem;
  line-height: 1;
  margin-left: 4px;
  transition: background 100ms ease, color 100ms ease, border-color 100ms ease;
}
.row-action-btn:hover { color: var(--color-text-primary); }
.row-action-btn--del:hover {
  background: var(--color-error);
  border-color: var(--color-error);
  color: white;
}
.row-action-btn--dup:hover {
  background: var(--color-text-primary);
  border-color: var(--color-text-primary);
  color: white;
}
.row-action-btn--undo {
  font-size: 0.8125rem;
  padding: 0 var(--space-sm);
  color: var(--color-text-primary);
}
.row-action-btn--undo:hover {
  background: var(--color-surface-alt);
}

/* Pending-delete row — dim + strikethrough. The cells stay readable
   but visually telegraph "this row is going away." Undo button (in the
   row-actions cell) restores. */
.is-editing .data-table tr.row--pending-delete td {
  text-decoration: line-through;
  color: var(--color-text-muted);
  background: rgba(214, 36, 36, 0.04);
}
.is-editing .data-table tr.row--pending-delete td.row-actions {
  text-decoration: none;   /* the Undo button shouldn't be struck-through */
}

/* Pending-new row — slightly stronger tint than .row--pending to
   distinguish "I just added this" from "I edited an existing row." */
.is-editing .data-table tr.row--pending-new {
  background: rgba(255, 49, 88, 0.08);
}
.cell-empty-placeholder {
  color: var(--color-text-muted);
  font-style: italic;
}

/* Empty-state type-picker label + spacing for the master row's
   "+ Add first line:" prefix. */
.items-edit-master__empty-label {
  font-weight: 500;
  color: var(--color-text-primary);
  margin-right: var(--space-sm);
}

.code-label {
  font-family: var(--font-mono);
  font-size: 0.8125rem;
  background: var(--color-surface-alt);
  border: 1px solid var(--color-border-default);
  border-radius: 3px;
  padding: .1em .4em;
  color: var(--color-text-primary);
  white-space: nowrap;
}

/* VDP Step 13 (2026-05-17 → 18): the legacy .badge + 4 variants
   block was deleted here. Replaced by .estimate-status-badge
   (Step 10) — same pale-bg + deep-text pattern with modern tokens
   + dot prefix. Verified zero template/JS consumers before delete. */

/* =========================================================
   UPLOAD ZONE
   ========================================================= */

/* VDP Step 13 — alias → canonical token migration. The hover/drag
   accent is now --color-text-primary (black, restrained) instead of
   --blue alias; --picked stays --color-success (visible confirmation
   of a valid PDF selected). */
.upload-zone {
  display: flex;
  align-items: center;
  gap: var(--space-lg);
  padding: var(--space-xl);
  border: 2px dashed var(--color-border-default);
  border-radius: var(--radius);
  cursor: pointer;
  transition: border-color .15s, background .15s;
  user-select: none;
}

.upload-zone:hover,
.upload-zone--drag  { border-color: var(--color-text-primary); background: var(--color-surface-alt); }
.upload-zone--picked { border-color: var(--color-success); background: var(--color-success-bg); }

.upload-zone__icon    { color: var(--color-text-muted); flex-shrink: 0; }
.upload-zone__text    { display: flex; flex-direction: column; gap: var(--space-xs); }
.upload-zone__primary { font-weight: 500; color: var(--color-text-primary); }
.upload-zone__secondary { font-size: 0.8125rem; color: var(--color-text-muted); }

.upload-filename { font-size: 0.875rem; color: var(--color-success); font-weight: 500; margin-top: var(--space-sm); min-height: 1.2em; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* VDP Step 13 (2026-05-17 → 18): the legacy .thumb-card* block
   (CabinetIQ-ported pre-Step-8 thumbnail grid) was deleted here.
   Replaced by .preview-thumb* (Step 8) — orientation-aware,
   asymmetric grid, sparkle-icon checkbox, modern tokens. Verified
   zero template/JS consumers before delete.

   The legacy .preview-toolbar* block (pre-Step-8 estimate-preview
   toolbar with sticky position + space-between layout) was also
   deleted here. Replaced by .preview-pages__toolbar +
   .preview-thumb-grid (Step 8). Verified zero consumers. */

/* =========================================================
   PROGRESS BAR
   VDP Step 9 (2026-05-15): tokens migrated to the new
   design system. Class names preserved so analyzing.html's
   JS (progress-fill width, aria-valuenow, etc.) keeps
   working without modification.
   ========================================================= */

.progress-bar {
  height: 8px;
  background: var(--color-border-default);
  border-radius: var(--radius-pill);
  overflow: hidden;
}

.progress-bar__fill {
  height: 100%;
  background: var(--color-accent);
  border-radius: var(--radius-pill);
  transition: width .4s ease;
}

.progress-label {
  font-size: 1rem;
  font-weight: 500;
  color: var(--color-text-primary);
  min-height: 1.4em;
}

/* =========================================================
   PER-PAGE STATUS LIST
   VDP Step 9: token migration in-place. Dot state class
   names (--waiting / --done / --sparse / --error) are
   contracted with analyzing.js — NEVER rename them.
   ========================================================= */

.page-status-list {
  text-align: left;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.page-status {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  font-size: 0.875rem;
  padding: var(--space-sm) var(--space-md);
  border-radius: var(--radius-sm);
  background: transparent;
  transition: background-color 0.1s ease-out;
}
.page-status:hover { background: var(--color-surface-alt); }

.page-status__dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  flex-shrink: 0;
  transition: background .2s;
}

/* Dot state classes — JS-contracted names. */
.page-status__dot--waiting { background: var(--color-text-muted); }
.page-status__dot--done    { background: var(--color-success); }
.page-status__dot--sparse  { background: var(--color-warning); }
.page-status__dot--error   { background: var(--color-error); }

.page-status__label  {
  font-weight: 500;
  min-width: 60px;
  color: var(--color-text-primary);
}
.page-status__result {
  color: var(--color-text-muted);
  font-size: 0.8125rem;
  margin-left: auto;
}

/* =========================================================
   SUMMARY CARDS
   VDP Step 10 (2026-05-17): token migration in-place. The shape
   (.summary-cards > .summary-card[--total]) is mirrored inside
   copilot_dom_refresh.js, so class names stay; colors migrate to
   the new system. The Total card was navy-bordered in V1;
   Matterport-restrained palette uses a slightly heavier border
   weight instead of color emphasis.
   ========================================================= */

.summary-cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: var(--space-lg);
  margin: var(--space-xl) 0 0;
}

.summary-card {
  background: var(--color-surface);
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius);
  padding: var(--space-lg);
}

.summary-card--total {
  border-color: var(--color-border-strong);
}

.summary-card__label {
  font-size: 0.75rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .05em;
  color: var(--color-text-muted);
  margin-bottom: var(--space-xs);
}

.summary-card__value {
  font-size: 1.5rem;
  font-weight: 600;
  color: var(--color-text-primary);
  font-variant-numeric: tabular-nums;
}

.summary-card--total .summary-card__value {
  font-size: 1.75rem;
  font-weight: 700;
}

.summary-card--total .summary-card__value { color: var(--color-accent); }

/* =========================================================
   PASSWORD STRENGTH METER
   ========================================================= */

.pwd-strength {
  margin-top: var(--space-sm);
}

.pwd-strength__bar {
  height: 4px;
  background: var(--color-border-default);
  border-radius: 2px;
  overflow: hidden;
  margin-bottom: var(--space-xs);
}

.pwd-strength__fill {
  height: 100%;
  border-radius: 2px;
  width: 0%;
  transition: width 0.3s ease, background 0.3s ease;
}

.pwd-strength__fill--weak   { width: 33%;  background: var(--color-error); }
.pwd-strength__fill--medium { width: 66%;  background: var(--color-warning); }
.pwd-strength__fill--strong { width: 100%; background: var(--color-success); }

.pwd-strength__label {
  font-size: 0.75rem;
  font-weight: 600;
  min-height: 1.2em;
  margin-bottom: var(--space-sm);
}

.pwd-strength__label--weak   { color: var(--color-error); }
.pwd-strength__label--medium { color: var(--color-warning); }
.pwd-strength__label--strong { color: var(--color-success); }

/* Password requirements list — VDP Step 3 visual fix.
   - 4px (--space-xs) gap between items horizontally + vertically
   - 14px text (--text-body-small) instead of 12.5px for legibility
   - 10px circle icons (was 8px) for better presence
   - Text stays gray on match; the COLOR cue is the icon green-fill, so
     unmet items still read at the same gray contrast as met items.
   - At >=768px the list splits into 2 columns (5 reqs distribute 3+2)
     so it doesn't dominate the form vertically. Per VDP Step 3 user
     clarification. */
.pwd-requirements {
  list-style: none;
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-xs);
  margin: var(--space-sm) 0 0 0;
  padding: 0;
}

@media (min-width: 768px) {
  .pwd-requirements { grid-template-columns: 1fr 1fr; }
}

.pwd-req {
  font-size: 0.875rem;
  color: var(--color-text-secondary);
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  line-height: 1.4;
  transition: color 0.15s;
}

.pwd-req::before {
  content: '';
  width: 10px;
  height: 10px;
  border-radius: 50%;
  border: 1.5px solid var(--color-text-muted);
  background: transparent;
  flex-shrink: 0;
  transition: background 0.15s, border-color 0.15s;
}

.pwd-req--met {
  color: var(--color-text-secondary);
}

.pwd-req--met::before {
  background: var(--color-success);
  border-color: var(--color-success);
}

/* =========================================================================
   PRICE LIST VIEW TOGGLE (detail.html)
   ========================================================================= */

.pl-view-toolbar {
  display: flex;
  gap: .5rem;
  align-items: center;
  margin-bottom: 1.25rem;
  flex-wrap: wrap;
}

/* Active state on the price-list view toggle. Overrides the subtle
   hover/active states from .btn-secondary--subtle so the active toggle
   reads as a filled-dark button (clear contrast with inactive). */
.pl-view-btn.is-active {
  background: var(--color-accent);
  color: var(--color-text-inverse);
  border-color: var(--color-accent);
}

.pl-view-btn.is-active:hover {
  background: var(--color-accent-hover);
  color: var(--color-text-inverse);
  border-color: var(--color-accent-hover);
}

.pl-view {
  display: none;
}

.pl-view.is-active {
  display: block;
}

.pl-view-saved-msg {
  color: var(--color-success);
  font-size: .85rem;
  margin-left: .5rem;
}

/* =========================================================================
   SETTINGS PAGE — sub-tab nav + body
   ========================================================================= */

/* VDP Step 12 (2026-05-17): token migration in-place + new classes
   for the per-page H2 title, sidebar separator (workspace/billing
   grouping), and Usage stat row. Mobile breakpoint bumped to 768px
   for system consistency with Steps 7/8/10/11. */

.settings-shell {
  display: grid;
  grid-template-columns: 220px 1fr;
  gap: var(--space-xl);
  align-items: start;
}

.settings-tabs {
  display: flex;
  flex-direction: column;
  gap: 2px;
  position: sticky;
  top: var(--space-md);
}

.settings-tab {
  display: block;
  padding: var(--space-sm) var(--space-md);
  border-radius: var(--radius-sm);
  color: var(--color-text-secondary);
  font-size: 0.9375rem;
  font-weight: 500;
  text-decoration: none;
  border-left: 2px solid transparent;
  transition: background-color 0.1s ease-out, color 0.1s ease-out;
}

.settings-tab:hover {
  background: var(--color-surface-alt);
  color: var(--color-text-primary);
  text-decoration: none;
}

.settings-tab.is-active {
  background: var(--color-surface-alt);
  color: var(--color-text-primary);
  font-weight: 600;
  border-left-color: var(--color-text-primary);
}

/* Visual divider between the Workspace group (Account/Prefs/Company)
   and the Billing group (Usage/Subscription). Subtle 1px line, no
   label — telegraphs the grouping without heading clutter. */
.settings-tab--separator {
  height: 1px;
  background: var(--color-border-subtle);
  margin: var(--space-sm) var(--space-sm);
  border: 0;
  padding: 0;
}

.settings-body {
  min-width: 0;  /* allow flex children to shrink correctly */
}

/* Usage page stat row (label above value) — refreshed from the prior
   inline "Total analyses run: <strong>N</strong>" paragraph. */
.settings-stat-row {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
}
.settings-stat-row__label {
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--color-text-muted);
}
.settings-stat-row__value {
  font-size: 1.5rem;
  font-weight: 700;
  color: var(--color-text-primary);
  font-variant-numeric: tabular-nums;
}

/* Subtle divider between logical field groups inside a panel.
   Used on Company page between identity fields and the new tax
   disclaimer field. */
.settings-section-divider {
  height: 1px;
  background: var(--color-border-subtle);
  margin: var(--space-lg) 0;
  border: 0;
}

/* FASE 4 — currency symbol-position radios (Settings → Company). */
.currency-position-group {
  display: flex;
  gap: var(--space-md);
  align-items: center;
}
.currency-position-option {
  display: inline-flex;
  align-items: center;
  gap: 4px;
}

@media (max-width: 768px) {
  .settings-shell {
    grid-template-columns: 1fr;
    gap: var(--space-lg);
  }
  .settings-tabs {
    flex-direction: row;
    flex-wrap: wrap;
    position: static;
    gap: var(--space-xs);
  }
  .settings-tab {
    border-left: none;
    border-bottom: 2px solid transparent;
  }
  .settings-tab.is-active {
    border-bottom-color: var(--color-text-primary);
    border-left-color: transparent;
  }
  /* The workspace/billing visual divider doesn't make sense in a
     horizontal-wrap layout — hide it on mobile. */
  .settings-tab--separator { display: none; }
}

/* Logo in topbar — replaces the D-mark when tenant has uploaded one */
.app-topbar__logo-img {
  max-height: 32px;
  max-width: 140px;
  object-fit: contain;
}

/* Subscription tier cards (Settings → Subscription).
   VDP Step 12 (2026-05-17): token migration in-place + current-plan
   border switched to explicit --color-text-primary per Phase A Q4.
   Matterport-restrained palette (no coral, no brand alias). */
.tier-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: var(--space-lg);
}

.tier-card {
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius);
  padding: var(--space-lg);
  background: var(--color-surface);
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
}

.tier-card--current {
  border-color: var(--color-text-primary);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--color-text-primary), transparent 88%);
}

.tier-card__name  { font-size: 1rem; color: var(--color-text-primary); font-weight: 600; }
.tier-card__price { font-size: 1.5rem; font-weight: 700; color: var(--color-text-primary); margin: var(--space-xs) 0; font-variant-numeric: tabular-nums; }
.tier-card__blurb { color: var(--color-text-muted); font-size: 0.875rem; line-height: 1.5; flex: 1; margin-bottom: var(--space-md); }
.tier-card__includes { color: var(--color-text-secondary); font-size: 0.875rem; line-height: 1.4; margin: 0 0 var(--space-sm); }


/* (Former Day 15a "DESTRUCTIVE BUTTON" section consolidated into the
   main BUTTONS section above in VDP Step 4. .btn-danger now has a full
   state matrix and red focus ring alongside the rest of the button
   variants.) */


/* =========================================================
   DAY 15a — "More actions" dropdown (native <details>)
   ========================================================= */
.more-actions {
  position: relative;
  margin:   0;
}

/* Hide the default disclosure triangle on the summary */
.more-actions__summary {
  list-style: none;
  cursor: pointer;
  user-select: none;
}
.more-actions__summary::-webkit-details-marker { display: none; }
.more-actions__summary::marker { content: ''; }

.more-actions__menu {
  position:      absolute;
  top:           calc(100% + 4px);
  right:         0;
  min-width:     180px;
  margin:        0;
  padding:       4px;
  list-style:    none;
  background:    var(--color-surface);
  border:        1px solid var(--color-border-default);
  border-radius: var(--radius-sm);
  box-shadow:    var(--shadow-md);
  z-index:       60;   /* above sticky footer (z=50) */
}

/* VDP Step 10 (2026-05-17): upward variant for dropdowns anchored
   in the sticky footer. Flips vertical positioning (bottom: 100%+gap
   instead of top: 100%+gap) and inverts the shadow direction so the
   visual lift points upward. Applied via class on the menu — the
   sticky-footer template sets it; other consumers (header dropdowns)
   keep default downward open. */
.more-actions__menu--up {
  top:        auto;
  bottom:     calc(100% + 4px);
  box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.08);
}

.more-actions__item {
  display:       block;
  width:         100%;
  text-align:    left;
  background:    transparent;
  color:         var(--color-text-primary);
  /* Menu items can be <button> OR <a> (e.g. the Export CSV downloads).
     Anchors otherwise inherit the global a:hover underline — suppress it
     here (+ on :hover, which must out-specify a:hover) so every item
     behaves the same: gray-bg hover, no underline. */
  text-decoration: none;
  border:        none;
  padding:       var(--space-sm) var(--space-md);
  font:          inherit;
  font-size:     0.875rem;
  font-family:   var(--font-sans);
  border-radius: 4px;
  cursor:        pointer;
}

.more-actions__item:hover {
  background:      var(--color-border-default);  /* #E5E5E5 — clear hover, was --color-surface-alt #FAFAFA (too light) */
  text-decoration: none;
}

/* VDP Step 10 — destructive variant (red text, error-bg hover).
   Mirrors .estimates-kebab-item--danger from Step 7. */
.more-actions__item--danger {
  color: var(--color-error);
}
.more-actions__item--danger:hover {
  background: var(--color-error-bg);
  color:      var(--color-error);
}


/* =========================================================
   DAY 15a — Re-analyze PDF confirmation dialog (<dialog>)
   ========================================================= */
/* VDP Step 13 (2026-05-17 → 18): in-place token migration. Class
   names preserved — JS contracts (#reanalyze-dialog, #cancel-reanalyze,
   etc.) untouched. Backdrop uses the canonical --color-overlay-darker. */
.reanalyze-dialog {
  border:        1px solid var(--color-border-default);
  border-radius: var(--radius);
  padding:       0;
  background:    var(--color-surface);
  color:         var(--color-text-primary);
  box-shadow:    var(--shadow-lg);
  max-width:     min(440px, 92vw);
  font:          inherit;
}
.reanalyze-dialog::backdrop {
  background: var(--color-overlay-darker);
}

.reanalyze-dialog form {
  padding: var(--space-xl) var(--space-xl) var(--space-lg);
  margin:  0;
}

.reanalyze-dialog__title {
  margin:    0 0 var(--space-md) 0;
  font-size: 1.0625rem;
  font-weight: 600;
  color:     var(--color-text-primary);
}

.reanalyze-dialog__body {
  color:     var(--color-text-secondary);
  font-size: 0.875rem;
  margin:    0 0 var(--space-lg) 0;
  line-height: 1.5;
}

.reanalyze-dialog__actions {
  display:        flex;
  justify-content: flex-end;
  gap:            8px;
  margin-top:     20px;
}


/* =========================================================
   DAY 15a — Copilot edit toast (top-right of viewport)
   ========================================================= */
.copilot-toast-container {
  position:    fixed;
  top:         24px;
  right:       24px;
  z-index:     1002;          /* above launcher (1000), below panel header */
  display:     flex;
  flex-direction: column;
  gap:         8px;
  pointer-events: none;       /* container is invisible; toasts re-enable */
}

/* VDP Step 13 — token migration in-place. Left-border accent uses
   --color-accent (black) instead of the dead --brand/#4361EE coral-
   era blue. Refresh button uses the system primary recipe. */
.copilot-toast {
  display:       inline-flex;
  align-items:   center;
  gap:           var(--space-sm);
  background:    var(--color-surface);
  color:         var(--color-text-primary);
  border:        1px solid var(--color-border-default);
  border-left:   4px solid var(--color-accent);
  border-radius: var(--radius-sm);
  padding:       var(--space-sm) var(--space-md);
  font-size:     0.875rem;
  box-shadow:    var(--shadow-md);
  pointer-events: auto;
  transform:     translateX(20px);
  opacity:       0;
  transition:    transform 180ms ease-out, opacity 180ms ease-out;
}

.copilot-toast--open {
  transform: translateX(0);
  opacity:   1;
}

.copilot-toast__icon {
  font-size: 1.1rem;
}

.copilot-toast__text {
  flex: 0 1 auto;
}

.copilot-toast__refresh {
  background:    var(--color-accent);
  color:         var(--color-accent-text);
  border:        none;
  border-radius: var(--radius-pill);
  padding:       4px var(--space-md);
  font:          inherit;
  font-size:     0.8125rem;
  cursor:        pointer;
}

.copilot-toast__refresh:hover {
  background: var(--color-accent-hover);
}

.copilot-toast__close {
  background: transparent;
  border:     none;
  color:      var(--color-text-muted);
  cursor:     pointer;
  font-size:  1.2rem;
  line-height: 1;
  padding:    0 4px;
}

.copilot-toast__close:hover {
  color: var(--color-text-primary);
}

@media (prefers-reduced-motion: reduce) {
  .copilot-toast { transition: none !important; }
}

@media (max-width: 640px) {
  .copilot-toast-container {
    top:   16px;
    right: 16px;
    left:  16px;
  }
  .copilot-toast { width: 100%; }
}


/* =========================================================
   DAY 15b commit 1 — Preview modal for LARGE Copilot tools
   ========================================================= */

/* VDP Step 13 — token migration in-place. Class names preserved
   (JS contracts: #preview-dialog, #close-preview-dialog, etc.). */
.preview-dialog {
  /* Centering: see global `dialog[open] { margin: auto }` rule near
     .visually-hidden (Bundle Estética/UX item 1, 2026-05-31). */
  border:        1px solid var(--color-border-default);
  border-radius: var(--radius);
  padding:       0;
  background:    var(--color-surface);
  color:         var(--color-text-primary);
  box-shadow:    var(--shadow-lg);
  max-width:     min(720px, 92vw);
  max-height:    80vh;
  font:          inherit;
  overflow:      hidden;       /* inner element handles scroll */
}
.preview-dialog::backdrop {
  background: var(--color-overlay-darker);
}

.preview-dialog__inner {
  display:        flex;
  flex-direction: column;
  max-height:     80vh;        /* matches dialog cap */
}

.preview-dialog__header {
  display:         flex;
  align-items:     center;
  justify-content: space-between;
  padding:         var(--space-lg) var(--space-xl);
  border-bottom:   1px solid var(--color-border-subtle);
  flex:            0 0 auto;
}

.preview-dialog__title {
  margin:    0;
  font-size: 1.0625rem;
  font-weight: 600;
  color:     var(--color-text-primary);
}

.preview-dialog__close {
  background:    transparent;
  border:        none;
  font-size:     1.5rem;
  line-height:   1;
  color:         var(--color-text-muted);
  cursor:        pointer;
  padding:       0 6px;
  border-radius: 4px;
}
.preview-dialog__close:hover { color: var(--color-text-primary); }

.preview-dialog__body {
  flex:       1 1 auto;
  overflow-y: auto;
  padding:    var(--space-lg) var(--space-xl);
  max-height: 60vh;
}

.preview-dialog__error {
  margin:        0 var(--space-xl) var(--space-sm);
  padding:       var(--space-sm) var(--space-md);
  background:    var(--color-error-bg);
  border:        1px solid color-mix(in srgb, var(--color-error), white 75%);
  color:         var(--color-error);
  border-radius: var(--radius-sm);
  font-size:     0.875rem;
  flex:          0 0 auto;
}
.preview-dialog__error[hidden] { display: none; }

.preview-dialog__actions {
  display:         flex;
  justify-content: flex-end;
  gap:             var(--space-sm);
  padding:         var(--space-md) var(--space-xl) var(--space-lg);
  border-top:      1px solid var(--color-border-subtle);
  flex:            0 0 auto;
}

/* Totals header — shown above the diff table */
.preview-totals-header {
  display:       grid;
  grid-template-columns: 1fr auto;
  gap:           6px var(--space-lg);
  padding:       var(--space-md) var(--space-lg);
  background:    var(--color-surface-alt);
  border:        1px solid var(--color-border-default);
  border-radius: var(--radius-sm);
  margin-bottom: var(--space-lg);
  font-size:     0.875rem;
}
.preview-totals-header__label { color: var(--color-text-muted); }
.preview-totals-header__value {
  text-align: right;
  font-variant-numeric: tabular-nums;
}
.preview-totals-header__value--total { font-weight: 600; color: var(--color-text-primary); }
.preview-totals-header__arrow { color: var(--color-text-muted); margin: 0 4px; }
.preview-totals-header__new { color: var(--color-success); font-weight: 500; }

/* Value-diff table (4 LARGE tools) */
.preview-value-diff {
  width:           100%;
  border-collapse: collapse;
  font-size:       0.875rem;
}
.preview-value-diff thead th {
  text-align:    left;
  font-weight:   600;
  color:         var(--color-text-muted);
  padding:       8px 10px;
  border-bottom: 1px solid var(--color-border-default);
  font-size:     0.8125rem;
  text-transform: uppercase;
  letter-spacing: 0.02em;
}
.preview-value-diff tbody td {
  padding:       8px 10px;
  border-bottom: 1px solid var(--color-border-default);
  vertical-align: top;
}
.preview-value-diff tbody tr:hover td { background: var(--color-surface-alt); }
.preview-value-diff__code { font-family: 'SF Mono', Menlo, monospace; font-size: 0.8125rem; }
.preview-value-diff__desc { color: var(--color-text-muted); max-width: 260px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.preview-value-diff__current { text-align: right; color: var(--color-text-primary); font-variant-numeric: tabular-nums; }
.preview-value-diff__after {
  text-align: right;
  font-variant-numeric: tabular-nums;
  background: var(--color-success-bg);
  color: #065F46;
  font-weight: 500;
}
.preview-value-diff__after--unchanged {
  background: transparent;
  color: var(--color-text-muted);
  font-weight: 400;
}

.preview-value-diff__footer-note {
  padding:    10px 0 4px 0;
  text-align: center;
  font-size:  0.8125rem;
  color:      var(--color-text-muted);
  font-style: italic;
}

/* Generic / reorder-fallback */
.preview-generic {
  padding:    24px 12px;
  text-align: center;
}
.preview-generic__summary {
  font-size:     1rem;
  color:         var(--color-text-primary);
  margin-bottom: 12px;
}
.preview-generic__list {
  list-style:    none;
  padding:       0;
  margin:        12px auto 0 auto;
  max-width:     420px;
  text-align:    left;
  font-size:     0.875rem;
  color:         var(--color-text-muted);
}
.preview-generic__list li {
  padding:       4px 0;
  border-bottom: 1px dashed var(--color-border-default);
}
.preview-generic__list li:last-child { border-bottom: none; }

/* ===========================================================================
   Reorder diff renderer (Day 15b commit 2) — reorder_items LARGE tool
   =========================================================================== */

.preview-reorder {
  font-size: 0.875rem;
}

.preview-reorder__header {
  padding:       10px 12px;
  background:    var(--color-surface-alt);
  border-radius: var(--radius-sm);
  margin-bottom: 8px;
  color:         var(--color-text-primary);
}
.preview-reorder__header-label {
  color:       var(--color-text-muted);
  font-weight: 500;
}
.preview-reorder__header-value {
  font-family: 'SF Mono', Menlo, monospace;
  font-size:   0.8125rem;
}
.preview-reorder__header-direction {
  color: var(--color-text-muted);
}

.preview-reorder__summary {
  color:         var(--color-text-muted);
  font-size:     0.8125rem;
  margin-bottom: 12px;
}

.preview-reorder__list {
  list-style: none;
  padding:    0;
  margin:     0;
}

.preview-reorder__item {
  display:               grid;
  grid-template-columns: 28px 80px 1fr;
  gap:                   12px;
  padding:               6px 8px;
  border-bottom:         1px solid var(--color-border-default);
  align-items:           center;
}
.preview-reorder__item:last-child { border-bottom: none; }

.preview-reorder__direction {
  font-weight:         600;
  font-size:           1.1rem;
  text-align:          center;
  font-variant-numeric: tabular-nums;
}

.preview-reorder__position {
  color:                var(--color-text-muted);
  font-family:          'SF Mono', Menlo, monospace;
  font-size:            0.8125rem;
  font-variant-numeric: tabular-nums;
  text-align:           right;
}

.preview-reorder__desc {
  color:         var(--color-text-primary);
  overflow:      hidden;
  text-overflow: ellipsis;
  white-space:   nowrap;
}

.preview-reorder__item--up .preview-reorder__direction {
  color: var(--color-success);
}

.preview-reorder__item--down .preview-reorder__direction {
  /* --warning not yet in main.css (tracked in V2 polish backlog).
     Inline amber fallback keeps the renderer working today; when the
     token lands the renderer picks it up automatically. */
  color: var(--warning);
}

.preview-reorder__item--unchanged {
  opacity: 0.5;
}
.preview-reorder__item--unchanged .preview-reorder__direction {
  color:       var(--color-text-muted);
  font-weight: 400;
}

.preview-reorder__footer-note {
  padding:    10px 0 4px 0;
  text-align: center;
  font-size:  0.8125rem;
  color:      var(--color-text-muted);
  font-style: italic;
}

@media (max-width: 640px) {
  .preview-reorder__item {
    grid-template-columns: 24px 64px 1fr;
    gap:                   8px;
  }
  .preview-reorder__position { font-size: 0.75rem; }
}

/* Apply button spinner */
.preview-apply-spinner {
  display:        inline-block;
  animation:      preview-spin 0.8s linear infinite;
}
@keyframes preview-spin {
  to { transform: rotate(360deg); }
}

@media (prefers-reduced-motion: reduce) {
  .preview-apply-spinner { animation: none !important; }
}

@media (max-width: 640px) {
  .preview-dialog { max-width: 100vw; max-height: 100vh; border-radius: 0; }
  .preview-dialog__inner { max-height: 100vh; }
  .preview-dialog__body { padding: 12px; }
  .preview-value-diff__desc { max-width: 140px; }
}


/* ===========================================================================
   Destructive confirmation dialog (Day 15b commit 4)

   Wired by app/static/js/copilot.js (openConfirmDialog) when a DESTRUCTIVE
   Copilot tool returns a confirmation_token via SSE. Visually distinct from
   the LARGE preview-dialog: smaller (no large data table to show), red
   accent border + ⚠ icon to signal irreversibility, no × close affordance
   (force explicit Cancel-or-Confirm). Reuses the .copilot-undo-toast--error
   red palette from commit 3 (visual-language consistency: red = destructive
   or error). Backdrop click is a deliberate no-op — see openConfirmDialog
   comment for rationale.
   =========================================================================== */

/* VDP Step 13 — token migration in-place. Class names preserved
   (JS contracts: #confirm-dialog, #confirm-destructive, etc.). */
.confirm-dialog {
  /* Centering: see global `dialog[open] { margin: auto }` rule near
     .visually-hidden (Bundle Estética/UX item 1, 2026-05-31).
     Phase F polish: dropped the heavy 2px red frame for a clean 1px card —
     severity now reads from the tidy icon badge + the red verb button, not an
     alarming border. */
  border:        1px solid var(--color-border-default);
  border-radius: var(--radius-lg);
  background:    var(--color-surface);
  box-shadow:    var(--shadow-lg);
  padding:       0;
  width:         min(440px, 92vw);
  max-width:     440px;
  font:          inherit;
}

.confirm-dialog::backdrop {
  background: var(--color-overlay-dark);
}

.confirm-dialog__inner {
  padding:        var(--space-xl) var(--space-xl) var(--space-lg);
  display:        flex;
  flex-direction: column;
  gap:            var(--space-md);
}

.confirm-dialog__icon {
  /* Phase F polish: a tidy circular badge instead of a bare 2rem glyph. */
  width:           44px;
  height:          44px;
  margin:          0 auto;
  display:         flex;
  align-items:     center;
  justify-content: center;
  border-radius:   50%;
  background:      var(--color-error-bg);
  color:           var(--color-error);
  font-size:       1.375rem;
  line-height:     1;
  /* Subtle pulse on open. Honors prefers-reduced-motion below. */
  animation:       confirm-icon-pulse 0.4s ease-out;
}

.confirm-dialog__title {
  margin:      0;
  font-size:   1.0625rem;
  font-weight: 600;
  color:       var(--color-text-primary);
  text-align:  center;
  line-height: 1.4;
}

.confirm-dialog__warning {
  margin:      0;
  font-size:   0.875rem;
  color:       var(--color-text-secondary);
  line-height: 1.5;
  text-align:  center;
  padding:     0 var(--space-sm);
}

/* In-modal error banner — appears when POST /api/copilot/confirm
   returns 410 / 404 / 500 or network error. Same shape as the
   preview-dialog__error. */
.confirm-dialog__error {
  margin:        4px 0 0 0;
  padding:       var(--space-sm) var(--space-md);
  background:    var(--color-error-bg);
  border:        1px solid color-mix(in srgb, var(--color-error), white 75%);
  border-radius: var(--radius-sm);
  color:         var(--color-error);
  font-size:     0.8125rem;
  line-height:   1.4;
}

.confirm-dialog__actions {
  display:         flex;
  justify-content: flex-end;
  gap:             var(--space-sm);
  margin-top:      var(--space-sm);
  padding-top:     var(--space-md);
  border-top:      1px solid var(--color-border-subtle);
}

/* Phase E (Price List Data Integrity, 2026-05-19). The CSV import
   conflict modal wraps three forms (Cancel / Skip / Update); on
   narrow viewports they stack vertically rather than crowding the
   row. Each button stays full-width when stacked so the labels
   stay readable. */
.confirm-dialog__actions--multi {
  flex-wrap: wrap;
}
.confirm-dialog__actions--multi > form {
  flex: 0 0 auto;
}
@media (max-width: 540px) {
  .confirm-dialog__actions--multi {
    flex-direction: column-reverse;
    align-items: stretch;
  }
  .confirm-dialog__actions--multi > form {
    width: 100%;
  }
  .confirm-dialog__actions--multi > form > .btn {
    width: 100%;
  }
}

/* Wider dialog variant for the CSV-conflict preview, which needs to
   fit a 5-column table of changes. Caps at 720px so the modal still
   feels contained, not a takeover. */
.confirm-dialog.confirm-dialog--wide {
  width:     min(720px, 95vw);
  max-width: 720px;
}

/* =========================================================
   PL CATEGORY MINI-MODAL — Estética/UX item #2 C-3.5 (2026-06-01)
   Inline-editor type-cell picker. Replaces the failed in-cell
   <input list> attempt (suppressed by item-5 ellipsis
   overflow:hidden on type cells) with a clean-scope <dialog>
   where the datalist popup works reliably + the category list
   scales to many custom categories.
   ========================================================= */
.pl-cat-modal {
  width:         min(480px, 95vw);
  max-width:     480px;
  padding:       0;
  border:        none;
  border-radius: var(--radius-md, 8px);
  box-shadow:    var(--shadow-lg);
  background:    var(--color-bg);
}
.pl-cat-modal::backdrop {
  background: rgba(0, 0, 0, 0.4);
}
.pl-cat-modal__inner {
  padding: var(--space-lg);
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
}
.pl-cat-modal__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
}
.pl-cat-modal__title {
  margin: 0;
  font-size: 1.0625rem;
  font-weight: 600;
}
.pl-cat-modal__close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  padding: 0;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--color-text-muted);
  cursor: pointer;
  font-size: 1rem;
  line-height: 1;
  transition: background 120ms ease, color 120ms ease, border-color 120ms ease;
}
.pl-cat-modal__close:hover {
  background: var(--color-surface-alt);
  color: var(--color-text-primary);
  border-color: var(--color-border-default);
}
.pl-cat-modal__close:focus-visible {
  outline: 2px solid var(--color-text-primary);
  outline-offset: 2px;
}
.pl-cat-modal__context {
  margin: 0;
  padding: var(--space-sm) var(--space-md);
  background: var(--color-surface-alt);
  border-radius: var(--radius-sm);
  font-size: 0.875rem;
  color: var(--color-text-primary);
}
.pl-cat-modal__context-label {
  color: var(--color-text-muted);
  font-weight: 500;
  margin-right: var(--space-xs);
}
.pl-cat-modal__field {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
}
.pl-cat-modal__field-label {
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--color-text-muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.pl-cat-modal__input {
  height: var(--height-input);
  padding: 0 var(--space-md);
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius-sm);
  font-size: 1rem;
  font-family: var(--font-sans);
  background: var(--color-bg);
  color: var(--color-text-primary);
  transition: border-color 0.15s ease-out, box-shadow 0.15s ease-out;
}
.pl-cat-modal__input:focus {
  outline: none;
  border-color: var(--color-text-primary);
  box-shadow: 0 0 0 3px rgba(17, 17, 17, 0.10);
}
.pl-cat-modal__hint {
  margin: 0;
  font-size: 0.8125rem;
  color: var(--color-text-muted);
}
.pl-cat-modal__list-wrap {
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius-sm);
  max-height: 240px;
  overflow-y: auto;
}
.pl-cat-modal__list-empty {
  margin: 0;
  padding: var(--space-md);
  font-size: 0.875rem;
  color: var(--color-text-muted);
  text-align: center;
}
.pl-cat-modal__list-empty strong {
  color: var(--color-text-primary);
  font-weight: 600;
}
.pl-cat-modal__list {
  list-style: none;
  margin: 0;
  padding: 0;
}
.pl-cat-modal__list-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  width: 100%;
  padding: var(--space-sm) var(--space-md);
  background: transparent;
  border: none;
  border-bottom: 1px solid var(--color-border-subtle);
  text-align: left;
  font-family: var(--font-sans);
  font-size: 0.9375rem;
  color: var(--color-text-primary);
  cursor: pointer;
  transition: background 120ms ease;
}
.pl-cat-modal__list-item:last-child {
  border-bottom: none;
}
.pl-cat-modal__list-item:hover,
.pl-cat-modal__list-item:focus-visible {
  background: var(--color-surface-alt);
  outline: none;
}
.pl-cat-modal__list-item.is-current {
  background: color-mix(in srgb, var(--color-text-primary), white 95%);
  font-weight: 500;
}
.pl-cat-modal__list-item-name {
  flex: 1 1 auto;
}
.pl-cat-modal__list-item-current-marker {
  font-size: 0.75rem;
  color: var(--color-text-muted);
  margin-left: var(--space-xs);
}
.pl-cat-modal__list-item-count {
  flex: 0 0 auto;
  font-size: 0.8125rem;
  color: var(--color-text-muted);
  font-variant-numeric: tabular-nums;
}
.pl-cat-modal__footer {
  display: flex;
  justify-content: flex-end;
  gap: var(--space-sm);
}

/* =========================================================
   EDITABLE-TABLE ERROR TOAST — Estética/UX item #2 C-5
   (2026-06-01)
   Visible validation-error surface for the inline editor.
   Replaces the prior hover-only `title` tooltip on .cell-error,
   which was invisible on touch devices. Toast appears above the
   editable region when a save returns 400 with errors[]; per-cell
   .cell-error markers remain as secondary detail on desktop.
   ========================================================= */
.editable-table-error-toast {
  display:          flex;
  align-items:      flex-start;
  justify-content:  space-between;
  gap:              var(--space-md);
  padding:          var(--space-md) var(--space-lg);
  margin-bottom:    var(--space-md);
  background:       var(--color-error-bg);
  color:            var(--color-error);
  border:           1px solid color-mix(in srgb, var(--color-error), white 70%);
  border-radius:    var(--radius-sm);
  font-size:        0.9375rem;
  line-height:      1.4;
}
.editable-table-error-toast__text {
  margin:           0;
  flex:             1 1 auto;
  /* Allow long messages with code/type names to wrap cleanly */
  word-break:       break-word;
}
.editable-table-error-toast__close {
  flex:             0 0 auto;
  display:          inline-flex;
  align-items:      center;
  justify-content:  center;
  width:            32px;
  height:           32px;
  margin:           -4px -4px -4px 0;  /* tighten visual + keep tap target */
  padding:          0;
  background:       transparent;
  border:           1px solid transparent;
  border-radius:    var(--radius-sm);
  color:            var(--color-error);
  cursor:           pointer;
  font-size:        0.875rem;
  line-height:      1;
  transition:       background 120ms ease, border-color 120ms ease;
}
.editable-table-error-toast__close:hover {
  background:       color-mix(in srgb, var(--color-error), white 85%);
  border-color:     color-mix(in srgb, var(--color-error), white 60%);
}
.editable-table-error-toast__close:focus-visible {
  outline:          2px solid var(--color-error);
  outline-offset:   2px;
}

/* =========================================================
   MANAGE-CATEGORIES dialog — Estética/UX item #2 C-7b
   (2026-06-01)
   Central modal reached from the "Manage categories" button in
   the PL detail header. Separate component from the pick-mode
   .pl-cat-modal (per-cell picker); shares only the data blobs
   + title-casing helper. Three internal states (LIST / RENAME /
   DELETE) swapped via [data-cmgmt-state] + the [hidden] attribute.
   ========================================================= */
.category-mgmt-dialog {
  width:         min(560px, 95vw);
  max-width:     560px;
  padding:       0;
  border:        none;
  border-radius: var(--radius-md, 8px);
  box-shadow:    var(--shadow-lg);
  background:    var(--color-bg);
}
.category-mgmt-dialog::backdrop {
  background: rgba(0, 0, 0, 0.4);
}
.category-mgmt-dialog__inner {
  padding: var(--space-lg);
}
.category-mgmt-dialog__panel {
  display:        flex;
  flex-direction: column;
  gap:            var(--space-md);
}
.category-mgmt-dialog__panel[hidden] { display: none; }

.category-mgmt-dialog__header {
  display:         flex;
  align-items:     center;
  justify-content: space-between;
  gap:             var(--space-md);
}
.category-mgmt-dialog__title {
  margin:      0;
  font-size:   1.0625rem;
  font-weight: 600;
}
.category-mgmt-dialog__title-target {
  font-weight: 500;
  color:       var(--color-text-secondary);
}
.category-mgmt-dialog__back {
  display:         inline-flex;
  align-items:     center;
  padding:         var(--space-xs) var(--space-sm);
  background:      transparent;
  border:          none;
  border-radius:   var(--radius-sm);
  color:           var(--color-text-muted);
  font-size:       0.875rem;
  cursor:          pointer;
  font-family:     var(--font-sans);
  transition:      color 120ms ease, background 120ms ease;
}
.category-mgmt-dialog__back:hover {
  color:      var(--color-text-primary);
  background: var(--color-surface-alt);
}
.category-mgmt-dialog__close {
  display:         inline-flex;
  align-items:     center;
  justify-content: center;
  width:           32px;
  height:          32px;
  padding:         0;
  background:      transparent;
  border:          1px solid transparent;
  border-radius:   var(--radius-sm);
  color:           var(--color-text-muted);
  cursor:          pointer;
  font-size:       1rem;
  line-height:     1;
  flex-shrink:     0;
}
.category-mgmt-dialog__close:hover {
  background:   var(--color-surface-alt);
  color:        var(--color-text-primary);
  border-color: var(--color-border-default);
}
.category-mgmt-dialog__close:focus-visible {
  outline:        2px solid var(--color-text-primary);
  outline-offset: 2px;
}

.category-mgmt-dialog__intro {
  margin:    0;
  font-size: 0.875rem;
  color:     var(--color-text-muted);
  line-height: 1.45;
}

/* LIST: rows + per-row actions */
.category-mgmt-dialog__list {
  list-style: none;
  margin:     0;
  padding:    0;
  border:     1px solid var(--color-border-default);
  border-radius: var(--radius-sm);
  max-height: 320px;
  overflow-y: auto;
}
.category-mgmt-dialog__row {
  display:         flex;
  align-items:     center;
  justify-content: space-between;
  gap:             var(--space-md);
  padding:         var(--space-sm) var(--space-md);
  border-bottom:   1px solid var(--color-border-subtle);
  font-size:       0.9375rem;
}
.category-mgmt-dialog__row:last-child { border-bottom: none; }
.category-mgmt-dialog__row-name {
  flex: 1 1 auto;
  font-weight: 500;
  color: var(--color-text-primary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.category-mgmt-dialog__row-count {
  flex: 0 0 auto;
  font-size: 0.8125rem;
  color: var(--color-text-muted);
  font-variant-numeric: tabular-nums;
}
.category-mgmt-dialog__row-actions {
  flex: 0 0 auto;
  display: flex;
  gap: var(--space-xs);
}

.category-mgmt-dialog__empty {
  margin:     0;
  padding:    var(--space-md);
  font-size:  0.875rem;
  color:      var(--color-text-muted);
  text-align: center;
  border:     1px dashed var(--color-border-default);
  border-radius: var(--radius-sm);
}

.category-mgmt-dialog__placeholder {
  margin:    var(--space-md) 0;
  padding:   var(--space-md);
  font-size: 0.875rem;
  color:     var(--color-text-muted);
  background: var(--color-surface-alt);
  border-radius: var(--radius-sm);
  text-align: center;
}

/* RENAME state additions (C-7b-2) ------------------------------------ */
.category-mgmt-dialog__source-line {
  margin: 0;
  padding: var(--space-sm) var(--space-md);
  background: var(--color-surface-alt);
  border-radius: var(--radius-sm);
  font-size: 0.875rem;
  color: var(--color-text-primary);
}
.category-mgmt-dialog__source-label {
  color: var(--color-text-muted);
  margin-right: var(--space-xs);
}
.category-mgmt-dialog__source-count {
  color: var(--color-text-muted);
  margin-left: var(--space-xs);
  font-size: 0.8125rem;
}
.category-mgmt-dialog__field {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
}
.category-mgmt-dialog__field-label {
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--color-text-muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.category-mgmt-dialog__input {
  height: var(--height-input);
  padding: 0 var(--space-md);
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius-sm);
  font-size: 1rem;
  font-family: var(--font-sans);
  background: var(--color-bg);
  color: var(--color-text-primary);
  transition: border-color 0.15s ease-out, box-shadow 0.15s ease-out;
}
.category-mgmt-dialog__input:focus {
  outline: none;
  border-color: var(--color-text-primary);
  box-shadow: 0 0 0 3px rgba(17, 17, 17, 0.10);
}
.category-mgmt-dialog__hint {
  margin: 0;
  font-size: 0.8125rem;
  color: var(--color-text-muted);
}
.category-mgmt-dialog__list-wrap {
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius-sm);
  max-height: 200px;
  overflow-y: auto;
}
.category-mgmt-dialog__list--scrollable {
  /* In rename/delete contexts the list lives inside .list-wrap which
     gives it scroll + border. The base .category-mgmt-dialog__list
     (in LIST state) is itself the scroll container. */
  border: 0;
  border-radius: 0;
  max-height: none;
  overflow: visible;
}
.category-mgmt-dialog__row--pickable {
  cursor: pointer;
  transition: background 120ms ease;
}
.category-mgmt-dialog__row--pickable:hover,
.category-mgmt-dialog__row--pickable:focus-visible {
  background: var(--color-surface-alt);
  outline: none;
}
.category-mgmt-dialog__list-empty {
  margin: 0;
  padding: var(--space-md);
  font-size: 0.8125rem;
  color: var(--color-text-muted);
  text-align: center;
  border-top: 1px solid var(--color-border-subtle);
}
.category-mgmt-dialog__warning {
  margin: 0;
  padding: var(--space-sm) var(--space-md);
  font-size: 0.875rem;
  line-height: 1.45;
  background: var(--color-warning-bg, #fef6e7);
  color: var(--color-warning, #8a5a00);
  border: 1px solid color-mix(in srgb, var(--color-warning, #c98a00), white 70%);
  border-radius: var(--radius-sm);
}
.category-mgmt-dialog__warning strong {
  display: block;
  margin-bottom: 2px;
  font-weight: 600;
}
/* C-7b-2 FIX 1 (2026-06-01) — plain-language summary line above the
   detailed merge-collision breakdown. Bolder weight + slight bottom
   margin separates it visually from the detailed text below. */
.category-mgmt-dialog__warning-plain {
  margin: 0 0 var(--space-sm) 0;
  font-weight: 600;
  color: var(--color-text-primary);
  line-height: 1.45;
}

/* DELETE state additions (C-7b-3 — 2026-06-01) ----------------------- */
.category-mgmt-dialog__radio-group {
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius-sm);
  padding: var(--space-md);
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}
.category-mgmt-dialog__radio-group > .category-mgmt-dialog__field-label {
  /* legend rendering — uppercase-style header above the options */
  display: block;
  margin-bottom: var(--space-xs);
  padding: 0;
}
.category-mgmt-dialog__radio-option {
  display: flex;
  align-items: flex-start;
  gap: var(--space-sm);
  padding: var(--space-xs) 0;
  font-size: 0.9375rem;
  color: var(--color-text-primary);
  cursor: pointer;
}
.category-mgmt-dialog__radio-option input[type="radio"] {
  margin-top: 3px;   /* align with first line of label text */
  flex-shrink: 0;
}
.category-mgmt-dialog__radio-label {
  flex: 1 1 auto;
  line-height: 1.4;
}
.category-mgmt-dialog__radio-detail {
  margin: 0 0 var(--space-xs) var(--space-lg);
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
}
.category-mgmt-dialog__select {
  height: var(--height-input);
  padding: 0 var(--space-md);
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius-sm);
  font-size: 0.9375rem;
  font-family: var(--font-sans);
  background: var(--color-bg);
  color: var(--color-text-primary);
  appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23888888' d='M6 8L1 3h10z'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right var(--space-md) center;
  padding-right: var(--space-3xl);
}
.category-mgmt-dialog__select:focus {
  outline: none;
  border-color: var(--color-text-primary);
  box-shadow: 0 0 0 3px rgba(17, 17, 17, 0.10);
}

/* Destructive variant of the warning block — red-tinted for cascade-
   delete (vs amber for the rename/reassign merge-collision case). */
.category-mgmt-dialog__warning--destructive {
  background:   var(--color-error-bg);
  color:        var(--color-error);
  border-color: color-mix(in srgb, var(--color-error), white 70%);
}
.category-mgmt-dialog__warning--destructive strong {
  color: var(--color-error);
}

.category-mgmt-dialog__footer {
  display:         flex;
  justify-content: flex-end;
  gap:             var(--space-sm);
}

/* =========================================================
   TYPE-PICKER button — Estética/UX item #2 C-4 (2026-06-01)
   The item_form.html Type field is a button styled like a
   normal field input + a caret, opening the shared category
   mini-modal. Matches .field input height/border/padding for
   visual consistency in the form. Hidden #type input adjacent
   carries the actual submitted value.
   ========================================================= */
.type-picker {
  display:        flex;
  align-items:    center;
  justify-content: space-between;
  gap:            var(--space-sm);
  width:          100%;
  height:         var(--height-input);
  padding:        0 var(--space-lg);
  background:     var(--color-bg);
  border:         1px solid var(--color-border-default);
  border-radius:  var(--radius-sm);
  font-size:      1rem;
  font-family:    var(--font-sans);
  color:          var(--color-text-primary);
  cursor:         pointer;
  text-align:     left;
  transition:     border-color 0.15s ease-out, box-shadow 0.15s ease-out;
}
.type-picker:hover {
  border-color: var(--color-text-muted);
}
.type-picker:focus-visible {
  outline:        none;
  border-color:   var(--color-text-primary);
  box-shadow:     0 0 0 3px rgba(17, 17, 17, 0.10);
}
.type-picker__label {
  flex: 1 1 auto;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.type-picker__caret {
  flex: 0 0 auto;
  color: var(--color-text-muted);
  font-size: 0.875rem;
  line-height: 1;
}

/* Scrollable preview area inside the wide CSV-conflict modal. Caps
   table height so a 200-row collision doesn't push the action buttons
   off-screen. */
.csv-conflict-preview {
  max-height: 40vh;
  overflow:   auto;
  border:     1px solid var(--color-border-subtle);
  border-radius: var(--radius-sm);
}
.csv-conflict-preview .data-table {
  margin: 0;
}

/* Spinner inside the destructive verb button — mirrors
   .preview-apply-spinner from commit 1. Same keyframe + same
   speed (0.8s linear) for visual consistency across modals. */
.confirm-verb-spinner {
  display:   inline-block;
  animation: confirm-verb-spin 0.8s linear infinite;
}
@keyframes confirm-verb-spin {
  to { transform: rotate(360deg); }
}

@keyframes confirm-icon-pulse {
  0%   { transform: scale(0.8); opacity: 0; }
  60%  { transform: scale(1.1); opacity: 1; }
  100% { transform: scale(1);   opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .confirm-dialog__icon { animation: none; }
  .confirm-verb-spinner { animation: none !important; }
}

@media (max-width: 640px) {
  .confirm-dialog {
    width:         92vw;
    max-width:     none;
    border-radius: var(--radius);
  }
  .confirm-dialog__inner { padding: 20px 18px 14px 18px; }
  .confirm-dialog__title { font-size: 1rem; }
}


/* =========================================================
   DAY 15b commit 1 — Undo toast (top-right, below edit toast)
   ========================================================= */

.copilot-undo-toast-container {
  position:    fixed;
  top:         80px;            /* below edit toast (top: 24px + ~48px height) */
  right:       24px;
  z-index:     1003;            /* above panel (1001), above edit toast (1002) */
  display:     flex;
  flex-direction: column;
  gap:         8px;
  pointer-events: none;
}

.copilot-undo-toast {
  display:       inline-flex;
  align-items:   center;
  gap:           10px;
  background:    var(--color-surface);
  color:         var(--color-text-primary);
  border:        1px solid var(--color-border-default);
  border-left:   4px solid var(--color-success);
  border-radius: var(--radius-sm);
  padding:       10px 14px;
  font-size:     0.875rem;
  box-shadow:    0 6px 16px rgba(0, 0, 0, 0.15);
  pointer-events: auto;
  transform:     translateX(20px);
  opacity:       0;
  transition:    transform 200ms ease-out, opacity 200ms ease-out;
}

.copilot-undo-toast--open {
  transform: translateX(0);
  opacity:   1;
}

.copilot-undo-toast__icon { font-size: 1rem; }
.copilot-undo-toast__text { flex: 0 1 auto; }

.copilot-undo-toast__undo {
  background:    transparent;
  color:         var(--color-text-muted);
  border:        1px solid var(--color-border-default);
  border-radius: 14px;
  padding:       4px 12px;
  font:          inherit;
  font-size:     0.8125rem;
  cursor:        not-allowed;       /* enabled when backend available (commit 3+) */
  opacity:       0.55;
  /* VDP Step 4: transition + full-inversion hover (matches .btn-secondary
     pattern) so the Undo button reads as a black-inversion CTA when
     enabled, instead of the prior color-only enabled state. Pill shape
     (14px radius) preserved — only the hover behavior shifts. */
  transition:
    background-color 0.15s ease-out,
    color 0.15s ease-out,
    border-color 0.15s ease-out;
}
/* Enabled state matches .btn-primary visual language — black solid bg
   default + red hover. Undo is time-critical and deserves the brand-
   identity flash on hover rather than the .btn-secondary inversion
   treatment. Pill shape (14px radius) preserved. */
.copilot-undo-toast__undo:not(:disabled) {
  cursor: pointer;
  opacity: 1;
  background: var(--color-text-primary);
  color: var(--color-text-inverse);
  border-color: var(--color-text-primary);
}
.copilot-undo-toast__undo:not(:disabled):hover {
  background: var(--color-accent-vibrant);
  color: var(--color-text-inverse);
  border-color: var(--color-accent-vibrant);
}
.copilot-undo-toast__undo:not(:disabled):active {
  background: #D62150;
  border-color: #D62150;
}

/* Error state — Day 15b commit 3. Shown when /api/copilot/undo returns
   410 / 404 / 500 / network error. Replaces the success ✓ icon with ⚠
   in JS; here we re-color the toast border + background to make the
   error visually distinct from the success state. */
.copilot-undo-toast--error {
  border-color:    var(--color-error);
  background:      var(--color-error-bg);
}
.copilot-undo-toast--error .copilot-undo-toast__icon {
  color: var(--color-error);
}

.copilot-undo-toast__close {
  background: transparent;
  border:     none;
  color:      var(--color-text-muted);
  cursor:     pointer;
  font-size:  1.2rem;
  line-height: 1;
  padding:    0 4px;
}
.copilot-undo-toast__close:hover { color: var(--color-text-primary); }

@media (prefers-reduced-motion: reduce) {
  .copilot-undo-toast { transition: none !important; }
}

@media (max-width: 640px) {
  .copilot-undo-toast-container { top: 70px; right: 16px; left: 16px; }
  .copilot-undo-toast { width: 100%; }
}


/* ===========================================================================
   Auto-DOM-refresh region flash (Day 15b commit 5)

   200ms subtle background fade applied to #estimate-summary-region and
   #estimate-items-region after copilot_dom_refresh.js patches them in
   place. Peripheral "this just changed" affordance — the user-initiated
   action (Apply / Undo / Confirm / SMALL tool) is the primary signal.

   Honors prefers-reduced-motion (animation suppressed; class becomes
   a no-op).
   =========================================================================== */

.copilot-region-flash {
  animation: copilot-region-flash 200ms ease-out;
}

@keyframes copilot-region-flash {
  0%   { background-color: var(--surface-highlight); }
  100% { background-color: transparent; }
}

@media (prefers-reduced-motion: reduce) {
  .copilot-region-flash { animation: none; }
}


/* ===========================================================================
   DASHBOARD — VDP Step 5

   Single-column dashboard with stacked sections: header → stats →
   quick actions → dynamic section (Get Started in empty state /
   Recent Estimates + Support footer in has-data state).

   Whole-card / whole-row click affordance via <a> wrappers — cursor
   pointer, hover lift on actionable surfaces.

   Responsive: 4-col stats at ≥1024 (2×2 ≥768, 1-col below), 2-col
   quick actions at ≥768, 3-col Get Started at ≥1024 (1-col below).
   =========================================================================== */

/* ----- Header ----- */
.dashboard-header {
  margin-bottom: var(--space-2xl);
}
.dashboard-header__title {
  font-family: var(--font-sans);
  font-size: 1.75rem;
  font-weight: 700;
  line-height: 1.25;
  letter-spacing: -0.015em;
  color: var(--color-text-primary);
  margin: 0 0 var(--space-xs);
}
.dashboard-header__subtitle {
  font-size: 0.875rem;
  color: var(--color-text-muted);
  margin: 0;
}

/* ----- Stats (3 compact cards — VDP Step 5 adjustment: dropped 4th
   token card; reduced padding for denser visual presence) ----- */
.dashboard-stats {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-lg);
  margin-bottom: var(--space-3xl);
}
@media (min-width: 768px) {
  .dashboard-stats { grid-template-columns: repeat(3, 1fr); }
}

.dashboard-stat-card {
  background: var(--color-surface);
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
  padding: var(--space-md) var(--space-lg);
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
}

.dashboard-stat-card__label {
  font-size: 0.75rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-text-muted);
}

.dashboard-stat-card__value {
  font-size: 1.75rem;
  font-weight: 700;
  letter-spacing: -0.02em;
  line-height: 1.1;
  color: var(--color-text-primary);
  font-variant-numeric: tabular-nums;
}

/* ----- Actions row (VDP Step 5 adjustment: 50/50 split — left column
   stacks 2 Quick Action cards, right column hosts a single full-height
   Setup Assistance hero card). Below 1024px, both columns stack. ----- */
.dashboard-actions-row {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-xl);
  margin-bottom: var(--space-3xl);
}
@media (min-width: 1024px) {
  .dashboard-actions-row {
    grid-template-columns: 1fr 1fr;
    align-items: stretch;
  }
}

.dashboard-actions-row__quick {
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
}

/* Each Quick Action card fills equal height in its column */
.dashboard-actions-row__quick .dashboard-action-card {
  flex: 1;
}

/* Quick Action card — horizontal layout: text-left + icon-circle right.
   VDP Step 5 adjustment per user feedback ("cards look too empty"):
   prominent coral icon circle on the right gives each card iconic
   presence at compact heights (~120-140px). */
.dashboard-action-card {
  background: var(--color-surface);
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  padding: var(--space-lg) var(--space-xl);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-lg);
  min-height: 120px;
  color: var(--color-text-primary);
  text-decoration: none;
  cursor: pointer;
  transition:
    transform 0.15s ease-out,
    box-shadow 0.15s ease-out,
    border-color 0.15s ease-out;
}
.dashboard-action-card:hover {
  transform: translateY(-2px);
  box-shadow: var(--shadow-md);
  border-color: var(--color-border-strong);
  text-decoration: none;
  color: var(--color-text-primary);
}
.dashboard-action-card:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px rgba(17, 17, 17, 0.12), var(--shadow-sm);
}

.dashboard-action-card__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  flex: 1;
  min-width: 0;
}

.dashboard-action-card__title {
  font-size: 1rem;
  font-weight: 600;
  color: var(--color-text-primary);
}

.dashboard-action-card__desc {
  font-size: 0.875rem;
  color: var(--color-text-muted);
  line-height: 1.45;
}

/* Coral circle on the right side of each Quick Action card.
   --color-accent-vibrant-bg (#FFE8EE) base + coral stroke icon. */
.dashboard-action-card__icon-circle {
  flex-shrink: 0;
  width: 64px;
  height: 64px;
  border-radius: 50%;
  background: var(--color-accent-vibrant-bg);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--color-accent-vibrant);
}

/* ----- Setup Assistance hero card (50/50 row right column) -----
   Photo + dark-overlay + white-text mode per VDP Step 5 adjustment.
   When app/static/images/setup-assistance.jpg lands, the inline-style
   background-image swaps the placeholder gray for the photo; overlay
   + content stay the same. Placeholder "IMAGE" badge in top-right
   marks the awaiting-photo state. ----- */
.dashboard-setup-card {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  border-radius: var(--radius-lg);
  overflow: hidden;
  min-height: 280px;
  background-color: #F5F5F5;
  background-size: cover;
  background-position: center;
  border: 1px solid var(--color-border-default);
  color: var(--color-text-inverse);
  text-decoration: none;
  cursor: pointer;
  transition:
    transform 0.15s ease-out,
    box-shadow 0.15s ease-out,
    border-color 0.15s ease-out;
}
.dashboard-setup-card::before {
  content: '';
  position: absolute;
  inset: 0;
  background: rgba(17, 17, 17, 0.60);
  z-index: 1;
}
.dashboard-setup-card:hover {
  transform: translateY(-2px);
  box-shadow: var(--shadow-md);
  border-color: var(--color-border-strong);
  text-decoration: none;
  color: var(--color-text-inverse);
}
.dashboard-setup-card:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px rgba(17, 17, 17, 0.12), var(--shadow-sm);
}

/* Placeholder badge — small uppercase label in top-right, white-30%
   opacity. Visible faintly over the dark overlay as a marker that
   this is awaiting an image. Remove the .placeholder markup once a
   real photo is in place. */
.dashboard-setup-card__placeholder {
  position: absolute;
  top: var(--space-md);
  right: var(--space-md);
  z-index: 2;
  font-size: 0.6875rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  font-weight: 600;
  color: rgba(255, 255, 255, 0.30);
  pointer-events: none;
}

.dashboard-setup-card__content {
  position: relative;
  z-index: 2;
  padding: var(--space-xl);
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}

.dashboard-setup-card__title {
  font-size: 1.25rem;
  font-weight: 700;
  color: var(--color-text-inverse);
  margin: 0;
  line-height: 1.3;
}

.dashboard-setup-card__desc {
  font-size: 0.875rem;
  color: rgba(255, 255, 255, 0.85);
  line-height: 1.55;
  margin: 0;
}

.dashboard-setup-card__cta {
  font-size: 0.875rem;
  font-weight: 500;
  color: var(--color-accent-vibrant);
  margin-top: var(--space-xs);
}

/* ----- Get Started (empty state — 3 onboarding cards) ----- */
.dashboard-get-started {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-xl);
  margin-bottom: var(--space-3xl);
}
@media (min-width: 1024px) {
  .dashboard-get-started { grid-template-columns: repeat(3, 1fr); }
}

/* ----- Onboarding card (empty state Get Started — 3 cards) -----
   Photo background + dark overlay + white circle (top-left) with icon
   + white text content at bottom. VDP Step 5 adjustment: all 3 cards
   use the same hero treatment (was: icon + photo mixed). White circle
   carries a stroke icon that previews the card's topic. Placeholder
   "IMAGE" badge top-right marks the awaiting-photo state. ----- */
.dashboard-onboarding-card {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  border-radius: var(--radius-lg);
  overflow: hidden;
  min-height: 240px;
  background-color: #F5F5F5;
  background-size: cover;
  background-position: center;
  border: 1px solid var(--color-border-default);
  color: var(--color-text-inverse);
  text-decoration: none;
  cursor: pointer;
  transition:
    transform 0.15s ease-out,
    box-shadow 0.15s ease-out,
    border-color 0.15s ease-out;
}
.dashboard-onboarding-card::before {
  content: '';
  position: absolute;
  inset: 0;
  background: rgba(17, 17, 17, 0.55);
  z-index: 1;
}
.dashboard-onboarding-card:hover {
  transform: translateY(-2px);
  box-shadow: var(--shadow-md);
  border-color: var(--color-border-strong);
  text-decoration: none;
  color: var(--color-text-inverse);
}
.dashboard-onboarding-card:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px rgba(17, 17, 17, 0.12), var(--shadow-sm);
}

/* White circle (~56px) holding the stroke icon. Sits top-left over the
   photo + overlay. Icon stroke is --color-text-primary (dark) so it
   reads on the white circle. */
.dashboard-onboarding-card__icon-circle {
  position: absolute;
  top: var(--space-lg);
  left: var(--space-lg);
  z-index: 2;
  width: 56px;
  height: 56px;
  border-radius: 50%;
  background: var(--color-text-inverse);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--color-text-primary);
}

.dashboard-onboarding-card__placeholder {
  position: absolute;
  top: var(--space-md);
  right: var(--space-md);
  z-index: 2;
  font-size: 0.6875rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  font-weight: 600;
  color: rgba(255, 255, 255, 0.30);
  pointer-events: none;
}

.dashboard-onboarding-card__content {
  position: relative;
  z-index: 2;
  padding: var(--space-xl);
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}

.dashboard-onboarding-card__title {
  font-size: 1.125rem;
  font-weight: 700;
  color: var(--color-text-inverse);
  margin: 0;
  line-height: 1.3;
}

.dashboard-onboarding-card__desc {
  font-size: 0.875rem;
  color: rgba(255, 255, 255, 0.85);
  line-height: 1.55;
  margin: 0;
}

.dashboard-onboarding-card__cta {
  font-size: 0.875rem;
  font-weight: 500;
  color: var(--color-accent-vibrant);
  margin-top: var(--space-xs);
}

/* ----- Recent Estimates (has-data state) ----- */
.dashboard-recent-estimates {
  background: var(--color-surface);
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
  overflow: hidden;
  margin-bottom: var(--space-2xl);
}

.dashboard-recent-estimates__list {
  display: flex;
  flex-direction: column;
}

.dashboard-estimate-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-lg);
  padding: var(--space-md) var(--space-lg);
  border-bottom: 1px solid var(--color-border-subtle);
  color: var(--color-text-primary);
  text-decoration: none;
  transition: background-color 0.15s ease-out;
}
.dashboard-estimate-item:last-child {
  border-bottom: none;
}
.dashboard-estimate-item:hover {
  background: var(--color-surface-alt);
  text-decoration: none;
  color: var(--color-text-primary);
}
.dashboard-estimate-item:focus-visible {
  outline: none;
  background: var(--color-surface-alt);
  box-shadow: inset 0 0 0 2px var(--color-text-primary);
}

.dashboard-estimate-item__main {
  flex: 1;
  min-width: 0;
}

.dashboard-estimate-item__project {
  font-size: 1rem;
  font-weight: 500;
  color: var(--color-text-primary);
  margin-bottom: 2px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.dashboard-estimate-item__status {
  font-size: 0.875rem;
  color: var(--color-text-secondary);
  font-variant-numeric: tabular-nums;
}

.dashboard-estimate-item__time {
  font-size: 0.875rem;
  color: var(--color-text-muted);
  white-space: nowrap;
  flex-shrink: 0;
}

/* Per-status inline color cues (no badges — text-only color cue
   keeps the row visually quiet per VDP Step 5 user decision). */
.estimate-status--done       { color: var(--color-success); font-weight: 500; }
.estimate-status--analyzing  { color: var(--color-info); font-weight: 500; }
/* Pending bumped muted->warning amber (2026-06-10) so the dashboard matches the
   project-list status colors app-wide. Cancelled stays muted (de-emphasized). */
.estimate-status--pending    { color: var(--color-warning); font-weight: 500; }
.estimate-status--cancelled  { color: var(--color-text-muted); }
.estimate-status--error      { color: var(--color-error); font-weight: 500; }

.dashboard-recent-estimates__footer {
  padding: var(--space-md) var(--space-lg);
  border-top: 1px solid var(--color-border-subtle);
  text-align: right;
}

.dashboard-recent-estimates__view-all {
  font-size: 0.875rem;
  font-weight: 500;
  color: var(--color-accent-vibrant);
  text-decoration: none;
}
.dashboard-recent-estimates__view-all:hover {
  color: color-mix(in srgb, var(--color-accent-vibrant), black 12%);
  text-decoration: none;
}

/* (VDP Step 5 adjustment: .dashboard-support-footer removed.
   Setup Assistance now lives in the actions-row right column as a
   tall hero card, not as a small footer band. The empty-state
   onboarding still surfaces it as Card 1 of Get Started, but only
   inside Get Started — no second instance below the Recent Estimates
   list in has-data state.) */

/* =========================================================
   VDP STEP 6 — PROJECTS INDEX (2026-05-15)

   Container-wrapped projects list with toolbar (search + sort) and
   a 2-column grid of project cards. Each card has an absolutely-
   positioned kebab menu that reveals on hover (desktop) or stays
   visible (mobile/tablet). Kebab dropdown provides Edit + Delete
   which reuse the Day 17 shared edit modal + confirm-dialog form
   infrastructure.

   The new card block is .projects-grid-card (NOT .project-card)
   to avoid stepping on price_lists/index.html's reuse of the
   legacy .project-card BEM tree. Price Lists migrates to the
   new pattern in VDP Step 11.
   ========================================================= */

.projects-container {
  background: var(--color-surface);
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
  padding: var(--space-xl);
}

/* ----- Toolbar (search + sort) ----- */
.projects-toolbar {
  display: flex;
  align-items: center;
  gap: var(--space-lg);
}

.projects-toolbar__search { flex: 1 1 60%; min-width: 0; }
.projects-toolbar__sort   { flex: 0 0 auto; }

.projects-search {
  position: relative;
  display: block;
}

.projects-search__icon {
  position: absolute;
  left: var(--space-lg);
  top: 50%;
  transform: translateY(-50%);
  color: var(--color-text-muted);
  pointer-events: none;
  display: flex;
}

.projects-search__input {
  width: 100%;
  height: 40px;
  padding: 0 var(--space-lg) 0 var(--space-3xl);
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius-sm);
  font-size: 0.9375rem;
  font-family: var(--font-sans);
  color: var(--color-text-primary);
  background: var(--color-bg);
  transition: border-color 0.15s ease-out, box-shadow 0.15s ease-out;
}

.projects-search__input:focus {
  outline: none;
  border-color: var(--color-text-primary);
  box-shadow: 0 0 0 3px rgba(17, 17, 17, 0.10);
}

.projects-search__input::placeholder { color: var(--color-text-muted); }

.projects-search__input:disabled {
  background: var(--color-surface-alt);
  color: var(--color-text-disabled);
  cursor: not-allowed;
}

.projects-search__input:disabled + .projects-search__icon,
.projects-search__input:disabled ~ .projects-search__icon {
  color: var(--color-text-disabled);
}

.projects-sort {
  height: 40px;
  padding: 0 var(--space-3xl) 0 var(--space-lg);
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius-sm);
  font-size: 0.9375rem;
  font-family: var(--font-sans);
  color: var(--color-text-primary);
  background: var(--color-bg);
  cursor: pointer;
  appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23888888' d='M6 8L1 3h10z'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right var(--space-lg) center;
  transition: border-color 0.15s ease-out, box-shadow 0.15s ease-out;
}

.projects-sort:focus {
  outline: none;
  border-color: var(--color-text-primary);
  box-shadow: 0 0 0 3px rgba(17, 17, 17, 0.10);
}

.projects-sort:disabled {
  background-color: var(--color-surface-alt);
  color: var(--color-text-disabled);
  cursor: not-allowed;
}

/* ----- Divider between toolbar and grid ----- */
.projects-divider {
  height: 1px;
  background: var(--color-border-subtle);
  margin: var(--space-lg) 0;
  border: 0;
}

/* ----- Grid of cards ----- */
.projects-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--space-lg);
}

.projects-grid-card {
  position: relative;
  background: var(--color-card-tint);  /* soft warm lift (Phase F polish) */
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius);
  transition:
    transform 0.15s ease-out,
    box-shadow 0.15s ease-out,
    border-color 0.15s ease-out;
  /* Bundle Estética/UX item 6 Layer 2 sub-step 2b-4 (2026-05-31):
     Grid items default to min-width: auto (won't shrink below
     content's intrinsic min-content). Without this override, an
     unbreakable child (long project name, long PL block_code) would
     push the grid item past viewport at narrow widths even though
     the grid restacks to 1fr at <=768px. Same pattern as Layer 1's
     .app-content { min-width: 0 }. Applies to both surfaces using
     this card (projects index + PL index via shared .projects-grid). */
  min-width: 0;
}

.projects-grid-card:hover {
  transform: translateY(-2px);
  box-shadow: var(--shadow-md);
  border-color: var(--color-border-strong);
}

.projects-grid-card__body-link {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  padding: var(--space-lg);
  padding-right: var(--space-3xl);   /* leave room for kebab corner */
  text-decoration: none;
  color: var(--color-text-primary);
}
.projects-grid-card__body-link:hover { text-decoration: none; }

.projects-grid-card__name {
  font-size: 1rem;
  font-weight: 600;
  line-height: 1.4;
  color: var(--color-text-primary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.projects-grid-card__client {
  font-size: 0.875rem;
  color: var(--color-text-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.projects-grid-card__stats {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin-top: var(--space-md);
}

.projects-grid-card__count {
  font-size: 0.875rem;
  font-weight: 500;
  color: var(--color-text-secondary);
}

.projects-grid-card__date {
  font-size: 0.75rem;
  color: var(--color-text-muted);
  letter-spacing: 0.01em;
}

/* ----- Kebab button (top-right corner of card) -----
   Hover treatment polish (2026-05-15): black-by-default for
   discoverability (instead of muted gray), light-gray CIRCULAR
   background on direct hover for clear "this is clickable"
   affordance, and a darker active state. border-radius 50% is
   constant so the focus outline + hover halo are circular.
   Shared visual treatment with .estimate-row__kebab (Step 7). */
.projects-grid-card__kebab {
  position: absolute;
  top: var(--space-sm);
  right: var(--space-sm);
  width: 28px;
  height: 28px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: 0;
  border-radius: 50%;
  color: var(--color-text-primary);
  cursor: pointer;
  opacity: 1;
  transition: opacity 0.3s ease, background-color 0.15s ease-out;
  z-index: 2;
}

.projects-grid-card__kebab:hover {
  background: var(--color-border-subtle);   /* #F0F0F0 — soft gray halo */
}

.projects-grid-card__kebab:active {
  background: var(--color-border-default);  /* #E5E5E5 — slightly darker on press */
}

.projects-grid-card__kebab:focus-visible {
  outline: 2px solid var(--color-text-primary);
  outline-offset: 1px;
}

/* Desktop: kebab fades in on card hover. Tablet/mobile: always visible
   (no hover state available with touch input). */
@media (min-width: 1024px) {
  .projects-grid-card__kebab { opacity: 0; }
  .projects-grid-card:hover .projects-grid-card__kebab,
  .projects-grid-card__kebab:focus-visible,
  .projects-grid-card__kebab[aria-expanded="true"] { opacity: 1; }
}

/* ----- Kebab dropdown menu ----- */
.projects-grid-card__dropdown {
  position: absolute;
  top: calc(var(--space-sm) + 28px + 4px);
  right: var(--space-sm);
  min-width: 140px;
  background: var(--color-bg);
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-md);
  padding: var(--space-xs);
  z-index: 10;
  display: none;
}

.projects-grid-card__dropdown.is-open { display: block; }

/* Pinear tarjetas Commit 2 (2026-06-08) — open-kebab clipping fix.
   The card's :hover transform (translateY) creates a stacking context
   that traps the dropdown's z-index, so a later-in-DOM sibling card
   paints over the open menu (Delete vanishes under the next card).
   Raising the card itself WHILE its dropdown is open lifts the whole
   subtree above neighboring cards. Hooks the existing JS-toggled
   .is-open (no JS change). :has() is an established pattern here
   (logo-remove, icon picker, copilot-launcher offset). Shared BEM tree
   -> fixes the same latent bug on the Projects index too. */
.projects-grid-card:has(.projects-grid-card__dropdown.is-open) {
  z-index: 20;
}

.projects-kebab-item {
  display: block;
  width: 100%;
  padding: var(--space-sm) var(--space-md);
  background: transparent;
  border: 0;
  border-radius: 4px;
  font-size: 0.875rem;
  font-weight: 500;
  font-family: var(--font-sans);
  color: var(--color-text-primary);
  text-align: left;
  cursor: pointer;
  transition: background-color 0.1s ease;
}

.projects-kebab-item:hover { background: var(--color-surface-alt); }
.projects-kebab-item:focus-visible {
  outline: 2px solid var(--color-text-primary);
  outline-offset: -2px;
}

.projects-kebab-item--danger { color: var(--color-error); }
.projects-kebab-item--danger:hover { background: var(--color-error-bg); }

.projects-kebab-divider {
  height: 1px;
  background: var(--color-border-subtle);
  margin: 2px 0;
  border: 0;
}

/* ----- Empty + no-results states (inside container) ----- */
.projects-empty,
.projects-no-results {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  padding: var(--space-3xl) var(--space-lg);
  gap: var(--space-lg);
}

.projects-empty__icon {
  color: var(--color-text-muted);
  display: flex;
  align-items: center;
  justify-content: center;
}

.projects-empty__title {
  font-size: 1.25rem;
  font-weight: 700;
  line-height: 1.3;
  color: var(--color-text-primary);
}

.projects-empty__desc {
  font-size: 1rem;
  line-height: 1.55;
  color: var(--color-text-secondary);
  max-width: 460px;
}

.projects-no-results__message {
  font-size: 1rem;
  color: var(--color-text-secondary);
}

.projects-no-results__message strong {
  color: var(--color-text-primary);
  font-weight: 600;
}

/* The container hides itself when an inline empty/no-results state is
   active so the toolbar still sits inside the same envelope card. */
.projects-grid.is-hidden { display: none; }

/* ----- Responsive ----- */
@media (max-width: 1023px) {
  /* Tablet: grid stays 2-col, kebab is always visible (already the
     default outside the >=1024 media query above). */
}

@media (max-width: 767px) {
  .projects-toolbar {
    flex-direction: column;
    align-items: stretch;
    gap: var(--space-md);
  }
  .projects-toolbar__search,
  .projects-toolbar__sort { flex: 0 0 auto; width: 100%; }
  .projects-sort { width: 100%; }

  .projects-grid { grid-template-columns: 1fr; }

  .projects-container { padding: var(--space-lg); }
}

/* =========================================================
   VDP STEP 7 — PROJECT DETAIL (2026-05-15)

   Page-level layout: project header sits OUTSIDE a container card
   (full-width, page-bg), followed by an estimates container card
   mirroring the Step 6 toolbar + list pattern. Each estimate row
   shows a status dot + filename + status/total + items/time, with
   a hover-revealed kebab corner offering View detail / Re-analyze /
   Delete.

   Has-estimates: "Upload PDF" header button opens an overlay modal
   containing the existing upload-zone form. Empty-estimates: the
   container body is replaced by the inline upload form; the header
   Upload button is hidden so the dropzone is the only affordance.

   The new estimate row uses its own .estimate-row* BEM tree. The
   toolbar / search / sort recipes echo Step 6 with a fresh
   .estimates-* namespace (per Phase A Q4) so this commit doesn't
   refactor Step 6.
   ========================================================= */

/* ----- Project header (outside the container) ----- */
.project-detail-header { margin-bottom: var(--space-2xl); }

.project-detail-header__client {
  font-size: 1rem;
  color: var(--color-text-muted);
  margin-top: var(--space-xs);
}

.project-detail-header__description {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  text-overflow: ellipsis;
  margin-top: var(--space-md);
  font-size: 0.875rem;
  line-height: 1.55;
  color: var(--color-text-secondary);
  max-width: 720px;
}

.project-detail-header__dates {
  margin-top: var(--space-md);
  font-size: 0.75rem;
  color: var(--color-text-muted);
  letter-spacing: 0.01em;
}

.project-detail-header__dates-sep {
  margin: 0 var(--space-sm);
  color: var(--color-border-strong);
}

/* VDP Step 7 retrofit (2026-05-17 → 18): the prior
   .project-detail-header__total + .project-detail-header__actions
   rules were removed here. Total estimated + the 3 action buttons
   migrated to the new .project-detail-footer (sticky) per the
   sticky-footer pattern decision captured at 1bc1e8e (already
   applied to Step 10 + Step 11). */

/* ----- Project detail content wrapper (Step 7 retrofit) ----- */
.project-detail-content--has-footer { padding-bottom: 100px; }

/* ----- Project detail sticky footer ----- */
.project-detail-footer {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 50;
  background: var(--color-bg);
  border-top: 1px solid var(--color-border-default);
  box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.06);
  padding: var(--space-md) var(--space-2xl);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-lg);
  min-height: 64px;
}

.project-detail-footer__total {
  display: inline-flex;
  align-items: baseline;
  gap: var(--space-sm);
  min-width: 0;
}
.project-detail-footer__total-label {
  font-size: 0.875rem;
  font-weight: 500;
  color: var(--color-text-secondary);
}
.project-detail-footer__total-amount {
  font-size: 1.125rem;
  font-weight: 700;
  color: var(--color-text-primary);
  font-variant-numeric: tabular-nums;
}
.project-detail-footer__total--empty {
  font-size: 0.875rem;
  font-weight: 500;
  color: var(--color-text-muted);
}

.project-detail-footer__actions {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  flex-wrap: wrap;
  flex-shrink: 0;
}

@media (max-width: 767px) {
  .project-detail-footer {
    flex-direction: column;
    align-items: stretch;
    gap: var(--space-sm);
    padding: var(--space-md) var(--space-lg);
  }
  .project-detail-footer__total { justify-content: center; }
  .project-detail-footer__actions { width: 100%; justify-content: center; }
  .project-detail-content--has-footer { padding-bottom: 160px; }
}

/* ----- Estimates container (same shell as .projects-container) ----- */
.estimates-container {
  background: var(--color-surface);
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
  padding: var(--space-xl);
}

.estimates-container__title {
  font-size: 1rem;
  font-weight: 600;
  color: var(--color-text-primary);
  margin-bottom: var(--space-lg);
}

/* ----- Toolbar (search + sort) — namespaced parallel of .projects-toolbar ----- */
.estimates-toolbar {
  display: flex;
  align-items: center;
  gap: var(--space-lg);
}

.estimates-toolbar__search { flex: 1 1 60%; min-width: 0; }
.estimates-toolbar__sort   { flex: 0 0 auto; }

.estimates-search { position: relative; display: block; }

.estimates-search__icon {
  position: absolute;
  left: var(--space-lg);
  top: 50%;
  transform: translateY(-50%);
  color: var(--color-text-muted);
  pointer-events: none;
  display: flex;
}

.estimates-search__input {
  width: 100%;
  height: 40px;
  padding: 0 var(--space-lg) 0 var(--space-3xl);
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius-sm);
  font-size: 0.9375rem;
  font-family: var(--font-sans);
  color: var(--color-text-primary);
  background: var(--color-bg);
  transition: border-color 0.15s ease-out, box-shadow 0.15s ease-out;
}

.estimates-search__input:focus {
  outline: none;
  border-color: var(--color-text-primary);
  box-shadow: 0 0 0 3px rgba(17, 17, 17, 0.10);
}

.estimates-search__input::placeholder { color: var(--color-text-muted); }

.estimates-sort {
  height: 40px;
  padding: 0 var(--space-3xl) 0 var(--space-lg);
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius-sm);
  font-size: 0.9375rem;
  font-family: var(--font-sans);
  color: var(--color-text-primary);
  background: var(--color-bg);
  cursor: pointer;
  appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23888888' d='M6 8L1 3h10z'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right var(--space-lg) center;
  transition: border-color 0.15s ease-out, box-shadow 0.15s ease-out;
}

.estimates-sort:focus {
  outline: none;
  border-color: var(--color-text-primary);
  box-shadow: 0 0 0 3px rgba(17, 17, 17, 0.10);
}

.estimates-divider {
  height: 1px;
  background: var(--color-border-subtle);
  margin: var(--space-lg) 0;
  border: 0;
}

/* ----- Estimates list ----- */
.estimates-list { display: flex; flex-direction: column; }

.estimate-row {
  position: relative;
  border-bottom: 1px solid var(--color-border-subtle);
  transition: background-color 0.1s ease-out;
}
.estimate-row:last-child { border-bottom: 0; }

.estimate-row:hover { background: var(--color-surface-alt); }

/* Bundle A #11 (2026-05-28) — body-link is flex-ROW with the cover
   thumbnail on the LEFT (fixed 120×90) and the text wrapper on the
   right (flex:1 1 0 + min-width:0 so the filename ellipsis-truncates
   instead of pushing the thumb off-axis). Kebab stays absolute
   top-right via .estimate-row's position:relative — unaffected by
   this layout change. */
.estimate-row__body-link {
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: var(--space-md);
  padding: var(--space-md) var(--space-lg);
  padding-right: var(--space-3xl);   /* room for kebab corner */
  text-decoration: none;
  color: var(--color-text-primary);
}
.estimate-row__body-link:hover { text-decoration: none; }

/* Text wrapper — holds the existing name/meta/footer as a flex-column
   so they keep their 2px tight stacking even though the parent is
   now flex-row. min-width:0 is the critical bit for filename ellipsis
   to work inside a flex item (default min-width:auto would block it). */
.estimate-row__text {
  display: flex;
  flex-direction: column;
  gap: 2px;
  flex: 1 1 0;
  min-width: 0;
}

/* PDF cover thumbnail (page 0). Fixed 4:3 landscape slot, 120×90px.
   Cover JPEG generated by PDFProcessor at upload time; the route
   resolves duplicate rows to the original sibling's thumb URL.
   object-fit:contain letterboxes portrait covers cleanly inside the
   landscape box (vertical bars top/bottom) and lets landscape covers
   fill closer to edge — never stretched, never forced. Subtle border
   + surface-alt background lets the letterbox bars read as
   intentional rather than broken. */
.estimate-row__cover-thumb {
  flex:            0 0 120px;
  width:           120px;
  height:          90px;
  border:          1px solid var(--color-border-subtle);
  border-radius:   var(--radius-sm);
  background:      var(--color-surface-alt);
  overflow:        hidden;
  display:         flex;
  align-items:     center;
  justify-content: center;
}
.estimate-row__cover-thumb-img {
  width:      100%;
  height:     100%;
  object-fit: contain;
  display:    block;
}
.estimate-row__cover-thumb-placeholder {
  color:       var(--color-text-muted);
  line-height: 0;
  opacity:     0.7;
}
.estimate-row__cover-thumb-placeholder svg { display: block; }

/* Line 1 — status dot + filename */
.estimate-row__name {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  font-size: 1rem;
  font-weight: 600;
  color: var(--color-text-primary);
  min-width: 0;
}

.estimate-row__name-text {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.estimate-status-dot {
  flex-shrink: 0;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--color-text-muted);
}
.estimate-status-dot--done       { background: var(--color-success); }
.estimate-status-dot--analyzing  { background: var(--color-info); }
/* Pending bumped muted->warning (2026-06-10) so the dot agrees with the
   amber status label below. */
.estimate-status-dot--pending    { background: var(--color-warning); }
.estimate-status-dot--cancelled  { background: var(--color-text-muted); }
.estimate-status-dot--error      { background: var(--color-error); }

/* Semantic estimate-status LABEL colors (2026-06-10) — the status word in the
   project-detail estimate list. Sober colored text (house deep-foreground
   tokens), matching the dots; the dollar total stays secondary gray (it sits
   OUTSIDE this span). Mirrors the dashboard's .estimate-status--* scheme. */
.estimate-status-text { font-weight: 600; }
.estimate-status-text--done       { color: var(--color-success); }
.estimate-status-text--analyzing  { color: var(--color-info); }
.estimate-status-text--pending    { color: var(--color-warning); }
.estimate-status-text--cancelled  { color: var(--color-text-muted); }
.estimate-status-text--error      { color: var(--color-error); }

/* Line 2 — status text + total amount */
.estimate-row__meta {
  font-size: 0.875rem;
  font-weight: 500;
  color: var(--color-text-secondary);
  margin-left: calc(8px + var(--space-md));   /* align with name text */
}

/* Line 3 — items count · updated time · creator, all flush-left.
   Roles "created by": a third segment (creator) joined the original
   two, so the old justify-content:space-between (a 2-end split) spread
   them into left/center/right. Switched to a flush-left flowing line
   with `·` separators + column-gap, matching .projects-grid-card__stats
   so the metadata reads consistently across cards. flex-wrap lets it
   drop cleanly on narrow/mobile viewports instead of overflowing. */
.estimate-row__footer {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  column-gap: var(--space-xs);
  font-size: 0.75rem;
  color: var(--color-text-muted);
  letter-spacing: 0.01em;
  margin-left: calc(8px + var(--space-md));
  margin-top: 2px;
}

/* ----- Kebab corner on estimate rows -----
   Matches the Step 6 .projects-grid-card__kebab treatment for
   consistency: black-by-default, light-gray circular hover halo,
   darker active state. See that block above for rationale. */
.estimate-row__kebab {
  position: absolute;
  top: var(--space-md);
  right: var(--space-sm);
  width: 28px;
  height: 28px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: 0;
  border-radius: 50%;
  color: var(--color-text-primary);
  cursor: pointer;
  opacity: 1;
  transition: opacity 0.3s ease, background-color 0.15s ease-out;
  z-index: 2;
}

.estimate-row__kebab:hover {
  background: var(--color-border-subtle);   /* #F0F0F0 */
}

.estimate-row__kebab:active {
  background: var(--color-border-default);  /* #E5E5E5 */
}

.estimate-row__kebab:focus-visible {
  outline: 2px solid var(--color-text-primary);
  outline-offset: 1px;
}

/* Desktop hover-reveal. Tablet/mobile keep kebab visible (no hover). */
@media (min-width: 1024px) {
  .estimate-row__kebab { opacity: 0; }
  .estimate-row:hover .estimate-row__kebab,
  .estimate-row__kebab:focus-visible,
  .estimate-row__kebab[aria-expanded="true"] { opacity: 1; }
}

.estimate-row__dropdown {
  position: absolute;
  top: calc(var(--space-md) + 28px + 4px);
  right: var(--space-sm);
  min-width: 160px;
  background: var(--color-bg);
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-md);
  padding: var(--space-xs);
  z-index: 60;   /* above the z-50 fixed .project-detail-footer (was 10 — the
                    downward dropdown's lower items got covered by the footer
                    on bottom rows); matches .more-actions__menu's z-60 */
  display: none;
}

.estimate-row__dropdown.is-open { display: block; }

.estimates-kebab-item {
  display: block;
  width: 100%;
  padding: var(--space-sm) var(--space-md);
  background: transparent;
  border: 0;
  border-radius: 4px;
  font-size: 0.875rem;
  font-weight: 500;
  font-family: var(--font-sans);
  color: var(--color-text-primary);
  text-align: left;
  text-decoration: none;
  cursor: pointer;
  transition: background-color 0.1s ease;
}

.estimates-kebab-item:hover {
  background: var(--color-surface-alt);
  text-decoration: none;
  color: var(--color-text-primary);
}

.estimates-kebab-item:focus-visible {
  outline: 2px solid var(--color-text-primary);
  outline-offset: -2px;
}

.estimates-kebab-item--danger { color: var(--color-error); }
.estimates-kebab-item--danger:hover {
  background: var(--color-error-bg);
  color: var(--color-error);
}

.estimates-kebab-divider {
  height: 1px;
  background: var(--color-border-subtle);
  margin: 2px 0;
  border: 0;
}

/* Hide the list when search filters all rows out; the no-results
   panel toggles visible in its place. */
.estimates-list.is-hidden { display: none; }

/* ----- Empty + no-results states (inside container) ----- */
.estimates-empty {
  display: flex;
  flex-direction: column;
  gap: var(--space-lg);
}

.estimates-empty__helper {
  font-size: 0.875rem;
  color: var(--color-text-muted);
  max-width: 560px;
  line-height: 1.55;
}

.estimates-no-results {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  padding: var(--space-3xl) var(--space-lg);
  gap: var(--space-lg);
}

.estimates-no-results__message {
  font-size: 1rem;
  color: var(--color-text-secondary);
}

.estimates-no-results__message strong {
  color: var(--color-text-primary);
  font-weight: 600;
}

/* ----- Upload PDF modal (has-estimates state) ----- */
.upload-pdf-dialog {
  border: 0;
  padding: 0;
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
  background: var(--color-bg);
  max-width: 560px;
  width: 92%;
}

.upload-pdf-dialog::backdrop {
  background: rgba(17, 17, 17, 0.45);
}

.upload-pdf-dialog__inner { padding: var(--space-2xl); }

.upload-pdf-dialog__title {
  font-size: 1.25rem;
  font-weight: 700;
  color: var(--color-text-primary);
  margin-bottom: var(--space-lg);
}

.upload-pdf-dialog__actions {
  margin-top: var(--space-lg);
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--space-md);
}

/* ----- Responsive ----- */
@media (max-width: 767px) {
  .estimates-toolbar {
    flex-direction: column;
    align-items: stretch;
    gap: var(--space-md);
  }
  .estimates-toolbar__search,
  .estimates-toolbar__sort { flex: 0 0 auto; width: 100%; }
  .estimates-sort { width: 100%; }

  .estimates-container { padding: var(--space-lg); }

  /* Step 7 retrofit — project-detail-header__actions stack rule
     deleted; the actions row migrated to .project-detail-footer
     which has its own mobile stack handling. */
}

/* =========================================================
   VDP STEP 8 — ESTIMATE PREVIEW (2026-05-15)

   Page header sits outside two stacked container cards (Pages,
   Price list). A sticky bottom bar holds the live page-selection
   counter + Analyze button. The Analyze button lives OUTSIDE the
   form and submits it via the HTML form="previewForm" attribute,
   so Enter on the select still submits naturally.

   Thumbnails render at the largest practical size for each
   breakpoint (4 / 3 / 2 / 1 columns) so the user can verify the
   PDF content before sending to analysis. Selected state uses a
   2px black border + subtle surface tint — restrained, no coral
   (coral is reserved for sparing brand moments per Step 1 spec).
   ========================================================= */

/* ----- Page header ----- */
.preview-header { margin-bottom: var(--space-2xl); }

.preview-header__subtitle {
  font-size: 0.875rem;
  color: var(--color-text-muted);
  margin-top: var(--space-xs);
  letter-spacing: 0.01em;
}

.preview-header__subtitle-sep {
  margin: 0 var(--space-sm);
  color: var(--color-border-strong);
}

/* ----- Container cards (Pages, Price list) ----- */
.preview-card {
  background: var(--color-surface);
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
  padding: var(--space-xl);
}

.preview-card + .preview-card { margin-top: var(--space-lg); }

.preview-card__title {
  font-size: 1rem;
  font-weight: 600;
  color: var(--color-text-primary);
}

.preview-card__divider {
  height: 1px;
  background: var(--color-border-subtle);
  margin: var(--space-lg) 0;
  border: 0;
}

/* ----- Pages card toolbar (Select all / Deselect all) ----- */
.preview-pages__toolbar {
  display: flex;
  gap: var(--space-sm);
  margin-bottom: var(--space-lg);
  flex-wrap: wrap;
}

/* ----- Thumbnail grid -----
   align-items: start so each card takes its intrinsic height and
   stays top-aligned within its row. With mixed portrait/landscape
   PDFs this leaves any gap BETWEEN rows rather than inside cards
   (which is the honest asymmetric look the user picked over a
   forced-uniform aspect ratio). */
.preview-thumb-grid {
  display: grid;
  gap: var(--space-lg);
  grid-template-columns: 1fr;
  align-items: start;
}
@media (min-width: 768px)  { .preview-thumb-grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1024px) { .preview-thumb-grid { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 1440px) { .preview-thumb-grid { grid-template-columns: repeat(4, 1fr); } }

/* ----- Thumbnail card ----- */
.preview-thumb {
  position: relative;
  background: var(--color-bg);
  border: 2px solid transparent;
  border-radius: var(--radius);
  cursor: pointer;
  user-select: none;
  transition:
    transform 0.15s ease-out,
    box-shadow 0.15s ease-out,
    border-color 0.15s ease-out,
    background-color 0.15s ease-out;
  overflow: hidden;
}

.preview-thumb:hover {
  transform: translateY(-2px);
  box-shadow: var(--shadow-md);
}

.preview-thumb:hover:not(.preview-thumb--selected) {
  border-color: var(--color-border-strong);
}

/* Hide the actual checkbox; the visible square is a sibling span. */
.preview-thumb__input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
  width: 0;
  height: 0;
}

/* Visible checkbox-style indicator at top-left of the card. */
.preview-thumb__check {
  position: absolute;
  top: var(--space-sm);
  left: var(--space-sm);
  width: 24px;
  height: 24px;
  border-radius: 4px;
  border: 1.5px solid var(--color-border-strong);
  background: var(--color-bg);
  display: flex;
  align-items: center;
  justify-content: center;
  color: transparent;
  z-index: 2;
  transition: background-color 0.15s ease-out, border-color 0.15s ease-out, color 0.15s ease-out;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
}

.preview-thumb__check svg { display: block; }

/* Selected state — applied to the card. */
.preview-thumb--selected {
  border-color: var(--color-text-primary);
  background: var(--color-surface-alt);
}

.preview-thumb--selected .preview-thumb__check {
  background: var(--color-text-primary);
  border-color: var(--color-text-primary);
  color: var(--color-text-inverse);
}

/* Image frame — orientation-aware (Step 8 V2 fix).
   PyMuPDF's pixmap respects the page's native orientation, so each
   thumbnail JPEG already has the right aspect ratio. The frame
   defers entirely to the image's intrinsic dimensions: landscape
   pages render landscape, portrait portrait, asymmetric grids OK.
   The grid uses a single fixed column width per breakpoint and
   each cell expands vertically to fit its image — natural and
   honest. */
.preview-thumb__image-frame {
  background: #F4F4F4;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}

.preview-thumb__image {
  width: 100%;
  height: auto;          /* intrinsic — page orientation preserved */
  display: block;
  transition: opacity 0.15s ease-out;
}

/* Deselected fade applies to the IMAGE only — keep the card chrome
   crisp so the user can still read the page label and reconsider. */
.preview-thumb:not(.preview-thumb--selected) .preview-thumb__image { opacity: 0.55; }

/* Placeholder fallback (no thumbnail URL available). Without an
   intrinsic image to set height, give the frame an explicit
   portrait-ish min-height so the card isn't a flat strip. */
.preview-thumb__placeholder {
  font-size: 2rem;
  color: var(--color-text-muted);
  padding: 3rem 0;
}

.preview-thumb__label {
  padding: var(--space-sm) var(--space-md);
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--color-text-secondary);
  text-align: center;
  border-top: 1px solid var(--color-border-subtle);
}

.preview-thumb--selected .preview-thumb__label {
  color: var(--color-text-primary);
}

/* ----- Inline error states inside cards ----- */
.preview-empty-pages,
.preview-empty-pricelist {
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
  padding: var(--space-lg);
  border-radius: var(--radius-sm);
  background: var(--color-warning-bg);
  border: 1px solid var(--color-warning);
  border-left-width: 3px;
}

.preview-empty-pages__title,
.preview-empty-pricelist__title {
  font-size: 1rem;
  font-weight: 600;
  color: var(--color-text-primary);
}

.preview-empty-pages__body,
.preview-empty-pricelist__body {
  font-size: 0.875rem;
  line-height: 1.55;
  color: var(--color-text-secondary);
  max-width: 560px;
}

.preview-empty-pages__action,
.preview-empty-pricelist__action {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  align-self: flex-start;
}

/* ----- Price-list select wrapper ----- */
.preview-pricelist__field {
  max-width: 480px;
}

.preview-pricelist__field select {
  width: 100%;
  height: var(--height-input);
  padding: 0 var(--space-3xl) 0 var(--space-lg);
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius-sm);
  font-size: 1rem;
  font-family: var(--font-sans);
  color: var(--color-text-primary);
  background: var(--color-bg);
  cursor: pointer;
  appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23888888' d='M6 8L1 3h10z'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right var(--space-lg) center;
  transition: border-color 0.15s ease-out, box-shadow 0.15s ease-out;
}

.preview-pricelist__field select:focus {
  outline: none;
  border-color: var(--color-text-primary);
  box-shadow: 0 0 0 3px rgba(17, 17, 17, 0.10);
}

/* ----- Sticky footer ----- */
.preview-footer {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 90;
  background: var(--color-bg);
  border-top: 1px solid var(--color-border-default);
  box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.06);
  padding: var(--space-md) var(--space-2xl);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-lg);
  min-height: 64px;
}

/* Cancel + Analyze grouped on the right; counter stays left (footer is
   space-between, so this keeps it two children). */
.preview-footer__actions {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
}

.preview-footer__counter {
  display: inline-flex;
  align-items: center;
  gap: var(--space-sm);
  font-size: 0.875rem;
  font-weight: 500;
  color: var(--color-text-secondary);
}

.preview-footer__counter-icon { display: inline-flex; }

/* Counter color states — JS sets data-state on the counter span. */
.preview-footer__counter[data-state="none"]    { color: var(--color-warning); }
.preview-footer__counter[data-state="partial"] { color: var(--color-text-secondary); }
.preview-footer__counter[data-state="all"]     { color: var(--color-text-primary); }

/* Push the page content above the sticky footer so the last
   thumbnail row isn't hidden when scrolled to the bottom. */
.preview-content--has-footer { padding-bottom: 100px; }

/* ----- Responsive ----- */
@media (max-width: 767px) {
  .preview-card { padding: var(--space-lg); }

  .preview-footer {
    flex-direction: column;
    align-items: stretch;
    gap: var(--space-sm);
    padding: var(--space-md) var(--space-lg);
  }
  .preview-footer__counter { justify-content: center; }
  .preview-footer .btn { width: 100%; }

  .preview-content--has-footer { padding-bottom: 140px; }
}

/* =========================================================
   VDP STEP 9 — ESTIMATE ANALYZING (2026-05-15)

   Visual migration only. All SSE/JS contracts preserved:
   - #progress-fill width updated by JS (SSE progress)
   - #progress-label textContent replaced by JS
   - #analysis-hint textContent replaced by JS on cancel/stuck
     (initially empty after Step 9 — the misleading "leave the
     page" copy was removed; element retained so JS can still
     write Cancel/Stuck messages)
   - #cancel-analysis-btn click handler unchanged
   - #reset-analysis-row.hidden flipped false by JS on stuck
   - #page-row-N → .page-status__dot states (--waiting / --done /
     --sparse / --error) class swaps by JS
   - #page-result-N textContent set by JS

   Layout: page header outside the card, container card centered
   max-width 720px. No sticky footer (transitional page; Cancel
   lives inside the card with Reset alongside when revealed).
   ========================================================= */

.analyzing-header {
  text-align: center;
  margin: var(--space-2xl) auto 0;
  max-width: 720px;
  padding: 0 var(--space-lg);
}

.analyzing-header__breadcrumb {
  display: inline-block;
  text-align: left;
  margin-bottom: var(--space-md);
}

.analyzing-header__subtitle {
  font-size: 0.875rem;
  color: var(--color-text-muted);
  margin-top: var(--space-xs);
  letter-spacing: 0.01em;
}

.analyzing-header__subtitle-sep {
  margin: 0 var(--space-sm);
  color: var(--color-border-strong);
}

.analyzing-card {
  background: var(--color-surface);
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
  padding: var(--space-2xl);
  margin: var(--space-xl) auto var(--space-2xl);
  max-width: 720px;
  text-align: left;
}

/* M1 (2026-05-18, Regression Sprint): .analyzing-status wrapper
   removed — the spinner now lives in the same row as the progress
   bar + percent, and the status label moved to the sticky footer. */

/* Progress bar row: spinner + bar + percentage number */
.analyzing-progress {
  display: flex;
  align-items: center;
  gap: var(--space-md);
}
.analyzing-progress .progress-bar { flex: 1 1 auto; }

.analyzing-progress__percent {
  font-size: 0.875rem;
  font-weight: 500;
  color: var(--color-text-secondary);
  min-width: 3.5em;
  text-align: right;
  font-variant-numeric: tabular-nums;   /* prevent digit-width jitter */
}

/* Spinner — CSS-only rotation. Hidden via the [hidden] attribute
   set by JS on terminal states (cancelled, error, done). */
.analyzing-spinner {
  display: inline-flex;
  width: 18px;
  height: 18px;
  color: var(--color-text-primary);
  animation: analyzing-spin 1s linear infinite;
  flex-shrink: 0;
}
.analyzing-spinner svg { display: block; }

@keyframes analyzing-spin {
  to { transform: rotate(360deg); }
}

/* Card-internal dividers */
.analyzing-card__divider {
  height: 1px;
  background: var(--color-border-subtle);
  margin: var(--space-lg) 0;
  border: 0;
}

/* M1: .analyzing-hint + .analyzing-actions blocks removed. The
   #analysis-hint element was deleted (its messages consolidate
   into #progress-label in the sticky footer). The actions row
   migrated to the sticky footer (.analyzing-footer__actions). */

/* ----- M1 sticky footer (2026-05-18, Regression Sprint) -----
   Mirrors .estimate-footer / .price-list-footer / .project-detail-
   footer recipes. Same fixed-bottom + white-bg + upward-shadow
   pattern. No Copilot-shrink rule (Copilot doesn't mount on the
   analyzing page). */
.analyzing-content--has-footer { padding-bottom: 100px; }

.analyzing-footer {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 50;
  background: var(--color-bg);
  border-top: 1px solid var(--color-border-default);
  box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.06);
  padding: var(--space-md) var(--space-2xl);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-lg);
  min-height: 64px;
}

/* Footer status (left) — formerly the inline #progress-label.
   Inherits the Step 9 .progress-label font-size + weight rule.
   The min-height keeps the footer height stable when the text
   is empty mid-transition. */
.analyzing-footer__status {
  flex: 1 1 auto;
  min-width: 0;
  min-height: 1.4em;
  overflow-wrap: anywhere;   /* "Error: <long message>" wraps cleanly */
}

.analyzing-footer__actions {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  flex-wrap: wrap;
  flex-shrink: 0;
}

@media (max-width: 767px) {
  .analyzing-card { padding: var(--space-lg); }

  .analyzing-footer {
    flex-direction: column;
    align-items: stretch;
    gap: var(--space-sm);
    padding: var(--space-md) var(--space-lg);
  }
  .analyzing-footer__status { text-align: center; }
  .analyzing-footer__actions { width: 100%; justify-content: center; }
  .analyzing-footer__actions .btn { flex: 1 1 0; min-width: 0; }
  .analyzing-content--has-footer { padding-bottom: 160px; }
}

/* =========================================================
   VDP STEP 10 — ESTIMATE DETAIL (2026-05-17)

   Highest-traffic page. Header outside containers carries the
   breadcrumb, name, status pill, subtitle, and timestamps row.
   Items region preserves the .panel + .data-table shape because
   copilot_dom_refresh.js re-renders it after Copilot mutations —
   any class rename there breaks auto-DOM-refresh. Same for the
   .summary-cards shape inside #estimate-summary-region.

   Sticky footer carries the always-visible total + Refresh /
   Download / More actions ▾. Footer shrinks (right: 480px) when
   the Copilot panel is open via body.copilot-open. Tax disclaimer
   sits between the summary cards and the footer; clicking it
   opens an inline 2-option dropdown that POSTs to
   /estimates/<id>/tax-disclaimer and updates the visible text
   without a page reload.

   Copilot affordance state machine (driven from copilot.js by
   toggling body classes):
     - no class:        launcher visible (never-opened)
     - copilot-was-open: tab visible
     - copilot-open:    both hidden
   On mobile (<768px) the floating tab is suppressed and the
   bottom-right launcher serves the re-open role.
   ========================================================= */

/* ----- Page header ----- */
.estimate-detail-header {
  margin-bottom: var(--space-2xl);
}

/* Batch 6 Group 3 (D) — timestamps + Duplicate button share one row, matching
   PL: button right-aligned on the Created/Updated line (no extra row below). */
.estimate-detail-header__timestamps-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  flex-wrap: wrap;
  margin-top: var(--space-xs);
}
.estimate-detail-header__actions {
  display: inline-flex;
  align-items: center;
  gap: var(--space-sm);
  flex-shrink: 0;
}

.estimate-detail-header__title-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-lg);
  margin-top: var(--space-xs);
}

.estimate-detail-header__title-row h1 {
  margin: 0;
  flex: 1 1 auto;
  min-width: 0;
  /* Filename can be long — wrap rather than overflow. */
  overflow-wrap: anywhere;
}

.estimate-detail-header__subtitle {
  font-size: 0.875rem;
  color: var(--color-text-muted);
  margin-top: var(--space-md);
  letter-spacing: 0.01em;
}

.estimate-detail-header__subtitle-sep {
  margin: 0 var(--space-sm);
  color: var(--color-border-strong);
}

.estimate-detail-header__timestamps {
  font-size: 0.75rem;
  color: var(--color-text-muted);
  margin: 0;  /* Group 3 (D): spacing moved to .estimate-detail-header__timestamps-row */
  letter-spacing: 0.01em;
}

/* ----- Status pill ----- */
.estimate-status-badge {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  padding: 2px var(--space-md);
  border-radius: var(--radius-pill);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  white-space: nowrap;
  flex-shrink: 0;
}

.estimate-status-badge::before {
  content: '';
  display: inline-block;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
}

.estimate-status-badge--done {
  background: var(--color-success-bg);
  color: var(--color-success);
}
.estimate-status-badge--analyzing {
  background: var(--color-info-bg);
  color: var(--color-info);
}
.estimate-status-badge--pending,
.estimate-status-badge--cancelled {
  background: var(--color-surface-alt);
  color: var(--color-text-muted);
}
.estimate-status-badge--error {
  background: var(--color-error-bg);
  color: var(--color-error);
}
/* Batch 6 Group 2 — yellow "trial/limited" badge variant (reuses the badge
   recipe; only the colorway is new). Used for trial/freemium plan state. */
.estimate-status-badge--trial {
  background: var(--color-warning-bg);
  color: var(--color-warning);
}
/* Batch 6 Group 3 — blue "workspace context" variant. text-transform:none so
   the workspace proper name keeps its typed case (e.g. "DrawIQ", not "DRAWIQ"). */
.estimate-status-badge--workspace {
  background: var(--color-info-bg);
  color: var(--color-info);
  text-transform: none;
}

/* Settings/Account meta rows — role/plan pills + workspace/invited-by, aligned. */
.account-meta {
  margin-top: var(--space-lg);
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}
.account-meta__row {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
}
.account-meta__label {
  min-width: 92px;
  color: var(--color-text-muted);
  font-size: 0.875rem;
}
.account-meta__value {
  font-weight: 600;
  color: var(--color-text-primary);
}

/* Batch 6 Group 3 (#4b) — unsaved-changes cue + dirty Save emphasis. */
.form-dirty-cue {
  color: var(--color-warning);
  font-size: 0.85rem;
  font-weight: 500;
  margin-left: var(--space-md);
}
.btn.is-dirty {
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-warning), transparent 80%);
}

/* Batch 6 Group 3 (#9) — clear-staged-logo X (Preferences + Branding slot). */
.settings-logo-clear {
  border: none;
  background: none;
  cursor: pointer;
  color: var(--color-error);
  font-size: 1.25rem;
  line-height: 1;
  padding: 0 6px;
  border-radius: 4px;
}
.settings-logo-clear:hover { background: var(--color-error-bg); }

/* Anchor variant — used by Settings > Usage's plan-name badge
   (clickable → /settings/subscription). Suppress the global anchor
   underline + add subtle hover dim. Color is inherited from the
   colorway modifier (--done / --analyzing / etc.). */
a.estimate-status-badge {
  text-decoration: none;
  transition: filter 0.1s ease-out;
}
a.estimate-status-badge:hover {
  text-decoration: none;
  filter: brightness(0.95);
}

/* === TEAM (Roles Commit 4) === */
/* Role badge — pill mirroring .estimate-status-badge's geometry. Colorways:
   owner = solid dark accent (top role), member = info-blue, viewer = neutral
   grey. Uppercase, nowrap, flex-shrink:0 so it never collapses next to a
   long email. */
.role-badge {
  display: inline-flex;
  align-items: center;
  padding: 2px var(--space-md);
  border-radius: var(--radius-pill);
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  white-space: nowrap;
  flex-shrink: 0;
}
.role-badge--owner {
  background: var(--color-accent);
  color: var(--color-accent-text);
}
.role-badge--member {
  background: var(--color-info-bg);
  color: var(--color-info);
}
.role-badge--viewer {
  background: var(--color-surface-alt);
  color: var(--color-text-muted);
  border: 1px solid var(--color-border-default);
}

/* Members list — one row per person. Identity (name + email) takes the
   flexible left; meta (badge + status) is fixed-width on the right. The
   identity column has min-width:0 so the email ellipsis-truncates instead
   of pushing the badge off-screen — the load-bearing bit for ~375px mobile
   (no wide table, no horizontal scroll). */
.team-list {
  list-style: none;
  margin: 0;
  padding: 0;
  border-top: 1px solid var(--color-border-subtle);
}
.team-member {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  padding: var(--space-md) 0;
  border-bottom: 1px solid var(--color-border-subtle);
}
.team-member__identity {
  display: flex;
  flex-direction: column;
  gap: 1px;
  min-width: 0;            /* lets the email truncate inside the flex item */
}
.team-member__name {
  font-weight: 500;
  color: var(--color-text-primary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.team-member__email {
  font-size: 0.8125rem;
  color: var(--color-text-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.team-member__meta {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  flex-shrink: 0;          /* badge + status never get squeezed */
}
.team-member__status {
  font-size: 0.75rem;
  color: var(--color-text-muted);
}

/* Pending-invite rows carry extra actions (Resend / Revoke) on the meta
   side. Allow the row to wrap so on ~375px the action group drops below the
   email instead of overflowing; the identity keeps the flexible top line. */
.team-member--invite {
  flex-wrap: wrap;
  row-gap: var(--space-sm);
}
.team-member--invite .team-member__identity {
  flex: 1 1 12rem;
}
.team-member__meta--actions {
  flex-wrap: wrap;
  gap: var(--space-sm);
}

/* Active-member rows carry the role select + Deactivate. The row NEVER wraps:
   the identity shrinks and truncates (name + email both ellipsis) while the
   controls keep a fixed width on the right and are never pushed off-screen. */
.team-member--member {
  flex-wrap: nowrap;
}
.team-member--member .team-member__identity {
  flex: 1 1 auto;
  min-width: 0;   /* lets BOTH name and email ellipsis-truncate */
}
.team-role-form { display: inline; }
.team-role-select {
  font-size: 0.8125rem;
  padding: 4px var(--space-sm);
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius-sm, 6px);
  background: var(--color-surface, #fff);
  color: var(--color-text-primary);
}
.team-role-select:disabled {
  background: var(--color-surface-alt);
  color: var(--color-text-muted);
  cursor: not-allowed;
}

/* Mobile (≤640px): stack the role select ABOVE the Deactivate button on the
   right, both right-aligned, fixed width. The identity column (name + email,
   both truncating) takes the rest. Member rows only — pending/invite rows
   keep their own wrap behavior. Desktop is untouched (rule is gated below the
   breakpoint). */
@media (max-width: 640px) {
  .team-member--member .team-member__meta--actions {
    flex-direction: column;
    align-items: flex-end;
    flex-wrap: nowrap;
    gap: var(--space-xs);
  }
}

/* ----- Tax disclaimer + inline dropdown ----- */
.estimate-tax-disclaimer {
  position: relative;
  text-align: right;
  margin: var(--space-md) 0 0;
}

.estimate-tax-disclaimer__toggle {
  background: none;
  border: 0;
  padding: var(--space-xs) var(--space-sm);
  font-size: 0.8125rem;
  font-family: var(--font-sans);
  color: var(--color-text-muted);
  cursor: pointer;
  border-radius: var(--radius-sm);
  transition: color 0.1s ease-out, background-color 0.1s ease-out;
}
.estimate-tax-disclaimer__toggle:hover {
  color: var(--color-text-secondary);
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-thickness: 1px;
  text-decoration-color: var(--color-border-strong);
}
.estimate-tax-disclaimer__toggle:focus-visible {
  outline: 2px solid var(--color-text-primary);
  outline-offset: 2px;
}

.estimate-tax-dropdown {
  position: absolute;
  top: calc(100% + 4px);
  right: var(--space-sm);
  min-width: 200px;
  background: var(--color-bg);
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-md);
  padding: var(--space-xs);
  z-index: 30;
  display: none;
  text-align: left;
}
.estimate-tax-dropdown.is-open { display: block; }

.estimate-tax-dropdown__item {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  width: 100%;
  padding: var(--space-sm) var(--space-md);
  background: transparent;
  border: 0;
  border-radius: 4px;
  font-size: 0.875rem;
  font-weight: 500;
  font-family: var(--font-sans);
  color: var(--color-text-primary);
  text-align: left;
  cursor: pointer;
  transition: background-color 0.1s ease;
}
.estimate-tax-dropdown__item:hover { background: var(--color-surface-alt); }
.estimate-tax-dropdown__item:focus-visible {
  outline: 2px solid var(--color-text-primary);
  outline-offset: -2px;
}

.estimate-tax-dropdown__radio {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  border: 1.5px solid var(--color-border-strong);
  flex-shrink: 0;
}

.estimate-tax-dropdown__item[aria-checked="true"] .estimate-tax-dropdown__radio {
  border-color: var(--color-text-primary);
}
.estimate-tax-dropdown__item[aria-checked="true"] .estimate-tax-dropdown__radio::after {
  content: '';
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--color-text-primary);
}

/* ----- Sticky footer -----
   In-place token migration of .panel / .data-table / .summary-cards /
   .code-label / .row--unmatched lives at their original locations
   (the older PROGRESS BAR / SUMMARY CARDS sections higher up in this
   file). copilot_dom_refresh.js depends on those class names, so the
   migration is colors-only — same names, new tokens. ----- */
.estimate-footer {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 50;
  background: var(--color-bg);
  border-top: 1px solid var(--color-border-default);
  box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.06);
  padding: var(--space-md) var(--space-2xl);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-lg);
  min-height: 64px;
  transition: right 0.25s ease-out;
}

/* Footer shrinks to make room for the open Copilot panel (desktop).
   right MUST match .copilot-panel width (copilot.css) — flat 480px,
   no computed sync. */
body.copilot-open .estimate-footer { right: 480px; }

.estimate-footer__total {
  display: inline-flex;
  align-items: baseline;
  gap: var(--space-sm);
  min-width: 0;
}
.estimate-footer__total-label {
  font-size: 0.875rem;
  font-weight: 500;
  color: var(--color-text-secondary);
}
.estimate-footer__total-amount {
  font-size: 1.125rem;
  font-weight: 700;
  color: var(--color-text-primary);
  font-variant-numeric: tabular-nums;
}

/* === ESTIMATE VIEW MODES (estimate / neutral) ===
   The mode is STORED on the estimate (estimate.view_mode) and
   server-rendered onto data-view-mode — on the .estimate-view-scope
   wrapper (detail page, covers footer/modals/Copilot) and per-row on
   list/dashboard cards. So it's identical across every browser/device.
   Source of truth for what each mode shows is app/view_modes.py; this is
   its on-screen expression (the PDF reads the same flags server-side).
   Phase 2 adds COUNT rules (hide value columns + totals) under this same
   selector. */

/* Money symbol — a hideable span around the literal '$' so Neutral can
   drop the currency without re-rendering. (Phase 4's currency selector
   will fill this span from tenant config.) */
.money-sym { /* visible by default; no rule needed in Estimate mode */ }

/* Dual header/footer labels: show the money wording by default; the
   neutral wording is hidden until neutral/count. */
.vm-label--neutral { display: none; }

/* Neutral AND Count both de-monetize: drop the $, the tax notice, and
   swap price->value labels. (Count = strict superset; it adds the
   column/total hides below.) */
[data-view-mode="neutral"] .money-sym,
[data-view-mode="count"] .money-sym { display: none; }
[data-view-mode="neutral"] .estimate-tax-disclaimer,
[data-view-mode="count"] .estimate-tax-disclaimer { display: none; }
[data-view-mode="neutral"] .vm-label--money,
[data-view-mode="count"] .vm-label--money { display: none; }
[data-view-mode="neutral"] .vm-label--neutral,
[data-view-mode="count"] .vm-label--neutral { display: inline; }

/* COUNT only — hide the value columns + all totals, keeping the listing
   (Code/Description/Qty/Unit/Page) + items count. CSS hide only; the
   shape-locked #estimate-summary-region / #estimate-footer-total stay in
   the DOM so copilot_dom_refresh.js keeps patching them.
   Value cells key on the existing data-cell-field; value headers on the
   .vm-label spans they (uniquely) carry; <col> widths -> 0 so the freed
   space is absorbed by the no-width Description column (option A:
   Code+Page anchored, Description widens, no gap). */
[data-view-mode="count"] .data-table td[data-cell-field="unit_value"],
[data-view-mode="count"] .data-table td[data-cell-field="line_value"],
[data-view-mode="count"] .data-table thead th:has(.vm-label) { display: none; }
[data-view-mode="count"] .data-table > colgroup > col.col-unit-value,
[data-view-mode="count"] .data-table > colgroup > col.col-line-value { width: 0; }
[data-view-mode="count"] #estimate-summary-region,
[data-view-mode="count"] .panel__header-meta { display: none; }
/* The footer total readout is hidden with visibility (NOT display) so its
   space stays reserved — the footer is flex/space-between, and removing the
   element from flow would slide the action buttons left. visibility keeps
   the buttons exactly where they sit in estimate/neutral. */
[data-view-mode="count"] .estimate-footer__total { visibility: hidden; }

/* View-mode toggle — small segmented control in the detail header. */
/* Batch 6 — the view-modes "?" + toggle as one right-aligned cluster, so the
   "?" stays glued to the LEFT of the toggle on every screen (no stray dots). */
.view-mode-cluster {
  display: inline-flex;
  align-items: center;
  gap: var(--space-sm);
  margin-left: auto;
}
.view-mode-cluster .help-dot { margin-left: 0; }
.view-mode-cluster .view-mode-toggle { margin-left: 0; }

.view-mode-toggle {
  display: inline-flex;
  margin-left: auto;
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius-md);
  overflow: hidden;
}
.view-mode-toggle__btn {
  border: none;
  background: var(--color-surface);
  color: var(--color-text-secondary);
  font-size: 0.8125rem;
  font-weight: 600;
  padding: var(--space-xs) var(--space-md);
  cursor: pointer;
}
.view-mode-toggle__btn + .view-mode-toggle__btn {
  border-left: 1px solid var(--color-border-default);
}
.view-mode-toggle__btn.is-active {
  background: var(--color-accent);
  color: #fff;
}

.estimate-footer__actions {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  flex-wrap: wrap;
  flex-shrink: 0;
}

/* Push the page content above the sticky footer so the last
   summary card / tax disclaimer isn't hidden when scrolled. */
.estimate-detail-content--has-footer { padding-bottom: 100px; }

/* ----- Copilot floating tab (desktop reopen affordance) -----
   Hidden by default. Body class drives visibility:
     no class            → launcher (never-opened-this-session)
     copilot-open        → both hidden (panel up)
     copilot-was-open    → tab visible (hidden-after-open)
   Suppressed on mobile (<768px); launcher takes over there.

   Step 10 V2 (visual review fix): black bg + white sparkle icon
   instead of pale-bg + chevron. After the launcher disappears the
   tab is the ONLY remaining "Copilot exists" cue — a subtle pale
   chevron didn't carry that weight. The sparkle reuses the launcher
   icon for brand continuity; the black halo guarantees visibility
   against any page-content color. */
.copilot-floating-tab {
  display: none;
  position: fixed;
  right: 0;
  top: 50%;
  transform: translateY(-50%);
  width: 32px;
  height: 80px;
  align-items: center;
  justify-content: center;
  background: var(--color-text-primary);
  border: 1px solid var(--color-text-primary);
  border-right: 0;
  border-radius: var(--radius) 0 0 var(--radius);
  box-shadow: -2px 0 10px rgba(0, 0, 0, 0.18);
  color: var(--color-text-inverse);
  cursor: pointer;
  z-index: 40;
  transition: transform 0.15s ease-out, background-color 0.15s ease-out;
}
.copilot-floating-tab:hover {
  background: var(--color-accent-active);
  transform: translateY(-50%) translateX(-2px);
}
.copilot-floating-tab:focus-visible {
  outline: 2px solid var(--color-text-primary);
  outline-offset: 2px;
}
.copilot-floating-tab svg { display: block; }

@media (min-width: 768px) {
  body.copilot-was-open:not(.copilot-open) .copilot-floating-tab {
    display: inline-flex;
  }
}

/* Launcher visibility — hidden when panel is open OR has been
   opened-then-closed (the tab takes over). */
body.copilot-open .copilot-launcher,
body.copilot-was-open .copilot-launcher {
  display: none;
}

/* Launcher position — clear any sticky footer present on the page.
   Default bottom: 24px lives on the .copilot-launcher rule itself
   (copilot.css). Pages with a sticky footer push the launcher above
   it so it doesn't sit on top of the footer's right-side action
   buttons. PL-4 (2026-06-02) added .price-list-footer to the
   selector list — the PL detail page's sticky footer has the same
   shape as .estimate-footer (position:fixed bottom:0 min-height:64px)
   so the same offsets apply. :has() is supported in all modern
   browsers (Chrome 105+, Edge 105+, Safari 15.4+, Firefox 121+);
   older Firefox falls back to the default 24px which just covers
   the footer buttons — a visual nit, not a regression. */
body:has(.estimate-footer) .copilot-launcher,
body:has(.price-list-footer) .copilot-launcher {
  bottom: 88px;   /* 64px desktop footer + 24px gap */
}
@media (max-width: 767px) {
  body:has(.estimate-footer) .copilot-launcher,
  body:has(.price-list-footer) .copilot-launcher {
    bottom: 140px;  /* mobile footer stacks taller */
  }
}

/* Mobile: floating tab is suppressed; launcher always serves as
   the re-open affordance per Q4. Override the "was-open hides
   launcher" rule for <768px so the launcher comes back. */
@media (max-width: 767px) {
  body.copilot-was-open:not(.copilot-open) .copilot-launcher {
    display: inline-flex;
  }
  body.copilot-open .estimate-footer,
  body.copilot-open .price-list-footer { right: 0; }
}

/* ----- Responsive ----- */
@media (max-width: 767px) {
  .estimate-detail-header__title-row { flex-direction: column; align-items: flex-start; gap: var(--space-sm); }
  .estimate-footer {
    flex-direction: column;
    align-items: stretch;
    gap: var(--space-sm);
    padding: var(--space-md) var(--space-lg);
  }
  .estimate-footer__total { justify-content: center; }
  .estimate-footer__actions { width: 100%; justify-content: center; }
  .estimate-detail-content--has-footer { padding-bottom: 160px; }
}

/* =========================================================
   VDP STEP 11 — PRICE LISTS (2026-05-17)

   Index reuses Step 6 patterns directly:
     - .projects-container envelope
     - .projects-toolbar / __search / __sort
     - .projects-grid + .projects-grid-card BEM tree
     - .projects-empty / .projects-no-results
   The legacy .project-card classes on price_lists/index.html
   are retired in this commit; the index template migrates to
   .projects-grid-card. Backlog tracks the eventual rename to
   a generic .list-grid-card namespace (Step 14 cleanup).

   Detail introduces a parallel sticky-footer recipe mirroring
   Step 10's .estimate-footer, but WITHOUT the Copilot panel
   shrink rule (Copilot doesn't mount on price-list pages).
   The footer count carries id="price-list-footer-count" as
   forward-looking insurance for future Copilot price-list
   tools — the ID sits dormant today.
   ========================================================= */

/* ----- Detail page header (mirror Step 7 / Step 10) ----- */
.price-list-detail-header { margin-bottom: var(--space-2xl); }

.price-list-detail-header__title-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-lg);
  margin-top: var(--space-xs);
}

.price-list-detail-header__title-row h1 {
  margin: 0;
  flex: 1 1 auto;
  min-width: 0;
  overflow-wrap: anywhere;
}

.price-list-detail-header__description {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  text-overflow: ellipsis;
  margin-top: var(--space-md);
  font-size: 0.875rem;
  line-height: 1.55;
  color: var(--color-text-secondary);
  max-width: 720px;
}

/* Timestamps row: flex with optional "Set as default" button right.
   Step 11 V2 — promoted the prior subtle text link to a proper button
   colocated with timestamps so it's discoverable without eating a
   separate line of vertical space. */
.price-list-detail-header__timestamps-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  margin-top: var(--space-md);
  flex-wrap: wrap;
}

.price-list-detail-header__timestamps {
  font-size: 0.75rem;
  color: var(--color-text-muted);
  letter-spacing: 0.01em;
  margin: 0;
}

.price-list-detail-header__timestamps-sep {
  margin: 0 var(--space-sm);
  color: var(--color-border-strong);
}

.price-list-set-default-form { margin: 0; flex-shrink: 0; }

/* Duplicate Sprint (2026-05-22) — wraps Set-as-default + Duplicate
   in a single right-side cluster so they sit side-by-side without
   wrapping. */
.price-list-detail-header__actions {
  display: inline-flex;
  align-items: center;
  gap: var(--space-sm);
  flex-shrink: 0;
}

/* Bundle Estética/UX item 6 Layer 2 sub-step 2b-2 (2026-05-31):
   CSV import page grid. Desktop = 2-col side-by-side (Upload box +
   Expected format docs). Mobile = single column (upload box on top
   per source order, format docs below — Gus's intended mobile
   layout). Replaces the prior inline style="grid-template-
   columns:1fr 1fr" at price_lists/import.html:13 which had no
   @media override and left both panels squished side-by-side on
   narrow viewports. */
.csv-import-grid {
  display: grid;
  gap: 1.5rem;
  grid-template-columns: 1fr 1fr;
  max-width: 900px;
  align-items: start;
}
@media (max-width: 767px) {
  .csv-import-grid { grid-template-columns: 1fr; }
}

/* ----- Items container (parallel of .estimate-container) ----- */
.price-list-items-container {
  background: var(--color-surface);
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
  padding: var(--space-xl);
}

.price-list-items-container__title {
  font-size: 1rem;
  font-weight: 600;
  color: var(--color-text-primary);
  margin-bottom: var(--space-lg);
}

/* ----- Items toolbar (search + view toggles) ----- */
.price-list-items-toolbar {
  display: flex;
  align-items: center;
  gap: var(--space-lg);
  flex-wrap: wrap;
}

.price-list-items-toolbar__search {
  flex: 1 1 60%;
  min-width: 240px;
}

.price-list-items-toolbar__views {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  flex: 0 0 auto;
  flex-wrap: wrap;
}

/* The .pl-view-toolbar block above is the legacy container — leave it
   in place because price_list_view.js's wiring references its child
   button IDs, not its wrapper class. We render the buttons inline in
   .price-list-items-toolbar__views instead. */

.price-list-items-divider {
  height: 1px;
  background: var(--color-border-subtle);
  margin: var(--space-lg) 0;
  border: 0;
}

/* ----- Item action buttons (inline Edit + Delete per row) -----
   .data-table colors are already migrated (Step 10). These rules
   style the per-row Edit link and Delete button consistently with
   the system .btn-link / .btn-danger-link recipes. */
.price-list-item-actions {
  white-space: nowrap;
  text-align: right;
}

.price-list-item-actions > * + * { margin-left: var(--space-md); }

/* ----- Empty + no-results states (inside items container) ----- */
.price-list-items-empty,
.price-list-items-no-results {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  padding: var(--space-3xl) var(--space-lg);
  gap: var(--space-lg);
}

.price-list-items-empty__icon {
  color: var(--color-text-muted);
  display: flex;
  align-items: center;
  justify-content: center;
}

.price-list-items-empty__title {
  font-size: 1.25rem;
  font-weight: 700;
  color: var(--color-text-primary);
}

.price-list-items-empty__desc {
  font-size: 1rem;
  line-height: 1.55;
  color: var(--color-text-secondary);
  max-width: 460px;
}

.price-list-items-empty__actions {
  display: flex;
  gap: var(--space-md);
  flex-wrap: wrap;
  justify-content: center;
}

.price-list-items-no-results__message {
  font-size: 1rem;
  color: var(--color-text-secondary);
}
.price-list-items-no-results__message strong {
  color: var(--color-text-primary);
  font-weight: 600;
}

/* ----- Sticky footer ----- */
.price-list-footer {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 50;
  background: var(--color-bg);
  border-top: 1px solid var(--color-border-default);
  box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.06);
  padding: var(--space-md) var(--space-2xl);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-lg);
  min-height: 64px;
}

.price-list-footer__count {
  font-size: 1.125rem;
  font-weight: 700;
  color: var(--color-text-primary);
  font-variant-numeric: tabular-nums;
}

.price-list-footer__actions {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  flex-wrap: wrap;
  flex-shrink: 0;
}

/* Push the page content above the sticky footer. */
.price-list-detail-content--has-footer { padding-bottom: 100px; }

/* ----- Responsive ----- */
@media (max-width: 767px) {
  .price-list-items-container { padding: var(--space-lg); }

  .price-list-items-toolbar { flex-direction: column; align-items: stretch; }
  .price-list-items-toolbar__search,
  .price-list-items-toolbar__views { width: 100%; }
  .price-list-items-toolbar__views { justify-content: stretch; }
  .price-list-items-toolbar__views .pl-view-btn { flex: 1 1 0; }

  .price-list-detail-header__title-row {
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-sm);
  }

  .price-list-footer {
    flex-direction: column;
    align-items: stretch;
    gap: var(--space-sm);
    padding: var(--space-md) var(--space-lg);
  }
  .price-list-footer__count { text-align: center; }
  .price-list-footer__actions { width: 100%; justify-content: center; }
  /* Bundle Estética/UX item 6 Layer 2 sub-step 2b-3 (2026-05-31):
     `flex: 1 1 0; min-width: 0` was making each button shrinkable
     to zero width BEFORE the base rule's flex-wrap could trigger —
     so all 6 PL footer buttons (Generate PDF / Share / Export CSV
     / Import CSV / Edit / + Add item, for editors) crammed into
     one row at ~57px each, unreadable at 375px. Changing to
     `flex: 0 0 auto` lets buttons keep their natural content width
     and wrap onto multiple rows when the row genuinely can't fit
     them — base rule's flex-wrap: wrap + gap: var(--space-sm)
     (8px both axes) takes care of the row layout. Padding-bottom
     bumped 180→220 below to clear the now-taller (2-3 row)
     wrapping footer.
     NOTE on the 3 other sticky footers: estimate + project have
     no per-btn squish rule (their 3 buttons sit at natural width
     and fit one row at 375); analyzing keeps `flex: 1 1 0` because
     its 2 buttons distributing equally is the intentional visual
     (no squish problem with only 2 items). Asymmetry deliberate. */
  .price-list-footer__actions .btn { flex: 0 0 auto; }

  .price-list-detail-content--has-footer { padding-bottom: 220px; }

  /* Item tables — horizontal scroll on narrow viewports preserves
     all columns (Code/Type/Description/Price/Unit/actions). */
  .pl-view .panel { overflow-x: auto; }

  /* Bundle Estética/UX item 6 Layer 2 sub-step 2a (2026-05-31):
     Mirror the PL precedent above on the estimate items region.
     At narrow viewports the 8-col estimate items table (Code /
     Description / Qty / Unit / Unit price / Total / Page / row-
     actions) is wider than the mobile viewport; Layer 1's global
     overflow-x: hidden contains the document but clips the table
     content. Wrapping each .panel inside #estimate-items-region
     in overflow-x: auto lets the user horizontally scroll inside
     that panel to access all columns. Desktop unaffected (no
     overflow → no scrollbar appears). Scoped via
     #estimate-items-region so other .panel surfaces on estimate
     detail (e.g. summary region) aren't affected. */
  #estimate-items-region .panel { overflow-x: auto; }

  /* Bundle Estética/UX item 6 Layer 2 sub-step 2a fix (2026-05-31):
     Explicit min-width on the tables themselves so the panel's
     overflow-x: auto wrappers above have something to scroll. With
     table-layout: fixed + width: 100% (from Item 5), the table was
     compressing to fit the narrow viewport AND letting cells
     overlap rather than overflowing the panel — the overflow path
     existed but had nothing to overflow with. min-width forces the
     table to its full functional width regardless of viewport,
     ensuring overflow → scroll → usable columns. Works identically
     in read mode AND edit mode (min-width is mode-independent, so
     the .is-editing col-row-actions 0→75px expansion doesn't
     trigger different layout behavior between modes). The code column
     doubled (100->200 estimate / 120->220 PL), so each min-width gets
     the same +100 to keep Description from cramping. Estimate
     820px = 8 cols' worth (read 600 / edit 675 + ~145-220 desc).
     PL 700px = 5 cols' worth (read 390 / edit 465 + ~235-310
     desc). Desktop unaffected (above 767px breakpoint). */
  #estimate-items-region .data-table { min-width: 900px; }
  [data-editable-table-surface="price_list"] .data-table { min-width: 730px; }

  /* Bundle Estética/UX item 6 Layer 2 sub-step 2b-5 (2026-05-31):
     Comparator results table (3-col: Code/Type + A side + B side at
     18%/41%/41%) cramps at narrow viewports. Wrapped in
     .comparator-table-wrap (added to comparator/result.html); apply
     overflow-x + min-width here. */
  .comparator-table-wrap { overflow-x: auto; }
  .comparator-table { min-width: 600px; }

  /* Bundle Estética/UX item 6 Layer 2 sub-step 2b-5 (2026-05-31):
     Admin contact-messages table (6-col with mostly-fixed
     percentage widths). Same pattern. Wrapped in .admin-table-wrap. */
  .admin-table-wrap { overflow-x: auto; }
  .admin-table { min-width: 720px; }

  /* Bundle Estética/UX item 6 Layer 2 sub-step 2b-7 (2026-05-31):
     Settings logo preview (#settings-logo-preview) was an inline-
     styled horizontal flex row (image + label + "Remove logo"
     checkbox with margin-left:auto pushing it right). Overflowed at
     narrow viewports. Reflow to column-stack so image is on top,
     label below, Remove checkbox below — no horizontal overflow. */
  #settings-logo-preview {
    flex-direction: column;
    align-items: flex-start;
  }
  #settings-logo-preview > label {
    margin-left: 0;
  }
}

/* Bundle Estética/UX item 6 Layer 2 sub-step 2b-14 (2026-05-31):
   "Remove logo" affordance — icon + label chip with hidden checkbox
   inside. `label.` qualifier bumps specificity to (0,1,1) so it ties
   + beats `.field label` (0,1,1) via later source order.

   Idle state: light pill with muted text.
   Hover:     red-tinted pill (hint of destructive intent).
   Armed (input:checked): solid red pill with white text — telegraphs
                          "removal queued, will fire on Save".
   The native checkbox stays interactive (Tab-focusable, Space-toggles,
   submits with the form) but is visually hidden so only the styled
   chip reads as the affordance. `.settings-logo-remove input[type=
   "checkbox"]` (specificity 0,2,1) beats `.field input` (0,1,1)
   without needing !important. */
.field label.settings-logo-remove {
  display:         inline-flex;
  /* align-self: flex-start opts OUT of .field's parent flex stretch
     (the .field wrapper is display:flex; flex-direction:column with
     default align-items:stretch, which forces children to fill the
     full width regardless of their own display:inline-flex). Without
     this, the chip sprawls panel-wide instead of hugging icon+text. */
  align-self:      flex-start;
  align-items:     center;
  gap:             var(--space-xs);
  padding:         var(--space-xs) var(--space-sm);
  border:          1px solid var(--color-border-default);
  border-radius:   var(--radius-sm);
  background:      var(--color-bg);
  color:           var(--color-text-muted);
  font-size:       0.85rem;
  font-weight:     400;
  cursor:          pointer;
  margin:          0 0 var(--space-sm) 0;
  transition:      color 120ms ease, background 120ms ease, border-color 120ms ease;
}
.field label.settings-logo-remove:hover {
  color:        var(--color-error);
  border-color: var(--color-error);
  background:   var(--color-error-bg);
}
.field label.settings-logo-remove:focus-within {
  outline:        2px solid var(--color-text-primary);
  outline-offset: 2px;
}
/* Armed state — :has() is the cleanest hook (Chrome 105+, Safari 15.4+,
   Firefox 121+ — all covered for V1 user base). */
.field label.settings-logo-remove:has(input:checked) {
  color:        white;
  background:   var(--color-error);
  border-color: var(--color-error);
  font-weight:  500;
}
.settings-logo-remove__icon {
  display:     inline-flex;
  align-items: center;
  flex-shrink: 0;
  line-height: 0;
}
/* Hide the native checkbox visually but keep it accessible (Tab-focus,
   Space-toggle, form-submit). Specificity (0,2,1) beats `.field input`
   (0,1,1) cleanly — no !important needed. */
.settings-logo-remove input[type="checkbox"] {
  position:       absolute;
  width:          1px;
  height:         1px;
  padding:        0;
  margin:         -1px;
  border:         0;
  opacity:        0;
  pointer-events: none;
  clip:           rect(0 0 0 0);
}

/* Bundle Estética/UX item 6 Layer 2 sub-step 2b-14 (2026-05-31):
   --inline modifier — compact upload box for surfaces that don't want
   the full-column-width treatment. Used on Settings → Preferences
   where the box sits alone in the panel; without this it sprawled
   906px across the panel rather than reading as a compact action.
   Desktop only — at ≤767px the existing .logo-slot__upload @media
   rule (width: 100%) wins via later source order. max-width: 100%
   guarantees the box can never push past its container at any
   viewport, eliminating the page-widening regression risk Gus called
   out (no fixed px width — content sizes the box, container caps it). */
@media (min-width: 768px) {
  .logo-slot__upload--inline {
    display:    inline-flex;
    /* Opt out of .field's parent flex-stretch (same reason as
       .settings-logo-remove above) — without align-self:flex-start,
       inline-flex still gets stretched to fill the column width. */
    align-self: flex-start;
    width:      max-content;
    max-width:  100%;
  }
}

/* =========================================================
   ERROR PAGES — M3 from Regression Testing Sprint (2026-05-18)
   Custom 404 (and any future 5xx surfaces) extend layout/app.html
   and render this centered card-less treatment. Topbar shows
   per layout/app.html's existing auth/anon gating.
   ========================================================= */

.error-page {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  padding: var(--space-4xl) var(--space-lg);
  gap: var(--space-lg);
  max-width: 560px;
  margin: 0 auto;
}

.error-page__icon {
  color: var(--color-text-muted);
  display: flex;
  align-items: center;
  justify-content: center;
}

.error-page__title {
  font-size: 1.75rem;
  font-weight: 700;
  color: var(--color-text-primary);
  letter-spacing: -0.015em;
  line-height: 1.25;
  margin: 0;
}

.error-page__description {
  font-size: 1rem;
  line-height: 1.55;
  color: var(--color-text-secondary);
  margin: 0;
}

.error-page__actions {
  display: flex;
  gap: var(--space-md);
  flex-wrap: wrap;
  justify-content: center;
  margin-top: var(--space-md);
}

@media (max-width: 767px) {
  .error-page { padding: var(--space-3xl) var(--space-lg); }
  .error-page__actions { flex-direction: column; align-items: stretch; width: 100%; }
  .error-page__actions .btn { width: 100%; }
}


/* ===================================================================
   Reports section — horizontal pill tabs + empty state
   PDF Document Generation V1 modular Sprint, Phase C (2026-05-20).
   Sub-page navigation for /reports/headers, /reports/footers,
   /reports/notes — three peer entities, horizontal tabs across the
   top (parallel to but visually distinct from the settings sidebar).
   =================================================================== */

.reports-tabs {
  display:        flex;
  gap:            var(--space-xs);
  margin-bottom:  var(--space-lg);
  padding-bottom: 0;
  border-bottom:  1px solid var(--color-border-subtle);
}

.reports-tab {
  display:        inline-block;
  padding:        0.6rem var(--space-md);
  font-size:      0.9375rem;
  font-weight:    500;
  color:          var(--color-text-muted);
  text-decoration: none;
  border-bottom:  2px solid transparent;
  margin-bottom:  -1px; /* overlap the tabs container's border-bottom */
  transition:     color 120ms ease, border-color 120ms ease;
}

.reports-tab:hover {
  color:           var(--color-text-primary);
  text-decoration: none;
}

.reports-tab.is-active {
  color:                var(--color-text-primary);
  border-bottom-color:  var(--color-text-primary);
}

/* Empty state inside any /reports/* sub-page. Phase C is empty-only;
   Phase E will keep this as fallback when len(assets) == 0. */
.reports-empty {
  display:         flex;
  flex-direction:  column;
  align-items:     center;
  justify-content: center;
  text-align:      center;
  padding:         var(--space-2xl) var(--space-lg);
  gap:             var(--space-md);
}

.reports-empty__icon {
  color:         var(--color-text-muted);
  margin-bottom: var(--space-xs);
}

.reports-empty__title {
  margin:      0;
  font-size:   1.125rem;
  font-weight: 600;
  color:       var(--color-text-primary);
}

.reports-empty__desc {
  margin:      0;
  max-width:   46ch;
  font-size:   0.9375rem;
  line-height: 1.5;
  color:       var(--color-text-secondary);
}

.reports-empty__actions {
  margin-top: var(--space-sm);
  display:    flex;
  gap:        var(--space-sm);
}


/* ===================================================================
   Reports — Trix editor surface
   PDF Document Generation V1 modular Sprint, Phase E.3 (2026-05-20).
   Trix's default editor height (~6em) is unusably small. This
   override gives the editing surface comparable height to other
   major form fields. Loaded only on reports/asset_form.html via
   {% block head %} → vendor/trix/trix.css + this rule.
   =================================================================== */
.trix-content {
  min-height: 300px;
}


/* ===================================================================
   Reports — list grid + cards
   PDF Document Generation V1 modular Sprint, Phase E.4 (2026-05-20).

   Mirrors the .projects-grid recipe (VDP Step 6) with a Reports-
   specific namespace per FLAG B. Step 14 backlog tracks the
   eventual consolidation into a generic .list-grid-card namespace.
   Known, planned duplication — not accidental.
   =================================================================== */

.reports-container {
  background:    var(--color-surface);
  border:        1px solid var(--color-border-default);
  border-radius: var(--radius);
  box-shadow:    var(--shadow-sm);
  padding:       var(--space-xl);
}

/* ----- Toolbar (above grid; FLAG A) ----- */
.reports-toolbar {
  display:         flex;
  justify-content: flex-end;
  align-items:     center;
  gap:             var(--space-md);
  margin-bottom:   var(--space-lg);
}

/* ----- Grid ----- */
.reports-grid {
  display:               grid;
  grid-template-columns: repeat(2, 1fr);
  gap:                   var(--space-lg);
}

.reports-grid-card {
  position:      relative;
  background:    var(--color-card-tint);  /* soft warm lift (Phase F polish) */
  border:        1px solid var(--color-border-default);
  border-radius: var(--radius);
  transition:
    transform 0.15s ease-out,
    box-shadow 0.15s ease-out,
    border-color 0.15s ease-out;
  /* Bundle Estética/UX item 6 Layer 2 sub-step 2b-4 (2026-05-31):
     min-width: 0 — same fix as .projects-grid-card. Grid items
     default to min-width: auto; without override, asset cards
     (Headers/Footers/Notes lists) with long asset names or long
     embedded preview content overflowed the viewport on mobile. */
  min-width: 0;
}

.reports-grid-card:hover {
  transform:    translateY(-2px);
  box-shadow:   var(--shadow-md);
  border-color: var(--color-border-strong);
}

.reports-grid-card__body-link {
  display:         flex;
  flex-direction:  column;
  gap:             var(--space-xs);
  padding:         var(--space-lg);
  padding-right:   var(--space-3xl);  /* leave room for kebab corner */
  text-decoration: none;
  color:           var(--color-text-primary);
}
.reports-grid-card__body-link:hover { text-decoration: none; }

.reports-grid-card__name {
  font-size:     1rem;
  font-weight:   600;
  line-height:   1.4;
  color:         var(--color-text-primary);
  white-space:   nowrap;
  overflow:      hidden;
  text-overflow: ellipsis;
}

.reports-grid-card__label {
  font-size:     0.8125rem;
  color:         var(--color-text-muted);
  white-space:   nowrap;
  overflow:      hidden;
  text-overflow: ellipsis;
}

/* 2-line clamp for the striptags preview. Combined with Jinja's
   truncate(120) hard cap, this gives a visual ellipsis when 120
   chars wrap to >2 lines at narrow card widths. Same -webkit-
   line-clamp pattern as .price-list-detail-header__description. */
.reports-grid-card__preview {
  font-size:           0.875rem;
  line-height:         1.5;
  color:               var(--color-text-secondary);
  margin-top:          var(--space-xs);
  display:             -webkit-box;
  -webkit-line-clamp:  2;
  -webkit-box-orient:  vertical;
  overflow:            hidden;
}

.reports-grid-card__date {
  font-size:      0.75rem;
  color:          var(--color-text-muted);
  letter-spacing: 0.01em;
  margin-top:     var(--space-sm);
}

/* ===============================================
   Bundle A #8 (2026-05-28) — asset card preview
   Browser-rendered approximation of a header/footer's
   branding block, embedded in the list cards so
   they're distinguishable at a glance by SHAPE.
   PARALLEL to render_branding_block in the PDF
   templates (NOT shared — locked 2026-05-28).
   Scoped under .asset-card-preview* — no leak,
   PDF .header-layout/.header-block rules unaffected.
   =============================================== */

/* Outer slot — fixed visual size; the inner is laid out at 2× this
   width and transform:scale(0.5)'d back, so we fit ANY card width
   without per-card math (the wrapper takes width:100% of whatever
   card column owns it). overflow:hidden clips anything that runs
   past the visual box. Subtle border + white bg so the preview
   reads as a discrete sub-element of the card. */
.asset-card-preview {
  width:         100%;
  height:        90px;
  min-height:    90px;    /* belt — overflowing content (tall Trix images)
                             must NOT push the slot taller */
  max-height:    90px;    /* suspenders — same constraint from the other side */
  margin-bottom: var(--space-sm);
  border:        1px solid var(--color-border-subtle);
  border-radius: var(--radius-sm);
  background:    white;
  overflow:      hidden;
  position:      relative;
  flex:          0 0 90px; /* and, in the .reports-grid-card__body-link
                              flex column, never grow/shrink — defensive
                              against any flex-context regression */
}

/* Inner — pre-scale layout. width:200% + scale(0.5) → visual width
   matches the wrapper. transform-origin:top left anchors the box at
   (0,0) of the wrapper (no centering offset, no negative-translate
   compensation needed). Print units (pt) so layout numbers stay
   parallel to the PDF macro's; browsers honor pt as ~1.333px. */
.asset-card-preview__inner {
  width:           200%;
  height:          200%;
  padding:         10pt 14pt;
  box-sizing:      border-box;
  overflow:        hidden;  /* defensive: prevents content (tall images)
                                from visually escaping even momentarily */
  transform:       scale(0.5);
  transform-origin: top left;
  font-family:     'Inter', 'Helvetica', 'Arial', sans-serif;
  font-size:       10pt;
  line-height:     1.45;
  color:           #333;
}

/* Two-cell layout (logo + text) — mirrors PDF .header-layout */
.asset-card-preview__layout {
  width:           100%;
  border-collapse: collapse;
}
.asset-card-preview__layout td {
  vertical-align: top;
  padding:        0;
}

/* Logo cell shrinks to content via the width:1% + white-space:nowrap
   CSS-table idiom (same as PDF macro). */
.asset-card-preview__logo-cell {
  width:        1%;
  white-space:  nowrap;
}
.asset-card-preview__logo-cell img {
  max-width:    200pt;
  max-height:   60pt;
  object-fit:   contain;
  margin:       0 12pt;
  display:      block;
}
.asset-card-preview__logo-cell--right { text-align: right; }

/* Centered layout — REDESIGN 2026-05-28.
   Side branches (.asset-card-preview__layout, a <table>) get implicit
   height-bounding from CSS-table's max-of-cells row algorithm and can
   safely render full content_html (embedded <img>/<figure>/<figcaption>
   all included). The center branch has NO equivalent natural bound —
   its block stack sums children heights and previously broke the slot.
   Fix: drop embedded HTML in the center body (template emits plain
   text via {{ body }} = striptags|trim) AND bound the center container
   tight with flex column + 2-line text clamp. The logo is flex-shrink:0
   so it CAN'T be cropped by the flex algorithm — the only thing that
   can clip is the bottom of overflowing text via -webkit-line-clamp:2. */
.asset-card-preview__center {
  height:          100%;
  overflow:        hidden;
  display:         flex;
  flex-direction:  column;
  justify-content: center;
  align-items:     center;
  gap:             6pt;
}
.asset-card-preview__center-img {
  max-height:  60pt;
  max-width:   100%;
  width:       auto;
  height:      auto;
  flex-shrink: 0;          /* whole logo always visible — flex can't shrink it */
  object-fit:  contain;
  display:     block;
}
/* Plain text below logo in center branch. 2-line clamp keeps stack
   bounded; ellipsis truncates anything beyond. text-align:center
   matches the centered logo above. */
.asset-card-preview__center-text {
  font-size:           10pt;
  line-height:         1.4;
  color:               #333;
  text-align:          center;
  max-width:           100%;
  display:             -webkit-box;
  -webkit-line-clamp:  2;
  -webkit-box-orient:  vertical;
  overflow:            hidden;
  word-wrap:           break-word;
}

/* Solo logo (no body) — position respected via text-align */
.asset-card-preview__solo-logo--left   { text-align: left; }
.asset-card-preview__solo-logo--center { text-align: center; }
.asset-card-preview__solo-logo--right  { text-align: right; }
.asset-card-preview__solo-logo img {
  max-width:    200pt;
  max-height:   60pt;
  object-fit:   contain;
  display:      inline-block;
}

/* Text block — matches PDF .header-block tone. Safety overflow guards
   for content_html (Trix output can contain wide images / long words). */
.asset-card-preview__text {
  font-size:   10pt;
  color:       #333;
  word-wrap:   break-word;
}

/* Trix content_html is rendered WITHOUT a .trix-content ancestor in the
   card, so Trix's own vendor CSS (max-width:100%, neutralized margins,
   etc.) doesn't apply. Every cap below is a hard override of:
   - HTML width/height attrs on <img> (Trix writes width="3840"
     height="2160" directly on uploaded images — presentation hints
     that beat normal CSS unless we !important)
   - default UA figure margin (1em 40px)
   - the <a> anchor Trix wraps around <img>+<figcaption> for click-to-
     open
   - the <figcaption class="attachment__caption"> with .attachment__name
     + .attachment__size spans
   Selector scoped to .asset-card-preview so it touches the card preview
   ONLY — no leak to the editor, the asset detail page, or anywhere else
   trix-rendered HTML is shown. */

/* ALL images in the preview (logo cells, center logo, solo logo, AND
   embedded body images). width:auto + height:auto force the img to
   compute its own size from the intrinsic ratio (instead of obeying
   the HTML width/height attrs); max-width + max-height then constrain
   aspect-preserving. 60pt height cap matches the logo cap so the
   visual scale stays uniform regardless of source. */
.asset-card-preview img {
  width:      auto       !important;
  height:     auto       !important;
  max-width:  100%       !important;
  max-height: 60pt       !important;
  object-fit: contain    !important;
}

/* Trix's <figure class="attachment attachment--preview"> wraps embedded
   images. Reset margins + cap dimensions so the figure can't be taller
   than the image it holds + can't add extra vertical space. */
.asset-card-preview figure,
.asset-card-preview .attachment,
.asset-card-preview .attachment--preview {
  display:    inline-block !important;
  margin:     0            !important;
  padding:    0            !important;
  max-width:  100%         !important;
  max-height: 60pt         !important;
  overflow:   hidden       !important;
  line-height: 1           !important;
}

/* The <a> Trix wraps around <img>+<figcaption> inside the figure. */
.asset-card-preview .attachment a,
.asset-card-preview figure a {
  display:    inline-block !important;
  margin:     0            !important;
  padding:    0            !important;
  line-height: 0           !important;
}

/* Hide the whole caption stack (figcaption + name span + size span).
   Mirrors the PDF macro's behavior (default.html.j2:217). */
.asset-card-preview figcaption,
.asset-card-preview .attachment__caption,
.asset-card-preview .attachment__name,
.asset-card-preview .attachment__size {
  display: none !important;
}

/* Genuinely-empty asset — same fixed slot height as the filled
   preview, slightly muted surface bg + centered muted hint. Reads as
   an intentional empty state (not a broken/empty bordered box). The
   hint is OUTSIDE the scale wrapper so it renders at natural UI size,
   not scaled-down preview size. */
.asset-card-preview--empty {
  display:         flex;
  align-items:     center;
  justify-content: center;
  background:      var(--color-surface-alt);
}
.asset-card-preview__empty-hint {
  font-size:   0.8125rem;
  color:       var(--color-text-muted);
  font-style:  italic;
}


/* ----- Kebab button (top-right corner) ----- */
.reports-grid-card__kebab {
  position:        absolute;
  top:             var(--space-sm);
  right:           var(--space-sm);
  width:           28px;
  height:          28px;
  display:         flex;
  align-items:     center;
  justify-content: center;
  background:      transparent;
  border:          0;
  border-radius:   50%;
  color:           var(--color-text-primary);
  cursor:          pointer;
  opacity:         1;
  transition:      opacity 0.3s ease, background-color 0.15s ease-out;
  z-index:         2;
}

.reports-grid-card__kebab:hover  { background: var(--color-border-subtle); }
.reports-grid-card__kebab:active { background: var(--color-border-default); }
.reports-grid-card__kebab:focus-visible {
  outline:        2px solid var(--color-text-primary);
  outline-offset: 1px;
}

/* Desktop: kebab fades in on card hover. Tablet/mobile: always visible. */
@media (min-width: 1024px) {
  .reports-grid-card__kebab { opacity: 0; }
  .reports-grid-card:hover .reports-grid-card__kebab,
  .reports-grid-card__kebab:focus-visible,
  .reports-grid-card__kebab[aria-expanded="true"] { opacity: 1; }
}

/* ----- Kebab dropdown ----- */
.reports-grid-card__dropdown {
  position:      absolute;
  top:           calc(var(--space-sm) + 28px + 4px);
  right:         var(--space-sm);
  min-width:     140px;
  background:    var(--color-bg);
  border:        1px solid var(--color-border-default);
  border-radius: var(--radius-sm);
  box-shadow:    var(--shadow-md);
  padding:       var(--space-xs);
  z-index:       10;
  display:       none;
}

.reports-grid-card__dropdown.is-open { display: block; }

/* Pinear tarjetas Commit 4 (2026-06-08) — open-kebab clipping fix, same
   mechanism + same approach as .projects-grid-card (Commit 2). The card's
   :hover transform (line ~7272) creates a stacking context that traps the
   dropdown's z-index; adding the Pin item lengthens the menu enough to
   overhang the next card, so a later sibling paints over Delete. Raising
   the card while its dropdown is open lifts the whole subtree above
   neighbors. Reports-scoped class — does not touch PL/Projects. */
.reports-grid-card:has(.reports-grid-card__dropdown.is-open) {
  z-index: 20;
}

.reports-kebab-item {
  display:       block;
  width:         100%;
  padding:       var(--space-sm) var(--space-md);
  background:    transparent;
  border:        0;
  border-radius: 4px;
  font-size:     0.875rem;
  font-weight:   500;
  font-family:   var(--font-sans);
  color:         var(--color-text-primary);
  text-align:    left;
  cursor:        pointer;
  transition:    background-color 0.1s ease;
}

.reports-kebab-item:hover { background: var(--color-surface-alt); }
.reports-kebab-item:focus-visible {
  outline:        2px solid var(--color-text-primary);
  outline-offset: -2px;
}

.reports-kebab-item--danger { color: var(--color-error); }
.reports-kebab-item--danger:hover { background: var(--color-error-bg); }

.reports-kebab-divider {
  height:     1px;
  background: var(--color-border-subtle);
  margin:     2px 0;
  border:     0;
}

/* Per-card inline duplicate form — display: contents removes the
   form element from layout flow so the button sits flush inside the
   dropdown like the delete button (FLAG D). */
.reports-kebab-form {
  display: contents;
  margin:  0;
}

/* ----- Responsive ----- */
@media (max-width: 767px) {
  .reports-container { padding: var(--space-lg); }
  .reports-grid      { grid-template-columns: 1fr; }
  .reports-toolbar   { justify-content: stretch; }
  .reports-toolbar .btn { flex: 1 1 auto; }
}


/* ===============================================
   Reports — logo slot (Phase F, F23/F27/F35)
   Asset editor's structured logo field on header
   and footer forms. Stacked layout in V1
   (heading → preview/remove → file → position
   toggles); the fancier two-column right-panel
   layout is post-F polish per backlog.
   =============================================== */

.logo-slot {
  background: var(--color-surface-alt);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-md, 8px);
  padding: var(--space-lg);
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
}

/* Item 1 UX (Bundle A, 2026-05-27): the individual-logo wrap is one
   flex child of .logo-slot — recreate the flex-column / --space-md gap
   internally so its children keep their spacing. is-disabled dims +
   blocks pointer events when "use company logo" is on (asset_logo.js
   also flips `disabled` on buttons + the file input for accessibility;
   hidden inputs stay submittable so the user's previous position
   survives a flag round-trip). */
.logo-slot__own {
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
  transition: opacity 0.15s ease;
}
.logo-slot__own.is-disabled {
  opacity: 0.45;
  pointer-events: none;
  user-select: none;
}

/* Item 1 UX (Bundle A, 2026-05-27): scoped checkbox reset for the
   logo slot. Without this, .field input (main.css:596 — height:
   var(--height-input); padding; width:100%) pumps the checkbox to a
   full-width row-height input box. Mirrors the established
   .field-checkbox 18×18 + accent-color reset. */
.logo-slot input[type="checkbox"] {
  width: 18px;
  height: 18px;
  padding: 0;
  border: 0;
  cursor: pointer;
  accent-color: var(--color-accent);
  flex-shrink: 0;
}

.logo-slot__heading {
  /* Reuse the field-label weight/color; logo-slot is a labeled
     group rather than a single labeled input. */
  font-weight: 500;
  color: var(--color-text-primary);
  margin: 0;
}

.logo-slot__preview {
  display: flex;
  align-items: center;
  gap: var(--space-md);
}

/* Item 7 (Bundle A, 2026-05-28): unified preview-box dimensions —
   tenant-logo preview (top-right) + per-asset logo preview (below)
   render at the SAME size (200×80, 5:2) regardless of source image
   aspect. object-fit:contain letterboxes the image inside; the box
   itself stays consistent so the two read as a coherent pair. */
.logo-slot__preview-img {
  width: 200px;
  height: 80px;
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius-sm);
  background: white;
  object-fit: contain;
}

.logo-slot__remove {
  background: none;
  border: none;
  padding: 0;
  color: var(--color-error);
  cursor: pointer;
  font-size: 0.875rem;
  text-decoration: underline;
}
.logo-slot__remove:hover { color: #8b1a14; }

/* Item 7 (Bundle A, 2026-05-28) — 2-column layout.
   LEFT col = source pill + position picker (controls).
   RIGHT col = tenant preview + per-asset block (preview/upload/remove).
   Collapses to a single column under 640px (small tablet / mobile). */
.logo-slot__grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--space-lg);
  align-items: start;
}
.logo-slot__col {
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
  min-width: 0;
}

/* Field group inside a column — small caption above the control.
   align-items: flex-start so child controls (pill, position-toggle)
   shrink to their content width instead of stretching to fill the
   column. The narrow-viewport media queries below explicitly set
   width:100% where they want full-bleed (position-toggle <480px). */
.logo-slot__field {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-xs, 4px);
}
.logo-slot__field-label {
  color: var(--color-text-muted);
  font-size: 0.8125rem;
  font-weight: 500;
  letter-spacing: 0.02em;
}
.logo-slot__hint {
  margin: var(--space-xs, 4px) 0 0 0;
}

/* Source pill — segmented two-segment control replacing the original
   checkbox + inline-label row. Drives the hidden #logo_use_tenant
   input via asset_logo.js; wire format unchanged ('1' or empty). */
.logo-slot__pill {
  display: inline-flex;
  border: 1px solid var(--color-border-default);
  border-radius: 999px;
  overflow: hidden;
  background: white;
  align-self: flex-start;
}
.logo-slot__pill-segment {
  background: transparent;
  border: none;
  padding: var(--space-sm) var(--space-lg);
  cursor: pointer;
  font-size: 0.875rem;
  color: var(--color-text-primary);
  transition: background 120ms ease, color 120ms ease;
}
.logo-slot__pill-segment + .logo-slot__pill-segment {
  border-left: 1px solid var(--color-border-default);
}
/* Hover on the INACTIVE segment: gray bg per the menu/dropdown
   convention (--color-border-default #E5E5E5, NOT the near-white
   --color-surface-alt #FAFAFA which left text washed out). Text color
   is RE-asserted so nothing inherited (button defaults, a:hover) can
   wash it. Same class of fix as the Copilot UX Polish dropdown-hover. */
.logo-slot__pill-segment:hover:not(:disabled):not(.is-active) {
  background: var(--color-border-default);
  color: var(--color-text-primary);
}
.logo-slot__pill-segment:focus-visible {
  outline: 2px solid var(--color-text-primary);
  outline-offset: -2px;
  color: var(--color-text-primary);
}
.logo-slot__pill-segment.is-active {
  background: var(--color-text-primary);
  color: white;
}
/* Active + hover/focus: keep dark bg + white text (no wash). */
.logo-slot__pill-segment.is-active:hover,
.logo-slot__pill-segment.is-active:focus-visible {
  background: var(--color-text-primary);
  color: white;
}
.logo-slot__pill-segment:disabled {
  cursor: not-allowed;
  color: var(--color-text-muted);
}

/* 3-button segmented position picker (F38) — Left | Center | Right.
   NOT a dropdown: 3 visible affordances so the choice is obvious.
   Active button uses primary-color background; the divider between
   buttons is the border-right on every btn except the last. */
.position-toggle {
  display: inline-flex;
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: white;
}

.position-toggle__btn {
  background: transparent;
  border: none;
  border-right: 1px solid var(--color-border-default);
  padding: var(--space-sm) var(--space-lg);
  cursor: pointer;
  font-size: 0.875rem;
  color: var(--color-text-primary);
  transition: background 120ms ease, color 120ms ease;
}
.position-toggle__btn:last-child { border-right: none; }
.position-toggle__btn:hover      { background: var(--color-surface-alt); }
.position-toggle__btn:focus-visible {
  outline: 2px solid var(--color-text-primary);
  outline-offset: -2px;
}
.position-toggle__btn.is-active {
  background: var(--color-text-primary);
  color: white;
}

/* Item 7 (Bundle A, 2026-05-28): grid + position-toggle narrow stacking.
   Below 640px the 2-col grid collapses to single-column (handled by
   .logo-slot__grid above). The position-toggle goes full-width
   segmented under 480px (see breakpoint below). */


/* ===============================================
   Reports — logo-slot polish (Phase G, G12)
   Replaces the dated native file input with a
   styled <label> over a visually-hidden input,
   adds responsive behavior at 480px viewport.
   The original .logo-slot rules above remain in
   force; these additions extend them.
   =============================================== */

/* Visually hidden but focusable input — pointer-events:none so the
   <label> takes all clicks, opacity:0 so it's invisible, 1px
   dimensions keep it focusable for keyboard nav + screen readers.
   Do NOT use display:none (breaks form submit on some browsers +
   makes the control invisible to assistive tech). */
/* Bundle Estética/UX item 6 Layer 2 sub-step 2b-13 (2026-05-31):
   !important on width/height/position/opacity to beat the
   .field input { width: 100% } cascade. Without it, the file input
   inside the .logo-slot (which is wrapped in .field) is rendered at
   100% of its containing block — 344px instead of 1px — extending
   165px past viewport at 375 mobile and producing horizontal page-
   scroll. position-absolute keeps it removed from flow; opacity 0
   keeps it invisible; the styled .logo-slot__upload label is the
   visible clickable surface. The intent was always 1×1; only the
   specificity was wrong. */
.logo-slot__upload-input {
  position:       absolute !important;
  opacity:        0        !important;
  pointer-events: none;
  width:          1px      !important;
  height:         1px      !important;
}

/* Modern dashed-border affordance. Clicking anywhere in this block
   opens the native file picker via the for=logo_file binding. */
.logo-slot__upload {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-sm);
  padding: var(--space-md) var(--space-lg);
  border: 1.5px dashed var(--color-border-default);
  border-radius: var(--radius-md, 8px);
  background: white;
  color: var(--color-text-primary);
  cursor: pointer;
  transition: border-color 120ms ease, background 120ms ease;
}
.logo-slot__upload:hover {
  border-color: var(--color-text-primary);
  background: var(--color-surface-alt);
}
.logo-slot__upload:focus-within {
  outline: 2px solid var(--color-text-primary);
  outline-offset: 2px;
}
/* Item 10 Part B (Bundle A, 2026-05-28) — drag-over feedback.
   asset_logo.js toggles this class on dragenter/dragleave/drop. Swaps
   the resting dashed-border-on-white look for a solid-border-with-
   tint look so the drop target reads as "ready to accept" rather than
   "click to upload". Part A (notes drag-drop reorder) deferred to
   V1.5+ — see DEFERRED_BACKLOG.md. */
.logo-slot__upload.is-drag-over {
  border-style: solid;
  border-color: var(--color-text-primary);
  background:   var(--color-surface-alt);
}
.logo-slot__upload-icon {
  font-size: 1.25rem;
  line-height: 1;
  font-weight: 500;
}
.logo-slot__upload-text {
  font-size: 0.9375rem;
  /* Truncate a long picked filename to one line inside the dashed box
     (it's a flex item of .logo-slot__upload, flex-wrap:wrap — min-width:0
     lets it shrink below content, max-width:100% pins it to the box so a
     30-char name ellipsis-truncates instead of spilling past the edge on
     mobile). Same pattern as .team-member__name. JS sets a title with the
     full name for hover/long-press. The short default prompt is unaffected. */
  min-width: 0;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Confirmation state — asset_logo.js adds this when a logo file is
   picked. Same --color-success token as the upload-modal filenames and
   .upload-zone--picked, so "file selected" reads consistently everywhere.
   This editor has no inline preview of the picked file, so the green is
   the only "your selection took" signal. Placed BEFORE --error so an
   error-flash (which can briefly coexist with a prior pick) still wins. */
.logo-slot__upload-text--picked {
  color:       var(--color-success);
  font-weight: 500;
}
/* Brief inline error state for the soft drop validation (wrong file
   extension). asset_logo.js sets this for ~2.5s then restores. */
.logo-slot__upload-text--error {
  color:       var(--color-error);
  font-weight: 500;
}
.logo-slot__upload-hint {
  color: var(--color-text-muted);
  font-size: 0.8125rem;
  margin-left: auto;
}

/* Bundle Estética/UX item 6 Layer 2 sub-step 2b-13 (2026-05-31):
   Mobile restack of the entire .logo-slot section at ≤767px.
   - 2-col grid → single column (the old comment claimed this already
     happened at 640px, but no such rule existed — column-collapse
     wasn't actually wired; this rule fills the gap)
   - Pill switches to vertical-stack with horizontal separator instead
     of left separator (so 2 segments share the column at full width
     without text cramping or wrapping mid-label)
   - Position toggle goes full-width with 3 equal-flex buttons
   - Upload box goes full-width, hint wraps to its own row
   Replaces the prior @media (max-width: 480px) block — 480 was too
   narrow (cramping persists at 481-767). The 767 breakpoint matches
   the rest of the app's mobile @media queries. */
@media (max-width: 767px) {
  .logo-slot__grid { grid-template-columns: 1fr; }

  .logo-slot__pill {
    flex-direction: column;
    width:          100%;
    border-radius:  var(--radius-sm);
  }
  .logo-slot__pill-segment            { width: 100%; text-align: center; }
  .logo-slot__pill-segment + .logo-slot__pill-segment {
    border-left: 0;
    border-top:  1px solid var(--color-border-default);
  }

  .position-toggle      { width: 100%; display: flex; }
  .position-toggle__btn { flex: 1; }

  .logo-slot__upload    { width: 100%; justify-content: flex-start; }
  .logo-slot__upload-hint {
    margin-left: 0;
    margin-top:  2px;
    display:     block;
    flex-basis:  100%;
  }
}


/* ===============================================
   Reports — associate view (Phase G)
   The full-page composer for Header / Footer /
   Notes selection per estimate.
   =============================================== */

.associate-form {
  max-width: 720px;
  margin: 0 auto var(--space-3xl);
}

.associate-form__copy {
  margin-bottom: var(--space-xl);
}

.associate-section {
  background: white;
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius-lg);
  padding: var(--space-xl);
  margin-bottom: var(--space-lg);
}
.associate-section h2 {
  margin: 0 0 var(--space-md) 0;
  font-size: 1.0625rem;
  font-weight: 600;
}
.associate-section__hint {
  font-weight: 400;
  font-size: 0.875rem;
  color: var(--color-text-muted);
  margin-left: var(--space-sm);
}
/* Bundle A #9 (2026-05-28) — section heading + "+ Create new" button
   share a row so the action sits at the natural top-right of each
   section without disrupting the existing h2 baseline. The h2's own
   margin-bottom is suppressed (the row provides spacing via
   margin-bottom on the head wrapper). */
.associate-section__head {
  display:         flex;
  align-items:     center;
  justify-content: space-between;
  gap:             var(--space-md);
  margin-bottom:   var(--space-md);
}
.associate-section__head h2 {
  margin: 0;
  flex:   1 1 auto;
  min-width: 0;
}

.associate-select {
  width: 100%;
  padding: var(--space-sm) var(--space-md);
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius-sm);
  background: white;
  font-size: 0.9375rem;
  color: var(--color-text-primary);
}
.associate-select:focus {
  outline: 2px solid var(--color-text-primary);
  outline-offset: -1px;
}

.associate-empty {
  color: var(--color-text-muted);
  font-size: 0.9375rem;
  margin: 0;
}
.associate-empty a {
  color: var(--color-text-primary);
  font-weight: 500;
  text-decoration: underline;
}

/* Notes list — ordered selected entries with per-row controls. */
.notes-selected {
  list-style: none;
  padding: 0;
  margin: 0 0 var(--space-md) 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
}
.notes-selected__row {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  padding: var(--space-sm) var(--space-md);
  background: var(--color-surface-alt);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-sm);
}
.notes-selected__name {
  flex: 1;
  font-size: 0.9375rem;
}
.notes-selected__controls {
  display: flex;
  gap: var(--space-xs);
}

/* Small icon-only buttons used inside the notes list + dialogs. */
.icon-btn {
  background: transparent;
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius-sm);
  width: 28px;
  height: 28px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  color: var(--color-text-primary);
  font-size: 0.875rem;
  line-height: 1;
  transition: background 120ms ease, border-color 120ms ease;
}
.icon-btn:hover  { background: var(--color-surface-alt); }
.icon-btn:focus-visible { outline: 2px solid var(--color-text-primary); outline-offset: 1px; }
.icon-btn:disabled {
  opacity: 0.35;
  cursor: not-allowed;
}
.notes-selected__remove { color: var(--color-error); }

.notes-add {
  margin-top: var(--space-md);
}

.associate-footer {
  position: sticky;
  bottom: 0;
  background: white;
  border-top: 1px solid var(--color-border-default);
  padding: var(--space-md) var(--space-xl);
  margin: var(--space-2xl) calc(var(--space-xl) * -1) 0;
  display: flex;
  gap: var(--space-md);
  justify-content: flex-end;
  align-items: center;
}

.page-header__subtle {
  color: var(--color-text-muted);
  font-weight: 400;
  font-size: 0.875em;
}

@media (max-width: 640px) {
  .associate-form { max-width: 100%; }
  .associate-section { padding: var(--space-lg); }
  .associate-footer {
    flex-direction: column-reverse;
    align-items: stretch;
  }
  .associate-footer .btn { width: 100%; }
}


/* ===============================================
   Reports — Generate-PDF confirmation modal
   (Phase G, G1) — informational, NOT destructive.
   Distinct from .confirm-dialog (destructive).
   =============================================== */

.generate-dialog {
  border: none;
  border-radius: var(--radius-lg);
  padding: 0;
  max-width: 420px;
  width: 90%;
  background: white;
  box-shadow: 0 10px 40px rgba(0,0,0,0.15);
}
.generate-dialog::backdrop {
  background: rgba(0, 0, 0, 0.4);
}
.generate-dialog__inner {
  padding: var(--space-xl);
  position: relative;
}
.generate-dialog__close {
  position: absolute;
  top: var(--space-md);
  right: var(--space-md);
  width: 32px;
  height: 32px;
  font-size: 1.25rem;
}
.generate-dialog__title {
  margin: 0 0 var(--space-md) 0;
  font-size: 1.125rem;
  font-weight: 600;
}
.generate-dialog__intro {
  margin: 0 0 var(--space-md) 0;
  color: var(--color-text-muted);
  font-size: 0.9375rem;
}
.generate-dialog__summary {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--space-sm) var(--space-md);
  margin: 0 0 var(--space-xl) 0;
  font-size: 0.9375rem;
}
.generate-dialog__summary dt {
  color: var(--color-text-muted);
  font-weight: 500;
}
.generate-dialog__summary dd {
  margin: 0;
  color: var(--color-text-primary);
}
.generate-dialog__actions {
  display: flex;
  gap: var(--space-sm);
  justify-content: flex-end;
  flex-wrap: wrap;
}
/* Family 3 (f) commit 3 (2026-06-03) — Ungroup checkbox styling.
   Discreet, smaller-than-body label + breathing room above the
   action buttons row so the checkbox doesn't crowd them. */
.generate-dialog__option {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  font-size: 0.8125rem;
  color: var(--color-text-secondary);
  margin: var(--space-md) 0 var(--space-lg);
}
.generate-dialog__option input[type="checkbox"] {
  margin: 0;
}


/* ===============================================
   Email Share — share dialog (Commit B, 2026-05-23)
   Two-tab modal (Create + Manage). Parallel to
   .generate-dialog shape; wider (560px) for the
   URL input + manage list rows.
   =============================================== */

.share-dialog {
  border: none;
  border-radius: var(--radius-lg);
  padding: 0;
  max-width: 560px;
  width: 92%;
  background: white;
  box-shadow: 0 10px 40px rgba(0,0,0,0.15);
}
.share-dialog::backdrop {
  background: rgba(0, 0, 0, 0.4);
}
.share-dialog__inner {
  padding: var(--space-xl);
  position: relative;
}
.share-dialog__close {
  position: absolute;
  top: var(--space-md);
  right: var(--space-md);
  width: 32px;
  height: 32px;
  font-size: 1.25rem;
}
.share-dialog__title {
  margin: 0 0 var(--space-xs) 0;
  font-size: 1.125rem;
  font-weight: 600;
  padding-right: 40px; /* clear of × button */
}
.share-dialog__subtitle {
  margin: 0 0 var(--space-md) 0;
  color: var(--color-text-muted);
  font-size: 0.875rem;
}
.share-dialog__tabs {
  display: flex;
  gap: 0;
  border-bottom: 1px solid var(--color-border);
  margin: 0 0 var(--space-md) 0;
}
.share-dialog__tab {
  background: transparent;
  border: none;
  padding: var(--space-sm) var(--space-md);
  color: var(--color-text-muted);
  font-size: 0.9375rem;
  font-weight: 500;
  cursor: pointer;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
}
.share-dialog__tab.is-active {
  color: var(--color-text-primary);
  border-bottom-color: var(--color-text-primary);
}
.share-dialog__tab-count {
  display: inline-block;
  background: var(--color-border-default);
  color: var(--color-text-muted);
  font-size: 0.75rem;
  padding: 1px 6px;
  border-radius: 8px;
  margin-left: 4px;
  min-width: 18px;
  text-align: center;
}
.share-dialog__pane {
  min-height: 200px;
}
.share-dialog__state {
  /* States are toggled via [hidden]; no per-state visibility CSS. */
}
.share-dialog__intro {
  margin: 0 0 var(--space-md) 0;
  color: var(--color-text-primary);
  font-size: 0.9375rem;
  line-height: 1.5;
}
.share-dialog__intro--success {
  color: var(--color-success);
}
.share-dialog__hint {
  margin: 0;
  color: var(--color-text-muted);
  font-size: 0.9375rem;
  display: flex;
  align-items: center;
  gap: var(--space-sm);
}
.share-dialog__error {
  margin: 0 0 var(--space-md) 0;
  color: var(--color-error);
  font-size: 0.9375rem;
}
.share-dialog__spinner {
  display: inline-block;
  animation: generating-spin 1.2s linear infinite;
  color: var(--color-text-muted);
  font-size: 1.25rem;
}
.share-dialog__ttl {
  border: none;
  padding: 0;
  margin: 0 0 var(--space-xl) 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
}
.share-dialog__ttl-legend {
  margin: 0 0 var(--space-sm) 0;
  font-weight: 500;
  font-size: 0.875rem;
  color: var(--color-text-primary);
}
.share-dialog__ttl-option {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  padding: var(--space-xs) 0;
  cursor: pointer;
  font-size: 0.9375rem;
}
.share-dialog__url-row {
  display: flex;
  gap: var(--space-sm);
  margin: 0 0 var(--space-xs) 0;
}
.share-dialog__url-input {
  flex: 1;
  min-width: 0;
  padding: 8px 10px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  font-size: 0.875rem;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  background: #f9fafb;
  color: var(--color-text-primary);
}
.share-dialog__copied-msg {
  margin: 0 0 var(--space-md) 0;
  color: var(--color-success);
  font-size: 0.875rem;
}
.share-dialog__actions {
  display: flex;
  gap: var(--space-sm);
  justify-content: flex-end;
  flex-wrap: wrap;
  margin-top: var(--space-md);
}
.share-dialog__list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  max-height: 320px;
  overflow-y: auto;
}
.share-dialog__row {
  display: grid;
  grid-template-columns: 1fr auto;
  grid-template-rows: auto auto;
  grid-template-areas:
    "url    actions"
    "meta   actions";
  gap: 2px var(--space-md);
  padding: var(--space-sm) var(--space-md);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: #fdfdfe;
}
.share-dialog__row-url {
  grid-area: url;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.8125rem;
  color: var(--color-text-primary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}
.share-dialog__row-meta {
  grid-area: meta;
  color: var(--color-text-muted);
  font-size: 0.8125rem;
}
.share-dialog__row-actions {
  grid-area: actions;
  display: flex;
  gap: var(--space-sm);
  align-items: center;
}


/* ===============================================
   Reports — Generating interstitial (Phase G, G4)
   Auto-submitting page shown between
   "Save & generate PDF" and the streamed PDF
   download. Resolves F21 (loading state).
   =============================================== */

.generating-page {
  max-width: 480px;
  margin: var(--space-4xl) auto;
  text-align: center;
  padding: var(--space-2xl);
}
.generating-page__spinner {
  font-size: 2.5rem;
  display: inline-block;
  animation: generating-spin 1.2s linear infinite;
  color: var(--color-text-muted);
  margin-bottom: var(--space-md);
}
@keyframes generating-spin {
  to { transform: rotate(360deg); }
}
.generating-page__title {
  font-size: 1.25rem;
  font-weight: 600;
  margin: 0 0 var(--space-sm) 0;
}
.generating-page__hint {
  color: var(--color-text-muted);
  font-size: 0.9375rem;
  margin: 0 0 var(--space-xl) 0;
}
.generating-page__noscript {
  color: var(--color-error);
  margin-bottom: var(--space-md);
}
.generating-page__back {
  margin-top: var(--space-2xl);
  font-size: 0.875rem;
  color: var(--color-text-muted);
}
.generating-page__back a {
  color: var(--color-text-primary);
  text-decoration: underline;
}

/* Phase G fix #2 — done-state + error-state styles + large Back button.
   The default state is spinner; JS toggles [hidden] on the three
   sibling .generating-page__state blocks based on fetch lifecycle. */

.generating-page__state[hidden] { display: none; }

.generating-page__check {
  font-size: 3rem;
  line-height: 1;
  color: var(--color-success, #16745F);
  margin-bottom: var(--space-md);
  display: inline-block;
}

.generating-page__error-icon {
  font-size: 2.25rem;
  line-height: 1;
  color: var(--color-error);
  background: var(--color-error-bg);
  width: 64px;
  height: 64px;
  border-radius: 999px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-weight: 700;
  margin-bottom: var(--space-md);
}

/* Large "Back to <project>" button — primary CTA in the done/error
   states. Wider + taller than .btn-sm so the user can't miss it,
   matching the "make it a real done-state, not a flicker" intent. */
.generating-page__back-btn {
  margin-top: var(--space-xl);
  min-width: 260px;
  height: 48px;
  padding: 0 var(--space-xl);
  font-size: 1rem;
}
/* Batch 6 — the 260px min-width overflows narrow phones; full-width on mobile
   so the back button always fits the viewport (label is generic + short now). */
@media (max-width: 480px) {
  .generating-page__back-btn { min-width: 0; width: 100%; }
}

.generating-page__noscript-hint {
  color: var(--color-text-muted);
  margin: var(--space-md) 0;
}
.generating-page__noscript-form {
  margin-top: var(--space-xl);
}


/* ===============================================
   Duplicate Sprint (2026-05-22) — one-time
   refresh-totals alert dialog. Informational
   (neutral styling, NOT destructive red),
   distinct from _confirm_dialog.html (destructive)
   and _generate_confirm_dialog.html (Phase G
   generate-PDF). Reuses the Phase G dialog
   patterns + tokens for visual consistency.
   =============================================== */

.dup-refresh-dialog {
  border: none;
  border-radius: var(--radius-lg);
  padding: 0;
  max-width: 420px;
  width: 90%;
  background: white;
  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15);
}
.dup-refresh-dialog::backdrop {
  background: rgba(0, 0, 0, 0.4);
}
.dup-refresh-dialog__inner {
  padding: var(--space-xl);
  position: relative;
}
.dup-refresh-dialog__close {
  position: absolute;
  top: var(--space-md);
  right: var(--space-md);
  width: 32px;
  height: 32px;
  font-size: 1.25rem;
}
.dup-refresh-dialog__title {
  margin: 0 0 var(--space-md) 0;
  font-size: 1.125rem;
  font-weight: 600;
}
.dup-refresh-dialog__body {
  margin: 0 0 var(--space-xl) 0;
  font-size: 0.9375rem;
  color: var(--color-text-primary);
  line-height: 1.5;
}
.dup-refresh-dialog__actions {
  display: flex;
  gap: var(--space-sm);
  justify-content: flex-end;
  flex-wrap: wrap;
}


/* ===============================================
   Duplicate Sprint name modal (2026-05-22 addition)
   "Rename or accept" dialog, OS-file-manager-style.
   Editable text input pre-filled with the suggested
   name; Accept submits, Cancel closes no-op.
   Neutral styling (NOT destructive).
   =============================================== */

.dup-name-dialog {
  border: none;
  border-radius: var(--radius-lg);
  padding: 0;
  max-width: 440px;
  width: 90%;
  background: white;
  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15);
}
.dup-name-dialog::backdrop {
  background: rgba(0, 0, 0, 0.4);
}
.dup-name-dialog__inner {
  padding: var(--space-xl);
  position: relative;
}
.dup-name-dialog__close {
  position: absolute;
  top: var(--space-md);
  right: var(--space-md);
  width: 32px;
  height: 32px;
  font-size: 1.25rem;
}
.dup-name-dialog__title {
  margin: 0 0 var(--space-md) 0;
  font-size: 1.125rem;
  font-weight: 600;
}
.dup-name-dialog__body {
  margin: 0 0 var(--space-md) 0;
  font-size: 0.9375rem;
  color: var(--color-text-muted);
}
.dup-name-dialog__actions {
  display: flex;
  gap: var(--space-sm);
  justify-content: flex-end;
  flex-wrap: wrap;
}


/* ===============================================
   Estimate Comparator (Sprint A — Mode 1, 2026-05-22)
   Selection view + result view + per-field red.
   Mode 2 retrofit (Sprint B) adds asymmetric
   styles; this block is the Mode 1 foundation.
   =============================================== */

.comparator-body { padding-bottom: var(--space-3xl); }

/* ----- Selection view ----- */

.comparator-selection {
  /* Workflow view, not a form — paired two-slot layout needs room
     to breathe. 1080px is the sweet spot between the form-width
     (720px feels cramped here, post-smoke feedback 2026-05-22) and
     full app-content width (1600px, which leaves an absurd gap
     between the slots on wide screens). The slots already collapse
     to single-column below 640px via @media below — desktop
     spacious, mobile sane. */
  max-width: 1080px;
  margin: 0 auto;
}

.comparator-selection__search {
  margin-bottom: var(--space-xl);
}

.comparator-selection__search input {
  width: 100%;
  padding: var(--space-md) var(--space-lg);
  font-size: 1rem;
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius-md, 8px);
  background: white;
}

.comparator-selection__results {
  position: relative;
  margin-bottom: var(--space-xl);
  max-height: 360px;
  overflow-y: auto;
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-md, 8px);
  background: white;
}

/* Bottom scroll-fade affordance REMOVED — Bundle Estética/UX item 6
   Layer 2 sub-step 2b-10 (2026-05-31). The ::after gradient + inset
   box-shadow (added 2026-05-28) was reading as a stray shadow
   landing on the bordered list card rather than as a scroll cue at
   the page level. The native scrollbar on the overflow:auto list is
   sufficient affordance for "more rows below"; the visual fade was
   superfluous and visually noisy inside the card. comparator_select.js
   still toggles the .has-overflow-bottom class on scroll / resize /
   results-refresh — harmless CSS no-op now, kept so the JS doesn't
   need a paired change for a pure visual revert. */

.comparator-selection__empty {
  padding: var(--space-xl);
  text-align: center;
  color: var(--color-text-muted);
  font-size: 0.9375rem;
}

.comparator-result-card {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  padding: var(--space-md) var(--space-lg);
  border-bottom: 1px solid var(--color-border-subtle);
  transition: background 100ms ease;
}
.comparator-result-card:last-child { border-bottom: none; }
.comparator-result-card:hover { background: var(--color-surface-alt); }

.comparator-result-card__main { flex: 1; min-width: 0; }
.comparator-result-card__name {
  font-weight: 500;
  color: var(--color-text-primary);
  font-size: 0.9375rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.comparator-result-card__meta {
  color: var(--color-text-muted);
  font-size: 0.8125rem;
  margin-top: 2px;
}

/* Two slots side-by-side. */
.comparator-selection__slots {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-md);
  margin-bottom: var(--space-xl);
}

.comparator-slot {
  background: var(--color-surface-alt);
  border: 1.5px dashed var(--color-border-default);
  border-radius: var(--radius-md, 8px);
  padding: var(--space-md) var(--space-lg);
  display: flex;
  align-items: center;
  gap: var(--space-md);
  min-height: 64px;
}
.comparator-slot--filled {
  background: white;
  border-style: solid;
}
.comparator-slot__label {
  font-weight: 600;
  font-size: 0.875rem;
  color: var(--color-text-muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  flex-shrink: 0;
}
.comparator-slot__content {
  flex: 1;
  font-size: 0.9375rem;
  color: var(--color-text-primary);
  min-width: 0;
  word-break: break-word;
}
.comparator-slot__remove {
  flex-shrink: 0;
}

.comparator-selection__actions {
  display: flex;
  justify-content: center;
}

/* Recent comparisons list (post-smoke 2026-05-22). Rendered by JS
   below the Compare CTA. Hidden by default; revealed when at least
   one entry exists in localStorage for this tenant.

   Final-tweak post-smoke: title bumped from muted-gray to primary
   text + slightly larger so users notice it (Gus almost missed the
   recents existed). Container is viewport-safe via max-width: 100%
   and items use overflow-wrap: anywhere so long PDF filenames
   wrap cleanly inside the link instead of overflowing. */
.comparator-recent {
  margin-top: var(--space-2xl);
  padding-top: var(--space-xl);
  border-top: 1px solid var(--color-border-subtle);
  max-width: 100%;
  box-sizing: border-box;
}
.comparator-recent__title {
  font-size: 1rem;
  font-weight: 600;
  color: var(--color-text-primary);
  margin: 0 0 var(--space-md) 0;
  /* Drop the uppercase-+-letter-spacing styling — too quiet.
     Plain primary text reads as a section heading users notice. */
}
.comparator-recent__list {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  max-width: 100%;
}
.comparator-recent__item {
  padding: 0;
  max-width: 100%;
}
.comparator-recent__link {
  display: block;
  padding: var(--space-sm) var(--space-md);
  background: var(--color-surface-alt);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-sm);
  color: var(--color-text-primary);
  font-size: 0.9375rem;
  text-decoration: none;
  /* Long filenames wrap inside the link instead of overflowing the
     container — viewport-safe even on narrow screens. */
  word-break: break-word;
  overflow-wrap: anywhere;
  max-width: 100%;
  box-sizing: border-box;
  transition: background 100ms ease;
}
.comparator-recent__link:hover {
  background: white;
  border-color: var(--color-border-default);
  text-decoration: none;
}

@media (max-width: 640px) {
  .comparator-selection__slots {
    grid-template-columns: 1fr;
  }
}


/* ----- Result view: two-card header ----- */

.comparator-headers {
  display: grid;
  /* Match the table's column widths so each header card sits exactly
     over its column. Empty spacer takes the leftmost 18% above the
     Code/Type column (which needs no header card). Post-smoke
     feedback 2026-05-22 — earlier 1fr 1fr split was 50/50 across
     full width and didn't align with the 18%/41%/41% table columns. */
  grid-template-columns: 18% 41% 41%;
  gap: 0;
  margin-bottom: var(--space-xl);
}
.comparator-headers__spacer { /* intentionally empty — visual gutter */ }

.comparator-header {
  padding: var(--space-md) var(--space-lg);
  border-radius: var(--radius-md, 8px);
  /* Slight inset so the two color cards don't touch each other at
     the seam — visual breathing room WITHOUT breaking the
     header-over-column alignment (the box's outer position still
     spans the column; only the visible card is inset). */
  margin: 0 4px;
  /* Bumped to 1.5px solid + darker border-color per fix #3 — the
     1px subtle border was visually invisible against the tinted
     background. Per-side border-color comes from --a/--b variants
     below. */
  border: 1.5px solid;
}
/* CC10: blue for A, amber for B. Neutral tints — don't fight the
   red diff highlight in the table below. Darker border-color +
   print-color-adjust:exact ensures backgrounds render in browser
   print + WeasyPrint PDF (which otherwise strips background-color).
   Post-smoke 2026-05-22: previous border colors (#C6DDF1 / #ECD7A4)
   were too close to the background; eye lost the box edges. */
.comparator-header--a {
  background: #EEF4FB;
  border-color: #6FA8D1;
  /* WeasyPrint + browser print: force background to render */
  print-color-adjust: exact;
  -webkit-print-color-adjust: exact;
}
.comparator-header--b {
  background: #FBF3E0;
  border-color: #C9A14A;
  print-color-adjust: exact;
  -webkit-print-color-adjust: exact;
}
.comparator-header__label {
  font-size: 0.75rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--color-text-muted);
  margin-bottom: 2px;
}
.comparator-header__mode {
  font-size: 0.7rem;
  font-weight: 500;
  color: var(--color-text-muted);
  margin-bottom: 6px;
}
.comparator-header__name {
  font-weight: 600;
  font-size: 1rem;
  margin-bottom: 4px;
  word-break: break-word;
}
.comparator-header__meta {
  color: var(--color-text-muted);
  font-size: 0.875rem;
}
.comparator-header__sep {
  margin: 0 4px;
}

@media (max-width: 640px) {
  .comparator-headers { grid-template-columns: 1fr; }
}


/* ----- Result view: comparison table ----- */

.comparator-table {
  width: 100%;
  /* Item 6 post-smoke 2026-05-22: switched from default collapse
     to separate + zero spacing so the thead A/B cells can carry
     border-radius (rounded corners). Body rows still read as a
     continuous list because adjacent cells touch at zero spacing.
     Border-bottom on cells remains the visible row divider. */
  border-collapse: separate;
  border-spacing: 0;
  font-size: 0.9375rem;
  margin-bottom: var(--space-xl);
}
/* Table header — only the "Code / Type" label survives. The A and B
   cells render as inert empty space. The big ESTIMATE A blue /
   ESTIMATE B amber cards ABOVE the table already identify which
   column is which; an "A / B" sub-strip was visual redundancy.
   No background, no border, no border-radius — tightened vertical
   padding brings the first item row closer to the top header cards
   (less dead space). Cleanup applied 2026-05-22 (post-Mode-1
   strip removal). */
.comparator-table thead th {
  font-weight: 600;
  font-size: 0.8125rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--color-text-muted);
  padding: var(--space-xs) var(--space-md);
  text-align: left;
  border: none;
  background: transparent;
}
.comparator-table tbody td {
  /* Item 2 post-smoke 2026-05-22: tighter vertical padding + darker
     row divider — pushes the visual from "spaced cards" to
     "scannable list". Horizontal padding unchanged. */
  padding: var(--space-sm) var(--space-md);
  vertical-align: top;
  border-bottom: 1px solid var(--color-border-default);
}
.comparator-table__code {
  width: 18%;
}
.comparator-table__side {
  width: 41%;
}

/* Bundle Estética/UX item 6 Layer 2 sub-step 2b-9 (2026-05-31):
   A / B column labels for the table header — hidden at desktop
   (the big ESTIMATE A / B header cards above the table already
   identify columns), shown at ≤767px where the cards stack
   vertically above and lose proximity to the table rows.
   Colors use the SAME saturated border tokens as the header
   cards (#6FA8D1 / #C9A14A) rather than the pale card
   backgrounds (#EEF4FB / #FBF3E0) so the labels read clearly
   on the white table-header background. Hardcoded literals
   (not tenant brand color) so A stays blue + B stays amber
   regardless of which tenant brand color is set in Settings. */
.comparator-table__side-label {
  display: none;
}
@media (max-width: 767px) {
  .comparator-table__side-label {
    display:        inline;
    font-weight:    700;
    font-size:      0.875rem;
    letter-spacing: 0.04em;
  }
  .comparator-table__side-label--a { color: #4D87B5; }
  .comparator-table__side-label--b { color: #A6852E; }
}
.comparator-row__code {
  font-weight: 600;
  display: block;
}
.comparator-row__type {
  display: block;
  font-size: 0.8125rem;
  color: var(--color-text-muted);
  text-transform: lowercase;
  margin-top: 2px;
}

/* Cell — 2-line compact list. Line 1 = description (no label),
   line 2 = "Qty: N" + tab-gap + "U.Price: $X.XX". Labels are
   muted gray (sibling spans, NOT inside the diff-affected value
   span — so labels stay gray even when the value is red).
   Decimal alignment via tabular-nums + min-width on .price.
   Tighter margin between desc and qty-price line per post-Mode-1
   polish tweak (2026-05-22). */
.comparator-cell__desc {
  font-size: 0.9375rem;
  color: var(--color-text-primary);
  margin-bottom: 2px;
  word-wrap: break-word;
  overflow-wrap: break-word;
}
.comparator-cell__qty-price {
  display: flex;
  align-items: baseline;
  gap: var(--space-xl);  /* tab-like gap between qty-group and price-group */
  flex-wrap: wrap;       /* mobile safety: wraps before overflowing */
  font-size: 0.9375rem;
  font-variant-numeric: tabular-nums;
}
.comparator-cell__qty-group,
.comparator-cell__price-group {
  display: inline-flex;
  align-items: baseline;
  gap: 4px;  /* tight gap between label and value */
}
.comparator-cell__label {
  color: var(--color-text-muted);
  font-size: 0.8125rem;
  font-weight: 400;
}
.comparator-cell__qty {
  /* Left-anchored; no min-width — width hugs the digits. */
}
.comparator-cell__price {
  text-align: right;
  /* Wide enough for prices up to ~$9,999.99 to right-align cleanly.
     Larger amounts wrap (acceptable edge case). */
  min-width: 88px;
}
.comparator-cell__empty-dash { color: var(--color-text-muted); }
.comparator-cell__placeholder {
  color: var(--color-text-muted);
  font-style: italic;
  font-size: 0.875rem;
}

/* CC7: per-field red. Applied to .comparator-cell__desc /
   __qty / __price when the row's `diffs` set contains that field.
   .comparator-diff is the modifier class. */
.comparator-diff {
  color: var(--color-error);
  font-weight: 600;
}
/* Red-diff caption (2026-06-10) — sober explanatory line under the result
   header cards: the highlighted differences in the table are shown in red.
   Breathing room above + below; matches the diff red token. */
.comparator-diff-note {
  margin: var(--space-lg) 0;
  text-align: center;
  font-size: 0.875rem;
  color: var(--color-error);
}
/* The ▲ marker (CC6 / MC7) — color-independent diff signal for
   B/W printing + color-blind users. Inline with the value. */
.comparator-diff__marker {
  display: inline-block;
  margin-left: 4px;
  font-size: 0.75rem;
  color: var(--color-error);
  vertical-align: text-top;
}

/* Cell that has no content on its side (only_a row's B cell, or
   only_b row's A cell). Subtle visual cue without screaming "bug". */
.comparator-table__cell--empty {
  background: var(--color-surface-alt);
}

/* Totals + diff footer. */
.comparator-totals-row td {
  font-weight: 600;
  border-top: 2px solid var(--color-text-primary);
  padding-top: var(--space-md);
}
/* Mixed view-mode notice in place of an apples+oranges combined total
   (one estimate priced, the other neutral/count). Muted + italic — an
   informational caption, not an error. Shared by the table tfoot td and
   the sticky-footer span. */
.comparator-totals-note {
  font-weight: 400;
  font-style: italic;
  color: var(--color-text-secondary);
}
.comparator-diff-row td {
  border-bottom: none;
}
.comparator-diff-row__value {
  font-weight: 700;
  font-size: 1.0625rem;
}

.comparator-empty-state {
  padding: var(--space-2xl);
  text-align: center;
  color: var(--color-text-muted);
}


/* ----- Result view: sticky bar + branding modal -----
   Post-smoke 2026-05-22: replaced the inline branding form with
   a Phase-F/G-style sticky footer (~64px non-duplicating) + a
   modal containing the full branding picker. The result page is
   now JUST the comparison; configuration lives in a focused
   modal that opens on Generate click. Pattern reuses Phase G's
   fetch+Blob+done-state machine, in-modal instead of as a
   separate interstitial page. */

/* Sticky footer — matches the .estimate-footer pattern shape +
   spacing. Anchored at viewport bottom; result page gains
   padding-bottom to compensate. */
.comparator-sticky-footer {
  position: sticky;
  bottom: 0;
  z-index: 10;
  background: white;
  border-top: 1px solid var(--color-border-default);
  padding: var(--space-md) var(--space-xl);
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--space-md);
  margin: var(--space-2xl) calc(var(--space-xl) * -1) 0;
}
.comparator-sticky-footer__totals {
  display: flex;
  align-items: baseline;
  gap: var(--space-sm);
  font-size: 0.9375rem;
  flex-wrap: wrap;
}
.comparator-sticky-footer__total {
  display: inline-flex;
  align-items: baseline;
  gap: 4px;
  font-variant-numeric: tabular-nums;
}
.comparator-sticky-footer__label {
  color: var(--color-text-muted);
  font-size: 0.875rem;
}
.comparator-sticky-footer__sep {
  color: var(--color-text-muted);
}
.comparator-sticky-footer__diff strong {
  font-size: 1rem;
}

/* Below 640px the bar stacks vertically (matching Phase F/G
   mobile collapse pattern). Button goes full-width on top, totals
   below — keeps the primary action thumb-reachable. */
@media (max-width: 640px) {
  .comparator-sticky-footer {
    flex-direction: column-reverse;
    align-items: stretch;
    padding: var(--space-sm) var(--space-md);
    gap: var(--space-sm);
  }
  .comparator-sticky-footer .btn { width: 100%; }
  .comparator-sticky-footer__totals { justify-content: center; }
}

/* Desktop (≥641px): switch from sticky → fixed so the bar is
   pinned to the viewport bottom on short comparisons too, matching
   .estimate-footer / .price-list-footer / project-detail footer.
   Mobile (≤640px) above keeps position: sticky so the existing
   mobile layout is untouched. .comparator-body padding-bottom: 96px
   already clears the bar. */
@media (min-width: 641px) {
  .comparator-sticky-footer {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 50;
    margin: 0;
    box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.06);
  }
}

/* Body padding so the sticky bar doesn't overlap the last row of
   the comparison table. Applied to .comparator-body (the section
   wrapper in templates/comparator/layout.html). */
.comparator-body {
  /* Already has padding-bottom: var(--space-3xl); — overrides any
     conflicting rule below. Bar height ~64px desktop / ~120px mobile;
     space-3xl is 48px so add extra. */
  padding-bottom: 96px;
}


/* ============================================================
   Comparator Mode 2 (Sprint B Phase B.3, 2026-05-23)
   ============================================================

   New surfaces:
     .comparator-toggle               — segmented control (vs-est | vs-plan)
     .comparator-slot--plan           — variant of comparator-slot for the
                                        plan-upload partial
     .comparator-plan-upload          — file-input + filename pill
     .comparator-plan-pl-checkbox     — "Apply a price list?" row
     .comparator-plan-pl-dropdown     — conditional PL select
     .comparator-mode-banner          — "Estimate vs Plan — …" banner
                                        at the top of the result view

   All token-based so the design tokens (color/space/radius) keep
   these aligned with the rest of the app.
*/

/* ----- Segmented toggle (vs-est | vs-plan) ----- */

.comparator-toggle {
  display: inline-flex;
  margin: var(--space-lg) 0;
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius-md, 8px);
  overflow: hidden;
  background: var(--color-surface);
}
.comparator-toggle__option {
  padding: var(--space-sm) var(--space-lg);
  font-size: 0.875rem;
  font-weight: 500;
  color: var(--color-text-muted);
  text-decoration: none;
  border-right: 1px solid var(--color-border-default);
  transition: background 0.15s, color 0.15s;
}
.comparator-toggle__option:last-child { border-right: none; }
.comparator-toggle__option:hover {
  background: var(--color-surface-alt);
  color: var(--color-text-primary);
  text-decoration: none;
}
.comparator-toggle__option.is-active {
  background: var(--color-text-primary);
  color: var(--color-surface);
  font-weight: 600;
}
.comparator-toggle__option:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}


/* ----- Slot B in vs-plan mode ----- */

.comparator-slot--plan {
  /* Inherits .comparator-slot's base layout (dashed border, padding,
     radius). The override is just to align the inner form's spacing
     since the form replaces the slot's content/remove children. */
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}
.comparator-plan-form {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  margin: 0;
}

.comparator-plan-upload {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-sm);
}
.comparator-plan-upload__pick {
  /* The <label> styled as a button — clicking opens the hidden file input. */
  cursor: pointer;
  margin: 0;
}
.comparator-plan-upload__filename {
  display: inline-flex;
  align-items: center;
  padding: var(--space-xs) var(--space-sm);
  background: var(--color-surface-alt);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-sm);
  font-size: 0.8125rem;
  color: var(--color-success);
  font-weight: 500;
  max-width: min(320px, 100%);   /* 320px desktop cap, but never wider than the box (mobile) */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.comparator-plan-upload__clear {
  /* Inherits .btn .btn-secondary .btn-sm — nothing extra needed. */
}

.comparator-plan-pl-checkbox {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  font-size: 0.875rem;
  color: var(--color-text-primary);
  cursor: pointer;
}
.comparator-plan-pl-checkbox input { margin: 0; }

.comparator-plan-pl-dropdown {
  /* Indent under the checkbox to visually anchor it as a sub-option. */
  margin-left: var(--space-lg);
  max-width: 360px;
}


/* ----- Mode banner (result view, Mode 2) ----- */

.comparator-mode-banner {
  background: var(--color-surface-alt);
  border-left: 3px solid var(--color-text-muted);
  padding: var(--space-sm) var(--space-md);
  margin-bottom: var(--space-lg);
  font-size: 0.9375rem;
  color: var(--color-text-primary);
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
}
.comparator-mode-banner__hint {
  display: block;
  margin-top: 2px;
  color: var(--color-text-muted);
  font-size: 0.8125rem;
}


/* ----- Plan analyzing shell (Mode 2) -----
   Reuses .analyzing-* classes from the existing
   estimates/analyzing.html stack (.analyzing-content--has-footer,
   .analyzing-header, .analyzing-card, .analyzing-progress,
   .analyzing-spinner, .progress-bar, .page-status*,
   .analyzing-footer). No comparator-specific overrides needed —
   the SSE client pattern is shared. */


/* ----- Comparator PDF modal ----- */

.comparator-pdf-dialog {
  border: none;
  border-radius: var(--radius-lg);
  padding: 0;
  max-width: 560px;
  width: 92%;
  background: white;
  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15);
}
.comparator-pdf-dialog::backdrop {
  background: rgba(0, 0, 0, 0.4);
}
.comparator-pdf-dialog__inner {
  padding: var(--space-xl);
  position: relative;
}
.comparator-pdf-dialog__inner--centered {
  text-align: center;
  padding: var(--space-2xl) var(--space-xl);
}
.comparator-pdf-dialog__close {
  position: absolute;
  top: var(--space-md);
  right: var(--space-md);
  width: 32px;
  height: 32px;
  font-size: 1.25rem;
}
.comparator-pdf-dialog__title {
  margin: 0 0 var(--space-md) 0;
  font-size: 1.125rem;
  font-weight: 600;
}
.comparator-pdf-dialog__intro,
.comparator-pdf-dialog__hint {
  color: var(--color-text-muted);
  font-size: 0.9375rem;
  margin: 0 0 var(--space-md) 0;
}

/* Form state — branding picker inside the modal. */
.comparator-pdf-dialog__branding {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-md);
  margin-bottom: var(--space-lg);
}
.comparator-pdf-dialog__field {
  display: flex;
  flex-direction: column;
  gap: 4px;
  font-size: 0.875rem;
  color: var(--color-text-muted);
}
.comparator-pdf-dialog__field select {
  padding: var(--space-sm) var(--space-md);
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius-sm);
  background: white;
  font-size: 0.9375rem;
  color: var(--color-text-primary);
}
.comparator-pdf-dialog__notes {
  margin-bottom: var(--space-lg);
}
.comparator-pdf-dialog__notes-label {
  display: block;
  font-size: 0.875rem;
  color: var(--color-text-muted);
  margin-bottom: var(--space-sm);
}
.comparator-pdf-dialog__actions {
  display: flex;
  gap: var(--space-sm);
  justify-content: flex-end;
  flex-wrap: wrap;
}
.comparator-pdf-dialog__actions--centered { justify-content: center; }

/* Generating state — spinner. Reuses .generating-page__spinner
   animation from Phase G. */
.comparator-pdf-dialog__spinner {
  font-size: 2.5rem;
  display: inline-block;
  animation: generating-spin 1.2s linear infinite;
  color: var(--color-text-muted);
  margin-bottom: var(--space-md);
}

/* Done state — success checkmark. */
.comparator-pdf-dialog__check {
  font-size: 3rem;
  line-height: 1;
  color: var(--color-success, #16745F);
  margin-bottom: var(--space-md);
  display: inline-block;
}

/* Error state — exclamation badge. */
.comparator-pdf-dialog__error-icon {
  font-size: 2.25rem;
  line-height: 1;
  color: var(--color-error);
  background: var(--color-error-bg);
  width: 64px;
  height: 64px;
  border-radius: 999px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-weight: 700;
  margin-bottom: var(--space-md);
}

/* Close button on the done state — large primary, anchors the
   completion moment (matches Phase G generating.html done state). */
.comparator-pdf-dialog__close-btn {
  margin-top: var(--space-md);
  min-width: 200px;
  height: 44px;
  padding: 0 var(--space-xl);
}

@media (max-width: 480px) {
  .comparator-pdf-dialog__branding { grid-template-columns: 1fr; }
  .comparator-pdf-dialog__close-btn { width: 100%; }
}

/* Tablet + desktop (≥481px): the branding picker stays as a 2-col
   grid here, which is where the latent overflow lived — grid items
   default to min-width: auto, so a <select> whose intrinsic width
   (driven by its longest option text) exceeded the 1fr track would
   push the whole modal past its 560px max-width and trigger a
   horizontal scrollbar inside the dialog. min-width: 0 on the grid
   item lets it shrink; width/max-width: 100% on the select pins it
   to its cell. The notes-row name-ellipsis covers the same trap for
   long note titles. Mobile (≤480px) above already collapses the
   grid to 1fr so the overflow path doesn't exist there — these
   rules don't fire below 481px. */
@media (min-width: 481px) {
  .comparator-pdf-dialog__field { min-width: 0; }
  .comparator-pdf-dialog__field select { width: 100%; max-width: 100%; }
  .notes-selected__name {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
}


/* =========================================================
   NOTIFICATION BELL — Support Tickets Sprint Commit A
   ========================================================= */

/* The bell is a <details> element so native open/close handles state.
   JS only wires click-outside, ESC, and the mark-read fetch.
   Mounted in layout/app.html between the usage indicator and the
   user-name span. */

.notification-bell {
  position: relative;
  /* <details> default browser behavior renders a list-marker triangle
     on <summary>. Hide it — the bell icon IS the affordance. */
}

.notification-bell > summary {
  list-style: none;
}
.notification-bell > summary::-webkit-details-marker {
  display: none;
}

.notification-bell__trigger {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  padding: var(--space-xs) var(--space-sm);
  color: var(--color-text-secondary);
  border-radius: var(--radius-sm);
  transition: color 120ms ease, background 120ms ease;
  user-select: none;
}

.notification-bell__trigger:hover,
.notification-bell[open] .notification-bell__trigger {
  color: var(--color-text-primary);
  background: var(--color-surface-alt);
}

.notification-bell__icon {
  display: block;
  vertical-align: middle;
}

.notification-bell__badge {
  position: absolute;
  top: 2px;
  right: 2px;
  min-width: 16px;
  height: 16px;
  padding: 0 4px;
  background: var(--color-accent-vibrant);
  color: var(--color-text-inverse);
  font-size: 10px;
  font-weight: 700;
  line-height: 16px;
  text-align: center;
  border-radius: var(--radius-pill);
  pointer-events: none;
  box-shadow: 0 0 0 2px var(--color-bg);
}

/* The dropdown is absolutely positioned under the bell trigger.
   Use a high z-index so it floats above page content but stays under
   any modal <dialog> (which the browser puts on its own top layer). */
.notification-bell__dropdown {
  position: absolute;
  top: calc(100% + var(--space-xs));
  right: 0;
  width: 360px;
  max-width: calc(100vw - var(--space-lg) * 2);
  max-height: 70vh;
  overflow-y: auto;
  background: var(--color-surface);
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius-md, 8px);
  box-shadow: var(--shadow-lg);
  z-index: 50;
}

/* Estética/UX item 6 — mobile bell dropdown anchoring (2026-06-01).
   At ≤768px the bell sits ~64px from the right viewport edge; the
   base `position: absolute; right: 0; width: 360px` anchors the
   dropdown to the bell, producing a left-edge clip ≈ −20px (verified
   empirically via the offenders query: left=-20, right=323 at vw=375
   with bell at right=311). Switch to `position: fixed` anchored to
   the viewport on mobile — decouples from the bell's positioning
   context entirely. Topbar is 56px tall (see .app-topbar `height: 56px`
   above); 8px gutter on each side keeps the dropdown clear of the
   edges. Desktop (≥769px) keeps the original absolute/right:0/360px
   behavior unchanged. */
@media (max-width: 768px) {
  .notification-bell__dropdown {
    position:   fixed;
    top:        calc(56px + var(--space-xs));
    left:       var(--space-sm);
    right:      var(--space-sm);
    width:      auto;
    max-width:  none;
    max-height: calc(100vh - 56px - var(--space-lg));
  }
}

.notification-bell__header {
  padding: var(--space-md) var(--space-lg);
  border-bottom: 1px solid var(--color-border-subtle);
  position: sticky;
  top: 0;
  background: var(--color-surface);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm);
}
.notification-bell__close {
  border: none;
  background: none;
  cursor: pointer;
  font-size: 1.5rem;
  line-height: 1;
  color: var(--color-text-muted);
  padding: 0 var(--space-xs);
  border-radius: 4px;
  flex-shrink: 0;
}
.notification-bell__close:hover { color: var(--color-text-primary); }

/* Seeding overlay (2026-06-30) — register/verify "setting up your workspace". */
.seeding-overlay {
  position: fixed;
  inset: 0;
  z-index: 2000;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(255, 255, 255, 0.92);
}
.seeding-overlay__box { text-align: center; }
.seeding-overlay__spinner {
  width: 40px;
  height: 40px;
  margin: 0 auto var(--space-md);
  border: 3px solid var(--color-border-default);
  border-top-color: var(--color-text-primary);
  border-radius: 50%;
  animation: seeding-spin 0.8s linear infinite;
}
@keyframes seeding-spin { to { transform: rotate(360deg); } }
.seeding-overlay__text {
  margin: 0;
  font-size: 1rem;
  font-weight: 500;
  color: var(--color-text-primary);
}

.notification-bell__header-title {
  font-size: 0.875rem;
  font-weight: 600;
  color: var(--color-text-primary);
}

.notification-bell__list {
  list-style: none;
  margin: 0;
  padding: 0;
}

.notification-bell__item {
  padding: var(--space-md) var(--space-lg);
  border-bottom: 1px solid var(--color-border-subtle);
  /* Per-element colors set explicitly below — title + body stay
     dark/legible regardless of read state (ST-NEW-8). Read/unread
     distinction lives ENTIRELY in the background tint + the accent
     dot on the title; dimming the body text made it washed-out and
     hard to read (caught in Commit B smoke). */
}

.notification-bell__item:last-child {
  border-bottom: none;
}

/* Unread = surface-alt background + a 3px accent bar on the left edge.
   The old ::before accent dot was dropped (the type-icon now owns the
   left gutter), but the background tint alone read too subtly — so the
   left bar restores a clear unread signal that coexists with the icon
   (bar is on the item edge; icon sits inside next to the title). The
   bar is an inset box-shadow, NOT border-left, so read + unread rows
   keep identical text alignment (a real border would shift content 3px).
   No color change on the text itself — readability stays constant. */
.notification-bell__item--unread {
  background: var(--color-surface-alt);
  box-shadow: inset 3px 0 0 0 var(--color-accent-vibrant);
}

/* Title row: leading semantic glyph + title text (Notification Icons
   mini-sprint). The icon owns the left gutter; align-items:flex-start
   keeps it top-aligned with the first line of a wrapping title. */
.notification-bell__item-head {
  display: flex;
  align-items: flex-start;
  gap: var(--space-sm);
}

.notification-bell__title {
  font-size: 0.875rem;
  font-weight: 600;
  line-height: 1.3;
  margin-bottom: var(--space-xs);
  color: var(--color-text-primary);
}

/* Semantic notification glyph. Color is driven by currentColor so a
   single `color:` line per palette recolors the inline SVG's fill +
   stroke. Same --color-* tokens the admin status dots use. The old
   unread ::before accent dot was REMOVED — the glyph now occupies the
   left gutter, and unread state reads from the item's background tint
   (.notification-bell__item--unread) alone. */
.notification-bell__msg-icon {
  flex-shrink: 0;
  display: inline-flex;
  line-height: 0;
  margin-top: 1px; /* optical align with title cap height */
}

.notification-bell__msg-icon-svg {
  width: 16px;
  height: 16px;
}

.notification-bell__msg-icon--info    { color: var(--color-info); }
.notification-bell__msg-icon--warning { color: var(--color-warning); }
.notification-bell__msg-icon--success { color: var(--color-success); }
.notification-bell__msg-icon--error   { color: var(--color-error); }
.notification-bell__msg-icon--muted   { color: var(--color-text-muted); }

.notification-bell__body {
  font-size: 0.8125rem;
  line-height: 1.45;
  color: var(--color-text-primary);
  white-space: pre-wrap;
  word-break: break-word;
}

.notification-bell__links {
  margin-top: var(--space-sm);
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
}

.notification-bell__link {
  /* Use existing .btn-secondary .btn-sm tokens; nothing extra needed. */
  text-decoration: none;
}

.notification-bell__time {
  display: block;
  margin-top: var(--space-xs);
  font-size: 0.75rem;
  color: var(--color-text-muted);
}

.notification-bell__empty {
  padding: var(--space-xl) var(--space-lg);
  text-align: center;
  font-size: 0.8125rem;
  color: var(--color-text-muted);
}

@media (max-width: 480px) {
  .notification-bell__dropdown {
    width: calc(100vw - var(--space-md) * 2);
    right: calc(var(--space-md) * -1);
  }
}


/* =========================================================
   ADMIN AREA — Support Tickets Sprint Commit A (extended in C)
   ========================================================= */

/* Admin section reuses the settings sidebar pattern (settings-shell
   + settings-tabs) but with its own class names so the two surfaces
   can drift independently. */

.admin-shell {
  display: grid;
  grid-template-columns: 220px 1fr;
  gap: var(--space-2xl);
  margin-top: var(--space-xl);
}

.admin-tabs {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
}

.admin-tab {
  padding: var(--space-sm) var(--space-md);
  color: var(--color-text-secondary);
  text-decoration: none;
  border-radius: var(--radius-sm);
  font-size: 0.875rem;
  font-weight: 500;
  transition: background 120ms ease, color 120ms ease;
}

.admin-tab:hover {
  color: var(--color-text-primary);
  background: var(--color-surface-alt);
}

.admin-tab.is-active {
  color: var(--color-text-primary);
  background: var(--color-surface-alt);
  font-weight: 600;
}

.admin-body {
  min-width: 0;
}

/* Admin form layout — send-notification form uses these. */
.admin-form {
  max-width: 640px;
}

.admin-form__field {
  margin-bottom: var(--space-lg);
}

.admin-form__label {
  display: block;
  margin-bottom: var(--space-xs);
  font-size: 0.875rem;
  font-weight: 600;
  color: var(--color-text-primary);
}

.admin-form__input,
.admin-form__textarea,
.admin-form__select {
  width: 100%;
  padding: var(--space-sm) var(--space-md);
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius-sm);
  font-size: 0.875rem;
  font-family: inherit;
  background: var(--color-surface);
  color: var(--color-text-primary);
}

.admin-form__textarea {
  min-height: 140px;
  resize: vertical;
}

.admin-form__hint {
  margin-top: var(--space-xs);
  font-size: 0.75rem;
  color: var(--color-text-muted);
}

.admin-form__error {
  margin-top: var(--space-xs);
  font-size: 0.8125rem;
  color: var(--color-error);
}

.admin-form__scope-radios,
.admin-form__link-rows {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}

.admin-form__link-row {
  display: grid;
  grid-template-columns: 1fr 2fr auto;
  gap: var(--space-sm);
  align-items: center;
}

.admin-form__link-add {
  align-self: flex-start;
}

/* Icon picker — Notification Icons mini-sprint. Radio-grid of selectable
   glyph tiles, same checked-state idiom as .contact-form__chip
   (visually-hidden radio + :has(:checked) highlight). Each tile shows
   the glyph in its own semantic color + the key name. */
.admin-form__icon-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
  gap: var(--space-sm);
  border: none;
  padding: 0;
  margin: 0;
}

.admin-form__icon-option {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-start;
  gap: var(--space-xs);
  padding: var(--space-sm);
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius-md);
  cursor: pointer;
  text-align: center;
  transition: background 0.12s ease, border-color 0.12s ease;
}

.admin-form__icon-option input[type='radio'] {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}

.admin-form__icon-option:hover {
  background: var(--color-surface-alt);
}

.admin-form__icon-option:has(input[type='radio']:checked) {
  border-color: var(--color-accent);
  box-shadow: inset 0 0 0 1px var(--color-accent);
  background: var(--color-surface-alt);
}

.admin-form__icon-option:has(input[type='radio']:focus-visible) {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

/* Picker tiles render the glyph a touch larger than the 16px bell size
   for legibility while scanning the grid. */
.admin-form__icon-option .notification-bell__msg-icon-svg {
  width: 22px;
  height: 22px;
}

.admin-form__icon-default {
  font-size: 1.25rem;
  line-height: 0;
  height: 22px;
  display: inline-flex;
  align-items: center;
  color: var(--color-text-muted);
}

.admin-form__icon-name {
  font-size: 0.6875rem;
  line-height: 1.2;
  color: var(--color-text-muted);
  text-transform: capitalize;
}

.admin-form__actions {
  display: flex;
  gap: var(--space-sm);
  margin-top: var(--space-xl);
}

@media (max-width: 768px) {
  .admin-shell {
    grid-template-columns: 1fr;
  }
  .admin-tabs {
    flex-direction: row;
    flex-wrap: wrap;
  }
  .admin-form__link-row {
    grid-template-columns: 1fr;
  }
}

/* Topbar Admin nav link gating — Commit A wires the visible link;
   styling reuses .app-topbar__navlink so nothing else needed. */


/* =========================================================
   CONTACT FORM — Support Tickets Sprint Commit B
   ========================================================= */

.contact-form {
  max-width: 640px;
}

.contact-form__field {
  margin-bottom: var(--space-xl);
}

.contact-form__label,
.contact-form__legend {
  display: block;
  margin-bottom: var(--space-sm);
  font-size: 0.875rem;
  font-weight: 600;
  color: var(--color-text-primary);
}

.contact-form__chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
  border: none;
  padding: 0;
  margin: 0;
}

.contact-form__chip {
  position: relative;
  display: inline-flex;
  align-items: center;
  padding: var(--space-sm) var(--space-lg);
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius-pill);
  cursor: pointer;
  font-size: 0.875rem;
  background: var(--color-surface);
  transition: background 120ms ease, border-color 120ms ease, color 120ms ease;
  user-select: none;
}

.contact-form__chip input[type='radio'] {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}

.contact-form__chip:hover {
  background: var(--color-surface-alt);
}

.contact-form__chip:has(input[type='radio']:checked) {
  background: var(--color-accent);
  color: var(--color-text-inverse);
  border-color: var(--color-accent);
}

.contact-form__chip:has(input[type='radio']:focus-visible) {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

.contact-form__textarea {
  width: 100%;
  padding: var(--space-sm) var(--space-md);
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius-sm);
  font-size: 0.875rem;
  font-family: inherit;
  background: var(--color-surface);
  color: var(--color-text-primary);
  min-height: 180px;
  resize: vertical;
}

.contact-form__counter {
  margin-top: var(--space-xs);
  font-size: 0.75rem;
  color: var(--color-text-muted);
  text-align: right;
}

.contact-form__hint {
  margin-top: var(--space-xs);
  font-size: 0.75rem;
  color: var(--color-text-muted);
}

.contact-form__actions {
  margin-top: var(--space-xl);
}


/* =========================================================
   ADMIN CONTACT VIEW — Support Tickets Sprint Commit C
   ========================================================= */

/* Status dots (STC-4 lock): one colored dot per status. Reuses
   existing VDP semantic-color tokens so future palette tweaks
   propagate automatically. */
.status-dot {
  display: inline-block;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  vertical-align: middle;
  margin-right: 4px;
  background: var(--color-text-muted);  /* default fallback */
}
.status-dot--new         { background: var(--color-info); }
.status-dot--in_progress { background: var(--color-warning); }
.status-dot--resolved    { background: var(--color-success); }
.status-dot--closed      { background: var(--color-text-muted); }

/* Admin sidebar badge for the "Contact messages" link's new-count.
   Same shape as the bell badge but admin-tab-specific positioning. */
.admin-tab__badge {
  display: inline-block;
  min-width: 18px;
  height: 18px;
  padding: 0 6px;
  margin-left: 6px;
  background: var(--color-accent-vibrant);
  color: var(--color-text-inverse);
  font-size: 11px;
  font-weight: 700;
  line-height: 18px;
  text-align: center;
  border-radius: var(--radius-pill);
  vertical-align: 1px;
}

/* Filter bar above the list. Chips + dropdowns + search + sort. */
.admin-list-filters {
  margin-bottom: var(--space-xl);
}

.admin-list-filters__chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
  margin-bottom: var(--space-md);
}

.admin-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: var(--space-xs) var(--space-md);
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius-pill);
  font-size: 0.8125rem;
  color: var(--color-text-secondary);
  text-decoration: none;
  background: var(--color-surface);
  transition: background 120ms ease, color 120ms ease, border-color 120ms ease;
}
.admin-chip:hover {
  background: var(--color-surface-alt);
  color: var(--color-text-primary);
}
.admin-chip.is-active {
  background: var(--color-accent);
  color: var(--color-text-inverse);
  border-color: var(--color-accent);
}
.admin-chip.is-active .status-dot {
  /* Override the colored dot when chip is dark-active so the dot
     stays visible against the dark background. */
  box-shadow: 0 0 0 1px var(--color-text-inverse);
}

.admin-chip__count {
  font-size: 0.6875rem;
  opacity: 0.75;
  padding-left: 4px;
  border-left: 1px solid currentColor;
  margin-left: 4px;
}

.admin-list-filters__row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
  align-items: center;
}
.admin-list-filters__row .admin-form__select,
.admin-list-filters__row .admin-form__input {
  width: auto;
  min-width: 160px;
  flex: 0 1 auto;
}
.admin-list-filters__row input[type='search'] {
  flex: 1 1 220px;
}

.admin-list-banner {
  padding: var(--space-sm) var(--space-md);
  background: var(--color-warning-bg);
  border: 1px solid var(--color-warning);
  border-radius: var(--radius-sm);
  color: var(--color-warning);
  font-size: 0.8125rem;
  margin-bottom: var(--space-md);
}

.admin-list-empty {
  padding: var(--space-2xl);
  text-align: center;
  color: var(--color-text-muted);
  background: var(--color-surface-alt);
  border-radius: var(--radius-sm);
}

/* Admin list table. */
.admin-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.875rem;
}
.admin-table th {
  text-align: left;
  padding: var(--space-sm) var(--space-md);
  background: var(--color-surface-alt);
  border-bottom: 1px solid var(--color-border-default);
  font-weight: 600;
  color: var(--color-text-secondary);
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.admin-table td {
  padding: var(--space-md);
  border-bottom: 1px solid var(--color-border-subtle);
  color: var(--color-text-primary);
  vertical-align: top;
}
.admin-table__row:hover {
  background: var(--color-surface-alt);
}
.admin-table__sub {
  font-size: 0.75rem;
  color: var(--color-text-muted);
  margin-top: 2px;
}
.admin-table__preview {
  color: var(--color-text-secondary);
  font-size: 0.8125rem;
  line-height: 1.4;
}

/* Detail page. */
.admin-back-link {
  display: inline-block;
  margin-bottom: var(--space-md);
  font-size: 0.875rem;
  color: var(--color-text-secondary);
  text-decoration: none;
}
.admin-back-link:hover {
  color: var(--color-text-primary);
}

.admin-detail__header {
  margin-bottom: var(--space-xl);
}
.admin-detail__from {
  font-size: 0.875rem;
  font-weight: 400;
  color: var(--color-text-muted);
}

.admin-detail__meta {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: var(--space-md);
  margin-top: var(--space-md);
}
.admin-detail__meta > div {
  font-size: 0.8125rem;
}
.admin-detail__meta dt {
  color: var(--color-text-muted);
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  margin-bottom: 2px;
}
.admin-detail__meta dd {
  margin: 0;
  color: var(--color-text-primary);
}
.admin-detail__meta code {
  font-family: var(--font-mono);
  font-size: 0.75rem;
  color: var(--color-text-secondary);
}

.admin-detail__message-block {
  margin-bottom: var(--space-xl);
  padding: var(--space-lg);
  background: var(--color-surface-alt);
  border-radius: var(--radius-md, 8px);
}
.admin-detail__message-block h3 {
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--color-text-muted);
  margin-bottom: var(--space-sm);
  font-weight: 600;
}
.admin-detail__message-body {
  white-space: pre-wrap;
  word-break: break-word;
  line-height: 1.5;
  color: var(--color-text-primary);
}

.admin-detail__status-form {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  margin-bottom: var(--space-xl);
  padding-bottom: var(--space-lg);
  border-bottom: 1px solid var(--color-border-subtle);
}

.admin-detail__reply-hint {
  font-size: 0.8125rem;
  color: var(--color-text-secondary);
  background: var(--color-info-bg);
  padding: var(--space-md);
  border-radius: var(--radius-sm);
  line-height: 1.5;
}
.admin-detail__reply-hint code {
  font-family: var(--font-mono);
  font-size: 0.75rem;
  background: var(--color-bg);
  padding: 2px 4px;
  border-radius: 3px;
}


/* =========================================================
   SUPPORT-LINK SHORTCUT — admin send-notification form
   Mini-sprint 2026-05-24 (SL-LOCK-1..5).
   ========================================================= */

.admin-form__shortcut {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  margin-bottom: var(--space-sm);
  padding: var(--space-xs) 0;
  font-size: 0.875rem;
  color: var(--color-text-primary);
  cursor: pointer;
  user-select: none;
}

.admin-form__shortcut input[type='checkbox'] {
  margin: 0;
  cursor: pointer;
}

.admin-form__shortcut code {
  font-family: var(--font-mono);
  font-size: 0.75rem;
  background: var(--color-surface-alt);
  padding: 2px 4px;
  border-radius: 3px;
  color: var(--color-text-secondary);
}

/* SL-LOCK-2 inline hint when all 3 manual rows are full. Warning
   palette + thin left rule reads as "advisory" rather than "error". */
.admin-form__shortcut-hint {
  margin: 0 0 var(--space-md) 0;
  padding: var(--space-xs) var(--space-sm);
  background: var(--color-warning-bg);
  border-left: 3px solid var(--color-warning);
  color: var(--color-warning);
  font-size: 0.8125rem;
  border-radius: var(--radius-sm);
}

/* SL-LOCK-3 visual lock on the managed row — inputs read as
   "not user-editable while the checkbox is checked". */
.admin-form__link-row.is-managed input {
  background: var(--color-surface-alt);
  color: var(--color-text-secondary);
  cursor: not-allowed;
}

/* Hide the Remove button on the managed row — the checkbox is the
   single source of state, so removing the row should happen via
   uncheck, not via Remove. Eliminates the foot-gun of
   Remove-on-managed-row leaving the checkbox stuck checked. */
.admin-form__link-row.is-managed [data-link-remove] {
  display: none;
}


/* ===============================================
   Bundle A #9 (2026-05-28) — inline asset-create
   modal used on associate.html + associate_price_
   list.html. Single <dialog> reused for header /
   footer / note via JS-driven mutation of form
   action + asset_type + title + logo-slot
   visibility. Lightweight design — content_html
   is a plain <textarea>, no Trix vendor load on
   associate pages.
   =============================================== */

.asset-create-modal {
  width:         min(640px, 92vw);
  max-width:     640px;
  max-height:    90vh;
  padding:       0;
  border:        1px solid var(--color-border-default);
  border-radius: var(--radius);
  background:    var(--color-surface);
  box-shadow:    var(--shadow-lg);
  color:         var(--color-text-primary);
  font:          inherit;
}
.asset-create-modal::backdrop {
  background: var(--color-overlay-dark);
}

/* Form is a flex column so the header pinned-top, body scrolls,
   footer pinned-bottom — keeps Create/Cancel always reachable even
   for long content. */
.asset-create-modal__form {
  display:        flex;
  flex-direction: column;
  max-height:     90vh;
}

.asset-create-modal__header {
  display:         flex;
  align-items:     center;
  justify-content: space-between;
  padding:         var(--space-lg) var(--space-xl);
  border-bottom:   1px solid var(--color-border-subtle);
  gap:             var(--space-md);
}
.asset-create-modal__header h2 {
  margin:      0;
  font-size:   1.125rem;
  font-weight: 600;
}

/* × close button — same affordance as other dismissable modals.
   No background, hover darkens. */
.asset-create-modal__close {
  background: transparent;
  border:     0;
  font-size:  1.5rem;
  line-height: 1;
  cursor:     pointer;
  color:      var(--color-text-muted);
  padding:    0 var(--space-xs);
  border-radius: var(--radius-sm);
}
.asset-create-modal__close:hover  { color: var(--color-text-primary); }
.asset-create-modal__close:focus-visible {
  outline: 2px solid var(--color-text-primary);
  outline-offset: 2px;
}

/* Body scrolls when content_html + logo slot together exceed
   available height (e.g. on shorter viewports). */
.asset-create-modal__body {
  padding:        var(--space-lg) var(--space-xl);
  overflow-y:     auto;
  display:        flex;
  flex-direction: column;
  gap:            var(--space-lg);
}

.asset-create-modal__textarea {
  font-family: inherit;
  font-size:   0.9375rem;
  width:       100%;
  min-height:  120px;
  padding:     var(--space-sm) var(--space-md);
  border:      1px solid var(--color-border-default);
  border-radius: var(--radius-sm);
  background:  white;
  color:       var(--color-text-primary);
  resize:      vertical;
  line-height: 1.4;
  box-sizing:  border-box;
}
.asset-create-modal__textarea:focus {
  outline: 2px solid var(--color-text-primary);
  outline-offset: -1px;
}

.asset-create-modal__footer {
  display:         flex;
  justify-content: flex-end;
  align-items:     center;
  gap:             var(--space-sm);
  padding:         var(--space-md) var(--space-xl);
  border-top:      1px solid var(--color-border-subtle);
  background:      var(--color-bg);
}



/* === PHOTO INPUT — VALIDATION SURFACE (Commit 3) === */
/* Mobile-first: stacked, photo pinned at 35vh (always glanceable while
   scrolling rows), sticky confirm at the bottom. Desktop >=768px:
   two-pane grid, sticky photo left ~45%. */

.pv-layout {
  display: block;
}

.pv-photo {
  position: sticky;
  top: 0;
  z-index: 5;
  height: 35vh;
  background: var(--color-surface-alt, #FAFAFA);
  border-bottom: 1px solid var(--color-border-default, #E5E5E5);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: var(--space-sm, 8px);
}

.pv-photo__link { display: flex; max-height: 100%; max-width: 100%; }

.pv-photo__img {
  max-height: 27vh;
  max-width: 100%;
  object-fit: contain;
  border-radius: var(--radius-sm, 4px);
}

.pv-photo__hint {
  margin: var(--space-xs, 4px) 0 0;
  font-size: 0.78rem;
  color: var(--color-text-muted, #777);
}
.pv-photo__hint-desktop { display: none; }

.pv-main { padding: var(--space-md, 12px); }

.pv-header__subtitle { color: var(--color-text-muted, #777); }

.pv-review-chip {
  display: inline-block;
  margin-top: var(--space-xs, 4px);
  /* Fix 2 (2026-06-07): air BELOW the chip on desktop too (before the
     Category-for-this-list control). Consolidated HERE in the base rule
     — one declaration serves both viewports; the duplicate mobile-block
     line was removed (same selector, same property, same value). */
  margin-bottom: var(--space-md, 12px);
  padding: 2px 10px;
  border-radius: 999px;
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--color-accent-vibrant, #FF3158);
  background: color-mix(in srgb, var(--color-accent-vibrant, #FF3158) 10%, transparent);
}

.pv-table .form-input { width: 100%; min-width: 0; }
.pv-th-qty { width: 5.5rem; }
.pv-th-actions { width: 5rem; }
.pv-input-qty { max-width: 5rem; }

/* Uncertain = AMBER everywhere (2026-06-07): medium attention — "the
   photo reading needs your eye". Conflict = RED (--color-error): high
   attention — collides with existing data. The two triage levels must
   be unmistakable at a glance (founder smoke caught the old coral/red
   pair reading as the same). Same warning-token convention as the
   sparse-page dot. */
.pv-row--uncertain {
  border-left: 3px solid var(--color-warning, #B45309);
  background: color-mix(in srgb, var(--color-warning, #B45309) 6%, transparent);
}

.pv-flag-uncertain {
  color: var(--color-warning, #B45309);
  font-size: 0.82rem;
  font-weight: 600;
  display: inline-block;
}

.pv-badge-nomatch {
  display: inline-block;
  font-size: 0.78rem;
  color: var(--color-text-muted, #777);
  border: 1px solid var(--color-border-default, #E5E5E5);
  border-radius: var(--radius-sm, 4px);
  padding: 1px 6px;
}

.pv-cell-actions { white-space: nowrap; text-align: right; }
.pv-btn-ok,
.pv-btn-del {
  border: 1px solid var(--color-border-default, #E5E5E5);
  background: var(--color-bg, #fff);
  border-radius: var(--radius-sm, 4px);
  padding: 2px 8px;
  cursor: pointer;
  font-size: 0.95rem;
  line-height: 1.4;
}
.pv-btn-ok { color: var(--color-success, #16745F); }
.pv-btn-del { color: var(--color-text-muted, #777); }
.pv-btn-ok:hover, .pv-btn-del:hover { background: var(--color-border-default, #E5E5E5); }

/* Sticky confirm bar (mobile) — house sticky-footer posture. */
.pv-confirm {
  position: sticky;
  bottom: 0;
  background: var(--color-bg, #fff);
  border-top: 1px solid var(--color-border-subtle, #eee);
  padding: var(--space-sm, 8px) 0 var(--space-md, 12px);
  margin-top: var(--space-lg, 20px);
}
.pv-confirm__actions {
  display: flex;
  align-items: center;
  gap: var(--space-md, 12px);
}
.pv-discard {
  color: var(--color-text-muted, #777);
  text-decoration: none;
}
.pv-discard:hover { text-decoration: underline; }
.pv-confirm__error { color: var(--color-error, #b3261e); margin: var(--space-xs, 4px) 0 0; }

/* Fullscreen photo overlay (mobile zoom) — native pinch via touch-action. */
.pv-overlay {
  position: fixed;
  inset: 0;
  z-index: 1000;
  background: rgba(0, 0, 0, 0.92);
  display: flex;
  align-items: center;
  justify-content: center;
  touch-action: pinch-zoom;
  overflow: auto;
}
/* Honest overlay fix (Phase 1, 2026-06-07): photo-anchored wrapper +
   top-center thumb-size close. Plain CSS a healthy page would use —
   deliberately NO visual-viewport pin / no expanded-canvas armor
   (the isolation experiment depends on that). */
.pv-overlay__imgwrap {
  position: relative;          /* the ✕ anchors to the PHOTO, not the screen */
  display: flex;
  flex: none;
  min-width: 0;                /* flex min-content trap */
  max-width: 100vw;
  max-height: 100vh;
}
.pv-overlay__img { max-width: 100vw; max-height: 100vh; object-fit: contain; }
.pv-overlay__close {
  position: absolute;
  top: 16px;                   /* clearly ON the image, top-center (founder pick) */
  left: 50%;
  transform: translateX(-50%);
  width: 56px;
  height: 56px;
  border: 1px solid rgba(0, 0, 0, 0.15);
  border-radius: 50%;
  /* Any-photo contrast (2026-06-07): the dark scrim vanished over dark
     photo regions on the real phone. White circle + dark glyph + shadow
     reads over BOTH dark and light photos. */
  background: rgba(255, 255, 255, 0.92);
  color: #111;
  font-size: 2rem;
  line-height: 1;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.45);
  cursor: pointer;
}

/* Desktop >=768px: two-pane, photo sticky in the left column. */
@media (min-width: 768px) {
  .pv-layout {
    display: grid;
    grid-template-columns: 45% minmax(0, 1fr);
    gap: var(--space-xl, 28px);
    align-items: start;
  }
  .pv-photo {
    position: sticky;
    top: var(--space-md, 12px);
    height: auto;
    max-height: calc(100vh - 2 * var(--space-md, 12px));
    border: 1px solid var(--color-border-default, #E5E5E5);
    border-radius: var(--radius-md, 8px);
    padding: var(--space-md, 12px);
  }
  .pv-photo__img { max-height: calc(100vh - 120px); }
  .pv-photo__hint-desktop { display: inline; }
  .pv-photo__hint-mobile { display: none; }
  .pv-main { padding: 0; }
  .pv-confirm { position: static; border-top: none; }
}

/* --- Photo Input Commit 4: PL shape additions --- */
.pv-bulk-type {
  display: flex;
  align-items: center;
  gap: var(--space-sm, 8px);
  margin-bottom: var(--space-sm, 8px);
}
.pv-bulk-type__label { font-weight: 600; font-size: 0.9rem; }

.type-picker--row { padding: 2px 8px; font-size: 0.85rem; }

.pv-th-price { width: 7rem; }
.pv-th-type { width: 9rem; }
.pv-input-price { max-width: 7rem; }

/* Counting list (count view mode) — hide the price column across the three
   photo-validate surfaces. Driven by the .price-list-view-scope[data-view-mode]
   wrapper (B unified count + neutral onto one mechanism, replacing the old
   pv-count class). The price INPUTS stay in the DOM (the JS binds them); they
   arrive empty and the import route forces each to 1. Neutral keeps the column
   but the global .money-sym / .vm-label rules de-monetize it. */
[data-view-mode="count"] .pv-th-price,
[data-view-mode="count"] .pv-cell-price,
[data-view-mode="count"] .pv-mrow__price,
[data-view-mode="count"] .pv-sheet-field--price { display: none; }

.pv-badge-dup {
  display: inline-block;
  font-size: 0.78rem;
  font-weight: 600;
  color: var(--color-error, #b3261e);
  border: 1px solid var(--color-error, #b3261e);
  border-radius: var(--radius-sm, 4px);
  padding: 1px 6px;
}

.pv-row-error {
  display: block;
  font-size: 0.78rem;
  color: var(--color-error, #b3261e);
  margin-top: 2px;
}

.pv-typemodal {
  position: fixed;
  inset: 0;
  z-index: 1003;   /* above the bottom sheet (1002) — the sheet's Type
                      field opens this picker on top of it (2026-06-07) */
  background: rgba(0, 0, 0, 0.35);
  display: flex;
  align-items: center;
  justify-content: center;
}
.pv-typemodal__panel {
  background: var(--color-bg, #fff);
  border-radius: var(--radius-md, 8px);
  box-shadow: var(--shadow-lg, 0 10px 40px rgba(0,0,0,.2));
  padding: var(--space-md, 12px);
  width: min(320px, calc(100vw - 32px));
  max-height: 70vh;
  display: flex;
  flex-direction: column;
}
.pv-typemodal__list {
  list-style: none;
  margin: 0 0 var(--space-sm, 8px);
  padding: 0;
  overflow-y: auto;
}
.pv-typemodal__option {
  display: block;
  width: 100%;
  text-align: left;
  border: none;
  background: none;
  padding: 8px 10px;
  border-radius: var(--radius-sm, 4px);
  cursor: pointer;
  font-size: 0.95rem;
}
.pv-typemodal__option:hover { background: var(--color-border-default, #E5E5E5); }
.pv-typemodal__new { display: flex; gap: var(--space-xs, 4px); }

/* --- Photo Input visual polish (2026-06-06) --- */

/* #1 Upload page: styled file trigger row. The button + .upload-filename
   are existing house components; only the layout + empty-state modifier
   are new, scoped to this row. */
.photo-file-row {
  display: flex;
  align-items: center;
  gap: var(--space-md, 12px);
  margin-top: var(--space-xs, 4px);
}
/* The documented phantom-file-input cascade (Estética/UX item 6, in
   CLAUDE.md): `.field input { width:100% }` (0,1,1) overrides
   `.visually-hidden { width:1px }` (0,1,0), blowing the hidden input to
   full width + absolute -> page H-scroll. Caught AGAIN here by the
   2026-06-07 measurement harness on photo/new. Scoped re-assert wins
   at (0,2,0). */
.photo-file-row .visually-hidden {
  width: 1px;
  height: 1px;
}
.photo-file-row .upload-filename { margin-top: 0; }
.photo-file-row .upload-filename--empty {
  color: var(--color-text-muted, #777);
  font-weight: 400;
}
.photo-file-hint {
  margin: var(--space-xs, 4px) 0 0;
  font-size: 0.78rem;
  color: var(--color-text-muted, #777);
}

/* #2 Type-modal filter — mirrors the app search-input language
   (comparator-selection__search), compact for the mini-modal. */
.pv-typemodal__filter {
  margin-bottom: var(--space-sm, 8px);
}
.pv-typemodal__filter input {
  width: 100%;
  padding: var(--space-sm, 8px) var(--space-md, 12px);
  font-size: 0.95rem;
  border: 1px solid var(--color-border-default, #E5E5E5);
  border-radius: var(--radius-md, 8px);
  background: white;
}
.pv-typemodal__filter input:focus {
  outline: none;
  border-color: var(--color-text, #111);
}

/* #3 Editable grid, modern editable-table feel. EVERY selector is
   .pv-table-prefixed — the global .table / .form-input are untouched
   (app-wide table modernization is a separate future decision). */
/* The scroll WRAPPER is UNCONDITIONAL (not media-scoped) on purpose:
   on desktop the table fits 100% and the wrapper never scrolls (no-op);
   on phones it is the containment that stops the grid from expanding
   the page viewport — and it must exist at FIRST layout, before any
   media query re-evaluation (2026-06-06 measurement diagnostic). */
.pv-table-scroll {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  width: 100%;       /* constrain to the viewport, never content width */
  max-width: 100%;
}
.pv-table {
  border-collapse: collapse;
  width: 100%;
}
.pv-table thead th {
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-text-muted, #777);
  text-align: left;
  padding: var(--space-sm, 8px) var(--space-sm, 8px);
  border-bottom: 1px solid var(--color-border-default, #E5E5E5);
}
.pv-table td {
  padding: var(--space-sm, 8px);
  height: 44px;
  border: none;
  border-bottom: 1px solid var(--color-border-subtle, #eee);
  vertical-align: middle;
}
.pv-table tbody tr:hover {
  background: var(--color-surface-alt, #FAFAFA);
}
/* Inputs: borderless + transparent at rest; thin bottom underline in the
   text color on focus (the architectural cue). Padding identical in both
   states so text never jumps. */
.pv-table .form-input {
  border: 1px solid transparent;
  border-radius: 0;
  background: transparent;
  padding: 4px 2px;
  width: 100%;
  min-width: 0;
  font-size: 0.95rem;
}
.pv-table .form-input:focus {
  outline: none;
  border-color: transparent;
  border-bottom: 1px solid var(--color-text, #111);
  background: var(--color-bg, #fff);
}
.pv-table .type-picker--row {
  border: 1px solid transparent;
  background: transparent;
}
.pv-table .type-picker--row:hover {
  border-color: var(--color-border-default, #E5E5E5);
  background: var(--color-bg, #fff);
}
/* The uncertain tint keeps its left accent; the row hairline system
   replaces the boxed look underneath it. */
.pv-table .pv-row--uncertain td { background: inherit; }

/* #4 Conflict prompt as a distinct card. Update/Skip/Cancel live INSIDE
   (they answer the card's question), separated by a top hairline; clear
   air below before the Import/Discard confirm row. */
.pv-conflicts {
  border: 1px solid var(--color-border-default, #E5E5E5);
  border-radius: var(--radius-md, 8px);
  background: var(--color-surface-alt, #FAFAFA);
  padding: var(--space-lg, 16px);
  margin-bottom: var(--space-xl, 28px);
}
.pv-conflicts__title {
  margin: 0 0 var(--space-sm, 8px);
  font-size: 1rem;
  font-weight: 600;
}
.pv-conflicts__list {
  margin: 0 0 var(--space-md, 12px);
  padding-left: 1.2em;
}
.pv-conflicts__list li {
  padding: 2px 0;
  font-size: 0.9rem;
}
.pv-conflicts__actions {
  display: flex;
  gap: var(--space-sm, 8px);
  padding-top: var(--space-md, 12px);
  border-top: 1px solid var(--color-border-default, #E5E5E5);
}

/* --- Photo Input visual polish: MOBILE fixes (2026-06-06) ---
   All .pv-* scoped; desktop (>=768px) untouched by every rule below. */
@media (max-width: 767px) {

  /* A) The grid scrolls inside its WRAPPER — not the page. REWORKED
     after the 2026-06-06 measurement diagnostic: the display:block-on-
     table shortcut let the thead/tbody anonymous boxes escape the
     block's containment and EXPANDED the page layout viewport on
     phones (EST 461px, PL 751px — the whole-page pan + tiny-render
     symptom). The canonical wrapper-div pattern (.pv-table-scroll in
     validate.html) contains correctly; the table stays a real table.
     Per-column min-widths keep cells comfortable; page stays put. */
  .pv-main { min-width: 0; overflow-x: hidden; }
  /* ITEM 3 (2026-06-07): code column +1.5rem (~3ch) — many real codes
     are wide (WALL-001-UP-LAQ). Modest bump per founder; MOBILE ONLY
     (inside the max-width:767px block; desktop columns untouched). */
  .pv-table .pv-input-code  { min-width: 8.5rem; }
  .pv-table .pv-input-desc  { min-width: 11rem; }
  .pv-table .pv-input-price { min-width: 5.5rem; }
  .pv-table .pv-input-qty   { min-width: 4rem; }
  .pv-table .pv-btn-type    { min-width: 7rem; }
  .pv-table .pv-cell-status { min-width: 10rem; }

  /* A+D) Status badges read on ONE line inside the scrollable grid —
     no more 4-line stacks in a crushed column. */
  .pv-cell-status .pv-badge-nomatch,
  .pv-cell-status .pv-flag-uncertain,
  .pv-cell-status .pv-badge-dup,
  .pv-cell-status .pv-row-error {
    white-space: nowrap;
  }
  .pv-cell-status .pv-row-error { display: inline-block; }

  /* B) Air between the review chip and the Category control.
     (The chip's margin-bottom moved to the BASE rule on 2026-06-07 —
     Fix 2 made it both-viewport; only the bulk-type top gap stays
     mobile-scoped.) */
  .pv-bulk-type   { margin-top: var(--space-sm, 8px); }

  /* E) Conflict actions: stacked full-width — all three visible and
     comfortably tappable; nothing falls off the edge. */
  .pv-conflicts__actions {
    flex-direction: column;
    align-items: stretch;
  }
  .pv-conflicts__actions .btn { width: 100%; }
}

/* Photo Input — By Photo row inside the EST upload dialog (2026-06-07).
   Own row below the pre-existing two-button actions row (which is back
   to its pre-sprint state). Right-aligned to group with the action
   cluster; same on desktop and mobile. */
.pv-byphoto-row {
  display: flex;
  justify-content: flex-end;
  margin-top: var(--space-sm, 8px);
}

/* Photo Input polish (2026-06-07) — ITEM 2: breathing room between the
   name field and the confirm actions. NOT media-scoped: applies to
   desktop too (flagged + pre-approved; on the PL intent, where no name
   field renders, it adds the same small gap above the actions). */
.pv-confirm__actions {
  margin-top: var(--space-lg, 16px);
}

/* === PHOTO INPUT — MOBILE ROW-EDITOR (2026-06-07) === */
/* Root-cause-driven design: the mobile layout viewport inflates to reach
   FOCUSABLE elements past the screen edge (proven by the isolation
   experiments). So on phones the grid (full of inputs) hides entirely;
   rows render as plain-text list items that fit 375 with ellipsis, and
   editing happens in a bottom sheet whose fields stack INSIDE the
   viewport. Desktop (>=768px) keeps the editable grid untouched. */

/* The swap */
.pv-mlist { display: none; }
@media (max-width: 767px) {
  .pv-table-scroll { display: none; }   /* display:none focusables do not leak */
  .pv-mlist { display: block; }
}

/* Collapsed rows — plain text, one line, whole row tappable */
.pv-mlist {
  list-style: none;
  margin: 0 0 var(--space-sm, 8px);
  padding: 0;
}
.pv-mrow {
  display: flex;
  align-items: center;
  gap: var(--space-sm, 8px);
  min-height: 48px;
  padding: var(--space-sm, 8px) var(--space-xs, 4px);
  border-bottom: 1px solid var(--color-border-subtle, #eee);
  cursor: pointer;
}
.pv-mrow:active { background: var(--color-surface-alt, #FAFAFA); }
.pv-mrow__dot {
  flex: none;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: transparent;
}
/* Triage dots — uncertain = HOLLOW AMBER RING (medium attention: validate
   the reading) vs conflict = SOLID RED DOT (high attention: collides with
   existing data). The house semantic tokens are deliberately deep-toned,
   so at dot size hue alone wasn't unmistakable — shape joins the encoding
   (and reads for color-impaired eyes too). 2026-06-07. */
.pv-mrow--uncertain .pv-mrow__dot,
.pv-mrow--conflict .pv-mrow__dot { width: 10px; height: 10px; }
.pv-mrow--uncertain .pv-mrow__dot {
  background: var(--color-warning-bg, #FFF8EB);
  border: 2.5px solid var(--color-warning, #B45309);
}
.pv-mrow--conflict .pv-mrow__dot { background: var(--color-error, #B42318); }
.pv-mrow__code {
  flex: none;
  max-width: 9rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-weight: 600;
}
.pv-mrow__mid {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--color-text-muted, #777);
}
.pv-mrow__price { flex: none; font-variant-numeric: tabular-nums; }
.pv-mrow__note {
  flex: none;
  font-size: 0.75rem;
  color: var(--color-text-muted, #777);
}

/* Bottom sheet */
.pv-sheet {
  position: fixed;
  inset: 0;
  z-index: 1002;
  background: rgba(17, 17, 17, 0.45);
  display: flex;
  align-items: flex-end;
}
.pv-sheet__panel {
  background: var(--color-bg, #fff);
  border-radius: var(--radius-md, 8px) var(--radius-md, 8px) 0 0;
  box-shadow: var(--shadow-lg, 0 -10px 40px rgba(0,0,0,.25));
  width: 100%;
  max-height: 80vh;
  overflow-y: auto;
  padding: var(--space-lg, 16px) var(--space-lg, 16px)
           calc(var(--space-lg, 16px) + env(safe-area-inset-bottom, 0px));
}
.pv-sheet__title { margin: 0 0 var(--space-md, 12px); font-size: 1.05rem; }
.pv-sheet__panel .field { margin-bottom: var(--space-md, 12px); }

.pv-sheet__conflict {
  border: 1px solid var(--color-error, #b3261e);
  border-radius: var(--radius-sm, 4px);
  background: color-mix(in srgb, var(--color-error, #b3261e) 6%, transparent);
  padding: var(--space-sm, 8px) var(--space-md, 12px);
  margin-bottom: var(--space-md, 12px);
}
.pv-sheet__conflict-title {
  margin: 0 0 var(--space-xs, 4px);
  font-weight: 600;
  font-size: 0.9rem;
  color: var(--color-error, #b3261e);
}
.pv-sheet__conflict-list { margin: 0; font-size: 0.85rem; }
.pv-sheet__conflict-list dt {
  float: left;
  clear: left;
  width: 6.5rem;
  font-weight: 600;
  color: var(--color-text-muted, #777);
}
.pv-sheet__conflict-list dd { margin: 0 0 2px 7rem; }

.pv-sheet__uncertain {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm, 8px);
  /* amber — matches the uncertain dot/flag semantic (2026-06-07) */
  border-left: 3px solid var(--color-warning, #B45309);
  background: color-mix(in srgb, var(--color-warning, #B45309) 6%, transparent);
  padding: var(--space-sm, 8px) var(--space-md, 12px);
  margin-bottom: var(--space-md, 12px);
  font-size: 0.85rem;
}

.pv-sheet__match-note {
  margin: 0 0 var(--space-md, 12px);
  font-size: 0.85rem;
  color: var(--color-text-muted, #777);
}

.pv-sheet__actions {
  display: flex;
  justify-content: space-between;
  gap: var(--space-md, 12px);
  margin-top: var(--space-sm, 8px);
}

/* Conflict card split view (2026-06-07): mobile = one-line summary (red
   dots + per-row sheet carry the detail); desktop = read-only grouped
   detail (by code, changed fields only). The Update/Skip/Cancel buttons
   in the same card remain the ONLY decision surface on both. */
.pv-conflicts__summary {
  margin: 0 0 var(--space-md, 12px);
  font-size: 0.9rem;
}
/* Solid red dot at the END of the summary line (after the text — at the
   start it would read as a bullet): quiet cue tying the sentence to the
   red conflict dots in the list above. 2026-06-07. */
.pv-conflicts__summary::after {
  content: "";
  display: inline-block;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--color-error, #B42318);
  margin-left: var(--space-sm, 8px);
  vertical-align: middle;
}
@media (min-width: 768px) {
  .pv-conflicts__summary { display: none; }
}
@media (max-width: 767px) {
  .pv-conflicts__list { display: none; }
}

/* Desktop grouped detail */
.pv-conflicts__list { margin: 0 0 var(--space-md, 12px); padding: 0; list-style: none; }
.pv-cgroup { margin-bottom: var(--space-sm, 8px); }
.pv-cgroup__code { font-weight: 600; }
.pv-cgroup__type {
  color: var(--color-text-muted, #777);
  font-size: 0.85rem;
  margin-left: var(--space-xs, 4px);
}
.pv-cgroup__deltas {
  margin: 2px 0 0;
  padding-left: var(--space-lg, 16px);
  list-style: none;
  font-size: 0.85rem;
}
.pv-cgroup__deltas li { margin-bottom: 1px; }
.pv-cgroup__field {
  display: inline-block;
  min-width: 6.5rem;
  font-weight: 600;
  color: var(--color-text-muted, #777);
}

/* =========================================================
   TWO-FACTOR AUTH (2FA) — Phase 2 auth-UX polish (2026-06-09)
   Shared by /settings/security (enroll + recovery + status) and the
   login TOTP step. Token-based only — same family as .panel / .auth-card
   / .field / .error-page. Mobile already read well; the desktop enroll
   view was the cramped one (QR jammed top-left, key + field misaligned).
   ========================================================= */

/* Enroll: QR + manual-key, side by side on desktop, stacked on mobile. */
.totp-enroll {
  display: flex;
  gap: var(--space-2xl);
  flex-wrap: wrap;
  align-items: flex-start;
  margin: var(--space-lg) 0 var(--space-xl);
}
.totp-enroll__qr {
  width: 200px;
  max-width: 100%;          /* never overflow a narrow column / mobile width */
  aspect-ratio: 1 / 1;      /* stay square even when width is constrained */
  box-sizing: border-box;   /* padding is the quiet-zone, inside the 200px */
  flex-shrink: 0;
  padding: var(--space-md); /* extra white quiet-zone around the QR */
  background: #fff;
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius);
}
/* viewBox is injected in totp_service.qr_svg, so width/height:100% scales the
   whole QR (all four position-markers) to fit, fit-and-centered — no clip. */
.totp-enroll__qr svg { display: block; width: 100%; height: 100%; }
.totp-enroll__alt {
  flex: 1 1 220px;
  min-width: 0;
}
.totp-enroll__alt-label {
  margin: 0 0 var(--space-sm);
  font-size: 0.8125rem;
  color: var(--color-text-muted);
}

/* Manual key (the 32-char secret) + its copy button on one row. */
.totp-key-row {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  flex-wrap: wrap;
}
.totp-manual-key {
  font-family: var(--font-mono);
  font-size: 0.9375rem;
  letter-spacing: 0.02em;
  word-break: break-all;
  background: var(--color-surface-alt);
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius-sm);
  padding: var(--space-sm) var(--space-md);
  color: var(--color-text-primary);
}

/* The verify code field — centered, mono, spaced (reads as a code, not
   free text). Shared by the enroll verify + the login TOTP step. */
.totp-code-input {
  font-family: var(--font-mono);
  font-size: 1.125rem;
  letter-spacing: 0.25em;
  text-align: center;
}

/* On/Off status pill (no global .badge exists; scoped to 2FA). */
.totp-status {
  display: inline-flex;
  align-items: center;
  font-weight: 600;
  font-size: 0.8125rem;
  padding: 2px var(--space-sm);
  border-radius: var(--radius-pill);
}
.totp-status--on  { color: var(--color-success); background: var(--color-success-bg); }
.totp-status--off {
  color: var(--color-text-secondary);
  background: var(--color-surface-alt);
  border: 1px solid var(--color-border-default);
}

/* Recovery codes — readable 2-col grid of mono chips. */
.recovery-codes {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-sm) var(--space-lg);
  margin: var(--space-md) 0;
}
.recovery-code {
  font-family: var(--font-mono);
  font-size: 0.9375rem;
  letter-spacing: 0.04em;
  text-align: center;
  background: var(--color-surface-alt);
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius-sm);
  padding: var(--space-sm) var(--space-md);
  color: var(--color-text-primary);
}

/* Divider between the regenerate-recovery and disable blocks. */
.totp-divider {
  margin: var(--space-xl) 0;
  border: none;
  border-top: 1px solid var(--color-border-default);
}

@media (max-width: 520px) {
  .totp-enroll { gap: var(--space-lg); }
  .recovery-codes { grid-template-columns: 1fr; }
}

/* === DEMO / SAMPLE DATA BANNER (Sample Project Sprint, 2026-06-10) ===
   Sober, low-chrome notice shown to the owner while the workspace holds
   seeded SAMPLE content. Restrained info palette (no loud color), single
   subtle border — sits quietly above page content, dismissible. */
.demo-banner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-lg);
  margin: 0 0 var(--space-xl);
  padding: var(--space-md) var(--space-lg);
  background: var(--color-info-bg);
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius);
  color: var(--color-text-secondary);
  font-size: 0.9rem;
  line-height: 1.4;
}
.demo-banner__text { min-width: 0; }
.demo-banner__text strong { color: var(--color-text-primary); font-weight: 600; }
.demo-banner__text em { font-style: normal; font-weight: 600; color: var(--color-info); }
.demo-banner__actions {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  flex-shrink: 0;
}
.demo-banner__form { margin: 0; }
.demo-banner__dismiss {
  border: none;
  background: transparent;
  color: var(--color-text-muted);
  font-size: 1.25rem;
  line-height: 1;
  padding: 0 var(--space-xs);
  cursor: pointer;
  border-radius: var(--radius-sm);
}
.demo-banner__dismiss:hover { color: var(--color-text-primary); }

/* Sample-comparison link on a demo project header. */
.project-detail-header__demo-compare { margin-top: var(--space-md); }

@media (max-width: 600px) {
  .demo-banner {
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-md);
  }
  .demo-banner__actions { width: 100%; justify-content: space-between; }
}

/* Admin "Send banner" (Day 45) — platform announcement banner. Reuses the
   .demo-banner layout with 4 semantic color variants (red/blue/green/amber). */
.admin-banner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-lg);
  margin: 0 0 var(--space-xl);
  padding: var(--space-md) var(--space-lg);
  border: 1px solid var(--color-border-default);
  border-left-width: 4px;
  border-radius: var(--radius);
  font-size: 0.9rem;
  line-height: 1.4;
}
.admin-banner__text { min-width: 0; }
.admin-banner__text strong { font-weight: 600; }
.admin-banner__actions {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  flex-shrink: 0;
}
.admin-banner__form { margin: 0; }
.admin-banner__dismiss {
  border: none;
  background: transparent;
  color: var(--color-text-muted);
  font-size: 1.25rem;
  line-height: 1;
  padding: 0 var(--space-xs);
  cursor: pointer;
  border-radius: var(--radius-sm);
}
.admin-banner__dismiss:hover { color: var(--color-text-primary); }
/* Color variants — background tint + accent border + strong-title color. */
.admin-banner--red   { background: var(--color-error-bg);   border-left-color: var(--color-error);   color: var(--color-text-secondary); }
.admin-banner--red   strong { color: var(--color-error); }
.admin-banner--blue  { background: var(--color-info-bg);    border-left-color: var(--color-info);    color: var(--color-text-secondary); }
.admin-banner--blue  strong { color: var(--color-info); }
.admin-banner--green { background: var(--color-success-bg); border-left-color: var(--color-success); color: var(--color-text-secondary); }
.admin-banner--green strong { color: var(--color-success); }
.admin-banner--amber { background: var(--color-warning-bg); border-left-color: var(--color-warning); color: var(--color-text-secondary); }
.admin-banner--amber strong { color: var(--color-warning); }
@media (max-width: 600px) {
  .admin-banner { flex-direction: column; align-items: flex-start; gap: var(--space-md); }
  .admin-banner__actions { width: 100%; justify-content: space-between; }
}
/* Admin send-form banner color picker (Day 45). */
.admin-form__banner-colors { display: flex; flex-wrap: wrap; gap: var(--space-md); }
.admin-form__color-option {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  cursor: pointer;
  font-size: 0.85rem;
  text-transform: capitalize;
}
.admin-form__color-swatch {
  width: 1rem; height: 1rem;
  border-radius: 50%;
  border: 1px solid var(--color-border-default);
  display: inline-block;
}
.admin-form__color-option--red   .admin-form__color-swatch { background: var(--color-error); }
.admin-form__color-option--blue  .admin-form__color-swatch { background: var(--color-info); }
.admin-form__color-option--green .admin-form__color-swatch { background: var(--color-success); }
.admin-form__color-option--amber .admin-form__color-swatch { background: var(--color-warning); }

/* Delete-sample-data confirmation page (Day 45) — named item list. */
.demo-confirm__group {
  margin: var(--space-lg) 0 var(--space-xs);
  font-size: 0.8rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--color-text-muted);
}
.demo-confirm__list {
  margin: 0;
  padding-left: var(--space-lg);
  color: var(--color-text-primary);
}
.demo-confirm__list li { margin: 0.15rem 0; }
/* Prose warning block — plain paragraphs (NOT the two-column .flash flex), with
   breathing room above (from the list/intro) and below (to the actions). */
.demo-confirm__warning {
  margin: var(--space-xl) 0;
  padding: var(--space-md) var(--space-lg);
  background: var(--color-info-bg);
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius);
  color: var(--color-text-secondary);
  font-size: 0.9rem;
  line-height: 1.5;
}
.demo-confirm__warning p { margin: 0; }
.demo-confirm__warning p + p { margin-top: var(--space-md); }


/* === VISUAL COMPARISON (Heat Map) — Commit 4 === */

/* Two-level Compare tab strip (Estimates | Heat Map) */
.compare-tabs {
  display: flex;
  gap: var(--space-xs);
  margin-bottom: var(--space-lg);
  border-bottom: 1px solid var(--color-border-default);
}
.compare-tabs__tab {
  padding: var(--space-sm) var(--space-md);
  font-size: 0.9rem;
  font-weight: 600;
  color: var(--color-text-secondary);
  text-decoration: none;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
}
.compare-tabs__tab:hover { color: var(--color-text-primary); }
.compare-tabs__tab.is-active {
  color: var(--color-text-primary);
  border-bottom-color: var(--color-accent);
}
/* Subtle accent drawing the eye to Heat Map without shouting. Slightly
   smaller than the label + baseline-aligned so it doesn't disturb the
   [ Estimates | Heat Map ] alignment on desktop or mobile. */
.compare-tabs__flair { font-size: 0.85em; line-height: 1; vertical-align: baseline; }

.vc-body { max-width: 1000px; }
.vc-section-title { font-size: 1rem; margin: var(--space-xl) 0 var(--space-md); }

/* Upload card */
.vc-upload-card {
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius);
  background: var(--color-surface);
  padding: var(--space-lg);
  box-shadow: var(--shadow-sm);
}
.vc-upload-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-md);
}
.vc-drop {
  display: block;
  border: 1.5px dashed var(--color-border-strong);
  border-radius: var(--radius-sm);
  padding: var(--space-lg);
  cursor: pointer;
  background: var(--color-bg);
  transition: border-color .15s, background .15s;
}
.vc-drop:hover { border-color: var(--color-accent); }
.vc-drop.is-filled { border-style: solid; border-color: var(--color-success); }
.vc-drop__label { display: block; font-weight: 600; font-size: 0.9rem; margin-bottom: var(--space-xs); }
.vc-drop__hint { color: var(--color-text-muted); font-weight: 400; }
.vc-drop__input { position: absolute; width: 1px; height: 1px; opacity: 0; overflow: hidden; }
.vc-drop__filename {
  display: block;
  font-size: 0.85rem;
  color: var(--color-text-muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  max-width: 100%;
}
.vc-drop__filename.is-chosen { color: var(--color-success); font-weight: 600; }
.vc-upload-actions { margin-top: var(--space-lg); }

/* Comparisons list */
.vc-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-sm); }
.vc-card {
  display: flex; align-items: center; justify-content: space-between;
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius);
  background: var(--color-surface);
  padding: var(--space-md) var(--space-lg);
  gap: var(--space-md);
}
.vc-card__main { display: flex; flex-direction: column; gap: 4px; flex: 1; min-width: 0; text-decoration: none; color: inherit; }
.vc-card__names { display: flex; align-items: center; gap: var(--space-sm); min-width: 0; font-weight: 600; }
.vc-card__name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 42%; }
.vc-card__vs { color: var(--color-text-muted); font-weight: 400; font-size: 0.8rem; flex: none; }
.vc-card__meta { display: flex; align-items: center; gap: var(--space-md); font-size: 0.82rem; color: var(--color-text-muted); flex-wrap: wrap; }
.vc-card__delete { flex: none; }

/* Status pill — house semantic colors */
.vc-status {
  display: inline-block; padding: 2px 10px; border-radius: var(--radius-pill);
  font-size: 0.72rem; font-weight: 700; text-transform: uppercase; letter-spacing: .02em;
}
.vc-status--done       { color: var(--color-success); background: var(--color-success-bg); }
.vc-status--processing { color: var(--color-info);    background: var(--color-info-bg); }
.vc-status--pending    { color: var(--color-warning); background: var(--color-warning-bg); }
.vc-status--error      { color: var(--color-error);   background: var(--color-error-bg); }
.vc-status--cancelled  { color: var(--color-text-muted); background: var(--color-surface-alt); }

/* Pairing summary (processing) */
.vc-pairing-summary { font-weight: 600; color: var(--color-text-primary); margin: 0 0 var(--space-xs); }

/* Viewer */
.vc-viewer__head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--space-md); margin-bottom: var(--space-md); }
.vc-viewer__title { font-size: 1.15rem; display: flex; align-items: center; gap: var(--space-sm); flex-wrap: wrap; }
.vc-viewer__title span { overflow: hidden; text-overflow: ellipsis; }

/* Unmatched chips */
.vc-chips { display: flex; flex-wrap: wrap; gap: var(--space-sm); margin-bottom: var(--space-md); }
.vc-chip { padding: 4px 12px; border-radius: var(--radius-pill); font-size: 0.8rem; font-weight: 600; }
.vc-chip--added   { color: var(--color-warning); background: var(--color-warning-bg); }
.vc-chip--removed { color: var(--color-info);    background: var(--color-info-bg); }

/* Toolbar: pager + style toggle */
.vc-viewer__toolbar {
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-md);
  flex-wrap: wrap; margin-bottom: var(--space-md);
}
.vc-pager { display: flex; align-items: center; gap: var(--space-md); }
.vc-pager__btn { min-width: 44px; min-height: 44px; font-size: 1.3rem; line-height: 1; padding: 0; }
.vc-pager__label { font-size: 0.85rem; color: var(--color-text-secondary); min-width: 12ch; text-align: center; }

.vc-styletoggle { display: inline-flex; border: 1px solid var(--color-border-default); border-radius: var(--radius-sm); overflow: hidden; }
.vc-styletoggle__opt {
  border: none; background: var(--color-bg); color: var(--color-text-secondary);
  padding: var(--space-sm) var(--space-lg); font-size: 0.85rem; font-weight: 600; cursor: pointer; min-height: 44px;
}
.vc-styletoggle__opt + .vc-styletoggle__opt { border-left: 1px solid var(--color-border-default); }
.vc-styletoggle__opt.is-active { background: var(--color-accent); color: var(--color-accent-text); }

/* Stage */
.vc-stage {
  position: relative; border: 1px solid var(--color-border-default); border-radius: var(--radius);
  background: #fff; overflow: auto; text-align: center; min-height: 200px;
}
.vc-stage__canvas, .vc-stage__img { max-width: 100%; height: auto; display: inline-block; vertical-align: top; }
.vc-stage__loading {
  position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  color: var(--color-text-muted); font-size: 0.9rem; background: rgba(255,255,255,0.6);
}

/* Controls: slider + score */
.vc-controls { display: flex; align-items: center; justify-content: space-between; gap: var(--space-lg); margin-top: var(--space-md); flex-wrap: wrap; }
.vc-slider { display: flex; align-items: center; gap: var(--space-sm); flex: 1; min-width: 240px; position: relative; }
.vc-slider.is-disabled { opacity: 0.4; pointer-events: none; }
.vc-slider__end { font-size: 0.8rem; color: var(--color-text-muted); flex: none; }
.vc-slider__input { flex: 1; height: 28px; cursor: pointer; }
.vc-slider__caption { position: absolute; top: -16px; left: 50%; transform: translateX(-50%); font-size: 0.72rem; color: var(--color-text-muted); text-transform: uppercase; letter-spacing: .04em; }
.vc-score { font-size: 0.82rem; color: var(--color-text-muted); flex: none; }
/* Info line below the toolbar (score + mobile export). Lives in .vc-controls,
   which is display:none on desktop — so .vc-dl-mobile is mobile-only for free. */
/* ONE line, NO wrap (a wrap added height and made the mobile viewer scroll):
   score shrinks/ellipsis on the left, the two compact VIEW↓/ZIP↓ on the right. */
.vc-controls__info { display: flex; align-items: center; justify-content: space-between; gap: var(--space-sm); flex-wrap: nowrap; width: 100%; }
.vc-controls__info .vc-score { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 0.78rem; }
.vc-dl-mobile-group { display: flex; align-items: center; gap: var(--space-md); flex-wrap: nowrap; flex: none; }
.vc-dl-mobile {
  display: inline-flex; align-items: center; gap: 2px;
  font-size: 0.78rem; font-weight: 700; letter-spacing: .02em; color: var(--color-accent);
  text-decoration: none; white-space: nowrap; flex: none;
  background: none; border: none; cursor: pointer; padding: 0;   /* button reset */
}
.vc-dl-mobile:hover { text-decoration: underline; }

/* Notice (error / cancelled) */
.vc-notice {
  border: 1px solid var(--color-border-default); border-radius: var(--radius);
  background: var(--color-surface); padding: var(--space-xl); text-align: center;
}
.vc-notice__actions { display: flex; gap: var(--space-md); justify-content: center; align-items: center; margin-top: var(--space-lg); flex-wrap: wrap; }

/* Mobile — design-constraint from the start */
@media (max-width: 767px) {
  .vc-upload-grid { grid-template-columns: 1fr; }
  .vc-viewer__toolbar { flex-direction: column; align-items: stretch; }
  .vc-pager { justify-content: space-between; }
  .vc-styletoggle { width: 100%; }
  .vc-styletoggle__opt { flex: 1; text-align: center; }
  .vc-controls { flex-direction: column; align-items: stretch; }
  .vc-slider { width: 100%; margin-top: var(--space-md); }
  .vc-card { flex-wrap: wrap; }
  .vc-card__name { max-width: 38vw; }
}

/* The toolbar slider + open-window button are DESKTOP-only; hidden by default
   so mobile (<=767px) stays byte-identical (it keeps the full-width bottom
   slider). */
.vc-toolbar-mid { display: none; }
.vc-openwin { display: none; }
.vc-download { display: none; }   /* desktop-only; mobile uses native long-press-to-save */
.vc-slider__caption-inline { font-size: 0.72rem; color: var(--color-text-muted); text-transform: uppercase; letter-spacing: .04em; flex: none; }

/* Desktop-only viewer polish (Commit 4 follow-up). Scoped to >=768px so the
   mobile (<=767px) layout stays byte-identical (founder: "mobile is perfect,
   do not touch it"; Day-36 desktop-scoped precedent). */
@media (min-width: 768px) {
  /* Slider moves UP into the toolbar row (pager / slider / toggle); the bottom
     controls block is hidden on desktop so only the plan sits below the row. */
  .vc-toolbar-mid {
    display: flex; align-items: center; gap: var(--space-md);
    flex: 1; min-width: 0;
  }
  .vc-toolbar-mid .vc-slider { flex: 1; min-width: 0; margin-top: 0; }
  .vc-toolbar-mid .vc-score { flex: none; margin-left: var(--space-sm); white-space: nowrap; }
  /* Compact − / + ends on the toolbar slider (Less/More overflowed into the
     score on the desktop row). Mobile's bottom slider keeps Less/More. */
  .vc-slider__end--sym { font-size: 1.05rem; line-height: 1; min-width: 1ch; text-align: center; }
  .vc-controls { display: none; }
  .vc-openwin {
    display: inline-flex; align-items: center; justify-content: center;
    width: 40px; height: 40px; flex: none;
    border: 1px solid var(--color-border-default); border-radius: var(--radius-sm);
    background: var(--color-bg); color: var(--color-text-secondary); cursor: pointer;
  }
  .vc-openwin:hover { color: var(--color-text-primary); border-color: var(--color-accent); }
  /* Download dropdown (details/summary) — desktop-only, next to ↗ */
  .vc-download { display: inline-flex; position: relative; flex: none; }
  .vc-download__summary { list-style: none; }
  .vc-download__summary::-webkit-details-marker { display: none; }
  .vc-download[open] .vc-download__summary { color: var(--color-text-primary); border-color: var(--color-accent); }
  .vc-download__menu {
    position: absolute; right: 0; top: calc(100% + 4px); z-index: 20;
    min-width: 180px; display: flex; flex-direction: column;
    background: var(--color-bg); border: 1px solid var(--color-border-default);
    border-radius: var(--radius-sm); box-shadow: var(--shadow-md); padding: var(--space-xs);
  }
  .vc-download__menu > button,
  .vc-download__menu > a {
    text-align: left; background: none; border: none; cursor: pointer;
    padding: var(--space-sm) var(--space-md); font-size: 0.85rem;
    color: var(--color-text-primary); text-decoration: none; border-radius: var(--radius-sm);
  }
  .vc-download__menu > button:hover,
  .vc-download__menu > a:hover { background: var(--color-border-default); }
  /* (4b) the two plan names on ONE line; ellipsis + title tooltip if they
     don't fit (reclaims vertical space) */
  .vc-viewer__title { flex-wrap: nowrap; }
  .vc-viewer__title span[title] {
    min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  /* (4c) fit the full plan within the viewport height so the viewer doesn't
     scroll vertically (height-cap preserves aspect; width-cap still applies) */
  .vc-stage__canvas, .vc-stage__img { max-height: calc(100vh - 300px); width: auto; }
}

/* === HELP ZONE ===
   Authenticated /help list + search + topic page (Day 47, Sprint 1 / Commit 2).
   Token-only; reuses the app shell (.page-header, .breadcrumb-back). */
.help-intro {
  font-size: 1rem;
  color: var(--color-text-secondary);
  line-height: 1.55;
  margin-bottom: var(--space-xl);
  max-width: 60ch;
}

/* Search */
.help-search {
  display: flex;
  gap: var(--space-sm);
  align-items: center;
  flex-wrap: wrap;
  margin-bottom: var(--space-2xl);
}
.help-search__input {
  flex: 1 1 280px;
  min-width: 0;
  height: var(--height-input);
  padding: 0 var(--space-md);
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius-sm);
  font-size: 1rem;
  font-family: var(--font-sans);
  color: var(--color-text-primary);
  background: var(--color-bg);
}
.help-search__input:focus { outline: none; border-color: var(--color-text-primary); }
.help-search__btn { flex: 0 0 auto; }
.help-search__clear {
  flex: 0 0 auto;
  font-size: 0.875rem;
  color: var(--color-text-muted);
  text-decoration: none;
}
.help-search__clear:hover { color: var(--color-text-primary); }

.help-result-count {
  font-size: 0.875rem;
  color: var(--color-text-muted);
  margin-bottom: var(--space-lg);
}

.help-empty {
  padding: var(--space-2xl);
  text-align: center;
  color: var(--color-text-secondary);
  background: var(--color-surface);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius);
}
.help-empty p { margin-bottom: var(--space-sm); }
.help-empty a { color: var(--color-text-primary); }

/* Sections + topic cards */
.help-section { margin-bottom: var(--space-2xl); }
.help-section__title {
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-text-muted);
  margin-bottom: var(--space-md);
}
.help-topic-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: var(--space-md);
}
.help-topic-card {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  padding: var(--space-lg);
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius);
  background: var(--color-bg);
  text-decoration: none;
  box-shadow: var(--shadow-sm);
  transition: border-color 120ms ease, box-shadow 120ms ease;
}
.help-topic-card:hover {
  border-color: var(--color-border-strong);
  box-shadow: var(--shadow-md);
  text-decoration: none;
}
.help-topic-card__title { font-size: 1rem; font-weight: 600; color: var(--color-text-primary); }
.help-topic-card__arrow { color: var(--color-text-muted); flex: 0 0 auto; }
.help-topic-card:hover .help-topic-card__arrow { color: var(--color-text-primary); }

/* Topic page (long-form, app-authored HTML) */
.help-topic { max-width: 70ch; color: var(--color-text-primary); line-height: 1.6; }
.help-topic h1 { font-size: 2rem; font-weight: 600; letter-spacing: -0.02em; margin-bottom: var(--space-lg); }
.help-topic h2 { font-size: 1.25rem; font-weight: 600; margin-top: var(--space-2xl); margin-bottom: var(--space-sm); }
.help-topic h3 { font-size: 1.0625rem; font-weight: 600; margin-top: var(--space-xl); margin-bottom: var(--space-xs); }
.help-topic p { margin-bottom: var(--space-md); color: var(--color-text-secondary); }
.help-topic .help-lead {
  font-size: 1.125rem;
  color: var(--color-text-primary);
  line-height: 1.55;
  margin-bottom: var(--space-xl);
}
.help-topic ul, .help-topic ol { margin: 0 0 var(--space-md) var(--space-xl); color: var(--color-text-secondary); }
.help-topic li { margin-bottom: var(--space-xs); }
.help-topic code {
  font-family: var(--font-mono);
  font-size: 0.875em;
  background: var(--color-surface-alt);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-sm);
  padding: 0.1em 0.35em;
}
.help-topic a { color: var(--color-info); text-decoration: underline; }
.help-topic a:hover { color: var(--color-text-primary); }
.help-topic strong { color: var(--color-text-primary); font-weight: 600; }

/* Media placeholders — real assets land later; until then a missing /static
   image renders inside an intentional dashed box so the alt text reads as a
   placeholder, not a broken-image accident. Soften/replace once assets exist. */
.help-topic img {
  display: block;
  max-width: 100%;
  height: auto;
  margin: var(--space-lg) 0;
  padding: var(--space-md);
  min-height: 80px;
  border: 1px dashed var(--color-border-strong);
  border-radius: var(--radius);
  background: var(--color-surface-alt);
  color: var(--color-text-muted);
  font-size: 0.8125rem;
}

/* Practice-file lists inside a topic — no bullet/indent (higher specificity
   than .help-topic ul, so no !important needed). */
.help-topic ul.help-practice { list-style: none; margin-left: 0; }
.help-topic ul.help-practice li {
  padding: var(--space-sm) 0;
  border-bottom: 1px solid var(--color-border-subtle);
}

/* Related */
.help-related {
  margin-top: var(--space-3xl);
  padding-top: var(--space-xl);
  border-top: 1px solid var(--color-border-subtle);
}
.help-related__title {
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-text-muted);
  margin-bottom: var(--space-md);
}

@media (max-width: 640px) {
  .help-topic-list { grid-template-columns: 1fr; }
  .help-search__input { flex: 1 1 100%; }
}

/* === HELP AFFORDANCE POLISH (Commit 11) ===
   Two clarity treatments shared by the topic pages and the carousel slides:
   a prev/next footer that reuses the .help-topic-card visual language, and a
   "Download" chip that reads as an action, not a file reference. Both live
   INSIDE .help-topic (and .help-download also inside .help-carousel__slide),
   where the generic `a { text-decoration: underline; color: info }` rules would
   otherwise bleed in — so each carries a same-or-higher-specificity reset. */

/* Prev/next footer — same border/shadow/hover tokens as .help-topic-card, but a
   column layout (overline + title) instead of the list card's row. */
.help-topic-nav {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-md);
  margin-top: var(--space-2xl);
  padding-top: var(--space-xl);
  border-top: 1px solid var(--color-border-subtle);
}
.help-topic a.help-topic-nav__card {
  flex: 1 1 240px;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: var(--space-md) var(--space-lg);
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius);
  background: var(--color-bg);
  box-shadow: var(--shadow-sm);
  text-decoration: none;
  color: var(--color-text-primary);
  transition: border-color 120ms ease, box-shadow 120ms ease;
}
.help-topic a.help-topic-nav__card:hover {
  border-color: var(--color-border-strong);
  box-shadow: var(--shadow-md);
  text-decoration: none;
  color: var(--color-text-primary);
}
.help-topic-nav__card--next { text-align: right; align-items: flex-end; }
.help-topic-nav__over {
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-text-muted);
}
.help-topic-nav__title { font-size: 1rem; font-weight: 600; color: var(--color-text-primary); }
.help-topic-nav__arrow { color: var(--color-text-muted); }
.help-topic a.help-topic-nav__card:hover .help-topic-nav__arrow { color: var(--color-text-primary); }

/* Download chip — action-first ("Download …" + a ↓ glyph), distinct from inline
   prose links. Shared by topic pages and carousel "try it" slides. */
.help-download {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  padding: var(--space-xs) var(--space-md);
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius-sm);
  background: var(--color-surface);
  color: var(--color-text-primary);
  font-size: 0.875rem;
  font-weight: 600;
  line-height: 1.3;
  transition: border-color 120ms ease, background 120ms ease;
}
.help-download__icon { font-weight: 700; }
.help-download__note { color: var(--color-text-muted); font-size: 0.875rem; }
/* Resets so the chip survives the in-content `a` underline/color rules. */
.help-topic a.help-download,
.help-carousel__slide a.help-download { text-decoration: none; color: var(--color-text-primary); }
.help-topic a.help-download:hover,
.help-carousel__slide a.help-download:hover {
  text-decoration: none;
  color: var(--color-text-primary);
  border-color: var(--color-border-strong);
  background: var(--color-bg);
}
/* Carousel "try it" download row — a div (not a p) so it dodges the slide's
   `p` spacing rules; holds one or two chips, wrapping on narrow widths. */
.help-download-row { display: flex; flex-wrap: wrap; gap: var(--space-sm); margin-bottom: var(--space-md); }

/* === HELP CAROUSEL ===
   The in-layout "?" affordance + the carousel modal (Day 47, Sprint 2 /
   Commit 4). Token-only. The "?" is in-flow (mobile-safe); the dialog is
   top-layer centered via the global dialog[open]{margin:auto} rule. */

/* The "?" dot — small, in-flow, sits beside a title or in a card header. */
.help-dot {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  padding: 0;
  margin-left: var(--space-sm);
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius-pill);
  background: var(--color-surface);
  color: var(--color-text-muted);
  vertical-align: middle;  /* clean alignment when inline in the metadata line */
  font-size: 0.8125rem;
  font-weight: 600;
  line-height: 1;
  cursor: pointer;
  vertical-align: middle;
  transition: border-color 120ms ease, color 120ms ease, background 120ms ease;
}
.help-dot:hover {
  border-color: var(--color-border-strong);
  color: var(--color-text-primary);
  background: var(--color-bg);
}
.help-dot:focus-visible { outline: 2px solid var(--color-info); outline-offset: 2px; }

/* The carousel dialog */
.help-carousel {
  width: min(640px, 92vw);
  max-width: 640px;
  padding: 0;
  border: none;
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-xl);
  background: var(--color-bg);
  color: var(--color-text-primary);
  overflow: hidden;
}
.help-carousel::backdrop { background: rgba(17, 17, 17, 0.45); }
.help-carousel__inner { display: flex; flex-direction: column; max-height: 88vh; }

.help-carousel__header {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  padding: var(--space-md) var(--space-lg);
  border-bottom: 1px solid var(--color-border-subtle);
}
.help-carousel__index {
  flex: 1 1 auto;
  min-width: 0;
  height: 36px;
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius-sm);
  padding: 0 var(--space-sm);
  font-size: 0.875rem;
  font-family: var(--font-sans);
  color: var(--color-text-primary);
  background: var(--color-bg);
}
.help-carousel__close {
  flex: 0 0 auto;
  width: 32px;
  height: 32px;
  padding: 0;
  border: none;
  background: transparent;
  cursor: pointer;
  font-size: 1.5rem;
  line-height: 1;
  color: var(--color-text-muted);
}
.help-carousel__close:hover { color: var(--color-text-primary); }

.help-carousel__viewport { overflow: hidden; flex: 1 1 auto; }
.help-carousel__track { display: flex; transition: transform 250ms ease; }
.help-carousel__slide {
  flex: 0 0 100%;
  box-sizing: border-box;
  padding: var(--space-xl);
  max-height: 64vh;
  overflow-y: auto;
}
.help-carousel__slide h3 { font-size: 1.25rem; font-weight: 600; margin: var(--space-sm) 0 var(--space-sm); }
.help-carousel__slide p { color: var(--color-text-secondary); line-height: 1.6; margin-bottom: var(--space-md); }
.help-carousel__slide ul { margin: 0 0 var(--space-md) var(--space-xl); color: var(--color-text-secondary); }
.help-carousel__slide li { margin-bottom: var(--space-xs); }
.help-carousel__slide a { color: var(--color-info); text-decoration: underline; cursor: pointer; }
.help-carousel__slide strong { color: var(--color-text-primary); font-weight: 600; }
.help-carousel__slide code {
  font-family: var(--font-mono);
  font-size: 0.875em;
  background: var(--color-surface-alt);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-sm);
  padding: 0.1em 0.35em;
}
/* Media placeholders — dashed frame until real assets land (matches the
   topic page treatment). */
.help-carousel__slide img {
  display: block;
  max-width: 100%;
  height: auto;
  margin: 0 0 var(--space-lg);
  padding: var(--space-md);
  min-height: 80px;
  border: 1px dashed var(--color-border-strong);
  border-radius: var(--radius);
  background: var(--color-surface-alt);
  color: var(--color-text-muted);
  font-size: 0.8125rem;
}

.help-carousel__dots {
  display: flex;
  gap: var(--space-xs);
  justify-content: center;
  flex-wrap: wrap;
  padding: var(--space-sm) var(--space-lg);
}
.help-carousel__dot {
  width: 8px;
  height: 8px;
  padding: 0;
  border: none;
  border-radius: var(--radius-pill);
  background: var(--color-border-default);
  cursor: pointer;
}
.help-carousel__dot.is-active { background: var(--color-text-primary); }

.help-carousel__nav {
  display: flex;
  justify-content: space-between;
  gap: var(--space-sm);
  padding: var(--space-md) var(--space-lg);
  border-top: 1px solid var(--color-border-subtle);
}
.help-carousel__footer {
  text-align: center;
  font-size: 0.75rem;
  color: var(--color-text-muted);
  padding: 0 var(--space-lg) var(--space-md);
  margin: 0;
}

@media (prefers-reduced-motion: reduce) {
  .help-carousel__track { transition: none; }
}
@media (max-width: 640px) {
  .help-carousel { width: 96vw; }
  .help-carousel__slide { padding: var(--space-lg); max-height: 58vh; }
}

/* --- Smart-twinkle + docked launcher + fly-and-dock (Commit 5) --- */

/* Gentle ring pulse on the "?" while help is new. Modeled on the
   copilot-arrival-pulse ring technique (coral #FF3158), but a DEDICATED
   keyframe — the copilot one bakes in the panel's depth shadow, wrong for a
   small button. Slow + low-opacity so it reads as a twinkle, not a nag.
   Suppressed under prefers-reduced-motion below. */
@keyframes help-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(255, 49, 88, 0); }
  50%      { box-shadow: 0 0 0 5px rgba(255, 49, 88, 0.35); }
}
.help-dot--pulse { animation: help-pulse 2.4s ease-in-out infinite; }

/* Docked topbar launcher — always-available "?" beside the bell. */
.app-topbar__help {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 30px;
  height: 30px;
  padding: 0;
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius-pill);
  background: var(--color-surface);
  color: var(--color-text-secondary);
  font-size: 0.875rem;
  font-weight: 600;
  line-height: 1;
  cursor: pointer;
  transition: border-color 120ms ease, color 120ms ease, background 120ms ease;
}
.app-topbar__help:hover { border-color: var(--color-border-strong); color: var(--color-text-primary); }
.app-topbar__help:focus-visible { outline: 2px solid var(--color-info); outline-offset: 2px; }

/* Feedback (megaphone) button — mirrors .app-topbar__help (light-work). */
.app-topbar__feedback {
  display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto;
  width: 30px; height: 30px; padding: 0;
  border: 1px solid var(--color-border-default); border-radius: var(--radius-pill);
  background: var(--color-surface); color: var(--color-text-secondary);
  cursor: pointer;
  transition: border-color 120ms ease, color 120ms ease, background 120ms ease;
}
.app-topbar__feedback:hover { border-color: var(--color-border-strong); color: var(--color-text-primary); }
.app-topbar__feedback:focus-visible { outline: 2px solid var(--color-info); outline-offset: 2px; }

/* Feedback modal — reuses the .confirm-dialog family chrome. */
.feedback-dialog {
  border: 1px solid var(--color-border-default); border-radius: var(--radius-lg);
  background: var(--color-surface); box-shadow: var(--shadow-lg);
  padding: var(--space-xl); max-width: 32rem; width: min(34rem, 92vw); font: inherit;
}
.feedback-dialog::backdrop { background: var(--color-overlay-dark); }
.feedback-dialog__head { display: flex; align-items: center; justify-content: space-between; gap: var(--space-md); }
.feedback-dialog__title { margin: 0; font-size: 1.0625rem; font-weight: 600; color: var(--color-text-primary); }
.feedback-dialog__close { border: none; background: none; font-size: 1.5rem; line-height: 1; color: var(--color-text-muted); cursor: pointer; padding: 0 4px; }
.feedback-dialog__close:hover { color: var(--color-text-primary); }
.feedback-dialog__sub { margin: var(--space-xs) 0 var(--space-md); font-size: 0.875rem; color: var(--color-text-secondary); line-height: 1.5; }
.feedback-dialog__error { margin-bottom: var(--space-md); }

/* Fly-and-dock close — scale + translate the dialog toward the top-right
   launcher, then close (the cheap version; the pixel-perfect FLIP morph is
   deferred). Reduced-motion collapses it to a plain opacity fade. */
.help-carousel { transition: transform 320ms ease, opacity 320ms ease; }
.help-carousel--docking { transform: translate(42vw, -44vh) scale(0.12); opacity: 0; }

@media (prefers-reduced-motion: reduce) {
  .help-dot--pulse { animation: none; }
  .help-carousel { transition: opacity 150ms ease; }
  .help-carousel--docking { transform: none; opacity: 0; }
}

/* Onboarding welcome card (Commit 8) */
.onboarding-welcome { max-width: 62ch; }
.onboarding-welcome__lead {
  font-size: 1.125rem;
  color: var(--color-text-primary);
  line-height: 1.55;
  margin-bottom: var(--space-md);
}
.onboarding-welcome__sub { color: var(--color-text-secondary); margin-bottom: var(--space-xl); }
.onboarding-welcome__actions { display: flex; gap: var(--space-sm); flex-wrap: wrap; }

/* Settings -> Guided help toggle (Commit 8) */
.settings-help-toggle {
  display: flex;
  align-items: flex-start;
  gap: var(--space-sm);
  cursor: pointer;
  margin-bottom: var(--space-sm);
}
.settings-help-toggle input { margin-top: 3px; flex: 0 0 auto; }
.settings-help-toggle span { color: var(--color-text-secondary); }

/* === AUDIT LOG (Settings, Fase 2 Commit 3) === */
.audit-filters {
  display: flex;
  align-items: flex-end;
  flex-wrap: wrap;
  gap: var(--space-sm);
  margin-bottom: var(--space-lg);
}
.audit-filters__field { display: flex; flex-direction: column; gap: 4px; }
.audit-filters__field--grow { flex: 1 1 220px; }
.audit-filters__label {
  font-size: var(--text-xs);
  color: var(--color-text-secondary);
}
.audit-window-select,
.audit-search-input {
  padding: 6px 10px;
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius-sm);
  font: inherit;
}
.audit-search-input { width: 100%; }
.audit-filters__clear { align-self: center; }

/* Read-only table — text only (no focusable cells), so horizontal scroll on
   narrow screens is safe (the mobile viewport-expansion law applies only to
   focusable elements past the edge). */
.audit-table-wrap { overflow-x: auto; }
.audit-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--text-sm);
}
.audit-table th,
.audit-table td {
  text-align: left;
  padding: var(--space-sm) var(--space-md);
  border-bottom: 1px solid var(--color-border-default);
  white-space: nowrap;
}
.audit-table th {
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--color-text-secondary);
}
.audit-table__time { font-variant-numeric: tabular-nums; color: var(--color-text-secondary); }
.audit-table__summary { white-space: normal; }
.audit-table__ip { font-variant-numeric: tabular-nums; color: var(--color-text-secondary); }
.audit-event { font-weight: 600; }
.audit-event--failed { color: var(--color-error, #B42318); }

.audit-pager {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-sm);
  margin-top: var(--space-lg);
}
.audit-pager__count { color: var(--color-text-secondary); font-size: var(--text-sm); }
.audit-pager__nav { display: flex; gap: var(--space-sm); }

.audit-empty {
  padding: var(--space-xl) var(--space-md);
  text-align: center;
  color: var(--color-text-secondary);
}

/* === USAGE METER (Phase E3 — $ bucket consumption) === */
.usage-meter {
  margin-top: var(--space-md);
  padding-top: var(--space-md);
  border-top: 1px solid var(--color-border-default);
}
.usage-meter__head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--space-md);
  margin-bottom: var(--space-xs);
}
.usage-meter__label { font-weight: 600; }
.usage-meter__amount { color: var(--color-text-muted); font-size: 0.9rem; }
.usage-meter__track {
  height: 8px;
  background: var(--color-surface-alt);
  border: 1px solid var(--color-border-default);
  border-radius: 999px;
  overflow: hidden;
}
.usage-meter__fill {
  height: 100%;
  background: var(--color-success);
  transition: width 0.3s ease;
}
.usage-meter--warn   .usage-meter__fill { background: var(--color-warning); }
.usage-meter--danger .usage-meter__fill { background: var(--color-error); }
.usage-meter__note {
  margin: var(--space-xs) 0 0;
  font-size: 0.85rem;
  color: var(--color-text-muted);
}
.usage-meter__note--warn { color: var(--color-warning); font-weight: 600; }

/* === TENANT PANEL (Phase F2) — status dots + manual-highlight === */
.status-dot--active    { background: var(--color-success); }
.status-dot--free      { background: var(--color-info); }
.status-dot--past_due  { background: var(--color-warning); }
.status-dot--canceled  { background: var(--color-text-muted); }
.status-dot--suspended { background: var(--color-error); }
/* Hard suspension (STEP 2) — red fill + black ring fuses "danger" + "severe" so
   an abuser stands out from plain-red soft-suspend and plain-black archived. */
.status-dot--locked_out { background: var(--color-error); box-shadow: 0 0 0 2px var(--color-text-primary); }
/* Equalize the Suspend (freeze) / Lock out (hard) buttons so they read as a
   matched pair regardless of label width. */
.tenant-action-btn { min-width: 10.5rem; }
/* Each tenant action (freeze / lock-out / escalate) is a labeled block so its
   reason field reads as tied to its button — not two identical stacked boxes. */
.tenant-action-block__label { font-size: 0.8rem; font-weight: 600; color: var(--color-text-secondary); margin-bottom: var(--space-xs); }
.tenant-action-row { display: flex; gap: var(--space-sm); align-items: center; flex-wrap: wrap; }
.tenant-action-reason { flex: 1; min-width: 14rem; }
[data-highlightable].is-highlighted { background: var(--color-warning-bg); }
.admin-table__email { max-width: 34ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* === TENANT DETAIL (Phase F2 cosmetic) — compact stat grid + member actions === */
.tenant-stat-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--space-md) var(--space-lg); }
.tenant-stat { display: flex; flex-direction: column; gap: 2px; }
.tenant-stat__label { font-size: 0.8rem; color: var(--color-text-muted); }
.tenant-stat__value { font-weight: 600; }
.tenant-member-actions { display: flex; gap: var(--space-xs); align-items: center; }
@media (max-width: 640px) { .tenant-stat-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

/* === ARCHIVE (Phase F3b) — archived status dot + type-to-confirm dialog === */
.status-dot--archived { background: var(--color-text-primary); }
/* Matches the .confirm-dialog family chrome (Phase F polish) — clean 1px card +
   shadow; the type-to-confirm form/markup + its JS are unchanged. */
.archive-dialog { border: 1px solid var(--color-border-default); border-radius: var(--radius-lg); background: var(--color-surface); box-shadow: var(--shadow-lg); padding: var(--space-xl); max-width: 30rem; width: min(460px, 92vw); font: inherit; }
.archive-dialog::backdrop { background: var(--color-overlay-dark); }
.archive-dialog h3 { margin: 0 0 var(--space-sm); font-size: 1.0625rem; font-weight: 600; color: var(--color-text-primary); }
.archive-dialog p { font-size: 0.875rem; color: var(--color-text-secondary); line-height: 1.5; }
.archive-dialog__field { display: block; margin: var(--space-md) 0; }
.archive-dialog__field > span { display: block; font-size: 0.85rem; margin-bottom: 4px; color: var(--color-text-muted); }
.archive-dialog__actions { display: flex; justify-content: flex-end; gap: var(--space-sm); margin-top: var(--space-lg); padding-top: var(--space-md); border-top: 1px solid var(--color-border-subtle); }
