The Studio furniture pane was present in the DOM the whole time and still
looked empty. Every row existed, had real dimensions, and its image loaded
with a 200 — and none of it was visible.
The pane was wrapped in a `motion/react-m` element declared with
`initial={{ opacity: 0 }}` and `animate={{ opacity: 1 }}`. That minimal entry
renders the element but never runs the animation, so the inline style stayed at
opacity 0 and the content was painted transparently forever. Measured on the
live release: the table sat at opacity 1 directly inside a wrapper pinned at
`style="opacity: 0"`.
It went unnoticed because playwright.ui.config.ts sets reducedMotion to
"reduce", under which the animation is skipped and the element lands straight
on its final value. The existing Studio specs therefore passed while the real
browser showed nothing. That also means the perf win from the minimal entry
was never actually delivering a working fade — it only hid the breakage.
The decorative 150ms fade is now a CSS animation (.studio-list-fade-in). A CSS
animation cannot strand content this way: if it never runs, the element is
simply opaque. motion/react-m had exactly one usage in the app and is gone;
the four files that use the full motion/react are untouched and unaffected.
Adds e2e/ui/studio-visibility.spec.ts, which opts out of reduced motion and
asserts no ancestor of a furniture row is faded below 0.9. Verified it fails
on the old code with `Received: 0` and passes on the new, so this cannot
regress silently again.
The UI screenshot suite caught a regression from the previous commit: the admin
article form grew 3px and its baseline no longer matched.
The cause was the blanket `.btn` min-height bump from 40px to 44px. That was
the wrong way round — 40px already clears WCAG 2.2 AA, which asks for 24px, and
44px is a touch-target guideline. Growing every button for mouse users only made
each admin dialog and table 4px taller for no benefit.
The 44px floor now sits behind `@media (pointer: coarse)`, so finger input gets
the comfortable target and desktop keeps its density. A hybrid laptop still
uses the desktop metrics for its trackpad, which is the behaviour the previous
attempt got wrong in both directions.
Closes the four remaining LOW items.
Search and news archive caching
- A leading-wildcard LIKE cannot use an index, so every /search section cost a
COUNT(*) scan plus an ordered page fetch, and /news did the same for its
archive. Both now cache: search per section for 30s, the archive for 60s
under the existing news revision so publishing an article drops it at once.
- Sections are cached independently, so one slow query cannot hold up the rest
and a failure is not cached as a result.
- The cached value is passed through cacheSafe() so the Redis path and the
in-process path return the same types; without it a cache hit would hand the
events grid a string where a miss hands it a Date, and it calls toISOString()
on that field. Dates are revived on the way out so the public signatures of
loadNewsArchive and loadPublicSearch are unchanged.
- Archive entries are keyed on the REQUESTED page rather than the clamped one,
so two requests that clamp onto the same page cannot alias each other.
motion/react out of the public bundle
- Converted the six public-facing users: the radio player, the typewriter text
(a motion.span with no animation props at all), the photo lightbox, the
animated counter, the footer CMS-info popup and the scroll reveal. That was
the actual entry points — the counter and the popup reach the public home page
and footer through static imports, so removing only the three originally named
would have left the library in the bundle anyway.
- Each animation moved to a CSS class, and the two that animate on exit now hold
the element for the length of the fade, which is what AnimatePresence used to
do.
- motion/react now only ships with /admin and the two already-lazy nav panels.
- Two safety fixes came out of this: the scroll reveal starts at opacity 0, so
it is forced visible under prefers-reduced-motion and via a <noscript> rule in
the root layout; and it now emits the .motion-reveal class, which the theme
panel's "Scroll Reveal" toggle selects and which previously matched nothing.
- The CMS-info backdrop became a real button in a pointer-transparent layer
instead of a handler on a static element, so click-outside-to-dismiss is
reachable by keyboard.
Fewer duplicate router refreshes
- Next.js re-renders the current route as part of a server action's own response
when that action revalidates, and applies it with a seeded navigation; the
router only skips its own update when the action did NOT revalidate. So the
refresh after such an action fetched the same tree twice.
- useServerAction takes an opt-in `revalidated` flag that skips it. It is opt-in
per call rather than derived from an action name, since a rename would
silently change behaviour. Applied to the two user-facing call sites whose
actions were verified to revalidate their own route.
Touch targets
- .btn was the one shared control at 40px; it and the lightbox and CMS-info
close buttons are now 44px, as is the password toggle (the auth input already
reserved 44px for it). The remaining 32px icon buttons pass WCAG 2.2 AA, which
only asks for 24px; enlarging those inside inputs and overlays was left alone
because it risks visual breakage that cannot be checked from here.
Second review pass covering security, performance, admin tooling and the
public/room flows. All HIGH and MEDIUM findings from the audit are resolved;
nothing in this commit changes the visible feature set.
Authentication & session security
- CSP is now set on the request headers in the proxy, which is what Next.js
uses to derive the render nonce, so the nonce is effective.
- 2FA: an already-enabled user cannot re-enroll, the setup endpoint is
rate-limited per account, and confirmed codes are persisted so the second
secret no longer silently never applies.
- Password reset revokes the ticket, authTicket and all personal access
tokens, and bumps the token version so existing sessions die. The same
revocation is now wired into the staff-side password reset.
- /reset and /verify return a stable error code instead of raw text; the
mail lookups are ordered by id so duplicates cannot vary between runs.
- Resending the verification mail gets a per-address cooldown on top of the
per-user limit.
- Issue API tokens with the narrower radio/ticket ability set instead of "*".
Authorization & input handling
- Mid-rank staff can no longer keep dynamically granted non-view admin.*
permissions: existing grants are revoked by migration and the grant lookup
is restricted to "%.view". Rank guards use the dynamic super-admin check.
- Alerting a user is permission-checked and audited like the other tools.
- Material mutations (giveCredits/giveDuckets/giveDiamonds, the admin user
actions route, bulk user actions) are capped and rank-guarded, and bulk
ids are bounded.
- updateRoom / updateRoomItem write through a field allowlist, and items
may only be edited through their own room.
- Classnames reaching the filesystem are validated before use so a crafted
value cannot escape the asset directories.
- The word filter now also covers offline mails, guild forum threads and
replies, and user mottos.
- Media uploads are validated by magic bytes, /api/media requires the page
edit permission, APP_URL must be configured once mail is enabled, and the
diagnostics error route checks the fetch site header.
Admin tooling
- Secret settings render masked and cannot be overwritten with a blank or
an arbitrary raw key; radio credentials are new password inputs.
- Commandocentrum balance changes are audited.
- Admin list pagination reads the caller's per-page instead of the max, and
the log exporter caps offset and search length.
Performance
- Catalog translations are cached per module, with a cheap revision hash;
the public online count uses a stale window instead of hammering the DB.
- The cache warmup now primes the payload the home route actually reads.
- TopHeader batches its queries into one round trip, and LCP avatars load
eagerly.
- motion/react and sonner are no longer part of the root layout; the nav
dropdown and mobile nav panels are lazy client chunks. Anonymous visitors
again get the navigation chrome, and public pages get an edge cacheable
response.
Accessibility
- Nested <main> elements in phase pages became <section>; the page entrance
and route progress animations are pure CSS that respect reduced motion.
- add theme-aware helpers: btn-brand, btn-glass-dark, auth-input, auth-label,
auth-alert, explore-pill, avatar-tile, aurora blobs and a hero scroll cue
- reorder backdrop-filter declarations so the glass blur survives the
production CSS optimizer in modern Chromium
- hero: aurora glow, frosted recent-users chip, premium CTA buttons and cue
- explore nav: icon pills with hover arrows; features: gradient icon tiles
- stats: single glass panel with column dividers; join CTA: aurora + ring
- auth forms: visible labels, icon inputs, eye/password toggle, gradient
submit and pill footer links
- auth pages: gradient card frame, aurora accents on the intro panel and
hover-lift avatar tiles; unified pill-shaped top bar
- drop the hard-coded register banner image in favor of the framed card
The palette lives in plain CSS (:root/ThemeVars/admin remap), so Tailwind
never generated bg-primary, bg-destructive, text-foreground and similar
utilities. Destructive buttons rendered as invisible white text on light
surfaces (e.g. the Nitro cleanup delete button). Re-declare the color tokens
as @theme inline so utilities resolve through var() and runtime theme
overrides keep working.
Keep every animation transform/opacity-only so frames never repaint:
- hero ring and loading glow pulse via opacity instead of background-position / box-shadow
- button shine sweeps with transform, not left
- floating glass chips drop animated backdrop-filter (it re-samples every frame)
- promote continuously animated layers (particles, halo, float) with will-change
- drop the negligible blur on moving clouds and remove the unused gradient-shift
Add background_effect (aurora/particles), background_overlay tint and
opacity to the theme manager, rendered site-wide by ThemeVars on every
public page. Polish the home and register pages (hero mascot, live stat
pulse, date pills, photo strip, CTA band, theme-aware register intro,
i18n for home/register section).
- hero-ring: continuously shifting gradient hairline around the hero
frame (mask-drawn, reduced-motion safe).
- glass-chip: frosted floating pills under the CTAs that bob on a
staggered loop, live online counter keeps ticking for the Online chip.
- Taller hero for more presence on desktop.
The first cloud pass was to subtle: only five, up to 190s per crossing,
and they froze off-screen under prefers-reduced-motion. Rework:
- Eight clouds across the top 60% of the viewport with visible drift
(28s–66s loops, staggered by negative delays so they are always mid
scene on load).
- Higher opacity/steeper size contrast in light mode; dark mode dims
them slightly.
- Reduced motion now freezes a static, evenly-spread cloud field across
the width instead of pushing the clouds off-screen.
Add a fixed, decorative layer of soft clouds that slowly float across
the Habbo sky behind the content:
- Five clouds at staggered sizes, heights, opacities and loop timings
(60s–190s) so the drift feels organic.
- Dimmed further in dark mode; frozen by prefers-reduced-motion.
- Pure decoration: aria-hidden, pointer-events: none, no color
utilities in markup (cloud shapes live in globals.css).
- Slow Ken Burns drift on the hero artwork for cinematic depth.
- Gentle breathing pulse on the brand glows behind Frank and the hero.
- Silkier reveal easing (cubic-bezier .22/1/.36/1, 0.55s) site-wide.
- Eased, longer hover transitions on the hero CTAs.
- Smooth page scrolling, all guarded by prefers-reduced-motion.
Give the home page a clear, professional information hierarchy:
- Add eyebrow labels + headings for Features, Live stats and Community
sections using reusable, theme-aware .eyebrow / .section-title styles.
- Loosen the vertical rhythm (gap-8/10) so each block breathes.
- New messages resolve via the existing English fallback for all locales.
Refine the public UI for a cleaner, more professional and scannable
landing experience without leaving the classic Habbo style:
- SurfaceCard: softer layered shadow, gradient accent hairline on the top
edge and a bolder header title across all public cards.
- Home: gradient hotel-name in the hero headline, shine effect on the
primary CTA, hairline on the top bar.
- Login/register: consistent avatar tiles with rounded corners, subtle
borders and a gentle hover lift; uniform username sizing.
- Add reusable theme-aware .card-hairline and .gradient-text utilities.
Replace the premium dark-gaming redesign of home, login and register with
the original classic landing (Habbo sky background, AuthTopBar, SurfaceCard
layout). Keeps the theme background visible again and adds a subtle
theme-aware brand halo behind the hero/Frank plus a soft primary glow on
card hover.
Also upgrades dependencies: next 16.3.5, vite 8.3.0 (typescript 7.0.2 was
already latest). Temporarily lowers pnpm minimumReleaseAge to 60 min so the
fresh 16.3.5 release can be installed; restore to 1440 once it is 24h old.
Replaced the classic Habbo landing style on the home, login and register pages with a modern premium dark-gaming look: always-dark hero canvas with brand glows, grid overlay and ambient orbs, glass panels, gradient text and floating art. Adds shared LandingTopBar, AuthShell and BrandFrank components plus reusable premium CSS utilities. Build, typecheck and lint pass.
- Stack the /me dashboard hero (avatar + info) on small screens instead of
crowding them side by side; center the avatar and info on mobile
- Respect prefers-reduced-motion in the Reveal scroll animation
- Make the desktop pitch-in (NavDropdown) and mobile menu more consistent;
give the mobile menu a max-height with overscroll containment so long
navs scroll instead of overflowing on small phones
- Add safe-area insets for notched devices on header, nav and footer
- Polish whole-link content-cards (community tiles) with hover lift and a
visible focus ring
- Enforce a comfortable min touch height on all .btn buttons
- Enhance SurfaceCard with refined borders, layered shadows, and polished
primary-tinted header with an icon container
- Update homepage blocks (hero, features, stats, login, news, photos) for
proper mobile/tablet/desktop responsiveness and tighter mobile spacing
- Improve content-card, stat-block, card-grid and card styling with subtle
depth (layered shadow + inner hairline) and smoother hover states
- Refine site header, footer, top header and site layout spacing for small
screens while keeping a consistent professional look on large screens
- Add min-tap-height targets and responsive typography on mobile
Fetch the Nitro client iframe with high priority so the browser starts the
game document before competing resources, and replace the bare /client
spinner with a branded boot screen. Preconnect and eager loading were already
in place; typing support for the iframe fetchPriority prop is added in a
React type augmentation.
Rework the register terms block into a single clickable accept control with
a custom check state, error shake and inline hint, and dim the submit button
until the terms are accepted. The register page gets labeled sections
(account details / credentials), a corrected banner overlay, translated
show/hide toggles and a captcha slot that reserves height to avoid layout
shifts. English is the source of truth; other locales fall back to it.
- Card-shaped loading skeletons with shimmer text lines
- Hover lift and accent shadow on furni cards
- Subtle dotted floor pattern behind previews and detail image
- Sticky action footer in the detail drawer
- Theme-aware slim scrollbars inside the admin panel
- Nicer empty state with icon tile
- Self-host Nunito and Pixelify Sans via next/font instead of Google Fonts CDN
(removes render-blocking external stylesheets and preconnects)
- Remove stale mariadb entry from serverExternalPackages (app uses mysql2)
- Exclude unused src/generated Prisma client from typecheck and remove it
- Add unit tests for swf-parser, effectmap and figuremap (0% coverage -> 90%+)
- Replace next/font/google with <link> tags in <head> (loads fonts client-side at runtime)
- Define --font-nunito and --font-pixel CSS variables in globals.css with font-family fallbacks
- Remove @prisma/client from serverExternalPackages in next.config.ts (devDep only)
Shared ErrorScreen composition, i18n for en/nl/it, Sentry on route errors, and a self-contained global-error fallback that matches the hotel gold theme.
Co-authored-by: Cursor <[email protected]>
- Add framer-motion and tailwindcss-animate for transitions
- Add page transitions via AnimatePresence in the site layout
- Convert nav dropdown and mobile menu to animated motion components
- Add entry animation to the radio player widget
- Add reveal/stagger animations to the home page views
- Add shared motion utilities and reusable animated components
Light admin derives from the public light theme; dark keeps HK overrides. Remap inputs, cards, muted text, and kill the public wallpaper on admin so fonts and boxes stay readable in both modes.
Co-authored-by: Cursor <[email protected]>
Replace the long flat nav with hub entries, shared AdminHubChrome tabs, and AdminPageShell. Existing URLs stay stable; Settings/Radio and other sections now share one chrome.
Co-authored-by: Cursor <[email protected]>
Public cards no longer paint with always-dark admin surfaces, and admin pages fully remap background/text/surface so shadcn UI stays readable on the admin canvas.
Co-authored-by: Cursor <[email protected]>
Derive admin/public text colors from WCAG contrast, unify ThemeVars CSS emission, and keep navbar overrides in sync with readable vars.
Co-authored-by: Cursor <[email protected]>
- Mobile nav: uses <details>/<summary> for toggle, no useState, no useEffect
- Nav dropdown: uses <details>/<summary>, no useState, no event listeners
- Desktop: separate div with desktop nav items (hidden on mobile)
- Mobile: hamburger dropdown with absolute positioned panel
- CSS: removed ::after pseudo-element with transform, replaced hover
with background-color only, added details[open] CSS rules
- No backdrop-filter, no will-change, no transition-all, no GPU hacks
- Works on every device without JS state management