dev-api-ui 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/README.md +36 -0
  2. package/package.json +40 -0
  3. package/ui/ThemeProvider.tsx +16 -0
  4. package/ui/components/ArticlePage.tsx +673 -0
  5. package/ui/components/AuthPage.tsx +109 -0
  6. package/ui/components/Callout.tsx +79 -0
  7. package/ui/components/Chart.tsx +100 -0
  8. package/ui/components/CodeTabs.tsx +145 -0
  9. package/ui/components/ComparePage.tsx +364 -0
  10. package/ui/components/DashboardPage.tsx +773 -0
  11. package/ui/components/DocsNav.tsx +80 -0
  12. package/ui/components/Footer.tsx +136 -0
  13. package/ui/components/HubPage.tsx +529 -0
  14. package/ui/components/Modal.tsx +412 -0
  15. package/ui/components/Nav.tsx +162 -0
  16. package/ui/components/OnThisPage.tsx +56 -0
  17. package/ui/components/ParamsTable.tsx +86 -0
  18. package/ui/components/RangeBar.tsx +68 -0
  19. package/ui/components/SeriesChart.tsx +218 -0
  20. package/ui/components/SeriesPage.tsx +461 -0
  21. package/ui/components/StatusMark.tsx +48 -0
  22. package/ui/components/publictrades/ExternalLink.tsx +37 -0
  23. package/ui/components/publictrades/FactsCard.tsx +40 -0
  24. package/ui/components/publictrades/LedgerFooter.tsx +22 -0
  25. package/ui/components/publictrades/LedgerNav.tsx +78 -0
  26. package/ui/components/publictrades/Mark.tsx +40 -0
  27. package/ui/components/publictrades/SourceBlock.tsx +91 -0
  28. package/ui/components/publictrades/Tape.tsx +164 -0
  29. package/ui/components/publictrades/ThreeStamps.tsx +47 -0
  30. package/ui/components/publictrades/types.ts +19 -0
  31. package/ui/data/series.ts +851 -0
  32. package/ui/index.ts +50 -0
  33. package/ui/publictrades.ts +16 -0
  34. package/ui/themes.css +81 -0
@@ -0,0 +1,673 @@
1
+ 'use client';
2
+
3
+ import { ReactNode } from 'react';
4
+
5
+ // ─── Types ────────────────────────────────────────────────────────────────────
6
+
7
+ export type ArticleKind =
8
+ | 'Guide'
9
+ | 'Blog'
10
+ | 'Methodology'
11
+ | 'Learn'
12
+ | 'Report'
13
+ | 'FAQ'
14
+ | 'Glossary'
15
+ | 'Coverage';
16
+
17
+ interface RelatedCard {
18
+ type: string;
19
+ title: string;
20
+ href?: string;
21
+ }
22
+
23
+ interface CompareRow {
24
+ q: string;
25
+ naive: string;
26
+ pit: string;
27
+ }
28
+
29
+ interface AuthorCard {
30
+ initials: string;
31
+ name: string;
32
+ bio: string;
33
+ }
34
+
35
+ export interface ArticlePageProps {
36
+ kind: ArticleKind;
37
+ breadcrumb: string;
38
+ title: string;
39
+ lede: string;
40
+ date: string;
41
+ readTime: string;
42
+ author: AuthorCard;
43
+ keyPoints: string[];
44
+ compareRows: CompareRow[];
45
+ related: RelatedCard[];
46
+ brand?: string;
47
+ apiUrl?: string;
48
+ }
49
+
50
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
51
+
52
+ function InlineCode({ children }: { children: string }) {
53
+ return (
54
+ <code
55
+ style={{
56
+ fontFamily: 'var(--mono)',
57
+ fontSize: 14,
58
+ background: '#F2F2EF',
59
+ border: '0.5px solid var(--hairline-2)',
60
+ borderRadius: 3,
61
+ padding: '1px 5px',
62
+ }}
63
+ >
64
+ {children}
65
+ </code>
66
+ );
67
+ }
68
+
69
+ function CheckIcon(): ReactNode {
70
+ return (
71
+ <svg
72
+ viewBox="0 0 24 24"
73
+ width="15"
74
+ height="15"
75
+ fill="none"
76
+ stroke="currentColor"
77
+ strokeWidth="2"
78
+ strokeLinecap="round"
79
+ strokeLinejoin="round"
80
+ aria-hidden="true"
81
+ >
82
+ <path d="M5 12l4 4 10-10" />
83
+ </svg>
84
+ );
85
+ }
86
+
87
+ function ArrowIcon(): ReactNode {
88
+ return (
89
+ <svg
90
+ viewBox="0 0 24 24"
91
+ width="13"
92
+ height="13"
93
+ fill="none"
94
+ stroke="currentColor"
95
+ strokeWidth="1.8"
96
+ strokeLinecap="round"
97
+ strokeLinejoin="round"
98
+ aria-hidden="true"
99
+ >
100
+ <path d="M5 12h14" />
101
+ <path d="M13 6l6 6-6 6" />
102
+ </svg>
103
+ );
104
+ }
105
+
106
+ // ─── Sub-sections ─────────────────────────────────────────────────────────────
107
+
108
+ function KeyPoints({ points }: { points: string[] }) {
109
+ return (
110
+ <section style={{ padding: '22px 0' }}>
111
+ <div
112
+ style={{
113
+ background: 'var(--surface)',
114
+ border: '0.5px solid var(--hairline-2)',
115
+ borderRadius: 8,
116
+ padding: '18px 22px',
117
+ }}
118
+ >
119
+ <div
120
+ style={{
121
+ fontSize: 11,
122
+ fontWeight: 600,
123
+ textTransform: 'uppercase',
124
+ letterSpacing: '.07em',
125
+ color: 'var(--tertiary)',
126
+ marginBottom: 12,
127
+ }}
128
+ >
129
+ Key points
130
+ </div>
131
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
132
+ {points.map((k, i) => (
133
+ <div
134
+ key={i}
135
+ style={{
136
+ display: 'flex',
137
+ alignItems: 'flex-start',
138
+ gap: 10,
139
+ fontSize: 15,
140
+ color: 'var(--secondary)',
141
+ }}
142
+ >
143
+ <span style={{ flex: 'none', color: 'var(--accent)', marginTop: 2 }}>
144
+ <CheckIcon />
145
+ </span>
146
+ <span>{k}</span>
147
+ </div>
148
+ ))}
149
+ </div>
150
+ </div>
151
+ </section>
152
+ );
153
+ }
154
+
155
+ function CompareTable({ rows }: { rows: CompareRow[] }) {
156
+ const cols = [
157
+ { key: 'q', label: 'Query' },
158
+ { key: 'naive', label: 'Naive store' },
159
+ { key: 'pit', label: 'Point-in-time' },
160
+ ];
161
+ return (
162
+ <div
163
+ style={{
164
+ background: 'var(--surface)',
165
+ border: '0.5px solid var(--hairline-2)',
166
+ borderRadius: 8,
167
+ overflow: 'hidden',
168
+ marginTop: 18,
169
+ }}
170
+ >
171
+ {/* Tinted header */}
172
+ <div
173
+ style={{
174
+ display: 'grid',
175
+ gridTemplateColumns: '1fr 1fr 1fr',
176
+ gap: 16,
177
+ padding: '10px 18px',
178
+ borderBottom: '0.5px solid var(--hairline-2)',
179
+ background: '#F4F4F1',
180
+ }}
181
+ >
182
+ {cols.map((c) => (
183
+ <span
184
+ key={c.key}
185
+ style={{
186
+ fontSize: 10,
187
+ fontWeight: 600,
188
+ textTransform: 'uppercase',
189
+ letterSpacing: '.07em',
190
+ color: 'var(--tertiary)',
191
+ }}
192
+ >
193
+ {c.label}
194
+ </span>
195
+ ))}
196
+ </div>
197
+ {/* Rows — horizontal hairlines, no zebra */}
198
+ {rows.map((r, i) => (
199
+ <div
200
+ key={i}
201
+ style={{
202
+ display: 'grid',
203
+ gridTemplateColumns: '1fr 1fr 1fr',
204
+ gap: 16,
205
+ padding: '12px 18px',
206
+ borderBottom: '0.5px solid var(--hairline)',
207
+ fontSize: 14,
208
+ }}
209
+ >
210
+ <span style={{ color: 'var(--secondary)' }}>{r.q}</span>
211
+ <span style={{ color: 'var(--down)' }}>{r.naive}</span>
212
+ <span style={{ color: 'var(--accent)' }}>{r.pit}</span>
213
+ </div>
214
+ ))}
215
+ </div>
216
+ );
217
+ }
218
+
219
+ function PullQuote({ children }: { children: string }) {
220
+ return (
221
+ <div
222
+ style={{
223
+ margin: '30px 0',
224
+ padding: '4px 0',
225
+ borderTop: '0.5px solid var(--hairline)',
226
+ borderBottom: '0.5px solid var(--hairline)',
227
+ }}
228
+ >
229
+ <p
230
+ style={{
231
+ fontSize: 22,
232
+ color: 'var(--ink)',
233
+ lineHeight: 1.5,
234
+ fontWeight: 500,
235
+ padding: '20px 0',
236
+ }}
237
+ >
238
+ {children}
239
+ </p>
240
+ </div>
241
+ );
242
+ }
243
+
244
+ function AuthorSection({ author }: { author: AuthorCard }) {
245
+ return (
246
+ <section
247
+ style={{
248
+ padding: '30px 0',
249
+ marginTop: 18,
250
+ borderTop: '0.5px solid var(--hairline)',
251
+ }}
252
+ >
253
+ <div style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
254
+ <span
255
+ style={{
256
+ width: 44,
257
+ height: 44,
258
+ borderRadius: '50%',
259
+ background: 'var(--ink)',
260
+ color: '#fff',
261
+ display: 'inline-flex',
262
+ alignItems: 'center',
263
+ justifyContent: 'center',
264
+ fontFamily: 'var(--mono)',
265
+ fontSize: 15,
266
+ fontWeight: 600,
267
+ flex: 'none',
268
+ }}
269
+ >
270
+ {author.initials}
271
+ </span>
272
+ <div>
273
+ <div style={{ fontSize: 15, fontWeight: 500 }}>{author.name}</div>
274
+ <div style={{ fontSize: 13, color: 'var(--tertiary)' }}>{author.bio}</div>
275
+ </div>
276
+ </div>
277
+ </section>
278
+ );
279
+ }
280
+
281
+ function RelatedCards({ cards }: { cards: RelatedCard[] }) {
282
+ return (
283
+ <section
284
+ style={{
285
+ maxWidth: 1100,
286
+ margin: '0 auto',
287
+ padding: '8px 32px 0',
288
+ }}
289
+ >
290
+ <div style={{ borderTop: '0.5px solid var(--hairline)', paddingTop: 30 }}>
291
+ <h2
292
+ style={{
293
+ fontFamily: 'var(--mono)',
294
+ fontSize: 20,
295
+ fontWeight: 600,
296
+ letterSpacing: '-.01em',
297
+ marginBottom: 16,
298
+ }}
299
+ >
300
+ Keep reading
301
+ </h2>
302
+ <div
303
+ style={{
304
+ display: 'grid',
305
+ gridTemplateColumns: 'repeat(auto-fit, minmax(240px, 1fr))',
306
+ gap: 14,
307
+ }}
308
+ >
309
+ {cards.map((a, i) => (
310
+ <a
311
+ key={i}
312
+ href={a.href ?? '#'}
313
+ style={{
314
+ display: 'flex',
315
+ flexDirection: 'column',
316
+ gap: 8,
317
+ background: 'var(--surface)',
318
+ border: '0.5px solid var(--hairline-2)',
319
+ borderRadius: 8,
320
+ padding: '18px 20px',
321
+ color: 'inherit',
322
+ textDecoration: 'none',
323
+ minHeight: 112,
324
+ }}
325
+ >
326
+ <span
327
+ style={{
328
+ fontSize: 11,
329
+ fontWeight: 600,
330
+ textTransform: 'uppercase',
331
+ letterSpacing: '.07em',
332
+ color: 'var(--accent)',
333
+ }}
334
+ >
335
+ {a.type}
336
+ </span>
337
+ <span
338
+ style={{ fontSize: 15, fontWeight: 500, color: 'var(--ink)', lineHeight: 1.4 }}
339
+ >
340
+ {a.title}
341
+ </span>
342
+ <span
343
+ style={{
344
+ marginTop: 'auto',
345
+ display: 'inline-flex',
346
+ alignItems: 'center',
347
+ gap: 6,
348
+ fontSize: 13,
349
+ fontWeight: 500,
350
+ color: 'var(--accent)',
351
+ }}
352
+ >
353
+ Read <ArrowIcon />
354
+ </span>
355
+ </a>
356
+ ))}
357
+ </div>
358
+ </div>
359
+ </section>
360
+ );
361
+ }
362
+
363
+ // ─── Main component ───────────────────────────────────────────────────────────
364
+
365
+ export default function ArticlePage({
366
+ kind,
367
+ breadcrumb,
368
+ title,
369
+ lede,
370
+ date,
371
+ readTime,
372
+ author,
373
+ keyPoints,
374
+ compareRows,
375
+ related,
376
+ brand = 'Console',
377
+ apiUrl = 'https://api.console.dev',
378
+ }: ArticlePageProps) {
379
+ return (
380
+ <>
381
+ {/* Reading column */}
382
+ <article style={{ maxWidth: 740, margin: '0 auto', padding: '0 28px' }}>
383
+
384
+ {/* Header */}
385
+ <header style={{ padding: '34px 0 26px', borderBottom: '0.5px solid var(--hairline)' }}>
386
+ {/* Breadcrumb */}
387
+ <div
388
+ style={{
389
+ display: 'flex',
390
+ alignItems: 'center',
391
+ gap: 8,
392
+ fontSize: 13,
393
+ color: 'var(--tertiary)',
394
+ }}
395
+ >
396
+ <a href="#" style={{ color: 'var(--secondary)', textDecoration: 'none' }}>
397
+ {kind}
398
+ </a>
399
+ <span>/</span>
400
+ <span style={{ color: 'var(--ink)' }}>{breadcrumb}</span>
401
+ </div>
402
+
403
+ {/* Eyebrow */}
404
+ <div
405
+ style={{
406
+ display: 'inline-flex',
407
+ alignItems: 'center',
408
+ gap: 8,
409
+ fontSize: 12,
410
+ fontWeight: 600,
411
+ letterSpacing: '.04em',
412
+ textTransform: 'uppercase',
413
+ color: 'var(--accent)',
414
+ marginTop: 18,
415
+ }}
416
+ >
417
+ <span
418
+ style={{
419
+ width: 6,
420
+ height: 6,
421
+ borderRadius: '50%',
422
+ background: 'var(--accent)',
423
+ display: 'inline-block',
424
+ }}
425
+ />
426
+ {kind}
427
+ </div>
428
+
429
+ {/* H1 */}
430
+ <h1
431
+ style={{
432
+ fontFamily: 'var(--mono)',
433
+ fontSize: 34,
434
+ fontWeight: 600,
435
+ letterSpacing: '-.02em',
436
+ marginTop: 12,
437
+ lineHeight: 1.14,
438
+ }}
439
+ >
440
+ {title}
441
+ </h1>
442
+
443
+ {/* Lede */}
444
+ <p
445
+ style={{
446
+ fontSize: 19,
447
+ color: 'var(--secondary)',
448
+ lineHeight: 1.55,
449
+ marginTop: 14,
450
+ }}
451
+ >
452
+ {lede}
453
+ </p>
454
+
455
+ {/* Byline */}
456
+ <div
457
+ style={{
458
+ display: 'flex',
459
+ alignItems: 'center',
460
+ gap: 14,
461
+ flexWrap: 'wrap',
462
+ marginTop: 18,
463
+ fontSize: 13,
464
+ color: 'var(--tertiary)',
465
+ }}
466
+ >
467
+ <span style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
468
+ <span
469
+ style={{
470
+ width: 24,
471
+ height: 24,
472
+ borderRadius: '50%',
473
+ background: 'var(--ink)',
474
+ color: '#fff',
475
+ display: 'inline-flex',
476
+ alignItems: 'center',
477
+ justifyContent: 'center',
478
+ fontFamily: 'var(--mono)',
479
+ fontSize: 11,
480
+ fontWeight: 600,
481
+ }}
482
+ >
483
+ {author.initials}
484
+ </span>
485
+ <span style={{ color: 'var(--ink)', fontWeight: 500 }}>{author.name}</span>
486
+ </span>
487
+ <span>·</span>
488
+ <span>{date}</span>
489
+ <span>·</span>
490
+ <span>{readTime}</span>
491
+ </div>
492
+ </header>
493
+
494
+ {/* Lede paragraph (answer-first) */}
495
+ <section style={{ padding: '26px 0 4px' }}>
496
+ <p style={{ fontSize: 18, color: 'var(--ink)', lineHeight: 1.65 }}>
497
+ <b style={{ fontWeight: 600 }}>
498
+ Point-in-time data returns the value that was actually published as of a given
499
+ timestamp
500
+ </b>
501
+ , with no later revisions folded in. It is the difference between a backtest you can
502
+ trust and one quietly contaminated by information the market did not yet have.
503
+ </p>
504
+ </section>
505
+
506
+ {/* Key points box */}
507
+ <KeyPoints points={keyPoints} />
508
+
509
+ {/* Body sections */}
510
+ <section style={{ padding: '8px 0 0' }}>
511
+
512
+ <h2
513
+ id="what-it-means"
514
+ style={{
515
+ fontFamily: 'var(--mono)',
516
+ fontSize: 22,
517
+ fontWeight: 600,
518
+ letterSpacing: '-.01em',
519
+ margin: '30px 0 4px',
520
+ scrollMarginTop: 70,
521
+ }}
522
+ >
523
+ What &quot;point-in-time&quot; means
524
+ </h2>
525
+ <p
526
+ style={{ fontSize: 17, color: '#3A3D44', lineHeight: 1.7, marginTop: 16 }}
527
+ >
528
+ Most data providers serve you the <em>current best estimate</em> of a historical value.
529
+ When a figure is revised — a GDP print restated, a settlement corrected — the old number
530
+ silently disappears. Query last March today and you get March as it is understood now,
531
+ not as it was understood in March.
532
+ </p>
533
+ <p
534
+ style={{ fontSize: 17, color: '#3A3D44', lineHeight: 1.7, marginTop: 16 }}
535
+ >
536
+ A point-in-time store keeps every vintage. Ask for the value{' '}
537
+ <InlineCode>as_of=2026-03-14</InlineCode> and you receive exactly what a subscriber
538
+ would have seen on that date, revisions excluded.
539
+ </p>
540
+
541
+ <h2
542
+ id="look-ahead"
543
+ style={{
544
+ fontFamily: 'var(--mono)',
545
+ fontSize: 22,
546
+ fontWeight: 600,
547
+ letterSpacing: '-.01em',
548
+ margin: '30px 0 4px',
549
+ scrollMarginTop: 70,
550
+ }}
551
+ >
552
+ Why look-ahead bias ruins backtests
553
+ </h2>
554
+ <p
555
+ style={{ fontSize: 17, color: '#3A3D44', lineHeight: 1.7, marginTop: 16 }}
556
+ >
557
+ If your historical series contains values that were only knowable later, your strategy is
558
+ implicitly trading on the future. Returns look spectacular in simulation and collapse in
559
+ production. The bias is subtle precisely because the numbers are <em>real</em> — just not
560
+ real <em>yet</em>.
561
+ </p>
562
+
563
+ {/* Warn callout — local inline (fs=15, margin 22px 0 per design; shared Callout default left at 14) */}
564
+ <div style={{
565
+ display: 'flex', alignItems: 'flex-start', gap: 12,
566
+ margin: '22px 0',
567
+ background: '#F4F4F1', border: '0.5px solid var(--hairline-2)',
568
+ borderRadius: 8, padding: '16px 18px',
569
+ }}>
570
+ <span style={{ flex: 'none', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: 20, height: 20, color: 'var(--warn)', marginTop: 1 }}>
571
+ <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
572
+ <path d="M12 9v4" /><path d="M12 17h.01" />
573
+ <path d="M10.3 4.3 2.5 18a2 2 0 0 0 1.7 3h15.6a2 2 0 0 0 1.7-3L13.7 4.3a2 2 0 0 0-3.4 0z" />
574
+ </svg>
575
+ </span>
576
+ <div>
577
+ <div style={{ fontSize: 15, fontWeight: 500, color: 'var(--ink)' }}>The tell</div>
578
+ <div style={{ fontSize: 15, color: 'var(--secondary)', marginTop: 3, lineHeight: 1.55 }}>
579
+ A backtest that beats live trading by a wide, consistent margin almost always has a
580
+ look-ahead leak. Point-in-time data closes the most common one.
581
+ </div>
582
+ </div>
583
+ </div>
584
+
585
+ <h2
586
+ id="how-console-stamps"
587
+ style={{
588
+ fontFamily: 'var(--mono)',
589
+ fontSize: 22,
590
+ fontWeight: 600,
591
+ letterSpacing: '-.01em',
592
+ margin: '30px 0 4px',
593
+ scrollMarginTop: 70,
594
+ }}
595
+ >
596
+ How {brand} stamps every value
597
+ </h2>
598
+ <p
599
+ style={{ fontSize: 17, color: '#3A3D44', lineHeight: 1.7, marginTop: 16 }}
600
+ >
601
+ Every observation we store carries the timestamp it became public. Add an{' '}
602
+ <InlineCode>as_of</InlineCode> parameter and the API replays the series as it stood on
603
+ that date — no special endpoint, same envelope.
604
+ </p>
605
+
606
+ {/* Dark code block — static single-pane */}
607
+ <div style={{ borderRadius: 8, overflow: 'hidden', border: '0.5px solid #23262B', background: 'var(--code-bg)', marginTop: 18 }}>
608
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', borderBottom: '0.5px solid #23262B', padding: '10px 14px' }}>
609
+ <span style={{ fontFamily: 'var(--mono)', fontSize: 11, color: 'var(--code-dim, #6B7480)' }}>replay · point-in-time</span>
610
+ <span style={{ fontFamily: 'var(--mono)', fontSize: 11, color: 'var(--code-dim, #6B7480)' }}>copy</span>
611
+ </div>
612
+ <pre style={{ margin: 0, padding: '16px', fontFamily: 'var(--mono)', fontSize: 13, lineHeight: 1.7, color: 'var(--code-fg)', overflowX: 'auto' }}>
613
+ <span style={{ color: '#5BC0EB' }}>$</span>{` curl ${apiUrl}/v1/fx/eurusd?as_of=2026-03-14\n`}
614
+ <span style={{ color: '#6B7480' }}>{'# → the rate as published on 2026-03-14, not today\'s restatement'}</span>
615
+ </pre>
616
+ </div>
617
+
618
+ <h2
619
+ id="compare"
620
+ style={{
621
+ fontFamily: 'var(--mono)',
622
+ fontSize: 22,
623
+ fontWeight: 600,
624
+ letterSpacing: '-.01em',
625
+ margin: '30px 0 8px',
626
+ scrollMarginTop: 70,
627
+ }}
628
+ >
629
+ Naive vs point-in-time
630
+ </h2>
631
+
632
+ {/* Compare table — tinted header, horizontal hairlines, no zebra */}
633
+ <CompareTable rows={compareRows} />
634
+
635
+ {/* Pull-quote */}
636
+ <PullQuote>
637
+ &ldquo;If you cannot reproduce the number you traded on, you do not have a backtest — you have
638
+ a story.&rdquo;
639
+ </PullQuote>
640
+
641
+ <h2
642
+ id="putting-together"
643
+ style={{
644
+ fontFamily: 'var(--mono)',
645
+ fontSize: 22,
646
+ fontWeight: 600,
647
+ letterSpacing: '-.01em',
648
+ margin: '10px 0 4px',
649
+ scrollMarginTop: 70,
650
+ }}
651
+ >
652
+ Putting it together
653
+ </h2>
654
+ <p
655
+ style={{ fontSize: 17, color: '#3A3D44', lineHeight: 1.7, marginTop: 16 }}
656
+ >
657
+ Point-in-time is the default on every {brand} series — fx, metals, and power alike. You
658
+ do not opt in; you opt out by omitting <InlineCode>as_of</InlineCode> to get the latest.
659
+ Either way, every value names the feed and timestamp it came from, so the number is
660
+ auditable end to end.
661
+ </p>
662
+ </section>
663
+
664
+ {/* Author card */}
665
+ <AuthorSection author={author} />
666
+
667
+ </article>
668
+
669
+ {/* Keep reading — full-width rail, same max-width as rest of site */}
670
+ <RelatedCards cards={related} />
671
+ </>
672
+ );
673
+ }