summaryrefslogtreecommitdiff
path: root/static
diff options
context:
space:
mode:
authorSivaldo <sivaldodavi@disroot.org>2026-06-13 19:20:23 -0300
committerSivaldo <sivaldodavi@disroot.org>2026-06-13 19:20:23 -0300
commit7316238f16655bd127b04476bbf7864b27f940f9 (patch)
tree61f25290b3e5fd14894e52754cfe6dd4e85845ef /static
Commit inicialHEADmain
Diffstat (limited to 'static')
-rw-r--r--static/CNAME1
-rw-r--r--static/dark-mode.js59
-rw-r--r--static/dashboard.js130
-rw-r--r--static/footnotes.js77
-rw-r--r--static/home-filter.js72
-rw-r--r--static/keyboard-shortcuts.js73
-rw-r--r--static/main.css1560
-rw-r--r--static/manifest.json34
-rw-r--r--static/offline.html22
-rw-r--r--static/quiz-engine.js170
-rw-r--r--static/quiz-parser.js144
-rw-r--r--static/quiz-runtime.js92
-rw-r--r--static/sw.js68
-rw-r--r--static/toc.js52
14 files changed, 2554 insertions, 0 deletions
diff --git a/static/CNAME b/static/CNAME
new file mode 100644
index 0000000..488dfd6
--- /dev/null
+++ b/static/CNAME
@@ -0,0 +1 @@
+dir.sivaldodavi.com
diff --git a/static/dark-mode.js b/static/dark-mode.js
new file mode 100644
index 0000000..8e2d761
--- /dev/null
+++ b/static/dark-mode.js
@@ -0,0 +1,59 @@
+/**
+ * Dark Mode Toggle
+ * Respeita a preferência do sistema e permite alternar manualmente
+ * Salva a preferência no localStorage
+ */
+
+const DARK_MODE_KEY = 'caderno_dark_mode';
+
+function initDarkMode() {
+ // Verificar preferência salva
+ const saved = localStorage.getItem(DARK_MODE_KEY);
+ let isDark;
+
+ if (saved !== null) {
+ isDark = saved === 'true';
+ } else {
+ // Respeitar preferência do sistema
+ isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
+ }
+
+ applyDarkMode(isDark);
+}
+
+function applyDarkMode(isDark) {
+ const html = document.documentElement;
+ const toggle = document.getElementById('dark-mode-toggle');
+
+ if (isDark) {
+ html.setAttribute('data-theme', 'dark');
+ if (toggle) toggle.textContent = '☀️';
+ } else {
+ html.removeAttribute('data-theme');
+ if (toggle) toggle.textContent = '🌙';
+ }
+
+ localStorage.setItem(DARK_MODE_KEY, isDark);
+}
+
+function toggleDarkMode() {
+ const html = document.documentElement;
+ const isDark = html.getAttribute('data-theme') === 'dark';
+ applyDarkMode(!isDark);
+}
+
+// Inicializar quando o DOM estiver pronto
+if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', initDarkMode);
+} else {
+ initDarkMode();
+}
+
+// Detectar mudanças na preferência do sistema
+window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
+ const saved = localStorage.getItem(DARK_MODE_KEY);
+ if (saved === null) {
+ // Se o usuário não escolheu manualmente, seguir o sistema
+ applyDarkMode(e.matches);
+ }
+});
diff --git a/static/dashboard.js b/static/dashboard.js
new file mode 100644
index 0000000..c14369f
--- /dev/null
+++ b/static/dashboard.js
@@ -0,0 +1,130 @@
+/**
+ * Dashboard de Progresso
+ * Lê dados do localStorage e mostra estatísticas de estudo
+ */
+
+var PROGRESS_PREFIX = window.PROGRESS_PREFIX || 'quiz_progress_';
+
+function initDashboard() {
+ const dashboard = document.getElementById('dashboard-stats');
+ if (!dashboard) return;
+
+ const stats = calculateStats();
+ renderDashboard(stats);
+}
+
+function calculateStats() {
+ const allKeys = Object.keys(localStorage);
+ const progressKeys = allKeys.filter((k) => k.startsWith(PROGRESS_PREFIX));
+
+ const stats = {
+ totalQuizzes: progressKeys.length,
+ totalAcertos: 0,
+ totalErros: 0,
+ disciplinas: {},
+ };
+
+ progressKeys.forEach((key) => {
+ try {
+ const data = JSON.parse(localStorage.getItem(key));
+ if (!data || !data.questions) return;
+
+ const disciplina = key.replace(PROGRESS_PREFIX, '').split('_')[0];
+ if (!stats.disciplinas[disciplina]) {
+ stats.disciplinas[disciplina] = {
+ acertos: 0,
+ erros: 0,
+ total: 0,
+ };
+ }
+
+ stats.disciplinas[disciplina].acertos += data.acertos || 0;
+ stats.disciplinas[disciplina].erros += data.erros || 0;
+ stats.disciplinas[disciplina].total += data.questions.length;
+
+ stats.totalAcertos += data.acertos || 0;
+ stats.totalErros += data.erros || 0;
+ } catch (e) {}
+ });
+
+ return stats;
+}
+
+function renderDashboard(stats) {
+ const dashboard = document.getElementById('dashboard-stats');
+ if (!dashboard) return;
+
+ const totalQuestoes = stats.totalAcertos + stats.totalErros;
+ const pctGeral = totalQuestoes > 0 ? Math.round((stats.totalAcertos / totalQuestoes) * 100) : 0;
+
+ let html = `
+ <div class="dashboard-container">
+ <h2>📊 Seu Progresso de Estudo</h2>
+
+ <div class="dashboard-overview">
+ <div class="stat-card">
+ <div class="stat-label">Quizzes Iniciados</div>
+ <div class="stat-value">${stats.totalQuizzes}</div>
+ </div>
+ <div class="stat-card">
+ <div class="stat-label">Taxa de Acerto Geral</div>
+ <div class="stat-value">${pctGeral}%</div>
+ </div>
+ <div class="stat-card">
+ <div class="stat-label">Questões Respondidas</div>
+ <div class="stat-value">${totalQuestoes}</div>
+ </div>
+ </div>
+
+ <div class="dashboard-disciplines">
+ <h3>Por Disciplina</h3>
+ `;
+
+ Object.entries(stats.disciplinas).forEach(([disc, data]) => {
+ const pct = data.total > 0 ? Math.round((data.acertos / data.total) * 100) : 0;
+ const nivel = pct >= 80 ? '🟢 Dominado' : pct >= 60 ? '🟡 Bom' : '🔴 Revisar';
+
+ html += `
+ <div class="disc-stat">
+ <div class="disc-name">${disc}</div>
+ <div class="disc-bar">
+ <div class="disc-progress" style="width: ${pct}%"></div>
+ </div>
+ <div class="disc-info">
+ <span>${data.acertos}/${data.total}</span>
+ <span>${pct}%</span>
+ <span>${nivel}</span>
+ </div>
+ </div>
+ `;
+ });
+
+ html += `
+ </div>
+ <div class="dashboard-actions">
+ <button class="btn" onclick="clearAllProgress()">Limpar Histórico</button>
+ </div>
+ </div>
+ `;
+
+ dashboard.innerHTML = html;
+}
+
+function clearAllProgress() {
+ if (confirm('Tem certeza que quer limpar todo o histórico de progresso?')) {
+ const allKeys = Object.keys(localStorage);
+ allKeys.forEach((key) => {
+ if (key.startsWith(PROGRESS_PREFIX)) {
+ localStorage.removeItem(key);
+ }
+ });
+ initDashboard();
+ }
+}
+
+// Inicializar quando o DOM estiver pronto
+if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', initDashboard);
+} else {
+ initDashboard();
+}
diff --git a/static/footnotes.js b/static/footnotes.js
new file mode 100644
index 0000000..1d2700a
--- /dev/null
+++ b/static/footnotes.js
@@ -0,0 +1,77 @@
+/**
+ * Footnotes - Sistema de notas flutuantes (floater)
+ */
+
+let activeFloater = null;
+
+document.addEventListener('click', (e) => {
+ const fn = e.target.closest('a[href^="#fn:"]');
+ const close = e.target.closest('.f-close');
+
+ if (close) {
+ closeFloater();
+ return;
+ }
+
+ if (fn) {
+ e.preventDefault();
+ e.stopPropagation();
+ closeFloater();
+
+ const el = document.getElementById(fn.getAttribute('href').slice(1));
+ if (!el) return;
+
+ const clone = el.cloneNode(true);
+ clone.querySelector('.footnote-backref')?.remove();
+
+ const parent = fn.closest('p, li');
+ const quote = parent ? parent.innerText.slice(0, 40) + '…' : 'Nota';
+
+ openFloater(fn, quote, clone.innerHTML);
+ fn.classList.add('active');
+ } else if (activeFloater && !e.target.closest('.floater')) {
+ closeFloater();
+ }
+});
+
+function openFloater(target, quote, html) {
+ const f = document.createElement('div');
+ f.className = 'floater';
+ f.innerHTML = `<div class="f-label"><span>comentário</span><span class="f-close" role="button" tabindex="0" aria-label="Fechar nota">×</span></div><div class="f-quote">${quote}</div><div>${html}</div>`;
+
+ document.body.appendChild(f);
+
+ const r = target.getBoundingClientRect();
+ const top = window.scrollY + r.bottom + 8;
+ const padding = 20;
+ let left = window.scrollX + r.left;
+ const maxLeft = window.innerWidth - f.offsetWidth - padding;
+
+ if (left > maxLeft) left = Math.max(padding, maxLeft);
+
+ f.style.top = top + 'px';
+ f.style.left = left + 'px';
+
+ activeFloater = f;
+
+ // Permitir fechar com Enter ou Space
+ f.querySelector('.f-close').addEventListener('keydown', (e) => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ closeFloater();
+ }
+ });
+}
+
+function closeFloater() {
+ if (activeFloater) {
+ activeFloater.remove();
+ activeFloater = null;
+ }
+ document.querySelectorAll('a[href^="#fn:"]').forEach((a) => a.classList.remove('active'));
+}
+
+// Fechar com Escape
+document.addEventListener('keydown', (e) => {
+ if (e.key === 'Escape') closeFloater();
+});
diff --git a/static/home-filter.js b/static/home-filter.js
new file mode 100644
index 0000000..b2d1a47
--- /dev/null
+++ b/static/home-filter.js
@@ -0,0 +1,72 @@
+/**
+ * Home Filter — filtra disciplinas conforme você digita
+ * Só funciona quando há itens [data-filterable] na página
+ */
+document.addEventListener('DOMContentLoaded', () => {
+ const items = document.querySelectorAll('[data-filterable]');
+ if (!items.length) return;
+
+ // Cria campo de busca
+ const wrap = document.createElement('div');
+ wrap.className = 'filter-container';
+ wrap.innerHTML = `
+ <input
+ type="search"
+ id="home-filter"
+ class="filter-input"
+ placeholder="Buscar disciplina…"
+ aria-label="Filtrar disciplinas"
+ autocomplete="off"
+ spellcheck="false"
+ >
+ <span class="filter-count" id="filter-count" aria-live="polite"></span>
+ `;
+
+ // Insere antes do disc-grid dentro do <main>
+ const grid = document.querySelector('.disc-grid');
+ if (grid) {
+ grid.parentNode.insertBefore(wrap, grid);
+ } else {
+ const main = document.querySelector('main');
+ if (main) main.insertBefore(wrap, main.firstChild);
+ }
+
+ const input = document.getElementById('home-filter');
+ const count = document.getElementById('filter-count');
+
+ // Normaliza texto para comparação (remove acentos, maiúsculas)
+ function normalize(str) {
+ return str.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase();
+ }
+
+ input.addEventListener('input', () => {
+ const q = normalize(input.value.trim());
+ let visible = 0;
+
+ items.forEach(item => {
+ const text = normalize(item.textContent);
+ const match = !q || text.includes(q);
+ item.style.display = match ? '' : 'none';
+ if (match) visible++;
+ });
+
+ count.textContent = q ? `${visible} resultado${visible !== 1 ? 's' : ''}` : '';
+ });
+
+ // Esc limpa e desfoca
+ input.addEventListener('keydown', e => {
+ if (e.key === 'Escape') {
+ input.value = '';
+ input.dispatchEvent(new Event('input'));
+ input.blur();
+ }
+ });
+
+ // Foco automático na home com / ou Ctrl+K
+ document.addEventListener('keydown', e => {
+ if ((e.key === '/' || (e.ctrlKey && e.key === 'k')) && document.activeElement !== input) {
+ e.preventDefault();
+ input.focus();
+ }
+ });
+});
diff --git a/static/keyboard-shortcuts.js b/static/keyboard-shortcuts.js
new file mode 100644
index 0000000..53cfe44
--- /dev/null
+++ b/static/keyboard-shortcuts.js
@@ -0,0 +1,73 @@
+/**
+ * Keyboard Shortcuts
+ * Atalhos para acelerar a navegação e resposta de quizzes
+ *
+ * Atalhos:
+ * 1-5: Selecionar opção A-E
+ * Espaço: Próxima questão
+ * Esc: Fechar notas/floaters
+ * ?: Mostrar ajuda
+ */
+
+const SHORTCUTS = {
+ '1': () => selectOptionByLetter(0),
+ '2': () => selectOptionByLetter(1),
+ '3': () => selectOptionByLetter(2),
+ '4': () => selectOptionByLetter(3),
+ '5': () => selectOptionByLetter(4),
+ ' ': (e) => {
+ e.preventDefault();
+ const nextBtn = document.getElementById('btn-next');
+ if (nextBtn && nextBtn.style.display !== 'none') {
+ nextBtn.click();
+ }
+ },
+ 'Escape': () => {
+ // Fechar floaters se existir
+ if (typeof closeFloater === 'function') {
+ closeFloater();
+ }
+ },
+ '?': (e) => {
+ e.preventDefault();
+ showKeyboardHelp();
+ },
+};
+
+function selectOptionByLetter(index) {
+ const buttons = document.querySelectorAll('.opt');
+ if (buttons[index]) {
+ buttons[index].click();
+ }
+}
+
+function showKeyboardHelp() {
+ const help = `
+⌨️ Atalhos de Teclado
+
+Quizzes:
+ 1-5 Selecionar opção A-E
+ SPC Próxima questão
+ ESC Fechar notas
+
+Geral:
+ ESC Fechar floaters
+ ? Mostrar esta ajuda
+ `;
+
+ alert(help);
+}
+
+document.addEventListener('keydown', (e) => {
+ // Ignorar se está digitando em um input/textarea
+ if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') {
+ return;
+ }
+
+ const key = e.key;
+ const handler = SHORTCUTS[key];
+
+ if (handler) {
+ handler(e);
+ }
+});
diff --git a/static/main.css b/static/main.css
new file mode 100644
index 0000000..d4257a4
--- /dev/null
+++ b/static/main.css
@@ -0,0 +1,1560 @@
+/* ── VARIÁVEIS E RESET ── */
+:root {
+ --serif: 'Lora', serif;
+ --mono: 'JetBrains Mono', monospace;
+ --text: #dedad4;
+ --text-2: #a8a8a8;
+ --muted: #6a6a75;
+ --surface: #1a1a1f;
+ --border: #2a2a30;
+ --border-2: #3a3a40;
+ --amber: #c9a961;
+ --t: 150ms ease;
+}
+
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+html {
+ background: #0f0f13;
+ color: var(--text);
+ font-family: var(--serif);
+ font-size: 16px;
+ line-height: 1.6;
+}
+
+body {
+ background: #0f0f13;
+ color: var(--text);
+ font-family: var(--serif);
+ display: flex;
+ flex-direction: column;
+ min-height: 100vh;
+}
+
+/* ── HEADER ── */
+header {
+ padding: 1.2rem 2rem;
+ border-bottom: 1px solid var(--border);
+ background: rgba(15, 15, 19, 0.95);
+ backdrop-filter: blur(4px);
+ position: sticky;
+ top: 0;
+ z-index: 100;
+}
+
+.site-name {
+ font-family: var(--mono);
+ font-size: 0.75rem;
+ letter-spacing: 0.15em;
+ text-transform: uppercase;
+ color: var(--text);
+ text-decoration: none;
+ transition: color var(--t);
+}
+
+.site-name:hover {
+ color: var(--amber);
+}
+
+.header-sep {
+ color: var(--border-2);
+ margin: 0 0.5rem;
+}
+
+.breadcrumb {
+ color: var(--text-2);
+ font-size: 0.9rem;
+}
+
+.breadcrumb-link {
+ color: var(--text-2);
+ text-decoration: none;
+ transition: color var(--t);
+}
+
+.breadcrumb-link:hover {
+ color: var(--text);
+}
+
+/* ── MAIN ── */
+main {
+ flex: 1;
+ padding: 2rem;
+ max-width: 900px;
+ margin: 0 auto;
+ width: 100%;
+}
+
+/* ── FADE ANIMATION ── */
+.fade {
+ animation: fadeIn 200ms ease;
+}
+
+@keyframes fadeIn {
+ from { opacity: 0; }
+ to { opacity: 1; }
+}
+
+.fade-out {
+ animation: fadeOut 100ms ease;
+}
+
+@keyframes fadeOut {
+ from { opacity: 1; }
+ to { opacity: 0; }
+}
+
+/* ── GRID DE DISCIPLINAS ── */
+.disc-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
+ gap: 1.2rem;
+ margin-bottom: 2rem;
+}
+
+.disc-card {
+ display: flex;
+ flex-direction: column;
+ padding: 1.2rem;
+ border: 1px dashed var(--border);
+ border-radius: 4px;
+ text-decoration: none;
+ color: var(--text);
+ transition: all var(--t);
+ cursor: pointer;
+}
+
+.disc-card:hover {
+ border-color: var(--amber);
+ background: color-mix(in srgb, var(--amber) 3%, transparent);
+ transform: translateY(-2px);
+}
+
+.disc-card-name {
+ font-size: 1rem;
+ font-weight: 600;
+ margin-bottom: 0.5rem;
+ line-height: 1.3;
+}
+
+.disc-card-meta {
+ font-family: var(--mono);
+ font-size: 0.62rem;
+ color: var(--muted);
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+}
+
+/* ── BOTÃO VOLTAR ── */
+.disc-back {
+ margin-bottom: 1.2rem;
+}
+
+.disc-back a {
+ font-family: var(--mono);
+ font-size: 0.68rem;
+ color: var(--muted);
+ text-decoration: none;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+ transition: color var(--t);
+}
+
+.disc-back a:hover {
+ color: var(--text-2);
+}
+
+/* ── DISCIPLINA ── */
+.discipline {
+ margin-bottom: 2.5rem;
+}
+
+.discipline-name {
+ font-family: var(--mono);
+ font-size: 0.68rem;
+ color: var(--muted);
+ letter-spacing: 0.1em;
+ text-transform: uppercase;
+ margin-bottom: 0.65rem;
+ padding-bottom: 0.45rem;
+ border-bottom: 1px solid var(--border);
+}
+
+/* ── INTRO/NOTAS DA DISCIPLINA ── */
+.disc-intro {
+ margin: 1.2rem 0 1.8rem;
+ padding: 1rem 1.2rem;
+ border-left: 2px solid var(--amber);
+ background: color-mix(in srgb, var(--amber) 5%, transparent);
+ border-radius: 0 4px 4px 0;
+}
+
+.disc-intro-prose {
+ font-size: 0.9rem;
+ color: var(--text-2);
+ margin: 0;
+}
+
+.disc-intro-prose p:first-child {
+ margin-top: 0;
+}
+
+.disc-intro-prose p:last-child {
+ margin-bottom: 0;
+}
+
+.disc-intro-refs {
+ margin-top: 0.75rem;
+ padding-top: 0.5rem;
+ border-top: 1px solid var(--border);
+}
+
+/* ── TÓPICOS (SUBPASTAS) ── */
+.topic-section {
+ margin-bottom: 0.5rem;
+}
+
+.topic-head {
+ font-family: var(--mono);
+ font-size: 0.62rem;
+ color: var(--muted);
+ letter-spacing: 0.12em;
+ text-transform: uppercase;
+ margin: 1.4rem 0 0.3rem;
+ padding-bottom: 0.3rem;
+ border-bottom: 1px dashed var(--border);
+}
+
+.subject-group {
+ padding: 0.85rem 0;
+ border-bottom: 1px solid var(--border);
+}
+
+.subject-group:last-child {
+ border-bottom: none;
+}
+
+.subject-head {
+ font-size: 0.95rem;
+ color: var(--text);
+ margin-bottom: 0.5rem;
+}
+
+.item-row {
+ display: flex;
+ justify-content: space-between;
+ align-items: baseline;
+ padding: 0.35rem 0;
+ cursor: pointer;
+ text-decoration: none;
+ color: var(--text-2);
+ transition: opacity var(--t);
+ gap: 1rem;
+}
+
+.item-row:hover {
+ opacity: 0.55;
+ color: var(--text);
+}
+
+.item-kind {
+ font-family: var(--mono);
+ font-size: 0.62rem;
+ letter-spacing: 0.1em;
+ text-transform: uppercase;
+ color: var(--muted);
+ min-width: 3.2rem;
+ flex-shrink: 0;
+}
+
+.item-kind.txt {
+ color: var(--amber);
+}
+
+.item-kind.qz {
+ color: var(--text-2);
+}
+
+.item-title {
+ flex: 1;
+ font-size: 0.9rem;
+}
+
+.item-meta {
+ font-family: var(--mono);
+ font-size: 0.66rem;
+ color: var(--muted);
+ white-space: nowrap;
+ flex-shrink: 0;
+}
+
+/* ── LEITOR DE TEXTOS & PROSE ── */
+.text-meta {
+ font-family: var(--mono);
+ font-size: 0.68rem;
+ color: var(--muted);
+ margin-bottom: 1.4rem;
+ letter-spacing: 0.02em;
+ display: flex;
+ gap: 0.6rem;
+ align-items: center;
+ flex-wrap: wrap;
+}
+
+.text-meta .dot {
+ width: 3px;
+ height: 3px;
+ border-radius: 50%;
+ background: var(--border-2);
+}
+
+.text-title {
+ font-size: 1.4rem;
+ line-height: 1.35;
+ margin-bottom: 1.6rem;
+ color: var(--text);
+ font-weight: 600;
+}
+
+.prose {
+ font-size: 1rem;
+ line-height: 1.8;
+ color: var(--text);
+}
+
+.prose p {
+ margin-bottom: 1.1rem;
+}
+
+.prose h2 {
+ font-family: var(--serif);
+ font-size: 1.1rem;
+ font-weight: 600;
+ margin: 1.8rem 0 0.8rem;
+ color: var(--text);
+}
+
+.prose h3 {
+ font-family: var(--mono);
+ font-size: 0.72rem;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ color: var(--muted);
+ margin: 1.6rem 0 0.6rem;
+}
+
+.prose blockquote {
+ border-left: 2px solid var(--border-2);
+ padding: 0.2rem 0 0.2rem 1rem;
+ margin: 1rem 0;
+ color: var(--text-2);
+ font-style: italic;
+}
+
+.prose em {
+ color: var(--text-2);
+}
+
+.prose strong {
+ color: var(--text);
+ font-weight: 600;
+}
+
+.prose code {
+ font-family: var(--mono);
+ font-size: 0.85em;
+ background: var(--surface);
+ padding: 0.05em 0.35em;
+ border-radius: 2px;
+ color: var(--text-2);
+}
+
+/* ── TABELAS ── */
+.prose table {
+ width: 100%;
+ border-collapse: collapse;
+ margin: 1.8rem 0;
+ font-size: 0.9rem;
+ color: var(--text-2);
+}
+
+.prose th {
+ font-family: var(--mono);
+ font-size: 0.68rem;
+ letter-spacing: 0.05em;
+ text-transform: uppercase;
+ color: var(--muted);
+ border-bottom: 1px solid var(--border-2);
+ padding: 0.5rem 0.6rem;
+ text-align: left;
+}
+
+.prose td {
+ padding: 0.6rem;
+ border-bottom: 1px solid var(--border);
+ vertical-align: top;
+}
+
+.prose tr:hover td {
+ color: var(--text);
+}
+
+@media (max-width: 480px) {
+ .prose table {
+ overflow-x: auto;
+ display: block;
+ }
+}
+
+/* ── NOTAS DE RODAPÉ ── */
+a[href^="#fn:"] {
+ text-decoration: none;
+ color: var(--amber);
+ cursor: pointer;
+ transition: opacity var(--t);
+}
+
+a[href^="#fn:"]:hover {
+ opacity: 0.7;
+}
+
+a[href^="#fn:"].active {
+ font-weight: 600;
+}
+
+.footnotes {
+ margin-top: 2rem;
+ padding-top: 1rem;
+ border-top: 1px solid var(--border);
+ font-size: 0.9rem;
+}
+
+.footnote-backref {
+ text-decoration: none;
+ color: var(--amber);
+}
+
+/* ── FLOATER (POPUP DE NOTAS) ── */
+.floater {
+ position: fixed;
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ padding: 1rem;
+ max-width: 320px;
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
+ z-index: 1000;
+ animation: slideUp 150ms ease;
+}
+
+@keyframes slideUp {
+ from {
+ opacity: 0;
+ transform: translateY(10px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+.f-label {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ font-family: var(--mono);
+ font-size: 0.62rem;
+ color: var(--muted);
+ letter-spacing: 0.1em;
+ text-transform: uppercase;
+ margin-bottom: 0.5rem;
+ padding-bottom: 0.5rem;
+ border-bottom: 1px solid var(--border);
+}
+
+.f-close {
+ cursor: pointer;
+ font-size: 1.2rem;
+ color: var(--text-2);
+ transition: color var(--t);
+}
+
+.f-close:hover {
+ color: var(--text);
+}
+
+.f-quote {
+ font-size: 0.85rem;
+ color: var(--text-2);
+ font-style: italic;
+ margin-bottom: 0.5rem;
+}
+
+/* ── SUMÁRIO (TOC) ── */
+.toc {
+ margin: 1.5rem 0 2rem;
+ padding: 1rem;
+ background: color-mix(in srgb, var(--amber) 3%, transparent);
+ border: 1px solid var(--border);
+ border-radius: 4px;
+}
+
+.toc-label {
+ font-family: var(--mono);
+ font-size: 0.62rem;
+ color: var(--muted);
+ letter-spacing: 0.1em;
+ text-transform: uppercase;
+ margin-bottom: 0.75rem;
+}
+
+#toc-list {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+.toc-h2 {
+ margin-bottom: 0.5rem;
+}
+
+.toc-h2 a {
+ font-size: 0.95rem;
+ color: var(--text);
+ text-decoration: none;
+ transition: color var(--t);
+}
+
+.toc-h2 a:hover {
+ color: var(--amber);
+}
+
+.toc-h3 {
+ margin-left: 1.2rem;
+ margin-bottom: 0.3rem;
+}
+
+.toc-h3 a {
+ font-size: 0.85rem;
+ color: var(--text-2);
+ text-decoration: none;
+ transition: color var(--t);
+}
+
+.toc-h3 a:hover {
+ color: var(--text);
+}
+
+/* ── REFERÊNCIAS ── */
+.references {
+ margin-top: 2rem;
+ padding-top: 1.5rem;
+ border-top: 1px solid var(--border);
+}
+
+.ref-head {
+ font-family: var(--mono);
+ font-size: 0.62rem;
+ color: var(--muted);
+ letter-spacing: 0.1em;
+ text-transform: uppercase;
+ margin-bottom: 1rem;
+}
+
+.ref-list {
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+}
+
+.ref-list p {
+ font-size: 0.9rem;
+ color: var(--text-2);
+ margin: 0;
+ line-height: 1.5;
+}
+
+/* ── QUIZ: LAYOUT GERAL ── */
+.progress-wrap {
+ width: 100%;
+ height: 3px;
+ background: var(--border);
+ border-radius: 2px;
+ overflow: hidden;
+ margin-bottom: 1.5rem;
+}
+
+.progress-fill {
+ height: 100%;
+ background: var(--amber);
+ width: 0%;
+ transition: width 300ms ease;
+}
+
+.q-meta {
+ font-family: var(--mono);
+ font-size: 0.68rem;
+ color: var(--muted);
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ margin-bottom: 1.2rem;
+}
+
+#quiz-body {
+ margin-bottom: 1.5rem;
+}
+
+/* ── QUIZ: MÚLTIPLA ESCOLHA ── */
+.q-text {
+ font-size: 1rem;
+ line-height: 1.8;
+ color: var(--text);
+ margin-bottom: 1.5rem;
+}
+
+.options {
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+ margin-bottom: 1.5rem;
+}
+
+.opt {
+ display: flex;
+ align-items: flex-start;
+ gap: 0.75rem;
+ padding: 0.75rem 1rem;
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ cursor: pointer;
+ transition: all var(--t);
+ font-family: var(--serif);
+ font-size: 0.95rem;
+ color: var(--text);
+ text-align: left;
+ min-height: 3rem;
+}
+
+.opt:hover:not(:disabled) {
+ border-color: var(--amber);
+ background: color-mix(in srgb, var(--amber) 5%, transparent);
+}
+
+.opt:disabled {
+ cursor: not-allowed;
+ opacity: 0.7;
+}
+
+.opt.correct {
+ border-color: #4ade80;
+ background: color-mix(in srgb, #4ade80 15%, transparent);
+}
+
+.opt.wrong {
+ border-color: #ef4444;
+ background: color-mix(in srgb, #ef4444 15%, transparent);
+}
+
+.opt.revealed {
+ opacity: 0.5;
+}
+
+.opt-letter {
+ font-family: var(--mono);
+ font-size: 0.75rem;
+ font-weight: 600;
+ color: var(--muted);
+ min-width: 1.5rem;
+ flex-shrink: 0;
+}
+
+/* ── QUIZ: RESPOSTA ABERTA ── */
+.open-area {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+textarea {
+ padding: 1rem;
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ color: var(--text);
+ font-family: var(--serif);
+ font-size: 0.95rem;
+ line-height: 1.6;
+ resize: vertical;
+ min-height: 120px;
+ transition: border-color var(--t);
+}
+
+textarea:focus {
+ outline: none;
+ border-color: var(--amber);
+}
+
+textarea::placeholder {
+ color: var(--muted);
+}
+
+/* ── QUIZ: FEEDBACK ── */
+.feedback-line {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ font-family: var(--mono);
+ font-size: 0.75rem;
+ letter-spacing: 0.05em;
+ text-transform: uppercase;
+ color: var(--text-2);
+ opacity: 0;
+ transition: opacity var(--t);
+}
+
+.feedback-line.show {
+ opacity: 1;
+}
+
+.feedback-dot {
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ background: var(--text-2);
+}
+
+.feedback-line.ok .feedback-dot {
+ background: #4ade80;
+}
+
+.feedback-line.bad .feedback-dot {
+ background: #ef4444;
+}
+
+/* ── QUIZ: COMENTÁRIO ── */
+.comment-block {
+ padding: 1rem;
+ background: color-mix(in srgb, var(--amber) 5%, transparent);
+ border-left: 2px solid var(--amber);
+ border-radius: 0 4px 4px 0;
+ max-height: 0;
+ overflow: hidden;
+ transition: max-height var(--t);
+}
+
+.comment-block.show {
+ max-height: 500px;
+}
+
+.comment-label {
+ font-family: var(--mono);
+ font-size: 0.62rem;
+ color: var(--muted);
+ letter-spacing: 0.1em;
+ text-transform: uppercase;
+ margin-bottom: 0.5rem;
+}
+
+.comment-block > div:last-child {
+ font-size: 0.9rem;
+ color: var(--text-2);
+ line-height: 1.6;
+}
+
+/* ── QUIZ: REFERÊNCIA (RESPOSTA ABERTA) ── */
+.ref-block {
+ padding: 1rem;
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ max-height: 0;
+ overflow: hidden;
+ transition: max-height var(--t));
+}
+
+.ref-block.show {
+ max-height: 500px;
+}
+
+.ref-label {
+ font-family: var(--mono);
+ font-size: 0.62rem;
+ color: var(--muted);
+ letter-spacing: 0.1em;
+ text-transform: uppercase;
+ margin-bottom: 0.5rem;
+}
+
+.ref-block > div:last-child {
+ font-size: 0.9rem;
+ color: var(--text-2);
+ line-height: 1.6;
+}
+
+/* ── QUIZ: BOTÕES ── */
+.next-row {
+ display: flex;
+ justify-content: flex-end;
+ margin-top: 1.5rem;
+}
+
+.open-btns {
+ display: flex;
+ gap: 0.75rem;
+ justify-content: flex-end;
+}
+
+.btn {
+ padding: 0.6rem 1.2rem;
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ color: var(--text);
+ font-family: var(--serif);
+ font-size: 0.9rem;
+ cursor: pointer;
+ transition: all var(--t);
+ text-decoration: none;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.btn:hover:not(:disabled) {
+ border-color: var(--amber);
+ background: color-mix(in srgb, var(--amber) 10%, transparent);
+}
+
+.btn:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+.btn-ok {
+ border-color: #4ade80;
+ color: #4ade80;
+}
+
+.btn-ok:hover:not(:disabled) {
+ background: color-mix(in srgb, #4ade80 15%, transparent);
+}
+
+.btn-bad {
+ border-color: #ef4444;
+ color: #ef4444;
+}
+
+.btn-bad:hover:not(:disabled) {
+ background: color-mix(in srgb, #ef4444 15%, transparent);
+}
+
+/* ── QUIZ: RESULTADO ── */
+.res-score {
+ font-size: 3rem;
+ font-weight: 600;
+ color: var(--amber);
+ text-align: center;
+ margin-bottom: 0.5rem;
+}
+
+.res-subtitle {
+ font-size: 1rem;
+ color: var(--text-2);
+ text-align: center;
+ margin-bottom: 1.5rem;
+}
+
+.res-divider {
+ height: 1px;
+ background: var(--border);
+ margin: 1.5rem 0;
+}
+
+.res-stats {
+ display: flex;
+ justify-content: center;
+ gap: 3rem;
+ margin-bottom: 1.5rem;
+}
+
+.rs-item {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.rs-num {
+ font-size: 1.8rem;
+ font-weight: 600;
+}
+
+.rs-num.g {
+ color: #4ade80;
+}
+
+.rs-num.r {
+ color: #ef4444;
+}
+
+.rs-label {
+ font-family: var(--mono);
+ font-size: 0.62rem;
+ color: var(--muted);
+ letter-spacing: 0.1em;
+ text-transform: uppercase;
+}
+
+.res-msg {
+ padding: 1rem;
+ background: color-mix(in srgb, var(--amber) 5%, transparent);
+ border-left: 2px solid var(--amber);
+ border-radius: 0 4px 4px 0;
+ margin-bottom: 1.5rem;
+}
+
+.res-msg-label {
+ font-family: var(--mono);
+ font-size: 0.62rem;
+ color: var(--muted);
+ letter-spacing: 0.1em;
+ text-transform: uppercase;
+ margin-bottom: 0.5rem;
+}
+
+.res-msg-body {
+ font-size: 0.95rem;
+ color: var(--text-2);
+ line-height: 1.6;
+}
+
+.res-btns {
+ display: flex;
+ gap: 0.75rem;
+ justify-content: center;
+ flex-wrap: wrap;
+}
+
+/* ── EXTRAS DE DISCIPLINA (COLAPSÁVEIS) ── */
+.disc-extras-wrap {
+ margin: 1.5rem 0 2rem;
+}
+
+.disc-extra {
+ margin-bottom: 0.75rem;
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ overflow: hidden;
+}
+
+.disc-extra-summary {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0.75rem 1rem;
+ background: var(--surface);
+ cursor: pointer;
+ user-select: none;
+ transition: background var(--t);
+}
+
+.disc-extra-summary:hover {
+ background: color-mix(in srgb, var(--amber) 5%, transparent);
+}
+
+.disc-extra-label {
+ font-family: var(--mono);
+ font-size: 0.68rem;
+ color: var(--muted);
+ letter-spacing: 0.1em;
+ text-transform: uppercase;
+}
+
+.disc-extra-count {
+ font-family: var(--mono);
+ font-size: 0.62rem;
+ color: var(--text-2);
+ letter-spacing: 0.05em;
+}
+
+.disc-extra-chevron {
+ color: var(--text-2);
+ transition: transform var(--t);
+}
+
+.disc-extra[open] .disc-extra-chevron {
+ transform: rotate(180deg);
+}
+
+.disc-extra-body {
+ padding: 1rem;
+ border-top: 1px solid var(--border);
+}
+
+.disc-extra-row {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ padding: 0.5rem 0;
+ text-decoration: none;
+ color: var(--text-2);
+ transition: color var(--t);
+}
+
+.disc-extra-row:hover {
+ color: var(--text);
+}
+
+.disc-extra-icon {
+ font-size: 0.8rem;
+ color: var(--muted);
+ min-width: 1.2rem;
+}
+
+.disc-extra-name {
+ flex: 1;
+ font-size: 0.9rem;
+}
+
+.disc-extra-meta {
+ font-family: var(--mono);
+ font-size: 0.62rem;
+ color: var(--muted);
+ letter-spacing: 0.05em;
+ text-transform: uppercase;
+}
+
+.disc-extra-prose {
+ font-size: 0.9rem;
+ color: var(--text-2);
+}
+
+.disc-extra-prose p {
+ margin-bottom: 0.75rem;
+}
+
+.disc-extra-prose p:last-child {
+ margin-bottom: 0;
+}
+
+.disc-extra-kind-label {
+ font-family: var(--mono);
+ font-size: 0.62rem;
+ color: var(--muted);
+ letter-spacing: 0.1em;
+ text-transform: uppercase;
+ margin: 1rem 0 0.5rem;
+ padding-bottom: 0.5rem;
+ border-bottom: 1px dashed var(--border);
+}
+
+.disc-extra-kind-label:first-child {
+ margin-top: 0;
+}
+
+/* ── SUBJECT QUIZZES (LINKS PARA EXERCÍCIOS) ── */
+.subject-quizzes {
+ margin: 1.5rem 0 2rem;
+ padding: 1rem;
+ background: color-mix(in srgb, var(--amber) 3%, transparent);
+ border: 1px solid var(--border);
+ border-radius: 4px;
+}
+
+.sq-label {
+ font-family: var(--mono);
+ font-size: 0.62rem;
+ color: var(--muted);
+ letter-spacing: 0.1em;
+ text-transform: uppercase;
+ margin-bottom: 0.75rem;
+}
+
+.sq-list {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+}
+
+.sq-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 0.6rem 0.75rem;
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: 3px;
+ text-decoration: none;
+ color: var(--text);
+ transition: all var(--t);
+}
+
+.sq-item:hover {
+ border-color: var(--amber);
+ background: color-mix(in srgb, var(--amber) 8%, transparent);
+}
+
+.sq-name {
+ font-size: 0.9rem;
+ flex: 1;
+}
+
+.sq-meta {
+ font-family: var(--mono);
+ font-size: 0.62rem;
+ color: var(--muted);
+ letter-spacing: 0.05em;
+ text-transform: uppercase;
+ flex-shrink: 0;
+}
+
+/* ── SUBJECT LIST ── */
+.subject-list {
+ display: flex;
+ flex-direction: column;
+ gap: 0;
+}
+
+.subject-row {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 0.75rem 0;
+ border-bottom: 1px solid var(--border);
+ text-decoration: none;
+ color: var(--text);
+ transition: all var(--t);
+}
+
+.subject-row:last-child {
+ border-bottom: none;
+}
+
+.subject-row:hover {
+ color: var(--amber);
+ padding-left: 0.5rem;
+}
+
+.subject-row-name {
+ font-size: 0.95rem;
+ flex: 1;
+}
+
+.subject-row-meta {
+ font-family: var(--mono);
+ font-size: 0.62rem;
+ color: var(--muted);
+ letter-spacing: 0.1em;
+ text-transform: uppercase;
+ flex-shrink: 0;
+}
+
+.empty-note {
+ font-size: 0.9rem;
+ color: var(--muted);
+ font-style: italic;
+ padding: 1rem 0;
+}
+
+/* ── RESPONSIVIDADE ── */
+@media (max-width: 768px) {
+ main {
+ padding: 1.5rem;
+ }
+
+ .disc-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .res-stats {
+ gap: 2rem;
+ }
+
+ header {
+ padding: 1rem 1.5rem;
+ }
+}
+
+@media (max-width: 480px) {
+ main {
+ padding: 1rem;
+ }
+
+ .text-title {
+ font-size: 1.2rem;
+ }
+
+ .prose {
+ font-size: 0.95rem;
+ }
+
+ .options {
+ gap: 0.5rem;
+ }
+
+ .opt {
+ padding: 0.6rem 0.75rem;
+ min-height: 2.5rem;
+ }
+
+ .floater {
+ max-width: 280px;
+ }
+
+ .res-score {
+ font-size: 2.5rem;
+ }
+
+ .res-stats {
+ gap: 1.5rem;
+ }
+}
+
+/* ── FLASHCARD ── */
+.flashcard-area {
+ display: flex;
+ flex-direction: column;
+ gap: 1.5rem;
+}
+
+.flashcard {
+ perspective: 1000px;
+ cursor: pointer;
+ height: 300px;
+ position: relative;
+}
+
+.flashcard-front,
+.flashcard-back {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ backface-visibility: hidden;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border: 2px solid var(--amber);
+ border-radius: 4px;
+ padding: 2rem;
+ text-align: center;
+ transition: transform 0.6s;
+}
+
+.flashcard-front {
+ background: var(--surface);
+ color: var(--text);
+ transform: rotateY(0deg);
+}
+
+.flashcard-back {
+ background: var(--amber);
+ color: #0f0f13;
+ transform: rotateY(180deg);
+}
+
+.flashcard.flipped .flashcard-front {
+ transform: rotateY(180deg);
+}
+
+.flashcard.flipped .flashcard-back {
+ transform: rotateY(0deg);
+}
+
+.flashcard-hint {
+ font-size: 1.2rem;
+ opacity: 0.7;
+}
+
+.flashcard-btns {
+ display: flex;
+ gap: 0.75rem;
+ justify-content: center;
+}
+
+/* ── FILTRO NA HOME ── */
+.filter-container {
+ margin-bottom: 2rem;
+ display: flex;
+ gap: 1rem;
+ align-items: center;
+}
+
+.filter-input {
+ flex: 1;
+ padding: 0.75rem 1rem;
+ background: var(--surface);
+ color: var(--text);
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ font-size: 1rem;
+ font-family: var(--serif);
+}
+
+.filter-input:focus {
+ outline: none;
+ border-color: var(--amber);
+}
+
+.filter-count {
+ color: var(--amber);
+ font-size: 0.9rem;
+ white-space: nowrap;
+}
+
+/* ── DASHBOARD ── */
+.dashboard-container {
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ padding: 2rem;
+ margin-bottom: 2rem;
+}
+
+.dashboard-overview {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 1rem;
+ margin: 1.5rem 0;
+}
+
+.stat-card {
+ background: #0f0f13;
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ padding: 1.5rem;
+ text-align: center;
+}
+
+.stat-label {
+ font-size: 0.9rem;
+ color: var(--muted);
+ margin-bottom: 0.5rem;
+}
+
+.stat-value {
+ font-size: 2rem;
+ font-weight: 600;
+ color: var(--amber);
+}
+
+.dashboard-disciplines {
+ margin-top: 2rem;
+}
+
+.disc-stat {
+ margin-bottom: 1.5rem;
+ padding-bottom: 1rem;
+ border-bottom: 1px solid var(--border);
+}
+
+.disc-name {
+ font-weight: 600;
+ margin-bottom: 0.5rem;
+}
+
+.disc-bar {
+ height: 8px;
+ background: #0f0f13;
+ border-radius: 4px;
+ overflow: hidden;
+ margin-bottom: 0.5rem;
+}
+
+.disc-progress {
+ height: 100%;
+ background: #4ade80;
+ transition: width 0.3s;
+}
+
+.disc-info {
+ display: flex;
+ justify-content: space-between;
+ font-size: 0.85rem;
+ color: var(--muted);
+}
+
+.dashboard-actions {
+ margin-top: 2rem;
+ text-align: center;
+}
+
+/* ── MODO ESCURO TOGGLE ── */
+#dark-mode-toggle {
+ background: none;
+ border: none;
+ font-size: 1.5rem;
+ cursor: pointer;
+ margin-left: auto;
+ padding: 0.5rem;
+ border-radius: 4px;
+ transition: background-color var(--t);
+}
+
+#dark-mode-toggle:hover {
+ background-color: var(--border);
+}
+
+/* ── PRINT-FRIENDLY ── */
+@media print {
+ header,
+ .filter-container,
+ .dashboard-container,
+ .btn,
+ .next-row,
+ #dark-mode-toggle,
+ .flashcard-btns,
+ .open-btns {
+ display: none;
+ }
+
+ body {
+ background: white;
+ color: black;
+ padding: 0;
+ }
+
+ main {
+ max-width: 100%;
+ padding: 0;
+ }
+
+ .q-text,
+ .flashcard-back {
+ page-break-inside: avoid;
+ }
+
+ .flashcard {
+ border: 1px solid black;
+ background: white;
+ color: black;
+ }
+
+ .flashcard-front,
+ .flashcard-back {
+ position: static;
+ transform: none;
+ border: none;
+ background: white;
+ color: black;
+ }
+}
+
+/* ── RESPONSIVIDADE MOBILE ── */
+@media (max-width: 768px) {
+ .dashboard-overview {
+ grid-template-columns: 1fr;
+ }
+
+ .flashcard {
+ height: 250px;
+ }
+}
+
+@media (max-width: 480px) {
+ .flashcard {
+ height: 200px;
+ }
+
+ .filter-container {
+ flex-direction: column;
+ }
+
+ .filter-input {
+ width: 100%;
+ }
+}
+
+/* ── QUIZ INLINE (dentro do assunto) ── */
+.inline-quiz-wrap {
+ margin: 2.5rem 0 1.5rem;
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ overflow: hidden;
+}
+
+.inline-quiz-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0.9rem 1.2rem;
+ background: var(--surface);
+ border-bottom: 1px solid var(--border);
+}
+
+.inline-quiz-head .sq-label {
+ margin: 0;
+ padding: 0;
+ border: none;
+}
+
+.inline-quiz-link {
+ font-family: var(--mono);
+ font-size: 0.65rem;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ color: var(--muted);
+ text-decoration: none;
+ transition: color var(--t);
+}
+
+.inline-quiz-link:hover {
+ color: var(--amber);
+}
+
+#inline-quiz-body,
+#inline-quiz-player,
+#inline-quiz-result {
+ padding: 1.2rem;
+}
+
+.btn-start-quiz {
+ width: 100%;
+ padding: 0.85rem;
+ justify-content: center;
+ background: color-mix(in srgb, var(--amber) 8%, transparent);
+ border-color: var(--amber);
+ color: var(--amber);
+}
+
+.btn-start-quiz:hover {
+ background: color-mix(in srgb, var(--amber) 15%, transparent);
+}
+
+/* ── TABELA NA PROSE ── */
+.prose table {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: 0.88rem;
+ margin: 1.5rem 0;
+}
+
+.prose th {
+ font-family: var(--mono);
+ font-size: 0.7rem;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ color: var(--muted);
+ padding: 0.55rem 0.75rem;
+ border-bottom: 1px solid var(--border-2);
+ text-align: left;
+}
+
+.prose td {
+ padding: 0.55rem 0.75rem;
+ border-bottom: 1px solid var(--border);
+ color: var(--text-2);
+ vertical-align: top;
+}
+
+.prose tr:last-child td {
+ border-bottom: none;
+}
+
+.prose tr:hover td {
+ background: color-mix(in srgb, var(--amber) 3%, transparent);
+}
diff --git a/static/manifest.json b/static/manifest.json
new file mode 100644
index 0000000..6460cbe
--- /dev/null
+++ b/static/manifest.json
@@ -0,0 +1,34 @@
+{
+ "name": "Caderno de Estudos - Direito",
+ "short_name": "Caderno",
+ "description": "Caderno interativo de estudos com quizzes e simulados de Direito",
+ "start_url": "/",
+ "scope": "/",
+ "display": "standalone",
+ "background_color": "#0f0f13",
+ "theme_color": "#c9a961",
+ "orientation": "portrait-primary",
+ "icons": [
+ {
+ "src": "data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 192 192'><rect fill='%230f0f13' width='192' height='192'/><text x='96' y='120' font-size='80' font-weight='bold' text-anchor='middle' fill='%23c9a961' font-family='monospace'>D</text></svg>",
+ "sizes": "192x192",
+ "type": "image/svg+xml",
+ "purpose": "any"
+ },
+ {
+ "src": "data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 192 192'><rect fill='%230f0f13' width='192' height='192'/><text x='96' y='120' font-size='80' font-weight='bold' text-anchor='middle' fill='%23c9a961' font-family='monospace'>D</text></svg>",
+ "sizes": "512x512",
+ "type": "image/svg+xml",
+ "purpose": "maskable"
+ }
+ ],
+ "categories": ["education", "productivity"],
+ "screenshots": [
+ {
+ "src": "data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 540 720'><rect fill='%230f0f13' width='540' height='720'/><text x='270' y='360' font-size='60' font-weight='bold' text-anchor='middle' fill='%23c9a961' font-family='monospace'>Caderno</text></svg>",
+ "sizes": "540x720",
+ "type": "image/svg+xml",
+ "form_factor": "narrow"
+ }
+ ]
+}
diff --git a/static/offline.html b/static/offline.html
new file mode 100644
index 0000000..0cb1bdc
--- /dev/null
+++ b/static/offline.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html lang="pt-BR">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>Sem conexão · Caderno</title>
+<style>
+ body { background:#0f0f13; color:#dedad4; font-family:'Georgia',serif; display:flex;
+ align-items:center; justify-content:center; min-height:100vh; margin:0; text-align:center; padding:2rem; }
+ h1 { font-size:1.2rem; margin-bottom:.75rem; }
+ p { font-size:.9rem; color:#6a6a75; }
+ a { color:#c9a961; }
+</style>
+</head>
+<body>
+ <div>
+ <h1>Você está offline</h1>
+ <p>Páginas já visitadas estão disponíveis em cache.<br>
+ <a href="/">Voltar ao início</a></p>
+ </div>
+</body>
+</html>
diff --git a/static/quiz-engine.js b/static/quiz-engine.js
new file mode 100644
index 0000000..03e50a5
--- /dev/null
+++ b/static/quiz-engine.js
@@ -0,0 +1,170 @@
+/**
+ * Quiz Engine - Renderiza questões na página
+ */
+
+function _renderQuestion(q) {
+ if (!q) return;
+ const body = document.getElementById('quiz-body');
+ if (!body) return;
+
+ const counter = document.getElementById('q-counter');
+ if (counter) {
+ counter.textContent = `${_qState.qIndex + 1} de ${_qState.questions.length}`;
+ }
+
+ const prog = document.getElementById('prog');
+ if (prog) {
+ const pct = ((_qState.qIndex + 1) / _qState.questions.length) * 100;
+ prog.style.width = pct + '%';
+ }
+
+ if (q.type === 'mc') {
+ body.innerHTML = `<div class="mc-area fade">
+ <p class="q-text">${md(q.text)}</p>
+ <div class="options">
+ ${q.options
+ .map(
+ (o, i) => `<button class="opt" data-opt="${esc(o)}" data-answer="${esc(q.answer)}" onclick="selectOpt(this)" aria-label="Opção ${LETRAS[i]}: ${o}">
+ <span class="opt-letter">${LETRAS[i]}</span>
+ <span>${md(o)}</span>
+ </button>`
+ )
+ .join('')}
+ </div>
+ <div class="feedback-line" id="feedback" aria-live="polite" aria-atomic="true"><span class="feedback-dot"></span><span id="feedback-msg"></span></div>
+ ${q.comment ? `<div class="comment-block" id="comment"><div class="comment-label">comentário</div><div>${md(q.comment)}</div></div>` : ''}
+ </div>`;
+ } else {
+ // Modo Flashcard para questões abertas
+ const isRevealed = _qState.flipped || false;
+ body.innerHTML = `<div class="flashcard-area fade">
+ <p class="q-text">${md(q.text)}</p>
+ <div class="flashcard-simple" id="flashcard">
+ ${!isRevealed ? `<button class="btn btn-reveal" onclick="flipFlashcard()">Revelar Resposta</button>` : `<div class="ref-label">resposta de referência</div><div class="ref-content">${md(q.reference)}</div>`}
+ </div>
+ <div class="feedback-line" id="feedback" aria-live="polite" aria-atomic="true"><span class="feedback-dot"></span><span id="feedback-msg"></span></div>
+ ${q.comment ? `<div class="comment-block" id="comment"><div class="comment-label">comentário</div><div>${md(q.comment)}</div></div>` : ''}
+ <div class="flashcard-btns" id="flashcard-btns">
+ <button class="btn btn-ok" onclick="markFlashcardOk()">✓ Acertei</button>
+ <button class="btn btn-bad" onclick="markFlashcardBad()">✗ Errei</button>
+ </div>
+ </div>`;
+ }
+}
+
+function selectOpt(btn) {
+ if (_qState.answered) return;
+ _qState.answered = true;
+ const chosen = btn.dataset.opt;
+ const correct = btn.dataset.answer;
+ btn.closest('.options').querySelectorAll('.opt').forEach((b) => {
+ b.disabled = true;
+ if (b.dataset.opt === correct) b.classList.add('correct');
+ else if (b === btn) b.classList.add('wrong');
+ else b.classList.add('revealed');
+ });
+ if (chosen === correct) {
+ _qState.acertos++;
+ setFeedback(true, 'Correto');
+ } else {
+ _qState.erros++;
+ setFeedback(false, 'Incorreto — a correta está destacada');
+ }
+ document.getElementById('comment')?.classList.add('show');
+ document.getElementById('btn-next').style.display = '';
+}
+
+function flipFlashcard() {
+ const card = document.getElementById('flashcard');
+ if (card) {
+ _qState.flipped = true;
+ _renderQuestion(_qState.questions[_qState.qIndex]);
+ }
+}
+
+function markFlashcardOk() {
+ _qState.answered = true;
+ _qState.acertos++;
+ setFeedback(true, 'Marcado como acertado');
+ document.getElementById('comment')?.classList.add('show');
+ document.getElementById('flashcard-btns').innerHTML = '';
+ document.getElementById('btn-next').style.display = '';
+}
+
+function markFlashcardBad() {
+ _qState.answered = true;
+ _qState.erros++;
+ setFeedback(false, 'Marcado como errado');
+ document.getElementById('comment')?.classList.add('show');
+ document.getElementById('flashcard-btns').innerHTML = '';
+ document.getElementById('btn-next').style.display = '';
+}
+
+function setFeedback(ok, msg) {
+ const fb = document.getElementById('feedback');
+ fb.className = 'feedback-line show ' + (ok ? 'ok' : 'bad');
+ document.getElementById('feedback-msg').textContent = msg;
+}
+
+function _buildResult() {
+ const { acertos, erros, questions } = _qState;
+ const total = questions.length;
+ const pct = total ? Math.round((acertos / total) * 100) : 0;
+ let nivel, detalhe;
+ if (pct >= 90) {
+ nivel = 'Excelente';
+ detalhe = 'Domínio consistente do conteúdo. Mantenha revisão periódica.';
+ } else if (pct >= 70) {
+ nivel = 'Bom';
+ detalhe = 'Base sólida com pontos a refinar. Revise os erros e releia os comentários.';
+ } else if (pct >= 50) {
+ nivel = 'Regular';
+ detalhe = 'Compreensão parcial, com lacunas importantes. Retome o material teórico.';
+ } else {
+ nivel = 'Insuficiente';
+ detalhe = 'Os conceitos centrais ainda não estão consolidados. Estude o tema e refaça.';
+ }
+ document.getElementById('res-pct').textContent = pct + '%';
+ document.getElementById('res-a').textContent = acertos;
+ document.getElementById('res-a2').textContent = acertos;
+ document.getElementById('res-e').textContent = erros;
+ document.getElementById('res-t').textContent = total;
+ document.getElementById('res-msg-body').innerHTML = `<strong>${nivel}</strong> — ${acertos} de ${total} (${pct}%). ${detalhe}`;
+}
+
+function _saveProgress(key) {
+ try {
+ localStorage.setItem(PROGRESS_PREFIX + key, JSON.stringify({
+ questions: _qState.questions,
+ qIndex: _qState.qIndex,
+ acertos: _qState.acertos,
+ erros: _qState.erros,
+ flipped: _qState.flipped,
+ }));
+ } catch (e) {}
+}
+
+function _loadProgress(key) {
+ try {
+ const s = localStorage.getItem(PROGRESS_PREFIX + key);
+ if (!s) return false;
+ const d = JSON.parse(s);
+ if (!d.questions || !d.questions.length) return false;
+ Object.assign(_qState, {
+ questions: d.questions,
+ qIndex: d.qIndex || 0,
+ acertos: d.acertos || 0,
+ erros: d.erros || 0,
+ flipped: d.flipped || false,
+ });
+ return true;
+ } catch (e) {
+ return false;
+ }
+}
+
+function _clearProgress(key) {
+ try {
+ localStorage.removeItem(PROGRESS_PREFIX + key);
+ } catch (e) {}
+}
diff --git a/static/quiz-parser.js b/static/quiz-parser.js
new file mode 100644
index 0000000..c62beb5
--- /dev/null
+++ b/static/quiz-parser.js
@@ -0,0 +1,144 @@
+/**
+ * Quiz Parser - Converte sintaxe customizada em JSON estruturado
+ * Sintaxe suportada:
+ * ? Enunciado da questão
+ * - Opção A / - Opção B
+ * = Opção correta (incluída nas opções automaticamente se ausente)
+ * > Comentário (pode repetir; linha vazia separa parágrafos)
+ *
+ * ? Questão aberta
+ * [ Resposta de referência (pode repetir)
+ * > Comentário
+ *
+ * --- ou linha em branco separam questões
+ * # linha ignorada
+ */
+
+function parseQuizBody(raw) {
+ if (!raw || !raw.trim()) return [];
+ const lines = raw.replace(/\r\n/g, '\n').split('\n');
+ const result = [];
+ let enunciado = '', tipo = '', opcoes = [], gabarito = '', referencia = '', comentario = '';
+
+ function flush() {
+ if (!enunciado.trim()) return;
+ const q = { type: tipo || 'mc', text: enunciado.trim() };
+ if (tipo === 'mc') {
+ q.options = opcoes;
+ q.answer = gabarito;
+ } else {
+ q.reference = referencia.trim();
+ }
+ if (comentario.trim()) q.comment = comentario.trim();
+ result.push(q);
+ enunciado = '';
+ tipo = '';
+ opcoes = [];
+ gabarito = '';
+ referencia = '';
+ comentario = '';
+ }
+
+ for (const rawLine of lines) {
+ const line = rawLine.replace(/\r$/, '').trimEnd();
+ if (/^---+\s*$/.test(line)) {
+ flush();
+ continue;
+ }
+ if (line.trim() === '') continue;
+ const mark = line[0];
+ const content = line.length > 1 ? line.slice(2) : '';
+ switch (mark) {
+ case '#':
+ break;
+ case '?':
+ flush();
+ enunciado = content;
+ break;
+ case '-':
+ opcoes.push(content);
+ tipo = 'mc';
+ break;
+ case '=':
+ gabarito = content;
+ tipo = 'mc';
+ if (content && !opcoes.includes(content)) opcoes.push(content);
+ break;
+ case '[':
+ referencia += (referencia ? '\n' : '') + content;
+ tipo = 'open';
+ break;
+ case '>':
+ comentario += (comentario ? '\n' : '') + content;
+ break;
+ default:
+ if (enunciado && !tipo) enunciado += '\n' + line;
+ else if (tipo === 'open' && referencia) referencia += '\n' + line;
+ }
+ }
+ flush();
+ return result;
+}
+
+/* ── UTILITÁRIOS BASE ── */
+const LETRAS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
+
+function shuffle(arr) {
+ const a = [...arr];
+ for (let i = a.length - 1; i > 0; i--) {
+ const j = Math.floor(Math.random() * (i + 1));
+ [a[i], a[j]] = [a[j], a[i]];
+ }
+ return a;
+}
+
+function prepareQuizQuestions(questions) {
+ const letterCounts = [];
+ let previousCorrectIndex = -1;
+ return questions.map((q) => {
+ if (!q || q.type !== 'mc' || !Array.isArray(q.options) || q.options.length < 2) {
+ return q;
+ }
+ const answerIndex = q.options.findIndex((o) => o === q.answer);
+ if (answerIndex < 0) {
+ return { ...q, options: shuffle(q.options) };
+ }
+ const optionCount = q.options.length;
+ const availableIndexes = Array.from({ length: optionCount }, (_, i) => i);
+ const lowestCount = Math.min(...availableIndexes.map((i) => letterCounts[i] || 0));
+ let candidates = availableIndexes.filter((i) => (letterCounts[i] || 0) === lowestCount);
+ if (optionCount > 1 && candidates.length > 1) {
+ candidates = candidates.filter((i) => i !== previousCorrectIndex);
+ }
+ const correctIndex = shuffle(candidates)[0] ?? 0;
+ const otherOptions = shuffle(q.options.filter((_, i) => i !== answerIndex));
+ const arrangedOptions = [...otherOptions];
+ arrangedOptions.splice(correctIndex, 0, q.options[answerIndex]);
+ letterCounts[correctIndex] = (letterCounts[correctIndex] || 0) + 1;
+ previousCorrectIndex = correctIndex;
+ return { ...q, options: arrangedOptions };
+ });
+}
+
+/* Para uso em atributos HTML (onclick="...") */
+function esc(s) {
+ return String(s ?? '')
+ .replace(/&/g, '&amp;')
+ .replace(/</g, '&lt;')
+ .replace(/>/g, '&gt;')
+ .replace(/"/g, '&quot;')
+ .replace(/'/g, '&#39;');
+}
+
+/* Para innerHTML: escapa HTML e renderiza Markdown inline.
+ Suporta: **negrito**, *itálico*, `código`, \n→<br> */
+function md(s) {
+ return String(s ?? '')
+ .replace(/&/g, '&amp;')
+ .replace(/</g, '&lt;')
+ .replace(/>/g, '&gt;')
+ .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
+ .replace(/\*(.+?)\*/g, '<em>$1</em>')
+ .replace(/`(.+?)`/g, '<code>$1</code>')
+ .replace(/\n/g, '<br>');
+}
diff --git a/static/quiz-runtime.js b/static/quiz-runtime.js
new file mode 100644
index 0000000..d6e824d
--- /dev/null
+++ b/static/quiz-runtime.js
@@ -0,0 +1,92 @@
+/**
+ * Quiz Runtime — controla o fluxo de quiz/simulado na página dedicada
+ */
+
+const PROGRESS_PREFIX = window.PROGRESS_PREFIX = 'quiz_progress_';
+
+// Estado global do quiz (página de quiz dedicada)
+let _qState = {
+ questions: [],
+ qIndex: 0,
+ acertos: 0,
+ erros: 0,
+ answered: false,
+ flipped: false,
+};
+
+function nextQuestion() {
+ _qState.qIndex++;
+ _qState.answered = false;
+ _qState.flipped = false;
+ if (_qState.qIndex >= _qState.questions.length) {
+ showResult();
+ return;
+ }
+ const body = document.getElementById('quiz-body');
+ if (!body) return;
+ body.classList.add('fade-out');
+ setTimeout(() => {
+ body.classList.remove('fade-out');
+ _renderQuestion(_qState.questions[_qState.qIndex]);
+ _saveProgress(window.QUIZ_KEY);
+ }, 100);
+}
+
+function showResult() {
+ const wrap = document.getElementById('quiz-wrap');
+ const result = document.getElementById('quiz-result');
+ if (wrap) wrap.style.display = 'none';
+ if (result) result.style.display = '';
+ _buildResult();
+ _clearProgress(window.QUIZ_KEY);
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+}
+
+function restartQuiz() {
+ _clearProgress(window.QUIZ_KEY);
+ _qState.questions = prepareQuizQuestions(parseQuizBody(
+ typeof RAW_QUIZ !== 'undefined' ? RAW_QUIZ : ''
+ ));
+ _qState.qIndex = 0;
+ _qState.acertos = 0;
+ _qState.erros = 0;
+ _qState.answered = false;
+ _qState.flipped = false;
+ const wrap = document.getElementById('quiz-wrap');
+ const result = document.getElementById('quiz-result');
+ if (result) result.style.display = 'none';
+ if (wrap) wrap.style.display = '';
+ _renderQuestion(_qState.questions[0]);
+}
+
+function initQuiz() {
+ // Só inicializa na página de quiz (onde existe #quiz-body)
+ if (!document.getElementById('quiz-body')) return;
+
+ const raw = typeof RAW_QUIZ !== 'undefined' ? RAW_QUIZ : '';
+ const key = window.QUIZ_KEY;
+
+ if (!_loadProgress(key)) {
+ _qState.questions = prepareQuizQuestions(parseQuizBody(raw));
+ _qState.qIndex = 0;
+ _qState.acertos = 0;
+ _qState.erros = 0;
+ }
+
+ _qState.answered = false;
+ _qState.flipped = false;
+
+ const wrap = document.getElementById('quiz-wrap');
+ const result = document.getElementById('quiz-result');
+ if (result) result.style.display = 'none';
+ if (wrap) wrap.style.display = '';
+
+ if (_qState.qIndex >= _qState.questions.length) {
+ showResult();
+ return;
+ }
+
+ _renderQuestion(_qState.questions[_qState.qIndex]);
+}
+
+document.addEventListener('DOMContentLoaded', initQuiz);
diff --git a/static/sw.js b/static/sw.js
new file mode 100644
index 0000000..71f0e5f
--- /dev/null
+++ b/static/sw.js
@@ -0,0 +1,68 @@
+/* Service Worker — caderno de estudos
+ Estratégia: cache-first para assets estáticos, network-first para HTML
+ Versão: 3 (com suporte a stale-while-revalidate) */
+const CACHE = 'caderno-v3';
+const STATIC = [
+ '/',
+ '/offline.html'
+];
+
+self.addEventListener('install', e => {
+ e.waitUntil(
+ caches.open(CACHE).then(c => c.addAll(STATIC)).then(() => self.skipWaiting())
+ );
+});
+
+self.addEventListener('activate', e => {
+ e.waitUntil(
+ caches.keys().then(keys =>
+ Promise.all(keys.filter(k => k !== CACHE).map(k => caches.delete(k)))
+ ).then(() => self.clients.claim())
+ );
+});
+
+self.addEventListener('fetch', e => {
+ const url = new URL(e.request.url);
+
+ // Ignora requisições externas (fontes Google, etc.)
+ if (url.origin !== location.origin) return;
+
+ if (e.request.mode === 'navigate') {
+ // HTML: network-first, fallback para offline.html
+ e.respondWith(
+ fetch(e.request)
+ .then(res => {
+ const clone = res.clone();
+ caches.open(CACHE).then(c => c.put(e.request, clone));
+ return res;
+ })
+ .catch(() => caches.match(e.request).then(r => r || caches.match('/offline.html')))
+ );
+ } else {
+ // Assets (CSS, JS, imagens): stale-while-revalidate
+ e.respondWith(
+ caches.match(e.request).then(cached => {
+ // Se estiver em cache, retorna imediatamente e atualiza em background
+ if (cached) {
+ // Atualizar em background
+ fetch(e.request).then(res => {
+ if (res && res.status === 200) {
+ const clone = res.clone();
+ caches.open(CACHE).then(c => c.put(e.request, clone));
+ }
+ });
+ return cached;
+ }
+
+ // Se não estiver em cache, busca da rede
+ return fetch(e.request).then(res => {
+ if (res && res.status === 200) {
+ const clone = res.clone();
+ caches.open(CACHE).then(c => c.put(e.request, clone));
+ }
+ return res;
+ });
+ })
+ );
+ }
+});
diff --git a/static/toc.js b/static/toc.js
new file mode 100644
index 0000000..950533e
--- /dev/null
+++ b/static/toc.js
@@ -0,0 +1,52 @@
+/**
+ * Table of Contents - Gera sumário automático de headings
+ */
+
+document.addEventListener('DOMContentLoaded', () => {
+ const prose = document.getElementById('prose');
+ const tocNav = document.getElementById('toc-nav');
+ const tocList = document.getElementById('toc-list');
+
+ if (!prose || !tocNav || !tocList) return;
+
+ const headings = prose.querySelectorAll('h2, h3');
+ if (!headings.length) return;
+
+ const used = new Set();
+
+ headings.forEach((h) => {
+ // Gerar ID único baseado no texto
+ let base = h.textContent
+ .trim()
+ .normalize('NFD')
+ .replace(/[\u0300-\u036f]/g, '')
+ .toLowerCase()
+ .replace(/[^a-z0-9]+/g, '-')
+ .replace(/^-+|-+$/g, '');
+
+ let id = base || 'sec';
+ let i = 2;
+ while (used.has(id)) id = base + '-' + i++;
+
+ h.id = id;
+ used.add(id);
+
+ // Criar item no sumário
+ const li = document.createElement('li');
+ li.className = 'toc-' + h.tagName.toLowerCase();
+
+ const a = document.createElement('a');
+ a.href = '#' + id;
+ a.textContent = h.textContent;
+ a.addEventListener('click', (e) => {
+ e.preventDefault();
+ h.scrollIntoView({ behavior: 'smooth', block: 'start' });
+ });
+
+ li.appendChild(a);
+ tocList.appendChild(li);
+ });
+
+ // Mostrar sumário se houver conteúdo
+ tocNav.style.display = '';
+});