*,*::before,*::after{margin:0;padding:0;box-sizing:border-box;}
:root{
  /* ── THEME TOKENS ──────────────────────────────────────────────
     These are fallback defaults only (used if no theme file's
     html[data-theme] rules match — e.g. data-theme is missing or
     misspelled). Every file in /themes/ is loaded up front (see the
     <link> tags in <head>) and each one overrides these via a
     higher-specificity html[data-theme="..."] selector. To add a new
     theme: copy /themes/justwitter.css to /themes/yourtheme.css,
     change the values, add its <link> in <head>, and add its name to
     AVAILABLE_THEMES in the <script> at the bottom of this file. */
  --paper:#f7f4ee;--ink:#1a1a1a;--ink-mid:#666;
  --ink-faint:#6e6e6e;--rule:#e0ddd6;--red:#D10F24;
  --yellow:#FFE600;--black:#111;
  --surface:#fff;      /* card / panel background */
  --on-accent:#fff;    /* text & borders sitting on --red / --black fills */
  --font-display:'Bangers',cursive;   /* shouty headings, logo, buttons, tabs */
  --font-mono:'Courier Prime',monospace; /* body copy, inputs, timestamps */
  --font-accent:'Permanent Marker',cursive; /* hero tagline sticker */
  --hdr-h:56px;--ftr-h:56px;
}
html,body{background:var(--paper);color:var(--ink);font-family:var(--font-mono);min-height:100vh;-webkit-font-smoothing:antialiased;-webkit-tap-highlight-color:transparent;overscroll-behavior-y:none;}
a{color:inherit;text-decoration:none;}
button{font-family:inherit;}

/* ── HEADER ── */
.hdr{position:fixed;top:0;left:0;right:0;height:var(--hdr-h);display:flex;align-items:center;justify-content:space-between;padding:0 1.25rem;z-index:100;background:var(--red);border-bottom:4px solid var(--black);}
.hdr-logo{font-family:var(--font-display);font-size:1.25rem;letter-spacing:.08em;color:var(--on-accent);cursor:pointer;background:none;border:none;padding:0;}
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0;}
.skip-link{position:absolute;left:-9999px;top:0;z-index:1000;background:var(--yellow);color:var(--black);padding:.6rem 1rem;font-family:var(--font-mono);font-weight:bold;border:3px solid var(--black);border-radius:4px;}
.skip-link:focus{left:1rem;top:1rem;}
.hdr-actions{display:flex;align-items:center;gap:.6rem;}
.hdr-user{font-family:var(--font-mono);font-size:.8rem;color:var(--on-accent);}
.hdr-btn{height:34px;padding:0 .85rem;background:var(--icon-fill, rgba(0,0,0,.08));border:none;border-radius:4px;color:var(--on-accent);font-family:var(--font-display);font-size:.75rem;letter-spacing:.1em;cursor:pointer;}
.hdr-btn-profile{display:inline-flex;align-items:center;gap:.45em;}
.hdr-station-avatar{width:18px;height:18px;border-radius:50%;object-fit:cover;flex-shrink:0;display:block;}
.hdr-station-icon{flex-shrink:0;}
.hdr-btn:hover{background:var(--icon-fill-hover, rgba(0,0,0,.14));}
.hdr-btn-primary{background:var(--surface);color:var(--red);}
.hdr-btn-primary:hover{background:var(--black);color:var(--on-accent);border-color:var(--black);}
/* Theme toggle now lives in the footer (#footerThemeToggle, bottom-left —
   see the footer markup near the bottom of this file), swapped with the
   burger which now lives here in the header instead. This class is kept
   for the footer button and for terminal's invert toggle, which reuses
   its circular icon-button sizing — see html[data-theme="terminal"]
   .hdr-invert-toggle. */
.hdr-theme-toggle{width:34px;height:34px;padding:0;border-radius:50%;font-size:1.05rem;letter-spacing:.06em;display:flex;align-items:center;justify-content:center;line-height:1;font-family:system-ui,sans-serif;background:var(--icon-fill, rgba(0,0,0,.08));}
/* Theme switcher — a single "···" button that cycles one theme forward
   through AVAILABLE_THEMES per click, not a dropdown/popover — a full
   theme-name list was taking up too much room. Used to render as a solid
   white pill (background/border/color below) distinct from every other
   header button; now that the whole header/footer icon set shares one
   filled-circle language via --icon-fill, that distinct pill look is
   dropped so this button matches .hdr-theme-toggle/.hdr-btn instead —
   sizing (height/padding/radius/max-width) is kept since some of it
   still matters for the non-circular states .hdr-theme-select can be
   combined with. */
.hdr-theme-select{height:34px;padding:0 .6rem;border-radius:999px;font-family:inherit;font-size:.72rem;cursor:pointer;max-width:140px;}
/* Header burger — now the permanent menu entry point in the header, for
   every theme and every auth state (it used to live in the footer, and
   used to be hidden here by default). Filled circle like every other
   header/footer icon button, via --icon-fill. */
.hdr-burger{display:flex;flex-direction:column;justify-content:center;align-items:center;gap:5px;width:34px;height:34px;background:var(--icon-fill, rgba(0,0,0,.08));border:none;border-radius:50%;cursor:pointer;padding:0;flex-shrink:0;}
.hdr-burger span{display:block;width:16px;height:2px;border-radius:2px;background:var(--on-accent);}

/* ── VIEWS ── */
.view{display:none;min-height:100vh;min-height:100lvh;min-height:100dvh;}
.view.active{display:flex;flex-direction:column;}

/* ── HERO ── */
.hero{min-height:100vh;min-height:100lvh;align-items:center;justify-content:center;text-align:center;padding:calc(var(--hdr-h) + 2rem) 1.5rem 5rem;gap:clamp(1.5rem,5vw,2.5rem);background:var(--yellow);position:relative;overflow:hidden;}
.hero::before{content:'';position:absolute;inset:0;background-image:radial-gradient(circle,rgba(0,0,0,0.06) 1px,transparent 1px);background-size:14px 14px;pointer-events:none;}

/* Two hero variants share the #viewHero container: .hero-bangers (the
   animated ambient slogan, default/justwitter theme) and .hero-kaiunta
   (eyebrow + headline + copy + CTAs, styled entirely in kaiunta.css).
   Both are plain classes (not inline styles) specifically so a theme's
   stylesheet can toggle display on either one — inline styles would
   always beat a stylesheet rule regardless of selector specificity. */
.hero-bangers{position:fixed;top:var(--hdr-h);bottom:var(--ftr-h);left:0;right:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:1rem;background:var(--yellow);}
.hero-kaiunta{display:none;}
/* kaiunta-only hero addons — hidden here by default (same pattern as
   hero-kaiunta above); kaiuntatheme.css unhides + styles whichever one
   #viewHero's data-hero-media attribute selects. */
.hero-kai-wave{display:none;}
.hero-kai-media{display:none;}
.hero-kai-mark{display:none;}

/* Mobile portrait containment fix — shared by BOTH themes, not theme-scoped.
   .hero itself is min-height:100dvh (a FULL device-height box), which is
   invisible whenever .hero-bangers is the thing actually rendering inside
   it, because .hero-bangers escapes into its own position:fixed box
   clipped to top:var(--hdr-h)/bottom:var(--ftr-h) (see above) — the
   oversized outer .hero never shows through in that case.
   But .hero-kaiunta (eyebrow/title/copy/CTAs + its background media) is
   NOT fixed like that — it's normal in-flow content, vertically centered
   across the WHOLE 100dvh box, not the visible strip between header and
   footer. .hero-kaiunta is what's on screen for: (a) kaiunta, always, and
   (b) justwitter when $justwitter_hero_actionable is on (.hero-actionable
   on #viewHero) — both cases covered below. On a short phone (iPhone 13
   mini and similar) that oversized box runs past the footer and needs a
   scroll to reach the bottom — reads as "too tall"/not contained, instead
   of hugging the header/footer the way .hero-bangers already does.
   Fix: give it the same fixed-and-clipped treatment .hero-bangers uses,
   only at phone-portrait widths (desktop's intentional full-bleed hero is
   untouched). overflow-y:auto is a safety net if the copy ever runs
   longer than that gap on a very short screen — scrolls internally
   instead of pushing the hero (and its background) past the footer again. */
@media (max-width: 640px) and (orientation: portrait) {
  html[data-theme="kaiunta"] .hero,
  html[data-theme="justwitter"] #viewHero.hero-actionable {
    position: fixed;
    top: var(--hdr-h);
    bottom: var(--ftr-h);
    left: 0;
    right: 0;
    min-height: 0;
    padding-top: 1.5rem;
    padding-bottom: 1.5rem;
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
  }
}

.hero-actions{display:flex;flex-direction:column;align-items:stretch;gap:.85rem;margin-top:.5rem;width:min(320px,88vw);opacity:0;animation:fadeUp .9s ease .7s forwards;position:relative;}
.btn-primary{height:60px;padding:0 2rem;background:var(--red);border:4px solid var(--black);font-family:var(--font-display);font-size:1.1rem;letter-spacing:.15em;color:var(--on-accent);cursor:pointer;transition:transform .1s,box-shadow .1s;width:100%;box-shadow:6px 6px 0 var(--black);}
button:disabled,button[disabled]{background:var(--rule);color:var(--ink-mid);border-color:var(--ink-mid);box-shadow:none;opacity:1;cursor:not-allowed;}
.btn-primary:hover{transform:translate(-2px,-2px);box-shadow:8px 8px 0 var(--black);}
.btn-primary:active{transform:translate(3px,3px);box-shadow:2px 2px 0 var(--black);}
.btn-ghost{height:60px;padding:0 2rem;background:var(--surface);border:4px solid var(--black);font-family:var(--font-display);font-size:1rem;letter-spacing:.15em;color:var(--black);cursor:pointer;transition:transform .1s,box-shadow .1s;box-shadow:6px 6px 0 var(--black);width:100%;}
.btn-ghost:hover{transform:translate(-2px,-2px);box-shadow:8px 8px 0 var(--black);background:var(--yellow);}
.btn-ghost:active{transform:translate(3px,3px);box-shadow:2px 2px 0 var(--black);}

.hero-divider{display:flex;align-items:center;gap:.75rem;position:relative;}
.hero-divider::before,.hero-divider::after{content:'';flex:1;border-top:3px solid var(--black);}
.hero-divider span{font-family:var(--font-display);font-size:.75rem;letter-spacing:.15em;color:var(--black);}

.hero-legal{max-width:38ch;font-size:.7rem;font-style:italic;color:rgba(0,0,0,0.65);line-height:1.8;text-align:center;opacity:0;animation:fadeUp .9s ease 1s forwards;position:relative;}
.hero-story-hook{max-width:38ch;font-size:.75rem;font-style:italic;color:rgba(0,0,0,0.75);line-height:1.8;text-align:center;opacity:0;animation:fadeUp .9s ease 1.1s forwards;position:relative;}
.hero-story-link{background:none;border:none;padding:0;font:inherit;font-style:normal;font-weight:700;color:var(--red);border-bottom:2px solid var(--red);cursor:pointer;transition:color .15s;}
.hero-story-link:hover{color:var(--black);border-bottom-color:var(--black);}

.hero-tune{display:flex;flex-direction:column;align-items:center;gap:.85rem;width:100%;max-width:380px;}
.hero-tune-label{font-family:var(--font-mono);font-size:.78rem;color:var(--ink-mid);text-align:center;margin:0;}
.hero-tune-row{display:flex;gap:.6rem;width:100%;align-items:stretch;justify-content:center;}
.hero-tune-row .btn-ghost{height:52px;font-size:.85rem;padding:0 1.75rem;box-shadow:4px 4px 0 var(--black);white-space:nowrap;flex:0 0 auto;width:auto;}
#heroTuneResult{width:100%;max-width:640px;}
#heroTuneResult .cast-card{margin-top:.25rem;}

/* ── AUTH ── */
#viewAuth{background:var(--paper);align-items:center;justify-content:center;padding:calc(var(--hdr-h) + 2rem) 1.25rem 3rem;}
.auth-card{width:min(380px,92vw);background:var(--surface);border:3px solid var(--black);border-radius:8px;box-shadow:6px 6px 0 var(--black);padding:1.5rem;display:flex;flex-direction:column;gap:1.1rem;}
.auth-title{font-family:var(--font-display);font-size:1.8rem;letter-spacing:.04em;color:var(--red);text-align:center;}
.auth-tabs{display:flex;gap:.5rem;}
.auth-tab{flex:1;padding:.6rem;text-align:center;font-family:var(--font-display);font-size:.85rem;letter-spacing:.08em;border:3px solid var(--black);background:var(--surface);cursor:pointer;border-radius:4px;color:var(--black);}
.auth-tab.active{background:var(--yellow);}
.auth-form{display:flex;flex-direction:column;gap:.9rem;}
.auth-form.hidden{display:none;}
.auth-field{display:flex;flex-direction:column;gap:.3rem;}
.auth-field label{font-size:.75rem;color:var(--ink-mid);}
.auth-field input{height:46px;border:3px solid var(--black);border-radius:6px;padding:0 .8rem;font-family:var(--font-mono);font-size:.95rem;outline:none;}
.auth-field input:focus{box-shadow:0 0 0 3px var(--yellow);}
.auth-error{color:var(--red);font-size:.8rem;font-family:var(--font-mono);min-height:1.1em;}
.auth-back{font-size:.8rem;color:var(--ink-mid);text-align:center;cursor:pointer;text-decoration:underline;}

/* ── FEED ── */
#viewFeed{background:var(--paper);align-items:center;padding:calc(var(--hdr-h) + 1.25rem) 1.25rem 6rem;gap:1rem;}
#viewThread{background:var(--paper);align-items:center;padding:calc(var(--hdr-h) + 1.25rem) 1.25rem 6rem;gap:1rem;}
#viewTopic{background:var(--paper);align-items:center;padding:calc(var(--hdr-h) + 1.25rem) 1.25rem 6rem;gap:1rem;}
#viewAllTopics{background:var(--paper);align-items:center;padding:calc(var(--hdr-h) + 1.25rem) 1.25rem 6rem;gap:1rem;}
.feed-header,.feed-search,.feed-list,.feed-more{width:100%;max-width:640px;}
.feed-header{display:flex;align-items:center;gap:.85rem;margin-bottom:.25rem;}
.feed-back{width:42px;height:42px;flex-shrink:0;border-radius:50%;background:var(--red);border:3px solid var(--black);box-shadow:3px 3px 0 var(--black);color:var(--on-accent);font-size:1.2rem;line-height:1;display:flex;align-items:center;justify-content:center;cursor:pointer;transition:transform .1s,box-shadow .1s;}
.feed-back:hover{transform:translate(-1px,-1px);box-shadow:4px 4px 0 var(--black);}
.feed-back:active{transform:translate(2px,2px);box-shadow:1px 1px 0 var(--black);}
.feed-title{font-family:var(--font-display);font-size:clamp(1.5rem,6vw,2.4rem);letter-spacing:.04em;color:var(--ink);text-shadow:3px 3px 0 var(--yellow);}
.feed-search{height:52px;background:var(--surface);border:3px solid var(--black);border-radius:6px;font-family:var(--font-mono);font-size:.9rem;padding:0 1rem;color:var(--ink);box-shadow:4px 4px 0 var(--black);outline:none;transition:box-shadow .15s;}
.feed-search::placeholder{color:var(--ink-faint);}
.feed-search:focus{box-shadow:0 0 0 3px var(--red);}
/* Distinct accent color (same red used for .feed-new-pill and other
   action buttons) + a visible text label, not just the icon+border look
   #feed-search already uses — was too easy to read as a second search
   control sitting right next to the real one. width:auto instead of a
   fixed square now that there's a label to fit alongside the icon. */
.feed-scan-btn{height:52px;width:auto;padding:0 .9rem;flex:0 0 auto;background:var(--red);color:var(--on-accent);border:3px solid var(--black);border-radius:6px;box-shadow:4px 4px 0 var(--black);display:flex;align-items:center;justify-content:center;gap:.4rem;font-family:var(--font-mono);font-size:.82rem;letter-spacing:.02em;line-height:1;cursor:pointer;transition:transform .1s,box-shadow .1s;}
.feed-scan-btn:hover{transform:translate(-2px,-2px);box-shadow:6px 6px 0 var(--black);}
.feed-scan-btn:active{transform:translate(2px,2px);box-shadow:2px 2px 0 var(--black);}
.feed-scan-btn:focus-visible{box-shadow:0 0 0 3px var(--black);}
.feed-scan-btn:disabled{background:var(--rule);border-color:var(--ink-mid);color:var(--ink-mid);box-shadow:none;transform:none;cursor:not-allowed;}
.feed-list{display:flex;flex-direction:column;gap:1.1rem;}
.feed-empty{font-family:var(--font-accent);color:var(--ink-mid);text-align:center;padding:2rem 0;}
.feed-more{display:flex;justify-content:center;}

/* ── STATIONS SEARCH + DIAL TOOLBAR ────────────────────────────────────
   Sticky under the header so both stay reachable while the list scrolls
   underneath — otherwise scrubbing the dial scrolls the controls
   themselves off-screen, and you'd have to scroll back up to tune again. */
.stations-toolbar{
  position:sticky;top:56px;z-index:50;background:var(--paper);
  width:100%;max-width:640px;display:flex;flex-direction:column;
  align-items:center;gap:.5rem;padding:.75rem 0 .6rem;
  border-bottom:3px solid var(--black);
}

/* ── STATIONS TUNING DIAL ──────────────────────────────────────────────
   A radio-tuner-styled scrubber that lets you drag through the currently
   loaded stations (most-recently-active first) instead of scrolling —
   companion to search, not a replacement: search finds a name you know,
   the dial is for browsing/discovery. */
.stations-dial{width:100%;display:flex;flex-direction:column;align-items:center;gap:.5rem;}
.stations-dial-readout{
  font-family:var(--font-display);font-size:1rem;letter-spacing:.03em;
  color:var(--ink);background:var(--yellow);border:2.5px solid var(--black);
  border-radius:999px;padding:.35rem 1rem;box-shadow:3px 3px 0 var(--black);
  cursor:pointer;max-width:100%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
  transition:transform .1s,box-shadow .1s;
}
.stations-dial-readout:hover{transform:translate(-1px,-1px);box-shadow:4px 4px 0 var(--black);}
.stations-dial-readout:active{transform:translate(1px,1px);box-shadow:2px 2px 0 var(--black);}
.stations-dial-range{
  -webkit-appearance:none;appearance:none;width:100%;height:34px;background:transparent;
  cursor:grab;touch-action:pan-y;
}
.stations-dial-range:active{cursor:grabbing;}
/* Track: a black "frequency bar" with fine red tuning ticks, like an FM dial face. */
.stations-dial-range::-webkit-slider-runnable-track{
  height:10px;border-radius:5px;background:
    repeating-linear-gradient(90deg,var(--red) 0 2px,transparent 2px 4%),
    var(--black);
  border:2px solid var(--black);
}
.stations-dial-range::-moz-range-track{
  height:10px;border-radius:5px;background:
    repeating-linear-gradient(90deg,var(--red) 0 2px,transparent 2px 4%),
    var(--black);
  border:2px solid var(--black);
}
/* Thumb: the needle. */
.stations-dial-range::-webkit-slider-thumb{
  -webkit-appearance:none;appearance:none;width:14px;height:28px;margin-top:-9px;
  background:var(--red);border:2.5px solid var(--black);border-radius:3px;
  box-shadow:2px 2px 0 var(--black);cursor:grab;
}
.stations-dial-range::-moz-range-thumb{
  width:14px;height:28px;background:var(--red);border:2.5px solid var(--black);
  border-radius:3px;box-shadow:2px 2px 0 var(--black);cursor:grab;
}
.stations-dial-range:active::-webkit-slider-thumb{cursor:grabbing;}
.stations-dial-range:active::-moz-range-thumb{cursor:grabbing;}

.search-user.station-tuned{border-color:var(--red);box-shadow:3px 3px 0 var(--red);scroll-margin-top:calc(var(--hdr-h) + 11rem);}

/* ── STATION TAGS ────────────────────────────────────────────────────────
   Twitter-esque: single-word tokens, own vocabulary, own language — a
   station can tag itself "sports" or "fútbol" in whatever language it
   wants, same as any hashtag, but not a multi-word phrase. A space ends
   a tag the same way it ends a hashtag on Twitter (see parseTags()), so
   "sports heartbreak" typed with no comma becomes two separate tags,
   "sports" and "heartbreak" — anyone who wants one combined idea has to
   write it as a single token themselves (SportsHeartbreak).
   Rendered as short pill tokens (max 3, ~24 chars each — enforced in JS
   at save time, see parseTags()) rather than one long red line of prose.
   A whole sentence dressed up with a "#" and alarm-red text read like a
   warning banner, not a tag; discrete pills make the "short label, not a
   paragraph" shape unmistakable regardless of what someone tries to type.
   UPDATE (topic pages): pills are now tap-to-filter — clicking one opens
   #viewTopic, a live-stations-plus-casts view scoped to that tag (see
   openTopic()/loadTopic() and api/topic.php). This supersedes the old
   "no closed list of chips" stance above: search is still how you find a
   tag you don't already know, but once a tag is showing on screen (a cast
   caption, a station's profile) it's now also a doorway into everything
   else using it — same instinct as tapping a hashtag anywhere else. */
.tag-pill-row{display:flex;flex-wrap:wrap;gap:.4rem;margin:.35rem 0 0;}
.tag-pill{
  display:inline-flex;align-items:center;
  font-family:var(--font-mono);font-size:.72rem;
  color:var(--ink);background:var(--surface);
  border:2px solid var(--black);border-radius:999px;
  padding:.2rem .65rem;white-space:nowrap;
  appearance:none;-webkit-appearance:none;cursor:pointer;
  transition:background-color .1s,transform .1s;
}
.tag-pill:hover{background:var(--paper);}
.tag-pill:active{transform:translate(1px,1px);}
.tag-pill:focus-visible{outline:3px solid var(--black);outline-offset:2px;}
.tag-pill::before{content:'#';color:var(--red);margin-right:.2rem;}
/* ── Tag suggestion dropdown (live "chill (482)" style autocomplete) ──── */
.tag-suggest-list{position:absolute;left:0;right:0;top:calc(100% + 4px);background:var(--surface);border:2px solid var(--black);border-radius:6px;box-shadow:3px 3px 0 var(--black);z-index:20;max-height:180px;overflow-y:auto;}
.tag-suggest-item{display:flex;align-items:center;justify-content:space-between;gap:.5rem;padding:.5rem .7rem;font-family:var(--font-mono);font-size:.82rem;cursor:pointer;border-bottom:1px solid var(--rule);}
.tag-suggest-item:last-child{border-bottom:none;}
.tag-suggest-item:hover,.tag-suggest-item.active{background:var(--paper);}
.tag-suggest-item .tag-suggest-name::before{content:'#';color:var(--red);}
.tag-suggest-item .tag-suggest-count{color:var(--ink-faint);font-size:.75rem;}
.search-user-tags{display:block;margin-top:.3rem;min-width:0;}
/* Browse-row tags (station/voice list) stay on a single line and scroll
   horizontally instead of wrapping — wrapping let a 3-tag station's card
   grow ~2.5x taller than a tag-less one, and that mismatched height was
   what made the fixed-size avatar next to it read as squashed/oval even
   though .cast-avatar itself never actually resizes. One predictable row
   height keeps the list scannable and keeps the avatar reading as round.
   Scoped to .search-user-tags only — profile pages, cast captions, and
   the trending strip keep the original wrapping .tag-pill-row behavior,
   since they don't sit next to a fixed-size avatar in a tight row. */
.search-user-tags .tag-pill-row{
  flex-wrap:nowrap;
  overflow-x:auto;
  -webkit-overflow-scrolling:touch;
  scrollbar-width:none;
  -ms-overflow-style:none;
  mask-image:linear-gradient(to right, #000 calc(100% - 20px), transparent 100%);
  -webkit-mask-image:linear-gradient(to right, #000 calc(100% - 20px), transparent 100%);
}
.search-user-tags .tag-pill-row::-webkit-scrollbar{display:none;}
.search-user-tags .tag-pill{font-size:.68rem;padding:.15rem .55rem;flex-shrink:0;}
/* Mouse-only click-drag affordance — see the pointerdown/pointermove
   handler below this markup's script section. Touch never sees this
   cursor (touch has no cursor), so it doesn't affect the native
   swipe-to-scroll phones already get for free. */
.search-user-tags .tag-pill-row{cursor:grab;}
.search-user-tags .tag-pill-row .tag-pill{cursor:inherit;}
.search-user-tags .tag-pill-row.tag-pill-row--dragging{cursor:grabbing;user-select:none;}
.search-user-tags .tag-pill-row.tag-pill-row--dragging .tag-pill{pointer-events:none;}

/* ── TOPIC VIEW ── — #viewTopic, opened by tapping any #tag pill.
   Live stations currently broadcasting under this tag first (reuses the
   same .search-user row + .live-badge markup as everywhere else live
   status shows up), then every cast tagged with it below (reuses
   castCard() — same cards as the feed, nothing new to render/maintain). */
.topic-live-section{width:100%;max-width:640px;display:flex;flex-direction:column;gap:.5rem;margin-bottom:.25rem;}
.topic-live-label{font-family:var(--font-display);font-size:.8rem;letter-spacing:.06em;color:var(--ink-mid);margin:0;}
.topic-live-list{display:flex;flex-direction:column;gap:.5rem;}

/* ── TRENDING TAGS STRIP — feed, above the list ── */
.trending-tags-row{display:flex;flex-wrap:wrap;gap:.4rem;width:100%;max-width:640px;}
.trending-tags-row .tag-pill{font-size:.72rem;}
/* No .trend-count here on purpose — this strip is a discovery directory,
   not a leaderboard, so tap counts intentionally never surface on these
   pills (unlike the all-topics list below, which still shows them). The
   underlying count still drives server-side sort order in trending_tags.php
   and trending_station_tags.php — the stations-tab strip below reuses this
   exact same .trending-tags-row/.tag-pill styling, just fed from a
   different endpoint. */

/* ── "see all topics" link — escape hatch out of the trending strip ────────
   trending-tags-row is deliberately bounded to a short recent window with
   a usage floor (see trending_tags.php), so most tags that exist are never
   going to show up there. This is the fallback: an all-time, searchable
   directory (#viewAllTopics / api/all_tags.php) reachable from here even
   when the trending strip is empty or just doesn't have the tag you want. */
.all-topics-link{
  width:100%;max-width:640px;text-align:left;
  background:none;border:none;padding:0;
  font-family:var(--font-mono);font-size:.78rem;color:var(--ink-mid);
  text-decoration:underline;cursor:pointer;margin-top:-.25rem;
}
.all-topics-link:hover{color:var(--ink);}

/* Caption above the trending strip — just states the window (3 days) so
   it reads as "recent activity," not an all-time ranking. Deliberately
   doesn't mention the server-side usage floor (see trending_tags.php) —
   that's an implementation detail, not something users need to know. */
.trending-tags-label{
  width:100%;max-width:640px;
  font-family:var(--font-mono);font-size:.72rem;color:var(--ink-faint);
  margin:0 0 -.2rem;
}


.cast-card{background:var(--surface);border:3px solid var(--black);border-radius:8px;box-shadow:5px 5px 0 var(--black);padding:1rem 1.1rem;display:flex;flex-direction:column;gap:.65rem;transition:border-color .15s;}
.cast-card.playing{border-color:var(--red);}
.cast-top{display:flex;align-items:center;gap:.7rem;background:none;border:none;padding:0;text-align:left;width:100%;font-family:inherit;cursor:pointer;}
.cast-top:focus-visible{outline:3px solid var(--black);outline-offset:2px;}
.cast-avatar{width:46px;height:46px;flex-shrink:0;border-radius:50%;border:3px solid var(--black);display:flex;align-items:center;justify-content:center;overflow:hidden;}
.cast-who{display:flex;flex-direction:column;line-height:1.3;}
.cast-name{font-family:var(--font-display);font-size:1rem;letter-spacing:.03em;color:var(--ink);}
.cast-handle{font-family:var(--font-mono);font-size:.8rem;color:var(--ink-mid);}
.cast-caption{font-family:var(--font-accent);font-size:1rem;color:var(--ink-mid);}
.cast-play-row{display:flex;align-items:center;gap:.7rem;}
.cast-waveform{flex:1;height:40px;display:flex;align-items:center;gap:1.5px;overflow:hidden;cursor:pointer;}
.cast-waveform:focus-visible,.cast-progress:focus-visible{outline:2px solid var(--black);outline-offset:2px;border-radius:4px;}
.cast-waveform .bar{flex-shrink:0;width:3px;border-radius:2px;background:var(--ink-faint);transition:background .1s;}
.cast-waveform .bar.played{background:var(--red);}
.cast-progress{flex:1;height:4px;background:var(--ink-faint);border-radius:2px;overflow:hidden;cursor:pointer;}
.cast-progress-fill{height:100%;width:0%;background:var(--red);border-radius:2px;transition:width .1s linear;}
.cast-play{width:38px;height:38px;flex-shrink:0;border-radius:50%;background:var(--red);border:3px solid var(--black);box-shadow:3px 3px 0 var(--black);color:var(--on-accent);font-size:.8rem;line-height:1;display:flex;align-items:center;justify-content:center;cursor:pointer;transition:transform .1s,box-shadow .1s;}
.cast-play:hover{transform:translate(-1px,-1px);box-shadow:4px 4px 0 var(--black);}
.cast-play:active{transform:translate(2px,2px);box-shadow:1px 1px 0 var(--black);}
.cast-progress{flex:1;height:18px;border-radius:3px;background-image:repeating-linear-gradient(90deg,var(--rule) 0 3px,transparent 3px 6px);}
.cast-card.playing .cast-progress{background-image:repeating-linear-gradient(90deg,var(--red) 0 3px,transparent 3px 6px);background-size:200% 100%;animation:wavemove .6s linear infinite;}
@keyframes wavemove{from{background-position-x:0;}to{background-position-x:-12px;}}
.cast-duration{font-family:var(--font-display);font-size:.8rem;letter-spacing:.08em;color:var(--ink-mid);flex-shrink:0;}
.cast-stats{font-family:var(--font-mono);font-size:.78rem;color:var(--ink-faint);border-top:2px dashed var(--rule);padding-top:.6rem;display:flex;align-items:center;gap:.4rem;flex-wrap:wrap;}
.cast-stats strong{color:var(--ink);}
.cast-replies{font-family:var(--font-display);color:var(--red);letter-spacing:.05em;cursor:pointer;}
.cast-like{display:inline-flex;align-items:center;gap:.3rem;cursor:pointer;font-family:var(--font-display);letter-spacing:.04em;color:var(--ink-mid);}
.cast-like.liked{color:var(--red);}
.cast-dot{color:var(--ink-faint);}
.cast-card-deleted{opacity:.65;}
.cast-unavailable{font-family:var(--font-mono);font-size:.85rem;color:var(--ink-faint);font-style:italic;}
.cast-delete{font-family:var(--font-mono);color:var(--ink-faint);cursor:pointer;font-size:.78rem;background:none;border:none;padding:0;}
.cast-delete:hover{color:var(--red);text-decoration:underline;}
.cast-report{background:none;border:none;padding:0;font-family:inherit;}
.cast-bookmark{cursor:pointer;color:var(--ink-faint);display:inline-flex;align-items:center;background:none;border:none;padding:0;font-family:inherit;}
.cast-bookmark.bookmarked{color:var(--red);}

/* ── "more" overflow menu (pin / delete / report) ──────────────────────
   These three are owner/moderation actions, not public engagement stats,
   so they don't belong flattened into .cast-stats alongside like/replies/
   share/bookmark — that's what was causing the row to wrap mid-group and
   strand "pin · delete" on its own orphaned line at narrow widths (see
   kaiuntatheme.css's old narrow-width .cast-stats font/gap hack, now
   removed — this is the real fix, that was a band-aid on the symptom).
   Tucking them behind a single ⋯ trigger keeps the public row short
   enough to always fit on one line, and matches the ⋯ language already
   used for the header's own menu button. */
.cast-more{position:relative;display:inline-flex;}
.cast-more-btn{background:none;border:none;padding:0 .2rem;font-family:inherit;font-size:1rem;line-height:1;color:var(--ink-faint);cursor:pointer;}
.cast-more-btn:hover{color:var(--ink);}
.cast-more.open .cast-more-btn{color:var(--ink);}
.cast-more-menu{display:none;flex-direction:column;position:absolute;bottom:calc(100% + 6px);right:0;min-width:150px;background:var(--surface);border:2px solid var(--black);border-radius:6px;box-shadow:3px 3px 0 var(--black);padding:.3rem;gap:.1rem;z-index:20;}
.cast-more.open .cast-more-menu{display:flex;}
.cast-more-item{width:100%;text-align:left;background:none;border:none;padding:.4rem .5rem;font-family:var(--font-mono);font-size:.78rem;color:var(--ink-mid);cursor:pointer;border-radius:4px;white-space:nowrap;}
.cast-more-item:hover{background:var(--paper);}
.cast-more-item:disabled{opacity:.4;cursor:not-allowed;}
.cast-more-item:disabled:hover{background:none;}
.cast-more-item.pinned{color:var(--red);}
.cast-more-item.cast-delete:hover{color:var(--red);background:var(--paper);text-decoration:none;}

/* ── caption/tags row — tags + "edit tags" flow on one line together
   instead of edit-tags being forced onto its own row underneath. The
   pill row and the edit button are both flex items of the same
   container now (.cast-caption-display uses display:contents so its
   child .tag-pill-row participates directly), and the row wraps as a
   whole via flex-wrap rather than each block-level child forcing a
   hard break. */
.cast-caption-row{display:flex;flex-wrap:wrap;align-items:center;column-gap:.5rem;row-gap:.35rem;}
.cast-caption-display{display:contents;}
.cast-caption-edit{flex:1 0 100%;margin-top:.2rem;}
.cast-edit-tags{background:none;border:none;padding:0;font-family:var(--font-mono);font-size:.72rem;color:var(--ink-faint);cursor:pointer;white-space:nowrap;}
.cast-edit-tags:hover{color:var(--ink);text-decoration:underline;}

/* ── REPORT MODAL ── */
.modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.5);display:flex;align-items:center;justify-content:center;z-index:200;padding:1rem;}
.modal{background:var(--surface);border:3px solid var(--black);border-radius:8px;box-shadow:6px 6px 0 var(--black);padding:1.5rem;width:100%;max-width:400px;display:flex;flex-direction:column;gap:.75rem;}
.modal h2{font-family:var(--font-display);font-size:1.3rem;letter-spacing:.03em;margin:0;}
.modal select,.modal textarea{width:100%;font-family:var(--font-mono);font-size:.85rem;padding:.5rem .7rem;border:2px solid var(--black);border-radius:4px;background:var(--surface);box-sizing:border-box;}
.modal textarea{resize:vertical;min-height:60px;}
.modal-actions{display:flex;gap:.5rem;justify-content:flex-end;margin-top:.25rem;}
.modal-actions button{flex:0 0 auto;width:auto;padding:.5rem 1.2rem;font-size:.85rem;}
.btn-sm{padding:.35rem .8rem;font-size:.78rem;box-shadow:2px 2px 0 var(--black);}

.prompt-banner{display:none;width:100%;max-width:640px;height:3.5rem;box-sizing:border-box;background:var(--yellow);border:3px solid var(--black);border-radius:6px;padding:.6rem .8rem;font-family:var(--font-mono);font-size:.8rem;line-height:1.4;text-align:center;align-items:center;justify-content:center;overflow:hidden;}
.prompt-banner.show{display:flex;}
.prompt-banner-text{display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;line-clamp:2;overflow:hidden;text-overflow:ellipsis;width:100%;transition:opacity .35s ease;}
.record-prompt{display:none;width:100%;height:5rem;box-sizing:border-box;background:var(--yellow);border:3px solid var(--black);border-radius:6px;padding:.6rem .8rem;font-family:var(--font-mono);font-size:.8rem;line-height:1.4;text-align:center;align-items:center;justify-content:center;overflow:hidden;}
.record-prompt.show{display:flex;}
.record-prompt .prompt-banner-text{width:100%;-webkit-line-clamp:3;line-clamp:3;}
.suggest-prompt-link{display:none;background:none;border:none;font-family:var(--font-mono);font-size:.72rem;color:var(--red);text-decoration:underline;cursor:pointer;padding:.15rem 0;margin-bottom:.4rem;}
.suggest-prompt-link.show{display:inline-block;}
.suggest-prompt-form{display:none;width:100%;flex-direction:column;gap:.4rem;margin-bottom:.4rem;}
.suggest-prompt-form.show{display:flex;}
.suggest-prompt-input{width:100%;font-family:var(--font-mono);font-size:.85rem;padding:.5rem .6rem;border:2px solid var(--black);border-radius:6px;background:#fff;}
.suggest-prompt-actions{display:flex;gap:.5rem;justify-content:flex-end;}
.suggest-prompt-actions button{font-family:var(--font-mono);font-size:.72rem;border:2px solid var(--black);border-radius:6px;background:var(--yellow);padding:.3rem .6rem;cursor:pointer;}
.suggest-prompt-status{font-family:var(--font-mono);font-size:.72rem;text-align:center;min-height:1em;}
.feed-new-pill{display:none;align-self:center;font-family:var(--font-display);font-size:.95rem;letter-spacing:.04em;color:var(--on-accent);background:var(--red);border:3px solid var(--black);border-radius:30px;box-shadow:3px 3px 0 var(--black);padding:.45rem 1.1rem;cursor:pointer;transition:transform .1s,box-shadow .1s;}
.feed-new-pill.show{display:inline-flex;align-items:center;gap:.4rem;}
.feed-new-pill:hover{transform:translate(-1px,-1px);box-shadow:4px 4px 0 var(--black);}
.feed-new-pill:active{transform:translate(2px,2px);box-shadow:1px 1px 0 var(--black);}

/* ── FEED TABS ── */
.feed-tabs{width:100%;max-width:640px;display:flex;gap:.4rem;margin-bottom:.25rem;}
.feed-tab{flex:1;display:inline-flex;align-items:center;justify-content:center;gap:.35rem;padding:.4rem;font-family:var(--font-display);font-size:.95rem;letter-spacing:.05em;background:none;border:2px solid var(--black);border-radius:4px;cursor:pointer;color:var(--ink-mid);transition:all .15s;}
.feed-tab svg{flex-shrink:0;}
.feed-tab.active{background:var(--black);color:var(--on-accent);}
@media(max-width:480px){
  .feed-tabs{gap:.3rem;}
  .feed-tab{padding:.3rem .2rem;font-size:.78rem;gap:.25rem;}
  .feed-tab svg{width:12px;height:12px;}
}

/* ── NOTIFICATIONS ── */
#viewNotifications{background:var(--paper);align-items:center;padding:calc(var(--hdr-h) + 1.25rem) 1.25rem 6rem;gap:1rem;}
.notif-list{width:100%;max-width:640px;display:flex;flex-direction:column;gap:.6rem;}
.notif-item{display:flex;align-items:flex-start;gap:.75rem;background:var(--surface);border:2px solid var(--black);border-radius:6px;padding:.7rem .9rem;cursor:pointer;box-shadow:3px 3px 0 var(--black);transition:transform .1s;}
.notif-item:active{transform:translate(2px,2px);box-shadow:1px 1px 0 var(--black);}
.notif-item.unread{border-color:var(--red);background:#fff9f9;}
.notif-avatar{flex-shrink:0;}
.notif-body{flex:1;display:flex;flex-direction:column;gap:.15rem;}
.notif-text{font-family:var(--font-mono);font-size:.85rem;line-height:1.4;}
.notif-text strong{color:var(--ink);}
.notif-time{font-family:var(--font-mono);font-size:.75rem;color:var(--ink-mid);}
.hdr-bell{position:relative;width:34px;height:34px;display:flex;align-items:center;justify-content:center;background:var(--icon-fill, rgba(0,0,0,.08));border:none;border-radius:50%;color:var(--on-accent);font-size:1rem;cursor:pointer;padding:0;}
.hdr-bell:hover{background:var(--icon-fill-hover, rgba(0,0,0,.14));}
.bell-badge{position:absolute;top:-2px;right:-4px;background:var(--red);color:var(--on-accent);font-family:var(--font-display);font-size:.65rem;min-width:16px;height:16px;border-radius:8px;display:flex;align-items:center;justify-content:center;padding:0 3px;border:2px solid var(--black);}

/* ── SEARCH ── */
#viewSearch{background:var(--paper);align-items:center;padding:calc(var(--hdr-h) + 1.25rem) 1.25rem 6rem;gap:1rem;}
.search-box{width:100%;max-width:640px;display:flex;gap:.5rem;align-items:stretch;}
.search-box input{flex:1;min-width:0;font-family:var(--font-mono);font-size:.95rem;padding:.5rem .8rem;border:3px solid var(--black);border-radius:4px;background:var(--surface);box-sizing:border-box;}
.search-box button{flex:0 0 64px;white-space:nowrap;}
.search-section{width:100%;max-width:640px;}
.search-section h2{font-family:var(--font-display);font-size:1rem;letter-spacing:.05em;color:var(--ink-mid);margin:0 0 .5rem;}
.search-users{display:flex;flex-direction:column;gap:.5rem;margin-bottom:1rem;}
.search-user{display:flex;align-items:center;gap:.75rem;background:var(--surface);border:2px solid var(--black);border-radius:6px;padding:.6rem .8rem;cursor:pointer;box-shadow:3px 3px 0 var(--black);}
.search-user:active{transform:translate(2px,2px);box-shadow:1px 1px 0 var(--black);}
.search-user:focus-visible,.notif-item:focus-visible{outline:3px solid var(--black);outline-offset:2px;}
.search-user-info{display:flex;flex-direction:column;flex:1 1 auto;min-width:0;}
.search-user-name{font-family:var(--font-display);font-size:1rem;letter-spacing:.03em;}
.search-user-handle{font-family:var(--font-mono);font-size:.75rem;color:var(--ink-mid);}
.search-casts{display:flex;flex-direction:column;gap:1.1rem;}

/* ── BOOKMARKS ── */
#viewBookmarks{background:var(--paper);align-items:center;padding:calc(var(--hdr-h) + 1.25rem) 1.25rem 6rem;gap:1rem;}

/* ── PROFILE ── */
#viewProfile{background:var(--paper);align-items:center;padding:calc(var(--hdr-h) + 1.25rem) 1.25rem 6rem;gap:1rem;}
.profile-header{width:100%;max-width:640px;background:var(--surface);border:3px solid var(--black);border-radius:8px;box-shadow:5px 5px 0 var(--black);padding:1.25rem 1.1rem;display:flex;flex-direction:column;gap:.75rem;}
.profile-top{display:flex;align-items:center;gap:1rem;}
.profile-avatar{width:64px;height:64px;flex-shrink:0;border-radius:50%;border:3px solid var(--black);display:flex;align-items:center;justify-content:center;overflow:hidden;}
.profile-info{flex:1;display:flex;flex-direction:column;gap:.2rem;}
.profile-name{font-family:var(--font-display);font-size:1.3rem;letter-spacing:.03em;}
.profile-handle{font-family:var(--font-mono);font-size:.82rem;color:var(--ink-mid);}
.profile-bio{font-family:var(--font-mono);font-size:.85rem;color:var(--ink-mid);line-height:1.5;}
.profile-stats{display:flex;gap:1.5rem;}
.profile-stat{display:flex;flex-direction:column;align-items:center;gap:.1rem;}
/* Own-eyes-only — deliberately NOT styled like .profile-stat-tab (no
   pointer cursor, no click target, no active/inactive state) since these
   aren't a filter into anything, just numbers. Kept visually separate
   (its own bordered strip, own label) from the public voices/replies/
   followers/following row above so it doesn't read as one more public
   stat — the "only you can see this" label exists because otherwise
   there's nothing on screen distinguishing a private number from a
   public one. */
.profile-private-stats{display:flex;align-items:center;gap:1rem;flex-wrap:wrap;margin-top:.6rem;padding:.5rem .8rem;border:1px dashed var(--rule);border-radius:6px;font-family:var(--font-mono);}
.profile-private-stats-label{font-size:.65rem;color:var(--ink-faint);letter-spacing:.03em;white-space:nowrap;}
.profile-private-stats-row{display:flex;gap:1rem;flex-wrap:wrap;font-size:.78rem;color:var(--ink-mid);}
.profile-private-stats-row strong{color:var(--ink);}
.profile-pinned-reel{display:none;flex-direction:column;gap:.6rem;margin-top:1rem;padding:.8rem;border:2px solid var(--black);border-radius:8px;background:var(--surface);}
.profile-pinned-reel.has-items{display:flex;}
.profile-pinned-reel-label{font-family:var(--font-display);font-size:.85rem;letter-spacing:.05em;color:var(--ink-mid);}
.cast-pin{font-family:var(--font-mono);color:var(--ink-faint);cursor:pointer;font-size:.78rem;background:none;border:none;padding:0;}
.cast-pin:hover{color:var(--ink);text-decoration:underline;}
.cast-pin.pinned{color:var(--red);}
.cast-pin:disabled{opacity:.4;cursor:not-allowed;text-decoration:none;}
.cast-pin:disabled:hover{color:var(--ink-faint);text-decoration:none;}
.profile-stat strong{font-family:var(--font-display);font-size:1.1rem;letter-spacing:.03em;}
.profile-stat span{font-family:var(--font-mono);font-size:.72rem;color:var(--ink-mid);}
.profile-actions{display:flex;gap:.6rem;}
.profile-list{width:100%;max-width:640px;display:flex;flex-direction:column;gap:1.1rem;}
.profile-lock-card{width:100%;background:var(--black);color:var(--on-accent);border:3px solid var(--black);border-radius:8px;box-shadow:5px 5px 0 var(--red);padding:1.5rem 1.25rem;display:flex;flex-direction:column;align-items:center;gap:.9rem;text-align:center;}
.profile-lock-text{font-family:var(--font-mono);font-size:.88rem;line-height:1.5;max-width:34ch;}
.profile-lock-card .btn-primary{width:auto;max-width:280px;padding:0 1.5rem;height:50px;font-size:.95rem;}

/* ── EDIT PROFILE ── */
.profile-edit{margin-top:.5rem;display:flex;flex-direction:column;gap:.6rem;}
.profile-edit textarea{width:100%;box-sizing:border-box;font-family:var(--font-mono);font-size:.85rem;padding:.5rem .7rem;border:2px solid var(--black);border-radius:4px;resize:vertical;min-height:70px;background:var(--surface);}
.profile-edit-actions{display:flex;gap:.6rem;align-items:center;flex-wrap:wrap;}
.profile-avatar-wrap{position:relative;display:inline-block;}
.profile-avatar-wrap input[type=file]{display:none;}
.avatar-edit-btn{position:absolute;bottom:0;right:0;width:20px;height:20px;background:var(--red);border:2px solid var(--black);border-radius:50%;color:var(--on-accent);font-size:.6rem;display:flex;align-items:center;justify-content:center;cursor:pointer;}

/* ── AVATAR FLIP (default icon ⇄ uploaded photo) ────────────────────────────
   Only used on the large profile-page avatar, not the small ones scattered
   through the feed/notifications — those are numerous and glanced at while
   scrolling, so an animation there would be noise rather than delight.
   Structure: two faces inside a 3D-perspective wrapper — front is the SAME
   default every avatar already shows with no photo (avatarStyle() color +
   the initials()/broadcast-icon svg), back is the uploaded photo. A
   .flipped class rotates the whole card. JS drives .flipped: added once
   automatically when the photo finishes loading (briefly reveals it),
   removed after a pause, then toggled again on hover/tap. Stations with no
   uploaded photo never get this markup at all — they just show the plain
   original default (colored circle + icon), no flip, no JS. */
.avatar-flip{width:64px;height:64px;perspective:600px;flex-shrink:0;}
.avatar-flip--sm{width:46px;height:46px;}
.avatar-flip-inner{position:relative;width:100%;height:100%;transition:transform .6s cubic-bezier(.4,.2,.2,1);transform-style:preserve-3d;}
.avatar-flip.flipped .avatar-flip-inner{transform:rotateY(180deg);}
.avatar-face{position:absolute;inset:0;border-radius:50%;border:3px solid var(--black);display:flex;align-items:center;justify-content:center;overflow:hidden;backface-visibility:hidden;}
.avatar-face--front{background:var(--surface);}
.avatar-face--back{transform:rotateY(180deg);}
.avatar-face--back img{width:100%;height:100%;object-fit:cover;display:block;}

/* ── AVATAR EDIT MODAL (crop/zoom/rotate/flip/brightness/contrast) ──────────
   Opens between "user picked a file" and "file gets uploaded" (see the
   avatarFileInput change handler in JS). The canvas itself always stays a
   fixed-size SQUARE — that's the shape actually uploaded/stored, matching
   every other avatar in the app (object-fit:cover + border-radius:50% is
   what turns a square photo circular, see .profile-avatar/.avatar-face
   above). This wrapper just clips the ON-SCREEN preview to a circle so
   the editing experience matches how the photo will actually be shown;
   corners outside that circle are still part of the exported square file,
   they're just never visible anywhere in the app. */
.avatar-edit-modal{max-width:380px;align-items:center;}
.avatar-edit-modal h2{align-self:flex-start;}
.avatar-edit-stage-wrap{width:240px;height:240px;border-radius:50%;overflow:hidden;border:3px solid var(--black);touch-action:none;cursor:grab;background:var(--ink-faint);flex-shrink:0;}
.avatar-edit-stage-wrap:active{cursor:grabbing;}
.avatar-edit-stage-wrap canvas{width:100%;height:100%;display:block;}
.avatar-edit-hint{font-family:var(--font-mono);font-size:.7rem;color:var(--ink-faint);text-align:center;margin:0;}
.avatar-edit-controls{width:100%;display:flex;flex-direction:column;gap:.3rem;}
.avatar-edit-row{display:flex;gap:.4rem;justify-content:center;margin-bottom:.3rem;}
.avatar-edit-row button{width:auto;flex:1;height:38px;font-size:.85rem;box-shadow:3px 3px 0 var(--black);}
.avatar-edit-label{display:flex;justify-content:space-between;font-family:var(--font-mono);font-size:.7rem;color:var(--ink-mid);text-transform:uppercase;letter-spacing:.06em;margin-top:.3rem;}
.avatar-edit-controls input[type=range]{width:100%;}

/* ── RECORD FAB ── */
/* Used to be a stack of two (record + scan) — scan now lives as a static
   .feed-scan-btn inline in the stations toolbar instead (see markup
   comment near #stationsToolbar), so .fab-stack just wraps this one
   button now. Kept as its own wrapper rather than folding the fixed
   positioning directly onto .record-fab, in case something else floats
   off this same bottom-right anchor later. */
.fab-stack{position:fixed;bottom:calc(1.5rem + 56px);right:1.5rem;z-index:90;display:flex;flex-direction:column;align-items:flex-end;gap:12px;}
.record-fab{width:64px;height:64px;border-radius:50%;background:var(--red);border:4px solid var(--black);box-shadow:6px 6px 0 var(--black);color:var(--on-accent);font-size:1.6rem;display:flex;align-items:center;justify-content:center;cursor:pointer;transition:transform .1s,box-shadow .1s;}
.record-fab:hover{transform:translate(-2px,-2px);box-shadow:8px 8px 0 var(--black);}
.record-fab:active{transform:translate(3px,3px);box-shadow:2px 2px 0 var(--black);}
.record-overlay{position:fixed;inset:0;background:rgba(17,17,17,.55);display:none;align-items:center;justify-content:center;z-index:200;padding:1.25rem;overflow-y:auto;}
.record-overlay.active{display:flex;}
.record-card{width:min(380px,92vw);background:var(--surface);border:3px solid var(--black);border-radius:8px;box-shadow:6px 6px 0 var(--black);padding:1.5rem;display:flex;flex-direction:column;align-items:center;gap:1.1rem;}
.record-title{font-family:var(--font-display);font-size:1.3rem;letter-spacing:.04em;}
.record-title:focus{outline:none;}
.record-title:focus-visible{outline:3px solid var(--black);outline-offset:3px;border-radius:2px;}
.record-circle{width:92px;height:92px;border-radius:50%;border:4px solid var(--black);background:var(--red);color:var(--on-accent);font-size:2rem;display:flex;align-items:center;justify-content:center;cursor:pointer;transition:transform .1s;padding:0;}
.record-circle:focus-visible{outline:3px solid var(--black);outline-offset:3px;}
.record-circle.recording{background:var(--yellow);color:var(--black);animation:pulse 1s infinite;}
@keyframes pulse{0%,100%{transform:scale(1);}50%{transform:scale(1.07);}}
.record-timer{font-family:var(--font-display);font-size:1.4rem;color:var(--ink-mid);}
.record-hint{font-size:.78rem;color:var(--ink-mid);text-align:center;}
.record-caption{width:100%;height:46px;border:3px solid var(--black);border-radius:6px;padding:0 .8rem;font-family:var(--font-mono);font-size:.9rem;outline:none;}
.record-caption:focus{box-shadow:0 0 0 3px var(--yellow);}

/* Was visually-hidden (sr-only) before — the caption field sat as a plain
   text box with only long grey placeholder text to explain itself, easy
   to miss right next to the big POST button. Promoted to a real, visible
   label so "this is where tags go" registers before someone posts, not
   after. "#" kept red to match tag-pill::before's accent so the label
   visually previews what the pill will look like once posted. */
.record-caption-label{display:flex;align-items:center;gap:.3rem;width:100%;font-family:var(--font-mono);font-size:.72rem;font-weight:700;letter-spacing:.03em;color:var(--ink-mid);text-transform:uppercase;}
.record-caption-label .tag-accent{color:var(--red);font-weight:700;}
.record-actions{display:flex;gap:.6rem;width:100%;}
.record-actions .btn-primary,.record-actions .btn-ghost{height:48px;font-size:.85rem;box-shadow:4px 4px 0 var(--black);}

/* Install banner only: left-align the title/body copy instead of the
   centered stack every other .record-card modal uses. Scoped to
   #installBanner specifically so recordOverlay/anonTokenOverlay/etc keep
   their centered look — this is the one modal that reads better as a
   left-aligned little "card" with full-width buttons below it. */
#installBanner .record-card{align-items:stretch;}
#installBanner .record-title,
#installBanner .record-hint{text-align:left;}

/* Same treatment as #installBanner above: left-align the title/body copy,
   keep the stacked full-width buttons as they already are. */
#confirmStationOverlay .record-card{align-items:stretch;}
#confirmStationOverlay .record-title,
#confirmStationOverlay .record-hint{text-align:left;}

/* Same again for the "save this code" modal — title/hint left-aligned,
   the token input row and buttons are untouched (already full-width). */
#anonTokenOverlay .record-card{align-items:stretch;}
#anonTokenOverlay .record-title,
#anonTokenOverlay .record-hint{text-align:left;}
.record-error{color:var(--red);font-size:.8rem;text-align:center;min-height:1.1em;}
.record-preview{display:none;width:100%;flex-direction:column;gap:.8rem;align-items:center;}
.record-preview.active{display:flex;}
.record-card.has-preview .record-pre-record{display:none;}

@keyframes fadeUp{from{opacity:0;transform:translateY(14px);}to{opacity:1;transform:translateY(0);}}
@keyframes cta-jolt{0%{transform:scale(1) rotate(0deg);}25%{transform:scale(1.12) rotate(-3deg);}50%{transform:scale(0.96) rotate(2deg);}75%{transform:scale(1.06) rotate(-1deg);}100%{transform:scale(1) rotate(0deg);}}
.cta-jolt{animation:cta-jolt .5s ease;}

/* ── STREAM VISUALIZER ──
   Fixed, full-viewport canvas that sits ON TOP of the current view as a
   translucent overlay (pointer-events:none, so taps/scrolls pass straight
   through — you can keep browsing/replying while it plays) rather than
   behind it. Behind-content was the first attempt (z-index:-1, relying on
   gaps in the page to show through) but several views — .hero-bangers
   among them — paint a fully opaque position:fixed background with zero
   gaps anywhere, so it was never actually visible. z-index:40 keeps it
   below the sticky filter bar (50), record button (90), footer (99),
   header (100), and modals (200), so chrome stays crisp on top; the low
   per-shape alpha values in the draw functions (see the "stream
   visualizer" JS block) are what keep it reading as a wash rather than a
   full takeover, not the layering. Theme-specific look (halftone glow vs.
   breathing ink circles) is chosen in JS by reading data-theme at draw
   time. Fades in on startAutoStream(), out on stopAutoStream(). */
.stream-viz{position:fixed;inset:0;width:100vw;height:100vh;z-index:40;pointer-events:none;opacity:0;transition:opacity .6s ease;}
.stream-viz.active{opacity:1;}

/* ── STREAM LOADING TOAST ──
   Small pill shown the instant "stream" is pressed, hidden once the
   first track actually starts playing — see showStreamLoadingToast()/
   hideStreamLoadingToast() near the auto-stream JS. The button itself
   flips to its "active"/playing look synchronously on click (see
   setAutoStreamBtnState(), needed for the iOS audio-unlock gesture), so
   without this there's a beat where the footer already looks like it's
   playing but nothing's audible yet — reads as broken rather than
   loading. Shape/position shared here; colors and type are per-theme
   (see justwitter.css / kaiuntatheme.css). Sits above the footer (99)
   but below modals (200), same layering note as .stream-viz above. */
.stream-loading-toast{position:fixed;left:50%;bottom:calc(var(--ftr-h) + 14px);transform:translateX(-50%) translateY(8px);z-index:105;padding:.55rem 1.1rem;border-radius:999px;font-size:.8rem;opacity:0;pointer-events:none;transition:opacity .2s ease,transform .2s ease;white-space:nowrap;}
.stream-loading-toast.visible{opacity:1;transform:translateX(-50%) translateY(0);}

/* ── FOOTER ── */
.site-footer{position:fixed;bottom:0;left:0;right:0;background:var(--black);border-top:4px solid var(--red);padding:0 1rem;height:var(--ftr-h);display:flex;align-items:center;gap:.75rem;z-index:99;}
.footer-link{background:none;border:none;font-family:var(--font-display);font-size:.95rem;letter-spacing:.12em;color:var(--on-accent);cursor:pointer;padding:0;text-decoration:none;transition:color .15s;}
.footer-link:hover{color:var(--yellow);}
.footer-right{margin-left:auto;display:flex;align-items:center;gap:.5rem;}
.footer-feed-btn{background:var(--icon-fill, rgba(0,0,0,.08));border:none;border-radius:4px;padding:.4rem 1rem;}
.footer-feed-btn:hover{background:var(--icon-fill-hover, rgba(0,0,0,.14));}
.footer-stations-btn{background:var(--icon-fill, rgba(0,0,0,.08));border:none;border-radius:4px;padding:.4rem 1rem;}
.footer-stations-btn:hover{background:var(--icon-fill-hover, rgba(0,0,0,.14));}
.footer-autostream-btn{background:var(--icon-fill, rgba(0,0,0,.08));border:none;border-radius:4px;padding:.4rem 1rem;}
.footer-autostream-btn:hover{background:var(--icon-fill-hover, rgba(0,0,0,.14));}
/* Active/playing state — filled yellow so it reads as "live" from across
   the room, distinct from the plain outline the other footer buttons use. */
.footer-autostream-btn.active{background:var(--yellow);color:var(--black);border-color:var(--yellow);}
.footer-autostream-btn.active:hover{background:var(--surface);color:var(--red);}

.menu-account{margin-bottom:.5rem;}
.menu-logout-btn{width:100%;text-align:left;background:none;border:3px solid var(--black);border-radius:6px;padding:.85rem 1rem;font-family:var(--font-display);font-size:1.1rem;letter-spacing:.06em;color:var(--red);cursor:pointer;transition:background .15s,color .15s;}
.menu-logout-btn:hover{background:var(--red);color:var(--on-accent);}

/* voices/stations menu links: justwitter never styled these (no CSS rule
   for .menu-nav-link at all — what showed was bare unstyled <button>
   chrome, not an intentional design), and the footer already covers the
   same two destinations there. Hidden by default, same pattern as
   .hero-kaiunta above; kaiuntatheme.css's own .menu-nav-links rule (which
   sets display:flex unconditionally) overrides this back on for kaiunta,
   same way .hero-kaiunta gets unhidden. */
.menu-nav-links{display:none;}

/* Text color for the accordion/close link/legal copy below was
   var(--black) — a token meant for structural borders/shadows, not
   body text. It happened to work for every theme whose --black is
   basically the same value as --ink (justwitter, kaiunta, folio,
   royale, pulse, y2k, monolith all define the two identically or
   near-identically), but phosphor/terminal/session deliberately keep
   --black as true near-black for borders while --ink is their
   bright/light readable text color — so this text rendered
   black-on-near-black there. Switched to var(--ink), the token every
   theme already guarantees contrasts with var(--paper). */
.menu-accordion{display:flex;flex-direction:column;margin-top:1rem;}
.menu-accordion-item{border-bottom:3px solid var(--black);}
.menu-accordion-trigger{display:flex;align-items:center;justify-content:space-between;width:100%;background:none;border:none;text-align:left;font-family:var(--font-display);font-size:1.6rem;letter-spacing:.06em;color:var(--ink);cursor:pointer;padding:1rem .25rem;transition:color .15s;}
.menu-accordion-trigger:hover{color:var(--red);}
.menu-accordion-chevron{font-size:1.2rem;transition:transform .25s ease;flex-shrink:0;margin-left:.5rem;}
.menu-accordion-item.open .menu-accordion-chevron{transform:rotate(180deg);}
.menu-accordion-panel{max-height:0;overflow:hidden;transition:max-height .35s ease;}
.menu-accordion-item.open .menu-accordion-panel{max-height:2400px;}
.menu-accordion-panel-inner{padding:0 .25rem 1.5rem;display:flex;flex-direction:column;gap:.5rem;}

/* Short landscape phones: shrink footer height for the tight vertical space */
@media(max-height:500px) and (orientation:landscape){
  :root{--ftr-h:44px;}
  .site-footer{height:var(--ftr-h);}
  .footer-feed-btn,.footer-stations-btn,.footer-autostream-btn{font-size:.72rem;padding:.3rem .75rem;}
  .fab-stack{bottom:calc(1rem + 44px);gap:8px;}
  .record-fab{width:52px;height:52px;}
  /* record-overlay modals (save-your-code, create-a-station, etc.) were
     vertically centered with no way to scroll to whatever didn't fit —
     on a short landscape phone screen (e.g. iPhone mini rotated) the
     title clipped clean off the top with no way to reveal it, since
     centered flex content that overflows isn't reliably scrollable back
     up to its start. Anchor to the top instead of centering, and shrink
     the card's own padding/type so more of it fits without scrolling
     at all. */
  .record-overlay{align-items:flex-start;padding:.75rem 1.25rem;}
  .record-card{padding:1rem 1.25rem;gap:.65rem;}
  .record-title{font-size:1.05rem;}
  .record-hint{font-size:.72rem;line-height:1.4;}
  .record-actions .btn-primary,.record-actions .btn-ghost{height:40px;}
}

/* ── PAGE SECTIONS (legal / sites) ── */
/* ── STATIONS DIRECTORY & MENU (now real views, not overlays) ── */
#viewStations{background:var(--paper);align-items:center;padding:calc(var(--hdr-h) + 1.25rem) 1.25rem 6rem;gap:1rem;}
#viewMenu{background:var(--paper);align-items:center;padding-top:56px;}
.section-inner{flex:1;max-width:680px;width:100%;margin:0 auto;padding:2rem 2rem 6rem;}
.section-back{display:inline-flex;align-items:center;gap:.5rem;font-family:var(--font-display);font-size:1.1rem;letter-spacing:.1em;color:var(--ink);background:none;border:none;cursor:pointer;padding:.5rem 0;margin-bottom:1.5rem;transition:opacity .15s;}
.section-back:hover{opacity:.7;}
.section-title{font-family:var(--font-display);font-size:clamp(2.5rem,8vw,5rem);letter-spacing:.04em;line-height:1;color:var(--red);-webkit-text-stroke:2px var(--black);text-shadow:4px 4px 0 var(--black);margin-bottom:1.5rem;}
.legal-subnav{display:flex;gap:.5rem;flex-wrap:wrap;margin-bottom:1.25rem;}
.legal-subnav-btn{font-family:var(--font-display);font-size:.95rem;letter-spacing:.12em;background:none;border:3px solid var(--black);color:var(--black);cursor:pointer;padding:.25em 1em;box-shadow:3px 3px 0 var(--black);transition:transform .1s,box-shadow .1s,background .1s;text-align:center;}
@media(max-width:480px){
  .legal-subnav{flex-direction:column;flex-wrap:nowrap;align-items:stretch;}
  .legal-subnav-btn{width:100%;}
}
.legal-subnav-btn.active{background:var(--yellow);}
.legal-subnav-btn:hover{transform:translate(-1px,-1px);box-shadow:4px 4px 0 var(--black);}
.legal-subnav-btn:active{transform:translate(2px,2px);box-shadow:1px 1px 0 var(--black);}
.legal-subpanel{display:none;flex-direction:column;gap:.5rem;}
.legal-subpanel.active{display:flex;}
.legal-text{font-family:var(--font-accent);font-size:clamp(0.75rem,2.2vw,0.95rem);font-style:normal;color:var(--ink);line-height:1.9;margin-bottom:.5rem;padding:.3em .7em .4em;border:3px solid var(--black);border-radius:4px;box-shadow:3px 3px 0 var(--black);display:inline-block;width:100%;}
.story-signoff-row{display:flex;align-items:center;justify-content:flex-end;gap:.6rem;margin-top:.75rem;}
.story-signoff-photo{width:44px;height:44px;border-radius:50%;object-fit:cover;border:3px solid var(--black);box-shadow:2px 2px 0 var(--black);flex-shrink:0;}
.story-signoff{font-family:var(--font-mono);font-size:.8rem;font-style:italic;color:var(--ink-mid);text-align:right;margin-top:0;line-height:1.7;}
.story-signoff a{color:var(--ink-mid);text-decoration:underline;font-style:normal;}
.story-signoff a:hover{color:var(--red);}
.legal-text strong{color:var(--ink);}
.legal-text a{color:var(--red);border-bottom:2px solid var(--red);text-decoration:none;transition:color .15s;}
.legal-text a:hover{color:var(--ink);border-bottom-color:var(--ink);}
.sites-list{display:flex;flex-direction:column;gap:1rem;margin-top:.5rem;}
.sites-list-link{display:flex;align-items:center;gap:.75rem;font-family:var(--font-display);font-size:1.6rem;letter-spacing:.05em;color:var(--red);text-decoration:none;text-shadow:2px 2px 0 var(--black);transition:color .15s;}
.sites-list-link:hover{color:var(--ink);}


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