From 79f87c25496271939c9e7792515e9dfde837f368 Mon Sep 17 00:00:00 2001
From: Matthias Nott <mnott@mnsoft.org>
Date: Wed, 18 Mar 2026 21:23:24 +0100
Subject: [PATCH] chore: add gitignore, remove FR figure duplicates
---
.gitignore | 5
business.md | 186 +++
tasks/research-api-security.md | 200 +++
Glidr.md | 6
screenshots/IMG_0739.PNG | 0
tasks/research-supermemo-sm2.md | 172 +++
screenshots/IMG_0742.PNG | 0
screenshots/ipad/ipad_home.png | 0
articles/supermemo-wired.md | 91 +
tasks/restructure_explanations.py | 275 +++++
tasks/PRD-Glidr.md | 1182 ++++++++++++++++++++++
screenshots/IMG_0741.PNG | 0
articles/how-it-works.md | 67 +
tasks/todo.md | 117 ++
screenshots/IMG_0737.PNG | 0
screenshots/IMG_0740.PNG | 0
/dev/null | 0
fix_explanation_formatting.py | 254 ++++
screenshots/IMG_0738.PNG | 0
tasks/research-content-analysis.md | 140 ++
tasks/research-tech-stack.md | 171 +++
app/glidr | 1
xcode_initial.png | 0
privacy.md | 196 +++
screenshots/IMG_0743.PNG | 0
25 files changed, 3,063 insertions(+), 0 deletions(-)
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..16df84c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+.DS_Store
+.obsidian
+__pycache__
+*.pyc
+
diff --git a/Glidr.md b/Glidr.md
new file mode 100644
index 0000000..4a84c4d
--- /dev/null
+++ b/Glidr.md
@@ -0,0 +1,6 @@
+---
+icloud-sync: true
+icloud-sync-exclude:
+ - "app/"
+---
+
diff --git a/SPL Exam Questions FR/figures/t10_q114 2.png b/SPL Exam Questions FR/figures/t10_q114 2.png
deleted file mode 100644
index 8e11b82..0000000
--- a/SPL Exam Questions FR/figures/t10_q114 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t10_q70 2.png b/SPL Exam Questions FR/figures/t10_q70 2.png
deleted file mode 100644
index 111bb39..0000000
--- a/SPL Exam Questions FR/figures/t10_q70 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t10_q77 2.png b/SPL Exam Questions FR/figures/t10_q77 2.png
deleted file mode 100644
index 8ea0232..0000000
--- a/SPL Exam Questions FR/figures/t10_q77 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t10_q88 2.png b/SPL Exam Questions FR/figures/t10_q88 2.png
deleted file mode 100644
index 6a781b3..0000000
--- a/SPL Exam Questions FR/figures/t10_q88 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t10_q94 2.png b/SPL Exam Questions FR/figures/t10_q94 2.png
deleted file mode 100644
index 069ad05..0000000
--- a/SPL Exam Questions FR/figures/t10_q94 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t20_q103 2.png b/SPL Exam Questions FR/figures/t20_q103 2.png
deleted file mode 100644
index da381c8..0000000
--- a/SPL Exam Questions FR/figures/t20_q103 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t20_q87 2.png b/SPL Exam Questions FR/figures/t20_q87 2.png
deleted file mode 100644
index 455e740..0000000
--- a/SPL Exam Questions FR/figures/t20_q87 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t20_q90 2.png b/SPL Exam Questions FR/figures/t20_q90 2.png
deleted file mode 100644
index bff75a0..0000000
--- a/SPL Exam Questions FR/figures/t20_q90 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t20_q96 2.png b/SPL Exam Questions FR/figures/t20_q96 2.png
deleted file mode 100644
index bff75a0..0000000
--- a/SPL Exam Questions FR/figures/t20_q96 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t30_q19 2.png b/SPL Exam Questions FR/figures/t30_q19 2.png
deleted file mode 100644
index aec8c85..0000000
--- a/SPL Exam Questions FR/figures/t30_q19 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t30_q20 2.png b/SPL Exam Questions FR/figures/t30_q20 2.png
deleted file mode 100644
index 4415e14..0000000
--- a/SPL Exam Questions FR/figures/t30_q20 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t30_q21 2.png b/SPL Exam Questions FR/figures/t30_q21 2.png
deleted file mode 100644
index 249e47b..0000000
--- a/SPL Exam Questions FR/figures/t30_q21 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t30_q27 2.svg b/SPL Exam Questions FR/figures/t30_q27 2.svg
deleted file mode 100644
index 0e0c8e6..0000000
--- a/SPL Exam Questions FR/figures/t30_q27 2.svg
+++ /dev/null
@@ -1,73 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 250" width="600" height="250">
- <rect width="600" height="250" fill="white"/>
-
- <!-- Title -->
- <text x="300" y="28" font-family="Arial, sans-serif" font-size="16" font-weight="bold" text-anchor="middle" fill="black">ICAO Chart Symbols — Obstacles</text>
-
- <!-- ===== A) Single lighted obstacle ===== -->
- <g transform="translate(75, 125)">
- <!-- Filled circle (base) -->
- <circle cx="0" cy="20" r="8" fill="black"/>
- <!-- Light rays (star) -->
- <line x1="0" y1="-5" x2="0" y2="-18" stroke="black" stroke-width="1.5"/>
- <line x1="9" y1="0" x2="18" y2="-6" stroke="black" stroke-width="1.5"/>
- <line x1="-9" y1="0" x2="-18" y2="-6" stroke="black" stroke-width="1.5"/>
- <line x1="6" y1="-9" x2="13" y2="-18" stroke="black" stroke-width="1.5"/>
- <line x1="-6" y1="-9" x2="-13" y2="-18" stroke="black" stroke-width="1.5"/>
- <line x1="9" y1="-5" x2="18" y2="-10" stroke="black" stroke-width="1.5"/>
- <line x1="-9" y1="-5" x2="-18" y2="-10" stroke="black" stroke-width="1.5"/>
- <!-- Label -->
- <text x="0" y="48" font-family="Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="middle" fill="black">A)</text>
- <text x="0" y="63" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="black">Single lighted</text>
- <text x="0" y="76" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="black">obstacle</text>
- </g>
-
- <!-- ===== B) Single unlighted obstacle ===== -->
- <g transform="translate(225, 125)">
- <!-- Filled circle (base) -->
- <circle cx="0" cy="20" r="8" fill="black"/>
- <!-- Label -->
- <text x="0" y="48" font-family="Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="middle" fill="black">B)</text>
- <text x="0" y="63" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="black">Single unlighted</text>
- <text x="0" y="76" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="black">obstacle</text>
- </g>
-
- <!-- ===== C) Group of lighted obstacles ===== -->
- <g transform="translate(375, 125)">
- <!-- Two filled circles side by side -->
- <circle cx="-12" cy="20" r="7" fill="black"/>
- <circle cx="12" cy="20" r="7" fill="black"/>
- <!-- Light rays above center -->
- <line x1="0" y1="-2" x2="0" y2="-16" stroke="black" stroke-width="1.5"/>
- <line x1="9" y1="2" x2="18" y2="-4" stroke="black" stroke-width="1.5"/>
- <line x1="-9" y1="2" x2="-18" y2="-4" stroke="black" stroke-width="1.5"/>
- <line x1="6" y1="-7" x2="13" y2="-16" stroke="black" stroke-width="1.5"/>
- <line x1="-6" y1="-7" x2="-13" y2="-16" stroke="black" stroke-width="1.5"/>
- <line x1="9" y1="-3" x2="18" y2="-8" stroke="black" stroke-width="1.5"/>
- <line x1="-9" y1="-3" x2="-18" y2="-8" stroke="black" stroke-width="1.5"/>
- <!-- Label -->
- <text x="0" y="48" font-family="Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="middle" fill="black">C)</text>
- <text x="0" y="63" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="black">Group of lighted</text>
- <text x="0" y="76" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="black">obstacles</text>
- </g>
-
- <!-- ===== D) Group of unlighted obstacles ===== -->
- <g transform="translate(525, 125)">
- <!-- Two filled circles side by side -->
- <circle cx="-12" cy="20" r="7" fill="black"/>
- <circle cx="12" cy="20" r="7" fill="black"/>
- <!-- Label -->
- <text x="0" y="48" font-family="Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="middle" fill="black">D)</text>
- <text x="0" y="63" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="black">Group of unlighted</text>
- <text x="0" y="76" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="black">obstacles</text>
- </g>
-
- <!-- Dividers -->
- <line x1="150" y1="50" x2="150" y2="220" stroke="#cccccc" stroke-width="1" stroke-dasharray="4,4"/>
- <line x1="300" y1="50" x2="300" y2="220" stroke="#cccccc" stroke-width="1" stroke-dasharray="4,4"/>
- <line x1="450" y1="50" x2="450" y2="220" stroke="#cccccc" stroke-width="1" stroke-dasharray="4,4"/>
-
- <!-- Border -->
- <rect width="598" height="248" x="1" y="1" fill="none" stroke="#333333" stroke-width="1"/>
-</svg>
diff --git a/SPL Exam Questions FR/figures/t30_q28 2.svg b/SPL Exam Questions FR/figures/t30_q28 2.svg
deleted file mode 100644
index a725669..0000000
--- a/SPL Exam Questions FR/figures/t30_q28 2.svg
+++ /dev/null
@@ -1,64 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 250" width="600" height="250">
- <rect width="600" height="250" fill="white"/>
-
- <!-- Title -->
- <text x="300" y="28" font-family="Arial, sans-serif" font-size="16" font-weight="bold" text-anchor="middle" fill="black">ICAO Chart Symbols — Airports</text>
-
- <!-- ===== A) Civil airport with paved runway ===== -->
- <g transform="translate(75, 120)">
- <!-- Circle -->
- <circle cx="0" cy="0" r="18" fill="none" stroke="black" stroke-width="2"/>
- <!-- Runway line through center (horizontal) -->
- <rect x="-5" y="-22" width="10" height="44" fill="black" rx="2"/>
- <!-- Label -->
- <text x="0" y="38" font-family="Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="middle" fill="black">A)</text>
- <text x="0" y="53" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="black">Civil airport</text>
- <text x="0" y="66" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="black">paved runway</text>
- </g>
-
- <!-- ===== B) Military airport ===== -->
- <g transform="translate(225, 120)">
- <!-- Circle with flag/military cross -->
- <circle cx="0" cy="0" r="18" fill="none" stroke="black" stroke-width="2"/>
- <!-- Runway line -->
- <rect x="-5" y="-22" width="10" height="44" fill="black" rx="2"/>
- <!-- Military crossbar (shorter horizontal bar across runway) -->
- <rect x="-18" y="-4" width="36" height="8" fill="black" rx="1"/>
- <!-- Label -->
- <text x="0" y="38" font-family="Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="middle" fill="black">B)</text>
- <text x="0" y="53" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="black">Military airport</text>
- <text x="0" y="66" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="black">paved runway</text>
- </g>
-
- <!-- ===== C) Civil airport with unpaved runway ===== -->
- <g transform="translate(375, 120)">
- <!-- Circle only, no fill runway bar -->
- <circle cx="0" cy="0" r="18" fill="none" stroke="black" stroke-width="2"/>
- <!-- Runway line (open/outline style to show unpaved) -->
- <rect x="-5" y="-22" width="10" height="44" fill="none" stroke="black" stroke-width="2" rx="2"/>
- <!-- Label -->
- <text x="0" y="38" font-family="Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="middle" fill="black">C)</text>
- <text x="0" y="53" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="black">Civil airport</text>
- <text x="0" y="66" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="black">unpaved runway</text>
- </g>
-
- <!-- ===== D) Heliport ===== -->
- <g transform="translate(525, 120)">
- <!-- Square with H -->
- <rect x="-20" y="-20" width="40" height="40" fill="none" stroke="black" stroke-width="2"/>
- <text x="0" y="8" font-family="Arial, sans-serif" font-size="24" font-weight="bold" text-anchor="middle" fill="black">H</text>
- <!-- Label -->
- <text x="0" y="38" font-family="Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="middle" fill="black">D)</text>
- <text x="0" y="53" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="black">Heliport</text>
- <text x="0" y="66" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="black"> </text>
- </g>
-
- <!-- Dividers -->
- <line x1="150" y1="50" x2="150" y2="220" stroke="#cccccc" stroke-width="1" stroke-dasharray="4,4"/>
- <line x1="300" y1="50" x2="300" y2="220" stroke="#cccccc" stroke-width="1" stroke-dasharray="4,4"/>
- <line x1="450" y1="50" x2="450" y2="220" stroke="#cccccc" stroke-width="1" stroke-dasharray="4,4"/>
-
- <!-- Border -->
- <rect width="598" height="248" x="1" y="1" fill="none" stroke="#333333" stroke-width="1"/>
-</svg>
diff --git a/SPL Exam Questions FR/figures/t30_q29 2.svg b/SPL Exam Questions FR/figures/t30_q29 2.svg
deleted file mode 100644
index ab158ba..0000000
--- a/SPL Exam Questions FR/figures/t30_q29 2.svg
+++ /dev/null
@@ -1,67 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 250" width="600" height="250">
- <rect width="600" height="250" fill="white"/>
-
- <!-- Title -->
- <text x="300" y="28" font-family="Arial, sans-serif" font-size="16" font-weight="bold" text-anchor="middle" fill="black">ICAO Chart Symbols — Spot Elevations</text>
-
- <!-- ===== A) General spot elevation ===== -->
- <g transform="translate(75, 120)">
- <!-- Small dot -->
- <circle cx="0" cy="0" r="3" fill="black"/>
- <!-- Elevation number next to dot -->
- <text x="10" y="5" font-family="Arial, sans-serif" font-size="14" fill="black">1234</text>
- <!-- Label -->
- <text x="20" y="38" font-family="Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="middle" fill="black">A)</text>
- <text x="20" y="53" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="black">General spot</text>
- <text x="20" y="66" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="black">elevation</text>
- </g>
-
- <!-- ===== B) Highest spot elevation on chart ===== -->
- <g transform="translate(225, 120)">
- <!-- Larger bold dot -->
- <circle cx="0" cy="0" r="5" fill="black"/>
- <!-- Bold elevation number -->
- <text x="10" y="6" font-family="Arial, sans-serif" font-size="16" font-weight="bold" fill="black">4808</text>
- <!-- Underline to indicate highest -->
- <line x1="10" y1="10" x2="54" y2="10" stroke="black" stroke-width="1.5"/>
- <!-- Label -->
- <text x="25" y="38" font-family="Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="middle" fill="black">B)</text>
- <text x="25" y="53" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="black">Highest spot</text>
- <text x="25" y="66" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="black">elevation on chart</text>
- </g>
-
- <!-- ===== C) Mountain peak / summit (filled triangle) ===== -->
- <g transform="translate(390, 120)">
- <!-- Filled triangle pointing up -->
- <polygon points="0,-22 -16,12 16,12" fill="black"/>
- <!-- Elevation number -->
- <text x="22" y="-10" font-family="Arial, sans-serif" font-size="13" fill="black">2962</text>
- <!-- Label -->
- <text x="0" y="38" font-family="Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="middle" fill="black">C)</text>
- <text x="0" y="53" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="black">Mountain peak</text>
- <text x="0" y="66" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="black">/ summit</text>
- </g>
-
- <!-- ===== D) Trigonometric point ===== -->
- <g transform="translate(530, 120)">
- <!-- Open triangle -->
- <polygon points="0,-22 -16,12 16,12" fill="none" stroke="black" stroke-width="2"/>
- <!-- Dot in center -->
- <circle cx="0" cy="3" r="3" fill="black"/>
- <!-- Elevation number -->
- <text x="22" y="-10" font-family="Arial, sans-serif" font-size="13" fill="black">1543</text>
- <!-- Label -->
- <text x="0" y="38" font-family="Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="middle" fill="black">D)</text>
- <text x="0" y="53" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="black">Trigonometric</text>
- <text x="0" y="66" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="black">point</text>
- </g>
-
- <!-- Dividers -->
- <line x1="150" y1="50" x2="150" y2="220" stroke="#cccccc" stroke-width="1" stroke-dasharray="4,4"/>
- <line x1="300" y1="50" x2="300" y2="220" stroke="#cccccc" stroke-width="1" stroke-dasharray="4,4"/>
- <line x1="450" y1="50" x2="450" y2="220" stroke="#cccccc" stroke-width="1" stroke-dasharray="4,4"/>
-
- <!-- Border -->
- <rect width="598" height="248" x="1" y="1" fill="none" stroke="#333333" stroke-width="1"/>
-</svg>
diff --git a/SPL Exam Questions FR/figures/t30_q36 2.png b/SPL Exam Questions FR/figures/t30_q36 2.png
deleted file mode 100644
index a950bf2..0000000
--- a/SPL Exam Questions FR/figures/t30_q36 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t30_q40 2.png b/SPL Exam Questions FR/figures/t30_q40 2.png
deleted file mode 100644
index 27eb565..0000000
--- a/SPL Exam Questions FR/figures/t30_q40 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t30_q41 2.png b/SPL Exam Questions FR/figures/t30_q41 2.png
deleted file mode 100644
index 810383b..0000000
--- a/SPL Exam Questions FR/figures/t30_q41 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t30_q44 2.png b/SPL Exam Questions FR/figures/t30_q44 2.png
deleted file mode 100644
index e933014..0000000
--- a/SPL Exam Questions FR/figures/t30_q44 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t30_q46 2.png b/SPL Exam Questions FR/figures/t30_q46 2.png
deleted file mode 100644
index 63e066a..0000000
--- a/SPL Exam Questions FR/figures/t30_q46 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t30_q47 2.png b/SPL Exam Questions FR/figures/t30_q47 2.png
deleted file mode 100644
index 7cb969b..0000000
--- a/SPL Exam Questions FR/figures/t30_q47 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t30_q57 2.png b/SPL Exam Questions FR/figures/t30_q57 2.png
deleted file mode 100644
index 7f5534c..0000000
--- a/SPL Exam Questions FR/figures/t30_q57 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t30_q58 2.png b/SPL Exam Questions FR/figures/t30_q58 2.png
deleted file mode 100644
index 16a5e24..0000000
--- a/SPL Exam Questions FR/figures/t30_q58 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t30_q59 2.png b/SPL Exam Questions FR/figures/t30_q59 2.png
deleted file mode 100644
index 4fc3118..0000000
--- a/SPL Exam Questions FR/figures/t30_q59 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t30_q61 2.png b/SPL Exam Questions FR/figures/t30_q61 2.png
deleted file mode 100644
index 16a5e24..0000000
--- a/SPL Exam Questions FR/figures/t30_q61 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t30_q62 2.png b/SPL Exam Questions FR/figures/t30_q62 2.png
deleted file mode 100644
index 8435e09..0000000
--- a/SPL Exam Questions FR/figures/t30_q62 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t30_q63 2.png b/SPL Exam Questions FR/figures/t30_q63 2.png
deleted file mode 100644
index 16a5e24..0000000
--- a/SPL Exam Questions FR/figures/t30_q63 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t30_q68 2.png b/SPL Exam Questions FR/figures/t30_q68 2.png
deleted file mode 100644
index 4dd22b9..0000000
--- a/SPL Exam Questions FR/figures/t30_q68 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t30_q69 2.png b/SPL Exam Questions FR/figures/t30_q69 2.png
deleted file mode 100644
index 4dd22b9..0000000
--- a/SPL Exam Questions FR/figures/t30_q69 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t30_q72 2.png b/SPL Exam Questions FR/figures/t30_q72 2.png
deleted file mode 100644
index 2a8577d..0000000
--- a/SPL Exam Questions FR/figures/t30_q72 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t30_q75 2.png b/SPL Exam Questions FR/figures/t30_q75 2.png
deleted file mode 100644
index 16a5e24..0000000
--- a/SPL Exam Questions FR/figures/t30_q75 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t30_q77 2.png b/SPL Exam Questions FR/figures/t30_q77 2.png
deleted file mode 100644
index 16a5e24..0000000
--- a/SPL Exam Questions FR/figures/t30_q77 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t30_q80 2.png b/SPL Exam Questions FR/figures/t30_q80 2.png
deleted file mode 100644
index 16a5e24..0000000
--- a/SPL Exam Questions FR/figures/t30_q80 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t30_q81 2.png b/SPL Exam Questions FR/figures/t30_q81 2.png
deleted file mode 100644
index eaaf438..0000000
--- a/SPL Exam Questions FR/figures/t30_q81 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t30_q82 2.png b/SPL Exam Questions FR/figures/t30_q82 2.png
deleted file mode 100644
index eaaf438..0000000
--- a/SPL Exam Questions FR/figures/t30_q82 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t30_q83 2.png b/SPL Exam Questions FR/figures/t30_q83 2.png
deleted file mode 100644
index 052de52..0000000
--- a/SPL Exam Questions FR/figures/t30_q83 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t30_q88 2.png b/SPL Exam Questions FR/figures/t30_q88 2.png
deleted file mode 100644
index 639e407..0000000
--- a/SPL Exam Questions FR/figures/t30_q88 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t30_q91 2.png b/SPL Exam Questions FR/figures/t30_q91 2.png
deleted file mode 100644
index aec8c85..0000000
--- a/SPL Exam Questions FR/figures/t30_q91 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t30_q92 2.png b/SPL Exam Questions FR/figures/t30_q92 2.png
deleted file mode 100644
index 4415e14..0000000
--- a/SPL Exam Questions FR/figures/t30_q92 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t30_q93 2.png b/SPL Exam Questions FR/figures/t30_q93 2.png
deleted file mode 100644
index 249e47b..0000000
--- a/SPL Exam Questions FR/figures/t30_q93 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t30_q94 2.png b/SPL Exam Questions FR/figures/t30_q94 2.png
deleted file mode 100644
index b6e3424..0000000
--- a/SPL Exam Questions FR/figures/t30_q94 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t30_q95 2.png b/SPL Exam Questions FR/figures/t30_q95 2.png
deleted file mode 100644
index a3d1d44..0000000
--- a/SPL Exam Questions FR/figures/t30_q95 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t30_q96 2.png b/SPL Exam Questions FR/figures/t30_q96 2.png
deleted file mode 100644
index c916e17..0000000
--- a/SPL Exam Questions FR/figures/t30_q96 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t40_q111 2.png b/SPL Exam Questions FR/figures/t40_q111 2.png
deleted file mode 100644
index 2544f98..0000000
--- a/SPL Exam Questions FR/figures/t40_q111 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t40_q114 2.png b/SPL Exam Questions FR/figures/t40_q114 2.png
deleted file mode 100644
index 2544f98..0000000
--- a/SPL Exam Questions FR/figures/t40_q114 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t40_q48 2.svg b/SPL Exam Questions FR/figures/t40_q48 2.svg
deleted file mode 100644
index 2a6e1a1..0000000
--- a/SPL Exam Questions FR/figures/t40_q48 2.svg
+++ /dev/null
@@ -1,102 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 350" width="500" height="350" style="background:white; font-family: Arial, sans-serif;">
-
- <defs>
- <marker id="arrowAxis" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
- <polygon points="0,0 10,3.5 0,7" fill="black"/>
- </marker>
- </defs>
-
- <!-- Axes -->
- <!-- Y axis (Performance) -->
- <line x1="70" y1="290" x2="70" y2="30" stroke="black" stroke-width="2" marker-end="url(#arrowAxis)"/>
- <!-- X axis (Arousal) -->
- <line x1="70" y1="290" x2="460" y2="290" stroke="black" stroke-width="2" marker-end="url(#arrowAxis)"/>
-
- <!-- Axis labels -->
- <text x="250" y="325" text-anchor="middle" font-size="15" font-weight="bold" fill="black">A (Arousal / Stress)</text>
- <!-- Y-axis label (rotated) -->
- <text x="22" y="165" text-anchor="middle" font-size="15" font-weight="bold" fill="black"
- transform="rotate(-90, 22, 165)">P (Performance)</text>
-
- <!-- Inverted-U curve
- X range: 70 to 450 (arousal: low to high)
- Y range: 290 (low) to 50 (high performance)
- Peak at arousal midpoint ~x=260, y=55
- A: (90, 270) low arousal, low performance
- B: (260, 55) peak
- C: (360, 140) high arousal, declining
- D: (430, 270) very high, very low
-
- Bezier: from A(90,270) through B(260,55) to D(430,270)
- Control points to create smooth inverted-U:
- CP1: (155, 55) pulling curve up
- CP2: (355, 55) holding it up then falling
- -->
- <path d="M 90,270 C 155,55 355,55 430,270"
- fill="none" stroke="#2255aa" stroke-width="3"/>
-
- <!-- Shaded zone around peak (optimal performance zone) -->
- <!-- Light band between x=200 and x=320 -->
- <path d="M 200,290 L 200,78 C 225,60 295,60 320,78 L 320,290 Z"
- fill="#e8f0ff" stroke="none" opacity="0.5"/>
-
- <!-- Point A: low arousal, low performance -->
- <!-- On curve at x=90: y=270 -->
- <circle cx="90" cy="270" r="7" fill="#c00" stroke="black" stroke-width="1.5"/>
- <text x="70" y="265" text-anchor="end" font-size="14" font-weight="bold" fill="#c00">A</text>
- <text x="55" y="248" text-anchor="middle" font-size="11" fill="#444">Low arousal,</text>
- <text x="55" y="261" text-anchor="middle" font-size="11" fill="#444">low performance</text>
-
- <!-- Point B: optimal, peak performance -->
- <!-- On curve at x=260, peak: y ~ 55 + small deviation from bezier calc -->
- <!-- At t=0.5 for cubic bezier A(90,270) CP1(155,55) CP2(355,55) D(430,270):
- x = (1-t)^3*90 + 3(1-t)^2*t*155 + 3(1-t)*t^2*355 + t^3*430
- = 0.125*90 + 0.375*155 + 0.375*355 + 0.125*430
- = 11.25 + 58.125 + 133.125 + 53.75 = 256.25
- y = 0.125*270 + 0.375*55 + 0.375*55 + 0.125*270
- = 33.75 + 20.625 + 20.625 + 33.75 = 108.75
- Hmm, mid-bezier y=109, not 55. The peak is NOT at t=0.5 for this bezier.
- The actual peak (minimum y) is at the top of the curve.
- Since CP1.y = CP2.y = 55, and A.y=D.y=270, the peak of the curve is AT y=55.
- The x-midpoint of control points: (155+355)/2 = 255. So peak is around x=255, y close to 55. -->
- <!-- Let's just use x=258, y=57 for point B (approximately correct) -->
- <circle cx="258" cy="62" r="7" fill="#007700" stroke="black" stroke-width="1.5"/>
- <text x="258" y="50" text-anchor="middle" font-size="14" font-weight="bold" fill="#007700">B</text>
- <text x="258" y="35" text-anchor="middle" font-size="12" fill="#007700" font-weight="bold">Optimal</text>
- <text x="258" y="18" text-anchor="middle" font-size="11" fill="#444">Peak performance</text>
-
- <!-- Point C: high arousal, declining -->
- <!-- Approximate on curve: x=360 -->
- <!-- t such that x=360:
- 90(1-t)^3 + 3*155(1-t)^2*t + 3*355(1-t)*t^2 + 430*t^3 = 360
- Rough estimate: t~0.73 gives x~360
- y at t=0.73: 0.0219*270 + 3*0.0729*0.73*55 + 3*0.27*0.5329*55 + 0.389*270
- = 5.9 + 8.8 + 23.8 + 105 = 143.5 ≈ 144 -->
- <circle cx="362" cy="144" r="7" fill="#e87000" stroke="black" stroke-width="1.5"/>
- <text x="375" y="140" text-anchor="start" font-size="14" font-weight="bold" fill="#e87000">C</text>
- <text x="390" y="125" text-anchor="middle" font-size="11" fill="#444">High arousal,</text>
- <text x="390" y="138" text-anchor="middle" font-size="11" fill="#444">declining</text>
-
- <!-- Point D: very high arousal, very low performance -->
- <circle cx="430" cy="270" r="7" fill="#c00" stroke="black" stroke-width="1.5"/>
- <text x="445" y="268" text-anchor="start" font-size="14" font-weight="bold" fill="#c00">D</text>
- <text x="445" y="285" text-anchor="start" font-size="11" fill="#444">Very low</text>
- <text x="445" y="298" text-anchor="start" font-size="11" fill="#444">performance</text>
-
- <!-- Axis tick labels -->
- <text x="65" y="295" text-anchor="end" font-size="11" fill="#666">Low</text>
- <text x="455" y="295" text-anchor="end" font-size="11" fill="#666">High</text>
- <text x="65" y="295" text-anchor="end" font-size="11" fill="#666">Low</text>
-
- <!-- Y axis: Low at bottom, High at top -->
- <text x="65" y="290" text-anchor="end" font-size="11" fill="#666">Low</text>
- <text x="65" y="50" text-anchor="end" font-size="11" fill="#666">High</text>
-
- <!-- Optimal zone label -->
- <text x="260" y="215" text-anchor="middle" font-size="11" fill="#2255aa" font-style="italic">Optimal zone</text>
-
- <!-- Title -->
- <text x="250" y="345" text-anchor="middle" font-size="14" font-weight="bold" fill="black">Yerkes-Dodson Curve</text>
-
-</svg>
diff --git a/SPL Exam Questions FR/figures/t40_q49 2.png b/SPL Exam Questions FR/figures/t40_q49 2.png
deleted file mode 100644
index 2544f98..0000000
--- a/SPL Exam Questions FR/figures/t40_q49 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t50_q101 2.png b/SPL Exam Questions FR/figures/t50_q101 2.png
deleted file mode 100644
index 0a89941..0000000
--- a/SPL Exam Questions FR/figures/t50_q101 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t50_q103 2.png b/SPL Exam Questions FR/figures/t50_q103 2.png
deleted file mode 100644
index 67e2206..0000000
--- a/SPL Exam Questions FR/figures/t50_q103 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t50_q107 2.png b/SPL Exam Questions FR/figures/t50_q107 2.png
deleted file mode 100644
index 54da553..0000000
--- a/SPL Exam Questions FR/figures/t50_q107 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t50_q129 2.png b/SPL Exam Questions FR/figures/t50_q129 2.png
deleted file mode 100644
index 5ff7c31..0000000
--- a/SPL Exam Questions FR/figures/t50_q129 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t50_q131 2.png b/SPL Exam Questions FR/figures/t50_q131 2.png
deleted file mode 100644
index 8038d79..0000000
--- a/SPL Exam Questions FR/figures/t50_q131 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t50_q145 2.png b/SPL Exam Questions FR/figures/t50_q145 2.png
deleted file mode 100644
index 8038d79..0000000
--- a/SPL Exam Questions FR/figures/t50_q145 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t50_q162 2.png b/SPL Exam Questions FR/figures/t50_q162 2.png
deleted file mode 100644
index fdf5ad2..0000000
--- a/SPL Exam Questions FR/figures/t50_q162 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t50_q179 2.png b/SPL Exam Questions FR/figures/t50_q179 2.png
deleted file mode 100644
index d176768..0000000
--- a/SPL Exam Questions FR/figures/t50_q179 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t50_q182 2.png b/SPL Exam Questions FR/figures/t50_q182 2.png
deleted file mode 100644
index 8038d79..0000000
--- a/SPL Exam Questions FR/figures/t50_q182 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t50_q200 2.png b/SPL Exam Questions FR/figures/t50_q200 2.png
deleted file mode 100644
index fdf5ad2..0000000
--- a/SPL Exam Questions FR/figures/t50_q200 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t50_q57 2.png b/SPL Exam Questions FR/figures/t50_q57 2.png
deleted file mode 100644
index a98d8d0..0000000
--- a/SPL Exam Questions FR/figures/t50_q57 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t50_q61 2.png b/SPL Exam Questions FR/figures/t50_q61 2.png
deleted file mode 100644
index 999cb9b..0000000
--- a/SPL Exam Questions FR/figures/t50_q61 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t50_q65 2.png b/SPL Exam Questions FR/figures/t50_q65 2.png
deleted file mode 100644
index 38673d7..0000000
--- a/SPL Exam Questions FR/figures/t50_q65 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t50_q66 2.png b/SPL Exam Questions FR/figures/t50_q66 2.png
deleted file mode 100644
index 0e21b36..0000000
--- a/SPL Exam Questions FR/figures/t50_q66 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t50_q71 2.png b/SPL Exam Questions FR/figures/t50_q71 2.png
deleted file mode 100644
index 22d5810..0000000
--- a/SPL Exam Questions FR/figures/t50_q71 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t50_q73 2.png b/SPL Exam Questions FR/figures/t50_q73 2.png
deleted file mode 100644
index d1d0ec6..0000000
--- a/SPL Exam Questions FR/figures/t50_q73 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t50_q76 2.png b/SPL Exam Questions FR/figures/t50_q76 2.png
deleted file mode 100644
index cf727e7..0000000
--- a/SPL Exam Questions FR/figures/t50_q76 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t50_q77 2.png b/SPL Exam Questions FR/figures/t50_q77 2.png
deleted file mode 100644
index 314b1fa..0000000
--- a/SPL Exam Questions FR/figures/t50_q77 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t50_q82 2.png b/SPL Exam Questions FR/figures/t50_q82 2.png
deleted file mode 100644
index b420608..0000000
--- a/SPL Exam Questions FR/figures/t50_q82 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t50_q90 2.png b/SPL Exam Questions FR/figures/t50_q90 2.png
deleted file mode 100644
index dc94e8c..0000000
--- a/SPL Exam Questions FR/figures/t50_q90 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t50_q92 2.png b/SPL Exam Questions FR/figures/t50_q92 2.png
deleted file mode 100644
index c037047..0000000
--- a/SPL Exam Questions FR/figures/t50_q92 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t50_q93 2.png b/SPL Exam Questions FR/figures/t50_q93 2.png
deleted file mode 100644
index f7eb616..0000000
--- a/SPL Exam Questions FR/figures/t50_q93 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t60_q153 2.png b/SPL Exam Questions FR/figures/t60_q153 2.png
deleted file mode 100644
index 50c8f26..0000000
--- a/SPL Exam Questions FR/figures/t60_q153 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t60_q161 2.png b/SPL Exam Questions FR/figures/t60_q161 2.png
deleted file mode 100644
index 1653876..0000000
--- a/SPL Exam Questions FR/figures/t60_q161 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t60_q164 2.png b/SPL Exam Questions FR/figures/t60_q164 2.png
deleted file mode 100644
index 8ffa2e8..0000000
--- a/SPL Exam Questions FR/figures/t60_q164 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t60_q167 2.png b/SPL Exam Questions FR/figures/t60_q167 2.png
deleted file mode 100644
index 1653876..0000000
--- a/SPL Exam Questions FR/figures/t60_q167 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t60_q171 2.png b/SPL Exam Questions FR/figures/t60_q171 2.png
deleted file mode 100644
index 1653876..0000000
--- a/SPL Exam Questions FR/figures/t60_q171 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t60_q46 2.png b/SPL Exam Questions FR/figures/t60_q46 2.png
deleted file mode 100644
index 1653876..0000000
--- a/SPL Exam Questions FR/figures/t60_q46 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t60_q6 2.svg b/SPL Exam Questions FR/figures/t60_q6 2.svg
deleted file mode 100644
index 706d4e1..0000000
--- a/SPL Exam Questions FR/figures/t60_q6 2.svg
+++ /dev/null
@@ -1,82 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400" width="400" height="400">
- <rect width="400" height="400" fill="white"/>
-
- <!-- Clip path for globe interior -->
- <defs>
- <clipPath id="globeClip">
- <circle cx="200" cy="200" r="150"/>
- </clipPath>
- </defs>
-
- <!-- Globe fill (light blue) -->
- <circle cx="200" cy="200" r="150" fill="#e8f4fc" stroke="black" stroke-width="2"/>
-
- <!-- Latitude lines (clipped to globe) -->
- <!-- 60N -->
- <ellipse cx="200" cy="125" rx="130" ry="20" fill="none" stroke="#aaaaaa" stroke-width="0.8" clip-path="url(#globeClip)"/>
- <!-- 30N -->
- <ellipse cx="200" cy="162" rx="150" ry="28" fill="none" stroke="#aaaaaa" stroke-width="0.8" clip-path="url(#globeClip)"/>
- <!-- Equator (0) — drawn separately, bold -->
- <ellipse cx="200" cy="200" rx="150" ry="32" fill="none" stroke="black" stroke-width="2" clip-path="url(#globeClip)"/>
- <!-- 30S -->
- <ellipse cx="200" cy="238" rx="150" ry="28" fill="none" stroke="#aaaaaa" stroke-width="0.8" clip-path="url(#globeClip)"/>
- <!-- 60S -->
- <ellipse cx="200" cy="275" rx="130" ry="20" fill="none" stroke="#aaaaaa" stroke-width="0.8" clip-path="url(#globeClip)"/>
-
- <!-- Longitude lines (meridians) — vertical ellipses, clipped -->
- <!-- Prime meridian (0°) -->
- <ellipse cx="200" cy="200" rx="10" ry="150" fill="none" stroke="black" stroke-width="1.5" clip-path="url(#globeClip)"/>
- <!-- 30W / 150E -->
- <ellipse cx="200" cy="200" rx="75" ry="150" fill="none" stroke="#aaaaaa" stroke-width="0.8" clip-path="url(#globeClip)"/>
- <!-- 60W / 120E -->
- <ellipse cx="200" cy="200" rx="130" ry="150" fill="none" stroke="#aaaaaa" stroke-width="0.8" clip-path="url(#globeClip)"/>
- <!-- 90W / 90E — just the axis line -->
- <line x1="200" y1="50" x2="200" y2="350" stroke="#aaaaaa" stroke-width="0.8" clip-path="url(#globeClip)"/>
-
- <!-- Globe outer border (drawn again on top to clean up edges) -->
- <circle cx="200" cy="200" r="150" fill="none" stroke="black" stroke-width="2"/>
-
- <!-- North / South pole dots -->
- <circle cx="200" cy="50" r="3" fill="black"/>
- <circle cx="200" cy="350" r="3" fill="black"/>
-
- <!-- Pole labels -->
- <text x="200" y="38" font-family="Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="black">North Pole</text>
- <text x="200" y="370" font-family="Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="black">South Pole</text>
-
- <!-- Equator label -->
- <text x="362" y="204" font-family="Arial, sans-serif" font-size="12" text-anchor="start" fill="black">Equator</text>
- <line x1="350" y1="200" x2="362" y2="202" stroke="black" stroke-width="1"/>
-
- <!-- Equator circumference annotation -->
- <text x="200" y="245" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="#333333">≈ 40,075 km / ≈ 21,600 NM</text>
-
- <!-- Axis line (N-S, dashed) -->
- <line x1="200" y1="50" x2="200" y2="350" stroke="#555555" stroke-width="1" stroke-dasharray="6,4"/>
-
- <!-- Latitude label 30N -->
- <text x="356" y="165" font-family="Arial, sans-serif" font-size="11" text-anchor="start" fill="#555555">30°N</text>
- <line x1="349" y1="162" x2="356" y2="163" stroke="#555555" stroke-width="0.8"/>
-
- <!-- Latitude label 60N -->
- <text x="338" y="128" font-family="Arial, sans-serif" font-size="11" text-anchor="start" fill="#555555">60°N</text>
- <line x1="330" y1="125" x2="338" y2="126" stroke="#555555" stroke-width="0.8"/>
-
- <!-- Latitude label 30S -->
- <text x="356" y="241" font-family="Arial, sans-serif" font-size="11" text-anchor="start" fill="#555555">30°S</text>
- <line x1="349" y1="238" x2="356" y2="239" stroke="#555555" stroke-width="0.8"/>
-
- <!-- Latitude label 60S -->
- <text x="338" y="278" font-family="Arial, sans-serif" font-size="11" text-anchor="start" fill="#555555">60°S</text>
- <line x1="330" y1="275" x2="338" y2="276" stroke="#555555" stroke-width="0.8"/>
-
- <!-- Prime meridian label -->
- <text x="200" y="395" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="#555555">0° / Prime Meridian</text>
-
- <!-- Title -->
- <text x="200" y="20" font-family="Arial, sans-serif" font-size="15" font-weight="bold" text-anchor="middle" fill="black">Earth — Latitude and Longitude</text>
-
- <!-- Border -->
- <rect width="398" height="398" x="1" y="1" fill="none" stroke="#333333" stroke-width="1"/>
-</svg>
diff --git a/SPL Exam Questions FR/figures/t80_q102 2.png b/SPL Exam Questions FR/figures/t80_q102 2.png
deleted file mode 100644
index 8ba9d9c..0000000
--- a/SPL Exam Questions FR/figures/t80_q102 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t80_q111 2.png b/SPL Exam Questions FR/figures/t80_q111 2.png
deleted file mode 100644
index 27e7e19..0000000
--- a/SPL Exam Questions FR/figures/t80_q111 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t80_q112 2.png b/SPL Exam Questions FR/figures/t80_q112 2.png
deleted file mode 100644
index 27e7e19..0000000
--- a/SPL Exam Questions FR/figures/t80_q112 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t80_q113 2.png b/SPL Exam Questions FR/figures/t80_q113 2.png
deleted file mode 100644
index e67f072..0000000
--- a/SPL Exam Questions FR/figures/t80_q113 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t80_q123 2.png b/SPL Exam Questions FR/figures/t80_q123 2.png
deleted file mode 100644
index 726dc33..0000000
--- a/SPL Exam Questions FR/figures/t80_q123 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t80_q129 2.png b/SPL Exam Questions FR/figures/t80_q129 2.png
deleted file mode 100644
index c1cb83a..0000000
--- a/SPL Exam Questions FR/figures/t80_q129 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t80_q132 2.png b/SPL Exam Questions FR/figures/t80_q132 2.png
deleted file mode 100644
index 726dc33..0000000
--- a/SPL Exam Questions FR/figures/t80_q132 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t80_q150 2.png b/SPL Exam Questions FR/figures/t80_q150 2.png
deleted file mode 100644
index 27e7e19..0000000
--- a/SPL Exam Questions FR/figures/t80_q150 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t80_q151 2.png b/SPL Exam Questions FR/figures/t80_q151 2.png
deleted file mode 100644
index c1cb83a..0000000
--- a/SPL Exam Questions FR/figures/t80_q151 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t80_q152 2.png b/SPL Exam Questions FR/figures/t80_q152 2.png
deleted file mode 100644
index 827c68c..0000000
--- a/SPL Exam Questions FR/figures/t80_q152 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t80_q66 2.png b/SPL Exam Questions FR/figures/t80_q66 2.png
deleted file mode 100644
index 88c2603..0000000
--- a/SPL Exam Questions FR/figures/t80_q66 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t80_q75 2.png b/SPL Exam Questions FR/figures/t80_q75 2.png
deleted file mode 100644
index 8786956..0000000
--- a/SPL Exam Questions FR/figures/t80_q75 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t80_q87 2.png b/SPL Exam Questions FR/figures/t80_q87 2.png
deleted file mode 100644
index 5f98e71..0000000
--- a/SPL Exam Questions FR/figures/t80_q87 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t80_q90 2.png b/SPL Exam Questions FR/figures/t80_q90 2.png
deleted file mode 100644
index 8786956..0000000
--- a/SPL Exam Questions FR/figures/t80_q90 2.png
+++ /dev/null
Binary files differ
diff --git a/SPL Exam Questions FR/figures/t80_q95 2.png b/SPL Exam Questions FR/figures/t80_q95 2.png
deleted file mode 100644
index 8a5593b..0000000
--- a/SPL Exam Questions FR/figures/t80_q95 2.png
+++ /dev/null
Binary files differ
diff --git a/app/glidr b/app/glidr
new file mode 120000
index 0000000..4bd91d8
--- /dev/null
+++ b/app/glidr
@@ -0,0 +1 @@
+/Users/i052341/dev/apps/glidr
\ No newline at end of file
diff --git a/articles/how-it-works.md b/articles/how-it-works.md
new file mode 100644
index 0000000..49eae1c
--- /dev/null
+++ b/articles/how-it-works.md
@@ -0,0 +1,67 @@
+# How Glidr Helps You Learn
+
+## The Problem with Cramming
+
+German psychologist Hermann Ebbinghaus measured in 1885 that without reinforcement, your brain discards roughly half of new information within 30 minutes, and up to 80% within 24 hours.
+
+For your SPL theory exam, forgetting is not just inconvenient - it is dangerous. You need knowledge that stays with you in the cockpit, not just until the test.
+
+## The Solution: Spaced Repetition
+
+Spaced repetition works by showing you a card just before you would forget it. Each time you successfully recall something, the next review is pushed further into the future. Each successful recall also strengthens the memory trace, making it harder to lose.
+
+The result: you spend less time reviewing things you already know well, and more time on the things that actually need attention.
+
+## The SM-2 Algorithm
+
+In 1987, Polish researcher Piotr Wozniak created SuperMemo, the first program to automate this process. His algorithm, SM-2, became the foundation for nearly every modern flashcard app - including Glidr.
+
+Every card has a personal interval (days until it appears again) and an easiness factor (how quickly that interval grows). Your ratings update both values, so the schedule adapts to you.
+
+## How Your Rating Controls Everything
+
+After each card, you rate how well you recalled the answer:
+
+- **Again** - did not remember. Card resets to tomorrow.
+- **Hard** - remembered, but a real struggle. Returns soon.
+- **Good** - remembered with normal effort. Standard growth.
+- **Easy** - instant recall. Interval jumps forward.
+
+Be honest. Rating "Easy" when you hesitated tells the algorithm to wait longer than it should - and you will have forgotten it by the time it returns.
+
+## The Spacing Effect
+
+A card rated "Good" might be scheduled for:
+
+- First review: 1 day
+- Second review: 6 days
+- Third review: ~15 days
+- Fourth review: ~38 days
+
+After three or four successful reviews, you may not see a card for months - because it no longer needs reinforcement. That is the algorithm doing exactly what it was designed to do.
+
+## Study Mode vs Cram Mode
+
+**Study mode** uses spaced repetition. Cards appear according to the SM-2 schedule - only cards due today, in the order that helps you most. This builds deep, lasting knowledge. Do not skip days if you can avoid it.
+
+**Cram mode** ignores the schedule entirely. It is useful the night before your exam as a final confidence check, or when previewing a new topic. It does not update intervals, so your long-term schedule is unaffected.
+
+- Daily sessions: Study Mode
+- Night before the exam: Cram Mode
+- New chapter preview: Cram, then switch to Study
+
+## Tips for SPL Exam Prep
+
+1. **Start early.** Begin 6-8 weeks before your exam to give the algorithm room to space intervals properly.
+
+2. **Do your due cards every day.** A consistent 10-minute session every morning is far more effective than a 2-hour marathon once a week.
+
+3. **Aim for 20-30 new cards per day.** Introducing too many creates an overwhelming backlog within a week.
+
+4. **Use the ratings honestly.** There is no score - no one sees your ratings. Admit when you struggled.
+
+5. **Trust the schedule.** If a card is not due, reviewing early does not help and disrupts the spacing that makes the system work.
+
+Good luck, and good soaring.
+
+*Based on the SM-2 algorithm by Dr. Piotr Wozniak (1987) - [supermemo.com](https://supermemo.com)*
diff --git a/articles/supermemo-wired.md b/articles/supermemo-wired.md
new file mode 100644
index 0000000..9d19293
--- /dev/null
+++ b/articles/supermemo-wired.md
@@ -0,0 +1,91 @@
+# Want to Remember Everything You'll Ever Learn? Surrender to This Algorithm
+
+*By Gary Wolf, Wired Magazine, April 2008*
+
+## The Man Behind SuperMemo
+
+The winter sun sets in mid-afternoon in Kolobrzeg, Poland, but the early twilight does not deter people from taking their regular outdoor promenade. Bundled up in parkas with fur-trimmed hoods, strolling hand in mittened hand along the edge of the Baltic Sea, off-season tourists from Germany stop openmouthed when they see a tall, well-built, nearly naked man running up and down the sand.
+
+"Kalt? Kalt?" one of them calls out. The man gives a polite but vague answer, then turns and dives into the waves. After swimming back and forth in the 40-degree water for a few minutes, he emerges from the surf and jogs briefly along the shore. The wind is strong, but the man makes no move to get dressed.
+
+Piotr Wozniak's quest for anonymity has been successful. Nobody along this string of little beach resorts recognizes him as the inventor of a technique to turn people into geniuses. A portion of this technique, embodied in a software program called SuperMemo, has enthusiastic users around the world. They apply it mainly to learning languages, and it's popular among people for whom fluency is a necessity - students from Poland or other poor countries aiming to score well enough on English-language exams to study abroad.
+
+## The Core Insight: Timing Is Everything
+
+SuperMemo is based on the insight that there is an ideal moment to practice what you've learned. Practice too soon and you waste your time. Practice too late and you've forgotten the material and have to relearn it. The right time to practice is just at the moment you're about to forget. Unfortunately, this moment is different for every person and each bit of information.
+
+Fortunately, human forgetting follows a pattern. We forget exponentially. A graph of our likelihood of getting the correct answer on a quiz sweeps quickly downward over time and then levels off. This pattern has long been known to cognitive psychology, but it has been difficult to put to practical use. It's too complex for us to employ with our naked brains.
+
+Twenty years ago, Wozniak realized that computers could easily calculate the moment of forgetting if he could discover the right algorithm. SuperMemo is the result of his research. It predicts the future state of a person's memory and schedules information reviews at the optimal time.
+
+## How SuperMemo Works
+
+SuperMemo is a program that keeps track of discrete bits of information you've learned and want to retain. Your chance of recalling a given word when you need it declines over time according to a predictable pattern. SuperMemo tracks this so-called forgetting curve and reminds you to rehearse your knowledge when your chance of recalling it has dropped to, say, 90 percent. When you first learn a new vocabulary word, your chance of recalling it will drop quickly. But after SuperMemo reminds you of the word, the rate of forgetting levels out. The program tracks this new decline and waits longer to quiz you the next time.
+
+## Ebbinghaus and the Forgetting Curve
+
+In the late 1800s, a German scientist named Hermann Ebbinghaus made up lists of nonsense syllables and measured how long it took to forget and then relearn them. In experiments of breathtaking rigor and tedium, Ebbinghaus practiced and recited from memory 2.5 nonsense syllables a second, then rested for a bit and started again. Finally, in 1885, he published a monograph called *Memory: A Contribution to Experimental Psychology*. The book became the founding classic of a new discipline.
+
+Ebbinghaus discovered many lawlike regularities of mental life. He was the first to draw a learning curve. Among his original observations was an account of a strange phenomenon: the spacing effect.
+
+## The Spacing Effect: Psychology's Best-Kept Secret
+
+Ebbinghaus showed that it's possible to dramatically improve learning by correctly spacing practice sessions. The efficiencies created by precise spacing are so large, and the improvement in performance so predictable, that from nearly the moment Ebbinghaus described the spacing effect, psychologists have been urging educators to use it to accelerate human progress.
+
+The spacing effect is "one of the most remarkable phenomena to emerge from laboratory research on learning," the psychologist Frank Dempster wrote in 1988, in a paper titled "The Spacing Effect: A Case Study in the Failure to Apply the Results of Psychological Research." How would computer scientists feel if people continued to use slide rules for engineering calculations? Psychologists who studied the spacing effect thought they possessed a solution to a problem that had frustrated humankind since before written language: how to remember what's been learned.
+
+## Wozniak's Quest: From Paper Cards to Algorithm
+
+As a student at the Poznan University of Technology in western Poland in the 1980s, Wozniak was overwhelmed by the sheer number of things he was expected to learn. He wasn't just trying to pass his exams; he was trying to learn. He couldn't help noticing that within a few months of completing a class, only a fraction of the knowledge he had so painfully acquired remained in his mind.
+
+So he created an analog database, with each entry consisting of a question and answer on a piece of paper. Every time he reviewed a word, phrase, or fact, he meticulously noted the date and marked whether he had forgotten it. By 1984, Wozniak's database contained 3,000 English words and phrases and 1,400 facts culled from biology, each with a complete repetition history.
+
+## The Impossible Math of Memorization
+
+According to Wozniak's first calculations, success was impossible. The problem wasn't learning the material; it was retaining it. He found that 40 percent of his English vocabulary vanished over time. Sixty percent of his biology answers evaporated. Using some simple calculations, he figured out that with his normal method of study, it would require two hours of practice every day to learn and retain a modest English vocabulary of 15,000 words.
+
+As Wozniak later wrote: "The process of increasing the size of my databases gradually progressed at the cost of knowledge retention." In other words, as his list grew, so did his forgetting. He was climbing a mountain of loose gravel and making less and less progress at each step.
+
+## Why Memorization Matters
+
+"The people who criticize memorization - how happy would they be to spell out every letter of every word they read?" asks Robert Bjork, chair of UCLA's psychology department and one of the most eminent memory researchers. "You can't escape memorization. There is an initial process of learning the names of things. That's a stage we all go through. It's all the more important to go through it rapidly."
+
+## Retrieval Strength vs Storage Strength
+
+Long-term memory, the Bjorks said, can be characterized by two components: retrieval strength and storage strength. Retrieval strength measures how likely you are to recall something right now. Storage strength measures how deeply the memory is rooted.
+
+Some memories may have high storage strength but low retrieval strength. Take an old address or phone number. Try to think of it; you may feel that it's gone. But a single reminder could be enough to restore it for months or years. Conversely, some memories have high retrieval strength but low storage strength - easily accessible now but likely forgotten in days.
+
+The amount of storage strength you gain from practice is inversely correlated with the current retrieval strength. In other words, the harder you have to work to get the right answer, the more the answer is sealed in memory. Precisely those things that seem to signal we're learning well - easy performance on drills, fluency during a lesson - are misleading when it comes to predicting whether we will remember it in the future.
+
+## The Optimal Moment to Study
+
+Robert Bjork, working with Thomas Landauer of Bell Labs, published results involving nearly 700 undergraduate students. They were looking for the optimal moment to rehearse something so that it would later be remembered. Their results were impressive: **The best time to study something is at the moment you are about to forget it.**
+
+Obviously, computers were the answer. What was needed was not an academic psychologist but a tinkerer, somebody with a lot of time on his hands, a talent for mathematics, and a strangely literal temperament that made him think he should actually recall the things he learned.
+
+## From Punch Cards to Personal Computers
+
+All of Wozniak's early work was done on paper. "We had a single mainframe of Polish-Russian design, with punch cards," he recalls. The personal computer revolution was already far along in the US by the time Wozniak managed to get his hands on an Amstrad PC 1512, imported through quasi-legal means from Hamburg, Germany. With this he was able to compute the difficulty of any fact or study item and adjust the predicted forgetting curve for every item and user.
+
+After the collapse of Polish communism, Wozniak and some fellow students formed a company, SuperMemo World. By 1995, their program became the first Polish product shown at Comdex in Las Vegas.
+
+## The Algorithmic Life
+
+The reason the inventor of SuperMemo pursues extreme anonymity is not because he's paranoid but because he wants to avoid random interruptions to a long-running experiment he's conducting on himself. Wozniak is a kind of algorithmic man. He's exploring what it's like to live in strict obedience to reason.
+
+His days are blocked into distinct periods: a creative period, a reading and studying period, an exercise period, an eating period, a resting period, and then a second creative period. He doesn't get up at a regular hour and is passionate against alarm clocks.
+
+## Incremental Reading: Beyond Flashcards
+
+Wozniak has invented a way to apply his learning system to unstructured information from books and articles, winnowing written material down to discrete chunks that can be memorized, and then scheduling them for efficient learning. He calls it incremental reading, and it has come to dominate his intellectual life.
+
+"Once you get the snippets you need," Wozniak says, "your books disappear. They gradually evaporate. They have been translated into knowledge."
+
+## The Cost of Genius
+
+Wozniak wrote a checklist describing how to become a genius. His advice was straightforward yet strangely terrible: You must clarify your goals, gain knowledge through spaced repetition, preserve health, work steadily, minimize stress, refuse interruption, and never resist sleep when tired. This should lead to radically improved intelligence and creativity. The only cost: turning your back on every convention of social life.
+
+It is a severe prescription. And yet, when linked to genuine rewards, even the chilliest of systems can have a certain visceral appeal. By projecting the achievement of extreme memory back along the forgetting curve, by provably linking the distant future - when we will know so much - to the few minutes we devote to studying today, Wozniak has found a way to condition his temperament along with his memory. He is making the future noticeable. He is trying not just to learn many things but to warm the process of learning itself with a draft of utopian ecstasy.
+
+*Originally published in Wired Magazine, April 21, 2008*
diff --git a/business.md b/business.md
new file mode 100644
index 0000000..bcbaba4
--- /dev/null
+++ b/business.md
@@ -0,0 +1,186 @@
+- generic [ref=e1]:
+ - banner "App Store Connect" [ref=e3]:
+ - generic [ref=e4]:
+ - heading "App Store Connect" [level=1] [ref=e20]:
+ - link "App Store Connect" [ref=e21] [cursor=pointer]:
+ - /url: /
+ - navigation "Global" [ref=e7]:
+ - list [ref=e23]:
+ - listitem [ref=e24]:
+ - link "Apps" [ref=e25] [cursor=pointer]:
+ - /url: /apps
+ - listitem [ref=e26]:
+ - link "Analytics" [ref=e27] [cursor=pointer]:
+ - /url: /analytics
+ - listitem [ref=e28]:
+ - link "Trends" [ref=e29] [cursor=pointer]:
+ - /url: /trends
+ - listitem [ref=e30]:
+ - link "Reports" [ref=e31] [cursor=pointer]:
+ - /url: /itc/payments_and_financial_reports
+ - listitem [ref=e32]:
+ - link "Business" [ref=e33] [cursor=pointer]:
+ - /url: /business
+ - listitem [ref=e34]:
+ - link "Users and Access" [ref=e35] [cursor=pointer]:
+ - /url: /access/users
+ - button "Matthias Nott Matthias Nott Account name menu" [ref=e37] [cursor=pointer]:
+ - generic:
+ - generic: Matthias Nott
+ - generic: Matthias Nott
+ - img [ref=e38]
+ - generic [ref=e46]:
+ - generic [ref=e47]:
+ - heading "Business" [level=1] [ref=e48]
+ - navigation [ref=e49]:
+ - listitem [ref=e50] [cursor=pointer]:
+ - generic [ref=e51]: Agreements
+ - generic [ref=e54]:
+ - img [ref=e55]
+ - paragraph [ref=e57]:
+ - generic [ref=e58]:
+ - text: The Apple Developer Program License Agreement has been updated and needs to be reviewed. In order to update your existing apps and submit new apps, the Account Holder must review and accept the updated agreement by signing in to their
+ - link "account" [active] [ref=e59] [cursor=pointer]:
+ - /url: https://developer.apple.com/account
+ - text: .
+ - generic [ref=e61]:
+ - heading "Matthias Nott" [level=2] [ref=e63]
+ - generic [ref=e64]:
+ - generic [ref=e65]:
+ - paragraph [ref=e66]: ch de la Tarpa 8A
+ - paragraph [ref=e67]: Troistorrents, Valais 1872
+ - paragraph [ref=e68]: Switzerland
+ - generic [ref=e69]:
+ - paragraph [ref=e71]: "85427149"
+ - generic [ref=e73]:
+ - paragraph [ref=e74]: 175 Countries or Regions
+ - paragraph [ref=e75]:
+ - button "View" [ref=e76] [cursor=pointer]
+ - generic [ref=e78]:
+ - heading "Agreements" [level=2] [ref=e79]
+ - table [ref=e80]:
+ - rowgroup [ref=e81]:
+ - row "Type Countries or Regions Effective Date Status" [ref=e82]:
+ - columnheader "Type" [ref=e83]
+ - columnheader "Countries or Regions" [ref=e84]
+ - columnheader "Effective Date" [ref=e85]
+ - columnheader "Status" [ref=e86]
+ - columnheader [ref=e87]
+ - rowgroup [ref=e88]:
+ - row "Paid Apps Agreement All Countries or Regions View 30 Dec 2025 - 30 Dec 2026 Active" [ref=e89]:
+ - cell "Paid Apps Agreement" [ref=e90]
+ - cell "All Countries or Regions View" [ref=e91]:
+ - generic [ref=e93]:
+ - paragraph [ref=e94]: All Countries or Regions
+ - paragraph [ref=e95]:
+ - button "View" [ref=e96] [cursor=pointer]
+ - cell "30 Dec 2025 - 30 Dec 2026" [ref=e97]
+ - cell "Active" [ref=e98]
+ - cell [ref=e99]:
+ - button [ref=e101] [cursor=pointer]:
+ - img [ref=e102]
+ - row "Free Apps Agreement All Countries or Regions View 30 Dec 2025 - 30 Dec 2026 Active (New Agreement Available)" [ref=e104]:
+ - cell "Free Apps Agreement" [ref=e105]
+ - cell "All Countries or Regions View" [ref=e106]:
+ - generic [ref=e108]:
+ - paragraph [ref=e109]: All Countries or Regions
+ - paragraph [ref=e110]:
+ - button "View" [ref=e111] [cursor=pointer]
+ - cell "30 Dec 2025 - 30 Dec 2026" [ref=e112]
+ - cell "Active (New Agreement Available)" [ref=e113]
+ - cell [ref=e114]
+ - generic [ref=e116]:
+ - generic [ref=e117]:
+ - heading "Bank Accounts" [level=2] [ref=e118]:
+ - text: Bank Accounts
+ - button [ref=e121] [cursor=pointer]:
+ - img [ref=e122]
+ - paragraph [ref=e124]:
+ - button "See More" [ref=e125] [cursor=pointer]
+ - table [ref=e126]:
+ - rowgroup [ref=e127]:
+ - row "Account Country or Region Bank Currency Royalty Currencies Status" [ref=e128]:
+ - columnheader "Account" [ref=e129]
+ - columnheader "Country or Region" [ref=e130]
+ - columnheader "Bank Currency" [ref=e131]
+ - columnheader "Royalty Currencies" [ref=e132]
+ - columnheader "Status" [ref=e133]
+ - columnheader [ref=e134]
+ - rowgroup [ref=e135]:
+ - row "UBS4000 (840L) Switzerland CHF USD Active" [ref=e136]:
+ - cell "UBS4000 (840L)" [ref=e137]:
+ - button "UBS4000 (840L)" [ref=e138] [cursor=pointer]
+ - cell "Switzerland" [ref=e139]
+ - cell "CHF" [ref=e140]
+ - cell "USD" [ref=e141]:
+ - paragraph [ref=e142]: USD
+ - cell "Active" [ref=e143]
+ - cell [ref=e144]:
+ - button [ref=e147] [cursor=pointer]:
+ - img [ref=e148]
+ - generic [ref=e151]:
+ - heading "Tax Forms" [level=2] [ref=e152]:
+ - text: Tax Forms
+ - button [ref=e153] [cursor=pointer]:
+ - img [ref=e154]
+ - table [ref=e156]:
+ - rowgroup [ref=e157]:
+ - row "Tax Form Nickname Date Submitted Status" [ref=e158]:
+ - columnheader "Tax Form" [ref=e159]
+ - columnheader "Nickname" [ref=e160]
+ - columnheader "Date Submitted" [ref=e161]
+ - columnheader "Status" [ref=e162]
+ - columnheader [ref=e163]
+ - rowgroup [ref=e164]:
+ - row "U.S. Certificate of Foreign Status of Beneficial Owner - 22 Oct 2011 Active" [ref=e165]:
+ - cell "U.S. Certificate of Foreign Status of Beneficial Owner" [ref=e166]:
+ - button "U.S. Certificate of Foreign Status of Beneficial Owner" [ref=e167] [cursor=pointer]
+ - cell "-" [ref=e168]
+ - cell "22 Oct 2011" [ref=e169]
+ - cell "Active" [ref=e170]:
+ - paragraph [ref=e171]:
+ - generic [ref=e172]: Active
+ - cell [ref=e173]
+ - generic [ref=e175]:
+ - heading "Compliance" [level=2] [ref=e176]
+ - table [ref=e177]:
+ - rowgroup [ref=e178]:
+ - row "Regulation Countries or Regions Last Updated Status" [ref=e179]:
+ - columnheader "Regulation" [ref=e180]
+ - columnheader "Countries or Regions" [ref=e181]
+ - columnheader "Last Updated" [ref=e182]
+ - columnheader "Status" [ref=e183]
+ - columnheader [ref=e184]
+ - rowgroup [ref=e185]:
+ - row "Digital Services Act 27 Countries or Regions View 30 Dec 2025 Active" [ref=e186]:
+ - cell "Digital Services Act" [ref=e187]:
+ - button "Digital Services Act" [ref=e188] [cursor=pointer]:
+ - paragraph [ref=e189]: Digital Services Act
+ - cell "27 Countries or Regions View" [ref=e190]:
+ - generic [ref=e192]:
+ - paragraph [ref=e193]: 27 Countries or Regions
+ - paragraph [ref=e194]:
+ - button "View" [ref=e195] [cursor=pointer]
+ - cell "30 Dec 2025" [ref=e196]
+ - cell "Active" [ref=e197]:
+ - paragraph [ref=e198]: Active
+ - cell [ref=e199]
+ - contentinfo [ref=e10]:
+ - generic [ref=e11]:
+ - list [ref=e200]:
+ - listitem [ref=e201]:
+ - link "App Store Connect" [ref=e202] [cursor=pointer]:
+ - /url: /apps
+ - list [ref=e12]:
+ - listitem [ref=e13]: Copyright © 2026 Apple Inc. All rights reserved. |
+ - listitem [ref=e14]:
+ - link "Terms of Service" [ref=e15] [cursor=pointer]:
+ - /url: /WebObjects/iTunesConnect.woa/wa/termsOfService
+ - text: "|"
+ - listitem [ref=e16]:
+ - link "Privacy Policy" [ref=e17] [cursor=pointer]:
+ - /url: https://www.apple.com/legal/privacy
+ - text: "|"
+ - listitem [ref=e18]:
+ - link "Contact Us" [ref=e19] [cursor=pointer]:
+ - /url: /contact-us
\ No newline at end of file
diff --git a/fix_explanation_formatting.py b/fix_explanation_formatting.py
new file mode 100644
index 0000000..4bfe82b
--- /dev/null
+++ b/fix_explanation_formatting.py
@@ -0,0 +1,254 @@
+#!/usr/bin/env python3
+"""
+Fix explanation formatting in SPL exam question files.
+
+Converts parenthetical option references like "(A)" in prose sentences
+into bullet points with bolded option references like "**(A)**".
+
+Pattern:
+ "Some intro. La construction métallique (A) utilise des feuilles. La construction (B) utilise..."
+Becomes:
+ "Some intro.
+ - La construction métallique **(A)** utilise des feuilles.
+ - La construction **(B)** utilise..."
+"""
+
+import re
+import os
+import glob
+
+BASE_DIR = "/Users/i052341/Daten/Cloud/04 - Ablage/Ablage 2020 - 2029/Ablage 2025/Hobbies 2025/Segelflug/Theorie/Glidr"
+
+# Pattern to detect option references (A), (B), (C), (D)
+OPTION_REF_PATTERN = re.compile(r'\([ABCD]\)')
+
+
+def bold_option_refs(text):
+ """Replace (A) with **(A)** in text."""
+ return re.sub(r'\(([ABCD])\)', r'**(\1)**', text)
+
+
+def sentence_contains_option(sentence):
+ """Check if a sentence contains a parenthetical option reference."""
+ return bool(OPTION_REF_PATTERN.search(sentence))
+
+
+def split_into_sentences(text):
+ """
+ Split text into sentences at '. ' boundaries where next sentence
+ starts with an uppercase letter (including accented chars).
+ """
+ parts = re.split(r'(?<=\w)\.\s+(?=[A-ZÀÂÄÈÉÊËÎÏÔÙÛÜÇ])', text)
+ return parts
+
+
+def join_sentences(sentences):
+ """Join sentences back into a paragraph, adding periods where needed."""
+ parts = []
+ for s in sentences:
+ s = s.strip()
+ if not s:
+ continue
+ if not s.endswith('.'):
+ s = s + '.'
+ parts.append(s)
+ return ' '.join(parts)
+
+
+def process_explanation_text(text):
+ """
+ Process a block of explanation text (one paragraph / multiple sentences).
+
+ If the text contains option references in multiple sentences,
+ split those into bullets.
+
+ Returns the processed text as a string (may contain newlines for bullets).
+ """
+ stripped = text.strip()
+
+ # Already a bullet - leave it alone
+ if stripped.startswith('- ') or stripped.startswith('* '):
+ return text
+
+ # No option references - leave it alone
+ if not OPTION_REF_PATTERN.search(text):
+ return text
+
+ # Split into sentences
+ sentences = split_into_sentences(stripped)
+
+ if len(sentences) <= 1:
+ # Single sentence - just bold the option refs
+ return bold_option_refs(text)
+
+ # Count how many sentences have option refs
+ option_sentence_indices = [i for i, s in enumerate(sentences) if sentence_contains_option(s)]
+
+ if len(option_sentence_indices) <= 1:
+ # Only one sentence has option refs - just bold them inline
+ return bold_option_refs(text)
+
+ # Multiple sentences have option refs - convert them to bullets
+ first_opt_idx = option_sentence_indices[0]
+ last_opt_idx = option_sentence_indices[-1]
+
+ intro_sentences = sentences[:first_opt_idx]
+ middle_sentences = sentences[first_opt_idx:last_opt_idx + 1]
+ outro_sentences = sentences[last_opt_idx + 1:]
+
+ output_lines = []
+
+ # Intro as regular text
+ if intro_sentences:
+ output_lines.append(join_sentences(intro_sentences))
+
+ # Middle sentences (option-containing and any in between) as bullets
+ for s in middle_sentences:
+ s_clean = s.strip().rstrip('.')
+ bolded = bold_option_refs(s_clean)
+ output_lines.append(f'- {bolded}.')
+
+ # Outro as regular text
+ if outro_sentences:
+ output_lines.append(join_sentences(outro_sentences))
+
+ return '\n'.join(output_lines)
+
+
+def process_explanation_block(lines):
+ """
+ Process a block of lines from an explanation section.
+ Groups consecutive non-special lines into paragraphs and processes each.
+ """
+ result = []
+ i = 0
+
+ while i < len(lines):
+ line = lines[i]
+
+ # Empty line - keep as is
+ if not line.strip():
+ result.append(line)
+ i += 1
+ continue
+
+ # Already a bullet line - keep as is
+ if line.strip().startswith('- ') or line.strip().startswith('* '):
+ result.append(line)
+ i += 1
+ continue
+
+ # Header line - keep as is
+ if line.strip().startswith('#'):
+ result.append(line)
+ i += 1
+ continue
+
+ # Regular text line - collect into a paragraph
+ para_lines = []
+ while i < len(lines):
+ current = lines[i]
+ # Stop at empty lines, bullets, or headers
+ if not current.strip():
+ break
+ if current.strip().startswith('- ') or current.strip().startswith('* '):
+ break
+ if current.strip().startswith('#'):
+ break
+ para_lines.append(current)
+ i += 1
+
+ if not para_lines:
+ i += 1
+ continue
+
+ # Join the paragraph lines and process
+ para_text = ' '.join(l.strip() for l in para_lines)
+ processed = process_explanation_text(para_text)
+
+ # Add processed text (may be multiple lines due to bullets)
+ result.extend(processed.split('\n'))
+
+ return result
+
+
+def process_file(filepath):
+ """Process a single markdown file, fixing explanation formatting."""
+ with open(filepath, 'r', encoding='utf-8') as f:
+ content = f.read()
+
+ lines = content.split('\n')
+ result_lines = []
+ changes_made = 0
+ i = 0
+
+ while i < len(lines):
+ line = lines[i]
+
+ # Check if this is an explanation header
+ if re.match(r'^#### (Explanation|Erklärung|Explication)\s*$', line.strip()):
+ result_lines.append(line)
+ i += 1
+
+ # Collect lines until next #### or ### header
+ explanation_lines = []
+ while i < len(lines):
+ current = lines[i]
+ if re.match(r'^####? ', current) or re.match(r'^### ', current):
+ break
+ explanation_lines.append(current)
+ i += 1
+
+ # Process the explanation block
+ processed = process_explanation_block(explanation_lines)
+
+ # Count if there was a change
+ if explanation_lines != processed:
+ changes_made += 1
+
+ result_lines.extend(processed)
+ else:
+ result_lines.append(line)
+ i += 1
+
+ new_content = '\n'.join(result_lines)
+
+ if new_content != content:
+ with open(filepath, 'w', encoding='utf-8') as f:
+ f.write(new_content)
+
+ return changes_made
+
+
+def main():
+ """Process all SPL exam question files."""
+ patterns = [
+ os.path.join(BASE_DIR, "SPL Exam Questions EN", "*.md"),
+ os.path.join(BASE_DIR, "SPL Exam Questions DE", "*.md"),
+ os.path.join(BASE_DIR, "SPL Exam Questions FR", "*.md"),
+ ]
+
+ total_files = 0
+ total_changes = 0
+
+ for pattern in patterns:
+ files = sorted(glob.glob(pattern))
+ for filepath in files:
+ filename = os.path.basename(filepath)
+ # Skip combined index files
+ if filename.startswith("SPL Exam Questions"):
+ continue
+
+ changes = process_file(filepath)
+ total_files += 1
+ total_changes += changes
+
+ lang_folder = os.path.basename(os.path.dirname(filepath))
+ status = f" {changes} explanations converted" if changes > 0 else " (no changes)"
+ print(f"[{lang_folder}] {filename}{status}")
+
+ print(f"\nTotal: {total_files} files processed, {total_changes} explanations converted to bullets")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/privacy.md b/privacy.md
new file mode 100644
index 0000000..b04792f
--- /dev/null
+++ b/privacy.md
@@ -0,0 +1,196 @@
+- generic [active] [ref=e1]:
+ - banner "App Store Connect" [ref=e3]:
+ - generic [ref=e4]:
+ - heading "App Store Connect" [level=1] [ref=e20]:
+ - link "App Store Connect" [ref=e21] [cursor=pointer]:
+ - /url: /
+ - navigation "Global" [ref=e7]:
+ - list [ref=e23]:
+ - listitem [ref=e24]:
+ - link "Apps" [ref=e25] [cursor=pointer]:
+ - /url: /apps
+ - listitem [ref=e26]:
+ - link "Analytics" [ref=e27] [cursor=pointer]:
+ - /url: /analytics
+ - listitem [ref=e28]:
+ - link "Trends" [ref=e29] [cursor=pointer]:
+ - /url: /trends
+ - listitem [ref=e30]:
+ - link "Reports" [ref=e31] [cursor=pointer]:
+ - /url: /itc/payments_and_financial_reports
+ - listitem [ref=e32]:
+ - link "Business" [ref=e33] [cursor=pointer]:
+ - /url: /business
+ - listitem [ref=e34]:
+ - link "Users and Access" [ref=e35] [cursor=pointer]:
+ - /url: /access/users
+ - button "Matthias Nott Matthias Nott Account name menu" [ref=e37] [cursor=pointer]:
+ - generic:
+ - generic: Matthias Nott
+ - generic: Matthias Nott
+ - img [ref=e38]
+ - generic [ref=e43]:
+ - button "Apps menu, Glider Pilot, selected" [ref=e48] [cursor=pointer]:
+ - generic [ref=e49]:
+ - generic "Glider Pilot" [ref=e50]:
+ - img "Glider Pilot" [ref=e51]
+ - generic [ref=e52]: Glider Pilot
+ - img [ref=e54]
+ - navigation "Apps" [ref=e57]:
+ - list [ref=e58]:
+ - listitem [ref=e59]:
+ - link "Distribution" [ref=e60] [cursor=pointer]:
+ - /url: /apps/6760631689/distribution
+ - listitem [ref=e61]:
+ - link "TestFlight" [ref=e62] [cursor=pointer]:
+ - /url: /teams/69a6de75-2050-47e3-e053-5b8c7c11a4d1/apps/6760631689/testflight
+ - listitem [ref=e63]:
+ - link "Xcode Cloud" [ref=e64] [cursor=pointer]:
+ - /url: /teams/69a6de75-2050-47e3-e053-5b8c7c11a4d1/apps/6760631689/ci
+ - main [ref=e68]:
+ - generic [ref=e73]:
+ - navigation "Distribution" [ref=e75]:
+ - list [ref=e76]:
+ - listitem [ref=e77]:
+ - generic [ref=e78]:
+ - heading "iOS App" [level=2] [ref=e80]
+ - list [ref=e81]:
+ - listitem [ref=e82]:
+ - link "1.0 Prepare for Submission" [ref=e83] [cursor=pointer]:
+ - /url: /apps/6760631689/distribution/ios/version/inflight
+ - generic [ref=e84]:
+ - img [ref=e85]
+ - text: 1.0 Prepare for Submission
+ - button "Add Platform" [ref=e87] [cursor=pointer]
+ - listitem [ref=e88]:
+ - separator [ref=e89]
+ - listitem [ref=e90]:
+ - heading "General" [level=2] [ref=e92]
+ - list [ref=e93]:
+ - listitem [ref=e94]:
+ - link "App Information" [ref=e95] [cursor=pointer]:
+ - /url: /apps/6760631689/distribution/info
+ - generic [ref=e96]: App Information
+ - listitem [ref=e97]:
+ - link "App Review" [ref=e98] [cursor=pointer]:
+ - /url: /apps/6760631689/distribution/reviewsubmissions
+ - generic [ref=e99]: App Review
+ - listitem [ref=e100]:
+ - link "History" [ref=e101] [cursor=pointer]:
+ - /url: /apps/6760631689/distribution/activity/ios/versions
+ - generic [ref=e102]: History
+ - listitem [ref=e103]:
+ - separator [ref=e104]
+ - listitem [ref=e105]:
+ - heading "App Store" [level=2] [ref=e107]
+ - generic [ref=e108]:
+ - heading "Trust & Safety" [level=3] [ref=e110]
+ - list [ref=e111]:
+ - listitem [ref=e112]:
+ - link "App Privacy" [ref=e113] [cursor=pointer]:
+ - /url: /apps/6760631689/distribution/privacy
+ - generic [ref=e114]: App Privacy
+ - listitem [ref=e115]:
+ - link "App Accessibility" [ref=e116] [cursor=pointer]:
+ - /url: /apps/6760631689/distribution/accessibility
+ - generic [ref=e117]: App Accessibility
+ - listitem [ref=e118]:
+ - link "Ratings and Reviews" [ref=e119] [cursor=pointer]:
+ - /url: /apps/6760631689/distribution/ratings/ios
+ - generic [ref=e120]: Ratings and Reviews
+ - generic [ref=e121]:
+ - heading "Growth & Marketing" [level=3] [ref=e123]
+ - list [ref=e124]:
+ - listitem [ref=e125]:
+ - link "In-App Events" [ref=e126] [cursor=pointer]:
+ - /url: /apps/6760631689/distribution/events
+ - generic [ref=e127]: In-App Events
+ - listitem [ref=e128]:
+ - link "Custom Product Pages" [ref=e129] [cursor=pointer]:
+ - /url: /apps/6760631689/distribution/productpages
+ - generic [ref=e130]: Custom Product Pages
+ - listitem [ref=e131]:
+ - link "Product Page Optimization" [ref=e132] [cursor=pointer]:
+ - /url: /apps/6760631689/distribution/optimization
+ - generic [ref=e133]: Product Page Optimization
+ - listitem [ref=e134]:
+ - link "Promo Codes" [ref=e135] [cursor=pointer]:
+ - /url: /apps/6760631689/distribution/promo_codes/generate
+ - generic [ref=e136]: Promo Codes
+ - listitem [ref=e137]:
+ - link "Game Center" [ref=e138] [cursor=pointer]:
+ - /url: /apps/6760631689/distribution/gamecenter
+ - generic [ref=e139]: Game Center
+ - generic [ref=e140]:
+ - heading "Monetization" [level=3] [ref=e142]
+ - list [ref=e143]:
+ - listitem [ref=e144]:
+ - link "Pricing and Availability" [ref=e145] [cursor=pointer]:
+ - /url: /apps/6760631689/distribution/pricing
+ - generic [ref=e146]: Pricing and Availability
+ - listitem [ref=e147]:
+ - link "In-App Purchases" [ref=e148] [cursor=pointer]:
+ - /url: /apps/6760631689/distribution/iaps
+ - generic [ref=e149]: In-App Purchases
+ - listitem [ref=e150]:
+ - link "Subscriptions" [ref=e151] [cursor=pointer]:
+ - /url: /apps/6760631689/distribution/subscriptions
+ - generic [ref=e152]: Subscriptions
+ - generic [ref=e153]:
+ - heading "Featuring" [level=3] [ref=e155]
+ - list [ref=e156]:
+ - listitem [ref=e157]:
+ - link "Nominations" [ref=e158] [cursor=pointer]:
+ - /url: /apps/6760631689/distribution/nominations
+ - generic [ref=e159]: Nominations
+ - generic [ref=e161]:
+ - generic [ref=e163]:
+ - heading "App Privacy" [level=2] [ref=e165]
+ - button "Publish" [disabled] [ref=e167]
+ - separator [ref=e168]
+ - generic [ref=e169]:
+ - generic [ref=e170]:
+ - generic [ref=e171]:
+ - heading "Privacy Policy" [level=3] [ref=e172]
+ - button "Edit" [ref=e173] [cursor=pointer]
+ - generic [ref=e175]:
+ - button "English (U.S.)" [disabled] [ref=e176]
+ - button "?" [ref=e179] [cursor=pointer]
+ - generic [ref=e180]:
+ - generic [ref=e181]:
+ - generic [ref=e183]:
+ - generic [ref=e184]: Privacy Policy URL
+ - button "More information" [ref=e186] [cursor=pointer]: "?"
+ - paragraph [ref=e187]: –
+ - generic [ref=e188]:
+ - generic [ref=e189]:
+ - generic [ref=e190]: User Privacy Choices URL
+ - paragraph [ref=e191]: (Optional)
+ - button "More information" [ref=e193] [cursor=pointer]: "?"
+ - paragraph [ref=e194]: –
+ - separator [ref=e195]
+ - generic [ref=e197]:
+ - paragraph [ref=e198]:
+ - generic [ref=e199]:
+ - paragraph [ref=e200]: The App Store is designed to be a safe and trusted place for people to discover apps from talented developers just like you. Your app can influence culture and change lives, so that's why we're counting on you to help us protect users' privacy.
+ - paragraph [ref=e201]: After clicking Get Started, you'll be asked to provide some information about your app's data collection practices. This information will appear on your app's product page, where users can see what data your app collects and how it's used.
+ - button "Get Started" [ref=e203] [cursor=pointer]
+ - contentinfo [ref=e10]:
+ - generic [ref=e11]:
+ - list [ref=e204]:
+ - listitem [ref=e205]:
+ - link "App Store Connect" [ref=e206] [cursor=pointer]:
+ - /url: /apps
+ - list [ref=e12]:
+ - listitem [ref=e13]: Copyright © 2026 Apple Inc. All rights reserved. |
+ - listitem [ref=e14]:
+ - link "Terms of Service" [ref=e15] [cursor=pointer]:
+ - /url: /WebObjects/iTunesConnect.woa/wa/termsOfService
+ - text: "|"
+ - listitem [ref=e16]:
+ - link "Privacy Policy" [ref=e17] [cursor=pointer]:
+ - /url: https://www.apple.com/legal/privacy
+ - text: "|"
+ - listitem [ref=e18]:
+ - link "Contact Us" [ref=e19] [cursor=pointer]:
+ - /url: /contact-us
\ No newline at end of file
diff --git a/screenshots/IMG_0737.PNG b/screenshots/IMG_0737.PNG
new file mode 100644
index 0000000..9a7b97c
--- /dev/null
+++ b/screenshots/IMG_0737.PNG
Binary files differ
diff --git a/screenshots/IMG_0738.PNG b/screenshots/IMG_0738.PNG
new file mode 100644
index 0000000..c7f0c29
--- /dev/null
+++ b/screenshots/IMG_0738.PNG
Binary files differ
diff --git a/screenshots/IMG_0739.PNG b/screenshots/IMG_0739.PNG
new file mode 100644
index 0000000..95e6a44
--- /dev/null
+++ b/screenshots/IMG_0739.PNG
Binary files differ
diff --git a/screenshots/IMG_0740.PNG b/screenshots/IMG_0740.PNG
new file mode 100644
index 0000000..bd7c4fb
--- /dev/null
+++ b/screenshots/IMG_0740.PNG
Binary files differ
diff --git a/screenshots/IMG_0741.PNG b/screenshots/IMG_0741.PNG
new file mode 100644
index 0000000..7a8d02a
--- /dev/null
+++ b/screenshots/IMG_0741.PNG
Binary files differ
diff --git a/screenshots/IMG_0742.PNG b/screenshots/IMG_0742.PNG
new file mode 100644
index 0000000..b172917
--- /dev/null
+++ b/screenshots/IMG_0742.PNG
Binary files differ
diff --git a/screenshots/IMG_0743.PNG b/screenshots/IMG_0743.PNG
new file mode 100644
index 0000000..bdb1db3
--- /dev/null
+++ b/screenshots/IMG_0743.PNG
Binary files differ
diff --git a/screenshots/ipad/ipad_home.png b/screenshots/ipad/ipad_home.png
new file mode 100644
index 0000000..fa9d17b
--- /dev/null
+++ b/screenshots/ipad/ipad_home.png
Binary files differ
diff --git a/tasks/PRD-Glidr.md b/tasks/PRD-Glidr.md
new file mode 100644
index 0000000..4fa4cbc
--- /dev/null
+++ b/tasks/PRD-Glidr.md
@@ -0,0 +1,1182 @@
+# PRD: Glidr — SPL Exam Preparation App
+
+**Version:** 1.0
+**Date:** 2026-03-15
+**Status:** Ready for Implementation
+**Author:** Atlas (Principal Software Architect)
+
+---
+
+## 1. Executive Summary
+
+### 1.1 Product Overview
+
+Glidr is a mobile flashcard and spaced repetition app designed to help glider pilots prepare for the EASA Sailplane Pilot License (SPL) theoretical knowledge exam. The app covers all 9 exam subjects, uses the SuperMemo SM-2 spaced repetition algorithm to optimize long-term retention, and includes a cram mode for last-minute exam preparation.
+
+The app is sold as a one-time purchase (~$49.99) on the App Store and Google Play. Content (questions, answers, explanations) is served from a remote API at tekmidian.com and cached locally, enabling question corrections and updates without app re-releases.
+
+### 1.2 Target Users
+
+- Primary: German/Swiss/European glider pilot students preparing for SPL theoretical exam
+- Secondary: Existing pilots refreshing knowledge or preparing for recurrency checks
+- Language: English and French (bilingual, selectable in-app)
+
+### 1.3 Success Metrics
+
+- App Store rating >= 4.5 stars
+- >= 80% of purchased users complete at least one full subject review cycle
+- Content update delivery: corrections visible to users within 24 hours of server update
+- Exam pass rate correlation: users who complete >= 3 full cycles pass exam at >= 90%
+
+### 1.4 Timeline Estimate
+
+| Phase | Duration | Deliverable |
+|-------|----------|-------------|
+| Phase 1: Foundation | 3 weeks | App shell, DB schema, content download, question display |
+| Phase 2: Learning Engine | 2 weeks | SM-2 implementation, review queue, self-assessment |
+| Phase 3: Cram Mode | 1 week | Cram mode, session stats |
+| Phase 4: Purchase + Polish | 2 weeks | IAP, onboarding, UI polish, accessibility |
+| Phase 5: Backend | 1 week | Server setup, API, manifest system |
+| Phase 6: Testing + Release | 2 weeks | QA, beta, App Store submission |
+| **Total** | **~11 weeks** | |
+
+---
+
+## 2. Product Requirements
+
+### 2.1 Functional Requirements
+
+#### FR-01: Content Display
+- Display multiple choice questions with 4 answer options (A, B, C, D)
+- Show question text in selected language (English or French)
+- Support embedded figures (PNG and SVG) within questions
+- Figures must be zoomable via pinch gesture and double-tap
+- Display correct answer and explanation after user selects an answer
+- Explanation collapses/expands on tap
+
+#### FR-02: Subject Navigation
+- List all 9 subjects on home screen with progress indicators
+- Each subject shows: total cards, due today, learned cards, new cards
+- Tap subject to begin study session
+
+#### FR-03: Spaced Repetition Mode
+- Implement SuperMemo SM-2 algorithm exactly as specified
+- Present cards due today in review sessions
+- After revealing answer, user rates recall using 4-button self-assessment: Again / Hard / Good / Easy
+- SM-2 state (n, EF, interval) updated immediately after each rating
+- Session ends when all due cards have been reviewed
+- Show session summary: cards reviewed, correct count, next due date
+
+#### FR-04: Cram Mode
+- Accessible from each subject screen
+- Shows ALL cards in a subject (ignoring SM-2 schedule)
+- User answers, then sees correct answer + explanation
+- No self-assessment rating in cram mode
+- Cram progress does not modify SM-2 state
+- Session score shown at end (X/Y correct)
+
+#### FR-05: Content Updates
+- On each app launch: check remote manifest for content version changes
+- If new version available: download updated subject JSON in background
+- Update local SQLite database with new/changed questions
+- Show user notification when update is available and downloaded
+- Full offline operation after initial download
+
+#### FR-06: Purchase / Unlock
+- Free tier: first 10 questions of each subject are accessible without purchase
+- Paid tier: one-time purchase unlocks all 9 subjects fully
+- Restore Purchases button in Settings
+- Purchase state persisted in flutter_secure_storage
+
+#### FR-07: Language Selection
+- Language toggle (EN / FR) accessible from Settings and from subject screen
+- Switching language immediately updates all displayed content
+- Language preference persisted across app sessions
+
+#### FR-08: Progress and Statistics
+- Per-subject statistics: total, new, learning, review, mature (interval > 21 days)
+- Overall retention rate (correct / total reviews)
+- Study streak counter (consecutive days with at least 1 review)
+- Estimated exam readiness per subject (percentage of cards with EF >= 2.0 and interval >= 7)
+
+#### FR-09: Settings
+- Language selection (EN / FR)
+- New cards per day per subject (1-20, default 5)
+- Review reminder notification (optional, time picker)
+- Restore purchases
+- App version + build number
+
+### 2.2 Non-Functional Requirements
+
+#### NFR-01: Performance
+- App launch to home screen: < 1.5 seconds (cold start)
+- Question display including image: < 300ms
+- SM-2 state save after rating: < 50ms
+- Content manifest check: non-blocking (background thread)
+
+#### NFR-02: Offline Support
+- Full functionality after initial content download (no network required)
+- Initial download required only once; graceful offline handling after
+
+#### NFR-03: Platform Support
+- iOS 16.0+ (primary)
+- Android 10+ (API level 29+) (secondary, same codebase)
+- Tested on iPhone 13/14/15 (primary); Pixel 6/7 and Samsung Galaxy S21/S22 (Android)
+
+#### NFR-04: Accessibility
+- Dynamic Type support (text scales with system font size)
+- VoiceOver / TalkBack support for all interactive elements
+- Minimum tap target size: 44x44pt
+
+#### NFR-05: Data Privacy
+- No user accounts, no personal data collected
+- No analytics in v1 (add opt-in analytics in v2)
+- Purchase state stored locally only
+- GDPR-compliant: no EU data transfer
+
+---
+
+## 3. System Architecture
+
+### 3.1 High-Level Architecture
+
+```
+┌─────────────────────────────────────────────────┐
+│ GLIDR iOS/Android App │
+│ │
+│ ┌──────────────┐ ┌───────────────────────┐ │
+│ │ Flutter UI │ │ Business Logic │ │
+│ │ (Widgets) │◄──►│ (Riverpod Providers) │ │
+│ └──────────────┘ └───────────┬───────────┘ │
+│ │ │
+│ ┌───────────────────────────────▼─────────────┐ │
+│ │ Repository Layer (Drift ORM) │ │
+│ └───────────────────────────────┬─────────────┘ │
+│ │ │
+│ ┌───────────────┐ ┌────────────▼──────────────┐ │
+│ │ Secure Storage│ │ SQLite Database (Drift) │ │
+│ │ (flutter_ │ │ questions, progress, │ │
+│ │ secure_ │ │ sessions, settings │ │
+│ │ storage) │ └───────────────────────────┘ │
+│ └───────────────┘ │
+│ │
+│ ┌──────────────────────────────────────────────┐ │
+│ │ Content Sync Service (Dio HTTP) │ │
+│ └──────────────────┬───────────────────────────┘ │
+└─────────────────────│───────────────────────────┘
+ │ HTTPS + X-Glidr-Key header
+ ▼
+┌─────────────────────────────────────────────────┐
+│ tekmidian.com/glidr/api/v1/ │
+│ │
+│ manifest.json Apache/PHP auth layer │
+│ subjects/air_law.json Rate limiting │
+│ subjects/meteo.json Static file serving │
+│ figures/*.png HTTPS (Let's Encrypt) │
+│ figures/*.svg │
+└─────────────────────────────────────────────────┘
+```
+
+### 3.2 Flutter App Layer Architecture
+
+```
+lib/
+├── main.dart
+├── app.dart # MaterialApp, routing, theme
+├── core/
+│ ├── constants.dart # API base URL, API key (from --dart-define)
+│ ├── router.dart # go_router route definitions
+│ └── theme.dart # Color scheme, typography
+├── data/
+│ ├── database/
+│ │ ├── database.dart # Drift database definition
+│ │ ├── tables/ # Drift table definitions
+│ │ └── daos/ # Data access objects per domain
+│ ├── models/ # Freezed immutable data classes
+│ │ ├── question.dart
+│ │ ├── card_progress.dart
+│ │ ├── study_session.dart
+│ │ └── subject.dart
+│ ├── repositories/ # Repository interfaces + implementations
+│ │ ├── question_repository.dart
+│ │ ├── progress_repository.dart
+│ │ └── content_sync_repository.dart
+│ └── api/
+│ ├── glidr_api_client.dart # Dio client with auth interceptor
+│ └── models/ # API response models
+├── domain/
+│ └── sm2/
+│ ├── sm2_algorithm.dart # Pure SM-2 calculation functions
+│ └── review_scheduler.dart # Queue building logic
+├── presentation/
+│ ├── home/
+│ │ ├── home_screen.dart
+│ │ └── home_provider.dart
+│ ├── study/
+│ │ ├── study_screen.dart # SM-2 review session
+│ │ ├── study_provider.dart
+│ │ └── widgets/
+│ │ ├── question_card.dart
+│ │ ├── answer_options.dart
+│ │ ├── rating_buttons.dart
+│ │ └── figure_viewer.dart # photo_view zoomable image
+│ ├── cram/
+│ │ ├── cram_screen.dart
+│ │ └── cram_provider.dart
+│ ├── subject_detail/
+│ │ ├── subject_detail_screen.dart
+│ │ └── subject_detail_provider.dart
+│ ├── stats/
+│ │ ├── stats_screen.dart
+│ │ └── stats_provider.dart
+│ └── settings/
+│ ├── settings_screen.dart
+│ └── settings_provider.dart
+└── l10n/
+ ├── app_en.arb
+ └── app_fr.arb
+```
+
+---
+
+## 4. Data Models
+
+### 4.1 Question JSON Format (Remote API)
+
+Each subject is served as a single JSON file. Example structure:
+
+```json
+{
+ "subject_id": "air_law",
+ "subject_code": "01",
+ "name": {
+ "en": "Air Law",
+ "fr": "Droit aérien"
+ },
+ "version": "1.0.0",
+ "updated_at": "2026-03-15T00:00:00Z",
+ "questions": [
+ {
+ "id": "air_law_q1",
+ "number": 1,
+ "text": {
+ "en": "The holder of an SPL license or LAPL(S) license completed a total of 9 winch launches, 4 launches in aero-tow and 2 bungee launches during the last 24 months. What launch methods may the pilot conduct as PIC today?",
+ "fr": "Le titulaire d'une licence SPL ou LAPL(S) a effectué au total 9 lancements au treuil, 4 lancements en remorqué et 2 lancements au sandow au cours des 24 derniers mois. Quelles méthodes de lancement le pilote peut-il effectuer en tant que commandant de bord aujourd'hui?"
+ },
+ "options": {
+ "en": {
+ "A": "Winch and bungee.",
+ "B": "Winch, bungee and aero-tow.",
+ "C": "Winch and aero-tow.",
+ "D": "Aero-tow and bungee."
+ },
+ "fr": {
+ "A": "Treuil et sandow.",
+ "B": "Treuil, sandow et remorqué.",
+ "C": "Treuil et remorqué.",
+ "D": "Remorqué et sandow."
+ }
+ },
+ "correct": "A",
+ "explanation": {
+ "en": "Under Part-SFCL (SFCL.010 and SFCL.160), a pilot must have completed at least 5 launches using a specific launch method within the preceding 24 months...",
+ "fr": "Selon la Part-SFCL (SFCL.010 et SFCL.160), un pilote doit avoir effectué au moins 5 lancements en utilisant une méthode de lancement spécifique au cours des 24 mois précédents..."
+ },
+ "figures": []
+ },
+ {
+ "id": "pfp_q14",
+ "number": 14,
+ "text": {
+ "en": "What is the maximum payload according to the loading table?",
+ "fr": "Quelle est la charge utile maximale selon le tableau de chargement?"
+ },
+ "options": {
+ "en": { "A": "580 kg", "B": "450 kg", "C": "525 kg", "D": "600 kg" },
+ "fr": { "A": "580 kg", "B": "450 kg", "C": "525 kg", "D": "600 kg" }
+ },
+ "correct": "B",
+ "explanation": {
+ "en": "According to the Discus loading table shown...",
+ "fr": "Selon le tableau de chargement du Discus affiché..."
+ },
+ "figures": [
+ {
+ "filename": "bazl_30_q14_discus_loading_table.png",
+ "type": "png",
+ "alt_en": "Discus aircraft loading table",
+ "alt_fr": "Tableau de chargement de l'aéronef Discus",
+ "position": "question"
+ }
+ ]
+ }
+ ]
+}
+```
+
+### 4.2 Manifest JSON Format
+
+```json
+{
+ "api_version": "1",
+ "manifest_version": "1.0.0",
+ "updated_at": "2026-03-15T00:00:00Z",
+ "subjects": {
+ "air_law": { "version": "1.0.0", "sha256": "abc...", "question_count": 110, "size_bytes": 85000 },
+ "aircraft_general_knowledge": { "version": "1.0.0", "sha256": "def...", "question_count": 110, "size_bytes": 92000 },
+ "communications": { "version": "1.0.0", "sha256": "ghi...", "question_count": 90, "size_bytes": 71000 },
+ "flight_performance": { "version": "1.0.0", "sha256": "jkl...", "question_count": 90, "size_bytes": 68000 },
+ "human_performance": { "version": "1.0.0", "sha256": "mno...", "question_count": 110, "size_bytes": 83000 },
+ "meteorology": { "version": "1.0.0", "sha256": "pqr...", "question_count": 110, "size_bytes": 88000 },
+ "navigation": { "version": "1.0.0", "sha256": "stu...", "question_count": 141, "size_bytes": 115000 },
+ "operational_procedures": { "version": "1.0.0", "sha256": "vwx...", "question_count": 110, "size_bytes": 84000 },
+ "principles_of_flight": { "version": "1.0.0", "sha256": "yza...", "question_count": 110, "size_bytes": 86000 }
+ }
+}
+```
+
+### 4.3 SQLite Database Schema (Drift)
+
+```sql
+-- Questions table (populated from downloaded JSON)
+CREATE TABLE questions (
+ id TEXT NOT NULL PRIMARY KEY,
+ subject_id TEXT NOT NULL,
+ number INTEGER NOT NULL,
+ text_en TEXT NOT NULL,
+ text_fr TEXT NOT NULL,
+ options_en TEXT NOT NULL, -- JSON string: {"A":"...","B":"...","C":"...","D":"..."}
+ options_fr TEXT NOT NULL,
+ correct TEXT NOT NULL, -- "A" | "B" | "C" | "D"
+ explanation_en TEXT NOT NULL,
+ explanation_fr TEXT NOT NULL,
+ figures TEXT NOT NULL -- JSON array string (empty "[]" if no figures)
+);
+
+-- SM-2 progress per card
+CREATE TABLE card_progress (
+ question_id TEXT NOT NULL PRIMARY KEY,
+ repetition_number INTEGER NOT NULL DEFAULT 0,
+ easiness_factor REAL NOT NULL DEFAULT 2.5,
+ interval_days INTEGER NOT NULL DEFAULT 1,
+ next_review_date TEXT, -- "YYYY-MM-DD", NULL = new (never reviewed)
+ last_review_date TEXT, -- "YYYY-MM-DD"
+ total_reviews INTEGER NOT NULL DEFAULT 0,
+ correct_reviews INTEGER NOT NULL DEFAULT 0,
+ created_at TEXT NOT NULL,
+ updated_at TEXT NOT NULL,
+ FOREIGN KEY (question_id) REFERENCES questions(id)
+);
+
+-- Study sessions
+CREATE TABLE study_sessions (
+ id TEXT NOT NULL PRIMARY KEY, -- UUID
+ started_at TEXT NOT NULL, -- ISO 8601
+ ended_at TEXT,
+ mode TEXT NOT NULL, -- "spaced_repetition" | "cram"
+ subject_id TEXT, -- NULL = mixed
+ cards_reviewed INTEGER NOT NULL DEFAULT 0,
+ cards_correct INTEGER NOT NULL DEFAULT 0
+);
+
+-- Individual review events
+CREATE TABLE review_log (
+ id TEXT NOT NULL PRIMARY KEY, -- UUID
+ session_id TEXT NOT NULL,
+ question_id TEXT NOT NULL,
+ reviewed_at TEXT NOT NULL, -- ISO 8601
+ quality INTEGER NOT NULL, -- 0-5 (SM-2 grade) or -1 for cram
+ time_to_answer_ms INTEGER,
+ was_cram INTEGER NOT NULL DEFAULT 0,
+ FOREIGN KEY (session_id) REFERENCES study_sessions(id),
+ FOREIGN KEY (question_id) REFERENCES questions(id)
+);
+
+-- Content version tracking
+CREATE TABLE content_manifest (
+ subject_id TEXT NOT NULL PRIMARY KEY,
+ version TEXT NOT NULL,
+ sha256 TEXT NOT NULL,
+ downloaded_at TEXT NOT NULL,
+ question_count INTEGER NOT NULL
+);
+
+-- App settings (key-value)
+CREATE TABLE settings (
+ key TEXT NOT NULL PRIMARY KEY,
+ value TEXT NOT NULL
+);
+-- Default settings:
+-- language: "en"
+-- new_cards_per_day: "5"
+-- reminder_enabled: "false"
+-- reminder_time: "19:00"
+-- is_unlocked: "false"
+-- onboarding_complete: "false"
+```
+
+---
+
+## 5. Learning Algorithm: SM-2 Implementation
+
+### 5.1 Core Algorithm
+
+```dart
+// lib/domain/sm2/sm2_algorithm.dart
+
+class SM2Result {
+ final int repetitionNumber;
+ final double easinessFactor;
+ final int intervalDays;
+ final DateTime nextReviewDate;
+
+ const SM2Result({
+ required this.repetitionNumber,
+ required this.easinessFactor,
+ required this.intervalDays,
+ required this.nextReviewDate,
+ });
+}
+
+/// Grade 0: complete blackout
+/// Grade 1: incorrect, remembered on seeing answer
+/// Grade 2: incorrect, correct seemed easy after
+/// Grade 3: correct with serious difficulty
+/// Grade 4: correct after hesitation
+/// Grade 5: perfect response
+SM2Result computeNextInterval({
+ required int currentRepetitionNumber, // n
+ required double currentEasinessFactor, // EF
+ required int currentIntervalDays, // I
+ required int quality, // 0-5
+}) {
+ assert(quality >= 0 && quality <= 5);
+
+ int n = currentRepetitionNumber;
+ double ef = currentEasinessFactor;
+ int interval = currentIntervalDays;
+
+ if (quality < 3) {
+ // Failed review: restart interval sequence, keep EF
+ n = 0;
+ interval = 1;
+ } else {
+ // Successful review: advance interval
+ switch (n) {
+ case 0:
+ interval = 1;
+ case 1:
+ interval = 6;
+ default:
+ interval = (interval * ef).ceil();
+ }
+ n += 1;
+ }
+
+ // Update EF (clamp to minimum 1.3)
+ ef = ef + (0.1 - (5 - quality) * (0.08 + (5 - quality) * 0.02));
+ ef = ef.clamp(1.3, double.infinity);
+
+ final nextDate = DateTime.now().add(Duration(days: interval));
+
+ return SM2Result(
+ repetitionNumber: n,
+ easinessFactor: ef,
+ intervalDays: interval,
+ nextReviewDate: DateTime(nextDate.year, nextDate.month, nextDate.day),
+ );
+}
+```
+
+### 5.2 Quality Grade Mapping (4-Button UI)
+
+The app presents 4 buttons (Anki-style simplification of SM-2's 0-5 scale):
+
+| Button | Color | SM-2 Grade | Effect |
+|--------|-------|-----------|--------|
+| Again | Red | 1 | Reset interval to 1 day, EF -0.54 |
+| Hard | Orange | 3 | EF -0.14, interval advances slowly |
+| Good | Green | 4 | EF unchanged, interval advances normally |
+| Easy | Blue | 5 | EF +0.10, interval advances faster |
+
+### 5.3 Daily Review Queue
+
+```dart
+// lib/domain/sm2/review_scheduler.dart
+
+Future<List<Question>> buildDailyQueue({
+ required String? subjectId, // null = all subjects
+ required int maxNewCards,
+}) async {
+ final today = DateTime.now();
+ final todayStr = '${today.year}-${today.month.toString().padLeft(2,'0')}-${today.day.toString().padLeft(2,'0')}';
+
+ // 1. Due cards (have been reviewed before, scheduled for today or earlier)
+ final dueCards = await _progressDao.getDueCards(
+ subjectId: subjectId,
+ asOf: todayStr,
+ );
+
+ // 2. New cards (never reviewed, next_review_date IS NULL)
+ final newCards = await _progressDao.getNewCards(
+ subjectId: subjectId,
+ limit: maxNewCards,
+ );
+
+ // Shuffle each group independently, then concatenate: due first, then new
+ dueCards.shuffle();
+ newCards.shuffle();
+
+ return [...dueCards, ...newCards];
+}
+```
+
+### 5.4 Cram Mode
+
+```dart
+Future<List<Question>> buildCramQueue({
+ required String subjectId,
+ required bool shuffled,
+}) async {
+ final questions = await _questionDao.getAllForSubject(subjectId);
+ if (shuffled) questions.shuffle();
+ return questions;
+}
+
+// Cram review recording — does NOT update card_progress SM-2 state
+Future<void> recordCramReview({
+ required String sessionId,
+ required String questionId,
+ required bool wasCorrect,
+}) async {
+ await _reviewLogDao.insert(ReviewLogEntry(
+ id: const Uuid().v4(),
+ sessionId: sessionId,
+ questionId: questionId,
+ reviewedAt: DateTime.now().toIso8601String(),
+ quality: wasCorrect ? 4 : 1, // approximate mapping for stats
+ wasCram: true,
+ ));
+ // card_progress is intentionally NOT updated
+}
+```
+
+---
+
+## 6. API Design
+
+### 6.1 Base Configuration
+
+```
+Base URL: https://tekmidian.com/glidr/api/v1
+Auth Header: X-Glidr-Key: {compile-time-injected-secret}
+Content-Type: application/json
+```
+
+### 6.2 Endpoints
+
+#### GET /manifest.json
+Returns the current version hash for all subjects. Used to detect if local content needs updating.
+
+Response:
+```json
+{
+ "api_version": "1",
+ "manifest_version": "1.2.0",
+ "updated_at": "2026-03-15T00:00:00Z",
+ "subjects": {
+ "air_law": { "version": "1.2.0", "sha256": "abc123...", "question_count": 110, "size_bytes": 85000 },
+ ...
+ }
+}
+```
+
+#### GET /subjects/{subject_id}.json
+Returns full question set for a subject. Only called when local hash differs from manifest.
+
+Path parameters:
+- `subject_id`: one of `air_law`, `aircraft_general_knowledge`, `communications`, `flight_performance`, `human_performance`, `meteorology`, `navigation`, `operational_procedures`, `principles_of_flight`
+
+Response: Full subject JSON as described in Section 4.1.
+
+#### GET /figures/{filename}
+Serves a figure file (PNG or SVG). Called only if figures are NOT bundled with the app.
+
+Note: For v1, all figures are bundled as Flutter assets. This endpoint is a fallback for future use or figure-only updates.
+
+### 6.3 Error Handling
+
+| HTTP Status | Meaning | App Behavior |
+|------------|---------|-------------|
+| 200 | Success | Process response |
+| 304 | Not Modified (future: ETag support) | Use cached content |
+| 403 | Invalid API key | Log error silently, use cached content |
+| 404 | Subject not found | Log error, skip that subject update |
+| 429 | Rate limited | Retry after 60 seconds |
+| 500 | Server error | Use cached content, retry next launch |
+
+All errors are non-fatal: the app always falls back to cached local content.
+
+### 6.4 Content Sync Flow
+
+```
+App Launch
+ │
+ ├── Is local content available?
+ │ NO → Show "Downloading content..." screen
+ │ Download all 9 subjects + figures sequentially
+ │ Show progress bar
+ │ On complete → proceed to Home
+ │
+ │ YES → Proceed to Home immediately
+ │ Background: check manifest
+ │ │
+ │ ├── Hash differs for any subject?
+ │ │ YES → Download updated subject JSON
+ │ │ Update SQLite questions table
+ │ │ Show subtle "Content updated" banner
+ │ │
+ │ └── No changes → do nothing
+ │
+ └── Home Screen
+```
+
+---
+
+## 7. UI Flows and Screens
+
+### 7.1 Screen Inventory
+
+| Screen | Route | Purpose |
+|--------|-------|---------|
+| Splash | `/` | App init, content check, auth check |
+| Onboarding | `/onboarding` | First-run: explain app, prompt purchase or free trial |
+| Home | `/home` | Subject list with progress indicators |
+| Subject Detail | `/subject/:id` | Subject overview, start study/cram |
+| Study Session | `/study/:id` | SM-2 review session |
+| Cram Session | `/cram/:id` | Cram mode session |
+| Session Complete | `/session-complete` | Post-session summary |
+| Statistics | `/stats` | Overall and per-subject stats |
+| Settings | `/settings` | Language, IAP, preferences |
+| Paywall | `/paywall` | Purchase screen for locked content |
+
+### 7.2 Home Screen Layout
+
+```
+┌─────────────────────────────┐
+│ Glidr [Stats] [⚙]│
+├─────────────────────────────┤
+│ Good morning! │
+│ Review streak: 🔥 7 days │
+│ │
+│ Due today: 23 cards │
+│ [Start All Reviews] │
+├─────────────────────────────┤
+│ Subjects │
+│ │
+│ 01 Air Law ■■■□□ │
+│ Due: 3 · New: 5 │
+│ │
+│ 02 Aircraft General ■■□□□ │
+│ Due: 7 · New: 5 │
+│ │
+│ 03 Communications ■□□□□ │
+│ Due: 0 · New: 5 │
+│ │
+│ ... (scrollable list) │
+└─────────────────────────────┘
+```
+
+### 7.3 Study Session Flow
+
+```
+1. Question Screen
+ ┌─────────────────────────────┐
+ │ Air Law Q23 of 34 │
+ │ Progress bar ████░░░░░░ │
+ ├─────────────────────────────┤
+ │ │
+ │ Question text here... │
+ │ │
+ │ [Figure if present, │
+ │ pinch to zoom] │
+ │ │
+ ├─────────────────────────────┤
+ │ ○ A) Option text │
+ │ ○ B) Option text │
+ │ ○ C) Option text │
+ │ ○ D) Option text │
+ └─────────────────────────────┘
+
+2. Answer Revealed Screen (after tapping an option)
+ ┌─────────────────────────────┐
+ │ Air Law Q23 of 34 │
+ ├─────────────────────────────┤
+ │ │
+ │ Question text... │
+ │ │
+ ├─────────────────────────────┤
+ │ ○ A) Option ← Wrong │
+ │ ✓ B) Option ← CORRECT │
+ │ ○ C) Option │
+ │ ○ D) Option │
+ ├─────────────────────────────┤
+ │ > Explanation (tap to │
+ │ expand) │
+ │ Under Part-SFCL... │
+ ├─────────────────────────────┤
+ │ How well did you know it? │
+ │ [Again] [Hard] [Good][Easy]│
+ └─────────────────────────────┘
+
+3. Session Complete Screen
+ ┌─────────────────────────────┐
+ │ Session Complete! │
+ │ │
+ │ Reviewed: 23 cards │
+ │ Correct: 18 (78%) │
+ │ Next review: Tomorrow │
+ │ │
+ │ [Back to Home] │
+ └─────────────────────────────┘
+```
+
+### 7.4 Cram Mode Flow
+
+```
+1. Subject screen → [Cram Mode] button
+2. Cram session: question → tap to reveal → correct/incorrect tap
+3. No self-rating buttons (just "Next" arrow)
+4. Session end: score summary (X/Y)
+```
+
+### 7.5 Figure Viewer
+
+When a question includes a figure:
+- Figure displayed inline in the question card
+- `photo_view` widget wraps the image
+- Pinch-to-zoom with min/max scale (0.5x - 5.0x)
+- Double-tap resets to fit
+- Single-tap on figure expands to full-screen overlay
+- Full-screen overlay: same zoom behavior + close button (X)
+
+---
+
+## 8. Technology Stack
+
+### 8.1 Dependencies (pubspec.yaml)
+
+```yaml
+dependencies:
+ flutter:
+ sdk: flutter
+
+ # State management
+ flutter_riverpod: ^2.5.0
+ riverpod_annotation: ^2.3.0
+
+ # Database
+ drift: ^2.18.0
+ sqlite3_flutter_libs: ^0.5.0
+ path_provider: ^2.1.0
+ path: ^1.9.0
+
+ # HTTP
+ dio: ^5.4.0
+
+ # Immutable data models
+ freezed_annotation: ^2.4.0
+ json_annotation: ^4.9.0
+
+ # Image zoom
+ photo_view: ^0.15.0
+
+ # In-app purchase
+ in_app_purchase: ^3.1.0
+
+ # Secure storage (API key, purchase state)
+ flutter_secure_storage: ^9.0.0
+
+ # Navigation
+ go_router: ^13.0.0
+
+ # UUID generation
+ uuid: ^4.4.0
+
+ # Crypto (SHA-256 for manifest verification)
+ crypto: ^3.0.0
+
+ # SVG rendering
+ flutter_svg: ^2.0.0
+
+ # Internationalization
+ flutter_localizations:
+ sdk: flutter
+ intl: ^0.19.0
+
+dev_dependencies:
+ flutter_test:
+ sdk: flutter
+ drift_dev: ^2.18.0
+ riverpod_generator: ^2.3.0
+ build_runner: ^2.4.0
+ freezed: ^2.4.0
+ json_serializable: ^6.7.0
+ flutter_lints: ^4.0.0
+```
+
+### 8.2 Flutter Configuration
+
+```
+flutter:
+ assets:
+ - assets/figures/ # All 58 figure files (PNG + SVG)
+ - assets/fonts/ # Custom fonts if used
+
+ generate: true # Enable l10n generation
+```
+
+Build-time injection of API key (never in source):
+```bash
+flutter build ios --dart-define=GLIDR_API_KEY=your_secret_here
+flutter build appbundle --dart-define=GLIDR_API_KEY=your_secret_here
+```
+
+---
+
+## 9. Backend Setup (tekmidian.com)
+
+### 9.1 Directory Structure
+
+```
+/var/www/tekmidian.com/public_html/glidr/
+├── api/
+│ └── v1/
+│ ├── .htaccess # Auth + CORS headers
+│ ├── manifest.json # Regenerated on each content update
+│ ├── subjects/
+│ │ ├── air_law.json
+│ │ ├── aircraft_general_knowledge.json
+│ │ ├── communications.json
+│ │ ├── flight_performance.json
+│ │ ├── human_performance.json
+│ │ ├── meteorology.json
+│ │ ├── navigation.json
+│ │ ├── operational_procedures.json
+│ │ └── principles_of_flight.json
+│ └── figures/
+│ ├── *.png (50 files)
+│ └── *.svg (8 files)
+└── tools/
+ └── generate_manifest.php # CLI tool to regenerate manifest.json
+```
+
+### 9.2 .htaccess Auth Configuration
+
+```apache
+# /glidr/api/v1/.htaccess
+Options -Indexes
+
+# CORS for potential web use
+Header always set Access-Control-Allow-Origin "*"
+Header always set Access-Control-Allow-Headers "X-Glidr-Key, Content-Type"
+
+# Auth check via environment variable
+SetEnvIf HTTP_X_GLIDR_KEY "^your_secret_here$" GLIDR_AUTH=1
+Order Deny,Allow
+Deny from all
+Allow from env=GLIDR_AUTH
+
+# Better: use a PHP wrapper for auth to avoid key in .htaccess
+```
+
+Better approach — PHP auth wrapper:
+
+```php
+<?php
+// auth.php — included by all JSON-serving endpoints
+$provided_key = $_SERVER['HTTP_X_GLIDR_KEY'] ?? '';
+$expected_key = getenv('GLIDR_API_KEY'); // Set in Apache/server environment
+
+if (empty($expected_key) || $provided_key !== $expected_key) {
+ http_response_code(403);
+ header('Content-Type: application/json');
+ echo json_encode(['error' => 'Forbidden']);
+ exit;
+}
+```
+
+### 9.3 Content Update Workflow
+
+When questions need to be corrected or updated:
+
+1. Edit the source Markdown files (already in Obsidian vault)
+2. Run the conversion tool (see Section 10.4) to regenerate JSON files
+3. Upload new JSON files to server via SFTP/rsync
+4. Run `generate_manifest.php` to update version hashes
+5. App users receive update on next launch
+
+---
+
+## 10. Implementation Checklists
+
+### 10.1 Phase 1: Foundation (3 weeks)
+
+#### Database & Models
+- [ ] Set up Flutter project with all dependencies listed in Section 8.1
+- [ ] Create Drift database with all tables from Section 4.3
+- [ ] Generate Drift DAOs for questions, card_progress, review_log, settings
+- [ ] Create Freezed data models for Question, CardProgress, StudySession
+- [ ] Write unit tests for all DAOs
+
+#### Content Download
+- [ ] Implement Dio HTTP client with X-Glidr-Key auth interceptor
+- [ ] Implement manifest.json fetch and local version comparison
+- [ ] Implement subject JSON download with SHA-256 verification
+- [ ] Implement SQLite import from downloaded JSON
+- [ ] Implement first-run download screen with progress indicator
+- [ ] Implement background update check on subsequent launches
+
+#### Question Display
+- [ ] Implement question card widget (text + options)
+- [ ] Implement figure viewer widget using photo_view
+- [ ] Implement SVG rendering via flutter_svg
+- [ ] Implement answer selection with color feedback (correct/incorrect)
+- [ ] Implement explanation collapsible section
+
+#### App Shell
+- [ ] Set up go_router with all routes from Section 7.1
+- [ ] Implement Riverpod provider structure
+- [ ] Implement home screen with subject list
+- [ ] Implement subject detail screen
+- [ ] Implement bilingual content switching (EN/FR)
+
+#### Testing Checklist — Phase 1
+- [ ] Unit tests: JSON parsing for all 9 subjects
+- [ ] Unit tests: SHA-256 manifest verification
+- [ ] Unit tests: SQLite import idempotency (re-importing same data is safe)
+- [ ] Widget tests: question card displays correctly for question with figure
+- [ ] Widget tests: question card displays correctly for question without figure
+- [ ] Integration test: full download flow on fresh install
+- [ ] Manual test: figure zoom works on real device
+
+### 10.2 Phase 2: Learning Engine (2 weeks)
+
+#### SM-2 Algorithm
+- [ ] Implement `computeNextInterval()` function (Section 5.1)
+- [ ] Unit test all quality grades 0-5 with known expected outputs
+- [ ] Unit test EF clamping at 1.3 minimum
+- [ ] Unit test interval reset on quality < 3 (keeps EF, resets n and I)
+- [ ] Unit test progression: n=0→1day, n=1→6days, n=2→6*EF days
+
+#### Review Queue
+- [ ] Implement `buildDailyQueue()` — due cards + new cards (Section 5.3)
+- [ ] Implement "new cards per day" limit from settings
+- [ ] Implement queue shuffle
+- [ ] Unit test queue: due cards before new cards
+- [ ] Unit test queue: respects new card limit
+
+#### Study Session UI
+- [ ] Implement study session screen with question display
+- [ ] Implement progress bar (X of Y reviewed)
+- [ ] Implement answer tap → reveal flow
+- [ ] Implement 4-button rating row (Again / Hard / Good / Easy)
+- [ ] On rating: call `computeNextInterval()`, persist to card_progress
+- [ ] Log review to review_log table
+- [ ] Implement within-session re-show for quality < 3 cards
+- [ ] Implement session completion detection
+- [ ] Implement session summary screen
+
+#### Testing Checklist — Phase 2
+- [ ] Unit tests: all SM-2 branches (quality 0-5, all n values, EF edge cases)
+- [ ] Unit tests: daily queue respects new card limit
+- [ ] Integration test: complete a study session of 10 cards, verify DB state
+- [ ] Integration test: rating "Again" re-shows card in same session
+- [ ] Integration test: session completion triggers summary screen
+- [ ] Manual test: study 20 cards in one session, check review_log entries
+
+### 10.3 Phase 3: Cram Mode (1 week)
+
+- [ ] Implement cram mode queue builder (all cards in subject, shuffled)
+- [ ] Implement cram session screen (question → reveal → next, no rating buttons)
+- [ ] Implement cram correct/incorrect tap (for session stats only)
+- [ ] Verify cram reviews do NOT modify card_progress SM-2 state
+- [ ] Log cram reviews to review_log with was_cram=1
+- [ ] Implement cram session summary screen
+
+#### Testing Checklist — Phase 3
+- [ ] Unit test: cram review does not modify card_progress
+- [ ] Integration test: cram session records in review_log with was_cram=1
+- [ ] Integration test: SM-2 state unchanged after cram session
+
+### 10.4 Phase 4: Purchase + Polish (2 weeks)
+
+#### In-App Purchase
+- [ ] Create non-consumable IAP product in App Store Connect
+- [ ] Create one-time product in Google Play Console
+- [ ] Implement `in_app_purchase` purchase flow
+- [ ] Implement "Restore Purchases" button
+- [ ] Persist unlock state in flutter_secure_storage
+- [ ] Implement paywall screen with subject preview (first 10 questions free)
+- [ ] Gate subject access: > Q10 requires purchase
+
+#### Statistics Screen
+- [ ] Implement per-subject stats: new / learning / review / mature counts
+- [ ] Implement overall retention rate calculation
+- [ ] Implement study streak counter
+- [ ] Implement "exam readiness" indicator per subject
+
+#### Onboarding
+- [ ] Implement first-run onboarding (3-4 screens: welcome, how it works, language, purchase/trial)
+- [ ] Implement first-run content download trigger
+
+#### UI Polish
+- [ ] Implement review notification (local notification at configured time)
+- [ ] Implement settings screen (language, new cards/day, reminder, restore, version)
+- [ ] Accessibility: VoiceOver labels on all interactive elements
+- [ ] Dynamic Type: test at all font sizes
+- [ ] Dark mode support
+
+#### Testing Checklist — Phase 4
+- [ ] Manual test: purchase flow on Sandbox environment (iOS)
+- [ ] Manual test: restore purchases after app delete and reinstall
+- [ ] Manual test: free tier correctly limits to 10 questions per subject
+- [ ] Accessibility audit: VoiceOver navigate entire study session
+- [ ] Manual test: dark mode on iOS and Android
+
+### 10.5 Phase 5: Backend (1 week)
+
+- [ ] Provision `tekmidian.com/glidr/api/v1/` directory
+- [ ] Set GLIDR_API_KEY server environment variable
+- [ ] Write and deploy `.htaccess` auth rules
+- [ ] Write Markdown-to-JSON conversion script (Python or PHP)
+- [ ] Convert all 9 subject Markdown files to JSON format
+- [ ] Convert French Markdown files and merge into bilingual JSON
+- [ ] Copy all figures to server
+- [ ] Write and run `generate_manifest.php` to create initial manifest.json
+- [ ] Test all endpoints with curl (with and without API key)
+- [ ] Test rate limiting (verify 429 on abuse)
+- [ ] Document update workflow in a README
+
+#### Security Checklist — Phase 5
+- [ ] HTTPS with valid SSL certificate on tekmidian.com
+- [ ] API key not present in any git repository (use environment variable)
+- [ ] Directory listing disabled (Options -Indexes)
+- [ ] Server error pages don't expose stack traces
+- [ ] Test: direct URL access without API key returns 403
+
+### 10.6 Phase 6: Testing + Release (2 weeks)
+
+- [ ] Full regression test on iPhone 13/14/15 (real devices)
+- [ ] Full regression test on Android Pixel 6/7 and Samsung Galaxy S21
+- [ ] TestFlight beta (iOS): 5-10 beta testers
+- [ ] Beta feedback incorporated
+- [ ] App Store listing: screenshots, description (EN + FR), keywords
+- [ ] App Store Review Guidelines compliance check
+- [ ] Privacy Policy published (required for App Store)
+- [ ] Submit for App Store review
+- [ ] Google Play closed testing track
+- [ ] Submit for Play Store review
+
+---
+
+## 11. Content Conversion Tool
+
+A command-line Python script to convert the existing Obsidian Markdown files to the JSON format expected by the API.
+
+### 11.1 Script Purpose
+
+Input: `/SPL Exam Questions/01 - Air Law.md` + `/SPL Exam Questions FR/01 - Droit aérien.md`
+Output: `air_law.json` matching the schema in Section 4.1
+
+### 11.2 Parsing Logic
+
+The script must handle:
+1. Parse `### Q{N}: {text} ^q{N}` as question text and number
+2. Parse `- A) ... B) ... C) ... D) ...` as answer options
+3. Parse `**Correct: {letter})**` as correct answer
+4. Parse `> **Explanation:** {text}` as explanation
+5. Parse `![[figures/{filename}]]` and `` as figure references
+6. Match each English question to its French counterpart by question number
+7. Combine into bilingual JSON structure
+
+### 11.3 Script Location
+
+`/Users/i052341/dev/apps/glidr/tools/convert_questions.py`
+
+(To be created as part of Phase 5)
+
+---
+
+## 12. Risk Assessment
+
+| Risk | Probability | Impact | Mitigation |
+|------|-------------|--------|-----------|
+| App Store rejection (IAP implementation) | Low | High | Follow IAP guidelines strictly; add required Restore Purchases button |
+| Question count discrepancy (Markdown vs spec) | Medium | Low | Conversion script validates counts and flags mismatches |
+| tekmidian.com SSL certificate expiry breaking content sync | Low | Medium | Set calendar reminder for cert renewal; app falls back to cached content |
+| API key extracted from binary | Low | Low | App contains no user data; question bank has limited commercial value; rotate key if abused |
+| Flutter major version breaking dependencies | Low | Medium | Pin to Flutter stable channel; update dependencies deliberately, not on latest |
+| French translation gaps | Medium | Medium | Conversion script flags any English questions without a matching French question |
+
+---
+
+## 13. Future Enhancements (v2 Backlog)
+
+- User accounts: sync progress across devices via iCloud/server
+- More content: additional national question banks (Germany, Austria, UK)
+- Mock exam mode: simulate real SPL exam (random selection, timed, per EASA format)
+- Detailed analytics: per-topic weak areas, study time tracking
+- Opt-in analytics: understand which questions users find hardest
+- Push notifications: study reminders
+- Certificate pinning: harden API against MITM attacks
+- Apple App Attest: prevent automated bulk downloading
+- Widget: iOS home screen widget showing "cards due today"
+- Apple Watch: quick review on watch
+- Web version: study on desktop browser (Flutter Web)
+
+---
+
+## Appendix A: Subject IDs Reference
+
+| Code | Subject Name (EN) | Subject Name (FR) | subject_id |
+|------|-------------------|-------------------|------------|
+| 01 | Air Law | Droit aérien | air_law |
+| 02 | Aircraft General Knowledge | Connaissances générales de l'aéronef | aircraft_general_knowledge |
+| 03 | Communications | Communications | communications |
+| 04 | Flight Performance and Planning | Performances et planification du vol | flight_performance |
+| 05 | Human Performance | Performance humaine | human_performance |
+| 06 | Meteorology | Météorologie | meteorology |
+| 07 | Navigation | Navigation | navigation |
+| 08 | Operational Procedures | Procédures opérationnelles | operational_procedures |
+| 09 | Principles of Flight | Principes du vol | principles_of_flight |
+
+## Appendix B: Question Count Reference
+
+| Subject | Questions in Current Files | Target (Full Bank) |
+|---------|---------------------------|-------------------|
+| Air Law | 50 (QuizVDS) | 110 |
+| Aircraft General Knowledge | 50 (QuizVDS) | 110 |
+| Communications | 50 (QuizVDS) | 90 |
+| Flight Performance and Planning | 30 (QuizVDS) | 90 |
+| Human Performance | 50 (QuizVDS) | 110 |
+| Meteorology | 50 (QuizVDS) | 110 |
+| Navigation | 80 (QuizVDS + SFVS) | 141 |
+| Operational Procedures | 50 (QuizVDS) | 110 |
+| Principles of Flight | 50 (QuizVDS) | 110 |
+| **Total** | **510** | **981** |
+
+The app works with whatever is in the JSON files. The full bank (981 questions) can be built out over time by adding BAZL mock exam questions, which are already partially present in the Markdown files as supplementary sections.
+
+## Appendix C: SM-2 EF Delta at Each Quality Grade
+
+| Quality | EF Delta | Example: EF=2.5 → |
+|---------|----------|-------------------|
+| 5 | +0.10 | 2.60 |
+| 4 | 0.00 | 2.50 |
+| 3 | -0.14 | 2.36 |
+| 2 | -0.32 | 2.18 |
+| 1 | -0.54 | 1.96 |
+| 0 | -0.80 | 1.70 |
+
+Formula: `delta = 0.1 - (5 - q) * (0.08 + (5 - q) * 0.02)`
diff --git a/tasks/research-api-security.md b/tasks/research-api-security.md
new file mode 100644
index 0000000..6465f1b
--- /dev/null
+++ b/tasks/research-api-security.md
@@ -0,0 +1,200 @@
+# API Security Research: Content Protection for Glidr
+
+## Threat Model
+
+What we are protecting:
+- A question bank (981 questions with explanations) that represents significant editorial work
+- Not financial data, medical data, or personal information
+- Business goal: prevent competitors from bulk-downloading questions for free
+
+Realistic threats:
+1. **Casual scraping** — someone writes a script to download all content
+2. **Competitor copying** — another app developer bulk-downloads the question bank
+3. NOT a concern: sophisticated nation-state attackers, reverse engineers with unlimited resources
+
+Conclusion: We need "good enough" security, not military-grade protection. A determined attacker with the app binary can always extract keys, but we want to raise the bar above casual scraping.
+
+---
+
+## Security Options Evaluated
+
+### Option 1: API Key in App Binary (Basic, Recommended for v1)
+
+**How it works:**
+- A shared secret (e.g., `X-Glidr-Key: abc123...`) is embedded in the app
+- Server validates this header on all content endpoints
+- Key is obfuscated in the binary using compile-time string splitting or encryption
+
+**Implementation:**
+```dart
+// Store as environment variable injected at build time
+// Never hardcode in plain text in source
+const apiKey = String.fromEnvironment('GLIDR_API_KEY');
+
+// In HTTP client
+dio.options.headers['X-Glidr-Key'] = apiKey;
+```
+
+**Server side (PHP example):**
+```php
+$key = $_SERVER['HTTP_X_GLIDR_KEY'] ?? '';
+if ($key !== getenv('GLIDR_API_KEY')) {
+ http_response_code(403);
+ die('Forbidden');
+}
+```
+
+**Pros:** Simple, zero infrastructure cost, stops casual scrapers
+**Cons:** Key is extractable from app binary by determined attacker
+**Verdict:** Sufficient for protecting a $49.99 exam app's question bank
+
+---
+
+### Option 2: Certificate Pinning
+
+**How it works:**
+- App validates that tekmidian.com presents the exact SSL certificate it expects
+- Prevents man-in-the-middle attacks even on compromised networks
+
+**Implementation in Dio:**
+```dart
+(dio.httpClientAdapter as IOHttpClientAdapter).onHttpClientCreate = (client) {
+ client.badCertificateCallback = (cert, host, port) => false;
+ SecurityContext context = SecurityContext();
+ context.setTrustedCertificatesBytes(pinnedCertBytes);
+ return HttpClient(context: context);
+};
+```
+
+**Pros:** Prevents MITM interception of API key
+**Cons:** Certificate rotation causes app to break; requires app update when cert expires
+**Verdict:** Nice-to-have, add in v2 if abuse becomes a problem
+
+---
+
+### Option 3: Apple App Attest + Google Play Integrity
+
+**How it works:**
+- Apple/Google cryptographically verify the app is genuine and unmodified
+- Backend receives attestation token, validates with Apple/Google servers
+- Only genuine, unmodified app instances can download content
+
+**Pros:** Strongest protection available; cannot be bypassed without jailbreak
+**Cons:**
+ - Requires backend infrastructure to validate attestation tokens
+ - Apple App Attest has rate limits (free tier: limited attestations/day)
+ - Adds significant implementation complexity
+ - Overkill for a small hobby/niche app
+
+**Verdict:** Not needed for v1. Consider for v2 if piracy becomes a real problem.
+
+---
+
+### Option 4: JWT Tokens with Device Fingerprinting
+
+**How it works:**
+- App registers with backend using device ID + purchase receipt
+- Backend issues a JWT valid for this device
+- JWT is used for all content requests
+- Content is tied to a specific "account"
+
+**Pros:** Can revoke access per device; enables future account features
+**Cons:** Requires user accounts infrastructure; adds backend complexity
+**Verdict:** Not needed for v1 one-time purchase model with no accounts
+
+---
+
+## Recommended Security Architecture for Glidr v1
+
+### Layer 1: HTTPS (mandatory, baseline)
+All API traffic over HTTPS. tekmidian.com must have a valid SSL certificate.
+
+### Layer 2: API Key Header
+- Obfuscated compile-time constant injected via `--dart-define`
+- Never committed to source control
+- Stored in CI/CD environment variables
+- Rotatable by releasing a new app version
+
+### Layer 3: Rate Limiting on Server
+- Limit to 100 requests/hour per IP
+- Limit to 10 full-bank downloads per day globally
+- Simple nginx or PHP-level throttling
+
+### Layer 4: Content Structure (Defense in Depth)
+- Serve individual subject JSON files, not one giant file
+- App downloads only what it needs (avoids one-request full dump)
+
+### Layer 5: Monitoring
+- Server access logs reviewed periodically
+- Alert on unusual download patterns
+
+---
+
+## API Endpoint Design
+
+Base URL: `https://tekmidian.com/glidr/api/v1/`
+
+All requests require header: `X-Glidr-Key: {secret}`
+
+```
+GET /manifest.json
+ Response: { "version": "1.2.0", "subjects": { "air_law": "abc123hash", ... } }
+
+GET /subjects/{subject_id}.json
+ Response: Full subject JSON with all questions
+ Example: /subjects/air_law.json
+
+GET /figures/{filename}
+ Response: Image file (PNG or SVG)
+ Example: /figures/bazl_30_q08_ask21_speed_polar.png
+```
+
+### Manifest Response Example
+```json
+{
+ "version": "1.2.0",
+ "updated_at": "2026-03-15T00:00:00Z",
+ "subjects": {
+ "air_law": { "hash": "sha256:abc...", "question_count": 110, "size_bytes": 45000 },
+ "meteorology": { "hash": "sha256:def...", "question_count": 110, "size_bytes": 52000 }
+ },
+ "figures": {
+ "hash": "sha256:ghi...",
+ "count": 58,
+ "size_bytes": 4200000
+ }
+}
+```
+
+App caches subject hashes locally. On launch, compares cached hash vs remote manifest. Downloads only changed subjects.
+
+---
+
+## Server Setup on tekmidian.com
+
+Minimal PHP implementation:
+
+```
+/glidr/
+ api/
+ v1/
+ .htaccess # auth check + routing
+ auth.php # API key validation
+ manifest.json # pre-generated static file
+ subjects/
+ air_law.json
+ meteorology.json
+ ... (9 files)
+ figures/
+ *.png
+ *.svg
+```
+
+`.htaccess`:
+```apache
+RewriteEngine On
+RewriteCond %{HTTP:X-Glidr-Key} !={GLIDR_API_KEY}
+RewriteRule ^ - [F,L]
+```
+
+This is the simplest possible implementation — Apache validates the key before serving any file.
diff --git a/tasks/research-content-analysis.md b/tasks/research-content-analysis.md
new file mode 100644
index 0000000..49577f5
--- /dev/null
+++ b/tasks/research-content-analysis.md
@@ -0,0 +1,140 @@
+# Content Analysis: SPL Exam Question Files
+
+## Summary
+
+**Total questions: 510 (English) + matching French translation**
+
+| Subject | File | EN Questions | Notes |
+|---------|------|-------------|-------|
+| 01 Air Law | 01 - Air Law.md | 50 | Source: QuizVDS.it EASA ECQB-SPL |
+| 02 Aircraft General Knowledge | 02 - Aircraft General Knowledge.md | 50 | |
+| 03 Communications | 03 - Communications.md | 50 | |
+| 04 Flight Performance and Planning | 04 - Flight Performance and Planning.md | 30 | (shorter) |
+| 05 Human Performance | 05 - Human Performance.md | 50 | |
+| 06 Meteorology | 06 - Meteorology.md | 50 | |
+| 07 Navigation | 07 - Navigation.md | 80 | Incl. Swiss SFVS exercises |
+| 08 Operational Procedures | 08 - Operational Procedures.md | 50 | |
+| 09 Principles of Flight | 09 - Principles of Flight.md | 50 | |
+
+Note: SPL Exam Questions.md index page states the full bank has 981 questions total across all sources (QuizVDS, SFVS, BAZL mock exams). The files counted above represent the QuizVDS-sourced questions. Navigation has 141 total per index including Swiss exercises.
+
+## Question Format (Markdown)
+
+Each question follows this exact structure:
+
+```markdown
+### Q{N}: {Question text} ^q{N}
+> *[FR](../SPL Exam Questions FR/01 - Droit aérien.md#^q{N})*
+- A) {option}
+- B) {option}
+- C) {option}
+- D) {option}
+**Correct: {letter})**
+
+> **Explanation:** {detailed explanation paragraph}
+```
+
+### Key structural observations:
+- Questions use `### Q{N}:` headers with Obsidian block reference anchors `^q{N}`
+- Each English question links directly to its French counterpart via relative path
+- 4 answer options always labeled A, B, C, D
+- Correct answer on its own line: `**Correct: X)**`
+- Explanation in a blockquote immediately after
+- Some questions embed images using Obsidian wiki-link syntax: `![[figures/filename.png]]`
+- Some use standard markdown: ``
+
+## Image/Figure References
+
+Two syntaxes in use:
+1. **Obsidian wiki-link**: `![[figures/bazl_30_q08_ask21_speed_polar.png]]` - used in BAZL mock exam questions
+2. **Standard markdown**: `` - used in custom-created figures
+
+### Figures directory: 58 total files
+- 8 SVG files (custom-drawn navigation diagrams)
+- 50 PNG files (BAZL official exam images: speed polars, charts, maps, airport diagrams)
+
+Images primarily appear in:
+- Navigation (SVG diagrams)
+- Flight Performance and Planning (BAZL speed polars, loading tables, approach charts, maps)
+- Operational Procedures (ground signals)
+
+## French Version
+
+- Identical structure to English
+- Same filenames (French titles): `01 - Droit aérien.md`, `06 - Météorologie.md`, etc.
+- Same question count confirmed for files checked (50 Air Law, 50 Meteorology)
+- Questions use `^q{N}` anchors matching English versions
+- Links back to English: `> *[EN](../SPL Exam Questions/01 - Air Law.md#^q1)*`
+- Shares the same `figures/` directory (images are language-neutral)
+
+## Recommended JSON Format for App
+
+```json
+{
+ "version": "1.0.0",
+ "updated_at": "2026-03-15T00:00:00Z",
+ "subjects": [
+ {
+ "id": "air_law",
+ "code": "01",
+ "name": {
+ "en": "Air Law",
+ "fr": "Droit aérien"
+ },
+ "questions": [
+ {
+ "id": "air_law_q1",
+ "number": 1,
+ "text": {
+ "en": "The holder of an SPL license...",
+ "fr": "Le titulaire d'une licence SPL..."
+ },
+ "options": {
+ "en": {
+ "A": "Winch and bungee.",
+ "B": "Winch, bungee and aero-tow.",
+ "C": "Winch and aero-tow.",
+ "D": "Aero-tow and bungee."
+ },
+ "fr": {
+ "A": "Treuil et sandow.",
+ "B": "...",
+ "C": "...",
+ "D": "..."
+ }
+ },
+ "correct": "A",
+ "explanation": {
+ "en": "Under Part-SFCL...",
+ "fr": "Selon la Part-SFCL..."
+ },
+ "figures": []
+ },
+ {
+ "id": "pfp_q14",
+ "number": 14,
+ "text": { "en": "...", "fr": "..." },
+ "options": { "en": {...}, "fr": {...} },
+ "correct": "B",
+ "explanation": { "en": "...", "fr": "..." },
+ "figures": [
+ {
+ "filename": "bazl_30_q14_discus_loading_table.png",
+ "type": "png",
+ "alt": "Discus loading table"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
+```
+
+### Key design decisions:
+- Bilingual content embedded in single question object (not separate files)
+- `figures` array per question (empty array if no figures)
+- `id` format: `{subject_code}_{qN}` for stable cross-referencing
+- `version` + `updated_at` at top level for update detection
+- Correct answer stored as letter string "A"/"B"/"C"/"D"
+- Subjects have both `id` (slug) and `code` (01-09 for ordering)
diff --git a/tasks/research-supermemo-sm2.md b/tasks/research-supermemo-sm2.md
new file mode 100644
index 0000000..54f8fd0
--- /dev/null
+++ b/tasks/research-supermemo-sm2.md
@@ -0,0 +1,172 @@
+# SuperMemo SM-2 Algorithm Research
+
+## The SM-2 Algorithm (Complete Technical Specification)
+
+Source: https://super-memory.com/english/ol/sm2.htm
+
+### Per-Card State Variables
+
+Each card (flashcard item) tracks three values:
+- `n` — repetition number (count of successful reviews, starts at 0)
+- `EF` — easiness factor (starts at 2.5, minimum 1.3)
+- `I` — current interval in days (time until next review)
+
+### Interval Schedule
+
+```
+I(1) = 1 # after first successful review: 1 day
+I(2) = 6 # after second successful review: 6 days
+I(n) = I(n-1) * EF # for n > 2: multiply previous interval by EF
+```
+
+Fractional intervals are rounded up to the nearest integer day.
+
+### Quality Grades (0–5 scale)
+
+| Grade | Meaning |
+|-------|---------|
+| 5 | Perfect response — no hesitation |
+| 4 | Correct after hesitation |
+| 3 | Correct with serious difficulty |
+| 2 | Incorrect — correct answer seemed easy to recall after seeing it |
+| 1 | Incorrect — correct answer remembered after seeing it |
+| 0 | Complete blackout — no recollection |
+
+### Easiness Factor Adjustment Formula
+
+After each review:
+```
+EF' = EF + (0.1 - (5 - q) * (0.08 + (5 - q) * 0.02))
+```
+
+Where `q` is the quality grade (0–5). Results at each grade:
+- q=5: EF increases by +0.10
+- q=4: EF unchanged (delta = 0)
+- q=3: EF decreases by -0.14
+- q=2: EF decreases by -0.32
+- q=1: EF decreases by -0.54
+- q=0: EF decreases by -0.80
+
+EF is clamped to minimum 1.3. Items with EF below 1.3 were found to be "annoyingly often repeated" and indicate a poorly-formed question.
+
+### Reset Rule (Quality < 3)
+
+When a review scores 0, 1, or 2:
+- Reset repetition counter to n=1 (restart interval sequence: 1 day, then 6 days)
+- **Do NOT change the EF** (keep accumulated EF to influence future spacing)
+- Re-show the card the same session until it scores >= 4
+
+### Session Behavior
+
+Within a single study session:
+- All items scoring < 4 are re-shown in the same session
+- Session continues until all shown items score >= 4
+- New intervals only schedule for the NEXT day's review (not within-session repetitions)
+
+---
+
+## Practical Implementation Notes
+
+### Data Model Per Card
+
+```typescript
+interface CardProgress {
+ cardId: string;
+ repetitionNumber: number; // n (0 = never studied)
+ easinessFactor: number; // EF (default 2.5, min 1.3)
+ intervalDays: number; // I (days until next review)
+ nextReviewDate: string; // ISO date string
+ lastReviewDate: string | null;
+ totalReviews: number;
+ correctReviews: number;
+}
+```
+
+### Scheduling Logic
+
+```typescript
+function scheduleNextReview(card: CardProgress, quality: number): CardProgress {
+ let { n, EF, I } = card;
+
+ if (quality < 3) {
+ // Failed: reset sequence but keep EF
+ n = 0;
+ I = 1;
+ } else {
+ // Successful: advance sequence
+ if (n === 0) I = 1;
+ else if (n === 1) I = 6;
+ else I = Math.ceil(I * EF);
+ n += 1;
+ }
+
+ // Update EF (clamp to 1.3 minimum)
+ EF = Math.max(1.3, EF + 0.1 - (5 - quality) * (0.08 + (5 - quality) * 0.02));
+
+ const nextDate = addDays(today(), I);
+ return { ...card, n, EF, I, nextReviewDate: nextDate };
+}
+```
+
+---
+
+## Cram Mode
+
+Cram mode ignores all SM-2 scheduling — it simply shows all cards in a subject (or selected subset) sequentially or randomly, without affecting the SM-2 state. Two approaches:
+
+### Option A: Fully Isolated Cram
+- Cram sessions use a separate "cram_progress" table
+- SM-2 state for normal mode is untouched
+- Good for: pre-exam cramming without corrupting spacing data
+
+### Option B: Cram Reads but Doesn't Write SM-2
+- Cram shows cards from the full bank
+- Correct/incorrect tracked for session stats only
+- SM-2 nextReviewDate is not modified
+- Simpler implementation, recommended approach
+
+### Recommended Cram Mode UX
+- Show cards in random order within a subject
+- Show question → user answers → reveal correct answer + explanation
+- Show session score at end (X/Y correct, per-subject breakdown)
+- No self-rating needed (just correct/incorrect tap)
+
+---
+
+## Multi-Subject / Multi-Deck Handling
+
+Each subject is a separate "deck" but SM-2 state is unified per-card. Recommended approach:
+
+### Daily Review Queue
+- At app open: query all cards where `nextReviewDate <= today`
+- Group by subject for display
+- Allow "study all due" or "study due in [subject]"
+
+### New Card Introduction
+- Introduce N new cards per day per subject (configurable, default: 5)
+- New cards have n=0 and no scheduled date
+- Introduce after reviewing due cards (or before — user preference)
+
+### Statistics
+- Per-subject: total cards, due today, learned (n>=2), new
+- Overall retention rate (quality >= 3 / total reviews)
+- Streak tracking (days in a row with at least 1 review)
+
+---
+
+## Self-Assessment UX for SPL App
+
+Recommended: Use a simplified 3-button rating instead of the full 0-5 scale (less cognitive load for mobile):
+
+| Button | Label | Maps to SM-2 Grade |
+|--------|-------|--------------------|
+| Again | Didn't know it | 1 |
+| Hard | Knew it with difficulty | 3 |
+| Good | Knew it well | 4 |
+| Easy | Perfect recall | 5 |
+
+This is the same simplification Anki uses. The 4-button approach covers all meaningful SM-2 branches:
+- "Again" → reset (q=1)
+- "Hard" → decrease EF significantly (q=3)
+- "Good" → keep EF stable (q=4)
+- "Easy" → increase EF (q=5)
diff --git a/tasks/research-tech-stack.md b/tasks/research-tech-stack.md
new file mode 100644
index 0000000..25414e2
--- /dev/null
+++ b/tasks/research-tech-stack.md
@@ -0,0 +1,171 @@
+# Tech Stack Research: Cross-Platform Mobile Framework
+
+## Decision: Flutter
+
+**Recommendation: Flutter with Dart**
+
+### Justification
+
+| Criterion | Flutter | React Native | Winner |
+|-----------|---------|-------------|--------|
+| UI consistency | Custom renderer, pixel-perfect on all devices | Native widgets, slight platform differences | Flutter |
+| Performance | 60/120fps via Impeller engine, compiled to native | Improved via New Architecture (Fabric), near-native | Flutter (slight edge) |
+| Offline data (SQLite) | sqflite package, excellent | better-sqlite3/op-sqlite, good | Tie |
+| Image handling + zoom | photo_view package, InteractiveViewer widget | react-native-image-zoom-viewer, good | Tie |
+| App Store IAP | in_app_purchase plugin (official Flutter team) | react-native-iap, community | Flutter (official plugin) |
+| Remote content download | dio + path_provider, well-documented | react-native-fs + axios, good | Tie |
+| Learning curve | Dart (2-3 week ramp for new devs) | JavaScript/React (huge talent pool) | React Native |
+| iOS-first development | Excellent, first-class Xcode integration | Excellent | Tie |
+| Educational app examples | Many quiz/flashcard apps | Many | Tie |
+| Bundle size | Larger (~10MB base) | Smaller (~5MB base) | React Native |
+| Hot reload speed | Fast | Fast | Tie |
+
+**Key deciding factors for Glidr:**
+1. **Pixel-perfect UI** matters for zoomable aviation charts/diagrams
+2. **Official IAP plugin** reduces risk for the one-time purchase model
+3. **Flutter 46% market share** in 2025, strong momentum
+4. **No JavaScript bridge** for image rendering means smoother zoom/pan experience
+5. App is data-heavy but not UI-heavy — Flutter's widget library handles quiz UX cleanly
+
+---
+
+## Recommended Full Stack
+
+### Mobile App
+- **Framework**: Flutter 3.x (Dart)
+- **State Management**: Riverpod 2.x (Provider successor, better testability)
+- **Local Database**: SQLite via `sqflite` + `sqflite_common_ffi` for tests
+- **ORM/Query builder**: `drift` (type-safe SQLite layer, formerly Moor)
+- **HTTP Client**: `dio` with interceptors for auth headers
+- **Image zoom**: `photo_view` package
+- **IAP**: `in_app_purchase` (official Flutter plugin, supports both App Store + Play Store)
+- **Secure storage**: `flutter_secure_storage` (Keychain on iOS, EncryptedSharedPreferences on Android)
+- **Internationalization**: Flutter's built-in `intl` + ARB files (en, fr)
+- **JSON serialization**: `json_serializable` + `freezed` for immutable data classes
+
+### Backend (Content API)
+- **Hosting**: tekmidian.com — static file server or lightweight Node.js/PHP
+- **Language**: PHP 8.x or Node.js (whatever tekmidian.com already runs)
+- **Content delivery**: Pre-built JSON files served with auth header validation
+- **No database needed** for v1 — question bank is static JSON files per subject
+
+### Content Update Mechanism
+1. App checks `/api/v1/manifest.json` on launch (version hash per subject)
+2. If local version hash differs from remote: download updated subject JSON
+3. Questions stored in SQLite after first download
+4. Images bundled with app for v1 (to avoid complex asset downloading); OR served from CDN with local cache
+
+---
+
+## Local Database Schema (SQLite via Drift)
+
+### Tables
+
+```sql
+-- Question bank (populated from downloaded JSON)
+CREATE TABLE questions (
+ id TEXT PRIMARY KEY, -- "air_law_q1"
+ subject_id TEXT NOT NULL, -- "air_law"
+ number INTEGER NOT NULL,
+ text_en TEXT NOT NULL,
+ text_fr TEXT NOT NULL,
+ options_en TEXT NOT NULL, -- JSON: {"A":"...", "B":"...", "C":"...", "D":"..."}
+ options_fr TEXT NOT NULL,
+ correct TEXT NOT NULL, -- "A", "B", "C", or "D"
+ explanation_en TEXT NOT NULL,
+ explanation_fr TEXT NOT NULL,
+ figures TEXT NOT NULL -- JSON array: [{"filename":"...", "type":"png"}]
+);
+
+-- SM-2 progress per card
+CREATE TABLE card_progress (
+ question_id TEXT PRIMARY KEY,
+ repetition_number INTEGER NOT NULL DEFAULT 0,
+ easiness_factor REAL NOT NULL DEFAULT 2.5,
+ interval_days INTEGER NOT NULL DEFAULT 1,
+ next_review_date TEXT, -- ISO date "2026-03-16", NULL = new card
+ last_review_date TEXT,
+ total_reviews INTEGER NOT NULL DEFAULT 0,
+ correct_reviews INTEGER NOT NULL DEFAULT 0,
+ created_at TEXT NOT NULL,
+ updated_at TEXT NOT NULL
+);
+
+-- Study session log (for stats/history)
+CREATE TABLE study_sessions (
+ id TEXT PRIMARY KEY,
+ started_at TEXT NOT NULL,
+ ended_at TEXT,
+ mode TEXT NOT NULL, -- "spaced_repetition" | "cram"
+ subject_id TEXT, -- NULL = mixed/all
+ cards_reviewed INTEGER NOT NULL DEFAULT 0,
+ cards_correct INTEGER NOT NULL DEFAULT 0
+);
+
+-- Per-review log (for detailed analytics)
+CREATE TABLE review_log (
+ id TEXT PRIMARY KEY,
+ session_id TEXT NOT NULL,
+ question_id TEXT NOT NULL,
+ reviewed_at TEXT NOT NULL,
+ quality INTEGER NOT NULL, -- 0-5
+ time_to_answer_ms INTEGER, -- optional: track hesitation time
+ was_cram INTEGER NOT NULL DEFAULT 0 -- boolean
+);
+
+-- Content manifest (version tracking)
+CREATE TABLE content_manifest (
+ subject_id TEXT PRIMARY KEY,
+ version TEXT NOT NULL,
+ downloaded_at TEXT NOT NULL,
+ question_count INTEGER NOT NULL
+);
+
+-- App settings
+CREATE TABLE settings (
+ key TEXT PRIMARY KEY,
+ value TEXT NOT NULL
+);
+```
+
+---
+
+## Image Handling Strategy
+
+### Option A: Bundle All Images with App (Recommended for v1)
+- All 58 figures (50 PNG + 8 SVG) = estimated ~5-10 MB total
+- Bundle in Flutter assets: `assets/figures/`
+- Display with `photo_view` for pinch-zoom
+- Pro: works offline immediately, no download complexity
+- Con: app binary is larger, updating images requires app update
+
+### Option B: Download with Question Pack
+- Images served from tekmidian.com alongside JSON
+- Downloaded to local app documents directory
+- Cached permanently
+- Pro: images can be updated without app release
+- Con: more complex implementation, first-run requires download
+
+**Recommendation**: Bundle images in v1. Total size is small (<10MB). Move to Option B if the question bank grows significantly or if images need frequent correction.
+
+---
+
+## One-Time Purchase Model
+
+### App Store (iOS)
+- IAP type: **Non-Consumable** (purchased once, restored across devices)
+- Product ID: `com.tekmidian.glidr.fullaccess` (example)
+- Free tier: first 10 questions of each subject (preview)
+- Paid tier: full question bank (unlock all 9 subjects)
+- Restore purchases button required by App Store guidelines
+
+### Play Store (Android)
+- IAP type: **One-time product** (equivalent to non-consumable)
+- Same product unlock logic
+
+### Implementation with `in_app_purchase` plugin
+```dart
+// Check purchase status at app start
+final purchases = await InAppPurchase.instance.queryPastPurchases();
+final isUnlocked = purchases.any((p) => p.productID == 'com.tekmidian.glidr.fullaccess');
+```
diff --git a/tasks/restructure_explanations.py b/tasks/restructure_explanations.py
new file mode 100644
index 0000000..89b6c9a
--- /dev/null
+++ b/tasks/restructure_explanations.py
@@ -0,0 +1,275 @@
+#!/usr/bin/env python3
+"""
+Restructure explanation text in SPL exam question files to use markdown bullets
+for option analysis sentences.
+"""
+
+import re
+import os
+from pathlib import Path
+
+BASE_DIR = Path("/Users/i052341/Daten/Cloud/04 - Ablage/Ablage 2020 - 2029/Ablage 2025/Hobbies 2025/Segelflug/Theorie/Glidr")
+
+SUBJECT_DIRS = {
+ "EN": BASE_DIR / "SPL Exam Questions EN",
+ "DE": BASE_DIR / "SPL Exam Questions DE",
+ "FR": BASE_DIR / "SPL Exam Questions FR",
+}
+
+EXPLANATION_HEADERS = {
+ "EN": "#### Explanation",
+ "DE": "#### Erklärung",
+ "FR": "#### Explication",
+}
+
+KEY_TERMS_HEADERS = {
+ "EN": "#### Key Terms",
+ "DE": "#### Begriffe",
+ "FR": "#### Termes clés",
+}
+
+# Patterns that identify option-analysis sentences.
+# Each pattern must match the START of a sentence (after splitting on sentence boundaries).
+# Strategy: a sentence is an option sentence if it STARTS with an option reference.
+# This catches all forms: "Option A is...", "Option A (label) contains...", "Options A and B are...", etc.
+OPTION_PATTERNS = [
+ # EN/DE: sentence starts with "Option" or "Options" followed by one or more letters A-D
+ r"^Options?\s+[A-D]",
+ # EN/DE: "Die Option A" or "Nur Option A"
+ r"^(?:Die|Nur|Only)\s+Options?\s+[A-D]",
+ # EN bare letter: "A is wrong", "B is incorrect" (only when followed by a form of "to be")
+ r"^[A-D]\s+(?:is|are|was|were|would|can|cannot|does|did|has|have)\b",
+ # DE bare letter: "A ist falsch"
+ r"^[A-D]\s+(?:ist|sind|war|w\u00e4re)\b",
+ # FR: "L'option A" or "L\u2019option A"
+ r"^L['\u2019]?options?\s+[A-D]",
+ # FR bare letter: "A est incorrecte"
+ r"^[A-D]\s+est\b",
+ # Generic: "Option A:" or "Option A —" or "Option A–"
+ r"^Options?\s+[A-D](?:\s*(?:,|and|und|et)\s*(?:and\s+|und\s+|et\s+)?[A-D])*\s*(?::|—|–)",
+ # "Seule l'option C" (FR), "Nur Option C" (DE), "Only Option C" (EN) - correct answer callouts
+ r"^(?:Seule\s+l['\u2019]?option|Nur\s+Option|Only\s+Option)\s+[A-D]",
+]
+
+# Compiled combined pattern (case-sensitive)
+# We split by detecting sentence starts matching any option pattern.
+# Strategy: split the explanation text into sentences, then bucket them.
+
+def split_into_sentences(text):
+ """
+ Split text into sentences. Split on sentence-ending punctuation followed by
+ a space and a capital letter. Handles:
+ - ". Capital"
+ - '." Capital' (period inside quotes)
+ - ".'" / ".»" etc.
+ """
+ # Split on: period (optionally followed by closing quote/bracket) then whitespace then capital
+ parts = re.split(r'(?<=\.)["\'\u201d\u2019»]?\s+(?=[A-Z\xdc\xc4\xd6L\'"«\u201c\u2018])', text.strip())
+ return parts
+
+
+def is_option_sentence(sentence, lang):
+ """Return True if the sentence is an option-analysis sentence."""
+ s = sentence.strip()
+ for pat in OPTION_PATTERNS:
+ if re.match(pat, s, re.IGNORECASE):
+ return True
+ return False
+
+
+def bold_option_reference(sentence):
+ """
+ Bold the initial option reference in a sentence.
+ e.g. "Option A is wrong..." -> "**Option A** is wrong..."
+ "Option A (label) contains..." -> "**Option A** (label) contains..."
+ "Options A and B are..." -> "**Options A and B** are..."
+ "L'option A est..." -> "**L'option A** est..."
+ "Only Option C correctly..." -> "Only **Option C** correctly..."
+ "A is wrong" -> "**A** is wrong"
+ """
+ s = sentence.strip()
+
+ patterns_to_bold = [
+ # EN/DE multi: "Options A, B, and C" / "Options A und B" (must come before single)
+ (r'^(Options?\s+[A-D](?:\s*(?:,|and|und)\s*(?:and\s+|und\s+)?[A-D])+)', r'**\1**'),
+ # EN/DE single: "Option A"
+ (r'^(Options?\s+[A-D])\b', r'**\1**'),
+ # FR multi: "L'option A et B"
+ (r"^(L['\u2019]?options?\s+[A-D](?:\s*(?:,|et)\s*(?:et\s+)?[A-D])+)", r'**\1**'),
+ # FR single: "L'option A"
+ (r"^(L['\u2019]?options?\s+[A-D])\b", r'**\1**'),
+ # EN "Only Option C" -> "Only **Option C**"
+ (r'^(Only\s+)(Options?\s+[A-D])\b', r'\1**\2**'),
+ # DE "Nur Option C"
+ (r'^(Nur\s+)(Options?\s+[A-D])\b', r'\1**\2**'),
+ # FR "Seule l'option C"
+ (r"^(Seule\s+)(l['\u2019]?options?\s+[A-D])\b", r'\1**\2**'),
+ # DE "Die Option A"
+ (r'^(Die\s+)(Options?\s+[A-D])\b', r'\1**\2**'),
+ # Bare letter: "A is", "B est"
+ (r'^([A-D])(\s+(?:is|are|ist|sind|est|sont|was|w\u00e4re|serait)\b)', r'**\1**\2'),
+ ]
+
+ for pat, repl in patterns_to_bold:
+ new_s = re.sub(pat, repl, s, count=1, flags=re.IGNORECASE)
+ if new_s != s:
+ return new_s
+
+ return s
+
+
+def restructure_explanation(explanation_text, lang):
+ """
+ Given the raw explanation text (without the header line), restructure it:
+ - Sentences before the first option-analysis sentence -> main paragraph
+ - Option-analysis sentences -> bullet points
+ Returns (new_text, was_changed: bool)
+ """
+ text = explanation_text.strip()
+ if not text:
+ return text, False
+
+ sentences = split_into_sentences(text)
+ if not sentences:
+ return text, False
+
+ # Find index of first option sentence
+ first_option_idx = None
+ for i, s in enumerate(sentences):
+ if is_option_sentence(s, lang):
+ first_option_idx = i
+ break
+
+ if first_option_idx is None:
+ # No option sentences found, leave as-is
+ return text, False
+
+ # Main paragraph: sentences before first option sentence
+ main_sentences = sentences[:first_option_idx]
+ option_sentences = sentences[first_option_idx:]
+
+ # Verify that option_sentences are indeed all option-like (some trailing sentences might not be)
+ # We keep going: once we see an option sentence, everything else goes to bullets
+ # (this matches the described format where option analysis is at the end)
+
+ main_paragraph = " ".join(s.strip() for s in main_sentences if s.strip())
+
+ bullets = []
+ for s in option_sentences:
+ s = s.strip()
+ if not s:
+ continue
+ # Remove trailing period for bullet, then re-add
+ s_clean = s.rstrip(".")
+ bolded = bold_option_reference(s_clean)
+ bullets.append(f"- {bolded}.")
+
+ if not bullets:
+ return text, False
+
+ # Build new text
+ parts = []
+ if main_paragraph:
+ parts.append(main_paragraph)
+ parts.append("") # blank line
+ parts.extend(bullets)
+
+ new_text = "\n".join(parts)
+
+ # Only report change if something actually changed
+ changed = new_text.strip() != text.strip()
+ return new_text, changed
+
+
+def process_file(filepath, lang, explanation_header, key_terms_header):
+ """
+ Process a single markdown file. Returns (content, count_restructured).
+ """
+ content = filepath.read_text(encoding="utf-8")
+ lines = content.split("\n")
+
+ count = 0
+ result_lines = []
+ i = 0
+
+ while i < len(lines):
+ line = lines[i]
+
+ # Check if this line is the explanation header
+ if line.strip() == explanation_header:
+ result_lines.append(line)
+ i += 1
+
+ # Collect blank lines after header
+ while i < len(lines) and lines[i].strip() == "":
+ result_lines.append(lines[i])
+ i += 1
+
+ # Collect the explanation body until we hit:
+ # - key terms header
+ # - next question (### Q)
+ # - end of file
+ explanation_body_lines = []
+ while i < len(lines):
+ l = lines[i]
+ if (l.strip() == key_terms_header or
+ l.strip().startswith("### Q") or
+ l.strip().startswith("#### ")):
+ break
+ explanation_body_lines.append(l)
+ i += 1
+
+ # The explanation body (trim trailing blanks)
+ raw_body = "\n".join(explanation_body_lines).rstrip()
+
+ new_body, changed = restructure_explanation(raw_body, lang)
+
+ if changed:
+ count += 1
+ result_lines.append(new_body)
+ result_lines.append("") # trailing blank line
+ else:
+ # Restore original lines
+ for bl in explanation_body_lines:
+ result_lines.append(bl)
+
+ else:
+ result_lines.append(line)
+ i += 1
+
+ new_content = "\n".join(result_lines)
+ return new_content, count
+
+
+def main():
+ total_restructured = 0
+ total_files = 0
+
+ for lang, dir_path in SUBJECT_DIRS.items():
+ explanation_header = EXPLANATION_HEADERS[lang]
+ key_terms_header = KEY_TERMS_HEADERS[lang]
+
+ md_files = sorted(dir_path.glob("*.md"))
+ for filepath in md_files:
+ # Skip the combined file
+ if "SPL Exam Questions" in filepath.name:
+ continue
+
+ new_content, count = process_file(
+ filepath, lang, explanation_header, key_terms_header
+ )
+
+ if count > 0:
+ filepath.write_text(new_content, encoding="utf-8")
+ print(f" [{lang}] {filepath.name}: {count} explanation(s) restructured")
+ total_restructured += count
+ else:
+ print(f" [{lang}] {filepath.name}: no changes")
+
+ total_files += 1
+
+ print(f"\nDone. {total_restructured} explanations restructured across {total_files} files.")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/tasks/todo.md b/tasks/todo.md
new file mode 100644
index 0000000..5f0db08
--- /dev/null
+++ b/tasks/todo.md
@@ -0,0 +1,117 @@
+# Glidr - TODO
+
+## Session 2026-03-16: Xcode Build + App Store Submission
+
+### What Was Done
+
+#### Xcode Project Setup
+- Created Glidr.xcodeproj / Glidr.xcworkspace (renamed from Runner)
+- All Runner references replaced with Glidr in: pbxproj, Podfile, xcscheme, xcworkspacedata, xcconfig files, storyboards
+- Team ID: 7KU642K5ZL, Bundle ID: com.tekmidian.glidr
+- Signing: Automatic with Apple Development: Matthias Nott
+- Created Runner.xcscheme symlink -> Glidr.xcscheme (needed for flutter CLI)
+
+#### Build Issues Fixed
+- Homebrew rsync 3.4.1 incompatible with Xcode distribution (causes "Copy failed")
+ - Fix: `mv /opt/homebrew/bin/rsync /opt/homebrew/bin/rsync.bak` before distribution, restore after
+- Icon alpha channels: App Store rejects transparent icons
+ - Fix: Python PIL script removes alpha, saves as RGB
+- Overflow UI indicator ("RFLOWED BY" text): leadingWidth too small in home_screen.dart
+ - Fix: Wrapped in ClipRect + OverflowBox, increased leadingWidth to 110
+
+#### App Store Connect (Apple ID: 6760631689)
+- App name: Glider Pilot (Glidr was taken)
+- Apple account: mn@mnsoft.org
+- Build 1.0.0 uploaded successfully (with dSYM warning - non-blocking)
+- Pricing: FREE ($0.00) - changed from initial $49.99 mistake
+- In-App Purchase created:
+ - Name: Full Access
+ - Product ID: com.tekmidian.glidr.fullaccess
+ - Type: Non-Consumable
+ - Price: $49.99 (all 175 regions)
+ - Localization: "Full Access - All Questions" / "Unlock all 950 SPL exam questions in EN/FR/DE."
+- Category: Education
+- Age Rating: 4+ (all "None" for all categories)
+- Content Rights: No third-party content
+- Encryption: None (no custom encryption)
+- 7 screenshots uploaded (resized from 1320x2868 to 1284x2778 for 6.5" display)
+- Description, promotional text, keywords, support URL, marketing URL all filled
+- Build attached to version, compliance handled
+- Review contact: Matthias Nott, mn@mnsoft.org, +41 79 000 0000 (placeholder)
+- Review notes filled
+
+#### App Store Submission Blockers (remaining)
+- [x] Privacy Policy URL - set to https://youdrill.com/glidr/privacy.html
+- [x] Content Rights - set to "no third-party content"
+- [ ] iPad 13-inch screenshot - created at screenshots/ipad/ipad_home.png but NOT uploaded yet
+- [ ] App Privacy practices - need to click "Get Started" and fill in (app collects no data)
+- [ ] Agreement Update dialog keeps appearing - may need to re-accept developer agreement
+- [ ] Submit for review (click "Add for Review")
+
+#### Docker / youdrill.com Server
+- Created youdrill-website container (nginx:alpine) alongside existing youdrill-api (node:22-alpine)
+- docker-compose.yml at /opt/data/youdrill/docker-compose.yml - has both services
+- Traefik config at /opt/data/traefik/dynamic/youdrill.yaml - routes /glidr/ to website, rest to API
+- Static files at /opt/data/youdrill/website/
+ - /glidr/privacy.html - privacy policy (live, verified 200)
+ - /glidr/support.html - support page
+ - /glidr/index.html - landing page
+ - /index.html - root (for healthcheck)
+- Container is healthy
+
+#### Code Changes Made
+- ios/Glidr/Info.plist: CFBundleDisplayName changed to "Glider Pilot"
+- lib/screens/home_screen.dart: title changed to "Glider Pilot", leading wrapped in ClipRect/OverflowBox
+- create_icon.py: OUTPUT_DIR path updated from Runner to Glidr
+- ios/Glidr.xcodeproj/xcshareddata/xcschemes/Glidr.xcscheme: LaunchAction buildConfiguration changed to Release
+- Runner.xcscheme symlink created pointing to Glidr.xcscheme
+
+#### App Icon
+- Smiling glider plane icon restored from git history (commit a8a0c56)
+- Alpha channels removed from all 15 icon PNGs for App Store compliance
+- Icons in ios/Glidr/Assets.xcassets/AppIcon.appiconset/
+
+### Known Issues
+- App crashes on iPhone restart when deployed via Xcode Cmd+R (development signing issue)
+ - Dev-signed apps have get-task-allow=true, iOS 26 kills process without debugger
+ - Fix: Use flutter build ipa --release + devicectl install, or TestFlight
+ - flutter build ipa --release --no-pub (then xcrun devicectl device install app)
+- Homebrew rsync must be temporarily moved aside for Xcode distribution
+- Scheme rename to "Glider Pilot" broke flutter CLI - reverted to "Glidr" with Runner symlink
+- Simulators must be shut down before flutter device detection (xcrun simctl shutdown all)
+- flutter run --release hangs on device detection with Xcode 26 beta
+
+### Deploy Commands
+```bash
+# Build release IPA
+cd /Users/i052341/dev/apps/glidr
+flutter build ipa --release --no-pub
+
+# Install on iPhone
+xcrun devicectl device install app --device 00008150-001609EA3CEA401C build/ios/ipa/glidr.ipa
+
+# Archive and distribute (Xcode GUI)
+# 1. Move homebrew rsync: mv /opt/homebrew/bin/rsync /opt/homebrew/bin/rsync.bak
+# 2. Open Glidr.xcworkspace, set destination to "Any iOS Device"
+# 3. Product > Archive
+# 4. Distribute App > App Store Connect > Distribute
+# 5. Restore rsync: mv /opt/homebrew/bin/rsync.bak /opt/homebrew/bin/rsync
+```
+
+## Previously Completed (2026-03-15)
+
+- [x] Flutter app built and deployed to iPhone (release build)
+- [x] 950 questions across 9 subjects in EN/FR/DE
+- [x] SM-2 spaced repetition, cram mode, browse all, statistics
+- [x] Smiling glider app icon
+- [x] Content sync from youdrill.com
+- [x] Articles system
+- [x] Converter handles all 3 languages
+
+## Next Up
+- [ ] Upload iPad screenshot to ASC
+- [ ] Complete App Privacy practices in ASC
+- [ ] Submit for App Review
+- [ ] Update phone number in review contact
+- [ ] Test release build on iPhone (standalone restart)
+- [ ] TestFlight testing
diff --git a/xcode_initial.png b/xcode_initial.png
new file mode 100644
index 0000000..77044eb
--- /dev/null
+++ b/xcode_initial.png
Binary files differ
--
Gitblit v1.3.1