rvlib-shipkit 2.1.1 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/package.json +2 -1
  3. package/src/agent-time/mask.ts +43 -0
  4. package/src/agent-time/render.ts +187 -0
  5. package/src/agent-time/report.ts +335 -0
  6. package/src/agent-time/shape.ts +271 -0
  7. package/src/agent-time/signals.ts +84 -0
  8. package/src/agent-time/store.ts +379 -0
  9. package/src/agent-time/timeline.ts +218 -0
  10. package/src/agent-time/turns.ts +135 -0
  11. package/src/assets/kits/identity-kit.ts +7 -3
  12. package/src/assets/run-assets.ts +18 -4
  13. package/src/checks/SK004-gitignore-policy.check.ts +11 -6
  14. package/src/checks/SK013-one-way-in-policy.check.ts +13 -8
  15. package/src/cli/cmd_Calls.ts +53 -0
  16. package/src/cli/cmd_Commands.ts +163 -43
  17. package/src/cli/cmd_Review.ts +948 -0
  18. package/src/cli/cmd_Shot.ts +63 -0
  19. package/src/cli/cmd_Start.ts +8 -4
  20. package/src/cli/cmd_Time.ts +112 -0
  21. package/src/cli/cmd_Trailer.ts +361 -0
  22. package/src/cli/kit/builtins.ts +40 -0
  23. package/src/cli/kit/calls.ts +177 -0
  24. package/src/cli/kit/families.ts +67 -0
  25. package/src/cli/kit/index.ts +8 -0
  26. package/src/cli/kit/pages-check.ts +36 -0
  27. package/src/cli/kit/project-cli.ts +73 -0
  28. package/src/cli/kit/verb.ts +167 -0
  29. package/src/cli.ts +15 -2
  30. package/src/collections/collection-types.ts +6 -1
  31. package/src/collections/providers.ts +3 -0
  32. package/src/collections/review.ts +70 -0
  33. package/src/core/layout.ts +12 -2
  34. package/src/core/migrate-layout.ts +31 -8
  35. package/src/core/one-way-in-policy.ts +12 -4
  36. package/src/reflect/prompt-corpus.ts +15 -114
  37. package/src/reflect/transcript.ts +254 -0
  38. package/src/review/anim.ts +228 -0
  39. package/src/review/auto.ts +141 -0
  40. package/src/review/format.ts +150 -0
  41. package/src/review/http.ts +23 -0
  42. package/src/review/marks.ts +43 -0
  43. package/src/review/media.ts +45 -0
  44. package/src/review/model.ts +854 -0
  45. package/src/review/project.ts +110 -0
  46. package/src/review/role.ts +48 -0
  47. package/src/review/routes.ts +228 -0
  48. package/src/review/store.ts +1002 -0
  49. package/src/review/threads.ts +237 -0
  50. package/src/review/trailer-desk.ts +385 -0
  51. package/src/review/trailer-model.ts +183 -0
  52. package/src/review/trailer-routes.ts +136 -0
  53. package/src/review/unit.ts +165 -0
  54. package/src/review/watchers.ts +54 -0
  55. package/src/scaffold/cli-skeleton.ts +149 -0
  56. package/src/scaffold/files/agent-index.md +2 -2
  57. package/src/scaffold/files/tsconfig.json +3 -0
  58. package/src/scaffold/files/verify-skill.md +1 -1
  59. package/src/scaffold/project-scaffold.ts +90 -12
  60. package/src/serve/embed.ts +35 -0
  61. package/src/serve/hub-app/activity-view.tsx +1 -1
  62. package/src/serve/hub-app/activity.css +0 -4
  63. package/src/serve/hub-app/dock-view.tsx +5 -4
  64. package/src/serve/hub-app/hash-route.ts +28 -1
  65. package/src/serve/hub-app/header-bar.tsx +3 -1
  66. package/src/serve/hub-app/home-st.ts +10 -2
  67. package/src/serve/hub-app/home-strip.tsx +3 -2
  68. package/src/serve/hub-app/home.html +1 -0
  69. package/src/serve/hub-app/hub-app.tsx +80 -11
  70. package/src/serve/hub-app/hub.css +45 -13
  71. package/src/serve/hub-app/icons.tsx +9 -0
  72. package/src/serve/hub-app/index.html +1 -0
  73. package/src/serve/hub-app/insights-view.tsx +4 -3
  74. package/src/serve/hub-app/keys.ts +14 -10
  75. package/src/serve/hub-app/layout-st.ts +63 -2
  76. package/src/serve/hub-app/planned-view.tsx +3 -2
  77. package/src/serve/hub-app/planned.css +0 -1
  78. package/src/serve/hub-app/project-list.css +22 -4
  79. package/src/serve/hub-app/project-list.tsx +41 -8
  80. package/src/serve/hub-app/projects-svc.ts +4 -0
  81. package/src/serve/hub-app/repo-sections.ts +15 -3
  82. package/src/serve/hub-app/repo-st.ts +45 -3
  83. package/src/serve/hub-app/review/clip-st.ts +208 -0
  84. package/src/serve/hub-app/review/kind-view.tsx +171 -0
  85. package/src/serve/hub-app/review/kit.tsx +113 -0
  86. package/src/serve/hub-app/review/review-answer.tsx +239 -0
  87. package/src/serve/hub-app/review/review-board.tsx +46 -0
  88. package/src/serve/hub-app/review/review-list.tsx +156 -0
  89. package/src/serve/hub-app/review/review-page.tsx +101 -0
  90. package/src/serve/hub-app/review/review-st.ts +779 -0
  91. package/src/serve/hub-app/review/review-views.tsx +503 -0
  92. package/src/serve/hub-app/review/trailer-page.tsx +416 -0
  93. package/src/serve/hub-app/review/trailer-st.ts +602 -0
  94. package/src/serve/hub-app/review/trailer-timeline.tsx +207 -0
  95. package/src/serve/hub-app/review.css +329 -0
  96. package/src/serve/hub-app/theme-st.ts +56 -0
  97. package/src/serve/hub-app/tree-build.ts +4 -0
  98. package/src/serve/hub-app/tree-row.tsx +1 -1
  99. package/src/serve/hub-app/tree-svc.ts +4 -0
  100. package/src/serve/hub-runtime.ts +5 -3
  101. package/src/serve/same-origin.ts +5 -1
  102. package/src/serve/serve-hub.ts +8 -0
  103. package/src/shipkit-files.ts +12 -4
  104. package/src/shot/client.ts +67 -0
  105. package/src/shot/clip-encoder.ts +146 -0
  106. package/src/shot/dom-clip.ts +124 -0
  107. package/src/shot/serve-shot.ts +54 -0
  108. package/src/shot/shooter.ts +343 -0
  109. package/src/shot/shot-ask.ts +189 -0
  110. package/src/shot/shot.shipkit.ts +14 -0
  111. package/src/smoke/run-smoke.ts +2 -2
  112. package/src/start-repo.ts +29 -14
  113. package/src/suite/judge-suites.ts +56 -10
  114. package/src/suite/select.ts +2 -5
  115. package/src/suite/suite-cli.ts +3 -2
  116. package/src/trailer/adapter.ts +78 -0
  117. package/src/trailer/build.ts +603 -0
  118. package/src/trailer/ffmpeg.ts +72 -0
  119. package/src/trailer/footage.ts +173 -0
  120. package/src/trailer/grammar.ts +1290 -0
  121. package/src/trailer/graph.ts +508 -0
  122. package/src/trailer/holes.ts +72 -0
  123. package/src/trailer/load-unit.ts +31 -0
  124. package/src/trailer/look.ts +124 -0
  125. package/src/trailer/passage.ts +129 -0
  126. package/src/trailer/plan.ts +192 -0
  127. package/src/trailer/prune.ts +193 -0
  128. package/src/trailer/score/compose.ts +240 -0
  129. package/src/trailer/score/dsp.ts +272 -0
  130. package/src/trailer/score/palette.ts +188 -0
  131. package/src/trailer/score/pattern.ts +91 -0
  132. package/src/trailer/score/score.ts +214 -0
  133. package/src/trailer/score/sound.ts +112 -0
  134. package/src/trailer/score/synth.ts +361 -0
  135. package/src/trailer/states.ts +76 -0
  136. package/src/trailer/stitch.ts +233 -0
  137. package/src/trailer/titles.ts +379 -0
  138. package/src/trailer/url.ts +108 -0
  139. package/src/trailer/vertical-draw.ts +286 -0
  140. package/src/trailer/vertical.ts +148 -0
  141. package/src/trailer/vocab.ts +59 -0
  142. package/src/trailer/words.ts +254 -0
@@ -0,0 +1,207 @@
1
+ // the trailer page's timeline: the sections, the shots, the captions and the notes of the render on screen, a
2
+ // playhead, a click or a drag to seek, a hover with the shot's frame from the contact strip; then every shot as a card
3
+ import { observer } from 'mobx-react-lite'
4
+ import { useEffect, useRef, useState, type CSSProperties, type RefObject } from 'react'
5
+ import { clock, shotAt, type ShotSpan, type Timeline } from '../../../review/trailer-model.ts'
6
+ import { captionText, type PlacedNote, type TrailerSt } from './trailer-st.ts'
7
+
8
+ const RULER = 18
9
+ const SECTIONS = 22
10
+ const SHOTS = 46
11
+ const CAPTIONS = 22
12
+ const GAP = 3
13
+ const HEIGHT = RULER + SECTIONS + SHOTS + CAPTIONS + GAP * 3
14
+ const ROW = { ruler: 0, sections: RULER + GAP, shots: RULER + SECTIONS + GAP * 2, captions: RULER + SECTIONS + SHOTS + GAP * 3 }
15
+
16
+ /** a section's hue, one of the page's kind hues by its order: the shot blocks of a section share it */
17
+ export const hueOf = (tl: Timeline, section: string | null): string => {
18
+ const i = section === null ? -1 : tl.sections.findIndex((s) => s.name === section)
19
+ return i < 0 ? 'hue-none' : `hue-k${(i % 6) + 1}`
20
+ }
21
+
22
+ /** a shot's frame from the render's contact strip, `w` pixels wide; a plain tile when there is no strip */
23
+ export const Thumb = observer(({ st, index, w, style }: { st: TrailerSt; index: number; w: number; style?: CSSProperties }) => {
24
+ const s = st.shown
25
+ const url = st.stripUrl
26
+ const g = s?.strip
27
+ const h = Math.round((w * (s?.timeline.size[1] ?? 9)) / (s?.timeline.size[0] ?? 16))
28
+ const base: CSSProperties = { width: w, height: h, ...style }
29
+ if (!url || !g || !s) return <div className="tr-thumb" style={base} />
30
+ const n = s.timeline.shots.length
31
+ const rows = Math.ceil(n / g.cols)
32
+ const k = w / g.tileW
33
+ const col = index % g.cols
34
+ const row = Math.floor(index / g.cols)
35
+ return (
36
+ <div className="tr-thumb" style={{
37
+ ...base,
38
+ backgroundImage: `url(${url})`,
39
+ backgroundSize: `${(g.cols * g.tileW + (g.cols - 1) * g.pad) * k}px ${(rows * g.tileH + (rows - 1) * g.pad) * k}px`,
40
+ backgroundPosition: `${-col * (g.tileW + g.pad) * k}px ${-row * (g.tileH + g.pad) * k}px`,
41
+ }} />
42
+ )
43
+ })
44
+
45
+ /** the ruler's step: the smallest that leaves room for a label */
46
+ function stepOf(pps: number): number {
47
+ for (const s of [0.5, 1, 2, 5, 10, 15, 30, 60]) if (s * pps >= 56) return s
48
+ return 120
49
+ }
50
+
51
+ export const TrailerTimeline = observer(({ st }: { st: TrailerSt }) => {
52
+ const tl = st.timeline
53
+ const scroller = useRef<HTMLDivElement>(null)
54
+ const [width, setWidth] = useState(800)
55
+ const [hover, setHover] = useState<{ t: number; x: number } | null>(null)
56
+ useEffect(() => {
57
+ const el = scroller.current
58
+ if (!el) return
59
+ const ro = new ResizeObserver(() => setWidth(el.clientWidth))
60
+ ro.observe(el)
61
+ setWidth(el.clientWidth)
62
+ return () => ro.disconnect()
63
+ }, [])
64
+ // a zoom keeps the playhead where the eye is
65
+ useEffect(() => {
66
+ const el = scroller.current
67
+ if (!el || !tl) return
68
+ el.scrollLeft = Math.max(0, (st.time / tl.total) * width * st.zoom - width / 2)
69
+ }, [st.zoom, width, tl, st])
70
+ if (!tl) return null
71
+ const W = Math.max(width, width * st.zoom)
72
+ const xOf = (t: number): number => (t / tl.total) * W
73
+ const tOf = (clientX: number): number => {
74
+ const el = scroller.current
75
+ if (!el) return 0
76
+ const r = el.getBoundingClientRect()
77
+ return Math.max(0, Math.min(tl.total, ((clientX - r.left + el.scrollLeft) / W) * tl.total))
78
+ }
79
+ const step = stepOf(W / tl.total)
80
+ const ticks: number[] = []
81
+ for (let t = 0; t <= tl.total + 1e-6; t += step) ticks.push(t)
82
+ const cur = st.shot
83
+ const lit = new Set(st.captionsKey.split('\n'))
84
+ return (
85
+ <div className="tr-timelinewrap">
86
+ <div ref={scroller} className="tr-timeline"
87
+ onPointerDown={(e) => {
88
+ if ((e.target as Element).closest('[data-note]')) return
89
+ e.currentTarget.setPointerCapture(e.pointerId)
90
+ st.seekPaused(tOf(e.clientX))
91
+ }}
92
+ onPointerMove={(e) => {
93
+ const t = tOf(e.clientX)
94
+ if (e.buttons & 1 && e.currentTarget.hasPointerCapture(e.pointerId)) st.seekPaused(t)
95
+ setHover({ t, x: e.clientX })
96
+ }}
97
+ onPointerLeave={() => setHover(null)}>
98
+ <div className="tr-track" style={{ width: W, height: HEIGHT }}>
99
+ {ticks.map((t) => (
100
+ <div key={t} className="tr-tick" style={{ left: xOf(t), top: ROW.ruler, height: HEIGHT }}>
101
+ <span>{t >= 60 ? `${Math.floor(t / 60)}:${String(Math.floor(t % 60)).padStart(2, '0')}` : `${t}s`}</span>
102
+ </div>
103
+ ))}
104
+ {tl.sections.map((s) => (
105
+ <div key={`${s.name}@${s.start}`} className={`tr-section ${hueOf(tl, s.name)}${st.section?.name === s.name ? ' on' : ''}`} title={`${s.name} · ${clock(s.start)} → ${clock(s.end)}`}
106
+ style={{ left: xOf(s.start) + 1, width: Math.max(2, xOf(s.end) - xOf(s.start) - 2), top: ROW.sections, height: SECTIONS, lineHeight: `${SECTIONS}px` }}>
107
+ {s.name}
108
+ </div>
109
+ ))}
110
+ {tl.shots.map((s, i) => <ShotBlock key={`${s.name}@${i}`} s={s} i={i} x={xOf(s.start)} w={xOf(s.end) - xOf(s.start)} on={cur === s} hue={hueOf(tl, s.section)} />)}
111
+ {tl.shots.flatMap((s) =>
112
+ s.captions.map((c, j) => (
113
+ <div key={`${s.name}-${j}`} className={`tr-caption${lit.has(`${c.start}|${c.text}`) ? ' on' : ''}`} title={captionText(c)}
114
+ style={{ left: xOf(c.start), width: Math.max(2, xOf(Math.min(c.end, tl.total)) - xOf(c.start)), top: ROW.captions, height: CAPTIONS, lineHeight: `${CAPTIONS}px` }}>
115
+ {captionText(c).replaceAll('\n', ' ')}
116
+ </div>
117
+ )),
118
+ )}
119
+ {st.here.map((n) => <NotePin key={n.note.id} st={st} n={n} x={xOf(n.at ?? 0)} />)}
120
+ {st.draft ? <div className="tr-draftmark" style={{ left: xOf(st.draft.time) - 1, height: HEIGHT }} /> : null}
121
+ <Playhead st={st} W={W} total={tl.total} scroller={scroller} />
122
+ </div>
123
+ </div>
124
+ {hover ? <HoverCard st={st} t={hover.t} x={hover.x} scroller={scroller} /> : null}
125
+ </div>
126
+ )
127
+ })
128
+
129
+ const ShotBlock = ({ s, i, x, w, on, hue }: { s: ShotSpan; i: number; x: number; w: number; on: boolean; hue: string }) => (
130
+ <div className={`tr-shot ${hue}${on ? ' on' : ''}${i % 2 ? ' odd' : ''}`} title={`${s.name} · ${clock(s.start)} · ${s.length} s`}
131
+ style={{ left: x + 0.5, width: Math.max(1, w - 1), top: ROW.shots, height: SHOTS }}>
132
+ {w > 26 ? s.name : ''}
133
+ {w > 44 ? <div className="tr-shottime">{clock(s.start)}</div> : null}
134
+ </div>
135
+ )
136
+
137
+ const NotePin = observer(({ st, n, x }: { st: TrailerSt; n: PlacedNote; x: number }) => {
138
+ const on = st.selected === n.note.id
139
+ return (
140
+ <>
141
+ <div className={`tr-noteline ns-${n.note.state}${on ? ' on' : ''}`} style={{ left: x, top: ROW.sections, height: HEIGHT - ROW.sections }} />
142
+ <div data-note className={`tr-notepin ns-${n.note.state}${on ? ' on' : ''}`} title={`#${n.note.n} · ${n.note.text}`} style={{ left: x - 8 }}
143
+ onPointerDown={(e) => e.stopPropagation()} onClick={() => st.goTo(n)} />
144
+ </>
145
+ )
146
+ })
147
+
148
+ /** the playhead; while it plays, the timeline scrolls to keep it in view */
149
+ const Playhead = observer(({ st, W, total, scroller }: { st: TrailerSt; W: number; total: number; scroller: RefObject<HTMLDivElement | null> }) => {
150
+ const x = (st.time / total) * W
151
+ useEffect(() => {
152
+ const el = scroller.current
153
+ if (!el) return
154
+ if (x < el.scrollLeft + 8 || x > el.scrollLeft + el.clientWidth - 24) el.scrollLeft = Math.max(0, x - el.clientWidth * 0.2)
155
+ }, [x, scroller])
156
+ return <div className="tr-playhead" style={{ left: x - 1, height: HEIGHT }}><div className="tr-playknob" /></div>
157
+ })
158
+
159
+ /** under the mouse: the shot's frame, its name, the time */
160
+ const HoverCard = observer(({ st, t, x, scroller }: { st: TrailerSt; t: number; x: number; scroller: RefObject<HTMLDivElement | null> }) => {
161
+ const tl = st.timeline
162
+ if (!tl) return null
163
+ const s = shotAt(tl, t)
164
+ if (!s) return null
165
+ const i = tl.shots.indexOf(s)
166
+ const top = scroller.current?.getBoundingClientRect().top ?? 0
167
+ return (
168
+ <div className="tr-hover" style={{ left: Math.max(8, Math.min(window.innerWidth - 216, x - 104)), bottom: window.innerHeight - (top - 8) }}>
169
+ <Thumb st={st} index={i} w={192} />
170
+ <div className="tr-hoverrow"><b>{s.name}</b><span>{clock(t)}</span></div>
171
+ <div className="tr-dim">{s.section ?? 'opening'} · shot {i + 1}/{tl.shots.length} · {s.length} s</div>
172
+ </div>
173
+ )
174
+ })
175
+
176
+ /** every shot as a card: its frame, its name, its start and length, its open notes; the one on screen lit and kept in
177
+ * view */
178
+ export const ShotCards = observer(({ st }: { st: TrailerSt }) => {
179
+ const tl = st.timeline
180
+ const box = useRef<HTMLDivElement>(null)
181
+ const cur = st.shotIndex
182
+ useEffect(() => {
183
+ const el = box.current
184
+ const card = el?.children[cur] as HTMLElement | undefined
185
+ if (!el || !card) return
186
+ const left = card.offsetLeft - el.offsetLeft
187
+ if (left < el.scrollLeft || left + card.offsetWidth > el.scrollLeft + el.clientWidth) el.scrollTo({ left: left - el.clientWidth / 2 + card.offsetWidth / 2, behavior: 'smooth' })
188
+ }, [cur])
189
+ if (!tl) return null
190
+ const counts = new Map<string, number>()
191
+ for (const n of st.here) if (n.note.state === 'open') counts.set(n.note.shot, (counts.get(n.note.shot) ?? 0) + 1)
192
+ return (
193
+ <div ref={box} className="tr-shotcards">
194
+ {tl.shots.map((s, i) => {
195
+ const n = counts.get(s.name) ?? 0
196
+ return (
197
+ <button type="button" key={`${s.name}@${i}`} className={`tr-shotcard ${hueOf(tl, s.section)}${i === cur ? ' on' : ''}`} onMouseDown={(e) => e.preventDefault()} onClick={() => st.seek(s.start)}>
198
+ <Thumb st={st} index={i} w={136} />
199
+ <div className="tr-shotname"><span className="tr-dim">{i + 1} </span>{s.name}</div>
200
+ <div className="tr-dim tr-small">{clock(s.start)} · {s.length} s{s.captions.length > 0 ? ` · ${s.captions.length} title${s.captions.length > 1 ? 's' : ''}` : ''}</div>
201
+ {n > 0 ? <span className="tr-shotnotes">{n}</span> : null}
202
+ </button>
203
+ )
204
+ })}
205
+ </div>
206
+ )
207
+ })
@@ -0,0 +1,329 @@
1
+ /* the review page and the trailer page (hub-app/review/): pkm-multi's Revue and Trailer tabs on the hub's tokens.
2
+ A status, a reaction, a note state and a section each set --tone or --hue, and the parts read it, so no rule here
3
+ writes a colour (tests/hub-css-invariants.test.ts) */
4
+ .st-pending{--tone:var(--blue)}
5
+ .st-approved,.re-approved{--tone:var(--ok)}
6
+ .st-approved-with-fix,.re-approved-with-fix,.st-raised{--tone:var(--warn)}
7
+ .st-rejected,.re-rejected{--tone:var(--red)}
8
+ .st-archived,.st-superseded,.st-withdrawn,.st-wontfix,.st-rated{--tone:var(--dim)}
9
+ .st-wip{--tone:var(--k2)}
10
+ .st-fixed{--tone:var(--k3)}
11
+ .ns-open{--tone:var(--warn)}
12
+ .ns-done{--tone:var(--ok)}
13
+ .ns-wontfix{--tone:var(--dim)}
14
+ .hue-k1{--hue:var(--k1)}.hue-k2{--hue:var(--k2)}.hue-k3{--hue:var(--k3)}.hue-k4{--hue:var(--k4)}.hue-k5{--hue:var(--k5)}.hue-k6{--hue:var(--k6)}
15
+ .hue-none{--hue:var(--dim)}
16
+
17
+ /* ── the three columns ── */
18
+ .rv-cols{flex:1;min-height:0}
19
+ .rv-pane{height:100%;min-width:0;display:flex;flex-direction:column}
20
+ .rv-scroll{flex:1;min-height:0;overflow-y:auto}
21
+ .rv-dim,.tr-dim{color:var(--dim)}
22
+ .rv-small,.tr-small{font-size:11px}
23
+ .rv-mono,.tr-mono{font:11.5px ui-monospace,Menlo,monospace}
24
+ .rv-pre{white-space:pre-wrap;overflow-wrap:anywhere}
25
+ .rv-center{text-align:center}
26
+ .rv-empty{padding:var(--s3);color:var(--dim);font-size:12.5px;display:flex;gap:var(--s2);align-items:center}
27
+ .rv-err{padding:var(--s1) var(--s3);color:var(--errink);font-size:12px;white-space:pre-wrap}
28
+ .rv-lanehead .sp,.rv-viewhead .sp,.tr-cutrow .sp,.tr-transport .sp,.tr-noteshead .sp,.tr-noteacts .sp{flex:1}
29
+ .rv-divider{display:flex;align-items:center;gap:var(--s2);margin:var(--s2) 0 var(--s1);color:var(--dim);font-size:11px}
30
+ .rv-divider::after{content:'';flex:1;border-top:1px solid var(--line)}
31
+ .rv-cols textarea,.tr-notescol textarea,.rv-phone textarea{min-height:0;font:12.5px/1.45 system-ui,sans-serif;padding:var(--s1) var(--s2)}
32
+
33
+ /* ── the list ── */
34
+ .rv-listcol{height:100%;display:flex;flex-direction:column;min-height:0;background:var(--panel);container-type:inline-size}
35
+ .rv-filters{display:flex;flex-direction:column;gap:var(--s2);padding:var(--s2) var(--s3);border-bottom:1px solid var(--line)}
36
+ .rv-search{background:var(--bg);border:1px solid var(--line);border-radius:4px;color:var(--ink);padding:var(--s1) var(--s2);font-size:12.5px;outline:none}
37
+ .rv-search:focus{border-color:var(--accent)}
38
+ .rv-selects{display:flex;gap:var(--s1)}
39
+ .rv-selects select{flex:1;min-width:0}
40
+ .rv-whose{align-self:flex-start}
41
+ .rv-switches{display:flex;gap:var(--s3);font-size:12px}
42
+ .rv-switch{display:inline-flex;align-items:center;gap:var(--s1);cursor:pointer;color:var(--ink)}
43
+ .rv-count{font-size:11px;color:var(--dim)}
44
+ .rv-listscroll{flex:1;min-height:0;overflow-y:auto;padding:var(--s1) 0 var(--s4)}
45
+ .rv-lanehead{display:flex;align-items:center;gap:var(--s2);margin:var(--s2) var(--s2) var(--s0);padding:var(--s1) var(--s2);border-radius:4px;border-left:4px solid var(--lane,var(--dim));background:var(--panel2);font-size:12px}
46
+ .rv-lanehead b{color:var(--lane,var(--dim))}
47
+ .rv-row{display:flex;gap:var(--s2);align-items:center;margin:var(--s1) var(--s2);padding:var(--s1) var(--s2);border-radius:6px;cursor:pointer;
48
+ border:1px solid color-mix(in srgb,var(--tone) 45%,var(--line));background:color-mix(in srgb,var(--tone) 9%,var(--panel))}
49
+ .rv-row:hover{background:color-mix(in srgb,var(--tone) 16%,var(--panel))}
50
+ .rv-row.on{background:color-mix(in srgb,var(--tone) 22%,var(--panel));box-shadow:inset 3px 0 0 var(--accent)}
51
+ .rv-row.laned,.rv-vrow.laned{border-left:4px solid var(--lane)}
52
+ .rv-row.muted .rv-thumb,.rv-row.muted .rv-rowbody{opacity:.55}
53
+ .rv-thumb{width:56px;height:56px;flex:none;border-radius:4px;overflow:hidden;background:repeating-conic-gradient(var(--chk1) 0 25%,var(--chk2) 0 50%) 0 0/12px 12px}
54
+ .rv-thumb.sm{width:32px;height:32px}
55
+ .rv-thumb.xs{width:36px;height:36px;cursor:pointer}
56
+ .rv-thumbimg{width:100%;height:100%;object-fit:cover;display:block}
57
+ .rv-thumbnone{width:100%;height:100%;display:grid;place-items:center;color:var(--dim);background:var(--panel2)}
58
+ .rv-rowbody{flex:1;min-width:0}
59
+ .rv-rowtitle{font-size:13px;line-height:1.3;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
60
+ .rv-rowtitle.strong{font-weight:700}
61
+ .rv-unread{display:inline-block;width:8px;height:8px;border-radius:4px;background:var(--blue);margin-right:var(--s1);vertical-align:middle}
62
+ .rv-rowmeta{display:flex;gap:var(--s1);align-items:center;margin-top:var(--s0);min-width:0}
63
+ .rv-badge{flex:none;font-size:9.5px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;padding:0 var(--s1);border-radius:3px;line-height:16px;
64
+ color:var(--bg);background:var(--tone,var(--dim));white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:120px}
65
+ .rv-badge.outline{background:none;color:var(--ink);border:1px solid var(--line)}
66
+ .rv-about{font-size:11px;color:var(--dim);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}
67
+ .rv-fold{flex:none;display:grid;place-items:center;width:24px;height:24px;padding:0;border:1px solid var(--line);border-radius:999px;color:var(--dim)}
68
+ .rv-fold .cvt{width:14px;height:14px;transform:rotate(90deg)}
69
+ .rv-fold .cvt.open{transform:rotate(-90deg)}
70
+ .rv-fold:hover{color:var(--ink);border-color:var(--accent)}
71
+ /* a narrow list: the title on its own line on top, a smaller image under it beside the badges */
72
+ @container (max-width:250px){
73
+ .rv-row{display:grid;grid-template-columns:40px minmax(0,1fr) auto;grid-template-areas:"title title fold" "thumb meta meta" "rework rework rework";align-items:center;column-gap:var(--s2);row-gap:var(--s1)}
74
+ .rv-row .rv-rowbody{display:contents}
75
+ .rv-row .rv-rowtitle{grid-area:title}
76
+ .rv-row .rv-thumb{grid-area:thumb;width:40px;height:40px}
77
+ .rv-row .rv-rowmeta{grid-area:meta;flex-wrap:wrap;margin-top:0}
78
+ .rv-row .rv-fold{grid-area:fold}
79
+ .rv-row > .rv-rowbody > :not(.rv-rowtitle):not(.rv-rowmeta){grid-area:rework}
80
+ .rv-row.muted .rv-rowtitle,.rv-row.muted .rv-rowmeta{opacity:.55}
81
+ /* the filters: one select a line, the whose bar across the width, the switches one a line */
82
+ .rv-filters{padding:var(--s2)}
83
+ .rv-selects{flex-direction:column}
84
+ .rv-whose{align-self:stretch}
85
+ .seg.rv-whose button{flex:1 1 auto;justify-content:center;padding:0 var(--s1);font-size:11px;white-space:nowrap}
86
+ .rv-switches{flex-direction:column;gap:var(--s1)}
87
+ }
88
+ .rv-vrow{display:flex;gap:var(--s1);align-items:center;margin:var(--s0) var(--s2) var(--s0) var(--s4);padding:var(--s0) var(--s1);border-radius:4px;cursor:pointer;opacity:.8;
89
+ border:1px solid color-mix(in srgb,var(--tone) 40%,var(--line));background:color-mix(in srgb,var(--tone) 8%,var(--panel));font-size:11.5px}
90
+ .rv-vrow.on{opacity:1;box-shadow:inset 3px 0 0 var(--accent)}
91
+ .rv-vn{flex:none}
92
+ .rv-vtitle{min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
93
+ .rv-lane{flex:none;font-size:10px;font-weight:700;color:var(--bg);background:var(--lane);border-radius:3px;padding:0 var(--s1);line-height:16px}
94
+ .rv-auto{flex:none;font-size:10px;font-weight:700;color:var(--bg);background:var(--warn);border-radius:3px;padding:0 var(--s1);line-height:16px}
95
+ .rv-reworkrow{display:flex;margin-top:var(--s1)}
96
+ .rv-rework{display:inline-flex;align-items:center;gap:var(--s1);font-size:11px;font-weight:700;color:var(--lane);border:1px solid var(--lane);border-radius:4px;padding:0 var(--s1);white-space:nowrap}
97
+ .rv-spin{width:10px;height:10px;border-radius:50%;border:2px solid var(--lane);border-top-color:transparent;animation:rot 2.4s linear infinite}
98
+ .rv-listen{display:inline-flex;align-items:center;gap:var(--s1);font-size:11px;font-weight:700;color:var(--dim);border:1px solid var(--line);border-radius:10px;padding:0 var(--s2);white-space:nowrap;line-height:18px}
99
+ .rv-listen.on{color:var(--ok);border-color:color-mix(in srgb,var(--ok) 55%,var(--line))}
100
+
101
+ /* ── the views ── */
102
+ .rv-views{display:flex;flex-direction:column;gap:var(--s4);padding:var(--s3)}
103
+ .rv-viewsbar{display:flex;flex-wrap:wrap;gap:var(--s2);justify-content:space-between;align-items:center}
104
+ .rv-viewsctl{display:inline-flex;gap:var(--s2);align-items:center}
105
+ .rv-jumps{display:flex;flex-wrap:wrap;gap:var(--s1);flex:1;min-width:0}
106
+ .rv-jump{font-size:11.5px;padding:var(--s0) var(--s2);border-color:transparent;color:var(--dim);display:inline-flex;align-items:center;gap:var(--s1)}
107
+ .rv-jump.on{color:var(--ink);border-color:var(--line);background:var(--panel2)}
108
+ .rv-jdot{width:8px;height:8px;border-radius:4px;background:var(--tone)}
109
+ .rv-view{scroll-margin-top:8px;padding-left:var(--s2);border-left:4px solid transparent}
110
+ .rv-view.active{border-left-color:var(--line)}
111
+ .rv-view[class*=' re-']{border-left-color:var(--tone)}
112
+ .rv-viewhead{display:flex;flex-wrap:wrap;gap:var(--s2);align-items:center;margin-bottom:var(--s2)}
113
+ .rv-viewname{font-size:14px}
114
+ .rv-reacts{display:inline-flex;gap:var(--s1)}
115
+ .rv-react{font-size:11.5px;padding:0 var(--s2);line-height:20px;color:var(--tone);border-color:color-mix(in srgb,var(--tone) 45%,var(--line));background:color-mix(in srgb,var(--tone) 10%,transparent)}
116
+ .rv-react.on{color:var(--bg);background:var(--tone);border-color:var(--tone)}
117
+ .rv-react.off{opacity:.45}
118
+ .rv-word{margin-top:var(--s2)}
119
+ .rv-raised{padding:var(--s2) var(--s3);border:1px solid var(--line);border-left:4px solid var(--warn);border-radius:4px;background:var(--panel)}
120
+ .rv-raisedimg{display:block;max-width:100%;max-height:320px;margin-top:var(--s2);border-radius:4px}
121
+ .rv-pairgrid{margin:0 auto;display:grid;gap:var(--s2)}
122
+ .rv-cell{display:flex;flex-direction:column;align-items:center;gap:var(--s0);min-width:0}
123
+ .rv-frame{position:relative;width:100%;max-width:100%;border-radius:4px;border:1px solid var(--line);overflow:hidden;
124
+ background:repeating-conic-gradient(var(--chk1) 0 25%,var(--chk2) 0 50%) 0 0/16px 16px}
125
+ .rv-frame.swipe{cursor:ew-resize;touch-action:none}
126
+ .rv-fill{position:absolute;inset:0}
127
+ .rv-caption{font-size:11px;color:var(--dim)}
128
+ .rv-caption.mod,.rv-modcap{color:var(--k2)}
129
+ .rv-curtain{position:absolute;top:0;bottom:0;width:2px;transform:translateX(-1px);background:var(--k2);pointer-events:none}
130
+ .rv-modes{position:relative;display:inline-flex;flex-direction:column;gap:var(--s1)}
131
+ .rv-onion{position:absolute;top:100%;left:0;right:0;z-index:3;margin-top:var(--s0)}
132
+ .rv-marks{position:absolute;inset:0;user-select:none}
133
+ .rv-marks.draw{cursor:crosshair}
134
+ .rv-marks.pick{cursor:pointer}
135
+ .rv-markimg,.rv-video{display:block;width:100%;height:100%;pointer-events:none}
136
+ .rv-rings{position:absolute;inset:0;width:100%;height:100%;pointer-events:none}
137
+ .rv-ring{fill:none;stroke:var(--warn);stroke-width:3}
138
+ .rv-ringshade{fill:none;stroke:var(--solid);stroke-opacity:.35;stroke-width:7}
139
+ .rv-rect{position:absolute;border:2px solid var(--warn);box-shadow:0 0 0 1px var(--hi1);background:color-mix(in srgb,var(--warn) 7%,transparent);cursor:pointer}
140
+ .rv-rect.on{border-width:3px;border-color:var(--blue)}
141
+ .rv-rect.faint{border-style:dashed;opacity:.6;box-shadow:none;background:none}
142
+ .rv-rectn{position:absolute;left:-2px;top:-2px;transform:translateY(-100%);padding:0 var(--s1);border-radius:3px;background:var(--warn);color:var(--bg);font-size:11px;font-weight:800;line-height:16px;white-space:nowrap}
143
+ .rv-rect.on .rv-rectn{background:var(--blue)}
144
+ .rv-drag{position:absolute;border:2px dashed var(--warn)}
145
+ .rv-downloads{display:flex;gap:var(--s2);justify-content:flex-end;margin-top:var(--s1)}
146
+ .rv-dl{font-size:11.5px;text-decoration:none}
147
+ .rv-gallery{display:flex;flex-wrap:wrap;gap:var(--s2);align-items:flex-start}
148
+ .rv-tile{display:flex;flex-direction:column;gap:var(--s1);padding:var(--s1);border-radius:6px;border:2px solid var(--line)}
149
+ .rv-tile[class*=' re-']{border-color:var(--tone);background:color-mix(in srgb,var(--tone) 8%,transparent)}
150
+ .rv-tile.big{width:100%}
151
+ .rv-tileimg{border-radius:4px;cursor:zoom-in;margin:0 auto;width:100%;background:repeating-conic-gradient(var(--chk1) 0 25%,var(--chk2) 0 50%) 0 0/12px 12px}
152
+ .rv-tile.big .rv-tileimg{cursor:zoom-out}
153
+ .rv-tileimg img{display:block;width:100%;height:100%;object-fit:contain}
154
+ .rv-tilelabel{font-size:11px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
155
+ .rv-clipbar{display:flex;gap:var(--s1);align-items:center;margin-top:var(--s2)}
156
+ .rv-clipbar button{padding:0 var(--s2);line-height:22px}
157
+ .rv-scrub{flex:1;min-width:60px}
158
+ .rv-sound{display:inline-flex;align-items:center;gap:var(--s1)}
159
+ .rv-sound button{padding:0 var(--s1);line-height:22px}
160
+ .rv-sound input{width:72px}
161
+ .rv-phonepair{display:flex;flex-direction:column;gap:var(--s2)}
162
+
163
+ /* ── the answer ── */
164
+ .rv-form{display:flex;flex-direction:column;gap:var(--s2);padding:var(--s3)}
165
+ .rv-status{padding:var(--s1) var(--s3);border-radius:4px;border:1px solid color-mix(in srgb,var(--tone) 45%,var(--line));border-left:4px solid var(--tone);background:color-mix(in srgb,var(--tone) 10%,var(--panel))}
166
+ .rv-statushead{font-weight:800;color:var(--tone);display:flex;gap:var(--s1);align-items:center;flex-wrap:wrap}
167
+ .rv-title{margin:0;font-size:17px;line-height:1.3}
168
+ .rv-what{font-size:13px}
169
+ .rv-what ul{margin:var(--s1) 0;padding-left:var(--s4)}
170
+ .rv-what p{margin:var(--s1) 0}
171
+ .rv-keeps{color:var(--ok);font-size:13px}
172
+ .rv-facts{display:flex;flex-direction:column}
173
+ .rv-fact{display:flex;gap:var(--s2);align-items:center;min-height:24px;font-size:12.5px}
174
+ .rv-factl{width:64px;flex:none;color:var(--dim);font-size:11px}
175
+ .rv-factv{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
176
+ .rv-linked{padding:var(--s1) var(--s2);border:1px solid var(--line);border-radius:4px;background:var(--panel)}
177
+ .rv-linkedrow{display:flex;gap:var(--s2);align-items:center;font-size:12px}
178
+ .rv-linkedtitle{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;cursor:pointer}
179
+ .rv-outgoing,.rv-messages{display:flex;flex-direction:column;gap:var(--s1)}
180
+ .rv-outrow{display:flex;gap:var(--s1);align-items:flex-start}
181
+ .rv-outrow textarea{flex:1;min-height:44px}
182
+ .rv-outbadge{flex:none;max-width:170px;margin-top:var(--s0);font-size:11px;padding:0 var(--s2);line-height:20px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
183
+ color:var(--tone,var(--ink));border-color:color-mix(in srgb,var(--tone,var(--line)) 50%,var(--line))}
184
+ .rv-outbadge.rect{--tone:var(--warn)}
185
+ .rv-outbadge.rect.on{--tone:var(--blue)}
186
+ .rv-x{flex:none;padding:0 var(--s1);border:0}
187
+ .rv-label{font-size:12px;font-weight:600;margin-top:var(--s1)}
188
+ .rv-listenrow{display:flex;gap:var(--s2);align-items:center;font-size:11px}
189
+ .rv-verdicts{display:flex;flex-direction:column;gap:var(--s1)}
190
+ .rv-verdict{text-align:left;padding:var(--s1) var(--s3);font-size:13px;border-color:color-mix(in srgb,var(--tone) 60%,var(--line))}
191
+ .rv-verdict .rv-vglyph{color:var(--tone);font-weight:800}
192
+ .rv-verdict.chosen{background:var(--tone);color:var(--bg);border-color:var(--tone)}
193
+ .rv-verdict.chosen .rv-vglyph{color:var(--bg)}
194
+ .rv-verdict.off{opacity:.5}
195
+ .rv-msg{max-width:88%;align-self:flex-start;padding:var(--s1) var(--s2);border-radius:6px;border:1px solid var(--line);background:var(--panel2)}
196
+ .rv-msg.mine{align-self:flex-end;border-color:var(--hair);background:var(--wash)}
197
+ .rv-msgimg{display:block;max-width:100%;max-height:160px;margin-top:var(--s1);border-radius:4px}
198
+ .rv-replyrow,.rv-formfoot{display:flex;gap:var(--s2);justify-content:space-between}
199
+ .rv-replyrow{justify-content:flex-end}
200
+
201
+ /* ── the phone: the page covers the hub's panes, one pane at a time, the verdicts under the thumb ── */
202
+ .rv-phone,.tr-phone{position:fixed;inset:0;z-index:30;background:var(--bg)}
203
+ .rv-phone{display:flex;flex-direction:column}
204
+ .rv-phonetop{flex:none;display:flex;gap:var(--s2);align-items:center;padding:var(--s1) var(--s2);border-bottom:1px solid var(--line);background:var(--bg-top);font-size:13px}
205
+ .rv-phonetabs{margin:var(--s2);align-self:stretch;display:flex}
206
+ .rv-phonetabs button{flex:1;font-size:13px;padding:var(--s1) var(--s2)}
207
+ .rv-phonepane{flex:1;min-height:0;overflow-y:auto}
208
+ .rv-verdictbar{flex:none;display:flex;gap:var(--s1);padding:var(--s2);border-top:1px solid var(--line);background:var(--panel)}
209
+ .rv-verdictbar .rv-verdict{flex:1;text-align:center;font-size:14px;padding:var(--s2) var(--s1);color:var(--tone)}
210
+ .rv-verdictbar .rv-verdict.chosen{color:var(--bg)}
211
+
212
+ /* ── the trailer page ── */
213
+ .tr-cuts{display:flex;flex-direction:column;gap:var(--s2);padding:var(--s3)}
214
+ .tr-trailername{font-weight:800}
215
+ .tr-cutrows{display:flex;flex-direction:column;gap:var(--s0)}
216
+ .tr-cutrow{display:flex;align-items:center;gap:var(--s1);width:100%;padding:var(--s0) var(--s2);border:0;border-radius:4px;text-align:left;font-size:13px}
217
+ .tr-cutrow:hover{background:var(--panel2)}
218
+ .tr-cutrow.on{background:var(--sel);box-shadow:inset 2px 0 0 var(--accent);font-weight:700}
219
+ .tr-cutrow.unrendered{opacity:.45}
220
+ .tr-cutname{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
221
+ .tr-edited{color:var(--warn)}
222
+ .tr-cuterr{color:var(--red);font-weight:800}
223
+ .tr-opencount{flex:none;min-width:16px;padding:0 var(--s1);border-radius:8px;background:var(--warn);color:var(--bg);font-size:10px;font-weight:700;text-align:center;line-height:16px}
224
+ .tr-cutage{flex:none;font-size:11px;color:var(--dim);font-variant-numeric:tabular-nums;white-space:nowrap}
225
+ .tr-keys{margin-top:var(--s3);padding:var(--s2);border:1px solid var(--line);border-radius:6px;background:var(--panel);display:flex;flex-direction:column;gap:var(--s0)}
226
+ .tr-keyshead{font-size:11px;font-weight:700;color:var(--dim)}
227
+ .tr-keyrow{display:flex;gap:var(--s2);align-items:baseline;font-size:11px}
228
+ .tr-keyrow kbd{min-width:44px;text-align:center}
229
+ .tr-player{display:flex;flex-direction:column;gap:var(--s2);padding:var(--s3)}
230
+ .tr-banner{display:flex;gap:var(--s2);align-items:center;justify-content:space-between;padding:var(--s2) var(--s3);border-radius:4px;border:1px solid var(--hair);background:var(--wash);font-size:12.5px}
231
+ .tr-banner.warn{border-color:color-mix(in srgb,var(--warn) 50%,var(--line));background:color-mix(in srgb,var(--warn) 10%,var(--panel))}
232
+ .tr-screen{background:var(--solid);border-radius:8px;padding:var(--s2);display:flex;justify-content:center}
233
+ .tr-frame{position:relative}
234
+ .tr-frame video{display:block;width:100%;height:100%;border-radius:4px;background:var(--solid)}
235
+ .tr-clickzone{position:absolute;inset:0;cursor:crosshair}
236
+ .tr-paused{position:absolute;left:var(--s2);top:var(--s2);padding:var(--s0) var(--s2);border-radius:4px;background:var(--scrim);color:var(--tag);font-size:12px;font-variant-numeric:tabular-nums;pointer-events:none}
237
+ .tr-pin{position:absolute;transform:translate(-50%,-100%);pointer-events:none}
238
+ .tr-pinhead{width:22px;height:22px;border-radius:50% 50% 50% 0;transform:rotate(-45deg);background:var(--tone);border:2px solid var(--tag);box-shadow:0 2px 8px var(--shadow);display:flex;align-items:center;justify-content:center}
239
+ .tr-pinhead.on{width:28px;height:28px}
240
+ .tr-pinhead span{transform:rotate(45deg);color:var(--tag);font-weight:800;font-size:11px}
241
+ .tr-pulse{position:absolute;left:calc(50% - 12px);bottom:-6px;width:24px;height:24px;border-radius:12px;border:2px solid var(--warn);animation:trpulse 1.1s ease-out infinite}
242
+ @keyframes trpulse{from{transform:scale(.6);opacity:1}to{transform:scale(1.8);opacity:0}}
243
+ .tr-transport{display:flex;flex-wrap:wrap;gap:var(--s1);align-items:center}
244
+ .tr-tbtn{width:32px;height:32px;padding:0;border-radius:16px;border-color:transparent;font-size:15px}
245
+ .tr-tbtn.big{width:44px;height:44px;border-radius:22px;background:var(--ink);color:var(--bg);font-size:17px}
246
+ .tr-time{margin-left:var(--s1);line-height:1.15;font-variant-numeric:tabular-nums}
247
+ .tr-timebig{font-size:18px;font-weight:800}
248
+ .tr-timebig span{font-size:12px;font-weight:500}
249
+ .tr-file{font-size:11px;max-width:140px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
250
+ .tr-notebtn{color:var(--bg);background:var(--warn);border-color:var(--warn);font-weight:700;padding:var(--s1) var(--s3)}
251
+ .tr-now{display:flex;flex-wrap:wrap;gap:var(--s2);align-items:center;min-height:30px;font-size:12.5px}
252
+ .tr-secbadge{padding:0 var(--s2);border-radius:3px;background:var(--hue);color:var(--bg);font-size:11px;font-weight:800;text-transform:uppercase;letter-spacing:.04em;line-height:20px}
253
+ .tr-secbadge.small{text-transform:none;letter-spacing:0;line-height:16px;font-size:10.5px}
254
+ .tr-shotnow{font-size:16px}
255
+ .tr-capnow{font-style:italic;padding:0 var(--s2);border-radius:4px;background:color-mix(in srgb,var(--warn) 18%,transparent);white-space:pre-line}
256
+ .tr-timelinewrap{position:relative}
257
+ .tr-timeline{overflow-x:auto;overflow-y:hidden;border-radius:6px;background:var(--bg);border:1px solid var(--line);padding:var(--s1) 0 var(--s2);cursor:col-resize;user-select:none}
258
+ .tr-track{position:relative}
259
+ .tr-tick{position:absolute;border-left:1px solid var(--hi3)}
260
+ .tr-tick span{position:absolute;left:3px;top:1px;font-size:10px;color:var(--dim);font-variant-numeric:tabular-nums;white-space:nowrap}
261
+ .tr-section{position:absolute;border-radius:4px;background:color-mix(in srgb,var(--hue) 55%,var(--bg));color:var(--ink);font-size:11px;font-weight:700;padding-left:var(--s1);overflow:hidden;white-space:nowrap;text-overflow:ellipsis;opacity:.8}
262
+ .tr-section.on{background:var(--hue);color:var(--bg);opacity:1;box-shadow:0 0 0 2px var(--hi1)}
263
+ .tr-shot{position:absolute;border-radius:3px;background:var(--panel2);border-top:3px solid color-mix(in srgb,var(--hue) 60%,var(--bg));color:var(--ink);font-size:11px;font-weight:600;padding:var(--s0) var(--s1) 0;overflow:hidden;white-space:nowrap;opacity:.85}
264
+ .tr-shot.odd{background:var(--panel)}
265
+ .tr-shot.on{background:color-mix(in srgb,var(--hue) 70%,var(--bg));border-top-color:var(--hue);color:var(--bg);font-weight:800;box-shadow:0 0 0 2px var(--ink);opacity:1;z-index:1}
266
+ .tr-shottime{font-size:9px;opacity:.6;font-variant-numeric:tabular-nums}
267
+ .tr-caption{position:absolute;border-radius:3px;background:color-mix(in srgb,var(--warn) 18%,transparent);color:var(--warn);border:1px solid color-mix(in srgb,var(--warn) 35%,transparent);font-size:10px;font-style:italic;padding:0 var(--s1);overflow:hidden;white-space:nowrap;text-overflow:ellipsis}
268
+ .tr-caption.on{background:var(--warn);color:var(--bg)}
269
+ .tr-noteline{position:absolute;border-left:1px dashed var(--tone);opacity:.5;pointer-events:none}
270
+ .tr-noteline.on{border-left-style:solid;opacity:.9}
271
+ .tr-notepin{position:absolute;top:0;width:16px;height:16px;border-radius:50% 50% 50% 0;transform:rotate(-45deg);background:var(--tone);border:1px solid var(--shadow);cursor:pointer;z-index:3}
272
+ .tr-notepin.on{border:2px solid var(--tag)}
273
+ .tr-draftmark{position:absolute;top:0;width:2px;background:var(--warn);z-index:3;pointer-events:none}
274
+ .tr-playhead{position:absolute;top:0;width:2px;background:var(--red);z-index:4;pointer-events:none;box-shadow:0 0 6px var(--red)}
275
+ .tr-playknob{position:absolute;left:-5px;top:-2px;width:12px;height:8px;border-radius:3px;background:var(--red)}
276
+ .tr-hover{position:fixed;width:208px;padding:var(--s2);border-radius:6px;background:var(--panel2);border:1px solid var(--line);box-shadow:0 6px 24px var(--shadow);pointer-events:none;z-index:50;font-size:12px}
277
+ .tr-hoverrow{display:flex;justify-content:space-between;margin-top:var(--s1)}
278
+ .tr-thumb{border-radius:4px;background-color:var(--panel2);background-repeat:no-repeat;flex:none}
279
+ .tr-shotcards{display:flex;gap:var(--s2);overflow-x:auto;padding:var(--s1) var(--s0) var(--s2)}
280
+ .tr-shotcard{position:relative;flex:none;width:148px;padding:var(--s1);border-radius:6px;text-align:left;border:1px solid var(--line);background:var(--panel)}
281
+ .tr-shotcard.on{border:2px solid var(--hue);background:color-mix(in srgb,var(--hue) 12%,var(--panel))}
282
+ .tr-shotname{font-size:12px;font-weight:800;margin-top:var(--s1);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
283
+ .tr-shotnotes{position:absolute;top:var(--s2);right:var(--s2);min-width:18px;height:18px;border-radius:9px;background:var(--warn);color:var(--bg);font-size:11px;font-weight:800;text-align:center;line-height:18px;padding:0 var(--s1)}
284
+ .tr-montage{display:flex;flex-direction:column;gap:var(--s1);align-items:flex-start}
285
+ .tr-montagebody{width:100%;font-size:12.5px;padding:var(--s2) var(--s3);border:1px solid var(--line);border-radius:4px;background:var(--panel);overflow-x:auto}
286
+ .tr-notescol{height:100%;display:flex;flex-direction:column;min-height:0}
287
+ .tr-noteshead{display:flex;gap:var(--s2);align-items:center;padding:var(--s2) var(--s3);border-bottom:1px solid var(--line)}
288
+ .tr-openbadge{font-size:10px;font-weight:700;text-transform:uppercase;padding:0 var(--s1);border-radius:3px;line-height:16px;color:var(--warn);border:1px solid color-mix(in srgb,var(--warn) 50%,var(--line))}
289
+ .tr-openbadge.on{background:var(--warn);color:var(--bg)}
290
+ .tr-notesscroll{flex:1;min-height:0;overflow-y:auto;padding:var(--s2);display:flex;flex-direction:column;gap:var(--s2)}
291
+ .tr-elsewhere{font-size:11px;font-weight:700;color:var(--dim);margin-top:var(--s3)}
292
+ .tr-draft{padding:var(--s2) var(--s3);border-radius:6px;border:1px solid var(--warn);box-shadow:0 0 0 3px color-mix(in srgb,var(--warn) 18%,transparent);display:flex;flex-direction:column;gap:var(--s1)}
293
+ .tr-drafthead{display:flex;justify-content:space-between;gap:var(--s2);align-items:center;font-size:12.5px}
294
+ .tr-pinned{color:var(--warn);font-size:11px;font-weight:700}
295
+ .tr-draftbtns{display:flex;justify-content:flex-end;gap:var(--s1)}
296
+ .tr-note{border-radius:6px;border:1px solid var(--line);border-left:4px solid var(--tone);padding:var(--s2);background:var(--panel);cursor:pointer;display:flex;flex-direction:column;gap:var(--s1)}
297
+ .tr-note.ns-done:not(.on),.tr-note.ns-wontfix:not(.on){opacity:.72}
298
+ .tr-note.on{border:2px solid var(--tone);border-left-width:5px;background:var(--panel2)}
299
+ .tr-notetop{display:flex;gap:var(--s2)}
300
+ .tr-notebody{flex:1;min-width:0}
301
+ .tr-notemeta{display:flex;flex-wrap:wrap;gap:var(--s1);align-items:center;font-size:12px}
302
+ .tr-statebadge{font-size:10px;font-weight:700;padding:0 var(--s1);border-radius:3px;background:var(--tone);color:var(--bg);line-height:16px}
303
+ .tr-moved{font-size:11px;color:var(--warn)}
304
+ .tr-notetext{font-size:13px;margin-top:var(--s0);white-space:pre-wrap;overflow-wrap:anywhere}
305
+ .tr-notetext.struck{text-decoration:line-through}
306
+ .tr-edit{display:flex;flex-direction:column;gap:var(--s1)}
307
+ .tr-reply{padding:var(--s1) var(--s2);border-radius:4px;border-left:3px solid var(--tone);background:color-mix(in srgb,var(--tone) 10%,var(--panel));font-size:12px}
308
+ .tr-noteacts{display:flex;flex-wrap:wrap;gap:var(--s1);align-items:center}
309
+ .tr-noteacts button{font-size:12px;padding:0 var(--s2);line-height:22px}
310
+ .tr-done{color:var(--ok);border-color:color-mix(in srgb,var(--ok) 50%,var(--line))}
311
+ .tr-delete{color:var(--red);border-color:transparent}
312
+ .tr-delete.sure{background:var(--red);color:var(--bg)}
313
+ .tr-still{position:relative;flex:none}
314
+ .tr-still img{display:block;width:100%;height:100%;object-fit:cover;border-radius:4px;background:var(--panel2)}
315
+ .tr-stillpin{position:absolute;width:12px;height:12px;transform:translate(-50%,-50%);border-radius:6px;background:var(--tone);border:2px solid var(--tag);box-shadow:0 1px 4px var(--shadow)}
316
+ .tr-phone{overflow-y:auto;display:flex;flex-direction:column}
317
+ .tr-phone > *{flex:none}
318
+ .tr-phone .tr-notescol{height:auto}
319
+ /* the dispatcher's board and a kind's data view (review-board.tsx, kind-view.tsx) */
320
+ .rv-board{padding:var(--s2) var(--s3)}
321
+ .rv-boardrow{padding:var(--s2);border:1px solid var(--line);border-radius:4px;margin-bottom:var(--s1);cursor:pointer;font-size:12.5px}
322
+ .rv-boardrow:hover,.rv-boardrow.on{background:var(--panel)}
323
+ .rv-boardrow .rv-linkedtitle{display:block}
324
+ .rv-kindview{display:flex;flex-direction:column;gap:var(--s2);margin:var(--s2) 0}
325
+ .rv-kindtext{font:12px/1.5 ui-monospace,Menlo,monospace;background:var(--panel);border:1px solid var(--line);border-radius:3px;padding:var(--s2) var(--s3);margin:0;white-space:pre-wrap;overflow-wrap:anywhere}
326
+ .rv-anim select{font-size:12px}
327
+ /* a sound view (review-views.tsx SoundBody): its waveform over the player, a row a side on a pair */
328
+ .rv-sound{display:flex;flex-direction:column;gap:var(--s2)}
329
+ .rv-soundrow{display:flex;flex-direction:column;gap:var(--s1);background:var(--panel);border:1px solid var(--line);border-radius:4px;padding:var(--s2)}
@@ -0,0 +1,56 @@
1
+ // the page's theme, a node of the root (home-st.ts): his pick (dark, light or
2
+ // auto, which follows the system) kept per browser, resolved to dark or light
3
+ // on <html data-theme>, where hub.css swaps its token block. Applied at
4
+ // construction, before the first render, so a reload paints in the theme.
5
+ import { autorun, makeAutoObservable, runInAction } from 'mobx'
6
+
7
+ export const THEMES = ['dark', 'light', 'auto'] as const
8
+ export type ThemePick = (typeof THEMES)[number]
9
+ export type Theme = 'dark' | 'light'
10
+
11
+ const KEY = 'hub:theme'
12
+
13
+ function loadPick(): ThemePick {
14
+ try {
15
+ const v = globalThis.localStorage?.getItem(KEY)
16
+ return (THEMES as readonly string[]).includes(v ?? '') ? (v as ThemePick) : 'auto'
17
+ } catch {
18
+ return 'auto'
19
+ }
20
+ }
21
+
22
+ export class ThemeSt {
23
+ pick: ThemePick = loadPick()
24
+ /** what the system asks for, read live */
25
+ system: Theme = 'dark'
26
+
27
+ constructor() {
28
+ makeAutoObservable(this)
29
+ const mq = typeof window !== 'undefined' && typeof window.matchMedia === 'function' ? window.matchMedia('(prefers-color-scheme: light)') : null
30
+ if (mq != null) {
31
+ this.system = mq.matches ? 'light' : 'dark'
32
+ mq.addEventListener('change', (e) => runInAction(() => (this.system = e.matches ? 'light' : 'dark')))
33
+ }
34
+ const root = typeof document !== 'undefined' ? document.documentElement : null
35
+ if (root != null) autorun(() => {
36
+ root.dataset.theme = this.resolved
37
+ root.style.colorScheme = this.resolved
38
+ })
39
+ }
40
+
41
+ get resolved(): Theme {
42
+ return this.pick === 'auto' ? this.system : this.pick
43
+ }
44
+
45
+ set(p: ThemePick): void {
46
+ this.pick = p
47
+ try {
48
+ globalThis.localStorage?.setItem(KEY, p)
49
+ } catch { /* private window or blocked site data: the pick lasts the tab */ }
50
+ }
51
+
52
+ /** the foot's button: dark, light, auto, then dark again */
53
+ cycle(): void {
54
+ this.set(THEMES[(THEMES.indexOf(this.pick) + 1) % THEMES.length]!)
55
+ }
56
+ }
@@ -74,6 +74,8 @@ export const KIND_SECTION: Record<ResourceType, SectionId> = {
74
74
  post: 'media',
75
75
  wiki: 'wikis',
76
76
  process: 'processes',
77
+ card: 'review',
78
+ cut: 'trailer',
77
79
  }
78
80
 
79
81
  /** a process's colour: up reads green, crashed red, on its way orange,
@@ -392,6 +394,8 @@ export const TYPE_MARK: Record<string, TypeMark> = {
392
394
  test: { glyph: '\u2263', hue: 'k5', word: 'a test file: the check that runs it, its last runs against its budget, the command that runs it alone' },
393
395
  reflection: { glyph: '\u203b', hue: 'k2', word: 'a reflection: friction this repo logged against the wiki, waiting to be folded in' },
394
396
  process: { glyph: '\u223f', hue: 'k1', word: 'a global process this repo declares: its state, its menu, its log' },
397
+ card: { glyph: '\u25a3', hue: 'k2', word: 'a review card waiting for your answer: approve, fix, reject, circle a detail' },
398
+ cut: { glyph: '\u25b6', hue: 'k3', word: 'a trailer cut: play it with its timeline, pin a note on any frame' },
395
399
  done: { glyph: '\u2713', hue: 'k5', word: 'a done ask: answered, filed under done, out of every count above it' },
396
400
  }
397
401
 
@@ -45,7 +45,7 @@ export const SectionRowView = observer(({ app, row }: { app: RepoSt; row: TreeRo
45
45
  const key = s == null ? null : sectionKeyLabel(id)
46
46
  const el = useInSight(t.cursor === row.key)
47
47
  return (
48
- <div ref={el} className={`tsec${s?.empty === true ? ' empty' : ''}${s?.opens === false ? ' noop' : ''}${s?.on === true ? ' cur' : ''}${t.cursor === row.key ? ' on' : ''}`} aria-current={s?.on === true ? 'page' : undefined}
48
+ <div ref={el} className={`tsec s-${id}${s?.empty === true ? ' empty' : ''}${s?.opens === false ? ' noop' : ''}${s?.on === true ? ' cur' : ''}${t.cursor === row.key ? ' on' : ''}`} aria-current={s?.on === true ? 'page' : undefined}
49
49
  title={s == null ? row.label : `${s.title} (${key})`} onClick={() => t.click(row)}>
50
50
  <Fold app={app} row={row} />
51
51
  <Icon d={ICON[id]} />
@@ -144,6 +144,10 @@ export class TreeSvc {
144
144
  this.cursor = row.key
145
145
  this.activate(row)
146
146
  }
147
+ /** a click in the page: the arrows are the page's again until the tree is walked or clicked */
148
+ leave(): void {
149
+ this.cursor = null
150
+ }
147
151
  /** the chevron: fold or unfold, never open */
148
152
  fold(row: TreeRow): void {
149
153
  this.cursor = row.key