Files
negotium/src/style.css
T
Aculix Technologies b846717db9 fix: touch reachability, contrast and alignment
A pass over desktop, tablet and phone widths, measuring rather than
eyeballing.

The one that matters: a task could not be deleted on a phone. The delete
button is revealed on hover, touch devices report hover: none, so it sat
at zero opacity with nothing to reveal it. The README told people to hover
over a task and click it. It is now always visible where hover does not
exist.

Touch targets were under the 44px minimum: the checkbox at 24, delete at
32, the footer links at 23 tall. They now get 44px hit areas from overlays
that leave the drawn size alone, behind a pointer: coarse query so a mouse
keeps small precise targets and the full row stays draggable.

Two contrast failures in light mode. The date was the brand blue on the
near-white background at 3.54:1, so text now uses a darker --accent-text
and reads 5.14:1. Completed rows carried opacity 0.8 over an already
mid-contrast blue, which came to 3.34:1; the tint, the strikethrough and
the colour say "done" well enough without it, and dropping it gives 4.78:1.
Both themes now pass AA everywhere measured.

The checkbox sat vertically centred, so on a task wrapping to five lines
it floated in the middle of the block. Rows align to the top and task text
gets a 24px line box, matching the checkbox exactly, which also spaces
wrapped lines better.

Reduced motion was only honoured by the empty-state drawing. Svelte's
transitions run in JavaScript and never saw the media query, so tasks
still flew in on a stagger. The component reads the preference directly
and collapses its durations, and CSS covers the rest. The lift on a
dragged card stays, since it tracks the pointer rather than playing at you.

Long unbroken words already wrapped and no breakpoint scrolled sideways.
2026-08-16 02:36:48 +05:30

675 lines
13 KiB
CSS

: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;
}