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;
+ }
}
/* ============================================================