/* The layout is fully absolute-positioned, so <body> has no height and paints
   nothing — the transcript card's rounded corners reveal the <html> canvas.
   Under an OS dark theme DaisyUI sets :root --b1 (and color-scheme) dark, so
   that canvas goes near-black. data-theme="light" on <html> forces the light
   theme; !important pins the canvas to the chrome gray over DaisyUI's
   higher-specificity :root rule. */
html {
  background-color: #f5f5f5 !important;
  color-scheme: light;
}

/* ONE source of truth for where the transcript card starts (#580). The card
   and the buttons pinned to its corners each carried their own constant per
   breakpoint — three apiece — which is how they drift: at wide widths the
   buttons sat 10px inside the card, in the compact band 18px, and any layout
   change could strand them over the grey entirely. Everything derives from
   this now, so they cannot disagree. */
body {
  --card-top: 78px;      /* 8px of canvas below the navbar so the card floats */
  /* how far the TEXT column sits inside the card. It differs by band because
     the mechanism differs — absolutely positioned at 10px here, flow plus the
     holder's padding in the compact band, padding alone on mobile — so each
     band states it once and the corner buttons derive from it. */
  --text-inset: 10px;
}

/* body[data-theme] (0,1,1) outguns DaisyUI's data-theme background (which
   paints the body white and loads after this file) without needing !important —
   the 8px gaps around the transcript card (#375) expose the body, so it must
   be the chrome grey for the card corners to show. */
body,
body[data-theme="light"] {
  font-family: system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Fira Sans','Droid Sans','Helvetica Neue',sans-serif;
  line-height: 1.8;
  background-color: #f5f5f5;
  /* #354: pin the app shell to the viewport. The layout is fully absolute-
     positioned, but the absolute #hypertranscript bled ~18px of overflow into
     the document, letting the whole page (and the fixed top bar) scroll — and
     a restored scroll position could stick it off-screen. A fixed body of the
     exact viewport size removes any document scroll while keeping every
     absolute panel in place (a fixed body is still their containing block). */
  position: fixed;
  inset: 0;
  overflow: hidden;
  overscroll-behavior: none;
}

input, output, select, button, textarea {
  display: inline-block;
  padding: 8px;
  margin-bottom:8px;
  margin-right:4px;
  font-size: 12pt;
  border-radius: 3px;
  border: 1px solid #ddd;
}

video {
  border-radius: 3px;
}

button, input[type="submit"], input[type="file"] {
  border: 1px solid #ccc;
  background-color: #ddd;
}

button:hover, input[type="submit"]:hover, input[type="file"]:hover {
  border: 1px solid #999;
  background-color: #ccc;
  cursor: pointer;
}

input[type="number"] {
  width: 42px;
}

/* DaisyUI makes navbar-start and navbar-end each 50% wide, which together leave
   the center (search) no room so it overlaps the toolbar buttons. Make the side
   sections flexible and let the center + search shrink. The .navbar prefix beats
   DaisyUI's single-class specificity (it loads after this file). */
.navbar .navbar-start,
.navbar .navbar-end {
  width: auto;
  flex: 0 0 auto;        /* size to the buttons; never overflow into the center */
}

.navbar .navbar-center {
  flex: 1 1 auto;        /* take the space between the sides, shrink when tight */
  min-width: 0;
}

.navbar-center .form-control {
  width: 100%;
  min-width: 0;
  align-items: center;   /* keep the search centered in the available space */
}

#search-box {
  width: 100%;
  max-width: 230px;
  min-width: 0;
  margin-right: 4px;
}

/* No room to type, so no search (#592). responsive.js measures what the
   navbar has left after its two fixed sides and sets this; the box would
   otherwise sit at its 48px floor with ~2px of text room, visible and
   focusable and useless. The 580px rule below is the no-JS floor. */
body.search-cramped .navbar-center {
  display: none;
}
/* ...and the right-hand group still has to sit on the right. .navbar-center
   was the only flex: 1 1 auto item, so removing it left both fixed sides
   bunched at the left with the free space trailing behind Save/Export/New.
   The auto margin takes over the growing the hidden item used to do. */
body.search-cramped .navbar-end {
  margin-left: auto;
}

/* Fixed-width side panel so its grey background fills to the transcript edge,
   with an equal 16px gutter on both sides (matches left:400px on
   .transcript-holder). Items fill the 368px content width, so the video and the
   Local Storage panel line up with identical left/right gutters. */
.side-panel {
  width: 400px;
  box-sizing: border-box;
  /* the video's top rides the navbar buttons' top (#617): the navbar sits
     4px down (its optical-centring margin, below) and its 48px buttons are
     centred in the 70px band, so they start at y=15 */
  padding-top: 15px;
  padding-left: 16px;
  padding-right: 16px;
  /* stop content 60px above the bottom so the Recents card's bottom lines up
     with the transcript card (play bar 52px + 8px float gap) */
  padding-bottom: 60px;
}

.top-bar-container {
  /*display: flex;*/
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
}

dialog::backdrop {
  backdrop-filter: blur(1px);
}

.vertically-centre {
  padding-top: 40%;
}

.hyperaudio-transcript {
  font-size: 13pt;
}

/* for accessibility */
.hidden-label-holder {
  height:0;
  opacity: 0;
}

/* former inline styles */
.transcript-holder {
  display: flex;
  justify-content: center;
  background-color: #ffffff;
  border-radius: 0.5rem;
  overflow-y:scroll;
  position: absolute;
  top: var(--card-top);
  left: 400px;
  right: 16px;
  bottom: 60px;          /* play bar (52px) + 8px gap — the corners read as a card */
  transition: left 0.5s ease;
}

/* The navbar's rung in the z-ladder is 40, inline on .main-panel (#539): it
   is a stacking context, so NOTHING inside it — the FILE dropdown above all —
   can out-stack a sibling with a higher z. At its old z of 2 the docked play
   bar (z 3) covered the open menu's lower entries on short viewports and
   swallowed their clicks. 40 slots the navbar deliberately: above the play
   bar (3) and corner floats (20-30), below the drawer (45) and the mobile
   band's 60. Ladder: tooltips 100, navbar 60 (mobile) / 40, drawer 45/44,
   corner floats 20-30, playbar 3. */

/* Docked play bar: play/pause + full-width seek, spanning the transcript area.
   Tracks the sidebar collapse the same way .transcript-holder does (left 400<->0). */
#playbar {
  position: absolute;
  left: 0;                 /* full screen width — a global transport bar */
  right: 0;
  bottom: 0;
  height: 52px;
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 0 16px;
  background-color: #f5f5f5;
  border-top: 1px solid #e5e7eb;
  z-index: 3;
}

#playbar-seek {
  flex: 1;
}

#playbar-volume {
  flex: 0 0 90px;
}

/* match the play button (btn-sm) height so the bar reads as one row */
#playbar-rate {
  height: 2rem;
  min-height: 2rem;
  flex: 0 0 auto;
}

#playbar-time {
  font-variant-numeric: tabular-nums;
  font-size: 80%;
  opacity: 0.7;
  white-space: nowrap;
}

/* Hover play/pause overlay on the media (#336). Covers the video/poster; the
   badge fades in on hover and while paused, and toggles play via togglePlay(). */
#media-play-overlay {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 0;
  padding: 0;
  border: 0;
  background: transparent;
  cursor: pointer;
  border-radius: 0.5rem;       /* match the video */
  opacity: 0;
  transition: opacity 0.15s ease;
}

#player-frame:hover #media-play-overlay,
#player-frame.is-paused #media-play-overlay,
#media-play-overlay:focus-visible {
  opacity: 1;
}

.media-play-overlay-badge {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 56px;
  height: 56px;
  border-radius: 9999px;
  background: rgba(0, 0, 0, 0.55);
  color: #fff;
}

.hyperaudio-transcript {
  position: absolute; 
  top: 10px; 
  bottom: 0px; 
  background-color: #ffffff; 
  padding-top: 48px; 
  width: 520px; 
  outline: none;
}

#file-dropdown-mobile, #file-dropdown-symbol-mobile, #transcribe-label-mobile {
  display: none;
}

@media screen and (max-width: 1120px) {
  /* the compact navbar (#480) is shorter, so the card starts higher — and the
     corner buttons follow it because they derive from this (#580) */
  body {
    /* 58px assumed a compact navbar that no longer exists — the navbar is 74px
       here, so that value put the card UNDER it (#580). The measured value
       from responsive.js is the authority; this only has to be right enough
       for the first paint, before the script runs, so it matches the wide
       band rather than flashing a card that then jumps down. */
    --card-top: 78px;
    --text-inset: 18px;  /* holder padding (8px) + the transcript's own offset */
  }

  #file-dropdown-symbol-mobile {
    display: block;
  }

  #file-dropdown-text, #file-dropdown-symbol {
    display: none;
  }

  .hyperaudio-transcript {
    position: relative;
    width: 520px;
  }

  .transcript-holder {
    /* keep flex-centering and let left:400/right:0 size the holder between the
       sidebar and the edge — width:100% here overflowed past the right edge.
       Clear the navbar (top) and play bar (bottom) to match the wide layout. */
    display: flex;
    justify-content: center;
    background-color: #ffffff;
    overflow-y:scroll;
    right: 16px;
    bottom: 60px;
    padding: 8px;
  }

  #transcribe-label {
    display: none;
  }

  #transcribe-label-mobile {
    display: block;
  }


}

/* The Recents drawer backdrop only exists on the small-screen layout; hide it
   by default (desktop). */
#drawer-backdrop { display: none; }

/* =========================================================================
   Small-screen layout (#349) — phones and tablet portrait.
   A single column: fixed navbar at the very top, the video pinned just below
   it (collapsible), the transcript scrolling full-width beneath, and the play
   bar docked at the bottom. Recents becomes an off-canvas drawer. This replaces
   the desktop absolute two-pane; !important is needed on a few rules to beat
   the inline .style.left the sidebar-toggle JS writes.
   ========================================================================= */
@media screen and (max-width: 948px) {
  body {
    --nav-h: 70px;       /* matches the navbar's inline min-height */
    --player-h: 210px;   /* video area + a 44px controls row */
    /* the card follows the pinned player as it collapses; the corner buttons
       ride it automatically, being derived from the same variable */
    --card-top: calc(var(--nav-h) + var(--player-h));
    --text-inset: 8px;   /* holder padding only: the transcript is in flow here */
  }
  body.video-collapsed {
    --player-h: 44px;    /* controls row only; video hidden (audio-only button) */
  }

  /* the side panel no longer lays anything out — its children (player + recents)
     are individually fixed below */
  .side-panel {
    position: static !important;
    width: auto !important;
    height: auto;
    padding: 0;
    background: none;
    overflow: visible;
  }

  /* navbar (the .main-panel) pinned to the very top */
  .main-panel {
    position: fixed !important;
    top: 0;
    left: 0 !important;
    right: 0;
    width: auto;
  }
  .main-panel .navbar {
    min-height: var(--nav-h);
    padding-top: 4px;
    padding-bottom: 4px;
  }

  /* pinned, collapsible player just below the navbar */
  #player-pane {
    position: fixed;
    top: var(--nav-h);
    left: 0;
    right: 0;
    height: var(--player-h);
    z-index: 15;
    background: #f5f5f5;
    padding: 4px 8px 0;
    box-sizing: border-box;
    box-shadow: 0 2px 6px rgba(0,0,0,0.08);
    transition: height 0.25s ease;
    overflow: visible;   /* let control tooltips show; the video is clipped by #player-frame */
  }
  #player-pane #player-frame {
    height: calc(var(--player-h) - 44px);   /* 0 when collapsed → video hidden */
    overflow: hidden;
    display: flex;
    align-items: center;
    justify-content: center;   /* centre the video; grey shows either side */
    transition: height 0.25s ease;
  }
  #player-pane #hyperplayer {
    height: 100%;
    width: auto !important;    /* size to the video's aspect, not the full frame */
    max-width: 100%;
    object-fit: contain;
    background: transparent;   /* let the grey player background show, not black */
    border-radius: 0.5rem;     /* rounded corners hug the video itself */
  }
  #player-pane #player-controls {
    height: 40px;
    margin-top: 4px;
    overflow-x: auto;
  }
  /* Keep the floating Regenerate button below the pinned player (its top-20
     utility class would sit it on the video); tracks the collapse var. */
  #regenerate-float-btn {
    top: calc(var(--nav-h) + var(--player-h) + 8px);
    right: 8px;
    transition: top 0.25s ease;
  }

  /* Paragraph timecodes live in the transcript's left margin, which the
     full-width mobile layout has no room for — hide the toggle and any nodes. */
  #player-controls label[for="show-timecodes"] { display: none !important; }
  .para-timecode { display: none; }

  /* transcript fills the space between the pinned player and the play bar */
  .transcript-holder {
    position: fixed !important;
    top: var(--card-top) !important;
    left: 0 !important;
    right: 0 !important;
    bottom: 52px !important;
    transition: top 0.25s ease;
  }
  .hyperaudio-transcript {
    position: relative;
    top: auto;
    bottom: auto;
    width: 100%;
    max-width: 100%;
    box-sizing: border-box;
    /* Top padding stays at the desktop 48px: the ⓘ and copy buttons are pinned
       to the card's top corners at holder-top + 10px and are 32px tall, so text
       starting any higher runs underneath them. The shorthand used to reset this
       to 8px, which put the first line under both icons at these widths. */
    padding: 48px 12px 48px;
  }

  #playbar {
    left: 0 !important;
    right: 0;
    gap: 8px;
    padding: 0 8px;
  }
  #playbar-seek { min-width: 0; }

  html { overflow-x: hidden; }
  /* let button tooltips drop below the bar over the video */
  .main-panel .navbar { overflow: visible; }

  /* Lift the navbar above the pinned player and transcript so its tooltips (which
     drop below the buttons, over the video) are not covered. Beats the inline
     z-index:40 on .main-panel (#539). */
  .main-panel { z-index: 60 !important; }

  /* Recents: off-canvas drawer, toggled by #sidebar-toggle (see responsive.js) */
  #recents-pane {
    position: fixed;
    top: var(--nav-h);
    bottom: 52px;
    left: 0;
    width: min(85vw, 320px);
    z-index: 45;
    background: #f5f5f5;
    padding: 8px;
    box-sizing: border-box;
    overflow-y: auto;
    transform: translateX(-100%);
    transition: transform 0.25s ease;
    box-shadow: 2px 0 10px rgba(0,0,0,0.25);
  }
  body.drawer-open #recents-pane { transform: none; }

  #drawer-backdrop {
    display: block;
    position: fixed;
    inset: 0;
    z-index: 44;
    background: rgba(0,0,0,0.4);
    opacity: 0;
    visibility: hidden;
    transition: opacity 0.25s ease;
  }
  body.drawer-open #drawer-backdrop { opacity: 1; visibility: visible; }

  /* keep the sidebar toggle available (it opens the drawer); it was hidden at
     ≤480 before */
  #sidebar-toggle { display: inline-flex; }

  /* fluid modals / overlays so they fit ~375px screens */
  /* !important beats the inline max-width:36rem on the transcribe modal so it
     fits the viewport (and its tab row can then wrap instead of overflowing). */
  .modal-box { max-width: calc(100vw - 24px) !important; }
  #captionsource-alert {
    width: calc(100vw - 24px) !important;
    max-width: 480px;
    left: 12px !important;
    right: 12px;
    margin: -70px auto 0 !important;
  }
  #captions-display { width: 100%; }
  /* The transcribe tabs are a fixed 150px each with nowrap labels, which
     overflowed the narrowed modal. Let them share the row and shrink evenly,
     with the label WRAPPING to two lines rather than collapsing into an
     overlapping garble. (Selectors are class/name-based — the ARIA tab roles
     were removed in #402, and the radio groups were renamed when the modal
     gained the Local/Cloud switch.) */
  .modal-box .tabs { flex-wrap: wrap; width: 100%; box-sizing: border-box; }
  .modal-box [name="local_tabs"],
  .modal-box [name="cloud_tabs"] {
    flex: 1 1 0;
    width: auto !important;
    min-width: 6rem;
    white-space: normal !important;
    height: auto;
    min-height: 3rem;
    line-height: 1.15;
    font-size: 0.8rem;
    padding: 4px 6px;
  }

  /* Hide the scrollbars on the mobile scroll surfaces (touch scrolling still
     works) — otherwise the transcript's always-on scrollbar shows as a grey
     sliver at the right edge. */
  .transcript-holder,
  #player-controls,
  #recents-pane { scrollbar-width: none; }
  .transcript-holder::-webkit-scrollbar,
  #player-controls::-webkit-scrollbar,
  #recents-pane::-webkit-scrollbar { width: 0; height: 0; display: none; }
}

/* =========================================================================
   Compact toolbar for the narrowest screens (#349). Between 948px and 580px the
   pinned/drawer layout is active but the navbar still shows the search box and
   the full toolbar. Below 580px there is no longer room, so drop the search box
   and FILE menu, shrink and right-align the buttons, and trim the play bar.
   ========================================================================= */
@media screen and (max-width: 580px) {
  /* navbar: drop search + FILE menu, compact and right-align the rest */
  .navbar-center { display: none; }
  .navbar-start .dropdown { display: none; }
  .main-panel .navbar {
    flex-wrap: nowrap;
    gap: 2px;
    padding-left: 4px;
    padding-right: 4px;
  }
  .navbar .navbar-start,
  .navbar .navbar-end { gap: 2px; }
  .navbar .btn {
    margin-right: 0 !important;
    height: 40px;
    min-height: 40px;
    padding-left: 8px;
    padding-right: 8px;
  }
  .navbar .btn-square { width: 40px; padding-left: 0; padding-right: 0; }
  /* the Recents/drawer toggle stays hard left; everything else is pushed right */
  .navbar .navbar-start { flex: 1 1 auto; min-width: 0; }
  .navbar-start #sidebar-toggle { margin-right: auto !important; }

  /* play bar: hardware volume/mute, slimmer rate select */
  #playbar-volume,
  #playbar-mute { display: none; }
  #playbar-rate {
    padding-inline-start: 8px;
    padding-inline-end: 20px;
    min-width: 0;
    font-size: 85%;
  }
}

@media screen and (max-width: 480px) {
  .caption-row input.line1, .caption-row input.line2 {
    width: 24ch;
  }

  .caption-row input.start, .caption-row input.end {
    width: 12ch;
  }
}

/* Minimal tooltip styling — mirrors DaisyUI .tooltip + data-tip so a future
   tailwind rebuild can take over without HTML changes. Positioned below
   since the toolbar is at the top of the page. Show is delayed 500ms so a
   quick mouse pass doesn't flash a tooltip; hide is instant. */
.tooltip {
  position: relative;
}
.tooltip::before {
  content: attr(data-tip);
  position: absolute;
  top: calc(100% + 6px);
  left: 50%;
  transform: translateX(-50%);
  padding: 4px 10px;
  background: oklch(0.27 0.02 264);
  color: #fff;
  border-radius: 6px;
  font-size: 12px;
  font-weight: 400;
  white-space: nowrap;
  pointer-events: none;
  z-index: 100;
  opacity: 0;
  transition: opacity 0s;
}
.tooltip::after {
  content: '';
  position: absolute;
  top: 100%;
  left: 50%;
  transform: translateX(-50%);
  border: 4px solid transparent;
  border-bottom-color: oklch(0.27 0.02 264);
  pointer-events: none;
  z-index: 100;
  opacity: 0;
  transition: opacity 0s;
}
.tooltip:hover::before,
.tooltip:hover::after {
  opacity: 1;
  transition: opacity 0s 0.5s;
}

/* Sidebar toggle sits at the left edge when the sidebar is collapsed, so
   left-anchor its tooltip bubble to avoid running off-screen. The arrow
   stays centred under the button. */
#sidebar-toggle.tooltip::before {
  left: 0;
  transform: none;
}

/* The player controls' first buttons sit at the sidebar's left edge too
   (#546): "Picture-in-picture" is one of the longest tips in the app and a
   centred bubble ran half off-screen. Same treatment; the arrow stays
   centred under the button. */
#pip-btn.tooltip::before,
#audio-only-btn.tooltip::before {
  left: 0;
  transform: none;
}

/* Boot-restore anti-flash (#473): while the inline head script's class is
   up, the demo transcript is invisible but keeps its layout — the card shows
   briefly empty instead of showing the wrong transcript. Removed by
   bootLibrary when the restore lands (every path), or the inline timeout. */
html.ha-restoring #hypertranscript {
  visibility: hidden;
}

/* Copy-transcript button (#467): pinned to the transcript card's top-right
   corner — the toolbar is crowded on mobile — position:fixed because the
   holder itself is the scroll container (same reasoning as the caption
   editor's Regenerate float). Offsets mirror .transcript-holder's edges;
   hidden in caption mode by the view-switch wiring. */
#transcript-copy-btn {
  position: fixed;
  top: calc(var(--card-top) + var(--text-inset));   /* rides the text's top edge (#580) */
  right: 28px;            /* holder right (16px) + 12px inset */
  z-index: 20;            /* above content, below the drawer (45) and navbar (60) */
  color: oklch(var(--bc) / 0.55);
}
#transcript-copy-btn:hover { color: oklch(var(--bc)); }
/* the loader owns the card while a transcription runs (#hypertranscript
   carries aria-busy via setTranscriptBusy) — nothing to copy, no button */
html:has(#hypertranscript[aria-busy="true"]) #transcript-copy-btn { display: none; }
#transcript-copy-btn[data-copied] { color: oklch(var(--su, var(--p))); }
/* right-anchor the tooltip bubble so it opens inward, not off the card edge */
#transcript-copy-btn.tooltip::before {
  left: auto;
  right: 0;
  transform: none;
}
@media screen and (max-width: 948px) {
  #transcript-copy-btn {
    right: 12px;
    transition: top 0.25s ease; /* follows the video collapse like the holder */
  }
}
/* Project-info ⓘ (#480): the copy button's top-LEFT mirror — top-right acts
   on the content, top-left learns about it. Unlike the right edge, the
   holder's LEFT edge moves with the sidebar (400 <-> 0), so the button
   tracks body.sidebar-collapsed with the same 0.5s ease. */
#transcript-info-btn {
  position: fixed;
  top: calc(var(--card-top) + var(--text-inset));   /* rides the text's top edge (#580) */
  left: 412px;            /* holder left (400px) + 12px inset */
  z-index: 20;
  color: oklch(var(--bc) / 0.55);
  transition: left 0.5s ease;
}
body.sidebar-collapsed #transcript-info-btn { left: 12px; }
#transcript-info-btn:hover { color: oklch(var(--bc)); }
html:has(#hypertranscript[aria-busy="true"]) #transcript-info-btn { display: none; }
/* left-anchor the tooltip bubble so it opens inward, not off the card edge */
#transcript-info-btn.tooltip::before {
  left: 0;
  transform: none;
}
@media screen and (max-width: 948px) {
  #transcript-info-btn {
    left: 12px;
    transition: top 0.25s ease;
  }
}
/* the compact navbar band (#480): the holder's top drops 78px -> 58px at
   1120, and both corner buttons must ride the card's edge with it. Bounded
   below at 949 so the <=948 var-based rules above keep the last word.

   76px, not the holder's 58 + 10: in THIS band the holder also takes 8px of
   padding and #hypertranscript keeps its own top:10px, so the text column
   starts at 76 — 58 + 10 parked the buttons 8px above it and left a gap under
   them half again the size of the one above. Every band aligns the buttons
   with the transcript element's top edge (88 at >1120, ~290 at <=948); this
   is that same edge, arrived at through two more offsets. */

/* Undo/redo (#400): the history module injects ↶/↷ beside the strikethrough
   button, but the top bar has no room to give — especially on mobile. So:
   fine-pointer devices hide them entirely (⌘Z owns undo there; the rule at
   the end of this block), and touch devices get them RELOCATED into the
   floating corner-button pattern the ⓘ and copy buttons established — fixed
   to the transcript card's TOP edge, left of the copy button.

   Top, not bottom, deliberately: the virtual keyboard eats the bottom of the
   viewport at exactly the moment the user is editing, which is when undo is
   wanted. The card's top edge stays visible while typing.

   Touch means ALL touch: pointer media queries cannot tell Android from iOS.
   iOS technically doesn't need these (the system three-finger gestures reach
   the history module via beforeinput historyUndo/historyRedo), but those
   gestures are undiscoverable and the redundancy is harmless. Android has no
   system undo path at all — these buttons are it.

   CSS-only relocation, on purpose: the history module is another author's
   work (#400 branch) and stays untouched. The buttons stay in the toolbar's
   DOM; position:fixed just paints them at the card edge. Their inline
   margin-right (set by the injector for the toolbar layout) would shift the
   fixed box, hence the !important zero. Offsets mirror #transcript-copy-btn
   band for band; sized down to the corner buttons' 2rem so the trio reads
   as one family. */
#transcript-undo,
#transcript-redo {
  position: fixed;
  top: calc(var(--card-top) + var(--text-inset));   /* rides the text's top edge (#580) */
  z-index: 20;
  width: 2rem;
  height: 2rem;
  min-height: 2rem;
  padding: 0;
  margin: 0 !important;    /* neutralize the injector's toolbar margin */
  font-size: 1rem;
}
#transcript-undo { right: 108px; }  /* copy (28px) + 2 button widths + gaps */
#transcript-redo { right: 68px; }
/* the loader owns the card while a transcription runs — same rule as ⓘ/copy */
html:has(#hypertranscript[aria-busy="true"]) #transcript-undo,
html:has(#hypertranscript[aria-busy="true"]) #transcript-redo { display: none; }
@media screen and (max-width: 948px) {
  #transcript-undo,
  #transcript-redo {
    transition: top 0.25s ease;  /* follows the video collapse like the holder */
  }
  #transcript-undo { right: 92px; }   /* copy at 12px on this band */
  #transcript-redo { right: 52px; }
}
/* Only show what can act (#524): a disabled ↶/↷ earns no corner space on a
   phone — it disappears instead of greying. Slots stay FIXED (no sliding
   into the neighbour's spot): during a rapid undo-tap run, a slid-in redo
   would land exactly under the finger the moment redo becomes available.
   A vanished button leaves the finger over nothing, which is harmless —
   and the common nothing-to-undo state gets its clean two-button corner
   back. The history module sets [disabled]; this just honours it. */
#transcript-undo[disabled],
#transcript-redo[disabled] { display: none; }
/* Desktop: the keyboard owns undo. Hidden entirely — the top bar stays clear
   and the corner cluster stays two buttons on the devices that don't need
   these. Must stay LAST in this block so it wins over the float rules. */
@media (hover: hover) and (pointer: fine) {
  #transcript-undo,
  #transcript-redo { display: none; }
}

/* Corner buttons vs scrolling text (#480): the mask-fade experiment was
   removed — masking .transcript-holder faded the card's own top edge and
   rounded corners away with the text, since the holder paints the chrome AND
   is the scrollport. A fade needs those roles split (card chrome on the
   holder, an inner scroll wrapper carrying the mask) — deferred. The
   autoscroll landing offset (editor-core SCROLL_TOP_GAP) keeps parked lines
   clear of the corner buttons meanwhile. */

/* screen-reader-only live region for the copy announcement */
#transcript-copy-status {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* Two deviations from the vendored player stylesheet live here rather than in
   the file itself (#495), so css/hyperaudio-lite-player.css stays byte-identical
   to upstream and can be re-vendored without anyone having to notice a fork.
   This sheet is linked after it, so equal-specificity rules here win. */

/* Unread words sit a shade darker than upstream's #777 — a contrast decision
   from "best practices and accessibility" (5e37ad6), not drift. */
.hyperaudio-transcript .unread {
  color: #666;
}

/* Search: highlight only the matched substring. searchPhrase wraps the match in
   <mark class="search-mark"> and also tags the whole word .search-match. Upstream
   paints .search-mark pink at (0,2,0), so this must outrank it — hence the
   descendant + element selector rather than a bare mark.search-mark, which at
   (0,1,1) would lose and turn every hit pink on the next re-vendor. Tinted with a
   much lighter version of the signature purple (DaisyUI primary at low opacity).
   Nothing styles .search-match now, so the whole-word background it used to
   inherit needs no neutralising. */
.hyperaudio-transcript mark.search-mark {
  background-color: oklch(var(--p) / 0.22);
  color: inherit;
  border-radius: 2px;
}

/* Top-anchor the Transcribe modal. Its three tabs have different heights, and a
   grid-centred modal re-centres (and visibly jumps) when you switch tabs. Pin it
   to the top so it only grows downward. (#349 follow-up) */
#transcribe-modal + .modal { align-items: start; }
#transcribe-modal + .modal .modal-box { margin-top: 3rem; }

/* Find & replace (#25). The replace panel drops below the search box; the
   currently-focused match is highlighted in a distinct colour from the other
   (purple) search hits. */
#find-row { display: flex; align-items: center; gap: 2px; }
/* the search box + its dropdown panel; the panel is anchored to this wrapper so
   it lines up with the search box (left edge and width) rather than the wider
   centred form-control */
#search-wrap { position: relative; flex: 1 1 auto; min-width: 0; max-width: 230px; }
#search-wrap #search-box { width: 100%; max-width: none; margin-right: 0; padding-right: 30px; }
/* Clear control (#558): inside the box's right edge, above the input's own
   padding so the text never runs under it. */
#search-wrap #search-clear {
  position: absolute;
  right: 4px;
  top: 50%;
  transform: translateY(-50%);
  z-index: 2;
  opacity: 0.55;
}
#search-wrap #search-clear:hover { opacity: 1; }
/* .btn's display:inline-flex outranks the hidden attribute's UA display:none */
#search-wrap #search-clear[hidden] { display: none; }
#replace-panel {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  right: 0;
  min-width: 240px;   /* keep the action buttons visible when the search box is narrow */
  max-width: 90vw;
  z-index: 70;
  background: #ffffff;
  border: 1px solid #d1d5db;
  border-radius: 8px;
  padding: 0;
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.18);
  text-align: left;
  overflow: hidden;
}
#replace-panel[hidden] { display: none; }
/* the replace box fills the panel so it aligns edge-to-edge with the search box */
#replace-panel #replace-box { width: 100%; border: 0; border-radius: 0; box-shadow: none; }
#replace-panel input, #replace-panel button { margin-bottom: 0; }
#replace-actions { display: flex; align-items: center; gap: 3px; padding: 6px; border-top: 1px solid #eee; }
#replace-actions .btn-xs { padding-left: 6px; padding-right: 6px; }
#find-match-count {
  font-size: 78%;
  opacity: 0.7;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.replace-spacer { flex: 1; }
/* The panel's explicit exit, set a little apart from the actions it follows */
#replace-close { margin-left: 4px; opacity: 0.6; }
#replace-close:hover { opacity: 1; }
/* Outranks both the general hit above and upstream's .search-mark on specificity
   rather than on source order, so reordering this sheet cannot silently lose the
   active-match colour. */
.hyperaudio-transcript mark.search-mark.active { background-color: #f6c344; color: inherit; border-radius: 2px; }

/* When the replace panel is open, drop the transcript start so the panel doesn't
   cover the first line. */
.hyperaudio-transcript { transition: padding-top 0.2s ease; }
body.find-replace-open .hyperaudio-transcript { padding-top: 108px; }

/* Export modal progress bar (#289) — the tailwind build doesn't include
   DaisyUI's .progress component, so style the native element in the signature
   purple (the DaisyUI primary), mirroring .progress-primary. */
#export-progress {
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  height: 8px;
  border: 0;
  border-radius: 9999px;
  overflow: hidden;
  background-color: oklch(var(--p) / 0.2);
}
#export-progress::-webkit-progress-bar { background-color: transparent; }
#export-progress::-webkit-progress-value {
  background-color: oklch(var(--p));
  border-radius: 9999px;
  transition: width 0.2s ease;
}
#export-progress::-moz-progress-bar {
  background-color: oklch(var(--p));
  border-radius: 9999px;
}

/* =========================================================================
   UI polish (#375)
   ========================================================================= */

/* Recents: the heading lives INSIDE the white card (so the card top can align
   with the transcript card when the video is collapsed), and the card matches
   the video/transcript corner treatment. */
#recents-card {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
  margin-top: 12px;
  background: #ffffff;
  border-radius: 0.5rem;
}
#recents-title {
  flex-shrink: 0;
  font-weight: 700;
  font-size: 1.05rem;
  padding: 12px 16px 4px;
  margin: 0;
}
#recents-scroll {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  border-radius: 0 0 0.5rem 0.5rem;
}

/* The caption editor's floating Regenerate button is injected as
   fixed top-20 right-8 with no z-index, so positioned content (the caption
   rows / transcript holder) painted over it. Lift it above content, below
   the drawer (45) and navbar (60). */
#regenerate-float-btn { z-index: 20; }

/* Video collapse (audio-only button), desktop: slide the frame shut instead
   of a display:none pop, at the same 0.5s ease as the sidebar slide so all
   panel motion matches. The controls row and Recents card animate in step
   (their margins move under body.video-collapsed below). overflow hidden also
   clips the play overlay while shut; 480px accommodates portrait videos at
   the 368px card width. Mobile keeps its own pinned-pane animation. */
@media screen and (min-width: 949px) {
  #player-frame {
    overflow: hidden;
    max-height: 480px;
    transition: max-height 0.5s ease, opacity 0.5s ease;
  }
  body.video-collapsed #player-frame {
    max-height: 0;
    opacity: 0;
  }
  #player-controls {
    transition: margin-top 0.5s ease;
  }
  #recents-card {
    transition: margin-top 0.5s ease;
  }
}

/* Player controls row (formerly inline styles in index.html, moved here so
   state rules below can override without !important). */
#player-controls {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 12px;   /* the same air above the row as #recents-card puts below it (#617) */
  width: 100%;
}

/* Speakers / Timecodes switches: the tailwind build has the DaisyUI .toggle
   base but not the -sm / -primary variants, so size and tint them here. */
#player-controls .toggle {
  height: 1.25rem;
  width: 2.5rem;
  --handleoffset: 1.25rem;
  margin-bottom: 0;
  border: none;
}
#player-controls .toggle:checked {
  color: oklch(var(--p));
}

/* Switches inside modals (export options, skip-gaps) — same build gap as the
   player toggles: tint them primary when on, drop the global input margin, and
   keep them from shrinking when the label text wraps. */
.modal-box .toggle {
  margin-bottom: 0;
  flex-shrink: 0;
}
.modal-box .toggle:checked {
  color: oklch(var(--p));
}

/* Transcribe modal — Local / Cloud service switch (#390). CSS-only: the two
   sr-only sibling radios drive both the segmented control's active state and
   which engine tab-group is shown, so no extra JS is needed. */
.service-mode-radio {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}
.service-switch {
  display: inline-flex;
  gap: 3px;
  margin-bottom: 16px;
  padding: 3px;
  border: 1px solid oklch(var(--bc) / 0.2);
  border-radius: 9999px;
}
.service-switch label {
  padding: 5px 22px;
  border-radius: 9999px;
  cursor: pointer;
  font-size: 0.9rem;
  line-height: 1.2;
  user-select: none;
}
#service-local:checked ~ .service-switch label[for="service-local"],
#service-cloud:checked ~ .service-switch label[for="service-cloud"] {
  background-color: oklch(var(--p));
  color: oklch(var(--pc));
}
#cloud-group { display: none; }
#service-cloud:checked ~ #local-group { display: none; }
#service-cloud:checked ~ #cloud-group { display: block; }

/* Modal label-buttons made keyboard-focusable by js/a11y.js (#402): give them
   a clear focus ring — as labels they don't get the browser's button focus
   styling for free. */
label[data-a11y-wired]:focus-visible {
  outline: 2px solid oklch(var(--p));
  outline-offset: 2px;
}

/* API key field with a show/hide eye toggle (#390). */
.key-field {
  position: relative;
  width: 100%;
  max-width: 20rem;
}
.key-field .input {
  padding-right: 2.4rem;
}
.key-eye {
  position: absolute;
  right: 6px;
  top: 50%;
  transform: translateY(-50%);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 4px;
  background: none;
  border: 0;
  cursor: pointer;
  color: inherit;
  opacity: 0.55;
}
.key-eye:hover { opacity: 1; }

/* Buttons pick up an 8px margin-bottom from the global input/button rule,
   which nudges them off-centre in flex rows — neutralise in the bars. */
.navbar .btn,
#player-controls .btn,
#player-controls button,
#playbar .btn,
#playbar button {
  margin-bottom: 0;
}

/* a touch more air between the search box and the find/replace toggle */
#find-row { gap: 6px; }

/* With the video collapsed (audio-only), the controls row shares the line with
   the navbar buttons, and the Recents card top aligns with the transcript card
   top. Derived, not measured: the navbar buttons span 15..63 (centre 39), so
   the 32px row starts at 23 = the side panel's 15px top padding + 8; the card
   then starts at 78 = 23 + 32 + 23. The earlier constants (11/27) were
   measured before the navbar gained its 4px margin and left the row 4px high
   (#617). Desktop two-pane only — the pinned-player layout handles small
   screens. */
@media screen and (min-width: 949px) {
  body.video-collapsed #player-controls {
    margin-top: 8px;
  }
  body.video-collapsed #recents-card {
    margin-top: 23px;
  }
}

/* Transcript/captions view switch: a recessed track with a raised thumb that
   slides under the active segment, so the current view reads as a position in
   a switch — distinct from the filled action buttons beside it. Colors come
   from the daisyUI theme vars (base-200 track, base-100 thumb). */
#view-switch {
  position: relative;
  display: inline-flex;
  padding: 4px;
  border-radius: 0.5rem;
  background-color: oklch(var(--b3));
  margin-right: 16px;
}
#view-switch::before {
  content: "";
  position: absolute;
  top: 4px;
  left: 4px;
  width: 44px;
  height: 40px;
  border-radius: 0.375rem;
  background-color: oklch(var(--b1));
  box-shadow: 0 1px 3px oklch(var(--bc) / 0.25);
  transition: transform 0.15s ease-out;
}
#view-switch.captions-active::before {
  transform: translateX(44px);
}
@media (prefers-reduced-motion: reduce) {
  #view-switch::before { transition: none; }
}
#view-switch .btn {
  position: relative; /* sits above the sliding thumb */
  width: 44px;
  height: 40px;
  min-height: 0;
  border: none;
  background: transparent;
  color: oklch(var(--bc) / 0.4);
}
#view-switch .btn:hover {
  background: transparent;
  color: oklch(var(--bc) / 0.75);
}
#view-switch .btn[aria-pressed="true"] {
  color: oklch(var(--bc));
}

/* Soften the navbar's outline buttons to sit in the same family as the view
   switch: quiet base-300 borders and a tint hover, instead of daisyUI's
   near-black border + black-fill hover inversion, which read heavier than
   everything around them. */
.navbar .btn-outline {
  border-color: oklch(var(--b3));
  color: oklch(var(--bc) / 0.75);
  background-color: oklch(var(--b1));
}
.navbar .btn-outline:hover {
  border-color: oklch(var(--bc) / 0.3);
  background-color: oklch(var(--b2));
  color: oklch(var(--bc));
}

/* Align the navbar's left edge with the transcript card: the card sits flush
   with .main-panel while the sidebar is open (left offset 0) and floats with a
   16px gutter when collapsed, so the navbar's leading padding follows suit —
   animated to track the card's 0.5s slide. Desktop two-pane only; the
   small-screen layout keeps the default navbar padding. The .main-panel
   prefix beats DaisyUI's single-class .navbar padding (tailwind-min.css
   loads after this file). */
@media screen and (min-width: 949px) {
  .main-panel .navbar {
    padding-left: 0;
    /* Optically centre the buttons in the full band between the screen top
       and the transcript card's top (78px): the borderless navbar reads as
       one space with the 8px floating-card gap below it, so geometric
       centring in the 70px navbar alone left them sitting 4px high. The
       margin shifts the whole centred bar; the card is absolutely
       positioned, so nothing else moves. */
    margin-top: 4px;
    transition: padding-left 0.5s ease;
  }
  body.sidebar-collapsed .main-panel .navbar {
    padding-left: 16px;
  }
}

/* Recents rows (#434): name + hover/focus actions (rename, delete). The
   daisyUI menu lays li content out column-wise for submenus — force a row so
   the action buttons sit beside the name, which truncates rather than wraps. */
#file-picker .recents-row {
  flex-direction: row;
  align-items: center;
  flex-wrap: nowrap;
  /* the daisyUI menu wraps with flex-shrink:0 items, so a long name sizes the
     row to its content and pushes the action icons off-screen — clamp the row
     to the list and let the name's ellipsis absorb the difference */
  max-width: 100%;
}
/* breathing room between consecutive rows (headings carry their own) */
#file-picker .recents-row + .recents-row {
  margin-top: 4px;
}
#file-picker .recents-row .file-item {
  display: block;
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* The actions stretch to the row's full height so the kebab's hover pill
   matches the name pill beside it, with a small gap between the two (#456
   visual pass) — a shorter, flush square read as a mismatched afterthought. */
#file-picker .recents-actions {
  display: flex;
  align-items: center;
  align-self: stretch;
  gap: 2px;
  margin-left: 4px;
  opacity: 0;
}
/* daisyUI styles every direct child of a menu li as a menu item — strip that
   from the actions span so hovering the kebab shows ONE affordance (the
   button's own), not a pill around a pill */
#file-picker .recents-actions,
#file-picker .recents-actions:hover,
#file-picker .recents-actions:active {
  background: none;
  padding: 0;
}
#file-picker .recents-row:hover .recents-actions,
#file-picker .recents-row:focus-within .recents-actions {
  opacity: 1;
}
/* Hovering anywhere in the row — the kebab included — lights the name pill
   too, so the row reads as one unit while its actions are in use. Same fill
   as daisyUI's own menu-item hover; held while the row's menu is open; the
   active row keeps its stronger state. */
#file-picker .recents-row:hover .file-item:not(.active),
#file-picker .recents-row:focus-within .file-item:not(.active),
#file-picker .recents-row:has(.recents-kebab[aria-expanded="true"]) .file-item:not(.active) {
  background-color: oklch(var(--bc) / 0.1);
}
/* neutralise the global button chrome (border + grey fill) for the tiny
   in-row actions; they read as quiet icons until hovered. Radius matches the
   row pill (--rounded-btn, like daisyUI's menu items) since they now share a
   height. */
#file-picker .recents-actions button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* an explicit square at the menu-row height (0.5rem padding + 1.25rem line
     = 2.25rem): matches the app's btn-square icon-button language and centres
     the glyph optically. NOT aspect-ratio:1 — that resolves width after flex
     sizing, so the actions span sized to the bare icon and the square
     overflowed the row off the card edge. */
  width: 2.25rem;
  height: 2.25rem;
  border: none;
  background: transparent;
  margin: 0;
  padding: 0;
  border-radius: var(--rounded-btn, 0.5rem);
  color: oklch(var(--bc) / 0.55);
  cursor: pointer;
  font-size: 11px;
  line-height: 1;
}
#file-picker .recents-actions button:hover {
  border: none;
  background-color: oklch(var(--b2));
  color: oklch(var(--bc));
}
.recents-rename-input {
  width: 100%;
  box-sizing: border-box;
  margin: 0;
  padding: 2px 6px;
  font-size: inherit;
  font-family: inherit;
  border: 1px solid oklch(var(--p));
  border-radius: 0.25rem;
  background-color: oklch(var(--b1));
}
/* notices above the Recents list: quota problems (error tone, auto-dismiss)
   and the one-time autosave disclosure (info tone, sticky until ✕) — both
   replace what used to be a blocking alert() */
#recents-notice {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  margin: 0 16px 8px;
  padding: 8px 12px;
  border-radius: 0.5rem;
  font-size: 12px;
}
#recents-notice.notice-error {
  background-color: oklch(var(--er) / 0.12);
  color: oklch(var(--er));
}
#recents-notice.notice-info {
  background-color: oklch(var(--b2));
  color: oklch(var(--bc) / 0.8);
}
#recents-notice .recents-notice-dismiss {
  margin: 0 0 0 auto;
  padding: 0 2px;
  border: none;
  background: transparent;
  color: inherit;
  font-size: 11px;
  line-height: 1.4;
  cursor: pointer;
}
#recents-notice .recents-notice-dismiss:hover {
  border: none;
  background: transparent;
  opacity: 0.7;
}

/* Line the Recents content up on one 16px inset: the menu's default 8px
   padding put row pills 8px left of the notice box above them. */
#file-picker {
  padding: 0 16px 8px;
}

/* The active row: daisyUI's menu .active is a near-black pill, which reads as
   alarming now that auto-add marks the new entry active immediately. A quiet
   base-200 fill + weight says "current" without shouting. */
#file-picker .file-item.active,
#file-picker .file-item.active:hover {
  background-color: oklch(var(--b2));
  color: inherit;
  font-weight: 600;
}

/* notice action (e.g. Restore after deleting the loaded entry): a quiet
   primary-colored text button; when present it takes the right-push role and
   the ✕ tucks in beside it */
#recents-notice .recents-notice-action {
  margin: 0 0 0 auto;
  padding: 0 2px;
  border: none;
  background: transparent;
  color: oklch(var(--p));
  font-size: 12px;
  font-weight: 600;
  line-height: 1.4;
  cursor: pointer;
}
#recents-notice .recents-notice-action:hover {
  border: none;
  background: transparent;
  text-decoration: underline;
}
#recents-notice .recents-notice-action + .recents-notice-dismiss {
  margin-left: 4px;
}

/* Row kebab menu (#436): one shared, fixed-position menu so it never clips
   against the Recents scroll container. */
#file-picker .recents-row:has(.recents-kebab[aria-expanded="true"]) .recents-actions {
  opacity: 1; /* keep the anchor visible while its menu is open */
}
/* Info modal (#456): one consistent layout — the project name is the only
   large heading; every section sits under the same small muted label, and
   all data rows share one size. The label:value rows match the shape
   editor-core's setTranscriptionInfo writes, so live engine reports and the
   stored per-project rebuild render identically. */
#info-modal + .modal #project-info-name {
  padding-right: 32px; /* clear of the ✕ */
  overflow-wrap: anywhere;
}
#info-modal + .modal .info-section { margin-top: 14px; }
#info-modal + .modal .info-section-label {
  margin: 0 0 4px;
  font-weight: 600;
  font-size: 0.72rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: oklch(var(--bc) / 0.55);
}
#info-modal + .modal .info-rows p,
#info-modal + .modal #summary,
#info-modal + .modal #topics {
  margin: 2px 0;
  font-size: 0.9rem;
  overflow-wrap: anywhere;
}

/* A summary can carry real line structure (the benchmark writes one line per
   measured size) — honour its newlines in the modal. */
#info-modal + .modal #summary {
  white-space: pre-line;
}


/* Row hover popout (#456): full name + stored summary/topics, floated to the
   RIGHT of the panel so it never covers the row or its kebab. Fixed to escape
   the scroll clip; pointer-transparent — purely informational. */
#recents-popout {
  position: fixed;
  z-index: 50;
  /* FIXED, not max-: a shrink-to-fit card changed width with every title,
     so moving down the list made the card jump about. */
  width: 300px;
  padding: 10px 14px;
  border-radius: 0.5rem;
  background-color: oklch(var(--b1));
  box-shadow: 0 4px 16px oklch(var(--bc) / 0.15), 0 0 0 1px oklch(var(--bc) / 0.06);
  pointer-events: none;
  font-size: 13px;
  line-height: 1.45;
}
#recents-popout p { margin: 0; }
#recents-popout p + p { margin-top: 6px; }
#recents-popout .recents-popout-name {
  font-weight: 600;
  overflow-wrap: anywhere; /* full filenames without spaces must wrap, not clip */
  /* ...but only so far: a long interview title ran to five lines and stretched
     the card past the poster it sits above. Two lines, then ellipsis — the
     full name is one click away in the ⓘ. */
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
  /* The clamp draws the ellipsis; this CLIPS. (Chrome normalizes display away
     from -webkit-box when the unprefixed line-clamp is also present, and a
     third line then rendered below the ellipsis.) An explicit line-height
     makes the two-line ceiling exact rather than inherited-dependent. */
  line-height: 1.35;
  max-height: 2.7em;
}
#recents-popout .recents-popout-topics {
  opacity: 0.7;
  font-size: 12px;
}

#recents-menu {
  position: fixed;
  z-index: 50;
  display: flex;
  flex-direction: column;
  min-width: 150px;
  padding: 4px;
  border-radius: 0.5rem;
  background-color: oklch(var(--b1));
  box-shadow: 0 4px 16px oklch(var(--bc) / 0.15), 0 0 0 1px oklch(var(--bc) / 0.06);
}
/* fences Delete off from the reversible actions (#480) */
#recents-menu .recents-menu-sep {
  border: none;
  border-top: 1px solid oklch(var(--bc) / 0.12);
  margin: 4px 6px;
}
#recents-menu button {
  display: flex;
  align-items: center;
  gap: 8px;
  border: none;
  background: transparent;
  margin: 0;
  padding: 8px 16px;
  border-radius: 0.375rem;
  font-size: 14px; /* match the FILE dropdown's item size */
  text-align: left;
  color: oklch(var(--bc) / 0.85);
  cursor: pointer;
}
#recents-menu button:hover,
#recents-menu button:focus-visible {
  border: none;
  background-color: oklch(var(--b2));
  color: oklch(var(--bc));
}
#recents-menu button.confirming,
#recents-menu button.confirming:hover {
  color: oklch(var(--er));
  font-weight: 600;
}

/* Starred/Recents section headings (#440, kept for #456) — same weight as
   the panel's static "Recents" h2, which hides while these are rendered
   (nothing starred = static h2 only, exactly the default look). The picker's
   own 16px inset aligns them with where the static h2 sits. */
#file-picker .recents-group-heading {
  padding: 12px 0 4px;
  pointer-events: none;
}
#file-picker .recents-group-heading h2 {
  /* daisyUI styles any direct child of a menu li as a menu item — zero that
     out so the heading sits at the same 16px inset as the static h2 */
  margin: 0;
  padding: 0;
  background: none;
  font-weight: 700;
  font-size: 1.05rem;
}

/* The export adjust panel's number inputs (speed, target minutes/seconds)
   drop the browser spinner buttons — the slider covers coarse speed changes
   and the spinners just crowded the small boxes. */
#export-adjust-panel input[type="number"]::-webkit-outer-spin-button,
#export-adjust-panel input[type="number"]::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}
#export-adjust-panel input[type="number"] {
  -moz-appearance: textfield;
  appearance: textfield;
}

/* The project dialog's destructive confirm: tailwind-min.css was built before
   btn-error appeared in markup, so only fragments of the utility survived the
   purge (hover fill yes, resting fill no). Style the resting state explicitly
   from the theme vars, matching what the full build would produce. */
#project-dialog .btn-error {
  background-color: oklch(var(--er));
  border-color: oklch(var(--er));
  color: oklch(var(--erc));
}

/* Navbar Save button's dirty dot (#449): a primary-content dot ringed in
   primary while unsaved changes exist — the native app's exact treatment. */
#project-save-dirty-dot {
  display: none;
  position: absolute;
  top: 4px;
  right: 4px;
  width: 9px;
  height: 9px;
  border-radius: 9999px;
  background-color: oklch(var(--pc));
  box-shadow: 0 0 0 1.5px oklch(var(--p));
  z-index: 2;
  pointer-events: none;
}
#project-save-btn.dirty #project-save-dirty-dot {
  display: block;
}

/* Second-tab guard banner (#450): this tab edits without the working-copy
   slot; the owning tab has autosave/crash recovery. */
#tab-guard-banner {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  margin: 0 16px 8px;
  padding: 8px 12px;
  border-radius: 0.5rem;
  font-size: 12px;
  background-color: oklch(var(--b2));
  color: oklch(var(--bc) / 0.8);
}
/* the ✕, styled like the panel notices' dismiss */
#tab-guard-banner button {
  margin: 0 0 0 auto;
  padding: 0 2px;
  border: none;
  background: transparent;
  color: inherit;
  font-size: 11px;
  line-height: 1.4;
  cursor: pointer;
}
#tab-guard-banner button:hover {
  border: none;
  background: transparent;
  opacity: 0.7;
}

/* Export progress pill (#502). Fixed above the play bar rather than in
   #side-notices, which is an off-canvas drawer on the small-screen layout —
   invisible on exactly the devices where building a container takes longest.
   Right-aligned so it does not cover the transcript's left reading edge, and
   below the corner buttons' z-index band so it never fights them. */
#project-progress {
  position: fixed;
  right: 16px;
  bottom: 64px;               /* play bar (52px) + 12px */
  z-index: 30;
  max-width: calc(100vw - 32px);
  padding: 8px 14px;
  border-radius: 9999px;
  background: oklch(var(--n, 0.2 0 0));
  color: oklch(var(--nc, 0.98 0 0));
  font-size: 0.85rem;
  line-height: 1.4;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.25);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
@media screen and (max-width: 948px) {
  #project-progress {
    right: 12px;
    left: 12px;
    bottom: 60px;
    text-align: center;
  }
}
@media (prefers-reduced-motion: reduce) {
  #project-progress { transition: none; }
}

/* An unavailable export source reads as unavailable (#608). Disabling the
   radio alone left its label at full strength, so "Edited media" looked like a
   choice you could make when there was nothing to edit. The summary beside it
   still says why — "(no strikeouts or skipped silences)". */
.export-source-off {
  opacity: 0.5;
  cursor: default;
}

/* Host-contributed rows (#604): present, and plainly not yet the editor's.
   Dimmed rather than greyed-out — they are clickable, and the badge says
   what the dimming means. Opening one promotes it to an ordinary row, which
   is why nothing here is styled as permanently different. */
#file-picker .recents-row-external .file-item {
  opacity: 0.6;
}
#file-picker .recents-row-external .recents-actions {
  opacity: 1;              /* the badge is not a hover-revealed action */
  flex: 0 0 auto;
}
#file-picker .recents-external-badge {
  display: inline-block;
  padding: 2px 8px;
  margin-right: 8px;
  border: 1px dashed oklch(var(--bc) / 0.35);
  border-radius: 9999px;
  font-size: 0.7rem;
  white-space: nowrap;
  color: oklch(var(--bc) / 0.55);
}

/* The in-progress transcription row (#525): the same anatomy and widths as a
   normal row, with a spinner in the kebab's slot as the wordless "something
   is happening" signal — always visible, where a kebab reveals on hover. */
#file-picker .recents-row-transcribing .recents-actions {
  opacity: 1;
  align-items: center;
  /* the kebab is a 2rem square button; give the spinner the same footprint
     so the name column ends where every other row's does */
  width: 2rem;
  justify-content: center;
}
#file-picker .recents-transcribing-spinner {
  width: 14px;
  height: 14px;
  border: 2px solid oklch(var(--p) / 0.25);
  border-top-color: oklch(var(--p));
  border-radius: 50%;
  animation: ha-transcribing-spin 0.8s linear infinite;
}
@keyframes ha-transcribing-spin {
  to { transform: rotate(360deg); }
}
@media (prefers-reduced-motion: reduce) {
  #file-picker .recents-transcribing-spinner { animation: none; }
}

/* One transcription at a time (#525): the engines share one transcript
   element, one player and one busy flag — starting a second while one runs
   produced interleaved errors. The entry points grey out while the
   transcript is busy (same :has pattern as the corner buttons); the modal's
   own submit labels need no gating because starting closes the modal and
   reopening goes through these. Batch transcription is its own feature. */
html.ha-transcribing #new-transcription-btn,
html.ha-transcribing #upload-transcribe-btn {
  pointer-events: none;
  opacity: 0.4;
}

/* Warning dress for the shared dialog (#506): reserved for state-divergence
   warnings ("captions have been edited") — an amber left border and a tinted
   title band on the standard modal chassis. Bold enough that the warning
   registers, without the full-bleed cyan alert this replaces. --wa is
   DaisyUI's per-theme warning colour (v4: OKLCH components, so oklch() not
   hsl()), so both themes get a fitting amber. */
#project-dialog .modal-box.modal-warning {
  border-left: 6px solid oklch(var(--wa));
}
#project-dialog .project-dialog-title {
  background: oklch(var(--wa) / 0.18);
  color: inherit;
  font-weight: 700;
  /* bleed the band to the box edges (modal-box padding is 1.5rem) */
  margin: -1.5rem -1.5rem 1rem -1.5rem;
  padding: 0.9rem 3rem 0.9rem 1.5rem; /* right clears the ✕ button */
  border-radius: 1rem 1rem 0 0; /* match --rounded-box on the top corners */
}
#project-dialog .modal-box.modal-warning .project-dialog-title {
  border-radius: 0.6rem 1rem 0 0; /* inner top-left sits against the border */
}

/* Transcribe modal: the URL source is the rare path (and the sharp one — CORS),
   so it sits behind a plain-disclosure reveal under the file picker instead of
   competing with it. Native <details>: no JS, and the engines' listeners on
   the inputs inside are untouched. */
.url-source summary { cursor: pointer; font-size: 0.85rem; opacity: 0.7; user-select: none; width: fit-content; }
.url-source summary:hover { opacity: 1; }
.url-source[open] summary { margin-bottom: 10px; }
.url-source { margin-top: 4px; }

/* Popout poster thumbnail (#523 phase A): first-frame capture for video,
   tinted wave glyph for audio. Fixed 16:9 box so the card doesn't reflow
   when the async poster lands. */
#recents-popout .recents-popout-thumb {
  width: 100%;
  aspect-ratio: 16 / 9;
  border-radius: 6px;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-bottom: 8px;
  color: #5b6472;
}
#recents-popout .recents-popout-poster,
#recents-popout .recents-popout-glyph {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* Settings modal (#615): the Info modal's section idiom, with rows that put
   the control on the right and a muted hint under the label. */
#settings-modal + .modal .settings-section { margin-top: 16px; }
#settings-modal + .modal .settings-section-label {
  margin: 0 0 4px;
  font-weight: 600;
  font-size: 0.72rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: oklch(var(--bc) / 0.55);
}
#settings-modal + .modal .settings-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 8px 0;
  border-top: 1px solid oklch(var(--bc) / 0.08);
  line-height: 1.4;
}
#settings-modal + .modal .settings-section-label + .settings-row { border-top: none; }
#settings-modal + .modal .settings-row-toggle { cursor: pointer; }
#settings-modal + .modal .settings-text { flex: 1 1 auto; min-width: 0; }
#settings-modal + .modal .settings-hint {
  display: block;
  font-size: 0.8rem;
  color: oklch(var(--bc) / 0.6);
}
#settings-modal + .modal .settings-value { opacity: 0.7; }
#settings-modal + .modal .settings-row-stack { flex-direction: column; align-items: stretch; gap: 6px; }
#settings-modal + .modal .settings-models { list-style: none; margin: 0; padding: 0; }
#settings-modal + .modal .settings-models:empty { display: none; }
#settings-modal + .modal .settings-models li {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 4px 0 4px 12px;
  font-size: 0.9rem;
}
#settings-modal + .modal .settings-models .settings-model-size { opacity: 0.6; margin-left: 6px; }
#settings-modal + .modal .settings-row .btn { margin-bottom: 0; flex: 0 0 auto; }
#settings-modal + .modal .toggle { margin-bottom: 0; flex-shrink: 0; }
