Files
crossword/site/index.html
T
plparkandClaude Opus 5 96262fc690 Add British literature puzzle and a two-tab shell
index.html 은 왼쪽 탭(러시아 문학 / 영국 문학)과 게임을 띄우는 iframe 만 갖는
셸로 바꾸고, 게임 본체는 편별 페이지로 분리했다. 탭 상태는 해시(#russian,
#british)에 남으므로 링크로 바로 들어갈 수 있고, 같은 탭을 다시 눌러도
프레임을 다시 읽어들이지 않아 풀던 내용이 유지된다.

두 편이 쓰는 화면 양식과 게임 엔진은 site/assets/ 로 빼내 한 벌만 둔다.
엔진에는 셸 안에서 열렸을 때 여백을 줄이는 표시와, 첫 렌더에서 단서 목록을
스크롤하지 않게 하는 처리(프레임 안에서 머리말이 밀려 올라감)를 더했다.

영국 문학 편은 9x9 39칸 12항목. 가로/세로로 길이 2 이상 이어지는 모든 구간이
단서가 붙은 항목과 일대일로 대응하는지 확인했다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 16:32:30 +09:00

193 lines
6.4 KiB
HTML

<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>가로세로 낱말풀이</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Black+Han+Sans&family=Gothic+A1:wght@300;500;700;900&display=swap" rel="stylesheet">
<style>
:root{
--ink:#12100E;
--panel:#171410;
--paper:#E7E1D3;
--amber:#E8B23C;
--vermilion:#C6362A;
--slate:#8A8272;
--bone:#D8D2C4;
--line:#2A251F;
--ui:"Gothic A1","Apple SD Gothic Neo","Malgun Gothic","Noto Sans KR",sans-serif;
--display:"Black Han Sans","Gothic A1","Apple SD Gothic Neo",sans-serif;
--rail:238px;
}
*{box-sizing:border-box}
html,body{height:100%}
body{
margin:0;background:var(--ink);color:var(--bone);
font-family:var(--ui);font-weight:300;
display:grid;grid-template-columns:var(--rail) minmax(0,1fr);
-webkit-text-size-adjust:100%;
}
/* ---------- 왼쪽 탭 ---------- */
.rail{
background:var(--panel);border-right:1px solid var(--line);
display:flex;flex-direction:column;min-height:0;overflow:auto;
}
.brand{padding:24px 20px 20px;border-bottom:2px solid var(--vermilion)}
.brand .mark{
font-size:10px;letter-spacing:.3em;color:var(--slate);
font-weight:700;text-transform:uppercase;margin-bottom:9px;
}
.brand h1{
font-family:var(--display);font-weight:400;margin:0;
font-size:26px;line-height:1.06;color:var(--paper);letter-spacing:-.01em;
}
.brand h1 em{font-style:normal;color:var(--vermilion);display:block}
.nav{padding:16px 0;flex:1 0 auto}
.nav .label{
font-size:10px;font-weight:900;letter-spacing:.28em;color:var(--slate);
padding:0 20px 10px;
}
.nav a{
display:block;text-decoration:none;color:inherit;
padding:12px 18px 12px 17px;border-left:3px solid transparent;
border-bottom:1px solid #221E19;transition:background .12s linear;
}
.nav a .kr{display:block;font-size:15px;font-weight:700;color:var(--bone)}
.nav a .og{display:block;font-size:10.5px;letter-spacing:.14em;color:var(--slate);margin-top:3px}
.nav a:hover{background:#1F1B16}
.nav a:hover .kr{color:var(--amber)}
.nav a[aria-current="page"]{background:var(--vermilion);border-left-color:var(--amber)}
.nav a[aria-current="page"] .kr,
.nav a[aria-current="page"] .og{color:#fff}
.nav a:focus-visible{outline:2px solid var(--amber);outline-offset:-2px}
.rail-foot{padding:16px 20px 22px;border-top:1px solid var(--line)}
.rail-foot a{
font-size:11px;font-weight:700;letter-spacing:.08em;color:var(--slate);
text-decoration:none;border-bottom:1px solid transparent;
}
.rail-foot a:hover{color:var(--amber);border-bottom-color:var(--amber)}
/* ---------- 본 프레임 ---------- */
.stage{border:0;width:100%;height:100%;display:block;background:var(--ink)}
/* ---------- 좁은 화면: 위쪽 가로 탭 ---------- */
@media (max-width:820px){
body{grid-template-columns:1fr;grid-template-rows:auto minmax(0,1fr)}
.rail{
border-right:0;border-bottom:2px solid var(--vermilion);
flex-direction:row;align-items:stretch;overflow:visible;
}
.brand{
padding:12px 14px;border-bottom:0;border-right:1px solid var(--line);
display:flex;flex-direction:column;justify-content:center;flex:none;
}
.brand .mark{display:none}
.brand h1{font-size:17px}
.brand h1 em{display:inline;margin-left:6px}
.nav{display:flex;padding:0;flex:1 1 auto;min-width:0}
.nav .label{display:none}
.nav a{
flex:1 1 0;min-width:0;display:flex;flex-direction:column;justify-content:center;
border-bottom:0;border-left:0;border-top:3px solid transparent;
padding:10px 12px;text-align:center;
}
.nav a[aria-current="page"]{border-left-color:transparent;border-top-color:var(--amber)}
.nav a .kr{font-size:13.5px}
.nav a .og{display:none}
.rail-foot{display:none}
}
@media (prefers-reduced-motion:reduce){
*{animation:none !important;transition:none !important}
}
</style>
</head>
<body>
<nav class="rail">
<div class="brand">
<div class="mark">Crossword</div>
<h1>가로세로<em>낱말풀이</em></h1>
</div>
<div class="nav">
<div class="label">문학 편</div>
<a href="russian.html" target="stage" data-key="russian">
<span class="kr">러시아 문학</span>
<span class="og">Русская литература</span>
</a>
<a href="british.html" target="stage" data-key="british">
<span class="kr">영국 문학</span>
<span class="og">British literature</span>
</a>
</div>
<div class="rail-foot">
<a id="popout" href="russian.html" target="_blank" rel="noopener">새 창에서 열기 ↗</a>
</div>
</nav>
<!-- src 은 아래 스크립트가 해시를 보고 넣는다 (미리 넣으면 두 번 읽어들이게 된다) -->
<iframe class="stage" id="stage" name="stage" title="낱말풀이 게임판"></iframe>
<script>
(function(){
"use strict";
var PAGES = {
russian: {file:'russian.html', name:'러시아 문학'},
british: {file:'british.html', name:'영국 문학'}
};
var DEFAULT = 'russian';
var stage = document.getElementById('stage');
var popout = document.getElementById('popout');
var links = Array.prototype.slice.call(document.querySelectorAll('.nav a[data-key]'));
function keyFromHash(){
var k = decodeURIComponent(String(location.hash || '').replace(/^#\/?/, ''));
return PAGES[k] ? k : DEFAULT;
}
function apply(){
var key = keyFromHash();
var page = PAGES[key];
// 같은 페이지면 다시 읽어들이지 않는다 (풀던 내용 유지)
if (stage.getAttribute('src') !== page.file) stage.setAttribute('src', page.file);
links.forEach(function(a){
if (a.dataset.key === key) a.setAttribute('aria-current', 'page');
else a.removeAttribute('aria-current');
});
popout.setAttribute('href', page.file);
document.title = '가로세로 낱말풀이 · ' + page.name;
}
links.forEach(function(a){
a.addEventListener('click', function(ev){
if (ev.metaKey || ev.ctrlKey || ev.shiftKey || ev.button !== 0) return;
ev.preventDefault(); // 스크립트가 살아 있으면 해시로 처리
var k = a.dataset.key;
if (keyFromHash() === k && location.hash) return;
location.hash = '#' + k;
if (!location.hash) apply(); // 해시 변화가 없을 때를 위한 보험
});
});
window.addEventListener('hashchange', apply);
if (!location.hash) {
try { history.replaceState(null, '', '#' + DEFAULT); } catch(_) {}
}
apply();
})();
</script>
</body>
</html>