tfrere commited on
Commit
e55fad0
·
1 Parent(s): 7e35f67

ui: add dark mode

Browse files
assets/images/moon.svg ADDED
assets/images/sun.svg ADDED
dist/assets/images/memorycoalescing.png CHANGED

Git LFS Details

  • SHA256: 088cd848100ab26abbffdcc7c0e8f18a83facd0a8637c460e3ac88d483b04b46
  • Pointer size: 130 Bytes
  • Size of remote file: 94.1 kB

Git LFS Details

  • SHA256: 1094fe9aeb953c743791445ee6d7e73a5a89fa85fe60f4312266d1265e7c591a
  • Pointer size: 130 Bytes
  • Size of remote file: 94.1 kB
dist/assets/images/moon.svg ADDED
dist/assets/images/sun.svg ADDED
dist/index.html CHANGED
The diff for this file is too large to render. See raw diff
 
dist/main.bundle.js CHANGED
@@ -5662,12 +5662,199 @@ function postMessageToHFSpaces(elementId) {
5662
  }
5663
 
5664
  ;// ./src/index.js
 
 
 
5665
  // import { plotClusters } from './clusters'
5666
 
5667
 
5668
 
 
 
5669
  document.addEventListener("DOMContentLoaded", function () {
5670
  console.log("DOMContentLoaded");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5671
  loadFragments();
5672
  init_memory_plot();
5673
  syncHFSpacesURLHash();
 
5662
  }
5663
 
5664
  ;// ./src/index.js
5665
+ function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = src_unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t["return"] || t["return"](); } finally { if (u) throw o; } } }; }
5666
+ function src_unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return src_arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? src_arrayLikeToArray(r, a) : void 0; } }
5667
+ function src_arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
5668
  // import { plotClusters } from './clusters'
5669
 
5670
 
5671
 
5672
+ // Dark mode is now handled manually via a CSS class on <html> and injected styles
5673
+
5674
  document.addEventListener("DOMContentLoaded", function () {
5675
  console.log("DOMContentLoaded");
5676
+
5677
+ // Inject minimal styles for the theme toggle button
5678
+ var styleEl = document.createElement('style');
5679
+ styleEl.textContent = "\n .theme-toggle-btn{position:absolute;top:16px;left:16px;z-index:10000;display:inline-flex;align-items:center;justify-content:center;width:40px;height:40px;border-radius:999px;background:rgba(255,255,255,0.9);backdrop-filter:saturate(150%) blur(6px);cursor:pointer;border:1px solid transparent;outline:none;box-shadow:none;-webkit-appearance:none;appearance:none;-webkit-tap-highlight-color:transparent}\n .theme-toggle-btn:hover{border-color:transparent;box-shadow:none}\n .theme-toggle-btn:focus,.theme-toggle-btn:focus-visible{outline:none;border-color:transparent;box-shadow:none}\n .theme-toggle-btn img{width:22px;height:22px;transition:filter .15s ease}\n .theme-toggle-btn.dark img{filter: brightness(0) invert(1)}\n @media (prefers-color-scheme: dark){.theme-toggle-btn{background:rgba(30,30,30,0.85);border-color:transparent;box-shadow:none}}\n ";
5680
+ document.head.appendChild(styleEl);
5681
+
5682
+ // Inject dark mode CSS (scoped via html.dark)
5683
+ var darkCSS = "\n html.dark{color-scheme:dark}\n html.dark body{background:#242525;color:#e5e7eb}\n html.dark a{color:#93c5fd}\n html.dark .figure-legend{color:#9ca3af}\n html.dark d-article,html.dark d-article *{color:white!important;}\n html.dark d-contents{background:#242525}\n html.dark d-contents nav a{color:#cbd5e1}\n html.dark d-contents nav a:hover{text-decoration:underline solid rgba(255,255,255,0.6)}\n html.dark .note-box{background:#111;border-left-color:#888}\n html.dark .note-box-title{color:#d1d5db}\n html.dark .note-box-content{color:#e5e7eb}\n html.dark .large-image-background{background:#242525}\n html.dark .boxed-image{background:#111;border-color:#262626;box-shadow:0 4px 6px rgba(0,0,0,.6)}\n html.dark #graph-all,html.dark #controls,html.dark .memory-block,html.dark .activation-memory,html.dark .gradient-memory{background:#111;border-color:#262626;box-shadow:0 4px 6px rgba(0,0,0,.6);color:#e5e7eb}\n html.dark label,html.dark .memory-title{color:#e5e7eb}\n html.dark .memory-value{color:#93c5fd}\n html.dark input,html.dark select,html.dark textarea{background:#0f0f0f;color:#e5e7eb;border:1px solid #333}\n html.dark input:hover,html.dark select:hover,html.dark textarea:hover{border-color:#60a5fa}\n html.dark input:focus,html.dark select:focus,html.dark textarea:focus{border-color:#3b82f6;box-shadow:0 0 0 2px rgba(59,130,246,0.35)}\n html.dark input[type=range]{background:#333}\n html.dark input[type=range]::-webkit-slider-thumb{background:#3b82f6}\n html.dark .plotly_caption{color:#9ca3af}\n html.dark .theme-toggle-btn{background:rgba(30,30,30,0.85);border-color:transparent}\n html.dark d-article img{background:white}\n html.dark summary {color:black !important;}\n html.dark .katex-container {color:white !important;}\n html.dark d-code {background: white!important;}\n /* Table borders in dark mode */\n html.dark table{border-color:#262626}\n html.dark th,html.dark td{border-color:#262626}\n html.dark thead tr,html.dark tbody tr{border-color:#262626}\n html.dark d-byline, html.dark d-article{border-top: 1px solid rgba(255, 255, 255, 0.5);}\n html.dark d-byline h3{color:white;}\n html.dark d-math *, html.dark span.katex{color:white !important;}\n html.dark d-appendix { color: white}\n html.dark h1, html.dark h2, html.dark h3, html.dark h4, html.dark h5, html.dark h6 { color: white}\n html.dark .l-body { background: white;}\n \n \n ";
5684
+ var darkStyleEl = document.createElement('style');
5685
+ darkStyleEl.id = 'darkmode-css';
5686
+ darkStyleEl.textContent = darkCSS;
5687
+ document.head.appendChild(darkStyleEl);
5688
+
5689
+ // Inject equivalent dark CSS into all ShadowRoots using :host-context(.dark)
5690
+ // This ensures styles also apply inside web components with Shadow DOM
5691
+ var shadowDarkCSS = darkCSS.replace(/html\.dark/g, ':host-context(.dark)');
5692
+ var injectDarkStylesIntoRoot = function injectDarkStylesIntoRoot(root) {
5693
+ // Only target ShadowRoots here
5694
+ if (!root || !(root instanceof ShadowRoot)) return;
5695
+ if (root.querySelector('style#darkmode-css-shadow')) return;
5696
+ var style = document.createElement('style');
5697
+ style.id = 'darkmode-css-shadow';
5698
+ style.textContent = shadowDarkCSS;
5699
+ root.appendChild(style);
5700
+ };
5701
+
5702
+ // Normalize inline SVGs: ensure viewBox and preserveAspectRatio for responsiveness
5703
+ var normalizeSvgElement = function normalizeSvgElement(svgEl) {
5704
+ try {
5705
+ if (!svgEl || svgEl.hasAttribute('viewBox')) return;
5706
+ var widthAttr = svgEl.getAttribute('width');
5707
+ var heightAttr = svgEl.getAttribute('height');
5708
+ if (!widthAttr || !heightAttr) return;
5709
+ var width = parseFloat(widthAttr);
5710
+ var height = parseFloat(heightAttr);
5711
+ if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) return;
5712
+ svgEl.setAttribute('viewBox', "0 0 ".concat(width, " ").concat(height));
5713
+ if (!svgEl.hasAttribute('preserveAspectRatio')) {
5714
+ svgEl.setAttribute('preserveAspectRatio', 'xMidYMid meet');
5715
+ }
5716
+ } catch (_) {
5717
+ // no-op
5718
+ }
5719
+ };
5720
+ var processRootForSVGs = function processRootForSVGs(root) {
5721
+ if (!root || typeof root.querySelectorAll !== 'function') return;
5722
+ var svgs = root.querySelectorAll('svg:not([viewBox])');
5723
+ svgs.forEach(function (svg) {
5724
+ return normalizeSvgElement(svg);
5725
+ });
5726
+ };
5727
+ var _scanNodeForShadowRoots = function scanNodeForShadowRoots(node) {
5728
+ if (!node) return;
5729
+ if (node.shadowRoot) {
5730
+ injectDarkStylesIntoRoot(node.shadowRoot);
5731
+ processRootForSVGs(node.shadowRoot);
5732
+ }
5733
+ // Traverse children
5734
+ if (node.childNodes && node.childNodes.length) {
5735
+ node.childNodes.forEach(function (child) {
5736
+ // Process SVGs in this subtree as well
5737
+ processRootForSVGs(child);
5738
+ _scanNodeForShadowRoots(child);
5739
+ });
5740
+ }
5741
+ };
5742
+
5743
+ // Intercept future shadow roots
5744
+ var originalAttachShadow = Element.prototype.attachShadow;
5745
+ Element.prototype.attachShadow = function (init) {
5746
+ var shadow = originalAttachShadow.call(this, init);
5747
+ try {
5748
+ injectDarkStylesIntoRoot(shadow);
5749
+ processRootForSVGs(shadow);
5750
+ } catch (e) {}
5751
+ return shadow;
5752
+ };
5753
+
5754
+ // Initial sweep for any existing shadow roots
5755
+ _scanNodeForShadowRoots(document.documentElement);
5756
+ // Initial pass for regular DOM SVGs
5757
+ processRootForSVGs(document);
5758
+
5759
+ // Observe DOM mutations to catch dynamically added components
5760
+ var mo = new MutationObserver(function (mutations) {
5761
+ var _iterator = _createForOfIteratorHelper(mutations),
5762
+ _step;
5763
+ try {
5764
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
5765
+ var m = _step.value;
5766
+ m.addedNodes && m.addedNodes.forEach(function (n) {
5767
+ _scanNodeForShadowRoots(n);
5768
+ processRootForSVGs(n);
5769
+ });
5770
+ }
5771
+ } catch (err) {
5772
+ _iterator.e(err);
5773
+ } finally {
5774
+ _iterator.f();
5775
+ }
5776
+ });
5777
+ mo.observe(document.documentElement, {
5778
+ childList: true,
5779
+ subtree: true
5780
+ });
5781
+
5782
+ // Create the toggle button
5783
+ var btn = document.createElement('button');
5784
+ btn.className = 'theme-toggle-btn';
5785
+ btn.setAttribute('type', 'button');
5786
+ btn.setAttribute('aria-label', 'Basculer le mode sombre');
5787
+ // Reuse icons declared in HTML and move them into the button
5788
+ var sunIcon = document.getElementById('sunIcon');
5789
+ var moonIcon = document.getElementById('moonIcon');
5790
+ if (sunIcon && moonIcon) {
5791
+ // Make sure they adopt button sizing
5792
+ sunIcon.style.display = 'none';
5793
+ sunIcon.style.width = '22px';
5794
+ sunIcon.style.height = '22px';
5795
+ moonIcon.style.display = 'none';
5796
+ moonIcon.style.width = '22px';
5797
+ moonIcon.style.height = '22px';
5798
+ btn.appendChild(sunIcon);
5799
+ btn.appendChild(moonIcon);
5800
+ }
5801
+ document.body.appendChild(btn);
5802
+ var setIcon = function setIcon(enabled) {
5803
+ // enabled = dark mode enabled -> show sun (to indicate turning off), hide moon
5804
+ sunIcon.style.display = enabled ? '' : 'none';
5805
+ moonIcon.style.display = enabled ? 'none' : '';
5806
+ btn.setAttribute('title', enabled ? 'Désactiver le mode sombre' : 'Activer le mode sombre');
5807
+ btn.setAttribute('aria-pressed', String(enabled));
5808
+ btn.classList.toggle('dark', enabled);
5809
+ };
5810
+ var setDark = function setDark(enabled) {
5811
+ document.documentElement.classList.toggle('dark', enabled);
5812
+ setIcon(enabled);
5813
+ };
5814
+ var THEME_KEY = 'theme';
5815
+ var savedTheme = null;
5816
+ try {
5817
+ savedTheme = localStorage.getItem(THEME_KEY);
5818
+ } catch (e) {}
5819
+ var media = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)');
5820
+ var prefersDark = media ? media.matches : false;
5821
+ // Initialisation: priorité à la préférence sauvegardée, sinon préférence système
5822
+ if (savedTheme === 'dark') {
5823
+ setDark(true);
5824
+ } else if (savedTheme === 'light') {
5825
+ setDark(false);
5826
+ } else {
5827
+ setDark(prefersDark);
5828
+ }
5829
+
5830
+ // Si l'utilisateur a déjà choisi manuellement, on ne suit plus la préférence système
5831
+ var manualOverride = savedTheme === 'dark' || savedTheme === 'light';
5832
+
5833
+ // React to system preference changes dynamically (no persistence)
5834
+ if (media && typeof media.addEventListener === 'function') {
5835
+ media.addEventListener('change', function (e) {
5836
+ if (!manualOverride) {
5837
+ setDark(e.matches);
5838
+ }
5839
+ });
5840
+ } else if (media && typeof media.addListener === 'function') {
5841
+ // Fallback for older browsers
5842
+ media.addListener(function (e) {
5843
+ if (!manualOverride) {
5844
+ setDark(e.matches);
5845
+ }
5846
+ });
5847
+ }
5848
+
5849
+ // Toggle handler — for réduire les glitches, attendre le next frame avant d'ajuster l'icône
5850
+ btn.addEventListener('click', function () {
5851
+ manualOverride = true;
5852
+ var next = !document.documentElement.classList.contains('dark');
5853
+ setDark(next);
5854
+ try {
5855
+ localStorage.setItem(THEME_KEY, next ? 'dark' : 'light');
5856
+ } catch (e) {}
5857
+ });
5858
  loadFragments();
5859
  init_memory_plot();
5860
  syncHFSpacesURLHash();
dist/main.bundle.js.map CHANGED
The diff for this file is too large to render. See raw diff
 
src/index.html CHANGED
@@ -58,6 +58,9 @@
58
  </script>
59
  </d-front-matter>
60
  <d-title>
 
 
 
61
  <h1 class="l-page" style="text-align: center;">The Ultra-Scale Playbook:<br>Training LLMs on GPU Clusters</h1>
62
  <div id="title-plot" class="main-plot-container l-screen" style="overflow-x: hidden; width: 100%; text-align: center;">
63
  <div style="display: flex; justify-content: center; position: relative;">
@@ -65,12 +68,14 @@
65
  </div>
66
  <p style="text-align: cekter; font-style: italic; margin-top: 10px; max-width: 900px; margin-left: auto; margin-right: auto;">We ran over 4,000 scaling experiments on up to 512 GPUs and measured throughput (size of markers) and GPU utilization (color of markers). Note that both are normalized per model size in this visualization.</p>
67
 
68
- <div class="order-button-container">
69
- <button class="order-button" style="margin: 0 8px;" onclick="window.open('https://www.lulu.com/shop/nouamane-tazi-and-ferdinand-mom-and-haojun-zhao-and-phuc-nguyen/the-ultra-scale-playbook/paperback/product-45yk9dj.html?page=1&pageSize=4', '_blank')">Order Book</button>
70
- <button class="order-button" style="margin: 0 8px;" onclick="window.open('https://huggingface.co/nanotron', '_blank')">Get PDF</button>
71
- </div>
72
  </div>
 
73
  </d-title>
 
 
 
 
74
  <d-byline></d-byline>
75
  <d-article>
76
  <d-contents>
@@ -101,7 +106,7 @@
101
  <aside>Note that we're still missing pipeline parallelism in this widget. To be added as an exercise for the reader.</aside>
102
 
103
  <div class="large-image-background-transparent">
104
- <div style="display: grid; grid-template-columns: 1fr 1fr; align-items: center;">
105
  <div id="graph-all">
106
  <div class="figure-legend">Memory usage breakdown</div>
107
  <div id="graph"></div>
@@ -837,7 +842,7 @@
837
  frame.style.width = frame.contentWindow.document.documentElement.scrollWidth + 'px';
838
  });
839
  </script> -->
840
- <!-- <p><img alt="dp_ourjourney_memoryusage.svg" src="/assets/images/dp_ourjourney_memoryusage.svg" /></p> -->
841
 
842
 
843
  <p>We've also seen that data parallelism starts to have some limiting communication overhead above a certain level of scaling. Do we have other options for these larger models or large batch sizes? We do have some solutions, thankfully - they involve either moving some tensors to the CPU or splitting the weights/gradients/optimizer states tensors across GPU devices.</p>
 
58
  </script>
59
  </d-front-matter>
60
  <d-title>
61
+ <!-- Theme toggle icons (declared once in DOM for clarity; visibility controlled via JS) -->
62
+ <img id="sunIcon" src="assets/images/sun.svg" alt="Sun icon" style="display:none;width:22px;height:22px;color:inherit"/>
63
+ <img id="moonIcon" src="assets/images/moon.svg" alt="Moon icon" style="display:none;width:22px;height:22px;color:inherit"/>
64
  <h1 class="l-page" style="text-align: center;">The Ultra-Scale Playbook:<br>Training LLMs on GPU Clusters</h1>
65
  <div id="title-plot" class="main-plot-container l-screen" style="overflow-x: hidden; width: 100%; text-align: center;">
66
  <div style="display: flex; justify-content: center; position: relative;">
 
68
  </div>
69
  <p style="text-align: cekter; font-style: italic; margin-top: 10px; max-width: 900px; margin-left: auto; margin-right: auto;">We ran over 4,000 scaling experiments on up to 512 GPUs and measured throughput (size of markers) and GPU utilization (color of markers). Note that both are normalized per model size in this visualization.</p>
70
 
71
+
 
 
 
72
  </div>
73
+
74
  </d-title>
75
+ <div class="order-button-container">
76
+ <button class="order-button" style="margin: 0 8px;" onclick="window.open('https://www.lulu.com/shop/nouamane-tazi-and-ferdinand-mom-and-haojun-zhao-and-phuc-nguyen/the-ultra-scale-playbook/paperback/product-45yk9dj.html?page=1&pageSize=4', '_blank')">Order Book</button>
77
+ <button class="order-button" style="margin: 0 8px;" onclick="window.open('https://huggingface.co/nanotron', '_blank')">Get PDF</button>
78
+ </div>
79
  <d-byline></d-byline>
80
  <d-article>
81
  <d-contents>
 
106
  <aside>Note that we're still missing pipeline parallelism in this widget. To be added as an exercise for the reader.</aside>
107
 
108
  <div class="large-image-background-transparent">
109
+ <div id="graph-container">
110
  <div id="graph-all">
111
  <div class="figure-legend">Memory usage breakdown</div>
112
  <div id="graph"></div>
 
842
  frame.style.width = frame.contentWindow.document.documentElement.scrollWidth + 'px';
843
  });
844
  </script> -->
845
+ <p><img alt="dp_ourjourney_memoryusage.svg" src="/assets/images/dp_ourjourney_memoryusage.svg" /></p>
846
 
847
 
848
  <p>We've also seen that data parallelism starts to have some limiting communication overhead above a certain level of scaling. Do we have other options for these larger models or large batch sizes? We do have some solutions, thankfully - they involve either moving some tensors to the CPU or splitting the weights/gradients/optimizer states tensors across GPU devices.</p>
src/index.js CHANGED
@@ -2,9 +2,234 @@
2
  import { init_memory_plot } from './memory'
3
  import { loadFragments } from './fragmentLoader'
4
  import { syncHFSpacesURLHash } from './syncHFSpacesURLHash'
 
5
 
6
  document.addEventListener("DOMContentLoaded", () => {
7
  console.log("DOMContentLoaded");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  loadFragments();
9
  init_memory_plot();
10
  syncHFSpacesURLHash();
 
2
  import { init_memory_plot } from './memory'
3
  import { loadFragments } from './fragmentLoader'
4
  import { syncHFSpacesURLHash } from './syncHFSpacesURLHash'
5
+ // Dark mode is now handled manually via a CSS class on <html> and injected styles
6
 
7
  document.addEventListener("DOMContentLoaded", () => {
8
  console.log("DOMContentLoaded");
9
+
10
+ // Inject minimal styles for the theme toggle button
11
+ const styleEl = document.createElement('style');
12
+ styleEl.textContent = `
13
+ .theme-toggle-btn{position:absolute;top:16px;left:16px;z-index:10000;display:inline-flex;align-items:center;justify-content:center;width:40px;height:40px;border-radius:999px;background:rgba(255,255,255,0.9);backdrop-filter:saturate(150%) blur(6px);cursor:pointer;border:1px solid transparent;outline:none;box-shadow:none;-webkit-appearance:none;appearance:none;-webkit-tap-highlight-color:transparent}
14
+ .theme-toggle-btn:hover{border-color:transparent;box-shadow:none}
15
+ .theme-toggle-btn:focus,.theme-toggle-btn:focus-visible{outline:none;border-color:transparent;box-shadow:none}
16
+ .theme-toggle-btn img{width:22px;height:22px;transition:filter .15s ease}
17
+ .theme-toggle-btn.dark img{filter: brightness(0) invert(1)}
18
+ @media (prefers-color-scheme: dark){.theme-toggle-btn{background:rgba(30,30,30,0.85);border-color:transparent;box-shadow:none}}
19
+ `;
20
+ document.head.appendChild(styleEl);
21
+
22
+ // Inject dark mode CSS (scoped via html.dark)
23
+ const darkCSS = `
24
+ html.dark{color-scheme:dark}
25
+ html.dark body{background:#242525;color:#e5e7eb}
26
+ html.dark a{color:#93c5fd}
27
+ html.dark .figure-legend{color:#9ca3af}
28
+ html.dark d-article,html.dark d-article *{color:white!important;}
29
+ html.dark d-contents{background:#242525}
30
+ html.dark d-contents nav a{color:#cbd5e1}
31
+ html.dark d-contents nav a:hover{text-decoration:underline solid rgba(255,255,255,0.6)}
32
+ html.dark .note-box{background:#111;border-left-color:#888}
33
+ html.dark .note-box-title{color:#d1d5db}
34
+ html.dark .note-box-content{color:#e5e7eb}
35
+ html.dark .large-image-background{background:#242525}
36
+ html.dark .boxed-image{background:#111;border-color:#262626;box-shadow:0 4px 6px rgba(0,0,0,.6)}
37
+ html.dark #graph-all,html.dark #controls,html.dark .memory-block,html.dark .activation-memory,html.dark .gradient-memory{background:#111;border-color:#262626;box-shadow:0 4px 6px rgba(0,0,0,.6);color:#e5e7eb}
38
+ html.dark label,html.dark .memory-title{color:#e5e7eb}
39
+ html.dark .memory-value{color:#93c5fd}
40
+ html.dark input,html.dark select,html.dark textarea{background:#0f0f0f;color:#e5e7eb;border:1px solid #333}
41
+ html.dark input:hover,html.dark select:hover,html.dark textarea:hover{border-color:#60a5fa}
42
+ html.dark input:focus,html.dark select:focus,html.dark textarea:focus{border-color:#3b82f6;box-shadow:0 0 0 2px rgba(59,130,246,0.35)}
43
+ html.dark input[type=range]{background:#333}
44
+ html.dark input[type=range]::-webkit-slider-thumb{background:#3b82f6}
45
+ html.dark .plotly_caption{color:#9ca3af}
46
+ html.dark .theme-toggle-btn{background:rgba(30,30,30,0.85);border-color:transparent}
47
+ html.dark d-article img{background:white}
48
+ html.dark summary {color:black !important;}
49
+ html.dark .katex-container {color:white !important;}
50
+ html.dark d-code {background: white!important;}
51
+ /* Table borders in dark mode */
52
+ html.dark table{border-color:#262626}
53
+ html.dark th,html.dark td{border-color:#262626}
54
+ html.dark thead tr,html.dark tbody tr{border-color:#262626}
55
+ html.dark d-byline, html.dark d-article{border-top: 1px solid rgba(255, 255, 255, 0.5);}
56
+ html.dark d-byline h3{color:white;}
57
+ html.dark d-math *, html.dark span.katex{color:white !important;}
58
+ html.dark d-appendix { color: white}
59
+ html.dark h1, html.dark h2, html.dark h3, html.dark h4, html.dark h5, html.dark h6 { color: white}
60
+ html.dark .l-body { background: white;}
61
+
62
+
63
+ `;
64
+ const darkStyleEl = document.createElement('style');
65
+ darkStyleEl.id = 'darkmode-css';
66
+ darkStyleEl.textContent = darkCSS;
67
+ document.head.appendChild(darkStyleEl);
68
+
69
+ // Inject equivalent dark CSS into all ShadowRoots using :host-context(.dark)
70
+ // This ensures styles also apply inside web components with Shadow DOM
71
+ const shadowDarkCSS = darkCSS.replace(/html\.dark/g, ':host-context(.dark)');
72
+
73
+ const injectDarkStylesIntoRoot = (root) => {
74
+ // Only target ShadowRoots here
75
+ if (!root || !(root instanceof ShadowRoot)) return;
76
+ if (root.querySelector('style#darkmode-css-shadow')) return;
77
+ const style = document.createElement('style');
78
+ style.id = 'darkmode-css-shadow';
79
+ style.textContent = shadowDarkCSS;
80
+ root.appendChild(style);
81
+ };
82
+
83
+ // Normalize inline SVGs: ensure viewBox and preserveAspectRatio for responsiveness
84
+ const normalizeSvgElement = (svgEl) => {
85
+ try {
86
+ if (!svgEl || svgEl.hasAttribute('viewBox')) return;
87
+ const widthAttr = svgEl.getAttribute('width');
88
+ const heightAttr = svgEl.getAttribute('height');
89
+ if (!widthAttr || !heightAttr) return;
90
+ const width = parseFloat(widthAttr);
91
+ const height = parseFloat(heightAttr);
92
+ if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) return;
93
+ svgEl.setAttribute('viewBox', `0 0 ${width} ${height}`);
94
+ if (!svgEl.hasAttribute('preserveAspectRatio')) {
95
+ svgEl.setAttribute('preserveAspectRatio', 'xMidYMid meet');
96
+ }
97
+ } catch (_) {
98
+ // no-op
99
+ }
100
+ };
101
+
102
+ const processRootForSVGs = (root) => {
103
+ if (!root || typeof root.querySelectorAll !== 'function') return;
104
+ const svgs = root.querySelectorAll('svg:not([viewBox])');
105
+ svgs.forEach((svg) => normalizeSvgElement(svg));
106
+ };
107
+
108
+ const scanNodeForShadowRoots = (node) => {
109
+ if (!node) return;
110
+ if (node.shadowRoot) {
111
+ injectDarkStylesIntoRoot(node.shadowRoot);
112
+ processRootForSVGs(node.shadowRoot);
113
+ }
114
+ // Traverse children
115
+ if (node.childNodes && node.childNodes.length) {
116
+ node.childNodes.forEach((child) => {
117
+ // Process SVGs in this subtree as well
118
+ processRootForSVGs(child);
119
+ scanNodeForShadowRoots(child);
120
+ });
121
+ }
122
+ };
123
+
124
+ // Intercept future shadow roots
125
+ const originalAttachShadow = Element.prototype.attachShadow;
126
+ Element.prototype.attachShadow = function(init) {
127
+ const shadow = originalAttachShadow.call(this, init);
128
+ try {
129
+ injectDarkStylesIntoRoot(shadow);
130
+ processRootForSVGs(shadow);
131
+ } catch (e) {}
132
+ return shadow;
133
+ };
134
+
135
+ // Initial sweep for any existing shadow roots
136
+ scanNodeForShadowRoots(document.documentElement);
137
+ // Initial pass for regular DOM SVGs
138
+ processRootForSVGs(document);
139
+
140
+ // Observe DOM mutations to catch dynamically added components
141
+ const mo = new MutationObserver((mutations) => {
142
+ for (const m of mutations) {
143
+ m.addedNodes && m.addedNodes.forEach((n) => {
144
+ scanNodeForShadowRoots(n);
145
+ processRootForSVGs(n);
146
+ });
147
+ }
148
+ });
149
+ mo.observe(document.documentElement, { childList: true, subtree: true });
150
+
151
+ // Create the toggle button
152
+ const btn = document.createElement('button');
153
+ btn.className = 'theme-toggle-btn';
154
+ btn.setAttribute('type', 'button');
155
+ btn.setAttribute('aria-label', 'Basculer le mode sombre');
156
+ // Reuse icons declared in HTML and move them into the button
157
+ const sunIcon = document.getElementById('sunIcon');
158
+ const moonIcon = document.getElementById('moonIcon');
159
+
160
+ if (sunIcon && moonIcon) {
161
+ // Make sure they adopt button sizing
162
+ sunIcon.style.display = 'none';
163
+ sunIcon.style.width = '22px';
164
+ sunIcon.style.height = '22px';
165
+ moonIcon.style.display = 'none';
166
+ moonIcon.style.width = '22px';
167
+ moonIcon.style.height = '22px';
168
+ btn.appendChild(sunIcon);
169
+ btn.appendChild(moonIcon);
170
+ }
171
+ document.body.appendChild(btn);
172
+
173
+ const setIcon = (enabled) => {
174
+ // enabled = dark mode enabled -> show sun (to indicate turning off), hide moon
175
+ sunIcon.style.display = enabled ? '' : 'none';
176
+ moonIcon.style.display = enabled ? 'none' : '';
177
+ btn.setAttribute('title', enabled ? 'Désactiver le mode sombre' : 'Activer le mode sombre');
178
+ btn.setAttribute('aria-pressed', String(enabled));
179
+ btn.classList.toggle('dark', enabled);
180
+ };
181
+
182
+ const setDark = (enabled) => {
183
+ document.documentElement.classList.toggle('dark', enabled);
184
+ setIcon(enabled);
185
+ };
186
+
187
+ const THEME_KEY = 'theme';
188
+ let savedTheme = null;
189
+ try {
190
+ savedTheme = localStorage.getItem(THEME_KEY);
191
+ } catch (e) {}
192
+
193
+ const media = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)');
194
+ const prefersDark = media ? media.matches : false;
195
+ // Initialisation: priorité à la préférence sauvegardée, sinon préférence système
196
+ if (savedTheme === 'dark') {
197
+ setDark(true);
198
+ } else if (savedTheme === 'light') {
199
+ setDark(false);
200
+ } else {
201
+ setDark(prefersDark);
202
+ }
203
+
204
+ // Si l'utilisateur a déjà choisi manuellement, on ne suit plus la préférence système
205
+ let manualOverride = savedTheme === 'dark' || savedTheme === 'light';
206
+
207
+ // React to system preference changes dynamically (no persistence)
208
+ if (media && typeof media.addEventListener === 'function') {
209
+ media.addEventListener('change', (e) => {
210
+ if (!manualOverride) {
211
+ setDark(e.matches);
212
+ }
213
+ });
214
+ } else if (media && typeof media.addListener === 'function') {
215
+ // Fallback for older browsers
216
+ media.addListener((e) => {
217
+ if (!manualOverride) {
218
+ setDark(e.matches);
219
+ }
220
+ });
221
+ }
222
+
223
+ // Toggle handler — for réduire les glitches, attendre le next frame avant d'ajuster l'icône
224
+ btn.addEventListener('click', () => {
225
+ manualOverride = true;
226
+ const next = !document.documentElement.classList.contains('dark');
227
+ setDark(next);
228
+ try {
229
+ localStorage.setItem(THEME_KEY, next ? 'dark' : 'light');
230
+ } catch (e) {}
231
+ });
232
+
233
  loadFragments();
234
  init_memory_plot();
235
  syncHFSpacesURLHash();