/** * 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; /* nudge so the grip centres on the block's first line */ margin-block-start: 0.12em; /* 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 { from { opacity: 0; transform: translateX(-4px); } to { opacity: 1; transform: 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-4); padding: var(--space-4); } [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); } [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); } [data-words-inspector-value] { font-size: var(--words-font-size-sm); color: var(--words-content-color); font-variant-numeric: tabular-nums; } [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; } /* ── 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); } figure[data-words-block='image'] img { max-inline-size: 100%; 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; } 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); }