posimai-root/posimai-sc/index.html

1009 lines
61 KiB
HTML
Raw 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.

<!DOCTYPE html>
<html lang="ja" data-app-id="posimai-sc">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="
default-src 'self';
script-src 'self' https://cdn.jsdelivr.net https://unpkg.com 'unsafe-inline' 'unsafe-eval';
style-src 'self' https://fonts.googleapis.com 'unsafe-inline';
font-src https://fonts.gstatic.com;
connect-src 'self' https://api.soar-enrich.com;
img-src 'self' data:;
worker-src 'self';
manifest-src 'self';
frame-ancestors 'none';
base-uri 'self';
form-action 'self'
">
<meta name="robots" content="noindex, nofollow">
<script>
(function(){
var t=localStorage.getItem('posimai-sc-theme')||'system';
var dark=t==='dark'||(t==='system'&&matchMedia('(prefers-color-scheme:dark)').matches);
document.documentElement.setAttribute('data-theme',dark?'dark':'light');
})();
</script>
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover, interactive-widget=resizes-content">
<meta name="description" content="情報処理安全確保支援士試験 概念学習・理解度チェックアプリ">
<meta name="color-scheme" content="dark light">
<meta name="theme-color" content="#0C1221" media="(prefers-color-scheme: dark)">
<meta name="theme-color" content="#EEF2FF" media="(prefers-color-scheme: light)">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="SC">
<link rel="manifest" href="/manifest.json">
<link rel="icon" type="image/png" href="/logo.png">
<link rel="apple-touch-icon" href="/logo.png">
<title>SC — 支援士</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=Geist:wght@300;400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
:root,[data-theme="dark"]{
--accent:#818CF8;
--accent-dim:rgba(129,140,248,0.10);
--accent-border:rgba(129,140,248,0.22);
--bg:#0C1221;
--surface:rgba(15,22,40,0.82);
--surface2:rgba(26,34,53,0.92);
--border:rgba(255,255,255,0.07);
--border2:rgba(255,255,255,0.04);
--text:#F1F5F9;
--text2:#94A3B8;
--text3:#64748B;
--ok:#4ADE80;
--warn:#FB923C;
--err:#F87171;
--radius:12px;
--radius-sm:8px;
--safe-top:env(safe-area-inset-top, 0px);
--safe-right:env(safe-area-inset-right, 0px);
--safe-bottom:env(safe-area-inset-bottom, 0px);
--safe-left:env(safe-area-inset-left, 0px);
}
[data-theme="light"]{
--accent:#6366F1;
--accent-dim:rgba(99,102,241,0.08);
--accent-border:rgba(99,102,241,0.22);
--bg:#EEF2FF;
--surface:rgba(255,255,255,0.88);
--surface2:rgba(238,242,255,0.92);
--border:rgba(0,0,0,0.07);
--border2:rgba(0,0,0,0.04);
--text:#0F172A;
--text2:#475569;
--text3:#94A3B8;
--ok:#16A34A;
--warn:#D97706;
--err:#DC2626;
}
html{height:100%}
html,body{background:var(--bg);color:var(--text);font-family:'Geist',sans-serif;overflow:hidden}
body{min-height:100%;min-height:-webkit-fill-available;height:100%}
.aurora{position:fixed;inset:0;pointer-events:none;overflow:hidden;z-index:0}
.aurora-blob{position:absolute;border-radius:50%;will-change:transform}
.aurora-blob-1{width:800px;height:550px;background:radial-gradient(ellipse,rgba(129,140,248,0.14) 0%,transparent 68%);top:-120px;right:-100px;filter:blur(90px);animation:ab1 20s ease-in-out infinite alternate}
.aurora-blob-2{width:650px;height:480px;background:radial-gradient(ellipse,rgba(167,139,250,0.10) 0%,transparent 68%);bottom:-100px;left:-80px;filter:blur(100px);animation:ab2 26s ease-in-out infinite alternate}
.aurora-blob-3{width:440px;height:360px;background:radial-gradient(ellipse,rgba(99,102,241,0.07) 0%,transparent 68%);top:45%;right:28%;filter:blur(80px);animation:ab3 16s ease-in-out infinite alternate}
@keyframes ab1{from{transform:translate(0,0) scale(1)}to{transform:translate(-60px,80px) scale(1.1)}}
@keyframes ab2{from{transform:translate(0,0) scale(1)}to{transform:translate(80px,-50px) scale(1.07)}}
@keyframes ab3{from{transform:translate(0,0) scale(1)}to{transform:translate(-50px,60px) scale(0.93)}}
[data-theme="light"] .aurora-blob{opacity:0.35}
#app{position:relative;z-index:1;box-sizing:border-box;height:100vh;height:100dvh;max-height:100dvh;display:flex;flex-direction:column;min-height:0;padding:var(--safe-top) var(--safe-right) var(--safe-bottom) var(--safe-left)}
/* Header */
header{display:flex;align-items:center;justify-content:space-between;padding:0 16px;height:52px;border-bottom:1px solid var(--border);background:rgba(12,18,33,0.75);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);flex-shrink:0}
[data-theme="light"] header{background:rgba(238,242,255,0.88)}
.brand{display:flex;align-items:center;gap:10px}
.brand-home{display:flex;align-items:center;gap:8px;cursor:pointer;padding:4px 6px;border-radius:var(--radius-sm);transition:background .15s;margin-left:-6px;background:none;border:none;font-family:inherit}
.brand-home:hover{background:var(--accent-dim)}
.brand-dot{width:8px;height:8px;border-radius:50%;background:var(--accent);box-shadow:0 0 8px var(--accent);flex-shrink:0}
.brand-title{font-size:14px;font-weight:600;letter-spacing:-0.01em;color:var(--text)}
.brand-sub{font-size:11px;color:var(--text3)}
.icon-btn{background:none;border:1px solid var(--border);border-radius:var(--radius-sm);padding:6px;cursor:pointer;color:var(--text2);display:flex;align-items:center;justify-content:center;transition:color .15s,border-color .15s;font-family:inherit}
.icon-btn:hover{color:var(--accent);border-color:var(--accent-border)}
/* Body */
#body{flex:1;min-height:0;display:flex;overflow:hidden}
/* Sidebar */
#sidebar{width:240px;flex-shrink:0;min-height:0;border-right:1px solid var(--border);overflow-y:auto;-webkit-overflow-scrolling:touch;background:rgba(12,18,33,0.5);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);display:flex;flex-direction:column}
[data-theme="light"] #sidebar{background:rgba(238,242,255,0.6)}
.sidebar-search{padding:10px 12px;border-bottom:1px solid var(--border)}
.search-wrap{position:relative}
.search-icon{position:absolute;left:8px;top:50%;transform:translateY(-50%);color:var(--text3);pointer-events:none}
.search-input{width:100%;background:var(--surface2);border:1px solid var(--border);border-radius:var(--radius-sm);padding:7px 10px 7px 30px;font-size:12px;color:var(--text);outline:none;font-family:'Geist',sans-serif;transition:border-color .15s}
.search-input:focus{border-color:var(--accent-border)}
.search-input::placeholder{color:var(--text3)}
.progress-wrap{padding:10px 12px;border-bottom:1px solid var(--border)}
.progress-label{font-size:10px;color:var(--text3);letter-spacing:.07em;text-transform:uppercase;margin-bottom:6px;display:flex;justify-content:space-between}
.progress-track{height:3px;background:var(--surface2);border-radius:2px;overflow:hidden}
.progress-fill{height:100%;background:var(--accent);border-radius:2px;transition:width .4s}
.sidebar-cat{padding:8px 12px 3px;font-size:10px;font-weight:600;color:var(--text3);letter-spacing:.10em;text-transform:uppercase;display:flex;align-items:center;gap:6px}
.sidebar-cat-line{flex:1;height:1px;background:var(--border)}
.sidebar-item{display:flex;align-items:center;gap:8px;padding:7px 12px;cursor:pointer;font-size:12px;color:var(--text2);transition:background .12s,color .12s;border-left:2px solid transparent;user-select:none}
.sidebar-item:hover{background:var(--accent-dim);color:var(--text)}
.sidebar-item.active{background:var(--accent-dim);color:var(--accent);border-left-color:var(--accent)}
.item-num{font-family:'JetBrains Mono',monospace;font-size:9px;font-weight:500;background:var(--surface2);border:1px solid var(--border2);border-radius:4px;padding:2px 5px;min-width:26px;text-align:center;flex-shrink:0;color:var(--text3);transition:background .15s,color .15s}
.sidebar-item.active .item-num{background:var(--accent-dim);border-color:var(--accent-border);color:var(--accent)}
.sidebar-item.done .item-num{background:rgba(74,222,128,.12);border-color:rgba(74,222,128,.25);color:var(--ok)}
.item-title{flex:1;line-height:1.3}
.item-check{width:12px;height:12px;flex-shrink:0;color:var(--ok);opacity:0;transition:opacity .15s}
.sidebar-item.done .item-check{opacity:1}
/* Main */
#main{flex:1;min-height:0;overflow-y:auto;-webkit-overflow-scrolling:touch;overscroll-behavior:contain;padding:24px 28px;touch-action:pan-y}
/* Home */
.home-hero{text-align:center;padding:36px 0 28px;max-width:500px;margin:0 auto}
.home-hero h1{font-size:26px;font-weight:300;letter-spacing:-.02em;line-height:1.2}
.home-hero h1 span{color:var(--accent)}
.home-hero p{font-size:13px;color:var(--text2);margin-top:8px;line-height:1.7}
.home-hint{font-size:12px;color:var(--text3);margin-top:16px;line-height:1.65;max-width:460px;margin-inline:auto;padding:10px 14px;text-align:left;border-radius:var(--radius-sm);border:1px solid var(--border);background:var(--surface2)}
.home-hint code{font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--text2);background:var(--surface);padding:2px 6px;border-radius:4px;border:1px solid var(--border)}
.home-hint strong{color:var(--text2);font-weight:600}
.home-hint kbd{font-family:'JetBrains Mono',monospace;font-size:10px;padding:2px 5px;border:1px solid var(--border);border-radius:4px;background:var(--surface)}
.home-tools{display:flex;justify-content:center;flex-wrap:wrap;gap:10px;margin-bottom:16px}
.sync-card{max-width:540px;margin:0 auto 16px;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:12px 14px}
.sync-title{font-size:11px;font-weight:600;color:var(--text3);letter-spacing:.08em;text-transform:uppercase;display:flex;align-items:center;gap:6px;margin-bottom:8px}
.sync-text{font-size:12px;color:var(--text2);line-height:1.6}
.sync-row{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-top:8px}
.sync-status{font-size:11px;color:var(--text3)}
.sync-error{font-size:11px;color:var(--err);margin-top:4px}
.sync-input{flex:1;min-width:220px;background:var(--surface2);border:1px solid var(--border);border-radius:var(--radius-sm);padding:7px 10px;font-size:12px;color:var(--text);outline:none;font-family:'JetBrains Mono',monospace}
.sync-input:focus{border-color:var(--accent-border)}
.glossary-intro{font-size:12px;color:var(--text2);line-height:1.65;margin-bottom:12px}
.glossary-search-wrap{position:relative;margin-bottom:8px}
.glossary-search-icon{position:absolute;left:8px;top:50%;transform:translateY(-50%);color:var(--text3);pointer-events:none}
.glossary-meta-line{font-size:10px;color:var(--text3);margin-bottom:8px}
.glossary-list{max-height:min(58vh,520px);overflow-y:auto;-webkit-overflow-scrolling:touch;border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--surface2);padding:4px 12px;margin-top:4px}
.glossary-row{padding:10px 0;border-bottom:1px solid var(--border);cursor:pointer;text-align:left;transition:background .12s}
.glossary-row:last-child{border-bottom:none}
.glossary-row:hover{background:var(--accent-dim)}
.glossary-term{font-weight:600;font-size:13px;color:var(--text);margin-bottom:4px}
.glossary-hint{font-size:12px;color:var(--text2);line-height:1.55;margin-bottom:6px;word-break:break-word}
.glossary-unit{display:flex;align-items:center;gap:6px;font-size:11px;color:var(--text3)}
.glossary-num{font-family:'JetBrains Mono',monospace;font-size:10px;padding:1px 5px;border-radius:4px;border:1px solid var(--border2);background:var(--surface)}
.kbd-tiny{font-family:'JetBrains Mono',monospace;font-size:10px;padding:1px 5px;border:1px solid var(--border);border-radius:4px;background:var(--surface);color:var(--text2)}
.exam-setup-text{font-size:12px;color:var(--text2);line-height:1.65;margin-bottom:4px}
.drill-row-link{cursor:pointer}
.stats-row{display:flex;gap:10px;justify-content:center;margin-top:20px;flex-wrap:wrap}
.stat-card{background:var(--surface);backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);border:1px solid var(--border);border-radius:var(--radius);padding:14px 22px;text-align:center}
.stat-val{font-family:'JetBrains Mono',monospace;font-size:22px;font-weight:500;color:var(--accent)}
.stat-lbl{font-size:10px;color:var(--text3);margin-top:3px;letter-spacing:.05em;text-transform:uppercase}
.home-cats{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:28px;max-width:540px;margin-inline:auto}
.home-cat-card{background:var(--surface);backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);border:1px solid var(--border);border-radius:var(--radius);padding:18px;cursor:pointer;transition:border-color .2s,background .2s;position:relative;overflow:hidden}
.home-cat-card::before{content:'';position:absolute;inset:0;border-radius:var(--radius);background:linear-gradient(145deg,rgba(255,255,255,0.025) 0%,transparent 55%);pointer-events:none}
.home-cat-card:hover{border-color:var(--accent-border);background:var(--accent-dim)}
.home-cat-icon{width:30px;height:30px;border-radius:8px;background:var(--accent-dim);border:1px solid var(--accent-border);display:flex;align-items:center;justify-content:center;margin-bottom:10px;color:var(--accent)}
.home-cat-title{font-size:13px;font-weight:600}
.home-cat-sub{font-size:11px;color:var(--text3);margin-top:2px}
.home-cat-progress{height:3px;background:var(--border);border-radius:2px;margin-top:10px;overflow:hidden}
.home-cat-bar{height:100%;background:var(--accent);border-radius:2px;transition:width .5s}
/* Card */
.card{background:var(--surface);backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);border:1px solid var(--border);border-radius:var(--radius);padding:20px;margin-bottom:14px;position:relative;overflow:hidden}
.card::before{content:'';position:absolute;inset:0;border-radius:var(--radius);background:linear-gradient(145deg,rgba(255,255,255,0.025) 0%,transparent 55%);pointer-events:none}
.card-title{font-size:11px;font-weight:600;color:var(--text3);letter-spacing:.09em;text-transform:uppercase;margin-bottom:14px;display:flex;align-items:center;gap:6px}
.card-title svg{width:13px;height:13px;color:var(--accent)}
/* Unit header */
.unit-header{margin-bottom:18px}
.unit-meta{min-width:0}
.unit-cat-badge{font-size:10px;font-weight:600;letter-spacing:.07em;text-transform:uppercase;color:var(--accent);background:var(--accent-dim);border:1px solid var(--accent-border);border-radius:20px;padding:3px 9px;display:inline-flex;align-items:center}
.unit-title{font-size:19px;font-weight:600;letter-spacing:-.01em;line-height:1.3}
/* Concept text */
.concept-text{font-size:13px;line-height:1.85;color:var(--text2)}
.concept-text strong{color:var(--text);font-weight:600}
.concept-text .hl{color:var(--accent);font-weight:500}
.concept-text p{margin-bottom:10px}
.concept-text p:last-child{margin-bottom:0}
.formula-box .inline-em{font-weight:600;color:var(--text);font-family:'JetBrains Mono',monospace}
.formula-box{background:var(--surface2);border:1px solid var(--accent-border);border-radius:var(--radius-sm);padding:12px 16px;margin:12px 0;font-family:'JetBrains Mono',monospace;font-size:11.5px;color:var(--accent);line-height:1.8}
.formula-label{font-size:10px;color:var(--text3);font-family:'Geist',sans-serif;font-weight:600;letter-spacing:.07em;text-transform:uppercase;margin-bottom:6px}
/* Keypoints */
.key-list{list-style:none;display:flex;flex-direction:column;gap:8px}
.key-item{display:flex;gap:10px;font-size:13px;color:var(--text2);line-height:1.65}
.key-dot{width:5px;height:5px;border-radius:50%;background:var(--accent);flex-shrink:0;margin-top:8px}
/* Buttons */
.btn-sm{display:flex;align-items:center;gap:4px;padding:7px 13px;border-radius:var(--radius-sm);font-size:12px;font-weight:500;cursor:pointer;border:1px solid var(--border);background:var(--surface);color:var(--text2);transition:all .15s;font-family:'Geist',sans-serif}
.btn-sm:hover:not(:disabled){color:var(--accent);border-color:var(--accent-border)}
.btn-sm:disabled{opacity:.4;cursor:default}
.btn-accent{background:rgba(129,140,248,0.12);color:var(--accent);border-color:var(--accent-border)}
.btn-accent:hover:not(:disabled){background:rgba(129,140,248,0.20)}
[data-theme="light"] .btn-accent{background:rgba(99,102,241,0.10);color:var(--accent)}
/* Quiz */
.quiz-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:14px}
.quiz-title-label{font-size:11px;font-weight:600;color:var(--text3);letter-spacing:.09em;text-transform:uppercase;display:flex;align-items:center;gap:6px}
.quiz-score-lbl{font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--text3)}
.q-card{background:var(--surface2);border:1px solid var(--border);border-radius:var(--radius);padding:14px;margin-bottom:10px;transition:border-color .2s,background .2s}
.q-card.correct{border-color:rgba(74,222,128,.35);background:rgba(74,222,128,.04)}
.q-card.wrong{border-color:rgba(248,113,113,.30);background:rgba(248,113,113,.03)}
.q-num{font-size:10px;font-weight:600;color:var(--text3);letter-spacing:.08em;text-transform:uppercase;margin-bottom:7px;display:flex;align-items:center;gap:5px}
.q-text{font-size:13px;color:var(--text);line-height:1.7;margin-bottom:12px;word-break:break-word;overflow-wrap:anywhere;line-break:auto}
.q-choices{display:flex;flex-direction:column;gap:6px}
.q-choice{display:flex;align-items:flex-start;gap:9px;padding:8px 12px;border-radius:var(--radius-sm);border:1px solid var(--border);background:var(--surface);cursor:pointer;font-size:12px;color:var(--text2);transition:all .15s;text-align:left;font-family:'Geist',sans-serif;line-height:1.5;width:100%;word-break:break-word;overflow-wrap:anywhere;line-break:auto}
.q-choice:hover:not(:disabled){border-color:var(--accent-border);color:var(--text);background:var(--accent-dim)}
.q-choice:disabled{cursor:default}
.q-choice.sel-ok{border-color:rgba(74,222,128,.45);background:rgba(74,222,128,.09);color:var(--ok)}
.q-choice.sel-ng{border-color:rgba(248,113,113,.40);background:rgba(248,113,113,.07);color:var(--err)}
.q-choice.rev-ok{border-color:rgba(74,222,128,.35);background:rgba(74,222,128,.05);color:var(--ok)}
.choice-key{font-family:'JetBrains Mono',monospace;font-size:10px;font-weight:600;min-width:16px;flex-shrink:0;margin-top:1px}
.q-exp{margin-top:10px;padding:9px 11px;background:rgba(129,140,248,.05);border:1px solid var(--accent-border);border-radius:var(--radius-sm);font-size:12px;color:var(--text2);line-height:1.65}
.q-exp strong{color:var(--accent)}
.quiz-result{background:var(--surface2);border:1px solid var(--border);border-radius:var(--radius);padding:22px;text-align:center;margin-top:14px}
.result-score{font-family:'JetBrains Mono',monospace;font-size:38px;font-weight:500;color:var(--accent);letter-spacing:-.02em}
.result-lbl{font-size:11px;color:var(--text3);margin-top:4px;letter-spacing:.05em;text-transform:uppercase}
.result-msg{font-size:13px;color:var(--text2);margin-top:10px;line-height:1.5}
.result-actions{margin-top:16px;display:flex;gap:8px;justify-content:center;flex-wrap:wrap}
/* Unit nav */
.unit-nav{display:flex;gap:8px;flex-wrap:wrap;margin-top:4px;padding-bottom:24px}
/* Mobile */
.sidebar-toggle{display:none}
.overlay-mob{display:none;position:fixed;inset:0;z-index:40;background:rgba(0,0,0,.5)}
.overlay-mob.open{display:block}
@media(max-width:680px){
#sidebar{position:fixed;left:0;top:calc(52px + var(--safe-top));bottom:var(--safe-bottom);z-index:50;transform:translateX(-100%);transition:transform .25s cubic-bezier(.2,.9,.2,1);width:260px}
#sidebar.open{transform:translateX(0)}
.sidebar-toggle{display:flex}
#main{padding:10px}
.home-hero{padding:28px 0 20px}
.card{padding:15px;margin-bottom:12px}
.unit-header{margin-bottom:14px}
.unit-nav{padding-bottom:20px}
.home-cats{grid-template-columns:1fr}
.q-card{padding:12px;margin-bottom:8px}
.q-text{font-size:12.5px;word-break:break-word;overflow-wrap:anywhere;line-break:auto}
.q-choice{padding:8px 10px;font-size:11.5px;line-height:1.55;word-break:break-word;overflow-wrap:anywhere;line-break:auto}
.q-exp{font-size:11.5px}
}
::-webkit-scrollbar{width:4px}
::-webkit-scrollbar-track{background:transparent}
::-webkit-scrollbar-thumb{background:var(--border);border-radius:2px}
/* Badges */
.badge-row{display:flex;align-items:center;gap:6px;margin-bottom:8px;flex-wrap:wrap}
.badge{display:inline-flex;align-items:center;gap:3px;font-size:10px;font-weight:600;padding:3px 8px;border-radius:20px;letter-spacing:.04em}
.badge-high{background:rgba(248,113,113,.12);border:1px solid rgba(248,113,113,.3);color:var(--err)}
.badge-mid{background:rgba(251,146,60,.10);border:1px solid rgba(251,146,60,.28);color:var(--warn)}
.badge-base{background:rgba(129,140,248,.08);border:1px solid rgba(129,140,248,.2);color:var(--accent)}
.badge-diff{background:var(--surface2);border:1px solid var(--border);color:var(--text3)}
.score-chip{font-family:'JetBrains Mono',monospace;font-size:9px;font-weight:600;padding:1px 5px;border-radius:4px;background:rgba(74,222,128,.12);border:1px solid rgba(74,222,128,.25);color:var(--ok);margin-left:auto;flex-shrink:0}
.score-chip.partial{background:rgba(251,146,60,.10);border-color:rgba(251,146,60,.28);color:var(--warn)}
.score-chip.zero{background:var(--surface2);border-color:var(--border);color:var(--text3)}
.weak-dot{font-size:9px;font-weight:700;padding:1px 5px;border-radius:4px;background:rgba(248,113,113,.12);border:1px solid rgba(248,113,113,.3);color:var(--err);margin-left:auto;flex-shrink:0}
/* Today's review */
.today-section{margin-bottom:20px}
.today-label{font-size:10px;font-weight:600;color:var(--text3);letter-spacing:.09em;text-transform:uppercase;display:flex;align-items:center;gap:6px;margin-bottom:10px}
.today-label svg{width:13px;height:13px;color:var(--accent)}
.today-card{display:flex;align-items:center;gap:10px;padding:12px 16px;background:var(--surface);backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);border:1px solid var(--border);border-radius:var(--radius);cursor:pointer;transition:border-color .15s,background .15s;margin-bottom:8px}
.today-card:hover{border-color:var(--accent-border);background:var(--accent-dim)}
.today-card:last-child{margin-bottom:0}
.today-tag{font-size:9px;font-weight:700;padding:2px 7px;border-radius:20px;letter-spacing:.04em;flex-shrink:0}
.today-tag-weak{background:rgba(248,113,113,.12);border:1px solid rgba(248,113,113,.3);color:var(--err)}
.today-tag-new{background:rgba(129,140,248,.08);border:1px solid rgba(129,140,248,.2);color:var(--accent)}
.today-tag-review{background:rgba(251,146,60,.10);border:1px solid rgba(251,146,60,.28);color:var(--warn)}
.today-num{font-family:'JetBrains Mono',monospace;font-size:10px;color:var(--text3);flex-shrink:0;min-width:30px}
.today-title{font-size:13px;font-weight:500;color:var(--text);flex:1}
.today-arrow{color:var(--text3);flex-shrink:0}
/* Exam tips card */
.exam-tips-text{font-size:13px;line-height:1.8;color:var(--text2)}
.exam-tips-text strong{color:var(--text);font-weight:600}
.exam-tips-text .hl{color:var(--accent);font-weight:500}
.tips-point{display:flex;gap:8px;margin-bottom:8px;font-size:12px;color:var(--text2);line-height:1.65}
.tips-point:last-child{margin-bottom:0}
.tips-icon{flex-shrink:0;width:16px;height:16px;border-radius:50%;background:rgba(248,113,113,.15);border:1px solid rgba(248,113,113,.3);display:flex;align-items:center;justify-content:center;margin-top:2px;font-size:9px;color:var(--err);font-weight:700}
/* Visual diagrams */
.viz-flow{display:flex;align-items:center;gap:6px;margin:14px 0;flex-wrap:wrap;justify-content:center}
.vf-node{background:var(--surface2);border:1px solid var(--border);border-radius:var(--radius-sm);padding:10px 14px;text-align:center;font-size:12px;font-weight:600;color:var(--text);min-width:68px}
.vf-sub{font-size:10px;color:var(--text3);font-weight:400;margin-top:3px}
.vf-hl{border-color:var(--accent-border) !important;background:var(--accent-dim) !important;color:var(--accent) !important}
.vf-hl .vf-sub{color:rgba(129,140,248,.65)}
.vf-arrow{color:var(--text3);font-size:15px;flex-shrink:0}
/* Security-specific diagrams */
.viz-cia{display:flex;gap:8px;margin:14px 0;flex-wrap:wrap}
.cia-card{flex:1;min-width:100px;border-radius:var(--radius-sm);padding:12px;text-align:center;border:1px solid}
.cia-c{background:rgba(129,140,248,.08);border-color:rgba(129,140,248,.25)}.cia-c .cia-name{color:var(--accent)}
.cia-i{background:rgba(74,222,128,.07);border-color:rgba(74,222,128,.22)}.cia-i .cia-name{color:var(--ok)}
.cia-a{background:rgba(251,146,60,.07);border-color:rgba(251,146,60,.22)}.cia-a .cia-name{color:var(--warn)}
.cia-name{font-size:13px;font-weight:700;margin-bottom:4px}
.cia-en{font-size:10px;color:var(--text3);margin-bottom:6px}
.cia-desc{font-size:11px;color:var(--text2);line-height:1.6}
.viz-layers{display:flex;flex-direction:column;gap:4px;margin:14px 0}
.layer-item{padding:8px 14px;border-radius:var(--radius-sm);font-size:12px;font-weight:600;border:1px solid;display:flex;align-items:center;justify-content:space-between}
.layer-sub{font-size:10px;font-weight:400;color:var(--text3)}
.layer-1{background:rgba(129,140,248,.08);border-color:rgba(129,140,248,.2);color:var(--accent)}
.layer-2{background:rgba(74,222,128,.07);border-color:rgba(74,222,128,.2);color:var(--ok)}
.layer-3{background:rgba(251,146,60,.07);border-color:rgba(251,146,60,.2);color:var(--warn)}
.layer-4{background:rgba(248,113,113,.07);border-color:rgba(248,113,113,.2);color:var(--err)}
/* Weak drill mode */
.drill-unit-lbl{font-size:10px;font-weight:600;color:var(--text3);letter-spacing:.07em;text-transform:uppercase;margin-bottom:10px;padding-bottom:8px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between}
.drill-prog-wrap{margin-bottom:14px}
.drill-prog-label{font-size:10px;color:var(--text3);display:flex;justify-content:space-between;margin-bottom:4px}
.drill-prog-bar{height:3px;background:var(--surface2);border-radius:2px;overflow:hidden}
.drill-prog-fill{height:100%;background:var(--accent);border-radius:2px;transition:width .3s}
.drill-result{text-align:center;padding:20px 0}
.drill-result-pct{font-family:'JetBrains Mono',monospace;font-size:42px;font-weight:500;color:var(--accent)}
.drill-result-sub{font-size:11px;color:var(--text3);letter-spacing:.05em;text-transform:uppercase;margin-top:4px}
.drill-result-detail{font-size:13px;color:var(--text2);margin-top:14px;line-height:1.7}
.drill-unit-row{display:flex;align-items:center;justify-content:space-between;padding:7px 12px;background:var(--surface2);border:1px solid var(--border);border-radius:var(--radius-sm);margin-bottom:5px;font-size:12px}
.drill-unit-row-title{color:var(--text2);flex:1}
.drill-unit-row-ok{color:var(--ok);font-weight:600;font-family:'JetBrains Mono',monospace;font-size:11px}
.drill-unit-row-ng{color:var(--err);font-weight:600;font-family:'JetBrains Mono',monospace;font-size:11px}
/* Step mode */
.step-pips{display:flex;gap:6px;margin-bottom:18px}
.step-pip{height:4px;flex:1;border-radius:2px;background:var(--border);transition:background .3s}
.step-pip.s-active{background:var(--accent)}
.step-pip.s-done{background:rgba(74,222,128,.5)}
.flash-card{background:var(--surface2);border:2px solid var(--border);border-radius:var(--radius);padding:22px 18px;text-align:center;min-height:120px;display:flex;flex-direction:column;align-items:center;justify-content:center;cursor:pointer;transition:border-color .2s,background .15s;margin-bottom:14px;user-select:none;-webkit-tap-highlight-color:transparent}
.flash-card:hover{border-color:var(--accent-border);background:var(--accent-dim)}
.flash-card-front{display:flex;flex-direction:column;align-items:center;gap:10px;width:100%}
.flash-card-term{font-size:15px;font-weight:600;color:var(--text);line-height:1.45;letter-spacing:-.01em}
.flash-card-hint{font-size:11px;color:var(--text3);letter-spacing:.04em}
.flash-card-hint-footer{margin-top:10px;width:100%}
.flash-card-back{font-size:13px;color:var(--text);line-height:1.75;padding-top:12px;border-top:1px solid var(--border);width:100%;margin-top:0;text-align:left}
.step-count{font-size:10px;color:var(--text3);text-align:center;margin-bottom:10px;letter-spacing:.04em}
.s2choice{display:flex;gap:10px;margin-bottom:10px}
.s2btn{flex:1;padding:16px 10px;border-radius:var(--radius);border:2px solid var(--border);background:var(--surface);font-size:14px;font-weight:600;color:var(--text2);cursor:pointer;transition:border-color .2s,background .15s,color .15s;font-family:'Geist',sans-serif;line-height:1.35}
.s2btn:hover:not(:disabled){border-color:var(--accent-border);color:var(--accent);background:var(--accent-dim)}
.s2btn:disabled{cursor:default}
.s2btn.s2-ok{border-color:rgba(74,222,128,.5) !important;background:rgba(74,222,128,.1) !important;color:var(--ok) !important}
.s2btn.s2-ng{border-color:rgba(248,113,113,.4) !important;background:rgba(248,113,113,.08) !important;color:var(--err) !important}
.s2-exp{font-size:12px;color:var(--text2);padding:8px 11px;background:rgba(129,140,248,.05);border:1px solid var(--accent-border);border-radius:var(--radius-sm);margin-bottom:10px;line-height:1.65}
.s2-exp strong{color:var(--accent)}
.step-action-row{display:flex;gap:8px;justify-content:flex-end;margin-top:4px}
/* Concept expand */
.concept-expand-btn{display:inline-flex;align-items:center;gap:5px;font-size:11px;color:var(--accent);background:none;border:none;cursor:pointer;padding:4px 0 0;font-family:'Geist',sans-serif;font-weight:500}
.concept-expand-btn:hover{opacity:.75}
.concept-chevron-wrap{display:inline-flex;min-width:11px;height:11px;align-items:center;justify-content:center;flex-shrink:0}
</style>
</head>
<body>
<div class="aurora" aria-hidden="true">
<div class="aurora-blob aurora-blob-1"></div>
<div class="aurora-blob aurora-blob-2"></div>
<div class="aurora-blob aurora-blob-3"></div>
</div>
<div id="app" x-data="scApp" x-init="init()">
<div class="overlay-mob" :class="{open:sidebarOpen}" @click="sidebarOpen=false" aria-hidden="true"></div>
<header>
<div class="brand">
<button class="icon-btn sidebar-toggle" @click="sidebarOpen=!sidebarOpen" aria-label="メニュー">
<i data-lucide="menu" style="width:16px;height:16px"></i>
</button>
<button class="brand-home" @click="goHome()" aria-label="ホームへ戻る">
<div class="brand-dot"></div>
<span class="brand-title">SC</span>
<span class="brand-sub">情報処理安全確保支援士</span>
</button>
</div>
<div style="display:flex;align-items:center;gap:8px">
<button class="icon-btn" @click="toggleTheme()" aria-label="テーマ切替">
<i data-lucide="sun" style="width:15px;height:15px" x-show="isDark"></i>
<i data-lucide="moon" style="width:15px;height:15px" x-show="!isDark"></i>
</button>
</div>
</header>
<div id="body">
<nav id="sidebar" :class="{open:sidebarOpen}">
<div class="sidebar-search">
<div class="search-wrap">
<i data-lucide="search" class="search-icon" style="width:12px;height:12px"></i>
<input class="search-input" type="search" placeholder="単元を検索..." x-model="search" aria-label="単元検索">
</div>
</div>
<div class="progress-wrap">
<div class="progress-label">
<span>進捗</span>
<span x-text="doneCount + ' / ' + totalCount"></span>
</div>
<div class="progress-track">
<div class="progress-fill" :style="'width:' + progressPct + '%'"></div>
</div>
</div>
<template x-for="cat in filteredCats" :key="cat.id">
<div>
<div class="sidebar-cat">
<span x-text="cat.label"></span>
<div class="sidebar-cat-line"></div>
</div>
<template x-for="u in cat.units" :key="u.id">
<div class="sidebar-item"
role="button" tabindex="0"
:aria-label="u.num + ': ' + u.title"
:aria-pressed="!!(currentUnit && currentUnit.id===u.id)"
:class="{active: currentUnit && currentUnit.id===u.id, done: isDone(u.id)}"
@click="openUnit(u); sidebarOpen=false"
@keydown.enter.prevent="openUnit(u); sidebarOpen=false"
@keydown.space.prevent="openUnit(u); sidebarOpen=false">
<span class="item-num" x-text="u.num"></span>
<span class="item-title" x-text="u.title"></span>
<span class="score-chip" x-show="bestScore(u.id) && !hasWrong(u.id)" :class="scoreChipCls(u.id)" x-text="bestScore(u.id)"></span>
<span class="weak-dot" x-show="hasWrong(u.id)" x-text="bestScore(u.id)||'苦'"></span>
<i data-lucide="check" class="item-check" style="width:11px;height:11px" x-show="!bestScore(u.id) && !hasWrong(u.id)"></i>
</div>
</template>
</div>
</template>
</nav>
<main id="main" tabindex="-1">
<!-- HOME -->
<div x-show="!currentUnit && !weakDrillActive && !glossaryViewActive && !examActive">
<div class="home-hero">
<h1>情報処理<span>安全確保支援士</span></h1>
<p>セキュリティの基礎から攻撃手法・法規まで、AM2試験に対応した31単元を体系的に学習します。開発者の視点から「なぜそうなのか」を理解しましょう。</p>
<p class="home-hint">各単元の概念タブの先頭に<strong>初学者向け</strong>の短い用語ガイドを置いています。ブックマーク用に URL 例: <code>?unit=s01</code>単元ID。サイドバーが開いているとき <kbd>/</kbd> で単元検索にフォーカスできます。</p>
<div class="stats-row">
<div class="stat-card">
<div class="stat-val" x-text="doneCount"></div>
<div class="stat-lbl">修了単元</div>
</div>
<div class="stat-card">
<div class="stat-val" x-text="totalCount"></div>
<div class="stat-lbl">全単元</div>
</div>
<div class="stat-card">
<div class="stat-val" x-text="totalBestCorrect + '/' + totalBestPossible"></div>
<div class="stat-lbl">クイズ正解</div>
</div>
</div>
</div>
<!-- Today's review -->
<div class="today-section" x-show="todayUnits.length > 0">
<div class="today-label">
<i data-lucide="calendar-check" style="width:13px;height:13px"></i>
今日の学習
</div>
<template x-for="u in todayUnits" :key="u.id">
<div class="today-card" @click="openUnit(u)">
<span class="today-tag" :class="u.todayTag==='苦手'?'today-tag-weak':u.todayTag==='復習'?'today-tag-review':'today-tag-new'" x-text="u.todayTag"></span>
<span class="today-num" x-text="u.num"></span>
<span class="today-title" x-text="u.title"></span>
<i data-lucide="chevron-right" class="today-arrow" style="width:14px;height:14px"></i>
</div>
</template>
</div>
<div class="home-tools">
<button class="btn-sm" type="button" @click="openGlossary()">
<i data-lucide="library" style="width:12px;height:12px"></i>
用語インデックス
</button>
<button class="btn-sm" type="button" @click="openExam()">
<i data-lucide="clipboard-check" style="width:12px;height:12px"></i>
試験モード
</button>
</div>
<div class="sync-card">
<div class="sync-title">
<i data-lucide="cloud" style="width:12px;height:12px"></i>
端末間の再開同期
</div>
<div class="sync-text">PC とスマホで同じ学習状態を再開するため、クラウドに進捗を保存します。</div>
<div class="sync-row" x-show="syncEnabled">
<span class="sync-status" x-text="syncStatus"></span>
<button class="btn-sm" type="button" :disabled="syncBusy" @click="syncNow()">今すぐ同期</button>
</div>
<div class="sync-row" x-show="!syncEnabled">
<input class="sync-input" type="password" placeholder="posimai_api_key または JWT" x-model="syncToken" aria-label="同期キー入力">
<button class="btn-sm btn-accent" type="button" @click="enableSyncWithInput()">同期を有効化</button>
</div>
<div class="sync-error" x-show="syncLastError" x-text="syncLastError"></div>
</div>
<!-- Weak drill button -->
<div style="margin-bottom:20px;display:flex;justify-content:center">
<button class="btn-sm" :class="weakDrillCandidates.length ? 'btn-accent' : ''"
:disabled="!weakDrillCandidates.length"
@click="weakDrillCandidates.length && startWeakDrill()">
<i data-lucide="zap" style="width:12px;height:12px"></i>
<span x-text="weakDrillCandidates.length ? '弱点特訓(' + weakDrillCandidates.length + '単元)' : '弱点なし'"></span>
</button>
</div>
<div class="home-cats">
<template x-for="cat in categories" :key="cat.id">
<div class="home-cat-card" @click="openUnit(cat.units[0])">
<div class="home-cat-icon">
<i :data-lucide="cat.icon" style="width:15px;height:15px"></i>
</div>
<div class="home-cat-title" x-text="cat.label"></div>
<div class="home-cat-sub" x-text="cat.units.length + ' 単元'"></div>
<div class="home-cat-progress">
<div class="home-cat-bar" :style="'width:' + catPct(cat) + '%'"></div>
</div>
</div>
</template>
</div>
</div>
<!-- GLOSSARY INDEX -->
<div x-show="glossaryViewActive && !currentUnit && !weakDrillActive">
<div class="card glossary-panel">
<div style="display:flex;align-items:flex-start;justify-content:space-between;gap:12px;margin-bottom:12px;flex-wrap:wrap">
<div class="card-title" style="margin-bottom:0">
<i data-lucide="library" style="width:13px;height:13px"></i>
用語インデックス
</div>
<button class="btn-sm" type="button" @click="closeGlossary()" aria-label="用語インデックスを閉じる">
<i data-lucide="x" style="width:11px;height:11px"></i>
閉じる
</button>
</div>
<p class="glossary-intro">各単元の「初学者向け」枠から用語と短い説明を集約しています。行を押すとその単元を開きます。開いているとき <kbd class="kbd-tiny">/</kbd> で検索欄にフォーカスします。</p>
<div class="glossary-search-wrap">
<i data-lucide="search" class="glossary-search-icon" style="width:12px;height:12px"></i>
<input class="search-input glossary-search-input" type="search" placeholder="用語・説明・単元で検索..." x-model="glossarySearch" aria-label="用語インデックスの検索">
</div>
<div class="glossary-meta-line" x-text="filteredGlossary.length + ' 件 / 全 ' + glossaryRows.length + ' 件'"></div>
<div class="glossary-list">
<template x-for="row in filteredGlossary" :key="row.unitId+'-'+row.term">
<div class="glossary-row" role="button" tabindex="0"
@click="openUnitFromGlossary(row.unitId)"
@keydown.enter.prevent="openUnitFromGlossary(row.unitId)"
@keydown.space.prevent="openUnitFromGlossary(row.unitId)">
<div class="glossary-term" x-text="row.term"></div>
<div class="glossary-hint" x-text="row.hint"></div>
<div class="glossary-unit">
<span class="glossary-num" x-text="row.num"></span>
<span x-text="row.title"></span>
<i data-lucide="chevron-right" style="width:14px;height:14px;flex-shrink:0;opacity:0.55;margin-left:auto"></i>
</div>
</div>
</template>
</div>
</div>
</div>
<!-- EXAM MODE -->
<div x-show="examActive && !currentUnit && !weakDrillActive">
<div class="card">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:14px;flex-wrap:wrap;gap:8px">
<div class="card-title" style="margin-bottom:0">
<i data-lucide="clipboard-check" style="width:13px;height:13px"></i>
試験モード
</div>
<button class="btn-sm" type="button" @click="exitExam()" aria-label="試験モードを終了する">
<i data-lucide="x" style="width:11px;height:11px"></i>
終了
</button>
</div>
<div x-show="examPhase==='setup'">
<p class="exam-setup-text">頻出単元を優先し、最大10問をランダムに出題します問題が足りない場合は重要・その他単元も混ざります。ここでの結果は単元ごとのベストスコアには保存されません。</p>
<div style="display:flex;justify-content:center;margin-top:12px">
<button class="btn-sm btn-accent" type="button" @click="startExamRun()">10問に挑戦</button>
</div>
</div>
<div x-show="examPhase==='run' && examCurrentEntry">
<div class="drill-prog-wrap">
<div class="drill-prog-label">
<span x-text="'試験 ' + (examIdx+1) + ' / ' + examTotal"></span>
<span x-text="examCurrentEntry?.unit?.num || ''"></span>
</div>
<div class="drill-prog-bar">
<div class="drill-prog-fill" :style="'width:' + (examTotal ? (examIdx/examTotal*100) : 0) + '%'"></div>
</div>
</div>
<div class="drill-unit-lbl">
<span x-text="(examCurrentEntry?.unit?.catLabel || '') + ' — ' + (examCurrentEntry?.unit?.title || '')"></span>
</div>
<div class="q-card" :class="exQCardCls()">
<div class="q-text" x-html="safeHtml(examCurrentEntry?.q?.q || '')"></div>
<div class="q-choices">
<template x-for="(ch, ci) in (examCurrentEntry?.q?.choices || [])" :key="ci">
<button class="q-choice" type="button"
:class="exChoiceCls(ci, examCurrentEntry?.q?.answer ?? -1)"
:disabled="exAnswered()"
@click="doExamAnswer(ci, examCurrentEntry?.q?.answer ?? -1, examCurrentEntry?.q?.exp || '')">
<span class="choice-key" x-text="keys[ci]"></span>
<span x-html="safeHtml(ch)"></span>
</button>
</template>
</div>
<div class="q-exp" x-show="exAnswered()">
<strong>解説:</strong> <span x-html="safeHtml(examQuizState[examIdx]?.exp)"></span>
</div>
</div>
<div class="quiz-result" x-show="exAnswered()" style="margin-top:14px">
<div class="result-actions">
<button class="btn-sm btn-accent" type="button" @click="nextExamQ()"
x-text="examIdx < examTotal - 1 ? '次の問題へ' : '結果を見る'"></button>
</div>
</div>
</div>
<div x-show="examPhase==='result'">
<div class="drill-result">
<div class="drill-result-pct" x-text="examCorrectCount + ' / ' + examTotal"></div>
<div class="drill-result-sub">正解(試験モード)</div>
</div>
<div x-show="examWrongItems.length" style="margin-top:14px">
<div class="card-title" style="font-size:12px;margin-bottom:8px">要復習(不正解)</div>
<template x-for="(w, wi) in examWrongItems" :key="'w'+wi">
<div class="drill-unit-row drill-row-link" role="button" tabindex="0"
@click="openUnitFromExam(w.unitId)"
@keydown.enter.prevent="openUnitFromExam(w.unitId)"
@keydown.space.prevent="openUnitFromExam(w.unitId)">
<span class="drill-unit-row-title" x-text="w.num + ' ' + w.title"></span>
<span class="drill-unit-row-ng">単元へ</span>
</div>
</template>
</div>
<div class="result-actions" style="margin-top:18px">
<button class="btn-sm btn-accent" type="button" @click="startExamRun()">もう一度</button>
<button class="btn-sm" type="button" @click="exitExam()">ホームへ</button>
</div>
</div>
</div>
</div>
<!-- WEAK DRILL -->
<div x-show="weakDrillActive">
<div class="card">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:14px">
<div class="card-title" style="margin-bottom:0">
<i data-lucide="zap" style="width:13px;height:13px"></i>
弱点集中特訓
</div>
<button class="btn-sm" type="button" @click="exitWeakDrill()" aria-label="弱点特訓を終了する">
<i data-lucide="x" style="width:11px;height:11px"></i>
特訓をやめる
</button>
</div>
<!-- Summary after all done -->
<div x-show="weakDrillDone" class="drill-result">
<div class="drill-result-pct" x-text="weakDrillClearCount + ' / ' + weakDrillResults.length"></div>
<div class="drill-result-sub">単元クリア</div>
<div class="drill-result-detail">
<template x-for="r in weakDrillResults" :key="r.unitId">
<div class="drill-unit-row">
<span class="drill-unit-row-title" x-text="r.num + ' ' + r.unitTitle"></span>
<span :class="r.correct >= r.total ? 'drill-unit-row-ok' : 'drill-unit-row-ng'"
x-text="r.correct + '/' + r.total"></span>
</div>
</template>
</div>
<div class="result-actions" style="margin-top:18px">
<button class="btn-sm btn-accent" @click="exitWeakDrill()">
<i data-lucide="home" style="width:11px;height:11px"></i>
ホームへ
</button>
</div>
</div>
<!-- Active drill -->
<div x-show="!weakDrillDone && weakDrillCurrentUnit">
<div class="drill-prog-wrap">
<div class="drill-prog-label">
<span x-text="'単元 ' + (weakDrillUnitIdx+1) + ' / ' + weakDrillUnits.length"></span>
<span x-text="weakDrillCurrentUnit?.num"></span>
</div>
<div class="drill-prog-bar">
<div class="drill-prog-fill" :style="'width:' + ((weakDrillUnitIdx)/weakDrillUnits.length*100) + '%'"></div>
</div>
</div>
<div class="drill-unit-lbl">
<span x-text="weakDrillCurrentUnit?.catLabel + ' — ' + weakDrillCurrentUnit?.title"></span>
</div>
<template x-for="(q, qi) in (weakDrillCurrentUnit?.quiz || [])" :key="qi">
<div class="q-card" :class="wdQCardCls(qi)">
<div class="q-num">
<i data-lucide="help-circle" style="width:11px;height:11px"></i>
<span x-text="qi+1"></span>
</div>
<div class="q-text" x-html="safeHtml(q.q)"></div>
<div class="q-choices">
<template x-for="(ch, ci) in q.choices" :key="ci">
<button class="q-choice"
:class="wdChoiceCls(qi, ci, q.answer)"
:disabled="wdAnswered(qi)"
@click="doWeakDrillAnswer(qi, ci, q.answer, q.exp)">
<span class="choice-key" x-text="keys[ci]"></span>
<span x-html="safeHtml(ch)"></span>
</button>
</template>
</div>
<div class="q-exp" x-show="wdAnswered(qi)">
<strong>解説:</strong> <span x-html="safeHtml(weakDrillQuizState[qi]?.exp)"></span>
</div>
</div>
</template>
<div class="quiz-result" x-show="weakDrillAllAnswered" style="margin-top:14px">
<div class="result-score" x-text="weakDrillUnitScore"></div>
<div class="result-lbl">正解 / 全問</div>
<div class="result-actions">
<button class="btn-sm btn-accent" @click="nextWeakDrillUnit()"
x-text="weakDrillUnitIdx < weakDrillUnits.length - 1 ? '次の単元へ' : '結果を見る'">
</button>
</div>
</div>
</div>
</div>
</div>
<!-- UNIT VIEW -->
<div x-show="currentUnit && !weakDrillActive">
<div class="unit-header">
<div class="unit-meta" x-show="currentUnit">
<div class="badge-row">
<div class="unit-cat-badge" x-text="currentUnit?.catLabel"></div>
<div class="badge" :class="freqBadgeCls" x-text="freqLabel"></div>
<div class="badge badge-diff" x-text="diffLabel"></div>
</div>
<div class="unit-title" x-text="currentUnit?.title"></div>
</div>
</div>
<!-- Concept -->
<div class="card" x-show="currentUnit && !stepMode">
<div class="card-title">
<i data-lucide="book-open" style="width:13px;height:13px"></i>
概念解説
</div>
<div class="concept-text" x-html="safeHtml(conceptPreview)"></div>
<div class="concept-text" x-show="conceptExpanded" x-html="safeHtml(conceptRest)"></div>
<button type="button" class="concept-expand-btn" x-show="conceptRest"
:aria-expanded="conceptExpanded ? 'true' : 'false'"
@click="conceptExpanded = !conceptExpanded">
<span class="concept-chevron-wrap" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" x-show="!conceptExpanded"><polyline points="6 9 12 15 18 9"></polyline></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" x-show="conceptExpanded"><polyline points="18 15 12 9 6 15"></polyline></svg>
</span>
<span x-text="conceptExpanded ? '閉じる' : 'もっと詳しく'"></span>
</button>
</div>
<!-- Keypoints -->
<div class="card" x-show="!stepMode && currentUnit?.keypoints?.length">
<div class="card-title">
<i data-lucide="zap" style="width:13px;height:13px"></i>
重要ポイント
</div>
<ul class="key-list">
<template x-for="kp in (currentUnit?.keypoints||[])" :key="kp">
<li class="key-item">
<div class="key-dot"></div>
<span x-html="safeHtml(kp)"></span>
</li>
</template>
</ul>
</div>
<!-- Exam tips -->
<div class="card" x-show="currentUnit?.examtips?.length && !stepMode" style="border-color:rgba(248,113,113,.18);background:rgba(248,113,113,.03)">
<div class="card-title" style="color:var(--err)">
<i data-lucide="alert-triangle" style="width:13px;height:13px;color:var(--err)"></i>
試験対策メモ
</div>
<template x-for="tip in (currentUnit?.examtips||[])" :key="tip">
<div class="tips-point">
<div class="tips-icon">!</div>
<span x-html="safeHtml(tip)"></span>
</div>
</template>
</div>
<!-- Study mode buttons -->
<div style="display:flex;gap:8px;margin-bottom:14px;flex-wrap:wrap;align-items:center" x-show="currentUnit && !stepMode">
<button class="btn-sm btn-accent" type="button" @click="startStepMode()"
x-show="currentUnit?.keypoints?.length">
<i data-lucide="layers" style="width:11px;height:11px"></i>
3ステップで学ぶ
</button>
<button class="btn-sm" type="button" @click="startStepModeClearQuiz()"
x-show="currentUnit?.keypoints?.length && currentUnit?.quiz?.length && Object.keys(quizState).length">
<i data-lucide="refresh-cw" style="width:11px;height:11px"></i>
理解度をクリアして学ぶ
</button>
</div>
<!-- Step mode card -->
<div class="card" x-show="stepMode" style="margin-bottom:14px">
<div style="display:flex;flex-wrap:wrap;align-items:flex-start;justify-content:space-between;gap:10px;margin-bottom:14px">
<div class="card-title" style="margin-bottom:0;flex:1;min-width:0">
<i data-lucide="layers" style="width:13px;height:13px"></i>
3ステップで学ぶ
</div>
<div style="display:flex;gap:8px;flex-wrap:wrap;justify-content:flex-end">
<button class="btn-sm" type="button" @click="stepGoBack()" aria-label="1つ前のカードに戻る">
<i data-lucide="arrow-left" style="width:11px;height:11px"></i>
戻る
</button>
<button class="btn-sm" type="button" @click="finishStepMode()" aria-label="学習フローを終了して通常画面に戻る">
<i data-lucide="check" style="width:11px;height:11px"></i>
学習フローを終了
</button>
</div>
</div>
<div class="step-pips">
<div class="step-pip" :class="stepPipCls(1)"></div>
<div class="step-pip" :class="stepPipCls(2)"></div>
<div class="step-pip" :class="stepPipCls(3)"></div>
</div>
<!-- Step 1: flashcards -->
<div x-show="stepStep === 1">
<div class="card-title" style="margin-bottom:10px">
<i data-lucide="book-open" style="width:12px;height:12px"></i>
Step 1 — 用語確認
</div>
<div class="step-count" x-text="(stepKpIdx+1) + ' / ' + (currentUnit?.keypoints?.length||0) + ' ポイント'"></div>
<div class="flash-card" role="button" tabindex="0" :aria-expanded="stepFlashRevealed || !keypointHasFlip(stepKpIdx)"
@click="advanceStep1Card()" @keydown.enter.prevent="advanceStep1Card()" @keydown.space.prevent="advanceStep1Card()">
<div class="flash-card-front" x-show="!stepFlashRevealed && keypointHasFlip(stepKpIdx)">
<div class="flash-card-term" x-html="safeHtml(keypointTerm(stepKpIdx))"></div>
<div class="flash-card-hint">タップして全文を表示</div>
</div>
<div class="flash-card-back" x-show="stepFlashRevealed"
x-html="safeHtml(currentUnit?.keypoints?.[stepKpIdx])"></div>
<div class="flash-card-hint flash-card-hint-footer"
x-show="stepFlashRevealed || !keypointHasFlip(stepKpIdx)">タップで次へ</div>
</div>
</div>
<!-- Step 2: 2-choice drills -->
<div x-show="stepStep === 2">
<div class="card-title" style="margin-bottom:10px">
<i data-lucide="git-branch" style="width:12px;height:12px"></i>
Step 2 — 二択ドリル
</div>
<template x-if="unitDrills.length > 0">
<div>
<div class="step-count" x-text="(stepDrillIdx+1) + ' / ' + unitDrills.length + ' 問'"></div>
<div class="q-text" style="font-size:13px;margin-bottom:12px"
x-html="safeHtml(unitDrills[stepDrillIdx]?.q)"></div>
<div class="s2choice">
<template x-for="(ch, ci) in (unitDrills[stepDrillIdx]?.choices||[])" :key="ci">
<button class="s2btn"
:class="stepDrillAnswered ? (ci === unitDrills[stepDrillIdx].answer ? 's2-ok' : (stepDrillSelected === ci ? 's2-ng' : '')) : ''"
:disabled="stepDrillAnswered"
@click="doStepDrill(ci)">
<span x-html="safeHtml(ch)"></span>
</button>
</template>
</div>
<div class="s2-exp" x-show="stepDrillAnswered">
<strong>解説:</strong> <span x-html="safeHtml(unitDrills[stepDrillIdx]?.exp)"></span>
</div>
<div class="step-action-row">
<button class="btn-sm btn-accent" @click="nextStepDrill()" x-show="stepDrillAnswered">
<span x-text="stepDrillIdx < unitDrills.length - 1 ? '次の問題' : 'Step 3 へ'"></span>
<i data-lucide="arrow-right" style="width:11px;height:11px"></i>
</button>
</div>
</div>
</template>
</div>
<!-- Step 3: header only (quiz is below) -->
<div x-show="stepStep === 3">
<div class="card-title" style="margin-bottom:8px">
<i data-lucide="check-circle-2" style="width:12px;height:12px"></i>
Step 3 — 本番問題
</div>
<p style="font-size:12px;color:var(--text2);line-height:1.65;margin-bottom:8px">
下の理解度チェックで確認します。途中まで答えた内容はそのまま続きからできます。最初から出し直すときは、全問に一度答えたあとに表示される「やり直し」か、フロー外の「理解度をクリアして学ぶ」を使ってください。
</p>
<p style="font-size:11px;color:var(--text3);line-height:1.55;margin-bottom:12px"
x-show="Object.keys(quizState).length">
現在の解答は保持されています。
</p>
<button class="btn-sm btn-accent" type="button" @click="scrollToComprehension()">
<i data-lucide="chevron-down" style="width:11px;height:11px"></i>
理解度チェックへ移動
</button>
</div>
</div>
<!-- Quiz -->
<div id="comprehension-quiz" class="card" tabindex="-1" x-show="currentUnit?.quiz?.length && (!stepMode || stepStep === 3)">
<div class="quiz-header">
<div class="quiz-title-label">
<i data-lucide="check-circle-2" style="width:13px;height:13px"></i>
理解度チェック
</div>
<div class="quiz-score-lbl" x-text="scoreLabel"></div>
</div>
<template x-for="(q, qi) in (currentUnit?.quiz||[])" :key="qi">
<div class="q-card" :class="qCardCls(qi)">
<div class="q-num">
<i data-lucide="help-circle" style="width:11px;height:11px"></i>
<span x-text="qi+1"></span>
</div>
<div class="q-text" x-html="safeHtml(q.q)"></div>
<div class="q-choices">
<template x-for="(ch, ci) in q.choices" :key="ci">
<button class="q-choice"
:class="choiceCls(qi,ci,q.answer)"
:disabled="answered(qi)"
@click="doAnswer(qi,ci,q.answer,q.exp)">
<span class="choice-key" x-text="keys[ci]"></span>
<span x-html="safeHtml(ch)"></span>
</button>
</template>
</div>
<div class="q-exp" x-show="answered(qi)">
<strong>解説:</strong> <span x-html="safeHtml(quizState[qi]?.exp)"></span>
</div>
</div>
</template>
<!-- Result -->
<div class="quiz-result" x-show="allAnswered">
<div class="result-score" x-text="resultScore"></div>
<div class="result-lbl">正解 / 全問</div>
<div class="result-msg" x-text="resultMsg"></div>
<div class="result-actions">
<button class="btn-sm" @click="resetQuiz">
<i data-lucide="refresh-cw" style="width:11px;height:11px"></i>
やり直し
</button>
<button class="btn-sm btn-accent" @click="nextUnit" :disabled="!hasNext">
次の単元
<i data-lucide="arrow-right" style="width:11px;height:11px"></i>
</button>
</div>
</div>
</div>
<!-- Navigation -->
<div class="unit-nav">
<button class="btn-sm" @click="prevUnit" :disabled="!hasPrev">
<i data-lucide="arrow-left" style="width:11px;height:11px"></i>
前の単元
</button>
<button class="btn-sm btn-accent" @click="markDone" x-show="!isDone(currentUnit?.id)">
<i data-lucide="check" style="width:11px;height:11px"></i>
学習済みにする
</button>
<button class="btn-sm" @click="nextUnit" :disabled="!hasNext">
次の単元
<i data-lucide="arrow-right" style="width:11px;height:11px"></i>
</button>
</div>
</div>
</main>
</div>
</div>
<script type="module" src="/js/app.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.2.4/dist/purify.min.js" integrity="sha384-eEu5CTj3qGvu9PdJuS+YlkNi7d2XxQROAFYOr59zgObtlcux1ae1Il3u7jvdCSWu" crossorigin="anonymous" defer></script>
<script src="https://unpkg.com/lucide@0.344.0/dist/umd/lucide.min.js" integrity="sha384-tTkFttkBclaU1cloKwOi9xk3pbao3VZxTjLNBt8iFABWDBQibbAbWpVmO28zMuxq" crossorigin="anonymous" defer></script>
<script src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.3/dist/cdn.min.js" integrity="sha384-iZD2X8o1Zdq0HR5H/7oa8W30WS4No+zWCKUPD7fHRay9I1Gf+C4F8sVmw7zec1wW" crossorigin="anonymous" defer></script>
</body>
</html>