Skip to content

Refuse a page that moves something the reader cannot stop [#87] - #154

Merged
iderex merged 1 commit into
mainfrom
motion-answers-the-reader
Aug 13, 2026
Merged

Refuse a page that moves something the reader cannot stop [#87]#154
iderex merged 1 commit into
mainfrom
motion-answers-the-reader

Conversation

@iderex

@iderex iderex commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

What was wrong

A reader who tells their system to reduce motion has told every page, and the
pages this site serves today answer them. The pages this generator produces
answer nothing, because they carry no stylesheet at all:

go run . build | grep '^wrote'
wrote dist/index.html (1140 bytes)
wrote dist/privacy/index.html (3350 bytes, 2 checked, 3 promised, 2 residual)
wrote dist/.well-known/security.txt (379 bytes)
grep -c -iE '<style|rel="stylesheet"|transition|animation' dist/index.html dist/privacy/index.html
dist/index.html:0
dist/privacy/index.html:0

So nothing has been dropped yet, and nothing would notice on the day it is. The
byte budget reads a page with no query as the smaller one, and every other row
in the table reads something a stylesheet is not.

What this does

The invariant table gains page-motion-answers-the-reader over the produced
pages. It refuses a declaration that moves something where no query asking the
reader about motion switches it off.

Two shapes answer the reader and both are in what this project publishes today.
A declaration inside a query asking for no preference applies only to a reader
who has not asked for less, so the answer decides whether it exists. A
declaration in the ordinary cascade is answered by a query asking for reduce
that switches the same property off, which is the blanket override most
stylesheets are written with, and a row refusing that shape is one somebody
takes out the first time they write a stylesheet. Motion written inside the
reduce query is refused on its own, because it is one word from the repair and
what a reader gets for asking is the motion.

Two bounds, both stated at the operator. The row reads bytes and cannot resolve
a selector, so a reduce query switching a property off answers for every
declaration of that property as far as it can see; what it refuses is the page
that never asks, which is what a generator produces when it renders a stylesheet
written without the query, and a page that asks and misses one rule belongs to
the legs that load it in a browser. The property set is a floor: motion written
through a vendor-prefixed property, through an animation element inside an image
or through anything the set does not name is refused by nothing here.

The report is the other half of the same change rather than a second topic.
Nothing this build writes moves, so the row would have printed ok over pages
carrying none of what it judges, and a green mark over an empty set reads as a
set of pages that answer the reader. A row that counts what it judges prints
what it read instead:

go run . invariants | grep motion-answers
  page-motion-answers-the-reader: 2 file(s) of every page the build produced carried no declarations that move something, so this rule decided nothing

Only such a row carries the counter. Where the page itself is the subject every
page is one, and the file count the run already prints says so.

Closes

Nothing. #87 asks for four things and this is one of them. Loading every page
under each emulated scheme and under an emulated request to reduce motion, and
reddening a run where the browser is present and will not start, are the other
three, and they wait on the browser in #63. The issue already carries that split
and this does not restate it.

What was run

The gate, at 878938cfe0203dc3e3e503df44f0a04b88c98b76:

go run . ci
gate: 6 legs, in order: format, vet, test, build, links, invariants
  needs-network was not asked for. Asking costs a request to the public name from whatever machine runs it, and a verdict that moves when somebody else's service does rather than when this tree changes. Ask with: go run ./harness/needs-network
  format: ok, 37 file(s)
  vet: ok
  test: ok, 18 test file(s)
  build: ok, 3 file(s)
  links: every reference that stays inside this site resolves to a file the build wrote
  invariants: ok, 26 rule(s) decided, 1 owed and not decided
6 of 6 legs ran. None was skipped.

The set that needs the network was not asked for, which the run names above. No
test in this change was skipped, and none of them needs a display or elevation.

That the row bites, against the real frame rather than a fixture. With
<style>a { transition: color 120ms }</style> added to templates/page.html.tmpl:

go run . invariants
  page-motion-answers-the-reader: REFUSED, 2 violation(s)
    it refuses a produced page carrying a declaration that moves something, where no query asking the reader about motion switches it off
    dist/index.html: line 8 declares transition: color 120ms, and no query asking the reader about motion switches transition off
    dist/privacy/index.html: line 8 declares transition: color 120ms, and no query asking the reader about motion switches transition off
exit status 1

and with the query that answers it added beside the same declaration,
@media (prefers-reduced-motion: reduce) { a { transition: none } }:

go run . invariants | grep motion-answers
  page-motion-answers-the-reader: ok, 2 declarations that move something in 2 file(s) of every page the build produced
exit status 0

The frame is back as it was on main in the commit that is pushed; the two runs
above were taken with it edited and it was restored afterwards.

That the guard is load bearing, by taking it out. With the row deleted from the
table and nothing else changed:

go test ./internal/invariant/ -count=1
--- FAIL: TestEveryRowRefusesItsOwnViolationAndPassesTheNeighbour (0.00s)
    invariant_test.go:194: the table holds 25 row(s) and this test carries 26 violation(s); a row without one proves nothing
--- FAIL: TestRunSaysARowFoundNothingToDecide (0.22s)
    invariant_test.go:687: the run does not say the row decided nothing; it said:
        invariants: 25 rule(s), in order: page-declares-its-language, ...

and with it back:

go test ./internal/invariant/ -count=1
ok  github.com/Flowfin/site/internal/invariant  3.864s

The near miss the suite spends its effort on is the last case in
TestTheMotionRowTellsTheQueryFromTheCascade: the query is written, the
property is written, and the value is the one that moves. It is one word from
the shape above it in the same table, and it is the shape a reader who set the
preference actually meets.

The means

Go, in the package that already holds the table, because a rule here is a row
and adding one is adding a row. It carries the three things this repository asks
of a means. It is refusable, and the refusal goes through the same operator every
other row uses. The guard is shown to bite by the runs above and shown to be load
bearing by deleting it. Every number in this body carries the command that
produced it.

A rule written into the workflow that reports it would have no suite that could
prove it bites and no way to run it on the machine where the mistake was made,
which is the position this repository already took for the rules over commit
messages.

Who read it

Nobody but me. The ruleset on the branch requires no approving review, so this
is merged by the person who opened it, and the runs above stand in place of a
second reader rather than beside one.

A reader who tells their system to reduce motion has told every page, and the
answer belongs to them. The pages this site serves today carry the query that
answers it. The pages this generator produces carry no stylesheet at all, so
nothing has been dropped yet and nothing would notice on the day it is: the byte
budget counts a page with no query as the smaller one, and every other row reads
something a stylesheet is not.

The table gains a row over the produced pages. A declaration that moves
something is answered either by sitting inside a query asking for no preference,
or by a query asking for reduce that switches the same property off. Both shapes
are in what this project publishes today, and a row refusing the second would be
one somebody takes out the first time they write a stylesheet. Motion written
inside the reduce query is refused on its own, because it is one word from the
repair and what a reader gets for asking is the motion.

The row reads bytes and cannot resolve a selector, so a reduce query switching a
property off answers for every declaration of that property as far as it can
see. What it refuses is the page that never asks, which is what a generator
produces when it renders a stylesheet somebody wrote without the query. A page
that asks and misses one rule is for the legs that load it in a browser.

The report is the other half and not a second topic. Nothing this build writes
moves, so the row would have printed ok over pages carrying none of what it
judges, and a green mark over an empty set reads as a set of pages that answer
the reader. A row that counts what it judges prints what it read instead. Only
such a row carries the counter: where the page itself is the subject, every page
is one, and the file count already says so.

This is the static half of the issue. Loading every page under an emulated
request to reduce motion waits on the browser, and nothing here claims it.

Signed-off-by: Nils Lehnen <[email protected]>
@iderex iderex added the tests label Aug 13, 2026
@iderex iderex self-assigned this Aug 13, 2026
@iderex
iderex merged commit 807975b into main Aug 13, 2026
17 checks passed
@iderex
iderex deleted the motion-answers-the-reader branch August 13, 2026 00:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant