html, body, #form1 {
    height: 100%;
    overflow: hidden;
}

#wrapper {
    height: 100%;
}

/* Preloader: full-screen overlay shown until the view image has loaded */
#preloader {
    position: fixed;
    inset: 0;
    z-index: 1080; /* above content, above Bootstrap modal backdrop (1050) */
    display: flex;
    align-items: center;
    justify-content: center;
    background: #fff;
    transition: opacity 0.3s ease;
}

#preloader.is-hidden {
    opacity: 0;
    visibility: hidden; /* removes it from hit-testing once faded out */
}

#preloader .spinner-border {
    width: 3rem;
    height: 3rem;
}

/* Stage: positioning context for the image + hotspot overlay */
.visualizer-stage {
    position: relative;
    display: inline-block; /* shrink-wraps to the image so the overlay matches its box */
    max-width: 100%;
    line-height: 0; /* remove inline-image whitespace */
}

.visualizer-image {
    display: block;
    max-width: 100%;
    height: auto;
}

/* Overlay: sits exactly on top of the image, hosts the clickable items */
.visualizer-overlay {
    position: absolute;
    inset: 0;
    pointer-events: none; /* let only the hotspots receive clicks */
}

/* View switcher: fixed top-right button group, above the image/overlay */
#viewMenu {
    position: fixed;
    top: 1rem;
    right: 1rem;
    z-index: 1030; /* above content, below preloader (1080) and modal (1055) */
    /* Many views: wrap onto multiple lines rather than run off the left edge. */
    flex-wrap: wrap;
    justify-content: flex-end;
    max-width: calc(100% - 2rem);
}

/* The active button in a switcher (the page's view menu, a panel modal's image
   menu). Clicking it is a no-op, so every state keeps the same colour rather
   than darkening the way a Bootstrap button would.

   Bootstrap 5.3 resolves .btn colours from these custom properties, and its own
   `.btn.active` rule (specificity 20) would otherwise beat a plain
   `.btn-view-active` (10) in the resting state while losing to the :hover
   selector - which shows up as a button that is only orange while hovered.
   Setting the variables on `.btn.btn-view-active` matches that specificity and
   colours every state from one place. */
.btn.btn-view-active {
    --bs-btn-color: #fff;
    --bs-btn-bg: #f25929;
    --bs-btn-border-color: #f25929;
    --bs-btn-hover-color: #fff;
    --bs-btn-hover-bg: #f25929;
    --bs-btn-hover-border-color: #f25929;
    --bs-btn-active-color: #fff;
    --bs-btn-active-bg: #f25929;
    --bs-btn-active-border-color: #f25929;
    --bs-btn-focus-shadow-rgb: 242, 89, 41;
}

/* Hotspot marker, positioned by percentage (see visualizer.js item.x / item.y).
   The marker shows the hotspot icon image (Assets/Gfx/hotspot-icon-n.png). */
.visualizer-hotspot {
    position: absolute;
    transform: translate(-50%, -50%); /* x/y mark the centre of the hotspot */
    pointer-events: auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: transparent;
    cursor: pointer;
    transition: transform 0.15s ease;
    animation: visualizer-pulse 2s infinite;
}

.visualizer-hotspot-icon {
    display: block;
    width: 32px;
    height: 32px;
    /* Matches the icon's red so no ring shows behind its circular edge. */
    border-radius: 50%;
    background: #e41e26;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
    position: relative;
    z-index: 1; /* keep the marker icon on top of the connected label */
}

.visualizer-hotspot:hover,
.visualizer-hotspot:focus {
    transform: translate(-50%, -50%) scale(1.15);
    outline: none;
}

/* Hover label: the item name shown to the right of the marker. Hidden by
   default; only revealed on hover on devices with a fine hover-capable
   pointer (i.e. desktop with a mouse), never on touch. */
.visualizer-hotspot-label {
    position: absolute;
    /* Start at the marker centre and tuck under it so the label reads as one
       piece with the hotspot; the marker icon sits on top of this left edge. */
    left: 50%;
    top: 50%;
    transform: translateY(-50%);
    display: flex;
    align-items: center;
    height: 32px; /* match the hotspot marker height */
    padding: 0 12px 0 24px; /* extra left padding clears the marker icon */
    white-space: nowrap;
    font-size: 14px;
    line-height: 1;
    color: #000;
    background: #fff;
    border-radius: 16px; /* pill: rounded ends match the circular marker */
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
    pointer-events: none; /* the label never intercepts clicks */
    opacity: 0;
    visibility: hidden;
    transition: opacity 0.12s ease;
}

/* Flipped label: shown to the LEFT of the marker when a right-side label would
   overflow the viewport's right edge (see positionLabel() in visualizer.js).
   Mirror of the default: anchored on the right, clearance padding on the right. */
.visualizer-hotspot.label-left .visualizer-hotspot-label {
    left: auto;
    right: 50%;
    padding: 0 24px 0 12px;
    justify-content: flex-end;
}

@media (hover: hover) and (pointer: fine) {
    .visualizer-hotspot:hover .visualizer-hotspot-label,
    .visualizer-hotspot:focus-visible .visualizer-hotspot-label {
        opacity: 1;
        visibility: visible;
    }
}

/* Pulse ring around the marker to draw the eye. */
@keyframes visualizer-pulse {
    0%   { box-shadow: 0 0 0 0 rgba(227, 11, 23, 0.5); }
    70%  { box-shadow: 0 0 0 12px rgba(227, 11, 23, 0); }
    100% { box-shadow: 0 0 0 0 rgba(227, 11, 23, 0); }
}

/* --- Hotspot modal ------------------------------------------------------- */
/* Widen the dialog to 95vw (overrides Bootstrap modal-xl's 1140px cap). On wider
   screens this cap is tightened further to the image's own width - see the fit
   rules further down. `margin: auto` keeps the box centred as it changes size. */
#visualizerModal .modal-dialog {
    max-width: 95vw;
    margin-left: auto;
    margin-right: auto;
}

#visualizerModal .modal-content {
    position: relative;
    overflow: visible; /* let the close button sit outside the top-right corner */
}

/* Close button: a round white badge above the modal's top-right corner. The
   whole badge is the button, so clicking anywhere on it closes the modal. */
.visualizer-modal-close {
    position: absolute;
    top: -18px;
    right: -18px;
    z-index: 2;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 36px;
    height: 36px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: #fff;
    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
    cursor: pointer;
    transition: background-color 0.15s ease;
}

.visualizer-modal-close:hover,
.visualizer-modal-close:focus-visible {
    background: #e41e26;
}

/* Invert the × to white so it stays visible on the red hover background. */
.visualizer-modal-close:hover .btn-close,
.visualizer-modal-close:focus-visible .btn-close {
    filter: invert(1) grayscale(100%) brightness(200%);
}

.visualizer-modal-close .btn-close {
    display: block;
    padding: 0;
    margin: 0;
    width: 14px;
    height: 14px;
    background-size: 14px;
    pointer-events: none; /* clicks belong to the badge button */
}

/* The two item images stack vertically, full modal width. */
.visualizer-modal-images {
    display: flex;
    flex-direction: column;
    gap: 1rem;
}

.visualizer-modal-img {
    display: block;
    width: 100%;
    height: auto; /* natural size by default (mobile: columns stack, page scrolls) */
    object-fit: contain;
    object-position: top;
}

/* Each modal image sits in a figure; clicking anywhere on it enlarges the
   image into the zoom layer. */
.visualizer-modal-figure {
    position: relative;
}

.visualizer-img-zoomable {
    cursor: zoom-in;
}

/* Caption box floating over the assembled image's top-left corner in the item
   modal: the exploded image, the item's title, its text, then its links.
   Opaque, so it reads as a panel sitting on the image rather than a tint over
   it. */
.visualizer-img-caption {
    position: absolute;
    top: 1rem;
    left: 1rem;
    z-index: 1; /* above the image, below the modal's close/back badges */
    /* A third of the image width, however long the text is. */
    width: 33%;
    max-height: calc(100% - 2rem);
    overflow-y: auto; /* long WYSIWYG text scrolls inside the box */
    overflow-x: hidden; /* never sideways - see the wrapping rules below */
    padding: 0.75rem 1rem;
    border-radius: 6px;
    background: #c6ccc5;
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
    color: #212529;
    cursor: auto; /* not part of the figure's click-to-zoom affordance */
    text-align: left;
}

/* The exploded image, at the top of the box above the item's name. */
.visualizer-img-caption-img {
    display: block;
    width: 100%;
    height: auto; /* keep the aspect ratio */
    margin-bottom: 0.5rem;
}

.visualizer-img-caption-title {
    font-size: 1.1rem;
    font-weight: 600;
    margin-bottom: 0.35rem;
}

.visualizer-img-caption-text {
    font-size: 0.9rem;
    line-height: 1.4;
}

/* The box hides overflow on the x-axis, so anything wider than it has to wrap
   rather than be clipped. A long unbroken run - a URL, a part number - is the
   usual culprit in WYSIWYG text; `overflow-wrap` breaks one only when it cannot
   fit, leaving normal prose to wrap at spaces as usual. */
.visualizer-img-caption-title,
.visualizer-img-caption-text {
    overflow-wrap: break-word;
    word-break: break-word; /* Safari < 15 does not honour the above on its own */
}

/* A table pasted from the editor has its own intrinsic width and would other-
   wise be cut off at the box edge. */
.visualizer-img-caption-text table {
    width: 100%;
    table-layout: fixed;
}

/* Preformatted blocks never wrap on their own. */
.visualizer-img-caption-text pre {
    white-space: pre-wrap;
    overflow-wrap: break-word;
}

/* The WYSIWYG text arrives as its own paragraphs; drop the trailing margin so
   the box doesn't gain a gap at the bottom. */
.visualizer-img-caption-text > *:last-child {
    margin-bottom: 0;
}

/* An image inside the WYSIWYG text fills the box width, whatever dimensions
   were set on it in the editor. */
.visualizer-img-caption-text img {
    display: block;
    width: 100%;
    height: auto; /* keep the aspect ratio */
}

/* The item's links, below the caption text: a wrapping row of pills. */
.visualizer-img-caption-links {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    margin-top: 0.75rem;
}

.visualizer-img-caption-link {
    display: inline-flex;
    align-items: center;
    padding: 0.25rem 0.75rem;
    border: 1px solid rgba(0, 0, 0, 0.15);
    border-radius: 999px;
    background: #e6e6e6;
    color: inherit;
    text-decoration: none;
    font-size: 0.85rem;
    /* Pills stay on one line where they fit, but a label longer than the box
       has to wrap: the caption hides overflow on the x-axis, so `nowrap` would
       clip it instead of scrolling to it. */
    max-width: 100%;
    overflow-wrap: break-word;
    word-break: break-word;
    transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}

.visualizer-img-caption-link:hover,
.visualizer-img-caption-link:focus {
    background: #f25929;
    border-color: #f25929;
    color: #fff;
}

/* Full-modal image overlay shown while an image is enlarged. Sits over the
   body content but below the close/back badges so the modal can still close. */
.visualizer-zoom-layer {
    position: absolute;
    inset: 0;
    z-index: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 1rem;
    background: #fff;
    border-radius: inherit;
}

.visualizer-zoom-img {
    /* Fill the width, but let BOTH dimensions shrink together so the aspect
       ratio is preserved. As a flex item, capping only the height while width
       stays 100% squashes the image, so cap the width too and keep the
       intrinsic ratio via object-fit. */
    width: 100%;
    height: auto;
    max-width: 100%;
    max-height: 100%;
    object-fit: contain;
    flex: 0 1 auto;
    min-width: 0;
    min-height: 0;
}

/* Carousel prev/next arrows: round badges vertically centred at each edge of
   the zoom layer (shown only when an accessory has more than one image). */
.visualizer-zoom-nav {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    z-index: 2;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.9);
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
    color: #212529;
    font-size: 20px;
    line-height: 1;
    cursor: pointer;
    transition: background-color 0.15s ease, color 0.15s ease;
}

.visualizer-zoom-nav-prev { left: 12px; }
.visualizer-zoom-nav-next { right: 12px; }

.visualizer-zoom-nav:hover,
.visualizer-zoom-nav:focus-visible {
    background: #e41e26;
    color: #fff;
}

/* On phones the arrows are hidden - users swipe instead (the dots remain). */
@media (max-width: 767.98px) {
    .visualizer-zoom-nav {
        display: none;
    }
}

/* Dots indicator: centred at the bottom of the zoom layer. */
.visualizer-zoom-dots {
    position: absolute;
    bottom: 14px;
    left: 50%;
    transform: translateX(-50%);
    z-index: 2;
    display: flex;
    gap: 8px;
}

.visualizer-zoom-dot {
    width: 10px;
    height: 10px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: rgba(0, 0, 0, 0.25);
    box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.6);
    cursor: pointer;
    transition: background-color 0.15s ease;
}

.visualizer-zoom-dot.is-active {
    background: #e41e26;
}

/* The shrink pill reuses the back-pill look/position (top-left corner). It sits
   above the zoom layer, and its icon needs to line up with the name text. */
.visualizer-img-shrink {
    z-index: 3;
}

.visualizer-img-shrink .visualizer-modal-back-arrow {
    display: inline-flex;
    align-items: center;
}

/* While zoomed, hide the normal body content behind the overlay, and hide the
   panel back pill (the shrink pill takes its place). The shrink pill also
   carries .visualizer-modal-back for styling, so exclude it from the hide. */
.visualizer-zoomed .modal-body {
    visibility: hidden;
}

.visualizer-zoomed .visualizer-modal-back:not(.visualizer-img-shrink) {
    display: none;
}

/* A panel modal shows the panel image (with its own hotspots) in the left
   column. Make the reused .visualizer-stage span the column width. */
.visualizer-panel-stage {
    display: block;
    width: 100%;
    max-width: 100%;
    /* Clip anything that sits past the image box - a hotspot marker placed near
       an edge is centred on its x/y, so half of it overhangs the stage and would
       otherwise widen the column and add a horizontal scrollbar. */
    overflow: hidden;
}

/* Fill the stage width so the panel image can't overflow the column; height
   follows the aspect ratio. */
.visualizer-panel-stage .visualizer-image {
    width: 100%;
    max-width: 100%;
}

/* A panel modal builds every pane up front - the panel itself plus one per
   inner item - and navigating only toggles which is visible. `hidden` does the
   hiding, so this just stops Bootstrap's display rules from overriding it. */
.visualizer-panel-session > [hidden] {
    display: none;
}

/* Panel image switcher: one button per image, in the stage's top-right corner.
   Mirrors the page's #viewMenu, but scoped to the panel modal. Only rendered
   when the panel has more than one image. */
.visualizer-panel-menu {
    position: absolute;
    top: 1rem;
    right: 1rem;
    z-index: 2; /* above the image and the hotspot overlay */
    /* Long names: wrap rather than run off the left edge of the image. */
    flex-wrap: wrap;
    justify-content: flex-end;
    max-width: calc(100% - 2rem);
}

/* "Back" button shown in an item modal that was opened from a panel; returns
   to that panel's modal. A pill (arrow + panel name) at the top-left corner,
   matching the height/look of the round close badge. */
.visualizer-modal-back {
    position: absolute;
    top: -18px;
    left: -18px;
    z-index: 2;
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    height: 36px;
    padding: 0 1rem;
    border: 0;
    border-radius: 18px; /* half the height -> pill */
    background: #fff;
    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
    color: #212529;
    font-weight: 600;
    line-height: 1;
    white-space: nowrap;
    cursor: pointer;
    transition: background-color 0.15s ease, color 0.15s ease;
}

.visualizer-modal-back-arrow {
    font-size: 18px;
    line-height: 1;
}

.visualizer-modal-back:hover,
.visualizer-modal-back:focus-visible {
    background: #e41e26;
    color: #fff;
}

/* Two-column layout (Bootstrap md and up): the modal sizes to its content but
   is capped at 90vh, after which each column scrolls on its own. On smaller
   screens the columns stack and the modal flows/scrolls naturally instead —
   otherwise the images get squeezed into a fraction of the height. */
@media (min-width: 768px) {
    #visualizerModal .modal-content {
        max-height: 90vh; /* size to content, but never taller than 90vh */
    }

    #visualizerModal .modal-body {
        overflow: hidden; /* the right column scrolls, not the body as a whole */
        display: flex;
        flex: 1 1 auto;
        min-height: 0;
    }

    #visualizerModal .modal-body > .row {
        flex: 1 1 auto;
        min-height: 0;
        width: 100%;
    }

    .visualizer-modal-images {
        min-height: 0;
        max-height: 100%;
        /* An image constrained by height is narrower than the column, so centre
           it rather than letting it sit off to one side. */
        align-items: center;
        justify-content: center;
    }
}

/* --------------------------------------------------------------------------
   Fitting the modal images to the height that is actually available.

   Every image in a modal was sized `width:100%; height:auto`, so its height came
   out of the aspect ratio and the modal simply had to be tall enough. When it is
   not - a short window, or an embed, where the modal's 90vh cap resolves against
   the IFRAME rather than the browser window - the image overflowed and was
   clipped at the bottom.

   Constraining both axes instead lets the WIDTH give way: the image shrinks to
   whatever height there is, keeps its aspect ratio, and stays fully visible.

   For `max-height: 100%` to mean anything, every box between .modal-body and the
   image needs a definite height to resolve against, and every flex item in that
   chain needs `min-height: 0` (the default `auto` refuses to shrink below the
   content and silently defeats the cap). The chain is:

     .modal-body  >  .visualizer-panel-session?  >  div  >  .row
       >  .visualizer-modal-images  >  (.visualizer-modal-figure | .visualizer-stage)
       >  img

   The panel-session wrapper and its unclassed pane div only exist in a panel
   modal; an item modal goes straight from .modal-body to the .row.
   -------------------------------------------------------------------------- */
@media (min-width: 768px) {
    /* The panel session and its visible pane are plain blocks, so they have to
       be told to carry the height down rather than growing to fit content. */
    #visualizerModal .visualizer-panel-session,
    #visualizerModal .visualizer-panel-session > div:not([hidden]) {
        display: flex;
        flex-direction: column;
        min-height: 0;
        max-height: 100%;
    }

    #visualizerModal .modal-body > .row,
    #visualizerModal .visualizer-panel-session > div:not([hidden]) > .row {
        min-height: 0;
        max-height: 100%;
    }

    #visualizerModal .visualizer-modal-images {
        min-height: 0;
        max-height: 100%;
    }

    /* Once the image is height-constrained it is narrower than the column, and a
       dialog that still filled modal-xl would leave a band of empty modal down
       either side of the picture. So the DIALOG is capped to the width its image
       actually needs at the current height.

       `--ngc-modal-aspect` is set from the visible image's natural dimensions by
       trackModalAspect() in visualizer.js. It has to come from JS: deriving the
       width from the height in CSS alone is circular, and `fit-content` resolves
       against the image's natural width before the height cap applies, so the
       box always came out full-width.

       The 90vh here mirrors the .modal-content cap above; the extra ~4.5rem is
       the modal's own chrome (body padding + borders) which is not part of the
       image's box. */
    #visualizerModal .modal-dialog {
        max-width: min(95vw, calc((90vh - 4.5rem) * var(--ngc-modal-aspect, 2) + 2.5rem));
    }

    /* The item modal's image. */
    #visualizerModal .visualizer-modal-figure {
        display: flex;
        justify-content: center;
        min-height: 0;
        max-height: 100%;
    }

    #visualizerModal .visualizer-modal-img {
        width: auto;
        max-width: 100%;
        max-height: 100%;
        object-fit: contain;
    }

    /* The panel modal's image, which carries its own hotspot overlay. The
       overlay is positioned against the stage, so the stage must shrink WITH the
       image - otherwise the hotspots would drift off the features they mark.
       Sizing the stage to its content keeps the two locked together. */
    #visualizerModal .visualizer-panel-stage {
        width: auto;
        max-width: 100%;
        min-height: 0;
        max-height: 100%;
        margin-left: auto;
        margin-right: auto;
    }

    #visualizerModal .visualizer-panel-stage .visualizer-image {
        width: auto;
        max-width: 100%;
        max-height: 100%;
        object-fit: contain;
    }
}

/* On small screens the modal spans almost the full viewport width, so badges
   offset outside the corners (top/left/right: -18px) get clipped by the screen
   edge. Tuck them just inside the modal corners instead. */
@media (max-width: 767.98px) {
    .visualizer-modal-close {
        top: 8px;
        right: 8px;
    }

    .visualizer-modal-back {
        top: 8px;
        left: 8px;
    }

    /* Leave room at the top of the body so the now-inside badges don't overlap
       the content (image / title). */
    #visualizerModal .modal-body {
        padding-top: 3.25rem;
    }
}

