|
|
/**
|
|
|
* words.css — eidos visual for the Words editor (content layer).
|
|
|
*
|
|
|
* Targets the `data-words-*` attributes the engine's render tree emits
|
|
|
* (see soma `engine/render.ts`) plus the editor chrome that layers over
|
|
|
* the content: the left block gutter and the floating selection bubble
|
|
|
* (their Svelte lives in `words-block-gutter.svelte` /
|
|
|
* `words-bubble.svelte`; the visuals live here). The settings drawer
|
|
|
* keeps its own recipe.
|
|
|
*
|
|
|
* Tokens are the `--words-*` recipe surface (eidos/lib/recipes/base.ts).
|
|
|
* The reading typography is deliberately document-grade (generous size +
|
|
|
* line-height + block rhythm), not the tight UI scale.
|
|
|
*/
|
|
|
|
|
|
/* ── Frame ────────────────────────────────────────────────────────────── */
|
|
|
|
|
|
[data-words] {
|
|
|
/* size cascade — md is the default; sm / lg override below.
|
|
|
content-px is gutter-aware (leaves room for the block handle in the
|
|
|
left margin + the settings button in the right margin) with enough
|
|
|
clearance that the dashed block outline never crowds the grip. */
|
|
|
--_words-content-px: 3rem;
|
|
|
--_words-content-py: var(--words-content-py-md);
|
|
|
--_words-content-min: var(--words-content-min-block-size-md);
|
|
|
--_words-content-max: var(--words-content-max-block-size-md);
|
|
|
--_words-content-font-size: var(--words-font-size-lg);
|
|
|
--_words-radius: var(--words-radius-md);
|
|
|
/* Fixed height of the top menu bar — shared so the absolute inspector
|
|
|
panels (sidebar / drawer) can offset themselves below it. */
|
|
|
--_words-bar-h: 2.75rem;
|
|
|
|
|
|
position: relative;
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
inline-size: 100%;
|
|
|
font-family: var(--words-font-family);
|
|
|
color: var(--words-color);
|
|
|
background: var(--words-bg);
|
|
|
border: var(--words-border-width) solid var(--words-border);
|
|
|
border-radius: var(--_words-radius);
|
|
|
box-shadow: var(--words-shadow);
|
|
|
transition:
|
|
|
border-color var(--words-transition-duration) var(--words-transition-ease),
|
|
|
box-shadow var(--words-transition-duration) var(--words-transition-ease);
|
|
|
}
|
|
|
|
|
|
[data-words][data-size='sm'] {
|
|
|
--_words-content-px: 2.5rem;
|
|
|
--_words-content-py: var(--words-content-py-sm);
|
|
|
--_words-content-min: var(--words-content-min-block-size-sm);
|
|
|
--_words-content-max: var(--words-content-max-block-size-sm);
|
|
|
--_words-content-font-size: var(--words-font-size-md);
|
|
|
--_words-radius: var(--words-radius-sm);
|
|
|
}
|
|
|
|
|
|
[data-words][data-size='lg'] {
|
|
|
--_words-content-px: 3.5rem;
|
|
|
--_words-content-py: var(--words-content-py-lg);
|
|
|
--_words-content-min: var(--words-content-min-block-size-lg);
|
|
|
--_words-content-max: var(--words-content-max-block-size-lg);
|
|
|
--_words-content-font-size: calc(var(--words-font-size-lg) + 2px);
|
|
|
--_words-radius: var(--words-radius-lg);
|
|
|
}
|
|
|
|
|
|
[data-words]:focus-within {
|
|
|
border-color: var(--words-border-hover);
|
|
|
box-shadow: var(--words-focus-shadow);
|
|
|
}
|
|
|
|
|
|
[data-words][data-invalid] {
|
|
|
border-color: var(--words-invalid-border);
|
|
|
}
|
|
|
|
|
|
[data-words][data-disabled] {
|
|
|
opacity: var(--words-disabled-opacity);
|
|
|
pointer-events: none;
|
|
|
}
|
|
|
|
|
|
/* Pre-mount frame (client-only editor — see words.svelte). Holds the
|
|
|
min height so swapping in the live editor doesn't shift layout. */
|
|
|
[data-words][aria-busy='true'] {
|
|
|
min-block-size: calc(var(--_words-content-min) + 2 * var(--_words-content-py));
|
|
|
}
|
|
|
|
|
|
/* ── Menu bar (formatting toolbar) ────────────────────────────────────── */
|
|
|
|
|
|
/* A fixed-height top row of formatting controls + view toggles. The frame
|
|
|
is `flex-direction: column`, so the bar sits above the content naturally;
|
|
|
the absolute inspector panels offset below it via `--_words-bar-h`. */
|
|
|
[data-words-menubar] {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: var(--space-1);
|
|
|
flex-wrap: nowrap;
|
|
|
block-size: var(--_words-bar-h);
|
|
|
padding-inline: var(--space-2);
|
|
|
overflow-x: auto;
|
|
|
overflow-y: hidden;
|
|
|
border-block-end: var(--words-border-width) solid var(--words-border);
|
|
|
background: var(--words-bg);
|
|
|
border-start-start-radius: var(--_words-radius);
|
|
|
border-start-end-radius: var(--_words-radius);
|
|
|
scrollbar-width: thin;
|
|
|
}
|
|
|
/* Groups read as clusters separated by hairline dividers. */
|
|
|
[data-words-menubar-group] {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: 1px;
|
|
|
padding-inline: var(--space-1);
|
|
|
border-inline-end: 1px solid var(--words-border);
|
|
|
}
|
|
|
[data-words-menubar-group]:last-of-type {
|
|
|
border-inline-end: none;
|
|
|
}
|
|
|
[data-words-menubar-spacer] {
|
|
|
flex: 1 1 auto;
|
|
|
}
|
|
|
/* Active formatting button — marks reflect via `aria-pressed` (CommandButton),
|
|
|
list / align / inspector via `data-active`. Both read as the same accent. */
|
|
|
[data-words-menubar] [data-active],
|
|
|
[data-words-menubar] [aria-pressed='true'] {
|
|
|
color: var(--color-primary-solid);
|
|
|
background: color-mix(in srgb, var(--color-primary-solid) var(--words-wash-subtle), transparent);
|
|
|
}
|
|
|
/* Preview is read-only — the surface alone looks identical to edit, so make
|
|
|
the switch obvious: collapse the formatting groups, leaving the bar as just
|
|
|
the view controls (inspector toggle + Edit/Preview). */
|
|
|
[data-words][data-mode='preview'] [data-words-menubar-group] {
|
|
|
display: none;
|
|
|
}
|
|
|
|
|
|
/* Park the docked inspector panels below the bar so they don't cover it. */
|
|
|
[data-words]:has([data-words-menubar]) [data-words-sidebar],
|
|
|
[data-words]:has([data-words-menubar]) [data-words-drawer-panel] {
|
|
|
inset-block-start: var(--_words-bar-h);
|
|
|
}
|
|
|
|
|
|
/* ── Left gutter — block handle ───────────────────────────────────────── */
|
|
|
|
|
|
[data-words-block-gutter] {
|
|
|
position: absolute;
|
|
|
inset-inline-start: 0.35rem;
|
|
|
z-index: 2;
|
|
|
/* The script sets `top` to the block's vertical midpoint; this
|
|
|
transform pulls the grip up by half its own height so the icon
|
|
|
lands ON the midpoint rather than starting from it. Works
|
|
|
regardless of block height (paragraph, code, table, image). */
|
|
|
transform: translateY(-50%);
|
|
|
/* slide smoothly between blocks as the cursor moves */
|
|
|
transition: top var(--words-transition-duration) var(--words-transition-ease);
|
|
|
animation: words-gutter-in var(--words-transition-duration) var(--words-transition-ease);
|
|
|
}
|
|
|
@keyframes words-gutter-in {
|
|
|
/* `translateY(-50%)` keeps the grip vertically centered against the
|
|
|
block midpoint — the keyframe MUST preserve it on both frames, or
|
|
|
the end state snaps the grip back to its `top` anchor and the
|
|
|
centering is lost the moment the animation completes. */
|
|
|
from {
|
|
|
opacity: 0;
|
|
|
transform: translateY(-50%) translateX(-4px);
|
|
|
}
|
|
|
to {
|
|
|
opacity: 1;
|
|
|
transform: translateY(-50%) translateX(0);
|
|
|
}
|
|
|
}
|
|
|
[data-words-block-gutter] [data-words-block-handle] {
|
|
|
color: var(--words-placeholder-color);
|
|
|
cursor: grab;
|
|
|
transition:
|
|
|
transform var(--words-transition-duration) var(--words-transition-ease),
|
|
|
color var(--words-transition-duration) var(--words-transition-ease);
|
|
|
}
|
|
|
/* The grip is AMBIENT — no `:hover` transform. The old `scale(1.18)` grew the
|
|
|
button under the cursor, which shifted its hitbox and produced a hover↔scale
|
|
|
jitter ("the grip trembles"). */
|
|
|
|
|
|
/* Fade-in for the active-block gutter rail (the `[data-words-active]::before`
|
|
|
marker further down). */
|
|
|
@keyframes words-outline-in {
|
|
|
from {
|
|
|
opacity: 0;
|
|
|
}
|
|
|
to {
|
|
|
opacity: 1;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* Drop indicator painted while a block is dragged by its grip. The drag
|
|
|
controller positions it frame-relative at the nearest inter-block seam;
|
|
|
the bar spans the target block's width. `pointer-events: none` keeps it
|
|
|
clear of the drag's own dragover hit-testing. */
|
|
|
[data-words-block-drop-line] {
|
|
|
position: absolute;
|
|
|
z-index: 5;
|
|
|
height: 2px;
|
|
|
margin-block-start: -1px;
|
|
|
border-radius: 1px;
|
|
|
background: var(--color-primary-solid);
|
|
|
box-shadow: 0 0 0 1px color-mix(in srgb, var(--color-primary-solid) var(--words-wash-strong), transparent);
|
|
|
pointer-events: none;
|
|
|
}
|
|
|
|
|
|
/* ── Selection bubble toolbar ─────────────────────────────────────────── */
|
|
|
|
|
|
/* Floating card, viewport-anchored (the soma BubbleMenu writes
|
|
|
--_words-bubble-x / -y from the selection's client rect, so position
|
|
|
is fixed). Centred horizontally on the selection; sits above it for
|
|
|
side='top', below for side='bottom'. */
|
|
|
[data-words-bubble-menu] {
|
|
|
position: fixed;
|
|
|
inset-block-start: var(--_words-bubble-y, 0);
|
|
|
inset-inline-start: var(--_words-bubble-x, 0);
|
|
|
z-index: 30;
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: var(--words-command-gap);
|
|
|
padding: var(--space-1);
|
|
|
background: var(--words-bg);
|
|
|
border: var(--words-border-width) solid var(--words-border);
|
|
|
border-radius: var(--words-radius-md);
|
|
|
box-shadow: var(--depth-overlay-shadow), var(--depth-overlay-halo);
|
|
|
animation: words-bubble-in var(--words-transition-duration) var(--words-transition-ease);
|
|
|
}
|
|
|
[data-words-bubble-menu][data-side='top'] {
|
|
|
transform: translate(-50%, calc(-100% - 0.5rem));
|
|
|
}
|
|
|
[data-words-bubble-menu][data-side='bottom'] {
|
|
|
transform: translate(-50%, 0.5rem);
|
|
|
}
|
|
|
[data-words-bubble-menu][hidden] {
|
|
|
display: none;
|
|
|
}
|
|
|
/* Opacity-only so the keyframe doesn't fight the data-side transform. */
|
|
|
@keyframes words-bubble-in {
|
|
|
from {
|
|
|
opacity: 0;
|
|
|
}
|
|
|
to {
|
|
|
opacity: 1;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* Active mark — the soma CommandButton stamps data-active / aria-pressed
|
|
|
on the composed eidos Button; tint it to read as "on". (The Button
|
|
|
recipe owns every other state: hover, focus, disabled, sizing.) */
|
|
|
[data-words-bubble-menu] [data-button][data-active] {
|
|
|
background: var(--words-primary-solid);
|
|
|
color: var(--words-command-solid-color);
|
|
|
}
|
|
|
|
|
|
/* Thin vertical rule between groups. */
|
|
|
[data-words-bubble-menu] [data-words-bubble-divider] {
|
|
|
flex: 0 0 auto;
|
|
|
inline-size: 1px;
|
|
|
block-size: 1.15rem;
|
|
|
margin-inline: var(--space-0-5);
|
|
|
background: var(--words-border);
|
|
|
}
|
|
|
|
|
|
/* Turn-into trigger (an eidos Button) — keep it slim in the bubble. */
|
|
|
[data-words-bubble-menu] [data-words-bubble-trigger] {
|
|
|
white-space: nowrap;
|
|
|
}
|
|
|
|
|
|
/* Inline link editor row (replaces the toolbar while editing a link). */
|
|
|
[data-words-bubble-menu] [data-words-bubble-link] {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: var(--space-1);
|
|
|
}
|
|
|
[data-words-bubble-menu] [data-words-bubble-link] input {
|
|
|
inline-size: 15rem;
|
|
|
padding: var(--space-1) var(--space-2);
|
|
|
border: var(--words-border-width) solid var(--words-border);
|
|
|
border-radius: var(--words-command-radius);
|
|
|
background: var(--words-bg);
|
|
|
color: var(--words-content-color);
|
|
|
font-family: inherit;
|
|
|
font-size: var(--words-font-size-md);
|
|
|
outline: none;
|
|
|
}
|
|
|
[data-words-bubble-menu] [data-words-bubble-link] input:focus {
|
|
|
border-color: var(--words-border-hover);
|
|
|
}
|
|
|
|
|
|
/* Bubble colour menu — Text + Background swatch grids. The dropdown content
|
|
|
carries `data-words-bubble-color`; each `[data-words-bubble-swatch]` paints
|
|
|
its hue from the inline `--words-swatch` custom property. */
|
|
|
[data-words-bubble-color] {
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
gap: var(--space-2);
|
|
|
padding: var(--space-2);
|
|
|
}
|
|
|
[data-words-bubble-color-section] {
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
gap: var(--space-1);
|
|
|
}
|
|
|
[data-words-bubble-color-label] {
|
|
|
display: inline-flex;
|
|
|
align-items: center;
|
|
|
gap: 4px;
|
|
|
font-size: var(--words-font-size-sm);
|
|
|
color: var(--words-command-color);
|
|
|
--icon-size: 13px;
|
|
|
}
|
|
|
[data-words-bubble-swatches] {
|
|
|
display: grid;
|
|
|
grid-template-columns: repeat(10, 1fr);
|
|
|
gap: 4px;
|
|
|
}
|
|
|
[data-words-bubble-swatch] {
|
|
|
inline-size: 1.15rem;
|
|
|
block-size: 1.15rem;
|
|
|
padding: 0;
|
|
|
border: 1px solid color-mix(in srgb, var(--words-content-color) var(--words-wash-soft), transparent);
|
|
|
border-radius: var(--words-radius-xs);
|
|
|
background: var(--words-swatch, var(--words-bg));
|
|
|
cursor: pointer;
|
|
|
transition: box-shadow var(--words-transition-duration) var(--words-transition-ease, ease);
|
|
|
}
|
|
|
[data-words-bubble-swatch]:hover,
|
|
|
[data-words-bubble-swatch]:focus-visible {
|
|
|
outline: none;
|
|
|
box-shadow: 0 0 0 2px color-mix(in srgb, var(--_words-accent) var(--words-wash-strong), transparent);
|
|
|
}
|
|
|
/* "Default / none" chip — a diagonal strike over the surface tone. */
|
|
|
[data-words-bubble-swatch][data-clear] {
|
|
|
background:
|
|
|
linear-gradient(
|
|
|
to bottom right,
|
|
|
transparent calc(50% - 1px),
|
|
|
color-mix(in srgb, var(--words-content-color) var(--words-wash-strong), transparent) calc(50% - 1px),
|
|
|
color-mix(in srgb, var(--words-content-color) var(--words-wash-strong), transparent) calc(50% + 1px),
|
|
|
transparent calc(50% + 1px)
|
|
|
),
|
|
|
var(--words-bg);
|
|
|
}
|
|
|
|
|
|
/* Turn-into menu item — label + trailing active tick. */
|
|
|
[data-words-bubble-into-item] {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: space-between;
|
|
|
gap: var(--space-4);
|
|
|
}
|
|
|
/* trailing active tick (eidos Icon) */
|
|
|
[data-words-bubble-into-item] [data-icon] {
|
|
|
color: var(--words-primary-text);
|
|
|
}
|
|
|
|
|
|
/* ── Block inspector ──────────────────────────────────────────────────── */
|
|
|
|
|
|
/* Shared body — a vertical stack of labelled property rows. Reused by
|
|
|
every inspector presentation (sidebar / drawer / popover). The vertical
|
|
|
layout gives each control a full-width row, so nothing crowds. */
|
|
|
/* The inspector body lives inside a soma `Words.Drawer` (for live provider
|
|
|
reactivity); that host adds no layout — only the inner panel does. */
|
|
|
[data-words-inspector-host] {
|
|
|
display: contents;
|
|
|
}
|
|
|
[data-words-inspector] {
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
gap: var(--space-3);
|
|
|
/* Tightened from space-4 → space-2 (16px → 8px) — the sidebar is narrow
|
|
|
and the previous padding ate ~20% of the usable inline width. */
|
|
|
padding: var(--space-2);
|
|
|
}
|
|
|
|
|
|
/* ── Chrome typeface — ONE sans across every panel ─────────────────────────
|
|
|
The editor CONTENT may be themed serif (`--words-font-family`), but every
|
|
|
piece of CHROME (toolbar · bubble · slash · drawer · inspector · gutter
|
|
|
menus) must render in a single UI sans (`--words-chrome-font`) or controls
|
|
|
show mixed faces. Shipped components impose their own `font-family`, and the
|
|
|
"content" archetype (Accordion content, etc.) maps to the READING serif —
|
|
|
which leaked into the inspector labels (Lora). `[scope] *` is only (0,1,0),
|
|
|
EQUAL to a component's `[data-x]` font rule, so post-code-split LOAD ORDER
|
|
|
decided the winner. DOUBLING the scope attribute → (0,2,0) beats any
|
|
|
single-attribute component font regardless of order, on the root AND every
|
|
|
descendant. */
|
|
|
[data-words-menubar][data-words-menubar],
|
|
|
[data-words-menubar][data-words-menubar] *,
|
|
|
[data-words-bubble-menu][data-words-bubble-menu],
|
|
|
[data-words-bubble-menu][data-words-bubble-menu] *,
|
|
|
[data-words-slash-menu][data-words-slash-menu],
|
|
|
[data-words-slash-menu][data-words-slash-menu] *,
|
|
|
[data-words-drawer][data-words-drawer],
|
|
|
[data-words-drawer][data-words-drawer] *,
|
|
|
[data-words-inspector][data-words-inspector],
|
|
|
[data-words-inspector][data-words-inspector] *,
|
|
|
[data-words-block-handle-menu][data-words-block-handle-menu],
|
|
|
[data-words-block-handle-menu][data-words-block-handle-menu] *,
|
|
|
[data-words-block-inserter-menu][data-words-block-inserter-menu],
|
|
|
[data-words-block-inserter-menu][data-words-block-inserter-menu] * {
|
|
|
font-family: var(--words-chrome-font);
|
|
|
}
|
|
|
/* Depth accent — the active node's nesting level (1 = top-level block,
|
|
|
2 = column, 3 = block-inside-a-column, …) is stamped on the inspector
|
|
|
root. The header mark + wash pick up the SAME doctrinal palette the
|
|
|
canvas paints on the active-block dotted outline, so both surfaces read
|
|
|
in agreement. Routed through the global `--color-*-solid` tokens (set on
|
|
|
`:root`) so the accent is theme-aware AND inherits across the popover
|
|
|
portal, where `--words-*` set on `[data-words]` would not reach. */
|
|
|
[data-words-inspector] {
|
|
|
--_words-accent: var(--color-primary-solid);
|
|
|
}
|
|
|
[data-words-inspector][data-words-inspector-depth='2'] {
|
|
|
--_words-accent: var(--color-affirm-solid); /* teal — column level */
|
|
|
}
|
|
|
[data-words-inspector][data-words-inspector-depth='3'] {
|
|
|
--_words-accent: var(--color-loss-solid); /* violet — block in a column */
|
|
|
}
|
|
|
[data-words-inspector][data-words-inspector-depth='4'] {
|
|
|
--_words-accent: var(--color-risk-solid); /* amber — deeper nesting */
|
|
|
}
|
|
|
/* Header — breadcrumb trail + title, bled to the panel edges with a
|
|
|
hairline divider and a barely-there accent wash. */
|
|
|
[data-words-inspector-head] {
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
gap: var(--space-1-5);
|
|
|
margin: calc(var(--space-2) * -1) calc(var(--space-2) * -1) 0;
|
|
|
padding: var(--space-2-5) var(--space-3);
|
|
|
border-block-end: 1px solid var(--words-border);
|
|
|
background: color-mix(in srgb, var(--_words-accent) var(--words-wash-faint), var(--words-bg));
|
|
|
}
|
|
|
[data-words-inspector-title] {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: var(--space-2);
|
|
|
min-inline-size: 0;
|
|
|
font-size: var(--words-font-size-md);
|
|
|
font-weight: var(--words-strong-font-weight);
|
|
|
letter-spacing: -0.01em;
|
|
|
color: var(--words-content-color);
|
|
|
}
|
|
|
[data-words-inspector-title-mark] {
|
|
|
flex: none;
|
|
|
inline-size: 0.5rem;
|
|
|
block-size: 0.5rem;
|
|
|
border-radius: 2px;
|
|
|
background: var(--_words-accent);
|
|
|
box-shadow: 0 0 0 3px color-mix(in srgb, var(--_words-accent) var(--words-wash-subtle), transparent);
|
|
|
}
|
|
|
[data-words-inspector-title-text] {
|
|
|
min-inline-size: 0;
|
|
|
overflow: hidden;
|
|
|
text-overflow: ellipsis;
|
|
|
white-space: nowrap;
|
|
|
}
|
|
|
/* Delete-block button — pinned to the right edge of the inspector title row. */
|
|
|
[data-words-inspector-delete] {
|
|
|
flex: none;
|
|
|
margin-inline-start: auto;
|
|
|
color: var(--color-threat-solid);
|
|
|
}
|
|
|
/* Invalid free-text input (column width / gap / image URL) — a red ring
|
|
|
that does not shift layout. Set imperatively by the panel's commit
|
|
|
helpers when a value fails validation; cleared on the next valid commit
|
|
|
or while the user edits. */
|
|
|
[data-words-inspector-text][data-invalid] {
|
|
|
border-color: var(--color-threat-solid);
|
|
|
box-shadow: 0 0 0 1px color-mix(in srgb, var(--color-threat-solid) var(--words-wash-strong), transparent);
|
|
|
}
|
|
|
|
|
|
/* Breadcrumb trail (shown from depth ≥ 2). A single, non-wrapping row of
|
|
|
muted crumbs that scrolls rather than reflowing the header — without
|
|
|
this rule the raw <nav> wrapped and broke the columns layout. The last
|
|
|
(active) crumb lives in the title below, so every crumb here is an
|
|
|
ancestor link except the non-clickable document root. */
|
|
|
[data-words-inspector-breadcrumb] {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
flex-wrap: nowrap;
|
|
|
gap: 1px;
|
|
|
min-inline-size: 0;
|
|
|
overflow-x: auto;
|
|
|
scrollbar-width: none;
|
|
|
font-size: var(--words-font-size-xs, 0.6875rem);
|
|
|
line-height: 1.4;
|
|
|
color: var(--words-placeholder-color);
|
|
|
}
|
|
|
[data-words-inspector-breadcrumb]::-webkit-scrollbar {
|
|
|
display: none;
|
|
|
}
|
|
|
[data-words-breadcrumb-crumb] {
|
|
|
flex: 0 0 auto;
|
|
|
max-inline-size: 8rem;
|
|
|
overflow: hidden;
|
|
|
text-overflow: ellipsis;
|
|
|
white-space: nowrap;
|
|
|
margin: 0;
|
|
|
padding: 1px 5px;
|
|
|
border: 0;
|
|
|
border-radius: var(--words-radius-xs);
|
|
|
background: transparent;
|
|
|
color: inherit;
|
|
|
font: inherit;
|
|
|
letter-spacing: 0.01em;
|
|
|
}
|
|
|
button[data-words-breadcrumb-crumb] {
|
|
|
cursor: pointer;
|
|
|
transition:
|
|
|
background var(--words-transition-duration) var(--words-transition-ease, ease),
|
|
|
color var(--words-transition-duration) var(--words-transition-ease, ease);
|
|
|
}
|
|
|
button[data-words-breadcrumb-crumb]:hover {
|
|
|
background: color-mix(in srgb, var(--_words-accent) var(--words-wash-subtle), transparent);
|
|
|
color: var(--words-content-color);
|
|
|
}
|
|
|
button[data-words-breadcrumb-crumb]:focus-visible {
|
|
|
outline: none;
|
|
|
box-shadow: 0 0 0 2px color-mix(in srgb, var(--_words-accent) var(--words-wash-strong), transparent);
|
|
|
}
|
|
|
[data-words-breadcrumb-crumb][data-root] {
|
|
|
font-weight: 600;
|
|
|
opacity: 0.7;
|
|
|
}
|
|
|
[data-words-breadcrumb-sep] {
|
|
|
flex: 0 0 auto;
|
|
|
opacity: 0.45;
|
|
|
user-select: none;
|
|
|
}
|
|
|
|
|
|
[data-words-inspector-section] {
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
gap: var(--space-2-5);
|
|
|
}
|
|
|
[data-words-inspector-section-title] {
|
|
|
font-size: var(--words-font-size-sm);
|
|
|
font-weight: var(--words-strong-font-weight);
|
|
|
color: var(--words-placeholder-color);
|
|
|
text-transform: uppercase;
|
|
|
letter-spacing: 0.05em;
|
|
|
}
|
|
|
|
|
|
/* Accordion sections — the ghost accordion renders flat (no separation
|
|
|
between Typography / Color / Layout / Spacing / Border). Give each
|
|
|
section a hairline top divider and a header that reads as a section
|
|
|
label (uppercase, tracked, muted → solid when open) so the inspector
|
|
|
has clear structure instead of one undifferentiated stack. */
|
|
|
[data-words-inspector] [data-accordion-item] + [data-accordion-item] {
|
|
|
border-block-start: 1px solid var(--words-border);
|
|
|
}
|
|
|
[data-words-inspector] [data-accordion-trigger] {
|
|
|
text-transform: uppercase;
|
|
|
letter-spacing: 0.05em;
|
|
|
font-size: var(--words-font-size-xs, 0.6875rem);
|
|
|
font-weight: 600;
|
|
|
color: var(--words-command-color);
|
|
|
}
|
|
|
[data-words-inspector] [data-accordion-trigger][data-state='open'] {
|
|
|
color: var(--words-content-color);
|
|
|
}
|
|
|
[data-words-inspector] [data-accordion-content] {
|
|
|
padding-block-end: var(--space-2-5);
|
|
|
}
|
|
|
|
|
|
[data-words-inspector-row] {
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
gap: var(--space-1-5);
|
|
|
}
|
|
|
/* Inspector row gated by a sibling toggle (e.g. image Width / Height /
|
|
|
Align gated by Fullwidth). Reduce contrast + block pointer events so
|
|
|
the user sees the field is dimmed but not removed (gives context for
|
|
|
what Fullwidth controls). The actual controls inside still set
|
|
|
`disabled`/`aria-disabled`; this is the visual sugar. */
|
|
|
[data-words-inspector-row][data-disabled] {
|
|
|
opacity: 0.45;
|
|
|
pointer-events: none;
|
|
|
}
|
|
|
/* Image src field shares its row with an inline `Upload` button. The
|
|
|
wrapping group is the row's "value" slot — `data-inline` gives it
|
|
|
`justify-content: space-between` against the label, and the inner
|
|
|
flex distributes input (stretch) + button (auto) along the row. */
|
|
|
[data-words-inspector-src-group] {
|
|
|
display: flex;
|
|
|
flex-direction: row;
|
|
|
align-items: center;
|
|
|
gap: var(--space-1);
|
|
|
flex: 1;
|
|
|
min-inline-size: 0;
|
|
|
}
|
|
|
[data-words-inspector-src-group] input[data-words-inspector-text] {
|
|
|
flex: 1;
|
|
|
min-inline-size: 0;
|
|
|
}
|
|
|
/* Single-line row variant — label on the left, control on the right.
|
|
|
Used by `WordsColorRow` (each color = `Label [Picker]` per the
|
|
|
inspector's compact spec). Sliders / toggle groups keep the stacked
|
|
|
layout (label above, control below) because the control is wider. */
|
|
|
[data-words-inspector-row][data-inline] {
|
|
|
flex-direction: row;
|
|
|
align-items: center;
|
|
|
justify-content: space-between;
|
|
|
gap: var(--space-2);
|
|
|
}
|
|
|
/* The eidos ColorPicker root declares `inline-size: 100%` (so it fills
|
|
|
form rows by default). Inside an inline inspector row that override
|
|
|
collapses the label-to-picker distribution because the picker stretches
|
|
|
to the full remaining width. Pin to auto here so `justify-content:
|
|
|
space-between` actually pushes the picker's trigger to the right edge. */
|
|
|
[data-words-inspector-row][data-inline] [data-color-picker] {
|
|
|
inline-size: auto;
|
|
|
}
|
|
|
/* Pin the trigger to a fixed inline size so every color row's picker has
|
|
|
the same visual width regardless of the hex string length (6 chars vs
|
|
|
8 chars with alpha). `tabular-nums` keeps the hex characters at a
|
|
|
consistent advance width so the text doesn't jitter while the picker
|
|
|
value oscillates during drag. */
|
|
|
[data-words-inspector-row][data-inline] [data-color-picker-trigger] {
|
|
|
inline-size: 7.5rem;
|
|
|
justify-content: flex-start;
|
|
|
font-variant-numeric: tabular-nums;
|
|
|
}
|
|
|
[data-words-inspector-rowhead] {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: space-between;
|
|
|
gap: var(--space-2);
|
|
|
}
|
|
|
[data-words-inspector-label] {
|
|
|
font-size: var(--words-font-size-sm);
|
|
|
color: var(--words-command-color);
|
|
|
/* Font-family is inherited from `[data-words-inspector]` (forced sans
|
|
|
at the container level). */
|
|
|
}
|
|
|
[data-words-inspector-value] {
|
|
|
font-size: var(--words-font-size-sm);
|
|
|
color: var(--words-content-color);
|
|
|
font-variant-numeric: tabular-nums;
|
|
|
}
|
|
|
/* Compact numeric input in `WordsNumRow` rowhead — pins width so the
|
|
|
label / slider layout doesn't shift as the user types longer numbers.
|
|
|
`tabular-nums` keeps the digits at uniform advance width. */
|
|
|
[data-words-inspector-rowhead] [data-number-field] {
|
|
|
/* Wide enough for − 1.6 + (decimals + both steppers). The old 4.5rem
|
|
|
clipped the value and hid decimals on line-height / font-size. */
|
|
|
inline-size: 6.75rem;
|
|
|
flex: 0 0 auto;
|
|
|
}
|
|
|
/* Inline rows (label left, control right) that host a NumberField — pin the
|
|
|
field to the SAME fixed width as the rowhead fields. A `data-inline` row
|
|
|
distributes leftover space via `space-between`, so a longer label (e.g.
|
|
|
"Ancho") left a narrower field than a shorter one ("Alto") — the image
|
|
|
Width / Height fields came out visibly different widths. Pinning makes
|
|
|
every inline numeric field identical. */
|
|
|
[data-words-inspector-row][data-inline] [data-number-field] {
|
|
|
inline-size: 6.75rem;
|
|
|
flex: 0 0 auto;
|
|
|
}
|
|
|
|
|
|
/* ── Unified inspector field frame ─────────────────────────────────────
|
|
|
Every inspector NumberField reads as ONE bordered box — the same chrome
|
|
|
as the plain text inputs — regardless of the `variant` the row component
|
|
|
passes (`ghost` rendered borderless, `outline` drew its own; that mix is
|
|
|
the "some ring, some black border, some nothing" inconsistency). The
|
|
|
extra [data-size] bumps specificity to 0,3,0 so this beats the recipe's
|
|
|
per-variant rules (0,2,0) without touching the components. The steppers
|
|
|
sit INSIDE the box (borderless), the value centers, focus is one accent
|
|
|
ring shared with the text inputs. */
|
|
|
[data-words-inspector] [data-number-field][data-size] {
|
|
|
border: 1px solid var(--words-border);
|
|
|
border-radius: var(--words-radius-xs);
|
|
|
background: var(--words-bg);
|
|
|
padding-inline: 2px;
|
|
|
block-size: 1.75rem;
|
|
|
transition:
|
|
|
border-color var(--words-transition-duration) var(--words-transition-ease, ease),
|
|
|
box-shadow var(--words-transition-duration) var(--words-transition-ease, ease);
|
|
|
}
|
|
|
[data-words-inspector] [data-number-field][data-size]:hover:not([data-focused]) {
|
|
|
border-color: var(--words-border-hover);
|
|
|
}
|
|
|
[data-words-inspector] [data-number-field][data-size][data-focused] {
|
|
|
/* Single focus frame — one accent ring; the border stays structural so a
|
|
|
focused field shows ONE highlight, not an accent border AND a ring. */
|
|
|
box-shadow: 0 0 0 2px color-mix(in srgb, var(--_words-accent) var(--words-wash-strong), transparent);
|
|
|
}
|
|
|
/* Eidos TextArea (image alt) shares the inspector input chrome. Retokenise it
|
|
|
to the WORDS palette so it reads identically to the plain text inputs —
|
|
|
same fill + border + radius + size — instead of the global neutral skin
|
|
|
(transparent fill, lighter border) that made it look unframed and odd. */
|
|
|
[data-words-inspector] [data-textarea] {
|
|
|
--_textarea-border: var(--words-border);
|
|
|
--_textarea-border-hover: var(--words-border-hover);
|
|
|
--_textarea-bg: var(--words-bg);
|
|
|
--_textarea-radius: var(--words-radius-xs);
|
|
|
--_textarea-font-size: var(--words-font-size-sm);
|
|
|
}
|
|
|
[data-words-inspector] [data-textarea][data-focused] [data-textarea-input] {
|
|
|
/* Same single-ring focus as every other inspector field. */
|
|
|
border-color: var(--words-border);
|
|
|
box-shadow: 0 0 0 2px color-mix(in srgb, var(--_words-accent) var(--words-wash-strong), transparent);
|
|
|
}
|
|
|
[data-words-inspector] [data-number-field-input] {
|
|
|
inline-size: 100%;
|
|
|
min-inline-size: 0;
|
|
|
text-align: center;
|
|
|
font-variant-numeric: tabular-nums;
|
|
|
font-size: var(--words-font-size-sm);
|
|
|
}
|
|
|
[data-words-inspector] [data-number-field-increment-trigger],
|
|
|
[data-words-inspector] [data-number-field-decrement-trigger] {
|
|
|
flex: 0 0 auto;
|
|
|
inline-size: 1.15rem;
|
|
|
min-inline-size: 1.15rem;
|
|
|
block-size: 1.4rem;
|
|
|
border-color: transparent;
|
|
|
background: transparent;
|
|
|
color: var(--words-command-color);
|
|
|
border-radius: var(--words-radius-xs);
|
|
|
}
|
|
|
[data-words-inspector] [data-number-field-increment-trigger]:hover:not([data-disabled]),
|
|
|
[data-words-inspector] [data-number-field-decrement-trigger]:hover:not([data-disabled]) {
|
|
|
background: color-mix(in srgb, var(--words-content-color) var(--words-wash-subtle), transparent);
|
|
|
color: var(--words-content-color);
|
|
|
}
|
|
|
|
|
|
/* WordsSpacingRow — 4-side editor with PAIR chain.
|
|
|
Layout: a vertical stack of TWO pair rows. Each pair holds two cells
|
|
|
(label-above-input) plus its OWN link toggle. The two pairs are
|
|
|
independent — chaining one does not chain the other. This makes the
|
|
|
pair semantics unmistakable visually. */
|
|
|
[data-words-inspector-spacing] {
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
gap: var(--space-2);
|
|
|
}
|
|
|
[data-words-inspector-spacing-head] {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: space-between;
|
|
|
gap: var(--space-2);
|
|
|
}
|
|
|
[data-words-inspector-spacing-grid] {
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
gap: var(--space-2);
|
|
|
}
|
|
|
[data-words-inspector-spacing-pair] {
|
|
|
display: grid;
|
|
|
/* Two equal-width inputs + a small fixed slot for the link toggle. */
|
|
|
grid-template-columns: 1fr 1fr auto;
|
|
|
align-items: end;
|
|
|
gap: var(--space-1);
|
|
|
/* Bump icon size in this scope so the link/unlink chip is visible at
|
|
|
the Toggle xs size (default `--icon-size` is too small here). */
|
|
|
--icon-size: 14px;
|
|
|
}
|
|
|
[data-words-inspector-spacing-cell] {
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
gap: 2px;
|
|
|
min-inline-size: 0;
|
|
|
}
|
|
|
[data-words-inspector-spacing-cell-label] {
|
|
|
font-size: var(--words-font-size-xs, 0.75rem);
|
|
|
color: var(--words-content-color);
|
|
|
opacity: 0.7;
|
|
|
letter-spacing: 0.02em;
|
|
|
}
|
|
|
[data-words-inspector-spacing-cell] [data-number-field] {
|
|
|
inline-size: 100%;
|
|
|
min-inline-size: 0;
|
|
|
}
|
|
|
[data-words-inspector-spacing-cell] [data-number-field-input] {
|
|
|
inline-size: 100%;
|
|
|
text-align: center;
|
|
|
font-variant-numeric: tabular-nums;
|
|
|
font-size: var(--words-font-size-sm);
|
|
|
}
|
|
|
|
|
|
/* Block panel — bare text inputs (code language, image alt + caption).
|
|
|
Styled to match the inspector chrome (sans, sm size, ghost
|
|
|
background) so they sit among the ToggleGroups without visual noise.
|
|
|
`<datalist>` suggestions render natively on focus. The shared
|
|
|
`[data-words-inspector-text]` selector is for plain text edits;
|
|
|
`[data-words-inspector-code-language]` keeps the original selector
|
|
|
for backward compatibility — both inherit the same base rules. */
|
|
|
[data-words-inspector-text],
|
|
|
[data-words-inspector-code-language] {
|
|
|
flex: 1 1 0;
|
|
|
min-inline-size: 0;
|
|
|
min-block-size: 1.75rem;
|
|
|
font: inherit;
|
|
|
font-size: var(--words-font-size-sm);
|
|
|
padding: 4px 8px;
|
|
|
border: 1px solid var(--words-border);
|
|
|
border-radius: var(--words-radius-xs);
|
|
|
background: var(--words-bg);
|
|
|
color: var(--words-content-color);
|
|
|
transition:
|
|
|
border-color var(--words-transition-duration) var(--words-transition-ease, ease),
|
|
|
box-shadow var(--words-transition-duration) var(--words-transition-ease, ease);
|
|
|
}
|
|
|
[data-words-inspector-text]:hover:not(:focus),
|
|
|
[data-words-inspector-code-language]:hover:not(:focus) {
|
|
|
border-color: var(--words-border-hover);
|
|
|
}
|
|
|
/* Single focus frame — ONE accent ring (box-shadow), NOT an inset `outline`
|
|
|
and NOT also recolouring the border to accent. A field that drew an accent
|
|
|
border AND a ring read as two stacked frames; the ring alone is the focus
|
|
|
signal, the border stays structural. Shared by every inspector field. */
|
|
|
[data-words-inspector-text]:focus-visible,
|
|
|
[data-words-inspector-code-language]:focus-visible {
|
|
|
outline: none;
|
|
|
box-shadow: 0 0 0 2px color-mix(in srgb, var(--_words-accent) var(--words-wash-strong), transparent);
|
|
|
}
|
|
|
|
|
|
/* Callout intent swatch — small color dot rendered INSIDE the
|
|
|
ToggleGroup chip alongside its localized label. Color comes from
|
|
|
the doctrinal palette token for each intent (so a `risk` swatch
|
|
|
matches the actual `risk` callout's accent). Lives in the chip
|
|
|
button's flex flow; sits at the inline-start. */
|
|
|
[data-words-intent-swatch] {
|
|
|
display: inline-block;
|
|
|
inline-size: 0.625rem;
|
|
|
block-size: 0.625rem;
|
|
|
border-radius: 50%;
|
|
|
margin-inline-end: 4px;
|
|
|
vertical-align: -1px;
|
|
|
background: var(--color-neutral-solid);
|
|
|
}
|
|
|
[data-words-intent-swatch][data-intent='neutral'] {
|
|
|
background: var(--color-neutral-solid);
|
|
|
}
|
|
|
[data-words-intent-swatch][data-intent='affirm'] {
|
|
|
background: var(--color-affirm-solid);
|
|
|
}
|
|
|
[data-words-intent-swatch][data-intent='fulfill'] {
|
|
|
background: var(--color-fulfill-solid);
|
|
|
}
|
|
|
[data-words-intent-swatch][data-intent='risk'] {
|
|
|
background: var(--color-risk-solid);
|
|
|
}
|
|
|
[data-words-intent-swatch][data-intent='threat'] {
|
|
|
background: var(--color-threat-solid);
|
|
|
}
|
|
|
[data-words-intent-swatch][data-intent='loss'] {
|
|
|
background: var(--color-loss-solid);
|
|
|
}
|
|
|
|
|
|
/* Border-style swatch — the chip IS the line preview. No lucide icon
|
|
|
for solid/dashed/dotted is fitting, so we render a short top border
|
|
|
in the current ink. The swatch line scales with the chip width so
|
|
|
bigger sizes show a longer sample. */
|
|
|
[data-words-border-style-swatch] {
|
|
|
display: inline-block;
|
|
|
inline-size: 1.25rem;
|
|
|
block-size: 0;
|
|
|
border-block-start-width: 2px;
|
|
|
border-block-start-color: currentColor;
|
|
|
}
|
|
|
[data-words-border-style-swatch='solid'] {
|
|
|
border-block-start-style: solid;
|
|
|
}
|
|
|
[data-words-border-style-swatch='dashed'] {
|
|
|
border-block-start-style: dashed;
|
|
|
}
|
|
|
[data-words-border-style-swatch='dotted'] {
|
|
|
border-block-start-style: dotted;
|
|
|
}
|
|
|
|
|
|
/* WordsBlockPanel hierarchical subsections (table cell + table).
|
|
|
Sits inside `data-words-inspector-section` and groups related rows
|
|
|
under a small label. Keeps the visual separation between scopes
|
|
|
(cell-level vs table-level) without the heaviness of a second
|
|
|
accordion item. */
|
|
|
[data-words-inspector-subsection] {
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
gap: var(--space-2);
|
|
|
padding-block: var(--space-1);
|
|
|
}
|
|
|
[data-words-inspector-subsection] + [data-words-inspector-subsection] {
|
|
|
border-block-start: 1px dashed var(--words-border);
|
|
|
padding-block-start: var(--space-2);
|
|
|
}
|
|
|
[data-words-inspector-subsection-title] {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: space-between;
|
|
|
gap: var(--space-2);
|
|
|
min-block-size: 1.5rem;
|
|
|
font-size: var(--words-font-size-xs, 0.75rem);
|
|
|
color: var(--words-content-color);
|
|
|
opacity: 0.75;
|
|
|
text-transform: uppercase;
|
|
|
letter-spacing: 0.04em;
|
|
|
font-weight: 600;
|
|
|
}
|
|
|
/* Per-column child-block list (columns panel). Each item: a flexible
|
|
|
selectable label + a trailing delete button. Lists the column's
|
|
|
contents so the user can select (→ that block's panel) or delete
|
|
|
any block from the inspector. */
|
|
|
[data-words-inspector-block-list] {
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
gap: var(--space-1);
|
|
|
}
|
|
|
[data-words-inspector-sublabel] {
|
|
|
font-size: var(--words-font-size-xs, 0.6875rem);
|
|
|
font-weight: 600;
|
|
|
color: var(--words-content-color);
|
|
|
opacity: 0.55;
|
|
|
text-transform: uppercase;
|
|
|
letter-spacing: 0.04em;
|
|
|
}
|
|
|
[data-words-inspector-block-item] {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: var(--space-1);
|
|
|
}
|
|
|
[data-words-inspector-block-select] {
|
|
|
flex: 1 1 auto;
|
|
|
min-width: 0;
|
|
|
text-align: start;
|
|
|
padding: var(--space-1) var(--space-2);
|
|
|
font-size: var(--words-font-size-sm, 0.8125rem);
|
|
|
color: var(--words-content-color);
|
|
|
background: color-mix(in srgb, var(--words-content-color) var(--words-wash-faint), transparent);
|
|
|
border: 1px solid var(--words-border);
|
|
|
border-radius: var(--words-radius-xs);
|
|
|
cursor: pointer;
|
|
|
overflow: hidden;
|
|
|
white-space: nowrap;
|
|
|
text-overflow: ellipsis;
|
|
|
}
|
|
|
[data-words-inspector-block-select]:hover {
|
|
|
background: color-mix(in srgb, var(--words-content-color) var(--words-wash-faint), transparent);
|
|
|
}
|
|
|
|
|
|
/* Table insert/delete ops row — 3 small buttons in a horizontal flex
|
|
|
group (above/below/delete for rows; left/right/delete for columns).
|
|
|
Sits below the count display so the user reads "Rows: 3" then sees
|
|
|
the actions, mirroring Notion/Word table panels. */
|
|
|
[data-words-inspector-table-ops] {
|
|
|
display: flex;
|
|
|
gap: var(--space-1);
|
|
|
flex-wrap: wrap;
|
|
|
}
|
|
|
|
|
|
[data-words-inspector-empty] {
|
|
|
padding: var(--space-2) 0;
|
|
|
font-size: var(--words-font-size-sm);
|
|
|
color: var(--words-placeholder-color);
|
|
|
}
|
|
|
|
|
|
/* Sidebar — docked, always visible (builder-style). The rail reserves a
|
|
|
right gutter; the sidebar itself is absolutely positioned so the frame's
|
|
|
height follows the CONTENT, not the (usually taller) inspector. A flex
|
|
|
row would stretch the frame to the inspector's height and leave dead
|
|
|
space below the text — the inspector instead scrolls within the content
|
|
|
height. */
|
|
|
[data-words][data-inspector='sidebar'] {
|
|
|
padding-inline-end: 16rem;
|
|
|
}
|
|
|
[data-words-sidebar] {
|
|
|
position: absolute;
|
|
|
inset-block: 0;
|
|
|
inset-inline-end: 0;
|
|
|
inline-size: 16rem;
|
|
|
overflow-y: auto;
|
|
|
border-inline-start: var(--words-border-width) solid var(--words-border);
|
|
|
border-start-end-radius: var(--_words-radius);
|
|
|
border-end-end-radius: var(--_words-radius);
|
|
|
background: var(--words-bg);
|
|
|
}
|
|
|
|
|
|
/* Drawer — slide-in panel from the right edge + a fixed toggle. */
|
|
|
/* Clip the frame on the X axis so the closed panel — parked off the right
|
|
|
edge via translateX(100%) — is genuinely HIDDEN and the open panel appears
|
|
|
to emerge from the edge. Without this the frame's overflow stays visible,
|
|
|
so the closed panel just sits outside the right border (visible, "only
|
|
|
shifts"). `clip` on X only (not `overflow: hidden`): the block-gutter grip
|
|
|
lives in the left margin INSIDE the frame, and the bubble toolbar overflows
|
|
|
vertically — both must stay visible, so the Y axis is left untouched. */
|
|
|
[data-words][data-inspector='drawer'] {
|
|
|
overflow-x: clip;
|
|
|
}
|
|
|
[data-words-drawer-toggle] {
|
|
|
position: absolute;
|
|
|
inset-block-start: var(--space-3);
|
|
|
inset-inline-end: var(--space-3);
|
|
|
z-index: 21;
|
|
|
}
|
|
|
[data-words-drawer-panel] {
|
|
|
position: absolute;
|
|
|
inset-block: 0;
|
|
|
inset-inline-end: 0;
|
|
|
z-index: 20;
|
|
|
inline-size: 16rem;
|
|
|
max-inline-size: 85%;
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
overflow-y: auto;
|
|
|
background: var(--words-bg);
|
|
|
border-inline-start: var(--words-border-width) solid var(--words-border);
|
|
|
border-start-end-radius: var(--_words-radius);
|
|
|
border-end-end-radius: var(--_words-radius);
|
|
|
box-shadow: var(--depth-overlay-shadow), var(--depth-overlay-halo);
|
|
|
transform: translateX(100%);
|
|
|
transition: transform var(--words-transition-duration) var(--words-transition-ease);
|
|
|
}
|
|
|
[data-words-drawer-panel][data-open] {
|
|
|
transform: translateX(0);
|
|
|
}
|
|
|
[data-words-drawer-panel-head] {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: space-between;
|
|
|
padding: var(--space-2) var(--space-2) var(--space-2) var(--space-4);
|
|
|
border-block-end: var(--words-border-width) solid var(--words-border);
|
|
|
font-size: var(--words-font-size-md);
|
|
|
font-weight: var(--words-strong-font-weight);
|
|
|
color: var(--words-content-color);
|
|
|
}
|
|
|
|
|
|
/* Popover — a corner toggle floats the inspector. */
|
|
|
[data-words-popover-anchor] {
|
|
|
position: absolute;
|
|
|
inset-block-start: var(--space-3);
|
|
|
inset-inline-end: var(--space-3);
|
|
|
z-index: 21;
|
|
|
}
|
|
|
[data-words-inspector-popover] {
|
|
|
inline-size: 16rem;
|
|
|
padding: 0;
|
|
|
}
|
|
|
|
|
|
/* ── Content surface (the contenteditable) ───────────────────────────── */
|
|
|
|
|
|
[data-words-content] {
|
|
|
position: relative;
|
|
|
flex: 1 1 auto;
|
|
|
min-block-size: var(--_words-content-min);
|
|
|
max-block-size: var(--_words-content-max);
|
|
|
overflow-y: auto;
|
|
|
padding: var(--_words-content-py) var(--_words-content-px);
|
|
|
font-size: var(--_words-content-font-size);
|
|
|
line-height: 1.7;
|
|
|
color: var(--words-content-color);
|
|
|
outline: none;
|
|
|
-webkit-font-smoothing: antialiased;
|
|
|
text-rendering: optimizeLegibility;
|
|
|
/* Soft line breaks: Shift+Enter inserts a literal `\n` into the text
|
|
|
model (`insertLineBreak`). With the default `white-space: normal` the
|
|
|
browser collapses that `\n` to a space — the break is invisible.
|
|
|
`pre-wrap` preserves newlines AND still wraps long lines; this is the
|
|
|
canonical editable-surface rule (ProseMirror / Lexical / TipTap all
|
|
|
set it on the editor root). Code blocks render as `<pre>` whose UA
|
|
|
`white-space: pre` overrides this inherited value, so they keep their
|
|
|
own behaviour. */
|
|
|
white-space: pre-wrap;
|
|
|
}
|
|
|
|
|
|
/* Active-block marker. `words-activate.svelte` stamps `data-words-active`
|
|
|
+ a nesting-depth index (`data-words-depth`) on the active block's DOM
|
|
|
node — imperatively, NOT via the document model, so it never re-renders
|
|
|
the contenteditable (which would fight the caret). A dotted OUTLINE
|
|
|
(not border → zero reflow) shows the inspector's current target on the
|
|
|
canvas. The hue shifts by depth so nested structure reads at a glance:
|
|
|
blue at top level, then teal / violet / amber descending into columns. */
|
|
|
/* Active-block rail — a single vertical bar in the GUTTER (left margin), at a
|
|
|
FIXED column (frame-relative, so it never shifts right for nested blocks),
|
|
|
one fixed colour. Its top/height track the active block; the rail element is
|
|
|
rendered by `words-active-rail.svelte` and positioned here. */
|
|
|
[data-words-active-rail] {
|
|
|
position: absolute;
|
|
|
inset-inline-start: calc(var(--_words-content-px) - 1.4rem);
|
|
|
z-index: 1;
|
|
|
inline-size: 3px;
|
|
|
border-radius: 2px;
|
|
|
background: var(--color-primary-solid);
|
|
|
pointer-events: none;
|
|
|
transition:
|
|
|
top var(--words-transition-duration) var(--words-transition-ease),
|
|
|
height var(--words-transition-duration) var(--words-transition-ease);
|
|
|
}
|
|
|
|
|
|
[data-words-content] :where([data-words-node='block']):first-child {
|
|
|
margin-block-start: 0;
|
|
|
}
|
|
|
[data-words-content] :where([data-words-node='block']):last-child {
|
|
|
margin-block-end: 0;
|
|
|
}
|
|
|
|
|
|
/* ── Placeholder ──────────────────────────────────────────────────────── */
|
|
|
|
|
|
[data-words-placeholder] {
|
|
|
position: absolute;
|
|
|
inset-block-start: var(--_words-content-py);
|
|
|
inset-inline-start: var(--_words-content-px);
|
|
|
font-size: var(--_words-content-font-size);
|
|
|
line-height: 1.7;
|
|
|
color: var(--words-placeholder-color);
|
|
|
pointer-events: none;
|
|
|
user-select: none;
|
|
|
}
|
|
|
[data-words-placeholder][data-hidden] {
|
|
|
display: none;
|
|
|
}
|
|
|
|
|
|
/* ── Block rhythm ─────────────────────────────────────────────────────── */
|
|
|
|
|
|
[data-words-content] :where(p, h1, h2, h3, blockquote, pre, ul, ol, table, figure, hr, [data-words-block='callout']) {
|
|
|
margin-block: var(--words-block-gap) 0;
|
|
|
}
|
|
|
|
|
|
/* Columns block — flex layout for side-by-side content. Each child
|
|
|
`[data-words-column]` is a flex item; the engine sets `flex: <user-
|
|
|
chosen value or 1>` inline so equal-width is the default and the
|
|
|
user can resize via the inspector. Gap reuses the block-gap token
|
|
|
so columns breathe the same vertical rhythm as the surrounding
|
|
|
content. Each column is also a flex container itself (column
|
|
|
direction) so its nested blocks stack vertically. */
|
|
|
[data-words-content] [data-words-block='columns'] {
|
|
|
display: flex;
|
|
|
flex-direction: row;
|
|
|
gap: var(--words-block-gap);
|
|
|
margin-block: var(--words-block-gap) 0;
|
|
|
align-items: stretch;
|
|
|
}
|
|
|
[data-words-content] [data-words-block='columns'] [data-words-column] {
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
min-inline-size: 0;
|
|
|
/* Visual cue while editing — without this, an empty columns block
|
|
|
collapses to 0 height and the user can't see where the layout is.
|
|
|
The dashed outline + breathing min-height make the structure
|
|
|
discoverable; the soft tone fades into the background once the
|
|
|
user fills the columns with real content. No padding — child
|
|
|
blocks own their own padding via the inspector's Spacing section
|
|
|
so the column dashed border traces exactly the content edge. */
|
|
|
min-block-size: 3em;
|
|
|
padding: 0;
|
|
|
border: 1px dashed var(--words-border);
|
|
|
border-radius: var(--words-radius-xs);
|
|
|
transition: border-color var(--words-transition-duration) var(--words-transition-ease);
|
|
|
}
|
|
|
[data-words-content] [data-words-block='columns'] [data-words-column]:hover,
|
|
|
[data-words-content] [data-words-block='columns'] [data-words-column]:focus-within {
|
|
|
border-color: var(--words-content-color);
|
|
|
}
|
|
|
/* Preview is read-only — the dashed border + min-height are editing
|
|
|
affordances; an empty column shouldn't advertise itself. Hide them. */
|
|
|
[data-words][data-mode='preview'] [data-words-block='columns'] [data-words-column] {
|
|
|
border-color: transparent;
|
|
|
min-block-size: 0;
|
|
|
}
|
|
|
/* Reset the universal block-rhythm `margin-block` for the FIRST nested
|
|
|
block of each column — otherwise that extra gap pushes the column
|
|
|
contents down relative to the column's frame, breaking vertical
|
|
|
alignment with siblings. */
|
|
|
[data-words-content] [data-words-block='columns'] [data-words-column] > *:first-child {
|
|
|
margin-block-start: 0;
|
|
|
}
|
|
|
|
|
|
|
|
|
/* ── Paragraph ────────────────────────────────────────────────────────── */
|
|
|
|
|
|
[data-words-block='paragraph'] {
|
|
|
margin-block: var(--words-block-gap) 0;
|
|
|
}
|
|
|
|
|
|
/* ── Headings ─────────────────────────────────────────────────────────── */
|
|
|
|
|
|
[data-words-block='heading'] {
|
|
|
margin-block: calc(var(--words-block-gap) * 1.5) 0;
|
|
|
color: var(--words-heading-color);
|
|
|
font-weight: var(--words-heading-font-weight);
|
|
|
line-height: var(--words-heading-line-height);
|
|
|
letter-spacing: -0.01em;
|
|
|
}
|
|
|
h1[data-words-block='heading'] {
|
|
|
font-size: var(--words-heading-h1-font-size);
|
|
|
}
|
|
|
h2[data-words-block='heading'] {
|
|
|
font-size: var(--words-heading-h2-font-size);
|
|
|
}
|
|
|
h3[data-words-block='heading'] {
|
|
|
font-size: var(--words-heading-h3-font-size);
|
|
|
}
|
|
|
|
|
|
/* ── Quote ────────────────────────────────────────────────────────────── */
|
|
|
|
|
|
blockquote[data-words-block='quote'] {
|
|
|
padding-inline-start: var(--words-quote-px);
|
|
|
border-inline-start: var(--words-quote-border-width) solid var(--words-primary-border);
|
|
|
color: var(--words-quote-color);
|
|
|
font-style: italic;
|
|
|
}
|
|
|
|
|
|
/* ── Code block ───────────────────────────────────────────────────────── */
|
|
|
|
|
|
pre[data-words-block='code'] {
|
|
|
padding: calc(var(--words-code-py) * 3) calc(var(--words-code-px) * 3);
|
|
|
border-radius: var(--words-radius-sm);
|
|
|
background: var(--words-code-bg);
|
|
|
color: var(--words-code-color);
|
|
|
font-family: var(--words-code-font-family);
|
|
|
font-size: 0.9em;
|
|
|
line-height: 1.6;
|
|
|
overflow-x: auto;
|
|
|
tab-size: 2;
|
|
|
}
|
|
|
pre[data-words-block='code'] code {
|
|
|
font-family: inherit;
|
|
|
white-space: pre;
|
|
|
}
|
|
|
|
|
|
/* syntax tokens (highlightWordsCode) */
|
|
|
[data-words-code-token='keyword'] {
|
|
|
color: var(--words-primary-text);
|
|
|
font-weight: var(--words-strong-font-weight);
|
|
|
}
|
|
|
[data-words-code-token='string'] {
|
|
|
color: var(--words-affirm-text);
|
|
|
}
|
|
|
[data-words-code-token='number'] {
|
|
|
color: var(--words-fulfill-text);
|
|
|
}
|
|
|
[data-words-code-token='comment'] {
|
|
|
color: var(--words-content-color);
|
|
|
opacity: 0.6;
|
|
|
font-style: italic;
|
|
|
}
|
|
|
[data-words-code-token='function'] {
|
|
|
color: var(--words-secondary-text);
|
|
|
}
|
|
|
|
|
|
/* ── Lists ────────────────────────────────────────────────────────────── */
|
|
|
|
|
|
[data-words-block='list'] {
|
|
|
padding-inline-start: var(--words-list-padding-inline-start);
|
|
|
}
|
|
|
[data-words-block='list'][data-words-list-kind='check'] {
|
|
|
padding-inline-start: 0;
|
|
|
list-style: none;
|
|
|
}
|
|
|
li[data-words-node='list-item'] {
|
|
|
margin-block: 0.25em;
|
|
|
}
|
|
|
li[data-words-node='list-item'][data-words-indent='1'] {
|
|
|
margin-inline-start: var(--words-list-padding-inline-start);
|
|
|
}
|
|
|
li[data-words-node='list-item'][data-words-indent='2'] {
|
|
|
margin-inline-start: calc(var(--words-list-padding-inline-start) * 2);
|
|
|
}
|
|
|
|
|
|
/* check toggle */
|
|
|
[data-words-list-kind='check'] li[data-words-node='list-item'] {
|
|
|
display: flex;
|
|
|
align-items: flex-start;
|
|
|
gap: var(--space-2);
|
|
|
}
|
|
|
[data-words-check-toggle] {
|
|
|
flex: 0 0 auto;
|
|
|
inline-size: 1.1em;
|
|
|
block-size: 1.1em;
|
|
|
margin-block-start: 0.3em;
|
|
|
border: 1.5px solid var(--words-border-hover);
|
|
|
border-radius: var(--words-radius-sm);
|
|
|
background: var(--words-bg);
|
|
|
cursor: pointer;
|
|
|
transition:
|
|
|
background var(--words-transition-duration) var(--words-transition-ease),
|
|
|
border-color var(--words-transition-duration) var(--words-transition-ease);
|
|
|
}
|
|
|
[data-words-check-toggle][data-words-checked='true'] {
|
|
|
border-color: var(--words-primary-solid);
|
|
|
background: var(--words-primary-solid)
|
|
|
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='white' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3.5 8.5l3 3 6-7'/%3E%3C/svg%3E")
|
|
|
center / 70% no-repeat;
|
|
|
}
|
|
|
[data-words-checked='true'] :where(span[data-words-node='text']) {
|
|
|
color: var(--words-content-color);
|
|
|
opacity: 0.55;
|
|
|
text-decoration: line-through;
|
|
|
}
|
|
|
|
|
|
/* ── Table ────────────────────────────────────────────────────────────── */
|
|
|
|
|
|
table[data-words-block='table'] {
|
|
|
inline-size: 100%;
|
|
|
border-collapse: collapse;
|
|
|
font-size: 0.95em;
|
|
|
}
|
|
|
table[data-words-block='table'] :where(td, th) {
|
|
|
padding: var(--space-1-5) var(--space-2-5);
|
|
|
border: var(--words-border-width) solid var(--words-border);
|
|
|
text-align: start;
|
|
|
vertical-align: top;
|
|
|
}
|
|
|
table[data-words-block='table'] th {
|
|
|
background: var(--words-code-bg);
|
|
|
font-weight: var(--words-strong-font-weight);
|
|
|
}
|
|
|
[data-words-node='table-cell'][data-words-cell-align='center'] {
|
|
|
text-align: center;
|
|
|
}
|
|
|
[data-words-node='table-cell'][data-words-cell-align='right'] {
|
|
|
text-align: end;
|
|
|
}
|
|
|
[data-words-node='table-cell'][data-words-cell-vertical='middle'] {
|
|
|
vertical-align: middle;
|
|
|
}
|
|
|
[data-words-node='table-cell'][data-words-cell-vertical='bottom'] {
|
|
|
vertical-align: bottom;
|
|
|
}
|
|
|
|
|
|
/* ── Image ────────────────────────────────────────────────────────────── */
|
|
|
|
|
|
figure[data-words-block='image'] {
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
align-items: center;
|
|
|
gap: var(--space-2);
|
|
|
/* Browser default for <figure> is `margin-inline: 40px` — it eats
|
|
|
~80px from every column / container the figure sits in. Reset to 0
|
|
|
so the image can use the full content area of its parent (the
|
|
|
block-gap-aware margin-block reset above only touched the vertical
|
|
|
axis). */
|
|
|
margin-inline: 0;
|
|
|
}
|
|
|
/* When no explicit width attr is set, cap at the container so the image
|
|
|
doesn't break layout. When the user typed an explicit width in the
|
|
|
inspector (renders as `<img width="X">`), let that win — including
|
|
|
values LARGER than the natural / container size (intentional upscale
|
|
|
for hero use-cases). */
|
|
|
figure[data-words-block='image'] img:not([width]) {
|
|
|
max-inline-size: 100%;
|
|
|
block-size: auto;
|
|
|
border-radius: var(--words-radius-sm);
|
|
|
}
|
|
|
figure[data-words-block='image'] img[width] {
|
|
|
block-size: auto;
|
|
|
border-radius: var(--words-radius-sm);
|
|
|
}
|
|
|
figure[data-words-block='image'][data-words-image-align='left'] {
|
|
|
align-items: flex-start;
|
|
|
}
|
|
|
figure[data-words-block='image'][data-words-image-align='right'] {
|
|
|
align-items: flex-end;
|
|
|
}
|
|
|
/* Fullwidth: drop the figure's flex layout (which sized it to its
|
|
|
content + caused overflow on `inline-size: 100%` chains with the
|
|
|
column's flex parent) and force the figure to fill its container.
|
|
|
The column is `display: flex; align-items: normal` and its child
|
|
|
flex items default to `flex: 0 1 auto` — that auto basis collapses
|
|
|
block-level children to their intrinsic content (= the img's natural
|
|
|
width). Setting `inline-size: 100%` explicitly + `box-sizing:
|
|
|
border-box` is what makes the figure fill the column's content area
|
|
|
without overflowing past its padding. The img inside then stretches
|
|
|
inside the figure. */
|
|
|
figure[data-words-block='image'][data-words-image-full-width] {
|
|
|
display: block;
|
|
|
inline-size: 100%;
|
|
|
max-inline-size: 100%;
|
|
|
box-sizing: border-box;
|
|
|
}
|
|
|
figure[data-words-block='image'][data-words-image-full-width] img {
|
|
|
display: block;
|
|
|
inline-size: 100%;
|
|
|
max-inline-size: 100%;
|
|
|
block-size: auto;
|
|
|
box-sizing: border-box;
|
|
|
}
|
|
|
figure[data-words-block='image'] figcaption {
|
|
|
font-size: 0.85em;
|
|
|
color: var(--words-quote-color);
|
|
|
text-align: center;
|
|
|
}
|
|
|
figure[data-words-block='image'][data-words-block-selected] {
|
|
|
outline: 2px solid var(--words-primary-solid);
|
|
|
outline-offset: 3px;
|
|
|
border-radius: var(--words-radius-sm);
|
|
|
}
|
|
|
figure[data-words-block='image'][data-words-image-status='uploading'] {
|
|
|
opacity: 0.6;
|
|
|
}
|
|
|
figure[data-words-block='image'][data-words-image-status='error'] {
|
|
|
outline: 2px solid var(--words-threat-solid);
|
|
|
outline-offset: 3px;
|
|
|
}
|
|
|
|
|
|
/* ── Divider ──────────────────────────────────────────────────────────── */
|
|
|
|
|
|
hr[data-words-block='divider'] {
|
|
|
block-size: 0;
|
|
|
border: none;
|
|
|
border-block-start: var(--words-border-width) solid var(--words-border);
|
|
|
margin-block: calc(var(--words-block-gap) * 1.25) 0;
|
|
|
}
|
|
|
hr[data-words-block='divider'][data-words-block-selected] {
|
|
|
border-block-start-color: var(--words-primary-solid);
|
|
|
border-block-start-width: 2px;
|
|
|
}
|
|
|
|
|
|
/* ── Callout ──────────────────────────────────────────────────────────── */
|
|
|
|
|
|
[data-words-block='callout'] {
|
|
|
padding: var(--space-3) var(--space-4);
|
|
|
border: var(--words-border-width) solid var(--words-neutral-border);
|
|
|
border-inline-start-width: 3px;
|
|
|
border-radius: var(--words-radius-sm);
|
|
|
background: var(--words-neutral-track);
|
|
|
}
|
|
|
[data-words-callout-title] {
|
|
|
margin-block-end: var(--space-1);
|
|
|
font-weight: var(--words-strong-font-weight);
|
|
|
}
|
|
|
[data-words-block='callout'] :where([data-words-node='block']):first-child {
|
|
|
margin-block-start: 0;
|
|
|
}
|
|
|
[data-words-callout-intent='affirm'] {
|
|
|
border-color: var(--words-affirm-border);
|
|
|
background: var(--words-affirm-track);
|
|
|
}
|
|
|
[data-words-callout-intent='affirm'] [data-words-callout-title] {
|
|
|
color: var(--words-affirm-text);
|
|
|
}
|
|
|
[data-words-callout-intent='fulfill'] {
|
|
|
border-color: var(--words-fulfill-border);
|
|
|
background: var(--words-fulfill-track);
|
|
|
}
|
|
|
[data-words-callout-intent='fulfill'] [data-words-callout-title] {
|
|
|
color: var(--words-fulfill-text);
|
|
|
}
|
|
|
[data-words-callout-intent='risk'] {
|
|
|
border-color: var(--words-risk-border);
|
|
|
background: var(--words-risk-track);
|
|
|
}
|
|
|
[data-words-callout-intent='risk'] [data-words-callout-title] {
|
|
|
color: var(--words-risk-text);
|
|
|
}
|
|
|
[data-words-callout-intent='threat'] {
|
|
|
border-color: var(--words-threat-border);
|
|
|
background: var(--words-threat-track);
|
|
|
}
|
|
|
[data-words-callout-intent='threat'] [data-words-callout-title] {
|
|
|
color: var(--words-threat-text);
|
|
|
}
|
|
|
[data-words-callout-intent='loss'] {
|
|
|
border-color: var(--words-loss-border);
|
|
|
background: var(--words-loss-track);
|
|
|
}
|
|
|
[data-words-callout-intent='loss'] [data-words-callout-title] {
|
|
|
color: var(--words-loss-text);
|
|
|
}
|
|
|
|
|
|
/* ── Inline marks ─────────────────────────────────────────────────────── */
|
|
|
|
|
|
[data-words-marks~='bold'] {
|
|
|
font-weight: var(--words-strong-font-weight);
|
|
|
}
|
|
|
[data-words-marks~='italic'] {
|
|
|
font-style: italic;
|
|
|
}
|
|
|
[data-words-marks~='underline'] {
|
|
|
text-decoration-line: underline;
|
|
|
text-underline-offset: 0.18em;
|
|
|
}
|
|
|
[data-words-marks~='strike'] {
|
|
|
text-decoration-line: line-through;
|
|
|
}
|
|
|
[data-words-marks~='underline'][data-words-marks~='strike'] {
|
|
|
text-decoration-line: underline line-through;
|
|
|
}
|
|
|
[data-words-marks~='code'] {
|
|
|
padding: var(--words-code-py) var(--words-code-px);
|
|
|
border-radius: var(--words-code-radius);
|
|
|
background: var(--words-code-bg);
|
|
|
font-family: var(--words-code-font-family);
|
|
|
font-size: 0.9em;
|
|
|
}
|
|
|
|
|
|
/* ── Link ─────────────────────────────────────────────────────────────── */
|
|
|
|
|
|
a[data-words-node='link'] {
|
|
|
color: var(--words-primary-text);
|
|
|
text-decoration-line: underline;
|
|
|
text-underline-offset: 0.15em;
|
|
|
text-decoration-thickness: 1px;
|
|
|
cursor: pointer;
|
|
|
}
|
|
|
a[data-words-node='link']:hover {
|
|
|
text-decoration-thickness: 2px;
|
|
|
}
|
|
|
|
|
|
/* ── Find/replace matches ─────────────────────────────────────────────── */
|
|
|
|
|
|
mark[data-words-find-match] {
|
|
|
border-radius: 2px;
|
|
|
background: var(--words-fulfill-track);
|
|
|
color: inherit;
|
|
|
}
|
|
|
mark[data-words-find-match][data-words-find-active] {
|
|
|
background: var(--words-fulfill-solid);
|
|
|
color: var(--words-fulfill-text);
|
|
|
}
|
|
|
|
|
|
/* ── Selection ────────────────────────────────────────────────────────── */
|
|
|
|
|
|
[data-words-content] ::selection {
|
|
|
background: color-mix(in srgb, var(--words-primary-solid) var(--words-wash-soft), transparent);
|
|
|
}
|