/* editing.benheath.cc — house tokens verbatim from planner.benheath.cc (DESIGN.md 0.4), then this app.
   PLACE vs ACTION: --nav (cyan) is where you ARE and every link; --accent (violet) is what you DO or
   have CHOSEN. Nothing is told apart by hue alone. Dark, dense, quiet. */
:root {
  --bg: #0a0b0f;
  --panel: #121419;
  --panel2: #1a1d24;
  --panel3: #22262f;    /* pressed / selected well */
  --border: #2a2f3a;
  --border-hi: #3a4150; /* a hairline that must be SEEN: hover border */
  --text: #edeef2;
  --muted: #99a0b0;
  --accent: #8b7cff;
  --nav: #4ddbe8;
  --focus: var(--accent); /* pre-action, so it shares the action hue */
  --disc: #ff69ae;
  --ok: #3ad894;
  --bad: #ff7070;
  --warn: #ffb44d;
  --ink: #0a0b0f;       /* text on ANY filled accent — always dark */
  --edge: inset 0 1px 0 rgba(255,255,255,.075); /* containers only */
}
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
  background: var(--bg);
  color: var(--text);
  font: 14px/1.45 -apple-system, BlinkMacSystemFont, system-ui, "Segoe UI", Roboto, sans-serif;
}
button { font: inherit; color: inherit; background: var(--panel2); border: 1px solid var(--border); border-radius: 7px; padding: 5px 11px; cursor: pointer; }
button:hover { border-color: var(--border-hi); }
button:disabled { opacity: .5; cursor: default; }
input, select { font: inherit; color: var(--text); background: var(--panel2); border: 1px solid var(--border); border-radius: 6px; padding: 4px 8px; }
input:focus, select:focus, button:focus-visible { outline: none; border-color: var(--focus); box-shadow: 0 0 0 2px rgba(139,124,255,.28); }
/* A link takes you somewhere and writes nothing — navigation, so every anchor is --nav. */
a { color: var(--nav); text-decoration: none; }
a:hover { text-decoration: underline; }
.spacer { flex: 1; }

/* ---- header ---- */
header {
  position: sticky; top: 0; z-index: 20;
  display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
  padding: 12px 22px; background: rgba(10,11,15,.92); backdrop-filter: blur(6px);
  border-bottom: 1px solid var(--border); box-shadow: var(--edge);
}
/* The planner's centring trick, with the fix the planner does not need: `flex: 1 1 0` makes the two
   side columns equal in WIDTH (grow from a zero basis), where `1 1 auto` only grows them by an equal
   AMOUNT and leaves the 340px difference between the clusters, which put the tabs 171px left of
   centre. What happens when the row runs out of room is fitHeader()'s job, not a breakpoint's: the
   rungs are `header.compact` and `header.wrapped` at the foot of this file. */
.brand, .hdrright { flex: 1 1 0; min-width: 0; }
h1.brand { margin: 0; font-size: 17px; font-weight: 650; letter-spacing: .2px; }
/* The wordmark, the planner's .plannerpick: a button that does not look like one until you go near
   it. It truncates rather than wrapping — two lines of it would grow the header and push the tabs
   down over the board. */
.apppick { display: inline-flex; align-items: center; gap: 7px; max-width: 100%; min-width: 0;
  margin: 0; padding: 4px 10px 4px 2px; font: inherit; color: var(--text);
  background: transparent; border: none; border-radius: 8px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.apppick:hover { background: var(--panel2); border-color: transparent; }
.apppick .pcaret { font-size: 11px; font-weight: 400; color: var(--muted); }
.hdrright { display: flex; align-items: center; justify-content: flex-end; gap: 10px; }
/* Refresh and the settings cog, the planner's #btnMore sizing. Muted until you are on them: they are
   chrome, and chrome that shouts competes with the board. */
.iconbtn { display: inline-flex; align-items: center; justify-content: center; flex: none; padding: 6px 8px; color: var(--muted); }
.iconbtn svg { width: 17px; height: 17px; display: block; }
.iconbtn:hover { color: var(--text); }
/* In flight the glyph turns and the button is disabled — the disable is what stops a second click
   stacking a second /api/board on a slow board, the turning is what says so. */
#btnRefresh.busy svg { animation: rot .8s linear infinite; }
@keyframes rot { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { #btnRefresh.busy svg { animation: none; } }

.viewtoggle { display: inline-flex; gap: 4px; background: var(--panel); border: 1px solid var(--border); border-radius: 9px; padding: 3px; flex: none; }
.tab { background: none; border: 1px solid transparent; color: var(--muted); border-radius: 7px; padding: 5px 12px; white-space: nowrap; }
.tab:hover { color: var(--text); border-color: transparent; background: var(--panel2); }
.tab.active { color: var(--nav); background: var(--panel3); border-color: var(--border); }
.tabcount { font-size: 11.5px; color: var(--muted); font-variant-numeric: tabular-nums; }
.tab.active .tabcount { color: var(--nav); }

/* The filter row: two mark groups then two selects. The wider gap between groups is the only grouping
   device — a rule between them would be a fourth line in a header that already has three.
   flex-wrap is load-bearing, not decoration: a right column too narrow for the row (a very long
   signed-in name, an unusually wide filter set) must fold the row onto a second line INSIDE its own
   column. Without it the row overflows to the LEFT — silently, with no scrollbar — and draws itself
   straight over the tabs, which is what it did at 1200px before the breakpoint below moved. */
.filters { display: flex; align-items: flex-end; justify-content: flex-end; flex-wrap: wrap; gap: 10px; }
.filters select { max-width: 128px; font-size: 12.5px; padding: 4px 6px; }
.filters select.on { border-color: var(--accent); color: var(--text); } /* a chosen filter is an ACTION */
.fgroup { display: inline-flex; align-items: center; gap: 4px; flex: none; }
.fgroup:empty { display: none; }   /* a board carrying no platforms leaves no gap behind */

/* Each filter names what it filters: the planner's .nifield, at header size. The caption sits ABOVE
   its controls rather than beside them because the header's scarce axis is horizontal — fitHeader()
   spends its whole ladder buying width back — and a stacked caption costs none of it, only ~14px of
   height the header can spare. That is what lets the captions survive at every width instead of
   being the first thing shed.
   align-items: flex-end on the row above is the pair of this: the four controls sit on one baseline
   whatever their captions do, which is the line the eye actually reads the row along. */
.ffield { display: inline-flex; flex-direction: column; align-items: flex-start; gap: 4px; flex: none; }
.ffield[hidden] { display: none; }   /* display:flex would otherwise beat [hidden] */
/* Caption weight, not value weight: 10.5px muted is quieter than .tabcount, so it reads as a label
   on the controls and never as a fifth thing to click. */
.fcap { font-size: 10.5px; line-height: 1; letter-spacing: .3px; color: var(--muted); white-space: nowrap; }
.selfield { cursor: pointer; }   /* it is a <label>: the caption focuses the select */

/* Filtering by a mark: OFF is genuinely off, ON is full ink plus an accent edge plus a ring — three
   signals, none of them a hue on its own (the planner's .nirow picker, at header size). There is no
   "All" mark: clicking the lit one clears the filter, exactly like the planner's chips. */
.platpick { padding: 0; border: none; background: none; border-radius: 999px; line-height: 0; }
.platpick .plat { opacity: .45; transition: opacity .12s, border-color .12s, background .12s, box-shadow .12s; }
.platpick:hover .plat { opacity: .85; }
.platpick.on .plat { opacity: 1; border-color: var(--accent); background: rgba(139,124,255,.2); box-shadow: 0 0 0 2px var(--bg), 0 0 0 3px var(--accent); }
.platpick:focus-visible { outline: none; box-shadow: none; }
.platpick:focus-visible .plat { opacity: 1; box-shadow: 0 0 0 2px var(--bg), 0 0 0 3px var(--focus); }
button.pridot { cursor: pointer; opacity: .45; transition: opacity .12s, box-shadow .12s; }
button.pridot:hover:not([aria-disabled="true"]) { opacity: 1; }
/* The ring is --accent, not the planner's currentColor: Stuck's ink is --ink (it is the one filled
   circle), so a currentColor ring on it was dark-on-dark and the pressed state vanished. Accent is
   also the honest hue here — this is a filter you have CHOSEN, which is what --accent means. */
button.pridot[aria-pressed="true"] { opacity: 1; box-shadow: 0 0 0 2px var(--bg), 0 0 0 3px var(--accent); }
/* An option no ticket on the board carries: still there, so the row of four never shifts under the
   pointer between polls, but unpressable — the row can never offer a filter that empties the board.
   aria-disabled rather than the disabled attribute (T4): a disabled button is out of the tab order
   and shows no tooltip in Safari, so the one circle that most needs to say why it is dead would be
   the only one that could not. It stays reachable and keeps its sentence; the click is a no-op.
   .28 rather than the old .16: it has to read as unavailable next to an off circle at .45, but it
   also has to be legible enough to be worth hovering. */
button.pridot[aria-disabled="true"] { opacity: .28; cursor: default; }
button.pridot:focus-visible { outline: none; opacity: 1; box-shadow: 0 0 0 2px var(--bg), 0 0 0 3px var(--focus); }

/* ---- views ---- */
/* --footh is the fixed footer's measured height (app.js measureChrome). The scrolling tabs reserve
   it as padding so the last card is never parked under the pipeline; the Feedback tab, which is one
   screen rather than a page, subtracts it from its height instead (see .fb). The 40px fallback is
   one un-wrapped chip row, so nothing overlaps in the frame before the first measurement. */
.view { padding: 18px 22px calc(24px + var(--footh, 40px)); }
/* min(track, 100%) rather than a bare track: a minmax floor wider than the viewport overflows the
   page silently, with no scrollbar to show it. Measured at 375px, where the wide grid below pushed
   9px off the right edge. */
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(320px, 100%), 1fr)); gap: 12px; }
/* In-Progress only: the tile takes ~116px off the card's width, and at 320px that left the body
   narrower than the assign control it has to hold. */
.grid.wide { grid-template-columns: repeat(auto-fill, minmax(min(370px, 100%), 1fr)); }
/* A group heading now names a PERSON, so it carries their avatar and centres on it rather than
   sitting on a text baseline. */
.grouphd { display: flex; align-items: center; gap: 8px; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; color: var(--muted); margin: 4px 0 10px; }
.grouphd:not(:first-child) { margin-top: 26px; }
.grouphd b { color: var(--text); font-weight: 700; letter-spacing: .6px; }
.grouphd .avatar.none { border-style: dashed; }   /* unset, in the same language as .pridot.none */
/* Your own row is the pinned one; the tag says why it is first without shouting about it. */
.grouphd .you { color: var(--accent); border: 1px solid var(--accent); border-radius: 999px; padding: 0 6px; font-size: 9.5px; letter-spacing: .8px; }
.grouphd .n { margin-left: 2px; }
.empty { color: var(--muted); font-size: 13px; padding: 40px 0; text-align: center; }
.hint { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; font-size: 12.5px; color: var(--muted); background: var(--panel); border: 1px solid var(--border); border-radius: 9px; padding: 9px 12px; margin-bottom: 14px; }
.mount { color: var(--muted); font-size: 13px; padding: 40px 0; text-align: center; }

/* ---- the shared marks: platform glyphs, priority dots, the video glyph (public/marks.js) ----
   Sizes, shapes and colours are the siblings', not new ones. .plat is social.benheath.cc's;
   .pridot is planner.benheath.cc's, which reads this same Asana field. */
.plat { display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; padding: 0 5px; border-radius: 999px; font-size: 11px; font-weight: 600; border: 1px solid var(--border); background: var(--panel2); color: var(--text); }
.plat svg { width: 14px; height: 14px; display: block; }
.plat:not(:has(svg)) { width: auto; } /* unknown platform falls back to its name */
/* Marks are MONOCHROME (social's finding, kept): every platform owns an unmistakable silhouette, so
   the brand tints bought nothing and cost a lot — they sat 1-2 OKLab dE from --bad, --disc and --nav
   under deuteranopia, i.e. indistinguishable from failure, discovery and place, four to a card. */
.glyph { flex: none; display: inline-flex; align-items: center; color: var(--muted); }
.glyph svg { width: 13px; height: 13px; display: block; }
.glyph.play { color: var(--ok); }
/* Priority: one circle ahead of the name, so H/M/L/S scans down the grid without costing a line.
   24px not 20 — the planner's size, and WCAG 2.5.8 where it is a control.
   The two hex values are Asana's own colours for these four options (red for High and Stuck,
   cool-gray for Medium and Low, read live 2026-07-26) via the planner's COLORS table, so the same
   ticket wears the same circle in both apps. They are option colours, not house tokens: a rename or
   a recolour in Asana is a one-line change here. */
.pridot {
  width: 24px; height: 24px; flex: 0 0 24px; border-radius: 50%; border: 1px solid var(--border); padding: 0;
  font-size: 11px; font-weight: 700; line-height: 1; background: none; color: var(--muted);
  display: inline-flex; align-items: center; justify-content: center;
}
.pridot.none { border-style: dashed; opacity: .75; }   /* unset carries a dash: a hole reads as a gap */
.pridot.p-high { color: #e8646a; border-color: #e8646a; background: #e8646a26; }
.pridot.p-med, .pridot.p-low { color: #8b929e; border-color: #8b929e; background: #8b929e26; }
/* Stuck is the one alarm on this board — it means the work has stopped, not that it is urgent — so
   it is the one circle that fills. Same red as High; the fill and the S are what tell them apart. */
.pridot.p-stuck { color: var(--ink); background: #e8646a; border-color: #e8646a; }

/* ---- ticket card ----
   The call-sheet card's language (planner .cscard): ONE frame whatever the state, an even border, a
   fixed reading order (priority + title, kind, where it is going, who has it), and a footer pinned to
   the bottom so a row of cards lines its controls up. The yellow left band is gone: 15 of the 22
   tickets in Ready to Edit are simply unclaimed, and striping two thirds of a grid in --warn made a
   normal Tuesday look like an incident. Unclaimed now shows where it is actionable — the assign
   control wears the dashed, unset edge the whole family uses for "nothing here yet". */
.card {
  display: flex; flex-direction: column; gap: 9px;
  background: var(--panel); border: 1px solid var(--border);
  border-radius: 12px; padding: 13px 15px; cursor: pointer; box-shadow: var(--edge);
  transition: transform .16s ease, border-color .16s ease, background-color .16s ease, box-shadow .16s ease;
}
.card:hover { border-color: var(--border-hi); background: var(--panel2); transform: translateY(-2px); box-shadow: var(--edge), 0 12px 28px rgba(0,0,0,.45); }
.card:active { transform: translateY(0); box-shadow: var(--edge); }
/* the marks sit on --panel2; when the card lifts to --panel2 they would vanish into it */
.card:hover .plat { background: var(--panel); }
@media (prefers-reduced-motion: reduce) { .card:hover { transform: none; } }
/* ---- the In-Progress hover-play tile (social.benheath.cc's .vtile, same rules) ----
   Square, not the source's 9:16: a row of cards has to line its footers up, and a portrait tile
   would set every card's height from the narrowest column on the page. object-fit: cover crops the
   9:16 cut into it, which is what social does with the same footage. */
.card.hastile { flex-direction: row; align-items: stretch; gap: 12px; }
.cbody { display: flex; flex-direction: column; gap: 9px; flex: 1; min-width: 0; }
.vtile {
  position: relative; flex: 0 0 104px; width: 104px; aspect-ratio: 1; align-self: flex-start;
  background: var(--panel3); border: 1px solid var(--border); border-radius: 9px;
  padding: 0; overflow: hidden; cursor: pointer;
}
.vtile:hover, .vtile:focus-visible { border-color: var(--border-hi); }
.vtile video { width: 100%; height: 100%; object-fit: cover; display: block; }
.playov { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; pointer-events: none; }
/* White, not the glyph's --ok: over a picture the mark has to read as an affordance on the video,
   and the drop shadow is what keeps it legible on a bright first frame. */
.playov .glyph { color: rgba(255,255,255,.88); filter: drop-shadow(0 2px 10px rgba(0,0,0,.75)); }
.playov .glyph svg { width: 26px; height: 26px; }
.vtile.ph { display: flex; align-items: center; justify-content: center; border-style: dashed; }
.tileword { font-size: 10px; line-height: 1.35; color: var(--muted); text-align: center; padding: 0 7px; }
/* No hover to play on: the still IS the preview and a tap opens the real player. Autoplaying
   sixteen signed streams on a phone is exactly the request storm the lazy loading exists to avoid. */
@media (hover: none) { .playov .glyph svg { width: 22px; height: 22px; } }

.cardtop { display: flex; align-items: flex-start; gap: 9px; }
.cardtop .pridot { margin-top: 1px; }
.ctitle { margin: 0; font-size: 14px; font-weight: 650; line-height: 1.35; overflow-wrap: anywhere; flex: 1; min-width: 0; }
.cardtop .glyph { margin-top: 4px; }
.badge { flex: none; font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .8px; border: 1px solid; border-radius: 999px; padding: 1px 8px; }
.badge.none { color: var(--muted); border-color: var(--border); border-style: dashed; }
.cmeta { font-size: 12px; color: var(--muted); overflow-wrap: anywhere; }
.chips { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
.chip { font-size: 10.5px; color: var(--muted); background: var(--panel2); border: 1px solid var(--border); border-radius: 999px; padding: 1px 8px; white-space: nowrap; }
/* margin-top:auto pins the footer to the bottom of a stretched grid cell, so every card in a row
   offers its assign control on the same line however much metadata sits above it */
.cfoot { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-top: auto; padding-top: 2px; }
/* One height for whichever control the footer ends up holding. A <select> and the assignee button
   render 2px apart on their own, which staggered the claimed cards against the free ones. */
.cfoot select { font-size: 12px; max-width: 170px; }
.cfoot select, .cfoot .who { min-height: 30px; }
/* The unassigned state, carried by the control that fixes it: dashed is "unset" everywhere in this
   family (.pridot.none, the planner's chips, social's picker). Solid and lit the moment you touch it. */
.card.unassigned .assign { border-style: dashed; color: var(--muted); }
.card.unassigned .assign:hover { border-style: solid; border-color: var(--border-hi); color: var(--text); }
.card.unassigned .assign:focus { border-style: solid; color: var(--text); }
.who { display: inline-flex; align-items: center; gap: 7px; font-size: 12.5px; color: var(--muted); background: none; border: 1px solid transparent; padding: 3px 7px; }
.who:hover { border-color: var(--border-hi); background: var(--panel2); }
.who .re { color: var(--muted); font-size: 11px; opacity: 0; }
.who:hover .re { opacity: 1; }
.avatar { width: 20px; height: 20px; border-radius: 50%; background: var(--panel3); border: 1px solid var(--border); flex: none; display: inline-flex; align-items: center; justify-content: center; font-size: 10px; color: var(--muted); object-fit: cover; }
.cexpand { border-top: 1px solid var(--border); padding-top: 9px; font-size: 12.5px; color: var(--muted); display: flex; flex-direction: column; gap: 8px; }
.cexpand[hidden] { display: none; } /* display:flex above beats the UA [hidden] rule */
.cexpand .notes { white-space: pre-wrap; overflow-wrap: anywhere; max-height: 240px; overflow-y: auto; }
.cexpand .none { font-style: italic; }

/* ---- rich view slide-over ---- */
.slideover { position: fixed; inset: 0; z-index: 40; }
.slideover[hidden] { display: none; }
.rvscrim { position: absolute; inset: 0; background: rgba(10,11,15,.6); backdrop-filter: blur(2px); }
.rvpanel {
  position: absolute; top: 0; right: 0; bottom: 0; width: min(940px, 100%);
  display: flex; flex-direction: column; background: var(--panel);
  border-left: 1px solid var(--border); box-shadow: -18px 0 40px rgba(0,0,0,.45);
}
.rvhead { display: flex; align-items: center; gap: 12px; padding: 12px 16px; border-bottom: 1px solid var(--border); }
.ghost { background: none; border-color: transparent; color: var(--nav); padding: 4px 8px; }
.ghost:hover { background: var(--panel2); border-color: var(--border); }
.rvtitle { flex: 1; min-width: 0; font-size: 14.5px; font-weight: 650; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rvwho { display: inline-flex; align-items: center; gap: 7px; font-size: 12.5px; color: var(--muted); flex: none; }
.rvpills { display: flex; align-items: center; gap: 6px; padding: 10px 16px 0; flex-wrap: wrap; }
.pill { font-size: 11.5px; color: var(--muted); background: var(--panel2); border: 1px solid var(--border); border-radius: 999px; padding: 3px 11px; }
.pill.on { color: var(--ink); background: var(--accent); border-color: var(--accent); font-weight: 650; } /* a chosen version is an ACTION */
.pill .k { opacity: .8; font-size: 10.5px; }
.rvbody { flex: 1; min-height: 0; display: flex; gap: 14px; padding: 12px 16px; }
/* Review content is portrait (1080x1920 verified) — the player box is 9:16 first. */
.rvplayer { flex: 0 0 clamp(240px, 34%, 380px); min-width: 0; display: flex; flex-direction: column; overflow-y: auto; }
.rvcomments { flex: 1; min-width: 0; display: flex; flex-direction: column; border: 1px solid var(--border); border-radius: 11px; background: var(--panel2); overflow: hidden; }
.rvplayer .ph, .rvcomments .ph { flex: 1; display: flex; flex-direction: column; gap: 10px; align-items: center; justify-content: center; text-align: center; padding: 18px; font-size: 12.5px; color: var(--muted); }
.rvplayer .ph { border: 1px dashed var(--border); border-radius: 11px; background: #000; aspect-ratio: 9/16; flex: none; }
.rvlinks { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; padding: 12px 16px; border-top: 1px solid var(--border); font-size: 12.5px; }

/* ---- player pane (DESIGN.md 3.4, review.js) ----
   Portrait first: the stage takes the pane's height and derives its width from 9:16, so a
   1080x1920 cut fills the box instead of floating in letterbox bars. */
.pl { display: flex; flex-direction: column; gap: 8px; }
.plstagewrap { flex: 0 0 auto; display: flex; justify-content: center; }
/* ponytail: the box is 9:16 off the pane width, full stop — never stretched to fill leftover
   height, so the scrubber always sits directly under the picture. A pane too short for it scrolls
   (.rvplayer), which beats cropping or distorting a portrait cut. */
.plstage {
  position: relative; aspect-ratio: 9 / 16; width: 100%;
  background: #000; border: 1px solid var(--border); border-radius: 11px; overflow: hidden; cursor: pointer;
}
.plstage video { width: 100%; height: 100%; object-fit: contain; display: block; background: #000; }
.plstage:fullscreen { aspect-ratio: auto; height: 100%; width: 100%; max-width: none; border: none; border-radius: 0; }

/* The scrubber. Playhead and progress are --accent: the one thing on this pane you are driving. */
.pltrack { position: relative; height: 22px; flex: none; cursor: pointer; touch-action: none; }
.pltrack::before { content: ''; position: absolute; left: 0; right: 0; top: 9px; height: 4px; border-radius: 3px; background: var(--panel3); }
.plfill { position: absolute; left: 0; top: 9px; height: 4px; width: 0; border-radius: 3px; background: var(--accent); }
.plhead { position: absolute; top: 5px; left: 0; width: 12px; height: 12px; margin-left: -6px; border-radius: 50%; background: var(--accent); box-shadow: 0 0 0 2px var(--panel); }
.pltrack:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; border-radius: 6px; }
.plpins { position: absolute; inset: 0; pointer-events: none; }
.plpin {
  position: absolute; top: -6px; margin-left: -10px; width: 20px; height: 20px; padding: 0;
  border-radius: 50%; overflow: hidden; background: var(--panel3); border: 1px solid var(--border-hi);
  pointer-events: auto; display: grid; place-items: center; font-size: 9.5px; color: var(--muted);
}
.plpin:hover { border-color: var(--accent); }
.plpin img { width: 100%; height: 100%; object-fit: cover; display: block; }

.plctl { display: flex; align-items: center; gap: 7px; flex: none; flex-wrap: wrap; }
.plbtn { padding: 3px 8px; font-size: 12px; line-height: 1.3; }
.plplay { min-width: 34px; }
.plhd.on { color: var(--ink); background: var(--accent); border-color: var(--accent); font-weight: 650; }
.pltime { font-size: 11.5px; color: var(--muted); font-variant-numeric: tabular-nums; white-space: nowrap; }
.plspacer { flex: 1; }
.plspeed { font-size: 11.5px; padding: 3px 6px; }
.plvol { width: 66px; accent-color: var(--accent); }
.plarm { font-size: 11px; padding: 2px 8px; border-radius: 999px; color: var(--ink); background: var(--accent); border-color: var(--accent); font-weight: 650; white-space: nowrap; }
/* One row under the scrubber, like the reference: volume, CC, Transcript and HD live under "⋯"
   because eleven controls need ~510px and the portrait player is 300-450px wide. */
.plmore { min-width: 30px; }
.plmore.on { color: var(--ink); background: var(--accent); border-color: var(--accent); font-weight: 650; }
.plextra { display: flex; align-items: center; gap: 7px; flex: none; flex-wrap: wrap; }
.plextra[hidden] { display: none; }
/* Sits directly under the picture it is about, centred on it. Quiet: the sentence is muted and only
   the link carries --nav, because reading it is not an emergency. */
.plerr { flex: none; font-size: 11.5px; color: var(--muted); text-align: center; text-wrap: balance; }
.plerr a { color: var(--nav); white-space: nowrap; }
.plcc.on, .pltx.on { color: var(--ink); background: var(--accent); border-color: var(--accent); font-weight: 650; }
.plstage video::cue { background: rgba(10,11,15,.78); color: var(--text); font-size: 84%; }

/* ---- annotation overlay (DESIGN.md 3.4, BRIEF.md Phase 4) ----
   The canvas sits over the picture and is transparent to the pointer, so play/pause still belongs
   to the video; it only takes clicks while a draw tool is armed. */
/* width/height are NOT decoration. A <canvas> is a replaced element, so an absolutely positioned
   one with `inset: 0` and `width: auto` resolves to its INTRINSIC size (the width/height
   attributes, which carry the device-pixel backing store) instead of stretching to the stage. It
   drew at devicePixelRatio scale and overflowed the picture until this was pinned to 100%. */
.placv { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
.placv.drawing { pointer-events: auto; cursor: crosshair; }
.planchors { position: absolute; inset: 0; pointer-events: none; }
.planchor {
  position: absolute; width: 26px; height: 26px; margin: -13px 0 0 -13px; padding: 0;
  border-radius: 50% 50% 50% 2px; overflow: hidden; pointer-events: auto;
  background: var(--panel3); border: 2px solid var(--panel); box-shadow: 0 2px 8px rgba(0,0,0,.6);
  display: grid; place-items: center;
}
.planchor:hover, .planchor.on { border-color: var(--accent); }
.planchorav { width: 100%; height: 100%; object-fit: cover; display: grid; place-items: center; font-size: 10px; color: var(--text); }

/* ---- transcript panel ---- */
.pltxp { flex: none; max-height: 210px; overflow-y: auto; background: var(--panel); border: 1px solid var(--border); border-radius: 9px; padding: 5px; }
.pltxp[hidden] { display: none; }
.pltxline {
  display: flex; align-items: baseline; gap: 8px; width: 100%; text-align: left;
  background: none; border: 1px solid transparent; border-radius: 6px; padding: 3px 6px; font-size: 12.5px;
}
.pltxline:hover { background: var(--panel2); border-color: var(--border); }
.pltxsp { flex: none; font-size: 10px; font-weight: 700; letter-spacing: .8px; color: var(--muted); text-transform: uppercase; }
.pltxt { flex: none; font-size: 11px; color: var(--muted); font-variant-numeric: tabular-nums; }
.pltxbody { color: var(--text); }
.pltxempty { color: var(--muted); font-size: 12px; padding: 10px 6px; text-align: center; }

/* ---- the Asana strip on the review screen (R4) ----
   Above the comments in both hosts. Marks and dots are the shared ones (marks.js); the chips are the
   planner's .nichip, unscoped from its .nirow. */
/* ponytail: a hard cap rather than a media query. The comments pane has a definite height, the
   strip's content does not (Vertical has ten live options and a script can be a thousand words), and
   a flex item with basis auto would take the whole pane on a phone. 46% guarantees the comment list
   the other half and gives the strip one scroller of its own. */
.astrip {
  flex: 0 1 auto; max-height: 46%; overflow-y: auto; min-height: 0;
  display: flex; flex-direction: column; gap: 7px;
  padding: 9px 10px; border-bottom: 1px solid var(--border); background: var(--panel);
}
.astrip.busy { opacity: .72; }   /* a write is in flight: the whole strip is inert, and says so */
.asrow { display: flex; align-items: flex-start; gap: 9px; }
.aslab { flex: 0 0 58px; padding-top: 5px; font-size: 10px; font-weight: 700; letter-spacing: .9px; text-transform: uppercase; color: var(--muted); }
.asctl { flex: 1; min-width: 0; }
.chiprow { display: flex; flex-wrap: wrap; align-items: center; gap: 5px; }
.astrip .fgroup { flex-wrap: wrap; }

/* The planner's pickable chip, carrying each option's own live Asana colour in --nc. Selected
   differs by THREE things that are not colour: a solid rather than dashed edge, bolder type, and a
   filled rather than hollow dot. There is no "none" chip — clicking the lit one clears the field. */
.nichip { display: inline-flex; align-items: center; gap: 6px; min-height: 26px; padding: 3px 9px;
  font-size: 11.5px; border-radius: 999px; border: 1px dashed var(--border); background: var(--panel2);
  color: var(--muted); cursor: pointer; transition: border-color .12s, background .12s, color .12s; }
.nichip .dot { width: 7px; height: 7px; flex: none; border-radius: 50%; border: 1.5px solid var(--nc, var(--muted)); background: none; }
.nichip:hover:not(:disabled) { color: var(--text); border-color: var(--nc, var(--muted)); }
.nichip[aria-pressed="true"] { border-style: solid; border-width: 1.5px; padding: 2.5px 8.5px;
  border-color: var(--nc, var(--text)); color: var(--nc, var(--text)); font-weight: 700;
  background: color-mix(in srgb, var(--nc, var(--text)) 16%, transparent); }
.nichip[aria-pressed="true"] .dot { background: var(--nc, var(--text)); }
.nichip:disabled { cursor: default; }

.asscript { display: flex; flex-direction: column; gap: 7px; align-items: flex-start; }
.asbtn { font-size: 12px; padding: 4px 11px; }
.asbtn[aria-expanded="true"] { color: var(--nav); border-color: var(--border-hi); } /* where you ARE */
.asbtn:disabled { color: var(--muted); border-style: dashed; }   /* no script: the app's unset language */
/* The script is prose, not markup: line breaks are the author's and are kept, the measure is capped
   so a wide pane does not hand the reader a 140-character line. */
.asprose { white-space: pre-wrap; overflow-wrap: anywhere; max-width: 62ch; font-size: 12.5px;
  line-height: 1.55; color: var(--text); background: var(--panel2); border: 1px solid var(--border);
  border-radius: 9px; padding: 9px 11px; }

/* ---- comments panel (DESIGN.md 3.4, review.js A5) ----
   Dark, dense, quiet. The only --accent things are the ones you are DOING: the send button, the
   timestamp chip, an armed clock, a reaction you have given, an active filter. */
.cm { display: flex; flex-direction: column; min-height: 0; flex: 1; }
/* Comments / Details (DESIGN.md 3.4). The active tab is --nav: it is where you ARE. */
.cmtabs { display: flex; align-items: center; gap: 4px; padding: 6px 10px 0; flex: none; }
.cmtab { background: none; border: 1px solid transparent; color: var(--muted); font-size: 12.5px; padding: 4px 10px; }
.cmtab:hover { color: var(--text); background: var(--panel3); border-color: transparent; }
.cmtab.on { color: var(--nav); background: var(--panel3); border-color: var(--border); }
.cmdet { margin: 0; display: grid; grid-template-columns: minmax(88px, auto) 1fr; gap: 5px 12px; font-size: 12.5px; }
.cmdet dt { color: var(--muted); }
.cmdet dd { margin: 0; overflow-wrap: anywhere; font-variant-numeric: tabular-nums; }

.cmbar { display: flex; align-items: center; gap: 6px; padding: 8px 10px; border-bottom: 1px solid var(--border); flex: none; }
.cmbar[hidden], .cmcomp[hidden] { display: none; }
.cmsearch { flex: 1; min-width: 0; font-size: 12.5px; padding: 4px 8px; }
.cmsearch::-webkit-search-cancel-button { filter: invert(.6); }
.cmfilterbtn, .cmsort { font-size: 12px; padding: 4px 8px; flex: none; }
.cmfilterbtn.on { border-color: var(--accent); color: var(--text); } /* a chosen filter is an ACTION */
.cmlist { flex: 1; min-height: 0; overflow-y: auto; padding: 8px 10px; display: flex; flex-direction: column; gap: 8px; }
.cmempty { color: var(--muted); font-size: 12.5px; text-align: center; padding: 28px 6px; }

.cmc { background: var(--panel); border: 1px solid var(--border); border-radius: 10px; padding: 8px 10px; display: flex; flex-direction: column; gap: 5px; }
.cmc.reply { background: var(--panel2); margin-left: 16px; margin-top: 3px; border-left: 2px solid var(--border-hi); }
.cmc.resolved { opacity: .62; }
.cmc.pending { opacity: .55; }                     /* optimistic, not yet confirmed by Shade */
.cmc.focus { border-color: var(--accent); }        /* a scrubber pin pointed here */
.cmhd { display: flex; align-items: center; gap: 7px; font-size: 12px; }
.cmname { font-size: 12.5px; font-weight: 650; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cmwhen { color: var(--muted); font-size: 11px; white-space: nowrap; }
.cmspacer { flex: 1; }
.cmts { font-size: 11px; padding: 1px 8px; border-radius: 999px; color: var(--ink); background: var(--accent); border-color: var(--accent); font-weight: 650; font-variant-numeric: tabular-nums; }
.cmvisicon { font-size: 11px; opacity: .75; }
.cmres { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .8px; color: var(--ok); }
.cmbody { font-size: 13px; white-space: pre-wrap; overflow-wrap: anywhere; }

.cmreacts { display: flex; align-items: center; gap: 4px; flex-wrap: wrap; }
.cmreact { font-size: 11.5px; line-height: 1.3; padding: 1px 7px; border-radius: 999px; background: var(--panel2); }
.cmreact.on { border-color: var(--accent); }
.cmaddreact { color: var(--muted); }
.cmpicker { display: inline-flex; gap: 3px; padding: 2px 4px; background: var(--panel3); border: 1px solid var(--border); border-radius: 999px; }

.cmacts { display: flex; align-items: center; gap: 4px; flex-wrap: wrap; }
.cmlink { background: none; border-color: transparent; color: var(--muted); font-size: 11.5px; padding: 2px 6px; }
.cmlink:hover { color: var(--text); background: var(--panel2); border-color: var(--border); }
.cmlink.bad { color: var(--bad); }
.cmconfirm { font-size: 11.5px; color: var(--muted); margin-right: 2px; }

.cmreply { display: flex; flex-direction: column; gap: 6px; margin-left: 16px; margin-top: 4px; }
.cmcomp { flex: none; display: flex; flex-direction: column; gap: 6px; padding: 9px 10px; border-top: 1px solid var(--border); background: var(--panel); }
.cmtext { font: inherit; font-size: 13px; color: var(--text); background: var(--panel2); border: 1px solid var(--border); border-radius: 8px; padding: 6px 9px; resize: vertical; min-height: 46px; }
.cmtext:focus { outline: none; border-color: var(--focus); box-shadow: 0 0 0 2px rgba(139,124,255,.28); }
.cmcrow { display: flex; align-items: center; gap: 6px; }
.cmclock { font-size: 11.5px; padding: 3px 8px; font-variant-numeric: tabular-nums; }
.cmclock.on { color: var(--ink); background: var(--accent); border-color: var(--accent); font-weight: 650; }
.cmvis { font-size: 11.5px; padding: 3px 6px; }
.cmsend { font-size: 12px; padding: 4px 14px; color: var(--ink); background: var(--accent); border-color: var(--accent); font-weight: 650; }
.cmsend:hover { border-color: var(--accent); filter: brightness(1.08); }
.cmmenu { min-width: 218px; }

/* ---- A6: mentions, attachments, draw tools (quiet: nothing shows until it is asked for) ---- */
.cmmark { font-size: 11px; padding: 1px 6px; border-radius: 999px; background: var(--panel2); color: var(--muted); }
.cmmark.on { color: var(--ink); background: var(--accent); border-color: var(--accent); }
.cmat { background: var(--panel3); border-radius: 4px; padding: 0 3px; font-weight: 600; }

/* align-items matters: without it a text pill stretches to the height of a neighbouring thumbnail
   and renders as an oval. */
.cmatts { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 6px; }
.cmattimg { display: block; width: 92px; height: 62px; border: 1px solid var(--border); border-radius: 7px; overflow: hidden; background: var(--panel2); }
.cmattimg img { width: 100%; height: 100%; object-fit: cover; display: block; }
.cmattimg:hover { border-color: var(--border-hi); }
.cmattfile { font-size: 11.5px; padding: 2px 8px; border: 1px solid var(--border); border-radius: 999px; background: var(--panel2); }

.cmicon { font-size: 12px; padding: 3px 8px; line-height: 1.3; }
.cmicon.on { color: var(--ink); background: var(--accent); border-color: var(--accent); }
.cmattach { display: flex; flex-wrap: wrap; gap: 5px; }
.cmattach[hidden] { display: none; }
.cmchip { display: inline-flex; align-items: center; gap: 5px; font-size: 11.5px; padding: 2px 4px 2px 9px; border: 1px solid var(--border); border-radius: 999px; background: var(--panel2); max-width: 220px; }
.cmchip.pending { opacity: .6; }
.cmchipx { background: none; border: none; color: var(--muted); padding: 0 4px; font-size: 13px; line-height: 1; }
.cmchipx:hover { color: var(--bad); }

.cmdraw { display: flex; align-items: center; gap: 5px; flex-wrap: wrap; padding: 5px 6px; background: var(--panel2); border: 1px solid var(--border); border-radius: 8px; }
.cmdraw[hidden] { display: none; }
.cmtool { font-size: 12px; padding: 2px 8px; line-height: 1.35; }
.cmtool.on { color: var(--ink); background: var(--accent); border-color: var(--accent); } /* the tool you CHOSE */
.cmdrawsep { width: 1px; height: 18px; background: var(--border); }
.cmswatch { width: 17px; height: 17px; padding: 0; border-radius: 50%; border: 1px solid var(--border); }
.cmswatch.on { box-shadow: 0 0 0 2px var(--panel2), 0 0 0 3px var(--accent); }
.cmsize { font-size: 11.5px; padding: 2px 5px; }

.cmmentions { display: flex; flex-direction: column; gap: 2px; max-height: 168px; overflow-y: auto; background: var(--panel2); border: 1px solid var(--border); border-radius: 8px; padding: 4px; }
.cmmentions[hidden] { display: none; }
.cmmention { display: flex; align-items: center; gap: 8px; width: 100%; text-align: left; background: none; border: 1px solid transparent; border-radius: 6px; padding: 4px 7px; font-size: 12.5px; }
.cmmention:hover, .cmmention.on { background: var(--panel3); border-color: var(--border); }

/* ---- the app's own sign-in screen (T10) ---- */
/* The planner's overlay, class for class, because it is the same door onto the same workspace and a
   second look for it would only make one of the two feel like the wrong site. Opaque and full-bleed:
   an unauthorised board must not be legible behind its own gate, even for a frame.
   Not to be confused with .signin below, which asks for a SHADE key and gates video, not the app. */
.signinoverlay { position: fixed; inset: 0; z-index: 1000; background: var(--bg); display: flex; align-items: center; justify-content: center; }
.signinbox { text-align: center; padding: 40px; max-width: 420px; }
.signinbrand { font-size: 26px; font-weight: 700; margin-bottom: 8px; }
.signinsub { color: var(--muted); margin-bottom: 26px; line-height: 1.5; }
.signinbtn { display: inline-block; background: var(--accent); color: var(--ink); font-weight: 700; padding: 11px 24px; border-radius: 10px; text-decoration: none; } /* the one ACTION on the screen */
.signinbtn:hover { filter: brightness(1.08); }
.signinnote { margin-top: 18px; font-size: 12px; color: var(--muted); line-height: 1.5; }
.signinerr { color: var(--bad); margin-top: 18px; font-size: 13px; }

/* ---- Shade sign-in card (DESIGN.md 3.5) ---- */
/* Gates review features only, never the board. Same card in a pane and in the modal. */
.signin { display: flex; flex-direction: column; gap: 9px; align-items: stretch; text-align: left; max-width: 380px; width: 100%; }
.signhd { margin: 0; font-size: 14px; font-weight: 650; }
.signrow { display: flex; gap: 7px; }
.signkey { flex: 1; min-width: 0; font-size: 13px; }
.signgo { color: var(--ink); background: var(--accent); border-color: var(--accent); font-weight: 650; } /* the ACTION */
.signgo:hover { border-color: var(--accent); filter: brightness(1.06); }
.signerr { font-size: 12px; color: var(--bad); }
.signerr[hidden] { display: none; }
.signnote { margin: 0; font-size: 11.5px; line-height: 1.5; color: var(--muted); }
.signdlg { border: 1px solid var(--border); border-radius: 12px; background: var(--panel); color: var(--text); padding: 16px 18px; box-shadow: var(--edge), 0 18px 44px rgba(0,0,0,.55); }
.signdlg::backdrop { background: rgba(10,11,15,.6); backdrop-filter: blur(2px); }

/* ---- Feedback tab (DESIGN.md 3.4): rail | player + verdict bar | comments, one screen high ---- */
/* --hdrh and --footh are both MEASURED (app.js measureChrome), because both wrap: the header to two
   lines on a narrow desktop, the pipeline row when it runs out of chips' worth of width. This tab is
   a fixed three-column surface, so the footer cannot be paid for with padding the way the scrolling
   tabs pay for it — it comes out of the height, which is what stops the columns growing under the
   footer and giving this screen a second scrollbar. 34px is this section's own padding plus the gap
   the design leaves above the footer. */
#viewFeedback { padding: 14px 22px 16px; } /* this tab is one screen, not a scrolling page */
/* NO min-height. One paired with this height fought it: below roughly 620px of viewport the floor
   won, the block grew past the fixed footer and buried 39px of the comments column — the composer —
   underneath it, and gave this one-screen tab a page scrollbar as well. The three columns already
   scroll inside themselves, so a short window makes them short rather than making them overlap. */
.fb { display: flex; gap: 14px; height: calc(100dvh - var(--hdrh, 62px) - var(--footh, 40px) - 34px); }
.fbrail { flex: 0 0 210px; min-width: 0; overflow-y: auto; display: flex; flex-direction: column; gap: 6px; padding-right: 2px; }
.fbrailhd { display: flex; align-items: baseline; gap: 8px; font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; color: var(--muted); padding: 0 2px 2px; }
.fbrailhd b { color: var(--text); }
/* The identity prompt (R7) is the board's own .hint, stacked: at 210px the row form leaves the
   button 40px of width. Nothing else about it changes — same panel, same muted voice. */
.fbrail .hint { flex-direction: column; align-items: stretch; margin-bottom: 0; }
.fbrow {
  display: flex; flex-direction: column; gap: 3px; width: 100%; text-align: left; flex: none;
  background: var(--panel); border: 1px solid var(--border); border-left: 3px solid transparent;
  border-radius: 9px; padding: 8px 10px;
}
.fbrow.on { border-left-color: var(--nav); background: var(--panel2); } /* where you ARE */
.fbrowt { font-size: 12.5px; font-weight: 650; line-height: 1.3; overflow-wrap: anywhere; }
.fbrow.on .fbrowt { color: var(--nav); }
.fbrowm { display: flex; align-items: center; gap: 6px; font-size: 11px; color: var(--muted); overflow-wrap: anywhere; }
/* one size down from the cards': a 210px rail row is an 11px line, not a 14px one */
.fbrowm .pridot { width: 18px; height: 18px; flex: 0 0 18px; font-size: 9.5px; }
.fbrowm .glyph svg { width: 11px; height: 11px; }
/* The rail's version reuses the pill's own violet rather than inventing a third chip: it is the same
   fact the rich view states, one size down for an 11px line. */
.fbrowm .fbrowv { flex: none; padding: 0 5px; border-radius: 5px; font-size: 10px; font-weight: 700;
  color: var(--accent); background: color-mix(in srgb, var(--accent) 16%, transparent); }

.fbmain { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 10px; }
.fbplayer { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
.fbplayer .pl { flex: 1 1 auto; min-height: 0; }
/* In the rich view the stage is 9:16 off the pane WIDTH. Here the middle column is wide and short,
   so the portrait box is driven off the available HEIGHT instead and centred. */
.fbplayer .plstagewrap { flex: 1 1 auto; min-height: 0; }
.fbplayer .plstage { width: auto; height: 100%; max-width: 100%; }
/* ...which means the picture is narrower than the column, and the scrubber, its avatar pins and the
   control row ran ~40px past both edges of the frame. review.js publishes the measured stage width
   as --stagew (syncStageWidth) and the rows follow it. The floor keeps the control row on one line
   if the viewport is ever short enough to squeeze the picture below it. */
.fbplayer .pltrack, .fbplayer .plctl, .fbplayer .plextra, .fbplayer .pltxp, .fbplayer .plerr {
  width: max(var(--stagew, 100%), 264px); max-width: 100%; margin-inline: auto;
}
.fbplayer .ph { flex: 1; display: flex; flex-direction: column; gap: 10px; align-items: center; justify-content: center; text-align: center; padding: 18px; font-size: 12.5px; color: var(--muted); border: 1px dashed var(--border); border-radius: 11px; background: #000; }

.fbverdict { flex: none; display: flex; align-items: center; justify-content: center; gap: 8px; flex-wrap: wrap; padding: 9px; border-top: 1px solid var(--border); }
.fbv { font-size: 12.5px; font-weight: 650; padding: 6px 16px; }        /* verdicts are ACTIONS */
.fbv.bad { color: var(--bad); border-color: rgba(255,112,112,.45); }
.fbv.bad:hover { border-color: var(--bad); background: rgba(255,112,112,.1); }
.fbv.ok { color: var(--ok); border-color: rgba(58,216,148,.45); }
.fbv.ok:hover { border-color: var(--ok); background: rgba(58,216,148,.1); }
.fbv.accent { color: var(--accent); border-color: rgba(139,124,255,.45); }
.fbv.accent:hover { border-color: var(--accent); background: rgba(139,124,255,.12); }
.fbv.on { color: var(--ink); font-weight: 700; }
.fbv.on.bad { background: var(--bad); border-color: var(--bad); }
.fbv.on.ok { background: var(--ok); border-color: var(--ok); }
.fbv.on.accent { background: var(--accent); border-color: var(--accent); }
.fbconfirm { font-size: 12.5px; color: var(--text); }
.fblink { background: none; border-color: transparent; color: var(--muted); font-size: 12px; }
.fblink:hover { color: var(--text); background: var(--panel2); border-color: var(--border); }

.fbcomments { flex: 0 0 clamp(300px, 33%, 420px); min-width: 0; display: flex; flex-direction: column; border: 1px solid var(--border); border-radius: 11px; background: var(--panel2); overflow: hidden; }
.fbcomments .ph { flex: 1; display: flex; flex-direction: column; gap: 10px; align-items: center; justify-content: center; text-align: center; padding: 18px; font-size: 12.5px; color: var(--muted); }

/* ---- modals (R5) ----
   social.benheath.cc's .ovl / .modal / .mbtns shell, value for value, so the family's three apps
   ask a question the same way. z-index 70 clears the slide-over (40) and the identity menu (60) and
   stays UNDER the toasts (80), because the toast is how a modal reports what it just did. */
.ovl { position: fixed; inset: 0; z-index: 70; background: rgba(0,0,0,.55); display: flex; align-items: center; justify-content: center; padding: 20px; }
.modal { position: relative; width: min(560px, 94vw); max-height: 90vh; overflow: auto; padding: 18px;
  background: var(--panel2); border: 1px solid var(--border); border-radius: 14px;
  box-shadow: var(--edge), 0 20px 60px rgba(0,0,0,.6); }
.modal h3 { margin: 0 0 4px; font-size: 16px; padding-right: 42px; } /* the title must not run under the X */
.modal .sub { color: var(--muted); font-size: 12px; line-height: 1.45; margin-bottom: 14px; }
.ovlx { position: absolute; top: 8px; right: 8px; width: 32px; height: 32px; padding: 0; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center; font-size: 15px; line-height: 1; color: var(--muted); }
.ovlx:hover { color: var(--text); border-color: var(--border-hi); }
.mbtns { display: flex; justify-content: flex-end; align-items: center; gap: 8px; flex-wrap: wrap; margin-top: 16px; }
/* The one ACTION in the dialog wears --accent solid; everything else is a way out. */
.primary { color: var(--ink); background: var(--accent); border-color: var(--accent); font-weight: 650; }
.primary:hover:not(:disabled) { border-color: var(--accent); filter: brightness(1.08); }
/* A shut gate must not look like a live button. The house `button:disabled { opacity: .5 }` leaves a
   saturated accent still reading as pressable, which invites the mash-and-assume-it-is-broken; a
   disabled primary drops to the app's unset language and only fills with --accent once it opens. */
.primary:disabled { background: var(--panel2); border-color: var(--border); color: var(--muted); }
.vlab { font-size: 10px; font-weight: 700; letter-spacing: .9px; text-transform: uppercase; color: var(--muted); }

/* The field as it stands, verbatim, above the button that destroys it. Monospace because the thing
   being compared is a URL, and pre-wrap because the team's hand-kept version notes are line breaks. */
.vprev { display: flex; flex-direction: column; gap: 6px; padding: 10px 11px; margin-bottom: 12px;
  background: var(--panel); border: 1px solid var(--border); border-radius: 9px; }
.vraw { white-space: pre-wrap; overflow-wrap: anywhere; max-height: 180px; overflow-y: auto;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11.5px; line-height: 1.55; color: var(--text); }
.vraw.none { font-family: inherit; font-size: 12.5px; font-style: italic; color: var(--muted); }
.vwarn { font-size: 11.5px; color: var(--warn); }
.vinput { width: 100%; padding: 8px 10px; font-size: 12.5px; }
.vnote { min-height: 17px; margin-top: 6px; font-size: 11.5px; color: var(--muted); }
.vnote.ok { color: var(--ok); }
.vnote.warn { color: var(--warn); }
.vnote.bad { color: var(--bad); }

/* The submit checklist. Whole rows are labels, so the text is as big a hit box as the box itself. */
.cklist { display: flex; flex-direction: column; gap: 1px; }
.ckrow { display: flex; align-items: flex-start; gap: 10px; padding: 7px 8px; border-radius: 8px;
  font-size: 13px; line-height: 1.45; cursor: pointer; }
.ckrow:hover { background: var(--panel3); }
.ckrow input { flex: none; width: 15px; height: 15px; margin: 2px 0 0; padding: 0; accent-color: var(--accent); cursor: pointer; }
.ckrow:has(input:checked) { color: var(--muted); }   /* answered fades back so the outstanding ones lead */
.ckrow.blocked, .ckrow.blocked:hover { cursor: default; background: none; color: var(--muted); }
.ckwhy { display: block; margin-top: 2px; font-size: 11.5px; color: var(--bad); }
.ckwho { display: flex; align-items: center; gap: 10px; margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--border); }
.ckwho select { flex: 1; min-width: 0; font-size: 12.5px; }
.ckhint { margin-top: 10px; font-size: 12px; color: var(--muted); }
.ckhint.ok { color: var(--ok); }

/* The two things an editor does to a ticket they hold, on the card and in the rich view. Both are
   ACTIONS, so both are --accent; "update version" is the quieter of the two because handing the cut
   over is the one that leaves. */
.cacts { display: flex; flex-wrap: wrap; gap: 6px; }
.cact { font-size: 11.5px; padding: 3px 9px; color: var(--muted); }
.cact:hover { color: var(--text); }
.cact.go, .pillact { color: var(--accent); border-color: rgba(139,124,255,.45); font-weight: 650; }
.cact.go:hover, .pillact:hover { border-color: var(--accent); background: rgba(139,124,255,.12); }
.pillact { font-size: 11.5px; padding: 3px 11px; border-radius: 999px; }

/* The source material's own line on a card: a place to go and, right of it, the one thing you can do
   with it. It is NOT in .cfoot, because a second control there wraps the footer and the assign
   buttons stop lining up across a row (measured at 1064: 10 of 14). */
.cfootage { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }

/* R6: what a Download footage click is about to fetch, quoted before it starts. Deliberately the
   quiet .cact and not an accent button — the footer's loud control is the one that hands the ticket
   on, and a card that shouts twice is a card nobody reads. */
.dlist { display: flex; flex-direction: column; gap: 4px; margin: 2px 0; }
.drow { display: flex; align-items: baseline; gap: 8px; font-size: 12px; padding: 5px 9px;
  background: var(--panel2); border: 1px solid var(--border); border-radius: 6px; }
.drow .dn { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.drow .ds, .drow .dst { flex: none; font-size: 11.5px; color: var(--muted); white-space: nowrap; }
.drow.ok { border-color: rgba(58,216,148,.4); }
.drow.ok .dst { color: var(--ok); }
.drow.bad .ds { color: var(--bad); }

/* ---- menus (openMenu, ported from the planner; review.js's comment filter uses the same chrome) ---- */
.menu {
  position: fixed; z-index: 60; min-width: 230px; max-height: 62vh; overflow-y: auto;
  background: var(--panel); border: 1px solid var(--border); border-radius: 10px; padding: 6px;
  box-shadow: 0 14px 34px rgba(0,0,0,.5); box-shadow: var(--edge), 0 14px 34px rgba(0,0,0,.5);
}
.menu[hidden] { display: none; }
.menuhd { font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; color: var(--muted); padding: 7px 9px 5px; }
.menuitem { display: flex; align-items: center; gap: 8px; width: 100%; text-align: left; background: none; border: 1px solid transparent; border-radius: 7px; padding: 6px 9px; font-size: 13px; }
.menuitem:hover { background: var(--panel2); border-color: var(--border); }
.menuitem.on { color: var(--accent); } /* who you have CHOSEN to be — an ACTION's colour */
.menusep { height: 1px; background: var(--border); margin: 6px 4px; }
/* Every row in a menu is navigation, so --nav cannot be what tells them apart: an anchor row wears
   the same ink as a button row, and the tick plus --accent mark the one you are on. */
a.menuitem { color: var(--text); }
a.menuitem:hover { text-decoration: none; }
.menuicon { flex: none; display: inline-flex; width: 16px; height: 16px; color: var(--muted); }
.menuicon svg { width: 100%; height: 100%; display: block; }
.menuitem:hover .menuicon { color: var(--text); }
.menuitem.on .menuicon { color: var(--accent); }
/* The family switcher's ticked row is the app you are already IN, which is a place, not a choice —
   the same --nav the tabs and every link in this app wear (DESIGN.md 0.4). Without this the one
   rule above would carry both meanings at once. */
.appmenu .menuitem.on, .appmenu .menuitem.on .menuicon { color: var(--nav); }
.menutx { display: flex; flex-direction: column; min-width: 0; }
.menulb { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.menusub { font-size: 11px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ---- the footer pipeline (T2), the planner's footer chip for chip ----
   z-index 20 is the header's: the footer is chrome, so the slide-over (40), menus (60) and modals
   (70) all still come over the top of it. */
footer {
  position: fixed; bottom: 0; left: 0; right: 0; z-index: 20;
  display: flex; align-items: center; gap: 10px;
  padding: 8px 22px; background: rgba(10,11,15,.94); backdrop-filter: blur(6px);
  border-top: 1px solid var(--border); overflow-x: auto; box-shadow: var(--edge);
}
.ftlabel { font-size: 10.5px; text-transform: uppercase; letter-spacing: 1.2px; color: var(--muted); flex: none; }
.counts { display: flex; gap: 6px; flex-wrap: wrap; }
.countchip { font-size: 11px; color: var(--muted); border: 1px solid var(--border); border-radius: 999px; padding: 2px 9px; background: var(--panel); white-space: nowrap; }
.countchip b { color: var(--text); font-weight: 600; margin-left: 4px; }
/* Stuck has no tab here — this app's three tabs are Tom's — so a chip carrying a stuck count IS the
   route in and has to read as a control. "⚠ N stuck" states it in words; the amber reinforces. */
button.countchip { line-height: inherit; cursor: pointer; }
.countchip.hasstuck { border-color: var(--warn); color: var(--text); }
.countchip.hasstuck:hover { background: var(--panel2); border-color: var(--warn); }
.countchip .stuckflag { color: var(--warn); font-weight: 600; margin-left: 6px; }

/* ---- the stuck list, inside a modal (the planner's rows, its tone, its tabless host) ----
   Tone is carried by a left rule and the age number, never by colour alone — every row also says
   "over by N days" in words. */
.stuckmodal { width: min(760px, 94vw); } /* wider than the ask-a-question modals: these rows are a list */
.stucksub { font-size: 12.5px; color: var(--muted); line-height: 1.45; margin-bottom: 14px; }
.stucklist { display: flex; flex-direction: column; gap: 8px; }
.stuckrow {
  display: flex; align-items: center; gap: 14px; padding: 10px 14px;
  background: var(--panel); border: 1px solid var(--border); border-left: 3px solid var(--border); border-radius: 10px;
}
.stuckrow.warn { border-left-color: var(--warn); }
.stuckrow.bad { border-left-color: var(--bad); }
.stuckage { flex: none; width: 58px; text-align: center; line-height: 1.1; }
.stuckage .sanum { display: block; font-size: 21px; font-weight: 680; font-variant-numeric: tabular-nums; }
.stuckage .saunit { display: block; font-size: 10.5px; color: var(--muted); text-transform: uppercase; letter-spacing: .5px; }
.stuckrow.warn .sanum { color: var(--warn); }
.stuckrow.bad .sanum { color: var(--bad); }
.stuckmid { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; }
.stuckname {
  background: none; border: none; padding: 0; text-align: left; font-size: 14px; font-weight: 550;
  color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
button.stuckname { cursor: pointer; }
button.stuckname:hover { color: var(--nav); }
.stuckmeta { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; font-size: 11.5px; color: var(--muted); }
/* Trailing, not leading (the planner separates with a ::before): this line wraps on a long ticket
   name, and a leading dot starts the second line with a bullet that separates nothing. As a ::after
   the same dot ends the first line, where it reads as "carries on below". */
.stuckmeta > span:not(:last-child)::after { content: '·'; margin-left: 8px; }
.stucksrc { font-style: italic; }
.stuckopen { flex: none; font-size: 12px; color: var(--muted); }
.stuckopen:hover { color: var(--nav); }

/* ---- toasts ---- */
/* Lifted clear of the fixed footer: a toast that covers the pipeline row hides the thing the click
   that produced it may have just changed. */
#toasts { position: fixed; left: 50%; bottom: calc(22px + var(--footh, 40px)); transform: translateX(-50%); z-index: 80; display: flex; flex-direction: column; gap: 8px; align-items: center; }
.toast { background: var(--panel2); border: 1px solid var(--border); border-radius: 9px; padding: 8px 14px; font-size: 12.5px; box-shadow: 0 8px 24px rgba(0,0,0,.5); }
.toast.bad { border-color: var(--bad); color: var(--bad); }

/* ---- narrow ---- */
/* THE LADDER, applied by fitHeader() in rung order rather than by a breakpoint. R2's 1770px was
   measured honestly and went stale the moment T1 changed what the right column holds (the identity
   chip left, two icon buttons arrived); a measurement cannot go stale. The rungs, cheapest first:
   1. compact — the two selects narrow. Buys ~80px and costs nothing but label room.
   2. wrapped — the filter row takes its own line. This is the FLOOR: the filters are the app's
      controls, not furniture, so the rung after it would be hiding one, and the planner's own
      answer (shed the tab labels) needs tab icons this app does not have. */
header.compact .filters select { max-width: 88px; }
header.wrapped { row-gap: 10px; }
/* flex-basis, not width: a `flex: 1 1 0` column ignores width on the main axis and would try to
   share the first line instead of dropping to its own. */
header.wrapped .hdrright { order: 3; flex: 1 1 100%; justify-content: flex-end; }
/* ...which leaves the top line with a growing brand and nothing to its right, so the tabs were
   shoved 389px off centre the moment the row wrapped. A generated third column restores the
   equal-columns trick with no extra element: it exists only while the row is wrapped. */
header.wrapped::after { content: ''; order: 2; flex: 1 1 0; }
header.wrapped .filters select { max-width: 200px; }
@media (max-width: 900px) {
  header { padding: 10px 14px; }
  .viewtoggle { order: 3; width: 100%; justify-content: center; }
  /* On a phone three unclamped selects are wider than the viewport, and justify-content: flex-end
     then pushed the identity chip clean off the LEFT edge (measured -102px) — unreachable, with no
     horizontal scrollbar to give it away, because nothing overflowed to the right. Let them wrap. */
  .hdrright { flex-wrap: wrap; }
  .filters { order: 4; flex: 1 1 100%; flex-wrap: wrap; }
  /* the select's WRAPPER is the flex child now, so it is the one that has to grow */
  .filters .selfield { flex: 1 1 auto; min-width: 96px; }
  .filters select { max-width: none; width: 100%; }
  .view { padding: 14px 14px 24px; }
  /* In flow, not pinned: on a phone a fixed bar is a third of the thumb zone, and measureChrome
     reads position: static as "reserve nothing", so the padding above is the whole story. */
  footer { position: static; padding: 10px 14px calc(14px + env(safe-area-inset-bottom)); backdrop-filter: none; -webkit-backdrop-filter: none; }
  .stuckrow { gap: 10px; padding: 9px 11px; }
  .stuckage { width: 46px; }
  .stuckage .sanum { font-size: 18px; }
  .stuckname { white-space: normal; }
  /* one item per line here, so the separator has nothing left to separate */
  .stuckmeta > span:not(:last-child)::after { content: none; }
  .rvbody { flex-direction: column; overflow-y: auto; }
  .rvplayer { flex: none; }
  #viewFeedback { padding: 14px 14px 40px; }
  .fb { flex-direction: column; height: auto; min-height: 0; }
  .fbrail { flex: none; max-height: 190px; }
  .fbplayer .plstage { width: 100%; height: auto; }
  .fbcomments { flex: none; min-height: 340px; }
  /* Phone modals are bottom sheets, the same move social and the planner make: pinned low, rounded
     top, the body scrolls and the buttons stay in thumb reach. */
  .ovl { align-items: flex-end; padding: 0; }
  .modal { width: 100%; max-height: 92vh; border-radius: 16px 16px 0 0; }
}
