feat(site): redesign promotion experience
This commit is contained in:
parent
6930906583
commit
13dd160c16
39 changed files with 5764 additions and 5738 deletions
|
|
@ -1,5 +1,7 @@
|
|||
---
|
||||
import { ko, en } from "../i18n/content";
|
||||
import { landingKo, landingEn } from "../i18n/landing";
|
||||
import socialPreviewKo from "../assets/og-designpaca-ko.png";
|
||||
import socialPreviewEn from "../assets/og-designpaca-en.png";
|
||||
import "../styles/fonts.css";
|
||||
import "../styles/tokens.css";
|
||||
import "../styles/base.css";
|
||||
|
|
@ -8,72 +10,82 @@ interface Props {
|
|||
lang: "ko" | "en";
|
||||
title: string;
|
||||
description: string;
|
||||
translationPath?: string | null;
|
||||
}
|
||||
|
||||
const { lang, title, description } = Astro.props;
|
||||
|
||||
// 언어별로 자기 폰트만 받는다. 두 벌을 한 페이지에 로드하면 예산을 넘긴다.
|
||||
const FONT_CSS = {
|
||||
ko: "https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css",
|
||||
en: "https://api.fontshare.com/v2/css?f%5B%5D=switzer@400,500,600&display=swap",
|
||||
} as const;
|
||||
|
||||
const alt = lang === "ko" ? "/en/" : "/";
|
||||
|
||||
// 헤더 내비게이션. 본문과 같은 콘텐츠 파일에서 온다 — 라벨을 레이아웃에
|
||||
// 하드코딩하면 두 언어판이 어긋난다.
|
||||
const nav = (lang === "ko" ? ko : en).nav;
|
||||
|
||||
// 내비 앵커는 홈의 섹션을 가리킨다. 홈이 아닌 페이지(쇼케이스 등)에서는
|
||||
// "#pipeline" 이 그 페이지 안의 없는 앵커가 되어 아무 일도 하지 않는다 —
|
||||
// 홈 경로를 붙여 홈 섹션으로 건너가게 한다.
|
||||
const copy = lang === "ko" ? landingKo : landingEn;
|
||||
const socialPreview = lang === "ko" ? socialPreviewKo : socialPreviewEn;
|
||||
const socialPreviewAlt = lang === "ko"
|
||||
? "designpaca의 일곱 단계 디자인 파이프라인 그래픽"
|
||||
: "The seven-stage designpaca pipeline graphic";
|
||||
const path = Astro.url.pathname;
|
||||
const normalizedPath = path.replace(/\/+$/, "") || "/";
|
||||
const isKoHome = normalizedPath === "/";
|
||||
const isEnHome = normalizedPath === "/en";
|
||||
const inferredTranslationPath = isKoHome ? "/en/" : isEnHome ? "/" : null;
|
||||
const translationPath = Object.prototype.hasOwnProperty.call(Astro.props, "translationPath")
|
||||
? Astro.props.translationPath
|
||||
: inferredTranslationPath;
|
||||
const homePath = lang === "ko" ? "/" : "/en/";
|
||||
const normalizedPath = Astro.url.pathname.replace(/\/+$/, "") || "/";
|
||||
const isHome = normalizedPath === (lang === "ko" ? "/" : "/en");
|
||||
const navItems = nav.items.map((i) => ({
|
||||
...i,
|
||||
href: !isHome && i.href.startsWith("#") ? `${homePath}${i.href}` : i.href,
|
||||
const showcasePath = lang === "ko" ? "/showcase/" : "/en/showcase/";
|
||||
const isHome = isKoHome || isEnHome;
|
||||
const navItems = copy.nav.items.map((item) => ({
|
||||
...item,
|
||||
href: isHome ? item.href : `${homePath}${item.href}`,
|
||||
}));
|
||||
|
||||
// 파비콘: 페이지의 스파인 모티프를 그대로 쓴 단순 기하 마크.
|
||||
// data: URI 안의 SVG 는 상위 문서의 커스텀 프로퍼티를 읽지 못한다.
|
||||
// 하드 게이트 #1 의 유일한 예외이고, 값은 tokens.css 와 손으로 맞춘다.
|
||||
// MARK_BG = --accent-inverse · MARK_FG = --surface-inverse
|
||||
const MARK_BG = "#0E6B63";
|
||||
const MARK_FG = "#F4F2EC";
|
||||
const FAVICON =
|
||||
"data:image/svg+xml," +
|
||||
encodeURIComponent(
|
||||
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<rect width="32" height="32" rx="3" fill="${MARK_BG}"/>
|
||||
<path d="M11 7v18" stroke="${MARK_FG}" stroke-width="2" stroke-linecap="round"/>
|
||||
<circle cx="11" cy="10" r="3" fill="${MARK_FG}"/>
|
||||
<circle cx="11" cy="16" r="3" fill="${MARK_FG}"/>
|
||||
<circle cx="11" cy="22" r="3" fill="${MARK_FG}"/>
|
||||
<path d="M18 10h7M18 16h5M18 22h7" stroke="${MARK_FG}" stroke-width="2" stroke-linecap="round" opacity=".55"/>
|
||||
</svg>`,
|
||||
);
|
||||
const repositoryUrl = "https://git.chanpaca.net/yunchan/designpaca";
|
||||
const canonical = new URL(path, Astro.site);
|
||||
const translated = translationPath ? new URL(translationPath, Astro.site) : null;
|
||||
const socialPreviewUrl = new URL(socialPreview.src, Astro.site);
|
||||
const markBg = "%23c43f20";
|
||||
const markFg = "%23faf9f4";
|
||||
const favicon = `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='3' fill='${markBg}'/%3E%3Cpath d='M8 6v20M8 7h12M8 11h8M8 15h13M8 19h9M8 23h12' stroke='${markFg}' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E`;
|
||||
const schema = isHome
|
||||
? {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebSite",
|
||||
name: "designpaca",
|
||||
url: new URL("/", Astro.site).href,
|
||||
description,
|
||||
inLanguage: ["ko-KR", "en-US"],
|
||||
}
|
||||
: {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebPage",
|
||||
name: title,
|
||||
url: canonical.href,
|
||||
description,
|
||||
inLanguage: lang === "ko" ? "ko-KR" : "en-US",
|
||||
};
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang={lang} class="no-js">
|
||||
<html lang={lang}>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>{title}</title>
|
||||
<meta name="description" content={description} />
|
||||
<link rel="icon" href={FAVICON} />
|
||||
<meta name="theme-color" content="#f2f1eb" />
|
||||
<meta name="color-scheme" content="light" />
|
||||
<link rel="icon" href={favicon} />
|
||||
|
||||
<link rel="canonical" href={new URL(Astro.url.pathname, Astro.site)} />
|
||||
<link rel="alternate" hreflang="ko" href={new URL("/", Astro.site)} />
|
||||
<link rel="alternate" hreflang="en" href={new URL("/en/", Astro.site)} />
|
||||
<link rel="alternate" hreflang="x-default" href={new URL("/", Astro.site)} />
|
||||
<link rel="canonical" href={canonical} />
|
||||
{translated && !isHome && <link rel="alternate" hreflang={lang} href={canonical} />}
|
||||
{translated && !isHome && <link rel="alternate" hreflang={lang === "ko" ? "en" : "ko"} href={translated} />}
|
||||
{translated && !isHome && <link rel="alternate" hreflang="x-default" href={lang === "ko" ? canonical : translated} />}
|
||||
{isHome && <link rel="alternate" hreflang="ko" href={new URL("/", Astro.site)} />}
|
||||
{isHome && <link rel="alternate" hreflang="en" href={new URL("/en/", Astro.site)} />}
|
||||
{isHome && <link rel="alternate" hreflang="x-default" href={new URL("/", Astro.site)} />}
|
||||
|
||||
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin />
|
||||
<link
|
||||
rel="preconnect"
|
||||
href={lang === "ko" ? "https://cdn.jsdelivr.net" : "https://api.fontshare.com"}
|
||||
crossorigin
|
||||
rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/gh/wanteddev/wanted-sans@v1.0.3/packages/wanted-sans/fonts/webfonts/variable/split/WantedSansVariable.min.css"
|
||||
/>
|
||||
<link rel="stylesheet" href={FONT_CSS[lang]} />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&display=swap"
|
||||
|
|
@ -84,309 +96,210 @@ const FAVICON =
|
|||
<meta property="og:title" content={title} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content={canonical} />
|
||||
<meta property="og:locale" content={lang === "ko" ? "ko_KR" : "en_US"} />
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
|
||||
{/* JS 가 살아 있으면 no-js 를 떼고 진입 연출을 켠다 */}
|
||||
<script is:inline>
|
||||
document.documentElement.classList.remove("no-js");
|
||||
</script>
|
||||
<meta property="og:image" content={socialPreviewUrl} />
|
||||
<meta property="og:image:width" content={socialPreview.width} />
|
||||
<meta property="og:image:height" content={socialPreview.height} />
|
||||
<meta property="og:image:alt" content={socialPreviewAlt} />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:image" content={socialPreviewUrl} />
|
||||
<meta name="twitter:image:alt" content={socialPreviewAlt} />
|
||||
<script type="application/ld+json" set:html={JSON.stringify(schema)} />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<a class="skip" href="#main">{lang === "ko" ? "본문으로 건너뛰기" : "Skip to content"}</a>
|
||||
|
||||
<header class="site-header">
|
||||
<div class="header-inner glass">
|
||||
<span class="rim" aria-hidden="true"></span>
|
||||
<a class="wordmark" href={lang === "ko" ? "/" : "/en/"} aria-label="designpaca">
|
||||
<svg class="mark" viewBox="0 0 32 32" width="20" height="20" aria-hidden="true">
|
||||
<path d="M11 7v18" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" />
|
||||
<circle cx="11" cy="10" r="3" fill="currentColor" />
|
||||
<circle cx="11" cy="16" r="3" fill="currentColor" />
|
||||
<circle cx="11" cy="22" r="3" fill="currentColor" />
|
||||
<div class="header-inner shell">
|
||||
<a class="wordmark" href={homePath} translate="no" aria-label={lang === "ko" ? "designpaca 홈" : "designpaca home"}>
|
||||
<svg class="wordmark-mark" viewBox="0 0 24 24" width="22" height="22" aria-hidden="true">
|
||||
<path d="M4 2.5v19M4 3h13M4 6h9M4 9h15M4 12h10M4 15h14M4 18h8M4 21h12" />
|
||||
</svg>
|
||||
<span>designpaca</span>
|
||||
</a>
|
||||
|
||||
<nav class="nav-main" aria-label={lang === "ko" ? "섹션" : "Sections"}>
|
||||
{navItems.map((i) => <a href={i.href}>{i.label}</a>)}
|
||||
{/* 페이지를 벗어나는 유일한 링크다. 화살표로 그렇다고 알린다 */}
|
||||
<a class="nav-repo" href={nav.repo} rel="noopener">
|
||||
{nav.repoLabel}
|
||||
<svg viewBox="0 0 12 12" width="9" height="9" aria-hidden="true">
|
||||
<path d="M3.5 8.5 8.5 3.5M4.5 3.5h4v4" stroke="currentColor"
|
||||
stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" fill="none" />
|
||||
</svg>
|
||||
</a>
|
||||
<nav class="nav-primary" aria-label={copy.nav.menuLabel}>
|
||||
{navItems.map((item) => <a href={item.href}>{item.label}</a>)}
|
||||
</nav>
|
||||
|
||||
<div class="header-end">
|
||||
{/* 언어는 국가가 아니다. 국기 아이콘을 쓰지 않는다.
|
||||
좁은 화면(<480px)에서는 이 버튼이 사라지고 메덹 안 행으로 들어간다 */}
|
||||
<a
|
||||
class="btn btn-quiet btn-sm lang-btn"
|
||||
href={alt}
|
||||
hreflang={lang === "ko" ? "en" : "ko"}
|
||||
lang={lang === "ko" ? "en" : "ko"}
|
||||
>
|
||||
{lang === "ko" ? "English" : "한국어"}
|
||||
<div class="header-actions">
|
||||
<a class="header-archive" href={showcasePath}>{copy.nav.showcaseLabel}</a>
|
||||
{translationPath && (
|
||||
<a
|
||||
class="header-lang"
|
||||
href={translationPath}
|
||||
hreflang={lang === "ko" ? "en" : "ko"}
|
||||
lang={lang === "ko" ? "en" : "ko"}
|
||||
aria-label={copy.nav.langLabel}
|
||||
>
|
||||
{lang === "ko" ? "EN" : "KO"}
|
||||
</a>
|
||||
)}
|
||||
<a class="header-repo" href={repositoryUrl} rel="noopener" aria-label={copy.nav.repoLabel}>
|
||||
<svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true">
|
||||
<path d="M4 12 12 4M6 4h6v6" />
|
||||
</svg>
|
||||
</a>
|
||||
|
||||
{/* 모바일 메뉴. details 라 JS 가 0바이트에 가깝고, 열림 상태,
|
||||
Esc, 포커스 순서, aria-expanded 를 브라우저가 이미 맞게 한다.
|
||||
직접 만들면 그 넷을 전부 다시 구현해야 하고 보통 틀린다.
|
||||
시트는 알약 전체 폭으로 열린다 — 11rem 짜리 조각 상자가 아니라 */}
|
||||
<details class="nav-mobile">
|
||||
<summary class="btn btn-quiet btn-sm" aria-label={nav.menu} aria-haspopup="menu">
|
||||
<svg class="i-burger" viewBox="0 0 16 16" width="16" height="16" aria-hidden="true">
|
||||
<path d="M2 4h12M2 8h12M2 12h12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
|
||||
</svg>
|
||||
<svg class="i-close" viewBox="0 0 16 16" width="16" height="16" aria-hidden="true">
|
||||
<path d="M3.5 3.5l9 9M12.5 3.5l-9 9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
|
||||
</svg>
|
||||
</summary>
|
||||
<nav class="nav-sheet glass" aria-label={nav.menu}>
|
||||
{navItems.map((i, idx) => (
|
||||
<a href={i.href}>
|
||||
<span class="sheet-num" aria-hidden="true">{String(idx + 1).padStart(2, "0")}</span>
|
||||
{i.label}
|
||||
</a>
|
||||
))}
|
||||
<a href={nav.repo} rel="noopener">
|
||||
<span class="sheet-num" aria-hidden="true">↗</span>
|
||||
{nav.repoLabel}
|
||||
</a>
|
||||
<a
|
||||
class="sheet-lang"
|
||||
href={alt}
|
||||
hreflang={lang === "ko" ? "en" : "ko"}
|
||||
lang={lang === "ko" ? "en" : "ko"}
|
||||
>
|
||||
<span class="sheet-num" aria-hidden="true">A</span>
|
||||
{lang === "ko" ? "English" : "한국어"}
|
||||
</a>
|
||||
</nav>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main id="main" class="page">
|
||||
<main id="main" tabindex="-1">
|
||||
<slot />
|
||||
</main>
|
||||
|
||||
|
||||
<script is:inline>
|
||||
// 메덥 닫기 두 가지. details 의 나머지(Esc·포커스·aria)는 브라우저 몫.
|
||||
// 1) 행을 누르면 앵커로 스크롤하고 메덥은 스스로 닫는다 —
|
||||
// 열린 채로 남아 본문을 가리는 것이 이 메뉴의 유일한 결함이었다.
|
||||
// 2) 바깥을 누르면 닫는다. scroll 리스너는 쓰지 않는다(게이트 10).
|
||||
(function () {
|
||||
var menu = document.querySelector(".nav-mobile");
|
||||
if (!menu) return;
|
||||
menu.addEventListener("click", function (e) {
|
||||
if (e.target && e.target.closest && e.target.closest("a")) {
|
||||
menu.removeAttribute("open");
|
||||
}
|
||||
});
|
||||
document.addEventListener("click", function (e) {
|
||||
if (menu.open && !(e.target && e.target.closest && e.target.closest(".nav-mobile"))) {
|
||||
menu.removeAttribute("open");
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
// 진입 연출. 한 번 보이면 계속 보인다.
|
||||
// scroll 리스너를 쓰지 않는다(하드 게이트 10).
|
||||
(function () {
|
||||
var els = document.querySelectorAll(".reveal");
|
||||
if (!("IntersectionObserver" in window) || !els.length) {
|
||||
els.forEach(function (el) { el.classList.add("is-in"); });
|
||||
return;
|
||||
}
|
||||
var io = new IntersectionObserver(
|
||||
function (entries) {
|
||||
entries.forEach(function (e) {
|
||||
if (!e.isIntersecting) return;
|
||||
e.target.classList.add("is-in");
|
||||
io.unobserve(e.target);
|
||||
});
|
||||
},
|
||||
{ rootMargin: "200px 0px 200px 0px", threshold: 0 },
|
||||
);
|
||||
els.forEach(function (el) { io.observe(el); });
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<style>
|
||||
.skip {
|
||||
position: absolute;
|
||||
left: var(--space-3);
|
||||
top: calc(var(--space-3) * -4);
|
||||
padding: var(--space-2) var(--space-3);
|
||||
background: var(--accent);
|
||||
color: var(--accent-ink);
|
||||
border-radius: var(--radius-sm);
|
||||
z-index: 10;
|
||||
transition: transform var(--dur-quick) var(--ease-out);
|
||||
}
|
||||
.skip:focus { transform: translateY(calc(var(--space-3) * 5)); }
|
||||
|
||||
/* 상단에 붙은 풀블리드 바 대신 떠 있는 헤더 */
|
||||
/* 상단에 붙은 바가 아니라 떠 있는 유리 알약 */
|
||||
/* 언어 전환 링크에는 lang 속성이 붙는다(스크린리더가 "English" 를
|
||||
영어로 읽어야 하므로 옳다). 그런데 [lang] 셀렉터가 타입 스케일을
|
||||
통째로 재정의해서 이 링크만 16px 이 됐다 — 스케일 밖 단계가 하나 생긴다.
|
||||
크기는 헤더에서 확정하고 링크는 상속만 받는다. */
|
||||
.site-header {
|
||||
font-size: var(--step-0);
|
||||
position: fixed;
|
||||
top: var(--space-3);
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 3;
|
||||
padding-inline: var(--gutter);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
min-height: var(--header-h);
|
||||
background: var(--surface);
|
||||
border-bottom: var(--line-width) solid var(--line);
|
||||
}
|
||||
|
||||
.header-inner {
|
||||
max-width: 1180px;
|
||||
margin-inline: auto;
|
||||
display: flex;
|
||||
min-height: var(--header-h);
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-4);
|
||||
padding: var(--space-2) var(--space-2) var(--space-2) var(--space-4);
|
||||
border-radius: var(--radius-frame);
|
||||
position: relative; /* 모바일 시트의 기준 — 알약 전체 폭으로 펼친다 */
|
||||
gap: var(--space-6);
|
||||
}
|
||||
|
||||
.wordmark {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
gap: var(--space-3);
|
||||
min-height: var(--control-h-sm);
|
||||
font-size: 1rem;
|
||||
font-weight: var(--weight-medium);
|
||||
letter-spacing: -0.01em;
|
||||
text-decoration: none;
|
||||
}
|
||||
.mark { color: var(--accent); flex: none; }
|
||||
/* ---- 섹션 내비 (데스크톱) --------------------------------- */
|
||||
.nav-main {
|
||||
display: none;
|
||||
gap: var(--space-1);
|
||||
margin-inline: auto;
|
||||
}
|
||||
.nav-main a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: var(--control-h-sm);
|
||||
padding-inline: var(--space-3);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--ink-muted);
|
||||
text-decoration: none;
|
||||
transition: color var(--dur-instant) var(--ease-out),
|
||||
background-color var(--dur-instant) var(--ease-out);
|
||||
}
|
||||
.nav-main a:hover { color: var(--ink); background: var(--glass-tint); }
|
||||
/* 바깥으로 나가는 링크는 섹션 앵커와 같은 무게로 두지 않는다 */
|
||||
.nav-repo { gap: var(--space-1); opacity: 0.72; }
|
||||
.nav-repo:hover { opacity: 1; }
|
||||
|
||||
.header-end {
|
||||
.wordmark-mark {
|
||||
flex: none;
|
||||
color: var(--accent);
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
stroke-width: 1.5;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
|
||||
.nav-primary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
flex: none;
|
||||
justify-content: center;
|
||||
gap: clamp(var(--space-3), 1.6vw, var(--space-6));
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* ---- 모바일 메뉴 ------------------------------------------
|
||||
details 를 쓴 이유는 JS 를 줄이기 위해서만이 아니다. 열림 상태,
|
||||
Esc, 포커스 순서, aria-expanded 를 브라우저가 이미 맞게 한다.
|
||||
직접 만들면 그 넷을 전부 다시 구현해야 하고 보통 틀린다.
|
||||
1′ 국소 레퍼런스(navbar.gallery → browserbase.com): 항목이 많은 사이트는
|
||||
내비 마크업을 아예 복제해 분리한다. 우리는 5행이라 복제하지 않고
|
||||
시트를 알약 전체 폭으로 펼친다 — 조각 상자보다 계기판답다. */
|
||||
.nav-mobile { position: static; }
|
||||
.nav-mobile > summary {
|
||||
list-style: none;
|
||||
.nav-primary a,
|
||||
.header-actions a {
|
||||
min-height: var(--control-h-sm);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: var(--control-h); /* 터치 대상은 44px — 36px 는 마우스용 */
|
||||
min-height: var(--control-h);
|
||||
padding: 0;
|
||||
color: var(--ink-muted);
|
||||
font-size: var(--step--1);
|
||||
font-weight: var(--weight-medium);
|
||||
line-height: 1.2;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.nav-mobile > summary::-webkit-details-marker { display: none; }
|
||||
.nav-mobile > summary::marker { content: ""; }
|
||||
.nav-mobile[open] > summary {
|
||||
background: var(--glass-tint);
|
||||
|
||||
.nav-primary a {
|
||||
position: relative;
|
||||
padding-inline: var(--space-2);
|
||||
}
|
||||
|
||||
.nav-primary a::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: var(--space-2);
|
||||
bottom: 0;
|
||||
left: var(--space-2);
|
||||
height: 1px;
|
||||
background: var(--ink);
|
||||
transform: scaleX(0);
|
||||
transform-origin: left;
|
||||
transition: transform var(--dur-quick) var(--ease-out);
|
||||
}
|
||||
|
||||
.nav-primary a:hover,
|
||||
.nav-primary a:focus-visible,
|
||||
.header-actions a:hover,
|
||||
.header-actions a:focus-visible {
|
||||
color: var(--ink);
|
||||
}
|
||||
.i-close { display: none; }
|
||||
.nav-mobile[open] .i-burger { display: none; }
|
||||
.nav-mobile[open] .i-close { display: block; }
|
||||
|
||||
.nav-sheet {
|
||||
/* 닫힌 details 의 자식에 display 를 주면 UA 숨김을 덮어써
|
||||
'보이지 않지만 레이아웃에 사는' 시트가 된다(320px 오버플로의 원인).
|
||||
닫힘을 명시적으로 다시 선언한다 — details 패턴의 필수 안전장치 */
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: calc(100% + var(--space-2));
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: var(--space-2);
|
||||
z-index: 4;
|
||||
border-radius: var(--radius-frame);
|
||||
/* 메뉴는 콘텐츠가 아니라 크롬이다. .glass 의 4.5% 잉크는 알약에는
|
||||
맞는 재질이지만 시트에는 가독성 버그다 — 뒤 글자가 비쳐 겹친다.
|
||||
배경만 채우고 테두리 광(::after rim)·그림자는 유리 재질 그대로 */
|
||||
background: color-mix(in oklab, var(--surface) 94%, white 6%);
|
||||
backdrop-filter: none;
|
||||
-webkit-backdrop-filter: none;
|
||||
.nav-primary a:hover::after,
|
||||
.nav-primary a:focus-visible::after {
|
||||
transform: scaleX(1);
|
||||
}
|
||||
.nav-mobile[open] .nav-sheet { display: grid; }
|
||||
.nav-sheet a {
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
min-height: calc(var(--control-h) + 4px);
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.header-archive {
|
||||
padding-inline: var(--space-3);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--ink);
|
||||
text-decoration: none;
|
||||
}
|
||||
.nav-sheet a:hover { background: var(--glass-tint-2); }
|
||||
.sheet-num {
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--mono-size);
|
||||
color: var(--accent);
|
||||
flex: none;
|
||||
}
|
||||
.sheet-lang { color: var(--ink-muted); }
|
||||
.sheet-lang:hover { color: var(--ink); }
|
||||
|
||||
/* 열림 연출 — transform/opacity 만(게이트 8) */
|
||||
.nav-mobile[open] .nav-sheet {
|
||||
animation: sheet-in 180ms var(--ease-out) both;
|
||||
}
|
||||
@keyframes sheet-in {
|
||||
from { opacity: 0; transform: translateY(-6px); }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.nav-mobile[open] .nav-sheet { animation: none; }
|
||||
border-left: var(--line-width) solid var(--line);
|
||||
}
|
||||
|
||||
@media (min-width: 900px) {
|
||||
.nav-main { display: flex; }
|
||||
.nav-mobile { display: none; }
|
||||
.header-lang,
|
||||
.header-repo {
|
||||
min-width: var(--control-h-sm);
|
||||
padding-inline: var(--space-2);
|
||||
border: var(--line-width) solid var(--line-strong);
|
||||
}
|
||||
|
||||
/* 480px 미만: 언어 버튼은 헤더에서 물러나고 메덥 행으로 간다.
|
||||
320px 실측에서 워드마크+English+버거가 알약을 7px 밀어냈다 */
|
||||
@media (max-width: 480px) {
|
||||
.lang-btn { display: none; }
|
||||
.header-inner { gap: var(--space-2); }
|
||||
.header-repo svg {
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
stroke-width: 1.5;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
@media (max-width: 64rem) {
|
||||
.header-inner { gap: var(--space-3); }
|
||||
.header-archive { display: none !important; }
|
||||
}
|
||||
|
||||
@media (max-width: 48rem) {
|
||||
.header-inner {
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
grid-template-rows: 3.5rem 3.25rem;
|
||||
gap: 0 var(--space-3);
|
||||
}
|
||||
|
||||
.wordmark { grid-column: 1; grid-row: 1; }
|
||||
.header-actions { grid-column: 2; grid-row: 1; }
|
||||
.nav-primary {
|
||||
grid-column: 1 / -1;
|
||||
grid-row: 2;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-1);
|
||||
border-top: var(--line-width) solid var(--line);
|
||||
}
|
||||
.nav-primary a {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
padding-inline: var(--space-1);
|
||||
text-align: center;
|
||||
}
|
||||
.header-repo { display: none !important; }
|
||||
}
|
||||
|
||||
@media (max-width: 23rem) {
|
||||
.wordmark { gap: var(--space-2); }
|
||||
.nav-primary a { font-size: 0.7rem; }
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue