From 34c40317a783f1f07b90313b708291d8133d403f Mon Sep 17 00:00:00 2001 From: Mohamed Elsherif Date: Thu, 3 Sep 2026 00:43:07 -0700 Subject: [PATCH 1/8] Fix Battle Tanks narrow portrait layout --- battle-tanks/styles.css | 2 +- releases.json | 2 +- tests/battle-tanks-page.test.js | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/battle-tanks/styles.css b/battle-tanks/styles.css index 80400d8..2d2b088 100644 --- a/battle-tanks/styles.css +++ b/battle-tanks/styles.css @@ -17,7 +17,7 @@ /* Landscape full screen uses a side command deck, leaving the 16:9 arena nearly full-height. */ @media(max-width:520px){.mobile-controls{display:grid}.controls>.readouts,.controls>.power-up-panel,.controls>.control-pad,.controls>.fire-button{display:none}} -@media (min-width:521px) and (max-height:900px), (min-width:701px) and (max-width:850px){.game-battle-tanks .game-layout{grid-template-areas:"arena compact" "arena settings";grid-template-columns:minmax(0,1fr) minmax(300px,360px);grid-template-rows:auto auto;align-items:start}.game-battle-tanks .arena-wrap{grid-area:arena}.game-battle-tanks .mobile-controls{position:static;display:grid;grid-area:compact;grid-template-columns:repeat(3,minmax(0,1fr))}.game-battle-tanks .controls{grid-area:settings;max-height:none;gap:8px;overflow:visible}.game-battle-tanks .controls>.readouts,.game-battle-tanks .controls>.power-up-panel,.game-battle-tanks .controls>.control-pad,.game-battle-tanks .controls>.color-controls,.game-battle-tanks .controls>.fire-button,.game-battle-tanks .controls>#fullscreen,.game-battle-tanks .controls>.instructions{display:none}} +@media (min-width:521px) and (max-height:900px) and (orientation:landscape), (min-width:701px) and (max-width:850px){.game-battle-tanks .game-layout{grid-template-areas:"arena compact" "arena settings";grid-template-columns:minmax(0,1fr) minmax(300px,360px);grid-template-rows:auto auto;align-items:start}.game-battle-tanks .arena-wrap{grid-area:arena}.game-battle-tanks .mobile-controls{position:static;display:grid;grid-area:compact;grid-template-columns:repeat(3,minmax(0,1fr))}.game-battle-tanks .controls{grid-area:settings;max-height:none;gap:8px;overflow:visible}.game-battle-tanks .controls>.readouts,.game-battle-tanks .controls>.power-up-panel,.game-battle-tanks .controls>.control-pad,.game-battle-tanks .controls>.color-controls,.game-battle-tanks .controls>.fire-button,.game-battle-tanks .controls>#fullscreen,.game-battle-tanks .controls>.instructions{display:none}} @media (orientation:landscape){.arena-wrap:fullscreen,.arena-wrap.is-fullscreen{--battle-command-deck:clamp(150px,18vw,280px);box-sizing:border-box;grid-template-columns:minmax(0,1fr) var(--battle-command-deck);grid-template-rows:100%;place-items:center;padding:env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left)}.arena-wrap:fullscreen canvas,.arena-wrap.is-fullscreen canvas{grid-column:1;grid-row:1;width:min(100%,177.78dvh);height:auto;max-width:100%;max-height:100%;object-fit:contain}.arena-wrap:fullscreen .fullscreen-controls,.arena-wrap.is-fullscreen .fullscreen-controls{position:relative;inset:auto;box-sizing:border-box;display:grid;width:100%;height:100%;grid-column:2;grid-row:1;grid-template-columns:repeat(2,minmax(0,1fr));align-content:center;align-self:stretch;justify-self:stretch;gap:7px;padding:clamp(8px,1.2vw,16px);border-width:0 0 0 1px;border-radius:0;transform:none}.fullscreen-controls>.compact-readouts,.fullscreen-controls>.fullscreen-inventory,.fullscreen-controls>.compact-control-group,.fullscreen-controls>.compact-loadout,.fullscreen-controls>.fullscreen-fire{grid-column:1/-1}.arena-wrap:fullscreen .arena-health,.arena-wrap.is-fullscreen .arena-health,.arena-wrap:fullscreen .impact-callout,.arena-wrap.is-fullscreen .impact-callout{left:calc((100% - var(--battle-command-deck))/2)}.arena-wrap:fullscreen[data-active="1"] .active-flag,.arena-wrap.is-fullscreen[data-active="1"] .active-flag{right:calc(var(--battle-command-deck) + 12px)}.arena-wrap:fullscreen .arena-share,.arena-wrap.is-fullscreen .arena-share{left:calc((100% - var(--battle-command-deck))/2)}} @media (orientation:landscape) and (max-height:500px){.arena-wrap:fullscreen,.arena-wrap.is-fullscreen{--battle-command-deck:clamp(148px,18vw,190px)}.arena-wrap:fullscreen .fullscreen-controls,.arena-wrap.is-fullscreen .fullscreen-controls{gap:4px;padding:6px}.fullscreen-controls button{min-height:44px;font-size:10px}.fullscreen-controls label,.fullscreen-controls select,.fullscreen-controls span{min-width:0;font-size:9px}.fullscreen-inventory{gap:4px}.fullscreen-inventory>strong{display:none}.fullscreen-inventory .inventory{overflow:hidden}.fullscreen-inventory .inventory button{min-height:44px;padding:3px 5px;font-size:9px}.fullscreen-controls .inventory:not(:has(button)){display:none}.fullscreen-controls .compact-loadout{grid-template-columns:1fr}.fullscreen-controls .compact-loadout label{display:none}.fullscreen-controls .compact-loadout select{min-height:44px}.fullscreen-controls .compact-loadout span{grid-column:1;text-align:center}} @media(max-height:420px){.power-card,.result-card{border-radius:14px}.power-card{gap:2px;padding:8px 14px}.power-card-icon{font-size:26px}.power-card h2{font-size:1.25rem}.power-card #power-card-effect{font-size:.75rem}.result-card{gap:3px;padding:8px 14px}.result-burst{font-size:30px}.result-card h2{font-size:1.55rem}.result-card p:not(.result-kicker){font-size:.75rem}.result-actions button{min-height:44px}} diff --git a/releases.json b/releases.json index 68df4e6..f4e0c18 100644 --- a/releases.json +++ b/releases.json @@ -11,7 +11,7 @@ "fixes": [ "Restored normal page scrolling in iPad browsers so players can reach content below the game area.", "Reduced unused top spacing and compacted modern game layouts, including Tetris controls in phone landscape, so gameplay stays visible without scrolling when space permits.", - "Restored the Battle Tanks full-screen action in compact tablet and short-viewport controls." + "Restored the Battle Tanks full-screen action in compact tablet and phone-landscape controls without shrinking narrow portrait battlefields." ], "technical": [ "Applied shared short-viewport and tablet layout rules consistently across the modern arcade." diff --git a/tests/battle-tanks-page.test.js b/tests/battle-tanks-page.test.js index 04304fe..e3b1061 100644 --- a/tests/battle-tanks-page.test.js +++ b/tests/battle-tanks-page.test.js @@ -91,7 +91,9 @@ test('Battle Tanks keeps mobile and short-landscape controls visible without cov assert.match(styles, /fullscreen-inventory \.inventory button\{min-height:44px/); assert.match(styles, /compact-loadout select\{min-height:44px/); assert.match(styles, /fullscreen-inventory[^}]*overflow:hidden/); + assert.match(styles, /min-width:521px\) and \(max-height:900px\) and \(orientation:landscape\), \(min-width:701px\) and \(max-width:850px\)/); assert.match(styles, /max-height:900px[\s\S]*?\.game-battle-tanks \.mobile-controls\{[^}]*position:static;[^}]*display:grid;[^}]*grid-template-columns:repeat\(3,minmax\(0,1fr\)\)/); + assert.doesNotMatch(styles, /min-width:521px\) and \(max-height:900px\),/); assert.match(styles, /max-height:900px[\s\S]*?\.game-battle-tanks \.controls>\.readouts[^}]*display:none/); }); From 3adfa892cf6258a81912fa42f946ab12a58a1aa9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 4 Sep 2026 16:05:36 +0000 Subject: [PATCH 2/8] Bump docker/setup-qemu-action in the action-versions group Bumps the action-versions group with 1 update: [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action). Updates `docker/setup-qemu-action` from 4.2.0 to 4.3.0 - [Release notes](https://github.com/docker/setup-qemu-action/releases) - [Commits](https://github.com/docker/setup-qemu-action/compare/96fe6ef7f33517b61c61be40b68a1882f3264fb8...1f40c72289eff860ee54a304f1438e3cff362e0a) --- updated-dependencies: - dependency-name: docker/setup-qemu-action dependency-version: 4.3.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: action-versions ... Signed-off-by: dependabot[bot] --- .github/workflows/container.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/container.yml b/.github/workflows/container.yml index 92c80a2..3150302 100644 --- a/.github/workflows/container.yml +++ b/.github/workflows/container.yml @@ -91,7 +91,7 @@ jobs: - name: Check out repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up QEMU - uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0 + uses: docker/setup-qemu-action@1f40c72289eff860ee54a304f1438e3cff362e0a # v4.3.0 - name: Set up Docker Buildx uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 - name: Log in to Docker Hub diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d74af10..a7c2f47 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -98,7 +98,7 @@ jobs: git push origin "refs/tags/v${RELEASE_VERSION}" fi - name: Set up QEMU - uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0 + uses: docker/setup-qemu-action@1f40c72289eff860ee54a304f1438e3cff362e0a # v4.3.0 - name: Log in to Docker Hub uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: From 80418b7695f11a48416dc931410e02d91d0e4bc8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2026 16:17:35 +0000 Subject: [PATCH 3/8] Bump node in the container-versions group across 1 directory Bumps the container-versions group with 1 update in the / directory: node. Updates `node` from `2d984a1` to `ef24c50` --- updated-dependencies: - dependency-name: node dependency-version: 26-alpine dependency-type: direct:production dependency-group: container-versions ... Signed-off-by: dependabot[bot] --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 8cd1924..68a2e10 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,9 @@ -FROM node:26-alpine@sha256:2d984a15c9b54fd0aeb608b8e0d0d83529eb34d2966db27a1fb4f1edc3d298a3 AS dependencies +FROM node:26-alpine@sha256:ef24c5053d50fdc3e4e56eb4e7ddb7861874ab0fdc797046ba897581deb8e868 AS dependencies WORKDIR /app COPY package.json package-lock.json ./ RUN npm ci --omit=dev -FROM node:26-alpine@sha256:2d984a15c9b54fd0aeb608b8e0d0d83529eb34d2966db27a1fb4f1edc3d298a3 +FROM node:26-alpine@sha256:ef24c5053d50fdc3e4e56eb4e7ddb7861874ab0fdc797046ba897581deb8e868 ARG BUILD_VERSION=dev ARG BUILD_CHANNEL=dev ENV NODE_ENV=production PORT=8080 From c2d11412ff7d438f87c9bdc5633157cdf5834aeb Mon Sep 17 00:00:00 2001 From: Mohamed Elsherif Date: Fri, 11 Sep 2026 20:43:11 -0700 Subject: [PATCH 4/8] Add game-native about page --- about.css | 86 ++++++++++++++++++++++++++++++ about.html | 91 ++++++++++++++++++++++++++++++++ arcade.css | 3 +- arcade.js | 6 ++- docs/architecture.md | 2 +- service-worker.js | 4 +- tests/about-page.test.js | 44 +++++++++++++++ tests/game-events.test.js | 2 +- tests/online-rooms.test.js | 2 +- tests/shared-game-assets.test.js | 1 + 10 files changed, 234 insertions(+), 7 deletions(-) create mode 100644 about.css create mode 100644 about.html create mode 100644 tests/about-page.test.js diff --git a/about.css b/about.css new file mode 100644 index 0000000..e577ebb --- /dev/null +++ b/about.css @@ -0,0 +1,86 @@ +:root { + color-scheme:light; + --page:#fff4d6; + --ink:#192541; + --muted:#58647a; + --panel:#fff9e8; + --line:#192541; + --accent:#765cff; + --pink:#ff4fa3; + --mint:#6ef2d0; + --yellow:#ffd84d; + --sky:#56a8ff; + --shadow:#192541; +} +:root[data-theme="dark"] { color-scheme:dark;--page:#0a1020;--ink:#fff4d6;--muted:#b7c2dc;--panel:#111a33;--line:#fff4d6;--shadow:#030712; } +* { box-sizing:border-box; } +body { min-height:100vh;margin:0;color:var(--ink);background:linear-gradient(90deg,#ffffff06 1px,transparent 1px),linear-gradient(#ffffff06 1px,transparent 1px),radial-gradient(circle at 8% 0,#765cff38,transparent 34rem),var(--page);background-size:32px 32px,32px 32px,auto,auto;font-family:var(--pixel-body,ui-monospace,"SFMono-Regular",Consolas,monospace);overflow-x:hidden; } +a { color:inherit; } +.about-shell { width:min(1120px,calc(100% - 40px));margin:0 auto;padding:clamp(38px,6vw,78px) 0 72px; } +.about-hero { display:grid;grid-template-columns:minmax(0,1.45fr) minmax(280px,.55fr);gap:clamp(32px,6vw,76px);align-items:center;margin-bottom:52px; } +.eyebrow,.panel-kicker { margin:0 0 18px;color:var(--pink);font:700 12px/1.2 var(--pixel-font,ui-monospace,monospace);letter-spacing:.1em;text-transform:uppercase; } +.eyebrow { display:inline-block;padding:8px 12px;color:#192541;background:var(--yellow);border:2px solid var(--line);box-shadow:4px 4px 0 var(--line); } +h1,h2,.player-card strong,.button,.resource-links span,.social-grid strong { font-family:var(--pixel-font,ui-monospace,monospace); } +h1 { max-width:740px;margin:0;font-size:clamp(40px,7vw,76px);font-weight:700;letter-spacing:-.065em;line-height:1.08;text-wrap:balance;text-shadow:5px 5px 0 var(--accent); } +h1 span { color:var(--pink); } +.lede { max-width:720px;margin:28px 0 0;color:var(--muted);font-size:clamp(17px,2vw,20px);line-height:1.65; } +.hero-actions { display:flex;flex-wrap:wrap;gap:14px;margin-top:30px; } +.button { display:inline-flex;align-items:center;justify-content:center;gap:9px;min-height:48px;padding:10px 16px;border:2px solid var(--line);font-size:11px;font-weight:700;text-decoration:none;box-shadow:4px 4px 0 var(--line);transition:transform 140ms steps(2,end),box-shadow 140ms steps(2,end); } +.button-primary { color:#192541;background:var(--mint); } +.button-secondary { background:var(--panel); } +.button:hover { transform:translate(4px,4px);box-shadow:0 0 0 var(--line); } +.button:focus-visible,.resource-links a:focus-visible,.patent-list a:focus-visible,.social-grid a:focus-visible { outline:3px solid var(--mint);outline-offset:4px; } +.player-card { position:relative;padding:28px;background:var(--panel);border:3px solid var(--line);box-shadow:8px 8px 0 var(--pink),12px 12px 0 var(--line);transform:rotate(1.5deg); } +.player-card::after { position:absolute;top:14px;right:14px;width:42px;height:42px;background:linear-gradient(90deg,var(--yellow) 0 30%,transparent 30% 70%,var(--yellow) 70%),linear-gradient(var(--yellow) 0 30%,transparent 30% 70%,var(--yellow) 70%);content:"";opacity:.8; } +.player-label { margin:0 0 10px;color:var(--pink);font-size:11px;font-weight:800;letter-spacing:.12em;text-transform:uppercase; } +.player-card>strong { display:block;padding-right:46px;font-size:clamp(19px,3vw,27px); } +.player-card dl { display:grid;gap:0;margin:24px 0 0; } +.player-card dl div { display:grid;grid-template-columns:90px 1fr;gap:12px;padding:12px 0;border-top:1px solid color-mix(in srgb,var(--line) 22%,transparent); } +.player-card dt { color:var(--muted);font-size:12px;text-transform:uppercase; } +.player-card dd { margin:0;font-size:14px;font-weight:800; } +.about-grid { display:grid;grid-template-columns:1.25fr .75fr;gap:24px; } +.panel { position:relative;padding:clamp(25px,4vw,40px);background:var(--panel);border:3px solid var(--line);box-shadow:7px 7px 0 var(--accent),11px 11px 0 var(--line); } +.panel::before { position:absolute;top:0;right:0;left:0;height:8px;background:repeating-linear-gradient(90deg,var(--accent) 0 20px,color-mix(in srgb,var(--accent) 42%,transparent) 20px 28px);content:""; } +.panel h2,.social-section h2 { margin:0;font-size:clamp(26px,4vw,38px);letter-spacing:-.045em;line-height:1.2; } +.panel>p:not(.panel-kicker) { margin:20px 0 0;color:var(--muted);font-size:16px;line-height:1.7; } +.career-path { display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:8px;margin:30px 0 0;padding:0;list-style:none; } +.career-path li { position:relative;padding-top:19px;text-align:center; } +.career-path li::before { position:absolute;top:3px;right:50%;left:-58%;height:3px;background:var(--accent);content:""; } +.career-path li:first-child::before { left:50%; } +.career-path li::after { position:absolute;top:0;left:calc(50% - 5px);width:10px;height:10px;background:var(--mint);border:2px solid var(--line);content:""; } +.career-path span { font-size:12px;font-weight:800; } +.community-panel { --accent:var(--mint); } +.resource-links { display:grid;gap:12px;margin-top:28px; } +.resource-links a { display:grid;grid-template-columns:1fr auto;gap:5px 12px;padding:15px;color:inherit;background:color-mix(in srgb,var(--accent) 9%,transparent);border:1px solid color-mix(in srgb,var(--line) 28%,transparent);text-decoration:none; } +.resource-links span { font-size:13px; } +.resource-links small { grid-column:1;color:var(--muted);font-size:12px;line-height:1.45; } +.resource-links b { grid-area:1/2/3;font-size:20px;align-self:center; } +.patent-panel { --accent:var(--yellow);display:grid;grid-template-columns:.4fr 1.6fr;gap:28px;margin-top:30px; } +.patent-list { display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px; } +.patent-list a { display:flex;flex-direction:column;gap:10px;min-height:170px;padding:18px;background:color-mix(in srgb,var(--yellow) 12%,transparent);border:2px solid var(--line);text-decoration:none; } +.patent-list small { color:var(--muted);font-size:11px;font-weight:800;text-transform:uppercase; } +.patent-list span { flex:1;font-size:15px;font-weight:800;line-height:1.45; } +.patent-list b { color:var(--accent);font-size:12px; } +.patent-list i { font-style:normal; } +.social-section { margin-top:58px; } +.social-grid { display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px;margin-top:24px; } +.social-grid a { display:grid;grid-template-columns:38px 1fr;gap:3px 11px;align-items:center;min-height:92px;padding:16px;background:var(--panel);border:2px solid var(--line);box-shadow:4px 4px 0 var(--line);text-decoration:none;transition:transform 140ms steps(2,end),box-shadow 140ms steps(2,end); } +.social-grid a:hover { transform:translate(3px,3px);box-shadow:1px 1px 0 var(--line); } +.social-grid a>span { grid-row:1/3;display:grid;width:38px;height:38px;place-items:center;color:#192541;background:var(--sky);border:2px solid var(--line);font-size:13px;font-weight:950; } +.social-grid a:nth-child(2n)>span { background:var(--mint); } +.social-grid a:nth-child(3n)>span { background:var(--pink); } +.social-grid strong { font-size:12px; } +.social-grid small { color:var(--muted);font-size:11px; } +:root[data-arcade-theme="cabinet"] .about-shell { width:min(1240px,calc(100% - 32px)); } +:root[data-arcade-theme="cabinet"] :is(.player-card,.panel) { box-shadow:5px 5px 0 var(--yellow); } +:root[data-arcade-theme="calm"] { --page:#eef4f1;--ink:#223a33;--muted:#61746d;--panel:#f8fbf9;--line:#527068;--accent:#356f61;--pink:#9b5548;--mint:#789d90;--yellow:#d9b968;--sky:#668e9b;--shadow:#263c34; } +:root[data-arcade-theme="calm"][data-color-mode="dark"] { --page:#111917;--ink:#e5efeb;--muted:#9fb0a9;--panel:#18231f;--line:#91a59d;--shadow:#000; } +:root[data-arcade-theme="calm"] body { background:var(--page);font-family:Georgia,"Times New Roman",serif; } +:root[data-arcade-theme="calm"] .about-shell { width:min(1020px,calc(100% - 48px)); } +:root[data-arcade-theme="calm"] :is(h1,h2,.player-card strong,.button,.resource-links span,.social-grid strong) { font-family:Georgia,"Times New Roman",serif; } +:root[data-arcade-theme="calm"] h1 { text-shadow:none; } +:root[data-arcade-theme="calm"] :is(.player-card,.panel,.social-grid a) { border-width:1px;box-shadow:none;transform:none; } +:root[data-arcade-theme="calm"] .panel::before { height:3px; } +@media (max-width:800px) { .about-hero,.about-grid,.patent-panel { grid-template-columns:1fr; } .player-card { width:min(100%,520px);transform:none; } .social-grid { grid-template-columns:repeat(2,minmax(0,1fr)); } } +@media (max-width:520px) { .about-shell,:root[data-arcade-theme="calm"] .about-shell { width:min(100% - 28px,1120px);padding-top:32px; } .about-hero { margin-bottom:38px; } .hero-actions { flex-direction:column; } .panel { padding:26px 20px;box-shadow:5px 5px 0 var(--accent),8px 8px 0 var(--line); } .career-path { grid-template-columns:1fr;gap:0; } .career-path li { padding:0 0 14px 25px;text-align:left; } .career-path li::before { top:0;bottom:0;right:auto;left:5px;width:3px;height:auto; } .career-path li:first-child::before { left:5px;top:5px; } .career-path li:last-child::before { bottom:calc(100% - 5px); } .career-path li::after { top:3px;left:0; } .patent-list,.social-grid { grid-template-columns:1fr; } .patent-list a { min-height:0; } } +@media (prefers-reduced-motion:reduce) { .button,.social-grid a { transition:none; } } diff --git a/about.html b/about.html new file mode 100644 index 0000000..adb201c --- /dev/null +++ b/about.html @@ -0,0 +1,91 @@ + + + + + + + + About Mohamed Elsherif · JavaScript Playground + + + + + + +
+
+
+

Meet the maker

+

Built by Mohamed Elsherif.

+

I’m a software architect with more than 20 years in software engineering. I build systems, share what I learn, and still make time for a good JavaScript game.

+ +
+ +
+ +
+
+

Career map

+

Building services that scale

+

I work on Apple’s Services Architecture and Efficiency team, helping online services scale reliably and run efficiently. Earlier chapters of my career include Azure identity at Microsoft, integrations at Okta, enterprise mobility at VMware, and Metrics and Reporting infrastructure at Hulu.

+
    +
  1. Hulu
  2. +
  3. VMware
  4. +
  5. Okta
  6. +
  7. Microsoft
  8. +
  9. Apple
  10. +
+
+ + +
+ +
+
+

Granted achievements

+

Patents

+
+ +
+ + +
+ + + + diff --git a/arcade.css b/arcade.css index 25f1219..02dbdbc 100644 --- a/arcade.css +++ b/arcade.css @@ -10,6 +10,7 @@ body:not(.arcade-has-topbar)::before { display:block; height:env(safe-area-inset body.arena-fullscreen .arcade-topbar { display:none; } body.arena-fullscreen .arcade-account { display:none; } .arcade-account a, .arcade-account button { padding: 8px 11px; color: inherit; background: transparent; border: 0; border-radius: 9px; font: inherit; text-decoration: none; cursor: pointer; } +.arcade-account a[aria-current="page"] { text-decoration:underline; text-decoration-thickness:2px; text-underline-offset:5px; } .arcade-account button { color: #10271f; background: #83e6c3; } .arcade-appearance-button::before { margin-right:6px; content:'◐'; } .arcade-audio-button::before { margin-right:6px; content:'♪'; }.arcade-audio-button[data-audio-muted="true"]::before{content:'×';}.arcade-audio-button:disabled{cursor:not-allowed;opacity:.55} @@ -67,7 +68,7 @@ body.arena-fullscreen .arcade-account { display:none; } .result-share-cancel { color: #f7f8fc; background: rgba(255,255,255,.09); } .result-share-confirm { color: #10271f; background: #83e6c3; } @media (max-width: 760px) { .arcade-topbar-inner { align-items:flex-start; flex-direction:column; gap:11px; } .arcade-account { width:100%; justify-content:flex-start; overflow-x:auto; padding:2px 3px 5px; scrollbar-width:none; } .arcade-account::-webkit-scrollbar { display:none; } } -@media (max-width: 520px) { .arcade-topbar { padding-inline:12px; } .arcade-home > span:last-child { font-size:14px; } .arcade-account > a:first-of-type { display:none; } .arcade-account a,.arcade-account button { white-space:nowrap; } } +@media (max-width: 520px) { .arcade-topbar { padding-inline:12px; } .arcade-home > span:last-child { font-size:14px; } .arcade-account > .arcade-scores-link { display:none; } .arcade-account a,.arcade-account button { white-space:nowrap; } } @media (max-width: 520px) { .arcade-install-hint { right:max(12px,env(safe-area-inset-right)); bottom:max(12px,env(safe-area-inset-bottom)); left:auto; } .arcade-install-dialog form { padding:19px; } } @media (max-width: 480px) { .result-share-content { padding: 17px; } .result-share-actions button { flex: 1; } } @media (prefers-reduced-motion: reduce) { .arcade-install-hint { animation:none; } } diff --git a/arcade.js b/arcade.js index 6dc0eed..7c269a7 100644 --- a/arcade.js +++ b/arcade.js @@ -149,7 +149,7 @@ home.setAttribute('aria-label', 'JavaScript Playground home'); home.innerHTML = 'JavaScript Playground'; const account = document.createElement('nav'); - account.className = 'arcade-account'; account.setAttribute('aria-label', 'Arcade account and appearance'); + account.className = 'arcade-account'; account.setAttribute('aria-label', 'Arcade navigation and account'); topbarInner.append(home, account); topbar.append(topbarInner); const appearanceButton = document.createElement('button'); appearanceButton.type = 'button'; appearanceButton.className = 'arcade-appearance-button'; appearanceButton.textContent = 'Appearance'; const audioButton = document.createElement('button'); audioButton.type = 'button'; audioButton.className = 'arcade-audio-button'; audioButton.textContent = 'Sound'; audioButton.setAttribute('aria-haspopup', 'dialog'); @@ -316,7 +316,9 @@ if (game && audio) { updateAudioControls(); account.append(audioButton); } if (saveManager) account.append(saveManager.button); if (game) { const achievements = document.createElement('button'); achievements.type = 'button'; achievements.className = 'achievement-nav'; achievements.textContent = 'Achievements'; achievements.addEventListener('click', () => { achievementDialog.showModal(); loadAchievements().catch(() => {}); }); account.append(achievements); } - const scores = document.createElement('a'); scores.href = `${rootPath}profile.html#leaderboards`; scores.textContent = 'Top scores'; account.append(scores); + const about = document.createElement('a'); about.href = `${rootPath}about.html`; about.className = 'arcade-about-link'; about.textContent = 'About'; + if (location.pathname.endsWith('/about.html')) about.setAttribute('aria-current', 'page'); + const scores = document.createElement('a'); scores.href = `${rootPath}profile.html#leaderboards`; scores.className = 'arcade-scores-link'; scores.textContent = 'Top scores'; account.append(about, scores); if (currentUser) { const profile = document.createElement('a'); profile.href = `${rootPath}profile.html`; profile.textContent = currentUser.gamertag; const logout = document.createElement('button'); logout.type = 'button'; logout.textContent = 'Sign out'; logout.addEventListener('click', async () => { await api('/api/auth/logout', { method: 'POST', body: '{}' }); currentUser = null; render(); }); diff --git a/docs/architecture.md b/docs/architecture.md index d8bb5d8..f9e9ff0 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -21,7 +21,7 @@ flowchart LR ```mermaid flowchart TB subgraph Browser - Pages[Home, profile, game pages] + Pages[Home, profile, about, game pages] Arcade[arcade.js: account, appearance, results, achievements] Events[game-events.js: browser domain events] Shared[room UI, colors, sharing] diff --git a/service-worker.js b/service-worker.js index b308f35..ae1092f 100644 --- a/service-worker.js +++ b/service-worker.js @@ -1,9 +1,11 @@ 'use strict'; -const CACHE_NAME = 'js-playground-v14'; +const CACHE_NAME = 'js-playground-v15'; const APP_SHELL = [ './', './index.html', + './about.html', + './about.css', './arcade.css', './arcade.js', './fonts/Silkscreen-Regular.ttf', diff --git a/tests/about-page.test.js b/tests/about-page.test.js new file mode 100644 index 0000000..94ed3a9 --- /dev/null +++ b/tests/about-page.test.js @@ -0,0 +1,44 @@ +'use strict'; + +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const test = require('node:test'); + +const page = fs.readFileSync('about.html', 'utf8'); +const styles = fs.readFileSync('about.css', 'utf8'); +const shell = fs.readFileSync('arcade.js', 'utf8'); +const worker = fs.readFileSync('service-worker.js', 'utf8'); + +test('shared arcade navigation exposes the about page', () => { + assert.match(shell, /about\.href = `\$\{rootPath\}about\.html`/); + assert.match(shell, /about\.setAttribute\('aria-current', 'page'\)/); +}); + +test('about page presents the owner, work, community projects, and patents', () => { + assert.match(page, /Mohamed Elsherif/); + assert.match(page, /Apple’s Services Architecture and Efficiency team/); + assert.match(page, /AskDeveloper Podcast/); + assert.match(page, /US11531532B2/); + assert.match(page, /US11956232B2/); +}); + +test('about page links to the public profile and social destinations safely', () => { + for (const destination of ['bashmohandes.com', 'askdeveloper.com', 'linkedin.com', 'github.com', 'youtube.com', 'soundcloud.com', 'tiktok.com', 'x.com', 'facebook.com']) { + assert.match(page, new RegExp(`href="https://(?:www\\.)?${destination.replace('.', '\\.')}`)); + } + const externalLinks = page.match(/ { + assert.match(page, /src="theme-init\.js"[\s\S]*href="arcade\.css"[\s\S]*href="about\.css"/); + assert.match(styles, /data-arcade-theme="cabinet"/); + assert.match(styles, /data-arcade-theme="calm"/); + assert.match(styles, /@media \(max-width:520px\)/); +}); + +test('about page is available from the offline app shell', () => { + assert.match(worker, /'\.\/about\.html'/); + assert.match(worker, /'\.\/about\.css'/); +}); diff --git a/tests/game-events.test.js b/tests/game-events.test.js index 5abc292..6676a94 100644 --- a/tests/game-events.test.js +++ b/tests/game-events.test.js @@ -62,7 +62,7 @@ test('modern controllers publish domain facts without importing audio', () => { test('shared shell notifications and extension boundaries use the event contract', () => { const shell = read('arcade.js'), guide = read('docs/game-events.md'), adr = read('docs/adr/0014-browser-domain-event-bus.md'); - for (const page of ['index.html','profile.html']) assert.match(read(page), /scripts\/game-events\.js[\s\S]*arcade\.js/); + for (const page of ['index.html','profile.html','about.html']) assert.match(read(page), /scripts\/game-events\.js[\s\S]*arcade\.js/); for (const type of ['system:theme-changed','account:user-changed','achievement:unlocked','score:top']) assert.match(shell, new RegExp(type)); assert.match(read('profile.js'), /account:user-changed/); assert.match(guide, /not a command bus, durable event log, WebSocket protocol, or trust boundary/); diff --git a/tests/online-rooms.test.js b/tests/online-rooms.test.js index 4daa980..386497e 100644 --- a/tests/online-rooms.test.js +++ b/tests/online-rooms.test.js @@ -63,7 +63,7 @@ test('online pages cache-bust shared contract changes',()=>{ assert.match(page,new RegExp(`scripts/online-rooms\\.js\\?v=${rooms.CONTRACT_VERSION}`),`${file} must request the current shared contract`); } const worker=fs.readFileSync('service-worker.js','utf8'); - assert.match(worker,/CACHE_NAME = 'js-playground-v14'/); + assert.match(worker,/CACHE_NAME = 'js-playground-v15'/); }); test('Battle Tanks ignores obsolete sockets and owns one reconnect timer',()=>{ const app=fs.readFileSync('battle-tanks/scripts/app.js','utf8'); diff --git a/tests/shared-game-assets.test.js b/tests/shared-game-assets.test.js index a7b8b3d..edd6bdc 100644 --- a/tests/shared-game-assets.test.js +++ b/tests/shared-game-assets.test.js @@ -162,6 +162,7 @@ test('result sharing preserves user activation when the preview is confirmed', ( test('every arcade page opts into shared iPhone safe-area handling', () => { const pages = [ 'index.html', + 'about.html', 'profile.html', 'pong/index.html', 'pong/classic/index.html', From 4bb4028fb0eb0ed2f29ac6271d0e3bf42d75a062 Mon Sep 17 00:00:00 2001 From: Mohamed Elsherif Date: Fri, 11 Sep 2026 20:44:49 -0700 Subject: [PATCH 5/8] Mention About page in release notes --- releases.json | 5 +++-- tests/release-notes.test.js | 6 ++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/releases.json b/releases.json index a5c807a..183d3aa 100644 --- a/releases.json +++ b/releases.json @@ -4,10 +4,11 @@ { "version": "1.2.0", "title": "JavaScript Playground 1.2.0", - "summary": "Achievements now celebrate key Tetris and Battle Tanks milestones at the moment players earn them.", + "summary": "Achievements now celebrate key Tetris and Battle Tanks milestones as they happen, and a new About page introduces the person behind the arcade.", "highlights": [ "See achievement unlocks during active play when reaching Tetris level 10, clearing four lines at once, or completing eligible Battle Tanks power-up and tactical challenges.", - "Hear the Tetris music accelerate when the tower rises above two-thirds of the playfield, then settle back to its normal tempo when the danger clears." + "Hear the Tetris music accelerate when the tower rises above two-thirds of the playfield, then settle back to its normal tempo when the danger clears.", + "Meet Mohamed Elsherif through a new game-native About page with his career, community work, patents, websites, and social profiles." ], "fixes": [ "Prevents live achievement notifications from appearing again when the final game result is recorded.", diff --git a/tests/release-notes.test.js b/tests/release-notes.test.js index 19ad5fb..9d1b363 100644 --- a/tests/release-notes.test.js +++ b/tests/release-notes.test.js @@ -29,6 +29,12 @@ test('release notes render tailored public and GitHub views', () => { assert.match(markdown, /^## Technical notes$/m); }); +test('current release notes introduce the new About page', () => { + const release = releaseForVersion(manifest, packageVersion); + assert.match(release.summary, /new About page/); + assert.ok(release.highlights.some(item => /Mohamed Elsherif[\s\S]*career[\s\S]*social profiles/.test(item))); +}); + test('build information exposes release notes only for an exact stable version', () => { assert.deepEqual(createBuildInformation({}, manifest), { version: 'dev', channel: 'dev', release: null }); assert.deepEqual(createBuildInformation({ BUILD_VERSION: 'sha-1234567', BUILD_CHANNEL: 'alpha' }, manifest), { version: 'sha-1234567', channel: 'alpha', release: null }); From d87545bb2d5defd76c136bea3f4e71bf8b5284f2 Mon Sep 17 00:00:00 2001 From: Mohamed Elsherif Date: Fri, 11 Sep 2026 20:46:12 -0700 Subject: [PATCH 6/8] Prepare 1.2.5 release notes --- package-lock.json | 4 ++-- package.json | 2 +- releases.json | 20 +++++++++++++++++--- tests/release-notes.test.js | 2 ++ 4 files changed, 22 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 30f053d..bb7986c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "javascript-playground", - "version": "1.2.0", + "version": "1.2.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "javascript-playground", - "version": "1.2.0", + "version": "1.2.5", "dependencies": { "ws": "8.21.3" }, diff --git a/package.json b/package.json index aaeea2e..45d23d0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "javascript-playground", - "version": "1.2.0", + "version": "1.2.5", "private": true, "description": "Browser games with authoritative online multiplayer", "main": "server/index.js", diff --git a/releases.json b/releases.json index 183d3aa..e2dea69 100644 --- a/releases.json +++ b/releases.json @@ -1,14 +1,28 @@ { "schemaVersion": 1, "releases": [ + { + "version": "1.2.5", + "title": "JavaScript Playground 1.2.5", + "summary": "A new About page introduces Mohamed Elsherif and connects the arcade with his work across software architecture and the developer community.", + "highlights": [ + "Meet Mohamed Elsherif through a game-native profile of his career, community work, granted patents, websites, and social profiles.", + "Open the About page from the shared arcade navigation on phones, tablets, and desktops." + ], + "fixes": [ + "Keeps the About link available in compact phone navigation while moving the less essential Top scores shortcut out of the narrow layout." + ], + "technical": [ + "Adds the responsive About page to every experience theme and the offline application shell, with regression coverage for content, links, navigation, and safe-area behavior." + ] + }, { "version": "1.2.0", "title": "JavaScript Playground 1.2.0", - "summary": "Achievements now celebrate key Tetris and Battle Tanks milestones as they happen, and a new About page introduces the person behind the arcade.", + "summary": "Achievements now celebrate key Tetris and Battle Tanks milestones at the moment players earn them.", "highlights": [ "See achievement unlocks during active play when reaching Tetris level 10, clearing four lines at once, or completing eligible Battle Tanks power-up and tactical challenges.", - "Hear the Tetris music accelerate when the tower rises above two-thirds of the playfield, then settle back to its normal tempo when the danger clears.", - "Meet Mohamed Elsherif through a new game-native About page with his career, community work, patents, websites, and social profiles." + "Hear the Tetris music accelerate when the tower rises above two-thirds of the playfield, then settle back to its normal tempo when the danger clears." ], "fixes": [ "Prevents live achievement notifications from appearing again when the final game result is recorded.", diff --git a/tests/release-notes.test.js b/tests/release-notes.test.js index 9d1b363..3f919cb 100644 --- a/tests/release-notes.test.js +++ b/tests/release-notes.test.js @@ -31,8 +31,10 @@ test('release notes render tailored public and GitHub views', () => { test('current release notes introduce the new About page', () => { const release = releaseForVersion(manifest, packageVersion); + assert.equal(release.version, '1.2.5'); assert.match(release.summary, /new About page/); assert.ok(release.highlights.some(item => /Mohamed Elsherif[\s\S]*career[\s\S]*social profiles/.test(item))); + assert.doesNotMatch(releaseForVersion(manifest, '1.2.0').summary, /About page/); }); test('build information exposes release notes only for an exact stable version', () => { From d4e098477ce3f1a1202527c1e9d5a56b97ab4a9b Mon Sep 17 00:00:00 2001 From: Mohamed Elsherif Date: Fri, 11 Sep 2026 20:54:10 -0700 Subject: [PATCH 7/8] Address About page accessibility review --- about.css | 17 ++++++++++------- tests/about-page.test.js | 25 +++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/about.css b/about.css index e577ebb..921c758 100644 --- a/about.css +++ b/about.css @@ -7,18 +7,21 @@ --line:#192541; --accent:#765cff; --pink:#ff4fa3; + --pink-text:#a31558; --mint:#6ef2d0; --yellow:#ffd84d; --sky:#56a8ff; + --focus:#4b2fc9; + --link-accent:#4b2fc9; --shadow:#192541; } -:root[data-theme="dark"] { color-scheme:dark;--page:#0a1020;--ink:#fff4d6;--muted:#b7c2dc;--panel:#111a33;--line:#fff4d6;--shadow:#030712; } +:root[data-color-mode="dark"] { color-scheme:dark;--page:#0a1020;--ink:#fff4d6;--muted:#b7c2dc;--panel:#111a33;--line:#fff4d6;--pink-text:#ff70b7;--focus:#6ef2d0;--link-accent:#ffd84d;--shadow:#030712; } * { box-sizing:border-box; } body { min-height:100vh;margin:0;color:var(--ink);background:linear-gradient(90deg,#ffffff06 1px,transparent 1px),linear-gradient(#ffffff06 1px,transparent 1px),radial-gradient(circle at 8% 0,#765cff38,transparent 34rem),var(--page);background-size:32px 32px,32px 32px,auto,auto;font-family:var(--pixel-body,ui-monospace,"SFMono-Regular",Consolas,monospace);overflow-x:hidden; } a { color:inherit; } .about-shell { width:min(1120px,calc(100% - 40px));margin:0 auto;padding:clamp(38px,6vw,78px) 0 72px; } .about-hero { display:grid;grid-template-columns:minmax(0,1.45fr) minmax(280px,.55fr);gap:clamp(32px,6vw,76px);align-items:center;margin-bottom:52px; } -.eyebrow,.panel-kicker { margin:0 0 18px;color:var(--pink);font:700 12px/1.2 var(--pixel-font,ui-monospace,monospace);letter-spacing:.1em;text-transform:uppercase; } +.eyebrow,.panel-kicker { margin:0 0 18px;color:var(--pink-text);font:700 12px/1.2 var(--pixel-font,ui-monospace,monospace);letter-spacing:.1em;text-transform:uppercase; } .eyebrow { display:inline-block;padding:8px 12px;color:#192541;background:var(--yellow);border:2px solid var(--line);box-shadow:4px 4px 0 var(--line); } h1,h2,.player-card strong,.button,.resource-links span,.social-grid strong { font-family:var(--pixel-font,ui-monospace,monospace); } h1 { max-width:740px;margin:0;font-size:clamp(40px,7vw,76px);font-weight:700;letter-spacing:-.065em;line-height:1.08;text-wrap:balance;text-shadow:5px 5px 0 var(--accent); } @@ -29,10 +32,10 @@ h1 span { color:var(--pink); } .button-primary { color:#192541;background:var(--mint); } .button-secondary { background:var(--panel); } .button:hover { transform:translate(4px,4px);box-shadow:0 0 0 var(--line); } -.button:focus-visible,.resource-links a:focus-visible,.patent-list a:focus-visible,.social-grid a:focus-visible { outline:3px solid var(--mint);outline-offset:4px; } +.button:focus-visible,.resource-links a:focus-visible,.patent-list a:focus-visible,.social-grid a:focus-visible { outline:3px solid var(--focus);outline-offset:4px; } .player-card { position:relative;padding:28px;background:var(--panel);border:3px solid var(--line);box-shadow:8px 8px 0 var(--pink),12px 12px 0 var(--line);transform:rotate(1.5deg); } .player-card::after { position:absolute;top:14px;right:14px;width:42px;height:42px;background:linear-gradient(90deg,var(--yellow) 0 30%,transparent 30% 70%,var(--yellow) 70%),linear-gradient(var(--yellow) 0 30%,transparent 30% 70%,var(--yellow) 70%);content:"";opacity:.8; } -.player-label { margin:0 0 10px;color:var(--pink);font-size:11px;font-weight:800;letter-spacing:.12em;text-transform:uppercase; } +.player-label { margin:0 0 10px;color:var(--pink-text);font-size:11px;font-weight:800;letter-spacing:.12em;text-transform:uppercase; } .player-card>strong { display:block;padding-right:46px;font-size:clamp(19px,3vw,27px); } .player-card dl { display:grid;gap:0;margin:24px 0 0; } .player-card dl div { display:grid;grid-template-columns:90px 1fr;gap:12px;padding:12px 0;border-top:1px solid color-mix(in srgb,var(--line) 22%,transparent); } @@ -60,7 +63,7 @@ h1 span { color:var(--pink); } .patent-list a { display:flex;flex-direction:column;gap:10px;min-height:170px;padding:18px;background:color-mix(in srgb,var(--yellow) 12%,transparent);border:2px solid var(--line);text-decoration:none; } .patent-list small { color:var(--muted);font-size:11px;font-weight:800;text-transform:uppercase; } .patent-list span { flex:1;font-size:15px;font-weight:800;line-height:1.45; } -.patent-list b { color:var(--accent);font-size:12px; } +.patent-list b { color:var(--link-accent);font-size:12px; } .patent-list i { font-style:normal; } .social-section { margin-top:58px; } .social-grid { display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px;margin-top:24px; } @@ -73,8 +76,8 @@ h1 span { color:var(--pink); } .social-grid small { color:var(--muted);font-size:11px; } :root[data-arcade-theme="cabinet"] .about-shell { width:min(1240px,calc(100% - 32px)); } :root[data-arcade-theme="cabinet"] :is(.player-card,.panel) { box-shadow:5px 5px 0 var(--yellow); } -:root[data-arcade-theme="calm"] { --page:#eef4f1;--ink:#223a33;--muted:#61746d;--panel:#f8fbf9;--line:#527068;--accent:#356f61;--pink:#9b5548;--mint:#789d90;--yellow:#d9b968;--sky:#668e9b;--shadow:#263c34; } -:root[data-arcade-theme="calm"][data-color-mode="dark"] { --page:#111917;--ink:#e5efeb;--muted:#9fb0a9;--panel:#18231f;--line:#91a59d;--shadow:#000; } +:root[data-arcade-theme="calm"] { --page:#eef4f1;--ink:#223a33;--muted:#61746d;--panel:#f8fbf9;--line:#527068;--accent:#356f61;--pink:#9b5548;--pink-text:#854238;--mint:#789d90;--yellow:#d9b968;--sky:#668e9b;--focus:#315f54;--link-accent:#315f54;--shadow:#263c34; } +:root[data-arcade-theme="calm"][data-color-mode="dark"] { --page:#111917;--ink:#e5efeb;--muted:#9fb0a9;--panel:#18231f;--line:#91a59d;--pink-text:#e8a092;--focus:#d9b968;--link-accent:#d9b968;--shadow:#000; } :root[data-arcade-theme="calm"] body { background:var(--page);font-family:Georgia,"Times New Roman",serif; } :root[data-arcade-theme="calm"] .about-shell { width:min(1020px,calc(100% - 48px)); } :root[data-arcade-theme="calm"] :is(h1,h2,.player-card strong,.button,.resource-links span,.social-grid strong) { font-family:Georgia,"Times New Roman",serif; } diff --git a/tests/about-page.test.js b/tests/about-page.test.js index 94ed3a9..2ca7448 100644 --- a/tests/about-page.test.js +++ b/tests/about-page.test.js @@ -9,6 +9,15 @@ const styles = fs.readFileSync('about.css', 'utf8'); const shell = fs.readFileSync('arcade.js', 'utf8'); const worker = fs.readFileSync('service-worker.js', 'utf8'); +function contrast(left, right) { + const luminance = color => { + const channels = color.slice(1).match(/../g).map(value => parseInt(value, 16) / 255).map(value => value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4); + return 0.2126 * channels[0] + 0.7152 * channels[1] + 0.0722 * channels[2]; + }; + const values = [luminance(left), luminance(right)]; + return (Math.max(...values) + 0.05) / (Math.min(...values) + 0.05); +} + test('shared arcade navigation exposes the about page', () => { assert.match(shell, /about\.href = `\$\{rootPath\}about\.html`/); assert.match(shell, /about\.setAttribute\('aria-current', 'page'\)/); @@ -38,6 +47,22 @@ test('about page supports every experience theme and phone layouts', () => { assert.match(styles, /@media \(max-width:520px\)/); }); +test('about page uses the current color-mode contract and contrast-safe text and focus colors', () => { + assert.match(styles, /:root\[data-color-mode="dark"\]/); + assert.doesNotMatch(styles, /data-theme=/); + assert.match(styles, /color:var\(--pink-text\)/); + assert.match(styles, /outline:3px solid var\(--focus\)/); + assert.match(styles, /\.patent-list b \{ color:var\(--link-accent\)/); + for (const [foreground, background, minimum] of [ + ['#a31558', '#fff9e8', 4.5], + ['#4b2fc9', '#fff9e8', 3], + ['#315f54', '#f8fbf9', 4.5], + ['#ff70b7', '#111a33', 4.5], + ['#6ef2d0', '#111a33', 3], + ['#d9b968', '#18231f', 4.5] + ]) assert.ok(contrast(foreground, background) >= minimum, `${foreground} should meet ${minimum}:1 against ${background}`); +}); + test('about page is available from the offline app shell', () => { assert.match(worker, /'\.\/about\.html'/); assert.match(worker, /'\.\/about\.css'/); From aa7f654079fcce2291d4ca8eca505063f9512dc6 Mon Sep 17 00:00:00 2001 From: Mohamed Elsherif Date: Fri, 11 Sep 2026 21:03:18 -0700 Subject: [PATCH 8/8] Improve About hero contrast --- about.css | 9 +++++---- tests/about-page.test.js | 2 ++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/about.css b/about.css index 921c758..4d12bf3 100644 --- a/about.css +++ b/about.css @@ -7,6 +7,7 @@ --line:#192541; --accent:#765cff; --pink:#ff4fa3; + --pink-heading:#e13a87; --pink-text:#a31558; --mint:#6ef2d0; --yellow:#ffd84d; @@ -15,7 +16,7 @@ --link-accent:#4b2fc9; --shadow:#192541; } -:root[data-color-mode="dark"] { color-scheme:dark;--page:#0a1020;--ink:#fff4d6;--muted:#b7c2dc;--panel:#111a33;--line:#fff4d6;--pink-text:#ff70b7;--focus:#6ef2d0;--link-accent:#ffd84d;--shadow:#030712; } +:root[data-color-mode="dark"] { color-scheme:dark;--page:#0a1020;--ink:#fff4d6;--muted:#b7c2dc;--panel:#111a33;--line:#fff4d6;--pink-heading:#ff70b7;--pink-text:#ff70b7;--focus:#6ef2d0;--link-accent:#ffd84d;--shadow:#030712; } * { box-sizing:border-box; } body { min-height:100vh;margin:0;color:var(--ink);background:linear-gradient(90deg,#ffffff06 1px,transparent 1px),linear-gradient(#ffffff06 1px,transparent 1px),radial-gradient(circle at 8% 0,#765cff38,transparent 34rem),var(--page);background-size:32px 32px,32px 32px,auto,auto;font-family:var(--pixel-body,ui-monospace,"SFMono-Regular",Consolas,monospace);overflow-x:hidden; } a { color:inherit; } @@ -25,7 +26,7 @@ a { color:inherit; } .eyebrow { display:inline-block;padding:8px 12px;color:#192541;background:var(--yellow);border:2px solid var(--line);box-shadow:4px 4px 0 var(--line); } h1,h2,.player-card strong,.button,.resource-links span,.social-grid strong { font-family:var(--pixel-font,ui-monospace,monospace); } h1 { max-width:740px;margin:0;font-size:clamp(40px,7vw,76px);font-weight:700;letter-spacing:-.065em;line-height:1.08;text-wrap:balance;text-shadow:5px 5px 0 var(--accent); } -h1 span { color:var(--pink); } +h1 span { color:var(--pink-heading); } .lede { max-width:720px;margin:28px 0 0;color:var(--muted);font-size:clamp(17px,2vw,20px);line-height:1.65; } .hero-actions { display:flex;flex-wrap:wrap;gap:14px;margin-top:30px; } .button { display:inline-flex;align-items:center;justify-content:center;gap:9px;min-height:48px;padding:10px 16px;border:2px solid var(--line);font-size:11px;font-weight:700;text-decoration:none;box-shadow:4px 4px 0 var(--line);transition:transform 140ms steps(2,end),box-shadow 140ms steps(2,end); } @@ -76,8 +77,8 @@ h1 span { color:var(--pink); } .social-grid small { color:var(--muted);font-size:11px; } :root[data-arcade-theme="cabinet"] .about-shell { width:min(1240px,calc(100% - 32px)); } :root[data-arcade-theme="cabinet"] :is(.player-card,.panel) { box-shadow:5px 5px 0 var(--yellow); } -:root[data-arcade-theme="calm"] { --page:#eef4f1;--ink:#223a33;--muted:#61746d;--panel:#f8fbf9;--line:#527068;--accent:#356f61;--pink:#9b5548;--pink-text:#854238;--mint:#789d90;--yellow:#d9b968;--sky:#668e9b;--focus:#315f54;--link-accent:#315f54;--shadow:#263c34; } -:root[data-arcade-theme="calm"][data-color-mode="dark"] { --page:#111917;--ink:#e5efeb;--muted:#9fb0a9;--panel:#18231f;--line:#91a59d;--pink-text:#e8a092;--focus:#d9b968;--link-accent:#d9b968;--shadow:#000; } +:root[data-arcade-theme="calm"] { --page:#eef4f1;--ink:#223a33;--muted:#61746d;--panel:#f8fbf9;--line:#527068;--accent:#356f61;--pink:#9b5548;--pink-heading:#9b5548;--pink-text:#854238;--mint:#789d90;--yellow:#d9b968;--sky:#668e9b;--focus:#315f54;--link-accent:#315f54;--shadow:#263c34; } +:root[data-arcade-theme="calm"][data-color-mode="dark"] { --page:#111917;--ink:#e5efeb;--muted:#9fb0a9;--panel:#18231f;--line:#91a59d;--pink-heading:#e8a092;--pink-text:#e8a092;--focus:#d9b968;--link-accent:#d9b968;--shadow:#000; } :root[data-arcade-theme="calm"] body { background:var(--page);font-family:Georgia,"Times New Roman",serif; } :root[data-arcade-theme="calm"] .about-shell { width:min(1020px,calc(100% - 48px)); } :root[data-arcade-theme="calm"] :is(h1,h2,.player-card strong,.button,.resource-links span,.social-grid strong) { font-family:Georgia,"Times New Roman",serif; } diff --git a/tests/about-page.test.js b/tests/about-page.test.js index 2ca7448..3fbf006 100644 --- a/tests/about-page.test.js +++ b/tests/about-page.test.js @@ -50,10 +50,12 @@ test('about page supports every experience theme and phone layouts', () => { test('about page uses the current color-mode contract and contrast-safe text and focus colors', () => { assert.match(styles, /:root\[data-color-mode="dark"\]/); assert.doesNotMatch(styles, /data-theme=/); + assert.match(styles, /h1 span \{ color:var\(--pink-heading\)/); assert.match(styles, /color:var\(--pink-text\)/); assert.match(styles, /outline:3px solid var\(--focus\)/); assert.match(styles, /\.patent-list b \{ color:var\(--link-accent\)/); for (const [foreground, background, minimum] of [ + ['#e13a87', '#fff4d6', 3], ['#a31558', '#fff9e8', 4.5], ['#4b2fc9', '#fff9e8', 3], ['#315f54', '#f8fbf9', 4.5],