/* Weezinc Stream — Signal design system. Source of truth: docs/design/weezinc-stream-mockup.html */

/* 1. tokens */
:root{
  --bg:#08080d; --card:#0d0d15; --line:#1c1c28; --accent:#4d7cfe;
  --live:#ff3b30; --warn:#f7b731; --ok:#3ddc84;
  --twitch:#a970ff; --youtube:#ff4e45; --kick:#53fc18;
  --text:#fff; --text-2:#c9c9d4; --text-3:#8a8a97;
  --radius:10px;
  --mono:ui-monospace,'Cascadia Mono',Consolas,monospace;
}

/* 2. base */
*{box-sizing:border-box;margin:0;padding:0}
/* The universal reset above strips the default list indent, which leaves
   outside-positioned markers hanging into the surrounding gutter (owner
   report 2026-07-31, the /setup app guides). Restore it for real lists;
   custom lists (list-style:none) zero it in their own rules. */
ol,ul{padding-left:1.3em}
body{background:var(--bg);color:var(--text);
  font-family:'Inter',system-ui,-apple-system,sans-serif;font-size:15px;line-height:1.5}
a{color:var(--accent);text-decoration:none}
/* Default padding so a bare .card can never render text flush against its
   border (a recurring bug — owner report 2026-07-31). Modifier classes and
   inline styles come later in the cascade and still override it. */
.card{background:var(--card);border:1px solid var(--line);border-radius:var(--radius);padding:18px 20px}
.mono{font-family:var(--mono)}
.btn{display:inline-block;background:var(--accent);color:#fff;font-weight:700;
  padding:10px 22px;border-radius:8px;border:0;cursor:pointer;font-size:14px}
.btn:hover{filter:brightness(1.1)}
.btn-ghost{background:transparent;border:1px solid var(--line);color:var(--text-2)}
.input,select.input{background:#12121c;border:1px solid var(--line);border-radius:8px;
  padding:10px 12px;color:var(--text);font-size:14px;font-family:inherit;min-width:170px;
  /* Without this, native pickers (datetime, calendar icon) render light. */
  color-scheme:dark}
.input:focus{outline:none;border-color:var(--accent)}
/* appearance:none removes the arrow as well as the chrome, so draw one back. */
select.input{appearance:none;padding-right:32px;
  background-image:linear-gradient(45deg,transparent 50%,var(--text-3) 50%),
    linear-gradient(135deg,var(--text-3) 50%,transparent 50%);
  background-position:calc(100% - 17px) calc(50% + 1px),calc(100% - 12px) calc(50% + 1px);
  background-size:5px 5px,5px 5px;background-repeat:no-repeat}
.btn:disabled{opacity:.4;cursor:not-allowed;filter:none}

/* First-run walkthrough. A step that cannot be observed gets a dashed dash
   rather than an empty circle — an unfillable tick reads as an accusation. */
.step{display:flex;gap:14px;padding:15px 0;border-top:1px solid var(--line)}
.step:first-of-type{border-top:0}
.step-num{flex:none;width:26px;height:26px;border-radius:50%;font-size:12px;
  border:1px solid var(--line);color:var(--text-3);
  display:flex;align-items:center;justify-content:center}
.step.done .step-num{background:var(--ok);border-color:var(--ok);color:#08080d;font-weight:700}
.step.no-tick .step-num{border-style:dashed}
.step-title{color:var(--text-2);font-size:14px;font-weight:600}
.step.done .step-title{color:var(--text-3)}
.step-detail{color:var(--text-3);font-size:13px;margin-top:4px}
.risk{padding:12px 0;border-top:1px solid var(--line)}
.risk:first-of-type{border-top:0}
.risk-quote{color:var(--text-2);font-size:13px;font-style:italic;margin-top:6px}

/* Upload drop zone. The browser's default file input ignores the dark theme
   entirely — it renders a light-grey "Choose File" chip. The real <input> is
   still here, stretched invisibly over the zone, so the native picker, the
   accept filter and form validation all keep working without JS. */
.dropzone{position:relative;display:flex;flex-direction:column;align-items:center;
  gap:6px;padding:26px 20px;border:1.5px dashed var(--line);border-radius:12px;
  background:#0a0a12;cursor:pointer;text-align:center;
  transition:border-color .15s,background .15s}
.dropzone:hover{border-color:var(--accent);background:#0c0c17}
.dropzone.is-over{border-color:var(--accent);background:rgba(77,124,254,.08)}
.dropzone input[type=file]{position:absolute;inset:0;opacity:0;cursor:pointer}
.dz-icon{color:var(--text-3);transition:color .15s}
.dropzone:hover .dz-icon,.dropzone.is-over .dz-icon{color:var(--accent)}
.dz-main{color:var(--text-2);font-size:14px}
.dz-main b{color:var(--accent);font-weight:600}
.dz-sub{color:var(--text-3);font-size:12px}

.file-chip{display:none;align-items:center;gap:10px;margin-top:10px;padding:10px 12px;
  border:1px solid var(--line);border-radius:10px;background:#12121c}
.file-chip.show{display:flex}
.fc-name{flex:1;min-width:0;color:var(--text-2);font-size:13px;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.fc-size{flex-shrink:0;color:var(--text-3);font-size:12px}
.fc-x{background:none;border:0;padding:0 2px;color:var(--text-3);
  font-size:15px;line-height:1;cursor:pointer}
.fc-x:hover{color:var(--live)}

/* A 2 GB upload with no feedback looks identical to a hung one. */
.upload-bar{display:none;height:6px;margin-top:12px;border-radius:999px;
  background:#12121c;overflow:hidden}
.upload-bar.show{display:block}
.upload-bar span{display:block;height:100%;width:0;border-radius:999px;
  background:var(--accent);transition:width .2s}
.tag{display:inline-block;font-size:11px;font-weight:600;padding:2px 9px;
  border-radius:999px;border:1px solid var(--line);color:var(--text-3)}
.tag.addon{border-color:rgba(247,183,49,.4);color:var(--warn)}
.chat-tabs{display:flex;gap:6px;margin-bottom:8px}
.chat-tab{background:var(--card);border:1px solid var(--line);color:var(--text-3);
  border-radius:999px;padding:4px 12px;font-size:12px;cursor:pointer}
.chat-tab.active{color:var(--text);border-color:var(--accent)}
.chat-tab[data-platform="twitch"]{color:var(--twitch)}
.chat-tab[data-platform="twitch"].active{border-color:var(--twitch)}
.chat-tab[data-platform="youtube"]{color:var(--youtube)}
.chat-tab[data-platform="youtube"].active{border-color:var(--youtube)}
.chat-tab[data-platform="kick"]{color:var(--kick)}
.chat-tab[data-platform="kick"].active{border-color:var(--kick)}
.hint{color:var(--text-3);font-size:12px;margin-top:8px}
.platform-twitch{color:var(--twitch)}
.platform-youtube{color:var(--youtube)}
.platform-kick{color:var(--kick)}
.platform-custom,.platform-local{color:var(--text-2)}
.chat-row{display:flex;align-items:center;gap:8px;padding:5px 8px;border-radius:6px}
.chat-row:hover{background:rgba(255,255,255,.03)}
.chat-row:hover .chat-mod-btn{opacity:1}
.chat-avatar{width:22px;height:22px;border-radius:50%;flex:none;object-fit:cover}
.chat-avatar-fallback{display:flex;align-items:center;justify-content:center;
  font-size:10px;font-weight:700;color:#08080d}
.chat-avatar-twitch{background:var(--twitch)}
.chat-avatar-youtube{background:var(--youtube)}
.chat-avatar-kick{background:var(--kick)}
/* A message sent to every channel shows one dot per platform. The wrapper must
   be a flex container: .chat-dot is a span, and width/height only apply to it
   because it is blockified as a flex item — nested in a plain span the dots
   would silently collapse to nothing. */
/* Was an inline style="height:280px". Moved into a class so the popped-out
   layout below can override it — an inline height would have needed
   !important to beat. */
.chat-feed-pane{height:280px;overflow-y:auto}
/* Also a class rather than an inline style, for the same reason: body.popout
   below hides it, and an inline display would win. */
.chat-send-row{display:flex;gap:8px;margin-top:8px}
/* The floating always-on-top window (Document Picture-in-Picture).
   Its OWN class rather than body.dock: reusing the OBS dock's rules would tie
   this window to a ~350px OBS panel, so a dock tweak would silently change the
   game overlay.
   A reading surface -- everything that exists for interaction is hidden with
   CSS rather than removed, so bringing the chat back to the page restores it
   with no bookkeeping and the widget never learns there are two modes. */
body.popout{margin:0;min-height:100vh;background:var(--bg);color:var(--text);font-size:12px}
body.popout #chat-popout-pane{display:flex;flex-direction:column;height:100vh;padding:6px;box-sizing:border-box}
body.popout .chat-feed-pane{flex:1;min-height:0;height:auto;overflow-y:auto}
body.popout #widget-chat-send,
body.popout #widget-chat-status,
body.popout #chat-tabs,
body.popout .chat-avatar,
body.popout .chat-ts,
body.popout .chat-mod-btn{display:none}
/* Compact rows: more conversation visible beats bigger type, so you can catch
   up after a firefight without scrolling. */
body.popout .chat-row{padding:1px 4px;gap:5px;line-height:1.35;align-items:baseline;flex-wrap:wrap}
/* Rank as its own visual channel, separate from the name colour, so a mod
   asking you to check something cannot look like a first-time viewer. Most
   rows have no rank and get no marker, which is what keeps the bar free. */
.chat-row[data-rank]{border-left:2px solid transparent;padding-left:6px}
.chat-row[data-rank="broadcaster"]{border-left-color:var(--warn)}
.chat-row[data-rank="moderator"]{border-left-color:var(--ok)}
.chat-row[data-rank="subscriber"]{border-left-color:var(--accent)}
.chat-row[data-rank="verified"]{border-left-color:var(--text-3)}
.chat-dots{display:inline-flex;align-items:center;gap:3px;flex:none}
.chat-dot{width:7px;height:7px;border-radius:50%;flex:none}
.chat-dot-twitch{background:var(--twitch)}
.chat-dot-youtube{background:var(--youtube)}
.chat-dot-kick{background:var(--kick)}
.chat-author{font-weight:700}
.chat-author-twitch{color:var(--twitch)}
.chat-author-youtube{color:var(--youtube)}
.chat-author-kick{color:var(--kick)}
.chat-text{color:var(--text-2)}
.chat-ts{margin-left:auto;color:var(--text-3);font-size:11px;flex:none}
.chat-mod-btn{background:transparent;border:1px solid var(--line);color:var(--text-3);
  border-radius:6px;width:20px;height:20px;font-size:11px;line-height:1;cursor:pointer;
  flex:none;opacity:.35;transition:opacity .1s}
.chat-mod-btn:hover{color:var(--text-2);border-color:var(--text-3)}
#widget-chat-feed{scrollbar-width:thin;scrollbar-color:var(--line) transparent}
#widget-chat-feed::-webkit-scrollbar{width:8px}
#widget-chat-feed::-webkit-scrollbar-track{background:transparent}
#widget-chat-feed::-webkit-scrollbar-thumb{background:var(--line);border-radius:8px}
#widget-chat-feed::-webkit-scrollbar-thumb:hover{background:var(--text-3)}
@media (prefers-reduced-motion:reduce){
  *,*::before,*::after{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important}
}
:focus-visible{outline:2px solid var(--accent);outline-offset:2px}

/* 3. app shell */
.app-shell{display:grid;grid-template-columns:230px 1fr;grid-template-rows:1fr auto;min-height:100vh}
/* Inside the desktop app there is no sidebar and no dock, so the grid must not
   keep reserving a 230px column for one. Also drops main's max-width and the
   padding that existed to clear the dock: the app gives each page a pane, and
   a pane is already narrow. */
.app-shell.desktop{grid-template-columns:1fr}
.app-shell.desktop main{padding:24px 28px;padding-bottom:24px;max-width:none}
.sidebar{grid-row:1/3;border-right:1px solid var(--line);padding:20px 14px;
  display:flex;flex-direction:column;gap:4px;position:sticky;top:0;height:100vh}
.wordmark-link{text-decoration:none;display:inline-block;padding:4px 8px;margin:-4px -8px}
.wordmark{font-weight:800;letter-spacing:.05em;font-size:15px;color:var(--text)}
.wordmark b{color:var(--accent);font-weight:800}
.sidebar .wordmark{margin-bottom:18px;padding:0 12px}
.sidebar .wordmark .glyph{display:none}
.sidebar a{display:flex;gap:10px;align-items:center;color:var(--text-3);padding:9px 12px;
  border-radius:8px;font-size:14px}
.sidebar a.active{color:var(--text);background:rgba(77,124,254,.14)}
.sidebar a:hover{color:var(--text-2)}
.sidebar a svg{flex:none}
.sidebar .account{margin-top:auto;border-top:1px solid var(--line);padding:12px 12px 0;
  font-size:12px;color:var(--text-3)}
.sidebar .account form{display:inline}
.sidebar .account button{background:none;border:0;color:var(--accent);font-size:12px;
  cursor:pointer;padding:0}
.app-shell main{padding:34px 40px;max-width:1060px;width:100%;padding-bottom:90px}
main h1{font-size:22px;font-weight:700}
.page-sub{color:var(--text-3);font-size:14px;margin:4px 0 26px}

/* 4. live dock */
.live-dock{grid-column:2;position:sticky;bottom:0;border-top:1px solid var(--line);
  background:linear-gradient(180deg,rgba(13,13,21,.92),#0d0d15);backdrop-filter:blur(8px);
  padding:10px 24px;display:flex;align-items:center;gap:16px;font-size:13px;min-height:46px}
.dock-live{display:none}
.is-live .dock-live{display:flex;align-items:center;gap:16px;width:100%;flex-wrap:wrap}
.is-live .dock-offline{display:none}
.dock-offline{color:var(--text-3);display:flex;gap:8px;align-items:center;flex-wrap:wrap}
.badge-live{color:var(--live);font-weight:800;letter-spacing:.06em}
.badge-live::before{content:'●';margin-right:6px;animation:pulse 2s infinite}
@keyframes pulse{50%{opacity:.4}}
.dock-dot{width:8px;height:8px;border-radius:50%;display:inline-block}
.dock-dot.ok{background:var(--ok)}
.dock-dot.warn{background:var(--warn)}
.dock-dot.off{background:#2a2a38}
.dock-chip{display:flex;gap:6px;align-items:center;font-size:12px;font-weight:600}
.dock-chip.has-failure{color:var(--warn)}
.dock-chip-warn{cursor:help}
.dock-right{margin-left:auto;display:flex;gap:14px;align-items:center;color:var(--text-3)}

/* 5. dashboard */
.status-hero{padding:26px;margin-bottom:16px}
.status-hero .off{display:flex;gap:12px;align-items:center;color:var(--text-3)}
.is-live .status-hero .off{display:none}
.status-hero .on{display:none}
.is-live .status-hero .on{display:flex;gap:22px;align-items:center;flex-wrap:wrap}
.status-hero .live-word{color:var(--live);font-weight:900;font-size:30px;letter-spacing:.04em}
.status-hero .live-word::before{content:'●';margin-right:10px;animation:pulse 2s infinite}
.status-hero .metric{display:flex;flex-direction:column}
.status-hero .metric .v{font-family:var(--mono);font-size:19px;color:var(--text)}
.status-hero .metric .k{font-size:11px;color:var(--text-3);text-transform:uppercase;letter-spacing:.09em}
.obs-card{padding:22px;margin-bottom:16px}
.obs-card h3,.add-card h3,.set-card h3,.overlay-card h3{font-size:15px;font-weight:600;margin-bottom:14px}
.kv{display:flex;gap:10px;align-items:center;margin-bottom:10px;flex-wrap:wrap}
.kv .k{width:92px;color:var(--text-3);font-size:13px;flex:none}
.kv .v{font-family:var(--mono);font-size:13px;background:#12121c;border:1px solid var(--line);
  border-radius:7px;padding:7px 11px;color:var(--text-2);
  /* Overlay and dock URLs are one long unbreakable token, so without these the
     value box cannot get narrower than the whole URL -- it pushed the card
     wide and gave the chat pane a horizontal scrollbar in the desktop app.
     BOTH rules are needed: overflow-wrap lets the text break mid-token, and
     min-width:0 overrides a flex item's default min-width:auto, which
     otherwise refuses to shrink below the content's minimum width no matter
     what wrapping allows. */
  overflow-wrap:anywhere;min-width:0}
.kv .btn-ghost{padding:6px 12px;font-size:12px}
.dest-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:12px}
.dest-tile{padding:16px 18px;display:flex;justify-content:space-between;align-items:center}
.dest-tile .nm{font-weight:700;font-size:14px}
.dest-tile .st{font-size:12px;margin-top:3px}
.st.ok{color:var(--ok)}
.st.warn{color:var(--warn)}
.st.off{color:var(--text-3)}

/* 6. toggle */
.toggle{position:relative;width:40px;height:22px;display:inline-block;flex:none}
.toggle input{opacity:0;width:0;height:0}
.toggle span{position:absolute;inset:0;background:#2a2a38;border-radius:999px;transition:.15s;cursor:pointer}
.toggle span::after{content:'';position:absolute;left:2px;top:2px;width:18px;height:18px;
  background:#8a8a97;border-radius:50%;transition:.15s}
.toggle input:checked+span{background:var(--accent)}
.toggle input:checked+span::after{transform:translateX(18px);background:#fff}

/* 7. destinations */
.dest-row{padding:16px 20px;display:flex;align-items:center;gap:14px;margin-bottom:10px;flex-wrap:wrap}
.dest-row .dot{width:9px;height:9px;border-radius:50%;flex:none}
.dest-row .nm{font-weight:700;font-size:14px;width:90px}
.dest-row .key{font-family:var(--mono);font-size:12px;color:var(--text-3)}
.dest-row .url{font-size:12px;color:var(--text-3);flex:1;min-width:160px;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.dest-row .actions{margin-left:auto;display:flex;gap:10px;align-items:center}

/* per-destination connection guide */
/* Row footer: the Guide disclosure on the left, the control-room escape hatch
   on the right. align-items:start so an expanded guide does not drag the link
   down the page with it. */
.dest-footer{flex-basis:100%;display:flex;align-items:flex-start;
  justify-content:space-between;gap:16px;margin-top:4px;
  border-top:1px solid var(--line);padding-top:10px}
.dest-footer .dest-guide{flex:1 1 auto;margin-top:0;border-top:0;padding-top:0}
.control-room{flex:none;font-size:12px;color:var(--text-3);white-space:nowrap;
  padding-top:1px}
.control-room:hover{color:var(--accent)}
.dest-guide{flex-basis:100%;margin-top:4px;border-top:1px solid var(--line);padding-top:10px}
.dest-guide-summary{cursor:pointer;font-size:13px;color:var(--text-2);
  list-style:none;display:inline-flex;align-items:center;gap:6px;padding:2px 0}
.dest-guide-summary::-webkit-details-marker{display:none}
.dest-guide-summary::before{content:'';width:0;height:0;
  border-left:5px solid currentColor;border-top:4px solid transparent;
  border-bottom:4px solid transparent;transition:transform .15s ease}
.dest-guide[open] .dest-guide-summary::before{transform:rotate(90deg)}
.dest-guide-summary:hover{color:var(--text)}
.dest-guide-body{padding:10px 0 4px;max-width:70ch}
.dest-guide-lede{font-size:13px;color:var(--text-2);line-height:1.6;margin:0 0 14px}
/* A precondition, not a step — deliberately outside the numbered list, and
   styled as a warning rather than a task so it never reads as an unfinished
   item waiting for a tick. Left rule + heading colour, not colour alone. */
.dest-guide-pre{margin:0 0 16px;padding:10px 0 10px 12px;
  border-left:2px solid var(--warn)}
.dest-guide-pre-title{margin:0 0 3px;font-size:11px;font-weight:700;
  letter-spacing:.06em;text-transform:uppercase;color:var(--warn)}
.dest-guide-steps{list-style:none;margin:0;padding:0;display:flex;
  flex-direction:column;gap:14px}
.dest-guide-step{display:flex;gap:12px;align-items:flex-start}
.dest-guide-marker{flex:none;width:22px;height:22px;border-radius:50%;
  display:flex;align-items:center;justify-content:center;font-size:11px;
  font-weight:700;border:1px solid var(--line);color:var(--text-3)}
.dest-guide-step.is-done .dest-guide-marker{border-color:var(--ok);color:var(--ok)}
.dest-guide-title{margin:0 0 3px;font-size:13px;font-weight:700;color:var(--text);
  display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.dest-guide-done-label{font-size:10px;font-weight:700;letter-spacing:.04em;
  text-transform:uppercase;color:var(--ok);border:1px solid var(--ok);
  border-radius:999px;padding:1px 6px}
.dest-guide-detail{margin:0;font-size:13px;line-height:1.6;color:var(--text-2)}
/* A finished step stays fully readable — it is also the instructions for
   repeating it. Only the heading dims; the detail text keeps full contrast. */
.dest-guide-step.is-done .dest-guide-title{color:var(--text-2)}
@media (prefers-reduced-motion:reduce){
  .dest-guide-summary::before{transition:none}
}
/* Verdict on whether the saved YouTube key belongs to the connected channel.
   Advisory only — it never disables the toggle or the save. The label carries
   the meaning in words; colour is reinforcement, not the signal. */
.key-check{flex-basis:100%;display:flex;align-items:baseline;gap:8px;
  flex-wrap:wrap;font-size:12px;line-height:1.5;margin-top:6px}
.key-check-label{font-weight:700;letter-spacing:.03em;text-transform:uppercase;
  font-size:10px;border:1px solid currentColor;border-radius:999px;padding:1px 6px}
.key-check-detail{color:var(--text-2)}
.key-check-ok{color:var(--ok)}
.key-check-bad{color:var(--live)}
.key-check-muted{color:var(--text-3)}
.linklike{background:none;border:0;padding:0;color:var(--accent);font-size:12px;
  cursor:pointer;text-decoration:underline}
.add-card{padding:22px;margin-top:18px}
.field-row{display:flex;gap:10px;flex-wrap:wrap;margin-bottom:12px}

/* 8. chat / schedule / veils */
.veiled{position:relative}
.veiled>.veil-content{opacity:.35;pointer-events:none}
.veil{position:absolute;inset:0;display:flex;flex-direction:column;gap:6px;
  align-items:center;justify-content:center;text-align:center;z-index:2}
.veil b{font-size:17px}
.veil p{color:var(--text-3);font-size:13px;max-width:340px}
.split-2{display:grid;grid-template-columns:2fr 1fr;gap:14px}
.chat-pane{padding:18px;display:flex;flex-direction:column;gap:12px;min-height:340px}
.chat-msg{display:flex;gap:9px;font-size:13px;align-items:baseline}
.chat-msg .dot{width:8px;height:8px;border-radius:50%;flex:none;align-self:center}
.chat-msg b{flex:none}
.chat-msg .t{margin-left:auto;font-family:var(--mono);font-size:10px;color:var(--text-3)}
.chat-input{margin-top:auto;display:flex;gap:8px}
.overlay-card{padding:18px}
.dropzone{border:1.5px dashed #2a2a38;border-radius:var(--radius);padding:36px 20px;
  text-align:center;color:var(--text-3);font-size:14px;margin-bottom:16px}
/* Destination picker. The native checkbox paints a light-grey square that the
   dark theme cannot reach, and the thing being chosen IS a platform — so the
   whole chip takes that platform's colour when selected. --pf is set inline
   per row, and only for platforms that actually have a colour token. */
.check{position:relative;display:inline-flex;gap:8px;align-items:center;font-size:13px;
  background:#12121c;border:1px solid var(--line);border-radius:999px;
  padding:8px 14px;cursor:pointer;color:var(--text-3);
  transition:color .15s,border-color .15s,background .15s}
.check:hover{border-color:var(--text-3)}
.check input{position:absolute;width:0;height:0;opacity:0}
.check .dot{width:7px;height:7px;border-radius:50%;background:currentColor;
  opacity:.4;transition:opacity .15s}
.check:has(input:checked){color:var(--pf,var(--accent));border-color:currentColor}
.check:has(input:checked) .dot{opacity:1}
/* Keyboard users get a focus ring even though the real input is invisible. */
.check:has(input:focus-visible){outline:2px solid var(--accent);outline-offset:2px}

/* Scheduled-item state. Six values come out of the scheduler; each gets a
   colour that says what to do about it, not just that it differs. */
.st-tag{display:inline-block;font-size:11px;font-weight:600;padding:3px 10px;
  border-radius:999px;border:1px solid currentColor;text-transform:capitalize}
.st-pending,.st-cancelled{color:var(--text-3)}
.st-playing{color:var(--live)}
.st-completed{color:var(--ok)}
.st-skipped{color:var(--warn)}
.st-failed{color:var(--live);background:rgba(255,59,48,.1)}

/* Shared row for the uploads and schedule lists: name grows, meta and the
   action stay put, and a hairline separates rows instead of a gap. */
.list-row{display:flex;align-items:center;gap:12px;padding:11px 0;
  border-top:1px solid var(--line)}
.list-row:first-of-type{border-top:0}
.lr-name{flex:1;min-width:0;color:var(--text-2);font-size:13px;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.lr-meta{flex-shrink:0;color:var(--text-3);font-size:12px}
.list-row .btn-ghost{padding:6px 12px;font-size:12px}
.upcoming{margin-top:18px;padding:16px 20px;display:flex;gap:14px;align-items:center;flex-wrap:wrap}

/* 9. history */
table.hist{width:100%;border-collapse:collapse;font-size:13px}
table.hist th{text-align:left;color:var(--text-3);font-size:11px;text-transform:uppercase;
  letter-spacing:.08em;font-weight:600;padding:10px 12px;border-bottom:1px solid var(--line)}
table.hist td{padding:12px;border-bottom:1px solid var(--line);color:var(--text-2)}
table.hist td.mono{font-family:var(--mono);font-size:12.5px}
table.hist tr:last-child td{border-bottom:0}
table.hist a{color:inherit}
table.hist tr.rowlink:hover td{background:#10101a;cursor:pointer}
.ubar{display:flex;gap:10px;align-items:center;margin-bottom:9px;font-size:12px}
.ubar .lab{width:70px;font-weight:600}
.ubar .track{flex:1;height:6px;background:#16161f;border-radius:999px;overflow:hidden;display:flex}
.ubar .fill{height:100%;border-radius:999px}
.ubar .pct{width:42px;text-align:right;font-family:var(--mono);color:var(--text-3)}

/* 10. settings */
.set-card{padding:22px;margin-bottom:16px;position:relative}
.set-card .tag{position:absolute;top:18px;right:18px}
.trow{display:flex;justify-content:space-between;align-items:center;padding:8px 0;
  border-bottom:1px solid #12121c;font-size:14px;color:var(--text-2)}
.trow:last-child{border-bottom:0}
.locked{opacity:.8}
.meter{height:8px;background:#16161f;border-radius:999px;margin-top:12px;overflow:hidden}
.meter .fill{width:0%;height:100%;background:var(--warn)}
.meter-cap{display:flex;justify-content:space-between;font-size:11px;color:var(--text-3);margin-top:5px}

/* 11. landing + login */
.land-nav{display:flex;align-items:center;justify-content:space-between;
  padding:20px 40px;max-width:1240px;margin:0 auto}
.land-nav-links{display:flex;align-items:center;gap:22px;flex-wrap:wrap}
.land-nav-links a{color:var(--text-3);font-size:13px;padding-bottom:2px;border-bottom:2px solid transparent}
.land-nav-links a:hover{color:var(--text-2)}
.land-nav-links a.active{color:var(--text);border-bottom-color:var(--accent)}
.land-nav-links a.btn{border-bottom:none}
.land-nav-cta{box-shadow:0 0 12px rgba(77,124,254,.35)}
.land-nav-toggle{display:none;flex-direction:column;justify-content:center;
  gap:5px;width:32px;height:32px;background:transparent;border:0;
  cursor:pointer;padding:0}
.land-nav-toggle-bar{display:block;width:100%;height:2px;background:var(--text-2);
  border-radius:2px}
/* flex so the inner <a> is blockified exactly like its sibling nav links —
   as a plain block it kept its inline line-height and sat off-baseline */
.land-nav-dropdown{position:relative;display:flex;align-items:center}
.land-nav-dropdown-menu{position:absolute;top:100%;left:0;background:#0d0d15;
  border:1px solid var(--line);border-radius:8px;padding:6px;display:none;
  flex-direction:column;min-width:220px;z-index:20}
.land-nav-dropdown:hover .land-nav-dropdown-menu,
.land-nav-dropdown:focus-within .land-nav-dropdown-menu{display:flex}
.land-nav-dropdown-menu a{padding:8px 10px;border-radius:6px;font-size:13px;
  color:var(--text-2);border-bottom:none}
.land-nav-dropdown-menu a:hover{background:var(--line);color:var(--text)}
.hero{position:relative;min-height:88vh;display:flex;align-items:flex-start;
  justify-content:center;overflow:hidden;text-align:center}
.cascade{position:absolute;inset:0;width:100%;height:100%;pointer-events:none}
.hero-inner{position:relative;padding-top:19vh;max-width:820px;margin:0 auto}
.hero h1{font-weight:900;font-size:clamp(44px,7cqw,84px);line-height:1.04;letter-spacing:-.02em}
.hero .sub{color:var(--text-3);font-size:18px;margin-top:18px}
.hero .ctas{margin-top:30px;display:flex;gap:12px;justify-content:center;flex-wrap:wrap}
.hero .platforms{margin-top:44px;display:flex;gap:18px;justify-content:center;
  font-size:13px;font-weight:600;flex-wrap:wrap}
.land-section{max-width:1080px;margin:0 auto;padding:70px 32px}
.land-section h2{font-weight:600;font-size:28px;margin-bottom:8px}
.land-section .lede{color:var(--text-3);margin-bottom:36px}
/* The three steps are a SEQUENCE, so they never orphan: the auto-fit rule
   .feature-grid uses would put 01+02 on one row and leave 03 alone beside an
   empty half-row at iPad-portrait width. Three across, or stacked — never 2+1.
   Selector is doubled up deliberately: .feature-grid is defined BELOW this and
   carries equal specificity, so a bare .hiw-steps loses on source order. */
.hiw-steps{margin-top:24px}
.feature-grid.hiw-steps{grid-template-columns:repeat(3,1fr)}
@container (max-width:700px){
  .feature-grid.hiw-steps{grid-template-columns:1fr}
}
.pipeline-vertical{display:none}
.feature-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:16px}
.faq-group-label{color:var(--text-3);font-size:12px;text-transform:uppercase;
  letter-spacing:.08em;font-weight:600;margin:28px 0 12px}
.faq-group-label:first-of-type{margin-top:0}
.feature{padding:22px}
.feature .ico{width:34px;height:34px;border-radius:8px;background:rgba(77,124,254,.12);
  display:flex;align-items:center;justify-content:center;margin-bottom:14px}
.feature h3{font-size:16px;font-weight:600;margin-bottom:6px}
.feature p{color:var(--text-3);font-size:14px}
.feature .tag{margin-top:12px}
@keyframes icon-draw{
  0%,20%{stroke-dashoffset:20}
  60%,100%{stroke-dashoffset:0}
}
.icon-draw{stroke-dasharray:20;animation:icon-draw 3s ease-in-out infinite}
@keyframes icon-spin{
  from{transform:rotate(0deg)}
  to{transform:rotate(360deg)}
}
.icon-spin{transform-origin:8px 8px;animation:icon-spin 3s linear infinite}
@keyframes icon-pulse-dot{
  0%,100%{opacity:1}
  50%{opacity:.25}
}
.icon-pulse-dot{animation:icon-pulse-dot 2s ease-in-out infinite}
.land-footer{border-top:1px solid var(--line);padding:36px 40px 0}
.land-footer-cols{display:flex;justify-content:space-between;gap:40px;max-width:1240px;margin:0 auto;padding-bottom:24px;text-align:left;flex-wrap:wrap}
.land-footer-brand p{color:var(--text-3);font-size:12px;margin-top:6px;max-width:220px}
.land-footer-links{display:flex;flex-direction:column;gap:10px}
.land-footer-links a{color:var(--text-3);font-size:13px}
.land-footer-links a:hover{color:var(--text-2)}
.land-footer-bottom{border-top:1px solid var(--line);text-align:center;color:var(--text-3);font-size:12px;padding:14px 16px}
.price-value{margin:10px 0 14px}
.price-amount{font-size:28px;font-weight:800}
.price-period{color:var(--text-3);font-size:13px;margin-left:4px}
.price-features{list-style:none;padding-left:0;color:var(--text-3);font-size:13px;display:flex;flex-direction:column;gap:6px}
.price-features li{padding-left:16px;position:relative}
.price-features li::before{content:'—';position:absolute;left:0;color:var(--text-3)}
.auth-wrap{min-height:100vh;display:flex;align-items:center;justify-content:center;
  position:relative;overflow:hidden;padding:20px}
.auth-card{position:relative;width:100%;max-width:380px;padding:30px;z-index:2}
.auth-card .wordmark{display:block;text-align:center;margin-bottom:22px;font-size:17px}
.auth-card .input{width:100%;min-width:0;margin-bottom:12px}
.auth-card .btn{width:100%}
.auth-error{border:1px solid rgba(247,183,49,.4);color:var(--warn);border-radius:8px;
  padding:9px 12px;font-size:13px;margin-bottom:14px}

/* 12. responsive */
@media (max-width:860px){
  .app-shell{grid-template-columns:64px 1fr}
  .sidebar{padding:16px 10px}
  .sidebar .lbl{display:none}
  .sidebar .wordmark .glyph{display:inline;font-size:15px}
  .sidebar a{justify-content:center;padding:10px}
  .sidebar .account{padding-left:0;text-align:center}
  .app-shell main{padding:20px 16px;padding-bottom:90px}
  .live-dock{flex-wrap:wrap;gap:8px;padding:8px 12px}
  .status-hero .on{gap:14px}
  .dest-row .url{display:none}
}
@media (max-width:560px){
  .split-2{grid-template-columns:1fr}
  .dock-right{margin-left:0}
}
/* 13. marketing page — container queries, not viewport media queries, so
   the editor's Desktop/Tablet/Mobile toggle (which resizes .site-container
   in place) actually reflows the page. Scoped to .site-container, which
   only wraps marketing.ejs — inert everywhere else. */
.site-container{container-type:inline-size}
/* Sticky footer: short pages (404, use-cases hub) otherwise left the footer
   floating mid-viewport above a large void. Excluded from the admin editor's
   narrowed preview, which sets its own height in JS to simulate devices. */
.site-container:not(.editor-narrowed){display:flex;flex-direction:column;min-height:100vh}
/* Children use max-width + margin:0 auto to centre themselves. As flex items
   those auto margins beat align-items:stretch and shrink them to content
   width, so restore full width explicitly and let max-width do the centring. */
.site-container:not(.editor-narrowed) > *{width:100%}
.site-container:not(.editor-narrowed) .land-footer{margin-top:auto}
@container (max-width:860px){
  .land-nav{padding:16px 20px;flex-wrap:wrap}
  .land-nav-toggle{display:flex}
  .land-nav-links{display:none;flex-direction:column;align-items:stretch;
    width:100%;order:3;gap:0;margin-top:14px;
    border-top:1px solid var(--line);padding-top:14px}
  .land-nav-links.open{display:flex}
  .land-nav-links a{padding:12px 4px;border-bottom:1px solid var(--line)}
  .land-nav-links a:last-child,
  .land-nav-links a.btn{border-bottom:0}
  .land-nav-links a.btn{margin-top:8px;text-align:center}
  .land-nav-dropdown{display:contents}
  .land-nav-dropdown-menu{display:none !important}
  .land-section{padding:48px 20px}
}
@container (max-width:560px){
  .pipeline-horizontal{display:none}
  .pipeline-vertical{display:block}
  .land-nav-links a{font-size:15px}
  .land-footer-links a{font-size:15px}
  .land-footer-brand p{font-size:14px}
  .price-features{font-size:14px}
  .hint{font-size:13px}
  .land-footer-bottom{font-size:13px}
}
.editor-toolbar{position:fixed;bottom:16px;right:16px;z-index:9999;background:var(--card);
  border:1px solid var(--line);border-radius:var(--radius);padding:12px;
  display:flex;flex-direction:column;gap:8px;width:220px;font-size:13px;
  box-shadow:0 8px 24px rgba(0,0,0,.4)}
.editor-drag-handle{cursor:move;color:var(--text-3);font-size:12px;font-weight:600;
  letter-spacing:.04em;padding-bottom:4px;border-bottom:1px solid var(--line);user-select:none}
.editor-status{color:var(--text-3)}
.editor-undo-redo{display:flex;gap:8px}
.editor-undo-redo .btn{flex:1;line-height:1.3}
.editor-keybind{display:block;color:var(--text-3);font-size:10px;font-weight:400;white-space:nowrap}
.editor-preview-toggle{display:flex;gap:4px}
.editor-preview-btn{flex:1;background:transparent;border:1px solid var(--line);color:var(--text-2);
  border-radius:6px;padding:6px 4px;font-size:12px;cursor:pointer}
.editor-preview-btn.active{background:var(--accent);color:#fff;border-color:var(--accent)}
.editor-back-link{color:var(--text-3);font-size:12px;text-align:center;text-decoration:none}
.site-container.editor-narrowed{margin:32px auto;overflow-y:auto;
  border:1px solid var(--line);border-radius:20px;box-shadow:0 20px 60px rgba(0,0,0,.5);
  scrollbar-width:thin;scrollbar-color:var(--line) transparent}
.site-container.editor-narrowed::-webkit-scrollbar{width:8px}
.site-container.editor-narrowed::-webkit-scrollbar-track{background:transparent}
.site-container.editor-narrowed::-webkit-scrollbar-thumb{background:var(--line);border-radius:4px}
.site-container.editor-narrowed::-webkit-scrollbar-thumb:hover{background:var(--text-3)}

/* OBS Custom Browser Docks render in a ~300-400px panel with no app shell.
   Everything here is scoped under body.dock so it cannot affect the main app. */
/* --text, not --text-1: the palette has no --text-1, and because this rule
   out-specifies body{color:var(--text)} the typo replaced a working colour
   with an invalid one, which resolves to inherit -> browser-default black on
   the near-black dock background. See test/css-variables.test.js. */
/* min-height:100vh so the dark background covers the WHOLE dock panel. The
   stream-info dock is only as tall as its cards (~360px in a ~1170px OBS
   dock), and a body box that stops short leaves the rest to the canvas —
   which OBS's browser panel does not necessarily paint dark. The chat dock
   hid this by accident because its feed is calc(100vh - 92px). */
body.dock{margin:0;min-height:100vh;background:var(--bg);color:var(--text);font-size:13px}
/* Names the panel inside OBS, where docks otherwise carry only OBS's own tiny
   title text. Sticks so it stays visible while the info dock's body scrolls. */
body.dock .dock-title{position:sticky;top:0;z-index:1;padding:7px 10px;font-weight:700;
  font-size:13px;letter-spacing:.02em;background:var(--bg);border-bottom:1px solid var(--line)}
/* The chat dock pins its send box at the bottom, so its feed is a fixed
   calc() height. Any header added on top would push the send box off-screen,
   so the chat page is a flex column instead: title + health are auto-height
   and the feed takes the rest — robust to further header changes. */
body.dock.dock-chat-page{display:flex;flex-direction:column;height:100vh;min-height:0}
body.dock.dock-chat-page .dock-title{position:static}
body.dock.dock-chat-page .dock-feed{flex:1 1 auto;height:auto;min-height:0}
body.dock .dock-feed{height:calc(100vh - 92px);overflow-y:auto;padding:8px}
body.dock .dock-send{display:flex;gap:4px;padding:8px;border-top:1px solid var(--line)}
body.dock .dock-send .input{flex:1;min-width:0}
body.dock .dock-card{display:flex;flex-direction:column;gap:6px;padding:10px;border-bottom:1px solid var(--line)}
body.dock .dock-card .input{width:100%}
/* At dock width the author and message are what matter; the timestamp is the
   first thing to go. */
body.dock .chat-ts{display:none}
body.dock .chat-row{flex-wrap:wrap}

/* The one pricing fact that matters during the beta. A plain lede line
   disappears among the other lede lines; this must read as an announcement. */
.beta-note{display:inline-block;margin:2px 0 30px;padding:10px 16px;
  border:1px solid rgba(77,124,254,.4);border-radius:10px;
  background:rgba(77,124,254,.1);color:var(--text);font-size:15px}

/* Chat panes before the first message: say what belongs here. */
.chat-empty{color:var(--text-3);font-size:13px;padding:18px 4px}

/* Broadcast rows are clickable; make the id read as the link it is. */
table.hist td a{color:var(--accent);text-decoration:none}

/* OBS Stream Info dock — live vitals header (2026-08-07). Scoped under
   body.dock so it cannot leak into the main app. */
body.dock .dock-vitals{padding:8px 10px;border-bottom:1px solid var(--line);font-size:12px}
body.dock .dock-vitals .dv-line{display:flex;gap:10px;align-items:center;font-weight:600}
body.dock .dock-vitals .dv-dests{display:flex;flex-direction:column;gap:2px;margin-top:4px;color:var(--text-3)}
body.dock .dock-vitals .dv-row{display:flex;flex-direction:column;gap:2px}
body.dock .dock-vitals .dv-row-top{display:flex;gap:8px;align-items:center}
body.dock .dock-vitals .dv-row .grow{margin-left:auto}
/* The eye icon is what makes this read as "viewer count" rather than blank
   space — a bare dim number here (2026-08-14) was invisible enough that the
   owner asked where the viewer count even was. */
body.dock .dock-vitals .dv-count{color:var(--text-2);font-variant-numeric:tabular-nums}
/* Unified warning badge (2026-08-14): relay failures, OAuth expiry and save
   failures all render through this one severity-coloured, click-to-expand
   pattern instead of three different-looking messages. */
body.dock .dock-vitals .dv-warn-badge{
  width:16px;height:16px;border-radius:50%;border:none;padding:0;
  font-size:11px;font-weight:800;line-height:16px;text-align:center;
  cursor:pointer;color:#08080d}
body.dock .dock-vitals .dv-warn-badge.ok{background:var(--ok);font-weight:900}
body.dock .dock-vitals .dv-warn-badge.warn{background:var(--warn)}
body.dock .dock-vitals .dv-warn-badge.crit{background:var(--live);color:#fff}
body.dock .dock-vitals .dv-warn-dropdown{
  display:flex;flex-direction:column;gap:6px;margin:4px 0 2px;padding:8px;
  border:1px solid var(--line);border-radius:8px;background:rgba(255,255,255,.03)}
body.dock .dock-vitals .dv-warn-item strong{display:block;font-size:12px;margin-bottom:2px;color:var(--text)}
body.dock .dock-vitals .dv-warn-item p{margin:0 0 6px;font-size:11px;color:var(--text-3);line-height:1.35}
body.dock .dock-vitals .dv-warn-item .btn{font-size:11px;padding:3px 8px}

/* Chat dock — per-platform liveness line (2026-08-07). */
body.dock .dock-chat-health{display:flex;gap:10px;padding:4px 8px;font-size:11px;color:var(--text-3);border-bottom:1px solid var(--line)}
body.dock .dock-chat-health .h-item{display:flex;gap:5px;align-items:center}

/* Dock vitals-header per-destination toggle + confirm-on-off (2026-08-07). */
body.dock .dock-vitals .dv-row .toggle{margin-left:6px}
body.dock .dock-vitals .dv-confirm{margin-left:6px;font-size:11px;padding:2px 8px;border-radius:6px;border:1px solid var(--warn);background:transparent;color:var(--warn);cursor:pointer}
