PageSourceSearch

https://bradleysmaths.co.uk/hub-engine.js

js bradleysmaths.co.uk collected 2026-10-09 00:58:49 UTC 47,272 bytes, 816 lines download raw bytes

1// hub-engine.js - Bradley Gold Standard Unified Engine 
2  const BradleyHub = {
3    state: {
4        tier: localStorage.getItem('bradley_tier') || 'gcse',
5        seenIds: JSON.parse(localStorage.getItem('bradley_seen_ids') || '[]'),
6        correctIds: JSON.parse(localStorage.getItem('bradley_correct_ids') || '[]'),
7        // --- NEW: PAYWALL TRACKING ---
8        freeQuestionsLeft: localStorage.getItem('bradley_free_left') !== null ? parseInt(localStorage.getItem('bradley_free_left')) : 20,
9        isPremium: localStorage.getItem('bradley_premium') === 'true',
10        masterVault: [],
11        activeMonths: ['2025-12', '2026-01', '2026-02', '2026-03', '2026-04', '2026-05', '2026-06', '2026-07', '2026-08'], // Updated with standard YYYY-MM
12        currentGroup: null, 
13        isTeacherMode: false
14    },
15
16    // AUTOMATIC WORKSHEET LOOKUP DIRECTORY (Separated by Tier)
17    worksheetDirectory: {
18        gcse: {
19            // --- NUMBER ---
20            "Natural Numbers and Integers": "https://payhip.com/b/NF60m",
21            "Squares and Cubes": "https://payhip.com/b/qOE5o",
22            "Prime Numbers HCF LCM": "https://payhip.com/b/ul4LH",
23            "Rational and Irrational Numbers and Reciprocals": "https://payhip.com/b/ND45g",
24            "Priority of Operations": "https://payhip.com/b/85Ieb",
25            "Using a Calculator": "https://payhip.com/b/KTXaO",
26            "Integer Indices and Roots": "https://payhip.com/b/fBMNb",
27            "Fractional Indices and Roots": "https://payhip.com/b/iU6Nj",
28            "Standard Form": "https://payhip.com/b/ZAkTu",
29            "Surds": "https://payhip.com/b/BaZjD",
30            "Ordering": "https://payhip.com/b/peXuz",
31            "Operations with Integers and Decimals": "https://payhip.com/b/NKrd4",
32            "Multiplication and Division of Fractions": "https://payhip.com/b/Qd4GN",
33            "Addition and Subtraction of Fractions": "https://payhip.com/b/Da8mk",
34            "Operations with Mixed Numbers": "https://payhip.com/b/c0tPv",
35            "Rounding, Estimation and Error Intervals": "https://payhip.com/b/eg8sA",
36            "Limits of Accuracy and Bounds": "https://payhip.com/b/qYp8g",
37            "Fraction Decimal Conversion": "https://payhip.com/b/XoqhS",
38            "Fraction, Decimal, Percentage Conversions": "https://payhip.com/b/K7zsa",
39            "Recurring Decimals to Fractions": "https://payhip.com/b/dAvne",
40            "Basic Percentages": "https://payhip.com/b/U3CGa",
41            "Percentage Change and Interest": "https://payhip.com/b/X8Yo2",
42            "Reverse Percentages": "https://payhip.com/b/UYy0Q",
43            "Ratio and Sharing": "https://payhip.com/b/o7A2P",
44            "Direct and Inverse Proportion": "https://payhip.com/b/JYBkq",
45            "Compound Units: Speed, Density, Pressure": "https://payhip.com/b/DK7b1",
46            "Rates of Pay, Unit pricing, and Currency": "https://payhip.com/b/WeoVy",
47            "Time": "https://payhip.com/b/flD19",
48            "Growth and Decay": "https://payhip.com/b/eRHBx",
49            "General Iterative Processes": "https://payhip.com/b/rAIp4",
50            "Systematic Listing and the Product Rule": "https://payhip.com/b/o3aVn",
51            "Scale Factors, Diagrams, and Maps": "https://payhip.com/b/SIUqC",
52            "Comparing Lengths, Areas, and Volumes": "https://payhip.com/b/bgm5B",
53
54            // --- ALGEBRA ---
55            "Introduction to Algebra": "https://payhip.com/b/KaG8c",
56            "Simplifying Expressions": "https://payhip.com/b/yAeG9",
57            "Expanding Brackets": "https://payhip.com/b/yac9b",
58            "Factorising Expressions 1": "https://payhip.com/b/SHDG1",
59            "Factorising Expressions 2": "https://payhip.com/b/xqVUj",
60            "Completing the Square": "https://payhip.com/b/miLap",
61            "Algebraic Proof": "https://payhip.com/b/EKtuw",
62            "Algebraic Fractions 1": "https://payhip.com/b/KPeax",
63            "Algebraic Fractions 2": "https://payhip.com/b/jJHdP",
64            "Rational Expressions": "https://payhip.com/b/Natzp",
65            "Indices": "https://payhip.com/b/hGzMV",
66            "Constructing Expressions": "https://payhip.com/b/ZFqwQ",
67            "Solving Linear Equations": "https://payhip.com/b/v65fA",
68            "Solving Fractional Equations": "https://payhip.com/b/wdgX1",
69            "Simultaneous Linear Equations": "https://payhip.com/b/Cq1JG",
70            "Simultaneous Equations Non-Linear": "https://payhip.com/b/BqWhb",
71            "Quadratics Equations Factorising": "https://payhip.com/b/lGQis",
72            "Quadratic Equations - Formula and Completing the Square": "https://payhip.com/b/7CT3V",
73            "Iteration": "https://payhip.com/b/IoYTp",
74            "Changing the Subject of a Formula": "https://payhip.com/b/9r34U",
75            "GCSE Higher Tier Maths - Linear inequalities": "https://payhip.com/b/d84xC",
76            "Linear inequalities in One Variable": "https://payhip.com/b/d84xC",
77            "Linear Inequalities in two Variables": "https://payhip.com/b/r7o0O",
78            "Sequences": "https://payhip.com/b/GWKby",
79            "Functions: Domain and Range": "https://payhip.com/b/F9KRd",
80            "Inverse Functions": "https://payhip.com/b/SIpux",
81            "Composite Functions": "https://payhip.com/b/A7Wup",
82            "Cartesian Coordinates": "https://payhip.com/b/qkrPY",
83            "Plotting Linear Graphs": "https://payhip.com/b/lHJUa",
84            "Gradient": "https://payhip.com/b/uEo2p",
85            "Length and Midpoint of a Line Segment": "https://payhip.com/b/zijkw",
86            "Equations of Linear Graphs": "https://payhip.com/b/U8umo",
87            "Parallel and Perpendicular Lines 1": "https://payhip.com/b/y07Oc",
88            "Parallel and Perpendicular Lines 2": "https://payhip.com/b/NZ7ha",
89            "Graphs in Practical Situations": "https://payhip.com/b/7keWD",
90            "Rates of Change from Graphs": "https://payhip.com/b/cpuDV",
91            "Plotting Curved Graphs": "https://payhip.com/b/5b3Av",
92            "Solving Equations Graphically": "https://payhip.com/b/uF4cK",
93            "Sketching Graphs": "https://payhip.com/b/t8xQq",
94            "Graphical Representation of Inverse Proportion": "https://payhip.com/b/uEa8o",
95            "Quadratic Inequalities": "https://payhip.com/b/3oWKQ",
96            "Gradients and Area under Graphs": "https://payhip.com/b/9wXvI",
97            "Equation of a Circle": "https://payhip.com/b/OnqfT",
98            "Trigonometric Graphs": "https://payhip.com/b/uyDvx",
99            "Solving Trig Equations Graphically": "https://payhip.com/b/AtoLW",
100            "Transformations": "https://payhip.com/b/FDaTw",
101
102            // --- GEOMETRY ---
103            "Angle Properties": "https://payhip.com/b/lxSzN",
104            "Triangles": "https://payhip.com/b/BHgYW",
105            "Quadrilaterals": "https://payhip.com/b/eXEl8",
106            "Polygons": "https://payhip.com/b/4zntl",
107            "Circle Properties": "https://payhip.com/b/mCeEQ",
108            "Symmetry in 2D": "https://payhip.com/b/8nGQw",
109            "Geometrical Constructions": "https://payhip.com/b/ErOuY",
110            "Loci": "https://payhip.com/b/Ql5p0",
111            "Scale Drawings and Bearings": "https://payhip.com/b/iW9eY",
112            "Similarity in 2D": "https://payhip.com/b/L2dNU",
113            "Triangle Congruence": "https://payhip.com/b/FS9zC",
114            "Geometrical Proof": "https://payhip.com/b/R1v2e",
115            "Introduction to 3D Solids and Nets": "https://payhip.com/b/PjiBG",
116            "Plans and Elevations": "https://payhip.com/b/9KeKR",
117            "Symmetry in 3D": "https://payhip.com/b/AfsYi",
118            "Similarity in 3D": "https://payhip.com/b/iBzE5",
119            "Circle Theorems 1": "https://payhip.com/b/RLgSJ",
120            "Circle Theorems 2": "https://payhip.com/b/wm0xL",
121            "Circle Theorem Proofs": "https://payhip.com/b/1rJUj",
122            "Units of Measure": "https://payhip.com/b/OItpm",
123            "Perimeter and Area in 2D": "https://payhip.com/b/zukBR",
124            "Circumference and Area of Circles": "https://payhip.com/b/K8QAT",
125            "Arcs and Sectors": "https://payhip.com/b/fiURV",
126            "Surface Area and Volume in 3D": "https://payhip.com/b/aJhQL",
127            "Compound Shapes in 2D": "https://payhip.com/b/z2KF1",
128            "Compound Shapes in 3D": "https://payhip.com/b/m8Stz",
129            "Pythagoras Theorem": "https://payhip.com/b/9dSh3",
130            "Right Angled Trigonometry": "https://payhip.com/b/h3BS6",
131            "Exact Trigonometric values": "https://payhip.com/b/GToVZ",
132            "Sine Rule": "https://payhip.com/b/EaYM1",
133            "Cosine Rule": "https://payhip.com/b/RHeNL",
134            "Trigonometry in 3D": "https://payhip.com/b/QsiXK",
135            "Vectors in 2D": "https://payhip.com/b/lemlz",
136            "Vector Geometry": "https://payhip.com/b/CZOJ4",
137            "Vector Problem Solving": "https://payhip.com/b/LTjeD",
138
139            // --- PROBABILITY & STATISTICS ---
140            "Introduction to Probability": "https://payhip.com/b/Z7KYo",
141            "Relative and Expected Frequencies": "https://payhip.com/b/csAoM",
142            "Systematic listing Strategies": "https://payhip.com/b/1nLdO",
143            "Frequency Trees": "https://payhip.com/b/3Qiey",
144            "Sets and Venn Diagrams": "https://payhip.com/b/oW2c1",
145            "The Addition Rule": "https://payhip.com/b/Jr67s",
146            "Probability Tree diagrams": "https://payhip.com/b/8EhLZ",
147            "Probability of Combined Events": "https://payhip.com/b/FGC0u",
148            "Conditional Probability": "https://payhip.com/b/MoE4j",
149            "Sampling": "https://payhip.com/b/rCsYM",
150            "Data Handling and Interpretation": "https://payhip.com/b/YC2BF",
151            "Averages and Spread – Ungrouped Data": "https://payhip.com/b/vRHXh",
152            "Averages and Spread – Grouped Data": "https://payhip.com/b/vZuiV",
153            "Statistical Charts and Diagrams": "https://payhip.com/b/59j80",
154            "Time Series": "https://payhip.com/b/19T3Q",
155            "Scatter Diagrams": "https://payhip.com/b/JLTGt",
156            "Cumulative Frequency Diagrams": "https://payhip.com/b/fmvej",
157            "Box and Whisker Plots": "https://payhip.com/b/L5f6Y",
158            "Histograms": "https://payhip.com/b/ND5E9"
159        },
160        igcse: {
161            // --- NUMBER ---
162            "Natural Numbers and Integers": "https://payhip.com/b/3mzNO",
163            "Squares and Cubes": "https://payhip.com/b/dOlIS",
164            "Prime Numbers, HCF, LCM": "https://payhip.com/b/Ljdog",
165            "Rationals, Irrationals and Reciprocals": "https://payhip.com/b/PivFe",
166            "Sets and Venn Diagrams": "https://payhip.com/b/Xg21p",
167            "Integer Indices": "https://payhip.com/b/ao0mZ",
168            "Fraction Decimal Conversion": "https://payhip.com/b/Kj3IV",
169            "Fractions, Decimals, Percentages": "https://payhip.com/b/x41Cy",
170            "Recurring Decimals": "https://payhip.com/b/XYDWV",
171            "Ordering": "https://payhip.com/b/2DA9q",
172            "The Four Operations (Integers and Decimals)": "https://payhip.com/b/sOdmt",
173            "Multiplication and Division of Fractions": "https://payhip.com/b/M1Q2H",
174            "Addition and Subtraction of Fractions": "https://payhip.com/b/5RxOz",
175            "The Four Operations with Mixed Numbers": "https://payhip.com/b/gpWKf",
176            "Fractional Indices and Roots": "https://payhip.com/b/ZKKG2",
177            "Standard Form": "https://payhip.com/b/rTMY4",
178            "Estimation": "https://payhip.com/b/dyVCM",
179            "Limits of Accuracy": "https://payhip.com/b/9LFtZ",
180            "Ratio and Proportion": "https://payhip.com/b/djNVP",
181            "Rates": "https://payhip.com/b/pVxqP",
182            "Percentages (Basic Calculations)": "https://payhip.com/b/ePmz8",
183            "Percentage Change and Interest": "https://payhip.com/b/Ebcut",
184            "Reverse Percentages": "https://payhip.com/b/1ZaHA",
185            "Using a Calculator": "https://payhip.com/b/L2W09",
186            "Time": "https://payhip.com/b/XKUVE",
187            "Money": "https://payhip.com/b/fVsJi",
188            "Exponential Growth and Decay": "https://payhip.com/b/JrS9d",
189            "Surds": "https://payhip.com/b/QTHoU",
190
191    // --- ALGEBRA ---
192
193            "Introduction to Algebra": "https://payhip.com/b/z4xbu",
194            "Simplifying Expressions": "https://payhip.com/b/HU9z8",
195            "Expanding Products": "https://payhip.com/b/G9rN4",
196            "Factoring Expressions": "https://payhip.com/b/emNE1",
197            "Factorising 2": "https://payhip.com/b/ZsgUq",
198            "Completing the Square": "https://payhip.com/b/NTGZX",
199            "Algebraic Fractions 1": "https://payhip.com/b/2UYz3",
200            "Algebraic Fractions 2": "https://payhip.com/b/x02Ge",
201            "Factorise and Simplify Rational Expressions": "https://payhip.com/b/V07A3",
202            "Indices II": "https://payhip.com/b/0RBC9",
203            "Constructing Expressions, Equations and Formulas": "https://payhip.com/b/5gq1m",
204            "Solving Linear Equations in One Unknown": "https://payhip.com/b/yxGRl",
205            "Fractional Linear Equations": "https://payhip.com/b/8H6Yg",
206            "Simultaneous Linear Equations": "https://payhip.com/b/XW1im",
207            "Simultaneous Equations (Linear – Quadratic)": "https://payhip.com/b/0CP5n",
208            "Simultaneous Equations in Context": "https://payhip.com/b/WZ92k",
209            "Quadratic Equations 1": "https://payhip.com/b/elEAX",
210            "Non-Factorisable Quadratics": "https://payhip.com/b/vs4gh",
211            "Changing the Subject of a Formula": "https://payhip.com/b/Ipgz3",
212            "Linear Inequalities": "https://payhip.com/b/HYsu3",
213            "Linear Inequalities in Two Variables": "https://payhip.com/b/Of64Q",
214            "Sequences": "https://payhip.com/b/5HqEG",
215            "Proportion": "https://payhip.com/b/e47ks",
216            "Graphs in Practical Situations": "https://payhip.com/b/wyZ3k",
217            "Plotting Curved Graphs": "https://payhip.com/b/D82tp",
218            "Solving equations graphically": "https://payhip.com/b/EnSXH",
219            "Sketching graphs": "https://payhip.com/b/Et8dD",
220            "Estimating Gradients and Basic Differentiation": "https://payhip.com/b/Znxo4",
221            "Stationary Points": "https://payhip.com/b/pL8io",
222            "Functions, Domain, Range and Notation": "https://payhip.com/b/Ttui3",
223            "Inverse Functions": "https://payhip.com/b/w0oYj",
224            "Composite Functions": "https://payhip.com/b/vU1Tk",
225
226        //COORDINATE GEOMETRY AND GEOMETRY
227
228            "Cartesian Coordinates": "https://payhip.com/b/nkrUT",
229            "Plotting Linear Graphs": "https://payhip.com/b/gdyY8",
230            "Gradient of a Line": "https://payhip.com/b/kqp1P",
231            "Length and Midpoint of a Line Segment": "https://payhip.com/b/BO8dT",
232            "Equations of Linear Graphs": "https://payhip.com/b/hboKE",
233            "Parallel and Perpendicular Lines, Core Concepts": "https://payhip.com/b/WfQZA",
234            "Parallel and Perpendicular Lines, Mixed Problems": "https://payhip.com/b/yhHQo",
235            "Triangles": "https://payhip.com/b/iCURe",
236            "Quadrilaterals": "https://payhip.com/b/JWU5q",
237            "Polygons and Circles": "https://payhip.com/b/InbTp",
238            "Introduction to 3D Solids and Nets": "https://payhip.com/b/C0nbc",
239            "Geometric Constructions and Nets": "https://payhip.com/b/k96M2",
240            "Scale Drawings and Bearings": "https://payhip.com/b/yjBmH",
241            "Similarity in 2D Shapes": "https://payhip.com/b/R9W2O",
242            "Solids and the Linear, Area, Volume Relationship": "https://payhip.com/b/e9UMS",
243            "Symmetry in 2D Shapes": "https://payhip.com/b/E0TnV",
244            "Symmetry in 3D Shapes": "https://payhip.com/b/8k2wM",
245            "Angle Properties": "https://payhip.com/b/hlX9d",
246            "Circle Theorems 1": "https://payhip.com/b/1y6TA",
247            "Circle Theorems 2": "https://payhip.com/b/xrmnz",
248
249        //--- MENSURATION AND TRIGONOMETRY
250        
251            "Units of Measure": "https://payhip.com/b/Fp1f6",
252            "Area and Perimeter": "https://payhip.com/b/XrjWA",
253            "Circles": "https://payhip.com/b/K4up0",
254            "Arcs and Sectors": "https://payhip.com/b/gR9jw",
255            "Surface Areas and Volumes": "https://payhip.com/b/RH1sZ",
256            "2D Compound Shapes": "https://payhip.com/b/a1FT0",
257            "3D Compound Shapes": "https://payhip.com/b/Y6gsh",
258            "Pythagoras Theorem": "https://payhip.com/b/uZ8cD",
259            "SOHCAHTOA": "https://payhip.com/b/kXZLe",
260            "Exact Trigonometric Values": "https://payhip.com/b/cHHoW",
261            "Graphs of Trigonometric Functions": "https://payhip.com/b/VxA7J",
262            "Solving Trigonometric Equations": "https://payhip.com/b/ea1Ti",
263            "The Sine Rule": "https://payhip.com/b/5UfgG",
264            "The Cosine Rule": "https://payhip.com/b/KLYAp",
265            "Pythagoras and Trigonometry in 3D": "https://payhip.com/b/udQPT",
266
267        //tRANSFORMATIONS AND VECTORS, PROBABILITY, AND STATISTICS
268                
269            "Transformations": "https://payhip.com/b/U2nMw",
270            "Vectors in Two Dimensions": "https://payhip.com/b/oKRjO",
271            "Magnitude of a Vector": "https://payhip.com/b/LbBqE",
272            "Vector Geometry": "https://payhip.com/b/wzbn9",
273            "An Introduction to Probability": "https://payhip.com/b/kMVWL",
274            "Relative and Expected Frequencies": "https://payhip.com/b/ug9KE",
275            "Probability of Combined Events": "https://payhip.com/b/3AZnV",
276            "Conditional Probability": "https://payhip.com/b/XIALH",
277            "Data Handling and Interpretation": "https://payhip.com/b/Rwbot",
278            "Averages and Measures of Spread (Ungrouped Data)": "https://payhip.com/b/Q8WfX",
279            "Averages and Measures of Spread (Grouped Data)": "https://payhip.com/b/reAKG",
280            "Statistical Charts and Diagrams": "https://payhip.com/b/xuigq",
281            "Scatter Diagrams": "https://payhip.com/b/i5O9W",
282            "Cumulative Frequency Diagrams": "https://payhip.com/b/NhqEx",
283            "Histograms": "https://payhip.com/b/4AZYG"
284        }
285    },
286
287    // DEFINING THE REVISION GROUPS (Funnel Logic)
288    getMenuMapping() {
289        if (this.state.tier === 'gcse') {
290            return [
291                { id: "number", label: "Number", areas: ["Number", "Ratio, Proportion & Rates of Change"], link: "https://payhip.com/b/IVWzJ", btnText: "Master GCSE Number: Download the Pack" },
292                { id: "algebra", label: "Algebra", areas: ["Algebra"], link: "https://payhip.com/b/wRN86", btnText: "Master GCSE Algebra: Download the Pack" },
293                { id: "geometry", label: "Geometry", areas: ["Geometry & Measures", "Coordinate Geometry & Geometry"], link: "https://payhip.com/b/XAGch", btnText: "Master GCSE Geometry: Download the Pack" },
294                { id: "stats", label: "Stats & Prob", areas: ["Statistics", "Probability"], link: "https://payhip.com/b/RVbqM", btnText: "Master GCSE Stats & Prob: Download the Pack" }
295            ];
296        } else {
297            // IGCSE Extended 0580 Mapping
298            return [
299                { id: "num", label: "Number & Ratio", areas: ["Number", "Ratio, Proportion & Rates of Change"], link: "https://payhip.com/b/XEV2Z", btnText: "Master IGCSE Number: Download the Pack" },
300                { id: "alg", label: "Algebra", areas: ["Algebra", "Differentiation"], link: "https://payhip.com/b/mg5YS", btnText: "Master IGCSE Algebra: Download the Pack" },
301                { id: "geom", label: "Coordinate Geometry & Geometry", areas: ["Geometry & Measures", "Coordinate Geometry & Geometry", "Angles", "Circle Theorems", "Transformations"], link: "https://payhip.com/b/L6skH", btnText: "Master IGCSE Geometry: Download the Pack" },
302                { id: "trig", label: "Mensuration & Trigonometry", areas: ["Mensuration", "Trigonometry", "3D Shapes", "SOHCAHTOA", "Sine rule", "Cosine rule"], link: "https://payhip.com/b/KjXoP", btnText: "Master IGCSE Mensuration & Trig: Download the Pack" },
303                { id: "vec", label: "Vectors, Stats & Probability", areas: ["Vectors", "Statistics", "Probability"], link: "https://payhip.com/b/rsVCz", btnText: "Master IGCSE Vectors & Stats: Download the Pack" }
304            ];
305        }
306    },
307
308    async init(mode, tier) {
309        this.state.isTeacherMode = (mode === 'audit');
310        this.state.tier = tier || this.state.tier;
311        localStorage.setItem('bradley_tier', this.state.tier);
312
313        if (this.state.isTeacherMode) {
314            const picker = document.getElementById('month-picker');
315            await this.loadMonthData(picker ? picker.value : '2026-04');
316            this.renderAuditList(); 
317        } else {
318            await this.loadAllActiveMonths();
319            this.renderMenu();
320        }
321    },
322
323    // --- DATA LOADING ---
324    async loadAllActiveMonths() {
325        this.state.masterVault = [];
326        for (const period of this.state.activeMonths) {
327            const [year, mm] = period.split('-');
328            await this.fetchFile(`problems/${this.state.tier}/${year}/${mm}.js`);
329        }
330    },
331
332    async loadMonthData(period) {
333        this.state.masterVault = [];
334        const [year, mm] = period.split('-');
335        await this.fetchFile(`problems/${this.state.tier}/${year}/${mm}.js`);
336    },
337
338    async fetchFile(path) {
339        try {
340            const response = await fetch(path + '?v=' + new Date().getTime());
341            if (!response.ok) return;
342            const text = await response.text();
343            const arrayMatch = text.match(/\[[\s\S]*\]/);
344            if (arrayMatch) {
345                const monthData = eval(arrayMatch[0]);
346                this.state.masterVault = [...this.state.masterVault, ...monthData];
347            }
348        } catch (e) { console.error("Filing error:", path); }
349    },
350
351    // --- VIEW B: THE STUDENT HUB (GAMIFIED) ---
352    renderMenu() {
353        const mapping = this.getMenuMapping();
354        const score = this.state.correctIds.filter(id => (this.state.tier === 'gcse' ? id.startsWith('002') : id.startsWith('003'))).length;
355
356        let rank = "Novice";
357        let nextTarget = 5;
358        if (score >= 50) { rank = "Grade 9 Elite"; nextTarget = "MAX"; }
359        else if (score >= 30) { rank = "Grade 7-8 Master"; nextTarget = 50; }
360        else if (score >= 15) { rank = "Grade 6 Scholar"; nextTarget = 30; }
361        else if (score >= 5) { rank = "Grade 5 Challenger"; nextTarget = 15; }
362
363        let progressHTML = nextTarget === "MAX" 
364            ? `<div style="font-size: 0.9rem; color: #065f46;">Highest Rank Achieved!</div>`
365            : `<div style="font-size: 0.85rem; color: #555;">Next rank at ${nextTarget} correct (Currently ${score})</div>
366               <div style="width: 100%; background: #ddd; height: 10px; border-radius: 5px; margin-top: 5px;">
367                   <div style="width: ${(score/nextTarget)*100}%; background: var(--brand-purple); height: 100%; border-radius: 5px; transition: width 0.5s;"></div>
368               </div>`;
369        let premiumStatus = this.state.isPremium 
370            ? `<div style="margin-top: 15px; padding-top: 15px; border-top: 1px solid rgba(0,0,0,0.1); font-weight: bold; color: #059669;">🔓 Premium Access Unlocked</div>`
371            : `<div style="margin-top: 15px; padding-top: 15px; border-top: 1px solid rgba(0,0,0,0.1); font-weight: bold; color: #d97706;">⏳ Free Trial: ${this.state.freeQuestionsLeft}
371 Questions Remaining</div>`;
372
373        const today = new Date();
374        const p1 = new Date("2026-05-14");
375        const p2 = new Date("2026-06-03");
376        const p3 = new Date("2026-06-10");
377        
378        let countdownText = "";
379        let daysToP1 = Math.ceil((p1 - today) / (1000 * 60 * 60 * 24));
380        let daysToP2 = Math.ceil((p2 - today) / (1000 * 60 * 60 * 24));
381        let daysToP3 = Math.ceil((p3 - today) / (1000 * 60 * 60 * 24));
382
383        if (daysToP1 >= 0) countdownText = `🚨 <strong>${daysToP1} Days</strong> until Paper 1!`;
384        else if (daysToP2 >= 0) countdownText = `🚨 <strong>${daysToP2} Days</strong> until Paper 2!`;
385        else if (daysToP3 >= 0) countdownText = `🚨 <strong>${daysToP3} Days</strong> until Paper 3!`;
386        else countdownText = `GCSE Exams are finished! Great work!`;
387
388        document.getElementById('hub-stage').innerHTML = `
389            <div class="info-panel" style="background: white; border: 1px solid #ddd; text-align: center; box-shadow: 0 4px 6px rgba(0,0,0,0.05);">
390                <div style="background: #fee2e2; border: 1px solid #f87171; color: #991b1b; padding: 12px; border-radius: 8px; font-size: 1.2rem; margin-bottom: 20px;">
391                    ${this.state.tier === 'gcse' ? countdownText : 'IGCSE Exams Complete!'}
392                </div>
393                <div class="hub-stats" style="background: var(--brand-green-light); padding: 20px; border-radius: 8px; margin-bottom: 25px; border: 2px solid var(--brand-green);">
394                    <h3 style="margin: 0 0 10px 0; color: var(--brand-purple-dark);">Your Revision Rank: ${rank}</h3>
395                    ${progressHTML}
396                    ${premiumStatus}
397                </div>
398                <h3 style="color: var(--brand-purple);">Step 1: Choose an Area</h3>
399                <div class="menu-grid">
400                    ${mapping.map(m => `<button class="menu-btn" onclick="BradleyHub.showDifficultyMenu('${m.id}')">${m.label}</button>`).join('')}
401                    <button class="menu-btn random" style="background: linear-gradient(135deg, var(--brand-purple), var(--brand-purple-dark)); color: white; border: none; font-weight: bold; box-shadow: 0 4px 10px rgba(0,0,0,0.15);" onclick="BradleyHub.showDifficultyMenu('all')">★ Random Exam Mix ★</button>
402                </div>
403                <p style="margin-top:30px;"><a href="index.html" style="color: var(--text-muted); font-size: 0.85rem; text-decoration: underline;">Return to Homepage</a></p>
404            </div>`;
405    },
406
407    showDifficultyMenu(groupId) {
408        let diffButtons = '';
409        if (this.state.tier === 'gcse') {
410            diffButtons = `
411                <button class="menu-btn" onclick="BradleyHub.serveArena('${groupId}', '35')">Grades 3 - 5</button>
412                <button class="menu-btn" onclick="BradleyHub.serveArena('${groupId}', '46')">Grades 4 - 6</button>
413                <button class="menu-btn" onclick="BradleyHub.serveArena('${groupId}', '57')">Grades 5 - 7</button>
414                <button class="menu-btn" onclick="BradleyHub.serveArena('${groupId}', '68')">Grades 6 - 8</button>
415                <button class="menu-btn" onclick="BradleyHub.serveArena('${groupId}', '79')">Grades 7 - 9</button>
416            `;
417        } else {
418            diffButtons = `
419                <button class="menu-btn" onclick="BradleyHub.serveArena('${groupId}', 'EC')">Grades E - C</button>
420                <button class="menu-btn" onclick="BradleyHub.serveArena('${groupId}', 'DB')">Grades D - B</button>
421                <button class="menu-btn" onclick="BradleyHub.serveArena('${groupId}', 'CA')">Grades C - A</button>
422                <button class="menu-btn" onclick="BradleyHub.serveArena('${groupId}', 'BA*')">Grades B - A*</button>
423            `;
424        }
425        
426        document.getElementById('hub-stage').innerHTML = `
427            <div class="info-panel" style="background: white; border: 1px solid #ddd; text-align: center; box-shadow: 0 4px 6px rgba(0,0,0,0.05);">
428                <h3 style="color: var(--brand-purple);">Step 2: Select Target Difficulty</h3>
429                <p>We will tailor the questions to match your target grade.</p>
430                <div class="menu-grid">
431                    <button class="menu-btn random" style="background: linear-gradient(135deg, var(--brand-green), #047857); color: white;" onclick="BradleyHub.serveArena('${groupId}', 'all')">★ Any Difficulty ★</button>
432                    ${diffButtons}
433                </div>
434                <p style="margin-top:30px;">
435                    <button class="btn" style="background: var(--text-muted); color: white !important;" onclick="BradleyHub.renderMenu()">← Back to Topics</button>
436                </p>
437            </div>
438        `;
439    },
440
441    checkPremiumAccess() {
442        const unlocked = localStorage.getItem('app_unlocked') === 'true';
443        if (!unlocked) {
444            window.location.href = "unlock.html";
445            return false;
446        }
447        this.state.isPremium = true;
448        return true;
449    },
450
451    serveArena(groupId, targetDifficulty = 'all') {
452        this.state.currentGroup = groupId;
453        if (!this.state.isPremium && this.state.freeQuestionsLeft <= 0) {
454            this.renderPaywall();
455            return;
456        }
457        const today = new Date();
458        today.setHours(0,0,0,0);
459        
460        const mapping = this.getMenuMapping();
461        const group = mapping.find(m => m.id === groupId);
462
463        let pool = this.state.masterVault.filter(p => {
464            const unseen = !this.state.correctIds.includes(p.id);
465            const isPast = new Date(p.date) <= today;
466            
467            let diffMatch = true;
468            if (targetDifficulty !== 'all' && p.difficulty) {
469                let grades = p.difficulty.replace(/[^A-Za-z0-9*]/g, ' ').split(/\s+/).filter(Boolean);
470                
471                const targetMap = {
472                    '35': ['3', '4', '5'], '46': ['4', '5', '6'], '57': ['5', '6', '7'], '68': ['6', '7', '8'], '79': ['7', '8', '9'],
473                    'EC': ['E', 'D', 'C'], 'DB': ['D', 'C', 'B'], 'CA': ['C', 'B', 'A'], 'BA*': ['B', 'A', 'A*']
474                };
475                
476                const validGrades = targetMap[targetDifficulty] || [];
477                diffMatch = grades.some(g => validGrades.includes(g));
478            }
479
480            if (groupId === 'all') return unseen && isPast && diffMatch;
481            
482            const areaMatch = group.areas.includes(p.major_area) || group.areas.includes(p.topic);
483            return areaMatch && unseen && isPast && diffMatch;
484        });
485
486        if (pool.length === 0) {
487            document.getElementById('hub-stage').innerHTML = `
488                <div class="info-panel" style="text-align: center;">
489                    <h2>Grade Range Mastered!</h2>
490                    <p>You have successfully answered all available questions for this specific topic and grade level!</p>
491                    <button class="btn btn-purple" style="margin-top: 15px;" onclick="BradleyHub.showDifficultyMenu('${groupId}')">Try Another Grade</button>
492                    <br><br>
493                    <button class="btn" style="background: var(--text-muted); color: white !important;" onclick="BradleyHub.renderMenu()">Return to Main Menu</button>
494                </div>`;
495            return;
496        }
497
498        const prob = pool[Math.floor(Math.random() * pool.length)];
499        
500        const tierDirectory = this.worksheetDirectory[this.state.tier] || {};
501        const specificLink = tierDirectory[prob.subtopic];
502        
503        let finalLink = specificLink ? specificLink : (groupId === 'all' ? prob.payhip_link : group.link);
504        let finalText = specificLink ? `Need practice? Get the ${prob.subtopic} Worksheet` : (groupId === 'all' ? prob.button_text : group.btnText);
505
506        if (!finalLink || finalLink === "undefined") finalLink = "https://bradleysmaths.co.uk";
507        if (!finalText || finalText === "undefined") finalText = "Get the Complete Revision Pack";
508
509        const stage = document.getElementById('hub-stage');
510        stage.innerHTML = '';
511        stage.appendChild(this.createInteractiveCard(prob, finalLink, finalText));
512        if (window.MathJax) MathJax.typesetPromise();
513    },
514
515    // --- THE UNIVERSAL CARD GENERATOR ---
516    createProblemCard(prob, isAudit, forcedLink, forcedText) {
517        const card = document.createElement('div');
518        card.className = 'daily-widget';
519        if (isAudit) card.style.borderLeft = "8px solid #d9534f";
520
521        let imgHTML = '';
522        if (prob.img === "true") {
523            const d = new Date(prob.date);
524            const yyyy = d.getFullYear(); 
525            const mm = String(d.getMonth() + 1).padStart(2, '0'); 
526            const folderName = `${yyyy}-${mm}`; 
527            const dd = String(d.getDate()).padStart(2, '0');
528            const t = this.state.tier === 'gcse' ? 'g' : 'i';
529            imgHTML = `<img src="images/${folderName}/${t}_${dd}.png" class="question-img" style="margin: 20px auto; display: block;">`;
530        }
531
532        const link = forcedLink || prob.payhip_link;
533        const bText = forcedText || prob.button_text;
534
535        card.innerHTML = `
536            <span class="widget-header">${prob.date.toUpperCase()} | ${prob.topic} | Grade ${prob.difficulty}</span>
537            <div class="question-box">${prob.q}</div>
538            ${imgHTML}
539            <div id="action-area-${prob.id}"><button class="reveal-btn" onclick="BradleyHub.revealSolution('${prob.id}', ${isAudit})">Show Model Answer</button></div>
540            <div id="sol-${prob.id}" class="step-container" style="display:none;">
541                <h3 style="text-align:left; color: var(--brand-purple);">Model Solution</h3>
542                ${prob.steps.map(s => `<div class="step" style="display:block;"><span class="step-text">Step</span>${s}</div>`).join('')}
543                <div class="bradley-insight-box insight-${prob.bradley_insight.type}">
544                    <span class="insight-title">The Head Teacher's Eye</span>
545                    ${prob.bradley_insight.content}
546                </div>
547                ${!isAudit ? `<div style="display:flex; gap:10px; margin-top:20px;"><button class="btn btn-purple" style="flex:1;" onclick="BradleyHub.serveArena('${this.state.currentGroup}')">Next Question</button><button class="btn" style="flex:1; background: var(--text-muted); color: white !important;" onclick="BradleyHub.renderMenu()">Change Area</button></div>` : ''}
548                <a href="${link}" target="_blank" class="btn-buy" style="display:block; text-align:center; margin-top:20px; background: var(--brand-green); color: white !important;">${bText}</a>
549            </div>`;
550        return card;
551    },
552
553    extractFinalAnswer(finalStep) {
554        if (!finalStep) return "";
555        let answerPart = finalStep.split("Final Answer:")[1];
556        if (!answerPart) return "";
557        answerPart = answerPart.replace(/<[^>]*>/g, "");
558        answerPart = answerPart.replace(/\s+/g, " ").trim();
559        return answerPart;
560    },
561
562    generateDistractors(correct, prob) {
563        const distractors = [];
564        if (correct.includes("<") || correct.includes(">") || correct.includes("\\le") || correct.includes("\\ge")) {
565            let d1 = correct.replace(/(<|>|\\le|\\ge)/g, match => {
566                if (match === '<') return '>';
567                if (match === '>') return '<';
568                if (match === '\\le') return '\\ge';
569                if (match === '\\ge') return '\\le';
570                return match;
571            });
572            let d2 = correct.replace(/<|\\le/g, "="); 
573            let d3 = correct.replace(/-?\d+/g, m => (parseInt(m) + 1).toString()); 
574            return [...new Set([d1, d2, d3])];
575        }
576
577        const numRegex = /-?\d+(\.\d+)?/g;
578        const matches = correct.match(numRegex);
579        if (!matches) {
580            return ["Not enough information", "The opposite of the correct answer", "None of the parts are correct"];
581        }
582
583        const tweakNumber = (valStr, strategy) => {
584            let num = parseFloat(valStr);
585            let isDecimal = valStr.includes('.');
586            let decPlaces = isDecimal ? valStr.split('.')[1].length : 0;
587            
588            let newVal;
589            if (strategy === 'high') newVal = num > 20 ? num * 1.1 : num + (isDecimal ? 0.5 : 2);
590            else if (strategy === 'low') {
591                newVal = num > 20 ? num * 0.9 : num - (isDecimal ? 0.5 : 2);
592                if (newVal < 0 && num > 0) newVal = num / 2;
593            } 
594            else if (strategy === 'slip') newVal = num + (isDecimal ? 1.5 : 10);
595
596            return isDecimal ? newVal.toFixed(decPlaces) : Math.round(newVal).toString();
597        };
598
599        let d1 = correct.replace(numRegex, m => tweakNumber(m, 'high'));
600        let d2 = correct.replace(numRegex, m => tweakNumber(m, 'low'));
601        let d3 = correct.replace(numRegex, m => tweakNumber(m, 'slip'));
602
603        if (d1 === correct) d1 += " (approx)";
604        if (d2 === correct || d2 === d1) d2 = "None of the parts match";
605        if (d3 === correct || d3 === d2) d3 = "Cannot be determined";
606
607        distractors.push(d1, d2, d3);
608        return [...new Set(distractors)];
609    },
610
611    checkAnswer(probId, isCorrect, feedbackHTML) {
612        const feedbackBox = document.getElementById(`feedback-${probId}`);
613        const optionsBox = document.getElementById(`options-${probId}`);
614        if (!feedbackBox) return;
615
616        feedbackBox.style.display = "block";
617        let justLeveledUp = false;
618        let rankName = "";
619        
620        const getScore = () => this.state.correctIds.filter(id => (this.state.tier === 'gcse' ? id.startsWith('002') : id.startsWith('003'))).length;
621
622        if (isCorrect) {
623            if (!this.state.correctIds.includes(probId)) {
624                this.state.correctIds.push(probId);
625                localStorage.setItem('bradley_correct_ids', JSON.stringify(this.state.correctIds));
626                
627                let newScore = getScore();
628                if (newScore === 5) { justLeveledUp = true; rankName = "Grade 5 Challenger"; }
629                else if (newScore === 15) { justLeveledUp = true; rankName = "Grade 6 Scholar"; }
630                else if (newScore === 30) { justLeveledUp = true; rankName = "Grade 7-8 Master"; }
631                else if (newScore === 50) { justLeveledUp = true; rankName = "Grade 9 Elite"; }
632            }
633
634            let currentScore = getScore();
635            let extraMessage = `<br><div style="margin-top: 10px; font-size: 0.95rem; color: #047857;">⭐ <strong>Current Score:</strong> ${currentScore} correct answers!</div>`;
636            if (justLeveledUp) {
637                extraMessage += `<div style="margin-top: 5px; font-size: 1.15rem; color: #059669; font-weight: bold; text-transform: uppercase;">🎉 LEVEL UP! You are now a ${rankName}! 🎉</div>`;
638            }
639
640            feedbackBox.style.background = "#d1fae5";
641            feedbackBox.style.color = "#065f46";
642            feedbackBox.style.border = "1px solid #34d399";
643            feedbackBox.innerHTML = feedbackHTML + extraMessage;
644        
645        } else {
646            let currentScore = getScore();
647            let extraMessage = `<br><div style="margin-top: 10px; font-size: 0.95rem; color: #991b1b;">⭐ <strong>Current Score:</strong> ${currentScore} correct answers. Keep going!</div>`;
648
649            feedbackBox.style.background = "#fee2e2";
650            feedbackBox.style.color = "#991b1b";
651            feedbackBox.style.border = "1px solid #f87171";
652            feedbackBox.innerHTML = feedbackHTML + extraMessage;
653        }
654
655        if (optionsBox) optionsBox.style.display = "none";
656        this.revealSolution(probId, false);
657    },
658
659    // --- INTERACTIVE CARD GENERATOR ---
660    createInteractiveCard(prob, link, btnText) {
661        const card = document.createElement('div');
662        card.className = 'daily-widget';
663
664        const finalStep = prob.steps[prob.steps.length - 1];
665        const correct = this.extractFinalAnswer(finalStep);
666
667        let optionsObj = [];
668        optionsObj.push({ 
669            text: correct, 
670            isCorrect: true, 
671            feedback: `<strong>Correct!</strong> Outstanding work. Here is the fully worked solution:` 
672        });
673
674        if (prob.wrong_options && prob.wrong_options.length > 0) {
675            prob.wrong_options.forEach(wo => {
676                optionsObj.push({ 
677                    text: wo.ans, 
678                    isCorrect: false, 
679                    feedback: `<strong>Not quite!</strong> ${wo.feedback}` 
680                });
681            });
682        } else {
683            const distractors = this.generateDistractors(correct, prob);
684            distractors.forEach(d => {
685                optionsObj.push({ 
686                    text: d, 
687                    isCorrect: false, 
688                    feedback: `<strong>Not quite.</strong> Don't worry, review the model answer below to see where you went wrong!` 
689                });
690            });
691        }
692
693        for (let i = optionsObj.length - 1; i > 0; i--) {
694            const j = Math.floor(Math.random() * (i + 1));
695            [optionsObj[i], optionsObj[j]] = [optionsObj[j], optionsObj[i]];
696        }
697        
698        optionsObj.push({ 
699            text: "None of the above", 
700            isCorrect: false, 
701            feedback: `<strong>Not quite!</strong> It looks like you need some extra help with this topic. Perhaps you should consider purchasing the worksheet below for targeted help.` 
702        });
703
704        let imgHTML = '';
705        if (prob.img === "true") {
706            const d = new Date(prob.date);
707            const yyyy = d.getFullYear(); 
708            const mm = String(d.getMonth() + 1).padStart(2, '0');
709            const folderName = `${yyyy}-${mm}`; 
710            const dd = String(d.getDate()).padStart(2, '0');
711            const t = this.state.tier === 'gcse' ? 'g' : 'i';
712            imgHTML = `<img src="images/${folderName}/${t}_${dd}.png" class="question-img" style="margin: 20px auto; display: block;">`;
713        }
714
715        const tierDirectory = this.worksheetDirectory[this.state.tier] || {};
716        const specificLink = tierDirectory[prob.subtopic];
717        
718        let finalLink = specificLink ? specificLink : (this.state.currentGroup === 'all' ? prob.payhip_link : link);
719        let finalText = specificLink ? `Need practice? Get the ${prob.subtopic} Worksheet` : (this.state.currentGroup === 'all' ? prob.button_text : btnText);
720
721        if (!finalLink || finalLink === "undefined") finalLink = "https://bradleysmaths.co.uk";
722        if (!finalText || finalText === "undefined") finalText = "Get the Complete Revision Pack";
723
724        card.innerHTML = `
725            <span class="widget-header">${prob.date.toUpperCase()} | ${prob.topic} | Grade ${prob.difficulty}</span>
726            <div class="question-box">${prob.q}</div>
727            ${imgHTML}
728
729            <div id="action-area-${prob.id}" style="text-align: center; margin-top: 20px;">
730                <button class="reveal-btn" onclick="BradleyHub.revealOptions('${prob.id}')">I'm Ready: Reveal Answer Options</button>
731            </div>
732
733            <div id="options-${prob.id}" class="mcq-options" style="display:none; margin-top: 20px;">
734                <p style="margin-bottom: 15px; font-weight: bold; color: var(--brand-purple); text-align: center;">Select your answer:</p>
735                <div style="display: flex; flex-direction: column; gap: 8px; align-items: center;">
736                    ${optionsObj.map(opt => {
737                        const safeFeedback = opt.feedback.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/"/g, "&quot;");
738                        return `
739                        <button class="mcq-btn" style="width: 100%; max-width: 400px; padding: 12px; font-size: 1rem; border: 1px solid var(--brand-purple); border-radius: 6px; background: white; cursor: pointer;" 
740                                onclick="BradleyHub.checkAnswer('${prob.id}', ${opt.isCorrect}, '${safeFeedback}')">
741                            ${opt.text}
742                        </button>`
743                    }).join('')}
744                </div>
745            </div>
746
747            <div id="feedback-${prob.id}" class="feedback-box" style="display:none; margin-top: 15px; text-align: center; padding: 10px; border-radius: 5px;"></div>
748
749            <div id="sol-${prob.id}" class="step-container" style="display:none;">
750                <h3 style="text-align:left; color: var(--brand-purple);">Model Solution</h3>
751                ${prob.steps.map(s => `<div class="step" style="display:block !important; visibility:visible !important; opacity:1 !important; height:auto !important; margin-bottom:12px;"><span class="step-text" style="font-weight:bold; color:var(--brand-purple); margin-right:8px;">Step</span>${s}</div>`).join('')}
752
753                <div class="bradley-insight-box insight-${prob.bradley_insight.type}" style="margin-top: 20px;">
754                    <span class="insight-title">The Head Teacher's Eye</span>
755                    ${prob.bradley_insight.content}
756                </div>
757
758                <div style="display:flex; gap:10px; margin-top:20px;">
759                    <button class="btn btn-purple" style="flex:1;" onclick="BradleyHub.serveArena('${this.state.currentGroup}')">Next Question</button>
760                    <button class="btn" style="flex:1; background: var(--text-muted); color: white !important;" onclick="BradleyHub.renderMenu()">Change Area</button>
761                </div>
762
763                <a href="${finalLink}" target="_blank" class="btn-buy" style="display:block; text-align:center; margin-top:20px; background: var(--brand-green); color: white !important;">${finalText}</a>
764            </div>
765        `;
766        return card;
767    },
768
769    revealOptions(id) {
770        const opts = document.getElementById(`options-${id}`);
771        const act = document.getElementById(`action-area-${id}`);
772        if (opts) opts.style.display = 'block';
773        if (act) act.style.display = 'none';
774    },
775
776    revealSolution(id, isAudit) {
777        const sol = document.getElementById(`sol-${id}`);
778        const act = document.getElementById(`action-area-${id}`);
779        if (sol) sol.style.display = 'block';
780        if (act) act.style.display = 'none';
781        
782        if (!isAudit && !this.state.seenIds.includes(id)) {
783            this.state.seenIds.push(id);
784            localStorage.setItem('bradley_seen_ids', JSON.stringify(this.state.seenIds));
785            if (!this.state.isPremium) {
786                this.state.freeQuestionsLeft--;
787                localStorage.setItem('bradley_free_left', this.state.freeQuestionsLeft.toString());
788            }
789        }
790        if (window.MathJax) MathJax.typesetPromise();
791    },
792
793    renderAuditList() {
794        const container = document.getElementById('hub-container');
795        if (!container) return;
796        container.innerHTML = '';
797        this.state.masterVault.sort((a, b) => new Date(b.date) - new Date(a.date));
798        this.state.masterVault.forEach(prob => container.appendChild(this.createProblemCard(prob, true)));
799        if (window.MathJax) MathJax.typesetPromise();
800    },
801
802    loadSpecificMonth(mm) { 
803        const picker = document.getElementById('month-picker');
804        if (picker && picker.value !== mm) {
805            picker.value = mm;
806        }
807        this.init('audit', this.state.tier); 
808    },
809
810    switchAuditTier(newTier) {
811        this.state.tier = newTier;
812        localStorage.setItem('bradley_tier', newTier);
813        this.init('audit', newTier);
814    }
815   //this comment should force a change in github 
816};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.