/* =========================================================================
 * SoloPub shared stylesheet
 *
 * Source of truth for product CSS. Loaded by:
 *   - SSR templates via <link rel="stylesheet" href="/styles.css"> in base.html
 *   - SPA shell via <link rel="stylesheet" href="/styles.css"> in web/app/index.html
 *
 * See docs/design.md for the contract: tokens, spacing, type scale,
 * component patterns, system states, navigation rules, and a11y spec.
 * Changes to any component's DOM shape or class name land here first,
 * then in the templates + SPA components that consume them.
 * ========================================================================= */

/* --- Design tokens -------------------------------------------------------

   Refined for v0.1 polish pass. Three layers:

   1. Palette + surfaces — dark-first, light-mode redefined inside
      the @media block below. Every token in the original 13-token
      contract (pinned by tests/js/styles.test.mjs) is preserved.

   2. Surface layering / typography scale / radii / motion — added
      for the polish pass. Radii, motion, and typography are mode-
      invariant; surface layering varies by mode for elevation depth.

   3. Elevation (shadows) — dark uses low-opacity neutrals paired
      with a 1px inset highlight (--shadow-inset). Light uses crisper
      blue-grey shadows on white. Commit 2 propagates these through
      cards / buttons / inputs / modals.

   ----------------------------------------------------------------- */

:root {
  /* Palette — dark default, cyberpunk neon-mint pass. Surfaces go to
     near-OLED black so --accent halos instead of competes; borders
     lift slightly cooler so panel edges glint against the deep bg.
     Accent is near-max-saturation mint with a slight cyan lean —
     explicitly not Matrix-green (#00FF00 reads dated) and not
     lime/spring (reads childish). --danger pivots to hot magenta-pink
     so destructive surfaces complete the cyberpunk neon duo through
     existing tokens (no new tokens; no component edits). */
  --bg: #02040a;
  --surface: #0b1020;
  --surface-2: #151c33;
  --surface-raised: #1e2640;
  --surface-overlay: #101630;
  --surface-input: #0b1020;
  --border: #2a3454;

  --text: #e8eaf0;
  --text-muted: #a0a8b8;
  --text-subtle: #6a7180;

  --accent: #00ff55;
  --accent-hover: #17FFC4;
  --focus: var(--accent);  /* dark mode: focus ring uses accent — visible against --bg via outline-offset gap */

  /* Foreground when sitting ON the --accent fill (button text / step
     marker glyph). Dark mode keeps the existing behavior (near-black
     glyph on neon accent); light mode overrides to true black so the
     bright accent can carry dark text cleanly. */
  --on-accent: var(--bg);

  /* Accent-as-text variant. In dark mode, identical to --accent — the
     neon green reads as text against deep surfaces (hover lifts to
     --accent-hover, the cyan-mint). In light mode, overridden to
     --text + --text-subtle so links read as primary text and
     de-emphasise on hover without forcing a second green hue. */
  --accent-text: var(--accent);
  --accent-text-hover: var(--accent-hover);

  /* Glow halos for transient interaction states (button hover/press,
     action-btn, nav-active-hover). Off in both themes — the accent
     fill itself carries enough pop against the surrounding surface,
     and adding outer halos was reading as too busy. Kept as tokens
     so the system can re-enable them later (per theme) without
     re-editing every rule. */
  --glow-accent-soft: none;
  --glow-accent-strong: none;
  --glow-action-soft: none;
  --glow-action-strong: none;
  --glow-danger-soft: none;
  --glow-danger-strong: none;

  /* Tinted background for active / pressed states (active-nav items,
     pressed-toggle action buttons, ghost-button pressed, tab actives).
     Dark mode at 12% / 16% gives a quiet wash because the accent itself
     pops against the deep surface; light mode bumps to 25% / 35% so
     the tint reads clearly against white. The same token covers both
     "10% pressed-button" and "12% active-nav" old call sites — the
     2-point difference at low percentages wasn't perceptible. */
  --active-tint: color-mix(in srgb, var(--accent) 12%, transparent);
  --active-tint-hover: color-mix(in srgb, var(--accent) 16%, transparent);

  /* Text / glyph color when sitting inside an activated surface
     (action-btn[aria-pressed=true], etc.). Dark mode aliases to the
     accent-text token (= neon green), so the favorited star + count
     glow against the dark active-tint bg. Light mode overrides to a
     slate-green mix so the glyph feels branded without going pure
     accent (which would fail contrast on the pale-mint active-tint
     bg). See @media light below. */
  --active-text: var(--accent-text);

  --warning-bg: #06060a;  /* essentially surface — the bright fg + lemon-glow border carry the warning meaning */
  --warning-fg: #ffea00;  /* acid lemon — cyberpunk caution, ~17:1 on bg */
  --danger: #ff3e7f;

  /* Radii — replaces the scattered 3/4/6/8 px values used throughout
     pre-polish rules. Commit 2 propagates these to every consumer. */
  --radius-sm: 6px;
  --radius-md: 12px;
  --radius-lg: 16px;
  --radius-full: 9999px;

  /* Motion — replaces hardcoded 150ms / 200ms timings. */
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
  --duration-fast: 120ms;
  --duration: 180ms;
  --duration-slow: 280ms;

  /* Typography scale — replaces ad-hoc rem values per rule. */
  --text-2xs: 0.6875rem;  /* 11px — chips, timestamps */
  --text-xs:  0.8125rem;  /* 13px — handles, captions */
  --text-sm:  0.9375rem;  /* 15px — secondary */
  --text-base: 1rem;      /* 16px — body */
  --text-md:  1.0625rem;  /* 17px — emphasized body */
  --text-lg:  1.25rem;    /* 20px — small headings */
  --text-xl:  1.5rem;     /* 24px — page headings */
  --text-2xl: 1.875rem;   /* 30px — display */

  /* Elevation — dark variant. Subtle, paired with --shadow-inset for
     a hairline top highlight that fakes a light source on the card
     edge. Use --shadow-sm for cards, --shadow-md for raised popovers,
     --shadow-lg for modals. */
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.45),
               0 1px 1px rgba(0, 0, 0, 0.30);
  --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.50),
               0 2px 4px rgba(0, 0, 0, 0.35);
  --shadow-lg: 0 16px 40px rgba(0, 0, 0, 0.55),
               0 6px 12px rgba(0, 0, 0, 0.35);
  --shadow-inset: inset 0 1px 0 rgba(255, 255, 255, 0.04);

  /* Modal backdrop scrim. Dim, no blur — keeps text behind crisp and
     skips the GPU cost of a backdrop-filter. */
  --backdrop: rgba(0, 0, 0, 0.5);
}

/* Light-mode token override — activated by the OS / browser
   preference. Every token in the dark block that varies by mode is
   re-declared so the dark-first base stays authoritative and the
   light theme is a pure remap. Mode-invariant tokens (radii, motion,
   typography scale) inherit from :root.
   WCAG AA contrast targets: text/bg ≥ 7:1, text-muted/bg ≥ 4.5:1.
   See docs/design.md §4. No explicit user toggle ships in this
   release — respecting the system preference is enough for v1. */
@media (prefers-color-scheme: light) {
  :root {
    /* Light-mode partner palette — deep cyan accent paired with
       rose-magenta danger. Cyan reads as a coherent shift from the
       dark-mode neon mint (both are green-cyan-family) without
       trying to be cyberpunk-bright on white, which AA can't
       support. Values verified against WCAG AA on white (#ffffff)
       AND --bg (#f5f7fb) AND the 12%/16% accent-tinted bg used for
       .nav-item--active / .notification-tab--active — every text/bg
       pair clears the 4.5:1 target without needing a tint override. */
    --bg: #f5f7fb;
    --surface: #ffffff;
    --surface-2: #eef2f8;
    --surface-raised: #ffffff;
    --surface-overlay: #ffffff;
    --surface-input: #ffffff;
    --border: #dde3ed;

    --text: #16181d;
    --text-muted: #545b6b;
    --text-subtle: #6b7280;  /* slate-500; ~4.83:1 on --bg, clears AA */

    --accent: #00ff55;       /* unified with dark */
    --accent-hover: #17FFC4; /* unified across themes */
    --focus: var(--text-subtle); /* slate-500; ~4.83:1 on white, softer than --text */

    /* Light-mode active state mirrors dark mode's activated appearance:
       a dark teal-green pill (≈12% accent over dark surface) with
       neon-green text and border. Same visual identity in both themes
       so the "activated" pill reads consistently, and so primary
       CTA (bright lime fill) doesn't compete with activated states
       for attention. ~10.75:1 contrast — strong AA. */
    --active-tint: #132D29;                    /* dark teal — matches dark active-tint */
    --active-tint-hover: #16372B;              /* slightly lifted/greener for hover on active */
    --active-text: var(--accent);              /* #00ff55 — solid neon glyph + count */

    /* Black on lime — explicit, the bright accent fill needs dark text. */
    --on-accent: #000000;

    /* Accent-as-text in light mode aliases to the primary text family
       — bright lime would fail AA on white, and a darker green felt
       like a forced second hue. Using --text + --text-subtle means
       links read as "important text" (matching --text) and de-emphasise
       on hover to --text-subtle. The accent fill is the only place the
       brand color appears in light mode. */
    --accent-text: var(--text);
    --accent-text-hover: var(--text-subtle);

    --warning-bg: #FFEA00;   /* mirrors dark warning-fg — acid lemon fill */
    --warning-fg: #06060A;   /* near-black text on the lemon bg; ~16.8:1 */
    --danger: #C91D78;       /* 5.34:1 on white, 4.98:1 on --bg */

    /* Crisper shadows on white: blue-grey at low alpha gives the
       Linear / Vercel "lift" without becoming muddy. */
    --shadow-sm: 0 1px 2px rgba(15, 23, 42, 0.06),
                 0 1px 1px rgba(15, 23, 42, 0.04);
    --shadow-md: 0 4px 12px rgba(15, 23, 42, 0.08),
                 0 2px 4px rgba(15, 23, 42, 0.04);
    --shadow-lg: 0 16px 40px rgba(15, 23, 42, 0.12),
                 0 6px 12px rgba(15, 23, 42, 0.06);
    --shadow-inset: inset 0 1px 0 rgba(255, 255, 255, 0.6);

    /* Lighter scrim on a light page so the dialog still reads as
       lifted without slamming the page to near-black. */
    --backdrop: rgba(15, 23, 42, 0.4);
  }
}

/* --- Motion --------------------------------------------------------------

   Respect the OS reduce-motion preference by collapsing animation +
   transition durations to a sub-frame constant. The !important ensures
   this overrides per-element rules declared lower in the sheet and
   rules set by browser defaults. */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* --- Typeface — Inter ----------------------------------------------------

   Inter is self-hosted under /fonts/ so the existing CSP
   (`default-src 'self'`) doesn't need a third-party font-src.
   InterVariable.woff2 is a single variable font carrying the full
   100–900 weight axis plus an optical-size axis (14–32). Italics
   load from a parallel file. font-display: swap shows the system
   fallback during the ~50ms load window, then swaps in without
   FOIT (flash of invisible text).
   ----------------------------------------------------------------- */

@font-face {
  font-family: 'InterVariable';
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url('/fonts/InterVariable.woff2') format('woff2-variations'),
       url('/fonts/InterVariable.woff2') format('woff2');
}

@font-face {
  font-family: 'InterVariable';
  font-style: italic;
  font-weight: 100 900;
  font-display: swap;
  src: url('/fonts/InterVariable-Italic.woff2') format('woff2-variations'),
       url('/fonts/InterVariable-Italic.woff2') format('woff2');
}

/* --- Base / reset -------------------------------------------------------- */

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

body {
  font-family: 'InterVariable', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  font-optical-sizing: auto;
  /* cv11 = single-story a (slightly more refined / less typewriter-y).
     ss01 = open digits. tnum = tabular figures (handles, counts,
     timestamps align in columns). Inter's variable font supports
     these features. */
  font-feature-settings: 'cv11', 'ss01';
  max-width: 720px;
  margin: 0 auto;
  padding: 1rem;
  background: var(--bg);
  color: var(--text);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

/* Tabular figures for numeric-heavy small text — counts, timestamps,
   handles. Keeps them column-aligned. */
.handle,
.status-permalink,
.action-btn,
.char-counter,
.stats {
  font-feature-settings: 'cv11', 'ss01', 'tnum';
}

/* Larger display text gets tighter letter-spacing. Inter at large
   sizes benefits from a slight negative tracking — typical
   Linear/Vercel treatment. */
h1, h2, h3 {
  letter-spacing: -0.015em;
}

a {
  color: var(--accent-text);
}

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

h1 {
  font-size: 1.5rem;
  line-height: 1.4;
}

.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;
}

/* --- Focus ring ---------------------------------------------------------- */

a:focus-visible,
button:focus-visible,
.btn:focus-visible,
summary:focus-visible,
input:focus-visible,
textarea:focus-visible,
select:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

/* --- Forms ---------------------------------------------------------------

   Inputs use the --surface-input fill (same as --surface on dark,
   pure white on light) with --radius-md corners and generous
   padding. Focus state pairs an accent border with a 3px glow ring
   (via color-mix on --accent) — Linear/Vercel-style affordance.
   Hover slightly brightens the border to flag interactivity before
   click. All transitions go through the motion tokens so
   prefers-reduced-motion collapses them uniformly. */

input[type="text"],
input[type="password"],
input[type="email"],
input[type="search"],
input[type="number"],
input[type="url"],
textarea,
select {
  width: 100%;
  background: var(--surface-input);
  border: 1px solid var(--border);
  color: var(--text);
  padding: 0.7rem 0.9rem;
  border-radius: var(--radius-md);
  font-size: var(--text-base);
  line-height: 1.5;
  font-family: inherit;
  transition: border-color var(--duration-fast) var(--ease-out),
              box-shadow var(--duration-fast) var(--ease-out),
              background var(--duration-fast) var(--ease-out);
}

input::placeholder,
textarea::placeholder {
  color: var(--text-subtle);
}

input[type="text"]:hover,
input[type="password"]:hover,
input[type="email"]:hover,
input[type="search"]:hover,
input[type="number"]:hover,
input[type="url"]:hover,
textarea:hover,
select:hover {
  border-color: var(--text-subtle);
}

input[type="text"]:focus,
input[type="password"]:focus,
input[type="email"]:focus,
input[type="search"]:focus,
input[type="number"]:focus,
input[type="url"]:focus,
textarea:focus,
select:focus {
  border-color: var(--accent);
  outline: none;
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent);
}

label {
  display: block;
  margin-bottom: 0.4rem;
  color: var(--text-muted);
  font-size: var(--text-xs);
  font-weight: 500;
  letter-spacing: 0.01em;
  text-transform: uppercase;
}

.form-group {
  margin-bottom: 1.25rem;
}

/* --- OAuth authorize: 2FA step layout ----------------------------------- */
/* When 2FA is enrolled, the authorize form renders password and TOTP as
   two visually-distinct "steps" so the second field is impossible to
   miss (Mastodon's reference UI uses two pages; we use one page styled
   to look like two steps). The form stays a single POST — both factors
   submit together. The step framing is purely presentational. */

.auth-step {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 0.75rem 1rem;
  align-items: start;
  margin-bottom: 1.5rem;
  padding-top: 0.25rem;
}

.auth-step + .auth-step {
  border-top: 1px solid var(--border);
  padding-top: 1.25rem;
}

.auth-step-marker {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.75rem;
  height: 1.75rem;
  border-radius: 50%;
  background: var(--accent);
  color: var(--on-accent);
  font-weight: 600;
  font-size: 0.95rem;
  flex: none;
}

.auth-step-body label {
  display: block;
  font-weight: 600;
  margin-bottom: 0.25rem;
}

.auth-step-helper {
  margin: 0 0 0.5rem 0;
  font-size: 0.875rem;
  color: var(--text-muted);
}

.auth-step-body input {
  width: 100%;
}

/* Owner handle as a header row inside the first-party sign-in card,
   divided from the password field below. */
.auth-handle {
  margin: 0 0 1.25rem 0;
  padding-bottom: 1rem;
  border-bottom: 1px solid var(--border);
  font-weight: 600;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}

/* --- Buttons -------------------------------------------------------------

   Three-axis button system: size × color × shape.

     Size (base .btn = default 44px lg)
       .btn          default — primary CTAs, 44px min, generous padding
       .btn-sm       compact — header nav, action row, chip-like, 32px

     Color (orthogonal, compose with size)
       .btn          primary, accent-filled
       .btn-ghost    transparent + 1px border, muted text
       .btn-danger   transparent + danger-tinted

     Shape
       Default .btn is pill (--radius-full). Pills look right for
       standalone full-size CTAs.
       Default .btn-sm is rounded rectangle (--radius-md). Pills
       become awkward at compact sizes inside header rows.
       .btn-block forces --radius-md regardless of size — a pill
       stretched to 100% width becomes a lozenge.

   Composition examples:
     .btn                  → primary pill, large
     .btn .btn-ghost       → ghost pill, large
     .btn .btn-sm          → primary rect, compact (header CTA)
     .btn .btn-ghost .btn-sm  → ghost rect, compact (header nav)
     .btn .btn-block       → primary rect, full-width (sign-in)
*/

button,
.btn {
  background: var(--accent);
  color: var(--on-accent);
  font-weight: 600;
  border: 1px solid transparent;
  padding: 0.65rem 1.5rem;
  border-radius: var(--radius-full);
  font-size: var(--text-base);
  line-height: 1.2;
  cursor: pointer;
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.4rem;
  /* Anchors styled as buttons (.btn on an <a>, e.g. the SSR public-
     page sign-in) must not show the global link underline. */
  text-decoration: none;
  /* No persistent glow on rest state — glow is reserved for transient
     interaction feedback (:hover and :active). The transition for
     box-shadow is kept so the hover/press glows ease in/out cleanly. */
  transition: background-color var(--duration-fast) var(--ease-out),
              color var(--duration-fast) var(--ease-out),
              border-color var(--duration-fast) var(--ease-out),
              box-shadow var(--duration-fast) var(--ease-out),
              transform var(--duration-fast) var(--ease-out);
}

button:hover,
.btn:hover {
  background: var(--accent-hover);
  box-shadow: var(--glow-accent-soft);
}

button:active,
.btn:active {
  transform: translateY(1px);
  /* Brightest pop on the press moment — the "short-term interaction"
     glow. Fades out as soon as the button is released. Token resolves
     to `none` in dark mode and to a real halo in light. */
  box-shadow: var(--glow-accent-strong);
}

/* Compact size modifier — header nav, chips, action row.
   Switches to --radius-md because pill at this height becomes
   half-circle endcaps on each side, fighting compact alignment. */
.btn-sm {
  min-height: 32px;
  padding: 0.4rem 0.9rem;
  font-size: var(--text-sm);
  border-radius: var(--radius-md);
}

/* Block modifier — full width. Pills stretched to 100% look
   like candy lozenges; rectangular corners read as a proper CTA. */
.btn-block {
  width: 100%;
  border-radius: var(--radius-md);
}

/* Secondary — transparent surface, neutral text, bordered.
   Used for non-primary affordances (CW toggle, cancel, etc.).
   Ghosts deliberately do not glow — the accent halo is reserved for
   primary actions; ghosts should read as quiet support. */
.btn-ghost,
button.btn-ghost {
  background: transparent;
  color: var(--text-muted);
  border-color: var(--border);
  font-weight: 500;
  box-shadow: none;
}

.btn-ghost:hover,
button.btn-ghost:hover {
  color: var(--text);
  background: var(--surface-2);
  border-color: var(--text-subtle);
  box-shadow: none;
}

.btn-ghost[aria-pressed="true"] {
  color: var(--active-text);
  border-color: var(--accent);
  background: var(--active-tint);
}

/* Destructive — neutral ground, danger fg + border. The magenta-pink
   glow is reserved for hover/press feedback; the rest state stays
   quiet so destructive surfaces don't read as actively threatening
   when sitting idle. */
.btn-danger,
button.btn-danger {
  background: transparent;
  color: var(--danger);
  border-color: var(--danger);
  font-weight: 600;
}

.btn-danger:hover,
button.btn-danger:hover {
  background: var(--danger);
  color: var(--bg);
  box-shadow: var(--glow-danger-soft);
}

.btn-danger:active,
button.btn-danger:active {
  box-shadow: var(--glow-danger-strong);
}

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

button[disabled]:hover,
.btn[disabled]:hover {
  /* Disabled buttons must not respond to hover. The base rule's
     hover would otherwise re-color them as if interactable. */
  background: var(--accent);
}

.btn-ghost[disabled]:hover,
button.btn-ghost[disabled]:hover {
  background: transparent;
  color: var(--text-muted);
  border-color: var(--border);
}

.btn-danger[disabled]:hover,
button.btn-danger[disabled]:hover {
  background: transparent;
  color: var(--danger);
  border-color: var(--danger);
}

/* --- Code / pre ---------------------------------------------------------- */

pre {
  background: var(--surface);
  padding: 1rem;
  border-radius: 4px;
  overflow-x: auto;
  overflow-wrap: anywhere;
  border: 1px solid var(--border);
}

code {
  background: var(--surface);
  padding: 0.15rem 0.3rem;
  border-radius: 3px;
  font-size: 0.9em;
}

/* --- Card ----------------------------------------------------------------

   Cards lift off the page with --shadow-sm + --radius-lg. The
   1px --border outline remains as a hairline definition since
   the dark-mode shadow alone is too subtle against a near-black
   bg. On light mode the border still helps the card edge land
   cleanly against the off-white background.
   ----------------------------------------------------------------- */

.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  padding: 1.5rem;
  margin-top: 1rem;
}

/* --- Nav (Sign-in slot on public pages) ---------------------------------- */

.site-nav {
  display: flex;
  justify-content: flex-end;
  margin-bottom: 1rem;
}

/* --- Authed SPA site header (brand + nav + sign-out) --------------------- */

.site-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0.75rem 0;
  border-bottom: 1px solid var(--border);
  margin-bottom: 1rem;
  gap: 0.75rem;
  flex-wrap: wrap;
}

.site-header .brand {
  color: var(--text);
  text-decoration: none;
  font-weight: 700;
  font-size: 1.2rem;
  letter-spacing: -0.02em;
}

.site-header .brand:hover {
  color: var(--accent-text);
}

/* Brand identity "option A": refined wordmark with a sage-accent
   period. Kept as a class (not an inline color) so the SPA header
   stays clean of the verify.sh inline-style invariant. When the
   combined wordmark+logo SVG lands later, swap the <a class="brand">
   contents and this rule can retire. */
.brand-accent {
  /* Gradient wordmark accent — mint → magenta sweep. Uses the same
     two cyberpunk tokens (--accent + --danger) the rest of the
     palette pairs, so the brand mark reads as the design's
     headline duo without introducing new tokens. */
  background: linear-gradient(135deg, var(--accent), var(--danger));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

/* --- Branding footer (shared by SSR `_footer.html` + SPA `footer.js`) ---- */

.site-footer {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 0.4rem;
  margin-top: 2rem;
  padding: 1rem 0;
  border-top: 1px solid var(--border);
  color: var(--text-subtle);
  font-size: var(--text-sm);
}

.site-footer a {
  color: var(--text-subtle);
  text-decoration: underline;
}

.site-footer a:hover {
  color: var(--accent-text);
}

.site-header .nav {
  display: flex;
  gap: 0.4rem;
  align-items: center;
}

/* Primary nav item — icon + label, ghost-style, with an accent
   active-page indicator. The active item also carries
   aria-current="page" (set in header.js); this is the matching
   visual. */
.nav-item {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  font-size: var(--text-sm);
  font-weight: 500;
  padding: 0.4rem 0.75rem;
  min-height: 36px;
  border: 1px solid transparent;
  border-radius: var(--radius-md);
  color: var(--text-muted);
  background: transparent;
  text-decoration: none;
  cursor: pointer;
  transition: color var(--duration-fast) var(--ease-out),
              background-color var(--duration-fast) var(--ease-out);
}

.nav-item:hover {
  color: var(--text);
  background: var(--surface-2);
}

.nav-item .icon {
  width: 1.05rem;
  height: 1.05rem;
  flex: none;
}

.nav-item--active {
  color: var(--active-text);
  background: var(--active-tint);
}

.nav-item--active:hover {
  color: var(--active-text);
  background: var(--active-tint-hover);
  box-shadow: var(--glow-accent-soft);
}

/* New Post — accent-filled CTA so the primary action stays prominent.
   When it's the current page the fill already reads as "here", so the
   active modifier is a no-op on it. */
.nav-cta {
  color: var(--on-accent);
  background: var(--accent);
  font-weight: 600;
}

.nav-cta:hover {
  color: var(--on-accent);
  background: var(--accent-hover);
}

.nav-cta.nav-item--active,
.nav-cta.nav-item--active:hover {
  color: var(--on-accent);
  background: var(--accent);
}

/* --- Account menu (avatar dropdown) -------------------------------------- */

.nav-menu {
  position: relative;
  display: inline-flex;
}

.nav-menu-trigger {
  display: inline-flex;
  align-items: center;
  gap: 0.2rem;
  padding: 0.2rem 0.35rem 0.2rem 0.25rem;
  min-height: 36px;
  border: 1px solid var(--border);
  border-radius: var(--radius-full);
  background: transparent;
  color: var(--text-muted);
  cursor: pointer;
  transition: background-color var(--duration-fast) var(--ease-out),
              border-color var(--duration-fast) var(--ease-out);
}

.nav-menu-trigger:hover {
  background: var(--surface-2);
  border-color: var(--text-subtle);
}

.nav-menu-trigger .icon {
  width: 0.9rem;
  height: 0.9rem;
  flex: none;
  transition: transform var(--duration) var(--ease-out);
}

.nav-menu-trigger[aria-expanded="true"] .icon {
  transform: rotate(180deg);
}

.nav-menu-panel {
  position: absolute;
  top: calc(100% + 0.4rem);
  right: 0;
  z-index: 90;
  min-width: 172px;
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  padding: 0.35rem;
  background: var(--surface-overlay);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-lg);
  animation: nav-menu-in var(--duration) var(--ease-out);
}

@keyframes nav-menu-in {
  from { opacity: 0; transform: translateY(-4px); }
  to { opacity: 1; transform: none; }
}

.nav-menu-item {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: 0.6rem;
  width: 100%;
  min-height: auto;
  padding: 0.55rem 0.6rem;
  border: none;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--text);
  font-size: var(--text-sm);
  font-weight: 500;
  text-align: left;
  text-decoration: none;
  cursor: pointer;
}

.nav-menu-item:hover,
.nav-menu-item:focus-visible {
  background: var(--surface-2);
  color: var(--text);
}

.nav-menu-item .icon {
  width: 1rem;
  height: 1rem;
  flex: none;
  color: var(--text-muted);
}

/* Mobile: collapse the primary nav to icons. The labels stay in the
   DOM (visually hidden) so the icon-only links keep an accessible
   name. */
@media (max-width: 599px) {
  .nav-label {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
  }
  .nav-item {
    padding: 0.4rem 0.5rem;
    gap: 0;
  }
}

/* --- Avatar -------------------------------------------------------------- */

.avatar,
.avatar-placeholder {
  width: 96px;
  height: 96px;
  border-radius: 50%;
  flex-shrink: 0;
  /* Inset-edge treatment: a hairline ring + top highlight so the
     avatar reads as a distinct object against a same-tone surface
     (the placeholder bg and --surface are close in dark mode).
     Applies to the 96px hero and the 48px status-card avatars
     alike — one consistent edge. */
  border: 1px solid var(--border);
  box-shadow: var(--shadow-inset);
}

.avatar {
  object-fit: cover;
  background: var(--surface-2);
}

.avatar-placeholder {
  position: relative;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  /* Glyph defaults — sit above the .avatar-pixels SVG layer (which is
     position: absolute, inset: 0). `color: var(--on-accent)` keeps the
     initial near-black in both themes against the vibrant neon grid;
     contrast clears ≥7:1 on every palette entry. */
  font-size: 2.25rem;
  font-weight: 600;
  text-transform: uppercase;
  user-select: none;
  color: var(--on-accent);
}

/* The pixel-grid background layer for `.avatar-placeholder`. 25 rects
   on a 0..5 viewBox quantize a 2-color neon gradient (cells computed
   by `solopub_core::avatar::pixel_cells`, mirrored byte-for-byte by
   `web/app/lib/avatar.js::pixelCells`). `crispEdges` prevents the
   rasteriser from softening cell borders into a continuous gradient.
   The hairline rect strokes draw light grid lines between cells. */
.avatar-pixels {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  shape-rendering: crispEdges;
}

/* Hairline grid lines between cells — stroked with `var(--bg)` so
   they read as the page surface cutting through the avatar: deep
   near-black in dark mode (#02040a), cool near-white in light mode
   (#f5f7fb). `vector-effect: non-scaling-stroke` pins the stroke at a
   constant 0.5 CSS pixel regardless of avatar size (96 / 48 / 28 px),
   so the line stays a true hairline instead of scaling with the
   viewBox. */
.avatar-pixels rect {
  stroke: var(--bg);
  stroke-width: 0.1;
  vector-effect: non-scaling-stroke;
}

/* Drop the gridlines at card + nav sizes — cells are too small for
   the line to register as anything other than visual noise. */
.avatar-placeholder.avatar-sm .avatar-pixels rect,
.avatar-placeholder.avatar-xs .avatar-pixels rect {
  stroke-width: 0;
}

.avatar-initial {
  position: relative;
  z-index: 1;
  line-height: 1;
}

/* Muted fallback variant — no pixel-grid SVG, just a centred glyph
   (icon or letter) on a muted surface. Two uses:
   1. ConversationRow's "empty mailbox" state — MailIcon centred when
      the conversation has no other participants.
   2. `lib/img-fallback.js` + `web/img-fallback.js` — broken uploaded
      avatar: the canonical handle isn't reachable from the <img> so
      we can't reproduce the per-account pixel grid; the muted look
      signals "this isn't the real avatar" instead of competing with
      the proper pixel-grid placeholder. */
.avatar-placeholder--icon {
  background: var(--surface-2);
  color: var(--text-muted);
}

/* --- Profile header ------------------------------------------------------ */

.profile-header {
  display: flex;
  gap: 1.5rem;
  align-items: flex-start;
  margin-bottom: 1.75rem;
  padding-bottom: 1.5rem;
  border-bottom: 1px solid var(--border);
}


/* Pure size modifier — works on both .avatar and .avatar-placeholder.
   Glyph scaling for the placeholder variant is split below so the
   image variant doesn't inherit a font-size it has no use for. */
.avatar-sm {
  width: 48px;
  height: 48px;
}

.avatar-placeholder.avatar-sm {
  font-size: 1.15rem;
}

/* Extra-small — the account-menu trigger avatar in the header. */
.avatar-xs {
  width: 28px;
  height: 28px;
}

.avatar-placeholder.avatar-xs {
  font-size: 0.7rem;
}

.profile-meta {
  min-width: 0;
  flex: 1;
}

.profile-meta .display-name {
  margin: 0;
  font-size: var(--text-2xl);
  line-height: 1.15;
  letter-spacing: -0.02em;
}

.profile-meta .handle {
  display: inline-block;
  color: var(--text-muted);
  font-size: var(--text-sm);
  margin-top: 0.35rem;
}

.bio {
  margin-top: 1rem;
  line-height: 1.6;
  color: var(--text);
}

.bio p {
  margin: 0 0 0.5rem;
}

.bio p:last-child {
  margin-bottom: 0;
}

.stats {
  margin-top: 0.75rem;
  color: var(--text-subtle);
  font-size: var(--text-xs);
}

/* Phase 8.2 — federated custom emojis injected into display_name + bio
   + status content via lib/emoji.js. Sized to the host font's
   ascender so the inline-image baselines align with the surrounding
   text. `width`/`height` are also set on the `<img>` for layout
   stability before the bytes load. */
.custom-emoji {
  display: inline-block;
  vertical-align: text-bottom;
  width: 1.2em;
  height: 1.2em;
  margin: 0 0.05em;
}

/* Phase 8.1 — header banner above the profile header. Mastodon serves
   1500x500 banners; SoloPub aspects the image into a fixed-height band
   so layouts stay predictable regardless of source dimensions. The
   image is decorative (the display_name carries the identity);
   templates set alt="" and aria-hidden so screen readers skip it. */
.profile-banner {
  margin-bottom: 1.5rem;
  border-radius: var(--radius-lg);
  overflow: hidden;
  aspect-ratio: 3 / 1;
  background: var(--surface-2);
}
.profile-banner img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Phase 8.1 — "Joined Nov 2022" row between bio and stats. Same
   subdued tone as the handle so the visual rhythm of the header
   stays consistent. */
.profile-joined-date {
  margin-top: 0.75rem;
  color: var(--text-muted);
  font-size: var(--text-sm);
}

/* Phase 8.1 — inline lock indicator next to the display name on a
   locked account. The badge stays vertical-aligned with the
   display-name's optical baseline via a small inline-flex wrap. */
.profile-locked {
  display: inline-flex;
  margin-left: 0.4em;
  vertical-align: middle;
  color: var(--text-muted);
}
.profile-locked svg {
  width: 0.75em;
  height: 0.75em;
}

/* Phase 8.1 — Mastodon-shape profile fields list ("Website",
   "GitHub", …). Two-column DL: name on the left, value on the right;
   stacks on narrow viewports. Borders separate rows so the list
   reads cleanly even with HTML in the value cell. */
.profile-fields {
  margin: 1rem 0 0;
  padding: 0;
  display: grid;
  gap: 0;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  overflow: hidden;
}
.profile-field {
  display: grid;
  /* Name column sizes to its content (typically one word — "Website",
     "Pronouns", "GitHub"), capped at 40% so a freak long label can't
     starve the value column. The 12rem cap from the polish pass forced
     letter-level wrap on URLs inside narrow deck columns. */
  grid-template-columns: minmax(min-content, 40%) minmax(0, 1fr);
  border-bottom: 1px solid var(--border);
}
.profile-field:last-child {
  border-bottom: none;
}
.profile-field-name {
  margin: 0;
  padding: 0.5rem 0.75rem;
  background: var(--surface-2);
  color: var(--text-muted);
  font-size: var(--text-sm);
  font-weight: 500;
  word-break: break-word;
}
.profile-field-value {
  margin: 0;
  padding: 0.5rem 0.75rem;
  font-size: var(--text-sm);
  /* URLs without spaces would otherwise refuse to wrap and blow out
     the cell; `overflow-wrap: anywhere` lets the browser break at any
     point, but only when needed (regular prose still breaks at word
     boundaries). Avoids the `word-break: break-word` letter-soup wrap
     ("https://sethde" / "ckard.com") in narrow deck columns. */
  overflow-wrap: anywhere;
  word-break: normal;
}
.profile-field-value a {
  color: var(--accent-text);
}

/* Deck-column profile detail (the .deck-column "Profile" panel) is a
   compact, single-pane view of a profile in 360px of width. The
   public profile page (templates/profile.html) keeps its larger
   metrics because it lives on a wide standalone page; these overrides
   only kick in inside a column container.

   Structurally, the SPA profile (web/app/pages/profile.js) puts the
   header (avatar + identity) at the top and lets the bio, joined,
   stats, fields, and actions sit as siblings BELOW the header at full
   column width — no avatar-column indent. */
.deck-column .profile-header {
  gap: 0.85rem;
  align-items: center;
  margin-bottom: 0.75rem;
  padding-bottom: 0.75rem;
}
.deck-column .profile-header .avatar,
.deck-column .profile-header .avatar-placeholder {
  width: 64px;
  height: 64px;
  flex: none;
}
.deck-column .profile-header .avatar-placeholder {
  font-size: 1.5rem;
}
.deck-column .profile-meta .display-name {
  font-size: var(--text-lg);
  letter-spacing: -0.015em;
}
.deck-column .bio {
  margin-top: 0.5rem;
  font-size: var(--text-sm);
  line-height: 1.5;
}
.deck-column .profile-joined-date,
.deck-column .stats {
  margin-top: 0.5rem;
}
.deck-column .profile-fields {
  margin-top: 0.75rem;
}
.deck-column .profile-field-name,
.deck-column .profile-field-value {
  padding: 0.4rem 0.6rem;
}
/* Compact buttons in the column-mode action row so Follow / Stop
   notifying can sit side-by-side without wrapping. margin-bottom
   gives the action row clear separation from the status feed that
   follows — without it, .status-card (which has no margin-top) sits
   flush against the buttons and reads as overlap. */
.deck-column .profile-actions {
  margin-top: 0.85rem;
  margin-bottom: 1.25rem;
  padding-bottom: 1rem;
  border-bottom: 1px solid var(--border);
}
.deck-column .profile-actions .btn {
  padding: 0.4rem 0.85rem;
  font-size: var(--text-sm);
  min-height: 0;
}

@media (max-width: 479px) {
  .profile-header {
    flex-direction: column;
    align-items: center;
    text-align: center;
  }
  .profile-meta .handle {
    display: block;
  }
  .profile-field {
    grid-template-columns: 1fr;
  }
  .profile-field-name {
    border-bottom: 1px solid var(--border);
  }
}

/* Phase 8.3 — canonical-URL affordances. "View on {domain}" link on
   the profile column; small icon-only "open original" link on account
   rows and notification rows. Token-only treatment so the
   verify.sh inline-style invariant stays green. */
.profile-external-link {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  margin-top: 0.4rem;
  color: var(--text-muted);
  font-size: var(--text-sm);
  text-decoration: none;
}
.profile-external-link:hover,
.profile-external-link:focus-visible {
  color: var(--accent-text);
  text-decoration: underline;
}
.profile-external-link svg {
  width: 0.95em;
  height: 0.95em;
}

.account-external-link,
.notification-external-link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  margin-left: 0.4rem;
  padding: 0.15rem;
  border-radius: var(--radius-sm);
  color: var(--text-muted);
}
.account-external-link:hover,
.account-external-link:focus-visible,
.notification-external-link:hover,
.notification-external-link:focus-visible {
  color: var(--accent-text);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
}
.account-external-link svg,
.notification-external-link svg {
  width: 0.9em;
  height: 0.9em;
}
/* The account-row layout (flex container, .account-link flex:1) is
   defined further down; the icon-only link sits flush to the right
   without needing further layout overrides. */

/* --- Status card --------------------------------------------------------- */

.status-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  padding: 1.5rem;
  margin-bottom: 1rem;
  transition: border-color var(--duration-fast) var(--ease-out),
              box-shadow var(--duration-fast) var(--ease-out);
}

/* Subtle accent-tinted border on hover so cards visibly respond
   instead of staying inert. The card itself isn't a primary
   interaction target (its action-row buttons are), so the response
   is bordered only — no glow halo. */
.status-card:hover {
  border-color: color-mix(in srgb, var(--accent) 35%, var(--border));
}

/* Whole-card click target — Mastodon-style. Cards in feeds + threads
   route to the thread on click; the detail variant + ancestor/
   descendant chrome opts out of this state (see below). */
.status-card--clickable {
  cursor: pointer;
}

/* Freshly-mounted Feed cards fade in for 200ms. The .status-card--enter
   class is applied by Feed.js on mount and removed after the animation
   finishes so the class never accumulates in the DOM and reduce-motion
   users collapse the animation to a frame via the global override. */
.status-card--enter {
  animation: status-card-enter 200ms ease-out;
}

@keyframes status-card-enter {
  from {
    opacity: 0;
    transform: translateY(2px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

/* Booster annotation above a boost wrapper. Mastodon-style: small,
   muted, prefixed with the boost icon. Indented to align with the
   display name (after the avatar) so the eye reads "X boosted →
   <original card>". The link inside takes the booster's profile so
   the whole-card click handler bails via the `a` exclusion. */
.status-card-booster {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  margin-bottom: 0.5rem;
  font-size: 0.875rem;
  color: var(--text-muted);
}

.status-card-booster svg {
  width: 14px;
  height: 14px;
  flex: none;
}

.status-card-booster a {
  display: inline-flex;
  align-items: baseline;
  gap: 0.3rem;
  color: inherit;
  text-decoration: none;
  min-width: 0;
}

.status-card-booster a:hover .status-card-booster-name,
.status-card-booster a:focus-visible .status-card-booster-name {
  text-decoration: underline;
  color: var(--accent-text);
}

.status-card-booster-name {
  font-weight: 600;
  color: var(--text);
}

.status-card-booster-handle {
  color: var(--text-muted);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.status-header {
  display: flex;
  flex-wrap: nowrap;
  gap: 0.75rem;
  align-items: flex-start;
  margin-bottom: 0.75rem;
}

/* Avatar sits in the header row only — Mastodon-faithful. Content,
   media, and the action row below reclaim the full card width
   instead of indenting under the avatar. In SPA mode the avatar and
   identity slots live inside .status-card-account-link (a profile
   link wrapper); in SSR / canonical mode they sit directly under
   .status-header. Both selectors match. */
.status-header > .avatar,
.status-header > .avatar-placeholder,
.status-card-account-link > .avatar,
.status-card-account-link > .avatar-placeholder {
  flex: none;
}

/* SPA-only: clicking the avatar or the stacked name/@handle pair
   navigates to the actor's profile. Keeps the existing two-column
   header layout (avatar | identity | permalink) by behaving as a
   nested flex row that absorbs the same space the old direct-child
   identity used to. Hover/focus surfaces an underline + accent on the
   display-name so it reads as a link without painting the avatar. */
.status-card-account-link {
  display: flex;
  gap: 0.75rem;
  align-items: flex-start;
  flex: 1;
  min-width: 0;
  text-decoration: none;
  color: inherit;
  border-radius: var(--radius-md);
}

.status-card-account-link:hover .display-name,
.status-card-account-link:focus-visible .display-name {
  text-decoration: underline;
  color: var(--accent-text);
}

.status-card-account-link:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* Name + handle stacked, between avatar and the right-aligned
   permalink. min-width:0 lets a long handle truncate instead of
   shoving the timestamp off the row. */
.status-identity {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  min-width: 0;
  flex: 1;
}

.status-header .display-name {
  font-weight: 600;
  line-height: 1.3;
}

.status-header .handle {
  color: var(--text-muted);
  font-size: var(--text-xs);
}

/* In the stacked identity block the handle drops below the name.
   Scoped to .status-identity so any legacy inline header markup
   is unaffected. */
.status-identity .handle {
  display: block;
}

/* Slim text back-link on the status-detail permalink page. Replaces
   the old avatar+name .status-profile-header block — the card now
   carries the avatar, so the permalink page only needs a lightweight
   "back to profile" affordance (Mastodon has no redundant header). */
.status-back {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  margin-bottom: 1rem;
  color: var(--text-muted);
  text-decoration: none;
  font-size: var(--text-sm);
  font-weight: 500;
}

.status-back:hover {
  color: var(--accent-text);
}

.status-permalink {
  margin-left: auto;
  color: var(--text-muted);
  text-decoration: none;
  font-size: 0.85rem;
}

.status-permalink:hover {
  text-decoration: underline;
}

.status-content {
  line-height: 1.6;
}

.status-content p {
  margin: 0 0 0.5rem;
}

.status-content p:last-child {
  margin-bottom: 0;
}

/* --- Content warning disclosure ------------------------------------------ */

.cw {
  margin-bottom: 0.5rem;
}

.cw-summary {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  background: var(--warning-bg);
  color: var(--warning-fg);
  border: 1px solid color-mix(in srgb, var(--warning-fg) 45%, transparent);
  padding: 0.5rem 0.9rem;
  /* A rounded rectangle (not a pill) so it reads as a disclosure-panel
     header — paired with the right-aligned chevron below — rather than
     a candy-pill button. */
  border-radius: var(--radius-md);
  cursor: pointer;
  user-select: none;
  font-size: var(--text-xs);
  font-weight: 500;
  letter-spacing: 0.01em;
  transition: background var(--duration-fast) var(--ease-out),
              border-color var(--duration-fast) var(--ease-out);
}

/* Label fills the row so the chevron is pushed to the right edge. */
.cw-summary .cw-label {
  flex: 1;
  min-width: 0;
}

/* Chevron expand/collapse cue. Points down when collapsed, rotates to
   point up when the disclosure is open — the universal "this panel
   expands" affordance. Inherits --warning-fg via currentColor. */
.cw-summary .icon {
  flex: none;
  width: 0.95rem;
  height: 0.95rem;
  transition: transform var(--duration) var(--ease-out);
}

.cw[open] .cw-summary .icon {
  transform: rotate(180deg);
}

/* Pseudo-element warning dot — keeps the marker on the same line as
   the label without forcing every consumer to add an SVG. The dot
   uses --warning-fg at full opacity so it reads as a clear badge. */
.cw-summary::before {
  content: '';
  display: inline-block;
  width: 0.45rem;
  height: 0.45rem;
  border-radius: var(--radius-full);
  background: var(--warning-fg);
  flex: none;
}

.cw-summary:hover {
  background: color-mix(in srgb, var(--warning-bg) 75%, var(--warning-fg) 12%);
  border-color: color-mix(in srgb, var(--warning-fg) 50%, transparent);
}

.cw[open] .cw-summary {
  margin-bottom: 0.75rem;
}

/* --- Media grid ---------------------------------------------------------- */
/*
 * Count-based templates: the modifier class (`media-grid--n1` …
 * `media-grid--n4`) is set by the SSR presenter and the SPA
 * `mediaGridClass` helper so the grid shape adapts to the attachment
 * count rather than collapsing to a single tall column when a narrow
 * deck has portrait images. The outer grid owns the rounded corners so
 * the inner tiles read as one seamless card with hairline gaps.
 */
.media-grid {
  display: grid;
  gap: 0.25rem;
  margin-top: 0.75rem;
  border-radius: var(--radius-md);
  overflow: hidden;
}

/* 1 — full-width, gently landscape so portrait crops aren't extreme. */
.media-grid--n1 {
  grid-template-columns: 1fr;
  aspect-ratio: 16 / 10;
}

/* 2 — side by side, each cell roughly square. */
.media-grid--n2 {
  grid-template-columns: 1fr 1fr;
  aspect-ratio: 16 / 9;
}

/* 3 — left tall + right two stacked (Mastodon's split). */
.media-grid--n3 {
  grid-template-columns: 1fr 1fr;
  grid-template-rows: 1fr 1fr;
  aspect-ratio: 16 / 10;
}
.media-grid--n3 .media-item:first-child { grid-row: 1 / span 2; }

/* 4 — 2×2 grid. */
.media-grid--n4 {
  grid-template-columns: 1fr 1fr;
  grid-template-rows: 1fr 1fr;
  aspect-ratio: 1 / 1;
}

/* When every tile in the grid is the .media-broken text fallback —
   typically a status whose attachments are video / audio that SoloPub
   doesn't yet render with a player — drop the count grid's
   aspect-ratio so the row sizes to the broken-tile content (anchored
   at .media-broken's `min-height: 120px`) instead of stretching a
   small "Attachment" label into a large empty-looking box. Mixed
   posts (image + broken) keep the aspect-ratio so the image still
   gets a proportional cell. */
.media-grid:not(:has(.media-item:not(.media-broken))) {
  aspect-ratio: auto;
}

.media-item {
  display: block;
  width: 100%;
  height: 100%;
  /* grid items refuse to shrink below their intrinsic content size by
     default; this lets the <img> honor width/height: 100% instead of
     pushing the column wider than its track. */
  min-width: 0;
  min-height: 0;
  overflow: hidden;
  background: var(--surface-2);
  line-height: 0;
}

.media-item img {
  display: block;
  width: 100%;
  height: 100%;
  /* The grid's aspect-ratio + cell sizing dictates height now;
     `object-fit: cover` crops portrait images cleanly into landscape
     cells. The full image is still available via the wrapping anchor. */
  object-fit: cover;
}

.media-broken {
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 1rem;
  color: var(--text-muted);
  background: var(--surface-2);
  min-height: 120px;
  font-size: var(--text-sm);
  /* Reset the line-height: 0 inherited from .media-item (which sets
     it to kill the <img> inline-gap). Without this, a failed image
     whose alt text wraps to two lines collapses the lines on top of
     each other — common on unreachable federated media with a
     multi-word description. */
  line-height: 1.4;
  overflow-wrap: anywhere;
}

/* Reading defaults (owner-SPA-only, applied via StatusCard's
 * `mediaSensitiveOverride` prop). The blur is purely a click-through
 * affordance: the underlying <a> still navigates to the full image,
 * matching the design contract that the SPA never hides content from
 * the owner's own viewport. */
.media-grid--blurred .media-item img {
  filter: blur(24px);
  transition: filter 200ms ease;
}

.media-grid--blurred .media-item:hover img,
.media-grid--blurred .media-item:focus-within img {
  filter: blur(0);
}

/* --- Action row (authed SPA only) ---------------------------------------- */

.action-row {
  display: flex;
  gap: 0.5rem;
  margin-top: 0.75rem;
  padding-top: 0.75rem;
  border-top: 1px solid var(--border);
}

/* Status-card action buttons. Compact rectangle — same shape
   semantics as .btn-ghost.btn-sm but kept as its own class because
   it's the canonical action row from docs/design.md §6. */
.action-btn {
  background: transparent;
  color: var(--text-muted);
  font-weight: 500;
  padding: 0.4rem 0.85rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  min-height: 32px;
  font-size: var(--text-sm);
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  transition: background-color var(--duration-fast) var(--ease-out),
              color var(--duration-fast) var(--ease-out),
              border-color var(--duration-fast) var(--ease-out),
              box-shadow var(--duration-fast) var(--ease-out);
}

.action-btn:hover {
  color: var(--accent-text);
  background: var(--surface-2);
  border-color: color-mix(in srgb, var(--accent) 50%, var(--border));
  box-shadow: var(--glow-action-soft);
}

.action-btn:active {
  box-shadow: var(--glow-action-strong);
}

.action-btn[aria-pressed="true"] {
  color: var(--active-text);
  border-color: var(--accent);
  background: var(--active-tint);
}

.action-btn[disabled] {
  opacity: 0.5;
  cursor: not-allowed;
}

/* Action-row SVG icons. Sized to the text cap-height and inheriting
   currentColor so the aria-pressed accent recolor carries to the
   glyph (e.g. a favorited star fills accent). */
.action-btn .icon {
  width: 1rem;
  height: 1rem;
  flex: none;
}

/* --- Modal ---------------------------------------------------------------

   Controlled dialog from web/app/components/modal.js. Replaces the
   browser confirm() for destructive actions. The focus trap, Esc, and
   backdrop-click dismissal live in the component; this is presentation
   only. Backdrop dims (no blur, see --backdrop); the dialog lifts on
   --shadow-lg and rises 4px on open. */

.modal-backdrop {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1rem;
  background: var(--backdrop);
  animation: modal-backdrop-in var(--duration) var(--ease-out);
}

.modal {
  width: 100%;
  max-width: 440px;
  background: var(--surface-overlay);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  padding: 1.5rem;
  animation: modal-in var(--duration) var(--ease-out);
}

.modal-header {
  margin-bottom: 0.6rem;
}

.modal-title {
  margin: 0;
  font-size: var(--text-lg);
  font-weight: 650;
  letter-spacing: -0.01em;
}

/* Caller body copy — quiet supporting text under the title. */
.modal-text {
  margin: 0;
  color: var(--text-muted);
  font-size: var(--text-sm);
  line-height: 1.5;
}

.modal-actions {
  display: flex;
  justify-content: flex-end;
  gap: 0.5rem;
  margin-top: 1.5rem;
}

@keyframes modal-backdrop-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

@keyframes modal-in {
  from { opacity: 0; transform: translateY(4px); }
  to { opacity: 1; transform: none; }
}

/* --- Toasts --------------------------------------------------------------

   Transient status banners from web/app/components/toast.js. The stack
   is a persistent polite live region anchored bottom-right (desktop) /
   bottom-center (mobile). Auto-dismiss after 4s; each carries a manual
   ×. Variants tint a 3px left border only. */

.toast-stack {
  position: fixed;
  z-index: 110;
  right: 1rem;
  bottom: 1rem;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  /* The empty/idle container must not eat clicks; each toast re-enables
     pointer events for its own surface + dismiss button. */
  pointer-events: none;
}

.toast {
  pointer-events: auto;
  display: flex;
  align-items: center;
  gap: 0.75rem;
  min-width: 240px;
  max-width: 360px;
  padding: 0.7rem 0.85rem;
  background: var(--surface-overlay);
  border: 1px solid var(--border);
  border-left-width: 3px;
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-md);
  color: var(--text);
  font-size: var(--text-sm);
  animation: toast-in var(--duration) var(--ease-out);
}

.toast-message {
  flex: 1;
  min-width: 0;
}

/* Override the global button rule — the dismiss is a quiet glyph, not
   an accent-filled CTA. */
.toast-dismiss {
  flex: none;
  min-height: auto;
  padding: 0 0.15rem;
  background: transparent;
  border: none;
  color: var(--text-muted);
  font-size: 1.2rem;
  line-height: 1;
}

.toast-dismiss:hover {
  background: transparent;
  color: var(--text);
}

.toast--error { border-left-color: var(--danger); }
.toast--success { border-left-color: var(--accent); }
.toast--neutral { border-left-color: var(--text-subtle); }

@keyframes toast-in {
  from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: none; }
}

@media (max-width: 599px) {
  .toast-stack {
    right: 1rem;
    left: 1rem;
    align-items: center;
  }
  .toast {
    width: 100%;
    max-width: 420px;
  }
}

/* --- System states -------------------------------------------------------

   Three shared "your data isn't here yet" surfaces from
   docs/design.md §7 — loading skeleton, empty, error — all driven by
   the primitives in web/app/components/system-states.js. The outer
   .loading-skeleton wrapper keeps role="status" + aria-live="polite"
   so the existing a11y contract holds; the interior is now
   content-shaped (.skeleton-card mirroring the real .status-card).
*/

.loading-skeleton {
  /* Outer container — no padding/centering. The .skeleton-card
     children own their own spacing so loading and loaded states have
     matching vertical rhythm (no jump on transition). */
}

.skeleton-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  padding: 1.5rem;
  margin-bottom: 1rem;
}

.skeleton-card-header {
  display: flex;
  align-items: flex-start;
  gap: 0.75rem;
  margin-bottom: 0.9rem;
}

.skeleton-identity {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  flex: 1;
  min-width: 0;
  padding-top: 0.25rem;
}

.skeleton-card-body {
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
}

/* Building blocks — circle (avatar) + line (text). Pulse via
   .skeleton-pulse keyframes so prefers-reduced-motion collapses it
   via the global override. */
.skeleton-circle,
.skeleton-line {
  background: var(--surface-2);
  animation: skeleton-pulse 1.5s var(--ease-in-out) infinite;
}

.skeleton-circle {
  width: 48px;
  height: 48px;
  border-radius: var(--radius-full);
  flex: none;
}

.skeleton-line {
  height: 0.85rem;
  border-radius: var(--radius-sm);
}

.skeleton-line--name {
  width: 35%;
  height: 1rem;
}

.skeleton-line--handle {
  width: 22%;
  height: 0.75rem;
}

.skeleton-line--short {
  width: 60%;
}

@keyframes skeleton-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.55; }
}

/* Empty state — quiet icon + title + supporting copy + optional CTA. */
.empty-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.85rem;
  padding: 3rem 1.5rem;
  text-align: center;
  color: var(--text-muted);
}

.empty-state-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 56px;
  height: 56px;
  border-radius: var(--radius-full);
  background: var(--surface-2);
  color: var(--text-subtle);
}

.empty-state-icon .icon {
  width: 1.5rem;
  height: 1.5rem;
}

.empty-state-title {
  margin: 0;
  font-size: var(--text-md);
  font-weight: 600;
  color: var(--text);
}

.empty-state-description {
  margin: 0;
  max-width: 36ch;
  color: var(--text-muted);
  font-size: var(--text-sm);
  line-height: 1.5;
}

.empty-state .btn {
  margin-top: 0.5rem;
}

/* Error state — same column layout, danger-tinted icon, retry as
   a quiet ghost button. Subtle background hint via color-mix so the
   block reads as a discrete error region without screaming. */
.error-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.75rem;
  padding: 2rem 1.5rem;
  text-align: center;
  border: 1px solid color-mix(in srgb, var(--danger) 30%, var(--border));
  border-radius: var(--radius-lg);
  background: color-mix(in srgb, var(--danger) 6%, transparent);
}

.error-state-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border-radius: var(--radius-full);
  background: color-mix(in srgb, var(--danger) 14%, transparent);
  color: var(--danger);
}

.error-state-icon .icon {
  width: 1.25rem;
  height: 1.25rem;
}

.error-state-title {
  margin: 0;
  font-size: var(--text-md);
  font-weight: 600;
  color: var(--text);
}

.error-state-description {
  margin: 0;
  max-width: 40ch;
  color: var(--text-muted);
  font-size: var(--text-sm);
  line-height: 1.5;
}

/* --- Compose form -------------------------------------------------------- */

/* Compose lives inside narrow deck columns (400px), so tighten the
   surrounding card padding from the default 1.5rem. The form itself
   carries its own vertical rhythm via the row gaps below. */
.compose-form.card {
  padding: 0.85rem;
  margin-top: 0.75rem;
  /* Anchors the drag-and-drop overlay below so it covers the whole form. */
  position: relative;
}

/* Drag-and-drop dropzone. Outline gives a soft "yes, this is a drop
   target" hint; the centered overlay reinforces it. Pointer-events
   on the overlay are disabled so the drag events keep firing on the
   form below (the overlay must not become its own drop target). */
.compose-form--drag-over {
  outline: 2px dashed var(--accent);
  outline-offset: -2px;
}

.compose-dropzone-overlay {
  position: absolute;
  inset: 0;
  z-index: 50;
  display: flex;
  align-items: center;
  justify-content: center;
  background: color-mix(in srgb, var(--surface-raised) 88%, transparent);
  color: var(--accent-text);
  font-weight: 600;
  pointer-events: none;
  border-radius: var(--radius-md);
}

/* Top row that holds the visibility + language chips. Mastodon-style:
   quiet pills that show the current option + open a menu. The row
   anchors each menu via .compose-chip-wrap's relative positioning. */
.compose-chip-row {
  display: flex;
  gap: 0.4rem;
  flex-wrap: wrap;
  margin-bottom: 0.6rem;
}

.compose-chip-wrap {
  position: relative;
  display: inline-flex;
}

.compose-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  min-height: 28px;
  padding: 0.2rem 0.55rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-full);
  background: transparent;
  color: var(--text-muted);
  font-size: var(--text-xs);
  font-weight: 500;
  white-space: nowrap;
  cursor: pointer;
  transition: color var(--duration-fast) var(--ease-out),
              background-color var(--duration-fast) var(--ease-out),
              border-color var(--duration-fast) var(--ease-out);
}

.compose-chip:hover {
  color: var(--accent-text);
  background: var(--surface-2);
  border-color: color-mix(in srgb, var(--accent) 50%, var(--border));
}

.compose-chip[aria-expanded="true"] {
  color: var(--active-text);
  border-color: var(--accent);
  background: var(--active-tint);
}

.compose-chip[disabled] {
  opacity: 0.55;
  cursor: not-allowed;
}

.compose-chip--invalid {
  border-color: var(--danger);
  color: var(--danger);
}

.compose-chip > .icon {
  width: 0.9rem;
  height: 0.9rem;
  flex: none;
}

/* The trailing chevron is smaller + muted; rotates when the menu is open. */
.compose-chip > .icon:last-child {
  width: 0.75rem;
  height: 0.75rem;
  opacity: 0.7;
  transition: transform var(--duration-fast) var(--ease-out);
}

.compose-chip[aria-expanded="true"] > .icon:last-child {
  transform: rotate(180deg);
  opacity: 1;
}

/* Anchor the menu to the chip (not the row). The base .nav-menu-panel
   rule sets right: 0 because nav-menu's trigger sits on the right edge
   of the header; for compose chips we want the panel to open against
   the chip's left edge instead. */
.compose-chip-menu {
  right: auto;
  left: 0;
  min-width: 200px;
}

/* Language chip menu — the freeform "Custom (BCP-47)" input sits
   above the preset list. The two-column preset rows show the BCP-47
   code on the left in a muted monospace and the native name on the
   right, so the eye can find a language quickly without parsing the
   code each time. */
.compose-chip-menu--language {
  min-width: 230px;
}

.compose-chip-menu-custom {
  padding: 0.35rem 0.6rem 0.5rem;
  border-bottom: 1px solid var(--border);
  margin-bottom: 0.25rem;
}

.compose-chip-menu-custom-label {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  font-size: var(--text-xs);
  color: var(--text-subtle);
}

.compose-chip-menu-custom-label input {
  font-size: var(--text-sm);
  padding: 0.35rem 0.5rem;
}

.compose-chip-menu-custom-label input[aria-invalid="true"] {
  border-color: var(--danger);
  color: var(--danger);
}

.compose-chip-menu--language .nav-menu-item {
  display: grid;
  grid-template-columns: 2.25rem 1fr;
  align-items: baseline;
}

.compose-chip-menu--language .nav-menu-item .lang-code {
  font-family: var(--font-mono, ui-monospace, SFMono-Regular, monospace);
  font-size: var(--text-xs);
  color: var(--text-subtle);
  text-transform: lowercase;
}

.compose-chip-menu--language .nav-menu-item[aria-current="true"] {
  color: var(--accent-text);
}

/* The CW row uses a bare <input> (with a visually-hidden label) so the
   global `label { text-transform: uppercase }` doesn't paint a big
   "CONTENT WARNING" header above the input. The input itself stays
   the standard input style, just visually grouped with the body. */
.compose-cw {
  margin-bottom: 0.5rem;
}

.compose-cw input {
  padding: 0.5rem 0.7rem;
  font-size: var(--text-sm);
}

.compose-body {
  margin-bottom: 0;
}

.compose-form textarea {
  resize: vertical;
  min-height: 4.5rem;
  font-family: inherit;
  font-size: var(--text-sm);
  padding: 0.55rem 0.7rem;
}

/* Anchors the emoji picker popover above its trigger. The wrap sits
   in the bottom actions row (and, in a follow-up commit, inside the
   ComposeToolbar). */
.compose-emoji-wrap {
  position: relative;
  display: inline-flex;
}

/* Emoji picker popover — Unicode-only v1 (no custom emoji). Lays out
   as: category tabs across the top, 8-column grid below, both inside
   a small floating panel that anchors above the trigger via the
   wrap's relative positioning. */
.emoji-picker {
  position: absolute;
  left: 0;
  bottom: calc(100% + 0.4rem);
  z-index: 90;
  width: 296px;
  max-height: 320px;
  display: flex;
  flex-direction: column;
  background: var(--surface-overlay);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-lg);
  animation: nav-menu-in var(--duration) var(--ease-out);
}

.emoji-picker-tabs {
  display: flex;
  gap: 0.15rem;
  padding: 0.35rem;
  border-bottom: 1px solid var(--border);
  overflow-x: auto;
}

.emoji-picker-tab {
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  padding: 0.25rem 0.4rem;
  font-size: 1.05rem;
  line-height: 1;
  cursor: pointer;
  transition: background-color var(--duration-fast) var(--ease-out),
              border-color var(--duration-fast) var(--ease-out);
}

.emoji-picker-tab:hover,
.emoji-picker-tab:focus-visible {
  background: var(--surface-2);
}

.emoji-picker-tab--active {
  border-color: color-mix(in srgb, var(--accent) 50%, var(--border));
  background: var(--active-tint);
}

.emoji-picker-grid {
  display: grid;
  grid-template-columns: repeat(8, 1fr);
  gap: 2px;
  overflow-y: auto;
  padding: 0.35rem;
}

.emoji-cell {
  background: transparent;
  border: none;
  padding: 0.3rem;
  border-radius: var(--radius-sm);
  font-size: 1.2rem;
  line-height: 1;
  cursor: pointer;
  transition: background-color var(--duration-fast) var(--ease-out);
}

.emoji-cell:hover,
.emoji-cell:focus-visible {
  background: var(--surface-2);
  outline: none;
}

/* Mastodon-style bottom action row: four icon-only buttons on the
   left (image / poll / CW / emoji), counter + filled Post button on
   the right. Compact enough to fit in a 400px deck column without
   pushing the Post button off the right edge. */
.compose-toolbar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 0.4rem;
  margin-top: 0.6rem;
  padding-top: 0.55rem;
  border-top: 1px solid var(--border);
}

.compose-toolbar-actions {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  min-width: 0;
}

.compose-toolbar-submit {
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
  flex: none;
}

/* Pin the counter to the same 32px slot as its row neighbors so the
   text baseline stays centered with the icon buttons + Post button.
   Without explicit height the text-box is ~22px (font 15 × line-
   height 1.5) which the flex parent centers — but that centering
   measurement disagrees with the 32px sibling boxes by ~0.5–1px in
   some renderers, enough to read as misaligned at this scale. */
.compose-toolbar-submit .char-counter {
  display: inline-flex;
  align-items: center;
  height: 32px;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}

/* Toolbar icon buttons render borderless (Mastodon parity) — the
   .action-btn base draws a 1px outline because it's used on status
   cards as a pill; here we want the glyph to read as a naked icon,
   with only the hover/active background giving it a touch target. */
.compose-toolbar .action-btn {
  padding: 0;
  width: 32px;
  height: 32px;
  min-height: 32px;
  justify-content: center;
  border: 1px solid transparent;
  border-radius: var(--radius-full);
  color: var(--text-muted);
  background: transparent;
}

.compose-toolbar .action-btn:hover {
  background: var(--surface-2);
  color: var(--accent-text);
  border-color: transparent;
  box-shadow: none;
}

.compose-toolbar .action-btn[aria-pressed="true"] {
  color: var(--active-text);
  background: var(--active-tint);
  border-color: transparent;
}

.compose-toolbar .action-btn[disabled] {
  background: transparent;
  border-color: transparent;
}

.compose-toolbar .action-btn .icon {
  width: 1.15rem;
  height: 1.15rem;
}

/* The image action is a <label> wrapping a visually-hidden file input
   so the label click opens the picker without us programmatically
   invoking the input. The global `label` rule sets
   `margin-bottom: 0.4rem`, which throws the flex-centered icon row
   off by 0.4rem — the label sits visibly higher than the sibling
   <button> icons. Zero the margin (and the other label defaults) so
   the label behaves identically to the <button> neighbors. */
.compose-toolbar-image {
  position: relative;
  margin-bottom: 0;
  text-transform: none;
  letter-spacing: 0;
  font-weight: inherit;
  color: inherit;
}

.compose-toolbar-image--disabled {
  opacity: 0.45;
  pointer-events: none;
  cursor: not-allowed;
}

/* Filled accent submit. Smaller and rounded-rect (Mastodon-style),
   not the full .btn pill — the toolbar reads as the action surface,
   not a CTA. */
.compose-post-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 32px;
  min-height: 0;
  padding: 0 0.95rem;
  background: var(--accent);
  color: var(--on-accent);
  border: 1px solid transparent;
  border-radius: var(--radius-md);
  font-weight: 600;
  font-size: var(--text-sm);
  line-height: 1;
  cursor: pointer;
  transition: background-color var(--duration-fast) var(--ease-out),
              box-shadow var(--duration-fast) var(--ease-out);
}

.compose-post-btn:hover {
  box-shadow: var(--glow-action-soft);
}

.compose-post-btn:active {
  box-shadow: var(--glow-action-strong);
}

.compose-post-btn[disabled] {
  opacity: 0.5;
  cursor: not-allowed;
  box-shadow: none;
}

.char-counter {
  font-size: var(--text-sm);
  color: var(--text-subtle);
}

/* Approaching the limit — amber heads-up before the hard danger. */
.char-counter.char-counter--warn {
  color: var(--warning-fg);
}

.char-counter.char-counter--over {
  color: var(--danger);
  font-weight: 600;
}

/* --- Page wrappers ------------------------------------------------------- */

.page {
  max-width: 720px;
  margin: 0 auto;
  padding: 1rem 0;
}

/* --- Feed tail (Load more) ---------------------------------------------- */

.load-more {
  text-align: center;
  padding: 1rem 0;
}

/* --- Profile search / follow controls ------------------------------------ */

.profile-search {
  display: flex;
  gap: 0.5rem;
  margin-bottom: 1rem;
}

.profile-search input {
  flex: 1;
}

.profile-actions {
  display: flex;
  gap: 0.5rem;
  margin-top: 0.75rem;
  flex-wrap: wrap;
}

.follow-pending {
  color: var(--text-muted);
  font-size: 0.9rem;
  margin: 0.5rem 0 1rem;
}

/* --- Settings form + dropzones ------------------------------------------ */

.settings-form + .settings-form {
  margin-top: 1rem;
}

.settings-form h3 {
  margin: 0 0 0.75rem;
  font-size: 1.1rem;
}

.settings-form pre {
  margin: 0.5rem 0;
}

.dropzone {
  margin-bottom: 1rem;
}

.dropzone > label > span {
  display: block;
  margin-bottom: 0.35rem;
  font-weight: 600;
  color: var(--text);
}

.dropzone-surface {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.75rem;
  border: 1px dashed var(--border);
  border-radius: 6px;
  background: var(--surface-2);
  flex-wrap: wrap;
}

.dropzone-surface input[type="file"] {
  flex: 1;
  min-width: 0;
  background: transparent;
  padding: 0;
  border: none;
}

.dropzone-empty {
  color: var(--text-muted);
  font-size: 0.9rem;
}

.dropzone-header-preview {
  display: block;
  width: 100%;
  max-width: 240px;
  height: auto;
  border-radius: 6px;
  object-fit: cover;
  background: var(--surface);
}

/* --- Reply banner (compose page) ----------------------------------------- */

.reply-banner {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 0.5rem 0.75rem;
  margin-bottom: 0.75rem;
  color: var(--text-muted);
  font-size: 0.9rem;
}

.reply-banner strong {
  color: var(--text);
  font-weight: 600;
}

.reply-banner--error {
  background: var(--warning-bg);
  border-color: var(--warning-fg);
  color: var(--warning-fg);
}

/* --- Sign-out confirmation banner ---------------------------------------- */

.signed-out {
  margin: 3rem auto;
  padding: 1rem 1.5rem;
  max-width: 320px;
  text-align: center;
  font-size: 1rem;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
}

/* --- Deck (authed multi-column shell) ------------------------------------ */

/* The deck owns the viewport, escaping the global 720px body width. The Deck
   component adds `deck-mode` to <body> while mounted and removes it on unmount,
   so the loading/redirect placeholders and every SSR surface keep the centered
   720px layout. */
body.deck-mode {
  max-width: none;
  margin: 0;
  padding: 0;
  height: 100vh;
  overflow: hidden;
}

.deck {
  display: flex;
  height: 100vh;
}

/* Left rail — fixed-width vertical column (brand/account, nav, compose, foot).
   Width matches .deck-column so the rail and the columns to its right form a
   visually-uniform grid of equal-width panels. */
.deck-rail {
  flex: none;
  width: 360px;
  display: flex;
  flex-direction: column;
  gap: 1rem;
  padding: 1rem;
  border-right: 1px solid var(--border);
  background: var(--surface);
  overflow-y: auto;
}

.rail-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
}

.deck-rail .brand {
  color: var(--text);
  text-decoration: none;
  font-weight: 700;
  font-size: 1.2rem;
  letter-spacing: -0.02em;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.deck-rail .brand:hover {
  color: var(--accent-text);
}

.rail-nav {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}

/* Left-align the rail nav items (icon + label) instead of the header's
   centered pills. */
.rail-nav .nav-item {
  justify-content: flex-start;
  width: 100%;
}

/* The compose button is the mobile-only path into the compose modal; on
   desktop the persistent panel below handles new posts. */
.rail-compose-btn {
  display: none;
}

.rail-foot {
  margin-top: auto;
}

.rail-foot .site-footer {
  margin-top: 0;
  border-top: 1px solid var(--border);
  justify-content: flex-start;
}

/* Columns scroller — the rest of the viewport. */
.deck-columns {
  flex: 1;
  display: flex;
  overflow-x: auto;
  overflow-y: hidden;
  height: 100vh;
}

.deck-column {
  flex: none;
  width: 360px;
  max-width: 100vw;
  display: flex;
  flex-direction: column;
  height: 100%;
  border-right: 1px solid var(--border);
}

.column-header {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  padding: 0.75rem 1rem;
  border-bottom: 1px solid var(--border);
  background: var(--surface);
}

.column-title {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin: 0;
  font-size: 1.15rem;
  letter-spacing: -0.015em;
}

/* Programmatic focus target (route focus moves here); the heading itself
   shouldn't paint a focus ring like an interactive control. */
.column-title:focus {
  outline: none;
}

.column-title .icon {
  width: 1.1rem;
  height: 1.1rem;
  flex: none;
  color: var(--text-muted);
}

.column-controls {
  display: inline-flex;
  gap: 0.25rem;
}

.column-control {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: transparent;
  color: var(--text-muted);
  cursor: pointer;
  transition: color var(--duration-fast) var(--ease-out),
              background-color var(--duration-fast) var(--ease-out),
              border-color var(--duration-fast) var(--ease-out);
}

.column-control:hover {
  background: var(--surface-2);
  color: var(--text);
  border-color: color-mix(in srgb, var(--accent) 50%, var(--border));
}

.column-control .icon {
  width: 1.1rem;
  height: 1.1rem;
  flex: none;
  color: var(--text-muted);
}

.column-control:hover .icon {
  color: var(--text);
}

/* Streaming connection-state indicator (Phase 1). A small dot in the column
   header next to the manual Refresh button — color reads at a glance, the
   `aria-label` / `title` carries the literal state for AT and tooltips.
   The reduced-motion media query at the top of this file collapses the
   pulse animation automatically. */
.connection-indicator {
  display: inline-block;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  flex: none;
  background: var(--text-subtle);
}

.connection-indicator--live {
  background: var(--accent);
}

.connection-indicator--connecting,
.connection-indicator--reconnecting {
  background: var(--warning-fg);
  animation: connection-pulse 1.4s ease-in-out infinite;
}

.connection-indicator--offline {
  background: var(--danger);
}

@keyframes connection-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.35; }
}

/* --- Notifications column (Phase 2) ------------------------------------- */

.notification-tabs {
  display: flex;
  gap: 0.25rem;
  padding: 0.5rem 0.75rem;
  border-bottom: 1px solid var(--border);
  background: var(--surface);
}

.notification-tab {
  appearance: none;
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--radius-md);
  padding: 0.35rem 0.75rem;
  min-height: 32px;
  color: var(--text-muted);
  font-size: var(--text-sm);
  font-weight: 500;
  cursor: pointer;
  transition: color var(--duration-fast) var(--ease-out),
              background-color var(--duration-fast) var(--ease-out);
}

.notification-tab:hover {
  color: var(--text);
  background: var(--surface-2);
}

.notification-tab--active {
  color: var(--active-text);
  background: var(--active-tint);
}

.notifications-list {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  padding: 0.75rem;
}

.notification-item {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  padding: 0.75rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
}

/* Unread treatment — accent left edge so the unread state reads at a
   glance without changing the overall density. */
.notification-item--unread {
  border-left: 3px solid var(--accent);
  padding-left: calc(0.75rem - 2px);
}

.notification-header {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.5rem;
  font-size: var(--text-sm);
  color: var(--text-muted);
}

.notification-actor {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  color: var(--text);
  text-decoration: none;
}

.notification-actor:hover .display-name {
  color: var(--accent-text);
}

.notification-actor-text {
  display: inline-flex;
  flex-direction: column;
  min-width: 0;
}

.notification-actor-text .display-name {
  font-weight: 600;
  color: var(--text);
  font-size: var(--text-sm);
}

.notification-actor-text .handle {
  color: var(--text-muted);
  font-size: var(--text-xs);
}

.notification-verb {
  color: var(--text-muted);
}

.notification-time {
  margin-left: auto;
  color: var(--text-subtle);
  font-size: var(--text-xs);
}

.notification-actions {
  display: flex;
  gap: 0.5rem;
  align-items: center;
}

.notification-action-error {
  color: var(--danger);
  font-size: var(--text-xs);
}

/* Mention / reply notifications: minimal icon-led verb row instead of the
   actor-led `.notification-header`. The embedded StatusCard below carries
   the actor identity, so the row only needs to identify the notification
   type ("Replied to you" or "Mentioned you") with a leading glyph. */
.notification-verb-row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  color: var(--text-muted);
  font-size: var(--text-sm);
  font-weight: 500;
}

.notification-verb-row .icon {
  width: 18px;
  height: 18px;
  flex: 0 0 auto;
}

.notification-verb-row .notification-verb-text {
  color: var(--text-muted);
}

/* favourite / reblog verb row: type-icon-led, single-line, xs avatar.
   The type-icon glyph color is owned by the wrapping span so the SVG
   (stroke=currentColor) picks up the tint from a single style hook. */
.notification-verb-row--action {
  font-size: var(--text-sm);
  flex-wrap: wrap;
}

.notification-type-icon {
  display: inline-flex;
  align-items: center;
  flex: 0 0 auto;
  color: var(--text-muted);
}

.notification-type-icon .icon {
  width: 18px;
  height: 18px;
}

.notification-type-icon--fav    { color: var(--warning-fg); }
.notification-type-icon--boost  { color: var(--accent); }
.notification-type-icon--follow { color: var(--accent); }

/* Within the icon-led verb row, push the right-side affordance
   (timestamp for `follow` / `favourite` / `reblog`, action buttons for
   `follow_request`) flush right. The vertical-stack `.notification-actions`
   used elsewhere keeps its default flow when not nested in this row. */
.notification-verb-row--action .notification-time,
.notification-verb-row--action .notification-actions {
  margin-left: auto;
}

/* Inline actor block for the action-row variant: avatar + display-name
   on one line, no handle (the compact preview below carries handle). */
.notification-actor--inline {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  color: var(--text);
  text-decoration: none;
}

.notification-actor--inline:hover .notification-actor-name {
  color: var(--accent-text);
}

.notification-actor-name {
  color: var(--text);
  font-weight: 700;
  font-size: var(--text-sm);
}

/* Handle next to the display name in follow / follow_request rows.
   favourite / reblog rows don't render this — their compact preview
   below already carries the handle, so duplicating would crowd the row. */
.notification-actor-handle {
  color: var(--text-muted);
  font-size: var(--text-xs);
}

/* xs avatar override for the verb row — matches Mastodon's tight identity
   chip size. The Avatar component's `size="xs"` already emits 28px; the
   override pulls it to 24px so the row stays single-line. */
.notification-verb-row .avatar-xs,
.notification-verb-row .avatar-placeholder.avatar-xs {
  width: 24px;
  height: 24px;
}

/* Compact status preview — favourite / reblog notifications. The
   embedded status is the owner's own post; this surface is a
   click-through summary, not an interactive card. Click anywhere
   (other than an inner mention / hashtag / link) routes to status
   detail via the stretched-link pattern: a standalone <a> covers the
   card, while mentions inside the post content sit one z-index above
   so they intercept their own clicks. This avoids the invalid
   nested-anchor pattern that would arise if the whole preview were
   itself an anchor. */
.status-preview-compact {
  position: relative;
  display: flex;
  gap: 0.5rem;
  padding: 0.5rem 0.75rem;
  border-radius: var(--radius-md);
  background: var(--surface-2);
  color: var(--text);
  transition: background var(--duration-fast) var(--ease-out);
}

.status-preview-compact:hover {
  background: var(--surface-raised);
}

/* Focus follows the stretched link so keyboard users see the whole card
   highlight, not just a 0-pixel anchor. */
.status-preview-compact:focus-within {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* Stretched link — invisible but interactive, covers the full card. */
.status-preview-compact-link {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  z-index: 0;
}

/* The stretched link's native focus ring would draw at the card edge;
   we render the focus state on the parent (:focus-within above) so
   suppress the default outline on the link itself. */
.status-preview-compact-link:focus {
  outline: none;
}

/* Inner content anchors (mentions, hashtags, external links the server
   sanitized into the post HTML) sit above the stretched link so they
   handle their own clicks instead of routing to status detail. */
.status-preview-compact .spc-content a {
  position: relative;
  z-index: 1;
}

.status-preview-compact .avatar,
.status-preview-compact .avatar-placeholder {
  flex: 0 0 auto;
}

.spc-body {
  min-width: 0;
  flex: 1 1 auto;
}

.spc-identity {
  display: inline-flex;
  gap: 0.35rem;
  align-items: baseline;
  flex-wrap: wrap;
}

.spc-identity .display-name {
  color: var(--text);
  font-weight: 600;
  font-size: var(--text-sm);
}

.spc-identity .handle {
  color: var(--text-muted);
  font-size: var(--text-xs);
}

.spc-content {
  color: var(--text);
  margin-top: 0.15rem;
  font-size: var(--text-sm);
  line-height: 1.4;
  overflow-wrap: anywhere;
}

.spc-content :where(p) {
  margin: 0;
}

.spc-cw {
  color: var(--text-muted);
  font-weight: 500;
  margin-right: 0.25rem;
}

.spc-attachments {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  color: var(--text-muted);
  font-size: var(--text-xs);
  margin-top: 0.25rem;
}

.spc-attachments .icon {
  width: 14px;
  height: 14px;
}

/* --- Thread / detail column ---------------------------------------------
   Mastodon-style merged panel: ancestor → focused → descendants stack
   as one continuous surface with thin dividers between items instead
   of separate floating cards. The focused status carries the expanded
   detail footer; everything else is shaped the same. */

.thread-page {
  display: flex;
  flex-direction: column;
  gap: 0;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  overflow: hidden;
}

.thread-section {
  display: flex;
  flex-direction: column;
  gap: 0;
}

/* Cards inside the thread lose their floating-card chrome and become
   stacked items in the panel. A thin border-bottom separates adjacent
   items; the panel's outer border carries the rounded edge. */
.thread-page .status-card {
  border: none;
  border-bottom: 1px solid var(--border);
  border-radius: 0;
  margin-bottom: 0;
  box-shadow: none;
  background: transparent;
  transition: background var(--duration-fast) var(--ease-out);
}

/* No bottom divider under the very last item — the panel's own border
   is the bottom edge. */
.thread-page .thread-section:last-child > .status-card:last-child {
  border-bottom: none;
}

/* Hover: subtle background tint instead of the accent-border tint used
   in feeds. The merged panel has no individual card borders to tint. */
.thread-page .status-card:hover {
  background: color-mix(in srgb, var(--surface-hover) 50%, transparent);
  border-color: var(--border); /* keep the divider visible */
}

/* Focused card isn't clickable, so no hover tint. */
.thread-page .status-card--detail:hover {
  background: transparent;
}

/* Ancestor + descendant statuses read as context — slight opacity
   pull-back lets the focused status feel like the anchor. Mastodon
   doesn't indent thread replies; depth reads from chronological
   order + the in_reply_to hierarchy, not visual offset. */
.thread-page .status-card--ancestor,
.thread-page .status-card--descendant {
  opacity: 0.92;
}

/* --- Status-detail expanded footer -------------------------------------- */

/* The focused status in the Thread column. Carries the Mastodon
   "detail" footer with absolute timestamp + visibility + boost/
   favorite counts. Whole-card click is suppressed in the component
   (no `--clickable` class) since clicking is a no-op when already
   focused. */
.status-card--detail {
  cursor: default;
}

.status-detail-footer {
  margin-top: 0.75rem;
  padding-top: 0.75rem;
  border-top: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  color: var(--text-muted);
  font-size: var(--text-sm);
}

.status-detail-meta {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
}

.status-detail-meta time {
  font-feature-settings: 'cv11', 'ss01', 'tnum';
}

.status-detail-sep {
  color: var(--text-muted);
  opacity: 0.7;
}

.status-detail-visibility {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
}

.status-detail-visibility .icon {
  width: 1em;
  height: 1em;
}

.status-detail-stats {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding-top: 0.5rem;
  border-top: 1px solid var(--border);
  flex-wrap: wrap;
}

.status-detail-stat strong {
  color: var(--text);
  font-weight: 600;
  margin-right: 0.25rem;
}

/* --- Status overflow menu (Phase 3) ------------------------------------- */

.status-menu {
  position: relative;
  display: inline-flex;
  margin-left: auto; /* right-align inside .action-row */
}

/* Pin the dropdown to the trigger; the existing .nav-menu-panel rules
   carry the surface/shadow/anim — this just nudges the position so the
   panel hangs from below-right of the More button. */
.status-menu-panel {
  top: calc(100% + 0.4rem);
  right: 0;
}

.nav-menu-item--danger {
  color: var(--danger);
}

.nav-menu-item--danger .icon {
  color: var(--danger);
}

.nav-menu-item--danger:hover,
.nav-menu-item--danger:focus-visible {
  background: color-mix(in srgb, var(--danger) 12%, transparent);
  color: var(--danger);
}

/* --- Profile stat links (Phase 3) --------------------------------------- */

/* Stats line in the profile header — the followers / following counts now
   open the deck's accounts column. Underline on hover only (matches the
   other muted-link treatment); the surrounding muted color stays so the
   line still reads as a quiet meta row. */
.stat-link {
  color: inherit;
  text-decoration: none;
}

.stat-link:hover {
  color: var(--text);
  text-decoration: underline;
}

/* --- Account row (Phase 3) ---------------------------------------------- */

.account-list {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  padding: 0.75rem;
}

.account-row {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.75rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
}

.account-link {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  flex: 1;
  min-width: 0;
  color: var(--text);
  text-decoration: none;
}

.account-link:hover .display-name {
  color: var(--accent-text);
}

.account-identity {
  display: inline-flex;
  flex-direction: column;
  min-width: 0;
}

.account-identity .display-name {
  font-weight: 600;
  font-size: var(--text-sm);
  color: var(--text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.account-identity .handle {
  color: var(--text-muted);
  font-size: var(--text-xs);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.account-follow-btn {
  flex: none;
}

/* --- Add-column modal (Phase 4) ----------------------------------------- */

.add-column-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}

.add-column-item {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  width: 100%;
  padding: 0.65rem 0.75rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: transparent;
  color: var(--text);
  font-size: var(--text-sm);
  text-align: left;
  cursor: pointer;
  transition: background-color var(--duration-fast) var(--ease-out);
}

.add-column-item:hover {
  background: var(--surface-2);
}

.add-column-item[disabled] {
  opacity: 0.5;
  cursor: not-allowed;
}

.add-column-item[aria-pressed="true"] {
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  border-color: color-mix(in srgb, var(--accent) 30%, var(--border));
}

.add-column-item .icon {
  width: 1.1rem;
  height: 1.1rem;
  flex: none;
  color: var(--text-muted);
}

.add-column-label {
  flex: 1;
  font-weight: 500;
}

.add-column-hint {
  color: var(--text-subtle);
  font-size: var(--text-xs);
}

.add-column-param {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  margin-top: 0.75rem;
  padding-top: 0.75rem;
  border-top: 1px solid var(--border);
}

.add-column-param input[type="text"] {
  margin-top: 0.25rem;
}

/* --- Search column (Phase 4) -------------------------------------------- */

.search-form {
  display: flex;
  gap: 0.5rem;
  padding: 0.75rem;
  border-bottom: 1px solid var(--border);
}

.search-input {
  flex: 1;
  min-width: 0;
}

.search-results {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  padding: 0.75rem 0;
}

.search-section {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.search-section-heading {
  margin: 0 0.75rem 0.25rem;
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

.hashtag-list {
  list-style: none;
  margin: 0;
  padding: 0 0.75rem;
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}

.hashtag-link {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem 0.75rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  color: var(--text);
  text-decoration: none;
  font-size: var(--text-sm);
  font-weight: 500;
}

.hashtag-link:hover {
  background: var(--surface-2);
  color: var(--accent-text);
}

.hashtag-link .icon {
  width: 1rem;
  height: 1rem;
  color: var(--text-muted);
}

/* --- Compose media + poll (Phase 5) ------------------------------------- */

/* Attached-image preview. Each pending file renders as a tile with
   overlay buttons (Mastodon parity): X in the top-left to remove,
   ALT chip in the bottom-left to toggle the inline editor. The
   editor spans the full grid row when open. */
.compose-media-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: 0.4rem;
  margin: 0.5rem 0 0.75rem;
}

.compose-media-tile {
  position: relative;
  aspect-ratio: 16 / 10;
  background: var(--surface-2);
  border-radius: var(--radius-md);
  overflow: hidden;
}

.compose-media-tile-img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transition: filter 200ms ease;
}

/* Mirrors the read-side `.media-grid--blurred` affordance: when a
   CW is set, the compose preview blurs attached images so the
   owner sees the receive-side appearance. Hover / focus lifts the
   blur so they can still confirm what they attached. */
.compose-media-grid--blurred .compose-media-tile-img {
  filter: blur(24px);
}

.compose-media-grid--blurred .compose-media-tile:hover .compose-media-tile-img,
.compose-media-grid--blurred .compose-media-tile:focus-within .compose-media-tile-img {
  filter: blur(0);
}

/* Shared overlay-button base. Sits on top of the image with an
   opaque dark wash so it reads against any photo. The base button
   rule at the top of the file sets padding + min-height + transform
   on every <button>; the resets below force the overlay buttons
   back to their compact sizing instead of inheriting the 44px
   tap target. */
.compose-media-tile-remove,
.compose-media-tile-alt {
  position: absolute;
  padding: 0;
  min-height: 0;
  border: none;
  background: rgba(0, 0, 0, 0.78);
  color: #fff;
  font-weight: 600;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  border-radius: var(--radius-full);
  transition: background var(--duration-fast) var(--ease-out);
  /* Override the global button hover/active effects — the accent
     glow + translateY don't belong on an overlay affordance. */
  box-shadow: none;
}

.compose-media-tile-remove:hover,
.compose-media-tile-alt:hover,
.compose-media-tile-remove:focus-visible,
.compose-media-tile-alt:focus-visible {
  background: rgba(0, 0, 0, 0.92);
  box-shadow: none;
}

.compose-media-tile-remove:active,
.compose-media-tile-alt:active {
  transform: none;
  box-shadow: none;
}

.compose-media-tile-remove {
  top: 0.4rem;
  left: 0.4rem;
  width: 28px;
  height: 28px;
  justify-content: center;
}

.compose-media-tile-remove .icon {
  width: 16px;
  height: 16px;
  stroke-width: 2.5;
}

.compose-media-tile-alt {
  bottom: 0.4rem;
  left: 0.4rem;
  height: 24px;
  padding: 0 0.55rem;
  font-size: 0.72rem;
  letter-spacing: 0.04em;
}

/* The empty-alt chip adopts the established warning-banner pattern
   so it works in both color modes: --warning-bg + --warning-fg
   swap meanings per mode (yellow fill in light, dark+yellow text
   in dark) and the border anchors the read in both. */
.compose-media-tile-alt--empty {
  background: var(--warning-bg);
  color: var(--warning-fg);
  border: 1px solid color-mix(in srgb, var(--warning-fg) 60%, transparent);
}

.compose-media-tile-alt--empty:hover,
.compose-media-tile-alt--empty:focus-visible {
  background: color-mix(in srgb, var(--warning-bg) 70%, var(--warning-fg) 16%);
}

.compose-media-tile-alt .icon {
  width: 12px;
  height: 12px;
}

/* Inline editor — spans the full grid row so the textarea has
   room to breathe in a 360px column. */
.compose-media-alt-editor {
  grid-column: 1 / -1;
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  padding: 0.5rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
}

.compose-media-alt-editor textarea {
  width: 100%;
  min-height: 60px;
  resize: vertical;
  font-family: inherit;
  font-size: var(--text-sm);
}

.compose-media-alt-editor-actions {
  display: flex;
  justify-content: flex-end;
  gap: 0.4rem;
}

.compose-poll {
  margin: 0 0 0.75rem;
  padding: 0.75rem;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
}

.compose-poll-options {
  list-style: none;
  margin: 0 0 0.75rem;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.compose-poll-option {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.compose-poll-option label {
  flex: 1;
  min-width: 0;
}

.compose-poll-option input[type="text"] {
  width: 100%;
}

.compose-poll-controls {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.75rem;
}

.compose-poll-expires,
.compose-poll-multiple {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  font-size: var(--text-sm);
  color: var(--text-muted);
}

/* --- Poll display (Phase 5, inside StatusCard) -------------------------- */

.poll {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  margin-top: 0.5rem;
  padding: 0.75rem;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
}

.poll-options {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}

/* Result-mode rows show a horizontal fill behind the title; the fill width
   is driven by the inline `--poll-fill` set by the renderer. */
.poll-option {
  position: relative;
  padding: 0.5rem 0.75rem;
  border-radius: var(--radius-sm);
  font-size: var(--text-sm);
}

.poll-option--result {
  background: var(--surface);
  border: 1px solid var(--border);
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
}

.poll-option--result::before {
  content: "";
  position: absolute;
  inset: 0 auto 0 0;
  width: var(--poll-fill, 0%);
  background: color-mix(in srgb, var(--accent) 18%, transparent);
  pointer-events: none;
}

.poll-option--own .poll-option-title::before {
  content: "✓ ";
  color: var(--accent-text);
}

.poll-option-title,
.poll-option-percent {
  position: relative;
}

.poll-option-percent {
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

/* Pre-vote rows use real radio / checkbox inputs; the label wraps the
   whole row so click anywhere selects. */
.poll-option--choice {
  padding: 0;
}

.poll-option-label {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem 0.75rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  cursor: pointer;
  background: var(--surface);
}

.poll-option-label:hover {
  background: var(--surface-2);
}

.poll-footer {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  font-size: var(--text-xs);
  color: var(--text-muted);
}

.poll-meta {
  display: inline-flex;
  gap: 0.4rem;
  margin-left: auto;
}

.column-body {
  flex: 1;
  overflow-y: auto;
  padding: 1rem;
}

/* Existing pages wrap their content in `.page` (max-width 720, centered). Inside
   a deck column that cap/centering is unwanted — the column already bounds it. */
.column-body .page {
  max-width: none;
  padding: 0;
}

.compose-panel h2 {
  margin: 0 0 0.5rem;
  font-size: 1.05rem;
  font-weight: 600;
}

/* --- Conversations / DMs column (Phase 6) -------------------------------- */

.conversation-list {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  padding: 0.75rem;
}

.conversation-row {
  display: flex;
  align-items: stretch;
  gap: 0.5rem;
  padding: 0;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  overflow: hidden;
}

.conversation-row:hover {
  background: var(--surface-2);
}

.conversation-row--unread {
  border-color: color-mix(in srgb, var(--accent) 40%, var(--border));
  background: color-mix(in srgb, var(--accent) 6%, var(--surface));
}

/* Primary "open thread" surface inside the row — a real <button> so
   keyboard activation, focus rings, and screen-reader semantics come
   for free. Nested-interactive a11y is satisfied by keeping this and
   the .conversation-delete <button> as siblings rather than nested. */
.conversation-open {
  display: flex;
  flex: 1;
  align-items: flex-start;
  gap: 0.75rem;
  padding: 0.75rem;
  background: transparent;
  border: none;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
  min-width: 0;
}

.conversation-open:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: -2px;
}

.conversation-avatars {
  display: inline-flex;
  flex-direction: row;
  flex: none;
}

.conversation-avatars > * + * {
  margin-left: -0.6rem;
}

.conversation-body {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}

.conversation-header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.5rem;
  min-width: 0;
}

.conversation-handles {
  font-weight: 600;
  font-size: var(--text-sm);
  color: var(--text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
  flex: 1;
}

.conversation-time {
  color: var(--text-subtle);
  font-size: var(--text-xs);
  flex: none;
}

.conversation-preview {
  margin: 0;
  font-size: var(--text-sm);
  color: var(--text-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  display: block;
}

.conversation-preview--empty {
  color: var(--text-subtle);
  font-style: italic;
}

.conversation-delete {
  flex: none;
  background: transparent;
  border: none;
  color: var(--text-muted);
  padding: 0.25rem;
  border-radius: var(--radius-sm);
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

.conversation-delete:hover {
  background: var(--surface-raised);
  color: var(--danger);
}

.conversation-delete:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.conversation-delete .icon {
  width: 1rem;
  height: 1rem;
}

/* --- Lists column (Phase 6) --------------------------------------------- */

.lists-body {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  padding: 0.75rem;
}

.lists-create {
  display: flex;
  gap: 0.5rem;
  align-items: center;
  flex-wrap: wrap;
  padding: 0.5rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
}

.lists-create input[type="text"] {
  flex: 1;
  min-width: 6rem;
}

.lists-rows {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}

.list-row {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: 0.5rem 0.6rem;
}

.list-row-view,
.list-row-edit {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.list-row-edit input[type="text"] {
  flex: 1;
  min-width: 4rem;
}

.list-row-title {
  flex: 1;
  font-weight: 500;
  font-size: var(--text-sm);
  color: var(--text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.list-row-policy {
  font-size: var(--text-xs);
  color: var(--text-subtle);
  flex: none;
}

.list-row-control {
  flex: none;
  background: transparent;
  border: 1px solid transparent;
  color: var(--text-muted);
  padding: 0.3rem;
  border-radius: var(--radius-sm);
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

.list-row-control:hover {
  background: var(--surface-2);
  color: var(--text);
}

.list-row-control:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.list-row-control--danger:hover {
  color: var(--danger);
}

.list-row-control .icon {
  width: 1rem;
  height: 1rem;
}

/* --- Management columns shared (Phase 6) -------------------------------- */

/* The account-list scaffold renders the per-row Action buttons in a
   flex container; multiple buttons (follow_requests has Accept + Reject)
   wrap onto a second line on narrow columns instead of overflowing. */
.account-row-actions {
  display: inline-flex;
  flex: none;
  gap: 0.4rem;
  flex-wrap: wrap;
  justify-content: flex-end;
}

/* --- Push notifications settings (Phase 6) ----------------------------- */

.push-alerts {
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: 0.75rem 1rem;
  margin: 0.5rem 0 0;
}

.push-alerts legend {
  padding: 0 0.5rem;
  font-size: var(--text-xs);
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

.push-alert-row {
  display: block;
  margin: 0.25rem 0;
  font-size: var(--text-sm);
  color: var(--text);
}

/* Mobile (≤720px): one column at a time, rail reflows to a top bar.
   The breakpoint is the sum of .deck-rail (360px) + one .deck-column
   (360px) — any narrower and the desktop two-up layout would clip
   horizontally, with a previously-visible 701–719px gap. */
@media (max-width: 720px) {
  .deck {
    flex-direction: column;
    height: 100vh;
  }

  .deck-rail {
    width: auto;
    flex-direction: row;
    align-items: center;
    gap: 0.75rem;
    padding: 0.5rem 0.75rem;
    border-right: none;
    border-bottom: 1px solid var(--border);
    overflow: visible;
  }

  .rail-head {
    flex: 1;
    min-width: 0;
  }

  .rail-nav {
    flex-direction: row;
  }

  /* Compose is a modal on mobile — hide the persistent panel, show the button. */
  .rail-compose {
    display: none;
  }

  .rail-compose-btn {
    display: inline-flex;
  }

  .rail-foot {
    display: none;
  }

  .deck-columns {
    flex: 1;
    height: auto;
    overflow: hidden;
  }

  .deck-column {
    width: 100%;
    border-right: none;
  }

  /* Show only the route-focused column. */
  .deck-column:not([data-focused]) {
    display: none;
  }

  /* On mobile the column is the full viewport and the profile-action
     buttons (Follow / Stop notifying / Edit profile) are the primary
     touch targets. Restore the .btn 44px minimum that the column-mode
     compaction at .deck-column .profile-actions .btn turned off. */
  .deck-column .profile-actions .btn {
    min-height: 44px;
    padding: 0.55rem 1.1rem;
    font-size: var(--text-base);
  }
}


/* solopub content-type: text/css; charset=utf-8 */
