- 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]>
467 lines
12 KiB
CSS
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);
|
|
}
|
|
}
|