MEDIABROWSER 배포 사이트 + 공개 릴리스 저장소 전환 (DEPLOYMENT_PLAN §5-ter, BACKLOG 12차) — site/: 프로모션(라이트 글래스모피즘, CSS scroll-driven 패럴랙스+IO 등장, 모션·투명도 축소 존중, no-JS 백지 방지 html.js 게이트)·릴리스(플랫폼 그룹, OS 자동 감지 권장, 이전 버전 접힘, 빈 상태)·사용 안내(설치→첫 다운로드→관리→휴대폰 연결→FAQ) 3페이지. 히어로/가이드 이미지는 VD_DATA_DIR 격리 프로필로 띄운 실제 앱 다운로드 패널 캡처(밝기 보정). 릴리스 데이터는 CORS 회피를 위해 배포 시 fetch-releases.mjs 가 releases.json 으로 정적 생성(git 무시). 공개 저장소 Video-Downloader/releases 신설(비공개 소스 저장소는 자산 익명 다운로드 불가) — CI 업로드를 env RELEASE_REPO 로 전환, draft 익명 비노출이라 QA 후 Publish 흐름 유지. build/deploy-site.ps1(wrangler pages deploy) 추가, 라이브: https://mediabrowser-7r4.pages.dev — Playwright 로 3페이지 전수 스크린샷 검증(빈 상태+샘플 데이터 렌더 포함)

This commit is contained in:
Yun Chan 2026-08-18 00:14:39 +09:00
parent b536622606
commit c8b061959b
15 changed files with 923 additions and 2 deletions

1
site/assets/icons.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

35
site/fetch-releases.mjs Normal file
View file

@ -0,0 +1,35 @@
// 배포 전에 공개 릴리스 저장소의 릴리스 목록을 releases.json 으로 굽는다.
// (브라우저에서 Forgejo API 를 직접 호출하면 CORS 에 막히므로 빌드 시점에 정적 생성)
// 사용: node site/fetch-releases.mjs
import { writeFileSync } from "node:fs";
const API = "https://git.chanpaca.net/api/v1/repos/Video-Downloader/releases/releases?limit=50";
const res = await fetch(API, { headers: { Accept: "application/json" } });
if (!res.ok) throw new Error(`릴리스 API 실패: HTTP ${res.status}`);
const raw = await res.json();
const releases = raw
.filter((r) => !r.draft)
.sort((a, b) => new Date(b.published_at) - new Date(a.published_at))
.map((r) => ({
tag: r.tag_name,
name: r.name || r.tag_name,
published_at: r.published_at,
prerelease: r.prerelease,
notes: r.body || "",
assets: (r.assets || []).map((a) => ({
name: a.name,
size: a.size,
url: a.browser_download_url,
})),
}));
const out = {
generated_at: new Date().toISOString(),
repo_url: "https://git.chanpaca.net/Video-Downloader/releases",
releases,
};
writeFileSync(new URL("./releases.json", import.meta.url), JSON.stringify(out, null, 2));
console.log(`releases.json 생성: 릴리스 ${releases.length}건`);

149
site/guide.html Normal file
View file

@ -0,0 +1,149 @@
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>사용 안내 - MEDIABROWSER</title>
<meta name="description" content="MEDIABROWSER 설치부터 첫 다운로드, 다운로드 관리, 휴대폰 연결까지의 사용 안내.">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><rect width='100' height='100' rx='24' fill='%232050f0'/><path d='M50 22v36M36 44l14 14 14-14M28 74h44' stroke='white' stroke-width='9' stroke-linecap='round' stroke-linejoin='round' fill='none'/></svg>">
<link 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="styles.css">
</head>
<body>
<nav class="nav glass" aria-label="주 메뉴">
<a class="nav__brand" href="index.html">
<span class="nav__mark"><svg class="icon" aria-hidden="true"><use href="assets/icons.svg#download-simple"/></svg></span>
MEDIABROWSER
</a>
<div class="nav__links">
<a href="index.html#features">기능</a>
<a href="index.html#mobile">모바일</a>
<a href="guide.html" aria-current="page">사용 안내</a>
</div>
<a class="btn btn--primary btn--sm" href="releases.html">다운로드</a>
</nav>
<header class="pagehead">
<div class="orbs" aria-hidden="true">
<div class="orb orb--a"></div>
<div class="orb orb--b"></div>
</div>
<div class="wrap">
<h1>사용 안내</h1>
<p class="sub">설치부터 첫 다운로드, 휴대폰 연결까지. 처음이라면 위에서부터 순서대로 따라오세요.</p>
</div>
</header>
<div class="wrap guide-layout">
<aside class="guide-toc glass" aria-label="목차">
<a href="#install">1. 설치</a>
<a href="#first">2. 첫 다운로드</a>
<a href="#manage">3. 다운로드 관리</a>
<a href="#phone">4. 휴대폰 연결</a>
<a href="#faq">자주 묻는 질문</a>
</aside>
<main>
<section class="guide-step" id="install">
<h2><span class="featlist__icon"><svg class="icon" aria-hidden="true"><use href="assets/icons.svg#rocket-launch"/></svg></span>설치</h2>
<p><a href="releases.html" style="color:var(--accent);font-weight:700;text-decoration:none">다운로드 페이지</a>에서 플랫폼에 맞는 빌드를 받으세요.</p>
<ul>
<li><svg class="icon" aria-hidden="true"><use href="assets/icons.svg#windows-logo"/></svg>
<span><b>Windows</b>: 설치자(exe)를 실행하고 안내를 따르면 끝입니다. x64 와 arm64 를 제공합니다.</span></li>
<li><svg class="icon" aria-hidden="true"><use href="assets/icons.svg#apple-logo"/></svg>
<span><b>macOS 프리뷰</b>: tar.gz 를 풀고 앱을 실행합니다. 아직 서명이 없어 처음 실행 시 보안 확인이 필요할 수 있습니다.</span></li>
<li><svg class="icon" aria-hidden="true"><use href="assets/icons.svg#linux-logo"/></svg>
<span><b>Linux 프리뷰</b>: tar.gz 를 풀고 실행 파일을 실행합니다.</span></li>
<li><svg class="icon" aria-hidden="true"><use href="assets/icons.svg#android-logo"/></svg>
<span><b>Android</b>: APK 를 설치합니다. 데스크톱 앱과 페어링해 쓰는 컴패니언입니다.</span></li>
</ul>
<p>다운로드에 필요한 도구(yt-dlp, ffmpeg, deno)는 앱이 필요할 때 자동으로 설치합니다. 따로 준비할 것은 없습니다.</p>
</section>
<section class="guide-step" id="first">
<h2><span class="featlist__icon"><svg class="icon" aria-hidden="true"><use href="assets/icons.svg#cursor-click"/></svg></span>첫 다운로드</h2>
<p>MEDIABROWSER 는 브라우저입니다. 평소처럼 사이트에 들어가 영상을 재생하면 나머지는 앱이 합니다.</p>
<ul>
<li><svg class="icon" aria-hidden="true"><use href="assets/icons.svg#globe-hemisphere-west"/></svg>
<span>주소창에 사이트를 입력해 이동하고, 원하는 영상을 <b>재생</b>합니다.</span></li>
<li><svg class="icon" aria-hidden="true"><use href="assets/icons.svg#waveform"/></svg>
<span>재생이 시작되면 스트림(HLS, DASH, MP4)이 감지되어 오른쪽 <b>다운로드 패널</b>에 나타납니다.</span></li>
<li><svg class="icon" aria-hidden="true"><use href="assets/icons.svg#download-simple"/></svg>
<span>목록에서 <b>다운로드</b>를 누르면 로그인 상태 그대로 내려받기가 시작됩니다.</span></li>
</ul>
<figure class="guide-media glass">
<img src="assets/panel-downloads.png" alt="다운로드 패널의 빈 상태 화면. 영상을 재생하면 여기에 나타납니다 라는 안내 문구가 표시되어 있다." width="426" height="330">
<figcaption>감지 전의 다운로드 패널. 재생을 시작하면 이 목록이 채워집니다.</figcaption>
</figure>
</section>
<section class="guide-step" id="manage">
<h2><span class="featlist__icon"><svg class="icon" aria-hidden="true"><use href="assets/icons.svg#stack"/></svg></span>다운로드 관리</h2>
<p>패널의 탭으로 전체, 진행, 완료, 실패를 나눠 보고, 항목마다 속도와 남은 시간, 세그먼트 진행을 확인합니다.</p>
<ul>
<li><svg class="icon" aria-hidden="true"><use href="assets/icons.svg#check-circle"/></svg>
<span><b>모두 일시정지 / 모두 재개</b>로 한 번에 멈추고 다시 시작합니다. 항목별 일시정지도 됩니다.</span></li>
<li><svg class="icon" aria-hidden="true"><use href="assets/icons.svg#check-circle"/></svg>
<span><b>예약</b>을 걸면 지정한 시간부터 내려받습니다. 이미 받은 영상은 중복 확인으로 알려줍니다.</span></li>
<li><svg class="icon" aria-hidden="true"><use href="assets/icons.svg#folders"/></svg>
<span>받은 영상은 <b>라이브러리</b>에서 모아 보고, 재생목록과 즐겨찾기, 아카이브로 정리합니다.</span></li>
<li><svg class="icon" aria-hidden="true"><use href="assets/icons.svg#check-circle"/></svg>
<span>목록은 <b>CSV</b> 로 내보내 기록으로 남길 수 있습니다.</span></li>
</ul>
</section>
<section class="guide-step" id="phone">
<h2><span class="featlist__icon"><svg class="icon" aria-hidden="true"><use href="assets/icons.svg#device-mobile"/></svg></span>휴대폰 연결</h2>
<p>데스크톱 앱에 내장된 서버가 휴대폰과 라이브러리를 이어줍니다. 같은 네트워크에서 페어링 한 번이면 됩니다.</p>
<ul>
<li><svg class="icon" aria-hidden="true"><use href="assets/icons.svg#qr-code"/></svg>
<span>휴대폰에서 컴패니언 앱을 열고, 데스크톱 앱이 보여주는 <b>QR 코드</b>를 스캔합니다.</span></li>
<li><svg class="icon" aria-hidden="true"><use href="assets/icons.svg#play-circle"/></svg>
<span>페어링이 끝나면 라이브러리의 영상을 휴대폰에서 <b>바로 재생</b>하거나 <b>파일로 전송</b>받습니다.</span></li>
<li><svg class="icon" aria-hidden="true"><use href="assets/icons.svg#shield-check"/></svg>
<span>연결은 내 네트워크 안에서만 이루어집니다. 외부 서버를 거치지 않습니다.</span></li>
</ul>
</section>
<section class="guide-step" id="faq">
<h2><span class="featlist__icon"><svg class="icon" aria-hidden="true"><use href="assets/icons.svg#gear"/></svg></span>자주 묻는 질문</h2>
<div class="guide-faq">
<details>
<summary>ffmpeg 나 yt-dlp 를 따로 설치해야 하나요?</summary>
<p>아니요. 필요한 시점에 앱이 자동으로 내려받아 관리합니다. 설정에서 직접 지정한 경로가 있으면 그걸 우선합니다.</p>
</details>
<details>
<summary>받은 파일은 어디에 저장되나요?</summary>
<p>기본 저장 폴더에 저장되며, 설정에서 폴더를 바꿀 수 있습니다. 완료 목록에서 파일 위치를 바로 열 수 있습니다.</p>
</details>
<details>
<summary>다운로드가 중간에 끊기면 처음부터 다시 받나요?</summary>
<p>아니요. 세그먼트 단위로 이어받기를 지원해서 받다 만 지점부터 다시 진행합니다.</p>
</details>
<details>
<summary>업데이트는 어떻게 하나요?</summary>
<p>설정의 앱 업데이트 항목에서 새 버전을 확인할 수 있고, 다운로드 페이지에서 새 설치자를 받아 덮어 설치하면 됩니다.</p>
</details>
<details>
<summary>어떤 사이트를 지원하나요?</summary>
<p>표준 스트리밍 형식(HLS, DASH, MP4)을 쓰는 사이트에서 동작합니다. 콘텐츠의 이용 약관과 저작권을 지키는 범위에서 사용해 주세요.</p>
</details>
</div>
</section>
</main>
</div>
<footer>
<div class="wrap footer__row">
<span class="footer__brand">MEDIABROWSER</span>
<a href="releases.html">다운로드</a>
<a href="guide.html">사용 안내</a>
<a href="https://git.chanpaca.net/Video-Downloader/releases" rel="noopener">릴리스 저장소</a>
<span class="footer__copy">© 2026 MEDIABROWSER</span>
</div>
</footer>
<script src="site.js" defer></script>
</body>
</html>

156
site/index.html Normal file
View file

@ -0,0 +1,156 @@
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>MEDIABROWSER - 보던 영상, 그 자리에서 내 것으로</title>
<meta name="description" content="MEDIABROWSER 는 재생 중인 미디어를 감지해 내려받는 데스크톱 브라우저입니다. 받은 영상은 라이브러리로 관리하고 휴대폰으로 이어봅니다.">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><rect width='100' height='100' rx='24' fill='%232050f0'/><path d='M50 22v36M36 44l14 14 14-14M28 74h44' stroke='white' stroke-width='9' stroke-linecap='round' stroke-linejoin='round' fill='none'/></svg>">
<link 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="styles.css">
</head>
<body>
<nav class="nav glass" aria-label="주 메뉴">
<a class="nav__brand" href="index.html" aria-current="page">
<span class="nav__mark"><svg class="icon" aria-hidden="true"><use href="assets/icons.svg#download-simple"/></svg></span>
MEDIABROWSER
</a>
<div class="nav__links">
<a href="#features">기능</a>
<a href="#mobile">모바일</a>
<a href="guide.html">사용 안내</a>
</div>
<a class="btn btn--primary btn--sm" href="releases.html">다운로드</a>
</nav>
<header class="hero">
<div class="orbs" aria-hidden="true">
<div class="orb orb--a"></div>
<div class="orb orb--b"></div>
<div class="orb orb--c"></div>
</div>
<div class="wrap hero__grid">
<div>
<h1>보던 영상,<br>그 자리에서 내 것으로.</h1>
<p class="sub">MEDIABROWSER 는 재생 중인 미디어를 감지해 내려받는 브라우저입니다.
받은 영상은 라이브러리로 관리하고, 휴대폰으로 이어봅니다.</p>
<div class="hero__cta">
<a class="btn btn--primary" href="releases.html">다운로드</a>
<a class="btn btn--ghost" href="guide.html">사용 안내</a>
</div>
</div>
<div class="hero__visual reveal">
<figure class="hero__panel glass">
<img src="assets/panel-downloads.png" alt="MEDIABROWSER 다운로드 패널. 전체·진행·완료·실패 탭과 모두 일시정지, 모두 재개, CSV 버튼이 있고, 영상을 재생하면 여기에 나타납니다 라는 안내가 보인다." width="426" height="330">
<figcaption class="hero__caption">앱의 다운로드 패널. 재생을 감지하면 이 목록에 자동으로 쌓입니다.</figcaption>
</figure>
</div>
</div>
</header>
<section id="features" class="section-pad">
<div class="wrap">
<h2 class="reveal" style="font-size:clamp(28px,3.6vw,40px)">주소를 붙여넣는 게 아니라,<br>그냥 봅니다.</h2>
<div class="bento">
<div class="bento__cell bento__cell--wide reveal">
<div class="bento__wave"><svg class="icon" aria-hidden="true"><use href="assets/icons.svg#waveform"/></svg></div>
<div>
<div class="bento__chipline" aria-label="지원 형식">
<span class="chip">HLS</span><span class="chip">DASH</span><span class="chip">MP4</span>
</div>
<h3>재생하면 감지됩니다</h3>
<p>내장 브라우저에서 영상이 재생되는 순간 스트림을 인식해 다운로드 목록에 올립니다.
별도 확장이나 주소 복사가 필요 없습니다.</p>
</div>
</div>
<div class="bento__cell glass reveal">
<h3>로그인 그대로</h3>
<p>탭의 쿠키와 Referer 를 그대로 물려받아 내려받습니다. 브라우저에서 보이던 영상은 그대로 받아집니다.</p>
</div>
<div class="bento__cell glass reveal">
<h3>끊겨도 이어서</h3>
<p>병렬 분할 다운로드와 이어받기, 그리고 ffmpeg 리먹스까지 자동으로 처리합니다. AES-128 스트림도 지원합니다.</p>
</div>
</div>
</div>
</section>
<section class="section-pad" style="background:linear-gradient(180deg,transparent,var(--bg-deep) 30%,var(--bg-deep) 70%,transparent)">
<div class="wrap">
<h2 class="reveal" style="font-size:clamp(28px,3.6vw,40px)">다운로드는 관리가 절반입니다.</h2>
<div class="featlist">
<div class="featlist__item reveal">
<div class="featlist__icon"><svg class="icon" aria-hidden="true"><use href="assets/icons.svg#stack"/></svg></div>
<div><h3>속도와 세그먼트가 보입니다</h3><p>항목마다 속도, 남은 시간, 세그먼트 맵을 실시간으로 표시합니다.</p></div>
</div>
<div class="featlist__item reveal">
<div class="featlist__icon"><svg class="icon" aria-hidden="true"><use href="assets/icons.svg#play-circle"/></svg></div>
<div><h3>전체 일시정지와 재개</h3><p>한 번에 멈추고 한 번에 다시 시작합니다. 예약 시간을 걸어둘 수도 있습니다.</p></div>
</div>
<div class="featlist__item reveal">
<div class="featlist__icon"><svg class="icon" aria-hidden="true"><use href="assets/icons.svg#check-circle"/></svg></div>
<div><h3>중복은 미리 알려줍니다</h3><p>이미 받은 영상을 다시 받으려 하면 내려받기 전에 확인해 줍니다.</p></div>
</div>
<div class="featlist__item reveal">
<div class="featlist__icon"><svg class="icon" aria-hidden="true"><use href="assets/icons.svg#folders"/></svg></div>
<div><h3>라이브러리와 아카이브</h3><p>받은 영상을 모아 보고, 정리하고, 재생목록과 즐겨찾기로 관리합니다.</p></div>
</div>
</div>
</div>
</section>
<section id="mobile" class="section-pad">
<div class="wrap mobile__grid">
<div class="mobile__stage reveal" aria-hidden="true">
<div class="mobile__card mobile__card--a glass">
<svg class="icon"><use href="assets/icons.svg#qr-code"/></svg>
<div><strong>QR 페어링</strong><span>화면의 코드를 한 번 스캔</span></div>
</div>
<div class="mobile__card mobile__card--b glass">
<svg class="icon"><use href="assets/icons.svg#device-mobile"/></svg>
<div><strong>스트리밍</strong><span>같은 네트워크에서 바로 재생</span></div>
</div>
<div class="mobile__card mobile__card--c glass">
<svg class="icon"><use href="assets/icons.svg#download-simple"/></svg>
<div><strong>전송</strong><span>파일을 휴대폰에 저장</span></div>
</div>
</div>
<div>
<h2 class="reveal" style="font-size:clamp(28px,3.6vw,40px)">받은 영상,<br>휴대폰에서 이어보기.</h2>
<p class="sub reveal" style="margin-top:20px">데스크톱 앱에 내장된 서버가 라이브러리를 휴대폰으로 스트리밍하거나 전송합니다.
QR 코드 한 번으로 페어링하고, Android 컴패니언 앱으로 어디서든 이어봅니다. iOS 는 준비 중입니다.</p>
</div>
</div>
</section>
<section class="section-pad" style="padding-top:0">
<div class="wrap">
<div class="band glass reveal">
<h2>Windows 에서 시작하세요.</h2>
<p class="sub">Windows 는 설치자로 바로 시작할 수 있습니다.
macOS 와 Linux 는 새 셸의 프리뷰 빌드, Android 는 컴패니언 앱을 제공합니다.</p>
<div class="band__oses">
<div class="band__os"><svg class="icon" aria-hidden="true"><use href="assets/icons.svg#windows-logo"/></svg>Windows</div>
<div class="band__os"><svg class="icon" aria-hidden="true"><use href="assets/icons.svg#apple-logo"/></svg>macOS 프리뷰</div>
<div class="band__os"><svg class="icon" aria-hidden="true"><use href="assets/icons.svg#linux-logo"/></svg>Linux 프리뷰</div>
<div class="band__os"><svg class="icon" aria-hidden="true"><use href="assets/icons.svg#android-logo"/></svg>Android</div>
</div>
<a class="btn btn--primary" href="releases.html">다운로드</a>
</div>
</div>
</section>
<footer>
<div class="wrap footer__row">
<span class="footer__brand">MEDIABROWSER</span>
<a href="releases.html">다운로드</a>
<a href="guide.html">사용 안내</a>
<a href="https://git.chanpaca.net/Video-Downloader/releases" rel="noopener">릴리스 저장소</a>
<span class="footer__copy">© 2026 MEDIABROWSER</span>
</div>
</footer>
<script src="site.js" defer></script>
</body>
</html>

76
site/releases.html Normal file
View file

@ -0,0 +1,76 @@
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>다운로드 - MEDIABROWSER</title>
<meta name="description" content="MEDIABROWSER 릴리스 다운로드. Windows 설치자, macOS·Linux 프리뷰 빌드, Android 컴패니언 APK.">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><rect width='100' height='100' rx='24' fill='%232050f0'/><path d='M50 22v36M36 44l14 14 14-14M28 74h44' stroke='white' stroke-width='9' stroke-linecap='round' stroke-linejoin='round' fill='none'/></svg>">
<link 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="styles.css">
</head>
<body>
<nav class="nav glass" aria-label="주 메뉴">
<a class="nav__brand" href="index.html">
<span class="nav__mark"><svg class="icon" aria-hidden="true"><use href="assets/icons.svg#download-simple"/></svg></span>
MEDIABROWSER
</a>
<div class="nav__links">
<a href="index.html#features">기능</a>
<a href="index.html#mobile">모바일</a>
<a href="guide.html">사용 안내</a>
</div>
<a class="btn btn--primary btn--sm" href="releases.html" aria-current="page">다운로드</a>
</nav>
<header class="pagehead">
<div class="orbs" aria-hidden="true">
<div class="orb orb--a"></div>
<div class="orb orb--b"></div>
</div>
<div class="wrap">
<h1>다운로드</h1>
<p class="sub">사용하는 플랫폼에 맞는 빌드를 받으세요. 설치가 끝나면
<a href="guide.html" style="color:var(--accent);font-weight:700;text-decoration:none">사용 안내</a>에서 첫 다운로드까지 3분이면 충분합니다.</p>
</div>
</header>
<div class="wrap rel-layout">
<aside class="rel-rail">
<div class="rel-rail__box glass">
<h3>플랫폼</h3>
<a class="rel-rail__os" href="#windows"><svg class="icon" aria-hidden="true"><use href="assets/icons.svg#windows-logo"/></svg>Windows</a>
<a class="rel-rail__os" href="#macos"><svg class="icon" aria-hidden="true"><use href="assets/icons.svg#apple-logo"/></svg>macOS 프리뷰</a>
<a class="rel-rail__os" href="#linux"><svg class="icon" aria-hidden="true"><use href="assets/icons.svg#linux-logo"/></svg>Linux 프리뷰</a>
<a class="rel-rail__os" href="#android"><svg class="icon" aria-hidden="true"><use href="assets/icons.svg#android-logo"/></svg>Android</a>
</div>
<div class="rel-rail__box glass">
<p class="rel-rail__note">Windows 는 설치자(exe)를 실행하면 됩니다.
macOS 와 Linux 는 tar.gz 를 풀어 실행하는 프리뷰 빌드입니다.
전체 버전 기록은 <a href="https://git.chanpaca.net/Video-Downloader/releases" rel="noopener">릴리스 저장소</a>에 있습니다.</p>
</div>
</aside>
<main id="release-root" aria-live="polite">
<div class="rel-empty glass" id="release-loading">
<svg class="icon" aria-hidden="true"><use href="assets/icons.svg#download-simple"/></svg>
<h2>릴리스 목록을 불러오는 중</h2>
<p>잠시만 기다려 주세요.</p>
</div>
</main>
</div>
<footer>
<div class="wrap footer__row">
<span class="footer__brand">MEDIABROWSER</span>
<a href="releases.html">다운로드</a>
<a href="guide.html">사용 안내</a>
<a href="https://git.chanpaca.net/Video-Downloader/releases" rel="noopener">릴리스 저장소</a>
<span class="footer__copy">© 2026 MEDIABROWSER</span>
</div>
</footer>
<script src="site.js" defer></script>
</body>
</html>

131
site/site.js Normal file
View file

@ -0,0 +1,131 @@
// MEDIABROWSER 사이트 공용 스크립트
// 1) 스크롤 등장(reveal) — IntersectionObserver, 모션 축소 설정이면 즉시 표시
// 2) releases.html — releases.json(배포 시 생성)을 읽어 릴리스 목록 렌더
(function initReveal() {
const items = document.querySelectorAll(".reveal");
if (!items.length) return;
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches || !("IntersectionObserver" in window)) {
return; // .js 게이트를 켜지 않으면 CSS 기본값(표시 상태) 그대로
}
document.documentElement.classList.add("js");
const io = new IntersectionObserver((entries) => {
for (const e of entries) {
if (e.isIntersecting) { e.target.classList.add("in"); io.unobserve(e.target); }
}
}, { threshold: 0.15, rootMargin: "0px 0px -8% 0px" });
items.forEach((el) => io.observe(el));
})();
// ── 릴리스 렌더 ──────────────────────────────────────────────
const PLATFORMS = [
{ id: "windows", label: "Windows", icon: "windows-logo", sub: "설치자를 실행하면 됩니다",
match: (n) => /setup.*\.exe$/i.test(n) || /win/i.test(n) },
{ id: "macos", label: "macOS 프리뷰", icon: "apple-logo", sub: "tar.gz 를 풀어 실행하는 프리뷰 빌드",
match: (n) => /osx|darwin|mac/i.test(n) },
{ id: "linux", label: "Linux 프리뷰", icon: "linux-logo", sub: "tar.gz 를 풀어 실행하는 프리뷰 빌드",
match: (n) => /linux/i.test(n) },
{ id: "android", label: "Android", icon: "android-logo", sub: "컴패니언 앱 APK",
match: (n) => /\.apk$/i.test(n) },
];
function detectPreferredAsset(name) {
const ua = navigator.userAgent;
if (/Windows/i.test(ua)) return /setup-x64\.exe$/i.test(name);
if (/Android/i.test(ua)) return /\.apk$/i.test(name);
if (/Mac/i.test(ua)) return /osx-arm64/i.test(name);
if (/Linux/i.test(ua)) return /linux-x64/i.test(name);
return false;
}
function fmtSize(bytes) {
if (!bytes) return "";
const mb = bytes / 1048576;
return mb >= 1024 ? (mb / 1024).toFixed(1) + " GB" : mb.toFixed(1) + " MB";
}
function fmtDate(iso) {
if (!iso) return "";
const d = new Date(iso);
return `${d.getFullYear()}년 ${d.getMonth() + 1}월 ${d.getDate()}일`;
}
function esc(s) {
return String(s).replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[c]));
}
function assetRow(a, recommended) {
return `<div class="dl-asset">
<div><div class="dl-asset__name">${esc(a.name)}</div><div class="dl-asset__size">${fmtSize(a.size)}</div></div>
<a class="btn btn--primary btn--sm ${recommended ? "dl-reco" : ""}" href="${esc(a.url)}" title="${recommended ? "이 기기에 권장" : ""}">받기</a>
</div>`;
}
function groupsHtml(assets) {
const used = new Set();
const groups = PLATFORMS.map((p) => {
const list = assets.filter((a) => !used.has(a.name) && p.match(a.name));
list.forEach((a) => used.add(a.name));
if (!list.length) return "";
return `<div class="dl-group" id="${p.id}">
<div class="dl-group__head"><svg class="icon" aria-hidden="true"><use href="assets/icons.svg#${p.icon}"/></svg>${p.label}</div>
<div class="dl-group__sub">${p.sub}</div>
${list.map((a) => assetRow(a, detectPreferredAsset(a.name))).join("")}
</div>`;
}).filter(Boolean);
const rest = assets.filter((a) => !used.has(a.name));
if (rest.length) {
groups.push(`<div class="dl-group"><div class="dl-group__head">기타</div><div class="dl-group__sub">그 밖의 파일</div>
${rest.map((a) => assetRow(a, false)).join("")}</div>`);
}
return `<div class="dl-groups">${groups.join("")}</div>`;
}
function releaseCard(r, featured) {
const pill = r.prerelease ? `<span class="rel-pill rel-pill--pre">프리뷰</span>` : `<span class="rel-pill">안정판</span>`;
const meta = `<div class="rel-featured__meta">
<span class="rel-featured__tag">${esc(r.name || r.tag)}</span>${pill}
<span class="rel-featured__date">${fmtDate(r.published_at)}</span>
</div>
${r.notes ? `<p class="rel-featured__notes">${esc(r.notes)}</p>` : ""}
${groupsHtml(r.assets)}`;
if (featured) return `<article class="rel-featured glass">${meta}</article>`;
return `<details><summary><span class="rel-featured__tag" style="font-size:19px">${esc(r.name || r.tag)}</span>${pill}
<span class="rel-featured__date">${fmtDate(r.published_at)}</span></summary>
<div class="rel-old__body">${r.notes ? `<p class="rel-featured__notes">${esc(r.notes)}</p>` : ""}${groupsHtml(r.assets)}</div>
</details>`;
}
function emptyState() {
return `<div class="rel-empty glass">
<svg class="icon" aria-hidden="true"><use href="assets/icons.svg#rocket-launch"/></svg>
<h2>첫 공개 릴리스를 준비하고 있어요</h2>
<p>빌드 파이프라인은 이미 돌아가고 있습니다. 곧 이 자리에서 설치자를 받을 수 있습니다.
먼저 <a href="guide.html" style="color:var(--accent);font-weight:700;text-decoration:none">사용 안내</a>를 둘러보세요.</p>
</div>`;
}
(async function initReleases() {
const root = document.getElementById("release-root");
if (!root) return;
try {
const res = await fetch("releases.json", { cache: "no-cache" });
if (!res.ok) throw new Error("HTTP " + res.status);
const data = await res.json();
const releases = (data.releases || []);
if (!releases.length) { root.innerHTML = emptyState(); return; }
const [latest, ...older] = releases;
let html = releaseCard(latest, true);
if (older.length) {
html += `<div class="rel-old"><h2 style="font-size:20px;margin-top:14px">이전 버전</h2>
${older.map((r) => releaseCard(r, false)).join("")}</div>`;
}
root.innerHTML = html;
} catch (err) {
root.innerHTML = `<div class="rel-empty glass">
<h2>목록을 불러오지 못했어요</h2>
<p>잠시 후 새로고침해 주세요. 급하다면
<a href="https://git.chanpaca.net/Video-Downloader/releases" style="color:var(--accent);font-weight:700;text-decoration:none" rel="noopener">릴리스 저장소</a>에서 직접 받을 수 있습니다.</p>
</div>`;
}
})();

289
site/styles.css Normal file
View file

@ -0,0 +1,289 @@
/* MEDIABROWSER 사이트 공용 디자인 시스템
라이트 단일 테마(사용자 지정) · 글래스모피즘 · 절제된 스크롤 모션
반경 체계: 버튼=필, 카드/패널=20px, 칩/입력=12px */
:root {
--bg: #f5f7fc;
--bg-deep: #e9edf8;
--ink: #171a23;
--muted: #5b6273;
--line: rgba(23, 26, 35, 0.08);
--accent: #2050f0;
--accent-deep: #1740c8;
--accent-soft: rgba(32, 80, 240, 0.09);
--glass: rgba(255, 255, 255, 0.58);
--glass-strong: rgba(255, 255, 255, 0.78);
--glass-brd: rgba(255, 255, 255, 0.72);
--shadow-soft: 0 24px 60px -24px rgba(32, 60, 160, 0.18);
--shadow-lift: 0 32px 80px -28px rgba(32, 60, 160, 0.28);
--r-card: 20px;
--r-chip: 12px;
--nav-h: 68px;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html { scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }
body {
font-family: "Wanted Sans Variable", "Wanted Sans", system-ui, "Apple SD Gothic Neo", "Malgun Gothic", sans-serif;
background: var(--bg);
color: var(--ink);
line-height: 1.6;
-webkit-font-smoothing: antialiased;
overflow-x: hidden;
}
::selection { background: var(--accent); color: #fff; }
.icon { width: 1.25em; height: 1.25em; fill: currentColor; flex: none; vertical-align: -0.25em; }
/* ── 배경 오브(패럴랙스 레이어) ─────────────────────────────── */
.orbs { position: absolute; inset: 0; overflow: hidden; pointer-events: none; z-index: -1; }
.orb { position: absolute; border-radius: 50%; filter: blur(70px); opacity: 0.55; }
.orb--a { width: 560px; height: 560px; left: -140px; top: -120px;
background: radial-gradient(circle at 35% 35%, #c9d6ff, #eef1fb 70%); }
.orb--b { width: 480px; height: 480px; right: -120px; top: 160px;
background: radial-gradient(circle at 40% 40%, #d8e4ff, #f0f3fc 70%); }
.orb--c { width: 380px; height: 380px; left: 34%; top: 560px;
background: radial-gradient(circle at 50% 40%, #dfe9ff, #f4f6fd 72%); opacity: 0.45; }
/* 스크롤 패럴랙스: 지원 브라우저에서만, 모션 축소 설정이면 정지 */
@media (prefers-reduced-motion: no-preference) {
@supports (animation-timeline: scroll()) {
.orb { animation: orb-drift linear both; animation-timeline: scroll(); }
.orb--a { --drift: 180px; }
.orb--b { --drift: -240px; }
.orb--c { --drift: 120px; }
}
}
@keyframes orb-drift { to { transform: translateY(var(--drift, 0px)); } }
/* ── 글래스 재질 ────────────────────────────────────────────── */
.glass {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.42), rgba(255, 255, 255, 0.16)), var(--glass);
backdrop-filter: blur(18px) saturate(160%);
-webkit-backdrop-filter: blur(18px) saturate(160%);
border: 1px solid var(--glass-brd);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.65), var(--shadow-soft);
}
@media (prefers-reduced-transparency: reduce) {
.glass { background: rgba(255, 255, 255, 0.96); backdrop-filter: none; -webkit-backdrop-filter: none; }
}
/* ── 내비게이션 ─────────────────────────────────────────────── */
.nav {
position: fixed; top: 14px; left: 50%; transform: translateX(-50%);
width: min(1120px, calc(100% - 32px)); height: var(--nav-h);
display: flex; align-items: center; gap: 28px; padding: 0 22px;
border-radius: 999px; z-index: 50;
}
.nav__brand {
font-weight: 800; letter-spacing: 0.04em; font-size: 15px;
color: var(--ink); text-decoration: none; display: flex; align-items: center; gap: 9px;
}
.nav__mark {
width: 30px; height: 30px; border-radius: 9px; display: grid; place-items: center;
background: linear-gradient(135deg, var(--accent), #4d79ff); color: #fff;
}
.nav__mark .icon { width: 16px; height: 16px; }
.nav__links { display: flex; gap: 22px; margin-left: auto; }
.nav__links a { color: var(--muted); text-decoration: none; font-size: 14.5px; font-weight: 600; }
.nav__links a:hover, .nav__links a[aria-current="page"] { color: var(--ink); }
.btn {
display: inline-flex; align-items: center; justify-content: center; gap: 8px;
border-radius: 999px; font-weight: 700; text-decoration: none; cursor: pointer;
border: 0; white-space: nowrap;
transition: transform 0.18s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.18s, background 0.18s;
}
.btn:active { transform: translateY(1px) scale(0.98); }
.btn--primary {
background: var(--accent); color: #fff; padding: 12px 22px; font-size: 15px;
box-shadow: 0 10px 24px -10px rgba(32, 80, 240, 0.55);
}
.btn--primary:hover { background: var(--accent-deep); }
.btn--ghost {
background: rgba(255, 255, 255, 0.6); color: var(--ink); padding: 12px 22px; font-size: 15px;
border: 1px solid var(--line);
}
.btn--ghost:hover { background: #fff; }
.btn--sm { padding: 9px 16px; font-size: 14px; }
/* ── 공통 레이아웃 ──────────────────────────────────────────── */
.wrap { width: min(1120px, calc(100% - 40px)); margin: 0 auto; }
section { position: relative; }
.section-pad { padding: 108px 0; }
h1, h2, h3 { line-height: 1.22; letter-spacing: -0.02em; font-weight: 800; }
.sub { color: var(--muted); font-size: 17px; max-width: 58ch; }
/* 스크롤 등장 — JS 가 html 에 .js 를 붙였을 때만 숨김(no-JS 에서도 내용이 보이는 점진적 향상) */
html.js .reveal { opacity: 0; transform: translateY(22px); transition: opacity 0.7s cubic-bezier(0.16, 1, 0.3, 1), transform 0.7s cubic-bezier(0.16, 1, 0.3, 1); }
html.js .reveal.in { opacity: 1; transform: none; }
@media (prefers-reduced-motion: reduce) { html.js .reveal { opacity: 1; transform: none; transition: none; } }
/* ── 히어로 ─────────────────────────────────────────────────── */
.hero { padding: calc(var(--nav-h) + 78px) 0 96px; min-height: 86dvh; display: flex; align-items: center; }
.hero__grid { display: grid; grid-template-columns: 7fr 5fr; gap: 56px; align-items: center; }
.hero h1 { font-size: clamp(38px, 5.2vw, 62px); }
.hero .sub { margin-top: 22px; font-size: 18px; }
.hero__cta { display: flex; gap: 14px; margin-top: 34px; }
.hero__visual { position: relative; }
.hero__panel {
border-radius: var(--r-card); padding: 14px;
transform: rotate(1.8deg);
}
.hero__panel img {
display: block; width: 100%; border-radius: 12px;
box-shadow: 0 18px 44px -18px rgba(10, 16, 40, 0.45);
}
.hero__caption { margin-top: 12px; font-size: 13.5px; color: var(--muted); text-align: center; }
/* ── 벤토 ───────────────────────────────────────────────────── */
.bento { display: grid; grid-template-columns: 3fr 2fr; grid-template-rows: auto auto; gap: 20px; margin-top: 52px; }
.bento__cell { border-radius: var(--r-card); padding: 34px; position: relative; overflow: hidden; }
.bento__cell--wide { grid-row: span 2; display: flex; flex-direction: column; justify-content: flex-end; min-height: 380px;
background: linear-gradient(160deg, #1b2440 0%, #22346e 55%, #2050f0 130%); color: #fff; }
.bento__cell--wide h3, .bento__cell--wide p { color: #fff; }
.bento__cell--wide p { opacity: 0.82; }
.bento__wave { position: absolute; top: 30px; left: 34px; opacity: 0.9; }
.bento__wave .icon { width: 44px; height: 44px; color: #9db6ff; }
.bento__cell h3 { font-size: 21px; margin-bottom: 10px; }
.bento__cell p { color: var(--muted); font-size: 15.5px; }
.bento__chipline { display: flex; gap: 8px; margin-bottom: 18px; flex-wrap: wrap; }
.chip {
display: inline-flex; align-items: center; gap: 6px; padding: 6px 12px;
border-radius: 999px; font-size: 13px; font-weight: 700;
background: rgba(255, 255, 255, 0.14); border: 1px solid rgba(255, 255, 255, 0.22); color: #dfe8ff;
}
/* ── 기능 그리드(카드 없는 정리) ────────────────────────────── */
.featlist { display: grid; grid-template-columns: repeat(2, 1fr); gap: 40px 64px; margin-top: 56px; }
.featlist__item { display: flex; gap: 16px; align-items: flex-start; }
.featlist__icon {
width: 42px; height: 42px; flex: none; border-radius: var(--r-chip); display: grid; place-items: center;
background: var(--accent-soft); color: var(--accent);
}
.featlist__icon .icon { width: 21px; height: 21px; }
.featlist__item h3 { font-size: 17px; margin-bottom: 4px; }
.featlist__item p { color: var(--muted); font-size: 15px; }
/* ── 모바일 섹션 ────────────────────────────────────────────── */
.mobile__grid { display: grid; grid-template-columns: 5fr 7fr; gap: 56px; align-items: center; }
.mobile__stage { position: relative; min-height: 320px; }
.mobile__card { position: absolute; border-radius: var(--r-card); display: flex; align-items: center; gap: 14px; padding: 20px 24px; }
.mobile__card .icon { width: 30px; height: 30px; color: var(--accent); }
.mobile__card strong { display: block; font-size: 15.5px; }
.mobile__card span { font-size: 13.5px; color: var(--muted); }
.mobile__card--a { top: 8%; left: 4%; transform: rotate(-2.4deg); }
.mobile__card--b { top: 40%; right: 6%; transform: rotate(1.6deg); }
.mobile__card--c { bottom: 4%; left: 14%; transform: rotate(-1deg); }
/* ── 플랫폼 밴드 ────────────────────────────────────────────── */
.band { border-radius: 28px; padding: 64px 56px; text-align: center; }
.band h2 { font-size: clamp(28px, 3.6vw, 40px); }
.band .sub { margin: 16px auto 0; }
.band__oses { display: flex; gap: 34px; justify-content: center; margin: 36px 0 40px; color: var(--muted); }
.band__os { display: flex; flex-direction: column; align-items: center; gap: 8px; font-size: 13.5px; font-weight: 600; }
.band__os .icon { width: 34px; height: 34px; color: var(--ink); }
.band__os--soon { opacity: 0.55; }
/* ── 푸터 ───────────────────────────────────────────────────── */
footer { padding: 48px 0 60px; }
.footer__row { display: flex; align-items: center; gap: 26px; flex-wrap: wrap; border-top: 1px solid var(--line); padding-top: 32px; }
.footer__row a { color: var(--muted); text-decoration: none; font-size: 14px; font-weight: 600; }
.footer__row a:hover { color: var(--ink); }
.footer__brand { font-weight: 800; letter-spacing: 0.04em; font-size: 14px; }
.footer__copy { margin-left: auto; color: var(--muted); font-size: 13.5px; }
/* ── 릴리스 페이지 ──────────────────────────────────────────── */
.pagehead { padding: calc(var(--nav-h) + 74px) 0 30px; }
.pagehead h1 { font-size: clamp(32px, 4.2vw, 48px); }
.pagehead .sub { margin-top: 16px; }
.rel-layout { display: grid; grid-template-columns: 280px 1fr; gap: 44px; align-items: start; padding-bottom: 110px; }
.rel-rail { position: sticky; top: calc(var(--nav-h) + 30px); display: flex; flex-direction: column; gap: 10px; }
.rel-rail__box { border-radius: var(--r-card); padding: 22px; }
.rel-rail__box h3 { font-size: 15px; margin-bottom: 12px; }
.rel-rail__os { display: flex; align-items: center; gap: 10px; padding: 9px 10px; border-radius: var(--r-chip);
color: var(--ink); text-decoration: none; font-size: 14.5px; font-weight: 600; }
.rel-rail__os:hover { background: rgba(255, 255, 255, 0.75); }
.rel-rail__os .icon { width: 19px; height: 19px; color: var(--muted); }
.rel-rail__note { font-size: 13.5px; color: var(--muted); line-height: 1.55; }
.rel-rail__note a { color: var(--accent); font-weight: 700; text-decoration: none; }
.rel-featured { border-radius: 24px; padding: 40px; }
.rel-featured__meta { display: flex; align-items: baseline; gap: 14px; flex-wrap: wrap; }
.rel-featured__tag { font-size: 30px; font-weight: 800; letter-spacing: -0.02em; }
.rel-featured__date { color: var(--muted); font-size: 14.5px; }
.rel-pill { display: inline-flex; padding: 4px 12px; border-radius: 999px; font-size: 12.5px; font-weight: 800;
background: var(--accent-soft); color: var(--accent); }
.rel-pill--pre { background: rgba(214, 122, 20, 0.12); color: #a35a00; }
.rel-featured__notes { margin-top: 18px; color: var(--muted); font-size: 15px; white-space: pre-line; max-width: 70ch; }
.dl-groups { display: grid; grid-template-columns: repeat(2, 1fr); gap: 18px; margin-top: 30px; }
.dl-group { border-radius: var(--r-card); background: rgba(255, 255, 255, 0.66); border: 1px solid var(--line); padding: 22px; }
.dl-group__head { display: flex; align-items: center; gap: 10px; margin-bottom: 6px; font-weight: 800; font-size: 16px; }
.dl-group__head .icon { width: 21px; height: 21px; color: var(--ink); }
.dl-group__sub { font-size: 13.5px; color: var(--muted); margin-bottom: 14px; }
.dl-asset { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 9px 0; }
.dl-asset + .dl-asset { border-top: 1px solid var(--line); }
.dl-asset__name { font-size: 14px; font-weight: 600; overflow-wrap: anywhere; }
.dl-asset__size { color: var(--muted); font-size: 12.5px; }
.dl-asset .btn { flex: none; }
.dl-reco { outline: 2px solid var(--accent); outline-offset: 2px; }
.rel-old { margin-top: 30px; display: flex; flex-direction: column; gap: 14px; }
.rel-old details { border-radius: var(--r-card); border: 1px solid var(--line); background: rgba(255, 255, 255, 0.55); }
.rel-old summary { cursor: pointer; padding: 18px 22px; font-weight: 700; display: flex; align-items: center; gap: 12px; list-style: none; }
.rel-old summary::-webkit-details-marker { display: none; }
.rel-old summary .rel-featured__date { margin-left: auto; }
.rel-old__body { padding: 0 22px 20px; }
.rel-empty { border-radius: 24px; padding: 72px 40px; text-align: center; }
.rel-empty .icon { width: 44px; height: 44px; color: var(--accent); }
.rel-empty h2 { font-size: 24px; margin: 18px 0 10px; }
.rel-empty p { color: var(--muted); max-width: 48ch; margin: 0 auto; }
/* ── 가이드 페이지 ──────────────────────────────────────────── */
.guide-layout { display: grid; grid-template-columns: 240px 1fr; gap: 52px; align-items: start; padding-bottom: 110px; }
.guide-toc { position: sticky; top: calc(var(--nav-h) + 30px); border-radius: var(--r-card); padding: 22px; }
.guide-toc a { display: block; padding: 7px 10px; border-radius: 10px; color: var(--muted); text-decoration: none; font-size: 14.5px; font-weight: 600; }
.guide-toc a:hover { color: var(--ink); background: rgba(255, 255, 255, 0.75); }
.guide-step { padding: 44px 0; border-top: 1px solid var(--line); }
.guide-step:first-of-type { border-top: 0; padding-top: 10px; }
.guide-step h2 { font-size: 24px; display: flex; align-items: center; gap: 13px; }
.guide-step h2 .featlist__icon { width: 38px; height: 38px; }
.guide-step > p { color: var(--muted); margin-top: 14px; max-width: 66ch; }
.guide-step ul { margin: 16px 0 0 2px; list-style: none; display: flex; flex-direction: column; gap: 9px; }
.guide-step li { display: flex; gap: 10px; color: var(--ink); font-size: 15px; }
.guide-step li .icon { width: 18px; height: 18px; color: var(--accent); margin-top: 4px; }
.guide-step li span b { font-weight: 750; }
.guide-media { margin-top: 24px; border-radius: var(--r-card); padding: 12px; max-width: 460px; }
.guide-media img { display: block; width: 100%; border-radius: 12px; }
.guide-media figcaption { font-size: 13px; color: var(--muted); padding: 10px 6px 2px; }
.guide-faq { margin-top: 18px; display: flex; flex-direction: column; gap: 10px; }
.guide-faq details { border: 1px solid var(--line); border-radius: var(--r-chip); background: rgba(255, 255, 255, 0.55); }
.guide-faq summary { cursor: pointer; padding: 15px 18px; font-weight: 700; font-size: 15px; list-style: none; }
.guide-faq summary::-webkit-details-marker { display: none; }
.guide-faq p { padding: 0 18px 16px; color: var(--muted); font-size: 14.5px; }
/* ── 모바일 반응형 ──────────────────────────────────────────── */
@media (max-width: 860px) {
.nav { gap: 14px; }
.nav__links { display: none; }
.hero { min-height: auto; padding-bottom: 64px; }
.hero__grid, .mobile__grid { grid-template-columns: 1fr; gap: 40px; }
.bento { grid-template-columns: 1fr; }
.bento__cell--wide { min-height: 300px; }
.featlist { grid-template-columns: 1fr; gap: 28px; }
.section-pad { padding: 72px 0; }
.band { padding: 48px 24px; }
.band__oses { gap: 22px; flex-wrap: wrap; }
.rel-layout, .guide-layout { grid-template-columns: 1fr; gap: 28px; }
.rel-rail, .guide-toc { position: static; }
.dl-groups { grid-template-columns: 1fr; }
.mobile__stage { min-height: 380px; }
.rel-featured { padding: 26px; }
}