From e6612ea8ef06faac73191692874e04404cfc31f8 Mon Sep 17 00:00:00 2001 From: Paul Bakaus Date: Mon, 27 Jul 2026 17:10:38 -0700 Subject: [PATCH] Page the deck on its long axis, and never let decoration hide the cards Field-checked in a real browser, which surfaced three defects the DOM tests could not: the generic .media img display rule defeated [hidden] and floated an empty block over the shimmer and its sketching note; the deal animation left every card at opacity zero in an unfocused tab, because rAF throttling is real and decoration must never gate content; and the sketch poll's cache-busting query missed the anchored /img route, so a landed sketch kept shimmering forever. The grid is now a snap-scrolling deck: one row in a wide viewport, one column in a tall one, with edge arrows that appear only on overflow and page one card at a time. Co-Authored-By: Claude Fable 5 --- skill/scripts/serve-question.mjs | 64 ++++++++++++++++++++++++++++++-- tests/serve-question.test.mjs | 2 + 2 files changed, 62 insertions(+), 4 deletions(-) diff --git a/skill/scripts/serve-question.mjs b/skill/scripts/serve-question.mjs index e55fad065..7b2330ca0 100644 --- a/skill/scripts/serve-question.mjs +++ b/skill/scripts/serve-question.mjs @@ -411,8 +411,25 @@ 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; } - .grid { display: grid; gap: 1.6rem; grid-template-columns: repeat(auto-fit, minmax(min(23rem, 100%), 1fr)); width: 100%; } + .stage { width: 100%; display: flex; flex-direction: column; gap: 1.5rem; position: relative; } + /* 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::-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; } + .nav:hover { border-color: var(--ks-kinpaku-deep); color: var(--ks-kinpaku-pale); } + .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%); } + @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 > .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); } + } .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); } .card-inner { position: relative; height: 100%; transform-style: preserve-3d; transition: transform .7s cubic-bezier(.16, 1, .3, 1); } @@ -461,6 +478,9 @@ function page() { .media.sketching { position: relative; } .media.sketching .shimmer { position: absolute; inset: 0; } .media img.sketch { position: relative; z-index: 1; } + /* The generic .media img display:block would defeat [hidden] and float an + empty block over the shimmer; an unloaded sketch must truly not render. */ + .media img[hidden] { display: none; } /* The standing exit as a card: present with full anatomy, never dressed as a contender. Graphite instead of kinpaku, and it never takes the lead ring. */ .card.canon .face { border-color: var(--ks-rule); background: var(--ks-graphite); } @@ -510,6 +530,8 @@ function page() { ${payload.question ? `

${esc(payload.question)}

` : ''}
${cards}
+ +