* { 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 and 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