SettingsButton, QbzToggle and QbzSelect declared their own `enabled` over
Item's, and ReactiveRingsPanel its own `palette` over Item's QPalette: Qt
flagged all four at start (propertyCache: "overrides a member of the base
object"). The shadowed `enabled` never disabled input by itself — each
control filtered clicks and keys by hand, and any child that did not would
have kept receiving events. The inherited property now drives the visuals,
the focus policy and the guards, and disables the subtree the way Item
does; the rings' colour list is `ringPalette`.
cc
Address the owner's kiosk feedback on top of the release hardening:
- Chrome: the back bar is a drag surface and shrank to 44px with one shared
button form for Back/Forward/Connect/Cast; Qobuz Connect and Cast are a
permanent cluster in the bar; the dead Share-logs button left the kiosk
Settings header.
- Transport bar: the hidden overflow menu is gone; Volume is always visible
with a vertical sheet, and Desktop mode has its own button.
- Now Playing: one centred transport row (New-bar order); lyrics are centred
(shared LyricsLinesView gains an opt-in centered, desktop default false).
- Local Library: the media funnel and an Open (ephemeral) entry point reach
the kiosk; Artists is its own multi-column round-card grid (kiosk-only, the
desktop LocalArtistsTab is untouched); the Artists drill-down query no
longer hangs off a stale derived guard.
- Search: the All-tab dashboard shelves render again — they were mounted in a
Loader Component that read the delegate id under reuse and bound an empty
model; they are direct children now.
- Album detail: icon actions the height of the quality badge plus play next /
later / queue, an inline floating quality badge, and zebra rows; both album
headers show a quality badge.
- Immersive: a quiet auto-hiding kiosk exit moved to the bottom, clear of the
window controls.
Regression coverage in scripts/test_qt_kiosk_feedback.mjs (wired into the
runtime gate). Verified on a private authenticated Xvfb; QML audits 6/6,
Node gate 5/5, release build 0 warnings.
cc
Integrate the Kiosk profile hardening round: 12 dedicated Kiosk views plus
22 shared opt-in hosts, shared navigation history with Back/Forward, Albums
as the default library tab, native paged Local Library (artists/tracks),
lightweight MyQBZ grid and detail, Ephemeral lifecycle, bounded lists and
artwork sized to the physical frame, static skeletons, 64px transport bar,
Settings touch reflow and a persistent Immersive exit.
Desktop keeps its prior behavior: every shared component defaults to the
desktop path and only opts into Kiosk behavior through explicit properties
(origin-relative ScrollMemory, viewport-bounded artwork requests).
Regression coverage: kiosk navigation and artwork Node tests added to the
runtime gate, scroll-origin fixtures for fixed and grouped rows, i18n
strings for all eight locales.
Verified by 750 release tests, six QML audits, Kiosk smokes at 800x480 and
1280x800 and a Desktop A/B with seven pixel-identical captures.
cc
The cinematic split card's shared shrink-to-fit typeScale is retired: a
long title now word-wraps at its nominal size instead of scaling the
whole metadata block down, and the album/artist lines elide instead of
dictating a reduction. The bottom-right song card's badge also drops the
overAmbient chip — that card only mounts over dark surfaces and the dark
backdrop read as a bigger badge; the themed look was already right.
cc
The window-controls capsule only offered fullscreen and exit; on
platforms without native traffic lights there was no way to minimize or
maximize from immersive. Non-macOS expands to four slots (minus,
square/copy for maximize/restore, fullscreen, X) with the same
expansion-bool gating; macOS keeps the two-slot capsule since the native
controls already cover the rest.
cc
The card's waveform, seek fill, elapsed time and Now Playing indicator
now share the AmbientAccent album-palette color (the local complement
derivation is gone). The Now Playing indicator and the quality badge grow
1.5x (EqualizerBars gains a barScale knob), and the metadata stacks three
lines - title, album, artist - with the shared type scale measuring all
three. The lyrics variant dims its karaoke base like the other immersive
lyric surfaces.
cc
New theme/AmbientAccent helper: reads ambientPrimary (the album's own
dominant hue) and lifts its lightness into a legible band. The shell's
ambientAccent is the +180-degree complement, which is how a deep-blue
cover ended up with orange chrome; accents now stay in the album palette.
ImmersiveTrackMeta tints its Now Playing indicator with it instead of the
theme accent (no contrast guarantee over ambient), opts its badge into
overAmbient and shadows its text rows for light covers.
cc
Immersive surfaces sit directly on the ambient field, where theme tokens
have no contrast guarantee — a light cover washed the split Track Info
panel and the quality badges out completely. QualityBadgeFull,
TrackInfoBody, InfoMetaCell and InfoCreditCell gain an opt-in overAmbient
mode: fixed light text with the restrained native shadow the lyrics
surfaces already use, and the badge swaps the theme surface for its own
translucent dark scrim covering only the chip. Defaults stay off, so the
desktop modal and every themed mount are untouched. The immersive Track
Info panel and song card opt in.
cc
LyricsLineRow gains inactiveColor (default theme.textSecondary, so the
desktop lyrics panel is unchanged) and LyricsLinesView forwards it. The
immersive lyric surfaces pass 35% white: their active overlay is solid
white and the theme's textSecondary base was barely dimmer, so the timed
karaoke fill read as static text.
cc
Keep segmented controls within their visual well, use the active theme for playback indicators, disconnect inactive pulse receivers and improve near-fit song metadata.
cx
Hide the waveform controls and force every now-playing bar back to the classic seekbar while preserving the implementation for a later release. Keep full-screen timed lyrics centered, slightly larger and wrapped without changing the approved typography or fill.
cx
Reuse the split lyrics renderer for full-screen timed fill, add a contrast shadow and expose the immersive song card there. Render the compact seekbar as a stable three-sine motif and make the larger waveform use normalized rounded capsules with stronger peaks.
cx
Make immersive scopes responsive and derive readable colors from album artwork. Use distinct full-size bar and compact contour renderers for the seek waveform.
cx
Add a theme-aware goniometer and pitch-locked oscilloscope to the large dock and immersive focus modes.
Gate scope DSP and scenegraph updates by actual panel visibility so hidden modes remain idle, including while shader scenes cover immersive panels.
cx
9c47d5b2e moved it unconditionally, and it had no business moving on Linux. The
whole reason for the move is that on macOS the band's left edge is the native
traffic lights' corner (x ~ 7-85, the pill's exact spot). Nothing else has
traffic lights, so nothing else had the problem — the control just relocated on
platforms nobody asked about.
Gated on `QbzShell.isMacos && !QbzShell.systemTitleBar`, the same predicate the
view's old `chromeLeftInset` used, so the trailing position appears in exactly
the configuration that motivated it. Everywhere else the trigger is back at
x = 0, where it has always been.
Positioned with `x` rather than swapped anchors: toggling between anchors.left
and anchors.right means clearing one with `undefined` on every flip, and this is
one expression either way.
Built and smoked on Linux (gate green). macOS behaviour is unchanged by this
commit — it was already the trailing one there.
cc
The rule was derived from two data points and generalised into a negation:
owner's Vulkan + mirror=false -> upside-down (Vulkan needs the mirror)
Xvfb Mesa GL + mirror=true -> upside-down (GL does not)
"mirror on everything EXCEPT OpenGL" fits both, and it was wrong about the one
API nobody had run. On the Mac mini M2 the Line Bed comes out upside-down, so
Metal wants mirror=false like OpenGL rather than =true like Vulkan — the
generalisation reached a third platform on the strength of a shape it had never
been tested against.
Kept as a NEGATION with Metal excluded rather than rewritten as "Vulkan ||
D3D". Naming the APIs that DO mirror would silently stop mirroring on any enum
this file failed to list: an unknown member is `undefined` in QML and compares
false, so a typo or a future backend would degrade to "no mirror" with no error.
This way only Metal's behaviour changes and every other backend keeps exactly
what it was verified with.
All four RHI scenes carry the line, because the convention belongs to the
graphics API and not to the scene — a per-scene split would be the surprising
result, not the expected one. Only LineBed made it observable: it is the one
directional scene, so a flip reads instantly, where Plasma and TunnelFlow are
near-radial and hide it. RIBBON IS THE ONE TO CHECK NEXT — it is a spectrogram
with a time axis, so if the convention really is per-API it should have been
flipped too, and if it now looks flipped the rule is per-scene after all.
The three sibling headers said "mirror on Vulkan/Metal/D3D" and were corrected
in place; they had become comments that contradict their own code.
Built and smoked on Linux (gate green). The observation this fixes is the
owner's, on the Mac.
cc
It sat at the band's LEFT edge, which on macOS is the traffic lights' corner —
x ~ 7-85, the pill's exact spot. The band was therefore being inset sideways to
make room for one 44px control, and that inset is what read as wrong: the shell
HeaderBar's rule is right for the shell, where the lights head a full toolbar
row, and wrong here, where the row is three floating glass pills over artwork.
Moving the pill instead of the row is the smaller change and the better one. The
left half of the band goes empty and the lights get that corner to themselves;
the search field is CENTRED on the band rather than laid out after the trigger,
so nothing else shifts. Every other platform is unaffected — the trigger simply
changed sides.
Its right margin RESERVES the capsule's expanded width (112) plus a 12px gap
rather than anchoring to the capsule's live left edge. The capsule grows
leftward on hover, so anchoring to it would slide this pill sideways every time
the pointer crossed the other one — motion with no meaning. Reserving the
maximum keeps both still. The radius becomes height/2, the same silhouette as
the capsule it now sits next to.
An earlier attempt dropped the WHOLE band below the lights; it is reverted here.
It moved the search field and the capsule down to solve a problem only the
trigger had.
The capsule stays. Worth recording why, since it looks redundant on macOS and is
only half so: its fullscreen toggle writes `Window.visibility`, which the green
light already does — but its X calls `QbzImmersive.open = false`, which EXITS
IMMERSIVE, and no traffic light can do that. It is one of three exits (Escape
and the player bar are the others) and the only visible one in the chrome.
Built and smoked on Linux, built on the Mac mini M2. The macOS geometry needs
eyes on a Mac.
cc
QBZ-I18N WAS NOT FRONTEND-AGNOSTIC, whatever its own header said. No slint or
tauri entry in its Cargo.toml — and eight `include_str!` reaching into
`crates/qbz-ui/translations/`. Deleting the Slint crate, which is planned before
the Qt release, would have failed to compile `qbz-i18n` and with it `qbz-qt`,
i.e. the entire app, over a data directory nobody read as a dependency. The
extraction that created this crate took the code and left the data behind.
The eight .po files move to `crates/qbz-i18n/translations/` (git tracks them as
renames, so history follows) and the includes become `../translations/`.
`qbz-ui/build.rs` borrows them back through `../qbz-i18n/translations` for as
long as that tree exists — the direction of the dependency is what matters, and
it now points the way the deletion needs.
MACOS: THE IMMERSIVE BAND GOES BELOW THE TRAFFIC LIGHTS, not beside them. It
was inset to their right with the shell HeaderBar's rule, which is right for the
shell — the lights sit at the head of a full toolbar row there — and wrong here,
where the row is three floating glass pills over artwork and a 44px pill parked
against the lights reads as debris.
Vertical clearance comes from `SafeArea.margins.top`, the same question the left
inset asks in the other axis: with ExpandedClientAreaHint the client area covers
the titlebar region and SafeArea reports how much of it is unsafe, with a 28px
floor for a 0 reading. The band keeps `leftMargin: 24` on every platform, which
puts the trigger's leading edge in the same 20-24 band the lights start in, and
the 16px gap under them is the band's own margin reused rather than a new
number.
The view trigger matches the cluster's width on macOS (SafeArea left margin,
78 floor): stacked, a 44px pill under a ~78px cluster reads as misaligned even
with the left edges lined up. Its radius becomes height/2 rather than a constant
18, or the widened pill would have been a rounded rectangle beside three
circles. Every other platform keeps the natural 44px, and the glyph was already
centred so it needed nothing.
The menu opens downward from a band that now has more room under it, so the
flyout has more clearance than before rather than less.
Built and smoked on Linux (gate green) and built on the Mac mini M2 (3m32s, no
errors). The macOS geometry itself needs eyes on a Mac.
cc
TWO FINDINGS, both of which falsified something that was being assumed.
FLUIDITY IS NOT FRAME RATE. Driven at QBZ_PULSE_MS=10 the shell presented ~100
frames/s — qt-batches confirmed it, and the script now prints the pulse it ran
with so two logs cannot be confused — and the bars looked exactly as they had at
30. They would: every published frame was applied as a STEP, so the extra
presents repainted the same discrete values three times over. What the Slint
build does differently is not rate, it is `animate bar-h { 90ms }` — it moves
BETWEEN values.
So the interpolation is back, ON THE PULSE, and it costs nothing. The old
interpolator was expensive because of its private 16 ms Timer, not because it
interpolated: it presented at its own rate, unsynchronised with everything else.
Easing on the pulse presents exactly as often as stepping on the pulse did — one
repaint per period either way, only the value differs. Self-parking survives: a
converged ease writes nothing and schedules no repaint. The price is lag, and it
is the honest one — k = 0.45 is ~83% there in three pulses (~100 ms), matching
the reference's 90 ms, against 46 ms of FFT window and 77 ms of Rust smoothing
already in the chain.
The from/to/progress triple is gone with it, and its consumers moved to `cur`.
Note for the next reader: `SpectrumBand` drove all three of its modes from
`Connections` on those properties, and a Connections handler naming a property
that no longer exists is NOT a compile error — QML warns at runtime and simply
never fires. That is how the whole band went dead at once; grep for
`on<Name>Changed` when removing a property, not only for direct reads.
PLASMA WAS DRIVEN BY THE SLOWEST SIGNALS IN THE PIPELINE while the fast ones
travelled beside it unconnected. `transientAmp`, `bandsLo` and `bandsHi` were
published, parsed and stored, and never handed to the item — the shader declared
them and received zeros — so everything ran off the energy bands, which decay
x0.85 per frame (~473 ms) against the bars' x0.65 (~178 ms).
Worse, `beat` stops being a beat on dense material: it decays x0.88 per tick, so
above ~3 hits/s it never falls far enough to come back as a spike and settles
into a shallow ripple around 0.7. A splat multiplied by it becomes a steady
glow, and the busier the music the flatter the picture — the most frenetic input
produced the least motion. `beatAc` subtracts a slow density floor, so a hit
reads as a hit at any tempo.
The scene itself was rebuilt around the reference the owner filmed (Butterchurn
on a percussion-heavy track). Its recipe is three parts: one thin annulus
injected per frame that the feedback warp marches inward, so the concentric
rings ARE the last N frames of audio laid out in space; retention high enough
for that to stack (this held 0.85, where a trail died in ~15 frames and nothing
ever accumulated); and angular structure from reading the spectrum BY ANGLE.
But a CLOSED ring was wrong, and the owner named why: a perfect inner
circumference reads as a drawn shape, hard-edges the field, and at this
retention successive rings pack into one solid disc — which is what killed the
ambient feel. The annulus is gated by angle now, so a hit paints arcs the
conveyor carries inward, the circle never closes, and the gaps stay open for the
background. Noise bends, frays and thins those arcs so they are made of plasma
rather than of compass.
The worms are back at full strength with tighter cores and a contrast curve — a
raw gaussian is all shoulder and reads as haze — and their orbits now reach past
the arc band instead of being trapped inside it. Colour comes from ONE
album-derived triad, so everything used to be a shade of the same hue; a
Rodrigues hue turn splits it into worms, arcs and an ambient wash that sit apart
whatever album is playing. That wash is what stops the background being void for
the foreground to be a cut-out against. A soft limiter replaces the hard clamp:
at 0.945 retention a clamp pins the middle to white and the structure vanishes.
Release build clean, offscreen smoke green, 421/421.
cc
Follow-up to 66ba12bd5, which fixed the coverflow but left two of its
neighbours and a gate that could not see either of them.
QueueTabsPanel built one ImmQueueRow per queue track when opened —
including every collapsed 0-height history slot, which on the reported
session was 1162 of 1935. The slot stays (its height is what makes the
collapsed-history arithmetic work); the row inside is now built only
within a viewport of the visible band.
SuggestionsPanel and AppShell's QueuePanel move behind Loaders. The
queue panel was never the freeze — its rows are paginated at 40 in
Rust — but a closed panel should not be constructed, and the audit
cannot know about a cap that lives on the other side of the bridge.
Gating it also let SuggestionsPanel drop the onVisibleChanged half of
its load hook: mounting IS becoming visible now.
The audit is the real content of this commit. Written narrow — only a
model fed by unbounded user data counts, because the wide rule reports
145 sites and a gate that noisy gets switched off in a day — it then
reported the pre-fix tree CLEAN. A gate that cannot catch the bug it
was written for is worth nothing, so it was tested against that tree
and fixed on two counts:
- CROSS-FILE. The `visible:` sat in ImmersiveView.qml while the two
Repeaters over the whole queue lived in CoverflowPanel.qml. There
is now a transitive index: a component is heavy if its own file has
an unbounded view or if it mounts something that does.
- MULTI-LINE. The coverflow's gate spans two lines, so a first-line
read saw `QbzShaderScene.scene === 0`, matched no known gate, and
skipped it.
With both, it finds all five mounts on the pre-fix tree, including the
two that froze the app. Known gates also grew past tab predicates: the
coverflow was keyed on QbzImmersive.mode.
Verified on a copy of the real session state with the 1935-row queue
restored: launches to a painted Discover, idle at 1.0% CPU.
cc
A session with 1935 persisted queue rows came up frozen for ~28s at
120% CPU, every launch, deterministically. It looked like corrupted
local state — moving ~/.local/share/qbz aside "fixed" it — but nothing
was corrupt: that is simply a queue the UI could not carry.
The chain. AppShell mounts ImmersiveView unconditionally (its own
comment: while closed it is "an invisible, non-interactive Item"), and
`visible: false` stops an item painting, never building. CoverflowPanel
reads the flat WHOLE-queue document — unlike the queue panel's, which
paginates at 40 — and runs two Repeaters over it, the forward list and
a reversed copy. So every launch built ~4000 CoverCards, each an image
with a masked layer, for a panel nobody had opened.
Two gates, because either alone leaves it broken:
- CoverflowPanel and both QueueTabsPanel mounts move behind a Loader
keyed on the condition they already carried in `visible:`. That is
the launch freeze.
- The coverflow delegates become zero-size, non-clipping slots whose
CoverCard exists only within |offset| < 6.5 (the fan paints 3.5;
the margin covers the 320ms ease). That is opening it, which the
Loader alone would still freeze.
The reference does create one element per row and hides |offset| > 3
(CoverflowPanel.slint), and that is fine there: a Slint element is
cheap where a QML delegate is not. Copying its shape literally is what
cost the 28 seconds.
Verified against the real session state, queue restored, not a
synthetic one: 120% CPU -> 2.9%, and a screenshot of Discover fully
painted where it used to sit on the skeleton.
cc
Port the immersive GPU scenes: Plasma, Tunnel, Aurora, Ambient, Spectral
Ribbon and Line Bed as QQuickRhiItems/ShaderEffects under a pulse-driven
ShaderSceneLayer, plus the Qt-only Tunnel Flow (Tauri Canvas2D port) fed
by the Viz16 stream. Scene rows live in the header view menu (no Off row);
g cycles the ring. A fresh immersive open defaults to Split > Lyrics
(owner ruling).
OpenGL link fix (owner smoke: Tunnel black with "uniform _22 declared as
type buf and type buf"): every ShaderEffect fragment with its own uniform
layout now ships a paired vertex stage declaring the identical block —
the shared spectrum.vert only ever linked under Vulkan.
RHI hardening: render pass descriptors on the ping-pong targets (the
Tunnel Flow segfault / Plasma dead window), mirrorVertically tied to the
backend (!== OpenGL), ribbon sub-uploads carry setSourceSize.
km
- AlbumReactivePanel: drop the eight 100ms Behaviors on 30 Hz data (a
retriggered Behavior animates at display rate and every animation
frame is a full-window present), and read the settle — not the
publishing bridge list — in the energy formulas (the .length guard
re-evaluated the visual bindings on every FFT publish).
- LyricsSyncEngine: the fast cadence rides the pulse; the 250 ms idle
re-check keeps its own timer.
- EqualizerBars: the phase accumulates on the pulse and the 'active'
gate is host-provided (invisible subtrees still dirty the window).
Measured on the owner's machine, immersive reactive panel open:
132 -> ~33 presents/s, 83% -> ~25% GPU.
km
Qt Quick has no partial redraws: any dirty item presents the WHOLE
window, and on the owner's hybrid laptop each full-window present costs
~1.2% GPU flat (KWin composite + dGPU scanout; area-independent, while
the app's own render is 0.08 ms/frame). The bill was presents/s, and
the shell ran unsynchronised ~30 Hz clocks plus two leaks:
- QbzShell.pulseMs: ONE Rust-driven 33 ms edge that every continuous
animator ticks on (knob QBZ_PULSE_MS). VizSettle now stashes on
publish and applies on the pulse (its 16 ms interpolation timer is
gone); AmbientField and ImmersiveAtmosphere ride the same edge.
- VizSettle writes NOTHING while not live: the always-mounted but
invisible immersive panels were dirtying 28 bar rects per FFT
publish (named via QT_LOGGING_RULES=qt.quick.dirty=true).
- AmbientField renders inline again: ShaderEffectSource
scheduleUpdate() double-presents (minimal repro fbo_repro.qml in the
investigation folder).
- Background mode 2 (ImmersiveAtmosphere) composites its four drifting
layers + base + dim + scrim in ONE opaque ShaderEffect
(atmosphere.frag, baked by build.rs); numeric parity vs the Image
stack verified (atmo-shader-parity-check.py, delta 0.0). The
four-Image stack stays as the software/failure fallback.
Measured on the owner's machine: 93-97% -> 25-26% GPU, ~34 W -> 12-13 W,
~33 presents/s in both background modes. Full story:
qbz-nix-docs/qt-frontend/2026-08-11-scenegraph-batches/GPU-COST-INVESTIGATION.md
km
The reference shows a skeleton while loading and never paints cached rows
first, and its comments present that as the design. It is not: the instant
paint existed during development and was removed because it GLITCHED -
results visibly jumped between the cached paint and the fresh one. The
owner's reading is that the glitch was a Slint rendering problem rather than
something inherent to stale-while-revalidate, so it comes back here as a
reversible experiment: DEFAULT ON, with a ui_prefs opt-out
(cortinilla_instant_paint) that needs no rebuild to flip. The skeleton path
stays fully implemented as both the opt-out target and the cold-miss path.
What is cached is the FINISHED payload, not the API response. Reviving the
existing CAPA A result cache would have left the expensive half on the table:
a cortinilla payload is the response AFTER ranking, truncation, i18n titles,
the top-result pick and - the part no API cache can shortcut - the local
library query and its derived grouping.
qbz-app gains a generic PayloadCache<T>. The store never inspects T, which is
what lets both frontends share it despite their payload types having diverged
(one raw artwork_url vs an art_url/art_path pair). In-memory only: process
lifetime IS the TTL, because a payload embeds local rows and resolved artwork
paths and a persisted entry would resurrect albums the user has deleted.
Four properties are what should keep it from glitching, and each is a real
constraint rather than a hope:
- Same shape in both paints. The entry is written AFTER the local sections
are appended, so there is no mid-apply reflow left to see.
- Equality-gated replacement. CortinillaData derives PartialEq, so an
unchanged repeat query produces ZERO second paint.
- The selection survives - the publish path never writes selectedIndex.
- The scroll survives - C5's contentY latch. Without it the second paint
yanks the user to the top, which is the glitch by another name.
There are exactly TWO write points, and the placement is load-bearing: after
pass-1 artwork resolution, and again at the pass-2 republish once downloaded
covers land. Writing earlier caches rows with empty art_path, and since the
QML draws art_path only, a hit would paint cover-less rows AND the gate would
fire on every hit - the exact repaint this exists to avoid.
Invalidation: bound per session, dropped on logout, and cleared from
reload_browse - the single chokepoint every local-library mutation passes
through, since there is no library-changed signal to subscribe to.
Also fixes the immersive skeleton's scope bug, which is the twin of the one
C4b fixed on the desktop and which I missed there: a Timer derives from
QtObject, which has no parent property, so the unqualified parent resolved
out of the block entirely - the timer ran forever, pulse never changed, and
the skeleton never pulsed. Found by qmllint, not by reading. It picks up the
reduceMotion tick with the fix.
Contract: qbz-nix-docs/qt-frontend/2026-08-03-cortinilla-parity, commit C11
(section 5.c, rulings R1/R6, R7-10).
Gate: build clean, 258/258 tests + 2 new in qbz-app, both QML audits OK,
qmllint clean on both cortinillas, offscreen smoke 0 complaints + home
published.
cc
The owner ruled 3-C: build the seam here so the kiosk port consumes it
instead of having to invent it. The earlier plan was to wait for kiosk to
define it, but kiosk is not implemented - it is a contract - so "wait for
kiosk" meant waiting on a date that does not exist.
The shape is no longer a guess, which was the only objection to building it
early. Two independent sources fix it: the kiosk contract mandates "FORCED
reduce-motion while in kiosk", and the reference computes the flag as
kiosk_profile || !use_gpu_renderer.
QbzShell.reduceMotion is derived, never a preference - do not add a Settings
row for it. The existing play-indicator toggle is a preference and a
different thing; binding them would make "animate the play indicator" change
this dropdown's pulse rate.
Only the kiosk half is wired, and the omission is deliberate rather than
partial: the renderer tier has no honest source here. It is a QML-side fact,
this port runs on the GPU in every real session, and the one place a probe
would report software is the offscreen smoke, which forces it by definition.
Wiring it would turn reduce-motion on exactly where it means nothing. The
kiosk literal is a named const with a pointer to their contract; flipping it
is one line on their side.
The cortinilla skeleton is the ONE consumer: its single host Timer drops
from 700ms to 2000ms. No new timer, no per-delegate animation - the
reference's own note records that this pulse was the main cost of opening
the cortinilla on weak GPUs.
Giving that Column an id also fixes a real scope bug on the way past. The
Timer read an unqualified `parent`, and Timer derives from QtObject, which
has no parent property - so it resolved up the scope chain, out of the block
entirely. The sibling rectangles' parent.parent[.parent] chains are now
explicit too.
EqualizerBars.qml and the immersive contract's D11 both recorded that Qt had
no reduce-motion twin. Both are corrected: the seam exists, those surfaces
are simply not consumers yet, and wiring each is its own one-liner.
Contract: qbz-nix-docs/qt-frontend/2026-08-03-cortinilla-parity, commit C4b
(section 5.g G1-G5, ruling R10; D-BUG-1 folded in).
Gate: build clean, 258/258 tests, both QML audits OK, offscreen smoke
0 complaints + home published. AC-39: exactly one behavioural consumer.
cc
QbzBridge carried five domains (Settings, Search, Playlist, Genre filter,
Discover config); its own boot() comment called the split "TEMP (phase 23)".
TRACK-RULES section 2 requires one cxx-qt bridge per domain. This moves the
Search domain out verbatim, ahead of the cortinilla parity work, so the
refactor and the behaviour change stay separately reviewable.
QbzSearch (search_bridge.rs) takes 7 properties and 11 invokables. Two
deliberate changes ride the move:
- selected_index -> cortinilla_selected_index. It is cortinilla-only
state and the bare name on an app-wide singleton is a collision hazard.
- cortinilla_scroll_y f32 -> f64, unifying with the immersive twin in
immersive_bridge.rs, which was already f64.
cortinilla_query is declared here but not yet wired; C2 wires it, which is
what makes Enter carry the text the user actually typed instead of the last
successfully loaded payload's query.
Every Rust writer of a moving property was re-pointed BY HAND, never by
regex over the multi-line ui() closures: the seven crate::ui( sites in
search_qt.rs plus two that live outside it and that the QML-only xref audit
cannot see - settings_qt.rs (the live Settings > Appearance path) and
main.rs toggle_intelligent_search. All 48 QML consumer sites across
Cortinilla.qml, HeaderBar.qml and SearchView.qml were updated, including the
one Connections block, whose onSelectedIndexChanged handler was renamed with
its property.
A stale 6.0 base in flat_index_content_y is left in place and annotated: it
mirrors a Flickable viewport margin rather than content space, so every row
reports 6px low. C4 corrects it with its unit test; this commit is a move.
Contract: qbz-nix-docs/qt-frontend/2026-08-03-cortinilla-parity, commit C0.
Gate: build clean (no new warnings), 256/256 tests, qml_resolution_audit
214 files, qml_singleton_xref 28 bridges with QbzSearch resolving, offscreen
smoke 0 complaints + home published.
cc
Root overlay self-gated on QbzImmersive.open with the load-bearing
click-blocker, 6s auto-hide with the VNC hover-storm delta guard,
fullscreen guard, exit paths 2-4, seek arrows, the header band (view
menu in Slint order, search field on the inert B1 surface, window
controls with the first FullScreen write in the Qt frontend), the
ViewModeMenu Immersive row live, Shift+I in Main.qml with the
text-input gate, QbzLineEdit fieldActive seam (D16). Panels are stubs;
B3/B4/B5 replace them. RFB evidence in scratchpad/immersive-b2/.
km