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