Fix the pause: it would not hide, it was off centre, and it was too small
Three faults in 1.0.4, and the first was the serious one.
1. It never went away. Core hides the overlay with `#pause { display: none }`
and shows it with `#pause.paused { display: flex }`. 1.0.4 styled the BARE
#pause with display:flex, which wins on specificity, so unpausing removed
the class and changed nothing — GAME PAUSED sat over the table for the rest
of the session. Every rule here is now scoped to `.paused` and `display` is
not set anywhere.
2. It was off centre because I repositioned it. Core already centres it: a
full-width band at top calc(50vh - 100px), column flex, centred both ways,
pointer-events none. That layout was correct and 1.0.4 fought it with
left/bottom/transform. It is left alone now; only the height changes, and
the top moves up by exactly half the extra so the centring holds.
3. It was small: a 322x116 panel in the corner. It is now core's full-width
band at 300px with a 170px dial — against core's own 100px — and the
caption at 24px to match core's.
Guarded: check-behaviour now parses the stylesheet and fails any rule that sets
display on an unscoped #pause. Verified it bites. It strips CSS comments first,
because the first version of the guard reported the comment explaining the
guard — which is exactly what the template guard did last time, so that is twice
now and the lesson is written down properly.
Verified by driving the real toggle: running → display none, box 0x0, no class;
paused → display flex, 341x302, centre X exactly on the viewport centre;
unpaused again → display none, box 0x0.
Version 1.0.5.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
ad771ff2b5
commit
a1cad97d47
@@ -183,6 +183,6 @@ The build runs all three guards before it writes anything:
|
||||
check-rules: OK — 87 rules, 21 files scanned, 3 aliases + 41 constant sets checked, 275 formulas verified
|
||||
check-kits: OK — 25 roles, 10 trades, 218 catalogue items, every kit key resolves, every posting can use what it carries, every loadout distinct
|
||||
check-lang: OK — en.json, 945 keys, no leaf/branch collisions
|
||||
check-behaviour: OK — 45 behavioural tests
|
||||
check-behaviour: OK — 46 behavioural tests
|
||||
```
|
||||
<!-- guards:end -->
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
2026/08/12-19:24:52.815175 17303f000 Delete type=3 #1
|
||||
2026/08/12-19:31:16.133590 1737ab000 Delete type=3 #1
|
||||
|
||||
@@ -1 +1 @@
|
||||
2026/08/12-19:24:52.889991 17384b000 Delete type=3 #1
|
||||
2026/08/12-19:31:16.207708 171f87000 Delete type=3 #1
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
2026/08/12-19:24:52.817229 17303f000 Delete type=3 #1
|
||||
2026/08/12-19:31:16.135472 171f87000 Delete type=3 #1
|
||||
|
||||
@@ -1 +1 @@
|
||||
2026/08/12-19:24:52.871528 174057000 Delete type=3 #1
|
||||
2026/08/12-19:31:16.192039 171f87000 Delete type=3 #1
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
2026/08/12-19:24:52.853027 17303f000 Delete type=3 #1
|
||||
2026/08/12-19:31:16.173317 172f9f000 Delete type=3 #1
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
2026/08/12-19:24:52.884011 174057000 Delete type=3 #1
|
||||
2026/08/12-19:31:16.202720 1737ab000 Delete type=3 #1
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
2026/08/12-19:24:52.837710 17303f000 Delete type=3 #1
|
||||
2026/08/12-19:31:16.157576 172793000 Delete type=3 #1
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
2026/08/12-19:24:52.880282 174057000 Delete type=3 #1
|
||||
2026/08/12-19:31:16.199567 172793000 Delete type=3 #1
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
2026/08/12-19:24:52.876362 172833000 Delete type=3 #1
|
||||
2026/08/12-19:31:16.195999 1737ab000 Delete type=3 #1
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
2026/08/12-19:24:52.888015 174057000 Delete type=3 #1
|
||||
2026/08/12-19:31:16.206279 172f9f000 Delete type=3 #1
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
2026/08/12-19:24:52.808706 17303f000 Delete type=3 #1
|
||||
2026/08/12-19:31:16.127123 171f87000 Delete type=3 #1
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
2026/08/12-19:24:52.882429 174057000 Delete type=3 #1
|
||||
2026/08/12-19:31:16.201269 172793000 Delete type=3 #1
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
2026/08/12-19:24:52.863812 17384b000 Delete type=3 #1
|
||||
2026/08/12-19:31:16.184597 172793000 Delete type=3 #1
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
2026/08/12-19:24:52.857469 17384b000 Delete type=3 #1
|
||||
2026/08/12-19:31:16.178030 172f9f000 Delete type=3 #1
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
2026/08/12-19:24:52.811553 17303f000 Delete type=3 #1
|
||||
2026/08/12-19:31:16.129805 171f87000 Delete type=3 #1
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
2026/08/12-19:24:52.819322 17303f000 Delete type=3 #1
|
||||
2026/08/12-19:31:16.137478 1737ab000 Delete type=3 #1
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
2026/08/12-19:24:52.813317 17384b000 Delete type=3 #1
|
||||
2026/08/12-19:31:16.131540 171f87000 Delete type=3 #1
|
||||
|
||||
+66
-63
@@ -1688,100 +1688,103 @@
|
||||
/* ============================================================
|
||||
THE PAUSE — a uniselector, stepping
|
||||
============================================================
|
||||
Foundry's pause is a <figure id="pause"> holding an <img class="fa-spin"> and a
|
||||
<figcaption>. Both are replaced here without touching the markup.
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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. It is the right object for this
|
||||
game — 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 coming back to.
|
||||
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.
|
||||
|
||||
It STEPS rather than sweeps. A continuous spin reads as a loading throbber; a
|
||||
discrete click every 700ms reads as a machine working through a list, which is what
|
||||
the department is doing while the table is out of the room.
|
||||
|
||||
No new text: Foundry's own caption is localised and this only restyles it. A string
|
||||
hardcoded here would be one the lang file could never reach.
|
||||
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.
|
||||
============================================================ */
|
||||
|
||||
#pause {
|
||||
/* Core pins this with BOTH top and bottom, so an auto height stretches it down the
|
||||
whole screen — 180px tall for a 16px caption. Pin it once, here, and size to
|
||||
content. The arm is rotated, so its bounding box is the diagonal of its own square:
|
||||
the panel is laid out from the flex basis, never from getBoundingClientRect. */
|
||||
position: fixed;
|
||||
left: 50%; bottom: 14vh; top: auto; right: auto;
|
||||
transform: translateX(-50%);
|
||||
height: auto; width: max-content;
|
||||
display: flex; align-items: center; gap: 16px;
|
||||
min-width: 320px; padding: 14px 22px 14px 16px;
|
||||
background: color-mix(in srgb, var(--rb-void) 88%, transparent);
|
||||
border: 1px solid var(--rb-line);
|
||||
border-top: 2px solid var(--rb-yellow);
|
||||
border-radius: 0;
|
||||
box-shadow: 0 10px 40px rgba(0,0,0,.55);
|
||||
/* 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;
|
||||
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%,
|
||||
transparent 100%);
|
||||
}
|
||||
|
||||
/* the dial: eleven contacts, standing still */
|
||||
#pause::before {
|
||||
/* the dial: eleven contacts, standing still, behind the arm */
|
||||
#pause.paused::before {
|
||||
content: "";
|
||||
position: absolute; left: 16px; top: 50%;
|
||||
width: 52px; height: 52px; margin-top: -26px;
|
||||
position: absolute;
|
||||
top: 24px; left: 50%; margin-left: -85px;
|
||||
width: 170px; height: 170px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid var(--rb-hairline);
|
||||
background:
|
||||
repeating-conic-gradient(from -90deg,
|
||||
var(--rb-line) 0deg 2.4deg, transparent 2.4deg 32.727deg);
|
||||
-webkit-mask: radial-gradient(circle, transparent 0 15px, #000 16px);
|
||||
mask: radial-gradient(circle, transparent 0 15px, #000 16px);
|
||||
opacity: .95;
|
||||
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;
|
||||
}
|
||||
|
||||
/* the arm. The <img> keeps its box and loses its pixels: object-position walks the
|
||||
raster out of the frame, which leaves a paintable element without a second wrapper. */
|
||||
#pause img {
|
||||
position: relative; z-index: 1;
|
||||
flex: 0 0 52px; width: 52px; height: 52px; margin: 0;
|
||||
object-fit: none; object-position: -999px -999px;
|
||||
border: none; filter: none;
|
||||
raster out of frame, which leaves a paintable element without adding a wrapper. */
|
||||
#pause.paused img {
|
||||
width: 170px; height: 170px;
|
||||
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 entirely, and the whole point is that it is stepping */
|
||||
background: conic-gradient(from -92deg,
|
||||
var(--rb-yellow) 0deg 5deg,
|
||||
var(--rb-yellow-dim) 5deg 30deg,
|
||||
transparent 30deg 360deg);
|
||||
-webkit-mask: radial-gradient(circle, #000 0 22px, transparent 23px);
|
||||
mask: radial-gradient(circle, #000 0 22px, transparent 23px);
|
||||
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;
|
||||
}
|
||||
/* Foundry adds fa-spin to that img; ours is the only rotation that should run. */
|
||||
#pause img.fa-spin { animation: rb-uniselector 7.7s steps(11, end) 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 */
|
||||
#pause::after {
|
||||
#pause.paused::after {
|
||||
content: "";
|
||||
position: absolute; left: 38px; top: 50%;
|
||||
width: 8px; height: 8px; margin-top: -4px;
|
||||
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;
|
||||
}
|
||||
|
||||
#pause figcaption {
|
||||
#pause.paused figcaption {
|
||||
font-family: var(--rb-display);
|
||||
font-size: 13px; letter-spacing: .22em; text-transform: uppercase;
|
||||
font-size: 24px;
|
||||
letter-spacing: .3em;
|
||||
text-transform: uppercase;
|
||||
color: var(--rb-yellow);
|
||||
text-shadow: none; padding: 0; margin: 0;
|
||||
border-left: 1px solid var(--rb-hairline); padding-left: 16px;
|
||||
line-height: 1.2;
|
||||
text-shadow: 0 2px 18px var(--rb-void);
|
||||
}
|
||||
|
||||
@keyframes rb-uniselector { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
|
||||
@keyframes rb-pause-pip { 0%, 100% { opacity: .25; } 50% { opacity: 1; } }
|
||||
|
||||
/* A stepping dial in the corner of the eye is exactly the sort of thing a reduced-motion
|
||||
preference is asking us not to do. Keep the object, stop the movement. */
|
||||
/* 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. */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
#pause img, #pause img.fa-spin, #pause::after { animation: none; }
|
||||
#pause img { transform: rotate(-32deg); }
|
||||
#pause.paused,
|
||||
#pause.paused img,
|
||||
#pause.paused img.fa-spin,
|
||||
#pause.paused::after { animation: none; }
|
||||
#pause.paused img { transform: rotate(-32.7deg); }
|
||||
}
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"id": "ringbrp",
|
||||
"title": "The Custodians",
|
||||
"description": "A Basic Roleplaying game of agents who cross the crossing to contain what should not be there — and who come back a little less from here each time.",
|
||||
"version": "1.0.4",
|
||||
"version": "1.0.5",
|
||||
"compatibility": {
|
||||
"minimum": 13,
|
||||
"verified": "14.364"
|
||||
|
||||
@@ -426,6 +426,28 @@ test("every declared document type has a label and a sheet section", async () =>
|
||||
}
|
||||
});
|
||||
|
||||
// Core hides the pause overlay with `#pause { display: none }` and shows it with
|
||||
// `#pause.paused { display: flex }`. A system rule on the BARE #pause that sets display
|
||||
// wins on specificity and the overlay never goes away — the game unpauses, play carries
|
||||
// on, and GAME PAUSED sits over the table for the rest of the session. Shipped exactly
|
||||
// that in 1.0.4. Anything targeting #pause must be scoped to .paused.
|
||||
test("no stylesheet rule sets display on a bare #pause", async () => {
|
||||
const { readFile } = await import("node:fs/promises");
|
||||
const raw = await readFile(new URL("../styles/ringbrp.css", import.meta.url), "utf8");
|
||||
// Comments first, or this guard reports the comment that explains the guard — which
|
||||
// is precisely what the template guard did the last time round.
|
||||
const css = raw.replace(/\/\*[\s\S]*?\*\//g, "");
|
||||
// every rule whose selector mentions #pause, with its declaration block
|
||||
for (const m of css.matchAll(/([^{}]*#pause[^{}]*)\{([^{}]*)\}/g)) {
|
||||
const selector = m[1].trim().replace(/\s+/g, " ");
|
||||
const body = m[2];
|
||||
const scoped = selector.split(",").every(sel => !sel.includes("#pause") || /#pause[^,]*\.paused/.test(sel));
|
||||
if (scoped) continue;
|
||||
assert.ok(!/(^|;)\s*display\s*:/.test(body),
|
||||
`"${selector}" sets display on an unscoped #pause — the overlay will not hide on unpause`);
|
||||
}
|
||||
});
|
||||
|
||||
/* ---------------------------------------------------------------- */
|
||||
|
||||
await runAll();
|
||||
|
||||
Reference in New Issue
Block a user