/*
 * Components: buttons, cards, RSVP controls, forms, admin bits.
 * All tap targets >= 44px. No hover-only affordances.
 */

/* ---- Buttons ---- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  min-height: 48px;
  padding: 0.7rem 1.2rem;
  border: 1px solid transparent;
  border-radius: var(--radius-md);
  font: inherit;
  font-weight: 600;
  line-height: 1;
  text-decoration: none;
  cursor: pointer;
  background: var(--color-surface);
  color: var(--color-text);
  border-color: var(--color-border);
  -webkit-tap-highlight-color: transparent;
}

.btn:active {
  transform: translateY(1px);
}

.btn-primary {
  background: var(--color-accent);
  color: var(--color-accent-contrast);
  border-color: transparent;
}

.btn-block {
  width: 100%;
}

.btn-lg {
  min-height: 56px;
  font-size: 1.1rem;
}

.btn-sm {
  min-height: 36px;
  padding: 0.35rem 0.7rem;
  font-size: 0.85rem;
}

/* A selected filter chip, wherever it sits. Scoping this to .roster-tools was
   what made a chip outside that wrapper render as though nothing was applied. */
.btn.is-selected {
  background: var(--color-accent);
  color: var(--color-accent-contrast);
  border-color: transparent;
}

/* Tap-to-text link on the admin guest list. */
.phone-link {
  color: var(--color-accent);
  text-decoration: none;
  white-space: nowrap;
}

.btn:disabled,
.btn[aria-disabled="true"] {
  opacity: 0.5;
  pointer-events: none;
}

/* ---- Cards (stacked, no tables in guest views) ---- */
.card {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-card);
  padding: 1.25rem;
}

.card + .card {
  margin-top: 1rem;
}

.pill {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.2rem 0.7rem;
  border-radius: 999px;
  font-size: 0.8rem;
  font-weight: 600;
  white-space: nowrap;
  border: 1px solid var(--color-border);
  background: var(--color-bg);
}
.pill-yes {
  color: var(--color-success);
  border-color: color-mix(in srgb, var(--color-success) 45%, var(--color-border));
}
.pill-no {
  color: var(--color-danger);
  border-color: color-mix(in srgb, var(--color-danger) 45%, var(--color-border));
}
.pill-maybe {
  color: var(--color-text-muted);
}

/* ---- RSVP three-way control: big, thumb-reachable buttons ----
   Yes takes the full width on its own row. Three equal columns cramped every
   label at 375px, and the three answers were never equally likely. */
.rsvp-choices {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 0.6rem;
}

.rsvp-choices button {
  min-height: 56px;
  font-size: 1rem;
  width: 100%; /* button_to wraps each in a form; fill the grid cell */
}
.rsvp-choices form {
  display: flex;
}
.rsvp-choices > :first-child {
  grid-column: 1 / -1;
}
.rsvp-choices > :first-child button {
  min-height: 68px;
  font-size: 1.2rem;
}

/* Highlight the currently-selected RSVP. The label takes --color-bg, not #fff:
   a dark theme's success/danger are light tints, and white on them fails AA. */
.rsvp-choices .is-selected {
  color: var(--color-bg);
  border-color: transparent;
}
.rsvp-choices .is-selected.rsvp-yes {
  background: var(--color-success);
}
.rsvp-choices .is-selected.rsvp-no {
  background: var(--color-danger);
}
.rsvp-choices .is-selected.rsvp-maybe {
  background: var(--color-text-muted);
}

/* ---- Forms: native inputs, large targets ---- */
.field {
  display: block;
  margin-bottom: 1rem;
}
.field label {
  display: block;
  font-weight: 600;
  margin-bottom: 0.35rem;
}
.field .hint {
  color: var(--color-text-muted);
  font-size: 0.85rem;
  margin-top: 0.2rem;
}

input[type="text"],
input[type="email"],
input[type="tel"],
input[type="number"],
input[type="password"],
input[type="datetime-local"],
input[type="search"],
select,
textarea {
  width: 100%;
  min-height: 48px;
  padding: 0.6rem 0.75rem;
  font: inherit;
  color: var(--color-text);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
}

textarea {
  min-height: 96px;
  resize: vertical;
}

/* Safari draws its own search-field chrome, which quietly ignores
   border-radius and padding, so the native appearance has to go first. */
input[type="search"] {
  appearance: none;
  -webkit-appearance: none;
}
input[type="search"]::-webkit-search-decoration,
input[type="search"]::-webkit-search-cancel-button {
  -webkit-appearance: none;
}

/* The roster's search sits in a row of .btn-sm filter chips, so it takes their
   height and radius rather than the 48px of a standalone form field -- a
   full-size input next to small chips is what looked out of place. */
.filter-search input[type="search"] {
  width: 100%;
  min-height: 36px;
  padding: 0.35rem 0.8rem;
  font-size: 0.85rem;
}

input:focus-visible,
select:focus-visible,
textarea:focus-visible,
.btn:focus-visible {
  outline: 3px solid color-mix(in srgb, var(--color-accent) 55%, transparent);
  outline-offset: 2px;
}

/* ---- Party header / hero ---- */
.hero {
  margin-bottom: 1.5rem;
}
/* The eyebrow above a heading. Defined on the class itself: it is used on the
   invite CTA, the crawler sheet, the recap and the check-in screen, and until
   this moved out of .hero those three rendered as plain body copy. */
.theme-tag {
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-size: 0.75rem;
  font-weight: 700;
  color: var(--color-accent);
}
/* An eyebrow labels the element under it, so it sits closer than the stack's
   own rhythm. Equal specificity to `.stack > * + *`, and components.css loads
   after base.css, so this wins; .stack-sm already lands on the same 0.5rem. */
.theme-tag + * {
  margin-top: 0.5rem;
}
/* Optional themed banner (shared/_section_header) fills the header width. */
.section-header-banner {
  margin-bottom: 1.5rem;
}
.section-header-img {
  display: block;
  width: 100%;
  height: auto;
  max-width: 100%;
  border-radius: var(--radius-lg);
}
/* Optional per-class art; degrades to nothing when no file is present.
   Tall/skinny art is capped by height, not width, so it doesn't dominate. */
/* A thumbnail while browsing, larger once chosen: the art is the reward for
   picking, not a 300px tax on every one of 34 rows. */
.class-art {
  flex: 0 0 auto;
  width: 3.25rem;
  height: 3.25rem;
  object-fit: contain;
  border-radius: var(--radius-sm);
}
/* Expanded, the art stops being a thumbnail beside the name and becomes a
   banner above it. contain, not cover: class art is mixed aspect ratios and
   cropping a face out is worse than letterboxing. */
.class-card:has(input:checked) .class-art,
.character-sheet .class-art {
  width: 100%;
  height: 300px;
}
@media (prefers-reduced-motion: no-preference) {
  .class-art {
    transition: width 0.18s ease, height 0.18s ease;
  }
}
.class-art-sm {
  width: 3rem;
  height: 3rem;
  object-fit: contain;
  margin: 0 0.75rem 0 0;
  flex-shrink: 0;
}
.meta-line {
  display: flex;
  gap: 0.5rem;
  color: var(--color-text-muted);
  font-size: 0.95rem;
}

/* ---- Admin dashboard ----
   The one screen in the app designed for a laptop. Everything here is an
   instrument reading: dense, aligned, quiet. Structure comes from the same
   mono/uppercase label device the invite page uses (.rule), not from color. */
.admin-head {
  margin-bottom: 0.5rem;
}
.admin-head-top {
  display: flex;
  align-items: center;
  gap: 1rem;
}
.admin-head-top h1 {
  margin: 0;
  min-width: 0;
  font-size: 1.7rem;
}
.admin-head-top .btn {
  margin-left: auto;
  flex-shrink: 0;
}
.admin-head-meta {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin: 0.5rem 0 0;
  font-size: 0.9rem;
}

/* Page links as text, not a wall of grey buttons: none of them is the main
   action. Padding keeps a 44px target even though they read as links. */
.admin-links {
  display: flex;
  flex-wrap: wrap;
  gap: 0 1.1rem;
  margin-top: 0.35rem;
}
.admin-links a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  font-size: 0.9rem;
  font-weight: 600;
  text-decoration: none;
}
.admin-links a:hover {
  text-decoration: underline;
}

/* Both funnels side by side: RSVP status and character status, as the design
   doc requires. Each RSVP count links to the roster filtered to it. */
.funnels {
  display: grid;
  gap: 1rem;
}
.funnel {
  padding: 0.9rem 1rem;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  background: var(--color-surface);
}
.funnel-label {
  margin: 0 0 0.75rem;
  font-family: var(--font-mono);
  font-size: 0.7rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.18em;
  color: var(--color-text-muted);
}
.funnel-stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(4.5rem, 1fr));
  gap: 0.4rem;
}
.stat {
  display: block;
  padding: 0.4rem 0.55rem;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  color: inherit;
  text-decoration: none;
}
a.stat:hover {
  border-color: var(--color-accent);
}
a.stat.is-selected {
  border-color: var(--color-accent);
  background: color-mix(in srgb, var(--color-accent) 12%, var(--color-surface));
  box-shadow: inset 0 0 0 1px var(--color-accent);
}
.stat-n {
  display: block;
  font-family: var(--font-mono);
  font-size: 1.35rem;
  font-weight: 700;
  line-height: 1.1;
  font-variant-numeric: tabular-nums;
}
.stat-l {
  display: block;
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-text-muted);
}
.funnel-note {
  margin: 0.75rem 0 0;
  font-size: 0.85rem;
  color: var(--color-text-muted);
}

.nudge {
  margin-top: 1rem;
  border-color: color-mix(in srgb, var(--color-danger) 45%, var(--color-border));
}
.nudge-head {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.5rem;
}
.nudge-head .btn {
  margin-left: auto;
}
.nudge-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
  gap: 0.1rem 1rem;
  margin: 0.6rem 0 0;
  padding-left: 1.25rem;
  font-size: 0.9rem;
}

/* One bar for every roster filter. The groups wrap as a unit so a group label
   never separates from its own chips, and search is pushed to the end. */
.filter-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem 1.25rem;
  margin: 0.75rem 0 0.6rem;
}
.filter-group {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.4rem;
}
.filter-group-label {
  font-size: 0.75rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--color-text-muted);
}
/* Search fills whatever the chip groups leave. Three groups rarely leave 14rem,
   so in practice it drops to its own full-width row -- which reads as a choice,
   where a 12rem box stranded at the right margin read as a wrap. */
.filter-search {
  display: flex;
  flex: 1 1 14rem;
}

/* The one line that makes stacking legible: what is applied, how much it left,
   and the way out. Each token removes only itself; Reset removes everything. */
.filter-summary {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.4rem;
  padding: 0.5rem 0.7rem;
  margin-bottom: 0.6rem;
  border: 1px solid var(--color-border);
  border-left: 3px solid var(--color-accent);
  border-radius: var(--radius-md);
  background: color-mix(in srgb, var(--color-accent) 5%, var(--color-surface));
  font-size: 0.85rem;
}
.filter-count {
  margin-right: 0.35rem;
  color: var(--color-text-muted);
}
.filter-count strong {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  color: var(--color-text);
}
.filter-token {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  min-height: 36px;
  padding: 0.25rem 0.6rem;
  border: 1px solid var(--color-accent);
  border-radius: 999px;
  background: var(--color-surface);
  color: inherit;
  font-size: 0.8rem;
  text-decoration: none;
}
/* The dimension leads, quietly, so the value after it means something. */
.filter-token-group {
  color: var(--color-text-muted);
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}
/* The x is the affordance, so it is what lights up, not the whole token. */
.filter-token-x {
  color: var(--color-text-muted);
  font-size: 0.75rem;
}
.filter-token:hover .filter-token-x,
.filter-token:focus-visible .filter-token-x {
  color: var(--color-danger);
}
.filter-reset {
  margin-left: auto;
}
.door-entry {
  margin-bottom: 0.75rem;
}

/* A guest row: identity, then statuses in fixed slots, then actions. The slots
   hold their position when empty, so a whole column reads straight down and
   "who has no crawler" is a glance, not a search. */
.guest-row {
  display: grid;
  gap: 0.5rem;
  padding: 0.8rem 0;
  border-bottom: 1px solid var(--color-border);
}
/* Column headers only make sense once the row is a grid; on phones the row
   stacks, so the header has nothing to label. */
.roster-head {
  display: none;
}
.roster-sort {
  font-family: var(--font-mono);
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--color-muted);
  text-decoration: none;
}
.roster-sort:hover,
.roster-sort.is-selected {
  color: var(--color-text);
}

.guest-row:last-child {
  border-bottom: 0;
}
.guest-row:first-child {
  padding-top: 0;
}
.guest-id strong {
  display: block;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.guest-meta {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin-top: 0.15rem;
  font-size: 0.85rem;
  color: var(--color-text-muted);
}
/* On a phone you read one row at a time, so let the pills wrap. The fixed
   slots that make a column scannable only matter at laptop width. */
.guest-status {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
}
/* Phone: the actions reflow to as many columns as fit and stack outright on a
   narrow screen, instead of running off the side of the row. */
.guest-actions {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
  gap: 0.4rem;
}
.guest-actions form {
  margin: 0;
  display: contents;
}
.guest-actions .btn {
  width: 100%;
  white-space: nowrap;
}

/* Secondary tools. Print and superlatives are desk work, so they sit last. */
.tools {
  display: grid;
  gap: 1rem;
  align-items: start;
}
/* The .card + .card stacking margin is for a vertical list; this grid spaces
   itself with gap, so the margin only drops every card after the first out of
   line with the one beside it. */
.tools > .card + .card {
  margin-top: 0;
}
.tool-title {
  font-family: var(--font-mono);
  font-size: 0.7rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.18em;
  color: var(--color-text-muted);
  margin-bottom: 0.6rem;
}
.tool-hint {
  margin: -0.25rem 0 0.75rem;
  font-size: 0.85rem;
}
.tool-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(9.5rem, 1fr));
  gap: 0.4rem;
}
.tool-grid .btn {
  width: 100%;
}
.raffle-name {
  display: block;
  font-size: 1.25rem;
  margin: 0.25rem 0 0.6rem;
}

/* Laptop: the roster becomes a real three-column instrument, and the compact
   button sizing kicks in. The status slots take a fixed width rather than a
   fraction, because equal fractions align only until one pill wraps.
   Below this width the rows stack and every tap target stays >= 44px. */
@media (min-width: 60rem) {
  .funnels {
    grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
  }
  .tools {
    grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr));
  }
  .guest-row {
    grid-template-columns: minmax(8rem, 1fr) auto auto;
    align-items: center;
    gap: 1rem;
  }
  .roster-head {
    display: grid;
    padding-bottom: 0.4rem;
  }
  .guest-status {
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: 7rem;
  }
  /* Four actions in one row would crowd out the status columns on a small
     laptop, so they sit as a fixed 2x2 block. The empty check-in slot keeps
     each button in the same cell on every row. */
  .guest-actions {
    grid-template-columns: repeat(2, 1fr);
    width: 15rem;
  }
  .guest-actions .btn {
    min-height: 36px;
    padding: 0.35rem 0.7rem;
    font-size: 0.85rem;
  }
}

/* ---- Admin (desktop-tolerant, still fine on phone) ---- */
.admin-bar {
  background: var(--color-surface);
  border-bottom: 1px solid var(--color-border);
  padding: 0.6rem 1.1rem;
  display: flex;
  align-items: center;
  gap: 1rem;
  font-size: 0.9rem;
}
.admin-bar a {
  color: var(--color-text);
  text-decoration: none;
  font-weight: 600;
}

/* Admin RSVP list: stacked rows that read fine on a phone. */
.roster-row {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 0.85rem 0;
  border-bottom: 1px solid var(--color-border);
}
.roster-row:last-child {
  border-bottom: 0;
}
.roster-row .who {
  min-width: 0;
}
.roster-row .who strong {
  display: block;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.copy-link {
  font-family: var(--font-mono);
  font-size: 0.8rem;
  word-break: break-all;
}

.summary-tiles {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 0.6rem;
  margin-bottom: 1.25rem;
}
.tile {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: 0.75rem;
  text-align: center;
}
.tile .n {
  font-family: var(--font-mono);
  font-size: 1.6rem;
  font-weight: 700;
  display: block;
}
.tile .l {
  font-size: 0.75rem;
  color: var(--color-text-muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

/* ---- Invitation: one dispatch, one decision ----
   The invite page's own layer. No cards at the top: the party details are set
   as run-in labelled lines so they read as prose, which keeps the RSVP inside
   the first screen instead of below two boxes. */
.invite-hero {
  margin-bottom: 1.5rem;
}

.invite-eyebrow,
.step-eyebrow {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 0.65rem;
  font-family: var(--font-mono);
  font-size: 0.72rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.18em;
  color: var(--color-accent);
}
.invite-mark {
  flex: 0 0 auto;
  width: 0.5rem;
  height: 0.5rem;
  background: var(--color-accent);
}

.invite-title,
.step-title {
  margin: 0 0 1.1rem;
  font-size: clamp(1.9rem, 8vw, 2.5rem);
  line-height: 0.98;
  letter-spacing: -0.015em;
  text-wrap: balance;
}

/* Run-in labels ("When: ..."). The label names the field inline, in the accent,
   so a whole block of details needs no headings and no boxes. */
.dispatch {
  padding-top: 0.9rem;
  border-top: 1px solid var(--color-accent);
}
.dispatch > * {
  margin: 0 0 0.45rem;
}
.dispatch > *:last-child {
  margin-bottom: 0;
}
.dispatch > p > b {
  color: var(--color-accent);
}
.dispatch .trix-content {
  margin-top: 0.9rem;
  color: var(--color-text-muted);
}

/* ---- Countdown: an instrument reading, not a row of stat tiles ---- */
.clock {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0.2rem 0.65rem;
  margin-top: 1.1rem;
  padding-top: 0.9rem;
  border-top: 1px solid var(--color-border);
}
.clock-readout {
  font-size: 1.5rem;
  font-weight: 700;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}
.clock-readout em {
  margin: 0 0.4rem 0 0.05rem;
  font-style: normal;
  font-size: 0.7em;
  color: var(--color-text-muted);
}
/* Inline variant: one superlative's deadline inside its own card, where the
   panel rule and the big readout would both be shouting. */
.clock-sm {
  margin-top: 0;
  padding-top: 0;
  border-top: 0;
  gap: 0.2rem 0.4rem;
}
.clock-sm .clock-readout {
  font-size: 0.95rem;
}
.clock-label {
  margin: 0;
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--color-text-muted);
}

/* The one decision on the page, and the only tinted panel. */
.invite-decision {
  padding: 1.1rem;
  border: 1px solid color-mix(in srgb, var(--color-accent) 35%, var(--color-border));
  border-radius: var(--radius-lg);
  background: color-mix(in srgb, var(--color-accent) 7%, var(--color-surface));
}
.invite-decision-title {
  margin: 0 0 0.85rem;
  font-size: 1.15rem;
}
.dispatch-note {
  margin: 0.9rem 0 0;
  font-size: 0.88rem;
  line-height: 1.45;
  color: var(--color-text-muted);
}
.dispatch-note b {
  color: var(--color-danger);
}

/* Labelled divider: everything under it is a "later" task, not the decision. */
.rule {
  display: flex;
  align-items: center;
  /* The Guests rule carries buttons, and nowrap ran them off a phone screen.
     A rule that is only a label and its hairline has nothing to wrap. */
  flex-wrap: wrap;
  gap: 0.75rem;
  margin: 1.75rem 0 0.25rem;
  font-family: var(--font-mono);
  font-size: 0.7rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.18em;
  color: var(--color-text-muted);
}
.rule::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--color-border);
}

/* One orchestrated page-load moment: the dispatch arrives a line at a time.
   Nothing else animates, and the RSVP panel is deliberately excluded: the
   decision should never be withheld from a guest who is ready to tap. */
@media (prefers-reduced-motion: no-preference) {
  .invite-eyebrow,
  .invite-title,
  .dispatch,
  .clock {
    animation: dispatch-in 0.4s cubic-bezier(0.2, 0.7, 0.3, 1) backwards;
  }
  .invite-title,
.step-title {
    animation-delay: 0.06s;
  }
  .dispatch {
    animation-delay: 0.12s;
  }
  .clock {
    animation-delay: 0.18s;
  }
}

@keyframes dispatch-in {
  from {
    opacity: 0;
    transform: translateY(6px);
  }
}

/* ---- The intake manifest ----
   Four loose cards said nothing about being a sequence, and guests stopped at
   the first one. This is a ledger instead: hairline rows, mono numerals, one
   lit row at a time. It carries the invite page's own discipline (no boxes,
   run-in labels, prose) down past the RSVP. Exactly one row is open -- the
   first incomplete step -- and that is the only place a primary button lives,
   so "what do I do next" is answered by the page's structure. */
.intake {
  margin-top: 1.75rem;
}
.intake-head {
  display: flex;
  align-items: center;
  gap: 1rem;
}
/* The eyebrow already carries its own bottom margin; the meter aligns to it. */
.intake-head .invite-eyebrow {
  margin-bottom: 0;
}
.intake-head .dot-track {
  margin-left: auto;
}
.intake-lede {
  margin: 0.55rem 0 0;
  font-size: 0.92rem;
  color: var(--color-text-muted);
}
.intake-lede strong {
  color: var(--color-text);
}

/* One task. A <details> so the open/closed state is native: keyboard, screen
   readers and a guest reopening a finished row all work with no JS. */
/* Padded in, pulled back out: the content of every row is inset, so the lit
   row's border has breathing room, while the hairlines and the lit box still
   span the full ledger width. Applied to every row so the numeral column stays
   a column -- a lit row that indents its own number makes the sequence ragged. */
.task {
  padding: 0 0.9rem;
  margin: 0 -0.9rem;
  /* Every row carries the lit panel's border widths, transparent when unlit, so
     the panel can't nudge its own numeral out of the column. */
  border: 1px solid transparent;
  border-left-width: 3px;
  border-top-color: var(--color-border);
  scroll-margin-top: 1rem;
}
.task:first-of-type {
  margin-top: 0.85rem;
}
.task:last-of-type {
  border-bottom-color: var(--color-border);
}

.task-row {
  display: flex;
  align-items: center;
  gap: 0.85rem;
  min-height: 56px;
  padding: 0.7rem 0;
  cursor: pointer;
  list-style: none;
}
/* The default disclosure triangle competes with the number, which is the real
   marker here. */
.task-row::-webkit-details-marker {
  display: none;
}

/* Centred, not left-aligned: a one-character check and a two-digit numeral
   have to sit on the same axis. */
.task-mark {
  flex: 0 0 1.6rem;
  text-align: center;
  font-family: var(--font-mono);
  font-size: 0.95rem;
  font-weight: 700;
  color: var(--color-text-muted);
}
.task-text {
  flex: 1;
  min-width: 0;
}
.task-name {
  display: block;
  font-family: var(--font-display);
  font-size: 1.02rem;
  line-height: 1.2;
}
.task-note {
  display: block;
  margin-top: 0.1rem;
  font-size: 0.83rem;
  line-height: 1.35;
  color: var(--color-text-muted);
}
.task-state {
  flex: 0 0 auto;
  font-family: var(--font-mono);
  font-size: 0.62rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.16em;
  color: var(--color-text-muted);
}

.task-body {
  padding: 0.25rem 0 1.25rem 2.45rem;
}
/* The lit row's body clears its own border rather than sitting against it. */
.task.is-open .task-row {
  padding-top: 1rem;
}
.task-body > * + * {
  margin-top: 0.9rem;
}
.task-body p {
  margin: 0;
}

/* The one lit row. Everything the guest should do next is inside it, and
   nothing else on the page carries the accent. */
/* A closed panel on all four sides. Lit only on the left, top and bottom, it
   read as a box someone had cut the right edge off. The row is marked by its
   border and its tint, never by an indent: the mono numerals are a column, and
   shifting one of them makes the sequence ragged. */
.task.is-open {
  border: 1px solid var(--color-accent);
  border-left-width: 3px;
  border-radius: var(--radius-md);
  background: color-mix(in srgb, var(--color-accent) 5%, transparent);
}
/* The panel draws its own bottom edge, so the next row must not draw one too. */
.task.is-open + .task {
  border-top: 0;
}
.task.is-open .task-mark,
.task.is-open .task-state {
  color: var(--color-accent);
}
.task.is-open .task-name {
  font-size: 1.15rem;
}

/* Done: the row shrinks to its answer. This is the whole point -- a finished
   crawler was as loud as an untouched task and buried the ones still owed. */
.task.is-done .task-mark {
  color: var(--color-success);
}
.task.is-done .task-name,
.task.is-done .task-state {
  color: var(--color-text-muted);
}

/* Uppercase headings are a DCC-theme rule for h1-h3; the task name is a span,
   so it opts in explicitly to sit in the same voice. */
[data-theme="dcc"] .task-name {
  text-transform: uppercase;
  font-weight: 700;
  letter-spacing: 0.02em;
}

/* ---- The settled RSVP ----
   Answered, the decision panel stops shouting. It stays one tap from changing
   so a mis-tap is never a trap, but it no longer competes with the tasks. */
.rsvp-settled {
  margin-top: 1.25rem;
  padding: 0.7rem 0.9rem;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface);
}
.rsvp-settled > summary {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  min-height: 30px;
  cursor: pointer;
  list-style: none;
}
.rsvp-settled > summary::-webkit-details-marker {
  display: none;
}
.rsvp-settled-mark {
  color: var(--color-success);
  font-weight: 700;
}
.rsvp-settled-text {
  flex: 1;
  font-weight: 600;
}
.rsvp-settled-change {
  font-family: var(--font-mono);
  font-size: 0.68rem;
  text-transform: uppercase;
  letter-spacing: 0.16em;
  color: var(--color-accent);
}

/* Dietary checkboxes: a wrapping row of tap targets, not a column of them. */
.diet-options {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 0.9rem;
  margin: 0.35rem 0 0.6rem;
}
.diet-options label {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  font-weight: 500;
}

/* ---- Guest-facing section nav ---- */
.party-nav {
  display: flex;
  gap: 0.4rem;
  overflow-x: auto;
  margin: 0 0 1.25rem;
  padding-bottom: 0.25rem;
  -webkit-overflow-scrolling: touch;
}
.party-nav-link {
  flex: 0 0 auto;
  min-height: 40px;
  display: inline-flex;
  align-items: center;
  padding: 0 0.9rem;
  border-radius: 999px;
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  color: var(--color-text);
  text-decoration: none;
  font-weight: 600;
  font-size: 0.9rem;
}
.party-nav-link.is-active {
  background: var(--color-accent);
  color: var(--color-accent-contrast);
  border-color: transparent;
}

/* ---- Message board ---- */
.post-head {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 0.35rem;
}
.post-body {
  overflow-wrap: anywhere;
}
.post-reply {
  margin-top: 0.75rem;
  padding-left: 0.9rem;
  border-left: 3px solid var(--color-border);
}
.btn.post-delete,
.btn.photo-delete {
  min-height: 32px;
  padding: 0.2rem 0.6rem;
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--color-danger);
  background: transparent;
  border-color: transparent;
}
.reply-toggle {
  margin-top: 0.75rem;
}
.reply-toggle summary {
  cursor: pointer;
  color: var(--color-accent);
  font-weight: 600;
  min-height: 44px;
  display: flex;
  align-items: center;
}

/* ---- Photo wall ---- */
.photo-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 0.6rem;
}
.photo-tile {
  margin: 0;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  overflow: hidden;
}
.photo-tile img {
  display: block;
  width: 100%;
  aspect-ratio: 1 / 1;
  object-fit: cover;
}
.photo-tile figcaption {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.4rem 0.55rem;
  font-size: 0.8rem;
}
.photo-tile figcaption .muted {
  margin-left: auto;
}
.photo-tile .photo-delete-form {
  display: contents;
}

/* The label IS the file picker's button, so it has to read as tappable and NOT
   as a text field - a guest tried to paste his photo into the caption box.
   Beats .field label's display:block, and the dashed accent border keeps it
   distinct from both the inputs above it and the solid Upload button below. */
.field label.photo-picker {
  display: flex;
  margin-bottom: 0;
  font-size: 1.05rem;
  color: var(--color-accent);
  background: transparent;
  border: 2px dashed var(--color-accent);
}
.field label.photo-picker:active {
  transform: translateY(1px);
}
.photo-picker:has(+ input:focus-visible) {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

/* The tile image is a button so it can open the lightbox; strip the chrome. */
.photo-open {
  display: block;
  width: 100%;
  padding: 0;
  border: 0;
  background: none;
  cursor: zoom-in;
  -webkit-tap-highlight-color: transparent;
}
.photo-open:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: -2px;
}

/* ---- Lightbox ---- */
.lightbox {
  width: 100vw;
  max-width: 100vw;
  height: 100dvh;
  max-height: 100dvh;
  margin: 0;
  padding: 0;
  border: 0;
  /* Opaque on the dialog itself, not just ::backdrop - the dialog covers the
     viewport, and a see-through one leaves the dimmed page ghosting behind
     the photo. */
  background: rgba(0, 0, 0, 0.96);
  overflow: hidden;
}
.lightbox::backdrop {
  background: rgba(0, 0, 0, 0.96);
}

/* Paging through the photos is scroll-snap, not JavaScript. */
.lightbox-strip {
  display: flex;
  height: 100%;
  overflow-x: auto;
  overflow-y: hidden;
  scroll-snap-type: x mandatory;
  overscroll-behavior-x: contain;
  scrollbar-width: none;
}
.lightbox-strip::-webkit-scrollbar {
  display: none;
}

.lightbox-slide {
  display: flex;
  flex: 0 0 100%;
  flex-direction: column;
  justify-content: center;
  gap: 0.75rem;
  margin: 0;
  padding: 3.5rem 0.75rem 1rem;
  scroll-snap-align: start;
  scroll-snap-stop: always;
}
.lightbox-slide img {
  display: block;
  width: 100%;
  min-height: 0;
  flex: 1 1 auto;
  object-fit: contain;
}
.lightbox-slide figcaption {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  flex: 0 0 auto;
  color: #fff;
  font-size: 0.9rem;
}
.lightbox-slide figcaption .muted {
  color: rgba(255, 255, 255, 0.65);
}
.lightbox-slide figcaption .btn {
  margin-left: auto;
}

.lightbox-nav {
  position: fixed;
  min-width: 48px;
  min-height: 48px;
  padding: 0;
  border: 0;
  border-radius: 999px;
  /* Translucent white, not black: these sit on a near-black backdrop. */
  background: rgba(255, 255, 255, 0.15);
  color: #fff;
  font-size: 1.6rem;
  line-height: 1;
  cursor: pointer;
}
.lightbox-nav:active {
  background: rgba(255, 255, 255, 0.3);
}
.lightbox-prev,
.lightbox-next {
  top: 50%;
  transform: translateY(-50%);
}
.lightbox-prev {
  left: 0.5rem;
}
.lightbox-next {
  right: 0.5rem;
}
.lightbox-close {
  top: 0.75rem;
  right: 0.5rem;
  font-size: 1.2rem;
}

@media (min-width: 34rem) {
  .photo-grid {
    grid-template-columns: repeat(3, 1fr);
  }
}

/* ---- Character wizard ---- */
.wizard-progress {
  display: flex;
  gap: 0.4rem;
  justify-content: center;
  margin-bottom: 1.5rem;
}
.wizard-dot {
  width: 0.55rem;
  height: 0.55rem;
  border-radius: 999px;
  background: var(--color-border);
}
.wizard-dot.is-done {
  background: var(--color-accent);
  opacity: 0.6;
}
.wizard-dot.is-current {
  background: var(--color-accent);
  transform: scale(1.35);
}

/* One-decision-per-screen option cards (radio inputs) */
.choice-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 0.6rem;
}
.choice {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 64px;
  padding: 0.75rem;
  text-align: center;
  font-weight: 600;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface);
  cursor: pointer;
}
.choice input {
  position: absolute;
  opacity: 0;
  width: 0;
  height: 0;
}
.choice.is-selected,
.choice:has(input:checked) {
  border-color: var(--color-accent);
  background: color-mix(in srgb, var(--color-accent) 16%, var(--color-surface));
  box-shadow: inset 0 0 0 1px var(--color-accent);
}

/* ---- Character wizard steps ---- */
.wizard-head {
  margin-bottom: 1.25rem;
}
.wizard-head .step-title {
  font-size: clamp(1.7rem, 7vw, 2.2rem);
}

/* The class step's brief: the one block a guest who has never heard of the
   book has to read, so it is boxed and accented rather than left as body copy.
   The FAQ under it reuses .reply-toggle's summary styling. */
.brief {
  margin-top: 1rem;
  padding: 0.9rem 1rem;
  border: 1px solid var(--color-accent);
  border-radius: var(--radius-md);
  background: color-mix(in srgb, var(--color-accent) 8%, var(--color-surface));
}
.brief > p {
  margin: 0 0 0.55rem;
}
.brief > p:last-child {
  margin-bottom: 0;
}
.brief b {
  color: var(--color-accent);
}
.brief-note {
  font-size: 0.9rem;
  color: var(--color-text-muted);
}

/* The example claim card. Dark art on a dark theme, so it needs its own edge. */
.faq-figure {
  display: block;
  width: 80%;
  height: auto;
  margin-top: 0.6rem;
  border: 2px solid var(--color-border);
  border-radius: var(--radius-md);
}
/* The primer for anyone who has never heard of the book -- the thing a guest
   with no context needs most, sitting directly under the accented brief. As a
   small line of link-coloured text it read as a caption and nobody opened it.
   It is a control now: full width, its own edge, and a marker that says which
   way it goes. */
.faq > summary {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  min-height: 56px;
  margin-top: 1rem;
  padding: 0 1rem;
  border: 1px solid var(--color-accent);
  border-radius: var(--radius-md);
  background: var(--color-surface);
  font-family: var(--font-display);
  font-size: 1.1rem;
  font-weight: 700;
  color: var(--color-text);
  cursor: pointer;
  list-style: none;
}
.faq > summary::-webkit-details-marker {
  display: none;
}
/* A plus that becomes a minus. Unambiguous at a glance, and it needs no
   rotation for anyone reading with motion turned down. */
.faq > summary::after {
  content: "+";
  margin-left: auto;
  font-family: var(--font-mono);
  font-size: 1.5rem;
  font-weight: 400;
  line-height: 1;
  color: var(--color-accent);
}
.faq[open] > summary {
  margin-bottom: 0.25rem;
  border-bottom-left-radius: 0;
  border-bottom-right-radius: 0;
  background: color-mix(in srgb, var(--color-accent) 10%, var(--color-surface));
}
.faq[open] > summary::after {
  content: "\2013";
}
/* One question per row. Collapsed, the whole primer is a scannable list of
   questions instead of two screens of prose, and the answer a guest wants is
   one tap away. Rows share a hairline instead of each drawing a box. */
.faq-q {
  border-bottom: 1px solid var(--color-border);
}
.faq-q:last-child {
  border-bottom: 0;
}
.faq-q > summary {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  min-height: 44px;
  padding: 0.5rem 0.2rem;
  font-weight: 600;
  cursor: pointer;
  list-style: none;
}
.faq-q > summary::-webkit-details-marker {
  display: none;
}
.faq-q > summary::after {
  content: "+";
  margin-left: auto;
  font-family: var(--font-mono);
  font-size: 1.2rem;
  line-height: 1;
  color: var(--color-accent);
}
.faq-q[open] > summary::after {
  content: "\2013";
}
.faq-q > p {
  margin: 0 0 0.9rem;
  padding: 0 0.2rem;
  color: var(--color-text-muted);
}

.class-tools {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin-bottom: 0.85rem;
}
.class-tools .btn {
  margin-left: auto;
}

/* Class selection cards. Compact by default: 34 classes at full detail was
   fifteen screens of scrolling to make one choice. */
.class-list {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}
.class-card {
  display: block;
  position: relative;
  padding: 1rem;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface);
  cursor: pointer;
}
.class-card-input {
  position: absolute;
  opacity: 0;
  width: 0;
  height: 0;
}
.class-card:not(.is-full):has(input:checked) {
  border-color: var(--color-accent);
  background: color-mix(in srgb, var(--color-accent) 12%, var(--color-surface));
  box-shadow: inset 0 0 0 1px var(--color-accent);
}
/* Taken classes stay on the page, greyed out. They still open like any other
   card, they just have no radio behind them, so clickable but not selectable.
   The filter is pure CSS: its checkbox has no name, so it never posts. */
.class-card.is-full {
  cursor: pointer;
}
/* Dim the text, not the art or the TAKEN stamp. Opacity on the card - or on
   .class-card-head, which holds both the art and the stamp - would clamp them
   too: a child can't undo a parent's opacity. */
.class-card.is-full .class-card-title,
.class-card.is-full .class-desc,
.class-card.is-full .class-reveal {
  opacity: 0.5;
}
/* Stamped over the art rather than beside it: a taken card still opens, so the
   "Taken by" line alone reads as a detail instead of a wall. Expanded only - on
   a 3.25rem thumbnail the stamp was smaller than the "Taken by" line it repeats.
   A grid item on the art's own cell, so it tracks the banner. */
.class-card.is-full:has(input:checked) .class-card-head::before {
  content: "TAKEN";
  grid-area: 1 / 1;
  align-self: center;
  justify-self: center;
  padding: 0.35rem 0.9rem;
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--color-bg) 78%, transparent);
  color: var(--color-danger);
  font-size: 1.1rem;
  font-weight: 700;
  letter-spacing: 0.16em;
  line-height: 1;
}
.class-taken {
  margin: 0 0 0.5rem;
  font-style: italic;
}
.class-tools {
  margin-bottom: 1rem;
}
.class-filter {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.95rem;
  cursor: pointer;
}
.class-tools:has(#hide-full-classes:checked) ~ .class-list .class-card.is-full {
  display: none;
}
/* Grid, not flex: only 19 of 34 classes have art, and a reserved first column
   keeps every name on the same left edge so the list stays scannable. */
.class-card-head {
  display: grid;
  grid-template-columns: 3.25rem 1fr;
  align-items: center;
  gap: 0.75rem;
  margin-bottom: 0.35rem;
}
/* Expanded, the reserved art column collapses and the head stacks: banner
   first, then the name. Only when there is art - a class with none should not
   grow an empty row. */
.class-card:has(input:checked):has(.class-art) .class-card-head {
  grid-template-columns: 1fr;
}
.class-card:has(input:checked):has(.class-art) .class-card-title {
  grid-column: 1;
}
.class-card-title {
  grid-column: 2;
}
.class-card-title {
  min-width: 0;
}
.class-name {
  display: block;
  font-size: 1.1rem;
  line-height: 1.2;
}
.class-slots,
.class-card .class-taken {
  display: block;
  font-size: 0.78rem;
  color: var(--color-text-muted);
}

/* The mechanics of the class you picked, and only that one. */
.class-reveal {
  display: none;
}
.class-card:has(input:checked) .class-reveal {
  display: block;
}
/* Flavor line: the class speaking, not information. Italic keeps it distinct
   without competing with the traits block below it. */
.class-desc {
  margin: 0 0 0.35rem;
  font-style: italic;
  color: var(--color-text-muted);
}
/* Advantage + rule: label above the text, hairline grouping them as the
   mechanical half of a class (flavor description sits outside). */
.class-traits {
  display: grid;
  gap: 0.7rem;
  margin: 0.75rem 0 0;
  padding-left: 0.85rem;
  border-left: 2px solid var(--color-accent);
}
.class-trait-label {
  display: block;
  font-size: 0.7rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--color-accent);
  margin-bottom: 0.15rem;
}
.class-trait p {
  margin: 0;
  letter-spacing: normal;
  line-height: 1.5;
  max-width: 46ch;
}

/* Stats */
.stat-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 0.6rem;
}
.stat-grid-sm {
  gap: 0.4rem;
  margin-top: 0.75rem;
}
.stat-field label {
  display: block;
  font-weight: 700;
  font-size: 0.8rem;
  letter-spacing: 0.05em;
  margin-bottom: 0.25rem;
  color: var(--color-text-muted);
}
.stat-field input {
  text-align: center;
  font-family: var(--font-mono);
  font-size: 1.2rem;
}
.stat-chip {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 0.5rem;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-surface);
}
.stat-chip .mono {
  font-size: 1.4rem;
  font-weight: 700;
}
.stat-abbr {
  font-size: 0.7rem;
  color: var(--color-text-muted);
  letter-spacing: 0.05em;
}

/* Character sheet + roster */
.sheet-line {
  display: flex;
  align-items: center;
  gap: 0.6rem;
}
.sheet-line strong {
  margin-left: 0.2rem;
}
.sheet-edit {
  margin-left: auto;
  font-size: 0.85rem;
}
.roster-card {
  display: flex;
  align-items: center;
  gap: 1rem;
  flex-wrap: wrap;
}
.roster-card .stat-grid-sm {
  grid-template-columns: repeat(6, auto);
  margin-top: 0;
}

/* ---- QR door pass ---- */
.door-pass summary {
  cursor: pointer;
  min-height: 44px;
  display: flex;
  align-items: center;
}
.qr {
  /* Always render the QR on white so it scans regardless of theme. */
  background: #fff;
  padding: 0.75rem;
  border-radius: var(--radius-md);
  width: min(260px, 70vw);
  margin: 0 auto;
}
.qr svg {
  display: block;
  width: 100%;
  height: auto;
}

/* ---- Two truths & two lies (claims) ---- */
.claims-group {
  padding: 1rem;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface);
}
.claims-group-truth {
  border-left: 4px solid var(--color-success);
}
.claims-group-lie {
  border-left: 4px solid var(--color-danger);
}
.claims-group-title {
  margin: 0;
  font-size: 1.1rem;
}
.claims-field textarea {
  width: 100%;
}
.claims-field-count {
  text-align: right;
  margin-top: 0.15rem;
}
.claims-progress {
  text-align: center;
}
/* Shared "n of m" meter: the claims form and the intake manifest both count
   the same way, so they count with the same device. */
.dot-track {
  display: flex;
  gap: 0.4rem;
}
.claims-progress .dot-track {
  justify-content: center;
}
.dot {
  width: 0.7rem;
  height: 0.7rem;
  border-radius: 999px;
  background: var(--color-border);
}
.dot.is-filled {
  background: var(--color-accent);
}
.claim-row.is-excluded {
  opacity: 0.7;
}

/* Roster cards open to the class's flavour text: "Yardsale" tells a guest
   nothing about the crawler standing next to them. Same head either way, so
   the summary carries .roster-card and only the marker is extra. */
.roster-summary {
  cursor: pointer;
  list-style: none;
}
.roster-summary::-webkit-details-marker {
  display: none;
}
.roster-summary::after {
  content: "+";
  margin-left: auto;
  font-family: var(--font-mono);
  font-size: 1.35rem;
  line-height: 1;
  color: var(--color-accent);
}
details[open] > .roster-summary::after {
  content: "\2013";
}
.roster-desc {
  margin: 0.75rem 0 0;
  color: var(--color-text-muted);
}

/* The System announcing an achievement. Loud on purpose: this is the one
   moment the app is allowed to interrupt, and it is what the book does. */
.achievement-flash {
  border: 2px solid var(--color-accent, var(--color-border));
  border-radius: var(--radius-md);
  padding: 0.85rem 1rem;
  margin-bottom: 1rem;
  background: var(--color-surface);
}
.achievement-flash-art {
  display: block;
  width: 100%;
  max-width: 22rem;
  height: auto;
  margin: 0 auto 0.6rem;
}
.achievement-flash-title {
  font-weight: 800;
  letter-spacing: 0.08em;
  text-align: center;
  margin-bottom: 0.6rem;
}
.achievement-flash-item + .achievement-flash-item {
  margin-top: 0.6rem;
  padding-top: 0.6rem;
  border-top: 1px solid var(--color-border);
}
.achievement-flash-flavor {
  color: var(--color-muted, inherit);
  font-size: 0.9rem;
}
.achievement-flash-reward {
  font-size: 0.85rem;
  margin-top: 0.2rem;
}
