From e9ccd9bc9837849a9b2696321faf2f64d0b005ab Mon Sep 17 00:00:00 2001 From: npeter83 Date: Fri, 25 Sep 2026 14:20:23 +0200 Subject: [PATCH] =?UTF-8?q?fix(player):=20the=20YouTube=20player's=20full?= =?UTF-8?q?=20window=20filled=20only=20its=20card=20=E2=80=94=20a=20blurre?= =?UTF-8?q?d=20ancestor=20trapped=20the=20fixed=20stage=20(E28)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit E28 S3d1 moved the modal's card from `.glass-card` (no blur) to the `.glass` panel role, whose `backdrop-filter` makes the card the containing block of the fixed "full window" stage: F showed a 894×161 strip inside the card with a scrolling, wrapped control bar. While the stage is maximised the card now drops its blur (`html .glass:has(> .player-stage--max)`, outranking the dark theme's rule); measured live: the stage 1268×861 = the viewport, the blur back on exit. --- frontend/src/index.css | 8 ++++++++ frontend/src/lib/playerStage.contract.test.ts | 19 +++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 frontend/src/lib/playerStage.contract.test.ts diff --git a/frontend/src/index.css b/frontend/src/index.css index 76f93eba..ec252b89 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1071,6 +1071,14 @@ html[data-theme="light"] { here escapes it — a higher level would claim an authority this layer neither has nor needs. */ z-index: var(--z-index-rail); } +/* …and it only fills the viewport while no ancestor has a filter: a `backdrop-filter` makes that + ancestor the fixed stage's containing block. The modal's card is a blurred `.glass` panel (E28 S3d1), + so while the stage is maximised the card drops its blur — the stage covers the card anyway. `html ` + outranks `html[data-theme="dark"] .glass`, which sets the blur again (lib/playerStage.contract.test.ts). */ +html .glass:has(> .player-stage--max) { + backdrop-filter: none; + -webkit-backdrop-filter: none; +} /* Thin, theme-aware scrollbars */ * { diff --git a/frontend/src/lib/playerStage.contract.test.ts b/frontend/src/lib/playerStage.contract.test.ts new file mode 100644 index 00000000..4d8002aa --- /dev/null +++ b/frontend/src/lib/playerStage.contract.test.ts @@ -0,0 +1,19 @@ +import { readFileSync } from "node:fs"; + +import { expect, it } from "vitest"; + +// The YouTube player's "full window" (F) is a `position: fixed; inset: 0` stage INSIDE the modal's card. +// A fixed element is sized by the viewport only while no ancestor has a filter: a `backdrop-filter` +// makes that ancestor its containing block. E28 S3d1 moved the card from `.glass-card` (no blur) to +// `.glass` (blurred), and the full window shrank to a 894×161 strip inside the card (E28 audit, seen +// live). jsdom does no layout, so what is pinned is the escape rule: while the stage is maximised, the +// card drops its blur, with a selector that outranks `html[data-theme="dark"] .glass` (0,2,1). +const css = readFileSync(new URL("../index.css", import.meta.url), "utf8"); + +it("a maximised player stage takes the blur off the glass card it sits in", () => { + const at = css.indexOf("html .glass:has(> .player-stage--max)"); + expect(at, "the escape rule").toBeGreaterThan(css.indexOf('html[data-theme="dark"] .glass')); + const body = css.slice(at, css.indexOf("}", at)); + expect(body).toMatch(/(^|[\s;{])backdrop-filter:\s*none/); + expect(body).toMatch(/-webkit-backdrop-filter:\s*none/); +});