/* FuriPage — the design system (T-302, ux-design.md §5–§9/§23/§29–§34).
 *
 * Light mode only (§5): the palette is declared once on `:root` and is never
 * re-keyed by the OS preference or a theme attribute — `color-scheme` stays
 * `light` so form controls never render a dark half the rest of the page has
 * no answer for.
 *
 * Every color in a component rule reads a token; hex literals live only in
 * the token block below and in the `data-accent` theme blocks. Each accent is
 * hand-checked for WCAG AA both ways — as link text on `--bg` (≥4.5:1) and as
 * a button fill under `--accent-ink` white (≥4.5:1); the numbers are in the
 * PR that introduced them.
 */

:root {
  /* Ground and surfaces — warm off-white, never pure white (§5). */
  --bg: #faf9f7;            /* page ground */
  --surface: #ffffff;       /* cards, inputs, menus */
  --surface-muted: #f1efe9; /* hovered cards, quiet wells */

  /* Text — charcoal, never pure black. `--ink-muted` clears AA on every
     surface above (6.9:1 on --surface-muted, the worst case it meets). */
  --ink: #1f1d1b;
  --ink-muted: #565049;

  /* Lines. `--line` separates things the layout already separates; it is a
     hairline and is allowed to be faint. `--control-line` is the border of a
     form control — the one border doing work — and clears WCAG 1.4.11's 3:1
     against --surface (4.05:1) and --bg (3.85:1). */
  --line: #e4e0d8;
  --control-line: #837e70;

  /* Status colors — all ≥4.5:1 as text on --bg and on their own soft tint. */
  --danger: #a92822;
  --danger-ink: #ffffff;
  --danger-hover: #8f211c;
  --danger-soft: #f7e7e5;
  --warning: #8a5a00;
  --warning-soft: #f7eedd;
  --success: #1b6e3f;
  --success-soft: #e6f2ea;
  --info: #0b6b60;
  --info-soft: #e6f2f0;

  /* Flash borders — the soft edge of each status box, one step stronger than
     the tint it sits on so the banner reads as a box without a heavy frame. */
  --danger-border: #e5b6b2;
  --warning-border: #e3c07a;
  --success-border: #9fd4b4;
  --info-border: #a9cfc9;

  /* Accent — soft violet by default (§5): gender-neutral, not corporate, and
     quiet enough to sit next to character artwork without competing (§6).
     5.48:1 on --bg as link text; 5.77:1 under white as a button fill. */
  --accent: #6558b8;
  --accent-hover: #54479b;
  --accent-ink: #ffffff;
  --accent-soft: #ece9f9; /* 4.83:1 under --accent text (`.tab-on`) */

  /* Type. Anuphan carries body and UI (§7); IBM Plex Sans Thai is the
     metadata/admin voice; IBM Plex Mono is for code and handles only. */
  --font-body: "Anuphan", "IBM Plex Sans Thai", system-ui, sans-serif;
  --font-meta: "IBM Plex Sans Thai", "Anuphan", system-ui, sans-serif;
  --font-mono: "IBM Plex Mono", ui-monospace, monospace;

  /* 4px spacing scale (§32): 4 8 12 16 24 32 48 64. */
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-5: 1.5rem;
  --space-6: 2rem;
  --space-7: 3rem;
  --space-8: 4rem;

  /* Radius (§31): controls 6–8, buttons 8–10, cards 12–16, artwork 10–14.
     Avatars are circles and use the pill value. */
  --radius-sm: 8px;
  --radius-md: 10px;
  --radius-lg: 16px;
  --radius-art: 12px;
  --radius-pill: 999px;

  /* Shadows: subtle only (§30) — floating panels, never cards. */
  --shadow: 0 1px 3px rgba(31, 29, 27, 0.1), 0 1px 2px rgba(31, 29, 27, 0.04);

  /* Content widths (§8): public 880, dashboard 1100, admin 1280. */
  --width-public: 880px;
  --width-console: 1100px;
  --width-admin: 1280px;

  /* Motion (§34): one duration, low and calm. */
  --motion: 150ms;

  color-scheme: light;
}

/* Accent themes (§23, T-203) — the profile-scoped customization. A theme
 * overrides exactly the accent tokens and nothing else; typography, layout
 * and surfaces are deliberately not themeable. Every value is subdued to sit
 * in the same family as the default violet and is hand-checked for AA in
 * both directions, same as the default above.
 */

:root[data-accent="lavender"] {
  --accent: #6f60b8; /* 4.94:1 on --bg · 5.20:1 under white */
  --accent-hover: #5b4d9c;
  --accent-soft: #f2effc; /* 4.58:1 under the accent */
}

:root[data-accent="indigo"] {
  --accent: #4758a4; /* 6.24:1 · 6.56:1 */
  --accent-hover: #3a498a;
  --accent-soft: #e9ecf5; /* 5.56:1 */
}

:root[data-accent="rose"] {
  --accent: #ab4767; /* 5.21:1 · 5.48:1 */
  --accent-hover: #8f3956;
  --accent-soft: #f7e8ed; /* 4.63:1 */
}

:root[data-accent="forest"] {
  --accent: #3a7549; /* 5.22:1 · 5.50:1 */
  --accent-hover: #2f5f3c;
  --accent-soft: #e9f1ea; /* 4.77:1 */
}

:root[data-accent="amber"] {
  --accent: #8f6318; /* 5.03:1 · 5.30:1 */
  --accent-hover: #755112;
  --accent-soft: #f5edde; /* 4.55:1 */
}

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

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

body {
  margin: 0;
  /* Mobile edge padding 16px, tablet 24px (§36). The header and footer share
     this padding so the page reads as one column with furniture at each end. */
  padding-inline: 1rem;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: 1rem; /* body 15–16px (§7) */
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}

@media (min-width: 48rem) {
  body {
    padding-inline: 1.5rem;
  }
}

/* Content containers (§8). `.wrap` is the public measure; `/me` and `/report`
 * add `wrap-console` and `/admin` adds `wrap-admin` (T-305 adopts them). */
.wrap {
  max-width: var(--width-public);
  margin-inline: auto;
  padding-block: var(--space-7) var(--space-8);
}

.wrap-console {
  max-width: var(--width-console);
}

.wrap-admin {
  max-width: var(--width-admin);
}

@media (max-width: 30rem) {
  .wrap {
    padding-block: var(--space-6) var(--space-6);
  }
}

/* --------------------------------------------------------------------------
 * The skip link — off-canvas until focused, first thing on screen for tab
 * users (§35).
 * ---------------------------------------------------------------------- */

.skip {
  position: absolute;
  inset-inline-start: -100vw;
  inset-block-start: 0;
  background: var(--accent);
  color: var(--accent-ink);
  padding: 0.7rem 1.4rem;
  border-end-end-radius: var(--radius-sm);
  z-index: 10;
}

.skip:focus {
  inset-inline-start: 0;
}

/* --------------------------------------------------------------------------
 * Site header (§9): brand, a couple of nav links, the language switcher and
 * either the sign-in link or the avatar menu. Every control clears the 44px
 * touch target; the menu is a <details>, so it opens with no script at all.
 * ---------------------------------------------------------------------- */

.site {
  max-width: var(--width-public);
  margin-inline: auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  padding-block: var(--space-1);
}

.site-brand {
  font-family: var(--font-body);
  font-size: 1.0625rem;
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--ink);
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: 44px;
  transition: color var(--motion) ease;
}

.site-brand::before {
  content: "";
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  background: var(--accent);
  flex: none;
}

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

@media (max-width: 24rem) {
  /* The tightest phones: the wordmark is allowed to shrink (§9 mobile) while
     every control around it keeps its target. */
  .site-brand {
    font-size: 0.9375rem;
  }
}

.site-nav {
  display: flex;
  align-items: center;
  gap: var(--space-1);
}

/* At phone widths the header row can add up to more than 360px of minimum
   content (§36): the nav is allowed to flow onto a second line rather than
   push the page into horizontal scroll. Every control keeps its 44px target;
   only the row they sit in gives way. */
@media (max-width: 30rem) {
  .site-nav {
    flex-wrap: wrap;
    justify-content: flex-end;
    row-gap: 0;
  }
}

.site-nav a {
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  padding-inline: var(--space-2);
  color: var(--ink);
  text-decoration: none;
  transition: color var(--motion) ease;
}

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

/* A compact variant of `.btn` for the header row — the header's sign-in is a
   secondary, never a second primary competing with the page's own CTA (§29). */
.site-nav .btn {
  padding: 0.5rem 1rem;
}

.lang-switch {
  font-weight: 500;
}

.user-menu {
  position: relative;
}

.user-trigger {
  list-style: none;
  cursor: pointer;
  min-height: 44px;
  min-width: 44px;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  border-radius: var(--radius-pill);
}

.user-trigger::-webkit-details-marker {
  display: none;
}

.nav-avatar {
  width: 2rem;
  height: 2rem;
  border-radius: 50%;
  object-fit: cover;
  background: var(--surface-muted);
  border: 1px solid var(--line);
}

.nav-initial {
  width: 2rem;
  height: 2rem;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--accent-soft);
  color: var(--accent);
  font-weight: 500;
}

.user-panel {
  position: absolute;
  inset-inline-end: 0;
  top: calc(100% + 0.25rem);
  min-width: 11rem;
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow);
  padding: var(--space-1);
  z-index: 20;
}

.user-panel a {
  color: var(--ink);
  text-decoration: none;
  padding: 0.6rem 0.75rem;
  border-radius: var(--radius-sm);
  min-height: 44px;
  display: flex;
  align-items: center;
  white-space: nowrap;
  transition: background-color var(--motion) ease;
}

.user-panel a:hover {
  background: var(--surface-muted);
}

/* --------------------------------------------------------------------------
 * Typography (§7)
 * ---------------------------------------------------------------------- */

h1,
h2,
h3 {
  font-family: var(--font-body);
  line-height: 1.3;
  text-wrap: balance;
  margin: 0;
  overflow-wrap: anywhere;
}

h1 {
  /* Page heading 26–32px (§7). */
  font-size: clamp(1.625rem, 1.3rem + 1.5vw, 2rem);
  font-weight: 600;
  letter-spacing: -0.01em;
}

h2 {
  font-size: 1.25rem;
  font-weight: 600;
}

h3 {
  font-size: 1.0625rem;
  font-weight: 600;
}

/* Profile display names, 28–36px (§7) — for the public identity header. */
.profile-name {
  font-size: clamp(1.75rem, 1.4rem + 2vw, 2.25rem);
  font-weight: 600;
  letter-spacing: -0.01em;
  line-height: 1.25;
  overflow-wrap: anywhere;
  min-width: 0;
}

/* A page hero title, at most 40px (§7). */
.hero-title {
  font-size: clamp(1.75rem, 1.4rem + 2.5vw, 2.5rem);
  font-weight: 600;
  letter-spacing: -0.01em;
  line-height: 1.2;
  overflow-wrap: anywhere;
}

p {
  margin: 0;
  overflow-wrap: anywhere;
}

a {
  color: var(--accent);
  transition: color var(--motion) ease;
}

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

.lede {
  font-size: 1.0625rem;
  color: var(--ink-muted);
  margin-block-start: var(--space-4);
}

.note {
  font-size: 0.875rem;
  color: var(--ink-muted);
}

.muted {
  color: var(--ink-muted);
  font-size: 0.875rem;
}

.strong {
  font-weight: 500;
}

.mono {
  font-family: var(--font-mono);
  font-size: 0.875em;
}

.mono.big {
  font-size: 1.25rem;
}

/* The small "FuriPage" eyebrow at the top of a page body — metadata, so
   it takes the metadata voice (§7). */
.brand {
  font-family: var(--font-meta);
  font-size: 0.75rem;
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-muted);
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.brand::before {
  content: "";
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  background: var(--accent);
}

dl.facts {
  font-family: var(--font-meta);
  font-size: 0.8125rem;
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: 0.4rem 1.25rem;
  margin: 0;
}

dl.facts dt {
  color: var(--ink-muted);
}

dl.facts dd {
  margin: 0;
  overflow-wrap: anywhere;
  min-width: 0;
}

/* Long names (§7): Thai, Latin, CJK and emoji must wrap (or truncate) without
   breaking the layout. Flex children that carry text get out of the way with
   `min-width: 0`; the wrap utilities are the escape hatch for future views. */

.profile-head .stack,
.card-head > * {
  min-width: 0;
}

.truncate {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}

/* --------------------------------------------------------------------------
 * Stacks — vertical rhythm (§32). Sections separate by 32–48px.
 * ---------------------------------------------------------------------- */

.stack {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.stack-lg {
  display: flex;
  flex-direction: column;
  gap: var(--space-6); /* 32px; used on `<main>` as the whole class list */
}

/* --------------------------------------------------------------------------
 * Buttons (§29) — four levels: primary, secondary, ghost, destructive.
 * One primary per section; destructive styling only on confirm surfaces.
 * ---------------------------------------------------------------------- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  font: inherit;
  font-weight: 500;
  min-height: 44px;
  padding: 0.55rem 1.25rem;
  border-radius: var(--radius-md);
  border: 1px solid transparent;
  background: var(--accent);
  color: var(--accent-ink);
  text-decoration: none;
  cursor: pointer;
  transition: background-color var(--motion) ease, border-color var(--motion) ease,
    color var(--motion) ease;
}

.btn:hover {
  background: var(--accent-hover);
}

/* Secondary — the quiet outline next to a primary. `.btn-quiet` is the name
   the existing markup speaks; `.btn-secondary` is the canonical one (§29). */
.btn-secondary,
.btn-quiet {
  background: var(--surface);
  border-color: var(--control-line);
  color: var(--ink);
}

.btn-secondary:hover,
.btn-quiet:hover {
  background: var(--surface-muted);
  color: var(--ink);
}

/* Ghost — no border, no fill; the lightest action on the page. */
.btn-ghost {
  background: transparent;
  color: var(--ink);
}

.btn-ghost:hover {
  background: var(--surface-muted);
  color: var(--ink);
}

/* Destructive — red only where the action is already a confirmed
   destructive one (the dialog, the danger zone), never as a standing
   warning color on the page (§24). */
.btn-danger,
.btn.danger {
  background: var(--danger);
  color: var(--danger-ink);
}

.btn-danger:hover,
.btn.danger:hover {
  background: var(--danger-hover);
}

/* The in-text button: submits and small actions living inside copy. The
   negative inline margin keeps the text on the grid while the 44px target
   stands proud of the line box (§35). */
.btn-link {
  font: inherit;
  font-size: inherit;
  display: inline-flex;
  align-items: center;
  padding: 0 var(--space-1);
  margin-inline: calc(-1 * var(--space-1));
  min-height: 44px;
  border: 0;
  background: none;
  color: var(--accent);
  cursor: pointer;
  text-decoration: underline;
}

.btn-link:hover {
  color: var(--accent-hover);
}

.btn-link.danger {
  color: var(--danger);
}

.btn-link.danger:hover {
  color: var(--danger-hover);
}

/* Reorder arrows: a fixed square so up and down align in a column of cards. */
.btn-link.move {
  justify-content: center;
  min-width: 44px;
  min-height: 44px;
  text-decoration: none;
  line-height: 1;
}

.btn-link.move:hover {
  text-decoration: none;
  background: var(--surface-muted);
  border-radius: var(--radius-sm);
}

.actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
  margin-block-start: var(--space-6);
}

/* A form that is only a button row (admin actions, reorder posts). */
form.inline {
  display: inline;
}

/* --------------------------------------------------------------------------
 * Cards (§30) — a thin border, a 12–16px radius, a neutral surface, and a
 * hover that tints the background instead of throwing a shadow.
 * ---------------------------------------------------------------------- */

.card {
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--surface);
  padding: 1.1rem 1.25rem;
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  transition: background-color var(--motion) ease;
}

.card:hover {
  background: var(--surface-muted);
}

.card-head {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: var(--space-4);
  flex-wrap: wrap;
}

.card-head p {
  margin: 0;
}

/* The character grid (§21/§36, issue #49): one column on phones, two on
   tablets, three where the console measure can afford them. `minmax(0,1fr)`
   everywhere so a long name truncates inside its card instead of stretching
   the track past the viewport. */
.card-grid {
  display: grid;
  gap: var(--space-4);
  grid-template-columns: minmax(0, 1fr);
}

@media (min-width: 40rem) {
  .card-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (min-width: 64rem) {
  .card-grid {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}

/* Text moved out of sight, not out of reach (§35): a table caption or a
   control's extra wording that would only crowd the eye. */
.vh {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  border: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
}

/* --------------------------------------------------------------------------
 * Badges and status (§35) — never color alone. A badge is a neutral pill of
 * text; a status carries a dot, so shape and text carry the meaning even
 * where the hue cannot.
 * ---------------------------------------------------------------------- */

.badges {
  display: flex;
  gap: 0.4rem;
  flex-wrap: wrap;
}

.badge {
  font-size: 0.75rem;
  font-weight: 500;
  padding: 0.15rem 0.6rem;
  border-radius: var(--radius-pill);
  border: 1px solid var(--line);
  color: var(--ink-muted);
  background: var(--surface);
  white-space: nowrap;
}

.status {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-size: 0.8125rem;
  font-weight: 500;
}

.status::before {
  content: "";
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  background: currentColor;
  flex: none;
}

.status-success {
  color: var(--success);
}

.status-warning {
  color: var(--warning);
}

.status-danger {
  color: var(--danger);
}

.status-muted {
  color: var(--ink-muted);
}

/* The storage quota tiers on `/me` — text-level warnings that arrive with
   their own words, so the hue is only ever the second signal. */
.quota-warning {
  color: var(--warning);
  font-weight: 500;
}

.quota-critical {
  color: var(--danger);
  font-weight: 500;
}

/* The quota bar (§42) — a track with a fill; the markup pairs it with the
   textual percentage, which stays the primary signal.
 *
 * The fill's width cannot arrive as an inline `style=` (the CSP has no
   `unsafe-inline`), so the view rounds the percentage to the nearest step of
   five and says it as `data-fill` — one attribute rule per step below, 0–100.
   The step is fine because the bar is the second signal: the sentence above
   it carries the exact number. */
.quotabar {
  display: block;
  height: 0.5rem;
  border-radius: var(--radius-pill);
  background: var(--surface-muted);
  border: 1px solid var(--line);
  overflow: hidden;
}

.quotabar-fill {
  display: block;
  height: 100%;
  border-radius: inherit;
  background: var(--accent);
}

.quotabar-fill[data-fill="0"] { width: 0; }
.quotabar-fill[data-fill="5"] { width: 5%; }
.quotabar-fill[data-fill="10"] { width: 10%; }
.quotabar-fill[data-fill="15"] { width: 15%; }
.quotabar-fill[data-fill="20"] { width: 20%; }
.quotabar-fill[data-fill="25"] { width: 25%; }
.quotabar-fill[data-fill="30"] { width: 30%; }
.quotabar-fill[data-fill="35"] { width: 35%; }
.quotabar-fill[data-fill="40"] { width: 40%; }
.quotabar-fill[data-fill="45"] { width: 45%; }
.quotabar-fill[data-fill="50"] { width: 50%; }
.quotabar-fill[data-fill="55"] { width: 55%; }
.quotabar-fill[data-fill="60"] { width: 60%; }
.quotabar-fill[data-fill="65"] { width: 65%; }
.quotabar-fill[data-fill="70"] { width: 70%; }
.quotabar-fill[data-fill="75"] { width: 75%; }
.quotabar-fill[data-fill="80"] { width: 80%; }
.quotabar-fill[data-fill="85"] { width: 85%; }
.quotabar-fill[data-fill="90"] { width: 90%; }
.quotabar-fill[data-fill="95"] { width: 95%; }
.quotabar-fill[data-fill="100"] { width: 100%; }

.quotabar.warning .quotabar-fill {
  background: var(--warning);
}

.quotabar.critical .quotabar-fill {
  background: var(--danger);
}

/* --------------------------------------------------------------------------
 * Forms (§42) — plain stacked `<label class="field">` blocks, working with
   no JavaScript at all.
 * ---------------------------------------------------------------------- */

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

.field-label {
  font-size: 0.875rem;
  font-weight: 500;
  color: var(--ink);
}

.field-hint {
  font-size: 0.8125rem;
  color: var(--ink-muted);
}

/* The error message under a field. `.error` is the name the legal fallback
   speaks; `.field-error` is the canonical one. */
.field-error,
.error {
  font-size: 0.8125rem;
  font-weight: 500;
  color: var(--danger);
}

input,
textarea,
select {
  font: inherit;
  color: inherit;
  background: var(--surface);
  /* Not `--line` — a control's border is the one border doing work, and
     WCAG 1.4.11 asks 3:1 of exactly this edge. */
  border: 1px solid var(--control-line);
  border-radius: var(--radius-sm);
  padding: 0.55rem 0.75rem;
  min-height: 44px;
  max-width: 100%;
  transition: border-color var(--motion) ease;
}

input:focus-visible,
textarea:focus-visible,
select:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 0;
}

input[type="checkbox"],
input[type="radio"] {
  min-height: 0;
  width: auto;
  accent-color: var(--accent);
}

form.stack {
  gap: var(--space-4);
}

form.stack .actions {
  margin-block-start: 0;
}

.check {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: 0.875rem;
  color: var(--ink-muted);
  min-height: 44px;
}

.check input {
  width: auto;
}

/* --------------------------------------------------------------------------
 * Flash banners (§43) — the outcome of a POST→redirect→GET. One box per
 * level, tinted softly, dismissible when the optional layer is present.
 * ---------------------------------------------------------------------- */

.flash {
  position: relative;
  border: 1px solid;
  border-radius: var(--radius-md);
  padding: 0.65rem 1rem;
  font-size: 0.9375rem;
  overflow-wrap: anywhere;
}

.flash-success {
  color: var(--success);
  border-color: var(--success-border);
  background: var(--success-soft);
}

.flash-info {
  color: var(--info);
  border-color: var(--info-border);
  background: var(--info-soft);
}

.flash-warning {
  color: var(--warning);
  border-color: var(--warning-border);
  background: var(--warning-soft);
}

.flash-error {
  color: var(--danger);
  border-color: var(--danger-border);
  background: var(--danger-soft);
}

/* Added by /app.js when it runs; without it the banner simply stays. */
.flash-dismiss {
  position: absolute;
  inset-block-start: 0.25rem;
  inset-inline-end: 0.25rem;
  width: 44px;
  height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 1.25rem;
  line-height: 1;
  color: inherit;
  background: transparent;
  border: 0;
  border-radius: var(--radius-sm);
  cursor: pointer;
}

.flash-dismiss:hover {
  background: var(--surface);
}

/* --------------------------------------------------------------------------
 * Empty states (§22) — a small icon, typography and a way out; no big
 * illustrations on every page.
 * ---------------------------------------------------------------------- */

.empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: var(--space-3);
  padding: var(--space-7) var(--space-4);
  color: var(--ink-muted);
}

.empty-icon {
  color: var(--ink-muted);
}

.empty .btn {
  margin-block-start: var(--space-2);
}

/* --------------------------------------------------------------------------
 * Breadcrumbs and tabs.
 * ---------------------------------------------------------------------- */

.breadcrumb {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.25rem 0.5rem;
  font-size: 0.8125rem;
  color: var(--ink-muted);
  list-style: none;
  margin: 0;
  padding: 0;
}

.breadcrumb li + li::before {
  content: "·";
  margin-inline-end: 0.5rem;
  color: var(--ink-muted);
}

/* The profile switcher. The selected tab is marked three ways — a filled
   background, an accent border, and a heavier name — so the choice is never
   carried by hue alone; `aria-current` says the same to assistive tech. */
.tabs {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.tab {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  min-height: 44px;
  justify-content: center;
  padding: 0.5rem 0.9rem;
  border: 1px solid var(--control-line);
  border-radius: var(--radius-md);
  background: transparent;
  color: var(--ink);
  text-decoration: none;
  line-height: 1.4;
  transition: background-color var(--motion) ease, border-color var(--motion) ease;
}

.tab:hover {
  background: var(--surface-muted);
}

.tab-on,
.tab-on:hover {
  border-color: var(--accent);
  background: var(--accent-soft);
  color: var(--accent);
  font-weight: 500;
}

.tab .mono {
  font-size: 0.75rem;
}

/* --------------------------------------------------------------------------
 * Media — avatars, artwork, upload previews. Boxes are sized here so a photo
 * cannot push the layout around it.
 * ---------------------------------------------------------------------- */

.avatar,
.media-thumb,
.thumb-inline {
  object-fit: cover;
  background: var(--surface-muted);
  border: 1px solid var(--line);
  flex: none;
}

/* Avatars are circles (§31). */
.avatar {
  width: 4rem;
  height: 4rem;
  border-radius: 50%;
}

/* The placeholder every avatar-less slot renders (#57): the caller's own
 * avatar class sizes and circles the box exactly as it sized the picture it
 * stands in for; this only centres the initial on the muted surface the
 * picture would have covered. The same tokens the nav's own initial uses,
 * so "no picture" reads as one shape everywhere it can happen. */
.avatar-fallback {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--surface-muted);
  color: var(--ink-muted);
  font-weight: 500;
  user-select: none;
}

.avatar-lg {
  width: 6rem;
  height: 6rem;
  border-radius: 50%;
}

.avatar-xl {
  width: 8rem;
  height: 8rem;
  border-radius: 50%;
}

@media (max-width: 30rem) {
  .avatar-xl {
    width: 6rem;
    height: 6rem;
  }
}

/* Artwork takes the 10–14px radius (§31). */
.media-thumb {
  width: 6rem;
  height: 6rem;
  border-radius: var(--radius-art);
}

.thumb-inline {
  width: 2rem;
  height: 2rem;
  border-radius: var(--radius-sm);
  vertical-align: middle;
}

.media-current {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
}

.media-slot {
  display: inline-block;
  max-width: 100%;
  position: relative;
}

.reference-img {
  width: 100%;
  height: auto;
  border-radius: var(--radius-art);
  border: 1px solid var(--line);
}

/* The banner (§12): the profile container's full width, cropped to ~10:3.
   The crop — not the upload — decides the box, so a portrait photo becomes a
   banner instead of a wall; `overflow: hidden` keeps the cover-cropped image
   and the report link's absolute corner inside the rounded edge. */
.banner {
  aspect-ratio: 10 / 3;
  overflow: hidden;
  border: 1px solid var(--line);
  border-radius: var(--radius-art);
  position: relative;
  /* Pull the identity header up so its avatar rides onto the picture (§12):
   * half of the 6rem avatar (--space-7) of overlap, plus the 32px the parent
   * column's row gap (--space-6) would otherwise put back. The negative
   * margin sits on the banner's own bottom edge, so it only ever shifts what
   * follows — nothing escapes the flow. */
  margin-block-end: calc(-1 * (var(--space-7) + var(--space-6)));
}

.banner-img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Only the avatar overlaps — the words stay where they would have sat without
   it: the padding hands back exactly what the banner's negative margin took
   (overlap + row gap + the 16px the banner used to keep). Scoped to a header
   that directly follows a banner, so banner-less profiles, the character page
   and every other `.profile-head` keep their old geometry untouched. At phone
   widths the header wraps below the avatar and the padding simply becomes the
   gap between the two. */
.banner + .profile-head .stack {
  padding-block-start: calc(var(--space-7) + var(--space-6) + var(--space-4));
}

.reference {
  position: relative;
}

/* The report affordance — small and quiet; reporting is a real path, but it
   is not what the page is for (§24). */
.report-link {
  font-size: 0.75rem;
  color: var(--ink-muted);
}

.media-slot .report-link,
.banner .report-link,
.reference .report-link {
  position: absolute;
  inset-block-start: 0.4rem;
  inset-inline-end: 0.4rem;
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  padding-inline: 0.6rem;
  border-radius: var(--radius-sm);
  background: var(--surface);
  border: 1px solid var(--line);
  text-decoration: none;
}

/* --------------------------------------------------------------------------
 * Profile furniture (kept neutral here; T-304 owns the presentation).
 * ---------------------------------------------------------------------- */

.profile-head {
  display: flex;
  gap: 1.25rem;
  align-items: flex-start;
  flex-wrap: wrap;
}

.profile-head .stack {
  gap: 0.35rem;
}

.character-hero {
  display: flex;
  gap: 1.25rem;
  align-items: flex-start;
  flex-wrap: wrap;
  min-width: 0;
}

.item-actions {
  display: flex;
  gap: var(--space-4);
  flex-wrap: wrap;
  font-size: 0.875rem;
  align-items: center;
}

/* A plain link in an action row is still an action (§35): it gets the same
   44px target the buttons beside it already promise. */
.item-actions a {
  min-height: 44px;
  display: inline-flex;
  align-items: center;
}

.item-actions form {
  display: inline;
}

/* --------------------------------------------------------------------------
 * Lists and tables.
 * ---------------------------------------------------------------------- */

ul.plain {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  font-size: 0.9375rem;
}

ul.plain li {
  display: flex;
  gap: var(--space-3);
  flex-wrap: wrap;
  align-items: baseline;
}

table.table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.875rem;
  font-family: var(--font-meta); /* the admin voice (§7) */
}

table.table th,
table.table td {
  text-align: left;
  padding: 0.5rem 0.6rem;
  border-block-end: 1px solid var(--line);
}

table.table th {
  color: var(--ink-muted);
  font-weight: 500;
}

/* --------------------------------------------------------------------------
 * The danger zone (§19) — collapsed behind its own summary until the reader
 * asks for it; the destructive button waits inside.
 * ---------------------------------------------------------------------- */

.danger-zone {
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--surface);
  padding: 0.25rem 1.25rem;
}

.danger-zone summary {
  cursor: pointer;
  font-weight: 500;
  min-height: 44px;
  display: flex;
  align-items: center;
}

.danger-zone[open] {
  padding-block: var(--space-4);
}

.danger-zone[open] summary {
  margin-block-end: var(--space-4);
}

/* The account card's folded-away deletion details (#59): the same
 * collapsed-behind-a-summary shape the danger zone uses, quieter still —
 * nothing about it is destructive from this site, so the summary is a plain
 * quiet link rather than a warning. The UWU ID link above it stays visible
 * and un-promoted. */
.account-details {
  margin-block-start: var(--space-3);
}

.account-details summary {
  cursor: pointer;
  color: var(--ink-muted);
  min-height: 44px;
  display: flex;
  align-items: center;
}

.account-details[open] summary {
  margin-block-end: var(--space-2);
}

/* Upcoming work, stated as work — not dressed up as features that exist. */
.next {
  border-block-start: 1px solid var(--line);
  padding-block-start: var(--space-5);
  margin-block-start: var(--space-6);
}

.next ul {
  margin: 0.75rem 0 0;
  padding-inline-start: 1.1rem;
  color: var(--ink-muted);
}

.next li + li {
  margin-block-start: 0.35rem;
}

/* --------------------------------------------------------------------------
 * Site footer — the same measure as the header, the legal links and the
 * language switcher, every link a 44px target.
 * ---------------------------------------------------------------------- */

.foot {
  max-width: var(--width-public);
  margin-inline: auto;
  padding-block: var(--space-3) var(--space-8);
  border-block-start: 1px solid var(--line);
  font-size: 0.875rem;
}

.foot nav {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) 1.25rem;
}

.foot a {
  color: var(--ink-muted);
  text-decoration: none;
  min-height: 44px;
  display: inline-flex;
  align-items: center;
}

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

/* --------------------------------------------------------------------------
 * Focus (§35) — one visible ring, on everything interactive, keyboard-only.
 * ---------------------------------------------------------------------- */

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* --------------------------------------------------------------------------
 * Motion (§34) — low, 120–200ms, and nothing at all for readers who asked
 * the system for less.
 * ---------------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    transition-duration: 0.01ms !important;
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    scroll-behavior: auto !important;
  }
}
