diff --git a/app/globals.css b/app/globals.css index 0840c3e..33a4cc9 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1023,6 +1023,48 @@ html.theme-blueprint .line-through { text-decoration-thickness: 1.5px; } +/* Then a red-ink "APPROVED" stamp thuds onto the row a beat after the strike + -- a hard scale-down with a small overshoot, settling tilted a few degrees + like a QC stamp pressed onto the drawing. Scoped to the board's to-do rows: + the title button is a flex row, so the stamp rides at its right end (the + scheduled panel's completed rows are truncate buttons, where a flex item + would get clipped by the ellipsis). */ +html.theme-blueprint [data-group-card] .line-through::after { + content: "APPROVED"; + flex: none; + margin-left: 4px; + padding: 1px 6px; + border: 1.5px solid oklch(0.58 0.2 25 / 0.75); + box-shadow: inset 0 0 0 1px oklch(0.58 0.2 25 / 0.28); + border-radius: 2px; + color: oklch(0.58 0.2 25 / 0.9); + font-size: 10px; + line-height: 1.6; + letter-spacing: 0.08em; + opacity: 0.9; + transform: rotate(-6deg); + animation: bp-stamp 380ms cubic-bezier(0.3, 0.9, 0.4, 1) 160ms backwards; + mix-blend-mode: multiply; + pointer-events: none; +} +@keyframes bp-stamp { + 0% { + opacity: 0; + transform: rotate(-14deg) scale(2.1); + } + 60% { + opacity: 0.9; + transform: rotate(-5deg) scale(0.92); + } + 80% { + transform: rotate(-6.5deg) scale(1.06); + } + 100% { + opacity: 0.9; + transform: rotate(-6deg) scale(1); + } +} + /* To-do checkbox: a small technical box -- squarer corner, crisp stroke, and registration ticks that snap in on hover/focus like a plotter aligning to a point. (The box is the base-ui checkbox button; its stroke/fill already come @@ -1512,21 +1554,25 @@ html.theme-vaporwave body { oklch(0.955 0.055 190) 100% ); } -/* Faint scanlines rolling slowly, like a monitor left on in the morning. */ +/* Rolling scanlines: a fixed overlay over the whole app -- the "monitor" + sits over everything (cards, dialogs, toasts), the same move as cyberpunk's + CRT layer, so the texture is actually visible instead of hiding behind the + content. A 1px plum line every 4px, rolling downward at about 1px/s like a + monitor left on all morning. */ html.theme-vaporwave body::before { content: ""; position: fixed; inset: 0; - z-index: -1; + z-index: 999; pointer-events: none; background-image: repeating-linear-gradient( 0deg, - oklch(0.42 0.16 320 / 0.035) 0px, - oklch(0.42 0.16 320 / 0.035) 1px, + oklch(0.35 0.16 330 / 0.06) 0px, + oklch(0.35 0.16 330 / 0.06) 1px, transparent 1px, transparent 4px ); - animation: vw-roll 16s linear infinite; + animation: vw-roll 4s linear infinite; } @keyframes vw-roll { from { @@ -1537,6 +1583,260 @@ html.theme-vaporwave body::before { } } +/* The striped synth sun: the vaporwave icon, fixed in the sky behind the + content (it shows through the translucent lanes like light through glass). + Big and low, sitting on the outrun floor's horizon line. Solid across the + top, classic horizontal slats below, the slats drifting slowly downward + like the sun melting toward the horizon. Muted pastels close to the sky's + own lightness, so it reads as part of the sky rather than a sticker on it. */ +html.theme-vaporwave body::after { + content: ""; + position: fixed; + left: 50%; + bottom: 7vh; + width: 320px; + height: 320px; + z-index: -1; + pointer-events: none; + border-radius: 50%; + transform: translateX(-50%); + background: linear-gradient( + 180deg, + oklch(0.93 0.055 90) 0%, + oklch(0.91 0.075 48) 38%, + oklch(0.89 0.09 30) 68%, + oklch(0.86 0.085 350) 100% + ); + filter: drop-shadow(0 0 26px oklch(0.85 0.1 40 / 0.3)); + -webkit-mask-image: linear-gradient(180deg, #000 0 55%, rgba(0, 0, 0, 0) 55%), + repeating-linear-gradient(180deg, #000 0 10px, rgba(0, 0, 0, 0) 10px 18px); + mask-image: linear-gradient(180deg, #000 0 55%, rgba(0, 0, 0, 0) 55%), + repeating-linear-gradient(180deg, #000 0 10px, rgba(0, 0, 0, 0) 10px 18px); + animation: vw-sun 5s linear infinite; +} +@keyframes vw-sun { + from { + -webkit-mask-position: 0 0, 0 0; + mask-position: 0 0, 0 0; + } + to { + -webkit-mask-position: 0 0, 0 18px; + mask-position: 0 0, 0 18px; + } +} + +/* The outrun floor: a perspective grid across the bottom of the viewport, + behind the content (it shows through the translucent lanes). The horizontal + lines crawl toward the viewer while the verticals converge on the + vanishing point. Rendered by the .vw-floor element in app/layout.tsx, + visible only under this theme. */ +.vw-floor { + display: none; +} +html.theme-vaporwave .vw-floor { + display: block; + position: fixed; + left: 50%; + bottom: 0; + width: 200vw; + height: 75vh; + z-index: -1; + pointer-events: none; + transform: translateX(-50%) perspective(300px) rotateX(60deg); + transform-origin: 50% 100%; + opacity: 0.8; + background-image: + repeating-linear-gradient(90deg, oklch(0.62 0.25 350 / 0.5) 0 2px, transparent 2px 90px), + repeating-linear-gradient(0deg, oklch(0.62 0.25 350 / 0.5) 0 2px, transparent 2px 44px); + -webkit-mask-image: linear-gradient(180deg, transparent 0%, #000 35%); + mask-image: linear-gradient(180deg, transparent 0%, #000 35%); + animation: vw-floor 2.2s linear infinite; +} +@keyframes vw-floor { + from { + background-position: 0 0, 0 0; + } + to { + background-position: 0 0, 0 44px; + } +} + +/* Palm silhouettes: a couple of minimal palms rooted at either side of the + horizon -- the classic vaporwave framing, kept faint so they read as + scenery, not stickers. Drawn as stroked arcs in the .vw-palm SVGs in + app/layout.tsx; they paint above the floor grid (DOM order) but stay + behind the content. Visible only under this theme. */ +.vw-palm { + display: none; +} +.vw-palm-defs { + position: absolute; + width: 0; + height: 0; + overflow: hidden; +} +html.theme-vaporwave .vw-palm { + display: block; + position: fixed; + bottom: 1vh; + z-index: -1; + pointer-events: none; + color: oklch(0.47 0.12 325); +} +html.theme-vaporwave .vw-palm--left-main { + left: 18vw; + width: 150px; + height: 187px; + opacity: 0.5; +} +html.theme-vaporwave .vw-palm--left-small { + left: 26vw; + width: 90px; + height: 112px; + opacity: 0.3; +} +html.theme-vaporwave .vw-palm--right-main { + right: 2vw; + width: 150px; + height: 187px; + opacity: 0.5; + transform: scaleX(-1); +} +html.theme-vaporwave .vw-palm--right-small { + right: 10vw; + width: 90px; + height: 112px; + opacity: 0.3; + transform: scaleX(-1); +} + +/* VHS hiccup: every so often a card drops out of the signal for half a + second -- a 1-2px jitter, a red/cyan split on the title, and a tracking + band sweeping down the panel, all inside one short window (the first 5% + of each cycle). Cards run three alternating phases with different periods + and offsets, so the board flickers organically instead of in lockstep. + The :not(.opacity-50) guard matters: while dnd-kit drives a drag it sets + an inline `transform` on the card, and a running animation would override + that inline value (animations sit above inline styles in the cascade) -- + the card stops following the pointer. opacity-50 is exactly the class the + card gets while dragging, so the hiccup stands down for the duration. + (overflow: hidden keeps the tracking band clipped to the card while it + sweeps; the hover lift uses the separate `translate` property, so the + jitter doesn't fight it.) */ +html.theme-vaporwave [data-group-card]:not(.opacity-50) { + overflow: hidden; + animation: vw-card-glitch 10s linear infinite; +} +html.theme-vaporwave [data-group-card]:not(.opacity-50) > div:first-child h3 { + animation: vw-title-glitch 10s linear infinite; +} +html.theme-vaporwave [data-group-card]:not(.opacity-50)::after { + content: ""; + position: absolute; + left: 0; + right: 0; + top: -12%; + height: 12px; + opacity: 0; + pointer-events: none; + background: + repeating-linear-gradient(0deg, oklch(0.45 0.15 330 / 0.28) 0 1px, transparent 1px 3px), + linear-gradient( + 180deg, + transparent 0%, + oklch(0.72 0.2 350 / 0.22) 20%, + oklch(0.99 0.01 220 / 0.4) 50%, + oklch(0.78 0.15 200 / 0.22) 80%, + transparent 100% + ); + animation: vw-tracking 10s linear infinite; +} +/* Phase B: a longer period and an offset start, so neighbors don't hiccup + together. (Static styles above are the idle state -- before a card's + first delay elapses, and under reduced motion, everything just sits still.) */ +html.theme-vaporwave [data-category-lane] .overflow-y-auto > div:nth-child(3n + 2) [data-group-card]:not(.opacity-50), +html.theme-vaporwave [data-category-lane] .overflow-y-auto > div:nth-child(3n + 2) [data-group-card]:not(.opacity-50) > div:first-child h3, +html.theme-vaporwave [data-category-lane] .overflow-y-auto > div:nth-child(3n + 2) [data-group-card]:not(.opacity-50)::after { + animation-duration: 13s; + animation-delay: 3s; +} +/* Phase C: the longest period, offset furthest. */ +html.theme-vaporwave [data-category-lane] .overflow-y-auto > div:nth-child(3n) [data-group-card]:not(.opacity-50), +html.theme-vaporwave [data-category-lane] .overflow-y-auto > div:nth-child(3n) [data-group-card]:not(.opacity-50) > div:first-child h3, +html.theme-vaporwave [data-category-lane] .overflow-y-auto > div:nth-child(3n) [data-group-card]:not(.opacity-50)::after { + animation-duration: 17s; + animation-delay: 7s; +} +@keyframes vw-card-glitch { + 0%, + 5%, + 100% { + transform: translateX(0); + } + 0.8% { + transform: translateX(-1.5px); + } + 1.6% { + transform: translateX(-1.5px); + } + 2.4% { + transform: translateX(1px); + } + 3.2% { + transform: translateX(1px); + } + 4% { + transform: translateX(-0.75px); + } + 4.6% { + transform: translateX(0.5px); + } +} +@keyframes vw-title-glitch { + 0%, + 5%, + 100% { + text-shadow: none; + } + 0.8% { + text-shadow: + 1.5px 0 oklch(0.68 0.2 350 / 0.55), + -1.5px 0 oklch(0.85 0.15 200 / 0.55); + } + 1.6% { + text-shadow: + -1.5px 0 oklch(0.68 0.2 350 / 0.55), + 1.5px 0 oklch(0.85 0.15 200 / 0.55); + } + 2.4% { + text-shadow: + 1px 0 oklch(0.68 0.2 350 / 0.55), + -1px 0 oklch(0.85 0.15 200 / 0.55); + } + 3.2% { + text-shadow: + -0.5px 0 oklch(0.68 0.2 350 / 0.55), + 0.5px 0 oklch(0.85 0.15 200 / 0.55); + } +} +@keyframes vw-tracking { + 0% { + top: -12%; + opacity: 0; + } + 0.8% { + opacity: 0.85; + } + 4.8% { + opacity: 0.85; + } + 5%, + 100% { + top: 112%; + opacity: 0; + } +} + /* Primary blooms with a soft magenta glow; focus glows electric cyan. */ html.theme-vaporwave .bg-primary { box-shadow: 0 4px 18px oklch(0.62 0.25 350 / 0.35); @@ -1731,18 +2031,58 @@ html.theme-notebook body { background-image: linear-gradient(180deg, oklch(0.88 0.024 82), oklch(0.84 0.028 80)); } -/* Ruled paper on the white card surfaces: blue rules + a red margin line on - the left, the classic notebook page. */ -html.theme-notebook [data-slot="card"], -html.theme-notebook [data-slot="dialog-content"] { +/* The lane is the notebook page itself: white paper over the warm desk with + the classic ruling -- blue lines + a red margin rule running down the left + (inside the lane's own padding, so the post-its never cover it). Group + cards sit on top as post-its (below), so the ruling shows around them. */ +html.theme-notebook [data-category-lane] { + background-color: oklch(0.978 0.008 90); background-image: - linear-gradient(90deg, transparent 24px, oklch(0.6 0.19 25 / 0.4) 24px 26px, transparent 26px), + linear-gradient(90deg, transparent 6px, oklch(0.6 0.19 25 / 0.4) 6px 8px, transparent 8px), repeating-linear-gradient(180deg, transparent 0 27px, oklch(0.68 0.09 250 / 0.4) 27px 28px); + box-shadow: + 0 1px 2px rgb(96 74 40 / 0.14), + 0 10px 24px -10px rgb(96 74 40 / 0.3); } -/* Group cards keep their own pastel tint (they read as index cards) but carry - a faint red margin line so they stay in the stationery family. */ + +/* Group cards are post-its stuck onto the page: a slightly darker adhesive + band along the top, and a soft lift shadow that runs heavier to one side, + like a sheet curling off the paper. (:not(.ring-2) keeps the drag + drop-target ring -- a box-shadow utility -- from being overpainted.) */ html.theme-notebook [data-group-card] { - background-image: linear-gradient(90deg, transparent 20px, oklch(0.6 0.19 25 / 0.3) 20px 22px, transparent 22px); + border-width: 2px; + background-image: linear-gradient(180deg, rgb(96 74 40 / 0.07) 0 15%, transparent 15%); +} +html.theme-notebook [data-group-card]:not(.ring-2) { + box-shadow: + 1px 1px 2px rgb(96 74 40 / 0.12), + 6px 9px 16px -6px rgb(96 74 40 / 0.32); +} +html.theme-notebook [data-group-card]:not(.ring-2):hover { + box-shadow: + 1px 2px 3px rgb(96 74 40 / 0.14), + 8px 12px 20px -6px rgb(96 74 40 / 0.4); +} +/* A post-it is never stuck perfectly straight: alternate a hair of tilt down + the column so a boardful don't all lean the same way. (While a card is + being dragged, dnd-kit's inline transform overrides this -- the post-it + straightens out in your hand.) */ +html.theme-notebook [data-category-lane] .overflow-y-auto > div:nth-child(2n) [data-group-card] { + transform: rotate(0.4deg); +} +html.theme-notebook [data-category-lane] .overflow-y-auto > div:nth-child(2n + 1) [data-group-card] { + transform: rotate(-0.4deg); +} + +/* Hand-cut paper edges: the classic uneven border-radius so pages, cards, and + dialogs look drawn by hand instead of machine-rounded. */ +html.theme-notebook [data-category-lane] { + border-radius: 170px 10px 150px 10px / 10px 150px 10px 170px; +} +html.theme-notebook [data-slot="card"], +html.theme-notebook [data-slot="dialog-content"], +html.theme-notebook [data-group-card] { + border-radius: 255px 15px 225px 15px / 15px 225px 15px 255px; } /* Blue-ink focus. */ @@ -1783,6 +2123,34 @@ html.theme-notebook .line-through { background-size: 100% 60%; } } +/* Then a red pen draws a quick check at the end of the line -- grading the + homework. (Scoped to the board's to-do rows: the title button is a flex + row, so the check rides at its right end. It starts fully clipped and + reveals left-to-right, timed to land as the highlighter sweep finishes.) */ +html.theme-notebook [data-group-card] .line-through::after { + content: "✓"; + flex: none; + margin-left: 2px; + color: oklch(0.55 0.22 27 / 0.95); + font-size: 1.15em; + line-height: 1; + transform: translateY(-1px); + clip-path: inset(-25% 100% -25% 0); + animation: nb-check 300ms ease-out 300ms forwards; + pointer-events: none; +} +@keyframes nb-check { + to { + clip-path: inset(-25% -5% -25% 0); + } +} +/* The global reduced-motion guard kills the reveal animation -- fall back to + the check simply sitting there, already drawn. */ +@media (prefers-reduced-motion: reduce) { + html.theme-notebook [data-group-card] .line-through::after { + clip-path: none; + } +} @layer base { * { diff --git a/app/layout.tsx b/app/layout.tsx index 80035b2..12cbf72 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -116,6 +116,36 @@ export default function RootLayout({ children }: LayoutProps<"/">) { __html: `!(function(){try{var r=document.documentElement,c=['theme-default','theme-sunset','theme-meadow','theme-honey','theme-rose','theme-lavender','theme-slate','theme-blueprint','theme-vaporwave','theme-notebook','dark','ocean','theme-pine','theme-plum','theme-midnight','theme-ember','theme-rosewood','theme-cyberpunk','theme-starfield','light'];for(var i=0;i=0?'dark':'light';}}catch(e){}})();`, }} /> + {/* Vaporwave scenery (see app/globals.css): the outrun floor grid, + plus palm silhouettes at either side of the horizon. All fixed + behind the content, hidden by default; the theme class on + switches them on. */} +
+ + + + + + + + + + + + + + + + + + + + + + + + + {children} diff --git a/components/board/category-lane.tsx b/components/board/category-lane.tsx index cfdd07c..fd2b2f6 100644 --- a/components/board/category-lane.tsx +++ b/components/board/category-lane.tsx @@ -97,6 +97,7 @@ export function CategoryLane({ category }: { category: CategoryDTO }) { <>