diff --git a/docs/studio/PROGRESS.md b/docs/studio/PROGRESS.md index 028a8ad..1c33422 100644 --- a/docs/studio/PROGRESS.md +++ b/docs/studio/PROGRESS.md @@ -29,6 +29,43 @@ derivations to resolve rather than fixtures. --- +### 2026-08-19 — The Reflective redesign + +Requested in review: cleaner, more intuitive controls; explanations behind a hoverable question +mark instead of paragraphs in every box; the look of reflective.org / simulator.reflective.org / +tomas-fe.vercel.app. + +**Design sources, read rather than imagined.** The SAI simulator frontend +(`~/GitHub/reflective-simulator/sai-simulator-fe`) uses Radix Themes, Inter, lucide icons, a +zero-delay `CircleHelp` tooltip beside every control, and a builder-left/plots-right layout; +reflective.org contributes deep navy `#091834`, gold `#f1b80d` and steel `#466f8d`; tomas-fe the +warm paper ground (`#f5f0eb` on `#242220`). Plume Studio now composes exactly those pieces — +lucide-react is a real dependency because it is literally their icon set. + +**What changed.** +- Every field's description and provenance moved behind a `CircleHelp` hover (a real ` ); @@ -256,35 +280,44 @@ export function App() { onSubmit={onSubmit} /> ) : ( - stage.sections.map((section) => ( -
-

{section.title}

- {section.note ?

{section.note}

: null} -
- {section.fields.map((path) => { - const spec = specs.get(path); - if (!spec) return null; - return ( - - ); - })} -
+
+
+ {stage.sections.map((section) => ( +
+

+ {section.title} + {section.note ? ( + {section.note} + ) : null} +

+
+ {section.fields.map((path) => { + const spec = specs.get(path); + if (!spec) return null; + return ( + + ); + })} +
+
+ ))} +
+
+ {StagePanel ? : null}
- )) +
)} - {StagePanel ? : null} - {error ?

{error}

: null}
diff --git a/studio/web/src/Field.tsx b/studio/web/src/Field.tsx index ef2821e..80f5737 100644 --- a/studio/web/src/Field.tsx +++ b/studio/web/src/Field.tsx @@ -13,6 +13,7 @@ */ import { useEffect, useRef, useState } from "react"; +import { HelpTip } from "./HelpTip"; import { type FieldSpec, coerce, display } from "./schema"; import type { StaleField } from "./types"; @@ -163,14 +164,19 @@ export function Field({ {spec.label} {spec.unit ? ({spec.unit}) : null} - {stateLabel ? {stateLabel} : null} + + {stateLabel ? {stateLabel} : null} + {spec.description || provenanceNote(spec) ? ( + + {spec.description ? {spec.description} : null} + {provenanceNote(spec)} + + ) : null} +
{control(spec, value, disabled, emit)} - {spec.description ?

{spec.description}

: null} -

{provenanceNote(spec)}

- {stale ? (

diff --git a/studio/web/src/HelpTip.tsx b/studio/web/src/HelpTip.tsx new file mode 100644 index 0000000..a4c8a90 --- /dev/null +++ b/studio/web/src/HelpTip.tsx @@ -0,0 +1,34 @@ +// Copyright (C) 2026 University Corporation for Atmospheric Research +// SPDX-License-Identifier: Apache-2.0 +/** + * The hover explanation: a CircleHelp icon that reveals the field's description and provenance. + * + * Requested in review: boxes should not carry walls of explanation text -- a question mark you can + * hover. The pattern (and the icon) come from the SAI simulator's ScenarioBuilder, which pairs + * every control with a zero-delay tooltip. + * + * A + + {children} + + + ); +} diff --git a/studio/web/src/panels.tsx b/studio/web/src/panels.tsx index 35472a9..9ee49fe 100644 --- a/studio/web/src/panels.tsx +++ b/studio/web/src/panels.tsx @@ -15,6 +15,7 @@ import { useEffect, useState } from "react"; import { Chart, type Series } from "./Chart"; +import { HelpTip } from "./HelpTip"; import { display, valueAt } from "./schema"; import { tickLabel } from "./scales"; @@ -61,14 +62,17 @@ function usePanel( function Frame({ title, hint, + help, loading, error, children, }: { title: string; - // `| undefined` rather than only `?`: exactOptionalPropertyTypes is on, so a caller passing - // `hint={maybeUndefined}` is different from a caller omitting it, and both are fine here. + // `hint` is VISIBLE and for data (totals, extremes -- numbers the reader scans for). `help` is + // the explanation, behind a CircleHelp hover per the design review: boxes hold controls and + // numbers, not paragraphs. `| undefined` because exactOptionalPropertyTypes is on. hint?: string | undefined; + help?: React.ReactNode; loading?: boolean | undefined; error?: string | undefined; children?: React.ReactNode; @@ -76,7 +80,10 @@ function Frame({ return (

-

{title}

+

+ {title} + {help ? {help} : null} +

{loading ? computing… : null}
{hint ?

{hint}

: null} @@ -116,10 +123,11 @@ export function SzaPanel({ config, load }: PanelProps) { hint={ daylight === null ? undefined - : `${daylight} h of daylight; minimum SZA ${tickLabel( + : `${daylight} h of daylight · minimum SZA ${tickLabel( typeof data?.min_sza_deg === "number" ? data.min_sza_deg : 0, - )}°. Shaded is night, where photolysis stops.` + )}°` } + help="Photolysis follows the sun: this is what the configured day and latitude mean for the chemistry. Shaded bands are night, where photolysis stops. The release marker shows the solar angle the plume first sees." loading={loading} {...(error ? { error } : {})} > @@ -164,8 +172,14 @@ export function ClimatologyPanel({ config, load }: PanelProps) { title="ERA5 temperature profile" hint={ data - ? `Zonal-mean monthly climatology (1991–2020), ${String(data.dataset_id)} · ` + - `${selected === "era5" ? "the marked point IS the run's temperature" : "context — the run uses the typed values (dataset: user)"}` + ? selected === "era5" + ? "the marked point IS the run's temperature" + : "context — this run uses the typed values (dataset: user)" + : undefined + } + help={ + data + ? `ERA5 zonal-mean monthly climatology, 1991–2020 (${String(data.dataset_id)}). The profile is drawn at this latitude and month; switch "Ambient state from" to era5 and the run derives its temperature and water vapour from it.` : undefined } loading={loading} @@ -213,7 +227,7 @@ export function ParcelPanel({ config }: PanelProps) { return (
@@ -257,11 +271,7 @@ export function ParcelPanel({ config }: PanelProps) { Given {specifiedBy.replace(/_/g, " ")}; the rest of the emission follows from it.

-

- t = 0 is the moment this volume is defined (SCIENCE-2, answered); how the parcel formed - is out of scope. The volume is a modelling choice — the next stage shows what it does to - the concentration. -

+
@@ -292,7 +302,7 @@ export function ConcentrationPanel({ config, load }: PanelProps) { return ( @@ -337,11 +347,10 @@ export function DilutionPanel({ config, load }: PanelProps) { title="Volume expansion V(t)/V₀" hint={ usesCurve - ? "Every regime drawn; the selected one is solid. The gap between D2 and D3 is a factor of three in dilution rate — easier to judge as two curves than as two names." - : `The CONSTANT regime ignores these curves and dilutes at a fixed ${display( - data?.constant_rate_per_s, - )} s⁻¹.` + ? undefined + : `CONSTANT regime: fixed ${display(data?.constant_rate_per_s)} s⁻¹ — these curves are not used.` } + help="Every regime is drawn; the selected one is solid. The gap between D2 and D3 is a factor of three in dilution rate — far easier to judge as two curves than as two names. Hover to read V/V₀ at a given time." loading={loading} {...(error ? { error } : {})} > @@ -373,10 +382,9 @@ export function SizeDistributionPanel({ config, load }: PanelProps) { hint={ total === null ? undefined - : `${tickLabel(total)} cm⁻³ total, seeded into ${display( - data?.n_bins, - )} bins. Dry diameters — SA and radius are wet.` + : `${tickLabel(total)} cm⁻³ total · ${display(data?.n_bins)} bins` } + help="The background distribution TOMAS is actually seeded with — the condensation sink the fresh plume competes against for H₂SO₄. Diameters are DRY; surface area and radius in results are wet." loading={loading} {...(error ? { error } : {})} > @@ -417,7 +425,7 @@ export function SpeciesPanel({ config }: PanelProps) { return (
{entries.map(([name, value]) => ( @@ -457,7 +465,7 @@ export function BinsPanel({ config, load }: PanelProps) { return ( diff --git a/studio/web/src/styles.css b/studio/web/src/styles.css index 9a44040..aff51cb 100644 --- a/studio/web/src/styles.css +++ b/studio/web/src/styles.css @@ -1,47 +1,64 @@ /* Copyright (C) 2026 University Corporation for Atmospheric Research SPDX-License-Identifier: Apache-2.0 - A dense instrument panel, not a marketing page: the reader is a scientist checking 42 values - before spending four minutes of compute. Three colours carry meaning and nothing else does -- - blue for the accent and the current stage, amber for an override the user pinned, red for a value - that no longer follows from its inputs. Everything else is ink on paper. */ + The Reflective look, applied to an instrument panel. Palette lifted from the family this tool + belongs to: tomas-fe's warm paper ground (#f5f0eb on #242220), reflective.org's deep navy + (#091834) for primary actions and headings, its golden accent (#f1b80d) for highlights and + overrides, steel blue (#466f8d) in support. Inter, as the SAI simulator uses. Explanations live + behind CircleHelp tooltips, not in the boxes -- the boxes hold controls and numbers. */ :root { - --surface: #ffffff; - --raised: #f7f8fa; - --sunken: #eef0f4; - --line: #d8dce4; - --line-strong: #b9c0cc; - --ink: #14181f; - --ink-2: #4a5260; - --ink-3: #79818f; - --accent: #2a78d6; - --accent-ink: #1b5ba8; - --override: #eda100; - --override-bg: #fff8e6; - --stale: #d43d2f; - --stale-bg: #fdf0ee; - --ok: #1baf7a; + --surface: #f5f0eb; + --raised: #fffdf9; + --sunken: #ece5db; + --line: #ded5c8; + --line-strong: #c3b7a5; + --ink: #242220; + --ink-2: #57534c; + --ink-3: #8a847a; + --navy: #091834; + --navy-2: #1d3557; + --steel: #466f8d; + --accent: #2a6bb8; + --accent-ink: #1b4f8f; + --gold: #dfa30b; + --gold-bg: #fdf4dc; + --stale: #c93a2c; + --stale-bg: #fbeeec; + --ok: #1a9e6f; + --tip-bg: #242220; + --tip-ink: #f5f0eb; + --tip-muted: #b8b0a4; --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace; + --sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, system-ui, sans-serif; + --radius: 10px; + --shadow: 0 1px 2px rgba(36, 34, 32, 0.06), 0 4px 14px rgba(36, 34, 32, 0.05); } @media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) { - --surface: #14171c; - --raised: #1b1f26; - --sunken: #23282f; - --line: #2f353e; - --line-strong: #454d59; - --ink: #e8ebf0; - --ink-2: #aeb6c2; - --ink-3: #7d8694; + --surface: #0c1424; + --raised: #14203a; + --sunken: #1b2947; + --line: #263654; + --line-strong: #3c4f74; + --ink: #e9ecf3; + --ink-2: #b3bccd; + --ink-3: #7e8aa3; + --navy: #d3e4f6; + --navy-2: #a9c6e8; + --steel: #7fa6c4; --accent: #5b9ce8; --accent-ink: #8dbcf2; - --override: #e8b13b; - --override-bg: #2b2413; + --gold: #f1b80d; + --gold-bg: #2b2413; --stale: #f0705f; --stale-bg: #2d1a17; --ok: #3fca97; + --tip-bg: #f5f0eb; + --tip-ink: #242220; + --tip-muted: #57534c; + --shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 4px 14px rgba(0, 0, 0, 0.3); } } @@ -53,17 +70,17 @@ body { margin: 0; background: var(--surface); color: var(--ink); - font: 14px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif; + font: 14px/1.5 var(--sans); } main { - max-width: 1240px; + max-width: 1400px; margin: 0 auto; - padding: 20px 24px 64px; + padding: 18px 28px 64px; display: grid; - grid-template-columns: minmax(0, 1fr) 260px; + grid-template-columns: minmax(0, 1fr) 250px; grid-template-areas: "header header" "nav nav" "stage runs"; - gap: 0 28px; + gap: 0 26px; } main.loading, @@ -73,34 +90,41 @@ main.fatal { color: var(--ink-2); } +/* ---- header ------------------------------------------------------------------------------ */ + header { grid-area: header; - border-bottom: 1px solid var(--line); - padding-bottom: 12px; + display: flex; + align-items: baseline; + gap: 12px; + border-bottom: 2px solid var(--navy); + padding-bottom: 10px; margin-bottom: 14px; } h1 { - font-size: 19px; - margin: 0 0 3px; - letter-spacing: -0.01em; + font-size: 18px; + font-weight: 700; + letter-spacing: -0.02em; + margin: 0; + color: var(--navy); } .sub { margin: 0; color: var(--ink-3); - font-size: 12.5px; + font-size: 12px; } .header-stale { color: var(--stale); font-weight: 600; } -/* ---- stage navigation ------------------------------------------------------------------ */ +/* ---- stage stepper ----------------------------------------------------------------------- */ nav.stages { grid-area: nav; display: flex; - gap: 4px; - margin-bottom: 22px; + gap: 6px; + margin-bottom: 18px; flex-wrap: wrap; } @@ -108,43 +132,45 @@ nav.stages { display: flex; align-items: center; gap: 7px; - padding: 7px 11px 7px 8px; + padding: 8px 13px 8px 10px; background: var(--raised); border: 1px solid var(--line); - border-radius: 6px; + border-radius: 999px; color: var(--ink-2); - font-size: 12.5px; + font: 500 12.5px/1 var(--sans); cursor: pointer; - font-family: inherit; + box-shadow: var(--shadow); + transition: border-color 120ms, color 120ms; } .stage-tab:hover { - border-color: var(--line-strong); + border-color: var(--steel); color: var(--ink); } .stage-tab .num { display: grid; place-items: center; - width: 19px; - height: 19px; + width: 22px; + height: 22px; border-radius: 50%; background: var(--sunken); - color: var(--ink-3); - font-size: 11px; - font-weight: 700; - font-variant-numeric: tabular-nums; + color: var(--ink-2); } .stage-tab.current { - background: var(--accent); - border-color: var(--accent); - color: #fff; + background: var(--navy); + border-color: var(--navy); + color: var(--surface); font-weight: 600; } .stage-tab.current .num { - background: rgba(255, 255, 255, 0.24); - color: #fff; + background: rgba(255, 255, 255, 0.18); + color: inherit; +} +@media (prefers-color-scheme: dark) { + :root:not([data-theme="light"]) .stage-tab.current { + color: #0c1424; + } } -/* A stale field on a stage the user is not looking at is exactly the case spec section 8 calls - "unmissable", so the marker lives in the navigation, not only on the field. */ +/* Stale on a stage the user is not looking at must be unmissable (spec section 8). */ .stage-tab.has-stale { border-color: var(--stale); color: var(--stale); @@ -153,85 +179,95 @@ nav.stages { background: var(--stale); color: #fff; } -/* Both at once: `current` paints the background blue and `has-stale` painted the text red, which - left red-on-blue and a label you could not read. The stale state wins the whole tab, since it is - the more urgent of the two things being said. */ .stage-tab.current.has-stale { background: var(--stale); border-color: var(--stale); color: #fff; - font-weight: 600; -} -.stage-tab.current.has-stale .num { - background: rgba(255, 255, 255, 0.24); - color: #fff; } -/* ---- stage body ------------------------------------------------------------------------ */ +/* ---- stage body -------------------------------------------------------------------------- */ section.stage { grid-area: stage; min-width: 0; } .stage-head { - margin-bottom: 20px; + margin-bottom: 14px; } .stage-head h2 { font-size: 16px; - margin: 0 0 4px; + font-weight: 700; + color: var(--navy); + margin: 0 0 3px; } .blurb { - margin: 0 0 5px; + margin: 0 0 3px; color: var(--ink-2); - max-width: 68ch; + font-size: 13px; + max-width: 72ch; } .refs { margin: 0; - font-size: 11.5px; + font-size: 11px; color: var(--ink-3); font-family: var(--mono); } .blocked-on { - color: var(--override); + color: var(--gold); font-weight: 600; } +/* Controls on the left, graphs on the right -- the simulator's builder layout. */ +.stage-grid { + display: grid; + grid-template-columns: minmax(320px, 420px) minmax(0, 1fr); + gap: 22px; + align-items: start; +} +.viz { + position: sticky; + top: 14px; + min-width: 0; +} +.viz .panel:first-child { + margin-top: 0; + padding-top: 0; + border-top: 0; +} + .section { - margin-bottom: 26px; + margin-bottom: 18px; } .section h3 { + display: flex; + align-items: center; + gap: 6px; font-size: 11px; + font-weight: 600; text-transform: uppercase; - letter-spacing: 0.07em; - color: var(--ink-3); - margin: 0 0 6px; - padding-bottom: 5px; - border-bottom: 1px solid var(--line); -} -.note { - margin: 0 0 12px; - font-size: 12px; - color: var(--ink-2); - max-width: 76ch; + letter-spacing: 0.08em; + color: var(--steel); + margin: 0 0 8px; } - .fields { display: grid; - grid-template-columns: repeat(auto-fill, minmax(268px, 1fr)); - gap: 14px; + grid-template-columns: 1fr; + gap: 10px; } -/* ---- one field ------------------------------------------------------------------------- */ +/* ---- one field --------------------------------------------------------------------------- */ .field { background: var(--raised); border: 1px solid var(--line); - border-radius: 7px; - padding: 10px 12px 11px; + border-radius: var(--radius); + padding: 10px 12px; min-width: 0; + box-shadow: var(--shadow); } .field-derived { background: var(--sunken); + box-shadow: none; } .field-stale { border-color: var(--stale); @@ -240,29 +276,34 @@ section.stage { .field-head { display: flex; - align-items: baseline; + align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 6px; } +.field-head-right { + display: inline-flex; + align-items: center; + gap: 6px; +} .field label { font-weight: 600; - font-size: 13px; + font-size: 12.5px; } .unit { color: var(--ink-3); font-weight: 400; font-family: var(--mono); - font-size: 11.5px; + font-size: 11px; } .badge { - font-size: 9.5px; + font-size: 9px; text-transform: uppercase; - letter-spacing: 0.06em; + letter-spacing: 0.07em; font-weight: 700; - padding: 2px 5px; - border-radius: 3px; + padding: 2px 6px; + border-radius: 999px; white-space: nowrap; } .badge-auto { @@ -270,31 +311,89 @@ section.stage { color: var(--ink-3); } .badge-override { - background: var(--override-bg); - color: var(--override); - border: 1px solid var(--override); + background: var(--gold-bg); + color: var(--gold); + border: 1px solid var(--gold); } .badge-stale { background: var(--stale); color: #fff; } +/* ---- the help tooltip -------------------------------------------------------------------- */ + +.helptip { + position: relative; + display: inline-flex; +} +.helptip-trigger { + display: inline-flex; + align-items: center; + padding: 2px; + margin: 0; + border: 0; + background: none; + color: var(--ink-3); + cursor: help; + border-radius: 50%; +} +.helptip-trigger:hover, +.helptip-trigger:focus-visible { + color: var(--steel); + outline: none; +} +.helptip-body { + display: none; + position: absolute; + top: calc(100% + 8px); + right: -10px; + width: 300px; + background: var(--tip-bg); + color: var(--tip-ink); + border-radius: 8px; + padding: 10px 12px; + font-size: 12px; + font-weight: 400; + line-height: 1.5; + text-transform: none; + letter-spacing: normal; + box-shadow: 0 6px 24px rgba(0, 0, 0, 0.25); + z-index: 40; +} +.helptip:hover .helptip-body, +.helptip-trigger:focus-visible + .helptip-body { + display: block; +} +.tip-desc { + display: block; + margin-bottom: 7px; +} +.tip-prov { + display: block; + font-family: var(--mono); + font-size: 10.5px; + color: var(--tip-muted); + overflow-wrap: anywhere; +} + +/* ---- controls ---------------------------------------------------------------------------- */ + .field input[type="number"], .field input[type="text"], .field select { width: 100%; - padding: 6px 8px; + padding: 7px 9px; border: 1px solid var(--line-strong); - border-radius: 5px; - background: var(--surface); + border-radius: 7px; + background: var(--raised); color: var(--ink); font: 13px var(--mono); } .field input:focus, .field select:focus { - outline: 2px solid var(--accent); + outline: 2px solid var(--steel); outline-offset: -1px; - border-color: var(--accent); + border-color: var(--steel); } .field input:disabled, .field select:disabled { @@ -305,58 +404,67 @@ section.stage { .switch { display: flex; align-items: center; - gap: 7px; + gap: 8px; font-family: var(--mono); font-size: 12.5px; color: var(--ink-2); } .switch input { - width: 15px; - height: 15px; - accent-color: var(--accent); + width: 16px; + height: 16px; + accent-color: var(--steel); } .fixed-value { display: flex; align-items: center; gap: 8px; - padding: 5px 8px; + padding: 6px 9px; background: var(--sunken); border: 1px dashed var(--line-strong); - border-radius: 5px; + border-radius: 7px; } .fixed-value code { font-family: var(--mono); font-size: 12.5px; } .fixed-why { - font-size: 10.5px; + font-size: 10px; color: var(--ink-3); text-transform: uppercase; letter-spacing: 0.05em; } -.field-desc { - margin: 7px 0 0; - font-size: 11.5px; - color: var(--ink-2); +.draft { + position: relative; } -.field-prov { - margin: 5px 0 0; - font-size: 10.5px; - color: var(--ink-3); - font-family: var(--mono); - overflow-wrap: anywhere; +.draft-hint { + position: absolute; + right: 7px; + top: 50%; + transform: translateY(-50%); + font-size: 9px; + text-transform: uppercase; + letter-spacing: 0.05em; + font-weight: 600; + color: var(--steel); + background: var(--raised); + padding: 2px 5px; + border-radius: 4px; + pointer-events: none; +} +.draft input { + width: 100%; } -/* ---- the stale decision ---------------------------------------------------------------- */ +/* ---- the stale decision ------------------------------------------------------------------ */ .stale-box { margin-top: 9px; padding: 9px 10px; - background: var(--surface); + background: var(--raised); border: 1px solid var(--stale); - border-radius: 6px; + border-radius: 8px; } .stale-box p { margin: 0 0 5px; @@ -374,21 +482,21 @@ section.stage { margin-top: 8px; } -/* ---- buttons --------------------------------------------------------------------------- */ +/* ---- buttons ----------------------------------------------------------------------------- */ button { - font: 500 12.5px/1 inherit; - font-family: inherit; - padding: 8px 13px; - border-radius: 6px; - border: 1px solid var(--accent); - background: var(--accent); - color: #fff; + font: 600 12.5px/1 var(--sans); + padding: 9px 15px; + border-radius: 8px; + border: 1px solid var(--navy); + background: var(--navy); + color: var(--surface); cursor: pointer; + transition: background 120ms, border-color 120ms; } button:hover:not(:disabled) { - background: var(--accent-ink); - border-color: var(--accent-ink); + background: var(--navy-2); + border-color: var(--navy-2); } button:disabled { opacity: 0.45; @@ -398,27 +506,61 @@ button.ghost { background: transparent; color: var(--ink-2); border-color: var(--line-strong); + font-weight: 500; } button.ghost:hover:not(:disabled) { background: var(--raised); color: var(--ink); - border-color: var(--line-strong); + border-color: var(--steel); } button.primary { - padding: 9px 18px; - font-weight: 600; + padding: 10px 20px; +} +@media (prefers-color-scheme: dark) { + /* Solid buttons in dark are light-filled, so their text is the dark surface colour. The rule is + an element selector inside :root:not(...), which OUTRANKS single-class rules -- so every + button-shaped thing that is not a solid button must be re-stated here, or it silently renders + dark-on-dark. That is exactly how the stage tabs became illegible on first render. */ + :root:not([data-theme="light"]) button { + color: #0c1424; + } + :root:not([data-theme="light"]) button.ghost { + color: var(--ink-2); + } + :root:not([data-theme="light"]) button.ghost:hover:not(:disabled) { + color: var(--ink); + } + :root:not([data-theme="light"]) .stage-tab { + color: var(--ink-2); + } + :root:not([data-theme="light"]) .stage-tab:hover { + color: var(--ink); + } + :root:not([data-theme="light"]) .stage-tab.has-stale { + color: var(--stale); + } + :root:not([data-theme="light"]) .stage-tab.current.has-stale { + color: #fff; + } + :root:not([data-theme="light"]) .helptip-trigger { + color: var(--ink-3); + } + :root:not([data-theme="light"]) .helptip-trigger:hover, + :root:not([data-theme="light"]) .helptip-trigger:focus-visible { + color: var(--steel); + } } .nav-row { display: flex; align-items: center; justify-content: space-between; - margin-top: 26px; + margin-top: 22px; padding-top: 14px; border-top: 1px solid var(--line); } .progress { - font-size: 11.5px; + font-size: 11px; color: var(--ink-3); font-family: var(--mono); } @@ -428,42 +570,44 @@ button.primary { padding: 9px 11px; background: var(--stale-bg); border: 1px solid var(--stale); - border-radius: 6px; + border-radius: 8px; color: var(--stale); font-size: 12px; font-family: var(--mono); white-space: pre-wrap; } -/* ---- review ---------------------------------------------------------------------------- */ +/* ---- review ------------------------------------------------------------------------------ */ .review-summary { display: flex; - gap: 32px; - padding: 14px 16px; + gap: 34px; + padding: 16px 18px; background: var(--raised); border: 1px solid var(--line); - border-radius: 7px; + border-radius: var(--radius); + box-shadow: var(--shadow); margin-bottom: 14px; } .review-summary > div { display: flex; flex-direction: column; - gap: 2px; + gap: 3px; } .big { - font-size: 22px; + font-size: 24px; font-weight: 700; line-height: 1; font-variant-numeric: tabular-nums; + color: var(--navy); } .big.ok { color: var(--ok); - font-size: 17px; + font-size: 18px; } .big.bad { color: var(--stale); - font-size: 17px; + font-size: 18px; } .small { font-size: 11px; @@ -488,8 +632,8 @@ table.diff th { text-align: left; font-size: 10px; text-transform: uppercase; - letter-spacing: 0.06em; - color: var(--ink-3); + letter-spacing: 0.07em; + color: var(--steel); border-bottom: 1px solid var(--line); padding: 5px 8px; } @@ -542,10 +686,10 @@ td.now { } .submit-row input { flex: 1; - padding: 8px 10px; + padding: 9px 11px; border: 1px solid var(--line-strong); - border-radius: 5px; - background: var(--surface); + border-radius: 7px; + background: var(--raised); color: var(--ink); font-size: 13px; } @@ -556,7 +700,7 @@ td.now { max-width: 76ch; } -/* ---- runs rail ------------------------------------------------------------------------- */ +/* ---- runs rail --------------------------------------------------------------------------- */ aside.runs { grid-area: runs; @@ -564,12 +708,11 @@ aside.runs { } aside.runs h3 { font-size: 11px; + font-weight: 600; text-transform: uppercase; - letter-spacing: 0.07em; - color: var(--ink-3); + letter-spacing: 0.08em; + color: var(--steel); margin: 0 0 8px; - padding-bottom: 5px; - border-bottom: 1px solid var(--line); } aside.runs ul { list-style: none; @@ -577,25 +720,26 @@ aside.runs ul { padding: 0; display: flex; flex-direction: column; - gap: 7px; + gap: 8px; } aside.runs li { display: flex; flex-direction: column; gap: 2px; - padding: 8px 10px; + padding: 9px 11px; background: var(--raised); border: 1px solid var(--line); - border-radius: 6px; + border-radius: var(--radius); + box-shadow: var(--shadow); font-size: 12px; } .run-label { font-weight: 600; } .run-state { - font-size: 10px; + font-size: 9.5px; text-transform: uppercase; - letter-spacing: 0.06em; + letter-spacing: 0.07em; font-weight: 700; color: var(--ink-3); } @@ -615,26 +759,14 @@ aside.runs li { } .dirty { font-size: 10px; - color: var(--override); + color: var(--gold); font-weight: 600; } -@media (max-width: 940px) { - main { - grid-template-columns: minmax(0, 1fr); - grid-template-areas: "header" "nav" "stage" "runs"; - } - aside.runs { - margin-top: 30px; - } -} - -/* ---- preview panels -------------------------------------------------------------------- */ +/* ---- preview panels ---------------------------------------------------------------------- */ .panel { - margin-top: 26px; - padding-top: 16px; - border-top: 1px solid var(--line); + margin-top: 20px; } .panel-head { display: flex; @@ -643,31 +775,31 @@ aside.runs li { } .panel-head h3 { font-size: 11px; + font-weight: 600; text-transform: uppercase; - letter-spacing: 0.07em; - color: var(--ink-3); + letter-spacing: 0.08em; + color: var(--steel); margin: 0 0 6px; - border: 0; - padding: 0; } .panel-status { - font-size: 10.5px; + font-size: 10px; color: var(--ink-3); font-family: var(--mono); } .panel-hint { - margin: 0 0 10px; - font-size: 12px; - color: var(--ink-2); - max-width: 78ch; + margin: 0 0 8px; + font-size: 11.5px; + color: var(--ink-3); + max-width: 80ch; } figure.chart { margin: 0; background: var(--raised); border: 1px solid var(--line); - border-radius: 7px; - padding: 10px 12px 6px; + border-radius: var(--radius); + box-shadow: var(--shadow); + padding: 12px 14px 6px; } figure.chart svg { width: 100%; @@ -678,11 +810,10 @@ figure.chart svg { figure.chart figcaption { font-size: 11px; color: var(--ink-3); - padding: 4px 2px 4px; + padding: 4px 2px; min-height: 1.4em; } -/* Grid and axes stay recessive: they are scaffolding, not data. */ .chart-grid { stroke: var(--line); stroke-width: 1; @@ -700,7 +831,6 @@ figure.chart figcaption { fill: var(--ink-3); font-size: 10.5px; } - .chart-line { fill: none; stroke: var(--accent); @@ -715,7 +845,6 @@ figure.chart figcaption { .chart-line.dashed { stroke-dasharray: 5 4; } - .chart-series-label { fill: var(--accent-ink); font-size: 10.5px; @@ -725,8 +854,6 @@ figure.chart figcaption { fill: var(--ink-3); font-weight: 400; } - -/* The band is the only fill on the chart, so it must sit clearly behind the line. */ .chart-band { fill: var(--sunken); } @@ -736,16 +863,15 @@ figure.chart figcaption { stroke-dasharray: 3 3; } .chart-marker { - fill: var(--override); + fill: var(--gold); stroke: var(--raised); stroke-width: 2; } .chart-marker-label { - fill: var(--override); + fill: var(--gold); font-size: 10px; font-weight: 700; } - .chart-readout { display: flex; gap: 14px; @@ -768,13 +894,13 @@ figure.chart figcaption { padding: 9px 11px; background: var(--stale-bg); border: 1px solid var(--stale); - border-radius: 6px; + border-radius: 8px; color: var(--stale); font-size: 12px; font-family: var(--mono); } -/* ---- parcel diagram -------------------------------------------------------------------- */ +/* ---- parcel diagram ---------------------------------------------------------------------- */ .parcel { display: flex; @@ -782,18 +908,19 @@ figure.chart figcaption { align-items: center; background: var(--raised); border: 1px solid var(--line); - border-radius: 7px; - padding: 12px 14px; + border-radius: var(--radius); + box-shadow: var(--shadow); + padding: 14px 16px; flex-wrap: wrap; } .parcel svg { - width: 320px; + width: 300px; max-width: 100%; height: auto; } .parcel-box { fill: var(--sunken); - stroke: var(--accent); + stroke: var(--steel); stroke-width: 2; } .parcel-dim, @@ -830,16 +957,17 @@ figure.chart figcaption { color: var(--ink-2); } -/* ---- species bars ---------------------------------------------------------------------- */ +/* ---- species bars ------------------------------------------------------------------------ */ .bars { background: var(--raised); border: 1px solid var(--line); - border-radius: 7px; - padding: 10px 14px; + border-radius: var(--radius); + box-shadow: var(--shadow); + padding: 12px 14px; display: flex; flex-direction: column; - gap: 5px; + gap: 6px; } .bar-row { display: grid; @@ -860,7 +988,7 @@ figure.chart figcaption { height: 12px; } .bar-fill { - background: var(--accent); + background: var(--steel); height: 100%; border-radius: 3px; } @@ -870,25 +998,22 @@ figure.chart figcaption { text-align: right; } -/* A text field holds a draft until Enter or blur, so a five-digit number is one edit and not five - (and does not lose focus after the first keystroke). */ -.draft { - position: relative; -} -.draft-hint { - position: absolute; - right: 6px; - top: 50%; - transform: translateY(-50%); - font-size: 9.5px; - text-transform: uppercase; - letter-spacing: 0.05em; - color: var(--accent); - background: var(--surface); - padding: 2px 4px; - border-radius: 3px; - pointer-events: none; +/* ---- responsive -------------------------------------------------------------------------- */ + +@media (max-width: 1080px) { + .stage-grid { + grid-template-columns: minmax(0, 1fr); + } + .viz { + position: static; + } } -.draft input { - width: 100%; +@media (max-width: 940px) { + main { + grid-template-columns: minmax(0, 1fr); + grid-template-areas: "header" "nav" "stage" "runs"; + } + aside.runs { + margin-top: 30px; + } }