Rebuild the pause overlay as a containment hold

The uniselector rotated forever, which is the one thing a hunting stepping switch never does, and it was carrying two outright bugs: the motion trail pointed the way the arm was going rather than the way it came, because conic angles run clockwise from the origin and the tip was declared first; and the hub pip ran on a free 2.6s sine against a 7.7s arm, so the light drifted permanently out of phase with the mechanism and meant nothing.

Replaced with an instrument that acquires, locks, reports and lets go: a static field, four corner brackets that close and snap, a scan line reading down the field at exactly cycle/3 so its third pass always lands on the lock, and a readout welded to the same cycle. Every layer reads --rb-pause-cycle, so nothing can drift. The field offset is derived from the column rather than measured and pasted in.

Also ignores the impeccable hook cache and keeps its config, which records why broken-image is suppressed for a stylesheet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
slaguru666
2026-08-30 21:08:23 +01:00
co-authored by Claude Opus 5
parent 2332b449ba
commit b31b1028a9
3 changed files with 178 additions and 58 deletions
+158 -58
View File
@@ -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 <figure id="pause"> holding an <img> and a <figcaption>, 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 <img>, and the <figcaption> — 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 <img> 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 <img> 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;
}
}
/* ============================================================