22 KiB
Layout Issue Catalogue — refer_landing_page
Date: 2026-06-16
Scope: all app/**/page.tsx, components/*.tsx, app/globals.css, tailwind.config.ts, app/intro/_components/HeroComposition.tsx.
Method: source review + live dev render (npx next dev -p 4510, all 5 routes, 200 OK). No browser available so visual verification is by DOM + class composition analysis; defects that can be derived from class+CSS math are flagged as Verified, defects that would need a viewport to be 100% sure are flagged as Likely.
Severity legend: 🔴 bug (visible defect) · 🟠 likely (probable defect worth fixing) · 🟡 polish (improvement, not a bug).
1. 🔴 border-current/30 and border-current/20 do NOT actually follow the current text color
Files:
app/lectures/page.tsx:86—<p ... className="mt-4 inline-flex w-fit border border-current/30 px-3 py-1 ..."> ... level ...app/lectures/page.tsx:90—<p ... className="mt-6 border-t border-current/20 pt-6 ...">app/standardization/page.tsx:145—<p ... className="mt-4 inline-flex w-fit border border-current/30 ...">app/standardization/page.tsx:150—<ul ... className="mt-6 space-y-4 border-t border-current/20 pt-6 ...">app/intro/page.tsx:246—<li className="flex gap-3 border-t border-current/20 pt-3">(thrust card points)
What's wrong: currentColor is a CSS keyword, not a hex. Tailwind's opacity-modifier syntax (border-current/30) compiles the color to a fixed fallback gray (rgb(229 231 235 / 0.3)), not the actual inherited text color. The intent is clearly "in the rest state, a 30%-opacity border in the card's own text color; on hover (when text becomes ivory), the border becomes 30%-opacity ivory." In practice the border stays a fixed gray that does not shift on hover.
Fix direction:
- Replace
border-current/30withborder-vermillion/30(orborder-cobalt/30) for the static state, plus agroup-hover:border-ivory/30to honor the hover swap. Or: - Use a plain
border-currentand let the hover state'stext-ivorycarry it (no opacity). - Same for the
border-current/20rules.
Confidence: Verified. The CSS that Tailwind emits is a static color, not a currentColor cascade.
2. 🔴 HeroComposition node pulse animation drifts the circles off-position (SVG transform-origin bug)
File: app/intro/_components/HeroComposition.tsx:82-93
{nodes.map((n, i) => (
<circle key={i}
cx={n.x} cy={n.y}
r={i % 2 === 0 ? 9 : 6}
className="animate-node-pulse"
style={{ animationDelay: `${i * 0.5}s`, transformOrigin: `${n.x}px ${n.y}px` }}
/>
))}
Combined with the keyframe in tailwind.config.ts:96-99:
node-pulse: { 0%, 100%: { opacity: 0.35, transform: "scale(1)" }, 50%: { opacity: 1, transform: "scale(1.12)" } }
What's wrong: SVG <circle> elements have a CSS transform-box default of view-box in modern Chrome/Safari but fill-box (or border-box historically) in some Firefox versions. The inline transformOrigin: '70px 90px' is in the element's own coordinate space, NOT relative to the circle's bounding box. When transform: scale(1.12) is applied at the 50% keyframe, the circle scales from the SVG user-units origin (0,0) instead of from its own (cx, cy). In Chrome with transform-box: view-box (current default), the circle stays roughly in place (origin = SVG user origin = 70,90 if you set the inline transformOrigin to that), but Firefox's older default causes the circle to translate by 0.12 * 70 = 8.4px to the right and 0.12 * 90 = 10.8px down on every pulse.
Fix direction: add transform-box: fill-box; transform-origin: center; to the circle (either via a new utility in globals.css or inline style={{ transformBox: 'fill-box', transformOrigin: 'center' }}). Then scale(1.12) will scale the circle around its own center regardless of cx/cy.
Confidence: Likely (Chrome 79+ default view-box mostly masks this; Firefox pre-122 / pre-2024 still drifts). Worth fixing because the docstring in DESIGN.md calls out node-pulse as a key visual.
3. 🔴 HeroComposition bottom-right annotation text overlaps the QUIC stream curves
File: app/intro/_components/HeroComposition.tsx:99-109
<text x="360" y="356" textAnchor="end" fill="#0A0A0A" fontSize="11" fontWeight="600" letterSpacing="2">
QUIC · STREAMS
</text>
The four QUIC stream paths are rendered with y = 250 + i * 30 (i = 0..3 → 250, 280, 310, 340) and end points at 360, y-20 (so 230, 260, 290, 320). The curves use control points up to y+40 (290, 320, 350, 380). The bottom-right text is at y=356. The 4th stream's curve crosses through the text region from roughly (240, 380) to (360, 320), which is the same y-band the text sits in.
Fix direction: move the text up to y=374 (clear of stream 4's body) or move the text outside the frame entirely (e.g. add a small white-rect "chip" behind it) — simplest: shift the text y to ~24 from the bottom and ensure no stream path control point enters that band. Or reduce the number of streams to 3.
Confidence: Verified by SVG geometry.
4. 🔴 HeroComposition outer frame stroke is clipped at the SVG edges
File: app/intro/_components/HeroComposition.tsx:43-51
<rect x="6" y="6" width="368" height="368" strokeWidth="1.5" />
A strokeWidth of 1.5 means the stroke straddles the path: 0.75px outside, 0.75px inside. With the rect at x=6, y=6 inside a viewBox="0 0 380 380", the 0.75px outer half is still inside the viewBox (x=6 - 0.75 = 5.25 is still > 0). So this is actually fine — I had it wrong on the first pass. No fix needed. Keeping it here so it doesn't get re-flagged.
Confidence: Verified. Frame is fine.
5. 🟠 Footer has excessive top whitespace (mt-24 × flex-1 main)
File: components/Footer.tsx:15
<footer className="mt-24 border-t border-ink bg-ink text-ivory">
app/layout.tsx:54-57:
<body className="flex min-h-screen flex-col">
<Header />
<main className="flex-1">{children}</main>
<Footer />
</body>
What's wrong: <main> is flex-1, so it grows to push the footer to the bottom of the viewport on short pages. Adding mt-24 (6rem) on the footer puts 6rem of empty space between the main content and the footer on every page — but only visible on pages that don't fill the viewport. The intro page is long enough that the footer sits at the natural end and the mt-24 collapses; on short pages (/lectures) the gap shows up.
Fix direction: drop mt-24 from <footer> (the border-t border-ink already provides visual separation; main content already has its own py-16 sm:py-24 section padding). If a deliberate breathing room is wanted, use mt-0 lg:mt-8 (no margin on mobile, small on desktop). The current value is 2-3× what's needed.
Confidence: Verified. flex-1 + mt-24 is a double-space.
6. 🟠 PageHeader <h1> has no word-break: keep-all / text-wrap: balance for Korean titles
File: components/PageHeader.tsx:32-34
<h1 className="headline-ko text-4xl leading-[1.05] text-ink sm:text-5xl lg:text-6xl">
{ko}
</h1>
What's wrong: Korean text is mostly space-free, so the browser breaks at any character. The longer PageHeader titles ("멀티에이전트 오케스트레이션", "메타버스 상호운용성과 표준", "표준화 활동") can break at awkward points at the 4xl/sm:5xl/lg:6xl scale. For a magazine aesthetic, line breaks should fall at natural word boundaries (which in CJK is between phrases separated by spaces).
Fix direction: add break-keep (Tailwind utility for word-break: keep-all) to the h1. Optionally also text-balance for visual balance. Cost: 2 utility classes.
Confidence: Likely. The shorter titles ("논문", "강의") won't trigger this; the longer ones on /lectures and /intro will.
7. 🟠 HeroComposition "in Motion" word is in a 12-character tail that gets line-broken by <p className="display"> clamp
File: app/intro/page.tsx:96-99
<p className="display mt-3 leading-[0.9]" aria-hidden>
Standards
<span className="block italic text-cobalt">in Motion</span>
</p>
What's wrong: "Standards" alone at clamp(3rem, 10vw, 9rem) on a lg:col-span-7 column (~54rem wide) fits on one line, "in Motion" is in a block so it forces onto a new line. The in "in Motion" prevents breaking inside that phrase. The visual effect is two stacked display lines. The current text-display font size is clamp(3rem, 10vw, 9rem) — at 1280px viewport, 10vw = 128px, capped at 9rem (144px). "Standards" at 144px in Fraunces is roughly 11× glyph width × 0.55em = 870px wide. The col-span-7 column is 7/12 × 78rem (with 12px padding × 2) = 45rem ≈ 720px. "Standards" overflows the column by ~150px. The display text will visually intrude into the col-span-5 SVG column or even cause horizontal overflow.
Fix direction: reduce the upper cap of display from 9rem to clamp(3rem, 8vw, 7rem) in tailwind.config.ts:74, or set lg:col-span-8 for the headline column (gives it 8/12 × ~78rem = ~52rem ≈ 832px) — but the layout is lg:grid-cols-12 gap-10 and the col-span-5 is hard-wired, so the cleaner fix is the font-size cap. Or break "Standards" into two lines: <span className="block">Stan</span><span>dards</span> — but this changes the design intent. Recommended: cap display at 7rem or 7.5rem.
Confidence: Likely. Need a real viewport to measure, but the math says it overflows at 1280+. The user has not yet given explicit viewport feedback, so the safer fix is to reduce the cap, not the layout.
8. 🟠 Member grid: 3rd group has 2 members in a 3-col grid → ghost cell with border-line only
File: app/members/page.tsx:151
<div className="mt-px grid gap-px bg-line sm:grid-cols-2 lg:grid-cols-3">
{g.members.map((m, mi) => ( ... ))}
</div>
The third group "학부 연구원" (line 41-49) has 2 members. With lg:grid-cols-3, 2 cards in 3 columns leaves a 1-cell-wide column with only the bg-line 1px background showing. The empty cell will appear as a thin ivory rectangle bounded by 1px lines on the right and bottom, next to the last filled card. Visually it looks like a half-finished row.
Fix direction: change lg:grid-cols-3 → lg:grid-cols-2 (uniform 2-col across all groups), or use auto-fill so the grid auto-fits, or add a flex-1 invisible filler div. The simplest: lg:grid-cols-2 (consistent with the 2-col advisor grid above it).
Confidence: Verified. Group 3 has exactly 2 placeholder members.
9. 🟠 Reveal initial opacity: 0 — no JS / no-JS fallback for content visibility
File: app/globals.css:104-110
.reveal { opacity: 0; transform: translateY(28px); ... }
What's wrong: Every page wraps its content in <Reveal> (with default variant="up"). If JavaScript fails to load (or fails to hydrate due to a network error on a Google Font CSS request, which we've already seen fail in this very environment — see next build log: request to https://fonts.googleapis.com/... failed, reason:), the user sees a fully blank page because every block stays at opacity: 0. There is no <noscript> rule that sets opacity: 1 for non-JS users, and no progressive enhancement (e.g. noscript tag, or a html.no-js .reveal { opacity: 1 } pattern).
This is a real concern because the build log already shows font fetches failing in this environment; if the React hydration also fails, the user sees nothing.
Fix direction: add to app/globals.css:
html.no-js .reveal { opacity: 1; transform: none; transition: none; }
and in app/layout.tsx <html> tag, default the className to include no-js, then in a small inline <script> (or in Header.tsx's "use client") strip the class on mount. Alternatively use the simpler motion-safe: / progressive approach: add @media (prefers-reduced-motion: reduce) to also force opacity: 1 (already in CSS at line 133-146) — but reduced-motion is not the same as no-JS.
The simplest robust fix: add <noscript><style>.reveal { opacity: 1 !important; transform: none !important; }</style></noscript> in app/layout.tsx.
Confidence: Likely. The build log proves font fetch failures happen in this network; hydration races are a related class of failure.
10. 🟡 PageHeader display fallback (when no display prop passed) puts the full English word in vermillion at cover scale
File: components/PageHeader.tsx:35-37
<p className="display mt-2 text-vermillion" aria-hidden>
{display ?? en}
</p>
When display is not provided, this renders the full en (e.g. "Publications", "Members", "Standardization") at 9rem cap. The longest is "Standardization" (14 chars) ≈ 14 × 144px × 0.5em = 1008px — wider than the col-span-8 column. Only the intro page passes display; the other 4 pages (/members, /publications, /lectures, /standardization) fall through to the en value.
Fix direction: either always require a display (shorter) prop, or apply a per-page override that fits, or set display-sm (which is clamp(2.25rem, 6vw, 4.5rem)) on this <p> instead of display. display-sm would be a safer default.
Confidence: Likely; "Standardization" specifically will overflow. The intro page is fine because it passes display="Standards".
11. 🟡 Publications: Counter for the year "2026" ticks 0→2026 on every page load
File: app/intro/page.tsx:49 — figures[2] = { value: 2026, label: "Issue · 발행" }
File: app/intro/page.tsx:152 — <Counter value={f.value} ...>
Counter (components/Counter.tsx) does 0 → 2026 over 1.6s with easeOutExpo (jumps to ~2018 in 0.5s). For a year value, the tick is conceptually odd — a year that "counts up" from 0 is jarring in a magazine layout where the issue number should be a fixed figure. Same applies to app/publications/page.tsx where figures[0].value = publications.length and figures[2].value = publications.filter(p => p.type === "Conference").length both legitimately count up.
Fix direction: pass an instant prop to Counter (default to true for value >= 1000 or for prefix/suffix containing a non-digit), or split into two components (Counter for stats, Static for years). Simpler: in intro/page.tsx change value: 2026 to render as static text: { value: 2026, label: "Issue · 발행", static: true } and have Counter respect the static flag.
Confidence: Likely (UX bug, not a layout bug — listed because the design spec in DESIGN.md §4 emphasizes "magazine scale" which implies static figures).
12. 🟡 Intro / Publications / Standardization "publication list" indices 01–05 in text-5xl leading-none text-ink-mute may be too large for a 3-col rail on the sm: breakpoint
File: app/publications/page.tsx:135-140
<div className="flex items-baseline gap-4 sm:col-span-3 sm:flex-col sm:gap-3">
<span className="font-display text-5xl leading-none text-ink-mute">{String(i + 1).padStart(2, "0")}</span>
...
At sm (640px-1023px), the rail is full-width (since the grid becomes sm:grid-cols-12 only on the <li>, not the rail). Wait — actually the parent <li> is sm:grid-cols-12, so on sm the rail takes sm:col-span-3 = 25% of 78rem ≈ 19.5rem = 312px. The 3 items stack vertically (sm:flex-col) with the "01" (text-5xl = 3rem = 48px) at the top. 48px number "01" plus 24px year "2024" plus 12px badge "Conference" with gap-3 between = ~120px tall. The title (col-span-9) is headline-ko text-xl ≈ 1.25rem and the title is multi-line. Both sides align by grid row stretch. Looks fine.
Confidence: Verified fine. No fix needed.
13. 🟡 Counter card vertical alignment when values are different magnitudes (2 / 4 / 2026)
File: app/intro/page.tsx:147-160
<section className="border-b border-ink">
<div className="container-content grid gap-px bg-line sm:grid-cols-3">
{figures.map((f, i) => (
<Reveal ...>
<div className="px-6 py-10">
<Counter value={f.value} suffix={f.suffix} className="display block text-ink" />
<p className="kicker mt-3">{f.label}</p>
</div>
</Reveal>
))}
</div>
</section>
The "display" value at clamp(3rem, 10vw, 9rem) for "2" (1 glyph) vs "2026" (4 glyphs) → "2026" is roughly 4× wider. All three cells have bg-ivory and px-6 py-10, so the kicker sits at the same vertical offset per cell. The widths are 1/3 of 78rem each (~26rem), and "2026" at 9rem is ~28rem wide — close to the cell width. On lg the "2026" will push against the cell padding. The "2" and "4" cells look sparse in comparison. Editorial choice — could be fine — but worth a note.
Fix direction: give the figures different display sizes per magnitude (use text-display-sm for years and 4-digit numbers), or apply text-3xl to 2 and 4 and display to 2026. Or shorten 2026 to 26 for the year.
Confidence: Likely; depends on exact Fraunces metrics which need a browser to verify.
14. 🟡 Header mobile menu: id="mobile-menu" missing an aria-controls connection on the toggle button
File: components/Header.tsx:79-94 and :99-126
The button has aria-controls="mobile-menu" and aria-expanded={open} (line 84-85). It also has aria-label switching between "메뉴 열기" / "메뉴 닫기" (line 82). The <nav id="mobile-menu"> (line 99-101) has the matching id. This is actually correct; I had it on my "suspect" list but on re-read it's fine. No fix needed.
Confidence: Verified. ARIA wiring is correct.
15. 🟡 flex-1 on <main> collides with flex-col body when content is short
File: app/layout.tsx:54-57
The body is flex min-h-screen flex-col. The main is flex-1 (= flex: 1 1 0%). This is the standard "sticky footer" pattern and works correctly. The Header is sticky top-0 z-50, which means it's position: sticky inside the flex column. Sticky elements inside a flex container have known issues with align-items: stretch (default): the sticky header doesn't actually stick on some browsers when inside a flex column. In Chrome 2020+ this is fixed, but worth flagging for older mobile browsers.
Fix direction: set the body's align-items to flex-start (or change flex-col to block and use margin-top: auto on main). Alternatively, leave the header outside the flex column by removing flex-1 from main and adding min-h-[calc(100vh-4.25rem)] to main.
Confidence: Likely; this is a browser-version-dependent issue.
16. 🟡 Header h-[4.25rem] uses an arbitrary value that doesn't align with tailwind's 4 (=1rem) spacing scale
File: components/Header.tsx:37 — <div className="container-content flex h-[4.25rem] items-center justify-between">
What's wrong: 4.25rem is 68px. The header is taller than typical (most 1-line headers are h-16 (64px) or h-14 (56px)). The brand block has 2 lines (사물인터넷 표준 연구실 + IoT Standards Lab kicker) so it needs ~64-72px height. Acceptable. But the value 4.25rem is brittle — if the brand changes from 2 lines to 1, the header keeps the same height leaving a 32px gap. Not a bug today, but a "trap" for future edits.
Fix direction: use min-h-[4.25rem] instead of h-[4.25rem], or define it as a CSS variable, or replace with h-16 (64px) + py-2 to allow brand to expand.
Confidence: Verified, but cosmetic.
17. 🟡 SectionLabel corner label is font-display text-sm font-medium tabular-nums tracking-wide — text-sm is 14px which is small for editorial display
File: components/SectionLabel.tsx:22 and app/globals.css:69
.corner-label { @apply font-display text-sm font-medium tabular-nums tracking-wide text-ink-mute; }
What's wrong: text-sm (0.875rem = 14px) is a small body size, used here for the section number "01 / 05". For an editorial magazine aesthetic, this corner label is too small to read as a "spread number". Most editorial spreads put the number at 16-18px (text-base / text-lg).
Fix direction: bump to text-base (16px) or text-[0.95rem] for better legibility while keeping the wide tracking.
Confidence: Editorial preference, not a bug.
Summary
| # | File | Severity | One-line |
|---|---|---|---|
| 1 | lectures/standardization/intro border-current/* |
🔴 | Doesn't actually follow currentColor — falls back to gray |
| 2 | HeroComposition circle transformOrigin |
🔴 | Node-pulse scales from SVG origin not circle center, drifts in some browsers |
| 3 | HeroComposition bottom-right text | 🔴 | "QUIC · STREAMS" text overlaps 4th stream curve |
| 4 | (re-flag of frame) | — | No issue; rect coords are inside viewBox |
| 5 | Footer mt-24 |
🟠 | Doubles up with flex-1 main, 6rem dead space on short pages |
| 6 | PageHeader <h1> |
🟠 | No break-keep for long Korean titles, breaks mid-syllable |
| 7 | Intro display "Standards" |
🟠 | At 9rem cap, overflows col-span-7 column at 1280+ |
| 8 | Members group 3 | 🟠 | 2 cards in 3-col grid → ghost cell with bg-line showing |
| 9 | Reveal no-JS fallback |
🟠 | No <noscript> rule — blank page if JS fails |
| 10 | PageHeader display fallback |
🟡 | When display prop absent, uses long en at 9rem (e.g. "Standardization" overflows) |
| 11 | Counter year 2026 ticks 0→2026 |
🟡 | Year should be static, not animated |
| 12 | Publications list rail | — | Verified fine; no fix |
| 13 | Counter cards different magnitudes | 🟡 | "2026" ~4× wider than "2"/"4" — visual imbalance |
| 14 | Header ARIA | — | Verified fine; no fix |
| 15 | Sticky header in flex column | 🟡 | Older mobile browsers: sticky inside flex can fail |
| 16 | Header h-[4.25rem] arbitrary |
🟡 | Brittle, hardcoded for current 2-line brand |
| 17 | SectionLabel text-sm |
🟡 | 14px is too small for an editorial spread number |
Real bugs to fix in priority order:
- (#1)
border-current/*opacity modifiers — visible on every lecture + standardization page. - (#3) HeroComposition bottom-right text/stream overlap — visible on
/introonly. - (#5) Footer
mt-24— visible on every page when content is short. - (#2) HeroComposition node-pulse drift — only on
/intro, and only on Firefox pre-2024. - (#7) Intro "Standards" overflow — only on
/introat 1280+ viewports. - (#8) Members group 3 ghost cell — visible on
/members. - (#9) No-JS fallback — affects every page.
The remaining items are polish (🟡).