Skip to content

feat(board): restyle the board page on the shared slice band - #971

Merged
ExtraToast merged 60 commits into
mainfrom
feat/board-slices-restyle
Sep 2, 2026
Merged

feat(board): restyle the board page on the shared slice band#971
ExtraToast merged 60 commits into
mainfrom
feat/board-slices-restyle

Conversation

@ExtraToast

@ExtraToast ExtraToast commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Implements #970.

What changed

The board page now uses the same band component as the esports pages. SliceBand gained a
layout prop: cover for games (art fills the slice) and aside for people (a portrait holds
the left of the slice). Hover behaviour, how much a row an open slice takes, touch behaviour and
image sizing are shared rather than duplicated. This deletes SeatRows.vue (481 lines) and
BoardManager.vue (647 lines), the two components the island page replaced.

Board member slices. The portrait sits on the left at full height; the name, nickname and
role stay on top of it whether the slice is open or closed. Opening a slice widens it and shows
the member's description to the right of the photo. Only members who actually wrote a
description can open, so the six early boards (which have none) render as a static row.

The group photograph is shown in full. It keeps its aspect ratio and is sized by height, so
it is never scaled beyond its own resolution (the old band stretched a 1000px-wide photo across
a 2560px window). Its right edge fades into the board's colour instead of ending on a hard line.
Boards without a photo render as a short strip sized to their text, not a full-height empty one.

Switching boards slides. Picking another board on the timeline slides the current one out
and the new one in, in the direction you're travelling in time; the timeline itself stays put.
This reuses the esports page's existing slide mechanism, extracted from SeasonSwipe.vue into
BandSwipe.vue so both pages share it. SeasonSwipe is now a thin wrapper. Slices don't open
during a slide, which previously animated two rows of layout inside a moving element.

Divider lines (BandRule.vue) above and below the member band, in the association blue,
drawn half over each band so they read as a join rather than a third band.

Timeline colours. A Stop can now carry its own accent colour, and boards pass theirs down,
so hovering a board fades the timeline's lit segment to that board's colour. No board has a
colour in the database yet, so everything currently renders in the association blue; nothing
hardcodes a colour.

New display font. Fugaz One has no İ, ı, ş, Ş, ğ or Ğ, so member names in those characters
lost letters. Replaced with our own modified cut containing them, renamed to Shellhouse One
because the SIL Open Font License forbids modified versions from using the original's reserved
name. The licence and a notice of what changed ship alongside the font.

Seed data. Board 10's six members (name, nickname, role only — no descriptions or portraits
yet), board 7's nicknames, and board 3's role capitalisation. BoardSeedParsingTest updated:
it asserted board 10 had no members and counted 46 rows.

Copy. Page heading, intro paragraph and the "put your name forward" call band rewritten:
board years follow the academic year, applications are by motivation letter over email between
March and May, and Discord is open all year.

Testing

Frontend unit suite passes (137 files, 1047 tests). Typecheck and lint pass. The end-to-end
board spec is rewritten in this branch: the old assertions targeted SeatRows' DOM
(board-seat-name-*, board-seat-monogram-*, board-numeral), none of which exists now.

Visual details — gradient stops, fade widths, animation timings — were tuned against
screenshots rather than asserted, as #970 records. Two things were measured rather than eyeballed:

  • Opening a slice no longer re-wraps its description text every frame (the text box was pinned
    to the slice's growing right edge; it now has a fixed width).
  • During a board slide, open slices in the outgoing/incoming bands went from 1/1 to 1/0.

Known gaps

  • Board members are still called "seats" in the frontend; the API has always called them
    members. Rename is a separate change.
  • On mobile, an open member's name is constrained to the portrait's width, which wraps it
    awkwardly. Left for a separate mobile pass.
  • Portraits still re-scale slightly while a slice widens, because object-fit: cover
    recalculates against the box it is given. Fixing it by sizing on height instead leaves a
    gap wherever a slice is wider than a portrait (measured: none at 1440px with six members,
    45px at 2560px, 192px at 3440px). Deliberately parked.

Notes for reviewers

  • The band's CSS class names are still team-slice* even though it now draws board members.
    Nine e2e specs select on those classes and are the evidence the esports pages didn't change,
    so they weren't renamed alongside the refactor.
  • Three new tokens in island.css: --photo-scrim (the overlay behind a name on a photo, the
    same darkness in both themes, because that text is white in both), and --board-wash /
    --board-wash-on (the banner's colour wash, weaker than the timeline's because it covers a
    much larger area).
  • The "add a seat" pane never drew its background pattern: its url() pointed one directory
    above src, so the dev server returned index.html and the browser rendered nothing. Both
    bands using that pane were affected.

Diff breakdown added removed, scaled to the largest row.

api                                                +74    -37    4
  production         █░                            +18    -12    2
  unit tests         █░                            +14     -5    1
  integration tests  █░                            +42    -20    1

frontend                                         +1998  -1435   37
  production         ████████████████░░░░░░░░░░  +1745  -1107   30
  unit tests         █░                            +52   -185    4
  e2e tests          ██░                          +201   -143    3

system-tests                                       +82    -26    1
  system tests       █░                            +82    -26    1

──────────────────────────────────────────────────────────────────
production                                       +1763  -1119
tests                                             +391   -379  0.22 test lines per prod line
total (hand-written)                             +2154  -1498  42 files

The seats were a component of their own — a vertical grid of rows that shared nothing with
the band the esports pages are drawn in. They are slices of that band now. `BannerSlices` is
`SliceBand` and takes a `layout`: `cover` is a game, where the art fills the slice, and
`aside` is a person, where a face holds the left of the slice at its own width and stays
there as the slice opens, so what grows is the room the words get. Collapsed, a name and a
role sit on the face over a scrim; open, they step off it and the blurb arrives beside them.

The strip above it stops stretching its photograph. Full-bleed was the defect: a window 2560
across asked for 2560 pixels of a photograph that has 1000, so the widest board was drawn at
two and a half times its size, and a band a few hundred tall cropped a 1.5:1 group photograph
to a quarter of its height. Given its own height and its own proportions the picture is about
530 across whatever the window is — every board downscales into that, and the whole
photograph is shown. The strip keeps the width; the picture no longer has to cover it.

The board's name, year, cheer and description move off the page and into the strip, beside
the picture rather than under it, on a wash of the board's own colour that fades in over the
picture's right edge. The wash is the island's own `--band-wash` tokens mixed with
transparent, which is what the timeline lights its bands with, so the two agree and the
page's ground still shows through.

The copy stops explaining the page and stops inventing an election. Anybody in the
association can ask to be on a board; there is no season to wait for and nothing to join
first, so membership is no longer the way in that the closing band offered.
…hing opens

The picture stands taller, which is also wider, because the photograph keeps its own
proportions and height is the only dial. At 38vw it is about 57% of the window across at any
size, which is the share that stops the strip reading as a line with a stamp at one end. The
ceiling stops below the point where the smallest photograph the association owns would be
asked for more pixels than it has.

Both pictures now dissolve at the edge the colour arrives from. A wash subtle enough to see
the page's ground through is invisible over a dark photograph, so however far the colour was
pulled back over the picture the two still met at a line. Fading the picture itself is what
the board rows on the old page did, and it is the join the strip wanted.

A member's slice keeps its name where it was. The face holds the left of the slice and keeps
the name, the nickname and what they were on it; what the slice grows is somewhere to the
right for the blurb, on a fade of the island's own ground. Nothing is rearranged to make
room, and prose is read on a flat surface rather than across a photograph.

Only a member who wrote something opens. Twenty-five of the forty-six board members wrote no
blurb and four whole boards wrote none between them, so a slice that grew onto an empty panel
was the common case rather than the exception. An item in the band now says whether it opens
onto anything, absent meaning it does, and every route into opening a slice asks: a pointer
arriving, focus arriving, a click, and the slice the band settles on when it first draws.
Where nothing in a band can open, it settles on nothing and stands still.

The board's number and year are set large enough to read at a glance, and a second wash comes
in from the top left and is gone by the bottom right, so the photograph is lit from a corner
rather than having colour bolted to one side.

The rank table had drifted from the seed, which its own two-way check caught: the third
board's role is spelled out now rather than joined with a slash, and the two boards that
wrote their commissioners in lower case have been given capitals. Those three spellings move
to a table of what the rule must still tolerate rather than leaving the assertions behind,
because the next board to invent a spelling will not check first.
The large Roman numeral was stamped across the lower left of the photograph, and the eyebrow
beside the picture now reads BOARD IX · 2025-2026 a few centimetres away. It was the same
fact said twice, and the louder of the two was said over the people in the group photograph.

Its scrim goes with it. That gradient existed only so white would read on whatever the
photograph happened to be underneath, and with nothing to make readable it was a wash over a
board's own picture for no reason. The band no longer takes a number either: the numeral was
the only thing that read it, and the eyebrow is composed in the domain from the same rule.

A board with no photograph now has no picture box at all rather than a box of no width, and
the strip carries its own height instead. That box was holding two absolutely positioned
things, the centred numeral and the way to a photograph, and with the words moved into the
strip the way in was sitting underneath them: the words panel is pulled back over the
picture, so it intercepted every click on the plus. The plus stands in among the words now,
where nothing covers it.

Four boards have no name, no cheer and nothing written about them, so what they draw is the
board's colour with the eyebrow in it. That is the design as it stands and it is left as it
is rather than given a stand-in.
…dths

The rules for the empty slice and its sentence had been pasted a second time inside the
stacked block, which split the way-in slice's own rule across the brace and left the two of
them applying on a phone alone. They already stand at the top level, so the copy goes.
…ssolve

The timeline lights a stop with --band-wash. The banner is a far larger area and the same
figures there read as a fill rather than a tint, so it gets its own pair beside them,
heavier in light for the reason the existing pairs are.

--photo-dissolve is a share of a picture's own width rather than a length, so the group
photograph and a portrait a third its size read as the same softness.
The diagonal came off the photograph and into the words panel, where its 0% is the panel's
own corner: nothing is drawn over the faces now but the dissolve at the picture's right
edge. It is held clear of the pull-back and faded in at its left, or its own edge draws a
line down that dissolve.

The floor of the picture's height rises so a narrow window reads as a hero rather than as a
line with a stamp at one end, and a board with no photograph carries the same figure. The
words are read on the banner's own wash rather than the timeline's, and the eyebrow is set
larger, since it is the line that says which board this is.
The band under it is a row of faces that each dissolve into their own wash, and a banner
ending on a hard line above them read as two pages rather than one page.
The figure was written into the rule that grows a slice and into the comment beside the
arithmetic that reads it. A band that wants its own pace can now say so in one place. Same
620ms as before.
…ide it

A slice grew by a multiple of the row, so the row had to be re-divided and the picture was
pushed into a column of its own to make the room: the face shrank to pay for the paragraph.
It grows by what the words need instead. The band hands the stylesheet the width a slice has
while nothing is open, the picture is that width open and the whole slice shut, which are the
same figure, and the slices either side give up the difference.

Given its own intrinsic width the picture came out half again as wide as the column the words
were placed off, which is why the panel used to cut a face in half.

The panel wears the banner's own recipe on the banner's own tokens, with no ground of its own,
so the island's pattern carries through it and the two bands read as one page. It sits under
the picture, so nothing is laid across a face: what reveals it is the picture's own dissolve,
which is drawn on an open slice alone. Shut, six faces in a row are six faces in a row.

The scrim under a name is cut to the foot of the slice and the glow with it. The faces are in
the middle and the top of the frame, and a scrim tall enough to reach them is a filter over a
photograph rather than a ground under a name.

A face and a paragraph are read where a game's line-up is glanced at, so the band takes 950ms
to open. The blurb is centred in its panel rather than held to 18rem from the top, which is
what left a field of ground under three lines of text.
Two photographic bands meeting at a fade read as one picture bleeding into another however
far the fade is drawn back, and made strong enough to divide them it dims the photographs it
is there to join. A strip between them, with a mark on it, is a join a reader can see.

The mark is the page's own: leaning ticks, the slant every button and chip is cut on and the
one the accent dash above a name already draws, and a rule trailing off to the right.

Its own ground rather than the island's, because a strip that shows the page's pattern sits
between two areas that also show it, and a repeat lining up on one side of a join and not the
other is what the strip is there to hide. It sits directly on the band below it.

The banner's own foot fade is cut back to match: the strip is the join now, and the fade is
only the last of the banner settling onto the page.
…s dark

Given a ground near the bands' own it read as the same dark again with three marks floating
in it, which is the busyness it was put there to end. What divides two bands has to be a
thing of its own, so it is filled with the colour and the mark is knocked out of it in the
near-black both themes keep for ink on a bright fill.
The wash reached its strongest at the far side of the panel, which is to say it deepened with
distance from the thing it was lighting, and it took a whole element with a mask to keep its
own edge off the photograph.

One gradient does it now, from the picture's top right corner, which is the panel's own top
left: it thins in every direction from there, so leftwards it is gone before the faces and
rightwards the page's own ground takes over. Drawn from the corner rather than clipped to it,
so there is no edge to hide and no element to mask. The carry under it does one job, giving
the picture's dissolve a colour to fade into.

Both figures come down: 4% and 10% dark, 7% and 16% light. A light source at one corner needs
far less colour than a field of it did.
Thin, it read as a line with the page's own dark either side of it, and the pattern showing
through those two dark bands is what still looked busy. A strip a mark can sit in divides the
bands on its own, so the fade at the banner's foot has nothing left to do: the photograph
runs to its edge and the colour takes over there.
The same decision the esports strip takes, and for the same reason: these bands are
photographs, the dark treatment is what suits them, and a ground that follows the viewer only
filters somebody's picture.

Where the esports band gets away with the tokens alone, this one paints its own ground. Its
panels are washes mixed with transparent so the island's pattern carries through them, and a
token cannot repaint what is behind them: on a light theme the panels came out near white with
white ink on them. The island's own texture, laid again over the dark ground.

The timeline above and the call band below stay in the reader's theme, exactly as the esports
pages leave them.
A stop may carry a colour of its own, and a board hands its own down. The line takes the
colour of the stop under the pointer, then of the stop being read, and each band highlights in
its own, so moving down a row of boards moves through their colours rather than watching one
colour reach further along.

Carried by the line's own stroke and its glow, both of which a browser interpolates, so the
colour fades from one to the next in step with the lit stretch travelling and costs no
animation of its own. Nothing changes on a strip whose stops have no colours, which is every
strip today.
…stack

A row of portraits at their own full height is photography and takes the dark treatment in
both themes. The banner is one photograph beside the board's own words, and it reads as the
page it is on, so it follows the theme the way the timeline above it and the call band below
it do.
A hero is a photograph. Held to a photograph's height with nothing in it, the strip was a
field of colour with a line of writing at the top, so the words decide the height now and the
colour thins away downwards into the page rather than ending on a line, which is the join
every photograph on the page makes.
Whole bands were being held dark in both themes, which darkened the page around the pictures
as well as the pictures. It belongs to the picture and to what is drawn on it: the scrim under
an overlaid name and the lift at the foot of a portrait take --color-void, near-black in both
halves and what the theme keeps it for, and the name, the nickname and the role take near-white
ink over them.

Everything else follows the reader: the panel beside the picture is the page's own ground with
the board's wash on it, and the description on it is the page's own ink. Open, the lift stops
at the picture's edge rather than hazing the foot of that panel.

A band whose members have no portraits has no photography in it, so it keeps the reader's
theme, and it is as tall as the names in it rather than as tall as a face.
Near-black is right in the dark half and reads as a hole cut in the page in the light one, so
--photo-scrim takes it up a few steps there. Its own token rather than --color-void, which is
ink on a bright fill and is the same in both halves by design. The near-white name over it
still clears contrast several times over.
A motivation letter over email is the way in and the window runs March to May, which the band
never said, and the Discord line stands all year, which is what keeps somebody wondering in
October from reading this as a door that is shut. The election is not mentioned: it is a fact
about a process nobody asked about.
The handover month is a fact about a calendar. That a board year runs with the academic one is
the thing a reader needs, and what the boards try to do with it, keeping the place open and
welcoming and putting on events where gamers meet one another, is what the page is an
invitation to.

The line about each board's own name, colour and cheer goes with it: the page below is made of
exactly those, so saying it first is telling a reader what they are about to look at.
…icture

Filled with its own mix of the colour and the pit it read as a block of paint with writing on
it, while the same board beside a photograph read as a tint: two different pages. The banner's
own recipe instead, on the same tokens and mixed with transparent, so the page's ground and
its pattern come through. The corner is the strip's own top left, since there is no picture
for the light to come off.
…years

The page is not an index of years, it is the boards and what they built, so the heading says
so. Two lines still, since the second takes the colour.
…knames

Six seats on the candidate board, with a name, a nickname and a role each and nothing else:
nobody has written a blurb or sat for a portrait yet, which is what the page draws a board
with no art as.

The seventh board's nicknames were never recorded, and board three's roles were written in
three different capitalisations with one of them reading Secretary/Treasurer, which is two
seats in one field rather than the one seat it was.

The seed test said the tenth board had no seats and counted forty-six of them. Both were
facts about the file rather than rules about it, so both move with it.
House rule, applied to every file this branch touches: a comma, a colon, a full stop or
brackets say the same thing. Sixty-seven in the frontend's own comments and copy, plus the
ones in the e2e and unit specs. The en-dashes in the board seed stay where they are: those
are board members' own words about themselves and not ours to edit.

Three comments go with them. The band's style block opened with two blocks that the code had
since contradicted, one saying the band was the same height whether or not it had a
photograph and one saying it sat in the island's column rather than edge to edge. The open
roster carried two descriptions of itself, the older one about a margin and a cap that no
longer exist. The empty slice carried two comments belonging to the way-in slice beside it,
which has its own.
Choosing another board on the line sends the one on screen off the way the reader is going and
brings the one they chose in from the other side. Back down the line the board on screen leaves
to the right and the older one arrives from the left, and forward is the mirror of it. The line
itself stays where it is, since it is the thing being travelled along.

The esports pages have done this for a while, so this is that mechanism rather than a second
one. It comes out of SeasonSwipe into BandSwipe beside the other island components both pages
share: the pass, the two stops on the page at once, the height carried from one to the other,
the stop leaving turned into a picture of itself so nothing tabs into it or finds two slices
under one name, and the crossfade under prefers-reduced-motion. The esports side keeps the
season vocabulary, its own class and its own testid, and hands the island a direction.

Which of two stops is later stays the domain's answer. Boards compare the terms they ran, which
is the same comparator the line is drawn in, so the order it reads in and the way a change
travels are one question asked twice.
…ce opening

The blurb's box was pinned to the slice's right edge, so it widened with the slice and the
prose was laid out again on every frame: three lines becoming two becoming one, a text layout
a frame across the whole band. Its left is the picture's edge and its width is the room the
words were given, both of which stand still, so the box never changes and the growth is a
layout of the row rather than of the row and its prose.

It also arrives after the growth instead of during it. Prose fading in under a box that is
still moving is two things happening at once, and the words are the slower read of the two.

Three more, all cheap: the lift at the foot of a portrait drops its 26px blur, which was a
second pass over a gradient already that soft and six offscreen renders a frame while the row
was moving; a slice contains its own layout and paint, so one slice moving is not six slices
painting; and a portrait decodes off the main thread, which is
where a board change spends its one long frame, mounting a second complete band. Not the
group photograph: the hero is what the page is, and a hero that paints a frame or two after
everything else is a hole where the board should be.

Measured at four times CPU throttle: hovering across the faces holds 60fps with no frame over
25ms, scrolling and idling likewise, and a board change costs a single frame at the swap.
Fugaz One has no İ, ı, ş, Ş, ğ or Ğ, so a display face on a site whose members are named in
them is a face with holes in it: that is why a name has never been allowed to use it, and why
--font-name exists at all. Fugaz One TR is the same face with those letters cut, so the whole
of the site's display type can be set in it.

The licence sits beside the file as FugazOneTR-OFL.txt, verbatim, the way the SIL Open Font
License asks. The face it replaces goes: two nearly identical display faces in one directory is
a question nobody should have to answer twice.

--font-name still names Barlow. What kept a name out of the display face was a correctness
constraint and is now a preference: a name is read, and a display face is shouted. Both
comments say so, and the glyph test now holds both faces to covering the letters rather than
recording which of them did not.
It is our modification of somebody else's font, and the licence has a rule about that: "Fugaz"
and "Fugaz One" are Reserved Font Names, and clause 3 of the SIL Open Font License is that no
modified version may use them as the name it presents to users. Calling it Fugaz One TR was
using one.

So the face is Shellhouse One, in the font's own name table as well as in every font-family
rule: family, full and PostScript names, and Blueshell as the manufacturer, since we made this
cut. What the licence does ask for stays exactly where it was: the original copyright notice
with both reserved names in it, the licence record, and the designer credit to Daniel
Hernandez, whose outlines these are. The description in the file says what was added and that
neither LatinoType nor he is affiliated with it or endorses it.

Beside the font: ShellhouseOne-OFL.txt, the licence verbatim, and ShellhouseOne-NOTICE.txt,
which records the six characters added, that they are composed from the original's own
outlines, that nothing else was changed, and why the name is what it is.
It ran past the picture and onto the panel, where the lift under it stops and the ground it is
read against is the page: near-white ink on a pale panel in the light half. It wraps at the
face's own width now, which is where its ground ends.
Boards are not all the same height, and a call band that stayed put while the board above it
travelled was the one thing on screen saying nothing was moving. It sits inside the pass now,
so what leaves and arrives is the whole board and everything the page says about it.
A slice opening is a row's layout animated over most of a second. The arriving band settled on
its own the moment it mounted, which is in the middle of a pass, so that animation ran inside a
subtree being translated across the screen, and ran twice: the board leaving is still on the
page with a slice of its own open. That is the stutter.

The swipe now says whether a pass is on and the bands read it: a band settles on the slice it
opens once the page has stopped, and answers no pointer until then, since whatever is under the
pointer mid-pass is not what the visitor was reaching for. Injected with a default of not
travelling, so a band outside a swipe behaves exactly as it did.

Counted mid-pass, before and after, as open slices in the leaving band over open slices in the
arriving one: 1/1 became 1/0.
…ning

It waited for the growth to finish, which it no longer has to: what made waiting worth it was
prose being re-wrapped on every frame, and the box it is laid out in stands still now. So it
begins a third of the way in and is there before the slice settles.

A third rather than half: the easing is front-loaded, so at half way the slice is already
within a few pixels of its width and a fade starting then reads as arriving afterwards.
Measured at 285ms the slice is at 438 of 528 and still moving.
… without a face

The words panel is pulled back over the picture so the words can sit close, and its wash came
with it: at the top, where the photograph is most solid, the colour lay across it and its own
edge read as a line down the picture. Drawn back to start at the picture's edge instead, it left
a strip of bare ground between the two, which was worse.

So the wash is a layer of its own, spanning the pulled-back panel and masked to nothing where
the photograph is still solid, at full by the picture's right edge. That stretch is the
picture's own dissolve, so the colour comes up exactly as the photograph goes down: no line on
the picture, no gap beside it. A little of the colour runs along the top as well, faintly, so
the corner is where the light is strongest rather than the only place it is at all.

A slice with no portrait loses the scrim under its name. There is no photograph beneath it, so
what it drew was a dark fade up the foot of the panel and nothing else. Per slice, since a board
may have portraits for some of its members and none for the rest.
…photograph's ink

The band knows whether a slice has art, so it says so: a bare slice carries a class, and the
two things that only make sense over a photograph read it.

The scrim under the name goes. With no picture beneath it, what it drew was a dark fade up the
foot of the panel, which in the light half is a grey band across a pale page.

The near-white ink goes with it. It is the ink a name needs on a photograph, and with no dark
ground under it, it is near-white ink on the page: a bare slice takes the theme's own ink, the
way the description beside it already does.

Said as a class rather than asked with :has(), and this is why: the rule that asked was written
and committed and never matched anything. What a slice is made of is something the band knows
at render time.
…un the other way

The mark was taller than the line and read as a separate thing beside it. Same weight now, and
more of them, each dash a little longer than the last and each closer to the next, the last one
running to the edge and handing straight over: what a reader sees is one line that begins as
dashes and gathers into a rule.

And it takes a mirror. A band closed with the same mark it was opened with reads as the same
rule drawn twice; the other way about, the two hold the band between them. A flip rather than a
second set of gradients, since there is nothing in here that reads.
It was opened with one and closed with nothing, so it ran into whatever followed it. The rule
under it is the same one mirrored: dashes at the right, the line off to the left.
What opens a band is that band's own colour, the board's on the board page. What closes it is
the site's, on any page that closes one, so the mirrored rule carries the blue with it rather
than saying the board's colour a second time at the foot.
A board's colour is the board: the line it sits on, the banner, the wash beside its photograph.
A rule holding a band is the site drawing a join, and it says so in the site's own colour at
both ends rather than repeating the board's. The mirror is about which way it runs again, and
nothing else.
…s a shift

A game's banner sits at 1.06 shut and comes back to 1 open, which is a slow push on a
landscape. On a row of faces it read as the wrong thing entirely: moving the pointer along, the
face being left zoomed in while the face being reached zoomed out, so what should be one shift
across the band was two zooms in opposite directions. A face is also scaled about its own
centre, so the framing drifted vertically while it happened.

One level now, open or shut, and the only thing that animates is the picture's width. Measured
across a hover: every portrait at scale 1, every box the same height at the same y, and the
widths the only figures that move.
The url was one directory too high: from src/components/island, ../../../assets is outside
src altogether. In dev the request fell through to the app's own index.html, so the browser was
handed a page where it wanted a jpeg and painted nothing, leaving the flat blue underneath it.
The pane has looked like that on both bands that carry one.

Caught by the build, which resolves what the browser only shrugs at: it emits
/src/assets/bg/shelly-bg-blue.jpg now, and that answers with an image.
@ExtraToast ExtraToast changed the title feat(board): the boards wear the band the games do feat(board): the boards wear the band the games do, and a board change is travel Sep 2, 2026
@ExtraToast ExtraToast changed the title feat(board): the boards wear the band the games do, and a board change is travel feat(board): restyle the board page on the shared slice band Sep 2, 2026
…o anything

A pointer and a click asked. The scroll that decides on a phone, the slice named from outside
after something is added, and the slice a rebuilt band settles on did not: each set the open
index directly, so a member who wrote nothing could be opened onto an empty panel, which is the
defect the expandable rule exists to prevent. Scrolling past such a member on a phone did it
every time.

One function answers it now and all five routes go through it. Where the answer is no, the
scroll leaves what was open open, the named slice is not opened, and a rebuilt band falls back
to the first slice that does open.
They asserted the structure the restyle replaced: a numeral that is gone, per-seat ids that went
with SeatRows, monograms and chevrons that no longer exist, a membership action the call band
dropped, and an identity block that now lives inside the banner. Roughly thirty assertions that
could not pass.

Rewritten against what the band renders. A name and a role are read through the slice, since
they are drawn on the face and have no ids of their own; a portrait is the slice's own picture;
a member with no portrait is a slice with no picture rather than a monogram; and whether a slice
is open is read from its class, because a shut slice keeps its words in the document and gives
them no room.

Three things the rewrite pinned down rather than assumed: a board with no photograph is shorter
than one with a photograph, a cheer is drawn inside quotation marks, and a second press does not
shut a seat, another seat opening does.

52 pass across chromium and mobile-chrome.
@ExtraToast
ExtraToast marked this pull request as ready for review September 2, 2026 16:08
@ExtraToast ExtraToast self-assigned this Sep 2, 2026
@ExtraToast ExtraToast added the enhancement New feature or request label Sep 2, 2026
Four cases asserted forty-six seats, five nickname-less seats on the seventh board and none at
all on the tenth. The seed has fifty-two seats now, the seventh board's nicknames are recorded
and the tenth is seated, so all four failed, and they were the API job's failure on this branch.

The numbers come out of the files instead. What is under test here is the loader: that
everything the files hold lands, once each, and survives a second run. A number typed beside
the assertion tests the file rather than the loader, and rots the day somebody seats a board,
which is exactly what happened. The files' own facts stay asserted in BoardSeedParsingTest,
where a change to them is supposed to fail.

The nickname case now counts every seat with none across the whole seed rather than the
seventh board's five: which boards those seats are on is the file's business.

24 tests pass against a real MariaDB.
…aying for the cold start

A shard failed on `page.navigate` exceeding five seconds against /membership/signup, and the
next test in the same class passed in about a second. That is the cold start: `docker compose up
--wait` calls a container healthy as soon as its own check passes, which is before nginx has
served a page or the api has answered anything, so the first navigation of the suite pays for a
cold server, a cold JVM and a first render, and whichever test ran first was charged the lot.

So the suite waits for it, once per JVM, before any test runs: the api until /health answers,
the frontend until it serves a page, then one throwaway page load that pays for the bundle and
the fonts. The five second ceiling stays exactly where it is, because it is still true that an
*action* needing longer is waiting on the wrong signal.

Two things this turned up while being verified locally, both of which would have made it worse
than what it replaces:

- Java's HttpClient defaults to HTTP/2, which over cleartext asks for an h2c upgrade the
  frontend's server never answers: every probe of an origin that serves 200 to curl in two
  milliseconds timed out, for the full ninety seconds. Pinned to HTTP/1.1.
- The api's health endpoint is /health, not /actuator/health, which 404s. A 404 would have
  satisfied a check written as "answers with anything under 500", so the gate would have passed
  without the api being up at all.

And navigateWithRetry goes: three attempts with a sleep between them, for the window after
compose reports healthy while ports are still binding. Nothing called it, and the gate is that
window's answer.
The page still declares --accent-ink, the board's colour mixed towards the theme's own so a
pale colour reads on a pale page and a dark one lifts off a dark page. Nothing read it: the
rule that did names .board-cheer, and the cheer moved inside the band when the page was
rebuilt, where a scoped rule in another component cannot reach it.

So the band reads it, with the ink of whatever it is in as the fallback, and the dead rule
goes. Two end-to-end cases were asserting this all along, one per theme, and would have passed
against plain chalk without noticing the colour had gone.
The same stale structure boards.spec.ts had, in the two specs that drive the pencils: the
board's own words under their old ids, per-seat names and roles under ids that went with
SeatRows, a monogram for a seat with no portrait, and a plate whose src was read to prove a
cancelled dialog changed nothing. Forty-two failures across the two files, every one of them a
locator that resolves to nothing.

Read through the slice now, since a name and a role are drawn on the face and have no ids of
their own, and through the slice's own picture for a portrait. A seat with no portrait draws no
picture rather than initials, which is what the two upload cases now assert either side of the
save.

One case was asserting the wrong reader: a board with no seats shows a visitor a paragraph
saying so, but shows somebody who may seat people the band with the way in at the end of it, so
what is empty is a slice in the row. It was logged in as a board member and waiting for the
paragraph.

110 pass across the three board specs in both projects.
…ing it

One case failed on the phone projection under load and passed alone, which is the shape of a
race rather than of a slow machine. It is a real one, and it belongs to the test.

Stacked, the scroll decides which seat is open, and a scroll releases a tap by design: the
choice stands until the visitor scrolls, at which point the scroll is their intent again.
Playwright scrolls an element into view as part of clicking it and that scroll event is
delivered asynchronously, so a press can be undone by its own scroll arriving after it. Whether
it does depends on how loaded the machine is, which is why it came and went.

Scrolling first and pressing second is both deterministic and the order a finger makes.

156 runs of the file, three passes over both projections at six workers, all green.
…cking it

The last e2e failure, and the same fault as the seat press one commit ago wearing different
clothes. A pencil is hidden until its slice is hovered. A click scrolls its target into view
first, that scroll takes the slice out from under the pointer, the hover goes with it and the
pencil disappears: 'element is visible, enabled and stable', then 'scrolling into view if
needed', then 'element is not visible' eight times over until the budget ran out. Load decides
whether the scroll was needed at all, which is why it came and went.

Both helpers scroll first, hover second, wait for the pencil, then press it. A slice already in
view has nothing to scroll.

99 runs green: both editor specs, two passes over both projections at eight workers, which is
what CI uses.

The same shape is still in esports-game-edit, esports-game-remove and esports-lineup-edit. None
has flaked, so none is changed here.
@ExtraToast
ExtraToast merged commit eeb954a into main Sep 2, 2026
25 checks passed
@github-actions github-actions Bot mentioned this pull request Sep 2, 2026
ExtraToast added a commit that referenced this pull request Sep 2, 2026
… by pressing it (#972)

The board specs carried this fault and it cost two rounds of red CI on #971: a pencil is hidden
until what it belongs to is hovered, a click scrolls its target into view first, and that scroll
takes the element out from under the pointer, so the click loses the hover it needs and reports
the pencil as not visible. Whether the scroll is needed at all depends on where the page is
sitting, which is what made it come and go.

The esports specs have the same shape in four places and have not flaked, which is luck: their
bands are shorter and the target is usually already in view. Scrolling first, hovering second
and pressing third costs nothing and is the order a hand makes.

48 pass across the three files in both projections.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant