diff --git a/.gitignore b/.gitignore index 698dd6b..6358afc 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,6 @@ packs/*/LOCK # macOS .DS_Store + +# Impeccable design-hook scratch; the config beside it records real decisions. +.impeccable/hook.cache.json diff --git a/.impeccable/config.json b/.impeccable/config.json new file mode 100644 index 0000000..717265f --- /dev/null +++ b/.impeccable/config.json @@ -0,0 +1,17 @@ +{ + "detector": { + "ignoreRules": [], + "ignoreFiles": [], + "ignoreValues": [ + { + "rule": "broken-image", + "value": "*", + "files": [ + "styles/ringbrp.css" + ], + "createdAt": "2026-08-30T18:38:10.587Z", + "reason": "Claude: stylesheet, not markup — the only mentions are prose in comments documenting Foundry's core pause DOM (
holding an ) and the #pause img selector. No image tag can exist in a .css file." + } + ] + } +} diff --git a/styles/ringbrp.css b/styles/ringbrp.css index bb43ad6..f06f130 100644 --- a/styles/ringbrp.css +++ b/styles/ringbrp.css @@ -1686,107 +1686,207 @@ .rb-dead { border-left: 3px solid var(--rb-white); padding-left: 8px; } /* ============================================================ - THE PAUSE — a uniselector, stepping + THE PAUSE — containment hold ============================================================ Foundry's pause is
holding an and a
, and core hides it with `#pause { display: none }` / shows it with `#pause.paused { display: - flex }`. The first version of this styled the bare #pause with display:flex and so - overrode the hide — the panel never went away when the game unpaused. EVERY rule here - that could affect visibility or layout is therefore scoped to `.paused`, and `display` - is not set anywhere. + flex }`. An early version styled the bare #pause with display:flex and so overrode the + hide — the panel never went away when the game unpaused, and GAME PAUSED sat over the + table for the rest of the session. EVERY rule here that could affect visibility or + layout is therefore scoped to `.paused`, and `display` is not set anywhere. There is a + guard in check-behaviour.mjs that enforces exactly this. - Core also already centres it: a full-width band, 180px tall, at top calc(50vh - 100px), - column flex, centred both ways, pointer-events none. That layout is correct and is - left alone — only its size and its looks change here. The first version repositioned - it with left/bottom and pushed it off centre for the trouble. + Core also already centres it: a full-width band, column flex, centred both ways, + pointer-events none. That layout is correct and is left alone — only its size and its + looks change here. An earlier version repositioned it with left/bottom and pushed it + off centre for the trouble. - A uniselector is the stepping switch inside a telephone exchange: an arm that clicks - round one contact at a time, hunting for a free line. The department's whole business - is a machine that says where things belong, and OPEN DAY is set inside one. Eleven - contacts, because eleven minutes is the interval this setting keeps returning to. + WHAT IT IS. The department is holding a moment still, and this is the instrument doing + the holding. It acquires, it locks, it reports the lock is good, it lets go. - It STEPS rather than sweeps — a contact every 700ms. A continuous spin reads as a - loading throbber; a discrete click reads as a machine working through a list. + There are only four paintable surfaces on this element — the figure's ::before and + ::after, the , and the
— so each is given exactly one job and no + surface does two things: + + ::before the FIELD. Grid and containment ring. Static; it is the screen. + img the LOCK. Four corner brackets that close, snap, hold, release. + ::after the SCAN. A line reading down the field, three passes per cycle. + figcaption the READOUT. Lifts when the lock takes, drops when it lets go. + + The keeps its box and loses its pixels: object-position walks the raster out of + frame, which leaves a paintable element without adding a wrapper to markup we do not + own. + + THE CYCLE, 4.8s: + + 0–22% acquire brackets wide and dim, closing + 22% LOCK they snap home; the readout comes up in the same frame + 22–70% hold the moment is held + 70–80% release brackets open and fade, readout drops + 80–100% rest dark, before it goes again + + The scan is cycle/3 rather than some free-running duration, so it divides the cycle + exactly and its third pass always lands on the lock instead of wandering across it. + Every animation here reads --rb-pause-cycle: change that one value and the whole + instrument re-times together, and nothing can drift out of phase with anything else. ============================================================ */ -/* Taller than core's 180px, and moved up by exactly half the extra so it stays centred. */ #pause.paused { - height: 300px; - top: calc(50vh - 150px); - gap: 28px; + /* One cycle and one field size, read by every layer below. */ + --rb-pause-cycle: 4.8s; + --rb-pause-field: 190px; + --rb-pause-gap: 26px; + --rb-pause-height: 300px; + /* The readout is pinned to exactly this so the column's height is known rather than + dependent on whether Michroma has loaded yet — see the figcaption rule. */ + --rb-pause-readout: 25.2px; /* 21px x 1.2 line-height */ + /* ::before and ::after are absolutely positioned and cannot see where flex put the + lock, so the offset is DERIVED from the column rather than measured and pasted in: + half the space left over once the field, the gap and the readout are accounted for. + Change the field size or the gap and this follows on its own. */ + --rb-pause-top: calc((var(--rb-pause-height) + - var(--rb-pause-field) + - var(--rb-pause-gap) + - var(--rb-pause-readout)) / 2); + + height: var(--rb-pause-height); + top: calc(50vh - (var(--rb-pause-height) / 2)); + gap: var(--rb-pause-gap); background: linear-gradient(to right, transparent 0%, - color-mix(in srgb, var(--rb-void) 82%, transparent) 38%, - color-mix(in srgb, var(--rb-void) 82%, transparent) 62%, + color-mix(in srgb, var(--rb-void) 86%, transparent) 34%, + color-mix(in srgb, var(--rb-void) 86%, transparent) 66%, transparent 100%); } -/* the dial: eleven contacts, standing still, behind the arm */ +/* THE FIELD — the screen the lock is drawn on. Ring inside a grid, both hairline, the + whole thing masked to fade out well before its own square edge so it reads as a + field of view rather than a box sitting on the canvas. */ #pause.paused::before { content: ""; position: absolute; - top: 24px; left: 50%; margin-left: -85px; - width: 170px; height: 170px; - border-radius: 50%; + top: var(--rb-pause-top); + left: 50%; + margin-left: calc(var(--rb-pause-field) / -2); + width: var(--rb-pause-field); + height: var(--rb-pause-field); background: - repeating-conic-gradient(from -90deg, - var(--rb-line) 0deg 1.6deg, transparent 1.6deg 32.727deg); - -webkit-mask: radial-gradient(circle, transparent 0 52px, #000 54px); - mask: radial-gradient(circle, transparent 0 52px, #000 54px); - opacity: .9; + radial-gradient(circle, transparent 0 71px, var(--rb-line) 71px 72px, transparent 72px), + repeating-linear-gradient(to right, var(--rb-line) 0 1px, transparent 1px 19px), + repeating-linear-gradient(to bottom, var(--rb-line) 0 1px, transparent 1px 19px); + opacity: .3; + -webkit-mask: radial-gradient(circle, #000 32%, transparent 74%); + mask: radial-gradient(circle, #000 32%, transparent 74%); } -/* the arm. The keeps its box and loses its pixels: object-position walks the - raster out of frame, which leaves a paintable element without adding a wrapper. */ +/* THE LOCK — four corner brackets, eight solid background layers (two arms a corner). + `linear-gradient(colour 0 0)` is the shorthand for a flat colour layer; each is sized + to an arm and pinned to its corner, so the set scales as one piece. */ #pause.paused img { - width: 170px; height: 170px; + --rb-lock-arm: 30px; + --rb-lock-line: 2px; + width: var(--rb-pause-field); + height: var(--rb-pause-field); object-fit: none; object-position: -9999px -9999px; border: none; box-shadow: none; filter: none; - /* the arm, and behind it the contact it has just left — without the trailing sector - a 700ms step is easy to miss, and the stepping is the whole idea */ - background: conic-gradient(from -91deg, - var(--rb-yellow) 0deg 3.5deg, - var(--rb-yellow-dim) 3.5deg 28deg, - transparent 28deg 360deg); - -webkit-mask: radial-gradient(circle, #000 0 78px, transparent 80px); - mask: radial-gradient(circle, #000 0 78px, transparent 80px); - animation: rb-uniselector 7.7s steps(11, end) infinite; + background: + linear-gradient(var(--rb-yellow) 0 0) 0 0 / var(--rb-lock-line) var(--rb-lock-arm) no-repeat, + linear-gradient(var(--rb-yellow) 0 0) 0 0 / var(--rb-lock-arm) var(--rb-lock-line) no-repeat, + linear-gradient(var(--rb-yellow) 0 0) 100% 0 / var(--rb-lock-line) var(--rb-lock-arm) no-repeat, + linear-gradient(var(--rb-yellow) 0 0) 100% 0 / var(--rb-lock-arm) var(--rb-lock-line) no-repeat, + linear-gradient(var(--rb-yellow) 0 0) 0 100% / var(--rb-lock-line) var(--rb-lock-arm) no-repeat, + linear-gradient(var(--rb-yellow) 0 0) 0 100% / var(--rb-lock-arm) var(--rb-lock-line) no-repeat, + linear-gradient(var(--rb-yellow) 0 0) 100% 100% / var(--rb-lock-line) var(--rb-lock-arm) no-repeat, + linear-gradient(var(--rb-yellow) 0 0) 100% 100% / var(--rb-lock-arm) var(--rb-lock-line) no-repeat; +} +/* Core adds fa-spin to that img; ours must be the only animation running on it. One + declaration for both selectors so the two can never be given different timings. */ +#pause.paused img, +#pause.paused img.fa-spin { + animation: rb-pause-lock var(--rb-pause-cycle) linear infinite; } -/* core adds fa-spin to that img; ours must be the only rotation running */ -#pause.paused img.fa-spin { animation: rb-uniselector 7.7s steps(11, end) infinite; } -/* the hub, and the amber that says the record is being written */ +/* THE SCAN — one line reading down the field. Bright in the middle and gone at the ends, + so it never collides with the brackets it passes. */ #pause.paused::after { content: ""; position: absolute; - top: 101px; left: 50%; margin-left: -8px; - width: 16px; height: 16px; - border-radius: 50%; - background: var(--rb-yellow); - animation: rb-pause-pip 2.6s ease-in-out infinite; + top: var(--rb-pause-top); + left: 50%; + margin-left: calc(var(--rb-pause-field) / -2); + width: var(--rb-pause-field); + height: 2px; + background: linear-gradient(to right, + transparent 0%, + var(--rb-yellow-dim) 20%, + var(--rb-yellow) 50%, + var(--rb-yellow-dim) 80%, + transparent 100%); + animation: rb-pause-scan calc(var(--rb-pause-cycle) / 3) linear infinite; } +/* THE READOUT */ #pause.paused figcaption { font-family: var(--rb-display); - font-size: 24px; - letter-spacing: .3em; + font-size: 21px; + letter-spacing: .4em; + /* letter-spacing adds its gap after the last glyph too; pull the line back by one to + keep the word optically centred under the field */ + text-indent: .4em; text-transform: uppercase; color: var(--rb-yellow); line-height: 1.2; + /* pinned, so --rb-pause-top stays correct even before the display face loads */ + height: var(--rb-pause-readout); text-shadow: 0 2px 18px var(--rb-void); + animation: rb-pause-readout var(--rb-pause-cycle) linear infinite; } -@keyframes rb-uniselector { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } -@keyframes rb-pause-pip { 0%, 100% { opacity: .25; } 50% { opacity: 1; } } +/* Acquire on an S-curve that spends its time travelling and arrives late, so the closing + fills the acquire phase instead of finishing early and leaving the brackets sitting + still waiting for the readout. Hold flat, then ease-in out of the hold: a release + accelerates, it does not glide. */ +@keyframes rb-pause-lock { + 0% { transform: scale(1.14); opacity: .3; animation-timing-function: cubic-bezier(.6,.02,.3,1); } + 22% { transform: scale(1); opacity: 1; } + 70% { transform: scale(1); opacity: 1; animation-timing-function: ease-in; } + 80% { transform: scale(1.06); opacity: .16; } + 100% { transform: scale(1.14); opacity: .3; } +} -/* A stepping dial in the corner of the eye is exactly what this preference is about. - Keep the object, stop the movement — including core's own pulse on the whole figure. */ +/* Fades in and out at the extremes so the line is never cut off mid-stroke at the edge + of the field it is reading. */ +@keyframes rb-pause-scan { + 0% { transform: translateY(0); opacity: 0; } + 14% { opacity: .9; } + 86% { opacity: .9; } + 100% { transform: translateY(calc(var(--rb-pause-field) - 2px)); opacity: 0; } +} + +/* Welded to the lock: up at 22%, down at 70%, and the 1% ramps are ~48ms, which reads as + a readout switching rather than a caption fading. */ +@keyframes rb-pause-readout { + 0%, 21% { opacity: .34; } + 22%, 69% { opacity: 1; } + 70%, 100% { opacity: .34; } +} + +/* An instrument acquiring in the corner of the eye is exactly what this preference is + about. Keep the object, stop the movement — including core's own pulse on the whole + figure. Rest in the LOCKED state, which is the one frame of the cycle that reads as a + complete thought: brackets closed, readout lit, scan holding on the ring. */ @media (prefers-reduced-motion: reduce) { #pause.paused, #pause.paused img, #pause.paused img.fa-spin, - #pause.paused::after { animation: none; } - #pause.paused img { transform: rotate(-32.7deg); } + #pause.paused::after, + #pause.paused figcaption { animation: none; } + #pause.paused img { transform: scale(1); opacity: 1; } + #pause.paused figcaption { opacity: 1; } + #pause.paused::after { + transform: translateY(calc(var(--rb-pause-field) / 2)); + opacity: .9; + } } /* ============================================================