diff --git a/index-1440.png b/index-1440.png deleted file mode 100644 index e5a43297..00000000 Binary files a/index-1440.png and /dev/null differ diff --git a/web/scripts/check-build.mjs b/web/scripts/check-build.mjs index 30e533c7..c43ba4ce 100644 --- a/web/scripts/check-build.mjs +++ b/web/scripts/check-build.mjs @@ -68,10 +68,45 @@ if (!/\.card\{[^}]*min-width:0/.test(css.replace(/\s+/g, ""))) { // --color-bg does not exist in this theme; the page paints --color-ink. { const flat = css.replace(/\s+/g, ""); - if (!/\.cmp[^{]*first-child\)?\{[^}]*position:sticky/.test(flat)) { - fail.push("comparison tables have no sticky first column — the row label scrolls away from its Yes/No cells on mobile"); - } else if (/\.cmp[^{]*first-child\)?\{[^}]*background:var\(--color-bg\)/.test(flat)) { - fail.push("sticky comparison column uses `--color-bg`, which this theme does not define — it resolves to transparent and cells scroll under the label"); + if (!/first-child\)?\{[^}]*position:sticky/.test(flat)) { + fail.push("wide tables have no sticky first column — the row label scrolls away from its cells on mobile"); + } else if (/first-child\)?\{[^}]*background:var\(--color-bg\)/.test(flat)) { + fail.push("sticky column uses `--color-bg`, which this theme does not define — it resolves to transparent and cells scroll under the label"); + } + /* BOTH class names, because the fix was applied to one and not the other. + .cmp got it; .patch -- the interactive routing matrix on the home page, the + worse case at 403px of 736 hidden -- was missed for a full commit. A check + naming only the class that was fixed would have passed throughout. */ + for (const cls of ["cmp", "patch"]) { + if (!new RegExp(`\\.${cls}[,)][^{]*first-child`).test(flat)) { + fail.push(`.${cls} tables are not in the sticky-first-column rule — their label column scrolls away on mobile`); + } + } + /* TWO PSEUDO-ELEMENT AFFORDANCES, and both need a haystack wider than the + CSS bundle plus a pattern looser than the source spelling. + + WHERE. Astro inlines a component's scoped styles into the pages that use + it, so `.shot-open`'s rule is in features.html and in NO file under + _astro/. A check reading only the bundle reports it missing on a build + where it is present and working -- which is how the first version of this + check failed, against a glyph already verified in a browser. + + HOW SPELLED. Lightning CSS rewrites `::after` to the CSS2 `:after`, which + is two bytes shorter and identical in meaning. Matching the source + spelling finds nothing in the output. `::?after` accepts either. */ + const built = flat + readdirSync(DIST) + .filter((f) => f.endsWith(".html")) + .map((f) => readFileSync(join(DIST, f), "utf8").replace(/\s+/g, "")) + .join(""); + + if (!/\.scroll-hint::?after\{/.test(built)) { + fail.push("no `.scroll-hint::after` in the built output — wide tables give no sign they scroll on mobile"); + } + /* The lightbox expand glyph. `cursor: zoom-in` is the only other affordance + and it does not exist on a touch device -- which is the viewport where the + screenshots are smallest and expanding them matters most. */ + if (!/\.shot-open::?after\{/.test(built)) { + fail.push("no `.shot-open::after` in the built output — the lightbox is undiscoverable on touch, where `cursor: zoom-in` means nothing"); } } diff --git a/web/src/components/Lightbox.astro b/web/src/components/Lightbox.astro index 7528b1c4..aecaf6b2 100644 --- a/web/src/components/Lightbox.astro +++ b/web/src/components/Lightbox.astro @@ -107,12 +107,48 @@ does not change the page's appearance -- only its behaviour. */ :global(.shot-open) { display: block; + position: relative; width: 100%; padding: 0; border: 0; background: none; cursor: zoom-in; } + + /* A VISIBLE GLYPH, because `cursor: zoom-in` says nothing to a phone. + + A touch device has no pointer to change shape, so on the viewport where + these images are SMALLEST the affordance was entirely absent: measured at + 390px each 2880x1800 capture renders at 335x210 CSS px, an 8.6x reduction, + and nothing on screen suggested it could be opened. The one reader who most + needs to expand the picture was the one reader never told they could. + + Drawn in the corner rather than centred so it never covers the part of the + screenshot someone is trying to read, and always present rather than + hover-only -- a hover-revealed hint has the same problem as the cursor. */ + :global(.shot-open::after) { + content: ""; + position: absolute; + right: 0.625rem; + bottom: 0.625rem; + width: 1.75rem; + height: 1.75rem; + border: 1px solid var(--color-line-strong, #39435a); + border-radius: 0.5rem; + background-color: rgb(11 13 17 / 0.72); + /* The two-arrow expand mark, inline so it costs no request. currentColor is + not available to a background image, hence the literal stroke. */ + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='%23edf2f8' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 4h4v4M8 16H4v-4M16 4l-5 5M4 16l5-5'/%3E%3C/svg%3E"); + background-position: center; + background-repeat: no-repeat; + background-size: 1rem 1rem; + backdrop-filter: blur(4px); + transition: background-color 140ms ease; + pointer-events: none; + } + :global(.shot-open:hover::after) { + background-color: rgb(11 13 17 / 0.9); + } :global(.shot-open:focus-visible) { outline: 2px solid var(--color-primary-bright, #78a6e8); outline-offset: 3px; diff --git a/web/src/components/MixMatrix.astro b/web/src/components/MixMatrix.astro index fdcad5dc..453e9796 100644 --- a/web/src/components/MixMatrix.astro +++ b/web/src/components/MixMatrix.astro @@ -50,7 +50,7 @@ const DESTS = [
-
+
Routing matrix: which ingest audio tracks each destination receives diff --git a/web/src/pages/comparison.astro b/web/src/pages/comparison.astro index eaff0c9b..001c13e8 100644 --- a/web/src/pages/comparison.astro +++ b/web/src/pages/comparison.astro @@ -127,7 +127,7 @@ const selectors = [ One axis, and it is the one this project exists for. The shared ground — everything all three do — is in the table below this one.

-
+
@@ -215,7 +215,7 @@ const selectors = [ plainly — it means the choice between these three comes down to the audio, and not to whether the fundamentals are there.

-
+
diff --git a/web/src/pages/features.astro b/web/src/pages/features.astro index 5bb551c9..4ee7882c 100644 --- a/web/src/pages/features.astro +++ b/web/src/pages/features.astro @@ -168,69 +168,44 @@ const sections = [ they select an audio track per destination rather than mix one.

-
-
- - - - - - - - - - { - [ - ["Per-destination audio mix from one multitrack ingest", "Yes", "No", "No"], - ["Channel-level mix matrix with per-cell gain", "Yes", "No", "No"], - ["Per-destination loudness target, measured after routing", "Yes", "No", "No"], - ["Multitrack archive — every ingest track preserved, stream-copied", "Yes", "No", "No"], - ["Per-track stems as 24-bit WAV or FLAC, segment-aligned", "Yes", "No", "No"], - ["Track annotations — what each incoming track actually is", "Yes", "No", "No"], - ["Typed SRT rejections — the publisher is told why it was refused", "Yes", "No", "n/a"], - /* Label kept character-for-character identical to the copy in - comparison.astro. It used to read "Runs on your hardware" - here and "Runs on hardware you control" there, which is the - drift this table has already produced twice — and - check-build.mjs now fails the build if the two disagree. */ - ["Runs on hardware you control, no per-destination pricing", "Yes", "Yes", "No"], - ].map(([cap, poly, rs, ri]) => ( - - - - - - - )) - } - -
Capability - polyemesis - - Restreamer - - restream.io -
{cap}{poly} - {rs} - - {ri} -
-
+ {/* THE FULL TABLE USED TO BE REPEATED HERE, all eight rows of it, and it + was character-for-character identical to the primary table on + /comparison -- 9 of 9 rows, verified. -

- All three restream one input to many platforms. The difference is what - happens to the audio on the way: polyemesis is the only one that lets - this platform take the clean mix while that one takes - the full mix, from a single upload and without re-encoding the picture. -

-

- - The full comparison → - -

+ It cost authority rather than bytes. A reader who studied it here and + met the same table again on /comparison learned that the page named + for the comparison had nothing further to offer, which is the wrong + thing to teach about the page doing the arguing. Keeping two copies + honest also needed a build check, and that check existed because they + had already drifted twice. + + What is left is the one row that is actually the product's thesis, and + a door to the page that carries the evidence. The claim still appears + on this page; the argument now lives in exactly one place. */} +
+

+ The row that matters +

+

+ Per-destination audio mix from one multitrack ingest. +

+

+ polyemesis Yes · + Restreamer No · + restream.io No +

+

+ All three restream one input to many platforms. The difference is what + happens to the audio on the way: polyemesis is the only one that lets + this platform take the clean mix while that one takes + the full mix, from a single upload and without re-encoding the picture. +

+

+ + All eight capabilities, against five products → + +

+
diff --git a/web/src/styles/global.css b/web/src/styles/global.css index 0e309eb9..8d9024c8 100644 --- a/web/src/styles/global.css +++ b/web/src/styles/global.css @@ -216,7 +216,20 @@ Pinning the first column keeps the question on screen next to the answer. An opaque background is required, not decorative: without it the scrolling cells slide visibly underneath the label. */ - .cmp :is(th, td):first-child { + /* EVERY wide table, not just the comparison ones. + * + * This started as a fix for /comparison and was measured afterwards on the + * other two: the routing matrix on the home page hides 403px of 736 (55%) at + * a 390px viewport, and it is the worse case of the two. That table is + * INTERACTIVE -- you scroll right to reach a destination column and click a + * crosspoint -- so losing the track label means clicking a cell without + * knowing which of six tracks it belongs to. + * + * The background is a variable because the two tables sit on different + * grounds: the comparison tables on the page, the matrix inside a + * surface-filled card. An opaque background is required either way, and the + * wrong opaque colour is as visible as none. */ + :is(.cmp, .patch) :is(th, td):first-child { position: sticky; left: 0; z-index: 1; @@ -224,17 +237,22 @@ rather than the role, so the obvious guess (--color-bg) does not exist and silently resolves to transparent -- which still LOOKS like a working sticky column until something scrolls underneath it. */ - background: var(--color-ink); + background: var(--sticky-bg, var(--color-ink)); } /* The head row paints its own surface, so its pinned cell has to match that rather than the page, or the corner reads as a hole in the header. */ .cmp thead :is(th, td):first-child { background: var(--color-surface); } + /* The matrix is inside a card that is already surface-coloured, so both its + head and its body cells take that instead of the page colour. */ + .patch { + --sticky-bg: var(--color-surface); + } /* The edge that says "this column is pinned, the rest moved". Drawn with a shadow rather than a border so it costs no layout width and cannot push the table wider -- which is the problem one row up in this file. */ - .cmp :is(th, td):first-child::after { + :is(.cmp, .patch) :is(th, td):first-child::after { content: ""; position: absolute; inset-block: 0; @@ -243,6 +261,35 @@ background: var(--color-line); } + /* THE TABLE GIVES NO SIGN IT SCROLLS, so this draws one. + * + * macOS and iOS both hide overlay scrollbars until a scroll is already in + * progress, which means the affordance only appears once you have performed + * the action it exists to suggest. Measured at 390px, the visible frame shows + * 348 of 736px -- 47% -- and the other 53% is undiscoverable. + * + * A fade at the right edge is the conventional answer and it reads without + * instructions: content that dissolves rather than ending says it continues. + * + * Scoped by width rather than by measuring overflow, because every table + * carrying this is min-width 46rem and therefore always overflows below it. + * A JS-measured version would be exact and would also be one more script for + * a hint that is already correct at every width it applies to. */ + @media (max-width: 48rem) { + .scroll-hint { + position: relative; + } + .scroll-hint::after { + content: ""; + position: absolute; + inset-block: 0; + right: 0; + width: 2.5rem; + pointer-events: none; + background: linear-gradient(to right, rgb(11 13 17 / 0), var(--sticky-bg, var(--color-ink))); + } + } + .chip { display: inline-flex; align-items: center;