@tailwind base; @tailwind components; @tailwind utilities; html, body, #app { height: 100%; } /* Track the VISUAL viewport, not the layout viewport. * * On a phone the two diverge the moment the on-screen keyboard opens or the URL bar * retracts: `100%` keeps the taller layout viewport, which slides the sticky header * off screen and can park a focused field underneath the keyboard. `dvh` is the unit * that follows the box actually being shown. * * Same three selectors rather than a fourth rule, so the box model is unchanged and * only the number moves; behind @supports so a browser without `dvh` keeps the * `100%` above instead of falling back to nothing. */ @supports (height: 100dvh) { html, body, #app { height: 100dvh; } } /* Landscape on a notched phone puts the cutout down one SIDE of the page, and with * `viewport-fit=cover` (index.html) the page is drawing under it. Applied to body so * every view inherits it, rather than each container remembering; the insets are 0 * on every device that has no cutout, and 0 in portrait, so this costs nothing where * it isn't needed. Top and bottom are NOT done here — a sticky header and a scrolling * board need them in their own boxes, not on the page. */ body { padding-left: env(safe-area-inset-left, 0px); padding-right: env(safe-area-inset-right, 0px); } body { @apply bg-neutral-50 text-neutral-900 antialiased; } @media (prefers-color-scheme: dark) { body { @apply bg-neutral-950 text-neutral-100; } } /* An untagged note's fill is GENERATED from its id, not chosen from the palette — * see `derivedFill` in notes/colors.ts for why nine keys was never going to be * enough. That means it cannot be a Tailwind class, and it cannot be a plain inline * style either: light and dark are two different computed colours and an inline * style has no way to answer a media query. So the card publishes both as custom * properties and this rule picks between them. * * The fallbacks are the draft case. A note with no id yet has nothing to hash, so it * publishes no properties and lands on the plain card surface — one colour change at * save time, rather than every draft sharing a fill and then changing anyway. */ .note-tint { background-color: var(--tint-light, #ffffff); } @media (prefers-color-scheme: dark) { .note-tint { background-color: var(--tint-dark, #171717); } } /* Motion is a feature, not a given. Anyone whose OS says "reduce motion" has told * us something about vestibular comfort or attention, and the answer is to arrive * instantly rather than to animate faster. * * Global and blunt on purpose: it catches every Tailwind `transition` utility * already scattered through the components, and it will catch the M7 motion work * without each new component having to remember. Near-zero rather than `none` so * `transitionend`/`animationend` listeners still fire and no state machine that * waits on them can hang. JS-driven motion can't be reached from CSS — that reads * the same preference through `useReducedMotion`. */ @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; scroll-behavior: auto !important; } } /* Controls that a card or row reveals on hover — the note toolbar, the drag grip, * per-item delete buttons. Marked with this class rather than expressed inline, * because the rule below is the whole reason they need naming. * * A finger cannot hover. On a device with no hover at all these are not "harder to * reach", they are unreachable — and for a note card the hover toolbar is the ONLY * way to pin, colour or archive. So where hovering is impossible they are simply * always present. `@media (hover: none)` asks the device directly, which is more * honest than inferring it from viewport width: a small window on a laptop still * hovers, and a large tablet still doesn't. * * Placed after the Tailwind directives so it wins on source order against the * `opacity-0` / `pointer-events-none` utilities it overrides, without !important. * The `group-hover:` reveal stays in the markup — the trigger differs per component * (named groups), only this fallback is shared. */ @media (hover: none) { .hover-reveal { opacity: 1; pointer-events: auto; } } /* A note card's own controls — the drag grip and the pin/colour/archive/trash set. * ONE element each, in TWO placements, because "always visible" and "floating over * the card's top corners" cannot both be true without the controls sitting on top of * the note's own words. * * That is exactly what a phone got. `.hover-reveal` above made the pills permanent * where nothing can hover (task 2697, correctly — they are the only way to pin or * archive), but they were still absolutely positioned, so they covered the title: * "thought sync tauri app" rendered as "ught sync tauri app" with the grip parked on * the first three characters. * * So the default — no hover — is a footer row IN FLOW, which cannot overlap anything * by construction. A pointer that can hover lifts them back out into the floating * pills they have always been, where they cost no vertical space and appear only on * approach. `display: contents` drops the flex row itself out of the way so each * child positions against the card. * * Keyed on hover rather than width, for the reason `.hover-reveal` already gives: a * narrow window on a laptop still hovers, and a wide tablet still doesn't. */ .note-actions { display: flex; align-items: center; justify-content: flex-end; gap: 0.125rem; margin-top: 0.5rem; } .note-grip { /* Pushes the action set to the far edge, so the two read as opposite ends of a * footer rather than as a clump. */ margin-right: auto; } .note-actions-set { position: relative; /* the colour popover anchors here in both placements */ } @media (hover: hover) { .note-actions { display: contents; } .note-grip { position: absolute; left: 0.375rem; top: 0.375rem; z-index: 10; margin-right: 0; } .note-actions-set { position: absolute; right: 0.375rem; top: 0.375rem; } } /* The per-card colour popover, anchored to whichever end of the card the action set * currently occupies: it opens DOWNWARD from a floating top-corner pill, and UPWARD * from a footer row, so in both cases it grows into the card rather than off it. */ .note-swatches { position: absolute; right: 0; bottom: 100%; margin-bottom: 0.375rem; z-index: 20; } @media (hover: hover) { .note-swatches { top: 100%; bottom: auto; margin-top: 0.375rem; margin-bottom: 0; } } /* Board motion (M7). Defined once here rather than three times in BoardView's * markup, because "how the board moves" is one idea even though the pinned, other * and non-board grids are three TransitionGroups. * * Vue's TransitionGroup does the FLIP itself: it measures each card before and * after the list changes and transitions the difference away. All we supply is the * curve. Deliberately fast and small — the brief is continuity, so a card should * read as having MOVED rather than as having performed. * * Leavers are NOT taken out of flow with `position: absolute`, which is the usual * TransitionGroup trick: this masonry is CSS multi-column, and an absolutely * positioned child escapes its column to the container's origin — it would fly * across the board on its way out. Keeping them in flow costs a small settle when * the element is finally removed, so the leave is the shortest of the three. * * Reduced motion needs no special case here: the global guard above already * collapses every duration, so this degrades to an instant cut. */ .board-move { transition: transform 220ms cubic-bezier(0.2, 0, 0, 1); } .board-enter-active { transition: opacity 180ms ease-out, transform 180ms cubic-bezier(0.2, 0, 0, 1); } .board-leave-active { transition: opacity 120ms ease-in; } .board-enter-from { opacity: 0; /* Barely a scale — enough to read as arriving, not as zooming. */ transform: scale(0.98); } .board-leave-to { opacity: 0; } /* Editor open/close (M7). The backdrop only fades; the panel scales from the point * the card occupied, which is what carries "this card became the editor" without * the distortion a true card→panel scale would put through the text. * * Enter is slower than leave on purpose: arriving wants to be noticed as a * connection, leaving just wants to be out of the way. LEAVE_MS in NoteEditor.vue * must stay in step with the leave duration here — it waits that long before * telling its host to unmount it. */ .editor-enter-active { transition: opacity 200ms ease-out; } .editor-leave-active { transition: opacity 140ms ease-in; } .editor-enter-from, .editor-leave-to { opacity: 0; } .editor-enter-active .editor-panel { transition: transform 200ms cubic-bezier(0.2, 0, 0, 1); } .editor-leave-active .editor-panel { transition: transform 140ms ease-in; } .editor-enter-from .editor-panel, .editor-leave-to .editor-panel { transform: scale(0.94); } /* Lens cross-fade (M7). Only fires between genuinely different surfaces; the * board's own lenses share a component and reflow in place instead. Out is quicker * than in so the gap between them stays short — `mode="out-in"` means the two * durations are additive, and anything slower starts to feel like waiting. */ .lens-enter-active { transition: opacity 150ms ease-out; } .lens-leave-active { transition: opacity 100ms ease-in; } .lens-enter-from, .lens-leave-to { opacity: 0; } @layer components { .icon-btn { @apply rounded-md p-1.5 text-neutral-500 transition hover:bg-black/5 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand dark:text-neutral-400 dark:hover:bg-white/10; } /* Finger-sized where a finger is the input. p-1.5 around an 18px icon lands near * 30px — comfortable for a cursor, too small for a thumb (Android asks 48dp, * WCAG 2.2 AAA asks 44px). Scoped to coarse pointers so desktop chrome doesn't * inflate; inline-flex because a bare min-height would leave the icon off-centre. */ @media (pointer: coarse) { .icon-btn { @apply inline-flex min-h-[2.75rem] min-w-[2.75rem] items-center justify-center; } } /* A small action sitting beside a chip on a card — reminder Done/snooze today. * Quiet at rest so a board full of reminders doesn't read as a wall of buttons, * and it earns contrast on hover/focus. */ .chip-btn { @apply rounded-full px-2 py-0.5 text-xs text-neutral-500 transition hover:bg-black/5 hover:text-neutral-800 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand dark:text-neutral-400 dark:hover:bg-white/10 dark:hover:text-neutral-100; } /* Same finger rule as .icon-btn: comfortable for a cursor is too small for a * thumb, and these are the primary action on a due note. */ @media (pointer: coarse) { .chip-btn { @apply inline-flex min-h-[2.25rem] items-center justify-center px-3; } } .nav-link { @apply flex items-center gap-2 rounded-lg px-3 py-2 font-medium text-neutral-600 transition hover:bg-neutral-200/60 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand dark:text-neutral-300 dark:hover:bg-neutral-800; } .nav-link-active { @apply bg-neutral-200 text-neutral-900 dark:bg-neutral-800 dark:text-neutral-100; } }