@font-face { font-family: "Anuphan"; src: url("fonts/Anuphan-Regular.ttf") format("truetype"); font-weight: 400; }
@font-face { font-family: "Anuphan"; src: url("fonts/Anuphan-Medium.ttf") format("truetype"); font-weight: 500; }
@font-face { font-family: "Anuphan"; src: url("fonts/Anuphan-SemiBold.ttf") format("truetype"); font-weight: 600; }
@font-face { font-family: "Anuphan"; src: url("fonts/Anuphan-Bold.ttf") format("truetype"); font-weight: 700; }
/* Curated Thai+Latin caption faces (OFL). One weight each is enough for the picker
   preview; the burn resolves every weight via fontconfig from the family name. */
@font-face { font-family: "Kanit"; src: url("fonts/Kanit-Regular.ttf") format("truetype"); font-weight: 400; }
@font-face { font-family: "Noto Sans Thai"; src: url("fonts/NotoSansThai-Bold.ttf") format("truetype"); font-weight: 400; }
@font-face { font-family: "IBM Plex Sans Thai"; src: url("fonts/IBMPlexSansThai-Bold.ttf") format("truetype"); font-weight: 400; }

:root {
  --bg: #ffffff;
  --panel-bg: #fafafa;
  --border: #e4e4e7;
  --text: #18181b;
  --text-muted: #71717a;
  --accent: #2563eb;
  --accent-soft: #eff6ff;
  /* Clip status. Home had no status palette at all, so the one signal a
     returning creator actually scans for ("which clip is half-finished") had
     nowhere to live. Declared as tokens because the CSS audit found 41 raw hex
     values bypassing 7 tokens and 8 different ambers for one meaning. */
  --ok: #15803d;    --ok-soft: #f0fdf4;    --ok-border: #bbf7d0;
  --warn: #b45309;  --warn-soft: #fffbeb;  --warn-border: #fde68a;
  --on-accent: #ffffff;
  --scrim: rgba(24,24,27,.72);
  /* Ring around a chip that sits on top of a video frame. The status badge is
     opaque, so its TEXT is safe over any frame (measured 4.63-4.84:1). What
     failed was finding the chip: its pale fill scores 1.04:1 against a white
     region of a real frame and its border 1.21:1, so on a bright frame the
     whole badge dissolved. This ring reads 3.94:1 there; on a dark frame the
     pale fill carries itself (17.77:1). The pair covers both. */
  --chip-ring: rgba(24,24,27,.55);
  --shadow-card: 0 1px 2px rgba(24,24,27,.06), 0 1px 3px rgba(24,24,27,.04);
  --shadow-lift: 0 4px 12px rgba(24,24,27,.10);
}

* { box-sizing: border-box; }
body { margin: 0; font-family: "Anuphan", system-ui, sans-serif; background: var(--bg); color: var(--text); }

/* The `hidden` attribute must ALWAYS win. Any rule that sets an explicit
   display (flex/grid/block) silently outranks the UA's `[hidden]{display:none}`,
   so `el.hidden = true` sets the property but leaves the element on screen —
   a bug that has bitten this project repeatedly (.panel-group, .review-chat,
   .rail-item, and most recently .ai-card, where step-4 cards stayed visible on
   step 1 even though .hidden was true). One global rule instead of remembering
   a per-class override every time. The specific [hidden] rules further down are
   now redundant belt-and-braces. */
[hidden] { display: none !important; }

/* Login gate overlay. Full-viewport so nothing behind it is reachable while
   logged out — reuses .panel's card look (bg/border/radius) rather than
   inventing a second visual language for one screen. z-index above
   everything else in the app, including the job bar. */
.auth-screen { position: fixed; inset: 0; z-index: 1000; display: flex;
  align-items: center; justify-content: center; background: var(--bg); }
.auth-card { width: 100%; max-width: 360px; display: flex; flex-direction: column;
  gap: .9rem; align-items: center; text-align: center; background: var(--panel-bg);
  border: 1px solid var(--border); border-radius: 12px; padding: 2rem 1.5rem; }
.auth-card h2 { margin: 0; }
.auth-card p { margin: 0; color: var(--text-muted); font-size: .9rem; }
.auth-free { font-size: .78rem !important; }

/* In-app browser notice. Replaces the Google button (which cannot work here),
   so it must carry the whole job: why nothing will happen, the exact tap that
   fixes it, and a fallback that works even when the menu wording differs. */
.inapp-notice { display: flex; flex-direction: column; gap: .6rem; width: 100%;
  border: 1px solid var(--warn-border); background: var(--warn-soft);
  border-radius: 10px; padding: .9rem .8rem; text-align: left; }
.inapp-why { margin: 0; font-size: .86rem; color: var(--warn); line-height: 1.5; }
.inapp-why b { color: var(--warn); }
.inapp-hint { margin: 0; font-size: .9rem; font-weight: 600; color: var(--text); line-height: 1.5; }
/* Emphasis follows what actually fixes the problem on THIS device: the Chrome
   intent is one tap on Android, so it is the filled button there; on iOS there
   is no equivalent and copying the link is the real action. Same two controls,
   swapped weight — never two filled buttons competing. */
.inapp-chrome, .inapp-copy { display: block; width: 100%; text-align: center;
  font: inherit; font-size: .9rem; font-weight: 600; padding: .6rem .8rem;
  border-radius: 9px; cursor: pointer; text-decoration: none; }
.inapp-chrome { background: var(--accent); color: var(--on-accent); border: 0; }
.inapp-chrome:hover { filter: brightness(1.06); }
.inapp-copy { background: var(--bg); color: var(--accent); border: 1px solid var(--accent); }
.inapp-copy:hover { background: var(--accent-soft); }
/* No Chrome intent (iOS): copy becomes the primary action. */
.inapp-notice.no-intent .inapp-copy { background: var(--accent); color: var(--on-accent); border: 0; }
.inapp-notice.no-intent .inapp-copy:hover { filter: brightness(1.06); }
.auth-consent { font-size: .8rem !important; color: var(--text-muted); line-height: 1.7; }
/* The two legally-load-bearing links get a real tap target (UI critic: they
   rendered 15px tall vs the 24px WCAG floor) without growing the layout —
   padding out, negative margin back in. */
.auth-consent a { color: var(--accent); text-decoration: underline;
  display: inline-block; padding: 6px 2px; margin: -6px -2px; }
.auth-error { color: #b91c1c; font-size: .82rem !important; line-height: 1.5;
  background: #fef2f2; border: 1px solid #fecaca; border-radius: 8px; padding: .5rem .65rem; }

/* Dev-only login harness — deliberately loud/unlike the real .auth-card so it
   can never be mistaken for production login (see CLAUDE.md: this is a full
   auth bypass, local dev only). Amber/black hazard-stripe accent, not the
   app's normal palette. */
.dev-auth-screen { background: rgba(20, 14, 0, .92); }
.dev-auth-card { border: 2px dashed #f5a623; background: var(--panel-bg); }
.dev-auth-badge { align-self: stretch; background: #f5a623; color: #1a1200;
  font-weight: 700; font-size: .78rem; letter-spacing: .02em; padding: .35rem .6rem;
  border-radius: 6px; text-transform: uppercase; }
.dev-auth-input { width: 100%; box-sizing: border-box; padding: .5rem .7rem;
  border: 1px solid var(--border); border-radius: 8px; background: var(--bg);
  color: inherit; font-size: .95rem; }

/* App shell: tool rail (far left) + workspace. Rail is desktop-only. */
.app { display: flex; min-height: 100vh; }
.workspace { flex: 1; min-width: 0; }

.rail { display: none; }  /* hidden on mobile — header already says "Reels Studio" */
@media (min-width: 768px) {
  .rail { display: flex; flex-direction: column; align-items: center; gap: .5rem;
    width: 76px; flex-shrink: 0; padding: .9rem .4rem; border-right: 1px solid var(--border); background: var(--panel-bg); }
  .rail-brand { width: 34px; height: 34px; display: grid; place-items: center; color: var(--accent); margin-bottom: .4rem; }
  .rail-brand svg { width: 24px; height: 24px; }
  .rail-item { display: flex; flex-direction: column; align-items: center; gap: .25rem; width: 100%;
    font-family: inherit; font-size: .68rem; color: var(--text-muted); background: none; border: none;
    border-radius: 10px; padding: .55rem .2rem; cursor: pointer; }
  .rail-item svg { width: 22px; height: 22px; }
  .rail-item:hover { background: #fff; color: var(--text); }
  .rail-item.active { color: var(--accent); background: var(--accent-soft); }
  .rail-item.mobile-only { display: none; }   /* the panel is always-visible chrome here */
  #btn-find-toggle { display: none; }         /* find/replace is permanent chrome here */
  .rail-soon { margin-top: auto; display: flex; flex-direction: column; align-items: center; gap: .2rem;
    font-size: .6rem; color: var(--text-muted); text-align: center; }
  .rail-soon-mark { width: 26px; height: 26px; display: grid; place-items: center; border: 1.5px dashed var(--border);
    border-radius: 8px; color: var(--text-muted); font-size: 1rem; }
}

header { display: flex; flex-wrap: wrap; gap: .6rem; align-items: center; justify-content: space-between;
  padding: .7rem 1.1rem; border-bottom: 1px solid var(--border); }
.header-left { display: flex; align-items: baseline; gap: .5rem; }
header h1 { font-size: 1rem; font-weight: 600; margin: 0; }
#mode-badge { font-size: .72rem; color: var(--text-muted); }
/* Ambient quota status (Task 12). Colours per data-warn: low = amber, out =
   red — everything else (unset, "ok") uses the same muted tone as mode-badge.
   Visibility is the global [hidden]{display:none!important} rule above. */
/* .quota-pill is shared by the header pill (#quota-pill) and Home's
   #home-quota (Task 5 amendment) — refreshQuota() writes the same text and
   data-warn to both, so both must look identical. */
.quota-pill { font-size: .72rem; color: var(--text-muted); padding: .15rem .5rem;
  border: 1px solid var(--border); border-radius: 999px; white-space: nowrap; }
.quota-pill[data-warn="low"] { color: #92400e; border-color: #92400e; }
.quota-pill[data-warn="out"] { color: #b91c1c; border-color: #b91c1c; }
/* `overflow: hidden` is load-bearing on EVERY screen, not cosmetic. WebKit —
   the engine Safari runs — adds this <select>'s widest OPTION to the scrollWidth
   of every ancestor, even though the control paints at its own width. A clip is
   named things like "copy_87B07B33-9D5B-4BD2-B587-31662B606E74-3", so measured
   at iPhone 15 Pro size that leaked 183px into `.header-left` and 68px into the
   page, and Safari then let the whole app pan sideways — which is what cut the
   stepper and the clip name off on the LEFT in the owner's screenshots, four
   reports over one day. Chromium does not do this, which is why it took a real
   WebKit build to find. Six candidates were measured; this was the only one
   that reached 0 without changing the rendered width. */
.clip-picker { font-family: inherit; font-size: .82rem; max-width: 200px; padding: .35rem .5rem; overflow: hidden;
  border: 1px solid var(--border); border-radius: 8px; background: #fff; color: var(--text); cursor: pointer; }
.upload-btn { display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px;
  border: 1px solid var(--border); border-radius: 8px; cursor: pointer; color: var(--text); }
.upload-btn:hover { border-color: var(--accent); color: var(--accent); }
.upload-btn svg { width: 17px; height: 17px; }
.header-right { display: flex; align-items: center; gap: .7rem; }
#status { font-size: .8rem; color: var(--text-muted); }

.btn-export { display: inline-flex; align-items: center; gap: .4rem; font-family: inherit; font-weight: 600;
  font-size: .85rem; padding: .5rem .95rem; border: none; border-radius: 999px; cursor: pointer;
  background: var(--accent); color: #fff; }
.btn-export:hover { opacity: .9; }
.btn-export .icon { width: 16px; height: 16px; }
.btn-export:disabled { cursor: default; opacity: 1; }
/* while burning: the button fills left→right with the encode %, and its icon spins */
.btn-export.burning { background:
  linear-gradient(90deg, var(--accent) var(--p,0%), color-mix(in srgb, var(--accent) 35%, #fff) var(--p,0%)); }
.btn-export.burning .icon { animation: burn-spin 1s linear infinite; }
@keyframes burn-spin { to { transform: rotate(360deg); } }

/* Full-width real-time progress bar pinned to the very top of the window. */
.job-bar { position: fixed; top: 0; left: 0; right: 0; height: 3px; z-index: 100;
  background: color-mix(in srgb, var(--accent) 18%, transparent); }
.job-bar[hidden] { display: none; }
/* A shimmer sweeps the fill so a stalled percent (e.g. transcribe waiting on
   the ASR call at 40%) still reads as "working", not frozen. */
.job-bar-fill { height: 100%; width: 0; background-color: var(--accent); transition: width .25s ease;
  background-image: linear-gradient(90deg, transparent, rgba(255,255,255,.55), transparent);
  background-size: 160px 100%; background-repeat: no-repeat;
  animation: job-shimmer 1.2s linear infinite; }
@keyframes job-shimmer {
  from { background-position: -160px 0; }
  to   { background-position: calc(100% + 160px) 0; }
}

/* ===== base = MOBILE: stack panel under the preview ===== */
main { display: flex; flex-direction: column-reverse; gap: 1rem; padding: 1rem .9rem; align-items: center; }

/* Contextual Style panel (ElevenLabs Edit-Text shape) */
.panel { width: 100%; max-width: 420px; display: flex; flex-direction: column; gap: 1rem;
  background: var(--panel-bg); border: 1px solid var(--border); border-radius: 12px; padding: 1rem; }
.panel-group { display: flex; flex-direction: column; gap: 1rem; }
.panel-group[hidden] { display: none; }  /* [hidden] must beat the flex display above */
.panel-title { text-transform: uppercase; letter-spacing: .03em; font-size: .74rem; color: var(--text-muted); }
.field { display: flex; flex-direction: column; gap: .45rem; }
.field > label { font-size: .78rem; color: var(--text-muted); display: flex; justify-content: space-between; }
.divider { height: 1px; background: var(--border); }
/* One-line explanation under a choice (e.g. คำต่อคำ) — answers "กดตอนไหน" */
.field-hint { margin: .2rem 0 0; font-size: .74rem; line-height: 1.5; color: var(--text-muted); }
.mini-ico { width: 14px; height: 14px; vertical-align: -2px; }

/* single-line text inputs in the panel (EP title, hex) */
.panel input[type="text"] { width: 100%; font-family: inherit; font-size: .9rem; padding: .5rem .65rem;
  border: 1px solid var(--border); border-radius: 8px; background: #fff; color: var(--text); }
.panel input[type="text"]:focus { outline: none; border-color: var(--accent); }
#eng-hex { text-transform: uppercase; }

/* In-font weight list — each option rendered in its own weight (ElevenLabs font-list pattern) */
.font-list { display: flex; flex-direction: column; gap: .4rem; }
/* Segmented mode toggle: both options visible at once, no dropdown to open.
   Matches CapCut's Styles tab and Descript's caption-style picker — a
   frequently-toggled binary choice shown as tappable options, not hidden
   behind a closed <select>. */
.mode-toggle { display: flex; gap: .4rem; }
.mode-toggle .font-opt { flex: 1; text-align: center; }
.font-opt { font-family: "Anuphan", sans-serif; font-size: .95rem; text-align: left; padding: .55rem .7rem;
  border: 1px solid var(--border); border-radius: 8px; background: #fff; color: var(--text); cursor: pointer; }
.font-opt:hover { border-color: var(--accent); }
.font-opt.active { border-color: var(--accent); background: var(--accent-soft); color: var(--accent); }
.font-select { width: 100%; font-size: .95rem; padding: .55rem .7rem; border: 1px solid var(--border);
  border-radius: 8px; background: #fff; color: var(--text); cursor: pointer; }
.font-select:hover { border-color: var(--accent); }
.check-row { display: flex; align-items: center; gap: .5rem; cursor: pointer; }
.check-row input { width: auto; margin: 0; }
/* Rail panel-toggles: notification dot (e.g. edited-after-burn on รีวิว AI). */
.rail-item { position: relative; }
.rail-item[hidden] { display: none !important; }
.rail-dot { position: absolute; top: 4px; right: 8px; width: 8px; height: 8px;
  border-radius: 50%; background: #f59e0b; }
.rail-dot[hidden] { display: none; }
.head-actions { display: flex; align-items: center; gap: .5rem; }

/* Workflow stepper: Benevity-style — numbered circles + connector line, labels
   BELOW, in its OWN strip under the header (informational, never gating).
   ✓ filled = done, accent ring = current, grey = upcoming, amber ! = stale burn. */
.step-strip { position: relative; display: flex; align-items: center; justify-content: center;
  padding: .5rem 1.2rem .55rem; border-bottom: 1px solid var(--border); background: #fff;
  flex-shrink: 0; overflow-x: auto; }
.step-strip #status { position: absolute; right: 1.2rem; font-size: .75rem; color: var(--text-muted);
  max-width: 30%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.stepper { display: flex; align-items: flex-start; }
.step-col { display: flex; flex-direction: column; align-items: center; gap: 5px; width: 88px;
  background: none; border: none; cursor: pointer; font-family: inherit; padding: 0; }
.step-circle { width: 26px; height: 26px; border-radius: 50%; border: 2px solid var(--border);
  display: grid; place-items: center; font-size: .72rem; color: var(--text-muted);
  background: #fff; box-sizing: border-box; }
.step-col:hover .step-circle { border-color: var(--accent); color: var(--accent); }
.step-col.done .step-circle { background: var(--accent); border-color: var(--accent); color: #fff; }
.step-col.current .step-circle { border-color: var(--accent); color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft); }
.step-col.stale .step-circle { background: #f59e0b; border-color: #f59e0b; color: #fff; }
.step-label { font-size: .72rem; color: var(--text-muted); white-space: nowrap; }
.step-col.done .step-label { color: var(--accent); }
.step-col.current .step-label { color: var(--text); font-weight: 600; }
.step-col.stale .step-label { color: #92400e; }
.step-line { flex-shrink: 0; height: 2px; width: 42px; background: var(--border); margin-top: 12px; }
.step-line.done { background: var(--accent); }
/* Gentle invite on the current step — a soft breathing ring, never a gate. */
.step-col.current .step-circle { animation: step-pulse 2s ease-in-out 3; }
@keyframes step-pulse {
  0%, 100% { box-shadow: 0 0 0 3px var(--accent-soft); }
  50%      { box-shadow: 0 0 0 7px color-mix(in srgb, var(--accent) 22%, transparent); }
}
@media (prefers-reduced-motion: reduce) {
  .step-col.current .step-circle { animation: none; }
}

/* First-run walkthrough: ONE reactive coach-mark bubble anchored to the next
   real control; the control itself is the "Next" button (never blocks clicks). */
/* Fixed WIDTH, not max-width. With only a max-width this box is sized by the
   space left over from wherever `left` currently puts it, so renderCoachmark
   measured 77.75px for a bubble that renders at 250px, clamped against that
   number, and parked it 29px off the right edge of a 375px phone (measured
   against origin/main as a control: same bubble, right=367 there, 404 here).
   A width that does not depend on position makes the measurement — and the
   clamp built on it — mean the same thing every time. */
/* Phone-only replacements for two controls taken out of the bottom tab bar.
   Hidden by default and turned on in the phone block — the existing
   `.rail-item.mobile-only` rule only covers rail items, and these are not. */
.btn-back, .logout-m { display: none; }
.coachmark { position: fixed; z-index: 65; width: min(250px, calc(100vw - 16px));
  background: #fff;
  border: 1.5px solid var(--accent); border-radius: 10px; padding: .6rem .75rem;
  box-shadow: 0 8px 24px rgba(37,99,235,.22); display: flex; align-items: flex-start; gap: .5rem;
  animation: coach-in .25s ease; }
.coachmark[hidden] { display: none; }
@keyframes coach-in { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
.coach-text { display: flex; flex-direction: column; gap: .15rem; font-size: .8rem; line-height: 1.45; }
.coach-text b { color: var(--accent); font-size: .82rem; }
#coach-skip { border: none; background: none; cursor: pointer; font-family: inherit;
  font-size: .72rem; color: var(--text-muted); text-decoration: underline; padding: 0; margin-top: .1rem; }
#coach-close { border: none; background: none; cursor: pointer; color: var(--text-muted);
  font-size: .8rem; padding: 0; line-height: 1; }
.coachmark::before { content: ""; position: absolute; width: 10px; height: 10px; background: #fff;
  border: 1.5px solid var(--accent); transform: rotate(45deg); }
.coachmark[data-place="below"]::before { top: -6.5px; right: 24px; border-bottom: none; border-right: none; }
/* The bubble sits ABOVE its anchor, so the arrow hangs off the bottom edge
   pointing down at it. Added when the phone measurement showed there is rarely
   room beside a control: the renderer now moves the bubble off any anchor it
   would cover, and it needs a tail for that direction too. Centred, because at
   that point the bubble is centred on the anchor rather than edge-aligned. */
.coachmark[data-place="above"]::before { bottom: -6.5px; right: 24px;
  border-top: none; border-left: none; }
/* When the renderer had to move the bubble off a control it was covering, it
   centres the bubble on that control — so the arrow centres with it. Without
   this the tail keeps its edge offset and points at empty space. */
.coachmark[data-centred]::before { right: auto; left: calc(50% - 5px); }
.coachmark[data-place="right"]::before { left: -6.5px; top: 18px; border-top: none; border-right: none; }
.coachmark[data-place="left"]::before  { right: -6.5px; top: 18px; border-bottom: none; border-left: none; }
@media (prefers-reduced-motion: reduce) { .coachmark { animation: none; } }

/* Collapsed "advanced" style accordion — presets stay visible above it. */
#style-details > summary { cursor: pointer; font-size: .72rem; font-weight: 600;
  color: var(--text-muted); text-transform: uppercase; letter-spacing: .04em;
  padding: .25rem 0 .4rem; list-style: none; }
#style-details > summary::before { content: "▸ "; color: var(--text-muted); }
#style-details[open] > summary::before { content: "▾ "; }
#style-details > summary::-webkit-details-marker { display: none; }
.usage-meter { font-size: .7rem; color: var(--text-muted); white-space: nowrap; }
/* Burn split-button: the ONE primary CTA top-right; secondary export + cost
   live in its dropdown (research: leaders keep the top-right to a single CTA). */
.burn-group { position: relative; display: flex; align-items: stretch; }
.burn-group .btn-export { border-radius: 999px 0 0 999px; }
.burn-more { border: none; border-left: 1px solid rgba(255,255,255,.45); border-radius: 0 999px 999px 0;
  background: var(--accent); color: #fff; padding: 0 .6rem; cursor: pointer; font-size: .8rem; }
.burn-menu { position: absolute; top: calc(100% + 6px); right: 0; z-index: 40; min-width: 230px;
  background: #fff; border: 1px solid var(--border); border-radius: 10px; padding: .35rem;
  box-shadow: 0 8px 24px rgba(0,0,0,.14); display: flex; flex-direction: column; gap: .15rem; }
.burn-menu[hidden] { display: none; }
.burn-menu-item { text-align: left; font-family: inherit; font-size: .85rem; padding: .5rem .6rem;
  border: none; border-radius: 6px; background: none; cursor: pointer; color: var(--text); }
.burn-menu-item:hover { background: var(--accent-soft); color: var(--accent); }
.burn-menu-usage { padding: .35rem .6rem; border-top: 1px solid var(--border); margin-top: .15rem; }
.usage-toast { position: fixed; bottom: 16px; right: 16px; z-index: 60; background: #111827; color: #fff;
  font-size: .78rem; padding: .5rem .8rem; border-radius: 8px; box-shadow: 0 4px 14px rgba(0,0,0,.25); }
.usage-toast[hidden] { display: none; }
/* Learned-correction offer: sits above the usage toast, carries an action. */
.sweep-toast { position: fixed; bottom: 56px; right: 16px; z-index: 60; background: #fff;
  border: 1px solid var(--accent); color: var(--text); font-size: .8rem;
  padding: .5rem .7rem; border-radius: 8px; box-shadow: 0 4px 14px rgba(0,0,0,.18);
  display: flex; align-items: center; gap: .6rem; }
.sweep-toast[hidden] { display: none; }
.sweep-toast button { font-family: inherit; font-size: .78rem; padding: .3rem .7rem; cursor: pointer;
  border: 1px solid var(--accent); border-radius: 999px; background: var(--accent); color: #fff; }
/* "การเรียนรู้ของ AI" modal — usage-all / SkillOpt status / glossary, one panel */
.glossary-modal { border: 1px solid var(--border); border-radius: 12px; padding: 0;
  width: min(440px, 92vw); max-height: 78vh; box-shadow: 0 12px 40px rgba(0,0,0,.18); }
.glossary-modal::backdrop { background: rgba(0,0,0,.25); }
.ai-panel-body { overflow-y: auto; max-height: calc(78vh - 60px); padding: .7rem; display: flex; flex-direction: column; gap: 1.1rem; }
.ai-panel-section + .ai-panel-section { border-top: 1px solid var(--border); padding-top: .9rem; }
.ai-panel-title { font-size: .72rem; font-weight: 600; color: var(--text-muted); text-transform: uppercase;
  letter-spacing: .04em; margin-bottom: .4rem; }
.usage-all-summary { font-size: .85rem; font-weight: 600; }
.usage-all-breakdown { margin-top: .3rem; font-size: .78rem; color: var(--text-muted); display: flex; flex-direction: column; gap: .15rem; }
.skill-status { font-size: .8rem; color: var(--text); display: flex; flex-direction: column; gap: .2rem; }
.glossary-rows { display: flex; flex-direction: column; gap: .4rem; }
.g-row { display: flex; align-items: center; gap: .5rem; padding: .4rem .55rem;
  border: 1px solid var(--border); border-radius: 8px; font-size: .82rem; }
.g-row.brittle { background: #fef3c7; border-color: #f59e0b; }
.g-row .g-pair { flex: 1; }
.g-row .g-del { border: none; background: none; cursor: pointer; color: var(--text-muted); font-size: .9rem; }
.g-row .g-del:hover { color: #b91c1c; }
.g-empty { font-size: .8rem; color: var(--text-muted); padding: .6rem; }
.revise-badge { font-size: .7rem; padding: .2rem .5rem; border-radius: 999px; background: var(--panel-bg); color: var(--text-muted); white-space: nowrap; }
.revise-badge.stale { background: #fef3c7; color: #92400e; }
.preset-row { display: flex; flex-wrap: wrap; gap: .4rem; }
.preset-btn { font-size: .9rem; padding: .4rem .65rem; border: 1px solid var(--border);
  border-radius: 999px; background: #fff; color: var(--text); cursor: pointer; }
.preset-btn:hover { border-color: var(--accent); background: var(--accent-soft); color: var(--accent); }
/* One-shot attention flash on the preset row after a transcribe, so the user
   discovers the style presets at the moment captions first appear. */
.preset-row.flash-once { animation: preset-flash 1.2s ease; border-radius: 10px; }
@keyframes preset-flash {
  0%, 100% { box-shadow: 0 0 0 0 transparent; }
  30%      { box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 45%, transparent); }
}

input[type="range"] { width: 100%; accent-color: var(--accent); }
#eng-size-val { color: var(--text); font-variant-numeric: tabular-nums; }

.color-row { display: flex; align-items: center; gap: .5rem; }
.color-row input[type="color"] { width: 2.4rem; height: 2rem; border: 1px solid var(--border); border-radius: 8px; padding: 2px; background: #fff; cursor: pointer; }
.color-row input[type="text"] { flex: 1; font-family: inherit; font-size: .85rem; padding: .45rem .6rem;
  border: 1px solid var(--border); border-radius: 8px; text-transform: uppercase; }

.toggle-row { display: flex; align-items: center; justify-content: space-between; width: 100%;
  font-family: inherit; font-size: .9rem; color: var(--text); background: #fff; cursor: pointer;
  border: 1px solid var(--border); border-radius: 8px; padding: .55rem .7rem; }
.toggle-switch { width: 38px; height: 22px; border-radius: 999px; background: var(--border); position: relative; transition: background .15s; }
.toggle-switch::after { content: ""; position: absolute; top: 2px; left: 2px; width: 18px; height: 18px;
  border-radius: 50%; background: #fff; box-shadow: 0 1px 2px rgba(0,0,0,.2); transition: transform .15s; }
.toggle-row[aria-pressed="true"] .toggle-switch { background: var(--accent); }
.toggle-row[aria-pressed="true"] .toggle-switch::after { transform: translateX(16px); }

.panel-action { display: inline-flex; align-items: center; justify-content: center; gap: .45rem;
  font-family: inherit; font-size: .88rem; font-weight: 500; padding: .6rem 1rem; cursor: pointer;
  border: 1px solid var(--border); border-radius: 8px; background: #fff; color: var(--text); }
.panel-action:hover { border-color: var(--accent); color: var(--accent); }
.panel-action .icon { width: 18px; height: 18px; }

/* Preview + on-canvas caption selection */
.preview { position: relative; width: 100%; max-width: 420px; aspect-ratio: 9 / 16; height: auto;
  background: #000; border-radius: 12px; overflow: hidden; }
.preview video { width: 100%; height: 100%; object-fit: contain; }
#caption-overlay { position: absolute; inset: 0; pointer-events: none; }
/* left/right = 70/1080 (the burn's MarginL/R) so the preview wraps at the same
   relative width as the burn. The old 6.5% + `padding: 6px 4px` was ~8px
   narrower than the burn's 940px usable width, because `* { box-sizing:
   border-box }` makes that padding eat INTO the 87%. Horizontal padding is now
   0 so the wrap width is the real one. */
.cap { position: absolute; left: 6.4815%; right: 6.4815%; text-align: center; pointer-events: auto;
  cursor: ns-resize; color: #fff; text-shadow: 0 2px 4px rgba(0,0,0,.7); padding: 6px 0; font-weight: 500; }
/* pre-line is load-bearing, not cosmetic: without it the cue's stored newlines
   COLLAPSE INTO SPACES and Chrome re-wraps the text from scratch with its ICU
   Thai dictionary. That made the preview look correctly broken no matter what
   was actually stored — so a bad break could never be seen before export, which
   is exactly how one survived. Locked by web/tests/css_caption.test.js. */
.cap .cap-text { pointer-events: none; white-space: pre-line; }
.cap.en { color: #eee; font-size: .8em; font-weight: 400; }

/* ElevenLabs-style selection box: outline + corner handles + rotation nub */
.cap.active { outline: 1.5px solid var(--accent); border-radius: 4px; background: rgba(37,99,235,.06); }
.cap .grip, .cap .rot-handle { display: none; pointer-events: none; }
.cap.active .grip { display: block; position: absolute; width: 9px; height: 9px; background: #fff;
  border: 1.5px solid var(--accent); border-radius: 50%; }
.cap.active .grip-tl { top: -5px; left: -5px; } .cap.active .grip-tr { top: -5px; right: -5px; }
.cap.active .grip-bl { bottom: -5px; left: -5px; } .cap.active .grip-br { bottom: -5px; right: -5px; }
.cap.active .rot-handle { display: block; position: absolute; top: -20px; left: 50%; transform: translateX(-50%);
  width: 10px; height: 10px; background: #fff; border: 1.5px solid var(--accent); border-radius: 50%; }
.cap.active .rot-handle::before { content: ""; position: absolute; top: 9px; left: 50%; transform: translateX(-50%);
  width: 1.5px; height: 11px; background: var(--accent); }

/* User text overlays (Phase F): click empty canvas -> affordance -> creates one */
#text-overlay { position: absolute; inset: 0; pointer-events: none; }
.text-overlay-el { position: absolute; transform: translate(-50%, -50%); white-space: pre;
  cursor: move; pointer-events: auto; padding: 2px 5px; text-shadow: 0 2px 4px rgba(0,0,0,.7); }
.text-overlay-el.active { outline: 1.5px dashed var(--accent); border-radius: 4px; background: rgba(37,99,235,.08); }
.add-text-btn { position: absolute; transform: translate(-50%, -50%); pointer-events: auto;
  font-size: .78rem; padding: .35rem .7rem; border-radius: 999px; border: 1.5px dashed var(--accent);
  background: rgba(37,99,235,.12); color: var(--accent); cursor: pointer; white-space: nowrap; }
.add-text-btn:hover { background: rgba(37,99,235,.22); }

/* Selected-cue editor */
.muted { color: var(--text-muted); font-weight: 400; font-size: .8rem; }
.panel textarea { width: 100%; font-family: inherit; font-size: .9rem; padding: .5rem .65rem; resize: vertical;
  border: 1px solid var(--border); border-radius: 8px; background: #fff; color: var(--text); }
.panel textarea:focus, .panel input[type="number"]:focus { outline: none; border-color: var(--accent); }
.time-row { display: flex; align-items: center; gap: .5rem; }
.time-row input[type="number"] { width: 5.5rem; font-family: inherit; font-size: .85rem; padding: .4rem .5rem;
  border: 1px solid var(--border); border-radius: 8px; }
.cue-actions { display: flex; gap: .5rem; }
.mini-btn { flex: 1; font-family: inherit; font-size: .82rem; padding: .5rem; cursor: pointer;
  border: 1px solid var(--border); border-radius: 8px; background: #fff; color: var(--text); }
.mini-btn:hover { border-color: var(--accent); color: var(--accent); }
.mini-btn.danger:hover { border-color: #dc2626; color: #dc2626; }
.mini-btn.active { background: var(--accent); border-color: var(--accent); color: #fff; }

/* Which step's controls the panel is showing (step-scoped journey) */
.journey-title { font-size: .72rem; font-weight: 600; letter-spacing: .03em;
  text-transform: uppercase; color: var(--accent); padding-bottom: .5rem;
  border-bottom: 1px solid var(--border); margin-bottom: .25rem; }
.journey-hint { font-size: .8rem; line-height: 1.5; color: var(--text-muted);
  background: var(--panel-bg); border: 1px solid var(--border);
  border-radius: 8px; padding: .6rem .7rem; }

/* Shot-inspector media-swap tabs + picker grid */
#media-tabs { flex-wrap: wrap; margin-bottom: .5rem; }
#media-tabs .mini-btn { flex: 0 0 auto; padding: .35rem .6rem; font-size: .78rem; }
#image-thumb-video { width: 100%; border-radius: 8px; margin-bottom: .5rem; }
.media-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: .4rem; margin-top: .5rem; }
.media-cell { padding: 0; border: 1px solid var(--border); border-radius: 8px; overflow: hidden; cursor: pointer;
  aspect-ratio: 9 / 16; background: #f4f4f5; }
.media-cell:hover { border-color: var(--accent); }
.media-cell:disabled { opacity: .5; cursor: default; }
.media-cell img { width: 100%; height: 100%; object-fit: cover; display: block; }
.media-grid-msg { font-size: .8rem; color: var(--text-muted); grid-column: 1 / -1; }

/* Transport + multi-lane timeline */
.timeline-wrap { border-top: 1px solid var(--border); --lane-h: 58px; }
/* Drag the top edge to grow/shrink the timeline (scales every lane via --lane-h). */
.tl-resize { display: flex; align-items: center; justify-content: center; height: 9px; cursor: ns-resize;
  background: var(--panel-bg); border-bottom: 1px solid var(--border); }
.tl-resize:hover { background: var(--accent-soft); }
.tl-resize-grip { width: 34px; height: 3px; border-radius: 999px; background: #c4c4cc; }
.tl-resize:hover .tl-resize-grip { background: var(--accent); }
.transport { display: flex; align-items: center; gap: .6rem; padding: .45rem .9rem; border-bottom: 1px solid var(--border); }
.t-btn { width: 30px; height: 30px; border-radius: 50%; border: 1px solid var(--border); background: #fff; cursor: pointer; font-size: .7rem; color: var(--text); }
.t-btn:hover { border-color: var(--accent); color: var(--accent); }
.t-time { font-size: .8rem; color: var(--text-muted); font-variant-numeric: tabular-nums; }
.t-spacer { flex: 1; }
.tl-zoom { display: flex; align-items: center; gap: .4rem; color: var(--text-muted); }
.zoom-btn { width: 22px; height: 22px; border: 1px solid var(--border); border-radius: 6px; background: #fff; cursor: pointer; font-size: .78rem; color: var(--text); }
.zoom-btn:hover { border-color: var(--accent); color: var(--accent); }
.tl-zoom input[type="range"] { width: 88px; accent-color: var(--accent); }

.tl-grid { display: grid; grid-template-columns: 104px 1fr; }
.tl-gutter { border-right: 1px solid var(--border); background: var(--panel-bg); }
.tl-gutter-ruler { height: 22px; border-bottom: 1px solid var(--border); }
.tl-lane-label { display: flex; align-items: center; gap: .4rem; height: var(--lane-h); padding: 0 .7rem; border-bottom: 1px solid var(--border);
  font-size: .72rem; color: var(--text-muted); font-weight: 600; }
.tl-lane-label svg { width: 15px; height: 15px; }

.tl-scroll { overflow-x: auto; overflow-y: hidden; scrollbar-width: thin; }
.tl-inner { position: relative; min-width: 100%; }
/* The run-up spacers. 0 wide unless the phone turns them on, so desktop keeps
   click-to-seek against a strip that starts at its own left edge. */
/* The run-up reads as "outside the clip", not as an empty box someone forgot
   to fill — the owner saw the blank strip and asked what was wrong with it.
   A flat tint plus the same hairline the lanes use is enough to say "nothing
   lives here"; anything busier competes with the film for attention. */
.tl-pad { flex: 0 0 var(--tl-pad, 0px); background: #f4f4f5;
  border-bottom: 1px solid var(--border); }
.tl-scroll { display: flex; }
.tl-inner { flex: 0 0 auto; }
.tl-ruler { position: relative; height: 22px; border-bottom: 1px solid var(--border); background: #fafafa; }
.tl-ruler .tick { position: absolute; top: 0; font-size: .62rem; color: var(--text-muted); padding-left: 3px; border-left: 1px solid var(--border); height: 7px; line-height: 10px; }
/* The tick at the very end of the clip: its line stays on the boundary, its
   label reads back INTO the timeline. Measured 25px of page-level overflow on
   iOS from this one label, which scrolled the whole app right and clipped the
   stepper on the left. `translateX(-100%)` moves only the box, so the line
   itself does not move off the boundary it marks. */
.tl-ruler .tick.tick-end { transform: translateX(-100%);
  padding-left: 0; padding-right: 3px;
  border-left: none; border-right: 1px solid var(--border); }
.tl-lane { position: relative; height: var(--lane-h); border-bottom: 1px solid var(--border); }
.tl-lane-media { background: repeating-linear-gradient(90deg,#f4f4f5,#f4f4f5 1px,transparent 1px,transparent 40px); }
.tl-viewonly { position: absolute; top: 8px; right: 8px; z-index: 2; font-size: .58rem; color: #e5e7eb; background: rgba(0,0,0,.35); padding: 1px 6px; border-radius: 999px; }

.tl-cap { position: absolute; top: 7px; bottom: 7px; border-radius: 8px; background: #ede9fe; border: 1.5px solid #a78bfa;
  box-shadow: 0 1px 2px rgba(0,0,0,.06); padding: 4px 8px; overflow: hidden; cursor: grab; }
.tl-cap.sel { background: #c7d2fe; border-color: var(--accent); box-shadow: 0 2px 6px rgba(37,99,235,.2); }
.tl-cap.flash { background: #fde68a; border-color: #f59e0b; }
.tl-cap .th { font-size: .68rem; color: #4c1d95; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.tl-cap .en { font-size: .6rem; color: #6d28d9; opacity: .75; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.tl-cap.sel .th { color: #1e3a8a; } .tl-cap.sel .en { color: #2563eb; }

/* Text-overlay blocks (Phase I): amber, to read as a different family from the
   violet caption blocks. "T" glyph + truncated text, same trim/move affordance. */
.tl-text { position: absolute; top: 7px; bottom: 7px; border-radius: 8px; background: #fef3c7; border: 1.5px solid #f59e0b;
  box-shadow: 0 1px 2px rgba(0,0,0,.06); padding: 4px 8px; overflow: hidden; cursor: grab;
  display: flex; align-items: center; gap: 5px; }
.tl-text.sel { background: #fde68a; border-color: var(--accent); box-shadow: 0 2px 6px rgba(37,99,235,.2); }
.tl-text .t-ico { flex: none; font-weight: 800; font-size: .72rem; color: #b45309; line-height: 1; }
.tl-text .t-label { font-size: .68rem; color: #92400e; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.tl-text.sel .t-ico { color: #1e3a8a; } .tl-text.sel .t-label { color: #1e3a8a; }

/* AI image blocks (Nano Banana storyboard): green family. Ghost (dashed, no
   fill) = planned shot awaiting generation; solid + thumbnail bg once a PNG
   exists on disk. */
.tl-img { position: absolute; top: 7px; bottom: 7px; border-radius: 8px; background: #d1fae5 center/cover no-repeat;
  border: 1.5px solid #10b981; box-shadow: 0 1px 2px rgba(0,0,0,.06); padding: 4px 8px; overflow: hidden;
  cursor: grab; display: flex; align-items: flex-end; }
.tl-img.ghost { background-color: transparent; border-style: dashed; }
.tl-img.sel { border-color: var(--accent); box-shadow: 0 2px 6px rgba(37,99,235,.2); }
.tl-img .i-label { font-size: .62rem; font-weight: 700; color: #065f46; background: rgba(255,255,255,.75);
  padding: 0 4px; border-radius: 4px; white-space: nowrap; }
.tl-img.ghost .i-label { color: #047857; }
.tl-img.sel .i-label { color: #1e3a8a; }

/* AI image preview: full-frame fills the canvas; overlay floats at its x/y/w.
   pointer-events off (B7 adds drag-to-reposition once the shot-plan drawer lands). */
#image-overlay { position: absolute; inset: 0; pointer-events: none; }
.image-overlay-el.full { position: absolute; inset: 0; }
.image-overlay-el.full img { width: 100%; height: 100%; object-fit: cover; }
.image-overlay-el.overlay { position: absolute; cursor: move; pointer-events: auto; }
.image-overlay-el.overlay img { width: 100%; display: block; border-radius: 6px; }
.image-overlay-el.active { outline: 2px dashed var(--accent); }

/* Explicit height (not top+bottom): a <canvas> is a replaced element with an
   intrinsic 90x160 ratio, and setting only its width (positionThumbs) makes the
   browser derive height from that ratio (162px), overflowing the 58px lane and
   ignoring top/bottom. An explicit height pins the box; object-fit crops. */
.tl-thumb { position: absolute; top: 7px; height: calc(var(--lane-h) - 14px); object-fit: cover; opacity: .92; border-radius: 3px; }
.tl-playhead { position: absolute; top: 0; bottom: 0; width: 2px; background: #ef4444; z-index: 4; pointer-events: none; }

/* ===== DESKTOP: panel sits to the LEFT of the preview (ElevenLabs editor shell) ===== */
@media (min-width: 768px) {
  header { padding: .85rem 1.25rem; }
  header h1 { font-size: 1.05rem; }
  .btn-export { font-size: .9rem; padding: .55rem 1rem; }

  /* Desktop app shell fills the REAL viewport height — not a guessed
     constant. Header and the timeline are fixed-height chrome; main gets
     however much vertical space is actually left on the user's screen,
     the same way the ElevenLabs shell does (its canvas isn't a fixed px
     figure either — it's "whatever's left between the header and the
     transport bar"). Mobile is untouched: this block only applies >=768px. */
  .app { height: 100vh; }
  .workspace { display: flex; flex-direction: column; height: 100vh; }
  header { flex-shrink: 0; }
  .timeline-wrap { flex-shrink: 0; }

  main { flex: 1; min-height: 0; flex-direction: row; align-items: stretch; padding: 1.5rem 1rem; }
  /* Only the config panel scrolls internally when its content is taller than
     the real available height — canvas/video position and size are never
     affected by how long the panel's content is (min-height:0 is what lets
     a stretched flex item actually clip+scroll instead of growing to fit
     its content and dragging the row taller with it). Width is fluid
     (clamp) rather than a rigid 260px — a rigid panel+preview combo forced
     a hard ~750px minimum before anything at all could fit, so any real
     desktop window narrower than that (not unusual — an unmaximized
     browser, a half-screen split) fell through to the mobile layout even
     though it's clearly a desktop context. */
  .panel { width: clamp(200px, 24vw, 260px); flex-shrink: 0; min-height: 0; overflow-y: auto;
    scrollbar-width: thin; scrollbar-color: #d4d4d8 transparent; }
  /* scrollbar-width:thin alone renders an invisible/near-invisible thumb in
     Chromium — on common shorter laptop screens (1366x768 etc.) that made a
     genuinely-scrollable panel look broken/cut off with no visible affordance
     that "แปลเป็น ENG" and other fields exist below. Explicit thumb color
     (both APIs) makes the scrollbar itself the "more content below" signal. */
  .panel::-webkit-scrollbar { width: 6px; }
  .panel::-webkit-scrollbar-thumb { background: #d4d4d8; border-radius: 3px; }
  .panel::-webkit-scrollbar-thumb:hover { background: #a1a1aa; }
  /* Canvas: a flexible workspace stage the preview floats in — fills the width
     the panel doesn't use, instead of leaving dead space (matches the
     ElevenLabs editor's proportions: rail/panel narrow, canvas dominant).
     Height comes from the real viewport (via main's flex:1 above), not a
     hardcoded number, so it genuinely fits the desktop at any window size. */
  .canvas { flex: 1; display: flex; align-items: center; justify-content: center;
    margin-left: 1.5rem; padding: 1.25rem; background: var(--panel-bg); border-radius: 12px; min-height: 0; min-width: 0; }
  /* Preview: EXACTLY 9:16 (aspect-ratio, not a hardcoded box), grows to FILL the
     canvas height (canvas padding gives breathing room) so a large/full-screen
     window shows a big preview instead of a small 512px box floating in dead
     space. `max-width: 100%` still guards against horizontal overflow on a
     narrow canvas; height is bounded by the canvas, so it never pushes layout. */
  .preview { aspect-ratio: 9 / 16; height: 100%; width: auto; max-width: 100%;
    border-radius: 10px; flex-shrink: 1; }

  /* Docked review column: in-flow flex child of <main>, so opening it shrinks
     the canvas (flex:1) instead of covering it. `main .review-chat` outranks the
     base `.review-chat` rule (which appears later in the file). */
  main .review-chat { position: static; max-height: none; min-height: 0; flex-shrink: 0;
    width: clamp(260px, 24vw, 320px); margin-left: 1rem; border-top: none;
    border-left: 1px solid var(--border); border-radius: 12px; box-shadow: none; }
  main .transcript-panel { position: static; max-height: none; min-height: 0; flex-shrink: 0;
    width: clamp(280px, 26vw, 360px); margin-left: 1rem; border-top: none;
    border-left: 1px solid var(--border); border-radius: 12px; box-shadow: none; }
}

/* A narrower desktop window still deserves the rail/panel/canvas shell, not
   a fallback to the mobile stacked layout — only true small/phone widths
   should stack. Rail/timeline-gutter tighten slightly to give the fluid
   panel/canvas above a bit more room to breathe at the low end. */
@media (min-width: 560px) and (max-width: 767px) {
  .rail { display: flex; flex-direction: column; align-items: center; gap: .5rem;
    width: 60px; flex-shrink: 0; padding: .7rem .3rem; border-right: 1px solid var(--border); background: var(--panel-bg); }
  .rail-brand { width: 28px; height: 28px; display: grid; place-items: center; color: var(--accent); margin-bottom: .3rem; }
  .rail-brand svg { width: 20px; height: 20px; }
  .rail-item { display: flex; flex-direction: column; align-items: center; gap: .2rem; width: 100%;
    font-family: inherit; font-size: .6rem; color: var(--text-muted); background: none; border: none;
    border-radius: 10px; padding: .45rem .15rem; cursor: pointer; }
  .rail-item svg { width: 19px; height: 19px; }
  .rail-item.active { color: var(--accent); background: var(--accent-soft); }
  .rail-item.mobile-only { display: none; }
  #btn-find-toggle { display: none; }
  .rail-soon { margin-top: auto; display: flex; flex-direction: column; align-items: center; gap: .15rem;
    font-size: .55rem; color: var(--text-muted); text-align: center; }
  .rail-soon-mark { width: 22px; height: 22px; display: grid; place-items: center; border: 1.5px dashed var(--border);
    border-radius: 7px; color: var(--text-muted); font-size: .9rem; }

  .app { height: 100vh; }
  .workspace { display: flex; flex-direction: column; height: 100vh; min-width: 0; }
  header { flex-shrink: 0; padding: .65rem .8rem; }
  header h1 { font-size: .95rem; }
  .clip-picker { max-width: 130px; }
  .timeline-wrap { flex-shrink: 0; }

  main { flex: 1; min-height: 0; display: flex; flex-direction: row; align-items: stretch; padding: 1rem .6rem; }
  .panel { width: clamp(170px, 30vw, 220px); max-width: none; flex-shrink: 0; min-height: 0; overflow-y: auto;
    scrollbar-width: thin; scrollbar-color: #d4d4d8 transparent; }
  .panel::-webkit-scrollbar { width: 6px; }
  .panel::-webkit-scrollbar-thumb { background: #d4d4d8; border-radius: 3px; }
  .canvas { flex: 1; display: flex; align-items: center; justify-content: center;
    margin-left: 1rem; background: var(--panel-bg); border-radius: 12px; min-height: 0; min-width: 0; }
  .preview { aspect-ratio: 9 / 16; height: min(512px, 100%); width: auto; max-width: 100%;
    border-radius: 10px; flex-shrink: 1; }
}

/* AI review chat drawer */
/* Mobile: a capped bottom sheet (never covers the whole screen). Desktop docks
   it as a flex column inside <main> (override below) so the canvas shrinks
   instead of being covered. */
/* Transcript line-list (fast text editing) — same two-mode dock pattern as the
   review chat: fixed bottom sheet on mobile, in-flow right column on desktop. */
.transcript-panel { position: fixed; left: 0; right: 0; bottom: 0; max-height: 60vh;
  background: #fff; border-top: 1px solid var(--border); display: flex; flex-direction: column; z-index: 20;
  box-shadow: 0 -8px 24px rgba(0,0,0,.08); }
.transcript-panel[hidden] { display: none; }
/* Review tools strip (find/replace + filler flagging) above the cue list */
.review-tools { padding: .6rem .6rem 0; display: flex; flex-direction: column; gap: .4rem; }
.review-tools .time-row { gap: .4rem; }
.review-tools input[type="text"] { flex: 1; min-width: 0; font-family: inherit; font-size: .82rem;
  padding: .4rem .5rem; border: 1px solid var(--border); border-radius: 8px; }
.review-tools .mini-btn { flex: 0 0 auto; }
.review-tools-row { display: flex; align-items: center; justify-content: space-between; gap: .5rem; min-height: 1.6rem; }
.review-tools-note { font-size: .76rem; color: var(--text-muted); }
/* A cue containing a hesitation sound — flagged, never auto-changed */
.t-row.t-filler .t-th { border-color: #f59e0b; background: #fffbeb; }

/* Word chips (click to hear that exact moment). Present on every row but only
   revealed for the selected one, so the list stays compact and selecting a
   line needs no re-render. */
.t-words { display: none; }
/* grid-column is load-bearing, not cosmetic. `.t-row` is `auto 1fr` and
   `.t-time` spans rows 1-2, so auto-placement drops this strip into column 1 —
   the `auto` one — where its own width sizes the column and starves the `1fr`
   caption inputs. Measured: 9 Thai chips gave `298.2px 13.2px`, i.e. 13px-wide
   inputs you cannot type in. Spanning the row keeps it out of that track. */
.t-row.active .t-words { grid-column: 1 / -1;
  display: flex; flex-wrap: wrap; gap: .2rem; margin-top: .1rem; }
.t-word { font-family: inherit; font-size: .74rem; line-height: 1.2; cursor: pointer;
  padding: .15rem .35rem; border: 1px solid var(--border); border-radius: 5px;
  background: #fff; color: var(--text-muted); }
.t-word:hover { border-color: var(--accent); color: var(--accent); }

.transcript-rows { flex: 1; overflow-y: auto; padding: .6rem; display: flex; flex-direction: column; gap: .45rem; }
.t-row { display: grid; grid-template-columns: auto 1fr; gap: .1rem .55rem; padding: .45rem .55rem;
  border: 1px solid var(--border); border-radius: 8px; background: #fff; cursor: pointer; }
.t-row.active { border-color: var(--accent); background: var(--accent-soft); }
.t-time { font-size: .68rem; color: var(--text-muted); padding-top: .3rem; white-space: nowrap; grid-row: span 2; }
.t-row input { width: 100%; border: none; background: transparent; font-family: inherit;
  font-size: .85rem; padding: .12rem .25rem; border-radius: 4px; }
.t-row input:focus { outline: none; background: #fff; box-shadow: 0 0 0 2px var(--accent); }
.t-row input.t-en { font-size: .75rem; color: var(--text-muted); }
/* AI-fixed line (clean pass): amber tint + ↩ revert until the human verifies. */
.t-row.t-fixed { border-left: 3px solid #f59e0b; }
.t-row.t-fixed input.t-th { background: #fefce8; }
.t-revert { display: block; margin-top: .2rem; border: none; background: none; cursor: pointer;
  color: #92400e; font-size: .8rem; padding: 0; }
.review-chat { position: fixed; left: 0; right: 0; bottom: 0; max-height: 60vh;
  background: #fff; border-top: 1px solid var(--border); display: flex; flex-direction: column; z-index: 20;
  box-shadow: 0 -8px 24px rgba(0,0,0,.08); }
.review-chat[hidden] { display: none; }  /* [hidden] must beat the flex display above */
.review-head { display: flex; align-items: center; justify-content: space-between; padding: .8rem 1rem;
  border-bottom: 1px solid var(--border); font-weight: 600; font-size: .9rem; }
.review-close { border: none; background: none; font-size: 1rem; cursor: pointer; color: var(--text-muted); }
.review-messages { flex: 1; overflow-y: auto; padding: 1rem; display: flex; flex-direction: column; gap: .8rem; }
.review-msg { display: flex; flex-direction: column; gap: .4rem; }
.review-msg.user { align-items: flex-end; }
.review-bubble { max-width: 100%; padding: .55rem .7rem; border-radius: 10px; font-size: .85rem; line-height: 1.45;
  white-space: pre-wrap; }
.review-msg.assistant .review-bubble { background: var(--panel-bg); border: 1px solid var(--border); }
.review-msg.user .review-bubble { background: var(--accent-soft); border: 1px solid var(--accent); color: var(--accent); }
.suggestion { display: flex; align-items: center; gap: .5rem; padding: .5rem .6rem; border: 1px solid var(--border);
  border-radius: 8px; background: #fff; }
.suggestion-text { flex: 1; font-size: .8rem; line-height: 1.4; }
.suggestion-apply { flex-shrink: 0; font-family: inherit; font-size: .8rem; padding: .35rem .7rem; cursor: pointer;
  border: 1px solid var(--accent); border-radius: 999px; background: var(--accent); color: #fff; }
.suggestion-apply:disabled { background: var(--text-muted); border-color: var(--text-muted); cursor: default; }
.suggestion-apply.apply-all { align-self: stretch; margin-top: .2rem; padding: .5rem; font-weight: 600; }
/* "AI กำลังคิด" — three bouncing dots while a review call is in flight. */
.review-bubble.typing-dots { display: flex; align-items: center; gap: 4px; padding: .7rem .8rem; }
.review-bubble.typing-dots span { width: 6px; height: 6px; border-radius: 50%; background: var(--text-muted);
  animation: typing-bounce 1.1s infinite ease-in-out; }
.review-bubble.typing-dots span:nth-child(2) { animation-delay: .15s; }
.review-bubble.typing-dots span:nth-child(3) { animation-delay: .3s; }
@keyframes typing-bounce { 0%, 60%, 100% { transform: translateY(0); opacity: .5; } 30% { transform: translateY(-4px); opacity: 1; } }
@media (prefers-reduced-motion: reduce) {
  .review-bubble.typing-dots span { animation: none; opacity: .8; }
}
/* Next-step nudge (e.g. "แปลก่อนมั้ย?") — two side-by-side choices, no default. */
.nudge-actions { display: flex; gap: .5rem; margin-top: .5rem; }
.nudge-actions .suggestion-apply { flex: 1; text-align: center; }
.nudge-actions .nudge-skip { background: #fff; color: var(--text); border-color: var(--border); }
.nudge-actions .nudge-skip:hover { border-color: var(--accent); color: var(--accent); }
.review-input { display: flex; gap: .5rem; padding: .7rem; border-top: 1px solid var(--border); }
.review-input input { flex: 1; font-family: inherit; font-size: .85rem; padding: .5rem .6rem;
  border: 1px solid var(--border); border-radius: 8px; }
.review-input button { font-family: inherit; font-size: .85rem; padding: .5rem .9rem; cursor: pointer;
  border: none; border-radius: 8px; background: var(--accent); color: #fff; }

/* AI image style intake: preset chips (style, then density) + optional free text. */
.style-chip-row { display: flex; flex-wrap: wrap; gap: .4rem; margin: .4rem 0; }
.style-chip { font-family: inherit; font-size: .78rem; padding: .3rem .65rem; cursor: pointer;
  border: 1px solid var(--border); border-radius: 999px; background: #fff; color: var(--text); }
.style-chip:hover { border-color: var(--accent); color: var(--accent); }
.style-chip.active { background: var(--accent); border-color: var(--accent); color: #fff; }
.style-chip:disabled { opacity: .6; cursor: default; }
.style-freetext { display: block; width: 100%; font-family: inherit; font-size: .8rem; padding: .45rem .6rem;
  margin: .3rem 0 .5rem; border: 1px solid var(--border); border-radius: 8px; box-sizing: border-box; }
.style-freetext:focus { outline: none; border-color: var(--accent); }

/* Shot (image) inspector thumbnail + mode-toggle active state */
#image-thumb { width: 100%; border-radius: 8px; margin-bottom: .6rem; display: block; }
#btn-image-mode-full.active, #btn-image-mode-overlay.active { background: var(--accent); border-color: var(--accent); color: #fff; }

/* ElevenLabs-style AI cards + section labels */
.ai-card { display: flex; align-items: center; gap: .65rem; width: 100%; text-align: left; font-family: inherit;
  padding: .6rem; border: 1px solid var(--border); border-radius: 10px; background: #fff; color: var(--text); cursor: pointer; }
.ai-card:hover { border-color: var(--accent); }
.ai-card:disabled { opacity: .55; cursor: default; }
.ai-ico { width: 32px; height: 32px; border-radius: 8px; background: var(--accent-soft); color: var(--accent);
  display: grid; place-items: center; flex-shrink: 0; }
.ai-ico svg { width: 17px; height: 17px; }
.ai-txt { display: flex; flex-direction: column; }
.ai-title { font-size: .82rem; font-weight: 600; }
.ai-sub { font-size: .72rem; color: var(--text-muted); }

/* ---------- start screen ----------
   Covers the app until a clip is chosen. The app used to auto-open clips[0],
   so it always launched holding an old file. Uses `hidden` for its own
   visibility, which the global [hidden]{display:none !important} rule enforces
   — do NOT give this a competing explicit display. */
/* A PAGE inside the shell, not an overlay on top of it. It used to be
   `position: fixed; inset: 0; z-index: 70`, which painted over the rail — so
   the rail's own #btn-logout was unreachable from Home and a proxy button had
   to duplicate the handler just to let the owner sign out of the wrong
   account. Every future rail button would have needed the same proxy. As a
   flex sibling of .workspace the rail simply stays where it is.

   `start center`, not `center`: centring was fine when this card held four
   small tool tiles; holding a clip library it only centres in the case that
   matters most — a brand-new account with no clips — and pushed the card down
   206px on a 375x812 phone with nothing to scroll. */
.start-screen { flex: 1; min-width: 0; display: grid; place-items: start center;
  background: var(--bg, #f6f7f9); padding: 1.5rem; overflow: auto; }
/* 1120px, was 760: the card now holds a clip grid rather than four small tool
   tiles, and at 760 a 1280px screen showed three columns where five fit. */
.start-card { width: min(1120px, 100%); background: #fff; border: 1px solid var(--border);
  border-radius: 14px; padding: 1.6rem 1.5rem; box-shadow: 0 8px 30px rgba(0,0,0,.07); }
.start-title { margin: 0; font-size: 1.35rem; }
.start-upload { display: block; text-align: center; font-size: .85rem; font-weight: 600;
  padding: .7rem; border: 1px dashed var(--border); border-radius: 10px; cursor: pointer;
  color: var(--accent); margin-bottom: 1.2rem; }
.start-upload:hover { border-color: var(--accent); background: var(--accent-soft); }
.start-status { font-size: .78rem; color: var(--text-muted); margin-top: .8rem; min-height: 1.1em; }

/* ---- workspace Home ----
   Rebuilt 2026-08-13 against 8 real product home screens + 6 project-library
   pages. What changed and why is in
   docs/superpowers/specs/2026-08-13-home-after-login-design.md — the short
   version: 7/8 leaders render work as a thumbnail grid with a status badge and
   a relative time, 0/8 put spend or AI stats on the home screen, and every one
   that lost a blind comparison lost for the same reason: several equal-weight
   entry points and no single primary action. */
.home-top { display: flex; align-items: center; gap: .8rem; flex-wrap: wrap; }
.home-top-right { margin-left: auto; display: flex; align-items: center; gap: .5rem; }
.home-new-btn { display: inline-flex; align-items: center; gap: .4rem; border: 0;
  background: var(--accent); color: var(--on-accent); border-radius: 9px; cursor: pointer;
  padding: .55rem .95rem; font: inherit; font-weight: 600; font-size: .9rem;
  box-shadow: var(--shadow-card); }
.home-new-btn:hover { filter: brightness(1.06); }

/* scan handle: search + status chips */
.home-tools-row { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap;
  margin: 1.1rem 0 0; }
.home-search { display: inline-flex; align-items: center; gap: .4rem; flex: 1 1 190px;
  min-width: 0; max-width: 320px; border: 1px solid var(--border); background: var(--bg);
  border-radius: 9px; padding: .4rem .6rem; }
.home-search svg { width: 16px; height: 16px; color: var(--text-muted); flex: none; }
.home-search input { border: 0; outline: 0; background: none; font: inherit;
  font-size: .88rem; color: inherit; width: 100%; min-width: 0; }
.home-search:focus-within { border-color: var(--accent); }
/* Chips scroll sideways rather than wrap: wrapping cost 109px of vertical space
   at a 320px viewport (three lines) and pushed the first card to y=273. The
   overflow lives inside this element, so the page still never scrolls sideways. */
.home-chips { display: flex; gap: .35rem; flex-wrap: nowrap; overflow-x: auto;
  scrollbar-width: none; min-width: 0; }
.home-chips::-webkit-scrollbar { display: none; }
.home-chip { border: 1px solid var(--border); background: var(--bg); color: var(--text-muted);
  border-radius: 999px; padding: .35rem .7rem; font: inherit; font-size: .82rem;
  cursor: pointer; white-space: nowrap; }
.home-chip:hover { border-color: var(--accent); color: var(--accent); }
.home-chip.is-on { background: var(--accent-soft); border-color: var(--accent);
  color: var(--accent); font-weight: 600; }

/* clip grid. 130px, not 150: at a 320px viewport the content box is 288px and
   two 150px columns plus the .9rem gap needs 314.4px, so wider minimums silently
   dropped small phones to one card per row and made the page 2665px tall. */
.home-grid { display: grid; gap: .9rem; margin-top: 1.1rem;
  grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); }
@media (min-width: 768px) {
  .home-grid { gap: 1.1rem; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); }
}
.home-clip { display: flex; flex-direction: column; gap: .55rem; text-align: left;
  border: 0; background: none; padding: 0; font: inherit; color: inherit; cursor: pointer; }
.home-thumb { position: relative; aspect-ratio: 9 / 16; border-radius: 12px;
  border: 1px solid var(--border); background: var(--panel-bg); overflow: hidden;
  box-shadow: var(--shadow-card); transition: box-shadow .15s, transform .15s; }
.home-clip:hover .home-thumb { box-shadow: var(--shadow-lift); transform: translateY(-2px); }
.home-thumb video { width: 100%; height: 100%; object-fit: cover; pointer-events: none;
  display: block; }
.home-dur { position: absolute; right: .4rem; bottom: .4rem; background: var(--scrim);
  color: var(--on-accent); font-size: .72rem; padding: .1rem .35rem; border-radius: 5px; }
.home-badge { position: absolute; left: .4rem; top: .4rem; font-size: .72rem;
  font-weight: 600; padding: .15rem .45rem; border-radius: 999px; border: 1px solid;
  box-shadow: 0 0 0 1px var(--chip-ring); }
.home-badge.is-warn { color: var(--warn); background: var(--warn-soft); border-color: var(--warn-border); }
.home-badge.is-ok   { color: var(--ok);   background: var(--ok-soft);   border-color: var(--ok-border); }
.home-badge.is-idle { color: var(--text-muted); background: var(--panel-bg); border-color: var(--border); }
.home-name { font-size: .88rem; font-weight: 500; line-height: 1.3;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.home-when { font-size: .78rem; color: var(--text-muted); }
.home-none { color: var(--text-muted); font-size: .9rem; padding: 2rem 0; }

/* no clips yet — replaces the grid, never sits under it */
.home-empty { border: 2px dashed var(--border); border-radius: 14px; background: var(--panel-bg);
  padding: 3rem 1.25rem; text-align: center; margin-top: 1.1rem; }
.home-empty h2 { margin: .6rem 0 .3rem; font-size: 1.1rem; }
.home-empty p { margin: 0 0 1.1rem; color: var(--text-muted); font-size: .9rem; }
.home-empty svg { width: 40px; height: 40px; color: var(--text-muted); }
/* Whole-page drop target: Descript is the one leader that leads with a
   dropzone, and it does so on a screen with no project list. With a list to
   show, the page itself takes the drop and the button is the click affordance. */
.start-screen.is-dropping .home-empty,
.start-screen.is-dropping .home-grid { outline: 2px dashed var(--accent); outline-offset: 6px; }

/* Export file name (step 5) */
#out-name { width: 100%; font-family: inherit; font-size: .9rem; padding: .5rem .65rem;
  border: 1px solid var(--border); border-radius: 8px; background: #fff; color: var(--text); }
#out-name:focus { outline: none; border-color: var(--accent); }
.out-name-hint { font-size: .72rem; color: var(--text-muted); margin-top: .35rem; }

/* Brand kit editor (step 4) */
#brandkit-group textarea { width: 100%; font-family: inherit; font-size: .82rem; line-height: 1.45;
  padding: .5rem .6rem; border: 1px solid var(--border); border-radius: 8px;
  background: #fff; color: var(--text); resize: vertical; }
#brandkit-group textarea:focus { outline: none; border-color: var(--accent); }

/* Motion-look chips reuse .style-chip; the overlay needs a transform origin
   that matches ffmpeg's zoompan, which scales about the frame centre. */
.image-overlay-el { transform-origin: center center; }

/* A cue line longer than the burn's budget. thaiwrap emits it over-width and
   WrapStyle 0 lets libass re-break it with no Thai dictionary, so what burns
   can differ from what the preview shows. Warning only — never alters text. */
.t-row.t-over .t-th { box-shadow: inset 2px 0 0 #92400e; }
/* #d97706 with white text measured 3.19:1 — under the 4.5:1 floor for text
   this small. #92400e (the house warning colour) gives 7.0:1. */
.t-over-badge { margin-left: 6px; padding: 0 4px; border-radius: 3px;
  background: #92400e; color: #fff; font-size: 10px; font-variant-numeric: tabular-nums; }

/* ---------- carousel ---------- */
/* Reuses .glossary-modal (border/shadow/backdrop/max-height) plus the same
   .review-head + .ai-panel-body split, so padding and scrolling come from the
   established pattern rather than being re-invented. Two independent reviews
   caught the first version inheriting `padding: 0` with no body wrapper, which
   put Thai copy flush against the border. */
.carousel-modal { width: min(780px, 92vw); }
.car-body { display: flex; flex-direction: column; }
.carousel-modal textarea { width: 100%; font: inherit; line-height: 1.45;
  padding: .5rem .65rem; border: 1px solid var(--border); border-radius: 8px;
  resize: vertical; background: #fff; color: var(--text); }
.carousel-modal textarea:focus { outline: none; border-color: var(--accent); }
/* Thai above/below vowels and tone marks need room. Every rule here that can
   carry Thai sets a line-height on the house 1.45 step — the first version left
   .car-slide .th (the ONLY Thai-copy rule) with none while its English sibling
   had 1.5, which is backwards. */
.car-hint { color: var(--text-muted); font-size: .72rem; line-height: 1.45; margin: 0 0 .6rem; }
.car-hint code { background: var(--panel-bg); padding: 2px 5px; border-radius: 4px; }
.car-label { font-size: .72rem; line-height: 1.45; color: var(--text-muted);
  display: block; margin: .6rem 0 .25rem; }
.car-row { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
.car-row .car-label { margin: 0; }
.car-row input, .car-row select { font: inherit; font-size: .82rem; padding: .35rem .5rem;
  border-radius: 8px; border: 1px solid var(--border); background: #fff; color: var(--text); }
.car-row input { width: 78px; }
.car-note { color: var(--text-muted); font-size: .72rem; line-height: 1.45; margin: .4rem 0 0; }
.car-sections { margin-top: .35rem; font-size: .72rem; line-height: 1.45; color: var(--accent); }
.car-sections.bad { color: #92400e; }
.car-breakdown { font-size: .72rem; line-height: 1.45; color: var(--text-muted); margin-top: .35rem; }
.car-breakdown.warn { color: #92400e; }
.car-actions { display: flex; align-items: center; gap: .5rem; margin-top: .8rem; flex-wrap: wrap; }
.car-actions .mini-btn { flex: 0 0 auto; }
.car-status { font-size: .72rem; line-height: 1.45; color: var(--text-muted); margin-top: .5rem; }
.car-status.bad { color: #92400e; }
.car-plan-out { margin-top: .8rem; border-top: 1px solid var(--border); padding-top: .7rem; }
/* Dimmed while the form has moved on — the plan is still readable, but it no
   longer describes what the form would produce. */
.car-plan-out.stale { opacity: .45; }
.car-style { font-size: .72rem; line-height: 1.45; color: var(--text-muted); margin-bottom: .6rem; }
.car-slides { display: grid; gap: .4rem; }
.car-slide { border: 1px solid var(--border); border-radius: 8px; padding: .5rem .65rem; }
.car-slide .role { font-size: .68rem; line-height: 1.45; color: var(--accent); font-weight: 600; }
.car-slide .th { font-size: .82rem; line-height: 1.45; margin: .15rem 0; }
.car-slide .en { font-size: .68rem; line-height: 1.45; color: var(--text-muted); }
/* --car-ar comes from the chosen aspect so tiles match the requested shape. */
.car-results { margin-top: .8rem; display: grid; gap: .5rem;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); }
.car-cell { border: 1px solid var(--border); border-radius: 8px; overflow: hidden;
  background: var(--panel-bg); }
.car-cell img { display: block; width: 100%; aspect-ratio: var(--car-ar, 1 / 1);
  object-fit: cover; }
/* NOT `.cap` — that is the burn-preview caption overlay. */
.car-cap { font-size: .68rem; line-height: 1.45; padding: .35rem .5rem; color: var(--text-muted); }
/* A slide that FAILED is an error, not a warning: the house error red, not the
   amber used for "heads up". */
.car-cell.failed { border-color: #dc2626; }
.car-cell .err { font-size: .68rem; line-height: 1.45; color: #dc2626; padding: .5rem; }

/* Unsaved caption work — must be impossible to miss. */
#status.status-bad { color: #b91c1c; font-weight: 600; }

/* ---------- cost page ---------- */
/* Billing-page shape everyone already reads: total → category bars → ranking. */
.cost-modal { width: min(560px, 92vw); }
.cost-head { padding: .4rem 0 .9rem; border-bottom: 1px solid var(--border); margin-bottom: .9rem; }
.cost-total { font-size: 2.1rem; font-weight: 700; font-variant-numeric: tabular-nums; line-height: 1.15; }
.cost-sub { font-size: .78rem; color: var(--text-muted); margin-top: .2rem; }
.cost-sec { font-size: .72rem; letter-spacing: .05em; text-transform: uppercase;
  color: var(--text-muted); font-weight: 600; margin: .9rem 0 .45rem; }
.cost-row { display: grid; grid-template-columns: 9.5rem 1fr auto; gap: .6rem;
  align-items: center; padding: .28rem 0; font-size: .82rem; }
.cost-row.clip { grid-template-columns: 1fr auto; }
.cost-row .k { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cost-row .track { height: 10px; border-radius: 5px;
  background: color-mix(in srgb, var(--accent) 10%, transparent); }
.cost-row .bar { height: 100%; border-radius: 5px; background: var(--accent); min-width: 2px; }
.cost-row .v { font-variant-numeric: tabular-nums; color: var(--text-muted); white-space: nowrap; }
.cost-empty { font-size: .78rem; color: var(--text-muted); padding: .3rem 0; }
.cost-note { font-size: .68rem; color: var(--text-muted); line-height: 1.5;
  border-top: 1px solid var(--border); margin-top: 1rem; padding-top: .7rem; }

/* ---------- brand kit dialog (Home's door in) ---------- */
.bk-label { display: block; font-size: .74rem; font-weight: 600; margin: .6rem 0 .2rem; }
#brandkit-modal textarea { width: 100%; font: inherit; font-size: .8rem; padding: .45rem .55rem;
  border: 1px solid var(--border); border-radius: 8px; resize: vertical; }
.home-top-right { display: flex; align-items: center; gap: .6rem; }

/* ---------- asset library dialog ---------- */
/* .inp: one generic single-line text input for a dialog that isn't nested in
   .panel (`.panel input[type="text"]` doesn't reach here) — same visual
   treatment as #brandkit-modal's textarea, so this doesn't read as a second
   input style in the app. */
.inp { display: block; width: 100%; font: inherit; font-size: .85rem;
  padding: .45rem .6rem; border: 1px solid var(--border); border-radius: 8px;
  background: #fff; color: inherit; margin-bottom: .4rem; }
.inp:focus { outline: none; border-color: var(--accent); }
#assets-body textarea { width: 100%; font: inherit; font-size: .8rem; padding: .45rem .55rem;
  border: 1px solid var(--border); border-radius: 8px; resize: vertical; margin-bottom: .4rem; }
.asset-thumb { display: block; width: 100%; max-width: 160px; aspect-ratio: 1;
  object-fit: cover; border: 1px solid var(--border); border-radius: 8px; margin-bottom: .4rem; }

/* Gaps: stretches with no caption at all. Two kinds, two actions — "dropped"
   holds words the recogniser produced and lost downstream (recoverable from
   data already on disk), "unheard" holds nothing (re-transcribe or type it).
   Styled from the existing seven tokens only: a CSS audit on this project
   already found 41 raw hex colours bypassing them, and this is not the place
   to make it 42. The two kinds are told apart by a LABEL and a left border
   weight, never by colour alone. */
.gap-strip { padding: .5rem .6rem; display: flex; flex-direction: column; gap: .3rem;
  border-block-end: 1px solid var(--border); background: var(--panel-bg); }
.gap-strip-head { font-size: .78rem; color: var(--text-muted); }
.gap-strip-head b { color: var(--text); }
.gap-item { display: flex; align-items: baseline; gap: .45rem; width: 100%;
  text-align: start; font: inherit; font-size: .8rem; cursor: pointer;
  background: var(--bg); color: var(--text);
  border: 1px solid var(--border); border-inline-start-width: 3px;
  border-radius: 6px; padding: .35rem .5rem; }
/* .gap-item used to BE the button; it is now a row holding the seek surface
   (.gap-go) plus its own action (.gap-add), because a button inside a button is
   invalid markup. The hover moves to the seek surface so the row does not light
   up when the pointer is over the action. */
.gap-item { align-items: center; cursor: default; padding-inline-end: .35rem; }
.gap-go { flex: 1; min-width: 0; display: flex; align-items: baseline; gap: .45rem;
  text-align: start; font: inherit; font-size: .8rem; cursor: pointer;
  background: none; border: none; color: inherit; padding: 0; }
.gap-item:has(.gap-go:hover) { border-color: var(--accent); }
.gap-go:hover { color: var(--accent); }
.gap-add { flex: none; }
.gap-item[data-kind="unheard"] { border-inline-start-color: var(--border); }
.gap-item[data-kind="dropped"] { border-inline-start-color: var(--accent); }
.gap-item .gap-at { font-variant-numeric: tabular-nums; color: var(--text-muted);
  flex: none; font-size: .75rem; }
.gap-item .gap-tag { flex: none; font-size: .72rem; font-weight: 700; }
.gap-item[data-kind="dropped"] .gap-tag { color: var(--accent); }
.gap-item[data-kind="unheard"] .gap-tag { color: var(--text-muted); }
.gap-item .gap-text { flex: 1; min-width: 0; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; color: var(--text-muted); }

/* ---------- AI activity strip + list (web/ai_log.js) ---------- */
/* One line, always in the same place, so "is the AI doing anything?" is
   answered without hunting. Tokens use the existing type tokens rather than a
   new scale. The first draft of this block invented --line/--card/--ink/
   --danger, none of which this file defines, so every one would have fallen
   back to a browser default: exactly the failure the post-v3 CSS audit found
   from the other direction (41 raw hex colours bypassing 7 tokens). The
   tokens that exist are --bg --panel-bg --border --text --text-muted
   --accent --accent-soft; #dc2626 is the red .mini-btn.danger already uses. */
.ai-activity {
  display: inline-flex; align-items: center; gap: 8px;
  margin-left: 10px; padding: 4px 10px;
  border: 1px solid var(--border); border-radius: 999px;
  background: var(--panel-bg); color: var(--text-muted);
  font: inherit; font-size: 12px; cursor: pointer;
}
.ai-activity:hover { border-color: var(--accent); color: var(--text); }
.ai-activity-dot {
  width: 7px; height: 7px; border-radius: 50%; background: var(--text-muted); flex: none;
}
.ai-activity.busy .ai-activity-dot { background: var(--accent); animation: ai-pulse 1s ease-in-out infinite; }
.ai-activity.failed .ai-activity-dot { background: #dc2626; }
@keyframes ai-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .25; } }
@media (prefers-reduced-motion: reduce) {
  .ai-activity.busy .ai-activity-dot { animation: none; }
}
.ai-activity-rows { display: flex; flex-direction: column; gap: 6px; padding: 1rem; }
#ai-activity-modal .out-name-hint { padding: 0 1rem 1rem; }
.ai-row {
  display: grid; grid-template-columns: minmax(9rem, 1fr) auto; gap: 2px 12px;
  padding: 8px 10px; border: 1px solid var(--border); border-radius: 8px;
}
.ai-row.running { border-color: var(--accent); }
.ai-row.failed { border-color: #dc2626; }
.ai-row-name { font-weight: 600; }
.ai-row-what, .ai-row-meta { font-size: 12px; color: var(--text-muted); }
.ai-row-meta { text-align: right; }
.ai-row-err { grid-column: 1 / -1; font-size: 12px; color: #dc2626; }

/* The two structural caption gestures, stated where the editing happens.
   Same muted-footer treatment the gap strip and the AI panel already use —
   no new colour, no new scale. */
.transcript-keys {
  display: flex; flex-wrap: wrap; gap: .2rem 1rem;
  padding: .4rem .6rem; font-size: .72rem; color: var(--text-muted);
  border-block-end: 1px solid var(--border);
}
.transcript-keys kbd {
  font: inherit; font-size: .68rem; padding: .05rem .3rem;
  border: 1px solid var(--border); border-block-end-width: 2px;
  border-radius: 4px; background: var(--bg); color: var(--text);
}

/* The flow as a checklist, above the per-call rows. Elicit's Status panel:
   ticked steps keep the result they produced, the running one is marked, the
   ones not reached yet are visibly not reached. */
.ai-step {
  display: grid; grid-template-columns: 1.2rem minmax(7rem, 1fr) auto;
  gap: .4rem; align-items: baseline;
  padding: .3rem .1rem; font-size: .8rem;
}
.ai-step.waiting { color: var(--text-muted); }
.ai-step.running .ai-step-name { font-weight: 600; }
.ai-step-mark { text-align: center; color: var(--text-muted); }
.ai-step.done .ai-step-mark { color: var(--accent); }
.ai-step.running .ai-step-mark { color: var(--accent); }
.ai-step.failed .ai-step-mark, .ai-step.failed .ai-step-meta { color: #dc2626; }
.ai-step-meta { text-align: right; font-size: .74rem; color: var(--text-muted); }

/* ---------- learning curve (web/learn_chart.js) ---------- */
/* Dumbbell, one row per clip, oldest at the top. Two categorical hues,
   validated for this light surface with the dataviz validator:
     #2563eb (หลัง AI) / #b45309 (ก่อน AI) — CVD ΔE 31.3 protan, 27.3 tritan,
     normal-vision ΔE 34.6, both ≥ 3:1 on the surface. ALL CHECKS PASS.
   The app has no dark mode (no prefers-color-scheme rule anywhere in this
   file), so only the light palette is specified — deliberately, not skipped. */
.learn-hero { font-size: 1.15rem; font-weight: 600; margin: .2rem 0 .1rem; }
.learn-hero .learn-hero-sub { display: block; font-size: .78rem; font-weight: 400; color: var(--text-muted); }
.learn-legend { display: flex; flex-wrap: wrap; gap: .1rem .9rem; font-size: .72rem; color: var(--text-muted); margin-bottom: .4rem; }
.learn-key { display: inline-flex; align-items: center; gap: .3rem; }
.learn-key-note { color: var(--text-muted); }
.learn-dot { width: 9px; height: 9px; border-radius: 50%; display: inline-block; }
.learn-dot.before { background: #b45309; }
.learn-dot.after  { background: #2563eb; }

.learn-chart { display: flex; flex-direction: column; gap: .1rem; }
.learn-row { display: grid; grid-template-columns: 10rem 1fr 3.4rem; gap: .5rem; align-items: center; }
.learn-row-name { font-size: .72rem; color: var(--text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.learn-row-name b { color: var(--text); font-weight: 600; }
.learn-plot { position: relative; height: 22px; }
/* Hairline, solid, one shade off the surface — never dashed. */
.learn-grid { position: absolute; inset: 0; }
.learn-grid i { position: absolute; top: 0; bottom: 0; width: 1px; background: var(--border); }
.learn-bar { position: absolute; top: 50%; height: 2px; transform: translateY(-50%);
  background: color-mix(in srgb, #2563eb 30%, transparent); border-radius: 1px; }
.learn-bar.worse { background: color-mix(in srgb, #b45309 45%, transparent); }
.learn-pt { position: absolute; top: 50%; border-radius: 50%; transform: translate(-50%, -50%);
  /* 2px surface ring so overlapping markers stay countable — never a border
     drawn to separate marks. */
  box-shadow: 0 0 0 2px var(--bg); }
.learn-pt.before { background: #b45309; }
.learn-pt.after  { background: #2563eb; }
.learn-row-val { font-size: .74rem; text-align: right; font-variant-numeric: tabular-nums; color: var(--text); }
.learn-row.thin .learn-row-val::after { content: " ⚠"; color: var(--text-muted); }
.learn-axis { display: grid; grid-template-columns: 10rem 1fr 3.4rem; gap: .5rem; margin-top: .15rem; }
.learn-axis-ticks { position: relative; height: 1rem; font-size: .68rem; color: var(--text-muted); }
.learn-axis-ticks span { position: absolute; transform: translateX(-50%); font-variant-numeric: tabular-nums; }
.learn-table-wrap { margin-top: .5rem; font-size: .74rem; }
.learn-table-wrap summary { cursor: pointer; color: var(--text-muted); }
.learn-table table { border-collapse: collapse; width: 100%; margin-top: .3rem; }
.learn-table th, .learn-table td { text-align: right; padding: .2rem .35rem; border-bottom: 1px solid var(--border); font-variant-numeric: tabular-nums; }
.learn-table th:first-child, .learn-table td:first-child { text-align: left; font-variant-numeric: normal; }
/* The row label is two parts with different jobs: the clip identity may elide,
   the sample size never may — n is the secondary encoding the chart depends on
   and a clipped "8 บรร…" is the reader losing it. */
.learn-row-name { display: flex; gap: .3rem; align-items: baseline; }
.learn-row-name b { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.learn-row-n { flex: none; font-variant-numeric: tabular-nums; }
/* The interval sits under the point estimate, quieter than it but never
   optional — every source on unit-level rates requires the two to be read
   together. */
.learn-row-val { line-height: 1.15; }
.learn-row-ci { display: block; font-size: .62rem; color: var(--text-muted); font-weight: 400; }
/* A thin sample is made QUIETER, not hidden — the monitoring literature's rule
   is to suppress the alarm, not the data. */
.learn-row.thin { opacity: .55; }
.learn-warn { color: var(--text); margin-bottom: .3rem; }

/* ตั้ง API key เอง — ค่าคีย์ไม่เคยมาถึงหน้านี้ มีแค่สถานะกับ 4 ตัวท้าย */
.key-row { border-top: 1px solid var(--border); padding: .7rem 0; display: grid; gap: .3rem; }
.key-row:first-child { border-top: 0; }
.key-head { display: flex; align-items: center; gap: .5rem; }
.key-badge { font-size: .75rem; color: var(--text-muted); border: 1px solid var(--border);
  border-radius: 99px; padding: .05rem .5rem; }
.key-badge.on { color: var(--accent); border-color: var(--accent); }
.key-what { font-size: .8rem; color: var(--text-muted); }
.key-row input { font: inherit; padding: .45rem .6rem; border: 1px solid var(--border);
  border-radius: 8px; background: var(--bg); color: var(--text); }
.key-link { font-size: .78rem; color: var(--accent); text-decoration: none; }
.key-warn { font-size: .78rem; color: var(--text); background: var(--accent-soft);
  border-radius: 8px; padding: .4rem .55rem; }
/* top-right CTA: one pulse when the pipeline story advances (never looped) */
@keyframes cta-pulse {
  0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 55%, transparent); }
  100% { box-shadow: 0 0 0 14px transparent; }
}
.cta-pulse { animation: cta-pulse .9s ease-out 2; }

/* ── 2026-08-12 left-panel audit fixes (blind vs Descript/VEED/Riverside) ──
   Each step leads with its OWN primary card; the shared caption settings sink
   below a labelled divider. Flex order does the reordering so DOM stays put. */
aside.panel { display: flex; flex-direction: column; }
/* The ✕ has flex order 0, so once the title/action cards took negative orders
   it rendered mid-sheet between them — pin it into the title row instead
   (negative bottom margin lets the title flow up beside it). */
aside.panel > .panel-close { order: -11; margin-bottom: -40px; }
aside.panel > .journey-title { order: -10; padding-right: 44px; }
aside.panel > .journey-hint { order: -9; }
aside.panel[data-step="translate"] > #btn-translate { order: -1; }
aside.panel > #shared-label { order: 10; }
aside.panel > #recap-group { order: 11; }
aside.panel > #style-group { order: 12; }
.shared-label {
  margin-top: 1.1rem; padding-top: .9rem; border-top: 1px solid var(--border);
  font-size: .72rem; letter-spacing: .04em; color: var(--text-muted);
  text-transform: none;
  /* It is a <button> now (the phone uses it as the settings disclosure) —
     strip the UA chrome so the desktop keeps its plain divider-label look. */
  background: none; border-left: none; border-right: none; border-bottom: none;
  font-family: inherit; text-align: left; width: 100%; padding-left: 0;
  padding-right: 0; padding-bottom: 0; cursor: default;
}
/* section headers get real hierarchy: darker, bolder, breathing room */
.panel-title { font-weight: 600; color: var(--text); font-size: .8rem; }
.panel-group + .panel-group { margin-top: .4rem; padding-top: .8rem; border-top: 1px solid var(--border); }
/* the step whose controls the left panel is SHOWING (≠ pipeline's next step) */
.step-col.viewing .step-circle { outline: 2px solid var(--accent); outline-offset: 2px; }
/* checkbox labels hug their box — .field>label's space-between pushed them apart */
.field > label.check-row { justify-content: flex-start; }

/* preset tiles: each renders its own look on a video-dark stage */
.preset-tile { display: flex; flex-direction: column; gap: .3rem; padding: 0; border: 1px solid var(--border);
  border-radius: 10px; overflow: hidden; background: #fff; cursor: pointer; width: calc(50% - .25rem); }
.preset-stage { display: grid; place-items: center; height: 44px;
  background: linear-gradient(135deg, #2b2f36 0%, #14161a 70%), #14161a; }
.preset-sample { line-height: 1.35; white-space: nowrap; }
.preset-name { font-size: .68rem; color: var(--text-muted); padding: 0 .4rem .35rem; text-align: center; }
.preset-tile.active { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
.preset-tile.active .preset-name { color: var(--accent); font-weight: 600; }
.preset-row { display: flex; flex-wrap: wrap; gap: .5rem; }

/* ---- ซื้อเครดิต -----------------------------------------------------------
   `.review-head` เป็น flex ที่ปล่อยให้ลูกตัวสุดท้ายกินพื้นที่ที่เหลือ dialog
   อื่นหัวข้อยาวพอจนไม่เห็นผล แต่ "เติมเครดิต" สั้น ปุ่มปิดเลยยืดเต็มแถวจนดู
   เหมือนแถบ ไม่เหมือนปุ่ม — เห็นบนจอ ไม่ใช่จาก diff */
#buy-dialog .review-head > button { flex: 0 0 auto; }

/* QR กับข้อความรอบ ๆ อยู่กึ่งกลาง ของเดิมชิดซ้ายแล้วเหลือที่ว่างครึ่งขวา
   ทั้งที่ทั้งบล็อกมีแค่รูปสี่เหลี่ยมกับสองบรรทัด */
#buy-qr { text-align: center; }
#buy-qr img {
  display: block;
  margin: var(--space-3, 12px) auto;
  border-radius: 8px;
  background: #fff;   /* QR ต้องอยู่บนพื้นขาวเสมอ กล้องอ่านสลับสีไม่ได้ */
}

/* กลุ่มแพ็กเรียงลงมา ไม่ใช่ยืนข้างกัน
   เดิมเป็น `display:flex; flex-wrap:wrap` ตั้งแต่สมัยที่กล่องนี้มีปุ่มเรียบ ๆ 5 ปุ่ม
   พอเปลี่ยนเป็นการ์ดสองกลุ่ม กฎเก่าที่ค้างอยู่ทำให้สอง <section> ไปยืนข้างกัน
   หัวข้อสองอันเหลื่อมกัน 16px และเดสก์ท็อปเหลือที่ว่างขวามือ 108px */
#buy-plans { display: flex; flex-direction: column; gap: 1rem; }

/* ---------------------------------------------- กล่องเติมเครดิต: การ์ดแพ็ก
   ออกแบบจากหลักฐาน ไม่ใช่รสนิยม (วิจัย 2026-08-13, สรุปใน billing_ui.js):

   - เรียงลงมาแทนแถวเดียว: CXL วัดหน้า pricing มือถือ 78% ใช้การ์ดเรียงลง เฉลี่ย
     3.4 ตัวเลือก ส่วน carousel/แถวเลื่อนใช้กัน 8% และ 13% ของตารางที่เลื่อนได้
     ไม่มีสัญญาณบอกว่าเลื่อนได้เลย ของเดิมยัด 5 การ์ดแถวเดียวจนตัดคำกลางคำ
   - เป้ากดขั้นต่ำ 44px: WCAG 2.5.8 บังคับ 24px เป็นขั้นต่ำระดับ AA, Apple HIG
     และ Material อยู่ที่ 44-48 เลือกตัวสูงเพราะนิ้วโป้งคนไทยไม่ได้เล็กกว่าใคร
   - ใช้ token เดิมทั้งหมด ไม่มี hex ดิบ (บทเรียน CSS audit: 41 สีดิบเลี่ยง 7
     token มาแล้วครั้งหนึ่ง) */
.pack-group h4 { margin: 0 0 .45rem; font-size: .9rem; }
.pack-list { display: flex; flex-direction: column; gap: .5rem; }
.pack { width: 100%; display: grid; grid-template-columns: 1fr auto; align-items: center;
  gap: .1rem .6rem; min-height: 44px; padding: .6rem .8rem; cursor: pointer;
  font-family: inherit; text-align: left; background: var(--bg);
  border: 1px solid var(--border); border-radius: 10px; color: var(--text); }
.pack:hover { border-color: var(--accent); }
.pack-n { font-size: .95rem; font-weight: 600; }
.pack-price { font-size: 1.05rem; font-weight: 700; grid-column: 2; grid-row: 1; }
.pack-unit { font-size: .75rem; color: var(--text-muted); grid-column: 1; }
.pack-save { font-size: .72rem; color: var(--accent); grid-column: 2;
  justify-self: end; }
/* แพ็กที่เลือกอยู่ต้องอ่านออกโดยไม่ต้องพึ่งสีอย่างเดียว — เส้นขอบหนาขึ้น +
   พื้นหลังอ่อน + จุดเช็ค ทำงานได้ทั้งกับคนตาบอดสีและจอที่สีเพี้ยน */
.pack[aria-checked="true"] { border-color: var(--accent); border-width: 2px;
  background: var(--accent-soft); padding: calc(.6rem - 1px) calc(.8rem - 1px); }
.pack[aria-checked="true"] .pack-n::after { content: " ✓"; color: var(--accent); }

#buy-confirm { display: flex; flex-direction: column; gap: .4rem;
  margin-top: 1rem; }
/* ปุ่มจ่าย และ "ลิงก์ไปหน้าชำระเงิน" ต้องหน้าตาเดียวกัน — จังหวะที่ผูกมัดจริง
   คือลิงก์อันหลัง ไม่ใช่ปุ่มอันแรก มันเคยใส่คลาส .mini-btn ตัวเดียวกับปุ่ม "ปิด"
   (วัดได้ 201x35px) แปลว่าที่จังหวะสำคัญที่สุด "จ่าย" กับ "ยกเลิก" น้ำหนักเท่ากัน
   และเตี้ยกว่ากฎ 44px ที่ไฟล์นี้เขียนไว้เอง 9px */
.buy-go, a.buy-go { display: block; width: 100%; text-align: center;
  text-decoration: none; font-family: inherit; font-size: 1rem; font-weight: 600;
  min-height: 48px; padding: .7rem 1rem; cursor: pointer; color: #fff;
  background: var(--accent); border: 1px solid var(--accent);
  border-radius: 10px; }
.buy-go:hover { filter: brightness(1.08); }
/* แถบความน่าเชื่อถือต้องอยู่ "ติดปุ่ม" — Baymard วัดว่าความรู้สึกปลอดภัยมาจาก
   ตำแหน่งที่วางไอคอน/ข้อความเทียบกับจุดที่ต้องตัดสินใจ ไม่ใช่จากการมี HTTPS */
.buy-trust { margin: 0; font-size: .76rem; color: var(--text-muted); }

/* ยอดคงเหลือคือสมอของหน้าจอนี้ ไม่ใช่ข้อความประกอบ — คนตัดสินใจว่าจะเติม
   เท่าไหร่จาก "ตอนนี้เหลือเท่าไหร่" กล่องซื้อเครดิตของ CapCut เปิดด้วยตัวเลข
   ยอดคงเหลือที่ใหญ่ที่สุดบนจอด้วยเหตุผลเดียวกัน เดิมตัวนี้เป็น .9rem สีจาง
   คือเล็กกว่าราคาบนการ์ดทุกใบ (1.05rem หนา) กลับหัวกับที่ตั้งใจไว้ */
.buy-balance { margin: 0 0 1rem; font-size: 1.25rem; font-weight: 700;
  color: var(--text); }
.buy-balance .buy-balance-lead { display: block; font-size: .8rem;
  font-weight: 400; color: var(--text-muted); }
/* บรรทัดแยก "ซื้อไว้ / โควตาเดือนนี้" — เลขรวมข้างบนตอบไม่ได้ว่าอันไหนจะหาย */
.buy-balance .buy-balance-detail { display: block; margin-top: .2rem;
  font-size: .78rem; font-weight: 400; color: var(--text-muted); }

/* ใบเสร็จย้อนหลังเป็นของที่ต้องแตะจริง ไม่ใช่ข้อความประดับ — วัดได้ 15px
   ทั้งหัวข้อที่กางและลิงก์แต่ละใบ เตี้ยกว่ากฎ 44px ของไฟล์นี้เกือบสามเท่า */
#buy-dialog details > summary { min-height: 44px; display: flex;
  align-items: center; cursor: pointer; }
#buy-receipt-list { margin: 0; padding-left: 1.1rem; }
#buy-receipt-list li { min-height: 44px; display: flex; align-items: center; }
#buy-receipt-list a { min-height: 44px; display: flex; align-items: center; }

/* ที่ว่างท้ายรายการ เท่าความสูงปุ่มจ่ายที่ลอยทับ — ไม่งั้นการ์ดใบสุดท้าย
   เลื่อนยังไงก็ไม่พ้นปุ่ม (วัดได้ว่าโดนบัง 48 จาก 60px ที่จอ 1280x720) */
.pack-group:last-of-type { padding-bottom: 3.5rem; }
/* ปุ่มจ่ายติดขอบล่างของกล่องที่เลื่อนได้ — Baymard วัดบนมือถือว่าคีย์บอร์ดกิน
   จอครึ่งหนึ่งในแนวตั้ง และวิธีแก้ที่เห็นผลคือปุ่มยืนยันที่ไม่หลุดจอไปกับการ
   เลื่อน ไม่ใช่การย่อรายการให้สั้นลง */
#buy-confirm { position: sticky; bottom: 0; background: var(--bg);
  padding-top: .6rem; }
/* ปุ่มปิดของกล่องนี้เป็นทางออกทางเดียว (คลิกฉากหลังไม่ปิด) จึงต้องได้ 44px
   เท่ากับของอื่นในเส้นทาง ไม่ใช่ 35px แบบ .mini-btn ทั่วไป */
#buy-close { min-height: 44px; min-width: 44px; }
/* ทางกลับไปเลือกแพ็กใหม่ — กดจ่ายผิดแพ็กแล้วเดิมต้องปิดกล่องเปิดใหม่ */
.buy-back { background: none; border: 0; padding: .5rem 0; min-height: 44px;
  font-family: inherit; font-size: .85rem; color: var(--accent);
  text-align: left; cursor: pointer; }

/* กฎเก่า `#buy-plans { flex-wrap: wrap }` เคยอยู่ตรงนี้ — merge พามันกลับมา
   หลังกฎใหม่ จึงชนะ cascade และพาบั๊ก "สองกลุ่มยืนข้างกัน" กลับมาทั้งดุ้น
   ลบทิ้ง ตัวจริงอยู่ข้างบนบรรทัด #buy-plans { flex-direction: column } */

/* ===== MOBILE FIT (gauntlet round 1) ==================================== */

/* Drags own these surfaces. Without touch-action Safari runs its own pan/zoom
   in parallel with every pointer drag — the page scrolls while a cue is being
   trimmed. Harmless on desktop (mouse ignores touch-action). #tl-scroll still
   scrolls horizontally: the empty-space drag handler scrubs, which is the
   mobile-category gesture (fixed playhead, content moves) — native pan would
   fight it. */
#tl-scroll, #tl-resize, .cap, .text-overlay-el, .image-overlay-el.overlay { touch-action: none; }

/* Burn progress ON the preview (CapCut export screen: big % over the video).
   Desktop keeps the filling button instead — hidden entirely >=768px. */
.burn-overlay { position: absolute; inset: 0; z-index: 6; display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: .4rem;
  background: rgba(0, 0, 0, .55); border-radius: inherit; }
.burn-overlay[hidden] { display: none; }
/* The clip, held on screen for the wait. Sized to leave room for the buttons
   under it rather than to a fixed height — a 9:16 reel at 60vh already fills a
   phone. */
.burn-thumb { height: 46vh; max-width: 72vw; border-radius: 12px;
  object-fit: contain; box-shadow: 0 10px 34px rgba(0,0,0,.45); }
.burn-thumb[hidden] { display: none; }
/* 1.75rem = 28px on a 375px phone. Sized from measured competitor export
   screens, not taste: CapCut ~28px and Meta's Edits ~24px both lay the
   percentage over the video thumbnail as a SECONDARY element, because the
   thumbnail is what the user is actually waiting for. 3.2rem shipped first
   and owned the screen — measured at a 375px viewport: font-size 51.2px,
   line box 66.5px, the string "45%" 111px wide = 47.7% of the overlay's own
   width (55.8% of `.preview`). That is the owner's "ตัวเลข % ใหญ่เกินไป".
   Apple HIG and Material 3 specify no numeric size at all (Material's
   progress component has no text slot; Carbon calls the percentage "helper
   text"), so the band comes from the competitors. Locked by
   web/tests/css_burn_overlay.test.js. */
.burn-overlay-pct { font-size: 1.75rem; font-weight: 700; color: #fff; font-variant-numeric: tabular-nums; }
.burn-overlay-note { font-size: .85rem; color: #e5e7eb; }
/* Base scope, NOT inside the phone block. These two are the only way a user
   gets their finished work off the server, and desktop now sees them. Locked
   by web/tests/css_burn_overlay.test.js. */
.burn-download, .burn-srt { font: inherit; font-size: .9rem; font-weight: 600;
  color: #1d4ed8; background: #fff; border: none; border-radius: 999px;
  padding: .55rem 1rem; text-decoration: none; cursor: pointer; }
/* `font: inherit` is load-bearing, not tidiness: a <button> does NOT inherit
   font-family from body, and this file has no global button reset — every
   button class declares it individually. Without it the Thai label rendered
   in Arial next to an Anuphan pill (measured by a critic before shipping). */
/* Where the file landed, for the path that cannot reach Photos in one tap.
   Kapwing ships the same sentence; it is the only thing that answers "ผมเองยัง
   หาไม่เจอเลย" when the share sheet is unavailable. Sits under the buttons and
   wraps — it is a sentence, not a label. */
.burn-where { margin: .1rem 0 0; max-width: 30ch; text-align: center;
  font-size: .78rem; line-height: 1.6; color: rgba(255,255,255,.82); }
.burn-where b { color: #fff; font-weight: 600; }
/* The captions are the second choice here — the video is what was just made. */
.burn-srt { background: rgba(255,255,255,.16); color: #fff; }
/* Desktop hides the overlay WHILE burning — the filling button already tells
   that story on a big screen. It must NOT hide the success state: the only
   control that pulls the finished mp4 off the server (#burn-download) lives
   in here, and a blanket hide left every desktop user with no way to get
   their video. That was invisible while output_dir() defaulted to the
   operator's own ~/Downloads (the file just appeared on their disk); on the
   deployed server it lands on the VPS instead. Owner, 2026-08-13: "ตอนนี้
   เวลาใช้ desktop burn เสร็จไฟล์ก็ไม่อยู่ใน download".
   `.is-done` is set by showBurnDownload and cleared by setBurnProgress. */
@media (min-width: 768px) { .burn-overlay:not(.is-done) { display: none; } }
/* The success state must sit ABOVE #btn-expand-preview (z-index 7), which is
   a sibling and covers the overlay's own ✕ exactly: a critic probed 25 points
   over the close button on a phone and reached it at 0 of them, so the
   success overlay could not be dismissed by its own control. */
.burn-overlay.is-done { z-index: 8; }

.preview-expand { display: none; }   /* phone-only — shown in the mobile block */

/* Touch twins of the split/merge shortcuts — desktop keeps only the kbd hints. */
.transcript-touch-keys { display: flex; gap: .5rem; padding: .5rem .6rem 0; }
.transcript-touch-keys .mini-btn { flex: 1; min-height: 44px; }
@media (min-width: 768px) { .transcript-touch-keys { display: none; } }

@media (max-width: 767px) {
  /* The page itself does not pan sideways. CONTAINMENT, not a diagnosis — say
     so plainly: measured from the owner's iPhone with ?diag=1 on three separate
     reports, `scale peak 1.00` every time (the page never zoomed) with 25px of
     overflow and the page scrolled 25px right, which is what cut the stepper
     and the clip name off on the LEFT. Chromium reproduces none of it, in any
     state including mid-edit with the caption sheet open, so whatever lays out
     25px wider is something only Safari does and local measurement has not
     found it.
     Refusing the pan costs nothing here: everything in this app that is
     deliberately wider than the screen — the timeline, the action strips —
     carries its own scroller, so nothing becomes unreachable. It removes a
     symptom the owner has now hit four times while the hunt continues.
     `overflow-x: clip` would be the modern choice, but it is not safe next to
     the position:fixed sheets and overlays this layout depends on; `hidden`
     creates a scroll container that those are already living with. */
  html, body { overflow-x: hidden; }
  /* The rail reborn as a bottom tab bar. Below 560px it used to be
     display:none outright, which severed the ONLY doors to the transcript
     list and Home. Three tabs survive: Home, คำบรรยาย, รีวิว AI — everything
     else is a desktop power feature or is switched off by CLIP_ONLY. (Home
     used to carry tool cards that opened the rest; the clip grid replaced
     them.) Home no longer overlays this bar — it is a sibling page inside the
     shell, so the bar stays on screen there too and Home must reserve room for
     it (below). */
  .rail { display: flex; position: fixed; left: 0; right: 0; bottom: 0; z-index: 60;
    flex-direction: row; justify-content: space-around; align-items: stretch;
    background: #fff; border-top: 1px solid var(--border);
    padding: .1rem .5rem calc(.1rem + env(safe-area-inset-bottom)); }
  /* Reserve the bar's own height under Home's content. Measured 48px tall and
     fixed to the bottom; Home's generic 1.5rem padding left only a 29px gap
     below the last card, which clears today only because of where this
     library's content happens to end. Reserve the bar, not a coincidence. */
  .start-screen { padding-bottom: calc(48px + 1.5rem + env(safe-area-inset-bottom)); }
  /* The bar STAYS while a sheet is open. Hiding it (tried, CapCut-style) was
     reverted the same day: the owner's first real-device reaction was "เมนู
     หายไป จะกดไปอีกหน้าทำไม่ได้" — tab-to-tab switching matters more here
     than the ~50px of video it costs, because the tabs are how the three
     sheets swap. */
  .rail-brand, .rail-soon, .rail-item[aria-current],
  /* #btn-logout is NOT in this list. It used to be, and that was survivable
     only because Home carried a proxy button — Home painted over the rail, so
     the rail's own logout was unreachable anyway. Home is a sibling page now
     and the proxy is gone, which left a phone with no way out of the wrong
     account at all: measured display:none at 375px with nothing else bound to
     signOut. One button, reachable at every width, beats a second button that
     has to be kept in sync. */
  /* The bar carries DESTINATIONS only. Three items came out, each for its own
     reason, and all three stayed reachable somewhere better:
       · #btn-home — leaving the clip is not a peer of switching panels, and a
         mis-tap abandoned the work. Now the ‹ at the top left of what you are
         leaving (#btn-back).
       · #btn-logout — one tap ended the session, same size, right next to the
         controls used every minute. Now with the account settings, and it asks.
       · #btn-review-toggle — not in the MVP at all (owner: "ซ่อนไปก่อน ยังไม่
         เปิดใน MVP"), so it was spending a slot on nothing.
     Two left: คำบรรยาย and ปรับแต่ง — which is what the owner said he actually
     reaches for. Fewer, bigger, and none of them can lose his work. */
  #btn-home, #btn-logout, #btn-review-toggle { display: none; }
  #btn-glossary, #btn-carousel, #btn-assets, #btn-keys, #btn-cost { display: none; }
  /* Back sits where iOS puts back: the top-left of the view being left. */
  .btn-back { display: block; order: -1; width: 40px; min-height: 40px; margin-left: -.25rem;
    border: none; background: none; color: var(--accent); font-size: 1.6rem;
    line-height: 1; cursor: pointer; font-family: inherit; }
  /* Sign-out reads as what it is — a way out, not an action to reach for. */
  /* order, not source position: `aside.panel` pulls its title and action cards
     to NEGATIVE orders, so a child left at the default 0 floats above them —
     measured, sign-out rendered between the sheet title and the step's own
     action card. A way out of the account belongs at the floor of the panel. */
  .logout-m { order: 99; display: block; width: 100%; margin-top: 1.2rem; min-height: 44px;
    border: 1px solid var(--border); border-radius: 10px; background: none;
    font-family: inherit; font-size: .85rem; color: var(--text-muted); cursor: pointer; }
  .rail-item { display: flex; flex-direction: column; align-items: center; justify-content: center;
    gap: .1rem; flex: 1; min-height: 44px; font-family: inherit; font-size: .6rem;
    color: var(--text-muted); background: none; border: none; border-radius: 10px;
    padding: .15rem .2rem; cursor: pointer; }
  .rail-item svg { width: 19px; height: 19px; }
  .rail-item.active { color: var(--accent); }
  .rail-dot { top: 2px; right: calc(50% - 14px); }

  /* The bottom sheets dock on top of the tab bar rather than underneath.
     One height for all three sheets AND the preview formula below — change it
     in one place or the frame and the sheet drift apart. */
  /* dvh, not vh: on iPhone Safari vh includes the space BEHIND the bottom URL
     bar (~100px), so a 52vh sheet overflowed the visible screen — reported as
     "เมนูขึ้นมาบังหน้าจอ" on a real device. Chromium emulation could never
     catch this (dvh == vh there). The vh line stays as fallback for engines
     without dvh. */
  /* svh, not dvh: dvh can re-derive while the iOS keyboard animates and the
     preview keeps the mid-animation height — the "ratio เพี้ยน" the owner saw
     after dismissing the keyboard. svh never moves. */
  :root { --sheet-h: 44vh; --sheet-h: 44svh;
    --tabbar-h: calc(47px + env(safe-area-inset-bottom)); }
  .transcript-panel, .review-chat { bottom: var(--tabbar-h); }
  /* The cue list owns the sheet: fixed height (not max-) so #transcript-rows'
     flex:1 always gets the leftover — it had shrunk to an 88px peephole
     showing one cue of thirty. */
  .transcript-panel { height: var(--sheet-h); }

  /* One-screen shell (CapCut / Instagram Edits): preview pinned, timeline
     docked, NOTHING scrolls. Before this the mobile editor was a ~2,400px
     scrolling document, so preview and timeline were never on screen together
     and every timeline drag was done blind. dvh over vh where supported: vh
     ignores Safari's collapsing URL bar. */
  .workspace { display: flex; flex-direction: column; height: 100vh; height: 100dvh;
    padding-bottom: calc(56px + env(safe-area-inset-bottom)); }
  header, .step-strip { flex-shrink: 0; }
  main { flex: 1; min-height: 0; overflow: hidden; padding: .5rem .9rem; }
  .canvas { flex: 1; min-height: 0; display: flex; align-items: center;
    justify-content: center; width: 100%; }
  .preview { height: 100%; width: auto; max-width: 100%; }
  .timeline-wrap { flex-shrink: 0; --lane-h: 40px; }

  /* The settings/journey panel becomes the third bottom sheet, opened by the
     ปรับแต่ง tab. Same dock geometry as the other two. Visibility rides the
     `hidden` ATTRIBUTE (syncPanelVisibility in studio.js), not a display rule:
     the global [hidden]{display:none!important} is the one mechanism this
     codebase has proven always wins the cascade. */
  .panel { position: fixed; left: 0; right: 0; bottom: var(--tabbar-h);
    z-index: 20; max-height: var(--sheet-h); overflow-y: auto; max-width: none;
    border-radius: 12px 12px 0 0; box-shadow: 0 -8px 24px rgba(0,0,0,.08); }
  .panel-close { position: sticky; top: -1rem; align-self: flex-end; z-index: 2;
    margin: -.5rem -.4rem 0 0; width: 40px; height: 40px; border: none; border-radius: 50%;
    background: var(--panel-bg); color: var(--text-muted); font-size: 1rem; cursor: pointer; }

  /* Compact top bar (CapCut keeps only a slim strip: picker + small Export).
     The two-row header with a full-width burn pill read as "ใหญ่เกิน" on the
     real phone — one tight row now. */
  header h1, #mode-badge { display: none; }
  header { padding: .4rem .7rem; gap: .4rem; flex-wrap: nowrap; }
  /* The burn group NEVER leaves the screen: header-left is the only side
     allowed to shrink. Found on prod, invisible locally — the logged-in
     header carries #quota-pill (owner mode never shows it), which pushed the
     burn button past the right edge: "กด burn ไม่ได้ ปุ่มหาไม่เจอ". Quota
     already lives on Home (#home-quota); the ? walkthrough is desktop-taught
     anyway — both yield their pixels to the one button that ships the clip. */
  .header-left { flex: 1 1 auto; min-width: 0; gap: .35rem; align-items: center; }
  .header-right { flex: 0 0 auto; }
  #quota-pill, #btn-help { display: none; }
  .clip-picker { flex: 1 1 auto; min-width: 0; max-width: 40vw; font-size: .76rem; padding: .3rem .4rem; }
  .btn-export { font-size: .78rem; padding: .42rem .7rem; white-space: nowrap; }
  .workspace { padding-bottom: var(--tabbar-h); }

  /* Burn success on the phone: ✓ + pull-the-file link, dismissible.
     The pill styling lives at base scope now (desktop shows this state too).
     A phone-only copy used to sit here with the identical values — deleted,
     because two rules that agree today are a drift waiting to happen. */
  /* Export takes the whole phone (owner 2026-08-14, with CapCut's export
     screens as the reference). It used to be clipped to `.preview` — a ~300px
     box — so the one moment the user actually waits through was the smallest
     thing on the screen, with the tab bar and timeline still competing for
     attention around it.
     `inset: 0` on a FIXED element, so the iOS URL bar cannot cut the bottom off
     the way a `height: 100vh` would. z-index clears the tab bar (60) and both
     bottom sheets; `border-radius: inherit` from the base rule has to go or the
     full-screen surface keeps the preview's rounded corners. */
  /* BOTH selectors, and `.is-done` is not decoration here: `.burn-overlay.is-done`
     (0,2,0) is declared at base scope with `z-index: 8` and OUTRANKS a plain
     `.burn-overlay` (0,1,0) no matter how far down the file this sits — media
     queries add no specificity. Measured with only the plain selector: the
     success screen computed z-index 8 and the tab bar (60) and coach mark (65)
     painted straight through a "full-screen" takeover. Third specificity trap
     of the day; the fix is always to MATCH the selector, never to move the rule. */
  .burn-overlay, .burn-overlay.is-done { position: fixed; inset: 0; z-index: 200;
    gap: .7rem; background: #0b1020; border-radius: 0;
    padding: calc(1rem + env(safe-area-inset-top)) 1rem
             calc(1rem + env(safe-area-inset-bottom));
    /* Measured at 375x667 (the shortest phone still in use) the stack ends at
       596px against 667 — it fits. `auto` is insurance for the cases the
       measurement cannot cover: a long out_name wrapping the note, a larger
       system font, or a future fourth button. Without it there is no fallback
       at all and the last control is simply unreachable, which on a screen with
       no other exit is the difference between cramped and stuck.
       `safe center` because plain `center` on an overflowing flex column pushes
       the first child ABOVE the scroll origin, where no scrollbar can reach it —
       the overflow rule would then hide the ✕ instead of rescuing it. */
    overflow-y: auto; justify-content: safe center; }
  /* Freed from the 300px box, the clip can be the subject the research in
     web/tests/css_burn_overlay.test.js says it should be. The percentage does
     NOT grow with it — measured off the owner's own CapCut screenshot, their
     "29.5%" is ~27pt on a 393pt device, still inside the 1.5-2rem band. */
  .burn-thumb { height: 42vh; }
  .burn-overlay-close { position: absolute; top: calc(.5rem + env(safe-area-inset-top));
    right: .6rem; width: 44px; height: 44px;
    border: none; border-radius: 50%; background: rgba(255,255,255,.18); color: #fff;
    font-size: 1rem; cursor: pointer; }

  /* Status painted OVER the stepper at 390px (position:absolute right) —
     give it its own centered line instead. justify-content:center on an
     overflowing flex container CLIPS both ends unreachably (the classic
     flexbox bug — first/last step were cut off with no way to scroll to
     them); flex-start + margin:auto centers when it fits and scrolls from
     the left edge when it doesn't. */
  .step-strip { flex-wrap: wrap; justify-content: flex-start; }
  .stepper { margin: 0 auto; }
  /* All four steps must FIT the phone width — the desktop geometry (88px
     columns + 42px connectors = 478px) overflowed a 375-390px screen and put
     ขั้น 3 แปล / 4 เบิร์น behind an invisible horizontal scroll ("ไม่โชว์
     ขั้น 3 แปล", real device). Columns shrink to their labels and the
     connectors absorb the leftover, so every width ≥ ~320px shows all four. */
  .stepper { width: 100%; }
  .step-col { width: auto; flex: 0 1 auto; min-width: 0; }
  .step-line { flex: 1 1 8px; width: auto; min-width: 6px; }
  .step-label { font-size: .68rem; }
  /* The phone label doubles as the settings disclosure — make it feel like one. */
  .shared-label { cursor: pointer; min-height: 44px; color: var(--text); font-weight: 600; }
  .step-strip #status { position: static; max-width: 100%; flex-basis: 100%;
    text-align: center; margin-top: .2rem; }

  /* Edit mode (a bottom sheet is open): CapCut's batch-edit shape — page
     scroll locked, chrome out of the way, and the WHOLE 9:16 frame shrunk to
     fit above the sheet so the caption being edited is visible on the video. */
  .transcript-panel, .review-chat { max-height: var(--sheet-h); }
  body.sheet-open { overflow: hidden; }
  /* step-strip STAYS while a sheet is open — hiding it (round 2, to buy the
     frame ~90px) cost the doors to แปล/เบิร์น: "คุณซ่อน step ไว้ มันเลยกด
     ทั้งแปล eng และ burn ไม่ได้" (owner, real device). Visible steps beat a
     bigger frame. */
  body.sheet-open .ai-activity { display: none; }
  /* While a sheet is up the frame parks at the TOP of the stage (not centered
     behind the sheet) and shrinks to the room the sheet leaves. */
  body.sheet-open .canvas { align-items: flex-start; }
  body.sheet-open .preview {
    height: calc(100vh - var(--sheet-h) - var(--tabbar-h) - 172px);
    height: calc(100svh - var(--sheet-h) - var(--tabbar-h) - 172px);
    width: auto; max-width: 100%; }

  /* Timeline: the 104px label gutter was 27% of a 390px screen — icons only.
     The 9px resize strip is un-grabbable by touch; 22px is. */
  .tl-grid { grid-template-columns: 44px 1fr; }
  .tl-lane-label { padding: 0; justify-content: center; }
  .tl-lane-label span { display: none; }
  .tl-resize { height: 22px; }

  /* The top progress bar carries upload/transcribe/burn on a phone — 3px is
     a desktop whisper; 6px reads from arm's length. */
  .job-bar { height: 6px; }

  /* iOS Safari AUTO-ZOOMS the page when a focused input's font is under
     16px — and never zooms back out, leaving the whole app oversized
     ("หน้าขยาย ไม่ fit กับหน้าจอ", real device). 16px on every text entry
     a finger can reach kills the trigger. */
  .t-row input, .review-tools input[type="text"], .panel input[type="text"],
  #review-text, .clip-picker, .dev-auth-input { font-size: 16px; }
  /* ...and the English box needs its OWN line, because the rule above cannot
     reach it. `.t-row input.t-en` (0,2,1) outranks `.t-row input` (0,1,1) and a
     media query adds no specificity, so the .75rem set 800 lines earlier kept
     winning and only the ENGLISH caption triggered the zoom — which is exactly
     how it was reported (the owner was editing the translated captions).
     Match the selector; do not try to out-position it. */
  .t-row input.t-en { font-size: 16px; }
  /* The two rules above are a hand-kept LIST, and a list only covers what
     someone remembered. Measured at 375px after they shipped: 21 of 55
     focusable controls were still under 16px — the caption inspector's
     #cue-th/#cue-en, #out-name, #home-q, the font and pace <select>s, every
     number box in the cue/text/image panels. They set no font-size of their
     own; they INHERIT one, and any direct declaration beats an inherited
     value, so one blanket rule catches the whole class including controls
     nobody has written yet.
     This matters more than it looks: iOS never zooms back out, so a single
     untreated control anywhere leaves the app oversized for the rest of the
     session — the symptom then surfaces at some later, unrelated moment
     ("มันยังเบี้ยวหลังจากตัดเสร็จ" was reported about the SPLIT, which focuses
     a control that was already 16px). Range/checkbox/radio/colour have no
     text field to zoom into and keep their own sizing.
     `!important` is deliberate and is the same call as the global
     `[hidden] { display: none !important }` a few rules up: this is a PLATFORM
     CONSTRAINT, not a design preference. A bare element selector (0,0,1) loses
     to every `.panel textarea` (0,1,1), `.font-select` (0,1,0) and
     `#brandkit-modal textarea` (1,0,1) in the file — measured, that left 12 of
     the 21 still zooming — and out-specificing them one by one rebuilds the
     hand-kept list this rule exists to delete. Nothing may opt a touch control
     back under 16px, so nothing gets to win. */
  input:not([type="range"]):not([type="checkbox"]):not([type="radio"]):not([type="color"]),
  textarea, select { font-size: 16px !important; }

  /* Keyboard-shortcut hints teach gestures a phone doesn't have. */
  .transcript-keys { display: none; }

  /* Transport: the 30px play button is a mouse target; give the finger 44px. */
  .t-btn { width: 44px; height: 44px; font-size: .9rem; }

  /* Fullscreen position mode (owner's ask, entry adjusted so it doesn't fight
     tap-to-play): ⛶ on the frame corner expands the canvas over everything;
     the same button becomes ✓ เสร็จ. Captions drag with the existing pointer
     code — just on a screen big enough to be precise. */
  .preview-expand { display: grid; place-items: center; position: absolute;
    top: .5rem; right: .5rem; z-index: 7; min-width: 42px; height: 42px;
    padding: 0 .6rem; border: none; border-radius: 999px;
    background: rgba(0, 0, 0, .45); color: #fff; font-size: 1rem; cursor: pointer; }
  body.pv-full .canvas { position: fixed; inset: 0; z-index: 80; margin: 0;
    padding: 0; background: #000; display: flex; align-items: center;
    justify-content: center; }
  /* Strictly 9:16 even fullscreen. `height:100dvh + max-width:100vw` broke
     the aspect box into 390x844 — captions were then dragged against a frame
     TALLER than the real video, and snapped elsewhere on exit ("กด confirm
     รูปเล็กไม่ใช่ตำแหน่งเดียวกัน", real iPhone). min() keeps the box the
     largest true 9:16 that fits, so a margin means the same spot at every
     preview size. */
  body.pv-full .preview {
    height: min(100vh, calc(100vw * 16 / 9));
    height: min(100dvh, calc(100vw * 16 / 9));
    width: auto; max-width: 100vw; border-radius: 0; }

  /* Paused ▶ badge on the frame — the video is the play button here (wired in
     studio.js). Pseudo-element takes no events, so the tap lands on <video>. */
  .preview.is-paused::after { content: "▶"; position: absolute; left: 50%; top: 50%;
    transform: translate(-50%, -50%); z-index: 5; width: 64px; height: 64px;
    display: grid; place-items: center; padding-left: 6px; box-sizing: border-box;
    border-radius: 50%; background: rgba(0, 0, 0, .45); color: #fff; font-size: 24px; }

  /* Give the sheet back to the cue list (measured: 88px of 844 left for rows).
     Find/replace folds behind 🔍; split/merge appear only with a selected row
     (they act on the selection anyway — and a split button with no caret was
     a dead-looking control); chrome rows tighten. All attribute/class states
     here are driven from studio.js. */
  .transcript-panel .review-tools { display: none; }
  .transcript-panel.find-open .review-tools { display: flex; }
  /* ✂ ตัด / ⤴ รวม pulled from the phone for now (owner 2026-08-13) — the
     sheet is for reading+retyping; structural edits stay a desktop verb until
     a better phone gesture exists. Code stays wired; delete this line to
     bring them back. */
  .transcript-touch-keys { display: none; }
  /* Sheet title "คำบรรยายทั้งหมด" says what the open sheet already shows —
     its pixels go to the action strip. */
  .transcript-panel .review-head > span:first-child { display: none; }
  #btn-find-toggle { border: none; background: none; padding: .3rem .4rem; }
  /* The ✕ must NEVER scroll away: it sits at the END of the head-actions
     strip, which scrolls sideways — on a real iPhone it was off-screen and
     the sheet read as "กดออกไม่ได้". Anchor it to the header itself (its
     containing block is .review-head, outside the scroller, so the strip
     scrolls under it). */
  .review-head { padding: .55rem 52px .55rem .8rem; position: relative; }
  .review-head .review-close { position: absolute; top: 50%; right: .35rem;
    transform: translateY(-50%); width: 42px; height: 42px; display: grid;
    place-items: center; background: var(--panel-bg); border-radius: 50%; }
  /* Header actions: one line each, scroll sideways when tight — 53px-wide
     two-line-wrapped buttons were below comfortable touch width. */
  .head-actions { overflow-x: auto; }
  .head-actions .mini-btn { font-size: .74rem; padding: .3rem .5rem; white-space: nowrap; }
  /* Gap banner: one-line chips, not a permanently-mounted 78px card — and the
     find bar REPLACES it (both open at once left ~2.5 cue rows on screen). */
  .gap-strip { padding: .3rem .6rem; max-height: 72px; overflow-y: auto; }
  .gap-strip .gap-strip-head { display: none; }
  .transcript-panel.find-open .gap-strip { display: none; }
}

/* วิดีโอกำลังทยอยขึ้นเบื้องหลัง (audio-first upload)
   แถบเดียว ใต้ header อยู่เหนือทั้งสองคอลัมน์ เพราะมันพูดถึงคลิปทั้งคลิป
   ไม่ใช่แผงใดแผงหนึ่ง — และผู้ใช้ต้องเห็นมันโดยไม่ต้องเลื่อนหา */
.video-up {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 14px; font-size: 13px;
  background: var(--accent-soft, #eff6ff); color: var(--ink, #111827);
  border-bottom: 1px solid var(--line, #e5e7eb);
}
.video-up.is-failed { background: #fef2f2; color: #991b1b; }
.video-up #video-up-label { flex: 1 1 auto; min-width: 0; }
