Skip to content

[URGENT, please approve quickly] START Labs Landing Page#106

Merged
Yasin479 merged 8 commits intomainfrom
start-labs-site
Apr 19, 2026
Merged

[URGENT, please approve quickly] START Labs Landing Page#106
Yasin479 merged 8 commits intomainfrom
start-labs-site

Conversation

@Yasin479
Copy link
Copy Markdown
Contributor

@Yasin479 Yasin479 commented Apr 19, 2026

Summary by CodeRabbit

  • New Features

    • Launched Labs page with interactive timeline, expandable FAQ section, and application workflow
    • Introduced new animated visual effects to enhance the interface
  • Chores

    • Updated component library configuration and registry support
    • Added animation library dependency

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 19, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
start-munich Ready Ready Preview, Comment Apr 19, 2026 11:28pm

@Yasin479 Yasin479 requested a review from SimonBurmer April 19, 2026 23:29
@Yasin479 Yasin479 self-assigned this Apr 19, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 19, 2026

📝 Walkthrough

Walkthrough

A new "LABS" landing page feature is introduced with complete styling infrastructure, layout components, and reusable animated UI components. The addition includes CSS design tokens and utilities for the labs page, page routing/metadata setup, a main content component with interactive timeline and FAQ sections, and three new animation-driven UI components supporting visual effects.

Changes

Cohort / File(s) Summary
Labs Page Infrastructure
app/globals.css, app/labs/page.tsx, app/labs/layout.tsx, app/labs/LabsContent.tsx
Added /labs route with new CSS design tokens, page metadata (title, description, canonical/OG URLs), layout wrapper, and main content component. Content includes interactive timeline (week-by-week journey via useState), FAQ accordion, navigation anchors (about, program, manifesto, proof, faq, apply), and IntersectionObserver scroll-reveal setup. Page injects postMessage script for iframe height synchronization on load/resize.
Animated UI Components
components/ui/dotted-glow-background.tsx, components/ui/encrypted-text.tsx, components/ui/noise-texture.tsx
Three new client-side components: DottedGlowBackground (canvas-based animated dots with configurable colors/speeds, theme-aware via CSS variables, ResizeObserver and IntersectionObserver for performance); EncryptedText (character-by-character reveal animation using useInView, scramble states, and configurable charset); NoiseTexture (SVG-based procedural noise filter with feTurbulence and dynamic filterId).
UI Component Demos
components/dotted-glow-background-demo.tsx, components/encrypted-text-demo-2.tsx
Simple demo components showcasing DottedGlowBackground with logo/text overlay and EncryptedText with custom reveal delay/styling.
Configuration & Dependencies
components.json, package.json
Added iconLibrary: "lucide" and registries mapping (@aceternity, @magicui) to components.json. Added motion v^12.38.0 runtime dependency.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • SimonBurmer

Poem

🐰 Hop along, the labs are here to stay,
With animated dots that dance and sway,
Encrypted text reveals its hidden art,
Canvas canvas glows straight from the heart,
A landing page where innovation starts! ✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title 'START Labs Landing Page' clearly describes the main change (adding a new Labs landing page), but the '[URGENT, please approve quickly]' prefix is noise that detracts from the meaningful information. Remove the '[URGENT, please approve quickly]' prefix and use only 'START Labs Landing Page' or similar concise, descriptive title without urgency markers.
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch start-labs-site

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🧹 Nitpick comments (11)
app/labs/LabsContent.tsx (7)

120-154: Extract the "Apply Now" CTA into a reusable component.

The same bordered CTA markup (border slide-in on hover, identical styles + attrs) is duplicated at least 4 times in this file (L120-135 desktop nav, L139-154 mobile nav, L226-243 hero, L1890-1907 how-to-apply, L1973-1990 final apply). Extract it to a single <LabsCTAButton href size> component to kill ~100 lines and prevent drift.

Also applies to: 226-243, 1890-1907, 1973-1990

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/labs/LabsContent.tsx` around lines 120 - 154, Create a reusable
LabsCTAButton component and replace each duplicated CTA anchor with it:
implement a new component (e.g., LabsCTAButton) that accepts props href and size
(or variant) and renders the same anchor structure and styles (the bordered
slide-in hover effect, the inner <span> with group-hover:text-black, and the
absolute sliding <div> using --labs-accent) so all occurrences (the desktop nav
CTA, mobile CTA, hero CTA, how-to-apply CTA, and final apply CTA) call
<LabsCTAButton href="..." size="..." /> instead of duplicating the markup;
ensure it preserves className, style attributes, and accessibility (anchor href)
and import/use the component inside LabsContent.tsx where the original anchor
blocks at lines around the shown diffs were removed.

171-183: Autoplay background video: bandwidth & no fallback pause.

<video autoPlay muted loop> at hero starts downloading/decoding START_LABS_trimmed_video.mp4 on every visit, including mobile / reduced-data users. Consider:

  • Adding preload='metadata' (or conditional load) to reduce initial bandwidth.
  • Respecting prefers-reduced-motion (pause or hide video for users with that preference).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/labs/LabsContent.tsx` around lines 171 - 183, The hero <video> element
(src '/labs/videos/START_LABS_trimmed_video.mp4' with autoPlay muted loop
playsInline) is forcing full download/decoding; change it to preload='metadata'
and guard autoplay/attachment by checking user preferences before rendering the
<source>: detect prefers-reduced-motion via
window.matchMedia('(prefers-reduced-motion: reduce)') and
navigator.connection?.saveData (or equivalent) and if either is true, do not
render the <source> (or render a static poster only) and ensure the element does
not autoPlay; keep the poster attribute for fallback and make these checks
inside the LabsContent component so the video is conditionally loaded only when
allowed.

80-119: Inline onMouseEnter/onMouseLeave for hover color — use Tailwind.

Three nav links each attach imperative mouse handlers to toggle #888888#ffffff. This re-creates functions on every render and bypasses Tailwind. A hover:text-white text-[#888888] (or a utility class with a CSS transition) does the same thing declaratively.

♻️ Example
-                     <a
-                        href='#about'
-                        className='font-mono uppercase tracking-[0.15em] transition-all duration-200'
-                        style={{ color: '#888888', fontSize: '10px' }}
-                        onMouseEnter={(e) =>
-                           (e.currentTarget.style.color = '#ffffff')
-                        }
-                        onMouseLeave={(e) =>
-                           (e.currentTarget.style.color = '#888888')
-                        }
-                     >
-                        About
-                     </a>
+                     <a
+                        href='#about'
+                        className='font-mono uppercase tracking-[0.15em] text-[10px] text-[`#888`] hover:text-white transition-colors duration-200'
+                     >
+                        About
+                     </a>

As per coding guidelines: "Use Tailwind CSS for styling".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/labs/LabsContent.tsx` around lines 80 - 119, The three anchor elements in
LabsContent (the nav links "About", "How it works", "FAQ") use inline style
props and onMouseEnter/onMouseLeave handlers to toggle colors; remove the inline
style and event handlers and replace them with Tailwind utility classes on the
<a> elements (e.g. add text-[`#888888`] hover:text-white transition-colors
duration-200 and text-[10px] or an appropriate text size utility) while keeping
the existing font-mono uppercase tracking classes so the hover color and
transition are handled declaratively by Tailwind.

254-255: Hard-coded deadline duplicated — single-source it.

"30 April 2026" is hard-coded in the hero (L254-255) and the how-to-apply copy (L1885). Since the How-to-Apply section is currently commented out in the render tree, the user-visible copy is only in the hero today, but any future change will be missed in the other spot. Hoist to a module-level const (const APPLICATION_DEADLINE = '30 April 2026') so both strings stay in sync.

Also applies to: 1883-1886

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/labs/LabsContent.tsx` around lines 254 - 255, The hero and how-to-apply
copy duplicate the hard-coded deadline string; create a module-level constant
like const APPLICATION_DEADLINE = '30 April 2026' at the top of LabsContent.tsx
and replace the literal "30 April 2026" occurrences in the hero JSX and the
commented how-to-apply JSX with APPLICATION_DEADLINE (use string
interpolation/concatenation where needed, e.g. `Deadline:
${APPLICATION_DEADLINE} · ...`) so both places share the same single source of
truth.

588-589: Dead anchor <div>.

The empty <div className='absolute -top-20'></div> below the id='manifesto' section appears intended as a scroll anchor, but the parent <section> already has scroll-mt-20 and id='manifesto', making it redundant and semantically noisy.

♻️ Proposed removal
-         {/* Anchor for navigation */}
-         <div className='absolute -top-20'></div>
-
          {/* Noise texture */}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/labs/LabsContent.tsx` around lines 588 - 589, Remove the redundant empty
anchor div and rely on the existing section anchor: delete the <div
className='absolute -top-20'></div> that sits below the section with
id='manifesto' (the section already uses scroll-mt-20 and id='manifesto'), so
remove the dead DOM node in LabsContent.tsx to reduce semantic noise and keep
navigation behavior unchanged.

294-298: Prefer next/image over raw <img> for perf & LCP.

The page ships many <img> tags — including hero-adjacent decorative images and a remote logo hosted on raw.githubusercontent.com (L296, L309) which is not CDN-optimized. Since the libraries context confirms Next.js 15.3.9, using next/image with explicit width/height (or fill) gives you automatic lazy loading, responsive srcset, and prevents CLS. Remote images will need a remotePatterns entry in next.config.* for raw.githubusercontent.com (or, better, rehost those logos under /public/labs/images/).

Also applies to: 307-311, 750-754, 1203-1207, 1327-1344, 1747-1751, 1953-1956, 2165-2169

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/labs/LabsContent.tsx` around lines 294 - 298, Replace raw <img> usages in
LabsContent.tsx (e.g., the START Munich logo image and other occurrences around
lines listed) with Next.js's Image component: add "import Image from
'next/image'" to the top of the module, replace each <img ...> with <Image
src={...} alt="..." width={W} height={H} /> or use layout="fill"/fill with a
positioned container to avoid CLS, and ensure responsive props as appropriate;
for remote raw.githubusercontent.com sources either move those assets into
/public/labs/images/ and reference them with '/labs/images/...' or add a
remotePatterns entry for raw.githubusercontent.com in next.config.* so Next
Image can optimize them. Ensure each replaced instance preserves original
classNames/styles (apply to elements around the referenced ranges: 294-298,
307-311, 750-754, 1203-1207, 1327-1344, 1747-1751, 1953-1956, 2165-2169).

7-7: Remove redundant route segment config export from client component.

export const dynamic = 'force-dynamic' has no effect in client components ('use client'). Route segment configs only work when exported from server components (page.tsx, layout.tsx, route.ts). Since app/labs/page.tsx already exports this config, the export here is redundant noise.

♻️ Proposed removal
 import { NoiseTexture } from '@/components/ui/noise-texture';

-export const dynamic = 'force-dynamic';
-
 // Label Component - Consistent "//" labels throughout the page
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/labs/LabsContent.tsx` at line 7, Remove the redundant route segment
config export from the client component: delete the export const dynamic =
'force-dynamic' statement in LabsContent.tsx (the symbol "dynamic") since
LabsContent.tsx is a client component ('use client') and the same config is
already exported from app/labs/page.tsx; ensure no other code depends on that
export before committing the removal.
components/ui/dotted-glow-background.tsx (3)

201-203: Misleading name: regenThrottled isn't throttled.

regenThrottled is just an un-throttled call to regenDots. Either add actual throttling (e.g., requestAnimationFrame or a trailing setTimeout) to tame the storm of resizes during window drag, or rename it to regen to avoid implying behavior that doesn't exist.

♻️ Minimal rename
-    const regenThrottled = () => {
-      regenDots();
-    };
-
-    regenDots();
+    regenDots();

…and call regenDots() directly from handleResize.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/ui/dotted-glow-background.tsx` around lines 201 - 203, The helper
named regenThrottled is misleading because it simply calls regenDots without
throttling; either implement actual throttling inside regenThrottled (e.g.,
debounce via requestAnimationFrame or a trailing setTimeout) and use that in
handleResize, or rename regenThrottled to regen and call regenDots directly from
handleResize; refer to the regenThrottled function, regenDots function, and
handleResize to locate and update the logic accordingly.

151-306: Effect tears down and re-seeds dots whenever theme colors change.

resolvedColor and resolvedGlowColor are in the dependency array (L299-300) of the main RAF effect, so any theme toggle cancels the RAF loop, regenerates every dot (new random phases/speeds), and reattaches observers. Users will see a visible "restart" shimmer on every light/dark toggle. Since the colors are only read inside draw, you can hold them in refs updated by the color-resolution effect and omit them from this effect's deps — keeping the animation state continuous across theme changes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/ui/dotted-glow-background.tsx` around lines 151 - 306, The RAF
effect recreates the animation (tearing/regenerating dots) whenever theme colors
change because resolvedColor and resolvedGlowColor are in the useEffect deps;
move those values into mutable refs (e.g., colorRef/glowColorRef) and update
those refs from the color-resolution effect, then remove resolvedColor and
resolvedGlowColor from the dependency array of the main useEffect that defines
draw (which should read from colorRef.current and glowColorRef.current), so the
RAF loop, regenDots, observers, and dot phases/speeds (dots, draw, regenDots,
resize, containerRef, canvasRef) persist across theme toggles without
restarting.

273-276: window.resize + ResizeObserver both fire — one is enough.

ro (L174-175) already observes container and calls resize() on every size change (including parent layout changes). You then additionally bind window.addEventListener('resize', handleResize) (L286), which calls resize() and regenThrottled(). The result: on every window resize both observers fire, causing two resize() calls and one full dot regeneration. Consolidate into the ResizeObserver callback (and call regenDots from there) to remove the redundancy.

Also applies to: 286-286

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/ui/dotted-glow-background.tsx` around lines 273 - 276, The
ResizeObserver instance ro already calls resize() on container size changes, so
remove the redundant window.addEventListener('resize', handleResize) usage and
consolidate regeneration into the ResizeObserver callback: update the ro
callback (where resize() is invoked) to also call regenThrottled()/regenDots so
a single path handles both resize and regen, then delete or simplify
handleResize and the window listener registration/cleanup; ensure the cleanup
still disconnects ro and no longer tries to remove the removed window listener.
components/ui/encrypted-text.tsx (1)

60-66: useRef lazy-initializer uses captured text/charset but never re-inits.

scrambleCharsRef is initialized from the first-render values of text and charset. If the component mounts before entering view (so isInView is false and the effect at L68 hasn't yet reset the ref) and text changes during that window, the render at L136-153 will reach into stale scramble chars. In practice the effect re-syncs as soon as isInView flips true, but consider initializing to [] and doing all seeding inside the effect for clarity:

-  const scrambleCharsRef = useRef<string[]>(
-    text ? generateGibberishPreservingSpaces(text, charset).split("") : [],
-  );
+  const scrambleCharsRef = useRef<string[]>([]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/ui/encrypted-text.tsx` around lines 60 - 66, The scrambleCharsRef
useRef is seeded with captured text/charset on first render which can become
stale; change its initializer to an empty array and move the seeding logic into
the existing effect that runs when the component becomes visible (the effect
that currently resets on isInView), so that when isInView becomes true or when
text/charset change you assign scrambleCharsRef.current =
generateGibberishPreservingSpaces(text, charset).split("") (and reset any
related state like revealCount/lastFlipTimeRef if needed) to ensure the scramble
buffer is always in sync.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/globals.css`:
- Around line 432-442: The current .labs-reveal rule hides content by default
(opacity:0) which breaks when JS/hydration fails; change the CSS so the "hidden"
translate/opacity styles are applied only when a JS-enabled attribute is present
(e.g., [data-reveal-ready="true"]) instead of by default, and keep the no-JS
fallback visible; update selectors so the reveal/active rules target
.labs-reveal when data-reveal-ready="true" (and .labs-reveal.active when that
attribute is present) and then ensure the Labs client sets
data-reveal-ready="true" on the relevant container only after the reveal
observer is installed.

In `@app/labs/LabsContent.tsx`:
- Line 1190: Two sections use the same HTML id "proof" (ProofCalloutSection and
ProofSection), which will produce duplicate anchors when both are rendered; pick
one canonical section to keep and rename or remove the other's id (or delete the
dead component). Locate ProofCalloutSection and ProofSection in LabsContent,
decide which should remain as the canonical "#proof" anchor, remove or change
the id attribute on the other component (or remove the unused component entirely
if dead code), and update any internal links or references in LabsContent that
point to '#proof' to use the chosen canonical section name.
- Around line 1173-1178: The iframe in LabsContent (the Google Drive video
embed) lacks a title which breaks accessibility; update the iframe element in
LabsContent.tsx to include a descriptive title attribute (e.g., title="Demo
video: <short description>") so screen readers have a name, and add
loading="lazy" to defer offscreen loading; make this change on the iframe JSX
inside the LabsContent component where the Google Drive src is used.
- Around line 2321-2336: The inline Script with id 'iframe-height-sender' calls
window.parent.postMessage(..., '*') and never removes listeners; replace this by
moving the sendHeight logic into the LabsContent React component (e.g., inside a
useEffect) so you can register and clean up listeners (remove resize and
DOMContentLoaded handlers on unmount), and change the postMessage target from
'*' to a restricted origin or configurable allow-list (validate
window.parent.origin or accept a prop/ENV value) before posting; reference the
sendHeight function, the window.parent.postMessage call, and the
resize/DOMContentLoaded listeners when making the change.
- Around line 2190-2267: Add proper ARIA attributes to the FAQ accordion buttons
and their panels so assistive tech can announce state and relationships: on the
mapped button inside the faqs.map (components using openIndex, setOpenIndex, and
faq) add aria-expanded={openIndex === i} and aria-controls with a unique id like
`faq-panel-${i}`; then add a matching id="faq-panel-${i}" and role="region" (and
optionally aria-labelledby pointing back to the button id) on the collapsible
div that contains the answer. Also apply the same pattern to the timeline
<button> implementation referenced (the code that uses openIndex/toggle at the
timeline section) to add aria-expanded, aria-controls and a corresponding panel
id/role.

In `@app/labs/page.tsx`:
- Line 4: Remove the explicit runtime override by deleting the export "export
const dynamic = 'force-dynamic'" from the Labs page component so the route can
use Next.js's static optimization; ensure no request-time APIs (cookies(),
headers(), draftMode(), fetch(), searchParams, unstable_noStore(), connection(),
etc.) are introduced so the page remains eligible for static caching.

In `@components/dotted-glow-background-demo.tsx`:
- Around line 6-21: Replace non-standard Tailwind utilities with Tailwind v3
arbitrary-value equivalents: change the container's "md:size-100" to explicit
width/height arbitraries (e.g., replace md:size-100 with md:w-[100px]
md:h-[100px]) and swap the mask utilities "mask-radial-to-90%
mask-radial-at-center" (used on the DottedGlowBackground) for a valid arbitrary
mask like
mask-[radial-gradient(closest-side_at_center,black_0,transparent_90%)]; update
any matching uses (the outer div className and the DottedGlowBackground
className) so Tailwind generates the CSS.

In `@components/ui/dotted-glow-background.tsx`:
- Around line 188-199: The grid seeding loop starts i and j at -1 which produces
negative j values so j % 2 is negative and breaks the staggered offset; fix
either by starting both loops at 0 (change "for (let i = -1..."/"for (let j =
-1..." to start at 0) or keep the -1 bounds but normalize the parity test (use a
normalized modulo like ((j % 2) + 2) % 2) where the offset is computed, and
ensure the dots push logic (dots array population with x, y, phase, speed) uses
the corrected parity so the top row aligns consistently with neighbors.

In `@components/ui/encrypted-text.tsx`:
- Around line 129-155: Remove the non-standard role by deleting role="text" from
the motion.span (the element with ref, className and aria-label={text}) and add
aria-hidden="true" to each per-character <span> (the inner span rendered inside
the text.split().map) so the outer motion.span's aria-label is announced once
instead of each character being read individually.

In `@components/ui/noise-texture.tsx`:
- Around line 43-50: The SVG noise layer is decorative and should be hidden from
assistive tech; update the SVG element in components/ui/noise-texture.tsx (the
<svg> inside the NoiseTexture component) to include aria-hidden="true" and
focusable="false" so screen readers and keyboard focus ignore it (no visible
labels or role changes needed).

---

Nitpick comments:
In `@app/labs/LabsContent.tsx`:
- Around line 120-154: Create a reusable LabsCTAButton component and replace
each duplicated CTA anchor with it: implement a new component (e.g.,
LabsCTAButton) that accepts props href and size (or variant) and renders the
same anchor structure and styles (the bordered slide-in hover effect, the inner
<span> with group-hover:text-black, and the absolute sliding <div> using
--labs-accent) so all occurrences (the desktop nav CTA, mobile CTA, hero CTA,
how-to-apply CTA, and final apply CTA) call <LabsCTAButton href="..." size="..."
/> instead of duplicating the markup; ensure it preserves className, style
attributes, and accessibility (anchor href) and import/use the component inside
LabsContent.tsx where the original anchor blocks at lines around the shown diffs
were removed.
- Around line 171-183: The hero <video> element (src
'/labs/videos/START_LABS_trimmed_video.mp4' with autoPlay muted loop
playsInline) is forcing full download/decoding; change it to preload='metadata'
and guard autoplay/attachment by checking user preferences before rendering the
<source>: detect prefers-reduced-motion via
window.matchMedia('(prefers-reduced-motion: reduce)') and
navigator.connection?.saveData (or equivalent) and if either is true, do not
render the <source> (or render a static poster only) and ensure the element does
not autoPlay; keep the poster attribute for fallback and make these checks
inside the LabsContent component so the video is conditionally loaded only when
allowed.
- Around line 80-119: The three anchor elements in LabsContent (the nav links
"About", "How it works", "FAQ") use inline style props and
onMouseEnter/onMouseLeave handlers to toggle colors; remove the inline style and
event handlers and replace them with Tailwind utility classes on the <a>
elements (e.g. add text-[`#888888`] hover:text-white transition-colors
duration-200 and text-[10px] or an appropriate text size utility) while keeping
the existing font-mono uppercase tracking classes so the hover color and
transition are handled declaratively by Tailwind.
- Around line 254-255: The hero and how-to-apply copy duplicate the hard-coded
deadline string; create a module-level constant like const APPLICATION_DEADLINE
= '30 April 2026' at the top of LabsContent.tsx and replace the literal "30
April 2026" occurrences in the hero JSX and the commented how-to-apply JSX with
APPLICATION_DEADLINE (use string interpolation/concatenation where needed, e.g.
`Deadline: ${APPLICATION_DEADLINE} · ...`) so both places share the same single
source of truth.
- Around line 588-589: Remove the redundant empty anchor div and rely on the
existing section anchor: delete the <div className='absolute -top-20'></div>
that sits below the section with id='manifesto' (the section already uses
scroll-mt-20 and id='manifesto'), so remove the dead DOM node in LabsContent.tsx
to reduce semantic noise and keep navigation behavior unchanged.
- Around line 294-298: Replace raw <img> usages in LabsContent.tsx (e.g., the
START Munich logo image and other occurrences around lines listed) with
Next.js's Image component: add "import Image from 'next/image'" to the top of
the module, replace each <img ...> with <Image src={...} alt="..." width={W}
height={H} /> or use layout="fill"/fill with a positioned container to avoid
CLS, and ensure responsive props as appropriate; for remote
raw.githubusercontent.com sources either move those assets into
/public/labs/images/ and reference them with '/labs/images/...' or add a
remotePatterns entry for raw.githubusercontent.com in next.config.* so Next
Image can optimize them. Ensure each replaced instance preserves original
classNames/styles (apply to elements around the referenced ranges: 294-298,
307-311, 750-754, 1203-1207, 1327-1344, 1747-1751, 1953-1956, 2165-2169).
- Line 7: Remove the redundant route segment config export from the client
component: delete the export const dynamic = 'force-dynamic' statement in
LabsContent.tsx (the symbol "dynamic") since LabsContent.tsx is a client
component ('use client') and the same config is already exported from
app/labs/page.tsx; ensure no other code depends on that export before committing
the removal.

In `@components/ui/dotted-glow-background.tsx`:
- Around line 201-203: The helper named regenThrottled is misleading because it
simply calls regenDots without throttling; either implement actual throttling
inside regenThrottled (e.g., debounce via requestAnimationFrame or a trailing
setTimeout) and use that in handleResize, or rename regenThrottled to regen and
call regenDots directly from handleResize; refer to the regenThrottled function,
regenDots function, and handleResize to locate and update the logic accordingly.
- Around line 151-306: The RAF effect recreates the animation
(tearing/regenerating dots) whenever theme colors change because resolvedColor
and resolvedGlowColor are in the useEffect deps; move those values into mutable
refs (e.g., colorRef/glowColorRef) and update those refs from the
color-resolution effect, then remove resolvedColor and resolvedGlowColor from
the dependency array of the main useEffect that defines draw (which should read
from colorRef.current and glowColorRef.current), so the RAF loop, regenDots,
observers, and dot phases/speeds (dots, draw, regenDots, resize, containerRef,
canvasRef) persist across theme toggles without restarting.
- Around line 273-276: The ResizeObserver instance ro already calls resize() on
container size changes, so remove the redundant
window.addEventListener('resize', handleResize) usage and consolidate
regeneration into the ResizeObserver callback: update the ro callback (where
resize() is invoked) to also call regenThrottled()/regenDots so a single path
handles both resize and regen, then delete or simplify handleResize and the
window listener registration/cleanup; ensure the cleanup still disconnects ro
and no longer tries to remove the removed window listener.

In `@components/ui/encrypted-text.tsx`:
- Around line 60-66: The scrambleCharsRef useRef is seeded with captured
text/charset on first render which can become stale; change its initializer to
an empty array and move the seeding logic into the existing effect that runs
when the component becomes visible (the effect that currently resets on
isInView), so that when isInView becomes true or when text/charset change you
assign scrambleCharsRef.current = generateGibberishPreservingSpaces(text,
charset).split("") (and reset any related state like revealCount/lastFlipTimeRef
if needed) to ensure the scramble buffer is always in sync.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 532bc1d7-a430-41ef-aebb-2d9d6a9a1db7

📥 Commits

Reviewing files that changed from the base of the PR and between 09287c1 and afa5279.

⛔ Files ignored due to path filters (15)
  • package-lock.json is excluded by !**/package-lock.json
  • public/labs/images/demoday.jpg is excluded by !**/*.jpg
  • public/labs/images/labsbanner.JPG is excluded by !**/*.jpg
  • public/labs/images/letter.JPG is excluded by !**/*.jpg
  • public/labs/images/oneaim.png is excluded by !**/*.png
  • public/labs/images/partner.jpg is excluded by !**/*.jpg
  • public/labs/images/rocket.png is excluded by !**/*.png
  • public/labs/images/startmunich.png is excluded by !**/*.png
  • public/labs/images/timeline/final.JPG is excluded by !**/*.jpg
  • public/labs/images/timeline/midterm.JPG is excluded by !**/*.jpg
  • public/labs/images/timeline/onboarding.JPG is excluded by !**/*.jpg
  • public/labs/images/timeline/roast-session-1.JPG is excluded by !**/*.jpg
  • public/labs/images/timeline/roast-session-2.jpeg is excluded by !**/*.jpeg
  • public/labs/images/winner.jpg is excluded by !**/*.jpg
  • public/labs/videos/START_LABS_trimmed_video.mp4 is excluded by !**/*.mp4
📒 Files selected for processing (11)
  • app/globals.css
  • app/labs/LabsContent.tsx
  • app/labs/layout.tsx
  • app/labs/page.tsx
  • components.json
  • components/dotted-glow-background-demo.tsx
  • components/encrypted-text-demo-2.tsx
  • components/ui/dotted-glow-background.tsx
  • components/ui/encrypted-text.tsx
  • components/ui/noise-texture.tsx
  • package.json

Comment thread app/globals.css
Comment on lines +432 to +442
/* Reveal animation for scroll - More subtle */
.labs-reveal {
opacity: 0;
transform: translateY(12px);
transition: all 0.5s cubic-bezier(0.16, 1, 0.3, 1);
}

.labs-reveal.active {
opacity: 1;
transform: translateY(0);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid making page content invisible until client JS runs.

.labs-reveal starts at opacity: 0; if hydration or the reveal observer fails, Labs sections can remain hidden. Gate the hidden state behind a JS-ready attribute/class, and keep the no-JS fallback visible.

🛡️ Proposed CSS direction
   .labs-reveal {
-    opacity: 0;
-    transform: translateY(12px);
-    transition: all 0.5s cubic-bezier(0.16, 1, 0.3, 1);
+    opacity: 1;
+    transform: none;
   }
 
-  .labs-reveal.active {
+  .labs-page[data-reveal-ready="true"] .labs-reveal {
+    opacity: 0;
+    transform: translateY(12px);
+    transition: all 0.5s cubic-bezier(0.16, 1, 0.3, 1);
+  }
+
+  .labs-page[data-reveal-ready="true"] .labs-reveal.active {
     opacity: 1;
     transform: translateY(0);
   }
+
+  `@media` (prefers-reduced-motion: reduce) {
+    .labs-page[data-reveal-ready="true"] .labs-reveal {
+      opacity: 1;
+      transform: none;
+      transition: none;
+    }
+  }

Then set data-reveal-ready="true" from the Labs client code only after the reveal observer is installed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/globals.css` around lines 432 - 442, The current .labs-reveal rule hides
content by default (opacity:0) which breaks when JS/hydration fails; change the
CSS so the "hidden" translate/opacity styles are applied only when a JS-enabled
attribute is present (e.g., [data-reveal-ready="true"]) instead of by default,
and keep the no-JS fallback visible; update selectors so the reveal/active rules
target .labs-reveal when data-reveal-ready="true" (and .labs-reveal.active when
that attribute is present) and then ensure the Labs client sets
data-reveal-ready="true" on the relevant container only after the reveal
observer is installed.

Comment thread app/labs/LabsContent.tsx
Comment on lines +1173 to +1178
<iframe
src='https://drive.google.com/file/d/1HQK9_Kry5IDKO2OHlfFuNbhQ2kH8nNGO/preview'
className='w-full h-full border'
style={{ borderColor: 'var(--labs-border)' }}
allow='autoplay'
></iframe>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing iframe title breaks accessibility and fails WCAG 2.4.1.

The Google Drive video iframe has no title, so assistive tech announces it as an unnamed frame. Also consider loading="lazy" since this is below-the-fold.

♻️ Proposed fix
                   <iframe
                      src='https://drive.google.com/file/d/1HQK9_Kry5IDKO2OHlfFuNbhQ2kH8nNGO/preview'
                      className='w-full h-full border'
                      style={{ borderColor: 'var(--labs-border)' }}
                      allow='autoplay'
+                     loading='lazy'
+                     title='START Labs demo day video'
                   ></iframe>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<iframe
src='https://drive.google.com/file/d/1HQK9_Kry5IDKO2OHlfFuNbhQ2kH8nNGO/preview'
className='w-full h-full border'
style={{ borderColor: 'var(--labs-border)' }}
allow='autoplay'
></iframe>
<iframe
src='https://drive.google.com/file/d/1HQK9_Kry5IDKO2OHlfFuNbhQ2kH8nNGO/preview'
className='w-full h-full border'
style={{ borderColor: 'var(--labs-border)' }}
allow='autoplay'
loading='lazy'
title='START Labs demo day video'
></iframe>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/labs/LabsContent.tsx` around lines 1173 - 1178, The iframe in LabsContent
(the Google Drive video embed) lacks a title which breaks accessibility; update
the iframe element in LabsContent.tsx to include a descriptive title attribute
(e.g., title="Demo video: <short description>") so screen readers have a name,
and add loading="lazy" to defer offscreen loading; make this change on the
iframe JSX inside the LabsContent component where the Google Drive src is used.

Comment thread app/labs/LabsContent.tsx
function ProofCalloutSection() {
return (
<section
id='proof'
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Duplicate id='proof' on two different sections.

Both ProofCalloutSection (L1190) and ProofSection (L1668) declare id='proof'. They're currently commented out in LabsContent's render tree (L2350, L2352), so this is latent, but re-enabling either alongside the other produces invalid HTML and non-deterministic anchor jumps for #proof. Pick one canonical section and rename/remove the duplicate — or delete the unused section outright if it is dead code.

Also applies to: 1668-1668

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/labs/LabsContent.tsx` at line 1190, Two sections use the same HTML id
"proof" (ProofCalloutSection and ProofSection), which will produce duplicate
anchors when both are rendered; pick one canonical section to keep and rename or
remove the other's id (or delete the dead component). Locate ProofCalloutSection
and ProofSection in LabsContent, decide which should remain as the canonical
"#proof" anchor, remove or change the id attribute on the other component (or
remove the unused component entirely if dead code), and update any internal
links or references in LabsContent that point to '#proof' to use the chosen
canonical section name.

Comment thread app/labs/LabsContent.tsx
Comment on lines +2190 to +2267
<div className='space-y-2'>
{faqs.map((faq, i) => (
<div
key={i}
className='labs-reveal border-l-2 transition-all duration-200 bg-white/[0.02] border'
style={{
borderLeftColor:
openIndex === i
? 'var(--labs-accent)'
: 'transparent',
borderColor: 'rgba(255,255,255,0.05)',
transitionDelay: `${i * 50}ms`,
}}
>
<button
onClick={() =>
setOpenIndex(openIndex === i ? null : i)
}
className='w-full text-left p-6 flex justify-between items-start gap-4'
>
<div className='flex items-start gap-4 flex-1'>
<span
className='font-mono text-[10px] uppercase tracking-[0.15em] mt-1 transition-colors duration-200'
style={{
color:
openIndex === i
? 'var(--labs-accent)'
: 'var(--labs-text-meta)',
}}
>
{String(i + 1).padStart(2, '0')}
</span>
<span
className='font-mono font-medium transition-colors duration-200'
style={{
fontSize: '13px',
color:
openIndex === i
? 'var(--labs-text-primary)'
: 'var(--labs-text-body)',
}}
>
{faq.q}
</span>
</div>
<div
className='text-xl font-mono transition-all duration-200'
style={{
color:
openIndex === i
? 'var(--labs-accent)'
: 'var(--labs-text-meta)',
transform:
openIndex === i
? 'rotate(45deg)'
: 'rotate(0deg)',
}}
>
+
</div>
</button>
<div
className={`overflow-hidden transition-all duration-200 ${openIndex === i ? 'max-h-96 pb-6' : 'max-h-0'}`}
>
<div className='px-6 pl-[52px]'>
<p
className='font-mono leading-relaxed'
style={{
fontSize: '12px',
color: 'var(--labs-text-body)',
}}
>
{faq.a}
</p>
</div>
</div>
</div>
))}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

FAQ accordion buttons are missing ARIA state.

The expand/collapse buttons don't expose aria-expanded or aria-controls, so screen readers can't tell whether a question is open or which panel is revealed. Same issue applies to the timeline <button> at L646-696.

♻️ Proposed fix (FAQ)
                         <button
                            onClick={() =>
                               setOpenIndex(openIndex === i ? null : i)
                            }
+                           aria-expanded={openIndex === i}
+                           aria-controls={`faq-panel-${i}`}
                            className='w-full text-left p-6 flex justify-between items-start gap-4'
                         >
                            ...
                         </button>
                         <div
+                           id={`faq-panel-${i}`}
+                           role='region'
                            className={`overflow-hidden transition-all duration-200 ${openIndex === i ? 'max-h-96 pb-6' : 'max-h-0'}`}
                         >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/labs/LabsContent.tsx` around lines 2190 - 2267, Add proper ARIA
attributes to the FAQ accordion buttons and their panels so assistive tech can
announce state and relationships: on the mapped button inside the faqs.map
(components using openIndex, setOpenIndex, and faq) add aria-expanded={openIndex
=== i} and aria-controls with a unique id like `faq-panel-${i}`; then add a
matching id="faq-panel-${i}" and role="region" (and optionally aria-labelledby
pointing back to the button id) on the collapsible div that contains the answer.
Also apply the same pattern to the timeline <button> implementation referenced
(the code that uses openIndex/toggle at the timeline section) to add
aria-expanded, aria-controls and a corresponding panel id/role.

Comment thread app/labs/LabsContent.tsx
Comment on lines +2321 to +2336
<Script id='iframe-height-sender' strategy='afterInteractive'>
{`
function sendHeight() {
if (window.parent !== window) {
const height = document.documentElement.scrollHeight;
window.parent.postMessage({ type: 'iframe-height', height: height }, '*');
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', sendHeight);
} else {
sendHeight();
}
window.addEventListener('resize', sendHeight);
`}
</Script>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

postMessage with '*' target origin leaks to any embedder.

The injected script posts iframe-height messages to '*', meaning any page that embeds this route in an iframe receives them. The payload itself is benign (scroll height), but using '*' is a documented anti-pattern and will flag on any security review. Restrict to a known origin (or a configurable allow-list) and consider moving the logic out of next/script's inline payload into a plain useEffect — the current listener is also never removed on unmount.

♻️ Proposed fix sketch
-         <Script id='iframe-height-sender' strategy='afterInteractive'>
-            {`
-          function sendHeight() {
-            if (window.parent !== window) {
-              const height = document.documentElement.scrollHeight;
-              window.parent.postMessage({ type: 'iframe-height', height: height }, '*');
-            }
-          }
-          if (document.readyState === 'loading') {
-            document.addEventListener('DOMContentLoaded', sendHeight);
-          } else {
-            sendHeight();
-          }
-          window.addEventListener('resize', sendHeight);
-        `}
-         </Script>
+         {/* Move into a useEffect and restrict the targetOrigin to the known parent origin
+             (e.g. process.env.NEXT_PUBLIC_EMBED_PARENT_ORIGIN) so the message is only
+             delivered to the expected embedder and the resize listener is removed on unmount. */}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/labs/LabsContent.tsx` around lines 2321 - 2336, The inline Script with id
'iframe-height-sender' calls window.parent.postMessage(..., '*') and never
removes listeners; replace this by moving the sendHeight logic into the
LabsContent React component (e.g., inside a useEffect) so you can register and
clean up listeners (remove resize and DOMContentLoaded handlers on unmount), and
change the postMessage target from '*' to a restricted origin or configurable
allow-list (validate window.parent.origin or accept a prop/ENV value) before
posting; reference the sendHeight function, the window.parent.postMessage call,
and the resize/DOMContentLoaded listeners when making the change.

Comment thread app/labs/page.tsx
import type { Metadata } from 'next'
import LabsContent from './LabsContent'

export const dynamic = 'force-dynamic'
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect the Labs route for APIs that would require dynamic rendering.
# Expected result before removing `force-dynamic`: no matches for request-time APIs unless dynamic rendering is intentional.

rg -n -C3 '\b(cookies|headers|draftMode|unstable_noStore|connection)\s*\(|\bsearchParams\b|\bfetch\s*\(' app/labs

Repository: startmunich/website

Length of output: 45


🏁 Script executed:

cat -n app/labs/page.tsx

Repository: startmunich/website

Length of output: 919


🏁 Script executed:

rg -n "LabsContent" app/labs --type tsx --type ts -A 2 -B 2

Repository: startmunich/website

Length of output: 90


🏁 Script executed:

fd -type f -name "*LabsContent*"

Repository: startmunich/website

Length of output: 233


🏁 Script executed:

fd "LabsContent" app/labs

Repository: startmunich/website

Length of output: 87


🏁 Script executed:

rg -l "LabsContent" app/labs

Repository: startmunich/website

Length of output: 105


🏁 Script executed:

cat -n app/labs/LabsContent.tsx

Repository: startmunich/website

Length of output: 50376


Remove force-dynamic from the page route — it prevents static optimization without benefit.

The page exports only static metadata and renders a client component with no request-time APIs. The shell search confirmed no usage of cookies(), headers(), draftMode(), unstable_noStore(), connection(), searchParams, or fetch() in the Labs route. Removing this line allows the page to be properly cached.

⚡ Proposed change
-export const dynamic = 'force-dynamic'
-
 export const metadata: Metadata = {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const dynamic = 'force-dynamic'
export const metadata: Metadata = {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/labs/page.tsx` at line 4, Remove the explicit runtime override by
deleting the export "export const dynamic = 'force-dynamic'" from the Labs page
component so the route can use Next.js's static optimization; ensure no
request-time APIs (cookies(), headers(), draftMode(), fetch(), searchParams,
unstable_noStore(), connection(), etc.) are introduced so the page remains
eligible for static caching.

Comment on lines +6 to +21
<div className="relative flex size-60 items-end justify-end overflow-hidden rounded-md rounded-tl-3xl rounded-br-3xl rounded-bl-3xl border border-transparent px-4 shadow ring-1 shadow-black/10 ring-black/5 md:size-100 dark:shadow-white/10 dark:ring-white/5">
<img
src="https://assets.aceternity.com/logos/calcom.png"
alt="Cal.com logo"
className="absolute inset-0 z-20 m-auto size-10 md:size-20 dark:invert dark:filter"
/>
<div className="relative z-20 flex w-full justify-between px-2 py-3 backdrop-blur-[2px] md:px-4">
<p className="text-xs font-normal text-neutral-600 md:text-sm dark:text-neutral-400">
The modern call scheduling app
</p>
<p className="text-xs font-normal text-neutral-600 md:text-sm dark:text-neutral-400">
&rarr;
</p>
</div>
<DottedGlowBackground
className="pointer-events-none mask-radial-to-90% mask-radial-at-center"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check whether non-default Tailwind utilities used by the demo are defined in repo config/CSS.
# Expected result: custom definitions for size-100 and mask-radial utilities, or update the classes to Tailwind 3-safe forms.

rg -n -C3 'size-100|mask-radial-to|mask-radial-at|maskImage|mask-image|spacing.*100|extend:\s*\{' .

Repository: startmunich/website

Length of output: 1654


🏁 Script executed:

cat -n tailwind.config.ts

Repository: startmunich/website

Length of output: 3275


🏁 Script executed:

rg -l 'size-100|mask-radial|maskImage' --type css --type ts --type tsx

Repository: startmunich/website

Length of output: 90


🏁 Script executed:

fd -e css -e scss | head -20

Repository: startmunich/website

Length of output: 78


🏁 Script executed:

cat -n package.json | head -50

Repository: startmunich/website

Length of output: 1249


🏁 Script executed:

cat app/globals.css

Repository: startmunich/website

Length of output: 10248


Replace non-standard Tailwind utilities with Tailwind 3-compatible alternatives.

The utilities md:size-100 and mask-radial-to-90% mask-radial-at-center are not defined in this project's Tailwind configuration and will not generate CSS. Update both to use arbitrary values:

Required changes
-    <div className="relative flex size-60 items-end justify-end overflow-hidden rounded-md rounded-tl-3xl rounded-br-3xl rounded-bl-3xl border border-transparent px-4 shadow ring-1 shadow-black/10 ring-black/5 md:size-100 dark:shadow-white/10 dark:ring-white/5">
+    <div className="relative flex size-60 items-end justify-end overflow-hidden rounded-md rounded-tl-3xl rounded-br-3xl rounded-bl-3xl border border-transparent px-4 shadow ring-1 shadow-black/10 ring-black/5 md:size-[25rem] dark:shadow-white/10 dark:ring-white/5">
-        className="pointer-events-none mask-radial-to-90% mask-radial-at-center"
+        className="pointer-events-none [mask-image:radial-gradient(circle_at_center,black_0%,black_90%,transparent_100%)]"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/dotted-glow-background-demo.tsx` around lines 6 - 21, Replace
non-standard Tailwind utilities with Tailwind v3 arbitrary-value equivalents:
change the container's "md:size-100" to explicit width/height arbitraries (e.g.,
replace md:size-100 with md:w-[100px] md:h-[100px]) and swap the mask utilities
"mask-radial-to-90% mask-radial-at-center" (used on the DottedGlowBackground)
for a valid arbitrary mask like
mask-[radial-gradient(closest-side_at_center,black_0,transparent_90%)]; update
any matching uses (the outer div className and the DottedGlowBackground
className) so Tailwind generates the CSS.

Comment on lines +188 to +199
for (let i = -1; i < cols; i++) {
for (let j = -1; j < rows; j++) {
const x = i * gap + (j % 2 === 0 ? 0 : gap * 0.5); // offset every other row
const y = j * gap;
// Randomize phase and speed slightly per dot
const phase = Math.random() * Math.PI * 2;
const span = Math.max(max - min, 0);
const speed = min + Math.random() * span; // configurable rad/s
dots.push({ x, y, phase, speed });
}
}
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Grid seeding loop starts at -1j goes negative.

The nested loops iterate i, j from -1 to cols/rows, so the first row computes y = -gap (off-canvas) and dots at j = -1 have phase/speed assigned but are effectively invisible. If the intent is to extend one row/column past the edge for smoother offset tiling, fine — but note that j % 2 in JS returns a negative value for negative j (-1 % 2 === -1), making the j === 0 branch asymmetric vs neighbors. Worth either iterating from 0 or using ((j % 2) + 2) % 2 to keep the staggered offset consistent at the top edge.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/ui/dotted-glow-background.tsx` around lines 188 - 199, The grid
seeding loop starts i and j at -1 which produces negative j values so j % 2 is
negative and breaks the staggered offset; fix either by starting both loops at 0
(change "for (let i = -1..."/"for (let j = -1..." to start at 0) or keep the -1
bounds but normalize the parity test (use a normalized modulo like ((j % 2) + 2)
% 2) where the offset is computed, and ensure the dots push logic (dots array
population with x, y, phase, speed) uses the corrected parity so the top row
aligns consistently with neighbors.

Comment on lines +129 to +155
return (
<motion.span
ref={ref}
className={cn(className)}
aria-label={text}
role="text"
>
{text.split("").map((char, index) => {
const isRevealed = index < revealCount;
const displayChar = isRevealed
? char
: char === " "
? " "
: (scrambleCharsRef.current[index] ??
generateRandomCharacter(charset));

return (
<span
key={index}
className={cn(isRevealed ? revealedClassName : encryptedClassName)}
>
{displayChar}
</span>
);
})}
</motion.span>
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

role="text" is non-standard ARIA.

role="text" is not defined in ARIA 1.2/1.3 — it's a Safari-only extension and is ignored (and sometimes warned about) by AXE / other tooling. Since you already have aria-label={text} on the span, the role adds no a11y benefit. Drop the role; the per-character child <span>s may also benefit from aria-hidden="true" so the full aria-label is announced once instead of character-by-character.

♻️ Proposed fix
     <motion.span
       ref={ref}
       className={cn(className)}
       aria-label={text}
-      role="text"
     >
-      {text.split("").map((char, index) => {
+      {text.split("").map((char, index) => {
         const isRevealed = index < revealCount;
         const displayChar = isRevealed
           ? char
           : char === " "
             ? " "
             : (scrambleCharsRef.current[index] ??
               generateRandomCharacter(charset));

         return (
           <span
             key={index}
+            aria-hidden="true"
             className={cn(isRevealed ? revealedClassName : encryptedClassName)}
           >
             {displayChar}
           </span>
         );
       })}
     </motion.span>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/ui/encrypted-text.tsx` around lines 129 - 155, Remove the
non-standard role by deleting role="text" from the motion.span (the element with
ref, className and aria-label={text}) and add aria-hidden="true" to each
per-character <span> (the inner span rendered inside the text.split().map) so
the outer motion.span's aria-label is announced once instead of each character
being read individually.

Comment on lines +43 to +50
<svg
className={cn(
"pointer-events-none absolute inset-0 z-0 size-full opacity-50 select-none dark:opacity-[0.75]",
className
)}
xmlns="http://www.w3.org/2000/svg"
{...props}
>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Hide the decorative SVG from assistive technology.

This noise layer is visual-only, so exposing an unlabeled SVG can add noise to the accessibility tree.

♿ Proposed fix
     <svg
       className={cn(
         "pointer-events-none absolute inset-0 z-0 size-full opacity-50 select-none dark:opacity-[0.75]",
         className
       )}
+      aria-hidden="true"
+      focusable="false"
       xmlns="http://www.w3.org/2000/svg"
       {...props}
     >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<svg
className={cn(
"pointer-events-none absolute inset-0 z-0 size-full opacity-50 select-none dark:opacity-[0.75]",
className
)}
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<svg
className={cn(
"pointer-events-none absolute inset-0 z-0 size-full opacity-50 select-none dark:opacity-[0.75]",
className
)}
aria-hidden="true"
focusable="false"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/ui/noise-texture.tsx` around lines 43 - 50, The SVG noise layer is
decorative and should be hidden from assistive tech; update the SVG element in
components/ui/noise-texture.tsx (the <svg> inside the NoiseTexture component) to
include aria-hidden="true" and focusable="false" so screen readers and keyboard
focus ignore it (no visible labels or role changes needed).

@Yasin479 Yasin479 merged commit bd80645 into main Apr 19, 2026
3 checks passed
@Yasin479 Yasin479 deleted the start-labs-site branch April 19, 2026 23:54
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.

2 participants