Files
changedetection.io/changedetectionio/static/styles/scss/diff.scss
T

816 lines
30 KiB
SCSS

@use "settings" as *;
// Paint the page's own background (page colour + the fixed 130deg gradient at
// 0.91, pre-composited with color-mix) so a sticky bar stays opaque to content
// scrolling under it while looking identical to the page behind it.
//
// styles.scss paints that backdrop as a real fixed layer: body::after is a
// `position: fixed` box at the viewport's top-left corner, viewport-wide and
// `height: 100vh` tall. Anything that has to disappear into it must show the
// slice of that box it covers. background-attachment: fixed asks the engine for
// exactly that, and iOS Safari does not honour it - it anchors the layer to the
// document instead, so the bar sweeps the whole ramp (cyan to purple over a
// long page) while the gutters beside it hold still. Chromium does honour it,
// which is why only a device caught this.
//
// So derive the alignment from geometry and ask for no feature at all. Each bar
// keeps a constant viewport offset - .header at the top, #diff-header directly
// below it - and both are flush with the viewport's right edge at every width:
// the action rail insets them from the *left* on a wide screen and widens again
// on hover, but nothing moves their right edge. Anchor a viewport-sized copy of
// the gradient to that edge and push it up by the bar's own offset, and the bar
// paints body::after's slice by construction, on any engine.
//
// $y-offset: the bar's distance from the top of the viewport.
@mixin page-surface-gradient($y-offset: 0px) {
background: var(--color-background-gradient-second); // fallback, no color-mix
// Longhands from here down, and no second `background:` shorthand after them -
// one would silently reset size/position/origin back to their initial values.
background-image: linear-gradient(130deg,
color-mix(in srgb, var(--color-background-gradient-first) 91%, var(--color-background-page)),
color-mix(in srgb, var(--color-background-gradient-second) 91%, var(--color-background-page)) 41.07%,
color-mix(in srgb, var(--color-background-gradient-third) 91%, var(--color-background-page)) 84.05%);
// 100vh to match body::after's own height: 100vh - same unit, so the two
// resolve to the same number on iOS, where it means the large (toolbar
// collapsed) viewport and therefore holds still as the toolbar moves.
// 100vw for the width: the bars are as wide as the viewport only on a narrow
// screen, so 100% would compress the ramp into the rail's leftovers on a
// desktop one.
background-size: 100vw 100vh;
background-repeat: no-repeat;
// border-box, or the bar's own padding would offset the copy by that much.
background-origin: border-box;
background-position: right 0 top calc(-1 * #{$y-offset});
}
#diff-form {
background: rgba(0, 0, 0, .05);
padding: 1em;
border-radius: 10px;
margin-bottom: 1em;
color: #fff;
font-size: 0.9rem;
text-align: center;
label.from-to-label {
width: 4rem;
text-decoration: none;
padding: 0.5rem;
&#change-from {
color: #b30000;
background: #fadad7
}
&#change-to {
background: #eaf2c2;
color: #406619;
}
}
#diff-style {
>span {
display: inline-block;
padding: 0.3em;
label {
font-weight: normal;
}
}
}
* {
vertical-align: middle;
}
}
body.difference-page {
// Keep the top menu (watch URL, EDIT, theme/GitHub icons) on screen while the
// diff scrolls, stacked above #diff-header. Scoped to the diff page on
// purpose - upstream's @todo in parts/_top_menu.scss wants this globally, but
// that is a separate change.
//
// Sticky makes .header a stacking context, so its mobile drawer / overlay
// (z-index 10000 / 9999) are now capped by this value. 30 clears #diff-header
// below and still leaves toasts (10000, fixed) painting over the bar.
.header {
position: sticky;
top: 0;
z-index: 30;
@include page-surface-gradient(0px);
}
// The top line now carries the watch's name, so give it back the room the two
// stacked left insets were taking. Its text starts $common-gap twice in: the
// menu row's own padding, which every item on the row needs to clear the edge,
// plus the link's own left margin, which on the row's first item only repeats
// that padding. Drop the repeat and keep the row's padding: 8.8px back, and
// the line still sits on the same left edge as the diff below it rather than
// hard against the viewport. The right side of both is untouched - the link's
// right margin still separates it from the menu icons.
.current-diff-url {
margin-left: 0;
}
// Wherever the hamburger is (max-width: $desktop-wide-breakpoint, set in
// parts/_hamburger_menu.scss), the menu row is at its tightest and every item
// on it competes with the watch name for the same line. The heart is the one
// item on that row that is neither navigation nor status, so it is what gives
// way - on this page only, and only under that width. Desktop diff pages and
// every other page keep it.
//
// CSS rather than a {% if %} in base.html: the heart's own reason to exist is
// that it is always there, and a template branch would take it off the page
// entirely rather than fold it away with the rest of the row.
@media only screen and (max-width: $desktop-wide-breakpoint) {
#heart-us {
display: none;
}
}
// .app-main's 0.55rem gap sits between the two sticky bars, so #diff-header
// starts 8.8px below the top menu and then slides up to meet it over the
// first 8.8px of scroll - a visible twitch right as scrolling begins. The gap
// can't be preserved once stuck (it is outside both bars, so the diff would
// scroll through it), so close it here and let them sit flush at every scroll
// position. Only the .header/section.content pair is affected; .app-main has
// no other children.
.app-main {
gap: 0;
}
.tab-pane-inner {
scroll-margin-top: calc(var(--app-header-height, 0px) + var(--diff-header-height, 0px) + 1rem);
}
// The 2px activity strip is fixed on body, so it lives in the root stacking
// context and would paint over the mobile drawer now that the drawer is capped
// inside .header's context above. Drop it below .header rather than raising
// .header past the action rail (60), which would hide the rail's hover flyout.
// Still above #diff-header (20) and the minimap (10); on this page it now also
// passes under the rail's flyout, which is 2px of imperceptible overlap.
#pure-menu-horizontal-spinner {
z-index: 29;
}
// The thin blue line under the active tab. Nothing at that boundary has a
// border - the active tab and #diff-ui are both var(--color-background) - so
// it can only be the page gradient showing through a device-pixel row where
// neither white paints. Whether such a row exists comes down to one number,
// the bar's bottom edge minus the card's top edge, and measured across 14
// viewports at three device pixel ratios that number is 0.813, 0.203 or
// exactly 0.000 and never a whole pixel. At 0.000 the outcome belongs
// entirely to the engine's sub-pixel rounding - which is why a DPR 3 phone
// can show a line a DPR 1 desktop does not, and why 390x844 on the Screenshot
// tab, the exact case reported, measures 0.000.
//
// So take the margin out of the engine's hands instead of hoping two white
// edges land on the same device row. A pixel of overlap covers the gradient
// on any grid; measured the same way afterwards, the worst case over those 42
// readings becomes exactly 1.000px.
//
// The other candidate was a white skirt under the active tab
// (box-shadow: 0 1px 0) which would have touched nothing else, and it does not
// work: the shadow hangs below the bar's last child and #diff-header's
// overflow: auto - the thing enforcing its height cap - clips it. Measured
// with the card held 3px away so the skirt and the card are separable, it
// painted 0 device rows of the 3 it needs at 844x390 and 926x428, and 0 to 2
// in the sticky modes. Recorded here so it is not proposed again.
//
// What the overlap costs, stated rather than discovered later: where the bar
// is sticky it is invisible, and a device-pixel diff of the seam band at
// 390x844 and DPR 3 says so exactly - zero differing pixels, because the bar
// is opaque and paints over the card. Below the short-viewport breakpoint the
// bar has no background of its own, so the card's white also reaches the
// bottom pixel of the *inactive* tabs and the 5px gaps between them: 4374
// device pixels at 844x390, gradient turning white. The tab row ends up
// sitting flush on the card rather than floating a pixel above it, which is
// how tabs normally meet their panel and is the same continuity the active
// tab was asked for.
//
// Scoped to the diff page deliberately. diff.css is NOT diff-page-only - it
// is also served to the preview page (blueprint/ui/preview.py) and the
// extract page (processors/extract.py), and both have a #diff-ui of their own
// with no bar above it to absorb the pixel. Only difference.py sets the
// difference-page class.
#diff-ui {
margin-top: -1px;
}
}
#diff-header {
position: sticky;
// Sits directly under the now-sticky top menu. The 0 fallback (JS off) just
// slides this under the bar - the same degradation the minimap already takes.
top: var(--app-header-height, 0px);
z-index: 20;
// section.content insets its children by $common-gap so they clear the left
// rail, while the top menu above stays flush against it. A bar that paints
// the page's own backdrop therefore leaves an 8.8px strip of real page
// background down each side, and on a device those strips read as mismatched
// borders. Cancel the inset rather than try to blend across it: with no
// gutter there is nothing left to keep aligned, and the flush right edge is
// what page-surface-gradient anchors its copy of the page gradient to. The
// controls do not move - the bar grows outwards by exactly the margin it
// takes back.
//
// The width has to come from align-self, not width: 100%. .content-main
// centres its children, so an item is fit-content unless it stretches, and a
// percentage width resolves against the parent without the negative margins -
// it would leave the bar short by 17.6px.
align-self: stretch;
margin-left: -$common-gap;
margin-right: -$common-gap;
box-sizing: border-box;
// Center children at their intrinsic width, the same way section.content does
// for the non-sticky siblings it still lays out (align-items: center).
// Two rows: the toolbar and the tabs.
display: flex;
flex-direction: column;
align-items: center;
gap: 0.25rem;
// No bottom padding: the tab row is the bar's last child, and the active tab
// and #diff-ui below it are both var(--color-background). 4px of gradient
// between them reads as a seam across an otherwise continuous white surface -
// the same tabs on the extract page, which are not in a bar, measure 0. The
// top padding stays; nothing sits above the toolbar.
padding: 0.25rem 1rem 0;
// Keep the diff readable even with wrapped controls, zoom, or a short viewport.
// The budget is for the whole sticky stack, so the top menu above comes out of
// it - otherwise the two bars together take 64% of a 390px-tall landscape phone.
//
// svh, not dvh. dvh tracks the dynamic viewport, which on iOS Safari grows and
// shrinks as the browser's own toolbar collapses and expands while you scroll:
// the cap would then breathe mid-scroll, changing the header's height and
// shifting everything below it, and each transition also fires resize and
// re-runs the sticky measurement behind it. svh is defined as the
// toolbar-expanded minimum and holds still for the life of the page, so the
// budget is a constant. It is the smaller of the two, which is the safe side to
// be wrong on for a cap. The plain vh line stays as the fallback for engines
// without the new units.
max-height: calc(50vh - var(--app-header-height, 0px));
max-height: calc(50svh - var(--app-header-height, 0px));
overflow: auto;
// Same offset as `top` above: the bar's constant distance from the top of the
// viewport. With JS off the var is absent and the copy is misaligned by the
// top menu's height - the same tier of degradation `top` itself already takes.
@include page-surface-gradient(var(--app-header-height, 0px));
> * {
// A centered flex item is sized to fit-content, which has a min-content
// floor (the version <select> is as wide as its longest option). Without
// this cap a narrow viewport would centre an over-wide child and spill it
// out of both sides of the header.
max-width: 100%;
}
// base.html's wrap detector decides body.wrapped-tabs by flipping the tab <ul>
// to flex-wrap and seeing whether any tab lands on a second row - so what it
// measures against is the width of .tabs. _tabs.scss says in upstream's own
// words that .tabs has to take the full width of the centred column "so the
// wrap-detector JS has the real available space to measure against", and the
// two properties that would do it are commented out directly underneath. As
// shipped, .tabs is fit-content inside any centred column, so the detector
// measures a container that moves with the layout state it is judging: the
// active tab's label is bold, and selecting the widest one ("Current
// screenshot") grows .tabs by ~9px. In the band where that crosses the
// available width the verdict flips per tab - measured here at 350, 355 and
// 358px, where Text and Extract Data report one row and Current screenshot
// reports three. iOS puts the same band at 390px because its fonts are wider,
// which is how a phone showed one row in a screenshot and a vertical stack in
// a video of the same page.
//
// Give the detector a fixed target on this page only. Un-commenting the lines
// in _tabs.scss would fix it everywhere and change every page upstream owns;
// this PR does not have the standing to do that.
.tabs {
align-self: stretch;
// Never give up height to keep the controls whole - see #settings below.
flex: none;
// .tabs no longer shrink-wraps, so the grid would sit left-aligned in it.
ul {
justify-content: center;
// Stretching the container fixes one side of the detector's comparison;
// this fixes the other. `.active a { font-weight: bold }` in _tabs.scss
// makes the row's width depend on which label is currently bold: at 355px
// the Text and Extract states measure 332.9 and 330.5 against a 339.0
// container and stay on one row, while Current screenshot - the widest
// label - measures over it and wraps to three. Neutralising just that rule
// collapses all three states to 330.5 and the flipping stops, which is what
// identifies the weight as the variable rather than the container.
//
// So drop the bold here rather than reserve room for it. Reserving the bold
// width on every tab (a hidden bold copy behind each label) also stabilises
// it, but it makes the row ~14px wider in every state and that pushes 360px
// - a common Android width that fits on one row today - into the stacked
// layout. The active tab does not need the third signal: _tabs.scss already
// gives it its own background-color and its own text colour.
li.active a,
li :target a {
font-weight: inherit;
}
}
}
// The bar's height budget (max-height above) is smaller than its contents on a
// landscape phone: iOS gives the two datetime <select>s enough intrinsic width
// that From and To each take their own row, and From + To + Filters + tabs is
// then taller than 50svh minus the top menu. With overflow: auto on the bar
// itself the row that leaves the visible box is the last one - the tabs, which
// are the page's navigation and have no scrollbar to advertise themselves. Measured in Chromium with no iOS emulation at 736x414, 667x375,
// 780x360 and 844x330: the tab row sits outside #diff-header's own rect.
//
// So spend the budget on the row that can afford it. #settings is the only
// child allowed to shrink, and it is what scrolls; the tab row never does and
// is therefore always inside the bar at any viewport height. The shrink needs
// min-height: 0 - a flex item's automatic minimum size is its content, which
// is what would otherwise push the tabs back out.
#settings {
min-width: 0;
display: flex;
justify-content: center;
flex: 0 1 auto;
min-height: 0;
overflow-y: auto;
// Same reason the Filters popover carries it: a nested scroller inside a
// sticky bar that chains its overscroll to the diff behind is worse than no
// scroller at all on a touch screen.
overscroll-behavior: contain;
}
// One toolbar row: From/To chips and their selects, the version arrows, and
// the Filters toggle.
#diff-form {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 0.25rem 0.9rem;
padding: 0.2rem 0.7rem;
margin-bottom: 0;
fieldset {
margin: 0;
padding: 0;
border: 0;
}
// The From/To labels shrink to chips rather than hiding: display:none takes
// the <label> out of the accessibility tree and the selects lose their
// accessible name entirely, and two identically formatted datetime selects
// side by side need the red/green cue sighted users already know. The
// shipped width: 4rem is what makes them full-size plates. "From" is wider
// than "To", which left-ragged the selects once they stack, so the floor is
// wide enough for the longer word - it costs width only, never height.
.from-to-label {
width: auto;
min-width: 2.4rem;
text-align: center;
font-size: 0.75rem;
padding: 0.1rem 0.35rem;
border-radius: 3px;
line-height: 1.5;
}
}
#keyboard-nav {
display: flex;
align-items: center;
gap: 0.3rem;
// The arrows speak for themselves at this size; the links keep the full
// wording as aria-label and title (see diff.html).
strong,
.keyboard-nav-label {
display: none;
}
}
// Seven set-once options are a popover, not a permanent row of the bar. The
// swap happens only once diff-overview.js has wired the toggle up, so with
// scripting off the fieldset stays inline and submits as it does today.
#diff-filters-toggle {
display: none;
font-size: 0.85rem;
padding: 0.3em 0.8em;
}
&.diff-filters-js {
#diff-filters-toggle {
display: inline-block;
}
#diff-style {
display: none;
}
}
&.diff-filters-js.diff-filters-open {
#diff-style {
// fixed, not absolute: #diff-header carries overflow: auto to enforce the
// 50svh cap above, which clips an absolutely positioned descendant at the
// bar's bottom edge. The bar is sticky with no transform/filter/contain,
// so it is not a containing block for fixed and the panel escapes the
// clip. diff-overview.js parks it under the toggle.
display: block;
position: fixed;
z-index: 25;
min-width: 14rem;
// diff-overview.js caps max-height to the room below the button that the
// viewport actually has - on a landscape phone that is less than the
// seven rows need. The rows past the cap have to stay reachable, and the
// scroll must not chain out to the diff behind once they run out.
overflow-y: auto;
overscroll-behavior: contain;
padding: 0.5rem 0.75rem;
border-radius: 8px;
box-shadow: 0 4px 14px rgba(0, 0, 0, .28);
text-align: left;
// The bar repaints the page's own backdrop; so must anything that has to
// be opaque over the scrolling diff. This one is not pinned to an edge of
// the viewport - diff-overview.js decides where it goes - so the same
// file overwrites background-position from the coordinates it just set.
// The declaration here is what a placement-less first paint falls back to.
@include page-surface-gradient;
}
// Out-specifies #diff-form #diff-style > span's inline-block above.
#diff-form #diff-style > span {
display: block;
padding: 0.2em 0;
}
}
.diff-fieldset {
min-width: 0;
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 0.5rem;
> span {
max-width: 100%;
min-width: 0;
gap: 0.25rem;
// Size to content so From/To stay side-by-side on desktop (as they were
// before the header existed); shrink and wrap only when the row can't fit.
flex: 0 1 auto;
display: flex;
align-items: center;
// A <select> keeps the intrinsic min-content width of its longest option
// (min-width: 0 does not lower it), so label + select cannot fit a phone
// viewport side by side. Let the label drop above it instead of forcing
// the whole header wider than the screen.
flex-wrap: wrap;
}
select {
min-width: 0;
max-width: 100%;
flex: 1;
}
label {
flex-shrink: 0;
}
}
}
// The one narrow-viewport breakpoint for the whole bar - title, fit and
// gutters all hang off it rather than letting magic numbers multiply.
//
// Every selector below carries an ID on purpose. The rules it has to beat are
// written above as `#diff-header .diff-fieldset > span` and friends, and a
// single ID outranks any number of classes, so a class-only override loses
// silently and the chain goes on shrink-wrapping. Please don't tidy the IDs
// away.
@media (max-width: 700px) {
// Once the From/To chip takes inline width the <select> clips its own
// displayed value, and two snapshots from the same day differ only by the
// meridiem - so they read identically while closed. Shrinking the font alone
// cannot fix that: #settings, #diff-form, .diff-fieldset and the span are all
// sized to max-content, which *is* the select's text, so the container
// shrinks by exactly as much as the text does and the headroom stays pinned
// at zero at every font size. Pin the chain to the row first; only then does
// the smaller font buy anything.
#diff-header > #settings {
width: 100%;
}
#diff-header #diff-form {
width: 100%;
box-sizing: border-box;
// 16px of bar gutter plus 11.2px of form gutter a side is 54px of a 390px
// row spent on nothing.
padding-left: 0.2rem;
padding-right: 0.2rem;
}
#diff-header {
padding-left: 0.5rem;
padding-right: 0.5rem;
}
#diff-header #diff-form > .diff-fieldset {
flex: 1 1 100%;
}
#diff-header .diff-fieldset > span {
flex: 1 1 100%;
}
// With the chain pinned the select holds its width, so this is what actually
// buys the room. The worst realistic English value - a two-digit day, the
// longest month and a two-digit hour - still clips at 0.8rem.
#diff-header .diff-fieldset select {
font-size: 0.75rem;
padding-left: 0.3em;
padding-right: 0.3em;
}
}
#diff-ui {
background: var(--color-background);
padding: 1rem;
border-radius: 5px;
@media (min-width: 767px) {
min-width: 50%;
}
// The first tab 'text' diff
#text {
font-size: 11px;
}
pre {
white-space: break-spaces;
overflow-wrap: anywhere;
}
h1 {
display: inline;
font-size: 100%;
}
#result {
white-space: pre-wrap;
word-break: break-word;
overflow-wrap: break-word;
}
.source {
position: absolute;
right: 1%;
top: .2em;
}
@-moz-document url-prefix() {
body {
height: 99%;
/* Hide scroll bar in Firefox */
}
}
td#diff-col div {
text-align: justify;
white-space: pre-wrap;
}
.ignored {
background-color: #ccc;
/* border: #0d91fa 1px solid; */
opacity: 0.7;
}
.triggered {
background-color: #1b98f8;
}
/* ignored and triggered? make it obvious error */
.ignored.triggered {
background-color: #ff0000;
}
.tab-pane-inner#screenshot {
text-align: center;
img {
max-width: 99%;
}
}
// resets button margin to 0px
.pure-form button.reset-margin {
margin: 0px;
}
.diff-fieldset {
display: flex;
align-items: center;
gap: 4px;
flex-wrap: wrap;
}
ul#highlightSnippetActions {
list-style-type: none;
display: flex;
align-items: center;
justify-content: center;
gap: 1.5rem;
flex-wrap: wrap;
padding: 0;
margin: 0;
li {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
padding: 0.5rem;
gap: 0.3rem;
button, a {
white-space: nowrap;
}
}
span {
font-size: 0.8rem;
color: var(--color-text-input-description);
}
}
#cell-diff-jump-visualiser {
display: flex;
flex-direction: row;
gap: 1px;
background: var(--color-background);
border-radius: 3px;
overflow-x: hidden;
position: sticky;
top: calc(var(--app-header-height, 0px) + var(--diff-header-height, 0px));
z-index: 10;
padding-top: 1rem;
padding-bottom: 1rem;
justify-content: center;
> div {
flex: 1;
min-width: 1px;
max-width: 10px;
height: 10px;
background: var(--color-background-button-cancel);
opacity: 0.3;
border-radius: 1px;
transition: opacity 0.2s;
position: relative;
&.deletion {
background: #b30000; // Red for deletions
opacity: 1;
}
&.insertion {
background: #406619; // Green for insertions
opacity: 1;
}
&.note {
background: #406619; // Orange for changed/notes
opacity: 1;
}
&.mixed {
background: linear-gradient(to right, #b30000 50%, #406619 50%); // Half red, half green
opacity: 1;
}
&.current-position::after {
content: '';
position: absolute;
bottom: -6px;
left: 50%;
transform: translateX(-50%);
width: 0;
height: 0;
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-bottom: 4px solid var(--color-text);
}
&:hover {
opacity: 0.8;
cursor: pointer;
}
}
}
}
#text-diff-heading-area {
.snapshot-age {
padding: 4px;
margin: 0.5rem 0;
background-color: var(--color-background-snapshot-age);
border-radius: 3px;
font-weight: bold;
margin-bottom: 4px;
&.error {
background-color: var(--color-error-background-snapshot-age);
color: var(--color-error-text-snapshot-age);
}
> * {
padding-right: 1rem;
}
}
}
// Placed last on purpose. Several of these rules out-specify nothing - they
// tie with the rule they are overriding, and a tie is decided by source
// order. The minimap's sticky offset is written further down this file as
// `#diff-ui #cell-diff-jump-visualiser`, so a block that sits above it loses
// silently. Keep this one at the bottom.
//
// A phone held sideways is the one place the sticky design cannot pay for
// itself. 50svh of a 390px-tall viewport is 195px, the top menu takes 55 of it,
// and what is left has to hold From, To, Filters and the tab row - three of
// those on their own lines, because iOS gives the two datetime selects enough
// width that From and To cannot share one. #settings shrinking and scrolling
// (see above) keeps the tabs inside the bar, but a bar you have to scroll
// *inside* to reach half its controls is still a bar in the way, and no amount
// of trimming decoration finds 195px.
//
// So stop compensating for the budget here and give up the budget instead.
// Below 500px of viewport height both bars are ordinary static blocks: no
// height cap, no inner scroller, title and every control present at once, and
// the page scrolls as one document. The trade is the sticky guarantee itself -
// the controls and the tab row scroll away with the diff, so switching tabs
// means scrolling back up - which is the right side to be wrong on when the
// alternative is controls you cannot see. Portrait is untouched.
//
// Keyed on viewport height rather than orientation: a short desktop window has
// the identical problem for the identical reason.
@media (max-height: 500px) {
body.difference-page .header {
// Not a new state: parts/_top_menu.scss ships .header with its sticky
// commented out, so this is the position every other page's top menu is
// already in. z-index goes inert along with it, which hands the mobile
// drawer's 10000 back to the root stacking context.
position: static;
// Nothing scrolls underneath a static bar, so it neither needs its
// pre-composited copy of the page gradient nor may keep it: the copy is
// aligned by pushing it up by the bar's own *constant* viewport offset, and
// a bar that scrolls has no constant offset. It would slide out of phase
// with body::after's fixed layer and read as a moving seam. The shorthand,
// not background-image - it resets size, origin and position too.
background: none;
}
#diff-header {
position: static;
// The cap and the scroller are what the sticky design pays for its
// guarantee that the diff keeps half the viewport. There is no longer a
// guarantee to pay for.
max-height: none;
overflow: visible;
background: none;
#settings {
// Same: it only shrank and scrolled to keep the tab row inside the cap.
min-height: auto;
overflow-y: visible;
}
}
// .tab-pane-inner's scroll-margin-top is deliberately NOT overridden here,
// and dropping it to a bare 1rem is a trap worth naming. It reads as an
// allowance for pinned chrome, so with nothing pinned it looks like dead
// weight - but upstream's tabs.js rewrites an empty hash to the first tab's
// (tabs.js:15) and the browser then jumps to that pane on load, before any
// of this is a choice. At 1rem that jump lands with the whole static bar
// already scrolled off the top: measured -175.8px at 844x390, a landscape
// page that opens with no controls on it at all. The existing formula is the
// right number in both modes for two different reasons - in the sticky one it
// clears chrome that is pinned over the pane, in the static one it keeps
// chrome that sits above the pane on screen. Leave it alone.
// The minimap is sticky inside the diff card, offset to sit directly below
// the sticky stack. With that stack gone its offset would pin it a top menu
// plus a whole toolbar down the viewport - past halfway on a 390px-tall one,
// a strip floating in mid-screen beneath nothing. It is still worth pinning;
// pin it to the viewport, which is now free.
#diff-ui #cell-diff-jump-visualiser {
top: 0;
}
}