feat(shared): add Playground design system v0.1 with Tier 1+2 components

Aksel/Digdir-aligned design system for plugin Playgrounds — visual self-service
UIs that complement terminal slash-commands. Targets ms-ai-architect, okr,
llm-security, ultraplan-local, config-audit. Built for Norwegian public sector
decision-makers plus developer power-users — one visual family, two info
densities.

Generated by claude.ai/design (Anthropic) in a dialog-based design session
driven by a comprehensive brief covering all five target plugins, Aksel/Digdir
conventions, and domain-specific visual standards (NS 5814 ROS matrices, EU AI
Act 4-tier pyramide, Doerr OKR scoring, NIST CSF, OWASP threat modeling).
Per Anthropic Consumer Terms §4, ownership of outputs is assigned to the user;
licensed MIT.

shared/playground-design-system/ (5874 lines CSS + JSON):
- tokens.css: Inter font, Digdir blue #0062BA, deuteranopia-safe severity ramp,
  distinct severity-red (#A40E26) vs failure-red (#7D1A1A), plugin scope colors,
  light + dark themes
- base.css: reset, typography (17px body, 65ch measure), focus rings, buttons,
  badges, forms, Aksel 3-tier inline messages, prefers-reduced-motion support
- components.css: Tier 1 — radar/spider, 5x5 matrix-heatmap (bottom-left
  origin, ROS/DPIA), findings-browser, critique-card, wizard/stepper,
  live-meter with antipattern lints
- components-tier2.css: Tier 2 — decision-tree, traffic-lights with rationale,
  diff-review, treemap, distribution P10/P50/P90, command-pipeline output, AI
  Act 4-color pyramide, pipeline-cockpit, verdict-pill + 5-band risk-meter,
  codepoint-reveal (Unicode steg), small-multiples grid (16-cat posture),
  OWASP badges (LLM/ASI/AST/MCP)
- print.css: A4 stylesheet with BW severity hatching, kommune-logo slot,
  signature lines for offentlige dokumenter
- schemas/: finding.schema.json, okr-set.schema.json, ros-threat.schema.json
- README.md: usage guide, design principles, component reference, provenance

shared/playground-examples/:
- index.html: system showcase with all components live
- ros-lier-kommune.html: Lier kommune Copilot ROS-rapport (Scenario A)
- okr-baerum.html: Baerum kommune T2-2026 OKR live writer (Scenario B)
- security-vegvesen.html: SVV ToxicSkills findings review, 85 funn BLOCK
  (Scenario C)
- templates.html: A4 print template demos
- ros-app.js + ros-data.js: Scenario A interactivity

WCAG 2.1 AA throughout (UU-loven krav for offentlig sektor): focus rings, ARIA
attributes, keyboard navigation, severity numerical redundancy for deuteranopia
and BW print, semantic HTML.

Known limitation: Inter loaded via Google Fonts CDN violates self-contained
no-CDN constraint. System-stack fallback works offline. Self-host woff2 files
in Phase 2.
This commit is contained in:
Kjell Tore Guttormsen 2026-05-02 06:59:19 +02:00
commit 4a2bf3567a
16 changed files with 6065 additions and 0 deletions

View file

@ -0,0 +1,868 @@
<!doctype html>
<html lang="nb">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>OKR live-writer — Bærum kommune — T2 2026</title>
<link rel="stylesheet" href="../playground-design-system/tokens.css" />
<link rel="stylesheet" href="../playground-design-system/base.css" />
<link rel="stylesheet" href="../playground-design-system/components.css" />
<link rel="stylesheet" href="../playground-design-system/components-tier2.css" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&family=Source+Serif+4:wght@400;600&display=swap" rel="stylesheet">
<style>
.layout { display: grid; grid-template-rows: auto 1fr; min-height: 100vh; }
.page { padding: var(--space-8) 0 var(--space-16); }
.page__header {
display: flex; justify-content: space-between; align-items: flex-end;
gap: var(--space-6); margin-bottom: var(--space-6);
border-bottom: 1px solid var(--color-border-subtle);
padding-bottom: var(--space-4);
}
.page__title { display: flex; flex-direction: column; gap: 4px; }
.page__eyebrow {
font-size: var(--font-size-xs); text-transform: uppercase; letter-spacing: 0.1em;
color: var(--color-scope-okr); font-weight: var(--font-weight-semibold);
}
.page__meta { display: flex; gap: var(--space-4); font-size: var(--font-size-sm); color: var(--color-text-secondary); }
.page__meta-item { display: flex; gap: 6px; align-items: baseline; }
.page__meta-label { color: var(--color-text-tertiary); font-size: var(--font-size-xs); text-transform: uppercase; letter-spacing: 0.06em; }
/* Two-pane writer layout */
.writer {
display: grid;
grid-template-columns: 1.4fr 1fr;
gap: var(--space-6);
align-items: start;
}
.pane {
background: var(--color-surface);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-md);
overflow: hidden;
}
.pane__head {
padding: 10px 16px;
background: var(--color-bg-soft);
border-bottom: 1px solid var(--color-border-subtle);
display: flex; justify-content: space-between; align-items: center;
gap: var(--space-3);
}
.pane__title {
font-size: var(--font-size-sm); font-weight: var(--font-weight-semibold);
color: var(--color-text-primary); margin: 0;
display: flex; align-items: center; gap: 8px;
}
.pane__title-eyebrow {
font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em;
color: var(--color-text-tertiary); font-weight: var(--font-weight-medium);
}
.pane__body { padding: var(--space-5); }
/* Editor styling */
.editor {
font-family: var(--font-family-serif);
font-size: 18px;
line-height: 1.7;
min-height: 380px;
outline: none;
}
.editor h2 {
font-family: var(--font-family-sans);
font-size: var(--font-size-lg);
font-weight: var(--font-weight-semibold);
margin: 0 0 var(--space-2);
color: var(--color-text-primary);
}
.editor .objective {
font-family: var(--font-family-serif);
font-size: 22px;
font-weight: 600;
line-height: 1.35;
margin-bottom: var(--space-5);
color: var(--color-text-primary);
}
.editor .kr {
margin: 0 0 var(--space-4);
padding: var(--space-3) var(--space-4);
background: var(--color-bg-soft);
border-radius: var(--radius-sm);
border-left: 3px solid var(--color-scope-okr);
position: relative;
}
.editor .kr-label {
font-family: var(--font-family-mono);
font-size: 11px;
color: var(--color-scope-okr);
font-weight: var(--font-weight-semibold);
letter-spacing: 0.06em;
margin-bottom: 4px;
display: block;
}
.editor .kr-text { font-family: var(--font-family-serif); font-size: 18px; line-height: 1.5; }
/* Inline highlight overlays in the editor */
.hl {
background-image: linear-gradient(to bottom, transparent 0, transparent 60%, var(--hl-color, rgba(191,135,0,0.25)) 60%, var(--hl-color, rgba(191,135,0,0.25)) 100%);
cursor: help;
border-bottom: 2px solid var(--hl-border, var(--color-severity-medium));
padding-bottom: 1px;
}
.hl[data-issue="missing-baseline"] { --hl-color: rgba(191,135,0,0.22); --hl-border: var(--color-severity-medium); }
.hl[data-issue="vague-verb"] { --hl-color: rgba(204,90,0,0.22); --hl-border: var(--color-severity-high); }
.hl[data-issue="activity"] { --hl-color: rgba(164,14,38,0.18); --hl-border: var(--color-severity-critical); }
.hl[data-issue="no-deadline"] { --hl-color: rgba(191,135,0,0.22); --hl-border: var(--color-severity-medium); }
.hl[data-issue="no-metric"] { --hl-color: rgba(204,90,0,0.22); --hl-border: var(--color-severity-high); }
/* Score header */
.score-strip {
display: grid;
grid-template-columns: auto 1fr auto;
gap: var(--space-5);
align-items: center;
padding: var(--space-5);
background: var(--color-surface);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-md);
margin-bottom: var(--space-5);
}
.score-strip__num {
font-size: 48px;
font-weight: var(--font-weight-bold);
line-height: 1;
font-variant-numeric: tabular-nums;
letter-spacing: -0.02em;
color: var(--color-text-primary);
}
.score-strip__num small { font-size: 18px; color: var(--color-text-tertiary); font-weight: var(--font-weight-medium); }
.score-strip__bars { display: flex; flex-direction: column; gap: 6px; }
.score-strip__bar { display: grid; grid-template-columns: 70px 1fr 36px; gap: 8px; align-items: center; font-size: 12px; }
.score-strip__bar-label { color: var(--color-text-secondary); font-family: var(--font-family-mono); font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; }
.score-strip__bar-track { height: 6px; background: var(--color-surface-sunken); border-radius: var(--radius-pill); overflow: hidden; }
.score-strip__bar-fill { height: 100%; border-radius: var(--radius-pill); }
.score-strip__bar-num { font-family: var(--font-family-mono); font-variant-numeric: tabular-nums; color: var(--color-text-secondary); text-align: right; }
/* Live update indicator */
.live-dot {
display: inline-flex; align-items: center; gap: 6px;
font-size: 11px; color: var(--color-text-tertiary);
font-family: var(--font-family-mono); text-transform: uppercase; letter-spacing: 0.06em;
}
.live-dot__pulse {
width: 6px; height: 6px; border-radius: 50%;
background: var(--color-state-success);
box-shadow: 0 0 0 0 currentColor;
animation: pulse 1.6s infinite;
}
@keyframes pulse {
0% { box-shadow: 0 0 0 0 rgba(26,127,55,0.4); }
70% { box-shadow: 0 0 0 6px rgba(26,127,55,0); }
100% { box-shadow: 0 0 0 0 rgba(26,127,55,0); }
}
/* Critique stack */
.critiques { display: flex; flex-direction: column; gap: var(--space-3); }
.critique {
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-md);
background: var(--color-surface);
overflow: hidden;
transition: border-color 0.15s, box-shadow 0.15s;
}
.critique:hover { border-color: var(--color-border-moderate); box-shadow: var(--shadow-sm); }
.critique[data-active="true"] {
border-color: var(--color-primary-500);
box-shadow: 0 0 0 2px var(--color-primary-100);
}
.critique__head {
display: grid; grid-template-columns: auto 1fr auto;
gap: var(--space-3);
padding: 12px 14px;
align-items: center;
cursor: pointer;
}
.critique__sev {
width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0;
}
.critique[data-severity="high"] .critique__sev { background: var(--color-severity-high); }
.critique[data-severity="medium"] .critique__sev { background: var(--color-severity-medium); }
.critique[data-severity="low"] .critique__sev { background: var(--color-severity-low); }
.critique[data-severity="info"] .critique__sev { background: var(--color-state-info); }
.critique__title { font-size: var(--font-size-sm); font-weight: var(--font-weight-semibold); }
.critique__meta {
display: flex; gap: 6px; font-size: 11px;
font-family: var(--font-family-mono); color: var(--color-text-tertiary);
}
.critique__body {
padding: 0 14px 14px 30px;
display: flex; flex-direction: column; gap: 10px;
font-size: var(--font-size-sm);
line-height: 1.5;
color: var(--color-text-secondary);
}
.critique__quote {
padding: 8px 12px;
background: var(--color-bg-soft);
border-left: 2px solid var(--color-border-moderate);
border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
font-family: var(--font-family-serif);
font-size: var(--font-size-sm);
color: var(--color-text-primary);
font-style: italic;
}
.critique__suggestion {
padding: 10px 12px;
background: var(--color-severity-low-soft);
color: var(--color-severity-low-on);
border-radius: var(--radius-sm);
font-family: var(--font-family-serif);
font-size: var(--font-size-sm);
line-height: 1.5;
}
.critique__suggestion::before {
content: "→ Forslag: ";
font-family: var(--font-family-sans);
font-weight: var(--font-weight-semibold);
font-style: normal;
}
.critique__actions { display: flex; gap: 8px; padding-top: 4px; }
.critique__rule {
font-size: 11px; font-family: var(--font-family-mono);
color: var(--color-text-tertiary);
padding: 2px 6px;
background: var(--color-surface-sunken);
border-radius: var(--radius-sm);
}
/* Compare mode */
.compare-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0;
}
.compare-col { padding: var(--space-4); }
.compare-col + .compare-col { border-left: 1px solid var(--color-border-subtle); }
.compare-col__label {
font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em;
color: var(--color-text-tertiary); margin-bottom: 8px; font-weight: var(--font-weight-semibold);
}
/* Section headers */
.h3 { font-size: var(--font-size-md); font-weight: var(--font-weight-semibold); margin: 0 0 var(--space-3); color: var(--color-text-primary); }
.h4 { font-size: var(--font-size-sm); font-weight: var(--font-weight-semibold); margin: 0 0 var(--space-2); color: var(--color-text-secondary); text-transform: uppercase; letter-spacing: 0.06em; }
/* Terminology drawer */
.term-drawer {
margin-top: var(--space-6);
padding: var(--space-5);
background: var(--color-bg-soft);
border-radius: var(--radius-md);
border: 1px solid var(--color-border-subtle);
}
.term-row { display: grid; grid-template-columns: 200px 1fr; gap: var(--space-3); padding: 8px 0; border-top: 1px dashed var(--color-border-subtle); font-size: var(--font-size-sm); }
.term-row:first-of-type { border-top: none; }
.term-row dt { font-weight: var(--font-weight-semibold); color: var(--color-text-primary); }
.term-row dd { margin: 0; color: var(--color-text-secondary); line-height: 1.5; }
/* Toggle for view modes */
.view-toggle {
display: flex; gap: 2px; padding: 3px;
background: var(--color-bg-soft); border-radius: var(--radius-md);
}
.view-toggle button {
padding: 6px 12px; font-size: 12px; font-weight: var(--font-weight-medium);
background: transparent; border: none; border-radius: var(--radius-sm);
cursor: pointer; color: var(--color-text-secondary); font-family: inherit;
}
.view-toggle button[aria-pressed="true"] {
background: var(--color-surface); color: var(--color-text-primary);
box-shadow: var(--shadow-sm);
}
/* Cohort comparison */
.cohort-grid {
display: grid; grid-template-columns: 1fr 1fr 1fr; gap: var(--space-4);
margin-top: var(--space-3);
}
.cohort-card {
padding: var(--space-4);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-md);
background: var(--color-surface);
}
.cohort-card__head { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: var(--space-3); }
.cohort-card__name { font-weight: var(--font-weight-semibold); font-size: var(--font-size-sm); }
.cohort-card__count { font-size: 11px; color: var(--color-text-tertiary); font-family: var(--font-family-mono); }
.cohort-card__metric { display: flex; align-items: baseline; gap: 4px; margin-bottom: 8px; }
.cohort-card__metric-num { font-size: var(--font-size-2xl); font-weight: var(--font-weight-bold); font-variant-numeric: tabular-nums; letter-spacing: -0.01em; }
.cohort-card__metric-suffix { font-size: var(--font-size-sm); color: var(--color-text-tertiary); }
/* Final summary */
.final-banner {
padding: var(--space-5);
background: var(--color-severity-low-soft);
color: var(--color-severity-low-on);
border-radius: var(--radius-md);
border: 1px solid #BFDDC8;
display: grid; grid-template-columns: auto 1fr auto; gap: var(--space-5);
align-items: center;
margin-bottom: var(--space-5);
}
.final-banner__icon {
width: 44px; height: 44px; border-radius: 50%;
background: var(--color-severity-low); color: #fff;
display: flex; align-items: center; justify-content: center;
font-size: 24px; font-weight: var(--font-weight-bold);
}
@media (max-width: 1100px) {
.writer { grid-template-columns: 1fr; }
.cohort-grid { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<div class="layout">
<!-- HEADER STRIP ============================================ -->
<header style="background: var(--color-surface); border-bottom: 1px solid var(--color-border-subtle); padding: 12px 0;">
<div class="container" style="display: flex; justify-content: space-between; align-items: center;">
<div style="display: flex; align-items: center; gap: var(--space-4);">
<a href="index.html" style="text-decoration: none; color: var(--color-text-tertiary); font-size: var(--font-size-sm);">← Tilbake</a>
<span style="color: var(--color-border-moderate);">/</span>
<span style="font-size: var(--font-size-sm); color: var(--color-text-secondary);">Playground / Scenarios / OKR live writer</span>
</div>
<div style="display: flex; gap: var(--space-3); align-items: center;">
<span class="live-dot"><span class="live-dot__pulse"></span> Live · 4 forfattere</span>
<button class="btn btn--ghost" id="theme-toggle" aria-pressed="false">Mørk</button>
</div>
</div>
</header>
<main class="container page">
<!-- PAGE HEADER -->
<div class="page__header">
<div class="page__title">
<span class="page__eyebrow">OKR live-writer · Bærum kommune</span>
<h1 style="margin: 0; font-size: var(--font-size-3xl);">Tjeneste­utvikling — T2 2026</h1>
<div class="page__meta">
<span class="page__meta-item"><span class="page__meta-label">Avd.</span> Innbyggertjenester</span>
<span class="page__meta-item"><span class="page__meta-label">Eier</span> Anne Hovde</span>
<span class="page__meta-item"><span class="page__meta-label">Frist</span> 15. mai 2026</span>
<span class="page__meta-item"><span class="page__meta-label">Lagret</span> 12 sek siden</span>
</div>
</div>
<div style="display: flex; gap: var(--space-2);">
<button class="btn btn--ghost">Versjoner</button>
<button class="btn btn--secondary">Eksporter PDF</button>
<button class="btn btn--primary">Send til godkjenning</button>
</div>
</div>
<!-- SCORE STRIP -->
<div class="score-strip">
<div class="score-strip__num" id="score-num">62<small>/100</small></div>
<div class="score-strip__bars">
<div class="score-strip__bar">
<span class="score-strip__bar-label">Måling</span>
<div class="score-strip__bar-track"><div class="score-strip__bar-fill" style="width: 40%; background: var(--color-severity-medium);"></div></div>
<span class="score-strip__bar-num">4/10</span>
</div>
<div class="score-strip__bar">
<span class="score-strip__bar-label">Spesifikt</span>
<div class="score-strip__bar-track"><div class="score-strip__bar-fill" style="width: 60%; background: var(--color-severity-high);"></div></div>
<span class="score-strip__bar-num">6/10</span>
</div>
<div class="score-strip__bar">
<span class="score-strip__bar-label">Ambisjon</span>
<div class="score-strip__bar-track"><div class="score-strip__bar-fill" style="width: 70%; background: var(--color-severity-low);"></div></div>
<span class="score-strip__bar-num">7/10</span>
</div>
<div class="score-strip__bar">
<span class="score-strip__bar-label">Påvirkbart</span>
<div class="score-strip__bar-track"><div class="score-strip__bar-fill" style="width: 80%; background: var(--color-severity-low);"></div></div>
<span class="score-strip__bar-num">8/10</span>
</div>
</div>
<div style="display: flex; flex-direction: column; align-items: flex-end; gap: 4px;">
<span class="badge" style="background: var(--color-severity-medium-soft); color: var(--color-severity-medium-on);">Trenger arbeid</span>
<span style="font-size: 11px; color: var(--color-text-tertiary); font-family: var(--font-family-mono);">v0.4 · oppdatert kontinuerlig</span>
</div>
</div>
<!-- VIEW TOGGLE -->
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--space-4);">
<div class="view-toggle" role="tablist">
<button role="tab" aria-pressed="true" data-view="writer">Skriv (live-kritikk)</button>
<button role="tab" aria-pressed="false" data-view="rewrite">Sammenlign (før / etter)</button>
<button role="tab" aria-pressed="false" data-view="cohort">Kohort (avd.-gj.snitt)</button>
<button role="tab" aria-pressed="false" data-view="final">Endelig versjon</button>
</div>
<div style="font-size: 11px; color: var(--color-text-tertiary); font-family: var(--font-family-mono);">
Modell kjører lokalt · ingen data forlater Bærum nett
</div>
</div>
<!-- ========================================================= -->
<!-- VIEW 1: WRITER (live critique) -->
<!-- ========================================================= -->
<section class="view" data-view-content="writer">
<div class="writer">
<!-- LEFT: editor -->
<div class="pane">
<div class="pane__head">
<h2 class="pane__title">
<span class="pane__title-eyebrow">Utkast</span>
Tjenesteutvikling — utkast 0.4
</h2>
<span class="live-dot"><span class="live-dot__pulse"></span> Auto-kritikk</span>
</div>
<div class="pane__body">
<div class="editor" id="editor">
<p class="objective">
<span class="hl" data-issue="vague-verb" data-cid="c1">Forbedre</span>
digitale tjenester for innbyggerne i Bærum kommune slik at de
<span class="hl" data-issue="vague-verb" data-cid="c2">opplever bedre service</span>.
</p>
<h2 style="font-size: var(--font-size-sm); color: var(--color-text-tertiary); text-transform: uppercase; letter-spacing: 0.06em;">Nøkkelresultater</h2>
<div class="kr">
<span class="kr-label">KR1</span>
<p class="kr-text">
Øke andelen henvendelser løst i selvbetjeningsløsningen
<span class="hl" data-issue="missing-baseline" data-cid="c3">betydelig</span>
sammenlignet med i fjor.
</p>
</div>
<div class="kr">
<span class="kr-label">KR2</span>
<p class="kr-text">
<span class="hl" data-issue="activity" data-cid="c4">Lansere ny chatbot på kommune.no</span>
innen utgangen av tertialet.
</p>
</div>
<div class="kr">
<span class="kr-label">KR3</span>
<p class="kr-text">
Redusere ventetid for byggesaks­henvendelser
<span class="hl" data-issue="no-metric" data-cid="c5">vesentlig</span>.
</p>
</div>
<div class="kr">
<span class="kr-label">KR4</span>
<p class="kr-text">
Innbygger­tilfredshet på 4,2 av 5 målt i T2-undersøkelsen
<span class="hl" data-issue="no-deadline" data-cid="c6"></span>.
</p>
</div>
</div>
</div>
<div style="padding: 10px 16px; background: var(--color-bg-soft); border-top: 1px solid var(--color-border-subtle); display: flex; justify-content: space-between; align-items: center; font-size: 12px; color: var(--color-text-tertiary); font-family: var(--font-family-mono);">
<span>248 ord · 1 mål · 4 nøkkelresultater</span>
<span>Sist endret 14:23 · Anne H.</span>
</div>
</div>
<!-- RIGHT: critique panel -->
<div class="pane">
<div class="pane__head">
<h2 class="pane__title">
<span class="pane__title-eyebrow">Kritikk</span>
6 funn
</h2>
<span class="badge badge--soft">Regelsett: kommunal-okr-v2</span>
</div>
<div class="pane__body" style="padding: var(--space-3);">
<div class="critiques">
<article class="critique" data-severity="high" data-cid="c4" data-active="true">
<header class="critique__head">
<span class="critique__sev"></span>
<div>
<div class="critique__title">Aktivitet maskert som nøkkelresultat</div>
<div class="critique__meta"><span>KR2</span> · <span class="critique__rule">activity-not-outcome</span></div>
</div>
<span style="font-size: 18px; color: var(--color-text-tertiary);"></span>
</header>
<div class="critique__body">
<div class="critique__quote">«Lansere ny chatbot på kommune.no»</div>
<p>Et nøkkelresultat skal beskrive en <strong>endring i verden</strong>, ikke en aktivitet eller en leveranse. Lansering er en milepæl — det er en input, ikke et utfall.</p>
<div class="critique__suggestion">«Andelen innbyggere som får løst sitt spørsmål i første henvendelse økes fra 38 % (T1 2026) til 55 % innen 31. august 2026.»</div>
<div class="critique__actions">
<button class="btn btn--primary btn--sm">Bruk forslag</button>
<button class="btn btn--ghost btn--sm">Skjul</button>
</div>
</div>
</article>
<article class="critique" data-severity="high" data-cid="c5">
<header class="critique__head">
<span class="critique__sev"></span>
<div>
<div class="critique__title">Ingen målbar verdi</div>
<div class="critique__meta"><span>KR3</span> · <span class="critique__rule">no-metric</span></div>
</div>
<span style="font-size: 18px; color: var(--color-text-tertiary);"></span>
</header>
<div class="critique__body">
<div class="critique__quote">«Redusere ventetid … vesentlig»</div>
<p>«Vesentlig» kan ikke etterprøves. KR-et trenger en tallverdi (i dager / timer) og et utgangspunkt fra T1.</p>
<div class="critique__suggestion">«Median saksbehandlingstid for byggesak reduseres fra 47 dager (T1 2026) til 30 dager innen 31. august 2026.»</div>
</div>
</article>
<article class="critique" data-severity="medium" data-cid="c3">
<header class="critique__head">
<span class="critique__sev"></span>
<div>
<div class="critique__title">Mangler utgangspunkt</div>
<div class="critique__meta"><span>KR1</span> · <span class="critique__rule">missing-baseline</span></div>
</div>
<span style="font-size: 18px; color: var(--color-text-tertiary);"></span>
</header>
<div class="critique__body">
<div class="critique__quote">«… betydelig sammenlignet med i fjor»</div>
<p>«Sammenlignet med i fjor» er en relativ måling uten basisverdi. T1-tallet for selvbetjenings­andel finnes i Tableau-sett <span style="font-family: var(--font-family-mono); font-size: 12px;">tjeneste-kpi-2026q1</span>.</p>
<div class="critique__suggestion">«Andelen henvendelser fullført i selvbetjenings­løsningen økes fra 41 % (T1 2026) til 60 % innen 31. august 2026.»</div>
</div>
</article>
<article class="critique" data-severity="medium" data-cid="c1">
<header class="critique__head">
<span class="critique__sev"></span>
<div>
<div class="critique__title">Vagt verb i Objective</div>
<div class="critique__meta"><span>O</span> · <span class="critique__rule">vague-verb</span></div>
</div>
<span style="font-size: 18px; color: var(--color-text-tertiary);"></span>
</header>
<div class="critique__body">
<div class="critique__quote">«Forbedre digitale tjenester …»</div>
<p>«Forbedre» kan bety nesten hva som helst. Et godt Objective er kvalitativt og inspirerende, men det skal også gi retning. Hva betyr «bedre» for en innbygger her?</p>
<div class="critique__suggestion">«Innbyggere i Bærum får svar på sine kommunale spørsmål i løpet av samme dag — uten å måtte ringe.»</div>
</div>
</article>
<article class="critique" data-severity="medium" data-cid="c6">
<header class="critique__head">
<span class="critique__sev"></span>
<div>
<div class="critique__title">Mangler tidsfrist</div>
<div class="critique__meta"><span>KR4</span> · <span class="critique__rule">no-deadline</span></div>
</div>
<span style="font-size: 18px; color: var(--color-text-tertiary);"></span>
</header>
<div class="critique__body">
<p>KR-et nevner T2-undersøkelsen, men ikke når den gjennomføres eller når resultatet skal foreligge.</p>
<div class="critique__suggestion">«… målt i T2-undersøkelsen som gjennomføres uke 33-35 og rapporteres innen 15. september 2026.»</div>
</div>
</article>
<article class="critique" data-severity="info">
<header class="critique__head">
<span class="critique__sev"></span>
<div>
<div class="critique__title">Hint: Strekk-mål?</div>
<div class="critique__meta">Hele settet · <span class="critique__rule">stretch-suggestion</span></div>
</div>
<span style="font-size: 18px; color: var(--color-text-tertiary);"></span>
</header>
<div class="critique__body">
<p>Tre av fire KR-er ligger under 1,5× nåværende baseline når du har lagt inn tall. OKR fungerer best når 6070 % oppnåelse oppleves som godt arbeid. Vurder strekk på KR1.</p>
</div>
</article>
</div>
</div>
</div>
</div><!-- /writer -->
<!-- TERMINOLOGY -->
<div class="term-drawer">
<h3 class="h3" style="margin-bottom: var(--space-3);">Bærum-spesifikk OKR-ordliste</h3>
<p style="font-size: var(--font-size-sm); color: var(--color-text-secondary); margin-bottom: var(--space-4);">Plugin-en lærte disse begrepene fra Bærums egen styringspraksis. Andre kommuner forker pluginen og fyller på sine egne.</p>
<dl style="margin: 0;">
<div class="term-row">
<dt>Tertial</dt>
<dd>4-måneders styringsperiode (T1: jan-apr, T2: mai-aug, T3: sep-des). Erstatter «kvartal» i Bærums tekstmaler.</dd>
</div>
<div class="term-row">
<dt>Selvbetjenings­andel</dt>
<dd>KPI definert som henvendelser fullført uten saksbehandler-inngripen, kilde: <span style="font-family: var(--font-family-mono); font-size: 12px;">tjeneste-kpi-2026q1</span>.</dd>
</div>
<div class="term-row">
<dt>Innbygger­tilfredshet</dt>
<dd>5-punkts skala fra årlig undersøkelse. Kommunestyrets mål: ≥ 4,0 i alle avdelinger innen 2027.</dd>
</div>
<div class="term-row">
<dt>Strekk-mål</dt>
<dd>Bærums interne term for ambisiøs verdi (mål 70 %), brukt sammen med «forventet verdi» (mål 90 %).</dd>
</div>
</dl>
</div>
</section><!-- /view writer -->
<!-- ========================================================= -->
<!-- VIEW 2: REWRITE (before/after) -->
<!-- ========================================================= -->
<section class="view" data-view-content="rewrite" style="display: none;">
<h3 class="h3">Side ved side: utkast 0.4 → forslag</h3>
<p style="color: var(--color-text-secondary); font-size: var(--font-size-sm); margin-bottom: var(--space-4);">Plugin-ens forslag bruker baseline-tall den hentet fra Bærums KPI-katalog. Du kan godta hver endring enkeltvis.</p>
<div class="diff" style="background: var(--color-surface);">
<div class="diff__summary">
<div class="diff__summary-item"><span class="diff__summary-count" style="color: var(--color-severity-critical);">5</span><span>fjernet</span></div>
<div class="diff__summary-item"><span class="diff__summary-count" style="color: var(--color-severity-low);">+5</span><span>lagt til</span></div>
<div class="diff__summary-item"><span class="diff__summary-count">9</span><span>endringer</span></div>
</div>
<div class="diff__row">
<div class="diff__cell diff__cell--removed">Forbedre digitale tjenester for innbyggerne i Bærum kommune slik at de opplever bedre service.</div>
<div class="diff__cell diff__cell--added">Innbyggere i Bærum får svar på sine kommunale spørsmål i løpet av samme dag — uten å måtte ringe.</div>
</div>
<div class="diff__row">
<div class="diff__cell diff__cell--removed">KR1: Øke andelen henvendelser løst i selvbetjeningsløsningen betydelig sammenlignet med i fjor.</div>
<div class="diff__cell diff__cell--added">KR1: Andelen henvendelser fullført i selvbetjenings­løsningen økes fra 41 % (T1 2026) til 60 % innen 31. august 2026.</div>
</div>
<div class="diff__row">
<div class="diff__cell diff__cell--removed">KR2: Lansere ny chatbot på kommune.no innen utgangen av tertialet.</div>
<div class="diff__cell diff__cell--added">KR2: Andelen innbyggere som får løst sitt spørsmål i første henvendelse økes fra 38 % (T1 2026) til 55 % innen 31. august 2026.</div>
</div>
<div class="diff__row">
<div class="diff__cell diff__cell--removed">KR3: Redusere ventetid for byggesakshenvendelser vesentlig.</div>
<div class="diff__cell diff__cell--added">KR3: Median saksbehandlingstid for byggesak reduseres fra 47 dager (T1 2026) til 30 dager innen 31. august 2026.</div>
</div>
<div class="diff__row">
<div class="diff__cell diff__cell--removed">KR4: Innbyggertilfredshet på 4,2 av 5 målt i T2-undersøkelsen.</div>
<div class="diff__cell diff__cell--added">KR4: Innbyggertilfredshet på 4,2 av 5 målt i T2-undersøkelsen (uke 33-35), rapportert innen 15. september 2026.</div>
</div>
</div>
<div style="display: flex; gap: var(--space-3); justify-content: flex-end; margin-top: var(--space-4);">
<button class="btn btn--ghost">Avvis alle</button>
<button class="btn btn--secondary">Aksepter én og én</button>
<button class="btn btn--primary">Aksepter alle</button>
</div>
</section>
<!-- ========================================================= -->
<!-- VIEW 3: COHORT (anonymous benchmarking) -->
<!-- ========================================================= -->
<section class="view" data-view-content="cohort" style="display: none;">
<h3 class="h3">Hvordan du ligger an mot resten av Bærum</h3>
<p style="color: var(--color-text-secondary); font-size: var(--font-size-sm); margin-bottom: var(--space-4); max-width: var(--measure);">
Anonymisert sammenligning på tvers av avdelinger som bruker samme plugin. Tall hentes lokalt fra OKR-systemet — ingen tekst, kun aggregerte score.
</p>
<div class="cohort-grid">
<div class="cohort-card">
<div class="cohort-card__head">
<span class="cohort-card__name">Ditt sett</span>
<span class="cohort-card__count">Innbygger­tjenester</span>
</div>
<div class="cohort-card__metric">
<span class="cohort-card__metric-num">62</span>
<span class="cohort-card__metric-suffix">/100</span>
</div>
<div style="font-size: 12px; color: var(--color-text-tertiary);">6 åpne funn · 2 høy alvorlighet</div>
</div>
<div class="cohort-card">
<div class="cohort-card__head">
<span class="cohort-card__name">Avd.-median</span>
<span class="cohort-card__count">14 sett</span>
</div>
<div class="cohort-card__metric">
<span class="cohort-card__metric-num">71</span>
<span class="cohort-card__metric-suffix">/100</span>
</div>
<div style="font-size: 12px; color: var(--color-text-tertiary);">P25: 58 · P75: 84</div>
</div>
<div class="cohort-card">
<div class="cohort-card__head">
<span class="cohort-card__name">Kommune-median</span>
<span class="cohort-card__count">87 sett · alle avd.</span>
</div>
<div class="cohort-card__metric">
<span class="cohort-card__metric-num">68</span>
<span class="cohort-card__metric-suffix">/100</span>
</div>
<div style="font-size: 12px; color: var(--color-text-tertiary);">Beste avd.: Eiendom · 81</div>
</div>
</div>
<div style="margin-top: var(--space-6);">
<h4 class="h4">Hyppigste funn på tvers av Bærum (T2 så langt)</h4>
<div class="distribution">
<div class="distribution__row">
<span class="distribution__label">activity-not-outcome</span>
<div class="distribution__track">
<div class="distribution__band" style="left: 18%; right: 28%;"></div>
<div class="distribution__median" style="left: 41%;"><span class="distribution__median-label">41 % av sett</span></div>
</div>
</div>
<div class="distribution__row">
<span class="distribution__label">no-metric</span>
<div class="distribution__track">
<div class="distribution__band" style="left: 12%; right: 42%;"></div>
<div class="distribution__median" style="left: 33%;"><span class="distribution__median-label">33 %</span></div>
</div>
</div>
<div class="distribution__row">
<span class="distribution__label">missing-baseline</span>
<div class="distribution__track">
<div class="distribution__band" style="left: 22%; right: 22%;"></div>
<div class="distribution__median" style="left: 51%;"><span class="distribution__median-label">51 %</span></div>
</div>
</div>
<div class="distribution__row">
<span class="distribution__label">vague-verb</span>
<div class="distribution__track">
<div class="distribution__band" style="left: 30%; right: 18%;"></div>
<div class="distribution__median" style="left: 60%;"><span class="distribution__median-label">60 %</span></div>
</div>
</div>
<div class="distribution__row">
<span class="distribution__label">no-deadline</span>
<div class="distribution__track">
<div class="distribution__band" style="left: 8%; right: 56%;"></div>
<div class="distribution__median" style="left: 24%;"><span class="distribution__median-label">24 %</span></div>
</div>
</div>
</div>
<p style="font-size: 12px; color: var(--color-text-tertiary); margin-top: var(--space-3); font-family: var(--font-family-mono);">
Bånd = P25P75 på tvers av avd. · linje = median andel sett som har minst ett slikt funn
</p>
</div>
</section>
<!-- ========================================================= -->
<!-- VIEW 4: FINAL -->
<!-- ========================================================= -->
<section class="view" data-view-content="final" style="display: none;">
<div class="final-banner">
<div class="final-banner__icon"></div>
<div>
<div style="font-size: var(--font-size-lg); font-weight: var(--font-weight-semibold); margin-bottom: 2px;">Klar for godkjenning · score 91/100</div>
<div style="font-size: var(--font-size-sm); opacity: 0.9;">0 høye funn · 1 informasjonshint · alle KR har baseline, mål og frist</div>
</div>
<button class="btn btn--primary">Send til virksomhetsleder</button>
</div>
<article style="background: var(--color-surface); border: 1px solid var(--color-border-subtle); border-radius: var(--radius-md); padding: var(--space-8); max-width: 800px;">
<div style="font-size: 11px; color: var(--color-text-tertiary); text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: var(--space-2);">Bærum kommune · Innbyggertjenester · T2 2026 · v1.0</div>
<h2 style="font-family: var(--font-family-serif); font-size: 28px; line-height: 1.3; margin: 0 0 var(--space-6); color: var(--color-text-primary);">
Innbyggere i Bærum får svar på sine kommunale spørsmål i løpet av samme dag — uten å måtte ringe.
</h2>
<h3 class="h4" style="margin-bottom: var(--space-4);">Nøkkelresultater</h3>
<div style="display: flex; flex-direction: column; gap: var(--space-3);">
<div style="padding: var(--space-4); background: var(--color-bg-soft); border-left: 3px solid var(--color-scope-okr); border-radius: 0 var(--radius-sm) var(--radius-sm) 0;">
<div style="font-family: var(--font-family-mono); font-size: 11px; color: var(--color-scope-okr); font-weight: var(--font-weight-semibold); margin-bottom: 4px; letter-spacing: 0.06em;">KR1</div>
<div style="font-family: var(--font-family-serif); font-size: 17px; line-height: 1.5;">Andelen henvendelser fullført i selvbetjenings­løsningen økes fra <strong>41 %</strong> (T1 2026) til <strong>60 %</strong> innen 31. august 2026.</div>
</div>
<div style="padding: var(--space-4); background: var(--color-bg-soft); border-left: 3px solid var(--color-scope-okr); border-radius: 0 var(--radius-sm) var(--radius-sm) 0;">
<div style="font-family: var(--font-family-mono); font-size: 11px; color: var(--color-scope-okr); font-weight: var(--font-weight-semibold); margin-bottom: 4px; letter-spacing: 0.06em;">KR2</div>
<div style="font-family: var(--font-family-serif); font-size: 17px; line-height: 1.5;">Andelen innbyggere som får løst sitt spørsmål i første henvendelse økes fra <strong>38 %</strong> (T1 2026) til <strong>55 %</strong> innen 31. august 2026.</div>
</div>
<div style="padding: var(--space-4); background: var(--color-bg-soft); border-left: 3px solid var(--color-scope-okr); border-radius: 0 var(--radius-sm) var(--radius-sm) 0;">
<div style="font-family: var(--font-family-mono); font-size: 11px; color: var(--color-scope-okr); font-weight: var(--font-weight-semibold); margin-bottom: 4px; letter-spacing: 0.06em;">KR3</div>
<div style="font-family: var(--font-family-serif); font-size: 17px; line-height: 1.5;">Median saksbehandlingstid for byggesak reduseres fra <strong>47 dager</strong> (T1 2026) til <strong>30 dager</strong> innen 31. august 2026.</div>
</div>
<div style="padding: var(--space-4); background: var(--color-bg-soft); border-left: 3px solid var(--color-scope-okr); border-radius: 0 var(--radius-sm) var(--radius-sm) 0;">
<div style="font-family: var(--font-family-mono); font-size: 11px; color: var(--color-scope-okr); font-weight: var(--font-weight-semibold); margin-bottom: 4px; letter-spacing: 0.06em;">KR4</div>
<div style="font-family: var(--font-family-serif); font-size: 17px; line-height: 1.5;">Innbyggertilfredshet på <strong>4,2 av 5</strong> målt i T2-undersøkelsen (uke 3335), rapportert innen 15. september 2026.</div>
</div>
</div>
<div style="margin-top: var(--space-8); padding-top: var(--space-5); border-top: 1px solid var(--color-border-subtle); display: flex; justify-content: space-between; font-size: 12px; color: var(--color-text-tertiary); font-family: var(--font-family-mono);">
<span>Eier: Anne Hovde · Innbygger­tjenester</span>
<span>Generert med okr-writer-baerum v2.3 · 12 reviderte uttkast</span>
</div>
</article>
</section>
</main>
</div>
<script>
// Theme toggle
const themeBtn = document.getElementById('theme-toggle');
const setTheme = (t) => {
document.documentElement.setAttribute('data-theme', t);
themeBtn.textContent = t === 'dark' ? 'Lys' : 'Mørk';
themeBtn.setAttribute('aria-pressed', t === 'dark' ? 'true' : 'false');
try { localStorage.setItem('pg-theme', t); } catch(e) {}
};
setTheme(localStorage.getItem('pg-theme') || 'light');
themeBtn.addEventListener('click', () => {
setTheme(document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark');
});
// View toggle
const views = document.querySelectorAll('[data-view-content]');
document.querySelectorAll('.view-toggle button').forEach(btn => {
btn.addEventListener('click', () => {
const v = btn.dataset.view;
document.querySelectorAll('.view-toggle button').forEach(b => b.setAttribute('aria-pressed', b === btn ? 'true' : 'false'));
views.forEach(s => { s.style.display = s.dataset.viewContent === v ? '' : 'none'; });
try { history.replaceState(null, '', '#' + v); } catch(e) {}
});
});
// initial from hash
const initialView = (location.hash || '').replace('#','') || 'writer';
const tab = document.querySelector(`[data-view="${initialView}"]`);
if (tab) tab.click();
// Critique <-> editor highlighting
const editor = document.getElementById('editor');
document.querySelectorAll('.critique').forEach(c => {
c.querySelector('.critique__head').addEventListener('click', () => {
document.querySelectorAll('.critique').forEach(x => x.removeAttribute('data-active'));
c.setAttribute('data-active', 'true');
const cid = c.dataset.cid;
if (cid) {
const target = editor.querySelector(`[data-cid="${cid}"]`);
if (target) {
target.style.transition = 'background-color 0.6s';
target.style.backgroundColor = 'rgba(0, 98, 186, 0.18)';
setTimeout(() => { target.style.backgroundColor = ''; }, 1400);
}
}
});
});
// Hover linking from editor to critique
editor.querySelectorAll('.hl').forEach(hl => {
hl.addEventListener('mouseenter', () => {
const cid = hl.dataset.cid;
const c = document.querySelector(`.critique[data-cid="${cid}"]`);
if (c) c.style.outline = '2px solid var(--color-primary-300)';
});
hl.addEventListener('mouseleave', () => {
const cid = hl.dataset.cid;
const c = document.querySelector(`.critique[data-cid="${cid}"]`);
if (c) c.style.outline = '';
});
});
</script>
</body>
</html>