Files
puppy-tracker/src/style.css
T
Alexander Heldt 7a1c0f3808 Shorten the weigh-in label to "Weight"
"Weigh-in" was the longest label in the app and, since it moved into a
third-width button, close enough to the edge to wrap on a mid-size phone.
"Weight" clears it with room to spare and loses nothing: the section, the
chart and the dialog field are all called Weight already.

EVENT_LABELS follows the button rather than only the button changing. Every
type's button label and its log label are the same string today, and weight
would have been the one exception — "⚖️ Weight" to log it, "Weigh-in" in the
history row and "Log Weigh-in" on the dialog. The prose that says "weigh-ins"
as a plural noun is left alone; it reads as English, not as a label.

The changelog entry for the rows is amended again rather than gaining a
neighbour, for the same reason as last time: it is one description of one
panel, and the rename is part of what that panel now looks like.
2026-08-31 21:42:20 +00:00

1554 lines
40 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
: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;
--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;
--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;
--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); }
.bar-clock.awake { background: color-mix(in srgb, var(--accent) 18%, var(--surface)); color: var(--accent); }
.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); }
.big-clock.awake { background: linear-gradient(180deg, var(--surface), color-mix(in srgb, var(--accent) 10%, var(--surface))); }
.big-clock.awake .bc-time { color: var(--accent); }
/* 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-hint { margin: 10px 4px 0; line-height: 1.4; }
/* ---------- 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);
}
.ww.ongoing .ww-tag {
font-size: 0.75rem;
color: var(--accent);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.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); }
.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; }
.chart-days-picker {
display: flex;
gap: 6px;
margin: -4px 0 14px;
}
.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); }
.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); }
.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%; }
.training-chart { margin-top: 16px; }
/* ---------- collapsible panels ---------- */
section.collapsible > h2 {
cursor: pointer;
position: relative;
padding-right: 20px;
-webkit-user-select: none;
user-select: none;
}
section.collapsible > h2::after {
content: "▾";
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
transition: transform 0.15s ease;
color: var(--muted);
font-size: 0.85em;
}
section.collapsed > h2::after { transform: translateY(-50%) rotate(-90deg); }
section.collapsed > h2 { margin-bottom: 0; }
section.collapsed > :not(h2) { display: none; }
/* ---------- 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%; }