diff --git a/assets/css/search.css b/assets/css/search.css index bb1f447..d1c4774 100644 --- a/assets/css/search.css +++ b/assets/css/search.css @@ -82,10 +82,10 @@ box-sizing: border-box; } -html { - overflow-x: hidden; - width: 100%; -} +/* No overflow-x clip here on purpose. Hiding horizontal overflow on the root + does not remove it, it just makes whatever sits past the edge unreachable -- + a worse 1.4.10 outcome than a scrollbar. The widths below are sized so that + nothing overflows down to 320px, so there is nothing to clip. */ a { color: inherit; @@ -123,6 +123,10 @@ header h1 { font-size: 2.5rem; margin-bottom: 10px; text-shadow: 2px 2px 4px rgba(0,0,0,0.2); + /* "Learn@MDL" is one unbreakable token, and at 2.5rem it is wider than a + 320px viewport once text is resized to 200%. Breaking it is the lesser + evil: capping the size instead would be the thing 1.4.4 forbids. */ + overflow-wrap: break-word; } header p { @@ -130,13 +134,23 @@ header p { opacity: 0.9; } +/* Capped by height on roomy screens, but free to scale down by width once the + viewport is narrower than the lockup. Fixed heights made the UTL logo 313px + wide, which is wider than the 320px reflow target on its own (1.4.10). */ +.logo-mdl, +.logo-utl { + width: auto; + height: auto; + max-width: 100%; +} + .logo-mdl { - height: 100px; + max-height: 100px; margin-bottom: 15px; } .logo-utl { - height: 70px; + max-height: 70px; margin-bottom: 30px; } @@ -151,6 +165,7 @@ header p.header-meta { align-items: center; justify-content: center; gap: 16px; + flex-wrap: wrap; } .header-links a { @@ -196,10 +211,27 @@ header p.header-meta { margin-bottom: 20px; } +/* A control row at every width: the search field takes the slack and the two + buttons keep their intrinsic size. Wraps rather than overflows, so the row + still fits a 320px viewport (1.4.10) and still fits once the labels grow at + 200% text (1.4.4). Bottom-aligned so the buttons line up with the input + rather than with the label above it. */ .sticky-search-container { max-width: 600px; margin: 0 auto; width: 100%; + display: flex; + flex-wrap: wrap; + align-items: flex-end; + gap: 10px; +} + +/* Small basis on purpose: the row has to stay on one line down to 320px, or + the pinned bar doubles in height and swallows a 256px-tall viewport. The + field gives up width to the buttons rather than pushing them to a new row. */ +.search-box { + flex: 1 1 120px; + min-width: 0; } .main-content { @@ -475,6 +507,9 @@ header p.header-meta { flex-wrap: wrap; } +/* Each label-and-select pair wraps internally before the row does, so at 320px + or at 200% text the label stacks over its control instead of pushing the + results panel wider than the viewport (1.4.10, 1.4.4) */ .per-page-selector, .sort-options { display: flex; @@ -482,6 +517,8 @@ header p.header-meta { gap: 8px; font-size: 14px; color: var(--text-muted); + flex-wrap: wrap; + min-width: 0; } .per-page-selector select, @@ -493,29 +530,34 @@ header p.header-meta { color: var(--text-body); cursor: pointer; background: var(--surface); + max-width: 100%; } +/* Caps the column count without forcing it. Each track's floor is whichever + is larger: 240px, or the width one of --max-cards-per-row columns would get. + So the grid never exceeds that many columns, and drops to fewer once the + panel is too narrow to give each card 240px -- which is what happens at high + zoom. The previous rule pinned the count from 481px up, which at 200% zoom + left three ~170px columns breaking titles one word per line (1.4.10). */ .guides-grid { + --card-gap: 20px; display: grid; - grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr)); + grid-template-columns: repeat( + auto-fit, + minmax( + max( + 240px, + (100% - (var(--max-cards-per-row) - 1) * var(--card-gap)) + / var(--max-cards-per-row) + ), + 1fr + ) + ); grid-auto-columns: 1fr; - gap: 20px; + gap: var(--card-gap); margin-bottom: 30px; } -/* Limit maximum number of columns */ -@media (min-width: 481px) { - .guides-grid { - grid-template-columns: repeat(var(--max-cards-per-row), 1fr); - } -} - -@media (max-width: 480px) { - .guides-grid { - grid-template-columns: 1fr; - } -} - .guides-card { border-radius: 10px; padding: 20px; @@ -809,6 +851,14 @@ header p.header-meta { margin-bottom: 10px; } +/* Floats bottom-right, which means it can sit on top of content. Two rules + keep that from hiding anything: --back-to-top-clearance reserves room below + the page and past every focus target, and the button is dropped entirely at + high zoom, where it used to land on the sort control (1.4.10, 2.4.11). */ +:root { + --back-to-top-clearance: 100px; /* 40px offset + 50px button + 10px */ +} + #back-to-top { position: fixed; bottom: 40px; @@ -841,6 +891,11 @@ header p.header-meta { background-color: var(--surface-subtle); } +/* The last card can scroll clear of the button */ +body { + padding-bottom: max(20px, var(--back-to-top-clearance)); +} + .visually-hidden { position: absolute; width: 1px; @@ -877,6 +932,8 @@ header p.header-meta { #results, .results-section :is(a, button, input, select, [tabindex]) { scroll-margin-top: calc(var(--sticky-search-h) + 10px); + /* ...and the floating Back to top below it */ + scroll-margin-bottom: var(--back-to-top-clearance); } .section-heading:focus, @@ -895,19 +952,6 @@ header p.header-meta { grid-template-columns: 1fr; } - /* Search field and Filters share one row, bottom-aligned so the button - lines up with the input rather than with the label above it */ - .sticky-search-container { - display: flex; - align-items: flex-end; - gap: 10px; - } - - .search-box { - flex: 1; - min-width: 0; - } - .filter-toggle { display: inline-flex; align-items: center; @@ -1004,7 +1048,7 @@ header p.header-meta { @media (max-width: 768px) { body { - padding: 8px; + padding: 8px 8px max(8px, var(--back-to-top-clearance)); } .container { @@ -1012,18 +1056,28 @@ header p.header-meta { } .guides-grid { - gap: 15px; + --card-gap: 15px; } .results-controls { width: 100%; justify-content: space-between; + /* Below the width where the two groups fit side by side they stack, + rather than the row forcing a horizontal scrollbar */ + gap: 10px 15px; } .pagination { flex-wrap: wrap; } + /* Trimmed to match the shorter input at this size, still past the 44px + target floor */ + .filter-toggle { + height: 48px; + min-height: 48px; + } + #back-to-top { bottom: 20px; right: 20px; @@ -1036,6 +1090,10 @@ header p.header-meta { height: 20px; } + :root { + --back-to-top-clearance: 75px; /* 20px + 45px + 10px */ + } + header h1 { font-size: 2rem; } @@ -1068,6 +1126,21 @@ header p.header-meta { padding: 10px 0; } } +/* 480px wide is 400% zoom on a 1280px screen (or a phone); 500px tall is 200% + on a 720px-tall one. There the button would cover controls, so it goes. Home + still jumps to the top, and the search bar is pinned there anyway. Sits + after the 768px block so its zero clearance wins over the 75px there. */ +@media (max-width: 480px), (max-height: 500px) { + #back-to-top, + #back-to-top.show { + display: none; + } + + :root { + --back-to-top-clearance: 0px; + } +} + @media (prefers-reduced-motion: reduce) { *, *::before, *::after { transition-property: opacity, visibility, background-color, color, border-color, box-shadow !important;