markon-core 0.15.13

markon core - Mark it on.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
/* markon-live — the sphere morphs into the settings panel in-place on click.
 *
 * Note: --markon-live-user / --live-pulse-color are RUNTIME variables set
 * via JS (see collaboration-manager.ts), not design tokens. They share the
 * --markon-* prefix for legacy reasons but are not part of the editor.css
 * design palette and are not user-configurable through the styles panel. */

/* Collapsed sphere matches the narrow-screen TOC icon: neutral surface color,
   foreground used for the icon. User's broadcast color lives in the panel
   picker and on remote peers' focus pulse, not on the local sphere. */
.markon-live-container {
    position: fixed;
    z-index: var(--markon-z-chrome);
    width: 40px;
    height: 40px;
    border-radius: 50%;
    cursor: default;
    /* All three modes (default / broadcasting / live-off) share a 2.5px
       stroke; only the color changes. Width parity makes the sphere read
       as the *same* element across modes — what differs is mode color, not
       chrome thickness. The same width is used by the ToC and Chat
       spheres/panels so the floating-layer family looks unified. */
    /* Ring = your identity colour (always). Falls back to a neutral border
       before a colour is assigned. */
    border: 2.5px solid var(--markon-live-user, var(--markon-border-emphasis));
    box-sizing: border-box;
    overflow: hidden;
    background-color: var(--markon-bg-elevated);
    /* Sphere icon is secondary chrome — uses --markon-fg-subtle so it
       doesn't punch as hard as the document body text. */
    color: var(--markon-fg-subtle);
    user-select: none;
    display: flex;
    align-items: center;
    justify-content: center;
}

/* Broadcasting (speaker) — the identity-coloured inner ring (border) is joined
   by a soft outer ring (box-shadow, drawn outside the frame, not clipped by the
   container's overflow) that breathes slowly. Inner solid + outer breathing =
   "I'm broadcasting", calmer than a fast pulse. */
.markon-live-container.broadcasting {
    animation: markon-collab-breathe 3s ease-in-out infinite;
}
/* A soft glow hugging the sphere that gently breathes — "broadcasting" reads as
   the ball being quietly *lit*, not as a second hard ring. Blur (not a hard
   spread ring) + a moderate peak opacity keep it present but calm; only the
   opacity pulses. No gap ring — the glow sits directly on the sphere. */
@keyframes markon-collab-breathe {
    0%, 100% { box-shadow: 0 0 8px 2px color-mix(in srgb, var(--markon-live-user) 62%, transparent); }
    50%      { box-shadow: 0 0 8px 2px color-mix(in srgb, var(--markon-live-user) 18%, transparent); }
}

/* Expanded (panel) state — sphere becomes the panel rectangle in-place.
   Inherits the sphere's surface and text color so clicking feels like a
   pure geometric transform, not a skin swap. Border-radius = sphere radius
   (20px) so the anchor corner visually continues the sphere's curvature. */
.markon-live-container.expanded {
    --markon-modal-surface-border: var(--markon-live-user, var(--markon-border-emphasis));
    width: 260px;
    height: auto;
    max-width: calc(100vw - 16px);
    max-height: calc(100vh - 16px);
    cursor: default;
    display: block;
}

/* Sphere face — live icon when collapsed, close (X) icon when expanded.
   Clicking the face always toggles the panel, so a second click at the same
   spot is the exact reverse of the first. */
.markon-live-face {
    position: relative;
    width: 100%;
    height: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
}

.markon-live-face .icon-collab,
.markon-live-face .icon-close {
    pointer-events: none;
}

.markon-live-face .icon-close {
    display: none;
}

.markon-live-container.expanded .markon-live-face .icon-collab {
    display: none;
}

.markon-live-container.expanded .markon-live-face .icon-close {
    display: block;
}

/* The three face icons (live, off, close) are stacked at the same center so
   OFF can overlay its prohibited sign on top of the live icon, and the X
   (expanded state) replaces everything. */
.markon-live-face .icon-collab,
.markon-live-face .icon-off,
.markon-live-face .icon-close {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    pointer-events: none;
}

.markon-live-face .icon-off {
    display: none;
}
/* OFF mode: keep the live icon visible, overlay the prohibited sign on top. */
.markon-live-container.live-off .markon-live-face .icon-off {
    display: block;
}
/* Expanded: X replaces both — X is later in DOM so it overlays naturally. */
.markon-live-container.expanded .markon-live-face .icon-collab,
.markon-live-container.expanded .markon-live-face .icon-off {
    display: none;
}

/* OFF mode: the container's 2.5px border is the ring of the prohibited sign
   and the SVG draws only the slash on top. The slash mirrors the ring's exact
   colour expression — the author colour when one is assigned, the neutral
   border fallback otherwise — so ring + slash always read as one consistent 🚫
   instead of an author-coloured ring fighting a grey slash. */
.markon-live-container.live-off .icon-off {
    color: var(--markon-live-user, var(--markon-border-emphasis));
}

/* When expanded, face becomes a 40×40 button pinned to the panel's
   top-left corner — the exact position the sphere occupied before expand. */
.markon-live-container.expanded .markon-live-face {
    position: absolute;
    top: var(--markon-modal-frame-inset);
    left: var(--markon-modal-frame-inset);
    width: 40px;
    height: 40px;
    z-index: var(--markon-z-content-overlay);
    border-radius: var(--markon-radius-lg) 0 0 0;
}

/* Hover spin on the X close button — playful affordance confirming the
   button is interactive. Rotation is on the icon only so the 40×40 hit
   area and hover detection stay unchanged. */
.markon-live-container.expanded .markon-live-face .icon-close {
    transition: transform 0.35s ease;
}
.markon-live-container.expanded .markon-live-face:hover .icon-close {
    transform: translate(-50%, -50%) rotate(180deg);
}

/* The breathing ring (see .broadcasting above) makes no sense once the face
   collapses to a small corner button inside the expanded panel. */
.markon-live-container.expanded.broadcasting {
    animation: none;
}

/* Leader indicator — a colored dot above the sphere when following. Identity
   is the speaker's color, nothing else (no names). */
.markon-live-face .leader-info {
    position: absolute;
    bottom: 48px;
    right: 6px;
    width: 10px;
    height: 10px;
    border-radius: 50%;
    background: var(--leader-color, currentColor);
    box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.9);
    opacity: 0;
    transform: translateY(4px);
    transition: opacity 0.2s, transform 0.2s;
    pointer-events: none;
}

html[data-theme="dark"] .markon-live-face .leader-info {
    box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.6);
}

.markon-live-face .leader-info.show {
    opacity: 1;
    transform: translateY(0);
}

/* Body — panel content. display-swapped with the face like the TOC menu.
   Gap is the single source of vertical rhythm between rows; individual
   rows don't carry their own padding so the spacing stays uniform. */
.markon-live-body {
    display: none;
    position: relative;
    z-index: 1;
    padding: 8px 16px 16px;
    box-sizing: border-box;
    flex-direction: column;
    gap: 0;
}

.markon-live-container.expanded .markon-live-body {
    display: flex;
}

/* Header occupies the same 40px top band as the X face, so the title sits
   on the same visual row as the close button instead of a line below it. */
.markon-live-body .panel-header {
    font-weight: bold;
    font-size: 14px;
    height: 32px;
    line-height: 32px;
    text-align: center;
    margin: 0 32px 12px;
    user-select: none;
}

.markon-live-body .panel-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    font-size: 13px;
}

/* Mode row: single centered pill. The pill itself is the mode selector; no
   separate label — three self-describing buttons plus the panel title
   "Live" above is enough context. Centered so Broadcast's midpoint (middle
   button of an Off/Follow-symmetric pill) coincides with the panel center. */
.markon-live-body .panel-row-mode {
    display: flex;
    justify-content: center;
    align-items: center;
}

/* Section marker — compact icon above the identity / Live groups. */
.markon-live-body .panel-section-label {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    height: 18px;
    color: color-mix(in srgb, var(--markon-fg-muted) 78%, var(--markon-fg-default));
    gap: 10px;
    line-height: 1;
    margin: 0 0 8px;
}

.markon-live-body .panel-section-label::before,
.markon-live-body .panel-section-label::after {
    content: "";
    height: 1px;
    flex: 1 1 0;
    background: color-mix(in srgb, var(--markon-border-default) 72%, transparent);
}

.markon-live-body .panel-section-label svg {
    display: block;
    flex: 0 0 auto;
}

/* Identity row: colour picker + nickname input. Always interactive — your
   colour is your shared identity (used by annotation authorship), not a
   broadcast-only sub-option. */
.markon-live-body .panel-row-color {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 10px;
    margin-bottom: 16px;
}

.markon-live-body .identity-name {
    width: 100%;
    height: 26px;
    box-sizing: border-box;
    padding: 4px 8px;
    font: inherit;
    font-size: 12px;
    border: 1px solid var(--markon-border-default);
    border-radius: var(--markon-radius-sm);
    background: var(--markon-bg-default);
    color: var(--markon-fg-default);
    text-align: center;
}

.markon-live-body .identity-name::placeholder {
    color: var(--markon-fg-muted);
    opacity: 0.85;
}

/* Picker centers on the panel center ⇒ same center as the Broadcast button
   above. Fixed gap (instead of space-between) so the active dot can steal
   a couple of pixels from its neighbors to pay for its outline ring. */
.markon-live-body .color-picker {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 25px;
    gap: 7px;
    flex-wrap: nowrap;
}

.markon-live-body .color-dot {
    width: 21px;
    height: 21px;
    border-radius: 50%;
    cursor: pointer;
    box-sizing: border-box;
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--markon-fg-on-emphasis);
    font-size: 10px;
    font-weight: 600;
    line-height: 1;
    user-select: none;
    /* Slight shadow for legibility on lighter backgrounds. */
    text-shadow: 0 1px 1px rgba(0, 0, 0, 0.3);
}

/* Active indicator: thin ring hugging the dot in the dot's own color
   (piped through the --dot-color custom property that's set inline per
   swatch). Width stays the same as before; only the color changes so the
   active state reads as "this swatch, underlined in itself". */
.markon-live-body .color-dot.active {
    outline: 1.5px solid var(--dot-color, rgba(0, 0, 0, 0.7));
    outline-offset: 1px;
    /* Reserve extra horizontal space so the ring doesn't steal from the
       neighbor dots' visual gap; only the active dot pays this cost so the
       row still feels evenly distributed around it. */
    margin: 0 3px;
}

/* Segmented mode picker: Off | Broadcast | Follow. Fixed group width +
   flex share makes Off and Follow render at identical widths regardless of
   font rendering, which is what guarantees Broadcast's center equals the
   group's center — and thus the panel's center. */
.markon-live-body .mode-group {
    display: flex;
    width: 100%;
    max-width: 220px;
    border-radius: 999px;
    overflow: hidden;
    background: rgba(0, 0, 0, 0.06);
}

html[data-theme="dark"] .markon-live-body .mode-group {
    background: rgba(255, 255, 255, 0.08);
}

.markon-live-body .mode-btn {
    appearance: none;
    border: none;
    background: transparent;
    color: inherit;
    padding: 7px 14px;
    font: inherit;
    font-size: 12px;
    font-weight: 500;
    line-height: 1;
    cursor: pointer;
    border-radius: 999px;
    transition: background-color 0.12s, color 0.12s;
    text-align: center;
    white-space: nowrap;
}

/* Broadcast keeps its natural content width; Off and Follow split whatever
   remains in the group equally. Symmetry is structural, not dependent on a
   min-width guess against the current font. */
.markon-live-body .mode-btn[data-mode="broadcast"] {
    flex: 0 0 auto;
}
.markon-live-body .mode-btn[data-mode="off"],
.markon-live-body .mode-btn[data-mode="follow"] {
    flex: 1 1 0;
    min-width: 0;
}

.markon-live-body .mode-btn:hover {
    background: var(--markon-bg-hover);
}

.markon-live-body .mode-btn.active {
    background: var(--markon-accent);
    color: var(--markon-fg-on-emphasis);
}

/* Breathing pulse on the focused heading-section. Starts as a ring in the
   speaker's color (injected via --live-pulse-color) and fades out.
   Implemented via ::after so the pulse's box-shadow paints on top of the
   parent's border (a plain box-shadow on the element itself would render
   behind the border and look dimmed). */
.markon-live-focus-pulse::after {
    content: '';
    position: absolute;
    inset: 0;
    border-radius: inherit;
    pointer-events: none;
    z-index: var(--markon-z-content-overlay);
    animation: markon-live-focus-pulse 1.4s ease-out;
}

@keyframes markon-live-focus-pulse {
    0%   { box-shadow: 0 0 0 5px var(--live-pulse-color, rgba(9, 105, 218, 0.55)); }
    60%  { box-shadow: 0 0 0 3px var(--live-pulse-color, rgba(9, 105, 218, 0.28)); }
    100% { box-shadow: 0 0 0 0 transparent; }
}