feat(shared): Tier 3 wave 2 (12 components) + self-hosted fonts

Two changes in one commit because they were prepared together and the
component demos depend on the new self-hosted fonts.css.

Tier 3 wave 2 — 12 new components
---------------------------------
Adds components-tier3-supplement.css (886 lines) and 12 isolated demo
HTML pages under shared/playground-examples/components/:
toxic-flow chain, fleet-overview, kanban Keep/Review/Remove,
maturity-ladder, classify-and-transform, cycle-ribbon,
persistent-antipattern, suppressed-signals, ExpansionCard, ReadMore,
FormProgress, Aspirational-vs-Committed.

Reuses existing tokens — no new CSS custom properties. Honors the
Phase 1 feedback rules: no large pink areas for body text, severity-red
distinct from failure-red, dark mode via existing [data-theme="dark"].

Provenance: components-tier3-supplement.css and the 12 demo bodies were
authored by claude.ai/design (separate Anthropic instance) on 2026-05-03.
This commit only integrates them — path rewrites, font swap, generic
name substitution in fleet-overview demo data, README updates.
base.css from the export was deliberately NOT taken in because it
reverted the inline-message contrast fix from v0.1.

Self-hosted fonts (Inter, JetBrains Mono, Source Serif 4)
---------------------------------------------------------
Replaces all fonts.googleapis.com / fonts.gstatic.com requests with
.woff2 files bundled at shared/playground-design-system/fonts/.

Why:
- No data leaked to Google about end-user IPs and User-Agents.
- GDPR-safe for Norwegian public-sector deployments.
- Works offline / behind air-gapped firewalls.
- Forkers downloading the marketplace get a complete bundle.

All three families are SIL Open Font License 1.1 — license texts
included alongside the woff2 files. Source Serif 4 woff2 generated
locally from the upstream OTF release using
fonttools ttLib.woff2 compress; Inter and JetBrains Mono are
unmodified upstream webfont releases.

Total bundle: 9 woff2 files, ~940 KB. New fonts.css declares all
@font-face rules with font-display: swap. All 6 example HTMLs and 12
new component demos load it via a single relative path.

Verified
--------
- Privacy grep returns empty across plugins/ and shared/
- Google Fonts grep returns empty across shared/*.html
- Smoke test via python -m http.server: HTML + 7 stylesheets +
  Inter-Regular.woff2 all return 200

Doc updates
-----------
- shared/playground-design-system/README.md: file tree updated,
  Quick start snippet shows fonts.css link, "Self-hosted fonts"
  section added
- shared/playground-design-system/fonts/LICENSES.md: combined attribution
- README.md (root): Tier 3 wave 1+2 component list, Privacy-first bullet
- CLAUDE.md (root): tree entry expanded for new components + fonts

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-05-03 05:08:07 +02:00
commit f1fecf39b8
36 changed files with 2470 additions and 29 deletions

View file

@ -17,8 +17,8 @@ plugins/
ultra-cc-architect/ v0.1.0 — Claude-Code-specific architecture matching + skill-factory (extracted from ultraplan-local in v3.0.0)
shared/
playground-design-system/ v0.1 — Aksel/Digdir-aligned CSS design system + JSON schemas for plugin Playgrounds (consumed by ms-ai-architect, okr, llm-security, ultraplan-local, config-audit)
playground-examples/ — Reference scenarios (ROS-Lier, OKR-Bærum, security-Direktorat) + showcase landing
playground-design-system/ v0.1 — Aksel/Digdir-aligned CSS design system + JSON schemas + self-hosted Inter/JetBrains Mono/Source Serif 4 fonts (Tier 1+2+3 wave 1+wave 2 = 20 Tier 3 components total). Consumed by ms-ai-architect, okr, llm-security, ultraplan-local, config-audit
playground-examples/ — Reference scenarios (ROS-Lier, OKR-Bærum, security-Direktorat) + showcase landing + 12 isolated Tier 3 wave 2 component demos under components/
```
Hvert plugin er selvstendig med egen CLAUDE.md, README, hooks, agents og commands. `shared/` inneholder marketplace-nivå infrastruktur som flere plugins bygger på.

View file

@ -247,10 +247,12 @@ Shared design system for plugin Playgrounds — visual self-service UIs that com
Targets five plugins: `ms-ai-architect`, `okr`, `llm-security`, `ultraplan-local`, `config-audit`. Built for Norwegian public sector decision-makers (kommunaldirektører, sikkerhetsoffiserer, OKR-koordinatorer) plus developer power-users — one visual family, two information densities.
- **Tokens** — Inter font, body 17px, Digdir blue `#0062BA`, deuteranopia-safe severity ramp, distinct severity-red vs failure-red, plugin-scope colors, semantic CSS custom properties
- **Tokens** — Inter/JetBrains Mono/Source Serif 4 (all self-hosted, OFL 1.1), body 17px, Digdir blue `#0062BA`, deuteranopia-safe severity ramp, distinct severity-red vs failure-red, plugin-scope colors, semantic CSS custom properties
- **Tier 1 components** — radar/spider, 5×5 matrix-heatmap (bottom-left origin, ROS/DPIA), findings-browser, critique-card, wizard/stepper, live-meter with antipattern lints
- **Tier 2 components** — decision-tree (AI Act 4-step), traffic-lights, diff-review, treemap (token hotspots), distribution P10/P50/P90, command-pipeline output, AI Act 4-color pyramide, pipeline-cockpit, verdict-pill + 5-band risk-meter, codepoint-reveal (Unicode steganography), small-multiples grid (16-category posture without overcrowded radar), OWASP badges (LLM/ASI/AST/MCP)
- **Tier 3 components (wave 1+2, 20 total)** — pair-before-after, AI Act timeline, 3-track entry, FRIA rights-matrix, capability-matrix, parallel-agent-status, ErrorSummary, GuidePanel, toxic-flow chain, fleet-overview, kanban Keep/Review/Remove, maturity-ladder, classify-and-transform, cycle-ribbon, persistent-antipattern, suppressed-signals, ExpansionCard, ReadMore, FormProgress, Aspirational-vs-Committed
- **JSON schemas**`finding.schema.json`, `okr-set.schema.json`, `ros-threat.schema.json` for cross-plugin data interchange
- **Privacy-first** — all fonts self-hosted as woff2 in `fonts/`, zero external CDN requests, GDPR-safe for offentlig sektor, works offline / behind air-gapped firewalls
- **Reference scenarios** — Lier kommune ROS-rapport (ms-ai-architect), Bærum kommune T2 OKR live-writer, Direktoratet for digital tjenesteutvikling ToxicSkills findings review (85 funn, BLOCK)
→ [Full documentation](shared/playground-design-system/README.md) · [Browse showcase](shared/playground-examples/index.html)

View file

@ -20,7 +20,14 @@ shared/
│ ├── base.css # Reset, typography, primitives, focus, print
│ ├── components.css # Tier 1: radar, matrix, findings-browser, critique-card, wizard, live-meter
│ ├── components-tier2.css # Tier 2: decision-tree, traffic-lights, diff-review, treemap, distribution, command-pipeline, pyramide, pipeline-cockpit, verdict-pill+risk-meter, codepoint-reveal, small-multiples, OWASP badges
│ ├── components-tier3.css # Tier 3: pair-before-after, AI Act timeline, 3-track entry, FRIA rights-matrix, capability-matrix, parallel-agent-status, ErrorSummary, GuidePanel
│ ├── components-tier3.css # Tier 3 wave 1: pair-before-after, AI Act timeline, 3-track entry, FRIA rights-matrix, capability-matrix, parallel-agent-status, ErrorSummary, GuidePanel
│ ├── components-tier3-supplement.css # Tier 3 wave 2 (12): toxic-flow, fleet-overview, kanban Keep/Review/Remove, maturity-ladder, classify-and-transform, cycle-ribbon, persistent-antipattern, suppressed-signals, ExpansionCard, ReadMore, FormProgress, Aspirational-vs-Committed
│ ├── fonts.css # @font-face declarations for self-hosted fonts
│ ├── fonts/ # Self-hosted woff2 + license attribution
│ │ ├── Inter-{Regular,Medium,SemiBold,Bold}.woff2
│ │ ├── JetBrainsMono-{Regular,Medium,SemiBold}.woff2
│ │ ├── SourceSerif4-{Regular,Semibold}.woff2
│ │ └── LICENSES.md # All three are SIL OFL 1.1
│ ├── print.css # A4 print stylesheet with B/W severity patterns
│ └── schemas/ # Cross-plugin JSON schemas
│ ├── finding.schema.json # Used by llm-security, config-audit, ultraplan-review, ms-ai-review
@ -31,8 +38,22 @@ shared/
├── index.html # System showcase (browse all components)
├── ros-lier-kommune.html # Scenario A — ms-ai-architect ROS report
├── okr-baerum.html # Scenario B — OKR live writer
├── security-direktorat.html # Scenario C — llm-security findings review
├── security-direktorat.html # Scenario C — llm-security findings review
├── templates.html # Skeleton + print-template demos
├── tier3-preview.html # Tier 3 wave 1 visual preview
├── components/ # Tier 3 wave 2 — 12 isolated demo pages
│ ├── sankey-toxic-flow.html
│ ├── fleet-overview.html
│ ├── kanban.html
│ ├── maturity-ladder.html
│ ├── classify-transform.html
│ ├── cycle-ribbon.html
│ ├── persistent-antipattern.html
│ ├── suppressed-signals.html
│ ├── expansion-card.html
│ ├── read-more.html
│ ├── form-progress.html
│ └── aspirational-committed.html
├── ros-app.js # Scenario A interactivity
└── ros-data.js # Scenario A mock data
```
@ -50,8 +71,12 @@ To use the design system from a plugin's Playground:
<link rel="stylesheet" href="../../shared/playground-design-system/base.css">
<link rel="stylesheet" href="../../shared/playground-design-system/components.css">
<link rel="stylesheet" href="../../shared/playground-design-system/components-tier2.css">
<!-- Optional: include components-tier3.css for Tier 3 wave 1 components -->
<!-- Optional: include components-tier3-supplement.css for Tier 3 wave 2 (12 additional components) -->
<!-- Optional: only include print.css if scenario produces a printable A4 report -->
<link rel="stylesheet" href="../../shared/playground-design-system/print.css">
<!-- Self-hosted fonts (no external requests) -->
<link rel="stylesheet" href="../../shared/playground-design-system/fonts.css">
</head>
<body>
<header class="app-header">
@ -166,13 +191,15 @@ localStorage.setItem('theme', document.documentElement.dataset.theme);
Include `print.css` if your scenario produces an A4 report. Then add `class="no-print"` to interactive chrome (header, buttons, theme toggle), and use `class="page-break"` to force page breaks. Severity-coded matrix cells will automatically render as B/W-safe hatching patterns when printed. The `.print-header` and `.print-footer` blocks support kommune-logo slots and signature lines for offentlige dokumenter.
## Known limitations (Phase 1)
## Known limitations
1. **Google Fonts CDN dependency.** All example HTML files load Inter from `fonts.googleapis.com`. This violates the "self-contained, no CDN" constraint. The system-font-stack fallback in `base.css` works, so files render acceptably offline — but for production deployment, Inter should be self-hosted as woff2 files in `playground-design-system/fonts/`. Tracked for Phase 2.
2. **Tier 3 components missing.** Several components from the design brief are not yet implemented: rights-matrix (FRIA 12 EU Charter), capability-matrix (license × kapabilitet), fleet-overview (cross-project security dashboard), kanban (Keep/Review/Remove), sankey/toxic-flow chain (security TFA), classify-and-transform (OKR 5-bucket sorter), maturity-ladder (OKR/posture progression), parallel-agent-status panel (utredning, ultraexecute waves), suppressed-signals panel.
3. **No JavaScript framework.** Components are CSS-first. Interactivity (e.g. `aria-selected` toggling, sidepanel open/close, live-meter updates) must be wired by each Playground using vanilla JS. See `playground-examples/ros-app.js` for a reference implementation pattern.
4. **No icon set bundled.** The system assumes Lucide or Phosphor SVG sprites are inlined per Playground. Iconography is intentionally out-of-system to keep the shared system small.
5. **Mobile responsiveness is partial.** The 5×5 matrix, findings-browser, codepoint-reveal split-pane, and small-multiples grid have explicit `@media (max-width: ...)` rules. Other components may need polish for narrow viewports.
1. **No JavaScript framework.** Components are CSS-first. Interactivity (e.g. `aria-selected` toggling, sidepanel open/close, live-meter updates) must be wired by each Playground using vanilla JS. See `playground-examples/ros-app.js` for a reference implementation pattern.
2. **No icon set bundled.** The system assumes Lucide or Phosphor SVG sprites are inlined per Playground. Iconography is intentionally out-of-system to keep the shared system small.
3. **Mobile responsiveness is partial.** The 5×5 matrix, findings-browser, codepoint-reveal split-pane, and small-multiples grid have explicit `@media (max-width: ...)` rules. Other components may need polish for narrow viewports.
## Self-hosted fonts
All three font families (Inter, JetBrains Mono, Source Serif 4) are bundled as woff2 in `fonts/` and loaded via `fonts.css`. No external requests to Google Fonts or any CDN. All three are SIL OFL 1.1 — see `fonts/LICENSES.md` for full attribution.
## Versioning

View file

@ -0,0 +1,886 @@
/* =============================================================================
components-tier3-supplement.css
Tier 3 supplement 12 components added after Tier 3 main set.
Pinned rules:
- No big pink fills for text. Use surface bg + colored border + dark body text.
- severity-critical (#A40E26) state-failed (#7D1A1A). Don't conflate.
- Light + dark theme via existing tokens only.
============================================================================= */
/* =========================================================================
1. Sankey / Toxic-Flow Chain (.tfa-flow)
3-step: Input Access Exfil with mitigation shields breaking the chain.
========================================================================= */
.tfa-flow {
display: grid;
grid-template-columns: 1fr auto 1fr auto 1fr;
gap: 0;
align-items: stretch;
background: var(--color-surface);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-lg);
padding: var(--space-5);
position: relative;
}
.tfa-flow__verdict {
position: absolute;
top: -12px; right: var(--space-5);
padding: 4px 10px;
font-size: 11px;
font-weight: var(--font-weight-bold);
letter-spacing: 0.06em;
border-radius: var(--radius-pill);
background: var(--color-severity-critical);
color: #fff;
}
.tfa-flow__verdict[data-verdict="ALLOW"] { background: var(--color-state-success); }
.tfa-flow__verdict[data-verdict="WARN"] { background: var(--color-severity-medium); color: #fff; }
.tfa-flow__verdict[data-verdict="BLOCK"] { background: var(--color-severity-critical); }
.tfa-leg {
display: flex; flex-direction: column; gap: 6px;
padding: var(--space-3);
background: var(--color-surface);
border: 1px solid var(--color-border-subtle);
border-left-width: 4px;
border-radius: var(--radius-md);
cursor: pointer;
transition: background var(--duration-fast) var(--ease-default);
text-align: left;
}
.tfa-leg:hover { background: var(--color-bg-soft); }
.tfa-leg:focus-visible { outline: none; box-shadow: var(--shadow-focus); }
.tfa-leg[data-severity="medium"] { border-left-color: var(--color-severity-medium); }
.tfa-leg[data-severity="high"] { border-left-color: var(--color-severity-high); }
.tfa-leg[data-severity="critical"] { border-left-color: var(--color-severity-critical); }
.tfa-leg__label {
font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em;
color: var(--color-text-tertiary); font-weight: var(--font-weight-semibold);
}
.tfa-leg__name { font-size: var(--font-size-md); font-weight: var(--font-weight-semibold); color: var(--color-text-primary); }
.tfa-leg__source { font-family: var(--font-family-mono); font-size: 12px; color: var(--color-text-secondary); }
.tfa-leg__status {
margin-top: auto;
font-size: 11px;
font-weight: var(--font-weight-medium);
display: inline-flex; align-items: center; gap: 4px;
}
.tfa-leg__status[data-mit="unmitigated"] { color: var(--color-severity-critical); }
.tfa-leg__status[data-mit="partially_mitigated"] { color: var(--color-severity-medium); }
.tfa-leg__status[data-mit="mitigated"] { color: var(--color-state-success); }
/* Arrow connectors. Width grows with severity */
.tfa-arrow {
display: flex; align-items: center; justify-content: center;
position: relative;
min-width: 56px;
padding: 0 4px;
}
.tfa-arrow__line {
height: 4px;
width: 100%;
background: var(--color-border-moderate);
position: relative;
}
.tfa-arrow[data-severity="medium"] .tfa-arrow__line { background: var(--color-severity-medium); height: 6px; }
.tfa-arrow[data-severity="high"] .tfa-arrow__line { background: var(--color-severity-high); height: 8px; }
.tfa-arrow[data-severity="critical"] .tfa-arrow__line { background: var(--color-severity-critical); height: 10px; }
.tfa-arrow__line::after {
content: ""; position: absolute; right: -1px; top: 50%;
width: 0; height: 0;
border-left: 10px solid currentColor;
border-top: 8px solid transparent;
border-bottom: 8px solid transparent;
transform: translateY(-50%);
color: inherit;
}
.tfa-arrow[data-severity="medium"] .tfa-arrow__line { color: var(--color-severity-medium); }
.tfa-arrow[data-severity="high"] .tfa-arrow__line { color: var(--color-severity-high); }
.tfa-arrow[data-severity="critical"] .tfa-arrow__line { color: var(--color-severity-critical); }
.tfa-arrow__shield {
position: absolute;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
width: 32px; height: 32px;
background: var(--color-state-success);
color: #fff;
border-radius: 50%;
display: flex; align-items: center; justify-content: center;
border: 3px solid var(--color-surface);
font-size: 16px;
}
.tfa-arrow--mitigated .tfa-arrow__line {
background: repeating-linear-gradient(90deg, var(--color-state-success) 0 4px, transparent 4px 8px);
}
@media (max-width: 720px) {
.tfa-flow {
grid-template-columns: 1fr;
grid-template-rows: auto auto auto auto auto;
}
.tfa-arrow { min-height: 48px; min-width: auto; }
.tfa-arrow__line { width: 4px; height: 100%; }
.tfa-arrow[data-severity="medium"] .tfa-arrow__line { width: 6px; height: 100%; }
.tfa-arrow[data-severity="high"] .tfa-arrow__line { width: 8px; height: 100%; }
.tfa-arrow[data-severity="critical"] .tfa-arrow__line { width: 10px; height: 100%; }
.tfa-arrow__line::after {
right: 50%; top: auto; bottom: -1px; transform: translateX(50%);
border-left: 8px solid transparent;
border-right: 8px solid transparent;
border-top: 10px solid currentColor;
border-bottom: none;
}
}
/* =========================================================================
2. Fleet-Overview (.fleet-grid, .fleet-tile)
========================================================================= */
.fleet-toolbar {
display: flex; gap: var(--space-3); flex-wrap: wrap;
align-items: center;
padding: var(--space-3) var(--space-4);
background: var(--color-bg-soft);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-md);
margin-bottom: var(--space-3);
}
.fleet-toolbar__label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--color-text-tertiary); font-weight: var(--font-weight-semibold); }
.fleet-toolbar__spacer { flex: 1; }
.fleet-toolbar__count { font-size: var(--font-size-sm); color: var(--color-text-secondary); }
.fleet-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: var(--space-3);
}
@media (max-width: 980px) { .fleet-grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 540px) { .fleet-grid { grid-template-columns: 1fr; } }
.fleet-tile {
background: var(--color-surface);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-md);
padding: var(--space-3);
display: grid;
grid-template-rows: auto auto auto auto;
gap: 6px;
cursor: pointer;
transition: border-color var(--duration-fast), transform var(--duration-fast);
}
.fleet-tile:hover { border-color: var(--color-primary-300); transform: translateY(-1px); }
.fleet-tile:focus-visible { outline: none; box-shadow: var(--shadow-focus); }
.fleet-tile__row { display: flex; justify-content: space-between; align-items: center; gap: 8px; }
.fleet-tile__name {
font-family: var(--font-family-mono);
font-size: 12px;
color: var(--color-text-primary);
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
flex: 1;
}
.fleet-tile__grade {
width: 28px; height: 28px;
display: flex; align-items: center; justify-content: center;
font-weight: var(--font-weight-bold);
font-size: 13px;
border-radius: var(--radius-sm);
color: #fff;
flex-shrink: 0;
}
.fleet-tile__grade[data-grade="A"] { background: var(--color-state-success); }
.fleet-tile__grade[data-grade="B"] { background: #4D8E2F; }
.fleet-tile__grade[data-grade="C"] { background: var(--color-severity-medium); }
.fleet-tile__grade[data-grade="D"] { background: var(--color-severity-high); }
.fleet-tile__grade[data-grade="E"] { background: var(--color-severity-critical); }
.fleet-tile__grade[data-grade="F"] { background: var(--color-severity-extreme); }
.fleet-tile__meter {
height: 6px; border-radius: 3px;
background: var(--color-bg-soft);
overflow: hidden;
position: relative;
}
.fleet-tile__meter-fill { height: 100%; border-radius: 3px; }
.fleet-tile__meter-fill[data-band="1"] { background: var(--color-state-success); }
.fleet-tile__meter-fill[data-band="2"] { background: var(--color-severity-medium); }
.fleet-tile__meter-fill[data-band="3"] { background: var(--color-severity-high); }
.fleet-tile__meter-fill[data-band="4"] { background: var(--color-severity-critical); }
.fleet-tile__chip {
display: inline-flex; align-items: center;
font-size: 11px;
padding: 2px 8px;
border-radius: var(--radius-pill);
background: var(--color-bg-soft);
color: var(--color-text-secondary);
border: 1px solid var(--color-border-subtle);
width: fit-content;
}
.fleet-tile__meta {
display: flex; justify-content: space-between;
font-size: 11px; color: var(--color-text-tertiary);
font-family: var(--font-family-mono);
}
.fleet-tile__trend--better { color: var(--color-state-success); }
.fleet-tile__trend--worse { color: var(--color-severity-critical); }
.fleet-tile__trend--stable { color: var(--color-text-tertiary); }
/* =========================================================================
3. Kanban Keep / Review / Remove (.kanban-board)
========================================================================= */
.kanban-board {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: var(--space-4);
}
@media (max-width: 820px) { .kanban-board { grid-template-columns: 1fr; } }
.kanban-col {
background: var(--color-bg-soft);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-md);
padding: var(--space-3);
display: flex; flex-direction: column; gap: var(--space-3);
min-height: 320px;
}
.kanban-col__head {
display: flex; align-items: center; justify-content: space-between;
padding-bottom: var(--space-2);
border-bottom: 2px solid var(--color-border-subtle);
}
.kanban-col[data-bucket="keep"] .kanban-col__head { border-bottom-color: var(--color-state-success); }
.kanban-col[data-bucket="review"] .kanban-col__head { border-bottom-color: var(--color-state-warning); }
.kanban-col[data-bucket="remove"] .kanban-col__head { border-bottom-color: var(--color-severity-critical); }
.kanban-col__title { font-size: var(--font-size-md); font-weight: var(--font-weight-semibold); color: var(--color-text-primary); }
.kanban-col__count {
font-family: var(--font-family-mono);
font-size: 12px;
background: var(--color-surface);
padding: 2px 8px;
border-radius: var(--radius-pill);
color: var(--color-text-secondary);
border: 1px solid var(--color-border-subtle);
}
.kanban-card {
background: var(--color-surface);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-md);
padding: var(--space-3);
cursor: grab;
display: flex; flex-direction: column; gap: 6px;
transition: box-shadow var(--duration-fast);
}
.kanban-card:hover { box-shadow: var(--shadow-md); }
.kanban-card[data-verdict="BLOCK"] { border-color: var(--color-severity-critical); border-left-width: 4px; }
.kanban-card[data-verdict="trusted"] { border-left: 4px solid var(--color-state-success); }
.kanban-card[data-verdict="unknown"] { border-left: 4px solid var(--color-state-warning); }
.kanban-card__name { font-family: var(--font-family-mono); font-size: 13px; color: var(--color-text-primary); word-break: break-all; }
.kanban-card__meta { font-size: 11px; color: var(--color-text-tertiary); }
.kanban-card__reason { font-size: 12px; color: var(--color-text-secondary); }
.kanban-col__empty {
margin: auto;
text-align: center;
color: var(--color-text-tertiary);
font-size: var(--font-size-sm);
padding: var(--space-4);
}
.kanban-col__empty button { margin-top: var(--space-2); }
.kanban-actions { display: flex; gap: 4px; margin-top: 4px; }
.kanban-actions button {
flex: 1; font-size: 11px; padding: 4px 6px;
background: var(--color-bg-soft); border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-sm); color: var(--color-text-secondary);
cursor: pointer; font-family: inherit;
}
.kanban-actions button:hover { background: var(--color-surface-sunken); color: var(--color-text-primary); }
/* =========================================================================
4. Maturity-Ladder (.mat-ladder)
========================================================================= */
.mat-ladder {
display: flex; flex-direction: column;
background: var(--color-surface);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-md);
padding: var(--space-4);
gap: 0;
}
.mat-step {
display: grid;
grid-template-columns: 56px 1fr;
gap: var(--space-4);
padding: var(--space-3) 0;
position: relative;
}
.mat-step + .mat-step { border-top: 1px dashed var(--color-border-subtle); }
.mat-step__icon {
width: 44px; height: 44px;
border-radius: 50%;
display: flex; align-items: center; justify-content: center;
background: var(--color-surface);
border: 2px solid var(--color-border-moderate);
color: var(--color-text-tertiary);
font-weight: var(--font-weight-semibold);
font-size: 15px;
position: relative;
z-index: 1;
}
.mat-step[data-state="completed"] .mat-step__icon {
background: var(--color-state-success);
border-color: var(--color-state-success);
color: #fff;
}
.mat-step[data-state="current"] .mat-step__icon {
border-color: var(--color-primary-500);
color: var(--color-primary-700);
background: var(--color-surface);
}
/* progress ring around current step */
.mat-step__ring {
position: absolute;
inset: -4px;
border-radius: 50%;
pointer-events: none;
}
.mat-step__ring svg { width: 100%; height: 100%; transform: rotate(-90deg); }
.mat-step__ring circle { fill: none; stroke-width: 3; }
.mat-step__ring .ring-bg { stroke: var(--color-border-subtle); }
.mat-step__ring .ring-fill { stroke: var(--color-primary-500); }
.mat-step__name {
font-size: var(--font-size-md);
font-weight: var(--font-weight-semibold);
color: var(--color-text-primary);
display: flex; align-items: center; gap: 8px;
}
.mat-step[data-state="completed"] .mat-step__name { color: var(--color-text-secondary); }
.mat-step[data-state="future"] .mat-step__name { color: var(--color-text-tertiary); }
.mat-step__pill {
font-size: 11px; padding: 2px 8px; border-radius: var(--radius-pill);
text-transform: uppercase; letter-spacing: 0.06em; font-weight: var(--font-weight-semibold);
}
.mat-step__pill--current { background: var(--color-primary-100); color: var(--color-primary-700); }
.mat-step__pill--complete { background: transparent; color: var(--color-state-success); border: 1px solid currentColor; }
.mat-step__desc {
font-size: var(--font-size-sm);
color: var(--color-text-secondary);
margin-top: 2px;
max-width: 60ch;
}
.mat-step__progress {
margin-top: 6px;
display: flex; align-items: center; gap: 8px;
font-size: 12px; color: var(--color-text-tertiary);
}
.mat-step__progress-bar {
flex: 1; height: 4px;
background: var(--color-bg-soft);
border-radius: 2px;
overflow: hidden;
max-width: 200px;
}
.mat-step__progress-fill { height: 100%; background: var(--color-primary-500); border-radius: 2px; }
/* =========================================================================
5. Classify-and-Transform / 5-Bucket-Sorter (.cls-sorter)
========================================================================= */
.cls-sorter { display: flex; flex-direction: column; gap: var(--space-4); }
.cls-input {
background: var(--color-surface);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-md);
padding: var(--space-3);
}
.cls-input textarea {
width: 100%; min-height: 100px;
font-family: var(--font-family-sans);
font-size: var(--font-size-sm);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-sm);
padding: var(--space-2) var(--space-3);
background: var(--color-bg);
color: var(--color-text-primary);
resize: vertical;
}
.cls-input textarea:focus { outline: none; box-shadow: var(--shadow-focus); border-color: var(--color-border-focus); }
.cls-buckets {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: var(--space-3);
}
@media (max-width: 1100px) { .cls-buckets { grid-template-columns: repeat(3, 1fr); } }
@media (max-width: 720px) { .cls-buckets { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 460px) { .cls-buckets { grid-template-columns: 1fr; } }
.cls-bucket {
background: var(--color-surface);
border: 1px solid var(--color-border-subtle);
border-top-width: 4px;
border-radius: var(--radius-md);
padding: var(--space-3);
display: flex; flex-direction: column; gap: var(--space-2);
min-height: 200px;
}
.cls-bucket[data-egnethet="lav"] { border-top-color: var(--color-text-tertiary); }
.cls-bucket[data-egnethet="medium"] { border-top-color: var(--color-state-info); }
.cls-bucket[data-egnethet="hoy"] { border-top-color: var(--color-state-success); }
.cls-bucket__head {
display: flex; flex-direction: column; gap: 2px;
padding-bottom: var(--space-2);
border-bottom: 1px solid var(--color-border-subtle);
}
.cls-bucket__title { font-size: var(--font-size-sm); font-weight: var(--font-weight-semibold); color: var(--color-text-primary); }
.cls-bucket__egnethet {
font-size: 10px; text-transform: uppercase; letter-spacing: 0.08em;
color: var(--color-text-tertiary); font-weight: var(--font-weight-semibold);
}
.cls-bucket[data-egnethet="lav"] .cls-bucket__egnethet { color: var(--color-text-tertiary); }
.cls-bucket[data-egnethet="medium"] .cls-bucket__egnethet { color: var(--color-state-info); }
.cls-bucket[data-egnethet="hoy"] .cls-bucket__egnethet { color: var(--color-state-success); }
.cls-item {
background: var(--color-bg-soft);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-sm);
padding: 6px 8px;
font-size: 12px;
color: var(--color-text-primary);
cursor: grab;
display: flex; flex-direction: column; gap: 2px;
}
.cls-item__action {
font-size: 10px; text-transform: uppercase; letter-spacing: 0.06em;
color: var(--color-text-tertiary); font-weight: var(--font-weight-medium);
}
.cls-bucket__action {
margin-top: auto;
padding-top: var(--space-2);
border-top: 1px dashed var(--color-border-subtle);
}
.cls-bucket__empty {
font-size: 12px; color: var(--color-text-tertiary);
font-style: italic;
text-align: center;
padding: var(--space-3);
}
/* =========================================================================
6. Cycle Position Ribbon (.cycle-ribbon)
========================================================================= */
.cycle-ribbon {
position: relative;
background: var(--color-surface);
border-bottom: 1px solid var(--color-border-subtle);
padding: 8px var(--space-5);
display: flex; align-items: center; gap: var(--space-4);
font-size: 13px;
cursor: pointer;
overflow: hidden;
}
.cycle-ribbon::before {
content: ""; position: absolute; inset: 0;
background: var(--color-state-info);
opacity: 0.06;
width: var(--cycle-progress, 0%);
transition: width var(--duration-normal);
}
.cycle-ribbon[data-phase="planning"] { border-bottom-color: var(--color-state-info); }
.cycle-ribbon[data-phase="planning"]::before { background: var(--color-state-info); }
.cycle-ribbon[data-phase="execution"] { border-bottom-color: var(--color-state-success); }
.cycle-ribbon[data-phase="execution"]::before { background: var(--color-state-success); }
.cycle-ribbon[data-phase="retrospective_prep"] { border-bottom-color: var(--color-severity-medium); }
.cycle-ribbon[data-phase="retrospective_prep"]::before { background: var(--color-severity-medium); }
.cycle-ribbon > * { position: relative; z-index: 1; }
.cycle-ribbon__id { font-family: var(--font-family-mono); font-weight: var(--font-weight-semibold); color: var(--color-text-primary); white-space: nowrap; flex-shrink: 0; }
.cycle-ribbon__week { color: var(--color-text-secondary); font-family: var(--font-family-mono); white-space: nowrap; flex-shrink: 0; }
.cycle-ribbon__phase {
font-size: 11px; padding: 2px 8px;
border-radius: var(--radius-pill);
text-transform: uppercase; letter-spacing: 0.06em;
font-weight: var(--font-weight-semibold);
white-space: nowrap; flex-shrink: 0;
}
.cycle-ribbon[data-phase="planning"] .cycle-ribbon__phase { background: var(--color-primary-100); color: var(--color-primary-700); }
.cycle-ribbon[data-phase="execution"] .cycle-ribbon__phase { background: var(--color-severity-low-soft); color: var(--color-severity-low-on); }
.cycle-ribbon[data-phase="retrospective_prep"] .cycle-ribbon__phase { background: var(--color-severity-medium-soft); color: var(--color-severity-medium-on); }
.cycle-ribbon__msg { color: var(--color-text-secondary); flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cycle-ribbon__chev { color: var(--color-text-tertiary); transition: transform var(--duration-fast); }
.cycle-ribbon[aria-expanded="true"] .cycle-ribbon__chev { transform: rotate(180deg); }
.cycle-ribbon__panel {
background: var(--color-bg-soft);
border-bottom: 1px solid var(--color-border-subtle);
padding: var(--space-4) var(--space-5);
display: none;
font-size: var(--font-size-sm);
}
.cycle-ribbon__panel[data-open="true"] { display: block; }
@media (max-width: 720px) {
.cycle-ribbon__msg { display: none; }
}
/* =========================================================================
7. Persistent-Antipattern Badge (.pap-badge)
========================================================================= */
.pap-badge {
display: inline-flex; align-items: center; gap: 6px;
padding: 4px 10px;
background: var(--color-surface);
border: 1px solid var(--color-severity-critical);
border-radius: var(--radius-pill);
font-size: 12px;
font-weight: var(--font-weight-medium);
color: var(--color-severity-critical);
cursor: pointer;
position: relative;
}
.pap-badge::before {
content: "";
width: 8px; height: 8px;
border-radius: 50%;
background: var(--color-severity-critical);
animation: pap-pulse 2.4s var(--ease-default) infinite;
}
@keyframes pap-pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.45; transform: scale(0.7); }
}
@media (prefers-reduced-motion: reduce) {
.pap-badge::before { animation: none; opacity: 1; }
}
.pap-badge__count { font-family: var(--font-family-mono); font-weight: var(--font-weight-semibold); }
.pap-detail {
margin-top: var(--space-3);
background: var(--color-surface);
border: 1px solid var(--color-severity-critical);
border-left-width: 4px;
border-radius: var(--radius-md);
padding: var(--space-4);
display: none;
}
.pap-detail[data-open="true"] { display: block; }
.pap-detail h4 { margin: 0 0 4px; color: var(--color-severity-critical); font-size: var(--font-size-md); }
.pap-detail__cycles { display: flex; gap: 4px; flex-wrap: wrap; margin: var(--space-2) 0; }
.pap-detail__cycle {
font-family: var(--font-family-mono);
font-size: 11px;
padding: 2px 6px;
background: var(--color-bg-soft);
border-radius: var(--radius-sm);
color: var(--color-text-secondary);
}
.pap-detail__rec {
background: var(--color-bg-soft);
border-radius: var(--radius-sm);
padding: var(--space-2) var(--space-3);
margin-top: var(--space-2);
font-size: var(--font-size-sm);
color: var(--color-text-primary);
}
/* one-shot variant */
.pap-badge--oneshot {
border-style: dashed;
border-color: var(--color-severity-medium);
color: var(--color-severity-medium);
}
.pap-badge--oneshot::before { display: none; }
/* =========================================================================
8. Suppressed-Signals Panel (.suppressed)
========================================================================= */
.suppressed {
background: var(--color-bg-soft);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-md);
overflow: hidden;
}
.suppressed__head {
width: 100%;
display: flex; align-items: center; gap: var(--space-3);
padding: var(--space-3) var(--space-4);
background: transparent;
border: 0;
cursor: pointer;
font-family: inherit;
text-align: left;
color: var(--color-text-secondary);
}
.suppressed__head:hover { background: var(--color-surface-sunken); color: var(--color-text-primary); }
.suppressed__head:focus-visible { outline: none; box-shadow: var(--shadow-focus); }
.suppressed__chev { color: var(--color-text-tertiary); transition: transform var(--duration-fast); }
.suppressed[aria-expanded="true"] .suppressed__chev { transform: rotate(90deg); }
.suppressed__label { font-size: var(--font-size-sm); }
.suppressed__count {
font-family: var(--font-family-mono);
font-size: 12px;
background: var(--color-surface);
padding: 2px 8px;
border-radius: var(--radius-pill);
color: var(--color-text-secondary);
border: 1px solid var(--color-border-subtle);
margin-left: auto;
}
.suppressed__body {
display: none;
padding: 0 var(--space-4) var(--space-4);
}
.suppressed[aria-expanded="true"] .suppressed__body { display: block; }
.suppressed-group {
background: var(--color-surface);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-sm);
padding: var(--space-3);
}
.suppressed-group + .suppressed-group { margin-top: var(--space-2); }
.suppressed-group__head {
display: flex; justify-content: space-between; align-items: center; gap: 8px;
margin-bottom: 4px;
}
.suppressed-group__reason { font-family: var(--font-family-mono); font-size: 12px; color: var(--color-text-tertiary); }
.suppressed-group__count { font-size: 11px; color: var(--color-text-tertiary); }
.suppressed-group__desc { font-size: var(--font-size-sm); color: var(--color-text-secondary); margin: 0 0 6px; }
.suppressed-group__examples {
display: flex; gap: 4px; flex-wrap: wrap;
}
.suppressed-group__example {
font-family: var(--font-family-mono);
font-size: 11px;
background: var(--color-bg-soft);
padding: 2px 6px;
border-radius: var(--radius-sm);
color: var(--color-text-secondary);
}
/* =========================================================================
9. ExpansionCard (Aksel) (.expansion)
========================================================================= */
.expansion {
background: var(--color-surface);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-md);
overflow: hidden;
}
.expansion + .expansion { margin-top: var(--space-2); }
.expansion__head {
width: 100%;
display: flex; align-items: flex-start; gap: var(--space-3);
padding: var(--space-3) var(--space-4);
background: transparent;
border: 0;
cursor: pointer;
font-family: inherit;
text-align: left;
}
.expansion__head:hover { background: var(--color-bg-soft); }
.expansion__head:focus-visible { outline: none; box-shadow: var(--shadow-focus); }
.expansion__title { flex: 1; }
.expansion__title-main { font-size: var(--font-size-md); color: var(--color-text-primary); font-weight: var(--font-weight-medium); }
.expansion__title-sub { font-size: var(--font-size-sm); color: var(--color-text-secondary); margin-top: 2px; }
.expansion__chev {
color: var(--color-text-tertiary);
transition: transform var(--duration-normal) var(--ease-default);
flex-shrink: 0;
margin-top: 2px;
}
.expansion[aria-expanded="true"] .expansion__chev { transform: rotate(180deg); }
.expansion__body {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows var(--duration-normal) var(--ease-default);
}
.expansion[aria-expanded="true"] .expansion__body { grid-template-rows: 1fr; }
.expansion__body-inner { overflow: hidden; }
.expansion__body-inner > div {
padding: 0 var(--space-4) var(--space-4);
border-top: 1px solid var(--color-border-subtle);
padding-top: var(--space-3);
margin-top: -1px;
}
@media (prefers-reduced-motion: reduce) {
.expansion__body { transition: none; }
}
/* =========================================================================
10. ReadMore (Aksel) (.read-more)
========================================================================= */
.read-more {
display: inline;
}
.read-more__trigger {
display: inline-flex; align-items: center; gap: 4px;
background: transparent;
border: 0;
color: var(--color-text-link);
font-family: inherit;
font-size: inherit;
font-weight: var(--font-weight-medium);
cursor: pointer;
padding: 0;
text-decoration: underline;
text-decoration-thickness: 1px;
text-underline-offset: 3px;
}
.read-more__trigger:hover { color: var(--color-text-link-hover); }
.read-more__trigger:focus-visible { outline: none; box-shadow: var(--shadow-focus); border-radius: 2px; }
.read-more__chev { transition: transform var(--duration-fast); }
.read-more[aria-expanded="true"] .read-more__chev { transform: rotate(180deg); }
.read-more__body { display: none; margin-top: var(--space-2); }
.read-more[aria-expanded="true"] .read-more__body { display: block; }
/* =========================================================================
11. FormProgress (Aksel multi-step skjema) (.form-progress)
========================================================================= */
.form-progress {
background: var(--color-surface);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-md);
padding: var(--space-4);
display: flex; flex-direction: column; gap: var(--space-3);
width: 280px;
position: sticky;
top: var(--space-4);
}
.form-progress__autosave {
display: flex; align-items: center; gap: 6px;
font-size: 12px;
color: var(--color-text-tertiary);
padding-bottom: var(--space-2);
border-bottom: 1px solid var(--color-border-subtle);
}
.form-progress__autosave-dot {
width: 6px; height: 6px;
border-radius: 50%;
background: var(--color-state-success);
}
.form-progress__steps { display: flex; flex-direction: column; gap: 2px; }
.fp-step {
display: grid;
grid-template-columns: 28px 1fr;
gap: var(--space-2);
align-items: start;
padding: 8px;
border-radius: var(--radius-sm);
text-align: left;
background: transparent;
border: 0;
cursor: pointer;
font-family: inherit;
position: relative;
}
.fp-step:hover { background: var(--color-bg-soft); }
.fp-step:focus-visible { outline: none; box-shadow: var(--shadow-focus); }
.fp-step[disabled] { cursor: not-allowed; opacity: 0.5; }
.fp-step__num {
width: 22px; height: 22px;
border-radius: 50%;
display: flex; align-items: center; justify-content: center;
background: var(--color-surface);
border: 1.5px solid var(--color-border-moderate);
color: var(--color-text-tertiary);
font-size: 11px;
font-weight: var(--font-weight-semibold);
}
.fp-step[data-state="done"] .fp-step__num {
background: var(--color-state-success);
border-color: var(--color-state-success);
color: #fff;
}
.fp-step[data-state="in-progress"] .fp-step__num {
border-color: var(--color-primary-500);
color: var(--color-primary-700);
font-weight: var(--font-weight-bold);
}
.fp-step__name { font-size: var(--font-size-sm); color: var(--color-text-primary); font-weight: var(--font-weight-medium); }
.fp-step[data-state="done"] .fp-step__name { color: var(--color-text-secondary); font-weight: var(--font-weight-regular); }
.fp-step[data-state="in-progress"] .fp-step__name { color: var(--color-primary-700); font-weight: var(--font-weight-semibold); }
.fp-step__progress {
margin-top: 4px;
font-size: 11px;
color: var(--color-text-tertiary);
display: flex; align-items: center; gap: 6px;
}
.fp-step__bar {
flex: 1; height: 3px;
background: var(--color-bg-soft);
border-radius: 2px; overflow: hidden;
max-width: 80px;
}
.fp-step__bar-fill { height: 100%; background: var(--color-primary-500); }
.form-progress__remaining {
padding-top: var(--space-2);
border-top: 1px solid var(--color-border-subtle);
font-size: 12px; color: var(--color-text-tertiary);
display: flex; justify-content: space-between;
}
/* =========================================================================
12. Aspirational vs Committed Visual (.okr-mode)
Modifier added to OKR Objective cards
========================================================================= */
.okr-mode {
position: relative;
background: var(--color-surface);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-md);
padding: var(--space-4);
}
.okr-mode__gauge {
position: relative;
width: 88px; height: 88px;
display: flex; align-items: center; justify-content: center;
flex-shrink: 0;
}
.okr-mode__gauge svg { position: absolute; inset: 0; transform: rotate(-90deg); width: 100%; height: 100%; }
.okr-mode__gauge circle.gauge-bg { fill: none; stroke: var(--color-border-subtle); stroke-width: 6; }
.okr-mode__gauge circle.gauge-fill { fill: none; stroke: var(--color-state-success); stroke-width: 6; stroke-linecap: round; }
.okr-mode__gauge .gauge-value { font-family: var(--font-family-mono); font-size: 22px; font-weight: var(--font-weight-bold); color: var(--color-text-primary); position: relative; z-index: 1; }
/* aspirational variant — dashed stroke */
.okr-mode[data-mode="aspirational"] .okr-mode__gauge circle.gauge-fill {
stroke: var(--color-scope-okr);
stroke-dasharray: 6 4;
}
.okr-mode__badge {
position: absolute;
top: var(--space-2); right: var(--space-2);
font-size: 10px; font-weight: var(--font-weight-bold); letter-spacing: 0.08em;
padding: 2px 8px;
border-radius: var(--radius-sm);
}
.okr-mode[data-mode="aspirational"] .okr-mode__badge {
background: transparent;
color: var(--color-scope-okr);
border: 1px dashed var(--color-scope-okr);
}
.okr-mode[data-mode="committed"] .okr-mode__badge {
background: var(--color-primary-700);
color: #fff;
}
.okr-mode__row { display: flex; gap: var(--space-4); align-items: center; }
.okr-mode__objective { font-size: var(--font-size-md); color: var(--color-text-primary); flex: 1; }
.okr-mode__hint { font-size: 12px; color: var(--color-text-tertiary); margin-top: 4px; }

View file

@ -0,0 +1,83 @@
/*
* Self-hosted web fonts for Playground Design System.
*
* All three families are licensed under SIL Open Font License 1.1.
* Full license text and provenance: ./fonts/LICENSES.md
*
* Why self-hosted:
* - No external requests (no fonts.googleapis.com, no IP/UA leakage).
* - Works offline / behind air-gapped firewalls.
* - GDPR-compliant for Norwegian public-sector deployments.
*
* Bundle size: ~940 KB total across 9 woff2 files.
* Loaded via font-display: swap to avoid FOIT.
*/
/* ========== Inter (UI / body) ========== */
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url("./fonts/Inter-Regular.woff2") format("woff2");
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 500;
font-display: swap;
src: url("./fonts/Inter-Medium.woff2") format("woff2");
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 600;
font-display: swap;
src: url("./fonts/Inter-SemiBold.woff2") format("woff2");
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 700;
font-display: swap;
src: url("./fonts/Inter-Bold.woff2") format("woff2");
}
/* ========== JetBrains Mono (code) ========== */
@font-face {
font-family: "JetBrains Mono";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url("./fonts/JetBrainsMono-Regular.woff2") format("woff2");
}
@font-face {
font-family: "JetBrains Mono";
font-style: normal;
font-weight: 500;
font-display: swap;
src: url("./fonts/JetBrainsMono-Medium.woff2") format("woff2");
}
@font-face {
font-family: "JetBrains Mono";
font-style: normal;
font-weight: 600;
font-display: swap;
src: url("./fonts/JetBrainsMono-SemiBold.woff2") format("woff2");
}
/* ========== Source Serif 4 (occasional editorial accents) ========== */
@font-face {
font-family: "Source Serif 4";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url("./fonts/SourceSerif4-Regular.woff2") format("woff2");
}
@font-face {
font-family: "Source Serif 4";
font-style: normal;
font-weight: 600;
font-display: swap;
src: url("./fonts/SourceSerif4-Semibold.woff2") format("woff2");
}

Binary file not shown.

View file

@ -0,0 +1,92 @@
Copyright (c) 2016 The Inter Project Authors (https://github.com/rsms/inter)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION AND CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View file

@ -0,0 +1,93 @@
Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View file

@ -0,0 +1,93 @@
Copyright 2014 - 2023 Adobe (http://www.adobe.com/), with Reserved Font Name Source. All Rights Reserved. Source is a trademark of Adobe in the United States and/or other countries.
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View file

@ -0,0 +1,42 @@
# Font Licenses
All three font families bundled with Playground Design System are licensed
under the SIL Open Font License, Version 1.1 (OFL-1.1). They are free to
use, modify, embed, and redistribute under the terms of OFL-1.1.
Full license text per family:
- **Inter** (Regular, Medium, SemiBold, Bold) — `LICENSE-Inter.txt`
Copyright (c) 2016 The Inter Project Authors
Source: https://github.com/rsms/inter
Version bundled: 4.0
- **JetBrains Mono** (Regular, Medium, SemiBold) — `LICENSE-JetBrainsMono.txt`
Copyright 2020 The JetBrains Mono Project Authors
Source: https://github.com/JetBrains/JetBrainsMono
Version bundled: 2.304
- **Source Serif 4** (Regular, Semibold) — `LICENSE-SourceSerif4.md`
Copyright 20142023 Adobe (Reserved Font Name "Source")
Source: https://github.com/adobe-fonts/source-serif
Version bundled: 4.005
## Provenance
Files in this directory were obtained from the upstream release artifacts
linked above on 2026-05-03. Source Serif 4 woff2 files were generated locally
from the desktop OTF release using `fonttools ttLib.woff2 compress`; all
others are unmodified from upstream webfont releases.
## Why bundled
These fonts ship with the design system to eliminate runtime requests to
external CDNs (e.g., fonts.googleapis.com). This guarantees:
- No data leakage about end-user IPs / User-Agents to third parties.
- GDPR compliance for Norwegian public-sector deployments.
- Functioning Playgrounds in offline / air-gapped environments.
Each Playground HTML loads `../shared/playground-design-system/fonts.css`,
which declares all `@font-face` rules pointing at the .woff2 files in this
directory.

View file

@ -0,0 +1,100 @@
<!doctype html>
<html lang="nb">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Aspirational vs Committed · Tier 3 supp</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="stylesheet" href="../../playground-design-system/components-tier3-supplement.css" />
<link rel="stylesheet" href="../../playground-design-system/fonts.css" />
</head>
<body>
<header class="app-header">
<a href="../index.html" class="app-header__brand"><span class="app-header__brand-mark">P</span><span>Playground</span></a>
<span class="app-header__breadcrumb">/ Komponenter / Aspirational vs Committed</span>
</header>
<main class="container container--default" style="padding: var(--space-8) 0;">
<div style="margin-bottom: var(--space-6);">
<span style="font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--color-scope-okr); font-weight: var(--font-weight-semibold);">OKR · visuell modus-skille</span>
<h1 style="margin: 4px 0 6px;">Aspirational vs Committed</h1>
<p class="text-secondary" style="max-width: 65ch;">Modifier på Objective-card. Aspirational (0,7 = success) har stiplet ring + ASP-badge. Committed (1,0 = expected) har solid ring + COM-badge.</p>
</div>
<div style="display:grid; grid-template-columns: 1fr 1fr; gap: var(--space-4);">
<article class="okr-mode" data-mode="aspirational" title="Aspirasjon — 0,7 regnes som vellykket">
<span class="okr-mode__badge">ASP</span>
<div class="okr-mode__row">
<div class="okr-mode__gauge">
<svg viewBox="0 0 100 100" aria-hidden="true">
<circle class="gauge-bg" cx="50" cy="50" r="42"></circle>
<circle class="gauge-fill" cx="50" cy="50" r="42" stroke-dasharray="263.9" stroke-dashoffset="105.6"></circle>
</svg>
<span class="gauge-value">0,60</span>
</div>
<div>
<div class="okr-mode__objective">Bli landets ledende kommune på AI-assistert saksbehandling innen 2027</div>
<div class="okr-mode__hint">Aspirasjon — 0,7 regnes som vellykket</div>
</div>
</div>
</article>
<article class="okr-mode" data-mode="committed" title="Committed — 1,0 forventes oppnådd">
<span class="okr-mode__badge">COM</span>
<div class="okr-mode__row">
<div class="okr-mode__gauge">
<svg viewBox="0 0 100 100" aria-hidden="true">
<circle class="gauge-bg" cx="50" cy="50" r="42"></circle>
<circle class="gauge-fill" cx="50" cy="50" r="42" stroke-dasharray="263.9" stroke-dashoffset="26.4"></circle>
</svg>
<span class="gauge-value">0,90</span>
</div>
<div>
<div class="okr-mode__objective">Innfør sentralisert sensitivity-label-policy for alle 1 850 ansatte før 30. juni</div>
<div class="okr-mode__hint">Committed — 1,0 forventes oppnådd</div>
</div>
</div>
</article>
<article class="okr-mode" data-mode="aspirational">
<span class="okr-mode__badge">ASP</span>
<div class="okr-mode__row">
<div class="okr-mode__gauge">
<svg viewBox="0 0 100 100" aria-hidden="true">
<circle class="gauge-bg" cx="50" cy="50" r="42"></circle>
<circle class="gauge-fill" cx="50" cy="50" r="42" stroke-dasharray="263.9" stroke-dashoffset="184.7"></circle>
</svg>
<span class="gauge-value">0,30</span>
</div>
<div>
<div class="okr-mode__objective">Halver gjennomsnittlig saksbehandlings­tid på byggesøknader</div>
<div class="okr-mode__hint">Aspirasjon — 0,3 så langt, fortsatt rom for å akselerere</div>
</div>
</div>
</article>
<article class="okr-mode" data-mode="committed">
<span class="okr-mode__badge">COM</span>
<div class="okr-mode__row">
<div class="okr-mode__gauge">
<svg viewBox="0 0 100 100" aria-hidden="true">
<circle class="gauge-bg" cx="50" cy="50" r="42"></circle>
<circle class="gauge-fill" cx="50" cy="50" r="42" stroke-dasharray="263.9" stroke-dashoffset="0"></circle>
</svg>
<span class="gauge-value">1,00</span>
</div>
<div>
<div class="okr-mode__objective">Levér T2-rapport til kommunestyret senest 5. september</div>
<div class="okr-mode__hint">Committed — oppnådd</div>
</div>
</div>
</article>
</div>
</main>
</body>
</html>

View file

@ -0,0 +1,86 @@
<!doctype html>
<html lang="nb">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Classify &amp; Transform · Tier 3 supp</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="stylesheet" href="../../playground-design-system/components-tier3-supplement.css" />
<link rel="stylesheet" href="../../playground-design-system/fonts.css" />
</head>
<body>
<header class="app-header">
<a href="../index.html" class="app-header__brand"><span class="app-header__brand-mark">P</span><span>Playground</span></a>
<span class="app-header__breadcrumb">/ Komponenter / Classify-and-Transform</span>
</header>
<main class="container container--wide" style="padding: var(--space-8) 0;">
<div style="margin-bottom: var(--space-6);">
<span style="font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--color-scope-okr); font-weight: var(--font-weight-semibold);">OKR · /okr:skriv strategi-til-OKR</span>
<h1 style="margin: 4px 0 6px;">5-bucket-sorter</h1>
<p class="text-secondary" style="max-width: 65ch;">Lim inn tildelingsbrev øverst — hver krav-setning klassifiseres etter OKR-egnethet (lav, medium, høy).</p>
</div>
<div class="cls-sorter">
<div class="cls-input">
<textarea id="inputText">Bærum kommune skal redusere ventetid på saksbehandling med 30 % innen utgangen av 2026. Innbyggerportalen skal være tilgjengelig 99,5 % av tiden. Andelen selvbetjente saker skal øke fra 42 % til 65 %. Vi skal modernisere innbyggerportalen med AI-assistert chat. Det skal leveres kvartalsrapport til kommunestyret om digitaliseringsfremgang. Copilot for saksbehandlere skal piloteres før Q3.</textarea>
<div style="margin-top: var(--space-3); display: flex; gap: 8px;">
<button class="btn btn--primary btn--sm" onclick="alert('Mock: 6 setninger klassifisert.')">Klassifiser</button>
<span class="text-tertiary" style="font-size: 12px; align-self: center;">6 setninger funnet</span>
</div>
</div>
<div class="cls-buckets" id="buckets"></div>
</div>
</main>
<script>
const buckets = {
drift: { name: "Driftskrav", egnethet: "lav",
items: [{text: "Sikre at innbyggerportalen er tilgjengelig 99,5 % av tiden", action: "→ KPI"}],
cta: "Generer KPI-katalog" },
resultat: { name: "Resultatmål", egnethet: "hoy",
items: [
{text: "Redusere ventetid på saksbehandling med 30 %", action: "→ KR-kandidat"},
{text: "Øke andel selvbetjente saker fra 42 % til 65 %", action: "→ KR-kandidat"},
],
cta: "Generer KR-utkast" },
satsing: { name: "Strategiske satsinger", egnethet: "hoy",
items: [{text: "Modernisere innbyggerportalen med AI-assistert chat", action: "→ Objective-kandidat"}],
cta: "Generer Objective-utkast" },
rapportering: { name: "Rapportering", egnethet: "lav",
items: [{text: "Kvartalsrapport til kommunestyret om digitaliseringsfremgang", action: "→ Rapporteringsrutine"}],
cta: "Skriv rapportmal" },
oppdrag: { name: "Særskilte oppdrag", egnethet: "medium",
items: [{text: "Pilotere Copilot for saksbehandlere før Q3", action: "→ Case by case"}],
cta: "Vurder OKR vs prosjekt" },
};
function egnethetLabel(e) { return e === 'hoy' ? 'Høy egnethet' : e === 'medium' ? 'Medium egnethet' : 'Lav egnethet'; }
function render() {
document.getElementById('buckets').innerHTML = Object.entries(buckets).map(([key, b]) => `
<div class="cls-bucket" data-egnethet="${b.egnethet}" data-key="${key}">
<div class="cls-bucket__head">
<span class="cls-bucket__title">${b.name}</span>
<span class="cls-bucket__egnethet">${egnethetLabel(b.egnethet)}</span>
</div>
${b.items.length ? b.items.map(i => `
<div class="cls-item">
<span>${i.text}</span>
<span class="cls-item__action">${i.action}</span>
</div>
`).join('') : `<div class="cls-bucket__empty">Ingen setninger her.</div>`}
<div class="cls-bucket__action">
<button class="btn btn--secondary btn--sm" style="width:100%;" onclick="alert('Mock: ${b.cta}')">${b.cta}</button>
</div>
</div>
`).join('');
}
render();
</script>
</body>
</html>

View file

@ -0,0 +1,90 @@
<!doctype html>
<html lang="nb">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Cycle Position Ribbon · Tier 3 supp</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="stylesheet" href="../../playground-design-system/components-tier3-supplement.css" />
<link rel="stylesheet" href="../../playground-design-system/fonts.css" />
</head>
<body>
<header class="app-header">
<a href="../index.html" class="app-header__brand"><span class="app-header__brand-mark">P</span><span>Playground</span></a>
<span class="app-header__breadcrumb">/ Komponenter / Cycle Position Ribbon</span>
</header>
<!-- Live ribbon (under header, mock T2-2026 uke 3 av 16) -->
<button type="button" class="cycle-ribbon" data-phase="planning" aria-expanded="false" id="ribbon" style="--cycle-progress: 18%;" onclick="toggleRibbon()">
<span class="cycle-ribbon__id">T2-2026</span>
<span class="cycle-ribbon__week">Uke 3 / 16</span>
<span class="cycle-ribbon__phase">Planning</span>
<span class="cycle-ribbon__msg">Fokuser på check-in-rytme. Første team-check-in bør være innen uke 5.</span>
<span class="cycle-ribbon__chev" aria-hidden="true"></span>
</button>
<div class="cycle-ribbon__panel" id="ribbonPanel">
<div style="display:grid; grid-template-columns: repeat(3, 1fr); gap: var(--space-4); align-items:start;">
<div>
<div class="text-tertiary" style="font-size: 11px; text-transform: uppercase; letter-spacing: .06em;">Periode</div>
<strong>1. mai 31. august 2026</strong>
</div>
<div>
<div class="text-tertiary" style="font-size: 11px; text-transform: uppercase; letter-spacing: .06em;">Fase</div>
<strong>Planning (uke 12)</strong>
<div class="text-secondary" style="font-size: 12px;">Execution starter uke 3, retrospective_prep fra uke 14.</div>
</div>
<div>
<div class="text-tertiary" style="font-size: 11px; text-transform: uppercase; letter-spacing: .06em;">Neste milepel</div>
<strong>Team-check-in 1</strong>
<div class="text-secondary" style="font-size: 12px;">Senest 24. mai 2026 (uke 5).</div>
</div>
</div>
</div>
<main class="container container--wide" style="padding: var(--space-8) 0;">
<div style="margin-bottom: var(--space-6);">
<span style="font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--color-scope-okr); font-weight: var(--font-weight-semibold);">OKR · persistent header</span>
<h1 style="margin: 4px 0 6px;">Cycle Position Ribbon</h1>
<p class="text-secondary" style="max-width: 65ch;">Persistent stripe under app-header som viser hvor i tertialen brukeren er. Klikk for detaljpanel.</p>
</div>
<h2 style="font-size: var(--font-size-lg); margin: 0 0 var(--space-3);">Alle 3 faser</h2>
<div style="display:flex; flex-direction: column; gap: var(--space-4);">
<div class="cycle-ribbon" data-phase="planning" style="--cycle-progress: 12%; border-radius: var(--radius-md); border: 1px solid var(--color-border-subtle);">
<span class="cycle-ribbon__id">T2-2026</span>
<span class="cycle-ribbon__week">Uke 2 / 16</span>
<span class="cycle-ribbon__phase">Planning</span>
<span class="cycle-ribbon__msg">Sett mål og forankre med ledelse.</span>
<span class="cycle-ribbon__chev"></span>
</div>
<div class="cycle-ribbon" data-phase="execution" style="--cycle-progress: 50%; border-radius: var(--radius-md); border: 1px solid var(--color-border-subtle);">
<span class="cycle-ribbon__id">T2-2026</span>
<span class="cycle-ribbon__week">Uke 8 / 16</span>
<span class="cycle-ribbon__phase">Execution</span>
<span class="cycle-ribbon__msg">Halvveis. Halvveissamtale anbefales denne uka.</span>
<span class="cycle-ribbon__chev"></span>
</div>
<div class="cycle-ribbon" data-phase="retrospective_prep" style="--cycle-progress: 88%; border-radius: var(--radius-md); border: 1px solid var(--color-border-subtle);">
<span class="cycle-ribbon__id">T2-2026</span>
<span class="cycle-ribbon__week">Uke 14 / 16</span>
<span class="cycle-ribbon__phase">Retro-prep</span>
<span class="cycle-ribbon__msg">Forbered scoring og retrospektiv. Frist for KR-scoring: 25. august.</span>
<span class="cycle-ribbon__chev"></span>
</div>
</div>
</main>
<script>
function toggleRibbon() {
const r = document.getElementById('ribbon');
const open = r.getAttribute('aria-expanded') === 'true';
r.setAttribute('aria-expanded', open ? 'false' : 'true');
document.getElementById('ribbonPanel').setAttribute('data-open', open ? 'false' : 'true');
}
</script>
</body>
</html>

View file

@ -0,0 +1,85 @@
<!doctype html>
<html lang="nb">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>ExpansionCard · Tier 3 supp</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="stylesheet" href="../../playground-design-system/components-tier3-supplement.css" />
<link rel="stylesheet" href="../../playground-design-system/fonts.css" />
</head>
<body>
<header class="app-header">
<a href="../index.html" class="app-header__brand"><span class="app-header__brand-mark">P</span><span>Playground</span></a>
<span class="app-header__breadcrumb">/ Komponenter / ExpansionCard</span>
</header>
<main class="container container--default" style="padding: var(--space-8) 0;">
<div style="margin-bottom: var(--space-6);">
<span style="font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--color-text-tertiary); font-weight: var(--font-weight-semibold);">Aksel · progressive disclosure</span>
<h1 style="margin: 4px 0 6px;">ExpansionCard</h1>
<p class="text-secondary" style="max-width: 65ch;">Skjul sekundær informasjon bak klikkbar overskrift. Animert utvidelse respekterer prefers-reduced-motion.</p>
</div>
<section class="expansion" aria-expanded="false">
<button type="button" class="expansion__head" onclick="toggleExp(this)">
<span class="expansion__title">
<span class="expansion__title-main">Hva skjer hvis vi avviser denne ROS-rapporten?</span>
<span class="expansion__title-sub">Konsekvenser for utrullingsplanen</span>
</span>
<span class="expansion__chev" aria-hidden="true"></span>
</button>
<div class="expansion__body"><div class="expansion__body-inner"><div>
<p class="text-secondary" style="margin: 0 0 var(--space-2);">Avvises rapporten må arbeidsgruppen ta opp igjen tre trusler i kategori "Kritisk":</p>
<ul style="font-size: var(--font-size-sm); color: var(--color-text-secondary); margin: 0 0 var(--space-2); padding-left: 20px;">
<li>T-014 — DLP-policy for sensitivity labels</li>
<li>T-022 — Cross-tenant Schrems II-vurdering</li>
<li>T-031 — Audit-logging for prompt-historikk</li>
</ul>
<p class="text-secondary" style="margin: 0;">Forventet forsinkelse: 46 uker. Pilot-fasen flyttes fra juni til august.</p>
</div></div></div>
</section>
<section class="expansion" aria-expanded="false">
<button type="button" class="expansion__head" onclick="toggleExp(this)">
<span class="expansion__title">
<span class="expansion__title-main">Hvilke roller skal signere?</span>
<span class="expansion__title-sub">Sjekkliste før innsending</span>
</span>
<span class="expansion__chev" aria-hidden="true"></span>
</button>
<div class="expansion__body"><div class="expansion__body-inner"><div>
<ul style="font-size: var(--font-size-sm); color: var(--color-text-secondary); margin: 0; padding-left: 20px;">
<li>IT-sikkerhetsleder (Eli Bjerke)</li>
<li>Personvernombud (Tor Vagle)</li>
<li>Kommunaldirektør (sponsor)</li>
<li>Tjenesteeier for berørt fagsystem</li>
</ul>
</div></div></div>
</section>
<section class="expansion" aria-expanded="false">
<button type="button" class="expansion__head" onclick="toggleExp(this)">
<span class="expansion__title">
<span class="expansion__title-main">Tekniske detaljer for sentralisert konfig</span>
</span>
<span class="expansion__chev" aria-hidden="true"></span>
</button>
<div class="expansion__body"><div class="expansion__body-inner"><div>
<p class="text-secondary" style="margin: 0;">Konfig versjoneres i <code>git.fromaitochitta.com/playground/configs</code>, valideres ved CI mot <code>config.schema.json</code>, og distribueres via signert artifact til target-tenants.</p>
</div></div></div>
</section>
</main>
<script>
function toggleExp(btn) {
const sec = btn.parentElement;
const open = sec.getAttribute('aria-expanded') === 'true';
sec.setAttribute('aria-expanded', open ? 'false' : 'true');
}
</script>
</body>
</html>

View file

@ -0,0 +1,102 @@
<!doctype html>
<html lang="nb">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Fleet-Overview · Tier 3 supp</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="stylesheet" href="../../playground-design-system/components-tier3-supplement.css" />
<link rel="stylesheet" href="../../playground-design-system/fonts.css" />
</head>
<body>
<header class="app-header">
<a href="../index.html" class="app-header__brand"><span class="app-header__brand-mark">P</span><span>Playground</span></a>
<span class="app-header__breadcrumb">/ Komponenter / Fleet-Overview</span>
</header>
<main class="container container--wide" style="padding: var(--space-8) 0;">
<div style="margin-bottom: var(--space-6);">
<span style="font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--color-scope-security); font-weight: var(--font-weight-semibold);">llm-security · /security dashboard</span>
<h1 style="margin: 4px 0 6px;">Fleet-Overview</h1>
<p class="text-secondary" style="max-width: 65ch;">Cross-project posture på én skjerm. 4 kolonner desktop → 2 → 1.</p>
</div>
<div class="fleet-toolbar">
<span class="fleet-toolbar__label">Sortér</span>
<button class="chip" aria-pressed="true" onclick="sortFleet('worst')">Verste først</button>
<button class="chip" aria-pressed="false" onclick="sortFleet('alpha')">Alfabetisk</button>
<button class="chip" aria-pressed="false" onclick="sortFleet('recent')">Sist skannet</button>
<span class="fleet-toolbar__label" style="margin-left: var(--space-4);">Filter</span>
<button class="chip" aria-pressed="false" onclick="filterFleet('failing')">Kun F + E</button>
<button class="chip" aria-pressed="false" onclick="filterFleet('changed')">Kun med endringer</button>
<span class="fleet-toolbar__spacer"></span>
<span class="fleet-toolbar__count" id="fleetCount">12 prosjekter</span>
</div>
<div class="fleet-grid" id="fleetGrid"></div>
</main>
<script>
const projects = [
{ name: "lier-kommune/copilot-onboarding", grade: "A", risk: 12, band: 1, worst: "info-disclosure", scanned: "2026-05-02 14:11", trend: "stable", changed: false },
{ name: "baerum-kommune/okr-portal", grade: "B", risk: 28, band: 1, worst: "missing-rate-limit", scanned: "2026-05-02 09:32", trend: "better", changed: true },
{ name: "direktorat/sak-arkiv-mcp", grade: "C", risk: 44, band: 2, worst: "weak-auth", scanned: "2026-05-01 18:04", trend: "worse", changed: true },
{ name: "direktorat/llm-saksbehandler", grade: "F", risk: 87, band: 4, worst: "TFA chain (BLOCK)", scanned: "2026-05-02 02:55", trend: "worse", changed: true },
{ name: "trondheim/dpia-helper", grade: "B", risk: 22, band: 1, worst: "log-leakage", scanned: "2026-04-30 11:18", trend: "stable", changed: false },
{ name: "skatteetaten/intern-kb", grade: "D", risk: 61, band: 3, worst: "prompt-injection", scanned: "2026-05-02 07:42", trend: "better", changed: true },
{ name: "nav/saksbehandler-co", grade: "C", risk: 39, band: 2, worst: "ssrf-risk", scanned: "2026-05-01 23:01", trend: "stable", changed: false },
{ name: "udi/ai-translator", grade: "E", risk: 73, band: 3, worst: "data-residency", scanned: "2026-05-02 12:30", trend: "worse", changed: true },
{ name: "dsb/krise-bot", grade: "A", risk: 8, band: 1, worst: "minor-typo", scanned: "2026-04-29 16:50", trend: "stable", changed: false },
{ name: "domstol/dom-summary", grade: "B", risk: 25, band: 1, worst: "context-leakage", scanned: "2026-05-01 10:14", trend: "better", changed: true },
{ name: "helsedir/symptomsjekk", grade: "F", risk: 91, band: 4, worst: "PHI exfiltration", scanned: "2026-05-02 04:18", trend: "worse", changed: true },
{ name: "kommune/innsyn-mcp", grade: "C", risk: 47, band: 2, worst: "broad-scope", scanned: "2026-05-01 19:55", trend: "stable", changed: false },
];
const trendArrow = { better: "↗ bedre", worse: "↘ verre", stable: "→ stabil" };
const grid = document.getElementById('fleetGrid');
let mode = 'worst', filter = 'none';
function render() {
let list = projects.slice();
if (filter === 'failing') list = list.filter(p => p.grade === 'F' || p.grade === 'E');
if (filter === 'changed') list = list.filter(p => p.changed);
if (mode === 'worst') list.sort((a,b) => b.risk - a.risk);
if (mode === 'alpha') list.sort((a,b) => a.name.localeCompare(b.name));
if (mode === 'recent') list.sort((a,b) => b.scanned.localeCompare(a.scanned));
grid.innerHTML = list.map(p => `
<button class="fleet-tile" onclick="alert('Naviger til posture for ${p.name}')">
<div class="fleet-tile__row">
<span class="fleet-tile__name" title="${p.name}">${p.name}</span>
<span class="fleet-tile__grade" data-grade="${p.grade}">${p.grade}</span>
</div>
<div class="fleet-tile__meter"><div class="fleet-tile__meter-fill" data-band="${p.band}" style="width:${p.risk}%"></div></div>
<span class="fleet-tile__chip">${p.worst}</span>
<div class="fleet-tile__meta">
<span>${p.scanned}</span>
<span class="fleet-tile__trend--${p.trend}">${trendArrow[p.trend]}</span>
</div>
</button>
`).join('');
document.getElementById('fleetCount').textContent = list.length + ' prosjekter';
}
function sortFleet(m) {
mode = m;
document.querySelectorAll('.fleet-toolbar .chip').forEach(c => {
if (['Verste først', 'Alfabetisk', 'Sist skannet'].includes(c.textContent)) c.setAttribute('aria-pressed', 'false');
});
event.target.setAttribute('aria-pressed', 'true');
render();
}
function filterFleet(f) {
filter = filter === f ? 'none' : f;
event.target.setAttribute('aria-pressed', filter === f ? 'true' : 'false');
render();
}
render();
</script>
</body>
</html>

View file

@ -0,0 +1,81 @@
<!doctype html>
<html lang="nb">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>FormProgress · Tier 3 supp</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="stylesheet" href="../../playground-design-system/components-tier3-supplement.css" />
<link rel="stylesheet" href="../../playground-design-system/fonts.css" />
<style>
.demo-layout { display: grid; grid-template-columns: 280px 1fr; gap: var(--space-6); align-items: start; }
@media (max-width: 820px) { .demo-layout { grid-template-columns: 1fr; } .form-progress { width: 100%; position: static; } }
.form-area { background: var(--color-surface); border: 1px solid var(--color-border-subtle); border-radius: var(--radius-md); padding: var(--space-5); min-height: 360px; }
</style>
</head>
<body>
<header class="app-header">
<a href="../index.html" class="app-header__brand"><span class="app-header__brand-mark">P</span><span>Playground</span></a>
<span class="app-header__breadcrumb">/ Komponenter / FormProgress</span>
</header>
<main class="container container--wide" style="padding: var(--space-8) 0;">
<div style="margin-bottom: var(--space-6);">
<span style="font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--color-scope-architect); font-weight: var(--font-weight-semibold);">ms-ai-architect onboarding · OKR /oppsett full · DPIA</span>
<h1 style="margin: 4px 0 6px;">FormProgress</h1>
<p class="text-secondary" style="max-width: 65ch;">Vertikal sidebar for store skjema. Autosave-status, ferdig-prosent per steg, estimert resterende tid. Ikke å forveksle med horisontal stepper.</p>
</div>
<div class="demo-layout">
<aside class="form-progress">
<div class="form-progress__autosave">
<span class="form-progress__autosave-dot" aria-hidden="true"></span>
Lagret automatisk kl. 14:23
</div>
<div class="form-progress__steps">
<button type="button" class="fp-step" data-state="done">
<span class="fp-step__num"></span>
<span><span class="fp-step__name">Organisasjon &amp; kontekst</span></span>
</button>
<button type="button" class="fp-step" data-state="done">
<span class="fp-step__num"></span>
<span><span class="fp-step__name">Brukstilfeller</span></span>
</button>
<button type="button" class="fp-step" data-state="in-progress">
<span class="fp-step__num">3</span>
<span>
<span class="fp-step__name">Datakilder &amp; klassifisering</span>
<span class="fp-step__progress">
<span>62 %</span>
<span class="fp-step__bar"><span class="fp-step__bar-fill" style="width:62%"></span></span>
</span>
</span>
</button>
<button type="button" class="fp-step" data-state="todo" disabled title="Fullfør steg 3 først">
<span class="fp-step__num">4</span>
<span><span class="fp-step__name">Roller &amp; ansvar</span></span>
</button>
<button type="button" class="fp-step" data-state="todo" disabled title="Fullfør steg 3 først">
<span class="fp-step__num">5</span>
<span><span class="fp-step__name">Risiko &amp; tiltak</span></span>
</button>
</div>
<div class="form-progress__remaining">
<span>Resterende</span>
<span>~ 9 min</span>
</div>
</aside>
<section class="form-area">
<div class="text-tertiary" style="font-size: 11px; text-transform: uppercase; letter-spacing: .06em;">Steg 3 av 5</div>
<h2 style="margin: 4px 0 var(--space-4); font-size: var(--font-size-xl);">Datakilder &amp; klassifisering</h2>
<p class="text-secondary" style="font-size: var(--font-size-sm);">Skjemaet hadde 12 felt — 7 utfylt, 5 igjen. Estimert ferdig om 5 minutter.</p>
<div style="margin-top: var(--space-4); padding: var(--space-4); background: var(--color-bg-soft); border-radius: var(--radius-md); font-size: var(--font-size-sm); color: var(--color-text-tertiary);">[Skjema-felt placeholder — i produksjon: input/select/textarea]</div>
</section>
</div>
</main>
</body>
</html>

View file

@ -0,0 +1,144 @@
<!doctype html>
<html lang="nb">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Kanban · Keep/Review/Remove · Tier 3 supp</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="stylesheet" href="../../playground-design-system/components-tier3-supplement.css" />
<link rel="stylesheet" href="../../playground-design-system/fonts.css" />
<style>
.modal-bg { position: fixed; inset: 0; background: var(--color-overlay); display: none; align-items: center; justify-content: center; z-index: 100; padding: var(--space-4); }
.modal-bg[data-open="true"] { display: flex; }
.modal { background: var(--color-surface); border-radius: var(--radius-lg); padding: var(--space-5); max-width: 540px; width: 100%; box-shadow: var(--shadow-lg); max-height: 90vh; overflow: auto; }
.checklist { list-style: none; padding: 0; margin: var(--space-3) 0; display: flex; flex-direction: column; gap: 6px; }
.checklist li { display: flex; gap: 8px; font-size: var(--font-size-sm); }
.checklist .ok { color: var(--color-state-success); }
.checklist .no { color: var(--color-severity-critical); }
.checklist .un { color: var(--color-text-tertiary); }
</style>
</head>
<body>
<header class="app-header">
<a href="../index.html" class="app-header__brand"><span class="app-header__brand-mark">P</span><span>Playground</span></a>
<span class="app-header__breadcrumb">/ Komponenter / Kanban: Keep/Review/Remove</span>
</header>
<main class="container container--wide" style="padding: var(--space-8) 0;">
<div style="margin-bottom: var(--space-6);">
<span style="font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--color-scope-security); font-weight: var(--font-weight-semibold);">llm-security · /security plugin-audit</span>
<h1 style="margin: 4px 0 6px;">Kanban: Behold / Vurder / Fjern</h1>
<p class="text-secondary" style="max-width: 65ch;">Klassifisér installerte plugins/MCP-servere etter trust. Klikk-flytt mellom kolonner.</p>
</div>
<div class="kanban-board" id="board"></div>
</main>
<div class="modal-bg" id="modal" onclick="if(event.target===this) closeModal()">
<div class="modal" id="modalBody"></div>
</div>
<script>
const board = {
keep: { title: "Behold", items: [
{ name: "anthropic/claude-code-mcp", verdict: "trusted", meta: "Sist auditert 2026-04-15" },
{ name: "github/copilot-chat", verdict: "trusted", meta: "Sist auditert 2026-04-12" },
{ name: "lier-kommune/internal-mcp", verdict: "trusted", meta: "Sist auditert 2026-04-30" },
{ name: "digdir/auth-mcp", verdict: "trusted", meta: "Sist auditert 2026-05-01" },
]},
review: { title: "Vurder", items: [
{ name: "thirdparty/web-search", verdict: "unknown", meta: "Audit due 2026-06-01" },
{ name: "community/markdown-tools", verdict: "unknown", meta: "Audit due 2026-05-20" },
]},
remove: { title: "Fjern", items: [
{ name: "evil-project-health@1.2.3", verdict: "BLOCK", reason: "85 funn (24 critical), Unicode-steganografi, exfil-flow" },
]},
};
const checklists = {
trusted: [
{ok:'ok', text:'Source repo verifisert (signed commits)'},
{ok:'ok', text:'Maintainer kjent og aktiv'},
{ok:'ok', text:'Ingen kritiske funn siste audit'},
{ok:'ok', text:'Capabilities dokumentert og minst-mulig'},
{ok:'ok', text:'Ingen exfil-flow detektert'},
{ok:'ok', text:'Lisens kompatibel med offentlig bruk'},
{ok:'ok', text:'Versjon pinnet i lockfile'},
{ok:'ok', text:'Endringslogg konsistent med kode'},
{ok:'ok', text:'Trust-skår &gt; 80'},
],
unknown: [
{ok:'un', text:'Source repo verifisert'},
{ok:'ok', text:'Maintainer kjent'},
{ok:'un', text:'Audit ikke utført siste 90 d'},
{ok:'ok', text:'Capabilities dokumentert'},
{ok:'un', text:'Exfil-analyse ikke kjørt'},
{ok:'ok', text:'Lisens OK'},
{ok:'ok', text:'Versjon pinnet'},
{ok:'un', text:'Endringslogg ufullstendig'},
{ok:'un', text:'Trust-skår ikke beregnet'},
],
BLOCK: [
{ok:'no', text:'Unicode-tag-injeksjon i README (steganografi)'},
{ok:'no', text:'Exfil til webhook.site/abc123 detektert'},
{ok:'no', text:'24 kritiske TFA-funn'},
{ok:'no', text:'Maintainer ikke verifiserbar'},
{ok:'no', text:'Source-repo nylig opprettet (typosquat?)'},
{ok:'no', text:'Bash + filsystem + nett uten begrensning'},
{ok:'no', text:'Lisens uklar'},
{ok:'no', text:'Versjon ikke pinnet'},
{ok:'no', text:'Trust-skår: 4'},
],
};
function render() {
document.getElementById('board').innerHTML = ['keep','review','remove'].map(b => `
<div class="kanban-col" data-bucket="${b}">
<div class="kanban-col__head">
<span class="kanban-col__title">${board[b].title}</span>
<span class="kanban-col__count">${board[b].items.length}</span>
</div>
${board[b].items.length ? board[b].items.map((it, i) => `
<div class="kanban-card" data-verdict="${it.verdict}" onclick="openModal('${b}', ${i})">
<span class="kanban-card__name">${it.name}</span>
${it.meta ? `<span class="kanban-card__meta">${it.meta}</span>` : ''}
${it.reason ? `<span class="kanban-card__reason">${it.reason}</span>` : ''}
<div class="kanban-actions" onclick="event.stopPropagation()">
${b !== 'keep' ? `<button onclick="move('${b}','keep',${i})">→ Behold</button>` : ''}
${b !== 'review' ? `<button onclick="move('${b}','review',${i})">→ Vurder</button>` : ''}
${b !== 'remove' ? `<button onclick="move('${b}','remove',${i})">→ Fjern</button>` : ''}
</div>
</div>
`).join('') : `<div class="kanban-col__empty">Ingen i denne bøtten ennå.<br><button class="btn btn--secondary btn--sm" style="margin-top:8px;">+ Legg til</button></div>`}
</div>
`).join('');
}
function move(from, to, i) {
const item = board[from].items.splice(i, 1)[0];
board[to].items.push(item);
render();
}
function openModal(b, i) {
const it = board[b].items[i];
const cl = checklists[it.verdict] || checklists.unknown;
const sym = { ok: '✓', no: '✗', un: '?' };
document.getElementById('modalBody').innerHTML = `
<div style="display:flex; justify-content:space-between; align-items:center; gap:12px;">
<h3 style="margin:0;">${it.name}</h3>
<button class="btn btn--ghost btn--sm" onclick="closeModal()">Lukk</button>
</div>
<p class="text-secondary" style="font-size:var(--font-size-sm); margin:6px 0 0;">Trust-vurdering · ${it.verdict.toUpperCase()}</p>
<ul class="checklist">${cl.map(c => `<li><span class="${c.ok}">${sym[c.ok]}</span><span>${c.text}</span></li>`).join('')}</ul>
`;
document.getElementById('modal').setAttribute('data-open', 'true');
}
function closeModal() { document.getElementById('modal').setAttribute('data-open', 'false'); }
render();
</script>
</body>
</html>

View file

@ -0,0 +1,97 @@
<!doctype html>
<html lang="nb">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Maturity-Ladder · Tier 3 supp</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="stylesheet" href="../../playground-design-system/components-tier3-supplement.css" />
<link rel="stylesheet" href="../../playground-design-system/fonts.css" />
<style>
.demo-row { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-5); }
@media (max-width: 980px) { .demo-row { grid-template-columns: 1fr; } }
</style>
</head>
<body>
<header class="app-header">
<a href="../index.html" class="app-header__brand"><span class="app-header__brand-mark">P</span><span>Playground</span></a>
<span class="app-header__breadcrumb">/ Komponenter / Maturity-Ladder</span>
</header>
<main class="container container--wide" style="padding: var(--space-8) 0;">
<div style="margin-bottom: var(--space-6);">
<span style="font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--color-scope-okr); font-weight: var(--font-weight-semibold);">OKR · config-audit · security</span>
<h1 style="margin: 4px 0 6px;">Maturity-Ladder</h1>
<p class="text-secondary" style="max-width: 65ch;">Vertikal stepper med rik beskrivelse. Current step har progress-ring (her 65 %).</p>
</div>
<div class="demo-row">
<section>
<h2 style="font-size: var(--font-size-lg); margin: 0 0 var(--space-3);">OKR-modenhet (4 nivåer)</h2>
<div class="mat-ladder">
<div class="mat-step" data-state="completed">
<div class="mat-step__icon" aria-hidden="true"></div>
<div>
<div class="mat-step__name">Utforsker <span class="mat-step__pill mat-step__pill--complete">Fullført</span></div>
<div class="mat-step__desc">Eksperimenterer med OKR i 12 team. Ingen formell rytme.</div>
</div>
</div>
<div class="mat-step" data-state="current">
<div class="mat-step__icon" aria-hidden="true">
2
<span class="mat-step__ring" aria-hidden="true">
<svg viewBox="0 0 52 52"><circle class="ring-bg" cx="26" cy="26" r="24"></circle><circle class="ring-fill" cx="26" cy="26" r="24" stroke-dasharray="150.8" stroke-dashoffset="52.8"></circle></svg>
</span>
</div>
<div>
<div class="mat-step__name">Pilot <span class="mat-step__pill mat-step__pill--current"></span></div>
<div class="mat-step__desc">OKR i én avdeling. Kvartalsrytme etablert. Ledelse engasjert.</div>
<div class="mat-step__progress"><span>65 %</span><span class="mat-step__progress-bar"><span class="mat-step__progress-fill" style="width:65%"></span></span><span>til Skalering</span></div>
</div>
</div>
<div class="mat-step" data-state="future">
<div class="mat-step__icon" aria-hidden="true">3</div>
<div>
<div class="mat-step__name">Skalering</div>
<div class="mat-step__desc">OKR rullet ut til hele organisasjonen. Cross-team alignment.</div>
</div>
</div>
<div class="mat-step" data-state="future">
<div class="mat-step__icon" aria-hidden="true">4</div>
<div>
<div class="mat-step__name">Moden</div>
<div class="mat-step__desc">OKR er drift. Strategisk forankring fra Stortingsmelding til team-OKR.</div>
</div>
</div>
</div>
</section>
<section>
<h2 style="font-size: var(--font-size-lg); margin: 0 0 var(--space-3);">Config-modenhet (5 nivåer)</h2>
<div class="mat-ladder">
<div class="mat-step" data-state="completed"><div class="mat-step__icon" aria-hidden="true"></div>
<div><div class="mat-step__name">Bare <span class="mat-step__pill mat-step__pill--complete">Fullført</span></div>
<div class="mat-step__desc">Defaults overalt. Ingen sentralisert konfig.</div></div></div>
<div class="mat-step" data-state="completed"><div class="mat-step__icon" aria-hidden="true"></div>
<div><div class="mat-step__name">Configured <span class="mat-step__pill mat-step__pill--complete">Fullført</span></div>
<div class="mat-step__desc">Eksplisitte verdier per miljø. Ingen drift-deteksjon.</div></div></div>
<div class="mat-step" data-state="current"><div class="mat-step__icon" aria-hidden="true">3
<span class="mat-step__ring" aria-hidden="true"><svg viewBox="0 0 52 52"><circle class="ring-bg" cx="26" cy="26" r="24"></circle><circle class="ring-fill" cx="26" cy="26" r="24" stroke-dasharray="150.8" stroke-dashoffset="105.6"></circle></svg></span></div>
<div><div class="mat-step__name">Structured <span class="mat-step__pill mat-step__pill--current"></span></div>
<div class="mat-step__desc">Skjema-validert konfig. Versjonert i Git. Endringssporbarhet.</div>
<div class="mat-step__progress"><span>30 %</span><span class="mat-step__progress-bar"><span class="mat-step__progress-fill" style="width:30%"></span></span><span>til Automated</span></div></div></div>
<div class="mat-step" data-state="future"><div class="mat-step__icon" aria-hidden="true">4</div>
<div><div class="mat-step__name">Automated</div>
<div class="mat-step__desc">CI-validering. Auto-rollback ved feil. Drift-detektor.</div></div></div>
<div class="mat-step" data-state="future"><div class="mat-step__icon" aria-hidden="true">5</div>
<div><div class="mat-step__name">Governed</div>
<div class="mat-step__desc">Policy-as-code. Audit-trail. Approval-workflows for prod.</div></div></div>
</div>
</section>
</div>
</main>
</body>
</html>

View file

@ -0,0 +1,99 @@
<!doctype html>
<html lang="nb">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Persistent-Antipattern Badge · Tier 3 supp</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="stylesheet" href="../../playground-design-system/components-tier3-supplement.css" />
<link rel="stylesheet" href="../../playground-design-system/fonts.css" />
</head>
<body>
<header class="app-header">
<a href="../index.html" class="app-header__brand"><span class="app-header__brand-mark">P</span><span>Playground</span></a>
<span class="app-header__breadcrumb">/ Komponenter / Persistent-Antipattern Badge</span>
</header>
<main class="container container--wide" style="padding: var(--space-8) 0;">
<div style="margin-bottom: var(--space-6);">
<span style="font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--color-scope-okr); font-weight: var(--font-weight-semibold);">OKR · /okr:analyse cross-cycle</span>
<h1 style="margin: 4px 0 6px;">Persistent-Antipattern Badge</h1>
<p class="text-secondary" style="max-width: 65ch;">Markerer antipatterns som har dukket opp i 2+ påfølgende sykluser. Pulserende prikk skiller seg fra one-shot.</p>
</div>
<h2 style="font-size: var(--font-size-lg); margin: 0 0 var(--space-3);">I bruk i en finding-tabell</h2>
<table style="width:100%; border-collapse: collapse; background: var(--color-surface); border: 1px solid var(--color-border-subtle); border-radius: var(--radius-md); overflow:hidden;">
<thead>
<tr style="background: var(--color-bg-soft);">
<th style="text-align:left; padding: 10px 14px; font-size: 12px; text-transform: uppercase; letter-spacing: .06em; color: var(--color-text-tertiary); border-bottom: 1px solid var(--color-border-subtle);">Antipattern</th>
<th style="text-align:left; padding: 10px 14px; font-size: 12px; text-transform: uppercase; letter-spacing: .06em; color: var(--color-text-tertiary); border-bottom: 1px solid var(--color-border-subtle);">Funnet i</th>
<th style="text-align:left; padding: 10px 14px; font-size: 12px; text-transform: uppercase; letter-spacing: .06em; color: var(--color-text-tertiary); border-bottom: 1px solid var(--color-border-subtle);">Status</th>
</tr>
</thead>
<tbody>
<tr>
<td style="padding: 12px 14px;">Aktivitetsfokus i KR</td>
<td style="padding: 12px 14px; font-family: var(--font-family-mono); font-size: 12px; color: var(--color-text-secondary);">T1-25 · T2-25 · T3-25 · T1-26 · T2-26</td>
<td style="padding: 12px 14px;">
<button type="button" class="pap-badge" onclick="togglePap(0)" aria-expanded="false" aria-controls="papDetail0">
Vedvarende <span class="pap-badge__count">5 sykluser</span>
</button>
</td>
</tr>
<tr>
<td style="padding: 12px 14px;">Sandbagging av target-verdier</td>
<td style="padding: 12px 14px; font-family: var(--font-family-mono); font-size: 12px; color: var(--color-text-secondary);">T2-25 · T3-25 · T1-26</td>
<td style="padding: 12px 14px;">
<button type="button" class="pap-badge" onclick="togglePap(1)" aria-expanded="false" aria-controls="papDetail1">
Vedvarende <span class="pap-badge__count">3 sykluser</span>
</button>
</td>
</tr>
<tr>
<td style="padding: 12px 14px;">For mange KR per Objective</td>
<td style="padding: 12px 14px; font-family: var(--font-family-mono); font-size: 12px; color: var(--color-text-secondary);">T2-26</td>
<td style="padding: 12px 14px;">
<span class="pap-badge pap-badge--oneshot" title="Kun én syklus så langt">Én syklus</span>
</td>
</tr>
</tbody>
</table>
<section class="pap-detail" id="papDetail0" style="margin-top: var(--space-3);">
<h4>Aktivitetsfokus i KR</h4>
<p class="text-secondary" style="margin: 0; font-size: var(--font-size-sm);">KR-formuleringer beskriver aktiviteter ("Innføre nytt system", "Pilotere X") i stedet for målbare utfall. Vedvarende mønster på tvers av sykluser indikerer at OKR-coaching ikke har festet seg.</p>
<div class="pap-detail__cycles">
<span class="pap-detail__cycle">T1-2025 · 4 forekomster</span>
<span class="pap-detail__cycle">T2-2025 · 3 forekomster</span>
<span class="pap-detail__cycle">T3-2025 · 5 forekomster</span>
<span class="pap-detail__cycle">T1-2026 · 6 forekomster</span>
<span class="pap-detail__cycle">T2-2026 · 4 forekomster</span>
</div>
<div class="pap-detail__rec"><strong>Anbefaling:</strong> Vurder OKR-coaching eller retrospective-fokus på outcome vs activity. Spør "Hva endrer seg for innbyggeren hvis dette KR-et oppfylles?"</div>
</section>
<section class="pap-detail" id="papDetail1" style="margin-top: var(--space-3);">
<h4>Sandbagging av target-verdier</h4>
<p class="text-secondary" style="margin: 0; font-size: var(--font-size-sm);">Targets satt så lavt at de oppnås uten reell innsats. Score &gt; 0,9 to sykluser på rad uten endring i baseline.</p>
<div class="pap-detail__cycles">
<span class="pap-detail__cycle">T2-2025</span>
<span class="pap-detail__cycle">T3-2025</span>
<span class="pap-detail__cycle">T1-2026</span>
</div>
<div class="pap-detail__rec"><strong>Anbefaling:</strong> Innfør stretch-target som komplement, eller vurder aspirational vs committed-skille (se OKR-mode).</div>
</section>
</main>
<script>
function togglePap(i) {
const d = document.getElementById('papDetail' + i);
const open = d.getAttribute('data-open') === 'true';
d.setAttribute('data-open', open ? 'false' : 'true');
event.currentTarget.setAttribute('aria-expanded', open ? 'false' : 'true');
}
</script>
</body>
</html>

View file

@ -0,0 +1,59 @@
<!doctype html>
<html lang="nb">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>ReadMore · Tier 3 supp</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="stylesheet" href="../../playground-design-system/components-tier3-supplement.css" />
<link rel="stylesheet" href="../../playground-design-system/fonts.css" />
</head>
<body>
<header class="app-header">
<a href="../index.html" class="app-header__brand"><span class="app-header__brand-mark">P</span><span>Playground</span></a>
<span class="app-header__breadcrumb">/ Komponenter / ReadMore</span>
</header>
<main class="container container--narrow" style="padding: var(--space-8) 0;">
<div style="margin-bottom: var(--space-6);">
<span style="font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--color-text-tertiary); font-weight: var(--font-weight-semibold);">Aksel · inline disclosure</span>
<h1 style="margin: 4px 0 6px;">ReadMore</h1>
<p class="text-secondary">Inline-trigger for å skjule lange forklaringer mid-tekst.</p>
</div>
<article style="font-size: var(--font-size-md); line-height: var(--line-height-normal); color: var(--color-text-primary);">
<p>Sensitivity Labels brukes til å klassifisere dokumenter etter konfidensialitetsnivå.
<span class="read-more" aria-expanded="false">
<button type="button" class="read-more__trigger" onclick="toggleRm(this)">Les mer om hvordan dette håndheves <span class="read-more__chev"></span></button>
<span class="read-more__body">
Når et dokument merkes "Konfidensielt — intern", vil M365 Copilot ikke oppsummere innholdet for brukere uten samme tilgangsnivå.
DLP-policyen sjekker label-attributter ved hver prompt-respons og avbryter generering hvis cross-label data flyter sammen.
For Lier kommune betyr dette at saksbehandlere på Helse-avdelingen ikke utilsiktet kan dra inn HR-relatert informasjon i samme svar.
</span>
</span>
</p>
<p style="margin-top: var(--space-4);">Schrems II-vurdering kreves for cross-tenant data-flyt.
<span class="read-more" aria-expanded="false">
<button type="button" class="read-more__trigger" onclick="toggleRm(this)">Hva betyr Schrems II i praksis? <span class="read-more__chev"></span></button>
<span class="read-more__body">
EU-domstolen kjente Privacy Shield ugyldig i 2020. Overføring av personopplysninger til USA krever supplerende tiltak (TIAs, krypteringsnøkler i EU).
For Microsoft 365-tenants betyr dette at EU Data Boundary må være aktivert, og at audit-logger må bekrefte at prompt-data ikke forlater EØS.
</span>
</span>
</p>
</article>
</main>
<script>
function toggleRm(btn) {
const sec = btn.parentElement;
const open = sec.getAttribute('aria-expanded') === 'true';
sec.setAttribute('aria-expanded', open ? 'false' : 'true');
}
</script>
</body>
</html>

View file

@ -0,0 +1,117 @@
<!doctype html>
<html lang="nb">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Toxic-Flow Chain · Tier 3 supp</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="stylesheet" href="../../playground-design-system/components-tier3-supplement.css" />
<link rel="stylesheet" href="../../playground-design-system/fonts.css" />
</head>
<body>
<header class="app-header">
<a href="../index.html" class="app-header__brand"><span class="app-header__brand-mark">P</span><span>Playground</span></a>
<span class="app-header__breadcrumb">/ Komponenter / Toxic-Flow Chain (TFA)</span>
</header>
<main class="container container--wide" style="padding: var(--space-8) 0;">
<div style="margin-bottom: var(--space-6);">
<span style="font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--color-scope-security); font-weight: var(--font-weight-semibold);">llm-security · TFA</span>
<h1 style="margin: 4px 0 6px;">Toxic-Flow Chain</h1>
<p class="text-secondary" style="max-width: 65ch;">Trifecta Flow Analysis: Input → Access → Exfil. Hver leg viser type, kilde og mitigation-status. Tykkere arrows = høyere severity. Grønt skjold = mitigation som bryter kjeden.</p>
</div>
<h2 style="font-size: var(--font-size-lg); margin: 0 0 var(--space-3);">TFA-2026-118-001 — BLOCK</h2>
<div class="tfa-flow" id="flow1">
<span class="tfa-flow__verdict" data-verdict="BLOCK">BLOCK</span>
<button type="button" class="tfa-leg" data-severity="high" onclick="alert('Drill-down: Input leg — skill markdown fil')">
<span class="tfa-leg__label">Input</span>
<span class="tfa-leg__name">Untrusted data</span>
<span class="tfa-leg__source">skill markdown-fil</span>
<span class="tfa-leg__status" data-mit="unmitigated">● Ikke mitigert</span>
</button>
<span class="tfa-arrow" data-severity="critical" aria-hidden="true"><span class="tfa-arrow__line"></span></span>
<button type="button" class="tfa-leg" data-severity="critical" onclick="alert('Drill-down: Access leg — Bash + filsystem')">
<span class="tfa-leg__label">Access</span>
<span class="tfa-leg__name">Sensitive capability</span>
<span class="tfa-leg__source">Bash · filsystem-tilgang</span>
<span class="tfa-leg__status" data-mit="partially_mitigated">◐ Delvis mitigert</span>
</button>
<span class="tfa-arrow" data-severity="critical" aria-hidden="true"><span class="tfa-arrow__line"></span></span>
<button type="button" class="tfa-leg" data-severity="critical" onclick="alert('Drill-down: Exfil leg — webhook.site')">
<span class="tfa-leg__label">Exfil</span>
<span class="tfa-leg__name">External endpoint</span>
<span class="tfa-leg__source">webhook.site/abc123</span>
<span class="tfa-leg__status" data-mit="unmitigated">● Ikke mitigert</span>
</button>
</div>
<h2 style="font-size: var(--font-size-lg); margin: var(--space-8) 0 var(--space-3);">TFA-2026-118-002 — WARN (mitigation present)</h2>
<div class="tfa-flow">
<span class="tfa-flow__verdict" data-verdict="WARN">WARN</span>
<button type="button" class="tfa-leg" data-severity="medium">
<span class="tfa-leg__label">Input</span>
<span class="tfa-leg__name">User prompt</span>
<span class="tfa-leg__source">chat input</span>
<span class="tfa-leg__status" data-mit="mitigated">✓ Sanert</span>
</button>
<span class="tfa-arrow tfa-arrow--mitigated" data-severity="medium" aria-hidden="true">
<span class="tfa-arrow__line"></span>
<span class="tfa-arrow__shield" title="Sanitering bryter kjeden">🛡</span>
</span>
<button type="button" class="tfa-leg" data-severity="high">
<span class="tfa-leg__label">Access</span>
<span class="tfa-leg__name">Read-only DB query</span>
<span class="tfa-leg__source">SELECT-only role</span>
<span class="tfa-leg__status" data-mit="partially_mitigated">◐ RBAC aktiv</span>
</button>
<span class="tfa-arrow" data-severity="high" aria-hidden="true"><span class="tfa-arrow__line"></span></span>
<button type="button" class="tfa-leg" data-severity="high">
<span class="tfa-leg__label">Exfil</span>
<span class="tfa-leg__name">Logged endpoint</span>
<span class="tfa-leg__source">api.bærum.no/log</span>
<span class="tfa-leg__status" data-mit="mitigated">✓ Audit-sporet</span>
</button>
</div>
<h2 style="font-size: var(--font-size-lg); margin: var(--space-8) 0 var(--space-3);">TFA-2026-118-003 — ALLOW</h2>
<div class="tfa-flow">
<span class="tfa-flow__verdict" data-verdict="ALLOW">ALLOW</span>
<button type="button" class="tfa-leg" data-severity="medium">
<span class="tfa-leg__label">Input</span>
<span class="tfa-leg__name">Konfig-fil</span>
<span class="tfa-leg__source">checked-in config.toml</span>
<span class="tfa-leg__status" data-mit="mitigated">✓ Signert</span>
</button>
<span class="tfa-arrow tfa-arrow--mitigated" data-severity="medium" aria-hidden="true">
<span class="tfa-arrow__line"></span>
<span class="tfa-arrow__shield">🛡</span>
</span>
<button type="button" class="tfa-leg" data-severity="medium">
<span class="tfa-leg__label">Access</span>
<span class="tfa-leg__name">Local file read</span>
<span class="tfa-leg__source">repo-scope</span>
<span class="tfa-leg__status" data-mit="mitigated">✓ Sandboxed</span>
</button>
<span class="tfa-arrow tfa-arrow--mitigated" data-severity="medium" aria-hidden="true">
<span class="tfa-arrow__line"></span>
<span class="tfa-arrow__shield">🛡</span>
</span>
<button type="button" class="tfa-leg" data-severity="medium">
<span class="tfa-leg__label">Exfil</span>
<span class="tfa-leg__name">Stdout</span>
<span class="tfa-leg__source">terminal</span>
<span class="tfa-leg__status" data-mit="mitigated">✓ Lokalt</span>
</button>
</div>
</main>
</body>
</html>

View file

@ -0,0 +1,74 @@
<!doctype html>
<html lang="nb">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Suppressed-Signals · Tier 3 supp</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="stylesheet" href="../../playground-design-system/components-tier3-supplement.css" />
<link rel="stylesheet" href="../../playground-design-system/fonts.css" />
</head>
<body>
<header class="app-header">
<a href="../index.html" class="app-header__brand"><span class="app-header__brand-mark">P</span><span>Playground</span></a>
<span class="app-header__breadcrumb">/ Komponenter / Suppressed-Signals Panel</span>
</header>
<main class="container container--wide" style="padding: var(--space-8) 0;">
<div style="margin-bottom: var(--space-6);">
<span style="font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--color-scope-security); font-weight: var(--font-weight-semibold);">llm-security · ultraplan-local</span>
<h1 style="margin: 4px 0 6px;">Suppressed-Signals Panel</h1>
<p class="text-secondary" style="max-width: 65ch;">Synlig — men sammenklappet — liste over funn som ble nedgradert eller fjernet, og hvorfor. Aldri skjult i en meny: tillit krever transparens.</p>
</div>
<p class="text-tertiary" style="font-size: var(--font-size-sm); margin: 0 0 8px;">Etter funn-listen, før footer:</p>
<section class="suppressed" aria-expanded="false" id="sup">
<button type="button" class="suppressed__head" onclick="toggleSup()">
<span class="suppressed__chev" aria-hidden="true"></span>
<span class="suppressed__label">12 signaler ble dempet eller fjernet i denne kjøringen</span>
<span class="suppressed__count">12</span>
</button>
<div class="suppressed__body">
<div class="suppressed-group">
<div class="suppressed-group__head">
<span class="suppressed-group__reason">false_positive_glsl_keywords</span>
<span class="suppressed-group__count">8 funn</span>
</div>
<p class="suppressed-group__desc">Entropy-scanner flagget GLSL shader-keywords som secrets. Nedgradert til info etter regel-treff på <code>shaders/*.glsl</code>.</p>
<div class="suppressed-group__examples">
<span class="suppressed-group__example">uniform vec3</span>
<span class="suppressed-group__example">varying mat4</span>
<span class="suppressed-group__example">gl_FragCoord</span>
</div>
</div>
<div class="suppressed-group">
<div class="suppressed-group__head">
<span class="suppressed-group__reason">test_fixture_intended</span>
<span class="suppressed-group__count">3 funn</span>
</div>
<p class="suppressed-group__desc">Fixture-filer i <code>tests/fixtures/</code> med bevisst dummy-data (bestått manuell review 2026-04-22).</p>
</div>
<div class="suppressed-group">
<div class="suppressed-group__head">
<span class="suppressed-group__reason">judge_succinctness_filter</span>
<span class="suppressed-group__count">1 funn</span>
</div>
<p class="suppressed-group__desc">Findings under 4 ord — ikke handlebar. Filtrert ut av Judge.</p>
</div>
</div>
</section>
</main>
<script>
function toggleSup() {
const s = document.getElementById('sup');
const open = s.getAttribute('aria-expanded') === 'true';
s.setAttribute('aria-expanded', open ? 'false' : 'true');
}
</script>
</body>
</html>

View file

@ -8,9 +8,7 @@
<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">
<link rel="stylesheet" href="../playground-design-system/fonts.css" />
<style>
.hero { padding: var(--space-16) 0 var(--space-12); border-bottom: 1px solid var(--color-border-subtle); background: var(--color-bg-soft); }
.hero__eyebrow { font-size: var(--font-size-xs); text-transform: uppercase; letter-spacing: 0.12em; color: var(--color-text-tertiary); font-weight: var(--font-weight-semibold); margin-bottom: var(--space-3); }

View file

@ -8,9 +8,7 @@
<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">
<link rel="stylesheet" href="../playground-design-system/fonts.css" />
<style>
.layout { display: grid; grid-template-rows: auto 1fr; min-height: 100vh; }
.page { padding: var(--space-8) 0 var(--space-16); }

View file

@ -7,9 +7,7 @@
<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="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&display=swap" rel="stylesheet">
<link rel="stylesheet" href="../playground-design-system/fonts.css" />
<style>
/* Page-specific layout */
.layout { display: grid; grid-template-rows: auto 1fr; min-height: 100vh; }

View file

@ -8,9 +8,7 @@
<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&display=swap" rel="stylesheet">
<link rel="stylesheet" href="../playground-design-system/fonts.css" />
<style>
.layout { display: grid; grid-template-rows: auto 1fr; min-height: 100vh; }
.page { padding: var(--space-6) 0 var(--space-16); }

View file

@ -9,9 +9,7 @@
<link rel="stylesheet" href="../playground-design-system/components.css" />
<link rel="stylesheet" href="../playground-design-system/components-tier2.css" />
<link rel="stylesheet" href="../playground-design-system/print.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&display=swap" rel="stylesheet">
<link rel="stylesheet" href="../playground-design-system/fonts.css" />
<style>
.page { padding: var(--space-8) 0 var(--space-16); }
.tpl-grid { display: grid; grid-template-columns: 240px 1fr; gap: var(--space-8); align-items: start; }
@ -166,8 +164,7 @@
&lt;link rel="stylesheet" href="../playground-design-system/components.css" /&gt;
&lt;link rel="stylesheet" href="../playground-design-system/components-tier2.css" /&gt;
&lt;link rel="stylesheet" href="../playground-design-system/print.css" /&gt;
&lt;link rel="preconnect" href="https://fonts.googleapis.com"&gt;
&lt;link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&amp;family=JetBrains+Mono:wght@400;500;600&amp;display=swap" rel="stylesheet"&gt;
&lt;link rel="stylesheet" href="../playground-design-system/fonts.css" /&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;header class="app-header"&gt;

View file

@ -9,7 +9,7 @@
<link rel="stylesheet" href="../playground-design-system/components.css">
<link rel="stylesheet" href="../playground-design-system/components-tier2.css">
<link rel="stylesheet" href="../playground-design-system/components-tier3.css">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="../playground-design-system/fonts.css">
<style>
.preview-section {
padding: var(--space-10) 0;