:root { --bg-primary: #F8FAFB; --bg-surface: #FFFFFF; --text-primary: #1A1A1A; --text-secondary: #666666; --accent: #607afb; /* Darker sibling of --accent for text. The brand blue reads 3.54:1 on the light background, under the 4.5:1 needed for body-sized text. */ --accent-text: #4C5FD5; --border: #E0E0E0; --hover: #F5F5F5; --completed-bg: #EEF1FF; --completed-text: #4C5FD5; --transition-speed: 300ms; --font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; } html.dark { --bg-primary: #121212; --bg-surface: #1E1E1E; --text-primary: #E0E0E0; --text-secondary: #999999; --accent: #7B93FF; --accent-text: #8FA5FF; --border: #333333; --hover: #2A2A2A; --completed-bg: rgba(96, 122, 251, 0.15); --completed-text: #8FA5FF; } * { box-sizing: border-box; margin: 0; padding: 0; } body { font-family: var(--font-family); background-color: var(--bg-primary); color: var(--text-primary); transition: background-color var(--transition-speed) ease, color var(--transition-speed) ease; } .app { min-height: 100vh; background-color: var(--bg-primary); transition: background-color var(--transition-speed) ease; } .header { background-color: var(--bg-surface); border-bottom: 1px solid var(--border); /* The extra top padding is the iOS status bar when the app is installed to the home screen. Without it the header sits underneath the clock. */ padding: calc(24px + env(safe-area-inset-top)) 48px 24px; position: sticky; top: 0; z-index: 100; transition: all var(--transition-speed) ease; } .header-content { max-width: 1200px; margin: 0 auto; display: flex; justify-content: space-between; align-items: center; } .logo-section { display: flex; align-items: center; gap: 12px; } .logo-icon { width: 32px; height: 32px; color: var(--accent); } .app-title { font-size: 28px; font-weight: bold; color: var(--text-primary); transition: color var(--transition-speed) ease; } .header-actions { display: flex; align-items: center; gap: 16px; } .today-btn { display: flex; align-items: center; gap: 8px; padding: 8px 16px; background: transparent; border: 1px solid var(--border); border-radius: 8px; color: var(--text-secondary); font-size: 14px; cursor: pointer; transition: all 200ms ease; height: 40px; } .today-btn:hover { background-color: var(--hover); border-color: var(--accent); color: var(--accent); } .today-btn svg { width: 16px; height: 16px; } .theme-toggle { padding: 8px 16px; border-radius: 8px; background: transparent; border: 1px solid var(--border); color: var(--text-secondary); cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 200ms ease; font-size: 14px; height: 40px; } .theme-toggle:hover { background-color: var(--hover); border-color: var(--accent); color: var(--accent); transform: scale(1.1); } .theme-toggle:active { transform: scale(0.95); } .theme-icon { width: 20px; height: 20px; transition: transform 500ms ease; } .theme-icon.rotated { transform: rotate(360deg); } .main { padding: 40px 0 calc(40px + env(safe-area-inset-bottom)); } .container { max-width: 800px; margin: 0 auto; padding: 0 32px; } .content-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 32px; } .section-title { font-size: 32px; font-weight: bold; color: var(--text-primary); transition: color var(--transition-speed) ease; } .date-display { font-size: 16px; font-weight: 500; color: var(--accent-text); } .task-input-container { margin-bottom: 24px; } .task-input { width: 100%; padding: 16px 20px; font-size: 16px; border: 1px solid var(--border); border-radius: 12px; background-color: var(--bg-surface); color: var(--text-primary); transition: all 300ms cubic-bezier(0.4, 0, 0.2, 1); font-family: var(--font-family); } .task-input:focus { outline: none; border: 1px solid var(--border); background-color: var(--bg-primary); box-shadow: 0 8px 24px rgba(96, 122, 251, 0.12), 0 4px 8px rgba(0, 0, 0, 0.06); transform: scale(1.01); } .task-input::placeholder { color: var(--text-secondary); } .task-stats { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; opacity: 0; transition: opacity 300ms ease; } .task-stats.visible { opacity: 1; } .task-count { font-size: 14px; color: var(--text-secondary); font-weight: 500; } .clear-completed { background: transparent; border: none; color: var(--text-secondary); font-size: 14px; cursor: pointer; padding: 4px 8px; border-radius: 4px; transition: all 200ms ease; } .clear-completed:hover { color: var(--accent); background-color: var(--hover); } .task-list { min-height: 400px; position: relative; } /* A real list, so screen readers announce item counts and position. */ .task-items { position: relative; display: flex; flex-direction: column; gap: 12px; list-style: none; margin: 0; padding: 0; } .task-item { display: flex; align-items: flex-start; gap: 16px; padding: 16px 20px; background-color: var(--bg-surface); border: 1px solid var(--border); border-radius: 12px; cursor: grab; transition: all 300ms ease; position: relative; } .task-item:active { cursor: grabbing; } /* The lifted card. It stays fully opaque and sits above the list. The old ghosted 0.4 opacity read as "this card is disabled" rather than "you are holding this card". The gap that opens beneath it is the drop indicator. */ .task-item.dragging { cursor: grabbing; z-index: 20; box-shadow: 0 12px 28px rgba(0, 0, 0, 0.18), 0 4px 8px rgba(0, 0, 0, 0.1); border-color: var(--accent); /* Only while a drag is in flight, so an ordinary touch still scrolls. */ touch-action: none; user-select: none; } html.dark .task-item.dragging { box-shadow: 0 12px 28px rgba(0, 0, 0, 0.5), 0 4px 8px rgba(0, 0, 0, 0.35); } /* The released card easing back into its slot. It keeps the raised z-index so it stays above its neighbours on the way down, and supplies the transition that the lifted state deliberately withheld. */ .task-item.settling { z-index: 20; transition: transform 240ms cubic-bezier(0.2, 0, 0, 1), box-shadow 240ms ease, border-color 240ms ease; } .task-item:hover { background-color: var(--hover); transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); } .task-items:has(.dragging) .task-item:not(.dragging):hover { background-color: var(--bg-surface); box-shadow: none; } .task-item.completed { background-color: var(--completed-bg); } .checkbox { width: 24px; height: 24px; border: 2px solid var(--border); border-radius: 6px; background: transparent; cursor: pointer; display: block; transition: all 300ms ease; flex-shrink: 0; position: relative; } .checkbox:hover { border-color: var(--accent); transform: scale(1.1); } .checkbox.checked { background-color: var(--accent); border-color: var(--accent); transform: scale(1.1); } .checkbox.checked:hover { transform: scale(1.2); } .checkmark { width: 16px; height: 16px; color: white; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); } .task-text { flex: 1; font-size: 16px; /* 24px line box, the same height as the checkbox, so the two align on the first line without nudging either. */ line-height: 1.5; color: var(--text-primary); transition: all 300ms ease; word-break: break-word; } .task-item.completed .task-text { text-decoration: line-through; color: var(--completed-text); } .delete-btn { width: 32px; height: 32px; border: none; position: relative; background: transparent; color: var(--text-secondary); cursor: pointer; border-radius: 6px; display: flex; align-items: center; justify-content: center; opacity: 0; transition: all 200ms ease; flex-shrink: 0; } /* focus-within matters as much as hover here: the button is opacity 0 by default, so a keyboard user tabbing to it previously saw nothing at all: the focus outline was drawn on an invisible element. */ .task-item:hover .delete-btn, .task-item:focus-within .delete-btn { opacity: 1; } .delete-btn:hover { background-color: rgba(244, 67, 54, 0.1); color: #f44336; transform: scale(1.1); } .delete-btn svg { width: 16px; height: 16px; } .empty-state { text-align: center; padding: 60px 20px; color: var(--text-secondary); position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; } /* Replaces a 305 KB Lottie player that existed to draw this one picture. Same 200px box, so the empty state keeps its original proportions. */ .empty-art { width: 200px; height: 200px; margin: 0 auto 24px; color: var(--accent); } .empty-art-box { animation: empty-float 4s ease-in-out infinite; } .empty-art-motes path { animation: empty-mote 4s ease-in-out infinite; } .empty-art-motes path:nth-child(2) { animation-delay: 0.5s; } .empty-art-motes path:nth-child(3) { animation-delay: 1s; } @keyframes empty-float { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-4px); } } @keyframes empty-mote { 0%, 100% { opacity: 0.15; transform: translateY(0); } 50% { opacity: 0.6; transform: translateY(-6px); } } /* Decorative only, so respect a reduced-motion preference. */ /* Svelte's transitions run in JavaScript and never see this query, so App.svelte checks the same preference and zeroes its durations. This half covers everything driven by CSS. */ @media (prefers-reduced-motion: reduce) { .empty-art-box, .empty-art-motes path { animation: none; } *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; scroll-behavior: auto !important; } } .empty-state p { font-size: 16px; margin: 0; } /* Deliberately quiet. Export and import get reached for once in a blue moon, so they sit below the list in secondary text rather than taking a slot in the header next to things you use constantly. */ .data-footer { display: flex; align-items: center; gap: 8px; margin-top: 32px; padding-top: 16px; border-top: 1px solid var(--border); font-size: 13px; color: var(--text-secondary); flex-wrap: wrap; } .data-link { background: transparent; border: none; padding: 4px 6px; margin: 0; border-radius: 4px; font-family: var(--font-family); font-size: 13px; color: var(--text-secondary); cursor: pointer; transition: color 200ms ease, background-color 200ms ease; } .data-link:hover { color: var(--accent); background-color: var(--hover); } .data-sep { color: var(--border); } .data-status { margin-left: 4px; opacity: 0; transition: opacity 200ms ease; } .data-status:not(:empty) { opacity: 1; } .data-status.error { color: #d14343; } html.dark .data-status.error { color: #ff8a8a; } /* Touch devices. * * The delete button is revealed on hover, which does not exist here. Without * this it stays at zero opacity and a task cannot be deleted on a phone at * all, which is what the README told people to do. * * The rest widens hit areas to the 44px minimum. The overlays are positioned * so nothing moves: the controls keep the size they were drawn at, they are * just easier to hit. Kept behind the media query so a mouse still gets small, * precise targets and the full row stays available to drag. */ @media (hover: none) { .delete-btn { opacity: 1; } } @media (pointer: coarse) { .checkbox::after, .delete-btn::after { content: ''; position: absolute; top: 50%; left: 50%; width: 44px; height: 44px; transform: translate(-50%, -50%); } .today-btn, .theme-toggle { height: 44px; } .clear-completed, .data-link { display: inline-flex; align-items: center; min-height: 44px; } } .visually-hidden { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; } @media (max-width: 768px) { .header { padding: calc(16px + env(safe-area-inset-top)) 24px 16px; } .header-content { flex-direction: column; gap: 16px; align-items: flex-start; } .header-actions { width: 100%; justify-content: space-between; } .container { padding: 0 24px; } .content-header { flex-direction: column; align-items: flex-start; gap: 8px; } .section-title { font-size: 28px; } .task-item { padding: 20px; min-height: 48px; } .task-input { padding: 20px; font-size: 18px; } } @media (max-width: 480px) { .header { padding: calc(12px + env(safe-area-inset-top)) 16px 12px; } .container { padding: 0 16px; } .task-item { padding: 16px; } .task-input { padding: 16px; } } .checkbox:focus-visible, .delete-btn:focus-visible, .theme-toggle:focus-visible, .today-btn:focus-visible, .clear-completed:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; } html { scroll-behavior: smooth; }