You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/src/uix/eidos/components/words/words.css

1516 lines
56 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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

Powered by TurnKey Linux.