/** * Plan editor styling. * * MDXEditor's structural CSS is imported by the app; this retheme layer maps it * onto Ace's tokens so the editor reads as part of the product rather than as a * third-party widget. */ @import "./callout.css"; /* Evidence can extend beyond the clipped document column; only its panel takes hits. */ .plan-evidence-popover { pointer-events: none; position: fixed; inset: 0; z-index: 40; } .plan-evidence-panel { pointer-events: auto; position: fixed; width: min(20rem, calc(100vw - 24px)); max-height: calc(100dvh - 24px); overflow-y: auto; border-radius: var(--radius-md); background: var(--color-page); box-shadow: var(--shadow-overlay); outline: var(--edge-width) solid var(--color-edge); outline-offset: calc(-1 * var(--edge-width)); } .plan-research-draft-host, .plan-research-draft { width: 100%; max-width: var(--document-prose-measure); } .motion-research-draft { opacity: 0; transform: scale(var(--dropdown-pre-scale)); transform-origin: top left; transition: opacity var(--dropdown-open-dur) var(--dropdown-ease), transform var(--dropdown-open-dur) var(--dropdown-ease); } .motion-research-draft[data-motion="is-open"] { opacity: 1; transform: none; } .motion-research-draft[data-motion="is-closing"] { pointer-events: none; opacity: 0; transform: scale(var(--dropdown-closing-scale)); transition-duration: var(--dropdown-close-dur); } [data-research-ready] { box-shadow: var(--shadow-resting-strong); transition: box-shadow var(--duration-fast) var(--ease-out); } [data-research-ready]:hover, [data-research-ready]:focus-within { box-shadow: var(--shadow-raised); } .plan-research-open { position: absolute; z-index: 1; inset: 0; cursor: pointer; border: 0; border-radius: inherit; background: transparent; } /* Composer, progress and result share the document's prose column. */ .plan-research-composer, .plan-research-tracked { width: 100%; max-width: var(--document-prose-measure); } /* One surface: the composer is the field, so focus rings the whole card. */ .plan-research-composer { position: relative; display: grid; border: var(--edge-width) solid var(--color-edge); border-radius: var(--radius-lg); background: var(--color-page); box-shadow: var(--shadow-resting); } .plan-research-composer textarea { display: block; width: 100%; min-height: 4rem; resize: none; padding: 0.625rem 2.5rem 0.25rem 0.75rem; border: 0; background: transparent; font: inherit; font-size: var(--text-sm); color: var(--color-text-primary); } .plan-research-composer textarea::placeholder { color: var(--color-text-quaternary); } .plan-research-composer textarea:disabled { color: var(--color-text-tertiary); } .plan-research-dismiss { position: absolute; inset: 0.3125rem 0.3125rem auto auto; } .plan-research-composer-footer { display: flex; align-items: center; gap: 0.5rem; padding: 0.25rem 0.5rem 0.5rem 0.75rem; } .plan-research-message { flex: 1; min-width: 0; margin: 0; font-size: var(--text-xs); line-height: var(--text-xs--line-height); color: var(--color-text-tertiary); } .plan-research-message[data-tone="error"] { color: var(--color-destructive); } .plan-research-tracked { display: grid; } .plan-research-status { display: flex; align-items: center; gap: 0.5rem; min-height: 1.5rem; font-size: var(--text-sm); color: var(--color-text-tertiary); } /* Stages that lead with a badge give their header more room. */ .plan-research-status[data-lead] { margin-block-end: 0.25rem; } .plan-research-remove { display: flex; align-items: center; margin-inline-start: auto; } .plan-research-meta { color: var(--color-text-quaternary); } .plan-research-title { font-size: var(--text-sm); font-weight: 600; line-height: var(--text-sm--line-height); color: var(--color-text-primary); } .plan-research-brief { display: -webkit-box; overflow: hidden; -webkit-box-orient: vertical; -webkit-line-clamp: 2; font-weight: 400; } .plan-research-brief[data-open] { display: block; } .plan-research-more { display: block; padding: 0; border: 0; background: transparent; font: inherit; margin-block-start: calc(var(--spacing) * 0.5); border-radius: var(--radius-sm); font-size: var(--text-xs); font-weight: 400; color: var(--color-text-tertiary); cursor: pointer; } .plan-research-more:hover, .plan-research-more:focus-visible { color: var(--color-text-secondary); } .plan-research-card[data-stage="cancelled"] .plan-research-title { color: var(--color-text-tertiary); } .plan-research-summary { margin: 0; font-size: var(--text-sm); color: var(--color-text-secondary); } .plan-research-footer { display: flex; justify-content: flex-end; } .plan-research-dot { width: 0.5rem; height: 0.5rem; margin-inline: 0.1875rem; border-radius: 50%; background: var(--color-brand); animation: plan-research-pulse 1.4s var(--ease-out) infinite; } .plan-research-dot[data-idle] { background: var(--color-text-quaternary); animation: none; } @keyframes plan-research-pulse { 0% { box-shadow: 0 0 0 0 color-mix(in oklch, var(--color-brand) 45%, transparent); } 100% { box-shadow: 0 0 0 0.375rem transparent; } } .plan-research-badge { display: grid; flex-shrink: 0; place-items: center; width: 1.5rem; height: 1.5rem; border-radius: 50%; background: var(--color-danger-surface); color: var(--color-destructive); } .plan-research-badge[data-tone="brand"] { background: var(--color-brand-wash); color: var(--color-brand); } .plan-research-badge [data-nucleo-icon] { color: inherit; } .plan-research-callout { padding: 0.375rem 0.75rem; border-radius: var(--radius-md); background: var(--color-ruby-2); } .plan-research-callout p { margin: 0; padding-block: 0.125rem; font-size: var(--text-sm); line-height: var(--text-sm--line-height); color: var(--color-destructive-ink); } .plan-research-track { display: grid; grid-template-columns: repeat(5, 1fr); gap: 0.25rem; padding-inline: 0.25rem; } .plan-research-track span { position: relative; overflow: hidden; height: 0.3125rem; border-radius: var(--radius-full); background: var(--color-selected); transition: background-color 500ms var(--ease-out); } .plan-research-track span[data-state="now"], .plan-research-track span[data-state="done"] { background: var(--color-brand); } /* The sheen rides its own layer so it can fade without touching the fill. */ .plan-research-track span::after { content: ""; position: absolute; inset: 0; background: linear-gradient( 90deg, transparent 0%, color-mix(in oklch, white 70%, transparent) 50%, transparent 100% ); background-size: 50% 100%; background-repeat: no-repeat; opacity: 0; animation: plan-research-sheen 1.6s linear infinite; transition: opacity 400ms var(--ease-out); } .plan-research-track span[data-state="now"]::after { opacity: 1; transition-delay: var(--duration-base); } @keyframes plan-research-sheen { from { background-position: -100% 0; } to { background-position: 200% 0; } } .plan-research-found { padding-inline: 0.25rem; font-size: var(--text-xs); color: var(--color-text-tertiary); } /* Swap: layers share one grid cell and crossfade by opacity alone. */ .plan-research-swap { display: inline-grid; } .plan-research-swap > span { grid-area: 1 / 1; animation: plan-research-swap-in 360ms var(--ease-out) var(--duration-fast) both; } .plan-research-swap > span[data-initial] { animation: none; } .plan-research-swap > span[data-leaving] { animation: plan-research-swap-out 240ms var(--ease-out) both; } @keyframes plan-research-swap-in { from { opacity: 0; } } @keyframes plan-research-swap-out { to { opacity: 0; } } .plan-research-indicator { place-items: center; } .plan-research-indicator > span { display: grid; place-items: center; min-width: 0.875rem; min-height: 0.875rem; } .plan-research-indicator .plan-research-dot { margin: 0; } /* Fold: animates grid rows so height changes are never a jump. */ .plan-research-fold { display: grid; grid-template-rows: 0fr; opacity: 0; transition: grid-template-rows var(--acc-collapse) var(--motion-move), opacity var(--acc-collapse) var(--motion-move); } .plan-research-fold[data-open] { grid-template-rows: 1fr; opacity: 1; transition-duration: var(--acc-expand); } .plan-research-fold > div { min-height: 0; overflow: hidden; } .plan-research-tracked > .plan-research-fold > div { display: grid; gap: 0.5rem; padding-block-start: 0.625rem; } /* Inside the card a fold owns its gap, so a closed one takes no space. */ .plan-research-card .plan-research-fold { margin-block-start: -0.5rem; } .plan-research-card .plan-research-fold > div > * { margin-block-start: 0.5rem; } @media (prefers-reduced-motion: reduce) { .plan-research-dot, .plan-research-swap > span, .plan-research-track span::after { animation: none; } .motion-research-draft, .plan-research-fold, .plan-research-track span, .plan-research-track span::after { transition: none; } } .plan, .plan-decisions { --plan-gutter: var(--document-gutter); /* * A comfortable measure. The panel is resizable and a plan is read far more * often than it is written, so the text stops widening well before the pane * does. */ --plan-measure: var(--document-prose-measure); /* * Document body text, one step down from the old 17px so prose sits * closer to chrome and chat sizes. Resolved out here because MDXEditor * declares `--text-base`, `--text-sm` * and `--text-xs` on its own root, which sits between `:root` and the * document — so reading the theme token inside the editor gets its 1rem * rather than ours. `.plan` is outside that root, so this catches the * theme's value on the way past. */ --plan-text-xs: var(--text-xs); --plan-text-sm: var(--text-sm); --plan-body: var(--text-base); } /* Document views -------------------------------------------------------- */ .plan-workspace { position: relative; display: flex; flex: 1; min-height: 0; overflow: hidden; } .plan-document { position: relative; container-type: inline-size; flex: 1; min-width: 0; min-height: 0; background: var(--color-page); box-shadow: var(--shadow-raised); } @media (max-width: 600px) { .plan, .plan-decisions { --plan-gutter: var(--document-gutter-compact); } } @container (max-width: 40rem) { /* Anchor state keeps this lane stable when its marker leaves the viewport. */ .plan-document[data-plan-decision-lane] :is(.plan-content, .plan-preface) { --plan-leading-gutter: max( calc(var(--decision-marker-size) + var(--spacing) * 2), var(--plan-gutter) ); } /* Wide authored surfaces keep their own visible, keyboard-operable lane. */ .plan-content :is(pre, table, .plan-code-view, [role="tablist"]) { overscroll-behavior-inline: contain; } .plan-content :is(pre, table, .plan-code-view, [role="tablist"])::-webkit-scrollbar { block-size: 8px; } .plan-content :is(pre, table, .plan-code-view, [role="tablist"])::-webkit-scrollbar-track { background: var(--color-inset); } .plan-content :is(pre, table, .plan-code-view, [role="tablist"])::-webkit-scrollbar-thumb { border: 2px solid var(--color-inset); border-radius: var(--radius-lg); background: var(--color-text-quaternary); } } /* Reader-local comment chrome must not move collaborative line boxes. */ .plan-comment-layer { pointer-events: none; position: absolute; inset: 0; z-index: 2; } .plan-comment-chip, .plan-comment-orphan-button { z-index: 1; pointer-events: auto; border: var(--edge-width) solid var(--color-control-edge); background: var(--color-page); color: var(--color-text-secondary); box-shadow: var(--shadow-resting); } /* The button is the hit area: on a coarse pointer 44px wide and as tall as the first line, inside the document. */ .plan-comment-button { pointer-events: auto; position: absolute; border-radius: var(--radius-md); transition: opacity var(--duration-fast) var(--ease-out); } .plan-comment-chip { position: absolute; display: inline-flex; height: 24px; align-items: center; justify-content: center; gap: 0.125rem; border-radius: var(--radius-md); font-size: var(--text-xs); font-variant-numeric: tabular-nums; line-height: 1; } .plan-comment-button[data-slim] .plan-comment-chip { border-radius: var(--radius-sm); } .plan-comment-button[data-replies] .plan-comment-chip { background: var(--color-brand-wash); color: var(--color-brand-ink); } /* Another thread's card is open: this marker steps back and cannot be pressed. */ .plan-comment-button[data-dimmed] { opacity: 0.4; } /* The host receives hits so prose keeps native selection. */ .plan-comment-hit { pointer-events: none; position: absolute; } .plan-comment-button:hover .plan-comment-chip, .plan-comment-button:focus-visible .plan-comment-chip, .plan-comment-button[aria-expanded="true"] .plan-comment-chip, .plan-comment-orphan-button:hover, .plan-comment-orphan-button:focus-visible { background: var(--color-brand-wash); color: var(--color-brand-ink); } .plan-comment-preview, .plan-comment-card { pointer-events: auto; position: absolute; width: min(20rem, calc(100% - 1.5rem)); } .plan-comment-preview { /* A narrow preview can overlap prose, but never intercept it. */ pointer-events: none; max-width: 18rem; padding: 0.625rem; border-radius: var(--radius-md); background: var(--color-page); box-shadow: var(--shadow-overlay); outline: var(--edge-width) solid var(--color-edge); outline-offset: calc(-1 * var(--edge-width)); font-size: var(--text-xs); color: var(--color-text-primary); } .plan-comment-preview p { margin: 0; } .plan-comment-preview .plan-comment-preview-author { display: flex; } .plan-comment-preview .plan-comment-preview-note { margin-top: 0.125rem; } .plan-comment-preview .plan-comment-preview-replies { display: block; margin-top: 0.25rem; color: var(--color-text-tertiary); } .plan-comment-card { max-height: calc(100% - 1rem); overflow: auto; border-radius: var(--radius-lg); box-shadow: var(--shadow-overlay); --enter-x: 0; --enter-y: 0; animation: plan-comment-enter var(--duration-fast) var(--ease-out); } /* The card enters from its passage: across from the gutter, down from below, up from above. */ .plan-comment-card[data-side="right"] { --enter-x: 0.25rem; transform-origin: top left; } .plan-comment-card[data-side="below"] { --enter-y: -0.25rem; transform-origin: top left; } .plan-comment-card[data-side="above"] { --enter-y: 0.25rem; transform-origin: bottom left; } [data-plan-comment-card] { background: var(--color-page); box-shadow: none; } .plan-comment-close { margin: -0.25rem -0.25rem -0.25rem 0; color: var(--color-text-tertiary); } .plan-comment-back { margin-left: -0.5rem; color: var(--color-text-tertiary); } .plan-comment-group-item { display: flex; width: 100%; flex-direction: column; gap: 0.125rem; padding: 0.375rem 0.5rem; border-radius: var(--radius-md); text-align: left; } .plan-comment-group-item:hover { background: var(--color-hover); } .plan-comment-group-note { display: -webkit-box; overflow: hidden; -webkit-box-orient: vertical; -webkit-line-clamp: 1; } .plan-content [data-plan-comment-card] :is(p, ul, li, blockquote) { margin-block: 0; } .plan-content [data-plan-comment-card] ul { padding-inline-start: calc(1em + 3px); } :root[data-plan-coarse-pointer] .plan-comment-row { min-height: 44px; } .plan-comment-context { padding: 0.5rem 0.625rem; border-radius: var(--radius-md); background: var(--color-inset); } .plan-comment-context-copy { display: -webkit-box; overflow: hidden; -webkit-box-orient: vertical; -webkit-line-clamp: 3; line-height: var(--text-sm--line-height); } .plan-comment-composer[data-inset-send] { border-radius: var(--radius-md); background: var(--color-page); } .plan-comment-composer[data-inset-send] .plan-comment-composer-field { padding-right: 3rem; padding-bottom: 2.75rem; } .plan-comment-confirm { padding: 0.625rem; border-radius: var(--radius-md); background: var(--color-inset); } @keyframes plan-comment-enter { from { opacity: 0; transform: translate(var(--enter-x), var(--enter-y)); } to { opacity: 1; transform: none; } } .motion-comment-preview { opacity: 0; transform: scale(var(--dropdown-pre-scale)); transition: opacity var(--dropdown-open-dur) var(--dropdown-ease), transform var(--dropdown-open-dur) var(--dropdown-ease); will-change: opacity, transform; } .motion-comment-preview.is-open { opacity: 1; transform: none; } @starting-style { .motion-comment-preview.is-open { opacity: 0; transform: scale(var(--dropdown-pre-scale)); } } .motion-comment-preview.is-closing { opacity: 0; transform: scale(var(--dropdown-closing-scale)); transition-duration: var(--dropdown-close-dur); } .motion-comment-preview[data-motion-immediate] { transition: none; } .motion-comment-surface { opacity: 0; transform: scale(var(--dropdown-pre-scale)); transform-origin: top right; transition: opacity var(--dropdown-open-dur) var(--dropdown-ease), transform var(--dropdown-open-dur) var(--dropdown-ease); will-change: opacity, transform; } .motion-comment-surface[data-motion-presentation="sheet"] { transform: translateY(var(--drawer-distance)); transform-origin: bottom center; transition-duration: var(--drawer-open-dur); } .motion-comment-surface.is-open { opacity: 1; transform: none; } .motion-comment-surface.is-closing { pointer-events: none; opacity: 0; transform: scale(var(--dropdown-closing-scale)); transition-duration: var(--dropdown-close-dur); } .motion-comment-surface[data-motion-presentation="sheet"].is-closing { transform: translateY(var(--drawer-distance)); transition-duration: var(--drawer-close-dur); } .plan-comment-orphans { pointer-events: auto; position: absolute; top: 0.5rem; right: 0.5rem; } .plan-comment-orphan-button { border-radius: var(--radius-md); padding: 0.375rem 0.5rem; font-size: var(--text-sm); } .plan-comment-orphan-card { top: 2.5rem; right: 0; } .plan-comment-sheet-backdrop { --backdrop-opacity: 0.24; position: fixed; inset: 0; z-index: 70; background: rgb(var(--shadow-color)); opacity: calc(var(--backdrop-opacity) * (1 - var(--drawer-swipe-progress, 0))); transition: opacity var(--duration-base) var(--ease-out); } .plan-comment-sheet-viewport { pointer-events: none; position: fixed; inset: 0; z-index: 71; display: flex; align-items: flex-end; } .plan-comment-sheet-popup { pointer-events: auto; display: flex; width: 100%; flex-direction: column; height: calc(100dvh - 0.5rem); overflow: hidden; border-radius: var(--radius-xl) var(--radius-xl) 0 0; background: var(--color-page); box-shadow: 0 -1rem 3rem rgb(var(--shadow-color) / 22%); transform: translateY( calc(var(--drawer-snap-point-offset, 100%) + var(--drawer-swipe-movement-y, 0px)) ); transition: transform calc(var(--drawer-swipe-strength, 1) * var(--duration-base)) var(--ease-out); } .plan-comment-sheet-grabber { display: grid; width: 100%; height: 2rem; flex: none; place-items: center; border: 0; background: transparent; color: var(--color-text-tertiary); touch-action: none; } .plan-comment-sheet-grabber span { width: 2.25rem; height: 0.3rem; border-radius: var(--radius-full); background: var(--color-text-tertiary); } /* Sits in the grabber row, so a draft needs no header row; the sheet is touch-only, so 44px. */ .plan-comment-sheet-close { position: absolute; top: 0; right: 0.25rem; width: 2.75rem; height: 2.75rem; margin: 0; } .plan-comment-sheet-title { margin: 0; padding: 0 1rem 0.5rem; color: var(--color-text-primary); font-size: var(--text-sm); font-weight: 600; } .plan-comment-sheet-content { min-height: 0; flex: 1; overflow: auto; padding-right: env(safe-area-inset-right, 0px); padding-bottom: calc(env(safe-area-inset-bottom, 0px) + var(--drawer-keyboard-inset, 0px)); padding-left: env(safe-area-inset-left, 0px); overscroll-behavior: contain; } .plan-comment-sheet-content > [data-plan-comment-card] { border-radius: 0; box-shadow: none; } .plan-comment-sheet-popup[data-starting-style], .plan-comment-sheet-popup[data-ending-style] { transform: translateY(100%); } .plan-comment-sheet-backdrop[data-starting-style], .plan-comment-sheet-backdrop[data-ending-style] { opacity: 0; } @media (prefers-reduced-motion: reduce) { .plan-comment-card { animation: none; } .plan-comment-sheet-backdrop, .plan-comment-sheet-popup { transition-duration: 0ms; } } @media (prefers-reduced-motion: reduce) { .motion-comment-preview, .motion-comment-surface { transition: none; } } .plan-decisions { display: flex; height: 100%; min-width: 0; min-height: 0; flex-direction: column; overflow: hidden; container-type: inline-size; /* The workspace ground shows through the rail. */ background: transparent; } .plan-decisions-content { width: 100%; padding: var(--document-decisions-top-inset) var(--plan-gutter) var(--document-trailing-space); margin-inline: auto; max-width: calc(var(--plan-measure) + var(--plan-gutter) * 2); } .plan-decisions-content--empty { display: flex; width: 100%; max-width: none; align-items: safe center; justify-content: center; padding: var(--document-decisions-top-inset) var(--plan-gutter); } /* Decorator hosts are block-level plan content, not zero-size anchors. */ .plan .plan-content [data-plan-questionnaire], .plan .plan-content [data-plan-decision] { margin-block: 1rem; } /* Keep the one editable caret paragraph between consecutive cards compact. */ .plan .plan-content > :is([data-plan-questionnaire], [data-plan-decision]):has(+ p > br:only-child):has(+ p + :is([data-plan-questionnaire], [data-plan-decision])) { margin-block-end: 0; } .plan .plan-content > :is([data-plan-questionnaire], [data-plan-decision]) + p:has(> br:only-child):has(+ :is([data-plan-questionnaire], [data-plan-decision])) { margin-block: 0; } .plan .plan-content > :is([data-plan-questionnaire], [data-plan-decision]) + p:has(> br:only-child) + :is([data-plan-questionnaire], [data-plan-decision]) { margin-block-start: 0; } /* The empty caret paragraph before the first block takes no room; the block sits at the top padding. */ .plan .plan-content > p:has(> br:only-child):first-child:not(:last-child) { block-size: 0; margin-block: 0; overflow: visible; } .plan .plan-content > p:has(> br:only-child):first-child:not(:last-child) + :is([data-plan-questionnaire], [data-plan-decision]) { margin-block-start: 0; } /* Settled lines read as a tight list; the line's own paragraph margin is dropped. */ .plan .plan-content :is([data-plan-questionnaire], [data-plan-decision]):has([data-card-settled]) { margin-block: 0.5rem; } .plan .plan-content [data-card-settled] { margin-block: 0; } /* The inner card is hidden after resolution, but its Lexical decorator host also has document spacing. Remove that host from layout as well. */ .plan .plan-content [data-plan-questionnaire]:has([data-card-hidden]) { display: none; margin-block: 0; } .plan .plan-content :is([data-plan-questionnaire], [data-plan-decision]) article { width: 100%; } .decision-collapse { display: grid; grid-template-rows: 1fr; transition: grid-template-rows var(--duration-base) var(--ease-out), opacity var(--duration-base) var(--ease-out); } .decision-collapse.is-closing { grid-template-rows: 0fr; opacity: 0; } @media (prefers-reduced-motion: reduce) { .decision-collapse { transition: none; } } /* * The prose a sidecar card refers to, while somebody is pointing at that card. * * Registered in `CSS.highlights`, so it paints over the text without putting * anything in the document. One mark for both halves of the sidecar: questions * used to outline a block while comments washed a range, so the same fact — * this is the prose that card refers to — read as two different things * depending on which card it came from. `::highlight()` cannot draw an outline, * so the wash is what they can both be. * * Nothing is painted at rest. Standing marks for live comments and for decided * prose were tried and taken out: a plan accumulates decisions, so anything * permanent ends up covering most of the document, and a page that is mostly * highlighted has told you nothing. * * `background-color`, not `background`: only a handful of longhands are * permitted here, and the shorthand works today solely because browsers expand * it before filtering. Not worth depending on. * * The colour is recorded beside the declaration because it is calibrated as * a rendered wash, not as an abstract percentage of a darker accent. */ ::highlight(plan-related) { /* #d8eaee */ background-color: var(--color-brand-wash); } /* The passage a link is being added to, while its field holds the selection. */ ::highlight(plan-link-target) { background-color: var(--color-brand-wash); } /* * The same pointer, for a browser with no Highlight API. * * A block outline rather than a wash, because without the API there is no way * to mark part of one — less precise, and honest about it. Standing marks are * dropped entirely on this path: outlining every decided passage would box up * half the plan. */ .plan .plan-content [data-plan-related] { border-radius: var(--radius-sm); outline: 1px solid color-mix(in oklch, var(--color-brand) 60%, transparent); outline-offset: 3px; background: var(--color-brand-wash); } /* * Retheme MDXEditor onto Ace's tokens. * * MDXEditor ships a light Radix palette on its own root and only switches off * it for a `.dark-theme` class it expects the host to add. Ace has its own * theme, so rather than opt into a second one we point its variables at ours — * the editor then follows Ace into light or dark with nothing to keep in sync. * * These have to land on the same element MDXEditor sets them on; an ancestor * would lose to its own declaration. */ .plan .mdxeditor { background: transparent; font-family: var(--font-sans); --text-xs: var(--plan-text-xs); --text-sm: var(--plan-text-sm); --text-base: var(--plan-body); --basePageBg: transparent; --baseBase: var(--color-page); --baseBgSubtle: var(--color-inset); --baseBg: var(--color-hover); --baseBgHover: var(--color-hover); --baseBgActive: var(--color-selected); --baseLine: var(--color-edge); --baseBorder: var(--color-edge); --baseBorderHover: var(--color-brand); --baseSolid: var(--color-text-tertiary); --baseSolidHover: var(--color-text-primary); --baseText: var(--color-text-primary); --baseTextContrast: var(--color-text-primary); --accentBase: var(--color-page); --accentBgSubtle: var(--color-brand-wash); --accentBg: var(--color-brand-wash); --accentBgHover: var(--color-brand-wash); --accentBgActive: var(--color-brand-wash); --accentLine: var(--color-edge); --accentBorder: var(--color-edge); --accentBorderHover: var(--color-brand); --accentSolid: var(--color-brand); --accentSolidHover: var(--color-brand-hover); --accentText: var(--color-brand); --accentTextContrast: var(--color-text-primary); --admonitionTipBg: var(--color-success-wash); --admonitionTipBorder: var(--color-success); --admonitionInfoBg: var(--color-brand-wash); --admonitionInfoBorder: var(--color-brand); --admonitionCautionBg: var(--color-warning-wash); --admonitionCautionBorder: var(--color-warning); --admonitionDangerBg: var(--color-destructive-wash); --admonitionDangerBorder: var(--color-destructive); --admonitionNoteBg: var(--color-hover); --admonitionNoteBorder: var(--color-edge); --error-color: var(--color-destructive); --font-body: var(--font-sans); --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; } /* * Scoped to `.plan` on purpose. MDXEditor sets padding and colour directly * on its own content class, which is exactly as specific as ours, so a bare * `.plan-content` wins only by landing later in the bundle. That is a * property of how the CSS happens to be chunked, not of anything we control. */ .plan .plan-content { --plan-leading-gutter: var(--plan-gutter); --plan-wide-inline-size: calc(100cqi - var(--plan-leading-gutter) - var(--plan-gutter)); --plan-wide-inline-offset: calc((100% - var(--plan-wide-inline-size)) / 2); /* * The trailing space is deliberate and large: the caret should never sit * against the bottom of the pane, and a plan is usually appended to. */ padding: var(--document-top-inset) var(--plan-gutter) var(--document-trailing-space); padding-inline-start: var(--plan-leading-gutter); margin-inline: auto; max-width: calc(var(--plan-measure) + var(--plan-gutter) * 2); font-size: var(--plan-body); /* Prose wants looser leading than the UI rhythm the token pairs with. */ line-height: var(--document-line-height); color: var(--color-text-primary); } /* Host context above the document shares the prose column and pulls the title up under it. */ .plan .plan-preface { --plan-leading-gutter: var(--plan-gutter); padding: var(--document-top-inset) var(--plan-gutter) 0; padding-inline-start: var(--plan-leading-gutter); margin-inline: auto; max-width: calc(var(--plan-measure) + var(--plan-gutter) * 2); } /* * MDXEditor gives its placeholder the content class too. Pin its absolute box * across the wrapper so auto margins center it like the editable document; * without both insets it shrink-wraps left of the caret on a wide canvas. */ .plan .mdxeditor-root-contenteditable > .plan-content { inset-inline: 0; color: var(--color-text-quaternary); } :root[data-plan-coarse-pointer] .plan-menu-row { height: 44px; min-height: 44px; } :root[data-plan-coarse-pointer] .plan-menu-cell { width: 44px; height: 44px; min-width: 44px; } /* The bubble's block trigger carries a label, and its seams cost width a phone lacks. */ :root[data-plan-coarse-pointer] .plan-bubble-trigger { width: auto; padding-inline: 0.5rem 0.25rem; } /* * Document typography. * * Tailwind's preflight resets headings to inherit their size and weight and * strips list markers, which is right for an app chrome and wrong for prose. * A plan is prose, so the document elements are given their meaning back here. */ .plan-content :is(h1, h2, h3, h4, h5, h6) { line-height: 1.3; margin-block: 1.5em 0.5em; color: var(--color-text-primary); /* Short enough to weigh every break at once, and an 8/1 split reads as a mistake. */ text-wrap: balance; } .plan-content :is(h1, h2, h3, h4) { font-family: var(--font-document-heading); font-weight: var(--font-weight-document-heading); } .plan-content :is(h5, h6) { font-weight: var(--font-weight-semibold); } .plan-content :is(h1, h2, h3, h4, h5, h6):first-child { margin-block-start: 0; } /* * The first visible block starts at the column's top padding. Decision cards keep * a caret paragraph before them, and a resolved card's host is display: none, so * neither may push a leading heading down. A heading with only those before it * gets no top margin; the second rule gives it back once real content precedes it. */ .plan-content > :is(p:has(> br:only-child), [data-plan-questionnaire]:has([data-card-hidden])) ~ :is(h1, h2, h3, h4, h5, h6) { margin-block-start: 0; } .plan-content > :not(:is(p:has(> br:only-child), [data-plan-questionnaire]:has([data-card-hidden]))) ~ :is(h1, h2, h3, h4, h5, h6) { margin-block-start: 1.5em; } .plan-content h1 { font-size: var(--text-2xl); line-height: var(--text-2xl--line-height); } .plan-content h2 { font-size: var(--text-xl); line-height: var(--text-xl--line-height); } .plan-content h3 { font-size: var(--text-lg); line-height: var(--text-lg--line-height); } .plan-content :is(h4, h5, h6) { font-size: var(--plan-body); } /* iOS Safari zooms the page when it focuses editable text under 16px. */ @media (pointer: coarse) { .plan .plan-content, .plan-content :is(h4, h5, h6) { font-size: max(1rem, var(--plan-body)); } } .plan-content p { margin-block: 0.75em; } .plan-content :is(ul, ol) { margin-block: 0.75em; padding-inline-start: 1.5em; } .plan-content ul { list-style: disc; } .plan-content ol { list-style: decimal; } .plan-content li { margin-block: 0.25em; } /* Nested lists sit tighter than the block they interrupt. */ .plan-content li > :is(ul, ol) { margin-block: 0.25em; } .plan-content blockquote { margin-block: 1em; padding-inline-start: 1em; border-inline-start: 3px solid var(--color-edge); color: var(--color-text-secondary); } .plan-content a { color: var(--color-brand); text-decoration: underline; text-underline-offset: 2px; overflow-wrap: anywhere; } /* Prose has no scrollbar of its own, so an authored token must be allowed to wrap. */ .plan-content :is(p, li, blockquote, h1, h2, h3, h4, h5, h6) { overflow-wrap: anywhere; } .plan-content hr { margin-block: 1.5em; border: 0; border-block-start: var(--edge-width) solid var(--color-edge); } .plan-content :is(strong, b) { font-weight: 600; color: var(--color-text-primary); } /* Structural components ------------------------------------------------- */ .plan-content pre { background: var(--color-inset); border-radius: var(--radius-md); padding: 0.75rem 1rem; overflow-x: auto; font-family: ui-monospace, SFMono-Regular, monospace; font-size: var(--text-sm); } /* Inline code follows its parent heading or prose; block code has its own role. */ .plan-content :not(pre) > code { background: var(--color-inset); color: var(--color-text-primary); border-radius: var(--radius-sm); padding: 0.125em 0.3em; font-family: ui-monospace, SFMono-Regular, monospace; font-size: inherit; } /* * Tables scroll independently so a wide one never widens the document. * * `display: block` turns the table itself into the scrollport. Its inline size * is bounded by the document gutter, while the cells' minimum widths preserve * usable columns and create overflow inside that scrollport when necessary. */ .plan-content table { display: block; inline-size: 100%; max-inline-size: 100%; overflow-x: auto; border-collapse: collapse; margin-block: 1em; } .plan-content th, .plan-content td { border: var(--edge-width) solid var(--color-edge); padding: 0.375rem 0.625rem; text-align: start; /* Without this a cell collapses onto its neighbour when a column is narrow, and two values read as one word. */ vertical-align: top; min-inline-size: 7rem; /* A table is deliberately allowed to scroll as a whole; breaking each unbroken identifier into syllables makes its columns unreadable. */ word-break: normal; overflow-wrap: normal; } .plan-content th { font-weight: 600; background: var(--color-hover); color: var(--color-text-primary); } /* A cell's paragraph is the cell; it should not add document spacing. */ .plan-content :is(th, td) > p { margin-block: 0; word-break: normal; overflow-wrap: normal; } /* Images and rendered diagrams are readings of the document, never its width. */ .plan-content :is(img, svg) { max-inline-size: 100%; block-size: auto; } /* Visual top-level blocks can use the document beyond the prose measure. */ .plan-content > :is(p:has(> [data-plan-src]:first-child + br[data-lexical-managed-linebreak]:last-child), [data-plan-language="mermaid"], [data-plan-language="seecode"]) { inline-size: var(--plan-wide-inline-size); max-inline-size: var(--plan-wide-inline-size); margin-inline: var(--plan-wide-inline-offset); } /* * A compact table shrink-wraps around its columns from the prose's start edge, * like every other block; a wide one grows to the trailing document gutter * before scrolling. The row and column rails are fixed overlays and sit in * the gutter outside it. */ .plan-content > table { inline-size: max-content; max-inline-size: calc((100% + var(--plan-wide-inline-size)) / 2); } /* Name an empty header for what it holds. Counted per header cell, out of flow. */ .plan-content table tr:first-of-type { counter-reset: plan-column; } .plan-content table tr:first-of-type > th { counter-increment: plan-column; } .plan-content[contenteditable="true"] th > p:only-child:has(> br:only-child) { position: relative; } .plan-content[contenteditable="true"] th > p:only-child:has(> br:only-child)::before { content: "Column " counter(plan-column); position: absolute; inset-inline-start: 0; color: var(--color-text-quaternary); font-weight: 500; pointer-events: none; } /* The paragraph owns the wide row; the image keeps its intrinsic size inside it. */ .plan-content > p:has(> [data-plan-src]:first-child + br[data-lexical-managed-linebreak]:last-child) { display: grid; place-items: center; } /* * An image that could not load. In a sentence it is a chip like inline code, * so the sentence still reads; alone in its paragraph it is a quiet frame at * the prose width rather than the wide row a loaded image would use. */ .plan-content .plan-image-missing { display: inline-flex; align-items: center; gap: 0.3em; max-inline-size: 100%; vertical-align: bottom; padding: 0.125em 0.3em; border-radius: var(--radius-sm); background: var(--color-inset); color: var(--color-text-secondary); } .plan-content .plan-image-missing-label { overflow: hidden; min-inline-size: 0; text-overflow: ellipsis; white-space: nowrap; } .plan-content p:has(> [data-plan-src]:first-child + br[data-lexical-managed-linebreak]:last-child):has(.plan-image-missing) { display: block; /* Block layout honours `justify-items` too, which would still centre the frame. */ place-items: normal; inline-size: auto; max-inline-size: 100%; margin-inline: 0; } .plan-content p:has(> [data-plan-src]:first-child + br[data-lexical-managed-linebreak]:last-child) > [data-plan-src]:has(> .plan-image-missing) { display: block; } /* The managed break after a block frame would add an empty line beneath it. */ .plan-content p:has(> [data-plan-src]:first-child > .plan-image-missing) > br[data-lexical-managed-linebreak]:last-child { display: none; } .plan-content p:has(> [data-plan-src]:first-child + br[data-lexical-managed-linebreak]:last-child) .plan-image-missing { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: calc(var(--spacing) * 2); block-size: calc(var(--spacing) * 24); padding-inline: calc(var(--spacing) * 4); border: var(--edge-width) solid var(--color-edge); border-radius: var(--radius-md); color: var(--color-text-tertiary); font-size: var(--plan-text-xs); } .plan-content p:has(> [data-plan-src]:first-child + br[data-lexical-managed-linebreak]:last-child) .plan-image-missing-label { max-inline-size: 100%; } .plan-content :is([data-plan-type], [data-plan-panels], .planMath) { min-inline-size: 0; max-inline-size: 100%; } .plan-content .planMath [data-plan-preview] { max-inline-size: 100%; overflow-x: auto; } /* * A selected range of cells. * * `@lexical/table` marks one by adding the class the theme names under * `tableCellSelected` and does nothing else at all — no inline style, no * fallback. MDXEditor's theme names no table class of any kind, so before * `plan-editor.tsx` added these the selection was live and completely * invisible: dragging across cells appeared to do nothing while a * `TableSelection` was very much in effect, and the next keystroke replaced * everything it covered. */ .plan .plan-content .plan-cell-selected { background: var(--color-selected); } /* Set on the table while a cell range is being dragged out, so the browser's own text selection does not paint underneath ours. */ .plan .plan-content .plan-table-selecting, .plan .plan-content .disable-selection { user-select: none; } /* Table rails ------------------------------------------------------------ */ /* * The grips a table is reshaped from. * * Positioned by script from measured cell rectangles, because a `` * cannot contain a `
` and there is nowhere inside the node to put them. * So: `position: fixed`, and no Tailwind translate utility anywhere near them * — v4 emits the `translate` property, which composes with a transform rather * than replacing it. */ /* * The rail itself lets the pointer through; only its controls take it. * * Its button lane reaches past the gap above a table into the block above, * which is usually the paragraph somebody wants to click next. The grip lane * is unbroken and lies against the table, and the buttons a pointer heads for * sit beside the grip it came from, so crossing from table to control never * passes through a hole that would end the hover. */ .plan-rail { position: fixed; z-index: 40; overflow: hidden; pointer-events: none; animation: plan-rail-in var(--duration-fast) var(--ease-out); } @keyframes plan-rail-in { from { opacity: 0; } } .plan-rail > * { position: absolute; padding: 0; border: 0; background: transparent; } /* * Only the grip of the track being pointed at, or holding the caret, is drawn. * The rest wait at nothing, or faintly while the caret is in the table, so a * keyboard user can still see where every row and column can be taken from. */ .plan-grip { pointer-events: auto; opacity: 0; border-radius: var(--radius-sm); background: var(--color-hover); color: var(--color-text-quaternary); transition: opacity var(--duration-fast) var(--ease-out), background-color var(--duration-fast) var(--ease-out), color var(--duration-fast) var(--ease-out); } .plan-rail[data-plan-caret] .plan-grip { opacity: 0.35; } .plan-rail .plan-grip[data-plan-hot], .plan-rail .plan-grip:focus-visible, .plan-rail .plan-grip.is-held { opacity: 1; } .plan-grip:hover, .plan-grip:focus-visible { background: var(--color-selected); color: var(--color-text-secondary); } /* Six dots, the mark for "this can be dragged". Drawn rather than an icon so the dots stay on whole pixels at any track length. */ .plan-grip::before { content: ""; position: absolute; top: 50%; left: 50%; width: calc(var(--spacing) * 2); height: calc(var(--spacing) * 3); translate: -50% -50%; /* Stops are fractions of the tile's half-diagonal: a dot of about a pixel. */ background-image: radial-gradient(circle, currentcolor 35%, transparent 45%); background-size: var(--spacing) var(--spacing); } [data-plan-rail="column"] .plan-grip::before { width: calc(var(--spacing) * 3); height: calc(var(--spacing) * 2); } [data-plan-rail="column"] .plan-grip { cursor: grab; } [data-plan-rail="row"] .plan-grip { cursor: grab; } .plan-grip.is-held { cursor: grabbing; background: var(--color-brand); color: var(--color-page); } /* The header row's place in the rail. Not a control: the header cannot move and cannot be removed, so nothing is drawn there. */ .plan-rail .plan-grip.is-fixed { opacity: 0; cursor: default; } /* * Remove and insert, in the lane beyond the grips. * * Nothing here overlaps a grip, so a drag can begin anywhere along one. Hiding * is `opacity` rather than `display`, so a button keeps its size and its place * in the tab order while it is out of sight — and `pointer-events` with it, so * an invisible cross cannot be clicked by accident. */ /* * Each button's box is its whole target; the ghost drawn inside it is smaller, * by its padding, so a 24-pixel target reads as a 20-pixel control. */ .plan-grip-remove, .plan-insert, .plan-align { opacity: 0; pointer-events: none; padding: calc(var(--spacing) / 2); border-radius: var(--radius-sm); background-clip: content-box; color: var(--color-text-tertiary); transition: opacity var(--duration-fast) var(--ease-out), background-color var(--duration-fast) var(--ease-out), color var(--duration-fast) var(--ease-out); } /* An insert shows only while its own seam is aimed at, so beside the track being pointed at it takes the pointer while still invisible. It is later in the rail than a remove button, so where the two meet on a short row the insert wins, and the remove hides while an insert is aimed. */ .plan-insert[data-plan-live] { pointer-events: auto; } .plan-grip-remove[data-plan-shown], .plan-align[data-plan-shown], .plan-grip-remove:focus-visible, .plan-align:focus-visible, .plan-insert:hover, .plan-insert:focus-visible { opacity: 1; pointer-events: auto; } .plan-grip-remove:hover, .plan-insert:hover, .plan-insert:focus-visible, .plan-align:hover { background-color: var(--color-hover); color: var(--color-text-primary); } /* Drawn rather than lettered: at fifteen pixels a glyph is a smudge, and these have to read at a glance in the margin of a paragraph. */ .plan-grip-remove::before, .plan-grip-remove::after, .plan-insert::before, .plan-insert::after { content: ""; position: absolute; inset: 50% 30%; border-top: 1.5px solid currentcolor; translate: 0 -50%; } .plan-grip-remove::before { rotate: 45deg; } .plan-grip-remove::after { rotate: -45deg; } .plan-insert::after { rotate: 90deg; } /* * The alignment button reads as what it does: two lines, the lower one short * and sitting where the text will. A column with no alignment of its own shows * the same figure at half strength rather than a fourth icon, because "no * alignment" and "left" render identically in the table and a control claiming * otherwise would be lying about the source. */ .plan-align::before, .plan-align::after { content: ""; position: absolute; left: 7px; right: 7px; border-top: 1.5px solid currentcolor; } .plan-align::before { top: 9px; } .plan-align::after { top: 13px; } .plan-align[data-plan-align="default"] { opacity: 0; } .plan-align[data-plan-align="default"][data-plan-shown], .plan-align[data-plan-align="default"]:focus-visible { opacity: 0.55; } .plan-align[data-plan-align="default"]::after, .plan-align[data-plan-align="left"]::after { right: 11px; } .plan-align[data-plan-align="center"]::after { left: 9px; right: 9px; } .plan-align[data-plan-align="right"]::after { left: 11px; } /* Where the drag will land. A hairline, because it is a position rather than a region: anything wider reads as the width of what is being moved. */ .plan-drop { background: var(--color-brand); border-radius: 1px; pointer-events: none; } /* Where an aimed-at insert will put the new row or column, across the table. */ .plan-seam-line { position: fixed; z-index: 40; background: var(--color-brand); pointer-events: none; animation: plan-rail-in var(--duration-fast) var(--ease-out); } .plan-table-action-toolbar { position: fixed; z-index: 50; display: block; max-width: calc(100vw - 1rem); padding: 0.25rem; border-radius: var(--radius-lg); background: var(--color-page); box-shadow: var(--shadow-overlay); } .plan-table-action-groups, .plan-table-action-panel { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 0.25rem; } .plan-table-action-panel { grid-template-columns: repeat(2, minmax(0, 1fr)); padding-block-start: 0.25rem; border-block-start: var(--edge-width) solid var(--color-edge); } .plan-table-action { width: 100%; min-width: 0; min-height: 44px; padding: 0.375rem 0.5rem; border-radius: var(--radius-md); color: var(--color-text-secondary); font-size: var(--text-sm); line-height: 1.15; } .plan-table-action[aria-expanded="true"] { background: var(--color-selected); color: var(--color-text-primary); } .plan-table-action:hover, .plan-table-action:focus-visible { background: var(--color-hover); color: var(--color-text-primary); } .plan-table-action:disabled { opacity: 0.45; } @media (prefers-reduced-motion: reduce) { .plan-rail, .plan-seam-line { animation: none; } .plan-grip, .plan-grip-remove, .plan-insert, .plan-align { transition: none; } } /* Remote cursors --------------------------------------------------------- */ /* * The frame remote cursors are measured against. It wraps the prose and grows * with it, so carets scroll with the text they belong to. */ .plan .plan-frame { position: relative; } /* * Lexical owns the caret's geometry — position, height and offsets are written * inline on every paint — and hands us the peer's colour as a custom property. * Only appearance belongs here. The shadow widens a 1px caret that would * otherwise be easy to miss, without fighting the inline width. */ .plan .plan-cursor { background-color: var(--lexical-cursor-color, currentColor); box-shadow: 0 0 0 0.5px var(--lexical-cursor-color, currentColor); border-radius: 1px; } .plan .plan-cursor-name { position: absolute; inset-block-end: 100%; inset-inline-start: -1px; padding: 0 0.25rem; border-radius: var(--radius-sm); background: var(--lexical-cursor-color, currentColor); color: var(--color-page); font-size: var(--text-xs); line-height: 1rem; font-weight: 500; white-space: nowrap; max-inline-size: min(20rem, calc(100cqi - 2rem)); overflow: hidden; text-overflow: ellipsis; pointer-events: none; opacity: 0; transition: opacity var(--duration-base) var(--ease-out); } /* * The agent's caret is left at the end of the block it last wrote, so the line * above it is prose the reader has just been shown. Beside the caret, on its * own line, the label covers nothing. */ .plan .plan-cursor[data-plan-agent] .plan-cursor-name { inset-block-end: auto; inset-block-start: 50%; inset-inline-start: calc(100% + 0.25rem); translate: 0 -50%; } /* Raised while its peer is moving or typing, or while it is pointed at, then left to fade. */ .plan .plan-cursor[data-plan-active] .plan-cursor-name, .plan .plan-cursor[data-plan-hover] .plan-cursor-name { opacity: 1; } /* Beside the local caret, the label drops under its own line instead of covering the line the local user is writing on. */ .plan .plan-cursor[data-plan-below] .plan-cursor-name { inset-block-end: auto; inset-block-start: 100%; } /* * Where selections fall back to rect overlays, because the browser has no * Highlight API. The supported path is styled by Lexical from the same colour. */ .plan .plan-cursor-selection-bg { background-color: color-mix(in srgb, var(--lexical-cursor-color, currentColor) 30%, transparent); } @media (prefers-reduced-motion: reduce) { .plan * { transition: none !important; animation: none !important; } } /* Rendered previews ------------------------------------------------------ */ .plan-content [data-plan-preview]:empty { display: none; } /* * On the message rather than on the slot: everything in the slot is React's * now, including this, so the attribute goes on the element that says so * rather than on the element that holds it. */ .plan-content [data-plan-preview] [data-plan-error] { color: var(--color-destructive); font-size: var(--text-sm); padding: 0.5rem 0; } /* * Coloured code, and diffs. * * The renderer paints inside a shadow root, so none of the rules above reach * it and none of its reach out. What crosses is inherited: the custom * properties it reads for type, which are set here so a fence and its source * are the same size in the same face, and nothing else. */ .plan-content .plan-code-view { --diffs-font-family: ui-monospace, SFMono-Regular, monospace; --diffs-font-size: var(--text-sm); --diffs-line-height: 1.5; --diffs-tab-size: 2; --diffs-header-font-family: var(--font-sans); max-inline-size: 100%; overflow-x: auto; /* The renderer raises its header; keep that inside, under the block's controls. */ isolation: isolate; } /* The renderer's shadow root owns syntax painting; this host supplies the * readable width that the surrounding widget scrolls when a line exceeds it. */ .plan-content .plan-code-view > diffs-container { min-inline-size: max-content; } /* Each file in a patch is its own box, and they stack. */ .plan-content .plan-code-view diffs-container + diffs-container { display: block; margin-block-start: 0.5rem; } /* Source stays visible and editable beneath its preview. */ .plan-content [data-plan-source] { white-space: pre-wrap; overflow-wrap: normal; font-family: ui-monospace, SFMono-Regular, monospace; font-size: var(--text-sm); } /* * A general sibling, not an adjacent one: the block's chrome sits between the * preview and the source, and a block has exactly one source anyway. */ .plan-content [data-plan-preview]:not(:empty) ~ [data-plan-source] { opacity: 0.6; font-size: var(--text-sm); } /* * A block formula is an editable region exactly like a code block, and has to * read as one: the rendered result on the page, its source in a box below. * * A code block gets that box from the generic `pre` rule, and a formula renders * the same structure into plain `div`s — so without this it is a bare line of * monospace with no background, no padding and nothing in it, and inserting one * looks like the command did nothing at all. The box belongs to the source * alone; wrapping the preview too would put the rendered formula in a grey * well, which is not what a diagram does. */ .plan-content .planMath[data-plan-inline="false"] [data-plan-source] { --ace-formula-inset-block: 0.75rem; --ace-formula-inset-inline: 1rem; /* * No `display` here. A `div` is already block, so declaring it again buys * nothing — and this selector outranks the rule that hides the source when * the block is collapsed, which would keep a formula on screen forever. */ position: relative; background: var(--color-inset); border-radius: var(--radius-md); padding: var(--ace-formula-inset-block) var(--ace-formula-inset-inline); overflow-x: auto; } /* * A formula has no header row, so its show-source toggle sits in the block's * top-end corner instead of on a line of its own below the result. The preview's * scrollport stops short of it on both sides, so a long formula scrolls without * passing underneath and a centred one stays centred. */ .plan-content .planMath[data-plan-inline="false"] { position: relative; } .plan-content .planMath[data-plan-inline="false"] [data-plan-preview]:not(:empty) { margin-inline: calc(var(--spacing) * 8); } .plan-content .planMath[data-plan-inline="false"] [data-plan-chrome="block"] { position: absolute; inset-block-start: 0; inset-inline-end: 0; z-index: 1; } /* * Say what belongs in an empty one. Lexical fills an empty block with a managed * line break, so `:empty` never matches and the placeholder keys off that * instead. It sits out of flow so it cannot displace the caret in front of it, * which means it has to repeat the padding the caret is already inside. */ .plan-content .planMath[data-plan-inline="false"] [data-plan-source]:has(> br[data-lexical-managed-linebreak]:only-child)::before { content: "LaTeX, e.g. E = mc^2"; position: absolute; inset-block-start: var(--ace-formula-inset-block); inset-inline-start: var(--ace-formula-inset-inline); color: var(--color-text-quaternary); pointer-events: none; } /* * Showing a result without its source. * * Not the `hidden` attribute: a formula's source sets `display: block`, which * outranks the user agent's rule for `hidden` and would leave it on screen. */ .plan-content [data-plan-collapsed] [data-plan-source] { display: none; } .plan-content [data-plan-preview] svg { max-width: 100%; height: auto; } /* A SeeCode diagram keeps its rendered width and scrolls inside its own stage. */ .plan-content [data-plan-preview] .plan-seecode .sc-svg { max-width: none; max-inline-size: none; } /* * A diagram a little wider than the measure shrinks to fit; one that would * have to shrink past legibility keeps its drawn size and scrolls, with the * hidden side faded so the cut reads as "more this way" rather than a crop. */ .plan-content .plan-diagram { --ace-diagram-fade: calc(var(--spacing) * 8); max-inline-size: 100%; overflow-x: auto; overflow-y: hidden; overscroll-behavior-inline: contain; transition: opacity var(--duration-base) var(--ease-out); } .plan-content .plan-diagram svg { display: block; max-width: none; margin-inline: auto; } .plan-content .plan-diagram[data-fit] svg { max-width: 100%; } .plan-content .plan-diagram[data-more="end"] { mask-image: linear-gradient(to right, black calc(100% - var(--ace-diagram-fade)), transparent); } .plan-content .plan-diagram[data-more="start"] { mask-image: linear-gradient(to right, transparent, black var(--ace-diagram-fade)); } .plan-content .plan-diagram[data-more="both"] { mask-image: linear-gradient( to right, transparent, black var(--ace-diagram-fade), black calc(100% - var(--ace-diagram-fade)), transparent ); } /* The last drawing that worked, kept while its source is mid-edit. */ .plan-content .plan-diagram[data-stale] { opacity: 0.4; } /* * A code block is one box: header, rendered code, source. A border rather than * an outline, because the renderer paints over an inset outline. */ .plan-content .planCode { position: relative; display: flex; flex-direction: column; margin-block: 1em; } .plan-content .planCode:not([data-plan-presentation="diagram"]) { overflow: hidden; border: var(--edge-width) solid var(--color-edge); border-radius: var(--radius-lg); background: var(--color-page); } /* * A block's controls float in its top-end corner rather than on a bar of their * own, the same place a formula's toggle sits. At rest a code block shows only * its language, as quiet text; the menu and the source toggle arrive on hover * or focus. The fill fades in from the start so a long first line runs under * the label instead of colliding with it. */ .plan-content .planCode [data-plan-chrome="block"] { position: absolute; inset-block-start: 0; inset-inline-end: 0; z-index: 1; max-inline-size: 100%; } /* A long title is the one thing that gives way, from its end. */ .plan-content .planCode .plan-code-chrome { max-inline-size: 100%; & > * { flex-shrink: 0; } & > .plan-code-title { flex-shrink: 1; } } /* Room for the controls, so they never sit on a drawing or a diff's counts. */ .plan-content .planCode .plan-diagram { padding-block-start: calc(var(--spacing) * 8); } .plan-content .plan-diff-chrome-room { display: block; inline-size: calc(var(--spacing) * 26); } .plan-content .planCode .plan-code-chrome { position: relative; padding: calc(var(--spacing) * 1.5); background-color: var(--color-page); } .plan-content .planCode .plan-code-chrome::before { content: ""; position: absolute; inset-block: 0; inset-inline-end: 100%; inline-size: calc(var(--spacing) * 6); background: linear-gradient(to right, transparent, var(--color-page)); pointer-events: none; } .plan-content .plan-code-chrome :is(.plan-code-language, .plan-code-language-label) { font-size: var(--text-xs); } .plan-content .plan-code-chrome, .plan-content .plan-code-chrome .plan-code-toggle, .plan-content .plan-code-chrome .plan-code-language [data-nucleo-icon] { transition: opacity var(--duration-fast) var(--ease-out); } @media (prefers-reduced-motion: reduce) { .plan-content .plan-code-chrome, .plan-content .plan-code-chrome .plan-code-toggle, .plan-content .plan-code-chrome .plan-code-language [data-nucleo-icon], .plan-content .plan-diagram { transition: none; } } /* * Without hover the controls never leave, so on touch they get a band of their * own above the content instead of sitting on its first line. */ @media not ((hover: hover) and (pointer: fine)) { .plan-content .planCode:not([data-plan-presentation="diagram"]) { padding-block-start: calc(var(--spacing) * 9); } .plan-content .planCode .plan-diagram { padding-block-start: 0; } .plan-content .planCode .plan-code-chrome { background: none; } .plan-content .plan-diff-chrome-room { display: none; } } /* Revealed on hover or focus where a pointer can hover; always shown on touch. */ @media (hover: hover) and (pointer: fine) { :is(.plan-content .planCode, .plan-content .planMath[data-plan-inline="false"]):not(:hover, :focus-within, :has(.plan-code-chrome [aria-expanded="true"]), :has(.plan-code-chrome[data-editing])) { /* Only a named language says something the rendering does not. */ & .plan-code-chrome:not([data-kind="code"]), & .plan-code-toggle, & .plan-code-language [data-nucleo-icon] { opacity: 0; } /* * The hidden toggle keeps its place, so at rest only the label carries * a fill, reaching to the block's edge; code runs on under the empty * slot beside it. */ & .plan-code-chrome { background: none; } & .plan-code-chrome::before { content: none; } & .plan-code-chrome :is(.plan-code-language, .plan-code-language-label) { position: relative; background-color: var(--color-page); &::before { content: ""; position: absolute; inset-block: 0; inset-inline-end: 100%; inline-size: calc(var(--spacing) * 6); background: linear-gradient(to right, transparent, var(--color-page)); pointer-events: none; } &::after { content: ""; position: absolute; inset-block: 0; inset-inline-start: 100%; inline-size: calc(var(--spacing) * 1.5); background-color: var(--color-page); } } } } .plan-content .plan-code-toggle[aria-expanded="true"] { background: var(--color-brand-wash); color: var(--color-brand); } /* A global icon colour rule would otherwise keep the icon grey. */ .plan-content .plan-code-toggle[aria-expanded="true"] [data-nucleo-icon] { color: inherit; } .plan-content .plan-code-language-label { padding-inline: 0.5rem; font-size: var(--text-xs); color: var(--color-text-tertiary); } .plan-content .planCode .plan-code-view { border-radius: 0; } /* * One left edge for every line of text in the block: the language label, * rendered code, and source all start 0.75rem in. The renderer pads each line * by 1ch inside its shadow root, so its wrapper supplies the remainder in the * same monospace face. Diffs keep their gutter, which is its own column. */ .plan-content .planCode .plan-code-view[data-view="file"] { /* The renderer adds 8px above its code and 8px below; make both 0.75rem. */ padding-block: calc(0.75rem - 8px); padding-inline-start: calc(0.75rem - 1ch); font-family: ui-monospace, SFMono-Regular, monospace; font-size: var(--text-sm); } /* * Editable code and formula source also stays at 16px on touch, so iOS does * not zoom. The rendered code follows so a fence and its source still match. */ @media (pointer: coarse) { .plan-content :is(pre, [data-plan-source]), .plan-content [data-plan-preview]:not(:empty) ~ [data-plan-source], .plan-content .planCode .plan-code-view[data-view="file"] { font-size: max(1rem, var(--text-sm)); } .plan-content .plan-code-view { --diffs-font-size: max(1rem, var(--text-sm)); } } .plan-content .planCode [data-plan-source] { margin: 0; padding-inline: 0.75rem; border-radius: 0; background: var(--color-page); } /* Like a formula's: say what belongs in an empty fence. See that rule for the line break. */ .plan-content .planCode [data-plan-source] { position: relative; } .plan-content .planCode [data-plan-source]:has(> br[data-lexical-managed-linebreak]:only-child)::before { content: "Code"; position: absolute; inset-block-start: 0.75rem; inset-inline-start: 0.75rem; color: var(--color-text-quaternary); pointer-events: none; } .plan-content .planCode[data-plan-language="diff"] [data-plan-source]:has(> br[data-lexical-managed-linebreak]:only-child)::before { content: "Paste a unified diff"; } .plan-content .planCode[data-plan-language="mermaid"] [data-plan-source]:has(> br[data-lexical-managed-linebreak]:only-child)::before { content: "Mermaid, e.g. flowchart TD; A-->B"; } .plan-content .planCode [data-plan-preview]:not(:empty) ~ [data-plan-source] { border-block-start: var(--edge-width) solid var(--color-edge); background: var(--color-inset); } /* * Editing code or a diff swaps the coloured copy for its source in the same * box, so a line is never on screen twice. The source is the text now, not a * footnote to it. */ .plan-content .planCode[data-plan-swapped] [data-plan-preview] { display: none; } .plan-content .planCode[data-plan-swapped] [data-plan-preview]:not(:empty) ~ [data-plan-source] { opacity: 1; border-block-start: 0; background: var(--color-page); } @media (prefers-reduced-motion: no-preference) { .plan-content .planCode[data-plan-swapped] [data-plan-source] { animation: plan-source-in var(--duration-fast) var(--ease-out); } } @keyframes plan-source-in { from { opacity: 0; } } .plan-content .planCode [data-plan-preview] :is(.plan-diagram, [data-plan-error]) { padding-inline: 0.75rem; } /* * While its source is open, a drawn diagram keeps the error line's room, so a * mistake arriving after a pause does not push the source down under the caret. */ .plan-content .planCode[data-plan-language="mermaid"]:not([data-plan-collapsed]) [data-plan-preview]:has(> .plan-diagram):not(:has(> [data-plan-error])) { padding-block-end: calc(var(--spacing) * 20); } .plan-content .planCode[data-plan-language="mermaid"] [data-plan-preview] > [data-plan-error] { min-block-size: calc(var(--spacing) * 20); } /* * Diagram error: a quiet note under the last drawing that worked, not a panel. * It only arrives once the author pauses, so it reads as a hint, not an alarm. */ .plan-content .planCode [data-plan-preview] [data-plan-error]:has(.plan-error-badge) { display: flex; align-items: flex-start; gap: calc(var(--spacing) * 2); padding-block: calc(var(--spacing) * 2.5); } .plan-content .plan-error-badge { display: grid; flex-shrink: 0; place-items: center; height: var(--text-sm--line-height); color: var(--color-destructive); } .plan-content .plan-error-badge [data-nucleo-icon] { color: inherit; } .plan-content .plan-error-text { min-width: 0; } .plan-content .plan-error-title { font-size: var(--text-sm); font-weight: 500; color: var(--color-destructive-ink); } .plan-content .plan-error-message { display: inline; margin: 0; font-size: var(--text-sm); color: var(--color-text-tertiary); overflow-wrap: anywhere; line-height: var(--text-sm--line-height); } .plan-content .plan-error-message::before { content: "·"; margin-inline: 0.375rem; color: var(--color-text-quaternary); } .plan-content .plan-error-detail { margin: calc(var(--spacing) * 1) 0 0; padding: 0; overflow-x: auto; background: transparent; font-size: var(--text-xs); line-height: var(--text-xs--line-height); color: var(--color-text-tertiary); } .plan-content .plan-code-title { min-width: 0; max-inline-size: 16rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: var(--text-xs); color: var(--color-text-secondary); } .plan-content .plan-code-title::after { content: "·"; margin-inline: 0.375rem 0.125rem; color: var(--color-text-quaternary); } /* Tabs ------------------------------------------------------------------- */ .plan-content [data-plan-chrome="tabs"] { margin-block-end: 0.5rem; } .plan-content .plan-tab-strip { display: flex; align-items: center; gap: calc(var(--spacing) * 1); padding-block-end: calc(var(--spacing) * 1); border-block-end: var(--edge-width) solid var(--color-edge); } .plan-content .plan-tab { --plan-tab-surface: var(--color-page); position: relative; display: flex; flex-shrink: 0; /* Never wider than the strip, so a selected tab can always be shown whole. */ max-inline-size: 100%; border-radius: var(--radius-md); background: var(--plan-tab-surface); color: var(--color-text-quaternary); transition: color var(--duration-fast) var(--ease-out), background-color var(--duration-fast) var(--ease-out); } .plan-content .plan-tab:hover { --plan-tab-surface: var(--color-hover); color: var(--color-text-primary); } .plan-content .plan-tab[data-selected] { --plan-tab-surface: var(--color-selected); color: var(--color-text-primary); } /* * A removable tab always keeps a slot at its end for remove, so showing the * button never covers the label or moves the tabs beside it. */ .plan-content .plan-tab[data-removable] > [role="tab"] { padding-inline-end: calc(var(--spacing) * 6); } .plan-content .plan-tab-remove { position: absolute; inset-block-start: calc(50% - var(--spacing) * 2); inset-inline-end: calc(var(--spacing) * 1); display: grid; place-items: center; inline-size: calc(var(--spacing) * 4); block-size: calc(var(--spacing) * 4); border-radius: var(--radius-sm); color: var(--color-text-tertiary); opacity: 0; pointer-events: none; transition: opacity var(--duration-fast) var(--ease-out); } @media (hover: hover) { .plan-content .plan-tab:hover .plan-tab-remove { opacity: 1; pointer-events: auto; } } /* A long press reveals the remove control for a tab that is not selected. */ .plan-content .plan-tab[data-revealed] .plan-tab-remove { opacity: 1; pointer-events: auto; } .plan-content .plan-tab-remove:hover { color: var(--color-text-primary); } /* Touch shows removal on the selected tab; others stay compact until revealed. */ @media (hover: none) { .plan-content .plan-tab[data-selected] .plan-tab-remove { opacity: 1; pointer-events: auto; } .plan-content .plan-tab[data-removable]:not([data-selected]):not([data-revealed]) > [role="tab"] { padding-inline-end: calc(var(--spacing) * 2.5); } } /* Coarse pointers: a 44px target around the small remove button. */ :root[data-plan-coarse-pointer] .plan-content .plan-tab-remove::after { content: ""; position: absolute; inset: calc(50% - 22px); } .plan-content .plan-tab-add { flex-shrink: 0; opacity: 0; transition: opacity var(--duration-fast) var(--ease-out); } .plan-content .planTabs:hover .plan-tab-add, .plan-content .plan-tab-strip:focus-within .plan-tab-add { opacity: 1; } @media (hover: none) { .plan-content .plan-tab-add { opacity: 1; } } /* The field sizes to its text: the hidden copy sets the width the input fills. */ .plan-content .plan-tab-rename { display: inline-grid; flex-shrink: 0; max-inline-size: 16rem; } .plan-content .plan-tab-rename > * { grid-area: 1 / 1; min-inline-size: 4rem; padding: calc(var(--spacing) * 1) calc(var(--spacing) * 2.5); } .plan-content .plan-tab-rename > span { visibility: hidden; overflow: hidden; white-space: pre; } .plan-content .plan-tab-rename > input { inline-size: 100%; font: inherit; } /* A rule closes the panel so the block after it reads as outside the tabs. */ .plan-content [data-plan-panels] { padding-block-end: calc(var(--spacing) * 2); border-block-end: var(--edge-width) solid var(--color-edge); } .plan-content [data-plan-panels] > [hidden] { display: none; } /* Like the formula placeholder: out of flow, so the caret stays where it is. */ .plan-content[contenteditable="true"] .planTab > p:only-child:has(> br:only-child) { position: relative; } /* A tab with no blocks at all, as the Planner or MDX can write one. */ .plan-content[contenteditable="true"] .planTab:not(:has(> :not(br)))::before { content: "Write in this tab…"; display: block; color: var(--color-text-quaternary); pointer-events: none; } .plan-content[contenteditable="true"] .planTab > p:only-child:has(> br:only-child)::before { content: "Write in this tab…"; position: absolute; inset-inline-start: 0; color: var(--color-text-quaternary); pointer-events: none; } /* Status ----------------------------------------------------------------- */ /* * Sits in the document pane's header, so it never covers prose. A hidden state * is not `display: none`: the label stays in the accessibility tree so the live * region keeps reporting durability, and the box collapses to nothing. */ .plan-status { display: flex; min-width: 0; align-items: center; gap: calc(var(--spacing) * 1.5); font-size: var(--text-xs); line-height: var(--text-xs--line-height); white-space: nowrap; } .plan-status .btn { margin-inline-start: calc(var(--spacing) * 0.5); } .plan-status-text { display: flex; min-width: 0; flex-direction: column; } /* Shown where a tooltip cannot be reached; see the workspace status row. */ .plan-status-detail { display: none; color: var(--color-text-tertiary); white-space: normal; } .plan-status-dot { flex: none; width: 0.375rem; height: 0.375rem; border-radius: var(--radius-full); background: var(--color-text-tertiary); } .plan-status[data-level="notice"] .plan-status-dot { background: var(--color-warning); animation: plan-status-pulse calc(var(--duration-linger) * 2) var(--ease-out) infinite alternate; } .plan-status-label { color: var(--color-text-secondary); } .plan-status[data-level="alert"] .plan-status-label { color: var(--color-destructive-ink); } @keyframes plan-status-pulse { to { opacity: 0.35; } } @media (prefers-reduced-motion: reduce) { .plan-status[data-level="notice"] .plan-status-dot { animation: none; } } /* * The editor is read-only while the connection is down. Quieter ink and an * ordinary cursor say so before a keystroke silently does nothing; both go * the instant the connection returns, without a transition. */ .plan[data-plan-offline] .plan-content { /* Headings, lists and tables name the primary ink themselves. */ --color-text-primary: var(--color-text-secondary); color: var(--color-text-secondary); cursor: default; } /* What the agent changed ------------------------------------------------- */ /* * Marks that take no space and add no element. * * An agent editing below the fold must not shift the sentence somebody is in * the middle of typing, and a mark that expires a few seconds later must not * shift it back. So a block is washed, and a hole is drawn in `box-shadow` on * the edge of the block still beside it. A `::before` rule was the obvious way * to draw a hole and is the wrong one: it needs the block to become a * containing block, which re-anchors any absolutely positioned widget chrome * inside it for as long as the mark is up. * * Two colours, and they mean different things. `ring` is prose that was * written; `muted-foreground` is structure that moved or went, where no words * are new. Where the agent is working *now* is not said here at all — that is * its cursor, which comes and goes with the turn rather than waiting to be * read. */ /* * Registered so the colours can be animated: every part of a mark is drawn * from these two, so fading them fades the whole mark at once, at its full * shape, with nothing left to snap off at the end. */ @property --plan-wash { syntax: ""; inherits: false; initial-value: transparent; } @property --plan-hole { syntax: ""; inherits: false; initial-value: transparent; } .plan .plan-content [data-plan-change] { /* What the wash fades from and back to. */ --plan-wash-rest: transparent; --plan-wash: color-mix(in oklch, var(--color-brand-wash) 65%, transparent); background-color: var(--plan-wash); animation: plan-change-in var(--duration-linger) var(--ease-out); } /* * Moved, not written. In the structural colour rather than a fainter version * of the one above: the words are ones the reader may already have read, and * saying "new, but less so" about them would not be true. */ .plan .plan-content [data-plan-change="moved"] { --plan-wash: color-mix(in oklch, var(--color-text-quaternary) 9%, transparent); } /* * Prose is washed past its edges by an outline, so the wash neither moves the * text nor stops hard against it. Blocks that draw their own frame — code, * tables, cards — are only tinted, or the two edges would read as a double * border. Opaque over the page, so where the pieces below overlap they do not * darken. */ .plan .plan-content :is(p, h1, h2, h3, h4, h5, h6, ul, ol, blockquote)[data-plan-change] { --plan-wash-rest: var(--color-page); --plan-wash: color-mix(in oklab, var(--color-brand-wash) 65%, var(--color-page)); border-radius: var(--radius-sm); outline: 0.375rem solid var(--plan-wash); } .plan .plan-content :is(p, h1, h2, h3, h4, h5, h6, ul, ol, blockquote)[data-plan-change="moved"] { --plan-wash: color-mix(in oklab, var(--color-text-quaternary) 9%, var(--color-page)); } /* * Two changed blocks in a row are one region. The later one fills the margin * between them upward — an outset paints outside the box without taking any * space — by enough to cover the largest margin a prose block has, 1.5em above * a heading. Any excess lands on the block above, which is already washed the * same colour. Not on a quote, whose bar is a border this would replace. */ .plan .plan-content :is(p, h1, h2, h3, h4, h5, h6, ul, ol)[data-plan-change="added"] + :is(p, h1, h2, h3, h4, h5, h6, ul, ol)[data-plan-change="added"], .plan .plan-content :is(p, h1, h2, h3, h4, h5, h6, ul, ol)[data-plan-change="moved"] + :is(p, h1, h2, h3, h4, h5, h6, ul, ol)[data-plan-change="moved"] { border-image: linear-gradient(var(--plan-wash), var(--plan-wash)) fill 0 / 0 / 1.5em 0.375rem 0; } @keyframes plan-change-in { from { --plan-wash: var(--plan-wash-rest); } } /* * A hole where something was. * * One block can hold a hole above it and another below — delete the second and * fourth of four blocks and the third carries both — so the two sides cannot * each set `box-shadow`, or the cascade would keep only whichever rule came * last and one hairline would silently never draw. Custom properties do * cascade independently, so each side sets one and they compose here. */ .plan .plan-content [data-plan-gap-before], .plan .plan-content [data-plan-gap-after] { --plan-hole: var(--color-text-quaternary); box-shadow: var(--plan-gap-above, 0 0 transparent), var(--plan-gap-below, 0 0 transparent); } .plan .plan-content :is([data-plan-gap-before], [data-plan-gap-after]):not([data-plan-change]) { animation: plan-gap-in var(--duration-linger) var(--ease-out); } @keyframes plan-gap-in { from { --plan-hole: transparent; } } .plan .plan-content [data-plan-gap-before="removed"] { --plan-gap-above: 0 -0.375rem 0 -0.3125rem var(--plan-hole); } .plan .plan-content [data-plan-gap-after="removed"] { --plan-gap-below: 0 0.375rem 0 -0.3125rem var(--plan-hole); } /* * Where a block left from, as opposed to where one was deleted. Fainter, since * nothing was lost — the content is still in the plan, further up or down. */ .plan .plan-content [data-plan-gap-before="vacated"] { --plan-gap-above: 0 -0.375rem 0 -0.34375rem color-mix( in oklch, var(--plan-hole) 55%, transparent ); } .plan .plan-content [data-plan-gap-after="vacated"] { --plan-gap-below: 0 0.375rem 0 -0.34375rem color-mix(in oklch, var(--plan-hole) 55%, transparent); } /* * Expired. The store keeps every attribute on until this has run, so the mark * fades at its full shape and is only then taken off. */ .plan .plan-content :is([data-plan-change], [data-plan-gap-before], [data-plan-gap-after])[data-plan-fade] { animation: plan-change-out var(--duration-linger) var(--ease-out) forwards; } @keyframes plan-change-out { to { --plan-wash: var(--plan-wash-rest); --plan-hole: transparent; } } /* * The agent wrote this block and the reader is pointing at a card that refers * to it. * * Only reachable where there is no Highlight API, since that is the only path * on which a sidecar mark touches the element's own background. The two rules * have equal specificity, so without saying so the later one — this file's — * would simply replace the earlier and the pointer would lose its wash. * Pointing is something the reader just did and the agent's mark is ambient, * so the pointer wins; the gutter bar is in `box-shadow` and survives either * way. */ .plan .plan-content [data-plan-related][data-plan-change] { background: var(--color-brand-wash); } /* Reaching the ones off screen ------------------------------------------- */ /* * One chip per edge, in the leading margin rather than over the prose, and only * while something in that direction is still unread. The status chip has the * trailing corner at the bottom. */ .plan-changes { position: absolute; z-index: 3; inset-inline-start: 0.5rem; display: flex; flex-direction: column; align-items: flex-start; gap: 0.375rem; } .plan-changes[data-side="above"] { inset-block-start: 0.75rem; } .plan-changes[data-side="below"] { inset-block-end: 0.75rem; flex-direction: column-reverse; } /* * A narrow pane has no margin to sit in, and a chip pinned there covers the * first word of a line. Centred, it reads as floating over the document. */ @container (max-width: 44rem) { .plan-changes { inset-inline-start: 50%; translate: -50% 0; align-items: center; } } .plan-changes-bar { display: flex; align-items: stretch; border-radius: var(--radius-full); background: var(--color-page); outline: var(--edge-width) solid var(--color-edge); outline-offset: calc(-1 * var(--edge-width)); box-shadow: var(--shadow-raised); font-size: var(--text-sm); line-height: var(--text-sm--line-height); color: var(--color-text-secondary); } .plan-changes-go, .plan-changes-more { display: flex; align-items: center; justify-content: center; min-block-size: 1.5rem; min-inline-size: 1.5rem; cursor: pointer; } .plan-changes-go { gap: 0.1875rem; padding-inline: 0.375rem 0.3125rem; border-start-start-radius: var(--radius-full); border-end-start-radius: var(--radius-full); font-weight: 500; } .plan-changes-more { border-start-end-radius: var(--radius-full); border-end-end-radius: var(--radius-full); padding-inline-end: 0.125rem; } .plan-changes-go:hover, .plan-changes-more:hover, .plan-changes-more[aria-expanded="true"] { background: var(--color-selected); color: var(--color-text-primary); } .plan-changes-list, .plan-changes-empty { max-inline-size: min(24rem, calc(100cqi - 1rem)); margin: 0; padding: 0.25rem; max-height: 12rem; overflow-y: auto; list-style: none; border-radius: var(--radius-md); background: var(--color-page); outline: var(--edge-width) solid var(--color-edge); outline-offset: calc(-1 * var(--edge-width)); box-shadow: var(--shadow-overlay); font-size: var(--text-sm); line-height: var(--text-sm--line-height); } .plan-changes-empty { padding: 0.5rem 0.625rem; color: var(--color-text-quaternary); } /* The dot's slot is kept on every row, so read and unread text line up. */ .plan-changes-item { display: flex; align-items: baseline; gap: 0.5rem; padding: 0.25rem 0.5rem; border-radius: var(--radius-sm); } .plan-changes-item::before { content: ""; flex: none; align-self: center; width: 0.375rem; height: 0.375rem; border-radius: var(--radius-full); } .plan-changes-kind { flex: none; font-weight: 500; color: var(--color-text-tertiary); } /* Still waiting to be reached, as opposed to already shown and counted down. */ .plan-changes-item[data-unread]::before { background: var(--color-brand); } .plan-changes-item[data-unread] .plan-changes-kind { color: var(--color-text-primary); } .plan-changes-item[data-kind="removed"] .plan-changes-text { color: var(--color-text-secondary); text-decoration: line-through; } /* One line each: this is an index, not a diff. */ .plan-changes-text { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } ::highlight(plan-decided) { background-color: var(--color-success-wash); } /* Presence on a decision card: aligned to the 1.5rem mark beside the title. */ .presence-faces { display: flex; flex: none; align-items: center; block-size: 1.5rem; } @media (prefers-reduced-motion: no-preference) { .presence-face { animation: presence-face-in var(--badge-pop-dur) var(--motion-smooth-out) both; } } @keyframes presence-face-in { from { opacity: 0; scale: 0.85; } } .presence-face + .presence-face { margin-inline-start: -0.375rem; } .presence-more { margin-inline-start: 0.25rem; font-size: var(--text-xs); font-weight: 600; color: var(--color-text-tertiary); font-variant-numeric: tabular-nums; } /* * A resolved decision: a margin marker beside the prose it produced. * * Reader-local chrome drawn over the document like the comment layer, and * for the same reason: nothing here may move a collaborative line box. */ ::highlight(plan-decision) { background-color: var(--color-success-wash); } /* A card carried by markers takes no room in the plan. */ .plan .plan-content [data-plan-questionnaire]:has([data-plan-collapsed]) { margin-block: 0; } .plan-decision-layer { pointer-events: none; position: absolute; inset: 0; z-index: 2; } .plan-document [data-plan-scroll] { position: relative; } .plan-decision-marker { pointer-events: auto; position: absolute; display: grid; place-items: center; inline-size: var(--decision-marker-size); block-size: var(--decision-marker-size); padding: 0; border-radius: var(--radius-full); } /* * Getting close is enough: the hit area reaches past the drawn marker, but the * layer measures how far it may go sideways so it never covers the prose. */ .plan-decision-marker::before { content: ""; position: absolute; inset-block-start: calc(-1 * var(--plan-decision-reach-top, 0px)); inset-block-end: calc(-1 * var(--plan-decision-reach-bottom, 0px)); inset-inline-start: calc(-1 * var(--plan-decision-reach-start, 0px)); inset-inline-end: calc(-1 * var(--plan-decision-reach-end, 0px)); } .plan-decision-disc { --color-icon: var(--color-success); display: grid; place-items: center; inline-size: var(--decision-marker-size); block-size: var(--decision-marker-size); border-radius: var(--radius-full); background: var(--color-success-wash); color: var(--color-success); } .plan-decision-size-probe { position: absolute; inset-block-start: 0; inset-inline-start: 0; visibility: hidden; pointer-events: none; } /* Too narrow a gutter for a disc: a slim bar the height of the first line. */ .plan-decision-marker[data-compact] { place-items: center start; border-radius: 0; } .plan-decision-marker[data-compact] .plan-decision-disc { inline-size: 0.25rem; block-size: var(--plan-decision-line, 1lh); margin-inline-start: calc(var(--spacing) * 1); background: var(--color-success); } .plan-decision-marker[data-compact] svg { display: none; } .plan-decision-marker:hover .plan-decision-disc, .plan-decision-marker[aria-expanded="true"] .plan-decision-disc { --color-icon: var(--color-success-ink); outline: var(--edge-width) solid var(--color-success); } .plan-decision-marker:hover:not([aria-expanded="true"]) .plan-decision-disc { outline-color: var(--color-success-outline-subtle); } .plan-decision-marker:focus-visible .plan-decision-disc { --color-icon: var(--color-success-ink); } .plan-decision-pop { max-width: none; padding: 0.75rem; border-radius: var(--radius-lg); font-size: var(--text-sm); line-height: var(--text-sm--line-height); } .plan-decision-pop[data-pinned] { pointer-events: auto; } .plan-decision-pop[role="tooltip"] { font-size: var(--text-xs); line-height: var(--text-xs--line-height); } .plan-decision-pop[role="tooltip"] .plan-decision-answer { font-size: var(--text-xs); } .plan-decision-pop[role="tooltip"] .plan-decision-meta { font-size: var(--text-2xs); line-height: var(--text-2xs--line-height); } .plan-decision-pop p { margin: 0; } .plan-decision-head { display: flex; align-items: flex-start; gap: 0.5rem; } .plan-decision-question { flex: 1; line-height: var(--text-xs--line-height); font-weight: 500; color: var(--color-text-secondary); } .plan-decision-pop .plan-decision-tools { display: flex; margin: -0.125rem -0.25rem 0 0; opacity: 0; pointer-events: none; transition: opacity var(--duration-fast) var(--ease-out); } /* The close button is a compact 24px, like the comment card's. */ .plan-decision-tools .btn-icon { inline-size: 1.5rem; block-size: 1.5rem; min-inline-size: 1.5rem; min-block-size: 1.5rem; padding: 0; } .plan-decision-pop[data-pinned] .plan-decision-tools { opacity: 1; pointer-events: auto; } .plan-decision-pop .plan-decision-answer { display: flex; align-items: center; gap: 0.375rem; margin-block-start: 0.375rem; font-size: var(--text-sm); font-weight: 500; color: var(--color-success); } .plan-decision-pop .plan-decision-answer > span { display: inline; margin: 0; color: var(--color-text-primary); } /* What was not chosen: same size, lighter weight, a grey cross in the check's place. */ .plan-decision-pop .plan-decision-answer[data-rejected] { margin-block-start: 0.25rem; font-weight: 400; color: var(--color-text-quaternary); } .plan-decision-pop .plan-decision-answer[data-rejected] > span { color: var(--color-text-secondary); } .plan-decision-meta { display: flex; flex-wrap: wrap; align-items: center; column-gap: 1rem; row-gap: 0.5rem; /* A clear space separates what was decided from who and when. */ margin-block-start: 1rem; font-size: var(--text-xs); line-height: var(--text-xs--line-height); color: var(--color-text-tertiary); } .plan-decision-pop .plan-decision-meta > p { display: flex; align-items: flex-start; gap: 0.375rem; min-width: 0; overflow-wrap: anywhere; } .plan-decision-meta > p > :first-child { flex: none; } .plan-decision-meta > p > :is(img, [role="img"]) { margin-block: calc((1lh - 1.125rem) / 2); } .plan-decision-pop .plan-decision-meta span { display: inline; margin: 0; color: inherit; } .plan-decision-meta strong { font-weight: 500; color: var(--color-text-secondary); } .plan-decision-fold { display: grid; grid-template-rows: 0fr; transition: grid-template-rows var(--acc-collapse) var(--motion-move); } .plan-decision-fold > div { overflow: hidden; } .plan-decision-pop[data-pinned] .plan-decision-fold { grid-template-rows: 1fr; transition-duration: var(--acc-expand); } @media (prefers-reduced-motion: reduce) { .plan-decision-fold, .plan-decision-tools, .plan-decision-disc { transition: none; } } /* Keep the caret off the bottom edge while typing: browsers honour this when they scroll a selection into view. */ .plan [data-plan-scroll] { /* Wide blocks measure the usable scrollport, excluding a consuming scrollbar. */ container-type: inline-size; scroll-padding-block: 4rem 30%; } /* * Task-list checkboxes. MDXEditor draws the box on ::before and the check on * ::after of the list item; restyle both and centre them on the first line. */ .plan-content :is([class*="_listItemUnchecked"], [class*="_listItemChecked"]) { --plan-check-size: 1rem; --plan-check-top: calc((1lh - var(--plan-check-size)) / 2); } .plan-content :is([class*="_listItemUnchecked"], [class*="_listItemChecked"]):before { top: var(--plan-check-top); width: var(--plan-check-size); height: var(--plan-check-size); box-sizing: border-box; border: 1px solid var(--color-control-edge); border-radius: var(--radius-sm); background-color: var(--color-page); box-shadow: var(--shadow-resting); transition-property: background-color, border-color; } .plan-content [class*="_listItemUnchecked"]:hover:before { background-color: var(--color-selected); } .plan-content [class*="_listItemChecked"]:before { border-color: var(--color-brand); background-color: var(--color-brand); } .plan-content [class*="_listItemChecked"]:hover:before { border-color: var(--color-brand-hover); background-color: var(--color-brand-hover); } .plan-content [class*="_listItemChecked"]:after { top: calc(var(--plan-check-top) + 0.1875rem); left: 0.3125rem; right: auto; width: 0.3125rem; height: 0.5625rem; border-color: var(--color-white); border-width: 0 2px 2px 0; transform: rotate(45deg); animation: plan-check-in var(--duration-fast) var(--ease-out); } @keyframes plan-check-in { from { opacity: 0; transform: rotate(45deg) scale(0.8); } } @media (prefers-reduced-motion: reduce) { .plan-content [class*="_listItemChecked"]:after { animation: none; } .plan-content :is([class*="_listItemUnchecked"], [class*="_listItemChecked"]):before { transition: none; } } @keyframes plan-bubble-enter { from { opacity: 0; transform: translateY(4px) scale(var(--dropdown-pre-scale)); } to { opacity: 1; transform: none; } } .plan-formatting-toolbar { animation: plan-bubble-enter var(--duration-fast) var(--ease-out); } @media (prefers-reduced-motion: reduce) { .plan-formatting-toolbar { animation: none; } }