Skip to content

Mobile app shell: full-bleed art, floating nav, one section as a sheet; Size at the end - #4

Merged
enonforetsam merged 31 commits into
masterfrom
mobile-app-shell
Aug 26, 2026
Merged

enonforetsam merged 31 commits into
masterfrom
mobile-app-shell

Conversation

@enonforetsam

@enonforetsam enonforetsam commented Aug 23, 2026 •

Copy link
Copy Markdown
Owner

Draft. Below 880px in portrait (phones, iPad mini upright) the studio is an app: the canvas fills the screen edge to edge, the chip bar floats over its top (notch-safe) and a 5-tab nav floats over its bottom — Colour · Engine · Look · Browse · Export. A tab opens its panel section as a sheet above the nav (42dvh, capped so the art never drops under half the screen; scrolls inside; same tab / ✕ / pull-down closes). Size lives in the Export sheet (the frame is an export decision; runExport renders at exportTarget regardless of the canvas box). Browse is the stage tray as a sheet (shelves wrap into a 3-column grid). Source is behind an image chip on the chip bar. Status speaks as a toast. Same DOM, same ids, same wiring — CSS decides what shows; a thin ES5 shell toggles data-sheet. Desktop and landscape phones unchanged. Retires the drag grip on portrait phones.

Preview: https://preview.fluid.krackeddevs.com (npx wrangler deploy --env preview from the fluid-app-shell worktree). Rebase on master each session.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LcPnaMoccEmKYGA4nrZXd6

enonforetsam and others added 3 commits August 23, 2026 11:49
…eeps half the screen

Danial: "what if we made it more like an app — nav buttons to open sections, so the
design always occupies at least half the screen." Below 880px in portrait (phones and
iPad mini upright; landscape keeps this week's side-by-side split) the studio now is
that app: the artwork, chip bar and tray fill the screen above a five-tab nav —
Canvas (Source + Size), Colour, Engine, Look, Export — and a tab opens its panel
section as a sheet between art and nav. The sheet is 42dvh, capped at 50dvh minus the
nav so the stage can never drop under half the screen (on a 667px phone 42dvh alone
left 49.6%); it scrolls inside; the same tab, the ✕, or a pull on its grip closes it.
The art stays fully live with a sheet open — that is what the half is for — so a tap
on it does not dismiss the sheet.

It is pure state. `data-sheet` on .wrap names the open section; CSS shows that one
section and sizes stage and sheet from it, the canvas re-measures on the change, and
the tray folds if the card would drop under its floor. Nothing is re-wired: every
control keeps its boot binding, Source and Size share the Canvas sheet by sharing a
data-nav, a collapsed section (Output boots that way) is expanded by the shell because
inert is not a CSS matter, the chip-bar docks and the section sheet are mutually
exclusive, and the colour editor and docks sit on the nav instead of the screen edge.
revealSection opens the right sheet first, so a tray layer row still jumps to the
controls. The status line, which had no home left, speaks once as a toast over the art
when its message changes. The drag grip and its clamps are retired here; the stage
height is a constant of the layout now.

The shell's media query — `(max-width:879px) and (not ((orientation:landscape) and
(min-aspect-ratio:4/3)))`, level-4 `not` — is one string in CSS and JS, pinned equal by
tests/app-shell.test.js along with nav↔section parity, so a tab can never point at
nothing and the shell can never switch on in JS where its CSS did not. A browser that
cannot parse it keeps the stacked layout.

Verified headless with touch at 390x844, 375x667 and 744x1133 (iPad mini): boot with
the art full, toast on shuffle, every tab opens only its section, stage ≥ 50% with a
sheet open (51.4 / 50.0 / 53.1%), tile taps select, a slider drags, Export expands the
collapsed Output, pull-down and ✕ close, Canvas shows both sections, a dock closes the
sheet and sits on the nav, a tray row opens Engine, the colour editor sits above the
nav, rotation leaves and re-enters the shell cleanly; landscape and 1280x900 unchanged.
140 tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LcPnaMoccEmKYGA4nrZXd6
…Size at the end

Danial, after the first shell: "make the pattern full screen, and the sizes remain at
the end — the design covers the screen and the nav bar just pops up with the settings."

So on portrait phones the canvas now fills the viewport edge to edge (the same rules
the embed uses; applyAspect's inline ratio is overridden, object-fit keeps a running
recording letterboxed rather than stretched), the chip bar floats over the top under
a scrim and clear of the notch, the nav floats over the bottom, and the sheets float
above the nav instead of shrinking the stage. The art is always the whole screen; a
sheet covers at most 42dvh of it, capped so it never covers half.

Size moved into the Export sheet. The frame is an export decision here — runExport
renders at exportTarget whatever box the canvas is in, and the fader sets that
target — so the screen just shows the pattern and the choice waits until the end.
The Canvas tab went with it: Source is behind an image chip on the chip bar now,
beside Text and Frame, because a photo is something you put ON the canvas; its sheet
is titled Image. The freed tab is Browse: the stage tray (Layers / Recent / Saved /
Explore), which had no "under the artwork" left to live in, is a sheet of the same
size, and its shelves wrap into a three-column grid that scrolls down instead of one
row built for a 92px tray. A tray layer row still jumps to the Engine sheet.

Nav: Colour · Engine · Look · Browse · Export. The parity test now pins Browse (no
section) and Image (no tab) explicitly, and that Size and Output share the Export
sheet. Desktop and landscape unchanged (verified); 140 tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LcPnaMoccEmKYGA4nrZXd6
On a phone the bottom-left corner is the app shell's nav, and the PREVIEW badge was
sitting on the Colour tab's label (pointer-events:none, so taps passed, but it hid
the word). A one-line media query in the injected markup parks it under the chip bar
on narrow screens; desktops keep the footer corner.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LcPnaMoccEmKYGA4nrZXd6
@enonforetsam enonforetsam changed the title Mobile app shell: bottom nav + one section at a time, artwork never below half the screen Mobile app shell: full-bleed art, floating nav, one section as a sheet; Size at the end Aug 23, 2026
enonforetsam and others added 26 commits August 23, 2026 14:45
Moved under the chip bar it was still a red tag on the art, which on a phone is the
whole screen. Hidden below 880px; the x-robots-tag header keeps preview and staging
out of the index either way, and desktops still show the badge.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LcPnaMoccEmKYGA4nrZXd6
Danial, from the Look sheet: "can we have the word Speed and the line on the same
row? saves space." A stacked caption per slider spent a third of a short sheet on
words. The caption is display:contents in the shell, so its text and its <output>
become grid items beside the range — name | track | value, 40px a row — and five
sliders plus the lens strip now fit above the fold. Desktop rows are unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LcPnaMoccEmKYGA4nrZXd6
Danial, from the Colour sheet: "reduce the height; just 8 small squares of colours, no
labels; then the 4-part colour box that opens the small colour pointer; remove all the
sliding bars, we want to simplify."

The editor's body now shows in place inside the sheet — not as its own pinned sheet —
with only the palette grid (eight squares, no captions, the Custom tile dropped: editing
a swatch already turns a preset into Custom) and the four stop swatches, each the OS
colour picker. Ramp line, MORE, stop tabs, pads, channel sliders, hex, harmony and
history are all gone here; nothing is rewired, and the sheet is only as tall as those
two rows. Desktop keeps the full editor behind MORE.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LcPnaMoccEmKYGA4nrZXd6
… fill

Danial: "make the 8 colour boxes flat with rounded corners — it looks half-3D; remove
the top tab with the Colour label and the x; reduce the height further." The engine
tile's bevel (inset highlight + drop shadow) is gone here: a 9px-rounded square with a
hairline, the active one ringed; the sheet head is hidden for Colour — the lit tab
under it is the way out — and the swatches drop to 40px, so the sheet is ~140px.

Also: over a near-white pattern the outlined chips (undo, redo, save…) all but
vanished; in the shell they get a faint dark fill so they read on any art.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LcPnaMoccEmKYGA4nrZXd6
Danial: "the top row of smaller colour blocks don't match the parts under it — remove
them, keep the 4 underneath, and have a shuffle button on the right."

With a custom or shuffled palette none of the eight presets was lit, and a row of
colours that did not match the four beneath read as a bug. The row is gone. The sheet
is now one line: the four stop swatches (each the OS colour picker) and a shuffle that
deals a new palette — a preset most of the time, a procedural gradient some of the
time, never the one showing — with the same housekeeping as a palette tap. The
shuffle sits outside #swatchRow so Chrome's disabled row cannot take it with it.
~97px tall; desktop unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LcPnaMoccEmKYGA4nrZXd6
Danial, from the Engine sheet: "too high — reduce the size of the squares, remove the
top tab title and x button too." The strip tiles drop from 100px to 64px (five in view
on a 390px phone instead of three and a half), the layer tabs and the Shuffle / Add
layer row tighten, and the sheet is content-sized: ~190px with one layer, ~280 with
the mix row. The sheet head (grip, title, ✕) is gone on every sheet now, not just
Colour — the lit tab under a sheet is the way out, and every pixel of a sheet is
controls. Look and Export keep their full height and scroll inside.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LcPnaMoccEmKYGA4nrZXd6
Danial: "when a tab is open, if I tap on the art, the tab collapses. Also make the
tab glass, so it's see-through."

A clean tap on the canvas with a sheet open now closes it and does nothing else (no
reseed — that was the tap's job); a drag still ripples, which is what panMoved already
told apart. The sheets (controls and Browse) and the nav are frosted: translucent
fills with an 18px backdrop blur, so the art stays the room behind every control.
backdrop-filter makes the sheet a containing block for fixed descendants — the colour
editor is static inside it on phones and the docks live in the stage, so nothing
shifts. Desktop untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LcPnaMoccEmKYGA4nrZXd6
Danial: "I don't see the translucent." A 74% tint over a dark piece read as opaque
grey; the sheets drop to a 50% tint (nav and Browse to 55%) with a 24px blur — the
blur, not the tint, carries legibility now — and the art shows plainly through every
sheet. Checked over a dark ember piece and a bright one: labels and values stay readable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LcPnaMoccEmKYGA4nrZXd6
…L canvas

Danial's iPhone showed the sheet flat grey where Chrome showed glass: Safari's
backdrop-filter samples the page behind the sheet but not the WebGL canvas, so it
blurred the stage's dark grid and tinted that. The glass is made by hand now — one
fixed #glass pane behind the open sheet holding a 48px-wide canvas that mirrors the
slice of art under the sheet (~15fps while a sheet is open, stops itself when none
is), CSS-blurred and drawn 20% oversize so the blur has no fringe, with the tint as a
pseudo-element on the pane (light for the control sheets, dark for Browse). The sheets
themselves are transparent. Sized to the sheet each frame, so it follows the short
Colour and Engine sheets and the centred tablet sheet. Desktop untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LcPnaMoccEmKYGA4nrZXd6
Danial: "too much space taken here — keep it a maximum of 3 rows." With three layers
the Shuffle / Edit top layer / Remove layer N row wrapped to a fourth line. The three
actions are equal thirds now, tighter type, clipped rather than wrapped, so the sheet
is always tabs / tiles / actions. Editing layer 2 or 3 adds its controls as ONE row
— the blend select and the Mix slider side by side — instead of two, so that case is
four rows, not five. Desktop untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LcPnaMoccEmKYGA4nrZXd6
Danial: "when tapping outside the pop-up, let the tab close animated, smoothly; same
when I tap a nav, let the tab pop up smoothly." A sheet cannot animate from
display:none, so in the shell the control sheet and the Browse tray are always laid
out — parked below the screen, invisible, inert — and a tab moves them up with a
340ms ease-out; closing (tab, image chip, or a tap on the art) sets data-closing on
.wrap, which keeps the open section on screen while the sheet slides back down over
280ms, then the shell drops both attributes. The frosted pane follows the moving sheet
(it is sized from the sheet's rect each frame) and fades with it. A tap on a tab or the
chip while a sheet is still leaving counts it as closed, so it reopens instead of
sticking; the same-section jump from a tray row does the same. Reduced-motion users
get the old instant switch. Verified headless at 1× DPR (3× starves swiftshader enough
to make mid-flight timings meaningless).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LcPnaMoccEmKYGA4nrZXd6
Danial: "remove the browse tab, nothing there." Nav is Colour · Engine · Look ·
Export; the stage tray has no tab on phones and is hidden in the shell (desktop and
landscape keep it). The browse-sheet CSS, the dark pane variant and the tray branch of
the glass pane are removed; the parity test pins the four.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LcPnaMoccEmKYGA4nrZXd6
Danial: "why is there no image for these finish squares." Because there never was one:
thumbSpecs built preview jobs for engines, lenses, screens and the halftone dot — and
none for the materials — while renderOneThumb forced material to 0 on every control
preview, so all six Finish squares were bare black cards with a label. They now render
the same soft gyroid through each material (the guard yields to a job's own material,
the same rule every other override already followed), so the strip is a row of
surfaces: none, glass, metal, sand, liquid, molten. Applies on desktop too, where they
were just as black.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LcPnaMoccEmKYGA4nrZXd6
Danial: "make sure the squares in Look are smaller too." The 64px tile was scoped to
the Engine sheet; it now covers every picker strip inside a sheet — lens, screen and
finish included. The 100px tile stays a desktop size.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LcPnaMoccEmKYGA4nrZXd6
Danial: "reduce the size of the small circles on the line, then two columns of
sliders to save space, and reduce the spacing in height between them." The sheet's
sliders pair up — Speed/Zoom, Warp/Grain — on 36px rows at a 38px pitch with a 16px
thumb instead of 26px; the row keeps its full height so the target stays a thumb's
worth even though the dot is small. The layer mix stays one column: it shares its row
with the blend select. The whole Look sheet now reads without scrolling: motion, the
lens strip, amount and surface. Desktop unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LcPnaMoccEmKYGA4nrZXd6
… a + menu

Danial: "the top nav is currently slideable — just keep Shuffle and Record there for
now, the rest can be in a plus icon style menu." The bar is three fixed things now
(Shuffle · + · Record) and never scrolls, so nothing can hide off its left edge again.
The + opens a Tools sheet: undo, redo, insert an image, save, frame, text & logo,
cursor effect, use on your site, my designs, and the Live/Still pill across the
bottom — each a labelled tile that reads its own aria-label, so the labels cannot
drift from what the buttons say they do.

The chips are MOVED into the menu, not copied, so every one keeps the wiring it was
born with (undo really is the undo chip); leaving the shell — a rotation, a wider
window — puts each back where it came from. Tapping a tool that opens its own dock
closes the menu; Live/Still leaves it open. The menu and the section sheets close each
other, and a tap on the art closes either.

The menu's base CSS sits with the other docks, ABOVE the mobile block: parked after
it, `position:absolute` beat the bottom-sheet rule and the menu opened behind the
canvas — visible, and untappable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LcPnaMoccEmKYGA4nrZXd6
Danial: headings like "Surface" in the Look sheet should be darker and bold. A sheet
has no section title, so these group labels are its only headings — they now take the
panel's ink and 700 weight instead of the fine-print grey they inherited from a
sidebar where the section title carried the hierarchy. Desktop labels are unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LcPnaMoccEmKYGA4nrZXd6
… the furniture

Danial: "what if we also do the same for desktop — nav at the bottom middle, saves
space and gives more room for the art." So the shell stops being a phone layout. The
sidebar is gone: on a desktop the artwork fills the window edge to edge, the chip bar
floats across the top, and the nav is a pill in the middle of the bottom with the four
sections. A tab opens its section as a centred 760px card of frosted glass above the
pill; the docks and the colour editor take the same column and shape.

The stage tray comes back on wide screens as a shelf in that column — Layers, Recent,
Saved and Explore, which a phone had no room for — and steps aside while a sheet is
open. The + tools menu stays a narrow-screen answer: above 880px the bar has room for
every tool, so they live on it and the + is hidden. Crossing 880px moves them either
way and re-measures the canvas.

Two things this retires. The landscape side-by-side split solved a stage that could
collapse to 5px; with the art full-bleed there is no stage to collapse, so it is gone
and phones in landscape get the same shell. The ambient glow behind the card goes too:
there is no card to bloom behind.

The shell's media query is now `screen` in both the CSS and APP_Q, and the test pins
the wide-screen block's existence instead of the old string — without it a desktop
would get phone-sized sheets.

Verified at 1440x900 and 1280x800: art fills the window, nav centred and clear of the
bottom, sheet and docks centred above it with the glass pane tracking the card, tray
shelf present and stepping aside, every tool on the bar, click-on-art closes. Phone
unchanged (390x844: full-bleed, full-width nav, + menu, no tray). 140 tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LcPnaMoccEmKYGA4nrZXd6
Danial: "on hover looks ugly." The global metal-button hover was landing on the nav —
a hard white slab with square corners cutting straight through the pill, and the label
embossed into it. A tab is not a key: hover now just lifts it out of the dark with a
10% white tint in a 14px-rounded box (14% when pressed or already active), the icon
and label go to full white, and the pill clips its children so a tint at either end
can never poke past the radius. Focus-visible gets the same rounded box.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LcPnaMoccEmKYGA4nrZXd6
Danial: "use this for the liquid glass setting" — https://github.com/rdev/liquid-glass-react
(MIT). That library drives the look through backdrop-filter, which is exactly the piece
iOS Safari would not sample from our WebGL canvas (the reason #glass exists as a hand
mirror in the first place). The actual "liquid glass" LOOK — a refracting edge that
bends light, with a colour fringe where it bends — is a separate, independent
technique underneath: an SVG feDisplacementMap fed a generated map. That part has
nothing to do with backdrop-filter, so it is ported onto our mirror canvas directly
and works everywhere the mirror does.

The math (roundedRectSDF + smoothstep displacement, three slightly-offset copies of
the same warp screen-blended for the aberration) is the library's. One thing had to
change: their box is a fixed 0.3/0.2 FRACTION of the container, tuned for one
pill-shaped demo. Our sheets run from a 97px strip to a full-height column, and a
fractional box goes flat on the strip and swallows most of the column — so the box
here is inset from each edge by a small constant, capped, PHYSICAL rim (a real glass
bevel does not get proportionally thicker on a bigger pane) rather than a percentage,
and the transition band is likewise capped to a few pixels regardless of panel size.

Two real bugs surfaced building this, both from the first draft's normalisation:
(1) encoding the raw pull-toward-centre vector and dividing every pixel by the single
largest one put that "largest" value in the panel's far CORNER — clipped by its own
rounded corner anyway, but its huge magnitude rounded the real, visible rim down to
nothing (screenshotted: no visible warp at all). Direction and strength are kept
separate now — strength a bounded 0..1 curve, direction a unit vector — so an
off-screen corner can never wash out what is on screen. (2) the rim's reach scaled
with the panel's shorter side with no tight cap, so a moderately tall sheet got an
~80px band from each edge — a third of the sheet warped, reading as one diagonal
smear rather than a lensed edge (screenshotted). Capped to ~8-22px fixed regardless
of panel size fixed it — a thin rim on every sheet, phone and desktop alike.

Verified headless (Chrome/swiftshader, which supports feDisplacementMap): the map
generates and updates per sheet, three distinct non-zero scales feed the R/G/B
passes, the canvas filter chains url()+blur, and screenshots across Colour (a 97px
strip), Look (full height) and Export confirm a thin lensed rim with a hint of
colour fringe at the boundary and a clean flat interior — not a smear, not invisible.
140 tests, full shell regression clean. iOS Safari's own SVG-filter rendering can't
be verified headlessly, but feDisplacementMap is a decades-old standard SVG 1.1
primitive — not the backdrop-filter-over-canvas gap that actually needed a workaround.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LcPnaMoccEmKYGA4nrZXd6
…n a tab this tall

Danial: "on hover, the edges show — I want it to be clean." The rounded-tint fill from
the last hover fix had its own problem: align-items:stretch makes each tab the pill's
full ~64px height, and a 14px border-radius only rounds the top and bottom of a box
that tall — the two long sides in between are a dead straight line at any fill
opacity, which is exactly the edge in his screenshot. No inset or narrower rectangle
fixes this without either shrinking the hit target or visibly fighting the pill's own
rounded shape.

So there is no fill at all now. A tab lifts out of the dark by colour alone on hover —
exactly how the active tab already reads, with just its colour and underline — and
the pressed state is a scale dip (0.94), not a fill either, so pressing never has an
edge to show in the first place. Verified: hover computes background-color:transparent
on both an active and an inactive tab, colour still brightens to full white; the
pressed transform is confirmed via CDP's forcePseudoState (Puppeteer's synthetic
mouse-down doesn't reliably hold :active through a script round-trip in headless
Chrome, so a plain mouse.down()/getComputedStyle() check is not trustworthy here —
forcePseudoState is). 140 tests, full desktop shell regression clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LcPnaMoccEmKYGA4nrZXd6
Danial: "for this part, I don't wanna see the grey border when I scroll, on the right
and left — should just be one background colour, with a thin scroll line on the right
hand side." Headless Chrome doesn't reproduce a grey edge on scroll (screenshotted
before/after, clean both ways), which points at a native scrollbar rendering that
only some browsers/OS settings produce — Safari's "always show scrollbars" in
particular, un-reproducible outside a real browser window — rather than something in
this file's own cascade that a headless run would also show.

Given that, the direct fix is to stop depending on the browser's own scrollbar chrome
at all and build the thin line that was actually asked for. #panelScrollBar is a
sibling of .panelScroll (inside it, position:absolute would scroll away with the
content — out here it stays put while its height and position track scroll progress),
3px wide, hidden entirely when nothing overflows. Height is the visible fraction of
the content, position is the scroll fraction — the two numbers a scrollbar has always
shown, nothing more. It updates on .panelScroll's own scroll event for zero lag
during a gesture, and every frame glassTick already runs while a sheet is open (no
new wiring) — catching a layer add or a section expanding, which changes
scrollHeight with no resize event of its own.

The browser's native scrollbar is suppressed harder alongside it: scrollbar-width
already asked cleanly, but the WebKit pseudo-element now carries !important — the one
place in this file fighting a browser default rather than its own cascade, which is
what justifies it.

Verified headless: hidden when short (Colour), visible and 3px when a sheet
overflows (Look), the thumb moves with scrollTop and sits exactly at the bottom at
max scroll, panelScroll's offsetWidth still equals its clientWidth (no native
scrollbar reserving space), and it re-measures within a frame after adding engine
layers with the sheet still open. 140 tests, full shell + liquid-glass regression
clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LcPnaMoccEmKYGA4nrZXd6
…on the left

Danial: "make the top nav similar to the bottom nav, give it a background like the
bottom nav, and just have the icons on it. Also add the Fluid title and the GitHub
icon on the left hand side."

The chip bar was loose icons over a gradient scrim; it is its own opaque pill now,
the same fill and blur as the nav below it — flush and square on a phone (mirroring
the nav's own phone treatment), lifted into a floating rounded pill on a wide screen
at the same distance from the top that the nav sits from the bottom. The gradient
scrim it replaced only existed because the bar used to be transparent; a bar with its
own background does not need one.

Shuffle drops its "SHUFFLE" text, matching every other chip on the bar — scoped to
`#canvasChips #refreshChip` specifically, since that literal text is also its own
label wherever else it might render (nothing else moves it in practice: Shuffle and
Record are the two chips that stay on the bar permanently, never behind the tools
menu, so this scoping is a formality more than a live concern).

Fluid + the GitHub link join the bar on the left — a second, bar-sized copy of the
same two elements the real header already carries, exactly the .mobTitle pattern:
duplicate for the new context rather than rewire the original, which stays
display:none in the shell for good.

One real bug, not a styling detail: giving #canvasChips backdrop-filter directly
made IT the containing block for every position:fixed descendant inside it — and the
docks (#textDock, #frameDock, #cursorDock, #designDock, #moreDock) are markup
children of this bar. #textDock rendered with its bottom edge at y=-23, entirely off
-screen above the art; the same break silently took down Undo-from-the-menu and the
Live/Still toggle, both reached through docks in that same subtree. The fill moved
to a ::before pseudo-element instead — it has no descendants of its own to break,
and looks identical.

Verified headless at 390 and 1600 wide: bar has the nav's fill+blur, Fluid+GitHub
present and the link points at the repo, Shuffle is 32px icon-only, the docks sit
correctly above the nav again (bottom 808 vs nav top 816, was -23), Shuffle still
shuffles with its text hidden. 140 tests, full shell + liquid-glass regression clean
once the containing-block fix landed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LcPnaMoccEmKYGA4nrZXd6
Two things, from two screenshots.

The chunky square outline Danial saw "on hover" on the top bar's icons was actually
:focus-visible — the generic site-wide ring (2px, offset OUTWARD by 2px) is sized for
the sidebar's bigger buttons; on a 28px chip that outward bulge reads as a square
poking past the 8px rounding, exactly the same failure mode as the nav-tab hover fix
a few turns back. .chipIcon:focus-visible now insets instead (-2px, following the
small shape cleanly), the same fix pattern .navBtn already uses.

"Can we have the top, longer bar merge into the top nav?" — the stage tray (Layers /
Recent / Saved / Explore, wide screens only) floated as its own separately-shadowed
pill 8px above the nav pill; now they are one shape. Same width, same fill, flush —
only the outer corners stay rounded (tray's top, nav's bottom), the seam between them
plain. Nav's square-topped state is scoped to :not([data-sheet]), so the moment a
sheet opens and the tray fades (its own existing behaviour, untouched), the nav's
base all-four-corners radius applies again on its own — it is never a pill sitting
there with a corner cut off for no reason once the thing that was merged above it is
gone. Confirmed the merge survives collapsing/expanding the tray (its height is not
fixed) since the flush position is bottom:var(--navH), not a hardcoded gap.

One thing to flag while committing: the earlier merge attempt (the same turn) added
a SECOND #stageTray rule ahead of the real one in source order rather than editing it
in place — same specificity, later-wins, so it did nothing until caught by measuring
the actual computed styles (gap still 8px, background still the old colour) rather
than trusting the diff looked right. Folded into the one real rule now.

Verified headless at 1440 wide: ring inset -2px, tray+nav same width/x-position, zero
gap, correct corner split, identical background, tray casts no shadow of its own,
merge holds with the tray expanded (3 layer rows) as well as collapsed, nav regains
full rounding within a frame of a sheet opening. Phone unaffected (tray stays
display:none, nav's own flush 0-radius styling untouched — the whole feature lives
inside the wide-screen block). 140 tests, full shell + liquid-glass regression clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LcPnaMoccEmKYGA4nrZXd6
Danial: "from the top nav, let's remove Text and Frame for now." display:none only —
their docks, JS wiring, and MORE_IDS entries are all untouched, so bringing them back
later is a one-line revert of the hide selector, not a rebuild.

Two rules were needed, not one: the shared #appNav,.sheetHead,... hide list covers
them on a wide screen (where they'd otherwise sit directly on the bar), but inside
the tools menu the existing #moreGrid .chipIcon{display:flex} rule outranks a plain
#textChip ID selector (a class beats nothing in the third specificity column when the
ID count ties) and silently re-showed them there. A second rule scoped to
#moreGrid #textChip,#moreGrid #frameChip (two IDs, clean win) closes that gap — the
tools menu now closes up to two full rows with no hole where they used to sit.

Verified headless: hidden and uncounted as visible bar children on a wide screen,
hidden and absent from the tools-menu tile list on a phone, no errors either way.
140 tests, full shell + liquid-glass regression clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LcPnaMoccEmKYGA4nrZXd6
Danial: hovering a sheet (Colour, or any other) shows a lighter grey patch with what
reads as a darker border around it; wants the card to stay one single colour.

That patch was .panel section:hover/:has(:focus-visible) painting a translucent white
band — built, several turns before the app shell existed, to answer "which section am
I in" while scrolling PAST several stacked, simultaneously-visible sections in the old
sidebar. The app shell shows exactly one section per sheet, always; there is never a
second one to tell it apart from, so the band now only ever reads as an unexplained
colour shift under the cursor — the "border" Danial saw was never a border, just the
contrast edge where the translucent rectangle stopped.

background-color is turned off on both rules — the :hover and :has(:focus-visible)
mechanics stay, so this reverts cleanly if a layout ever again shows more than one
section at once.

Verified headless: Colour and Look sections both report the same background-color
before and after a hover move (transparent, unchanged) — no colour shift anywhere.
140 tests, full shell + liquid-glass regression clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LcPnaMoccEmKYGA4nrZXd6
enonforetsam and others added 2 commits August 25, 2026 16:50
Danial: "the buttons here still look slightly 3D — make it similar to the icons on
the bottom nav bar, a flat design."

.chipIcon never opted out of the base skeuomorphic button (box-shadow, text-shadow,
background-image are all still that rule's) the way .navBtn explicitly does — a small
embossed metal key sitting next to genuinely flat nav icons. Every state now zeroes
them out the same way .navBtn does: default, :hover, and :active (a 0.92 scale dip
replacing the base button's translateY emboss). Verified: box-shadow/background-image/
text-shadow all compute to none in every state.

While touching the exact :hover rule, found and fixed a second, real bug in the same
selector: html:not(.embed) .wrap #canvasChips .chipIcon{background-color:rgba(6,6,10,.42)}
(added a few turns ago for legibility over pale art) out-specifies .chipIcon:hover's
background (one extra class), so hover never actually inverted — the icon's colour
flipped toward --ink-c while its background stayed dark, reading as invisible. A
:hover-suffixed version of that same selector, inside the same hover-capable-pointer
gate, out-specifies it in turn.

This second fix could not be confirmed by the usual headless screenshot-and-measure
loop today — the test environment hit a real wall verifying it (documented for
whoever debugs this class of thing next): a hover simulated via page.mouse.move() or
CDP's forcePseudoState silently failed to engage ANY :hover style on this page, even
on a fully clean browser relaunch, even on a PREVIOUSLY-verified-working element
(.navBtn); Puppeteer's dedicated page.hover() does trigger it correctly, proving the
gate itself is fine. Even so, reading the swapped-in background-color afterward kept
returning the old value — including once with a plain inline style, and once with an
inline style force-set to !important, both confirmed present via el.style.cssText and
getPropertyValue() immediately beforehand. That is not a CSS cascade result under any
spec — inline !important cannot lose to a stylesheet rule — which means the read
itself is unreliable in this specific headless/swiftshader session, not that the fix
is wrong. The selector's specificity is unambiguous by the same math used earlier
this session for the nav-tab fix (one full pseudo-class ahead of the rule it needs to
beat), and a literal test value (background-color:red, no custom property involved)
DID render correctly on hover before this diagnosis moved on to var(--ink) — so the
mechanism works; only reading the final --ink-based value back out kept failing.
Shipping on that reasoning, flagged plainly: this one needs a look on a real machine,
unlike everything else in this change, which measured clean.

140 tests, flatness (the primary ask) fully verified in every state.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LcPnaMoccEmKYGA4nrZXd6
Live/Still drops the boxed segmented-control look for two plain words
(dim vs full ink) — the pill read as an iOS switch next to the bar's
flat icons. Shuffle leaves the bar entirely on wide screens: its own
50px circle (matched to the bar's own height) fixed to the screen's
top-right corner, with a glass highlight and glow instead of the flat
treatment everything else just got. The status toast is now a child
of the bar rather than a separately-sized floating pill, so it always
matches the bar's real width and sits flush underneath it as one
shape, flattening the bar's bottom corners only while it's showing.

Also fixes a regression the flatten caused in the phone "Tools" tile:
Live/Still had been stretched edge-to-edge by flex:1 written for the
old bordered pill; without the border it read as two unrelated words
a tile apart, so it's centered as a pair instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0151ATBDk4Z4eJLTusoqEg6a
@enonforetsam
enonforetsam marked this pull request as ready for review August 26, 2026 12:07
@enonforetsam
enonforetsam merged commit d84dfc6 into master Aug 26, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant