Files
pansy/web/src/styles/index.css
T
steveandClaude Fable 5 27f658c1f7
Build image / build-and-push (push) Successful in 2m55s
Gadfly review (reusable) / review (pull_request) Successful in 8m59s
Adversarial Review (Gadfly) / review (pull_request) Successful in 8m59s
Smoke-sweep fixes: exact saves, local dates, safer remove, readable markers
- Garden and plant dialogs keep centimeters as the source of truth
  (LengthField in lib/units.ts): a no-change Save no longer rewrites
  900 cm as 899.922 or a 45 cm spacing as 44.958, bumping versions and
  writing bogus history entries on the way.
- The UI stamps every date with the browser's local day (lib/dates.ts).
  Journal notes already did; plop placement, fill and removal now do too,
  so a 9 pm placement isn't "planted tomorrow". The fill endpoint gained an
  optional plantedAt; API and agent callers still default to UTC today.
- Removing an object that holds plants asks first and says how many go
  with it. An empty one still goes straight away (one Undo restores it).
- The expanded plant card's action row wraps instead of clipping "Delete".
- Monogram lettering switches to a dark ink on pale marker colors (garlic,
  cabbage, marigold) instead of near-white on near-white.
- Copy-as-plan proposes the next free year and warns when the typed name
  already exists, so two gardens can't both read as "the 2027 plan".
- Plan cards show the base name with a "2027 plan" tag, so the year — the
  point of the name — survives truncation.
- A rejected model spec now says which model and why: a wrapped
  ErrInvalidInput's reason reaches the client as the 400's message, and the
  Settings field shows it inline instead of toasting "invalid input".

Also defuses a clock bomb in TestRemainingReturnsWhenAPlantingIsRemoved,
which only passed while the real date was before 2026-08-01.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-22 22:11:12 -04:00

467 lines
12 KiB
CSS

@import 'tailwindcss';
/* ─────────────────────────────────────────────────────────────────────────────
"Organic" — the design tokens from docs/design_handoff_pansy_ui (its
_ds/…/styles.css is the source of truth; these are the same values under the
same names). Tailwind utilities reference these variables by name, and dark
mode is done by overriding the SAME variables on <html> from the theme script
in index.html — so there is exactly one set of names, no second stylesheet,
and no class swapping. Every namespace is reset first so the only colors,
fonts and radii that exist in this app are the design's.
───────────────────────────────────────────────────────────────────────────── */
@theme {
--color-*: initial;
--font-*: initial;
--shadow-*: initial;
--radius-*: initial;
--color-bg: #f5ead8;
--color-surface: #ebddc5;
--color-text: #201e1d;
--color-accent: #c67139;
--color-accent-2: #7a8a5e;
--color-divider: color-mix(in srgb, #201e1d 16%, transparent);
/* Monogram lettering on plant markers: paper on dark colors, marker-ink on
pale ones (lib/monogram.ts picks). Neither changes between modes — the
marker's own color doesn't either. */
--color-paper: #fffaf1;
--color-marker-ink: #201e1d;
--color-neutral-100: #f9f4ed;
--color-neutral-200: #eee7db;
--color-neutral-300: #dcd3c4;
--color-neutral-400: #c0b6a5;
--color-neutral-500: #a19786;
--color-neutral-600: #82796a;
--color-neutral-700: #645c50;
--color-neutral-800: #474238;
--color-neutral-900: #2e2b25;
--color-accent-100: #fff2eb;
--color-accent-200: #ffe1d0;
--color-accent-300: #ffc6a5;
--color-accent-400: #f6a06b;
--color-accent-500: #d67f48;
--color-accent-600: #b2622d;
--color-accent-700: #8c491a;
--color-accent-800: #643312;
--color-accent-900: #402310;
--color-accent-2-100: #f0fae1;
--color-accent-2-200: #e1eecc;
--color-accent-2-300: #ccdbb2;
--color-accent-2-400: #aebf92;
--color-accent-2-500: #8fa073;
--color-accent-2-600: #728157;
--color-accent-2-700: #56633f;
--color-accent-2-800: #3d472b;
--color-accent-2-900: #272e1b;
/* Ink + canvas roles alias the --p-* properties declared on :root below (the
names the theme script overrides), so text-ink-mute etc. flip with the
theme as well. */
--color-ink-strong: var(--p-ink-strong);
--color-ink-soft: var(--p-ink-soft);
--color-ink-mute: var(--p-ink-mute);
--color-field: var(--p-field);
--font-heading: 'Caprasimo', system-ui, sans-serif;
--font-body: 'Figtree', system-ui, sans-serif;
--radius-sm: 8px;
--radius-md: 16px;
--radius-xl: 22px;
--radius-lg: 28px;
}
@layer base {
:root {
/* Canvas + ink tokens (light values). Dark values live in index.html. */
--p-field: #efe3c9;
--p-grid-ink: #201e1d;
--p-ink-strong: #474238;
--p-ink-soft: #645c50;
--p-ink-mute: #82796a;
--p-bed-fill: #e3d0ac;
--p-bed-stroke: #b5a37f;
--p-ing-fill: #d6bf98;
--p-ing-stroke: #b1a07c;
--p-path-fill: #ece1cb;
--p-path-stroke: #cabfa6;
--p-bag-fill: #d8c5a2;
--p-bag-stroke: #a99677;
--p-bkt-fill: #cfc3ad;
--p-bkt-stroke: #9a8d76;
--p-tree-fill: #dce9c6;
--p-tree-stroke: #8fa073;
--p-str-fill: #d9cfbc;
--p-str-stroke: #a3947c;
/* Elevation — ink-tinted on the light ground (the theme script swaps in
ambient darkness). Plain custom properties rather than Tailwind's shadow
namespace, which inlines its values and so could not follow the override:
use the .elev-* classes below. */
--shadow-sm: 0 1px 2px color-mix(in srgb, #2e2b25 14%, transparent);
--shadow-md: 0 3px 10px color-mix(in srgb, #2e2b25 16%, transparent);
--shadow-lg: 0 12px 32px color-mix(in srgb, #2e2b25 22%, transparent);
}
html,
body,
#root {
height: 100%;
}
body {
margin: 0;
background: var(--color-bg);
color: var(--color-text);
font-family: var(--font-body);
font-size: 15px;
line-height: 1.55;
font-weight: 400;
-webkit-font-smoothing: antialiased;
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: var(--font-heading);
font-weight: 400;
line-height: 1.12;
letter-spacing: -0.015em;
margin: 0;
}
h1 {
font-size: 42px;
}
h2 {
font-size: 32px;
}
h3 {
font-size: 25px;
}
h4 {
font-size: 20px;
}
h5 {
font-size: 16px;
}
h6 {
font-size: 13px;
letter-spacing: 0.08em;
text-transform: uppercase;
}
a {
color: var(--color-accent);
text-underline-offset: 3px;
}
a:hover {
color: var(--color-accent-600);
}
button {
cursor: pointer;
font: inherit;
color: inherit;
}
button:disabled {
cursor: not-allowed;
}
:focus {
outline: none;
}
:focus-visible {
outline: 2px solid var(--color-accent);
outline-offset: 2px;
}
::selection {
background: color-mix(in srgb, var(--color-accent) 30%, transparent);
}
::-webkit-scrollbar {
height: 6px;
width: 6px;
}
::-webkit-scrollbar-thumb {
background: var(--color-neutral-400);
border-radius: 99px;
}
::-webkit-scrollbar-track {
background: transparent;
}
}
/* ── Components — the handful of classes the prototypes lean on ─────────── */
@layer components {
.elev-sm {
box-shadow: var(--shadow-sm);
}
.elev-md {
box-shadow: var(--shadow-md);
}
.elev-lg {
box-shadow: var(--shadow-lg);
}
/* Buttons: every control is a pill; labels are set in the display face. */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
cursor: pointer;
text-decoration: none;
font-family: var(--font-heading);
font-weight: 400;
font-size: 14px;
line-height: 1.2;
color: var(--color-text);
background: transparent;
border: 1px solid transparent;
padding: 8.8px 15.84px;
border-radius: 999px;
white-space: nowrap;
}
.btn svg {
display: block;
flex: none;
}
.btn:disabled {
opacity: 0.45;
cursor: not-allowed;
}
.btn-primary {
background: var(--color-accent);
color: var(--color-bg);
}
.btn-primary:hover:not(:disabled) {
background: var(--color-accent-600);
}
.btn-primary:active:not(:disabled) {
background: var(--color-accent-700);
}
.btn-secondary {
border-color: var(--color-divider);
}
.btn-secondary:hover:not(:disabled) {
background: color-mix(in srgb, var(--color-text) 7%, transparent);
}
.btn-secondary:active:not(:disabled) {
background: color-mix(in srgb, var(--color-text) 14%, transparent);
}
.btn-ghost {
color: var(--color-accent);
padding-inline: 4.4px;
}
.btn-ghost:hover:not(:disabled) {
background: color-mix(in srgb, var(--color-accent) 10%, transparent);
}
.btn-ghost:active:not(:disabled) {
background: color-mix(in srgb, var(--color-accent) 18%, transparent);
}
.btn-icon {
width: 36px;
height: 36px;
padding: 0;
flex: none;
}
.btn-block {
width: 100%;
}
/* A bare icon button on a surface (zoom pill, peek close): no border, a soft
accent wash on hover. */
.btn-soft:hover:not(:disabled) {
background: var(--color-accent-100);
}
/* Forms */
.field > label {
display: block;
font-size: 12px;
margin-bottom: 5px;
color: color-mix(in srgb, var(--color-text) 70%, transparent);
}
.input {
width: 100%;
min-height: 36px;
padding: 6px 14px;
font: inherit;
font-size: 14px;
color: var(--color-text);
caret-color: var(--color-accent);
background: var(--color-surface);
border: 1px solid var(--color-divider);
border-radius: 999px;
}
.input::placeholder {
color: var(--p-ink-mute);
opacity: 1;
}
.input:hover {
border-color: color-mix(in srgb, var(--color-text) 45%, transparent);
}
.input:focus-visible {
border-color: var(--color-accent);
outline-offset: 0;
}
.input:disabled,
.input[readonly] {
cursor: default;
}
.input:disabled {
opacity: 0.7;
}
textarea.input {
min-height: 90px;
resize: vertical;
border-radius: var(--radius-md);
}
/* Phone: 16px stops iOS Safari zooming the page on focus. */
.input-lg {
font-size: 16px;
min-height: 44px;
}
/* Tags */
.tag {
display: inline-flex;
align-items: center;
font-size: 11px;
letter-spacing: 0.02em;
padding: 3px 10px;
border-radius: 999px;
white-space: nowrap;
font-weight: 400;
}
.tag-accent {
background: var(--color-accent-100);
color: var(--color-accent-800);
}
.tag-accent-2 {
background: var(--color-accent-2-100);
color: var(--color-accent-2-800);
}
.tag-neutral {
background: var(--color-neutral-100);
color: var(--color-neutral-800);
}
.tag-outline {
border: 1px solid var(--color-accent);
color: var(--color-accent);
}
/* The raised panel every page builds from: cards, the editor's three regions. */
.panel {
background: var(--color-neutral-100);
border: 1px solid var(--color-divider);
border-radius: var(--radius-lg);
}
/* Segmented pill control: a neutral-200 track, the active option lifts. */
.seg {
display: inline-flex;
background: var(--color-neutral-200);
border-radius: 999px;
padding: 3px;
}
.seg-opt {
padding: 5px 16px;
cursor: pointer;
border: none;
border-radius: 999px;
white-space: nowrap;
font-family: var(--font-body);
font-size: 12.5px;
font-weight: 700;
line-height: 1.55;
background: transparent;
color: var(--p-ink-soft);
}
.seg-opt[aria-pressed='true'] {
background: var(--color-neutral-100);
color: var(--color-text);
box-shadow: var(--shadow-sm);
}
.seg-opt:disabled {
cursor: default;
}
/* On/Off pill */
.toggle {
cursor: pointer;
border: none;
border-radius: 999px;
padding: 6px 18px;
font-family: var(--font-body);
font-size: 12.5px;
font-weight: 700;
line-height: 1.55;
background: var(--color-neutral-300);
color: var(--p-ink-soft);
}
.toggle[aria-pressed='true'] {
background: var(--color-accent-2-300);
color: var(--color-accent-2-800);
}
.toggle:disabled {
cursor: default;
opacity: 0.8;
}
/* Chips: category filters, the phone tool strip, rail tabs share this shape. */
.chip {
cursor: pointer;
border: 1px solid var(--color-divider);
border-radius: 999px;
padding: 7px 16px;
font-family: var(--font-body);
font-size: 13px;
font-weight: 700;
line-height: 1.55;
background: transparent;
color: var(--p-ink-soft);
white-space: nowrap;
}
.chip[aria-pressed='true'],
.chip-active {
background: var(--color-accent-200);
border-color: var(--color-accent-400);
color: var(--color-accent-800);
}
/* Dialogs */
.dialog-backdrop {
position: fixed;
inset: 0;
background: color-mix(in srgb, #201e1d 40%, transparent);
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
z-index: 50;
}
.dialog {
background: var(--color-neutral-100);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-lg);
padding: 26px;
width: min(420px, 100%);
max-height: calc(100dvh - 40px);
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 14px;
}
.hr {
height: 1px;
border: 0;
background: var(--color-divider);
}
/* The editor's camera move: fit/focus animate, drags and zooms don't. */
.camera-anim {
transition: transform 0.48s cubic-bezier(0.22, 0.85, 0.3, 1);
}
}