Files
Waters ChurchandClaude Opus 5 469db5bc60 Bring the checklist app under version control
The project has lived only as a folder on the Mac Studio and, briefly, as a
transfer tarball. This is the first commit: the Express server and its web
front end, the Capacitor iOS and Android wrappers, and the release and backup
scripts.

The repo is the source, not a backup of the machine. Left out on purpose, by
the .gitignore files that were already in the tree: node_modules, build output,
mobile/www (regenerated from server/public on every build), and server/data,
which holds the live checklists and belongs on the NAS rather than in git.

Every credential is excluded too — server/.env, the Play upload keystore and
its properties file, the App Store Connect key and ios-signing.config. The one
secret that was not in an ignored file was the NAS SSH password, hard-coded in
build-deploy.sh and grepped back out of it by backup-to-drive.sh. Both now read
it from the login keychain (service checklist-nas, account Christian), falling
back to NAS_PASS in the environment, so rotating it is one keychain entry
rather than a history rewrite.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-24 13:00:57 -04:00

3076 lines
115 KiB
CSS
Raw Permalink 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.
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* Reserve the scrollbar gutter so opening a modal (which locks scroll, hiding the
scrollbar) doesn't shift the page sideways on desktop. */
html { scrollbar-gutter: stable; }
/* Scroll lock: while any overlay (modal / ☰ menu / Move popover) is open, the page
behind it can't scroll. Toggled on <html> and <body> by syncScrollLock(). */
html.scroll-locked, body.scroll-locked { overflow: hidden; }
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background: #14161b;
color: #eceef2;
min-height: 100vh;
min-height: 100dvh; /* at least the viewport, but grow with content so the whole page scrolls */
touch-action: manipulation;
/* Respect iOS notch / home indicator when installed as a PWA. The TOP inset is
carried by the sticky header (below) instead of the body, so the header's
opaque background covers the notch strip and scrolled content can't peek above it. */
padding: 0 env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
box-sizing: border-box;
}
.loading-overlay {
position: fixed;
inset: 0;
background: #14161b;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
gap: 12px;
z-index: 9999;
}
.loading-spinner {
width: 48px;
height: 48px;
border: 4px solid #2f3542;
border-top-color: #39a8ff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.loading-text {
color: #cfd5e3;
font-size: 14px;
letter-spacing: 0.2px;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.app-container {
display: flex;
flex-direction: column;
min-height: 100vh;
min-height: 100dvh;
}
header {
background: #1d2026;
color: white;
/* top padding carries the PWA notch inset so the opaque header fills it */
padding: calc(15px + env(safe-area-inset-top)) 20px 15px;
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
border-bottom: 1px solid #2e333d;
display: flex;
align-items: center;
/* flex-end so wrapped rows (toolbar / badge on narrower tablets) stay
right-aligned. The title's margin-right:auto still pins it far-left and
keeps the one-line desktop grouping (title · ...toolbar+badge). */
justify-content: flex-end;
gap: 16px;
flex-wrap: wrap;
/* The single sticky bar: the title, all list controls (view/hide toggles,
filters), the ☰ Menu, and the user badges all live here and stay pinned
while the board scrolls. Nothing else is sticky. */
position: sticky;
top: 0;
z-index: 30;
}
.header-left {
display: flex;
align-items: center;
gap: 12px;
margin-right: auto; /* title hugs the left; everything after it groups on the right */
}
/* All admin/list controls grouped on the right of the title bar. */
.header-controls {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
/* Undo / redo: compact icon buttons (full-admin only). */
.undo-redo {
display: inline-flex;
gap: 4px;
}
.icon-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 34px;
height: 34px;
padding: 0;
font-size: 17px;
line-height: 1;
background: #1d2026;
border: 1px solid #4a4a4a;
border-radius: 6px;
color: #cfd5e3;
cursor: pointer;
transition: background 0.15s ease, border-color 0.15s ease, opacity 0.15s ease;
}
.icon-btn:hover:not(:disabled) { background: #252932; border-color: #39a8ff; }
.icon-btn:disabled { opacity: 0.35; cursor: default; }
.icon-btn:focus-visible { outline: 2px solid #39a8ff; outline-offset: 2px; }
/* Search stays an icon button on desktop (label hidden); on mobile it becomes a
full search bar that fills the top row — see the ≤700 block. */
.search-bar-btn .sb-label { display: none; }
.header-right {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.user-badge {
display: inline-flex;
align-items: center;
gap: 6px;
background: #191c22;
border: 1px solid #2e333d;
color: #cfd5e3;
padding: 6px 12px;
border-radius: 999px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: background 0.2s, border-color 0.2s;
}
.user-badge:hover {
background: #2b2b2b;
border-color: #39a8ff;
}
.user-badge-icon {
font-size: 14px;
line-height: 1;
}
body.admin-mode .user-badge {
border-color: #f1c40f;
color: #f1c40f;
}
#adminToggleBtn {
padding: 6px 12px;
font-size: 12px;
}
body.admin-mode #adminToggleBtn {
background: #f1c40f;
color: #14161b;
font-weight: 600;
}
.app-tabs {
display: flex;
gap: 8px;
}
.tab-button {
padding: 8px 14px;
border-radius: 6px;
border: 1px solid #2e333d;
background: #191c22;
color: #cfd5e3;
cursor: pointer;
font-weight: 600;
transition: all 0.2s;
}
.tab-button.active {
background: #39a8ff;
color: #fff;
border-color: #39a8ff;
box-shadow: 0 2px 8px rgba(57,168,255,0.35);
}
/* Apply hover only where a true (mouse) pointer exists. On touch tablets a
tapped element keeps its :hover state ("sticky hover"); because :hover is
declared after .active with equal specificity, that stuck state used to
override the active highlight and flicker while scrolling. */
@media (hover: hover) {
.tab-button:hover {
background: #2b2b2b;
}
}
/* The active tab keeps its highlight even if a hover state lingers
(e.g. a hybrid touch + trackpad device that reports hover: hover). */
.tab-button.active:hover {
background: #39a8ff;
color: #fff;
border-color: #39a8ff;
box-shadow: 0 2px 8px rgba(57,168,255,0.35);
}
/* App logo — a sleek gradient checkmark, used in place of the old circle + title. */
.app-logo {
width: 38px;
height: 38px;
display: block;
flex-shrink: 0;
filter: drop-shadow(0 2px 6px rgba(0, 0, 0, 0.4));
transition: transform 0.18s cubic-bezier(0.22, 1, 0.36, 1);
}
@media (hover: hover) {
.app-logo:hover { transform: scale(1.06) rotate(-3deg); }
}
.main-content {
flex: 1;
display: flex;
flex-direction: column;
padding: 20px;
gap: 20px;
}
.tab-view {
display: none;
}
.tab-view.active {
display: flex;
flex-direction: column;
flex: 1;
gap: 16px; /* even breathing room between the tab strip, the menu, and the To Do/Done board */
}
.add-item-section {
background: #1d2026;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
border: 1px solid #2e333d;
}
.add-header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 10px;
}
.add-list-target {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: #b0b0b0;
white-space: nowrap;
}
.add-item-section h2 {
margin-bottom: 15px;
font-size: 16px;
color: #eceef2;
font-weight: 600;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: 500;
color: #b0b0b0;
font-size: 13px;
}
input[type="text"],
textarea,
select,
input[type="number"] {
width: 100%;
padding: 10px;
border: 1px solid #2e333d;
border-radius: 4px;
font-size: 14px;
font-family: inherit;
background: #14161b;
color: #eceef2;
touch-action: manipulation;
}
input[type="text"]:focus,
textarea:focus,
select:focus,
input[type="number"]:focus {
outline: none;
border-color: #39a8ff;
}
input[type="text"]::placeholder,
textarea::placeholder {
color: #666;
}
textarea {
resize: vertical;
min-height: 80px;
line-height: 1.5;
}
input[type="checkbox"] {
margin-right: 8px;
width: 18px;
height: 18px;
cursor: pointer;
vertical-align: middle;
}
/* The `.form-group label{display:block}` rule above is more specific than a bare
`.checkbox-label`, so qualify this one to win and lay the box beside the text. */
.checkbox-label,
.form-group label.checkbox-label {
display: flex;
align-items: flex-start; /* box sits beside the FIRST line, even when text wraps */
gap: 8px;
line-height: 1.35;
margin-bottom: 0;
cursor: pointer;
}
.checkbox-label input[type="checkbox"] {
position: static; /* drop the old top:18px hack that mis-placed the box */
top: auto;
flex: 0 0 auto; /* fixed size; never shrink or get pushed by the text */
width: 18px; /* beat `.modal input{width:100%}` which else stretches the box full-width */
height: 18px;
margin: 1px 0 0; /* nudge to line up with the first text line */
}
#isRecurring {
position: static;
top: auto;
transform: translateY(-2px);
}
#recurrenceOptions,
#editRecurrenceOptions {
background: #181b20;
padding: 15px;
border-radius: 4px;
margin-top: 10px;
border: 1px solid #2e333d;
}
#recurrenceOptions > div,
#editRecurrenceOptions > div {
margin-top: 10px;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
font-weight: 500;
transition: all 0.2s;
touch-action: manipulation;
}
.btn-primary {
background: #39a8ff;
color: white;
}
.btn-primary:hover {
background: #2980b9;
}
.btn-secondary {
background: #3a3a3a;
color: #eceef2;
}
.btn-secondary:hover {
background: #4a4a4a;
}
.btn-danger {
background: #ef4444;
color: white;
padding: 6px 12px;
font-size: 12px;
}
.btn-danger:hover {
background: #c0392b;
}
.checklists-section {
display: flex;
gap: 20px;
align-items: flex-start; /* To Do / Done size to their own content; the whole page scrolls */
/* Kanban rule: columns get a width FLOOR (below) and the board scrolls
sideways instead — many task sections (Monday…Friday) must never crush
the cards until buttons clip and titles wrap one word per line. */
overflow-x: auto;
-webkit-overflow-scrolling: touch;
padding-bottom: 6px; /* keep the scrollbar off the last row of cards */
}
.list-tabs-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.list-tabs {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
/* List tools live behind a single right-aligned "☰ Menu" dropdown so the top
of the board stays clean. */
.list-menu {
position: relative;
display: flex;
justify-content: flex-end;
/* spacing handled by .tab-view.active gap so it's even with the rest */
}
.list-menu-btn {
display: inline-flex;
align-items: center;
gap: 7px;
cursor: pointer;
padding: 7px 14px;
background: #1d2026;
border: 1px solid #4a4a4a;
border-radius: 6px;
color: #cfd5e3;
font-size: 13px;
font-weight: 600;
transition: background 0.15s ease, border-color 0.15s ease;
}
.list-menu-btn::after {
content: '▾';
font-size: 10px;
color: #39a8ff;
transition: transform 0.2s ease;
}
.list-menu.open .list-menu-btn::after { transform: rotate(180deg); }
.list-menu-btn:hover { background: #252932; border-color: #39a8ff; }
.list-menu.open .list-menu-btn { background: #1f3a52; border-color: #39a8ff; }
.list-menu-btn:focus-visible { outline: 2px solid #39a8ff; outline-offset: 2px; }
/* Dim backdrop so the lists don't show through the open menu (and a tap-to-close
target). Fixed → not clipped by .main-content's overflow:hidden. */
.list-menu-backdrop {
display: none;
position: fixed;
inset: 0;
z-index: 9998;
background: rgba(0, 0, 0, 0.45);
}
.list-menu.open .list-menu-backdrop { display: block; animation: menuBackdropIn 0.16s ease both; }
/* Fixed (not absolute) so it escapes overflow clipping; JS positions it each
open so it's always fully on-screen (flips above the button when needed). */
.list-menu-panel {
display: none;
position: fixed;
z-index: 9999;
flex-direction: column;
gap: 4px;
min-width: 230px;
max-width: min(300px, calc(100vw - 24px));
max-height: 80vh;
overflow-y: auto;
padding: 8px;
background: #191c22;
border: 1px solid #4a4a4a;
border-radius: 10px;
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.6);
}
.list-menu.open .list-menu-panel { display: flex; }
.list-menu-panel .btn { width: 100%; justify-content: flex-start; text-align: left; }
@media (prefers-reduced-motion: no-preference) {
@keyframes menuBackdropIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes menuPopIn { from { opacity: 0; transform: translateY(-8px) scale(0.97); } to { opacity: 1; transform: none; } }
@keyframes menuSheetUp { from { opacity: 0; transform: translateY(24px); } to { opacity: 1; transform: none; } }
/* No fill-mode: once the open animation ends it must stop overriding inline
transform/opacity, so the JS drag + close-out transition can take over. */
.list-menu.open .list-menu-panel { animation: menuPopIn 0.22s cubic-bezier(0.34, 1.56, 0.64, 1); }
.move-menu.open { animation: menuPopIn 0.18s cubic-bezier(0.22, 1, 0.36, 1); }
}
.list-menu-btn:active { transform: scale(0.96); }
/* Inline "Move to…" dropdown anchored to a task's Move button. */
.move-menu-backdrop { position: fixed; inset: 0; z-index: 1190; background: transparent; }
.move-menu {
position: fixed; z-index: 1200; flex-direction: column;
min-width: 220px; max-width: min(320px, calc(100vw - 16px));
padding: 6px; overflow-y: auto;
background: #20242e; border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 12px;
box-shadow: 0 18px 48px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(0, 0, 0, 0.25);
}
.move-menu-head {
padding: 6px 10px 8px; font-size: 0.72rem; font-weight: 600; letter-spacing: 0.04em;
text-transform: uppercase; color: #8a93a6;
}
.move-opt {
display: flex; align-items: center; gap: 10px; width: 100%;
padding: 9px 10px; border: none; border-radius: 8px; cursor: pointer; text-align: left;
background: transparent; color: #e7eaf0; font-size: 0.92rem;
transition: background 0.12s ease;
}
.move-opt:hover, .move-opt:focus-visible { background: rgba(99, 114, 145, 0.22); outline: none; }
.move-opt:active { background: rgba(99, 114, 145, 0.34); }
.move-dot { flex: 0 0 auto; width: 10px; height: 10px; border-radius: 50%; box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.2) inset; }
/* Section targets (Monday/Tuesday…) use a square outline so they read as columns, not lists. */
.move-dot-section { border-radius: 3px; background: transparent; border: 2px solid #8a93a6; box-shadow: none; box-sizing: border-box; }
.move-opt-name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.move-scope {
flex: 0 0 auto; padding: 2px 7px; border-radius: 999px;
background: rgba(255, 255, 255, 0.07); color: #9aa3b5;
font-size: 0.68rem; font-weight: 600; letter-spacing: 0.02em;
}
.menu-group-label {
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.6px;
color: #7c828c;
padding: 8px 4px 2px;
}
.menu-group-label:first-child { padding-top: 2px; }
.list-tab {
padding: 10px 14px;
border-radius: 6px;
border: 1px solid #2e333d;
background: #181b20;
color: #cfd5e3;
cursor: grab;
transition: all 0.2s;
font-weight: 600;
user-select: none;
}
.list-tab.active {
background: #39a8ff;
border-color: #39a8ff;
color: #fff;
box-shadow: 0 2px 8px rgba(57,168,255,0.35);
}
/* See .tab-button:hover above. This is the bug the user hit: on a tablet the
stuck hover wiped the active tab's blue fill but left the blue border, and
flickered on scroll / list switches. Gate hover to real pointers and keep
the active tab blue regardless. */
@media (hover: hover) {
.list-tab:hover {
background: #1d2026;
}
}
.list-tab.active:hover {
background: #39a8ff;
border-color: #39a8ff;
color: #fff;
box-shadow: 0 2px 8px rgba(57,168,255,0.35);
}
/* The tab being dragged is dimmed but stays in place as an anchor. */
.list-tab.dragging { opacity: 0.4; }
/* Vertical drop-line shown between tabs (and at the end of the strip) marking
exactly where the dragged list will land. A flex child in #listTabs, so it
sits in the gap; pointer-events:none so it never steals dragover events. */
.tab-drop-indicator {
flex: 0 0 auto;
align-self: stretch;
width: 3px;
min-height: 24px;
background: #39a8ff;
border-radius: 3px;
box-shadow: 0 0 8px rgba(57, 168, 255, 0.7);
pointer-events: none;
}
.list-actions .btn-small {
padding: 8px 12px;
font-size: 13px;
}
.checklist-column {
flex: 1;
min-width: 300px; /* the width floor — stacked layouts reset this to 0 */
background: #1d2026;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
display: flex;
flex-direction: column;
overflow: hidden; /* keep rounded corners; height now grows with content (no inner scroll) */
border: 1px solid #2e333d;
}
.checklist-column h2 {
padding: 15px 20px;
background: #181b20;
border-bottom: 1px solid #2e333d;
font-size: 16px;
color: #eceef2;
font-weight: 600;
}
.checklist-list {
padding: 15px;
}
.checklist-item {
background: #252932;
border: 1px solid #2e333d;
border-radius: 8px;
padding: 0; /* header strip + padded body provide spacing */
overflow: hidden; /* let the header strip respect the rounded top corners */
margin-bottom: 12px;
transition: all 0.2s;
}
/* Gate on real pointers too — otherwise task cards "stick" highlighted and
flicker as a finger drags over them while scrolling on a tablet. */
@media (hover: hover) {
.checklist-item:hover:not(.dragging) {
box-shadow: 0 4px 12px rgba(0,0,0,0.45);
border-color: #505050;
transform: translateY(-1px); /* gentle lift; animates via the card's transition:all 0.2s */
}
}
.checklist-item.done {
opacity: 0.6;
background: #2a3a2a;
border-color: #3a4a3a;
}
.checklist-item.done .checklist-item-header {
background: #233123; /* green-tinted header when done */
border-bottom-color: #314431;
}
/* The card being dragged is dimmed but stays in place as an anchor. */
.checklist-item.dragging {
opacity: 0.4;
}
/* Drop-line: a blue bar shown in the gap exactly where the task will land
(including below the last card, for end-of-list drops). pointer-events:none
so it never steals dragover events from the column → no flicker. Lives in
the column (not inside a card) so the card's overflow:hidden can't clip it. */
.drop-indicator {
height: 3px;
margin: 4px 2px 5px;
background: #39a8ff;
border-radius: 3px;
box-shadow: 0 0 8px rgba(57, 168, 255, 0.7);
pointer-events: none;
}
.checklist-item-header {
display: flex;
align-items: center;
flex-wrap: wrap; /* narrow card → the recurrence tag drops to its own line instead of crushing the title */
gap: 12px;
margin: 0;
padding: 11px 14px;
background: #242b36; /* slate header strip — the "top" accent, like the To Do/Done headers */
border-bottom: 1px solid #38414f;
}
.checklist-item-body {
padding: 12px 14px;
}
/* ── Collapsible task cards (per-user) ───────────────────────────────
Resting collapsed = the body is display:none. This is STATE (must work for
reduced-motion users too), so it lives OUTSIDE any prefers-reduced-motion block;
display:none also drops the hidden sub-step checkboxes out of the tab order for free. */
.checklist-item.collapsed .checklist-item-body { display: none; }
/* Reserve room on the right + absolutely-position the chevron so it always pins to the
header's top-right and never wraps below on mobile (where the header flex-wraps and the
recurrence tag drops to a 2nd line). */
.checklist-item-header.is-collapsible {
position: relative;
padding-right: 44px;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
touch-action: manipulation;
}
.collapse-toggle {
position: absolute;
top: 50%; right: 7px;
transform: translateY(-50%);
width: 30px; height: 30px;
display: inline-flex; align-items: center; justify-content: center;
padding: 0; border: 0; background: transparent; color: #8b95a7;
border-radius: 8px; cursor: pointer;
-webkit-tap-highlight-color: transparent; touch-action: manipulation;
}
.collapse-toggle::before { content: ''; position: absolute; inset: -8px; } /* ~46px hit target; header stays compact */
.collapse-toggle svg, .collapse-toggle .chevron path { pointer-events: none; }
@media (hover: hover) { .collapse-toggle:hover { color: #eceef2; background: rgba(255,255,255,0.06); } }
.collapse-toggle:focus-visible { outline: 2px solid #39a8ff; outline-offset: -2px; }
.collapse-toggle .chevron { display: block; transition: transform 0.2s cubic-bezier(0.22,1,0.36,1); }
/* Rotate off aria-expanded (flips synchronously on toggle), NOT the .collapsed class
(which is only added at collapse-finish), so the glyph tracks the body in both directions. */
.collapse-toggle[aria-expanded="false"] .chevron { transform: rotate(-90deg); }
.checklist-item input[type="checkbox"] {
margin-top: 2px;
width: 18px;
height: 18px;
cursor: pointer;
flex-shrink: 0;
accent-color: #2ecc71; /* green tick — matches the "done" flash */
}
.checklist-item-title {
flex: 1;
font-weight: 600;
font-size: 15px;
color: #eceef2;
word-wrap: break-word;
line-height: 1.4;
}
.checklist-item.done .checklist-item-title {
text-decoration: line-through;
color: #888;
}
.checklist-item-description {
margin: 0;
color: #b0b0b0;
font-size: 13px;
line-height: 1.6;
word-wrap: break-word;
white-space: normal; /* markdown structure handles layout now */
}
/* Markdown elements inside a description */
.checklist-item-description p { margin: 0 0 6px; }
.checklist-item-description p:last-child { margin-bottom: 0; }
.checklist-item-description ul,
.checklist-item-description ol { margin: 4px 0 6px; padding-left: 20px; }
.checklist-item-description li { margin: 2px 0; }
.checklist-item-description h4,
.checklist-item-description h5,
.checklist-item-description h6 { margin: 8px 0 4px; color: #cfd5e3; font-size: 13px; font-weight: 700; }
.checklist-item-description strong { color: #eceef2; }
/* ── Inline checkbox rows (md.js .md-task) — authored anywhere in a description ──
Shared shape for the LIVE list and the editor's formatted preview; interactive
affordances (cursor/hover/tap target) are scoped to .checklist-item-description so the
editor copy stays inert. The box is CSS-drawn (no unicode/emoji glyph → iPad-safe). */
.md-task { list-style: none; margin-left: -18px; } /* pull back the <ul> bullet indent (no :has — iPad-safe) */
.checklist-item-description .md-task,
.md-rich .md-task {
display: flex;
align-items: flex-start;
gap: 10px;
margin: 1px 0;
padding: 5px 6px 5px 2px;
border-radius: 8px;
line-height: 1.45;
}
.checklist-item-description .md-task {
min-height: 44px; /* comfortable row spacing */
cursor: default; /* the ROW is not the toggle target — only the box is */
-webkit-tap-highlight-color: transparent;
}
.checklist-item-description .md-task:focus-visible { outline: 2px solid #39a8ff; outline-offset: 1px; }
/* Only the box toggles — tapping the label (incl. an equipment word) never checks it. */
.checklist-item-description .md-task-box { cursor: pointer; touch-action: manipulation; }
.checklist-item-description .md-task-box::before {
content: ''; position: absolute; inset: -9px; /* enlarge the tap zone to ~38px (still box-only, doesn't reach the label) */
}
@media (hover: hover) { .checklist-item-description .md-task-box:hover { border-color: #6c93b8; } }
.md-task-box {
flex-shrink: 0; box-sizing: border-box;
width: 20px; height: 20px; margin-top: 1px;
border: 2px solid #55606f; border-radius: 5px; position: relative;
transition: background-color .12s ease, border-color .12s ease;
}
.md-task.is-done .md-task-box { background: #2ecc71; border-color: #2ecc71; }
.md-task.is-done .md-task-box::after { /* CSS-drawn check mark */
content: ''; position: absolute; left: 6px; top: 2px;
width: 4px; height: 9px; border: solid #12331f; border-width: 0 2px 2px 0; transform: rotate(45deg);
}
.md-task-label { flex: 1; padding-top: 1px; color: #d3d7df; word-break: break-word; }
.md-task.is-done .md-task-label { text-decoration: line-through; color: #7c828c; }
/* Display-only parent indicator when a task has checkboxes: a progress pill (n/m) that
turns into a green ✓ when every box is checked (you complete the task via the boxes). */
.subtask-parent {
flex-shrink: 0;
min-width: 34px;
height: 22px;
padding: 0 7px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 999px;
background: #2a3140;
border: 1px solid #3a4453;
color: #9fb2c8;
font-size: 12px;
font-weight: 700;
font-variant-numeric: tabular-nums;
line-height: 1;
cursor: default;
user-select: none;
}
.subtask-parent.complete { background: #1f3b2a; border-color: #2ecc71; color: #2ecc71; font-size: 14px; }
.form-hint { font-weight: 400; color: #7c828c; font-size: 12px; }
/* iOS Safari "data detectors" auto-linkify emails / phone-ish numbers / addresses
inside text (task descriptions here are full of IPs, ports, credentials, and
distro emails). Safari tags each generated link with x-apple-data-detectors and
gives it a stray underline + hijacks the tap into a Mail / call / share sheet.
Neutralize them everywhere so those reference values read as plain, inert text. */
a[x-apple-data-detectors],
a[x-apple-data-detectors] * {
color: inherit !important;
text-decoration: none !important;
-webkit-text-decoration: none !important;
pointer-events: none !important; /* the tap can't reach the OS link → no Mail/share sheet */
cursor: text !important;
-webkit-touch-callout: none !important;
}
.checklist-item-description a { color: #6db3f2; }
/* Tappable equipment terms (no URL ever shown — just a clean, tappable word).
No underline, no icon; a soft accent + medium weight signals it's interactive,
and a gentle rounded highlight appears on hover/focus. The box-shadow doubles
as padding so the highlight has breathing room without shifting the text. */
.equip-term {
cursor: pointer;
color: #8aa0c0;
font-weight: 600;
text-decoration: none;
border-radius: 4px;
white-space: nowrap;
transition: color 0.12s ease, background-color 0.12s ease, box-shadow 0.12s ease;
}
.equip-term:hover,
.equip-term:focus-visible {
color: #b3c5e0;
background-color: rgba(138, 160, 192, 0.18);
box-shadow: 0 0 0 3px rgba(138, 160, 192, 0.18);
outline: none;
}
/* Terms whose library entry has no photo: still tappable (shows name/note),
but rendered in a quieter slate so the photo ones stand out. */
.equip-term-noimg { color: #9aa4b2; font-weight: 500; }
.equip-term-noimg:hover,
.equip-term-noimg:focus-visible {
color: #c4cad4;
background-color: rgba(154, 164, 178, 0.16);
box-shadow: 0 0 0 3px rgba(154, 164, 178, 0.16);
}
/* ── Equipment photo lightbox (pan/zoom) ───────────────────────────── */
.equip-lightbox .equip-modal {
width: min(920px, 96vw);
max-width: 96vw;
max-height: 94vh; /* stay within the viewport when caption + thumbs + a long note stack up */
display: flex;
flex-direction: column;
padding: 0;
overflow: hidden;
gap: 0;
}
/* When the modal is height-capped, let the stage shrink so the header/caption/thumbs/note stay visible. */
.equip-lightbox .equip-stage { min-height: 0; }
.equip-lb-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 14px 16px 10px;
}
.equip-lb-head h3 { margin: 0; font-size: 17px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.equip-lb-close {
flex: 0 0 auto;
width: 34px; height: 34px;
display: inline-flex; align-items: center; justify-content: center;
border: 1px solid #3a3f4a; border-radius: 8px;
background: #1d2026; color: #cfd5e3; font-size: 15px; cursor: pointer;
}
.equip-lb-close:hover { background: #2a2f38; color: #fff; }
/* The zoom viewport. touch-action:none so pinch/drag drive OUR handler, not the
page — pinching the photo never zooms the whole webpage. */
.equip-stage {
position: relative;
flex: 1 1 auto;
min-height: 40vh;
height: min(64vh, 620px);
background: #0b0d10;
overflow: hidden;
touch-action: none;
cursor: zoom-in;
user-select: none;
-webkit-user-select: none;
}
.equip-stage.zoomed { cursor: grab; }
.equip-stage.zoomed.grabbing { cursor: grabbing; }
.equip-stage.loading { cursor: default; }
.equip-img {
position: absolute;
top: 0; left: 0;
transform-origin: 0 0;
-webkit-user-drag: none;
user-select: none;
max-width: none; /* JS sizes/transforms it — no CSS clamping */
max-height: none;
background: transparent;
display: block;
}
/* Promote to its own compositor layer ONLY while zoomed/panning. Leaving will-change on
permanently would hold a full-resolution GPU backing store (huge at 4K → can force a
downgraded/blurry layer or crash iPad Safari); scoping it keeps idle viewing cheap. */
.equip-stage.zoomed .equip-img { will-change: transform; }
/* Floating zoom controls (discoverable on every device; gestures work too). */
.equip-zoom-controls {
position: absolute;
right: 12px; bottom: 12px;
display: flex; gap: 6px;
padding: 5px;
background: rgba(18, 21, 26, 0.72);
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 12px;
z-index: 3;
}
.equip-zoom-btn {
width: 40px; height: 40px;
display: inline-flex; align-items: center; justify-content: center;
border: none; border-radius: 8px;
background: rgba(255,255,255,0.06); color: #e7ebf3;
font-size: 20px; line-height: 1; cursor: pointer;
transition: background 0.12s ease, transform 0.08s ease;
}
.equip-zoom-btn:hover { background: rgba(255,255,255,0.14); }
.equip-zoom-btn:active { transform: scale(0.92); }
.equip-zoom-btn:disabled { opacity: 0.35; cursor: default; }
/* First-run gesture hint; fades away after a moment or on first interaction. */
.equip-zoom-hint {
position: absolute;
left: 50%; bottom: 14px;
transform: translateX(-50%);
padding: 7px 13px;
background: rgba(18, 21, 26, 0.78);
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 999px;
color: #cfd5e3; font-size: 12px; white-space: nowrap;
pointer-events: none;
opacity: 0;
transition: opacity 0.35s ease;
z-index: 2;
}
.equip-zoom-hint.show { opacity: 1; }
/* Note panel: scrolls on its own so long troubleshooting steps never push the
photo around. Renders Markdown. */
.equip-note {
font-size: 14px; color: #cfd5e3; line-height: 1.5;
padding: 12px 16px 16px;
max-height: 26vh; overflow-y: auto;
border-top: 1px solid #2a2f38;
-webkit-overflow-scrolling: touch;
}
.equip-note:empty { display: none; }
.equip-note > :first-child { margin-top: 0; }
.equip-note > :last-child { margin-bottom: 0; }
.equip-note p { margin: 0 0 8px; }
.equip-note ul, .equip-note ol { margin: 0 0 8px; padding-left: 22px; }
.equip-note li { margin: 2px 0; }
.equip-note h1, .equip-note h2, .equip-note h3, .equip-note h4 { margin: 12px 0 6px; font-size: 15px; color: #fff; }
.equip-note code { background: #14171c; border: 1px solid #2e333d; border-radius: 4px; padding: 1px 5px; font-size: 12px; }
.equip-note pre { background: #14171c; border: 1px solid #2e333d; border-radius: 6px; padding: 10px; overflow-x: auto; }
.equip-note a { color: #6bb6ff; }
.equip-note strong { color: #fff; }
/* ── Lightbox gallery: prev/next arrows, caption, thumbnail strip ── */
.equip-nav {
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 44px; height: 44px;
display: inline-flex; align-items: center; justify-content: center;
border: 1px solid rgba(255,255,255,0.08);
border-radius: 999px;
background: rgba(18, 21, 26, 0.72);
backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
color: #e7ebf3; font-size: 26px; line-height: 1; cursor: pointer;
z-index: 4;
transition: background 0.12s ease, opacity 0.12s ease;
}
.equip-nav:hover { background: rgba(40, 46, 56, 0.9); }
.equip-nav:disabled { opacity: 0.28; cursor: default; }
.equip-nav-prev { left: 12px; }
.equip-nav-next { right: 12px; }
.equip-caption {
padding: 9px 16px;
display: flex; align-items: center; justify-content: center; gap: 10px;
font-size: 13px; color: #cfd5e3; text-align: center;
border-top: 1px solid #2a2f38;
}
.equip-caption-text { font-weight: 600; color: #eceef2; }
.equip-caption-count { color: #8b93a3; font-variant-numeric: tabular-nums; }
.equip-thumbs {
display: flex; gap: 8px; overflow-x: auto;
padding: 10px 16px;
border-top: 1px solid #2a2f38;
-webkit-overflow-scrolling: touch;
}
.equip-thumb-btn {
flex: 0 0 auto;
width: 60px; height: 60px;
padding: 0; border: 2px solid transparent; border-radius: 8px;
background: #0b0d10; cursor: pointer; overflow: hidden;
opacity: 0.6; transition: opacity 0.12s ease, border-color 0.12s ease;
}
.equip-thumb-btn img { width: 100%; height: 100%; object-fit: cover; display: block; }
.equip-thumb-btn:hover { opacity: 0.9; }
.equip-thumb-btn.active { opacity: 1; border-color: #39a8ff; }
@media (max-width: 700px) {
.equip-lightbox .equip-modal { width: 100vw; max-width: 100vw; height: 100%; max-height: 100%; border-radius: 0; }
.equip-stage { height: auto; flex: 1 1 auto; min-height: 0; }
.equip-note { max-height: 34vh; }
.equip-zoom-btn { width: 44px; height: 44px; } /* bigger tap targets */
.equip-thumbs { padding: 8px 12px; }
.equip-thumb-btn { width: 52px; height: 52px; }
}
/* Equipment manager: photo upload + preview + scope */
.equip-photo-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin: 8px 0 4px; }
.equip-upload-label { cursor: pointer; margin: 0; }
.equip-upload-status { font-size: 12px; color: #9aa4b2; }
.equip-url-row { display: flex; gap: 8px; align-items: center; margin: 4px 0 8px; }
.equip-url-input { flex: 1 1 auto; min-width: 0; margin: 0; }
/* Manager multi-photo gallery: each photo = drag handle + thumbnail + caption + remove. */
.equip-photos { display: flex; flex-direction: column; gap: 8px; margin: 6px 0; }
.equip-photos-empty { font-size: 12px; color: #7c828c; padding: 8px 2px; }
.equip-photo {
display: flex; align-items: center; gap: 10px;
padding: 6px; border: 1px solid #2e333d; border-radius: 8px; background: #16191f;
}
.equip-photo-handle { flex: 0 0 auto; color: #7c828c; cursor: grab; font-size: 14px; line-height: 1; user-select: none; letter-spacing: -2px; }
.equip-photo.dragging { opacity: 0.5; }
.equip-photo-thumb { flex: 0 0 auto; width: 48px; height: 48px; object-fit: cover; border-radius: 6px; background: #000; }
.equip-photo-thumb.equip-photo-vid { display: grid; place-items: center; color: #cfd5e3; font-size: 16px; background: #0f1216; border: 1px solid #2e333d; }
.equip-photo-caption { flex: 1 1 auto; min-width: 0; margin: 0; }
.equip-photo-remove {
flex: 0 0 auto; width: 32px; height: 32px;
display: inline-flex; align-items: center; justify-content: center;
border: 1px solid #3a3f4a; border-radius: 6px; background: #1d2026;
color: #8b93a3; cursor: pointer; font-size: 13px;
}
.equip-photo-remove:hover { color: #ff6b6b; border-color: #ff6b6b; }
.equip-note-input { width: 100%; margin: 4px 0 0; resize: vertical; min-height: 40px; font-family: inherit; line-height: 1.4; }
/* Manager attached-documents editor: icon + name field + remove, photo-row styling. */
.equip-docs-editor { display: flex; flex-direction: column; gap: 8px; margin: 6px 0; }
.equip-doc {
display: flex; align-items: center; gap: 10px;
padding: 6px; border: 1px solid #2e333d; border-radius: 8px; background: #16191f;
}
.equip-doc-ico { flex: 0 0 auto; width: 32px; text-align: center; font-size: 16px; }
.equip-doc-name { flex: 1 1 auto; min-width: 0; margin: 0; }
/* Lightbox: attached documents as tappable chips under the note. */
.equip-docs { display: flex; flex-wrap: wrap; gap: 8px; padding: 10px 2px 2px; }
.equip-docs a {
display: inline-flex; align-items: center;
padding: 6px 12px; border: 1px solid #3a3f4a; border-radius: 999px;
background: #1d2026; color: #9ecbff; text-decoration: none; font-size: 13px;
}
.equip-docs a:hover { border-color: #39a8ff; color: #cfe7ff; }
.equip-scope { margin: 4px 0 10px; }
.equip-form-actions { display: flex; gap: 8px; }
.equip-row-main { display: flex; align-items: center; gap: 10px; }
.equip-thumb { width: 40px; height: 40px; object-fit: cover; border-radius: 6px; background: #000; flex: 0 0 auto; }
.equip-thumb-empty { display: inline-flex; align-items: center; justify-content: center; font-size: 10px; color: #7c828c; border: 1px dashed #4a4a4a; }
.checklist-item.done .checklist-item-description {
color: #666;
}
.checklist-item-recurrence {
margin-left: auto; /* pin to the right edge of the header */
flex-shrink: 0;
max-width: 100%; /* never wider than the card — ellipsize instead of clipping */
font-size: 11px;
color: #8a93a6;
font-style: italic;
display: flex;
align-items: center;
gap: 5px;
white-space: nowrap;
}
.recurrence-text { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.recurrence-icon {
width: 13px;
height: 13px;
display: inline-block;
}
.checklist-item-actions {
margin: 12px 0 0;
display: flex;
justify-content: flex-end; /* right-aligned action buttons */
flex-wrap: wrap; /* narrow card → buttons wrap to a 2nd row, never clip off the edge */
gap: 8px;
}
.empty-state {
text-align: center;
padding: 40px 20px;
color: #666;
font-style: italic;
font-size: 14px;
}
/* Scrollbar styling */
.checklist-list::-webkit-scrollbar {
width: 10px;
}
.checklist-list::-webkit-scrollbar-track {
background: #181b20;
border-radius: 4px;
}
.checklist-list::-webkit-scrollbar-thumb {
background: #505050;
border-radius: 4px;
}
.checklist-list::-webkit-scrollbar-thumb:hover {
background: #606060;
}
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.55);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
}
.modal {
background: #191c22;
border: 1px solid #2e333d;
border-radius: 8px;
padding: 18px;
/* Never wider than the screen on narrow phones (leaves a 12px gutter each side). */
width: min(320px, calc(100vw - 24px));
box-shadow: 0 6px 24px rgba(0,0,0,0.45);
display: flex;
flex-direction: column;
gap: 12px;
/* Never taller than the (dynamic) viewport — scroll inside instead of clipping
past the top/bottom of the screen. dvh tracks mobile browser chrome collapsing;
the vh line is a fallback for engines without dvh support. */
max-height: calc(100vh - 24px);
max-height: calc(100dvh - 24px);
overflow-y: auto;
overscroll-behavior: contain;
}
.modal.wide {
width: min(500px, calc(100vw - 24px));
}
/* Offline / syncing status pill (bottom-center) */
.offline-badge {
position: fixed;
left: 50%;
bottom: 16px;
transform: translateX(-50%);
z-index: 11000;
padding: 7px 14px;
border-radius: 999px;
font-size: 13px;
font-weight: 600;
color: #fff;
background: #2f7d3a; /* syncing = green-ish */
border: 1px solid rgba(255,255,255,0.15);
box-shadow: 0 6px 20px rgba(0,0,0,0.4);
bottom: calc(16px + env(safe-area-inset-bottom));
}
.offline-badge.is-offline { background: #b25a00; } /* offline = amber */
/* "Tap your name" — checker chip (shared team logins only) */
body:not(.checker-mode) .checker-only { display: none !important; }
.checker-chip {
display: inline-flex;
align-items: center;
gap: 6px;
margin-right: 8px;
padding: 6px 12px;
border-radius: 999px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
color: #cfd5e3;
background: #1d2026;
border: 1px dashed #5a6273; /* dashed = "set me" call-to-action when unset */
}
.checker-chip:hover { background: #252932; }
.checker-chip.is-set {
color: #fff;
background: #1f3a52;
border: 1px solid #39a8ff; /* solid + blue once a name is picked */
}
.checker-chip-icon { font-size: 14px; }
#checkerAddRow { display: flex; }
/* Team-name roster manager (master + execs) */
/* roster-body uses the .user-mgmt-list scroll container (same as Team logins) */
.roster-team { padding: 10px 10px; border-bottom: 1px solid #1d2026; }
.roster-team:last-child { border-bottom: none; }
.roster-team-head { font-weight: 700; color: #eceef2; font-size: 14px; margin-bottom: 8px; }
.roster-chips { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 8px; }
.roster-chip {
display: inline-flex; align-items: center; gap: 6px;
padding: 5px 6px 5px 12px; border-radius: 999px;
background: #1d2026; border: 1px solid #4a4a4a; color: #e7eaf0; font-size: 13px;
}
.roster-del {
border: none; background: #444; color: #fff; cursor: pointer;
width: 18px; height: 18px; border-radius: 50%; line-height: 1; font-size: 13px; padding: 0;
}
.roster-del:hover { background: #c0392b; }
.roster-add { display: flex; gap: 8px; }
.roster-add-input { flex: 1; }
/* In-app camera capture */
.modal.equip-cam { width: min(560px, 94vw); }
.equip-cam-stage {
width: 100%;
background: #000;
border-radius: 8px;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
.equip-cam-stage video {
width: 100%;
max-height: 60vh;
display: block;
object-fit: contain;
background: #000;
}
.equip-cam-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
}
.equip-cam-actions .btn-primary { flex: 1 1 auto; }
/* Equipment library grouped (tree) by location */
.equip-tree-head {
display: flex;
align-items: center;
gap: 8px;
padding: 7px 8px;
margin-top: 2px;
cursor: pointer;
border-radius: 6px;
user-select: none;
font-weight: 700;
color: #eceef2;
position: sticky;
top: 0;
background: #191c22;
z-index: 1;
}
.equip-tree-head:hover { background: #242424; }
.equip-tree-group {
margin: 2px 0 10px 10px;
padding-left: 8px;
border-left: 2px solid #1d2026;
}
/* Collapsed groups stay rendered (fast, no rebuild on toggle) — just hidden. */
.equip-tree-group.closed { display: none; }
/* Overview ("Today at a glance") */
.overview-body { max-height: 70vh; overflow-y: auto; }
.ov-loc { font-weight: 700; font-size: 15px; color: #eceef2; margin: 12px 0 2px; }
.ov-loc:first-child { margin-top: 2px; }
.ov-team { font-size: 12px; text-transform: uppercase; letter-spacing: 0.04em; color: #8a93a6; margin: 6px 0 2px 2px; }
.ov-row {
display: flex; align-items: center; gap: 10px;
padding: 7px 8px; margin-left: 6px; border-radius: 8px; cursor: pointer;
}
.ov-row:hover { background: #242424; }
.ov-dot { flex: 0 0 auto; width: 10px; height: 10px; border-radius: 50%; }
.ov-name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: #eceef2; font-size: 14px; }
.ov-bar { flex: 0 0 110px; height: 8px; border-radius: 999px; background: #1d2026; overflow: hidden; }
.ov-fill { display: block; height: 100%; border-radius: 999px; background: #5c6bc0; }
.ov-fill.done { background: #2ecc71; }
.ov-fill.partial { background: #e0a800; }
.ov-fill.none { background: #444; }
.ov-count { flex: 0 0 auto; min-width: 38px; text-align: right; font-size: 12px; color: #cfd5e3; font-variant-numeric: tabular-nums; }
.ov-row.done .ov-count { color: #2ecc71; }
.ov-activity-head {
margin: 16px 0 6px; padding-top: 12px; border-top: 1px solid #1d2026;
font-size: 12px; text-transform: uppercase; letter-spacing: 0.04em; color: #8a93a6;
}
.ov-act { display: flex; align-items: baseline; gap: 8px; padding: 5px 8px; font-size: 13px; }
.ov-act-check { color: #2ecc71; flex: 0 0 auto; }
.ov-act-text { flex: 1 1 auto; min-width: 0; color: #cfd5e3; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ov-act-text b { color: #eceef2; }
.ov-act-meta { flex: 0 0 auto; color: #8a93a6; font-size: 11px; white-space: nowrap; }
/* ─── Activity Log (master-only audit view) ─── */
.activity-search {
width: 100%; box-sizing: border-box; margin: 0 0 8px;
padding: 8px 10px; background: #14161b; border: 1px solid #2e333d;
border-radius: 6px; color: #eceef2; font-size: 13px;
}
.activity-body { max-height: 70vh; overflow-y: auto; }
.act-day {
position: sticky; top: 0; z-index: 1; background: #1d2026;
color: #9aa4b2; font-size: 11px; font-weight: 700;
text-transform: uppercase; letter-spacing: 0.04em; padding: 9px 2px 4px;
}
.act-row { display: flex; gap: 9px; align-items: flex-start; padding: 7px 4px; border-bottom: 1px solid #20242c; }
.act-icon { flex: 0 0 auto; width: 18px; text-align: center; font-size: 13px; line-height: 1.5; }
.act-main { flex: 1 1 auto; min-width: 0; }
.act-line { font-size: 13px; color: #d7dbe3; line-height: 1.45; }
.act-who { font-weight: 600; color: #eceef2; }
.act-msg { color: #c2c8d2; }
.act-time { font-size: 11px; color: #7c828c; margin-top: 1px; font-variant-numeric: tabular-nums; }
.act-role {
display: inline-block; font-size: 10px; font-weight: 700; padding: 1px 6px;
border-radius: 999px; margin-right: 6px; vertical-align: middle;
text-transform: uppercase; letter-spacing: 0.03em;
}
.act-role-master { background: rgba(239, 68, 68, 0.18); color: #f3a3a3; }
.act-role-executive { background: rgba(57, 168, 255, 0.18); color: #8cc7ff; }
.act-role-team-admin { background: rgba(224, 168, 0, 0.18); color: #e0c060; }
.act-role-checker { background: rgba(46, 204, 113, 0.16); color: #74d39a; }
/* ─── Polish animations ──────────────────────────────────────────────
Gated to no-preference for accessibility. Entrance effects are applied only
via explicit action classes (.just-moved / .strip-*) so the 6s background
poll re-render never replays them. */
@media (prefers-reduced-motion: no-preference) {
/* Modals fade + pop in on open. */
@keyframes overlayIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes modalIn { from { opacity: 0; transform: translateY(10px) scale(0.98); } to { opacity: 1; transform: none; } }
.modal-overlay { animation: overlayIn 0.16s ease both; }
/* Springy back-out easing → the panel pops in with a hair of overshoot, so popups feel alive. */
.modal-overlay .modal { animation: modalIn 0.26s cubic-bezier(0.34, 1.56, 0.64, 1) both; }
/* Check ↔ uncheck: the moved item slides into its new column with a brief
color flash (green when completed, cool-blue when sent back to To Do). */
@keyframes itemArrive {
0% { opacity: 0; transform: translateY(-7px) scale(0.97); }
60% { transform: translateY(0) scale(1.01); }
100% { opacity: 1; transform: none; }
}
@keyframes flashDone { from { box-shadow: inset 0 0 0 9999px rgba(46, 204, 113, 0.18); } to { box-shadow: inset 0 0 0 9999px rgba(46, 204, 113, 0); } }
@keyframes flashTodo { from { box-shadow: inset 0 0 0 9999px rgba(143, 178, 230, 0.16); } to { box-shadow: inset 0 0 0 9999px rgba(143, 178, 230, 0); } }
/* Fallback — Motion One didn't load: the original CSS slide-in + color flash. */
html:not(.motion-on) .checklist-item.just-moved { animation: itemArrive 0.4s cubic-bezier(0.22, 1, 0.36, 1) both; }
html:not(.motion-on) .checklist-item.done.just-moved { animation: itemArrive 0.4s cubic-bezier(0.22, 1, 0.36, 1) both, flashDone 0.9s ease both; }
html:not(.motion-on) .checklist-item.just-moved:not(.done) { animation: itemArrive 0.4s cubic-bezier(0.22, 1, 0.36, 1) both, flashTodo 0.9s ease both; }
/* Motion One loaded: JS (Flip) springs the item into its new position; CSS keeps
only the color flash so the two compose instead of fighting over transform. */
html.motion-on .checklist-item.done.just-moved { animation: flashDone 0.9s ease both; }
html.motion-on .checklist-item.just-moved:not(.done) { animation: flashTodo 0.9s ease both; }
/* Hide / show lists: slide the strip out / in instead of snapping. */
.list-tabs.strip-collapsing, .list-tree.strip-collapsing {
transition: opacity 0.18s ease, transform 0.18s ease;
opacity: 0;
transform: translateY(-8px);
pointer-events: none;
}
@keyframes stripIn { from { opacity: 0; transform: translateY(-8px); } to { opacity: 1; transform: none; } }
.list-tabs.strip-expanding, .list-tree.strip-expanding { animation: stripIn 0.3s cubic-bezier(0.22, 1, 0.36, 1) both; }
body.lists-hidden .active-list-name { animation: stripIn 0.3s ease both; }
/* Modals fade + pop back OUT on close (mirror of overlayIn/modalIn). closeOverlay()
adds .closing, lets this play, THEN flips display:none — so the exit is visible
and the scroll lock holds until the modal is truly gone. */
@keyframes overlayOut { from { opacity: 1; } to { opacity: 0; } }
@keyframes modalOut { from { opacity: 1; transform: none; } to { opacity: 0; transform: translateY(10px) scale(0.98); } }
.modal-overlay.closing { animation: overlayOut 0.16s ease both; }
.modal-overlay.closing .modal { animation: modalOut 0.17s ease both; }
/* Move popover fades back out (mirror of its menuPopIn entrance). */
@keyframes movePopOut { from { opacity: 1; transform: none; } to { opacity: 0; transform: translateY(-8px) scale(0.97); } }
.move-menu.closing { animation: movePopOut 0.15s ease both; }
/* Press feedback — a quick squeeze confirms every tap/click. Transform-only
(compositor-friendly); disabled under reduced motion since it's in this block. */
.btn:active:not(:disabled),
.lists-toggle:active,
.user-badge:active,
.checker-chip:active,
.list-tab:active { transform: scale(0.96); transition: transform 0.08s ease; }
.icon-btn:active:not(:disabled) { transform: scale(0.9); transition: transform 0.08s ease; }
.checklist-item input[type="checkbox"]:active { transform: scale(0.85); transition: transform 0.1s ease; }
/* Color swatches grow on hover / squeeze on press; the selected ring eases in. */
.swatch { transition: transform 0.12s ease, box-shadow 0.16s ease; }
.swatch:hover { transform: scale(1.12); }
.swatch:active { transform: scale(0.94); }
/* Checker chip: smooth grey→blue state change + a confirm pop when a name is set. */
.checker-chip { transition: background 0.18s ease, border-color 0.18s ease, color 0.18s ease; }
@keyframes chipPop { 0% { transform: scale(1); } 50% { transform: scale(1.06); } 100% { transform: scale(1); } }
.checker-chip.just-set { animation: chipPop 0.22s cubic-bezier(0.22, 1, 0.36, 1); }
/* Undo/redo: a tiny directional nudge when the action fires. */
@keyframes nudgeLeft { 50% { transform: translateX(-3px); } }
@keyframes nudgeRight { 50% { transform: translateX(3px); } }
#undoBtn.nudge { animation: nudgeLeft 0.18s ease; }
#redoBtn.nudge { animation: nudgeRight 0.18s ease; }
/* Equipment lightbox photo 'develops' in just after the modal lands. */
/* Fade the whole zoom stage in on open (opacity only — the image's transform is
owned by the pan/zoom controller, so we must not animate transform here). */
@keyframes equipStageFade { from { opacity: 0; } to { opacity: 1; } }
#equipModal .equip-stage { animation: equipStageFade 0.28s ease both; }
/* The logo checkmark draws itself in on load. (Reduced motion → it's just
shown fully, since this dash/animation only exists in this block.) */
.app-logo-check { stroke-dasharray: 30; stroke-dashoffset: 30; animation: logoDraw 0.55s cubic-bezier(0.22, 1, 0.36, 1) 0.15s forwards; }
@keyframes logoDraw { to { stroke-dashoffset: 0; } }
}
.modal .modal-body textarea {
min-height: 90px;
}
.modal h3 {
margin: 0;
color: #eceef2;
font-size: 16px;
}
.modal-message {
font-size: 13px;
color: #eceef2;
line-height: 1.5;
}
.modal-message.danger {
color: #ff8a80;
}
.modal input {
width: 100%;
padding: 10px;
border: 1px solid #2e333d;
border-radius: 4px;
background: #14161b;
color: #eceef2;
}
/* Password show/hide eye toggle */
.password-field { position: relative; }
.password-field input { padding-right: 42px; }
.password-toggle {
position: absolute;
right: 4px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
cursor: pointer;
font-size: 16px;
line-height: 1;
padding: 6px 8px;
opacity: 0.65;
}
.password-toggle:hover { opacity: 1; }
/* Markdown formatting toolbar above description fields */
.md-toolbar {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-bottom: 6px;
}
.md-btn {
cursor: pointer;
min-width: 30px;
padding: 5px 9px;
font-size: 13px;
line-height: 1;
background: #1d2026;
border: 1px solid #4a4a4a;
border-radius: 5px;
color: #cfd5e3;
}
.md-btn:hover { background: #252932; border-color: #5a5a5a; }
.md-btn:active { transform: scale(0.95); }
.md-preview-btn { margin-left: auto; }
/* Active = this formatting is on at the caret (bold/italic/heading/list/link), or — for the
"Markdown" button — you're viewing the raw source. Same accent blue as the rest of the app. */
.md-btn.active {
background: #39a8ff;
border-color: #39a8ff;
color: #fff;
font-weight: 600;
}
.md-btn.active:hover { background: #2f8bcc; border-color: #2f8bcc; }
/* Formatted (contenteditable) editor — looks like the textarea, renders markdown. */
.md-rich {
height: auto; /* grow with content so it's never a tiny box + rarely needs resizing */
min-height: 140px;
max-height: 50vh;
resize: vertical; /* still draggable on desktop (disabled on mobile below) */
overflow-y: auto;
padding: 10px;
background: #14161b;
border: 1px solid #2e333d;
border-radius: 4px;
color: #eceef2;
font-size: 14px;
line-height: 1.5;
cursor: text;
word-wrap: break-word;
white-space: normal;
}
.md-rich:focus { outline: none; border-color: #39a8ff; }
.md-rich.is-empty:before {
content: attr(data-placeholder);
color: #666;
pointer-events: none;
}
/* Markdown elements rendered inside the editor */
.md-rich p { margin: 0 0 8px; }
.md-rich p:last-child { margin-bottom: 0; }
.md-rich ul, .md-rich ol { margin: 4px 0 8px; padding-left: 22px; }
.md-rich li { margin: 2px 0; }
.md-rich h4, .md-rich h5, .md-rich h6 { margin: 8px 0 4px; color: #cfd5e3; font-weight: 700; }
.md-rich h4 { font-size: 16px; }
.md-rich h5 { font-size: 15px; }
.md-rich h6 { font-size: 14px; }
.md-rich strong { color: #fff; }
.md-rich a { color: #6db3f2; }
/* Source mode: min height matches the formatted editor; JS auto-grows it with content. */
#editDescription, #itemDescription { min-height: 140px; }
/* On phones, drop the tiny native resize grip (hard to grab, and it steals taps near the
bottom of the box) — the editors auto-grow with content instead. */
@media (max-width: 700px) {
.md-rich, #editDescription, #itemDescription, .equip-note-input { resize: none; }
}
.modal-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
}
.loading-overlay {
position: fixed;
inset: 0;
background: #14161b;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
gap: 12px;
z-index: 9999;
}
.loading-spinner {
width: 48px;
height: 48px;
border: 4px solid #2f3542;
border-top-color: #39a8ff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.loading-text {
color: #cfd5e3;
font-size: 14px;
letter-spacing: 0.2px;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* ─── Web-only additions ──────────────────────────────────────── */
/* Admin-only elements are hidden unless body.admin-mode is set */
body:not(.admin-mode) .admin-only {
display: none !important;
}
/* Master-admin-only elements (user management, digests, import/export) are
hidden for team admins, who only manage their own lists. */
body:not(.master-mode) .master-only {
display: none !important;
}
/* Items in non-admin mode lose their edit/move/delete buttons */
body:not(.admin-mode) .checklist-item-actions {
display: none;
}
/* "Who did this" stamp on checked items */
.checklist-item-meta {
margin: 8px 0 0;
font-size: 12px;
color: #7f8c8d;
font-style: italic;
}
/* Toast notifications */
.toast {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%) translateY(20px);
background: #1d2026;
color: #eceef2;
border: 1px solid #2e333d;
border-radius: 6px;
padding: 10px 16px;
font-size: 13px;
box-shadow: 0 4px 16px rgba(0,0,0,0.5);
opacity: 0;
pointer-events: none;
transition: opacity 0.2s ease, transform 0.2s ease;
z-index: 10001;
max-width: 90vw;
}
.toast.show {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
.toast.success {
border-color: #2ecc71;
}
.toast.error {
border-color: #ef4444;
}
.list-tab-loc {
margin-left: 8px;
padding: 2px 8px;
background: rgba(241, 196, 15, 0.18);
border: 1px solid rgba(241, 196, 15, 0.5);
border-radius: 999px;
font-size: 11px;
font-weight: 500;
color: #f1c40f;
vertical-align: middle;
}
/* Drag affordance — only in admin mode */
body.admin-mode .list-tab[draggable="true"],
body.admin-mode .checklist-item[draggable="true"] {
cursor: grab;
}
body.admin-mode .list-tab[draggable="true"]:active,
body.admin-mode .checklist-item[draggable="true"]:active {
cursor: grabbing;
}
body:not(.admin-mode) .list-tab {
cursor: pointer;
}
/* User management modal */
.user-mgmt-list {
max-height: 360px;
overflow-y: auto;
flex-shrink: 0; /* don't collapse inside the flex-column modal on short screens; the modal scrolls instead */
border: 1px solid #2e333d;
border-radius: 6px;
/* No top/side padding: sticky location headers stick flush to the top and span
full width; rows carry their own inset padding. So nothing peeks above/behind. */
padding: 0 0 8px;
background: #181818;
}
.user-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 10px;
padding: 8px 10px;
border-bottom: 1px solid #1d2026;
flex-wrap: wrap; /* let the action buttons drop below when space is tight */
}
.user-row:last-child { border-bottom: 0; }
.user-row-main {
flex: 1 1 150px;
min-width: 150px; /* keep the name/scope readable; forces actions to wrap instead of overlapping */
}
.user-row-name {
font-weight: 600;
color: #eceef2;
font-size: 13px;
}
.user-row-username {
color: #888;
font-weight: 400;
font-size: 12px;
margin-left: 4px;
}
.user-row-meta {
margin-top: 4px;
display: flex;
gap: 6px;
flex-wrap: wrap;
}
.user-row-tag {
font-size: 11px;
padding: 2px 8px;
background: rgba(241, 196, 15, 0.15);
border: 1px solid rgba(241, 196, 15, 0.4);
color: #f1c40f;
border-radius: 999px;
}
.user-row-tag.admin {
background: rgba(239, 68, 68, 0.18);
border-color: rgba(239, 68, 68, 0.5);
color: #ff8a80;
}
.user-row-tag.empty {
background: #2a2a2a;
border-color: #2e333d;
color: #888;
}
/* A scope grant naming a location/team that was deleted from the directory. */
.user-row-tag-stale {
background: #2a2a2a;
border-color: #2e333d;
color: #8b93a3;
text-decoration: line-through;
text-decoration-color: rgba(139, 147, 163, 0.6);
}
.exec-cb-stale { color: #8b93a3; font-style: italic; }
.user-row-actions {
display: flex;
gap: 6px;
flex-wrap: wrap;
justify-content: flex-end;
}
/* ─── Manage Access: search + grouped, compact lists ─────────────── */
.mgmt-search {
width: 100%;
box-sizing: border-box;
margin-bottom: 8px;
padding: 8px 10px;
font-size: 13px;
background: #191c22;
border: 1px solid #2e333d;
border-radius: 6px;
color: #eceef2;
}
.mgmt-search:focus { outline: none; border-color: #39a8ff; }
/* Sticky location header inside the scrolling Team-logins list. Fully opaque and
above the rows so nothing shows above or behind it while scrolling. */
.mgmt-group-header {
position: sticky;
top: 0;
background: #242424;
color: #cfd5e3;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.6px;
padding: 8px 8px 6px;
border-bottom: 1px solid #3a3a3a;
z-index: 3;
}
.mgmt-group-count {
display: inline-block;
min-width: 18px;
text-align: center;
margin-left: 4px;
padding: 0 5px;
font-size: 10px;
color: #cfd5e3;
background: #1d2026;
border-radius: 999px;
}
/* Tighter rows so more fit without scrolling. */
.mgmt-section .user-row { padding: 6px 8px; }
/* Email tag stays on one line; full list shows on hover (title). */
.user-row-tag.email {
max-width: 280px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
background: rgba(255,255,255,0.05);
border-color: #3a3a3a;
color: #b8c0cc;
}
.user-row-actions .btn {
padding: 4px 10px;
font-size: 11px;
}
.user-mgmt-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
align-items: center;
}
.user-mgmt-grid input,
.user-mgmt-grid select {
padding: 6px 8px;
font-size: 12px;
}
@media (max-width: 700px) {
.user-mgmt-grid {
grid-template-columns: 1fr 1fr;
}
}
.directory-editor {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.dir-col {
background: #181818;
border: 1px solid #2e333d;
border-radius: 6px;
padding: 8px;
}
.dir-head {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.5px;
color: #888;
margin-bottom: 6px;
}
.dir-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 3px 0;
}
.dir-name {
font-size: 12px;
color: #eceef2;
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.dir-abbrev {
width: 60px !important; /* beat `.modal input{width:100%}` / `.dir-add input{flex:1}` — else it stretches and pushes the × off-screen */
flex: 0 0 auto;
box-sizing: border-box;
padding: 4px 6px;
font-size: 12px;
text-align: center;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.dir-add {
display: flex;
gap: 6px;
margin-top: 8px;
padding-top: 8px;
border-top: 1px dashed #2e333d;
}
.dir-add input {
flex: 1;
min-width: 0;
padding: 4px 6px;
font-size: 12px;
}
.dir-del {
flex-shrink: 0;
width: 24px;
height: 24px;
line-height: 1;
border: 1px solid #2e333d;
background: #2a2a2a;
color: #ff8a80;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.dir-del:hover {
background: #3a2a2a;
border-color: #ef4444;
}
.dir-row-actions { display: flex; gap: 4px; flex-shrink: 0; }
.dir-edit {
flex-shrink: 0;
width: 24px;
height: 24px;
line-height: 1;
border: 1px solid #2e333d;
background: #2a2a2a;
color: #9ec5fe;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
}
.dir-edit:hover { background: #25313f; border-color: #39a8ff; }
.dir-rename-input { flex: 1; min-width: 0; padding: 4px 6px; font-size: 12px; }
/* Inline editor for a team login (location/team/emails) */
.user-edit {
background: #181818;
border: 1px solid #333;
border-radius: 6px;
padding: 10px 12px;
margin: -2px 0 10px;
}
.user-edit-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px 10px;
}
.user-edit-grid label {
display: flex;
flex-direction: column;
gap: 3px;
font-size: 11px;
color: #b0b0b0;
}
.user-edit-grid label:last-child { grid-column: 1 / -1; }
.user-edit-grid select,
.user-edit-grid input {
width: 100%;
padding: 6px 8px;
font-size: 13px;
}
.user-edit-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-top: 8px;
}
@media (max-width: 700px) {
.user-edit-grid { grid-template-columns: 1fr; }
}
.exec-access {
background: #181818;
border: 1px solid #2e333d;
border-radius: 6px;
padding: 10px;
margin: -4px 0 10px;
}
.exec-cb-row {
display: flex;
flex-wrap: wrap;
gap: 6px 14px;
margin-top: 4px;
}
.exec-cb {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 12px;
color: #eceef2;
white-space: nowrap;
}
.exec-cb input {
width: 14px;
height: 14px;
}
.exec-access-help {
font-size: 11px;
color: #9aa;
margin-bottom: 8px;
line-height: 1.5;
}
/* ─── Wide, card-based "Manage Access" modal ─────────────────────── */
/* All four management popups (Manage Users, Equipment, Overview, Team Names) share
ONE size — same width AND a fixed height — so switching between them is consistent
instead of the window resizing to each one's content. The window scrolls as a whole
when content is taller; shorter content just leaves space below. */
.modal.mgmt {
width: min(1080px, 95vw);
/* dvh (not vh) so it never runs under mobile browser chrome; vh lines are fallbacks. */
height: min(94vh, 1200px);
height: min(94dvh, 1200px);
max-height: 94vh;
max-height: 94dvh;
overflow-y: auto;
}
/* The main content region FILLS the tall window and scrolls inside it, so the box is
always full instead of a short list floating with empty space below. The head /
search / add-equipment row stay fixed; only this region scrolls. Overrides the
list's own 360px / 70vh caps. */
.modal.mgmt > .user-mgmt-list,
.modal.mgmt > .roster-body,
.modal.mgmt > .overview-body,
.modal.mgmt > .activity-body,
.modal.mgmt > .mgmt-grid {
flex: 1 1 auto;
min-height: 0;
max-height: none;
overflow-y: auto;
}
/* Manage Users' two-column grid scrolls as one — let its section lists flow instead
of each being its own little 360px scroller. */
.modal.mgmt .mgmt-section .user-mgmt-list { max-height: none; overflow: visible; }
.mgmt-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.mgmt-head h3 { margin: 0; }
.mgmt-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
align-items: start;
}
.mgmt-section {
background: #191c22;
border: 1px solid #383838;
border-radius: 8px;
padding: 14px;
min-width: 0;
}
.mgmt-section > h4 {
margin: 0 0 10px;
font-size: 14px;
color: #eceef2;
font-weight: 600;
}
.mgmt-wide { grid-column: 1 / -1; }
.mgmt-section .user-mgmt-list {
max-height: 300px;
}
.mgmt-add {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid #333;
}
.mgmt-add h5 {
margin: 0 0 8px;
font-size: 11px;
color: #b0b0b0;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* Collapsible "Add equipment" form (progressive disclosure) — the modal is the
searchable list first; the form opens on demand (or automatically when editing),
so the list gets the whole window instead of being starved by a tall fixed form. */
.mgmt-add-toggle {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
min-height: 46px;
padding: 11px 14px;
background: #1c2732;
border: 1px solid #2f5573;
border-radius: 8px;
color: #cfe4ff;
font-size: 14px;
font-weight: 600;
text-align: left;
cursor: pointer;
transition: background 0.15s ease, border-color 0.15s ease;
}
.mgmt-add-toggle:hover { background: #22303e; border-color: #39a8ff; }
.mgmt-add-toggle:focus-visible { outline: 2px solid #39a8ff; outline-offset: 2px; }
.mgmt-add-caret { font-size: 17px; line-height: 1; color: #39a8ff; }
@media (prefers-reduced-motion: no-preference) {
.mgmt-add-caret { transition: transform 0.18s ease; }
}
#equipAddSection.open .mgmt-add-caret { transform: rotate(45deg); } /* + pivots toward × when open */
.mgmt-add-body { display: none; padding-top: 12px; }
#equipAddSection.open .mgmt-add-body { display: block; }
.mgmt-tip {
font-size: 11px;
color: #888;
margin: 8px 0 0;
line-height: 1.5;
}
@media (max-width: 760px) {
.mgmt-grid { grid-template-columns: 1fr; }
/* Management modals become a clean full-screen sheet: no wasted margins, a fixed
header + search, one smooth scroll region (the list), and the add-form docked
at the bottom. Safe-area insets keep it clear of the notch and home indicator. */
.modal.mgmt {
width: 100vw;
max-width: 100vw;
height: 100vh; /* fallback for engines without dvh */
height: 100dvh;
max-height: 100vh;
max-height: 100dvh;
border-radius: 0;
border-width: 0;
padding: max(12px, env(safe-area-inset-top)) 14px max(12px, env(safe-area-inset-bottom));
gap: 10px;
}
.mgmt-tip { display: none; } /* declutter — the header already says what this is */
.mgmt-head h3 { font-size: 18px; }
.mgmt-search { font-size: 16px; padding: 12px; } /* 16px avoids iOS focus-zoom; ≥44px tall */
/* Roomier, easier-to-tap rows and action buttons (all ≥44px touch targets). */
.user-row { padding: 11px 12px; gap: 12px; }
.user-row-name { font-size: 14px; }
.user-row-actions { gap: 8px; }
.user-row-actions .btn { min-height: 44px; padding: 10px 14px; }
.equip-thumb, .equip-thumb-empty { width: 48px; height: 48px; }
/* The collapsed add-form toggle is a big, obvious tap target at the bottom. */
.mgmt-add-toggle { min-height: 50px; font-size: 15px; }
.equip-form-actions .btn { min-height: 44px; flex: 1 1 auto; }
/* In-form controls (upload / take photo / add URL) also clear 44px. */
.equip-photo-row .btn,
.equip-upload-label,
.equip-url-row .btn { min-height: 44px; }
}
/* A full-screen sheet should rise from the bottom (iOS/Material idiom), not pop-scale
like a small centered dialog — scaling a whole-screen panel reads as a glitch. */
@media (max-width: 760px) and (prefers-reduced-motion: no-preference) {
@keyframes sheetUp { from { transform: translateY(100%); } to { transform: none; } }
/* No fill mode (not `both`): the sheet's resting position is its natural on-screen
spot, so the off-screen offset exists ONLY during the 0.28s entrance. If the
animation is ever interrupted or never advances, the sheet stays visible rather
than stranded below the fold. */
.modal-overlay .modal.mgmt { animation: sheetUp 0.28s cubic-bezier(0.22, 1, 0.36, 1); }
}
/* ─── Phone layout ───────────────────────────────────────────────
On narrow screens the side-by-side To Do / Done columns become thin,
cramped strips. Stack them full-width, let the section scroll as one,
drop the indentation, and use bigger tap targets. */
/* ─── List colors (swatches in the modal + colored tabs) ─────────── */
.swatch[data-color="blue"], .list-tab[data-color="blue"] { --lc: #39a8ff; }
.swatch[data-color="green"], .list-tab[data-color="green"] { --lc: #2ecc71; }
.swatch[data-color="amber"], .list-tab[data-color="amber"] { --lc: #e0a800; }
.swatch[data-color="red"], .list-tab[data-color="red"] { --lc: #ef4444; }
.swatch[data-color="purple"], .list-tab[data-color="purple"] { --lc: #9b59b6; }
.swatch[data-color="teal"], .list-tab[data-color="teal"] { --lc: #1abc9c; }
.swatch[data-color="pink"], .list-tab[data-color="pink"] { --lc: #e84393; }
.swatch[data-color="slate"], .list-tab[data-color="slate"] { --lc: #7f8c8d; }
.swatch[data-color="orange"], .list-tab[data-color="orange"] { --lc: #e67e22; }
.swatch[data-color="yellow"], .list-tab[data-color="yellow"] { --lc: #f1c40f; }
.swatch[data-color="lime"], .list-tab[data-color="lime"] { --lc: #8bc34a; }
.swatch[data-color="cyan"], .list-tab[data-color="cyan"] { --lc: #00bcd4; }
.swatch[data-color="indigo"], .list-tab[data-color="indigo"] { --lc: #5c6bc0; }
.swatch[data-color="brown"], .list-tab[data-color="brown"] { --lc: #8d6e63; }
.color-swatches { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 4px; }
.swatch {
width: 28px; height: 28px; border-radius: 50%;
border: 2px solid transparent; cursor: pointer; padding: 0;
background: var(--lc, #2a2a2a);
}
.swatch.selected { box-shadow: 0 0 0 2px #191c22, 0 0 0 4px #fff; }
.swatch-none { background: #2a2a2a; border: 1px solid #666; position: relative; }
.swatch-none::after {
content: ''; position: absolute; left: 3px; right: 3px; top: 50%;
height: 2px; background: #ef4444; transform: rotate(-45deg);
}
/* Custom color: rainbow ring until a color is picked, then fills solid (--lc). */
.swatch-custom {
position: relative;
overflow: hidden;
padding: 0;
background: var(--lc, conic-gradient(from 0deg, #ef4444, #e67e22, #f1c40f, #2ecc71, #1abc9c, #39a8ff, #9b59b6, #e84393, #ef4444));
}
.swatch-custom input[type="color"] {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
opacity: 0;
border: none;
padding: 0;
margin: 0;
cursor: pointer;
}
/* Colored tabs: left accent when inactive, full fill when active. */
.list-tab[data-color] { border-left: 5px solid var(--lc); }
.list-tab.active[data-color] { background: var(--lc); border-color: var(--lc); color: #fff; }
/* ─── Compact owner Location/Team filter ───────── */
/* Filter sits on its own row above the tabs, so the colored tabs use the full
width and pack several per row instead of being squeezed into a narrow column. */
.list-tabs-row { flex-direction: column; align-items: stretch; gap: 10px; }
.list-filter { display: flex; gap: 8px; flex-shrink: 0; flex-wrap: wrap; justify-content: flex-end; margin-left: auto; }
.list-filter select {
width: auto;
min-width: 128px;
max-width: 190px;
padding: 7px 10px;
font-size: 13px;
}
/* Shrink the owner scope badge so colored tabs stay compact. */
.list-tab-loc { margin-left: 6px; padding: 1px 6px; font-size: 10px; }
/* List repeat schedule: labeled inline rows so it reads like a sentence
("Every 2 weeks / on Wednesday", "Every 2 months / on day 21 of the month"). */
.reset-row {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
margin-top: 8px;
font-size: 13px;
color: #cfd5e3;
}
.reset-row input[type="number"] { width: 64px !important; text-align: center; flex: 0 0 auto; }
.reset-row select { width: auto !important; flex: 0 0 auto; min-width: 130px; }
.reset-hint { color: #7c828c; }
/* Intraday "repeat multiple times a day" editor. */
.reset-times-row { align-items: center; }
.reset-times-list { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.reset-time-entry { display: inline-flex; align-items: center; gap: 4px; }
.reset-time-entry input[type="time"] {
width: auto !important;
flex: 0 0 auto;
padding: 5px 8px;
background: #14171c;
color: #e7ebf3;
border: 1px solid #2e333d;
border-radius: 6px;
font-size: 13px;
}
.reset-time-remove {
border: none;
background: transparent;
color: #8b93a3;
font-size: 16px;
line-height: 1;
padding: 2px 4px;
cursor: pointer;
border-radius: 4px;
}
.reset-time-remove:hover { color: #ff6b6b; background: rgba(255,107,107,0.12); }
.reset-time-add { flex: 0 0 auto; }
.reset-times-hint { flex: 1 1 100%; }
/* Reusable task recurrence editor (add/edit task modals) — mirrors the list repeat editor. */
.rec-editor .rec-lbl { display: block; font-size: 12px; color: #b0b0b0; margin-bottom: 4px; }
.rec-editor .rec-type { width: 100%; }
/* Mobile: the tight inline "time × time × +Add" row is cramped and the × is a
poor tap target. Stack each time on its own full-width row with a big input and
a proper 40px remove button; make "+ Add time" full-width. Desktop is untouched. */
@media (max-width: 700px) {
.reset-times-row { flex-direction: column; align-items: stretch; gap: 8px; }
.reset-times-row > span:first-child { color: #b0b0b0; }
.reset-times-list { flex-direction: column; align-items: stretch; gap: 8px; width: 100%; }
.reset-time-entry { display: flex; width: 100%; align-items: center; gap: 8px; }
.reset-time-entry input[type="time"] { flex: 1 1 auto; padding: 11px 12px; font-size: 16px; }
.reset-time-remove {
flex: 0 0 auto; width: 42px; height: 42px;
display: inline-flex; align-items: center; justify-content: center;
font-size: 20px; border: 1px solid #3a3f4a; border-radius: 8px;
}
.reset-time-add { width: 100%; padding: 12px; }
}
/* Repeat indicator on a list tab / tree leaf (the list auto-resets on a schedule). */
.list-tab-repeat { margin-left: 6px; font-size: 11px; opacity: 0.85; }
.tree-repeat { margin-left: 6px; font-size: 11px; flex-shrink: 0; }
/* Hidden lists (visible only to editors): dimmed + a clear badge. */
.list-tab.list-hidden { opacity: 0.5; }
.list-tab.list-hidden.active { opacity: 1; }
.list-tab-hidden {
margin-left: 6px;
padding: 1px 6px;
font-size: 10px;
border-radius: 999px;
background: rgba(239, 68, 68, 0.18);
border: 1px solid rgba(239, 68, 68, 0.5);
color: #ff8a80;
vertical-align: middle;
}
/* ─── Family-tree navigation (master): Location → Team → Lists ────── */
.list-tree { display: none; }
body.admin-mode.tree-view .list-tabs { display: none; }
body.admin-mode.tree-view #listTree { display: block; padding: 4px 2px; }
body.admin-mode.lists-hidden #listTree { display: none; } /* "Hide lists" hides the tree too */
.tree-node { display: flex; align-items: center; gap: 8px; padding: 5px 8px; cursor: pointer; border-radius: 6px; user-select: none; }
.tree-node:hover { background: #242424; }
.tree-node-loc { font-weight: 700; color: #eceef2; font-size: 14px; margin-top: 2px; }
.tree-node-team { margin-left: 18px; color: #cfd5e3; font-size: 13px; }
.tree-caret { width: 12px; flex-shrink: 0; color: #8a93a6; font-size: 11px; }
.tree-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tree-count { margin-left: auto; font-size: 10px; color: #cfd5e3; background: #1d2026; border-radius: 999px; padding: 0 7px; }
.tree-leaf { display: flex; align-items: center; gap: 8px; margin-left: 40px; padding: 5px 8px; cursor: pointer; border-radius: 6px; color: #cfd5e3; font-size: 13px; }
.tree-leaf:hover { background: #242424; }
.tree-leaf.active { background: #39a8ff; color: #fff; }
.tree-leaf[draggable="true"] { cursor: grab; }
.tree-leaf.dragging { opacity: 0.4; }
/* Horizontal drop-line marking where a dragged leaf will land within its group. */
.tree-drop-indicator { height: 3px; margin: 1px 8px 1px 40px; background: #39a8ff; border-radius: 3px; box-shadow: 0 0 8px rgba(57, 168, 255, 0.7); pointer-events: none; }
.tree-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
.tree-leaf-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tree-hidden { font-size: 10px; color: #ff8a80; border: 1px solid rgba(239, 68, 68, 0.5); border-radius: 999px; padding: 0 6px; flex-shrink: 0; }
/* Cap the list strip so a big list count never eats the whole screen and pushes
the To Do / Done section off-view. The strip scrolls within instead. */
.list-tabs {
align-content: flex-start;
padding-right: 2px;
}
/* Full-admin controls bar: the view + "Hide lists" toggles grouped at the left,
the Location/Team filter pushed to the right (see .list-filter margin-left). */
.list-tabs-controls {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 10px;
flex-wrap: wrap;
}
.lists-toggle {
flex-shrink: 0;
min-width: 132px; /* fixed so the label swap (Tree/Tab, Hide/Show) never resizes the button */
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 7px 14px;
border-radius: 6px;
border: 1px solid #4a4a4a;
background: #1d2026;
color: #cfd5e3;
font-size: 13px;
font-weight: 600;
cursor: pointer;
}
.lists-toggle:hover { background: #252932; border-color: #5a5a5a; }
.lists-toggle .lt-icon { font-size: 15px; line-height: 1; }
.lists-toggle .lt-count { font-weight: 700; opacity: 0.85; }
/* Tab ↔ Tree view transition: the outgoing view shrinks + fades away, then the
incoming rows/tabs cascade into place (staggered via inline animation-delay). */
.list-tabs.view-leaving,
.list-tree.view-leaving {
transition: opacity 0.14s ease, transform 0.14s ease;
opacity: 0;
transform: scale(0.96);
transform-origin: top center;
pointer-events: none;
}
@keyframes viewCascadeIn {
from { opacity: 0; transform: translateY(-9px) scale(0.96); }
to { opacity: 1; transform: none; }
}
.list-tabs.view-entering > *,
.list-tree.view-entering > * {
animation: viewCascadeIn 0.34s cubic-bezier(0.22, 1, 0.36, 1) both;
}
@media (prefers-reduced-motion: reduce) {
.collapse-toggle .chevron { transition: none; } /* still rotates to reflect state, just instantly */
.col-fold .chevron { transition: none; }
.list-tabs.view-leaving,
.list-tree.view-leaving,
.list-tabs.view-entering > *,
.list-tree.view-entering > * {
animation: none !important;
transition: none !important;
opacity: 1;
transform: none;
}
}
/* The active list's name, shown only while the strip is hidden (keeps context). */
.active-list-name { display: none; }
body.admin-mode.lists-hidden .active-list-name {
display: block;
font-size: 13px;
color: #9aa9bd;
padding: 2px;
}
body.admin-mode.lists-hidden .active-list-name b { color: #eceef2; }
.active-list-scope {
margin-left: 4px;
padding: 1px 8px;
border-radius: 999px;
background: #1d2026;
border: 1px solid #2e333d;
color: #cfd5e3;
font-size: 11px;
white-space: nowrap;
}
/* Collapse the strip — only for editors (admin/exec), so members never get stuck hidden. */
body.admin-mode.lists-hidden .list-tabs { display: none; }
/* With the lists hidden, the sort/filter dropdowns and the tree toggle have
nothing to act on — hide them too so the bar stays clean. */
body.admin-mode.lists-hidden .list-filter,
body.admin-mode.lists-hidden #listViewToggle { display: none; }
/* Tablet (iPad portrait, split-screen, small windows): the full desktop header —
two toggles + two filters + undo/redo + ☰ Menu + the signed-in badge — is too
wide for one row above the 700px phone breakpoint, so the badge used to wrap to
a lonely right-aligned line with a big empty gap. Flatten the nested control
wrappers so the toolbar + badge flow together on ONE row, let the toggles size
to their content, and drop the location/team filters onto their own full-width
row. Labels stay (unlike phones) since there's room. admin-only stays hidden via
its !important rule, so this never exposes controls to checkers. */
@media (min-width: 701px) and (max-width: 1024px) {
header { gap: 8px 10px; }
.header-controls,
.list-tabs-controls { display: contents; }
.lists-toggle { min-width: 0; }
.list-filter {
order: 9;
flex: 1 1 100%;
margin-left: 0;
justify-content: flex-start;
gap: 8px;
}
.list-filter select { flex: 1 1 0; min-width: 0; max-width: 360px; }
}
@media (max-width: 700px) {
header { padding: calc(10px + env(safe-area-inset-top)) 12px 10px; gap: 6px; justify-content: flex-end; align-items: center; }
/* The compact logo shows on mobile too (sits at the far left of the bar). */
.header-left { display: flex; }
.app-logo { width: 32px; height: 32px; }
.main-content { padding: 10px; gap: 12px; }
/* Two tidy rows on a phone:
Row 1 = logo (far left) + search / report / who-you-are badge (far right) —
the always-visible, volunteer-facing cluster. For members this is
the whole header (a clean single row).
Row 2 = the admin toolbar, rendered as ONE cohesive full-width strip: a
subtle rounded container of equal-width buttons that fill the row
edge-to-edge, so it always reads as intentional (never a few icons
stranded on one side). list-tabs-controls + undo-redo stay flattened
so their buttons become direct segments of the strip; the master
location/team filters drop to their own full-width row inside it. */
.list-tabs-controls,
.undo-redo { display: contents; }
.icon-btn { width: 38px; height: 38px; font-size: 17px; }
.user-badge { padding: 8px 12px; font-size: 12px; }
/* Row 1: logo + a full-width search bar that eats the empty space, then the
report + identity badge on the right. The stretchy search bar is what makes
the row read as full/intentional instead of a couple icons floating with a
big gap. header-right grows to fill the width next to the logo. */
.header-right { order: 1; flex: 1 1 auto; min-width: 0; gap: 8px; align-items: center; justify-content: flex-end; }
.header-right .search-bar-btn {
flex: 1 1 auto;
width: auto;
min-width: 84px;
height: 38px;
justify-content: flex-start;
gap: 7px;
padding: 0 12px;
background: #14161b;
border-color: #2e333d;
color: #8b909b;
font-size: 13px;
font-weight: 500;
}
.header-right .search-bar-btn .sb-label { display: inline; }
.header-right .search-bar-btn .sb-icon { font-size: 15px; }
.header-controls {
display: flex;
order: 2;
flex: 1 1 100%;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-top: 6px;
padding: 6px;
background: #14161b;
border: 1px solid #2b303a;
border-radius: 12px;
}
/* Every toolbar button shares one clean, equal-width segment footprint. */
.header-controls .lists-toggle,
.header-controls .icon-btn,
.header-controls .list-menu-btn {
flex: 1 1 0;
min-width: 0;
width: auto;
height: 40px;
padding: 0 4px;
margin: 0;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 5px;
background: #191c22;
border: 1px solid #2e333d;
border-radius: 8px;
font-size: 16px;
}
.header-controls .lists-toggle:hover,
.header-controls .icon-btn:hover:not(:disabled),
.header-controls .list-menu-btn:hover { background: #22262e; border-color: #3a414d; }
.lists-toggle .lt-label,
.list-menu-btn .lmh-label { display: none; } /* icon-only on mobile */
.lists-toggle .lt-icon { font-size: 17px; }
.lists-toggle .lt-count { font-size: 12px; font-weight: 700; opacity: 0.85; }
.list-menu-btn .lmh-icon { font-size: 17px; }
/* Master location/team filters: their own full-width row inside the strip. */
.list-filter { order: 9; flex: 1 1 100%; margin: 2px 0 0; gap: 6px; }
.list-filter select { flex: 1 1 0; min-width: 0; max-width: none; padding: 8px 10px; }
/* Stack the two columns; the whole PAGE scrolls (only the top bar is sticky).
align-items:stretch overrides the desktop flex-start so each column fills the
full width (in a column flex, align-items is the horizontal axis) — otherwise
To Do / Done shrink to their content width and an empty column is a tiny box. */
.checklists-section {
flex-direction: column;
align-items: stretch;
gap: 12px;
}
.checklist-column { flex: 0 0 auto; overflow: hidden; min-width: 0; } /* stacked = full width; no floor needed */
/* Give each column a floor so an empty To Do / Done is still a proper full-width
box, not a sliver — consistent sizing whether or not it has tasks. */
.checklist-list { padding: 10px; min-height: 64px; }
.checklist-column h2 { padding: 12px 14px; font-size: 15px; }
/* Roomier cards on mobile. */
.checklist-item { margin-bottom: 10px; }
.checklist-item-header { padding: 12px 14px; }
.checklist-item-body { padding: 12px 14px; }
.checklist-item input[type="checkbox"] { width: 22px; height: 22px; }
.checklist-item-title { font-size: 16px; }
/* On a narrow header, let the repeat tag drop below the title instead of crowding it. */
.checklist-item-header { flex-wrap: wrap; }
.checklist-item-recurrence { margin-left: 30px; }
/* Edit / Move / Delete: wrap and fill the row as big tap targets. */
.checklist-item-actions { margin-top: 14px; flex-wrap: wrap; gap: 8px; }
.checklist-item-actions .btn { flex: 1 1 auto; min-width: 84px; padding: 11px 12px; font-size: 14px; }
/* List tabs easier to tap. The strip flows with the page now (no inner scroll/cap),
so the user scrolls past it — a leftover max-height here would spill the wrapped
tabs over the board. */
.list-tab { padding: 12px 16px; }
/* Tools menu becomes a bottom sheet that rises from the bottom over the dim
backdrop. Capped to ~62vh so the dimmed lists (and the menu button) stay
visible above it — never "items dumped on top of the lists". !important
overrides any JS popover coords if the viewport was resized while open. */
.list-menu.open .list-menu-panel {
position: fixed;
left: 10px !important;
right: 10px !important;
bottom: 10px !important;
top: auto !important;
width: auto;
min-width: 0;
max-width: none;
max-height: 62vh;
padding: 8px 10px 10px;
border-radius: 16px;
}
/* Grab-handle bar at the top of the sheet (clear "this is a sheet" affordance). */
.list-menu.open .list-menu-panel::before {
content: '';
flex: 0 0 auto;
width: 40px;
height: 4px;
margin: 2px auto 8px;
border-radius: 999px;
background: #555;
}
.list-menu-panel .btn { padding: 12px; font-size: 15px; }
.menu-group-label { font-size: 11px; padding: 10px 4px 3px; }
}
@media (max-width: 700px) and (prefers-reduced-motion: no-preference) {
.list-menu.open .list-menu-panel { animation: menuSheetUp 0.26s cubic-bezier(0.22, 1, 0.36, 1); }
}
@media (max-width: 700px) {
.directory-editor {
grid-template-columns: 1fr;
}
}
/* ─── Inter type + depth polish ──────────────────────────────────────
Tighter tracking on headings/titles (Inter reads crisper), tabular
figures so counts don't jitter, and a subtle resting shadow so cards
and the board sit on the page with a touch of depth. */
h1, h2, h3, .modal h3, .mgmt-head h3, .checklist-item-title, .checklist-column h2 {
letter-spacing: -0.011em;
}
.lt-count, .tree-count, .ov-count, .ov-bar + .ov-count, .mgmt-group-count,
.active-list-scope, .list-tab-loc, .checklist-item-meta {
font-variant-numeric: tabular-nums;
}
/* Resting depth on cards + the To Do/Done columns (hover adds more). */
.checklist-item { box-shadow: 0 1px 2px rgba(0, 0, 0, 0.22); }
.checklist-column { box-shadow: 0 2px 14px rgba(0, 0, 0, 0.35); }
/* Crisper hairline between the header strip and body inside a card. */
.checklist-item-header { border-bottom-color: #2e333d; }
/* ═══════════ Notes column + shared column reordering ═══════════ */
/* Hide a column entirely (the Notes column when it has no notes / Notes disabled). Kept in the DOM
so the FLIP refs and reorder logic stay stable; just not displayed. */
.checklist-column.col-hidden { display: none; }
/* A NOTE card: same shell as a task card, minus the checkbox, plus a thin amber left accent + glyph
so it self-identifies as reference (never reads as an unchecked task) even scrolled from its header. */
.checklist-item.is-note { border-left: 3px solid #e0a44e; }
.note-ico {
width: 20px; height: 20px; flex: 0 0 auto; margin-top: 1px;
display: grid; place-items: center; border-radius: 6px;
background: #2a2113; border: 1px solid #4a3a1e; color: #e0a44e;
}
.note-ico svg { display: block; }
/* A note's authored "- [ ]" lines are reference-only: render as a static bullet, never a tappable box. */
.checklist-item[data-kind="note"] .md-task-box {
border-radius: 50%; width: 6px; height: 6px; margin: 8px 6px 0 6px;
background: #6c7688; border: none; cursor: default; box-shadow: none;
}
.checklist-item[data-kind="note"] .md-task-box::before { display: none; } /* drop the tap-expansion hit area */
.checklist-item[data-kind="note"] .md-task { cursor: default; }
/* An attached-document link (uploaded via 📎) — small page glyph so it reads as
"a document lives here", like equipment terms read as tappable gear. */
.doc-link::before { content: '📄 '; }
/* Task-sections editor rows in the list modal (name field + remove / default pill). */
.list-section-row { display: flex; gap: 8px; align-items: center; margin: 4px 0; }
.list-section-row input { flex: 1 1 auto; min-width: 0; margin: 0; }
.list-section-default {
flex: 0 0 auto; font-size: 11px; color: #8b93a3;
border: 1px solid #2e333d; border-radius: 999px; padding: 3px 9px;
}
#listModalSectionAdd { margin-top: 4px; }
/* ── Cross-department requests ──────────────────────────────────────────────
The header badge is the "can't miss it" signal: red, always in the top bar
while any visible Requests inbox has open items, with a live count. */
.requests-badge {
display: inline-flex; align-items: center; gap: 5px;
padding: 6px 12px; border-radius: 999px; border: 1px solid rgba(239, 68, 68, 0.65);
background: rgba(239, 68, 68, 0.18); color: #ff8a80;
font-size: 13px; font-weight: 700; cursor: pointer;
-webkit-tap-highlight-color: transparent;
animation: requestsPulse 2.4s ease-in-out infinite;
}
@keyframes requestsPulse {
0%, 100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.35); }
50% { box-shadow: 0 0 0 5px rgba(239, 68, 68, 0); }
}
@media (prefers-reduced-motion: reduce) { .requests-badge { animation: none; } }
@media (hover: hover) { .requests-badge:hover { background: rgba(239, 68, 68, 0.3); color: #ffb3ac; } }
/* Open-count pill on the Requests list tab (and tree leaf). */
.list-tab-requests {
display: inline-flex; align-items: center; margin-left: 6px; padding: 1px 7px;
border-radius: 999px; background: rgba(239, 68, 68, 0.22);
border: 1px solid rgba(239, 68, 68, 0.55); color: #ff8a80;
font-size: 11px; font-weight: 700; font-style: normal;
}
/* Who asked, on the request card. */
.request-meta { color: #e0a800; }
/* Attached photos: tappable thumbnails that open the pan/zoom lightbox. */
.request-photo-strip { display: flex; flex-wrap: wrap; gap: 8px; margin: 10px 0 4px; }
.request-photo-open {
padding: 0; border: 1px solid #2e333d; border-radius: 8px; overflow: hidden;
background: #000; cursor: pointer; width: 84px; height: 84px; flex: 0 0 auto;
}
.request-photo-open img { width: 100%; height: 100%; object-fit: cover; display: block; }
.request-photo-open:focus-visible { outline: 2px solid #39a8ff; outline-offset: 1px; }
.request-photo-name { flex: 1 1 auto; min-width: 0; color: #8b93a3; font-size: 12px; }
/* ── Feedback column (team input) ───────────────────────────────────────────
Writable by everyone on the list — entry cards + an always-there composer.
Styled like task cards but lighter: text first, small attribution line. */
.feedback-entry {
background: #252932;
border: 1px solid #2e333d;
border-left: 3px solid #39a8ff;
border-radius: 8px;
padding: 10px 12px;
margin-bottom: 12px;
}
.feedback-text { color: #dfe3ea; font-size: 14px; line-height: 1.45; overflow-wrap: break-word; }
.feedback-meta { margin-top: 6px; color: #8b93a3; font-size: 12px; }
.feedback-actions { display: flex; gap: 6px; margin-top: 8px; }
.feedback-composer { display: flex; flex-direction: column; gap: 8px; margin-top: 2px; }
.feedback-input {
width: 100%; box-sizing: border-box; min-height: 64px; resize: vertical;
background: #1a1d23; color: #eceef2; border: 1px solid #2e333d; border-radius: 8px;
padding: 9px 11px; font: inherit; font-size: 14px; line-height: 1.4;
}
.feedback-input:focus { outline: none; border-color: #39a8ff; }
.feedback-composer .btn { align-self: flex-end; }
.feedback-editbox .feedback-actions { margin-top: 2px; }
/* Column header becomes a flex row so the reorder controls sit at the end without crowding the title. */
.checklist-column h2 { display: flex; align-items: center; gap: 8px; }
.checklist-column h2 .col-title { flex: 1; min-width: 0; }
.col-reorder { display: inline-flex; gap: 2px; flex: 0 0 auto; }
.col-move {
position: relative; width: 32px; height: 32px; padding: 0;
display: grid; place-items: center; border: 0; background: transparent;
color: #8b95a7; border-radius: 8px; cursor: pointer;
-webkit-tap-highlight-color: transparent; touch-action: manipulation;
}
.col-move::before { content: ''; position: absolute; inset: -7px; } /* ≥46px hit target */
.col-move svg { display: block; pointer-events: none; }
.col-move[disabled] { opacity: 0.28; cursor: default; }
@media (hover: hover) { .col-move:not([disabled]):hover { color: #eceef2; background: rgba(255,255,255,0.07); } }
@media (hover: hover) { .col-clear-feedback:hover { color: #ff8a80; background: rgba(239,68,68,0.12); } }
.col-move:focus-visible { outline: 2px solid #39a8ff; outline-offset: -2px; }
/* Only editors see the reorder controls; hidden in show mode like the other admin-only chrome. */
body:not(.admin-mode) .col-reorder { display: none; }
/* ── Fold a whole column (To Do / Done / Notes) to just its header ──────────
Per-user + local-only, like task collapse — and unlike .col-reorder it is
NOT admin-gated: every role gets the chevron. The .col-folded rule is the
RESTING STATE (keep it outside any reduced-motion block); display:none also
drops the hidden checkboxes/buttons out of the tab order. */
.checklist-column.col-folded .checklist-list { display: none; }
.checklist-column.col-folded h2 { border-bottom: none; } /* folded = header only; keep the rounded box clean */
.checklist-column h2.col-foldable { cursor: pointer; -webkit-tap-highlight-color: transparent; }
.col-fold {
position: relative; width: 32px; height: 32px; padding: 0; flex: 0 0 auto;
display: grid; place-items: center; border: 0; background: transparent;
color: #8b95a7; border-radius: 8px; cursor: pointer;
-webkit-tap-highlight-color: transparent; touch-action: manipulation;
}
.col-fold::before { content: ''; position: absolute; inset: -7px; } /* ≥46px hit target */
.col-fold svg { display: block; pointer-events: none; }
.col-fold .chevron { transition: transform 0.2s cubic-bezier(0.22,1,0.36,1); }
/* Bound to aria-expanded (flipped synchronously on tap), matching the task-card chevron. */
.col-fold[aria-expanded="false"] .chevron { transform: rotate(-90deg); }
@media (hover: hover) { .col-fold:hover { color: #eceef2; background: rgba(255,255,255,0.07); } }
.col-fold:focus-visible { outline: 2px solid #39a8ff; outline-offset: -2px; }
/* Hidden-item count pill — only rendered visible while folded, so an open header stays clean. */
.col-count {
display: none; flex: 0 0 auto; min-width: 22px; height: 22px; padding: 0 7px;
align-items: center; justify-content: center;
border-radius: 11px; background: rgba(255,255,255,0.08); color: #aab2c0;
font-size: 12px; font-weight: 600; line-height: 1;
}
.checklist-column.col-folded .col-count { display: inline-flex; }
/* Stacking: when the Notes column is showing there are THREE columns, which are too tight on an iPad
in portrait (768–834px) — stack them full-width there. MUST re-declare align-items:stretch and
flex:0 0 auto (the base section is align-items:flex-start, which in a column flex would shrink each
column to a left-aligned sliver — the same bug the ≤700 block fixes for two columns). */
@media (max-width: 960px) {
.checklists-section.has-notes { flex-direction: column; align-items: stretch; gap: 12px; }
.checklists-section.has-notes .checklist-column { flex: 0 0 auto; min-width: 0; } /* stacked = full width; no floor */
}
/* When columns are stacked, the reorder chevrons point up/down instead of left/right. */
@media (max-width: 960px) { .checklists-section.has-notes .col-move svg { transform: rotate(90deg); } }
@media (max-width: 700px) { .checklists-section:not(.has-notes) .col-move svg { transform: rotate(90deg); } }
/* ═══════════ Booth keyboard shortcuts ═══════════ */
.checklist-item.kb-focus { outline: 2px solid #39a8ff; outline-offset: 2px; }
.kb-cheat { position: fixed; inset: 0; z-index: 4000; display: none; align-items: center; justify-content: center; background: rgba(0,0,0,.55); padding: 20px; }
.kb-cheat.open { display: flex; }
.kb-cheat .panel { background: #1d2026; border: 1px solid #2e333d; border-radius: 14px; padding: 20px 24px; max-width: 440px; width: 100%; box-shadow: 0 24px 60px rgba(0,0,0,.5); }
.kb-cheat h3 { margin: 0 0 14px; font-size: 16px; color: #eceef2; }
.kb-cheat .krow { display: flex; justify-content: space-between; align-items: center; gap: 16px; padding: 8px 0; border-bottom: 1px solid #23272f; font-size: 14px; color: #cfd5e3; }
.kb-cheat .krow:last-of-type { border-bottom: 0; }
.kb-cheat kbd { font-family: inherit; font-size: 12px; font-weight: 700; background: #0f1216; border: 1px solid #2e333d; border-bottom-width: 2px; border-radius: 5px; padding: 2px 8px; color: #eceef2; white-space: nowrap; }
/* ═══════════ First-run welcome / tips ═══════════ */
.firstrun { position: fixed; inset: 0; z-index: 4001; display: none; align-items: center; justify-content: center; background: rgba(0,0,0,.6); padding: 20px; }
.firstrun.open { display: flex; }
.firstrun .panel { background: #1d2026; border: 1px solid #2e333d; border-radius: 16px; padding: 22px 24px; max-width: 460px; width: 100%; box-shadow: 0 24px 60px rgba(0,0,0,.55); }
.firstrun h3 { margin: 0 0 4px; font-size: 19px; font-weight: 800; color: #eceef2; }
.firstrun .lead { margin: 0 0 16px; color: #8a93a6; font-size: 13.5px; }
.firstrun .tip { display: flex; gap: 12px; align-items: flex-start; padding: 9px 0; border-bottom: 1px solid #23272f; }
.firstrun .tip:last-of-type { border-bottom: 0; }
.firstrun .tip .ic { width: 30px; height: 30px; flex: 0 0 auto; border-radius: 8px; background: #0f1216; border: 1px solid #2e333d; display: grid; place-items: center; font-size: 15px; }
.firstrun .tip .tx { font-size: 13.5px; line-height: 1.5; color: #cfd5e3; } .firstrun .tip .tx b { color: #eceef2; }
.firstrun .go { width: 100%; margin-top: 16px; }
/* ═══════════ Global search ═══════════ */
#searchBtn { font-size: 16px; }
.search-overlay { position: fixed; inset: 0; z-index: 4002; display: flex; align-items: flex-start; justify-content: center; background: rgba(0,0,0,.55); padding: 8vh 16px 16px; }
.search-panel { background: #1d2026; border: 1px solid #2e333d; border-radius: 14px; width: 100%; max-width: 560px; box-shadow: 0 24px 60px rgba(0,0,0,.5); overflow: hidden; max-height: 80vh; display: flex; flex-direction: column; }
.search-inputrow { display: flex; align-items: center; gap: 10px; padding: 12px 14px; border-bottom: 1px solid #2e333d; }
.search-inputrow svg { width: 18px; height: 18px; stroke: #8a93a6; fill: none; stroke-width: 2; flex: 0 0 auto; }
.search-inputrow input { flex: 1; background: transparent; border: 0; color: #eceef2; font-size: 16px; font-family: inherit; outline: none; min-width: 0; }
.search-inputrow input::placeholder { color: #7c828c; }
.search-results { overflow-y: auto; padding: 6px; }
.search-group { font-size: 11px; letter-spacing: .08em; text-transform: uppercase; color: #8a93a6; font-weight: 700; padding: 10px 10px 4px; }
.search-hit { display: flex; align-items: center; gap: 11px; padding: 9px 10px; border-radius: 8px; cursor: pointer; }
.search-hit:hover, .search-hit.active { background: #242b36; }
.search-hit .ic { width: 26px; height: 26px; flex: 0 0 auto; border-radius: 7px; display: grid; place-items: center; background: #0f1216; border: 1px solid #2e333d; font-size: 13px; }
.search-hit .main { min-width: 0; flex: 1; }
.search-hit .t { font-size: 14px; color: #eceef2; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.search-hit .sub { font-size: 12px; color: #8a93a6; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.search-empty { padding: 24px 12px; text-align: center; color: #8a93a6; font-size: 13.5px; }
/* ═══════════ Equipment video (a video media item, added by URL) ═══════════ */
.equip-video { display: none; width: 100%; max-height: 68vh; background: #000; border-radius: 8px; }
.equip-thumb-btn.equip-thumb-vid { display: grid; place-items: center; background: #0f1216; color: #cfd5e3; font-size: 15px; }
.equip-thumb-btn.equip-thumb-vid span { pointer-events: none; }
/* ═══════════ Incident reports (gear problems) ═══════════ */
/* Shared modal chrome used by the report form */
.modal .modal-header { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.modal .modal-header h3 { margin: 0; }
.modal .modal-footer { display: flex; justify-content: flex-end; gap: 8px; margin-top: 4px; }
.label-hint { color: #7d828c; font-weight: 400; font-size: 12px; }
/* Header + lightbox report triggers */
.report-issue-btn { color: #ffb020; font-size: 17px; line-height: 1; }
.report-issue-btn:hover { color: #ffca5c; }
.equip-report-btn {
margin-left: auto; margin-right: 6px;
background: transparent; border: 1px solid #b0451f; color: #ff9d6b;
border-radius: 999px; padding: 5px 12px; font-size: 13px; font-weight: 600; cursor: pointer;
white-space: nowrap;
}
.equip-report-btn:hover { background: rgba(176,69,31,0.18); color: #ffb894; }
/* Severity picker */
.incident-gear-row { display: flex; align-items: baseline; gap: 8px; padding: 8px 10px; background: #14161b; border: 1px solid #2e333d; border-radius: 6px; }
.incident-gear-label { color: #7d828c; font-size: 12px; }
.incident-gear-name { color: #eceef2; font-weight: 600; }
.severity-toggle { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.sev-btn {
display: flex; flex-direction: column; align-items: flex-start; gap: 2px;
text-align: left; padding: 10px 12px; cursor: pointer;
background: #14161b; border: 1px solid #2e333d; border-radius: 8px; color: #cfd5e3;
transition: border-color 0.12s, background 0.12s;
}
.sev-btn:hover { border-color: #4a5160; }
.sev-emoji { font-size: 18px; }
.sev-title { font-weight: 700; font-size: 14px; color: #eceef2; }
.sev-sub { font-size: 11px; color: #8b909b; line-height: 1.25; }
.sev-btn.sev-down.is-selected { border-color: #e5484d; background: rgba(229,72,77,0.14); }
.sev-btn.sev-minor.is-selected { border-color: #ffb020; background: rgba(255,176,32,0.13); }
.incident-reporting-as { font-size: 12px; color: #8b909b; margin-top: -2px; }
/* Known-issue history inside the gear lightbox */
.equip-issues { display: none; margin-top: 10px; border-top: 1px solid #2e333d; padding-top: 10px; }
.equip-issues-head { font-size: 13px; font-weight: 700; color: #ffb894; margin-bottom: 6px; }
.equip-issue { display: flex; align-items: flex-start; gap: 8px; padding: 8px; border-radius: 6px; background: #14161b; margin-bottom: 6px; }
.equip-issue.down { border-left: 3px solid #e5484d; }
.equip-issue.minor { border-left: 3px solid #ffb020; }
.equip-issue .ei-sev { font-size: 14px; line-height: 1.4; }
.equip-issue .ei-body { display: flex; flex-direction: column; gap: 2px; flex: 1; min-width: 0; }
.equip-issue .ei-what { color: #eceef2; font-weight: 600; font-size: 13px; }
.equip-issue .ei-details { color: #b9bec8; font-size: 12px; white-space: pre-wrap; }
.equip-issue .ei-meta { color: #7d828c; font-size: 11px; }
.equip-issue .ei-resolve { flex: 0 0 auto; align-self: center; }
/* Editor incidents triage view */
.incidents-controls { display: flex; align-items: center; gap: 12px; margin: 4px 0 10px; flex-wrap: wrap; }
.incidents-openonly { display: flex; align-items: center; gap: 6px; font-size: 13px; color: #cfd5e3; white-space: nowrap; }
.incidents-search { flex: 1; min-width: 140px; padding: 8px 10px; border: 1px solid #2e333d; border-radius: 6px; background: #14161b; color: #eceef2; }
.incidents-list { display: flex; flex-direction: column; gap: 8px; overflow-y: auto; }
.incidents-empty { color: #8b909b; text-align: center; padding: 24px 12px; }
.incident-row { display: flex; align-items: flex-start; gap: 10px; padding: 10px; border-radius: 8px; background: #14161b; border: 1px solid #2e333d; }
.incident-row.down { border-left: 3px solid #e5484d; }
.incident-row.minor { border-left: 3px solid #ffb020; }
.incident-row.is-resolved { opacity: 0.62; }
.incident-row .ir-sev { font-size: 15px; line-height: 1.4; }
.incident-row .ir-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.incident-row .ir-title { color: #eceef2; font-weight: 700; font-size: 14px; }
.incident-row .ir-scope { color: #7d828c; font-weight: 400; font-size: 12px; margin-left: 4px; }
.incident-row .ir-what { color: #d7dbe3; font-size: 13px; }
.incident-row .ir-details { color: #b9bec8; font-size: 12px; white-space: pre-wrap; }
.incident-row .ir-meta { color: #7d828c; font-size: 11px; margin-top: 2px; }
.incident-row .ir-action { flex: 0 0 auto; align-self: center; }
/* Menu count pill on the ⚠ Incidents button */
.menu-count { display: inline-block; margin-left: 6px; min-width: 18px; padding: 0 5px; border-radius: 999px; background: #e5484d; color: #fff; font-size: 11px; font-weight: 700; text-align: center; line-height: 18px; }
/* ═══════════ Reliability dashboard ═══════════ */
.reliability-controls { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin: 4px 0 10px; flex-wrap: wrap; }
.reliability-summary { color: #8b909b; font-size: 12px; }
.reliability-list { display: flex; flex-direction: column; gap: 8px; overflow-y: auto; }
.reliability-row { display: flex; align-items: center; gap: 12px; padding: 10px 12px; border-radius: 8px; background: #14161b; border: 1px solid #2e333d; cursor: pointer; transition: border-color 0.12s, background 0.12s; }
.reliability-row:hover { border-color: #4a5160; background: #171a20; }
.reliability-row.is-clear { opacity: 0.72; }
.reliability-row .rr-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.reliability-row .rr-name { color: #eceef2; font-weight: 700; font-size: 14px; }
.reliability-row .rr-scope { color: #7d828c; font-weight: 400; font-size: 12px; margin-left: 4px; }
.reliability-row .rr-meta { color: #8b909b; font-size: 12px; }
.reliability-row .rr-spark { display: flex; align-items: flex-end; gap: 2px; height: 24px; flex: 0 0 auto; }
.reliability-row .spark-bar { width: 5px; border-radius: 1px; background: #2e333d; }
.reliability-row .spark-bar.has { background: #39a8ff; }
.reliability-row .rr-badge { flex: 0 0 auto; min-width: 58px; text-align: right; }
.reliability-row .rr-open { display: inline-block; padding: 2px 8px; border-radius: 999px; background: rgba(229,72,77,0.16); color: #ff8a8d; font-size: 12px; font-weight: 700; }
.reliability-row .rr-clear { color: #4a8f5a; font-size: 12px; font-weight: 600; }
/* ═══════════ "New version available" banner ═══════════
Shown only when a deploy is detected while it ISN'T safe to auto-reload (dialog open, typing,
queued check-offs). Sits above everything so it can't be missed on a wall tablet. */
.update-banner {
position: fixed; left: 50%; bottom: 20px; transform: translateX(-50%);
z-index: 5000; display: flex; align-items: center; gap: 12px;
background: #1d2026; border: 1px solid #39a8ff; border-radius: 10px;
padding: 11px 14px; color: #eceef2; font-size: 14px;
box-shadow: 0 12px 34px rgba(0,0,0,.5);
}
/* ── First-launch setup (native apps only) ─────────────────────────────────────────────
The store build ships to anyone, so which server an install talks to is chosen here
rather than baked into the binary. Deliberately NOT a .modal-overlay: this appears
before sign-in, must not be dismissible, and the Android back-button handler skips it
for exactly that reason (it looks for .modal-overlay). */
.setup-overlay {
position: fixed;
inset: 0;
z-index: 10050;
background: #14161b;
display: flex;
align-items: center;
justify-content: center;
padding: max(20px, env(safe-area-inset-top)) 20px max(20px, env(safe-area-inset-bottom));
overflow-y: auto;
}
.setup-card { width: 100%; max-width: 420px; }
.setup-logo { width: 62px; height: 62px; margin: 0 auto 18px; }
.setup-logo svg { width: 100%; height: 100%; border-radius: 14px; display: block; }
.setup-overlay h2 { margin: 0 0 6px; text-align: center; font-size: 22px; letter-spacing: -0.02em; }
.setup-sub { margin: 0 0 22px; text-align: center; color: #9aa2ad; font-size: 13.5px; line-height: 1.5; }
.setup-choice {
display: block;
width: 100%;
text-align: left;
background: #1d2026;
border: 1px solid #2e333d;
border-radius: 12px;
padding: 15px 16px;
margin-bottom: 11px;
color: inherit;
font: inherit;
cursor: pointer;
transition: border-color .15s ease, background .15s ease, transform .08s ease;
}
.setup-choice:hover { border-color: #3d4450; background: #21242b; }
.setup-choice:active { transform: scale(0.99); }
.setup-choice-title { display: block; font-weight: 600; font-size: 15px; margin-bottom: 3px; }
.setup-choice-desc { display: block; color: #9aa2ad; font-size: 12.5px; line-height: 1.45; }
.setup-overlay label {
display: block; margin-bottom: 7px; font-size: 12.5px; color: #9aa2ad; font-weight: 500;
}
#setupAddress {
width: 100%;
padding: 12px 13px;
background: #1d2026;
border: 1px solid #2e333d;
border-radius: 9px;
color: #eceef2;
/* 16px min, or iOS Safari zooms the whole page the moment the field is focused */
font-size: 16px;
}
#setupAddress:focus { outline: none; border-color: #39a8ff; }
.setup-error {
margin-top: 10px; padding: 9px 11px; border-radius: 8px;
background: rgba(231, 76, 60, 0.12); border: 1px solid rgba(231, 76, 60, 0.35);
color: #f1948a; font-size: 12.5px; line-height: 1.45;
}
.setup-actions { display: flex; gap: 9px; justify-content: flex-end; margin-top: 16px; }
.setup-actions .btn { min-width: 84px; }