Files

351 lines
22 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`
```tsx
{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`
```tsx
<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`
```tsx
<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`
```tsx
<footer className="mt-24 border-t border-ink bg-ink text-ivory">
```
`app/layout.tsx:54-57`:
```tsx
<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`
```tsx
<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`
```tsx
<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`
```tsx
<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`
```css
.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`:
```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`
```tsx
<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`
```tsx
<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`
```tsx
<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`
```css
.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 (🟡).