diff --git a/db/migrations/014_plan_revisions.sql b/db/migrations/014_plan_revisions.sql new file mode 100644 index 0000000..971dc00 --- /dev/null +++ b/db/migrations/014_plan_revisions.sql @@ -0,0 +1,12 @@ +CREATE TABLE plan_revisions ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, + plan_id uuid NOT NULL REFERENCES roast_plans(id) ON DELETE CASCADE, + plan jsonb NOT NULL, + name text NOT NULL DEFAULT '', + created_at timestamptz NOT NULL DEFAULT now() +); +CREATE INDEX plan_revisions_plan ON plan_revisions(user_id, plan_id, created_at DESC); +ALTER TABLE actual_roasts ADD COLUMN IF NOT EXISTS manual_key text; +CREATE UNIQUE INDEX actual_roasts_manual_once ON actual_roasts(user_id, manual_key); +ALTER TABLE actual_roasts ADD COLUMN IF NOT EXISTS plan_snapshot jsonb; diff --git a/docs/bean-bro-workspace.md b/docs/bean-bro-workspace.md new file mode 100644 index 0000000..c29bed4 --- /dev/null +++ b/docs/bean-bro-workspace.md @@ -0,0 +1,20 @@ +# Coffee workspace and Ask Bean Bro + +The Planner now has a compact header, editable plan name, batch summary, annotated curve, contextual URL/curve actions, and an integrated Ask Bean Bro conversation. Mobile uses Plan / Ask Bean Bro tabs. Source URL and coffee name each occupy a full-width row; other basic coffee inputs use two columns. Timing calculations and explanations are expandable. + +## Plan → roast → taste → experiment + +- **Today** lists tasting reminders after three days, low green inventory below 500 g, unattached roasts, and experiments awaiting an outcome. These are disclosed reminder thresholds, not prescriptions about ideal rest or batch size. +- **Coffee history** groups by linked green inventory lot, otherwise by the entered coffee name. Unnamed plans stay separate. It shows plan variations, favorite batches, quick tasting feedback, detailed cupping links, and two-batch measured-curve comparisons. Histories are searchable and paginated; waveform data loads only for selected comparisons. +- **Experiments** capture a question, one change and an expected cup result. Quick tasting records balance, sweetness, favorites, and whether the experiment improved the cup. Favorites reflect the user's preference, not model grades. +- **Ask Bean Bro** saves its recent conversation with each plan, uses only that account's matching coffee history, and returns reviewable before/after proposals. Applying or creating a variation is explicit; stale proposals are rejected. Undo and plan history support recovery. Coffee facts and machine settings are outside the assistant's patch allowlist. +- **Roast day** provides a timer and manual milestone recording in Fahrenheit. Records persist in the account-scoped local draft offline. When online, Finish & record tasting creates a finished roast once for that timer session. Manual records export as JSON and are labeled as sparse observations, not continuous telemetry. Importing an Artisan file extracts its measurements, opens the uploaded roast, and can suggest a name-based plan match for confirmation. +- **Plan history** records meaningful changes transactionally. The latest 50 revisions are listed; revisions remain in personal/admin exports. Restore retains current inventory consumption, chat and roast-day records. New uploads, manual roasts and plan attachments capture plan targets; later plan edits do not rewrite those captured targets. Older records without snapshots remain clearly labeled as using the current linked plan. Past revisions or targets from before this release cannot be reconstructed. + +## Model configuration + +Admin → Bean Bro selects the configured provider/model for chat, prefill and finished-roast reviews. Test connection makes a small real request and reports the responding model. Provider credentials remain in the protected Pi configuration on the server. Auto follows the server model overrides and configured availability. Network-dependent assistance and cloud saves require connectivity; the roast-day draft remains available offline after the app shell has been cached. + +## Validation + +API regressions cover ownership, revision restore, scoped history/evidence, proposal allowlists, model connection permissions, idempotent manual completion, captured targets and exports. Chromium regressions cover plan switching, cloning, URL persistence, real print PDFs, chat persistence and reviewable edits, mobile tabs and field widths, offline reload with roast events, manual completion and coffee comparisons. Desktop/mobile screenshots were inspected and used to correct the layout. diff --git a/public/admin.html b/public/admin.html index ed13095..ba62227 100644 --- a/public/admin.html +++ b/public/admin.html @@ -100,23 +100,23 @@
-

LLM

+

Bean Bro

- Model used for URL prefill and finished-roast reviews. + Provider and model for Ask Bean Bro, URL prefill, and finished-roast reviews.

-

+

diff --git a/public/app.css b/public/app.css index 85fa846..bf53f60 100644 --- a/public/app.css +++ b/public/app.css @@ -2927,7 +2927,10 @@ select.f { cursor: pointer; color: var(--ink-2); font: inherit; - transition: border-color 0.12s, background 0.12s, color 0.12s; + transition: + border-color 0.12s, + background 0.12s, + color 0.12s; } .brewer-tile:hover { border-color: var(--line-strong); @@ -3113,7 +3116,9 @@ select.f { cursor: pointer; font: inherit; text-align: left; - transition: border-color 0.12s, background 0.12s; + transition: + border-color 0.12s, + background 0.12s; } .academy-lesson:hover { border-color: var(--ember); @@ -3144,13 +3149,23 @@ select.f { /* Warm-lit paper: a top light and a faint ember glow rising from the floor, so the * scenes sit in an environment instead of on a flat panel. */ background: - linear-gradient(180deg, rgba(255, 253, 248, 0.65), rgba(255, 253, 248, 0) 45%), - radial-gradient(90% 60% at 50% 115%, rgba(168, 72, 26, 0.07), transparent 60%), + linear-gradient( + 180deg, + rgba(255, 253, 248, 0.65), + rgba(255, 253, 248, 0) 45% + ), + radial-gradient( + 90% 60% at 50% 115%, + rgba(168, 72, 26, 0.07), + transparent 60% + ), var(--surface); border: 1px solid var(--line); border-radius: var(--radius-lg); padding: 8px; - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.6), 0 1px 3px rgba(27, 22, 20, 0.05); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.6), + 0 1px 3px rgba(27, 22, 20, 0.05); } .academy-stage svg { width: 100%; @@ -3191,65 +3206,343 @@ select.f { } /* Scene animations — restarted per slide because the SVG is rebuilt. */ -@keyframes ac-draw-kf { from { stroke-dashoffset: 900; } to { stroke-dashoffset: 0; } } -.ac-draw { stroke-dasharray: 900; animation: ac-draw-kf 2.6s ease-out forwards; } -@keyframes ac-pulse-kf { 0%, 100% { opacity: 1; } 50% { opacity: 0.25; } } -.ac-pulse { animation: ac-pulse-kf 1.6s ease-in-out infinite; } -@keyframes ac-breathe-kf { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.045); } } -.ac-breathe { animation: ac-breathe-kf 3s ease-in-out infinite; transform-box: fill-box; transform-origin: center; } -@keyframes ac-steam-kf { 0% { transform: translateY(6px); opacity: 0; } 30% { opacity: 0.55; } 100% { transform: translateY(-16px); opacity: 0; } } -.ac-steam { animation: ac-steam-kf 2.4s ease-out infinite; } -@keyframes ac-grow-w-kf { from { width: 0; } to { width: var(--w); } } -.ac-grow-w { animation: ac-grow-w-kf 1.4s ease-out forwards; } -@keyframes ac-grow-h-kf { from { height: 0; } to { height: 100px; } } -.ac-grow-h { animation: ac-grow-h-kf 1.6s ease-out forwards; } -@keyframes ac-grow-h-down-kf { from { height: 0; } to { height: 20px; } } -.ac-grow-h-down { animation: ac-grow-h-down-kf 2.4s ease-out forwards; } -@keyframes ac-spin-kf { to { transform: rotate(360deg); } } -.ac-spin { animation: ac-spin-kf 9s linear infinite; transform-box: fill-box; transform-origin: center; } -.ac-spin-slow { animation: ac-spin-kf 8s linear infinite; } -@keyframes ac-sparkle-kf { 0%, 100% { opacity: 0.2; transform: scale(0.7); } 50% { opacity: 1; transform: scale(1.25); } } -.ac-sparkle { animation: ac-sparkle-kf 1.8s ease-in-out infinite; transform-box: fill-box; transform-origin: center; } -@keyframes ac-drift-kf { 0% { transform: translate(0, 0); opacity: 0.9; } 100% { transform: translate(26px, -20px); opacity: 0; } } -.ac-drift { animation: ac-drift-kf 2.6s ease-out infinite; } -@keyframes ac-shake-kf { 0%, 100% { transform: translate(0); } 25% { transform: translate(-1.5px, 1px); } 75% { transform: translate(1.5px, -1px); } } -.ac-shake { animation: ac-shake-kf 0.28s linear infinite; } -@keyframes ac-ring-kf { from { transform: scale(0.6); opacity: 0.9; } to { transform: scale(1.5); opacity: 0; } } -.ac-ring { animation: ac-ring-kf 1.7s ease-out infinite; transform-box: fill-box; transform-origin: center; } -@keyframes ac-crack-kf { from { stroke-dashoffset: 120; } to { stroke-dashoffset: 0; } } -.ac-crackline { stroke-dasharray: 120; animation: ac-crack-kf 1.4s ease-in forwards; } -@keyframes ac-dissolve-kf { 0% { transform: translate(0, 0); opacity: 1; } 100% { transform: translate(56px, -34px); opacity: 0; } } -.ac-dissolve { animation: ac-dissolve-kf 2.4s ease-out infinite; } -@keyframes ac-float-kf { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-7px); } } -.ac-float { animation: ac-float-kf 2.6s ease-in-out infinite; } -@keyframes ac-rise-kf { 0% { transform: translateY(8px); opacity: 0; } 40% { opacity: 1; } 100% { transform: translateY(-22px); opacity: 0; } } -.ac-rise { animation: ac-rise-kf 2s ease-out infinite; } -@keyframes ac-drip-kf { 0% { transform: translateY(0); opacity: 1; } 100% { transform: translateY(14px); opacity: 0; } } -.ac-drip { animation: ac-drip-kf 1.1s ease-in infinite; } -@keyframes ac-tamp-kf { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(9px); } } -.ac-tamp { animation: ac-tamp-kf 2s ease-in-out infinite; } -@keyframes ac-slide-x-kf { 0%, 100% { transform: translateX(-46px); } 50% { transform: translateX(46px); } } -.ac-slide-x { animation: ac-slide-x-kf 5s ease-in-out infinite; } -@keyframes ac-dial-kf { 0%, 100% { transform: rotate(-38deg); } 50% { transform: rotate(38deg); } } -.ac-dial { animation: ac-dial-kf 6s ease-in-out infinite; } -@keyframes ac-pour-kf { 0%, 100% { opacity: 0.4; } 50% { opacity: 1; } } -.ac-pourline { animation: ac-pour-kf 1.6s ease-in-out infinite; } -@keyframes ac-maillard-kf { 0% { fill: #d9ba5a; } 100% { fill: #8a5a30; } } +@keyframes ac-draw-kf { + from { + stroke-dashoffset: 900; + } + to { + stroke-dashoffset: 0; + } +} +.ac-draw { + stroke-dasharray: 900; + animation: ac-draw-kf 2.6s ease-out forwards; +} +@keyframes ac-pulse-kf { + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.25; + } +} +.ac-pulse { + animation: ac-pulse-kf 1.6s ease-in-out infinite; +} +@keyframes ac-breathe-kf { + 0%, + 100% { + transform: scale(1); + } + 50% { + transform: scale(1.045); + } +} +.ac-breathe { + animation: ac-breathe-kf 3s ease-in-out infinite; + transform-box: fill-box; + transform-origin: center; +} +@keyframes ac-steam-kf { + 0% { + transform: translateY(6px); + opacity: 0; + } + 30% { + opacity: 0.55; + } + 100% { + transform: translateY(-16px); + opacity: 0; + } +} +.ac-steam { + animation: ac-steam-kf 2.4s ease-out infinite; +} +@keyframes ac-grow-w-kf { + from { + width: 0; + } + to { + width: var(--w); + } +} +.ac-grow-w { + animation: ac-grow-w-kf 1.4s ease-out forwards; +} +@keyframes ac-grow-h-kf { + from { + height: 0; + } + to { + height: 100px; + } +} +.ac-grow-h { + animation: ac-grow-h-kf 1.6s ease-out forwards; +} +@keyframes ac-grow-h-down-kf { + from { + height: 0; + } + to { + height: 20px; + } +} +.ac-grow-h-down { + animation: ac-grow-h-down-kf 2.4s ease-out forwards; +} +@keyframes ac-spin-kf { + to { + transform: rotate(360deg); + } +} +.ac-spin { + animation: ac-spin-kf 9s linear infinite; + transform-box: fill-box; + transform-origin: center; +} +.ac-spin-slow { + animation: ac-spin-kf 8s linear infinite; +} +@keyframes ac-sparkle-kf { + 0%, + 100% { + opacity: 0.2; + transform: scale(0.7); + } + 50% { + opacity: 1; + transform: scale(1.25); + } +} +.ac-sparkle { + animation: ac-sparkle-kf 1.8s ease-in-out infinite; + transform-box: fill-box; + transform-origin: center; +} +@keyframes ac-drift-kf { + 0% { + transform: translate(0, 0); + opacity: 0.9; + } + 100% { + transform: translate(26px, -20px); + opacity: 0; + } +} +.ac-drift { + animation: ac-drift-kf 2.6s ease-out infinite; +} +@keyframes ac-shake-kf { + 0%, + 100% { + transform: translate(0); + } + 25% { + transform: translate(-1.5px, 1px); + } + 75% { + transform: translate(1.5px, -1px); + } +} +.ac-shake { + animation: ac-shake-kf 0.28s linear infinite; +} +@keyframes ac-ring-kf { + from { + transform: scale(0.6); + opacity: 0.9; + } + to { + transform: scale(1.5); + opacity: 0; + } +} +.ac-ring { + animation: ac-ring-kf 1.7s ease-out infinite; + transform-box: fill-box; + transform-origin: center; +} +@keyframes ac-crack-kf { + from { + stroke-dashoffset: 120; + } + to { + stroke-dashoffset: 0; + } +} +.ac-crackline { + stroke-dasharray: 120; + animation: ac-crack-kf 1.4s ease-in forwards; +} +@keyframes ac-dissolve-kf { + 0% { + transform: translate(0, 0); + opacity: 1; + } + 100% { + transform: translate(56px, -34px); + opacity: 0; + } +} +.ac-dissolve { + animation: ac-dissolve-kf 2.4s ease-out infinite; +} +@keyframes ac-float-kf { + 0%, + 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-7px); + } +} +.ac-float { + animation: ac-float-kf 2.6s ease-in-out infinite; +} +@keyframes ac-rise-kf { + 0% { + transform: translateY(8px); + opacity: 0; + } + 40% { + opacity: 1; + } + 100% { + transform: translateY(-22px); + opacity: 0; + } +} +.ac-rise { + animation: ac-rise-kf 2s ease-out infinite; +} +@keyframes ac-drip-kf { + 0% { + transform: translateY(0); + opacity: 1; + } + 100% { + transform: translateY(14px); + opacity: 0; + } +} +.ac-drip { + animation: ac-drip-kf 1.1s ease-in infinite; +} +@keyframes ac-tamp-kf { + 0%, + 100% { + transform: translateY(0); + } + 50% { + transform: translateY(9px); + } +} +.ac-tamp { + animation: ac-tamp-kf 2s ease-in-out infinite; +} +@keyframes ac-slide-x-kf { + 0%, + 100% { + transform: translateX(-46px); + } + 50% { + transform: translateX(46px); + } +} +.ac-slide-x { + animation: ac-slide-x-kf 5s ease-in-out infinite; +} +@keyframes ac-dial-kf { + 0%, + 100% { + transform: rotate(-38deg); + } + 50% { + transform: rotate(38deg); + } +} +.ac-dial { + animation: ac-dial-kf 6s ease-in-out infinite; +} +@keyframes ac-pour-kf { + 0%, + 100% { + opacity: 0.4; + } + 50% { + opacity: 1; + } +} +.ac-pourline { + animation: ac-pour-kf 1.6s ease-in-out infinite; +} +@keyframes ac-maillard-kf { + 0% { + fill: #d9ba5a; + } + 100% { + fill: #8a5a30; + } +} /* Fill-shift animations ride a flat .ac-tint overlay at partial opacity so the * gradient shading underneath survives the color change. */ -.ac-tint, .ac-jb-color { opacity: 0.82; } -.ac-maillard-shift .ac-tint { animation: ac-maillard-kf 5s ease-in-out infinite alternate; } -@keyframes ac-tumble-kf { 0%, 100% { transform: translateY(0) rotate(0deg); } 50% { transform: translateY(-8px) rotate(6deg); } } -.ac-tumble { animation: ac-tumble-kf 1.6s ease-in-out infinite; transform-box: fill-box; transform-origin: center; } -@keyframes ac-flame-kf { 0%, 100% { transform: scaleY(1); } 50% { transform: scaleY(1.12); } } -.ac-flame { animation: ac-flame-kf 0.9s ease-in-out infinite; transform-box: fill-box; transform-origin: bottom center; } -.ac-delay-0 { animation-delay: 0s; } -.ac-delay-1 { animation-delay: 0.55s; } -.ac-delay-2 { animation-delay: 1.1s; } -.ac-delay-3 { animation-delay: 1.65s; } -.ac-delay-4 { animation-delay: 2.2s; } +.ac-tint, +.ac-jb-color { + opacity: 0.82; +} +.ac-maillard-shift .ac-tint { + animation: ac-maillard-kf 5s ease-in-out infinite alternate; +} +@keyframes ac-tumble-kf { + 0%, + 100% { + transform: translateY(0) rotate(0deg); + } + 50% { + transform: translateY(-8px) rotate(6deg); + } +} +.ac-tumble { + animation: ac-tumble-kf 1.6s ease-in-out infinite; + transform-box: fill-box; + transform-origin: center; +} +@keyframes ac-flame-kf { + 0%, + 100% { + transform: scaleY(1); + } + 50% { + transform: scaleY(1.12); + } +} +.ac-flame { + animation: ac-flame-kf 0.9s ease-in-out infinite; + transform-box: fill-box; + transform-origin: bottom center; +} +.ac-delay-0 { + animation-delay: 0s; +} +.ac-delay-1 { + animation-delay: 0.55s; +} +.ac-delay-2 { + animation-delay: 1.1s; +} +.ac-delay-3 { + animation-delay: 1.65s; +} +.ac-delay-4 { + animation-delay: 2.2s; +} @media (prefers-reduced-motion: reduce) { - .academy-stage svg * { animation: none !important; } + .academy-stage svg * { + animation: none !important; + } } /* Academy v2: progress + refined scene animations */ @@ -3277,92 +3570,312 @@ select.f { background: var(--ember); border-radius: 2px; } -@keyframes ac-fill-up-kf { from { transform: scaleY(0.12); } to { transform: scaleY(0.86); } } -.ac-fill-up { transform-box: fill-box; transform-origin: bottom; animation: ac-fill-up-kf 5s ease-out forwards; } -@keyframes ac-dry-shift-kf { 0% { fill: #7a8c5e; } 100% { fill: #d9ba5a; } } -.ac-dry-shift .ac-tint { animation: ac-dry-shift-kf 6s ease-in-out infinite alternate; } -@keyframes ac-needle-kf { 0%, 100% { transform: rotate(-52deg); } 50% { transform: rotate(52deg); } } -.ac-needle { animation: ac-needle-kf 7s ease-in-out infinite; } -@keyframes ac-pulse-soft-kf { 0%, 100% { opacity: 1; } 50% { opacity: 0.75; } } -.ac-pulse-soft { animation: ac-pulse-soft-kf 2.4s ease-in-out infinite; } +@keyframes ac-fill-up-kf { + from { + transform: scaleY(0.12); + } + to { + transform: scaleY(0.86); + } +} +.ac-fill-up { + transform-box: fill-box; + transform-origin: bottom; + animation: ac-fill-up-kf 5s ease-out forwards; +} +@keyframes ac-dry-shift-kf { + 0% { + fill: #7a8c5e; + } + 100% { + fill: #d9ba5a; + } +} +.ac-dry-shift .ac-tint { + animation: ac-dry-shift-kf 6s ease-in-out infinite alternate; +} +@keyframes ac-needle-kf { + 0%, + 100% { + transform: rotate(-52deg); + } + 50% { + transform: rotate(52deg); + } +} +.ac-needle { + animation: ac-needle-kf 7s ease-in-out infinite; +} +@keyframes ac-pulse-soft-kf { + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.75; + } +} +.ac-pulse-soft { + animation: ac-pulse-soft-kf 2.4s ease-in-out infinite; +} /* The 14-second "bean journey" clock — every piece of the hero scene rides it together. */ @keyframes ac-jb-color-kf { - 0%, 30% { fill: #7a8c5e; } - 47% { fill: #d9ba5a; } - 66% { fill: #b98d54; } - 84%, 100% { fill: #8a5a30; } + 0%, + 30% { + fill: #7a8c5e; + } + 47% { + fill: #d9ba5a; + } + 66% { + fill: #b98d54; + } + 84%, + 100% { + fill: #8a5a30; + } +} +.ac-jb-color { + animation: ac-jb-color-kf 14s linear infinite; } -.ac-jb-color { animation: ac-jb-color-kf 14s linear infinite; } @keyframes ac-jb-grow-kf { - 0%, 40% { transform: scale(1); } - 84%, 100% { transform: scale(1.12); } + 0%, + 40% { + transform: scale(1); + } + 84%, + 100% { + transform: scale(1.12); + } +} +.ac-jb-grow { + transform-box: fill-box; + transform-origin: center; + animation: ac-jb-grow-kf 14s linear infinite; } -.ac-jb-grow { transform-box: fill-box; transform-origin: center; animation: ac-jb-grow-kf 14s linear infinite; } @keyframes ac-jb-crack-kf { - 0%, 82% { opacity: 0; } - 86%, 100% { opacity: 1; } + 0%, + 82% { + opacity: 0; + } + 86%, + 100% { + opacity: 1; + } +} +.ac-jb-crack { + animation: ac-jb-crack-kf 14s linear infinite; } -.ac-jb-crack { animation: ac-jb-crack-kf 14s linear infinite; } @keyframes ac-jb-steam-kf { - 0%, 8% { opacity: 0; } - 18%, 42% { opacity: 1; } - 52%, 100% { opacity: 0; } + 0%, + 8% { + opacity: 0; + } + 18%, + 42% { + opacity: 1; + } + 52%, + 100% { + opacity: 0; + } +} +.ac-jb-steam { + animation: ac-jb-steam-kf 14s linear infinite; } -.ac-jb-steam { animation: ac-jb-steam-kf 14s linear infinite; } @keyframes ac-jb-marker-kf { - from { transform: translateX(0); } - to { transform: translateX(400px); } + from { + transform: translateX(0); + } + to { + transform: translateX(400px); + } +} +.ac-jb-marker { + animation: ac-jb-marker-kf 14s linear infinite; } -.ac-jb-marker { animation: ac-jb-marker-kf 14s linear infinite; } /* Academy v4: inside-the-bean / senses / gone-wrong scene animations */ @keyframes ac-plug-kf { - 0% { transform: translate(0, 0); opacity: 0.9; } - 60%, 100% { transform: translate(var(--dx), var(--dy)); opacity: 1; } + 0% { + transform: translate(0, 0); + opacity: 0.9; + } + 60%, + 100% { + transform: translate(var(--dx), var(--dy)); + opacity: 1; + } +} +.ac-plug { + animation: ac-plug-kf 3s ease-out infinite; } -.ac-plug { animation: ac-plug-kf 3s ease-out infinite; } @keyframes ac-wobble-kf { - 0%, 100% { transform: scale(1, 1) rotate(0deg); } - 25% { transform: scale(1.05, 0.95) rotate(-2deg); } - 75% { transform: scale(0.95, 1.05) rotate(2deg); } + 0%, + 100% { + transform: scale(1, 1) rotate(0deg); + } + 25% { + transform: scale(1.05, 0.95) rotate(-2deg); + } + 75% { + transform: scale(0.95, 1.05) rotate(2deg); + } +} +.ac-wobble { + animation: ac-wobble-kf 2.4s ease-in-out infinite; + transform-box: fill-box; + transform-origin: center; +} +@keyframes ac-shrink-kf { + 0% { + transform: scale(1); + } + 100% { + transform: scale(0.35); + } +} +.ac-shrink { + animation: ac-shrink-kf 6s ease-in-out infinite alternate; + transform-box: fill-box; + transform-origin: center; } -.ac-wobble { animation: ac-wobble-kf 2.4s ease-in-out infinite; transform-box: fill-box; transform-origin: center; } -@keyframes ac-shrink-kf { 0% { transform: scale(1); } 100% { transform: scale(0.35); } } -.ac-shrink { animation: ac-shrink-kf 6s ease-in-out infinite alternate; transform-box: fill-box; transform-origin: center; } @keyframes ac-gauge-kf { - 0% { transform: rotate(-58deg); } - 55%, 100% { transform: rotate(50deg); } + 0% { + transform: rotate(-58deg); + } + 55%, + 100% { + transform: rotate(50deg); + } +} +.ac-gauge-needle { + animation: ac-gauge-kf 3.2s ease-out infinite; } -.ac-gauge-needle { animation: ac-gauge-kf 3.2s ease-out infinite; } @keyframes ac-grow-foam-kf { - 0% { transform: scale(0.8); } - 60%, 100% { transform: scale(1); } + 0% { + transform: scale(0.8); + } + 60%, + 100% { + transform: scale(1); + } +} +.ac-grow-foam { + animation: ac-grow-foam-kf 4s ease-out infinite; + transform-box: fill-box; + transform-origin: center; +} +@keyframes ac-mortar-fade-kf { + 0%, + 20% { + opacity: 1; + } + 60% { + opacity: 0.25; + } + 100% { + opacity: 1; + } +} +.ac-mortar-fade { + animation: ac-mortar-fade-kf 4.5s ease-in-out infinite; +} +@keyframes ac-rebar-snap-kf { + 0%, + 70% { + opacity: 0; + } + 78%, + 100% { + opacity: 1; + } +} +.ac-rebar-snap { + animation: ac-rebar-snap-kf 6s linear infinite; } -.ac-grow-foam { animation: ac-grow-foam-kf 4s ease-out infinite; transform-box: fill-box; transform-origin: center; } -@keyframes ac-mortar-fade-kf { 0%, 20% { opacity: 1; } 60% { opacity: 0.25; } 100% { opacity: 1; } } -.ac-mortar-fade { animation: ac-mortar-fade-kf 4.5s ease-in-out infinite; } -@keyframes ac-rebar-snap-kf { 0%, 70% { opacity: 0; } 78%, 100% { opacity: 1; } } -.ac-rebar-snap { animation: ac-rebar-snap-kf 6s linear infinite; } @keyframes ac-burst-kf { - 0% { transform: translateY(6px) scale(0.6); opacity: 0; } - 30% { opacity: 1; } - 100% { transform: translateY(-30px) scale(1.1); opacity: 0; } + 0% { + transform: translateY(6px) scale(0.6); + opacity: 0; + } + 30% { + opacity: 1; + } + 100% { + transform: translateY(-30px) scale(1.1); + opacity: 0; + } +} +.ac-burst { + animation: ac-burst-kf 1.1s ease-out infinite; +} +@keyframes ac-grass-fade-kf { + 0% { + fill: #8a9575; + } + 40% { + fill: #7a8c5e; + } + 100% { + fill: #cdbd8f; + } +} +.ac-grass-fade .ac-tint { + animation: ac-grass-fade-kf 6s ease-in-out infinite alternate; +} +@keyframes ac-cross-a-kf { + 0%, + 45% { + opacity: 1; + } + 55%, + 100% { + opacity: 0; + } +} +.ac-cross-a { + animation: ac-cross-a-kf 4s ease-in-out infinite; +} +@keyframes ac-cross-b-kf { + 0%, + 45% { + opacity: 0; + } + 55%, + 100% { + opacity: 1; + } +} +.ac-cross-b { + animation: ac-cross-b-kf 4s ease-in-out infinite; } -.ac-burst { animation: ac-burst-kf 1.1s ease-out infinite; } -@keyframes ac-grass-fade-kf { 0% { fill: #8a9575; } 40% { fill: #7a8c5e; } 100% { fill: #cdbd8f; } } -.ac-grass-fade .ac-tint { animation: ac-grass-fade-kf 6s ease-in-out infinite alternate; } -@keyframes ac-cross-a-kf { 0%, 45% { opacity: 1; } 55%, 100% { opacity: 0; } } -.ac-cross-a { animation: ac-cross-a-kf 4s ease-in-out infinite; } -@keyframes ac-cross-b-kf { 0%, 45% { opacity: 0; } 55%, 100% { opacity: 1; } } -.ac-cross-b { animation: ac-cross-b-kf 4s ease-in-out infinite; } @keyframes ac-marker-cross-kf { - 0%, 100% { transform: translateX(-30px); } - 50% { transform: translateX(30px); } + 0%, + 100% { + transform: translateX(-30px); + } + 50% { + transform: translateX(30px); + } +} +.ac-marker-cross { + animation: ac-marker-cross-kf 4s ease-in-out infinite; +} +@keyframes ac-row-glow-kf { + 0%, + 100% { + opacity: 0.15; + } + 15%, + 30% { + opacity: 0.85; + } +} +.ac-row-glow { + animation: ac-row-glow-kf 6s ease-in-out infinite; } -.ac-marker-cross { animation: ac-marker-cross-kf 4s ease-in-out infinite; } -@keyframes ac-row-glow-kf { 0%, 100% { opacity: 0.15; } 15%, 30% { opacity: 0.85; } } -.ac-row-glow { animation: ac-row-glow-kf 6s ease-in-out infinite; } /* ── Flavor wheel (cupping) ───────────────────────────────────────────── */ .flavor-wheel-wrap { @@ -3375,9 +3888,537 @@ select.f { } /* Historical machine bands and pace multipliers no longer guide planning. */ -#sec-machine .band-track, .ledger-pace { display:none !important; } -#sec-machine .milestone-row { grid-template-columns:1fr 90px 110px; } +#sec-machine .band-track, +.ledger-pace { + display: none !important; +} +#sec-machine .milestone-row { + grid-template-columns: 1fr 90px 110px; +} -.collection-filters{display:flex;gap:12px;align-items:end;flex-wrap:wrap;margin-bottom:18px}.collection-filters .field:first-child{flex:1;min-width:220px}.collection-pagination{display:flex;gap:10px;align-items:center;margin-top:16px}.collection-pagination p{flex:1}.plan-title-link{color:var(--ember-strong);font-weight:650;text-decoration:underline;text-underline-offset:3px} +.collection-filters { + display: flex; + gap: 12px; + align-items: end; + flex-wrap: wrap; + margin-bottom: 18px; +} +.collection-filters .field:first-child { + flex: 1; + min-width: 220px; +} +.collection-pagination { + display: flex; + gap: 10px; + align-items: center; + margin-top: 16px; +} +.collection-pagination p { + flex: 1; +} +.plan-title-link { + color: var(--ember-strong); + font-weight: 650; + text-decoration: underline; + text-underline-offset: 3px; +} -#plans-body td:first-child { min-width:180px; } +#plans-body td:first-child { + min-width: 180px; +} +/* Coffee workspace */ +.eyebrow { + font-size: 10px; + letter-spacing: 0.14em; + color: var(--ember); + font-weight: 750; + text-transform: uppercase; +} +.coffee-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(270px, 1fr)); + gap: 18px; + margin-top: 22px; +} +.coffee-card { + padding: 24px; + border: 1px solid var(--line); + border-radius: 14px; + background: white; +} +.coffee-name { + display: block; + font-size: 22px; + font-family: var(--font-serif); + color: var(--ink); + margin: 12px 0; + text-decoration: none; +} +.next-actions, +.coffee-intro, +#workspace-content > .panel-card { + padding: 24px; + margin-bottom: 22px; +} +.next-action, +.history-row { + padding: 14px 0; + border-bottom: 1px solid var(--line); +} +.next-action a, +.history-row a { + font-weight: 650; + color: var(--ember-strong); +} +.next-action p, +.history-row p { + margin: 6px 0; +} +.comparison-chart svg { + width: 100%; + height: auto; +} +.workspace-dialog { + width: min(820px, 94vw); + max-height: 90vh; + overflow: auto; + border: 1px solid var(--line); + border-radius: 18px; + padding: 28px; +} +.workspace-dialog::backdrop { + background: #17120f88; +} +/* Planner: coffee workspace with an integrated conversation */ +.planner-page .app-header { + position: relative; + height: auto; + padding: 18px 28px; + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 14px; + border-bottom: 1px solid var(--line); +} +.planner-page .header-row-top { + flex: 1; + gap: 20px; + min-width: 260px; +} +.planner-page .brand h1 { + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.12em; + color: var(--ink-3); +} +.planner-page #header-coffee-name { + font: 600 23px var(--font-serif); + color: var(--ink); + margin-top: 4px; + cursor: pointer; +} +.planner-page .section-nav { + order: 3; + flex-basis: 100%; + padding: 0; + gap: 24px; + border: 0; +} +.planner-page .header-actions { + position: relative; + gap: 9px; + margin: 0; +} +.planner-page .header-tools-toggle { + display: inline-flex; +} +.planner-page .header-tools-group { + display: none !important; + position: absolute; + right: 0; + top: 100%; + z-index: 50; + padding: 12px; + min-width: 210px; + background: white; + box-shadow: var(--shadow-pop); + border: 1px solid var(--line); + border-radius: 12px; +} +.planner-page .header-tools-group.open { + display: flex !important; + flex-direction: column; + align-items: stretch; +} +.planner-page .header-tools-group .header-divider { + display: none; +} +.planner-page #nav-plans { + white-space: nowrap; + text-decoration: none; +} +.planner-layout { + display: grid; + grid-template-columns: minmax(0, 1fr) 340px; + gap: 26px; + padding: 28px; + align-items: start; +} +.planner-main { + min-width: 0; +} +.planner-page #plan-form { + padding: 0; + margin: 0; +} +.planner-page .workspace-grid { + display: flex; + flex-direction: column; + gap: 24px; + padding: 0; + margin: 0; + max-width: none; +} +.planner-page .form-col { + width: 100%; + min-width: 0; +} +.planner-page .dock { + position: static; + width: 100%; + display: flex; + flex-direction: column; + gap: 12px; +} +.planner-page .field-grid { + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); +} +.planner-page .source-field { + grid-column: 1/-1; + display: grid; + grid-template-columns: 1fr auto; + gap: 8px 14px; + align-items: center; +} +.planner-page .source-field .field-label, +.planner-page .source-field .field-note { + grid-column: 1/-1; +} +.planner-page .source-field input { + grid-column: 1; + min-width: 0; +} +.planner-page .source-field #nav-prefill { + grid-column: 2; + grid-row: 2; +} +.plan-overview { + padding: 22px 0 26px; +} +.overview-stats { + display: grid; + grid-template-columns: 2fr repeat(4, 1fr); + gap: 16px; + margin-top: 12px; +} +.overview-stats span { + display: block; + font-size: 11px; + color: var(--ink-3); + margin-bottom: 6px; +} +.overview-stats strong { + font-size: 18px; +} +.calculation-details > summary { + cursor: pointer; + padding: 14px; + background: var(--surface-sunken); + border-radius: 8px; + font-weight: 650; +} +.calculation-details .ledger-card { + margin-top: 10px; +} +.report-actions { + display: flex; + gap: 10px; + padding: 14px 0; +} +.experiment-panel { + padding: 24px; +} +.experiment-panel h2 { + margin: 8px 0; +} +.bean-bro-panel { + position: sticky; + top: 24px; + background: #fff; + border: 1px solid var(--line); + border-radius: 18px; + overflow: hidden; + box-shadow: var(--shadow-card); + min-width: 0; +} +.bean-bro-panel .drawer-head { + background: #e9f0e9; + padding: 20px; + border-bottom: 1px solid #d7e2d7; +} +.bean-bro-panel h3 { + margin: 0; +} +.bean-bro-panel .drawer-body { + padding: 18px; + max-height: calc(100vh - 120px); + overflow: auto; +} +.bro-intro { + font-size: 13px; + line-height: 1.6; + color: var(--ink-2); + margin-top: 0; +} +.chat-starters { + display: flex; + gap: 7px; + flex-wrap: wrap; + margin: 14px 0; +} +.bean-bro-panel .chat-thread { + min-height: 100px; + max-height: 340px; + overflow: auto; +} +.bean-bro-panel .chat-input-row { + display: flex; + flex-direction: column; + margin-top: 16px; +} +.bean-bro-panel textarea { + min-height: 90px; + resize: vertical; +} +.chat-msg { + white-space: pre-wrap; + overflow-wrap: anywhere; +} +.proposal-card { + padding: 14px; + border: 1px solid #d7e2d7; + border-radius: 12px; + margin: 12px 0; + background: #f7faf6; + overflow: auto; +} +.proposal-card p { + font-size: 12px; + line-height: 1.5; +} +.proposal-card button { + margin: 8px 6px 0 0; +} +.proposal-card table { + font-size: 11px; +} +.proposal-card a { + display: block; + margin: 6px 0; +} +.roast-clock { + font-size: 64px; + font-variant-numeric: tabular-nums; + margin: 14px 0; +} +.roast-event-grid { + display: grid; + gap: 12px; + margin: 20px 0; +} +.roast-event { + display: grid; + grid-template-columns: 1.2fr 1.5fr 1fr 100px 100px; + gap: 10px; + align-items: center; + padding: 14px; + border: 1px solid var(--line); + border-radius: 10px; +} +.roast-event input { + width: 100%; + padding: 10px; + border: 1px solid var(--line); + border-radius: 6px; +} +@media (max-width: 1150px) { + .planner-layout { + grid-template-columns: minmax(0, 1fr); + padding: 20px; + } + .bean-bro-panel { + position: static; + } + .bean-bro-panel .drawer-body { + max-height: none; + } + .overview-stats { + grid-template-columns: 2fr 1fr 1fr; + } + .roast-event { + grid-template-columns: 1fr 1fr; + } + .planner-page .app-header { + padding: 16px; + } + .planner-page .header-row-top { + justify-content: flex-start; + } + .planner-page .source-field { + grid-template-columns: minmax(0, 1fr) auto; + } +} +@media (max-width: 600px) { + .planner-layout { + padding: 14px; + } + .planner-page .field-grid { + grid-template-columns: 1fr 1fr; + } + .planner-page .field-grid .field { + min-width: 0; + } + .planner-page .source-field { + display: flex; + flex-direction: column; + align-items: stretch; + } + .planner-page .header-row-top { + flex-wrap: wrap; + } + .planner-page .header-actions { + flex-wrap: wrap; + } + .planner-page #header-coffee-name { + font-size: 20px; + } + .overview-stats { + grid-template-columns: 1fr 1fr; + } + .overview-stats > div:first-child { + grid-column: 1/-1; + } + .planner-page .section-nav { + overflow: auto; + white-space: nowrap; + } + .coffee-grid { + grid-template-columns: 1fr; + } + .roast-event { + grid-template-columns: 1fr 1fr; + } + .bean-bro-panel .chat-thread { + max-height: 400px; + } +} +.planner-page .coffee-basics { + grid-template-columns: repeat(2, minmax(0, 1fr)) !important; +} +.planner-page .coffee-basics [data-fid="0.1"] { + grid-column: 1/-1; +} +.bean-bro-panel .chat-thread { + height: auto; + min-height: 0; + margin: 16px 0; +} +.bean-bro-panel .chat-thread:empty { + display: none; +} +.mobile-workspace-tabs, +.mobile-curve-hint { + display: none; +} +#workspace-content a:not(.primary-btn) { + color: var(--ember-strong); +} +.comparison-chart { + margin-top: 16px; +} +@media (max-width: 1150px) { + .planner-page { + padding-bottom: 76px; + } + .mobile-workspace-tabs { + display: flex; + position: fixed; + bottom: 0; + left: 0; + right: 0; + z-index: 45; + gap: 12px; + padding: 12px max(16px, env(safe-area-inset-right)); + background: #fff; + border-top: 1px solid var(--line); + justify-content: center; + } + .mobile-workspace-tabs button { + flex: 1; + max-width: 350px; + } + .mobile-workspace-tabs [aria-pressed="true"] { + background: #e9f0e9; + border-color: #94ae99; + color: #264b36; + } + .planner-page:not(.bro-tab) .bean-bro-panel { + display: none; + } + .planner-page.bro-tab .planner-main { + display: none; + } + .planner-page .bean-bro-panel { + margin-bottom: 12px; + } + .planner-page .bean-bro-panel .chat-thread { + max-height: none; + } +} +@media (max-width: 600px) { + .planner-page .curve-wrap { + overflow-x: auto; + } + .planner-page .curve-wrap svg { + min-width: 600px; + } + .mobile-curve-hint { + display: block; + } + .planner-page .dock-card-head { + flex-wrap: wrap; + gap: 10px; + } + .planner-page .dock-card-head h2 { + white-space: nowrap; + } + .planner-page .coffee-basics [data-fid="0.1"] input { + width: 100%; + } +} +.planner-page .brand { + flex: 1 1 auto; + min-width: 0; + max-width: 100%; +} +.planner-page .brand-text { + min-width: 0; + max-width: 100%; +} +.planner-page #header-coffee-name { + white-space: normal; + overflow-wrap: anywhere; +} +.planner-page .header-row-top { + max-width: 100%; +} diff --git a/public/index.html b/public/index.html index 814d26a..4b345d0 100644 --- a/public/index.html +++ b/public/index.html @@ -84,7 +84,7 @@
▤ Your plans +