/** * 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. */ .plan { --plan-gutter: 2rem; container-type: inline-size; /* * 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. At the body size below this is roughly 85 characters a line, near * the top of the range a reader can track back from without losing the row. */ --plan-measure: 40rem; /* * 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-body: var(--text-base); } /* Document views -------------------------------------------------------- */ .plan-workspace { position: relative; display: flex; flex: 1; min-height: 0; overflow: hidden; } .plan-document { position: relative; flex: 1; min-width: 0; min-height: 0; background: var(--color-page); box-shadow: var(--shadow-raised); } /* 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-button, .plan-comment-orphan-button { 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); } .plan-comment-button { position: absolute; display: grid; width: 24px; height: 24px; place-items: center; border-radius: var(--radius-md); font-size: 0.75rem; } /* The host receives hits so prose keeps native selection. */ .plan-comment-hit { pointer-events: none; position: absolute; } .plan-comment-button:hover, .plan-comment-button:focus-visible, .plan-comment-orphan-button:hover, .plan-comment-orphan-button:focus-visible { background: var(--color-brand-wash); outline: 2px solid var(--color-brand); outline-offset: 2px; } .plan-comment-preview, .plan-comment-card { pointer-events: auto; position: absolute; width: min(24rem, 80%); } .plan-comment-preview { /* A narrow preview can overlap prose, but never intercept it. */ pointer-events: none; max-width: 18rem; padding: 0.5rem 0.625rem; border-radius: var(--radius-md); background: var(--color-page); box-shadow: var(--shadow-overlay); font-size: var(--text-sm); color: var(--color-text-secondary); } .plan-comment-preview p { margin: 0; font-style: italic; } .plan-comment-preview span { 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); } .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-decisions { display: flex; height: 100%; min-width: 0; min-height: 0; flex-direction: column; overflow: hidden; /* The workspace ground shows through the rail. */ background: transparent; } /* 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; } .plan .plan-content :is([data-plan-questionnaire], [data-plan-decision]) article { width: 100%; } /* * 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) { /* #dcf6fa */ 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); --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 { /* * 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: 1.5rem var(--plan-gutter) 60vh; 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: 1.6; color: var(--color-text-primary); } .plan .plan-content:focus { outline: none; } /* * 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) { font-weight: 600; 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, h5, h6):first-child { margin-block-start: 0; } .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); } .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; } .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 [data-plan-type] { --callout-accent: var(--color-gray-500); --callout-ink: var(--color-text-secondary); --callout-wash: var(--color-gray-100); border: var(--edge-width) solid color-mix(in oklab, var(--callout-accent) 18%, transparent); padding: 0.875rem 1rem; margin-block: 1rem; border-radius: var(--radius-lg); background: color-mix(in oklab, var(--callout-wash) 68%, var(--color-page)); box-shadow: inset 0 1px rgb(255 255 255 / 55%), var(--shadow-resting); transition: border-color var(--duration-fast) var(--ease-out), background var(--duration-fast) var(--ease-out); } .plan-content [data-plan-type="tip"] { --callout-accent: var(--color-success); --callout-ink: var(--color-success-ink); --callout-wash: var(--color-success-wash); } .plan-content [data-plan-type="important"] { --callout-accent: var(--color-brand); --callout-ink: var(--color-brand-ink); --callout-wash: var(--color-brand-wash); } .plan-content [data-plan-type="warning"] { --callout-accent: var(--color-warning); --callout-ink: var(--color-warning-ink); --callout-wash: var(--color-warning-wash); } .plan-content [data-plan-type="danger"] { --callout-accent: var(--color-destructive); --callout-ink: var(--color-destructive-ink); --callout-wash: var(--color-destructive-wash); } .plan-content [data-plan-type]:focus-within { border-color: color-mix(in oklab, var(--callout-accent) 34%, transparent); background: color-mix(in oklab, var(--callout-wash) 82%, var(--color-page)); } .plan-callout-heading { position: relative; display: flex; min-width: 0; min-height: 1.75rem; align-items: center; gap: 0.625rem; } .plan-callout-type { display: grid; width: 1.75rem; height: 1.75rem; flex: 0 0 auto; place-items: center; border: var(--edge-width) solid transparent; border-radius: var(--radius-md); background: transparent; color: var(--callout-ink); cursor: pointer; transition: background var(--duration-fast) var(--ease-out), border-color var(--duration-fast) var(--ease-out), transform var(--duration-fast) var(--ease-out); } .plan-callout-type:hover:not(:disabled), .plan-callout-type:focus-visible, .plan-callout-type[aria-expanded="true"] { border-color: color-mix(in oklab, var(--callout-accent) 24%, transparent); background: color-mix(in oklab, var(--callout-accent) 16%, transparent); } .plan-callout-type:active:not(:disabled) { transform: scale(0.94); } .plan-callout-type:focus-visible, .plan-callout-title:focus-visible, .plan-callout-option:focus-visible { outline: 2px solid var(--color-brand); outline-offset: 2px; } .plan-callout-type:disabled { cursor: default; } .plan-callout-title { min-width: 2rem; max-width: 100%; border-radius: var(--radius-sm); padding: 0.125rem 0.25rem; margin-inline-start: -0.25rem; font-size: var(--text-sm); font-weight: 600; line-height: var(--text-sm--line-height); color: var(--callout-ink); text-wrap: pretty; transition: background var(--duration-fast) var(--ease-out), box-shadow var(--duration-fast) var(--ease-out); } .plan-callout-title[contenteditable="true"] { cursor: text; } .plan-callout-title[contenteditable="true"]:hover { background: color-mix(in oklab, var(--callout-accent) 7%, transparent); } .plan-callout-title[contenteditable="true"]:focus { background: var(--color-page); box-shadow: 0 0 0 1px color-mix(in oklab, var(--callout-accent) 25%, transparent); color: var(--color-text-primary); outline: none; } .plan-callout-title:empty::before { content: attr(data-placeholder); color: var(--callout-ink); opacity: 0.76; pointer-events: none; } .plan-content [data-plan-body] { padding-inline-start: 2.375rem; color: var(--color-text-secondary); } .plan-content [data-plan-body] > :first-child { margin-block-start: 0.25rem; } .plan-content [data-plan-body] > :last-child { margin-block-end: 0; } .plan-callout-menu { position: absolute; top: calc(100% + 0.25rem); left: 0; z-index: 20; width: 10rem; padding: 0.25rem; border: var(--edge-width) solid var(--color-edge); border-radius: var(--radius-lg); background: var(--color-page); box-shadow: var(--shadow-overlay); animation: plan-callout-menu-in var(--duration-fast) var(--ease-out); } .plan-callout-option { --option-color: var(--color-gray-500); display: grid; width: 100%; height: 2rem; grid-template-columns: 1.25rem 1fr 1rem; align-items: center; gap: 0.5rem; border: 0; border-radius: var(--radius-sm); padding-inline: 0.5rem; background: transparent; color: var(--color-text-secondary); font-size: var(--text-sm); text-align: left; cursor: pointer; transition: background var(--duration-fast) var(--ease-out), color var(--duration-fast) var(--ease-out); } .plan-callout-option > svg:first-child { color: var(--option-color); } .plan-callout-option:hover, .plan-callout-option:focus-visible, .plan-callout-option[aria-checked="true"] { background: var(--color-hover); color: var(--color-text-primary); } .plan-callout-option[data-callout-type="tip"] { --option-color: var(--color-success); } .plan-callout-option[data-callout-type="important"] { --option-color: var(--color-brand); } .plan-callout-option[data-callout-type="warning"] { --option-color: var(--color-warning); } .plan-callout-option[data-callout-type="danger"] { --option-color: var(--color-destructive); } .plan-callout-option > svg:last-child { justify-self: end; } @keyframes plan-callout-menu-in { from { opacity: 0; transform: translateY(-0.25rem) scale(0.98); } to { opacity: 1; transform: translateY(0) scale(1); } } @media (prefers-reduced-motion: reduce) { .plan-callout-menu { animation: none; } } .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. Sized in `em` so it tracks whatever it sits in — a heading's * code should not shrink to body size — and left to `pre` when it is a block. */ .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: 0.875em; } /* Tables scroll independently so a wide one never widens the document. */ /* * Tables scroll independently so a wide one never widens the document. * * `display: block` is what allows that, but it also stops the table sizing * itself from its contents — columns get squeezed until neighbouring values * touch and read as one word. `width: max-content` gives that back, and the * `max-width` keeps a narrow table from stretching to fill the measure. */ .plan-content table { display: block; width: max-content; max-width: 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-width: 4rem; } .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; } /* * 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 `