diff --git a/api.php b/api.php index 95635c9..db42875 100644 --- a/api.php +++ b/api.php @@ -1912,6 +1912,100 @@ switch ($action) { echo json_encode(['success' => true]); break; + // ---------- OHJEET (GUIDES) ---------- + case 'guides': + requireAuth(); + $companyId = requireCompany(); + echo json_encode(dbLoadGuides($companyId)); + break; + + case 'guide': + requireAuth(); + requireCompany(); + $id = $_GET['id'] ?? ''; + $guide = dbLoadGuide($id); + if (!$guide) { http_response_code(404); echo json_encode(['error' => 'Ohjetta ei löydy']); exit; } + echo json_encode($guide); + break; + + case 'guide_save': + requireAuth(); + requireAdmin(); + $companyId = requireCompany(); + if ($method !== 'POST') break; + $input = json_decode(file_get_contents('php://input'), true); + $isNew = empty($input['id']); + $guide = [ + 'id' => $input['id'] ?? generateId(), + 'category_id' => $input['category_id'] ?? null, + 'title' => trim($input['title'] ?? ''), + 'content' => $input['content'] ?? '', + 'tags' => trim($input['tags'] ?? ''), + 'author' => $isNew ? currentUser() : ($input['author'] ?? currentUser()), + 'pinned' => !empty($input['pinned']), + 'luotu' => $isNew ? date('Y-m-d H:i:s') : ($input['luotu'] ?? date('Y-m-d H:i:s')), + 'muokattu' => $isNew ? null : date('Y-m-d H:i:s'), + 'muokkaaja' => $isNew ? '' : currentUser(), + ]; + if (empty($guide['title'])) { + http_response_code(400); + echo json_encode(['error' => 'Otsikko vaaditaan']); + exit; + } + dbSaveGuide($companyId, $guide); + dbAddLog($companyId, currentUser(), $isNew ? 'guide_create' : 'guide_update', $guide['id'], $guide['title'], ($isNew ? 'Loi' : 'Muokkasi') . ' ohjeen'); + echo json_encode($guide); + break; + + case 'guide_delete': + requireAuth(); + requireAdmin(); + $companyId = requireCompany(); + if ($method !== 'POST') break; + $input = json_decode(file_get_contents('php://input'), true); + $id = $input['id'] ?? ''; + $guide = dbLoadGuide($id); + dbDeleteGuide($id); + dbAddLog($companyId, currentUser(), 'guide_delete', $id, $guide ? $guide['title'] : '', 'Poisti ohjeen'); + echo json_encode(['success' => true]); + break; + + case 'guide_categories': + requireAuth(); + $companyId = requireCompany(); + echo json_encode(dbLoadGuideCategories($companyId)); + break; + + case 'guide_category_save': + requireAuth(); + requireAdmin(); + $companyId = requireCompany(); + if ($method !== 'POST') break; + $input = json_decode(file_get_contents('php://input'), true); + $cat = [ + 'id' => $input['id'] ?? generateId(), + 'nimi' => trim($input['nimi'] ?? ''), + 'sort_order' => (int)($input['sort_order'] ?? 0), + ]; + if (empty($cat['nimi'])) { + http_response_code(400); + echo json_encode(['error' => 'Kategorian nimi vaaditaan']); + exit; + } + dbSaveGuideCategory($companyId, $cat); + echo json_encode($cat); + break; + + case 'guide_category_delete': + requireAuth(); + requireAdmin(); + $companyId = requireCompany(); + if ($method !== 'POST') break; + $input = json_decode(file_get_contents('php://input'), true); + dbDeleteGuideCategory($input['id'] ?? ''); + echo json_encode(['success' => true]); + break; + // ---------- ARCHIVE ---------- case 'archived_customers': requireAuth(); diff --git a/db.php b/db.php index 68e46bd..085a3a4 100644 --- a/db.php +++ b/db.php @@ -419,6 +419,32 @@ function initDatabase(): void { FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE, INDEX idx_company_customer (company_id, customer_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", + + "CREATE TABLE IF NOT EXISTS guide_categories ( + id VARCHAR(20) PRIMARY KEY, + company_id VARCHAR(50) NOT NULL, + nimi VARCHAR(255) NOT NULL, + sort_order INT DEFAULT 0, + FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE, + INDEX idx_company (company_id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", + + "CREATE TABLE IF NOT EXISTS guides ( + id VARCHAR(20) PRIMARY KEY, + company_id VARCHAR(50) NOT NULL, + category_id VARCHAR(20) DEFAULT NULL, + title VARCHAR(500) NOT NULL, + content LONGTEXT, + tags VARCHAR(500) DEFAULT '', + author VARCHAR(100) DEFAULT '', + pinned TINYINT(1) DEFAULT 0, + luotu DATETIME, + muokattu DATETIME NULL, + muokkaaja VARCHAR(100) DEFAULT '', + FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE, + INDEX idx_company (company_id), + INDEX idx_category (category_id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", ]; foreach ($tables as $i => $sql) { @@ -951,6 +977,70 @@ function dbDeleteIpam(string $id): void { _dbExecute("DELETE FROM ipam WHERE id = ?", [$id]); } +// ==================== OHJEET (GUIDES) ==================== + +function dbLoadGuideCategories(string $companyId): array { + return _dbFetchAll("SELECT * FROM guide_categories WHERE company_id = ? ORDER BY sort_order, nimi", [$companyId]); +} + +function dbSaveGuideCategory(string $companyId, array $cat): void { + _dbExecute(" + INSERT INTO guide_categories (id, company_id, nimi, sort_order) + VALUES (?, ?, ?, ?) + ON DUPLICATE KEY UPDATE nimi = VALUES(nimi), sort_order = VALUES(sort_order) + ", [$cat['id'], $companyId, $cat['nimi'] ?? '', $cat['sort_order'] ?? 0]); +} + +function dbDeleteGuideCategory(string $catId): void { + _dbExecute("DELETE FROM guide_categories WHERE id = ?", [$catId]); +} + +function dbLoadGuides(string $companyId): array { + $rows = _dbFetchAll(" + SELECT g.*, gc.nimi AS category_name + FROM guides g + LEFT JOIN guide_categories gc ON g.category_id = gc.id + WHERE g.company_id = ? + ORDER BY g.pinned DESC, g.muokattu DESC, g.luotu DESC + ", [$companyId]); + foreach ($rows as &$r) { + $r['pinned'] = (bool)$r['pinned']; + } + return $rows; +} + +function dbLoadGuide(string $guideId): ?array { + $rows = _dbFetchAll(" + SELECT g.*, gc.nimi AS category_name + FROM guides g + LEFT JOIN guide_categories gc ON g.category_id = gc.id + WHERE g.id = ? + ", [$guideId]); + if (empty($rows)) return null; + $r = $rows[0]; + $r['pinned'] = (bool)$r['pinned']; + return $r; +} + +function dbSaveGuide(string $companyId, array $g): void { + _dbExecute(" + INSERT INTO guides (id, company_id, category_id, title, content, tags, author, pinned, luotu, muokattu, muokkaaja) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE + category_id = VALUES(category_id), title = VALUES(title), content = VALUES(content), + tags = VALUES(tags), pinned = VALUES(pinned), muokattu = VALUES(muokattu), muokkaaja = VALUES(muokkaaja) + ", [ + $g['id'], $companyId, !empty($g['category_id']) ? $g['category_id'] : null, + $g['title'] ?? '', $g['content'] ?? '', $g['tags'] ?? '', + $g['author'] ?? '', $g['pinned'] ? 1 : 0, + $g['luotu'] ?? date('Y-m-d H:i:s'), $g['muokattu'] ?? null, $g['muokkaaja'] ?? '' + ]); +} + +function dbDeleteGuide(string $guideId): void { + _dbExecute("DELETE FROM guides WHERE id = ?", [$guideId]); +} + // ==================== LIIDIT ==================== function dbLoadLeads(string $companyId): array { diff --git a/index.html b/index.html index aceea91..8d7a880 100644 --- a/index.html +++ b/index.html @@ -81,6 +81,7 @@ + @@ -325,6 +326,107 @@ + +
+
+ + +
+
+ +
+ + + +
+
+
+ +
+ + + + + + + +
+
+
@@ -779,6 +881,9 @@ + @@ -1316,6 +1421,23 @@
+ + + diff --git a/script.js b/script.js index 90f4c96..1ab6f06 100644 --- a/script.js +++ b/script.js @@ -195,7 +195,7 @@ async function showDashboard() { // Avaa oikea tabi URL-hashin perusteella (tai customers oletuks) const hash = window.location.hash.replace('#', ''); const [mainHash, subHash] = hash.split('/'); - const validTabs = ['customers', 'leads', 'tekniikka', 'archive', 'changelog', 'support', 'users', 'settings', 'companies']; + const validTabs = ['customers', 'leads', 'tekniikka', 'ohjeet', 'archive', 'changelog', 'support', 'users', 'settings', 'companies']; const startTab = validTabs.includes(mainHash) ? mainHash : 'customers'; switchToTab(startTab, subHash); } @@ -255,6 +255,7 @@ function switchToTab(target, subTab) { } if (target === 'archive') loadArchive(); if (target === 'changelog') loadChangelog(); + if (target === 'ohjeet') loadGuides(); if (target === 'support') { loadTickets(); showTicketListView(); if (document.getElementById('ticket-auto-refresh').checked) startTicketAutoRefresh(); } if (target === 'users') loadUsers(); if (target === 'settings') loadSettings(); @@ -3477,9 +3478,309 @@ document.getElementById('ipam-form')?.addEventListener('submit', async (e) => { document.getElementById('ipam-search-input')?.addEventListener('input', () => renderIpam()); +// ==================== OHJEET ==================== + +let guidesData = []; +let guideCategories = []; +let currentGuideId = null; + +// Markdown-renderöijä (kevyt, ei ulkoisia kirjastoja) +function renderMarkdown(md) { + if (!md) return ''; + let html = esc(md); + // Koodilohkot ``` ... ``` + html = html.replace(/```(\w*)\n([\s\S]*?)```/g, (m, lang, code) => `
${code}
`); + // Inline-koodi + html = html.replace(/`([^`]+)`/g, '$1'); + // Otsikot + html = html.replace(/^### (.+)$/gm, '

$1

'); + html = html.replace(/^## (.+)$/gm, '

$1

'); + html = html.replace(/^# (.+)$/gm, '

$1

'); + // Lihavointi + kursiivi + html = html.replace(/\*\*\*(.+?)\*\*\*/g, '$1'); + html = html.replace(/\*\*(.+?)\*\*/g, '$1'); + html = html.replace(/\*(.+?)\*/g, '$1'); + // Linkit + html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1'); + // Lainaukset + html = html.replace(/^> (.+)$/gm, '
$1
'); + // Vaakaviiva + html = html.replace(/^---$/gm, '
'); + // Listat: kerätään peräkkäiset lista-rivit yhteen + html = html.replace(/(^[\-\*] .+\n?)+/gm, (match) => { + const items = match.trim().split('\n').map(l => '
  • ' + l.replace(/^[\-\*] /, '') + '
  • ').join(''); + return ''; + }); + html = html.replace(/(^\d+\. .+\n?)+/gm, (match) => { + const items = match.trim().split('\n').map(l => '
  • ' + l.replace(/^\d+\. /, '') + '
  • ').join(''); + return '
      ' + items + '
    '; + }); + // Kappalejaot + html = html.replace(/\n\n/g, '

    '); + html = html.replace(/\n/g, '
    '); + return '

    ' + html + '

    '; +} + +async function loadGuides() { + try { + [guidesData, guideCategories] = await Promise.all([ + apiCall('guides'), + apiCall('guide_categories') + ]); + populateGuideCategoryFilter(); + renderGuidesList(); + showGuideListView(); + const isAdmin = currentUser?.role === 'admin' || currentUser?.role === 'superadmin'; + document.getElementById('btn-add-guide').style.display = isAdmin ? '' : 'none'; + document.getElementById('btn-manage-guide-cats').style.display = isAdmin ? '' : 'none'; + } catch (e) { console.error(e); } +} + +function populateGuideCategoryFilter() { + const sel = document.getElementById('guide-category-filter'); + const formSel = document.getElementById('guide-form-category'); + const opts = guideCategories.map(c => ``).join(''); + if (sel) sel.innerHTML = '' + opts; + if (formSel) formSel.innerHTML = '' + opts; +} + +function renderGuidesList() { + const query = (document.getElementById('guide-search-input')?.value || '').toLowerCase().trim(); + const catFilter = document.getElementById('guide-category-filter')?.value || ''; + + let filtered = guidesData; + if (catFilter) filtered = filtered.filter(g => g.category_id === catFilter); + if (query) { + filtered = filtered.filter(g => + (g.title || '').toLowerCase().includes(query) || + (g.tags || '').toLowerCase().includes(query) || + (g.category_name || '').toLowerCase().includes(query) || + (g.content || '').toLowerCase().includes(query) + ); + } + + const grid = document.getElementById('guides-grid'); + const noGuides = document.getElementById('no-guides'); + if (!grid) return; + + if (filtered.length === 0) { + grid.innerHTML = ''; + if (noGuides) noGuides.style.display = 'block'; + } else { + if (noGuides) noGuides.style.display = 'none'; + grid.innerHTML = filtered.map(g => { + const preview = (g.content || '').substring(0, 150).replace(/[#*`\[\]]/g, ''); + const tags = (g.tags || '').split(',').filter(t => t.trim()); + return `
    +
    + ${g.pinned ? '📌' : ''} + ${g.category_name ? `${esc(g.category_name)}` : ''} +
    +

    ${esc(g.title)}

    +

    ${esc(preview)}${(g.content || '').length > 150 ? '...' : ''}

    + + ${tags.length > 0 ? `
    ${tags.map(t => `${esc(t.trim())}`).join('')}
    ` : ''} +
    `; + }).join(''); + } +} + +function showGuideListView() { + document.getElementById('guides-list-view').style.display = ''; + document.getElementById('guide-read-view').style.display = 'none'; + document.getElementById('guide-edit-view').style.display = 'none'; +} +function showGuideReadView() { + document.getElementById('guides-list-view').style.display = 'none'; + document.getElementById('guide-read-view').style.display = ''; + document.getElementById('guide-edit-view').style.display = 'none'; +} +function showGuideEditView() { + document.getElementById('guides-list-view').style.display = 'none'; + document.getElementById('guide-read-view').style.display = 'none'; + document.getElementById('guide-edit-view').style.display = ''; +} + +async function openGuideRead(id) { + try { + const guide = await apiCall('guide&id=' + encodeURIComponent(id)); + currentGuideId = id; + document.getElementById('guide-read-title').textContent = guide.title; + document.getElementById('guide-read-meta').innerHTML = [ + guide.category_name ? `📁 ${esc(guide.category_name)}` : '', + `✎ ${esc(guide.author || 'Tuntematon')}`, + `📅 ${esc((guide.luotu || '').substring(0, 10))}`, + guide.muokattu ? `Päivitetty: ${timeAgo(guide.muokattu)} (${esc(guide.muokkaaja || '')})` : '' + ].filter(Boolean).join(''); + document.getElementById('guide-read-content').innerHTML = renderMarkdown(guide.content); + const tags = (guide.tags || '').split(',').filter(t => t.trim()); + document.getElementById('guide-read-tags').innerHTML = tags.length > 0 + ? tags.map(t => `${esc(t.trim())}`).join(' ') + : ''; + const isAdmin = currentUser?.role === 'admin' || currentUser?.role === 'superadmin'; + document.getElementById('guide-read-actions').style.display = isAdmin ? 'block' : 'none'; + showGuideReadView(); + } catch (e) { alert(e.message); } +} + +function openGuideEdit(guide) { + document.getElementById('guide-edit-title').textContent = guide ? 'Muokkaa ohjetta' : 'Uusi ohje'; + document.getElementById('guide-form-id').value = guide ? guide.id : ''; + document.getElementById('guide-form-title').value = guide ? guide.title : ''; + document.getElementById('guide-form-content').value = guide ? guide.content : ''; + document.getElementById('guide-form-tags').value = guide ? (guide.tags || '') : ''; + document.getElementById('guide-form-pinned').checked = guide ? guide.pinned : false; + document.getElementById('guide-form-content').style.display = ''; + document.getElementById('guide-preview-pane').style.display = 'none'; + populateGuideCategoryFilter(); + if (guide) document.getElementById('guide-form-category').value = guide.category_id || ''; + showGuideEditView(); + document.getElementById('guide-form-title').focus(); +} + +// Tallenna ohje +document.getElementById('guide-form')?.addEventListener('submit', async (e) => { + e.preventDefault(); + const id = document.getElementById('guide-form-id').value; + const body = { + title: document.getElementById('guide-form-title').value.trim(), + category_id: document.getElementById('guide-form-category').value || null, + content: document.getElementById('guide-form-content').value, + tags: document.getElementById('guide-form-tags').value.trim(), + pinned: document.getElementById('guide-form-pinned').checked, + }; + if (id) { + body.id = id; + const existing = guidesData.find(g => g.id === id); + if (existing) { body.luotu = existing.luotu; body.author = existing.author; } + } + try { + const saved = await apiCall('guide_save', 'POST', body); + await loadGuides(); + openGuideRead(saved.id); + } catch (e) { alert(e.message); } +}); + +async function deleteGuide(id) { + if (!confirm('Haluatko varmasti poistaa tämän ohjeen?')) return; + try { + await apiCall('guide_delete', 'POST', { id }); + await loadGuides(); + showGuideListView(); + } catch (e) { alert(e.message); } +} + +// Event listenerit +document.getElementById('guide-search-input')?.addEventListener('input', () => renderGuidesList()); +document.getElementById('guide-category-filter')?.addEventListener('change', () => renderGuidesList()); +document.getElementById('btn-add-guide')?.addEventListener('click', () => openGuideEdit(null)); +document.getElementById('btn-guide-back')?.addEventListener('click', () => { showGuideListView(); currentGuideId = null; }); +document.getElementById('btn-guide-edit-cancel')?.addEventListener('click', () => { + if (currentGuideId) openGuideRead(currentGuideId); else showGuideListView(); +}); +document.getElementById('guide-form-cancel')?.addEventListener('click', () => { + if (currentGuideId) openGuideRead(currentGuideId); else showGuideListView(); +}); +document.getElementById('btn-edit-guide')?.addEventListener('click', () => { + const guide = guidesData.find(g => g.id === currentGuideId); + if (guide) openGuideEdit(guide); +}); +document.getElementById('btn-delete-guide')?.addEventListener('click', () => { + if (currentGuideId) deleteGuide(currentGuideId); +}); + +// Markdown toolbar +document.querySelectorAll('.guide-tb-btn[data-md]').forEach(btn => { + btn.addEventListener('click', () => { + const ta = document.getElementById('guide-form-content'); + const start = ta.selectionStart; + const end = ta.selectionEnd; + const sel = ta.value.substring(start, end); + let ins = ''; + switch (btn.dataset.md) { + case 'bold': ins = `**${sel || 'teksti'}**`; break; + case 'italic': ins = `*${sel || 'teksti'}*`; break; + case 'h2': ins = `\n## ${sel || 'Otsikko'}\n`; break; + case 'h3': ins = `\n### ${sel || 'Alaotsikko'}\n`; break; + case 'ul': ins = `\n- ${sel || 'kohta'}\n`; break; + case 'ol': ins = `\n1. ${sel || 'kohta'}\n`; break; + case 'link': ins = `[${sel || 'linkki'}](https://)`; break; + case 'code': ins = sel.includes('\n') ? `\n\`\`\`\n${sel}\n\`\`\`\n` : `\`${sel || 'koodi'}\``; break; + case 'quote': ins = `\n> ${sel || 'lainaus'}\n`; break; + } + ta.value = ta.value.substring(0, start) + ins + ta.value.substring(end); + ta.focus(); + ta.selectionStart = ta.selectionEnd = start + ins.length; + }); +}); + +// Esikatselu-toggle +document.getElementById('btn-guide-preview-toggle')?.addEventListener('click', () => { + const ta = document.getElementById('guide-form-content'); + const preview = document.getElementById('guide-preview-pane'); + if (ta.style.display !== 'none') { + preview.innerHTML = renderMarkdown(ta.value); + ta.style.display = 'none'; + preview.style.display = ''; + } else { + ta.style.display = ''; + preview.style.display = 'none'; + } +}); + +// Kategorianhallinta +document.getElementById('btn-manage-guide-cats')?.addEventListener('click', () => { + renderGuideCatList(); + document.getElementById('guide-cat-modal').style.display = 'flex'; +}); +document.getElementById('guide-cat-modal-close')?.addEventListener('click', () => { + document.getElementById('guide-cat-modal').style.display = 'none'; +}); + +function renderGuideCatList() { + const list = document.getElementById('guide-cat-list'); + if (!list) return; + if (guideCategories.length === 0) { + list.innerHTML = '

    Ei kategorioita.

    '; + return; + } + list.innerHTML = guideCategories.map(c => ` +
    + ${esc(c.nimi)} + +
    + `).join(''); +} + +document.getElementById('btn-guide-cat-add')?.addEventListener('click', async () => { + const inp = document.getElementById('guide-cat-new-name'); + const nimi = inp.value.trim(); + if (!nimi) return; + try { + await apiCall('guide_category_save', 'POST', { nimi, sort_order: guideCategories.length }); + inp.value = ''; + guideCategories = await apiCall('guide_categories'); + renderGuideCatList(); + populateGuideCategoryFilter(); + } catch (e) { alert(e.message); } +}); + +async function deleteGuideCategory(id, name) { + if (!confirm(`Poista kategoria "${name}"? Ohjeet siirtyvät kategoriattomiksi.`)) return; + try { + await apiCall('guide_category_delete', 'POST', { id }); + guideCategories = await apiCall('guide_categories'); + renderGuideCatList(); + populateGuideCategoryFilter(); + } catch (e) { alert(e.message); } +} + // ==================== MODUULIT ==================== -const ALL_MODULES = ['customers', 'support', 'leads', 'tekniikka', 'archive', 'changelog', 'settings']; +const ALL_MODULES = ['customers', 'support', 'leads', 'tekniikka', 'ohjeet', 'archive', 'changelog', 'settings']; const DEFAULT_MODULES = ['customers', 'support', 'archive', 'changelog', 'settings']; function applyModules(modules) { diff --git a/style.css b/style.css index 23ea2c4..513d063 100644 --- a/style.css +++ b/style.css @@ -1095,6 +1095,33 @@ span.empty { .ipam-type-free { display:inline-block; padding:1px 8px; border-radius:4px; font-size:0.75rem; font-weight:700; background:#d1fae5; color:#065f46; } .ipam-network-free { color:#059669; background:#ecfdf5; } +/* ==================== OHJEET ==================== */ +.guide-card { background:#fff; border-radius:12px; padding:1.25rem; box-shadow:0 1px 4px rgba(0,0,0,0.06); cursor:pointer; transition:transform 0.15s, box-shadow 0.15s; border:2px solid transparent; display:flex; flex-direction:column; } +.guide-card:hover { transform:translateY(-2px); box-shadow:0 4px 12px rgba(0,0,0,0.1); border-color:var(--primary-color); } +.guide-card.guide-pinned { border-color:#ffc107; background:#fffef5; } +.guide-card-header { display:flex; align-items:center; gap:0.5rem; margin-bottom:0.5rem; min-height:24px; } +.guide-pin-icon { font-size:0.9rem; } +.guide-category-badge { display:inline-block; padding:2px 8px; border-radius:10px; font-size:0.72rem; font-weight:600; background:var(--primary-light); color:var(--primary-color); } +.guide-card-title { font-size:1.05rem; color:#1a1a2e; margin-bottom:0.5rem; line-height:1.3; } +.guide-card-preview { font-size:0.85rem; color:#888; line-height:1.5; flex:1; margin-bottom:0.75rem; display:-webkit-box; -webkit-line-clamp:3; -webkit-box-orient:vertical; overflow:hidden; } +.guide-card-footer { display:flex; justify-content:space-between; font-size:0.78rem; color:#aaa; } +.guide-card-tags { display:flex; gap:0.3rem; flex-wrap:wrap; margin-top:0.5rem; } +.guide-tag { display:inline-block; padding:2px 8px; border-radius:10px; font-size:0.72rem; font-weight:600; background:#e8ebf0; color:#555; } +.guide-content h1 { font-size:1.5rem; color:var(--primary-color); margin:1.5rem 0 0.75rem; } +.guide-content h2 { font-size:1.25rem; color:var(--primary-color); margin:1.25rem 0 0.5rem; border-bottom:2px solid #f0f2f5; padding-bottom:0.3rem; } +.guide-content h3 { font-size:1.1rem; color:#333; margin:1rem 0 0.5rem; } +.guide-content p { margin-bottom:0.75rem; } +.guide-content ul, .guide-content ol { margin:0.5rem 0 0.75rem 1.5rem; } +.guide-content li { margin-bottom:0.25rem; } +.guide-content code { background:#f0f2f5; padding:2px 6px; border-radius:4px; font-size:0.88rem; font-family:'SF Mono',Monaco,Consolas,monospace; } +.guide-content pre { background:#1a1a2e; color:#e0e0e0; padding:1rem; border-radius:8px; overflow-x:auto; margin:0.75rem 0; } +.guide-content pre code { background:none; padding:0; color:inherit; } +.guide-content blockquote { border-left:4px solid var(--primary-color); margin:0.75rem 0; padding:0.5rem 1rem; background:#f8f9fb; color:#555; border-radius:0 8px 8px 0; } +.guide-content a { color:var(--primary-color); text-decoration:underline; } +.guide-content hr { border:none; border-top:2px solid #f0f2f5; margin:1.5rem 0; } +.guide-tb-btn { background:#f0f2f5; border:1px solid #ddd; padding:4px 10px; border-radius:5px; font-size:0.78rem; font-weight:600; cursor:pointer; color:#555; transition:all 0.15s; } +.guide-tb-btn:hover { background:var(--primary-color); color:#fff; border-color:var(--primary-color); } + /* Role badge */ .role-badge { display: inline-block;