Jeg er blevet spurgt ind til, hvordan man kunne arbejde med AI sammen med lærerstuderende i idræt. Det giver jeg herunder et bud på. Det skal blot ses som eksempler på, hvordan AI måske kan blive den enkeltes vej til at lære og forstå nogle af de begreber og fænomener, som til tider kan være svære at forstå i idræt.
En væsentlig pointe er dog også, at de studerendes egne eksempler og erfaringer bringes ind i en fælles debat. Ikke alene om, hvad der virker, og hvad der ikke virker, men også om, hvordan det virker, hvorfor det virker, og hvad der skal til for at gøre det endnu bedre. Tanken er således, at hverken processen eller resultaterne på noget tidspunkt betragtes som fuldendte. De er hele tiden til diskussion, afprøvning og videreudvikling. På den måde øges muligheden for, at de studerendes egen forståelse bliver en mediator for en forståelse, der hele tiden er i udvikling.
Jeg har abonnement på Gemini, Claude og ChatGPT og derudover adgang til Copilot. De kan noget forskelligt og anvendes derfor også i forskellige sammenhænge. Det er værd at bemærke, at Gemini, Claude og ChatGPT ikke alene giver mulighed for at chatte med AI. De giver også mulighed for mere agentisk arbejde og for at arbejde direkte med filer på computeren. Her kan de blandt andet bygge, redigere og videreudvikle filer, uden at man behøver at starte forfra hver gang. Det gør det muligt at arbejde videre med det samme produkt og løbende udbygge, ændre og kvalificere det.
Af mulige andre områder, som man kunne undersøge kunne være følgende:
- Anatomi og bevægeapparatet – Interaktiv krop, hvor man kan udforske muskler, knogler og led og se deres funktion i forskellige bevægelser.
- Bevægelsesanalyse – Visualisering af fx løb, spring, kast eller squat, hvor vinkler, ledbevægelser, tyngdepunkt og kraft kan undersøges.
- Fysiologi og energisystemer – Simulér, hvad der sker med puls, iltoptagelse og energiforsyning ved forskellige typer og intensiteter af fysisk aktivitet.
- Træningsplanlægning – Et interaktivt værktøj, hvor man sammensætter træningsforløb og undersøger betydningen af intensitet, varighed, restitution og progression.
- Puls og arbejdsintensitet – Lad de studerende ændre alder, hvilepuls og arbejdsbelastning og undersøge puls, pulszoner og træningsintensitet.
- Biomekanik – Eksperimentér med kraft, masse, acceleration, moment, balance og tyngdepunkt og se konsekvenserne direkte i en visualisering.
- Idrætsdidaktik – Et værktøj, hvor man kan ændre fx elevgruppe, rammer, mål og aktivitet og undersøge forskellige didaktiske valg og konsekvenser.
- Spil og taktik – Interaktive baner til fx fodbold, håndbold eller basketball, hvor spillere kan flyttes rundt, og forskellige taktiske situationer kan afprøves.
- Motorik og motorisk udvikling – Visualisér grundbevægelser og motoriske færdigheder, og arbejd med progression og variation af aktiviteter.
- Skader og forebyggelse – En interaktiv krop, hvor typiske idrætsskader kan undersøges med fokus på, hvilke strukturer der påvirkes, hvordan skader kan opstå, og hvordan risikoen kan reduceres.
I det nedenstående har jeg konstrueret forskellige hjælpemidler, som kunne anvendes i arbejdet med at forstå idræt. De kan naturligvis både forfines og udbygges. De skal derfor ikke ses som færdige produkter, men alene som eksempler på noget af det, der er muligt.
Alle koderne kan afvikles enten ved at gemme som en index.html fil eller som kode i onecompiler.com/html.
Forskellige AI’er
Du skal udvikle en komplet, interaktiv webapplikation til undervisning i idræt på læreruddannelsen. Applikationen skal især understøtte læring om menneskets anatomi, fysiologi, bevægelse og kroppens funktioner.
1. Teknisk ramme
Lav løsningen som en selvstændig webapplikation baseret på:
* HTML5
* CSS3
* JavaScript
* SVG, Canvas eller tilsvarende webteknologi til interaktive visualiseringer
Løsningen skal kunne:
1. afvikles direkte lokalt på en computer
2. kopieres til og afvikles i OneCompiler.com
3. fungere uden backend og database
4. fungere uden build-proces, npm eller installation af pakker
5. så vidt muligt fungere uden eksterne dependencies
6. fungere på både computer, tablet og mobil
Lav i første omgang gerne løsningen som én samlet index.html, hvor CSS og JavaScript er indlejret. Koden skal dog struktureres, så den senere let kan opdeles i separate filer.
Arkitekturen skal være modulær og udvidelsesvenlig. Det skal være muligt senere at tilføje nye kropsdele, organsystemer, animationer, læringsaktiviteter og datasæt uden at skulle omskrive hele applikationen.
2. Målgruppe og fagligt niveau
Målgruppen er lærerstuderende med idræt som undervisningsfag.
Siden skal derfor ikke være en medicinsk lærebog, men et visuelt og interaktivt undervisningsredskab, der gør anatomi og fysiologi forståelig i relation til:
* kroppen
* bevægelse
* fysisk aktivitet
* træning
* sundhed
* idrætsundervisning
Det faglige niveau skal være relevant for læreruddannelsen.
3. Hovedidé: Den interaktive menneskekrop
Applikationens centrale element skal være en stor interaktiv menneskekrop.
Kroppen skal så vidt muligt kunne ses:
* forfra
* bagfra
og på sigt gerne fra siden.
Brugeren skal kunne klikke på kroppen og undersøge den lag for lag.
Lav et system med anatomiske lag, fx:
1. Krop/ydre anatomi
2. Skelet
3. Muskler
4. Organer
5. Kredsløb
6. Luftveje og lunger
7. Fordøjelsessystem
8. Nervesystem
Brugeren skal kunne slå lag til og fra individuelt.
Hvis flere lag vises samtidig, skal visualiseringen stadig være forståelig.
4. “Split kroppen”
Lav mulighed for at “splitte” eller adskille kroppen i dens forskellige anatomiske systemer.
Eksempel:
Krop
→ muskler
→ skelet
→ organer
→ kredsløb
→ nervesystem
Lav gerne en eksploderet visning, slider eller anden interaktion, hvor lagene visuelt trækkes fra hinanden.
Det skal give den studerende en forståelse af, hvordan kroppens forskellige systemer ligger i forhold til hinanden.
5. Zoom og “dyk ned i kroppen”
Man skal kunne bevæge sig fra et overordnet niveau til mere detaljerede niveauer.
Eksempel:
Menneskekrop
→ ben
→ knæ
→ knæled
→ knogler, muskler, sener og ledbånd
eller:
Menneskekrop
→ brystkasse
→ hjerte
→ hjertets kamre
→ blodets vej gennem hjertet
Lav derfor et hierarkisk system, hvor en kropsdel kan have underdele.
Eksempel på datastruktur:
body
head
torso
arm
shoulder
upperArm
elbow
forearm
hand
leg
hip
thigh
knee
lowerLeg
ankle
foot
Dette princip skal kunne anvendes på både kropsregioner, knogler, muskler og organer.
6. Navngivning på tre sprog
Alle anatomiske strukturer skal kunne navngives på:
* dansk
* engelsk
* latin
Lav en global sprogindstilling.
Eksempel:
Dansk:
Lårben
English:
Femur / thigh bone
Latin:
Femur
Brugeren skal kunne skifte sprog uden at genindlæse siden.
Lav også mulighed for at vise:
* kun ét sprog
* dansk + latin
* dansk + engelsk
* alle tre sprog
Dette er især vigtigt i læreruddannelsessammenhæng.
7. Klikbare anatomiske strukturer
Når brugeren holder musen over en kropsdel, muskel, knogle eller et organ, skal området fremhæves.
Ved klik åbnes et informationspanel.
Panelet skal eksempelvis vise:
Navn
Dansk, engelsk og latin.
Type
Fx muskel, knogle, led, organ, sene eller ligament.
Placering
Hvor findes strukturen?
Funktion
Hvad gør den?
Betydning for bevægelse
Hvordan indgår den i kroppens bevægelser?
Idrætsrelevans
Hvorfor er den interessant i forbindelse med fysisk aktivitet og idrætsundervisning?
Relaterede strukturer
Klikbare links til relevante muskler, knogler, led eller organer.
Informationspanelet skal kunne udvides senere.
8. Skelet
Lav en skeletvisning.
De vigtigste knogler skal være klikbare og navngivne.
Medtag som minimum relevante større knogler som:
* kranium
* rygsøjle
* ribben
* brystben
* bækken
* kraveben
* skulderblad
* overarmsknogle
* spoleben
* albueben
* håndens knogler
* lårben
* knæskal
* skinneben
* lægben
* fodens knogler
Det skal senere være muligt at udvide med flere knogler.
9. Muskler
Lav en muskelvisning med de vigtigste muskler og muskelgrupper.
Fx:
* deltoideus
* biceps
* triceps
* pectoralis major
* trapezius
* latissimus dorsi
* rectus abdominis
* gluteus maximus
* quadriceps
* hamstrings
* gastrocnemius
Ved klik skal funktion og bevægelse forklares.
Hvis muligt skal der også stå fx:
* udspring
* hæfte
* funktion
* hvilke bevægelser musklen bidrager til
Lav datastrukturen, så flere muskler let kan tilføjes.
10. Bevægelse
Lav en særlig “Bevægelse”-funktion.
Her skal brugeren kunne vælge en bevægelse, fx:
* fleksion af albuen
* ekstension af albuen
* fleksion af knæet
* ekstension af knæet
* abduktion af skulderen
* squat
* hop
* løb
Kroppen skal herefter fremhæve de relevante:
* led
* muskler
* knogler
Hvis muligt, vis bevægelsen med en simpel animation.
Forklar samtidig, hvilke muskler der primært arbejder.
Dette område skal være designet til senere at kunne udvides med flere bevægelser og idrætsaktiviteter.
11. Interaktive fysiologiske forløb
Applikationen skal ikke kun vise statisk anatomi.
Lav et system til animationer, hvor den studerende kan følge processer gennem kroppen.
Der skal mindst være mulighed for senere at understøtte følgende tre forløb:
Iltens vej
Vis visuelt:
luft
→ mund/næse
→ svælg
→ luftrør
→ bronkier
→ lunger
→ alveoler
→ blod
→ hjerte
→ muskler
Brug animationer, pile eller bevægelige partikler.
Brugeren skal kunne:
* starte
* pause
* fortsætte
* nulstille
* ændre hastighed
Undervejs skal relevante strukturer fremhæves.
Madens vej
Vis:
mund
→ svælg
→ spiserør
→ mavesæk
→ tyndtarm
→ tyktarm
→ endetarm
Vis samtidig korte forklaringer af, hvad der sker de enkelte steder.
Blodets vej
Visualiser kredsløbet.
Vis fx:
krop
→ højre side af hjertet
→ lunger
→ venstre side af hjertet
→ arterier
→ muskler/organer
→ vener
→ hjerte
Det skal være muligt at følge en “bloddråbe” gennem kredsløbet.
12. Idrætsfaglig tilstand
Lav mulighed for at skifte mellem fx:
* Hvile
* Moderat aktivitet
* Hård fysisk aktivitet
Når tilstanden ændres, kan siden eksempelvis vise illustrative ændringer i:
* puls
* vejrtrækningsfrekvens
* iltbehov
* blodgennemstrømning
* muskelaktivitet
Hvis der anvendes konkrete tal, skal det tydeligt fremgå, at de er illustrative eksempler og ikke individuelle medicinske målinger.
13. Læringsfunktioner
Siden skal være et undervisningsredskab og ikke kun et anatomisk atlas.
Tilføj derfor en sektion kaldet fx “Træn din viden”.
Lav mindst følgende muligheder:
Find kropsdelen
Siden skriver fx:
“Find lårbenet.”
Den studerende klikker på kroppen.
Der gives feedback.
Hvad hedder den?
En anatomisk struktur fremhæves, og den studerende skal vælge eller skrive navnet.
Dansk ↔ latin
Match danske og latinske betegnelser.
Muskel og bevægelse
Eksempel:
“Hvilken muskel er særlig central ved fleksion i albueleddet?”
Følg processen
Den studerende skal placere strukturer i korrekt rækkefølge, fx iltens vej gennem kroppen.
14. Underviserfunktion
Lav arkitekturen, så der senere kan tilføjes en særlig undervisertilstand.
Den skal eksempelvis kunne:
* skjule anatomiske navne
* vise alle navne
* vælge bestemte anatomiske systemer
* vælge bestemte strukturer
* starte quiz
* vise spørgsmål på storskærm
* fremhæve en bestemt kropsdel
Det skal gøre applikationen anvendelig både til selvstudie og fælles undervisning.
15. Brugerflade
Lav et moderne, roligt og fagligt design.
Forslag til layout:
Topbjælke
* titel
* valg af sprog
* fuldskærm
* hjælp
Venstre panel
Navigation:
* Hele kroppen
* Skelet
* Muskler
* Led
* Organer
* Kredsløb
* Åndedræt
* Fordøjelse
* Nervesystem
* Bevægelse
* Processer
* Træn din viden
Midten
Stor interaktiv menneskekrop.
Højre panel
Information om den valgte anatomiske struktur.
På mindre skærme skal panelerne kunne foldes sammen.
16. Søgning
Lav en søgefunktion.
Brugeren skal fx kunne skrive:
“biceps”
“lårben”
“femur”
“quadriceps”
Søgningen skal kunne finde strukturen uanset om søgeordet er dansk, engelsk eller latin.
Når resultatet vælges:
1. navigeres til den korrekte visning
2. strukturen fremhæves
3. informationspanelet åbnes
17. Favoritter og fokus
Lav mulighed for at markere strukturer som favoritter eller som “skal læres”.
Eksempel:
⭐ Femur
⭐ M. biceps brachii
⭐ Tibia
Gem dette i browserens LocalStorage, så det virker uden database.
18. Udvidelsesvenlig datastruktur
Dette er meget vigtigt.
Undgå at hardcode alle informationer direkte ind i brugerfladen.
Opret i stedet en central datastruktur i JavaScript.
Eksempel:
{
id: “femur”,
type: “bone”,
names: {
da: “Lårben”,
en: “Femur”,
la: “Femur”
},
description: {…},
function: {…},
parent: “leg”,
related: […],
tags: […]
}
Lav tilsvarende datastrukturer for:
* muskler
* knogler
* led
* organer
* processer
* bevægelser
* quizspørgsmål
Det skal være let senere at flytte disse data til JSON-filer.
19. Modulær opbygning
Tænk applikationen som moduler.
Eksempel:
AnatomyModule
SkeletonModule
MuscleModule
MovementModule
CirculationModule
RespirationModule
DigestionModule
QuizModule
LanguageModule
SearchModule
Selv hvis den første prototype ligger i én HTML-fil, skal JavaScript struktureres efter dette princip.
20. Mulighed for senere udbygning
Design løsningen fra starten, så følgende senere kan tilføjes:
* flere muskler
* flere knogler
* detaljerede led
* sener og ligamenter
* nervesystem
* energisystemer
* aerob og anaerob energiomsætning
* muskelkontraktion
* træningsfysiologi
* biomekanik
* skader
* bevægelsesanalyse
* idrætsgrene
* video
* egne undervisningsaktiviteter
* flere quiztyper
* progression og niveauer
Lav gerne tydelige kommentarer i koden som:
// EXTENSION POINT: Add new muscles here
eller:
// EXTENSION POINT: Add new physiological processes here
21. Faglig korrekthed
Anatomiske navne og beskrivelser skal være fagligt korrekte.
Undgå at opfinde anatomiske oplysninger.
Hvis den grafiske prototype ikke kan rumme en anatomisk præcis gengivelse af hele menneskekroppen, skal du prioritere en god, funktionel og udvidelsesvenlig prototype frem for at foregive anatomisk præcision.
SVG-elementer skal navngives med meningsfulde ID’er, så de senere kan erstattes med mere detaljerede anatomiske SVG’er.
Eksempel:
id="femur-left"
id="biceps-right"
id="heart"
id="lungs"
22. Visuel kvalitet
Det må ikke ligne en simpel skoleopgave eller en samling HTML-knapper.
Lav en sammenhængende webapplikation med:
* tydeligt visuelt hierarki
* kortbaserede informationsområder
* moderne typografi
* diskrete animationer
* hover-effekter
* tooltips
* responsive paneler
* gode overgange mellem visninger
* tydelig markering af valgte anatomiske strukturer
Selve menneskekroppen skal visuelt være det dominerende element.
23. Tilgængelighed
Tænk også på:
* tastaturnavigation
* kontraster
* tooltips
* ARIA-labels
* mulighed for at anvende siden uden hover
* touch-betjening på tablets
24. Første prototype
Første version behøver ikke indeholde en medicinsk detaljeret 3D-model.
Det vigtigste er at skabe en fungerende prototype, hvor man tydeligt kan opleve konceptet.
Første prototype skal som minimum indeholde:
* interaktiv menneskekrop
* forside/standardvisning af kroppen
* skeletlag
* muskellag
* organlag
* mulighed for at slå lag til/fra
* mindst 15-20 klikbare anatomiske strukturer
* dansk/engelsk/latin
* informationspanel
* søgefunktion
* zoom eller drill-down
* mindst én interaktiv fysiologisk proces, fx iltens vej
* mindst én bevægelsesdemonstration
* en lille quizfunktion
* responsive design
* LocalStorage til relevante brugerindstillinger
25. Kvalitetssikring
Inden du afslutter:
1. Kontrollér at HTML, CSS og JavaScript fungerer sammen.
2. Kontrollér at der ikke er JavaScript-fejl.
3. Kontrollér at knapper og navigation virker.
4. Kontrollér at sprogskift virker.
5. Kontrollér at anatomiske elementer kan vælges.
6. Kontrollér at lag kan slås til og fra.
7. Kontrollér at layoutet fungerer på mindre skærme.
8. Kontrollér at løsningen kan køres direkte uden installation.
9. Undgå placeholder-knapper, som ikke gør noget. Hvis en funktion ikke implementeres endnu, skal den tydeligt markeres som en kommende funktion.
10. Bevar en klar og veldokumenteret struktur, så projektet efterfølgende kan videreudvikles af en anden AI eller udvikler.
26. Levering
Lever den komplette fungerende kode.
Start med den faktiske prototype og ikke kun en beskrivelse af, hvordan den kunne laves.
Hvis hele løsningen kan rummes fornuftigt i én fil, lever index.html med CSS og JavaScript indlejret.
Hvis løsningen bliver for omfattende til én fil, må den opdeles i:
* index.html
* styles.css
* app.js
* eventuelle datafiler
men giv i så fald også en meget kort forklaring på, hvordan filerne placeres og køres.
Prioritér, at den første version rent faktisk fungerer, frem for at implementere mange halvfærdige funktioner.
Dette er Googles chatbaserede AI-assistent/LLM.
<!DOCTYPE html>
<html lang="da">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Krop & Idræt - Interaktiv Anatomi og Fysiologi</title>
<style>
/* ==========================================================================
1. CSS VARIABLER & TEMA (Moderne, roligt og fagligt UI)
========================================================================== */
:root {
--bg-main: #0f172a;
--bg-panel: #1e293b;
--bg-card: #334155;
--bg-card-hover: #475569;
--border-color: #334155;
--text-main: #f8fafc;
--text-muted: #94a3b8;
--accent: #38bdf8;
--accent-hover: #0ea5e9;
--bone-color: #e2e8f0;
--muscle-color: #f87171;
--organ-color: #fb923c;
--highlight: #facc15;
--success: #4ade80;
--danger: #f43f5e;
--font-stack: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: var(--font-stack);
background-color: var(--bg-main);
color: var(--text-main);
line-height: 1.5;
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* ==========================================================================
2. TOP BAR
========================================================================== */
header {
background-color: var(--bg-panel);
border-bottom: 1px solid var(--border-color);
padding: 0.75rem 1.5rem;
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
z-index: 10;
}
.brand {
display: flex;
align-items: center;
gap: 0.75rem;
font-weight: 700;
font-size: 1.25rem;
color: var(--accent);
}
.brand svg {
width: 28px;
height: 28px;
fill: currentColor;
}
.header-controls {
display: flex;
align-items: center;
gap: 1rem;
flex-wrap: wrap;
}
.control-group {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
color: var(--text-muted);
}
select, input, button {
background-color: var(--bg-card);
color: var(--text-main);
border: 1px solid var(--border-color);
padding: 0.4rem 0.75rem;
border-radius: 6px;
font-size: 0.875rem;
font-family: inherit;
outline: none;
transition: all 0.2s ease;
}
select:focus, input:focus, button:focus {
border-color: var(--accent);
}
button {
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 0.4rem;
}
button:hover {
background-color: var(--bg-card-hover);
}
button.active {
background-color: var(--accent);
color: #000;
font-weight: 600;
}
/* ==========================================================================
3. HOVEDLAYOUT (3-KOLONNER: Navigation - Visualisering - Info)
========================================================================== */
.app-container {
display: grid;
grid-template-columns: 280px 1fr 360px;
flex: 1;
overflow: hidden;
position: relative;
}
@media (max-width: 1024px) {
.app-container {
grid-template-columns: 240px 1fr;
}
.info-panel {
position: absolute;
right: 0;
top: 0;
bottom: 0;
width: 320px;
box-shadow: -4px 0 15px rgba(0,0,0,0.5);
transform: translateX(100%);
transition: transform 0.3s ease;
z-index: 20;
}
.info-panel.open {
transform: translateX(0);
}
}
@media (max-width: 768px) {
.app-container {
grid-template-columns: 1fr;
}
.sidebar {
display: none; /* Kan slås op på mobil via drawer */
}
}
/* ==========================================================================
4. VENSTRE PANEL (Navigation & Moduler)
========================================================================== */
.sidebar {
background-color: var(--bg-panel);
border-right: 1px solid var(--border-color);
overflow-y: auto;
padding: 1rem;
display: flex;
flex-direction: column;
gap: 1.25rem;
}
.nav-section-title {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-muted);
margin-bottom: 0.5rem;
}
.nav-list {
list-style: none;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.nav-btn {
width: 100%;
text-align: left;
justify-content: flex-start;
border: 1px solid transparent;
padding: 0.6rem 0.8rem;
}
.nav-btn:hover {
background-color: var(--bg-card);
}
.layer-toggles {
display: flex;
flex-direction: column;
gap: 0.5rem;
background: var(--bg-card);
padding: 0.75rem;
border-radius: 8px;
}
.layer-toggle-item {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 0.875rem;
}
/* Switch styling */
.switch {
position: relative;
display: inline-block;
width: 36px;
height: 20px;
}
.switch input { opacity: 0; width: 0; height: 0; }
.slider {
position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0;
background-color: #475569;
transition: .3s;
border-radius: 20px;
}
.slider:before {
position: absolute; content: ""; height: 14px; width: 14px; left: 3px; bottom: 3px;
background-color: white;
transition: .3s;
border-radius: 50%;
}
input:checked + .slider { background-color: var(--accent); }
input:checked + .slider:before { transform: translateX(16px); }
/* ==========================================================================
5. CENTRALT OMRÅDE (Interaktiv Krop & Kontroller)
========================================================================== */
.viewport-panel {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: radial-gradient(circle at center, #1e293b 0%, #0b1120 100%);
overflow: hidden;
}
.view-toolbar {
position: absolute;
top: 1rem;
display: flex;
gap: 0.5rem;
background-color: rgba(30, 41, 59, 0.85);
backdrop-filter: blur(8px);
padding: 0.4rem;
border-radius: 8px;
border: 1px solid var(--border-color);
z-index: 5;
}
.svg-container {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
cursor: grab;
user-select: none;
}
.svg-container:active {
cursor: grabbing;
}
svg.human-body {
height: 90%;
max-height: 780px;
filter: drop-shadow(0 10px 20px rgba(0,0,0,0.5));
transition: transform 0.4s cubic-bezier(0.16, 1, 0.3, 1);
}
/* Interaktive SVG-elementer */
.anatomical-part {
cursor: pointer;
transition: all 0.2s ease;
}
.anatomical-part:hover {
filter: brightness(1.35) drop-shadow(0 0 6px var(--accent));
stroke: #fff;
stroke-width: 1.5;
}
.anatomical-part.active {
filter: drop-shadow(0 0 10px var(--highlight));
stroke: var(--highlight);
stroke-width: 2.5;
}
.anatomical-part.dimmed {
opacity: 0.2;
}
/* Partikler til fysiologiske forløb */
.particle {
pointer-events: none;
fill: var(--accent);
filter: drop-shadow(0 0 4px var(--accent));
transition: cx 0.2s linear, cy 0.2s linear;
}
/* ==========================================================================
6. HØJRE PANEL (Informationskort & Interaktive Tilstande)
========================================================================== */
.info-panel {
background-color: var(--bg-panel);
border-left: 1px solid var(--border-color);
overflow-y: auto;
padding: 1.25rem;
display: flex;
flex-direction: column;
gap: 1.25rem;
}
.card {
background-color: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 1rem;
}
.card h3 {
font-size: 1rem;
color: var(--accent);
margin-bottom: 0.5rem;
display: flex;
align-items: center;
justify-content: space-between;
}
.term-names {
margin-bottom: 1rem;
border-bottom: 1px solid var(--border-color);
padding-bottom: 0.75rem;
}
.term-title {
font-size: 1.4rem;
font-weight: 700;
color: #fff;
}
.term-sub {
font-size: 0.875rem;
color: var(--text-muted);
margin-top: 0.2rem;
}
.badge {
display: inline-block;
font-size: 0.75rem;
font-weight: 600;
padding: 0.2rem 0.5rem;
border-radius: 4px;
text-transform: uppercase;
background-color: var(--border-color);
color: #cbd5e1;
}
.badge.bone { background: #334155; color: #e2e8f0; border: 1px solid #94a3b8; }
.badge.muscle { background: #7f1d1d; color: #fca5a5; }
.badge.organ { background: #7c2d12; color: #fdba74; }
.meta-field {
margin-top: 0.75rem;
}
.meta-field-label {
font-size: 0.75rem;
text-transform: uppercase;
color: var(--text-muted);
font-weight: 600;
}
.meta-field-content {
font-size: 0.9rem;
margin-top: 0.2rem;
}
.related-tag {
display: inline-block;
background: var(--bg-main);
padding: 0.2rem 0.6rem;
border-radius: 4px;
font-size: 0.8rem;
margin-right: 0.4rem;
margin-top: 0.4rem;
cursor: pointer;
color: var(--accent);
border: 1px solid var(--border-color);
}
.related-tag:hover {
border-color: var(--accent);
}
/* ==========================================================================
7. STATS BARS FOR FYSIOLOGI / AKTIVITETSNIVEAU
========================================================================== */
.stat-row {
display: flex;
justify-content: space-between;
margin-bottom: 0.3rem;
font-size: 0.85rem;
}
.stat-bar-bg {
height: 8px;
background: var(--bg-main);
border-radius: 4px;
overflow: hidden;
margin-bottom: 0.75rem;
}
.stat-bar-fill {
height: 100%;
background: var(--accent);
width: 30%;
transition: width 0.5s ease-out;
}
/* Modal / Toast feedback for Quiz */
.feedback-box {
padding: 0.75rem;
border-radius: 6px;
font-size: 0.9rem;
margin-top: 0.75rem;
display: none;
}
.feedback-box.correct {
background: rgba(74, 222, 128, 0.15);
border: 1px solid var(--success);
color: var(--success);
display: block;
}
.feedback-box.incorrect {
background: rgba(244, 63, 94, 0.15);
border: 1px solid var(--danger);
color: var(--danger);
display: block;
}
</style>
</head>
<body>
<!-- ==========================================================================
TOP BAR
========================================================================== -->
<header>
<div class="brand">
<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 3c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3 1.34-3 3-3zm0 14.2c-2.5 0-4.71-1.28-6-3.22.03-1.99 4-3.08 6-3.08 1.99 0 5.97 1.09 6 3.08-1.29 1.94-3.5 3.22-6 3.22z"/></svg>
<span>Krop & Idræt <small style="font-size: 0.75rem; color: var(--text-muted); font-weight: normal;">Læreruddannelsen</small></span>
</div>
<!-- Hurtigsøgning -->
<div style="flex: 1; max-width: 380px; position: relative;">
<input type="text" id="global-search" placeholder="Søg muskel, knogle, latin... (fx femur, biceps)" style="width: 100%;">
<div id="search-results" style="position: absolute; top: 100%; left: 0; right: 0; background: var(--bg-card); border-radius: 6px; border: 1px solid var(--border-color); display: none; z-index: 50; max-height: 250px; overflow-y: auto;"></div>
</div>
<div class="header-controls">
<!-- Sprogvisning -->
<div class="control-group">
<label for="lang-select">Sprog:</label>
<select id="lang-select">
<option value="da">Dansk</option>
<option value="la">Latin</option>
<option value="en">English</option>
</select>
</div>
<div class="control-group">
<label for="lang-mode">Format:</label>
<select id="lang-mode">
<option value="primary">Kun valgt sprog</option>
<option value="dual" selected>Dansk + Latin</option>
<option value="all">Alle 3 sprog</option>
</select>
</div>
<button id="toggle-favorites" title="Vis mine bogmærker/pensumfokus">⭐ <span id="fav-count">0</span></button>
</div>
</header>
<!-- ==========================================================================
HOVEDCONTAINER
========================================================================== -->
<div class="app-container">
<!-- VENSTRE SIDEBAR: Navigation og Lag -->
<aside class="sidebar">
<div>
<div class="nav-section-title">Anatomiske Lag</div>
<div class="layer-toggles">
<div class="layer-toggle-item">
<span>Skelet (Knogler)</span>
<label class="switch">
<input type="checkbox" id="layer-skeleton" checked>
<span class="slider"></span>
</label>
</div>
<div class="layer-toggle-item">
<span>Muskelsystem</span>
<label class="switch">
<input type="checkbox" id="layer-muscles" checked>
<span class="slider"></span>
</label>
</div>
<div class="layer-toggle-item">
<span>Vitale Organer</span>
<label class="switch">
<input type="checkbox" id="layer-organs" checked>
<span class="slider"></span>
</label>
</div>
</div>
</div>
<div>
<div class="nav-section-title">Funktionelle Moduler</div>
<ul class="nav-list">
<li><button class="nav-btn active" data-mode="explore">🔍 Undersøg Anatomi</button></li>
<li><button class="nav-btn" data-mode="movement">🏃 Bevægelsesanalyse</button></li>
<li><button class="nav-btn" data-mode="physiology">🫁 Fysiologiske Forløb</button></li>
<li><button class="nav-btn" data-mode="training">📝 Træn Din Viden (Quiz)</button></li>
</ul>
</div>
<div id="module-subpanel">
<!-- Dynamisk sub-panel afhængig af aktivt modul -->
</div>
<div style="margin-top: auto; padding-top: 1rem; border-top: 1px solid var(--border-color); font-size: 0.75rem; color: var(--text-muted);">
Udviklet til idrætsundervisere. Klik på strukturer for biomekanik og træningsfaglig relevans.
</div>
</aside>
<!-- MIDTERSTE VIEWPORT: Interaktiv Krop -->
<main class="viewport-panel">
<!-- Værktøjslinje over kroppen -->
<div class="view-toolbar">
<button id="view-anterior" class="active">Forfra</button>
<button id="view-posterior">Bagfra</button>
<button id="view-split">Eksploderet (Split)</button>
<button id="reset-zoom" title="Nulstil position">↺ Centrer</button>
</div>
<div class="svg-container" id="svg-stage">
<!-- Menneskekroppen SVG -->
<svg id="human-body-svg" class="human-body" viewBox="0 0 400 800" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="boneGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#f8fafc" />
<stop offset="100%" stop-color="#cbd5e1" />
</linearGradient>
<linearGradient id="muscleGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#f87171" />
<stop offset="100%" stop-color="#dc2626" />
</linearGradient>
<linearGradient id="organGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#fb923c" />
<stop offset="100%" stop-color="#ea580c" />
</linearGradient>
</defs>
<!-- Baggrundskontur af kroppen (Hud/Form) -->
<g id="layer-contour" opacity="0.15" fill="#64748b">
<!-- Hoved & Krop konturskitse for orientering -->
<ellipse cx="200" cy="70" rx="35" ry="45"/>
<path d="M165,115 C130,140 100,200 95,280 C90,340 110,480 135,490 L135,760 L185,760 L195,490 L205,490 L215,760 L265,760 L265,490 C290,480 310,340 305,280 C300,200 270,140 235,115 Z"/>
</g>
<!-- 1. SKELETLAG -->
<g id="group-skeleton">
<!-- Kranium -->
<path id="skull" class="anatomical-part" fill="url(#boneGrad)" stroke="#475569"
d="M175,40 C175,25 225,25 225,40 C230,65 225,85 200,85 C175,85 170,65 175,40 Z" />
<!-- Rygsøjle (Columna vertebralis) -->
<path id="spine" class="anatomical-part" fill="none" stroke="url(#boneGrad)" stroke-width="12" stroke-dasharray="4,2"
d="M200,85 L200,320" />
<!-- Kraveben (Clavicula) -->
<path id="clavicle-left" class="anatomical-part" fill="none" stroke="url(#boneGrad)" stroke-width="5"
d="M200,120 Q160,115 140,130" />
<path id="clavicle-right" class="anatomical-part" fill="none" stroke="url(#boneGrad)" stroke-width="5"
d="M200,120 Q240,115 260,130" />
<!-- Brystben (Sternum) -->
<path id="sternum" class="anatomical-part" fill="url(#boneGrad)" stroke="#475569"
d="M195,125 L205,125 L203,195 L197,195 Z" />
<!-- Ribben (Costae) -->
<g id="ribs" class="anatomical-part" stroke="url(#boneGrad)" stroke-width="3" fill="none">
<path d="M195,135 Q150,140 160,180 Q195,190 197,185" />
<path d="M205,135 Q250,140 240,180 Q205,190 203,185" />
<path d="M195,150 Q140,160 155,205 Q195,215 197,205" />
<path d="M205,150 Q260,160 245,205 Q205,215 203,205" />
<path d="M195,170 Q140,180 155,230 Q195,235 197,225" />
<path d="M205,170 Q260,180 245,230 Q205,235 203,225" />
</g>
<!-- Bækken (Pelvis) -->
<path id="pelvis" class="anatomical-part" fill="url(#boneGrad)" stroke="#475569"
d="M160,290 C150,300 155,345 185,345 C195,345 200,335 200,325 C200,335 205,345 215,345 C245,345 250,300 240,290 C220,280 180,280 160,290 Z" />
<!-- Overarmsknogle (Humerus) -->
<path id="humerus-left" class="anatomical-part" fill="url(#boneGrad)" stroke="#475569"
d="M135,135 Q125,200 120,225" stroke-width="10" stroke-linecap="round" />
<path id="humerus-right" class="anatomical-part" fill="url(#boneGrad)" stroke="#475569"
d="M265,135 Q275,200 280,225" stroke-width="10" stroke-linecap="round" />
<!-- Underarm (Radius & Ulna) -->
<g id="forearm-left" class="anatomical-part" stroke="url(#boneGrad)" stroke-width="5" stroke-linecap="round">
<line x1="118" y1="230" x2="105" y2="310"/>
<line x1="123" y1="230" x2="110" y2="310"/>
</g>
<g id="forearm-right" class="anatomical-part" stroke="url(#boneGrad)" stroke-width="5" stroke-linecap="round">
<line x1="282" y1="230" x2="295" y2="310"/>
<line x1="277" y1="230" x2="290" y2="310"/>
</g>
<!-- Lårben (Femur) -->
<path id="femur-left" class="anatomical-part" fill="url(#boneGrad)" stroke="#475569"
d="M175,340 Q165,430 165,490" stroke-width="14" stroke-linecap="round" />
<path id="femur-right" class="anatomical-part" fill="url(#boneGrad)" stroke="#475569"
d="M225,340 Q235,430 235,490" stroke-width="14" stroke-linecap="round" />
<!-- Knæskal (Patella) -->
<circle id="patella-left" class="anatomical-part" cx="165" cy="495" r="7" fill="url(#boneGrad)" stroke="#475569"/>
<circle id="patella-right" class="anatomical-part" cx="235" cy="495" r="7" fill="url(#boneGrad)" stroke="#475569"/>
<!-- Underben (Tibia & Fibula) -->
<g id="shin-left" class="anatomical-part" stroke="url(#boneGrad)" stroke-width="6" stroke-linecap="round">
<line x1="165" y1="505" x2="160" y2="640" stroke-width="8"/>
<line x1="155" y1="510" x2="150" y2="635" stroke-width="4"/>
</g>
<g id="shin-right" class="anatomical-part" stroke="url(#boneGrad)" stroke-width="6" stroke-linecap="round">
<line x1="235" y1="505" x2="240" y2="640" stroke-width="8"/>
<line x1="245" y1="510" x2="250" y2="635" stroke-width="4"/>
</g>
</g>
<!-- 2. ORGANLAG -->
<g id="group-organs">
<!-- Hjerte -->
<path id="heart" class="anatomical-part" fill="#e11d48" stroke="#9f1239"
d="M195,160 C185,150 170,160 185,180 L195,190 L205,180 C220,160 205,150 195,160 Z" />
<!-- Lunger -->
<path id="lungs-left" class="anatomical-part" fill="#f43f5e" opacity="0.6" stroke="#be123c"
d="M185,140 C170,140 150,165 155,210 C165,225 185,215 185,200 Z" />
<path id="lungs-right" class="anatomical-part" fill="#f43f5e" opacity="0.6" stroke="#be123c"
d="M215,140 C230,140 250,165 245,210 C235,225 215,215 215,200 Z" />
<!-- Mavesæk -->
<path id="stomach" class="anatomical-part" fill="url(#organGrad)" stroke="#c2410c"
d="M190,210 C170,215 175,250 205,250 C215,250 215,230 200,220 Z" />
<!-- Tarme -->
<rect id="intestines" class="anatomical-part" x="175" y="255" width="50" height="40" rx="8"
fill="#ea580c" opacity="0.8" stroke="#9a3412" />
</g>
<!-- 3. MUSKELLAG -->
<g id="group-muscles">
<!-- Skuldermuskel (Deltoideus) -->
<path id="deltoid-left" class="anatomical-part" fill="url(#muscleGrad)" opacity="0.85"
d="M145,120 C125,130 120,165 130,175 C135,165 145,140 150,130 Z" />
<path id="deltoid-right" class="anatomical-part" fill="url(#muscleGrad)" opacity="0.85"
d="M255,120 C275,130 280,165 270,175 C265,165 255,140 250,130 Z" />
<!-- Den store brystmuskel (Pectoralis major) -->
<path id="pectoralis" class="anatomical-part" fill="url(#muscleGrad)" opacity="0.85"
d="M150,135 Q200,140 200,175 Q150,180 145,145 Z M250,135 Q200,140 200,175 Q250,180 255,145 Z" />
<!-- Biceps brachii -->
<path id="biceps-left" class="anatomical-part" fill="url(#muscleGrad)" opacity="0.85"
d="M125,170 Q115,200 123,225 Q133,205 125,170 Z" />
<path id="biceps-right" class="anatomical-part" fill="url(#muscleGrad)" opacity="0.85"
d="M275,170 Q285,200 277,225 Q267,205 275,170 Z" />
<!-- Mavemuskler (Rectus abdominis) -->
<rect id="abs" class="anatomical-part" x="185" y="195" width="30" height="85" rx="5"
fill="url(#muscleGrad)" opacity="0.8" stroke="#b91c1c" stroke-width="1" />
<!-- Lårmuskel (Quadriceps) -->
<path id="quads-left" class="anatomical-part" fill="url(#muscleGrad)" opacity="0.85"
d="M155,340 C145,390 150,460 162,485 C175,460 180,390 170,340 Z" />
<path id="quads-right" class="anatomical-part" fill="url(#muscleGrad)" opacity="0.85"
d="M245,340 C255,390 250,460 238,485 C225,460 220,390 230,340 Z" />
<!-- Lægmuskel (Gastrocnemius) -->
<path id="calves-left" class="anatomical-part" fill="url(#muscleGrad)" opacity="0.85"
d="M152,510 C140,540 145,590 157,620 C167,590 170,540 162,510 Z" />
<path id="calves-right" class="anatomical-part" fill="url(#muscleGrad)" opacity="0.85"
d="M248,510 C260,540 255,590 243,620 C233,590 230,540 238,510 Z" />
</g>
<!-- 4. FYSIOLOGISKE PARTIKLER (Dynamisk animation af blod/ilt) -->
<g id="group-particles"></g>
</svg>
</div>
</main>
<!-- HØJRE PANEL: Detaljeret Information & Pædagogiske Data -->
<aside class="info-panel" id="info-panel">
<!-- Standard visningskort -->
<div id="selection-card" class="card">
<div class="term-names">
<div style="display:flex; justify-content:space-between; align-items:flex-start;">
<div class="term-title" id="display-primary-name">Vælg en kropsdel</div>
<button id="btn-favorite" style="background:none; border:none; font-size:1.2rem; cursor:pointer;" title="Marker som favorit / pensum">☆</button>
</div>
<div class="term-sub" id="display-secondary-name">Klik på skelet, muskler eller organer</div>
<div style="margin-top: 0.5rem;"><span id="display-type-badge" class="badge">Vejledning</span></div>
</div>
<div class="meta-field">
<div class="meta-field-label">Anatomisk Placering</div>
<div class="meta-field-content" id="display-location">Brug musen eller touch til at undersøge kroppen for- og bagfra.</div>
</div>
<div class="meta-field">
<div class="meta-field-label">Funktion & Biomekanik</div>
<div class="meta-field-content" id="display-function">-</div>
</div>
<div class="meta-field" id="muscle-details" style="display:none;">
<div class="meta-field-label">Udspring & Hæfte (Origo / Insertio)</div>
<div class="meta-field-content" id="display-attachments">-</div>
</div>
<div class="meta-field">
<div class="meta-field-label" style="color: var(--accent);">Idræts- & Træningsfaglig Relevans</div>
<div class="meta-field-content" id="display-sports-relevance">-</div>
</div>
<div class="meta-field">
<div class="meta-field-label">Relaterede Strukturer</div>
<div class="meta-field-content" id="display-related">Ingen valgt</div>
</div>
</div>
<!-- Idrætsfysiologisk monitor (Belastningsprofiler) -->
<div class="card" id="physiology-monitor-card">
<h3>
<span>Aktivitetsprofil</span>
<span style="font-size:0.75rem; color:var(--text-muted); font-weight:normal;">Illustrativ model</span>
</h3>
<div style="display: flex; gap: 0.3rem; margin-bottom: 0.75rem;">
<button class="state-btn active" data-state="rest" style="flex:1;">Hvile</button>
<button class="state-btn" data-state="moderate" style="flex:1;">Moderat</button>
<button class="state-btn" data-state="intense" style="flex:1;">Maksimal</button>
</div>
<div class="stat-row">
<span>Puls (HR):</span>
<strong id="stat-hr">65 slag/min</strong>
</div>
<div class="stat-bar-bg"><div class="stat-bar-fill" id="bar-hr" style="width: 25%;"></div></div>
<div class="stat-row">
<span>Respiration:</span>
<strong id="stat-ventilation">12 åndedræt/min</strong>
</div>
<div class="stat-bar-bg"><div class="stat-bar-fill" id="bar-ventilation" style="width: 20%;"></div></div>
<div class="stat-row">
<span>Muskel-blodflow:</span>
<strong id="stat-bloodflow">15-20% af MV</strong>
</div>
<div class="stat-bar-bg"><div class="stat-bar-fill" id="bar-bloodflow" style="width: 20%;"></div></div>
</div>
</aside>
</div>
<!-- ==========================================================================
JAVASCRIPT: CENTRAL DATASTRUKTUR & MODULÆR LOGIK
========================================================================== -->
<script>
/* ==========================================================================
18. CENTRAL DATASTRUKTUR (Udvidelsesvenlig og let at flytte til JSON)
========================================================================== */
const ANATOMY_DATABASE = {
// EXTENSION POINT: Tilføj nye knogler her
"skull": {
id: "skull",
type: "bone",
names: { da: "Kranium", en: "Cranium / Skull", la: "Cranium" },
location: "Hovedets skeletstruktur.",
function: "Beskytter hjernen og danner fæste for tygge- og nakkemuskulatur.",
sportsRelevance: "Hjernerystelser (commotio cerebri) er et vigtigt emne i idrætsundervisning (kontaktidrætter som fodbold og håndbold). Forebyggelse og 'return-to-play'-protokoller er centrale.",
related: ["spine"]
},
"femur-left": {
id: "femur-left",
type: "bone",
names: { da: "Lårben (venstre)", en: "Left Femur", la: "Femur sinister" },
location: "Overbenet, forbinder hofteleddet med knæleddet.",
function: "Kroppens længste og stærkeste rørknogle. Vægtbærende løftestang i gang, løb og hop.",
sportsRelevance: "Udsættes for massive stødkræfter (3-5x kropsvægt ved løb). Central for kraftoverførsel i squat, afsæt og sprint.",
related: ["pelvis", "patella-left", "quads-left"]
},
"femur-right": {
id: "femur-right",
type: "bone",
names: { da: "Lårben (højre)", en: "Right Femur", la: "Femur dexter" },
location: "Overbenet, forbinder hofteleddet med knæleddet.",
function: "Kroppens længste og stærkeste rørknogle. Vægtbærende løftestang.",
sportsRelevance: "Væsentlig for løftegeometri og vinkelmoment ved spark og sprint.",
related: ["pelvis", "patella-right", "quads-right"]
},
"patella-left": {
id: "patella-left",
type: "bone",
names: { da: "Knæskal (venstre)", en: "Left Patella", la: "Patella sinistra" },
location: "Foran knæleddet, indlejret i quadriceps-senen.",
function: "Sesamknogle, der øger quadriceps-musklens vægtstangsarm over knæet.",
sportsRelevance: "Udsat for patellofemoralt smertesyndrom og springerknæ (patellar tendinopati) ved stor mængde springtræning.",
related: ["femur-left", "quads-left", "shin-left"]
},
"patella-right": {
id: "patella-right",
type: "bone",
names: { da: "Knæskal (højre)", en: "Right Patella", la: "Patella dextra" },
location: "Foran knæleddet, indlejret i quadriceps-senen.",
function: "Sesamknogle, der forbedrer ekstensionsmekanismen.",
sportsRelevance: "Biomekanisk fordel i knæstræk.",
related: ["femur-right", "quads-right", "shin-right"]
},
"shin-left": {
id: "shin-left",
type: "bone",
names: { da: "Skinneben & Lægben", en: "Tibia and Fibula", la: "Tibia et Fibula" },
location: "Underbenet.",
function: "Tibia bærer kropsvægten; Fibula yder stabilitet og danner ankelgaflen.",
sportsRelevance: "Skinnebensbetændelse (tibiaperiostit) er en af de hyppigste overbelastningsskader i løb og idræt ved for hurtig progressionsstigning.",
related: ["femur-left", "patella-left", "calves-left"]
},
"shin-right": {
id: "shin-right",
type: "bone",
names: { da: "Skinneben & Lægben", en: "Tibia and Fibula", la: "Tibia et Fibula" },
location: "Underbenet.",
function: "Vægtoverførsel og stabilitet til foden.",
sportsRelevance: "Stødabsorbering i landingsfaser.",
related: ["femur-right", "patella-right", "calves-right"]
},
"pelvis": {
id: "pelvis",
type: "bone",
names: { da: "Bækken", en: "Pelvis", la: "Pelvis" },
location: "Kroppens centrum, forbinder rygsøjlen med underekstremiteterne.",
function: "Fundament for kropsholdning, kraftoverførsel mellem over- og underkrop.",
sportsRelevance: "Bækkenets hældning (anterior/posterior tilt) påvirker lumbal lordose og stabiliteten i squat og løb.",
related: ["spine", "femur-left", "femur-right", "abs"]
},
"humerus-left": {
id: "humerus-left",
type: "bone",
names: { da: "Overarmsknogle (venstre)", en: "Left Humerus", la: "Humerus sinister" },
location: "Overarmen mellem skulderled og albueled.",
function: "Armens primære løftestang ved kast, træk og skub.",
sportsRelevance: "Udsættes for ekstreme torsionskræfter under kastebevægelser (håndbold, spydkast, baseball).",
related: ["clavicle-left", "deltoid-left", "biceps-left", "forearm-left"]
},
"humerus-right": {
id: "humerus-right",
type: "bone",
names: { da: "Overarmsknogle (højre)", en: "Right Humerus", la: "Humerus dexter" },
location: "Overarmen mellem skulderled og albueled.",
function: "Kraftfuld bevægelse i skulder og albue.",
sportsRelevance: "Kastearmens vigtigste knogle ved overhead-bevægelser.",
related: ["clavicle-right", "deltoid-right", "biceps-right", "forearm-right"]
},
"forearm-left": {
id: "forearm-left",
type: "bone",
names: { da: "Spoleben & Albueben", en: "Radius and Ulna", la: "Radius et Ulna" },
location: "Underarmen mellem albue og håndrod.",
function: "Muliggør pronation og supination (rotation af hånden) samt fleksion/ekstension.",
sportsRelevance: "Ketsjersport (tennisalbue/golfalbue), gymnastikgreb i barren og styrketræning.",
related: ["humerus-left", "biceps-left"]
},
"forearm-right": {
id: "forearm-right",
type: "bone",
names: { da: "Spoleben & Albueben", en: "Radius and Ulna", la: "Radius et Ulna" },
location: "Underarmen mellem albue og håndrod.",
function: "Rotation og fleksion i armen.",
sportsRelevance: "Overførsel af gribestyrke og modtagelse ved spring og redskabsgymnastik.",
related: ["humerus-right", "biceps-right"]
},
"spine": {
id: "spine",
type: "bone",
names: { da: "Rygsøjle", en: "Spinal Column", la: "Columna vertebralis" },
location: "Aksialt skelet fra kraniet til halebenet.",
function: "Understøtter kropsvægt, beskytter rygmarven, muliggør bevægelse i tre planer.",
sportsRelevance: "Kerne i al kraftoverførsel. Korrekt kropsholdning og neutral rygsøjle under tunge løft minimerer diskustrykket.",
related: ["skull", "pelvis", "abs"]
},
"ribs": {
id: "ribs",
type: "bone",
names: { da: "Ribben", en: "Ribs", la: "Costae" },
location: "Brystkassen (thorax).",
function: "Beskytter hjerte og lunger; udvider sig ved forceret vejrtrækning.",
sportsRelevance: "Interkostalmuskler udvider thorax under hårdt aerobt arbejde med stor minutventilation.",
related: ["sternum", "lungs-left", "lungs-right"]
},
"sternum": {
id: "sternum",
type: "bone",
names: { da: "Brystben", en: "Breastbone", la: "Sternum" },
location: "Midt på brystkassens forside.",
function: "Fortil forankring af ribbene og brystmuskulaturen.",
sportsRelevance: "Fæstepunkt for pectoralis major ved bænkpres og armstrækninger.",
related: ["ribs", "pectoralis"]
},
"clavicle-left": {
id: "clavicle-left",
type: "bone",
names: { da: "Kraveben (venstre)", en: "Clavicle", la: "Clavicula sinistra" },
location: "Mellem sternum og scapula.",
function: "Afstiver skulderbæltet og holder overarmen fri af kroppen.",
sportsRelevance: "Hyppig brudskade ved fald på udstrakt arm i fx cykling, rulleskøjter og fodbold.",
related: ["sternum", "deltoid-left"]
},
"clavicle-right": {
id: "clavicle-right",
type: "bone",
names: { da: "Kraveben (højre)", en: "Clavicle", la: "Clavicula dextra" },
location: "Mellem sternum og scapula.",
function: "Afstiver skulderbæltet.",
sportsRelevance: "Skulderstabilitet ved kontaktidræt.",
related: ["sternum", "deltoid-right"]
},
// EXTENSION POINT: Tilføj nye muskler her
"biceps-left": {
id: "biceps-left",
type: "muscle",
names: { da: "Den to-hovedede armbøjer", en: "Biceps", la: "M. biceps brachii" },
location: "Overarmens forside.",
function: "Fleksion i albueleddet og supination af underarmen.",
attachments: "Origo: Skulderbladet (scapula). Insertio: Spolebenet (tuberositas radii).",
sportsRelevance: "Aktiv ved klatring, pull-ups, kajakroning og brydning. Agonist ved albuefleksion.",
related: ["humerus-left", "forearm-left", "deltoid-left"]
},
"biceps-right": {
id: "biceps-right",
type: "muscle",
names: { da: "Den to-hovedede armbøjer", en: "Biceps", la: "M. biceps brachii" },
location: "Overarmens forside.",
function: "Fleksion og supination i overekstremiteten.",
attachments: "Origo: Scapula. Insertio: Tuberositas radii.",
sportsRelevance: "Essentiel trækmuskel i ketsjersport og klatring.",
related: ["humerus-right", "forearm-right", "deltoid-right"]
},
"deltoid-left": {
id: "deltoid-left",
type: "muscle",
names: { da: "Skuldermusklen (venstre)", en: "Deltoid", la: "M. deltoideus" },
location: "Omkranser skulderleddet.",
function: "Abduktion, fleksion og ekstension af armen.",
attachments: "Origo: Clavicula & scapula. Insertio: Tuberositas deltoidea på humerus.",
sportsRelevance: "Banebrydende i svømning (crawl/butterfly), volleyball-slag og kasteidræt.",
related: ["humerus-left", "clavicle-left"]
},
"deltoid-right": {
id: "deltoid-right",
type: "muscle",
names: { da: "Skuldermusklen (højre)", en: "Deltoid", la: "M. deltoideus" },
location: "Omkranser skulderleddet.",
function: "Løfter armen op til vandret og over.",
attachments: "Origo: Clavicula & scapula. Insertio: Humerus.",
sportsRelevance: "Kasteafvikling i håndbold og spydkast.",
related: ["humerus-right", "clavicle-right"]
},
"pectoralis": {
id: "pectoralis",
type: "muscle",
names: { da: "Den store brystmuskel", en: "Chest Muscle", la: "M. pectoralis major" },
location: "Brystkassens forside.",
function: "Adduktion og indadrotation af overarmen, horisontal fleksion.",
attachments: "Origo: Clavicula, sternum og ribbensbruske. Insertio: Humerus.",
sportsRelevance: "Kernetransformator ved skub (rugby, brystsvømning, kampsport og armbøjninger).",
related: ["sternum", "clavicle-left", "clavicle-right", "humerus-left", "humerus-right"]
},
"abs": {
id: "abs",
type: "muscle",
names: { da: "Den lige bugmuskel", en: "Six-pack / Rectus Abdominis", la: "M. rectus abdominis" },
location: "Forsiden af maven mellem brystben og skamben.",
function: "Fleksion af rygsøjlen og stabilisering af bugtrykket (intraabdominalt tryk).",
attachments: "Origo: Os pubis. Insertio: Processus xiphoideus og ribbensbruske 5-7.",
sportsRelevance: "Vigtig del af 'core'-stabilitet. Overfører energi fra ben til arme ved rotation, spark og kast.",
related: ["pelvis", "sternum", "spine"]
},
"quads-left": {
id: "quads-left",
type: "muscle",
names: { da: "Den firehovedede knæstrækker", en: "Quadriceps", la: "M. quadriceps femoris" },
location: "Lårets forside.",
function: "Ekstension i knæleddet; rectus femoris bøjer desuden i hoften.",
attachments: "Origo: Ilium og femur. Insertio: Tuberositas tibiae via patellasenen.",
sportsRelevance: "Hovedmotor i squat, cykling, hop, deceleration og spark. Eksentrisk belastning ved landing.",
related: ["femur-left", "patella-left", "shin-left"]
},
"quads-right": {
id: "quads-right",
type: "muscle",
names: { da: "Den firehovedede knæstrækker", en: "Quadriceps", la: "M. quadriceps femoris" },
location: "Lårets forside.",
function: "Kraftfuld ekstension af knæet.",
attachments: "Origo: Ilium og femur. Insertio: Tuberositas tibiae.",
sportsRelevance: "Afgørende for eksplosiv styrke og springhøjde.",
related: ["femur-right", "patella-right", "shin-right"]
},
"calves-left": {
id: "calves-left",
type: "muscle",
names: { da: "Den to-hovedede lægmuskel", en: "Calf muscle", la: "M. gastrocnemius" },
location: "Underbenets bagside.",
function: "Plantarfleksion i ankelleddet og fleksion i knæleddet.",
attachments: "Origo: Femurkondylerne. Insertio: Hælbenet via akillessenen.",
sportsRelevance: "Elastisk energiafgivelse ved løb og hop. Udsat for akillesseneruptur og forstrækninger ved hurtige retningsskift.",
related: ["femur-left", "shin-left"]
},
"calves-right": {
id: "calves-right",
type: "muscle",
names: { da: "Den to-hovedede lægmuskel", en: "Calf muscle", la: "M. gastrocnemius" },
location: "Underbenets bagside.",
function: "Plantarfleksion og afsæt.",
attachments: "Origo: Femur. Insertio: Tuber calcanei.",
sportsRelevance: "Vigtigste muskel for afsæt fra forfoden i sprint.",
related: ["femur-right", "shin-right"]
},
// EXTENSION POINT: Tilføj nye organer her
"heart": {
id: "heart",
type: "organ",
names: { da: "Hjertet", en: "Heart", la: "Cor" },
location: "I brysthulen mellem lungerne, let forskudt mod venstre.",
function: "Muskelpumpe, der pumper iltet blod ud til arbejdende muskler og afiltet blod til lungerne.",
sportsRelevance: "Træningsfysiologisk kernestruktur: Ved udholdenhedstræning øges slagvolumen (venstre ventrikels volumen), hvilket sænker hvilepulsen (sportsmandshjerte).",
related: ["lungs-left", "lungs-right"]
},
"lungs-left": {
id: "lungs-left",
type: "organ",
names: { da: "Venstre Lunge", en: "Left Lung", la: "Pulmo sinister" },
location: "Brysthulen.",
function: "Gasudveksling (optagelse af O2 og udskillelse af CO2).",
sportsRelevance: "Respirationsdybde og frekvens øges drastisk under arbejde for at udlufte CO2 og opretholde syre-base-balancen.",
related: ["heart", "ribs"]
},
"lungs-right": {
id: "lungs-right",
type: "organ",
names: { da: "Højre Lunge", en: "Right Lung", la: "Pulmo dexter" },
location: "Brysthulen.",
function: "Gasudveksling.",
sportsRelevance: "Begrænser sjældent iltoptagelsen hos raske, men vital for lungeventilation (VE) under VO2max-test.",
related: ["heart", "ribs"]
},
"stomach": {
id: "stomach",
type: "organ",
names: { da: "Mavesækken", en: "Stomach", la: "Gaster / Ventriculus" },
location: "Øvre venstre del af bughulen.",
function: "Nedbryder føde kemisk og mekanisk.",
sportsRelevance: "Under hård fysisk aktivitet omdirigeres blodet væk fra maven mod de arbejdende muskler. Derfor tåles tunge måltider tæt på træning dårligt.",
related: ["intestines"]
},
"intestines": {
id: "intestines",
type: "organ",
names: { da: "Tarmene", en: "Intestines", la: "Intestina" },
location: "Nederste del af bughulen.",
function: "Optagelse af næringsstoffer, væske og elektrolytter.",
sportsRelevance: "Kulhydrat- og elektrolytoptag under langdistanceløb og triatlon for at forhindre 'at gå kold'.",
related: ["stomach"]
}
};
/* ==========================================================================
BEVÆGELSES-DATABASE (Modul 10: Bevægelse)
========================================================================== */
const MOVEMENTS_DATABASE = {
"squat": {
id: "squat",
title: "Squat (Knæbøjning)",
description: "En af de mest fundamentale basisøvelser i idræt og styrketræning for underekstremiteten.",
agonists: ["quads-left", "quads-right"],
synergists: ["calves-left", "calves-right", "abs"],
bonesInvolved: ["femur-left", "femur-right", "patella-left", "patella-right", "pelvis", "shin-left", "shin-right"],
biomechanics: "Samtidig fleksion i hofte og knæ (excentrisk fase) efterfulgt af ekstension (koncentrisk fase). Kræver god ankelmobilitet for at bevare neutral ryg."
},
"biceps-curl": {
id: "biceps-curl",
title: "Albuefleksion (Biceps Curl)",
description: "Klassisk isolationsbevægelse for armens bøjemuskulatur.",
agonists: ["biceps-left", "biceps-right"],
synergists: ["forearm-left", "forearm-right"],
bonesInvolved: ["humerus-left", "humerus-right", "forearm-left", "forearm-right"],
biomechanics: "Kroppen fastholder overarmen stabilt, mens albuen bøjes mod modstand. Ren 1-leds hængselledsbevægelse."
},
"shoulder-press": {
id: "shoulder-press",
title: "Skulderpres (Overhead Press)",
description: "Flerledsøvelse til udvikling af vertikal presstyrke over hovedet.",
agonists: ["deltoid-left", "deltoid-right"],
synergists: ["abs", "pectoralis"],
bonesInvolved: ["clavicle-left", "clavicle-right", "humerus-left", "humerus-right", "spine"],
biomechanics: "Abduktion og opadrotation af scapula kombineret med albueekstension. Kræver stærk stabilitet i truncus (core)."
}
};
/* ==========================================================================
FYSIOLOGISKE FORLØB (Modul 11: Iltens vej & Kredsløb)
========================================================================== */
const PROCESSES_DATABASE = {
"oxygen": {
id: "oxygen",
title: "Iltens vej (Fra luft til muskelceller)",
steps: [
{ name: "1. Indånding", desc: "Luften trækkes ind gennem næse/mund og ledes via luftrøret til lungerne.", target: "lungs-left" },
{ name: "2. Alveoler & Diffusion", desc: "Ilt diffunderer over den tynde alveolemembran og binder sig til hæmoglobin i de røde blodlegemer.", target: "lungs-right" },
{ name: "3. Hjertets venstre side", desc: "Det iltrige blod ledes via lungevenerne til venstre forkammer og hjertekammer.", target: "heart" },
{ name: "4. Systemisk kredsløb", desc: "Hjertet pumper blodet ud gennem aorta til kroppens store arterier.", target: "spine" },
{ name: "5. Kapillærer & Muskelceller", desc: "Ilten afleveres til de arbejdende lårmuskler til brug i mitokondriernes aerobe forbrænding (ATP-dannelse).", target: "quads-left" }
],
pathCoords: [
{ x: 200, y: 70 },
{ x: 195, y: 140 },
{ x: 195, y: 180 },
{ x: 200, y: 300 },
{ x: 165, y: 400 }
]
},
"circulation": {
id: "circulation",
title: "Det store og lille kredsløb",
steps: [
{ name: "Lille kredsløb (Lungekredsløb)", desc: "Afiltet blod pumpes fra højre hjertekammer til lungerne for at afgive CO2 og hente O2.", target: "lungs-left" },
{ name: "Hjertets pumpe", desc: "Venstre hjertekammer genererer det høje tryk (systolisk tryk) til kroppen.", target: "heart" },
{ name: "Det store kredsløb (Kropskredsløb)", desc: "Arterier fordeler blodet til organer og skeletmuskulatur, hvorefter vener bringer det tilbage.", target: "quads-right" }
],
pathCoords: [
{ x: 195, y: 180 },
{ x: 220, y: 180 },
{ x: 195, y: 180 },
{ x: 235, y: 420 },
{ x: 200, y: 320 },
{ x: 195, y: 180 }
]
}
};
/* ==========================================================================
QUIZ OG LÆRINGSAKTIVITETER (Modul 13: Træn din viden)
========================================================================== */
const QUIZ_QUESTIONS = [
{
question: "Klik på lårbenet (Femur) på kroppen:",
expectedId: "femur-left",
altExpectedId: "femur-right",
type: "click",
explanation: "Lårbenet (Femur) er kroppens stærkeste knogle og bærer kropsvægten under gang og løb."
},
{
question: "Hvilken muskel er den primære agonist ved fleksion (bøjning) af albueleddet?",
options: [
{ label: "M. triceps brachii", correct: false },
{ label: "M. biceps brachii", correct: true },
{ label: "M. deltoideus", correct: false },
{ label: "M. quadriceps femoris", correct: false }
],
type: "choice",
targetId: "biceps-left",
explanation: "M. biceps brachii flekterer albuen og supinerer underarmen."
},
{
question: "Find og klik på den knogle, hvor patellofemoralt smertesyndrom og springerknæ kan opstå:",
expectedId: "patella-left",
altExpectedId: "patella-right",
type: "click",
explanation: "Patella (knæskallen) fungerer som vægtstangsarm for quadriceps og er stærkt udsat ved springtræning."
}
];
/* ==========================================================================
APP STATE MANAGEMENT
========================================================================== */
const AppState = {
lang: 'da',
langMode: 'dual',
currentMode: 'explore', // explore, movement, physiology, training
selectedId: null,
favorites: new Set(JSON.parse(localStorage.getItem('krop_idraet_favs') || '[]')),
physiologyState: 'rest', // rest, moderate, intense
processAnimTimer: null,
currentQuizIndex: 0
};
/* ==========================================================================
LANGUAGE & TERMINOLOGY MODULE
========================================================================== */
const LanguageModule = {
formatName(item) {
if (!item || !item.names) return '';
const da = item.names.da;
const la = item.names.la;
const en = item.names.en;
if (AppState.langMode === 'primary') {
return item.names[AppState.lang] || da;
} else if (AppState.langMode === 'dual') {
return `${da} <span style="font-weight:normal; font-style:italic; color:var(--text-muted);">(${la})</span>`;
} else {
return `${da} | <em>${la}</em> | <small>${en}</small>`;
}
}
};
/* ==========================================================================
RENDERER & VIEW MODULE
========================================================================== */
const ViewModule = {
init() {
this.bindEvents();
this.updateFavoritesCount();
this.renderSubpanel();
},
bindEvents() {
// Klik på anatomiske dele i SVG
document.querySelectorAll('.anatomical-part').forEach(el => {
el.addEventListener('click', (e) => {
const id = el.id;
if (AppState.currentMode === 'training') {
QuizModule.handleElementClick(id);
} else {
this.selectElement(id);
}
});
});
// Sprogvælger
document.getElementById('lang-select').addEventListener('change', (e) => {
AppState.lang = e.target.value;
this.refreshInfoPanel();
});
document.getElementById('lang-mode').addEventListener('change', (e) => {
AppState.langMode = e.target.value;
this.refreshInfoPanel();
});
// Hovednavigation
document.querySelectorAll('.nav-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.nav-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
AppState.currentMode = btn.dataset.mode;
this.renderSubpanel();
this.resetHighlights();
});
});
// Lagtoggles
document.getElementById('layer-skeleton').addEventListener('change', (e) => {
document.getElementById('group-skeleton').style.display = e.target.checked ? 'block' : 'none';
});
document.getElementById('layer-muscles').addEventListener('change', (e) => {
document.getElementById('group-muscles').style.display = e.target.checked ? 'block' : 'none';
});
document.getElementById('layer-organs').addEventListener('change', (e) => {
document.getElementById('group-organs').style.display = e.target.checked ? 'block' : 'none';
});
// Visningsvinkler (Forfra/Bagfra/Split)
document.getElementById('view-anterior').addEventListener('click', () => {
this.setViewAngle('anterior');
});
document.getElementById('view-posterior').addEventListener('click', () => {
this.setViewAngle('posterior');
});
document.getElementById('view-split').addEventListener('click', () => {
this.toggleSplitView();
});
document.getElementById('reset-zoom').addEventListener('click', () => {
this.resetView();
});
// Favorit-knap
document.getElementById('btn-favorite').addEventListener('click', () => {
if (!AppState.selectedId) return;
if (AppState.favorites.has(AppState.selectedId)) {
AppState.favorites.delete(AppState.selectedId);
} else {
AppState.favorites.add(AppState.selectedId);
}
localStorage.setItem('krop_idraet_favs', JSON.stringify(Array.from(AppState.favorites)));
this.updateFavoritesCount();
this.updateFavButtonState();
});
// Favorit-filterknap i topbaren
document.getElementById('toggle-favorites').addEventListener('click', () => {
this.showFavoritesList();
});
// Global søgning
const searchInput = document.getElementById('global-search');
const resultsBox = document.getElementById('search-results');
searchInput.addEventListener('input', (e) => {
const q = e.target.value.toLowerCase().trim();
if (q.length < 2) {
resultsBox.style.display = 'none';
return;
}
const matches = Object.values(ANATOMY_DATABASE).filter(item => {
return item.names.da.toLowerCase().includes(q) ||
item.names.en.toLowerCase().includes(q) ||
item.names.la.toLowerCase().includes(q);
});
if (matches.length === 0) {
resultsBox.innerHTML = '<div style="padding:0.6rem; font-size:0.85rem; color:var(--text-muted);">Ingen resultater fundet</div>';
} else {
resultsBox.innerHTML = matches.map(m => `
<div class="search-item" data-id="${m.id}" style="padding:0.6rem 0.8rem; cursor:pointer; font-size:0.875rem; border-bottom:1px solid var(--border-color);">
<strong>${m.names.da}</strong> <span style="color:var(--text-muted);">(${m.names.la})</span>
</div>
`).join('');
}
resultsBox.style.display = 'block';
resultsBox.querySelectorAll('.search-item').forEach(item => {
item.addEventListener('click', () => {
const id = item.dataset.id;
searchInput.value = '';
resultsBox.style.display = 'none';
ViewModule.selectElement(id);
});
});
});
// Fysiologiske knapper (Hvile / Moderat / Hård)
document.querySelectorAll('.state-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.state-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
AppState.physiologyState = btn.dataset.state;
this.updatePhysiologyDisplay();
});
});
},
selectElement(id) {
AppState.selectedId = id;
document.querySelectorAll('.anatomical-part').forEach(p => p.classList.remove('active', 'dimmed'));
const el = document.getElementById(id);
if (el) {
el.classList.add('active');
// Tag højde for at åbne mobilpanelet hvis lukket
document.getElementById('info-panel').classList.add('open');
}
this.refreshInfoPanel();
},
refreshInfoPanel() {
if (!AppState.selectedId || !ANATOMY_DATABASE[AppState.selectedId]) {
return;
}
const data = ANATOMY_DATABASE[AppState.selectedId];
document.getElementById('display-primary-name').innerHTML = LanguageModule.formatName(data);
document.getElementById('display-secondary-name').textContent = `${data.names.en} (Engelsk)`;
const badge = document.getElementById('display-type-badge');
badge.textContent = data.type;
badge.className = `badge ${data.type}`;
document.getElementById('display-location').textContent = data.location;
document.getElementById('display-function').textContent = data.function;
document.getElementById('display-sports-relevance').textContent = data.sportsRelevance;
const muscleBox = document.getElementById('muscle-details');
if (data.type === 'muscle' && data.attachments) {
muscleBox.style.display = 'block';
document.getElementById('display-attachments').textContent = data.attachments;
} else {
muscleBox.style.display = 'none';
}
// Relaterede strukturer som klikbare tags
const relContainer = document.getElementById('display-related');
if (data.related && data.related.length > 0) {
relContainer.innerHTML = data.related.map(relId => {
const relItem = ANATOMY_DATABASE[relId];
return relItem ? `<span class="related-tag" data-rel="${relId}">${relItem.names.da}</span>` : '';
}).join('');
relContainer.querySelectorAll('.related-tag').forEach(tag => {
tag.addEventListener('click', () => {
this.selectElement(tag.dataset.rel);
});
});
} else {
relContainer.textContent = 'Ingen relaterede data';
}
this.updateFavButtonState();
},
updateFavButtonState() {
const btn = document.getElementById('btn-favorite');
if (AppState.favorites.has(AppState.selectedId)) {
btn.textContent = '⭐';
btn.title = 'Fjern fra favoritter / pensum';
} else {
btn.textContent = '☆';
btn.title = 'Marker som favorit / pensum';
}
},
updateFavoritesCount() {
document.getElementById('fav-count').textContent = AppState.favorites.size;
},
showFavoritesList() {
if (AppState.favorites.size === 0) {
alert('Du har ikke markeret nogen strukturer som favorit endnu. Klik på stjernen (☆) i informationspanelet.');
return;
}
let listStr = 'Gemte favoritter / pensumfokus:\n';
AppState.favorites.forEach(id => {
if (ANATOMY_DATABASE[id]) {
listStr += `• ${ANATOMY_DATABASE[id].names.da} (${ANATOMY_DATABASE[id].names.la})\n`;
}
});
alert(listStr);
},
resetHighlights() {
document.querySelectorAll('.anatomical-part').forEach(p => p.classList.remove('active', 'dimmed'));
clearInterval(AppState.processAnimTimer);
document.getElementById('group-particles').innerHTML = '';
},
resetView() {
const svg = document.getElementById('human-body-svg');
svg.style.transform = 'none';
document.getElementById('group-skeleton').setAttribute('transform', '');
document.getElementById('group-muscles').setAttribute('transform', '');
document.getElementById('group-organs').setAttribute('transform', '');
},
setViewAngle(angle) {
document.getElementById('view-anterior').classList.toggle('active', angle === 'anterior');
document.getElementById('view-posterior').classList.toggle('active', angle === 'posterior');
const svg = document.getElementById('human-body-svg');
if (angle === 'posterior') {
// Illustrativ rotation: dæmper forreste ribben og brystben, fremhæver columna
svg.style.transform = 'rotateY(180deg)';
} else {
svg.style.transform = 'none';
}
},
toggleSplitView() {
const btn = document.getElementById('view-split');
btn.classList.toggle('active');
const isSplit = btn.classList.contains('active');
if (isSplit) {
// Trækker lagene fra hinanden horisontalt ("eksploderet visning")
document.getElementById('group-skeleton').setAttribute('transform', 'translate(-60, 0)');
document.getElementById('group-organs').setAttribute('transform', 'translate(0, 0)');
document.getElementById('group-muscles').setAttribute('transform', 'translate(60, 0)');
} else {
this.resetView();
}
},
updatePhysiologyDisplay() {
const s = AppState.physiologyState;
const hr = document.getElementById('stat-hr');
const vent = document.getElementById('stat-ventilation');
const flow = document.getElementById('stat-bloodflow');
const bHr = document.getElementById('bar-hr');
const bVent = document.getElementById('bar-ventilation');
const bFlow = document.getElementById('bar-bloodflow');
if (s === 'rest') {
hr.textContent = '60-70 slag/min';
vent.textContent = '12-14 åndedræt/min';
flow.textContent = '15-20% af minutvolumen';
bHr.style.width = '25%';
bVent.style.width = '20%';
bFlow.style.width = '20%';
} else if (s === 'moderate') {
hr.textContent = '130-150 slag/min';
vent.textContent = '28-35 åndedræt/min';
flow.textContent = '65-75% af minutvolumen';
bHr.style.width = '65%';
bVent.style.width = '60%';
bFlow.style.width = '70%';
} else {
hr.textContent = '180-200 slag/min';
vent.textContent = '50-60 åndedræt/min';
flow.textContent = '85-88% af minutvolumen';
bHr.style.width = '95%';
bVent.style.width = '95%';
bFlow.style.width = '90%';
}
},
/* Subpanel der ændrer sig efter aktiv fane */
renderSubpanel() {
const container = document.getElementById('module-subpanel');
if (AppState.currentMode === 'explore') {
container.innerHTML = `
<div class="nav-section-title">Hurtigfokus</div>
<div style="font-size:0.85rem; color:var(--text-muted);">
Udforsk anatomi ved at klikke direkte på kroppen eller søge foroven.
</div>
`;
} else if (AppState.currentMode === 'movement') {
container.innerHTML = `
<div class="nav-section-title">Vælg Bevægelse</div>
<select id="movement-select" style="width:100%; margin-bottom:0.75rem;">
<option value="squat">Squat (Knæbøjning)</option>
<option value="biceps-curl">Biceps Curl (Albuefleksion)</option>
<option value="shoulder-press">Skulderpres</option>
</select>
<button id="btn-run-movement" class="active" style="width:100%;">⚡ Fremhæv Arbejdende Muskler</button>
`;
document.getElementById('btn-run-movement').addEventListener('click', () => {
const val = document.getElementById('movement-select').value;
MovementModule.demonstrate(val);
});
} else if (AppState.currentMode === 'physiology') {
container.innerHTML = `
<div class="nav-section-title">Fysiologisk Kredsløb</div>
<select id="process-select" style="width:100%; margin-bottom:0.75rem;">
<option value="oxygen">Iltens vej (Fra mund til muskel)</option>
<option value="circulation">Det store og lille kredsløb</option>
</select>
<div style="display:flex; gap:0.4rem;">
<button id="btn-start-process" class="active" style="flex:1;">▶ Start</button>
<button id="btn-stop-process" style="flex:1;">⏹ Nulstil</button>
</div>
<div id="process-step-info" style="margin-top:0.75rem; font-size:0.85rem;"></div>
`;
document.getElementById('btn-start-process').addEventListener('click', () => {
const proc = document.getElementById('process-select').value;
PhysiologyModule.startProcess(proc);
});
document.getElementById('btn-stop-process').addEventListener('click', () => {
PhysiologyModule.stopProcess();
});
} else if (AppState.currentMode === 'training') {
container.innerHTML = `
<div class="nav-section-title">Træn din viden</div>
<div id="quiz-container">
<p id="quiz-question" style="font-size:0.9rem; font-weight:600; margin-bottom:0.75rem;"></p>
<div id="quiz-options" style="display:flex; flex-direction:column; gap:0.4rem;"></div>
<div id="quiz-feedback" class="feedback-box"></div>
<button id="quiz-next-btn" style="margin-top:0.75rem; width:100%; display:none;">Næste spørgsmål →</button>
</div>
`;
QuizModule.loadQuestion(AppState.currentQuizIndex);
}
}
};
/* ==========================================================================
MOVEMENT MODULE (Modul 10: Bevægelse & Biomekanik)
========================================================================== */
const MovementModule = {
demonstrate(movementId) {
const mov = MOVEMENTS_DATABASE[movementId];
if (!mov) return;
// Dæmp alle strukturer
document.querySelectorAll('.anatomical-part').forEach(p => p.classList.add('dimmed'));
// Fremhæv agonister kraftigt
mov.agonists.forEach(id => {
const el = document.getElementById(id);
if (el) {
el.classList.remove('dimmed');
el.classList.add('active');
}
});
// Vis information i infopanelet
document.getElementById('display-primary-name').textContent = mov.title;
document.getElementById('display-secondary-name').textContent = "Bevægelsesanalyse";
document.getElementById('display-type-badge').textContent = "Bevægelse";
document.getElementById('display-type-badge').className = "badge muscle";
document.getElementById('display-location').textContent = "Involverede led & knogler: " + mov.bonesInvolved.join(", ");
document.getElementById('display-function').textContent = mov.biomechanics;
document.getElementById('display-sports-relevance').textContent = mov.description;
document.getElementById('display-related').innerHTML = mov.agonists.map(ag => `<span class="related-tag">${ag}</span>`).join('');
}
};
/* ==========================================================================
PHYSIOLOGY MODULE (Modul 11: Iltens vej & Kredsløb partikelanimation)
========================================================================== */
const PhysiologyModule = {
startProcess(procId) {
this.stopProcess();
const proc = PROCESSES_DATABASE[procId];
if (!proc) return;
let currentStep = 0;
const particleContainer = document.getElementById('group-particles');
const stepInfoBox = document.getElementById('process-step-info');
// Opret en animeret partikel
const p = document.createElementNS("http://www.w3.org/2000/svg", "circle");
p.setAttribute("r", "9");
p.setAttribute("class", "particle");
particleContainer.appendChild(p);
const runStep = () => {
if (currentStep >= proc.steps.length) {
currentStep = 0;
}
const stepData = proc.steps[currentStep];
const coord = proc.pathCoords[currentStep];
// Flyt partiklen
if (coord) {
p.setAttribute("cx", coord.x);
p.setAttribute("cy", coord.y);
}
// Fremhæv målorganet/musklen
document.querySelectorAll('.anatomical-part').forEach(part => part.classList.remove('active'));
if (stepData.target) {
const targetEl = document.getElementById(stepData.target);
if (targetEl) targetEl.classList.add('active');
}
// Opdater forklaring
if (stepInfoBox) {
stepInfoBox.innerHTML = `
<div style="background:var(--bg-card); padding:0.5rem; border-radius:6px; border-left:3px solid var(--accent);">
<strong>${stepData.name}</strong><br>
<span>${stepData.desc}</span>
</div>
`;
}
currentStep++;
};
runStep();
AppState.processAnimTimer = setInterval(runStep, 2200);
},
stopProcess() {
clearInterval(AppState.processAnimTimer);
document.getElementById('group-particles').innerHTML = '';
document.querySelectorAll('.anatomical-part').forEach(part => part.classList.remove('active'));
}
};
/* ==========================================================================
QUIZ & LEARNING MODULE (Modul 13: Læringsfunktioner)
========================================================================== */
const QuizModule = {
loadQuestion(index) {
AppState.currentQuizIndex = index;
const q = QUIZ_QUESTIONS[index];
const qTitle = document.getElementById('quiz-question');
const qOptions = document.getElementById('quiz-options');
const feedback = document.getElementById('quiz-feedback');
const nextBtn = document.getElementById('quiz-next-btn');
feedback.className = 'feedback-box';
feedback.style.display = 'none';
nextBtn.style.display = 'none';
qOptions.innerHTML = '';
qTitle.textContent = `${index + 1}. ${q.question}`;
if (q.type === 'choice') {
q.options.forEach(opt => {
const btn = document.createElement('button');
btn.textContent = opt.label;
btn.style.width = '100%';
btn.style.textAlign = 'left';
btn.addEventListener('click', () => {
this.handleChoiceAnswer(opt.correct, q.explanation);
});
qOptions.appendChild(btn);
});
} else if (q.type === 'click') {
qOptions.innerHTML = `<span style="font-size:0.8rem; color:var(--text-muted); font-style:italic;">Klik direkte på den pågældende knogle/muskel i modellen i midten.</span>`;
}
},
handleChoiceAnswer(isCorrect, explanation) {
const feedback = document.getElementById('quiz-feedback');
const nextBtn = document.getElementById('quiz-next-btn');
if (isCorrect) {
feedback.textContent = `Korrekt! ${explanation}`;
feedback.className = 'feedback-box correct';
} else {
feedback.textContent = `Ikke helt rigtigt. Prøv igen eller læs: ${explanation}`;
feedback.className = 'feedback-box incorrect';
}
nextBtn.style.display = 'block';
nextBtn.onclick = () => this.nextQuestion();
},
handleElementClick(clickedId) {
const q = QUIZ_QUESTIONS[AppState.currentQuizIndex];
if (!q || q.type !== 'click') return;
const feedback = document.getElementById('quiz-feedback');
const nextBtn = document.getElementById('quiz-next-btn');
if (clickedId === q.expectedId || clickedId === q.altExpectedId) {
feedback.textContent = `Helt rigtigt fundet! ${q.explanation}`;
feedback.className = 'feedback-box correct';
nextBtn.style.display = 'block';
nextBtn.onclick = () => this.nextQuestion();
} else {
const clickedItem = ANATOMY_DATABASE[clickedId];
const name = clickedItem ? clickedItem.names.da : clickedId;
feedback.textContent = `Du klikkede på: ${name}. Det var ikke den rigtige struktur til denne opgave. Prøv igen!`;
feedback.className = 'feedback-box incorrect';
}
},
nextQuestion() {
let nextIndex = AppState.currentQuizIndex + 1;
if (nextIndex >= QUIZ_QUESTIONS.length) {
nextIndex = 0;
alert("Godt gået! Du har gennemført alle quizspørgsmål. Runden genstarter.");
}
this.loadQuestion(nextIndex);
}
};
/* ==========================================================================
INITIALISERING VED START
========================================================================== */
window.addEventListener('DOMContentLoaded', () => {
ViewModule.init();
// Vælg automatisk lårbensknoglen som startvisning
ViewModule.selectElement('femur-left');
});
</script>
</body>
</html>
Dette er OpenAI’s chatbaserede AI-assistent/LLM.
<!doctype html><html lang="da"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Kroppen i bevægelse</title><style>body{font-family:system-ui;margin:0;background:#f4f7fb;color:#172033}header{display:flex;gap:12px;align-items:center;padding:12px 18px;background:#172033;color:white}header h1{font-size:18px;margin:0;flex:1}.app{display:grid;grid-template-columns:240px minmax(320px,1fr) 320px;min-height:calc(100vh - 52px)}aside{padding:14px;background:white;border-right:1px solid #dce3ee}.right{border-left:1px solid #dce3ee;border-right:0}.nav button,.layer{width:100%;margin:4px 0;padding:9px;border:0;border-radius:8px;background:#eef3f9;text-align:left;cursor:pointer}.stage{padding:16px;display:flex;flex-direction:column;align-items:center}.toolbar{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:10px}button,select,input{font:inherit}.toolbar button,header button,header select{padding:7px 10px;border:1px solid #ccd5e2;border-radius:8px;background:white}.bodywrap{width:min(520px,95%);background:white;border-radius:18px;box-shadow:0 8px 30px #17203318;padding:12px}.bodywrap svg{width:100%;height:auto;max-height:72vh}.part{cursor:pointer;transition:.2s}.part:hover,.part.active{filter:drop-shadow(0 0 5px #0b74de);stroke:#0b74de!important;stroke-width:4!important}.hidden{display:none}.card{background:#f7f9fc;border:1px solid #e1e7ef;border-radius:12px;padding:12px;margin:10px 0}.muted{color:#64748b;font-size:13px}.search{width:100%;box-sizing:border-box;padding:9px;border:1px solid #ccd5e2;border-radius:8px}.results button{display:block;width:100%;text-align:left;border:0;background:white;padding:7px;cursor:pointer}.badge{display:inline-block;padding:3px 7px;border-radius:999px;background:#e7f0ff;font-size:12px}.process-dot{fill:#0b74de}.quizopt{display:block;width:100%;padding:8px;margin:6px 0;border:1px solid #ccd5e2;border-radius:8px;background:white}@media(max-width:900px){.app{grid-template-columns:1fr}.left,.right{border:0;border-bottom:1px solid #ddd}.left{display:none}.right{display:block}.bodywrap svg{max-height:55vh}}</style></head><body><header><h1>🫀 Kroppen i bevægelse</h1><select id="lang"><option value="da">Dansk</option><option value="en">English</option><option value="la">Latin</option><option value="da-la">Dansk + Latin</option><option value="all">Alle tre</option></select><button id="fs">Fuldskærm</button></header><main class="app"><aside class="left"><input id="search" class="search" placeholder="Søg: femur, biceps…"><div id="results" class="results"></div><h3>Visning</h3><div class="nav" id="nav"></div><h3>Lag</h3><label class="layer"><input type="checkbox" data-layer="skeleton" checked> Skelet</label><label class="layer"><input type="checkbox" data-layer="muscle" checked> Muskler</label><label class="layer"><input type="checkbox" data-layer="organ" checked> Organer</label><label class="layer"><input type="checkbox" data-layer="outer" checked> Ydre krop</label><div class="card"><b>Split kroppen</b><input id="explode" type="range" min="0" max="80" value="0" style="width:100%"><div class="muted">Træk lagene visuelt fra hinanden.</div></div></aside><section class="stage"><div class="toolbar"><button id="front">Forfra</button><button id="back">Bagfra</button><button id="zoomIn">Zoom +</button><button id="zoomOut">Zoom −</button><button id="reset">Nulstil</button><button id="oxygen">▶ Iltens vej</button><button id="move">↔ Albuefleksion</button></div><div class="bodywrap"><svg id="body" viewBox="0 0 400 720" role="img" aria-label="Interaktiv menneskekrop"><g id="viewport"><g id="outer" opacity=".16" fill="#8fb3c9"><circle cx="200" cy="60" r="43"/><path d="M155 110 Q200 90 245 110 L270 330 245 365 235 650 195 650 190 385 170 650 130 650 140 365 115 330z"/></g><g id="skeleton" fill="none" stroke="#7b8794" stroke-width="9" stroke-linecap="round"><path id="spine" class="part" data-id="spine" d="M200 115 L200 345"/><path id="clavicle" class="part" data-id="clavicle" d="M160 125 Q200 145 240 125"/><path id="ribs" class="part" data-id="ribs" d="M170 145 Q200 125 230 145 M165 170 Q200 145 235 170 M165 195 Q200 170 235 195"/><path id="humerus" class="part" data-id="humerus" d="M155 135 L120 260"/><path id="radius" class="part" data-id="radius" d="M120 270 L95 365"/><path id="ulna" class="part" data-id="ulna" d="M130 270 L110 370"/><path id="pelvis" class="part" data-id="pelvis" d="M160 330 Q200 365 240 330"/><path id="femur" class="part" data-id="femur" d="M170 365 L155 505"/><path id="patella" class="part" data-id="patella" d="M155 505 L155 512"/><path id="tibia" class="part" data-id="tibia" d="M155 520 L145 640"/><path id="fibula" class="part" data-id="fibula" d="M170 520 L165 640"/></g><g id="muscle" fill="#d86161" opacity=".8"><ellipse id="deltoid" class="part" data-id="deltoid" cx="145" cy="140" rx="22" ry="32"/><ellipse id="biceps" class="part" data-id="biceps" cx="125" cy="215" rx="15" ry="45"/><ellipse id="pectoralis" class="part" data-id="pectoralis" cx="185" cy="165" rx="35" ry="28"/><rect id="rectus" class="part" data-id="rectus" x="178" y="205" width="44" height="105" rx="18"/><ellipse id="quadriceps" class="part" data-id="quadriceps" cx="165" cy="435" rx="25" ry="65"/><ellipse id="gastrocnemius" class="part" data-id="gastrocnemius" cx="155" cy="575" rx="18" ry="50"/></g><g id="organ"><path id="lungs" class="part" data-id="lungs" fill="#e89aaa" d="M168 145 q-25 25-10 80 q28 5 35-35v-45z M232 145 q25 25 10 80 q-28 5-35-35v-45z"/><path id="heart" class="part" data-id="heart" fill="#b82035" d="M200 190 c-35-35-55 20 0 65 55-45 35-100 0-65z"/><path id="stomach" class="part" data-id="stomach" fill="#e3a85b" d="M205 255 q45 0 25 65 q-40 20-45-15 q25-10 20-50z"/></g><g id="processLayer"></g></g></svg></div><div id="status" class="muted">Klik på en struktur for at undersøge den.</div></section><aside class="right"><div id="info"><h2>Vælg en struktur</h2><p>Hold musen over eller klik på kroppen.</p></div><div class="card"><b>Idrætsfaglig tilstand</b><select id="activity" style="width:100%;margin-top:8px"><option value="rest">Hvile</option><option value="moderate">Moderat aktivitet</option><option value="hard">Hård aktivitet</option></select><div id="metrics" class="muted" style="margin-top:8px"></div></div><div class="card"><b>Træn din viden</b><div id="quiz"></div><button id="newQuiz" class="quizopt">Nyt spørgsmål</button></div></aside></main><script>
// Central data store. EXTENSION POINT: add structures here.
const structures=[
{id:'spine',type:'bone',names:{da:'Rygsøjle',en:'Spine',la:'Columna vertebralis'},where:'Kroppens centrale akse fra kraniet til bækkenet.',fn:'Bærer kroppen og beskytter rygmarven.',sport:'Stabilitet, kropsholdning og kraftoverførsel.'},
{id:'clavicle',type:'bone',names:{da:'Kraveben',en:'Clavicle',la:'Clavicula'},where:'Mellem brystben og skulder.',fn:'Forbinder overekstremiteten med kroppen.',sport:'Vigtig for skulderbæltets bevægelighed.'},
{id:'ribs',type:'bone',names:{da:'Ribben',en:'Ribs',la:'Costae'},where:'Omkring brysthulen.',fn:'Beskytter hjerte og lunger og deltager i respiration.',sport:'Brystkassens bevægelse ved vejrtrækning.'},
{id:'humerus',type:'bone',names:{da:'Overarmsknogle',en:'Humerus',la:'Humerus'},where:'Overarmen.',fn:'Indgår i skulder- og albueled.',sport:'Kraftarm ved kast og pres.'},
{id:'radius',type:'bone',names:{da:'Spoleben',en:'Radius',la:'Radius'},where:'Underarmen, tommelfingersiden.',fn:'Deltager i albue, håndled og rotation af underarmen.',sport:'Grebs- og kastebevægelser.'},
{id:'ulna',type:'bone',names:{da:'Albueben',en:'Ulna',la:'Ulna'},where:'Underarmen, lillefingersiden.',fn:'Danner en stor del af albueleddet.',sport:'Stabilitet ved fleksion og ekstension.'},
{id:'pelvis',type:'bone',names:{da:'Bækken',en:'Pelvis',la:'Pelvis'},where:'Mellem rygsøjle og ben.',fn:'Overfører belastning og beskytter bækkenorganer.',sport:'Central for løb, hop og stabilitet.'},
{id:'femur',type:'bone',names:{da:'Lårben',en:'Femur',la:'Femur'},where:'Låret.',fn:'Kroppens længste knogle; indgår i hofte og knæ.',sport:'Bærer store kræfter ved løb, hop og squat.'},
{id:'patella',type:'bone',names:{da:'Knæskal',en:'Patella',la:'Patella'},where:'Forsiden af knæet.',fn:'Øger quadriceps’ momentarm og beskytter knæet.',sport:'Vigtig ved knæekstension.'},
{id:'tibia',type:'bone',names:{da:'Skinneben',en:'Tibia',la:'Tibia'},where:'Mediale underben.',fn:'Primær vægtbærende knogle i underbenet.',sport:'Belastes ved løb og landing.'},
{id:'fibula',type:'bone',names:{da:'Lægben',en:'Fibula',la:'Fibula'},where:'Laterale underben.',fn:'Bidrager til ankelstabilitet og muskelfæster.',sport:'Stabilitet omkring underben og ankel.'},
{id:'deltoid',type:'muscle',names:{da:'Deltamusklen',en:'Deltoid',la:'M. deltoideus'},where:'Omkring skulderen.',fn:'Abducerer især armen; fibre bidrager også til fleksion/ekstension.',sport:'Kast, svømning og løft over hovedet.'},
{id:'biceps',type:'muscle',names:{da:'Tohovedet armbøjer',en:'Biceps brachii',la:'M. biceps brachii'},where:'Forsiden af overarmen.',fn:'Flekterer albuen og supinerer underarmen.',sport:'Træk, kast og klatring.',origin:'Scapula',insert:'Radius'},
{id:'pectoralis',type:'muscle',names:{da:'Store brystmuskel',en:'Pectoralis major',la:'M. pectoralis major'},where:'Forsiden af brystkassen.',fn:'Adduktion og indadrotation af overarmen.',sport:'Pres, kast og svømning.'},
{id:'rectus',type:'muscle',names:{da:'Lige bugmuskel',en:'Rectus abdominis',la:'M. rectus abdominis'},where:'Forsiden af bugvæggen.',fn:'Flekterer columna og bidrager til bugtryk.',sport:'Kropskontrol og stabilisering.'},
{id:'quadriceps',type:'muscle',names:{da:'Firehovedet knæstrækker',en:'Quadriceps',la:'M. quadriceps femoris'},where:'Forsiden af låret.',fn:'Ekstenderer knæet; rectus femoris bidrager også til hoftefleksion.',sport:'Squat, hop, løb og spark.'},
{id:'gastrocnemius',type:'muscle',names:{da:'Den store lægmuskel',en:'Gastrocnemius',la:'M. gastrocnemius'},where:'Bagsiden af underbenet.',fn:'Plantarflekterer anklen og bidrager til knæfleksion.',sport:'Afsæt ved løb og hop.'},
{id:'lungs',type:'organ',names:{da:'Lunger',en:'Lungs',la:'Pulmones'},where:'Brysthulen.',fn:'Gasudveksling mellem luft og blod.',sport:'Øget ventilation understøtter arbejde ved fysisk aktivitet.'},
{id:'heart',type:'organ',names:{da:'Hjerte',en:'Heart',la:'Cor'},where:'Brysthulen mellem lungerne.',fn:'Pumper blod gennem lunge- og systemkredsløb.',sport:'Minutvolumen stiger typisk under aktivitet.'},
{id:'stomach',type:'organ',names:{da:'Mavesæk',en:'Stomach',la:'Gaster'},where:'Øvre venstre del af bughulen.',fn:'Blander føde og påbegynder især nedbrydning af proteiner.',sport:'Fordøjelse og energitilgængelighed er relevant omkring aktivitet.'}
];
const byId=Object.fromEntries(structures.map(x=>[x.id,x])); let selected=null,scale=1,processTimer=null,processStep=0;
const $=s=>document.querySelector(s), $$=s=>[...document.querySelectorAll(s)];
const LanguageModule={mode:localStorage.lang||'da',name(x){let n=x.names,m=this.mode;if(m==='all')return `${n.da} · ${n.en} · ${n.la}`;if(m==='da-la')return `${n.da} · ${n.la}`;return n[m]||n.da},set(v){this.mode=v;localStorage.lang=v;if(selected) AnatomyModule.select(selected)}};
const AnatomyModule={select(id){selected=id;$$('.part').forEach(e=>e.classList.toggle('active',e.dataset.id===id));let x=byId[id];if(!x)return;$('#info').innerHTML=`<span class="badge">${x.type}</span><h2>${LanguageModule.name(x)}</h2><p><b>Placering:</b> ${x.where}</p><p><b>Funktion:</b> ${x.fn}</p><p><b>Idrætsrelevans:</b> ${x.sport}</p>${x.origin?`<p><b>Udspring:</b> ${x.origin}<br><b>Hæfte:</b> ${x.insert}</p>`:''}<button onclick="FavoriteModule.toggle('${id}')">${FavoriteModule.has(id)?'★ Fjern favorit':'☆ Skal læres'}</button>`;$('#status').textContent=LanguageModule.name(x)} };
const FavoriteModule={get(){return JSON.parse(localStorage.favs||'[]')},has(id){return this.get().includes(id)},toggle(id){let a=this.get();a=this.has(id)?a.filter(x=>x!==id):[...a,id];localStorage.favs=JSON.stringify(a);AnatomyModule.select(id)}};
const SearchModule={run(q){q=q.toLowerCase().trim();let r=structures.filter(x=>Object.values(x.names).some(n=>n.toLowerCase().includes(q))).slice(0,8);$('#results').innerHTML=q?r.map(x=>`<button data-find="${x.id}">${LanguageModule.name(x)}</button>`).join(''):'';$$('[data-find]').forEach(b=>b.onclick=()=>AnatomyModule.select(b.dataset.find))}};
const LayerModule={init(){$$('[data-layer]').forEach(c=>c.onchange=()=>$('#'+c.dataset.layer).classList.toggle('hidden',!c.checked));$('#explode').oninput=e=>{let v=+e.target.value;$('#skeleton').style.transform=`translateX(${-v}px)`;$('#muscle').style.transform=`translateX(${v}px)`;$('#organ').style.transform=`translateY(${-v/3}px)`}}};
const ProcessModule={oxygen(){clearInterval(processTimer);processStep=0;const pts=[[200,90],[200,130],[210,175],[225,205],[200,225],[170,430]];let g=$('#processLayer');g.innerHTML='<circle id="dot" class="process-dot" r="9" cx="200" cy="90"/>';processTimer=setInterval(()=>{processStep++;if(processStep>=pts.length){clearInterval(processTimer);return}let [x,y]=pts[processStep];$('#dot').setAttribute('cx',x);$('#dot').setAttribute('cy',y);$('#status').textContent=['Luft ind gennem næse/mund','Gennem luftvejene','Til lungerne','Gasudveksling → blod','Via hjertet','Ilt leveres til arbejdende muskel'][processStep]},850)}};
const MovementModule={elbow(){AnatomyModule.select('biceps');let b=$('#biceps'),h=$('#humerus'),r=$('#radius');[b,h,r].forEach(x=>x.classList.add('active'));let on=false,n=0;let t=setInterval(()=>{on=!on;r.style.transformOrigin='120px 270px';r.style.transform=`rotate(${on?-32:0}deg)`;if(++n>7){clearInterval(t);r.style.transform=''}},350);$('#status').textContent='Albuefleksion: biceps brachii er en central agonist; knoglerne humerus, radius og ulna indgår i regionen.'}};
const QuizModule={new(){let x=structures[Math.floor(Math.random()*structures.length)],opts=[x,...structures.filter(y=>y.id!==x.id).sort(()=>Math.random()-.5).slice(0,2)].sort(()=>Math.random()-.5);$('#quiz').innerHTML=`<p>Hvad hedder den fremhævede struktur?</p>${opts.map(o=>`<button class="quizopt" data-q="${o.id}">${LanguageModule.name(o)}</button>`).join('')}`;AnatomyModule.select(x.id);$$('[data-q]').forEach(b=>b.onclick=()=>{b.textContent+=(b.dataset.q===x.id?' ✓':' ✗')})}};
const ActivityModule={set(v){let m={rest:'Puls ca. 60–80/min · respiration ca. 12–18/min',moderate:'Puls og ventilation stiger; større blodgennemstrømning til arbejdende muskler.',hard:'Høj puls og ventilation; markant øget ilt- og energibehov.'};$('#metrics').innerHTML=m[v]+'<br><em>Illustrative eksempler – ikke individuelle medicinske målinger.</em>'}};
$$('.part').forEach(e=>{e.tabIndex=0;e.setAttribute('role','button');e.onclick=()=>AnatomyModule.select(e.dataset.id);e.onkeydown=ev=>{if(ev.key==='Enter'||ev.key===' ')AnatomyModule.select(e.dataset.id)}});
$('#lang').value=LanguageModule.mode;$('#lang').onchange=e=>LanguageModule.set(e.target.value);$('#search').oninput=e=>SearchModule.run(e.target.value);$('#fs').onclick=()=>document.documentElement.requestFullscreen?.();$('#zoomIn').onclick=()=>{scale=Math.min(2,scale+.15);$('#viewport').style.transform=`scale(${scale})`;$('#viewport').style.transformOrigin='200px 360px'};$('#zoomOut').onclick=()=>{scale=Math.max(.65,scale-.15);$('#viewport').style.transform=`scale(${scale})`};$('#reset').onclick=()=>location.reload();$('#oxygen').onclick=()=>ProcessModule.oxygen();$('#move').onclick=()=>MovementModule.elbow();$('#newQuiz').onclick=()=>QuizModule.new();$('#activity').onchange=e=>ActivityModule.set(e.target.value);$('#front').onclick=()=>$('#status').textContent='Forfra';$('#back').onclick=()=>{$('#viewport').style.transform=`scaleX(-1) scale(${scale})`;$('#status').textContent='Forenklet bagfra-visning'};
$('#nav').innerHTML=['Hele kroppen','Skelet','Muskler','Organer','Bevægelse','Processer','Træn din viden'].map(x=>`<button>${x}</button>`).join('');LayerModule.init();ActivityModule.set('rest');QuizModule.new();
</script></body></html>
Dette er OpenAI’s chatbaserede AI-assistent/LLM i en eksperimentel alpha-version.
<!DOCTYPE html>
<html lang="da">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AnatomiLab · Kroppen i bevægelse</title><link rel="icon" href="data:image/svg+xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 40 40%27%3E%3Crect width=%2740%27 height=%2740%27 rx=%2710%27 fill=%27%230d9488%27/%3E%3Cpath d=%27M8 27 20 8 32 27M13 21h14%27 stroke=%27white%27 fill=%27none%27 stroke-width=%273%27/%3E%3C/svg%3E">
<style>
/* ==========================================================================
1. CSS VARIABLER & DESIGNTOKENS
========================================================================== */
:root {
--bg-primary: #0f172a;
--bg-secondary: #1e293b;
--bg-tertiary: #334155;
--bg-glass: rgba(30, 41, 59, 0.85);
--bg-card: rgba(15, 23, 42, 0.75);
--text-primary: #f8fafc;
--text-secondary: #94a3b8;
--text-muted: #64748b;
--border-color: #334155;
--border-focus: #38bdf8;
--accent-blue: #0284c7;
--accent-sky: #38bdf8;
--accent-emerald: #10b981;
--accent-amber: #f59e0b;
--accent-rose: #f43f5e;
--accent-purple: #8b5cf6;
/* Anatomiske temafarver */
--color-bone: #e2e8f0;
--color-bone-stroke: #94a3b8;
--color-muscle: #e11d48;
--color-muscle-stroke: #9f1239;
--color-organ: #d97706;
--color-artery: #ef4444;
--color-vein: #3b82f6;
--color-nerve: #eab308;
--color-respiratory: #06b6d4;
--color-highlight: #facc15;
--font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.3);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.4), 0 2px 4px -1px rgba(0, 0, 0, 0.3);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.5), 0 4px 6px -2px rgba(0, 0, 0, 0.4);
--radius-sm: 6px;
--radius-md: 10px;
--radius-lg: 16px;
--radius-full: 9999px;
--transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);
--transition-base: 250ms cubic-bezier(0.4, 0, 0.2, 1);
--transition-slow: 400ms cubic-bezier(0.4, 0, 0.2, 1);
--sidebar-width: 290px;
--panel-width: 380px;
--header-height: 64px;
}
/* ==========================================================================
2. RESET & BASIS STYLING
========================================================================== */
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html, body {
width: 100%;
height: 100%;
overflow: hidden;
font-family: var(--font-family);
background-color: var(--bg-primary);
color: var(--text-primary);
font-size: 14px;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
button, input, select {
font-family: inherit;
font-size: inherit;
color: inherit;
border: none;
background: none;
outline: none;
}
button {
cursor: pointer;
user-select: none;
touch-action: manipulation;
}
a {
color: var(--accent-sky);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
button:focus-visible, select:focus-visible, input:focus-visible {
outline: 2px solid var(--accent-sky);
outline-offset: 2px;
}
/* ==========================================================================
3. APP LAYOUT
========================================================================== */
#app-container {
display: grid;
grid-template-rows: var(--header-height) 1fr auto;
grid-template-columns: var(--sidebar-width) 1fr var(--panel-width);
grid-template-areas:
"header header header"
"sidebar canvas panel"
"footer footer footer";
height: 100vh;
width: 100vw;
overflow: hidden;
position: relative;
}
/* ==========================================================================
4. TOPBAR / HEADER
========================================================================== */
header#top-header {
grid-area: header;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px;
background: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
z-index: 40;
gap: 12px;
}
.brand-section {
display: flex;
align-items: center;
gap: 12px;
}
.brand-logo {
width: 36px;
height: 36px;
border-radius: var(--radius-md);
background: linear-gradient(135deg, var(--accent-blue), var(--accent-emerald));
display: flex;
align-items: center;
justify-content: center;
font-weight: 800;
font-size: 18px;
color: white;
box-shadow: 0 0 12px rgba(2, 132, 199, 0.4);
}
.brand-title h1 {
font-size: 15px;
font-weight: 700;
color: var(--text-primary);
letter-spacing: -0.2px;
}
.brand-title span {
font-size: 11px;
color: var(--accent-sky);
display: block;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.header-controls {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: nowrap;
}
.pill-group {
display: flex;
background: var(--bg-primary);
padding: 3px;
border-radius: var(--radius-full);
border: 1px solid var(--border-color);
}
.pill-btn {
padding: 6px 12px;
border-radius: var(--radius-full);
font-size: 12px;
font-weight: 600;
color: var(--text-secondary);
transition: var(--transition-fast);
display: flex;
align-items: center;
gap: 6px;
}
.pill-btn.active {
background: var(--accent-blue);
color: white;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
}
.pill-btn:hover:not(.active) {
color: var(--text-primary);
}
.lang-dropdown-container {
position: relative;
}
.lang-select {
background: var(--bg-primary);
border: 1px solid var(--border-color);
color: var(--text-primary);
padding: 6px 12px;
border-radius: var(--radius-md);
font-size: 12px;
font-weight: 500;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
}
.lang-select option {
background: var(--bg-secondary);
color: var(--text-primary);
}
.exertion-control {
display: flex;
align-items: center;
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: var(--radius-full);
padding: 2px;
}
.exertion-btn {
padding: 5px 11px;
border-radius: var(--radius-full);
font-size: 11px;
font-weight: 600;
color: var(--text-muted);
transition: var(--transition-fast);
}
.exertion-btn.active[data-state="rest"] {
background: #059669;
color: white;
}
.exertion-btn.active[data-state="moderate"] {
background: var(--accent-amber);
color: #0f172a;
}
.exertion-btn.active[data-state="heavy"] {
background: var(--accent-rose);
color: white;
animation: pulseAlert 1.5s infinite;
}
@keyframes pulseAlert {
0%, 100% { opacity: 1; }
50% { opacity: 0.85; }
}
.search-box {
position: relative;
width: 220px;
}
.search-box input {
width: 100%;
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
padding: 7px 32px 7px 12px;
font-size: 12px;
color: var(--text-primary);
transition: var(--transition-fast);
}
.search-box input:focus {
border-color: var(--accent-sky);
width: 260px;
}
.search-box svg {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
width: 14px;
height: 14px;
fill: none;
stroke: var(--text-muted);
stroke-width: 2;
}
.search-results-dropdown {
position: absolute;
top: calc(100% + 4px);
left: 0;
right: 0;
max-height: 350px;
overflow-y: auto;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
box-shadow: var(--shadow-lg);
z-index: 100;
display: none;
}
.search-item {
padding: 9px 12px;
border-bottom: 1px solid var(--border-color);
cursor: pointer;
display: flex;
flex-direction: column;
gap: 2px;
}
.search-item:last-child {
border-bottom: none;
}
.search-item:hover {
background: var(--bg-tertiary);
}
.search-item-title {
font-weight: 600;
font-size: 13px;
color: var(--text-primary);
}
.search-item-sub {
font-size: 11px;
color: var(--text-secondary);
display: flex;
gap: 8px;
}
.search-badge {
font-size: 9px;
padding: 1px 5px;
border-radius: var(--radius-sm);
background: var(--accent-blue);
color: white;
text-transform: uppercase;
font-weight: 700;
}
.header-actions {
display: flex;
align-items: center;
gap: 6px;
}
.icon-btn {
width: 34px;
height: 34px;
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
color: var(--text-secondary);
background: var(--bg-primary);
border: 1px solid var(--border-color);
transition: var(--transition-fast);
position: relative;
}
.icon-btn:hover {
color: var(--text-primary);
border-color: var(--text-muted);
}
.icon-btn.active {
background: var(--accent-blue);
color: white;
border-color: var(--accent-blue);
}
.icon-btn svg {
width: 17px;
height: 17px;
stroke: currentColor;
fill: none;
stroke-width: 2;
}
/* ==========================================================================
5. VENSTRE SIDEBAR (Navigation & Lag)
========================================================================== */
aside#left-sidebar {
grid-area: sidebar;
background: var(--bg-secondary);
border-right: 1px solid var(--border-color);
display: flex;
flex-direction: column;
height: calc(100vh - var(--header-height));
overflow-y: auto;
z-index: 30;
padding: 14px 12px;
gap: 16px;
}
.sidebar-section-title {
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.8px;
color: var(--text-muted);
margin-bottom: 6px;
padding-left: 6px;
}
.nav-list {
list-style: none;
display: flex;
flex-direction: column;
gap: 3px;
}
.nav-item-btn {
width: 100%;
display: flex;
align-items: center;
gap: 10px;
padding: 9px 12px;
border-radius: var(--radius-md);
color: var(--text-secondary);
font-weight: 500;
font-size: 13px;
transition: var(--transition-fast);
text-align: left;
}
.nav-item-btn:hover {
background: var(--bg-tertiary);
color: var(--text-primary);
}
.nav-item-btn.active {
background: var(--accent-blue);
color: white;
font-weight: 600;
box-shadow: 0 2px 4px rgba(2, 132, 199, 0.25);
}
.nav-item-btn svg {
width: 18px;
height: 18px;
stroke: currentColor;
fill: none;
stroke-width: 2;
flex-shrink: 0;
}
.nav-badge {
margin-left: auto;
font-size: 10px;
padding: 2px 6px;
border-radius: var(--radius-full);
background: rgba(255, 255, 255, 0.15);
color: currentColor;
}
.layers-container {
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
padding: 8px;
display: flex;
flex-direction: column;
gap: 6px;
}
.layer-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 8px;
border-radius: var(--radius-sm);
background: var(--bg-secondary);
transition: var(--transition-fast);
}
.layer-row:hover {
background: var(--bg-tertiary);
}
.layer-label {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
user-select: none;
font-size: 12px;
font-weight: 500;
flex: 1;
}
.layer-dot {
width: 10px;
height: 10px;
border-radius: var(--radius-full);
flex-shrink: 0;
}
.layer-row input[type="checkbox"] {
accent-color: var(--accent-blue);
width: 15px;
height: 15px;
cursor: pointer;
}
.layer-opacity-slider {
width: 65px;
height: 4px;
accent-color: var(--accent-sky);
cursor: pointer;
margin-left: 8px;
}
/* ==========================================================================
6. CENTRALT LÆRRED (CANVAS MED SVG-KROP)
========================================================================== */
main#canvas-container {
grid-area: canvas;
position: relative;
background: radial-gradient(circle at center, #1e293b 0%, #0f172a 100%);
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
user-select: none;
}
.canvas-breadcrumbs {
position: absolute;
top: 14px;
left: 16px;
display: flex;
align-items: center;
gap: 6px;
background: var(--bg-glass);
backdrop-filter: blur(8px);
padding: 6px 14px;
border-radius: var(--radius-full);
border: 1px solid var(--border-color);
font-size: 12px;
font-weight: 500;
color: var(--text-secondary);
z-index: 10;
box-shadow: var(--shadow-sm);
}
.canvas-breadcrumbs span.active {
color: var(--accent-sky);
font-weight: 600;
}
.canvas-breadcrumbs .breadcrumb-sep {
color: var(--text-muted);
font-size: 10px;
}
.canvas-toolbar {
position: absolute;
bottom: 20px;
left: 20px;
display: flex;
flex-direction: column;
gap: 6px;
background: var(--bg-glass);
backdrop-filter: blur(8px);
padding: 6px;
border-radius: var(--radius-md);
border: 1px solid var(--border-color);
box-shadow: var(--shadow-md);
z-index: 15;
}
.canvas-tool-btn {
width: 34px;
height: 34px;
border-radius: var(--radius-sm);
display: flex;
align-items: center;
justify-content: center;
background: var(--bg-secondary);
color: var(--text-secondary);
transition: var(--transition-fast);
}
.canvas-tool-btn:hover {
background: var(--bg-tertiary);
color: var(--text-primary);
}
.canvas-tool-btn svg {
width: 18px;
height: 18px;
stroke: currentColor;
fill: none;
stroke-width: 2;
}
.split-slider-container {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background: var(--bg-glass);
backdrop-filter: blur(8px);
border: 1px solid var(--border-color);
padding: 8px 18px;
border-radius: var(--radius-full);
display: none;
align-items: center;
gap: 12px;
z-index: 20;
box-shadow: var(--shadow-lg);
}
.split-slider-container span {
font-size: 12px;
font-weight: 600;
color: var(--accent-sky);
white-space: nowrap;
}
.split-slider-container input[type="range"] {
width: 180px;
accent-color: var(--accent-sky);
cursor: pointer;
}
#svg-viewport {
width: 100%;
height: 100%;
cursor: grab;
transition: transform var(--transition-fast);
transform-origin: center center;
}
#svg-viewport:active {
cursor: grabbing;
}
svg.anatomy-svg {
width: 100%;
height: 100%;
display: block;
}
.anatomical-part {
cursor: pointer;
transition: fill 0.2s ease, stroke 0.2s ease, filter 0.2s ease, opacity 0.3s ease;
}
.anatomical-part:hover {
filter: drop-shadow(0 0 6px var(--accent-sky)) brightness(1.25);
}
.anatomical-part.selected {
stroke: var(--color-highlight) !important;
stroke-width: 3.5px !important;
filter: drop-shadow(0 0 8px rgba(250, 204, 21, 0.8)) brightness(1.3);
}
.anatomical-part.active-agonist {
stroke: #22c55e !important;
stroke-width: 3.5px !important;
fill: rgba(34, 197, 94, 0.4) !important;
animation: pulseAgonist 1.2s infinite;
}
.anatomical-part.active-antagonist {
stroke: #f97316 !important;
stroke-width: 3.5px !important;
fill: rgba(249, 115, 22, 0.4) !important;
}
@keyframes pulseAgonist {
0%, 100% { filter: drop-shadow(0 0 4px #22c55e); }
50% { filter: drop-shadow(0 0 12px #22c55e); }
}
#anatomy-tooltip {
position: absolute;
pointer-events: none;
background: var(--bg-glass);
backdrop-filter: blur(10px);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
padding: 7px 12px;
box-shadow: var(--shadow-md);
color: var(--text-primary);
font-size: 12px;
z-index: 100;
display: none;
flex-direction: column;
gap: 2px;
transform: translate(-50%, -120%);
}
#anatomy-tooltip .tt-name-da {
font-weight: 700;
color: var(--text-primary);
}
#anatomy-tooltip .tt-name-la {
font-style: italic;
color: var(--accent-sky);
font-size: 11px;
}
#anatomy-tooltip .tt-type {
font-size: 10px;
color: var(--text-muted);
text-transform: uppercase;
font-weight: 600;
}
.anatomy-label-pin {
pointer-events: none;
font-size: 10px;
font-weight: 600;
fill: #ffffff;
paint-order: stroke;
stroke: #0f172a;
stroke-width: 3px;
stroke-linecap: round;
stroke-linejoin: round;
opacity: 0;
transition: opacity 0.3s ease;
}
body.show-teacher-labels .anatomy-label-pin {
opacity: 1;
}
/* ==========================================================================
7. HØJRE INFORMATIONSPANEL
========================================================================== */
aside#right-panel {
grid-area: panel;
background: var(--bg-secondary);
border-left: 1px solid var(--border-color);
height: calc(100vh - var(--header-height));
overflow-y: auto;
display: flex;
flex-direction: column;
z-index: 30;
padding: 16px;
gap: 16px;
position: relative;
}
.panel-empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 70%;
text-align: center;
color: var(--text-muted);
gap: 12px;
padding: 20px;
}
.panel-empty-state svg {
width: 48px;
height: 48px;
stroke: var(--text-muted);
opacity: 0.5;
}
.panel-empty-state h3 {
font-size: 15px;
color: var(--text-secondary);
}
.panel-content {
display: flex;
flex-direction: column;
gap: 16px;
}
.structure-header {
display: flex;
flex-direction: column;
gap: 6px;
border-bottom: 1px solid var(--border-color);
padding-bottom: 14px;
}
.structure-type-badge-row {
display: flex;
align-items: center;
justify-content: space-between;
}
.structure-type-badge {
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
padding: 3px 8px;
border-radius: var(--radius-sm);
background: var(--accent-blue);
color: white;
}
.btn-favorite {
display: flex;
align-items: center;
gap: 5px;
font-size: 11px;
font-weight: 600;
color: var(--text-muted);
padding: 3px 8px;
border-radius: var(--radius-sm);
background: var(--bg-primary);
border: 1px solid var(--border-color);
transition: var(--transition-fast);
}
.btn-favorite.is-favorite {
color: var(--accent-amber);
border-color: var(--accent-amber);
}
.btn-favorite svg {
width: 14px;
height: 14px;
fill: currentColor;
}
.structure-names {
display: flex;
flex-direction: column;
gap: 3px;
}
.structure-title-da {
font-size: 20px;
font-weight: 800;
color: var(--text-primary);
line-height: 1.2;
}
.structure-title-la {
font-size: 14px;
font-style: italic;
color: var(--accent-sky);
}
.structure-title-en {
font-size: 12px;
color: var(--text-secondary);
}
.info-card {
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
padding: 12px 14px;
display: flex;
flex-direction: column;
gap: 6px;
}
.info-card-title {
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--text-muted);
display: flex;
align-items: center;
gap: 6px;
}
.info-card-title svg {
width: 14px;
height: 14px;
stroke: currentColor;
}
.info-card p {
font-size: 13px;
color: var(--text-secondary);
line-height: 1.5;
}
.info-card.didaktik-card {
border-left: 3px solid var(--accent-emerald);
background: linear-gradient(135deg, rgba(16, 185, 129, 0.05) 0%, rgba(15, 23, 42, 0.8) 100%);
}
.info-card.didaktik-card .info-card-title {
color: var(--accent-emerald);
}
.info-card.biomechanics-card {
border-left: 3px solid var(--accent-purple);
}
.info-card.biomechanics-card .info-card-title {
color: var(--accent-purple);
}
.related-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 4px;
}
.related-btn {
padding: 4px 9px;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
font-size: 11px;
font-weight: 500;
color: var(--text-secondary);
transition: var(--transition-fast);
display: flex;
align-items: center;
gap: 4px;
}
.related-btn:hover {
background: var(--accent-blue);
color: white;
border-color: var(--accent-blue);
}
/* ==========================================================================
8. MODALER / FLOATING OVERLAYS (Bevægelse, Forløb, Quiz, Underviser)
========================================================================== */
.overlay-panel {
position: absolute;
top: 14px;
right: calc(var(--panel-width) + 16px);
width: 380px;
max-height: calc(100% - 30px);
overflow-y: auto;
background: var(--bg-glass);
backdrop-filter: blur(12px);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-lg);
padding: 18px;
z-index: 25;
display: none;
flex-direction: column;
gap: 14px;
animation: fadeIn 0.2s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-8px); }
to { opacity: 1; transform: translateY(0); }
}
.overlay-header {
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid var(--border-color);
padding-bottom: 10px;
}
.overlay-title {
font-size: 15px;
font-weight: 700;
color: var(--text-primary);
display: flex;
align-items: center;
gap: 8px;
}
.overlay-close {
width: 26px;
height: 26px;
border-radius: var(--radius-sm);
display: flex;
align-items: center;
justify-content: center;
color: var(--text-muted);
transition: var(--transition-fast);
}
.overlay-close:hover {
color: var(--text-primary);
background: var(--bg-tertiary);
}
.movement-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px;
}
.movement-select-btn {
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
padding: 10px;
text-align: left;
font-size: 12px;
font-weight: 600;
color: var(--text-secondary);
transition: var(--transition-fast);
display: flex;
flex-direction: column;
gap: 3px;
}
.movement-select-btn:hover {
background: var(--bg-tertiary);
color: var(--text-primary);
}
.movement-select-btn.active {
background: var(--accent-blue);
color: white;
border-color: var(--accent-blue);
}
.movement-select-btn small {
font-size: 10px;
color: inherit;
opacity: 0.8;
font-weight: normal;
}
.process-controller {
display: flex;
flex-direction: column;
gap: 10px;
background: var(--bg-primary);
padding: 12px;
border-radius: var(--radius-md);
border: 1px solid var(--border-color);
}
.process-playback-bar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.btn-play {
background: var(--accent-emerald);
color: white;
padding: 6px 14px;
border-radius: var(--radius-full);
font-weight: 600;
font-size: 12px;
display: flex;
align-items: center;
gap: 6px;
}
.process-step-list {
display: flex;
flex-direction: column;
gap: 6px;
margin-top: 8px;
}
.process-step-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
padding: 6px 8px;
border-radius: var(--radius-sm);
background: var(--bg-secondary);
color: var(--text-secondary);
transition: var(--transition-fast);
}
.process-step-item.active {
background: var(--accent-sky);
color: #0f172a;
font-weight: 700;
}
.quiz-options-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.quiz-option-btn {
padding: 10px 12px;
border-radius: var(--radius-md);
background: var(--bg-primary);
border: 1px solid var(--border-color);
text-align: left;
font-size: 13px;
color: var(--text-primary);
transition: var(--transition-fast);
display: flex;
align-items: center;
justify-content: space-between;
}
.quiz-option-btn:hover {
background: var(--bg-tertiary);
border-color: var(--accent-sky);
}
.quiz-option-btn.correct {
background: rgba(16, 185, 129, 0.2);
border-color: #10b981;
color: #34d399;
font-weight: 700;
}
.quiz-option-btn.incorrect {
background: rgba(244, 63, 94, 0.2);
border-color: #f43f5e;
color: #fb7185;
}
.quiz-feedback {
padding: 10px 12px;
border-radius: var(--radius-md);
font-size: 12px;
line-height: 1.4;
display: none;
}
.quiz-feedback.show {
display: block;
}
.quiz-feedback.success {
background: rgba(16, 185, 129, 0.15);
border: 1px solid #10b981;
color: #34d399;
}
.quiz-feedback.failure {
background: rgba(244, 63, 94, 0.15);
border: 1px solid #f43f5e;
color: #fb7185;
}
.match-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.match-card {
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
padding: 8px 10px;
font-size: 12px;
font-weight: 600;
text-align: center;
cursor: pointer;
user-select: none;
transition: var(--transition-fast);
}
.match-card:hover {
background: var(--bg-tertiary);
}
.match-card.selected {
border-color: var(--accent-sky);
background: rgba(56, 189, 248, 0.15);
color: var(--accent-sky);
}
.match-card.matched {
background: rgba(16, 185, 129, 0.2);
border-color: #10b981;
color: #34d399;
cursor: default;
opacity: 0.6;
}
/* ==========================================================================
9. STATUSLINJE / FYSIOLOGI METRICS BAR
========================================================================== */
footer#bottom-bar {
grid-area: footer;
background: var(--bg-secondary);
border-top: 1px solid var(--border-color);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px;
height: 38px;
font-size: 11px;
color: var(--text-secondary);
z-index: 35;
}
.metrics-group {
display: flex;
align-items: center;
gap: 18px;
}
.metric-item {
display: flex;
align-items: center;
gap: 6px;
}
.metric-item strong {
color: var(--text-primary);
font-weight: 600;
}
.heart-icon {
color: var(--accent-rose);
animation: heartbeat 1s infinite;
display: inline-block;
}
@keyframes heartbeat {
0%, 100% { transform: scale(1); }
15% { transform: scale(1.25); }
30% { transform: scale(1); }
45% { transform: scale(1.15); }
}
.disclaimer-tag {
font-size: 10px;
color: var(--text-muted);
font-style: italic;
}
/* ==========================================================================
10. RESPONSIVITET & MOBIL OPTIMERING
========================================================================== */
@media (max-width: 1024px) {
#app-container {
grid-template-columns: 240px 1fr 320px;
}
:root {
--sidebar-width: 240px;
--panel-width: 320px;
}
}
@media (max-width: 768px) {
#app-container {
grid-template-columns: 1fr;
grid-template-rows: var(--header-height) 1fr auto;
grid-template-areas:
"header"
"canvas"
"footer";
}
aside#left-sidebar {
position: absolute;
top: var(--header-height);
left: 0;
bottom: 0;
width: 280px;
transform: translateX(-100%);
transition: transform var(--transition-base);
box-shadow: var(--shadow-lg);
}
aside#left-sidebar.open {
transform: translateX(0);
}
aside#right-panel {
position: absolute;
top: var(--header-height);
right: 0;
bottom: 0;
width: 100%;
max-width: 360px;
transform: translateX(100%);
transition: transform var(--transition-base);
box-shadow: var(--shadow-lg);
}
aside#right-panel.open {
transform: translateX(0);
}
.overlay-panel {
right: 10px;
left: 10px;
width: auto;
}
}
/* AnatomiLab — responsive learning workspace */
:root{--sidebar-width:244px;--panel-width:340px;--header-height:116px;--bg-primary:#0b1420;--bg-secondary:#142232;--bg-tertiary:#243649;--text-muted:#9bacc0;--accent-sky:#6edbc9;--accent-blue:#15786f;--border-color:#2b4053;}
body{font-size:16px}#app-container{height:100dvh;min-height:580px}header#top-header{flex-wrap:wrap;justify-content:flex-start;gap:10px 18px;padding:12px 20px}.brand-section{margin-right:auto}.brand-logo{background:#188c7d;border-radius:10px}.brand-title h1{font-size:22px;letter-spacing:-.7px}.brand-title span{font-size:12px;letter-spacing:1.5px;text-transform:uppercase}.search-box{order:5;flex:1;min-width:180px;max-width:none}.exertion-control{order:6}.header-actions{margin-left:auto}.pill-btn,.exertion-btn,.lang-select{font-size:14px;min-height:36px}.nav-item-btn,.layer-label{font-size:14px}.nav-item-btn{min-height:43px}.sidebar-section-title{font-size:12px;letter-spacing:1.4px}.layer-row{gap:8px}.layer-label{flex:1}#canvas-container{background:radial-gradient(ellipse at 50% 42%,#233a49 0%,#101e2b 64%);min-width:0}#svg-viewport{padding:50px 0 26px}.canvas-breadcrumbs{max-width:calc(100% - 24px);font-size:14px;flex-wrap:wrap}#right-panel{background:#142232}.info-card p{font-size:15px;line-height:1.65}.info-card-title{font-size:12px;letter-spacing:1px}.structure-title-da{font-size:28px}.structure-title-la,.structure-title-en,.related-btn{font-size:14px}footer#bottom-bar{height:auto;min-height:74px;flex-wrap:wrap;gap:8px;padding:12px 20px;font-size:14px}.metrics-group{flex-wrap:wrap;gap:12px 25px}.disclaimer-tag{font-size:12px}.overlay-panel{position:absolute;top:55px;bottom:16px;right:12px;width:min(410px,calc(100% - 24px));max-height:none;overflow:auto;background:#17293b}.overlay-panel p,.overlay-panel [style*="font-size:12px"],.overlay-panel [style*="font-size:11px"]{font-size:14px!important;line-height:1.55!important}button:focus-visible,.anatomical-part:focus-visible{outline:3px solid #8debd9;outline-offset:3px}.anatomical-part:focus-visible{filter:drop-shadow(0 0 6px #8debd9)}.mobile-toggle{display:none}
@media(min-width:1200px){:root{--header-height:76px}.search-box{order:0;flex:1;max-width:240px}.exertion-control{order:0}header#top-header{gap:12px}}
@media(max-width:1100px){#app-container{grid-template-columns:210px 1fr 290px}.brand-title span{display:none}}
@media(max-width:900px){#app-container{grid-template-columns:200px 1fr;grid-template-areas:"header header" "sidebar canvas" "footer footer"}aside#right-panel{position:absolute;right:0;top:var(--header-height);bottom:0;width:min(360px,100%);transform:translateX(105%);z-index:36}aside#right-panel.open{transform:translateX(0)}#btn-toggle-panel{display:flex}}
@media(max-width:640px){:root{--header-height:188px}#app-container{grid-template-columns:1fr;grid-template-areas:"header" "canvas" "footer";min-height:620px}header#top-header{padding:10px 12px;gap:8px}.brand-section{flex:1}.brand-title h1{font-size:20px}.brand-logo{width:30px;height:30px}.header-actions{order:0;margin:0;gap:3px}.lang-dropdown-container{order:3;flex:1}header>.pill-group{order:2}.search-box{order:4;flex:1 1 100%}.exertion-control{order:5;width:100%;justify-content:center}.mobile-toggle{display:flex}aside#left-sidebar{position:absolute;top:var(--header-height);left:0;bottom:0;width:280px;transform:translateX(-105%);z-index:36}aside#left-sidebar.open{transform:translateX(0)}footer#bottom-bar{padding:10px 12px}.metrics-group{gap:8px 16px;font-size:12px}.disclaimer-tag{font-size:12px}.overlay-panel{top:auto;bottom:0;left:8px;right:8px;width:auto;max-height:55%}.split-slider-container input[type=range]{width:110px}}
@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation:none!important;transition:none!important}}
/* Learning panels and motion demonstrations */
[hidden]{display:none!important}.subtle{font-size:13px;color:var(--text-secondary);line-height:1.5}#structure-count{margin:0 12px 10px}.model-meta{position:absolute;top:48px;left:18px;right:18px;display:flex;justify-content:space-between;align-items:center;gap:10px;color:#b3c6d6;font-size:12px;z-index:12}.model-meta label{display:flex;align-items:center;gap:8px}.model-meta select{max-width:145px;background:#203548;border-radius:6px;padding:6px}.model-caption{position:absolute;bottom:8px;left:65px;right:8px;text-align:center;font-size:12px;color:#b3c6d6;pointer-events:none}#svg-viewport{padding-top:88px;touch-action:none}.anatomical-part text{pointer-events:none}.anatomical-part.selected>*:not(text):not(title),.quiz-target>*:not(text):not(title),.process-current>*:not(text):not(title){stroke:#ffe18b!important;stroke-width:3!important;filter:brightness(1.2)}.process-current{filter:drop-shadow(0 0 5px #ffe18b)}#right-panel{position:relative}#right-panel>.overlay-panel{inset:0;width:100%;border:0;border-radius:0;max-height:100%;box-shadow:none;background:#142232;z-index:25}#panel-close{display:none}.search-item{display:block;width:100%;text-align:left;font-size:14px;white-space:normal}.search-results-dropdown{max-height:330px;overflow:auto}.process-step-list{display:flex;flex-direction:column;gap:6px;max-height:none}.process-step-item{text-align:left;font-size:14px;padding:9px;white-space:normal}.movement-grid{grid-template-columns:1fr 1fr}.movement-select-btn{font-size:14px}.movement-select-btn small{font-size:12px}.overlay-title{font-size:18px}.overlay-close{min-width:36px;min-height:36px}.overlay-panel{gap:14px}.quiz-option-btn{display:block;width:100%;margin-bottom:8px;font-size:14px}.match-columns{display:grid;grid-template-columns:1fr 1fr;gap:8px}.match-columns>div{display:flex;flex-direction:column;gap:8px}.match-card{font-size:14px;min-height:56px}.quiz-active .anatomy-label-pin,.quiz-active #anatomy-tooltip,.hide-names .anatomy-label-pin,.hide-names #anatomy-tooltip,.hide-names .structure-names{display:none!important}.quiz-active .anatomical-part{pointer-events:auto}.quiz-active .anatomical-part title{display:none}.anatomy-label-pin{font-size:12px;fill:white;paint-order:stroke;stroke:#0b1420;stroke-width:3px}.show-teacher-labels .anatomy-label-pin{display:block}#movement-diagram{width:100%;height:190px;background:#0d1b29;border-radius:12px}#movement-demo label{display:flex;align-items:center;gap:10px;margin-top:10px}#movement-range{width:100%}#process-track{padding:16px;background:#0d1b29;border:1px solid #2b4053;border-radius:12px;font-size:14px}.particle-rail{height:2px;background:#4b667a;position:relative;margin:20px 4px}.particle-rail::after{content:'›';position:absolute;right:0;top:-12px;font-size:22px}#process-particle{position:absolute;top:-5px;left:8%;width:12px;height:12px;border-radius:50%;background:#67e8f9}#exertion-description{flex-basis:100%;margin:0;max-width:1100px}footer#bottom-bar{gap:5px;min-height:95px}dialog{margin:auto;width:min(620px,92vw);max-height:86vh;overflow:auto;background:#152637;color:#eaf2f8;border:1px solid #436176;border-radius:18px;padding:26px;line-height:1.6}dialog p,dialog ul{margin:12px 0}dialog::backdrop{background:#07111cc9}#status-message:not(:empty),#storage-note:not([hidden]){position:fixed;bottom:18px;left:20px;right:20px;background:#214b5c;color:white;padding:12px;border-radius:10px;z-index:100}button:disabled{opacity:.6;cursor:default}.active-agonist>*:not(text):not(title){stroke:#6ee7b7!important}.active-antagonist>*:not(text):not(title){stroke:#f8ac72!important}.split-slider-container{bottom:35px}.layer-opacity-slider{width:100%}#drill-links{display:flex;flex-wrap:wrap;gap:8px}
@media(max-width:900px){aside#right-panel{position:absolute}#panel-close{display:block;margin:0 0 12px auto}.model-meta{font-size:12px}#view-note{max-width:180px}.overlay-panel{top:auto;bottom:30px;max-height:48%;}.overlay-panel:has(#quiz-question-box){max-height:44%}#canvas-container:has(.overlay-panel[style*="flex"]) #svg-viewport{padding-bottom:240px}}
@media(max-width:640px){#view-note{display:none} .model-meta{top:38px;justify-content:flex-end} .canvas-breadcrumbs{left:10px;top:10px;font-size:12px}#svg-viewport{padding-top:70px}.overlay-panel{bottom:24px;max-height:48%}#canvas-container:has(.overlay-panel[style*="flex"]) #svg-viewport{padding:10px 0 190px}.model-caption{font-size:10px;left:55px}.metrics-group{font-size:12px}#exertion-description{display:none}#app-container{min-height:720px} .canvas-tool-btn{width:40px;height:40px} .canvas-toolbar{left:6px;bottom:30px}#region-select{font-size:14px} }
#app-container{grid-template-rows:var(--header-height) minmax(0,1fr) auto}aside#left-sidebar,aside#right-panel{height:auto;min-height:0}main#canvas-container{min-height:0;background:radial-gradient(ellipse at 50% 40%,#253b49,#101d2b 70%)}.layer-row{display:grid;grid-template-columns:minmax(0,1fr) 24px;gap:8px;padding:10px}.layer-row .layer-opacity-slider{grid-column:1/-1;width:100%;margin:0}.layer-row input[type=checkbox]{width:20px;height:20px}.info-card p{color:#c2d0df}.search-box input{font-size:14px}#process-from,#process-to{display:block}.btn-play{min-height:36px;font-size:14px}
@media(min-width:641px) and (max-width:900px){aside#left-sidebar{position:relative;top:0;bottom:auto;width:auto;transform:none}.mobile-toggle{display:none}#btn-toggle-panel{display:flex}.overlay-panel{max-height:52%;width:calc(100% - 20px);left:10px;right:10px}}
@media(max-width:640px){header#top-header{align-content:center;gap:6px}.header-actions .icon-btn{width:30px}.brand-section{gap:6px}.brand-title h1{font-size:18px}.brand-logo{display:none}.model-caption{font-size:12px}.disclaimer-tag{font-size:12px}footer#bottom-bar{min-height:108px}.overlay-panel{max-height:48%;padding:12px;gap:10px}.overlay-panel:has(#quiz-question-box){max-height:48%}.quiz-active #canvas-container #svg-viewport{padding-bottom:260px!important}.quiz-active .canvas-toolbar{bottom:52%}.quiz-active .model-meta{display:none}}
@media(max-width:640px){:root{--header-height:226px}#app-container{min-height:790px}header#top-header{align-content:flex-start;padding-top:10px}.brand-section{min-height:32px}.header-actions{min-height:32px}.lang-dropdown-container{flex:1 1 100%}.lang-select{max-width:100%}.canvas-breadcrumbs{max-width:calc(100% - 20px)}aside#left-sidebar,aside#right-panel{bottom:0}}
@media(max-width:640px){#overlay-quiz .overlay-header{order:-3}#quiz-question-box{order:-2}#quiz-question-box{padding:10px;margin-top:0!important}#quiz-question-text{font-size:14px!important}#quiz-mode-title{display:none}.quiz-active #canvas-container #svg-viewport{padding:55px 0 285px!important}.quiz-active .canvas-toolbar{bottom:52%}#overlay-quiz .pill-group{flex-wrap:wrap;border-radius:12px}#overlay-quiz .pill-group button{flex:1 1 40%!important;padding:6px;min-height:34px}}
.structure-names .primary-name{font-size:28px;font-weight:700;line-height:1.2;color:var(--text-primary);font-style:normal}
</style>
</head>
<body>
<div id="app-container">
<!-- ==========================================================================
TOPBJÆLKE / HEADER
========================================================================== -->
<header id="top-header">
<div class="brand-section">
<button class="icon-btn mobile-toggle" id="btn-toggle-sidebar" title="Menu" aria-label="Åbn menu">
<svg viewBox="0 0 24 24"><path d="M4 6h16M4 12h16M4 18h16"/></svg>
</button>
<div class="brand-logo">A.</div>
<div class="brand-title">
<h1>AnatomiLab</h1>
<span>Læreruddannelsen</span>
</div>
</div>
<!-- Visningsvælger (Forfra / Bagfra / Split) -->
<div class="pill-group" role="group" aria-label="Visningsvinkel">
<button class="pill-btn active" id="btn-view-anterior" data-view="anterior" title="Anterior visning">
Forfra
</button>
<button class="pill-btn" id="btn-view-posterior" data-view="posterior" title="Posterior visning">
Bagfra
</button>
<button class="pill-btn" id="btn-view-split" data-view="split" title="Eksploderet anatomisk lagvisning">
Split kroppen
</button>
</div>
<!-- Sprogindstilling -->
<div class="lang-dropdown-container">
<select id="lang-select" class="lang-select" title="Vælg sprogvisning" aria-label="Sprogvisning">
<option value="da">Dansk</option>
<option value="la">Latin</option>
<option value="en">English</option>
<option value="da-la" selected>Dansk + Latin</option>
<option value="da-en">Dansk + Engelsk</option>
<option value="all">Alle tre sprog</option>
</select>
</div>
<!-- Fysiologisk tilstand / Belastning -->
<div class="exertion-control" role="group" aria-label="Idrætsfaglig belastningstilstand">
<button class="exertion-btn active" data-state="rest" title="Hviletilstand">Hvile</button>
<button class="exertion-btn" data-state="moderate" title="Moderat fysisk aktivitet">Moderat</button>
<button class="exertion-btn" data-state="heavy" title="Hård fysisk aktivitet">Hård aktivitet</button>
</div>
<!-- Søgning -->
<div class="search-box">
<input type="text" id="search-input" placeholder="Søg muskel, knogle, organ..." aria-label="Søg i anatomi">
<svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
<div id="search-results" class="search-results-dropdown"></div>
</div>
<!-- Handlinger (Underviser, Favoritter, Fuldskærm) -->
<div class="header-actions"><button class="icon-btn" id="btn-help" title="Hjælp og faglige kilder" aria-label="Hjælp">?</button>
<button class="icon-btn" id="btn-teacher-mode" title="Underviserfunktion (Blind test / Skjul navne)" aria-label="Undervisertilstand">
<svg viewBox="0 0 24 24"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg>
</button>
<button class="icon-btn" id="btn-show-favorites" title="Mine gemte fokuspunkter (Favoritter)" aria-label="Favoritter">
<svg viewBox="0 0 24 24"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
</button>
<button class="icon-btn" id="btn-fullscreen" title="Fuldskærm" aria-label="Fuldskærm">
<svg viewBox="0 0 24 24"><path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/></svg>
</button>
<button class="icon-btn mobile-toggle" id="btn-toggle-panel" title="Infopanel" aria-label="Vis informationspanel">
<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>
</button>
</div>
</header>
<!-- ==========================================================================
VENSTRE PANEL / NAVIGATION
========================================================================== -->
<aside id="left-sidebar" aria-label="Systemer og lag"><button class="mobile-toggle pill-btn" id="sidebar-close">Luk menu ×</button>
<!-- Hovedsystemer -->
<div>
<div class="sidebar-section-title">Udforsk kroppen</div><p id="structure-count" class="subtle"></p>
<ul class="nav-list">
<li>
<button class="nav-item-btn active" data-nav="all">
<svg viewBox="0 0 24 24"><circle cx="12" cy="5" r="3"/><path d="M12 8v13M9 13l3-2 3 2M9 21l3-4 3 4"/></svg>
<span>Hele kroppen</span>
</button>
</li>
<li>
<button class="nav-item-btn" data-nav="skeleton">
<svg viewBox="0 0 24 24"><path d="M12 2a4 4 0 0 0-4 4c0 3 4 5 4 5s4-2 4-5a4 4 0 0 0-4-4zM8 14h8M9 18h6M12 11v11"/></svg>
<span>Skelet</span>
<span class="nav-badge">16</span>
</button>
</li>
<li>
<button class="nav-item-btn" data-nav="muscles">
<svg viewBox="0 0 24 24"><path d="M6 16.5l-3-3 6-6 3 3M18 7.5l3 3-6 6-3-3M8.5 14l7-7"/></svg>
<span>Muskler</span>
<span class="nav-badge">18</span>
</button>
</li>
<li>
<button class="nav-item-btn" data-nav="organs">
<svg viewBox="0 0 24 24"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>
<span>Organer</span>
</button>
</li>
<li>
<button class="nav-item-btn" data-nav="circulation">
<svg viewBox="0 0 24 24"><path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>
<span>Kredsløb & Kar</span>
</button>
</li>
<li>
<button class="nav-item-btn" data-nav="respiratory">
<svg viewBox="0 0 24 24"><path d="M12 3v9M8 8a4 4 0 0 0-4 4v2a5 5 0 0 0 5 5h2M16 8a4 4 0 0 1 4 4v2a5 5 0 0 1-5 5h-2"/></svg>
<span>Respiration & Lunger</span>
</button>
</li>
<li>
<button class="nav-item-btn" data-nav="digestive">
<svg viewBox="0 0 24 24"><path d="M12 3c-4 0-6 3-6 6v3c0 3 2 5 6 5s6-2 6-5V9c0-3-2-6-6-6zM6 15c0 4 3 6 6 6s6-2 6-6"/></svg>
<span>Fordøjelse & Energi</span>
</button>
</li>
<li>
<button class="nav-item-btn" data-nav="nervous">
<svg viewBox="0 0 24 24"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg>
<span>Nervesystem</span>
</button>
</li>
</ul>
</div>
<button class="nav-item-btn" data-nav="joints">◎ Led, sener og ledbånd</button>
<!-- Idrætsdidaktik & Funktioner -->
<div>
<div class="sidebar-section-title">Idrætsundervisning</div>
<ul class="nav-list">
<li>
<button class="nav-item-btn" id="btn-open-movement" data-overlay="movement">
<svg viewBox="0 0 24 24"><circle cx="18" cy="5" r="3"/><path d="M13 8.5l-4 4 3 4M5 19l4-4 2 2 3-3"/></svg>
<span>Bevægelse</span>
</button>
</li>
<li>
<button class="nav-item-btn" id="btn-open-processes" data-overlay="processes">
<svg viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21 5 3"/></svg>
<span>Fysiologiske Forløb</span>
</button>
</li>
<li>
<button class="nav-item-btn" id="btn-open-quiz" data-overlay="quiz">
<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3M12 17h.01"/></svg>
<span>Træn din viden</span>
</button>
</li>
</ul>
</div>
<!-- Lag-styring -->
<div>
<div class="sidebar-section-title">Anatomiske Lag</div>
<div class="layers-container" id="layers-control">
<div class="layer-row">
<label class="layer-label">
<span class="layer-dot" style="background:#94a3b8;"></span>
<span>Ydre krop / Silhouette</span>
</label>
<input type="checkbox" id="layer-check-silhouette" checked data-layer="silhouette">
<input type="range" class="layer-opacity-slider" min="0" max="1" step="0.05" value="0.4" data-layer-opacity="silhouette" title="Gennemsigtighed">
</div>
<div class="layer-row">
<label class="layer-label">
<span class="layer-dot" style="background:var(--color-bone);"></span>
<span>Skelet</span>
</label>
<input type="checkbox" id="layer-check-skeleton" checked data-layer="skeleton">
<input type="range" class="layer-opacity-slider" min="0" max="1" step="0.05" value="1" data-layer-opacity="skeleton">
</div>
<div class="layer-row">
<label class="layer-label">
<span class="layer-dot" style="background:var(--color-muscle);"></span>
<span>Muskler</span>
</label>
<input type="checkbox" id="layer-check-muscles" checked data-layer="muscles">
<input type="range" class="layer-opacity-slider" min="0" max="1" step="0.05" value="0.9" data-layer-opacity="muscles">
</div>
<div class="layer-row">
<label class="layer-label">
<span class="layer-dot" style="background:var(--color-organ);"></span>
<span>Organer</span>
</label>
<input type="checkbox" id="layer-check-organs" checked data-layer="organs">
<input type="range" class="layer-opacity-slider" min="0" max="1" step="0.05" value="0.95" data-layer-opacity="organs">
</div>
<div class="layer-row">
<label class="layer-label">
<span class="layer-dot" style="background:var(--color-artery);"></span>
<span>Kredsløb</span>
</label>
<input type="checkbox" id="layer-check-circulation" checked data-layer="circulation">
<input type="range" class="layer-opacity-slider" min="0" max="1" step="0.05" value="0.85" data-layer-opacity="circulation">
</div>
<div class="layer-row">
<label class="layer-label">
<span class="layer-dot" style="background:var(--color-nerve);"></span>
<span>Nervesystem</span>
</label>
<input type="checkbox" id="layer-check-nervous" checked data-layer="nervous">
<input type="range" class="layer-opacity-slider" min="0" max="1" step="0.05" value="0.8" data-layer-opacity="nervous">
</div>
</div>
</div>
</aside>
<!-- ==========================================================================
MIDTER-OMRÅDE / CANVAS MED INTERAKTIV SVG
========================================================================== -->
<main id="canvas-container">
<div class="canvas-breadcrumbs" id="breadcrumbs">
<span class="breadcrumb-item" data-zoom-target="all">Menneskekrop</span>
<span class="breadcrumb-sep">/</span>
<span class="breadcrumb-item active" id="breadcrumb-current">Hele kroppen</span>
</div>
<div class="canvas-toolbar">
<button class="canvas-tool-btn" id="btn-zoom-in" title="Zoom ind (eller rul med mus)"><svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><line x1="11" y1="8" x2="11" y2="14"/><line x1="8" y1="11" x2="14" y2="11"/></svg></button>
<button class="canvas-tool-btn" id="btn-zoom-out" title="Zoom ud"><svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><line x1="8" y1="11" x2="14" y2="11"/></svg></button>
<button class="canvas-tool-btn" id="btn-zoom-reset" title="Nulstil visning"><svg viewBox="0 0 24 24"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg></button>
</div>
<div class="split-slider-container" id="split-slider-wrap">
<span>Lagafstand:</span>
<input type="range" id="split-slider" min="50" max="220" value="130" title="Træk lagene fra hinanden">
</div>
<div id="anatomy-tooltip">
<div class="tt-type">Knogle</div>
<div class="tt-name-da">Lårben</div>
<div class="tt-name-la">Os femoris</div>
</div>
<div class="model-meta"><span id="view-note">Forfra · kroppens højre side er til venstre på skærmen</span><label>Fokus <select id="region-select"><option value="body">Hele kroppen</option><option value="head">Hoved</option><option value="torso">Kropstamme</option><option value="arm">Arm</option><option value="leg">Ben</option><option value="knee">Knæ</option><option value="heart">Hjerte</option></select></label></div><div id="svg-viewport">
<svg id="anatomy-svg" class="anatomy-svg" viewBox="0 0 600 1000" preserveAspectRatio="xMidYMid meet" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="grad-muscle" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#fb7185" />
<stop offset="50%" stop-color="#e11d48" />
<stop offset="100%" stop-color="#9f1239" />
</linearGradient>
<linearGradient id="grad-bone" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#f8fafc" />
<stop offset="100%" stop-color="#cbd5e1" />
</linearGradient>
<linearGradient id="grad-heart" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#ef4444" />
<stop offset="100%" stop-color="#b91c1c" />
</linearGradient>
<linearGradient id="grad-lungs" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#67e8f9" />
<stop offset="100%" stop-color="#0891b2" />
</linearGradient>
<linearGradient id="grad-liver" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#d97706" />
<stop offset="100%" stop-color="#92400e" />
</linearGradient>
<linearGradient id="grad-digestive" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#f59e0b" />
<stop offset="100%" stop-color="#b45309" />
</linearGradient>
<filter id="glow-highlight" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur stdDeviation="4" result="blur" />
<feComposite in="SourceGraphic" in2="blur" operator="over" />
</filter>
</defs>
<g id="svg-transform-root">
<!-- Lag tegnes dynamisk her -->
</g>
</svg>
</div>
<p class="model-caption">Skematisk undervisningsmodel · ikke anatomisk målfast</p>
<!-- OVERLAY 1: BEVÆGELSESMODUL -->
<div class="overlay-panel" id="overlay-movement">
<div class="overlay-header">
<div class="overlay-title">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="18" cy="5" r="3"/><path d="M13 8.5l-4 4 3 4M5 19l4-4 2 2 3-3"/></svg>
<span>Biomekanik & Bevægelse</span>
</div>
<button class="overlay-close" data-close="movement" title="Luk">×</button>
</div>
<p style="font-size:12px; color:var(--text-secondary);">
Vælg en bevægelse for at se samspillet mellem <strong>agonister</strong> (primære motorer) og <strong>antagonister</strong> (bremsende modspillere):
</p>
<div class="movement-grid" id="movement-button-list"></div>
<div id="movement-demo" hidden><svg id="movement-diagram" viewBox="0 0 320 215" role="img" aria-label="Skematisk bevægelse"></svg><button id="movement-toggle" class="pill-btn">Afspil bevægelse</button><label class="subtle">Bevægelsesfase<input id="movement-range" type="range" min="0" max="100" value="0"></label><p class="subtle">Forenklet ledmodel. Træk i skyderen eller afspil.</p></div><div id="movement-details-card" class="info-card didaktik-card" style="display:none;">
<div class="info-card-title">Analyse af bevægelse</div>
<div id="movement-active-name" style="font-weight:700; font-size:14px; color:white;"></div>
<div style="font-size:12px; margin-top:4px;">
<div><strong style="color:#22c55e;">Agonist:</strong> <span id="movement-agonist-desc"></span></div>
<div style="margin-top:2px;"><strong style="color:#f97316;">Antagonist:</strong> <span id="movement-antagonist-desc"></span></div>
<div style="margin-top:2px;"><strong style="color:var(--accent-sky);">Ledfunktion:</strong> <span id="movement-joint-desc"></span></div>
</div>
<div id="movement-didactic-text" style="font-size:12px; color:var(--text-secondary); margin-top:8px; line-height:1.4;"></div>
</div>
</div>
<!-- OVERLAY 2: FYSIOLOGISKE FORLØB -->
<div class="overlay-panel" id="overlay-processes">
<div class="overlay-header">
<div class="overlay-title">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="5 3 19 12 5 21 5 3"/></svg>
<span>Fysiologiske Forløb</span>
</div>
<button class="overlay-close" data-close="processes" title="Luk">×</button>
</div>
<div class="pill-group" style="width:100%;">
<button class="pill-btn active" style="flex:1;" data-process="oxygen">Iltens vej</button>
<button class="pill-btn" style="flex:1;" data-process="blood">Blodets vej</button>
<button class="pill-btn" style="flex:1;" data-process="digestion">Madens vej</button>
</div>
<div id="process-track"><span id="process-from"></span><div class="particle-rail"><i id="process-particle"></i></div><span id="process-to"></span></div><p class="subtle">Partiklen viser rækkefølge, ikke fysisk hastighed eller afstand.</p><div class="process-controller">
<div class="process-playback-bar">
<button class="btn-play" id="btn-process-play">
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg>
<span id="btn-process-play-text">Start forløb</span>
</button>
<button class="pill-btn" id="btn-process-reset">Nulstil</button>
<select aria-label="Afspilningshastighed" id="process-speed" class="lang-select" style="padding:3px 8px; font-size:11px;">
<option value="0.5">0.5x</option>
<option value="1" selected>1.0x</option>
<option value="2">2.0x</option>
</select>
</div>
<div id="process-explanation" style="font-size:12px; line-height:1.4; color:var(--text-primary); margin-top:4px;">
Tryk på 'Start forløb' for at følge processen gennem kroppens anatomiske strukturer.
</div>
<div class="process-step-list" id="process-step-indicators"></div>
</div>
</div>
<!-- OVERLAY 3: TRÆN DIN VIDEN (QUIZ MODUL) -->
<div class="overlay-panel" id="overlay-quiz">
<div class="overlay-header">
<div class="overlay-title">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3M12 17h.01"/></svg>
<span>Træn din viden</span>
</div>
<button class="overlay-close" data-close="quiz" title="Luk">×</button>
</div>
<div class="pill-group" style="width:100%; flex-wrap:wrap;">
<button class="pill-btn active" data-quiztype="find" style="flex:1;">Find på kroppen</button>
<button class="pill-btn" data-quiztype="name" style="flex:1;">Hvad hedder den?</button>
<button class="pill-btn" data-quiztype="match" style="flex:1;">Dansk ↔ Latin</button>
<button class="pill-btn" data-quiztype="movement" style="flex:1;">Muskel & Bevægelse</button>
</div>
<button class="pill-btn" data-quiztype="sequence">Følg processen</button><div id="quiz-question-box" class="info-card" style="margin-top:6px;">
<div class="info-card-title" id="quiz-mode-title">Opgave</div>
<p id="quiz-question-text" style="font-weight:600; font-size:14px; color:var(--text-primary);"></p>
<div id="quiz-instruction" style="font-size:11px; color:var(--text-muted);"></div>
</div>
<div id="quiz-interaction-area"></div>
<div aria-live="polite" id="quiz-feedback-box" class="quiz-feedback"></div>
<div style="display:flex; justify-content:space-between; align-items:center; margin-top:8px;">
<span style="font-size:12px; color:var(--text-secondary);">Score: <strong id="quiz-score">0</strong> rigtige</span>
<button class="pill-btn" id="btn-next-question" style="background:var(--accent-blue); color:white;">Næste opgave →</button>
</div>
</div>
</main>
<!-- ==========================================================================
HØJRE INFORMATIONSPANEL (DETALJER OM VALGT STRUKTUR)
========================================================================== -->
<aside id="right-panel" aria-label="Strukturinformation"><button class="pill-btn" id="panel-close">Luk information ×</button>
<div class="panel-empty-state" id="panel-empty">
<svg viewBox="0 0 24 24" fill="none" stroke-width="1.5">
<circle cx="12" cy="12" r="10"/><path d="M12 16v-4M12 8h.01"/>
</svg>
<h3>Udforsk kroppen</h3>
<p>Klik på en muskel, knogle eller et organ på kroppen for at se fysiologisk funktion, udspring/hæfte og idrætsdidaktisk betydning.</p>
<div style="margin-top:10px;">
<button class="pill-btn" id="btn-demo-select" style="border:1px solid var(--border-color);">Prøv: Vælg Lårben (Femur)</button>
</div>
</div>
<div class="panel-content" id="panel-active-content" style="display:none;">
<div class="structure-header">
<div class="structure-type-badge-row">
<span class="structure-type-badge" id="info-type">Knogle</span>
<button class="btn-favorite" id="btn-toggle-fav">
<svg viewBox="0 0 24 24"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
<span id="btn-fav-text">Fokusemne</span>
</button>
</div>
<button class="pill-btn" id="btn-focus-structure">⊕ Dyk ned i strukturen</button><div class="structure-names">
<div class="structure-title-da" id="info-name-da">Lårben</div>
<div class="structure-title-la" id="info-name-la">Os femoris / Femur</div>
<div class="structure-title-en" id="info-name-en">Thigh bone / Femur</div>
</div>
</div>
<div class="info-card">
<div class="info-card-title">
<svg viewBox="0 0 24 24" fill="none" stroke-width="2"><circle cx="12" cy="10" r="3"/><path d="M12 2a8 8 0 0 0-8 8c0 5 8 12 8 12s8-7 8-12a8 8 0 0 0-8-8z"/></svg>
Placering & Opbygning
</div>
<p id="info-location"></p>
</div>
<div class="info-card biomechanics-card">
<div class="info-card-title">
<svg viewBox="0 0 24 24" fill="none" stroke-width="2"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg>
Funktion & Biomekanik
</div>
<p id="info-function"></p>
</div>
<div class="info-card" id="info-muscle-attachments-card" style="display:none;">
<div class="info-card-title">Udspring & Hæfte (Origo & Insertio)</div>
<p id="info-attachments"></p>
</div>
<div class="info-card didaktik-card">
<div class="info-card-title">
<svg viewBox="0 0 24 24" fill="none" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
Idrætsrelevans & Undervisning
</div>
<p id="info-didactic"></p>
</div>
<div class="info-card" id="drill-card" hidden><div class="info-card-title">Udforsk underdele</div><div id="drill-links" class="related-tags"></div></div><div class="info-card">
<div class="info-card-title">Relaterede strukturer</div>
<div class="related-tags" id="info-related-list"></div>
</div>
</div>
</aside>
<!-- ==========================================================================
STATUSLINJE / FYSIOLOGI DASHBOARD
========================================================================== -->
<footer id="bottom-bar">
<div class="metrics-group">
<div class="metric-item" title="Aktuel hjertefrekvens i den valgte idrætstilstand">
<span class="heart-icon">♥</span>
<span>Puls:</span>
<strong id="metric-hr">65 bpm</strong>
</div>
<div class="metric-item" title="Respirationsfrekvens (åndedrag pr. minut)">
<span>Vejrtrækning:</span>
<strong id="metric-rf">12 /min</strong>
</div>
<div class="metric-item" title="Hjertets minutvolumen (liter blod pumpet pr. minut)">
<span>Minutvolumen (Q):</span>
<strong id="metric-q">5.0 L/min</strong>
</div>
<div class="metric-item" title="Andel af minutvolumen dirigeret til arbejdende skeletmuskler">
<span>Muskelblodgennemstrømning:</span>
<strong id="metric-muscleflow" style="color:var(--accent-emerald);">~18%</strong>
</div>
</div>
<p id="exertion-description" class="subtle"></p><div class="disclaimer-tag">
* Fysiologiske tal er didaktiske modeller til læreruddannelsen og ikke individuelle medicinske målinger.
</div>
</footer>
</div>
<div class="overlay-panel" id="overlay-favorites"><div class="overlay-header"><h2 class="overlay-title">Mine fokusemner</h2><button class="overlay-close" data-close="favorites" aria-label="Luk favoritter">×</button></div><div id="favorite-list" class="related-tags"></div></div>
<div class="overlay-panel" id="overlay-teacher"><div class="overlay-header"><h2 class="overlay-title">Undervisning</h2><button class="overlay-close" data-close="teacher" aria-label="Luk undervisertilstand">×</button></div><label for="teacher-mode">Navne på modellen</label><select id="teacher-mode" class="lang-select"><option value="none">Vis ved valg</option><option value="all">Vis alle synlige navne</option><option value="hidden">Skjul navne</option></select><p id="teacher-status">Navne vises ved valg</p><p>Vælg systemer i lagpanelet og strukturer via søgning. Brug fokus til at vise én region på storskærm.</p><button class="pill-btn" id="teacher-quiz">Start fælles quiz</button></div>
<dialog id="help-dialog" aria-labelledby="help-title"><button id="help-close" class="pill-btn">Luk ×</button><h2 id="help-title">Kroppen i bevægelse</h2><p>Vælg et system, og klik på kroppen. Kombinér lag eller justér deres gennemsigtighed. Søg på dansk, engelsk eller latin.</p><p>Zoom med + og −, eller vælg en region under Fokus. Du kan trække modellen med mus eller finger. Dyk ned via strukturens underdele, og gå tilbage med stien over modellen.</p><p>Tab flytter fokus; Enter eller mellemrum vælger en struktur. Escape lukker paneler. Quizzer skjuler etiketter. På små skærme kan menu og information åbnes og lukkes fra topbjælken.</p><h3>Om modellen</h3><p>Figuren er en skematisk prototype. Størrelser, afstande, karforgreninger og lagadskillelse er forenklede. Parrede strukturer vælges samlet. Hjertets små felter viser fire kamre som et diagram; de er ikke en anatomisk gennemskæring. Farven blå angiver iltfattigt blod, som i virkeligheden er mørkerødt.</p><h3>Fagligt grundlag</h3><p>Funktioner og undervisningseksempler er korte, selvstændige faglige sammenfatninger. Læs videre i OpenStax Anatomy & Physiology 2e:</p><ul><li><a href="https://openstax.org/books/anatomy-and-physiology-2e/pages/11-5-muscles-of-the-pectoral-girdle-and-upper-limbs" target="_blank" rel="noopener">Muskler og bevægelse</a></li><li><a href="https://openstax.org/books/anatomy-and-physiology-2e/pages/19-1-heart-anatomy" target="_blank" rel="noopener">Hjertets anatomi</a></li><li><a href="https://openstax.org/books/anatomy-and-physiology-2e/pages/23-2-digestive-system-processes-and-regulation" target="_blank" rel="noopener">Fordøjelsens processer</a></li></ul><p>Appen fungerer offline; kildelinks kræver internet. Favoritter og indstillinger gemmes kun i denne browser. Hvis browseren blokerer lagring, fungerer appen stadig i den aktuelle session.</p></dialog>
<div id="status-message" role="status"></div><p id="storage-note" hidden>Browseren tillader ikke lagring. Indstillinger bevares kun i denne session.</p>
<!-- ==========================================================================
JAVASCRIPT: MODULÆR STRUKTUR TIL INTERAKTIV ANATOMI
========================================================================== -->
<script>
/**
* IDRÆTANATOMI & FYSIOLOGI - LÆRERUDDANNELSEN
* Arkitektur: Modulært struktureret applikation i ren Vanilla JS.
* Kan opdeles i separate moduler eller køres 100% selvstændigt i OneCompiler.com.
*/
// =============================================================================
// MODUL 1: CENTRAL ANATOMISK DATABASE (AnatomyData)
// =============================================================================
// EXTENSION POINT: Add anatomical structures and learning data here.
// Schema: id, type, names {da,en,la}, parent, layer, view, location, function, didactic, related.
const AnatomyData = {
"bones": {
"cranium": {
"id": "cranium",
"type": "bone",
"names": {
"da": "Kranium",
"la": "Cranium",
"en": "Skull"
},
"location": "Hovedets skeletstruktur bestående af hjernekassen og ansigtsskelettet.",
"function": "Beskytter hjernen og sanseorganer mod stød og traumer. Danner fæste for tyggemuskulatur og halsmuskler.",
"didactic": "Sammenlign kraniets beskyttende funktion med de lange knoglers rolle i bevægelse.",
"related": [
"brain",
"cervical_spine",
"trapezius"
],
"layer": "skeleton",
"parent": "head",
"tags": [
"bone",
"skeleton"
],
"view": "anterior"
},
"cervical_spine": {
"id": "cervical_spine",
"type": "bone",
"names": {
"da": "Halshvirvler",
"la": "Vertebrae cervicales (C1-C7)",
"en": "Cervical spine"
},
"location": "Øverste del af rygsøjlen mellem kranium og brysthvirvler.",
"function": "Høj bevægelighed (fleksion, ekstension, lateral fleksion, rotation). Bærer hovedets vægt via atlas (C1) og axis (C2).",
"didactic": "Undersøg nakkens bevægelser roligt og uden ydre belastning.",
"related": [
"cranium",
"thoracic_spine",
"trapezius"
],
"layer": "skeleton",
"parent": "torso",
"tags": [
"bone",
"skeleton"
],
"view": "anterior"
},
"thoracic_spine": {
"id": "thoracic_spine",
"type": "bone",
"names": {
"da": "Brysthvirvler",
"la": "Vertebrae thoracicae (Th1-Th12)",
"en": "Thoracic spine"
},
"location": "Midterste sektion af rygsøjlen, forbundet med ribbenene.",
"function": "Artikulerer med costae (ribben) og danner brystkassen. Begrænset ekstension, men tillader rotation som er essentiel ved rotationssport.",
"didactic": "Observer rotation af brystkassen under kast.",
"related": [
"ribs",
"lumbar_spine",
"latissimus_dorsi"
],
"layer": "skeleton",
"parent": "torso",
"tags": [
"bone",
"skeleton"
],
"view": "anterior"
},
"lumbar_spine": {
"id": "lumbar_spine",
"type": "bone",
"names": {
"da": "Lændehvirvler",
"la": "Vertebrae lumbales (L1-L5)",
"en": "Lumbar spine"
},
"location": "Nederste del af den bevægelige rygsøjle mellem brysthvirvler og korsben.",
"function": "Bærer den største del af kropsvægten. Solide hvirvellegemer med kraftige disci intervertebrales. Hovedsageligt fleksion og ekstension.",
"didactic": "Undersøg samspillet mellem ryg, hofter og bugmuskler ved løft.",
"related": [
"pelvis",
"thoracic_spine",
"rectus_abdominis"
],
"layer": "skeleton",
"parent": "torso",
"tags": [
"bone",
"skeleton"
],
"view": "anterior"
},
"clavicle": {
"id": "clavicle",
"type": "bone",
"names": {
"da": "Kraveben",
"la": "Clavicula",
"en": "Clavicle / Collarbone"
},
"location": "S-formet knogle mellem brystbenet (sternum) og skulderbladets acromion.",
"function": "Fungerer som en mekanisk afstiver (strut), der holder skulderleddet ude fra kroppen for maksimal bevægelsesfrihed.",
"didactic": "Find forbindelsen mellem brystben og skulder på modellen.",
"related": [
"sternum",
"scapula",
"deltoideus",
"pectoralis_major"
],
"layer": "skeleton",
"parent": "arm",
"tags": [
"bone",
"skeleton"
],
"view": "anterior"
},
"scapula": {
"id": "scapula",
"type": "bone",
"names": {
"da": "Skulderblad",
"la": "Scapula",
"en": "Shoulder blade"
},
"location": "Flad trekantet knogle på bagsiden af brystkassen.",
"function": "Danner ledskål (cavitas glenoidalis) for overarmen. Bevæger sig frit over brystkassen (scapulothorakale glideled).",
"didactic": "Observer skulderbladets bevægelse, når armen løftes.",
"related": [
"clavicle",
"humerus",
"trapezius",
"deltoideus",
"latissimus_dorsi"
],
"layer": "skeleton",
"parent": "arm",
"tags": [
"bone",
"skeleton"
],
"view": "posterior"
},
"sternum": {
"id": "sternum",
"type": "bone",
"names": {
"da": "Brystben",
"la": "Sternum",
"en": "Breastbone / Sternum"
},
"location": "Flad knogle midt foran på brystkassen.",
"function": "Hæfte for kraveben og de øverste ribbensbruske. Beskytter hjerte og lunger.",
"didactic": "Undersøg brystbenets forbindelse til kraveben og ribben.",
"related": [
"ribs",
"clavicle",
"pectoralis_major",
"heart"
],
"layer": "skeleton",
"parent": "torso",
"tags": [
"bone",
"skeleton"
],
"view": "anterior"
},
"ribs": {
"id": "ribs",
"type": "bone",
"names": {
"da": "Ribben",
"la": "Costae (1-12)",
"en": "Ribs"
},
"location": "12 par buede knogler der omslutter brysthulen.",
"function": "Beskytter vitale organer. Bevæges af åndedrætsmuskler (mm. intercostales) under inspiration og eksspiration.",
"didactic": "Observer brystkassens bevægelser ved rolig og dyb vejrtrækning.",
"related": [
"sternum",
"thoracic_spine",
"lungs",
"heart"
],
"layer": "skeleton",
"parent": "torso",
"tags": [
"bone",
"skeleton"
],
"view": "anterior"
},
"pelvis": {
"id": "pelvis",
"type": "bone",
"names": {
"da": "Bækken",
"la": "Pelvis",
"en": "Pelvis"
},
"location": "Knoglering bestående af hoftebenene (ilium, ischium, pubis) og korsbenet (sacrum).",
"function": "Overfører kroppens vægt fra rygsøjlen til benene. Danner ledskål (acetabulum) for hofteleddet.",
"didactic": "Sammenlign bækkenets og hofternes stilling i gang og squat.",
"related": [
"femur",
"lumbar_spine",
"gluteus_maximus",
"rectus_abdominis"
],
"layer": "skeleton",
"parent": "torso",
"tags": [
"bone",
"skeleton"
],
"view": "anterior"
},
"humerus": {
"id": "humerus",
"type": "bone",
"names": {
"da": "Overarmsknogle",
"la": "Humerus",
"en": "Humerus"
},
"location": "Knogle i overarmen mellem skulderled og albueled.",
"function": "Løftestang ved kast, skub, træk og svømmetag. Kugleleddet giver kroppens største bevægelsesudslag.",
"didactic": "Kastelængde afhænger biomekanisk af drejningsmoment og vægtstangsarme gennem humerus og underarm.",
"related": [
"scapula",
"radius",
"ulna",
"biceps",
"triceps",
"deltoideus"
],
"layer": "skeleton",
"parent": "arm",
"tags": [
"bone",
"skeleton"
],
"view": "anterior"
},
"radius": {
"id": "radius",
"type": "bone",
"names": {
"da": "Spoleben",
"la": "Radius",
"en": "Radius"
},
"location": "Knogle på tommelfingersiden af underarmen.",
"function": "Roterer omkring ulna ved pronation og supination (fx ved skruede kast eller ketchersport).",
"didactic": "Drej håndfladen op og ned, og følg spolebenets bevægelse på modellen.",
"related": [
"ulna",
"humerus",
"hand_bones",
"biceps"
],
"layer": "skeleton",
"parent": "arm",
"tags": [
"bone",
"skeleton"
],
"view": "anterior"
},
"ulna": {
"id": "ulna",
"type": "bone",
"names": {
"da": "Albueben",
"la": "Ulna",
"en": "Ulna"
},
"location": "Knogle på lillefingersiden af underarmen, danner albuens spids (olecranon).",
"function": "Stabilt hængselled med humerus, der tillader fleksion og ekstension af armen.",
"didactic": "Olecranon fungerer som vægtstangsfæste for m. triceps brachii under armstrækkere og kastebevægelser.",
"related": [
"radius",
"humerus",
"triceps"
],
"layer": "skeleton",
"parent": "arm",
"tags": [
"bone",
"skeleton"
],
"view": "anterior"
},
"hand_bones": {
"id": "hand_bones",
"type": "bone",
"names": {
"da": "Håndens knogler",
"la": "Ossa manus (carpus, metacarpus, phalanges)",
"en": "Hand bones"
},
"location": "Håndrod, mellemhånd og fingre.",
"function": "Præcisionsgreb og kraftgreb ved boldspil, klatring og redskabsgymnastik.",
"didactic": "Sammenlign præcisionsgreb og kraftgreb i forskellige idrætsaktiviteter.",
"related": [
"radius",
"ulna"
],
"layer": "skeleton",
"parent": "arm",
"tags": [
"bone",
"skeleton"
],
"view": "anterior"
},
"femur": {
"id": "femur",
"type": "bone",
"names": {
"da": "Lårben",
"la": "Os femoris / Femur",
"en": "Femur / Thigh bone"
},
"location": "Lårbenet ligger mellem hofte og knæ og er kroppens længste knogle.",
"function": "Overfører belastning mellem bækken og underben og danner led med bækken, tibia og patella.",
"didactic": "Undersøg lårbenets bevægelse i forhold til bækken og underben under squat.",
"related": [
"pelvis",
"patella",
"tibia",
"quadriceps",
"hamstrings",
"gluteus_maximus"
],
"layer": "skeleton",
"parent": "leg",
"tags": [
"bone",
"skeleton"
],
"view": "anterior"
},
"patella": {
"id": "patella",
"type": "bone",
"names": {
"da": "Knæskal",
"la": "Patella",
"en": "Kneecap / Patella"
},
"location": "Sesamknogle indlejret i quadriceps-senen foran knæleddet.",
"function": "Øger quadricepssenens momentarm omkring knæleddet. Virkningen afhænger blandt andet af knævinklen.",
"didactic": "Følg forbindelsen mellem quadriceps, patella og tibia ved et spark.",
"related": [
"femur",
"tibia",
"quadriceps"
],
"layer": "skeleton",
"parent": "leg",
"tags": [
"bone",
"skeleton"
],
"view": "anterior"
},
"tibia": {
"id": "tibia",
"type": "bone",
"names": {
"da": "Skinneben",
"la": "Tibia",
"en": "Shinbone / Tibia"
},
"location": "Store indvendige knogle i underbenet.",
"function": "Primære vægtbærende knogle i underbenet. Danner sammen med femur knæleddet, og med talus ankelleddet.",
"didactic": "Følg belastningens vej fra knæ til ankel ved en landing.",
"related": [
"femur",
"fibula",
"patella",
"foot_bones",
"gastrocnemius",
"tibialis_anterior"
],
"layer": "skeleton",
"parent": "leg",
"tags": [
"bone",
"skeleton"
],
"view": "anterior"
},
"fibula": {
"id": "fibula",
"type": "bone",
"names": {
"da": "Lægben",
"la": "Fibula",
"en": "Calf bone / Fibula"
},
"location": "Tynd knogle på ydersiden af underbenet.",
"function": "Ikke vægtbærende, men danner den ydre ankelkno (malleolus lateralis) og fæste for muskler.",
"didactic": "Find forbindelsen mellem lægbenet og anklens yderside.",
"related": [
"tibia",
"foot_bones"
],
"layer": "skeleton",
"parent": "leg",
"tags": [
"bone",
"skeleton"
],
"view": "anterior"
},
"foot_bones": {
"id": "foot_bones",
"type": "bone",
"names": {
"da": "Fodens knogler",
"la": "Ossa pedis (tarsus, metatarsus, phalanges)",
"en": "Foot bones"
},
"location": "Fodrod, mellemfod og tæer, danner fodens buer (longitudinelle og transverselle).",
"function": "Fungerer som støddæmper ved landing og stiv vægtstang under afsæt i løb og hop.",
"didactic": "Sammenlign fodens funktion ved stand, landing og afsæt.",
"related": [
"tibia",
"fibula",
"gastrocnemius"
],
"layer": "skeleton",
"parent": "leg",
"tags": [
"bone",
"skeleton"
],
"view": "anterior"
}
},
"muscles": {
"deltoideus": {
"id": "deltoideus",
"type": "muscle",
"names": {
"da": "Deltamusklen / Skuldermusklen",
"la": "Musculus deltoideus",
"en": "Deltoid muscle"
},
"location": "Omslutter skulderleddet på forsiden, ydersiden og bagsiden.",
"attachments": "Origo: Clavicula, acromion og spina scapulae. Insertio: Tuberositas deltoidea på humerus.",
"function": "Abduktion af armen (især over 15-90 grader), samt fleksion (pars clavicularis) og ekstension (pars spinalis).",
"didactic": "Aktiv ved volleyball-smash, håndboldskud, crawl i svømning og klatring. Bør trænes alsidigt (forreste, midterste og bageste del).",
"related": [
"humerus",
"scapula",
"clavicle",
"pectoralis_major",
"trapezius"
],
"layer": "muscles",
"parent": "arm",
"tags": [
"muscle",
"muscles"
],
"view": "anterior"
},
"biceps": {
"id": "biceps",
"type": "muscle",
"names": {
"da": "Den to-hovedede armbøjer",
"la": "Musculus biceps brachii",
"en": "Biceps"
},
"location": "Forsiden af overarmen.",
"attachments": "Origo: Caput longum (tuberculum supraglenoidale) og caput breve (processus coracoideus). Insertio: Tuberositas radii.",
"function": "Fleksion af albueleddet og kraftig supination af underarmen (dreje håndfladen opad). Svag fleksion i skulderleddet.",
"didactic": "Trænes ved chin-ups, klatring og roning. Fungerer som agonist sammen med m. brachialis ved trækbevægelser.",
"related": [
"humerus",
"radius",
"triceps"
],
"layer": "muscles",
"parent": "arm",
"tags": [
"muscle",
"muscles"
],
"view": "anterior"
},
"triceps": {
"id": "triceps",
"type": "muscle",
"names": {
"da": "Den tre-hovedede armstrækker",
"la": "Musculus triceps brachii",
"en": "Triceps"
},
"location": "Bagsiden af overarmen.",
"attachments": "Origo: Caput longum (scapula), caput mediale & laterale (humerus). Insertio: Olecranon på ulna.",
"function": "Ekstension af albueleddet. Caput longum assisterer ved ekstension og adduktion i skulderleddet.",
"didactic": "Primær muskel ved armstrækkere (push-ups), kastebevægelsens slutfase, brystsvømnings armtag og spydkast.",
"related": [
"humerus",
"ulna",
"scapula",
"biceps"
],
"layer": "muscles",
"parent": "arm",
"tags": [
"muscle",
"muscles"
],
"view": "posterior"
},
"pectoralis_major": {
"id": "pectoralis_major",
"type": "muscle",
"names": {
"da": "Den store brystmuskel",
"la": "Musculus pectoralis major",
"en": "Greater pectoral muscle"
},
"location": "Brystkassens forside.",
"attachments": "Udspring: Mediale clavicula, sternum og øvre ribbensbruske. Hæfte: Crista tuberculi majoris på humerus.",
"function": "Horisontal adduktion, indadrotation og fleksion af overarmen.",
"didactic": "Sammenlign musklens funktion i armstrækkere og kast.",
"related": [
"sternum",
"clavicle",
"humerus",
"deltoideus"
],
"layer": "muscles",
"parent": "torso",
"tags": [
"muscle",
"muscles"
],
"view": "anterior"
},
"trapezius": {
"id": "trapezius",
"type": "muscle",
"names": {
"da": "Kappemusklen",
"la": "Musculus trapezius",
"en": "Trapezius muscle"
},
"location": "Nakken og den øvre del af ryggen, formet som en trapez.",
"attachments": "Udspring: Nakkeben, ligamentum nuchae og torntappe C7–Th12. Hæfte: Laterale clavicula, acromion og spina scapulae.",
"function": "Elevation (løfte skuldre), retraktion (samle skulderblade) og opadrotation af scapula.",
"didactic": "Observer hvordan skulderbladene flyttes ved roning og løft af armene.",
"related": [
"cranium",
"scapula",
"thoracic_spine",
"cervical_spine"
],
"layer": "muscles",
"parent": "torso",
"tags": [
"muscle",
"muscles"
],
"view": "posterior"
},
"latissimus_dorsi": {
"id": "latissimus_dorsi",
"type": "muscle",
"names": {
"da": "Den brede rygmuskel",
"la": "Musculus latissimus dorsi",
"en": "Latissimus dorsi"
},
"location": "Den nedre og laterale del af ryggen.",
"attachments": "Udspring: Torntappe i nedre brystregion, thoracolumbalfascien, crista iliaca og nedre ribben. Hæfte: Bunden af sulcus intertubercularis på humerus.",
"function": "Adduktion, ekstension og indadrotation af overarmen (kraftigt træk nedad/bagud).",
"didactic": "Kroppens 'svømme- og klatremuskel'. Hovedmotor i pull-ups, kajakroning og crawlsvømning.",
"related": [
"humerus",
"pelvis",
"thoracic_spine",
"lumbar_spine"
],
"layer": "muscles",
"parent": "torso",
"tags": [
"muscle",
"muscles"
],
"view": "posterior"
},
"rectus_abdominis": {
"id": "rectus_abdominis",
"type": "muscle",
"names": {
"da": "Den lige bugmuskel / Mavemusklen",
"la": "Musculus rectus abdominis",
"en": "Rectus abdominis"
},
"location": "Midt foran på maven fra ribben til skamben.",
"attachments": "Udspring: Crista pubica og symphysis pubica. Hæfte: Processus xiphoideus og ribbensbrusk 5–7.",
"function": "Fleksion af hvirvelsøjlen og øgning af det intraabdominale tryk til stabilisering af truncus (core).",
"didactic": "Essentiel for kraftoverførsel mellem over- og underkrop ved spring, spark og løb. Trænes bedst i samspil med dybe mavemuskler.",
"related": [
"sternum",
"ribs",
"pelvis",
"lumbar_spine"
],
"layer": "muscles",
"parent": "torso",
"tags": [
"muscle",
"muscles"
],
"view": "anterior"
},
"obliquus_externus": {
"id": "obliquus_externus",
"type": "muscle",
"names": {
"da": "Den ydre skrå bugmuskel",
"la": "Musculus obliquus externus abdominis",
"en": "External oblique"
},
"location": "Flankerne af maven.",
"attachments": "Origo: Udvendig flade af ribben 5-12. Insertio: Crista iliaca og linea alba.",
"function": "Rotation af truncus til modsat side og lateral fleksion til samme side.",
"didactic": "Rotationsstyrke er nøglen til slående sportsgrene (golf, tennis, badminton, hockey) og kastebevægelser.",
"related": [
"ribs",
"pelvis",
"rectus_abdominis"
],
"layer": "muscles",
"parent": "torso",
"tags": [
"muscle",
"muscles"
],
"view": "anterior"
},
"gluteus_maximus": {
"id": "gluteus_maximus",
"type": "muscle",
"names": {
"da": "Den store sædemuskel / Baldemuskel",
"la": "Musculus gluteus maximus",
"en": "Gluteus maximus"
},
"location": "Bagsiden af bækkenet / balden.",
"attachments": "Origo: Os ilium, sacrum og coccygis. Insertio: Tractus iliotibialis og tuberositas glutea på femur.",
"function": "Kraftig ekstension og udadrotation i hofteleddet. Afgørende for oprejst gang.",
"didactic": "Bidrager til hofteekstension ved squat, acceleration og hop.",
"related": [
"pelvis",
"femur",
"hamstrings",
"quadriceps"
],
"layer": "muscles",
"parent": "leg",
"tags": [
"muscle",
"muscles"
],
"view": "posterior"
},
"quadriceps": {
"id": "quadriceps",
"type": "muscle",
"names": {
"da": "Den fire-hovedede knæstrækker",
"la": "Musculus quadriceps femoris",
"en": "Quadriceps"
},
"location": "Forsiden af låret (rectus femoris, vastus lateralis, medialis, intermedius).",
"attachments": "Origo: Spina iliaca anterior inferior (rectus femoris) og femur. Insertio: Patella og via lig. patellae til tuberositas tibiae.",
"function": "Ekstension i knæleddet; rectus femoris flekterer desuden hofteleddet.",
"didactic": "Sammenlign koncentrisk arbejde på vej op og excentrisk arbejde på vej ned i squat.",
"related": [
"femur",
"patella",
"tibia",
"pelvis",
"hamstrings"
],
"layer": "muscles",
"parent": "leg",
"tags": [
"muscle",
"muscles"
],
"view": "anterior"
},
"hamstrings": {
"id": "hamstrings",
"type": "muscle",
"names": {
"da": "Baglårsmusklerne / Hasemusklerne",
"la": "Mm. ischiocrurales (Biceps femoris, Semitendinosus, Semimembranosus)",
"en": "Hamstrings"
},
"location": "Bagsiden af låret.",
"attachments": "Udspring: Primært tuber ischiadicum; biceps femoris korte hoved udspringer fra femur. Hæfte: Proksimale tibia og caput fibulae, afhængigt af muskel.",
"function": "Fleksion af knæleddet og ekstension af hofteleddet. Bremser underbenet under løb.",
"didactic": "Bøjer knæet og hjælper de fleste af gruppens muskler med at strække hoften; vigtige i løbets svingfase.",
"related": [
"pelvis",
"femur",
"tibia",
"fibula",
"gluteus_maximus",
"quadriceps"
],
"layer": "muscles",
"parent": "leg",
"tags": [
"muscle",
"muscles"
],
"view": "posterior"
},
"gastrocnemius": {
"id": "gastrocnemius",
"type": "muscle",
"names": {
"da": "Den to-hovedede lægmuskel",
"la": "Musculus gastrocnemius",
"en": "Calf muscle / Gastrocnemius"
},
"location": "Øverste og overfladiske del af læggen.",
"attachments": "Origo: Condyli femoris (femur). Insertio: Via achillessenen (tendo calcaneus) på hælbenet (calcaneus).",
"function": "Plantarfleksion i ankelleddet (strække vristen) og fleksion i knæleddet.",
"didactic": "Afgørende for afsæt i løb og hop ('den elastiske energilagring i achillessenen'). Trænes ved tåløft med strakt knæ.",
"related": [
"femur",
"tibia",
"foot_bones",
"soleus",
"tibialis_anterior"
],
"layer": "muscles",
"parent": "leg",
"tags": [
"muscle",
"muscles"
],
"view": "posterior"
},
"soleus": {
"id": "soleus",
"type": "muscle",
"names": {
"da": "Flyndermusklen",
"la": "Musculus soleus",
"en": "Soleus"
},
"location": "Dybt for gastrocnemius i læggen.",
"attachments": "Origo: Bagsiden af tibia og fibula. Insertio: Achillessenen på calcaneus.",
"function": "Plantarfleksion i anklen. Består primært af langsomme udholdende type I muskelfibre.",
"didactic": "Vigtig for udholdenhed ved langdistanceløb og vedligeholdelse af stående balance.",
"related": [
"tibia",
"fibula",
"foot_bones",
"gastrocnemius"
],
"layer": "muscles",
"parent": "leg",
"tags": [
"muscle",
"muscles"
],
"view": "posterior"
},
"tibialis_anterior": {
"id": "tibialis_anterior",
"type": "muscle",
"names": {
"da": "Forreste skinnebensmuskel",
"la": "Musculus tibialis anterior",
"en": "Anterior tibial muscle"
},
"location": "Forsiden af skinnebenet.",
"attachments": "Udspring: Laterale kondyl og øvre laterale tibia samt membrana interossea. Hæfte: Os cuneiforme mediale og basis af metatars I.",
"function": "Dorsalfleksion (løfte tæerne) og inversion/supination af foden.",
"didactic": "Løfter foden under svingfasen i løb, så tæerne ikke rammer jorden. Svaghed eller overanstrengelse ses ved uvant gang eller løb.",
"related": [
"tibia",
"foot_bones",
"gastrocnemius"
],
"layer": "muscles",
"parent": "leg",
"tags": [
"muscle",
"muscles"
],
"view": "anterior"
}
},
"organs": {
"heart": {
"id": "heart",
"type": "organ",
"names": {
"da": "Hjertet",
"la": "Cor",
"en": "Heart"
},
"location": "Midt i brysthulen mellem lungerne, let forskudt mod venstre.",
"function": "Muskelpumpe bestående af to forkamre og to hjertekamre. Pumper iltet blod til kroppen (det store kredsløb) og afiltet blod til lungerne (det lille kredsløb).",
"didactic": "Diskutér hvordan puls og slagvolumen tilsammen bestemmer minutvolumen.",
"related": [
"lungs",
"aorta",
"vena_cava",
"sternum",
"ribs"
],
"layer": "organs",
"parent": "torso",
"tags": [
"organ",
"organs"
],
"view": "anterior"
},
"lungs": {
"id": "lungs",
"type": "organ",
"names": {
"da": "Lungerne",
"la": "Pulmones",
"en": "Lungs"
},
"location": "Brysthulen, beskyttet af ribbenene.",
"function": "Gasudveksling: Optager ilt (O2) fra indåndingsluften til blodet og udskiller kuldioxid (CO2) fra blodet til udåndingsluften i alveolerne.",
"didactic": "Sammenlign vejrtrækning før og efter aktivitet, og skeln mellem ventilation og gasudveksling.",
"related": [
"heart",
"trachea",
"ribs"
],
"layer": "respiratory",
"parent": "torso",
"tags": [
"organ",
"organs"
],
"view": "anterior"
},
"brain": {
"id": "brain",
"type": "organ",
"names": {
"da": "Hjernen",
"la": "Encephalon",
"en": "Brain"
},
"location": "Beskyttet i kraniet.",
"function": "Styrer motorisk kontrol, balance, koordination, beslutningstagning og sansning under idræt.",
"didactic": "Motorisk indlæring og automatisering af bevægelsesmønstre sker gennem synaptisk plasticitet i cerebellum og motorisk cortex.",
"related": [
"cranium",
"spinal_cord"
],
"layer": "nervous",
"parent": "head",
"tags": [
"organ",
"organs"
],
"view": "anterior"
},
"stomach": {
"id": "stomach",
"type": "organ",
"names": {
"da": "Mavesækken",
"la": "Gaster / Ventriculus",
"en": "Stomach"
},
"location": "Øverst i bughulen til venstre, under mellemgulvet.",
"function": "Mekanisk æltning og kemisk nedbrydning af mad via mavesyre og pepsin.",
"didactic": "Drøft hvorfor fordøjelse og fysisk aktivitet kan påvirke oplevelsen af et måltid.",
"related": [
"liver",
"intestines"
],
"layer": "digestive",
"parent": "torso",
"tags": [
"organ",
"organs"
],
"view": "anterior"
},
"liver": {
"id": "liver",
"type": "organ",
"names": {
"da": "Leveren",
"la": "Hepar",
"en": "Liver"
},
"location": "Øverst til højre i bughulen.",
"function": "Kroppens centrale stofskifteorgan. Oplagrer glykogen (op til ca. 100g) og frigiver glukose til blodet under fysisk aktivitet for at opretholde blodsukkeret.",
"didactic": "Leverens glykogen bidrager til at opretholde blodglukose; udtømning afhænger af kost, varighed og intensitet.",
"related": [
"stomach",
"intestines",
"heart"
],
"layer": "digestive",
"parent": "torso",
"tags": [
"organ",
"organs"
],
"view": "anterior"
},
"intestines": {
"id": "intestines",
"type": "organ",
"names": {
"da": "Tyndtarm",
"en": "Small intestine",
"la": "Intestinum tenue"
},
"location": "Nedre del af bughulen.",
"function": "Nedbrydning og optagelse af næringsstoffer. Sukkerarter og aminosyrer går til blodet; størstedelen af kostens langkædede fedt transporteres først via lymfen.",
"didactic": "Undersøg hvordan optagelse af næringsstoffer understøtter kroppens energiomsætning.",
"related": [
"stomach",
"colon",
"liver"
],
"layer": "digestive",
"parent": "torso",
"tags": [
"organ",
"organs"
],
"view": "anterior"
},
"mouth": {
"id": "mouth",
"type": "organ",
"names": {
"da": "Mund",
"en": "Mouth",
"la": "Cavitas oris"
},
"parent": "head",
"layer": "digestive",
"location": "Mundhulen i hovedet.",
"function": "Findeler mad og blander den med spyt. Luft kan også passere her.",
"didactic": "Tal om forskellen mellem luftens og madens vej.",
"related": [
"pharynx"
],
"tags": [
"organ",
"digestive"
],
"view": "anterior"
},
"pharynx": {
"id": "pharynx",
"type": "organ",
"names": {
"da": "Svælg",
"en": "Pharynx",
"la": "Pharynx"
},
"parent": "head",
"layer": "digestive",
"location": "Bag næse- og mundhulen.",
"function": "Fælles passage for luft og mad; ved synkning beskyttes luftvejene.",
"didactic": "Brug svælget til at sammenligne de to transportsystemer.",
"related": [
"trachea",
"esophagus"
],
"tags": [
"organ",
"digestive"
],
"view": "anterior"
},
"trachea": {
"id": "trachea",
"type": "organ",
"names": {
"da": "Luftrør",
"en": "Trachea",
"la": "Trachea"
},
"parent": "torso",
"layer": "respiratory",
"location": "Fra strubehovedet til hovedbronkierne.",
"function": "Leder luft og holdes åbent af bruskringe.",
"didactic": "Vejrtrækningen øges under fysisk aktivitet.",
"related": [
"bronchi",
"lungs"
],
"tags": [
"organ",
"respiratory"
],
"view": "anterior"
},
"bronchi": {
"id": "bronchi",
"type": "organ",
"names": {
"da": "Bronkier",
"en": "Bronchi",
"la": "Bronchi principales"
},
"parent": "lungs",
"layer": "respiratory",
"location": "Luftrørets forgreninger ind i lungerne.",
"function": "Fordeler luften videre til mindre luftveje.",
"didactic": "Følg luftens vej helt frem til gasudvekslingen.",
"related": [
"trachea",
"alveoli"
],
"tags": [
"organ",
"respiratory"
],
"view": "anterior"
},
"alveoli": {
"id": "alveoli",
"type": "organ",
"names": {
"da": "Alveoler",
"en": "Alveoli",
"la": "Alveoli pulmonis"
},
"parent": "lungs",
"layer": "respiratory",
"location": "Små luftfyldte blærer ved enden af luftvejene.",
"function": "Ilt diffunderer til blodet og kuldioxid til alveoleluften.",
"didactic": "Skeln mellem ventilation, gasudveksling og cellernes iltforbrug.",
"related": [
"lungs",
"heart"
],
"tags": [
"organ",
"respiratory"
],
"view": "anterior"
},
"esophagus": {
"id": "esophagus",
"type": "organ",
"names": {
"da": "Spiserør",
"en": "Esophagus",
"la": "Oesophagus"
},
"parent": "torso",
"layer": "digestive",
"location": "Bag luftrøret fra svælg til mavesæk.",
"function": "Peristaltiske bevægelser fører maden mod mavesækken.",
"didactic": "Sammenlign ufrivilligt glat muskelarbejde med skeletmusklernes arbejde.",
"related": [
"pharynx",
"stomach"
],
"tags": [
"organ",
"digestive"
],
"view": "anterior"
},
"colon": {
"id": "colon",
"type": "organ",
"names": {
"da": "Tyktarm",
"en": "Large intestine",
"la": "Intestinum crassum"
},
"parent": "torso",
"layer": "digestive",
"location": "Omkring tyndtarmen i bughulen.",
"function": "Optager vand og elektrolytter; tarmindholdet bliver til afføring.",
"didactic": "Giver anledning til at tale om væskebalance.",
"related": [
"intestines",
"rectum"
],
"tags": [
"organ",
"digestive"
],
"view": "anterior"
},
"rectum": {
"id": "rectum",
"type": "organ",
"names": {
"da": "Endetarm",
"en": "Rectum",
"la": "Rectum"
},
"parent": "torso",
"layer": "digestive",
"location": "Sidste del af tyktarmen i bækkenet.",
"function": "Opbevarer afføring før udskillelse gennem anus.",
"didactic": "Afslutter madens vej; næringsstoffer er hovedsageligt optaget tidligere.",
"related": [
"colon"
],
"tags": [
"organ",
"digestive"
],
"view": "anterior"
},
"right_atrium": {
"id": "right_atrium",
"type": "organ",
"names": {
"da": "Højre forkammer",
"en": "Right atrium",
"la": "Atrium dextrum"
},
"parent": "heart",
"layer": "organs",
"location": "Et af hjertets fire hulrum.",
"function": "Modtager iltfattigt blod fra kroppens hulvener.",
"didactic": "De to hjertehalvdele arbejder i serie: lungekredsløb og kropskredsløb.",
"related": [
"heart",
"lungs",
"aorta"
],
"tags": [
"organ",
"organs"
],
"view": "anterior"
},
"right_ventricle": {
"id": "right_ventricle",
"type": "organ",
"names": {
"da": "Højre hjertekammer",
"en": "Right ventricle",
"la": "Ventriculus dexter"
},
"parent": "heart",
"layer": "organs",
"location": "Et af hjertets fire hulrum.",
"function": "Pumper blod gennem lungearterierne til lungerne.",
"didactic": "De to hjertehalvdele arbejder i serie: lungekredsløb og kropskredsløb.",
"related": [
"heart",
"lungs",
"aorta"
],
"tags": [
"organ",
"organs"
],
"view": "anterior"
},
"left_atrium": {
"id": "left_atrium",
"type": "organ",
"names": {
"da": "Venstre forkammer",
"en": "Left atrium",
"la": "Atrium sinistrum"
},
"parent": "heart",
"layer": "organs",
"location": "Et af hjertets fire hulrum.",
"function": "Modtager iltrigt blod fra lungevenerne.",
"didactic": "De to hjertehalvdele arbejder i serie: lungekredsløb og kropskredsløb.",
"related": [
"heart",
"lungs",
"aorta"
],
"tags": [
"organ",
"organs"
],
"view": "anterior"
},
"left_ventricle": {
"id": "left_ventricle",
"type": "organ",
"names": {
"da": "Venstre hjertekammer",
"en": "Left ventricle",
"la": "Ventriculus sinister"
},
"parent": "heart",
"layer": "organs",
"location": "Et af hjertets fire hulrum.",
"function": "Pumper blod ud i aorta og det store kredsløb.",
"didactic": "De to hjertehalvdele arbejder i serie: lungekredsløb og kropskredsløb.",
"related": [
"heart",
"lungs",
"aorta"
],
"tags": [
"organ",
"organs"
],
"view": "anterior"
}
},
"vessels": {
"aorta": {
"id": "aorta",
"type": "vessel",
"names": {
"da": "Hovedpulsåren",
"la": "Aorta",
"en": "Aorta"
},
"location": "Udgår fra venstre hjertekammer og løber ned gennem bryst- og bughule.",
"function": "Fører iltrigt blod under højt tryk fra hjertet ud til alle kroppens organer og muskler.",
"didactic": "Følg forbindelsen fra venstre hjertekammer til kroppens væv.",
"related": [
"heart"
],
"layer": "circulation",
"parent": "torso",
"tags": [
"vessel",
"circulation"
],
"view": "anterior"
},
"vena_cava": {
"id": "vena_cava",
"type": "vessel",
"names": {
"da": "De store hulvener",
"la": "Vena cava superior et inferior",
"en": "Vena cava"
},
"location": "Løber parallelt med rygsøjlen tilbage til højre forkammer.",
"function": "Transporterer afiltet blod fra kroppen retur til hjertet.",
"didactic": "Venepumpen: Når benmusklerne (fx læggen) kontraheres under løb, presses veneblodet mod tyngdekraften op til hjertet via ensrettende veneklapper.",
"related": [
"heart",
"gastrocnemius"
],
"layer": "circulation",
"parent": "torso",
"tags": [
"vessel",
"circulation"
],
"view": "anterior"
}
},
"nervous": {
"spinal_cord": {
"id": "spinal_cord",
"type": "nerve",
"names": {
"da": "Rygmarven",
"la": "Medulla spinalis",
"en": "Spinal cord"
},
"location": "Løber inde i rygsøjlens hvirvelkanal.",
"function": "Hovedvej for nerveimpulser mellem hjernen og kroppen, samt center for spinalreflekser.",
"didactic": "Strækreflekser (f.eks. patellarrefleksen) udløses på rygmarvsniveau og er afgørende for lynhurtig muskelaktivering og stabilitet ved spring.",
"related": [
"brain",
"cervical_spine",
"lumbar_spine"
],
"layer": "nervous",
"parent": "torso",
"tags": [
"nerve",
"nervous"
],
"view": "anterior"
},
"sciatic_nerve": {
"id": "sciatic_nerve",
"type": "nerve",
"names": {
"da": "Iskiasnerven",
"la": "Nervus ischiadicus",
"en": "Sciatic nerve"
},
"location": "Udspringer fra lænderyg/bækken og løber ned ad bagsiden af låret.",
"function": "Kroppens tykkeste nerve, innerverer baglårets muskler og hele underbenet og foden.",
"didactic": "Afklemning af nerven (iskias) kan give udstrålende smerter i benet under løb eller tung styrketræning.",
"related": [
"hamstrings",
"pelvis",
"lumbar_spine"
],
"layer": "nervous",
"parent": "torso",
"tags": [
"nerve",
"nervous"
],
"view": "anterior"
}
},
"joints": {
"shoulder": {
"id": "shoulder",
"type": "joint",
"names": {
"da": "Skulderled",
"en": "Shoulder joint",
"la": "Articulatio humeri"
},
"parent": "arm",
"layer": "joints",
"location": "Mellem humerus og scapula.",
"function": "Kugleled: blandt andet fleksion, ekstension, abduktion og rotation.",
"didactic": "Undersøg hvordan skulderbladet følger armen i et kast.",
"related": [
"humerus",
"scapula",
"deltoideus"
],
"tags": [
"joint",
"joints"
],
"view": "anterior"
},
"elbow": {
"id": "elbow",
"type": "joint",
"names": {
"da": "Albueled",
"en": "Elbow joint",
"la": "Articulatio cubiti"
},
"parent": "arm",
"layer": "joints",
"location": "Mellem humerus, ulna og radius.",
"function": "Bøjer og strækker armen. Underarmens rotation sker i radioulnarleddene.",
"didactic": "Sammenlign biceps og triceps i en træk- og presøvelse.",
"related": [
"biceps",
"triceps",
"humerus",
"radius",
"ulna"
],
"tags": [
"joint",
"joints"
],
"view": "anterior"
},
"hip": {
"id": "hip",
"type": "joint",
"names": {
"da": "Hofteled",
"en": "Hip joint",
"la": "Articulatio coxae"
},
"parent": "leg",
"layer": "joints",
"location": "Mellem lårbenshovedet og bækkenets hofteskål.",
"function": "Kugleled, der overfører belastning og bevæger låret.",
"didactic": "Hofteekstension bidrager til afsæt i løb og hop.",
"related": [
"pelvis",
"femur",
"gluteus_maximus"
],
"tags": [
"joint",
"joints"
],
"view": "anterior"
},
"knee": {
"id": "knee",
"type": "joint",
"names": {
"da": "Knæled",
"en": "Knee joint",
"la": "Articulatio genus"
},
"parent": "leg",
"layer": "joints",
"location": "Mellem femur, tibia og patella.",
"function": "Primært fleksion og ekstension; rotation er mulig i bøjet stilling.",
"didactic": "Sammenlign knæets bevægelse i squat og spark.",
"related": [
"femur",
"tibia",
"patella",
"quadriceps",
"hamstrings",
"acl",
"patellar_tendon"
],
"tags": [
"joint",
"joints"
],
"view": "anterior"
},
"ankle": {
"id": "ankle",
"type": "joint",
"names": {
"da": "Ankelled",
"en": "Ankle joint",
"la": "Articulatio talocruralis"
},
"parent": "leg",
"layer": "joints",
"location": "Mellem tibia, fibula og talus.",
"function": "Dorsalfleksion og plantarfleksion af foden.",
"didactic": "Undersøg anklen under landing og afsæt.",
"related": [
"tibia",
"fibula",
"foot_bones",
"gastrocnemius",
"achilles"
],
"tags": [
"joint",
"joints"
],
"view": "anterior"
},
"acl": {
"id": "acl",
"type": "ligament",
"names": {
"da": "Forreste korsbånd",
"en": "Anterior cruciate ligament",
"la": "Ligamentum cruciatum anterius"
},
"parent": "knee",
"layer": "joints",
"location": "Inde i knæleddet mellem tibia og femur.",
"function": "Begrænser tibias fremadglidning og bidrager til rotationsstabilitet.",
"didactic": "Forklar forskellen mellem aktive muskler og passive ledbånd.",
"related": [
"knee",
"femur",
"tibia"
],
"tags": [
"ligament",
"joints"
],
"view": "anterior"
},
"patellar_tendon": {
"id": "patellar_tendon",
"type": "ligament",
"names": {
"da": "Knæskalsbånd",
"en": "Patellar ligament",
"la": "Ligamentum patellae"
},
"parent": "knee",
"layer": "joints",
"location": "Fra patella til tuberositas tibiae.",
"function": "Overfører quadricepskraft fra patella til tibia; kaldes også patellarsenen.",
"didactic": "Følg kraftens vej fra muskel til knogle ved et spark.",
"related": [
"quadriceps",
"patella",
"tibia"
],
"tags": [
"ligament",
"joints"
],
"view": "anterior"
},
"achilles": {
"id": "achilles",
"type": "tendon",
"names": {
"da": "Akillessene",
"en": "Achilles tendon",
"la": "Tendo calcaneus"
},
"parent": "ankle",
"layer": "joints",
"location": "Fra gastrocnemius og soleus til hælbenet.",
"function": "Overfører lægmusklernes kraft og lagrer elastisk energi.",
"didactic": "Sammenlign langsomme hælløft med hurtige afsæt.",
"related": [
"soleus",
"gastrocnemius",
"foot_bones"
],
"tags": [
"tendon",
"joints"
],
"view": "posterior"
}
}
};
const MovementData = [
{
"id": "elbow_flexion",
"name": {
"da": "Fleksion i albueleddet",
"en": "Elbow flexion",
"la": "Flexio cubiti"
},
"agonist": "M. biceps brachii & M. brachialis",
"antagonist": "M. triceps brachii",
"joint": "Albueleddet (Articulatio cubiti)",
"highlightParts": [
"biceps",
"humerus",
"radius",
"ulna"
],
"antagonistParts": [
"triceps"
],
"didactic": "Biceps, brachialis og brachioradialis bidrager til albuefleksion. Modstående muskler kan samtidig stabilisere leddet."
},
{
"id": "elbow_extension",
"name": {
"da": "Ekstension i albueleddet",
"en": "Elbow extension",
"la": "Extensio cubiti"
},
"agonist": "M. triceps brachii",
"antagonist": "M. biceps brachii",
"joint": "Albueleddet (Articulatio cubiti)",
"highlightParts": [
"triceps",
"humerus",
"ulna"
],
"antagonistParts": [
"biceps"
],
"didactic": "Klassisk armstrækker (push-up) og kastets slutfase. Agonisten triceps strækker armen mod modstand."
},
{
"id": "knee_extension",
"name": {
"da": "Ekstension i knæleddet",
"en": "Knee extension",
"la": "Extensio genus"
},
"agonist": "M. quadriceps femoris",
"antagonist": "Hamstrings (haserne)",
"joint": "Knæleddet (Articulatio genus)",
"highlightParts": [
"quadriceps",
"femur",
"patella",
"tibia"
],
"antagonistParts": [
"hamstrings"
],
"didactic": "Hovedbevægelsen ved spark til bold, cykling og opstigningsfasen i et squat."
},
{
"id": "knee_flexion",
"name": {
"da": "Fleksion i knæleddet",
"en": "Knee flexion",
"la": "Flexio genus"
},
"agonist": "Hamstrings (Mm. ischiocrurales)",
"antagonist": "M. quadriceps femoris",
"joint": "Knæleddet (Articulatio genus)",
"highlightParts": [
"hamstrings",
"femur",
"tibia",
"fibula"
],
"antagonistParts": [
"quadriceps"
],
"didactic": "Vigtig i svingfasen i løb, hvor underbenet bøjes op mod balden for at mindske inertimomentet og øge skridtfrekvensen."
},
{
"id": "shoulder_abduction",
"name": {
"da": "Abduktion i skulderleddet",
"en": "Shoulder abduction",
"la": "Abductio humeri"
},
"agonist": "M. deltoideus & M. supraspinatus",
"antagonist": "M. latissimus dorsi & M. pectoralis major",
"joint": "Skulderleddet (Articulatio humeri)",
"highlightParts": [
"deltoideus",
"scapula",
"clavicle",
"humerus"
],
"antagonistParts": [
"latissimus_dorsi",
"pectoralis_major"
],
"didactic": "Armen løftes ud til siden. Essentielt ved kastesport, volleyballblokering og svømning."
},
{
"id": "squat_movement",
"name": {
"da": "Squat (Knæbøjning)",
"en": "Squat movement",
"la": "Flexio/Extensio coxae et genus"
},
"agonist": "M. gluteus maximus & M. quadriceps femoris",
"antagonist": "Baglår og bugmuskler bidrager til koordination og stabilisering.",
"joint": "Hofte-, knæ- og ankelled (flerledsøvelse)",
"highlightParts": [
"gluteus_maximus",
"quadriceps",
"femur",
"tibia",
"pelvis"
],
"antagonistParts": [],
"didactic": "På vej ned bremser knæ- og hoftestrækkere excentrisk. På vej op arbejder de overvejende koncentrisk. Bugmusklerne stabiliserer kroppen."
},
{
"id": "jump",
"name": {
"da": "Hop og landing",
"en": "Jump and landing",
"la": "Saltus"
},
"agonist": "Quadriceps, gluteus maximus og lægmuskler",
"antagonist": "Baglår og kropsstammens muskler koordinerer og stabiliserer.",
"joint": "Hofte, knæ og ankel",
"highlightParts": [
"quadriceps",
"gluteus_maximus",
"gastrocnemius",
"femur",
"tibia",
"foot_bones",
"hip",
"knee",
"ankle"
],
"antagonistParts": [],
"didactic": "Afsættet kræver ekstension i hofte og knæ samt plantarfleksion i anklen. Ved landing bremses bevægelsen excentrisk."
},
{
"id": "run",
"name": {
"da": "Løb",
"en": "Running",
"la": "Cursus"
},
"agonist": "Skiftende aktivitet i hofte-, knæ- og ankelmuskler",
"antagonist": "Muskelrollerne skifter mellem stand- og svingfase.",
"joint": "Hofte, knæ og ankel",
"highlightParts": [
"quadriceps",
"hamstrings",
"gastrocnemius",
"gluteus_maximus",
"femur",
"tibia",
"hip",
"knee",
"ankle"
],
"antagonistParts": [],
"didactic": "Standfasen understøtter og driver kroppen frem; svingfasen fører benet frem til næste kontakt. Figuren er en forenklet bevægelsesmodel."
}
];
const ProcessData = {
"oxygen": {
"id": "oxygen",
"title": "Iltens vej gennem kroppen",
"color": "#06b6d4",
"steps": [
{
"organ": "mouth",
"name": "Mund / næse",
"desc": "Luft kommer ind gennem næse eller mund."
},
{
"organ": "pharynx",
"name": "Svælg",
"desc": "Luften passerer svælget og strubehovedet."
},
{
"organ": "trachea",
"name": "Luftrør",
"desc": "Bruskringe holder luftpassagen åben."
},
{
"organ": "bronchi",
"name": "Bronkier",
"desc": "Luften fordeles i stadig mindre luftveje."
},
{
"organ": "lungs",
"name": "Lunger",
"desc": "Ventilation fører frisk luft til lungerne."
},
{
"organ": "alveoli",
"name": "Alveoler → blod",
"desc": "Ilt diffunderer over i lungekapillærerne og bindes især til hæmoglobin."
},
{
"organ": "left_atrium",
"name": "Venstre forkammer",
"desc": "Lungevenerne fører iltrigt blod tilbage til hjertet."
},
{
"organ": "left_ventricle",
"name": "Venstre hjertekammer",
"desc": "Blodet pumpes ud i aorta."
},
{
"organ": "aorta",
"name": "Arterier → kapillærer",
"desc": "Blodet fordeler ilten til kroppens væv."
},
{
"organ": "quadriceps",
"name": "Muskelceller",
"desc": "Ilt diffunderer til muskelceller og bruges ved aerob ATP-dannelse."
}
]
},
"blood": {
"id": "blood",
"title": "Blodets vej (Store og lille kredsløb)",
"color": "#ef4444",
"steps": [
{
"organ": "vena_cava",
"name": "Krop → hulvener",
"desc": "Iltfattigt blod vender tilbage fra kroppens væv."
},
{
"organ": "right_atrium",
"name": "Højre forkammer",
"desc": "Blodet passerer trikuspidalklappen til højre hjertekammer."
},
{
"organ": "right_ventricle",
"name": "Højre hjertekammer",
"desc": "Blod pumpes gennem pulmonalklappen til lungearterierne."
},
{
"organ": "lungs",
"name": "Lunger",
"desc": "Blod afgiver CO₂ og optager O₂ i lungekapillærerne."
},
{
"organ": "left_atrium",
"name": "Venstre forkammer",
"desc": "Lungevenerne fører blod tilbage; det passerer mitralklappen."
},
{
"organ": "left_ventricle",
"name": "Venstre hjertekammer",
"desc": "Blod pumpes gennem aortaklappen til aorta."
},
{
"organ": "aorta",
"name": "Arterier",
"desc": "Blod føres væk fra hjertet til vævenes kapillærer."
},
{
"organ": "quadriceps",
"name": "Muskler / organer",
"desc": "Ilt afgives; kuldioxid optages. Blodet fortsætter gennem vener til hulvenerne."
}
]
},
"digestion": {
"id": "digestion",
"title": "Madens vej & Energiomsætning",
"color": "#f59e0b",
"steps": [
{
"organ": "mouth",
"name": "Mund",
"desc": "Tygning findeler maden. Spytamylase begynder stivelsesnedbrydning."
},
{
"organ": "pharynx",
"name": "Svælg",
"desc": "Synkning fører maden mod spiserøret."
},
{
"organ": "esophagus",
"name": "Spiserør",
"desc": "Peristaltik transporterer maden til mavesækken."
},
{
"organ": "stomach",
"name": "Mavesæk",
"desc": "Maden blandes med syre og enzymer; pepsin bidrager til proteinnedbrydning."
},
{
"organ": "intestines",
"name": "Tyndtarm",
"desc": "Nedbrydning og optagelse af næringsstoffer. De fleste fedtstoffer transporteres først via lymfen."
},
{
"organ": "colon",
"name": "Tyktarm",
"desc": "Vand og elektrolytter optages fra det resterende indhold."
},
{
"organ": "rectum",
"name": "Endetarm",
"desc": "Afføring opbevares og udskilles gennem anus. Mad passerer ikke gennem lever eller muskler."
}
]
}
};
const PhysiologyData = {
"rest": {
"name": "Hvile",
"hr": "65 bpm",
"rf": "12 /min",
"q": "5.0 L/min",
"muscleFlow": "~18%",
"pulseRateMs": 923,
"description": "I hvile modtager nyrer, mave-tarm og hjerne størstedelen af minutvolumen. Kun ca. 15-20% af blodet går til skeletmuskulaturen."
},
"moderate": {
"name": "Moderat aerob aktivitet (Jogging/Opvarmning)",
"hr": "135 bpm",
"rf": "26 /min",
"q": "15.5 L/min",
"muscleFlow": "~65%",
"pulseRateMs": 444,
"description": "Puls, ventilation og blodtilførsel til aktive muskler øges. Aerob energiomsætning bidrager væsentligt."
},
"heavy": {
"name": "Hård fysisk aktivitet (Interval/Maksimal)",
"hr": "185 bpm",
"rf": "50 /min",
"q": "24.0 L/min",
"muscleFlow": "~85%",
"pulseRateMs": 324,
"description": "Iltbehov og ventilation er høje. Både aerob og anaerob energiomsætning bidrager; forholdet afhænger af intensitet og varighed."
}
};
const QuizData = {
"findQuestions": [
{
"targetId": "femur",
"text": "Find lårbenet (Femur) på figuren.",
"hint": "Kroppens længste rørknogle i overbenet."
},
{
"targetId": "biceps",
"text": "Find M. biceps brachii på armen.",
"hint": "Musklen på forsiden af overarmen der bøjer albuen."
},
{
"targetId": "patella",
"text": "Find knæskallen (Patella).",
"hint": "Den lille sesamknogle foran knæleddet."
},
{
"targetId": "rectus_abdominis",
"text": "Find den lige bugmuskel (M. rectus abdominis).",
"hint": "Core-musklen midt foran på maven."
},
{
"targetId": "heart",
"text": "Find hjertet (Cor).",
"hint": "Ligger mellem lungerne, lidt mod kroppens venstre side."
},
{
"targetId": "gastrocnemius",
"text": "Find den to-hovedede lægmuskel (M. gastrocnemius).",
"hint": "Musklen øverst på bagsiden af underbenet."
}
],
"nameQuestions": [
{
"partId": "femur",
"question": "Hvad hedder den fremhævede knogle på latin?",
"options": [
"Os femoris",
"Tibia",
"Humerus",
"Fibula"
],
"correctIndex": 0,
"feedback": "Lårbenet hedder femur eller os femoris på latin."
},
{
"partId": "deltoideus",
"question": "Hvilken muskelgruppe er fremhævet på skulderen?",
"options": [
"M. pectoralis major",
"M. deltoideus",
"M. trapezius",
"M. latissimus dorsi"
],
"correctIndex": 1,
"feedback": "Rigtigt! M. deltoideus omslutter skulderleddet og er hovedmotor ved abduktion af armen."
},
{
"partId": "patella",
"question": "Hvad er knæskallens primære biomekaniske funktion?",
"options": [
"At forøge vægtstangsarmen for quadriceps",
"At producere ledvæske til hoften",
"At låse benet ved gang",
"At beskytte akillessenen"
],
"correctIndex": 0,
"feedback": "Patella øger quadricepssenens momentarm omkring knæet. Effekten afhænger blandt andet af ledvinklen."
}
],
"matchPairs": [
{
"da": "Lårben",
"la": "Femur"
},
{
"da": "Overarmsknogle",
"la": "Humerus"
},
{
"da": "Knæskal",
"la": "Patella"
},
{
"da": "Brystben",
"la": "Sternum"
},
{
"da": "Brystmuskel",
"la": "M. pectoralis major"
},
{
"da": "Kappemuskel",
"la": "M. trapezius"
}
],
"movementQuestions": [
{
"question": "Hvilken af disse muskler bidrager til fleksion i albuen?",
"options": [
"M. triceps brachii",
"M. biceps brachii",
"M. deltoideus",
"M. gastrocnemius"
],
"correctIndex": 1,
"feedback": "Biceps bøjer albuen sammen med blandt andre brachialis og brachioradialis."
},
{
"question": "Hvilket muskelarbejde udfører M. quadriceps, når du langsomt sænker dig ned i en squat?",
"options": [
"Koncentrisk muskelarbejde",
"Excentrisk muskelarbejde",
"Isometrisk muskelarbejde",
"Passivt stræk"
],
"correctIndex": 1,
"feedback": "Rigtigt! Quadriceps forlænges under spænding for at bremse kroppen mod tyngdekraften = excentrisk arbejde."
}
]
};
// =============================================================================
// MODUL 6: APPLIKATIONENS HOVEDMOTOR (AppCore)
// =============================================================================
const AppCore = {
state: {
currentView: "anterior", // "anterior", "posterior", "split"
activeLayers: {
silhouette: true,
skeleton: true,
muscles: true,
organs: true,
circulation: true,
nervous: true
},
layerOpacity: {
silhouette: 0.4,
skeleton: 1,
muscles: 0.9,
organs: 0.95,
circulation: 0.85,
nervous: 0.8
},
selectedPartId: null,
langMode: "da-la", // "da", "la", "en", "da-la", "da-en", "all"
exertionState: "rest", // "rest", "moderate", "heavy"
teacherMode: false,
zoomScale: 1,
panX: 0,
panY: 0,
isDragging: false,
dragStartX: 0,
dragStartY: 0,
favorites: [],
splitDistance: 130
},
bindEvents() {
// Visningsskift (Forfra, Bagfra, Split)
document.querySelectorAll(".pill-btn[data-view]").forEach(btn => {
btn.addEventListener("click", (e) => {
document.querySelectorAll(".pill-btn[data-view]").forEach(b => b.classList.remove("active"));
btn.classList.add("active");
this.setView(btn.dataset.view);
});
});
// Sprogvalg
const langSelect = document.getElementById("lang-select");
langSelect.addEventListener("change", (e) => {
this.state.langMode = e.target.value;
this.updateLanguageUI();
});
// Fysiologisk tilstand (Hvile, Moderat, Hård)
document.querySelectorAll(".exertion-btn[data-state]").forEach(btn => {
btn.addEventListener("click", () => {
document.querySelectorAll(".exertion-btn[data-state]").forEach(b => b.classList.remove("active"));
btn.classList.add("active");
this.setExertionState(btn.dataset.state);
});
});
// Lag checkbokse og opacity sliders
document.querySelectorAll("input[data-layer]").forEach(input => {
input.addEventListener("change", (e) => {
const layer = e.target.dataset.layer;
this.state.activeLayers[layer] = e.target.checked;
this.updateLayerVisibility();
});
});
document.querySelectorAll("input[data-layer-opacity]").forEach(slider => {
slider.addEventListener("input", (e) => {
const layer = e.target.dataset.layerOpacity;
this.state.layerOpacity[layer] = parseFloat(e.target.value);
this.updateLayerVisibility();
});
});
// Navigation i venstre sidebar
document.querySelectorAll(".nav-item-btn[data-nav]").forEach(btn => {
btn.addEventListener("click", () => {
document.querySelectorAll(".nav-item-btn[data-nav]").forEach(b => b.classList.remove("active"));
btn.classList.add("active");
this.isolateSystem(btn.dataset.nav);
});
});
// Overlays (Bevægelse, Forløb, Quiz)
document.querySelectorAll("[data-overlay]").forEach(btn => {
btn.addEventListener("click", () => {
const overlayName = btn.dataset.overlay;
this.toggleOverlay(overlayName);
});
});
document.querySelectorAll(".overlay-close").forEach(btn => {
btn.addEventListener("click", () => {
const overlayName = btn.dataset.close;
this.closeOverlay(overlayName);
});
});
// Zoom & Pan kontroller
document.getElementById("btn-zoom-in").addEventListener("click", () => this.zoom(1.25));
document.getElementById("btn-zoom-out").addEventListener("click", () => this.zoom(0.8));
document.getElementById("btn-zoom-reset").addEventListener("click", () => this.resetZoom());
document.getElementById("anatomy-svg").addEventListener("wheel", e => {
if (window.innerWidth < 641) return;
e.preventDefault(); this.zoom(e.deltaY < 0 ? 1.1 : 1/1.1);
}, {passive:false});
// Split-slider
const splitSlider = document.getElementById("split-slider");
splitSlider.addEventListener("input", (e) => {
this.state.splitDistance = parseInt(e.target.value);
this.renderSvgLayers();
});
// Undervisertilstand
document.getElementById("btn-teacher-mode").addEventListener("click", () => {
this.toggleTeacherMode();
});
// Søgefunktion
const searchInput = document.getElementById("search-input");
searchInput.addEventListener("input", (e) => this.handleSearch(e.target.value));
searchInput.addEventListener("focus", (e) => this.handleSearch(e.target.value));
document.addEventListener("click", (e) => {
if (!e.target.closest(".search-box")) {
document.getElementById("search-results").style.display = "none";
}
});
// Favoritter knap i topbar
document.getElementById("btn-show-favorites").addEventListener("click", () => {
this.showFavoritesModal();
});
// Mobil responsivitet knapper
const btnToggleSidebar = document.getElementById("btn-toggle-sidebar");
const leftSidebar = document.getElementById("left-sidebar");
if (btnToggleSidebar) {
btnToggleSidebar.addEventListener("click", () => {
leftSidebar.classList.toggle("open");
});
}
const btnTogglePanel = document.getElementById("btn-toggle-panel");
const rightPanel = document.getElementById("right-panel");
if (btnTogglePanel) {
btnTogglePanel.addEventListener("click", () => {
rightPanel.classList.toggle("open");
});
}
// Favorit-knap i højre infopanel
document.getElementById("btn-toggle-fav").addEventListener("click", () => {
if (this.state.selectedPartId) {
this.toggleFavorite(this.state.selectedPartId);
}
});
},
renderSvgLayers() {
const root = document.getElementById("svg-transform-root");
const isBack = this.state.currentView === "posterior";
const isSplit = this.state.currentView === "split";
const dist = this.state.splitDistance;
const getXOffset = (layerIndex) => {
if (!isSplit) return 0;
return (layerIndex - 2.5) * dist;
};
let html = `
<!-- LAG 0: YDRE SILHOUETTE / HUD -->
<g id="layer-silhouette" class="layer-group" transform="translate(${getXOffset(0)}, 0)" opacity="${this.state.layerOpacity.silhouette}">
<circle cx="300" cy="85" r="42" fill="#1e293b" stroke="#475569" stroke-width="2" />
<path d="M 282 125 L 260 140 L 210 170 L 195 270 L 180 370 L 175 440 L 190 440 L 205 370 L 220 270 L 250 200 L 240 370 L 235 450 L 250 450 L 260 630 L 250 820 L 245 920 L 290 920 L 295 820 L 290 630 L 295 470 L 305 470 L 310 630 L 305 820 L 310 920 L 355 920 L 350 820 L 340 630 L 350 450 L 365 450 L 360 370 L 350 200 L 380 270 L 395 370 L 410 440 L 425 440 L 420 370 L 405 270 L 390 170 L 340 140 L 318 125 Z"
fill="#1e293b" stroke="#475569" stroke-width="2" />
</g>
<!-- LAG 1: SKELET -->
<g id="layer-skeleton" class="layer-group" transform="translate(${getXOffset(2)}, 0)" opacity="${this.state.layerOpacity.skeleton}">
<g id="part-cranium" class="anatomical-part" data-id="cranium" data-type="bone">
<ellipse cx="300" cy="80" rx="34" ry="40" fill="url(#grad-bone)" stroke="var(--color-bone-stroke)" stroke-width="1.5" />
${!isBack ? `
<ellipse cx="288" cy="82" rx="7" ry="8" fill="#0f172a" />
<ellipse cx="312" cy="82" rx="7" ry="8" fill="#0f172a" />
<path d="M 300 90 L 296 99 L 304 99 Z" fill="#0f172a" />
<path d="M 288 108 Q 300 112 312 108" stroke="#0f172a" stroke-width="2" fill="none" />
` : `
<path d="M 285 85 Q 300 100 315 85" stroke="#94a3b8" stroke-width="2" fill="none" />
`}
<text x="300" y="70" class="anatomy-label-pin" text-anchor="middle">Kranium</text>
</g>
<g id="part-cervical_spine" class="anatomical-part" data-id="cervical_spine" data-type="bone">
<rect x="294" y="118" width="12" height="22" rx="3" fill="url(#grad-bone)" stroke="var(--color-bone-stroke)" />
<line x1="294" y1="125" x2="306" y2="125" stroke="#94a3b8" />
<line x1="294" y1="132" x2="306" y2="132" stroke="#94a3b8" />
</g>
<g id="part-clavicle" class="anatomical-part" data-id="clavicle" data-type="bone">
<path d="M 300 144 Q 260 138 230 152" stroke="url(#grad-bone)" stroke-width="6" stroke-linecap="round" fill="none" />
<path d="M 300 144 Q 340 138 370 152" stroke="url(#grad-bone)" stroke-width="6" stroke-linecap="round" fill="none" />
<text x="255" y="136" class="anatomy-label-pin">Kraveben</text>
</g>
${!isBack ? `
<g id="part-sternum" class="anatomical-part" data-id="sternum" data-type="bone">
<path d="M 295 146 L 305 146 L 304 185 L 300 196 L 296 185 Z" fill="url(#grad-bone)" stroke="var(--color-bone-stroke)" stroke-width="1.5" />
<text x="300" y="172" class="anatomy-label-pin" text-anchor="middle">Brystben</text>
</g>` : ''}
<g id="part-scapula" class="anatomical-part" data-id="scapula" data-type="bone" opacity="${isBack ? '1' : '0.45'}">
<path d="M 240 155 L 268 162 L 255 210 Z" fill="url(#grad-bone)" stroke="var(--color-bone-stroke)" stroke-width="1.5" />
<path d="M 360 155 L 332 162 L 345 210 Z" fill="url(#grad-bone)" stroke="var(--color-bone-stroke)" stroke-width="1.5" />
<text x="250" y="190" class="anatomy-label-pin">Skulderblad</text>
</g>
<g id="part-ribs" class="anatomical-part" data-id="ribs" data-type="bone">
<path d="M 296 156 C 265 156 248 180 252 205 C 255 225 275 245 296 250" stroke="url(#grad-bone)" stroke-width="3.5" fill="none" />
<path d="M 304 156 C 335 156 352 180 348 205 C 345 225 325 245 304 250" stroke="url(#grad-bone)" stroke-width="3.5" fill="none" />
<path d="M 296 170 C 260 170 242 195 248 220 C 252 240 275 258 296 265" stroke="url(#grad-bone)" stroke-width="3.5" fill="none" />
<path d="M 304 170 C 340 170 358 195 352 220 C 348 240 325 258 304 265" stroke="url(#grad-bone)" stroke-width="3.5" fill="none" />
<text x="300" y="220" class="anatomy-label-pin" text-anchor="middle">Ribben</text>
</g>
<g id="part-thoracic_spine" class="anatomical-part" data-id="thoracic_spine" data-type="bone">
<rect x="295" y="142" width="10" height="110" rx="3" fill="url(#grad-bone)" stroke="var(--color-bone-stroke)" opacity="${isBack ? '1' : '0.5'}" />
</g>
<g id="part-lumbar_spine" class="anatomical-part" data-id="lumbar_spine" data-type="bone">
<rect x="293" y="255" width="14" height="60" rx="3" fill="url(#grad-bone)" stroke="var(--color-bone-stroke)" />
<text x="300" y="285" class="anatomy-label-pin" text-anchor="middle">Lændehvirvler</text>
</g>
<g id="part-pelvis" class="anatomical-part" data-id="pelvis" data-type="bone">
<path d="M 252 315 C 240 330 240 365 260 385 C 275 395 292 390 300 375 C 308 390 325 395 340 385 C 360 365 360 330 348 315 C 330 310 315 322 300 325 C 285 322 270 310 252 315 Z"
fill="url(#grad-bone)" stroke="var(--color-bone-stroke)" stroke-width="2" />
<ellipse cx="270" cy="365" rx="9" ry="12" fill="#0f172a" />
<ellipse cx="330" cy="365" rx="9" ry="12" fill="#0f172a" />
<text x="300" y="350" class="anatomy-label-pin" text-anchor="middle">Bækken</text>
</g>
<g id="part-humerus" class="anatomical-part" data-id="humerus" data-type="bone">
<path d="M 226 156 L 210 268" stroke="url(#grad-bone)" stroke-width="10" stroke-linecap="round" />
<path d="M 374 156 L 390 268" stroke="url(#grad-bone)" stroke-width="10" stroke-linecap="round" />
<text x="210" y="220" class="anatomy-label-pin">Overarmsknogle</text>
</g>
<g id="part-radius" class="anatomical-part" data-id="radius" data-type="bone">
<path d="M 206 274 L 188 375" stroke="url(#grad-bone)" stroke-width="6" stroke-linecap="round" />
<path d="M 394 274 L 412 375" stroke="url(#grad-bone)" stroke-width="6" stroke-linecap="round" />
<text x="180" y="330" class="anatomy-label-pin">Spoleben</text>
</g>
<g id="part-ulna" class="anatomical-part" data-id="ulna" data-type="bone">
<path d="M 213 274 L 198 375" stroke="url(#grad-bone)" stroke-width="5" stroke-linecap="round" />
<path d="M 387 274 L 402 375" stroke="url(#grad-bone)" stroke-width="5" stroke-linecap="round" />
</g>
<g id="part-hand_bones" class="anatomical-part" data-id="hand_bones" data-type="bone">
<path d="M 188 378 L 180 435 M 184 410 L 174 418" stroke="url(#grad-bone)" stroke-width="6" stroke-linecap="round" />
<path d="M 412 378 L 420 435 M 416 410 L 426 418" stroke="url(#grad-bone)" stroke-width="6" stroke-linecap="round" />
</g>
<g id="part-femur" class="anatomical-part" data-id="femur" data-type="bone">
<path d="M 268 385 Q 262 500 266 615" stroke="url(#grad-bone)" stroke-width="15" stroke-linecap="round" />
<path d="M 332 385 Q 338 500 334 615" stroke="url(#grad-bone)" stroke-width="15" stroke-linecap="round" />
<text x="252" y="510" class="anatomy-label-pin">Lårben</text>
</g>
${!isBack ? `
<g id="part-patella" class="anatomical-part" data-id="patella" data-type="bone">
<circle cx="266" cy="624" r="8" fill="url(#grad-bone)" stroke="var(--color-bone-stroke)" stroke-width="2" />
<circle cx="334" cy="624" r="8" fill="url(#grad-bone)" stroke="var(--color-bone-stroke)" stroke-width="2" />
<text x="266" y="640" class="anatomy-label-pin" text-anchor="middle">Knæskal</text>
</g>` : ''}
<g id="part-tibia" class="anatomical-part" data-id="tibia" data-type="bone">
<path d="M 267 635 L 268 815" stroke="url(#grad-bone)" stroke-width="12" stroke-linecap="round" />
<path d="M 333 635 L 332 815" stroke="url(#grad-bone)" stroke-width="12" stroke-linecap="round" />
<text x="255" y="740" class="anatomy-label-pin">Skinneben</text>
</g>
<g id="part-fibula" class="anatomical-part" data-id="fibula" data-type="bone">
<path d="M 256 645 L 257 810" stroke="url(#grad-bone)" stroke-width="5" stroke-linecap="round" />
<path d="M 344 645 L 343 810" stroke="url(#grad-bone)" stroke-width="5" stroke-linecap="round" />
<text x="245" y="760" class="anatomy-label-pin">Lægben</text>
</g>
<g id="part-foot_bones" class="anatomical-part" data-id="foot_bones" data-type="bone">
<path d="M 268 820 L 264 860 L 255 865" stroke="url(#grad-bone)" stroke-width="8" stroke-linecap="round" />
<path d="M 332 820 L 336 860 L 345 865" stroke="url(#grad-bone)" stroke-width="8" stroke-linecap="round" />
<text x="300" y="870" class="anatomy-label-pin" text-anchor="middle">Fodknogler</text>
</g>
</g>
<!-- LAG 2: MUSKLER -->
<g id="layer-muscles" class="layer-group" transform="translate(${getXOffset(1)}, 0)" opacity="${this.state.layerOpacity.muscles}">
<g id="part-deltoideus" class="anatomical-part" data-id="deltoideus" data-type="muscle">
<path d="M 235 146 C 215 155 205 180 216 210 C 225 190 230 170 238 155 Z" fill="url(#grad-muscle)" stroke="var(--color-muscle-stroke)" />
<path d="M 365 146 C 385 155 395 180 384 210 C 375 190 370 170 362 155 Z" fill="url(#grad-muscle)" stroke="var(--color-muscle-stroke)" />
<text x="205" y="175" class="anatomy-label-pin">Deltoideus</text>
</g>
${!isBack ? `
<g id="part-biceps" class="anatomical-part" data-id="biceps" data-type="muscle">
<ellipse cx="218" cy="216" rx="9" ry="34" fill="url(#grad-muscle)" stroke="var(--color-muscle-stroke)" transform="rotate(7 218 216)" />
<ellipse cx="382" cy="216" rx="9" ry="34" fill="url(#grad-muscle)" stroke="var(--color-muscle-stroke)" transform="rotate(-7 382 216)" />
<text x="218" y="220" class="anatomy-label-pin" text-anchor="middle">Biceps</text>
</g>` : `
<g id="part-triceps" class="anatomical-part" data-id="triceps" data-type="muscle">
<ellipse cx="216" cy="216" rx="11" ry="38" fill="url(#grad-muscle)" stroke="var(--color-muscle-stroke)" transform="rotate(7 216 216)" />
<ellipse cx="384" cy="216" rx="11" ry="38" fill="url(#grad-muscle)" stroke="var(--color-muscle-stroke)" transform="rotate(-7 384 216)" />
<text x="216" y="220" class="anatomy-label-pin" text-anchor="middle">Triceps</text>
</g>`}
${!isBack ? `
<g id="part-pectoralis_major" class="anatomical-part" data-id="pectoralis_major" data-type="muscle">
<path d="M 296 150 C 265 150 236 160 228 178 C 240 200 270 208 296 200 Z" fill="url(#grad-muscle)" stroke="var(--color-muscle-stroke)" />
<path d="M 304 150 C 335 150 364 160 372 178 C 360 200 330 208 304 200 Z" fill="url(#grad-muscle)" stroke="var(--color-muscle-stroke)" />
<text x="300" y="180" class="anatomy-label-pin" text-anchor="middle">Pectoralis</text>
</g>` : ''}
<g id="part-trapezius" class="anatomical-part" data-id="trapezius" data-type="muscle" opacity="${isBack ? '1' : '0.4'}">
<path d="M 300 115 L 260 144 L 285 170 L 300 240 L 315 170 L 340 144 Z" fill="url(#grad-muscle)" stroke="var(--color-muscle-stroke)" />
<text x="300" y="138" class="anatomy-label-pin" text-anchor="middle">Trapezius</text>
</g>
<g id="part-latissimus_dorsi" class="anatomical-part" data-id="latissimus_dorsi" data-type="muscle" opacity="${isBack ? '1' : '0.5'}">
<path d="M 285 210 C 255 215 240 240 245 285 C 265 295 285 290 295 270 Z" fill="url(#grad-muscle)" stroke="var(--color-muscle-stroke)" />
<path d="M 315 210 C 345 215 360 240 355 285 C 335 295 315 290 305 270 Z" fill="url(#grad-muscle)" stroke="var(--color-muscle-stroke)" />
<text x="300" y="260" class="anatomy-label-pin" text-anchor="middle">Latissimus</text>
</g>
${!isBack ? `
<g id="part-rectus_abdominis" class="anatomical-part" data-id="rectus_abdominis" data-type="muscle">
<path d="M 288 206 L 298 206 L 298 335 L 288 335 Z" fill="url(#grad-muscle)" stroke="var(--color-muscle-stroke)" />
<path d="M 302 206 L 312 206 L 312 335 L 302 335 Z" fill="url(#grad-muscle)" stroke="var(--color-muscle-stroke)" />
<line x1="288" y1="240" x2="312" y2="240" stroke="#9f1239" stroke-width="2" />
<line x1="288" y1="275" x2="312" y2="275" stroke="#9f1239" stroke-width="2" />
<line x1="288" y1="310" x2="312" y2="310" stroke="#9f1239" stroke-width="2" />
<text x="300" y="260" class="anatomy-label-pin" text-anchor="middle">Rectus abdominis</text>
</g>
<g id="part-obliquus_externus" class="anatomical-part" data-id="obliquus_externus" data-type="muscle">
<path d="M 265 235 C 250 250 250 290 268 320 C 275 280 275 250 265 235 Z" fill="url(#grad-muscle)" stroke="var(--color-muscle-stroke)" />
<path d="M 335 235 C 350 250 350 290 332 320 C 325 280 325 250 335 235 Z" fill="url(#grad-muscle)" stroke="var(--color-muscle-stroke)" />
</g>` : ''}
${isBack ? `
<g id="part-gluteus_maximus" class="anatomical-part" data-id="gluteus_maximus" data-type="muscle">
<ellipse cx="270" cy="370" rx="28" ry="32" fill="url(#grad-muscle)" stroke="var(--color-muscle-stroke)" />
<ellipse cx="330" cy="370" rx="28" ry="32" fill="url(#grad-muscle)" stroke="var(--color-muscle-stroke)" />
<text x="300" y="375" class="anatomy-label-pin" text-anchor="middle">Gluteus maximus</text>
</g>` : ''}
${!isBack ? `
<g id="part-quadriceps" class="anatomical-part" data-id="quadriceps" data-type="muscle">
<path d="M 252 390 C 242 450 245 540 264 610 C 285 540 286 450 276 390 Z" fill="url(#grad-muscle)" stroke="var(--color-muscle-stroke)" />
<path d="M 348 390 C 358 450 355 540 336 610 C 315 540 314 450 324 390 Z" fill="url(#grad-muscle)" stroke="var(--color-muscle-stroke)" />
<text x="264" y="490" class="anatomy-label-pin" text-anchor="middle">Quadriceps</text>
</g>` : `
<g id="part-hamstrings" class="anatomical-part" data-id="hamstrings" data-type="muscle">
<path d="M 254 400 C 245 460 248 550 262 610 C 282 550 282 460 274 400 Z" fill="url(#grad-muscle)" stroke="var(--color-muscle-stroke)" />
<path d="M 346 400 C 355 460 352 550 338 610 C 318 550 318 460 326 400 Z" fill="url(#grad-muscle)" stroke="var(--color-muscle-stroke)" />
<text x="264" y="490" class="anatomy-label-pin" text-anchor="middle">Hamstrings</text>
</g>`}
<g id="part-gastrocnemius" class="anatomical-part" data-id="gastrocnemius" data-type="muscle">
<path d="M 255 640 C 245 680 246 720 262 760 C 275 730 278 680 269 640 Z" fill="url(#grad-muscle)" stroke="var(--color-muscle-stroke)" />
<path d="M 345 640 C 355 680 354 720 338 760 C 325 730 322 680 331 640 Z" fill="url(#grad-muscle)" stroke="var(--color-muscle-stroke)" />
<text x="256" y="690" class="anatomy-label-pin">Gastrocnemius</text>
</g>
<g id="part-tibialis_anterior" class="anatomical-part" data-id="tibialis_anterior" data-type="muscle" opacity="${!isBack ? '1' : '0.3'}">
<path d="M 268 645 L 264 770 L 270 770 Z" fill="url(#grad-muscle)" stroke="var(--color-muscle-stroke)" />
<path d="M 332 645 L 336 770 L 330 770 Z" fill="url(#grad-muscle)" stroke="var(--color-muscle-stroke)" />
</g>
</g>
<!-- LAG 3: ORGANER -->
<g id="layer-organs" class="layer-group" transform="translate(${getXOffset(3)}, 0)" opacity="${this.state.layerOpacity.organs}">
<g id="part-brain" class="anatomical-part" data-id="brain" data-type="organ">
<ellipse cx="300" cy="74" rx="26" ry="24" fill="#f472b6" stroke="#db2777" stroke-width="1.5" />
<path d="M 285 70 Q 300 60 315 70 M 288 78 Q 300 88 312 78" stroke="#db2777" stroke-width="1.5" fill="none" />
<text x="300" y="78" class="anatomy-label-pin" text-anchor="middle">Hjerne</text>
</g>
<g id="part-lungs" class="anatomical-part" data-id="lungs" data-type="organ">
<path d="M 270 155 C 255 160 250 210 256 240 C 265 245 285 240 290 225 C 290 190 285 160 270 155 Z" fill="url(#grad-lungs)" stroke="#0891b2" />
<path d="M 330 155 C 345 160 350 210 344 240 C 335 245 315 240 310 225 C 310 190 315 160 330 155 Z" fill="url(#grad-lungs)" stroke="#0891b2" />
<text x="270" y="200" class="anatomy-label-pin" text-anchor="middle">Lunge</text>
</g>
<g id="part-heart" class="anatomical-part" data-id="heart" data-type="organ">
<path d="M 300 182 C 290 174 282 184 286 198 C 291 215 302 223 309 229 C 319 219 324 199 316 187 C 311 181 306 180 300 182 Z"
fill="url(#grad-heart)" stroke="#991b1b" stroke-width="2" />
<text x="296" y="205" class="anatomy-label-pin" text-anchor="middle">Hjerte</text>
</g>
<g id="part-stomach" class="anatomical-part" data-id="stomach" data-type="organ">
<path d="M 305 235 C 325 235 330 255 320 275 C 310 280 295 270 300 255 Z" fill="url(#grad-digestive)" stroke="#b45309" />
<text x="316" y="255" class="anatomy-label-pin">Mavesæk</text>
</g>
<g id="part-liver" class="anatomical-part" data-id="liver" data-type="organ">
<path d="M 270 232 C 290 230 300 240 298 265 C 275 272 260 255 270 232 Z" fill="url(#grad-liver)" stroke="#78350f" />
<text x="280" y="255" class="anatomy-label-pin">Lever</text>
</g>
<g id="part-intestines" class="anatomical-part" data-id="intestines" data-type="organ">
<rect x="282" y="278" width="36" height="42" rx="10" fill="url(#grad-digestive)" stroke="#b45309" stroke-width="2" />
<path d="M 285 290 Q 300 300 315 290 M 285 305 Q 300 315 315 305" stroke="#78350f" stroke-width="2" fill="none" />
<text x="300" y="300" class="anatomy-label-pin" text-anchor="middle">Tarme</text>
</g>
</g>
<!-- LAG 4: KREDSLØB (Arterier & Vener) -->
<g id="layer-circulation" class="layer-group" transform="translate(${getXOffset(4)}, 0)" opacity="${this.state.layerOpacity.circulation}">
<g id="part-aorta" class="anatomical-part" data-id="aorta" data-type="vessel">
<path d="M 298 185 C 295 170 308 170 306 185 L 304 365 L 275 480" stroke="var(--color-artery)" stroke-width="3" fill="none" />
<path d="M 304 365 L 325 480" stroke="var(--color-artery)" stroke-width="3" fill="none" />
<path d="M 298 175 L 225 240" stroke="var(--color-artery)" stroke-width="2.5" fill="none" />
<path d="M 306 175 L 375 240" stroke="var(--color-artery)" stroke-width="2.5" fill="none" />
<text x="312" y="330" class="anatomy-label-pin">Aorta</text>
</g>
<g id="part-vena_cava" class="anatomical-part" data-id="vena_cava" data-type="vessel">
<path d="M 292 185 L 292 365 L 265 480" stroke="var(--color-vein)" stroke-width="3" fill="none" />
<path d="M 292 365 L 315 480" stroke="var(--color-vein)" stroke-width="3" fill="none" />
<path d="M 292 185 L 220 245" stroke="var(--color-vein)" stroke-width="2.5" fill="none" />
<path d="M 292 185 L 380 245" stroke="var(--color-vein)" stroke-width="2.5" fill="none" />
<text x="280" y="330" class="anatomy-label-pin">Vena Cava</text>
</g>
</g>
<!-- LAG 5: NERVESYSTEM (Gul) -->
<g id="layer-nervous" class="layer-group" transform="translate(${getXOffset(5)}, 0)" opacity="${this.state.layerOpacity.nervous}">
<g id="part-spinal_cord" class="anatomical-part" data-id="spinal_cord" data-type="nerve">
<path d="M 300 100 L 300 320" stroke="var(--color-nerve)" stroke-width="3.5" fill="none" />
<path d="M 300 135 L 220 250" stroke="var(--color-nerve)" stroke-width="1.8" fill="none" />
<path d="M 300 135 L 380 250" stroke="var(--color-nerve)" stroke-width="1.8" fill="none" />
<text x="306" y="145" class="anatomy-label-pin">Rygmarv</text>
</g>
<g id="part-sciatic_nerve" class="anatomical-part" data-id="sciatic_nerve" data-type="nerve">
<path d="M 300 320 Q 275 420 268 620" stroke="var(--color-nerve)" stroke-width="2.5" fill="none" />
<path d="M 300 320 Q 325 420 332 620" stroke="var(--color-nerve)" stroke-width="2.5" fill="none" />
<text x="260" y="440" class="anatomy-label-pin">Iskiasnerve</text>
</g>
</g>
`;
root.innerHTML = html;
this.attachSvgInteractiveEvents();
this.updateLayerVisibility();
},
attachSvgInteractiveEvents() {
const tooltip = document.getElementById("anatomy-tooltip");
document.querySelectorAll(".anatomical-part").forEach(elem => {
elem.addEventListener("mouseenter", (e) => {
const id = elem.dataset.id;
const item = this.getAnatomicalItem(id);
if (!item) return;
tooltip.style.display = "flex";
tooltip.querySelector(".tt-type").textContent = this.getTypeLabel(item.type);
tooltip.querySelector(".tt-name-da").textContent = item.names.da;
tooltip.querySelector(".tt-name-la").textContent = item.names.la;
const rect = elem.getBoundingClientRect();
const canvasRect = document.getElementById("canvas-container").getBoundingClientRect();
tooltip.style.left = (rect.left + rect.width / 2 - canvasRect.left) + "px";
tooltip.style.top = (rect.top - canvasRect.top - 8) + "px";
});
elem.addEventListener("mouseleave", () => {
tooltip.style.display = "none";
});
elem.addEventListener("click", (e) => {
e.stopPropagation();
const id = elem.dataset.id;
this.selectStructure(id);
if (this.onSvgPartClickedForQuiz) {
this.onSvgPartClickedForQuiz(id);
}
});
});
},
selectStructure(id) {
this.state.selectedPartId = id;
const item = this.getAnatomicalItem(id);
if (!item) return;
document.querySelectorAll(".anatomical-part").forEach(el => el.classList.remove("selected"));
document.querySelectorAll(`.anatomical-part[data-id="${id}"]`).forEach(el => el.classList.add("selected"));
document.getElementById("panel-empty").style.display = "none";
const content = document.getElementById("panel-active-content");
content.style.display = "flex";
document.getElementById("info-type").textContent = this.getTypeLabel(item.type);
document.getElementById("info-name-da").textContent = item.names.da;
document.getElementById("info-name-la").textContent = item.names.la;
document.getElementById("info-name-en").textContent = item.names.en;
document.getElementById("info-location").textContent = item.location;
document.getElementById("info-function").textContent = item.function;
document.getElementById("info-didactic").textContent = item.didactic;
const attachCard = document.getElementById("info-muscle-attachments-card");
if (item.attachments) {
attachCard.style.display = "flex";
document.getElementById("info-attachments").textContent = item.attachments;
} else {
attachCard.style.display = "none";
}
const relList = document.getElementById("info-related-list");
relList.innerHTML = "";
if (item.related && item.related.length > 0) {
item.related.forEach(relId => {
const relItem = this.getAnatomicalItem(relId);
if (relItem) {
const btn = document.createElement("button");
btn.className = "related-btn";
btn.textContent = this.formatName(relItem.names);
btn.addEventListener("click", () => this.selectStructure(relId));
relList.appendChild(btn);
}
});
}
this.updateFavoriteButtonState(id);
document.getElementById("breadcrumb-current").textContent = this.formatName(item.names);
if (window.innerWidth <= 768) {
document.getElementById("right-panel").classList.add("open");
}
},
formatName(names) {
if (!names) return "";
switch (this.state.langMode) {
case "da": return names.da;
case "la": return names.la;
case "en": return names.en;
case "da-la": return `${names.da} (${names.la})`;
case "da-en": return `${names.da} / ${names.en}`;
case "all": return `${names.da} [${names.la}] (${names.en})`;
default: return `${names.da} (${names.la})`;
}
},
updateExertionUI() {
const data = PhysiologyData[this.state.exertionState];
if (!data) return;
document.getElementById("metric-hr").textContent = data.hr;
document.getElementById("metric-rf").textContent = data.rf;
document.getElementById("metric-q").textContent = data.q;
document.getElementById("metric-muscleflow").textContent = data.muscleFlow;
const heartIcon = document.querySelector(".heart-icon");
if (heartIcon) {
heartIcon.style.animationDuration = `${data.pulseRateMs}ms`;
}
},
zoom(factor) {
this.state.zoomScale = Math.min(Math.max(this.state.zoomScale * factor, 0.6), 3.5);
this.applyTransform();
},
updateFavoriteButtonState(id) {
const btn = document.getElementById("btn-toggle-fav");
const isFav = this.state.favorites.includes(id);
btn.classList.toggle("is-favorite", isFav);
document.getElementById("btn-fav-text").textContent = isFav ? "Gemt som fokusemne" : "Tilføj som fokusemne";
},
updateFavoritesCount() {
const btn = document.getElementById("btn-show-favorites");
if (this.state.favorites.length > 0) {
btn.style.color = "var(--accent-amber)";
} else {
btn.style.color = "";
}
},
initMovementModule() {
const list = document.getElementById("movement-button-list");
list.innerHTML = "";
MovementData.forEach(mov => {
const btn = document.createElement("button");
btn.className = "movement-select-btn";
btn.innerHTML = `
<span>${mov.name.da}</span>
<small>${mov.joint}</small>
`;
btn.addEventListener("click", () => {
document.querySelectorAll(".movement-select-btn").forEach(b => b.classList.remove("active"));
btn.classList.add("active");
this.demonstrateMovement(mov);
});
list.appendChild(btn);
});
},
};
// =============================================================================
// ANATOMILAB MODULES — data, SVG interaction, learning and persistence.
// All state remains local. No network requests or package dependencies.
// =============================================================================
const $ = id => document.getElementById(id);
const StorageModule = {
read(key, fallback) { try { return JSON.parse(localStorage.getItem('anatomilab.v1.'+key)) ?? fallback; } catch { return fallback; } },
write(key, value) { try { localStorage.setItem('anatomilab.v1.'+key, JSON.stringify(value)); } catch { $('storage-note').hidden=false; } }
};
const LayerData = {
silhouette:{name:'Ydre krop',color:'#94a3b8'}, skeleton:{name:'Skelet',color:'#e2e8f0'}, muscles:{name:'Muskler',color:'#ec7189'},
organs:{name:'Organer',color:'#ebad54'}, circulation:{name:'Kredsløb',color:'#f16c70'}, respiratory:{name:'Åndedræt',color:'#67e8f9'}, digestive:{name:'Fordøjelse',color:'#f5b658'}, nervous:{name:'Nervesystem',color:'#f3d654'}, joints:{name:'Led og sener',color:'#a9a5ff'}
};
const RegionData={
body:{names:{da:'Menneskekrop',en:'Human body',la:'Corpus humanum'},parent:null,box:[130,25,340,880]},
head:{names:{da:'Hoved',en:'Head',la:'Caput'},parent:'body',box:[235,30,130,120]},
torso:{names:{da:'Kropstamme',en:'Trunk',la:'Truncus'},parent:'body',box:[235,130,130,265]},
arm:{names:{da:'Arm',en:'Upper limb',la:'Membrum superius'},parent:'body',box:[155,130,100,320]},
leg:{names:{da:'Ben',en:'Lower limb',la:'Membrum inferius'},parent:'body',box:[235,345,130,545]}
};
const AnatomyModule={
items(){return Object.values(AnatomyData).flatMap(g=>Object.values(g));},
get(id){return this.items().find(x=>x.id===id)||RegionData[id];},
init(){
const arms=['humerus','radius','ulna','hand_bones','biceps','triceps','deltoideus','clavicle','scapula'];
const legs=['femur','patella','tibia','fibula','foot_bones','gluteus_maximus','quadriceps','hamstrings','gastrocnemius','soleus','tibialis_anterior'];
for(const [group,items] of Object.entries(AnatomyData))for(const item of Object.values(items)){
item.layer??={bones:'skeleton',muscles:'muscles',organs:'organs',vessels:'circulation',nervous:'nervous',joints:'joints'}[group];
item.parent??=arms.includes(item.id)?'arm':legs.includes(item.id)?'leg':['brain','cranium'].includes(item.id)?'head':'torso';
item.tags??=[item.type,item.layer];item.view??=['triceps','gluteus_maximus','hamstrings','gastrocnemius','soleus','trapezius','latissimus_dorsi','scapula','achilles'].includes(item.id)?'posterior':'anterior';
}
for(const id of ['lungs','trachea','bronchi','alveoli'])this.get(id).layer='respiratory';
for(const id of ['stomach','liver','intestines','mouth','pharynx','esophagus','colon','rectum'])this.get(id).layer='digestive';
this.get('brain').layer='nervous';
},
children(id){return this.items().filter(x=>x.parent===id).concat(Object.entries(RegionData).filter(([,v])=>v.parent===id).map(([id,v])=>({id,...v})));}
};
AnatomyModule.init();
for(const m of MovementData){const joints=m.id.startsWith('elbow')?['elbow']:m.id.startsWith('knee')?['knee']:m.id==='shoulder_abduction'?['shoulder']:['hip','knee','ankle'];m.highlightParts=[...new Set([...m.highlightParts,...joints])];}
const LanguageModule={
visible(){return {da:['da'],en:['en'],la:['la'],'da-la':['da','la'],'da-en':['da','en'],all:['da','en','la']}[AppCore.state.langMode]||['da'];},
name(id){const x=AnatomyModule.get(id);return x?AppCore.formatName(x.names):id;},
update(){
document.querySelectorAll('[data-anatomy-name]').forEach(el=>el.textContent=this.name(el.dataset.anatomyName));
document.querySelectorAll('.anatomical-part').forEach(el=>{const label=this.name(el.dataset.id);el.setAttribute('aria-label',label);el.querySelectorAll('title').forEach(t=>t.textContent=label);});
['da','en','la'].forEach(lang=>{$('info-name-'+lang).hidden=!this.visible().includes(lang);$('info-name-'+lang).classList.toggle('primary-name',lang===this.visible()[0]);});
document.querySelectorAll('.movement-select-btn').forEach((b,i)=>b.firstElementChild.textContent=AppCore.formatName(MovementData[i].name));
}
};
const NavigationModule={
ensure(id){
const item=AnatomyModule.get(id);if(!item||!item.layer)return;
if(AppCore.state.currentView!=='split' && item.view!==AppCore.state.currentView){AppCore.state.currentView=item.view;AppCore.renderSvgLayers();}
if(!document.querySelector('.anatomical-part[data-id="'+id+'"]')){AppCore.state.currentView=item.view;AppCore.renderSvgLayers();}
if(!AppCore.state.activeLayers[item.layer]||AppCore.state.layerOpacity[item.layer]<.2){AppCore.state.activeLayers[item.layer]=true;AppCore.state.layerOpacity[item.layer]=1;}
// Isolate a selected structure's system when an overlying layer would conceal it.
if(item.layer==='skeleton'&&AppCore.state.activeLayers.muscles)AppCore.state.activeLayers.muscles=false;
AppCore.updateLayerVisibility();this.sync();
},
sync(){document.querySelectorAll('[data-view]').forEach(b=>{b.classList.toggle('active',b.dataset.view===AppCore.state.currentView);b.setAttribute('aria-pressed',b.dataset.view===AppCore.state.currentView);});document.querySelectorAll('input[data-layer]').forEach(cb=>cb.checked=!!AppCore.state.activeLayers[cb.dataset.layer]);document.querySelectorAll('[data-layer-opacity]').forEach(el=>el.value=AppCore.state.layerOpacity[el.dataset.layer]);$('split-slider-wrap').style.display=AppCore.state.currentView==='split'?'flex':'none';},
focus(id){
this.ensure(id);let box=RegionData[id]?.box;if([...$('region-select').options].some(o=>o.value===id))$('region-select').value=id;
const target=document.querySelector('.anatomical-part[data-id="'+id+'"]');
if(!box&&target){const b=target.getBBox();box=[b.x-28,b.y-28,b.width+56,b.height+56];}
if(box){AppCore.resetZoom();$('anatomy-svg').setAttribute('viewBox',box.join(' '));}
this.breadcrumbs(id);
},
breadcrumbs(id){let ids=[],next=id;const seen=new Set();while(next&&!seen.has(next)){seen.add(next);ids.unshift(next);next=AnatomyModule.get(next)?.parent;}
$('breadcrumbs').replaceChildren();ids.forEach((key,i)=>{if(i){const sep=document.createElement('span');sep.textContent=' / ';$('breadcrumbs').append(sep);}const b=document.createElement('button');b.textContent=LanguageModule.name(key);b.onclick=()=>key==='body'?AppCore.resetZoom():this.focus(key);$('breadcrumbs').append(b);});const hidden=document.createElement('span');hidden.id='breadcrumb-current';hidden.hidden=true;$('breadcrumbs').append(hidden);
}
};
const SearchModule={
render(query){const container=$('search-results');container.replaceChildren();if(!query.trim()){container.style.display='none';return;}const q=query.toLocaleLowerCase('da');const results=AnatomyModule.items().filter(x=>[...Object.values(x.names),...x.tags].some(v=>v.toLocaleLowerCase('da').includes(q)));
if(!results.length)container.textContent='Ingen resultater. Prøv et dansk, engelsk eller latinsk navn.';
for(const x of results.slice(0,16)){const b=document.createElement('button');b.className='search-item';b.textContent=LanguageModule.name(x.id);b.onclick=()=>{AppCore.selectStructure(x.id);NavigationModule.focus(x.id);container.style.display='none';$('search-input').value='';};container.append(b);}container.style.display='block';
},
favorites(){const list=$('favorite-list');list.replaceChildren();for(const id of AppCore.state.favorites){if(!AnatomyModule.get(id))continue;const b=document.createElement('button');b.className='related-btn';b.textContent='★ '+LanguageModule.name(id);b.onclick=()=>{AppCore.selectStructure(id);AppCore.closeOverlay('favorites');};list.append(b);}if(!list.children.length)list.textContent='Vælg en struktur og tryk på stjernen for at gemme et fokusemne.';AppCore.toggleOverlay('favorites');}
};
// EXTENSION POINT: geometry uses SVG viewBox coordinates and stable data IDs.
const GeometryData={
mouth:'<path d="M288 106 Q300 113 312 106" stroke="#efaf75" stroke-width="6" fill="none"/>',
pharynx:'<path d="M301 111 L301 126" stroke="#e4a777" stroke-width="6"/>',
trachea:'<path d="M297 125 L297 161" stroke="#67e8f9" stroke-width="7"/>',
bronchi:'<path d="M297 159 L274 181 M297 159 L326 181" stroke="#67e8f9" stroke-width="5" fill="none"/>',
alveoli:'<circle cx="272" cy="218" r="7" fill="#adf5ff"/><circle cx="281" cy="223" r="6" fill="#adf5ff"/><circle cx="271" cy="229" r="5" fill="#adf5ff"/>',
esophagus:'<path d="M305 126 L305 230 Q316 240 314 250" stroke="#e4a777" stroke-width="5" fill="none"/>',
colon:'<path d="M276 316 L272 280 Q272 271 281 271 L319 271 Q329 271 329 281 L329 319 Q328 330 303 332" stroke="#f5b658" stroke-width="8" fill="none"/>',
rectum:'<path d="M303 332 L303 352" stroke="#df8d46" stroke-width="7"/>',
soleus:'<path d="M255 762 L265 805 L272 762 M328 762 L335 805 L345 762" fill="#d3546f" stroke="#f598a9"/>',
shoulder:'<circle cx="226" cy="156" r="12"/><circle cx="374" cy="156" r="12"/>',
elbow:'<circle cx="210" cy="270" r="11"/><circle cx="390" cy="270" r="11"/>',
hip:'<circle cx="268" cy="383" r="12"/><circle cx="332" cy="383" r="12"/>',
knee:'<circle cx="266" cy="627" r="14"/><circle cx="334" cy="627" r="14"/>',
ankle:'<circle cx="268" cy="819" r="11"/><circle cx="332" cy="819" r="11"/>',
acl:'<path d="M259 616 L273 637 M327 616 L341 637" stroke="#d7cbff" stroke-width="4"/>',
patellar_tendon:'<path d="M266 634 L267 649 M334 634 L333 649" stroke="#bfbbff" stroke-width="5"/>',
achilles:'<path d="M265 798 L264 846 M335 798 L336 846" stroke="#bfbbff" stroke-width="5"/>',
right_atrium:'<rect x="290" y="183" width="9" height="12" rx="4" fill="#68acfa"/>',
right_ventricle:'<path d="M290 198 L301 198 L301 220 Q291 214 290 198" fill="#5b9bdc"/>',
left_atrium:'<rect x="303" y="183" width="10" height="12" rx="4" fill="#ffb3b8"/>',
left_ventricle:'<path d="M304 198 L316 198 Q318 215 305 224 Z" fill="#f9707c"/>'
};
const SVGModule={
decorate(){const root=$('svg-transform-root'),ns='http://www.w3.org/2000/svg';
for(const layer of Object.keys(LayerData)){if(!$('layer-'+layer)){const g=document.createElementNS(ns,'g');g.id='layer-'+layer;g.classList.add('layer-group');root.append(g);}}
for(const item of AnatomyModule.items()){let el=root.querySelector('[data-id="'+item.id+'"]');if(!el&&GeometryData[item.id]){el=document.createElementNS(ns,'g');el.id='part-'+item.id;el.classList.add('anatomical-part');el.dataset.id=item.id;el.dataset.type=item.type;el.innerHTML=GeometryData[item.id];}
if(el)$('layer-'+item.layer).append(el);
}
// Heart inset remains under the heart in the hierarchy but is selected separately.
// Back view mirrors internal organs to preserve anatomical left/right.
if(AppCore.state.currentView==='posterior')for(const id of ['organs','digestive','respiratory','circulation'])$('layer-'+id).setAttribute('transform','translate(600 0) scale(-1 1)');
const split=AppCore.state.currentView==='split';
const order=Object.keys(LayerData);order.forEach((layer,i)=>{if(split)$('layer-'+layer).setAttribute('transform',`translate(${(i-4)*AppCore.state.splitDistance*2},0)`);});
if(split){$('anatomy-svg').setAttribute('viewBox',`${100-8*AppCore.state.splitDistance} 0 ${400+16*AppCore.state.splitDistance} 1000`);}
for(const id of ['trapezius','latissimus_dorsi','gastrocnemius','soleus']){const el=$('part-'+id);if(el)el.style.display=AppCore.state.currentView==='posterior'||split?'':'none';}
if(AppCore.state.currentView==='posterior')$('part-tibialis_anterior')?.remove();
$('layer-joints').setAttribute('fill','transparent');$('layer-joints').setAttribute('stroke','#aba5ff');$('layer-joints').setAttribute('stroke-width','3');
root.querySelectorAll('.anatomy-label-pin').forEach(el=>el.remove());
root.querySelectorAll('.anatomical-part').forEach(el=>{
el.tabIndex=0;el.setAttribute('role','button');el.setAttribute('aria-label',LanguageModule.name(el.dataset.id));
const title=document.createElementNS(ns,'title');title.textContent=LanguageModule.name(el.dataset.id);el.prepend(title);
const b=el.getBBox();const label=document.createElementNS(ns,'text');label.setAttribute('x',b.x+b.width+10);label.setAttribute('y',b.y+b.height/2);label.setAttribute('class','anatomy-label-pin');label.dataset.anatomyName=el.dataset.id;label.textContent=LanguageModule.name(el.dataset.id);el.append(label);
el.addEventListener('keydown',e=>{if(e.key==='Enter'||e.key===' '){e.preventDefault();el.dispatchEvent(new MouseEvent('click',{bubbles:true}));}});
el.addEventListener('click',e=>{e.stopImmediatePropagation();if(QuizModule.active&&QuizModule.type==='find'){QuizModule.answer(el.dataset.id);return;}AppCore.selectStructure(el.dataset.id);},true);
el.addEventListener('mouseenter',()=>{if(QuizModule.active){$('anatomy-tooltip').style.display='none';return;}$('anatomy-tooltip').querySelector('.tt-name-da').textContent=LanguageModule.name(el.dataset.id);$('anatomy-tooltip').querySelector('.tt-name-la').textContent='';});
});
// Stable per-side shape IDs make detailed SVG replacement straightforward.
root.querySelectorAll('.anatomical-part').forEach(el=>{[...el.children].filter(x=>!['text','title'].includes(x.tagName)).forEach((shape,i)=>shape.id=el.dataset.id+'-'+(i===0?'right':i===1?'left':'detail-'+i));});
AppCore.updateLayerVisibility();LanguageModule.update();
}
};
// EXTENSION POINT: each process is a sequence of independently selectable steps.
const ProcessModule={key:'oxygen',index:0,progress:0,playing:false,last:0,raf:0,
init(){
$('btn-process-play').onclick=()=>{this.playing=!this.playing;this.last=0;this.controls();if(this.playing)this.raf=requestAnimationFrame(t=>this.tick(t));else cancelAnimationFrame(this.raf);};
$('btn-process-reset').onclick=()=>{this.pause();this.index=0;this.progress=0;this.render();};
document.querySelectorAll('[data-process]').forEach(b=>b.onclick=()=>{this.pause();this.key=b.dataset.process;this.index=0;this.progress=0;this.prepare();});
this.render(false);
},
prepare(){AppCore.state.currentView='anterior';AppCore.renderSvgLayers();AppCore.isolateSystem('process-'+this.key);AppCore.resetZoom();this.render();},
pause(){this.playing=false;cancelAnimationFrame(this.raf);this.last=0;this.controls();},
controls(){$('btn-process-play-text').textContent=this.playing?'Pause':this.index||this.progress?'Fortsæt':'Start forløb';$('process-track')?.classList.toggle('playing',this.playing);},
tick(t){if(!this.playing)return;if(this.last)this.progress+=Math.min(t-this.last,100)*Number($('process-speed').value)/2600;this.last=t;if(this.progress>=1){this.progress=0;this.index=(this.index+1)%ProcessData[this.key].steps.length;this.render();}const dot=$('process-particle');if(dot)dot.style.left=`${8+84*this.progress}%`;if(this.points){const [a,b]=this.points;const particle=$('body-particle');if(particle){particle.setAttribute('cx',a.x+(b.x-a.x)*this.progress);particle.setAttribute('cy',a.y+(b.y-a.y)*this.progress);}}this.raf=requestAnimationFrame(n=>this.tick(n));},
render(highlight=true){
const proc=ProcessData[this.key],current=proc.steps[this.index];$('process-explanation').textContent=current.desc;$('process-explanation').setAttribute('aria-live','polite');
$('process-step-indicators').replaceChildren();proc.steps.forEach((s,i)=>{const b=document.createElement('button');b.className='process-step-item'+(i===this.index?' active':'');b.textContent=(i+1)+'. '+s.name;b.setAttribute('aria-current',i===this.index?'step':'false');b.onclick=()=>{this.index=i;this.progress=0;this.render();};$('process-step-indicators').append(b);});
document.querySelectorAll('[data-process]').forEach(b=>b.classList.toggle('active',b.dataset.process===this.key));
$('process-from').textContent=current.name;$('process-to').textContent=proc.steps[(this.index+1)%proc.steps.length].name;
$('process-particle').style.background=proc.color;
if(highlight){document.querySelectorAll('.process-current').forEach(e=>e.classList.remove('process-current'));document.querySelectorAll('[data-id="'+current.organ+'"]').forEach(e=>e.classList.add('process-current'));}
if(highlight){
$('body-particle')?.remove();$('body-process-path')?.remove();
const currentEl=document.querySelector('[data-id="'+current.organ+'"]'),nextEl=document.querySelector('[data-id="'+proc.steps[(this.index+1)%proc.steps.length].organ+'"]');
if(currentEl&&nextEl){const point=el=>{const b=el.getBBox();return {x:b.x+b.width/2,y:b.y+b.height/2};};this.points=[point(currentEl),point(nextEl)];const [a,b]=this.points,ns='http://www.w3.org/2000/svg';const path=document.createElementNS(ns,'path');path.id='body-process-path';path.setAttribute('d',`M${a.x} ${a.y} L${b.x} ${b.y}`);path.setAttribute('stroke',proc.color);path.setAttribute('stroke-width','2');path.setAttribute('stroke-dasharray','4 4');path.setAttribute('fill','none');path.style.pointerEvents='none';const dot=document.createElementNS(ns,'circle');dot.id='body-particle';dot.setAttribute('cx',a.x);dot.setAttribute('cy',a.y);dot.setAttribute('r','5');dot.setAttribute('fill',proc.color);dot.setAttribute('stroke','white');dot.style.pointerEvents='none';$('svg-transform-root').append(path,dot);}
}
this.controls();
}
};
const MovementModule={mov:null,playing:false,phase:0,last:0,raf:0,
init(){AppCore.initMovementModule();$('movement-toggle').onclick=()=>{this.playing=!this.playing;this.last=0;$('movement-toggle').textContent=this.playing?'Pause bevægelse':'Afspil bevægelse';if(this.playing)this.raf=requestAnimationFrame(t=>this.tick(t));else cancelAnimationFrame(this.raf);};$('movement-range').oninput=e=>{this.pause();this.phase=Number(e.target.value)/100*Math.PI;this.draw();};},
pause(){this.playing=false;cancelAnimationFrame(this.raf);$('movement-toggle').textContent='Afspil bevægelse';},
select(mov){this.mov=mov;this.phase=0;this.pause();AppCore.state.currentView=['elbow_extension','knee_flexion'].includes(mov.id)?'posterior':'anterior';AppCore.renderSvgLayers();AppCore.isolateSystem('movement');AppCore.resetZoom();
mov.highlightParts.forEach(id=>document.querySelectorAll('[data-id="'+id+'"]').forEach(e=>e.classList.add('active-agonist')));mov.antagonistParts.forEach(id=>document.querySelectorAll('[data-id="'+id+'"]').forEach(e=>e.classList.add('active-antagonist')));
$('movement-details-card').style.display='flex';$('movement-active-name').textContent=AppCore.formatName(mov.name);$('movement-agonist-desc').textContent=mov.agonist;$('movement-antagonist-desc').textContent=mov.antagonist;$('movement-joint-desc').textContent=mov.joint;$('movement-didactic-text').textContent=mov.didactic;$('movement-demo').hidden=false;this.draw();
},
tick(t){if(!this.playing)return;if(this.last)this.phase+=Math.min(t-this.last,100)/850;this.last=t;this.draw();this.raf=requestAnimationFrame(x=>this.tick(x));},
draw(){if(!this.mov)return;const p=(1-Math.cos(this.phase))/2,id=this.mov.id;let lines='';
const line=(a,b,color='#dce8ef',w=9)=>`<line x1="${a[0]}" y1="${a[1]}" x2="${b[0]}" y2="${b[1]}" stroke="${color}" stroke-width="${w}" stroke-linecap="round"/>`;
const circle=(a,r=7)=>`<circle cx="${a[0]}" cy="${a[1]}" r="${r}" fill="#80e3c7"/>`;
if(id.includes('elbow')){const flex=id==='elbow_extension'?1-p:p;const shoulder=[110,28],elbow=[110,105],a=(80-flex*135)*Math.PI/180,hand=[110+88*Math.cos(a),105+88*Math.sin(a)];lines=line(shoulder,elbow)+line(elbow,hand)+line([100,40],[100,89],id==='elbow_flexion'?'#6ee7b7':'#ef9c65',12)+circle(elbow);}
else if(id.includes('knee')){const flex=id==='knee_extension'?1-p:p;const hip=[155,25],knee=[155,95],a=(90+flex*100)*Math.PI/180,foot=[155+85*Math.cos(a),95+85*Math.sin(a)];lines=line(hip,knee)+line(knee,foot)+circle(knee);}
else if(id==='shoulder_abduction'){const shoulder=[150,62],a=(90-p*90)*Math.PI/180,hand=[150+100*Math.cos(a),62+100*Math.sin(a)];lines=line([150,62],[150,160])+line(shoulder,hand,'#6ee7b7')+circle(shoulder)+circle([150,32],18);}
else{const squat=id==='run'?.25:p;const jump=id==='jump'?-25*Math.sin(p*Math.PI):0;const hip=[145-squat*28,94+squat*25+jump],knee=[145+squat*28,132+jump],foot=[145,185+jump],head=[hip[0]+squat*22,hip[1]-62];lines=line(head,hip)+line(hip,knee,'#6ee7b7')+line(knee,foot)+circle(knee)+circle([head[0],head[1]-18],15);if(id==='run'){const swing=[140-45*Math.sin(this.phase),135];lines+=line(hip,swing,'#ef9c65')+line(swing,[swing[0]-35,175],'#ef9c65');}lines+=line([60,200],[260,200],'#547084',2);}
$('movement-diagram').innerHTML=lines;$('movement-range').value=Math.round(p*100);
}
};
const QuizModule={active:false,type:'find',index:0,score:0,answered:false,order:[],matched:[],pending:null,
init(){document.querySelectorAll('[data-quiztype]').forEach(b=>b.onclick=()=>{this.type=b.dataset.quiztype;this.index=0;this.load();});$('btn-next-question').onclick=()=>{this.index++;this.load();};},
start(){this.active=true;this.load();},
stop(){this.active=false;LanguageModule.update();document.body.classList.remove('quiz-active');document.querySelectorAll('.quiz-target').forEach(e=>e.classList.remove('quiz-target'));},
feedback(ok,text){$('quiz-feedback-box').className='quiz-feedback show '+(ok?'success':'failure');$('quiz-feedback-box').textContent=text;if(ok&&!this.answered){this.score++;this.answered=true;$('quiz-score').textContent=this.score;}},
load(){
this.active=true;document.body.classList.add('quiz-active');document.querySelectorAll('.active-agonist,.active-antagonist,.process-current').forEach(e=>e.classList.remove('active-agonist','active-antagonist','process-current'));this.answered=false;this.pending=null;this.matched=[];this.order=[];
$('quiz-feedback-box').className='quiz-feedback';$('quiz-feedback-box').textContent='';$('quiz-interaction-area').replaceChildren();document.querySelectorAll('.selected,.quiz-target').forEach(e=>e.classList.remove('selected','quiz-target'));
document.querySelectorAll('[data-quiztype]').forEach(b=>b.classList.toggle('active',b.dataset.quiztype===this.type));
document.querySelectorAll('.anatomical-part title').forEach(t=>t.textContent='Vælg struktur');$('quiz-mode-title').textContent={find:'Find kropsdelen',name:'Hvad hedder den?',match:'Dansk ↔ latin',movement:'Muskel og bevægelse',sequence:'Følg processen'}[this.type];
if(this.type==='find'||this.type==='name'){
const qs=this.type==='find'?QuizData.findQuestions:QuizData.nameQuestions;this.question=qs[this.index%qs.length];const id=this.question.targetId||this.question.partId;NavigationModule.ensure(id);AppCore.isolateSystem(AnatomyModule.get(id).layer);AppCore.resetZoom();
$('quiz-question-text').textContent=this.question.text||this.question.question;$('quiz-instruction').textContent=this.type==='find'?'Klik på modellen. Du kan også bruge Tab og Enter.':'Vælg navnet på den gule struktur.';
if(this.type==='find'){$('quiz-interaction-area').textContent='Hint: '+this.question.hint;}
else{document.querySelectorAll('[data-id="'+id+'"]').forEach(e=>e.classList.add('quiz-target'));this.options(this.question);}
}else if(this.type==='movement'){this.question=QuizData.movementQuestions[this.index%QuizData.movementQuestions.length];$('quiz-question-text').textContent=this.question.question;$('quiz-instruction').textContent='Vælg ét svar.';this.options(this.question);}
else if(this.type==='match'){
$('quiz-question-text').textContent='Forbind dansk med latin';$('quiz-instruction').textContent='Vælg et dansk navn og derefter det latinske navn.';const wrap=document.createElement('div');wrap.className='match-columns';$('quiz-interaction-area').append(wrap);
for(const lang of ['da','la']){const col=document.createElement('div');wrap.append(col);const pairs=[...QuizData.matchPairs].sort(()=>Math.random()-.5);for(const pair of pairs){const b=document.createElement('button');b.className='match-card';b.textContent=pair[lang];b.dataset.pair=pair.da;b.dataset.lang=lang;b.onclick=()=>this.match(b);col.append(b);}}
}else{const proc=ProcessData[['oxygen','blood','digestion'][this.index%3]];this.question=proc;$('quiz-question-text').textContent='Sæt rækkefølgen: '+proc.title;$('quiz-instruction').textContent='Klik på trinnene i den rækkefølge, de foregår. Fortryd fjerner sidste valg.';const list=[...proc.steps].sort(()=>Math.random()-.5);for(const s of list){const b=document.createElement('button');b.className='quiz-option-btn';b.textContent=s.name;b.onclick=()=>{if(this.answered)return;this.order.push(s.organ);b.disabled=true;this.sequenceStatus();};$('quiz-interaction-area').append(b);}const out=document.createElement('p');out.id='sequence-answer';$('quiz-interaction-area').append(out);const undo=document.createElement('button');undo.className='pill-btn';undo.textContent='Fortryd';undo.onclick=()=>{if(this.answered)return;this.order.pop();$('quiz-interaction-area').querySelectorAll('.quiz-option-btn').forEach(b=>b.disabled=this.order.includes(proc.steps.find(s=>s.name===b.textContent).organ));this.sequenceStatus();};$('quiz-interaction-area').append(undo);}
document.querySelectorAll('.anatomical-part title').forEach(t=>t.textContent='Vælg struktur');
},
options(q){q.options.forEach((opt,i)=>{const b=document.createElement('button');b.className='quiz-option-btn';b.textContent=opt;b.onclick=()=>{if(this.answered)return;this.feedback(i===q.correctIndex,i===q.correctIndex?q.feedback:'Prøv igen.');if(this.answered)$('quiz-interaction-area').querySelectorAll('button').forEach(b=>b.disabled=true);};$('quiz-interaction-area').append(b);});},
answer(id){if(!this.active||this.type!=='find'||this.answered)return;this.feedback(id===this.question.targetId,id===this.question.targetId?'Rigtigt! '+LanguageModule.name(id):'Ikke helt. Prøv igen.');if(this.answered)document.querySelectorAll('[data-id="'+id+'"]').forEach(e=>e.classList.add('quiz-target'));},
match(b){if(b.disabled)return;if(!this.pending||this.pending.dataset.lang===b.dataset.lang){this.pending?.classList.remove('selected');this.pending=b;b.classList.add('selected');return;}const ok=this.pending.dataset.pair===b.dataset.pair;if(ok){this.pending.disabled=b.disabled=true;this.pending.classList.add('matched');b.classList.add('matched');this.matched.push(b.dataset.pair);}this.pending.classList.remove('selected');this.pending=null;this.feedback(false,ok?'Parret er korrekt.':'De to navne passer ikke sammen. Prøv igen.');if(this.matched.length===QuizData.matchPairs.length)this.feedback(true,'Alle par er rigtige!');},
sequenceStatus(){const proc=this.question;$('sequence-answer').textContent=this.order.map(id=>proc.steps.find(s=>s.organ===id).name).join(' → ');if(this.order.length===proc.steps.length){const ok=proc.steps.every((s,i)=>s.organ===this.order[i]);this.feedback(ok,ok?'Rigtig rækkefølge!':'Rækkefølgen er ikke korrekt endnu. Brug Fortryd og prøv igen.');}}
};
const TeacherModule={mode:'none',set(mode){this.mode=mode;document.querySelectorAll('.anatomical-part title').forEach(t=>t.textContent=mode==='hidden'?'Struktur':LanguageModule.name(t.parentElement.dataset.id));document.body.classList.toggle('show-teacher-labels',mode==='all');document.body.classList.toggle('hide-names',mode==='hidden');$('teacher-mode').value=mode;$('teacher-status').textContent={none:'Navne vises ved valg',all:'Etiketter på modellen',hidden:'Navne skjult til fælles øvelse'}[mode];},init(){$('teacher-mode').onchange=e=>this.set(e.target.value);$('teacher-quiz').onclick=()=>AppCore.toggleOverlay('quiz');}};
const SettingsModule={save(){StorageModule.write('preferences',{lang:AppCore.state.langMode,exertion:AppCore.state.exertionState,layers:AppCore.state.activeLayers,opacity:AppCore.state.layerOpacity});},load(){const p=StorageModule.read('preferences',{});if(['da','en','la','da-la','da-en','all'].includes(p.lang))AppCore.state.langMode=p.lang;if(PhysiologyData[p.exertion])AppCore.state.exertionState=p.exertion;for(const layer of Object.keys(LayerData)){if(typeof p.layers?.[layer]==='boolean')AppCore.state.activeLayers[layer]=p.layers[layer];const opacity=p.opacity?.[layer];if(typeof opacity==='number'&&opacity>=0&&opacity<=1)AppCore.state.layerOpacity[layer]=opacity;}const fav=StorageModule.read('favorites',[]);AppCore.state.favorites=Array.isArray(fav)?fav.filter(id=>typeof id==='string'&&AnatomyModule.get(id)):[];}};
const baseRender=AppCore.renderSvgLayers.bind(AppCore),baseSelect=AppCore.selectStructure.bind(AppCore);
Object.assign(AppCore,{
getAnatomicalItem(id){return AnatomyModule.get(id);},
getTypeLabel(type){return {bone:'Knogle',muscle:'Muskel',organ:'Organ',vessel:'Blodkar',nerve:'Nerve',joint:'Led',tendon:'Sene',ligament:'Ledbånd'}[type]||'Struktur';},
renderSvgLayers(){baseRender();SVGModule.decorate();NavigationModule.sync();$('view-note').textContent=this.state.currentView==='posterior'?'Bagfra · indre organer ses gennem kroppen':this.state.currentView==='split'?'Systemer adskilt · ikke anatomisk målfast':'Forfra · kroppens højre side er til venstre på skærmen';},
selectStructure(id){if(!AnatomyModule.get(id)?.type)return;NavigationModule.ensure(id);baseSelect(id);LanguageModule.update();NavigationModule.breadcrumbs(id);
const item=AnatomyModule.get(id);$('drill-links').replaceChildren();for(const child of AnatomyModule.children(id)){const b=document.createElement('button');b.className='related-btn';b.textContent=LanguageModule.name(child.id);b.onclick=()=>{this.selectStructure(child.id);NavigationModule.focus(child.id);};$('drill-links').append(b);} $('drill-card').hidden=!$('drill-links').children.length;
$('btn-focus-structure').onclick=()=>NavigationModule.focus(id);
document.querySelectorAll('.overlay-panel').forEach(p=>p.style.display='none');this.activeOverlay=null;ProcessModule.pause();MovementModule.pause();QuizModule.stop();$('right-panel').classList.add('open');$('left-sidebar').classList.remove('open');
},
updateLanguageUI(){if(this.state.selectedPartId){const item=AnatomyModule.get(this.state.selectedPartId);['da','en','la'].forEach(l=>$('info-name-'+l).textContent=item.names[l]);$('info-related-list').querySelectorAll('button').forEach((b,i)=>b.textContent=LanguageModule.name(item.related.filter(id=>AnatomyModule.get(id))[i]));NavigationModule.breadcrumbs(item.id);}LanguageModule.update();SettingsModule.save();},
updateLayerVisibility(){for(const layer of Object.keys(LayerData)){const el=$('layer-'+layer);if(el){el.style.display=this.state.activeLayers[layer]?'inline':'none';el.setAttribute('opacity',this.state.layerOpacity[layer]);}}NavigationModule.sync();},
isolateSystem(system){
const sets={all:['silhouette','skeleton','muscles'],organs:['silhouette','organs','respiratory','digestive'],circulation:['silhouette','organs','circulation','respiratory'],respiratory:['silhouette','respiratory'],digestive:['silhouette','digestive'],nervous:['silhouette','nervous'],movement:['silhouette','muscles','skeleton','joints'],'process-oxygen':['silhouette','respiratory','organs','digestive','circulation','muscles'],'process-blood':['silhouette','organs','respiratory','circulation','muscles'],'process-digestion':['silhouette','digestive']};
const enabled=sets[system]||['silhouette',system];for(const k of Object.keys(LayerData))this.state.activeLayers[k]=enabled.includes(k);this.updateLayerVisibility();document.querySelectorAll('[data-nav]').forEach(b=>b.classList.toggle('active',b.dataset.nav===system));$('left-sidebar').classList.remove('open');SettingsModule.save();
},
setView(view){this.state.currentView=view;this.resetZoom();this.renderSvgLayers();if(view==='split'){for(const k of Object.keys(LayerData))this.state.activeLayers[k]=true;this.updateLayerVisibility();}$('view-note').textContent=view==='split'?'Systemer adskilt · afstand og rækkefølge er skematiske':view==='posterior'?'Bagfra · indre organer ses gennem kroppen':'Forfra · kroppens højre side er til venstre på skærmen';},
resetZoom(){this.state.zoomScale=1;this.state.panX=0;this.state.panY=0;$('anatomy-svg').setAttribute('viewBox','130 20 340 890');this.applyTransform();$('region-select').value='body';NavigationModule.breadcrumbs('body');},
applyTransform(){const s=this.state.zoomScale;$('svg-transform-root').setAttribute('transform',`translate(${300*(1-s)+this.state.panX} ${450*(1-s)+this.state.panY}) scale(${s})`);},
handleSearch(q){SearchModule.render(q);},
toggleFavorite(id){const set=new Set(this.state.favorites);set.has(id)?set.delete(id):set.add(id);this.state.favorites=[...set];StorageModule.write('favorites',this.state.favorites);this.updateFavoriteButtonState(id);this.updateFavoritesCount();},
showFavoritesModal(){SearchModule.favorites();},
toggleTeacherMode(){this.toggleOverlay('teacher');},
toggleOverlay(name){const panel=$('overlay-'+name);if(!panel)return;const was=this.activeOverlay===name;ProcessModule.pause();MovementModule.pause();QuizModule.stop();document.querySelectorAll('.overlay-panel').forEach(p=>p.style.display='none');this.activeOverlay=was?null:name;if(name!=='processes'){$('body-particle')?.remove();$('body-process-path')?.remove();}if(was)return;
panel.style.display='flex';$('left-sidebar').classList.remove('open');if(innerWidth>900){$('right-panel').classList.add('open');}else $('right-panel').classList.remove('open');
if(name==='processes')ProcessModule.prepare();if(name==='quiz')QuizModule.start();if(name==='movement')MovementModule.select(MovementModule.mov||MovementData[0]);panel.querySelector('button')?.focus();
},
closeOverlay(name){$('overlay-'+name).style.display='none';$('body-particle')?.remove();$('body-process-path')?.remove();document.querySelectorAll('.process-current,.active-agonist,.active-antagonist').forEach(e=>e.classList.remove('process-current','active-agonist','active-antagonist'));this.activeOverlay=null;ProcessModule.pause();MovementModule.pause();QuizModule.stop();document.querySelector('[data-overlay="'+name+'"]')?.focus();},
demonstrateMovement(mov){MovementModule.select(mov);},
initProcessModule(){ProcessModule.init();},initQuizModule(){QuizModule.init();},
setExertionState(state){this.state.exertionState=state;this.updateExertionUI();$('exertion-description').textContent=PhysiologyData[state].description;SettingsModule.save();}
});
const AppModule={
init(){
for(const k of Object.keys(LayerData)){AppCore.state.activeLayers[k]=['silhouette','skeleton','muscles'].includes(k);AppCore.state.layerOpacity[k]=k==='silhouette'?.5:k==='muscles'?.8:1;}
SettingsModule.load();
$('layers-control').innerHTML=Object.entries(LayerData).map(([id,l])=>`<div class="layer-row"><label class="layer-label" for="layer-check-${id}"><span class="layer-dot" style="background:${l.color}"></span>${l.name}</label><input type="checkbox" id="layer-check-${id}" data-layer="${id}"><input class="layer-opacity-slider" type="range" min="0.1" max="1" step="0.05" data-layer-opacity="${id}" aria-label="Gennemsigtighed: ${l.name}"></div>`).join('');
// Disable inherited mouse-only panning; use pointer events in SVG coordinates.
AppCore.bindEvents();AppCore.renderSvgLayers();AppCore.resetZoom();MovementModule.init();ProcessModule.init();QuizModule.init();TeacherModule.init();
AppCore.updateExertionUI();AppCore.updateFavoritesCount();$('lang-select').value=AppCore.state.langMode;$('exertion-description').textContent=PhysiologyData[AppCore.state.exertionState].description;document.querySelectorAll('[data-state]').forEach(b=>b.classList.toggle('active',b.dataset.state===AppCore.state.exertionState));
$('btn-demo-select').onclick=()=>AppCore.selectStructure('femur');
document.querySelectorAll('input[data-layer],input[data-layer-opacity]').forEach(e=>e.addEventListener('change',()=>SettingsModule.save()));
$('btn-help').onclick=()=>$('help-dialog').showModal();$('help-close').onclick=()=>$('help-dialog').close();
$('panel-close').onclick=()=>$('right-panel').classList.remove('open');$('sidebar-close').onclick=()=>$('left-sidebar').classList.remove('open');
$('btn-fullscreen').onclick=async()=>{try{if(document.fullscreenElement)await document.exitFullscreen();else if(document.documentElement.requestFullscreen)await document.documentElement.requestFullscreen();else throw Error();}catch{$('status-message').textContent='Fuldskærm er ikke tilgængelig her. Brug browserens fuldskærmsfunktion.';}};
$('search-input').addEventListener('keydown',e=>{if(e.key==='ArrowDown'){e.preventDefault();$('search-results').querySelector('button')?.focus();}if(e.key==='Escape')$('search-results').style.display='none';});
document.addEventListener('keydown',e=>{if(e.key==='Escape'){if(AppCore.activeOverlay)AppCore.closeOverlay(AppCore.activeOverlay);$('right-panel').classList.remove('open');$('left-sidebar').classList.remove('open');$('search-results').style.display='none';}});
$('region-select').onchange=e=>{if(e.target.value==='body')AppCore.resetZoom();else NavigationModule.focus(e.target.value);};
const relocate=()=>{document.querySelectorAll('.overlay-panel').forEach(p=>(innerWidth>900?$('right-panel'):$('canvas-container')).append(p));};relocate();addEventListener('resize',relocate);
const svg=$('anatomy-svg');let drag=null;
svg.addEventListener('pointerdown',e=>{if(e.target.closest('.anatomical-part'))return;const pt=new DOMPoint(e.clientX,e.clientY).matrixTransform(svg.getScreenCTM().inverse());drag={x:pt.x,y:pt.y,px:AppCore.state.panX,py:AppCore.state.panY};svg.setPointerCapture(e.pointerId);});
svg.addEventListener('pointermove',e=>{if(!drag)return;const pt=new DOMPoint(e.clientX,e.clientY).matrixTransform(svg.getScreenCTM().inverse());AppCore.state.panX=drag.px+pt.x-drag.x;AppCore.state.panY=drag.py+pt.y-drag.y;AppCore.applyTransform();});svg.addEventListener('pointerup',()=>drag=null);svg.addEventListener('pointercancel',()=>drag=null);
document.querySelectorAll('button[title]').forEach(b=>{if(!b.getAttribute('aria-label'))b.setAttribute('aria-label',b.title);});
document.querySelector('[data-nav="skeleton"] .nav-badge').textContent=Object.keys(AnatomyData.bones).length;document.querySelector('[data-nav="muscles"] .nav-badge').textContent=Object.keys(AnatomyData.muscles).length;
$('structure-count').textContent=AnatomyModule.items().length+' strukturer';
LanguageModule.update();
}
};
// Optional browser agent interface. Unsupported browsers require no polyfill.
const AgentInterfaceModule={
init(){const context=document.modelContext;if(!context?.registerTool)return;const lifecycle=new AbortController();
const tool={name:'select_anatomical_structure',title:'Vælg anatomisk struktur',description:'Select an existing anatomical structure, reveal its system and show its information.',inputSchema:{type:'object',properties:{id:{type:'string'}},required:['id'],additionalProperties:false},annotations:{readOnlyHint:false,untrustedContentHint:false},execute(input){if(!input||typeof input.id!=='string'||!AnatomyModule.get(input.id)?.type)throw new Error('Unknown anatomical structure');AppCore.selectStructure(input.id);return {id:input.id,name:LanguageModule.name(input.id),view:AppCore.state.currentView};}};
try{Promise.resolve(context.registerTool(tool,{signal:lifecycle.signal})).catch(()=>{});}catch{}addEventListener('pagehide',()=>lifecycle.abort(),{once:true});
}
};
document.addEventListener('DOMContentLoaded',()=>{AppModule.init();AgentInterfaceModule.init();});
</script>
</body>
</html>
Dette er Anthropics chatbaserede AI-assistent/LLM.
<!DOCTYPE html>
<html lang="da">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Kroppen i bevægelse – interaktiv anatomi til idræt</title>
<!--
KROPPEN I BEVÆGELSE · prototype v0.1
Interaktivt undervisningsredskab i anatomi og fysiologi til idræt på læreruddannelsen.
Én selvstændig fil: HTML + CSS + JavaScript. Ingen build, ingen afhængigheder, ingen backend.
Kan åbnes direkte i en browser eller kopieres ind i OneCompiler (HTML).
STRUKTUR (kan senere splittes i filer):
<style> -> styles.css
<script> DATA-sektion -> data/structures.js, data/movements.js, data/processes.js (eller JSON)
<script> MODULER -> app.js (eller én fil pr. modul)
Moduler i JavaScript:
Store, Bus, LanguageModule (Lang), AnatomyModule (Anatomy: tegning, lag, valg, zoom, split),
InfoPanel, SearchModule, FavoritesModule, MovementModule, ProcessModule (ilt, blod, mad),
ActivityModule, QuizModule, TeacherModule, Layout, App.
-->
<style>
/* =====================================================================
DESIGN TOKENS
Idé: anatomisk atlas på en idrætshals-bane. Kølig, rolig papirflade,
petroleumsblå tekst, banelinjer som motiv på scenen. Latinske navne i kursiv serif.
===================================================================== */
:root{
--paper:#eef2f3; /* baggrund */
--panel:#fbfcfc; /* paneler */
--ink:#15313a; /* tekst */
--muted:#5b707a; /* sekundær tekst */
--line:#d8e1e4; /* skillelinjer */
--petrol:#0b5d66; /* primær accent */
--court:#2f8f5b; /* banegrøn – korrekt/aktiv */
--courtline:#f2c230; /* banelinjegul – quiz/fremhævning */
--sel:#6d3fd6; /* valgt anatomisk struktur */
--sel-soft:rgba(109,63,214,.14);
--warn:#c2412d;
--stage:#dfe8ea;
--r-lg:16px; --r-md:10px; --r-sm:7px;
--ui:"Avenir Next","Avenir","Segoe UI Variable Text","Segoe UI",system-ui,-apple-system,Roboto,"Helvetica Neue",Arial,sans-serif;
--serif:Charter,"Bitstream Charter","Sitka Text",Cambria,"Iowan Old Style",Georgia,serif;
--fs:1;
--beat:1s; --breath:4.3s;
}
*{box-sizing:border-box}
html,body{height:100%}
body{margin:0;font-family:var(--ui);font-size:calc(15px * var(--fs));line-height:1.45;color:var(--ink);background:var(--paper);overflow:hidden;-webkit-font-smoothing:antialiased}
button,input,select{font:inherit;color:inherit}
button{cursor:pointer}
:focus-visible{outline:3px solid var(--courtline);outline-offset:2px}
.sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
.la{font-family:var(--serif);font-style:italic}
/* ---------- App-ramme ---------- */
.app{display:grid;grid-template-rows:auto 1fr;height:100vh;height:100dvh}
header.top{display:flex;align-items:center;gap:10px;padding:8px 14px;background:var(--panel);border-bottom:1px solid var(--line);position:relative;z-index:40;padding-top:max(8px,env(safe-area-inset-top))}
.brand{display:flex;align-items:center;gap:10px;min-width:0;margin-right:6px}
.brand svg{flex:none}
.brand h1{margin:0;font-family:var(--serif);font-weight:600;font-size:1.15em;letter-spacing:.005em;white-space:nowrap}
.brand small{display:block;font-family:var(--ui);font-weight:400;color:var(--muted);font-size:.72em;letter-spacing:0}
.search{position:relative;flex:1;max-width:440px;margin-left:auto}
.search input{width:100%;padding:8px 12px 8px 34px;border:1px solid var(--line);border-radius:999px;background:#fff}
.search input:focus{border-color:var(--petrol);outline:none;box-shadow:0 0 0 3px rgba(11,93,102,.15)}
.search .ico{position:absolute;left:12px;top:50%;transform:translateY(-50%);color:var(--muted);pointer-events:none}
.results{position:absolute;left:0;right:0;top:calc(100% + 6px);background:#fff;border:1px solid var(--line);border-radius:var(--r-md);box-shadow:0 12px 32px rgba(21,49,58,.14);max-height:60vh;overflow:auto;display:none;z-index:60}
.results.open{display:block}
.results button{display:flex;flex-direction:column;align-items:flex-start;gap:1px;width:100%;text-align:left;border:0;background:none;padding:8px 12px;border-bottom:1px solid #eef2f3}
.results button[aria-selected="true"],.results button:hover{background:#eef6f6}
.results .r1{font-weight:600}
.results .r2{font-size:.82em;color:var(--muted)}
.results .empty{padding:10px 12px;color:var(--muted)}
.tools{display:flex;align-items:center;gap:6px}
.tools select{padding:7px 8px;border:1px solid var(--line);border-radius:var(--r-sm);background:#fff;max-width:150px}
.ibtn{display:inline-grid;place-items:center;min-width:36px;height:36px;padding:0 9px;border:1px solid var(--line);background:#fff;border-radius:var(--r-sm);color:var(--ink)}
.ibtn:hover{border-color:var(--petrol);color:var(--petrol)}
.ibtn[aria-pressed="true"]{background:var(--petrol);color:#fff;border-color:var(--petrol)}
.only-mobile{display:none}
.main{display:grid;grid-template-columns:282px minmax(0,1fr) 370px;min-height:0}
aside{background:var(--panel);overflow:auto;min-height:0;overscroll-behavior:contain}
aside.left{border-right:1px solid var(--line);padding:12px 12px 24px}
aside.right{border-left:1px solid var(--line);display:flex;flex-direction:column}
/* ---------- Venstre panel ---------- */
.sec{margin:4px 0 16px}
.sec h2{font-size:.78em;font-weight:600;color:var(--muted);margin:0 0 6px 4px;letter-spacing:.02em}
nav.modes{display:flex;flex-direction:column;gap:2px}
nav.modes button{display:flex;align-items:center;gap:10px;border:0;background:none;padding:7px 10px;border-radius:var(--r-sm);text-align:left;color:var(--ink)}
nav.modes button:hover{background:#eef4f5}
nav.modes button[aria-current="true"]{background:var(--petrol);color:#fff}
nav.modes .dot{width:10px;height:10px;border-radius:50%;flex:none;box-shadow:inset 0 0 0 1px rgba(0,0,0,.18)}
nav.modes hr{border:0;border-top:1px solid var(--line);margin:6px 4px}
.layers{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:4px}
.layers label{display:flex;align-items:center;gap:6px;padding:5px 6px;min-width:0;white-space:nowrap;overflow:hidden;border:1px solid var(--line);border-radius:var(--r-sm);font-size:.84em;cursor:pointer;background:#fff;user-select:none}
.layers label:has(input:checked){border-color:var(--petrol);background:#eef6f6}
.layers input{accent-color:var(--petrol);margin:0}
.layers .sw{width:9px;height:9px;border-radius:2px;flex:none}
.split{display:grid;grid-template-columns:1fr auto;align-items:center;gap:4px 8px;margin-top:10px}
.split label{grid-column:1/3}
.split input{flex:1;accent-color:var(--petrol)}
.btn{border:1px solid var(--line);background:#fff;padding:7px 12px;border-radius:var(--r-sm);font-weight:500}
.btn:hover{border-color:var(--petrol);color:var(--petrol)}
.btn.primary{background:var(--petrol);border-color:var(--petrol);color:#fff}
.btn.primary:hover{background:#094c54;color:#fff}
.btn:disabled{opacity:.45;cursor:not-allowed}
.seg{display:flex;border:1px solid var(--line);border-radius:var(--r-sm);overflow:hidden;background:#fff}
.seg button{flex:1;border:0;background:none;padding:6px 8px;font-size:.88em}
.seg button+button{border-left:1px solid var(--line)}
.seg button[aria-pressed="true"]{background:var(--petrol);color:#fff}
.vitals{margin-top:8px;display:grid;gap:6px;font-size:.84em}
.vital{display:grid;grid-template-columns:1fr auto;gap:2px 8px;align-items:center}
.vital .bar{grid-column:1/3;height:5px;border-radius:3px;background:#e3eaec;overflow:hidden}
.vital .bar i{display:block;height:100%;background:var(--petrol);transition:width .6s ease}
.vital b{font-weight:600}
.note{font-size:.78em;color:var(--muted);margin:6px 2px 0}
.favs{display:flex;flex-direction:column;gap:3px}
.favs .fav{display:flex;align-items:center;gap:6px;padding:4px 6px 4px 8px;border:1px solid var(--line);border-radius:var(--r-sm);background:#fff;font-size:.9em}
.favs .fav button.go{flex:1;border:0;background:none;text-align:left;padding:0}
.favs .fav button.x{border:0;background:none;color:var(--muted);padding:0 4px}
.check{display:flex;align-items:center;gap:8px;padding:3px 2px;font-size:.9em;cursor:pointer}
.check input{accent-color:var(--petrol)}
.soon{font-size:.8em;color:var(--muted);margin:4px 2px 0;padding-left:14px;border-left:2px dashed var(--line)}
/* ---------- Scene ---------- */
.stage{position:relative;overflow:hidden;background:var(--stage);min-width:0}
.stage::before{ /* banelinjer – idrætshal-motiv */
content:"";position:absolute;inset:0;pointer-events:none;
background:
linear-gradient(90deg,transparent calc(50% - 1px),rgba(255,255,255,.75) calc(50% - 1px),rgba(255,255,255,.75) calc(50% + 1px),transparent calc(50% + 1px)),
radial-gradient(circle at 50% 50%,transparent 129px,rgba(255,255,255,.7) 130px,rgba(255,255,255,.7) 132px,transparent 133px),
linear-gradient(0deg,transparent 18%,rgba(242,194,48,.35) 18%,rgba(242,194,48,.35) calc(18% + 2px),transparent calc(18% + 2px),transparent 82%,rgba(242,194,48,.35) 82%,rgba(242,194,48,.35) calc(82% + 2px),transparent calc(82% + 2px));
}
#body-svg{position:absolute;inset:0;width:100%;height:100%;touch-action:none;user-select:none;-webkit-user-select:none;cursor:grab}
#body-svg.panning{cursor:grabbing}
.overlay{position:absolute;z-index:5;display:flex;gap:6px;align-items:center}
.ov-tl{top:12px;left:12px;right:200px;flex-wrap:wrap}
.ov-tr{top:12px;right:12px;flex-direction:column;align-items:flex-end}
.ov-bl{left:12px;bottom:12px}
.ov-br{right:12px;bottom:12px}
.crumbs{display:flex;flex-wrap:wrap;gap:2px;align-items:center;background:rgba(251,252,252,.92);border:1px solid var(--line);border-radius:999px;padding:3px 6px;font-size:.84em;backdrop-filter:blur(4px)}
.crumbs button{border:0;background:none;padding:3px 7px;border-radius:999px;color:var(--petrol)}
.crumbs button:hover{background:#e6f0f1}
.crumbs button[aria-current="true"]{color:var(--ink);font-weight:600}
.crumbs span{color:var(--muted)}
.toolbar{display:flex;gap:6px;background:rgba(251,252,252,.92);border:1px solid var(--line);border-radius:12px;padding:5px;backdrop-filter:blur(4px)}
.toolbar .seg{border-radius:8px}
.legend{background:rgba(251,252,252,.94);border:1px solid var(--line);border-radius:12px;padding:8px 10px;font-size:.8em;display:none;gap:4px;flex-direction:column}
.legend.show{display:flex}
.legend i{display:inline-block;width:12px;height:12px;border-radius:3px;margin-right:6px;vertical-align:-2px}
.caption{position:absolute;left:50%;bottom:14px;transform:translateX(-50%);width:min(620px,calc(100% - 28px));background:rgba(21,49,58,.93);color:#fff;border-radius:14px;padding:10px 14px;z-index:6;display:none;box-shadow:0 10px 30px rgba(21,49,58,.25)}
.caption.show{display:block}
.caption b{display:block;font-size:1.02em;margin-bottom:2px}
.caption small{opacity:.75}
.caption p{margin:2px 0 0;font-size:.92em;opacity:.95}
.tooltip{position:fixed;z-index:80;pointer-events:none;background:var(--ink);color:#fff;padding:6px 10px;border-radius:8px;font-size:.85em;max-width:260px;opacity:0;transform:translateY(4px);transition:opacity .12s,transform .12s}
.tooltip.show{opacity:1;transform:none}
.tooltip .t2{display:block;opacity:.75;font-size:.9em}
.hint{position:absolute;left:50%;top:56px;transform:translateX(-50%);background:var(--courtline);color:var(--ink);font-weight:600;padding:8px 14px;border-radius:999px;z-index:7;display:none;box-shadow:0 6px 18px rgba(0,0,0,.12);max-width:calc(100% - 24px);text-align:center}
.hint.show{display:block}
/* ---------- Højre panel ---------- */
.tabs{display:flex;border-bottom:1px solid var(--line);position:sticky;top:0;background:var(--panel);z-index:2}
.tabs button{flex:1;border:0;background:none;padding:11px 8px;font-weight:600;color:var(--muted);border-bottom:3px solid transparent}
.tabs button[aria-selected="true"]{color:var(--ink);border-bottom-color:var(--petrol)}
.tabs button[hidden]{display:none}
.tabs .close{flex:none;display:none;padding:0 14px;font-size:1.3em;color:var(--muted)}
.tabpanel{padding:16px 18px 32px;overflow:auto;flex:1}
.tabpanel[hidden]{display:none}
.kicker{display:flex;flex-wrap:wrap;gap:6px;align-items:center;margin-bottom:6px}
.chip{display:inline-flex;align-items:center;gap:5px;border:1px solid var(--line);background:#fff;border-radius:999px;padding:3px 10px;font-size:.82em}
.chip .sw{width:8px;height:8px;border-radius:50%}
button.chip{cursor:pointer}
button.chip:hover{border-color:var(--petrol);color:var(--petrol)}
.info h3{font-family:var(--serif);font-size:1.6em;line-height:1.15;margin:4px 0 2px;font-weight:600}
.info .sub{color:var(--muted);margin:0 0 10px}
.names{width:100%;border-collapse:collapse;margin:8px 0 12px;font-size:.92em}
.names th{text-align:left;font-weight:500;color:var(--muted);width:78px;padding:4px 0;vertical-align:top}
.names td{padding:4px 0;border-bottom:1px solid #edf1f2}
.actions{display:flex;gap:6px;flex-wrap:wrap;margin:6px 0 14px}
.fact{margin:0 0 12px}
.fact h4{margin:0 0 2px;font-size:.8em;font-weight:600;color:var(--petrol)}
.fact p{margin:0}
.chips{display:flex;flex-wrap:wrap;gap:5px}
.muscle-box{display:grid;grid-template-columns:auto 1fr;gap:4px 10px;background:#f3f7f7;border-radius:var(--r-md);padding:10px 12px;margin:0 0 12px;font-size:.92em}
.muscle-box dt{font-weight:600;color:var(--petrol)}
.muscle-box dd{margin:0}
.welcome h3{font-family:var(--serif);font-size:1.45em;margin:0 0 8px;line-height:1.2}
.welcome ol{padding-left:20px;margin:8px 0}
.welcome li{margin:4px 0}
.muted{color:var(--muted)}
.mv-list{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:12px}
.mv-list button[aria-pressed="true"],.proc-list button[aria-pressed="true"]{background:var(--petrol);color:#fff;border-color:var(--petrol)}
.mini{width:100%;height:auto;max-height:260px;background:linear-gradient(#f5f8f9,#e8eff1);border-radius:var(--r-md);border:1px solid var(--line);display:block}
.ctrl{display:flex;align-items:center;gap:8px;margin:10px 0;flex-wrap:wrap}
.ctrl label{display:flex;align-items:center;gap:6px;font-size:.88em;color:var(--muted)}
.ctrl input[type=range]{accent-color:var(--petrol);width:110px}
.phase{font-weight:600;min-height:1.4em;margin:6px 0}
.roles{display:grid;gap:8px;margin:10px 0}
.roles h4{margin:0 0 4px;font-size:.8em;color:var(--muted);font-weight:600}
.roles .sw{width:10px;height:10px;border-radius:3px}
.steps{list-style:none;padding:0;margin:10px 0;display:flex;flex-direction:column;gap:3px;counter-reset:st}
.steps li button{display:flex;gap:10px;align-items:flex-start;width:100%;text-align:left;border:1px solid transparent;background:none;border-radius:var(--r-sm);padding:6px 8px}
.steps li button:hover{background:#eef4f5}
.steps li button[aria-current="true"]{background:#eef6f6;border-color:var(--petrol)}
.steps .n{flex:none;width:22px;height:22px;border-radius:50%;background:#e1eaec;display:grid;place-items:center;font-size:.78em;font-weight:600}
.steps li button[aria-current="true"] .n{background:var(--petrol);color:#fff}
.q-card{background:#fff;border:1px solid var(--line);border-radius:var(--r-lg);padding:14px}
.q-meta{display:flex;justify-content:space-between;font-size:.84em;color:var(--muted);margin-bottom:8px}
.q-prompt{font-family:var(--serif);font-size:1.25em;line-height:1.3;margin:4px 0 12px}
.q-opts{display:grid;gap:6px}
.q-opts button{text-align:left;border:1px solid var(--line);background:#fff;border-radius:var(--r-md);padding:9px 12px}
.q-opts button:hover:not(:disabled){border-color:var(--petrol)}
.q-opts button.ok{background:#e4f3ea;border-color:var(--court)}
.q-opts button.bad{background:#fbe9e5;border-color:var(--warn)}
.q-feedback{margin:10px 0 0;font-weight:600;min-height:1.4em}
.q-feedback.ok{color:var(--court)}
.q-feedback.bad{color:var(--warn)}
.order-answer{display:flex;flex-wrap:wrap;gap:5px;min-height:38px;padding:6px;border:1px dashed var(--line);border-radius:var(--r-md);margin-bottom:8px}
.q-types{display:grid;gap:6px;margin:10px 0}
.q-types label{display:flex;gap:8px;align-items:center;border:1px solid var(--line);border-radius:var(--r-md);padding:8px 10px;background:#fff;cursor:pointer}
.q-types label:has(input:checked){border-color:var(--petrol);background:#eef6f6}
.big-score{font-family:var(--serif);font-size:2.4em;margin:6px 0}
/* ---------- Modal og toast ---------- */
.modal{position:fixed;inset:0;background:rgba(21,49,58,.45);display:none;place-items:center;z-index:100;padding:16px}
.modal.open{display:grid}
.modal .box{background:var(--panel);border-radius:var(--r-lg);max-width:620px;width:100%;max-height:86vh;overflow:auto;padding:20px 22px;box-shadow:0 20px 60px rgba(0,0,0,.25)}
.modal h2{font-family:var(--serif);margin:0 0 10px}
.modal h3{font-size:1em;margin:14px 0 4px;color:var(--petrol)}
.modal kbd{border:1px solid var(--line);border-bottom-width:2px;border-radius:4px;padding:0 5px;background:#fff;font-size:.85em}
.toast{position:fixed;left:50%;bottom:22px;transform:translate(-50%,20px);background:var(--ink);color:#fff;padding:9px 16px;border-radius:999px;opacity:0;transition:.2s;z-index:120;pointer-events:none;font-size:.9em}
.toast.show{opacity:1;transform:translate(-50%,0)}
/* =====================================================================
SVG-KROP
===================================================================== */
.view{display:none}
.view.active{display:inline}
.layer{transition:opacity .25s}
.layer-ghost{opacity:calc(var(--explode,0) * .9);pointer-events:none}
.layer-caption{font-family:var(--ui);font-weight:600;fill:var(--ink);opacity:var(--explode,0);pointer-events:none}
.ghost{pointer-events:none;opacity:calc(1 - var(--explode,0))}
.ghostshape{fill:#f6f8f8;stroke:#b9c8cc;stroke-width:1.2}
.st{cursor:pointer;outline:none}
.deco{pointer-events:none}
.sk{fill:none!important;stroke-linecap:round;stroke-linejoin:round}
.skin{fill:#ead1bd;stroke:#c89f85;stroke-width:1.1}
.boneO{fill:#a9976f;stroke:#a9976f;stroke-width:2.2}
.bone{fill:#f3ecdc}
.bs{stroke:#d8cba8;stroke-width:5}
.hole{fill:#8d7d5e;opacity:.5}
.muscle{fill:#c55a51;stroke:#8c302b;stroke-width:.9}
.mline{fill:none;stroke:#8c302b;stroke-width:.8;opacity:.55}
.tendon{stroke:#ece4cc;stroke-width:6}
.tendonO{stroke:#b9ab86;stroke-width:8}
.joint{fill:rgba(11,93,102,.05);stroke:var(--petrol);stroke-width:1.8;stroke-dasharray:4 3;pointer-events:stroke}
.show-joints .joint{fill:rgba(11,93,102,.2);stroke-width:2.6;pointer-events:all}
.lig{stroke:#3d8a4a;stroke-width:2.6}
.cart{fill:#bfe0ee;stroke:#5f9bb4;stroke-width:.8}
.liver{fill:#8e3b36;stroke:#5e2320;stroke-width:1}
.kidney{fill:#a24a3c;stroke:#6a2b22;stroke-width:1}
.stomach{fill:#e3a08c;stroke:#a8604d;stroke-width:1}
.gut{fill:#e8ad94;stroke:#a8604d;stroke-width:1}
.gutS{stroke:#e0a088;stroke-width:7}
.colon{stroke:#c98a6d;stroke-width:10}
.lung{fill:#eaa1a6;stroke:#b86a72;stroke-width:1}
.airway{fill:#a7cde4;stroke:#5e93b3;stroke-width:1}
.airwayS{stroke:#8bbbd8;stroke-width:6}
.ring{stroke:#5e93b3;stroke-width:.8;fill:none;opacity:.7}
.alv{fill:#f6c7cb;stroke:#b86a72;stroke-width:.6}
.heart{fill:#b8423d;stroke:#7a2521;stroke-width:1}
.heartR{fill:#6b72c9;stroke:#3f4596;stroke-width:.8}
.heartL{fill:#e0504a;stroke:#8f2a26;stroke-width:.8}
.artery{stroke:#d23a36;stroke-width:5}
.arteryT{stroke:#d23a36;stroke-width:3.4}
.vein{stroke:#3e63c4;stroke-width:5}
.brain{fill:#efc3c0;stroke:#b98683;stroke-width:1}
.gyri{fill:none;stroke:#c99592;stroke-width:1;opacity:.8}
.nerve{stroke:#e0b20e;stroke-width:3.2}
.cord{stroke:#e0b20e;stroke-width:4.5}
.nerveT{stroke:#e0b20e;stroke-width:1.4;opacity:.8}
/* Tilstande: hover, fokus, valg */
.st.hov > :not(.deco):not(.sk){filter:brightness(1.07);stroke:var(--petrol);stroke-width:1.8}
.st.hov > .sk{filter:brightness(1.1) saturate(1.2)}
.st.sel > :not(.deco):not(.sk){stroke:var(--sel);stroke-width:2.4}
.st.sel > .boneO{fill:var(--sel);stroke:var(--sel);stroke-width:5}
.st.sel > .bone,.st.sel > .skin,.st.sel > .muscle{filter:brightness(1.06)}
.st.sel > .sk{stroke:var(--sel)!important}
.st.sel-sub > :not(.deco):not(.sk){stroke:var(--sel);stroke-width:1.6;stroke-dasharray:3 2}
.st.sel-sub > .sk{stroke:#a58be6!important}
.st.flash > :not(.deco){animation:flash .9s ease 2}
@keyframes flash{50%{opacity:.35}}
.is-detail{opacity:.9}
/* Bevægelse */
.st.mv-ago > .muscle{fill:#ef5a3c;animation:ago 1.4s ease-in-out infinite}
.st.mv-syn > .muscle{fill:#f0a23a}
.st.mv-ant > .muscle{fill:#7d9bd9;stroke:#2b4ea3;stroke-width:1.6}
.st.mv-joint > .joint{fill:rgba(47,143,91,.3);stroke:var(--court);stroke-width:3;stroke-dasharray:none}
.st.mv-bone > .bone{fill:#fff4c9}
@keyframes ago{50%{filter:brightness(1.25) saturate(1.2)}}
/* Processer */
.st.proc-hl > :not(.deco):not(.sk){stroke:var(--courtline);stroke-width:3}
.st.proc-hl > .sk{filter:drop-shadow(0 0 2px #fff) brightness(1.15)}
.particle{pointer-events:none}
/* Quiz */
.st.quiz-target > :not(.deco):not(.sk){stroke:var(--courtline);stroke-width:3.5;animation:qpulse 1.2s ease-in-out infinite}
.st.quiz-target > .sk{stroke:var(--courtline)!important}
.st.quiz-ok > :not(.deco){stroke:var(--court)!important;stroke-width:3!important}
.st.quiz-bad > :not(.deco){stroke:var(--warn)!important;stroke-width:3!important}
@keyframes qpulse{50%{stroke-opacity:.35}}
/* Fysiologisk tilstand */
.st[data-sid="heart"] > :not(.deco),.st.chamber > *{transform-box:fill-box;transform-origin:center;animation:beat var(--beat) ease-in-out infinite}
.st[data-sid="lungs"] > :not(.deco){transform-box:fill-box;transform-origin:50% 30%;animation:breathe var(--breath) ease-in-out infinite}
@keyframes beat{0%,60%,100%{transform:scale(1)}15%{transform:scale(1.07)}30%{transform:scale(.98)}}
@keyframes breathe{0%,100%{transform:scale(1)}45%{transform:scale(1.035,1.06)}}
.act-moderate .layer-muscles .muscle{fill:#d2544a}
.act-hard .layer-muscles .muscle{fill:#e2443a}
.act-hard .layer-muscles .t-muscle > .muscle{animation:work 1.1s ease-in-out infinite}
@keyframes work{50%{filter:brightness(1.15)}}
.blabel{font:600 7px var(--ui);fill:var(--ink);paint-order:stroke;stroke:#fff;stroke-width:2.4px;pointer-events:none}
.exploded .blabel{display:none}
/* ---------- Storskærm ---------- */
.big{--fs:1.22}
/* ---------- Responsivt ---------- */
@media (max-width:1180px){
.main{grid-template-columns:240px minmax(0,1fr) 330px}
.brand small{display:none}
}
@media (max-width:900px){
.only-mobile{display:inline-grid}
.brand h1 span{display:none}
.main{grid-template-columns:1fr;position:relative}
aside.left{position:fixed;left:0;top:var(--hdr,54px);bottom:0;width:min(300px,86vw);transform:translateX(-104%);transition:transform .25s ease;z-index:50;box-shadow:12px 0 30px rgba(21,49,58,.18)}
aside.left.open{transform:none}
aside.right{position:fixed;left:0;right:0;bottom:0;height:min(62vh,560px);transform:translateY(104%);transition:transform .25s ease;z-index:50;border-left:0;border-top:1px solid var(--line);border-radius:18px 18px 0 0;box-shadow:0 -10px 30px rgba(21,49,58,.18);padding-bottom:env(safe-area-inset-bottom)}
aside.right.open{transform:none}
.tabs{border-radius:18px 18px 0 0}
.tabs .close{display:block}
.ov-tl{right:12px;top:60px}
.ov-tr{top:10px;right:10px;left:10px;flex-direction:row;justify-content:space-between;align-items:flex-start}
.tools select{max-width:112px}
.search{max-width:none}
.caption{bottom:10px}
}
@media (max-width:520px){
header.top{flex-wrap:wrap;gap:8px}
.brand{flex:1}
.search{order:5;flex-basis:100%}
.tools select{max-width:104px}
#btnHelp{display:none}
}
@media (prefers-reduced-motion:reduce){
*,*::before,*::after{animation:none!important;transition:none!important}
}
</style>
</head>
<body>
<div class="app" id="app">
<header class="top" id="hdr">
<button class="ibtn only-mobile" id="btnMenu" aria-label="Åbn navigation" aria-controls="leftPanel" aria-expanded="false">☰</button>
<div class="brand">
<svg width="30" height="30" viewBox="0 0 30 30" aria-hidden="true"><circle cx="15" cy="15" r="14" fill="#0b5d66"/><circle cx="15" cy="8" r="3" fill="#f2c230"/><path d="M15 12v8M10 15l5-2 5 2M12 25l3-5 3 5" stroke="#fff" stroke-width="2" fill="none" stroke-linecap="round"/></svg>
<h1>Kroppen <span>i bevægelse</span><small>Anatomi og fysiologi til idræt</small></h1>
</div>
<div class="search" role="search">
<span class="ico" aria-hidden="true">⌕</span>
<input id="searchInput" type="search" placeholder="Søg: biceps, lårben, femur …" autocomplete="off" aria-label="Søg efter anatomisk struktur på dansk, engelsk eller latin" aria-controls="searchResults" aria-expanded="false" role="combobox">
<div class="results" id="searchResults" role="listbox" aria-label="Søgeresultater"></div>
</div>
<div class="tools">
<label class="sr" for="langSelect">Sprog for anatomiske navne</label>
<select id="langSelect" title="Sprog for anatomiske navne"></select>
<button class="ibtn" id="btnHelp" aria-label="Hjælp" title="Hjælp">?</button>
<button class="ibtn" id="btnFull" aria-label="Fuldskærm" title="Fuldskærm">⤢</button>
<button class="ibtn only-mobile" id="btnInfo" aria-label="Vis informationspanel" aria-controls="rightPanel" aria-expanded="false">ⓘ</button>
</div>
</header>
<div class="main">
<aside class="left" id="leftPanel" aria-label="Navigation og indstillinger">
<div class="sec">
<h2>Udforsk</h2>
<nav class="modes" id="navModes" aria-label="Visninger"></nav>
</div>
<div class="sec">
<h2>Anatomiske lag</h2>
<div class="layers" id="layerToggles"></div>
<div class="split">
<label for="explodeRange" class="muted" style="font-size:.85em">Split kroppen</label>
<input type="range" id="explodeRange" min="0" max="100" value="0" aria-label="Træk lagene fra hinanden">
<button class="btn" id="btnExplode" title="Split eller saml kroppens lag">Split</button>
</div>
</div>
<div class="sec">
<h2>Idrætsfaglig tilstand</h2>
<div class="seg" id="activitySeg" role="group" aria-label="Aktivitetsniveau"></div>
<div class="vitals" id="vitals"></div>
<p class="note">Tallene er illustrative eksempler for en rask voksen – ikke individuelle medicinske målinger.</p>
</div>
<div class="sec">
<h2>Skal læres ⭐</h2>
<div class="favs" id="favList"></div>
</div>
<div class="sec">
<h2>Undervisertilstand</h2>
<label class="check"><input type="checkbox" id="tHide"> Skjul anatomiske navne</label>
<label class="check"><input type="checkbox" id="tLabels"> Vis navne på kroppen</label>
<label class="check"><input type="checkbox" id="tBig"> Storskærm (større tekst)</label>
<p class="soon">Kommende funktioner: vælg bestemte strukturer til quiz, spørgsmål i fuld skærm til holdet, gemte undervisningsforløb.</p>
</div>
</aside>
<main class="stage" id="stage" aria-label="Interaktiv menneskekrop">
<svg id="body-svg" viewBox="0 0 400 780" role="application" aria-label="Menneskekrop. Brug Tab for at gå mellem strukturer og Enter for at vælge." aria-roledescription="anatomisk model"></svg>
<div class="overlay ov-tl"><div class="crumbs" id="crumbs" aria-label="Placering i kroppen"></div></div>
<div class="overlay ov-tr">
<div class="toolbar">
<div class="seg" role="group" aria-label="Visning">
<button id="vFront" aria-pressed="true">Forfra</button>
<button id="vBack" aria-pressed="false">Bagfra</button>
</div>
<button class="ibtn" id="zIn" aria-label="Zoom ind" title="Zoom ind (+)">+</button>
<button class="ibtn" id="zOut" aria-label="Zoom ud" title="Zoom ud (−)">−</button>
<button class="ibtn" id="zReset" aria-label="Vis hele kroppen" title="Hele kroppen (0)">⌂</button>
</div>
<div class="legend" id="legend"></div>
</div>
<div class="hint" id="hint" role="status" aria-live="polite"></div>
<div class="caption" id="caption" aria-live="polite"></div>
</main>
<aside class="right" id="rightPanel" aria-label="Information">
<div class="tabs" role="tablist">
<button role="tab" id="tabInfo" aria-selected="true" aria-controls="panelInfo">Information</button>
<button role="tab" id="tabAct" aria-selected="false" aria-controls="panelAct" hidden>Aktivitet</button>
<button class="close" id="btnCloseRight" aria-label="Luk panel">×</button>
</div>
<section class="tabpanel info" id="panelInfo" role="tabpanel" aria-labelledby="tabInfo"></section>
<section class="tabpanel" id="panelAct" role="tabpanel" aria-labelledby="tabAct" hidden></section>
</aside>
</div>
</div>
<div class="tooltip" id="tooltip" role="tooltip"></div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<div class="modal" id="helpModal" role="dialog" aria-modal="true" aria-labelledby="helpTitle">
<div class="box">
<h2 id="helpTitle">Sådan bruger du Kroppen i bevægelse</h2>
<h3>Udforsk</h3>
<p>Klik på en kropsdel, knogle, muskel eller et organ for at se navn, funktion og idrætsrelevans. Slå anatomiske lag til og fra i venstre side, eller vælg en færdig visning som <i>Skelet</i> eller <i>Muskler</i>.</p>
<h3>Dyk ned i kroppen</h3>
<p>Dobbeltklik på en struktur – eller brug <i>Zoom ind</i> i informationspanelet – for at zoome. Brødkrummestien øverst viser, hvor du er (fx Krop › Ben › Knæ › Knæled). Nogle strukturer har indre dele, som først kan vælges, når den overordnede struktur er valgt: klik fx på hjertet og derefter på et hjertekammer, eller på knæleddet og derefter på korsbåndet.</p>
<h3>Split kroppen</h3>
<p>Tænd for flere lag og træk i skyderen <i>Split kroppen</i> for at se lagene side om side.</p>
<h3>Bevægelse, processer og quiz</h3>
<p><i>Bevægelse</i> viser, hvilke led, knogler og muskler der arbejder. <i>Processer</i> følger ilten, blodet og maden gennem kroppen. <i>Træn din viden</i> indeholder fem quiztyper.</p>
<h3>Tastatur</h3>
<p><kbd>/</kbd> søg · <kbd>Tab</kbd> gå mellem strukturer · <kbd>Enter</kbd> vælg · <kbd>+</kbd>/<kbd>−</kbd> zoom · <kbd>0</kbd> hele kroppen · <kbd>V</kbd> vend forfra/bagfra · <kbd>Esc</kbd> luk.</p>
<h3>Om prototypen</h3>
<p class="muted">Tegningerne er skematiske og ikke anatomisk målfaste. Navne og beskrivelser er fagligt gennemgået, men prototypen erstatter ikke en lærebog. Favoritter og indstillinger gemmes kun i denne browser.</p>
<p style="text-align:right;margin:16px 0 0"><button class="btn primary" id="btnHelpClose">Luk</button></p>
</div>
</div>
<script>
'use strict';
/* =====================================================================
DATA-SEKTION
Alt fagligt indhold ligger her – ikke i brugerfladen.
Kan flyttes til JSON-filer: fjern geometri-hjælperne og gem shapes som
rene arrays [tag, attributter, css-klasse].
===================================================================== */
/* ---------- Geometri-hjælpere (SVG) ----------
Koordinatsystem: viewBox 0 0 400 780, midtlinje x=200.
Parrede strukturer (bilateral:true) tegnes KUN i venstre halvdel af billedet (x<200);
højre halvdel spejles automatisk. Forfra = personens højre side, bagfra = personens venstre side. */
const Geo = {
f: n => Math.round(n * 10) / 10,
cap(x1, y1, x2, y2, w1, w2) {
if (w2 === undefined) w2 = w1;
const f = Geo.f, dx = x2 - x1, dy = y2 - y1, L = Math.hypot(dx, dy) || 1, nx = -dy / L, ny = dx / L, r1 = w1 / 2, r2 = w2 / 2;
return `M${f(x1 + nx * r1)},${f(y1 + ny * r1)} L${f(x2 + nx * r2)},${f(y2 + ny * r2)} A${f(r2)},${f(r2)} 0 0 0 ${f(x2 - nx * r2)},${f(y2 - ny * r2)} L${f(x1 - nx * r1)},${f(y1 - ny * r1)} A${f(r1)},${f(r1)} 0 0 0 ${f(x1 + nx * r1)},${f(y1 + ny * r1)} Z`;
}
};
const P = (d, cls) => ['path', { d }, cls];
const C = (cx, cy, r, cls) => ['circle', { cx, cy, r }, cls];
const E = (cx, cy, rx, ry, cls, rot) => ['ellipse', rot ? { cx, cy, rx, ry, transform: `rotate(${rot} ${cx} ${cy})` } : { cx, cy, rx, ry }, cls];
const K = (x1, y1, x2, y2, w1, w2, cls) => ['path', { d: Geo.cap(x1, y1, x2, y2, w1, w2) }, cls];
const R = (x, y, w, h, rx, cls) => ['rect', { x, y, width: w, height: h, rx }, cls];
/* Knogler: en mørk kontur-kopi under en lys fyldt kopi giver en samlet silhuet */
const bones = (list, extra = []) => [...list.map(s => [s[0], s[1], 'boneO']), ...list.map(s => [s[0], s[1], 'bone']), ...extra];
const boneLine = (x1, y1, x2, y2, w, r1, r2) => [K(x1, y1, x2, y2, w, w), C(x1, y1, r1), C(x2, y2, r2)];
const both = sh => ({ front: sh, back: sh });
/* ---------- Lag (tegnerækkefølge: nederst → øverst) ---------- */
const LAYERS = [
{ id: 'skeleton', da: 'Skelet', color: '#d9ccaa' },
{ id: 'organs', da: 'Organer', color: '#8e3b36' },
{ id: 'digestion', da: 'Fordøjelse', color: '#e0a088' },
{ id: 'respiration', da: 'Åndedræt', color: '#8bbbd8' },
{ id: 'circulation', da: 'Kredsløb', color: '#d23a36' },
{ id: 'nervous', da: 'Nervesystem', color: '#e0b20e' },
{ id: 'muscles', da: 'Muskler', color: '#c55a51' },
{ id: 'skin', da: 'Krop', color: '#ead1bd' }
];
const LAYER_UI_ORDER = ['skin', 'muscles', 'skeleton', 'organs', 'circulation', 'respiration', 'digestion', 'nervous'];
const EXPLODE_ORDER = ['skin', 'muscles', 'skeleton', 'organs', 'digestion', 'respiration', 'circulation', 'nervous'];
const TYPE_LABEL = { region: 'Kropsregion', bone: 'Knogle', joint: 'Led', muscle: 'Muskel', tendon: 'Sene', ligament: 'Ledbånd', cartilage: 'Brusk', organ: 'Organ', organpart: 'Del af organ', vessel: 'Blodkar', airway: 'Luftvej', nerve: 'Nerve' };
const N = (da, en, la) => ({ da, en, la });
const I = (location, fn, movement, sport) => ({ location, function: fn, movement, sport });
/* =====================================================================
ANATOMISKE STRUKTURER
Felter: id, type, layer, names{da,en,la}, parent (hierarki), bilateral,
detail (kan først vælges når forælderen er valgt), shapes{front,back},
info{location,function,movement,sport}, muscle{origin,insertion},
related[], aliases[] (ekstra søgeord), cls (ekstra css-klasse)
===================================================================== */
const STRUCTURES = [
/* ----- KROPSREGIONER (lag: krop) ----- */
// EXTENSION POINT: Add new body regions here
{ id: 'body', type: 'region', layer: 'skin', names: N('Kroppen', 'Human body', 'Corpus humanum'),
info: I('Hele organismen.', 'Kroppen består af samarbejdende organsystemer: bevægeapparatet (skelet, led og muskler), kredsløb, åndedræt, fordøjelse og nervesystem.', 'Al bevægelse opstår, når nervesystemet aktiverer muskler, der trækker i knogler omkring led.', 'Idrætsundervisning handler om kroppen i bevægelse. Forståelse af systemernes samspil gør det lettere at forklare træning, belastning og sundhed.') },
{ id: 'torso', type: 'region', layer: 'skin', parent: 'body', names: N('Trup', 'Trunk (torso)', 'Truncus'),
info: I('Kroppens midterparti fra hals til bækken.', 'Rummer brysthule, bughule og bækkenhule med de indre organer. Rygsøjlen er kroppens bærende akse.', 'Trupmuskulaturen overfører kræfter mellem under- og overkrop og stabiliserer rygsøjlen.', 'En stærk og velkoordineret trup (core) er central i kast, sving, løb og løft.') },
{ id: 'thorax', type: 'region', layer: 'skin', parent: 'torso', names: N('Brystkasse', 'Chest', 'Thorax'),
shapes: { front: [P('M150,136 Q200,128 250,136 L268,146 Q280,166 272,200 L262,258 L138,258 L128,200 Q120,166 132,146 Z', 'skin')] },
info: I('Øverste del af truncus, afgrænset af ribben, brystben og brysthvirvler.', 'Beskytter hjerte og lunger. Brystkassens bevægelser er en del af vejrtrækningen.', 'Ribbenene løftes ved indånding, så brysthulen udvides.', 'Ved hårdt arbejde øges vejrtrækningens dybde og frekvens, og hjælpeåndedrætsmuskler tages i brug.') },
{ id: 'abdomen', type: 'region', layer: 'skin', parent: 'torso', names: N('Mave (bug)', 'Abdomen', 'Abdomen'),
shapes: { front: [P('M138,258 L262,258 Q250,300 253,330 L257,352 L143,352 L147,330 Q150,300 138,258 Z', 'skin')] },
info: I('Mellem brystkasse og bækken.', 'Bughulen rummer bl.a. mavesæk, tarme og lever. Nyrerne ligger bagerst, bag bughinden.', 'Bugmusklerne bøjer og roterer truncus og stabiliserer lænden.', 'Stærk og koordineret bugmuskulatur hjælper ved løft, kast og landinger.') },
{ id: 'pelvic-region', type: 'region', layer: 'skin', parent: 'torso', names: N('Bækkenregion', 'Pelvic region', 'Regio pelvica'),
shapes: { front: [P('M143,352 L257,352 Q264,376 252,394 Q230,404 200,406 Q170,404 148,394 Q136,376 143,352 Z', 'skin')] },
info: I('Nederste del af truncus omkring bækkenet.', 'Forbinder rygsøjle og ben og bærer overkroppens vægt. Rummer bl.a. endetarm og urinblære.', 'Bækkenets hældning påvirker lændens krumning og hofternes bevægelser.', 'Stabilitet omkring bækkenet er vigtig i løb, hop og retningsskift.') },
{ id: 'back', type: 'region', layer: 'skin', parent: 'torso', names: N('Ryg', 'Back', 'Dorsum'),
shapes: { back: [P('M150,136 Q200,128 250,136 L268,146 Q280,166 272,200 L262,258 Q250,300 253,330 L257,352 L143,352 L147,330 Q150,300 138,258 L128,200 Q120,166 132,146 Z', 'skin')] },
info: I('Kroppens bagside fra nakke til bækken.', 'Rummer rygsøjlen, rygmarven og rygmuskulaturen.', 'Rygmusklerne strækker og stabiliserer rygsøjlen og bevæger skulderbladene.', 'God løfteteknik med neutral ryg og stærk rygmuskulatur mindsker risikoen for overbelastning.') },
{ id: 'gluteal-region', type: 'region', layer: 'skin', parent: 'torso', names: N('Sæderegion', 'Gluteal region', 'Regio glutealis'),
shapes: { back: [P('M143,352 L257,352 Q264,376 252,394 Q230,404 200,406 Q170,404 148,394 Q136,376 143,352 Z', 'skin')] },
info: I('Bagsiden af bækkenet.', 'Domineres af sædemusklerne, der ligger over hofteleddet.', 'Ekstension, abduktion og rotation i hoften.', 'Sædemusklerne er kraftcenter ved sprint, hop og løft.') },
{ id: 'neck', type: 'region', layer: 'skin', parent: 'body', names: N('Hals', 'Neck', 'Collum'),
shapes: both([P('M186,94 L214,94 L217,140 L183,140 Z', 'skin')]),
info: I('Mellem hoved og brystkasse.', 'Indeholder halshvirvler, luftrør, spiserør, store blodkar og nerver.', 'Halshvirvelsøjlen giver hovedet stor bevægelighed: fleksion, ekstension, lateralfleksion og rotation.', 'Nakkens muskulatur stabiliserer hovedet, fx ved hovedstød, tacklinger og i kampsport.') },
{ id: 'head', type: 'region', layer: 'skin', parent: 'body', names: N('Hoved', 'Head', 'Caput'),
shapes: both([E(200, 60, 33, 40, 'skin')]),
info: I('Øverst på kroppen, forbundet med truncus via halsen.', 'Rummer hjernen og sanseorganer for syn, hørelse, ligevægt, lugt og smag samt indgangen til luftveje og fordøjelseskanal.', 'Hovedets stilling påvirker balancen. Ligevægtssansen i det indre øre registrerer hovedets bevægelser.', 'Relevant for balance og orientering i rummet (fx gymnastik og boldspil) og for forebyggelse af hovedskader.') },
{ id: 'arm', type: 'region', layer: 'skin', parent: 'body', names: N('Arm', 'Upper limb', 'Membrum superius'),
info: I('Fra skulder til hånd.', 'En kæde af led (skulder, albue, håndled og hånd) til at række, gribe og manipulere.', 'Skulderleddet er kroppens mest bevægelige led. Albuen bøjer og strækker, og underarmen roterer (pronation og supination).', 'Kast, slag, grib, støtte (fx håndstand) og svømmetag.') },
{ id: 'upperarm', type: 'region', layer: 'skin', parent: 'arm', bilateral: true, names: N('Overarm', 'Upper arm', 'Brachium'),
shapes: both([K(130, 172, 116, 280, 30, 24, 'skin')]),
info: I('Mellem skulder og albue.', 'Indeholder overarmsknoglen og musklerne, der bøjer og strækker albuen.', 'Biceps foran bøjer albuen, triceps bagpå strækker den – et klassisk agonist-antagonist-par.', 'Indgår i alle kaste-, støtte- og trækbevægelser.') },
{ id: 'forearm', type: 'region', layer: 'skin', parent: 'arm', bilateral: true, names: N('Underarm', 'Forearm', 'Antebrachium'),
shapes: both([K(114, 296, 100, 398, 24, 17, 'skin')]),
info: I('Mellem albue og håndled.', 'To knogler (spoleben og albueben) og muskler, der bevæger hånd og fingre.', 'Ved pronation og supination drejer spolebenet omkring albuebenet.', 'Gribestyrke og håndledsbevægelser i fx ketsjersport, klatring og redskabsgymnastik.') },
{ id: 'elbow', type: 'region', layer: 'skin', parent: 'arm', bilateral: true, names: N('Albue', 'Elbow', 'Cubitus'),
shapes: both([C(114, 288, 13, 'skin')]),
info: I('Midt på armen mellem overarm og underarm.', 'Rummer albueleddet.', 'Fleksion og ekstension samt rotation af underarmen.', 'Overbelastning kan give fx tennisalbue eller golfalbue (irritation ved senefæsterne).') },
{ id: 'shoulder', type: 'region', layer: 'skin', parent: 'arm', bilateral: true, names: N('Skulder', 'Shoulder', 'Regio deltoidea'),
shapes: both([E(136, 160, 19, 24, 'skin')]),
info: I('Overgangen mellem truncus og arm.', 'Omfatter skulderleddet og skulderbæltet (kraveben og skulderblad).', 'Bevægelse i alle planer: fleksion, ekstension, abduktion, adduktion og rotation.', 'Belastes meget ved kast og overhovedbevægelser, fx i håndbold, volleyball og svømning.') },
{ id: 'hand', type: 'region', layer: 'skin', parent: 'arm', bilateral: true, names: N('Hånd', 'Hand', 'Manus'),
shapes: both([E(96, 430, 13, 27, 'skin', 6), K(91, 410, 81, 430, 9, 7, 'skin')]),
info: I('Yderst på armen.', 'Fint koordinerede bevægelser og et stærkt greb med modstillelig tommelfinger.', 'Håndled og fingre bøjes og strækkes. Tommelfingeren kan føres over mod de andre fingre (opposition).', 'Boldhåndtering, greb om redskaber og ketsjere, støtte i gymnastik.') },
{ id: 'leg', type: 'region', layer: 'skin', parent: 'body', names: N('Ben', 'Lower limb', 'Membrum inferius'),
info: I('Fra hofte til fod.', 'Bærer kroppens vægt og driver os frem. Hofte, knæ og ankel arbejder sammen som en kæde.', 'Samspil mellem hofte-, knæ- og ankelled ved gang, løb, hop og landinger.', 'Benenes muskler er kroppens største muskelmasse og står for det meste af energiforbruget ved løb og cykling.') },
{ id: 'thigh', type: 'region', layer: 'skin', parent: 'leg', bilateral: true, names: N('Lår', 'Thigh', 'Regio femoris'),
shapes: both([K(173, 392, 170, 556, 54, 36, 'skin')]),
info: I('Mellem hofte og knæ.', 'Indeholder lårbenet og kraftige muskelgrupper: forlår (quadriceps), baglår (hamstrings) og adduktorer på indersiden.', 'Forlåret strækker knæet. Baglåret bøjer knæet og strækker hoften.', 'Afgørende for sprint, hop, spark og landinger. Baglårsskader er hyppige i sprintidrætter.') },
{ id: 'lowerleg', type: 'region', layer: 'skin', parent: 'leg', bilateral: true, names: N('Underben', 'Lower leg', 'Crus'),
shapes: both([K(170, 584, 172, 728, 34, 18, 'skin')]),
info: I('Mellem knæ og ankel.', 'To knogler (skinneben og lægben) og muskler, der bevæger fod og ankel.', 'Lægmusklerne plantarflekterer foden, musklerne på forsiden dorsalflekterer.', 'Afsæt og landing i løb og hop. Overbelastning kan give smerter langs skinnebenet.') },
{ id: 'knee', type: 'region', layer: 'skin', parent: 'leg', bilateral: true, names: N('Knæ', 'Knee', 'Genu'),
shapes: both([C(170, 568, 19, 'skin')]),
info: I('Mellem lår og underben.', 'Rummer knæleddet – kroppens største led.', 'Hovedsageligt fleksion og ekstension, let rotation når knæet er bøjet.', 'Et af de hyppigst skadede områder i idræt, fx korsbånds- og meniskskader.') },
{ id: 'foot', type: 'region', layer: 'skin', parent: 'leg', bilateral: true, names: N('Fod', 'Foot', 'Pes'),
shapes: { front: [E(169, 750, 17, 11, 'skin')], back: [E(172, 748, 12, 11, 'skin')] },
info: I('Yderst på benet.', 'Bærer og fordeler kroppens vægt. Fodens buer virker støddæmpende og som fjeder.', 'Plantarfleksion, dorsalfleksion, inversion og eversion.', 'Kontaktflade mod underlaget i næsten al idræt. Ankelforstuvning er en meget hyppig idrætsskade.') },
/* ----- SKELET ----- */
// EXTENSION POINT: Add new bones here
{ id: 'spine', type: 'bone', layer: 'skeleton', parent: 'back', names: N('Rygsøjle', 'Spine (vertebral column)', 'Columna vertebralis'),
shapes: {
front: bones([...Array.from({ length: 22 }, (_, i) => R(194, 98 + i * 11, 12, 9, 2)), P('M190,342 L210,342 L200,378 Z')]),
back: bones([...Array.from({ length: 22 }, (_, i) => R(191, 98 + i * 11, 18, 9, 2)), P('M188,342 L212,342 L200,382 Z')])
},
aliases: ['hvirvler', 'rygrad', 'vertebra', 'ryghvirvel'],
info: I('Midt i kroppens bagside fra kraniet til bækkenet.', 'Bærer kroppen, beskytter rygmarven og giver bevægelighed. Består af 7 hals-, 12 bryst- og 5 lændehvirvler samt korsben og haleben.', 'Fleksion, ekstension, lateralfleksion og rotation fordelt over mange små led. Bruskskiver (disci) mellem hvirvlerne virker støddæmpende.', 'Neutral rygposition ved løft fordeler belastningen. Rygsøjlens naturlige S-form giver fjedring.'),
related: ['spinal-cord', 'pelvis', 'ribs', 'skull'] },
{ id: 'ribs', type: 'bone', layer: 'skeleton', parent: 'thorax', bilateral: true, names: N('Ribben', 'Ribs', 'Costae'),
shapes: {
front: [0, 1, 2, 3, 4, 5, 6].map(i => { const y = 160 + i * 13, xo = [150, 145, 142, 141, 142, 145, 150][i]; return P(`M196,${y} C176,${y - 5} ${xo},${y + 2} ${xo + 2},${y + 22}`, 'bs'); }),
back: [0, 1, 2, 3, 4, 5, 6, 7].map(i => { const y = 150 + i * 13, xo = [152, 146, 142, 140, 140, 142, 146, 152][i]; return P(`M192,${y} C174,${y - 2} ${xo},${y + 4} ${xo + 2},${y + 22}`, 'bs'); })
},
aliases: ['ribbensbue', 'brystkasse'],
info: I('Brystkassen. 12 par, der udgår fra brysthvirvlerne.', 'Beskytter hjerte og lunger og deltager i vejrtrækningen. 7 par når brystbenet direkte, 3 par indirekte, og 2 par ender frit.', 'Ribbenene løftes ved indånding, bl.a. af de udvendige mellemribbensmuskler, så brystkassen udvides.', 'Ribbensbrud og -kontusioner kan opstå ved stød i kontaktsport.'),
related: ['sternum', 'spine', 'lungs', 'diaphragm'] },
{ id: 'sternum', type: 'bone', layer: 'skeleton', parent: 'thorax', names: N('Brystben', 'Breastbone', 'Sternum'),
shapes: { front: bones([P('M194,148 L206,148 L207,226 L200,238 L193,226 Z')]) },
info: I('Midt foran på brystkassen.', 'Forankrer ribbenene via ribbensbrusk og forbinder til kravebenene. Beskytter hjertet.', 'Følger brystkassens løft ved dyb indånding.', 'Ved hjertelungeredning (HLR) trykkes på den nederste halvdel af brystbenet.'),
related: ['ribs', 'clavicle', 'heart', 'pectoralis-major'] },
{ id: 'clavicle', type: 'bone', layer: 'skeleton', parent: 'shoulder', bilateral: true, names: N('Kraveben', 'Collarbone', 'Clavicula'),
shapes: both([P('M195,140 Q168,132 144,142', 'bs')]),
info: I('Vandret mellem brystbenet og skulderbladets yderste spids (acromion).', 'Holder skulderen ude fra kroppen og overfører kræfter fra arm til truncus.', 'Bevæger sig med skulderbladet, når armen løftes.', 'En af de hyppigst brækkede knogler ved fald på skulderen eller strakt arm, fx i cykling og ridning.'),
related: ['scapula', 'sternum', 'deltoid', 'trapezius'] },
{ id: 'scapula', type: 'bone', layer: 'skeleton', parent: 'shoulder', bilateral: true, names: N('Skulderblad', 'Shoulder blade', 'Scapula'),
shapes: { back: bones([P('M178,150 Q160,145 139,152 Q146,186 155,215 Q168,196 178,150 Z')], [P('M176,162 Q158,160 142,158', 'mline deco')]) },
info: I('Bag på brystkassen ud for ca. 2.–7. ribben.', 'Danner ledskålen i skulderleddet og er hæftested for mange muskler, bl.a. rotatormanchetten.', 'Glider på brystkassen (elevation, depression, protraktion, retraktion og rotation) og følger armen – den skapulohumerale rytme.', 'God skulderbladskontrol er vigtig ved kast, armstrækninger og håndstand.'),
related: ['clavicle', 'humerus', 'trapezius', 'shoulder-joint'] },
{ id: 'pelvis', type: 'bone', layer: 'skeleton', parent: 'pelvic-region', names: N('Bækken', 'Pelvis', 'Pelvis'),
shapes: both(bones([P('M146,338 Q140,366 158,384 Q172,394 186,390 Q194,398 200,398 Q206,398 214,390 Q228,394 242,384 Q260,366 254,338 Q236,350 222,352 Q210,360 200,360 Q190,360 178,352 Q164,350 146,338 Z')], [E(184, 381, 6, 5, 'hole deco'), E(216, 381, 6, 5, 'hole deco')])),
aliases: ['hofteben', 'os coxae'],
info: I('Nederst på truncus.', 'Består af de to hofteben (os coxae) og korsbenet. Overfører vægt fra rygsøjlen til benene og beskytter bækkenorganerne.', 'Ledskålen (acetabulum) danner hofteleddet med lårbenshovedet.', 'Mange store muskler udspringer fra bækkenet, fx sædemuskler, hamstrings og rectus femoris.'),
related: ['hip-joint', 'femur', 'spine', 'gluteus-maximus'] },
{ id: 'humerus', type: 'bone', layer: 'skeleton', parent: 'upperarm', bilateral: true, names: N('Overarmsknogle', 'Upper arm bone', 'Humerus'),
shapes: both(bones(boneLine(138, 160, 118, 282, 9, 10, 9))),
info: I('Overarmen, mellem skulder og albue.', 'Lang rørknogle. Hovedet danner skulderleddet, den nederste ende albueleddet.', 'Vægtstang for bl.a. deltoideus, pectoralis major og latissimus dorsi.', 'Ved kast sker en hurtig rotation omkring overarmsknoglens længdeakse.'),
related: ['shoulder-joint', 'elbow-joint', 'deltoid', 'biceps', 'triceps'] },
{ id: 'radius', type: 'bone', layer: 'skeleton', parent: 'forearm', bilateral: true, names: N('Spoleben', 'Radius', 'Radius'),
shapes: both(bones(boneLine(108, 296, 94, 396, 5, 5, 6))),
info: I('Underarmens tommelfingerside (lateralt).', 'Fører det meste af belastningen fra hånden videre mod albuen.', 'Drejer omkring albuebenet ved pronation og supination. Hæfte for biceps brachii.', 'Brud nær håndleddet er almindeligt ved fald på strakt arm, fx på skøjter eller rulleskøjter.'),
related: ['ulna', 'biceps', 'elbow-joint'] },
{ id: 'ulna', type: 'bone', layer: 'skeleton', parent: 'forearm', bilateral: true, names: N('Albueben', 'Ulna', 'Ulna'),
shapes: both(bones(boneLine(120, 292, 107, 398, 5, 7, 4))),
info: I('Underarmens lillefingerside (medialt).', 'Danner sammen med overarmsknoglen hængselleddet i albuen. Olecranon er den albuespids, man kan mærke.', 'Hæfte for triceps brachii på olecranon.', 'Stabil base for underarmen ved støtte og slag.'),
related: ['radius', 'triceps', 'elbow-joint'] },
{ id: 'hand-bones', type: 'bone', layer: 'skeleton', parent: 'hand', bilateral: true, names: N('Håndens knogler', 'Bones of the hand', 'Ossa manus'),
shapes: both([E(98, 410, 10, 7, 'boneO'), E(98, 410, 10, 7, 'bone'),
P('M92,418 L87,441 L85,458 M96,419 L94,444 L93,463 M100,419 L101,444 L102,462 M104,418 L107,440 L109,454 M90,414 L81,428 L76,441', 'bs')]),
aliases: ['fingre', 'håndrod', 'phalanges'],
info: I('Hånden.', '27 knogler: 8 håndrodsknogler, 5 mellemhåndsknogler og 14 fingerknogler.', 'Mange små led giver fin motorik og et stærkt greb.', 'Fingerskader og forstuvninger ses ofte i boldspil som volleyball og basketball.'),
related: ['radius', 'ulna'] },
{ id: 'femur', type: 'bone', layer: 'skeleton', parent: 'thigh', bilateral: true, names: N('Lårben', 'Thigh bone', 'Femur'),
shapes: both(bones([C(184, 380, 9), K(184, 380, 165, 396, 8, 8), C(160, 392, 7), K(165, 396, 168, 552, 11, 10), E(168, 556, 15, 8)])),
aliases: ['lårbensknogle'],
info: I('Låret, mellem hofte og knæ.', 'Kroppens længste og stærkeste knogle. Danner hofteleddet foroven og knæleddet forneden.', 'Vægtstang for kraftige muskler som quadriceps, hamstrings og gluteus maximus.', 'Tåler store belastninger ved løb, hop og landinger. Knoglevæv styrkes af belastning.'),
related: ['hip-joint', 'knee-joint', 'quadriceps', 'hamstrings', 'patella'] },
{ id: 'tibia', type: 'bone', layer: 'skeleton', parent: 'lowerleg', bilateral: true, names: N('Skinneben', 'Shinbone', 'Tibia'),
shapes: both(bones([E(172, 580, 16, 6), K(174, 584, 175, 724, 11, 8), C(179, 728, 5)])),
info: I('Underbenets inderside (medialt).', 'Bærer størstedelen af kropsvægten fra knæ til ankel.', 'Danner knæleddet med lårbenet og ankelleddet med talus. Den indvendige ankelknude er en del af skinnebenet.', 'Overbelastning ved meget løb kan give smerter langs skinnebenets inderside.'),
related: ['fibula', 'knee-joint', 'ankle-joint', 'tibialis-anterior', 'patellar-ligament'] },
{ id: 'fibula', type: 'bone', layer: 'skeleton', parent: 'lowerleg', bilateral: true, names: N('Lægben', 'Calf bone', 'Fibula'),
shapes: both(bones([C(156, 586, 5), K(156, 588, 160, 724, 5, 5), C(160, 729, 5)])),
info: I('Underbenets yderside (lateralt).', 'Tynd knogle, der kun bærer lidt vægt, men stabiliserer ankelleddet og er hæfte for muskler.', 'Den udvendige ankelknude er den nederste ende af lægbenet.', 'Ledbåndene på ankelens yderside, som hæfter på lægbenet, er dem, der oftest forstuves.'),
related: ['tibia', 'ankle-joint'] },
{ id: 'patella', type: 'bone', layer: 'skeleton', parent: 'knee-joint', bilateral: true, names: N('Knæskal', 'Kneecap', 'Patella'),
shapes: { front: bones([E(169, 566, 8, 10)]) },
info: I('Foran på knæet, indlejret i quadricepssenen.', 'Kroppens største sesamknogle. Beskytter knæet og øger quadriceps\' vægtstangsarm.', 'Glider i en rende på lårbenet, når knæet bøjes og strækkes.', 'Smerter omkring knæskallen er almindelige hos unge idrætsudøvere.'),
related: ['quadriceps', 'patellar-ligament', 'femur'] },
{ id: 'foot-bones', type: 'bone', layer: 'skeleton', parent: 'foot', bilateral: true, names: N('Fodens knogler', 'Bones of the foot', 'Ossa pedis'),
shapes: { front: [E(168, 746, 14, 7, 'boneO'), E(168, 746, 14, 7, 'bone'), P('M158,750 L155,762 M164,752 L162,765 M170,752 L170,766 M176,751 L178,764 M181,749 L184,760', 'bs')],
back: bones([E(172, 750, 9, 9)]) },
aliases: ['hælben', 'calcaneus', 'tæer', 'mellemfod'],
info: I('Foden.', '26 knogler: 7 fodrodsknogler (bl.a. hælben og talus), 5 mellemfodsknogler og 14 tåknogler.', 'Danner fodens længde- og tværbuer, der støddæmper og fjedrer.', 'Træthedsbrud i mellemfodsknoglerne kan opstå ved for hurtig stigning i løbemængde.'),
related: ['ankle-joint', 'achilles', 'tibia'] },
{ id: 'skull', type: 'bone', layer: 'skeleton', parent: 'head', names: N('Kranium', 'Skull', 'Cranium'),
shapes: {
front: bones([E(200, 54, 29, 34), P('M176,68 Q178,98 200,100 Q222,98 224,68 Z')], [E(188, 56, 7, 6, 'hole deco'), E(212, 56, 7, 6, 'hole deco'), P('M200,64 L196,74 L204,74 Z', 'hole deco'), P('M188,88 L212,88', 'mline deco')]),
back: bones([E(200, 54, 30, 36)])
},
aliases: ['hovedskal', 'kraniet', 'underkæbe'],
info: I('Hovedets knoglestruktur.', 'Beskytter hjernen og sanseorganerne og danner ansigtets skelet. Består af mange knogler, der er vokset sammen i sømme. Underkæben er bevægelig.', 'Underkæben bevæges i kæbeleddet ved tygning og tale.', 'Hjelme og regler beskytter mod hovedskader, fx i cykling, skiløb og kontaktsport.'),
related: ['brain', 'spine'] },
/* Led */
// EXTENSION POINT: Add new joints here
{ id: 'shoulder-joint', type: 'joint', layer: 'skeleton', parent: 'shoulder', bilateral: true, names: N('Skulderled', 'Shoulder joint', 'Articulatio humeri'),
shapes: both([C(138, 160, 13, 'joint')]),
info: I('Mellem overarmsknoglens hoved og skulderbladets ledskål.', 'Kugleled med stor bevægelighed men lille knoglestabilitet. Stabiliseres især af rotatormanchettens muskler.', 'Fleksion, ekstension, abduktion, adduktion samt indad- og udadrotation.', 'Skulderluksation og overbelastning ses ved kast og i kontaktsport.'),
related: ['humerus', 'scapula', 'deltoid'] },
{ id: 'elbow-joint', type: 'joint', layer: 'skeleton', parent: 'elbow', bilateral: true, names: N('Albueled', 'Elbow joint', 'Articulatio cubiti'),
shapes: both([C(114, 290, 12, 'joint')]),
info: I('Mellem overarmsknoglen, albuebenet og spolebenet.', 'Sammensat led: et hængselled mellem humerus og ulna og et drejeled mellem radius og ulna.', 'Fleksion og ekstension samt pronation og supination.', 'Kasteidrætter belaster især albuens inderside.'),
related: ['humerus', 'radius', 'ulna', 'biceps', 'triceps'] },
{ id: 'hip-joint', type: 'joint', layer: 'skeleton', parent: 'pelvic-region', bilateral: true, names: N('Hofteled', 'Hip joint', 'Articulatio coxae'),
shapes: both([C(183, 380, 12, 'joint')]),
info: I('Mellem lårbenshovedet og bækkenets ledskål (acetabulum).', 'Kugleled med dyb ledskål – stabilt og vægtbærende.', 'Fleksion, ekstension, abduktion, adduktion og rotation.', 'Hoftens bevægelighed og styrke påvirker løbeteknik, spark og squat.'),
related: ['femur', 'pelvis', 'gluteus-maximus'] },
{ id: 'knee-joint', type: 'joint', layer: 'skeleton', parent: 'knee', bilateral: true, names: N('Knæled', 'Knee joint', 'Articulatio genus'),
shapes: both([C(169, 568, 17, 'joint')]),
aliases: ['knæleddet'],
info: I('Mellem lårbenet, skinnebenet og knæskallen.', 'Kroppens største led. Et modificeret hængselled, der stabiliseres af korsbånd, sideledbånd og menisker.', 'Fleksion og ekstension, let rotation i bøjet stilling.', 'Knæskader (korsbånd, menisk) opstår typisk ved vrid og landinger. God landingsteknik er forebyggende.'),
related: ['femur', 'tibia', 'patella', 'quadriceps', 'hamstrings'] },
{ id: 'ankle-joint', type: 'joint', layer: 'skeleton', parent: 'foot', bilateral: true, names: N('Øvre ankelled', 'Ankle joint', 'Articulatio talocruralis'),
shapes: both([C(172, 728, 10, 'joint')]),
aliases: ['ankel', 'ankelled'],
info: I('Mellem skinneben, lægben og talus.', 'Hængselled, der stabiliseres af ledbånd på inder- og yderside.', 'Dorsalfleksion og plantarfleksion.', 'Ankelforstuvninger er blandt de hyppigste idrætsskader. Balancetræning virker forebyggende.'),
related: ['tibia', 'fibula', 'foot-bones', 'gastrocnemius'] },
/* Knæets indre dele (detail: vælges når knæleddet er valgt) */
// EXTENSION POINT: Add tendons and ligaments here
{ id: 'menisci', type: 'cartilage', layer: 'skeleton', parent: 'knee-joint', bilateral: true, detail: true, names: N('Menisker', 'Menisci', 'Meniscus medialis et lateralis'),
shapes: { front: [E(161, 576, 7, 2.6, 'cart'), E(179, 576, 7, 2.6, 'cart')] },
aliases: ['menisk'],
info: I('Mellem lårben og skinneben i knæleddet.', 'To halvmåneformede bruskskiver, der fordeler trykket og stabiliserer knæet.', 'Følger ledfladerne ved fleksion og rotation.', 'Kan rives over ved vrid på et belastet, bøjet knæ.'),
related: ['knee-joint', 'acl'] },
{ id: 'acl', type: 'ligament', layer: 'skeleton', parent: 'knee-joint', bilateral: true, detail: true, names: N('Forreste korsbånd', 'Anterior cruciate ligament', 'Ligamentum cruciatum anterius'),
shapes: { front: [P('M163,561 L177,578', 'lig')] },
aliases: ['korsbånd', 'ACL'],
info: I('Inde i knæleddet, fra skinnebenet op til lårbenets laterale kondyl.', 'Forhindrer at skinnebenet glider frem i forhold til lårbenet og begrænser rotation.', 'Stabiliserer især ved strakt knæ og ved rotation.', 'Korsbåndsskader sker ofte ved retningsskift og landinger, fx i fodbold og håndbold. Forebyggende træning kan mindske risikoen.'),
related: ['knee-joint', 'menisci', 'mcl'] },
{ id: 'mcl', type: 'ligament', layer: 'skeleton', parent: 'knee-joint', bilateral: true, detail: true, names: N('Indre sideledbånd', 'Medial collateral ligament', 'Ligamentum collaterale tibiale'),
shapes: { front: [P('M185,552 L187,592', 'lig')] },
aliases: ['sideledbånd', 'MCL'],
info: I('Knæets inderside, fra lårbenet til skinnebenet.', 'Modvirker at knæet presses indad (valgus).', 'Stabiliserer knæet i sideretningen.', 'Skades ved slag mod knæets yderside, fx ved tacklinger.'),
related: ['knee-joint', 'acl'] },
{ id: 'patellar-ligament', type: 'ligament', layer: 'skeleton', parent: 'knee-joint', bilateral: true, names: N('Knæskalssene', 'Patellar ligament', 'Ligamentum patellae'),
shapes: { front: [P('M170,576 L172,593', 'tendonO'), P('M170,576 L172,593', 'tendon')] },
aliases: ['patellasene', 'springerknæ'],
info: I('Fra knæskallens spids til skinnebenets knold (tuberositas tibiae).', 'Fortsættelsen af quadricepssenen. Overfører quadriceps\' kraft til skinnebenet.', 'Afgørende for ekstension af knæet.', 'Mange hop kan overbelaste senen og give springerknæ.'),
related: ['patella', 'quadriceps', 'tibia'] },
/* ----- ORGANER (øvrige) ----- */
// EXTENSION POINT: Add new organs here
{ id: 'liver', type: 'organ', layer: 'organs', parent: 'abdomen', names: N('Lever', 'Liver', 'Hepar'),
shapes: { front: [P('M146,256 Q168,236 222,244 Q236,248 228,260 Q200,278 160,284 Q144,274 146,256 Z', 'liver')] },
info: I('Øverst til højre i bughulen under mellemgulvet.', 'Kroppens største indre organ. Lagrer glykogen, danner galde, omdanner næringsstoffer og nedbryder affaldsstoffer.', 'Frigiver glukose til blodet under arbejde.', 'Leverens glykogen hjælper med at holde blodsukkeret stabilt under langvarig aktivitet. Laktat kan omdannes til glukose her.'),
related: ['small-intestine', 'stomach'] },
{ id: 'kidneys', type: 'organ', layer: 'organs', parent: 'abdomen', bilateral: true, names: N('Nyrer', 'Kidneys', 'Renes'),
shapes: { back: [E(176, 302, 11, 18, 'kidney', -12)] },
aliases: ['nyre'],
info: I('Bagerst i bughulen på hver side af rygsøjlen, delvist beskyttet af de nederste ribben.', 'Filtrerer blodet, danner urin og regulerer væske-, salt- og syre-base-balancen.', 'Blodgennemstrømningen til nyrerne falder under hårdt arbejde.', 'Væskeindtag før, under og efter træning støtter kroppens væskebalance.'),
related: ['aorta', 'spine'] },
/* ----- FORDØJELSE ----- */
{ id: 'mouth', type: 'organ', layer: 'digestion', parent: 'head', names: N('Mundhule', 'Oral cavity', 'Cavitas oris'),
shapes: { front: [E(200, 82, 10, 4, 'gut')] },
aliases: ['mund'],
info: I('Indgangen til fordøjelseskanalen.', 'Tygning sønderdeler maden, og spyt med enzymet amylase begynder nedbrydningen af stivelse.', 'Tygge- og tungemuskler bearbejder maden.', 'Væske og kulhydrat indtages gennem munden under længerevarende aktivitet.'),
related: ['pharynx', 'esophagus'] },
{ id: 'esophagus', type: 'organ', layer: 'digestion', parent: 'thorax', names: N('Spiserør', 'Esophagus', 'Oesophagus'),
shapes: { front: [P('M201,110 L203,248 Q205,258 213,260', 'gutS')] },
info: I('Fra svælget gennem brysthulen og mellemgulvet til mavesækken.', 'Transporterer maden til mavesækken.', 'Bølgeformede muskelsammentrækninger (peristaltik) fører maden frem – også hvis man står på hovedet.', 'Et stort måltid lige før træning kan give ubehag.'),
related: ['pharynx', 'stomach', 'diaphragm'] },
{ id: 'stomach', type: 'organ', layer: 'digestion', parent: 'abdomen', names: N('Mavesæk', 'Stomach', 'Gaster'),
shapes: { front: [P('M210,254 Q236,244 245,262 Q249,292 228,302 Q210,306 204,292 Q214,282 210,254 Z', 'stomach')] },
aliases: ['ventriculus', 'maven'],
info: I('Øverst i bughulen, mest til venstre under mellemgulvet.', 'Opbevarer maden og blander den med saltsyre og enzymet pepsin, så proteinfordøjelsen begynder.', 'Muskelvæggen blander indholdet og tømmer det gradvist til tyndtarmen.', 'Blodgennemstrømningen til fordøjelsesorganerne falder under hårdt arbejde, så store måltider tåles dårligt lige før.'),
related: ['esophagus', 'small-intestine', 'liver'] },
{ id: 'large-intestine', type: 'organ', layer: 'digestion', parent: 'abdomen', names: N('Tyktarm', 'Large intestine', 'Intestinum crassum'),
shapes: { front: [P('M198,362 L168,360 Q160,340 162,306 Q164,290 178,290 L224,290 Q238,290 238,304 L236,348 Q234,366 214,370 L204,372', 'colon')] },
aliases: ['colon'],
info: I('Som en ramme omkring tyndtarmen.', 'Optager vand og salte. Tarmbakterier nedbryder kostfibre.', 'Langsom transport og formning af affaldsstoffer.', 'Væskebalancen har betydning for præstation og temperaturregulering.'),
related: ['small-intestine', 'rectum'] },
{ id: 'small-intestine', type: 'organ', layer: 'digestion', parent: 'abdomen', names: N('Tyndtarm', 'Small intestine', 'Intestinum tenue'),
shapes: { front: [P('M206,298 C186,296 178,306 190,312 C206,318 226,312 222,322 C218,332 186,326 180,336 C176,346 204,348 216,340 C226,334 224,352 210,356', 'gutS')] },
info: I('Midt i bughulen fra mavesækken til tyktarmen.', 'Her sker det meste af fordøjelsen og optagelsen af næringsstoffer. Tarmtotter (villi) giver en meget stor overflade.', 'Peristaltik fører indholdet videre.', 'Kulhydrater optages her og lagres som glykogen i muskler og lever – vigtigt brændstof ved hård træning.'),
related: ['stomach', 'large-intestine', 'liver'] },
{ id: 'rectum', type: 'organ', layer: 'digestion', parent: 'pelvic-region', names: N('Endetarm', 'Rectum', 'Rectum'),
shapes: { front: [K(203, 372, 201, 398, 9, 8, 'gut')] },
info: I('Sidste del af tyktarmen, i bækkenet.', 'Opsamler affaldsstoffer før tømning.', 'Tømningen styres af lukkemuskler.', 'Bækkenbundens muskler støtter bækkenorganerne og aktiveres fx ved tunge løft.'),
related: ['large-intestine', 'pelvis'] },
/* ----- ÅNDEDRÆT ----- */
{ id: 'lungs', type: 'organ', layer: 'respiration', parent: 'thorax', bilateral: true, names: N('Lunger', 'Lungs', 'Pulmones'),
shapes: { front: [P('M190,158 Q170,148 152,166 Q139,200 143,252 Q168,258 192,246 Q197,200 190,158 Z', 'lung')], back: [P('M192,152 Q170,144 150,162 Q137,200 141,262 Q168,268 194,256 Q199,200 192,152 Z', 'lung')] },
aliases: ['lunge'],
info: I('I brysthulen på hver side af hjertet.', 'Udveksler gasser: ilt optages i blodet, og kuldioxid afgives. Højre lunge har 3 lapper, venstre 2.', 'Følger brystkassens og mellemgulvets bevægelser.', 'Ventilationen kan stige fra ca. 6–8 L/min i hvile til over 100 L/min ved maksimalt arbejde (illustrative værdier).'),
related: ['bronchi', 'alveoli', 'diaphragm', 'heart'] },
{ id: 'alveoli', type: 'organpart', layer: 'respiration', parent: 'lungs', bilateral: true, detail: true, names: N('Alveoler (lungeblærer)', 'Alveoli', 'Alveoli pulmonis'),
shapes: { front: [C(164, 228, 3.6, 'alv'), C(170, 232, 3.4, 'alv'), C(163, 236, 3.2, 'alv'), C(169, 239, 3, 'alv'), C(172, 225, 3, 'alv'), C(158, 232, 3, 'alv')] },
aliases: ['lungeblærer', 'lungeblære'],
info: I('For enden af de mindste luftveje i lungerne.', 'Flere hundrede millioner små luftsække med meget tynd væg og stor samlet overflade, omgivet af kapillærer.', 'Ilt diffunderer fra alveolen til blodet, kuldioxid den modsatte vej.', 'Den store overflade gør det muligt at optage langt mere ilt under hårdt arbejde.'),
related: ['lungs', 'bronchi'] },
{ id: 'pharynx', type: 'airway', layer: 'respiration', parent: 'neck', names: N('Svælg', 'Throat', 'Pharynx'),
shapes: { front: [K(200, 72, 200, 110, 9, 8, 'airway')] },
info: I('Bag næse- og mundhulen, ned til strubehoved og spiserør.', 'Fælles passage for luft og mad.', 'Ved synkning lukker strubelåget (epiglottis) luftvejen.', 'Ved hårdt arbejde trækkes luft ind gennem både næse og mund for at øge luftstrømmen.'),
related: ['trachea', 'esophagus', 'mouth'] },
{ id: 'trachea', type: 'airway', layer: 'respiration', parent: 'neck', names: N('Luftrør', 'Windpipe', 'Trachea'),
shapes: { front: [K(200, 112, 200, 176, 9, 9, 'airway'), P('M196,122 H204 M196,132 H204 M196,142 H204 M196,152 H204 M196,162 H204', 'ring deco')] },
info: I('Fra strubehovedet ned i brysthulen foran spiserøret.', 'Leder luft til og fra lungerne og holdes åbent af C-formede bruskringe.', 'Slimhinde med fimrehår transporterer partikler op og væk.', 'Kold, tør luft kan irritere luftvejene – relevant ved anstrengelsesudløst astma.'),
related: ['pharynx', 'bronchi'] },
{ id: 'bronchi', type: 'airway', layer: 'respiration', parent: 'thorax', names: N('Bronkier', 'Bronchi', 'Bronchi'),
shapes: { front: [P('M200,176 Q192,184 180,196 M200,176 Q208,184 220,196', 'airwayS'), P('M184,192 L174,204 M184,192 L182,212 M216,192 L226,204 M216,192 L218,212', 'ring deco')] },
aliases: ['bronkie', 'bronkioler'],
info: I('Luftrøret deler sig i højre og venstre hovedbronkie ud for ca. 4.–5. brysthvirvel.', 'Forgrener sig som et træ i stadig mindre luftveje (bronkioler) ud til alveolerne.', 'Glat muskulatur i væggen kan ændre luftvejenes diameter.', 'Ved anstrengelsesudløst astma trækker muskulaturen i de små luftveje sig sammen.'),
related: ['trachea', 'lungs', 'alveoli'] },
{ id: 'diaphragm', type: 'muscle', layer: 'respiration', parent: 'thorax', names: N('Mellemgulv', 'Diaphragm', 'Diaphragma'),
shapes: { front: [P('M140,258 Q168,224 200,240 Q232,224 260,258 Q232,246 200,252 Q168,246 140,258 Z', 'muscle')] },
muscle: { origin: 'Brystbenets nederste spids (processus xiphoideus), de nederste 6 ribben og lændehvirvlerne', insertion: 'Mellemgulvets senede midte (centrum tendineum)' },
info: I('Kuppelformet muskelplade mellem brysthule og bughule.', 'Den vigtigste indåndingsmuskel.', 'Når musklen trækker sig sammen, flader kuplen ud, brysthulen udvides, og luft strømmer ind. Udånding i hvile er passiv.', 'Ved hårdt arbejde hjælper flere muskler til, og udåndingen bliver aktiv, bl.a. med bugmusklerne.'),
related: ['lungs', 'ribs', 'rectus-abdominis'] },
/* ----- KREDSLØB ----- */
{ id: 'venae-cavae', type: 'vessel', layer: 'circulation', parent: 'torso', names: N('Hulvener', 'Venae cavae', 'Venae cavae'),
shapes: { front: [P('M192,146 L193,200 M194,224 L196,338', 'vein')] },
aliases: ['hulvene', 'vener', 'vene'],
info: I('Øvre hulvene fra hoved og arme, nedre hulvene fra underkrop og ben – begge munder ud i højre forkammer.', 'Fører iltfattigt blod tilbage til hjertet.', 'Muskelpumpen i benene og vejrtrækningen hjælper blodet tilbage mod hjertet.', 'Let aktivitet efter træning (aktiv nedkøling) støtter tilbageløbet af blod.'),
related: ['right-atrium', 'heart'] },
{ id: 'aorta', type: 'vessel', layer: 'circulation', parent: 'thorax', names: N('Hovedpulsåre', 'Aorta', 'Aorta'),
shapes: { front: [P('M206,214 L204,188 Q206,172 216,174 Q224,178 216,196 L212,340', 'artery')] },
aliases: ['arterie', 'pulsåre', 'arterier'],
info: I('Udgår fra venstre hjertekammer, buer over hjertet og løber ned foran rygsøjlen.', 'Kroppens største arterie. Fordeler iltet blod til hele kroppen.', 'Den elastiske væg udjævner trykket mellem hjerteslagene.', 'Blodtrykket stiger under arbejde, især det systoliske tryk.'),
related: ['left-ventricle', 'femoral-artery', 'heart'] },
{ id: 'femoral-artery', type: 'vessel', layer: 'circulation', parent: 'thigh', bilateral: true, names: N('Lårpulsåre', 'Femoral artery', 'Arteria femoralis'),
shapes: { front: [P('M208,340 Q186,356 182,392 L178,470 Q175,520 170,552', 'arteryT')] },
info: I('Forsiden og indersiden af låret.', 'Hovedforsyningen af iltet blod til benet.', 'Blodgennemstrømningen til benmusklerne øges mange gange under arbejde.', 'Pulsen kan mærkes i lysken. En kraftig blødning herfra er livstruende – relevant i førstehjælp.'),
related: ['aorta', 'quadriceps'] },
{ id: 'heart', type: 'organ', layer: 'circulation', parent: 'thorax', names: N('Hjerte', 'Heart', 'Cor'),
shapes: { front: [P('M187,208 Q185,193 199,194 Q207,188 216,193 Q233,192 232,212 Q230,236 213,250 Q193,238 187,208 Z', 'heart')] },
info: I('I brysthulen mellem lungerne, bag brystbenet, med spidsen mod venstre.', 'Muskelpumpe med to adskilte sider: højre side pumper iltfattigt blod til lungerne, venstre side pumper iltet blod ud i kroppen.', 'Puls og slagvolumen øges under arbejde, så hjertets minutvolumen stiger.', 'Konditionstræning øger slagvolumen – derfor har veltrænede ofte lav hvilepuls.'),
related: ['aorta', 'venae-cavae', 'lungs'] },
{ id: 'right-atrium', type: 'organpart', layer: 'circulation', parent: 'heart', detail: true, cls: 'chamber', names: N('Højre forkammer', 'Right atrium', 'Atrium dextrum'),
shapes: { front: [E(195, 207, 8, 10, 'heartR')] },
info: I('Hjertets højre side, øverst.', 'Modtager iltfattigt blod fra kroppen via hulvenerne.', 'Blodet passerer en hjerteklap (trikuspidalklappen) til højre hjertekammer.', 'Muskelpumpen i benene hjælper blodet tilbage til hjertet under bevægelse.'),
related: ['right-ventricle', 'venae-cavae'] },
{ id: 'right-ventricle', type: 'organpart', layer: 'circulation', parent: 'heart', detail: true, cls: 'chamber', names: N('Højre hjertekammer', 'Right ventricle', 'Ventriculus dexter'),
shapes: { front: [E(202, 227, 11, 13, 'heartR')] },
info: I('Hjertets højre side, nederst.', 'Pumper iltfattigt blod til lungerne via lungepulsåren (det lille kredsløb).', 'Arbejder mod et lavt tryk og har derfor tyndere væg end venstre kammer.', 'Pumper den samme mængde blod som venstre side – også under hårdt arbejde.'),
related: ['right-atrium', 'lungs'] },
{ id: 'left-atrium', type: 'organpart', layer: 'circulation', parent: 'heart', detail: true, cls: 'chamber', names: N('Venstre forkammer', 'Left atrium', 'Atrium sinistrum'),
shapes: { front: [E(218, 202, 9, 7, 'heartL')] },
info: I('Hjertets venstre side, øverst.', 'Modtager iltet blod fra lungerne via lungevenerne.', 'Blodet passerer en hjerteklap (mitralklappen) til venstre hjertekammer.', 'Udgangspunktet for iltleveringen til de arbejdende muskler.'),
related: ['left-ventricle', 'lungs'] },
{ id: 'left-ventricle', type: 'organpart', layer: 'circulation', parent: 'heart', detail: true, cls: 'chamber', names: N('Venstre hjertekammer', 'Left ventricle', 'Ventriculus sinister'),
shapes: { front: [E(218, 228, 11, 14, 'heartL')] },
info: I('Hjertets venstre side, nederst.', 'Pumper iltet blod ud i aorta og hele kroppen (det store kredsløb).', 'Har den tykkeste muskelvæg, fordi den skal skabe et højt tryk.', 'Udholdenhedstræning øger kammerets volumen og dermed slagvolumen.'),
related: ['left-atrium', 'aorta'] },
/* ----- NERVESYSTEM ----- */
{ id: 'brain', type: 'organ', layer: 'nervous', parent: 'head', names: N('Hjerne', 'Brain', 'Encephalon'),
shapes: both([E(200, 50, 26, 28, 'brain'), P('M186,40 Q194,34 200,42 Q206,34 214,40 M182,54 Q190,48 198,56 M202,56 Q210,48 218,54 M190,66 Q200,60 210,66', 'gyri deco')]),
aliases: ['lillehjerne', 'storhjerne', 'cerebrum'],
info: I('I kraniet.', 'Kroppens kontrolcenter. Planlægger og styrer bevægelser, behandler sanseindtryk og regulerer bl.a. vejrtrækning og hjerterytme.', 'Den motoriske hjernebark sender signaler til musklerne, og lillehjernen finjusterer koordination og balance.', 'Motorisk læring: nye bevægelser automatiseres gennem gentagelse og variation.'),
related: ['spinal-cord', 'skull'] },
{ id: 'spinal-cord', type: 'nerve', layer: 'nervous', parent: 'back', names: N('Rygmarv', 'Spinal cord', 'Medulla spinalis'),
shapes: both([P('M200,92 L200,300', 'cord'), P('M200,300 L195,346 M200,300 L200,350 M200,300 L205,346', 'nerveT deco')]),
info: I('I rygsøjlens kanal fra hjernestammen til ca. 1.–2. lændehvirvel. Herunder fortsætter nerverødderne (cauda equina).', 'Forbinder hjernen med kroppen og styrer reflekser.', 'Motoriske nervebaner går fra rygmarven ud til musklerne. Reflekser som knæreflekset sker via rygmarven.', 'Rygmarvsskader kan give lammelser – en af grundene til reglerne om hovedspring på lavt vand.'),
related: ['brain', 'spine', 'sciatic-nerve', 'femoral-nerve'] },
{ id: 'femoral-nerve', type: 'nerve', layer: 'nervous', parent: 'thigh', bilateral: true, names: N('Lårnerve', 'Femoral nerve', 'Nervus femoralis'),
shapes: { front: [P('M192,330 Q182,360 180,396 L176,452', 'nerve'), P('M178,420 L168,440 M177,436 L186,456', 'nerveT deco')] },
info: I('Fra lændens nervefletning gennem lysken til lårets forside.', 'Styrer bl.a. quadriceps og giver følesans på lårets forside.', 'Nødvendig for ekstension af knæet.', 'Uden aktivering af quadriceps er afsæt og landinger ikke mulige.'),
related: ['quadriceps', 'spinal-cord'] },
{ id: 'sciatic-nerve', type: 'nerve', layer: 'nervous', parent: 'thigh', bilateral: true, names: N('Iskiasnerve', 'Sciatic nerve', 'Nervus ischiadicus'),
shapes: { back: [P('M192,350 Q176,378 172,410 L170,470 Q169,520 168,560', 'nerve')] },
aliases: ['iskias'],
info: I('Fra bækkenet ned bag på låret.', 'Kroppens tykkeste nerve. Styrer hamstrings og – via sine grene – musklerne i underben og fod.', 'Nødvendig for bl.a. knæfleksion og ankelbevægelser.', 'Irritation af nerverødderne kan give iskiassmerter ned i benet.'),
related: ['hamstrings', 'spinal-cord', 'gastrocnemius'] },
{ id: 'ulnar-nerve', type: 'nerve', layer: 'nervous', parent: 'elbow', bilateral: true, names: N('Albuenerve', 'Ulnar nerve', 'Nervus ulnaris'),
shapes: { back: [P('M148,168 Q136,220 125,286 Q116,340 106,404', 'nerve')] },
aliases: ['snurreben', 'funny bone'],
info: I('Langs armens inderside. Passerer bag den indvendige albueknude.', 'Styrer bl.a. mange af håndens små muskler og giver følesans i lillefingersiden.', 'Vigtig for fingrenes finmotorik og greb.', 'Det er denne nerve, der giver den elektriske snurren, når man slår albuen.'),
related: ['elbow-joint', 'ulna'] },
/* ----- MUSKLER ----- */
// EXTENSION POINT: Add new muscles here (husk muscle.origin og muscle.insertion)
{ id: 'trapezius', type: 'muscle', layer: 'muscles', parent: 'back', bilateral: true, names: N('Kappemuskel', 'Trapezius', 'Musculus trapezius'),
shapes: { back: [P('M200,96 L200,252 Q186,226 176,190 Q162,158 132,150 Q160,136 186,120 Q194,108 200,96 Z', 'muscle')] },
muscle: { origin: 'Nakkeregionen (linea nuchae og ligamentum nuchae) og torntappene fra 7. halshvirvel til 12. brysthvirvel', insertion: 'Kravebenets yderste tredjedel, acromion og skulderbladskammen (spina scapulae)' },
aliases: ['traps'],
info: I('Øverst på ryggen og i nakken.', 'Bevæger og stabiliserer skulderbladet: øvre del løfter det, midterste del trækker det mod rygsøjlen, nedre del sænker det. Øvre og nedre del roterer det opad.', 'Nødvendig for at armen kan løftes helt over hovedet.', 'Skulderbladskontrol ved kast, håndstand og løft over hovedet.'),
related: ['scapula', 'clavicle', 'deltoid'] },
{ id: 'latissimus-dorsi', type: 'muscle', layer: 'muscles', parent: 'back', bilateral: true, names: N('Brede rygmuskel', 'Latissimus dorsi', 'Musculus latissimus dorsi'),
shapes: { back: [P('M178,212 Q156,216 134,200 Q138,244 152,276 Q170,318 198,338 L198,258 Q186,238 178,212 Z', 'muscle')] },
muscle: { origin: 'Torntappene fra ca. 7. brysthvirvel og nedefter (via fascia thoracolumbalis), hoftekammen og de nederste ribben', insertion: 'Forsiden af overarmsknoglen (bunden af sulcus intertubercularis)' },
aliases: ['lats', 'latissimus'],
info: I('Nedre og midterste del af ryggen, op mod armhulen.', 'Ekstension, adduktion og indadrotation af armen i skulderleddet.', 'Trækker armen ned og bagud mod kroppen.', 'Pull-ups, roning, svømning, klatring og kast.'),
related: ['humerus', 'shoulder-joint', 'pectoralis-major'] },
{ id: 'pectoralis-major', type: 'muscle', layer: 'muscles', parent: 'thorax', bilateral: true, names: N('Store brystmuskel', 'Pectoralis major', 'Musculus pectoralis major'),
shapes: { front: [P('M197,150 Q172,146 150,150 Q140,166 143,184 Q162,200 197,198 Z', 'muscle'), P('M150,160 Q175,168 196,170', 'mline deco')] },
muscle: { origin: 'Kravebenets mediale halvdel, brystbenet og ribbensbrusken fra ca. 1.–6. ribben', insertion: 'Kammen under det store knudepunkt på overarmsknoglen (crista tuberculi majoris)' },
aliases: ['brystmuskel', 'pecs'],
info: I('Forsiden af brystkassen.', 'Adduktion og indadrotation af armen i skulderleddet. Den øverste del bidrager til fleksion.', 'Fører armen ind foran kroppen.', 'Armstrækninger, bænkpres, kast og crawl.'),
related: ['humerus', 'sternum', 'deltoid'] },
{ id: 'obliquus-externus', type: 'muscle', layer: 'muscles', parent: 'abdomen', bilateral: true, names: N('Ydre skrå bugmuskel', 'External oblique', 'Musculus obliquus externus abdominis'),
shapes: { front: [P('M146,214 Q156,244 181,252 L183,332 Q164,334 152,322 Q144,280 146,214 Z', 'muscle'), P('M152,236 L176,262 M150,262 L178,290 M152,290 L178,316', 'mline deco')] },
muscle: { origin: 'Ydersiden af 5.–12. ribben', insertion: 'Hoftekammen, lyskebåndet og den senede midtlinje (linea alba)' },
aliases: ['skrå bugmuskler', 'obliques'],
info: I('Siden af maven.', 'Roterer truncus mod modsatte side og bøjer den til samme side. Begge sider sammen bøjer truncus forover.', 'Arbejder sammen med de indre skrå bugmuskler ved rotation.', 'Rotation i kast, slag (fx tennis og golf) og retningsskift.'),
related: ['rectus-abdominis', 'ribs', 'pelvis'] },
{ id: 'rectus-abdominis', type: 'muscle', layer: 'muscles', parent: 'abdomen', bilateral: true, names: N('Lige bugmuskel', 'Rectus abdominis', 'Musculus rectus abdominis'),
shapes: { front: [P('M185,214 L198,214 L198,348 L187,348 Q183,280 185,214 Z', 'muscle'), P('M185,244 H198 M184,274 H198 M184,304 H198', 'mline deco')] },
muscle: { origin: 'Skambenet (crista pubica og symphysis pubica)', insertion: 'Ribbensbrusken på 5.–7. ribben og brystbenets nederste spids' },
aliases: ['six-pack', 'mavemuskel', 'bugmuskel', 'mavebøjer'],
info: I('Forsiden af maven på hver side af midtlinjen.', 'Bøjer rygsøjlen forover, stabiliserer bækken og truncus og øger trykket i bughulen.', 'Senestrøg deler musklen i felter – deraf "six-pack".', 'Stabiliserer kroppen i spring, landinger og kast.'),
related: ['obliquus-externus', 'diaphragm', 'pelvis'] },
{ id: 'gluteus-maximus', type: 'muscle', layer: 'muscles', parent: 'gluteal-region', bilateral: true, names: N('Store sædemuskel', 'Gluteus maximus', 'Musculus gluteus maximus'),
shapes: { back: [P('M200,346 Q172,338 150,356 Q142,384 156,406 Q182,416 200,404 Z', 'muscle')] },
muscle: { origin: 'Bagsiden af hoftebenet (os ilium), korsbenet, halebenet og ledbåndet mellem kors- og sædeben', insertion: 'Lårets senestreng (tractus iliotibialis) og en ru flade bag på lårbenet (tuberositas glutea)' },
aliases: ['balde', 'sædemuskel', 'glutes', 'baller'],
info: I('Sæderegionen.', 'Ekstension og udadrotation i hofteleddet.', 'Strækker hoften, fx når man rejser sig fra squat eller går op ad trapper.', 'Kraftcenter i sprint, hop, squat og løft.'),
related: ['hip-joint', 'pelvis', 'femur', 'hamstrings'] },
{ id: 'quadriceps', type: 'muscle', layer: 'muscles', parent: 'thigh', bilateral: true, names: N('Firehovedet lårmuskel', 'Quadriceps', 'Musculus quadriceps femoris'),
shapes: { front: [P('M151,402 Q170,392 192,404 Q196,470 184,548 Q170,558 158,548 Q146,480 151,402 Z', 'muscle'), P('M171,406 Q167,480 170,546 M160,470 Q164,510 166,540 M186,470 Q182,510 176,544', 'mline deco')] },
muscle: { origin: 'Rectus femoris: bækkenet (spina iliaca anterior inferior). Vastus lateralis, medialis og intermedius: lårbenet', insertion: 'Via knæskallen og knæskalssenen på skinnebenets knold (tuberositas tibiae)' },
aliases: ['forlår', 'quads', 'lårmuskel', 'rectus femoris', 'vastus'],
info: I('Forsiden af låret.', 'Ekstension i knæleddet. Rectus femoris bøjer desuden i hoften.', 'Består af fire dele: rectus femoris, vastus lateralis, vastus medialis og vastus intermedius.', 'Afgørende ved afsæt, spark og squat – og ikke mindst ved bremsning og landing, hvor den arbejder excentrisk.'),
related: ['femur', 'patella', 'patellar-ligament', 'hamstrings', 'femoral-nerve'] },
{ id: 'hamstrings', type: 'muscle', layer: 'muscles', parent: 'thigh', bilateral: true, names: N('Baglårsmuskler', 'Hamstrings', 'Musculi ischiocrurales'),
shapes: { back: [P('M155,414 Q172,406 192,414 Q194,480 185,556 Q170,563 157,556 Q148,480 155,414 Z', 'muscle'), P('M173,416 Q170,480 171,552', 'mline deco')] },
muscle: { origin: 'Sædebensknuden (tuber ischiadicum). Biceps femoris\' korte hoved fra lårbenet', insertion: 'Biceps femoris: lægbenets hoved. Semitendinosus og semimembranosus: skinnebenets inderside' },
aliases: ['baglår', 'hasemuskler', 'biceps femoris', 'semitendinosus', 'semimembranosus'],
info: I('Bagsiden af låret.', 'Fleksion i knæleddet og ekstension i hofteleddet.', 'Gruppen består af biceps femoris, semitendinosus og semimembranosus.', 'Bremser underbenet i slutningen af svingfasen ved sprint – et typisk sted for fibersprængninger.'),
related: ['femur', 'tibia', 'quadriceps', 'gluteus-maximus', 'sciatic-nerve'] },
{ id: 'gastrocnemius', type: 'muscle', layer: 'muscles', parent: 'lowerleg', bilateral: true, names: N('Tvillingelægmuskel', 'Gastrocnemius', 'Musculus gastrocnemius'),
shapes: { back: [P('M155,580 Q145,612 152,652 Q162,670 171,660 Q180,670 189,652 Q195,612 186,580 Q170,572 155,580 Z', 'muscle'), P('M171,584 L171,656', 'mline deco')] },
muscle: { origin: 'Lårbenets mediale og laterale kondyl', insertion: 'Hælbenet (tuber calcanei) via achillessenen' },
aliases: ['læg', 'lægmuskel', 'calf', 'triceps surae'],
info: I('Bagsiden af underbenet – lægmusklen.', 'Plantarfleksion af foden. Bidrager desuden til fleksion i knæet.', 'Arbejder sammen med den dybere m. soleus via achillessenen.', 'Afsæt i løb og hop, tåhævning.'),
related: ['achilles', 'femur', 'foot-bones', 'tibialis-anterior'] },
{ id: 'achilles', type: 'tendon', layer: 'muscles', parent: 'lowerleg', bilateral: true, names: N('Achillessene', 'Achilles tendon', 'Tendo calcaneus'),
shapes: { back: [P('M171,658 L172,744', 'tendonO'), P('M171,658 L172,744', 'tendon')] },
aliases: ['hælsene', 'akillessene'],
info: I('Bag på underbenet ned til hælbenet.', 'Kroppens kraftigste sene. Forbinder lægmusklerne med hælbenet.', 'Overfører kraft ved plantarfleksion og lagrer elastisk energi.', 'Elastisk energi i senen gør løb og hop mere økonomiske. Senen kan overbelastes eller briste, fx i badminton.'),
related: ['gastrocnemius', 'foot-bones'] },
{ id: 'tibialis-anterior', type: 'muscle', layer: 'muscles', parent: 'lowerleg', bilateral: true, names: N('Forreste skinnebensmuskel', 'Tibialis anterior', 'Musculus tibialis anterior'),
shapes: { front: [K(160, 592, 166, 704, 12, 6, 'muscle')] },
muscle: { origin: 'Skinnebenets yderside og hinden mellem skinneben og lægben', insertion: 'Den inderste kileformede fodrodsknogle og basis af 1. mellemfodsknogle' },
aliases: ['skinnebensmuskel'],
info: I('Forsiden af underbenet, lateralt for skinnebenet.', 'Dorsalfleksion og inversion af foden.', 'Løfter forfoden, så tåen går fri af underlaget i gangens svingfase.', 'Bremser fodens nedsænkning efter hællanding. Overbelastning kan give smerter på forsiden af skinnebenet.'),
related: ['tibia', 'gastrocnemius', 'ankle-joint'] },
{ id: 'deltoid', type: 'muscle', layer: 'muscles', parent: 'shoulder', bilateral: true, names: N('Deltamuskel', 'Deltoid', 'Musculus deltoideus'),
shapes: both([P('M152,142 Q126,136 118,166 Q115,190 123,206 Q134,192 142,176 Q152,160 152,142 Z', 'muscle'), P('M136,146 Q128,170 124,198', 'mline deco')]),
muscle: { origin: 'Kravebenets yderste tredjedel, acromion og skulderbladskammen', insertion: 'Deltamusklens knold midt på overarmsknoglen (tuberositas deltoidea)' },
aliases: ['skuldermuskel', 'delts', 'deltoideus'],
info: I('Danner skulderens runde kontur.', 'Den midterste del fører armen ud til siden (abduktion). Forreste del bøjer og indadroterer, bageste del strækker og udadroterer armen.', 'Hovedmuskel, når armen løftes ud til siden.', 'Aktiv i kast, slag, svømmetag og støtte på hænderne.'),
related: ['shoulder-joint', 'humerus', 'trapezius', 'clavicle'] },
{ id: 'biceps', type: 'muscle', layer: 'muscles', parent: 'upperarm', bilateral: true, names: N('Tohovedet armbøjer', 'Biceps', 'Musculus biceps brachii'),
shapes: { front: [K(124, 188, 116, 272, 18, 12, 'muscle'), P('M121,196 Q119,230 117,264', 'mline deco')] },
muscle: { origin: 'Langt hoved: over skulderleddets ledskål (tuberculum supraglenoidale). Kort hoved: ravnenæbsudløberen (processus coracoideus)', insertion: 'Spolebenets knold (tuberositas radii) og via en senehinde til underarmens fascie' },
aliases: ['armbøjer', 'biceps brachii'],
info: I('Forsiden af overarmen.', 'Fleksion i albueleddet og supination af underarmen. Bidrager til fleksion i skulderen.', 'Agonist ved albuefleksion. Arbejder excentrisk, når en vægt sænkes kontrolleret.', 'Træk- og løftebevægelser, fx klatring, roning og ringe i gymnastik.'),
related: ['triceps', 'radius', 'elbow-joint', 'humerus'] },
{ id: 'triceps', type: 'muscle', layer: 'muscles', parent: 'upperarm', bilateral: true, names: N('Trehovedet armstrækker', 'Triceps', 'Musculus triceps brachii'),
shapes: { back: [K(127, 180, 114, 282, 22, 13, 'muscle'), P('M126,188 Q121,236 116,276', 'mline deco')] },
muscle: { origin: 'Langt hoved: under skulderleddets ledskål (tuberculum infraglenoidale). Laterale og mediale hoved: bagsiden af overarmsknoglen', insertion: 'Albuespidsen (olecranon) på albuebenet' },
aliases: ['armstrækker', 'triceps brachii'],
info: I('Bagsiden af overarmen.', 'Ekstension i albueleddet. Det lange hoved bidrager til at føre armen bagud og ind mod kroppen.', 'Agonist ved albueekstension og antagonist til biceps.', 'Armstrækninger, støtte i gymnastik, kuglestød og kast.'),
related: ['biceps', 'ulna', 'elbow-joint', 'humerus'] }
];
/* =====================================================================
BEVÆGELSER
agonists = primære muskler, synergists = hjælpe-/stabiliserende muskler,
antagonists = modsatvirkende muskler. mini = animation af figur (side/front).
keys = nøglestillinger [tid 0–1, vinkler i grader].
Vinkler: lean (trup forover), thR/thL (lår frem fra lodret), knR/knL (knæfleksion),
pfR/pfL (plantarfleksion), shR/shL (arm frem), elR/elL (albuefleksion), lift (px op), abd (abduktion, frontvisning).
===================================================================== */
// EXTENSION POINT: Add new movements and sports activities here
const MOVEMENTS = [
{ id: 'elbow-flex', name: 'Fleksion af albuen', group: 'Enkeltled', view: 'front', plane: 'Sagittalplanet, omkring en frontal akse',
joints: ['elbow-joint'], bones: ['humerus', 'radius', 'ulna'], agonists: ['biceps'], synergists: [], antagonists: ['triceps'],
text: 'Underarmen føres mod overarmen, så vinklen i albuen bliver mindre. Biceps brachii arbejder sammen med m. brachialis og m. brachioradialis (ikke vist i prototypen). Når vægten sænkes igen, arbejder de samme muskler excentrisk og bremser bevægelsen.',
sport: 'Træk og løft: klatring, roning, ringe og curl med håndvægt.',
mini: { view: 'side', prop: 'dumbbell', duration: 3, keys: [[0, { elR: 5, elL: 5 }], [0.5, { elR: 140, elL: 140 }], [1, { elR: 5, elL: 5 }]] },
phases: [[0.5, 'Fleksion: biceps arbejder koncentrisk'], [1, 'Tilbage: biceps arbejder excentrisk og bremser']] },
{ id: 'elbow-ext', name: 'Ekstension af albuen', group: 'Enkeltled', view: 'back', plane: 'Sagittalplanet, omkring en frontal akse',
joints: ['elbow-joint'], bones: ['humerus', 'radius', 'ulna'], agonists: ['triceps'], synergists: [], antagonists: ['biceps'],
text: 'Albuen strækkes, så vinklen øges. Triceps brachii er agonist. I animationen udføres bevægelsen med armene over hovedet, så triceps arbejder mod tyngdekraften.',
sport: 'Armstrækninger, støtte i gymnastik, kuglestød og kast.',
mini: { view: 'side', prop: 'dumbbell', duration: 3, keys: [[0, { shR: 170, shL: 170, elR: 140, elL: 140 }], [0.5, { shR: 170, shL: 170, elR: 8, elL: 8 }], [1, { shR: 170, shL: 170, elR: 140, elL: 140 }]] },
phases: [[0.5, 'Ekstension: triceps arbejder koncentrisk'], [1, 'Tilbage: triceps arbejder excentrisk og bremser']] },
{ id: 'knee-flex', name: 'Fleksion af knæet', group: 'Enkeltled', view: 'back', plane: 'Sagittalplanet, omkring en frontal akse',
joints: ['knee-joint'], bones: ['femur', 'tibia'], agonists: ['hamstrings'], synergists: ['gastrocnemius'], antagonists: ['quadriceps'],
text: 'Hælen føres op mod balden. Hamstrings er de primære knæbøjere. Gastrocnemius hjælper, fordi den krydser knæleddet.',
sport: 'Svingfasen i løb. Hamstrings bremser også underbenet lige før foden sættes i.',
mini: { view: 'side', duration: 3, keys: [[0, {}], [0.5, { thR: -8, knR: 115, pfR: 20 }], [1, {}]] },
phases: [[0.5, 'Fleksion: hamstrings arbejder koncentrisk'], [1, 'Tilbage: hamstrings arbejder excentrisk']] },
{ id: 'knee-ext', name: 'Ekstension af knæet', group: 'Enkeltled', view: 'front', plane: 'Sagittalplanet, omkring en frontal akse',
joints: ['knee-joint'], bones: ['femur', 'tibia', 'patella'], agonists: ['quadriceps'], synergists: [], antagonists: ['hamstrings'],
text: 'Knæet strækkes fra bøjet stilling. Quadriceps trækker via knæskallen og knæskalssenen i skinnebenet. Knæskallen øger musklens vægtstangsarm.',
sport: 'Spark og afsæt – og excentrisk arbejde ved landinger.',
mini: { view: 'side', bench: true, duration: 3, keys: [[0, { thR: 90, thL: 90, knR: 90, knL: 90, shR: 15, shL: 15, elR: 20, elL: 20 }], [0.5, { thR: 90, thL: 90, knR: 5, knL: 90, shR: 15, shL: 15, elR: 20, elL: 20 }], [1, { thR: 90, thL: 90, knR: 90, knL: 90, shR: 15, shL: 15, elR: 20, elL: 20 }]] },
phases: [[0.5, 'Ekstension: quadriceps arbejder koncentrisk'], [1, 'Tilbage: quadriceps arbejder excentrisk']] },
{ id: 'shoulder-abd', name: 'Abduktion af skulderen', group: 'Enkeltled', view: 'front', plane: 'Frontalplanet, omkring en sagittal akse',
joints: ['shoulder-joint'], bones: ['humerus', 'scapula', 'clavicle'], agonists: ['deltoid'], synergists: ['trapezius'], antagonists: ['latissimus-dorsi', 'pectoralis-major'],
text: 'Armen løftes ud til siden. Deltamusklens midterste del er agonist, og m. supraspinatus (rotatormanchetten, ikke vist) hjælper især i starten. Over ca. 90° skal skulderbladet rotere opad (trapezius og m. serratus anterior) – den skapulohumerale rytme.',
sport: 'Overhovedbevægelser i volleyball, håndbold, svømning og håndstand.',
mini: { view: 'front', duration: 3.5, keys: [[0, { abd: 0 }], [0.5, { abd: 165 }], [1, { abd: 0 }]] },
phases: [[0.5, 'Abduktion: deltoideus arbejder koncentrisk'], [1, 'Tilbage: deltoideus arbejder excentrisk']] },
{ id: 'squat', name: 'Squat', group: 'Sammensat bevægelse', view: 'front', plane: 'Primært sagittalplanet',
joints: ['hip-joint', 'knee-joint', 'ankle-joint'], bones: ['pelvis', 'femur', 'tibia', 'patella', 'spine'], agonists: ['quadriceps', 'gluteus-maximus'], synergists: ['hamstrings', 'gastrocnemius', 'rectus-abdominis'], antagonists: [],
text: 'En flerledsbevægelse. På vej ned bremser quadriceps og gluteus maximus excentrisk. På vej op strækker de knæ og hofte koncentrisk. Hamstrings og lægmuskler stabiliserer, mens bug- og rygmuskler holder rygsøjlen neutral.',
sport: 'Grundbevægelse for løft, landinger og afsæt. God squat-teknik overføres til mange idrætter.',
mini: { view: 'side', duration: 4, keys: [[0, {}], [0.45, { lean: 38, thR: 92, thL: 92, knR: 118, knL: 118, pfR: -26, pfL: -26, shR: 85, shL: 85 }], [0.55, { lean: 38, thR: 92, thL: 92, knR: 118, knL: 118, pfR: -26, pfL: -26, shR: 85, shL: 85 }], [1, {}]] },
phases: [[0.45, 'Ned: excentrisk arbejde – musklerne bremser'], [0.55, 'Bund: isometrisk arbejde'], [1, 'Op: koncentrisk arbejde – hofte og knæ strækkes']] },
{ id: 'jump', name: 'Hop', group: 'Sammensat bevægelse', view: 'back', plane: 'Primært sagittalplanet',
joints: ['hip-joint', 'knee-joint', 'ankle-joint'], bones: ['pelvis', 'femur', 'tibia', 'foot-bones'], agonists: ['gluteus-maximus', 'quadriceps', 'gastrocnemius'], synergists: ['hamstrings', 'achilles', 'deltoid'], antagonists: [],
text: 'Et lodret hop med modbevægelse. Kroppen sænkes hurtigt og vender til en eksplosiv strækning i hofte, knæ og ankel på samme tid. Den hurtige overgang udnytter stræk-afkortningscyklussen, hvor elastisk energi lagres i sener som achillessenen. Armsvinget giver ekstra højde. Ved landing arbejder de samme muskler excentrisk.',
sport: 'Basketball, volleyball, atletik og springgymnastik.',
mini: { view: 'side', duration: 3.2, keys: [[0, {}],
[0.28, { lean: 32, thR: 70, thL: 70, knR: 95, knL: 95, pfR: -25, pfL: -25, shR: -50, shL: -50 }],
[0.42, { lean: 8, thR: -4, thL: -4, pfR: 40, pfL: 40, shR: 165, shL: 165 }],
[0.58, { lean: 4, thR: 6, thL: 6, knR: 12, knL: 12, pfR: 30, pfL: 30, shR: 170, shL: 170, lift: 60 }],
[0.72, { lean: 6, thR: 10, thL: 10, knR: 15, knL: 15, pfR: 15, pfL: 15, shR: 90, shL: 90 }],
[0.84, { lean: 25, thR: 55, thL: 55, knR: 75, knL: 75, pfR: -20, pfL: -20, shR: 50, shL: 50 }], [1, {}]] },
phases: [[0.28, 'Modbevægelse: excentrisk'], [0.45, 'Afsæt: hofte, knæ og ankel strækkes eksplosivt'], [0.72, 'Svævefase'], [1, 'Landing: musklerne bremser excentrisk']] },
{ id: 'run', name: 'Løb', group: 'Sammensat bevægelse', view: 'back', plane: 'Primært sagittalplanet',
joints: ['hip-joint', 'knee-joint', 'ankle-joint', 'shoulder-joint'], bones: ['pelvis', 'femur', 'tibia', 'foot-bones'], agonists: ['gluteus-maximus', 'quadriceps', 'hamstrings', 'gastrocnemius'], synergists: ['tibialis-anterior', 'rectus-abdominis', 'obliquus-externus', 'deltoid', 'achilles'], antagonists: [],
text: 'Løb veksler mellem standfase og svævefase. I standfasen bremser quadriceps ved fodisættet og strækker ved afsættet sammen med gluteus maximus og lægmusklerne. I svingfasen føres låret frem af hoftebøjerne (m. iliopsoas, ikke vist), og hamstrings bremser underbenet. Tibialis anterior løfter forfoden. Armene svinger modsat benene og balancerer kroppens rotation.',
sport: 'Grundform i de fleste boldspil og i atletik.',
mini: { view: 'side', duration: 1.1, keys: (() => {
const A = { lean: 12, thR: 38, knR: 25, pfR: 0, thL: -22, knL: 75, pfL: 25, shR: -40, shL: 40, elR: 95, elL: 95, lift: 6 };
const B = { lean: 12, thR: 5, knR: 30, pfR: -25, thL: 20, knL: 110, pfL: 10, shR: 0, shL: 0, elR: 95, elL: 95, lift: 0 };
const sw = o => { const r = {}; Object.keys(o).forEach(k => { const m = k.match(/^(.*)([RL])$/); r[m ? m[1] + (m[2] === 'R' ? 'L' : 'R') : k] = o[k]; }); return r; };
return [[0, A], [0.25, B], [0.5, sw(A)], [0.75, sw(B)], [1, A]]; })() },
phases: [[1, 'Løbecyklus: stand- og svingfase skifter mellem benene']] }
];
/* =====================================================================
FYSIOLOGISKE PROCESSER
pts = punkter (forfra-koordinater) partiklen passerer. structs = fremhæves ved trinnet.
o2 (kun blodets vej) = blodet er iltet efter dette trin.
===================================================================== */
// EXTENSION POINT: Add new physiological processes here (fx energisystemer, nerveimpuls)
const PROCESSES = [
{ id: 'oxygen', name: 'Iltens vej', color: '#16a6c4', layers: ['respiration', 'circulation', 'muscles', 'digestion'], useActivity: true, loop: false,
intro: 'Følg ilten fra luften til de arbejdende muskler.',
steps: [
{ title: 'Indånding', structs: ['mouth', 'diaphragm'], pts: [[200, 84], [200, 95]], text: 'Mellemgulvet trækker sig sammen, brysthulen udvides, og luft strømmer ind gennem næse og mund. Næsen opvarmer, fugter og renser luften.' },
{ title: 'Svælget', structs: ['pharynx'], pts: [[200, 108]], text: 'Svælget er en fælles passage for luft og mad.' },
{ title: 'Luftrøret', structs: ['trachea'], pts: [[200, 140], [200, 174]], text: 'Bruskringe holder luftrøret åbent. Fimrehår og slim fanger partikler.' },
{ title: 'Bronkierne', structs: ['bronchi'], pts: [[184, 194]], text: 'Luftrøret deler sig i to hovedbronkier, der forgrener sig i stadig mindre luftveje.' },
{ title: 'Lungerne', structs: ['lungs'], pts: [[170, 210]], text: 'Luften fordeles ud i lungernes forgreninger.' },
{ title: 'Alveolerne', structs: ['alveoli'], pts: [[166, 232]], text: 'Gasudveksling: ilt diffunderer gennem den tynde væg ind i blodet, og kuldioxid går den modsatte vej.' },
{ title: 'Blodet', structs: ['left-atrium'], pts: [[190, 220], [218, 203]], text: 'Ilten bindes til hæmoglobin i de røde blodlegemer og føres med lungevenerne til venstre forkammer.' },
{ title: 'Venstre hjertekammer', structs: ['left-ventricle'], pts: [[218, 229]], text: 'Venstre hjertekammer pumper det iltede blod ud i aorta.' },
{ title: 'Aorta', structs: ['aorta'], pts: [[206, 200], [205, 182], [216, 176], [214, 200], [211, 330]], text: 'Hovedpulsåren fordeler blodet til hele kroppen.' },
{ title: 'Arterier i benet', structs: ['femoral-artery'], pts: [[190, 360], [182, 400], [178, 470]], text: 'Via lårpulsåren føres blodet til benets muskler. Under arbejde udvides karrene til de aktive muskler.' },
{ title: 'Musklerne', structs: ['quadriceps'], pts: [[172, 492], [170, 520]], text: 'I kapillærerne afgives ilt til muskelcellerne. I mitokondrierne bruges ilten til aerob energiomsætning (dannelse af ATP). Kuldioxid føres tilbage med blodet.' }
] },
{ id: 'blood', name: 'Blodets vej', color: '#d23a36', layers: ['circulation', 'respiration', 'muscles'], useActivity: true, loop: true,
intro: 'Følg en bloddråbe gennem det store og det lille kredsløb. Rød = iltet blod, blå = iltfattigt blod.',
steps: [
{ title: 'Musklerne', structs: ['quadriceps'], o2: false, pts: [[170, 500]], text: 'I musklernes kapillærer afgiver blodet ilt og optager kuldioxid. Blodet bliver iltfattigt.' },
{ title: 'Vener og hulvener', structs: ['venae-cavae'], o2: false, pts: [[180, 440], [186, 380], [194, 338], [194, 226]], text: 'Venerne fører blodet tilbage. Muskelpumpen og venernes klapper hjælper blodet opad.' },
{ title: 'Højre forkammer', structs: ['right-atrium'], o2: false, pts: [[195, 208]], text: 'Det iltfattige blod løber ind i højre forkammer.' },
{ title: 'Højre hjertekammer', structs: ['right-ventricle'], o2: false, pts: [[202, 228]], text: 'Højre hjertekammer pumper blodet til lungerne via lungepulsåren (det lille kredsløb).' },
{ title: 'Lungerne', structs: ['lungs', 'alveoli'], o2: false, pts: [[204, 196], [188, 186], [168, 205], [166, 230]], text: 'I lungernes kapillærer afgives kuldioxid, og blodet optager ilt.' },
{ title: 'Venstre forkammer', structs: ['left-atrium'], o2: true, pts: [[190, 214], [218, 203]], text: 'Det iltede blod løber via lungevenerne til venstre forkammer.' },
{ title: 'Venstre hjertekammer', structs: ['left-ventricle'], o2: true, pts: [[218, 229]], text: 'Hjertets kraftigste kammer pumper blodet ud i det store kredsløb.' },
{ title: 'Aorta', structs: ['aorta'], o2: true, pts: [[206, 200], [205, 182], [216, 176], [214, 200], [211, 330]], text: 'Hovedpulsåren fordeler blodet til hele kroppen.' },
{ title: 'Arterier', structs: ['femoral-artery'], o2: true, pts: [[190, 360], [182, 400], [178, 470]], text: 'Arterierne forgrener sig ud til muskler og organer – her lårpulsåren til benet.' }
] },
{ id: 'food', name: 'Madens vej', color: '#e0962f', layers: ['digestion', 'organs', 'respiration'], useActivity: false, loop: false,
intro: 'Følg maden gennem fordøjelseskanalen.',
steps: [
{ title: 'Munden', structs: ['mouth'], pts: [[200, 82]], text: 'Tænderne sønderdeler maden, og spyt med amylase begynder nedbrydningen af stivelse.' },
{ title: 'Svælget', structs: ['pharynx'], pts: [[200, 100], [200, 110]], text: 'Synkerefleksen fører maden videre. Strubelåget lukker luftvejen.' },
{ title: 'Spiserøret', structs: ['esophagus'], pts: [[201, 150], [203, 248], [213, 260]], text: 'Peristaltik – bølgeformede muskelsammentrækninger – fører maden ned til mavesækken.' },
{ title: 'Mavesækken', structs: ['stomach'], pts: [[228, 270], [236, 282], [222, 296], [208, 294]], text: 'Saltsyre og pepsin begynder proteinfordøjelsen, og maden blandes til en grødet masse.' },
{ title: 'Tyndtarmen', structs: ['small-intestine', 'liver'], pts: [[196, 300], [184, 306], [196, 314], [220, 316], [212, 328], [184, 332], [186, 344], [214, 342], [210, 356]], text: 'Galde fra leveren og enzymer fra bugspytkirtlen nedbryder fedt, kulhydrat og protein. Næringsstofferne optages gennem tarmtotterne til blodet.' },
{ title: 'Tyktarmen', structs: ['large-intestine'], pts: [[170, 358], [163, 330], [166, 298], [180, 290], [222, 290], [236, 304], [234, 350], [216, 368]], text: 'Vand og salte optages. Tarmbakterier nedbryder kostfibre.' },
{ title: 'Endetarmen', structs: ['rectum'], pts: [[203, 376], [201, 396]], text: 'Affaldsstofferne samles, indtil tarmen tømmes.' }
] }
];
/* ---------- Idrætsfaglig tilstand (illustrative værdier) ---------- */
const ACTIVITY_STATES = {
rest: { label: 'Hvile', hr: 65, hrTxt: '60–70 slag/min', rr: 14, rrTxt: '12–16 pr. min', vo2: 0.12, vo2Txt: 'ca. 0,3 L ilt/min', co: '≈ 5 L/min', muscleBlood: 18, muscleTxt: 'ca. 15–20 %', factor: 1 },
moderate: { label: 'Moderat', hr: 120, hrTxt: '110–130 slag/min', rr: 25, rrTxt: '20–30 pr. min', vo2: 0.5, vo2Txt: 'ca. 1,5 L ilt/min', co: '≈ 12 L/min', muscleBlood: 60, muscleTxt: 'ca. 55–65 %', factor: 1.8 },
hard: { label: 'Hård', hr: 180, hrTxt: '170–190 slag/min', rr: 45, rrTxt: '40–50 pr. min', vo2: 0.95, vo2Txt: 'ca. 3 L ilt/min', co: '≈ 20–25 L/min', muscleBlood: 83, muscleTxt: 'ca. 80–85 %', factor: 2.8 }
};
/* ---------- Navigation ---------- */
const NAV = [
{ id: 'body', label: 'Hele kroppen', layers: ['skin'], color: '#ead1bd' },
{ id: 'skeleton', label: 'Skelet', layers: ['skeleton'], color: '#d9ccaa' },
{ id: 'muscles', label: 'Muskler', layers: ['muscles'], color: '#c55a51' },
{ id: 'joints', label: 'Led', layers: ['skeleton'], joints: true, color: '#0b5d66' },
{ id: 'organs', label: 'Organer', layers: ['organs', 'circulation', 'respiration', 'digestion'], color: '#8e3b36' },
{ id: 'circulation', label: 'Kredsløb', layers: ['circulation'], color: '#d23a36' },
{ id: 'respiration', label: 'Åndedræt', layers: ['respiration'], color: '#8bbbd8' },
{ id: 'digestion', label: 'Fordøjelse', layers: ['digestion', 'organs'], color: '#e0a088' },
{ id: 'nervous', label: 'Nervesystem', layers: ['nervous'], color: '#e0b20e' },
{ sep: true },
{ id: 'movement', label: 'Bevægelse', mode: 'movement', color: '#2f8f5b' },
{ id: 'process', label: 'Processer', mode: 'process', color: '#16a6c4' },
{ id: 'quiz', label: 'Træn din viden', mode: 'quiz', color: '#f2c230' }
];
</script>
<script>
'use strict';
/* =====================================================================
MODULER
===================================================================== */
const NS = 'http://www.w3.org/2000/svg';
const MIRROR = 'matrix(-1 0 0 1 400 0)';
/* ---------- Utils ---------- */
const U = {
$: (s, r = document) => r.querySelector(s),
$$: (s, r = document) => Array.from(r.querySelectorAll(s)),
svg(tag, attrs = {}) { const e = document.createElementNS(NS, tag); for (const k in attrs) e.setAttribute(k, attrs[k]); return e; },
esc: s => String(s).replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])),
norm: s => String(s).toLowerCase().replace(/æ/g, 'ae').replace(/ø/g, 'oe').replace(/å/g, 'aa').normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[^a-z0-9 ]/g, ' ').replace(/\s+/g, ' ').trim(),
shuffle(a) { a = a.slice(); for (let i = a.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [a[i], a[j]] = [a[j], a[i]]; } return a; },
pick: a => a[Math.floor(Math.random() * a.length)],
clamp: (v, a, b) => Math.max(a, Math.min(b, v)),
smooth: t => t * t * (3 - 2 * t)
};
/* ---------- Store (LocalStorage med fallback) ---------- */
const Store = (() => {
const KEY = 'kroppen-i-bevaegelse.v1'; let mem = {};
try { mem = JSON.parse(localStorage.getItem(KEY) || '{}') || {}; } catch (e) { mem = {}; }
return {
get: (k, d) => (k in mem ? mem[k] : d),
set(k, v) { mem[k] = v; try { localStorage.setItem(KEY, JSON.stringify(mem)); } catch (e) { /* fx i sandkasse-iframe */ } }
};
})();
/* ---------- Bus (simpel event-bus mellem moduler) ---------- */
const Bus = { h: {}, on(e, f) { (this.h[e] = this.h[e] || []).push(f); }, emit(e, d) { (this.h[e] || []).forEach(f => f(d)); } };
/* ---------- Indeks og hierarki ---------- */
const S = {}; STRUCTURES.forEach(s => { S[s.id] = s; });
const CHILDREN = {}; STRUCTURES.forEach(s => { if (s.parent) (CHILDREN[s.parent] = CHILDREN[s.parent] || []).push(s.id); });
const MOVES = {}; MOVEMENTS.forEach(m => { MOVES[m.id] = m; });
const ancestors = id => { const a = []; let s = S[id]; while (s && s.parent) { a.unshift(s.parent); s = S[s.parent]; } return a; };
const descendants = id => { const out = []; (CHILDREN[id] || []).forEach(c => out.push(c, ...descendants(c))); return out; };
const hasShapes = s => !!(s && s.shapes && (s.shapes.front || s.shapes.back));
const layerOf = id => LAYERS.find(l => l.id === id);
/* ---------- Global tilstand ---------- */
const State = {
view: Store.get('view', 'front'),
layers: new Set(Store.get('layers', ['skin'])),
sel: null, focus: null,
mode: 'explore', preset: Store.get('preset', 'body'),
explode: 0,
teacher: Object.assign({ hide: false, labels: false, big: false }, Store.get('teacher', {})),
activity: Store.get('activity', 'rest')
};
/* ---------- Toast ---------- */
const Toast = { t: null, show(msg) { const el = U.$('#toast'); el.textContent = msg; el.classList.add('show'); clearTimeout(this.t); this.t = setTimeout(() => el.classList.remove('show'), 2400); } };
/* =====================================================================
LanguageModule
===================================================================== */
const Lang = {
modes: [['da', 'Dansk'], ['en', 'English'], ['la', 'Latin'], ['da+la', 'Dansk + latin'], ['da+en', 'Dansk + engelsk'], ['all', 'Alle tre sprog']],
mode: Store.get('lang', 'da+la'),
hidden() { return State.teacher.hide || Quiz.active; },
primary(s) { return this.mode === 'en' ? s.names.en : this.mode === 'la' ? s.names.la : s.names.da; },
secondary(s) { return this.mode === 'da+la' ? s.names.la : this.mode === 'da+en' ? s.names.en : this.mode === 'all' ? `${s.names.en} / ${s.names.la}` : ''; },
secIsLatin() { return this.mode === 'da+la'; },
label(s) { const p = this.primary(s), q = this.secondary(s); return q ? `${p} (${q})` : p; },
set(m) { this.mode = m; Store.set('lang', m); Bus.emit('lang'); },
init() {
const sel = U.$('#langSelect');
sel.innerHTML = this.modes.map(([v, t]) => `<option value="${v}">${t}</option>`).join('');
sel.value = this.mode;
sel.addEventListener('change', () => this.set(sel.value));
}
};
const sideTxt = side => side === 'right' ? ' (højre)' : side === 'left' ? ' (venstre)' : '';
/* =====================================================================
AnatomyModule – tegner kroppen, styrer lag, valg, zoom og split
===================================================================== */
const Anatomy = {
svg: null, views: {}, layerGroups: { front: {}, back: {} }, inst: {}, vb: { x: 0, y: -10, w: 400, h: 790 }, anim: null, hovSid: null,
init() {
this.svg = U.$('#body-svg');
const defs = U.svg('defs'); this.svg.appendChild(defs);
const useEl = (cls, view) => { const u = U.svg('use', { class: cls, href: '#ghost-' + view }); u.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', '#ghost-' + view); return u; };
['front', 'back'].forEach(view => {
const vg = U.svg('g', { id: 'view-' + view, class: 'view' });
const ghost = U.svg('g', { id: 'ghost-' + view });
STRUCTURES.filter(s => s.layer === 'skin' && s.shapes && s.shapes[view]).forEach(s => {
(s.bilateral ? [false, true] : [false]).forEach(m => {
const g = U.svg('g', m ? { transform: MIRROR } : {});
s.shapes[view].forEach(sp => g.appendChild(this.shape(sp, 'ghostshape')));
ghost.appendChild(g);
});
});
defs.appendChild(ghost); vg.appendChild(useEl('ghost', view));
LAYERS.forEach(L => {
const lg = U.svg('g', { id: `layer-${view}-${L.id}`, class: 'layer layer-' + L.id, 'data-layer': L.id });
lg.appendChild(useEl('layer-ghost', view));
const cap = U.svg('text', { class: 'layer-caption', x: 200, y: 800, 'text-anchor': 'middle' }); cap.textContent = L.da; lg.appendChild(cap);
STRUCTURES.filter(s => s.layer === L.id && s.shapes && s.shapes[view]).forEach(s => this.build(s, view, lg));
vg.appendChild(lg); this.layerGroups[view][L.id] = lg;
});
this.svg.appendChild(vg); this.views[view] = vg;
});
this.svg.appendChild(U.svg('g', { id: 'labels' }));
this.svg.appendChild(U.svg('g', { id: 'fx' }));
this.bind();
},
shape(sp, override) {
const [tag, attrs, cls] = sp; const e = U.svg(tag, attrs); const c = override || cls || '';
const strokeOnly = /\b(bs|tendon|tendonO|lig|gutS|colon|airwayS|artery|arteryT|vein|nerve|nerveT|cord|mline|gyri|ring)\b/.test(c);
e.setAttribute('class', c + (strokeOnly && !override ? ' sk' : ''));
return e;
},
build(s, view, lg) {
(s.bilateral ? [0, 1] : [-1]).forEach(k => {
const mirrored = k === 1;
const side = k < 0 ? 'center' : view === 'front' ? (mirrored ? 'left' : 'right') : (mirrored ? 'right' : 'left');
const id = s.id + (k >= 0 ? '-' + side : '') + (view === 'back' ? '-back' : '');
const g = U.svg('g', { id, class: `st t-${s.type}${s.detail ? ' is-detail' : ''}${s.cls ? ' ' + s.cls : ''}`, 'data-sid': s.id, 'data-side': side, tabindex: '0', role: 'button' });
if (mirrored) g.setAttribute('transform', MIRROR);
s.shapes[view].forEach(sp => g.appendChild(this.shape(sp)));
lg.appendChild(g);
(this.inst[s.id] = this.inst[s.id] || []).push({ g, sid: s.id, side, view, mirrored, layer: s.layer });
});
},
updateAria() {
Object.values(this.inst).flat().forEach(i => {
const s = S[i.sid];
i.g.setAttribute('aria-label', Lang.hidden() ? `${TYPE_LABEL[s.type]} – navn skjult` : `${Lang.label(s)}${sideTxt(i.side)}, ${TYPE_LABEL[s.type].toLowerCase()}`);
});
},
/* --- visning og lag --- */
setView(v) {
State.view = v; Store.set('view', v);
Object.entries(this.views).forEach(([k, g]) => g.classList.toggle('active', k === v));
U.$('#vFront').setAttribute('aria-pressed', v === 'front'); U.$('#vBack').setAttribute('aria-pressed', v === 'back');
this.refreshClasses(); this.updateLabels(); Bus.emit('view', v);
},
setLayers(list) { State.layers = new Set(list); this.applyLayers(); },
toggleLayer(id, on) { on ? State.layers.add(id) : State.layers.delete(id); if (!State.layers.size) State.layers.add(id === 'skin' ? 'skeleton' : 'skin'); this.applyLayers(); },
applyLayers() {
const vis = LAYERS.map(l => l.id).filter(id => State.layers.has(id));
const deeper = vis.filter(id => id !== 'skin' && id !== 'muscles').length > 0;
LAYERS.forEach(L => ['front', 'back'].forEach(v => {
const g = this.layerGroups[v][L.id];
g.style.display = State.layers.has(L.id) ? '' : 'none';
let op = 1;
if (State.explode < 0.05) {
if (L.id === 'skin' && vis.length > 1) op = 0.22;
if (L.id === 'muscles' && deeper) op = 0.55;
}
g.style.opacity = op;
}));
U.$$('#layerToggles input').forEach(inp => { inp.checked = State.layers.has(inp.value); });
Store.set('layers', [...State.layers]);
if (State.explode > 0) this.setExplode(State.explode);
this.refreshClasses(); this.updateLabels(); Bus.emit('layers');
},
/* --- split / eksploderet visning --- */
baseVB() {
const n = EXPLODE_ORDER.filter(id => State.layers.has(id)).length, t = State.explode;
const w = 400 + (n - 1) * 300 * t;
return { x: 200 - w / 2, y: -10, w, h: 790 + 30 * t };
},
setExplode(t) {
State.explode = t;
const order = EXPLODE_ORDER.filter(id => State.layers.has(id)), n = order.length;
LAYERS.forEach(L => {
const i = order.indexOf(L.id), off = i < 0 ? 0 : (i - (n - 1) / 2) * 300 * t;
['front', 'back'].forEach(v => { this.layerGroups[v][L.id].style.transform = `translate(${off}px,0px)`; });
});
this.svg.style.setProperty('--explode', t.toFixed(3));
this.svg.classList.toggle('exploded', t > 0.01);
if (t > 0.01) { State.focus = null; }
this.setVB(this.baseVB());
const fs = Math.max(13, this.vb.w / 400 * 11).toFixed(1);
U.$$('.layer-caption', this.svg).forEach(c => c.setAttribute('font-size', fs));
U.$('#explodeRange').value = Math.round(t * 100);
U.$('#btnExplode').textContent = t > 0.5 ? 'Saml' : 'Split';
// opacitet (uden rekursion)
const vis = LAYERS.map(l => l.id).filter(id => State.layers.has(id));
const deeper = vis.filter(id => id !== 'skin' && id !== 'muscles').length > 0;
['front', 'back'].forEach(v => {
this.layerGroups[v].skin.style.opacity = t < 0.05 && vis.length > 1 ? 0.22 : 1;
this.layerGroups[v].muscles.style.opacity = t < 0.05 && deeper ? 0.55 : 1;
});
this.updateLabels(); this.updateCrumbs();
},
animateExplode(to) {
const from = State.explode, t0 = performance.now(), dur = 900;
if (State.layers.size < 2 && to > 0) { this.setLayers(['skin', 'muscles', 'skeleton', 'organs', 'circulation', 'respiration', 'digestion', 'nervous']); Toast.show('Alle lag er slået til for at kunne splittes'); }
const step = now => { const p = Math.min(1, (now - t0) / dur); this.setExplode(from + (to - from) * U.smooth(p)); if (p < 1) requestAnimationFrame(step); };
requestAnimationFrame(step);
},
/* --- viewBox, zoom og panorering --- */
setVB(v) { this.vb = v; this.svg.setAttribute('viewBox', `${v.x} ${v.y} ${v.w} ${v.h}`); },
animateVB(to, ms = 480) {
cancelAnimationFrame(this.anim); const from = { ...this.vb }, t0 = performance.now();
const step = now => { const p = Math.min(1, (now - t0) / ms), k = U.smooth(p);
this.setVB({ x: from.x + (to.x - from.x) * k, y: from.y + (to.y - from.y) * k, w: from.w + (to.w - from.w) * k, h: from.h + (to.h - from.h) * k });
if (p < 1) this.anim = requestAnimationFrame(step); else this.updateLabels(); };
this.anim = requestAnimationFrame(step);
},
resetZoom() { State.focus = null; this.animateVB(this.baseVB()); this.updateCrumbs(); },
scale(vb = this.vb) { const r = this.svg.getBoundingClientRect(); return Math.min(r.width / vb.w, r.height / vb.h) || 1; },
clientToSvg(cx, cy, vb = this.vb) {
const r = this.svg.getBoundingClientRect(), s = this.scale(vb);
return { x: vb.x + (cx - r.left - (r.width - vb.w * s) / 2) / s, y: vb.y + (cy - r.top - (r.height - vb.h * s) / 2) / s };
},
zoomAround(cx, cy, f, base = this.vb) {
const p = this.clientToSvg(cx, cy, base), max = this.baseVB().w * 1.6;
const w = U.clamp(base.w * f, 45, max), k = w / base.w;
this.setVB({ x: p.x - (p.x - base.x) * k, y: p.y - (p.y - base.y) * k, w, h: base.h * k });
},
zoomBy(f) { const r = this.svg.getBoundingClientRect(); cancelAnimationFrame(this.anim); this.zoomAround(r.left + r.width / 2, r.top + r.height / 2, f); },
visibleInst(sid) { return (this.inst[sid] || []).filter(i => i.view === State.view && State.layers.has(i.layer)); },
bboxOf(sid, side) {
let list = this.visibleInst(sid);
if (!list.length) list = descendants(sid).flatMap(d => this.visibleInst(d));
if (side && side !== 'center') { const f = list.filter(i => i.side === side || i.side === 'center'); if (f.length) list = f; }
if (!list.length) return null;
let x1 = 1e9, y1 = 1e9, x2 = -1e9, y2 = -1e9;
list.forEach(i => { const b = i.g.getBBox(); if (!b.width && !b.height) return; const x = i.mirrored ? 400 - b.x - b.width : b.x;
x1 = Math.min(x1, x); y1 = Math.min(y1, b.y); x2 = Math.max(x2, x + b.width); y2 = Math.max(y2, b.y + b.height); });
return x2 < x1 ? null : { x: x1, y: y1, w: x2 - x1, h: y2 - y1 };
},
zoomTo(sid, side) {
if (sid === 'body') { this.resetZoom(); return; }
if (State.explode > 0) this.setExplode(0);
const b = this.bboxOf(sid, side || (State.sel && State.sel.sid === sid ? State.sel.side : null));
if (!b) { Toast.show('Strukturen er ikke synlig i det aktuelle lag'); return; }
const r = this.svg.getBoundingClientRect(), ar = (r.width / r.height) || 0.6;
const pad = Math.max(b.w, b.h) * 0.45 + 14;
let w = Math.max(b.w + pad * 2, 130), h = Math.max(b.h + pad * 2, 130);
if (w / h < ar) w = h * ar; else h = w / ar;
this.animateVB({ x: b.x + b.w / 2 - w / 2, y: b.y + b.h / 2 - h / 2, w, h });
State.focus = sid; this.updateCrumbs();
},
/* --- valg --- */
resolve(sid) {
let s = S[sid];
const f = State.sel ? State.sel.sid : null;
while (s && s.detail && !(f && (f === s.parent || ancestors(f).includes(s.parent) || f === s.id || S[f].parent === s.parent))) s = S[s.parent];
return s ? s.id : sid;
},
ensureVisible(sid) {
const s = S[sid];
if (State.explode > 0) this.setExplode(0);
if (hasShapes(s)) {
if (!State.layers.has(s.layer)) { State.layers.add(s.layer); this.applyLayers(); }
if (!s.shapes[State.view]) this.setView(s.shapes.front ? 'front' : 'back');
} else {
const ds = descendants(sid).map(d => S[d]).filter(hasShapes);
if (ds.length && !ds.some(d => State.layers.has(d.layer) && d.shapes[State.view])) {
const d = ds.find(x => x.layer === 'skin') || ds[0];
State.layers.add(d.layer); this.applyLayers();
if (!d.shapes[State.view]) this.setView(d.shapes.front ? 'front' : 'back');
}
}
},
select(sid, opt = {}) {
if (!S[sid]) return;
if (opt.ensure) this.ensureVisible(sid);
State.sel = { sid, side: opt.side || null };
this.refreshClasses(); this.updateCrumbs();
Info.show(sid);
if (opt.flash) (this.inst[sid] || []).forEach(i => { i.g.classList.remove('flash'); void i.g.getBBox(); i.g.classList.add('flash'); setTimeout(() => i.g.classList.remove('flash'), 1900); });
if (opt.zoom) this.zoomTo(sid, opt.side);
Bus.emit('select', sid);
},
goTo(sid) { if (State.mode === 'quiz') { const n = NAV.find(x => x.id === State.preset); App.nav(n && !n.mode ? n.id : 'body'); } this.select(sid, { ensure: true, flash: true }); Layout.openRight(); },
refreshClasses() {
U.$$('.st.sel, .st.sel-sub', this.svg).forEach(g => g.classList.remove('sel', 'sel-sub'));
if (!State.sel) return;
const own = this.inst[State.sel.sid] || [];
own.forEach(i => i.g.classList.add('sel'));
if (!own.some(i => i.view === State.view && State.layers.has(i.layer)))
descendants(State.sel.sid).forEach(d => (this.inst[d] || []).forEach(i => i.g.classList.add('sel-sub')));
},
setClass(ids, cls, on = true) { ids.forEach(id => (this.inst[id] || []).forEach(i => i.g.classList.toggle(cls, on))); },
clearClass(cls) { U.$$('.' + cls, this.svg).forEach(g => g.classList.remove(cls)); },
/* --- brødkrummer --- */
updateCrumbs() {
const el = U.$('#crumbs'); const id = State.sel ? State.sel.sid : null;
const chain = id ? ['body', ...ancestors(id).filter(a => a !== 'body'), id].filter((v, i, a) => a.indexOf(v) === i) : ['body'];
el.innerHTML = chain.map((c, i) => (i ? '<span aria-hidden="true">›</span>' : '') +
`<button data-id="${c}" ${c === id || (!id && c === 'body') ? 'aria-current="true"' : ''}>${U.esc(Lang.hidden() && c !== 'body' ? '?' : Lang.primary(S[c]))}</button>`).join('');
},
/* --- navne på kroppen (undervisertilstand) --- */
updateLabels() {
const g = U.$('#labels'); if (!g) return; g.innerHTML = '';
if (!State.teacher.labels || State.explode > 0.01 || Quiz.active || State.teacher.hide) return;
const onlySkin = State.layers.size === 1 && State.layers.has('skin');
Object.keys(this.inst).forEach(sid => {
const s = S[sid]; if (s.detail || (s.type === 'region' && !onlySkin)) return;
const i = this.visibleInst(sid).find(x => !x.mirrored); if (!i) return;
const b = i.g.getBBox(); if (!b.width) return;
const t = U.svg('text', { class: 'blabel', x: b.x + b.width / 2, y: b.y + b.height / 2 + 2, 'text-anchor': 'middle' });
t.textContent = Lang.primary(s); g.appendChild(t);
});
},
/* --- interaktion --- */
bind() {
const svg = this.svg, st = { ptr: new Map(), start: null, moved: false, pinch: null };
svg.addEventListener('pointerdown', e => {
st.ptr.set(e.pointerId, { x: e.clientX, y: e.clientY });
if (st.ptr.size === 1) { st.start = { x: e.clientX, y: e.clientY, vb: { ...this.vb } }; st.moved = false; }
else if (st.ptr.size === 2) { const [a, b] = [...st.ptr.values()]; st.pinch = { d: Math.hypot(a.x - b.x, a.y - b.y), vb: { ...this.vb }, cx: (a.x + b.x) / 2, cy: (a.y + b.y) / 2 }; st.moved = true; }
});
svg.addEventListener('pointermove', e => {
if (!st.ptr.has(e.pointerId)) { this.hover(e); return; }
st.ptr.set(e.pointerId, { x: e.clientX, y: e.clientY });
if (st.ptr.size === 2 && st.pinch) { const [a, b] = [...st.ptr.values()]; const d = Math.hypot(a.x - b.x, a.y - b.y) || 1; this.zoomAround(st.pinch.cx, st.pinch.cy, st.pinch.d / d, st.pinch.vb); return; }
if (st.start) {
const dx = e.clientX - st.start.x, dy = e.clientY - st.start.y;
if (!st.moved && Math.hypot(dx, dy) > 6) { st.moved = true; cancelAnimationFrame(this.anim); try { svg.setPointerCapture(e.pointerId); } catch (_) { } svg.classList.add('panning'); Tooltip.hide(); }
if (st.moved) { const k = 1 / this.scale(st.start.vb); this.setVB({ x: st.start.vb.x - dx * k, y: st.start.vb.y - dy * k, w: st.start.vb.w, h: st.start.vb.h }); }
}
});
const end = e => { st.ptr.delete(e.pointerId); if (st.ptr.size < 2) st.pinch = null; if (!st.ptr.size) { svg.classList.remove('panning'); st.start = null; if (st.moved) { this.updateLabels(); setTimeout(() => { st.moved = false; }, 0); } } };
svg.addEventListener('pointerup', end); svg.addEventListener('pointercancel', end);
svg.addEventListener('pointerleave', () => { Tooltip.hide(); this.setHover(null); });
svg.addEventListener('wheel', e => { e.preventDefault(); cancelAnimationFrame(this.anim); this.zoomAround(e.clientX, e.clientY, e.deltaY > 0 ? 1.14 : 1 / 1.14); }, { passive: false });
svg.addEventListener('click', e => { if (st.moved) return; const g = e.target.closest && e.target.closest('.st'); if (g) this.activate(g); });
svg.addEventListener('dblclick', e => { const g = e.target.closest && e.target.closest('.st'); if (g && !Quiz.active) this.zoomTo(this.resolve(g.dataset.sid), g.dataset.side); });
svg.addEventListener('keydown', e => { const g = e.target.closest && e.target.closest('.st'); if (g && (e.key === 'Enter' || e.key === ' ')) { e.preventDefault(); this.activate(g); } });
svg.addEventListener('focusin', e => { const g = e.target.closest && e.target.closest('.st'); if (!g) return; const sid = this.resolve(g.dataset.sid); this.setHover(sid); const r = g.getBoundingClientRect(); Tooltip.show(r.left + r.width / 2, r.top, sid, g.dataset.side); });
svg.addEventListener('focusout', () => { Tooltip.hide(); this.setHover(null); });
U.$('#crumbs').addEventListener('click', e => { const b = e.target.closest('button'); if (!b) return; const id = b.dataset.id; if (id === 'body') { this.select('body'); this.resetZoom(); } else { this.select(id, { ensure: true }); this.zoomTo(id); } });
},
hover(e) {
if (e.pointerType === 'touch') return;
const g = e.target.closest && e.target.closest('.st');
if (!g) { Tooltip.hide(); this.setHover(null); return; }
const sid = this.resolve(g.dataset.sid); this.setHover(sid); Tooltip.show(e.clientX, e.clientY, sid, g.dataset.side);
},
setHover(sid) { if (sid === this.hovSid) return; this.clearClass('hov'); this.hovSid = sid; if (sid) (this.inst[sid] || []).forEach(i => i.g.classList.add('hov')); },
activate(g) {
const sid = this.resolve(g.dataset.sid), side = g.dataset.side;
if (Quiz.onBodyClick(sid, side)) return;
if (State.mode === 'movement' || State.mode === 'process') Layout.showTab('info');
this.select(sid, { side }); Layout.openRight();
}
};
/* ---------- Tooltip ---------- */
const Tooltip = {
el: null,
show(x, y, sid, side) {
const s = S[sid], el = this.el || (this.el = U.$('#tooltip'));
const name = Lang.hidden() ? '?' : Lang.primary(s) + sideTxt(side);
const sec = Lang.hidden() ? '' : Lang.secondary(s);
el.innerHTML = `<b>${U.esc(name)}</b>${sec ? `<span class="t2${Lang.secIsLatin() ? ' la' : ''}">${U.esc(sec)}</span>` : ''}<span class="t2">${TYPE_LABEL[s.type]}</span>`;
const w = el.offsetWidth, h = el.offsetHeight;
el.style.left = U.clamp(x + 14, 6, innerWidth - w - 6) + 'px';
el.style.top = U.clamp(y - h - 12, 6, innerHeight - h - 6) + 'px';
el.classList.add('show');
},
hide() { (this.el || U.$('#tooltip')).classList.remove('show'); }
};
/* =====================================================================
InfoPanel
===================================================================== */
const Info = {
current: null, revealed: new Set(),
chip(id) { const s = S[id]; if (!s) return ''; const L = layerOf(s.layer);
return `<button class="chip" data-go="${id}"><span class="sw" style="background:${L.color}"></span>${U.esc(Lang.hidden() ? TYPE_LABEL[s.type] + ' ?' : Lang.primary(s))}</button>`; },
welcome() {
this.current = null;
U.$('#panelInfo').innerHTML = `<div class="welcome">
<h3>Klik på kroppen for at begynde</h3>
<p>Hver knogle, muskel, led og organ har navn på dansk, engelsk og latin samt en kort forklaring af funktion, betydning for bevægelse og idrætsrelevans.</p>
<ol>
<li>Vælg et lag – fx <b>Skelet</b> eller <b>Muskler</b> – i venstre side.</li>
<li>Klik på en struktur. Dobbeltklik for at zoome ind.</li>
<li>Brug <b>Split kroppen</b> til at se lagene side om side.</li>
<li>Prøv <b>Bevægelse</b>, <b>Processer</b> og <b>Træn din viden</b>.</li>
</ol>
<p class="muted">Tip: søg på dansk, engelsk eller latin – fx "lårben", "femur" eller "quads".</p></div>`;
},
show(sid) {
this.current = sid; const s = S[sid], L = layerOf(s.layer);
const hide = Lang.hidden() && !this.revealed.has(sid);
const fav = Favorites.has(sid);
const kids = CHILDREN[sid] || [];
const moves = MOVEMENTS.filter(m => [...m.agonists, ...m.synergists, ...m.antagonists, ...m.joints, ...m.bones].includes(sid));
const role = m => m.agonists.includes(sid) ? 'agonist' : m.synergists.includes(sid) ? 'hjælper' : m.antagonists.includes(sid) ? 'antagonist' : m.joints.includes(sid) ? 'led' : 'knogle';
const title = hide ? 'Navn skjult' : Lang.primary(s), sec = hide ? '' : Lang.secondary(s);
const f = (h, t) => t ? `<div class="fact"><h4>${h}</h4><p>${U.esc(t)}</p></div>` : '';
U.$('#panelInfo').innerHTML = `
<div class="kicker"><span class="chip"><span class="sw" style="background:${L.color}"></span>${TYPE_LABEL[s.type]}</span>${s.bilateral ? '<span class="chip">Parret struktur</span>' : ''}</div>
<h3 id="infoTitle">${U.esc(title)}</h3>
${sec ? `<p class="sub${Lang.secIsLatin() ? ' la' : ''}">${U.esc(sec)}</p>` : '<p class="sub"></p>'}
${hide ? '' : `<table class="names"><tr><th>Dansk</th><td>${U.esc(s.names.da)}</td></tr><tr><th>English</th><td>${U.esc(s.names.en)}</td></tr><tr><th>Latin</th><td class="la">${U.esc(s.names.la)}</td></tr></table>`}
<div class="actions">
<button class="btn" id="aFav" aria-pressed="${fav}">${fav ? '★ Skal læres' : '☆ Marker: skal læres'}</button>
${sid !== 'body' ? '<button class="btn" id="aZoom">Zoom ind</button>' : ''}
${hide ? '<button class="btn primary" id="aReveal">Vis navn</button>' : ''}
</div>
${f('Placering', s.info.location)}${f('Funktion', s.info.function)}
${s.muscle ? `<dl class="muscle-box"><dt>Udspring</dt><dd>${U.esc(s.muscle.origin)}</dd><dt>Hæfte</dt><dd>${U.esc(s.muscle.insertion)}</dd></dl>` : ''}
${f('Betydning for bevægelse', s.info.movement)}${f('Idrætsrelevans', s.info.sport)}
${moves.length ? `<div class="fact"><h4>Indgår i bevægelser</h4><div class="chips">${moves.map(m => `<button class="chip" data-move="${m.id}">${U.esc(m.name)} <span class="muted">· ${role(m)}</span></button>`).join('')}</div></div>` : ''}
${kids.length ? `<div class="fact"><h4>Underdele</h4><div class="chips">${kids.map(k => this.chip(k)).join('')}</div>${kids.some(k => S[k].detail) ? '<p class="note">Indre dele kan også vælges direkte på kroppen, når denne struktur er valgt.</p>' : ''}</div>` : ''}
${s.parent ? `<div class="fact"><h4>Del af</h4><div class="chips">${this.chip(s.parent)}</div></div>` : ''}
${s.related && s.related.length ? `<div class="fact"><h4>Relaterede strukturer</h4><div class="chips">${s.related.map(r => this.chip(r)).join('')}</div></div>` : ''}`;
const p = U.$('#panelInfo');
U.$('#aFav', p).onclick = () => { Favorites.toggle(sid); this.show(sid); };
const z = U.$('#aZoom', p); if (z) z.onclick = () => { Anatomy.ensureVisible(sid); Anatomy.zoomTo(sid); };
const rv = U.$('#aReveal', p); if (rv) rv.onclick = () => { this.revealed.add(sid); this.show(sid); };
p.scrollTop = 0;
},
bind() {
U.$('#panelInfo').addEventListener('click', e => {
const g = e.target.closest('[data-go]'); if (g) { Anatomy.select(g.dataset.go, { ensure: true, flash: true }); return; }
const m = e.target.closest('[data-move]'); if (m) Movement.open(m.dataset.move);
});
}
};
/* =====================================================================
SearchModule
===================================================================== */
const Search = {
idx: [], active: -1, items: [],
init() {
this.idx = STRUCTURES.map(s => ({ id: s.id, keys: [s.names.da, s.names.en, s.names.la, ...(s.aliases || [])].map(U.norm) }));
const inp = U.$('#searchInput'), box = U.$('#searchResults');
inp.addEventListener('input', () => this.run(inp.value));
inp.addEventListener('focus', () => { if (inp.value) this.run(inp.value); });
inp.addEventListener('keydown', e => {
if (e.key === 'ArrowDown') { e.preventDefault(); this.move(1); }
else if (e.key === 'ArrowUp') { e.preventDefault(); this.move(-1); }
else if (e.key === 'Enter') { e.preventDefault(); const it = this.items[Math.max(0, this.active)]; if (it) this.choose(it); }
else if (e.key === 'Escape') { this.close(); inp.blur(); }
});
box.addEventListener('click', e => { const b = e.target.closest('button'); if (b) this.choose(b.dataset.id); });
document.addEventListener('click', e => { if (!e.target.closest('.search')) this.close(); });
},
run(q) {
const n = U.norm(q).replace(/^(m|musculus|musculi|n|nervus|a|arteria) /, ''), box = U.$('#searchResults');
if (!n) { this.close(); return; }
const scored = [];
this.idx.forEach(e => { let best = 0; e.keys.forEach(k => { const kk = k.replace(/^(musculus|musculi|nervus|arteria) /, '');
if (kk === n || k === n) best = Math.max(best, 3); else if (kk.startsWith(n) || k.startsWith(n)) best = Math.max(best, 2); else if (k.includes(n)) best = Math.max(best, 1); });
if (best) scored.push([best, e.id]); });
scored.sort((a, b) => b[0] - a[0] || S[a[1]].names.da.localeCompare(S[b[1]].names.da, 'da'));
this.items = scored.slice(0, 9).map(x => x[1]); this.active = this.items.length ? 0 : -1;
box.innerHTML = this.items.length ? this.items.map((id, i) => { const s = S[id];
return `<button role="option" data-id="${id}" aria-selected="${i === 0}"><span class="r1">${U.esc(s.names.da)}</span><span class="r2">${U.esc(s.names.en)} · <span class="la">${U.esc(s.names.la)}</span> · ${TYPE_LABEL[s.type]}</span></button>`; }).join('')
: `<div class="empty">Ingen struktur matcher "${U.esc(q)}". Prøv et dansk, engelsk eller latinsk navn.</div>`;
box.classList.add('open'); U.$('#searchInput').setAttribute('aria-expanded', 'true');
},
move(d) { if (!this.items.length) return; this.active = (this.active + d + this.items.length) % this.items.length; U.$$('#searchResults button').forEach((b, i) => b.setAttribute('aria-selected', i === this.active)); },
close() { U.$('#searchResults').classList.remove('open'); U.$('#searchInput').setAttribute('aria-expanded', 'false'); },
choose(id) { this.close(); U.$('#searchInput').value = ''; Anatomy.goTo(id); if (hasShapes(S[id]) || S[id].type === 'region') Anatomy.zoomTo(id); }
};
/* =====================================================================
FavoritesModule
===================================================================== */
const Favorites = {
list: Store.get('favs', []),
has(id) { return this.list.includes(id); },
toggle(id) { this.list = this.has(id) ? this.list.filter(x => x !== id) : [...this.list, id]; Store.set('favs', this.list); this.render(); },
render() {
const el = U.$('#favList');
el.innerHTML = this.list.length ? this.list.filter(id => S[id]).map(id => `<div class="fav"><button class="go" data-id="${id}">⭐ ${U.esc(Lang.hidden() ? '?' : Lang.primary(S[id]))}</button><button class="x" data-x="${id}" aria-label="Fjern ${U.esc(S[id].names.da)} fra listen">×</button></div>`).join('')
: '<p class="note">Markér strukturer med ☆ i informationspanelet for at samle dem her.</p>';
},
init() { U.$('#favList').addEventListener('click', e => { const g = e.target.closest('[data-id]'), x = e.target.closest('[data-x]'); if (x) this.toggle(x.dataset.x); else if (g) { Anatomy.goTo(g.dataset.id); Layout.closeLeft(); } }); this.render(); }
};
/* =====================================================================
MovementModule
===================================================================== */
const Movement = {
cur: null, playing: true, speed: 1, t: 0, raf: null, last: 0,
ROLE_CLS: { agonists: 'mv-ago', synergists: 'mv-syn', antagonists: 'mv-ant' },
MINI_MUSCLE: {
biceps: { seg: 'upper', side: 1 }, triceps: { seg: 'upper', side: -1 }, quadriceps: { seg: 'thigh', side: 1 }, hamstrings: { seg: 'thigh', side: -1 },
gastrocnemius: { seg: 'shank', side: -1, a: 0.08, b: 0.5 }, 'tibialis-anterior': { seg: 'shank', side: 1, a: 0.12, b: 0.62 },
'gluteus-maximus': { seg: 'glute' }, deltoid: { seg: 'upper', side: 0, a: 0, b: 0.32 }, 'rectus-abdominis': { seg: 'trunk', side: 1, a: 0.08, b: 0.55 }
},
open(id) { App.nav('movement'); this.select(id); },
render() {
const p = U.$('#panelAct');
p.innerHTML = `<p class="muted" style="margin-top:0">Vælg en bevægelse. Kroppen viser de led, knogler og muskler, der indgår.</p>
<div class="mv-list">${MOVEMENTS.map(m => `<button class="chip" data-mv="${m.id}" aria-pressed="${this.cur && this.cur.id === m.id}">${U.esc(m.name)}</button>`).join('')}</div>
<div id="mvBody"></div>`;
p.querySelector('.mv-list').onclick = e => { const b = e.target.closest('[data-mv]'); if (b) this.select(b.dataset.mv); };
if (this.cur) this.renderBody();
},
select(id) {
this.cur = MOVES[id]; this.t = 0; this.playing = true;
U.$$('#panelAct [data-mv]').forEach(b => b.setAttribute('aria-pressed', b.dataset.mv === id));
this.highlight(); this.renderBody(); this.start();
},
highlight() {
const m = this.cur; this.clearHL(); if (!m) return;
Anatomy.setExplode(0); Anatomy.setLayers(['skeleton', 'muscles']); Anatomy.setView(m.view); Anatomy.resetZoom();
Object.entries(this.ROLE_CLS).forEach(([k, c]) => Anatomy.setClass(m[k], c));
Anatomy.setClass(m.joints, 'mv-joint'); Anatomy.setClass(m.bones, 'mv-bone');
const lg = U.$('#legend'); lg.classList.add('show');
lg.innerHTML = `<span><i style="background:#ef5a3c"></i>Primære muskler</span><span><i style="background:#f0a23a"></i>Hjælpe-/stabiliserende</span><span><i style="background:#7d9bd9"></i>Antagonister</span><span><i style="background:rgba(47,143,91,.45)"></i>Led</span><span><i style="background:#fff4c9;border:1px solid #a9976f"></i>Knogler</span>`;
},
clearHL() { ['mv-ago', 'mv-syn', 'mv-ant', 'mv-joint', 'mv-bone'].forEach(c => Anatomy.clearClass(c)); U.$('#legend').classList.remove('show'); },
renderBody() {
const m = this.cur, el = U.$('#mvBody'); if (!el || !m) return;
const list = ids => ids.length ? `<div class="chips">${ids.map(id => Info.chip(id)).join('')}</div>` : '<span class="muted">–</span>';
el.innerHTML = `
<svg class="mini" id="miniSvg" viewBox="0 0 300 330" role="img" aria-label="Animation: ${U.esc(m.name)}"></svg>
<div class="ctrl">
<button class="btn primary" id="mvPlay">${this.playing ? 'Pause' : 'Afspil'}</button>
<label>Hastighed <input type="range" id="mvSpeed" min="0.25" max="2" step="0.25" value="${this.speed}"></label>
</div>
<div class="phase" id="mvPhase" aria-live="polite"></div>
<h3 style="font-family:var(--serif);margin:8px 0 2px">${U.esc(m.name)}</h3>
<p class="muted" style="margin:0 0 8px">${U.esc(m.group)} · ${U.esc(m.plane)}</p>
<p>${U.esc(m.text)}</p>
<div class="roles">
<div><h4><span class="sw" style="display:inline-block;background:#ef5a3c"></span> Primære muskler (agonister)</h4>${list(m.agonists)}</div>
<div><h4><span class="sw" style="display:inline-block;background:#f0a23a"></span> Hjælpe- og stabiliserende strukturer</h4>${list(m.synergists)}</div>
<div><h4><span class="sw" style="display:inline-block;background:#7d9bd9"></span> Antagonister</h4>${list(m.antagonists)}</div>
<div><h4>Led</h4>${list(m.joints)}</div>
<div><h4>Knogler</h4>${list(m.bones)}</div>
</div>
<div class="fact"><h4>Idrætsrelevans</h4><p>${U.esc(m.sport)}</p></div>`;
U.$('#mvPlay').onclick = () => { this.playing = !this.playing; U.$('#mvPlay').textContent = this.playing ? 'Pause' : 'Afspil'; };
U.$('#mvSpeed').oninput = e => { this.speed = +e.target.value; };
el.onclick = e => { const g = e.target.closest('[data-go]'); if (g) { Anatomy.select(g.dataset.go, { ensure: false, flash: true }); Layout.showTab('info'); } };
this.draw();
},
start() { cancelAnimationFrame(this.raf); this.last = performance.now(); const loop = now => { const dt = Math.min(0.1, (now - this.last) / 1000); this.last = now;
if (this.playing && this.cur) { this.t = (this.t + dt * this.speed / this.cur.mini.duration) % 1; this.draw(); } this.raf = requestAnimationFrame(loop); }; this.raf = requestAnimationFrame(loop); },
stop() { cancelAnimationFrame(this.raf); this.clearHL(); },
pose(t) {
const keys = this.cur.mini.keys; let i = 0; while (i < keys.length - 2 && t > keys[i + 1][0]) i++;
const [t0, a] = keys[i], [t1, b] = keys[i + 1]; const u = U.smooth(U.clamp((t - t0) / ((t1 - t0) || 1), 0, 1));
const out = {}; new Set([...Object.keys(a), ...Object.keys(b)]).forEach(k => { out[k] = (a[k] || 0) + ((b[k] || 0) - (a[k] || 0)) * u; }); return out;
},
draw() {
const svg = U.$('#miniSvg'); if (!svg || !this.cur) return;
const m = this.cur, p = this.pose(this.t);
const ph = m.phases.find(x => this.t <= x[0]) || m.phases[m.phases.length - 1]; const pe = U.$('#mvPhase'); if (pe && pe.textContent !== ph[1]) pe.textContent = ph[1];
const roleCol = id => m.agonists.includes(id) ? '#ef5a3c' : m.synergists.includes(id) ? '#f0a23a' : m.antagonists.includes(id) ? '#7d9bd9' : null;
svg.innerHTML = m.mini.view === 'front' ? this.front(p, roleCol) : this.side(p, roleCol, m);
},
side(p, roleCol, m) {
const d2r = Math.PI / 180, L = { trunk: 100, upper: 56, fore: 50, thigh: 86, shank: 84, foot: 26 };
const u = a => ({ x: Math.sin(a * d2r), y: Math.cos(a * d2r) });
const add = (o, a, len) => { const v = u(a); return { x: o.x + v.x * len, y: o.y + v.y * len }; };
const H = { x: 0, y: 0 }, lean = p.lean || 0;
const Sh = { x: Math.sin(lean * d2r) * L.trunk, y: -Math.cos(lean * d2r) * L.trunk };
const Hd = { x: Sh.x + Math.sin(lean * d2r) * 30, y: Sh.y - Math.cos(lean * d2r) * 30 };
const leg = (th, kn, pf) => { const Kn = add(H, th, L.thigh), sa = th - kn, A = add(Kn, sa, L.shank), fa = sa + 90 - pf;
const heel = add(add(A, fa + 180, 6), fa - 90, 8), toe = add(add(A, fa, L.foot), fa - 90, 8); return { K: Kn, A, heel, toe, sa }; };
const arm = (sh, el) => { const El = add(Sh, sh, L.upper); return { E: El, W: add(El, sh + el, L.fore), sa: sh }; };
const R = { ...leg(p.thR || 0, p.knR || 0, p.pfR || 0), ...arm(p.shR || 0, p.elR || 0), th: p.thR || 0 };
const Lf = { ...leg(p.thL || 0, p.knL || 0, p.pfL || 0), ...arm(p.shL || 0, p.elL || 0) };
let ox = 140, oy;
if (m.mini.bench) { oy = 222; } else { const low = Math.max(R.heel.y, R.toe.y, Lf.heel.y, Lf.toe.y); oy = 312 - low - (p.lift || 0); }
const T = q => ({ x: (q.x + ox).toFixed(1), y: (q.y + oy).toFixed(1) });
const line = (a, b, w, c) => { const A = T(a), B = T(b); return `<line x1="${A.x}" y1="${A.y}" x2="${B.x}" y2="${B.y}" stroke="${c}" stroke-width="${w}" stroke-linecap="round"/>`; };
const poly = (pts, c) => `<polygon points="${pts.map(q => { const t = T(q); return t.x + ',' + t.y; }).join(' ')}" fill="${c}" stroke="${c}" stroke-width="3" stroke-linejoin="round"/>`;
const muscle = (id, a, b, segAngle, side, fa = 0.15, fb = 0.85, w = 9, off = 8) => {
const col = roleCol(id); if (!col) return '';
const n = { x: Math.cos(segAngle * d2r) * side, y: -Math.sin(segAngle * d2r) * side };
const P1 = { x: a.x + (b.x - a.x) * fa + n.x * off, y: a.y + (b.y - a.y) * fa + n.y * off }, P2 = { x: a.x + (b.x - a.x) * fb + n.x * off, y: a.y + (b.y - a.y) * fb + n.y * off };
return line(P1, P2, w, col);
};
const far = '#a9bcc3', near = '#2f4f5b', jointHL = m.joints.map(j => j.split('-')[0]);
let s = `<line x1="0" y1="312" x2="300" y2="312" stroke="#b7c7cc" stroke-width="2"/>`;
if (m.mini.bench) s += `<rect x="${ox - 70}" y="${oy + 8}" width="92" height="12" rx="3" fill="#c9b48a"/><rect x="${ox - 62}" y="${oy + 20}" width="8" height="${312 - oy - 20}" fill="#b09a70"/><rect x="${ox + 6}" y="${oy + 20}" width="8" height="${312 - oy - 20}" fill="#b09a70"/>`;
const limb = (X, c, w) => line(H, X.K, 16 * w, c) + line(X.K, X.A, 12 * w, c) + poly([X.A, X.heel, X.toe], c);
const armD = (X, c) => line(Sh, X.E, 12, c) + line(X.E, X.W, 10, c);
s += limb(Lf, far, 1) + armD(Lf, far);
if (m.mini.prop === 'dumbbell') s += this.db(T(Lf.W), far);
s += line(H, Sh, 24, near) + `<circle cx="${T(Hd).x}" cy="${T(Hd).y}" r="16" fill="${near}"/>`;
s += muscle('rectus-abdominis', H, Sh, -lean + 180, -1, 0.1, 0.55, 8, 11);
if (roleCol('gluteus-maximus')) { const g = { x: H.x - Math.cos(lean * d2r) * 11, y: H.y - Math.sin(lean * d2r) * 11 + 4 }; const G = T(g); s += `<circle cx="${G.x}" cy="${G.y}" r="11" fill="${roleCol('gluteus-maximus')}"/>`; }
s += limb(R, near, 1);
s += muscle('quadriceps', H, R.K, R.th, 1, 0.15, 0.85, 9, 7) + muscle('hamstrings', H, R.K, R.th, -1, 0.15, 0.85, 9, 7);
s += muscle('gastrocnemius', R.K, R.A, R.sa, -1, 0.08, 0.5, 9, 6) + muscle('tibialis-anterior', R.K, R.A, R.sa, 1, 0.12, 0.62, 6, 6);
s += armD(R, near);
s += muscle('biceps', Sh, R.E, p.shR || 0, 1, 0.18, 0.82, 7, 6) + muscle('triceps', Sh, R.E, p.shR || 0, -1, 0.15, 0.85, 7, 6) + muscle('deltoid', Sh, R.E, p.shR || 0, 0, 0, 0.3, 11, 0);
if (m.mini.prop === 'dumbbell') s += this.db(T(R.W), '#15313a');
const jp = { knee: R.K, elbow: R.E, hip: H, ankle: R.A, shoulder: Sh };
jointHL.forEach(j => { if (jp[j]) { const q = T(jp[j]); s += `<circle cx="${q.x}" cy="${q.y}" r="7" fill="none" stroke="#f2c230" stroke-width="3"/>`; } });
return s;
},
db(pt, c) { return `<rect x="${pt.x - 13}" y="${pt.y - 3}" width="26" height="6" rx="2" fill="${c}"/><rect x="${pt.x - 15}" y="${pt.y - 8}" width="6" height="16" rx="2" fill="${c}"/><rect x="${+pt.x + 9}" y="${pt.y - 8}" width="6" height="16" rx="2" fill="${c}"/>`; },
front(p, roleCol) {
const d2r = Math.PI / 180, a = (p.abd || 0) * d2r, cx = 150, near = '#2f4f5b', len = 106;
const arm = sgn => { const sx = cx + sgn * 34, sy = 112, ex = sx + sgn * Math.sin(a) * len, ey = sy + Math.cos(a) * len; const mx = sx + sgn * Math.sin(a) * 30, my = sy + Math.cos(a) * 30;
return `<line x1="${sx}" y1="${sy}" x2="${ex.toFixed(1)}" y2="${ey.toFixed(1)}" stroke="${near}" stroke-width="12" stroke-linecap="round"/>` +
(roleCol('deltoid') ? `<line x1="${sx}" y1="${sy}" x2="${mx.toFixed(1)}" y2="${my.toFixed(1)}" stroke="${roleCol('deltoid')}" stroke-width="15" stroke-linecap="round"/>` : '') +
`<circle cx="${sx}" cy="${sy}" r="8" fill="none" stroke="#f2c230" stroke-width="3"/>`; };
let s = `<line x1="0" y1="312" x2="300" y2="312" stroke="#b7c7cc" stroke-width="2"/>`;
s += `<line x1="${cx - 14}" y1="206" x2="${cx - 16}" y2="306" stroke="${near}" stroke-width="15" stroke-linecap="round"/><line x1="${cx + 14}" y1="206" x2="${cx + 16}" y2="306" stroke="${near}" stroke-width="15" stroke-linecap="round"/>`;
s += `<path d="M${cx - 36},108 L${cx + 36},108 L${cx + 24},210 L${cx - 24},210 Z" fill="${near}" stroke="${near}" stroke-width="6" stroke-linejoin="round"/>`;
if (roleCol('trapezius')) s += `<path d="M${cx - 10},92 L${cx - 34},108 M${cx + 10},92 L${cx + 34},108" stroke="${roleCol('trapezius')}" stroke-width="7" stroke-linecap="round"/>`;
if (roleCol('latissimus-dorsi')) s += `<path d="M${cx - 30},122 L${cx - 22},178 M${cx + 30},122 L${cx + 22},178" stroke="${roleCol('latissimus-dorsi')}" stroke-width="7" stroke-linecap="round"/>`;
s += `<circle cx="${cx}" cy="74" r="17" fill="${near}"/>` + arm(-1) + arm(1);
return s;
}
};
/* =====================================================================
ProcessModule (Respiration, Circulation, Digestion)
===================================================================== */
const Process = {
cur: null, path: null, d: 0, playing: false, speed: 1, raf: null, last: 0, step: -1, parts: [],
render() {
const p = U.$('#panelAct');
p.innerHTML = `<p class="muted" style="margin-top:0">Vælg en proces og følg den gennem kroppen.</p>
<div class="mv-list proc-list">${PROCESSES.map(x => `<button class="chip" data-pr="${x.id}" aria-pressed="${this.cur && this.cur.id === x.id}">${U.esc(x.name)}</button>`).join('')}</div><div id="prBody"></div>`;
p.querySelector('.proc-list').onclick = e => { const b = e.target.closest('[data-pr]'); if (b) this.select(b.dataset.pr); };
if (this.cur) this.renderBody();
},
select(id) {
this.cur = PROCESSES.find(x => x.id === id);
U.$$('#panelAct [data-pr]').forEach(b => b.setAttribute('aria-pressed', b.dataset.pr === id));
Anatomy.setExplode(0); Anatomy.setLayers(this.cur.layers); Anatomy.setView('front'); Anatomy.resetZoom();
this.build(); this.d = 0; this.step = -1; this.playing = true; this.renderBody(); this.makeParticles(); this.start();
},
build() {
const pts = [], ps = [];
this.cur.steps.forEach((st, si) => st.pts.forEach(q => { pts.push(q); ps.push(si); }));
if (this.cur.loop) { pts.push(pts[0]); ps.push(0); }
const cum = [0]; for (let i = 1; i < pts.length; i++) cum.push(cum[i - 1] + Math.hypot(pts[i][0] - pts[i - 1][0], pts[i][1] - pts[i - 1][1]));
const start = this.cur.steps.map((_, si) => cum[ps.indexOf(si)]);
this.path = { pts, ps, cum, L: cum[cum.length - 1], start };
},
at(d) {
const P = this.path; if (this.cur.loop) d = ((d % P.L) + P.L) % P.L;
if (d <= 0) return { x: P.pts[0][0], y: P.pts[0][1], step: P.ps[0], j: 0 };
if (d >= P.L) { const n = P.pts.length - 1; return { x: P.pts[n][0], y: P.pts[n][1], step: P.ps[n], j: n - 1 }; }
let j = 0; while (j < P.cum.length - 2 && P.cum[j + 1] < d) j++;
const u = (d - P.cum[j]) / ((P.cum[j + 1] - P.cum[j]) || 1);
return { x: P.pts[j][0] + (P.pts[j + 1][0] - P.pts[j][0]) * u, y: P.pts[j][1] + (P.pts[j + 1][1] - P.pts[j][1]) * u, step: u < 0.5 ? P.ps[j] : P.ps[j + 1], j };
},
makeParticles() {
const fx = U.$('#fx'); fx.innerHTML = ''; this.parts = [];
for (let i = 0; i < 7; i++) { const c = U.svg('circle', { r: i === 0 ? 5.5 : 3.6 - i * 0.25, class: 'particle', fill: this.cur.color, stroke: '#fff', 'stroke-width': i === 0 ? 1.6 : 0.8, opacity: 1 - i * 0.11 }); fx.appendChild(c); this.parts.push(c); }
},
renderBody() {
const el = U.$('#prBody'); if (!el) return; const c = this.cur;
el.innerHTML = `<p>${U.esc(c.intro)}</p>
<div class="ctrl">
<button class="btn primary" id="prPlay">${this.playing ? 'Pause' : (this.d > 0 ? 'Fortsæt' : 'Start')}</button>
<button class="btn" id="prReset">Nulstil</button>
<label>Hastighed <input type="range" id="prSpeed" min="0.25" max="3" step="0.25" value="${this.speed}"></label>
</div>
${c.useActivity ? `<p class="note">Tempoet følger den idrætsfaglige tilstand: <b>${ACTIVITY_STATES[State.activity].label}</b>.</p>` : ''}
<ol class="steps">${c.steps.map((s, i) => `<li><button data-st="${i}" ${i === this.step ? 'aria-current="true"' : ''}><span class="n">${i + 1}</span><span><b>${U.esc(s.title)}</b><br><span class="muted">${U.esc(s.text)}</span></span></button></li>`).join('')}</ol>`;
U.$('#prPlay').onclick = () => { this.playing = !this.playing; U.$('#prPlay').textContent = this.playing ? 'Pause' : 'Fortsæt'; };
U.$('#prReset').onclick = () => { this.d = 0; this.playing = false; U.$('#prPlay').textContent = 'Start'; this.frame(0); };
U.$('#prSpeed').oninput = e => { this.speed = +e.target.value; };
el.querySelector('.steps').onclick = e => { const b = e.target.closest('[data-st]'); if (b) { this.d = this.path.start[+b.dataset.st] + 0.01; this.frame(0); } };
},
start() { cancelAnimationFrame(this.raf); this.last = performance.now();
const loop = now => { const dt = Math.min(0.1, (now - this.last) / 1000); this.last = now; this.frame(this.playing ? dt : 0); this.raf = requestAnimationFrame(loop); };
this.raf = requestAnimationFrame(loop); },
frame(dt) {
if (!this.cur) return;
const f = this.cur.useActivity ? ACTIVITY_STATES[State.activity].factor : 1;
this.d += dt * 42 * this.speed * f;
const gap = 9, tail = gap * this.parts.length;
if (!this.cur.loop && this.d > this.path.L + tail + 60) this.d = 0;
this.parts.forEach((c, i) => {
const dd = this.d - i * gap;
if (!this.cur.loop && (dd < 0 || dd > this.path.L)) { c.setAttribute('visibility', 'hidden'); return; }
const q = this.at(dd); c.setAttribute('visibility', 'visible'); c.setAttribute('cx', q.x.toFixed(1)); c.setAttribute('cy', q.y.toFixed(1));
if (this.cur.id === 'blood') { const st = this.cur.steps[q.step]; c.setAttribute('fill', st.o2 ? '#e0312f' : '#3563d8'); }
});
const lead = this.at(U.clamp(this.d, 0, this.cur.loop ? 1e9 : this.path.L));
if (lead.step !== this.step) this.setStep(lead.step);
},
setStep(i) {
this.step = i; const st = this.cur.steps[i];
Anatomy.clearClass('proc-hl'); Anatomy.setClass(st.structs, 'proc-hl');
U.$$('#prBody [data-st]').forEach(b => b.toggleAttribute('aria-current', +b.dataset.st === i));
const cap = U.$('#caption'); cap.classList.add('show');
cap.innerHTML = `<small>${U.esc(this.cur.name)} – trin ${i + 1} af ${this.cur.steps.length}</small><b>${U.esc(st.title)}</b><p>${U.esc(st.text)}</p>`;
},
stop() { cancelAnimationFrame(this.raf); U.$('#fx').innerHTML = ''; Anatomy.clearClass('proc-hl'); U.$('#caption').classList.remove('show'); this.step = -1; }
};
/* =====================================================================
ActivityModule – hvile / moderat / hård
===================================================================== */
const Activity = {
init() {
U.$('#activitySeg').innerHTML = Object.entries(ACTIVITY_STATES).map(([k, v]) => `<button data-act="${k}">${v.label}</button>`).join('');
U.$('#activitySeg').onclick = e => { const b = e.target.closest('[data-act]'); if (b) this.set(b.dataset.act); };
this.set(State.activity);
},
set(k) {
State.activity = k; Store.set('activity', k); const a = ACTIVITY_STATES[k];
U.$$('#activitySeg button').forEach(b => b.setAttribute('aria-pressed', b.dataset.act === k));
const svg = Anatomy.svg; svg.style.setProperty('--beat', (60 / a.hr).toFixed(2) + 's'); svg.style.setProperty('--breath', (60 / a.rr).toFixed(2) + 's');
svg.classList.remove('act-rest', 'act-moderate', 'act-hard'); svg.classList.add('act-' + k);
const row = (lbl, val, pct) => `<div class="vital"><span>${lbl}</span><b>${val}</b><span class="bar"><i style="width:${pct}%"></i></span></div>`;
U.$('#vitals').innerHTML = row('Puls', a.hrTxt, a.hr / 2) + row('Vejrtrækning', a.rrTxt, a.rr * 1.8) + row('Iltbehov', a.vo2Txt, a.vo2 * 100) +
row('Blod til musklerne', a.muscleTxt, a.muscleBlood) + `<div class="vital"><span>Hjertets minutvolumen</span><b>${a.co}</b></div>`;
if (State.mode === 'process' && Process.cur) Process.renderBody();
}
};
/* =====================================================================
QuizModule – "Træn din viden"
===================================================================== */
const Quiz = {
active: false, qs: [], i: 0, score: 0, tries: 0, type: 'mixed', answered: false, order: [],
TYPES: [['mixed', 'Blandet'], ['find', 'Find kropsdelen'], ['name', 'Hvad hedder den?'], ['dala', 'Dansk ↔ latin'], ['muscle', 'Muskel og bevægelse'], ['order', 'Følg processen']],
pool() { return STRUCTURES.filter(s => hasShapes(s) && !s.detail && ['bone', 'muscle', 'organ', 'joint'].includes(s.type)); },
render() {
this.active = false; this.clear(); Anatomy.updateAria();
const best = Store.get('quizBest', {});
U.$('#panelAct').innerHTML = `<h3 style="font-family:var(--serif);margin:0 0 6px">Træn din viden</h3>
<p class="muted" style="margin:0">Vælg en quiztype. Navne på kroppen skjules undervejs.</p>
<div class="q-types">${this.TYPES.map(([k, t]) => `<label><input type="radio" name="qt" value="${k}" ${k === this.type ? 'checked' : ''}> ${t}${best[k] != null ? ` <span class="muted" style="margin-left:auto">bedst ${best[k]} %</span>` : ''}</label>`).join('')}</div>
<button class="btn primary" id="qStart">Start quiz med 8 spørgsmål</button>`;
U.$('#qStart').onclick = () => { this.type = (U.$('input[name=qt]:checked') || {}).value || 'mixed'; this.start(); };
},
start() {
const types = this.type === 'mixed' ? ['find', 'name', 'dala', 'muscle', 'order', 'find', 'name', 'dala'] : Array(8).fill(this.type);
this.qs = U.shuffle(types).map(t => this.make(t)).filter(Boolean); this.i = 0; this.score = 0; this.active = true;
State.sel = null; Anatomy.refreshClasses(); Anatomy.updateCrumbs(); Info.welcome();
Anatomy.updateAria(); Anatomy.updateLabels(); this.ask();
},
make(t) {
const pool = this.pool();
if (t === 'find') { const s = U.pick(pool); return { t, target: s.id }; }
if (t === 'name') { const s = U.pick(pool); return { t, target: s.id, opts: this.opts(s, x => x.type === s.type, x => Lang.primary(x)) }; }
if (t === 'dala') { const ok = pool.filter(s => U.norm(s.names.da) !== U.norm(s.names.la) && !U.norm(s.names.la).includes(U.norm(s.names.da))); const s = U.pick(ok); const rev = Math.random() < 0.5;
return { t, target: s.id, rev, opts: this.opts(s, x => x.type === s.type && ok.includes(x), x => rev ? x.names.da : x.names.la) }; }
if (t === 'muscle') { const m = U.pick(MOVEMENTS); const ant = m.antagonists.length && Math.random() < 0.35; const correct = U.pick(ant ? m.antagonists : m.agonists);
const others = U.shuffle(STRUCTURES.filter(s => s.type === 'muscle' && s.layer === 'muscles' && ![...m.agonists, ...m.synergists, ...m.antagonists].includes(s.id))).slice(0, 3);
return { t, move: m.id, ant, target: correct, opts: U.shuffle([correct, ...others.map(o => o.id)]).map(id => ({ id, label: Lang.primary(S[id]) })) }; }
if (t === 'order') { const p = U.pick(PROCESSES); const n = Math.min(5, p.steps.length); const st = Math.floor(Math.random() * (p.steps.length - n + 1));
const seq = p.steps.slice(st, st + n).map(x => x.title); return { t, proc: p.id, seq, shuffled: U.shuffle(seq.map((x, i) => i)) }; }
},
opts(s, filter, label) {
const others = U.shuffle(STRUCTURES.filter(x => x.id !== s.id && hasShapes(x) && filter(x) && label(x) !== label(s))).slice(0, 3);
return U.shuffle([s, ...others]).map(x => ({ id: x.id, label: label(x) }));
},
clear() { ['quiz-target', 'quiz-ok', 'quiz-bad'].forEach(c => Anatomy.clearClass(c)); U.$('#hint').classList.remove('show'); },
showOn(sid) { const s = S[sid]; Anatomy.setExplode(0); Anatomy.setLayers([s.layer]); U.$('#body-svg').classList.toggle('show-joints', s.type === 'joint'); if (!s.shapes[State.view]) Anatomy.setView(s.shapes.front ? 'front' : 'back'); Anatomy.resetZoom(); },
ask() {
this.clear(); this.tries = 0; this.answered = false; this.order = [];
const q = this.qs[this.i]; if (!q) return this.finish();
const meta = `<div class="q-meta"><span>Spørgsmål ${this.i + 1} af ${this.qs.length}</span><span>Point: ${this.score}</span></div>`;
let body = '';
const nm = id => Lang.primary(S[id]);
if (q.t === 'find') {
this.showOn(q.target);
body = `<p class="q-prompt">Find: ${U.esc(S[q.target].names.da)}${Lang.mode !== 'da' ? ` <span class="la" style="font-size:.8em">(${U.esc(S[q.target].names.la)})</span>` : ''}</p><p class="muted">Klik på strukturen på kroppen. Der er to forsøg.</p><button class="btn" id="qShow">Vis svaret</button>`;
const h = U.$('#hint'); h.textContent = 'Find: ' + S[q.target].names.da; h.classList.add('show');
} else if (q.t === 'name') {
this.showOn(q.target); Anatomy.setClass([q.target], 'quiz-target');
body = `<p class="q-prompt">Hvad hedder den struktur, der blinker på kroppen?</p>${this.optsHtml(q.opts)}`;
} else if (q.t === 'dala') {
const s = S[q.target];
body = `<p class="q-prompt">${q.rev ? `Hvad er den danske betegnelse for <span class="la">${U.esc(s.names.la)}</span>?` : `Hvad er den latinske betegnelse for "${U.esc(s.names.da)}"?`}</p>${this.optsHtml(q.opts, !q.rev)}`;
} else if (q.t === 'muscle') {
const m = MOVES[q.move];
body = `<p class="q-prompt">${q.ant ? 'Hvilken muskel er antagonist' : 'Hvilken muskel er særlig central (agonist)'} ved ${U.esc(m.name.toLowerCase())}?</p>${this.optsHtml(q.opts)}`;
} else if (q.t === 'order') {
const p = PROCESSES.find(x => x.id === q.proc);
body = `<p class="q-prompt">${U.esc(p.name)}: sæt trinnene i den rigtige rækkefølge.</p><p class="muted" style="margin-top:-6px">Tryk på trinnene i rækkefølge.</p>
<div class="order-answer" id="qAns" aria-live="polite"></div><div class="q-opts" id="qPool">${q.shuffled.map(i => `<button data-oi="${i}">${U.esc(q.seq[i])}</button>`).join('')}</div>
<div class="ctrl"><button class="btn" id="qUndo">Fortryd</button><button class="btn primary" id="qCheck" disabled>Tjek rækkefølge</button></div>`;
}
U.$('#panelAct').innerHTML = `<div class="q-card">${meta}${body}<p class="q-feedback" id="qFb" aria-live="polite"></p><div class="ctrl"><button class="btn primary" id="qNext" hidden>Næste</button><button class="btn" id="qStop">Afslut quiz</button></div></div>`;
Layout.showTab('act');
U.$('#qNext').onclick = () => { this.i++; this.ask(); };
U.$('#qStop').onclick = () => { this.end(); this.render(); };
const ops = U.$('.q-opts:not(#qPool)'); if (ops) ops.onclick = e => { const b = e.target.closest('[data-id]'); if (b && !this.answered) this.answerMC(b); };
const show = U.$('#qShow'); if (show) show.onclick = () => this.reveal(false);
if (q.t === 'order') {
U.$('#qPool').onclick = e => { const b = e.target.closest('[data-oi]'); if (!b || this.answered) return; this.order.push(+b.dataset.oi); b.disabled = true; this.renderOrder(); };
U.$('#qUndo').onclick = () => { const i = this.order.pop(); if (i != null) U.$(`#qPool [data-oi="${i}"]`).disabled = false; this.renderOrder(); };
U.$('#qCheck').onclick = () => this.checkOrder();
}
},
optsHtml(opts, latin) { return `<div class="q-opts">${opts.map(o => `<button data-id="${o.id}" class="${latin ? 'la' : ''}">${U.esc(o.label)}</button>`).join('')}</div>`; },
renderOrder() { const q = this.qs[this.i]; U.$('#qAns').innerHTML = this.order.map((i, k) => `<span class="chip">${k + 1}. ${U.esc(q.seq[i])}</span>`).join(''); U.$('#qCheck').disabled = this.order.length !== q.seq.length; },
checkOrder() { const ok = this.order.every((v, k) => v === k); this.answered = true; this.feedback(ok, ok ? 'Rigtigt! Rækkefølgen er korrekt.' : 'Ikke helt. Korrekt rækkefølge: ' + this.qs[this.i].seq.join(' → ')); },
answerMC(b) {
const q = this.qs[this.i], ok = b.dataset.id === q.target; this.answered = true;
U.$$('.q-opts button').forEach(x => { x.disabled = true; if (x.dataset.id === q.target) x.classList.add('ok'); });
if (!ok) b.classList.add('bad');
const s = S[q.target];
this.feedback(ok, (ok ? 'Rigtigt! ' : 'Ikke helt. ') + `${s.names.da} – ${s.names.la}.`);
if (q.t === 'name') { Anatomy.clearClass('quiz-target'); Anatomy.setClass([q.target], ok ? 'quiz-ok' : 'quiz-bad'); }
},
onBodyClick(sid) {
if (!this.active) return false;
const q = this.qs[this.i];
if (!q || q.t !== 'find' || this.answered) { Toast.show(q && q.t !== 'find' ? 'Svar i panelet til højre' : 'Tryk på Næste'); return true; }
const hit = sid === q.target || ancestors(sid).includes(q.target);
this.tries++;
if (hit) { Anatomy.setClass([q.target], 'quiz-ok'); this.answered = true; this.feedback(true, `Rigtigt! ${S[q.target].names.da} (${S[q.target].names.la}).`, this.tries === 1); }
else { Anatomy.clearClass('quiz-bad'); Anatomy.setClass([sid], 'quiz-bad');
if (this.tries >= 2) this.reveal(true, sid); else { const fb = U.$('#qFb'); fb.className = 'q-feedback bad'; fb.textContent = `Det er ${S[sid].names.da}. Prøv igen.`; } }
return true;
},
reveal(afterTries, wrong) { const q = this.qs[this.i]; this.answered = true; Anatomy.setClass([q.target], 'quiz-target');
this.feedback(false, (wrong ? `Det var ${S[wrong].names.da}. ` : '') + `${S[q.target].names.da} blinker nu på kroppen.`); },
feedback(ok, msg, point = true) {
if (ok && point) this.score++;
const fb = U.$('#qFb'); fb.className = 'q-feedback ' + (ok ? 'ok' : 'bad'); fb.textContent = msg;
U.$('#qNext').hidden = false; U.$('#qNext').textContent = this.i + 1 >= this.qs.length ? 'Se resultat' : 'Næste'; U.$('#qNext').focus();
U.$('#hint').classList.remove('show'); U.$('.q-meta span:last-child').textContent = 'Point: ' + this.score;
},
finish() {
const pct = Math.round(100 * this.score / this.qs.length), best = Store.get('quizBest', {});
if (best[this.type] == null || pct > best[this.type]) { best[this.type] = pct; Store.set('quizBest', best); }
this.end();
U.$('#panelAct').innerHTML = `<div class="q-card"><p class="muted" style="margin:0">Resultat</p><p class="big-score">${this.score} / ${this.qs.length}</p>
<p>${pct >= 80 ? 'Flot! Du har godt styr på det.' : pct >= 50 ? 'Godt på vej. Markér de strukturer, du vil øve, med ☆.' : 'Øv dig lidt mere – brug søgning og informationspanelet.'}</p>
<div class="ctrl"><button class="btn primary" id="qAgain">Prøv igen</button><button class="btn" id="qMenu">Vælg quiztype</button></div></div>`;
U.$('#qAgain').onclick = () => this.start(); U.$('#qMenu').onclick = () => this.render();
},
end() { this.active = false; this.clear(); U.$('#body-svg').classList.remove('show-joints'); Anatomy.updateAria(); Anatomy.updateLabels(); Anatomy.updateCrumbs(); }
};
/* =====================================================================
TeacherModule
===================================================================== */
const Teacher = {
init() {
const t = State.teacher;
[['#tHide', 'hide'], ['#tLabels', 'labels'], ['#tBig', 'big']].forEach(([sel, k]) => {
const el = U.$(sel); el.checked = !!t[k];
el.addEventListener('change', () => { t[k] = el.checked; Store.set('teacher', t); this.apply(); });
});
this.apply();
},
apply() {
document.body.classList.toggle('big', !!State.teacher.big);
Info.revealed.clear(); Anatomy.updateAria(); Anatomy.updateLabels(); Anatomy.updateCrumbs(); Favorites.render();
if (Info.current) Info.show(Info.current);
}
};
/* =====================================================================
Layout – paneler, faner, modal, fuldskærm
===================================================================== */
const Layout = {
mobile: () => matchMedia('(max-width:900px)').matches,
showTab(which) {
const info = which === 'info';
U.$('#tabInfo').setAttribute('aria-selected', info); U.$('#tabAct').setAttribute('aria-selected', !info);
U.$('#panelInfo').hidden = !info; U.$('#panelAct').hidden = info;
},
openRight() { if (this.mobile()) { U.$('#rightPanel').classList.add('open'); U.$('#btnInfo').setAttribute('aria-expanded', 'true'); this.closeLeft(); } },
closeRight() { U.$('#rightPanel').classList.remove('open'); U.$('#btnInfo').setAttribute('aria-expanded', 'false'); },
closeLeft() { U.$('#leftPanel').classList.remove('open'); U.$('#btnMenu').setAttribute('aria-expanded', 'false'); },
init() {
const setH = () => document.documentElement.style.setProperty('--hdr', U.$('#hdr').offsetHeight + 'px'); setH(); addEventListener('resize', setH);
U.$('#btnMenu').onclick = () => { const o = U.$('#leftPanel').classList.toggle('open'); U.$('#btnMenu').setAttribute('aria-expanded', o); if (o) this.closeRight(); };
U.$('#btnInfo').onclick = () => { const o = U.$('#rightPanel').classList.toggle('open'); U.$('#btnInfo').setAttribute('aria-expanded', o); if (o) this.closeLeft(); };
U.$('#btnCloseRight').onclick = () => this.closeRight();
U.$('#tabInfo').onclick = () => this.showTab('info'); U.$('#tabAct').onclick = () => this.showTab('act');
const modal = U.$('#helpModal');
U.$('#btnHelp').onclick = () => { modal.classList.add('open'); U.$('#btnHelpClose').focus(); };
U.$('#btnHelpClose').onclick = () => modal.classList.remove('open');
modal.addEventListener('click', e => { if (e.target === modal) modal.classList.remove('open'); });
U.$('#btnFull').onclick = () => {
const d = document, el = d.documentElement;
try { if (d.fullscreenElement) d.exitFullscreen(); else if (el.requestFullscreen) el.requestFullscreen().catch(() => Toast.show('Fuldskærm er ikke tilladt i dette vindue')); else Toast.show('Fuldskærm understøttes ikke her'); }
catch (e) { Toast.show('Fuldskærm understøttes ikke her'); }
};
document.addEventListener('keydown', e => {
if (e.key === 'Escape') { modal.classList.remove('open'); this.closeLeft(); this.closeRight(); Search.close(); return; }
if (/^(INPUT|SELECT|TEXTAREA)$/.test(e.target.tagName)) return;
if (e.key === '/') { e.preventDefault(); U.$('#searchInput').focus(); }
else if (e.key === '+' || e.key === '=') Anatomy.zoomBy(1 / 1.25);
else if (e.key === '-') Anatomy.zoomBy(1.25);
else if (e.key === '0') Anatomy.resetZoom();
else if (e.key === 'v' || e.key === 'V') Anatomy.setView(State.view === 'front' ? 'back' : 'front');
});
}
};
/* =====================================================================
App – samler modulerne
===================================================================== */
const App = {
init() {
Lang.init(); Anatomy.init(); Info.bind(); Search.init(); Favorites.init(); Layout.init();
// navigation
U.$('#navModes').innerHTML = NAV.map(n => n.sep ? '<hr>' : `<button data-nav="${n.id}"><span class="dot" style="background:${n.color}"></span>${n.label}</button>`).join('');
U.$('#navModes').onclick = e => { const b = e.target.closest('[data-nav]'); if (b) { this.nav(b.dataset.nav); if (Layout.mobile()) { Layout.closeLeft(); if (NAV.find(n => n.id === b.dataset.nav).mode) Layout.openRight(); } } };
// lag
U.$('#layerToggles').innerHTML = LAYER_UI_ORDER.map(id => { const L = layerOf(id); return `<label><input type="checkbox" value="${id}"><span class="sw" style="background:${L.color}"></span>${L.da}</label>`; }).join('');
U.$('#layerToggles').onchange = e => { if (e.target.value) { Anatomy.toggleLayer(e.target.value, e.target.checked); this.markNav(null); } };
U.$('#explodeRange').oninput = e => Anatomy.setExplode(+e.target.value / 100);
U.$('#btnExplode').onclick = () => Anatomy.animateExplode(State.explode > 0.5 ? 0 : 1);
// visning og zoom
U.$('#vFront').onclick = () => Anatomy.setView('front'); U.$('#vBack').onclick = () => Anatomy.setView('back');
U.$('#zIn').onclick = () => Anatomy.zoomBy(1 / 1.25); U.$('#zOut').onclick = () => Anatomy.zoomBy(1.25); U.$('#zReset').onclick = () => Anatomy.resetZoom();
Bus.on('lang', () => { Anatomy.updateAria(); Anatomy.updateLabels(); Anatomy.updateCrumbs(); Favorites.render(); if (Info.current) Info.show(Info.current); if (State.mode === 'movement') Movement.renderBody(); });
Activity.init(); Teacher.init();
Anatomy.setView(State.view); Anatomy.applyLayers(); Anatomy.setExplode(0); Anatomy.updateAria(); Anatomy.updateCrumbs();
const pre = NAV.find(n => n.id === State.preset && !n.mode); if (pre) this.markNav(pre.id);
U.$('#body-svg').classList.toggle('show-joints', State.preset === 'joints');
Info.welcome();
},
markNav(id) { State.preset = id; if (id) Store.set('preset', id); U.$$('#navModes [data-nav]').forEach(b => b.setAttribute('aria-current', b.dataset.nav === id)); },
nav(id) {
const n = NAV.find(x => x.id === id); if (!n) return;
if (n.mode) { this.setMode(n.mode); this.markNav(id); return; }
this.setMode('explore'); Anatomy.setExplode(0); Anatomy.setLayers(n.layers);
U.$('#body-svg').classList.toggle('show-joints', !!n.joints);
this.markNav(id);
},
setMode(mode) {
if (State.mode === mode && mode !== 'explore') return;
if (State.mode === 'movement') Movement.stop();
if (State.mode === 'process') Process.stop();
if (State.mode === 'quiz') Quiz.end();
State.mode = mode;
const tab = U.$('#tabAct');
if (mode === 'explore') { tab.hidden = true; Layout.showTab('info'); return; }
tab.hidden = false; tab.textContent = { movement: 'Bevægelse', process: 'Proces', quiz: 'Quiz' }[mode];
Layout.showTab('act');
if (mode === 'movement') { Movement.render(); Movement.select(Movement.cur ? Movement.cur.id : MOVEMENTS[0].id); }
if (mode === 'process') { Process.render(); Process.select(Process.cur ? Process.cur.id : PROCESSES[0].id); }
if (mode === 'quiz') Quiz.render();
}
};
document.addEventListener('DOMContentLoaded', () => App.init());
</script>
</body>
</html>
Dette er Googles AI-baserede udviklingsmiljø til programmering.
<!DOCTYPE html>
<html lang="da">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>IdrætAnatomi & Fysiologi | Læreruddannelsen</title>
<style>
/* ==========================================================================
1. CSS VARIABLER & DESIGNTOKENS
========================================================================== */
:root {
--bg-primary: #0f172a;
--bg-secondary: #1e293b;
--bg-tertiary: #334155;
--bg-card: rgba(15, 23, 42, 0.85);
--bg-glass: rgba(30, 41, 59, 0.92);
--text-primary: #f8fafc;
--text-secondary: #94a3b8;
--text-muted: #64748b;
--border-color: #334155;
--border-focus: #38bdf8;
--accent-blue: #0284c7;
--accent-sky: #38bdf8;
--accent-emerald: #10b981;
--accent-amber: #f59e0b;
--accent-rose: #f43f5e;
--accent-purple: #8b5cf6;
--color-bone: #f8fafc;
--color-bone-stroke: #94a3b8;
--color-muscle: #e11d48;
--color-muscle-stroke: #9f1239;
--color-organ: #d97706;
--color-artery: #ef4444;
--color-vein: #3b82f6;
--color-nerve: #eab308;
--color-highlight: #facc15;
--header-height: 60px;
--footer-height: 38px;
--sidebar-width: 280px;
--panel-width: 360px;
--radius-sm: 6px;
--radius-md: 10px;
--radius-lg: 16px;
--radius-full: 9999px;
--shadow-sm: 0 1px 3px rgba(0,0,0,0.3);
--shadow-md: 0 4px 6px -1px rgba(0,0,0,0.4);
--shadow-lg: 0 10px 20px -3px rgba(0,0,0,0.5);
}
/* ==========================================================================
2. RESET & BASIS STYLING
========================================================================== */
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html, body {
width: 100%;
height: 100%;
min-height: 100vh;
overflow: hidden;
background-color: var(--bg-primary);
color: var(--text-primary);
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
font-size: 13px;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
button, input, select {
font-family: inherit;
font-size: inherit;
color: inherit;
border: none;
background: none;
outline: none;
}
button {
cursor: pointer;
user-select: none;
touch-action: manipulation;
}
/* ==========================================================================
3. APP HOVEDLAYOUT (ROBUST FLEXBOX)
========================================================================== */
#app-container {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
min-height: 100vh;
overflow: hidden;
position: relative;
}
/* TOPBJÆLKE / HEADER */
header#top-header {
height: var(--header-height);
min-height: var(--header-height);
background: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px;
gap: 10px;
z-index: 50;
flex-shrink: 0;
}
.brand-section {
display: flex;
align-items: center;
gap: 10px;
flex-shrink: 0;
}
.brand-logo {
width: 34px;
height: 34px;
border-radius: var(--radius-md);
background: linear-gradient(135deg, var(--accent-blue), var(--accent-emerald));
display: flex;
align-items: center;
justify-content: center;
font-weight: 800;
font-size: 16px;
color: white;
box-shadow: 0 0 10px rgba(2, 132, 199, 0.4);
}
.brand-title h1 {
font-size: 14px;
font-weight: 700;
color: var(--text-primary);
letter-spacing: -0.2px;
white-space: nowrap;
}
.brand-title span {
font-size: 10px;
color: var(--accent-sky);
display: block;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* Midterkontroller */
.header-center {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: nowrap;
}
.pill-group {
display: flex;
background: var(--bg-primary);
padding: 3px;
border-radius: var(--radius-full);
border: 1px solid var(--border-color);
flex-shrink: 0;
}
.pill-btn {
padding: 5px 11px;
border-radius: var(--radius-full);
font-size: 12px;
font-weight: 600;
color: var(--text-secondary);
transition: all 0.15s ease;
white-space: nowrap;
}
.pill-btn.active {
background: var(--accent-blue);
color: white;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
}
.pill-btn:hover:not(.active) {
color: var(--text-primary);
}
/* Sprogvælger */
.lang-select {
background: var(--bg-primary);
border: 1px solid var(--border-color);
color: var(--text-primary);
padding: 5px 10px;
border-radius: var(--radius-md);
font-size: 12px;
font-weight: 500;
cursor: pointer;
}
.lang-select option {
background: var(--bg-secondary);
color: var(--text-primary);
}
/* Fysiologisk intensitet switch */
.exertion-control {
display: flex;
align-items: center;
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: var(--radius-full);
padding: 2px;
flex-shrink: 0;
}
.exertion-btn {
padding: 4px 9px;
border-radius: var(--radius-full);
font-size: 11px;
font-weight: 600;
color: var(--text-muted);
transition: all 0.15s ease;
white-space: nowrap;
}
.exertion-btn.active[data-state="rest"] {
background: #059669;
color: white;
}
.exertion-btn.active[data-state="moderate"] {
background: var(--accent-amber);
color: #0f172a;
}
.exertion-btn.active[data-state="heavy"] {
background: var(--accent-rose);
color: white;
}
/* Højre side i header */
.header-right {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
.search-box {
position: relative;
width: 180px;
}
.search-box input {
width: 100%;
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
padding: 6px 28px 6px 10px;
font-size: 12px;
color: var(--text-primary);
}
.search-box svg {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
width: 14px;
height: 14px;
fill: none;
stroke: var(--text-muted);
stroke-width: 2;
}
.search-results-dropdown {
position: absolute;
top: calc(100% + 4px);
left: 0;
right: 0;
max-height: 320px;
overflow-y: auto;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
box-shadow: var(--shadow-lg);
z-index: 100;
display: none;
}
.search-item {
padding: 8px 10px;
border-bottom: 1px solid var(--border-color);
cursor: pointer;
}
.search-item:hover {
background: var(--bg-tertiary);
}
.search-item-title {
font-weight: 600;
font-size: 12px;
color: var(--text-primary);
}
.search-item-sub {
font-size: 10px;
color: var(--text-secondary);
}
.icon-btn {
width: 32px;
height: 32px;
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
color: var(--text-secondary);
background: var(--bg-primary);
border: 1px solid var(--border-color);
transition: all 0.15s ease;
}
.icon-btn:hover {
color: var(--text-primary);
border-color: var(--text-muted);
}
.icon-btn.active {
background: var(--accent-blue);
color: white;
border-color: var(--accent-blue);
}
.icon-btn svg {
width: 16px;
height: 16px;
stroke: currentColor;
fill: none;
stroke-width: 2;
}
/* ==========================================================================
4. MIDTERSTE ARBEJDSOMRÅDE (Flex layout: Venstre - Canvas - Højre)
========================================================================== */
.app-main-layout {
display: flex;
flex: 1;
min-height: 0;
width: 100%;
position: relative;
overflow: hidden;
}
/* VENSTRE SIDEBAR */
aside#left-sidebar {
width: var(--sidebar-width);
min-width: var(--sidebar-width);
height: 100%;
background: var(--bg-secondary);
border-right: 1px solid var(--border-color);
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 16px;
padding: 14px 12px;
z-index: 30;
flex-shrink: 0;
transition: transform 0.25s ease;
}
.sidebar-section-title {
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.8px;
color: var(--text-muted);
margin-bottom: 6px;
padding-left: 4px;
}
.nav-list {
list-style: none;
display: flex;
flex-direction: column;
gap: 2px;
}
.nav-item-btn {
width: 100%;
display: flex;
align-items: center;
gap: 9px;
padding: 8px 10px;
border-radius: var(--radius-md);
color: var(--text-secondary);
font-weight: 500;
font-size: 12px;
transition: all 0.15s ease;
text-align: left;
}
.nav-item-btn:hover {
background: var(--bg-tertiary);
color: var(--text-primary);
}
.nav-item-btn.active {
background: var(--accent-blue);
color: white;
font-weight: 600;
}
.nav-item-btn svg {
width: 16px;
height: 16px;
stroke: currentColor;
fill: none;
stroke-width: 2;
flex-shrink: 0;
}
.layers-container {
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
padding: 6px;
display: flex;
flex-direction: column;
gap: 4px;
}
.layer-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 5px 6px;
border-radius: var(--radius-sm);
background: var(--bg-secondary);
}
.layer-label {
display: flex;
align-items: center;
gap: 6px;
cursor: pointer;
font-size: 11px;
font-weight: 500;
flex: 1;
}
.layer-dot {
width: 8px;
height: 8px;
border-radius: var(--radius-full);
flex-shrink: 0;
}
.layer-opacity-slider {
width: 60px;
height: 4px;
accent-color: var(--accent-sky);
cursor: pointer;
margin-left: 6px;
}
/* CENTRALT LÆRRED / CANVAS */
main#canvas-container {
flex: 1;
height: 100%;
position: relative;
background: radial-gradient(circle at center, #1e293b 0%, #0f172a 100%);
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
.canvas-breadcrumbs {
position: absolute;
top: 12px;
left: 14px;
display: flex;
align-items: center;
gap: 6px;
background: var(--bg-glass);
backdrop-filter: blur(8px);
padding: 5px 12px;
border-radius: var(--radius-full);
border: 1px solid var(--border-color);
font-size: 11px;
font-weight: 500;
color: var(--text-secondary);
z-index: 10;
}
.canvas-breadcrumbs span.active {
color: var(--accent-sky);
font-weight: 600;
}
.canvas-toolbar {
position: absolute;
bottom: 16px;
left: 16px;
display: flex;
flex-direction: column;
gap: 6px;
background: var(--bg-glass);
backdrop-filter: blur(8px);
padding: 5px;
border-radius: var(--radius-md);
border: 1px solid var(--border-color);
z-index: 15;
}
.canvas-tool-btn {
width: 32px;
height: 32px;
border-radius: var(--radius-sm);
display: flex;
align-items: center;
justify-content: center;
background: var(--bg-secondary);
color: var(--text-secondary);
transition: all 0.15s ease;
}
.canvas-tool-btn:hover {
background: var(--bg-tertiary);
color: var(--text-primary);
}
.canvas-tool-btn svg {
width: 16px;
height: 16px;
stroke: currentColor;
fill: none;
stroke-width: 2;
}
.split-slider-container {
position: absolute;
bottom: 16px;
left: 50%;
transform: translateX(-50%);
background: var(--bg-glass);
backdrop-filter: blur(8px);
border: 1px solid var(--border-color);
padding: 6px 16px;
border-radius: var(--radius-full);
display: none;
align-items: center;
gap: 10px;
z-index: 20;
}
.split-slider-container span {
font-size: 11px;
font-weight: 600;
color: var(--accent-sky);
}
.split-slider-container input[type="range"] {
width: 160px;
accent-color: var(--accent-sky);
}
/* SVG VIEWPORT */
#svg-viewport {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
cursor: grab;
user-select: none;
}
#svg-viewport:active {
cursor: grabbing;
}
svg.anatomy-svg {
width: 100%;
height: 100%;
max-width: 100%;
max-height: 100%;
display: block;
}
.anatomical-part {
cursor: pointer;
transition: filter 0.2s ease, stroke-width 0.2s ease;
}
.anatomical-part:hover {
filter: drop-shadow(0 0 6px var(--accent-sky)) brightness(1.25);
}
.anatomical-part.selected {
stroke: var(--color-highlight) !important;
stroke-width: 3.5px !important;
filter: drop-shadow(0 0 8px rgba(250, 204, 21, 0.8)) brightness(1.3);
}
.anatomical-part.active-agonist {
stroke: #22c55e !important;
stroke-width: 3.5px !important;
fill: rgba(34, 197, 94, 0.4) !important;
}
.anatomical-part.active-antagonist {
stroke: #f97316 !important;
stroke-width: 3.5px !important;
fill: rgba(249, 115, 22, 0.4) !important;
}
#anatomy-tooltip {
position: absolute;
pointer-events: none;
background: var(--bg-glass);
backdrop-filter: blur(8px);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
padding: 6px 10px;
color: var(--text-primary);
font-size: 11px;
z-index: 100;
display: none;
flex-direction: column;
transform: translate(-50%, -120%);
}
.anatomy-label-pin {
pointer-events: none;
font-size: 10px;
font-weight: 600;
fill: #ffffff;
paint-order: stroke;
stroke: #0f172a;
stroke-width: 3px;
opacity: 0;
transition: opacity 0.3s ease;
}
body.show-teacher-labels .anatomy-label-pin {
opacity: 1;
}
/* HØJRE INFORMATIONSPANEL */
aside#right-panel {
width: var(--panel-width);
min-width: var(--panel-width);
height: 100%;
background: var(--bg-secondary);
border-left: 1px solid var(--border-color);
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 14px;
padding: 14px;
z-index: 30;
flex-shrink: 0;
transition: transform 0.25s ease;
}
.panel-empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 70%;
text-align: center;
color: var(--text-muted);
gap: 10px;
padding: 16px;
}
.panel-empty-state svg {
width: 42px;
height: 42px;
stroke: var(--text-muted);
opacity: 0.5;
}
.structure-header {
display: flex;
flex-direction: column;
gap: 4px;
border-bottom: 1px solid var(--border-color);
padding-bottom: 10px;
}
.structure-type-badge-row {
display: flex;
align-items: center;
justify-content: space-between;
}
.structure-type-badge {
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
padding: 2px 7px;
border-radius: var(--radius-sm);
background: var(--accent-blue);
color: white;
}
.btn-favorite {
display: flex;
align-items: center;
gap: 4px;
font-size: 11px;
color: var(--text-muted);
padding: 2px 6px;
border-radius: var(--radius-sm);
border: 1px solid var(--border-color);
}
.btn-favorite.is-favorite {
color: var(--accent-amber);
border-color: var(--accent-amber);
}
.btn-favorite svg {
width: 12px;
height: 12px;
fill: currentColor;
}
.structure-title-da {
font-size: 18px;
font-weight: 800;
color: var(--text-primary);
}
.structure-title-la {
font-size: 13px;
font-style: italic;
color: var(--accent-sky);
}
.structure-title-en {
font-size: 11px;
color: var(--text-secondary);
}
.info-card {
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
padding: 10px 12px;
display: flex;
flex-direction: column;
gap: 5px;
}
.info-card-title {
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
color: var(--text-muted);
display: flex;
align-items: center;
gap: 5px;
}
.info-card-title svg {
width: 13px;
height: 13px;
stroke: currentColor;
}
.info-card p {
font-size: 12px;
color: var(--text-secondary);
line-height: 1.45;
}
.info-card.didaktik-card {
border-left: 3px solid var(--accent-emerald);
background: linear-gradient(135deg, rgba(16, 185, 129, 0.05) 0%, rgba(15, 23, 42, 0.8) 100%);
}
.info-card.didaktik-card .info-card-title {
color: var(--accent-emerald);
}
.info-card.biomechanics-card {
border-left: 3px solid var(--accent-purple);
}
.info-card.biomechanics-card .info-card-title {
color: var(--accent-purple);
}
.related-tags {
display: flex;
flex-wrap: wrap;
gap: 5px;
margin-top: 3px;
}
.related-btn {
padding: 3px 8px;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
font-size: 11px;
color: var(--text-secondary);
transition: all 0.15s ease;
}
.related-btn:hover {
background: var(--accent-blue);
color: white;
}
/* OVERLAYS (Bevægelse, Forløb, Quiz) */
.overlay-panel {
position: absolute;
top: 12px;
right: 16px;
width: 360px;
max-height: calc(100% - 24px);
overflow-y: auto;
background: var(--bg-glass);
backdrop-filter: blur(12px);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-lg);
padding: 16px;
z-index: 35;
display: none;
flex-direction: column;
gap: 12px;
}
.overlay-header {
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid var(--border-color);
padding-bottom: 8px;
}
.overlay-title {
font-size: 14px;
font-weight: 700;
color: var(--text-primary);
display: flex;
align-items: center;
gap: 6px;
}
.overlay-close {
width: 24px;
height: 24px;
border-radius: var(--radius-sm);
display: flex;
align-items: center;
justify-content: center;
color: var(--text-muted);
font-size: 16px;
}
.overlay-close:hover {
color: var(--text-primary);
background: var(--bg-tertiary);
}
.movement-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 6px;
}
.movement-select-btn {
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
padding: 8px;
text-align: left;
font-size: 11px;
font-weight: 600;
color: var(--text-secondary);
display: flex;
flex-direction: column;
gap: 2px;
}
.movement-select-btn:hover {
background: var(--bg-tertiary);
color: var(--text-primary);
}
.movement-select-btn.active {
background: var(--accent-blue);
color: white;
border-color: var(--accent-blue);
}
.process-controller {
display: flex;
flex-direction: column;
gap: 8px;
background: var(--bg-primary);
padding: 10px;
border-radius: var(--radius-md);
border: 1px solid var(--border-color);
}
.process-playback-bar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 6px;
}
.btn-play {
background: var(--accent-emerald);
color: white;
padding: 5px 12px;
border-radius: var(--radius-full);
font-weight: 600;
font-size: 11px;
display: flex;
align-items: center;
gap: 5px;
}
.process-step-list {
display: flex;
flex-direction: column;
gap: 4px;
margin-top: 6px;
}
.process-step-item {
font-size: 11px;
padding: 5px 8px;
border-radius: var(--radius-sm);
background: var(--bg-secondary);
color: var(--text-secondary);
}
.process-step-item.active {
background: var(--accent-sky);
color: #0f172a;
font-weight: 700;
}
.quiz-options-list {
display: flex;
flex-direction: column;
gap: 6px;
}
.quiz-option-btn {
padding: 8px 10px;
border-radius: var(--radius-md);
background: var(--bg-primary);
border: 1px solid var(--border-color);
text-align: left;
font-size: 12px;
color: var(--text-primary);
}
.quiz-option-btn:hover {
background: var(--bg-tertiary);
}
.quiz-option-btn.correct {
background: rgba(16, 185, 129, 0.2);
border-color: #10b981;
color: #34d399;
font-weight: 700;
}
.quiz-option-btn.incorrect {
background: rgba(244, 63, 94, 0.2);
border-color: #f43f5e;
color: #fb7185;
}
.quiz-feedback {
padding: 8px 10px;
border-radius: var(--radius-md);
font-size: 11px;
line-height: 1.4;
display: none;
}
.quiz-feedback.show { display: block; }
.quiz-feedback.success {
background: rgba(16, 185, 129, 0.15);
border: 1px solid #10b981;
color: #34d399;
}
.quiz-feedback.failure {
background: rgba(244, 63, 94, 0.15);
border: 1px solid #f43f5e;
color: #fb7185;
}
.match-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6px;
}
.match-card {
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
padding: 7px 8px;
font-size: 11px;
font-weight: 600;
text-align: center;
cursor: pointer;
}
.match-card:hover { background: var(--bg-tertiary); }
.match-card.selected {
border-color: var(--accent-sky);
background: rgba(56, 189, 248, 0.15);
color: var(--accent-sky);
}
.match-card.matched {
background: rgba(16, 185, 129, 0.2);
border-color: #10b981;
color: #34d399;
opacity: 0.6;
}
/* BUNDSTATUSLINJE */
footer#bottom-bar {
height: var(--footer-height);
min-height: var(--footer-height);
background: var(--bg-secondary);
border-top: 1px solid var(--border-color);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px;
font-size: 11px;
color: var(--text-secondary);
z-index: 50;
flex-shrink: 0;
}
.metrics-group {
display: flex;
align-items: center;
gap: 16px;
}
.metric-item {
display: flex;
align-items: center;
gap: 5px;
}
.metric-item strong {
color: var(--text-primary);
font-weight: 600;
}
.heart-icon {
color: var(--accent-rose);
display: inline-block;
animation: heartbeat 1s infinite;
}
@keyframes heartbeat {
0%, 100% { transform: scale(1); }
15% { transform: scale(1.25); }
30% { transform: scale(1); }
45% { transform: scale(1.15); }
}
.sidebar-header-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.drawer-close-btn {
display: none;
width: 28px;
height: 28px;
border-radius: var(--radius-sm);
align-items: center;
justify-content: center;
color: var(--text-muted);
cursor: pointer;
font-size: 20px;
line-height: 1;
}
.drawer-close-btn:hover {
color: var(--text-primary);
background: var(--bg-tertiary);
}
.sidebar-backdrop {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(2px);
z-index: 35;
display: none;
opacity: 0;
transition: opacity 0.2s ease;
}
.sidebar-backdrop.active {
display: block;
opacity: 1;
}
/* ==========================================================================
5. RESPONSIVITET: MOBIL, TABLET & SMÅ SKÆRME (F.EKS. ONECOMPILER)
========================================================================== */
@media (max-width: 950px) {
.drawer-close-btn {
display: flex;
}
aside#left-sidebar {
position: absolute;
top: 0;
left: 0;
bottom: 0;
width: 280px;
max-width: 85vw;
transform: translateX(-100%);
box-shadow: 4px 0 24px rgba(0,0,0,0.6);
z-index: 40;
}
aside#left-sidebar.open {
transform: translateX(0);
}
aside#right-panel {
position: absolute;
top: 0;
right: 0;
bottom: 0;
width: 360px;
max-width: 85vw;
transform: translateX(100%);
box-shadow: -4px 0 24px rgba(0,0,0,0.6);
z-index: 40;
}
aside#right-panel.open {
transform: translateX(0);
}
.overlay-panel {
right: 8px;
left: 8px;
width: auto;
max-height: calc(100% - 16px);
z-index: 45;
}
.search-box {
display: none;
}
}
@media (max-width: 750px) {
.brand-title span {
display: none;
}
.exertion-control {
display: none;
}
}
@media (max-width: 580px) {
header#top-header {
padding: 0 6px;
gap: 6px;
}
.brand-title h1 {
display: none;
}
.pill-btn {
padding: 4px 7px;
font-size: 11px;
}
.lang-select {
padding: 4px 5px;
font-size: 11px;
}
.icon-btn {
width: 30px;
height: 30px;
}
.metric-item:nth-child(2), .metric-item:nth-child(3), .metric-item:nth-child(4) {
display: none;
}
}
</style>
</head>
<body>
<div id="app-container">
<!-- ==========================================================================
TOPBJÆLKE / HEADER
========================================================================== -->
<header id="top-header">
<div class="brand-section">
<button class="icon-btn" id="btn-toggle-sidebar" title="Vis/skjul navigationsmenu" aria-label="Menu">
<svg viewBox="0 0 24 24"><path d="M4 6h16M4 12h16M4 18h16"/></svg>
</button>
<div class="brand-logo">IA</div>
<div class="brand-title">
<h1>IdrætAnatomi</h1>
<span>Læreruddannelsen</span>
</div>
</div>
<!-- Visningsvælger (Forfra / Bagfra / Split) & Sprog -->
<div class="header-center">
<div class="pill-group" role="group" aria-label="Visningsvinkel">
<button class="pill-btn active" id="btn-view-anterior" data-view="anterior" title="Anterior visning">
Forfra
</button>
<button class="pill-btn" id="btn-view-posterior" data-view="posterior" title="Posterior visning">
Bagfra
</button>
<button class="pill-btn" id="btn-view-split" data-view="split" title="Eksploderet anatomisk lagvisning">
Split kroppen
</button>
</div>
<select id="lang-select" class="lang-select" title="Vælg sprogvisning" aria-label="Sprogvisning">
<option value="da">Dansk</option>
<option value="la">Latin</option>
<option value="en">English</option>
<option value="da-la" selected>Dansk + Latin</option>
<option value="da-en">Dansk + Engelsk</option>
<option value="all">Alle tre sprog</option>
</select>
<div class="exertion-control" role="group" aria-label="Idrætsfaglig belastningstilstand">
<button class="exertion-btn active" data-state="rest" title="Hviletilstand">Hvile</button>
<button class="exertion-btn" data-state="moderate" title="Moderat aerob træning">Moderat</button>
<button class="exertion-btn" data-state="heavy" title="Højintensiv anaerob belastning">Hård aktivitet</button>
</div>
</div>
<!-- Højre kontroller (Søgning, Underviser, Detaljer) -->
<div class="header-right">
<div class="search-box">
<input type="text" id="search-input" placeholder="Søg muskel, knogle..." aria-label="Søg i anatomi">
<svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
<div id="search-results" class="search-results-dropdown"></div>
</div>
<button class="icon-btn" id="btn-teacher-mode" title="Underviserfunktion (Vis alle labels)" aria-label="Undervisertilstand">
<svg viewBox="0 0 24 24"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg>
</button>
<button class="icon-btn" id="btn-show-favorites" title="Mine gemte fokusemner" aria-label="Favoritter">
<svg viewBox="0 0 24 24"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
</button>
<button class="icon-btn" id="btn-toggle-panel" title="Vis/skjul detaljepanel" aria-label="Detaljepanel">
<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>
</button>
</div>
</header>
<!-- ==========================================================================
HOVEDOMRÅDE (Flex layout: Venstre menu - Lærred - Højre panel)
========================================================================== -->
<div class="app-main-layout">
<div id="sidebar-backdrop" class="sidebar-backdrop"></div>
<!-- VENSTRE PANEL / NAVIGATION -->
<aside id="left-sidebar">
<div class="sidebar-header-row">
<div class="sidebar-section-title" style="margin-bottom:0;">Navigation & Lag</div>
<button class="drawer-close-btn" id="btn-close-sidebar" aria-label="Luk menu">×</button>
</div>
<div>
<div class="sidebar-section-title">Anatomiske Systemer</div>
<ul class="nav-list">
<li>
<button class="nav-item-btn active" data-nav="all">
<svg viewBox="0 0 24 24"><circle cx="12" cy="5" r="3"/><path d="M12 8v13M9 13l3-2 3 2M9 21l3-4 3 4"/></svg>
<span>Hele kroppen</span>
</button>
</li>
<li>
<button class="nav-item-btn" data-nav="skeleton">
<svg viewBox="0 0 24 24"><path d="M12 2a4 4 0 0 0-4 4c0 3 4 5 4 5s4-2 4-5a4 4 0 0 0-4-4zM8 14h8M9 18h6M12 11v11"/></svg>
<span>Skelet & Knogler</span>
</button>
</li>
<li>
<button class="nav-item-btn" data-nav="muscles">
<svg viewBox="0 0 24 24"><path d="M6 16.5l-3-3 6-6 3 3M18 7.5l3 3-6 6-3-3M8.5 14l7-7"/></svg>
<span>Muskelsystem</span>
</button>
</li>
<li>
<button class="nav-item-btn" data-nav="organs">
<svg viewBox="0 0 24 24"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>
<span>Organer</span>
</button>
</li>
<li>
<button class="nav-item-btn" data-nav="circulation">
<svg viewBox="0 0 24 24"><path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>
<span>Kredsløb & Kar</span>
</button>
</li>
<li>
<button class="nav-item-btn" data-nav="nervous">
<svg viewBox="0 0 24 24"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg>
<span>Nervesystem</span>
</button>
</li>
</ul>
</div>
<div>
<div class="sidebar-section-title">Idrætsundervisning</div>
<ul class="nav-list">
<li>
<button class="nav-item-btn" id="btn-open-movement" data-overlay="movement">
<svg viewBox="0 0 24 24"><circle cx="18" cy="5" r="3"/><path d="M13 8.5l-4 4 3 4M5 19l4-4 2 2 3-3"/></svg>
<span>Bevægelsesmodul</span>
</button>
</li>
<li>
<button class="nav-item-btn" id="btn-open-processes" data-overlay="processes">
<svg viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21 5 3"/></svg>
<span>Fysiologiske Forløb</span>
</button>
</li>
<li>
<button class="nav-item-btn" id="btn-open-quiz" data-overlay="quiz">
<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3M12 17h.01"/></svg>
<span>Træn din viden (Quiz)</span>
</button>
</li>
</ul>
</div>
<div>
<div class="sidebar-section-title">Anatomiske Lag</div>
<div class="layers-container" id="layers-control">
<div class="layer-row">
<label class="layer-label">
<span class="layer-dot" style="background:#94a3b8;"></span>
<span>Ydre krop</span>
</label>
<input type="checkbox" id="layer-check-silhouette" checked data-layer="silhouette">
<input type="range" class="layer-opacity-slider" min="0" max="1" step="0.05" value="0.4" data-layer-opacity="silhouette" title="Gennemsigtighed">
</div>
<div class="layer-row">
<label class="layer-label">
<span class="layer-dot" style="background:var(--color-bone);"></span>
<span>Skelet</span>
</label>
<input type="checkbox" id="layer-check-skeleton" checked data-layer="skeleton">
<input type="range" class="layer-opacity-slider" min="0" max="1" step="0.05" value="1" data-layer-opacity="skeleton">
</div>
<div class="layer-row">
<label class="layer-label">
<span class="layer-dot" style="background:var(--color-muscle);"></span>
<span>Muskler</span>
</label>
<input type="checkbox" id="layer-check-muscles" checked data-layer="muscles">
<input type="range" class="layer-opacity-slider" min="0" max="1" step="0.05" value="0.9" data-layer-opacity="muscles">
</div>
<div class="layer-row">
<label class="layer-label">
<span class="layer-dot" style="background:var(--color-organ);"></span>
<span>Organer</span>
</label>
<input type="checkbox" id="layer-check-organs" checked data-layer="organs">
<input type="range" class="layer-opacity-slider" min="0" max="1" step="0.05" value="0.95" data-layer-opacity="organs">
</div>
<div class="layer-row">
<label class="layer-label">
<span class="layer-dot" style="background:var(--color-artery);"></span>
<span>Kredsløb</span>
</label>
<input type="checkbox" id="layer-check-circulation" checked data-layer="circulation">
<input type="range" class="layer-opacity-slider" min="0" max="1" step="0.05" value="0.85" data-layer-opacity="circulation">
</div>
<div class="layer-row">
<label class="layer-label">
<span class="layer-dot" style="background:var(--color-nerve);"></span>
<span>Nervesystem</span>
</label>
<input type="checkbox" id="layer-check-nervous" checked data-layer="nervous">
<input type="range" class="layer-opacity-slider" min="0" max="1" step="0.05" value="0.8" data-layer-opacity="nervous">
</div>
</div>
</div>
</aside>
<!-- CENTRALT LÆRRED MED SVG -->
<main id="canvas-container">
<div class="canvas-breadcrumbs" id="breadcrumbs">
<span>Menneskekrop</span>
<span>/</span>
<span class="active" id="breadcrumb-current">Hele kroppen</span>
</div>
<div class="canvas-toolbar">
<button class="canvas-tool-btn" id="btn-zoom-in" title="Zoom ind"><svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><line x1="11" y1="8" x2="11" y2="14"/><line x1="8" y1="11" x2="14" y2="11"/></svg></button>
<button class="canvas-tool-btn" id="btn-zoom-out" title="Zoom ud"><svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><line x1="8" y1="11" x2="14" y2="11"/></svg></button>
<button class="canvas-tool-btn" id="btn-zoom-reset" title="Nulstil zoom"><svg viewBox="0 0 24 24"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg></button>
</div>
<div class="split-slider-container" id="split-slider-wrap">
<span>Lagafstand:</span>
<input type="range" id="split-slider" min="50" max="220" value="130" title="Træk lagene fra hinanden">
</div>
<div id="anatomy-tooltip">
<div class="tt-type" style="font-size:9px; color:var(--text-muted); text-transform:uppercase;">Knogle</div>
<div class="tt-name-da" style="font-weight:700; color:var(--text-primary);">Lårben</div>
<div class="tt-name-la" style="font-style:italic; color:var(--accent-sky); font-size:10px;">Os femoris</div>
</div>
<div id="svg-viewport">
<svg id="anatomy-svg" class="anatomy-svg" viewBox="0 0 600 1000" preserveAspectRatio="xMidYMid meet" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="grad-muscle" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#fb7185" />
<stop offset="50%" stop-color="#e11d48" />
<stop offset="100%" stop-color="#9f1239" />
</linearGradient>
<linearGradient id="grad-bone" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#ffffff" />
<stop offset="100%" stop-color="#cbd5e1" />
</linearGradient>
<linearGradient id="grad-heart" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#ef4444" />
<stop offset="100%" stop-color="#991b1b" />
</linearGradient>
<linearGradient id="grad-lungs" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#67e8f9" />
<stop offset="100%" stop-color="#0891b2" />
</linearGradient>
<linearGradient id="grad-liver" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#d97706" />
<stop offset="100%" stop-color="#78350f" />
</linearGradient>
<linearGradient id="grad-digestive" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#f59e0b" />
<stop offset="100%" stop-color="#b45309" />
</linearGradient>
</defs>
<g id="svg-transform-root">
<!-- Anatomiske lag genereres straks af AppCore -->
</g>
</svg>
</div>
<!-- OVERLAY 1: BEVÆGELSESMODUL -->
<div class="overlay-panel" id="overlay-movement">
<div class="overlay-header">
<div class="overlay-title">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="18" cy="5" r="3"/><path d="M13 8.5l-4 4 3 4M5 19l4-4 2 2 3-3"/></svg>
<span>Biomekanik & Bevægelse</span>
</div>
<button class="overlay-close" data-close="movement" title="Luk">×</button>
</div>
<p style="font-size:11px; color:var(--text-secondary);">
Vælg en bevægelse for at se samspil mellem <strong>agonister</strong> (motorer) og <strong>antagonister</strong>:
</p>
<div class="movement-grid" id="movement-button-list"></div>
<div id="movement-details-card" class="info-card didaktik-card" style="display:none;">
<div class="info-card-title">Analyse af bevægelse</div>
<div id="movement-active-name" style="font-weight:700; font-size:13px; color:white;"></div>
<div style="font-size:11px; margin-top:3px;">
<div><strong style="color:#22c55e;">Agonist:</strong> <span id="movement-agonist-desc"></span></div>
<div style="margin-top:2px;"><strong style="color:#f97316;">Antagonist:</strong> <span id="movement-antagonist-desc"></span></div>
<div style="margin-top:2px;"><strong style="color:var(--accent-sky);">Led:</strong> <span id="movement-joint-desc"></span></div>
</div>
<div id="movement-didactic-text" style="font-size:11px; color:var(--text-secondary); margin-top:6px;"></div>
</div>
</div>
<!-- OVERLAY 2: FYSIOLOGISKE FORLØB -->
<div class="overlay-panel" id="overlay-processes">
<div class="overlay-header">
<div class="overlay-title">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="5 3 19 12 5 21 5 3"/></svg>
<span>Fysiologiske Forløb</span>
</div>
<button class="overlay-close" data-close="processes" title="Luk">×</button>
</div>
<div class="pill-group" style="width:100%;">
<button class="pill-btn active" style="flex:1;" data-process="oxygen">Iltens vej</button>
<button class="pill-btn" style="flex:1;" data-process="blood">Blodets vej</button>
<button class="pill-btn" style="flex:1;" data-process="digestion">Madens vej</button>
</div>
<div class="process-controller">
<div class="process-playback-bar">
<button class="btn-play" id="btn-process-play">
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg>
<span id="btn-process-play-text">Start forløb</span>
</button>
<button class="pill-btn" id="btn-process-reset">Nulstil</button>
<select id="process-speed" class="lang-select" style="padding:2px 6px; font-size:10px;">
<option value="0.5">0.5x</option>
<option value="1" selected>1.0x</option>
<option value="2">2.0x</option>
</select>
</div>
<div id="process-explanation" style="font-size:11px; line-height:1.4; color:var(--text-primary);">
Tryk på 'Start forløb' for at følge processen gennem kroppens anatomiske strukturer.
</div>
<div class="process-step-list" id="process-step-indicators"></div>
</div>
</div>
<!-- OVERLAY 3: TRÆN DIN VIDEN (QUIZ MODUL) -->
<div class="overlay-panel" id="overlay-quiz">
<div class="overlay-header">
<div class="overlay-title">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3M12 17h.01"/></svg>
<span>Træn din viden</span>
</div>
<button class="overlay-close" data-close="quiz" title="Luk">×</button>
</div>
<div class="pill-group" style="width:100%; flex-wrap:wrap;">
<button class="pill-btn active" data-quiztype="find" style="flex:1;">Find på kroppen</button>
<button class="pill-btn" data-quiztype="name" style="flex:1;">Hvad hedder den?</button>
<button class="pill-btn" data-quiztype="match" style="flex:1;">Dansk ↔ Latin</button>
<button class="pill-btn" data-quiztype="movement" style="flex:1;">Muskel & Bevægelse</button>
</div>
<div id="quiz-question-box" class="info-card" style="margin-top:4px;">
<div class="info-card-title" id="quiz-mode-title">Opgave</div>
<p id="quiz-question-text" style="font-weight:600; font-size:13px; color:var(--text-primary);"></p>
<div id="quiz-instruction" style="font-size:10px; color:var(--text-muted);"></div>
</div>
<div id="quiz-interaction-area"></div>
<div id="quiz-feedback-box" class="quiz-feedback"></div>
<div style="display:flex; justify-content:space-between; align-items:center; margin-top:6px;">
<span style="font-size:11px; color:var(--text-secondary);">Score: <strong id="quiz-score">0</strong> rigtige</span>
<button class="pill-btn" id="btn-next-question" style="background:var(--accent-blue); color:white;">Næste →</button>
</div>
</div>
</main>
<!-- HØJRE INFORMATIONSPANEL -->
<aside id="right-panel">
<div class="sidebar-header-row" style="padding-bottom:6px; border-bottom:1px solid var(--border-color);">
<div class="sidebar-section-title" style="margin-bottom:0;">Anatomisk Detaljepanel</div>
<button class="drawer-close-btn" id="btn-close-panel" aria-label="Luk detaljepanel">×</button>
</div>
<div class="panel-empty-state" id="panel-empty">
<svg viewBox="0 0 24 24" fill="none" stroke-width="1.5">
<circle cx="12" cy="12" r="10"/><path d="M12 16v-4M12 8h.01"/>
</svg>
<h3 style="font-size:14px; color:var(--text-secondary);">Udforsk kroppen</h3>
<p style="font-size:12px;">Klik på en muskel eller knogle på figuren for at se biomekanik, udspring/hæfte og idrætsdidaktik.</p>
<button class="pill-btn" id="btn-demo-select" style="border:1px solid var(--border-color); margin-top:8px;">Vælg Lårben (Femur)</button>
</div>
<div class="panel-content" id="panel-active-content" style="display:none; flex-direction:column; gap:12px;">
<div class="structure-header">
<div class="structure-type-badge-row">
<span class="structure-type-badge" id="info-type">Knogle</span>
<button class="btn-favorite" id="btn-toggle-fav">
<svg viewBox="0 0 24 24"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
<span id="btn-fav-text">Fokusemne</span>
</button>
</div>
<div style="margin-top:4px;">
<div class="structure-title-da" id="info-name-da">Lårben</div>
<div class="structure-title-la" id="info-name-la">Os femoris / Femur</div>
<div class="structure-title-en" id="info-name-en">Thigh bone / Femur</div>
</div>
</div>
<div class="info-card">
<div class="info-card-title">
<svg viewBox="0 0 24 24" fill="none" stroke-width="2"><circle cx="12" cy="10" r="3"/><path d="M12 2a8 8 0 0 0-8 8c0 5 8 12 8 12s8-7 8-12a8 8 0 0 0-8-8z"/></svg>
Placering & Opbygning
</div>
<p id="info-location"></p>
</div>
<div class="info-card biomechanics-card">
<div class="info-card-title">
<svg viewBox="0 0 24 24" fill="none" stroke-width="2"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg>
Funktion & Biomekanik
</div>
<p id="info-function"></p>
</div>
<div class="info-card" id="info-muscle-attachments-card" style="display:none;">
<div class="info-card-title">Udspring & Hæfte (Origo & Insertio)</div>
<p id="info-attachments"></p>
</div>
<div class="info-card didaktik-card">
<div class="info-card-title">
<svg viewBox="0 0 24 24" fill="none" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
Idrætsrelevans & Skadesforebyggelse
</div>
<p id="info-didactic"></p>
</div>
<div class="info-card">
<div class="info-card-title">Relaterede strukturer</div>
<div class="related-tags" id="info-related-list"></div>
</div>
</div>
</aside>
</div>
<!-- ==========================================================================
BUNDSTATUSLINJE / FYSIOLOGI METRICS
========================================================================== -->
<footer id="bottom-bar">
<div class="metrics-group">
<div class="metric-item" title="Aktuel hjertefrekvens">
<span class="heart-icon">♥</span>
<span>Puls:</span>
<strong id="metric-hr">65 bpm</strong>
</div>
<div class="metric-item" title="Respirationsfrekvens">
<span>Vejrtrækning:</span>
<strong id="metric-rf">12 /min</strong>
</div>
<div class="metric-item" title="Minutvolumen">
<span>Minutvolumen (Q):</span>
<strong id="metric-q">5.0 L/min</strong>
</div>
<div class="metric-item" title="Blod til arbejdende muskler">
<span>Muskelgennemstrømning:</span>
<strong id="metric-muscleflow" style="color:var(--accent-emerald);">~18%</strong>
</div>
</div>
<div style="font-size:10px; color:var(--text-muted); font-style:italic;">
* Fysiologiske tal er didaktiske modeller til læreruddannelsen.
</div>
</footer>
</div>
<!-- ==========================================================================
JAVASCRIPT LOGIK & MODULER
========================================================================== -->
<script>
const AnatomyData = {
bones: {
cranium: {
id: "cranium", type: "bone",
names: { da: "Kranium", la: "Cranium", en: "Skull" },
location: "Hovedets skeletstruktur bestående af hjernekassen og ansigtsskelettet.",
function: "Beskytter hjernen og sanseorganer mod stød og traumer. Danner fæste for tyggemuskulatur og halsmuskler.",
didactic: "Hjernerystelser (commotio cerebri) er en central risiko i kontaktidrætter. I skolen er det essentielt at undervise i 'Graduated Return to Play' samt beskyttelse af hovedet.",
related: ["brain", "cervical_spine", "trapezius"]
},
cervical_spine: {
id: "cervical_spine", type: "bone",
names: { da: "Halshvirvler", la: "Vertebrae cervicales (C1-C7)", en: "Cervical spine" },
location: "Øverste del af rygsøjlen mellem kranium og brysthvirvler.",
function: "Høj bevægelighed (fleksion, ekstension, lateral fleksion, rotation). Bærer hovedets vægt via atlas (C1) og axis (C2).",
didactic: "Vigtigt i gymnastik, spring og hovedspring i svømning. Korrekt nakkestabilitet og opvarmning modvirker spændingshovedpine.",
related: ["cranium", "thoracic_spine", "trapezius"]
},
thoracic_spine: {
id: "thoracic_spine", type: "bone",
names: { da: "Brysthvirvler", la: "Vertebrae thoracicae (Th1-Th12)", en: "Thoracic spine" },
location: "Midterste sektion af rygsøjlen, forbundet med ribbenene.",
function: "Artikulerer med ribben og danner brystkassen. Begrænset ekstension, men tillader rotation.",
didactic: "Thoracal mobilitet er forudsætning for god kasteteknik (håndbold, spydkast) og korrekt kropsholdning.",
related: ["ribs", "lumbar_spine", "latissimus_dorsi"]
},
lumbar_spine: {
id: "lumbar_spine", type: "bone",
names: { da: "Lændehvirvler", la: "Vertebrae lumbales (L1-L5)", en: "Lumbar spine" },
location: "Nederste del af den bevægelige rygsøjle mellem brysthvirvler og korsben.",
function: "Bærer den største del af kropsvægten. Solide hvirvellegemer med kraftige disci.",
didactic: "Central i dødløft, squat og landingsteknik. Neutral lænderyg minimerer risikoen for overbelastning.",
related: ["pelvis", "thoracic_spine", "rectus_abdominis"]
},
clavicle: {
id: "clavicle", type: "bone",
names: { da: "Kraveben", la: "Clavicula", en: "Clavicle / Collarbone" },
location: "S-formet knogle mellem brystbenet (sternum) og skulderbladets acromion.",
function: "Fungerer som en mekanisk afstiver, der holder skulderleddet ude fra kroppen for maksimal bevægelsesfrihed.",
didactic: "En af de hyppigst brækkede knogler hos børn og unge ved fald på udstrakt arm (cykling, rulleskøjter, fodbold).",
related: ["sternum", "scapula", "deltoideus", "pectoralis_major"]
},
scapula: {
id: "scapula", type: "bone",
names: { da: "Skulderblad", la: "Scapula", en: "Shoulder blade" },
location: "Flad trekantet knogle på bagsiden af brystkassen.",
function: "Danner ledskål (cavitas glenoidalis) for overarmen. Bevæger sig frit over brystkassen.",
didactic: "Scapulær kontrol ('skulderbladsstabilitet') er afgørende for at undgå impingement i kaste- og svømmeskuldre.",
related: ["clavicle", "humerus", "trapezius", "deltoideus", "latissimus_dorsi"]
},
sternum: {
id: "sternum", type: "bone",
names: { da: "Brystben", la: "Sternum", en: "Breastbone / Sternum" },
location: "Flad knogle midt foran på brystkassen.",
function: "Hæfte for kraveben og de øverste ribbensbruske. Beskytter hjerte og lunger.",
didactic: "Landmærke ved førstehjælp og hjertemassage (HLR i idrætsundervisning: trykpunkt midt på sternum).",
related: ["ribs", "clavicle", "pectoralis_major", "heart"]
},
ribs: {
id: "ribs", type: "bone",
names: { da: "Ribben", la: "Costae (1-12)", en: "Ribs" },
location: "12 par buede knogler der omslutter brysthulen.",
function: "Beskytter vitale organer. Bevæges af åndedrætsmuskler under inspiration og eksspiration.",
didactic: "Ved hård aerob aktivitet øges ribbensbevægelserne markant for at ventilere store mængder luft (op til 120-180 L/min).",
related: ["sternum", "thoracic_spine", "lungs", "heart"]
},
pelvis: {
id: "pelvis", type: "bone",
names: { da: "Bækken", la: "Pelvis / Os coxae", en: "Pelvis" },
location: "Knoglering bestående af hoftebenene og korsbenet.",
function: "Overfører kroppens vægt fra rygsøjlen til benene. Danner ledskål for hofteleddet.",
didactic: "Bækkenstilling (anterior/posterior bækkenkip) påvirker direkte kropsholdning, baglårsspænding og løbeøkonomi.",
related: ["femur", "lumbar_spine", "gluteus_maximus", "rectus_abdominis"]
},
humerus: {
id: "humerus", type: "bone",
names: { da: "Overarmsknogle", la: "Humerus", en: "Humerus" },
location: "Knogle i overarmen mellem skulderled og albueled.",
function: "Løftestang ved kast, skub, træk og svømmetag. Kugleleddet giver kroppens største bevægelsesudslag.",
didactic: "Kastelængde afhænger biomekanisk af drejningsmoment og vægtstangsarme gennem humerus og underarm.",
related: ["scapula", "radius", "ulna", "biceps", "triceps", "deltoideus"]
},
radius: {
id: "radius", type: "bone",
names: { da: "Spoleben", la: "Radius", en: "Radius" },
location: "Knogle på tommelfingersiden af underarmen.",
function: "Roterer omkring ulna ved pronation og supination (fx ved ketchersport og skruede kast).",
didactic: "Distal radiusfraktur er den klassiske faldskade ved fald fremad på udstrakt hånd.",
related: ["ulna", "humerus", "hand_bones", "biceps"]
},
ulna: {
id: "ulna", type: "bone",
names: { da: "Albueben", la: "Ulna", en: "Ulna" },
location: "Knogle på lillefingersiden af underarmen, danner albuens spids (olecranon).",
function: "Stabilt hængselled med humerus, der tillader fleksion og ekstension af armen.",
didactic: "Olecranon fungerer som vægtstangsfæste for m. triceps brachii under armstrækkere.",
related: ["radius", "humerus", "triceps"]
},
hand_bones: {
id: "hand_bones", type: "bone",
names: { da: "Håndens knogler", la: "Ossa manus", en: "Hand bones" },
location: "Håndrod, mellemhånd og fingre.",
function: "Præcisionsgreb og kraftgreb ved boldspil, klatring og gymnastik.",
didactic: "Grebstyrke er en stærk indikator for samlet muskelstyrke. Fingerforstuvninger er hyppige i skolen.",
related: ["radius", "ulna"]
},
femur: {
id: "femur", type: "bone",
names: { da: "Lårben", la: "Os femoris / Femur", en: "Femur / Thigh bone" },
location: "Kroppens længste og stærkeste rørknogle, placeret i låret.",
function: "Bærer kroppens samlede vægt under løb, hop og stød (tåler belastninger op til flere gange kropsvægten).",
didactic: "Aksen mellem hofte og knæ (Q-vinkel) har betydning for knæstabilitet og risiko for korsbåndsskader.",
related: ["pelvis", "patella", "tibia", "quadriceps", "hamstrings", "gluteus_maximus"]
},
patella: {
id: "patella", type: "bone",
names: { da: "Knæskal", la: "Patella", en: "Kneecap / Patella" },
location: "Sesamknogle indlejret i quadriceps-senen foran knæleddet.",
function: "Forøger vægtstangsarmen for m. quadriceps femoris med ca. 30%, hvilket markant øger kraften ved ekstension.",
didactic: "Springerknæ (patellar tendinopati) ses hyppigt ved megen spring- og løbetræning.",
related: ["femur", "tibia", "quadriceps"]
},
tibia: {
id: "tibia", type: "bone",
names: { da: "Skinneben", la: "Tibia", en: "Shinbone / Tibia" },
location: "Store indvendige knogle i underbenet.",
function: "Primære vægtbærende knogle i underbenet. Danner sammen med femur knæleddet.",
didactic: "Skinnebensbetændelse er en klassisk overbelastningsskade ved løb på hårdt underlag og for hurtig progression.",
related: ["femur", "fibula", "patella", "foot_bones", "gastrocnemius", "tibialis_anterior"]
},
fibula: {
id: "fibula", type: "bone",
names: { da: "Lægben", la: "Fibula", en: "Calf bone / Fibula" },
location: "Tynd knogle på ydersiden af underbenet.",
function: "Ikke vægtbærende, danner ydre ankelkno (malleolus lateralis) og muskelhæfte.",
didactic: "Vigtig for ankelstabilitet. Ved forstuvninger belastes ledbåndene omkring den laterale malleol.",
related: ["tibia", "foot_bones"]
},
foot_bones: {
id: "foot_bones", type: "bone",
names: { da: "Fodens knogler", la: "Ossa pedis", en: "Foot bones" },
location: "Fodrod, mellemfod og tæer, danner fodens støddæmpende buer.",
function: "Støddæmper ved landing og stiv vægtstang under afsæt i løb og hop.",
didactic: "Fodens pronation og supinering under løb. Styrkelse af fodmuskler forebygger skader.",
related: ["tibia", "fibula", "gastrocnemius"]
}
},
muscles: {
deltoideus: {
id: "deltoideus", type: "muscle",
names: { da: "Deltamusklen / Skuldermusklen", la: "Musculus deltoideus", en: "Deltoid" },
location: "Omslutter skulderleddet på forsiden, ydersiden og bagsiden.",
attachments: "Origo: Clavicula, acromion og spina scapulae. Insertio: Tuberositas deltoidea på humerus.",
function: "Abduktion af armen (over 15-90 grader), samt fleksion og ekstension.",
didactic: "Aktiv ved volleyball-smash, håndboldskud, crawl i svømning og klatring.",
related: ["humerus", "scapula", "clavicle", "pectoralis_major", "trapezius"]
},
biceps: {
id: "biceps", type: "muscle",
names: { da: "Den to-hovedede armbøjer", la: "Musculus biceps brachii", en: "Biceps" },
location: "Forsiden af overarmen.",
attachments: "Origo: Scapula (processus coracoideus & tuberculum supraglenoidale). Insertio: Tuberositas radii.",
function: "Fleksion i albueleddet og supination af underarmen (dreje håndfladen opad).",
didactic: "Trænes ved chin-ups, klatring og roning. Agonist ved trækbevægelser.",
related: ["humerus", "radius", "triceps"]
},
triceps: {
id: "triceps", type: "muscle",
names: { da: "Den tre-hovedede armstrækker", la: "Musculus triceps brachii", en: "Triceps" },
location: "Bagsiden af overarmen.",
attachments: "Origo: Scapula og humerus. Insertio: Olecranon på ulna.",
function: "Ekstension af albueleddet. Agonist i armstrækkere og kastebevægelsens slutfase.",
didactic: "Hovedmuskel i push-ups, bænkpres og slutfasen af kast og svømmetag.",
related: ["humerus", "ulna", "scapula", "biceps"]
},
pectoralis_major: {
id: "pectoralis_major", type: "muscle",
names: { da: "Den store brystmuskel", la: "Musculus pectoralis major", en: "Chest muscle / Pectoralis major" },
location: "Brystkassens forside.",
attachments: "Origo: Clavicula, sternum og ribben 1-6. Insertio: Humerus.",
function: "Horisontal adduktion, indadrotation og fleksion af overarmen.",
didactic: "Central i bænkpres, armbøjninger, brystsvømning og tackling i rugby.",
related: ["sternum", "clavicle", "humerus", "deltoideus"]
},
trapezius: {
id: "trapezius", type: "muscle",
names: { da: "Kappemusklen", la: "Musculus trapezius", en: "Trapezius" },
location: "Nakken og den øvre del af ryggen.",
attachments: "Origo: Kranium og torntappe C1-Th12. Insertio: Clavicula og scapula.",
function: "Elevation (løfte skuldre), retraktion (samle skulderblade) og stabilisering.",
didactic: "Modvirker fremadglidende skuldre. Svag nedre trapezius ses ofte hos elever ved megen skærmtid.",
related: ["cranium", "scapula", "thoracic_spine", "cervical_spine"]
},
latissimus_dorsi: {
id: "latissimus_dorsi", type: "muscle",
names: { da: "Den brede rygmuskel", la: "Musculus latissimus dorsi", en: "Latissimus dorsi" },
location: "Den nedre og laterale del af ryggen.",
attachments: "Origo: Torntappe Th7-L5, thoracolumbalfascien, crista iliaca. Insertio: Humerus.",
function: "Adduktion, ekstension og indadrotation af overarmen (kraftigt træk nedad/bagud).",
didactic: "Kroppens 'svømme- og klatremuskel'. Hovedmotor i pull-ups og crawl.",
related: ["humerus", "pelvis", "thoracic_spine", "lumbar_spine"]
},
rectus_abdominis: {
id: "rectus_abdominis", type: "muscle",
names: { da: "Den lige bugmuskel / Mavemusklen", la: "Musculus rectus abdominis", en: "Abs / Rectus abdominis" },
location: "Midt foran på maven fra ribben til skamben.",
attachments: "Origo: Ribbensbrusk 5-7 og sternum. Insertio: Symphysis pubica.",
function: "Fleksion af rygsøjlen og intraabdominalt tryk til stabilisering af truncus.",
didactic: "Essentiel for kraftoverførsel mellem over- og underkrop ved spring, spark og løb.",
related: ["sternum", "ribs", "pelvis", "lumbar_spine"]
},
obliquus_externus: {
id: "obliquus_externus", type: "muscle",
names: { da: "Den ydre skrå bugmuskel", la: "Musculus obliquus externus", en: "External oblique" },
location: "Flankerne af maven.",
attachments: "Origo: Ribben 5-12. Insertio: Crista iliaca og linea alba.",
function: "Rotation og sidebøjning af kroppen. Rotationskraft i slag og kast.",
didactic: "Afgørende for rotation i golf, tennis, badminton, hockey og kastesport.",
related: ["ribs", "pelvis", "rectus_abdominis"]
},
gluteus_maximus: {
id: "gluteus_maximus", type: "muscle",
names: { da: "Den store sædemuskel / Baldemuskel", la: "Musculus gluteus maximus", en: "Glutes / Gluteus maximus" },
location: "Bagsiden af bækkenet / balden.",
attachments: "Origo: Os ilium og sacrum. Insertio: Femur og tractus iliotibialis.",
function: "Kraftig ekstension og udadrotation i hofteleddet. Afsætskraft.",
didactic: "Kroppens kraftværk til acceleration, sprintafsæt, hop og rejsning fra squat.",
related: ["pelvis", "femur", "hamstrings", "quadriceps"]
},
quadriceps: {
id: "quadriceps", type: "muscle",
names: { da: "Den fire-hovedede knæstrækker", la: "Musculus quadriceps femoris", en: "Quadriceps" },
location: "Forsiden af låret.",
attachments: "Origo: Pelvis og femur. Insertio: Patella og tuberositas tibiae.",
function: "Ekstension i knæleddet; flekterer desuden hofteleddet via rectus femoris.",
didactic: "Agonist i squat, cykling, opbremsning og landing (excentrisk muskelarbejde).",
related: ["femur", "patella", "tibia", "pelvis", "hamstrings"]
},
hamstrings: {
id: "hamstrings", type: "muscle",
names: { da: "Baglårsmusklerne / Hasemusklerne", la: "Mm. ischiocrurales", en: "Hamstrings" },
location: "Bagsiden af låret.",
attachments: "Origo: Tuber ischiadicum (siddeknoglen). Insertio: Tibia og fibula.",
function: "Fleksion af knæleddet og ekstension af hofteleddet. Bremser underbenet i sprint.",
didactic: "Fibersprængninger i haserne forebygges effektivt med excentrisk træning (f.eks. Nordic Hamstrings).",
related: ["pelvis", "femur", "tibia", "fibula", "gluteus_maximus", "quadriceps"]
},
gastrocnemius: {
id: "gastrocnemius", type: "muscle",
names: { da: "Den to-hovedede lægmuskel", la: "Musculus gastrocnemius", en: "Calf / Gastrocnemius" },
location: "Øverste del af læggen på bagsiden.",
attachments: "Origo: Condyli femoris. Insertio: Achillessenen på calcaneus.",
function: "Plantarfleksion i ankelleddet (strække vristen) og fleksion i knæet.",
didactic: "Afgørende for afsæt i løb og hop via elastisk energilagring i achillessenen.",
related: ["femur", "tibia", "foot_bones", "soleus", "tibialis_anterior"]
},
soleus: {
id: "soleus", type: "muscle",
names: { da: "Flyndermusklen", la: "Musculus soleus", en: "Soleus" },
location: "Dybt i læggen.",
attachments: "Origo: Tibia og fibula. Insertio: Achillessenen.",
function: "Plantarfleksion i anklen. Består primært af udholdende type I fibre.",
didactic: "Vigtig for udholdenhed ved langdistanceløb og vedligeholdelse af stående balance.",
related: ["tibia", "fibula", "foot_bones", "gastrocnemius"]
},
tibialis_anterior: {
id: "tibialis_anterior", type: "muscle",
names: { da: "Forreste skinnebensmuskel", la: "Musculus tibialis anterior", en: "Tibialis anterior" },
location: "Forsiden af skinnebenet.",
attachments: "Origo: Tibia. Insertio: Mellemfodsknogler.",
function: "Dorsalfleksion (løfte tæerne) så foden ikke rammer jorden under svingfasen i løb.",
didactic: "Svaghed eller overanstrengelse ses ved uvant gang eller løb på hårdt underlag.",
related: ["tibia", "foot_bones", "gastrocnemius"]
}
},
organs: {
heart: {
id: "heart", type: "organ",
names: { da: "Hjertet", la: "Cor", en: "Heart" },
location: "Midt i brysthulen mellem lungerne, let forskudt mod venstre.",
function: "Pumper iltet blod til kroppen (det store kredsløb) og afiltet blod til lungerne (det lille kredsløb).",
didactic: "Konditionstræning forbedrer hjertets slagvolumen, hvilket sænker hvilepulsen og øger maksimal iltoptagelse (VO2max).",
related: ["lungs", "aorta", "vena_cava", "sternum", "ribs"]
},
lungs: {
id: "lungs", type: "organ",
names: { da: "Lungerne", la: "Pulmones", en: "Lungs" },
location: "Brysthulen, beskyttet af ribbenene.",
function: "Gasudveksling: Optager ilt fra luften og udskiller kuldioxid fra blodet i alveolerne.",
didactic: "Hos raske er lungerne sjældent flaskehalsen for VO2max; det er kredsløbets pumpekapacitet og musklernes kapillarisering.",
related: ["heart", "ribs"]
},
brain: {
id: "brain", type: "organ",
names: { da: "Hjernen", la: "Cerebrum", en: "Brain" },
location: "Beskyttet i kraniet.",
function: "Styrer motorik, balance, koordination og beslutningstagning i idræt.",
didactic: "Motorisk indlæring og automatisering af bevægelsesmønstre forankres i lillehjernen (cerebellum) og motorisk cortex.",
related: ["cranium", "spinal_cord"]
},
stomach: {
id: "stomach", type: "organ",
names: { da: "Mavesækken", la: "Gaster", en: "Stomach" },
location: "Øverst i bughulen til venstre under mellemgulvet.",
function: "Mekanisk og kemisk nedbrydning af mad via mavesyre og enzymer.",
didactic: "Blodgennemstrømningen til mave-tarm nedsættes kraftigt under hård træning. Tunge måltider tæt på træning hæmmer ydeevnen.",
related: ["liver", "intestines"]
},
liver: {
id: "liver", type: "organ",
names: { da: "Leveren", la: "Hepar", en: "Liver" },
location: "Øverst til højre i bughulen.",
function: "Oplagrer glykogen og frigiver glukose til blodet under fysisk aktivitet.",
didactic: "Når glykogendepoterne tømmes under langvarig aktivitet, falder intensiteten drastisk ('at ramme muren').",
related: ["stomach", "intestines", "heart"]
},
intestines: {
id: "intestines", type: "organ",
names: { da: "Tarmene", la: "Intestina", en: "Intestines" },
location: "Nedre del af bughulen.",
function: "Absorption af kulhydrater, vand og elektrolytter til blodet.",
didactic: "Væskebalance under idræt; hypotone/isotone drikke optimerer tarmens optagelseshastighed.",
related: ["stomach", "liver", "rectus_abdominis"]
}
},
vessels: {
aorta: {
id: "aorta", type: "vessel",
names: { da: "Hovedpulsåren", la: "Aorta", en: "Aorta" },
location: "Udgår fra venstre hjertekammer og løber ned gennem kroppen.",
function: "Fører iltrigt blod under højt tryk ud til arbejdende muskler og organer.",
didactic: "Det systoliske blodtryk stiger markant under hård idræt for at drive blodet ud i kapillærerne.",
related: ["heart"]
},
vena_cava: {
id: "vena_cava", type: "vessel",
names: { da: "De store hulvener", la: "Vena cava", en: "Vena cava" },
location: "Løber retur til højre forkammer.",
function: "Transporterer afiltet blod fra kroppen retur til hjertet via venepumpen.",
didactic: "Muskel-venepumpen i benene er afgørende for det venøse tilbageløb under løb.",
related: ["heart", "gastrocnemius"]
}
},
nervous: {
spinal_cord: {
id: "spinal_cord", type: "nerve",
names: { da: "Rygmarven", la: "Medulla spinalis", en: "Spinal cord" },
location: "Løber inde i rygsøjlens hvirvelkanal.",
function: "Hovedvej for nerveimpulser mellem hjerne og muskler samt center for reflekser.",
didactic: "Strækreflekser (f.eks. patellarrefleksen) udløses på rygmarvsniveau og giver lynhurtig muskelaktivering.",
related: ["brain", "cervical_spine", "lumbar_spine"]
},
sciatic_nerve: {
id: "sciatic_nerve", type: "nerve",
names: { da: "Iskiasnerven", la: "Nervus ischiadicus", en: "Sciatic nerve" },
location: "Løber fra lænderyg/bækken ned ad bagsiden af låret.",
function: "Innerverer baglåret og hele underbenet og foden.",
didactic: "Afklemning af nerven kan give smerter under løb eller tung styrketræning.",
related: ["hamstrings", "pelvis", "lumbar_spine"]
}
}
};
const MovementData = [
{
id: "elbow_flexion",
name: { da: "Fleksion i albueleddet", en: "Elbow flexion", la: "Flexio cubiti" },
agonist: "M. biceps brachii & M. brachialis",
antagonist: "M. triceps brachii",
joint: "Albueleddet (Articulatio cubiti)",
highlightParts: ["biceps", "humerus", "radius", "ulna"],
antagonistParts: ["triceps"],
didactic: "Koncentrisk arbejde af biceps ved trækbevægelser (chin-ups); excentrisk bremsende arbejde på vej ned."
},
{
id: "elbow_extension",
name: { da: "Ekstension i albueleddet", en: "Elbow extension", la: "Extensio cubiti" },
agonist: "M. triceps brachii",
antagonist: "M. biceps brachii",
joint: "Albueleddet (Articulatio cubiti)",
highlightParts: ["triceps", "humerus", "ulna"],
antagonistParts: ["biceps"],
didactic: "Klassisk armstrækker (push-up) og kastets slutfase. Triceps strækker armen mod modstand."
},
{
id: "knee_extension",
name: { da: "Ekstension i knæleddet", en: "Knee extension", la: "Extensio genus" },
agonist: "M. quadriceps femoris",
antagonist: "Hamstrings (haserne)",
joint: "Knæleddet (Articulatio genus)",
highlightParts: ["quadriceps", "femur", "patella", "tibia"],
antagonistParts: ["hamstrings"],
didactic: "Hovedbevægelsen ved spark til bold, cykling og opstigningsfasen i squat."
},
{
id: "knee_flexion",
name: { da: "Fleksion i knæleddet", en: "Knee flexion", la: "Flexio genus" },
agonist: "Hamstrings (Mm. ischiocrurales)",
antagonist: "M. quadriceps femoris",
joint: "Knæleddet (Articulatio genus)",
highlightParts: ["hamstrings", "femur", "tibia", "fibula"],
antagonistParts: ["quadriceps"],
didactic: "Vigtig i svingfasen i løb, hvor underbenet bøjes op mod balden for at øge skridtfrekvensen."
},
{
id: "shoulder_abduction",
name: { da: "Abduktion i skulderleddet", en: "Shoulder abduction", la: "Abductio humeri" },
agonist: "M. deltoideus & M. supraspinatus",
antagonist: "M. latissimus dorsi & M. pectoralis major",
joint: "Skulderleddet (Articulatio humeri)",
highlightParts: ["deltoideus", "scapula", "clavicle", "humerus"],
antagonistParts: ["latissimus_dorsi", "pectoralis_major"],
didactic: "Armen løftes ud til siden. Essentielt ved kastesport, volleyballblokering og svømning."
},
{
id: "squat_movement",
name: { da: "Squat (Knæbøjning)", en: "Squat movement", la: "Flexio/Extensio coxae et genus" },
agonist: "M. gluteus maximus & M. quadriceps femoris",
antagonist: "Hamstrings (synergist/antagonist)",
joint: "Hofte-, knæ- og ankelled (flerledsøvelse)",
highlightParts: ["gluteus_maximus", "quadriceps", "femur", "tibia", "pelvis"],
antagonistParts: ["hamstrings", "rectus_abdominis"],
didactic: "Kongeøvelsen for underkroppen. Træner triple-extension (ankel, knæ, hofte). Kræver god core-stabilitet."
}
];
const ProcessData = {
oxygen: {
id: "oxygen",
title: "Iltens vej gennem kroppen",
steps: [
{ name: "1. Næse & Svælg", organ: "cranium", desc: "Luft indåndes gennem næse/mund og opvarmes." },
{ name: "2. Luftrør & Bronkier", organ: "lungs", desc: "Luften ledes ned gennem trachea til lungerne." },
{ name: "3. Lungealveoler", organ: "lungs", desc: "Gasudveksling ved diffusion over den tynde membran til kapillærerne." },
{ name: "4. Venstre hjertekammer", organ: "heart", desc: "Det iltede blod pumpes ud af venstre ventrikel." },
{ name: "5. Aorta & Arterier", organ: "aorta", desc: "Hovedpulsåren fordeler ilten til arbejdende muskler." },
{ name: "6. Mitokondrier", organ: "quadriceps", desc: "Muskelcellerne udnytter ilten til aerob energiproduktion (ATP)." }
]
},
blood: {
id: "blood",
title: "Blodets kredsløb",
steps: [
{ name: "1. Højre hjertekammer", organ: "heart", desc: "Afiltet blod modtages og pumpes til lungerne." },
{ name: "2. Lungerne", organ: "lungs", desc: "Blodet iltes og udskiller kuldioxid." },
{ name: "3. Venstre hjertekammer", organ: "heart", desc: "Iltet blod pumpes ud i det store kredsløb." },
{ name: "4. Aorta & Muskler", organ: "aorta", desc: "Ilt og næringsstoffer leveres til arbejdende væv." },
{ name: "5. Vener retur", organ: "vena_cava", desc: "Afiltet blod føres retur via venepumpen." }
]
},
digestion: {
id: "digestion",
title: "Madens vej & Energi",
steps: [
{ name: "1. Mund & Spiserør", organ: "cranium", desc: "Maden tygges og kulhydratspaltning påbegyndes." },
{ name: "2. Mavesæk", organ: "stomach", desc: "Kemisk nedbrydning af proteiner." },
{ name: "3. Tyndtarm", organ: "intestines", desc: "Absorption af glukose og aminosyrer til blodet." },
{ name: "4. Lever (Glykogen)", organ: "liver", desc: "Glukose oplagres som glykogen til træning." },
{ name: "5. Muskler", organ: "quadriceps", desc: "Muskelglykogen udgør brændstof til højintensiv træning." }
]
}
};
const PhysiologyData = {
rest: {
name: "Hvile", hr: "65 bpm", rf: "12 /min", q: "5.0 L/min", muscleFlow: "~18%", pulseMs: 923
},
moderate: {
name: "Moderat", hr: "135 bpm", rf: "26 /min", q: "15.5 L/min", muscleFlow: "~65%", pulseMs: 444
},
heavy: {
name: "Hård aktivitet", hr: "185 bpm", rf: "50 /min", q: "24.0 L/min", muscleFlow: "~85%", pulseMs: 324
}
};
const QuizData = {
findQuestions: [
{ targetId: "femur", text: "Find lårbenet (Femur) på figuren.", hint: "Kroppens længste rørknogle i overbenet." },
{ targetId: "biceps", text: "Find M. biceps brachii på armen.", hint: "Musklen på forsiden af overarmen der bøjer albuen." },
{ targetId: "patella", text: "Find knæskallen (Patella).", hint: "Den lille sesamknogle foran knæleddet." },
{ targetId: "rectus_abdominis", text: "Find den lige bugmuskel (M. rectus abdominis).", hint: "Core-musklen midt foran på maven." },
{ targetId: "heart", text: "Find hjertet (Cor).", hint: "Pumpeorganet i venstre side af brysthulen." },
{ targetId: "gastrocnemius", text: "Find den to-hovedede lægmuskel (M. gastrocnemius).", hint: "Musklen øverst på bagsiden af underbenet." }
],
nameQuestions: [
{
partId: "femur",
question: "Hvad hedder den fremhævede knogle på latin?",
options: ["Os femoris", "Tibia", "Humerus", "Fibula"],
correctIndex: 0,
feedback: "Korrekt! Os femoris er det latinske navn for lårbenet."
},
{
partId: "deltoideus",
question: "Hvilken muskelgruppe er fremhævet på skulderen?",
options: ["M. pectoralis major", "M. deltoideus", "M. trapezius", "M. latissimus dorsi"],
correctIndex: 1,
feedback: "Rigtigt! M. deltoideus er hovedmotor ved abduktion af armen."
}
],
matchPairs: [
{ da: "Lårben", la: "Femur" },
{ da: "Overarmsknogle", la: "Humerus" },
{ da: "Knæskal", la: "Patella" },
{ da: "Brystben", la: "Sternum" },
{ da: "Brystmuskel", la: "M. pectoralis major" },
{ da: "Kappemuskel", la: "M. trapezius" }
],
movementQuestions: [
{
question: "Hvilken muskel er primær AGONIST ved fleksion i albueleddet?",
options: ["M. triceps brachii", "M. biceps brachii", "M. deltoideus", "M. gastrocnemius"],
correctIndex: 1,
feedback: "Korrekt! Biceps flekterer albuen, mens triceps fungerer som antagonist."
},
{
question: "Hvilket muskelarbejde udfører M. quadriceps, når du langsomt sænker dig ned i en squat?",
options: ["Koncentrisk", "Excentrisk", "Isometrisk", "Passivt stræk"],
correctIndex: 1,
feedback: "Rigtigt! Quadriceps forlænges under spænding for at bremse kroppen mod tyngdekraften = excentrisk arbejde."
}
]
};
// =============================================================================
// APPLIKATIONENS MOTOR
// =============================================================================
var AppCore = {
initialized: false,
state: {
currentView: "anterior",
activeLayers: {
silhouette: true,
skeleton: true,
muscles: true,
organs: true,
circulation: true,
nervous: true
},
layerOpacity: {
silhouette: 0.4,
skeleton: 1,
muscles: 0.9,
organs: 0.95,
circulation: 0.85,
nervous: 0.8
},
selectedPartId: null,
langMode: "da-la",
exertionState: "rest",
teacherMode: false,
zoomScale: 1,
panX: 0,
panY: 0,
isDragging: false,
dragStartX: 0,
dragStartY: 0,
favorites: [],
splitDistance: 130
},
getStoredFavorites() {
try {
if (typeof window !== "undefined" && "localStorage" in window && window.localStorage) {
const item = localStorage.getItem("ia_favorites");
return item ? JSON.parse(item) : [];
}
} catch (e) {
console.warn("localStorage er utilgængeligt (sandboxed miljø). Bruger hukommelse.");
}
return [];
},
setStoredFavorites(favs) {
try {
if (typeof window !== "undefined" && "localStorage" in window && window.localStorage) {
localStorage.setItem("ia_favorites", JSON.stringify(favs));
}
} catch (e) {
console.warn("Kunne ikke gemme til localStorage (sandboxed miljø).");
}
},
init() {
if (this.initialized) return;
this.initialized = true;
try {
this.state.favorites = this.getStoredFavorites();
this.bindEvents();
this.renderSvgLayers();
this.initMovementModule();
this.initProcessModule();
this.initQuizModule();
this.updateExertionUI();
this.updateFavoritesCount();
// Vælg automatisk lårbenet (Femur) som demonstration ved opstart
this.selectStructure("femur");
} catch (err) {
console.error("Fejl under initialisering af AppCore:", err);
}
},
bindEvents() {
// Mobil/tablet drawer knapper og backdrop
const btnToggleSidebar = document.getElementById("btn-toggle-sidebar");
const btnCloseSidebar = document.getElementById("btn-close-sidebar");
const leftSidebar = document.getElementById("left-sidebar");
const btnTogglePanel = document.getElementById("btn-toggle-panel");
const btnClosePanel = document.getElementById("btn-close-panel");
const rightPanel = document.getElementById("right-panel");
const backdrop = document.getElementById("sidebar-backdrop");
const updateBackdrop = () => {
const isAnyOpen = (leftSidebar && leftSidebar.classList.contains("open")) ||
(rightPanel && rightPanel.classList.contains("open"));
if (backdrop) {
if (isAnyOpen && window.innerWidth <= 950) {
backdrop.classList.add("active");
} else {
backdrop.classList.remove("active");
}
}
};
if (btnToggleSidebar && leftSidebar) {
btnToggleSidebar.addEventListener("click", () => {
leftSidebar.classList.toggle("open");
if (rightPanel) rightPanel.classList.remove("open");
updateBackdrop();
});
}
if (btnCloseSidebar && leftSidebar) {
btnCloseSidebar.addEventListener("click", () => {
leftSidebar.classList.remove("open");
updateBackdrop();
});
}
if (btnTogglePanel && rightPanel) {
btnTogglePanel.addEventListener("click", () => {
rightPanel.classList.toggle("open");
if (leftSidebar) leftSidebar.classList.remove("open");
updateBackdrop();
});
}
if (btnClosePanel && rightPanel) {
btnClosePanel.addEventListener("click", () => {
rightPanel.classList.remove("open");
updateBackdrop();
});
}
if (backdrop) {
backdrop.addEventListener("click", () => {
if (leftSidebar) leftSidebar.classList.remove("open");
if (rightPanel) rightPanel.classList.remove("open");
backdrop.classList.remove("active");
});
}
// Visningsvinkel
document.querySelectorAll(".pill-btn[data-view]").forEach(btn => {
btn.addEventListener("click", () => {
document.querySelectorAll(".pill-btn[data-view]").forEach(b => b.classList.remove("active"));
btn.classList.add("active");
this.setView(btn.dataset.view);
});
});
// Sprogvalg
const langSelect = document.getElementById("lang-select");
if (langSelect) {
langSelect.addEventListener("change", (e) => {
this.state.langMode = e.target.value;
this.updateLanguageUI();
});
}
// Belastningstilstand
document.querySelectorAll(".exertion-btn[data-state]").forEach(btn => {
btn.addEventListener("click", () => {
document.querySelectorAll(".exertion-btn[data-state]").forEach(b => b.classList.remove("active"));
btn.classList.add("active");
this.setExertionState(btn.dataset.state);
});
});
// Lag synlighed & gennemsigtighed
document.querySelectorAll("input[data-layer]").forEach(input => {
input.addEventListener("change", (e) => {
const layer = e.target.dataset.layer;
this.state.activeLayers[layer] = e.target.checked;
this.updateLayerVisibility();
});
});
document.querySelectorAll("input[data-layer-opacity]").forEach(slider => {
slider.addEventListener("input", (e) => {
const layer = e.target.dataset.layerOpacity;
this.state.layerOpacity[layer] = parseFloat(e.target.value);
this.updateLayerVisibility();
});
});
// System isolation i venstre menu
document.querySelectorAll(".nav-item-btn[data-nav]").forEach(btn => {
btn.addEventListener("click", () => {
document.querySelectorAll(".nav-item-btn[data-nav]").forEach(b => b.classList.remove("active"));
btn.classList.add("active");
this.isolateSystem(btn.dataset.nav);
// Luk menu på små skærme efter valg
if (window.innerWidth <= 950 && leftSidebar) {
leftSidebar.classList.remove("open");
updateBackdrop();
}
});
});
// Overlays
document.querySelectorAll("[data-overlay]").forEach(btn => {
btn.addEventListener("click", () => {
this.toggleOverlay(btn.dataset.overlay);
if (window.innerWidth <= 950 && leftSidebar) {
leftSidebar.classList.remove("open");
updateBackdrop();
}
});
});
document.querySelectorAll(".overlay-close").forEach(btn => {
btn.addEventListener("click", () => {
this.closeOverlay(btn.dataset.close);
});
});
// Zoom & Pan
document.getElementById("btn-zoom-in").addEventListener("click", () => this.zoom(1.25));
document.getElementById("btn-zoom-out").addEventListener("click", () => this.zoom(0.8));
document.getElementById("btn-zoom-reset").addEventListener("click", () => this.resetZoom());
const canvasEl = document.getElementById("canvas-container");
canvasEl.addEventListener("mousedown", (e) => {
if (e.target.closest(".overlay-panel") || e.target.closest(".canvas-toolbar") || e.target.closest(".split-slider-container")) return;
this.state.isDragging = true;
this.state.dragStartX = e.clientX - this.state.panX;
this.state.dragStartY = e.clientY - this.state.panY;
});
window.addEventListener("mousemove", (e) => {
if (!this.state.isDragging) return;
this.state.panX = e.clientX - this.state.dragStartX;
this.state.panY = e.clientY - this.state.dragStartY;
this.applyTransform();
});
window.addEventListener("mouseup", () => {
this.state.isDragging = false;
});
canvasEl.addEventListener("wheel", (e) => {
if (e.target.closest(".overlay-panel")) return;
e.preventDefault();
const delta = e.deltaY < 0 ? 1.12 : 0.89;
this.zoom(delta);
}, { passive: false });
// Split-slider
const splitSlider = document.getElementById("split-slider");
if (splitSlider) {
splitSlider.addEventListener("input", (e) => {
this.state.splitDistance = parseInt(e.target.value);
this.renderSvgLayers();
});
}
// Undervisertilstand
document.getElementById("btn-teacher-mode").addEventListener("click", () => {
this.toggleTeacherMode();
});
// Søgning
const searchInput = document.getElementById("search-input");
if (searchInput) {
searchInput.addEventListener("input", (e) => this.handleSearch(e.target.value));
searchInput.addEventListener("focus", (e) => this.handleSearch(e.target.value));
document.addEventListener("click", (e) => {
if (!e.target.closest(".search-box")) {
document.getElementById("search-results").style.display = "none";
}
});
}
// Favoritter
document.getElementById("btn-show-favorites").addEventListener("click", () => {
this.showFavoritesModal();
});
document.getElementById("btn-toggle-fav").addEventListener("click", () => {
if (this.state.selectedPartId) {
this.toggleFavorite(this.state.selectedPartId);
}
});
document.getElementById("btn-demo-select").addEventListener("click", () => {
this.selectStructure("femur");
});
},
renderSvgLayers() {
const root = document.getElementById("svg-transform-root");
if (!root) return;
const isBack = this.state.currentView === "posterior";
const isSplit = this.state.currentView === "split";
const dist = this.state.splitDistance;
const getXOffset = (layerIndex) => {
if (!isSplit) return 0;
return (layerIndex - 2.5) * dist;
};
root.innerHTML = `
<!-- LAG 0: YDRE SILHOUETTE -->
<g id="layer-silhouette" transform="translate(${getXOffset(0)}, 0)" opacity="${this.state.layerOpacity.silhouette}">
<circle cx="300" cy="85" r="42" fill="#1e293b" stroke="#475569" stroke-width="2" />
<path d="M 282 125 L 260 140 L 210 170 L 195 270 L 180 370 L 175 440 L 190 440 L 205 370 L 220 270 L 250 200 L 240 370 L 235 450 L 250 450 L 260 630 L 250 820 L 245 920 L 290 920 L 295 820 L 290 630 L 295 470 L 305 470 L 310 630 L 305 820 L 310 920 L 355 920 L 350 820 L 340 630 L 350 450 L 365 450 L 360 370 L 350 200 L 380 270 L 395 370 L 410 440 L 425 440 L 420 370 L 405 270 L 390 170 L 340 140 L 318 125 Z"
fill="#1e293b" stroke="#475569" stroke-width="2" />
</g>
<!-- LAG 1: SKELET -->
<g id="layer-skeleton" transform="translate(${getXOffset(2)}, 0)" opacity="${this.state.layerOpacity.skeleton}">
<!-- Kranium -->
<g id="part-cranium" class="anatomical-part" data-id="cranium">
<ellipse cx="300" cy="80" rx="34" ry="40" fill="url(#grad-bone)" stroke="var(--color-bone-stroke)" stroke-width="1.5" />
${!isBack ? `
<ellipse cx="288" cy="82" rx="7" ry="8" fill="#0f172a" />
<ellipse cx="312" cy="82" rx="7" ry="8" fill="#0f172a" />
<path d="M 300 90 L 296 99 L 304 99 Z" fill="#0f172a" />
<path d="M 288 108 Q 300 112 312 108" stroke="#0f172a" stroke-width="2" fill="none" />
` : `
<path d="M 285 85 Q 300 100 315 85" stroke="#94a3b8" stroke-width="2" fill="none" />
`}
<text x="300" y="70" class="anatomy-label-pin" text-anchor="middle">Kranium</text>
</g>
<!-- Halshvirvler -->
<g id="part-cervical_spine" class="anatomical-part" data-id="cervical_spine">
<rect x="294" y="118" width="12" height="22" rx="3" fill="url(#grad-bone)" stroke="var(--color-bone-stroke)" />
</g>
<!-- Kraveben -->
<g id="part-clavicle" class="anatomical-part" data-id="clavicle">
<path d="M 300 144 Q 260 138 230 152" stroke="url(#grad-bone)" stroke-width="6" stroke-linecap="round" fill="none" />
<path d="M 300 144 Q 340 138 370 152" stroke="url(#grad-bone)" stroke-width="6" stroke-linecap="round" fill="none" />
<text x="255" y="136" class="anatomy-label-pin">Kraveben</text>
</g>
<!-- Brystben -->
${!isBack ? `
<g id="part-sternum" class="anatomical-part" data-id="sternum">
<path d="M 295 146 L 305 146 L 304 185 L 300 196 L 296 185 Z" fill="url(#grad-bone)" stroke="var(--color-bone-stroke)" stroke-width="1.5" />
<text x="300" y="172" class="anatomy-label-pin" text-anchor="middle">Brystben</text>
</g>` : ''}
<!-- Skulderblad -->
<g id="part-scapula" class="anatomical-part" data-id="scapula" opacity="${isBack ? '1' : '0.45'}">
<path d="M 240 155 L 268 162 L 255 210 Z" fill="url(#grad-bone)" stroke="var(--color-bone-stroke)" stroke-width="1.5" />
<path d="M 360 155 L 332 162 L 345 210 Z" fill="url(#grad-bone)" stroke="var(--color-bone-stroke)" stroke-width="1.5" />
<text x="250" y="190" class="anatomy-label-pin">Skulderblad</text>
</g>
<!-- Ribben -->
<g id="part-ribs" class="anatomical-part" data-id="ribs">
<path d="M 296 156 C 265 156 248 180 252 205 C 255 225 275 245 296 250" stroke="url(#grad-bone)" stroke-width="3.5" fill="none" />
<path d="M 304 156 C 335 156 352 180 348 205 C 345 225 325 245 304 250" stroke="url(#grad-bone)" stroke-width="3.5" fill="none" />
<path d="M 296 170 C 260 170 242 195 248 220 C 252 240 275 258 296 265" stroke="url(#grad-bone)" stroke-width="3.5" fill="none" />
<path d="M 304 170 C 340 170 358 195 352 220 C 348 240 325 258 304 265" stroke="url(#grad-bone)" stroke-width="3.5" fill="none" />
<text x="300" y="220" class="anatomy-label-pin" text-anchor="middle">Ribben</text>
</g>
<!-- Rygsøjle -->
<g id="part-thoracic_spine" class="anatomical-part" data-id="thoracic_spine">
<rect x="295" y="142" width="10" height="110" rx="3" fill="url(#grad-bone)" stroke="var(--color-bone-stroke)" opacity="${isBack ? '1' : '0.5'}" />
</g>
<g id="part-lumbar_spine" class="anatomical-part" data-id="lumbar_spine">
<rect x="293" y="255" width="14" height="60" rx="3" fill="url(#grad-bone)" stroke="var(--color-bone-stroke)" />
<text x="300" y="285" class="anatomy-label-pin" text-anchor="middle">Lændehvirvler</text>
</g>
<!-- Bækken -->
<g id="part-pelvis" class="anatomical-part" data-id="pelvis">
<path d="M 252 315 C 240 330 240 365 260 385 C 275 395 292 390 300 375 C 308 390 325 395 340 385 C 360 365 360 330 348 315 C 330 310 315 322 300 325 C 285 322 270 310 252 315 Z"
fill="url(#grad-bone)" stroke="var(--color-bone-stroke)" stroke-width="2" />
<ellipse cx="270" cy="365" rx="9" ry="12" fill="#0f172a" />
<ellipse cx="330" cy="365" rx="9" ry="12" fill="#0f172a" />
<text x="300" y="350" class="anatomy-label-pin" text-anchor="middle">Bækken</text>
</g>
<!-- Overarm -->
<g id="part-humerus" class="anatomical-part" data-id="humerus">
<path d="M 226 156 L 210 268" stroke="url(#grad-bone)" stroke-width="10" stroke-linecap="round" />
<path d="M 374 156 L 390 268" stroke="url(#grad-bone)" stroke-width="10" stroke-linecap="round" />
<text x="210" y="220" class="anatomy-label-pin">Overarmsknogle</text>
</g>
<!-- Underarm -->
<g id="part-radius" class="anatomical-part" data-id="radius">
<path d="M 206 274 L 188 375" stroke="url(#grad-bone)" stroke-width="6" stroke-linecap="round" />
<path d="M 394 274 L 412 375" stroke="url(#grad-bone)" stroke-width="6" stroke-linecap="round" />
<text x="180" y="330" class="anatomy-label-pin">Spoleben</text>
</g>
<g id="part-ulna" class="anatomical-part" data-id="ulna">
<path d="M 213 274 L 198 375" stroke="url(#grad-bone)" stroke-width="5" stroke-linecap="round" />
<path d="M 387 274 L 402 375" stroke="url(#grad-bone)" stroke-width="5" stroke-linecap="round" />
</g>
<!-- Hånd -->
<g id="part-hand_bones" class="anatomical-part" data-id="hand_bones">
<path d="M 188 378 L 180 435 M 184 410 L 174 418" stroke="url(#grad-bone)" stroke-width="6" stroke-linecap="round" />
<path d="M 412 378 L 420 435 M 416 410 L 426 418" stroke="url(#grad-bone)" stroke-width="6" stroke-linecap="round" />
</g>
<!-- Lårben -->
<g id="part-femur" class="anatomical-part" data-id="femur">
<path d="M 268 385 Q 262 500 266 615" stroke="url(#grad-bone)" stroke-width="15" stroke-linecap="round" />
<path d="M 332 385 Q 338 500 334 615" stroke="url(#grad-bone)" stroke-width="15" stroke-linecap="round" />
<text x="252" y="510" class="anatomy-label-pin">Lårben</text>
</g>
<!-- Knæskal -->
${!isBack ? `
<g id="part-patella" class="anatomical-part" data-id="patella">
<circle cx="266" cy="624" r="8" fill="url(#grad-bone)" stroke="var(--color-bone-stroke)" stroke-width="2" />
<circle cx="334" cy="624" r="8" fill="url(#grad-bone)" stroke="var(--color-bone-stroke)" stroke-width="2" />
<text x="266" y="640" class="anatomy-label-pin" text-anchor="middle">Knæskal</text>
</g>` : ''}
<!-- Underben -->
<g id="part-tibia" class="anatomical-part" data-id="tibia">
<path d="M 267 635 L 268 815" stroke="url(#grad-bone)" stroke-width="12" stroke-linecap="round" />
<path d="M 333 635 L 332 815" stroke="url(#grad-bone)" stroke-width="12" stroke-linecap="round" />
<text x="255" y="740" class="anatomy-label-pin">Skinneben</text>
</g>
<g id="part-fibula" class="anatomical-part" data-id="fibula">
<path d="M 256 645 L 257 810" stroke="url(#grad-bone)" stroke-width="5" stroke-linecap="round" />
<path d="M 344 645 L 343 810" stroke="url(#grad-bone)" stroke-width="5" stroke-linecap="round" />
<text x="245" y="760" class="anatomy-label-pin">Lægben</text>
</g>
<!-- Fod -->
<g id="part-foot_bones" class="anatomical-part" data-id="foot_bones">
<path d="M 268 820 L 264 860 L 255 865" stroke="url(#grad-bone)" stroke-width="8" stroke-linecap="round" />
<path d="M 332 820 L 336 860 L 345 865" stroke="url(#grad-bone)" stroke-width="8" stroke-linecap="round" />
<text x="300" y="870" class="anatomy-label-pin" text-anchor="middle">Fodknogler</text>
</g>
</g>
<!-- LAG 2: MUSKLER -->
<g id="layer-muscles" transform="translate(${getXOffset(1)}, 0)" opacity="${this.state.layerOpacity.muscles}">
<!-- Deltoideus -->
<g id="part-deltoideus" class="anatomical-part" data-id="deltoideus">
<path d="M 235 146 C 215 155 205 180 216 210 C 225 190 230 170 238 155 Z" fill="url(#grad-muscle)" stroke="var(--color-muscle-stroke)" />
<path d="M 365 146 C 385 155 395 180 384 210 C 375 190 370 170 362 155 Z" fill="url(#grad-muscle)" stroke="var(--color-muscle-stroke)" />
<text x="205" y="175" class="anatomy-label-pin">Deltoideus</text>
</g>
<!-- Biceps / Triceps -->
${!isBack ? `
<g id="part-biceps" class="anatomical-part" data-id="biceps">
<ellipse cx="218" cy="216" rx="9" ry="34" fill="url(#grad-muscle)" stroke="var(--color-muscle-stroke)" transform="rotate(7 218 216)" />
<ellipse cx="382" cy="216" rx="9" ry="34" fill="url(#grad-muscle)" stroke="var(--color-muscle-stroke)" transform="rotate(-7 382 216)" />
<text x="218" y="220" class="anatomy-label-pin" text-anchor="middle">Biceps</text>
</g>` : `
<g id="part-triceps" class="anatomical-part" data-id="triceps">
<ellipse cx="216" cy="216" rx="11" ry="38" fill="url(#grad-muscle)" stroke="var(--color-muscle-stroke)" transform="rotate(7 216 216)" />
<ellipse cx="384" cy="216" rx="11" ry="38" fill="url(#grad-muscle)" stroke="var(--color-muscle-stroke)" transform="rotate(-7 384 216)" />
<text x="216" y="220" class="anatomy-label-pin" text-anchor="middle">Triceps</text>
</g>`}
<!-- Pectoralis major -->
${!isBack ? `
<g id="part-pectoralis_major" class="anatomical-part" data-id="pectoralis_major">
<path d="M 296 150 C 265 150 236 160 228 178 C 240 200 270 208 296 200 Z" fill="url(#grad-muscle)" stroke="var(--color-muscle-stroke)" />
<path d="M 304 150 C 335 150 364 160 372 178 C 360 200 330 208 304 200 Z" fill="url(#grad-muscle)" stroke="var(--color-muscle-stroke)" />
<text x="300" y="180" class="anatomy-label-pin" text-anchor="middle">Pectoralis</text>
</g>` : ''}
<!-- Trapezius -->
<g id="part-trapezius" class="anatomical-part" data-id="trapezius" opacity="${isBack ? '1' : '0.4'}">
<path d="M 300 115 L 260 144 L 285 170 L 300 240 L 315 170 L 340 144 Z" fill="url(#grad-muscle)" stroke="var(--color-muscle-stroke)" />
<text x="300" y="138" class="anatomy-label-pin" text-anchor="middle">Trapezius</text>
</g>
<!-- Latissimus dorsi -->
<g id="part-latissimus_dorsi" class="anatomical-part" data-id="latissimus_dorsi" opacity="${isBack ? '1' : '0.5'}">
<path d="M 285 210 C 255 215 240 240 245 285 C 265 295 285 290 295 270 Z" fill="url(#grad-muscle)" stroke="var(--color-muscle-stroke)" />
<path d="M 315 210 C 345 215 360 240 355 285 C 335 295 315 290 305 270 Z" fill="url(#grad-muscle)" stroke="var(--color-muscle-stroke)" />
<text x="300" y="260" class="anatomy-label-pin" text-anchor="middle">Latissimus</text>
</g>
<!-- Rectus abdominis -->
${!isBack ? `
<g id="part-rectus_abdominis" class="anatomical-part" data-id="rectus_abdominis">
<path d="M 288 206 L 298 206 L 298 335 L 288 335 Z" fill="url(#grad-muscle)" stroke="var(--color-muscle-stroke)" />
<path d="M 302 206 L 312 206 L 312 335 L 302 335 Z" fill="url(#grad-muscle)" stroke="var(--color-muscle-stroke)" />
<line x1="288" y1="240" x2="312" y2="240" stroke="#9f1239" stroke-width="2" />
<line x1="288" y1="275" x2="312" y2="275" stroke="#9f1239" stroke-width="2" />
<line x1="288" y1="310" x2="312" y2="310" stroke="#9f1239" stroke-width="2" />
<text x="300" y="260" class="anatomy-label-pin" text-anchor="middle">Rectus abdominis</text>
</g>
<g id="part-obliquus_externus" class="anatomical-part" data-id="obliquus_externus">
<path d="M 265 235 C 250 250 250 290 268 320 C 275 280 275 250 265 235 Z" fill="url(#grad-muscle)" stroke="var(--color-muscle-stroke)" />
<path d="M 335 235 C 350 250 350 290 332 320 C 325 280 325 250 335 235 Z" fill="url(#grad-muscle)" stroke="var(--color-muscle-stroke)" />
</g>` : ''}
<!-- Gluteus maximus -->
${isBack ? `
<g id="part-gluteus_maximus" class="anatomical-part" data-id="gluteus_maximus">
<ellipse cx="270" cy="370" rx="28" ry="32" fill="url(#grad-muscle)" stroke="var(--color-muscle-stroke)" />
<ellipse cx="330" cy="370" rx="28" ry="32" fill="url(#grad-muscle)" stroke="var(--color-muscle-stroke)" />
<text x="300" y="375" class="anatomy-label-pin" text-anchor="middle">Gluteus maximus</text>
</g>` : ''}
<!-- Quadriceps / Hamstrings -->
${!isBack ? `
<g id="part-quadriceps" class="anatomical-part" data-id="quadriceps">
<path d="M 252 390 C 242 450 245 540 264 610 C 285 540 286 450 276 390 Z" fill="url(#grad-muscle)" stroke="var(--color-muscle-stroke)" />
<path d="M 348 390 C 358 450 355 540 336 610 C 315 540 314 450 324 390 Z" fill="url(#grad-muscle)" stroke="var(--color-muscle-stroke)" />
<text x="264" y="490" class="anatomy-label-pin" text-anchor="middle">Quadriceps</text>
</g>` : `
<g id="part-hamstrings" class="anatomical-part" data-id="hamstrings">
<path d="M 254 400 C 245 460 248 550 262 610 C 282 550 282 460 274 400 Z" fill="url(#grad-muscle)" stroke="var(--color-muscle-stroke)" />
<path d="M 346 400 C 355 460 352 550 338 610 C 318 550 318 460 326 400 Z" fill="url(#grad-muscle)" stroke="var(--color-muscle-stroke)" />
<text x="264" y="490" class="anatomy-label-pin" text-anchor="middle">Hamstrings</text>
</g>`}
<!-- Gastrocnemius -->
<g id="part-gastrocnemius" class="anatomical-part" data-id="gastrocnemius">
<path d="M 255 640 C 245 680 246 720 262 760 C 275 730 278 680 269 640 Z" fill="url(#grad-muscle)" stroke="var(--color-muscle-stroke)" />
<path d="M 345 640 C 355 680 354 720 338 760 C 325 730 322 680 331 640 Z" fill="url(#grad-muscle)" stroke="var(--color-muscle-stroke)" />
<text x="256" y="690" class="anatomy-label-pin">Gastrocnemius</text>
</g>
<g id="part-tibialis_anterior" class="anatomical-part" data-id="tibialis_anterior" opacity="${!isBack ? '1' : '0.3'}">
<path d="M 268 645 L 264 770 L 270 770 Z" fill="url(#grad-muscle)" stroke="var(--color-muscle-stroke)" />
<path d="M 332 645 L 336 770 L 330 770 Z" fill="url(#grad-muscle)" stroke="var(--color-muscle-stroke)" />
</g>
</g>
<!-- LAG 3: ORGANER -->
<g id="layer-organs" transform="translate(${getXOffset(3)}, 0)" opacity="${this.state.layerOpacity.organs}">
<g id="part-brain" class="anatomical-part" data-id="brain">
<ellipse cx="300" cy="74" rx="26" ry="24" fill="#f472b6" stroke="#db2777" stroke-width="1.5" />
<text x="300" y="78" class="anatomy-label-pin" text-anchor="middle">Hjerne</text>
</g>
<g id="part-lungs" class="anatomical-part" data-id="lungs">
<path d="M 270 155 C 255 160 250 210 256 240 C 265 245 285 240 290 225 C 290 190 285 160 270 155 Z" fill="url(#grad-lungs)" stroke="#0891b2" />
<path d="M 330 155 C 345 160 350 210 344 240 C 335 245 315 240 310 225 C 310 190 315 160 330 155 Z" fill="url(#grad-lungs)" stroke="#0891b2" />
<text x="270" y="200" class="anatomy-label-pin" text-anchor="middle">Lunge</text>
</g>
<g id="part-heart" class="anatomical-part" data-id="heart">
<path d="M 292 182 C 285 170 270 178 275 195 C 280 210 296 226 304 228 C 312 216 318 200 310 188 C 304 180 296 182 292 182 Z"
fill="url(#grad-heart)" stroke="#991b1b" stroke-width="2" />
<text x="296" y="205" class="anatomy-label-pin" text-anchor="middle">Hjerte</text>
</g>
<g id="part-stomach" class="anatomical-part" data-id="stomach">
<path d="M 305 235 C 325 235 330 255 320 275 C 310 280 295 270 300 255 Z" fill="url(#grad-digestive)" stroke="#b45309" />
<text x="316" y="255" class="anatomy-label-pin">Mavesæk</text>
</g>
<g id="part-liver" class="anatomical-part" data-id="liver">
<path d="M 270 232 C 290 230 300 240 298 265 C 275 272 260 255 270 232 Z" fill="url(#grad-liver)" stroke="#78350f" />
<text x="280" y="255" class="anatomy-label-pin">Lever</text>
</g>
<g id="part-intestines" class="anatomical-part" data-id="intestines">
<rect x="282" y="278" width="36" height="42" rx="10" fill="url(#grad-digestive)" stroke="#b45309" stroke-width="2" />
<text x="300" y="300" class="anatomy-label-pin" text-anchor="middle">Tarme</text>
</g>
</g>
<!-- LAG 4: KREDSLØB -->
<g id="layer-circulation" transform="translate(${getXOffset(4)}, 0)" opacity="${this.state.layerOpacity.circulation}">
<g id="part-aorta" class="anatomical-part" data-id="aorta">
<path d="M 298 185 C 295 170 308 170 306 185 L 304 365 L 275 480" stroke="var(--color-artery)" stroke-width="3" fill="none" />
<path d="M 304 365 L 325 480" stroke="var(--color-artery)" stroke-width="3" fill="none" />
<path d="M 298 175 L 225 240" stroke="var(--color-artery)" stroke-width="2.5" fill="none" />
<path d="M 306 175 L 375 240" stroke="var(--color-artery)" stroke-width="2.5" fill="none" />
<text x="312" y="330" class="anatomy-label-pin">Aorta</text>
</g>
<g id="part-vena_cava" class="anatomical-part" data-id="vena_cava">
<path d="M 292 185 L 292 365 L 265 480" stroke="var(--color-vein)" stroke-width="3" fill="none" />
<path d="M 292 365 L 315 480" stroke="var(--color-vein)" stroke-width="3" fill="none" />
<path d="M 292 185 L 220 245" stroke="var(--color-vein)" stroke-width="2.5" fill="none" />
<path d="M 292 185 L 380 245" stroke="var(--color-vein)" stroke-width="2.5" fill="none" />
<text x="280" y="330" class="anatomy-label-pin">Vena Cava</text>
</g>
</g>
<!-- LAG 5: NERVESYSTEM -->
<g id="layer-nervous" transform="translate(${getXOffset(5)}, 0)" opacity="${this.state.layerOpacity.nervous}">
<g id="part-spinal_cord" class="anatomical-part" data-id="spinal_cord">
<path d="M 300 100 L 300 320" stroke="var(--color-nerve)" stroke-width="3.5" fill="none" />
<path d="M 300 135 L 220 250" stroke="var(--color-nerve)" stroke-width="1.8" fill="none" />
<path d="M 300 135 L 380 250" stroke="var(--color-nerve)" stroke-width="1.8" fill="none" />
<text x="306" y="145" class="anatomy-label-pin">Rygmarv</text>
</g>
<g id="part-sciatic_nerve" class="anatomical-part" data-id="sciatic_nerve">
<path d="M 300 320 Q 275 420 268 620" stroke="var(--color-nerve)" stroke-width="2.5" fill="none" />
<path d="M 300 320 Q 325 420 332 620" stroke="var(--color-nerve)" stroke-width="2.5" fill="none" />
<text x="260" y="440" class="anatomy-label-pin">Iskiasnerve</text>
</g>
</g>
`;
this.attachSvgInteractiveEvents();
this.updateLayerVisibility();
},
attachSvgInteractiveEvents() {
const tooltip = document.getElementById("anatomy-tooltip");
document.querySelectorAll(".anatomical-part").forEach(elem => {
elem.addEventListener("mouseenter", (e) => {
const id = elem.dataset.id;
const item = this.getAnatomicalItem(id);
if (!item) return;
tooltip.style.display = "flex";
tooltip.querySelector(".tt-type").textContent = this.getTypeLabel(item.type);
tooltip.querySelector(".tt-name-da").textContent = item.names.da;
tooltip.querySelector(".tt-name-la").textContent = item.names.la;
const rect = elem.getBoundingClientRect();
const canvasRect = document.getElementById("canvas-container").getBoundingClientRect();
tooltip.style.left = (rect.left + rect.width / 2 - canvasRect.left) + "px";
tooltip.style.top = (rect.top - canvasRect.top - 8) + "px";
});
elem.addEventListener("mouseleave", () => {
tooltip.style.display = "none";
});
elem.addEventListener("click", (e) => {
e.stopPropagation();
const id = elem.dataset.id;
this.selectStructure(id);
if (window.innerWidth <= 950) {
const rightPanel = document.getElementById("right-panel");
const leftSidebar = document.getElementById("left-sidebar");
const backdrop = document.getElementById("sidebar-backdrop");
if (leftSidebar) leftSidebar.classList.remove("open");
if (rightPanel) rightPanel.classList.add("open");
if (backdrop) backdrop.classList.add("active");
}
if (this.onSvgPartClickedForQuiz) {
this.onSvgPartClickedForQuiz(id);
}
});
});
},
selectStructure(id) {
this.state.selectedPartId = id;
const item = this.getAnatomicalItem(id);
if (!item) return;
document.querySelectorAll(".anatomical-part").forEach(el => el.classList.remove("selected"));
document.querySelectorAll(`.anatomical-part[data-id="${id}"]`).forEach(el => el.classList.add("selected"));
document.getElementById("panel-empty").style.display = "none";
const content = document.getElementById("panel-active-content");
content.style.display = "flex";
document.getElementById("info-type").textContent = this.getTypeLabel(item.type);
document.getElementById("info-name-da").textContent = item.names.da;
document.getElementById("info-name-la").textContent = item.names.la;
document.getElementById("info-name-en").textContent = item.names.en;
document.getElementById("info-location").textContent = item.location;
document.getElementById("info-function").textContent = item.function;
document.getElementById("info-didactic").textContent = item.didactic;
const attachCard = document.getElementById("info-muscle-attachments-card");
if (item.attachments) {
attachCard.style.display = "flex";
document.getElementById("info-attachments").textContent = item.attachments;
} else {
attachCard.style.display = "none";
}
const relList = document.getElementById("info-related-list");
relList.innerHTML = "";
if (item.related && item.related.length > 0) {
item.related.forEach(relId => {
const relItem = this.getAnatomicalItem(relId);
if (relItem) {
const btn = document.createElement("button");
btn.className = "related-btn";
btn.textContent = this.formatName(relItem.names);
btn.addEventListener("click", () => this.selectStructure(relId));
relList.appendChild(btn);
}
});
}
this.updateFavoriteButtonState(id);
document.getElementById("breadcrumb-current").textContent = this.formatName(item.names);
},
getAnatomicalItem(id) {
if (AnatomyData.bones[id]) return AnatomyData.bones[id];
if (AnatomyData.muscles[id]) return AnatomyData.muscles[id];
if (AnatomyData.organs[id]) return AnatomyData.organs[id];
if (AnatomyData.vessels[id]) return AnatomyData.vessels[id];
if (AnatomyData.nervous[id]) return AnatomyData.nervous[id];
return null;
},
getTypeLabel(type) {
const map = {
bone: "Knogle",
muscle: "Skeletmuskel",
organ: "Vitalt Organ",
vessel: "Blodkar / Kredsløb",
nerve: "Nervestruktur"
};
return map[type] || "Struktur";
},
formatName(names) {
if (!names) return "";
switch (this.state.langMode) {
case "da": return names.da;
case "la": return names.la;
case "en": return names.en;
case "da-la": return `${names.da} (${names.la})`;
case "da-en": return `${names.da} / ${names.en}`;
case "all": return `${names.da} [${names.la}] (${names.en})`;
default: return `${names.da} (${names.la})`;
}
},
updateLanguageUI() {
if (this.state.selectedPartId) {
this.selectStructure(this.state.selectedPartId);
}
},
updateLayerVisibility() {
Object.keys(this.state.activeLayers).forEach(layer => {
const g = document.getElementById(`layer-${layer}`);
if (!g) return;
g.style.display = this.state.activeLayers[layer] ? "inline" : "none";
g.setAttribute("opacity", this.state.layerOpacity[layer]);
});
},
isolateSystem(system) {
if (system === "all") {
Object.keys(this.state.activeLayers).forEach(k => this.state.activeLayers[k] = true);
} else {
Object.keys(this.state.activeLayers).forEach(k => this.state.activeLayers[k] = false);
if (system === "skeleton") this.state.activeLayers.skeleton = true;
if (system === "muscles") this.state.activeLayers.muscles = true;
if (system === "organs") this.state.activeLayers.organs = true;
if (system === "circulation") { this.state.activeLayers.circulation = true; this.state.activeLayers.organs = true; }
if (system === "nervous") { this.state.activeLayers.nervous = true; this.state.activeLayers.skeleton = true; }
}
document.querySelectorAll("input[data-layer]").forEach(cb => {
cb.checked = this.state.activeLayers[cb.dataset.layer];
});
this.updateLayerVisibility();
},
setView(view) {
this.state.currentView = view;
const splitWrap = document.getElementById("split-slider-wrap");
if (view === "split") {
splitWrap.style.display = "flex";
Object.keys(this.state.activeLayers).forEach(k => this.state.activeLayers[k] = true);
document.querySelectorAll("input[data-layer]").forEach(cb => cb.checked = true);
} else {
splitWrap.style.display = "none";
}
this.renderSvgLayers();
},
setExertionState(state) {
this.state.exertionState = state;
this.updateExertionUI();
},
updateExertionUI() {
const data = PhysiologyData[this.state.exertionState];
if (!data) return;
document.getElementById("metric-hr").textContent = data.hr;
document.getElementById("metric-rf").textContent = data.rf;
document.getElementById("metric-q").textContent = data.q;
document.getElementById("metric-muscleflow").textContent = data.muscleFlow;
const heartIcon = document.querySelector(".heart-icon");
if (heartIcon) {
heartIcon.style.animationDuration = `${data.pulseMs}ms`;
}
},
toggleTeacherMode() {
this.state.teacherMode = !this.state.teacherMode;
const btn = document.getElementById("btn-teacher-mode");
btn.classList.toggle("active", this.state.teacherMode);
document.body.classList.toggle("show-teacher-labels", this.state.teacherMode);
},
zoom(factor) {
this.state.zoomScale = Math.min(Math.max(this.state.zoomScale * factor, 0.6), 3.5);
this.applyTransform();
},
resetZoom() {
this.state.zoomScale = 1;
this.state.panX = 0;
this.state.panY = 0;
this.applyTransform();
},
applyTransform() {
const root = document.getElementById("svg-transform-root");
if (root) {
root.setAttribute("transform", `translate(${this.state.panX}, ${this.state.panY}) scale(${this.state.zoomScale})`);
}
},
toggleOverlay(name) {
const panel = document.getElementById(`overlay-${name}`);
if (!panel) return;
const isOpen = panel.style.display === "flex";
document.querySelectorAll(".overlay-panel").forEach(p => p.style.display = "none");
panel.style.display = isOpen ? "none" : "flex";
},
closeOverlay(name) {
const panel = document.getElementById(`overlay-${name}`);
if (panel) panel.style.display = "none";
},
handleSearch(query) {
const container = document.getElementById("search-results");
if (!query || query.trim().length < 1) {
container.style.display = "none";
return;
}
const q = query.trim().toLowerCase();
const results = [];
const check = (collection) => {
Object.values(collection).forEach(item => {
const da = item.names.da.toLowerCase();
const la = item.names.la.toLowerCase();
const en = item.names.en.toLowerCase();
if (da.includes(q) || la.includes(q) || en.includes(q)) {
results.push(item);
}
});
};
check(AnatomyData.bones);
check(AnatomyData.muscles);
check(AnatomyData.organs);
check(AnatomyData.vessels);
check(AnatomyData.nervous);
container.innerHTML = "";
if (results.length === 0) {
container.innerHTML = `<div style="padding:10px; font-size:11px; color:var(--text-muted);">Ingen fundet.</div>`;
container.style.display = "block";
return;
}
results.slice(0, 8).forEach(item => {
const row = document.createElement("div");
row.className = "search-item";
row.innerHTML = `
<div class="search-item-title">${item.names.da}</div>
<div class="search-item-sub">${this.getTypeLabel(item.type)} - ${item.names.la}</div>
`;
row.addEventListener("click", () => {
this.selectStructure(item.id);
container.style.display = "none";
document.getElementById("search-input").value = "";
});
container.appendChild(row);
});
container.style.display = "block";
},
toggleFavorite(id) {
const idx = this.state.favorites.indexOf(id);
if (idx > -1) {
this.state.favorites.splice(idx, 1);
} else {
this.state.favorites.push(id);
}
this.setStoredFavorites(this.state.favorites);
this.updateFavoriteButtonState(id);
this.updateFavoritesCount();
},
updateFavoriteButtonState(id) {
const btn = document.getElementById("btn-toggle-fav");
const isFav = this.state.favorites.includes(id);
btn.classList.toggle("is-favorite", isFav);
document.getElementById("btn-fav-text").textContent = isFav ? "Gemt" : "Fokusemne";
},
updateFavoritesCount() {
const btn = document.getElementById("btn-show-favorites");
if (btn) {
btn.style.color = this.state.favorites.length > 0 ? "var(--accent-amber)" : "";
}
},
showFavoritesModal() {
if (this.state.favorites.length === 0) {
alert("Du har ingen gemte fokusemner endnu. Klik på stjernen i informationspanelet for at gemme en muskel eller knogle.");
return;
}
let msg = "Dine gemte fokusemner:\n\n";
this.state.favorites.forEach((favId, i) => {
const item = this.getAnatomicalItem(favId);
if (item) {
msg += `${i + 1}. ${item.names.da} (${item.names.la})\n`;
}
});
alert(msg);
},
initMovementModule() {
const list = document.getElementById("movement-button-list");
list.innerHTML = "";
MovementData.forEach(mov => {
const btn = document.createElement("button");
btn.className = "movement-select-btn";
btn.innerHTML = `<span>${mov.name.da}</span><small style="opacity:0.7;">${mov.joint}</small>`;
btn.addEventListener("click", () => {
document.querySelectorAll(".movement-select-btn").forEach(b => b.classList.remove("active"));
btn.classList.add("active");
this.demonstrateMovement(mov);
});
list.appendChild(btn);
});
},
demonstrateMovement(mov) {
document.querySelectorAll(".anatomical-part").forEach(el => {
el.classList.remove("active-agonist", "active-antagonist");
});
mov.highlightParts.forEach(partId => {
document.querySelectorAll(`.anatomical-part[data-id="${partId}"]`).forEach(el => {
el.classList.add("active-agonist");
});
});
if (mov.antagonistParts) {
mov.antagonistParts.forEach(partId => {
document.querySelectorAll(`.anatomical-part[data-id="${partId}"]`).forEach(el => {
el.classList.add("active-antagonist");
});
});
}
const card = document.getElementById("movement-details-card");
card.style.display = "flex";
document.getElementById("movement-active-name").textContent = mov.name.da;
document.getElementById("movement-agonist-desc").textContent = mov.agonist;
document.getElementById("movement-antagonist-desc").textContent = mov.antagonist;
document.getElementById("movement-joint-desc").textContent = mov.joint;
document.getElementById("movement-didactic-text").textContent = mov.didactic;
},
initProcessModule() {
let currentProcessKey = "oxygen";
let isPlaying = false;
let currentStepIndex = 0;
let timer = null;
const playBtn = document.getElementById("btn-process-play");
const playText = document.getElementById("btn-process-play-text");
const resetBtn = document.getElementById("btn-process-reset");
const speedSelect = document.getElementById("process-speed");
const renderSteps = () => {
const proc = ProcessData[currentProcessKey];
const container = document.getElementById("process-step-indicators");
container.innerHTML = "";
proc.steps.forEach((step, idx) => {
const div = document.createElement("div");
div.className = `process-step-item ${idx === currentStepIndex ? "active" : ""}`;
div.textContent = step.name;
div.addEventListener("click", () => {
currentStepIndex = idx;
updateStepUI();
});
container.appendChild(div);
});
};
const updateStepUI = () => {
const proc = ProcessData[currentProcessKey];
const step = proc.steps[currentStepIndex];
if (!step) return;
document.getElementById("process-explanation").innerHTML = `<strong>${step.name}:</strong> ${step.desc}`;
document.querySelectorAll(".anatomical-part").forEach(el => el.classList.remove("selected"));
if (step.organ) {
document.querySelectorAll(`.anatomical-part[data-id="${step.organ}"]`).forEach(el => el.classList.add("selected"));
}
document.querySelectorAll(".process-step-item").forEach((el, idx) => {
el.classList.toggle("active", idx === currentStepIndex);
});
};
const nextStep = () => {
const proc = ProcessData[currentProcessKey];
currentStepIndex = (currentStepIndex + 1) % proc.steps.length;
updateStepUI();
};
playBtn.addEventListener("click", () => {
isPlaying = !isPlaying;
if (isPlaying) {
playText.textContent = "Pause";
const speed = parseFloat(speedSelect.value);
timer = setInterval(nextStep, 2500 / speed);
} else {
playText.textContent = "Fortsæt";
clearInterval(timer);
}
});
resetBtn.addEventListener("click", () => {
isPlaying = false;
clearInterval(timer);
playText.textContent = "Start forløb";
currentStepIndex = 0;
updateStepUI();
});
speedSelect.addEventListener("change", () => {
if (isPlaying) {
clearInterval(timer);
const speed = parseFloat(speedSelect.value);
timer = setInterval(nextStep, 2500 / speed);
}
});
document.querySelectorAll(".pill-btn[data-process]").forEach(btn => {
btn.addEventListener("click", () => {
document.querySelectorAll(".pill-btn[data-process]").forEach(b => b.classList.remove("active"));
btn.classList.add("active");
currentProcessKey = btn.dataset.process;
currentStepIndex = 0;
clearInterval(timer);
isPlaying = false;
playText.textContent = "Start forløb";
renderSteps();
updateStepUI();
});
});
renderSteps();
updateStepUI();
},
initQuizModule() {
let currentQuizType = "find";
let score = 0;
let currentQuestionIndex = 0;
const scoreElem = document.getElementById("quiz-score");
const feedbackBox = document.getElementById("quiz-feedback-box");
const nextBtn = document.getElementById("btn-next-question");
document.querySelectorAll(".pill-btn[data-quiztype]").forEach(btn => {
btn.addEventListener("click", () => {
document.querySelectorAll(".pill-btn[data-quiztype]").forEach(b => b.classList.remove("active"));
btn.classList.add("active");
currentQuizType = btn.dataset.quiztype;
currentQuestionIndex = 0;
loadQuestion();
});
});
const loadQuestion = () => {
feedbackBox.className = "quiz-feedback";
feedbackBox.textContent = "";
this.onSvgPartClickedForQuiz = null;
if (currentQuizType === "find") {
const q = QuizData.findQuestions[currentQuestionIndex % QuizData.findQuestions.length];
document.getElementById("quiz-mode-title").textContent = "Find på kroppen";
document.getElementById("quiz-question-text").textContent = q.text;
document.getElementById("quiz-instruction").textContent = "Klik direkte på kropsdelen på modellen.";
document.getElementById("quiz-interaction-area").innerHTML = `<p style="font-size:11px; color:var(--text-muted); font-style:italic;">Hint: ${q.hint}</p>`;
this.onSvgPartClickedForQuiz = (clickedId) => {
if (clickedId === q.targetId) {
score++;
scoreElem.textContent = score;
feedbackBox.className = "quiz-feedback show success";
feedbackBox.textContent = "Rigtigt! Du fandt den korrekte struktur.";
} else {
feedbackBox.className = "quiz-feedback show failure";
feedbackBox.textContent = "Ikke helt. Prøv igen eller kig på hintet.";
}
};
} else if (currentQuizType === "name") {
const q = QuizData.nameQuestions[currentQuestionIndex % QuizData.nameQuestions.length];
document.getElementById("quiz-mode-title").textContent = "Hvad hedder den?";
document.getElementById("quiz-question-text").textContent = q.question;
document.getElementById("quiz-instruction").textContent = "Vælg det korrekte svar.";
document.querySelectorAll(".anatomical-part").forEach(el => el.classList.remove("selected"));
document.querySelectorAll(`.anatomical-part[data-id="${q.partId}"]`).forEach(el => el.classList.add("selected"));
const area = document.getElementById("quiz-interaction-area");
area.innerHTML = '<div class="quiz-options-list"></div>';
const list = area.querySelector(".quiz-options-list");
q.options.forEach((opt, idx) => {
const btn = document.createElement("button");
btn.className = "quiz-option-btn";
btn.textContent = opt;
btn.addEventListener("click", () => {
if (idx === q.correctIndex) {
btn.classList.add("correct");
score++;
scoreElem.textContent = score;
feedbackBox.className = "quiz-feedback show success";
feedbackBox.textContent = q.feedback;
} else {
btn.classList.add("incorrect");
feedbackBox.className = "quiz-feedback show failure";
feedbackBox.textContent = "Forkert. Prøv igen.";
}
});
list.appendChild(btn);
});
} else if (currentQuizType === "match") {
document.getElementById("quiz-mode-title").textContent = "Dansk ↔ Latin Parring";
document.getElementById("quiz-question-text").textContent = "Match danske og latinske betegnelser";
document.getElementById("quiz-instruction").textContent = "Klik på et dansk ord og derefter det latinske.";
const area = document.getElementById("quiz-interaction-area");
area.innerHTML = '<div class="match-grid" id="match-cards-wrap"></div>';
const grid = area.querySelector("#match-cards-wrap");
let selectedDa = null;
let selectedLa = null;
const pairs = QuizData.matchPairs;
const daList = pairs.map(p => p.da).sort(() => Math.random() - 0.5);
const laList = pairs.map(p => p.la).sort(() => Math.random() - 0.5);
daList.forEach(item => {
const card = document.createElement("div");
card.className = "match-card";
card.textContent = item;
card.dataset.type = "da";
card.dataset.val = item;
card.addEventListener("click", () => handleCardClick(card));
grid.appendChild(card);
});
laList.forEach(item => {
const card = document.createElement("div");
card.className = "match-card";
card.textContent = item;
card.dataset.type = "la";
card.dataset.val = item;
card.addEventListener("click", () => handleCardClick(card));
grid.appendChild(card);
});
const handleCardClick = (card) => {
if (card.classList.contains("matched")) return;
if (card.dataset.type === "da") {
document.querySelectorAll('.match-card[data-type="da"]').forEach(c => c.classList.remove("selected"));
card.classList.add("selected");
selectedDa = card;
} else {
document.querySelectorAll('.match-card[data-type="la"]').forEach(c => c.classList.remove("selected"));
card.classList.add("selected");
selectedLa = card;
}
if (selectedDa && selectedLa) {
const pair = pairs.find(p => p.da === selectedDa.dataset.val && p.la === selectedLa.dataset.val);
if (pair) {
selectedDa.classList.add("matched");
selectedLa.classList.add("matched");
selectedDa.classList.remove("selected");
selectedLa.classList.remove("selected");
score++;
scoreElem.textContent = score;
selectedDa = null;
selectedLa = null;
} else {
selectedDa.classList.remove("selected");
selectedLa.classList.remove("selected");
selectedDa = null;
selectedLa = null;
}
}
};
} else if (currentQuizType === "movement") {
const q = QuizData.movementQuestions[currentQuestionIndex % QuizData.movementQuestions.length];
document.getElementById("quiz-mode-title").textContent = "Muskel & Bevægelse";
document.getElementById("quiz-question-text").textContent = q.question;
document.getElementById("quiz-instruction").textContent = "Vælg den korrekte biomekaniske forklaring.";
const area = document.getElementById("quiz-interaction-area");
area.innerHTML = '<div class="quiz-options-list"></div>';
const list = area.querySelector(".quiz-options-list");
q.options.forEach((opt, idx) => {
const btn = document.createElement("button");
btn.className = "quiz-option-btn";
btn.textContent = opt;
btn.addEventListener("click", () => {
if (idx === q.correctIndex) {
btn.classList.add("correct");
score++;
scoreElem.textContent = score;
feedbackBox.className = "quiz-feedback show success";
feedbackBox.textContent = q.feedback;
} else {
btn.classList.add("incorrect");
feedbackBox.className = "quiz-feedback show failure";
feedbackBox.textContent = "Ikke helt korrekt. Prøv igen.";
}
});
list.appendChild(btn);
});
}
};
nextBtn.addEventListener("click", () => {
currentQuestionIndex++;
loadQuestion();
});
loadQuestion();
}
};
if (typeof window !== "undefined") {
window.AppCore = AppCore;
}
// INITIALISER OMGÅENDE
function startApp() {
if (typeof AppCore !== "undefined" && !AppCore.initialized) {
AppCore.init();
}
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", startApp);
} else {
startApp();
}
if (typeof window !== "undefined" && window.addEventListener) {
window.addEventListener("load", startApp);
}
if (typeof setTimeout !== "undefined") {
setTimeout(startApp, 50);
setTimeout(startApp, 250);
}
</script>
</body>
</html>