It governs nearly every panel on the page — the training grid, the timing charts, both sleep patterns, the counts panel, all three walk views — but it sat inside Sleep, so it read as a sleep setting. That was already the reason its old home carried a comment explaining it was not one; a control needing a comment to deny what its position says is in the wrong position. It now has its own row under the quick actions, above the first panel that reads it, with a "Charts cover" label to say what it does. The row skips the card chrome: it is a control, not content, and giving it a section's surface and shadow would have made it look like a panel with nothing in it. No JS change — both the click wiring and renderChartWindow address the buttons by class, so they never cared where the picker lived. Also removes that old comment, which had stopped being wrong about the picker and started being wrong about the panel: the walks move carried it along by accident and left it sitting above Weight, explaining a control three panels away.
1673 lines
45 KiB
CSS
1673 lines
45 KiB
CSS
:root {
|
||
--bg: #f7f5ff;
|
||
--surface: #ffffff;
|
||
--text: #1c1b22;
|
||
--muted: #6b6b78;
|
||
--accent: #7c5cff;
|
||
--accent-soft: #ebe6ff;
|
||
--sleep: #6c8cff;
|
||
--eat: #ff9b3d;
|
||
--pee: #ffd23f;
|
||
--poo: #8a5a3b;
|
||
--weight: #2bb3a3;
|
||
--training: #b04ecf;
|
||
--walk: #3f9e63;
|
||
/* Asleep and awake each come in two values: the bright one for stripes,
|
||
tints and fills, and an -ink for text on those pale grounds. Both inks
|
||
flip with the theme; the brights don't. --sleep itself was doing both
|
||
jobs and failing the second — 2.6:1 on the timer pill it painted. */
|
||
--sleep-ink: #4258cc;
|
||
--wake: #f2b705;
|
||
--wake-ink: #6f4d00;
|
||
/* The two timers, on the dark theme, take a near-white with a sixth of the
|
||
gold mixed into it (#fdf4da): warm enough to belong to the sunshine, far
|
||
enough from --wake-ink not to be yellow text on a yellow pill again. In
|
||
the light theme the same pill is near-white cream, so anything pale would
|
||
vanish and it keeps the dark ink. Separate from --wake-ink so the list's
|
||
AWAKE labels stay gold and go on matching their stripe. */
|
||
--wake-timer-ink: #6f4d00;
|
||
--note: #6f7a90;
|
||
--danger: #d64545;
|
||
--gain: #2e9e5b;
|
||
--border: #e9e6f5;
|
||
--radius: 12px;
|
||
--shadow: 0 1px 2px rgba(20, 14, 60, 0.05), 0 4px 16px rgba(20, 14, 60, 0.05);
|
||
}
|
||
|
||
/* Dark palette. Two triggers, same values (CSS has no way to share them):
|
||
system prefers dark and the user hasn't forced light, OR the user picked
|
||
dark via the Settings toggle. The [data-theme] attribute (set from JS) beats
|
||
the media query on specificity, so an explicit choice always wins. */
|
||
@media (prefers-color-scheme: dark) {
|
||
:root:not([data-theme="light"]) {
|
||
--bg: #14131a;
|
||
--surface: #1f1e28;
|
||
--text: #ebeaf2;
|
||
--muted: #9c9bab;
|
||
--accent: #a690ff;
|
||
--accent-soft: #2a2640;
|
||
--sleep-ink: #8ba4ff;
|
||
--wake-ink: #f5c451;
|
||
--wake-timer-ink: #fdf4da;
|
||
--border: #2c2a3a;
|
||
--shadow: 0 1px 2px rgba(0, 0, 0, 0.3), 0 4px 16px rgba(0, 0, 0, 0.3);
|
||
}
|
||
}
|
||
:root[data-theme="dark"] {
|
||
--bg: #14131a;
|
||
--surface: #1f1e28;
|
||
--text: #ebeaf2;
|
||
--muted: #9c9bab;
|
||
--accent: #a690ff;
|
||
--accent-soft: #2a2640;
|
||
--sleep-ink: #8ba4ff;
|
||
--wake-ink: #f5c451;
|
||
--wake-timer-ink: #fdf4da;
|
||
--border: #2c2a3a;
|
||
--shadow: 0 1px 2px rgba(0, 0, 0, 0.3), 0 4px 16px rgba(0, 0, 0, 0.3);
|
||
}
|
||
|
||
* { box-sizing: border-box; }
|
||
|
||
html, body {
|
||
margin: 0;
|
||
padding: 0;
|
||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||
background: var(--bg);
|
||
color: var(--text);
|
||
line-height: 1.4;
|
||
}
|
||
|
||
body {
|
||
max-width: 720px;
|
||
margin: 0 auto;
|
||
padding: env(safe-area-inset-top, 0) 16px env(safe-area-inset-bottom, 0) 16px;
|
||
}
|
||
|
||
header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 20px 4px 8px;
|
||
}
|
||
|
||
h1 {
|
||
font-size: 1.5rem;
|
||
margin: 0;
|
||
}
|
||
|
||
.title {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 2px;
|
||
flex: 1 1 auto;
|
||
min-width: 0;
|
||
}
|
||
/* Truncate the name so a long one never runs into the action buttons. Scoped to
|
||
the header title so the auth-screen heading is unaffected. */
|
||
#app-title {
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.puppy-age {
|
||
font-size: 0.8rem;
|
||
color: var(--muted);
|
||
font-variant-numeric: tabular-nums;
|
||
/* One line in the common case; on a very narrow screen it wraps rather than
|
||
truncating, so the age is never cut off. The name (#app-title) is what
|
||
truncates to keep the buttons clear. */
|
||
}
|
||
|
||
.header-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.icon-btn {
|
||
padding: 6px 10px;
|
||
font-size: 1.1rem;
|
||
line-height: 1;
|
||
}
|
||
|
||
h2 {
|
||
font-size: 1rem;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.06em;
|
||
color: var(--muted);
|
||
margin: 0 0 12px;
|
||
}
|
||
|
||
main {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 24px;
|
||
padding: 16px 0 32px;
|
||
}
|
||
|
||
section {
|
||
background: var(--surface);
|
||
border-radius: var(--radius);
|
||
box-shadow: var(--shadow);
|
||
padding: 16px;
|
||
}
|
||
|
||
/* Paints over the notch/status-bar strip so content scrolling behind the
|
||
sticky day bar never peeks through above it. Zero-height where there is no
|
||
safe-area inset. */
|
||
body::before {
|
||
content: "";
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
height: env(safe-area-inset-top, 0px);
|
||
background: var(--bg);
|
||
z-index: 65;
|
||
}
|
||
|
||
.day-bar {
|
||
position: sticky;
|
||
top: env(safe-area-inset-top, 0px);
|
||
z-index: 60;
|
||
display: flex;
|
||
gap: 6px;
|
||
align-items: center;
|
||
justify-content: flex-end;
|
||
flex-wrap: nowrap;
|
||
padding: 8px 10px;
|
||
}
|
||
/* The face button shows the short date; the real input sits invisibly behind
|
||
it so its native picker still anchors here (and .showPicker() has a target). */
|
||
.day-date {
|
||
position: relative;
|
||
flex-shrink: 0;
|
||
display: inline-flex;
|
||
}
|
||
.day-date input[type="date"] {
|
||
position: absolute;
|
||
inset: 0;
|
||
width: 100%;
|
||
opacity: 0;
|
||
pointer-events: none;
|
||
}
|
||
#day-date-face {
|
||
font-variant-numeric: tabular-nums;
|
||
white-space: nowrap;
|
||
}
|
||
.day-bar button:not(.bar-clock) {
|
||
padding: 8px 10px;
|
||
flex-shrink: 0;
|
||
}
|
||
/* Keep the single row intact on narrow phones. */
|
||
@media (max-width: 370px) {
|
||
/* Three columns leave ~55px of text room on the narrowest phones, and
|
||
"⚖️ Weight" is the longest of the three; trimming the side padding keeps
|
||
it on one line down to about 360px. */
|
||
.action-row.three-up button { padding-left: 6px; padding-right: 6px; }
|
||
.day-bar { gap: 4px; }
|
||
.day-bar button:not(.bar-clock) { padding: 8px 7px; }
|
||
.bar-clock { font-size: 0.9rem; padding: 6px 8px; gap: 5px; }
|
||
}
|
||
.day-bar button:not(.bar-clock):disabled {
|
||
opacity: 0.4;
|
||
cursor: not-allowed;
|
||
}
|
||
.bar-clock {
|
||
display: flex;
|
||
/* It is a button (tapping it flips the sleep state), so undo the default
|
||
accent look — the asleep/awake classes below paint it. */
|
||
background: var(--surface);
|
||
color: var(--text);
|
||
align-items: center;
|
||
gap: 6px;
|
||
margin-right: auto; /* pin left; the flexible gap sits between it and the day controls */
|
||
padding: 7px 10px;
|
||
border-radius: 999px;
|
||
font-weight: 700;
|
||
font-size: 1rem;
|
||
font-variant-numeric: tabular-nums;
|
||
flex-shrink: 0;
|
||
}
|
||
.bar-clock[hidden] { display: none; }
|
||
/* Big timer still on screen: keep the pill's slot but show nothing. */
|
||
.bar-clock.standby { visibility: hidden; }
|
||
.bar-clock.asleep { background: color-mix(in srgb, var(--sleep) 18%, var(--surface)); color: var(--sleep-ink); }
|
||
/* Dark text on the yellow, the way the pee button already does it: a gold
|
||
light enough to read as sunshine is never legible as text on a pale ground.
|
||
12% where asleep takes 18% — equal percentages of these two hues are not
|
||
equally strong, and yellow at 18% shouted while the blue did not. */
|
||
.bar-clock.awake { background: color-mix(in srgb, var(--wake) 12%, var(--surface)); color: var(--wake-timer-ink); }
|
||
|
||
.big-clock {
|
||
text-align: center;
|
||
padding: 24px 16px;
|
||
}
|
||
.big-clock .bc-label {
|
||
font-size: 0.8rem;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.08em;
|
||
color: var(--muted);
|
||
margin-bottom: 6px;
|
||
}
|
||
.big-clock .bc-time {
|
||
font-size: 3.25rem;
|
||
font-weight: 700;
|
||
font-variant-numeric: tabular-nums;
|
||
line-height: 1.05;
|
||
letter-spacing: -0.01em;
|
||
}
|
||
.big-clock .bc-since {
|
||
margin-top: 6px;
|
||
font-size: 0.8rem;
|
||
color: var(--muted);
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
.big-clock.asleep { background: linear-gradient(180deg, var(--surface), color-mix(in srgb, var(--sleep) 10%, var(--surface))); }
|
||
.big-clock.asleep .bc-time { color: var(--sleep-ink); }
|
||
.big-clock.awake { background: linear-gradient(180deg, var(--surface), color-mix(in srgb, var(--wake) 10%, var(--surface))); }
|
||
.big-clock.awake .bc-time { color: var(--wake-timer-ink); }
|
||
|
||
/* Quick actions: a stack of explicit rows (see index.html) instead of one
|
||
auto-fit grid, so the grouping is the same at every width. Equal columns
|
||
within a row, two by default and three for the dialog row at the bottom. */
|
||
.actions {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
}
|
||
.action-row {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, 1fr);
|
||
gap: 10px;
|
||
}
|
||
.action-row.three-up { grid-template-columns: repeat(3, 1fr); }
|
||
|
||
button {
|
||
font: inherit;
|
||
border: none;
|
||
border-radius: var(--radius);
|
||
padding: 14px 12px;
|
||
cursor: pointer;
|
||
background: var(--accent);
|
||
color: white;
|
||
font-weight: 600;
|
||
transition: transform 0.08s ease, filter 0.15s ease;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
button:active { transform: scale(0.97); }
|
||
button:hover { filter: brightness(1.05); }
|
||
|
||
button.action.sleep { background: var(--sleep); }
|
||
button.action.eat { background: var(--eat); }
|
||
button.action.pee { background: var(--pee); color: #2b240a; }
|
||
button.action.poo { background: var(--poo); }
|
||
button.action.weight { background: var(--weight); }
|
||
button.action.walk { background: var(--walk); }
|
||
button.action.note { background: var(--note); }
|
||
/* Unlikely given the current sleep state (see renderActionHints) — dimmed
|
||
but fully tappable, so corrections are never blocked. */
|
||
button.action.unlikely { opacity: 0.4; }
|
||
|
||
/* The boundary that would repeat the latest sleep event is disabled outright
|
||
(see renderActionHints) — greyed out rather than merely dimmed, so it reads
|
||
as unavailable instead of just discouraged. */
|
||
button.action:disabled {
|
||
opacity: 0.35;
|
||
filter: grayscale(1);
|
||
cursor: not-allowed;
|
||
}
|
||
button.action:disabled:active { transform: none; }
|
||
button.action:disabled:hover { filter: grayscale(1); }
|
||
|
||
button.ghost {
|
||
background: transparent;
|
||
color: var(--text);
|
||
border: 1px solid var(--border);
|
||
font-weight: 500;
|
||
padding: 10px 14px;
|
||
}
|
||
|
||
button.danger { background: var(--danger); }
|
||
|
||
.stats {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(90px, 1fr));
|
||
gap: 10px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.stat {
|
||
background: var(--accent-soft);
|
||
padding: 12px;
|
||
border-radius: var(--radius);
|
||
text-align: center;
|
||
}
|
||
|
||
.stat-label {
|
||
font-size: 0.75rem;
|
||
color: var(--muted);
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.05em;
|
||
}
|
||
|
||
.stat-sub {
|
||
color: var(--muted);
|
||
font-size: 0.7rem;
|
||
margin-top: 2px;
|
||
}
|
||
|
||
.stat-value {
|
||
font-size: 1.25rem;
|
||
font-weight: 700;
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.lasts {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
}
|
||
|
||
.last-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
padding: 6px 4px;
|
||
font-size: 0.95rem;
|
||
border-bottom: 1px dashed var(--border);
|
||
}
|
||
.last-row:last-child { border-bottom: none; }
|
||
.last-row span:first-child { color: var(--muted); }
|
||
|
||
.muted-note {
|
||
color: var(--muted);
|
||
font-size: 0.8rem;
|
||
font-weight: normal;
|
||
}
|
||
|
||
/* ---------- timing range charts ---------- */
|
||
.timing-charts {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
.timing-name {
|
||
color: var(--muted);
|
||
font-size: 0.95rem;
|
||
padding: 0 4px;
|
||
}
|
||
.timing-chart { display: block; width: 100%; height: auto; }
|
||
/* Values wear ink, the words beside them are muted — the band alone carries
|
||
which type a chart is about (its heading says it too). */
|
||
.timing-chart text { fill: var(--text); font-size: 9px; font-variant-numeric: tabular-nums; }
|
||
.timing-chart .tm-word { fill: var(--muted); }
|
||
.timing-chart .tm-track { fill: var(--border); }
|
||
.timing-chart .tm-tick { stroke: var(--muted); stroke-width: 1; opacity: 0.45; }
|
||
.timing-chart .tm-band { fill-opacity: 1; }
|
||
/* The full spread sits behind the solid shortest-to-typical stretch, faded so
|
||
the everyday half of the range stays the one that reads first. */
|
||
.timing-chart .tm-range { fill-opacity: 0.32; }
|
||
.timing-chart .tm-pee { fill: var(--pee); }
|
||
.timing-chart .tm-poo { fill: var(--poo); }
|
||
.timing-chart .tm-eat { fill: var(--eat); }
|
||
/* "Since the last one" marker: ink over a surface-coloured ring, so it stays
|
||
readable on top of any band colour in either theme. */
|
||
.timing-chart .tm-now-ring { stroke: var(--surface); stroke-width: 7; stroke-linecap: round; }
|
||
.timing-chart .tm-now { stroke: var(--text); stroke-width: 3; stroke-linecap: round; }
|
||
.timing-chart .tm-over { fill: var(--text); }
|
||
.timing-note { margin: 2px 4px 0; }
|
||
.settings-hint { color: var(--muted); font-size: 0.8rem; margin: -4px 0 4px; line-height: 1.4; }
|
||
|
||
.history-controls {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
flex-wrap: wrap;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.history-controls label { color: var(--muted); font-size: 0.85rem; }
|
||
|
||
input[type="date"], input[type="datetime-local"], input[type="text"], input[type="number"], textarea {
|
||
font: inherit;
|
||
background: var(--bg);
|
||
color: var(--text);
|
||
border: 1px solid var(--border);
|
||
border-radius: 8px;
|
||
padding: 8px 10px;
|
||
width: 100%;
|
||
}
|
||
|
||
textarea { resize: vertical; }
|
||
|
||
.event-list {
|
||
list-style: none;
|
||
margin: 0;
|
||
padding: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
}
|
||
|
||
.wake-list {
|
||
list-style: none;
|
||
margin: 0;
|
||
padding: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
}
|
||
.ww {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
background: var(--bg);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius);
|
||
padding: 10px 12px;
|
||
}
|
||
.ww .ww-range {
|
||
font-variant-numeric: tabular-nums;
|
||
color: var(--muted);
|
||
min-width: 120px;
|
||
}
|
||
.ww .ww-dur {
|
||
font-weight: 600;
|
||
flex: 1;
|
||
}
|
||
.ww.ongoing {
|
||
border-color: var(--accent);
|
||
background: var(--accent-soft);
|
||
}
|
||
/* Every row can carry a state, not only the open one: the merged sleep/wake
|
||
list labels each row asleep or awake, where the label is the whole point. */
|
||
.ww .ww-tag {
|
||
font-size: 0.75rem;
|
||
font-weight: 600;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.05em;
|
||
color: var(--muted);
|
||
}
|
||
/* A stripe in the row's own colour, so an alternating list reads as alternating
|
||
before you get to the words. */
|
||
.ww.sleep-ww { border-left: 3px solid color-mix(in srgb, var(--sleep) 55%, transparent); }
|
||
.ww.wake-ww { border-left: 3px solid color-mix(in srgb, var(--wake) 75%, transparent); }
|
||
.ww.walk-ww { border-left: 3px solid color-mix(in srgb, var(--walk) 55%, transparent); }
|
||
.ww.sleep-ww .ww-tag { color: var(--sleep-ink); }
|
||
.ww.wake-ww .ww-tag { color: var(--wake-ink); }
|
||
.ww.walk-ww .ww-tag { color: var(--walk); }
|
||
.ww.ongoing .ww-tag { color: var(--accent); }
|
||
.ww.sleep-ww.ongoing {
|
||
border-color: var(--sleep);
|
||
background: color-mix(in srgb, var(--sleep) 14%, var(--surface));
|
||
}
|
||
.ww.sleep-ww.ongoing .ww-tag { color: var(--sleep-ink); }
|
||
/* Spelled out like the other two: the faded stripe above would otherwise win
|
||
over the generic .ww.ongoing on specificity ties and leave the open awake
|
||
row without its full-strength edge. The tag needs saying too — .ww.wake-ww
|
||
.ww-tag ties with .ww.ongoing .ww-tag and loses on order. */
|
||
.ww.wake-ww.ongoing {
|
||
border-color: var(--wake);
|
||
background: color-mix(in srgb, var(--wake) 16%, var(--surface));
|
||
}
|
||
.ww.wake-ww.ongoing .ww-tag { color: var(--wake-ink); }
|
||
.ww.walk-ww.ongoing {
|
||
border-color: var(--walk);
|
||
background: color-mix(in srgb, var(--walk) 14%, var(--surface));
|
||
}
|
||
.ww.walk-ww.ongoing .ww-tag { color: var(--walk); }
|
||
|
||
.event {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
background: var(--bg);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius);
|
||
padding: 10px 12px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.event .dot {
|
||
width: 12px;
|
||
height: 12px;
|
||
border-radius: 50%;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
/* Rail bracketing a sleep or walk pair in the history log (see historyRails).
|
||
It is drawn in the section's own padding, left of the cards, so tying rows
|
||
together costs no layout: nothing indents and no row moves. The anchored end
|
||
stops at the row's middle, level with its dot; the other ends overshoot the
|
||
6px list gap by 4px each so consecutive segments overlap into one line. */
|
||
.event.rail { position: relative; }
|
||
.event.rail::before {
|
||
content: "";
|
||
position: absolute;
|
||
left: -10px;
|
||
width: 0;
|
||
border-left: 2px dotted var(--rail, var(--border));
|
||
}
|
||
.event.rail-top::before { top: 50%; bottom: -4px; }
|
||
.event.rail-mid::before { top: -4px; bottom: -4px; }
|
||
.event.rail-bottom::before { top: -4px; bottom: 50%; }
|
||
.event.rail-sleep { --rail: color-mix(in srgb, var(--sleep) 70%, transparent); }
|
||
.event.rail-walk { --rail: color-mix(in srgb, var(--walk) 70%, transparent); }
|
||
.event[data-type="sleep-start"] .dot,
|
||
.event[data-type="sleep-end"] .dot { background: var(--sleep); }
|
||
.event[data-type="eat"] .dot { background: var(--eat); }
|
||
.event[data-type="pee"] .dot { background: var(--pee); }
|
||
.event[data-type="poo"] .dot { background: var(--poo); }
|
||
.event[data-type="weight"] .dot { background: var(--weight); }
|
||
.event[data-type="training"] .dot { background: var(--training); }
|
||
.event[data-type="walk-start"] .dot,
|
||
.event[data-type="walk-end"] .dot { background: var(--walk); }
|
||
.event[data-type="note"] .dot { background: var(--note); }
|
||
|
||
.event .time { font-variant-numeric: tabular-nums; color: var(--muted); min-width: 60px; }
|
||
.event .label { font-weight: 600; min-width: 110px; }
|
||
.event .note { color: var(--muted); font-size: 0.9rem; flex: 1; }
|
||
|
||
/* Notes log rows: a date instead of a time-of-day, then the note text. */
|
||
.event .note-date { font-weight: 600; white-space: nowrap; font-variant-numeric: tabular-nums; }
|
||
.event .note-text { flex: 1; white-space: pre-wrap; overflow-wrap: anywhere; }
|
||
|
||
.empty {
|
||
color: var(--muted);
|
||
text-align: center;
|
||
padding: 12px 0;
|
||
}
|
||
|
||
.status-pill {
|
||
font-size: 0.75rem;
|
||
padding: 4px 10px;
|
||
border-radius: 999px;
|
||
background: var(--accent-soft);
|
||
color: var(--accent);
|
||
font-weight: 600;
|
||
}
|
||
.status-pill.offline { background: #ffe0e0; color: #b03030; }
|
||
.status-pill.error { background: #ffe0e0; color: #b03030; }
|
||
.status-pill.syncing { background: #e0eaff; color: #2a4ab0; }
|
||
.status-pill.pending { background: #fff1d4; color: #8a5a00; }
|
||
@media (prefers-color-scheme: dark) {
|
||
.status-pill.offline { background: #4a1f1f; color: #ff9090; }
|
||
.status-pill.error { background: #4a1f1f; color: #ff9090; }
|
||
.status-pill.syncing { background: #1f2c4a; color: #9eb6ff; }
|
||
.status-pill.pending { background: #4a3a1f; color: #ffd28a; }
|
||
}
|
||
|
||
dialog {
|
||
border: none;
|
||
border-radius: var(--radius);
|
||
padding: 20px;
|
||
background: var(--surface);
|
||
color: var(--text);
|
||
box-shadow: var(--shadow);
|
||
max-width: 480px;
|
||
width: calc(100% - 32px);
|
||
}
|
||
|
||
dialog::backdrop { background: rgba(0,0,0,0.4); }
|
||
|
||
dialog h3 { margin: 0 0 12px; }
|
||
|
||
dialog label { display: block; font-size: 0.85rem; color: var(--muted); margin-bottom: 10px; }
|
||
/* The display rule above beats the UA [hidden] rule, so hide explicitly —
|
||
otherwise the weight/grams fields show on event types that don't use them. */
|
||
dialog label[hidden] { display: none; }
|
||
dialog label input, dialog label textarea { margin-top: 4px; }
|
||
|
||
dialog menu {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 8px;
|
||
padding: 0;
|
||
margin: 14px 0 0;
|
||
}
|
||
|
||
.photo-field {
|
||
margin-top: 10px;
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
align-items: center;
|
||
}
|
||
.photo-field button { padding: 8px 12px; }
|
||
.photo-preview {
|
||
margin-top: 8px;
|
||
width: 100%;
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 10px;
|
||
}
|
||
.photo-preview[hidden] { display: none; }
|
||
.photo-thumb { position: relative; }
|
||
.photo-thumb img {
|
||
display: block;
|
||
width: 76px;
|
||
height: 76px;
|
||
object-fit: cover;
|
||
border-radius: 8px;
|
||
border: 1px solid var(--border);
|
||
background: var(--accent-soft);
|
||
}
|
||
.photo-thumb-remove {
|
||
position: absolute;
|
||
top: -7px;
|
||
right: -7px;
|
||
width: 22px;
|
||
height: 22px;
|
||
padding: 0;
|
||
border-radius: 50%;
|
||
background: var(--danger);
|
||
color: #fff;
|
||
font-size: 14px;
|
||
line-height: 1;
|
||
}
|
||
|
||
.event .thumb {
|
||
width: 40px;
|
||
height: 40px;
|
||
border-radius: 6px;
|
||
object-fit: cover;
|
||
border: 1px solid var(--border);
|
||
flex-shrink: 0;
|
||
cursor: zoom-in;
|
||
background: var(--accent-soft);
|
||
}
|
||
|
||
#lightbox {
|
||
background: #000;
|
||
padding: 0;
|
||
border: none;
|
||
max-width: 100vw;
|
||
max-height: 100vh;
|
||
width: auto;
|
||
height: auto;
|
||
}
|
||
#lightbox::backdrop { background: rgba(0,0,0,0.85); }
|
||
#lightbox img {
|
||
display: block;
|
||
max-width: 95vw;
|
||
max-height: 95vh;
|
||
object-fit: contain;
|
||
}
|
||
.lightbox-close {
|
||
position: absolute;
|
||
top: 8px;
|
||
right: 8px;
|
||
background: rgba(0,0,0,0.5);
|
||
color: #fff;
|
||
font-size: 1.5rem;
|
||
line-height: 1;
|
||
padding: 4px 12px 6px;
|
||
border-radius: 999px;
|
||
font-weight: 400;
|
||
}
|
||
|
||
.time-row {
|
||
display: flex;
|
||
gap: 8px;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
}
|
||
.time-row input { flex: 1 1 120px; min-width: 0; }
|
||
.time-row button { padding: 8px 12px; }
|
||
|
||
/* The chart window is a page-level control rather than a panel's, so its row
|
||
skips the card chrome the sections have: no surface, no shadow, just a label
|
||
and the buttons sitting on the page background between two cards. */
|
||
.chart-window {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
padding: 0 4px;
|
||
}
|
||
.chart-window-label {
|
||
font-size: 0.75rem;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.06em;
|
||
color: var(--muted);
|
||
}
|
||
|
||
.chart-days-picker {
|
||
display: flex;
|
||
gap: 6px;
|
||
margin: -4px 0 14px;
|
||
}
|
||
/* No heading above it out here, so nothing to pull up under or clear beneath. */
|
||
.chart-window .chart-days-picker { margin: 0; }
|
||
.chart-days-picker button {
|
||
padding: 5px 12px;
|
||
font-size: 0.8rem;
|
||
}
|
||
.chart-days-picker button.active {
|
||
background: var(--accent);
|
||
border-color: transparent;
|
||
color: #fff;
|
||
}
|
||
|
||
.chart { margin-bottom: 16px; }
|
||
.chart:last-child { margin-bottom: 0; }
|
||
.chart-title {
|
||
font-size: 0.8rem;
|
||
color: var(--muted);
|
||
margin-bottom: 6px;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.04em;
|
||
}
|
||
.chart-svg { display: block; width: 100%; height: auto; }
|
||
.chart-svg text { fill: var(--muted); font-size: 9px; }
|
||
/* Dense 1h y-axes: smaller labels so every gridline can carry one. */
|
||
.chart-svg text.y-dense { font-size: 7px; }
|
||
.chart-svg .grid {
|
||
stroke: var(--border);
|
||
stroke-dasharray: 2 3;
|
||
}
|
||
.chart-svg .bar { cursor: pointer; }
|
||
.chart-svg .bar-faded { opacity: 0.7; }
|
||
/* The selected day's slice in every per-day chart: a soft accent band behind
|
||
the bars/cells (visible even when the day's values are all zero) plus an
|
||
accent-colored day label. */
|
||
.chart-svg .day-highlight {
|
||
fill: var(--accent);
|
||
fill-opacity: 0.08;
|
||
stroke: var(--accent);
|
||
stroke-opacity: 0.45;
|
||
}
|
||
.chart-svg text.day-label-sel { fill: var(--accent); font-weight: 600; }
|
||
.chart-svg .bar-sleep { fill: var(--sleep); }
|
||
.chart-svg .bar-walk { fill: var(--walk); }
|
||
.chart-svg .bar-pee { fill: var(--pee); }
|
||
.chart-svg .bar-poo { fill: var(--poo); }
|
||
.chart-svg .bar-eat { fill: var(--eat); }
|
||
.chart-svg .weight-line { stroke: var(--weight); stroke-width: 2; fill: none; }
|
||
.chart-svg .weight-dot { fill: var(--weight); }
|
||
.chart-svg .weight-hit { fill: transparent; cursor: pointer; }
|
||
.chart-svg .weight-hit.active {
|
||
fill: color-mix(in srgb, var(--weight) 22%, transparent);
|
||
stroke: var(--weight);
|
||
stroke-width: 1.5;
|
||
}
|
||
|
||
.weight-point-info,
|
||
.hour-point-info {
|
||
margin: 6px 4px 0;
|
||
min-height: 1.2em;
|
||
text-align: center;
|
||
}
|
||
|
||
.weight-summary {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, 1fr);
|
||
gap: 10px;
|
||
margin-bottom: 16px;
|
||
}
|
||
.weight-summary .stat-value.up { color: var(--gain); }
|
||
.weight-summary .stat-value.down { color: var(--danger); }
|
||
.ww.weight-ww { cursor: pointer; }
|
||
/* Keep each weight row to one line: the date/age column shrinks and ellipses,
|
||
the weight value stays a fixed size, right-aligned and always visible. */
|
||
.ww.weight-ww .ww-range {
|
||
min-width: 0;
|
||
flex: 1;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
.ww.weight-ww .ww-dur { flex: 0 0 auto; text-align: right; }
|
||
|
||
.legend {
|
||
display: flex;
|
||
gap: 14px;
|
||
font-size: 0.8rem;
|
||
color: var(--muted);
|
||
margin-top: 8px;
|
||
flex-wrap: wrap;
|
||
}
|
||
.lg { display: inline-flex; align-items: center; gap: 5px; }
|
||
.lg .sw {
|
||
width: 10px;
|
||
height: 10px;
|
||
border-radius: 2px;
|
||
display: inline-block;
|
||
}
|
||
.lg.pee .sw { background: var(--pee); }
|
||
.lg.poo .sw { background: var(--poo); }
|
||
.lg.eat .sw { background: var(--eat); }
|
||
|
||
/* Interactive legend: each item is a checkbox that toggles its metric. */
|
||
.legend-toggle label.lg { cursor: pointer; user-select: none; }
|
||
.legend-toggle input[type="checkbox"] { margin: 0; cursor: pointer; }
|
||
.legend-toggle label.pee input { accent-color: var(--pee); }
|
||
.legend-toggle label.poo input { accent-color: var(--poo); }
|
||
.legend-toggle label.eat input { accent-color: var(--eat); }
|
||
/* Dim an unchecked item so it's clear its bars are hidden. */
|
||
.legend-toggle label.lg:has(input:not(:checked)) { opacity: 0.5; }
|
||
|
||
/* ---------- auth (login / register) ---------- */
|
||
.auth-screen {
|
||
position: fixed;
|
||
inset: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 20px;
|
||
background: var(--bg);
|
||
z-index: 50;
|
||
}
|
||
/* The display rule above beats the UA [hidden] rule, so hide explicitly. */
|
||
.auth-screen[hidden] { display: none; }
|
||
.auth-card {
|
||
width: 100%;
|
||
max-width: 360px;
|
||
background: var(--surface);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius);
|
||
box-shadow: var(--shadow);
|
||
padding: 28px 22px;
|
||
}
|
||
.auth-card h1 { margin: 0 0 4px; font-size: 1.5rem; text-align: center; }
|
||
.auth-sub { margin: 0 0 20px; text-align: center; color: var(--muted); font-size: 0.9rem; }
|
||
.auth-card label {
|
||
display: block;
|
||
font-size: 0.85rem;
|
||
color: var(--muted);
|
||
margin-bottom: 12px;
|
||
}
|
||
.auth-card input[type="email"],
|
||
.auth-card input[type="password"],
|
||
.auth-card input[type="text"] {
|
||
font: inherit;
|
||
background: var(--bg);
|
||
color: var(--text);
|
||
border: 1px solid var(--border);
|
||
border-radius: 8px;
|
||
padding: 10px 12px;
|
||
width: 100%;
|
||
margin-top: 4px;
|
||
}
|
||
.auth-card #auth-submit { width: 100%; margin-top: 6px; }
|
||
.auth-error {
|
||
color: var(--danger);
|
||
font-size: 0.85rem;
|
||
margin: 0 0 12px;
|
||
text-align: center;
|
||
}
|
||
.auth-toggle {
|
||
margin: 16px 0 0;
|
||
text-align: center;
|
||
font-size: 0.85rem;
|
||
color: var(--muted);
|
||
}
|
||
button.linklike {
|
||
background: none;
|
||
border: none;
|
||
color: var(--accent);
|
||
font-weight: 600;
|
||
padding: 0 0 0 4px;
|
||
display: inline;
|
||
cursor: pointer;
|
||
}
|
||
button.linklike:hover { text-decoration: underline; filter: none; }
|
||
|
||
/* ---------- delete account ---------- */
|
||
.settings-sep {
|
||
border: none;
|
||
border-top: 1px solid var(--border);
|
||
margin: 18px 0 12px;
|
||
}
|
||
.danger-block { width: 100%; }
|
||
.danger-text {
|
||
font-size: 0.9rem;
|
||
color: var(--muted);
|
||
margin: 0 0 14px;
|
||
line-height: 1.4;
|
||
}
|
||
.danger-text strong { color: var(--danger); }
|
||
|
||
/* ---------- settings toggle switch ---------- */
|
||
.toggle-row {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
}
|
||
.toggle-row > span { color: var(--text); font-size: 0.95rem; }
|
||
input.switch {
|
||
appearance: none;
|
||
-webkit-appearance: none;
|
||
width: 44px;
|
||
height: 26px;
|
||
margin: 0;
|
||
border-radius: 13px;
|
||
background: var(--border);
|
||
position: relative;
|
||
cursor: pointer;
|
||
flex-shrink: 0;
|
||
transition: background 0.15s ease;
|
||
}
|
||
input.switch::after {
|
||
content: "";
|
||
position: absolute;
|
||
top: 3px;
|
||
left: 3px;
|
||
width: 20px;
|
||
height: 20px;
|
||
border-radius: 50%;
|
||
background: #fff;
|
||
transition: transform 0.15s ease;
|
||
}
|
||
input.switch:checked { background: var(--accent); }
|
||
input.switch:checked::after { transform: translateX(18px); }
|
||
|
||
/* ---------- update banner ---------- */
|
||
.update-banner {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
z-index: 70;
|
||
padding: calc(10px + env(safe-area-inset-top, 0)) 16px 10px;
|
||
background: var(--accent);
|
||
color: #fff;
|
||
box-shadow: var(--shadow);
|
||
}
|
||
.update-banner[hidden] { display: none; }
|
||
.update-banner-row {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 12px;
|
||
flex-wrap: wrap;
|
||
}
|
||
.update-changelog {
|
||
max-width: 480px;
|
||
margin: 6px auto 0;
|
||
padding: 0 0 0 18px;
|
||
font-size: 0.8rem;
|
||
line-height: 1.45;
|
||
opacity: 0.92;
|
||
}
|
||
.update-changelog[hidden] { display: none; }
|
||
|
||
.app-footer {
|
||
text-align: center;
|
||
padding: 4px 0 24px;
|
||
}
|
||
.app-footer .linklike { color: var(--muted); font-size: 0.8rem; }
|
||
|
||
.changelog-list {
|
||
list-style: none;
|
||
margin: 0 0 12px;
|
||
padding: 0;
|
||
max-height: 60vh;
|
||
overflow-y: auto;
|
||
}
|
||
.changelog-date {
|
||
color: var(--muted);
|
||
font-size: 0.75rem;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.04em;
|
||
margin: 10px 0 4px;
|
||
}
|
||
.changelog-date:first-child { margin-top: 0; }
|
||
.changelog-entry {
|
||
font-size: 0.9rem;
|
||
line-height: 1.45;
|
||
padding: 3px 0 3px 14px;
|
||
position: relative;
|
||
}
|
||
.changelog-entry::before {
|
||
content: "•";
|
||
position: absolute;
|
||
left: 2px;
|
||
color: var(--accent);
|
||
}
|
||
.update-banner-msg { font-size: 0.9rem; font-weight: 600; }
|
||
.update-banner-actions { display: flex; gap: 8px; }
|
||
.update-banner-btn {
|
||
background: #fff;
|
||
color: var(--accent);
|
||
font-weight: 700;
|
||
padding: 6px 14px;
|
||
}
|
||
.update-banner-btn.ghostish {
|
||
background: transparent;
|
||
color: #fff;
|
||
border: 1px solid rgba(255, 255, 255, 0.6);
|
||
}
|
||
.update-banner-btn:hover { filter: brightness(0.97); }
|
||
|
||
/* ---------- quick-log snackbar ---------- */
|
||
.snackbar {
|
||
position: fixed;
|
||
left: 50%;
|
||
bottom: calc(16px + env(safe-area-inset-bottom, 0));
|
||
transform: translate(-50%, 12px);
|
||
z-index: 60;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
max-width: calc(100% - 32px);
|
||
padding: 8px 8px 8px 16px;
|
||
background: var(--surface);
|
||
color: var(--text);
|
||
border: 1px solid var(--border);
|
||
border-radius: 999px;
|
||
box-shadow: var(--shadow);
|
||
opacity: 0;
|
||
pointer-events: none;
|
||
transition: opacity 0.18s ease, transform 0.18s ease;
|
||
}
|
||
.snackbar[hidden] { display: none; }
|
||
.snackbar.show {
|
||
opacity: 1;
|
||
transform: translate(-50%, 0);
|
||
pointer-events: auto;
|
||
}
|
||
.snackbar-msg {
|
||
font-size: 0.9rem;
|
||
font-weight: 500;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
.snackbar-action {
|
||
background: transparent;
|
||
color: var(--accent);
|
||
font-weight: 700;
|
||
padding: 6px 10px;
|
||
flex-shrink: 0;
|
||
}
|
||
.snackbar-action:hover { filter: none; background: var(--accent-soft); }
|
||
|
||
/* ---------- pattern charts (sleep timeline + hour heatmap) ---------- */
|
||
.chart-svg .stl-track {
|
||
fill: var(--bg);
|
||
stroke: var(--border);
|
||
stroke-width: 0.5;
|
||
}
|
||
.chart-svg .stl-sleep { fill: var(--sleep); }
|
||
/* Same actogram, walk windows instead of sleep ones (see drawActogram). */
|
||
.chart-svg .wtl-walk { fill: var(--walk); }
|
||
.chart-svg .stl-today { fill: var(--accent); font-weight: 600; }
|
||
/* Selected day: accent ring on the row's track + accent label. */
|
||
.chart-svg .stl-track.stl-selected {
|
||
stroke: var(--accent);
|
||
stroke-width: 1.2;
|
||
stroke-opacity: 0.8;
|
||
}
|
||
.chart-svg .stl-sel { fill: var(--accent); }
|
||
.chart-svg .stl-hit { fill: transparent; cursor: pointer; }
|
||
.chart-svg .stl-hit:hover { fill: var(--accent); fill-opacity: 0.08; }
|
||
|
||
/* Sleep trend lines: today strongest, the reference curves lighter/dashed. */
|
||
.chart-svg .trend-today {
|
||
stroke: var(--sleep);
|
||
stroke-width: 2.5;
|
||
stroke-linejoin: round;
|
||
fill: none;
|
||
}
|
||
.chart-svg .trend-yesterday {
|
||
stroke: var(--eat); /* orange — the gray it had before sank into the grid */
|
||
stroke-width: 1.5;
|
||
stroke-linejoin: round;
|
||
fill: none;
|
||
opacity: 0.85;
|
||
}
|
||
.chart-svg .trend-avg {
|
||
stroke: var(--weight); /* teal — keeps it apart from today's blue and its blue projected tail */
|
||
stroke-width: 1.5;
|
||
stroke-dasharray: 4 3;
|
||
stroke-linejoin: round;
|
||
fill: none;
|
||
opacity: 0.85;
|
||
}
|
||
/* Today's projected tail: same weight/color as today so it reads as its
|
||
continuation, dashed + faded because it hasn't happened yet. */
|
||
.chart-svg .trend-projected {
|
||
stroke: var(--sleep);
|
||
stroke-width: 2.5;
|
||
stroke-dasharray: 2 4;
|
||
stroke-linecap: round;
|
||
stroke-linejoin: round;
|
||
fill: none;
|
||
opacity: 0.55;
|
||
}
|
||
.chart-svg .trend-goal { fill: var(--sleep); fill-opacity: 0.12; }
|
||
.lg.trend-goal .sw { background: color-mix(in srgb, var(--sleep) 20%, var(--surface)); }
|
||
.lg.trend-today .sw { background: var(--sleep); }
|
||
.lg.trend-projected .sw { background: color-mix(in srgb, var(--sleep) 40%, var(--surface)); }
|
||
.lg.trend-yesterday .sw { background: var(--eat); }
|
||
.lg.trend-avg .sw { background: var(--weight); }
|
||
|
||
/* Walk trend: the sleep trend's line weights, in the walk palette. The goal is
|
||
a single line rather than a band — a walk total is a target to reach, with no
|
||
upper bound to overshoot. */
|
||
.chart-svg .wtrend-today {
|
||
stroke: var(--walk);
|
||
stroke-width: 2.5;
|
||
stroke-linejoin: round;
|
||
fill: none;
|
||
}
|
||
.chart-svg .wtrend-yesterday {
|
||
stroke: var(--eat);
|
||
stroke-width: 1.5;
|
||
stroke-linejoin: round;
|
||
fill: none;
|
||
opacity: 0.85;
|
||
}
|
||
.chart-svg .wtrend-avg {
|
||
stroke: var(--weight);
|
||
stroke-width: 1.5;
|
||
stroke-dasharray: 4 3;
|
||
stroke-linejoin: round;
|
||
fill: none;
|
||
opacity: 0.85;
|
||
}
|
||
.chart-svg .wtrend-goal {
|
||
stroke: var(--walk);
|
||
stroke-width: 1.5;
|
||
stroke-dasharray: 2 3;
|
||
opacity: 0.55;
|
||
}
|
||
.lg.wtrend-today .sw { background: var(--walk); }
|
||
.lg.wtrend-yesterday .sw { background: var(--eat); }
|
||
.lg.wtrend-avg .sw { background: var(--weight); }
|
||
.lg.wtrend-goal .sw { background: color-mix(in srgb, var(--walk) 45%, var(--surface)); }
|
||
.lg[hidden] { display: none; }
|
||
|
||
.chart-svg .hm-cell { stroke: none; }
|
||
/* Focused block: an accent ring, drawn at full opacity so it stays visible on
|
||
the palest (zero-count) cells. */
|
||
.chart-svg .hm-cell.hm-active {
|
||
stroke: var(--accent);
|
||
stroke-width: 1.2;
|
||
}
|
||
.chart-svg .hm-hit { fill: transparent; cursor: pointer; }
|
||
.chart-svg .hm-pee { fill: var(--pee); }
|
||
.chart-svg .hm-poo { fill: var(--poo); }
|
||
.chart-svg .hm-eat { fill: var(--eat); }
|
||
.chart-svg .hm-training { fill: var(--training); }
|
||
|
||
/* ---------- training ---------- */
|
||
.training-list {
|
||
list-style: none;
|
||
margin: 0 0 10px;
|
||
padding: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
.training-list:empty { margin: 0; }
|
||
|
||
.exercise {
|
||
background: var(--bg);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius);
|
||
padding: 10px 12px;
|
||
}
|
||
|
||
.ex-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.ex-main {
|
||
flex: 1;
|
||
min-width: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 2px;
|
||
}
|
||
|
||
.ex-name { font-weight: 600; }
|
||
.ex-meta { color: var(--muted); font-size: 0.8rem; }
|
||
|
||
button.ex-log {
|
||
background: var(--training);
|
||
padding: 8px 14px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.ex-detail { display: none; }
|
||
.exercise.expanded .ex-detail {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 10px;
|
||
margin-top: 10px;
|
||
padding-top: 10px;
|
||
border-top: 1px solid var(--border);
|
||
}
|
||
|
||
.ex-note {
|
||
flex: 1;
|
||
margin: 0;
|
||
color: var(--muted);
|
||
font-size: 0.9rem;
|
||
line-height: 1.4;
|
||
white-space: pre-wrap;
|
||
}
|
||
|
||
button.ex-edit { padding: 6px 12px; flex-shrink: 0; }
|
||
|
||
.training-add { width: 100%; }
|
||
/* Charts that sit under a panel's list rather than being the whole panel, so
|
||
they need the separation a list-then-chart stack doesn't get for free. */
|
||
.training-chart { margin-top: 16px; }
|
||
.walk-chart { margin-top: 16px; }
|
||
|
||
/* ---------- collapsible panels ---------- */
|
||
/* A panel folds by its own heading: h2 for a whole section, h3 for a block
|
||
inside one. Scoped to main on purpose — the pedigree tree uses .collapsed
|
||
for its own branches, and a bare selector here would fold those too. */
|
||
main .collapsible > h2,
|
||
main .collapsible > h3 {
|
||
cursor: pointer;
|
||
position: relative;
|
||
padding-right: 20px;
|
||
-webkit-user-select: none;
|
||
user-select: none;
|
||
}
|
||
main .collapsible > h2::after,
|
||
main .collapsible > h3::after {
|
||
content: "▾";
|
||
position: absolute;
|
||
right: 0;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
transition: transform 0.15s ease;
|
||
color: var(--muted);
|
||
font-size: 0.85em;
|
||
}
|
||
main .collapsed > h2::after,
|
||
main .collapsed > h3::after { transform: translateY(-50%) rotate(-90deg); }
|
||
main .collapsed > h2,
|
||
main .collapsed > h3 { margin-bottom: 0; }
|
||
main .collapsed > :not(h2):not(h3) { display: none; }
|
||
|
||
/* Sub-heading for a foldable block inside a panel: the section h2's look, a
|
||
size down, so it reads as subordinate to it rather than as a second panel. */
|
||
.subpanel > h3 {
|
||
font-size: 0.8rem;
|
||
font-weight: 600;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.06em;
|
||
color: var(--muted);
|
||
margin: 0 0 10px;
|
||
}
|
||
|
||
/* ---------- pedigree lookup ---------- */
|
||
.pedigree-screen {
|
||
padding-top: 12px;
|
||
}
|
||
.pedigree-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
padding: 12px 4px 8px;
|
||
}
|
||
.pedigree-header { flex-wrap: wrap; }
|
||
.pedigree-header h1 { font-size: 1.4rem; }
|
||
.ped-controls {
|
||
margin-left: auto;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
flex-shrink: 0;
|
||
}
|
||
#ped-foldall { padding: 5px 10px; font-size: 0.8rem; white-space: nowrap; }
|
||
.ped-zoom {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
flex-shrink: 0;
|
||
}
|
||
.ped-zoom .icon-btn { padding: 4px 9px; font-size: 1.1rem; }
|
||
#ped-zoom-reset {
|
||
padding: 5px 8px;
|
||
font-size: 0.8rem;
|
||
font-variant-numeric: tabular-nums;
|
||
min-width: 48px;
|
||
}
|
||
|
||
.pedigree-hint { margin: 4px 0 12px; }
|
||
/* The tree scales with the CSS `zoom` property, which reflows so the container
|
||
still scrolls to reach the edges at any zoom. */
|
||
.pedigree-tree { touch-action: pan-x pan-y; }
|
||
|
||
.pedigree-status {
|
||
margin: 10px 0;
|
||
font-size: 0.9rem;
|
||
color: var(--muted);
|
||
}
|
||
.pedigree-status.busy::before {
|
||
content: "";
|
||
display: inline-block;
|
||
width: 12px; height: 12px;
|
||
margin-right: 8px;
|
||
vertical-align: -1px;
|
||
border: 2px solid var(--accent);
|
||
border-top-color: transparent;
|
||
border-radius: 50%;
|
||
animation: ped-spin 0.8s linear infinite;
|
||
}
|
||
@keyframes ped-spin { to { transform: rotate(360deg); } }
|
||
.pedigree-status.error { color: var(--danger); }
|
||
.pedigree-status.done { color: var(--gain); }
|
||
|
||
/* disambiguation list */
|
||
.pedigree-choose {
|
||
display: grid;
|
||
gap: 8px;
|
||
margin: 8px 0 16px;
|
||
}
|
||
.ped-match {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 2px;
|
||
text-align: left;
|
||
padding: 10px 12px;
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius);
|
||
background: var(--surface);
|
||
color: var(--text);
|
||
cursor: pointer;
|
||
}
|
||
.ped-match:hover { border-color: var(--accent); }
|
||
.ped-match-name { font-weight: 600; }
|
||
.ped-match-meta { font-size: 0.8rem; color: var(--muted); }
|
||
|
||
/* looked-up dog */
|
||
.pedigree-subject {
|
||
margin: 6px 0 14px;
|
||
padding: 12px 14px;
|
||
border: 1px solid var(--border);
|
||
border-left: 4px solid var(--accent);
|
||
border-radius: var(--radius);
|
||
background: var(--surface);
|
||
}
|
||
.ped-subject-name { font-size: 1.15rem; font-weight: 700; }
|
||
.ped-subject-meta { font-size: 0.85rem; color: var(--muted); margin-top: 2px; }
|
||
|
||
/* Top-down family tree: the dog on top, parents branching below, connected by
|
||
lines drawn with each <li>'s ::before/::after (the classic CSS tree). Wider
|
||
than the screen once expanded, so the container scrolls horizontally. */
|
||
.pedigree-tree {
|
||
overflow-x: auto;
|
||
padding: 8px 0 28px;
|
||
}
|
||
.ped-tree-h, .ped-tree-h ul {
|
||
display: flex;
|
||
list-style: none;
|
||
margin: 0;
|
||
padding: 0;
|
||
}
|
||
.ped-tree-h {
|
||
/* "safe" centers when the tree fits and falls back to start-aligned (no
|
||
clipped/unreachable left edge) once it's wider than the screen. */
|
||
justify-content: safe center;
|
||
min-width: max-content;
|
||
padding: 4px 16px 12px;
|
||
}
|
||
.ped-tree-h ul {
|
||
justify-content: center;
|
||
position: relative;
|
||
padding-top: 22px; /* room for the connector from the parent above */
|
||
}
|
||
.ped-tree-h li {
|
||
position: relative;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
padding: 22px 6px 0;
|
||
}
|
||
/* Elbow from each child up to the horizontal bar shared by its siblings. */
|
||
.ped-tree-h li::before,
|
||
.ped-tree-h li::after {
|
||
content: "";
|
||
position: absolute;
|
||
top: 0;
|
||
width: 50%;
|
||
height: 22px;
|
||
border-top: 2px solid var(--border);
|
||
}
|
||
.ped-tree-h li::before { right: 50%; }
|
||
.ped-tree-h li::after { left: 50%; border-left: 2px solid var(--border); }
|
||
/* Vertical drop from a parent card down to its children's bar. */
|
||
.ped-tree-h ul::before {
|
||
content: "";
|
||
position: absolute;
|
||
top: 0;
|
||
left: 50%;
|
||
height: 22px;
|
||
border-left: 2px solid var(--border);
|
||
}
|
||
/* A lone parent connects with a straight line, no elbow. */
|
||
.ped-tree-h li:only-child::before,
|
||
.ped-tree-h li:only-child::after { display: none; }
|
||
/* Trim the outer half-lines at the ends of a sibling row. */
|
||
.ped-tree-h li:first-child::before,
|
||
.ped-tree-h li:last-child::after { border: 0 none; }
|
||
.ped-tree-h li:last-child::before { border-right: 2px solid var(--border); }
|
||
/* The dog sits on top with no connector above it. */
|
||
.ped-tree-h > li { padding-top: 0; }
|
||
.ped-tree-h > li::before,
|
||
.ped-tree-h > li::after { display: none; }
|
||
/* Collapsed: hide the ancestry below this dog (and its connectors go with it). */
|
||
.ped-tree-h li.collapsed > ul { display: none; }
|
||
|
||
.ped-card {
|
||
position: relative;
|
||
width: 140px;
|
||
box-sizing: border-box;
|
||
padding: 7px 18px 7px 20px;
|
||
border: 1px solid var(--border);
|
||
border-radius: 10px;
|
||
background: var(--surface);
|
||
box-shadow: var(--shadow);
|
||
text-align: center;
|
||
}
|
||
.ped-name { font-weight: 600; font-size: 0.85rem; line-height: 1.2; }
|
||
.ped-name.ped-unknown { color: var(--muted); font-weight: 500; font-style: italic; }
|
||
.ped-titles { font-size: 0.66rem; color: var(--accent); margin-top: 2px; line-height: 1.2; }
|
||
.ped-reg {
|
||
font-size: 0.66rem;
|
||
color: var(--muted);
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||
margin-top: 1px;
|
||
}
|
||
/* sire ♂ (blue) / dam ♀ (purple) accents on the parent cards */
|
||
.ped-sire > .ped-card { border-top: 3px solid var(--sleep); }
|
||
.ped-dam > .ped-card { border-top: 3px solid var(--training); }
|
||
.ped-sire > .ped-card::after,
|
||
.ped-dam > .ped-card::after {
|
||
position: absolute;
|
||
right: 6px;
|
||
top: 5px;
|
||
font-size: 0.72rem;
|
||
}
|
||
.ped-sire > .ped-card::after { content: "♂"; color: var(--sleep); }
|
||
.ped-dam > .ped-card::after { content: "♀"; color: var(--training); }
|
||
|
||
/* pedigree collapse: a dog filling more than one position gets a ×N badge, a
|
||
stable hue, and lights up (with every copy) when tapped. */
|
||
.ped-card.ped-repeat { cursor: pointer; }
|
||
.ped-repeat-badge {
|
||
position: absolute;
|
||
right: 5px;
|
||
bottom: 5px;
|
||
font-size: 0.6rem;
|
||
font-weight: 700;
|
||
line-height: 1;
|
||
padding: 2px 5px;
|
||
border-radius: 999px;
|
||
color: #fff;
|
||
background: hsl(var(--repeat-hue, 0), 58%, 48%);
|
||
}
|
||
.ped-card.ped-lit {
|
||
border-color: hsl(var(--repeat-hue, 0), 70%, 50%);
|
||
box-shadow: 0 0 0 2px hsl(var(--repeat-hue, 0), 70%, 50%), var(--shadow);
|
||
}
|
||
.pedigree-repeat-note { margin: 0 0 10px; }
|
||
|
||
/* expand/collapse toggle (top-left of the card) */
|
||
.ped-toggle {
|
||
position: absolute;
|
||
left: 5px;
|
||
top: 5px;
|
||
width: 18px; height: 18px;
|
||
padding: 0;
|
||
font-size: 0.9rem;
|
||
line-height: 1;
|
||
border: 1px solid var(--border);
|
||
border-radius: 5px;
|
||
background: var(--bg);
|
||
color: var(--muted);
|
||
cursor: pointer;
|
||
}
|
||
.ped-toggle:hover { border-color: var(--accent); color: var(--accent); }
|
||
|
||
/* ---- radial fan chart ---- */
|
||
.ped-fan { display: block; margin: 0 auto; max-width: none; }
|
||
.ped-wedge {
|
||
cursor: pointer;
|
||
stroke: var(--border);
|
||
stroke-width: 1;
|
||
/* outer rings tint gradually darker for depth */
|
||
fill: color-mix(in srgb, var(--accent-soft) calc(var(--gen, 1) * 7%), var(--surface));
|
||
transition: filter 0.1s ease;
|
||
}
|
||
.ped-wedge:hover { filter: brightness(0.95); }
|
||
/* a dog that appears more than once is filled with its stable hue */
|
||
.ped-wedge-repeat { fill: hsl(var(--repeat-hue, 0), 60%, 80%); stroke: hsl(var(--repeat-hue, 0), 45%, 60%); }
|
||
.ped-wedge.ped-lit {
|
||
fill: hsl(var(--repeat-hue, 0), 72%, 62%);
|
||
stroke: hsl(var(--repeat-hue, 0), 72%, 38%);
|
||
stroke-width: 2;
|
||
}
|
||
.ped-wedge-label {
|
||
font-size: 8px;
|
||
fill: var(--text);
|
||
text-anchor: middle;
|
||
dominant-baseline: central;
|
||
pointer-events: none;
|
||
}
|
||
.ped-fan-center { fill: var(--accent); stroke: none; cursor: pointer; }
|
||
.ped-fan-center-label {
|
||
fill: #fff;
|
||
font-size: 9px;
|
||
font-weight: 600;
|
||
dominant-baseline: central;
|
||
pointer-events: none;
|
||
}
|
||
@media (prefers-color-scheme: dark) {
|
||
:root:not([data-theme="light"]) .ped-wedge-label { fill: var(--text); }
|
||
}
|
||
.pedigree-caption {
|
||
margin: 10px 0 0;
|
||
font-size: 0.85rem;
|
||
color: var(--muted);
|
||
text-align: center;
|
||
}
|
||
|
||
/* ---- pee/poo confetti ---- */
|
||
#confetti-layer {
|
||
position: fixed;
|
||
inset: 0;
|
||
pointer-events: none;
|
||
overflow: hidden;
|
||
z-index: 9999;
|
||
}
|
||
.confetti-piece {
|
||
position: absolute;
|
||
line-height: 1;
|
||
will-change: transform, opacity;
|
||
animation-name: potty-firework;
|
||
animation-timing-function: ease-out;
|
||
/* `both` so the 0% state (invisible, at the bottom) also applies during the
|
||
per-piece launch delay — no flash before it takes off. */
|
||
animation-fill-mode: both;
|
||
}
|
||
/* Launch up from the bottom, rise to a peak while spreading sideways, slowing
|
||
(ease-out) and fading out as it reaches the top — a firework fountain.
|
||
Distances come from JS custom props. */
|
||
@keyframes potty-firework {
|
||
0% { opacity: 0; transform: translate(-50%, -50%) scale(0.5) rotate(0deg); }
|
||
10% { opacity: 1; }
|
||
70% { opacity: 1; }
|
||
100% { opacity: 0; transform: translate(calc(-50% + var(--dx)), calc(-50% + var(--peakY))) scale(1) rotate(var(--rot)); }
|
||
}
|
||
@media (prefers-reduced-motion: reduce) {
|
||
.confetti-piece { display: none; }
|
||
}
|
||
|
||
/* ---------- reminders ---------- */
|
||
.settings-subhead {
|
||
margin: 0 0 10px;
|
||
font-size: 0.95rem;
|
||
color: var(--text);
|
||
}
|
||
#reminders-rules {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
margin: 12px 0;
|
||
}
|
||
.reminder-row {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 10px;
|
||
}
|
||
/* Overrides dialog's block labels: each rule is a switch and its name on one
|
||
line, with the interval pushed to the far end. */
|
||
.reminder-row .reminder-name {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
margin: 0;
|
||
min-width: 0;
|
||
font-size: 0.9rem;
|
||
color: var(--text);
|
||
cursor: pointer;
|
||
}
|
||
.reminder-row .reminder-name input { margin-top: 0; }
|
||
.reminder-after {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
flex-shrink: 0;
|
||
font-size: 0.85rem;
|
||
color: var(--muted);
|
||
}
|
||
input.reminder-mins {
|
||
width: 4.75rem;
|
||
padding: 6px 8px;
|
||
text-align: right;
|
||
}
|
||
/* The full-size switch crowds a row that also carries a number field. */
|
||
.reminder-row input.switch {
|
||
width: 36px;
|
||
height: 22px;
|
||
border-radius: 11px;
|
||
}
|
||
.reminder-row input.switch::after {
|
||
top: 3px;
|
||
left: 3px;
|
||
width: 16px;
|
||
height: 16px;
|
||
}
|
||
.reminder-row input.switch:checked::after { transform: translateX(14px); }
|
||
#reminders-test { width: 100%; }
|