/* Chart Builder — two-pane modal body (spec §06). */ .builder { display: grid; grid-template-columns: minmax(320px, 360px) 1fr; /* Fill the modal body and never exceed it, so each pane scrolls internally rather than the whole modal growing past the viewport (otherwise a tall chart pushes the Create/Cancel actions below the fold). The `minmax(0, 1fr)` row lets the panes shrink below their content height so their own overflow kicks in. */ grid-template-rows: minmax(0, 1fr); height: 100%; min-height: 0; min-width: 0; } /* ── No-dataset states: Carbon no-data empty state / dataset chooser ──── */ .emptyState { display: flex; flex-direction: column; align-items: flex-start; gap: var(--space-4); max-width: 48ch; padding: var(--space-7) var(--space-6); } .emptyTitle { margin: 0; font-size: 16px; font-weight: 600; color: var(--text); } .emptyBody { margin: 0; font-size: 14px; line-height: 1.5; color: var(--text-secondary); } /* ── Left: configuration ─────────────────────────────────────────────── */ .configPane { display: flex; flex-direction: column; gap: var(--space-5); padding: var(--space-5); border-right: var(--border-width) solid var(--border); overflow-y: auto; min-width: 0; min-height: 0; } /* The dataset picker pinned at the top of the config pane. */ .datasetRow { display: flex; align-items: center; gap: var(--space-3); } /* Edit-in-place banner (spec §06 → Open in builder): names the snippet the builder is editing, so the primary "Save changes" action reads unambiguously. */ .editingBanner { margin: 0; font-size: 12px; color: var(--text-secondary); } .editingBanner strong { color: var(--text-primary); font-weight: 600; } /* ── Intent front door (spec §06 → Intent) ─────────────────────────────── A persistent "what do you want to show?" strip under the dataset picker. A quiet accent-soft wash marks it as the guided on-ramp without competing with the primary Create action (--accent stays reserved for primary actions). */ .intentStrip { display: flex; flex-direction: column; gap: var(--space-2); padding: var(--space-4); border: var(--border-width) solid var(--border); background: var(--accent-soft); } .intentQ { font-size: 13px; font-weight: 600; color: var(--text); } .intentSub { margin: 0; font-size: 12px; line-height: 1.45; color: var(--text-secondary); } .intentChips { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-top: var(--space-1); } .intentChip { appearance: none; height: var(--control-height); border: var(--border-width) solid var(--border-strong); background: var(--bg); color: var(--text); font: inherit; font-size: 12px; padding: 0 var(--space-3); cursor: pointer; transition: background var(--dur-fast) var(--ease), box-shadow var(--dur-fast) var(--ease); } .intentChip:hover { background: var(--field); } /* The chip whose layout the live chart matches (council-reserved: selection uses an accent ring, never an accent fill — fill stays the primary-action signal). */ .intentChip[aria-pressed='true'] { border-color: var(--accent); box-shadow: inset 0 0 0 1px var(--accent); } /* Disabled = the dataset can't satisfy this intent: perceivable but inert (the chip's accessible name carries the reason). */ .intentChip[aria-disabled='true'] { color: var(--text-placeholder); border-color: var(--border); background: var(--bg); cursor: not-allowed; } .intentChip:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; } /* ── Data section: filters, calculated fields, row preview (spec §06 → Data) ──── */ .dataSection { display: flex; flex-direction: column; gap: var(--space-3); } .transformGroup { display: flex; flex-direction: column; gap: var(--space-2); } .transformGroupHead { display: flex; align-items: center; justify-content: space-between; } /* "Add filter" / "Add field" — a low-emphasis ghost action, like Swap X/Y. */ .addRow { display: inline-flex; align-items: center; gap: 4px; border: none; background: transparent; color: var(--accent); font: inherit; font-size: 12px; cursor: pointer; padding: var(--space-1) var(--space-2); border-radius: var(--radius); } .addRow:hover { background: var(--layer-01); } .addRow:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; } /* A filter / calculated-field row, bordered like a channel block for visual rhyme. */ .transformBlock { display: flex; flex-direction: column; gap: var(--space-2); padding: var(--space-3); border: var(--border-width) solid var(--border); border-radius: var(--radius); } .transformBlockTop { display: flex; align-items: center; gap: var(--space-2); } .filterPredicate { display: flex; align-items: center; gap: var(--space-2); } .filterPredicate .mini { flex: none; } .valueInput { flex: 1; min-width: 0; height: var(--control-height); padding: 0 var(--space-2); font-size: 13px; } .rangeDash { flex: none; color: var(--text-placeholder); } /* Expression inputs (filter expression / calculate) — monospace, fill the row. */ .exprInput { flex: 1; min-width: 0; height: var(--control-height); padding: 0 var(--space-3); font-family: var(--font-mono); font-size: 12px; } .calcName { flex: 0 1 110px; min-width: 0; height: var(--control-height); padding: 0 var(--space-3); font-family: var(--font-mono); font-size: 12px; } .calcEquals { flex: none; color: var(--text-placeholder); font-family: var(--font-mono); } .exprInput[aria-invalid='true'] { border-color: var(--support-error); } /* Remove-row × is the shared IconButton (sm). */ /* Expression escape-hatch toggle — a low-emphasis link, like the hint fixes. */ .modeToggle { align-self: flex-start; border: none; background: transparent; color: var(--accent); font: inherit; font-size: 11px; cursor: pointer; padding: 2px var(--space-1); border-radius: var(--radius); } .modeToggle:hover { background: var(--layer-01); } .modeToggle:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; } .exprError, .exprWarn { display: flex; align-items: flex-start; gap: var(--space-2); margin: 0; font-size: 11px; line-height: 1.4; } /* Severity reads from the glyph shape + colour, not colour alone (arch 10 §3); the icon inherits the line's colour via currentColor (round error / triangle warning). */ .exprFeedbackIcon { flex: none; margin-top: 1px; } .exprError { color: var(--support-error); } .exprWarn { color: var(--support-warning-fg); } /* Contextual pointer to the Vega expression docs (shown when an expression exists). */ .exprHelp { margin: 0; font-size: 11px; line-height: 1.4; color: var(--text-secondary); } .exprHelpLink { color: var(--accent); text-decoration: none; border-radius: var(--radius); } .exprHelpLink:hover { text-decoration: underline; } .exprHelpLink:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; } /* Row preview disclosure (1D). */ .dataPreview { display: flex; flex-direction: column; gap: var(--space-2); } .previewToggle { display: inline-flex; align-items: center; gap: var(--space-2); align-self: flex-start; border: none; background: transparent; color: var(--text-secondary); font: inherit; font-size: 12px; cursor: pointer; padding: var(--space-1) var(--space-2); border-radius: var(--radius); } .previewToggle:hover { background: var(--layer-01); } .previewToggle:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; } .previewCaret { font-size: 10px; color: var(--text-placeholder); } .previewMeta { color: var(--text-placeholder); } /* The per-column type chip in the source-preview header (the table structure and cell styling now live in the shared DataTable). */ .previewColName { font-weight: 600; color: var(--text-secondary); } .previewColType { color: var(--text-placeholder); font-weight: 400; } .previewEmptyNote { margin: 0; font-size: 11px; color: var(--text-secondary); } .field { display: flex; flex-direction: column; gap: var(--space-2); } /* The mark picker carries six segments — too many for one 320–360px row. Override the SegmentedControl's fixed single-row height so it wraps; each option keeps the control height so the two rows read as even segments inside the shared border. */ .markPicker { flex-wrap: wrap; height: auto; } .markPickerOption { min-height: var(--control-height); } .fieldLabel { font-size: 12px; font-weight: 500; color: var(--text-secondary); } .swap { border: none; background: transparent; color: var(--accent); font: inherit; font-size: 12px; cursor: pointer; padding: var(--space-1) var(--space-2); border-radius: var(--radius); } .swap:hover { background: var(--layer-01); } .swap:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; } /* Channel name label (Marks card rows + the on-chart Columns/Rows shelves). */ .channelLabel { font-size: 12px; font-weight: 600; color: var(--text); } .transform { display: inline-flex; align-items: center; gap: 6px; } .miniLabel { font-size: 11px; color: var(--text-placeholder); } .toggle { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: var(--text-secondary); } .chartControls { display: flex; gap: var(--space-5); } .dimInput { width: 100px; height: var(--control-height); padding: 0 var(--space-3); font-size: 13px; } .warnings { display: flex; flex-direction: column; gap: var(--space-2); margin: 0; padding: var(--space-3); list-style: none; background: var(--layer-01); border: var(--border-width) solid var(--border); border-radius: var(--radius); } /* These take focus only programmatically (tabIndex -1) after a hint fix is applied, to keep focus off ; no visible ring for that script-driven move. */ .warnings:focus, .configPane:focus { outline: none; } .warning { display: flex; align-items: flex-start; gap: var(--space-2); font-size: 12px; line-height: 1.4; color: var(--text-secondary); } /* Filled status-warning glyph (arch 09 §5.2), in the contrast-safe amber. Replaces the old ⚠ text character so the warning matches the toast status family. */ .warningIcon { flex: none; margin-top: 1px; color: var(--support-warning-fg); } /* The hint text and its one-click remedies stacked, growing beside the icon. */ .warningBody { display: flex; flex-direction: column; gap: var(--space-2); min-width: 0; } /* Actionable-hint remedies: each fix is an offer, never forced — low-emphasis suggestions beside the advice, not commands. */ .warningFixes { display: flex; flex-wrap: wrap; gap: var(--space-2); } /* Inline link-style actions (arch 09 §4): content-sized accent text, no box — the smallness is the emphasis level. */ .warningFix { font: inherit; font-size: 12px; cursor: pointer; padding: var(--space-1) var(--space-2); border: none; border-radius: var(--radius); background: transparent; color: var(--accent); } .warningFix:hover { background: var(--control-hover-fill, var(--layer-01)); } .warningFix:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; } /* Explains the disabled Create action (contract 10: a disabled control must say why). `margin-top: auto` pins it just above the actions so the two read as one. */ .createHint { margin: auto 0 0; font-size: 12px; color: var(--text-secondary); text-align: right; } /* Cancel / Create are shared Buttons (secondary / primary, lg — modal footer). */ .actions { display: flex; justify-content: flex-end; gap: var(--space-3); margin-top: auto; } /* ── Right: live preview ─────────────────────────────────────────────── */ /* Right side = on-chart shelves stacked above the live preview (spec §06). */ .previewSide { display: flex; flex-direction: column; min-width: 0; min-height: 0; background: var(--bg); } .previewPane { display: flex; flex-direction: column; flex: 1; padding: var(--space-5); min-width: 0; min-height: 0; overflow: hidden; background: var(--bg); } .previewHint { margin: auto; font-size: 13px; color: var(--text-secondary); text-align: center; } .previewFrame { flex: 1; min-width: 0; min-height: 0; display: flex; /* A chart taller/wider than the pane scrolls *here*, inside a fixed viewport, so the modal keeps its shape. `safe` centring aligns to the start instead of clipping the top/left when the chart overflows. */ overflow: auto; align-items: safe center; justify-content: safe center; } .previewHost { width: 100%; } .previewError { margin: auto 0; padding: var(--space-3); } .previewErrorHeadline { margin: 0; font-size: 13px; line-height: 1.5; color: var(--support-error); } .previewErrorDetails { margin-top: var(--space-2); } .previewErrorSummary { font-size: 12px; color: var(--text-secondary); cursor: pointer; user-select: none; } .previewErrorDetail { margin: var(--space-2) 0 0; padding: var(--space-3); font-family: var(--font-mono); font-size: 12px; line-height: 1.4; color: var(--text); background: var(--layer-01); border: var(--border-width) solid var(--border); white-space: pre-wrap; word-break: break-word; } /* ── Encoding: field shelf + Tableau-style shelves/pills (spec §06 → Encoding) ─── */ /* Field shelf — the dataset's columns as clickable, type-glyphed chips (field-first). */ .fieldShelf { display: flex; flex-direction: column; gap: var(--space-2); } .shelfGroupHead { margin-top: var(--space-2); font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-secondary); } /* Scroll viewport around the field lists: a wide dataset (Superstore: 25 columns) must not push the Marks card and chart controls below the fold. Group headings pin to the top while their list scrolls under them. */ .shelfScroll { display: flex; flex-direction: column; gap: var(--space-2); max-height: 280px; overflow-y: auto; /* Scroll shadows (background-attachment trick): a soft cue at the cut edge that appears only while there is more list in that direction. The covers (first two layers, attachment: local) ride with the content and hide the shadows at the ends; rows are transparent so the shadows show through between them. */ background: linear-gradient(var(--bg) 30%, transparent), linear-gradient(transparent, var(--bg) 70%) 0 100%, linear-gradient(rgb(0 0 0 / 0.1), transparent), linear-gradient(transparent, rgb(0 0 0 / 0.1)) 0 100%; background-repeat: no-repeat; background-size: 100% 24px, 100% 24px, 100% 8px, 100% 8px; background-attachment: local, local, scroll, scroll; } /* An armed channel makes the shelf the live target — say so where the next click happens (NN/g #1): an accent ring on the list plus the status line above it. */ .shelfArmed { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: var(--radius); } .armedHint { margin: 0; font-size: 12px; color: var(--text-secondary); } .armedHint strong { color: var(--accent); } .shelfScroll .shelfGroupHead { position: sticky; top: 0; z-index: 1; margin-top: 0; padding: var(--space-2) 0; background: var(--layer-01); } .shelfList { display: flex; flex-direction: column; } /* Field rows are FLAT — hairline dividers and a hover fill, not per-row boxes: 25 outlined rectangles read as noise, and flat rows are denser, so more of a wide dataset is visible before the shelf scrolls (arch 09 §4 — boxes are for inputs and drop targets, not list rows). */ .shelfField { display: flex; align-items: center; gap: var(--space-2); width: 100%; padding: var(--space-2) var(--space-2); border: none; background: transparent; color: var(--text); font: inherit; font-size: 13px; text-align: left; cursor: pointer; transition: background var(--dur-fast) var(--ease); } .shelfField + .shelfField { border-top: var(--border-width) solid var(--border); } .shelfField:hover { background: var(--layer-01); } .shelfField:focus-visible { outline: 2px solid var(--focus); outline-offset: -1px; } .shelfFieldUsed { opacity: 0.55; } .shelfGlyph { min-width: 22px; padding: 1px var(--space-2); /* A passive type tag, not a control — subtle border (borders mark function). */ border: var(--border-width) solid var(--border); background: var(--layer-01); color: var(--text-secondary); font-family: var(--font-mono); font-size: 11px; text-align: center; } .shelfFieldName { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } /* Marks card — the non-positional encodings (Colour, Size). */ .marksCard { display: flex; flex-direction: column; gap: var(--space-3); padding: var(--space-3); border: var(--border-width) solid var(--border); border-radius: var(--radius); } .marksRow { display: grid; grid-template-columns: 44px 1fr; align-items: start; gap: var(--space-2); } /* Pill — a bound channel (field/count or constant). The per-channel transform controls (Aggregate/Bin/Granularity) flow inline beside the pill, wrapping only when the row truly runs out of room (a narrow window; the Marks card). */ .pillWrap { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2) var(--space-3); min-width: 0; } .pill { display: inline-flex; align-items: stretch; /* One pill height, and a floor on width so "Count" and "Customer Name" read as the same species; names stay content-sized above the floor (truncating to equal widths would cost more than it buys). */ height: 24px; min-width: 96px; max-width: 100%; background: var(--accent); color: var(--accent-contrast); border-radius: var(--radius); } .pillConst { background: var(--layer-02); color: var(--text); } .pillType { display: inline-flex; align-items: center; padding: 0 var(--space-2); border: none; background: color-mix(in srgb, #000 18%, var(--accent)); color: var(--accent-contrast); font-family: var(--font-mono); font-size: 11px; line-height: 1; cursor: pointer; } .pillType:hover:not(:disabled) { background: color-mix(in srgb, #000 30%, var(--accent)); } .pillType:disabled { cursor: default; opacity: 0.85; } .pillType[aria-expanded='true'] { background: color-mix(in srgb, #000 30%, var(--accent)); } .pillType:focus-visible { outline: 2px solid var(--accent-contrast); outline-offset: -2px; } .pillTag { display: inline-flex; align-items: center; padding: 0 var(--space-2); color: var(--text-secondary); font-size: 10px; text-transform: uppercase; letter-spacing: 0.03em; } .pillName { /* Grow into any pill slack so the remove ✕ stays at the pill's right edge. */ flex: 1; min-width: 0; align-self: center; padding: 0 var(--space-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; } .pillRemove { display: inline-flex; align-items: center; justify-content: center; padding: 0 var(--space-2); border: none; background: transparent; color: inherit; cursor: pointer; } .pillRemove:hover { background: color-mix(in srgb, var(--text) 14%, transparent); } .pillRemove:focus-visible { outline: 2px solid var(--focus); outline-offset: -2px; } /* Sizing + chrome come from ColorField (size="sm"); only the pill margin is local. */ .constColor { margin: 0 var(--space-2); } .constNumber { width: 64px; margin: 0 var(--space-2); padding: var(--space-1) var(--space-2); font-size: 12px; } .pillControls { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); } /* Empty assign target — armed by click, then filled by a clicked field. */ .slot { display: flex; align-items: center; gap: var(--space-2); min-height: var(--control-height); padding: var(--space-1) var(--space-2); border: var(--border-width) dashed var(--border-strong); background: var(--layer-01); } .slotActive { border-style: solid; border-color: var(--accent); box-shadow: inset 0 0 0 1px var(--accent); } .slotAssign { flex: 1; min-width: 0; padding: var(--space-1); border: none; background: transparent; color: var(--text-placeholder); font: inherit; font-size: 12px; text-align: left; cursor: pointer; } .slotAssign:hover { color: var(--text-secondary); } .slotAssign:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; } /* Ghost button (Carbon: links navigate, buttons act — this one acts). */ .slotConst { padding: var(--space-1) var(--space-2); border: none; border-radius: var(--radius); background: none; color: var(--accent); font: inherit; font-size: 11px; white-space: nowrap; cursor: pointer; transition: background var(--dur-fast) var(--ease); } .slotConst:hover { background: var(--layer-02); color: var(--accent-hover); } .slotConst:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; } /* On-chart Columns / Rows shelves (X/Y), stacked above the preview. */ .shelves { display: flex; flex-direction: column; gap: var(--space-3); padding: var(--space-5) var(--space-5) 0; } /* Swap sits beside the heading — an action flushed to the far edge of a very wide pane reads as unrelated to the shelves it acts on. */ .shelvesHead { display: flex; align-items: baseline; gap: var(--space-4); } .shelfStrip { display: flex; align-items: stretch; /* One fixed minimum for BOTH strips — sized to the tallest occupant (a 32px control + the slot padding), so Columns (which carries the Aggregate select) and Rows stay the same height instead of each hugging its own content. */ min-height: 48px; border: var(--border-width) solid var(--border); } .shelfName { flex: 0 0 84px; display: flex; align-items: center; padding: 0 var(--space-3); border-right: var(--border-width) solid var(--border); background: var(--layer-01); color: var(--text-secondary); font-size: 12px; } .shelfSlots { flex: 1; display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-3); min-width: 0; padding: var(--space-2) var(--space-3); } /* Empty slots keep a minimum presence; a filled pill hugs its content so the remove ✕ sits at its right edge rather than adrift in dead pill background. */ .shelfSlots .slot { min-width: 190px; } /* Chart properties — title/subtitle/size as a slim strip pinned under the preview (chart-level properties live on the chart side; council 2026-06-12). */ .chartProps { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-4); padding: var(--space-3) var(--space-5); border-top: var(--border-width) solid var(--border); } .propField { display: inline-flex; align-items: center; gap: var(--space-2); font-size: 12px; color: var(--text-secondary); white-space: nowrap; } .propInput { width: 200px; height: var(--control-height); padding: 0 var(--space-2); font-size: 13px; } .propInput:disabled { opacity: 0.5; cursor: not-allowed; }