Files

22 KiB
Raw Permalink Blame History

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/30 with border-vermillion/30 (or border-cobalt/30) for the static state, plus a group-hover:border-ivory/30 to honor the hover swap. Or:
  • Use a plain border-current and let the hover state's text-ivory carry it (no opacity).
  • Same for the border-current/20 rules.

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.


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&nbsp;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 &nbsp; 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-3lg: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:49figures[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 0105 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-widetext-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. (#1) border-current/* opacity modifiers — visible on every lecture + standardization page.
  2. (#3) HeroComposition bottom-right text/stream overlap — visible on /intro only.
  3. (#5) Footer mt-24 — visible on every page when content is short.
  4. (#2) HeroComposition node-pulse drift — only on /intro, and only on Firefox pre-2024.
  5. (#7) Intro "Standards" overflow — only on /intro at 1280+ viewports.
  6. (#8) Members group 3 ghost cell — visible on /members.
  7. (#9) No-JS fallback — affects every page.

The remaining items are polish (🟡).