The list said awake in gold while the big card and the day-bar pill still said it in purple, which left the app describing one state in two colours. Both move across, so asleep against awake is night-blue against day-gold everywhere. Text takes --wake-ink, not --wake. A gold light enough to read as sunshine is not legible as text on a pale ground: the bright value measures 1.71:1 against the card it would sit on, where the ink makes 4.96, and 4.71 on the pill's tint — the same split the pee button has always used, dark ink on the yellow rather than yellow ink. Worth noting for later: this now makes awake considerably easier to read than asleep, whose blue-on-blue pill has been sitting at 2.57:1 since long before any of this. Untouched, being a separate question about the sleep colour.
1641 lines
44 KiB
CSS
1641 lines
44 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;
|
||
/* Awake = sunshine. The bright gold is for stripes and fills; --wake-ink is
|
||
the same warmth pulled dark enough for a small uppercase label to be
|
||
readable, and is the one value that has to flip with the theme. */
|
||
--wake: #f2b705;
|
||
--wake-ink: #8f6400;
|
||
--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;
|
||
--wake-ink: #f5c451;
|
||
--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;
|
||
--wake-ink: #f5c451;
|
||
--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); }
|
||
/* 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. */
|
||
.bar-clock.awake { background: color-mix(in srgb, var(--wake) 18%, var(--surface)); color: var(--wake-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); }
|
||
.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-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-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);
|
||
}
|
||
/* 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); }
|
||
.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); }
|
||
/* 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; }
|
||
|
||
.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); }
|
||
/* 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%; }
|