From 08d1e3cee3f4b70757ebb6a95a6185c7c92f638a Mon Sep 17 00:00:00 2001 From: davegilligan Date: Sat, 16 May 2026 13:22:24 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20auto-select=20STT=20engine=20(Azure=20?= =?UTF-8?q?=E2=86=92=20Google=20Cloud=20=E2=86=92=20Whisper)=20and=20show?= =?UTF-8?q?=20provider=20in=20results?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removes user-facing engine/model/key/beam controls. The server now picks the best available engine automatically: 1. Microsoft Azure Speech — short clips (≤1MB, no diarization, audio/*) 2. Google Cloud Speech v2 — long audio, diarization, all languages 3. OpenAI Whisper GPU — local fallback Results display which provider was used (e.g. "Transcribed with Google Cloud Speech") via transcript-engine-badge and traceMeta. Co-Authored-By: Claude Sonnet 4.6 --- Do Better Tools v1/Case Workbench.html | 28 + Do Better Tools v1/app.jsx | 101 +++ Do Better Tools v1/data.js | 161 +++++ Do Better Tools v1/design/colors_and_type.css | 262 ++++++++ Do Better Tools v1/design/distress.css | 605 +++++++++++++++++ Do Better Tools v1/design/inter.css | 6 + Do Better Tools v1/design/tools-shell.css | 204 ++++++ Do Better Tools v1/shell.jsx | 212 ++++++ Do Better Tools v1/vendor/tweaks-panel.jsx | 568 ++++++++++++++++ Do Better Tools v1/views.jsx | 621 ++++++++++++++++++ api/transcribe.php | 329 ++++------ assets/js/tools.js | 207 +----- bvj-sample/Trondheim1.pdf | Bin 0 -> 4372880 bytes transcribe.php | 49 +- 14 files changed, 2937 insertions(+), 416 deletions(-) create mode 100644 Do Better Tools v1/Case Workbench.html create mode 100644 Do Better Tools v1/app.jsx create mode 100644 Do Better Tools v1/data.js create mode 100644 Do Better Tools v1/design/colors_and_type.css create mode 100644 Do Better Tools v1/design/distress.css create mode 100644 Do Better Tools v1/design/inter.css create mode 100644 Do Better Tools v1/design/tools-shell.css create mode 100644 Do Better Tools v1/shell.jsx create mode 100644 Do Better Tools v1/vendor/tweaks-panel.jsx create mode 100644 Do Better Tools v1/views.jsx create mode 100644 bvj-sample/Trondheim1.pdf diff --git a/Do Better Tools v1/Case Workbench.html b/Do Better Tools v1/Case Workbench.html new file mode 100644 index 0000000..ed6d527 --- /dev/null +++ b/Do Better Tools v1/Case Workbench.html @@ -0,0 +1,28 @@ + + + + + + Do Better Norge · Legal Tools — Case Workbench + + + + + + + + + + + + + + +
+ + + + + + + diff --git a/Do Better Tools v1/app.jsx b/Do Better Tools v1/app.jsx new file mode 100644 index 0000000..ecafc07 --- /dev/null +++ b/Do Better Tools v1/app.jsx @@ -0,0 +1,101 @@ +/* global React, ReactDOM, + Topbar, Manifesto, Disclaimer, ToolRail, ReasoningPanel, FootStrip, TRACE_WAITING, + AskView, SearchView, AdvocateView, RedactView, TimelineView, CasebookView, VoicesView, + TweaksPanel, useTweaks, TweakSection, TweakSlider, TweakRadio, TweakToggle, TweakSelect +*/ +const { useState, useEffect, useMemo } = React; + +const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ + "distress": 0.6, + "headline": "twelve", + "surface": "light" +}/*EDITMODE-END*/; + +const TOOL_INSTRUMENT = { + ask: "ECHR Art. 8 · Barneloven §43", + search: "Lovdata + Strasbourg + UNCRC", + advocate: "ECHR Art. 8 (partisan)", + redact: "GDPR · Personopplysningsloven", + timeline: "Pedersen v. Norway pattern", + cases: "ECHR Art. 8 · 23 judgments", + voices: "Verified testimony · court documents on file", +}; + +function App() { + const [tool, setTool] = useState("ask"); + const [trace, setTrace] = useState(TRACE_WAITING); + const [t, setT] = useTweaks(TWEAK_DEFAULTS); + + // Apply visual layer to + useEffect(() => { + const root = document.documentElement; + root.style.setProperty("--distress", String(t.distress)); + root.classList.toggle("surface-dark", t.surface === "dark"); + }, [t.distress, t.surface]); + + const headline = window.DBN_DATA.headlines[t.headline] || window.DBN_DATA.headlines.twelve; + const stats = window.DBN_DATA.stats; + + // Reset trace when tool changes + useEffect(() => { setTrace(TRACE_WAITING); }, [tool]); + + const view = useMemo(() => { + switch (tool) { + case "ask": return ; + case "search": return ; + case "advocate": return ; + case "redact": return ; + case "timeline": return ; + case "cases": return ; + case "voices": return ; + default: return ; + } + }, [tool]); + + return ( +
+ + + + +
+ +
+ + {view} +
+ +
+ + + + + + setT("distress", v)} + help="0 = somber & restrained · 1 = defiant editorial layout (bigger headline, deeper bleed, larger fold)."/> + + + + setT("headline", v)} + options={[ + { value: "twelve", label: "Twelve minutes" }, + { value: "twentythree", label: "Twenty-three violations" }, + { value: "members", label: "Numbers · 9,482 / 20,000" }, + { value: "compliance", label: "Compliance is love" }, + ]}/> + + + + setT("surface", v)} + options={[ + { value: "light", label: "Light" }, + { value: "dark", label: "Dark" }, + ]}/> + + +
+ ); +} + +ReactDOM.createRoot(document.getElementById("root")).render(); diff --git a/Do Better Tools v1/data.js b/Do Better Tools v1/data.js new file mode 100644 index 0000000..3cdcf51 --- /dev/null +++ b/Do Better Tools v1/data.js @@ -0,0 +1,161 @@ +/* Real data sourced from dobetternorge.no + Used by the tool views to back every claim with a real citation. */ + +window.DBN_DATA = { + /* ── Header stats ──────────────────────────────────────────────── */ + stats: { + echrViolations: 23, + echrLossRate: 64, // % ECHR cases lost 2017-22 + tribunalDecisions: 1731, + pendingStrasbourg: 20, + members: 9482, + memberTarget: 20000, + targetDate: "Dec 2026", + }, + + /* ── ECHR cases against Norway ─────────────────────────────────── */ + cases: [ + { + no: "01", + name: "Strand Lobben & Others v. Norway", + year: 2019, + ruling: "Art. 8", + summary: "Adoption by foster parents authorised despite biological mother's objections. Contact limited to 4-6 hrs/yr.", + chamber: "Grand Chamber", + vote: "13–4", + }, + { + no: "02", + name: "Abdi Ibrahim v. Norway", + year: 2021, + ruling: "Art. 8 + 9", + summary: "Somali mother; authorities failed to consider religious upbringing before authorising adoption by Christian foster parents.", + chamber: "Grand Chamber", + vote: "Unanimous", + }, + { + no: "03", + name: "A.S. v. Norway", + year: 2019, + ruling: "Art. 8", + summary: "Long-term foster placement treated as permanent from outset; parenting assessment based on vague, subjective criteria.", + chamber: "Chamber", + vote: "Unanimous", + }, + { + no: "04", + name: "Pedersen & Others v. Norway", + year: 2020, + ruling: "Art. 8", + summary: "Contact between parents and child reduced to two short visits per year. Strict regime of visits cemented separation.", + chamber: "Chamber", + vote: "6–1", + }, + { + no: "05", + name: "Hernehult v. Norway", + year: 2020, + ruling: "Art. 8", + summary: "Emergency removal of three children. Insufficient evidentiary basis and inadequate reunification efforts.", + chamber: "Chamber", + vote: "Unanimous", + }, + { + no: "06", + name: "K.O. & V.M. v. Norway", + year: 2019, + ruling: "Art. 8", + summary: "Restrictions on contact after care order not supported by convincing reasons; authorities failed the reunification duty.", + chamber: "Chamber", + vote: "Unanimous", + }, + { + no: "07", + name: "Johansen v. Norway", + year: 1996, + ruling: "Art. 8", + summary: "Mutual enjoyment by parent and child of each other's company is a fundamental element of family life.", + chamber: "Chamber", + vote: "12–5", + }, + ], + + /* ── Contact-visit benchmarks (visits/year after care order) ───── */ + contactBenchmarks: [ + { jurisdiction: "Norway", visits: "4–6", note: "post-removal regime" }, + { jurisdiction: "Sweden", visits: "12", note: "comparable Nordic" }, + { jurisdiction: "Germany", visits: "24", note: "EU benchmark" }, + { jurisdiction: "Poland", visits: "26+", note: "EU benchmark" }, + { jurisdiction: "ECHR benchmark",visits: "~40", note: "Art. 8 jurisprudence" }, + ], + + /* ── Voices ───────────────────────────────────────────────────── */ + voices: [ + { + initials: "EK", + name: "Elena K.", + role: "Polish mother — case before Oslo tingrett", + quote: "The meeting lasted less than a quarter of an hour. I was told I could see my daughter four times a year, two hours each time. Three years later they said she no longer had a bond with me. I am still her mother.", + }, + { + initials: "TR", + name: "Tomasz R.", + role: "Verified member · 2023", + quote: "Fathers are treated as secondary caregivers by default. My lawyer warned me before the first meeting: Do not expect to be heard. She was right.", + }, + { + initials: "GT", + name: "Gro Hillestad Thune", + role: "Former ECHR judge (17 yrs)", + quote: "This is an extremely serious warning from Strasbourg to the Norwegian authorities. We are talking about a systemic failure, not individual mistakes.", + }, + { + initials: "SS", + name: "Stephanos Stavros", + role: "Human rights lawyer, Council of Europe", + quote: "It is much easier in Norway for child welfare to take children from their parents and cut any contact than to have a real reunification plan. The Court is deeply critical of this.", + }, + ], + + /* ── International instruments cited across the tools ──────────── */ + instruments: [ + { code: "ECHR Art. 8", full: "European Convention on Human Rights, Article 8 — Right to respect for private and family life" }, + { code: "ECHR Art. 9", full: "European Convention on Human Rights, Article 9 — Freedom of thought, conscience and religion" }, + { code: "ECHR Art. 6", full: "European Convention on Human Rights, Article 6 — Right to a fair trial" }, + { code: "ECHR Art. 14", full: "European Convention on Human Rights, Article 14 — Prohibition of discrimination" }, + { code: "UNCRC Art. 3", full: "UN Convention on the Rights of the Child, Article 3 — Best interests of the child" }, + { code: "UNCRC Art. 9", full: "UN Convention on the Rights of the Child, Article 9 — Separation from parents" }, + { code: "UNCRC Art. 12", full: "UN Convention on the Rights of the Child, Article 12 — Right of the child to be heard" }, + { code: "ICCPR Art. 23", full: "International Covenant on Civil & Political Rights, Article 23 — Protection of the family" }, + { code: "EU CFR Art. 24", full: "EU Charter of Fundamental Rights, Article 24 — Rights of the child" }, + { code: "EU CFR Art. 7", full: "EU Charter of Fundamental Rights, Article 7 — Respect for private and family life" }, + { code: "Barneloven §31", full: "Lov om barn og foreldre §31 — Child's right to be heard from age 7; great weight from age 12" }, + { code: "Barneloven §42", full: "Lov om barn og foreldre §42 — Samværsrett ved samlivsbrudd (contact rights)" }, + { code: "Barneloven §43", full: "Lov om barn og foreldre §43 — Avtale eller avgjerd om samvær (contact agreement)" }, + { code: "Barnevernloven §4-19", full: "Lov om barnevernstjenester §4-19 — Samvær etter omsorgsovertakelse" }, + ], + + /* ── Headline variants (used by Tweaks: headline_variant) ──────── */ + headlines: { + twelve: { + eyebrow: "Family rights · Norway · since 2019", + title: "They took her child in twelve minutes.", + sub: "Open a tool. Every answer is cited. Every claim cross-checked.", + }, + twentythree: { + eyebrow: "European Court of Human Rights · 2015 – present", + title: "Twenty-three violations. Every one says the same.", + sub: "Article 8. Family life. Norway, condemned in Strasbourg.", + }, + members: { + eyebrow: "9,482 of 20,000 · target Dec 2026", + title: "Numbers are how courts listen.", + sub: "Every member is one more reason the Storting cannot look away.", + }, + compliance: { + eyebrow: "Oslo Syndrome · investigative series", + title: "Compliance is love. Trust the process.", + sub: "This workbench asks what happens when the process becomes the story.", + }, + }, +}; diff --git a/Do Better Tools v1/design/colors_and_type.css b/Do Better Tools v1/design/colors_and_type.css new file mode 100644 index 0000000..9a03181 --- /dev/null +++ b/Do Better Tools v1/design/colors_and_type.css @@ -0,0 +1,262 @@ +/* ============================================================================= + Do Better Norge — AI Legal Tools + Color & Typography tokens + Source of truth: dobetternorge-tools/assets/css/tools.css :root + showcase + ============================================================================= */ + +:root { + /* ── Surface / structural ───────────────────────────────────────────── */ + --dbn-bg: #f7f8fb; /* app & page background */ + --dbn-panel: #ffffff; /* cards, rails, panels */ + --dbn-panel-tinted: #fbfcfe; /* nested cards, source rows, empty states */ + --dbn-line: #d8dde7; /* hairline borders, dividers */ + + /* ── Ink (foreground text) ──────────────────────────────────────────── */ + --dbn-ink: #1b2330; /* primary body / headings */ + --dbn-ink-soft: #314158; /* secondary headings, numeric chips */ + --dbn-muted: #667085; /* secondary copy, labels, helper text */ + --dbn-muted-2: #98a2b3; /* tertiary / waiting status */ + + /* ── Brand primary — Norway flag BLUE ───────────────────────────────── */ + /* Pantone 281 C. Primary action, links, eyebrow, active state. The depth + of the navy is intentional — it carries the "official / institutional" + register a legal-research product needs. */ + --dbn-teal: #00205B; /* PRIMARY · Norway flag blue */ + --dbn-teal-dark: #001A4D; /* hover, dark-on-light text */ + --dbn-teal-soft: #E6EAF2; /* tinted bg, active tab, pill bg */ + --dbn-teal-soft-2: #C9D2E5; /* hover tint of soft */ + --dbn-teal-ring: rgba(0, 32, 91, 0.35); /* focus outline */ + + /* ── Brand secondary — Norway flag RED (citations, partisan, CTA) ────── */ + /* Pantone 200 C. Reserved for citation chips, source numbers, the + showcase CTA, the Advocate "take a side" signal, and the running-trace + pulse. Never use red as a generic primary — that's flag-blue's job. */ + --dbn-coral: #BA0C2F; /* ACCENT · Norway flag red */ + --dbn-coral-dark: #8E0926; /* hover */ + --dbn-coral-soft: #FBE5E9; + --dbn-coral-edge: #E7B7C0; /* red hairline */ + + /* Convenience aliases — prefer these in new code, the *-teal/*-coral + names are kept only so existing components keep working. */ + --dbn-flag-blue-primary: var(--dbn-teal); + --dbn-flag-blue-dark: var(--dbn-teal-dark); + --dbn-flag-blue-soft: var(--dbn-teal-soft); + --dbn-flag-red: var(--dbn-coral); + --dbn-flag-red-dark: var(--dbn-coral-dark); + --dbn-flag-red-soft: var(--dbn-coral-soft); + --dbn-flag-white: #FFFFFF; + + /* ── Brand tertiary — Amber (notice, "running", disclaimers) ────────── */ + --dbn-amber: #b7791f; + --dbn-amber-soft: #fff9f2; + --dbn-amber-edge: #f0d6bc; + --dbn-amber-ink: #68420d; /* dark text on amber bg */ + + /* ── Semantic — Confidence / status palette (used in timeline, badges) */ + --dbn-success: #16a34a; + --dbn-success-soft: #dcfce7; + --dbn-success-ink: #15803d; + --dbn-warning: #d97706; + --dbn-warning-soft: #fef3c7; + --dbn-warning-ink: #b45309; + --dbn-danger: #b41e1e; + --dbn-danger-soft: #fee2e2; + --dbn-danger-ink: #991b1b; + --dbn-neutral-chip: #eef0f5; /* low confidence / muted badge bg */ + + /* ── Speaker / categorical palette (transcribe tool) ────────────────── */ + --dbn-cat-blue-bg: #dbeafe; --dbn-cat-blue-ink: #1d4ed8; + --dbn-cat-violet-bg: #ede9fe; --dbn-cat-violet-ink: #6d28d9; + --dbn-cat-green-bg: #dcfce7; --dbn-cat-green-ink: #166534; + --dbn-cat-yellow-bg: #fef9c3; --dbn-cat-yellow-ink: #854d0e; + --dbn-cat-red-bg: #fee2e2; --dbn-cat-red-ink: #991b1b; + + /* ── Parent-site bridge ─────────────────────────────────────────────── */ + /* Historically a separate token; now the primary brand colour is itself + flag-blue, so this alias just points at the primary. The SSO bridge + card on the gate uses the same blue with a 6% alpha bg. */ + --dbn-flag-blue: var(--dbn-teal); + + /* ── Elevation ──────────────────────────────────────────────────────── */ + --dbn-shadow-card: 0 18px 45px rgba(25, 35, 52, 0.10); + --dbn-shadow-soft: 0 4px 16px rgba(0, 32, 91, 0.10); + --dbn-shadow-modal: 0 28px 92px rgba(0, 0, 0, 0.34); + --dbn-overlay: rgba(23, 32, 51, 0.62); + + /* ── Radius ─────────────────────────────────────────────────────────── */ + --dbn-r-xs: 4px; /* date-type badge, small chip */ + --dbn-r-sm: 6px; /* dense controls, alias inputs */ + --dbn-r-md: 8px; /* default — buttons, panels, cards, inputs */ + --dbn-r-pill: 999px; /* status pill, tag, cite chip */ + + /* ── Spacing (Inter-friendly 4/6/8/10/12/14/16/18/24/32/48/64 rhythm) */ + --dbn-space-1: 4px; + --dbn-space-2: 8px; + --dbn-space-3: 12px; + --dbn-space-4: 16px; + --dbn-space-5: 18px; /* app-shell side padding */ + --dbn-space-6: 24px; + --dbn-space-8: 32px; + --dbn-space-10: 48px; + --dbn-space-12: 64px; + + /* ── Typography — family ────────────────────────────────────────────── */ + /* SUBSTITUTION NOTE: Tools site declares the system stack + "Inter, ui-sans-serif, system-ui, -apple-system, …". We pull Inter from + Google Fonts in fonts/inter.css so cards render identically out-of-org. */ + --dbn-font-sans: Inter, ui-sans-serif, system-ui, -apple-system, + BlinkMacSystemFont, "Segoe UI", sans-serif; + --dbn-font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, + "Liberation Mono", monospace; + + /* ── Typography — weight scale ──────────────────────────────────────── */ + --dbn-w-regular: 400; + --dbn-w-medium: 500; + --dbn-w-semibold: 600; + --dbn-w-bold: 700; + --dbn-w-heavy: 800; + --dbn-w-black: 900; + + /* ── Typography — size scale (rem on a 16px root) ───────────────────── */ + --dbn-fs-3xs: 0.66rem; /* trace marker number */ + --dbn-fs-2xs: 0.70rem; /* eyebrow on stat, table TH, stack TAG */ + --dbn-fs-xs: 0.78rem; /* status pill, badge, hint */ + --dbn-fs-sm: 0.84rem; /* table TD, table caption, queue item */ + --dbn-fs-body: 0.92rem; /* default body, disclaimer */ + --dbn-fs-md: 1.00rem; /* answer / brief copy */ + --dbn-fs-lg: 1.05rem; /* corpus section title */ + --dbn-fs-xl: 1.20rem; /* source modal title */ + --dbn-fs-2xl: 1.50rem; /* section-heading (h2 on showcase) */ + --dbn-fs-3xl: 2.00rem; /* stat value, hiw step number */ + --dbn-fs-display: clamp(2rem, 5vw, 3rem); /* showcase hero */ + + /* ── Typography — line-height ───────────────────────────────────────── */ + --dbn-lh-tight: 1.10; /* hero, big display */ + --dbn-lh-snug: 1.25; /* modal title */ + --dbn-lh-normal: 1.45; + --dbn-lh-relaxed: 1.55; /* body, gate copy */ + --dbn-lh-loose: 1.65; /* brief / answer */ + + /* ── Typography — letter-spacing ────────────────────────────────────── */ + --dbn-tracking-tight: -0.02em; /* big numbers (stat value) */ + --dbn-tracking-cap: 0.06em; /* eyebrow / cap labels */ + --dbn-tracking-cap-2: 0.04em; + --dbn-tracking-cap-sm: 0.03em; /* badge */ +} + +/* ============================================================================= + Base — use these on bare elements within the design system + ============================================================================= */ + +html { + font-family: var(--dbn-font-sans); + color: var(--dbn-ink); + background: var(--dbn-bg); + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; +} + +body { margin: 0; } + +h1, h2, h3, h4, p { margin-top: 0; } + +/* ============================================================================= + Semantic type tokens — apply via class (.dbn-eyebrow, .dbn-h1, etc.) + Mirrors the implicit type stack derived from tools.css. + ============================================================================= */ + +.dbn-eyebrow { + margin: 0 0 var(--dbn-space-1); + color: var(--dbn-teal); + font-size: var(--dbn-fs-xs); + font-weight: var(--dbn-w-bold); + text-transform: uppercase; + letter-spacing: var(--dbn-tracking-cap-2); +} + +.dbn-display { /* showcase hero h1 */ + font-size: var(--dbn-fs-display); + font-weight: var(--dbn-w-heavy); + line-height: var(--dbn-lh-tight); + color: #fff; + margin: 0; +} + +.dbn-h1 { /* topbar h1, "Legal Tools" */ + font-size: 1.5rem; + font-weight: var(--dbn-w-heavy); + line-height: 1.2; + color: var(--dbn-ink); +} + +.dbn-h2 { /* section heading, tool title */ + font-size: var(--dbn-fs-2xl); + font-weight: var(--dbn-w-heavy); + line-height: 1.2; + color: var(--dbn-ink); +} + +.dbn-h3 { /* card title, source title */ + font-size: var(--dbn-fs-md); + font-weight: var(--dbn-w-bold); + line-height: 1.35; + color: var(--dbn-ink); +} + +.dbn-h4 { /* category card / source card title */ + font-size: 0.88rem; + font-weight: var(--dbn-w-bold); + line-height: 1.3; + color: var(--dbn-ink); +} + +.dbn-body { /* default paragraph */ + font-size: var(--dbn-fs-body); + line-height: var(--dbn-lh-relaxed); + color: var(--dbn-ink); +} + +.dbn-body-soft { /* muted secondary copy */ + font-size: var(--dbn-fs-body); + line-height: var(--dbn-lh-relaxed); + color: var(--dbn-muted); +} + +.dbn-answer { /* result/brief body (longer reading) */ + font-size: var(--dbn-fs-md); + line-height: var(--dbn-lh-loose); + color: var(--dbn-ink); +} + +.dbn-small { + font-size: var(--dbn-fs-sm); + line-height: var(--dbn-lh-normal); + color: var(--dbn-muted); +} + +.dbn-cap { /* uppercase badge / table TH text */ + font-size: var(--dbn-fs-2xs); + font-weight: var(--dbn-w-bold); + text-transform: uppercase; + letter-spacing: var(--dbn-tracking-cap); + color: var(--dbn-muted); +} + +.dbn-code, +.dbn-mono { + font-family: var(--dbn-font-mono); + font-size: 0.86em; + background: var(--dbn-teal-soft); + color: var(--dbn-ink); + padding: 1px 5px; + border-radius: var(--dbn-r-xs); +} + +.dbn-num { /* big stat number */ + font-size: var(--dbn-fs-3xl); + font-weight: var(--dbn-w-heavy); + letter-spacing: var(--dbn-tracking-tight); + line-height: 1; + color: var(--dbn-teal); + font-variant-numeric: tabular-nums; +} diff --git a/Do Better Tools v1/design/distress.css b/Do Better Tools v1/design/distress.css new file mode 100644 index 0000000..e4a00d2 --- /dev/null +++ b/Do Better Tools v1/design/distress.css @@ -0,0 +1,605 @@ +/* ============================================================================ + distress.css — editorial overlay on top of the Do Better Norge design system + + The base design system stays clean and institutional. This file layers + the "Norway in distress" treatment: cracked-flag motif, half-mast tint, + redaction marks, dossier stamps, manifesto type, evidence-trail-as-protest. + + Distress intensity is driven by --distress (0..1) set on . + Surface mode is driven by .surface-dark on . + ============================================================================ */ + +:root { + /* Set by the Tweaks panel. */ + --distress: 0.6; /* 0 = somber, 1 = defiant */ + --paper: #f4ecd8; /* dossier paper tone (used on stamps) */ + --paper-2: #e9dfc5; + --stamp: #6e1421; /* deep red ink stamp */ + --ink-stamp: #1b1b1b; /* black ink stamp / typewriter */ + --bleed: var(--dbn-coral); /* red bleed accent */ + + /* Derived metrics */ + --bleed-h: calc(2px + var(--distress) * 6px); + --flag-fade: calc(0.35 + var(--distress) * 0.65); + + /* Mono / typewriter family for case-file marks */ + --type-mono: "JetBrains Mono", "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; +} + +/* ─── Surface mode: dark ────────────────────────────────────────────────── */ +html.surface-dark { + --dbn-bg: #0c1018; + --dbn-panel: #11161f; + --dbn-panel-tinted: #161c27; + --dbn-line: #2a313f; + --dbn-ink: #ecf0f6; + --dbn-ink-soft: #c4cad5; + --dbn-muted: #8a93a3; + --dbn-muted-2: #5b6271; + --dbn-teal-soft: rgba(78, 124, 220, 0.18); + --dbn-teal-soft-2: rgba(78, 124, 220, 0.28); + --dbn-coral-soft: rgba(186, 12, 47, 0.20); + --dbn-amber-soft: rgba(183, 121, 31, 0.14); + --dbn-amber-edge: rgba(183, 121, 31, 0.38); + --dbn-amber-ink: #e2b87a; + --paper: #1a1f2a; + --paper-2: #232938; +} +html.surface-dark .tool-tab { color: var(--dbn-ink); } +html.surface-dark .tool-tab.is-active { background: rgba(78,124,220,0.22); border-color: rgba(78,124,220,0.45); } +html.surface-dark .secondary-button { background: #1f2735; } +html.surface-dark .answer p strong, +html.surface-dark .source-title { color: var(--dbn-ink); } + +/* ─── Body — fine grain noise + paper tone (subtle, scaled by --distress) */ +body.dossier { + background-color: var(--dbn-bg); + background-image: + /* paper grain */ + radial-gradient(circle at 23% 47%, rgba(0,0,0,calc(0.03 * var(--distress))) 0 1px, transparent 1px), + radial-gradient(circle at 71% 19%, rgba(0,0,0,calc(0.025 * var(--distress))) 0 1px, transparent 1px), + radial-gradient(circle at 8% 81%, rgba(0,0,0,calc(0.02 * var(--distress))) 0 1px, transparent 1px), + radial-gradient(circle at 88% 71%, rgba(0,0,0,calc(0.03 * var(--distress))) 0 1px, transparent 1px); + background-size: 7px 7px, 13px 13px, 11px 11px, 17px 17px; +} +html.surface-dark body.dossier { + background-image: + radial-gradient(circle at 23% 47%, rgba(255,255,255,calc(0.025 * var(--distress))) 0 1px, transparent 1px), + radial-gradient(circle at 71% 19%, rgba(255,255,255,calc(0.02 * var(--distress))) 0 1px, transparent 1px); +} + +/* ─── App shell: top bleed bar + side numbering ─────────────────────────── */ +.app-shell { position: relative; } +.app-shell::before { + /* the bleed: red bar at top, mimicking the flag colour eroding */ + content: ""; + position: absolute; left: 0; right: 0; top: 0; + height: var(--bleed-h); + background: + linear-gradient(90deg, + var(--bleed) 0%, + var(--bleed) 38%, + #00205B 38%, + #00205B 41%, + #fff 41%, + #fff 49%, + #00205B 49%, + #00205B 52%, + var(--bleed) 52%, + var(--bleed) 100%); + opacity: calc(0.55 + var(--distress) * 0.45); +} + +/* ─── Manifesto strip (between disclaimer and workspace) ────────────────── */ +.manifesto { + max-width: 1500px; + margin: 0 auto 14px; + display: grid; + grid-template-columns: 1fr auto; + gap: 18px; + align-items: end; + padding: calc(14px + var(--distress) * 18px) 22px calc(14px + var(--distress) * 14px) 26px; + border: 1px solid var(--dbn-line); + border-left: 6px solid var(--dbn-coral); + border-radius: 8px; + background: var(--dbn-panel); + position: relative; + overflow: hidden; +} +.manifesto::before { + /* the half-mast flag — faded, desaturated, sliced */ + content: ""; + position: absolute; + right: -40px; top: -10px; bottom: -10px; width: 320px; + background: + linear-gradient(180deg, + transparent 0%, + transparent 32%, + var(--dbn-coral) 32%, + var(--dbn-coral) 38%, + #fff 38%, + #fff 42%, + #00205B 42%, + #00205B 50%, + #fff 50%, + #fff 54%, + var(--dbn-coral) 54%, + var(--dbn-coral) 60%, + transparent 60%); + opacity: calc(0.10 + var(--distress) * 0.35); + filter: grayscale(calc(0.6 - var(--distress) * 0.4)) saturate(calc(0.4 + var(--distress) * 0.6)); + transform: skewX(-8deg); + pointer-events: none; + mask-image: linear-gradient(90deg, transparent 0%, #000 35%, #000 100%); + -webkit-mask-image: linear-gradient(90deg, transparent 0%, #000 35%, #000 100%); +} +html.surface-dark .manifesto::before { opacity: calc(0.20 + var(--distress) * 0.40); } + +.manifesto-copy { position: relative; z-index: 2; max-width: 880px; } +.manifesto-eyebrow { + font-family: var(--type-mono); + font-size: 0.74rem; + font-weight: 600; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--dbn-coral); + margin: 0 0 8px; + display: flex; align-items: center; gap: 10px; +} +.manifesto-eyebrow::before { + content: ""; + display: inline-block; + width: 28px; height: 2px; background: var(--dbn-coral); +} +.manifesto-title { + margin: 0; + font-weight: 900; + letter-spacing: -0.025em; + line-height: 1.02; + color: var(--dbn-ink); + font-size: clamp(1.7rem, calc(2rem + var(--distress) * 1.6rem), 3.4rem); + text-wrap: balance; +} +.manifesto-sub { + margin: 12px 0 0; + color: var(--dbn-muted); + font-size: 1rem; + line-height: 1.5; + max-width: 56ch; +} +.manifesto-stats { + display: grid; + grid-template-columns: repeat(2, minmax(110px, auto)); + gap: 16px 28px; + position: relative; z-index: 2; + align-self: center; +} +.manifesto-stat { text-align: right; min-width: 110px; } +.manifesto-stat-num { + font-size: clamp(1.6rem, 2.2rem, 2.6rem); + font-weight: 900; + letter-spacing: -0.04em; + color: var(--dbn-coral); + font-variant-numeric: tabular-nums; + line-height: 1; +} +.manifesto-stat-num.is-blue { color: var(--dbn-teal); } +.manifesto-stat-label { + font-family: var(--type-mono); + font-size: 0.66rem; + font-weight: 600; + letter-spacing: 0.10em; + text-transform: uppercase; + color: var(--dbn-muted); + margin-top: 4px; + max-width: 22ch; + margin-left: auto; +} + +/* ─── Dossier stamp ─────────────────────────────────────────────────────── */ +.stamp { + display: inline-flex; align-items: center; gap: 7px; + font-family: var(--type-mono); + font-size: 0.66rem; + font-weight: 700; + letter-spacing: 0.12em; + text-transform: uppercase; + padding: 5px 9px 4px; + color: var(--stamp); + border: 1.5px solid var(--stamp); + border-radius: 4px; + background: transparent; + transform: rotate(-1.5deg); + opacity: calc(0.45 + var(--distress) * 0.55); +} +.stamp.is-ink { color: var(--ink-stamp); border-color: var(--ink-stamp); } +html.surface-dark .stamp { color: #e8b6b9; border-color: rgba(232,182,185,0.55); } +html.surface-dark .stamp.is-ink { color: #c9cfdb; border-color: rgba(201,207,219,0.45); } +.stamp::before { + content: ""; + width: 8px; height: 8px; border-radius: 999px; + border: 1.5px solid currentColor; +} + +/* ─── Topbar overrides ──────────────────────────────────────────────────── */ +.topbar { padding-top: calc(10px + var(--distress) * 4px); } +.topbar .case-no { + font-family: var(--type-mono); + font-size: 0.68rem; + letter-spacing: 0.16em; + text-transform: uppercase; + color: var(--dbn-muted); + margin-top: 6px; + display: inline-flex; align-items: center; gap: 10px; +} +.topbar .case-no .pulse { + width: 8px; height: 8px; border-radius: 999px; + background: var(--dbn-coral); + box-shadow: 0 0 0 0 rgba(186,12,47,0.6); + animation: caseno-pulse 1.6s ease-out infinite; +} +@keyframes caseno-pulse { + 0%,100% { box-shadow: 0 0 0 0 rgba(186,12,47,0.55); } + 60% { box-shadow: 0 0 0 9px rgba(186,12,47,0); } +} + +/* ─── Tool rail: stat severity ──────────────────────────────────────────── */ +.tool-rail { padding: 12px; gap: 6px; position: relative; } +.tool-rail::before { + content: "TOOLS · CASE WORK"; + display: block; + font-family: var(--type-mono); + font-size: 0.6rem; + letter-spacing: 0.16em; + color: var(--dbn-muted); + margin: 4px 6px 8px; +} +.tool-tab { + position: relative; + padding: 12px 12px 12px 14px; + border-left: 2px solid transparent !important; + transition: background .15s, border-color .15s, color .15s; +} +.tool-tab span { font-size: 0.95rem; } +.tool-tab small { font-family: var(--type-mono); font-size: 0.66rem; letter-spacing: 0.06em; text-transform: uppercase; } +.tool-tab .tag { + position: absolute; top: 8px; right: 8px; + font-family: var(--type-mono); font-size: 0.6rem; font-weight: 700; + color: var(--dbn-coral); letter-spacing: 0.06em; +} +.tool-tab.is-active { border-left-color: var(--dbn-coral) !important; } +.tool-tab.is-active::before { + content: "▶"; + position: absolute; left: -10px; top: 50%; transform: translateY(-50%); + color: var(--dbn-coral); font-size: 8px; +} + +/* ─── Tool panel header: tab title + dossier corner fold ────────────────── */ +.tool-panel { position: relative; padding: 20px 22px 22px; } +.tool-panel .corner-fold { + position: absolute; top: 0; right: 0; + width: calc(28px + var(--distress) * 22px); + height: calc(28px + var(--distress) * 22px); + background: linear-gradient(225deg, var(--paper) 50%, transparent 50%); + border-bottom-left-radius: 6px; + opacity: calc(0.35 + var(--distress) * 0.5); + pointer-events: none; +} +.tool-panel .corner-fold::after { + content: ""; + position: absolute; right: 0; top: 0; + width: 100%; height: 100%; + background: linear-gradient(225deg, transparent 49%, rgba(0,0,0,0.08) 50%, transparent 51%); +} +html.surface-dark .tool-panel .corner-fold { opacity: calc(0.18 + var(--distress) * 0.32); } + +/* ─── Reasoning panel: case-file aesthetic ──────────────────────────────── */ +.reasoning-panel { padding: 18px 18px 20px; position: relative; } +.reasoning-panel .file-label { + font-family: var(--type-mono); + font-size: 0.62rem; + letter-spacing: 0.16em; + text-transform: uppercase; + color: var(--dbn-muted); + margin: 0 0 4px; +} +.reasoning-panel .case-meta { + font-family: var(--type-mono); + font-size: 0.74rem; + color: var(--dbn-ink-soft); + border-top: 1px dashed var(--dbn-line); + margin-top: 18px; padding-top: 14px; + display: grid; grid-template-columns: 1fr 1fr; gap: 6px 12px; +} +.reasoning-panel .case-meta span:nth-child(odd) { color: var(--dbn-muted); } + +/* ─── Citation chip — richer evidence treatment ─────────────────────────── */ +.cite-chip { + background: transparent !important; + border: 1px solid var(--dbn-coral) !important; + color: var(--dbn-coral) !important; +} +.cite-chip:hover { background: var(--dbn-coral) !important; color: #fff !important; } + +/* ─── Redaction motif ───────────────────────────────────────────────────── */ +.redact { + background: #0a0c11; + color: transparent !important; + border-radius: 1px; + padding: 1px 4px; + margin: 0 1px; + text-shadow: none !important; + letter-spacing: 0.02em; + user-select: none; + cursor: help; + position: relative; + display: inline-block; + vertical-align: baseline; + transition: background .2s; +} +.redact::after { + /* faint typewriter strike-through */ + content: ""; + position: absolute; left: 2px; right: 2px; top: 50%; height: 0.6px; + background: rgba(255,255,255,0.06); +} +.redact:hover { background: #6e1421; } + +/* ─── Headline / numbers ────────────────────────────────────────────────── */ +.bignum { + font-weight: 900; + font-variant-numeric: tabular-nums; + letter-spacing: -0.03em; + line-height: 1; + color: var(--dbn-coral); +} + +/* ─── Article 8 chip (used across tools) ────────────────────────────────── */ +.art-chip { + display: inline-flex; align-items: center; gap: 6px; + background: var(--dbn-coral-soft); + color: var(--dbn-coral); + border: 1px solid var(--dbn-coral); + border-radius: 999px; + padding: 3px 10px; + font-family: var(--type-mono); + font-size: 0.7rem; + font-weight: 700; + letter-spacing: 0.04em; +} +.art-chip.is-blue { + background: var(--dbn-teal-soft); + color: var(--dbn-teal); + border-color: var(--dbn-teal); +} +.art-chip.is-amber { + background: var(--dbn-amber-soft); + color: var(--dbn-amber-ink); + border-color: var(--dbn-amber-edge); +} + +/* ─── Bench/Bar — visit-rights chart ────────────────────────────────────── */ +.bench-row { display: grid; grid-template-columns: 130px 1fr 50px; gap: 10px; align-items: center; padding: 6px 0; } +.bench-row + .bench-row { border-top: 1px dashed var(--dbn-line); } +.bench-row.is-no { color: var(--dbn-coral); font-weight: 800; } +.bench-name { font-family: var(--type-mono); font-size: 0.78rem; letter-spacing: 0.04em; text-transform: uppercase; } +.bench-bar { + position: relative; height: 18px; background: var(--dbn-panel-tinted); border: 1px solid var(--dbn-line); border-radius: 2px; +} +.bench-fill { + position: absolute; left: 0; top: 0; bottom: 0; + background: var(--dbn-teal); + width: var(--w, 20%); +} +.bench-row.is-no .bench-fill { background: var(--dbn-coral); } +.bench-num { text-align: right; font-family: var(--type-mono); font-weight: 800; font-size: 0.95rem; font-variant-numeric: tabular-nums; } + +/* ─── Advocate side-picker ──────────────────────────────────────────────── */ +.side-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; } +.side-tile { + border: 1px solid var(--dbn-line); + background: var(--dbn-panel-tinted); + border-radius: 8px; + padding: 12px; + text-align: left; + cursor: pointer; + transition: all .15s; +} +.side-tile:hover { border-color: var(--dbn-coral); } +.side-tile.is-on { border-color: var(--dbn-coral); background: var(--dbn-coral-soft); box-shadow: inset 0 0 0 1px var(--dbn-coral); } +.side-tile-glyph { font-family: var(--type-mono); font-size: 0.66rem; color: var(--dbn-muted); letter-spacing: 0.1em; } +.side-tile-name { font-weight: 800; margin: 4px 0 2px; font-size: 1rem; } +.side-tile-desc { font-size: 0.78rem; color: var(--dbn-muted); line-height: 1.4; } + +/* ─── Timeline strip ─────────────────────────────────────────────────────── */ +.tl { + list-style: none; padding: 0; margin: 0; + position: relative; +} +.tl::before { + content: ""; position: absolute; left: 9px; top: 6px; bottom: 6px; + width: 1px; background: var(--dbn-line); +} +.tl li { position: relative; padding: 8px 0 14px 28px; } +.tl-dot { + position: absolute; left: 4px; top: 12px; + width: 12px; height: 12px; border-radius: 999px; + background: var(--dbn-panel); border: 2px solid var(--dbn-coral); +} +.tl li.is-bad .tl-dot { background: var(--dbn-coral); } +.tl-time { font-family: var(--type-mono); font-size: 0.7rem; letter-spacing: 0.04em; color: var(--dbn-muted); } +.tl-event { font-weight: 700; color: var(--dbn-ink); margin: 2px 0; } +.tl-detail { color: var(--dbn-muted); font-size: 0.88rem; line-height: 1.45; } + +/* ─── Cases table ───────────────────────────────────────────────────────── */ +.cases-table { width: 100%; border-collapse: collapse; font-size: 0.88rem; } +.cases-table th { + text-align: left; padding: 8px 10px 8px 0; + font-family: var(--type-mono); font-size: 0.66rem; + letter-spacing: 0.10em; text-transform: uppercase; color: var(--dbn-muted); + border-bottom: 1px solid var(--dbn-line); +} +.cases-table td { padding: 10px 10px 10px 0; border-bottom: 1px dashed var(--dbn-line); vertical-align: top; } +.cases-table tr:hover td { background: var(--dbn-panel-tinted); } +.cases-table .c-no { font-family: var(--type-mono); font-weight: 800; color: var(--dbn-coral); width: 36px; } +.cases-table .c-name { font-weight: 800; color: var(--dbn-ink); } +.cases-table .c-year { color: var(--dbn-muted); font-variant-numeric: tabular-nums; width: 50px; } +.cases-table .c-art { + display: inline-block; + font-family: var(--type-mono); font-size: 0.7rem; font-weight: 800; + padding: 2px 7px; border-radius: 3px; + background: var(--dbn-coral-soft); color: var(--dbn-coral); +} + +/* ─── Voice card (Elena K. style quote) ─────────────────────────────────── */ +.voice { + border-left: 4px solid var(--dbn-coral); + padding: 12px 0 12px 16px; + margin: 0; +} +.voice q { + display: block; + font-size: clamp(0.98rem, calc(0.92rem + var(--distress) * 0.4rem), 1.32rem); + font-weight: 600; + line-height: 1.45; + color: var(--dbn-ink); + letter-spacing: -0.005em; + quotes: "“" "”" "‘" "’"; + text-wrap: pretty; +} +.voice q::before { content: open-quote; color: var(--dbn-coral); font-weight: 900; } +.voice q::after { content: close-quote; color: var(--dbn-coral); font-weight: 900; } +.voice-by { margin-top: 8px; font-size: 0.78rem; color: var(--dbn-muted); display: flex; gap: 10px; align-items: center; } +.voice-initials { + width: 28px; height: 28px; border-radius: 999px; + background: var(--dbn-coral-soft); color: var(--dbn-coral); + display: inline-flex; align-items: center; justify-content: center; + font-family: var(--type-mono); font-weight: 800; font-size: 0.7rem; + border: 1px solid var(--dbn-coral); +} + +/* ─── Source card extra polish ───────────────────────────────────────────── */ +.source-card { background: var(--dbn-panel); } +.source-card.is-highlight { background: var(--dbn-coral-soft); } + +/* ─── Section heading (mini, inside tool panels) ─────────────────────────── */ +.h-section { + font-family: var(--type-mono); + font-size: 0.7rem; + font-weight: 700; + letter-spacing: 0.16em; + text-transform: uppercase; + color: var(--dbn-muted); + margin: 18px 0 10px; + display: flex; align-items: center; gap: 10px; +} +.h-section::after { + content: ""; flex: 1; height: 1px; background: var(--dbn-line); +} + +/* ─── Hairline divider with mono caption ────────────────────────────────── */ +.hairline { + border: 0; border-top: 1px solid var(--dbn-line); margin: 18px 0; +} + +/* ─── Tweaks panel — pin to bottom-right ─────────────────────────────────── */ +.tweaks-panel-host { z-index: 1000; } + +/* ─── Animations: cracked-flag pulse (used in topbar mark) ──────────────── */ +@keyframes flag-half-mast { + 0%, 100% { transform: translateY(0); } + 50% { transform: translateY(2px); } +} + +/* ─── Footer strip ───────────────────────────────────────────────────────── */ +.foot-strip { + max-width: 1500px; + margin: 24px auto 12px; + padding: 12px 4px; + display: flex; justify-content: space-between; gap: 16px; + border-top: 1px dashed var(--dbn-line); + font-family: var(--type-mono); + font-size: 0.7rem; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--dbn-muted); +} + +/* ─── Source modal-lite (inline detail panel) ───────────────────────────── */ +.source-detail { + margin-top: 10px; padding: 14px 16px; + border: 1px solid var(--dbn-coral); + background: var(--dbn-coral-soft); + border-radius: 8px; + position: relative; +} +.source-detail .close { + position: absolute; top: 6px; right: 8px; + background: transparent; border: 0; color: var(--dbn-coral); cursor: pointer; + font-family: var(--type-mono); font-size: 0.9rem; +} +.source-detail .head { font-family: var(--type-mono); font-size: 0.7rem; letter-spacing: 0.1em; color: var(--dbn-coral); text-transform: uppercase; } +.source-detail blockquote { margin: 8px 0 0; font-size: 0.95rem; line-height: 1.55; color: var(--dbn-ink); border-left: 2px solid var(--dbn-coral); padding-left: 12px; } + +/* ─── Search input row used in Search & Ask ─────────────────────────────── */ +.search-row { display: grid; grid-template-columns: 1fr auto; gap: 10px; } +.search-input { + width: 100%; border: 1px solid var(--dbn-line); border-radius: 8px; + padding: 12px 14px; background: var(--dbn-panel); color: var(--dbn-ink); + font-size: 0.98rem; +} +.search-input:focus-visible { outline: 3px solid var(--dbn-teal-ring); outline-offset: 2px; } + +/* ─── Pre-set filter pills ──────────────────────────────────────────────── */ +.pill-row { display: flex; gap: 6px; flex-wrap: wrap; margin: 8px 0 4px; } +.pill-row .pill { + border: 1px solid var(--dbn-line); + background: var(--dbn-panel); + color: var(--dbn-muted); + border-radius: 999px; + padding: 5px 11px; + font-size: 0.78rem; + font-weight: 600; + cursor: pointer; + transition: all .12s; +} +.pill-row .pill:hover { border-color: var(--dbn-teal); color: var(--dbn-teal); } +.pill-row .pill.is-on { background: var(--dbn-teal); color: #fff; border-color: var(--dbn-teal); } + +/* ─── Redaction tool — case-file paper ──────────────────────────────────── */ +.redact-paper { + background: var(--paper); + color: #1a1a1a; + border: 1px solid var(--paper-2); + border-radius: 4px; + padding: 22px 26px; + font-family: "Times New Roman", Georgia, serif; + font-size: 0.96rem; + line-height: 1.7; + position: relative; + box-shadow: 0 1px 0 var(--paper-2); +} +.redact-paper::after { + content: "CONFIDENTIAL"; + position: absolute; top: 14px; right: 18px; + font-family: var(--type-mono); font-size: 0.7rem; + letter-spacing: 0.16em; color: rgba(110,20,33,0.7); + border: 1.5px solid rgba(110,20,33,0.7); + padding: 4px 8px; border-radius: 3px; + transform: rotate(2deg); +} +html.surface-dark .redact-paper { color: #d8d0b6; } + +/* ─── Misc utility ──────────────────────────────────────────────────────── */ +.mono { font-family: var(--type-mono); } +.muted { color: var(--dbn-muted); } +.danger-ink { color: var(--dbn-coral); } +.tinted-card { background: var(--dbn-panel-tinted); border: 1px solid var(--dbn-line); border-radius: 8px; padding: 14px 16px; } + +/* ─── Hide manifesto half-mast on narrow screens ────────────────────────── */ +@media (max-width: 860px) { + .manifesto { grid-template-columns: 1fr; } + .manifesto-stats { justify-content: flex-start; } + .manifesto::before { display: none; } + .manifesto-stat, .manifesto-stat-label { text-align: left; margin-left: 0; } +} diff --git a/Do Better Tools v1/design/inter.css b/Do Better Tools v1/design/inter.css new file mode 100644 index 0000000..078bd88 --- /dev/null +++ b/Do Better Tools v1/design/inter.css @@ -0,0 +1,6 @@ +/* Inter — pulled from Google Fonts. + The tools.css :root specifies "Inter, ui-sans-serif, system-ui, …". + No webfont @font-face is shipped with the codebase, so we substitute with + the official Google Fonts CDN copy at the standard weights used (400/500/ + 600/700/800/900). If you have an in-house woff2, swap this file. */ +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap'); diff --git a/Do Better Tools v1/design/tools-shell.css b/Do Better Tools v1/design/tools-shell.css new file mode 100644 index 0000000..c806f68 --- /dev/null +++ b/Do Better Tools v1/design/tools-shell.css @@ -0,0 +1,204 @@ +/* Tools workspace — local styles only what the UI kit needs. + Tokens come from /colors_and_type.css; this file translates them into the + class names the production CSS uses, so the markup matches the live site. */ + +* { box-sizing: border-box; } +body { + margin: 0; + min-height: 100vh; + background: var(--dbn-bg); + color: var(--dbn-ink); + font-family: var(--dbn-font-sans); + -webkit-font-smoothing: antialiased; +} +button, input, textarea { font: inherit; } +button { border: 0; cursor: pointer; } +button:disabled { cursor: progress; opacity: 0.7; } +button:focus-visible, +input:focus-visible, +textarea:focus-visible { + outline: 3px solid var(--dbn-teal-ring); + outline-offset: 2px; +} + +/* eyebrow / shared */ +.eyebrow { + margin: 0 0 6px; + color: var(--dbn-teal); + font-size: 0.78rem; + font-weight: 700; + text-transform: uppercase; +} +h1, h2, h3, h4, p { margin-top: 0; } + +/* ── App shell ─────────────────────────────────────────────────────────── */ +.app-shell { min-height: 100vh; padding: 18px; } +.topbar { + display: flex; align-items: center; justify-content: space-between; gap: 18px; + max-width: 1500px; margin: 0 auto 12px; padding: 10px 2px; +} +.topbar h1 { margin: 0; font-size: 1.5rem; font-weight: 800; color: var(--dbn-ink); line-height: 1.2; } +.topbar-actions { display: flex; align-items: center; gap: 10px; } +.status-pill, .tool-badge { + display: inline-flex; align-items: center; min-height: 30px; padding: 0 10px; + border-radius: 999px; background: var(--dbn-teal-soft); color: var(--dbn-teal-dark); + font-size: 0.84rem; font-weight: 700; white-space: nowrap; +} +.status-pill.is-warning { background: var(--dbn-coral-soft); color: var(--dbn-coral); } +.secondary-button { + min-height: 44px; border-radius: 8px; padding: 0 18px; + background: #263344; color: #fff; font-weight: 700; +} + +.disclaimer { + max-width: 1500px; margin: 0 auto 12px; padding: 10px 12px; + border: 1px solid var(--dbn-amber-edge); border-radius: 8px; + background: var(--dbn-amber-soft); color: var(--dbn-amber-ink); font-size: 0.92rem; +} + +.workspace { + max-width: 1500px; margin: 0 auto; + display: grid; grid-template-columns: 190px minmax(0, 1fr) 370px; + gap: 14px; align-items: stretch; +} + +.tool-rail, .tool-panel, .reasoning-panel { + background: var(--dbn-panel); border: 1px solid var(--dbn-line); border-radius: 8px; +} + +/* ── Tool rail ─────────────────────────────────────────────────────────── */ +.tool-rail { padding: 10px; display: flex; flex-direction: column; gap: 8px; } +.tool-tab { + width: 100%; min-height: 68px; border-radius: 8px; padding: 11px; + background: transparent; color: var(--dbn-ink); text-align: left; + border: 1px solid transparent; cursor: pointer; +} +.tool-tab span { display: block; font-weight: 800; } +.tool-tab small { display: block; margin-top: 4px; color: var(--dbn-muted); } +.tool-tab.is-active { background: var(--dbn-teal-soft); border-color: rgba(0, 32, 91,0.30); } +.tool-tab:hover:not(.is-active) { background: var(--dbn-panel-tinted); } + +/* ── Tool panel ─────────────────────────────────────────────────────────── */ +.tool-panel { min-height: 720px; padding: 18px; } +.tool-heading, .reasoning-head { + display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; + margin-bottom: 16px; +} +.tool-heading h2, .reasoning-head h2 { margin: 0; font-size: 1.5rem; font-weight: 800; color: var(--dbn-ink); } + +.tool-form { display: grid; gap: 12px; } +.control-row { + display: flex; align-items: center; flex-wrap: wrap; gap: 12px; min-height: 34px; +} +.control-row .control-label, +.control-row label { + display: inline-flex; align-items: center; gap: 6px; + color: var(--dbn-muted); font-weight: 700; font-size: 0.92rem; +} +.control-label { color: var(--dbn-ink) !important; } + +.tool-form textarea { + width: 100%; border: 1px solid var(--dbn-line); border-radius: 8px; + background: #fff; color: var(--dbn-ink); + resize: vertical; min-height: 220px; padding: 14px; line-height: 1.55; +} +.form-footer { display: flex; align-items: center; justify-content: space-between; gap: 12px; } +.form-status { min-height: 22px; margin: 0; color: var(--dbn-muted); font-size: 0.88rem; } + +#runButton { + min-height: 44px; border-radius: 8px; padding: 0 18px; + background: var(--dbn-teal); color: #fff; font-weight: 700; +} + +/* ── Results / sources ─────────────────────────────────────────────────── */ +.results { margin-top: 18px; display: grid; gap: 12px; } +.empty-state, .result-section { + border: 1px solid var(--dbn-line); border-radius: 8px; padding: 16px; background: var(--dbn-panel-tinted); +} +.empty-state h3, .result-section h3 { margin: 0 0 8px; font-size: 0.98rem; color: var(--dbn-ink); } +.empty-state p, .result-section p, .result-section li { color: var(--dbn-muted); line-height: 1.55; margin: 0; } +.answer { color: var(--dbn-ink) !important; font-size: 1.03rem; line-height: 1.65; } +.answer p { margin: 0 0 12px; } + +.source-list { display: grid; gap: 10px; } +.source-card { + display: grid; grid-template-columns: 34px 1fr auto; gap: 12px; align-items: start; + border: 1px solid var(--dbn-line); border-radius: 8px; padding: 12px; + background: var(--dbn-panel-tinted); cursor: pointer; text-align: left; width: 100%; +} +.source-card:hover { border-color: rgba(0, 32, 91,0.40); } +.source-card.is-highlight { border-color: var(--dbn-coral); background: var(--dbn-coral-soft); } +.source-number { + display: inline-flex; align-items: center; justify-content: center; + width: 28px; height: 28px; border-radius: 999px; + background: var(--dbn-coral-soft); color: var(--dbn-coral); + font-weight: 900; font-variant-numeric: tabular-nums; +} +.source-title { font-weight: 800; color: var(--dbn-ink); line-height: 1.35; } +.source-meta { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; } +.source-tag { + background: var(--dbn-teal-soft); color: var(--dbn-teal-dark); + border-radius: 999px; font-size: 0.7rem; font-weight: 800; padding: 3px 8px; + text-transform: uppercase; letter-spacing: 0.03em; +} +.source-tag--score { background: #eef3fb; color: var(--dbn-ink-soft); } +.source-excerpt { color: var(--dbn-muted); margin-top: 8px; line-height: 1.5; font-size: 0.92rem; } +.source-aside { + align-self: stretch; display: grid; grid-template-rows: auto auto; gap: 6px; + font-size: 0.78rem; color: var(--dbn-muted); text-align: right; min-width: 90px; +} +.source-aside b { color: var(--dbn-ink); font-variant-numeric: tabular-nums; font-size: 0.92rem; } + +.cite-chip { + display: inline-flex; align-items: center; justify-content: center; + min-width: 18px; height: 18px; margin: 0 1px; padding: 0 5px; + border-radius: 999px; background: var(--dbn-coral-soft); color: var(--dbn-coral); + font-size: 0.72rem; font-weight: 800; font-variant-numeric: tabular-nums; + cursor: pointer; border: 1px solid rgba(186, 12, 47,0.25); vertical-align: 1px; +} +.cite-chip:hover { background: var(--dbn-coral); color: #fff; } + +/* ── Reasoning panel ───────────────────────────────────────────────────── */ +.reasoning-panel { min-height: 720px; padding: 16px; } +.trace-list { list-style: none; padding: 0; margin: 0; display: grid; gap: 12px; } +.trace-list li { display: grid; grid-template-columns: 14px 1fr; gap: 10px; } +.trace-list strong { display: block; margin-bottom: 3px; color: var(--dbn-ink); } +.trace-list p { margin: 0; color: var(--dbn-muted); line-height: 1.45; font-size: 0.88rem; } +.trace-status { width: 10px; height: 10px; margin-top: 5px; border-radius: 999px; background: var(--dbn-teal); } +.trace-status.running { background: var(--dbn-amber); animation: trace-pulse 1.4s ease-in-out infinite; } +.trace-status.warning { background: var(--dbn-coral); } +.trace-status.waiting { background: var(--dbn-muted-2); } +@keyframes trace-pulse { + 0%, 100% { opacity: 1; transform: scale(1); } + 50% { opacity: 0.4; transform: scale(0.7); } +} + +/* ── Upload zone ───────────────────────────────────────────────────────── */ +.upload-zone { + border: 2px dashed var(--dbn-line); border-radius: 8px; padding: 18px 14px; + text-align: center; cursor: pointer; transition: border-color .15s, background .15s; +} +.upload-zone:hover { border-color: var(--dbn-teal); background: #f7fdfb; } +.upload-icon { display: block; font-size: 1.8rem; line-height: 1; color: var(--dbn-teal); opacity: 0.55; margin-bottom: 6px; } +.upload-prompt p { margin: 4px 0 0; color: var(--dbn-muted); font-size: 0.88rem; } +.upload-browse { color: var(--dbn-teal); font-weight: 700; cursor: pointer; text-decoration: underline; text-underline-offset: 2px; } +.upload-hint { font-size: 0.76rem !important; opacity: 0.7; } + +/* ── Lang switcher / pills ─────────────────────────────────────────────── */ +.lang-switcher { display: flex; align-items: center; gap: 6px; } +.lang-btn { + background: var(--dbn-bg); border: 1px solid var(--dbn-line); border-radius: 999px; + color: var(--dbn-muted); cursor: pointer; font-size: 0.7rem; font-weight: 600; + letter-spacing: 0.03em; padding: 4px 10px; transition: all .15s; +} +.lang-btn.is-active { background: var(--dbn-teal); border-color: var(--dbn-teal); color: #fff; } + +@media (max-width: 1120px) { + .workspace { grid-template-columns: 170px minmax(0,1fr); } + .reasoning-panel { grid-column: 1 / -1; min-height: auto; } +} +@media (max-width: 760px) { + .workspace { grid-template-columns: 1fr; } + .tool-rail { flex-direction: row; overflow-x: auto; } + .tool-tab { min-width: 150px; } +} diff --git a/Do Better Tools v1/shell.jsx b/Do Better Tools v1/shell.jsx new file mode 100644 index 0000000..dc60602 --- /dev/null +++ b/Do Better Tools v1/shell.jsx @@ -0,0 +1,212 @@ +/* global React */ +const { useState, useEffect, useRef, useMemo } = React; + +/* ─── Stamp ─────────────────────────────────────────────────────── */ +function Stamp({ children, ink }) { + return {children}; +} + +/* ─── Art-chip (instrument badge) ───────────────────────────────── */ +function ArtChip({ code, tone = "red", title }) { + const cls = "art-chip" + (tone === "blue" ? " is-blue" : tone === "amber" ? " is-amber" : ""); + return {code}; +} + +/* ─── Redact span ───────────────────────────────────────────────── */ +function Redact({ children, len }) { + // render fixed-width blocks if `len` is passed; otherwise show children styled as bar + const text = len ? "█".repeat(len) : (children || "████"); + return {text}; +} + +/* ─── Cite chip (richer) ─────────────────────────────────────────── */ +function CiteChip({ n, active, onClick }) { + return ( + + {n} + + ); +} + +/* ─── Topbar with case-number ───────────────────────────────────── */ +function Topbar({ caseNo, status }) { + return ( +
+
+

Do Better Norge · tools.dobetternorge.no

+

Legal Tools · Case Workbench

+
+ + {caseNo} + · + session in memory · nothing stored +
+
+
+ ECHR Art. 8 monitoring + {status} + +
+
+ ); +} + +/* ─── Manifesto strip — replaces vanilla disclaimer with a headline + stats */ +function Manifesto({ headline, stats, intensity }) { + return ( +
+
+

{headline.eyebrow}

+

{headline.title}

+

{headline.sub}

+
+
+
+
{stats.echrViolations}
+
ECHR violations since 2015
+
+
+
{stats.echrLossRate}%
+
cases lost 2017–22
+
+
+
{stats.tribunalDecisions.toLocaleString()}
+
tribunal decisions analysed
+
+
+
{stats.pendingStrasbourg}+
+
pending Strasbourg
+
+
+
+ ); +} + +/* ─── Disclaimer (kept, but reskinned amber) ────────────────────── */ +function Disclaimer() { + return ( +
+ Notice.  + Legal information and preparation support, not final legal advice. Pasted text is processed in memory by default + under ECHR Art. 8 and Norwegian Personopplysningsloven principles. +
+ ); +} + +/* ─── Tool rail (themed) ────────────────────────────────────────── */ +const TOOLS = [ + { slug: "ask", label: "Ask", sub: "source-grounded", tag: "Art. 8" }, + { slug: "search", label: "Search", sub: "lovdata + echr", tag: "1.7K" }, + { slug: "advocate", label: "Advocate", sub: "take a side", tag: "★" }, + { slug: "redact", label: "Redact", sub: "in-memory only", tag: "GDPR" }, + { slug: "timeline", label: "Timeline", sub: "events extracted", tag: "12 min" }, + { slug: "cases", label: "Casebook", sub: "23 violations", tag: "23" }, + { slug: "voices", label: "Voices", sub: "verified accounts", tag: "4" }, +]; + +function ToolRail({ active, onSelect }) { + return ( + + ); +} + +/* ─── Reasoning panel (Evidence Trail) ──────────────────────────── */ +const TRACE_WAITING = [ + { label: "Waiting for query", detail: "Open a tool on the left. The evidence trail is built as the answer is generated.", status: "waiting" }, +]; + +function TraceList({ steps }) { + return ( +
    + {steps.map((s, i) => ( +
  1. + +
    + {s.label} +

    {s.detail}

    +
    +
  2. + ))} +
+ ); +} + +function ReasoningPanel({ steps, caseNo, instrument }) { + return ( + + ); +} + +/* ─── Footer strip ──────────────────────────────────────────────── */ +function FootStrip() { + return ( +
+ Do Better Norge · independent advocacy · founded by affected parents + Powered by CaveauAI · Blue Note Logic · in-memory RAG · no telemetry +
+ ); +} + +/* ─── Run helper (timed promise sequence for fake tool runs) ────── */ +function useRunSequence() { + const timers = useRef([]); + useEffect(() => () => timers.current.forEach(clearTimeout), []); + function clear() { timers.current.forEach(clearTimeout); timers.current = []; } + function run(sequence, onStep, onDone) { + clear(); + let cum = 0; + sequence.forEach((entry, idx) => { + cum += entry.delay; + const t = setTimeout(() => { + onStep(entry.step, idx, idx === sequence.length - 1); + if (idx === sequence.length - 1) onDone && onDone(); + }, cum); + timers.current.push(t); + }); + } + return { run, clear }; +} + +/* ─── Headline labeller / instrument helper ─────────────────────── */ +function lookupInstrument(code) { + const m = (window.DBN_DATA.instruments || []).find(i => i.code === code); + return m ? m.full : code; +} + +/* ─── Expose ────────────────────────────────────────────────────── */ +Object.assign(window, { + Stamp, ArtChip, Redact, CiteChip, + Topbar, Manifesto, Disclaimer, ToolRail, TOOLS, + ReasoningPanel, TraceList, TRACE_WAITING, + FootStrip, useRunSequence, lookupInstrument, +}); diff --git a/Do Better Tools v1/vendor/tweaks-panel.jsx b/Do Better Tools v1/vendor/tweaks-panel.jsx new file mode 100644 index 0000000..79ccfe9 --- /dev/null +++ b/Do Better Tools v1/vendor/tweaks-panel.jsx @@ -0,0 +1,568 @@ + +// tweaks-panel.jsx +// Reusable Tweaks shell + form-control helpers. +// +// Owns the host protocol (listens for __activate_edit_mode / __deactivate_edit_mode, +// posts __edit_mode_available / __edit_mode_set_keys / __edit_mode_dismissed) so +// individual prototypes don't re-roll it. Ships a consistent set of controls so you +// don't hand-draw , segmented radios, steppers, etc. +// +// Usage (in an HTML file that loads React + Babel): +// +// const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ +// "primaryColor": "#D97757", +// "palette": ["#D97757", "#29261b", "#f6f4ef"], +// "fontSize": 16, +// "density": "regular", +// "dark": false +// }/*EDITMODE-END*/; +// +// function App() { +// const [t, setTweak] = useTweaks(TWEAK_DEFAULTS); +// return ( +//
+// Hello +// +// +// setTweak('fontSize', v)} /> +// setTweak('density', v)} /> +// +// setTweak('primaryColor', v)} /> +// setTweak('palette', v)} /> +// setTweak('dark', v)} /> +// +//
+// ); +// } +// +// ───────────────────────────────────────────────────────────────────────────── + +const __TWEAKS_STYLE = ` + .twk-panel{position:fixed;right:16px;bottom:16px;z-index:2147483646;width:280px; + max-height:calc(100vh - 32px);display:flex;flex-direction:column; + transform:scale(var(--dc-inv-zoom,1));transform-origin:bottom right; + background:rgba(250,249,247,.78);color:#29261b; + -webkit-backdrop-filter:blur(24px) saturate(160%);backdrop-filter:blur(24px) saturate(160%); + border:.5px solid rgba(255,255,255,.6);border-radius:14px; + box-shadow:0 1px 0 rgba(255,255,255,.5) inset,0 12px 40px rgba(0,0,0,.18); + font:11.5px/1.4 ui-sans-serif,system-ui,-apple-system,sans-serif;overflow:hidden} + .twk-hd{display:flex;align-items:center;justify-content:space-between; + padding:10px 8px 10px 14px;cursor:move;user-select:none} + .twk-hd b{font-size:12px;font-weight:600;letter-spacing:.01em} + .twk-x{appearance:none;border:0;background:transparent;color:rgba(41,38,27,.55); + width:22px;height:22px;border-radius:6px;cursor:default;font-size:13px;line-height:1} + .twk-x:hover{background:rgba(0,0,0,.06);color:#29261b} + .twk-body{padding:2px 14px 14px;display:flex;flex-direction:column;gap:10px; + overflow-y:auto;overflow-x:hidden;min-height:0; + scrollbar-width:thin;scrollbar-color:rgba(0,0,0,.15) transparent} + .twk-body::-webkit-scrollbar{width:8px} + .twk-body::-webkit-scrollbar-track{background:transparent;margin:2px} + .twk-body::-webkit-scrollbar-thumb{background:rgba(0,0,0,.15);border-radius:4px; + border:2px solid transparent;background-clip:content-box} + .twk-body::-webkit-scrollbar-thumb:hover{background:rgba(0,0,0,.25); + border:2px solid transparent;background-clip:content-box} + .twk-row{display:flex;flex-direction:column;gap:5px} + .twk-row-h{flex-direction:row;align-items:center;justify-content:space-between;gap:10px} + .twk-lbl{display:flex;justify-content:space-between;align-items:baseline; + color:rgba(41,38,27,.72)} + .twk-lbl>span:first-child{font-weight:500} + .twk-val{color:rgba(41,38,27,.5);font-variant-numeric:tabular-nums} + + .twk-sect{font-size:10px;font-weight:600;letter-spacing:.06em;text-transform:uppercase; + color:rgba(41,38,27,.45);padding:10px 0 0} + .twk-sect:first-child{padding-top:0} + + .twk-field{appearance:none;box-sizing:border-box;width:100%;min-width:0;height:26px;padding:0 8px; + border:.5px solid rgba(0,0,0,.1);border-radius:7px; + background:rgba(255,255,255,.6);color:inherit;font:inherit;outline:none} + .twk-field:focus{border-color:rgba(0,0,0,.25);background:rgba(255,255,255,.85)} + select.twk-field{padding-right:22px; + background-image:url("data:image/svg+xml;utf8,"); + background-repeat:no-repeat;background-position:right 8px center} + + .twk-slider{appearance:none;-webkit-appearance:none;width:100%;height:4px;margin:6px 0; + border-radius:999px;background:rgba(0,0,0,.12);outline:none} + .twk-slider::-webkit-slider-thumb{-webkit-appearance:none;appearance:none; + width:14px;height:14px;border-radius:50%;background:#fff; + border:.5px solid rgba(0,0,0,.12);box-shadow:0 1px 3px rgba(0,0,0,.2);cursor:default} + .twk-slider::-moz-range-thumb{width:14px;height:14px;border-radius:50%; + background:#fff;border:.5px solid rgba(0,0,0,.12);box-shadow:0 1px 3px rgba(0,0,0,.2);cursor:default} + + .twk-seg{position:relative;display:flex;padding:2px;border-radius:8px; + background:rgba(0,0,0,.06);user-select:none} + .twk-seg-thumb{position:absolute;top:2px;bottom:2px;border-radius:6px; + background:rgba(255,255,255,.9);box-shadow:0 1px 2px rgba(0,0,0,.12); + transition:left .15s cubic-bezier(.3,.7,.4,1),width .15s} + .twk-seg.dragging .twk-seg-thumb{transition:none} + .twk-seg button{appearance:none;position:relative;z-index:1;flex:1;border:0; + background:transparent;color:inherit;font:inherit;font-weight:500;min-height:22px; + border-radius:6px;cursor:default;padding:4px 6px;line-height:1.2; + overflow-wrap:anywhere} + + .twk-toggle{position:relative;width:32px;height:18px;border:0;border-radius:999px; + background:rgba(0,0,0,.15);transition:background .15s;cursor:default;padding:0} + .twk-toggle[data-on="1"]{background:#34c759} + .twk-toggle i{position:absolute;top:2px;left:2px;width:14px;height:14px;border-radius:50%; + background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.25);transition:transform .15s} + .twk-toggle[data-on="1"] i{transform:translateX(14px)} + + .twk-num{display:flex;align-items:center;box-sizing:border-box;min-width:0;height:26px;padding:0 0 0 8px; + border:.5px solid rgba(0,0,0,.1);border-radius:7px;background:rgba(255,255,255,.6)} + .twk-num-lbl{font-weight:500;color:rgba(41,38,27,.6);cursor:ew-resize; + user-select:none;padding-right:8px} + .twk-num input{flex:1;min-width:0;height:100%;border:0;background:transparent; + font:inherit;font-variant-numeric:tabular-nums;text-align:right;padding:0 8px 0 0; + outline:none;color:inherit;-moz-appearance:textfield} + .twk-num input::-webkit-inner-spin-button,.twk-num input::-webkit-outer-spin-button{ + -webkit-appearance:none;margin:0} + .twk-num-unit{padding-right:8px;color:rgba(41,38,27,.45)} + + .twk-btn{appearance:none;height:26px;padding:0 12px;border:0;border-radius:7px; + background:rgba(0,0,0,.78);color:#fff;font:inherit;font-weight:500;cursor:default} + .twk-btn:hover{background:rgba(0,0,0,.88)} + .twk-btn.secondary{background:rgba(0,0,0,.06);color:inherit} + .twk-btn.secondary:hover{background:rgba(0,0,0,.1)} + + .twk-swatch{appearance:none;-webkit-appearance:none;width:56px;height:22px; + border:.5px solid rgba(0,0,0,.1);border-radius:6px;padding:0;cursor:default; + background:transparent;flex-shrink:0} + .twk-swatch::-webkit-color-swatch-wrapper{padding:0} + .twk-swatch::-webkit-color-swatch{border:0;border-radius:5.5px} + .twk-swatch::-moz-color-swatch{border:0;border-radius:5.5px} + + .twk-chips{display:flex;gap:6px} + .twk-chip{position:relative;appearance:none;flex:1;min-width:0;height:46px; + padding:0;border:0;border-radius:6px;overflow:hidden;cursor:default; + box-shadow:0 0 0 .5px rgba(0,0,0,.12),0 1px 2px rgba(0,0,0,.06); + transition:transform .12s cubic-bezier(.3,.7,.4,1),box-shadow .12s} + .twk-chip:hover{transform:translateY(-1px); + box-shadow:0 0 0 .5px rgba(0,0,0,.18),0 4px 10px rgba(0,0,0,.12)} + .twk-chip[data-on="1"]{box-shadow:0 0 0 1.5px rgba(0,0,0,.85), + 0 2px 6px rgba(0,0,0,.15)} + .twk-chip>span{position:absolute;top:0;bottom:0;right:0;width:34%; + display:flex;flex-direction:column;box-shadow:-1px 0 0 rgba(0,0,0,.1)} + .twk-chip>span>i{flex:1;box-shadow:0 -1px 0 rgba(0,0,0,.1)} + .twk-chip>span>i:first-child{box-shadow:none} + .twk-chip svg{position:absolute;top:6px;left:6px;width:13px;height:13px; + filter:drop-shadow(0 1px 1px rgba(0,0,0,.3))} +`; + +// ── useTweaks ─────────────────────────────────────────────────────────────── +// Single source of truth for tweak values. setTweak persists via the host +// (__edit_mode_set_keys → host rewrites the EDITMODE block on disk). +function useTweaks(defaults) { + const [values, setValues] = React.useState(defaults); + // Accepts either setTweak('key', value) or setTweak({ key: value, ... }) so a + // useState-style call doesn't write a "[object Object]" key into the persisted + // JSON block. + const setTweak = React.useCallback((keyOrEdits, val) => { + const edits = typeof keyOrEdits === 'object' && keyOrEdits !== null + ? keyOrEdits : { [keyOrEdits]: val }; + setValues((prev) => ({ ...prev, ...edits })); + window.parent.postMessage({ type: '__edit_mode_set_keys', edits }, '*'); + // Same-window signal so in-page listeners (deck-stage rail thumbnails) + // can react — the parent message only reaches the host, not peers. + window.dispatchEvent(new CustomEvent('tweakchange', { detail: edits })); + }, []); + return [values, setTweak]; +} + +// ── TweaksPanel ───────────────────────────────────────────────────────────── +// Floating shell. Registers the protocol listener BEFORE announcing +// availability — if the announce ran first, the host's activate could land +// before our handler exists and the toolbar toggle would silently no-op. +// The close button posts __edit_mode_dismissed so the host's toolbar toggle +// flips off in lockstep; the host echoes __deactivate_edit_mode back which +// is what actually hides the panel. +function TweaksPanel({ title = 'Tweaks', noDeckControls = false, children }) { + const [open, setOpen] = React.useState(false); + const dragRef = React.useRef(null); + // Auto-inject a rail toggle when a is on the page. The + // toggle drives the deck's per-viewer _railVisible via window message; + // state is mirrored from the same localStorage key the deck reads so + // the control reflects reality across reloads. The mechanism is the + // message — authors who want custom placement can post it directly + // and pass noDeckControls to suppress this one. + const hasDeckStage = React.useMemo( + () => typeof document !== 'undefined' && !!document.querySelector('deck-stage'), + [], + ); + // deck-stage enables its rail in connectedCallback, but this panel can + // mount before that element has upgraded. The initial read catches the + // common case; the listener covers mounting first. (Older deck-stage.js + // copies still wait for the host's __omelette_rail_enabled postMessage — + // same listener handles those.) + const [railEnabled, setRailEnabled] = React.useState( + () => hasDeckStage && !!document.querySelector('deck-stage')?._railEnabled, + ); + React.useEffect(() => { + if (!hasDeckStage || railEnabled) return undefined; + const onMsg = (e) => { + if (e.data && e.data.type === '__omelette_rail_enabled') setRailEnabled(true); + }; + window.addEventListener('message', onMsg); + return () => window.removeEventListener('message', onMsg); + }, [hasDeckStage, railEnabled]); + const [railVisible, setRailVisible] = React.useState(() => { + try { return localStorage.getItem('deck-stage.railVisible') !== '0'; } catch (e) { return true; } + }); + const toggleRail = (on) => { + setRailVisible(on); + window.postMessage({ type: '__deck_rail_visible', on }, '*'); + }; + const offsetRef = React.useRef({ x: 16, y: 16 }); + const PAD = 16; + + const clampToViewport = React.useCallback(() => { + const panel = dragRef.current; + if (!panel) return; + const w = panel.offsetWidth, h = panel.offsetHeight; + const maxRight = Math.max(PAD, window.innerWidth - w - PAD); + const maxBottom = Math.max(PAD, window.innerHeight - h - PAD); + offsetRef.current = { + x: Math.min(maxRight, Math.max(PAD, offsetRef.current.x)), + y: Math.min(maxBottom, Math.max(PAD, offsetRef.current.y)), + }; + panel.style.right = offsetRef.current.x + 'px'; + panel.style.bottom = offsetRef.current.y + 'px'; + }, []); + + React.useEffect(() => { + if (!open) return; + clampToViewport(); + if (typeof ResizeObserver === 'undefined') { + window.addEventListener('resize', clampToViewport); + return () => window.removeEventListener('resize', clampToViewport); + } + const ro = new ResizeObserver(clampToViewport); + ro.observe(document.documentElement); + return () => ro.disconnect(); + }, [open, clampToViewport]); + + React.useEffect(() => { + const onMsg = (e) => { + const t = e?.data?.type; + if (t === '__activate_edit_mode') setOpen(true); + else if (t === '__deactivate_edit_mode') setOpen(false); + }; + window.addEventListener('message', onMsg); + window.parent.postMessage({ type: '__edit_mode_available' }, '*'); + return () => window.removeEventListener('message', onMsg); + }, []); + + const dismiss = () => { + setOpen(false); + window.parent.postMessage({ type: '__edit_mode_dismissed' }, '*'); + }; + + const onDragStart = (e) => { + const panel = dragRef.current; + if (!panel) return; + const r = panel.getBoundingClientRect(); + const sx = e.clientX, sy = e.clientY; + const startRight = window.innerWidth - r.right; + const startBottom = window.innerHeight - r.bottom; + const move = (ev) => { + offsetRef.current = { + x: startRight - (ev.clientX - sx), + y: startBottom - (ev.clientY - sy), + }; + clampToViewport(); + }; + const up = () => { + window.removeEventListener('mousemove', move); + window.removeEventListener('mouseup', up); + }; + window.addEventListener('mousemove', move); + window.addEventListener('mouseup', up); + }; + + if (!open) return null; + return ( + <> + +
+
+ {title} + +
+
+ {children} + {hasDeckStage && railEnabled && !noDeckControls && ( + + + + )} +
+
+ + ); +} + +// ── Layout helpers ────────────────────────────────────────────────────────── + +function TweakSection({ label, children }) { + return ( + <> +
{label}
+ {children} + + ); +} + +function TweakRow({ label, value, children, inline = false }) { + return ( +
+
+ {label} + {value != null && {value}} +
+ {children} +
+ ); +} + +// ── Controls ──────────────────────────────────────────────────────────────── + +function TweakSlider({ label, value, min = 0, max = 100, step = 1, unit = '', onChange }) { + return ( + + onChange(Number(e.target.value))} /> + + ); +} + +function TweakToggle({ label, value, onChange }) { + return ( +
+
{label}
+ +
+ ); +} + +function TweakRadio({ label, value, options, onChange }) { + const trackRef = React.useRef(null); + const [dragging, setDragging] = React.useState(false); + // The active value is read by pointer-move handlers attached for the lifetime + // of a drag — ref it so a stale closure doesn't fire onChange for every move. + const valueRef = React.useRef(value); + valueRef.current = value; + + // Segments wrap mid-word once per-segment width runs out. The track is + // ~248px (280 panel − 28 body pad − 4 seg pad), each button loses 12px + // to its own padding, and 11.5px system-ui averages ~6.3px/char — so 2 + // options fit ~16 chars each, 3 fit ~10. Past that (or >3 options), fall + // back to a dropdown rather than wrap. + const labelLen = (o) => String(typeof o === 'object' ? o.label : o).length; + const maxLen = options.reduce((m, o) => Math.max(m, labelLen(o)), 0); + const fitsAsSegments = maxLen <= ({ 2: 16, 3: 10 }[options.length] ?? 0); + if (!fitsAsSegments) { + // onChange(e.target.value)}> + {options.map((o) => { + const v = typeof o === 'object' ? o.value : o; + const l = typeof o === 'object' ? o.label : o; + return ; + })} + + + ); +} + +function TweakText({ label, value, placeholder, onChange }) { + return ( + + onChange(e.target.value)} /> + + ); +} + +function TweakNumber({ label, value, min, max, step = 1, unit = '', onChange }) { + const clamp = (n) => { + if (min != null && n < min) return min; + if (max != null && n > max) return max; + return n; + }; + const startRef = React.useRef({ x: 0, val: 0 }); + const onScrubStart = (e) => { + e.preventDefault(); + startRef.current = { x: e.clientX, val: value }; + const decimals = (String(step).split('.')[1] || '').length; + const move = (ev) => { + const dx = ev.clientX - startRef.current.x; + const raw = startRef.current.val + dx * step; + const snapped = Math.round(raw / step) * step; + onChange(clamp(Number(snapped.toFixed(decimals)))); + }; + const up = () => { + window.removeEventListener('pointermove', move); + window.removeEventListener('pointerup', up); + }; + window.addEventListener('pointermove', move); + window.addEventListener('pointerup', up); + }; + return ( +
+ {label} + onChange(clamp(Number(e.target.value)))} /> + {unit && {unit}} +
+ ); +} + +// Relative-luminance contrast pick — checkmarks drawn over a swatch need to +// read on both #111 and #fafafa without per-option configuration. Hex input +// only (#rgb / #rrggbb); named or rgb()/hsl() colors fall through to "light". +function __twkIsLight(hex) { + const h = String(hex).replace('#', ''); + const x = h.length === 3 ? h.replace(/./g, (c) => c + c) : h.padEnd(6, '0'); + const n = parseInt(x.slice(0, 6), 16); + if (Number.isNaN(n)) return true; + const r = (n >> 16) & 255, g = (n >> 8) & 255, b = n & 255; + return r * 299 + g * 587 + b * 114 > 148000; +} + +const __TwkCheck = ({ light }) => ( + +); + +// TweakColor — curated color/palette picker. Each option is either a single +// hex string or an array of 1-5 hex strings; the card adapts — a lone color +// renders solid, a palette renders colors[0] as the hero (left ~2/3) with the +// rest stacked in a sharp column on the right. onChange emits the +// option in the shape it was passed (string stays string, array stays array). +// Without options it falls back to the native color input for back-compat. +function TweakColor({ label, value, options, onChange }) { + if (!options || !options.length) { + return ( +
+
{label}
+ onChange(e.target.value)} /> +
+ ); + } + // Native emits lowercase hex per the HTML spec, so + // compare case-insensitively. String() guards JSON.stringify(undefined), + // which returns the primitive undefined (no .toLowerCase). + const key = (o) => String(JSON.stringify(o)).toLowerCase(); + const cur = key(value); + return ( + +
+ {options.map((o, i) => { + const colors = Array.isArray(o) ? o : [o]; + const [hero, ...rest] = colors; + const sup = rest.slice(0, 4); + const on = key(o) === cur; + return ( + + ); + })} +
+
+ ); +} + +function TweakButton({ label, onClick, secondary = false }) { + return ( + + ); +} + +Object.assign(window, { + useTweaks, TweaksPanel, TweakSection, TweakRow, + TweakSlider, TweakToggle, TweakRadio, TweakSelect, + TweakText, TweakNumber, TweakColor, TweakButton, +}); diff --git a/Do Better Tools v1/views.jsx b/Do Better Tools v1/views.jsx new file mode 100644 index 0000000..98e45da --- /dev/null +++ b/Do Better Tools v1/views.jsx @@ -0,0 +1,621 @@ +/* global React, CiteChip, ArtChip, Stamp, Redact, useRunSequence */ +const { useState, useEffect, useRef, useMemo } = React; + +/* ============================================================================ + ASK — source-grounded legal question + ============================================================================ */ +const ASK_SUGGESTIONS = [ + "What weight does Barneloven §31 give to a child's stated preference?", + "Under ECHR Art. 8, what reunification duty does Norway owe after a care order?", + "Does Abdi Ibrahim v. Norway (2021) require religious matching of foster parents?", + "What contact frequency post-care-order has Strasbourg found compatible with Art. 8?", +]; + +function AskView({ onTrace, lang, setLang }) { + const [q, setQ] = useState(ASK_SUGGESTIONS[0]); + const [running, setRunning] = useState(false); + const [done, setDone] = useState(false); + const [highlight, setHighlight] = useState(null); + const { run } = useRunSequence(); + + function go() { + if (running) return; + setRunning(true); setDone(false); setHighlight(null); + onTrace([]); + const seq = [ + { delay: 400, step: { label: "Query interpreted", detail: "ECHR Art. 8 reunification duty · Barneloven §43 · post-care-order contact", status: "" } }, + { delay: 1200, step: { label: "Hybrid retrieval", detail: "Vector + BM25 · 24 candidates · reciprocal rank fusion · 7 selected", status: "" } }, + { delay: 1000, step: { label: "Reviewer pass running", detail: "Cross-checking every claim against retrieved Strasbourg + Lovdata chunks", status: "running" } }, + { delay: 1400, step: { label: "Reviewer: approved", detail: "All claims supported. Strand Lobben (2019) and Pedersen (2020) cited.", status: "" } }, + { delay: 700, step: { label: "Uncertainty noted", detail: "Post-2024 amendments to Barnevernsloven may not yet be indexed.", status: "warning" } }, + ]; + let acc = []; + run(seq, (st, idx, last) => { + acc = [...acc.map(s => s.status === "running" ? { ...s, status: "" } : s), st]; + onTrace([...acc]); + if (last) { setRunning(false); setDone(true); } + }); + } + + return ( +
+
+
+

Source-grounded ask · ECHR + Lovdata

+

Ask a legal question

+
+
+ + Reviewer on +
+
+ +
+ +