Last modified by Veronika Kocher on Friday, 04 Sep 2026 14:45

From version 5.9
edited by Veronika Kocher
on Thursday, 03 Sep 2026 9:34
Change comment: There is no comment for this version
To version 5.10
edited by Veronika Kocher
on Thursday, 03 Sep 2026 10:09
Change comment: There is no comment for this version

Summary

Details

Page properties
Content
... ... @@ -261,15 +261,104 @@
261 261  </div>
262 262  {{/html}}
263 263  
264 -{{html clean="false"}}
264 +\n\n
265 +\n\n{{html clean="false"}}
265 265  <!-- SN_HORIZONTAL_BOX_HARMONIZER_START -->
266 -<style data-sn-horizontal-box-harmonizer="4">
267 -[data-sn-equal-height-managed],
268 -[data-sn-first-section-aligned]{
267 +
268 +<style data-sn-horizontal-box-harmonizer="7">
269 +/* ---------- Typography: never justify headings ---------- */
270 +#xwikicontent h1,
271 +#xwikicontent h2,
272 +#xwikicontent h3,
273 +#xwikicontent h4,
274 +#xwikicontent h5,
275 +#xwikicontent h6,
276 +#xwikicontent .box-title,
277 +#xwikicontent [role="heading"],
278 +#xwikicontent h1 a,
279 +#xwikicontent h2 a,
280 +#xwikicontent h3 a,
281 +#xwikicontent h4 a,
282 +#xwikicontent h5 a,
283 +#xwikicontent h6 a,
284 +#xwikicontent .box-title a,
285 +#xwikicontent [role="heading"] a{
286 + text-align:left!important;
287 + text-justify:auto!important;
288 + word-spacing:normal!important;
289 +}
290 +
291 +/* ---------- Native XWiki semantic callouts ---------- */
292 +#xwikicontent .box.infomessage,
293 +#xwikicontent .infomessage,
294 +#xwikicontent .box.warningmessage,
295 +#xwikicontent .warningmessage,
296 +#xwikicontent .box.successmessage,
297 +#xwikicontent .successmessage,
298 +#xwikicontent .box.errormessage,
299 +#xwikicontent .errormessage,
300 +#xwikicontent [data-sn-callout-harmonized="1"]{
301 + box-sizing:border-box!important;
302 + border-radius:8px!important;
303 + border-top:1px solid rgba(0,0,0,.08)!important;
304 + border-right:1px solid rgba(0,0,0,.08)!important;
305 + border-bottom:1px solid rgba(0,0,0,.08)!important;
306 + border-left:4px solid var(--sn-callout-accent,#21b6d7)!important;
307 + padding:14px 16px!important;
308 + text-align:left!important;
309 + text-justify:auto!important;
310 + word-spacing:normal!important;
311 + box-shadow:none!important;
312 +}
313 +
314 +#xwikicontent .box.infomessage,
315 +#xwikicontent .infomessage{
316 + --sn-callout-accent:#21b6d7;
317 + background:#eaf7fb!important;
318 +}
319 +
320 +#xwikicontent .box.warningmessage,
321 +#xwikicontent .warningmessage{
322 + --sn-callout-accent:#f0aa00;
323 + background:#fff8e7!important;
324 +}
325 +
326 +#xwikicontent .box.successmessage,
327 +#xwikicontent .successmessage{
328 + --sn-callout-accent:#38b76a;
329 + background:#edf9f1!important;
330 +}
331 +
332 +#xwikicontent .box.errormessage,
333 +#xwikicontent .errormessage{
334 + --sn-callout-accent:#e94b13;
335 + background:#fff1ec!important;
336 +}
337 +
338 +#xwikicontent [data-sn-callout-harmonized="1"] > :first-child,
339 +#xwikicontent .infomessage > :first-child,
340 +#xwikicontent .warningmessage > :first-child,
341 +#xwikicontent .successmessage > :first-child,
342 +#xwikicontent .errormessage > :first-child{
343 + margin-top:0!important;
344 +}
345 +
346 +#xwikicontent [data-sn-callout-harmonized="1"] > :last-child,
347 +#xwikicontent .infomessage > :last-child,
348 +#xwikicontent .warningmessage > :last-child,
349 +#xwikicontent .successmessage > :last-child,
350 +#xwikicontent .errormessage > :last-child{
351 + margin-bottom:0!important;
352 +}
353 +
354 +#xwikicontent [data-sn-equal-height-managed],
355 +#xwikicontent [data-sn-first-section-aligned],
356 +#xwikicontent [data-sn-first-content-aligned]{
269 269   box-sizing:border-box;
270 270  }
271 271  </style>
272 -<script data-sn-horizontal-box-harmonizer="4">
360 +
361 +<script data-sn-horizontal-box-harmonizer="7">
273 273  (function(){
274 274   "use strict";
275 275  
... ... @@ -277,8 +277,10 @@
277 277   var ROW_TOLERANCE=6;
278 278   var MIN_W=150;
279 279   var MIN_H=36;
280 - var MAX_PARENT_CHILDREN=16;
369 + var MAX_PARENT_CHILDREN=18;
281 281   var SEARCH_DEPTH=5;
371 + var RESIZE_DEBOUNCE=220;
372 + var MIN_WIDTH_CHANGE=2;
282 282  
283 283   var root=
284 284   document.getElementById("xwikicontent")||
... ... @@ -290,10 +290,16 @@
290 290  
291 291   var heightManaged=new Set();
292 292   var sectionManaged=new Set();
384 + var contentManaged=new Set();
385 +
293 293   var originalMinHeight=new WeakMap();
294 294   var originalMarginTop=new WeakMap();
295 - var raf=0;
296 296  
389 + var resizeTimer=0;
390 + var lastWidth=-1;
391 + var hasApplied=false;
392 + var applying=false;
393 +
297 297   function num(v){
298 298   var n=parseFloat(v);
299 299   return Number.isFinite(n)?n:0;
... ... @@ -320,6 +320,13 @@
320 320   );
321 321   }
322 322  
420 + function normalizeText(text){
421 + return String(text||"")
422 + .replace(/\s+/g," ")
423 + .trim()
424 + .toUpperCase();
425 + }
426 +
323 323   function excluded(el){
324 324   return (
325 325   !el.matches||
... ... @@ -329,57 +329,84 @@
329 329   );
330 330   }
331 331  
332 - function namedLikeBox(el){
436 + /* ---------- Custom coloured callouts ---------- */
437 +
438 + function nativeCallout(el){
333 333   var c=
334 334   typeof el.className==="string"
335 - ? el.className
441 + ? el.className.toLowerCase()
336 336   : "";
337 337  
338 - return /(^|[s_-])(box|card|tile|panel|role|check|service|source|call|area|detail|resource|topic|warn|info)([s_-]|$)/i.test(c);
444 + return (
445 + /(^|\s)(infomessage|warningmessage|successmessage|errormessage)(\s|$)/.test(c)
446 + );
339 339   }
340 340  
341 - function boxLike(el){
342 - if(!visible(el,MIN_W,MIN_H)||excluded(el))return false;
449 + function customCallout(el){
450 + if(!visible(el,220,42)||excluded(el))return false;
343 343  
344 - var s=getComputedStyle(el);
345 345   var r=el.getBoundingClientRect();
346 346  
347 - var border=
348 - num(s.borderTopWidth)+
349 - num(s.borderRightWidth)+
350 - num(s.borderBottomWidth)+
351 - num(s.borderLeftWidth);
454 + if(r.height>520)return false;
352 352  
353 - var radius=Math.max(
354 - num(s.borderTopLeftRadius),
355 - num(s.borderTopRightRadius),
356 - num(s.borderBottomLeftRadius),
357 - num(s.borderBottomRightRadius)
358 - );
456 + var s=getComputedStyle(el);
359 359  
360 360   var bg=
361 361   s.backgroundColor&&
362 362   s.backgroundColor!=="rgba(0, 0, 0, 0)"&&
363 - s.backgroundColor!=="transparent";
461 + s.backgroundColor!=="transparent"&&
462 + s.backgroundColor!=="rgb(255, 255, 255)";
364 364  
365 - var shadow=
366 - s.boxShadow&&
367 - s.boxShadow!=="none";
464 + var left=
465 + num(s.borderLeftWidth);
368 368  
369 369   return (
370 - namedLikeBox(el)||
371 - border>=1||
372 - (bg&&radius>=3)||
373 - (shadow&&r.width>=180)
468 + bg&&
469 + left>=2
374 374   );
375 375   }
376 376  
473 + function harmoniseCustomCallouts(){
474 + Array.from(
475 + root.querySelectorAll(
476 + "div,aside,section"
477 + )
478 + ).forEach(function(el){
479 + if(nativeCallout(el))return;
480 + if(!customCallout(el))return;
481 +
482 + var s=getComputedStyle(el);
483 +
484 + el.setAttribute(
485 + "data-sn-callout-harmonized",
486 + "1"
487 + );
488 +
489 + if(
490 + s.borderLeftColor
491 + ){
492 + el.style.setProperty(
493 + "--sn-callout-accent",
494 + s.borderLeftColor
495 + );
496 + }
497 + });
498 + }
499 +
500 + /* ---------- Restore only values V7 itself changed ---------- */
501 +
377 377   function saveOriginal(el,map,property){
378 378   if(map.has(el))return;
379 379  
380 380   map.set(el,{
381 - value:el.style.getPropertyValue(property),
382 - priority:el.style.getPropertyPriority(property)
506 + value:
507 + el.style.getPropertyValue(
508 + property
509 + ),
510 + priority:
511 + el.style.getPropertyPriority(
512 + property
513 + )
383 383   });
384 384   }
385 385  
... ... @@ -393,11 +393,13 @@
393 393   original.priority||""
394 394   );
395 395   }else{
396 - el.style.removeProperty(property);
527 + el.style.removeProperty(
528 + property
529 + );
397 397   }
398 398   }
399 399  
400 - function reset(){
533 + function resetRuntime(){
401 401   heightManaged.forEach(function(el){
402 402   restoreOriginal(
403 403   el,
... ... @@ -422,16 +422,79 @@
422 422   );
423 423   });
424 424  
558 + contentManaged.forEach(function(el){
559 + restoreOriginal(
560 + el,
561 + originalMarginTop,
562 + "margin-top"
563 + );
564 +
565 + el.removeAttribute(
566 + "data-sn-first-content-aligned"
567 + );
568 + });
569 +
425 425   heightManaged.clear();
426 426   sectionManaged.clear();
572 + contentManaged.clear();
427 427   }
428 428  
575 + /* ---------- Card detection ---------- */
576 +
577 + function namedLikeBox(el){
578 + var c=
579 + typeof el.className==="string"
580 + ? el.className
581 + : "";
582 +
583 + return /(^|[\s_-])(box|card|tile|panel|role|check|service|source|call|area|detail|resource|topic|warn|info)([\s_-]|$)/i.test(c);
584 + }
585 +
586 + function boxLike(el){
587 + if(
588 + !visible(el,MIN_W,MIN_H)||
589 + excluded(el)
590 + ){
591 + return false;
592 + }
593 +
594 + var s=getComputedStyle(el);
595 + var r=el.getBoundingClientRect();
596 +
597 + var border=
598 + num(s.borderTopWidth)+
599 + num(s.borderRightWidth)+
600 + num(s.borderBottomWidth)+
601 + num(s.borderLeftWidth);
602 +
603 + var radius=Math.max(
604 + num(s.borderTopLeftRadius),
605 + num(s.borderTopRightRadius),
606 + num(s.borderBottomLeftRadius),
607 + num(s.borderBottomRightRadius)
608 + );
609 +
610 + var bg=
611 + s.backgroundColor&&
612 + s.backgroundColor!=="rgba(0, 0, 0, 0)"&&
613 + s.backgroundColor!=="transparent";
614 +
615 + var shadow=
616 + s.boxShadow&&
617 + s.boxShadow!=="none";
618 +
619 + return (
620 + namedLikeBox(el)||
621 + border>=1||
622 + (bg&&radius>=3)||
623 + (shadow&&r.width>=180)
624 + );
625 + }
626 +
429 429   function pickTarget(child){
430 430   if(!visible(child,MIN_W,MIN_H))return null;
431 431  
432 - if(boxLike(child)){
433 - return child;
434 - }
630 + if(boxLike(child))return child;
435 435  
436 436   var cr=child.getBoundingClientRect();
437 437   var q=[];
... ... @@ -493,7 +493,7 @@
493 493   return best;
494 494   }
495 495  
496 - function groupsByVisualRow(entries){
692 + function groupRows(entries){
497 497   var groups=[];
498 498  
499 499   entries
... ... @@ -533,7 +533,7 @@
533 533   return groups;
534 534   }
535 535  
536 - function validHorizontalGroup(group){
732 + function validRow(group){
537 537   if(group.entries.length<2)return false;
538 538  
539 539   var lefts=
... ... @@ -556,25 +556,26 @@
556 556  
557 557   if(widths.length<2)return false;
558 558  
559 - var minWidth=Math.min.apply(Math,widths);
560 - var maxWidth=Math.max.apply(Math,widths);
755 + var minW=
756 + Math.min.apply(Math,widths);
561 561  
758 + var maxW=
759 + Math.max.apply(Math,widths);
760 +
562 562   return (
563 - maxWidth/
564 - Math.max(1,minWidth)<=
762 + maxW/
763 + Math.max(1,minW)<=
565 565   1.9
566 566   );
567 567   }
568 568  
569 - function normalizeText(text){
570 - return String(text||"")
571 - .replace(/s+/g," ")
572 - .trim()
573 - .toUpperCase();
574 - }
768 + /* ---------- Known ARSN first section bars ---------- */
575 575  
576 - function exactKnownFirstSection(el){
577 - var text=normalizeText(el.textContent);
770 + function knownBar(el){
771 + var text=
772 + normalizeText(
773 + el.textContent
774 + );
578 578  
579 579   return (
580 580   text==="HIER STARTEN"||
... ... @@ -588,150 +588,274 @@
588 588   );
589 589   }
590 590  
591 - function visuallyFirstSectionBar(card){
592 - var cardRect=card.getBoundingClientRect();
593 - var candidates=[];
788 + function findKnownBar(card){
789 + var cardRect=
790 + card.getBoundingClientRect();
594 594  
595 - Array.from(
596 - card.querySelectorAll(
597 - "div,p,span,strong,h3,h4,h5,h6"
792 + var rows=
793 + Array.from(
794 + card.querySelectorAll(
795 + "div,p,span,strong,h3,h4,h5,h6"
796 + )
598 598   )
599 - ).forEach(function(el){
600 - if(!visible(el,20,6))return;
601 - if(excluded(el))return;
798 + .filter(function(el){
799 + if(!visible(el,20,6))return false;
800 + if(!knownBar(el))return false;
602 602  
603 - var r=el.getBoundingClientRect();
604 - var relativeTop=r.top-cardRect.top;
802 + var r=el.getBoundingClientRect();
605 605  
606 - if(
607 - relativeTop<45||
608 - relativeTop>cardRect.height*.72
609 - ){
610 - return;
611 - }
804 + var rel=
805 + r.top-
806 + cardRect.top;
612 612  
613 - var widthRatio=
614 - r.width/
615 - Math.max(1,cardRect.width);
808 + return (
809 + rel>=35&&
810 + rel<=
811 + cardRect.height*.75
812 + );
813 + })
814 + .sort(function(a,b){
815 + return (
816 + a.getBoundingClientRect().top-
817 + b.getBoundingClientRect().top
818 + );
819 + });
616 616  
617 - if(
618 - widthRatio<.55||
619 - widthRatio>1.05||
620 - r.height>54
621 - ){
622 - return;
623 - }
821 + return rows[0]||null;
822 + }
624 624  
625 - var text=normalizeText(el.textContent);
824 + function alignKnownBars(cards){
825 + var rows=
826 + cards.map(function(card){
827 + var bar=
828 + findKnownBar(card);
626 626  
627 - if(
628 - text.length<2||
629 - text.length>90
630 - ){
631 - return;
632 - }
830 + if(!bar)return null;
633 633  
634 - var style=getComputedStyle(el);
832 + var cr=
833 + card.getBoundingClientRect();
635 635  
636 - var bg=
637 - style.backgroundColor&&
638 - style.backgroundColor!=="rgba(0, 0, 0, 0)"&&
639 - style.backgroundColor!=="transparent";
835 + var br=
836 + bar.getBoundingClientRect();
640 640  
641 - var border=
642 - num(style.borderTopWidth)+
643 - num(style.borderRightWidth)+
644 - num(style.borderBottomWidth)+
645 - num(style.borderLeftWidth);
838 + return {
839 + bar:bar,
840 + relativeTop:
841 + br.top-
842 + cr.top
843 + };
844 + });
646 646  
647 - var mostlyUpper=
648 - text.length>=3&&
649 - text===text.toUpperCase();
846 + if(
847 + rows.some(function(row){
848 + return !row;
849 + })
850 + ){
851 + return false;
852 + }
650 650  
651 - var exact=exactKnownFirstSection(el);
854 + var target=
855 + Math.max.apply(
856 + Math,
857 + rows.map(function(row){
858 + return row.relativeTop;
859 + })
860 + );
652 652  
653 - var score=
654 - (exact?20:0)+
655 - (bg?6:0)+
656 - (border>=1?2:0)+
657 - (mostlyUpper?4:0)+
658 - (num(style.fontWeight)>=600?1:0);
862 + rows.forEach(function(row){
863 + var delta=
864 + target-
865 + row.relativeTop;
659 659  
660 - if(score>=7){
661 - candidates.push({
662 - el:el,
663 - relativeTop:relativeTop,
664 - width:r.width,
665 - score:score
666 - });
667 - }
668 - });
867 + if(delta<=1)return;
669 669  
670 - if(!candidates.length)return null;
869 + saveOriginal(
870 + row.bar,
871 + originalMarginTop,
872 + "margin-top"
873 + );
671 671  
672 - candidates.sort(function(a,b){
673 - if(a.score!==b.score)return b.score-a.score;
674 - if(Math.abs(a.relativeTop-b.relativeTop)>3){
675 - return a.relativeTop-b.relativeTop;
676 - }
677 - return b.width-a.width;
678 - });
875 + var current=
876 + num(
877 + getComputedStyle(
878 + row.bar
879 + ).marginTop
880 + );
679 679  
680 - // For exact known labels always prefer the earliest exact match.
681 - var exacts=candidates.filter(function(c){
682 - return exactKnownFirstSection(c.el);
882 + row.bar.style.setProperty(
883 + "margin-top",
884 + (
885 + current+
886 + delta
887 + )+"px"
888 + );
889 +
890 + row.bar.setAttribute(
891 + "data-sn-first-section-aligned",
892 + "7"
893 + );
894 +
895 + sectionManaged.add(
896 + row.bar
897 + );
683 683   });
684 684  
685 - if(exacts.length){
686 - exacts.sort(function(a,b){
687 - return a.relativeTop-b.relativeTop||b.width-a.width;
900 + return true;
901 + }
902 +
903 + /* ---------- Generic first content after heading ---------- */
904 +
905 + function heading(card){
906 + var cr=
907 + card.getBoundingClientRect();
908 +
909 + var rows=
910 + Array.from(
911 + card.querySelectorAll(
912 + "h1,h2,h3,h4,h5,h6,.box-title,[role=heading]"
913 + )
914 + )
915 + .filter(function(el){
916 + if(!visible(el,20,8))return false;
917 +
918 + var r=el.getBoundingClientRect();
919 +
920 + var rel=
921 + r.top-
922 + cr.top;
923 +
924 + return (
925 + rel>=0&&
926 + rel<=
927 + Math.min(
928 + 160,
929 + cr.height*.45
930 + )
931 + );
932 + })
933 + .sort(function(a,b){
934 + return (
935 + a.getBoundingClientRect().top-
936 + b.getBoundingClientRect().top
937 + );
688 688   });
689 - return exacts[0].el;
690 - }
691 691  
692 - // Generic fallback: earliest high-confidence bar.
693 - candidates.sort(function(a,b){
694 - return a.relativeTop-b.relativeTop||b.score-a.score||b.width-a.width;
695 - });
940 + return rows[0]||null;
941 + }
696 696  
697 - return candidates[0].el;
943 + function firstContent(card,h){
944 + if(!h)return null;
945 +
946 + var cr=
947 + card.getBoundingClientRect();
948 +
949 + var hr=
950 + h.getBoundingClientRect();
951 +
952 + var rows=
953 + Array.from(
954 + card.querySelectorAll(
955 + "p,div,ul,ol,dl,blockquote"
956 + )
957 + )
958 + .filter(function(el){
959 + if(
960 + el===h||
961 + h.contains(el)||
962 + el.contains(h)
963 + ){
964 + return false;
965 + }
966 +
967 + if(!visible(el,30,8))return false;
968 +
969 + var text=
970 + String(el.textContent||"")
971 + .replace(/\s+/g," ")
972 + .trim();
973 +
974 + if(text.length<3)return false;
975 +
976 + var r=el.getBoundingClientRect();
977 +
978 + if(
979 + r.top<
980 + hr.bottom-1
981 + ){
982 + return false;
983 + }
984 +
985 + if(
986 + r.top-cr.top>
987 + Math.min(
988 + 280,
989 + cr.height*.65
990 + )
991 + ){
992 + return false;
993 + }
994 +
995 + if(
996 + r.height>
997 + cr.height*.58
998 + ){
999 + return false;
1000 + }
1001 +
1002 + return true;
1003 + })
1004 + .sort(function(a,b){
1005 + var ar=
1006 + a.getBoundingClientRect();
1007 +
1008 + var br=
1009 + b.getBoundingClientRect();
1010 +
1011 + return (
1012 + ar.top-br.top||
1013 + ar.height-br.height
1014 + );
1015 + });
1016 +
1017 + return rows[0]||null;
698 698   }
699 699  
700 - function alignFirstSections(cards){
1020 + function alignGeneric(cards){
701 701   var rows=
702 702   cards.map(function(card){
703 - var bar=
704 - visuallyFirstSectionBar(card);
1023 + var h=
1024 + heading(card);
705 705  
706 - if(!bar)return null;
1026 + var c=
1027 + firstContent(
1028 + card,
1029 + h
1030 + );
707 707  
708 - var cardRect=
1032 + if(!h||!c)return null;
1033 +
1034 + var cr=
709 709   card.getBoundingClientRect();
710 710  
711 - var barRect=
712 - bar.getBoundingClientRect();
1037 + var rr=
1038 + c.getBoundingClientRect();
713 713  
714 714   return {
715 - card:card,
716 - bar:bar,
1041 + content:c,
717 717   relativeTop:
718 - barRect.top-cardRect.top,
719 - label:
720 - normalizeText(bar.textContent)
1043 + rr.top-cr.top
721 721   };
722 722   });
723 723  
724 - // Conservative: only align when every card in the horizontal row
725 - // has a reliable first section bar.
726 726   if(
727 727   rows.some(function(row){
728 728   return !row;
729 729   })
730 730   ){
731 - return null;
1052 + return false;
732 732   }
733 733  
734 - var targetTop=
1055 + var target=
735 735   Math.max.apply(
736 736   Math,
737 737   rows.map(function(row){
... ... @@ -741,54 +741,47 @@
741 741  
742 742   rows.forEach(function(row){
743 743   var delta=
744 - targetTop-
1065 + target-
745 745   row.relativeTop;
746 746  
747 747   if(delta<=1)return;
748 748  
749 749   saveOriginal(
750 - row.bar,
1071 + row.content,
751 751   originalMarginTop,
752 752   "margin-top"
753 753   );
754 754  
755 - var currentMargin=
1076 + var current=
756 756   num(
757 757   getComputedStyle(
758 - row.bar
1079 + row.content
759 759   ).marginTop
760 760   );
761 761  
762 - row.bar.style.setProperty(
1083 + row.content.style.setProperty(
763 763   "margin-top",
764 764   (
765 - currentMargin+
1086 + current+
766 766   delta
767 767   )+"px"
768 768   );
769 769  
770 - row.bar.setAttribute(
771 - "data-sn-first-section-aligned",
772 - "4"
1091 + row.content.setAttribute(
1092 + "data-sn-first-content-aligned",
1093 + "7"
773 773   );
774 774  
775 - sectionManaged.add(row.bar);
1096 + contentManaged.add(
1097 + row.content
1098 + );
776 776   });
777 777  
778 - return {
779 - labels:
780 - rows.map(function(row){
781 - return row.label;
782 - }),
783 - targetRelativeTop:
784 - Math.round(targetTop)
785 - };
1101 + return true;
786 786   }
787 787  
788 - function applyGroup(group){
789 - if(!validHorizontalGroup(group)){
790 - return null;
791 - }
1104 + function applyRow(group){
1105 + if(!validRow(group))return false;
792 792  
793 793   var cards=[];
794 794   var seen=new Set();
... ... @@ -800,25 +800,30 @@
800 800   }
801 801   });
802 802  
803 - if(cards.length<2)return null;
1117 + if(cards.length<2)return false;
804 804  
805 - var firstSection=
806 - alignFirstSections(cards);
1119 + if(
1120 + !alignKnownBars(cards)
1121 + ){
1122 + alignGeneric(cards);
1123 + }
807 807  
808 - // The section alignment can change natural card heights.
809 - // Measure only afterwards.
810 810   var maxHeight=0;
811 811  
812 812   cards.forEach(function(card){
813 - maxHeight=Math.max(
814 - maxHeight,
815 - card.getBoundingClientRect().height
816 - );
1128 + maxHeight=
1129 + Math.max(
1130 + maxHeight,
1131 + card.getBoundingClientRect().height
1132 + );
817 817   });
818 818  
819 - maxHeight=Math.ceil(maxHeight);
1135 + maxHeight=
1136 + Math.ceil(
1137 + maxHeight
1138 + );
820 820  
821 - if(maxHeight<MIN_H)return null;
1140 + if(maxHeight<MIN_H)return false;
822 822  
823 823   cards.forEach(function(card){
824 824   saveOriginal(
... ... @@ -834,125 +834,228 @@
834 834  
835 835   card.setAttribute(
836 836   "data-sn-equal-height-managed",
837 - "4"
1156 + "7"
838 838   );
839 839  
840 840   heightManaged.add(card);
841 841   });
842 842  
843 - return {
844 - cards:cards.length,
845 - height:maxHeight,
846 - firstSection:firstSection
847 - };
1162 + return true;
848 848   }
849 849  
850 - function run(){
851 - reset();
1165 + function calculate(force){
1166 + if(applying)return;
852 852  
1168 + var width=
1169 + Math.round(
1170 + window.innerWidth
1171 + );
1172 +
1173 + var meaningful=
1174 + lastWidth<0||
1175 + Math.abs(
1176 + width-
1177 + lastWidth
1178 + )>=
1179 + MIN_WIDTH_CHANGE;
1180 +
853 853   if(
854 - window.innerWidth<=MOBILE_MAX
1182 + hasApplied&&
1183 + !force&&
1184 + !meaningful
855 855   ){
856 856   return;
857 857   }
858 858  
859 - var parents=
860 - Array.from(
861 - root.querySelectorAll("*")
862 - ).filter(function(parent){
863 - return (
864 - visible(parent,MIN_W,MIN_H)&&
865 - parent.children&&
866 - parent.children.length>=2&&
867 - parent.children.length<=MAX_PARENT_CHILDREN
868 - );
869 - });
1189 + applying=true;
870 870  
871 - var touched=new Set();
1191 + try{
1192 + harmoniseCustomCallouts();
872 872  
873 - parents.forEach(function(parent){
874 - var entries=[];
1194 + if(width<=MOBILE_MAX){
1195 + if(hasApplied){
1196 + resetRuntime();
1197 + }
875 875  
876 - Array.from(parent.children).forEach(function(child){
877 - if(!visible(child,MIN_W,MIN_H))return;
1199 + hasApplied=false;
1200 + lastWidth=width;
1201 + return;
1202 + }
878 878  
879 - var target=pickTarget(child);
880 - if(!target)return;
1204 + if(hasApplied){
1205 + resetRuntime();
1206 + }
881 881  
882 - entries.push({
883 - target:target,
884 - rect:target.getBoundingClientRect()
1208 + var parents=
1209 + Array.from(
1210 + root.querySelectorAll("*")
1211 + )
1212 + .filter(function(parent){
1213 + return (
1214 + visible(
1215 + parent,
1216 + MIN_W,
1217 + MIN_H
1218 + )&&
1219 + parent.children&&
1220 + parent.children.length>=2&&
1221 + parent.children.length<=
1222 + MAX_PARENT_CHILDREN
1223 + );
885 885   });
886 - });
887 887  
888 - if(entries.length<2)return;
1226 + var touched=
1227 + new Set();
889 889  
890 - groupsByVisualRow(entries).forEach(function(group){
891 - if(!validHorizontalGroup(group))return;
1229 + parents.forEach(function(parent){
1230 + var entries=[];
892 892  
893 - var signature=
894 - group.entries.map(function(entry){
895 - return entry.target;
896 - });
1232 + Array.from(
1233 + parent.children
1234 + ).forEach(function(child){
1235 + if(
1236 + !visible(
1237 + child,
1238 + MIN_W,
1239 + MIN_H
1240 + )
1241 + ){
1242 + return;
1243 + }
897 897  
898 - if(
899 - !signature.some(function(el){
900 - return !touched.has(el);
901 - })
902 - ){
903 - return;
904 - }
1245 + var target=
1246 + pickTarget(child);
905 905  
906 - var applied=
907 - applyGroup(group);
1248 + if(!target)return;
908 908  
909 - if(applied){
910 - signature.forEach(function(el){
911 - touched.add(el);
1250 + entries.push({
1251 + target:target,
1252 + rect:
1253 + target.getBoundingClientRect()
912 912   });
913 - }
1255 + });
1256 +
1257 + groupRows(
1258 + entries
1259 + ).forEach(function(group){
1260 + if(!validRow(group))return;
1261 +
1262 + var targets=
1263 + group.entries.map(function(entry){
1264 + return entry.target;
1265 + });
1266 +
1267 + if(
1268 + !targets.some(function(el){
1269 + return !touched.has(el);
1270 + })
1271 + ){
1272 + return;
1273 + }
1274 +
1275 + if(applyRow(group)){
1276 + targets.forEach(function(el){
1277 + touched.add(el);
1278 + });
1279 + }
1280 + });
914 914   });
915 - });
1282 +
1283 + lastWidth=width;
1284 + hasApplied=true;
1285 + }finally{
1286 + applying=false;
1287 + }
916 916   }
917 917  
918 - function schedule(){
919 - if(raf)cancelAnimationFrame(raf);
1290 + function resize(){
1291 + if(resizeTimer){
1292 + clearTimeout(
1293 + resizeTimer
1294 + );
1295 + }
920 920  
921 - raf=requestAnimationFrame(function(){
922 - raf=requestAnimationFrame(function(){
923 - raf=0;
924 - run();
925 - });
926 - });
1297 + resizeTimer=
1298 + setTimeout(
1299 + function(){
1300 + resizeTimer=0;
1301 + calculate(false);
1302 + },
1303 + RESIZE_DEBOUNCE
1304 + );
927 927   }
928 928  
929 - addEventListener("resize",schedule,{passive:true});
930 - addEventListener("load",schedule,{once:true});
1307 + function waitImages(){
1308 + var images=
1309 + Array.from(
1310 + root.querySelectorAll("img")
1311 + );
931 931  
932 - root.addEventListener(
933 - "click",
934 - function(){
935 - setTimeout(schedule,0);
936 - },
937 - true
938 - );
1313 + return Promise.all(
1314 + images.map(function(img){
1315 + if(img.complete){
1316 + return Promise.resolve();
1317 + }
939 939  
940 - Array.from(root.querySelectorAll("img")).forEach(function(img){
941 - if(!img.complete){
942 - img.addEventListener("load",schedule,{once:true});
1319 + return new Promise(function(resolve){
1320 + var done=function(){
1321 + resolve();
1322 + };
1323 +
1324 + img.addEventListener(
1325 + "load",
1326 + done,
1327 + {once:true}
1328 + );
1329 +
1330 + img.addEventListener(
1331 + "error",
1332 + done,
1333 + {once:true}
1334 + );
1335 + });
1336 + })
1337 + );
1338 + }
1339 +
1340 + async function initial(){
1341 + if(
1342 + document.readyState!==
1343 + "complete"
1344 + ){
1345 + await new Promise(function(resolve){
1346 + addEventListener(
1347 + "load",
1348 + resolve,
1349 + {once:true}
1350 + );
1351 + });
943 943   }
944 - });
945 945  
946 - if(document.fonts&&document.fonts.ready){
947 - document.fonts.ready.then(schedule);
1354 + if(
1355 + document.fonts&&
1356 + document.fonts.ready
1357 + ){
1358 + try{
1359 + await document.fonts.ready;
1360 + }catch(error){}
1361 + }
1362 +
1363 + await waitImages();
1364 +
1365 + // Exactly one initial layout calculation.
1366 + calculate(true);
948 948   }
949 949  
950 - schedule();
951 - setTimeout(schedule,200);
952 - setTimeout(schedule,650);
953 - setTimeout(schedule,1400);
1369 + addEventListener(
1370 + "resize",
1371 + resize,
1372 + {passive:true}
1373 + );
1374 +
1375 + initial();
954 954  })();
955 955  </script>
956 -<!-- SN_HORIZONTAL_BOX_HARMONIZER_END -->
957 -{{/html}}
958 958  
1379 +<!-- SN_HORIZONTAL_BOX_HARMONIZER_END -->
1380 +{{/html}}\n