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

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

Summary

Details

Page properties
Content
... ... @@ -261,104 +261,15 @@
261 261  </div>
262 262  {{/html}}
263 263  
264 -\n\n
265 -\n\n{{html clean="false"}}
264 +{{html clean="false"}}
266 266  <!-- SN_HORIZONTAL_BOX_HARMONIZER_START -->
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]{
266 +<style data-sn-horizontal-box-harmonizer="4">
267 +[data-sn-equal-height-managed],
268 +[data-sn-first-section-aligned]{
357 357   box-sizing:border-box;
358 358  }
359 359  </style>
360 -
361 -<script data-sn-horizontal-box-harmonizer="7">
272 +<script data-sn-horizontal-box-harmonizer="4">
362 362  (function(){
363 363   "use strict";
364 364  
... ... @@ -366,10 +366,8 @@
366 366   var ROW_TOLERANCE=6;
367 367   var MIN_W=150;
368 368   var MIN_H=36;
369 - var MAX_PARENT_CHILDREN=18;
280 + var MAX_PARENT_CHILDREN=16;
370 370   var SEARCH_DEPTH=5;
371 - var RESIZE_DEBOUNCE=220;
372 - var MIN_WIDTH_CHANGE=2;
373 373  
374 374   var root=
375 375   document.getElementById("xwikicontent")||
... ... @@ -381,16 +381,10 @@
381 381  
382 382   var heightManaged=new Set();
383 383   var sectionManaged=new Set();
384 - var contentManaged=new Set();
385 -
386 386   var originalMinHeight=new WeakMap();
387 387   var originalMarginTop=new WeakMap();
295 + var raf=0;
388 388  
389 - var resizeTimer=0;
390 - var lastWidth=-1;
391 - var hasApplied=false;
392 - var applying=false;
393 -
394 394   function num(v){
395 395   var n=parseFloat(v);
396 396   return Number.isFinite(n)?n:0;
... ... @@ -417,13 +417,6 @@
417 417   );
418 418   }
419 419  
420 - function normalizeText(text){
421 - return String(text||"")
422 - .replace(/\s+/g," ")
423 - .trim()
424 - .toUpperCase();
425 - }
426 -
427 427   function excluded(el){
428 428   return (
429 429   !el.matches||
... ... @@ -433,84 +433,57 @@
433 433   );
434 434   }
435 435  
436 - /* ---------- Custom coloured callouts ---------- */
437 -
438 - function nativeCallout(el){
332 + function namedLikeBox(el){
439 439   var c=
440 440   typeof el.className==="string"
441 - ? el.className.toLowerCase()
335 + ? el.className
442 442   : "";
443 443  
444 - return (
445 - /(^|\s)(infomessage|warningmessage|successmessage|errormessage)(\s|$)/.test(c)
446 - );
338 + return /(^|[s_-])(box|card|tile|panel|role|check|service|source|call|area|detail|resource|topic|warn|info)([s_-]|$)/i.test(c);
447 447   }
448 448  
449 - function customCallout(el){
450 - if(!visible(el,220,42)||excluded(el))return false;
341 + function boxLike(el){
342 + if(!visible(el,MIN_W,MIN_H)||excluded(el))return false;
451 451  
344 + var s=getComputedStyle(el);
452 452   var r=el.getBoundingClientRect();
453 453  
454 - if(r.height>520)return false;
347 + var border=
348 + num(s.borderTopWidth)+
349 + num(s.borderRightWidth)+
350 + num(s.borderBottomWidth)+
351 + num(s.borderLeftWidth);
455 455  
456 - var s=getComputedStyle(el);
353 + var radius=Math.max(
354 + num(s.borderTopLeftRadius),
355 + num(s.borderTopRightRadius),
356 + num(s.borderBottomLeftRadius),
357 + num(s.borderBottomRightRadius)
358 + );
457 457  
458 458   var bg=
459 459   s.backgroundColor&&
460 460   s.backgroundColor!=="rgba(0, 0, 0, 0)"&&
461 - s.backgroundColor!=="transparent"&&
462 - s.backgroundColor!=="rgb(255, 255, 255)";
363 + s.backgroundColor!=="transparent";
463 463  
464 - var left=
465 - num(s.borderLeftWidth);
365 + var shadow=
366 + s.boxShadow&&
367 + s.boxShadow!=="none";
466 466  
467 467   return (
468 - bg&&
469 - left>=2
370 + namedLikeBox(el)||
371 + border>=1||
372 + (bg&&radius>=3)||
373 + (shadow&&r.width>=180)
470 470   );
471 471   }
472 472  
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 -
502 502   function saveOriginal(el,map,property){
503 503   if(map.has(el))return;
504 504  
505 505   map.set(el,{
506 - value:
507 - el.style.getPropertyValue(
508 - property
509 - ),
510 - priority:
511 - el.style.getPropertyPriority(
512 - property
513 - )
381 + value:el.style.getPropertyValue(property),
382 + priority:el.style.getPropertyPriority(property)
514 514   });
515 515   }
516 516  
... ... @@ -524,13 +524,11 @@
524 524   original.priority||""
525 525   );
526 526   }else{
527 - el.style.removeProperty(
528 - property
529 - );
396 + el.style.removeProperty(property);
530 530   }
531 531   }
532 532  
533 - function resetRuntime(){
400 + function reset(){
534 534   heightManaged.forEach(function(el){
535 535   restoreOriginal(
536 536   el,
... ... @@ -555,79 +555,16 @@
555 555   );
556 556   });
557 557  
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 -
570 570   heightManaged.clear();
571 571   sectionManaged.clear();
572 - contentManaged.clear();
573 573   }
574 574  
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 -
627 627   function pickTarget(child){
628 628   if(!visible(child,MIN_W,MIN_H))return null;
629 629  
630 - if(boxLike(child))return child;
432 + if(boxLike(child)){
433 + return child;
434 + }
631 631  
632 632   var cr=child.getBoundingClientRect();
633 633   var q=[];
... ... @@ -689,7 +689,7 @@
689 689   return best;
690 690   }
691 691  
692 - function groupRows(entries){
496 + function groupsByVisualRow(entries){
693 693   var groups=[];
694 694  
695 695   entries
... ... @@ -729,7 +729,7 @@
729 729   return groups;
730 730   }
731 731  
732 - function validRow(group){
536 + function validHorizontalGroup(group){
733 733   if(group.entries.length<2)return false;
734 734  
735 735   var lefts=
... ... @@ -752,26 +752,25 @@
752 752  
753 753   if(widths.length<2)return false;
754 754  
755 - var minW=
756 - Math.min.apply(Math,widths);
559 + var minWidth=Math.min.apply(Math,widths);
560 + var maxWidth=Math.max.apply(Math,widths);
757 757  
758 - var maxW=
759 - Math.max.apply(Math,widths);
760 -
761 761   return (
762 - maxW/
763 - Math.max(1,minW)<=
563 + maxWidth/
564 + Math.max(1,minWidth)<=
764 764   1.9
765 765   );
766 766   }
767 767  
768 - /* ---------- Known ARSN first section bars ---------- */
569 + function normalizeText(text){
570 + return String(text||"")
571 + .replace(/s+/g," ")
572 + .trim()
573 + .toUpperCase();
574 + }
769 769  
770 - function knownBar(el){
771 - var text=
772 - normalizeText(
773 - el.textContent
774 - );
576 + function exactKnownFirstSection(el){
577 + var text=normalizeText(el.textContent);
775 775  
776 776   return (
777 777   text==="HIER STARTEN"||
... ... @@ -785,274 +785,150 @@
785 785   );
786 786   }
787 787  
788 - function findKnownBar(card){
789 - var cardRect=
790 - card.getBoundingClientRect();
591 + function visuallyFirstSectionBar(card){
592 + var cardRect=card.getBoundingClientRect();
593 + var candidates=[];
791 791  
792 - var rows=
793 - Array.from(
794 - card.querySelectorAll(
795 - "div,p,span,strong,h3,h4,h5,h6"
796 - )
595 + Array.from(
596 + card.querySelectorAll(
597 + "div,p,span,strong,h3,h4,h5,h6"
797 797   )
798 - .filter(function(el){
799 - if(!visible(el,20,6))return false;
800 - if(!knownBar(el))return false;
599 + ).forEach(function(el){
600 + if(!visible(el,20,6))return;
601 + if(excluded(el))return;
801 801  
802 - var r=el.getBoundingClientRect();
603 + var r=el.getBoundingClientRect();
604 + var relativeTop=r.top-cardRect.top;
803 803  
804 - var rel=
805 - r.top-
806 - cardRect.top;
606 + if(
607 + relativeTop<45||
608 + relativeTop>cardRect.height*.72
609 + ){
610 + return;
611 + }
807 807  
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 - });
613 + var widthRatio=
614 + r.width/
615 + Math.max(1,cardRect.width);
820 820  
821 - return rows[0]||null;
822 - }
617 + if(
618 + widthRatio<.55||
619 + widthRatio>1.05||
620 + r.height>54
621 + ){
622 + return;
623 + }
823 823  
824 - function alignKnownBars(cards){
825 - var rows=
826 - cards.map(function(card){
827 - var bar=
828 - findKnownBar(card);
625 + var text=normalizeText(el.textContent);
829 829  
830 - if(!bar)return null;
627 + if(
628 + text.length<2||
629 + text.length>90
630 + ){
631 + return;
632 + }
831 831  
832 - var cr=
833 - card.getBoundingClientRect();
634 + var style=getComputedStyle(el);
834 834  
835 - var br=
836 - bar.getBoundingClientRect();
636 + var bg=
637 + style.backgroundColor&&
638 + style.backgroundColor!=="rgba(0, 0, 0, 0)"&&
639 + style.backgroundColor!=="transparent";
837 837  
838 - return {
839 - bar:bar,
840 - relativeTop:
841 - br.top-
842 - cr.top
843 - };
844 - });
641 + var border=
642 + num(style.borderTopWidth)+
643 + num(style.borderRightWidth)+
644 + num(style.borderBottomWidth)+
645 + num(style.borderLeftWidth);
845 845  
846 - if(
847 - rows.some(function(row){
848 - return !row;
849 - })
850 - ){
851 - return false;
852 - }
647 + var mostlyUpper=
648 + text.length>=3&&
649 + text===text.toUpperCase();
853 853  
854 - var target=
855 - Math.max.apply(
856 - Math,
857 - rows.map(function(row){
858 - return row.relativeTop;
859 - })
860 - );
651 + var exact=exactKnownFirstSection(el);
861 861  
862 - rows.forEach(function(row){
863 - var delta=
864 - target-
865 - row.relativeTop;
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);
866 866  
867 - if(delta<=1)return;
660 + if(score>=7){
661 + candidates.push({
662 + el:el,
663 + relativeTop:relativeTop,
664 + width:r.width,
665 + score:score
666 + });
667 + }
668 + });
868 868  
869 - saveOriginal(
870 - row.bar,
871 - originalMarginTop,
872 - "margin-top"
873 - );
670 + if(!candidates.length)return null;
874 874  
875 - var current=
876 - num(
877 - getComputedStyle(
878 - row.bar
879 - ).marginTop
880 - );
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 + });
881 881  
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 - );
680 + // For exact known labels always prefer the earliest exact match.
681 + var exacts=candidates.filter(function(c){
682 + return exactKnownFirstSection(c.el);
898 898   });
899 899  
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 - );
685 + if(exacts.length){
686 + exacts.sort(function(a,b){
687 + return a.relativeTop-b.relativeTop||b.width-a.width;
938 938   });
689 + return exacts[0].el;
690 + }
939 939  
940 - return rows[0]||null;
941 - }
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 + });
942 942  
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;
697 + return candidates[0].el;
1018 1018   }
1019 1019  
1020 - function alignGeneric(cards){
700 + function alignFirstSections(cards){
1021 1021   var rows=
1022 1022   cards.map(function(card){
1023 - var h=
1024 - heading(card);
703 + var bar=
704 + visuallyFirstSectionBar(card);
1025 1025  
1026 - var c=
1027 - firstContent(
1028 - card,
1029 - h
1030 - );
706 + if(!bar)return null;
1031 1031  
1032 - if(!h||!c)return null;
1033 -
1034 - var cr=
708 + var cardRect=
1035 1035   card.getBoundingClientRect();
1036 1036  
1037 - var rr=
1038 - c.getBoundingClientRect();
711 + var barRect=
712 + bar.getBoundingClientRect();
1039 1039  
1040 1040   return {
1041 - content:c,
715 + card:card,
716 + bar:bar,
1042 1042   relativeTop:
1043 - rr.top-cr.top
718 + barRect.top-cardRect.top,
719 + label:
720 + normalizeText(bar.textContent)
1044 1044   };
1045 1045   });
1046 1046  
724 + // Conservative: only align when every card in the horizontal row
725 + // has a reliable first section bar.
1047 1047   if(
1048 1048   rows.some(function(row){
1049 1049   return !row;
1050 1050   })
1051 1051   ){
1052 - return false;
731 + return null;
1053 1053   }
1054 1054  
1055 - var target=
734 + var targetTop=
1056 1056   Math.max.apply(
1057 1057   Math,
1058 1058   rows.map(function(row){
... ... @@ -1062,47 +1062,54 @@
1062 1062  
1063 1063   rows.forEach(function(row){
1064 1064   var delta=
1065 - target-
744 + targetTop-
1066 1066   row.relativeTop;
1067 1067  
1068 1068   if(delta<=1)return;
1069 1069  
1070 1070   saveOriginal(
1071 - row.content,
750 + row.bar,
1072 1072   originalMarginTop,
1073 1073   "margin-top"
1074 1074   );
1075 1075  
1076 - var current=
755 + var currentMargin=
1077 1077   num(
1078 1078   getComputedStyle(
1079 - row.content
758 + row.bar
1080 1080   ).marginTop
1081 1081   );
1082 1082  
1083 - row.content.style.setProperty(
762 + row.bar.style.setProperty(
1084 1084   "margin-top",
1085 1085   (
1086 - current+
765 + currentMargin+
1087 1087   delta
1088 1088   )+"px"
1089 1089   );
1090 1090  
1091 - row.content.setAttribute(
1092 - "data-sn-first-content-aligned",
1093 - "7"
770 + row.bar.setAttribute(
771 + "data-sn-first-section-aligned",
772 + "4"
1094 1094   );
1095 1095  
1096 - contentManaged.add(
1097 - row.content
1098 - );
775 + sectionManaged.add(row.bar);
1099 1099   });
1100 1100  
1101 - return true;
778 + return {
779 + labels:
780 + rows.map(function(row){
781 + return row.label;
782 + }),
783 + targetRelativeTop:
784 + Math.round(targetTop)
785 + };
1102 1102   }
1103 1103  
1104 - function applyRow(group){
1105 - if(!validRow(group))return false;
788 + function applyGroup(group){
789 + if(!validHorizontalGroup(group)){
790 + return null;
791 + }
1106 1106  
1107 1107   var cards=[];
1108 1108   var seen=new Set();
... ... @@ -1114,30 +1114,25 @@
1114 1114   }
1115 1115   });
1116 1116  
1117 - if(cards.length<2)return false;
803 + if(cards.length<2)return null;
1118 1118  
1119 - if(
1120 - !alignKnownBars(cards)
1121 - ){
1122 - alignGeneric(cards);
1123 - }
805 + var firstSection=
806 + alignFirstSections(cards);
1124 1124  
808 + // The section alignment can change natural card heights.
809 + // Measure only afterwards.
1125 1125   var maxHeight=0;
1126 1126  
1127 1127   cards.forEach(function(card){
1128 - maxHeight=
1129 - Math.max(
1130 - maxHeight,
1131 - card.getBoundingClientRect().height
1132 - );
813 + maxHeight=Math.max(
814 + maxHeight,
815 + card.getBoundingClientRect().height
816 + );
1133 1133   });
1134 1134  
1135 - maxHeight=
1136 - Math.ceil(
1137 - maxHeight
1138 - );
819 + maxHeight=Math.ceil(maxHeight);
1139 1139  
1140 - if(maxHeight<MIN_H)return false;
821 + if(maxHeight<MIN_H)return null;
1141 1141  
1142 1142   cards.forEach(function(card){
1143 1143   saveOriginal(
... ... @@ -1153,228 +1153,125 @@
1153 1153  
1154 1154   card.setAttribute(
1155 1155   "data-sn-equal-height-managed",
1156 - "7"
837 + "4"
1157 1157   );
1158 1158  
1159 1159   heightManaged.add(card);
1160 1160   });
1161 1161  
1162 - return true;
843 + return {
844 + cards:cards.length,
845 + height:maxHeight,
846 + firstSection:firstSection
847 + };
1163 1163   }
1164 1164  
1165 - function calculate(force){
1166 - if(applying)return;
850 + function run(){
851 + reset();
1167 1167  
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 -
1181 1181   if(
1182 - hasApplied&&
1183 - !force&&
1184 - !meaningful
854 + window.innerWidth<=MOBILE_MAX
1185 1185   ){
1186 1186   return;
1187 1187   }
1188 1188  
1189 - applying=true;
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 + });
1190 1190  
1191 - try{
1192 - harmoniseCustomCallouts();
871 + var touched=new Set();
1193 1193  
1194 - if(width<=MOBILE_MAX){
1195 - if(hasApplied){
1196 - resetRuntime();
1197 - }
873 + parents.forEach(function(parent){
874 + var entries=[];
1198 1198  
1199 - hasApplied=false;
1200 - lastWidth=width;
1201 - return;
1202 - }
876 + Array.from(parent.children).forEach(function(child){
877 + if(!visible(child,MIN_W,MIN_H))return;
1203 1203  
1204 - if(hasApplied){
1205 - resetRuntime();
1206 - }
879 + var target=pickTarget(child);
880 + if(!target)return;
1207 1207  
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 - );
882 + entries.push({
883 + target:target,
884 + rect:target.getBoundingClientRect()
1224 1224   });
886 + });
1225 1225  
1226 - var touched=
1227 - new Set();
888 + if(entries.length<2)return;
1228 1228  
1229 - parents.forEach(function(parent){
1230 - var entries=[];
890 + groupsByVisualRow(entries).forEach(function(group){
891 + if(!validHorizontalGroup(group))return;
1231 1231  
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 - }
1244 -
1245 - var target=
1246 - pickTarget(child);
1247 -
1248 - if(!target)return;
1249 -
1250 - entries.push({
1251 - target:target,
1252 - rect:
1253 - target.getBoundingClientRect()
893 + var signature=
894 + group.entries.map(function(entry){
895 + return entry.target;
1254 1254   });
1255 - });
1256 1256  
1257 - groupRows(
1258 - entries
1259 - ).forEach(function(group){
1260 - if(!validRow(group))return;
898 + if(
899 + !signature.some(function(el){
900 + return !touched.has(el);
901 + })
902 + ){
903 + return;
904 + }
1261 1261  
1262 - var targets=
1263 - group.entries.map(function(entry){
1264 - return entry.target;
1265 - });
906 + var applied=
907 + applyGroup(group);
1266 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 - });
909 + if(applied){
910 + signature.forEach(function(el){
911 + touched.add(el);
912 + });
913 + }
1281 1281   });
1282 -
1283 - lastWidth=width;
1284 - hasApplied=true;
1285 - }finally{
1286 - applying=false;
1287 - }
915 + });
1288 1288   }
1289 1289  
1290 - function resize(){
1291 - if(resizeTimer){
1292 - clearTimeout(
1293 - resizeTimer
1294 - );
1295 - }
918 + function schedule(){
919 + if(raf)cancelAnimationFrame(raf);
1296 1296  
1297 - resizeTimer=
1298 - setTimeout(
1299 - function(){
1300 - resizeTimer=0;
1301 - calculate(false);
1302 - },
1303 - RESIZE_DEBOUNCE
1304 - );
921 + raf=requestAnimationFrame(function(){
922 + raf=requestAnimationFrame(function(){
923 + raf=0;
924 + run();
925 + });
926 + });
1305 1305   }
1306 1306  
1307 - function waitImages(){
1308 - var images=
1309 - Array.from(
1310 - root.querySelectorAll("img")
1311 - );
929 + addEventListener("resize",schedule,{passive:true});
930 + addEventListener("load",schedule,{once:true});
1312 1312  
1313 - return Promise.all(
1314 - images.map(function(img){
1315 - if(img.complete){
1316 - return Promise.resolve();
1317 - }
932 + root.addEventListener(
933 + "click",
934 + function(){
935 + setTimeout(schedule,0);
936 + },
937 + true
938 + );
1318 1318  
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 - });
940 + Array.from(root.querySelectorAll("img")).forEach(function(img){
941 + if(!img.complete){
942 + img.addEventListener("load",schedule,{once:true});
1352 1352   }
944 + });
1353 1353  
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);
946 + if(document.fonts&&document.fonts.ready){
947 + document.fonts.ready.then(schedule);
1367 1367   }
1368 1368  
1369 - addEventListener(
1370 - "resize",
1371 - resize,
1372 - {passive:true}
1373 - );
1374 -
1375 - initial();
950 + schedule();
951 + setTimeout(schedule,200);
952 + setTimeout(schedule,650);
953 + setTimeout(schedule,1400);
1376 1376  })();
1377 1377  </script>
1378 -
1379 1379  <!-- SN_HORIZONTAL_BOX_HARMONIZER_END -->
1380 -{{/html}}\n
957 +{{/html}}
958 +