/* ==========================================================================
   FSP Finder — shared theme (new dark/teal design)
   Single stylesheet for all pages. Tweak tokens in :root.
   ========================================================================== */
:root{
  --ink:#1b1b1b;          /* page background        */
  --ink-2:#242424;        /* raised panels          */
  --paper:#e6e6e6;        /* headings / bright text */
  --body:#c9c9c9;         /* body text              */
  --accent:#26a69a;       /* teal accent (text)     */
  --accent-deep:#004d40;  /* teal buttons           */
  --redact:#b5462f;       /* flagged/explicit       */
  --mist:#8a8a8a;         /* muted text             */
  --line:#333;
  --radius:10px;
}
*{box-sizing:border-box;}
html{scroll-behavior:smooth;}
body{
  margin:0 auto; max-width:1080px; padding:0 24px 60px;
  background:var(--ink); color:var(--body);
  font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
  line-height:1.55; -webkit-font-smoothing:antialiased;
}
.mono{font-family:ui-monospace,Menlo,Consolas,"Liberation Mono",monospace;}
a{color:var(--accent);text-decoration:none;}
a:hover{text-decoration:underline;}
h1,h2,h3,h4,h5{color:var(--paper);line-height:1.25;margin:1.4rem 0 .6rem;}
h1{font-size:30px;} h2{font-size:26px;} h3{font-size:19px;} h4{font-size:16px;} h5{font-size:15px;}
p{margin:.7rem 0;}
ul,ol{margin:.6rem 0 .6rem 1.1rem;} li{margin:.3rem 0;}
hr{border:none;border-top:1px solid var(--line);margin:1.4rem 0;}
code{background:var(--ink-2);padding:.1rem .35rem;border-radius:4px;font-size:.9em;}
small{color:var(--mist);}
.muted{color:var(--mist);}
.lead{font-size:18px;color:var(--body);}
.center{text-align:center;}

/* form controls */
input[type=text],input[type=email],textarea,select{
  width:100%;background:var(--ink-2);color:var(--body);
  border:1px solid var(--line);border-radius:7px;padding:.6rem .7rem;font:inherit;font-size:14px;
}
textarea{resize:vertical;}
label{display:block;margin:.9rem 0 .3rem;color:var(--paper);font-weight:600;}

/* ---------- top bar ---------- */
.topbar{display:flex;align-items:center;justify-content:space-between;padding:22px 0;border-bottom:1px solid var(--line);}
.brand{font-weight:800;font-size:24px;letter-spacing:.01em;text-decoration:none;color:var(--paper);}
.brand:hover{text-decoration:none;}
.brand span{color:var(--accent);}
.topnav{display:flex;gap:28px;font-size:14px;}
.topnav a{color:var(--mist);text-decoration:none;}
.topnav a:hover{color:var(--paper);text-decoration:none;}

/* ---------- footer ---------- */
.site-footer{border-top:1px solid var(--line);padding:32px 0 10px;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:14px;margin-top:48px;}
.foot-links{display:flex;gap:20px;font-size:13px;flex-wrap:wrap;}
.foot-links a{color:var(--mist);text-decoration:none;}
.foot-links a:hover{color:var(--paper);text-decoration:none;}
.foot-note{font-size:12px;color:#5b6274;}

/* ---------- buttons ---------- */
.btn,.process-button,.btn-secondary{font:inherit;font-weight:600;border-radius:7px;cursor:pointer;display:inline-block;}
.process-button,.btn{background:var(--accent-deep);color:#fff;border:none;padding:12px 26px;font-size:15px;}
.process-button:hover,.btn:hover{background:#00695c;}
.process-button:disabled{opacity:.6;cursor:default;}
.btn-secondary{background:var(--ink-2);color:var(--accent);border:1px solid var(--accent-deep);padding:8px 16px;font-size:13px;}
.btn-secondary:hover{border-color:var(--accent);}

/* ---------- hero / dropzone (index) ---------- */
.hero{padding:44px 0 30px;}
.dropzone{display:block;margin-top:8px;border:1.5px dashed var(--line);border-radius:var(--radius);
  padding:40px 28px;text-align:center;background:linear-gradient(180deg,rgba(38,166,154,.05),transparent);
  transition:border-color .15s,background .15s;cursor:pointer;}
.dropzone:hover,.upload-form.drag-over .dropzone{border-color:var(--accent);background:rgba(38,166,154,.07);}
.dropzone .dz-title{font-size:16px;font-weight:600;margin-bottom:6px;color:var(--paper);}
.dropzone .dz-sub{font-size:13px;color:var(--mist);}
.browse-btn{display:inline-block;margin-top:18px;background:var(--ink-2);color:var(--paper);font-weight:600;font-size:14px;padding:10px 20px;border-radius:7px;border:1px solid var(--line);}
.dropzone:hover .browse-btn{border-color:var(--accent);}
.formats{font-size:11px;color:var(--mist);margin-top:14px;letter-spacing:.03em;}
#file-list-display{margin-top:16px;}
#file-list-display ul{list-style:none;padding:0;margin:0;}
#file-list-display li{display:flex;justify-content:space-between;align-items:center;background:var(--ink-2);border:1px solid var(--line);border-radius:7px;padding:9px 14px;margin:6px 0;font-size:14px;}
.dz-empty{color:var(--mist);font-size:13px;margin:6px 0 0;}
.remove-file-btn{background:transparent;border:none;color:var(--mist);cursor:pointer;font-size:16px;line-height:1;padding:0 4px;}
.remove-file-btn:hover{color:var(--redact);}
.cta-row{margin-top:22px;}

/* ---------- trust bar ---------- */
.trust{display:grid;grid-template-columns:repeat(3,1fr);gap:1px;background:var(--line);border:1px solid var(--line);border-radius:var(--radius);overflow:hidden;margin-top:34px;}
.trust-item{background:var(--ink-2);padding:20px 18px;}
.trust-item .t-label{font-size:11px;color:var(--accent);text-transform:uppercase;letter-spacing:.08em;margin-bottom:8px;font-weight:600;}
.trust-item p{margin:0;font-size:13.5px;color:var(--body);line-height:1.45;}

/* ---------- section / flow ---------- */
.section{padding:56px 0;border-top:1px solid var(--line);}
.section-head{margin-bottom:30px;}
.section-eyebrow{font-size:12px;color:var(--accent);text-transform:uppercase;letter-spacing:.1em;margin-bottom:10px;font-weight:600;}
.flow{display:flex;flex-direction:column;border-left:2px solid var(--line);margin-left:6px;}
.flow-step{position:relative;padding:0 0 34px 30px;}
.flow-step:last-child{padding-bottom:0;}
.flow-step::before{content:'';position:absolute;left:-7px;top:2px;width:12px;height:12px;border-radius:50%;background:var(--ink);border:2px solid var(--accent);}
.flow-step h3{font-size:16px;margin:0 0 6px;font-weight:600;display:flex;align-items:center;gap:10px;color:var(--paper);}
.flow-step h3 .tag{font-size:10px;color:#fff;background:var(--accent-deep);padding:2px 7px;border-radius:4px;letter-spacing:.04em;}
.flow-step p{margin:0;font-size:14px;color:var(--mist);max-width:58ch;}

/* ---------- advanced collapsible ---------- */
.adv{border:1px solid var(--line);border-radius:var(--radius);background:var(--ink-2);overflow:hidden;margin-top:22px;}
.collapsible-toggle{width:100%;text-align:left;background:transparent;border:none;color:var(--paper);font-size:14px;font-weight:600;padding:16px 20px;cursor:pointer;}
.collapsible-toggle::after{content:'▾';float:right;color:var(--mist);transition:transform .2s;}
.collapsible-toggle.active::after{transform:rotate(180deg);}
.collapsible-content{max-height:0;overflow:hidden;transition:max-height .25s;}
.adv-row{display:flex;align-items:flex-start;justify-content:space-between;gap:20px;padding:16px 20px;border-top:1px solid var(--line);}
.adv-row .r-title{font-size:13.5px;color:var(--paper);}
.adv-row .r-desc{font-size:12px;color:var(--mist);margin-top:2px;}
.adv-row textarea{margin-top:8px;background:var(--ink);}
.adv-row .grow{flex:1;}
.switch{position:relative;width:38px;height:22px;flex-shrink:0;display:inline-block;cursor:pointer;}
.switch input{opacity:0;width:0;height:0;position:absolute;}
.switch .track{position:absolute;inset:0;background:var(--line);border-radius:11px;transition:background .15s;}
.switch .track::after{content:'';position:absolute;top:2px;left:2px;width:18px;height:18px;border-radius:50%;background:var(--mist);transition:left .15s,background .15s;}
.switch input:checked + .track{background:rgba(38,166,154,.4);}
.switch input:checked + .track::after{left:18px;background:var(--accent);}

/* Advanced-settings hover detail. The trigger is a real button so the same text
   is reachable by keyboard and on touch, which a `title=` attribute is not. */
.adv-row .r-title{display:flex;align-items:center;gap:6px;}
.adv-tip{all:unset;cursor:help;flex-shrink:0;width:15px;height:15px;border:1px solid var(--line);border-radius:50%;color:var(--mist);font-size:10px;font-weight:600;line-height:15px;text-align:center;font-style:italic;}
.adv-tip:hover,.adv-tip:focus-visible{color:var(--accent);border-color:var(--accent);}
.adv-tip:focus-visible{outline:2px solid var(--accent);outline-offset:2px;}
/* position:fixed, and see static/index-adv-tip.js for why that is load-bearing:
   .adv and .collapsible-content are both overflow:hidden, so anything inside a
   row is clipped at the panel edge. Above .sticky-head's z-index range. */
.adv-tip-pop{position:fixed;z-index:9999;max-width:320px;background:#111;border:1px solid #3a3a3a;border-radius:8px;padding:10px 12px;font-size:12px;line-height:1.55;color:var(--body);pointer-events:none;}
.adv-select{background:var(--ink);color:var(--body);border:1px solid var(--line);border-radius:7px;padding:.5rem .6rem;font:inherit;font-size:14px;width:186px;flex-shrink:0;}
.adv-num{width:150px;background:var(--ink);color:var(--body);border:1px solid var(--line);border-radius:7px;padding:.5rem .6rem;font:inherit;font-size:14px;flex-shrink:0;}

/* ---------- changelog ---------- */
.ver{color:var(--accent);font-weight:600;margin-top:1.6rem;font-size:15px;}

/* ---------- job info box (review + waiting) ---------- */
.job-info-box{background:var(--ink-2);border:1px solid var(--line);border-radius:var(--radius);padding:14px 18px;margin:20px 0;font-size:14px;display:flex;flex-wrap:wrap;gap:6px 24px;align-items:center;}
.job-info-box .jib-item b{color:var(--paper);}
.job-info-box .jib-link{flex-basis:100%;display:flex;gap:10px;align-items:center;margin-top:4px;}
.job-info-box .jib-link a{word-break:break-all;}

/* ---------- help card (replaces clunky dropdown) ---------- */
.help-card{background:var(--ink-2);border:1px solid var(--line);border-left:3px solid var(--accent);border-radius:8px;padding:14px 18px;margin:16px 0;}
.help-card .hc-title{font-weight:600;color:var(--paper);margin-bottom:6px;}
.help-card ul{margin:.3rem 0 0 1.05rem;}
.help-card li{font-size:13.5px;color:var(--mist);}
.help-card s{color:var(--redact);}
.warn-inline{color:#ffcc00;}

/* ---------- status list (waiting) ---------- */
#job-list{list-style:none;padding:0;margin:.6rem 0;}
#job-list li{background:var(--ink-2);border:1px solid var(--line);border-radius:var(--radius);padding:12px 16px;margin:8px 0;}
.job-name{color:var(--paper);}
.job-state{color:var(--accent);}
.job-elapsed{color:var(--mist);}
.urlbox{background:var(--ink-2);border:1px solid var(--line);border-radius:7px;padding:10px 14px;font-size:13px;word-break:break-all;}

@media (max-width:640px){
  .trust{grid-template-columns:1fr;}
  .topnav{gap:16px;font-size:13px;}
}

/* ---- v2 fixes + review panel ---- */
#loading-overlay{display:none;}
.hidden{display:none;}
.topbar .brand,.topbar .brand:link,.topbar .brand:visited{color:var(--paper);}
.topbar .brand span{color:var(--accent);}
/* Single column since the track selector moved above (see .track-tabs). It was
   `grid-template-columns:210px 1fr` with a 28px gap, which cost the transcript
   238px of width on the one page whose job is reading long lyric lines. */
.review-container{margin-top:8px;}
.track-tabs-label{margin:0 0 8px;color:var(--mist);font-size:12px;text-transform:uppercase;letter-spacing:.06em;}
#file-list{list-style:none;padding:0;margin:0;}
.file-item{padding:10px 12px;border:1px solid var(--line);border-radius:8px;margin:6px 0;cursor:pointer;background:var(--ink-2);font-size:13.5px;color:var(--body);transition:border-color .12s,background .12s;}
.file-item:hover{border-color:var(--accent);}
.file-item.active{border-color:var(--accent);background:rgba(38,166,154,.12);color:var(--paper);font-weight:600;}
.track-meta{margin-bottom:14px;}
.track-title{font-size:20px;font-weight:700;color:var(--paper);line-height:1.2;}
.track-sub{font-size:13.5px;color:var(--mist);margin-top:3px;}
.track-sub a{color:var(--accent);}
.transcript-content form{display:flex;align-items:center;gap:12px;flex-wrap:wrap;background:var(--ink-2);border:1px solid var(--line);border-radius:10px;padding:14px 16px;margin:0 0 6px;}
.transcript-content form label{margin:0;font-weight:600;color:var(--paper);font-size:13.5px;}
.transcript-content form select{width:auto;min-width:150px;}
.finalize-submit{background:var(--accent-deep);color:#fff;border:none;border-radius:7px;padding:10px 18px;font:inherit;font-weight:600;font-size:14px;cursor:pointer;margin-left:auto;}
.finalize-submit:hover{background:#00695c;}
.finalize-status{font-size:12px;color:var(--mist);flex-basis:100%;}

@media (max-width:640px){.finalize-submit{margin-left:0;}}


/* v3 tweaks */
.job-state{color:var(--mist);}
.job-info-box{display:block;}
.job-info-box p{margin:.3rem 0;}
.accent-line{border-top-color:var(--accent);}
.transcript-content form{background:none;border:none;padding:0;display:block;margin:6px 0 2px;}
.download-controls{margin:6px 0;}
.dl-split{display:inline-flex;align-items:stretch;border-radius:8px;overflow:hidden;}
/* No divider rule here any more: the two halves are different fills as of
   2026-08-21, and a hairline on top of a tonal step is two dividers doing one
   job. */
.dl-split .finalize-submit{margin:0;border-radius:8px 0 0 8px;}

/* The format picker is a SETTING attached to the action, not a second action,
   and it has to look like it. It used to be the same --accent-deep fill at the
   same 600 weight and the same height as the submit button, so the control read
   as two equally weighted buttons and the eye had to work out which one did the
   thing. Nothing about the shape was wrong; the weight was.

   Three steps down and no other change: a darker fill than --accent-deep, muted
   text instead of white, and normal weight instead of bold. The height and the
   joined shape stay, because they are what says "this belongs to that button".

   The label keeps "(256kbps)". It is the one number anyone here is picky about,
   and dropping it to save ~40px would be trading the thing people came to check
   for width the ellipsis guard on .dl-label already handles. */
.dl-format{
  background:#00332b;color:#a8c9c3;border:none;border-radius:0 8px 8px 0;
  padding:0 11px;font-weight:400;font-size:12.5px;cursor:pointer;
  width:auto;min-width:0;
}
.dl-format:hover{background:#00443a;color:#d3e6e2;}
.dl-format:focus-visible{outline:2px solid var(--accent);outline-offset:-2px;}
.finalize-status{font-size:13px;color:var(--mist);margin-top:8px;}
.transcript-content table{font-size:14px;}
.transcript-content td,.transcript-content th{padding:.45rem .6rem;}
/* .seg-meta, not td:first-child: the preview play button (v7) is now the first
   cell in every row, so :first-child stopped meaning "the time column". */
.transcript-content td.seg-meta,.transcript-content th.time-column{white-space:nowrap;}
.topbar a:hover,.site-footer a:hover,.topbar .brand:hover{text-decoration:none !important;}

/* v4 review fixes */
.download-form{background:none !important;border:none !important;padding:0 !important;margin:6px 0 !important;display:block !important;}
.clickable-list .file-item.active,#file-list .file-item.active{background:rgba(38,166,154,.15) !important;color:var(--paper) !important;border-color:var(--accent) !important;}
.topbar a:hover,.topnav a:hover,.brand:hover,.site-footer a:hover,.foot-links a:hover{text-decoration:none !important;}
.transcript-content table{font-size:13px;}
body{max-width:1080px !important;}
/* v5 */
a{border-bottom:none;}
.topbar a,.topnav a,.brand,.site-footer a,.foot-links a{border-bottom:none !important;text-decoration:none !important;}
.transcript-content table{font-size:15px;}
.dropzone{max-width:540px;margin-left:auto;margin-right:auto;}
#file-list-display{max-width:540px;margin-left:auto;margin-right:auto;}
.cta-row{max-width:540px;margin-left:auto;margin-right:auto;text-align:center;}
/* The row is centred for the button; this sits under it, left-aligned, so it
   reads as a note about the thing above rather than as a second heading. */
.lang-note{text-align:left;margin:10px 0 0;font-size:13px;color:var(--mist);}
.adv{max-width:540px;margin-left:auto;margin-right:auto;}
/* v6: blue active track + header subtitle */
.clickable-list .file-item.active,#file-list .file-item.active{background:#0d47a1 !important;color:#fff !important;border-color:#0d47a1 !important;}
.brand-sub{color:var(--mist);font-weight:600;font-size:18px;}
@media (max-width:640px){.brand-sub{display:none;}}

.brand .brand-sub{color:var(--mist);}

.brand-sub, .topbar .brand .brand-sub{color:var(--mist) !important;}


/* ---- front-page additions ---- */
.get-started{text-align:center;margin:0 auto 22px;max-width:540px;}
.get-started .gs-title{margin:0 0 4px;}
.get-started .gs-sub{color:var(--mist);font-size:14px;}
.flow-home{display:grid;grid-template-columns:repeat(4,1fr);gap:26px;}
.flow-home .flow-step{border-top:1px solid var(--line);padding-top:16px;}
.flow-home .flow-num{color:var(--mist);font-size:13px;letter-spacing:.04em;margin-bottom:10px;}
.flow-home .flow-step h3{font-size:17px;margin:0 0 8px;color:var(--paper);}
.flow-home .flow-step p{color:var(--body);font-size:14px;line-height:1.55;margin:0;}
.pricing-card{background:var(--ink-2);border:1px solid var(--line);border-radius:12px;padding:26px 28px;}
.price{margin-bottom:14px;}
.price-amt{font-size:40px;font-weight:700;color:var(--paper);}
.price-unit{color:var(--mist);font-size:16px;}
.price-lead{color:var(--body);margin:0 0 16px;}
.price-note{color:var(--body);margin:0;line-height:1.6;}
.broadcast-note{background:var(--ink-2);border:1px solid var(--line);border-left:3px solid var(--accent);border-radius:8px;padding:16px 20px;margin-top:16px;color:var(--body);line-height:1.6;}
.broadcast-note b{color:var(--paper);}
.demo-lead{color:var(--body);line-height:1.6;margin:0 0 20px;}
.demo-toggle{display:inline-flex;gap:6px;background:var(--ink-2);border:1px solid var(--line);border-radius:999px;padding:4px;margin-bottom:20px;}
.demo-btn{border:none;background:none;color:var(--body);padding:8px 20px;border-radius:999px;cursor:pointer;font-size:14px;font-family:inherit;}
.demo-btn.active{background:var(--accent-deep);color:#fff;font-weight:600;}
.demo-player{width:100%;margin-bottom:18px;}
.demo-lyrics{background:var(--ink-2);border:1px solid var(--line);border-radius:10px;padding:20px 22px;color:var(--body);line-height:1.9;font-size:16px;}
.demo-lyrics-placeholder{color:var(--mist);font-style:italic;}
.lyric-cut{color:#d32f2f;text-decoration:line-through;}
@media(max-width:720px){.flow-home{grid-template-columns:repeat(2,1fr);}}
.flow-home .flow-step::before{display:none !important;}


/* ---- round 2 tweaks ---- */
.get-started{text-align:left;margin:0 0 22px;max-width:none;}
.dropzone,#file-list-display,.cta-row,.adv{max-width:75%;}
.demo-layout{display:grid;grid-template-columns:190px 1fr;gap:22px;align-items:start;}
.demo-tracklist{margin:0;}
.demo-tracklist .file-item{margin:0 0 8px;}
.demo-main{min-width:0;}
@media(max-width:720px){.demo-layout{grid-template-columns:1fr;}}

/* ---- round 3 ---- */
.intro-blurb{max-width:none;margin:0 0 16px;color:var(--body);line-height:1.6;text-align:left;}
.demo-protected{position:relative;}
.demo-protected .demo-player,.demo-protected .demo-lyrics{filter:blur(9px);pointer-events:none;transition:filter .25s;}
.demo-protected.revealed .demo-player,.demo-protected.revealed .demo-lyrics{filter:none;pointer-events:auto;}
.demo-warning{position:absolute;inset:0;z-index:3;display:flex;align-items:center;justify-content:center;text-align:center;padding:16px;cursor:pointer;color:var(--paper);font-weight:600;background:rgba(18,18,18,.55);border:1px solid var(--line);border-radius:10px;}
.demo-protected.revealed .demo-warning{display:none;}

.dz-limits{margin-top:6px;color:var(--mist);font-size:12px;}

.about-block h2{margin:0 0 10px;}
.hero-sep{border:none;border-top:1px solid var(--line);margin:32px 0;}


.upload-help{color:var(--body);line-height:1.6;margin:0 0 20px;}

.price-note{margin:0 0 14px;}
.pricing-card p:last-child{margin-bottom:0;}


/* ---- job footer: seed, report, re-transcribe ----
   align-items:flex-start so the disclosure panel grows DOWNWARD out of its own
   flex item instead of stretching the whole row to match. */
/* Two groups pushed apart: facts about this run on the left, the controls that
   change something on the right. space-between is what separates them, which is
   why there is no dot between the groups -- a dot joins items doing the same
   job, and these two groups do not. */
.retrans-row{
  display:flex;align-items:flex-start;justify-content:space-between;
  flex-wrap:wrap;gap:9px;
  margin-top:20px;color:var(--mist);font-size:13px;
}
.retrans-actions{display:flex;align-items:center;flex-wrap:wrap;gap:9px;min-height:24px;}
/* Both children get the chip's box height so their first lines sit on the same
   baseline. The row itself must stay flex-start (the panel grows downward out of
   the details, and centring would drag the meta down with it), which otherwise
   leaves the plain-text toggle riding above the bordered chip. */
.retrans-meta{display:flex;align-items:center;flex-wrap:wrap;gap:9px;min-height:24px;}
/* Mono because a seed is a value you copy and re-enter, not prose. */
.retrans-chip{
  background:var(--ink-2);border:1px solid var(--line);border-radius:5px;
  padding:2px 8px;font-size:12px;color:var(--body);
}
.retrans-sep{color:#4d4d4d;}
/* The same dot, for pages that have no .retrans-row to scope it to. */
.dot-sep{color:#4d4d4d;}
.retrans-row a.report-link,.retrans-row a.report-link:link,.retrans-row a.report-link:visited{color:var(--mist);}
.retrans-row a.report-link:hover{color:var(--accent);}

/* Identical to .flag-summary beside it, because they are the same kind of
   control. It used to be var(--accent) at rest -- the only teal thing in the
   row -- so the loudest item was also the one that spends a full GPU run on a
   gamble. Every property is set explicitly because style.css:99 puts
   padding:1.5rem and font-size:1.8rem on every bare <button>. */
.retrans-summary{
  display:inline-flex;align-items:center;min-height:24px;
  background:none;border:0;padding:0;
  font:inherit;color:var(--mist);cursor:pointer;user-select:none;
}
.retrans-summary:hover{color:var(--accent);}
.retrans-summary:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:3px;}
.retrans-summary::after{content:" \25BE";}
.retrans-summary[aria-expanded="true"]::after{content:" \25B4";}

.retrans-panel{display:flex;align-items:center;flex-wrap:wrap;gap:10px;margin-top:10px;}
/* The class rule would otherwise outrank the UA stylesheet's [hidden] and the
   panel would never close. Same guard .flag-panel carries below, needed here
   only since this stopped being a <details>. */
.retrans-panel[hidden]{display:none;}
.retrans-split{
  display:inline-flex;align-items:stretch;height:32px;
  border:1px solid var(--line);border-radius:7px;overflow:hidden;
}
/* Compound selectors, not bare `input`: app.css's own
   input[type=text],...,select rule and sakura's input styling both otherwise
   win, which is why this block used to be a wall of inline styles. */
/* height:100%, not align-items:stretch alone: sakura gives inputs an explicit
   height, which wins over the stretch and left the field sitting 10px shorter
   than the button inside their shared border. */
.retrans-split .retrans-seed{
  width:120px;height:100%;border:none;outline:none;
  background:var(--ink);color:var(--body);font-size:13px;padding:0 .7rem;
}
.retrans-split .retrans-btn{
  border:none;border-left:1px solid var(--line);
  background:var(--ink-2);color:var(--accent);
  font-size:13px;padding:0 .9rem;cursor:pointer;white-space:nowrap;
}
.retrans-split .retrans-btn:hover{background:var(--accent-deep);color:#fff;}
.retrans-split .retrans-btn:disabled{background:var(--ink-2);color:var(--mist);cursor:default;}
.retrans-hint{font-size:12px;color:var(--mist);}

.word-tip{position:fixed;z-index:9999;background:#0f1720;color:#e8eef2;border:1px solid var(--line);border-radius:8px;padding:8px 10px;font-size:12px;line-height:1.55;box-shadow:0 6px 20px rgba(0,0,0,.45);pointer-events:none;white-space:nowrap;}
.word-tip b{color:#9fb3c8;font-weight:600;display:inline-block;min-width:78px;}

.foot-note a.foot-link{color:inherit;text-decoration:none;}
.site-footer a.foot-link:hover{text-decoration:underline !important;}


/* ---- v7: segment preview ---- */
/* Narrow and glyph-only: this column repeats on every transcript row, so
   anything wider would compete with the words for attention. */
.transcript-content th.seg-play-col{width:30px;padding-left:0;padding-right:0;}
.transcript-content td.seg-play-cell{width:30px;padding-left:0;padding-right:0;text-align:center;vertical-align:middle;}

/* Compound selectors throughout, for the reason spelled out at style.css:366.
   style.css sets a GLOBAL button{padding:1.5rem;font-size:1.8rem;background:#004d40}
   at (0,0,1), which renders a bare button inside this table absurdly large;
   every property it or sakura would otherwise impose is reset explicitly here.
   Dropping any one of them lets that rule back in. */
.seg-play-cell .seg-play-btn{
  display:inline-flex;align-items:center;justify-content:center;
  width:22px;height:22px;padding:0;margin:0;
  border:1px solid var(--line);border-radius:50%;
  background:none;color:var(--mist);
  font-family:inherit;font-size:9px;font-weight:400;line-height:1;
  cursor:pointer;transition:color .15s,border-color .15s,background .15s;
}
.seg-play-cell .seg-play-btn::before{content:"\25B6";}          /* right triangle */
.seg-play-cell .seg-play-btn:hover{color:var(--accent);border-color:var(--accent);background:none;}
.seg-play-cell .seg-play-btn.loading::before{content:"\2026";font-size:12px;}
.seg-play-cell .seg-play-btn.playing{color:#fff;background:var(--accent-deep);border-color:var(--accent);}
.seg-play-cell .seg-play-btn.playing::before{content:"\25A0";font-size:10px;}  /* square */
.seg-play-cell .seg-play-btn.pv-error{color:#d32f2f;border-color:#d32f2f;}

/* The row being previewed. A tinted edge rather than a strong fill, so it can
   never be read as the red block a censored .span-mute gets. */
.transcript-content tr.pv-active td{background:rgba(38,166,154,.07);}
.transcript-content tr.pv-active td.seg-play-cell{box-shadow:inset 2px 0 0 var(--accent);}

/* Follow-along highlight. Background only, deliberately: .word-toggle.censored
   owns colour and line-through, so this has to compose with it, not fight it. */
.word-toggle.pv-current{background:rgba(38,166,154,.28);border-radius:3px;}

/* The preview controls. Since the bulk box moved to the word and the create
   button moved onto the transport, this is all the row holds -- which is the
   point: what is left is the two settings you change constantly. No margin of
   its own; the row owns the spacing. */
.preview-bar{display:flex;align-items:center;flex-wrap:wrap;gap:10px;margin:0;color:var(--mist);font-size:13px;}
.preview-bar .pv-label{font-weight:600;color:var(--body);}
.pv-toggle{display:inline-flex;background:var(--ink-2);border:1px solid var(--line);border-radius:999px;padding:3px;}
.pv-toggle .pv-btn{
  border:none;border-radius:999px;background:none;color:var(--body);
  padding:4px 12px;margin:0;
  font-family:inherit;font-size:12px;font-weight:400;line-height:1.4;
  cursor:pointer;white-space:nowrap;
}
.pv-toggle .pv-btn.active{background:var(--accent-deep);color:#fff;font-weight:600;}

/* The transcript table lost its <h5> heading, so this row is now two flex
   children rather than three. Margin below it stands in for the heading's. */
.transcript-head{margin-bottom:12px;}
/* What the transport covers, moved off the bar on 2026-08-29 so the seek track
   gets the whole row. .transcript-head is justify-content:space-between, so this
   sits hard right opposite the preview toggles. */
.head-right{display:flex;align-items:center;gap:10px;}


/* Visually hidden -- the play column's header cell has no visible label. */
.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;}


/* ---- v8: track header + download in one recessed box ---- */
.track-header{
  display:flex;align-items:center;justify-content:space-between;gap:16px;flex-wrap:wrap;
  background:var(--ink-2);border:1px solid var(--line);border-radius:var(--radius);
  padding:14px 18px;margin:0 0 16px;
}
/* The bottom margin now belongs to the box, not the text inside it. */
/* flex:1 1 0, not the default 0 1 auto. `auto` bases this item on its MAX-CONTENT
   width, and .track-header is flex-wrap:wrap -- so a long song title did not
   shrink, it pushed the whole download control onto a second line. Flex shrink
   only applies to items already sharing a line; the wrap decision happens first,
   off the flex-basis. A 0 basis makes the title yield instead.

   min-width:0 is still required: without it a flex item refuses to go below its
   min-content width and the ellipsis below never triggers. */
.track-meta{margin-bottom:0;flex:1 1 0;min-width:0;}
/* One line each, clipped with an ellipsis. The full text is on the title
   attribute in the template, same as the track tabs above the transcript. */
.track-header .track-title,
.track-header .track-sub{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}

/* The form is laid out by .track-header, so it contributes no box of its own.
   !important because app.css:199's .download-form rule already uses it. */
.track-header .download-form{margin:0 !important;flex-shrink:0;min-width:0;max-width:100%;}
.track-header .download-controls{margin:0;}

/* Width-independent guard, not a duplicate of the breakpoint below. .dl-split is
   inline-flex at a natural ~377px, so it can outgrow the box at any container
   width the media query does not cover -- notably just ABOVE 640px, where the
   210px track list leaves the right column narrower than the split itself. The
   label ellipsises as a last resort rather than bursting the box. */
.track-header .dl-split{max-width:100%;}
.track-header .dl-split .finalize-submit{min-width:0;}
.track-header .dl-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}

/* margin-left:auto (app.css:174) pushed the button to the right of a full-width
   form. Inside .dl-split that would shove it off its own select. */
.track-header .dl-split .finalize-submit{display:inline-flex;align-items:center;gap:8px;margin:0;}
.dl-icon{flex-shrink:0;}

/* Reserves one line so the box does not grow the instant a render starts. It
   wraps rather than stretching the row, which matters for the long failure
   messages ("Could not start render: ...", "Timed out waiting for the render"). */
.track-header .finalize-status{
  /* min-height must equal line-height exactly, or the first status message
     still nudges the box a couple of pixels taller. */
  margin-top:8px;line-height:1.35;min-height:1.35em;
  text-align:right;max-width:340px;margin-left:auto;
}

/* Stacked and full-width once the two halves stop fitting side by side.
   .dl-split is inline-flex and will not shrink on its own. */
@media (max-width:640px){
  .track-header{align-items:stretch;}
  .track-header .download-form{width:100%;}
  .track-header .dl-split{display:flex;width:100%;}
  .track-header .dl-split .finalize-submit{flex:1;justify-content:center;}
  .track-header .finalize-status{text-align:left;margin-left:0;max-width:none;}
}


/* ---- v9: one-line help, pinned head, seek bar ------------------------------
   The help block used to be six lines above the transcript, which is content
   read once and then scrolled past forever. */
/* Page title and the help control share one row: the title anchors the page and
   the button sits at the far end, where it is findable without competing. */
.page-head{
  display:flex;align-items:center;justify-content:space-between;
  gap:16px;flex-wrap:wrap;margin:1.9rem 0 0;
}
.page-title{margin:0;font-size:26px;color:var(--paper);}
.help-open-btn{
  display:inline-flex;align-items:center;gap:7px;
  border:1px solid var(--line);border-radius:6px;background:var(--ink-2);
  color:var(--body);font-size:13px;padding:5px 12px;cursor:pointer;
}
.help-open-btn:hover{border-color:var(--accent);color:var(--accent);}
/* Inherits stroke from the button, so it recolours with :hover for free. */
.help-icon{flex:none;}

/* ::backdrop is the browser's own layer, so the dim behind the dialog costs no
   markup. max-height + overflow because the list outgrows a short viewport. */
.help-dialog{
  border:1px solid var(--line);border-radius:var(--radius);
  background:var(--ink-2);color:var(--body);
  padding:18px 22px;max-width:560px;width:calc(100% - 32px);
  max-height:80vh;overflow-y:auto;
}
.help-dialog::backdrop{background:rgba(0,0,0,.6);}
.help-dialog-head{display:flex;align-items:center;justify-content:space-between;gap:12px;}
.help-dialog-head h5{margin:0;}
.help-close-btn{
  border:none;background:none;color:var(--mist);
  font-size:24px;line-height:1;padding:0 4px;cursor:pointer;
}
.help-close-btn:hover{color:var(--paper);}
.help-dialog ul{margin:14px 0 0;padding-left:20px;font-size:14px;}
.help-dialog li{margin-bottom:9px;}

/* Problem report ---------------------------------------------------------
   An inline disclosure in the job footer, NOT a dialog: the Re-transcribe
   control sitting beside it already works this way, so the row keeps one
   pattern instead of two. It reads as the quietest thing in a quiet row --
   this is a bug report, not a second thing to do with the track. */
.flag-summary{
  display:inline-flex;align-items:center;min-height:24px;
  background:none;border:0;padding:0;
  font:inherit;color:var(--mist);cursor:pointer;
}
.flag-summary::after{content:" \25BE";}
.flag-summary[aria-expanded="true"]::after{content:" \25B4";}
.flag-summary:hover{color:var(--accent);}
.flag-summary:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:3px;}
/* Once reported, the label IS the state -- there is nowhere else in this row for
   it to live, and the panel it came from is usually closed again. */
.flag-summary[data-flagged]:not([data-flagged=""]){color:#c98f7a;}

/* flex-basis:100% is what drops the panel onto its own line. It is the last item
   in .retrans-row, so opening it moves nothing already sitting on the first
   line -- which is the whole reason this is a sibling of the summary rather
   than a <details> wrapping it. */
.flag-panel{
  flex:1 0 100%;margin-top:2px;padding:14px;
  background:var(--ink-2);border:1px solid var(--line);border-radius:var(--radius);
}
/* The class rule would otherwise outrank the UA stylesheet's [hidden] and the
   panel would never close. Same for the two states inside it. */
.flag-panel[hidden],.flag-form[hidden],.flag-sent[hidden]{display:none;}

.flag-head{margin:0 0 10px;color:var(--paper);font-size:15px;font-weight:600;}

.flag-reasons{border:0;margin:0;padding:0;display:flex;flex-wrap:wrap;gap:7px;}
.flag-chip{margin:0;}
.flag-chip input{position:absolute;opacity:0;width:0;height:0;}
.flag-chip span{
  display:inline-block;padding:7px 12px;
  background:#1c1c1c;border:1px solid var(--line);border-radius:999px;
  color:var(--body);font-size:12.5px;line-height:1;cursor:pointer;
  transition:border-color .12s,color .12s,background .12s;
}
.flag-chip span:hover{border-color:#4a4a4a;}
.flag-chip input:checked + span{background:#123a35;border-color:var(--accent);color:#bfe8e2;}
/* The input is visually hidden, so the ring has to be drawn on the label or
   keyboard users get no focus indication at all. */
.flag-chip input:focus-visible + span{outline:2px solid var(--accent);outline-offset:2px;}

.flag-panel textarea.flag-note{
  width:100%;margin-top:12px;padding:9px 10px;
  background:#161616;border:1px solid var(--line);border-radius:8px;
  color:var(--body);font-family:inherit;font-size:13px;line-height:1.5;resize:vertical;
}
.flag-count{display:block;text-align:right;margin-top:4px;font-size:11px;color:var(--mist);}

/* Stated BEFORE the button. What a report does, and the part that is awkward to
   write -- there is no reply -- is exactly what somebody deserves to know
   before they commit, not after. */
.flag-outcome{
  margin:12px 0 0;padding:10px 12px;
  background:#15201f;border:1px solid #22403b;border-radius:8px;
  color:#9fc4bd;font-size:12.5px;line-height:1.55;
}
.flag-outcome b{color:#bfe8e2;font-weight:600;}

.flag-actions{display:flex;align-items:center;justify-content:flex-end;gap:9px;margin-top:12px;}
/* Takes the slack so the buttons stay pinned right whatever it says, and wraps
   instead of pushing them off the edge. */
.flag-status{flex:1;min-width:0;font-size:12px;color:var(--mist);}
.flag-status.err{color:#e2a08f;}

.flag-cancel{
  background:transparent;border:1px solid var(--line);color:var(--mist);
  border-radius:8px;padding:9px 14px;font-size:13px;line-height:1;cursor:pointer;
}
.flag-cancel:hover{color:var(--paper);}
.flag-send{
  background:var(--accent-deep);border:0;color:#dff3f0;
  border-radius:8px;padding:9px 16px;font-size:13px;font-weight:600;line-height:1;cursor:pointer;
}
.flag-send:disabled{opacity:.42;cursor:not-allowed;}
.flag-cancel:focus-visible,.flag-send:focus-visible{outline:2px solid var(--accent);outline-offset:2px;}

.flag-sent-body{margin:0 0 8px;color:var(--body);font-size:13px;}
.flag-sent-note{margin:0;color:var(--mist);font-size:12.5px;}

/* A word the isolated vocals do not contain, recovered from the full mix. Its
   own marking because it is the one word on the line the reviewer cannot check
   by ear in vocals-only mode. Underline rather than a colour: .censored already
   owns colour, and a mix word is very often also censored -- these have to
   stack, not fight.

   box-shadow rather than the border-bottom this used to carry: the near-miss
   hint below needs the dotted underline, and two dotted underlines differing
   only in hue is exactly the failure the confidence-meter notes warn about.
   box-shadow is otherwise unused on a word span, so the two stack cleanly. The
   split also reads better than it did -- SOLID means provenance (heard only in
   the mix), DOTTED means uncertainty (check this). */
.word-toggle.mix-word{box-shadow:inset 0 -1px 0 var(--accent);}

/* Advisory: whisper was unsure about this word AND it sits one edit from
   something we would have muted, or whisper masked it itself ("n***a"). Says
   "my best guess, but check it" -- it is NOT a flag and censors nothing. See
   cloud/near_miss.py, which computes it server-side so the lexicon stays in one
   place; there is no JavaScript behind this at all.

   :not(.censored) is load-bearing, not tidiness. .word-toggle.censored lives in
   style.css at (0,2,0) and app.css loads second, so without it a censored word
   would lose its red strikethrough to this rule -- the same trap CLAUDE.md
   records for .lo-conf.

   Amber, matching .qmtr.q-shaky: the meter already means "doubtful" in that
   colour, so an uncertain word and an uncertain line read as one idea. */
.word-toggle[data-near-miss]:not(.censored){border-bottom:1px dotted #ffcc00;}

/* Legend samples for the two treatments above. They need their OWN rules rather
   than reusing the real ones: the real selectors are .word-toggle.lo-conf and
   .word-toggle[data-near-miss], and the dialog samples are bare spans. The
   pre-existing <span class="lo-conf"> sample had no matching rule anywhere and
   so was never actually greyed -- the legend showed ordinary text and claimed
   it was dimmed. */
.help-dialog .help-lo-conf{color:#a0a0a0;}
.help-dialog .help-near-miss{border-bottom:1px dotted #ffcc00;}

/* The pinned block. Opaque background is what stops transcript rows showing
   through as they scroll under it; --ink is the page background, not --ink-2,
   because this sits on the page and not in a panel. */
.sticky-head{
  position:sticky;top:0;z-index:5;
  background:var(--ink);padding-top:10px;
}
/* Sticky needs a measured offset, not a guess: this block's height changes when
   the seek bar appears, when a finalize status message wraps, and on any narrow
   viewport that stacks the download control. --sticky-h is written by the
   preview script from a ResizeObserver. */
.transcript-content thead th{
  position:sticky;top:var(--sticky-h,0px);z-index:4;
  background:var(--ink);
}

/* Playback position for the previewing line. Permanently in the layout: it used
   to be [hidden] until something played, which changed the pinned head's height
   on every play and stop and shunted the whole transcript up and down. It greys
   out instead. Only the no-Web-Audio branch removes it, via inline display. */
.seek-bar{display:flex;align-items:center;gap:10px;padding-bottom:12px;}
.seek-bar[hidden]{display:none;}

/* Idle: nothing has been played yet. Dimmer than --mist so it reads as
   unavailable rather than merely quiet, and inert to the pointer. */
.seek-bar.seek-idle .seek-time{color:#4d4d4d;}
.seek-bar.seek-idle .seek-play,
.seek-bar.seek-idle .seek-play:hover{color:#4d4d4d;border-color:#2b2b2b;cursor:default;}
.seek-bar.seek-idle .seek-track{cursor:default;background:var(--ink);}
.seek-bar.seek-idle .seek-fill,
.seek-bar.seek-idle .seek-thumb{background:#333;}

/* Finished: holding the position of the line that just played. Fully live --
   the track still scrubs and the button still plays, they just start the line
   again from wherever the bar points rather than moving a running graph. Only
   .seek-idle is inert, because before anything has played there is no line for
   a position to refer to. */
.seek-play{
  width:26px;height:26px;flex-shrink:0;padding:0;
  border:1px solid var(--line);border-radius:50%;background:none;
  color:var(--body);font-size:10px;line-height:1;cursor:pointer;
}
.seek-play:hover{color:var(--accent);border-color:var(--accent);}
.seek-play::before{content:"\275A\275A";letter-spacing:1px;}
.seek-play.paused::before{content:"\25B6";font-size:11px;letter-spacing:0;}
.seek-time{
  font-family:ui-monospace,Menlo,Consolas,"Liberation Mono",monospace;
  font-size:12px;color:var(--mist);min-width:46px;
}
.seek-dur{text-align:right;}
/* Absolute bounds of what the bar covers, and ONLY in line mode -- armBar
   leaves it empty for the whole track, where it would read "0:00 to 3:34" and
   the duration already says so. It is not a restatement of the two clocks
   either side of the track, which is what the comment here used to claim: in
   line mode BOTH of those go window-relative, showing position within the line
   and the line's own length, so this is the only thing left saying where in the
   song you are.

   :empty is load-bearing, not tidiness. An empty flex child still costs its
   gap, so without this there are 10px of nothing sitting next to the mode
   select on every track-mode page. */
.seek-range{font-size:12px;color:var(--mist);white-space:nowrap;}
.seek-range:empty{display:none;}
.seek-track{
  flex:1;position:relative;height:6px;min-width:60px;
  border:1px solid var(--line);border-radius:999px;background:var(--ink-2);
}
/* A 6px-tall click target is a miss waiting to happen. This extends the hit
   area to 24px without changing the drawn height; clicks on it still land on
   .seek-track itself. */
.seek-track::before{content:"";position:absolute;inset:-9px 0;}
.seek-track:focus-visible{outline:2px solid var(--accent);outline-offset:4px;}
.seek-fill{width:0;height:100%;border-radius:999px;background:var(--accent);}
.seek-thumb{
  position:absolute;top:50%;left:0;width:13px;height:13px;
  margin:-7px 0 0 -6px;border-radius:50%;background:var(--accent);
  pointer-events:none;
  transition:width .1s,height .1s,margin .1s,box-shadow .1s;
}

/* The bar is DRAGGED, not just clicked, and it has to say so before it is
   touched. A grab cursor is the whole affordance here: the thumb is 13px on a
   6px rail, so without one the control reads as a click target and the drag
   goes undiscovered. Live playback and a finished line are both draggable; only
   .seek-idle, which is describing nothing, is not.

   The thumb grows on grab rather than on hover, so it acknowledges the press
   itself -- during a drag the cursor is usually nowhere near the thumb, which is
   the moment the extra feedback is worth most. Nothing here changes layout: the
   thumb is absolutely positioned and the growth is absorbed by its margin. */
.seek-track{cursor:grab;}
.seek-bar.seek-idle .seek-track{cursor:default;}
.seek-bar.seek-dragging .seek-track,
.seek-bar.seek-dragging{cursor:grabbing;}
.seek-bar.seek-dragging .seek-thumb{
  width:17px;height:17px;margin:-9px 0 0 -8px;
  box-shadow:0 0 0 4px rgba(38,166,154,.22);
}
/* The drag owns the drawing while it is happening (tick() stands down), so the
   fill must not animate toward the finger a frame behind it. */
.seek-bar.seek-dragging .seek-fill{transition:none;}

/* The censored spans, drawn on the bar. Same muteSpans() output the audio is
   muted with, so the bar shows the edits rather than only playing them -- which
   is the point of the whole track transport: what you see is what you hear.
   Above .seek-fill so a tick stays visible on played-through audio, below
   .seek-thumb so the handle is never lost behind one. */
.seek-mutes{position:absolute;inset:0;pointer-events:none;overflow:hidden;border-radius:999px;}
.seek-mutes i{
  position:absolute;top:0;height:100%;min-width:2px;
  /* #d32f2f is the colour .word-toggle.censored already uses (style.css:315).
     A tick and the word it stands for have to read as the same thing. */
  background:#d32f2f;border-radius:1px;
}
/* Idle bars describe nothing, so their ticks would be claiming a position on a
   scale that is not there. */
.seek-bar.seek-idle .seek-mutes{display:none;}

/* Waiting on the FIRST window of a run. Not shown for a prefetch that is merely
   topping the runway up: that is not the user waiting, and a bar that pulsed
   every fifteen seconds would read as a permanent fault. */
.seek-bar.seek-buffering .seek-fill{opacity:.45;}
.seek-bar.seek-buffering .seek-thumb{animation:seek-pulse 1s ease-in-out infinite;}
@keyframes seek-pulse{50%{opacity:.35;}}
/* A brief red edge rather than a message: it takes no layout space, and this
   bar lives in the pinned head where a line of text appearing would resize it. */
.seek-bar.seek-error .seek-track{border-color:#d32f2f;}

/* What the bar covers. A native select rather than a third .pv-toggle pill pair:
   the two pill groups are listening MODES that apply to whatever is playing,
   while this changes what "whatever is playing" even means. It sat on the bar
   until 2026-08-29, on the reasoning that it decides the bar's own length --
   true, but it and the create button were squeezing the seek track from both
   ends, and the row above had been empty on the right since the word-search box
   moved to the word. It is one row away and still the only route to line mode.

   Compound selector and every property set explicitly, the same problem
   app.css:1151 documents for the range fields. The form-control rule 40 lines
   up in this file --
       input[type=text],input[type=email],textarea,select{width:100%; ...}
   -- is specificity (0,1,1) and beats a lone .seek-mode (0,1,0) whatever the
   file order, which is what would stretch this select across the whole bar.
   (0,2,0) wins. Dropping any property below lets that rule back in. */
.transcript-head .seek-mode{
  width:auto;flex:0 0 auto;margin:0;padding:3px 22px 3px 8px;
  background:var(--ink-2);color:var(--body);
  border:1px solid var(--line);border-radius:999px;
  font-family:inherit;font-size:12px;line-height:1.4;cursor:pointer;
}
.transcript-head .seek-mode:disabled{color:#4d4d4d;border-color:#2b2b2b;cursor:default;}

@media (max-width:640px){
  /* The block is a large fraction of a phone viewport once the download control
     stacks, so pinning it would leave barely any transcript visible. */
  .sticky-head{position:static;padding-top:0;}
  .transcript-content thead th{position:static;}
  /* .seek-range goes, the mode select STAYS. The range is a restatement of the
     two clocks either side of the track; the select is the only way to reach
     line mode, so hiding it would strand anyone on a phone in whichever mode
     they last used on a desktop. */
  .seek-range{display:none;}
  .seek-bar{flex-wrap:wrap;}
}


/* ---- v9: waiting-page per-track boxes ----
   One box per uploaded track. Tracks are transcribed strictly one at a time
   (GPU ASG MaxSize=1, one T4, 1 GPU per task), so only the box on the GPU is
   expanded and the rest collapse to a single word -- three collapsed rows plus
   one open one is about the height of the old single progress block.

   Segment flex-grow values are set inline by the template from measured phase
   durations, so the two pre-worker phases visibly own the first half of the bar
   and a user watching a 3.5 minute cold start can see the wait is expected. */
.refresh-note{margin:0 0 22px;font-size:13.5px;}
.tracks-head{
  display:flex;justify-content:space-between;align-items:baseline;
  gap:12px;margin:0 0 10px;font-size:14px;color:var(--paper);
}
.tracks-head .muted{font-size:13px;}

.tbox{
  background:var(--ink-2);border:1px solid var(--line);border-radius:var(--radius);
  padding:12px 16px;margin:0 0 8px;
}
.tbox.on{border-color:var(--accent);}
.tbox.bad{border-color:var(--redact);}
.thead{display:flex;align-items:center;gap:10px;}
.tname{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
  font-size:14px;color:var(--paper);}
.tpill{font-size:12px;white-space:nowrap;color:var(--mist);}
.tpill.pill-done{color:var(--accent);}
.tpill.pill-bad{color:var(--redact);}
.tico{width:14px;text-align:center;flex:none;font-size:13px;color:var(--mist);}
.tico.ico-on{color:var(--accent);}
.tico.ico-done{color:var(--accent);}
.tico.ico-bad{color:var(--redact);}
.tico.ico-wait{color:var(--line);}

.tbar{display:flex;gap:3px;margin:12px 0 9px;}
.tseg{height:7px;border-radius:3px;background:var(--line);overflow:hidden;}
/* width, not transform: the segment is a fixed-width flex child and a transform
   would overflow its neighbour. 1s linear matches the tick, so the fill creeps
   rather than stepping. */
.tfill{height:100%;width:0;background:var(--accent);transition:width 1s linear;}
.tph{font-size:13.5px;color:var(--body);}

@media (max-width:640px){
  .tracks-head{flex-direction:column;align-items:flex-start;gap:2px;}
  .tbar{gap:2px;}
}


/* ---- v10: track selector above the transcript ----
   Equal-width tabs that CAP at the old 210px sidebar width and shrink to share
   the row when there are more of them. Fixed 210px does not fit the advertised
   maximum: the body is 1080px with 24px padding each side and box-sizing is
   border-box, so the content box is 1032px, and 5 x 210 + 4 x 8px gaps = 1082px
   -- 50px of overflow. flex:1 1 0 with max-width lets 5 settle at 200px each
   while 2 stay at 210px, so they are always identical to each other and always
   fit. Long names clip rather than wrap, so every tab is one line tall. */
.track-tabs-wrap{margin:18px 0 14px;}
.track-tabs{
  list-style:none;display:flex;gap:8px;padding:0;margin:0;flex-wrap:wrap;
}
.track-tabs .file-item{
  flex:1 1 0;min-width:0;max-width:210px;
  background:var(--ink-2);border:1px solid var(--line);border-radius:var(--radius);
  padding:8px 12px;font-size:13px;cursor:pointer;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
.track-tabs .file-item:hover{border-color:var(--mist);}
.track-tabs .file-item.active{
  background:rgba(38,166,154,.15);color:var(--paper);border-color:var(--accent);
}

/* Below the breakpoint the row would give each tab ~60px, which clips every
   name to nothing. Stack them full width instead. */
@media (max-width:640px){
  .track-tabs{flex-direction:column;}
  .track-tabs .file-item{max-width:none;}
}


/* --------------------------------------------------------------------------
   Admin page (/admin)
   Internal surface, not linked from anywhere. Its own classes rather than
   style= attributes: style-src still allows inline styles, but the 30 already
   in the templates are tracked as CSP debt in BACKLOG.md and this page has no
   reason to grow them.
   -------------------------------------------------------------------------- */
/* !important answers the blanket `body{max-width:1080px !important}` in the v4
   block above, which silently won and left this page at 1080 -- so the wide
   table below never had the room this rule exists to give it, and the summary
   strip had to wrap. */
body.admin-page{max-width:1320px !important;}
.section-first{border-top:none;padding-top:40px;}
.admin-note{font-size:13px;margin-top:-.6rem;}
.admin-warn{font-size:13px;color:var(--redact);border-left:2px solid var(--redact);padding-left:10px;}

/* "This transcript is not readable English", above the transcript it is about.
   Amber, the colour q-silent and .st-processing already use for "unreliable":
   red is spoken for on this page -- it means censored -- and a warning in the
   same red as the edits would read as an edit. Left rule only, so no radius
   (a rounded corner on a single-sided border is a hairline that stops). */
.lang-warn{border-left:2px solid #d3a04b;border-radius:0;padding:2px 0 2px 12px;margin:14px 0 12px;}

/* The same amber statement on the waiting page, where a track was stopped
   before transcription rather than transcribed badly. */
/* Amber, not --redact: a track stopped for its language is terminal but is not
   a breakage, and red on this site means either a failure or a censored word.
   Same amber as the review page's notice, for the same statement. */
.tbox.lang{border-color:#d3a04b;}
.tpill.pill-lang{color:#d3a04b;}
.tico.ico-lang{color:#d3a04b;}
/* The way through to the tracks that DID finish. Not .warn-inline: nothing here
   went wrong, and the page cannot reload into them on a mixed upload. */
.done-inline{font-size:13px;color:var(--mist);font-style:italic;}
.done-inline a{color:var(--accent);}
.tnote{border-left:2px solid #d3a04b;border-radius:0;padding:8px 0 2px 12px;margin:6px 0 2px;}
.tnote-msg{color:var(--body);font-size:13px;line-height:1.5;}
.tnote-btn{font:inherit;font-size:13px;font-weight:600;margin-top:8px;cursor:pointer;
  background:var(--ink-2);color:var(--paper);border:1px solid var(--line);
  border-radius:7px;padding:7px 14px;}
.tnote-btn:hover{border-color:var(--accent);color:var(--accent);}
.tnote-btn:disabled{opacity:.6;cursor:default;border-color:var(--line);color:var(--mist);}
.lang-warn-title{color:#d3a04b;font-size:13px;font-weight:600;}
.lang-warn-body{color:var(--body);font-size:13px;line-height:1.55;margin-top:3px;}
.admin-foot{font-size:12px;margin-top:18px;}

/* Column counts are EXPLICIT, never auto-fit. auto-fit lays down however many
   120px tracks happen to fit, which for the nine job tiles was eight -- so
   'flagged' sat alone on a second row beside an empty stretch of grid and the
   strip read as broken rather than as a strip. Every count below either
   divides its tile count or the last tile spans the remainder, so a row is
   never part-full at any width. */
.tiles{display:grid;gap:1px;background:var(--line);
  border:1px solid var(--line);border-radius:var(--radius);overflow:hidden;margin:22px 0;}
.tiles-jobs{grid-template-columns:repeat(9,1fr);}
.tiles-traffic{grid-template-columns:repeat(6,1fr);}
@media (max-width:1200px){
  .tiles-jobs{grid-template-columns:repeat(5,1fr);}
  .tiles-jobs .tile:last-child{grid-column:span 2;}   /* 9 into 5 + 4 */
}
@media (max-width:780px){
  .tiles-jobs{grid-template-columns:repeat(3,1fr);}
  .tiles-jobs .tile:last-child{grid-column:auto;}     /* 9 into 3 + 3 + 3 */
  .tiles-traffic{grid-template-columns:repeat(3,1fr);}
}
@media (max-width:520px){
  .tiles-jobs{grid-template-columns:repeat(2,1fr);}
  .tiles-jobs .tile:last-child{grid-column:span 2;}
  .tiles-traffic{grid-template-columns:repeat(2,1fr);}
}
.tile{background:var(--ink-2);padding:14px 16px;}
.tile-n{font-size:22px;font-weight:700;color:var(--paper);line-height:1.1;}
.tile-l{font-size:11px;color:var(--mist);text-transform:uppercase;letter-spacing:.06em;margin-top:4px;}

.admin-controls{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-bottom:14px;}
.admin-controls input[type=search]{background:var(--ink-2);color:var(--paper);border:1px solid var(--line);
  border-radius:7px;padding:8px 12px;font:inherit;font-size:13px;min-width:280px;}
/* width:auto undoes the global `select{width:100%}`, which put each control on a
   line of its own: three filters and a count stacked four deep above a table
   they take up less room than. */
.admin-controls select{width:auto;min-width:150px;background:var(--ink-2);color:var(--paper);
  border:1px solid var(--line);border-radius:7px;padding:8px 10px;font:inherit;font-size:13px;}
.admin-controls input[type=search]:focus,.admin-controls select:focus{outline:none;border-color:var(--accent);}

/* The table is the reason body.admin-page is wider; on a narrow viewport it
   scrolls inside this wrapper rather than making the whole page scroll. */
.admin-table-wrap{overflow-x:auto;border:1px solid var(--line);border-radius:var(--radius);}
.admin-table{width:100%;border-collapse:collapse;font-size:13px;}
.admin-table th,.admin-table td{padding:.5rem .7rem;text-align:left;border-bottom:1px solid var(--line);vertical-align:top;}
.admin-table thead th{background:var(--ink-2);color:var(--mist);font-size:11px;text-transform:uppercase;
  letter-spacing:.06em;font-weight:600;position:sticky;top:0;z-index:2;}
.admin-table tbody tr:hover{background:rgba(38,166,154,.06);}
.admin-table tbody tr:last-child td{border-bottom:none;}
.admin-table th.sortable{cursor:pointer;user-select:none;}
.admin-table th.sortable:hover{color:var(--paper);}
.admin-table th.sorted-asc::after{content:' \2191';color:var(--accent);}
.admin-table th.sorted-desc::after{content:' \2193';color:var(--accent);}
.admin-table td.nowrap{white-space:nowrap;}
.admin-table td.num{text-align:right;}
.admin-table td.track{max-width:360px;overflow-wrap:anywhere;color:var(--paper);}
.admin-table tr.row-archived td{opacity:.55;}
.admin-table .sub{display:block;font-size:11px;color:var(--mist);}
.admin-table .approx{color:var(--mist);}
.admin-table .edited{color:var(--accent);}
/* A report is the one thing on this table you are meant to act on, so it gets a
   colour of its own. Muted red rather than --redact at full strength: the tint
   on the row is already doing the work of catching the eye. */
/* Plain coloured text, exactly like .edited beside it. A tint across every cell
   of the row was far too loud for a table you scan: it made a handful of reports
   the first thing your eye hit on every screenful, and the Flag column already
   says which rows they are. Amber rather than --redact, because a report is a
   thing to look at, not a job that failed -- the Status column owns red. */
.admin-table .flagged{color:#c98f7a;}
/* Same amber as the review page's notice, for the same statement: this
   transcript is probably not what was sung. Muted grey above the cut, because a
   normal score is context, not news. */
.admin-table .lang-low{color:#d3a04b;}
.admin-check{display:inline-flex;align-items:center;gap:6px;margin:0;color:var(--mist);font-size:13px;
  font-weight:400;cursor:pointer;white-space:nowrap;}
.admin-check input{margin:0;}
/* Same size and colour as the checkbox label beside it: at the inherited 16px it
   sat a step larger than everything else on the row and read as a heading. */
.admin-count{font-size:13px;white-space:nowrap;}

.env-tag{display:inline-block;font-size:11px;font-weight:600;padding:1px 7px;border-radius:20px;
  border:1px solid var(--line);color:var(--mist);}
.env-prod{color:var(--accent);border-color:var(--accent-deep);}

.st{display:inline-block;font-size:12px;font-weight:600;}
.st-complete{color:var(--accent);}
.st-failed{color:var(--redact);}
.st-processing{color:#d3a04b;}
.st-queued{color:var(--mist);}

/* Summary strip: window picker + the two tile rows it drives. */
.admin-page h2{font-size:15px;text-transform:uppercase;letter-spacing:.07em;color:var(--mist);
  margin:26px 0 10px;font-weight:600;}
.summary-head{display:flex;align-items:baseline;justify-content:space-between;gap:16px;flex-wrap:wrap;}
.summary-head h2{margin-bottom:0;}
.win-picker{display:flex;gap:0;border:1px solid var(--line);border-radius:7px;overflow:hidden;}
.win-btn{font:inherit;font-size:12px;font-weight:600;background:var(--ink-2);color:var(--mist);
  border:none;border-right:1px solid var(--line);padding:7px 14px;cursor:pointer;}
.win-btn:last-child{border-right:none;}
.win-btn:hover{color:var(--paper);}
.win-btn.win-on{background:var(--accent-deep);color:#fff;}
.tiles-traffic{margin-top:-14px;border-top:none;border-top-left-radius:0;border-top-right-radius:0;}

/* --- Where the traffic came from (static/admin-map.js + static/world.svg) --- */
.geo{display:flex;gap:18px;align-items:flex-start;margin:18px 0 6px;}
.geo-map{flex:1 1 auto;min-width:0;}
/* The asset declares its own aspect ratio in the viewBox; height:auto keeps it,
   and min-width:0 above is what stops a flex item from refusing to shrink below
   its content and pushing the list off the row. */
.geo-map svg{width:100%;height:auto;display:block;}
.geo-side{flex:0 0 190px;display:flex;flex-direction:column;gap:12px;}

/* Land with no traffic. Above the page background rather than below it: an
   unvisited country is still a country, not a hole in the map. */
.wm-c{fill:#2f2f2f;stroke:var(--ink);stroke-width:1;vector-effect:non-scaling-stroke;}
.wm-c[data-uniq]:hover,.wm-dot:hover{stroke:var(--paper);stroke-width:1.5;}
.wm-dot{fill:#2f2f2f;stroke:var(--ink);stroke-width:1;vector-effect:non-scaling-stroke;}

/* One hue, five steps, dark to bright: this is a quantity of the SAME thing, so
   it gets a sequential ramp rather than the categorical colours used elsewhere on
   the site. Teal because that is the site's accent, and because red is spoken for
   -- on every other page it means censored.

   Each step sets BOTH fill and background on purpose. The same class paints a
   country (an SVG path, which has no background) and its legend swatch (a span,
   which has no fill), so one declaration covers both and the legend cannot come
   to disagree with the map it is explaining. Written the obvious way -- a fill
   ramp plus a background ramp -- the swatches shipped invisible, because `fill`
   on a <span> is silently nothing. */
.wm-q1{fill:#0e3b36;background:#0e3b36;}
.wm-q2{fill:#16564e;background:#16564e;}
.wm-q3{fill:#1f776b;background:#1f776b;}
.wm-q4{fill:#26a69a;background:#26a69a;}
.wm-q5{fill:#5fd3c4;background:#5fd3c4;}

.geo-top{list-style:none;margin:0;padding:0;font-size:12px;}
.geo-top li{display:flex;justify-content:space-between;gap:10px;padding:3px 0;
  border-bottom:1px solid var(--line);}
.geo-top li:last-child{border-bottom:none;}
.geo-name{color:var(--body);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.geo-n{color:var(--paper);font-weight:600;font-variant-numeric:tabular-nums;}
.geo-rest{color:var(--mist);justify-content:flex-start;}

.geo-legend{display:flex;align-items:center;gap:3px;flex-wrap:wrap;font-size:11px;color:var(--mist);}
.geo-sw{width:16px;height:10px;border-radius:2px;}
.geo-lg-end{font-variant-numeric:tabular-nums;}
.geo-lg-cap{flex-basis:100%;letter-spacing:.05em;text-transform:uppercase;}
.geo-note{margin-top:0;}

/* position:fixed and appended to <body>: it takes no space in the layout, so
   nothing reflows as it appears, and no ancestor's overflow can clip it against
   the edge of the map. Same idiom as .word-tip on the results page. */
.geo-tip{position:fixed;z-index:60;pointer-events:none;background:var(--ink-2);
  color:var(--paper);border:1px solid var(--line);border-radius:6px;
  padding:5px 9px;font-size:12px;white-space:nowrap;
  box-shadow:0 6px 18px rgba(0,0,0,.45);}
.geo-tip[hidden]{display:none;}

@media (max-width:780px){
  /* align-items:stretch, not the flex-start the row uses: stacked, the cross axis
     is the WIDTH, and flex-start there shrinks the map to its content and leaves
     it stranded against the left edge of a full-width page. */
  .geo{flex-direction:column;align-items:stretch;}
  .geo-side{flex:1 1 auto;width:100%;}
}
.tile-bad .tile-n{color:var(--redact);}


/* ---- v11: download progress on the button ---------------------------------
   The obvious home for this, a bar in .download-controls, is wrong here: that
   block lives in .sticky-head, whose height is MEASURED into --sticky-h by a
   ResizeObserver in results-preview.js. Anything that appears and disappears
   there resizes the pinned head and shunts the whole transcript up and down on
   every render -- the same problem .seek-bar solves by never hiding itself.

   Absolutely positioned inside the button, the fill contributes no layout
   height in any state, so there is nothing to reserve and --sticky-h never
   moves. It also puts the feedback on the control that was actually clicked. */
.dl-split .finalize-submit{position:relative;overflow:hidden;}
.dl-prog{position:absolute;left:0;right:0;bottom:0;height:3px;display:flex;gap:2px;}
/* A faint white wash rather than a grey fill, so an empty track is invisible
   against the button and idle needs no separate hidden state. */
.dl-pseg{height:100%;background:rgba(255,255,255,.09);overflow:hidden;}
/* Weights, not promises -- see the constants in results-review.js. */
/* Widths are the measured phase durations, same convention as the waiting
   page's .tseg flex-grow: the queue segment owns most of the bar because that is
   where most of the wait actually is (~80% of it). */
.dl-pseg-queue{flex:27;}
.dl-pseg-render{flex:8;}
/* 1s linear matches the tick, so the fill creeps rather than stepping. Exactly
   what .tfill does on the waiting page. */
.dl-pfill{display:block;height:100%;width:0;background:var(--accent);transition:width 1s linear;}
.dl-prog.dl-bad .dl-pfill{background:var(--redact);}

/* There was no disabled rule for this button at all, so it kept full teal and
   cursor:pointer for the whole render -- visually identical to idle, which is
   most of why a download read as a hung page. Compound selectors for the reason
   at app.css:354: style.css sets a global button{background:#004d40} that
   otherwise wins. */
.dl-split .finalize-submit:disabled{background:#04352e;color:var(--mist);cursor:default;}
.dl-split .finalize-submit:disabled:hover{background:#04352e;}
.dl-split .finalize-submit:disabled .dl-icon{opacity:.6;}
.dl-format:disabled{background:#04352e;color:var(--mist);cursor:default;}
.dl-format:disabled:hover{background:#04352e;}


/* ---- v11: per-line confidence meter ---------------------------------------
   Sits in the time column rather than on the row's leading edge, because
   tr.pv-active already owns that edge (inset box-shadow on the play cell)
   while a line previews. .seg-meta is also the right home on meaning: it is
   already the line's metadata column and already owns the metrics hover, so
   this is a visual summary of numbers the tooltip prints anyway.

   One amber ramp and no new hue. Teal is playback, red is censoring, and amber
   already means "the transcript is unreliable here" -- which is what lets the
   untranscribed warning become this scale's empty state rather than a second
   vocabulary sitting beside it. */
/* Its own column, so the header can name it -- unexplained bars beside a
   timestamp are decoration. Narrow and nowrap: this repeats on every row and
   must not compete with the words for width. */
/* width:1% + nowrap is the shrink-to-content idiom for a table that is
   width:100%: the browser gives each column its min-content width and hands the
   slack to the one column with no width set, which is Line -- the column whose
   whole job is holding long lyric lines.

   Both of these used fixed widths (140px from style.css:93 for the time, 60px
   here) and the meter ended up marooned: the timestamp is ~78px, so 140 left
   ~60px of dead space before the bars, and the 18px meter sat in a 60px cell.
   The header word "Quality" is now what sets this column's width, not a guess. */
.transcript-content th.time-column,
.transcript-content td.seg-meta{width:1%;}
.transcript-content th.quality-column,
.transcript-content td.seg-quality{width:1%;white-space:nowrap;}
.transcript-content td.seg-quality{cursor:help;}
/* 9px each side -- half the meter's own 18px width -- so the bars sit in even
   gutters between the timestamp and the first word of the line rather than
   hugging one neighbour. With the header hidden (sr-only) the column is exactly
   meter + those two gaps, so none of its width is slack. */
.transcript-content th.quality-column,
.transcript-content td.seg-quality{padding-left:9px;padding-right:9px;}
.qmtr{display:inline-flex;gap:2px;vertical-align:-1px;}
.qmtr i{display:block;width:3px;height:11px;border-radius:1px;background:#3a3a3a;}
/* Green for the two trustworthy tiers. Grey filled bars sat too close to the
   #3a3a3a empty track to read as filled at all, so a good line looked like an
   EMPTY meter -- the opposite of the intended message. Green is a fourth hue on
   this page (teal is playback, red censoring, amber unreliable) and it earns its
   place: nothing else says "fine, move on". Deliberately yellow-green rather
   than anything near --accent, so it cannot be misread as the playback
   highlight. Colour stays redundant with bar COUNT throughout, which is what
   keeps the ramp readable without colour vision.

   THE RAMP IS MONOTONIC IN HUE and that is the whole reason these two greens
   are this way round (corrected 2026-08-21). They were swapped: 4 bars took the
   lighter yellow-green and 3 bars the deeper one, so reading down the scale the
   hue went yellow-green -> green -> yellow -> orange. Four bars sat visibly
   CLOSER to the two-bar yellow than three bars did, which made the best score
   look like a warning and the gradient look broken. Deepest green is the best
   score; every step down moves toward the amber end and none moves back. */
.qmtr.q-clean i:nth-child(-n+4){background:#689f38;}
.qmtr.q-fair  i:nth-child(-n+3){background:#8bc34a;}
.qmtr.q-shaky i:nth-child(-n+2){background:#ffcc00;}
.qmtr.q-poor  i:nth-child(-n+1){background:#ff8f3f;}
/* q-none paints nothing: a row whose words carry no confidence at all, which
   today means a job analysed before they were recorded. An absent measurement
   must not read as a low score.

   q-silent is DIFFERENT and the distinction is the point: gap recovery ran over
   that span and came back with nothing, which is a finding rather than a gap in
   the data. Hollow amber outlines -- amber because that is already this page's
   colour for "unreliable here", hollow because there is no confidence to fill
   them with. This is what replaced the dashed amber warning box: the meaning
   moved into the meter, so the line itself could stop shouting. */
.qmtr.q-silent i{background:none;box-shadow:inset 0 0 0 1px #8a6d0b;}
/* The re-download link in the settled status line. Inherits accent colour from
   the base `a` rule; this only stops it inheriting .finalize-status's --mist. */
.finalize-status .dl-again{color:var(--accent);}
.finalize-status .dl-again:hover{text-decoration:underline;}

/* Help dialog, grouped. It is behind a button and read at most once or twice,
   so length costs nothing -- but an undifferentiated list of a dozen bullets
   is unscannable, hence the section labels. The dialog already scrolls
   (max-height:80vh; overflow-y:auto). */
.help-dialog .help-intro{margin:14px 0 0;font-size:14px;color:var(--body);}
.help-dialog .help-group{
  margin:18px 0 0;font-size:11px;font-weight:600;color:var(--accent);
  text-transform:uppercase;letter-spacing:.08em;
}
.help-dialog .help-group + ul{margin-top:7px;}
/* The meters and dimmed words shown inline as examples must not inherit the
   list's own colour, or the thing being described stops being visible. */
.help-dialog .qmtr{vertical-align:-1px;margin:0 2px;}


/* ---------- hand-made mute ranges (static/results-manual.js) ---------- */

/* The create control, on the TRANSPORT rather than in the row above, sitting
   immediately after the playhead clock because that is the number it starts
   from. It used to live in the control row and print " at 3:08.0" onto its own
   label, restating the clock 40px below it -- the coupling is spatial now, and
   said once.

   Round, dashed and red: dashed because it makes something that is not there
   yet, red because what it makes is a mute. Sized to .seek-play so the two ends
   of the bar's left cluster match. */
.add-range-btn{
  width:26px;height:26px;flex:0 0 26px;padding:0;
  border:1px dashed rgba(211,47,47,.55);border-radius:50%;
  background:transparent;color:#d32f2f;
  font-family:inherit;font-size:15px;line-height:1;cursor:pointer;
  display:grid;place-items:center;
}
.add-range-btn:hover{background:rgba(211,47,47,.15);border-style:solid;}

/* ---------- "do the other instances too" (static/results-bulk.js) ---------- */

/* position:fixed on document.body, the same idiom .word-tip uses above. That is
   the whole design: an inline chip after the word is in normal flow, so it
   shoves the rest of the line sideways and re-wraps it -- twice per decision,
   since it appears on censor and vanishes on un-censor. This has no width in the
   layout at all, and no ancestor's overflow can clip it.

   Opens BELOW the word; .word-tip prefers above, so the two cannot collide. */
.bulk-pop{
  position:fixed;z-index:9998;display:none;align-items:center;gap:6px;
  background:#0f1720;border:1px solid var(--line);border-radius:8px;
  padding:6px 8px;box-shadow:0 6px 20px rgba(0,0,0,.5);
  font-size:12.5px;white-space:nowrap;
}
.bulk-pop.open{display:flex;}
.bulk-nub{
  position:absolute;width:8px;height:8px;background:#0f1720;
  border-left:1px solid var(--line);border-top:1px solid var(--line);
  transform:rotate(45deg);
}
/* Pointing down instead of up when the panel had to flip above the word. */
.bulk-nub:not(.below){transform:rotate(225deg);}
.bulk-go{
  border:1px solid rgba(38,166,154,.55);border-radius:5px;
  background:rgba(38,166,154,.16);color:#4fd3c4;
  font-family:inherit;font-size:12px;font-weight:600;line-height:1.5;
  padding:3px 10px;cursor:pointer;
}
.bulk-go:hover{background:rgba(38,166,154,.3);}
.bulk-no{
  border:none;background:transparent;color:var(--mist);
  font-family:inherit;font-size:15px;line-height:1;cursor:pointer;padding:0 3px;
}
.bulk-no:hover{color:var(--paper);}

/* The row itself. Tinted so it reads as the reviewer's own annotation rather
   than as something the model heard -- every other row in this table is a
   reading of the audio, and this one is not. .lit is hover or focus, paired
   with the matching tick on the seek bar. */
.transcript-content tr.range-row td{background:rgba(211,47,47,.06);}
.transcript-content tr.range-row.lit td{background:rgba(211,47,47,.12);}

/* Time column. .range-meta rather than .seg-meta deliberately: that class is
   the metrics-tooltip target and carries a data-seg blob, and a hand-made range
   has no whisper metrics behind it. Same width behaviour as .time-column so the
   column does not jump when a range is added. */
.range-meta{
  width:1%;white-space:nowrap;color:var(--mist);font-size:13px;
  font-variant-numeric:tabular-nums;
}

.range-cell{display:flex;align-items:center;flex-wrap:wrap;gap:12px;}

/* The element the preview and the renderer both key on. Red like
   .word-toggle.censored, because it is the same thing happening to the audio --
   but NO line-through: there is no word here being struck out, and the label
   names a control rather than showing a deleted reading. Not a click target
   either; .range-del is the only way to remove one, so this must not look like
   a button. */
.word-toggle.mute-range{
  color:#d32f2f;text-decoration:none;cursor:default;
  border:1px solid rgba(211,47,47,.45);border-radius:4px;
  padding:1px 7px;font-size:12px;font-weight:600;white-space:nowrap;
}

/* Just a label and a field. The nudge and capture buttons that used to sit here
   went on 2026-08-29: you read the playhead clock on the bar and type what it
   says, which is fewer controls and the same answer. */
.range-edge{display:flex;align-items:center;gap:5px;}
.range-lbl{color:var(--mist);font-size:11px;}

/* Compound selectors and every property set explicitly, the same problem
   the .seek-mode rule above documents for itself. Two rules would otherwise reach these: sakura's bare
       textarea, select, input { padding:6px 10px; margin-bottom:10px;
                                 background:#f1f1f1; color:#4a4a4a; ... }
   which paints a light-grey field on a dark page and adds 10px of layout height
   per input inside a pinned-head table, and this file's own
       input[type=text],...,select { width:100%; ... }
   -- specificity (0,1,1) -- which would stretch each one across the row. These
   are (0,2,0) and (0,2,1). Dropping any property lets one of them back in. */
.range-cell .range-edge input{
  width:72px;
  background:var(--ink);color:var(--body);
  border:1px solid var(--line);border-radius:4px;
  padding:2px 6px;margin:0;
  font-family:ui-monospace,Menlo,Consolas,"Liberation Mono",monospace;
  font-size:12px;line-height:1.5;text-align:center;
  font-variant-numeric:tabular-nums;
}
.range-cell .range-edge input:focus{border-color:var(--accent);outline:0;}
/* A typed time that would invert the range is rejected and reverts. The field
   says so for a moment first, or the value appears to change itself back for no
   reason. */
.range-cell .range-edge input.bad{border-color:#d32f2f;color:#d32f2f;}

/* Deliberately NOT .seg-play-btn -- see the comment in results-manual.js's
   addRow. That class is what rowIndex() scans for, and a range in the follow
   index would steal the transcript highlight from the line it sits inside. */
.range-play{
  width:22px;height:22px;border-radius:50%;padding:0;
  border:1px solid rgba(211,47,47,.5);background:transparent;color:#d32f2f;
  cursor:pointer;display:grid;place-items:center;
}
.range-play:hover{background:rgba(211,47,47,.15);}
.range-play svg{width:9px;height:9px;fill:currentColor;}
/* While this range is what is playing, this control IS the stop button -- the
   same contract .seg-play-btn has. A square drawn in CSS rather than a second
   svg swapped in by script: the state is set by results-preview.js, which has no
   business knowing what glyph this button draws. */
.range-play.playing{background:rgba(211,47,47,.2);border-style:solid;}
.range-play.playing svg{display:none;}
.range-play.playing::after{
  content:'';width:8px;height:8px;background:currentColor;border-radius:1px;
}

.range-del{
  border:1px solid var(--line);border-radius:4px;
  background:transparent;color:var(--mist);
  font-family:inherit;font-size:11px;line-height:1;padding:4px 9px;
  cursor:pointer;margin-left:auto;
}
.range-del:hover{border-color:#d32f2f;color:#d32f2f;}

/* The tick standing for the row under the cursor. Four red marks on a
   four-minute track are otherwise indistinguishable, and a range is the one
   kind of edit with no word in the transcript to point at instead. */
.seek-mutes i.lit{background:#ff6b6b;box-shadow:0 0 0 2px rgba(255,107,107,.3);}

/* Under 640px the controls stack under the marker rather than squeezing the
   Line column into a ribbon. .range-cell already wraps; this only trims the
   fields so two fit on one line. */
@media (max-width:640px){
  .range-cell{gap:9px;}
  .range-cell .range-edge input{width:66px;}
  .range-del{margin-left:0;}
}


/* ---- Guided walkthrough (static/results-tour.js) --------------------------
   Every step is one data-tour-* bullet in #help-dialog, so there is no second
   copy of the copy anywhere. This file only has to dress the two floating
   pieces: the spotlight and the popover. */

/* Both head controls in one flex child -- .page-head is space-between, so a
   third top-level child would push them to opposite ends of the row. */
.page-head-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap;}

/* The primary of the two doors: this one does something, the other is the
   reference you fall back to. One tonal step, not a different shape -- they
   are the same kind of control and should still read as a pair. */
.tour-open-btn{border-color:rgba(38,166,154,.5);color:var(--accent);}
.tour-open-btn:hover{border-color:var(--accent);background:rgba(38,166,154,.12);}

/* The spotlight is ONE fixed div whose enormous spread shadow dims everything
   OUTSIDE it, so the anchor stays lit exactly where it already sits and no page
   element needs position:relative or a z-index of its own. That matters here
   more than usual: this page balances two sticky layers (.sticky-head at
   z-index 5, thead th at z-index 4 offset by a MEASURED --sticky-h), and
   touching either shunts the whole transcript under the reader.

   pointer-events:none, so a click on the spotlighted control still reaches the
   control -- which is the point of lighting it in place rather than drawing a
   picture of it. Same fixed-on-body idiom as .word-tip and .bulk-pop, one layer
   above both (9999 and 9998), because a tour is a mode and nothing else on the
   page should paint over it.
   The geometry is NOT transitioned, and that is a correction rather than a
   preference. Animating left/top/width/height meant the ring's position was
   only right once an animation had finished, and it does not always finish:
   observed frozen a third of the way in, so the spotlight sat in the wrong
   place indefinitely -- a 12px box near the top-left corner while the popover
   pointed at a word 450px down. Every repaint on scroll also restarted the
   interpolation against a target that was itself moving. A spotlight is a
   discrete answer to "which control", so it should be correct in the frame it
   is painted; only the fade in is animated, and that cannot misplace anything. */
.tour-ring{
  position:fixed;z-index:10000;pointer-events:none;
  border-radius:8px;border:2px solid var(--accent);
  box-shadow:0 0 0 9999px rgba(0,0,0,.62);
  animation:tour-ring-in .18s ease-out;
}
@keyframes tour-ring-in{from{opacity:0;}to{opacity:1;}}
/* A card step -- autosave has no control, no toast and no indicator, so there
   is genuinely nothing to point at. Collapsed to nothing in the corner rather
   than display:none, because the DIM IS THE SHADOW: hiding the ring would take
   the dimming with it and leave the card floating over a lit page. */
.tour-ring-off{
  left:-20px;top:-20px;width:0;height:0;border:none;border-radius:0;
  box-shadow:0 0 0 9999px rgba(0,0,0,.72);
}
body:not(.tour-on) .tour-ring,
body:not(.tour-on) .tour-pop{display:none;}

.tour-pop{
  position:fixed;z-index:10001;
  width:330px;max-width:calc(100vw - 16px);
  background:var(--ink-2);color:var(--body);
  border:1px solid var(--line);border-radius:var(--radius);
  padding:13px 15px 11px;
  box-shadow:0 10px 30px rgba(0,0,0,.55);
}
.tour-pop:focus{outline:none;}
.tour-pop:focus-visible{outline:2px solid var(--accent);outline-offset:2px;}

/* Points back at the control, the same construction .bulk-nub uses. */
.tour-nub{
  position:absolute;width:9px;height:9px;background:var(--ink-2);
  border-left:1px solid var(--line);border-top:1px solid var(--line);
  transform:rotate(45deg);
}
.tour-nub:not(.below){transform:rotate(225deg);}

.tour-pop-head{display:flex;align-items:center;gap:8px;}
.tour-pop-title{margin:0;font-size:15px;color:var(--paper);flex:1;}
.tour-new{
  flex:none;font-size:10px;font-weight:600;letter-spacing:.06em;
  text-transform:uppercase;color:#4fd3c4;
  border:1px solid rgba(38,166,154,.55);background:rgba(38,166,154,.16);
  border-radius:4px;padding:1px 6px;line-height:1.6;
}

/* Every property set explicitly. style.css:99 puts padding:1.5rem and
   font-size:1.8rem on every bare <button>, so anything left to inherit in here
   renders enormous -- the same reason the rest of this file uses compound
   selectors and spells out what it wants. */
.tour-close{
  flex:none;border:none;background:none;color:var(--mist);
  font-family:inherit;font-size:22px;line-height:1;padding:0 2px;cursor:pointer;
}
.tour-close:hover{color:var(--paper);}

.tour-pop-body{font-size:13.5px;line-height:1.55;margin:7px 0 0;}
.tour-pop-body b{color:var(--paper);}
.tour-pop-body s{color:var(--redact);}
/* The bullet's own inline samples travel with it out of the dialog, so they
   have to read the same in here. The dialog's rules are scoped to .help-dialog
   and do not reach this far. */
.tour-pop-body .help-lo-conf{color:#a0a0a0;}
.tour-pop-body .help-near-miss{border-bottom:1px dotted #ffcc00;}
.tour-pop-body .qmtr{vertical-align:-1px;margin:0 2px;}
.tour-pop-body .muted{color:var(--mist);}

.tour-pop-foot{display:flex;align-items:center;gap:8px;margin-top:12px;}
.tour-count{flex:1;font-size:12px;color:var(--mist);font-variant-numeric:tabular-nums;}
.tour-btn{
  border:1px solid var(--line);border-radius:6px;background:var(--ink);
  color:var(--body);font-family:inherit;font-size:12.5px;font-weight:600;
  line-height:1.5;padding:4px 12px;cursor:pointer;
}
.tour-btn:hover:not(:disabled){border-color:var(--accent);color:var(--accent);}
.tour-btn:disabled{color:#4d4d4d;border-color:#2b2b2b;cursor:default;}
.tour-next{
  border-color:rgba(38,166,154,.55);background:rgba(38,166,154,.16);color:#4fd3c4;
}
.tour-next:hover:not(:disabled){background:rgba(38,166,154,.3);color:#4fd3c4;}

@media (max-width:640px){
  .tour-pop{width:calc(100vw - 24px);}
}

/* Replaces the permalink box on /demo, which is the one block of that page that
   would otherwise be actively wrong: it says "save this link to return to this
   job" over a link to an example. Same recessed teal treatment as the box it
   stands in for, so the page's shape does not change. */
.demo-banner{
  border:1px solid var(--accent-deep);border-radius:5px;
  padding:.8rem 1.2rem;margin:1rem 0;
  background-color:rgba(0,77,64,.15);font-size:14px;
}
.demo-banner b{color:var(--paper);}

/* html{scroll-behavior:smooth} at the top of this file had no reduced-motion
   guard. It did not matter much while nothing scrolled the page by script; the
   tour does, so the preference has to be honoured somewhere, and honouring it
   once here covers every anchor jump on the site rather than only this one. */
@media (prefers-reduced-motion: reduce){
  html{scroll-behavior:auto;}
  .tour-ring{animation:none;}
}
