pandorga 1.12.0 → 1.14.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f0650221add1248fcfa882ccf2557c9d73d1de757239e6e396e8cec1ddefa512
4
- data.tar.gz: 2c6502ac5c6fdd2d6153490b88925e465b7c66f315502e61819b6541e530831b
3
+ metadata.gz: 9f6585d9644f2c5715b1c18088443f0a17d57f65783b0805899ad2882cc57643
4
+ data.tar.gz: f749368069fe9166d3f483292abed8f8da5b32b0c5f7c1da0b2e798241f859ed
5
5
  SHA512:
6
- metadata.gz: 9123d4ada63311688340fa8da4076a898dfb362764d15030da03cd62776def8172e9be28fc9d7fe47005930c004565860f8997ca46050c7149eb76332c94fa6d
7
- data.tar.gz: 6dbf48af21a4d6e56a0b21e070a951f8180424de1efbb906830d435200d736b4955781d0f04b3f7677be32cf1ce20a916bdad761faf41c6fd836ac48f06e1aa4
6
+ metadata.gz: 81fff53869d080e555aa8cb77e19c8d8882da7855d23786ec01d255ae0e0d47c9b357f26fba11d8ca663dd697778fe7d8f3315bbff59ecdbd64820e90b4cc385
7
+ data.tar.gz: dba781ffd7ee1879bdd6676d45117a72b3dbdc8582b9c76cf45e04b9757889d36a0345282226602a7d2350398e7eb2b4089b0d7468d0c81185e92d3983cfec3c
data/CHANGELOG.md CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [1.14.0] — 2026-10-20
6
+
7
+ ### Changed
8
+
9
+ - Pills, tags, facets, filters, and buttons use a borderless felt-pen highlighter wash with deterministic nth-child geometry variants, quiet and strong mark strengths, solid fill on hover, and section-accented active rail links (PLT-AC-46).
10
+ - The CV timeline pans in time: drag the chart, use the ‹ Today › controls, or focus it and use the arrow keys, Page Up/Down, Home and End. Zoom, resize and lane toggles keep the date at the center in view. Selecting a period now takes Shift+drag (a plain drag pans) (PLT-AC-47).
11
+
12
+ ### Fixed
13
+
14
+ - The CV timeline no longer handles an input twice when the inline placeholder boots before the timeline data loads; zoom buttons and resize were affected before this change (PLT-AC-47).
15
+
16
+ ## [1.13.0] — 2026-10-19
17
+
18
+ ### Added
19
+
20
+ - `pandorga export` and the new `pandorga thumbnails [root]` command write 16:9 center-cropped width variants (`variants/<stem>-{480,960,1440}.jpg`, progressive JPEG at quality 82, stripped of metadata, never upscaled) beside each local `/media/...` project thumbnail when ImageMagick is installed (logging one warning otherwise), export `thumbnail_srcset` on project front matter and `writing_index.json`, and render `srcset` and `sizes` on project cards and the home specimen (PLT-AC-44).
21
+
5
22
  ## [1.12.0] — 2026-10-19
6
23
 
7
24
  ### Changed
@@ -300,7 +300,7 @@
300
300
  *
301
301
  * A video URL still renders rather than vanishing, but it downloads
302
302
  * nothing until the reader asks for it. */
303
- function renderPosterMedia(src, alt) {
303
+ function renderPosterMedia(src, alt, options) {
304
304
  var url = String(src || '').trim();
305
305
  if (!url) return '';
306
306
  var isVideo = /\.(mp4|webm|mov)(\?|#|$)/i.test(url);
@@ -312,9 +312,15 @@
312
312
  '</div>'
313
313
  ].join('');
314
314
  }
315
+ var opts = options || {};
316
+ var srcset = String(opts.srcset || '').trim();
317
+ var sizes = String(opts.sizes || '').trim();
315
318
  return [
316
319
  '<div class="ledger-poster">',
317
- '<img src="', escapeAttr(url), '" alt="', escapeAttr(alt || ''),
320
+ '<img src="', escapeAttr(url), '"',
321
+ srcset ? ' srcset="' + escapeAttr(srcset) + '"' : '',
322
+ (srcset && sizes) ? ' sizes="' + escapeAttr(sizes) + '"' : '',
323
+ ' alt="', escapeAttr(alt || ''),
318
324
  '" loading="lazy" decoding="async"',
319
325
  // Thumbnails are frequently hotlinked to third-party hosts. When one
320
326
  // dies, remove the frame rather than leaving an empty plate behind.
@@ -324,6 +330,16 @@
324
330
  ].join('');
325
331
  }
326
332
 
333
+ /* Project thumbnail sizes (PLT-AC-44):
334
+ * - Home specimen (home.css PLT-AC-41): >= 900px column is max(35%, 20rem)
335
+ * inside max-width 80rem (1408px at html 110%), minus card padding =>
336
+ * ~431 CSS px at 1440px viewport (31vw = 446px -> 480w at DPR 1, 960w at
337
+ * DPR 2); below 900px full band width (390px at DPR 2 = 780px -> 960w).
338
+ * - Projects grid card (projects.css): 3 cols >= 1024px (~384px -> 28vw),
339
+ * 2 cols >= 700px (~46vw), 1 col below (92vw). */
340
+ var PROJECT_CARD_SIZES = '(min-width: 1024px) 28vw, (min-width: 700px) 46vw, 92vw';
341
+ var HOME_SPECIMEN_SIZES = '(min-width: 900px) 31vw, 92vw';
342
+
327
343
  function twoDigits(value) {
328
344
  var label = String(Number(value));
329
345
  return label.length < 2 ? '0' + label : label;
@@ -418,7 +434,10 @@
418
434
  if (options.frameCorners) {
419
435
  html += '<span class="ledger-frame-corners" aria-hidden="true"></span>';
420
436
  }
421
- var poster = renderPosterMedia(fm.thumbnail, name);
437
+ var poster = renderPosterMedia(fm.thumbnail, name, {
438
+ srcset: fm.thumbnail_srcset,
439
+ sizes: options.sizes || PROJECT_CARD_SIZES
440
+ });
422
441
  if (poster) {
423
442
  html += poster;
424
443
  } else if (options.glyphFallback && fm.icon) {
@@ -501,7 +520,10 @@
501
520
  var mark = fm.icon
502
521
  ? { cls: 'material-symbols-outlined', text: fm.icon }
503
522
  : { cls: 'is-initial', text: String(name || '').charAt(0).toUpperCase() };
504
- var poster = renderPosterMedia(fm.thumbnail, name);
523
+ var poster = renderPosterMedia(fm.thumbnail, name, {
524
+ srcset: fm.thumbnail_srcset,
525
+ sizes: options.sizes || HOME_SPECIMEN_SIZES
526
+ });
505
527
  html += '<div class="home-specimen-figure">';
506
528
  if (poster) {
507
529
  html += poster + '<span class="home-specimen-badge ' + mark.cls + '" aria-hidden="true">' +
@@ -140,6 +140,7 @@
140
140
  description: entry.description,
141
141
  excerpt: entry.excerpt,
142
142
  thumbnail: entry.thumbnail,
143
+ thumbnail_srcset: entry.thumbnail_srcset,
143
144
  language: entry.language,
144
145
  date: entry.date,
145
146
  last_updated: entry.last_updated,
@@ -21,6 +21,10 @@
21
21
  </ul>
22
22
  </div>
23
23
 
24
+ <div class="cv-timeline-control-group">
25
+ <p class="cv-timeline-hint font-body text-xs text-on-surface-variant">Shift+drag to select a period</p>
26
+ </div>
27
+
24
28
  <div class="cv-timeline-control-group" id="timeline-selection-panel" hidden>
25
29
  <h3 class="cv-timeline-control-title card-meta">Selected period</h3>
26
30
  <p id="timeline-selection-range" class="cv-timeline-range code-technical"></p>
@@ -58,9 +62,12 @@
58
62
  </div>
59
63
 
60
64
  <div class="cv-timeline-canvas">
61
- <div id="timeline-chart-wrapper" class="applet-canvas relative w-full overflow-x-auto">
65
+ <div id="timeline-chart-wrapper" class="applet-canvas relative w-full overflow-x-auto" tabindex="0" role="region" aria-label="Career timeline, use arrow keys to move in time">
62
66
  <div id="timeline-chart" class="relative"></div>
63
67
  <div id="timeline-zoom-controls" class="timeline-applet-control cv-timeline-zoom sticky left-0 z-10 flex items-center gap-2 border px-2 py-1.5 w-fit ml-2 mt-4 mb-4 pointer-events-auto">
68
+ <button type="button" id="timeline-pan-prev" class="timeline-zoom-btn timeline-applet-control font-label text-sm leading-none w-8 h-8 border text-on-surface-variant hover:border-primary hover:text-primary transition-colors" aria-label="Pan left">‹</button>
69
+ <button type="button" id="timeline-pan-today" class="timeline-zoom-btn timeline-applet-control font-label text-sm leading-none h-8 px-2 border text-on-surface-variant hover:border-primary hover:text-primary transition-colors" aria-label="Scroll to today">Today</button>
70
+ <button type="button" id="timeline-pan-next" class="timeline-zoom-btn timeline-applet-control font-label text-sm leading-none w-8 h-8 border text-on-surface-variant hover:border-primary hover:text-primary transition-colors" aria-label="Pan right">›</button>
64
71
  <button type="button" id="timeline-zoom-out" class="timeline-zoom-btn timeline-applet-control font-label text-sm leading-none w-8 h-8 border text-on-surface-variant hover:border-primary hover:text-primary transition-colors" aria-label="Zoom out">−</button>
65
72
  <span id="timeline-zoom-label" class="cv-timeline-zoom-label code-technical min-w-[3.5rem] text-center">2 yr</span>
66
73
  <button type="button" id="timeline-zoom-in" class="timeline-zoom-btn timeline-applet-control font-label text-sm leading-none w-8 h-8 border text-on-surface-variant hover:border-primary hover:text-primary transition-colors" aria-label="Zoom in">+</button>
@@ -182,6 +189,13 @@
182
189
  var el = document.getElementById('timeline-data');
183
190
 
184
191
  function bootTimeline(payload) {
192
+ /* The inline stub can boot before the runtime JSON arrives, and the JSON
193
+ then boots again. Abort the earlier boot's listeners on persistent
194
+ elements so each input is handled once, by the current data. */
195
+ if (bootTimeline.listeners) bootTimeline.listeners.abort();
196
+ var listeners = new AbortController();
197
+ bootTimeline.listeners = listeners;
198
+ var bootScope = { signal: listeners.signal };
185
199
  var meta = payload.meta || {};
186
200
  var events = payload.events || [];
187
201
  var buckets = Array.isArray(payload.writing_buckets) ? payload.writing_buckets : [];
@@ -255,7 +269,25 @@
255
269
  var zoomLabel = document.getElementById('timeline-zoom-label');
256
270
  var selectionLayer = null;
257
271
  var selectionBand = null;
258
- var dragState = { active: false, anchorMs: 0 };
272
+ var registeredTracks = new Set();
273
+ var isSelecting = false;
274
+ var isPanCandidate = false;
275
+ var isPanning = false;
276
+ var pointerId = null;
277
+ var anchorMs = 0;
278
+ var startX = 0;
279
+ var startScroll = 0;
280
+
281
+ function panTarget(scrollLeft, visibleW, maxScroll, step) {
282
+ // step is a signed fraction of the visible width (e.g. 0.1, -0.9)
283
+ return Math.round(Math.min(maxScroll, Math.max(0, scrollLeft + step * visibleW)));
284
+ }
285
+
286
+ function centerPreservingScroll(centerMs, xPosMs, visibleW, maxScroll) {
287
+ // xPosMs maps a timestamp to an x pixel within the scroll content
288
+ var target = xPosMs(centerMs) - visibleW / 2;
289
+ return Math.round(Math.min(maxScroll, Math.max(0, target)));
290
+ }
259
291
 
260
292
  function parseDate(s) {
261
293
  var p = s.split('-');
@@ -287,12 +319,14 @@
287
319
  var maxYear = new Date(maxT).getFullYear();
288
320
  var rangeMs = maxT - minT || MS_DAY;
289
321
  var chartW = 720;
322
+ var renderedClientW = 0;
290
323
 
291
324
  function visibleChartWidth() {
292
325
  return Math.max(480, (wrapper.clientWidth || 720) - LABEL_W);
293
326
  }
294
327
 
295
328
  function updateChartWidth() {
329
+ renderedClientW = wrapper.clientWidth;
296
330
  var visibleW = visibleChartWidth();
297
331
  var maxZoomYears = Math.max(MIN_ZOOM_YEARS, rangeMs / MS_YEAR);
298
332
  if (zoomYears > maxZoomYears) zoomYears = maxZoomYears;
@@ -311,13 +345,26 @@
311
345
  if (zoomLabel) zoomLabel.textContent = formatZoomLabel();
312
346
  }
313
347
 
348
+ function centerTimeMs() {
349
+ /* Use the width the chart was last rendered at: on resize the wrapper
350
+ is already at its new width while chartW still maps the old one. */
351
+ var x = wrapper.scrollLeft + (renderedClientW || wrapper.clientWidth) / 2;
352
+ return minT + ((x - LABEL_W) / chartW) * rangeMs;
353
+ }
354
+
355
+ function renderKeepingCenter() {
356
+ var c = centerTimeMs();
357
+ render();
358
+ wrapper.scrollLeft = centerPreservingScroll(c, xPosMs, wrapper.clientWidth, Math.max(0, container.offsetWidth - wrapper.clientWidth));
359
+ if (typeof updatePanButtons === 'function') updatePanButtons();
360
+ }
361
+
314
362
  function changeZoom(direction) {
315
363
  var maxZoomYears = Math.max(MIN_ZOOM_YEARS, rangeMs / MS_YEAR);
316
364
  var factor = direction > 0 ? 0.75 : 1.33;
317
365
  zoomYears = Math.max(MIN_ZOOM_YEARS, Math.min(maxZoomYears, zoomYears * factor));
318
366
  updateZoomLabel();
319
- render();
320
- scrollToToday();
367
+ renderKeepingCenter();
321
368
  }
322
369
 
323
370
  function scrollToToday() {
@@ -606,23 +653,82 @@
606
653
  }
607
654
 
608
655
  function setupSelectionDrag(track) {
609
- track.addEventListener('mousedown', function (e) {
610
- if (e.button !== 0 || e.target !== track) return;
611
- dragState.active = true;
612
- dragState.anchorMs = timeFromClientX(e.clientX);
613
- setSelection(dragState.anchorMs, dragState.anchorMs);
614
- e.preventDefault();
615
- });
656
+ registeredTracks.add(track);
616
657
  }
617
658
 
618
- window.addEventListener('mousemove', function (e) {
619
- if (!dragState.active) return;
620
- setSelection(dragState.anchorMs, timeFromClientX(e.clientX));
621
- });
659
+ if (wrapper) {
660
+ wrapper.addEventListener('pointerdown', function (e) {
661
+ if (e.button !== 0) return;
662
+ if (e.pointerType === 'touch') return;
663
+
664
+ if (e.shiftKey && registeredTracks.has(e.target)) {
665
+ isSelecting = true;
666
+ pointerId = e.pointerId;
667
+ anchorMs = timeFromClientX(e.clientX);
668
+ setSelection(anchorMs, anchorMs);
669
+ if (wrapper.setPointerCapture) {
670
+ try { wrapper.setPointerCapture(e.pointerId); } catch (err) {}
671
+ }
672
+ e.preventDefault();
673
+ } else {
674
+ isPanCandidate = true;
675
+ isPanning = false;
676
+ pointerId = e.pointerId;
677
+ startX = e.clientX;
678
+ startScroll = wrapper.scrollLeft;
679
+ }
680
+ }, bootScope);
681
+
682
+ wrapper.addEventListener('pointermove', function (e) {
683
+ if (isSelecting) {
684
+ setSelection(anchorMs, timeFromClientX(e.clientX));
685
+ } else if (isPanCandidate) {
686
+ var dx = e.clientX - startX;
687
+ if (!isPanning && Math.abs(dx) >= 4) {
688
+ isPanning = true;
689
+ if (wrapper.setPointerCapture) {
690
+ try { wrapper.setPointerCapture(e.pointerId); } catch (err) {}
691
+ }
692
+ wrapper.classList.add('is-dragging');
693
+ hideTip();
694
+ }
695
+ if (isPanning) {
696
+ wrapper.scrollLeft = startScroll - dx;
697
+ }
698
+ }
699
+ }, bootScope);
622
700
 
623
- window.addEventListener('mouseup', function () {
624
- dragState.active = false;
625
- });
701
+ var endPointerDrag = function () {
702
+ if (!isSelecting && !isPanCandidate) return;
703
+
704
+ if (pointerId !== null && wrapper.hasPointerCapture && wrapper.hasPointerCapture(pointerId)) {
705
+ try { wrapper.releasePointerCapture(pointerId); } catch (err) {}
706
+ }
707
+ wrapper.classList.remove('is-dragging');
708
+
709
+ var panned = isPanning;
710
+
711
+ isSelecting = false;
712
+ isPanCandidate = false;
713
+ isPanning = false;
714
+ pointerId = null;
715
+
716
+ if (panned) {
717
+ var suppressClick = function (clickEvent) {
718
+ clickEvent.stopPropagation();
719
+ clickEvent.preventDefault();
720
+ window.removeEventListener('click', suppressClick, true);
721
+ };
722
+ window.addEventListener('click', suppressClick, true);
723
+ setTimeout(function () {
724
+ window.removeEventListener('click', suppressClick, true);
725
+ }, 100);
726
+ }
727
+ };
728
+
729
+ wrapper.addEventListener('pointerup', endPointerDrag, bootScope);
730
+ wrapper.addEventListener('pointercancel', endPointerDrag, bootScope);
731
+ }
626
732
 
627
733
  function renderBars(track, items, color, barHeight, ongoingAllowed, laneH, gap) {
628
734
  items.forEach(function (ev) {
@@ -639,6 +745,7 @@
639
745
  }
640
746
 
641
747
  function render() {
748
+ registeredTracks.clear();
642
749
  updateChartWidth();
643
750
  container.innerHTML = '';
644
751
  barRegistry = [];
@@ -798,16 +905,16 @@
798
905
  container.style.height = y + 'px';
799
906
  container.style.minHeight = '';
800
907
  applySelectionHighlight();
908
+ updatePanButtons();
801
909
  }
802
910
 
803
911
  var resizeTimer;
804
912
  window.addEventListener('resize', function () {
805
913
  clearTimeout(resizeTimer);
806
914
  resizeTimer = setTimeout(function () {
807
- render();
808
- scrollToToday();
915
+ renderKeepingCenter();
809
916
  }, 150);
810
- });
917
+ }, bootScope);
811
918
 
812
919
  /* The bars carry their colour as an inline style, so a palette swap has to
813
920
  redraw them — the CSS variables alone cannot reach an already-painted
@@ -815,37 +922,129 @@
815
922
  document.addEventListener('shell-theme-change', function () {
816
923
  refreshColors();
817
924
  render();
818
- });
925
+ }, bootScope);
819
926
 
820
927
  document.querySelectorAll('.timeline-lane-toggle').forEach(function (cb) {
821
928
  cb.addEventListener('change', function () {
822
929
  laneVisible[cb.dataset.lane] = cb.checked;
823
- render();
824
- });
930
+ renderKeepingCenter();
931
+ }, bootScope);
825
932
  });
826
933
 
827
934
  var expandCb = document.getElementById('timeline-writing-expanded');
828
935
  if (expandCb) {
829
936
  expandCb.addEventListener('change', function () {
830
937
  writingExpanded = expandCb.checked;
831
- render();
832
- });
938
+ renderKeepingCenter();
939
+ }, bootScope);
833
940
  }
834
941
 
835
942
  if (selectionClear) {
836
- selectionClear.addEventListener('click', clearSelection);
943
+ selectionClear.addEventListener('click', clearSelection, bootScope);
944
+ }
945
+
946
+ function maxScrollLeft() {
947
+ return Math.max(0, container.offsetWidth - wrapper.clientWidth);
948
+ }
949
+
950
+ var reduceMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)');
951
+
952
+ function scrollBehavior() {
953
+ return reduceMotion && reduceMotion.matches ? 'auto' : 'smooth';
954
+ }
955
+
956
+ function panBy(step) {
957
+ wrapper.scrollTo({
958
+ left: panTarget(wrapper.scrollLeft, wrapper.clientWidth, maxScrollLeft(), step),
959
+ behavior: scrollBehavior()
960
+ });
961
+ }
962
+
963
+ var panPrevBtn = document.getElementById('timeline-pan-prev');
964
+ if (panPrevBtn) {
965
+ panPrevBtn.addEventListener('click', function () { panBy(-0.9); }, bootScope);
966
+ }
967
+
968
+ var panNextBtn = document.getElementById('timeline-pan-next');
969
+ if (panNextBtn) {
970
+ panNextBtn.addEventListener('click', function () { panBy(0.9); }, bootScope);
971
+ }
972
+
973
+ var panTodayBtn = document.getElementById('timeline-pan-today');
974
+ if (panTodayBtn) {
975
+ panTodayBtn.addEventListener('click', function () {
976
+ var todayX = xPosMs(todayT);
977
+ var target = Math.max(0, Math.min(maxScrollLeft(), todayX - wrapper.clientWidth + 24));
978
+ wrapper.scrollTo({ left: target, behavior: scrollBehavior() });
979
+ }, bootScope);
980
+ }
981
+
982
+ function setPanButtonDisabled(btn, disabled) {
983
+ if (!btn) return;
984
+ btn.disabled = disabled;
985
+ btn.setAttribute('aria-disabled', disabled ? 'true' : 'false');
986
+ }
987
+
988
+ function updatePanButtons() {
989
+ setPanButtonDisabled(panPrevBtn, wrapper.scrollLeft <= 0);
990
+ setPanButtonDisabled(panNextBtn, wrapper.scrollLeft >= maxScrollLeft() - 1);
991
+ }
992
+
993
+ if (wrapper) {
994
+ wrapper.addEventListener('scroll', updatePanButtons, { passive: true, signal: listeners.signal });
995
+ wrapper.addEventListener('keydown', function (e) {
996
+ if (e.target !== wrapper || e.ctrlKey || e.metaKey || e.altKey) return;
997
+ var handled = true;
998
+ switch (e.key) {
999
+ case 'ArrowLeft':
1000
+ panBy(e.shiftKey ? -0.9 : -0.1);
1001
+ break;
1002
+ case 'ArrowRight':
1003
+ panBy(e.shiftKey ? 0.9 : 0.1);
1004
+ break;
1005
+ case 'PageUp':
1006
+ panBy(-0.9);
1007
+ break;
1008
+ case 'PageDown':
1009
+ panBy(0.9);
1010
+ break;
1011
+ case 'Home':
1012
+ wrapper.scrollTo({ left: 0, behavior: scrollBehavior() });
1013
+ break;
1014
+ case 'End':
1015
+ var todayX = xPosMs(todayT);
1016
+ var target = Math.max(0, Math.min(maxScrollLeft(), todayX - wrapper.clientWidth + 24));
1017
+ wrapper.scrollTo({ left: target, behavior: scrollBehavior() });
1018
+ break;
1019
+ case '+':
1020
+ case '=':
1021
+ changeZoom(1);
1022
+ break;
1023
+ case '-':
1024
+ case '−':
1025
+ changeZoom(-1);
1026
+ break;
1027
+ default:
1028
+ handled = false;
1029
+ break;
1030
+ }
1031
+ if (handled) {
1032
+ e.preventDefault();
1033
+ }
1034
+ }, bootScope);
837
1035
  }
838
1036
 
839
1037
  if (zoomInBtn) {
840
- zoomInBtn.addEventListener('click', function () { changeZoom(1); });
1038
+ zoomInBtn.addEventListener('click', function () { changeZoom(1); }, bootScope);
841
1039
  }
842
1040
  if (zoomOutBtn) {
843
- zoomOutBtn.addEventListener('click', function () { changeZoom(-1); });
1041
+ zoomOutBtn.addEventListener('click', function () { changeZoom(-1); }, bootScope);
844
1042
  }
845
1043
 
846
1044
  updateZoomLabel();
847
1045
  render();
848
1046
  scrollToToday();
1047
+ updatePanButtons();
849
1048
  }
850
1049
 
851
1050
  function loadTimeline() {
@@ -154,7 +154,13 @@ layout: shell
154
154
  imagery still reads as finished rather than as broken. */
155
155
  function plateHtml(item, modifier) {
156
156
  var fm = item.front_matter || {};
157
- var poster = runtime.renderPosterMedia(fm.thumbnail, projectName(item));
157
+ var sizes = modifier === 'projects-plate--card'
158
+ ? '(min-width: 1024px) 28vw, (min-width: 700px) 46vw, 92vw'
159
+ : '(min-width: 900px) 38vw, 92vw';
160
+ var poster = runtime.renderPosterMedia(fm.thumbnail, projectName(item), {
161
+ srcset: fm.thumbnail_srcset,
162
+ sizes: sizes
163
+ });
158
164
  var cls = 'projects-plate' + (modifier ? ' ' + modifier : '');
159
165
  if (poster) return '<div class="' + cls + '">' + poster + '</div>';
160
166
  if (!fm.icon) return '';
data/_layouts/shell.html CHANGED
@@ -240,7 +240,7 @@
240
240
  {%- for _item in _group.pages -%}
241
241
  {%- assign _key = _item.key -%}
242
242
  {%- assign _p = _all_pages | where: "key", _key | first -%}
243
- <a href="{{ _p.url }}" class="site-rail-link{% if nav_active == _key %} is-active{% endif %}"
243
+ <a href="{{ _p.url }}" class="site-rail-link{% if nav_active == _key %} is-active{% if _p.accent and _p.accent != 'blue' %} page-accent--{{ _p.accent }}{% endif %}{% endif %}"
244
244
  {% if nav_active == _key %}aria-current="page"{% endif %}>{{ _p.title }}</a>
245
245
  {%- endfor -%}
246
246
  {%- endfor -%}