feat: Legal Tools v1 — multilingual landing, dashboard, SSO bridge

- Public landing page at / for unauthenticated users (EN/NO/UK/PL)
- Authenticated / shows Case Workbench dashboard with manifesto strip,
  stats, and launched-tool grid (Transcribe, Timeline, BVJ, Advocate,
  Deep Research, Corpus)
- Added includes/i18n.php with full 4-language translation layer
- Extended layout.php to Case Workbench shell with tool rail, lang switcher
- AI output language normalization extended to en/no/uk/pl in PHP agents
- SSO token validation in bootstrap.php / index.php (dobetternorge.no bridge)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-15 22:53:27 +02:00
parent ba6c197f1b
commit a3d46f9756
19 changed files with 1149 additions and 238 deletions
+498
View File
@@ -14,6 +14,461 @@
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
/* Do Better Norge v1 workbench */
:root {
--dbn-paper: #f6f2ea;
--dbn-ink: #16130f;
--dbn-blue: #00205b;
--dbn-red: #ba0c2f;
--dbn-teal: #0f766e;
--dbn-line: rgba(22, 19, 15, 0.16);
}
body {
background:
linear-gradient(90deg, rgba(0, 32, 91, 0.055) 1px, transparent 1px),
linear-gradient(180deg, rgba(186, 12, 47, 0.04), transparent 38rem),
var(--dbn-paper);
background-size: 44px 44px, auto, auto;
}
.app-shell {
padding: 18px clamp(14px, 2vw, 28px) 34px;
}
.topbar {
align-items: flex-start;
border-bottom: 2px solid rgba(22, 19, 15, 0.14);
padding-bottom: 16px;
}
.topbar h1 {
color: var(--dbn-ink);
font-size: clamp(1.8rem, 2.8vw, 3.2rem);
line-height: 0.95;
letter-spacing: 0;
}
.title-mark {
color: var(--dbn-red);
}
.case-no {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
margin-top: 9px;
color: #5d574e;
font-size: 0.86rem;
font-weight: 700;
}
.case-sep {
opacity: 0.45;
}
.pulse {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--dbn-teal);
box-shadow: 0 0 0 5px rgba(15, 118, 110, 0.12);
}
.shell-lang-switcher {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px;
border: 1px solid var(--dbn-line);
border-radius: 8px;
background: rgba(255, 255, 255, 0.64);
}
.shell-lang-switcher a {
display: inline-flex;
min-width: 36px;
min-height: 30px;
align-items: center;
justify-content: center;
border-radius: 6px;
color: var(--dbn-blue);
font-size: 0.78rem;
font-weight: 900;
text-decoration: none;
}
.shell-lang-switcher a.is-active,
.shell-lang-switcher a:hover {
background: var(--dbn-blue);
color: #fff;
}
.manifesto {
max-width: 1500px;
margin: 0 auto 12px;
display: grid;
grid-template-columns: minmax(0, 1.15fr) minmax(360px, 0.85fr);
gap: 14px;
border: 1px solid rgba(22, 19, 15, 0.18);
border-radius: 8px;
background:
linear-gradient(135deg, rgba(186, 12, 47, 0.14), transparent 44%),
linear-gradient(90deg, rgba(255, 255, 255, 0.72), rgba(255, 255, 255, 0.42));
overflow: hidden;
}
.manifesto-copy {
padding: clamp(20px, 3vw, 36px);
}
.manifesto-eyebrow {
margin: 0 0 8px;
color: var(--dbn-red);
font-size: 0.78rem;
font-weight: 900;
letter-spacing: 0;
text-transform: uppercase;
}
.manifesto-title {
max-width: 780px;
margin: 0;
color: var(--dbn-ink);
font-size: clamp(2rem, 5vw, 5.4rem);
line-height: 0.92;
letter-spacing: 0;
}
.manifesto-sub {
max-width: 760px;
margin: 14px 0 0;
color: #3f3932;
font-size: clamp(1rem, 1.3vw, 1.18rem);
line-height: 1.45;
}
.manifesto-stats {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
border-left: 1px solid rgba(22, 19, 15, 0.16);
}
.manifesto-stat {
min-height: 120px;
padding: 20px;
border: 0 solid rgba(22, 19, 15, 0.13);
border-bottom-width: 1px;
border-right-width: 1px;
background: rgba(255, 255, 255, 0.38);
}
.manifesto-stat strong {
display: block;
color: var(--dbn-blue);
font-size: clamp(1.8rem, 3vw, 3.2rem);
line-height: 1;
}
.manifesto-stat span {
display: block;
margin-top: 8px;
color: #4b453d;
font-size: 0.82rem;
font-weight: 800;
text-transform: uppercase;
}
.workspace {
grid-template-columns: 204px minmax(0, 1fr) 370px;
}
.tool-rail,
.tool-panel,
.reasoning-panel,
.gate-panel,
.cap-card,
.dashboard-tool-card {
border-color: rgba(22, 19, 15, 0.15);
box-shadow: 0 18px 45px rgba(22, 19, 15, 0.08);
}
.tool-tab {
position: relative;
min-height: 74px;
overflow: hidden;
}
.tool-tab em {
position: absolute;
right: 10px;
bottom: 8px;
color: rgba(0, 32, 91, 0.22);
font-size: 1.25rem;
font-style: normal;
font-weight: 900;
}
.tool-tab.is-active {
background: #fdf8ef;
border-color: rgba(186, 12, 47, 0.28);
color: var(--dbn-ink);
}
.tool-tab.is-active::before {
content: "";
position: absolute;
inset: 0 auto 0 0;
width: 4px;
background: var(--dbn-red);
}
.dashboard-shell .disclaimer {
margin-bottom: 18px;
}
.tool-dashboard-grid {
max-width: 1500px;
margin: 0 auto;
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 14px;
}
.dashboard-tool-card {
position: relative;
min-height: 250px;
padding: 22px;
border: 1px solid var(--dbn-line);
border-radius: 8px;
background: rgba(255, 255, 255, 0.82);
color: var(--dbn-ink);
text-decoration: none;
overflow: hidden;
}
.dashboard-tool-card:hover {
transform: translateY(-2px);
border-color: rgba(186, 12, 47, 0.36);
}
.dashboard-tool-card__icon {
color: rgba(0, 32, 91, 0.14);
font-size: 4.8rem;
font-weight: 900;
line-height: 1;
}
.dashboard-tool-card__badge {
position: absolute;
top: 20px;
right: 18px;
color: var(--dbn-red);
font-size: 0.75rem;
font-weight: 900;
text-transform: uppercase;
}
.dashboard-tool-card h2 {
margin: 12px 0 8px;
font-size: 1.45rem;
}
.dashboard-tool-card p {
color: #514b43;
line-height: 1.48;
}
.dashboard-tool-card strong {
position: absolute;
bottom: 20px;
color: var(--dbn-blue);
}
.dbn-public-tools {
min-height: 100vh;
}
.showcase-header {
background: rgba(255, 255, 255, 0.72);
border-bottom: 1px solid var(--dbn-line);
}
.showcase-header-inner {
align-items: flex-start;
padding-top: 22px;
padding-bottom: 22px;
}
.showcase-header-actions {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 10px;
}
.showcase-title {
max-width: 980px;
color: var(--dbn-ink);
font-size: clamp(2.4rem, 6vw, 6.4rem);
line-height: 0.9;
letter-spacing: 0;
}
.showcase-tagline {
max-width: 780px;
color: #403a33;
font-size: clamp(1.05rem, 1.6vw, 1.32rem);
}
.landing-hero {
max-width: 1500px;
margin: 18px auto;
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(280px, 420px);
gap: 14px;
padding: clamp(22px, 4vw, 54px);
border: 1px solid var(--dbn-line);
border-radius: 8px;
background:
linear-gradient(135deg, rgba(0, 32, 91, 0.12), transparent 34%),
linear-gradient(315deg, rgba(186, 12, 47, 0.16), transparent 40%),
#fffaf2;
}
.landing-hero h2 {
max-width: 840px;
margin: 0;
font-size: clamp(2rem, 5vw, 5.3rem);
line-height: 0.92;
letter-spacing: 0;
}
.landing-hero p {
max-width: 740px;
color: #403a33;
font-size: 1.08rem;
line-height: 1.5;
}
.landing-hero__stats {
display: grid;
gap: 10px;
align-content: center;
}
.landing-hero__stats div {
padding: 18px;
border: 1px solid rgba(0, 32, 91, 0.16);
border-radius: 8px;
background: rgba(255, 255, 255, 0.7);
}
.landing-hero__stats strong {
display: block;
color: var(--dbn-blue);
font-size: 2.5rem;
}
.landing-hero__stats span {
color: #514b43;
font-weight: 800;
}
.cap-grid--six {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.evidence-inner {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.evidence-inner article {
padding: 20px;
border-left: 4px solid rgba(186, 12, 47, 0.45);
background: rgba(255, 255, 255, 0.58);
}
.google-access-button {
display: flex;
align-items: center;
justify-content: center;
min-height: 48px;
margin: 18px 0;
border-radius: 8px;
background: var(--dbn-blue);
color: #fff;
font-weight: 900;
text-decoration: none;
}
.fallback-login {
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid var(--dbn-line);
}
.fallback-login summary {
cursor: pointer;
color: #655d53;
font-weight: 800;
}
@media (max-width: 1120px) {
.workspace {
grid-template-columns: 1fr;
}
.tool-rail {
flex-direction: row;
overflow-x: auto;
}
.tool-tab {
min-width: 170px;
}
.manifesto,
.landing-hero {
grid-template-columns: 1fr;
}
.manifesto-stats {
border-left: 0;
}
.tool-dashboard-grid,
.cap-grid--six,
.evidence-inner {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 680px) {
.topbar,
.showcase-header-inner {
flex-direction: column;
}
.topbar-actions,
.showcase-header-actions {
width: 100%;
justify-content: flex-start;
}
.manifesto-stats,
.tool-dashboard-grid,
.cap-grid--six,
.evidence-inner {
grid-template-columns: 1fr;
}
.dashboard-tool-card {
min-height: 220px;
}
}
* {
box-sizing: border-box;
}
@@ -3712,3 +4167,46 @@ a.dr-source-title-link:hover {
position: relative;
top: -1px;
}
/* v1 final cascade overrides */
body {
background:
linear-gradient(90deg, rgba(0, 32, 91, 0.055) 1px, transparent 1px),
linear-gradient(180deg, rgba(186, 12, 47, 0.04), transparent 38rem),
var(--dbn-paper);
background-size: 44px 44px, auto, auto;
color: var(--dbn-ink);
}
.app-shell { padding: 18px clamp(14px, 2vw, 28px) 34px; }
.topbar { align-items: flex-start; border-bottom: 2px solid rgba(22, 19, 15, 0.14); padding-bottom: 16px; }
.topbar h1 { color: var(--dbn-ink); font-size: clamp(1.8rem, 2.8vw, 3.2rem); line-height: .95; letter-spacing: 0; }
.workspace { grid-template-columns: 204px minmax(0, 1fr) 370px; }
.tool-rail, .tool-panel, .reasoning-panel, .gate-panel, .cap-card, .dashboard-tool-card {
border-color: rgba(22, 19, 15, 0.15);
box-shadow: 0 18px 45px rgba(22, 19, 15, 0.08);
}
.tool-tab { position: relative; min-height: 74px; overflow: hidden; }
.tool-tab em { position: absolute; right: 10px; bottom: 8px; color: rgba(0, 32, 91, .22); font-size: 1.25rem; font-style: normal; font-weight: 900; }
.tool-tab.is-active { background: #fdf8ef; border-color: rgba(186, 12, 47, .28); color: var(--dbn-ink); }
.tool-tab.is-active::before { content: ""; position: absolute; inset: 0 auto 0 0; width: 4px; background: var(--dbn-red); }
.showcase-title { max-width: 980px; color: var(--dbn-ink); font-size: clamp(2.4rem, 6vw, 6.4rem); line-height: .9; letter-spacing: 0; }
.showcase-tagline { max-width: 780px; color: #403a33; font-size: clamp(1.05rem, 1.6vw, 1.32rem); }
.cap-grid--six { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.evidence-inner { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.google-access-button { display: flex; align-items: center; justify-content: center; min-height: 48px; margin: 18px 0; border-radius: 8px; background: var(--dbn-blue); color: #fff; font-weight: 900; text-decoration: none; }
.fallback-login { margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--dbn-line); }
.fallback-login summary { cursor: pointer; color: #655d53; font-weight: 800; }
@media (max-width: 1120px) {
.workspace { grid-template-columns: 1fr; }
.tool-rail { flex-direction: row; overflow-x: auto; }
.tool-tab { min-width: 170px; }
.tool-dashboard-grid, .cap-grid--six, .evidence-inner { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 680px) {
.topbar, .showcase-header-inner { flex-direction: column; }
.topbar-actions, .showcase-header-actions { width: 100%; justify-content: flex-start; }
.manifesto-stats, .tool-dashboard-grid, .cap-grid--six, .evidence-inner { grid-template-columns: 1fr; }
}
+3 -1
View File
@@ -3,7 +3,7 @@
'use strict';
const els = {};
let lang = 'en';
let lang = window.DBN_TOOLS_LANG || localStorage.getItem('dbn-ui-lang') || 'en';
let uploadFiles = [];
let lastResult = null;
let branchContext = null;
@@ -121,10 +121,12 @@
function bindLang() {
els.langButtons.forEach((b) => {
b.classList.toggle('is-active', b.dataset.lang === lang);
b.addEventListener('click', () => {
els.langButtons.forEach((x) => x.classList.remove('is-active'));
b.classList.add('is-active');
lang = b.dataset.lang || 'en';
localStorage.setItem('dbn-ui-lang', lang);
});
});
}
+3 -1
View File
@@ -3,7 +3,7 @@
'use strict';
const els = {};
let lang = 'en';
let lang = window.DBN_TOOLS_LANG || localStorage.getItem('dbn-ui-lang') || 'en';
let uploadFiles = [];
let lastResult = null;
let branchContext = null;
@@ -148,10 +148,12 @@
function bindLang() {
els.langButtons.forEach((b) => {
b.classList.toggle('is-active', b.dataset.lang === lang);
b.addEventListener('click', () => {
els.langButtons.forEach((x) => x.classList.remove('is-active'));
b.classList.add('is-active');
lang = b.dataset.lang || 'en';
localStorage.setItem('dbn-ui-lang', lang);
});
});
}
+3 -1
View File
@@ -3,7 +3,7 @@
'use strict';
const els = {};
let lang = 'en';
let lang = window.DBN_TOOLS_LANG || localStorage.getItem('dbn-ui-lang') || 'en';
let uploadFiles = [];
let lastResult = null;
let branchContext = null;
@@ -102,10 +102,12 @@
function bindLang() {
els.langButtons.forEach((b) => {
b.classList.toggle('is-active', b.dataset.lang === lang);
b.addEventListener('click', () => {
els.langButtons.forEach((x) => x.classList.remove('is-active'));
b.classList.add('is-active');
lang = b.dataset.lang || 'en';
localStorage.setItem('dbn-ui-lang', lang);
});
});
}
+36 -9
View File
@@ -391,7 +391,14 @@ const VOCAB_PRESETS = {
custom: '',
};
let uiLang = localStorage.getItem('dbn-ui-lang') || 'en';
let uiLang = window.DBN_TOOLS_LANG || localStorage.getItem('dbn-ui-lang') || 'en';
function syncOutputLanguage(lang) {
const normalized = ['en', 'no', 'uk', 'pl'].includes(lang) ? lang : 'en';
document.querySelectorAll('input[name="language"]').forEach((input) => {
if (input.value === normalized) input.checked = true;
});
}
const TRANSCRIBE_I18N = {
en: {
@@ -670,6 +677,7 @@ function currentUiT(key, ...args) {
function applyTranscribeI18n(lang) {
uiLang = lang;
localStorage.setItem('dbn-ui-lang', lang);
syncOutputLanguage(lang);
document.querySelectorAll('[data-i18n]').forEach((el) => {
const text = currentUiT(el.dataset.i18n);
if (text != null) el.textContent = text;
@@ -695,6 +703,7 @@ function currentRedactT(key) {
function applyRedactI18n(lang) {
uiLang = lang;
localStorage.setItem('dbn-ui-lang', lang);
syncOutputLanguage(lang);
document.querySelectorAll('[data-i18n]').forEach((el) => {
const text = currentRedactT(el.dataset.i18n);
if (text != null) el.textContent = text;
@@ -750,6 +759,7 @@ function currentTimelineT(key) {
function applyTimelineI18n(lang) {
uiLang = lang;
localStorage.setItem('dbn-ui-lang', lang);
syncOutputLanguage(lang);
document.querySelectorAll('[data-i18n]').forEach((el) => {
const text = currentTimelineT(el.dataset.i18n);
if (text != null) el.textContent = text;
@@ -952,7 +962,7 @@ document.addEventListener('DOMContentLoaded', () => {
});
els.form?.addEventListener('submit', runTool);
els.passcodeForm?.addEventListener('submit', submitPasscode);
els.healthButton.addEventListener('click', checkHealth);
els.healthButton?.addEventListener('click', checkHealth);
setupUpload();
setupAliases();
setupAudio();
@@ -968,7 +978,7 @@ document.addEventListener('DOMContentLoaded', () => {
if (document.getElementById('uiLangSwitcher')) {
applyTranscribeI18n(uiLang);
}
els.results.addEventListener('click', (e) => {
els.results?.addEventListener('click', (e) => {
if (e.target.closest('#exportCsvBtn')) exportTimelineCSV(lastTimelineEvents);
if (e.target.closest('#dlTxt')) downloadTranscriptTxt();
if (e.target.closest('#dlSrt')) downloadTranscriptSrt();
@@ -978,7 +988,9 @@ document.addEventListener('DOMContentLoaded', () => {
if (e.target.closest('#rdlDocx')) downloadRedactedDocx();
});
const activeTool = document.body.dataset.activeTool || state.activeTool;
setTool(activeTool);
if (els.form && tools[activeTool]) {
setTool(activeTool);
}
if (state.authenticated) {
checkHealth();
@@ -990,18 +1002,24 @@ document.addEventListener('DOMContentLoaded', () => {
function setTool(toolName) {
state.activeTool = toolName;
const tool = tools[toolName];
if (!tool || !els.toolKind || !els.input) return;
const serverRenderedShell = els.tabs.some((tab) => tab.tagName === 'A');
els.tabs.forEach((button) => {
const active = button.dataset.tool === toolName;
button.classList.toggle('is-active', active);
button.setAttribute('aria-pressed', String(active));
});
els.toolKind.textContent = tool.kind;
els.toolTitle.textContent = tool.title;
els.toolBadge.textContent = tool.badge;
els.inputLabel.textContent = tool.label;
if (!serverRenderedShell) {
els.toolKind.textContent = tool.kind;
els.toolTitle.textContent = tool.title;
els.toolBadge.textContent = tool.badge;
els.inputLabel.textContent = tool.label;
}
els.input.value = '';
els.input.placeholder = tool.placeholder;
if (!serverRenderedShell) {
els.input.placeholder = tool.placeholder;
}
els.languageControl.classList.toggle('is-hidden', !tool.usesLanguage);
els.redactionControl.classList.toggle('is-hidden', toolName !== 'redact');
els.uploadZone.classList.toggle('is-hidden', toolName !== 'redact' && toolName !== 'timeline');
@@ -1031,6 +1049,12 @@ async function submitPasscode(event) {
throw new Error(data.error?.message || 'Credentials were not accepted.');
}
state.authenticated = true;
if (!els.app) {
const params = new URLSearchParams(window.location.search);
const dest = params.get('return') || '/';
window.location.href = dest.startsWith('/') && !dest.startsWith('//') ? dest : '/';
return;
}
els.gate.classList.add('is-hidden');
els.app.classList.remove('is-hidden');
els.loginPassword.value = '';
@@ -1116,6 +1140,7 @@ function resetUpload() {
}
function setupUpload() {
if (!els.uploadZone || !els.uploadInput) return;
els.uploadZone.addEventListener('dragover', (e) => {
e.preventDefault();
els.uploadZone.classList.add('is-drag-over');
@@ -1217,6 +1242,7 @@ async function handleFiles(fileList) {
}
function setupAliases() {
if (!els.addAliasRow || !els.aliasRows) return;
els.addAliasRow.addEventListener('click', () => {
const row = document.createElement('div');
row.className = 'alias-row';
@@ -1287,6 +1313,7 @@ async function postJson(url, payload) {
function setBusy(isBusy) {
const button = document.querySelector('#runButton');
if (!button) return;
button.disabled = isBusy;
if (state.activeTool === 'transcribe') {
button.textContent = isBusy ? currentUiT('running') : currentUiT('run');