/* NOTE (cleanup pass 1): the old light-parchment :root, body
     background, and body::before noise overlay that used to open this
     file have been deleted. All tokens (--paper, --ink, --gold, etc.)
     are now defined ONCE, in the "Palette: Literary Terminal" :root
     further down, which also aliases the old token names to the dark
     palette. The old --paper-rgb / --gold-rgb triplets are gone too:
     every rule that consumed them is overridden later in this file
     by the dark-layer restyle (or deleted in pass 3), so those
     declarations were already dead in the cascade. */

  * { box-sizing: border-box; margin: 0; padding: 0; }
  html { scroll-behavior: smooth; }

  .container {
    max-width: 880px;
    margin: 0 auto;
    padding: 60px 32px 120px;
    position: relative;
    z-index: 2;
  }

  header { text-align: center; margin-bottom: 80px; animation: fadeUp 0.8s ease; }
  .wordmark {
    line-height: 1;
  }
  .wordmark .ital { font-weight: 400; }

  .section {
    margin-bottom: 48px;
    opacity: 0; transform: translateY(12px);
    transition: opacity 0.5s ease, transform 0.5s ease;
    pointer-events: none;
  }
  .section.visible { opacity: 1; transform: translateY(0); pointer-events: auto; }
  /* Used when resuming a session from URL — fully hide the input
     sections so the progress section sits at the top instead of after
     a stack of empty space. Different from just removing .visible:
     that preserves the layout slot for the future fade-in animation;
     .hidden removes the slot entirely. */
  .section.hidden { display: none; }

  /* Section headers — both the static (transcript) and the clickable
     collapsible ones share inner element styling. */
  .section-label {
    display: flex; gap: 16px; margin-bottom: 16px;
  }
  .section-label { align-items: baseline; }

  .section-label .num {
    font-family: 'JetBrains Mono', monospace;
    font-size: 11px; color: var(--ink-faint);
    letter-spacing: 0.15em; text-transform: uppercase;
  }
  .section-label .name {
    font-family: 'Newsreader', serif; font-size: 22px;
    font-weight: 600; letter-spacing: -0.01em;
  }
  .section-label .rule {
    flex: 1; height: 1px; background: var(--rule);
  }




  /* DROP ZONE */




  .field-label .opt {
    color: var(--ink-faint); text-transform: none;
    letter-spacing: 0; font-style: italic; margin-left: 6px;
  }

  input[type="text"], input[type="number"], input[type="date"], textarea, select {
    width: 100%;
    background: rgba(255, 252, 245, 0.6);
    border: 1px solid var(--rule);
    padding: 12px 14px;
    font-family: 'Inter', sans-serif; font-size: 14px;
    color: var(--ink); border-radius: 2px;
    transition: all 0.2s ease;
  }
  input[type="text"]:focus, input[type="number"]:focus, input[type="date"]:focus,
  textarea:focus, select:focus {
    outline: none; border-color: var(--ink); background: #fffcf5;
  }
  textarea {
    resize: vertical; min-height: 100px;
    font-family: 'Inter', sans-serif; line-height: 1.55;
  }



  /* NOTE: the prototype .output-card / :hover / .selected / .selected::after
     rules that used to live here have been removed. They were dead
     cruft from the light-parchment proto.css and are fully superseded
     by the dark-palette .output-card block later in this file. The
     stale .output-card.selected::after drew a second checkmark glyph
     on top of the .output-check SVG badge — that was the "double tick"
     on selected output cards. */


  .chip {
    font-family: 'JetBrains Mono', monospace;
    font-size: 10px; text-transform: uppercase;
    letter-spacing: 0.08em; padding: 3px 7px;
    border: 1px solid var(--rule); border-radius: 2px;
    color: var(--ink-faint);
  }

  .podcast-types {
    flex-wrap: wrap;
  }
  .podcast-type {
    font-family: inherit;
  }
  .podcast-type-art {
    transition: border-color 0.2s ease, transform 0.2s ease;
    box-shadow: 0 2px 8px rgba(26, 22, 18, 0.12);
  }
  .podcast-type:hover .podcast-type-art {
    transform: translateY(-2px);
    box-shadow: 0 4px 14px rgba(26, 22, 18, 0.18);
  }
  .podcast-type-name {
    margin-top: 6px;
  }
  .podcast-type.selected .podcast-type-name {
    font-weight: 600;
  }

  /* Campaign edit-for-this-session prompt — shows the baseline content
     read-only with an Edit button below it. */
  .campaign-baseline {
    background: rgba(255, 252, 245, 0.4);
    border: 1px dashed var(--rule);
    border-radius: 2px;
    padding: 14px 16px;
    font-family: 'Inter', sans-serif;
    font-size: 14px;
    line-height: 1.55;
    color: var(--ink-soft);
    white-space: pre-wrap;
    cursor: default;
    user-select: text;
    /* Cap the height so very long campaign settings scroll rather than dominate */
    max-height: 240px;
    overflow-y: auto;
  }
  .campaign-baseline:empty::before {
    content: 'No baseline configured for this campaign.';
    font-style: italic;
    color: var(--ink-faint);
  }

  .generate-row { margin-top: 40px; text-align: center; }
  .generate-btn {
    position: relative; overflow: hidden;
  }
  .generate-btn:not(:disabled):hover {
    background: var(--oxblood);
    transform: translateY(-1px);
    box-shadow: 0 8px 24px rgba(139, 44, 44, 0.2);
  }
  .generate-btn .arrow {
    display: inline-block; margin-left: 8px;
    transition: transform 0.25s ease;
  }
  .generate-btn:not(:disabled):hover .arrow { transform: translateX(4px); }

  /* Generate-row + progress lives outside any section so it's always visible. */


  /* Indeterminate activity bar — sits above the stage rows. Visible
     when the progress section is in 'working' state; gone on done /
     failed. The "we're alive" signal during long stages, so a glance
     across the room tells you generation is still moving. */
  @keyframes activitySlide {
    0%   { transform: translateX(-100%); }
    50%  { transform: translateX(333%); }
    100% { transform: translateX(-100%); }
  }





  /* ----- Image gallery -----
   * Sits above the stage rows in the progress section. Up to four
   * images today (cover + three scenes); grid expands for future
   * runs that produce more. Cover gets a "Cover" badge. */
    
  @media (max-width: 540px) {
    .gallery { grid-template-columns: repeat(2, 1fr); gap: 10px; }
  }

  .modal-backdrop {
    position: fixed; inset: 0;
    display: none; align-items: center; justify-content: center;
    z-index: 100; animation: fadeIn 0.2s ease;
  }
  .modal-backdrop.open { display: flex; }
  .modal {
    max-width: 480px; width: calc(100% - 40px);
    animation: scaleIn 0.25s ease;
    position: relative;
  }
  .modal-title {
    margin-bottom: 6px;
  }
  .btn-primary:hover { background: var(--oxblood); border-color: var(--oxblood); }
  .btn-primary:disabled {
    border-color: var(--rule); }

  /* Access-code modal — cost breakdown */
  .cost-breakdown {
    border-left: 2px solid var(--gold);
  }
  .cost-row .lbl { font-family: 'Inter', sans-serif; }
  .cost-row .amt {
    font-family: 'JetBrains Mono', monospace;
    font-size: 12px;
  }
  .cost-row.total {
    color: var(--ink); font-weight: 500;
  }
  .cost-row.total .amt {
    font-size: 13px; font-weight: 500;
  }

/* Top-up modal — tier selection. */

  /* Insufficient-credits banner inside the access modal. */
  .insufficient-banner {
    border-left: 2px solid var(--oxblood);
  }
  .insufficient-banner strong { color: var(--ink); font-weight: 500; }
  /* Credit pill dropdown — small menu shown below the pill on click. */
  .credit-menu {
    margin-top: 4px;
    animation: scaleIn 0.15s ease;
    transform-origin: top right;
  }
  .credit-menu.open { display: block; }
  .credit-menu button + button {
    border-top: 1px solid var(--rule);
  }
  /* The credit pill needs to be a positioning context for the menu. */
  .auth-strip .credit-pill-wrap {
    position: relative;
    display: inline-block;
  }

  /* Ledger modal — wider than default, table-like body. */
  .viewer { max-width: 720px; max-height: 80vh; display: flex; flex-direction: column; }

/* Auth strip — top-right of viewport */
  .auth-strip {
    position: fixed;
    top: 16px;
    right: 24px;
    font-size: 13px;
    color: rgba(0, 0, 0, 0.65);
    z-index: 100;
  }
  .auth-strip a {
    color: rgba(0, 0, 0, 0.85);
    text-decoration: none;
    border: 1px solid rgba(0, 0, 0, 0.2);
    padding: 4px 12px;
    border-radius: 4px;
    transition: background 0.15s;
  }
  .auth-strip a:hover {
    background: rgba(0, 0, 0, 0.05);
  }
  .auth-strip .user-email {
    font-weight: 500;
    color: rgba(0, 0, 0, 0.95);
  }
  /* Credit balance pill in the auth strip. */
  .auth-strip .credit-pill {
    display: inline-flex; align-items: center; gap: 6px;
    padding: 4px 10px;
    border: 1px solid rgba(0, 0, 0, 0.2);
    border-radius: 4px;
    font-family: 'JetBrains Mono', monospace;
    font-size: 11px;
    letter-spacing: 0.05em;
    color: var(--ink-soft);
    cursor: pointer;
    transition: background 0.15s, border-color 0.15s;
  }
  .auth-strip .credit-pill:hover {
    background: rgba(0, 0, 0, 0.05);
    border-color: rgba(0, 0, 0, 0.4);
  }
  .auth-strip .credit-pill .credits-num {
    color: var(--ink); font-weight: 500;
  }
  .auth-strip .credit-pill.low .credits-num { color: var(--oxblood); }
  .auth-strip .credit-pill .add-icon {
    display: inline-block; width: 14px; height: 14px;
    line-height: 12px; text-align: center;
    border: 1px solid currentColor; border-radius: 50%;
    font-size: 12px; font-weight: 600;
  }
  .footer {
    margin-top: 80px; text-align: center;
    font-family: 'JetBrains Mono', monospace;
    font-size: 10px; color: var(--ink-faint);
    letter-spacing: 0.1em; text-transform: uppercase;
  }
  .footer .star { color: var(--gold); margin: 0 8px; }

  @keyframes fadeUp {
    from { opacity: 0; transform: translateY(16px); }
    to { opacity: 1; transform: translateY(0); }
  }
  @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
  @keyframes scaleIn {
    from { opacity: 0; transform: scale(0.96); }
    to { opacity: 1; transform: scale(1); }
  }
  @keyframes spin { to { transform: rotate(360deg); } }

  @media (max-width: 640px) {
    .container { padding: 40px 20px 80px; }
    .wordmark { font-size: 42px; }
  }

  /* ============================================================
     CAMPAIGNS PAGE — single-pane detail view
     ============================================================ */

  /* The campaigns page uses a wider container than the home page. */
  .container.campaigns-container {
    max-width: 1100px;
  }

  /* (cleanup pass 3) Single-pane now: the campaign list lives in the
     My-campaigns modal, so the old 280px/1fr split-pane grid (and its
     mobile stack) is gone. Only the top margin survives. The
     display:block override in campaigns.html's inline block is
     retired with it. */
  .campaigns-layout {
    margin-top: 40px;
  }

  /* List pane — left side, the primary nav. */

  /* Detail pane — right side, the body. */
  .campaign-detail {
    min-width: 0;  /* prevent overflow when content is too wide */
  }
  .campaign-detail-empty {
    font-family: 'Newsreader', serif;
    font-style: italic;
    font-size: 18px;
    color: var(--ink-faint);
    text-align: center;
    padding: 80px 40px;
  }
  .campaign-detail-header {
    margin-bottom: 32px;
    padding-bottom: 20px;
    border-bottom: 1px solid var(--rule);
  }
  .campaign-detail-name {
    font-family: 'Newsreader', serif;
    font-size: 32px;
    font-weight: 600;
    letter-spacing: -0.01em;
    line-height: 1.15;
    margin-bottom: 8px;
  }
  .campaign-detail-meta {
    font-family: 'JetBrains Mono', monospace;
    font-size: 11px;
    color: var(--ink-faint);
    letter-spacing: 0.05em;
    text-transform: uppercase;
  }
  .campaign-detail-meta .uuid {
    font-size: 10px;
    opacity: 0.75;
  }

  /* Sub-section blocks within a detail pane (Members, Sessions). */
  .detail-block {
    margin-bottom: 36px;
  }
  .detail-block-header {
    display: flex; align-items: baseline;
    gap: 12px;
    margin-bottom: 14px;
  }
  .detail-block-title {
    font-family: 'Newsreader', serif;
    font-size: 18px;
    font-weight: 600;
    letter-spacing: -0.01em;
  }
  .detail-block-rule {
    flex: 1; height: 1px; background: var(--rule);
  }

  /* Members list */
  .member-row {
    display: flex; align-items: center;
    gap: 12px;
    padding: 10px 14px;
    border: 1px solid var(--rule);
    border-radius: 2px;
    margin-bottom: 6px;
  }
  .member-info { flex: 1; min-width: 0; }
  .member-email {
    font-size: 14px;
    color: var(--ink);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .member-status-badge {
    font-family: 'JetBrains Mono', monospace;
    font-size: 9px;
    padding: 2px 6px;
    border-radius: 2px;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    flex-shrink: 0;
  }
  .member-status-badge.active {
    border: 1px solid var(--moss);
  }
  .member-status-badge.pending {
    border: 1px solid var(--gold);
  }
  .member-status-badge.creator {
    border: 1px solid var(--oxblood);
  }
  .member-actions {
    display: flex; gap: 6px;
    flex-shrink: 0;
  }

  /* Small destructive button — removes a member, deletes a session */
  .btn-tiny-destructive {
    background: none;
    border: 1px solid var(--rule);
    color: var(--ink-faint);
    padding: 4px 10px;
    font-family: 'JetBrains Mono', monospace;
    font-size: 10px;
    cursor: pointer;
    border-radius: 2px;
    transition: all 0.15s ease;
    text-transform: uppercase;
    letter-spacing: 0.05em;
  }
  .btn-tiny-destructive:hover {
    border-color: var(--oxblood);
    color: var(--oxblood);
  }

  /* Invite row — same shape as a member row but for the input form. */
  .invite-error {
    font-family: 'JetBrains Mono', monospace;
    font-size: 11px;
    color: var(--oxblood);
    margin-top: 8px;
    min-height: 14px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
  }

  /* Sessions list */
  .session-row {
    display: flex; align-items: center;
    gap: 12px;
    padding: 12px 14px;
    border: 1px solid var(--rule);
    border-radius: 2px;
    margin-bottom: 6px;
    transition: all 0.15s ease;
  }
  .session-info {
    flex: 1; min-width: 0;
    text-decoration: none;
    color: var(--ink);
  }
  .session-info:hover .session-name { color: var(--oxblood); }
  .session-name {
    font-family: 'Newsreader', serif;
    font-size: 15px;
    font-weight: 500;
    line-height: 1.3;
    margin-bottom: 2px;
    transition: color 0.15s ease;
  }
  .session-meta {
    font-family: 'JetBrains Mono', monospace;
    font-size: 10px;
    color: var(--ink-faint);
    letter-spacing: 0.05em;
    text-transform: uppercase;
    display: flex; gap: 10px;
  }

  /* "New session in this campaign" call-to-action at the bottom of
     the sessions list. */

  /* Sessions list empty state. */
  .sessions-empty {
    font-family: 'Newsreader', serif;
    font-style: italic;
    font-size: 14px;
    color: var(--ink-faint);
    padding: 16px 14px;
    text-align: center;
  }

  /* Mobile: stack the list above the detail. */
  @media (max-width: 720px) {
    .campaign-detail-name {
      font-size: 26px;
    }
  }

  /* ============================================================
     UI revisions: header action buttons, inline session artefacts,
     auth-strip link
     ============================================================ */

  /* Auth-strip in-page link — for "My campaigns". Same look as the
     existing <a> elements in the auth strip, but used for navigation
     between pages rather than auth actions. */
  .auth-strip-link {
    color: rgba(0, 0, 0, 0.85);
    text-decoration: none;
    border: 1px solid rgba(0, 0, 0, 0.2);
    padding: 4px 12px;
    border-radius: 4px;
    transition: background 0.15s;
    font-size: 13px;
  }
  .auth-strip-link:hover {
    background: rgba(0, 0, 0, 0.05);
  }

  /* Campaign-detail header: turn into a flex container so the action
     buttons sit top-right, with the title + meta on the left. */
  .campaign-detail-header {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 24px;
    flex-wrap: wrap;
  }
  .campaign-detail-header-main {
    flex: 1;
    min-width: 0;
  }
  .campaign-detail-actions {
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
    flex-shrink: 0;
    align-items: center;
  }

  .campaign-action-btn {
    background: none;
    border: 1px solid var(--rule);
    color: var(--ink-soft);
    padding: 8px 14px;
    font-family: 'JetBrains Mono', monospace;
    font-size: 11px;
    cursor: pointer;
    border-radius: 2px;
    transition: all 0.2s ease;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    text-decoration: none;
    white-space: nowrap;
    display: inline-block;
  }
  .campaign-action-btn:hover {
    border-color: var(--ink);
    color: var(--ink);
  }
  .campaign-action-btn.primary {
    border-color: var(--ink-soft);
    color: var(--ink);
  }
  .campaign-action-btn.primary:hover {
    background: var(--ink);
    color: var(--paper);
    border-color: var(--ink);
  }
  .campaign-action-btn.destructive {
    color: var(--ink-faint);
    border-color: var(--rule);
    /* visually subordinate — destructive shouldn't be the obvious
       primary action; pushed slightly to the right via margin */
    margin-left: 8px;
  }
  .campaign-action-btn.destructive:hover {
    border-color: var(--oxblood);
    color: var(--oxblood);
  }

  /* Session row — the row itself is an <a> for clickable navigation
     to /?session=<uuid>. Anchor-styling resets ensure it renders like
     the original div-based row. */
  .session-row {
    text-decoration: none;
    color: var(--ink);
    cursor: pointer;
  }

  /* Member row meta — hidden now that we removed the "Joined ..." line.
     Leaving the rule in place lets us re-enable it cheaply if a future
     iteration wants the secondary line back. */

  .campaign-detail-description {
    margin-top: 0.75rem;
    max-width: 60ch;
    color: var(--ink, #2a2622);
    font-size: 0.95rem;
    line-height: 1.55;
  }
  
  .session-description {
    margin-top: 0.35rem;
    max-width: 60ch;
    color: var(--ink-faint);
    font-size: 0.9rem;
    line-height: 1.5;
  }
  /* Mobile tweaks for the new header layout. */
  @media (max-width: 720px) {
    .campaign-detail-header {
      gap: 16px;
    }
    .campaign-detail-actions {
      width: 100%;
    }
  }

  .campaign-detail-description {
  margin-top: 0.75rem;
  /* No max-width — the description should fill the campaign detail
     pane, not sit in a narrow column inside it. The pane's own
     width is the right constraint. */
  color: var(--ink, #2a2622);
  font-size: 0.95rem;
  line-height: 1.55;
}
 
.session-description {
  margin-top: 0.35rem;
  max-width: 60ch;
  color: var(--ink-faint);
  font-size: 0.9rem;
  line-height: 1.5;
}
 
/* ----------- "Where are we up to" collapsible block ----------- */
 
.campaign-status-block {
  margin-top: 1.5rem;
  outline: none;
}
 
.campaign-status-summary {
  cursor: pointer;
  font-family: inherit;
  font-size: 1rem;
  font-weight: 600;
  color: var(--ink, #2a2622);
  padding: 0.4rem 0;
  /* Native disclosure triangle. We tried hiding it and rolling our
     own; the native one is cleaner and matches OS conventions. */
  list-style: revert;
  user-select: none;
}
 
.campaign-status-summary:hover {
  color: var(--accent, #8a4a3c);
}
 
.campaign-status-summary:focus-visible {
  outline: 2px solid var(--accent, #8a4a3c);
  outline-offset: 2px;
  border-radius: 2px;
}
 
.campaign-status-body {
  margin-top: 0.5rem;
  padding-left: 0.25rem;
}
 
.campaign-status-prose {
  color: var(--ink, #2a2622);
  font-size: 0.95rem;
  line-height: 1.55;
  /* Meant to be skim-read by players at the start of a session, so
     line-length stays narrower than the campaign description —
     comfortable for a one-paragraph orientation. */
  max-width: 70ch;
}
 
 

.feed-actions {
  display: flex;
  gap: 8px;
  margin: 12px 0 8px 0;
  flex-wrap: wrap;
}

.feed-help {
  font-size: 0.85em;
  color: var(--ink-faint);
  margin-bottom: 12px;
  line-height: 1.4;
}


/* The feed URL is long; let it scroll horizontally instead of
   wrapping or being clipped. */
#feedUrl {
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.85em;
}

/* Mirror the credit pill/menu pattern for the signed-in user. */
.user-pill-wrap { display: inline-flex; }

.user-pill {
  user-select: none;
  /* If .credit-pill has padding/border/background, copy them here.
     Easier: extend the existing rule instead — see note below. */
}

.user-menu.open { display: block; }


.user-menu button[disabled] {
  opacity: 0.45;
  cursor: not-allowed;
}

/* ===== Contact preferences (account modal, General tab) =====
   Paste into styles.css alongside the existing .account-* rules.
   The radio group uses the standard label-wraps-input pattern so
   clicking the text selects the radio. */

.contact-method-group {
  flex-direction: column;
}



.contact-method-option span {
  color: var(--ink);
  user-select: none;
}
/* =====================================================================
   Email-members modal — page-specific to campaigns.html. Append to
   the end of public/styles.css.

   Uses the existing palette (--paper, --ink, --ink-soft, --ink-faint,
   --rule, --oxblood, --moss, --gold). No new colour tokens.
   ===================================================================== */

/* Recipient list — checkboxes one per row, with the email greyed
   alongside the display name. Constrained-height with scroll so a
   campaign with many members doesn't blow out the modal. */
.email-recipients {
  max-height: 220px;
  overflow-y: auto;
  border: 1px solid var(--rule);
  border-radius: 2px;
  padding: 8px 12px;
  background: rgba(255, 252, 245, 0.4);
}
.email-recipient-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 6px 0;
  cursor: pointer;
  font-size: 14px;
  color: var(--ink);
}
.email-recipient-row input[type="checkbox"] {
  flex-shrink: 0;
  margin: 0;
}
.email-recipient-label {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.email-recipient-email {
  color: var(--ink-faint);
  font-family: 'JetBrains Mono', monospace;
  font-size: 12px;
  margin-left: 4px;
}
.email-recipients-empty {
  font-family: 'Newsreader', serif;
  font-style: italic;
  color: var(--ink-faint);
  font-size: 14px;
  padding: 6px 0;
}

/* Select-all / Clear — small link-style buttons under the recipient
   list. Match the .modal-sub typography. */
.email-recipients-controls {
  margin-top: 8px;
  display: flex;
  align-items: center;
  gap: 8px;
  font-family: 'JetBrains Mono', monospace;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
}
.email-link-btn {
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  color: var(--ink-soft);
  font: inherit;
  text-transform: inherit;
  letter-spacing: inherit;
  transition: color 0.15s ease;
}
.email-link-btn:hover {
  color: var(--ink);
  text-decoration: underline;
}
.email-link-sep {
  color: var(--rule);
}

/* "Send me a copy" checkbox row. */
.email-checkbox-row {
  display: flex;
  align-items: center;
  gap: 10px;
  cursor: pointer;
  font-size: 14px;
  color: var(--ink-soft);
}
.email-checkbox-row input[type="checkbox"] {
  margin: 0;
}

/* Result message — three states. Borders and tints in the existing
   palette so it feels native to the modal. */
.email-result-success {
  background: rgba(90, 110, 63, 0.08);
  border-left: 2px solid var(--moss);
  padding: 10px 14px;
  color: var(--ink-soft);
  font-family: 'Newsreader', serif;
  font-style: italic;
  font-size: 14px;
  border-radius: 0 2px 2px 0;
}
.email-result-partial {
  background: rgba(184, 137, 58, 0.08);
  border-left: 2px solid var(--gold);
  padding: 10px 14px;
  color: var(--ink-soft);
  font-family: 'Newsreader', serif;
  font-style: italic;
  font-size: 14px;
  border-radius: 0 2px 2px 0;
}
.email-result-error {
  background: rgba(139, 44, 44, 0.06);
  border-left: 2px solid var(--oxblood);
  padding: 10px 14px;
  color: var(--ink-soft);
  font-family: 'Newsreader', serif;
  font-style: italic;
  font-size: 14px;
  border-radius: 0 2px 2px 0;
}
/* =====================================================================
   Session-reminder modal — append to the end of public/styles.css.

   Uses the existing palette (--paper, --ink, --ink-soft, --ink-faint,
   --rule, --oxblood, --moss, --gold). No new colour tokens.
   ===================================================================== */

/* Action link inside the "Where are we up to" block. Sits next to
   "View latest journal"; same baseline so the two read as a pair. */
.campaign-status-actions {
  margin-top: 0.6rem;
  display: flex;
  gap: 1.25rem;
  align-items: baseline;
  flex-wrap: wrap;
}
.campaign-status-action {
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  font: inherit;
  font-size: 0.9rem;
  text-decoration: underline;
}
.campaign-status-action:hover {
  text-decoration: none;
}
.campaign-status-action:disabled {
  color: var(--ink-faint);
  cursor: not-allowed;
  text-decoration: none;
}

/* Result message — three states, matching the modal-sub typography. */
.reminder-result-success {
  background: rgba(90, 110, 63, 0.08);
  border-left: 2px solid var(--moss);
  padding: 10px 14px;
  color: var(--ink-soft);
  font-family: 'Newsreader', serif;
  font-style: italic;
  font-size: 14px;
  border-radius: 0 2px 2px 0;
}
.reminder-result-partial {
  background: rgba(184, 137, 58, 0.08);
  border-left: 2px solid var(--gold);
  padding: 10px 14px;
  color: var(--ink-soft);
  font-family: 'Newsreader', serif;
  font-style: italic;
  font-size: 14px;
  border-radius: 0 2px 2px 0;
}
.reminder-result-error {
  background: rgba(139, 44, 44, 0.06);
  border-left: 2px solid var(--oxblood);
  padding: 10px 14px;
  color: var(--ink-soft);
  font-family: 'Newsreader', serif;
  font-style: italic;
  font-size: 14px;
  border-radius: 0 2px 2px 0;
}
/* ===== Invitations =====
   (cleanup pass 3) Trimmed: the standalone invitations modal
   (.invitations-modal / -list / -empty) is retired — invitation rows
   now render inside the merged notifications modal (see
   notifications-modal.js). What survives here is the .invite-badge
   circle next to the user-pill email and the Notifications button.
   Hidden by JS when count === 0; the [hidden] selector below ensures
   the attribute actually wins over the .invite-badge display rule. */
 
.invite-badge {
  align-items: center;
  justify-content: center;
  height: 18px;
  font-weight: 600;
  letter-spacing: 0;
}
 
/* Required: my .invite-badge rule above sets display: inline-flex,
   which would otherwise beat the user-agent stylesheet's
   [hidden] { display: none }. Without this, hidden="" on the badge
   leaves it visible. */
.invite-badge[hidden] {
  display: none;
}
 
/* When the badge sits inside the user-menu button (which itself
   uses a flex layout to put the count after the label), nudge the
   margin so the visual gap matches the dropdown rhythm. */
.user-menu button .invite-badge {
  margin-left: 8px;
}
 
/* ===== Notifications =====
   Reuses .invite-badge for the shared badge styling (both invitations
   and notifications use the same circle). Layout/sizing rules for the
   modal and rows live in the dark-layer restyle further down.
 
   Per the design decision, "complete" notifications are a different
   tone than "failed" ones — colour-code per-kind via .kind-*
   classes on each row.
*/
 
/* Required: stops display:none being overridden by .notif-* layout
   rules. Same pattern as the .invite-badge[hidden] fix. */
 
.notification-message {
  font-size: 14px;
  color: var(--ink);
  line-height: 1.4;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
 
.notification-message strong {
  font-weight: 600;
  color: var(--ink);
}
 
.notification-message em {
  font-family: 'Newsreader', serif;
  font-style: italic;
  color: var(--ink-faint);
  font-weight: normal;
}
 
/* Per-kind status prefix colour. Subtle — the row remains primarily
   typography, the colour just hints at outcome. */
.notification-status {
  font-family: 'JetBrains Mono', monospace;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  margin-right: 6px;
}
 
.kind-self_running   .notification-status { color: var(--ink-faint); }
.kind-self_complete  .notification-status { color: var(--moss); }
.kind-self_failed    .notification-status { color: var(--oxblood); }
.kind-member_complete .notification-status { color: var(--moss); }
 
.notification-meta {
  margin-top: 3px;
  font-size: 12px;
  color: var(--ink-faint);
  font-family: 'JetBrains Mono', monospace;
}
 
.notification-link {
  font-size: 13px;
  color: var(--oxblood);
  text-decoration: none;
  font-family: 'JetBrains Mono', monospace;
  white-space: nowrap;
}
 
.notification-link:hover {
  text-decoration: underline;
}

/*
  proto.css — Literary Terminal palette overrides + chrome + hero.

  Loaded AFTER /styles.css. Variable-name collisions with styles.css
  are intentional: redefining --gold, --moss, --oxblood at :root
  cascades to every rule in styles.css that consumes them, so
  partial-emitted markup (modals, ledger rows, etc.) picks up the
  dark palette without per-rule overrides. Where the existing
  styles.css uses a hex literal instead of a variable, we override
  the specific rule explicitly below.

  Scope: step 1 of the proto migration — chrome (header row) + hero
  only. Modal restyling, panel restyling, and body section markup
  arrive in later steps; the modals will look visually inconsistent
  against this palette until step 6 lands.

  Selector strategy: every chrome rule targets the IDs/classes the
  existing partials/auth-strip.html emits (#creditPill, #userPill,
  #signedOutView, etc.) — no markup changes inside the partial.
  proto/index.html just wraps {{> auth-strip }} in a .chrome row
  alongside the wordmark.
*/

/* ============================================================
   Palette: Literary Terminal
   ============================================================ */
:root {
  /* color-scheme: dark tells the browser to render native widgets
     (date picker, scrollbars, autofill) using their dark variants.
     Without this, <input type="date"> displays a near-black calendar
     icon that's invisible against the panel background. */
  color-scheme: dark;

  /* Surfaces */
  --bg: #0b0907;
  --panel: #12100d;
  --panel-soft: #171410;
  --panel-deep: #08070544;

  /* Text */
  --text: #efe3c2;
  --text-muted: #b7ab8a;
  --text-dim: #7a715b;

  /* Accents — these names collide with styles.css on purpose. */
  --gold: #c6a56b;
  --gold-bright: #e2c78f;
  --gold-soft: #c6a56b22;

  --oxblood: #5a1f1f;
  --oxblood-bright: #7b2f2f;

  --moss: #4f674a;
  --moss-bright: #6a8862;

  /* Lines & shadows */
  --border: rgba(198, 165, 107, 0.22);
  --border-strong: rgba(198, 165, 107, 0.45);
  --border-faint: rgba(198, 165, 107, 0.1);
  --shadow: rgba(0, 0, 0, 0.55);

  /* ----------------------------------------------------------
     Production-token aliases (step 7).

     styles.css's campaigns and modal rules reference these token
     names directly (--paper, --ink, --ink-soft, --ink-faint,
     --rule, --paper-warm). Redefining them here so they inherit
     the dark palette restyles 90% of the campaigns page via
     cascade — no per-rule overrides needed.

     proto.css's own rules don't reference these names; they use
     the proto-native tokens above (--bg, --text, --border, etc.).
     The two sets coexist: production-shaped rules from styles.css
     keep working, proto-shaped rules from proto.css keep working,
     and both render dark.
     ---------------------------------------------------------- */
  --paper: var(--bg);
  --paper-warm: var(--panel);
  --ink: var(--text);
  --ink-soft: var(--text-muted);
  --ink-faint: var(--text-dim);
  --rule: var(--border);

  /* Typography */
  --font-display: "Newsreader", Georgia, serif;
  --font-mono: "JetBrains Mono", ui-monospace, monospace;

  /* Re-aliasing for styles.css consumers that use these names:
     styles.css's --paper, --ink, --rule, etc. are overridden below
     where each is referenced, since some are used in places where
     a dark-mode value would not work (e.g. paper-backed modals).
     The body and chrome regions get full dark treatment; modals
     keep their existing pale paper until step 6 restyles them. */
}

/* ============================================================
   Reset + base — override styles.css body background/colour.
   ============================================================ */
body {
  background: var(--bg);
  background-image: none;
  color: var(--text);
  font-family: var(--font-display);
  font-weight: 400;
  line-height: 1.5;
  min-height: 100vh;
}

/* (cleanup pass 1) The body::before { content: none } kill rule that
   used to sit here is gone — the parchment noise overlay it neutralised
   has been deleted at the top of the file. */

.page {
  max-width: 1400px;
  margin: 0 auto;
  position: relative;
}

/* ============================================================
   Chrome row — wordmark left, auth-strip right, over the hero.
   ============================================================ */
.chrome {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  z-index: 10;
  padding: 1.5rem 2rem;
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  max-width: 1400px;
  margin: 0 auto;
}

.wordmark {
  font-family: var(--font-display);
  font-size: 1.5rem;
  letter-spacing: 0.06em;
  color: var(--text);
  text-transform: uppercase;
  font-weight: 500;
  /* No opacity: 0 — the wordmark is in HTML, not in the PNG. */
}
.wordmark .ital {
  font-style: italic;
  color: var(--gold);
}

/* ============================================================
   Auth strip — restyles the partial's own elements.

   The partial emits:
     .auth-strip > #signedOutView | #signedInView
     #signedInView > .auth-strip-link.a (My campaigns)
                   > #notificationsButton.auth-strip-link
                   > .credit-pill-wrap > .credit-pill#creditPill
                                       > .credit-menu#creditMenu
                   > .user-pill-wrap   > .user-pill#userPill
                                       > .user-menu#userMenu

   The dropdowns partial toggles `.open` on the wrap elements,
   which is the same mechanism the prototype's .pill-wrap.open
   pattern uses — no JS changes needed.
   ============================================================ */
.auth-strip {
  display: flex;
  align-items: center;
  gap: 0.85rem;
  padding: 0.6rem 0.85rem 0.6rem 1.1rem;
  background:
    radial-gradient(ellipse at center,
      rgba(8, 7, 6, 0.55) 0%,
      rgba(8, 7, 6, 0.35) 70%,
      transparent 100%);
  border-radius: 999px;
}

/* signed-out view: single "Sign in" link, styled like a pill link */
#signedOutView a {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text);
  text-decoration: none;
  padding: 0.5rem 0.85rem;
  border: 1px solid var(--border);
  border-radius: 4px;
  transition: border-color 180ms ease, color 180ms ease;
}
#signedOutView a:hover {
  border-color: var(--border-strong);
  color: var(--gold-bright);
}

/* signed-in view: the inline span containing links + pills */
#signedInView {
  display: flex;
  align-items: center;
  gap: 0.85rem;
}

/* "My campaigns" + "Notifications" — uppercase mono links.

   Why the .auth-strip prefix on these selectors: styles.css has
   a rule `.auth-strip a { color: rgba(0,0,0,0.85); ... }` at
   specificity (0,0,1,1) — which beats the bare `.auth-strip-link`
   selector at (0,0,1,0). On the dark page that translates to
   near-black text on near-black background, invisible until
   hover (where styles.css's hover changes background slightly).
   The .auth-strip prefix bumps these to (0,0,2,0) so they win
   the cascade. */
.auth-strip .auth-strip-link {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text);
  text-decoration: none;
  background: none;
  border: none;
  cursor: pointer;
  padding: 0;
  transition: color 180ms ease;
  position: relative;
}
.auth-strip .auth-strip-link:hover { color: var(--gold-bright); }

/* Notifications badge — tiny dot/count over the right of the link */
.invite-badge {
  display: inline-block;
  min-width: 1.2em;
  padding: 0 0.4em;
  margin-left: 0.4em;
  font-size: 0.65rem;
  font-family: var(--font-mono);
  background: var(--oxblood-bright);
  color: var(--text);
  border-radius: 999px;
  line-height: 1.4;
  vertical-align: middle;
}

/* ============================================================
   Credit pill — partial emits .credit-pill#creditPill inside
   .credit-pill-wrap, with .credit-menu#creditMenu as the dropdown.
   ============================================================ */
.credit-pill-wrap {
  position: relative;
}

.credit-pill {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem 0.85rem;
  background: rgba(198, 165, 107, 0.1);
  border: 1px solid var(--border);
  border-radius: 4px;
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text);
  cursor: pointer;
  transition: border-color 180ms ease, background 180ms ease;
}
.credit-pill:hover {
  border-color: var(--border-strong);
  background: rgba(198, 165, 107, 0.18);
}
.credit-pill.low {
  border-color: rgba(160, 40, 40, 0.7);
  color: #d97d7d;
}
.credit-pill .credits-num {
  color: var(--gold-bright);
  font-weight: 500;
}
.credit-pill .add-icon {
  color: var(--gold);
  font-size: 0.7rem;
}

/* Credit dropdown menu — partial emits .credit-menu#creditMenu */
.credit-menu {
  position: absolute;
  top: calc(100% + 0.5rem);
  right: 0;
  min-width: 160px;
  background: rgba(18, 16, 13, 0.96);
  border: 1px solid var(--border-strong);
  border-radius: 4px;
  padding: 0.4rem 0;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.6);
  display: none;
  z-index: 20;
}
/* The dropdowns partial uses `.open` on the wrap to reveal the menu. */
.credit-pill-wrap.open .credit-menu { display: block; }

.credit-menu button {
  display: block;
  width: 100%;
  text-align: left;
  padding: 0.55rem 1rem;
  font-family: var(--font-display);
  font-size: 0.9rem;
  color: var(--text);
  background: none;
  border: none;
  cursor: pointer;
  transition: background 180ms ease, color 180ms ease;
}
.credit-menu button:hover {
  background: rgba(198, 165, 107, 0.1);
  color: var(--gold-bright);
}

/* ============================================================
   User pill — partial emits .user-pill#userPill inside
   .user-pill-wrap, with .user-menu#userMenu as the dropdown.

   The partial shows "Signed in as <email>"; we keep that, just
   restyle it. Avatar slot from the prototype isn't in the partial
   (the partial uses the email rather than initials), so we don't
   add one.
   ============================================================ */
.user-pill-wrap {
  position: relative;
}

.user-pill {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.5rem 0.75rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: rgba(198, 165, 107, 0.06);
  font-family: var(--font-mono);
  font-size: 0.7rem;
  letter-spacing: 0.05em;
  color: var(--text);
  cursor: pointer;
  transition: border-color 180ms ease, background 180ms ease;
}
.user-pill:hover {
  border-color: var(--border-strong);
  background: rgba(198, 165, 107, 0.12);
}
.user-pill .user-email {
  color: var(--gold-bright);
  font-weight: 500;
  max-width: 16ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.user-pill .add-icon {
  color: var(--gold);
  font-size: 0.7rem;
}

/* User dropdown menu */
.user-menu {
  position: absolute;
  top: calc(100% + 0.5rem);
  right: 0;
  min-width: 160px;
  background: rgba(18, 16, 13, 0.96);
  border: 1px solid var(--border-strong);
  border-radius: 4px;
  padding: 0.4rem 0;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.6);
  display: none;
  z-index: 20;
}
.user-pill-wrap.open .user-menu { display: block; }

/* ----- Mobile nav burger -----
   Hidden on desktop; shown <=600px where the My-campaigns /
   Notifications text links are display:none. Dropdown behaviour
   comes from dropdowns.js (same machinery as the pills); the menu
   itself reuses .user-menu, with .nav-burger-menu only adjusting
   width. The badge mirrors the top-level Notifications badge. */
.nav-burger-wrap {
  position: relative;
  display: none;
}
.nav-burger {
  position: relative;
  background: transparent;
  border: 1px solid var(--border);
  border-radius: 4px;
  color: var(--text-muted);
  font-size: 1rem;
  line-height: 1;
  padding: 0.4rem 0.55rem;
  cursor: pointer;
}
.nav-burger:hover {
  color: var(--gold-bright);
  border-color: var(--border-strong);
}
.nav-burger .nav-burger-badge {
  position: absolute;
  top: -8px;
  right: -8px;
  margin-left: 0;
}
.nav-burger-menu {
  min-width: 180px;
}
.nav-burger-menu .menu-sep {
  border-top: 1px solid var(--border-faint);
  margin: 0.35rem 0;
}

.user-menu a,
.user-menu button {
  display: block;
  width: 100%;
  text-align: left;
  padding: 0.55rem 1rem;
  font-family: var(--font-display);
  font-size: 0.9rem;
  color: var(--text);
  text-decoration: none;
  background: none;
  border: none;
  cursor: pointer;
  transition: background 180ms ease, color 180ms ease;
}
.user-menu a:hover,
.user-menu button:hover {
  background: rgba(198, 165, 107, 0.1);
  color: var(--gold-bright);
}

/* ============================================================
   Hero — painted PNG background, centred text overlay.

   PNG: public/proto/assets/hero.png (1024 × 480 = 32:15).
   The overlay heading sits over the compass rose in the centre
   of the image; the compass reads as background ornament behind
   the text.
   ============================================================ */
.hero {
  position: relative;
  width: 100%;
  background-image: url('/assets/hero.png');
  background-size: cover;
  background-position: center top;
  background-repeat: no-repeat;
  /* Aspect ratio matches the cropped PNG. If hero.png is regenerated
     at different dimensions, update this. */
  aspect-ratio: 1024 / 480;
  margin-bottom: 3rem;
  overflow: hidden;
  isolation: isolate;
}

.hero-text {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  text-align: center;
  padding: 4rem 2rem 2rem;
  /* Visible — heading is in HTML, not baked into the PNG. */
}

.hero-text h1 {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(2.5rem, 6vw, 4.5rem);
  line-height: 1.05;
  letter-spacing: -0.02em;
  margin: 0 0 0.6rem;
  color: var(--text);
  /* Soft drop shadow keeps the heading legible against the
     compass rose's gold ring without darkening the image. */
  text-shadow: 0 2px 16px rgba(0, 0, 0, 0.75),
               0 0 32px rgba(0, 0, 0, 0.5);
}
.hero-text p {
  font-family: var(--font-display);
  font-size: 1.15rem;
  color: var(--text-muted);
  max-width: 32ch;
  margin: 0 auto;
  font-style: italic;
  font-weight: 300;
  text-shadow: 0 1px 8px rgba(0, 0, 0, 0.8);
}

/* ============================================================
   Overrides for styles.css interactions
   ============================================================

   Two real bugs in step 1 trace back to the production styles.css:

   1) styles.css applies `position: fixed; top: 16px; right: 24px`
      to .auth-strip. That pulls the strip out of normal flow, so
      our .chrome flex row never gets a chance to place it. The
      pills end up pinned to the viewport corner regardless of
      what .chrome does. Override to static so the flex row
      controls placement again.

   2) Author CSS that sets `display: flex` (or any non-none
      display) on .auth-strip's children defeats the [hidden]
      attribute that auth.js uses to toggle signedInView /
      signedOutView. Result: both views render at once. The
      [hidden] attribute is the standard hide mechanism for this
      partial (auth.js does `$('signedInView').hidden = true/false`),
      so it MUST win. !important is justified here — the whole
      contract of [hidden] is "be hidden". */

.chrome .auth-strip {
  position: static;
}

.chrome .auth-strip [hidden],
.chrome .auth-strip[hidden] {
  display: none !important;
}

/* ============================================================
   Step 2 — Transcript panel
   ============================================================

   Panel shell — ported from the prototype's .panel rules.
   Numbered header (1, 2, 3, 4) with title + subtitle. */

.proto-main {
  max-width: 1200px;
  margin: 0 auto;
  padding: 0 2rem 4rem;
}

.panel {
  background: var(--panel);
  border: 1px solid var(--border-faint);
  border-radius: 6px;
  padding: 1.5rem 2rem;
  margin-bottom: 1.25rem;
  position: relative;
  box-shadow:
    inset 0 0 0 1px rgba(255, 255, 255, 0.015),
    0 4px 24px rgba(0, 0, 0, 0.35);
}

/* Collapse: hide the body of a panel marked .collapsed.
   The header stays visible (it's the click target to re-expand).
   Chevron rotates 180deg to point up when expanded; default
   markup orientation is "down" (collapsed).

   Layout: number badge — title — flexible rule space — summary
   text — chevron. All in one row, vertically centred. The
   subtitle line is gone (deliberately — redundant with the
   title for this design). */
.panel-header {
  display: flex;
  align-items: center;
  gap: 1rem;
  margin-bottom: 1.25rem;
  cursor: pointer;
  user-select: none;
}
.panel-header-text {
  flex-shrink: 0;
}
.panel-title {
  font-family: var(--font-mono);
  font-size: 0.95rem;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--text);
  margin: 0;
}
/* Summary text — visible in both expanded and collapsed states.
   Italic Newsreader, muted, sits between the title and the chevron.
   When collapsed it's the only readable indicator of what's set;
   when expanded it's a quiet reminder. */
.panel-summary {
  flex: 1;
  text-align: right;
  font-family: var(--font-display);
  font-style: italic;
  font-size: 0.95rem;
  color: var(--text-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
}
.panel-summary:empty {
  /* Don't reserve space for an empty summary — keep the row tight. */
  display: none;
}
.panel-chevron {
  width: 16px;
  height: 16px;
  flex-shrink: 0;
  color: var(--text-muted);
  transform: rotate(180deg);
  transition: transform 200ms ease, color 200ms ease;
}
.panel-header:hover .panel-chevron {
  color: var(--gold-bright);
}
.panel.collapsed .panel-chevron {
  transform: rotate(0deg);
}
.panel.collapsed .panel-body {
  display: none;
}
.panel.collapsed {
  /* Tighter padding when collapsed so the panel reads as a
     compact "tap to expand" strip rather than a full panel. */
  padding-top: 1.25rem;
  padding-bottom: 1.25rem;
}
.panel.collapsed .panel-header {
  margin-bottom: 0;
}

/* Placeholder body for the step-4-stub output panel. Removed
   when step 4 lands. */

.panel-number {
  flex-shrink: 0;
  width: 36px; height: 36px;
  border: 1px solid var(--border-strong);
  border-radius: 50%;
  display: grid; place-items: center;
  font-family: var(--font-mono);
  font-size: 0.85rem;
  color: var(--gold);
  background: var(--panel-deep);
  position: relative;
}
/* Decorative compass-rose notches around the number badge. */
.panel-number::before,
.panel-number::after {
  content: "";
  position: absolute;
  background: var(--border-strong);
}
.panel-number::before {
  top: -3px; bottom: -3px; left: 50%;
  width: 1px;
  transform: translateX(-50%);
}
.panel-number::after {
  left: -3px; right: -3px; top: 50%;
  height: 1px;
  transform: translateY(-50%);
}
/* .panel-title and .panel-subtitle: title is defined above in the
   panel-header block; subtitle is intentionally removed from the
   design (redundant with title + summary). */

/* ============================================================
   Transcript zone — single surface, two states.
   ============================================================

   Empty / paste state: textarea visible, hint bar at the bottom
   with "Browse for a file" + accepted-formats hint.

   Has-file state (#dropZone.has-file): textarea hidden, file-info
   card visible (filename, size, words, format badge, Replace).

   Drag state (#dropZone.dragging): border brightens.

   Error state (#dropZone.error): transient red flash; auto-clears
   after 4s. */


.transcript-zone {
  position: relative;
  min-height: 240px;
  background:
    radial-gradient(circle at 50% 30%, rgba(198, 165, 107, 0.04), transparent 65%),
    rgba(8, 7, 6, 0.92);
  border: 1px dashed var(--border);
  border-radius: 6px;
  padding: 1.25rem 1.25rem 3rem;
  /* bottom padding leaves room for the absolute-positioned hint bar */
  transition: border-color 180ms ease, background 180ms ease;
}
.transcript-zone:hover {
  border-color: var(--border-strong);
}
.transcript-zone.dragging {
  border-color: var(--gold-bright);
  border-style: solid;
  background:
    radial-gradient(circle at 50% 30%, rgba(198, 165, 107, 0.12), transparent 65%),
    rgba(12, 10, 8, 0.95);
}
.transcript-zone.processing {
  opacity: 0.6;
  pointer-events: none;
}
.transcript-zone.error {
  border-color: rgba(160, 40, 40, 0.7);
  border-style: solid;
}

/* Paste textarea — fills the zone in the empty state. */
/* Hide the textarea when a file is loaded — the file-info card
   takes its place inside the same zone. */

/* Hint bar at the bottom of the empty zone: Browse button + format hint. */


/* ============================================================
   File-info card — the has-file state of the transcript zone.
   ============================================================

   Renders into #dropZoneInner by renderAccepted() in the inline
   init script. Format chip on the left, name + word/size + format
   badge in the middle, Replace button on the right. */

.transcript-file-info {
  /* Hidden until JS populates it AND .has-file is set on the zone */
}

.file-info {
  display: flex;
  align-items: center;
  gap: 1rem;
  padding: 0.5rem 0;
}

.transcript-error {
  font-family: var(--font-mono);
  font-size: 0.78rem;
  letter-spacing: 0.04em;
  color: #d97d7d;
  padding: 0.85rem 0;
}

/* ============================================================
   Button row — primary action below each panel.
   ============================================================

   Used by the Continue / Submit / etc. action at the bottom of
   each step. Right-aligned to match the prototype's intent that
   the panel reads top-down and the action sits at the end. */


.button {
  display: inline-flex;
  align-items: center;
  gap: 0.65rem;
  padding: 0.85rem 1.4rem;
  border-radius: 4px;
  font-family: var(--font-mono);
  font-size: 0.78rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  transition: border-color 180ms ease, color 180ms ease, background 180ms ease;
  border: 1px solid var(--border);
  color: var(--text);
  background: transparent;
  cursor: pointer;
}
.button:hover:not(:disabled) {
  border-color: var(--gold-bright);
  color: var(--gold-bright);
  background: rgba(198, 165, 107, 0.08);
}
.button:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

/* ============================================================
   Step 3 — Session panel
   ============================================================

   Form fields shared with future panels: .field (single column),
   .field-row (two columns at desktop, collapses on mobile),
   .field-label (uppercase mono), .field-opt (faded subtle suffix),
   .select / input / textarea base styles in the dark palette.

   Baseline preview block: read-only campaign_setting.md content
   in a bordered box, with the "Edit" button to the right of the
   label. Empty state shows a placeholder string. */

.field {
  margin-bottom: 1.25rem;
}

.field-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1.25rem;
  margin-bottom: 1.25rem;
}
.field-row > .field {
  margin-bottom: 0;
}

.field-label {
  display: block;
  font-family: var(--font-mono);
  font-size: 0.7rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-muted);
  margin-bottom: 0.5rem;
}
.field-opt {
  text-transform: none;
  letter-spacing: 0;
  color: var(--text-dim);
  font-style: italic;
  font-family: var(--font-display);
}

/* Selects, text inputs, date inputs — uniform dark-palette look.
   The browser-native date picker can't be fully restyled, but
   the surrounding chrome can match. */
.select,
.field input[type="text"],
.field input[type="date"],
.field input[type="email"],
.field input[type="tel"],
.field-textarea {
  width: 100%;
  background: rgba(8, 7, 6, 0.6);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 0.65rem 0.85rem;
  color: var(--text);
  font-family: var(--font-mono);
  font-size: 0.85rem;
  line-height: 1.5;
  transition: border-color 180ms ease, background 180ms ease;
  outline: none;
}
.select {
  /* Native chevron is hidden via appearance:none + custom SVG.
     Keep the SVG mono'd in --text-muted to match the field set. */
  appearance: none;
  -webkit-appearance: none;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 14 14' fill='none' stroke='%23b7ab8a' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'><polyline points='3 5 7 9 11 5'/></svg>");
  background-repeat: no-repeat;
  background-position: right 0.85rem center;
  padding-right: 2.25rem;
}
.select:focus,
.field input:focus,
.field-textarea:focus {
  border-color: var(--border-strong);
  background: rgba(8, 7, 6, 0.85);
}

.field-textarea {
  resize: vertical;
  min-height: 80px;
  font-family: var(--font-mono);
}
.field-textarea::placeholder {
  color: var(--text-dim);
  font-style: italic;
}

/* Campaign baseline preview block. */
.campaign-baseline-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 0.5rem;
  gap: 1rem;
}
.campaign-baseline-header .field-label {
  margin-bottom: 0;
  flex: 1;
}

.baseline-edit-btn {
  font-family: var(--font-mono);
  font-size: 0.65rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-muted);
  background: rgba(198, 165, 107, 0.06);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 0.35rem 0.75rem;
  cursor: pointer;
  transition: border-color 180ms ease, color 180ms ease, background 180ms ease;
}
.baseline-edit-btn:hover {
  border-color: var(--border-strong);
  color: var(--gold-bright);
  background: rgba(198, 165, 107, 0.12);
}

.campaign-baseline-preview {
  background: rgba(8, 7, 6, 0.5);
  border: 1px solid var(--border-faint);
  border-radius: 4px;
  padding: 0.85rem 1rem;
  min-height: 80px;
  max-height: 240px;
  overflow-y: auto;
}
.baseline-empty {
  color: var(--text-dim);
  font-family: var(--font-display);
  font-style: italic;
  font-size: 0.9rem;
}
.baseline-content {
  margin: 0;
  font-family: var(--font-mono);
  font-size: 0.78rem;
  line-height: 1.55;
  color: var(--text-muted);
  white-space: pre-wrap;
  word-wrap: break-word;
}

/* ============================================================
   Campaign modal (proto-local) — per-modal overrides only
   ============================================================

   The base modal styling (palette, typography, buttons) is now
   global — see the "Step 6 — Shared modal restyle" block below.
   This block keeps only the things unique to the campaign modal:
   the wider container (to fit the setting editor) and the
   readonly-input visual state for edit mode. */

#campaignModal .modal {
  /* Wider than the default modal — the setting editor needs
     enough width for natural line lengths (~80 monospace
     characters per line). The base .modal width is sized for
     narrow forms (e.g. top-up tiers, account fields). */
  width: auto;
  max-width: 720px;
}
#campaignModal input[readonly] {
  opacity: 0.65;
  cursor: default;
}

/* ============================================================
   Step 4 — Output panel
   ============================================================

   Two-card grid (Narrative & Journal, Podcast Episode). Each
   card: check indicator (top-right), icon (top-left), title +
   desc + cost. Click toggles .selected.

   Sub-options blocks (.sub-options) live below the grid; visible
   only when .visible is added by JS based on which cards are
   active. */

.output-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1rem;
  margin-bottom: 1.5rem;
}

.output-card {
  position: relative;
  background: rgba(8, 7, 6, 0.55);
  border: 1px solid var(--border-faint);
  border-radius: 6px;
  padding: 1.25rem 1.4rem 1.25rem 4.5rem;
  /* left padding leaves room for the absolute-positioned icon */
  cursor: pointer;
  transition: border-color 180ms ease, background 180ms ease;
  min-height: 110px;
}
.output-card:hover {
  border-color: var(--border-strong);
  background: rgba(12, 10, 8, 0.7);
}
.output-card.selected {
  border-color: var(--gold);
  background:
    radial-gradient(circle at 30% 50%, rgba(198, 165, 107, 0.08), transparent 70%),
    rgba(14, 11, 8, 0.85);
}

/* Check indicator in the top-right corner. Empty circle outline
   when unselected; filled disc with check when selected. */
.output-check {
  position: absolute;
  top: 0.85rem;
  right: 0.85rem;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  border: 1px solid var(--border);
  display: grid;
  place-items: center;
  color: transparent;
  background: transparent;
  transition: border-color 180ms ease, background 180ms ease, color 180ms ease;
}
.output-card.selected .output-check {
  border-color: var(--gold);
  background: var(--gold);
  color: var(--bg);
}

/* Icon in the top-left, takes the carved-out left margin. */
.output-icon {
  position: absolute;
  top: 1.25rem;
  left: 1.4rem;
  width: 32px;
  height: 32px;
  color: var(--gold);
  opacity: 0.85;
}
.output-card.selected .output-icon {
  opacity: 1;
  color: var(--gold-bright);
}

.output-info {
  /* sits in the right-of-icon space */
}
.output-title {
  font-family: var(--font-display);
  font-size: 1.15rem;
  font-weight: 500;
  color: var(--text);
  margin: 0 0 0.35rem;
}
.output-desc {
  font-family: var(--font-display);
  font-size: 0.92rem;
  color: var(--text-muted);
  line-height: 1.4;
  margin: 0 0 0.75rem;
  font-style: italic;
}
.output-cost {
  font-family: var(--font-mono);
  font-size: 0.7rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--gold);
}

/* ============================================================
   Sub-options block — animations under Narrative, format + notes
   under Podcast. Visible only when the parent card is selected
   (.visible class is set by JS, not present in default markup).
   ============================================================ */

.sub-options {
  background: rgba(8, 7, 6, 0.4);
  border: 1px solid var(--border-faint);
  border-radius: 6px;
  padding: 1.1rem 1.4rem 1.25rem;
  margin-bottom: 1rem;
  display: none;
}
.sub-options.visible {
  display: block;
}
.sub-options-label {
  font-family: var(--font-mono);
  font-size: 0.7rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-muted);
  margin-bottom: 0.85rem;
}

/* Animation toggle rows — checkbox + title + desc + cost in one
   row, label is the click target so the whole row toggles. */
.sub-toggle {
  display: flex;
  align-items: flex-start;
  gap: 0.85rem;
  padding: 0.65rem 0;
  cursor: pointer;
  user-select: none;
  border-top: 1px solid var(--border-faint);
}
.sub-toggle:first-of-type {
  border-top: none;
}
.sub-toggle input[type="checkbox"] {
  flex-shrink: 0;
  width: 16px;
  height: 16px;
  margin-top: 0.15rem;
  cursor: pointer;
  /* color-scheme: dark on :root gives a usable native checkbox;
     no custom widget needed. */
}
.sub-toggle-text {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
}
.sub-toggle-title {
  font-family: var(--font-display);
  font-size: 1rem;
  color: var(--text);
}
.sub-toggle-desc {
  font-family: var(--font-display);
  font-size: 0.85rem;
  color: var(--text-muted);
  font-style: italic;
  line-height: 1.4;
}
.sub-toggle-cost {
  flex-shrink: 0;
  font-family: var(--font-mono);
  font-size: 0.7rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--gold);
  align-self: center;
}

/* ============================================================
   Podcast types — small grid of art tiles, click to select one.
   Single-select (radio-like behaviour).

   Option A layout: art on top, name + description below, radio
   indicator in the bottom-right corner. Tiles are ~280px wide
   minimum so descriptions read naturally without wrapping at
   every other word.
   ============================================================ */

.podcast-types {
  display: grid;
  /* auto-fit (not auto-fill) collapses unused tracks, and the
     420px cap stops the tiles stretching to fill the row — so
     with two formats in the catalog the pair sits centered in
     the frame rather than left-aligned beside an empty track. */
  grid-template-columns: repeat(auto-fit, minmax(280px, 420px));
  justify-content: center;
  gap: 1rem;
  margin-bottom: 0.5rem;
}
.podcast-type {
  /* Production styles.css sets `width: 96px` and a fixed
     square art block on these tiles. Override to auto so the
     grid track determines the width. Without this, tiles render
     as narrow 96px columns regardless of minmax. */
  width: auto;
  position: relative;
  background: rgba(8, 7, 6, 0.6);
  border: 1px solid var(--border-faint);
  border-radius: 6px;
  padding: 0.85rem 0.9rem 2.5rem;
  /* bottom padding leaves room for the absolute-positioned radio */
  cursor: pointer;
  text-align: left;
  font: inherit;
  color: inherit;
  transition: border-color 180ms ease, background 180ms ease;
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
}
.podcast-type:hover {
  border-color: var(--border-strong);
}
.podcast-type.selected {
  border-color: var(--gold);
  background:
    radial-gradient(circle at 50% 30%, rgba(198, 165, 107, 0.08), transparent 70%),
    rgba(14, 11, 8, 0.85);
}
.podcast-type-art {
  /* styles.css sets a fixed 96x96 art tile. Override both dimensions
     so the proto's wider tile takes effect. The source art is square
     and carries the show title baked into the top band — a 16:10
     (wide, short) frame with background-size:cover cropped that title
     off top and bottom. A 1:1 frame matches the source, so cover
     shows the whole image, title included. */
  width: 100%;
  height: auto;
  aspect-ratio: 1 / 1;
  background-color: var(--panel-deep);
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  border-radius: 4px;
  border: 1px solid var(--border-faint);
  flex-shrink: 0;
}
.podcast-type-name {
  font-family: var(--font-display);
  font-size: 1.02rem;
  font-weight: 500;
  color: var(--text);
  line-height: 1.3;
}
.podcast-type.selected .podcast-type-name {
  color: var(--gold-bright);
}
/* Prevent production's .podcast-type.selected .podcast-type-art
   rule (oxblood border + shadow) from leaking through. The proto
   indicates selection via the gold tile border and the radio,
   not via a coloured art border. */
.podcast-type.selected .podcast-type-art {
  border-color: var(--border-faint);
  box-shadow: none;
}
.podcast-type-desc {
  font-family: var(--font-display);
  font-size: 0.88rem;
  color: var(--text-muted);
  line-height: 1.45;
  font-style: italic;
  flex: 1;
}
.podcast-type-radio {
  position: absolute;
  bottom: 0.85rem;
  right: 0.85rem;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  border: 1px solid var(--border);
  background: transparent;
  transition: border-color 180ms ease, background 180ms ease;
}
.podcast-type.selected .podcast-type-radio {
  border-color: var(--gold);
  background: var(--gold);
  box-shadow: inset 0 0 0 3px var(--bg);
}

/* ============================================================
   Step 5 — Generate action row
   ============================================================

   Right-aligned button row that lives outside the panels. No
   border, no background — just the button sitting under panel 3.
   Hidden until a transcript is loaded; revealed alongside the
   downstream panels via revealDownstreamSections. */

.generate-row {
  display: flex;
  justify-content: flex-end;
  margin: 0.5rem 0 2rem;
}
/* Required: the display:flex above beats the UA's [hidden] rule —
   same pattern as .invite-badge[hidden]. Without this the Generate
   button shows (orphaned, disabled) before any session material is
   added, even though index.html's reveal logic sets hidden. */
.generate-row[hidden] { display: none; }

.generate-btn {
  display: inline-flex;
  align-items: center;
  gap: 0.7rem;
  padding: 0.85rem 1.6rem;
  border-radius: 4px;
  font-family: var(--font-mono);
  font-size: 0.85rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  font-weight: 500;
  color: var(--gold-bright);
  background: rgba(198, 165, 107, 0.15);
  border: 1px solid var(--gold);
  cursor: pointer;
  transition: background 180ms ease, border-color 180ms ease, color 180ms ease;
}
.generate-btn:hover:not(:disabled) {
  background: rgba(198, 165, 107, 0.28);
  border-color: var(--gold-bright);
}
.generate-btn:disabled {
  opacity: 0.4;
  cursor: not-allowed;
  background: transparent;
  color: var(--text-muted);
  border-color: var(--border);
}
.generate-btn .bolt {
  font-size: 0.95rem;
  line-height: 1;
}

/* ============================================================
   Progress panel — in-progress message + completion swap
   ============================================================ */

#progressPanel.working .panel-number,
#progressPanel.complete .panel-number,
#progressPanel.failed .panel-number {
  /* Override the compass-rose notches for this panel — replaced
     by the spinner / check / X via the inner element. */
  position: relative;
}
#progressPanel .panel-number::before,
#progressPanel .panel-number::after {
  display: none;
}

/* Spinner shown in the panel-number badge during working state.
   Border ring with one transparent side, rotated. */
.progress-spinner {
  display: inline-block;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  border: 1.5px solid var(--border-strong);
  border-top-color: var(--gold-bright);
  animation: spinner-rotate 900ms linear infinite;
}
@keyframes spinner-rotate {
  to { transform: rotate(360deg); }
}

/* In complete state: spinner replaced by a check glyph via the
   gold-on-gold tile. We rely on changing the spinner's class via
   JS — simpler to just style the wrapper and put a CSS-only check
   when complete. */
#progressPanel.complete .panel-number {
  border-color: var(--gold);
  background: var(--gold);
  color: var(--bg);
}
#progressPanel.complete .progress-spinner {
  display: none;
}
#progressPanel.complete .panel-number::after {
  /* Check mark via clip-path so it always renders against the
     gold tile; simpler than another SVG element. */
  display: block;
  position: static;
  content: '\2713';
  font-family: var(--font-mono);
  font-weight: 500;
  font-size: 1rem;
  width: auto;
  height: auto;
  background: transparent;
  transform: none;
  inset: auto;
  color: var(--bg);
  text-align: center;
}

#progressPanel.failed .panel-number {
  border-color: var(--oxblood-bright);
  background: var(--oxblood);
  color: var(--text);
}
#progressPanel.failed .progress-spinner {
  display: none;
}
#progressPanel.failed .panel-number::after {
  display: block;
  position: static;
  content: '\2715';
  font-family: var(--font-mono);
  font-weight: 500;
  font-size: 1rem;
  width: auto;
  height: auto;
  background: transparent;
  transform: none;
  inset: auto;
  color: var(--text);
  text-align: center;
}

.progress-message {
  font-family: var(--font-display);
  font-size: 1rem;
  color: var(--text-muted);
  line-height: 1.6;
  margin: 0 0 1rem;
  font-style: italic;
}

/* Per-artefact row in the complete/partial state — step 5.7.

   Three visual states via class on the row:
     .done       — artefact exists; row shows View+Download+Recreate
     .missing    — artefact wasn't generated; row is dimmed, shows Create
     .marked     — user has clicked Create or Recreate (queued state)

   Layout: label on the left, status text in the middle, action
   buttons on the right. Wraps to two lines on mobile (see media
   query). */

.terminal-rows {
  margin-top: 1rem;
  border-top: 1px solid var(--border-faint);
}
.terminal-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.9rem 0;
  border-bottom: 1px solid var(--border-faint);
}
.terminal-row.missing {
  opacity: 0.7;
}
.terminal-row.marked {
  opacity: 1;
  background: rgba(198, 165, 107, 0.05);
  /* slight gold wash to signal "queued" */
}

.terminal-row-label {
  font-family: var(--font-display);
  font-size: 1.05rem;
  color: var(--text);
  flex-shrink: 0;
}

.terminal-row-status {
  flex: 1;
  font-family: var(--font-mono);
  font-size: 0.7rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-dim);
  text-align: center;
}
.terminal-row.marked .terminal-row-status {
  color: var(--gold-bright);
}

.terminal-row-actions {
  display: flex;
  gap: 0.5rem;
  flex-shrink: 0;
}
.terminal-row-btn {
  font-family: var(--font-mono);
  font-size: 0.7rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text);
  background: rgba(198, 165, 107, 0.06);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 0.5rem 0.95rem;
  cursor: pointer;
  text-decoration: none;
  transition: border-color 180ms ease, color 180ms ease, background 180ms ease;
}
.terminal-row-btn:hover {
  border-color: var(--gold-bright);
  color: var(--gold-bright);
  background: rgba(198, 165, 107, 0.15);
}

/* Action button on a row — Create or Recreate. Slight gold tint
   to differentiate from the View/Download neutrals. */
.terminal-row-btn-action {
  color: var(--gold-bright);
  background: rgba(198, 165, 107, 0.1);
  border-color: var(--border-strong);
}

/* Top-up bar — appears below the rows once anything is marked.
   Direct-action: no modal step; clicking Confirm POSTs straight
   to /api/sessions/:id/topup. */

.topup-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 1rem 1.25rem;
  margin-top: 1rem;
  background: rgba(198, 165, 107, 0.08);
  border: 1px solid var(--gold);
  border-radius: 6px;
}
.topup-bar[hidden] {
  display: none;
}
.topup-bar-text {
  font-family: var(--font-mono);
  font-size: 0.78rem;
  letter-spacing: 0.06em;
  color: var(--text);
  flex: 1;
}
.topup-bar-actions {
  display: flex;
  gap: 0.6rem;
  flex-shrink: 0;
}

/* ============================================================
   Access (cost-confirm) modal — per-modal overrides only
   ============================================================

   The base modal styling is global (see "Step 6 — Shared modal
   restyle" below). Only the per-modal width override stays here. */

#accessModal .modal {
  width: auto;
  max-width: 560px;
}

/* Cost breakdown rows inside the modal. */
.cost-breakdown {
  background: rgba(8, 7, 6, 0.5);
  border: 1px solid var(--border-faint);
  border-radius: 4px;
  padding: 0.6rem 1rem;
  margin-bottom: 1rem;
}
.cost-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0.45rem 0;
  font-family: var(--font-mono);
  font-size: 0.78rem;
  color: var(--text-muted);
  border-top: 1px solid var(--border-faint);
}
.cost-row:first-child {
  border-top: none;
}
.cost-row .lbl {
  letter-spacing: 0.06em;
}
.cost-row .amt {
  color: var(--text);
  font-weight: 500;
}
.cost-row.total {
  border-top: 1px solid var(--border-strong);
  margin-top: 0.4rem;
  padding-top: 0.7rem;
}
.cost-row.total .lbl {
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--text);
}
.cost-row.total .amt {
  color: var(--gold-bright);
}

/* Insufficient-credits banner — shown above the cost breakdown
   when balance < total. */
.insufficient-banner {
  background: rgba(160, 40, 40, 0.12);
  border: 1px solid rgba(160, 40, 40, 0.45);
  border-radius: 4px;
  padding: 0.7rem 0.95rem;
  margin-bottom: 1rem;
  font-family: var(--font-display);
  font-size: 0.92rem;
  color: #d97d7d;
  font-style: italic;
}

/* ============================================================
   Step 5.6 — Gallery (incremental image strip)
   ============================================================

   Strip of clickable image tiles inside the in-progress / results
   panel. Tiles appear as their images land server-side, then
   stay visible into the complete state. The shared gallery
   lightbox partial owns the click behaviour (see
   /partials/gallery-lightbox.js and the wireGalleryTile call in
   the inline init). */

.gallery {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 1rem;
  margin: 1rem 0 0.5rem;
  align-items: start;
  /* don't stretch tiles to match the tallest in their row */
}

/* Gallery tile — an <a> wrapping a thumbnail. Cover tile (the
   portrait one) is taller and shows up first in the array. */
.gallery-item {
  display: flex;
  flex-direction: column;
  background: rgba(8, 7, 6, 0.55);
  border: 1px solid var(--border-faint);
  border-radius: 4px;
  overflow: hidden;
  transition: transform 180ms ease, border-color 180ms ease, box-shadow 180ms ease;
  text-decoration: none;
  /* gallery items are <a> elements */
  color: inherit;
}
.gallery-item:hover {
  transform: translateY(-2px);
  border-color: var(--border-strong);
  box-shadow: 0 6px 14px rgba(0, 0, 0, 0.5);
}

.gallery-thumb {
  display: block;
  width: 100%;
  aspect-ratio: 3 / 2;
  object-fit: cover;
  background: var(--panel-deep);
}
.gallery-item.cover .gallery-thumb {
  /* Cover image is portrait — 2:3 instead of 3:2. */
  aspect-ratio: 2 / 3;
}

/* Animation play-pip overlay on tiles whose image has an
   animation companion. Ported from production: a 36px circle
   centred on the thumbnail with a white play triangle, slight
   scale-up on hover. The lightbox plays the video natively. */
.gallery-item.has-animation {
  position: relative;
}
.gallery-item.has-animation::after {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 36px;
  height: 36px;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.65)
    url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='20' height='20'><path d='M9 7 L17 12 L9 17 Z' fill='white'/></svg>")
    center / 14px no-repeat;
  border: 1.5px solid rgba(255, 255, 255, 0.85);
  pointer-events: none;
  transition: transform 180ms ease, background-color 180ms ease;
}
.gallery-item.has-animation:hover::after {
  transform: translate(-50%, -50%) scale(1.1);
  background-color: rgba(0, 0, 0, 0.8);
}

/* ============================================================
   Step 7 — /proto/campaigns overrides
   ============================================================

   The campaigns page port relies primarily on the production-
   token aliases at :root (--paper, --ink, --rule, etc.) — those
   redefinitions cascade through styles.css's campaigns rules
   and restyle most of the page via inheritance.

   What this block covers is the cases where styles.css hardcodes
   colour values inline instead of using tokens — primarily the
   tinted backgrounds on status badges. The token-alias approach
   can't touch those.

   When /proto/campaigns is promoted to /campaigns (step 8), these
   rules either move into styles.css or, ideally, styles.css's
   hardcoded RGBAs get refactored to use tokens — at which point
   these overrides go away. */

/* ----- Status badges (member + session) -----
   Production uses tinted-alpha backgrounds built from the
   production moss / gold / oxblood literal colours. Re-skin with
   the proto's redefined values via gold/moss/oxblood vars. */

/* ----- Card backgrounds with hardcoded parchment alpha -----
   Several campaigns-page rules in styles.css set
   `background: rgba(255, 252, 245, 0.4)` (parchment at 40% alpha)
   directly, not via a token. Against the dark page that reads as
   a milky overlay — most of the elements rendered nearly white,
   killing contrast for the text inside. Override with a panel
   surface that matches the rest of the proto.

   Same treatment for hover states (slightly raised) so they keep
   their hover-distinguishable feel against the new palette. */

.member-row,
.session-row {
  background: rgba(8, 7, 6, 0.55);
  border-color: var(--border-faint);
}
.member-row.pending {
  background: rgba(198, 165, 107, 0.06);
  border-color: var(--border);
}
.session-row:hover {
  background: rgba(14, 11, 8, 0.85);
  border-color: var(--border-strong);
}
.session-row:hover .session-name {
  color: var(--gold-bright);
}

.member-status-badge.active {
  background: rgba(106, 136, 98, 0.14);
  /* moss-bright at low alpha */
  color: var(--moss-bright);
  border-color: var(--moss);
}
.member-status-badge.pending {
  background: rgba(198, 165, 107, 0.14);
  color: var(--gold-bright);
  border-color: var(--gold);
}
.member-status-badge.creator {
  background: rgba(123, 47, 47, 0.18);
  color: #d97d7d;
  border-color: var(--oxblood-bright);
}


/* ----- "+ New session" / list-pane primary button -----
   styles.css's `.campaign-action-btn.primary` rule (specificity
   0,0,2,0) was overriding the proto's single-class
   `.campaigns-new-session` rule (0,0,1,0). Boosted the selector
   to `.campaign-action-btn.campaigns-new-session` (0,0,2,0) so
   it wins on equal-specificity-later-rule basis. */

.campaign-action-btn.campaigns-new-session {
  background: rgba(198, 165, 107, 0.15);
  border: 1px solid var(--gold);
  color: var(--gold-bright);
}
.campaign-action-btn.campaigns-new-session:hover {
  background: rgba(198, 165, 107, 0.28);
  border-color: var(--gold-bright);
}

/* ----- Campaign status block — "Where are we up to" panel -----
   Re-skin with a gold tint to draw the eye. No production rule
   competes for these properties, so plain rules work. */

.campaign-status-block {
  background: rgba(198, 165, 107, 0.06);
  border-left-color: var(--gold);
}
  .campaign-status-action {
  color: var(--gold-bright);
}
  .campaign-status-action:hover {
  color: var(--gold);
}

/* ----- Chrome row width on /proto/campaigns -----
   Page has no hero, so the chrome floats over the first body
   element. Push the campaigns container down so it doesn't sit
   underneath the chrome. .chrome is ~80px tall (1.5rem top
   padding + content + 1.5rem bottom padding); 5.5rem (88px)
   leaves a small visual gap. */

.campaigns-container {
  padding-top: 5.5rem;
}

/* ============================================================
   Metadata edit modal — campaigns page (step 7 addendum)
   ============================================================

   Different markup conventions from the other modals:
     - Backdrop and modal are siblings (not nested) — backdrop is
       just a dim layer; the modal element is separately
       positioned over the page.
     - Visibility via the `hidden` attribute on each element,
       not the `.open` class on a wrapper.
     - Internal structure uses .modal-header / .modal-body /
       .modal-footer / .modal-close / .modal-help / .modal-status
       — none of which are styled by step 6's shared modal block.

   Production has these classes unstyled (relies on browser
   defaults). Bringing them into the dark palette here so the
   modal renders properly rather than as a half-broken block. */

/* Backdrop overrides — it's a sibling of .modal so the step-6
   "backdrop with flex centring" pattern doesn't apply. The
   backdrop just dims the page. */
#metadataEditBackdrop {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.7);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  z-index: 100;
}
#metadataEditBackdrop[hidden] {
  display: none;
}

/* The modal element is standalone — no flex container. Position
   it manually, centred. */
#metadataEditModal {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: 101;
  width: calc(100% - 2.5rem);
  max-width: 480px;
  /* .modal base styling from step 6 supplies background, border,
     box-shadow, padding. We only need positioning here. */
}
#metadataEditModal[hidden] {
  display: none;
}

/* Header — title + close button row. */

/* × close button — small icon-only button in the header's top right. */

/* Body — just spacing for the form fields. */

/* Help text under the title — explanatory blurb above the form. */

/* Status line — populated by the JS with success/error/info text. */
#metadataEditModal .modal-status {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.06em;
  color: var(--text-muted);
  min-height: 1.2em;
  margin-top: 0.85rem;
}
#metadataEditModal .modal-status-error {
  color: #d97d7d;
}
#metadataEditModal .modal-status-ok {
  color: var(--moss-bright);
}

/* Footer — Cancel + Save row. Mirrors .modal-actions from other
   modals (right-aligned, gap, top margin). */

/* .field input override inside this modal — production's .field
   rules expect particular parents; the modal-body's .field is at
   one level deeper, so the input/select styling from step 3
   reaches it via inheritance. No extra work needed. */

/* ============================================================
   Step 6 — Shared modal restyle (global)
   ============================================================

   Restyles the base modal classes (.modal-backdrop, .modal,
   .modal-title, .modal-sub, .modal-actions, .btn-primary,
   .btn-secondary) plus modal-specific classes for the four
   shared modals (top-up, account, ledger, notifications) to fit
   the Literary Terminal dark palette.

   Why global, not per-modal-ID:
     Every modal on /proto is supposed to be dark. Scoping by ID
     means 5x duplication of the same rules; a single global
     restyle is one rule per class. The previous step-3 and
     step-5 modals (#campaignModal, #accessModal) used ID-scoped
     restyles only because the other modals were still parchment
     at that point; now that everything's dark, the scoping is
     redundant.

   Per-modal-ID rules still live above this block, but ONLY for
   things genuinely unique to one modal (e.g. #campaignModal's
   wider container for the setting editor).

   styles.css's modal rules are still loaded — proto.css overrides
   them on the cascade, since proto.css loads after styles.css and
   each rule has the same specificity. */

/* Backdrop — dim the page, light blur. styles.css already sets
   position:fixed, z-index, and the .open transition; we just
   restyle the dim colour to match the dark palette. */
.modal-backdrop {
  background: rgba(0, 0, 0, 0.7);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}

/* Modal container. The base default is 480px wide; per-modal
   ID rules above override for wider modals (campaign, access,
   ledger, notifications). */
.modal {
  background: var(--panel);
  color: var(--text);
  border: 1px solid var(--border-strong);
  border-radius: 6px;
  box-shadow:
    inset 0 0 0 1px rgba(255, 255, 255, 0.015),
    0 8px 32px rgba(0, 0, 0, 0.7);
  padding: 1.75rem 2rem;
  /* Tall-modal guard: the backdrop centers the dialog, so anything
     taller than the viewport used to clip off the TOP with no way to
     scroll to it (the topup modal on mobile was the canonical case).
     Cap at viewport height minus a margin and scroll internally.
     dvh tracks the real visible height on mobile (URL bar collapse);
     the vh line is the fallback for engines without dvh. */
  max-height: calc(100vh - 32px);
  max-height: calc(100dvh - 32px);
  overflow-y: auto;
  overscroll-behavior: contain;
}

/* Native radio/checkbox controls render browser-blue by default,
   which clashes with the palette. accent-color recolors them
   everywhere (account modal contact prefs, product-updates opt-in,
   email recipient checkboxes, and anything future). */
input[type="radio"],
input[type="checkbox"] {
  accent-color: var(--gold);
}

.modal-title {
  font-family: var(--font-display);
  font-size: 1.5rem;
  font-weight: 500;
  color: var(--text);
  border-bottom: 1px solid var(--border-faint);
  padding-bottom: 0.75rem;
  margin: 0 0 0.75rem;
}

.modal-sub {
  font-family: var(--font-display);
  font-size: 0.95rem;
  color: var(--text-muted);
  font-style: italic;
  margin-bottom: 1.25rem;
}

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

/* Buttons. .btn-primary is the gold-accented confirm action;
   .btn-secondary is the muted dismiss/cancel. Both keep mono
   typography for the action label. */
.btn-secondary,
.btn-primary {
  font-family: var(--font-mono);
  font-size: 0.75rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  padding: 0.65rem 1.25rem;
  border-radius: 4px;
  cursor: pointer;
  transition: border-color 180ms ease, color 180ms ease, background 180ms ease;
}
.btn-secondary {
  background: transparent;
  border: 1px solid var(--border);
  color: var(--text-muted);
}
.btn-secondary:hover {
  border-color: var(--border-strong);
  color: var(--text);
}
.btn-primary {
  background: rgba(198, 165, 107, 0.15);
  border: 1px solid var(--border-strong);
  color: var(--gold-bright);
}
.btn-primary:hover:not(:disabled) {
  background: rgba(198, 165, 107, 0.25);
  border-color: var(--gold);
}
.btn-primary:disabled {
  opacity: 0.5;
  cursor: not-allowed;
  background: rgba(198, 165, 107, 0.05);
}
.btn-secondary.destructive {
  border-color: rgba(160, 40, 40, 0.5);
  color: #d97d7d;
}
.btn-secondary.destructive:hover {
  border-color: rgba(160, 40, 40, 0.8);
  color: #e89090;
}

/* ============================================================
   Top-up modal
   ============================================================
   Markup: partials/modals.html → #topupModal.
   Wiring: partials/topup-modal.js. Reads /api/billing/packages
   and renders one .tier-card per package.
*/

#topupModal .modal {
  max-width: 520px;
}
.topup-current {
  font-family: var(--font-mono);
  font-size: 0.7rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-muted);
  margin-bottom: 1rem;
  text-align: right;
}
.topup-current .credits-num {
  color: var(--gold-bright);
  font-weight: 500;
}


/* ----- Top-up modal sections -----
   (cleanup pass 4) The orphan topup-modal-plans.css (plan-card /
   tier-price-base card chooser) was confirmed STALE against
   topup-modal.js, which renders .pack-row / .plan-row with inline
   element styles instead — so that block was not merged. Only these
   two rules survive from it: modals.html's markup uses
   .topup-section / .topup-section-label, which otherwise have no
   styling anywhere. NOTE: this is the one deliberate visual change
   in the cleanup — the section labels gain the mono uppercase
   treatment they were designed for. */
.topup-section {
  margin-top: 1.1rem;
}
.topup-section-label {
  font-family: var(--font-mono);
  font-size: 0.66rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-muted);
  margin-bottom: 0.5rem;
}

.topup-error {
  font-family: var(--font-mono);
  font-size: 0.7rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: #d97d7d;
  text-align: center;
  min-height: 1.2em;
  margin-top: 0.85rem;
}

/* ============================================================
   Account modal
   ============================================================
   Markup: partials/modals.html → #accountModal.
   Two tabs: General (display name, email, contact prefs) and
   Manage billing (link to Stripe portal). Tabs are buttons with
   inline styles in the markup; we override via more-specific
   selectors here to keep that inline styling from winning.
*/

#accountModal .modal {
  max-width: 520px;
}

/* Tabs container. Inline styles on the partial were stripped
   (follow-up 4 in the proto migration). The corresponding rules
   for production-palette tabs were added to styles.css; these
   override into the dark palette. */
.account-tabs {
  border-bottom-color: var(--border-faint);
}
.account-tab {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-muted);
  transition: color 180ms ease, border-color 180ms ease;
}
.account-tab:hover {
  color: var(--text);
}
.account-tab.active {
  color: var(--gold-bright);
  border-bottom-color: var(--gold);
}

/* Contact-preference radio group. */
.contact-method-group {
  display: flex;
  gap: 0.5rem;
  flex-wrap: wrap;
}
.contact-method-option {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.55rem 0.85rem;
  background: rgba(8, 7, 6, 0.55);
  border: 1px solid var(--border-faint);
  border-radius: 4px;
  cursor: pointer;
  font-family: var(--font-display);
  font-size: 0.92rem;
  color: var(--text-muted);
  transition: border-color 180ms ease, color 180ms ease;
}
.contact-method-option:hover {
  border-color: var(--border-strong);
  color: var(--text);
}
.contact-method-option input[type="radio"] {
  margin: 0;
  cursor: pointer;
}
.contact-method-option:has(input[type="radio"]:checked) {
  border-color: var(--gold);
  color: var(--gold-bright);
  background: rgba(198, 165, 107, 0.08);
}

/* ============================================================
   Ledger modal — credit history
   ============================================================
   Markup: partials/modals.html → #ledgerModal.
   Wiring: partials/ledger-modal.js. Fetches /api/billing/ledger
   and renders rows with .kind-topup / .kind-debit / .kind-refund /
   .kind-chargeback classes for colour-coding.
*/

#ledgerModal .modal,
.ledger-modal {
  max-width: 620px;
  max-height: 80vh;
  display: flex;
  flex-direction: column;
}

.ledger-summary {
  display: flex;
  justify-content: space-between;
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-muted);
  padding: 0 0 0.85rem;
  margin-bottom: 0;
}
.ledger-summary .credits-num {
  color: var(--gold-bright);
  font-weight: 500;
}

.ledger-list {
  overflow-y: auto;
  margin: 0 -2rem;
  /* extend full-width by counter-padding the modal */
  border-top: 1px solid var(--border-faint);
  border-bottom: 1px solid var(--border-faint);
}
.ledger-empty {
  padding: 2.5rem 1.5rem;
  text-align: center;
  color: var(--text-dim);
  font-family: var(--font-display);
  font-style: italic;
  font-size: 0.95rem;
}
.ledger-row {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 0.85rem 1rem;
  padding: 0.85rem 2rem;
  border-bottom: 1px solid var(--border-faint);
  align-items: baseline;
}
.ledger-row:last-child {
  border-bottom: none;
}
.ledger-row .ledger-meta {
  grid-column: 1 / 2;
}
.ledger-row .ledger-amt {
  grid-column: 2 / 3;
  font-family: var(--font-mono);
  font-size: 0.88rem;
  text-align: right;
  white-space: nowrap;
}
/* Per-kind amount colours. moss for top-ups (incoming),
   oxblood for refunds/chargebacks (reversal), neutral text
   for debits (generation). */
.ledger-row.kind-topup .ledger-amt {
  color: var(--moss-bright);
}
.ledger-row.kind-refund .ledger-amt,
.ledger-row.kind-chargeback .ledger-amt {
  color: var(--oxblood-bright);
}
.ledger-row.kind-debit .ledger-amt {
  color: var(--text);
}
.ledger-row .ledger-title {
  font-family: var(--font-display);
  font-size: 0.95rem;
  color: var(--text);
  margin-bottom: 0.15rem;
}
.ledger-row .ledger-sub {
  font-family: var(--font-mono);
  font-size: 0.68rem;
  letter-spacing: 0.04em;
  color: var(--text-dim);
}
.ledger-row .ledger-link {
  font-family: var(--font-mono);
  font-size: 0.7rem;
  letter-spacing: 0.06em;
  color: var(--gold);
  text-decoration: none;
  margin-left: 0.75rem;
}
.ledger-row .ledger-link:hover {
  color: var(--gold-bright);
  text-decoration: underline;
}
.ledger-row .ledger-outputs {
  font-family: var(--font-display);
  font-size: 0.85rem;
  color: var(--text-muted);
  margin-top: 0.25rem;
}
.ledger-row .ledger-outputs span + span::before {
  content: '\00b7';
  margin: 0 0.4rem;
  color: var(--text-dim);
}

/* ============================================================
   Notifications modal
   ============================================================
   Markup: partials/modals.html → #notificationsModal.
   Mixed list: invitation rows (actionable) + notification rows
   (informational). One badge on the top-level Notifications
   button covers both.
*/

#notificationsModal .modal,
.notifications-modal {
  max-width: 620px;
  max-height: 80vh;
  display: flex;
  flex-direction: column;
}

.notifications-list {
  overflow-y: auto;
  margin: 0 -2rem;
  border-top: 1px solid var(--border-faint);
  border-bottom: 1px solid var(--border-faint);
}
.notifications-empty {
  padding: 2.5rem 1.5rem;
  text-align: center;
  color: var(--text-dim);
  font-family: var(--font-display);
  font-style: italic;
  font-size: 0.95rem;
}

/* Notification row: unseen dot, body, optional action. */
.notification-row {
  display: grid;
  grid-template-columns: 14px 1fr auto;
  align-items: center;
  gap: 0.85rem;
  padding: 0.85rem 2rem;
  border-bottom: 1px solid var(--border-faint);
  font-family: var(--font-display);
  font-size: 0.92rem;
  color: var(--text-muted);
}
.notification-row:last-child {
  border-bottom: none;
}
.notification-body {
  min-width: 0;
  /* allows long campaign names to ellipsis */
}
.notification-row.unseen {
  color: var(--text);
}
.notification-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: transparent;
  justify-self: center;
}
.notification-row.unseen .notification-dot {
  background: var(--gold);
}
.notification-row .notification-link {
  font-family: var(--font-mono);
  font-size: 0.7rem;
  letter-spacing: 0.08em;
  color: var(--gold);
  text-decoration: none;
}
.notification-row .notification-link:hover {
  color: var(--gold-bright);
  text-decoration: underline;
}

/* Invitation rows — actionable (Join / Decline). Render above
   notification rows in the same list. */
.invitation-row {
  padding: 1rem 2rem;
  border-bottom: 1px solid var(--border-faint);
}
.invitation-row:last-child {
  border-bottom: none;
}
.invitation-message {
  font-family: var(--font-display);
  font-size: 0.95rem;
  color: var(--text);
  line-height: 1.5;
  margin-bottom: 0.75rem;
}
.invitation-message strong {
  font-weight: 500;
  color: var(--gold-bright);
}
.invitation-actions {
  display: flex;
  gap: 0.6rem;
}
.invitation-actions .btn-primary,
.invitation-actions .btn-secondary {
  padding: 0.45rem 0.95rem;
  font-size: 0.68rem;
}
.invitation-error {
  margin-top: 0.5rem;
  font-family: var(--font-display);
  font-style: italic;
  font-size: 0.88rem;
  color: #d97d7d;
  min-height: 1.2em;
}

/* ============================================================
   Mobile — collapses the auth strip's optional links.
   ============================================================ */
@media (max-width: 600px) {
  .chrome { padding: 1rem; }
  .wordmark { font-size: 1.1rem; }
  .auth-strip { padding: 0.4rem 0.5rem 0.4rem 0.7rem; gap: 0.5rem; }
  /* Mobile chrome: the text links AND the user pill give way to the
     nav burger, which carries My campaigns / Notifications (badge
     included) / Account / Sign out. The strip is then wordmark +
     burger + credit pill — which fits, so the credit pill keeps its
     "credits" label. */
  .auth-strip-link { display: none; }
  .user-pill-wrap { display: none; }
  .nav-burger-wrap { display: inline-block; }

  /* Step 2: tighter padding for narrow screens */
  .proto-main { padding: 0 1rem 3rem; }
  .panel { padding: 1.5rem 1.25rem; }
  .panel-header { gap: 1rem; }
  .transcript-zone { padding: 1rem 1rem 3rem; }
  .file-info { flex-wrap: wrap; }

  /* Step 3: field-row collapses to single column on narrow screens.
     Modal padding also tightens. */
  .field-row { grid-template-columns: 1fr; gap: 0; }
  .field-row > .field { margin-bottom: 1.25rem; }
  .field-row > .field:last-child { margin-bottom: 0; }

  /* Campaign modal: clamp to viewport minus a small gutter on
     narrow screens so it doesn't overflow. */
  #campaignModal .modal { max-width: calc(100vw - 2rem); }

  /* Step 4: output grid stacks to one column; podcast types
     drop to a slightly narrower minmax so 2 still fit at narrow
     widths. */
  .output-grid { grid-template-columns: 1fr; }
  .podcast-types { grid-template-columns: 1fr; }

  /* Step 5/5.7: Generate button centres on narrow screens;
     terminal rows wrap their actions onto a second line if needed;
     topup bar stacks. */
  .generate-row { justify-content: center; }
  .generate-btn { justify-content: center; }
  .terminal-row { flex-wrap: wrap; align-items: flex-start; }
  .terminal-row-status { text-align: left; width: 100%; order: 99; padding-left: 0; }
  .topup-bar { flex-direction: column; align-items: stretch; }
  .topup-bar-actions { justify-content: flex-end; }
  #accessModal .modal { max-width: calc(100vw - 2rem); }

  /* Step 5.6: gallery drops to 2 columns on narrow screens. */
  .gallery { grid-template-columns: repeat(2, 1fr); gap: 0.65rem; }

  /* Step 6: shared modals shrink to viewport-minus-gutter, and
     the wider modal containers (ledger, notifications, account,
     topup) drop their per-modal max-width caps. The ledger /
     notifications row padding tightens to 1rem each side. */
  .modal { padding: 1.4rem 1.25rem; }
  #campaignModal .modal,
  #topupModal .modal,
  #accountModal .modal,
  #ledgerModal .modal,
  #notificationsModal .modal {
    max-width: calc(100vw - 2rem);
  }
  .ledger-list, .notifications-list { margin: 0 -1.25rem; }
  .ledger-row, .notification-row, .invitation-row { padding-left: 1.25rem; padding-right: 1.25rem; }
  .contact-method-group { flex-direction: column; }
}
/* ============================================================
   Site footer — legal links + contact + copyright banner.
   ============================================================

   (cleanup pass 2) Promoted from identical inline <style> blocks
   on index.html, campaigns.html, and marketing.html — three
   consumers, so per the "promote when there's a second consumer"
   rule this is shared chrome now. Full-width banner; sits outside
   .page so it spans the viewport. The footer MARKUP is still
   duplicated per page — partial-ising it is a separate decision. */
.site-footer {
  border-top: 1px solid var(--border-faint);
  background: var(--panel-deep);
  padding: 2rem 1.5rem;
}
.footer-inner {
  max-width: 1100px;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.85rem;
  text-align: center;
}
.footer-links {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.5rem 1.5rem;
}
.footer-links a {
  font-family: var(--font-mono);
  font-size: 0.8rem;
  letter-spacing: 0.04em;
  color: var(--text-muted);
  text-decoration: none;
}
.footer-links a:hover { color: var(--gold); }
.footer-copyright {
  font-family: var(--font-mono);
  font-size: 0.75rem;
  letter-spacing: 0.04em;
  color: var(--text-dim);
  margin: 0;
}
/* Desktop: links and copyright on one row, spaced apart. */
@media (min-width: 720px) {
  .footer-inner {
    flex-direction: row;
    justify-content: space-between;
    text-align: left;
  }
  .footer-links { flex-direction: row; }
}