From 58a2d3dccdae5cc87feed8371f99b5e4f7551140 Mon Sep 17 00:00:00 2001 From: Paul Bakaus Date: Mon, 27 Jul 2026 17:14:59 -0700 Subject: [PATCH] Bleed the deck to the viewport, fade the fuller side, let the glance take over Three field notes from a live review. The deck now escapes the content column and runs edge to edge, so a cut-off card sits at the screen edge where it reads as more cards instead of at an invisible container edge where it reads as a bug; the first card still aligns with the column via scroll padding. Whichever side hides more content wears a fade, and a hard edge means the end. The vertical pager grows from a bare chevron into labeled Back and More pills, because in a column deck it is the primary way forward. And hovering the inspiration thumb now takes over the whole media region instead of a timid zoom; the sketch is the promise, the inspiration is a glance, and the glance must cost nothing. Co-Authored-By: Claude Fable 5 --- skill/scripts/serve-question.mjs | 64 +++++++++++++++++++++++--------- 1 file changed, 46 insertions(+), 18 deletions(-) diff --git a/skill/scripts/serve-question.mjs b/skill/scripts/serve-question.mjs index 7b2330ca0..f93e37ec6 100644 --- a/skill/scripts/serve-question.mjs +++ b/skill/scripts/serve-question.mjs @@ -394,7 +394,7 @@ function page() { --ks-mono: "SFMono-Regular", "Roboto Mono", "JetBrains Mono", Consolas, monospace; } * { box-sizing: border-box; margin: 0; } - body { background: var(--ks-lacquer); color: var(--ks-text); font: 15px/1.55 var(--ks-font); padding: 1.8rem clamp(1rem, 5vw, 4rem) 2rem; min-height: 100dvh; display: flex; flex-direction: column; } + body { background: var(--ks-lacquer); color: var(--ks-text); font: 15px/1.55 var(--ks-font); padding: 1.8rem clamp(1rem, 5vw, 4rem) 2rem; min-height: 100dvh; display: flex; flex-direction: column; overflow-x: clip; } #ambient { position: fixed; inset: -40px; z-index: 0; background-size: cover; background-position: center; filter: blur(34px) saturate(1.05); opacity: 0; transition: opacity .55s ease, background-image .2s; pointer-events: none; } #scrim { position: fixed; inset: 0; z-index: 0; background: linear-gradient(180deg, oklch(7% 0.006 95 / 0.62), oklch(7% 0.006 95 / 0.78)); pointer-events: none; } header, main, footer { position: relative; z-index: 1; } @@ -411,10 +411,14 @@ function page() { h1 { font-family: var(--ks-font-display); font-weight: 100; font-size: clamp(2.6rem, 5vw, 4.2rem); letter-spacing: -0.01em; line-height: 1.02; color: var(--ks-champagne); } .question { color: var(--ks-text-muted); margin-top: .7rem; max-width: 52rem; } main { flex: 1; display: flex; align-items: center; width: 100%; max-width: 90rem; margin: 0 auto; } - .stage { width: 100%; display: flex; flex-direction: column; gap: 1.5rem; position: relative; } + .stage { width: 100%; display: flex; flex-direction: column; gap: 1.5rem; } + /* The deck bleeds to the viewport edges while the first card aligns with the + content column; a carousel cut off at an invisible container edge reads as + a rendering bug, but one cut off at the screen edge reads as more cards. */ + .deck-shell { position: relative; width: 100vw; margin-left: calc(50% - 50vw); } /* One row in a wide viewport, one column in a tall one; the deck scrolls on its axis with snap points and the arrows page it card by card. */ - .grid { display: flex; gap: 1.6rem; width: 100%; overflow-x: auto; overflow-y: hidden; scroll-snap-type: x mandatory; scrollbar-width: none; padding: 6px 2px; align-items: stretch; } + .grid { --deck-inset: max(clamp(1rem, 5vw, 4rem), calc((100vw - 90rem) / 2)); display: flex; gap: 1.6rem; width: 100%; overflow-x: auto; overflow-y: hidden; scroll-snap-type: x mandatory; scrollbar-width: none; padding: 6px var(--deck-inset); scroll-padding-inline: var(--deck-inset); align-items: stretch; } .grid::-webkit-scrollbar { display: none; } .grid > .card { flex: 0 0 clamp(20rem, 27vw, 27rem); scroll-snap-align: center; } .nav { position: absolute; z-index: 6; width: 42px; height: 42px; display: flex; align-items: center; justify-content: center; border-radius: 50%; background: oklch(7% 0.006 95 / 0.78); border: 1px solid var(--ks-rule); color: var(--ks-kinpaku); cursor: pointer; backdrop-filter: blur(6px); transition: border-color .2s, color .2s, opacity .2s; } @@ -422,13 +426,27 @@ function page() { .nav[disabled] { opacity: .25; cursor: default; } .nav[hidden] { display: none; } .nav svg { width: 16px; height: 16px; } - .nav.prev { left: -10px; top: 50%; transform: translateY(-50%); } - .nav.next { right: -10px; top: 50%; transform: translateY(-50%); } + .nav.prev { left: 14px; top: 50%; transform: translateY(-50%); } + .nav.next { right: 14px; top: 50%; transform: translateY(-50%); } + /* A side that hides more cards fades out; a hard edge means the end. */ + .fade { position: absolute; z-index: 5; pointer-events: none; opacity: 0; transition: opacity .3s ease; } + .fade-prev { left: 0; top: 0; bottom: 0; width: 88px; background: linear-gradient(90deg, var(--ks-lacquer), transparent); } + .fade-next { right: 0; top: 0; bottom: 0; width: 88px; background: linear-gradient(270deg, var(--ks-lacquer), transparent); } + .deck-shell.can-prev .fade-prev { opacity: 1; } + .deck-shell.can-next .fade-next { opacity: 1; } @media (max-aspect-ratio: 1/1) { - .grid { flex-direction: column; overflow-x: hidden; overflow-y: auto; scroll-snap-type: y mandatory; max-height: min(68dvh, 44rem); } + .grid { flex-direction: column; overflow-x: hidden; overflow-y: auto; scroll-snap-type: y mandatory; max-height: min(68dvh, 44rem); scroll-padding-block: 6px; } .grid > .card { flex: 0 0 auto; } - .nav.prev { left: 50%; top: -8px; transform: translate(-50%, 0) rotate(90deg); } - .nav.next { right: auto; left: 50%; top: auto; bottom: -8px; transform: translate(-50%, 0) rotate(90deg); } + /* In the vertical deck the pager is the primary way forward, so it grows + into a labeled pill instead of a bare chevron nobody notices. */ + .nav { width: auto; height: 38px; border-radius: 19px; padding: 0 16px; gap: 8px; border-color: var(--ks-kinpaku-deep); background: oklch(7% 0.006 95 / 0.88); font-family: var(--ks-mono); font-size: .62rem; letter-spacing: .2em; text-transform: uppercase; } + .nav svg { transform: rotate(90deg); } + .nav.prev::after { content: "Back"; } + .nav.next::after { content: "More"; } + .nav.prev { left: 50%; top: 6px; transform: translate(-50%, 0); } + .nav.next { right: auto; left: 50%; top: auto; bottom: 6px; transform: translate(-50%, 0); } + .fade-prev { top: 0; left: 0; right: 0; bottom: auto; width: auto; height: 72px; background: linear-gradient(180deg, var(--ks-lacquer), transparent); } + .fade-next { top: auto; left: 0; right: 0; bottom: 0; width: auto; height: 72px; background: linear-gradient(0deg, var(--ks-lacquer), transparent); } } .card { position: relative; perspective: 1400px; transform: rotate(var(--fan, 0deg)); transition: transform .25s cubic-bezier(.16, 1, .3, 1); } .card:hover { transform: rotate(0deg) translateY(-4px); } @@ -470,10 +488,12 @@ function page() { .fact-label { display: inline-block; font-family: var(--ks-mono); font-size: .6rem; letter-spacing: .18em; text-transform: uppercase; color: var(--ks-text-faint); margin-right: .55em; transform: translateY(-1px); } /* Inspiration rides picture-in-picture: the catalog world explains where the direction comes from without promising what the build will look like. */ - .pip { position: absolute; z-index: 2; left: 10px; bottom: 10px; margin: 0; width: 74px; border: 1px solid var(--ks-rule); border-radius: 6px; overflow: hidden; background: var(--ks-lacquer); cursor: zoom-in; transform-origin: bottom left; transition: transform .3s cubic-bezier(.16,1,.3,1), box-shadow .3s; box-shadow: 0 6px 18px oklch(0% 0 0 / 0.45); } - .pip img { display: block; width: 100%; aspect-ratio: 16/10; object-fit: cover; } - .pip figcaption { font-family: var(--ks-mono); font-size: .5rem; letter-spacing: .2em; text-transform: uppercase; color: var(--ks-text-faint); text-align: center; padding: 3px 0 4px; } - .pip:hover { transform: scale(2.6); box-shadow: 0 14px 40px oklch(0% 0 0 / 0.6); z-index: 3; } + /* Hovering the inspiration takes over the whole media region; the sketch is + the promise, the inspiration is a glance, so the glance must cost nothing. */ + .pip { position: absolute; z-index: 2; left: 10px; bottom: 10px; margin: 0; width: 84px; height: 64px; border: 1px solid var(--ks-rule); border-radius: 6px; overflow: hidden; background: var(--ks-lacquer); cursor: zoom-in; transition: left .35s cubic-bezier(.16,1,.3,1), bottom .35s cubic-bezier(.16,1,.3,1), width .35s cubic-bezier(.16,1,.3,1), height .35s cubic-bezier(.16,1,.3,1), border-radius .35s ease; box-shadow: 0 6px 18px oklch(0% 0 0 / 0.45); } + .pip img { display: block; width: 100%; height: 100%; object-fit: cover; } + .pip figcaption { position: absolute; left: 0; right: 0; bottom: 0; font-family: var(--ks-mono); font-size: .5rem; letter-spacing: .2em; text-transform: uppercase; color: var(--ks-text); text-align: center; padding: 3px 0 4px; background: oklch(7% 0.006 95 / 0.72); backdrop-filter: blur(3px); } + .pip:hover { left: 0; bottom: 0; width: 100%; height: 100%; border-radius: 0; z-index: 3; } .sketch-note { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; font-family: var(--ks-mono); font-size: .66rem; letter-spacing: .22em; text-transform: uppercase; color: var(--ks-text-faint); } .media.sketching { position: relative; } .media.sketching .shimmer { position: absolute; inset: 0; } @@ -529,9 +549,13 @@ function page() {

${esc(payload.title || 'Choose a direction')}

${payload.question ? `

${esc(payload.question)}

` : ''} -
${cards}
- - +
+
${cards}
+ + + + +