/** * 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); 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)); } /* ── 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 120ms var(--words-transition-ease); animation: words-gutter-in 140ms 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); } /* subtle displacement when the cursor lands on the handle */ [data-words-block-gutter] [data-words-block-handle]:hover { color: var(--words-content-color); transform: translateX(1px) scale(1.18); } /* dashed outline emphasising the block the handle acts on */ [data-words-block-outline] { position: absolute; z-index: 1; pointer-events: none; border: 1px dashed var(--words-border-hover); border-radius: var(--words-radius-sm); animation: words-outline-in var(--words-transition-duration) var(--words-transition-ease); } @keyframes words-outline-in { from { opacity: 0; } to { opacity: 1; } } /* ── 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(--shadow-overlay); animation: words-bubble-in 120ms 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); } /* 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); /* Force the system sans stack on EVERY descendant of the inspector (labels, values, subsection titles, NumberField inputs, ToggleGroup chips, accordion triggers, table cell counters, etc.). The editor's content font can be serif when the surface is themed for reading (`Words` body inherits `--words-font-family`); the inspector chrome must stay sans regardless or the controls render with mixed typography. The container declaration alone is NOT enough — eidos primitives (Accordion.Trigger, Button, NumberField.Input, ToggleGroup.Item, …) set their own `font-family` in their recipe CSS, breaking the cascade. The `* { font-family: inherit }` rule forces every descendant to inherit from the container regardless of recipe overrides. This is the standard pattern when you want a subtree to share one typeface against shipped components that ship their own font choices. */ font-family: system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; } [data-words-inspector] *, [data-words-inspector] *::before, [data-words-inspector] *::after { font-family: inherit; } [data-words-inspector-title] { font-size: var(--words-font-size-md); font-weight: var(--words-strong-font-weight); color: var(--words-content-color); } [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; } [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: baseline; justify-content: space-between; } [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] { inline-size: 4.5rem; } [data-words-inspector-rowhead] [data-number-field-input] { inline-size: 100%; text-align: end; font-variant-numeric: tabular-nums; font-size: var(--words-font-size-sm); } /* 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: end; 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. `` 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; font: inherit; font-size: var(--words-font-size-sm); padding: 4px 8px; border: 1px solid var(--words-border); border-radius: var(--radius-2, 4px); background: transparent; color: inherit; } [data-words-inspector-text]:focus-visible, [data-words-inspector-code-language]:focus-visible { outline: 2px solid var(--color-primary, currentColor); outline-offset: -1px; } /* 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] { 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; } /* 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. */ [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(--shadow-overlay); 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; } [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: ` 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(--radius-2, 4px); 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); } /* 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
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 ``), 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) 22%, transparent); }