Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file removed index-1440.png
Binary file not shown.
43 changes: 39 additions & 4 deletions web/scripts/check-build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,45 @@
// --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)) {

Check warning on line 81 in web/scripts/check-build.mjs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

`String.raw` should be used to avoid escaping `\`.

See more on https://sonarcloud.io/project/issues?id=rainmanjam_polyemesis&issues=AZ_-qgWhMHc42Ihu5ZZ3&open=AZ_-qgWhMHc42Ihu5ZZ3&pullRequest=337
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");
}
}

Expand Down
36 changes: 36 additions & 0 deletions web/src/components/Lightbox.astro
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion web/src/components/MixMatrix.astro
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ const DESTS = [
</div>

<div class="mt-10 overflow-hidden rounded-token border border-line bg-surface">
<div class="overflow-x-auto">
<div class="scroll-hint overflow-x-auto" style="--sticky-bg: var(--color-surface)">
<table class="patch w-full min-w-[46rem] border-collapse text-sm">
<caption class="sr-only">
Routing matrix: which ingest audio tracks each destination receives
Expand Down
4 changes: 2 additions & 2 deletions web/src/pages/comparison.astro
Original file line number Diff line number Diff line change
Expand Up @@ -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.
</p>
<div class="mt-6 overflow-x-auto rounded-token border border-line">
<div class="scroll-hint mt-6 overflow-x-auto rounded-token border border-line">
<table class="cmp w-full min-w-[46rem] border-collapse text-sm">
<thead class="bg-surface">
<tr class="border-b border-line">
Expand Down Expand Up @@ -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.
</p>
<div class="mt-6 overflow-x-auto rounded-token border border-line">
<div class="scroll-hint mt-6 overflow-x-auto rounded-token border border-line">
<table class="cmp w-full min-w-[46rem] border-collapse text-sm">
<thead class="bg-surface">
<tr class="border-b border-line">
Expand Down
99 changes: 37 additions & 62 deletions web/src/pages/features.astro
Original file line number Diff line number Diff line change
Expand Up @@ -168,69 +168,44 @@ const sections = [
they select an audio track per destination rather than mix one.
</p>

<div class="mt-8 overflow-x-auto">
<table class="w-full min-w-[38rem] border-collapse text-left text-sm">
<thead>
<tr class="border-b border-line-strong">
<th class="py-3 pr-4 font-medium text-fg">Capability</th>
<th class="w-28 py-3 pr-4 font-mono text-[11px] font-medium uppercase tracking-wider text-primary">
polyemesis
</th>
<th class="w-28 py-3 pr-4 font-mono text-[11px] font-medium uppercase tracking-wider text-subtle">
Restreamer
</th>
<th class="w-28 py-3 font-mono text-[11px] font-medium uppercase tracking-wider text-subtle">
restream.io
</th>
</tr>
</thead>
<tbody>
{
[
["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]) => (
<tr class="border-b border-line">
<td class="py-3 pr-4 leading-relaxed text-fg">{cap}</td>
<td class:list={[
"py-3 pr-4 font-mono text-[13px]",
poly === "Yes" ? "text-live" : "text-subtle",
]}>{poly}</td>
<td class:list={["py-3 pr-4 font-mono text-[13px]", rs === "Yes" ? "text-fg" : "text-subtle"]}>
{rs}
</td>
<td class:list={["py-3 font-mono text-[13px]", ri === "Yes" ? "text-fg" : "text-subtle"]}>
{ri}
</td>
</tr>
))
}
</tbody>
</table>
</div>
{/* 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.

<p class="mt-6 max-w-2xl text-sm leading-relaxed text-muted">
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
<em>this</em> platform take the clean mix while <em>that</em> one takes
the full mix, from a single upload and without re-encoding the picture.
</p>
<p class="mt-5">
<a href="/comparison" class="text-sm text-primary underline-offset-4 hover:underline">
The full comparison →
</a>
</p>
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. */}
<div class="mt-8 rounded-token border border-line bg-ink/40 p-6">
<p class="font-mono text-[11px] font-medium uppercase tracking-wider text-primary">
The row that matters
</p>
<p class="mt-3 text-balance text-lg leading-snug text-fg">
Per-destination audio mix from one multitrack ingest.
</p>
<p class="mt-2 text-sm leading-relaxed text-muted">
polyemesis <span class="font-mono text-[13px] text-live">Yes</span> ·
Restreamer <span class="font-mono text-[13px] text-subtle">No</span> ·
restream.io <span class="font-mono text-[13px] text-subtle">No</span>
</p>
<p class="mt-4 max-w-2xl text-sm leading-relaxed text-muted">
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
<em>this</em> platform take the clean mix while <em>that</em> one takes
the full mix, from a single upload and without re-encoding the picture.
</p>
<p class="mt-5">
<a href="/comparison" class="text-sm text-primary underline-offset-4 hover:underline">
All eight capabilities, against five products →
</a>
</p>
</div>
</div>
</section>

Expand Down
53 changes: 50 additions & 3 deletions web/src/styles/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -216,25 +216,43 @@
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;
/* --color-ink is what html and body actually paint. Named for the theme
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;
Expand All @@ -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;
Expand Down
Loading