feat(nav): unified navbar, account page, corpus summary widget, and i18n fixes

- New includes/nav.php: sticky site-wide nav with Tools dropdown, Dashboard
  link, compact language switcher, user identity → /account.php, Log out
- New account.php: credits & plan, profile, team, usage sections
- New api/corpus-summary.php: JSON endpoint for corpus doc count + last updated
- Replaces topbar in layout.php, layout_dashboard.php, and dashboard.php
- Fixes hardcoded Norwegian strings in dashboard.php credit cards via dbnToolsT()
- Adds 35 new i18n keys across all 4 languages (en/no/uk/pl) in i18n.php
- CSS: .dbn-nav navbar + .account-* account page styles in tools.css

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-23 19:11:39 +02:00
parent 33dc5406b2
commit 90117fa9de
8 changed files with 918 additions and 87 deletions
+230
View File
@@ -0,0 +1,230 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/bootstrap.php';
require_once __DIR__ . '/includes/FreeTier.php';
if (!dbnToolsIsAuthenticated()) {
header('Location: /?return=' . urlencode($_SERVER['REQUEST_URI'] ?? '/account.php'));
exit;
}
$uiLang = dbnToolsCurrentLanguage();
$authUser = dbnToolsAuthenticatedUser();
$isSso = dbnToolsIsFreeTier();
$email = (string)($authUser['email'] ?? '');
$role = (string)($authUser['role'] ?? '');
// Credits & plan (SSO users only)
$detail = $isSso ? FreeTier::balanceDetail((int)$_SESSION['dbn_tools_sso_uid']) : null;
$tier = $detail ? (string)$detail['tier'] : ($isSso ? 'free' : 'caveau');
$tierLabels = [
'free' => ['Free', '#f3f4f6', '#374151'],
'plus' => ['Plus', '#ddd6fe', '#5b21b6'],
'pro' => ['Pro', '#bfdbfe', '#1e40af'],
'caveau' => ['CaveauAI', '#d1fae5', '#065f46'],
];
$tierLabel = $tierLabels[$tier] ?? $tierLabels['free'];
$monthlyAllowance = $detail ? FreeTier::monthlyAllowance($tier) : 0;
$creditsUsed = $detail ? max(0, $monthlyAllowance - (int)$detail['balance']) : 0;
// Team (Caveau sessions only)
$teamMembers = [];
if (!$isSso && !empty($authUser['client_id'])) {
try {
$db = dbnToolsDb();
$stmt = $db->prepare(
"SELECT cu.email, cu.role, cu.created_at
FROM client_users cu
WHERE cu.client_id = ?
ORDER BY FIELD(cu.role,'owner','admin','editor','viewer'), cu.created_at ASC"
);
$stmt->execute([(int)$authUser['client_id']]);
$teamMembers = $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (Throwable $e) {
// non-fatal
}
}
// Renewal date label
$renewalLabel = '';
if ($detail) {
if (!empty($detail['trial_active']) && !empty($detail['trial_expires_at'])) {
$renewalLabel = date('d M Y', strtotime((string)$detail['trial_expires_at']))
. ' (' . (int)$detail['trial_days_remaining'] . ' ' . dbnToolsT('trial_days_left', $uiLang) . ')';
} elseif (!empty($detail['subscription_period_end'])) {
$renewalLabel = date('d M Y', strtotime((string)$detail['subscription_period_end']));
}
}
?>
<!doctype html>
<html lang="<?= htmlspecialchars($uiLang) ?>">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= htmlspecialchars(dbnToolsT('account_title', $uiLang)) ?> — Do Better Norge</title>
<meta name="robots" content="noindex, nofollow">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Crimson+Pro:wght@400;600;700&family=IBM+Plex+Sans:wght@400;500;600;700&display=swap">
<link rel="stylesheet" href="assets/css/tools.css">
</head>
<body data-authenticated="true" class="lt-app">
<script>
window.DBN_TOOLS_AUTHENTICATED = true;
window.DBN_TOOLS_LANG = <?= json_encode($uiLang, JSON_UNESCAPED_UNICODE) ?>;
</script>
<?php include __DIR__ . '/includes/nav.php'; ?>
<div class="account-shell">
<div class="account-hero">
<h1><?= htmlspecialchars(dbnToolsT('account_title', $uiLang)) ?></h1>
<p><?= htmlspecialchars($email) ?></p>
</div>
<?php if ($detail): ?>
<!-- ── Credits & Plan ───────────────────────────────────────── -->
<section class="account-section">
<p class="account-section__title"><?= htmlspecialchars(dbnToolsT('account_credits', $uiLang)) ?></p>
<span class="account-tier-pill" style="background:<?= htmlspecialchars($tierLabel[1]) ?>; color:<?= htmlspecialchars($tierLabel[2]) ?>;">
<?= htmlspecialchars($tierLabel[0]) ?>
</span>
<?php if (!empty($detail['trial_active'])): ?>
<span class="account-tier-pill" style="background:#fef3c7;color:#92400e;margin-left:0.4rem;">
<?= htmlspecialchars(dbnToolsT('trial_active_label', $uiLang)) ?>
</span>
<?php endif; ?>
<p class="account-credits-big">
<?= number_format((int)$detail['balance'] + (int)$detail['bonus_balance'], 0, ',', ' ') ?>
</p>
<p class="account-credits-sub">
<?= (int)$detail['balance'] ?> <?= htmlspecialchars(dbnToolsT('credits_monthly', $uiLang)) ?>
· <?= (int)$detail['bonus_balance'] ?> <?= htmlspecialchars(dbnToolsT('credits_bonus', $uiLang)) ?>
</p>
<?php if ($renewalLabel): ?>
<div class="account-row">
<span class="account-row__label"><?= htmlspecialchars(dbnToolsT('renewal_date', $uiLang)) ?></span>
<span class="account-row__value"><?= htmlspecialchars($renewalLabel) ?></span>
</div>
<?php endif; ?>
<?php if (in_array($tier, ['free'], true)): ?>
<a href="/pricing.php" class="account-upgrade-cta">Upgrade plan →</a>
<?php endif; ?>
</section>
<?php endif; ?>
<!-- ── Profile ──────────────────────────────────────────────── -->
<section class="account-section">
<p class="account-section__title"><?= htmlspecialchars(dbnToolsT('account_profile', $uiLang)) ?></p>
<div class="account-row">
<span class="account-row__label">Email</span>
<span class="account-row__value"><?= htmlspecialchars($email) ?></span>
</div>
<div class="account-row">
<span class="account-row__label">Login</span>
<span class="account-row__value">
<?php if ($isSso): ?>
<?= htmlspecialchars(dbnToolsT('login_method_sso', $uiLang)) ?>
<?php else: ?>
<?= htmlspecialchars(dbnToolsT('login_method_email', $uiLang)) ?>
<?php endif; ?>
</span>
</div>
<?php if ($role !== '' && $role !== 'sso'): ?>
<div class="account-row">
<span class="account-row__label">Role</span>
<span class="account-row__value"><span class="account-role-pill"><?= htmlspecialchars($role) ?></span></span>
</div>
<?php endif; ?>
</section>
<!-- ── Team ─────────────────────────────────────────────────── -->
<section class="account-section">
<p class="account-section__title"><?= htmlspecialchars(dbnToolsT('account_team', $uiLang)) ?></p>
<?php if ($isSso): ?>
<p style="color:var(--muted,#667085); font-size:0.88rem;"><?= htmlspecialchars(dbnToolsT('team_single_sso', $uiLang)) ?></p>
<?php elseif (empty($teamMembers)): ?>
<p style="color:var(--muted,#667085); font-size:0.88rem;">—</p>
<?php else: ?>
<table class="account-team-table">
<thead>
<tr>
<th>Email</th>
<th>Role</th>
<th>Added</th>
</tr>
</thead>
<tbody>
<?php foreach ($teamMembers as $member): ?>
<tr>
<td><?= htmlspecialchars($member['email']) ?></td>
<td><span class="account-role-pill"><?= htmlspecialchars($member['role']) ?></span></td>
<td><?= htmlspecialchars(date('d M Y', strtotime((string)$member['created_at']))) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</section>
<?php if ($detail): ?>
<!-- ── Usage ────────────────────────────────────────────────── -->
<section class="account-section">
<p class="account-section__title"><?= htmlspecialchars(dbnToolsT('account_usage', $uiLang)) ?></p>
<div class="account-row">
<span class="account-row__label"><?= htmlspecialchars(dbnToolsT('usage_credits_used', $uiLang)) ?></span>
<span class="account-row__value"><?= $creditsUsed ?> / <?= $monthlyAllowance ?></span>
</div>
<?php if ($monthlyAllowance > 0): ?>
<div class="account-usage-bar-wrap">
<div class="account-usage-bar" style="width:<?= min(100, round($creditsUsed / $monthlyAllowance * 100)) ?>%"></div>
</div>
<?php endif; ?>
<?php if (!empty($detail['storage_quota_bytes']) && (int)$detail['storage_quota_bytes'] > 0):
$storagePct = min(100, round((int)$detail['storage_used_bytes'] / (int)$detail['storage_quota_bytes'] * 100));
$usedMb = round((int)$detail['storage_used_bytes'] / 1048576, 1);
$quotaMb = round((int)$detail['storage_quota_bytes'] / 1048576, 0);
?>
<div class="account-row" style="margin-top:0.75rem;">
<span class="account-row__label"><?= htmlspecialchars(dbnToolsT('usage_storage_used', $uiLang)) ?></span>
<span class="account-row__value"><?= $usedMb ?> / <?= $quotaMb ?> MB</span>
</div>
<div class="account-usage-bar-wrap">
<div class="account-usage-bar" style="width:<?= $storagePct ?>%"></div>
</div>
<?php endif; ?>
<p style="margin-top:1rem; color:var(--muted,#667085); font-size:0.82rem; font-style:italic;">
<?= htmlspecialchars(dbnToolsT('usage_log_coming', $uiLang)) ?>
</p>
<?php if (empty($detail['survey_completed_at'])): ?>
<a href="https://dobetternorge.no/survey.php" class="account-survey-cta">
<div>
<strong><?= htmlspecialchars(dbnToolsT('earn_credits_eyebrow', $uiLang)) ?></strong>
<span><?= htmlspecialchars(dbnToolsT('survey_cta_text', $uiLang)) ?></span>
</div>
<span style="font-size:1.2rem;">→</span>
</a>
<?php endif; ?>
</section>
<?php endif; ?>
</div>
<?php require_once __DIR__ . '/includes/footer.php'; ?>
</body>
</html>
+45
View File
@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
/**
* GET /api/corpus-summary.php
* Returns: { doc_count: int, last_updated: string|null }
*
* Auth: SSO session only. Ensures dashboard tenant is provisioned.
*/
require_once __DIR__ . '/../includes/bootstrap.php';
header('Content-Type: application/json');
header('Cache-Control: no-store');
if (!dbnToolsIsAuthenticated() || !dbnToolsIsFreeTier()) {
http_response_code(401);
echo json_encode(['error' => 'auth_required']);
exit;
}
try {
$tenant = dbnToolsEnsureDashboardTenant();
} catch (Throwable $e) {
http_response_code(503);
echo json_encode(['error' => 'tenant_unavailable']);
exit;
}
try {
$db = dbnToolsDb();
$stmt = $db->prepare(
"SELECT COUNT(*) AS doc_count, MAX(created_at) AS last_updated
FROM client_documents
WHERE client_id = ? AND status = 'ready'"
);
$stmt->execute([(int)$tenant['client_id']]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
echo json_encode([
'doc_count' => (int)($row['doc_count'] ?? 0),
'last_updated' => $row['last_updated'] ?? null,
]);
} catch (Throwable $e) {
http_response_code(500);
echo json_encode(['error' => 'db_error']);
}
+359
View File
@@ -8814,3 +8814,362 @@ body.lt-landing {
color: rgba(22,19,15,0.80);
text-decoration: underline;
}
/* ═══════════════════════════════════════════════════════════════
UNIFIED SITE NAVBAR — .dbn-nav
═══════════════════════════════════════════════════════════════ */
.dbn-nav {
display: flex;
align-items: center;
gap: 0;
background: var(--dbn-blue, #00205b);
height: 52px;
padding: 0 1.5rem;
position: sticky;
top: 0;
z-index: 1000;
border-bottom: 2px solid var(--dbn-red, #ba0c2f);
font-family: 'IBM Plex Sans', sans-serif;
font-size: 0.875rem;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.18);
}
/* Brand */
.dbn-nav__brand {
display: flex;
align-items: center;
gap: 0.45rem;
color: #fff;
text-decoration: none;
font-weight: 700;
letter-spacing: -0.01em;
white-space: nowrap;
flex-shrink: 0;
}
.dbn-nav__brandmark { font-size: 1.15rem; }
.dbn-nav__brandname { font-size: 0.9rem; }
/* Centre links */
.dbn-nav__links {
display: flex;
align-items: stretch;
gap: 0;
margin-left: 1.75rem;
}
.dbn-nav__link {
display: flex;
align-items: center;
gap: 0.3rem;
color: rgba(255, 255, 255, 0.80);
padding: 0 1rem;
height: 52px;
text-decoration: none;
background: none;
border: none;
cursor: pointer;
font-family: inherit;
font-size: 0.875rem;
white-space: nowrap;
transition: color 0.12s, background 0.12s;
}
.dbn-nav__link:hover,
.dbn-nav__link.is-active {
color: #fff;
background: rgba(255, 255, 255, 0.10);
}
.dbn-nav__caret { font-size: 0.65rem; opacity: 0.7; }
/* Dropdown */
.dbn-nav__dropdown { position: relative; display: flex; align-items: stretch; }
.dbn-nav__panel {
display: none;
position: absolute;
top: calc(100% + 2px);
left: 0;
min-width: 240px;
background: #fff;
border: 1px solid var(--line, #d8dde7);
border-radius: 10px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.16), 0 2px 8px rgba(0, 0, 0, 0.08);
padding: 0.4rem 0;
z-index: 1100;
animation: navPanelIn 0.12s ease;
}
@keyframes navPanelIn {
from { opacity: 0; transform: translateY(-4px); }
to { opacity: 1; transform: translateY(0); }
}
.dbn-nav__dropdown.is-open .dbn-nav__panel { display: block; }
.dbn-nav__panel-item {
display: grid;
grid-template-columns: 2.5rem 1fr;
grid-template-rows: auto auto;
align-items: center;
padding: 0.55rem 1rem;
color: var(--dbn-ink, #16130f);
text-decoration: none;
font-size: 0.85rem;
gap: 0 0;
transition: background 0.1s;
}
.dbn-nav__panel-item:hover { background: var(--soft-teal, #e7f5f2); }
.dbn-nav__panel-badge {
grid-row: 1 / 3;
font-size: 0.7rem;
font-weight: 700;
color: var(--dbn-blue, #00205b);
background: rgba(0, 32, 91, 0.08);
border-radius: 4px;
padding: 2px 5px;
text-align: center;
align-self: center;
font-family: 'JetBrains Mono', monospace;
letter-spacing: 0;
}
.dbn-nav__panel-label {
grid-column: 2;
font-weight: 600;
color: var(--dbn-ink, #16130f);
line-height: 1.2;
}
.dbn-nav__panel-sub {
grid-column: 2;
font-size: 0.75rem;
color: var(--muted, #667085);
line-height: 1.2;
}
/* Right side */
.dbn-nav__right {
display: flex;
align-items: center;
gap: 0.6rem;
margin-left: auto;
}
/* Language switcher */
.dbn-nav__langs {
display: flex;
gap: 1px;
}
.dbn-nav__lang {
color: rgba(255, 255, 255, 0.55);
font-size: 0.7rem;
font-weight: 700;
text-decoration: none;
padding: 3px 5px;
border-radius: 3px;
letter-spacing: 0.04em;
transition: color 0.12s, background 0.12s;
}
.dbn-nav__lang:hover { color: #fff; background: rgba(255,255,255,0.12); }
.dbn-nav__lang.is-active { color: #fff; background: rgba(255,255,255,0.18); }
/* Auth right */
.dbn-nav__account-link {
display: flex;
flex-direction: column;
align-items: flex-end;
text-decoration: none;
padding: 0 0.25rem;
gap: 1px;
}
.dbn-nav__username {
color: #fff;
font-size: 0.8rem;
font-weight: 600;
line-height: 1;
max-width: 120px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.dbn-nav__account-badge {
color: rgba(255, 255, 255, 0.58);
font-size: 0.68rem;
line-height: 1;
text-transform: uppercase;
letter-spacing: 0.06em;
}
.dbn-nav__account-link:hover .dbn-nav__username { color: #fff; }
.dbn-nav__account-link:hover .dbn-nav__account-badge { color: rgba(255,255,255,0.82); }
.dbn-nav__logout,
.dbn-nav__login {
color: rgba(255, 255, 255, 0.80);
font-size: 0.82rem;
text-decoration: none;
padding: 0.3rem 0.65rem;
border-radius: 5px;
white-space: nowrap;
transition: color 0.12s, background 0.12s;
}
.dbn-nav__logout:hover { color: #fff; background: rgba(255,255,255,0.12); }
.dbn-nav__login {
border: 1px solid rgba(255, 255, 255, 0.30);
font-weight: 600;
}
.dbn-nav__login:hover { color: #fff; background: rgba(255,255,255,0.12); border-color: rgba(255,255,255,0.50); }
/* Push down any direct sibling so nav doesn't overlap */
.dbn-nav + .app-shell,
.dbn-nav + main,
.dbn-nav + .dash-shell { margin-top: 0; }
/* ═══════════════════════════════════════════════════════════════
ACCOUNT PAGE — .account-*
═══════════════════════════════════════════════════════════════ */
.account-shell {
max-width: 780px;
margin: 2.5rem auto 4rem;
padding: 0 1.5rem;
}
.account-hero {
margin-bottom: 2rem;
}
.account-hero h1 {
font-family: 'Crimson Pro', serif;
font-size: 2rem;
font-weight: 700;
color: var(--dbn-blue, #00205b);
margin: 0 0 0.25rem;
}
.account-hero p {
color: var(--muted, #667085);
font-size: 0.9rem;
margin: 0;
}
.account-section {
background: #fff;
border: 1px solid var(--line, #d8dde7);
border-radius: 12px;
padding: 1.5rem;
margin-bottom: 1.25rem;
}
.account-section__title {
font-size: 0.72rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--muted, #667085);
margin: 0 0 1rem;
padding-bottom: 0.6rem;
border-bottom: 1px solid var(--line, #d8dde7);
}
.account-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 1rem;
padding: 0.55rem 0;
border-bottom: 1px solid var(--line, #d8dde7);
font-size: 0.88rem;
}
.account-row:last-child { border-bottom: none; }
.account-row__label { color: var(--muted, #667085); flex-shrink: 0; }
.account-row__value { font-weight: 600; color: var(--dbn-ink, #16130f); text-align: right; }
.account-row__value a { color: var(--dbn-blue, #00205b); text-decoration: none; }
.account-row__value a:hover { text-decoration: underline; }
.account-tier-pill {
display: inline-flex;
align-items: center;
gap: 0.4rem;
padding: 3px 10px;
border-radius: 999px;
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.03em;
}
.account-credits-big {
font-family: 'Crimson Pro', serif;
font-size: 2.5rem;
font-weight: 700;
color: var(--dbn-blue, #00205b);
line-height: 1;
margin: 0.5rem 0 0.25rem;
}
.account-credits-sub {
font-size: 0.83rem;
color: var(--muted, #667085);
margin: 0 0 1rem;
}
.account-upgrade-cta {
display: inline-flex;
align-items: center;
gap: 0.4rem;
background: var(--dbn-blue, #00205b);
color: #fff;
text-decoration: none;
padding: 0.55rem 1.1rem;
border-radius: 8px;
font-size: 0.85rem;
font-weight: 600;
margin-top: 0.75rem;
transition: background 0.15s;
}
.account-upgrade-cta:hover { background: #001845; }
.account-team-table {
width: 100%;
border-collapse: collapse;
font-size: 0.85rem;
}
.account-team-table th {
text-align: left;
color: var(--muted, #667085);
font-weight: 600;
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.06em;
padding: 0.4rem 0;
border-bottom: 1px solid var(--line, #d8dde7);
}
.account-team-table td {
padding: 0.6rem 0;
border-bottom: 1px solid var(--line, #d8dde7);
color: var(--dbn-ink, #16130f);
}
.account-team-table tr:last-child td { border-bottom: none; }
.account-role-pill {
display: inline-block;
padding: 1px 8px;
border-radius: 4px;
font-size: 0.72rem;
font-weight: 600;
background: rgba(0,32,91,0.08);
color: var(--dbn-blue, #00205b);
}
.account-usage-bar-wrap {
height: 8px;
background: var(--line, #d8dde7);
border-radius: 4px;
overflow: hidden;
margin-top: 0.4rem;
}
.account-usage-bar {
height: 100%;
background: var(--dbn-blue, #00205b);
border-radius: 4px;
transition: width 0.5s ease;
}
.account-survey-cta {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
background: #fef3c7;
border-radius: 8px;
padding: 0.9rem 1.1rem;
margin-top: 0.75rem;
text-decoration: none;
color: #92400e;
}
.account-survey-cta strong { display: block; font-size: 0.9rem; }
.account-survey-cta span { font-size: 0.8rem; opacity: 0.85; }
.account-survey-cta:hover { background: #fde68a; }
+42 -37
View File
@@ -47,68 +47,50 @@ window.DBN_TOOLS_AUTHENTICATED = true;
window.DBN_TOOLS_LANG = <?= json_encode($uiLang, JSON_UNESCAPED_UNICODE) ?>;
</script>
<?php include __DIR__ . '/includes/nav.php'; ?>
<main id="appShell" class="app-shell dashboard-shell">
<header class="topbar">
<div>
<p class="eyebrow"><?= htmlspecialchars(dbnToolsT('brand_line', $uiLang)) ?></p>
<h1><?= htmlspecialchars(dbnToolsT('dashboard_title', $uiLang)) ?></h1>
<div class="case-no">
<span class="pulse"></span>
<span>family-legal</span>
<span class="case-sep">.</span>
<span><?= htmlspecialchars(dbnToolsT('retention', $uiLang)) ?></span>
</div>
</div>
<div class="topbar-actions">
<nav class="shell-lang-switcher" aria-label="Language">
<?php foreach (dbnToolsSupportedLanguages() as $langCode): ?>
<a href="<?= htmlspecialchars($langPath . '?lang=' . $langCode) ?>" class="<?= $langCode === $uiLang ? 'is-active' : '' ?>"><?= htmlspecialchars(dbnToolsLanguageLabel($langCode)) ?></a>
<?php endforeach; ?>
</nav>
<span id="healthPill" class="status-pill"><?= htmlspecialchars(dbnToolsT('session_active', $uiLang)) ?></span>
<?php if ($dashIsSso): ?>
<a href="/billing.php" class="tier-pill" style="background: <?= htmlspecialchars($tierLabel[1]) ?>; color: <?= htmlspecialchars($tierLabel[2]) ?>; padding: 4px 12px; border-radius: 999px; font-size: 0.82rem; font-weight: 600; text-decoration: none;">
<?= htmlspecialchars($tierLabel[0]) ?>
</a>
<?php endif; ?>
<button id="healthButton" class="secondary-button" type="button"><?= htmlspecialchars(dbnToolsT('health', $uiLang)) ?></button>
</div>
</header>
<?php if ($dashIsSso && $dashDetail): ?>
<section class="dashboard-status-row" style="display:grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap:1rem; margin: 1.5rem 0;">
<div class="status-card" style="background:#fff; border:1px solid #e5e7eb; border-radius:10px; padding:1.1rem 1.25rem;">
<p style="margin:0; color:#6b7280; font-size:0.85rem; text-transform:uppercase; letter-spacing:0.06em;">Tilgjengelige kreditter</p>
<p style="margin:0; color:#6b7280; font-size:0.85rem; text-transform:uppercase; letter-spacing:0.06em;"><?= htmlspecialchars(dbnToolsT('credits_available', $uiLang)) ?></p>
<p style="margin:0.35rem 0 0; font-size:1.8rem; font-weight:700; color:#00205B;">
<?php $eff = (int)$dashDetail['balance'] + (int)$dashDetail['bonus_balance']; ?>
<?= number_format($eff, 0, ',', ' ') ?>
</p>
<p style="margin:0; color:#6b7280; font-size:0.85rem;"><?= (int)$dashDetail['balance'] ?> månedlige · <?= (int)$dashDetail['bonus_balance'] ?> bonus · <a href="/billing.php">Detaljer</a></p>
<p style="margin:0; color:#6b7280; font-size:0.85rem;"><?= (int)$dashDetail['balance'] ?> <?= htmlspecialchars(dbnToolsT('credits_monthly', $uiLang)) ?> · <?= (int)$dashDetail['bonus_balance'] ?> <?= htmlspecialchars(dbnToolsT('credits_bonus', $uiLang)) ?> · <a href="/billing.php"><?= htmlspecialchars(dbnToolsT('details_link', $uiLang)) ?></a></p>
</div>
<?php if (in_array($dashTier, ['plus','pro'], true)): ?>
<a class="status-card" href="/min-sak.php" style="background:#fff; border:1px solid #e5e7eb; border-radius:10px; padding:1.1rem 1.25rem; text-decoration:none; color:inherit;">
<p style="margin:0; color:#6b7280; font-size:0.85rem; text-transform:uppercase; letter-spacing:0.06em;">Min sak</p>
<p style="margin:0.35rem 0 0; font-size:1.4rem; font-weight:700; color:#00205B;">Bygg din egen sak →</p>
<p style="margin:0; color:#6b7280; font-size:0.85rem; text-transform:uppercase; letter-spacing:0.06em;"><?= htmlspecialchars(dbnToolsT('my_case', $uiLang)) ?></p>
<p style="margin:0.35rem 0 0; font-size:1.4rem; font-weight:700; color:#00205B;"><?= htmlspecialchars(dbnToolsT('build_your_case', $uiLang)) ?> →</p>
<?php
$used = (int)$dashDetail['storage_used_bytes'];
$quota = (int)$dashDetail['storage_quota_bytes'];
$usedMb = $used > 0 ? round($used / 1048576, 1) : 0;
$quotaMb = $quota > 0 ? round($quota / 1048576, 0) : 0;
?>
<p style="margin:0; color:#6b7280; font-size:0.85rem;"><?= $usedMb ?> MB / <?= $quotaMb ?> MB brukt</p>
<p style="margin:0; color:#6b7280; font-size:0.85rem;"><?= $usedMb ?> MB / <?= $quotaMb ?> MB</p>
</a>
<?php else: ?>
<a class="status-card" href="/pricing.php" style="background:linear-gradient(135deg,#00205B,#003478); color:#fff; border-radius:10px; padding:1.1rem 1.25rem; text-decoration:none;">
<p style="margin:0; opacity:0.85; font-size:0.85rem; text-transform:uppercase; letter-spacing:0.06em;">Bygg din egen sak</p>
<p style="margin:0.35rem 0 0; font-size:1.4rem; font-weight:700;">Last opp dokumenter →</p>
<p style="margin:0; opacity:0.85; font-size:0.85rem;">Tilgjengelig fra Plus NOK 129/mnd</p>
<p style="margin:0; opacity:0.85; font-size:0.85rem; text-transform:uppercase; letter-spacing:0.06em;"><?= htmlspecialchars(dbnToolsT('build_your_case', $uiLang)) ?></p>
<p style="margin:0.35rem 0 0; font-size:1.4rem; font-weight:700;"><?= htmlspecialchars(dbnToolsT('upload_documents', $uiLang)) ?> →</p>
<p style="margin:0; opacity:0.85; font-size:0.85rem;"><?= htmlspecialchars(dbnToolsT('upgrade_from_plus', $uiLang)) ?></p>
</a>
<?php endif; ?>
<!-- Corpus summary widget — populated via fetch('/api/corpus-summary.php') -->
<a id="corpusSummaryCard" class="status-card" href="/dashboard/" style="background:#fff; border:1px solid #e5e7eb; border-radius:10px; padding:1.1rem 1.25rem; text-decoration:none; color:inherit; display:block;">
<p style="margin:0; color:#6b7280; font-size:0.85rem; text-transform:uppercase; letter-spacing:0.06em;"><?= htmlspecialchars(dbnToolsT('my_corpus', $uiLang)) ?></p>
<p id="corpusDocCount" style="margin:0.35rem 0 0; font-size:1.4rem; font-weight:700; color:#00205B;">—</p>
<p id="corpusUpdated" style="margin:0; color:#6b7280; font-size:0.85rem;"><?= htmlspecialchars(dbnToolsT('open_corpus', $uiLang)) ?> →</p>
</a>
<?php if ($showSurveyCta): ?>
<a class="status-card" href="https://dobetternorge.no/survey.php" style="background:#fef3c7; color:#92400e; border-radius:10px; padding:1.1rem 1.25rem; text-decoration:none;">
<p style="margin:0; font-size:0.85rem; text-transform:uppercase; letter-spacing:0.06em; opacity:0.85;">Tjen 25 ekstra kreditter</p>
<p style="margin:0.35rem 0 0; font-size:1.2rem; font-weight:700;">Ta vår 5-spørsmåls undersøkelse →</p>
<p style="margin:0; font-size:0.85rem; opacity:0.85;">Ingen salgspitch — bare research</p>
<p style="margin:0; font-size:0.85rem; text-transform:uppercase; letter-spacing:0.06em; opacity:0.85;"><?= htmlspecialchars(dbnToolsT('earn_credits_eyebrow', $uiLang)) ?></p>
<p style="margin:0.35rem 0 0; font-size:1.2rem; font-weight:700;"><?= htmlspecialchars(dbnToolsT('survey_btn', $uiLang)) ?> →</p>
<p style="margin:0; font-size:0.85rem; opacity:0.85;"><?= htmlspecialchars(dbnToolsT('survey_cta_text', $uiLang)) ?></p>
</a>
<?php endif; ?>
</section>
@@ -246,5 +228,28 @@ foreach ($tools as $slug => $item):
}
}());
</script>
<?php if ($dashIsSso): ?>
<script>
(function () {
var card = document.getElementById('corpusSummaryCard');
var countEl = document.getElementById('corpusDocCount');
var updEl = document.getElementById('corpusUpdated');
if (!card || !countEl) return;
fetch('/api/corpus-summary.php')
.then(function (r) { return r.ok ? r.json() : null; })
.then(function (data) {
if (!data) return;
countEl.textContent = data.doc_count !== undefined
? data.doc_count.toLocaleString() + ' docs'
: '—';
if (data.last_updated && updEl) {
var d = new Date(data.last_updated);
updEl.textContent = d.toLocaleDateString(undefined, { day: 'numeric', month: 'short', year: 'numeric' });
}
})
.catch(function () {});
}());
</script>
<?php endif; ?>
</body>
</html>
+136
View File
@@ -225,6 +225,40 @@ function dbnToolsTranslations(): array
'pricing_strip_title' => 'Start free. Upgrade when you need your own case.',
'pricing_strip_sub' => '30 free credits every month. Paid plans unlock private case storage — upload your documents and every tool references them.',
'pricing_strip_cta' => 'See all plans & pricing →',
// ── Nav + account + dashboard i18n (added 2026-05-23) ──────────
'nav_tools' => 'Tools',
'nav_dashboard' => 'Dashboard',
'nav_account' => 'Account',
'nav_login' => 'Log in',
'nav_logout' => 'Log out',
'credits_available' => 'Available credits',
'credits_monthly' => 'monthly',
'credits_bonus' => 'bonus',
'details_link' => 'Details',
'my_case' => 'My case',
'build_your_case' => 'Build your own case',
'upload_documents' => 'Upload documents',
'upgrade_from_plus' => 'Available from Plus NOK 129/month',
'my_corpus' => 'My corpus',
'open_corpus' => 'Open corpus',
'account_title' => 'Account',
'account_credits' => 'Credits & plan',
'account_profile' => 'Profile',
'account_team' => 'Team',
'account_usage' => 'Usage',
'renewal_date' => 'Next renewal',
'trial_active_label' => 'Trial active',
'trial_days_left' => 'days left',
'earn_credits_eyebrow'=> 'Earn 25 extra credits',
'survey_cta_text' => 'Answer 5 short questions — no sales pitch, just research.',
'survey_btn' => 'Take the survey',
'login_method_sso' => 'Google SSO',
'login_method_email' => 'Email & password',
'team_single_sso' => 'Single-user account (SSO)',
'usage_credits_used' => 'Credits used this month',
'usage_storage_used' => 'Storage used',
'usage_log_coming' => 'Detailed activity log coming soon',
'account_not_auth' => 'You must be logged in to view your account.',
],
'no' => [
'meta_title' => 'Do Better Norge - juridiske AI-verktøy',
@@ -383,6 +417,40 @@ function dbnToolsTranslations(): array
'pricing_strip_title' => 'Start gratis. Oppgrader når du trenger din egen sak.',
'pricing_strip_sub' => '30 gratis kreditter hver måned. Betalte planer låser opp privat sakslager — last opp dokumentene og alle verktøy refererer til dem.',
'pricing_strip_cta' => 'Se alle planer og priser →',
// ── Nav + account + dashboard i18n ──────────────────────────────
'nav_tools' => 'Verktøy',
'nav_dashboard' => 'Oversikt',
'nav_account' => 'Konto',
'nav_login' => 'Logg inn',
'nav_logout' => 'Logg ut',
'credits_available' => 'Tilgjengelige kreditter',
'credits_monthly' => 'månedlige',
'credits_bonus' => 'bonus',
'details_link' => 'Detaljer',
'my_case' => 'Min sak',
'build_your_case' => 'Bygg din egen sak',
'upload_documents' => 'Last opp dokumenter',
'upgrade_from_plus' => 'Tilgjengelig fra Plus 129 kr/mnd',
'my_corpus' => 'Min korpus',
'open_corpus' => 'Åpne korpus',
'account_title' => 'Konto',
'account_credits' => 'Kreditter og plan',
'account_profile' => 'Profil',
'account_team' => 'Team',
'account_usage' => 'Bruk',
'renewal_date' => 'Neste fornyelse',
'trial_active_label' => 'Prøveperiode aktiv',
'trial_days_left' => 'dager igjen',
'earn_credits_eyebrow'=> 'Tjen 25 ekstra kreditter',
'survey_cta_text' => 'Svar på 5 korte spørsmål — ingen salgspitch, bare research.',
'survey_btn' => 'Ta undersøkelsen',
'login_method_sso' => 'Google SSO',
'login_method_email' => 'E-post og passord',
'team_single_sso' => 'Enkeltbrukerkonto (SSO)',
'usage_credits_used' => 'Kreditter brukt denne måneden',
'usage_storage_used' => 'Lagring brukt',
'usage_log_coming' => 'Detaljert aktivitetslogg kommer snart',
'account_not_auth' => 'Du må være innlogget for å se kontoen din.',
],
'uk' => [
'meta_title' => 'Do Better Norge - юридичні AI інструменти',
@@ -541,6 +609,40 @@ function dbnToolsTranslations(): array
'pricing_strip_title' => 'Починайте безкоштовно. Оновлюйтеся, коли потрібна власна справа.',
'pricing_strip_sub' => '30 безкоштовних кредитів щомісяця. Платні плани відкривають приватне сховище справи — завантажте документи, і кожен інструмент посилається на них.',
'pricing_strip_cta' => 'Переглянути всі плани та ціни →',
// ── Nav + account + dashboard i18n ──────────────────────────────
'nav_tools' => 'Інструменти',
'nav_dashboard' => 'Огляд',
'nav_account' => 'Обліковий запис',
'nav_login' => 'Увійти',
'nav_logout' => 'Вийти',
'credits_available' => 'Доступні кредити',
'credits_monthly' => 'щомісячні',
'credits_bonus' => 'бонусні',
'details_link' => 'Деталі',
'my_case' => 'Моя справа',
'build_your_case' => 'Побудуйте власну справу',
'upload_documents' => 'Завантажити документи',
'upgrade_from_plus' => 'Доступно з Plus NOK 129/місяць',
'my_corpus' => 'Мій корпус',
'open_corpus' => 'Відкрити корпус',
'account_title' => 'Обліковий запис',
'account_credits' => 'Кредити та план',
'account_profile' => 'Профіль',
'account_team' => 'Команда',
'account_usage' => 'Використання',
'renewal_date' => 'Наступне оновлення',
'trial_active_label' => 'Пробний період активний',
'trial_days_left' => 'днів залишилось',
'earn_credits_eyebrow'=> 'Заробіть 25 додаткових кредитів',
'survey_cta_text' => 'Дайте відповідь на 5 коротких запитань — без реклами, лише дослідження.',
'survey_btn' => 'Пройти опитування',
'login_method_sso' => 'Google SSO',
'login_method_email' => 'Email та пароль',
'team_single_sso' => 'Одноосібний обліковий запис (SSO)',
'usage_credits_used' => 'Кредити використано цього місяця',
'usage_storage_used' => 'Використано сховища',
'usage_log_coming' => 'Детальний журнал активності незабаром',
'account_not_auth' => 'Увійдіть, щоб переглянути свій обліковий запис.',
],
'pl' => [
'meta_title' => 'Do Better Norge - prawne narzędzia AI',
@@ -699,6 +801,40 @@ function dbnToolsTranslations(): array
'pricing_strip_title' => 'Zacznij bezpłatnie. Rozszerz, gdy potrzebujesz własnej sprawy.',
'pricing_strip_sub' => '30 bezpłatnych kredytów miesięcznie. Płatne plany odblokują prywatne przechowywanie sprawy — prześlij dokumenty, a każde narzędzie do nich się odwoła.',
'pricing_strip_cta' => 'Zobacz wszystkie plany i cennik →',
// ── Nav + account + dashboard i18n ──────────────────────────────
'nav_tools' => 'Narzędzia',
'nav_dashboard' => 'Przegląd',
'nav_account' => 'Konto',
'nav_login' => 'Zaloguj',
'nav_logout' => 'Wyloguj',
'credits_available' => 'Dostępne kredyty',
'credits_monthly' => 'miesięczne',
'credits_bonus' => 'bonusowe',
'details_link' => 'Szczegóły',
'my_case' => 'Moja sprawa',
'build_your_case' => 'Zbuduj własną sprawę',
'upload_documents' => 'Prześlij dokumenty',
'upgrade_from_plus' => 'Dostępne od Plus NOK 129/miesiąc',
'my_corpus' => 'Mój korpus',
'open_corpus' => 'Otwórz korpus',
'account_title' => 'Konto',
'account_credits' => 'Kredyty i plan',
'account_profile' => 'Profil',
'account_team' => 'Zespół',
'account_usage' => 'Użycie',
'renewal_date' => 'Następne odnowienie',
'trial_active_label' => 'Okres próbny aktywny',
'trial_days_left' => 'dni pozostało',
'earn_credits_eyebrow'=> 'Zdobądź 25 dodatkowych kredytów',
'survey_cta_text' => 'Odpowiedz na 5 krótkich pytań — bez reklam, tylko badania.',
'survey_btn' => 'Wypełnij ankietę',
'login_method_sso' => 'Google SSO',
'login_method_email' => 'Email i hasło',
'team_single_sso' => 'Konto jednoosobowe (SSO)',
'usage_credits_used' => 'Kredyty użyte w tym miesiącu',
'usage_storage_used' => 'Użyte miejsce',
'usage_log_coming' => 'Szczegółowy dziennik aktywności wkrótce',
'account_not_auth' => 'Musisz być zalogowany, aby zobaczyć swoje konto.',
],
];
}
+3 -32
View File
@@ -31,6 +31,7 @@ if ($layoutAuthUser !== null) {
}
$layoutReturnUrl = urlencode($_SERVER['REQUEST_URI'] ?? '/');
// $layoutAuthUser / $layoutUserDisplay kept for backwards compat (now also used by nav.php internally)
?>
<!doctype html>
<html lang="<?= htmlspecialchars($uiLang) ?>">
@@ -58,6 +59,8 @@ window.DBN_FREE_TIER_BALANCE = <?= $layoutFreeTierBalance ?>;
</div>
<?php endif; ?>
<?php include __DIR__ . '/nav.php'; ?>
<?php if ($layoutIsGuest): ?>
<div id="guestBanner" class="guest-banner" role="alert" aria-live="polite">
<span>Du er ikke innlogget — verktøyene krever konto for å fungere.</span>
@@ -67,38 +70,6 @@ window.DBN_FREE_TIER_BALANCE = <?= $layoutFreeTierBalance ?>;
<?php endif; ?>
<main id="appShell" class="app-shell">
<header class="topbar">
<div>
<p class="eyebrow"><?= htmlspecialchars(dbnToolsT('brand_line', $uiLang)) ?></p>
<h1><?= htmlspecialchars(dbnToolsT('suite_title', $uiLang)) ?> <span class="title-mark">.</span> <?= htmlspecialchars(dbnToolsT('workspace_title', $uiLang)) ?></h1>
<div class="case-no">
<span class="pulse"></span>
<span>family-legal</span>
<span class="case-sep">.</span>
<span><?= htmlspecialchars(dbnToolsT('retention', $uiLang)) ?></span>
</div>
</div>
<div class="topbar-actions">
<nav class="shell-lang-switcher" aria-label="Language">
<?php foreach (dbnToolsSupportedLanguages() as $langCode): ?>
<a href="<?= htmlspecialchars($langPath . '?lang=' . $langCode) ?>" class="<?= $langCode === $uiLang ? 'is-active' : '' ?>"><?= htmlspecialchars(dbnToolsLanguageLabel($langCode)) ?></a>
<?php endforeach; ?>
</nav>
<?php if ($layoutIsGuest): ?>
<a href="/?return=<?= $layoutReturnUrl ?>" class="secondary-button" style="text-decoration:none;">Logg inn</a>
<?php else: ?>
<a href="/dashboard/" class="secondary-button" style="text-decoration:none;">📚 Min korpus</a>
<span id="healthPill" class="status-pill"><?= htmlspecialchars(dbnToolsT('session_active', $uiLang)) ?></span>
<button id="healthButton" class="secondary-button" type="button"><?= htmlspecialchars(dbnToolsT('health', $uiLang)) ?></button>
<span class="topbar-user">
<?php if ($layoutUserDisplay !== ''): ?>
<span class="topbar-user__name" title="<?= htmlspecialchars($layoutAuthUser['email'] ?? '') ?>"><?= htmlspecialchars($layoutUserDisplay) ?></span>
<?php endif; ?>
<a href="/api/logout.php" class="topbar-user__logout">Logg ut</a>
</span>
<?php endif; ?>
</div>
</header>
<section class="manifesto" role="banner">
<div class="manifesto-copy">
+1 -18
View File
@@ -73,25 +73,8 @@ window.DBN_DASHBOARD = {
};
</script>
<?php include __DIR__ . '/nav.php'; ?>
<div class="dash-shell">
<header class="dash-topbar" role="banner">
<a class="dash-brand" href="/dashboard/">
<span class="dash-brand__mark">⚖</span>
<span class="dash-brand__text">
<strong>Min korpus</strong>
<small>Do Better Norge</small>
</span>
</a>
<nav class="dash-topbar__tools" aria-label="Tools">
<a href="/dashboard.php" class="dash-topbar__link">← Tilbake til verktøy</a>
</nav>
<div class="dash-topbar__user">
<?php if ($dashUserDisplay !== ''): ?>
<span class="dash-topbar__username" title="<?= htmlspecialchars($dashAuthUser['email'] ?? '') ?>"><?= htmlspecialchars($dashUserDisplay) ?></span>
<?php endif; ?>
<a href="/api/logout.php" class="dash-topbar__logout">Logg ut</a>
</div>
</header>
<div class="dash-layout">
<nav class="dash-sidebar" aria-label="Dashboard sections">
+102
View File
@@ -0,0 +1,102 @@
<?php
declare(strict_types=1);
/**
* Unified site navbar — included by layout.php, layout_dashboard.php, and standalone pages.
* Assumes bootstrap.php (and i18n.php) has already been required.
*/
$_navGuest = !dbnToolsIsAuthenticated();
$_navAuth = $_navGuest ? null : dbnToolsAuthenticatedUser();
$_navEmail = (string)($_navAuth['email'] ?? '');
$_navUser = $_navEmail !== '' ? (strstr($_navEmail, '@', true) ?: $_navEmail) : '';
$_navLang = dbnToolsCurrentLanguage();
$_navTools = dbnToolsLaunchedTools($_navLang);
$_navPath = strtok((string)($_SERVER['REQUEST_URI'] ?? '/'), '?') ?: '/';
$_navOnDash = str_starts_with($_navPath, '/dashboard');
$_navReturnUrl = urlencode($_navPath);
?>
<nav class="dbn-nav" role="navigation" aria-label="<?= htmlspecialchars(dbnToolsT('suite_title', $_navLang)) ?>">
<a class="dbn-nav__brand" href="/dashboard.php">
<span class="dbn-nav__brandmark" aria-hidden="true">⚖</span>
<span class="dbn-nav__brandname">Do Better Norge</span>
</a>
<div class="dbn-nav__links" role="menubar">
<div class="dbn-nav__dropdown" data-nav-dropdown>
<button class="dbn-nav__link dbn-nav__dropper"
type="button"
aria-haspopup="menu"
aria-expanded="false"
aria-controls="navToolsPanel">
<?= htmlspecialchars(dbnToolsT('nav_tools', $_navLang)) ?>
<span class="dbn-nav__caret" aria-hidden="true">▾</span>
</button>
<div class="dbn-nav__panel" id="navToolsPanel" role="menu" aria-label="<?= htmlspecialchars(dbnToolsT('nav_tools', $_navLang)) ?>">
<?php foreach ($_navTools as $slug => $item): ?>
<a class="dbn-nav__panel-item" href="<?= htmlspecialchars($item['url']) ?>" role="menuitem">
<span class="dbn-nav__panel-badge" aria-hidden="true"><?= htmlspecialchars($item['icon']) ?></span>
<span class="dbn-nav__panel-label"><?= htmlspecialchars($item['label']) ?></span>
<span class="dbn-nav__panel-sub"><?= htmlspecialchars($item['sub']) ?></span>
</a>
<?php endforeach; ?>
</div>
</div>
<a class="dbn-nav__link<?= $_navOnDash ? ' is-active' : '' ?>"
href="/dashboard.php"
role="menuitem"
<?= $_navOnDash ? 'aria-current="page"' : '' ?>>
<?= htmlspecialchars(dbnToolsT('nav_dashboard', $_navLang)) ?>
</a>
</div>
<div class="dbn-nav__right">
<nav class="dbn-nav__langs" aria-label="Language">
<?php foreach (dbnToolsSupportedLanguages() as $lc): ?>
<a href="<?= htmlspecialchars($_navPath . '?lang=' . $lc) ?>"
class="dbn-nav__lang<?= $lc === $_navLang ? ' is-active' : '' ?>"
hreflang="<?= htmlspecialchars($lc) ?>"
aria-label="<?= htmlspecialchars(dbnToolsLanguageName($lc)) ?>"
title="<?= htmlspecialchars(dbnToolsLanguageName($lc)) ?>"><?= htmlspecialchars(strtoupper($lc)) ?></a>
<?php endforeach; ?>
</nav>
<?php if ($_navGuest): ?>
<a href="/?return=<?= $_navReturnUrl ?>" class="dbn-nav__login">
<?= htmlspecialchars(dbnToolsT('nav_login', $_navLang)) ?>
</a>
<?php else: ?>
<a class="dbn-nav__account-link" href="/account.php" title="<?= htmlspecialchars($_navEmail) ?>">
<?php if ($_navUser !== ''): ?>
<span class="dbn-nav__username"><?= htmlspecialchars($_navUser) ?></span>
<?php endif; ?>
<span class="dbn-nav__account-badge"><?= htmlspecialchars(dbnToolsT('nav_account', $_navLang)) ?></span>
</a>
<a href="/api/logout.php" class="dbn-nav__logout">
<?= htmlspecialchars(dbnToolsT('nav_logout', $_navLang)) ?>
</a>
<?php endif; ?>
</div>
</nav>
<script>
(function () {
var dropdowns = document.querySelectorAll('[data-nav-dropdown]');
dropdowns.forEach(function (dd) {
var btn = dd.querySelector('.dbn-nav__dropper');
var panel = dd.querySelector('.dbn-nav__panel');
if (!btn || !panel) return;
function open() { dd.classList.add('is-open'); btn.setAttribute('aria-expanded', 'true'); }
function close() { dd.classList.remove('is-open'); btn.setAttribute('aria-expanded', 'false'); }
function toggle() { dd.classList.contains('is-open') ? close() : open(); }
btn.addEventListener('click', function (e) { e.stopPropagation(); toggle(); });
document.addEventListener('click', function (e) {
if (!dd.contains(e.target)) close();
});
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape') close();
});
});
}());
</script>