Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0852427586 |
@@ -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-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-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-lang: OK — en.json, 945 keys, no leaf/branch collisions
|
||||||
check-behaviour: OK — 45 behavioural tests
|
check-behaviour: OK — 46 behavioural tests
|
||||||
```
|
```
|
||||||
<!-- guards:end -->
|
<!-- 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
|
THE PAUSE — a uniselector, stepping
|
||||||
============================================================
|
============================================================
|
||||||
Foundry's pause is a <figure id="pause"> holding an <img class="fa-spin"> and a
|
Foundry's pause is <figure id="pause"> holding an <img> and a <figcaption>, and core
|
||||||
<figcaption>. Both are replaced here without touching the markup.
|
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
|
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
|
round one contact at a time, hunting for a free line. The department's whole business
|
||||||
game — the department's whole business is a machine that says where things belong,
|
is a machine that says where things belong, and OPEN DAY is set inside one. Eleven
|
||||||
and OPEN DAY is set inside one. Eleven contacts, because eleven minutes is the
|
contacts, because eleven minutes is the interval this setting keeps returning to.
|
||||||
interval this setting keeps coming back to.
|
|
||||||
|
|
||||||
It STEPS rather than sweeps. A continuous spin reads as a loading throbber; a
|
It STEPS rather than sweeps — a contact every 700ms. A continuous spin reads as a
|
||||||
discrete click every 700ms reads as a machine working through a list, which is what
|
loading throbber; a discrete click reads as a machine working through a list.
|
||||||
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.
|
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
#pause {
|
/* Taller than core's 180px, and moved up by exactly half the extra so it stays centred. */
|
||||||
/* Core pins this with BOTH top and bottom, so an auto height stretches it down the
|
#pause.paused {
|
||||||
whole screen — 180px tall for a 16px caption. Pin it once, here, and size to
|
height: 300px;
|
||||||
content. The arm is rotated, so its bounding box is the diagonal of its own square:
|
top: calc(50vh - 150px);
|
||||||
the panel is laid out from the flex basis, never from getBoundingClientRect. */
|
gap: 28px;
|
||||||
position: fixed;
|
background: linear-gradient(to right,
|
||||||
left: 50%; bottom: 14vh; top: auto; right: auto;
|
transparent 0%,
|
||||||
transform: translateX(-50%);
|
color-mix(in srgb, var(--rb-void) 82%, transparent) 38%,
|
||||||
height: auto; width: max-content;
|
color-mix(in srgb, var(--rb-void) 82%, transparent) 62%,
|
||||||
display: flex; align-items: center; gap: 16px;
|
transparent 100%);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* the dial: eleven contacts, standing still */
|
/* the dial: eleven contacts, standing still, behind the arm */
|
||||||
#pause::before {
|
#pause.paused::before {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute; left: 16px; top: 50%;
|
position: absolute;
|
||||||
width: 52px; height: 52px; margin-top: -26px;
|
top: 24px; left: 50%; margin-left: -85px;
|
||||||
|
width: 170px; height: 170px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
border: 1px solid var(--rb-hairline);
|
|
||||||
background:
|
background:
|
||||||
repeating-conic-gradient(from -90deg,
|
repeating-conic-gradient(from -90deg,
|
||||||
var(--rb-line) 0deg 2.4deg, transparent 2.4deg 32.727deg);
|
var(--rb-line) 0deg 1.6deg, transparent 1.6deg 32.727deg);
|
||||||
-webkit-mask: radial-gradient(circle, transparent 0 15px, #000 16px);
|
-webkit-mask: radial-gradient(circle, transparent 0 52px, #000 54px);
|
||||||
mask: radial-gradient(circle, transparent 0 15px, #000 16px);
|
mask: radial-gradient(circle, transparent 0 52px, #000 54px);
|
||||||
opacity: .95;
|
opacity: .9;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* the arm. The <img> keeps its box and loses its pixels: object-position walks the
|
/* 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. */
|
raster out of frame, which leaves a paintable element without adding a wrapper. */
|
||||||
#pause img {
|
#pause.paused img {
|
||||||
position: relative; z-index: 1;
|
width: 170px; height: 170px;
|
||||||
flex: 0 0 52px; width: 52px; height: 52px; margin: 0;
|
object-fit: none; object-position: -9999px -9999px;
|
||||||
object-fit: none; object-position: -999px -999px;
|
border: none; box-shadow: none; filter: none;
|
||||||
border: none; filter: none;
|
|
||||||
/* the arm, and behind it the contact it has just left — without the trailing sector
|
/* 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 */
|
a 700ms step is easy to miss, and the stepping is the whole idea */
|
||||||
background: conic-gradient(from -92deg,
|
background: conic-gradient(from -91deg,
|
||||||
var(--rb-yellow) 0deg 5deg,
|
var(--rb-yellow) 0deg 3.5deg,
|
||||||
var(--rb-yellow-dim) 5deg 30deg,
|
var(--rb-yellow-dim) 3.5deg 28deg,
|
||||||
transparent 30deg 360deg);
|
transparent 28deg 360deg);
|
||||||
-webkit-mask: radial-gradient(circle, #000 0 22px, transparent 23px);
|
-webkit-mask: radial-gradient(circle, #000 0 78px, transparent 80px);
|
||||||
mask: radial-gradient(circle, #000 0 22px, transparent 23px);
|
mask: radial-gradient(circle, #000 0 78px, transparent 80px);
|
||||||
animation: rb-uniselector 7.7s steps(11, end) infinite;
|
animation: rb-uniselector 7.7s steps(11, end) infinite;
|
||||||
}
|
}
|
||||||
/* Foundry adds fa-spin to that img; ours is the only rotation that should run. */
|
/* core adds fa-spin to that img; ours must be the only rotation running */
|
||||||
#pause img.fa-spin { animation: rb-uniselector 7.7s steps(11, end) infinite; }
|
#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 hub, and the amber that says the record is being written */
|
||||||
#pause::after {
|
#pause.paused::after {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute; left: 38px; top: 50%;
|
position: absolute;
|
||||||
width: 8px; height: 8px; margin-top: -4px;
|
top: 101px; left: 50%; margin-left: -8px;
|
||||||
|
width: 16px; height: 16px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: var(--rb-yellow);
|
background: var(--rb-yellow);
|
||||||
animation: rb-pause-pip 2.6s ease-in-out infinite;
|
animation: rb-pause-pip 2.6s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
#pause figcaption {
|
#pause.paused figcaption {
|
||||||
font-family: var(--rb-display);
|
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);
|
color: var(--rb-yellow);
|
||||||
text-shadow: none; padding: 0; margin: 0;
|
line-height: 1.2;
|
||||||
border-left: 1px solid var(--rb-hairline); padding-left: 16px;
|
text-shadow: 0 2px 18px var(--rb-void);
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes rb-uniselector { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
|
@keyframes rb-uniselector { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
|
||||||
@keyframes rb-pause-pip { 0%, 100% { opacity: .25; } 50% { opacity: 1; } }
|
@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
|
/* A stepping dial in the corner of the eye is exactly what this preference is about.
|
||||||
preference is asking us not to do. Keep the object, stop the movement. */
|
Keep the object, stop the movement — including core's own pulse on the whole figure. */
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
#pause img, #pause img.fa-spin, #pause::after { animation: none; }
|
#pause.paused,
|
||||||
#pause img { transform: rotate(-32deg); }
|
#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",
|
"id": "ringbrp",
|
||||||
"title": "The Custodians",
|
"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.",
|
"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": {
|
"compatibility": {
|
||||||
"minimum": 13,
|
"minimum": 13,
|
||||||
"verified": "14.364"
|
"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();
|
await runAll();
|
||||||
|
|||||||
Reference in New Issue
Block a user