markon-core 0.13.10

markon core - Turn your markdown on.
Documentation
/* 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. */
    border: 2.5px solid var(--markon-border-strong);
    box-sizing: border-box;
    overflow: hidden;
    background-color: var(--markon-elevated);
    /* Sphere icon is secondary chrome — uses --markon-text-muted so it
       doesn't punch as hard as the document body text. */
    color: var(--markon-text-muted);
    user-select: none;
    display: flex;
    align-items: center;
    justify-content: center;
}

/* Broadcasting (speaker) — same 2.5px stroke, recolored to the user's
   broadcast color. Color comes from the --markon-live-user var set by
   CollaborationManager._updateUIState. */
.markon-live-container.broadcasting {
    border-color: var(--markon-live-user, var(--markon-border-strong));
}

/* 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 {
    width: 260px;
    height: auto;
    max-width: calc(100vw - 16px);
    max-height: calc(100vh - 16px);
    border-radius: 20px;
    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-live,
.markon-live-face .icon-close {
    pointer-events: none;
}

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

.markon-live-container.expanded .markon-live-face .icon-live {
    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-live,
.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-live,
.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. Ring and slash share the
   same disabled color so they read as a single 🚫 glyph. */
.markon-live-container.live-off {
    --markon-disabled: rgba(107, 114, 128, 0.75);
    border-color: var(--markon-disabled);
}
html[data-theme="dark"] .markon-live-container.live-off {
    --markon-disabled: rgba(180, 188, 200, 0.55);
    border-color: var(--markon-disabled);
}
.markon-live-container.live-off .icon-off {
    color: var(--markon-disabled);
}

/* 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: 0;
    left: 0;
    width: 40px;
    height: 40px;
    z-index: var(--markon-z-content-overlay);
    border-radius: 20px 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);
}

.markon-live-container.broadcasting .markon-live-face::after {
    content: '';
    position: absolute;
    inset: -5px;
    border-radius: 50%;
    border: 2px solid currentColor;
    animation: markon-live-pulse 2s infinite;
}

/* Broadcasting pulse makes no sense once the face is a small corner button. */
.markon-live-container.expanded.broadcasting .markon-live-face::after {
    display: none;
}

@keyframes markon-live-pulse {
    0%   { transform: scale(1);   opacity: 1; }
    100% { transform: scale(1.4); opacity: 0; }
}

/* 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;
    padding: 6px 16px 14px;
    box-sizing: border-box;
    flex-direction: column;
    gap: 14px;
}

.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: 30px;
    line-height: 30px;
    text-align: center;
    margin: 0;
}

.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;
}

/* Color picker is a sub-option of the Mode row, only meaningful when the
   current mode is Broadcast. The ↳ glyph + small centered label express the
   dependency; the whole row dims and becomes non-interactive otherwise. */
.markon-live-body .panel-row-color {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 6px;
    opacity: 0.4;
    pointer-events: none;
    transition: opacity 0.15s;
}

.markon-live-body .panel-row-color[aria-disabled="false"] {
    opacity: 1;
    pointer-events: auto;
}

.markon-live-body .panel-row-label {
    text-align: center;
    font-size: 10px;
    opacity: 0.55;
    line-height: 1.2;
    letter-spacing: 0.02em;
    padding: 5px 0;
}

/* 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;
    justify-content: center;
    gap: 6px;
    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: #fff;
    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: 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: rgba(0, 0, 0, 0.05);
}

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

.markon-live-body .mode-btn.active {
    background: var(--markon-primary);
    color: #fff;
}

/* 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; }
}