/* ================================================================== */
/* Mivia Sign — design tokens + base components                       */
/*                                                                    */
/* Follows the brand guidelines exactly:                              */
/*   Palette  : Chartreuse #D4E030 · Ink #0A0A0A · Bone #F0EEE9 ·     */
/*              White #FFFFFF                                         */
/*   Primary  : Plus Jakarta Sans (Regular/Medium/SemiBold only —     */
/*              never Light 300, never Bold 700)                      */
/*   Secondary: Geist Mono — labels, metadata, timestamps, technical  */
/*              readouts. Always ALL CAPS. Always Medium 500.         */
/*              Never used for body copy or large sizes.              */
/*                                                                    */
/* Default theme is LIGHT — bone ground, ink text. Dark surfaces      */
/* exist for hero cards + the plugin, not for the whole app.          */
/* ================================================================== */

/*
  Fonts are self-hosted from /brand/fonts/*.woff2 rather than
  loaded from Google Fonts. Three reasons:
    1. No DNS + TLS hop to fonts.gstatic.com on first paint —
       shaved ~120-250ms on cold loads.
    2. The woff2 URLs are stable under our control, so we can
       <link rel="preload"> them in Base.astro with matching
       URLs and be sure the fetcher + stylesheet match (Google
       rotates hashed URLs between versions, which breaks
       preload hint matching).
    3. Both files are tiny — Plus Jakarta Sans is a single
       variable woff2 covering 400/500/600 (~27KB), Geist Mono
       500 (~15KB). Preloaded, they usually arrive before
       first paint, so there's no FOUT at all on a warm
       connection.

  font-display: swap keeps the fallback visible if the real
  font is slow; the metric-matched fallback declarations below
  make the swap dimensionless so there's no layout shift even
  if the fallback does get painted for a frame.
*/
@font-face {
  font-family: "Plus Jakarta Sans";
  font-style: normal;
  font-weight: 200 800;  /* variable font covers full weight range */
  font-display: swap;
  src: url("/brand/fonts/plus-jakarta-sans-latin.woff2") format("woff2-variations"),
       url("/brand/fonts/plus-jakarta-sans-latin.woff2") format("woff2");
}
@font-face {
  font-family: "Geist Mono";
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url("/brand/fonts/geist-mono-500-latin.woff2") format("woff2");
}

/*
  Metric-matched fallbacks. The ascent / descent / line-gap /
  size-adjust numbers make "-fallback" occupy exactly the same
  line box as the real face, so when the real font swaps in
  there's zero layout shift (no CLS).

  Plus Jakarta Sans vs Arial — derived via capsize methodology:
    size-adjust     = xHeight(PJS) / xHeight(Arial)      = 102.4%
    ascent-override = ascent(PJS)  / (upm × size-adjust) =  99.6%
    descent-override= descent(PJS) / (upm × size-adjust) =  39.6%
    line-gap-override= 0 (PJS has zero typographic line-gap)

  The previous values (descent 23%, size-adjust 102%) had the
  descent number ~17pp too low, so the fallback box was
  noticeably shorter than the real face — that's where the
  tiny layout jolt on first paint was coming from.
*/
@font-face {
  font-family: "Plus Jakarta Sans-fallback";
  src: local("Arial");
  ascent-override: 99.6%;
  descent-override: 39.6%;
  line-gap-override: 0%;
  size-adjust: 102.4%;
}
@font-face {
  font-family: "Geist Mono-fallback";
  src: local("Menlo"), local("Monaco"), local("Consolas");
  ascent-override: 100.1%;
  descent-override: 25.4%;
  line-gap-override: 0%;
  size-adjust: 100.4%;
}

:root {
  /* Brand palette */
  --chartreuse:       #D4E030;
  --chartreuse-hover: #C2CE25;  /* one notch darker for interaction */
  --ink:              #0A0A0A;
  --bone:             #F0EEE9;
  --white:            #FFFFFF;

  /* Tokens (component-level). Keep these mapped to the brand
     primitives so a future theme flip is a one-file edit. */
  --bg:              var(--bone);      /* page ground */
  --surface:         var(--white);     /* raised cards */
  --surface-sunken:  #E6E3DD;          /* hover / inset */
  --fg:              var(--ink);       /* body text */
  --fg-dim:          rgba(10, 10, 10, 0.58);  /* secondary text */
  --fg-faint:        rgba(10, 10, 10, 0.38);  /* tertiary / disabled */
  --accent:          var(--chartreuse);
  --accent-hover:    var(--chartreuse-hover);
  --danger:          #E83D35;          /* from the "Offline Mode" plugin card */
  --border:          rgba(10, 10, 10, 0.12);
  --border-strong:   rgba(10, 10, 10, 0.22);
  --radius:          14px;
  --radius-sm:       8px;
  --radius-lg:       20px;

  /* Type — real face first, metric-matched fallback second,
     then system fonts. While Google Fonts is loading, the
     fallback paints in identical metrics so the swap is
     imperceptible. */
  --font:       "Plus Jakarta Sans", "Plus Jakarta Sans-fallback",
                -apple-system, BlinkMacSystemFont, "Inter",
                system-ui, sans-serif;
  --font-mono:  "Geist Mono", "Geist Mono-fallback",
                ui-monospace, SFMono-Regular, "SF Mono",
                Menlo, Consolas, monospace;

  /* Height of the sticky/fixed navigation bar. Exposed so page
     layouts can account for it (body padding, hero math, anchor
     scroll-margin). Updated per breakpoint below. */
  --nav-h: 56px;

  /* Site-wide content rail. Every page wrapper (hero inner,
     landing-section, main, library-wrap, plug-section-inner)
     resolves to the same content edge so vertical scroll across
     the site feels like one column. `--rail` is the inner content
     max-width; `--rail-x` is the horizontal padding around it.
     For one-layer wrappers (max-width + padding on the same
     element) use `max-width: calc(var(--rail) + 2 * var(--rail-x))`.
     For two-layer wrappers (outer bg/padding + inner div) put
     `max-width: var(--rail)` on the inner. */
  --rail:    1100px;
  --rail-x:  clamp(24px, 5vw, 64px);

  /* Contribution-heatmap ramp on the /projects page. Five fixed
     levels — empty + four chartreuse stops — bucketed in
     heatmap.js by fixed thresholds (1/3/6/10+). L0 reuses the
     existing surface-sunken bone tint so empty days read as
     part of the page, not as data. L3 is the brand chartreuse
     itself; L4 is one notch deeper for the heaviest days. */
  --heat-l0: #E6E3DD;
  --heat-l1: #EFF2C8;
  --heat-l2: #E0E580;
  --heat-l3: #D4E030;
  --heat-l4: #A8B324;
}
@media (max-width: 720px) {
  :root { --nav-h: 50px; }
}

* { box-sizing: border-box; }

/* Best-practice touch targets on iOS/Android: Apple HIG + MD
   both recommend 44×44 px minimum. Grab is disabled on decorative
   SVG so long-press doesn't pop an image menu. */
html {
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
  -webkit-tap-highlight-color: transparent;
  color-scheme: light;
  /*
    scroll-behavior stays `auto` (the default). Smooth scrolling
    was tempting for anchor jumps but it also animates the
    browser's scroll-restoration on refresh — so a hard reload
    would animate-scroll down to wherever the user last was,
    instead of landing instantly at y=0. Any page that needs a
    smooth anchor jump can opt in via element-level
    `scroll-behavior: smooth` or a JS scroll with
    `behavior: "smooth"`.
  */
}
img, svg { user-select: none; -webkit-user-drag: none; }

/* Honour prefers-reduced-motion — strip all animations + smooth
   scrolling. Lighthouse A11y + Apple requirement. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
}

[hidden] { display: none !important; }

html, body {
  margin: 0;
  padding: 0;
  color: var(--fg);
  font-family: var(--font);
  font-weight: 500;            /* Medium is our body weight */
  font-size: 16px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  /*
    Kill any horizontal scroll globally. A full-bleed chartreuse
    strip (or any section that extends beyond the viewport edge)
    will trigger iOS/trackpad elastic overflow sideways otherwise,
    which exposes the background through the document edge. Pair
    `overflow-x: clip` with `overscroll-behavior-x: none` so the
    rubber-band gesture resolves against the document edge without
    ever revealing anything behind it.
  */
  overflow-x: clip;
  overscroll-behavior-x: none;
}

/*
  Rubber-band overscroll colour — split top / bottom.
  ---------------------------------------------------------------
  The requirement is:
    Top bounce    → chartreuse (matches the sticky nav)
    Bottom bounce → ink        (matches the ink footer)

  Chromium + Safari propagate only the *computed background
  colour* of the root element to the "canvas" surface that shows
  during rubber-band. Gradient images on html do NOT stretch into
  the over-scroll area — they get clipped to the element box.

  Workaround that works in both engines:
   1. html gets a solid ink background colour → bottom bounce.
   2. A pseudo-element on html sits `position: fixed` across the
      top half of the viewport with chartreuse. position:fixed
      elements DO get revealed during rubber-band (Safari stages
      them on a layer that scales with the viewport). z-index: -1
      keeps it behind body so body's opaque bone paints over it
      during normal scroll — the chartreuse only surfaces when
      the body edge pulls down off the viewport during the top
      bounce.
*/
html { background: var(--ink); }
html::before {
  content: "";
  position: fixed;
  inset: 0 0 auto 0;           /* top-left-right, bottom auto */
  height: 50vh;
  background: var(--chartreuse);
  z-index: -1;
  pointer-events: none;
}
body {
  background: var(--bg);
  /*
    Room for the fixed nav — body content starts below the nav
    rather than underneath it. Fixed nav means the bar stays
    stationary during rubber-band on iOS (and sits above the
    compositor scroll on macOS) for a more stable feel than the
    old sticky nav which followed the content during the bounce.
  */
  padding-top: var(--nav-h);
}

/* Anchor-link jumps clear the fixed nav — otherwise the target
   heading would land behind the nav. */
:target, [id] { scroll-margin-top: calc(var(--nav-h) + 8px); }

body { min-height: 100vh; }

/*
  Smart ::selection that adapts to the bg of whatever element
  the user is dragging across. Driven by a custom property pair
  (--sel-bg / --sel-fg) seeded with the default at :root and
  overridden by an inline-script DOM walker (in Base.astro) on
  any element whose computed background-color matches the
  brand palette:
    chartreuse bg → 30% ink highlight (soft tint, doesn't blow
                    out on the bright surface)
    ink bg        → chartreuse highlight (re-asserts the brand
                    on the ink button INSIDE a chartreuse
                    section, where the parent's --sel-bg would
                    otherwise cascade through)
  Custom properties cascade naturally to ::selection on every
  descendant, so nothing else needs to know about this.
*/
:root {
  --sel-bg: var(--chartreuse);
  --sel-fg: var(--ink);
}
::selection {
  background: var(--sel-bg);
  color: var(--sel-fg);
}

a { color: inherit; text-decoration: none; }
/*
  No global a:hover colour change. A prior rule forced every <a>
  hover to ink, which turned chartreuse-on-ink pill buttons (e.g.
  the landing hero CTA) invisible on hover when their own :hover
  override happened to lose the specificity fight. Buttons define
  their own hover states; body-text links inherit ink already.
*/

/* Type hierarchy — SemiBold for titles, never Bold. */
h1, h2, h3, h4 {
  font-weight: 600;
  letter-spacing: -0.02em;
  margin: 0;
  color: var(--ink);
}
h1 { font-size: clamp(2rem, 4.5vw, 3rem);   line-height: 1.05; }
h2 { font-size: clamp(1.5rem, 3vw, 2rem);   line-height: 1.1; }
h3 { font-size: 1.125rem;                    line-height: 1.25; }

p { margin: 0; }

code, kbd {
  font-family: var(--font-mono);
  font-weight: 500;
  font-size: 0.85em;
  background: var(--surface-sunken);
  padding: 1px 6px;
  border-radius: 4px;
}

/* Canonical mono label: small, all caps, medium weight, 0.06em
   tracking. Use for role pills, quota chips, status readouts,
   table column headers, form label hints — anywhere the brand
   guidelines call for Geist Mono. Never use for body copy. */
.mono,
[data-mono] {
  font-family: var(--font-mono);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-feature-settings: "tnum";
}

/* Tnum on any element that shows a timestamp or figure. */
time, .tnum { font-feature-settings: "tnum"; }

main {
  max-width: calc(var(--rail) + 2 * var(--rail-x));
  margin: 0 auto;
  padding: 72px var(--rail-x) 96px;
  display: flex;
  flex-direction: column;
  gap: 32px;
}

header h1 { margin: 0 0 8px; }

header .tagline {
  color: var(--fg-dim);
  font-size: 1rem;
  margin: 0;
  line-height: 1.5;
}

/* ----------------- Header / top-of-page nav ----------------- */

#mivia-header {
  /*
    Fixed (not sticky) — stays pinned to the viewport during
    rubber-band bouncing on iOS Safari. On macOS Safari the
    compositor still shifts fixed elements during the bounce but
    the effect is subtler than sticky. Body gets matching
    padding-top: var(--nav-h) so content doesn't slide under.
  */
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 40;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  height: var(--nav-h);
  padding: 0 clamp(24px, 4vw, 48px);
  background: var(--chartreuse);
}
/* Pages can opt-in via body.nav-follows-body to make the nav
   dissolve into the page bg instead of presenting a chartreuse
   slab. The nav inherits body's computed bg-color in real time
   — no transition declaration on the nav itself, otherwise it
   would lag one cycle behind body's 600ms takeover transition.
   The inherited value tracks body's in-flight animated colour
   each frame, so nav + body change in perfect lock-step. */
body.nav-follows-body #mivia-header { background: inherit; }
/* Kill the global html::before chartreuse-top-50vh layer on
   nav-follows-body pages. With nav now bone, that chartreuse
   strip leaks into view during top-overscroll bounce (and
   anywhere else body bg doesn't fully paint), reading as a
   stale brand band the page no longer wants. The footer-bg JS
   sniffer already pins <html> bg to body's current colour for
   bottom overscroll, so removing the pseudo here gives bone
   (or chartreuse during the success takeover) at both edges. */
html:has(body.nav-follows-body) { background: var(--bone); }
html:has(body.nav-follows-body)::before { display: none; }

.mivia-header-brand {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  color: var(--ink);
  text-decoration: none;
}
.mivia-header-brand img,
.mivia-header-brand svg {
  height: 22px;
  width: auto;
  display: block;
  /*
    Declare the transform transition on the base rule so that the
    animation runs in both directions for same-page state flips
    (scroll → `.is-scrolled` toggles).
  */
  transform-origin: left top;
  transition: transform 560ms cubic-bezier(0.2, 0.8, 0.2, 1);
  will-change: transform;
  /*
    Pull the wordmark out of the root view-transition group so
    cross-page navigations morph its size instead of crossfading
    two snapshots. Astro's ClientRouter snapshots the whole page
    on both sides of the swap — even with transition:persist on
    the header, both snapshots capture the img at its two
    different scales, and the root crossfade makes it look like
    a teleport. Naming the img creates its own VT group; the
    browser animates that group's size between the old and new
    states. Same-duration + same easing as the CSS transition so
    both code paths feel identical.
  */
  view-transition-name: mivia-brand;
}
::view-transition-group(mivia-brand) {
  animation-duration: 560ms;
  animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
}


/*
  Oversized brand at page top on chartreuse-hero pages.
  ----------------------------------------------------
  On pages whose hero is chartreuse the nav bar visually merges with
  the hero, so the wordmark can breathe at 2× scale for a beat. As
  soon as the user scrolls any amount, body.is-scrolled is toggled
  on and the wordmark shrinks back to its 1× resting size.

  We scale via `transform` (not height/width) so the bounding box
  the flex parent measures stays at 22px — the rest of the nav row
  (links, avatar, Log in / Create account) never reflows even as
  the wordmark grows and shrinks. `transform-origin: left center`
  anchors the growth to the left padding edge so only empty
  vertical space to its right is borrowed.

  Reduced-motion users get the resting size immediately — no
  transition and no oversized state.
*/
body.has-chartreuse-hero .mivia-header-brand img,
body.has-chartreuse-hero .mivia-header-brand svg {
  transform: scale(1.6);
}
body.has-chartreuse-hero.is-scrolled .mivia-header-brand img,
body.has-chartreuse-hero.is-scrolled .mivia-header-brand svg {
  transform: scale(1);
}
@media (prefers-reduced-motion: reduce) {
  .mivia-header-brand img,
  .mivia-header-brand svg {
    transition: none;
  }
  body.has-chartreuse-hero .mivia-header-brand img,
  body.has-chartreuse-hero .mivia-header-brand svg {
    transform: none;
  }
}

.mivia-header-actions {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
}

.mivia-header-link {
  appearance: none;
  background: transparent;
  border: none;
  color: var(--fg);
  cursor: pointer;
  font-family: inherit;
  font-size: 0.9rem;
  font-weight: 500;
  font-variation-settings: "wght" 500;
  padding: 8px 12px;
  border-radius: var(--radius-sm);
  text-decoration: none;
  line-height: 1;
  position: relative;        /* so text paints above the pill */
  z-index: 1;
  /* inline-flex column lets a 0-height ::before phantom (always
     bold) reserve the bold-weight WIDTH of the label, while the
     visible text below renders at the link's current weight. The
     parent's outer width is therefore the bold width regardless
     of state — animating wght up/down on aria-current changes
     does not push siblings around. */
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  transition:
    color 120ms ease,
    font-variation-settings 240ms cubic-bezier(0.32, 0.72, 0, 1),
    font-weight 240ms cubic-bezier(0.32, 0.72, 0, 1);
}
.mivia-header-link::before {
  content: attr(data-text);
  font-weight: 700;
  font-variation-settings: "wght" 700;
  height: 0;
  overflow: hidden;
  visibility: hidden;
  pointer-events: none;
}
.mivia-header-link:focus-visible {
  outline: none;
}
/* Active + hover indicator — the variable font interpolates wght
   500 → 700; the ::before phantom above keeps the bounding box at
   bold width so neighbouring links don't shift. JS sets
   aria-current="page" on the matching link; :hover bolds whatever
   the cursor is over. */
.mivia-header-link[aria-current="page"],
.mivia-header-link:hover,
.mivia-header-link:focus-visible {
  font-weight: 700;
  font-variation-settings: "wght" 700;
}
/* If any sibling link is hovered, the current-page link relaxes
   back to normal weight — only one link should ever read as
   "selected" at a time, and the user's hover wins because that's
   the link they're about to click. */
.mivia-header-actions--desktop:has(.mivia-header-link:hover)
  .mivia-header-link[aria-current="page"]:not(:hover) {
  font-weight: 500;
  font-variation-settings: "wght" 500;
}

.mivia-header-actions--desktop {
  position: relative;
}

.mivia-header-cta {
  background: var(--ink);
  color: var(--chartreuse);
  border-radius: 999px;        /* fully rounded pill, like the primary buttons */
  padding: 10px 14px;          /* +2px on every side vs the bare nav link */
}
.mivia-header-cta:hover,
.mivia-header-cta:focus-visible {
  background: var(--ink);
  color: var(--white);
}
/* On nav-follows-body pages the nav is bone, not chartreuse, so
   the CTA's chartreuse text reads as a stray brand accent floating
   on a neutral bg. Drop the text to bone so the pill is just an
   ink slab with bone label, matching the page palette. Hover
   still goes to white for the bump. */
body.nav-follows-body .mivia-header-cta {
  color: var(--bone);
}
/* The CTA is its own affordance (filled pill) — the wght bump
   used by the text nav links is redundant noise on a button.
   Keep weight + variation steady on hover/focus, override the
   :has() rule for the desktop row. The phantom ::before is still
   harmless (height: 0) but we hide it explicitly for clarity. */
.mivia-header-cta,
.mivia-header-cta:hover,
.mivia-header-cta:focus-visible,
.mivia-header-cta[aria-current="page"],
.mivia-header-actions--desktop:has(.mivia-header-link:hover)
  .mivia-header-cta:not(:hover) {
  font-weight: 500;
  font-variation-settings: "wght" 500;
}
.mivia-header-cta::before { display: none; }

/* ----------------- Buttons ----------------- */
/*
  Three buttons:
    primary-button   → Ink bg, White text (confident, default action)
    accent-button    → Chartreuse bg, Ink text (shopping / upgrade moments)
    secondary-button → outlined ink (secondary / ghost action)
  `.reset-button` is an alias for the outlined look used inside
  success / error cards.
*/
.primary-button,
.accent-button,
.secondary-button,
.reset-button {
  appearance: none;
  font-family: inherit;
  font-weight: 600;
  font-size: 0.95rem;
  line-height: 1;
  padding: 13px 22px;
  border-radius: 999px;
  cursor: pointer;
  border: 1px solid transparent;
  transition: background 120ms ease, color 120ms ease,
              border-color 120ms ease, transform 60ms ease;
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
}

/*
  Button colours are resolved through CSS variables so a
  chartreuse-bg ancestor can flatten the hover state and flip
  the ink-fill text colour without needing per-page rules. The
  defaults below preserve the historical look on bone / ink /
  white surfaces.
    --btn-ink-fg / -hover         → ink-filled pill text
    --btn-ink-bg-hover            → ink-filled pill hover bg
    --btn-ghost-bg-hover          → outlined pill hover bg
    --btn-ghost-border-hover      → outlined pill hover border
  Chartreuse contexts (body.is-result-success and any element
  the smart-selection adapter tags as chartreuse-bg) override
  these so hover === rest visually.
*/
.primary-button {
  background: var(--ink);
  color: var(--btn-ink-fg, var(--white));
}
/* Default hover on bone backgrounds: brand chartreuse fill with
   ink text — the primary action lights up on hover with the
   site's signature colour. The CSS-var fallbacks here drive
   that default; chartreuse-bg contexts (body.is-result-success
   + smart-selection adapter) override the vars so a button
   already sitting on chartreuse stays calm on hover (rest ===
   hover) instead of fighting the ground colour. */
.primary-button:hover:not(:disabled) {
  background: var(--btn-ink-bg-hover, var(--chartreuse));
  color: var(--btn-ink-fg-hover, var(--ink));
}

.accent-button {
  background: var(--chartreuse);
  color: var(--ink);
}
.accent-button:hover:not(:disabled) {
  background: var(--chartreuse-hover);
}

.secondary-button,
.reset-button {
  background: transparent;
  color: var(--ink);
  border-color: var(--border-strong);
}
.secondary-button:hover:not(:disabled),
.reset-button:hover:not(:disabled) {
  background: var(--btn-ghost-bg-hover, rgba(10, 10, 10, 0.05));
  border-color: var(--btn-ghost-border-hover, var(--ink));
}

.primary-button:disabled,
.accent-button:disabled,
.secondary-button:disabled,
.reset-button:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

.primary-button:active:not(:disabled),
.accent-button:active:not(:disabled) {
  transform: scale(0.99);
}

/* ----------------- Inputs / fields ----------------- */
.field {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.field label,
.field-label {
  font-family: var(--font-mono);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-size: 0.72rem;
  color: var(--fg-dim);
}

.field input[type="text"],
.field input[type="email"],
.field input[type="password"],
.field input[type="search"],
.field select,
.field textarea,
input[type="text"].input,
input[type="email"].input,
input[type="password"].input {
  appearance: none;
  background: var(--surface);
  border: 1px solid var(--border);
  color: var(--ink);
  padding: 12px 14px;
  /* 16px+ stops iOS Safari from zooming when the field is
     focused — anything below 16 triggers the auto-zoom. */
  font-size: 1rem;
  font-family: inherit;
  border-radius: var(--radius-sm);
  outline: none;
  transition: border-color 120ms ease, background 120ms ease,
              box-shadow 120ms ease;
  width: 100%;
}
.field input:focus,
.field select:focus,
.field textarea:focus {
  border-color: var(--ink);
}
.field input::placeholder { color: var(--fg-faint); }

.field select {
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'><path fill='%230a0a0a' d='M6 8L0 0h12z'/></svg>");
  background-repeat: no-repeat;
  background-position: right 14px center;
  padding-right: 36px;
  cursor: pointer;
}

.field-help {
  color: var(--fg-dim);
  font-size: 0.82rem;
  line-height: 1.5;
  margin: 0;
}

/* ============================================================
   Whole-page drop UX — used by /sign + /verify.

   Replaces the old boxed dashed `.dropzone` pattern (still used
   below for the per-recipient form on /sign). The page itself
   is the drop target via `wireDocumentDrop` in app.js; the
   visual element here is just an idle hero, a processing
   ring, a result card, or an error card — depending on
   `data-stage` on the parent.

   Apple-feel motion: 480ms crossfades on stage swap, 600ms
   body-bg takeover on success, soft cubic-bezier ease.
   ============================================================ */

/* The page wrapper that holds the staged hero. Centred,
   generous padding, pulls the eye to the middle. */
.drop-page {
  max-width: calc(var(--rail) + 2 * var(--rail-x));
  margin: 0 auto;
  padding: clamp(80px, 14vw, 160px) var(--rail-x) clamp(80px, 14vw, 160px);
  min-height: calc(100vh - var(--nav-h));
  min-height: calc(100svh - var(--nav-h));
  display: flex;
  align-items: center;
  justify-content: center;
}
/* The upper cluster of the configure stage (eyebrow + title +
   filename pill + segmented control) is wrapped in
   .drop-configure-head. In SINGLE mode the wrapper is layout-
   transparent (display: contents) so the whole cluster including
   the buttons is centred as one unit by .drop-page's flex
   centring — preserving the existing single-mode look exactly.
   In MULTI mode we stop centring the entire (now very tall)
   cluster and instead pin .drop-configure-head to its OWN
   100vh-tall flex region with internal centring — locking the
   eyebrow / title / filename / segmented Y at the same place as
   single mode (matched via the padding-bottom that compensates
   for single's buttons + gap below the segmented control). The
   multi body then flows below the head in normal block flow. */
.drop-configure-head {
  display: contents;
}
.drop-page.is-mode-multi {
  flex-direction: column;
  align-items: stretch;
  justify-content: flex-start;
  padding-top: 0;
  padding-bottom: clamp(40px, 8vw, 80px);
}
.drop-page.is-mode-multi .drop-configure {
  display: block;
  text-align: center;
}
.drop-page.is-mode-multi .drop-configure-head {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 18px;
  /* Push the head down to land at the same Y as single-mode's
     centred cluster top. Single mode centres a cluster of
     height --single-cluster-h within 100vh - nav, putting the
     head_top at (100vh - nav - cluster) / 2. We use the same
     formula as padding-top so the head's first child sits at the
     same Y. The variable is measured by the inline script in
     sign.astro / sign-mock.astro on the very first setMode() call
     (while still in single mode); falls back to a 310px estimate
     before measurement. The head has no min-height so its box
     height = padding-top + content_h, and the body flows directly
     below the content (no extra gap). */
  padding-top: max(40px, calc((100vh - var(--nav-h) - var(--single-cluster-h, 310px)) / 2));
  padding-top: max(40px, calc((100svh - var(--nav-h) - var(--single-cluster-h, 310px)) / 2));
}
.drop-page.is-mode-multi .sign-mode-body--multi {
  margin-top: 24px;
}

/* The stage container. All four stages live as sibling
   sections; `data-stage` on this element decides which is
   visible. Each stage handles its own enter/exit animation
   via `.is-stage` rules below. */
.drop-stage {
  width: 100%;
  position: relative;
}
.drop-stage > section {
  display: none;
  opacity: 0;
  transform: translateY(8px) scale(0.99);
  transition:
    opacity 360ms cubic-bezier(0.32, 0.72, 0, 1),
    transform 360ms cubic-bezier(0.32, 0.72, 0, 1);
  text-align: center;
}
.drop-stage[data-stage="idle"]       .drop-idle,
.drop-stage[data-stage="configure"]  .drop-configure,
.drop-stage[data-stage="processing"] .drop-processing,
.drop-stage[data-stage="success"]    .drop-success,
.drop-stage[data-stage="error"]      .drop-error {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 18px;
  opacity: 1;
  transform: translateY(0) scale(1);
}

/* Idle copy. Big confident headline, small tagline,
   single Choose-a-file pill, format readout. */
.drop-idle .drop-eyebrow,
.drop-configure .drop-eyebrow,
.drop-success .drop-eyebrow,
.drop-error .drop-eyebrow {
  font-family: var(--font-mono);
  font-weight: 500;
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--fg-dim);
  margin: 0;
}
.drop-eyebrow--ink    { color: var(--ink) !important; }
.drop-eyebrow--danger { color: var(--danger) !important; }

.drop-headline {
  font-size: clamp(2.25rem, 6vw, 3.75rem);
  font-weight: 600;
  letter-spacing: -0.025em;
  line-height: 1.05;
  color: var(--ink);
  margin: 4px 0 0;
}
.drop-sub {
  font-size: 1.05rem;
  line-height: 1.55;
  color: var(--fg-dim);
  max-width: 48ch;
  margin: 0;
}

.drop-pick {
  display: inline-flex;
  align-items: center;
  cursor: pointer;
  padding: 14px 26px;
  border-radius: 999px;
  background: var(--ink);
  color: var(--bone);
  font-size: 0.95rem;
  font-weight: 600;
  letter-spacing: -0.005em;
  /* color transitions too so the text → ink swap on hover
     reads alongside the bg → chartreuse swap. */
  transition:
    background 160ms cubic-bezier(0.32, 0.72, 0, 1),
    color 160ms cubic-bezier(0.32, 0.72, 0, 1),
    transform 160ms cubic-bezier(0.32, 0.72, 0, 1);
  margin-top: 8px;
}
.drop-pick:hover  {
  background: var(--chartreuse);
  color: var(--ink);
}
.drop-pick:active { transform: scale(0.98); }

.drop-formats {
  font-family: var(--font-mono);
  font-weight: 500;
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--fg-dim);
  margin: 4px 0 0;
}

/* Processing — filename above, ring centred, phase below.
   The ring stroke colours flip on the chartreuse takeover, but
   the chartreuse takeover only fires AFTER processing ends, so
   inside this block we always show ink-on-bone. */
.drop-processing { padding-top: 8px; }
.drop-filename {
  font-family: var(--font-mono);
  font-weight: 500;
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--fg-dim);
  margin: 0;
  /* Stay on one line — long filenames truncate with an ellipsis
     instead of wrapping into a multi-line block that pushes the
     ring around. */
  display: block;
  max-width: min(60ch, 90%);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.drop-ring {
  width: 96px;
  height: 96px;
  display: block;
}
.drop-ring-track,
.drop-ring-head {
  fill: none;
  stroke-width: 4;
  stroke-linecap: round;
}
.drop-ring-track { stroke: rgba(10, 10, 10, 0.10); }
.drop-ring-head {
  stroke: var(--ink);
  /* Default: indeterminate-style — short visible arc that the
     `.is-indeterminate` animation rotates around the circle.
     determinate mode overrides via inline style. */
  stroke-dasharray: 60 229;
  stroke-dashoffset: 0;
  transform-origin: 50% 50%;
  transform: rotate(-90deg);
  transition:
    stroke-dashoffset 280ms cubic-bezier(0.32, 0.72, 0, 1);
}
.drop-ring-head.is-indeterminate {
  animation: drop-ring-spin 1.4s linear infinite;
}
@keyframes drop-ring-spin {
  from { transform: rotate(-90deg); }
  to   { transform: rotate(270deg); }
}
.drop-phase {
  font-size: 1rem;
  font-weight: 500;
  color: var(--ink);
  margin: 0;
}

/* Success + error — large headline, optional sub / fields,
   row of CTAs at the bottom. Result fields render as a
   two-column dl on wide and a single column on narrow. */
.drop-success .drop-result-title,
.drop-error .drop-error-title {
  font-size: clamp(2rem, 5vw, 3rem);
  font-weight: 600;
  letter-spacing: -0.025em;
  line-height: 1.1;
  color: var(--ink);
  margin: 4px 0 0;
  /* Cap the headline width so long copy doesn't sprawl the
     full viewport — keeps the eye anchored on a centred block. */
  max-width: 22ch;
}
.drop-success .drop-result-sub,
.drop-error .drop-error-sub {
  font-size: 1.05rem;
  line-height: 1.55;
  color: var(--fg-dim);
  max-width: 48ch;
  margin: 0;
}

/* Public-state attribution + note. Used when /verify resolves
   to a Mivia-signed file the viewer is NOT a stakeholder of —
   we surface the signer's name only and explain why the rest
   is hidden. Replaces the old dl-with-hint layout that wrapped
   short values into one-character columns. */
.drop-result-attribution {
  font-size: clamp(1.4rem, 3vw, 1.8rem);
  font-weight: 600;
  letter-spacing: -0.015em;
  line-height: 1.2;
  color: var(--ink);
  margin: 12px 0 0;
  max-width: 30ch;
}
.drop-result-note {
  font-size: 0.95rem;
  line-height: 1.55;
  color: var(--ink);
  opacity: 0.72;
  max-width: 44ch;
  margin: 0;
}

/* License message rendered by the file owner — shown to public
   viewers who verify one of their files. Slightly styled so it
   reads as a contact line rather than body copy. */
.drop-result-license-message {
  font-family: var(--font-mono);
  font-weight: 500;
  font-size: 0.92rem;
  line-height: 1.55;
  color: var(--ink);
  background: rgba(10, 10, 10, 0.10);
  padding: 12px 18px;
  border-radius: 999px;
  margin: 8px 0 0;
  max-width: 56ch;
  white-space: pre-wrap;
}

/* Owner-only recommendation banner — appears when the file was
   only fingerprinted (plugin auto-register) and the owner is
   verifying it themselves, prompting them to also watermark it
   before sharing wide. */
.drop-result-recommendation {
  font-size: 0.92rem;
  line-height: 1.55;
  color: var(--ink);
  background: rgba(10, 10, 10, 0.08);
  border-left: 3px solid var(--ink);
  padding: 12px 16px;
  border-radius: var(--radius-sm);
  margin: 8px 0 0;
  max-width: 56ch;
  text-align: left;
}
.drop-error .drop-error-title { color: var(--danger); }

.drop-result-fields {
  display: grid;
  /* Tight label column so the value column gets as much room as
     possible. dt sizes to its widest label; dd takes the rest of
     the row via `1fr`. */
  grid-template-columns: max-content 1fr;
  column-gap: 24px;
  row-gap: 0;
  /* Centre the dt and dd line-boxes vertically against the row's
     centre line. Baseline alignment was technically correct but
     visually wrong here — the dt is small caps with no descenders,
     so sharing a baseline with the dd's lowercase-bearing text
     parked the dt at the BOTTOM of the dd's glyph mass instead of
     its middle. With matching line-heights below, line-box centre
     ≈ glyph centre for both, so centre alignment puts the visual
     middles of the two texts on the same horizontal axis. */
  align-items: center;
  text-align: left;
  /* Adaptive width: shrink to fit the longest natural row, capped
     at the on-screen budget. ``fit-content(720px)`` is the magic
     value here — it sizes the grid to its intrinsic content width
     when that's smaller than 720px (so a table of short values
     stays compact and tightly centred), and clamps to 720px when
     a long filename would otherwise blow the layout out (in which
     case the dd's inner ellipsis-span kicks in). margin: auto on
     both sides keeps the result horizontally centred no matter
     which size the grid resolves to. */
  width: fit-content(720px);
  max-width: 100%;
  margin: 16px auto 8px;
}
.drop-result-fields dt,
.drop-result-fields dd {
  margin: 0;
  /* min-width: 0 is the canonical "let me shrink inside a grid
     track even though my content has its own intrinsic width"
     escape hatch — the inner ellipsis-span on the dd needs it. */
  min-width: 0;
  /* Modest vertical breathing room between rows — enough to read
     each pair as its own line, not so much that the table feels
     airy. 8px above + 8px below = ~16px between consecutive text
     lines on top of their natural line-height. */
  padding: 8px 0;
}
.drop-result-fields dt {
  font-family: var(--font-mono);
  font-weight: 500;
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: rgba(10, 10, 10, 0.62);
  /* Match the dd's line-height so the baseline alignment lines
     up cleanly without inheriting whatever ambient body
     line-height happens to be. */
  line-height: 1.4;
  /* Labels are short — no ellipsis needed, just don't let them
     wrap onto a second line if a future label gets long. */
  white-space: nowrap;
}
/* dd is a flex row so an optional info icon can sit beside the
   value without breaking the ellipsis truncation on the text. */
.drop-result-fields dd {
  display: flex;
  align-items: center;
  gap: 8px;
  color: var(--ink);
  font-size: 0.96rem;
  /* Match the dt's natural line-height so the centring resolves
     cleanly across rows. */
  line-height: 1.4;
}
/* The text span carries the ellipsis styles. flex: 0 1 auto +
   min-width: 0 lets it shrink below its intrinsic width when the
   row is too narrow, so `text-overflow: ellipsis` actually fires
   instead of the text just overflowing. */
.drop-result-fields dd > .dd-text {
  flex: 0 1 auto;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Info icon — small circle-i sitting next to any element that
   needs a hover-revealed explanation. Originally written for the
   verify card's Mark-type field (.drop-result-fields dd > .dd-info)
   but generalised so any markup can drop in
   `<span class="dd-info"><svg.../><span class="dd-info-bubble">…</span></span>`
   and get the same tooltip behaviour. tabindex=0 makes it
   focusable so keyboard users get the same affordance.
   `position: relative` anchors the absolutely-positioned bubble. */
.dd-info {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 16px;
  height: 16px;
  color: rgba(10, 10, 10, 0.55);
  cursor: pointer;
  transition: color 120ms ease;
}
.dd-info:hover,
.dd-info:focus-visible {
  color: var(--ink);
  outline: none;
}
.dd-info svg {
  width: 100%;
  height: 100%;
  display: block;
}
/* Hover/focus tooltip. Positioned above the icon, centred on it,
   with a small downward-pointing caret so the relationship reads
   instantly. pointer-events: none means the bubble doesn't
   intercept hover (which would create a flicker loop when the
   cursor crosses from icon → bubble → icon). */
.dd-info > .dd-info-bubble {
  position: absolute;
  bottom: calc(100% + 10px);
  left: 50%;
  transform: translateX(-50%) translateY(4px);
  width: max-content;
  max-width: min(320px, 80vw);
  padding: 10px 12px;
  background: var(--ink);
  color: var(--bone);
  border-radius: 8px;
  font-family: var(--font-sans, inherit);
  font-size: 0.8rem;
  font-weight: 400;
  line-height: 1.45;
  letter-spacing: 0;
  text-transform: none;
  text-align: left;
  white-space: normal;
  pointer-events: none;
  opacity: 0;
  transition: opacity 140ms ease, transform 140ms ease;
  z-index: 10;
  box-shadow: 0 8px 24px rgba(10, 10, 10, 0.18);
}
.dd-info > .dd-info-bubble::after {
  /* Caret pointing down to the icon. Same dark fill as the
     bubble; positioned just below the bubble's bottom edge. */
  content: "";
  position: absolute;
  top: 100%;
  left: 50%;
  transform: translateX(-50%);
  border: 5px solid transparent;
  border-top-color: var(--ink);
  border-bottom: 0;
}
.dd-info:hover > .dd-info-bubble,
.dd-info:focus-visible > .dd-info-bubble {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
}

.drop-result-row {
  display: flex;
  gap: 12px;
  flex-wrap: wrap;
  justify-content: center;
  margin-top: 12px;
}
.drop-cta {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 13px 22px;
  font-size: 0.95rem;
  font-weight: 600;
  border-radius: 999px;
  border: 1px solid transparent;
  cursor: pointer;
  text-decoration: none;
  font-family: inherit;
  transition: background 160ms cubic-bezier(0.32, 0.72, 0, 1),
              color 160ms cubic-bezier(0.32, 0.72, 0, 1);
  min-width: 160px;
}
.drop-cta--ink {
  background: var(--ink);
  color: var(--btn-ink-fg, var(--bone));
}
.drop-cta--ink:hover {
  background: var(--btn-ink-bg-hover, var(--chartreuse));
  color: var(--btn-ink-fg-hover, var(--ink));
}
.drop-cta--ghost {
  background: transparent;
  color: var(--ink);
  border-color: var(--ink);
}
.drop-cta--ghost:hover { background: var(--btn-ghost-bg-hover, rgba(10, 10, 10, 0.08)); }

/* Whole-viewport drag overlay. Sits below the persistent nav
   (z-index 40) so links remain clickable. The default state is
   invisible; `data-visible="1"` (set by app.js) fades it in. */
.drop-overlay {
  position: fixed;
  inset: 0;
  z-index: 30;
  background: rgba(10, 10, 10, 0.55);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
  opacity: 0;
  transition: opacity 200ms cubic-bezier(0.32, 0.72, 0, 1);
}
.drop-overlay[data-visible="1"] { opacity: 1; }
.drop-overlay-inner {
  padding: 28px 40px;
  border: 1.5px dashed var(--chartreuse);
  border-radius: var(--radius-lg);
  background: rgba(10, 10, 10, 0.30);
  animation: drop-overlay-pulse 1.6s cubic-bezier(0.32, 0.72, 0, 1) infinite;
}
.drop-overlay-text {
  font-family: var(--font-mono);
  font-weight: 500;
  font-size: 0.95rem;
  text-transform: uppercase;
  letter-spacing: 0.16em;
  color: var(--chartreuse);
  margin: 0;
}
@keyframes drop-overlay-pulse {
  0%, 100% { transform: scale(1);    border-color: rgba(212, 224, 48, 0.65); }
  50%      { transform: scale(1.02); border-color: rgba(212, 224, 48, 1.00); }
}

/* Whole-page chartreuse takeover on a successful match.
   Animates body bg over 600ms with the Apple ease. The drop-
   stage's success section already sits centred within the
   page; the takeover just changes the surrounding ground. */
body.is-result-success {
  background: var(--chartreuse);
  transition: background-color 600ms cubic-bezier(0.32, 0.72, 0, 1);
  /* Cascading button-colour overrides — every ink-filled pill
     inside the success body gets chartreuse text (was white)
     and a flat hover state (rest === hover). Outlined pills
     also drop their hover bg. The smart-selection adapter sets
     the same vars on any other chartreuse-bg element it finds,
     so this rule + the JS together cover all chartreuse
     contexts site-wide automatically. */
  --btn-ink-fg: var(--chartreuse);
  --btn-ink-fg-hover: var(--chartreuse);
  --btn-ink-bg-hover: var(--ink);
  --btn-ghost-bg-hover: transparent;
  --btn-ghost-border-hover: var(--ink);
}
/* Idle/error stages still use bone — only success goes
   chartreuse. The body-bg transition runs in both directions,
   so resetting from success → idle smoothly fades back. */

/* Reduced-motion respect. */
@media (prefers-reduced-motion: reduce) {
  .drop-stage > section,
  .drop-overlay,
  .drop-pick,
  .drop-cta,
  body.is-result-success,
  .drop-ring-head { transition: none !important; }
  .drop-ring-head.is-indeterminate,
  .drop-overlay-inner { animation: none !important; }
}

/* Responsive */
@media (max-width: 720px) {
  .drop-page { padding: 64px var(--rail-x) 64px; }
  /* Stack labels above values on phones — the dt + dd flex
     centring works either way, but a single column reads better
     when horizontal real-estate is tight. Tighten padding because
     each row now contributes two visual lines. */
  .drop-result-fields {
    grid-template-columns: 1fr;
    column-gap: 0;
  }
  .drop-result-fields dt { padding: 14px 0 2px; }
  .drop-result-fields dd { padding: 0 0 14px; }
  .drop-cta { width: 100%; max-width: 360px; }
}

/* ----------------- Dropzone (sign + verify) ----------------- */
.dropzone {
  border: 1.5px dashed var(--border-strong);
  border-radius: var(--radius);
  background: var(--surface);
  padding: 56px 24px;
  text-align: center;
  cursor: pointer;
  transition: border-color 120ms ease, background 120ms ease,
              transform 60ms ease;
}
.dropzone--compact { padding: 32px 24px; }
.dropzone--hero    { padding: 64px 24px; border-radius: var(--radius-lg); }

.dropzone:hover,
.dropzone:focus-within,
.dropzone.dropzone--over,
.dropzone.is-dragging {
  border-color: var(--ink);
  background: var(--surface);
  outline: none;
}
.dropzone.is-dragging { transform: scale(1.004); }

.dropzone-inner {
  display: flex;
  flex-direction: column;
  gap: 12px;
  align-items: center;
}

.dropzone-headline {
  font-size: 1.125rem;
  font-weight: 600;
  margin: 0;
  color: var(--ink);
}
.dropzone-sub {
  color: var(--fg-dim);
  margin: 0;
  font-size: 0.88rem;
}

.dropzone-button {
  display: inline-block;
  background: var(--ink);
  color: var(--white);
  padding: 10px 18px;
  border-radius: var(--radius-sm);
  font-size: 0.88rem;
  font-weight: 600;
  cursor: pointer;
  transition: background 120ms ease, color 120ms ease;
}
.dropzone-button:hover { background: #1a1a1a; color: var(--chartreuse); }

.dropzone-formats {
  /* Mono caption under the dropzone — technical readout territory. */
  font-family: var(--font-mono);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--fg-dim);
  font-size: 0.7rem;
  margin: 8px 0 0;
}

/* ----------------- Status / spinner / progress ----------------- */
.status {
  display: flex;
  gap: 16px;
  align-items: center;
  padding: 20px;
  background: var(--surface);
  border-radius: var(--radius);
  border: 1px solid var(--border);
}
.status p { margin: 0; color: var(--fg-dim); }

.spinner {
  width: 18px; height: 18px;
  border-radius: 50%;
  border: 2px solid var(--border);
  border-top-color: var(--ink);
  animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

.progress {
  width: 100%; height: 6px;
  background: var(--border);
  border-radius: 3px;
  overflow: hidden;
  margin-top: 2px;
}
.progress-bar {
  width: 0%; height: 100%;
  background: var(--ink);
  transition: width 0.15s linear;
}

/* ----------------- Result / error panes ----------------- */
.result, .error {
  background: var(--surface);
  border-radius: var(--radius);
  padding: 28px;
  border: 1px solid var(--border);
}
.result h2, .error h2 {
  margin: 0 0 16px;
  font-size: 1.25rem;
  font-weight: 600;
  letter-spacing: -0.01em;
}
.result h2 { color: var(--ink); }
.error  h2 { color: var(--danger); }
.error  p  { color: var(--fg-dim); margin: 0 0 20px; }

/* ----------------- Sign form (legacy shell) ----------------- */
.sign-form {
  display: flex;
  flex-direction: column;
  gap: 18px;
}

/* ----------------- Disclaimer card ----------------- */
.disclaimer {
  color: var(--fg-dim);
  font-size: 0.78rem;
  line-height: 1.55;
  margin: 0;
  padding: 14px 16px;
  background: var(--surface);
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
}

/* ----------------- Tabs (legacy two-tab switch) ----------------- */
.tabs {
  display: flex;
  gap: 4px;
  padding: 4px;
  background: var(--surface);
  border-radius: 10px;
  border: 1px solid var(--border);
  width: max-content;
}
.tab {
  background: transparent;
  border: none;
  color: var(--fg-dim);
  padding: 8px 20px;
  font-size: 0.85rem;
  font-weight: 500;
  font-family: inherit;
  border-radius: 7px;
  cursor: pointer;
  transition: background 120ms ease, color 120ms ease;
}
.tab:hover       { color: var(--ink); }
.tab--active     { background: var(--ink); color: var(--chartreuse); }

/* ----------------- Footer ----------------- */
footer {
  color: var(--fg-dim);
  font-size: 0.78rem;
  line-height: 1.6;
  margin-top: 24px;
  padding-top: 24px;
}
footer p { margin: 0; }

/* ----------------- Auth pages ----------------- */
/*
  Narrow auth card (login / signup / reset / claim) stays at
  ~440px — correct for a single-form flow. Account uses the
  same shell but widens to ~880px via .auth-card--wide so its
  subscription + plugin-token sections have room to breathe.
*/
.auth-wrap {
  display: flex;
  /*
    main { flex-direction: column } is inherited, so justify-content
    controls the vertical (main) axis. Horizontal centering needs
    align-items on the cross axis — without this, max-width on the
    card capped cross-axis stretch at its max, which visually
    anchored the card to the LEFT of main's inner padding area on
    wide viewports.
  */
  align-items: center;
  justify-content: center;
  padding: 64px clamp(24px, 4vw, 48px);
  width: 100%;
  box-sizing: border-box;
}
.auth-card {
  width: 100%;
  max-width: 440px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 36px clamp(24px, 4vw, 40px);
  box-sizing: border-box;
}
.auth-card--wide { max-width: 880px; padding: 48px clamp(24px, 4vw, 56px); }

/*
  Split-hero auth layout — chartreuse hero on the left, white
  .auth-card on the right. Used on /signup and /plugin-auth for
  the "first impression" pages where the moment matters. Stacks
  to single column at ≤ 960 px so mobile is always hero-above,
  card-below.

  Markup shape:
    <main class="auth-split-wrap">
      <div class="auth-split-hero"> headline + sub-lede + trust </div>
      <div class="auth-split-card">
        <section class="auth-card"> form </section>
      </div>
    </main>
*/
.auth-split-wrap {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  min-height: calc(100vh - var(--nav-h));
  min-height: calc(100svh - var(--nav-h));
  /* Push up into the nav region so the chartreuse hero reads
     as continuous with the chartreuse nav (same pattern as the
     landing hero). */
  margin-top: calc(var(--nav-h) * -1);
}
.auth-split-hero {
  background: var(--chartreuse);
  color: var(--ink);
  padding: calc(clamp(48px, 8vw, 96px) + var(--nav-h))
           clamp(32px, 5vw, 72px)
           clamp(48px, 8vw, 96px);
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 20px;
}
.auth-split-hero-eyebrow {
  font-family: var(--font-mono);
  font-weight: 500;
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: rgba(10, 10, 10, 0.72);
  margin: 0;
}
.auth-split-hero-title {
  font-size: clamp(2rem, 4.5vw, 3.25rem);
  font-weight: 600;
  letter-spacing: -0.03em;
  line-height: 1.05;
  margin: 0;
  max-width: 18ch;
  color: var(--ink);
}
.auth-split-hero-sub {
  font-size: clamp(1rem, 1.4vw, 1.12rem);
  line-height: 1.55;
  color: var(--ink);
  opacity: 0.78;
  margin: 0;
  max-width: 42ch;
}
.auth-split-hero-note {
  font-family: var(--font-mono);
  font-weight: 500;
  font-size: 0.68rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: rgba(10, 10, 10, 0.6);
  margin: 12px 0 0;
}

.auth-split-card {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 64px clamp(24px, 4vw, 48px);
  background: var(--bone);
}
/* The nested .auth-card keeps its existing styles (440px card,
   white surface, padding, border). No overrides needed — the
   split layout just provides a different container. */

@media (max-width: 960px) {
  .auth-split-wrap {
    grid-template-columns: 1fr;
    min-height: 0;
  }
  .auth-split-hero {
    padding: calc(clamp(32px, 6vw, 64px) + var(--nav-h))
             clamp(24px, 5vw, 48px)
             clamp(32px, 6vw, 64px);
  }
  .auth-split-card {
    padding: 48px clamp(24px, 4vw, 48px);
  }
}

/*
  Shared avatar preview — used by /signup and /account above the
  name field to show the live typographic face as the user types.
  Lives in styles.css (not scoped to a single page) so any form
  that wants a preview just drops the markup in. The disc stays
  chartreuse on every non-chartreuse surface (auth cards are white);
  the nav avatar is a separate component with its own translucent
  tint that blends into the chartreuse header.
*/
.mivia-avatar-preview {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  margin: 0 0 20px;
}
.mivia-avatar-preview-circle {
  width: 96px;
  height: 96px;
  border-radius: 999px;
  overflow: hidden;
  background: var(--chartreuse);
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.mivia-avatar-preview-circle svg {
  width: 100%;
  height: 100%;
  display: block;
}
.mivia-avatar-preview-note {
  margin: 0;
  font-family: var(--font-mono);
  font-weight: 500;
  font-size: 0.68rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--fg-dim);
  text-align: center;
}

/* Account-page inline <dl> grid shouldn't overflow on narrow. */
@media (max-width: 480px) {
  .auth-card--wide dl {
    grid-template-columns: 1fr !important;
    gap: 4px 0 !important;
  }
}
.auth-card h1 { margin: 0 0 6px; font-size: 1.5rem; }
.auth-card .subtitle {
  margin: 0 0 24px;
  color: var(--fg-dim);
  font-size: 0.9rem;
}
.auth-card form {
  display: flex;
  flex-direction: column;
  gap: 14px;
}
.auth-card label {
  display: flex;
  flex-direction: column;
  gap: 6px;
  font-family: var(--font-mono);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--fg-dim);
  font-size: 0.7rem;
}
.auth-card input[type="email"],
.auth-card input[type="password"],
.auth-card input[type="text"] {
  appearance: none;
  background: var(--bone);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 12px 14px;
  /* 16px+ prevents iOS auto-zoom on focus. */
  font-size: 1rem;
  color: var(--ink);
  font-family: inherit;
  font-weight: 500;
  text-transform: none;
  letter-spacing: normal;
  width: 100%;
}
.auth-card input:focus {
  outline: none;
  border-color: var(--ink);
}

/* ============== Floating-label field (Apple-style) ==============
   Input has a transparent placeholder=" " so :placeholder-shown
   only matches while the field is empty AND unfocused. When the
   user clicks in or types something, the sibling <label> slides
   from the centre of the field up to the top and shrinks. The
   actual placeholder character stays invisible (no glyph) — the
   label IS the visible affordance. Input ::placeholder is hidden
   so the space character doesn't render as a stray dot. */
.auth-card .floating-field {
  position: relative;
  display: block;
}
.auth-card .floating-field input[type="email"],
.auth-card .floating-field input[type="password"],
.auth-card .floating-field input[type="text"] {
  border-radius: 999px;          /* fully rounded pill, per request */
  /* Extra top padding reserves room for the floated label so the
     value text doesn't sit on top of it once the label rises. */
  padding: 26px 22px 10px;
  background: transparent;       /* ink outline only, no fill */
  border: 1px solid var(--ink);
}
.auth-card .floating-field input:focus {
  /* Just darken the border on focus — no halo. The label
     animation + the field state itself is enough affordance. */
  outline: none;
  border-color: var(--ink);
}
.auth-card .floating-field input::placeholder {
  color: transparent;
}
.auth-card .floating-field label {
  /* Override the auth-card's mono/uppercase label default so the
     floating label reads like body text at rest, then shrinks to
     a small caption on focus. */
  position: absolute;
  left: 22px;
  top: 50%;
  transform: translateY(-50%);
  font-family: inherit;
  font-weight: 500;
  font-size: 1rem;
  text-transform: none;
  letter-spacing: normal;
  color: var(--fg-dim);
  background: transparent;
  pointer-events: none;
  transition:
    top 220ms cubic-bezier(0.32, 0.72, 0, 1),
    transform 220ms cubic-bezier(0.32, 0.72, 0, 1),
    font-size 220ms cubic-bezier(0.32, 0.72, 0, 1),
    color 160ms ease;
  /* Reset the flex-column auth-card label rules above. */
  display: inline-block;
  gap: 0;
}
/* Floated state: focus, OR has-value (placeholder is hidden),
   OR has been autofilled by the browser. Keep the same family /
   case / spacing as the resting label — only size + position
   change — so the transition reads as a single shape morphing,
   not a font-swap. */
.auth-card .floating-field input:focus + label,
.auth-card .floating-field input:not(:placeholder-shown) + label,
.auth-card .floating-field input:autofill + label,
.auth-card .floating-field input:-webkit-autofill + label {
  top: 12px;
  transform: translateY(0);
  font-size: 0.7rem;
  color: var(--ink);
}
/* Suppress Chrome / Safari's pale-yellow autofill bg + replace
   with a bone-matching inset shadow so the input still reads
   as transparent on the bone login bg. The text-fill-color
   override pulls the typed value back to ink (Chrome forces
   dark blue otherwise). */
.auth-card .floating-field input:-webkit-autofill,
.auth-card .floating-field input:-webkit-autofill:hover,
.auth-card .floating-field input:-webkit-autofill:focus,
.auth-card .floating-field input:-webkit-autofill:active {
  -webkit-box-shadow: 0 0 0 1000px var(--bone) inset;
  -webkit-text-fill-color: var(--ink);
  caret-color: var(--ink);
  /* Re-state the ink border because the box-shadow inset
     replaces the visual surface but doesn't touch the border. */
  border: 1px solid var(--ink);
}
@media (prefers-reduced-motion: reduce) {
  .auth-card .floating-field label { transition: none; }
}

/* ---- Inline circular submit (Apple-style) ----
   A 40px ink disc that lives inside the password field. The
   form's :has() rule below fades it in only when BOTH the email
   and password inputs have content (placeholder-shown == false
   on both). Pressing it submits the form just like a normal
   submit button — JS handler doesn't need to change. */
.floating-field--with-submit input[type="password"] {
  /* Reserve room on the right so the typed value never sits
     under the submit disc. */
  padding-right: 60px;
}
/* When both a reveal toggle AND a submit chevron sit inside
   the same field, push the value-text further left so it
   never crosses either icon. */
.floating-field--with-toggle input[type="password"],
.floating-field--with-toggle input[type="text"] {
  padding-right: 102px;
}

/* ---- Password reveal toggle (eye icon) ----
   36px tappable target inside the field, sitting to the
   left of the submit chevron. Default state shows the eye
   without a slash (password is hidden, click to reveal); on
   .is-shown the slash crosses the eye (password is visible,
   click to hide). top:50% + translateY(-50%) is geometrically
   centred against the input's box height; the asymmetric
   floating-label padding (26/10) doesn't shift the box
   centre, only the typed-text baseline. */
.password-toggle {
  position: absolute;
  right: 54px;            /* clears the 40px chevron at right:10 + a 4px gap */
  top: 50%;
  transform: translateY(-50%);
  width: 36px;
  height: 36px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: none;
  border-radius: 999px;
  cursor: pointer;
  color: var(--ink);
  /* Hidden by default — only appears once the password
     field has content (typed or autofilled). Without input
     to reveal/hide, the icon has no purpose, and showing it
     alongside the chevron looks unbalanced. */
  opacity: 0;
  pointer-events: none;
  transition: opacity 200ms cubic-bezier(0.32, 0.72, 0, 1);
  padding: 0;
  line-height: 0;
}
.floating-field--with-toggle:has(input:not(:placeholder-shown)) .password-toggle,
.floating-field--with-toggle:has(input:-webkit-autofill) .password-toggle,
.floating-field--with-toggle:has(input:autofill) .password-toggle {
  opacity: 0.55;
  pointer-events: auto;
}
.password-toggle:hover { opacity: 1; }
.password-toggle:focus-visible {
  outline: none;
  opacity: 1;
  box-shadow: 0 0 0 2px var(--ink);
}
.password-toggle .pwt-icon {
  width: 22px;
  height: 22px;
  display: block;
}
/* Default state — password is hidden, show the eye. The
   eye-slash sits in the same flow but display: none so it
   doesn't take layout space. Toggle swaps which one renders. */
.password-toggle .pwt-icon-slash { display: none; }
.password-toggle.is-shown .pwt-icon-eye { display: none; }
.password-toggle.is-shown .pwt-icon-slash { display: block; }
@media (prefers-reduced-motion: reduce) {
  .password-toggle { transition: none; }
}
.floating-submit {
  appearance: none;
  position: absolute;
  right: 10px;
  top: 50%;
  transform: translateY(-50%) scale(0.85);
  width: 40px;
  height: 40px;
  border-radius: 999px;
  border: none;
  background: var(--ink);
  color: var(--white);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  opacity: 0;
  pointer-events: none;
  transition:
    opacity 200ms cubic-bezier(0.32, 0.72, 0, 1),
    transform 220ms cubic-bezier(0.32, 0.72, 0, 1),
    background-color 160ms ease,
    color 160ms ease;
}
.floating-submit svg {
  width: 26px;
  height: 26px;
  display: block;
}
/* Submit chevron visibility:
     - shows when the password field's wrapper has focus
       anywhere within it (input or chevron — :focus-within
       keeps it visible mid-click)
     - stays visible once the password has content, even
       after focus moves elsewhere (so tabbing to the eye
       toggle or the email field doesn't make the submit
       affordance vanish)
   Disappears only when the password is BOTH empty AND
   unfocused — i.e. there's nothing to submit and the user
   isn't engaged with that field. The input selector matches
   both type="password" and type="text" (after the reveal
   toggle flips it). */
.auth-card form:has(.floating-field--with-submit:focus-within) .floating-submit,
.auth-card form:has(.floating-field--with-submit input:not(:placeholder-shown)) .floating-submit,
.auth-card form:has(.floating-field--with-submit input:-webkit-autofill) .floating-submit,
.auth-card form:has(.floating-field--with-submit input:autofill) .floating-submit {
  opacity: 1;
  pointer-events: auto;
  transform: translateY(-50%) scale(1);
}

/* No on-blur red. Validation feedback only fires on submit
   click — the JS submit handler sets aria-invalid on each
   field that fails checkValidity(), and the existing
   [aria-invalid="true"] rule paints those red. The per-field
   input listener clears aria-invalid the moment the field
   becomes valid, so red drops live as the user fixes each. */

/* ---- Subtle shake on auth fail ----
   Three short 2px oscillations over ~280ms. translate3d +
   will-change hint the compositor to run the animation on
   the GPU at the device's native refresh rate (120Hz on
   ProMotion / high-refresh displays) — without these the
   browser sometimes runs CSS transforms on the main thread
   at 60Hz and the wiggle reads choppy. */
@keyframes auth-shake {
  0%, 100% { transform: translate3d(0, 0, 0); }
  25%      { transform: translate3d(-2px, 0, 0); }
  50%      { transform: translate3d(2px, 0, 0); }
  75%      { transform: translate3d(-2px, 0, 0); }
}
.auth-card form.is-shaking {
  animation: auth-shake 280ms ease-in-out;
  will-change: transform;
}
@media (prefers-reduced-motion: reduce) {
  .auth-card form.is-shaking { animation: none; }
}

/* ---- Loading spinner on the submit chevron ----
   Replaces the chevron glyph with a small rotating ring
   while the submit fetch is in flight. Same ink disc; only
   the inner mark changes. JS adds .is-loading on submit
   start, removes on response. */
.floating-submit.is-loading {
  pointer-events: none;
}
.floating-submit.is-loading svg { opacity: 0; }
.floating-submit.is-loading::after {
  content: "";
  position: absolute;
  width: 18px;
  height: 18px;
  border: 1.5px solid currentColor;
  border-top-color: transparent;
  border-radius: 999px;
  animation: floating-submit-spin 0.7s linear infinite;
}
@keyframes floating-submit-spin {
  to { transform: rotate(360deg); }
}
@media (prefers-reduced-motion: reduce) {
  .floating-submit.is-loading::after { animation: none; }
}

/* ---- Live password requirements (Apple-style) ----
   Always reserves its layout space so toggling visibility
   doesn't shove the auth-footer up and down — only opacity
   transitions. Each <li> ticks itself green via .is-met
   (set by JS as regex rules pass); the check inside the
   icon fades in on .is-met. */
.pw-rules {
  list-style: none;
  margin: 8px 0 0;
  padding: 0 22px;
  display: flex;
  flex-direction: column;
  gap: 4px;
  font-size: 0.78rem;
  color: var(--fg-dim);
  opacity: 0;
  pointer-events: none;
  transition: opacity 200ms cubic-bezier(0.32, 0.72, 0, 1);
}
/* Reveal when the password input is focused or has content.
   The list always occupies its natural height; only opacity
   changes, so neither the message nor the footer below shifts
   when the rules appear or disappear. */
.auth-card form:has(#signup-password:focus) .pw-rules,
.auth-card form:has(#signup-password:not(:placeholder-shown)) .pw-rules,
.auth-card form:has(#signup-password:autofill) .pw-rules,
.auth-card form:has(#signup-password:-webkit-autofill) .pw-rules {
  opacity: 1;
  pointer-events: auto;
}
.pw-rules li {
  display: flex;
  align-items: center;
  gap: 8px;
  transition: color 200ms ease;
}
.pw-rule-icon {
  width: 14px;
  height: 14px;
  flex-shrink: 0;
  display: block;
}
.pw-rule-icon .pw-check {
  opacity: 0;
  transition: opacity 200ms cubic-bezier(0.32, 0.72, 0, 1);
}
.pw-rules li.is-met {
  color: var(--ink);
}
.pw-rules li.is-met .pw-rule-icon circle {
  stroke: var(--ink);
}
.pw-rules li.is-met .pw-rule-icon .pw-check {
  opacity: 1;
}
@media (prefers-reduced-motion: reduce) {
  .pw-rules,
  .pw-rules li,
  .pw-rule-icon .pw-check { transition: none; }
}
/* No hover state — the chevron's appearance is enough of an
   affordance on its own. Keyboard users still get a focus
   ring so it's not invisible to them. */
.floating-submit:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px var(--ink);
}
.floating-submit:active { transform: translateY(-50%) scale(0.94); }
@media (prefers-reduced-motion: reduce) {
  .floating-submit,
  .floating-submit:active { transition: none; transform: translateY(-50%); }
}
.auth-card .primary-button { margin-top: 8px; }
.auth-card .msg {
  margin: 0;
  padding: 10px 12px;
  border-radius: var(--radius-sm);
  font-size: 0.85rem;
  background: var(--bone);
  border: 1px solid var(--border);
  color: var(--ink);
}
.auth-card .msg[data-kind="error"]   { border-color: var(--danger); color: var(--danger); }
.auth-card .msg[data-kind="success"] { border-color: var(--ink);    color: var(--ink); }

/* Invalid form fields — set on submit failure (e.g. wrong
   credentials at login). Red border only, no halo. The red
   stays even when the user clicks back into the field; the
   border drops back to ink the moment the input event fires
   (JS clears aria-invalid). */
.auth-card input[aria-invalid="true"],
.auth-card input[aria-invalid="true"]:focus {
  outline: none;
  border-color: var(--danger);
}

.auth-card .auth-divider {
  display: flex;
  align-items: center;
  gap: 12px;
  color: var(--fg-dim);
  font-family: var(--font-mono);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-size: 0.7rem;
  margin: 8px 0;
}
.auth-card .auth-divider::before,
.auth-card .auth-divider::after {
  content: "";
  height: 1px;
  flex: 1;
  background: var(--border);
}
.auth-card .social-button {
  appearance: none;
  background: var(--bone);
  border: 1px solid var(--border);
  color: var(--ink);
  padding: 11px 16px;
  border-radius: 999px;
  font-size: 0.9rem;
  font-weight: 500;
  font-family: inherit;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  transition: background 120ms ease, border-color 120ms ease;
}
.auth-card .social-button:hover:not(:disabled) {
  background: var(--bone);
  border-color: var(--ink);
}
.auth-card .social-button:disabled { opacity: 0.45; cursor: not-allowed; }

.auth-card .auth-footer {
  margin-top: 24px;
  padding-top: 16px;
  font-size: 0.85rem;
  color: var(--fg-dim);
  text-align: center;
  font-weight: 500;
}
.auth-card .auth-footer a { color: var(--ink); text-decoration: underline; }
.auth-card .auth-footer a:hover { color: var(--ink); }

/* Bare auth card — no white surface, no border, no padding,
   no shadow. Used on the chartreuse login page where the form
   sits directly on the brand bg without a containing chrome.
   The form's natural max-width (matching the input pills) is
   the only thing constraining horizontal extent. */
.auth-card--bare {
  background: transparent;
  border: none;
  padding: 0;
}
/* Match the submit button's height to the input pills. The
   floating-label inputs have asymmetric vertical padding
   (26px top, 10px bottom) which works out to ≈60px rendered
   height after the 1px border + line-height. The button
   normally sits at ≈44px; lift it via min-height so the
   form's three pills (email + password + submit) read as a
   uniform stack. */
.auth-card--bare .primary-button {
  min-height: 60px;
}
.auth-card--bare h1 {
  font-size: clamp(2rem, 4vw, 2.5rem);
  font-weight: 600;
  letter-spacing: -0.02em;
  margin: 0 0 28px;
  text-align: center;
  color: var(--ink);
}
/* Auth footer with two states. At rest only "Create an
   account" is visible, sitting dead centre. After a failed
   sign-in (JS adds .is-needed) Forgot password? fades in on
   the left and Create slides smoothly from centre to the
   right edge — flexbox can't transition justify-content, so
   we position both children absolutely and animate the
   transform/left properties instead. The footer reserves a
   fixed height so the parent layout doesn't reflow when the
   children change between centred and edge-anchored. */
.auth-card--bare .auth-footer {
  position: relative;
  height: 1.4rem;
  margin-top: 20px;
  padding: 0 22px;          /* matches the .floating-field input
                               horizontal padding so the link
                               edges line up with the visible
                               start/end of the field text */
}
.auth-card--bare .auth-footer #forgot-link {
  position: absolute;
  top: 50%;
  left: 22px;
  transform: translateY(-50%);
  opacity: 0;
  pointer-events: none;
  white-space: nowrap;
  transition: opacity 280ms cubic-bezier(0.32, 0.72, 0, 1);
}
.auth-card--bare .auth-footer.is-needed #forgot-link {
  opacity: 1;
  pointer-events: auto;
}
/* Create an account starts centred (left:50% + translateX(-50%))
   and on .is-needed slides to right:22px (left:calc(100%-22px)
   + translateX(-100%)). The matched transitions on left and
   transform keep the slide stable. */
.auth-card--bare .auth-footer a[href="/signup"] {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  /* Without white-space:nowrap the element shrink-to-fits
     to whatever width is "available" between its left edge
     and the parent's right edge. As we animate left toward
     the right edge, that available space shrinks to ~22px
     and the text wraps to 3 lines mid-transition. Force a
     single line so width stays at content-intrinsic. */
  white-space: nowrap;
  transition:
    left 380ms cubic-bezier(0.32, 0.72, 0, 1),
    transform 380ms cubic-bezier(0.32, 0.72, 0, 1);
}
.auth-card--bare .auth-footer.is-needed a[href="/signup"] {
  left: calc(100% - 22px);
  transform: translate(-100%, -50%);
}
@media (prefers-reduced-motion: reduce) {
  .auth-card--bare .auth-footer #forgot-link,
  .auth-card--bare .auth-footer a[href="/signup"] {
    transition: none;
  }
}
/* Signup's auth-footer is a single <span> ("Already have an
   account? Log in") — centre it absolutely inside the same
   reserved-height container so the page layout matches the
   login page exactly. */
.auth-card--bare .auth-footer > span {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  white-space: nowrap;
}

/* Footer links — calm at rest, underline on hover. No weight
   shift (the underline is the affordance), so no need for the
   ::before phantom or wght transitions. */
.auth-card--bare .auth-footer a {
  text-decoration: none;
  font-weight: 500;
  color: var(--ink);
  transition: color 120ms ease;
}
.auth-card--bare .auth-footer a:hover,
.auth-card--bare .auth-footer a:focus-visible {
  text-decoration: underline;
  text-underline-offset: 3px;
  outline: none;
}

/* Fullbleed auth wrap — reserve the entire space below the nav
   so the footer ships below the fold even on tall viewports.
   Matches the pattern used elsewhere on the site for hero
   sections that must own the first viewport. */
.auth-wrap--fullbleed {
  min-height: calc(100vh - var(--nav-h));
  min-height: calc(100svh - var(--nav-h));
}

/* ----------------- Status pills & status dots ----------------- */
/*
  Plugin-style status dots: small filled circle + uppercase mono
  label. Used on the quota chip, status lines, and anywhere we
  want to echo the plugin's chartreuse-dot + mono-caption pattern.
*/
.status-dot {
  display: inline-block;
  width: 8px; height: 8px;
  border-radius: 999px;
  background: var(--ink);
  vertical-align: middle;
  margin-right: 8px;
}
.status-dot--ok      { background: var(--ink); }
.status-dot--warn    { background: var(--chartreuse); }
.status-dot--err     { background: var(--danger); }
.status-dot--off     { background: transparent; border: 1px solid currentColor; }

/* Accessibility-visible-only class for screen-reader labels. */
.visually-hidden {
  position: absolute !important;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  border: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}

/* Keyboard focus ring — chartreuse glow at 3 px. Only shows for
   keyboard users (focus-visible) so mouse clicks don't light it
   up. Apple-level polish. */
:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 2px;
}

/* ================================================================
   Responsive refinements
   ================================================================
   Mobile-first assumptions baked into the base rules above:
   - font-size: 1rem (16px) on all inputs to kill iOS zoom
   - container widths use max-width (shrink below it naturally)
   - padding uses clamp() to scale down on narrow viewports

   Breakpoints (from desktop-down):
     1024 px — tablet landscape / small laptop
      720 px — tablet portrait; nav collapses to menu
      640 px — large phone; multi-col grids go 1-col
      480 px — compact phone; tighter spacing, smaller hero type
   ================================================================ */

@media (max-width: 720px) {
  main {
    padding: 48px clamp(20px, 5vw, 32px) 72px;
    gap: 28px;
  }
  header h1         { font-size: clamp(1.75rem, 7vw, 2.25rem); }
  header .tagline   { font-size: 0.95rem; }

  #mivia-header { padding: 10px 20px; gap: 10px; }
  .mivia-header-brand img { height: 18px; }

  .dropzone         { padding: 40px 20px; }
  .dropzone--hero   { padding: 48px 20px; }
  .dropzone--compact{ padding: 28px 20px; }
  .dropzone-headline { font-size: 1rem; }

  .primary-button,
  .accent-button,
  .secondary-button,
  .reset-button {
    /* iOS / Android tap-target min 44 px; 46 px clears pointer
       sloppiness on borders. */
    min-height: 46px;
    width: 100%;
  }

  .auth-card { padding: 28px 22px; }
  .auth-card--wide { padding: 32px 22px; }
}

@media (max-width: 480px) {
  main { padding: 40px 18px 64px; }
  .auth-wrap { padding: 40px 16px; }
  .auth-card { padding: 24px 20px; border-radius: 12px; }
  h1 { font-size: clamp(1.5rem, 8vw, 2rem); }

  #mivia-header { padding: 10px 16px; }
  .mivia-header-link { padding: 6px 8px; font-size: 0.86rem; }

  .dropzone { padding: 32px 16px; }
  .dropzone--hero { padding: 40px 16px; border-radius: 16px; }
}
